mirror of
https://github.com/Litlyx/litlyx
synced 2025-12-09 23:48:36 +01:00
refactoring
This commit is contained in:
@@ -135,7 +135,7 @@ const chartData = ref<ChartData<'line' | 'bar' | 'bubble'>>({
|
|||||||
type: 'bubble',
|
type: 'bubble',
|
||||||
stack: 'combined',
|
stack: 'combined',
|
||||||
borderColor: ["#fbbf24"]
|
borderColor: ["#fbbf24"]
|
||||||
},
|
}
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -367,6 +367,11 @@ const legendClasses = ref<string[]>([
|
|||||||
{{ (currentTooltipData as any)[tooltipNameIndex[index]] }}
|
{{ (currentTooltipData as any)[tooltipNameIndex[index]] }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="mt-3 font-normal flex flex-col text-[.9rem] dark:text-lyx-text-dark text-lyx-lightmode-text-dark"
|
||||||
|
v-if="(currentTooltipData as any).sessions > (currentTooltipData as any).visits">
|
||||||
|
<div> Unique visitors is greater than visits. </div>
|
||||||
|
<div> This can indicate bot traffic. </div>
|
||||||
|
</div>
|
||||||
<!-- <div class="bg-lyx-background-lighter h-[2px] w-full my-2"> </div> -->
|
<!-- <div class="bg-lyx-background-lighter h-[2px] w-full my-2"> </div> -->
|
||||||
</LyxUiCard>
|
</LyxUiCard>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
38
dashboard/components/dialog/ConfirmLogout.vue
Normal file
38
dashboard/components/dialog/ConfirmLogout.vue
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
|
||||||
|
const emit = defineEmits(['success', 'cancel'])
|
||||||
|
|
||||||
|
</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">
|
||||||
|
Are you sure to logout ?
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
<div class="flex justify-end gap-2">
|
||||||
|
<LyxUiButton type="secondary" @click="emit('cancel')">
|
||||||
|
Cancel
|
||||||
|
</LyxUiButton>
|
||||||
|
<LyxUiButton @click="emit('success')" type="danger">
|
||||||
|
Confirm
|
||||||
|
</LyxUiButton>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</UModal>
|
||||||
|
|
||||||
|
</template>
|
||||||
@@ -107,7 +107,7 @@ async function confirmSnapshot() {
|
|||||||
Cancel
|
Cancel
|
||||||
</LyxUiButton>
|
</LyxUiButton>
|
||||||
<LyxUiButton @click="confirmSnapshot()" type="primary" class="w-full text-center"
|
<LyxUiButton @click="confirmSnapshot()" type="primary" class="w-full text-center"
|
||||||
:disabled="snapshotName.length == 0">
|
:disabled="snapshotName.trim().length == 0">
|
||||||
Confirm
|
Confirm
|
||||||
</LyxUiButton>
|
</LyxUiButton>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -13,12 +13,42 @@ const props = defineProps<{
|
|||||||
}[]
|
}[]
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
function acceptInvite(project_id: string) {
|
async function acceptInvite(project_id: string) {
|
||||||
|
try {
|
||||||
|
await $fetch('/api/project/members/accept', {
|
||||||
|
method: 'POST',
|
||||||
|
body: JSON.stringify({ project_id }),
|
||||||
|
headers: useComputedHeaders({
|
||||||
|
custom: {
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
}
|
||||||
|
}).value
|
||||||
|
});
|
||||||
|
emit('success');
|
||||||
|
} catch (ex) {
|
||||||
|
console.error(ex);
|
||||||
|
alert('Error accepting invite');
|
||||||
|
emit('cancel');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function declineInvite(project_id: string) {
|
async function declineInvite(project_id: string) {
|
||||||
|
try {
|
||||||
|
await $fetch('/api/project/members/decline', {
|
||||||
|
method: 'POST',
|
||||||
|
body: JSON.stringify({ project_id }),
|
||||||
|
headers: useComputedHeaders({
|
||||||
|
custom: {
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
}
|
||||||
|
}).value
|
||||||
|
});
|
||||||
|
emit('success');
|
||||||
|
} catch (ex) {
|
||||||
|
console.error(ex);
|
||||||
|
alert('Error accepting invite');
|
||||||
|
emit('cancel');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
@@ -32,9 +62,9 @@ function declineInvite(project_id: string) {
|
|||||||
background: 'dark:bg-lyx-widget bg-lyx-lightmode-widget-light',
|
background: 'dark:bg-lyx-widget bg-lyx-lightmode-widget-light',
|
||||||
ring: 'border-solid border-[1px] border-[#262626]'
|
ring: 'border-solid border-[1px] border-[#262626]'
|
||||||
}">
|
}">
|
||||||
<div class="h-full flex flex-col gap-8 p-4">
|
<div class="h-full flex flex-col gap-8 p-6">
|
||||||
|
|
||||||
<div class="flex flex-col gap-2" v-for="invite of [...invites, ...invites, ...invites]">
|
<div class="flex flex-col gap-2" v-for="invite of invites">
|
||||||
|
|
||||||
<div class="dark:text-lyx-text text-lyx-lightmode-text">
|
<div class="dark:text-lyx-text text-lyx-lightmode-text">
|
||||||
You are invited to join
|
You are invited to join
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
|
|
||||||
import { DialogInviteManager } from '#components';
|
import { DialogConfirmLogout, DialogInviteManager } from '#components';
|
||||||
import CreateSnapshot from '../dialog/CreateSnapshot.vue';
|
import CreateSnapshot from '../dialog/CreateSnapshot.vue';
|
||||||
|
|
||||||
export type Entry = {
|
export type Entry = {
|
||||||
@@ -94,12 +94,25 @@ async function generatePDF() {
|
|||||||
|
|
||||||
const { setToken } = useAccessToken();
|
const { setToken } = useAccessToken();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
const { actions } = useProject();
|
||||||
|
|
||||||
|
|
||||||
|
const modal = useModal();
|
||||||
|
|
||||||
|
|
||||||
function onLogout() {
|
function onLogout() {
|
||||||
console.log('LOGOUT')
|
modal.open(DialogConfirmLogout, {
|
||||||
setToken('');
|
onSuccess() {
|
||||||
setLoggedUser(undefined);
|
modal.close();
|
||||||
router.push('/login');
|
console.log('LOGOUT');
|
||||||
|
setToken('');
|
||||||
|
setLoggedUser(undefined);
|
||||||
|
router.push('/login');
|
||||||
|
},
|
||||||
|
onCancel() {
|
||||||
|
modal.close();
|
||||||
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const { data: maxProjects } = useFetch("/api/user/max_projects", {
|
const { data: maxProjects } = useFetch("/api/user/max_projects", {
|
||||||
@@ -111,8 +124,6 @@ const { data: maxProjects } = useFetch("/api/user/max_projects", {
|
|||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
const modal = useModal();
|
|
||||||
|
|
||||||
function openPendingInvites() {
|
function openPendingInvites() {
|
||||||
if (!pendingInvites.value) return;
|
if (!pendingInvites.value) return;
|
||||||
if (pendingInvites.value.length == 0) return;
|
if (pendingInvites.value.length == 0) return;
|
||||||
@@ -120,14 +131,16 @@ function openPendingInvites() {
|
|||||||
console.log(pendingInvites);
|
console.log(pendingInvites);
|
||||||
modal.open(DialogInviteManager, {
|
modal.open(DialogInviteManager, {
|
||||||
invites: pendingInvites.value.map(e => {
|
invites: pendingInvites.value.map(e => {
|
||||||
return { project_id: e._id, project_name: e.project_name }
|
return { project_id: e.project_id, project_name: e.project_name }
|
||||||
}),
|
}),
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
modal.close();
|
modal.close();
|
||||||
|
actions.refreshProjectsList();
|
||||||
refreshInvites();
|
refreshInvites();
|
||||||
},
|
},
|
||||||
onCancel: () => {
|
onCancel: () => {
|
||||||
modal.close();
|
modal.close();
|
||||||
|
actions.refreshProjectsList();
|
||||||
refreshInvites();
|
refreshInvites();
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -4,6 +4,8 @@ import type { SettingsTemplateEntry } from './Template.vue';
|
|||||||
|
|
||||||
const { project, actions, projectList, isGuest, projectId } = useProject();
|
const { project, actions, projectList, isGuest, projectId } = useProject();
|
||||||
|
|
||||||
|
const { createErrorAlert, createAlert } = useAlert();
|
||||||
|
|
||||||
const entries: SettingsTemplateEntry[] = [
|
const entries: SettingsTemplateEntry[] = [
|
||||||
{ id: 'pname', title: 'Name', text: 'Project name' },
|
{ id: 'pname', title: 'Name', text: 'Project name' },
|
||||||
{ id: 'api', title: 'ApiKeys', text: 'Manage your authorization token' },
|
{ id: 'api', title: 'ApiKeys', text: 'Manage your authorization token' },
|
||||||
@@ -37,7 +39,7 @@ async function createApiKey() {
|
|||||||
apiKeys.value.push(res);
|
apiKeys.value.push(res);
|
||||||
newApiKeyName.value = '';
|
newApiKeyName.value = '';
|
||||||
} catch (ex: any) {
|
} catch (ex: any) {
|
||||||
alert(ex.message);
|
createErrorAlert('Error', ex.message, 10000);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -53,7 +55,7 @@ async function deleteApiKey(api_id: string) {
|
|||||||
newApiKeyName.value = '';
|
newApiKeyName.value = '';
|
||||||
await updateApiKeys();
|
await updateApiKeys();
|
||||||
} catch (ex: any) {
|
} catch (ex: any) {
|
||||||
alert(ex.message);
|
createErrorAlert('Error', ex.message, 10000);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -116,14 +118,12 @@ async function deleteProject() {
|
|||||||
|
|
||||||
|
|
||||||
} catch (ex: any) {
|
} catch (ex: any) {
|
||||||
alert(ex.message);
|
createErrorAlert('Error', ex.message);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const { createAlert } = useAlert()
|
|
||||||
|
|
||||||
function copyScript() {
|
function copyScript() {
|
||||||
if (!navigator.clipboard) alert('You can\'t copy in HTTP');
|
if (!navigator.clipboard) alert('You can\'t copy in HTTP');
|
||||||
|
|
||||||
@@ -172,7 +172,7 @@ function copyProjectId() {
|
|||||||
<LyxUiInput class="grow px-4 py-2" :disabled="isGuest" placeholder="ApiKeyName"
|
<LyxUiInput class="grow px-4 py-2" :disabled="isGuest" placeholder="ApiKeyName"
|
||||||
v-model="newApiKeyName">
|
v-model="newApiKeyName">
|
||||||
</LyxUiInput>
|
</LyxUiInput>
|
||||||
<LyxUiButton v-if="!isGuest" @click="createApiKey()" :disabled="newApiKeyName.length < 3"
|
<LyxUiButton v-if="!isGuest" @click="createApiKey()" :disabled="newApiKeyName.length.trim() < 3"
|
||||||
type="primary">
|
type="primary">
|
||||||
<i class="far fa-plus"></i>
|
<i class="far fa-plus"></i>
|
||||||
</LyxUiButton>
|
</LyxUiButton>
|
||||||
|
|||||||
@@ -116,7 +116,7 @@ const { showDrawer } = useDrawer();
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="relative">
|
<div class="relative pb-[6rem]">
|
||||||
|
|
||||||
<div v-if="invoicesPending || planPending"
|
<div v-if="invoicesPending || planPending"
|
||||||
class="backdrop-blur-[1px] z-[20] mt-20 w-full h-full flex items-center justify-center font-bold">
|
class="backdrop-blur-[1px] z-[20] mt-20 w-full h-full flex items-center justify-center font-bold">
|
||||||
|
|||||||
@@ -71,7 +71,6 @@ export function getDefaultSnapshots(project_id: TProjectSnapshot['project_id'],
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
const allTime: DefaultSnapshot = {
|
const allTime: DefaultSnapshot = {
|
||||||
project_id,
|
project_id,
|
||||||
_id: '___allTime' as any,
|
_id: '___allTime' as any,
|
||||||
@@ -83,8 +82,42 @@ export function getDefaultSnapshots(project_id: TProjectSnapshot['project_id'],
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
const last30Days: DefaultSnapshot = {
|
||||||
|
project_id,
|
||||||
|
_id: '___last30days' as any,
|
||||||
|
name: 'Last 30 days',
|
||||||
|
from: fns.startOfDay(fns.subDays(Date.now(), 30)),
|
||||||
|
to: fns.endOfDay(fns.subDays(Date.now(), 0)),
|
||||||
|
color: '#BC5090',
|
||||||
|
default: true
|
||||||
|
}
|
||||||
|
|
||||||
const snapshotList = [lastDay, today, lastMonth, currentMonth, lastWeek, currentWeek, allTime]
|
const last60Days: DefaultSnapshot = {
|
||||||
|
project_id,
|
||||||
|
_id: '___last60days' as any,
|
||||||
|
name: 'Last 60 days',
|
||||||
|
from: fns.startOfDay(fns.subDays(Date.now(), 60)),
|
||||||
|
to: fns.endOfDay(fns.subDays(Date.now(), 0)),
|
||||||
|
color: '#BC5090',
|
||||||
|
default: true
|
||||||
|
}
|
||||||
|
|
||||||
|
const last90Days: DefaultSnapshot = {
|
||||||
|
project_id,
|
||||||
|
_id: '___last90days' as any,
|
||||||
|
name: 'Last 90 days',
|
||||||
|
from: fns.startOfDay(fns.subDays(Date.now(), 90)),
|
||||||
|
to: fns.endOfDay(fns.subDays(Date.now(), 0)),
|
||||||
|
color: '#BC5090',
|
||||||
|
default: true
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
const snapshotList = [
|
||||||
|
lastDay, today, lastMonth, currentMonth,
|
||||||
|
lastWeek, currentWeek, allTime,
|
||||||
|
last30Days, last60Days, last90Days
|
||||||
|
]
|
||||||
|
|
||||||
return snapshotList;
|
return snapshotList;
|
||||||
|
|
||||||
|
|||||||
@@ -34,10 +34,19 @@ function createAlert(title: string, text: string, icon: string, ms: number) {
|
|||||||
}, 250)
|
}, 250)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function createSuccessAlert(title: string, text: string, ms?: number) {
|
||||||
|
return createAlert(title, text, 'far fa-circle-check', ms ?? 5000);
|
||||||
|
}
|
||||||
|
|
||||||
|
function createErrorAlert(title: string, text: string, ms?: number) {
|
||||||
|
return createAlert(title, text, 'far fa-triangle-exclamation', ms ?? 5000);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
function closeAlert(id: number) {
|
function closeAlert(id: number) {
|
||||||
alerts.value = alerts.value.filter(e => e.id != id);
|
alerts.value = alerts.value.filter(e => e.id != id);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useAlert() {
|
export function useAlert() {
|
||||||
return { alerts, createAlert, closeAlert }
|
return { alerts, createAlert, closeAlert, createSuccessAlert, createErrorAlert }
|
||||||
}
|
}
|
||||||
@@ -33,7 +33,10 @@ const guestProjectList = computed(() => {
|
|||||||
return guestProjectsRequest.data.value;
|
return guestProjectsRequest.data.value;
|
||||||
})
|
})
|
||||||
|
|
||||||
const refreshProjectsList = () => projectsRequest.refresh();
|
const refreshProjectsList = () => {
|
||||||
|
projectsRequest.refresh();
|
||||||
|
guestProjectsRequest.refresh();
|
||||||
|
}
|
||||||
|
|
||||||
const activeProjectId = ref<string | undefined>();
|
const activeProjectId = ref<string | undefined>();
|
||||||
|
|
||||||
|
|||||||
@@ -19,6 +19,8 @@ const router = useRouter();
|
|||||||
|
|
||||||
const { token, setToken } = useAccessToken();
|
const { token, setToken } = useAccessToken();
|
||||||
|
|
||||||
|
const { createErrorAlert } = useAlert();
|
||||||
|
|
||||||
async function handleOnSuccess(response: any) {
|
async function handleOnSuccess(response: any) {
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -97,7 +99,7 @@ function goBackToEmailLogin() {
|
|||||||
|
|
||||||
async function signInSelfhosted() {
|
async function signInSelfhosted() {
|
||||||
try {
|
try {
|
||||||
const result = await $fetch(`/api/auth/no_auth`, {
|
const result: any = await $fetch(`/api/auth/no_auth`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
body: JSON.stringify({ email: email.value, password: password.value })
|
body: JSON.stringify({ email: email.value, password: password.value })
|
||||||
@@ -124,7 +126,7 @@ async function signInSelfhosted() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
} catch (ex: any) {
|
} catch (ex: any) {
|
||||||
alert('Error during login.' + ex.message);
|
createErrorAlert('Error', 'Error during login.' + ex.message);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -137,7 +139,7 @@ async function signInWithCredentials() {
|
|||||||
body: JSON.stringify({ email: email.value, password: password.value })
|
body: JSON.stringify({ email: email.value, password: password.value })
|
||||||
})
|
})
|
||||||
|
|
||||||
if (result.error) return alert(result.message);
|
if (result.error) return createErrorAlert('Error', result.message);
|
||||||
|
|
||||||
setToken(result.access_token);
|
setToken(result.access_token);
|
||||||
|
|
||||||
@@ -156,8 +158,8 @@ async function signInWithCredentials() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
} catch (ex) {
|
} catch (ex: any) {
|
||||||
alert('Something went wrong.');
|
createErrorAlert('Error', 'Something went wrong.' + ex.message);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -258,7 +260,7 @@ async function signInWithCredentials() {
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
<div v-if="isNoAuth" @click="loginWithoutAuth"
|
<div v-if="isNoAuth"
|
||||||
class="flex text-[1.3rem] flex-col gap-4 items-center px-8 py-3 relative z-[2]">
|
class="flex text-[1.3rem] flex-col gap-4 items-center px-8 py-3 relative z-[2]">
|
||||||
<div class="flex flex-col gap-4 z-[100] w-[20vw] min-w-[20rem]">
|
<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>
|
<LyxUiInput class="px-3 py-2" placeholder="Email" v-model="email"></LyxUiInput>
|
||||||
|
|||||||
@@ -22,6 +22,8 @@ const showAddMember = ref<boolean>(false);
|
|||||||
|
|
||||||
const addMemberEmail = ref<string>("");
|
const addMemberEmail = ref<string>("");
|
||||||
|
|
||||||
|
const { createErrorAlert } = useAlert();
|
||||||
|
|
||||||
async function kickMember(email: string) {
|
async function kickMember(email: string) {
|
||||||
const sure = confirm('Are you sure to kick ' + email + ' ?');
|
const sure = confirm('Are you sure to kick ' + email + ' ?');
|
||||||
if (!sure) return;
|
if (!sure) return;
|
||||||
@@ -34,7 +36,7 @@ async function kickMember(email: string) {
|
|||||||
}),
|
}),
|
||||||
body: JSON.stringify({ email }),
|
body: JSON.stringify({ email }),
|
||||||
onResponseError({ request, response, options }) {
|
onResponseError({ request, response, options }) {
|
||||||
alert(response.statusText);
|
createErrorAlert('Error', response.statusText);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -58,7 +60,7 @@ async function addMember() {
|
|||||||
}),
|
}),
|
||||||
body: JSON.stringify({ email: addMemberEmail.value }),
|
body: JSON.stringify({ email: addMemberEmail.value }),
|
||||||
onResponseError({ request, response, options }) {
|
onResponseError({ request, response, options }) {
|
||||||
alert(response.statusText);
|
createErrorAlert('Error', response.statusText);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -101,12 +103,25 @@ function permissionToString(permission: TPermission) {
|
|||||||
}
|
}
|
||||||
return result.join('');
|
return result.join('');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function leaveProject() {
|
||||||
|
try {
|
||||||
|
await $fetch('/api/project/members/leave', {
|
||||||
|
headers: useComputedHeaders({}).value
|
||||||
|
});
|
||||||
|
location.reload();
|
||||||
|
} catch (ex: any) {
|
||||||
|
alert(ex.message);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="p-6 pt-10">
|
<div class="p-6 pt-10">
|
||||||
|
|
||||||
<div class="flex flex-col gap-8">
|
<div v-if="!isGuest" class="flex flex-col gap-8">
|
||||||
|
|
||||||
<div class="flex flex-col">
|
<div class="flex flex-col">
|
||||||
<div class="flex gap-4 items-center">
|
<div class="flex gap-4 items-center">
|
||||||
@@ -144,7 +159,9 @@ function permissionToString(permission: TPermission) {
|
|||||||
<div v-if="e.row.role !== 'OWNER' && !isGuest">
|
<div v-if="e.row.role !== 'OWNER' && !isGuest">
|
||||||
<LyxUiButton class="!px-2" type="secondary"
|
<LyxUiButton class="!px-2" type="secondary"
|
||||||
@click="openPermissionManagerDialog(e.row.id.toString())">
|
@click="openPermissionManagerDialog(e.row.id.toString())">
|
||||||
<i class="far fa-gear"></i>
|
<UTooltip text="Manage permissions">
|
||||||
|
<i class="far fa-gear"></i>
|
||||||
|
</UTooltip>
|
||||||
</LyxUiButton>
|
</LyxUiButton>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -181,6 +198,12 @@ function permissionToString(permission: TPermission) {
|
|||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div v-if="isGuest" class="flex flex-col gap-8 mt-[10vh]">
|
||||||
|
<div class="flex flex-col gap-4 items-center">
|
||||||
|
<div class="text-[1.2rem]"> Leave this project </div>
|
||||||
|
<LyxUiButton @click="leaveProject()" type="primary"> Leave </LyxUiButton>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ onMounted(() => {
|
|||||||
|
|
||||||
|
|
||||||
async function createProject() {
|
async function createProject() {
|
||||||
if (projectName.value.length < 2) return;
|
if (projectName.value.trim().length < 2) return;
|
||||||
|
|
||||||
Lit.event('create_project');
|
Lit.event('create_project');
|
||||||
|
|
||||||
@@ -34,7 +34,7 @@ async function createProject() {
|
|||||||
await $fetch('/api/project/create', {
|
await $fetch('/api/project/create', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
...signHeaders({ 'Content-Type': 'application/json' }),
|
...signHeaders({ 'Content-Type': 'application/json' }),
|
||||||
body: JSON.stringify({ name: projectName.value })
|
body: JSON.stringify({ name: projectName.value.trim() })
|
||||||
});
|
});
|
||||||
|
|
||||||
await actions.refreshProjectsList();
|
await actions.refreshProjectsList();
|
||||||
@@ -89,7 +89,7 @@ async function createProject() {
|
|||||||
|
|
||||||
<div>
|
<div>
|
||||||
|
|
||||||
<LyxUiButton type="primary" @click="createProject()" :disabled="projectName.length < 2">
|
<LyxUiButton type="primary" @click="createProject()" :disabled="projectName.trim().length < 2">
|
||||||
Create
|
Create
|
||||||
</LyxUiButton>
|
</LyxUiButton>
|
||||||
|
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ const items = [
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="lg:px-10 lg:py-8 h-dvh overflow-y-auto overflow-x-hidden hide-scrollbars !pb-[10rem]">
|
<div class="lg:px-10 h-full lg:py-8 overflow-hidden hide-scrollbars">
|
||||||
|
|
||||||
<div class="poppins font-semibold text-[1.3rem] lg:px-0 px-4 lg:py-0 py-4"> Settings </div>
|
<div class="poppins font-semibold text-[1.3rem] lg:px-0 px-4 lg:py-0 py-4"> Settings </div>
|
||||||
|
|
||||||
|
|||||||
@@ -13,23 +13,28 @@ export default defineEventHandler(async event => {
|
|||||||
|
|
||||||
const body = await readBody(event);
|
const body = await readBody(event);
|
||||||
|
|
||||||
if (body.name.length == 0) return setResponseStatus(event, 400, 'name is required');
|
const data = await getRequestData(event, [], ['OWNER']);
|
||||||
|
|
||||||
if (body.name.length < 3) return setResponseStatus(event, 400, 'name too short');
|
|
||||||
if (body.name.length > 32) return setResponseStatus(event, 400, 'name too long');
|
|
||||||
|
|
||||||
const data = await getRequestDataOld(event, { allowGuests: false, allowLitlyx: false, });
|
|
||||||
if (!data) return;
|
if (!data) return;
|
||||||
|
|
||||||
|
if (!body.name) return setResponseStatus(event, 400, 'body is required');
|
||||||
|
if (body.name.trim().length == 0) return setResponseStatus(event, 400, 'name is required');
|
||||||
|
if (body.name.trim().length < 3) return setResponseStatus(event, 400, 'name too short');
|
||||||
|
if (body.name.trim().length > 32) return setResponseStatus(event, 400, 'name too long');
|
||||||
|
|
||||||
const { project_id } = data;
|
const { project_id } = data;
|
||||||
|
|
||||||
|
|
||||||
|
const sameName = await ApiSettingsModel.exists({ project_id, apiName: body.name.trim() });
|
||||||
|
if (sameName) return setResponseStatus(event, 400, 'An api key with the same name exists');
|
||||||
|
|
||||||
|
|
||||||
const key = generateApiKey();
|
const key = generateApiKey();
|
||||||
|
|
||||||
const keyNumbers = await ApiSettingsModel.countDocuments({ project_id });
|
const keyNumbers = await ApiSettingsModel.countDocuments({ project_id });
|
||||||
|
|
||||||
if (keyNumbers >= 5) return setResponseStatus(event, 400, 'Api key limit reached');
|
if (keyNumbers >= 5) return setResponseStatus(event, 400, 'Api key limit reached');
|
||||||
|
|
||||||
const newApiSettings = await ApiSettingsModel.create({ project_id, apiKey: key, apiName: body.name, created_at: Date.now(), usage: 0 });
|
const newApiSettings = await ApiSettingsModel.create({ project_id, apiKey: key, apiName: body.name.trim(), created_at: Date.now(), usage: 0 });
|
||||||
|
|
||||||
return newApiSettings.toJSON();
|
return newApiSettings.toJSON();
|
||||||
|
|
||||||
|
|||||||
@@ -1,16 +1,18 @@
|
|||||||
|
|
||||||
export default defineEventHandler(async event => {
|
export default defineEventHandler(async event => {
|
||||||
|
|
||||||
const data = await getRequestDataOld(event, { requireSchema: false, allowGuests: false, allowLitlyx: false });
|
const data = await getRequestData(event, [], ['OWNER']);
|
||||||
if (!data) return;
|
if (!data) return;
|
||||||
|
|
||||||
const { project } = data;
|
const { project } = data;
|
||||||
|
|
||||||
const { name } = await readBody(event);
|
const { name } = await readBody(event);
|
||||||
|
|
||||||
if (name.length == 0) return setResponseStatus(event, 400, 'name is required');
|
if (name.trim()) 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');
|
||||||
|
|
||||||
project.name = name;
|
project.name = name.trim();
|
||||||
await project.save();
|
await project.save();
|
||||||
|
|
||||||
return { ok: true };
|
return { ok: true };
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ export default defineEventHandler(async event => {
|
|||||||
|
|
||||||
const body = await readBody(event);
|
const body = await readBody(event);
|
||||||
|
|
||||||
const newProjectName = body.name;
|
const newProjectName = body.name.trim();
|
||||||
|
|
||||||
if (!newProjectName) return setResponseStatus(event, 400, 'ProjectName too short');
|
if (!newProjectName) return setResponseStatus(event, 400, 'ProjectName too short');
|
||||||
if (newProjectName.length < 2) return setResponseStatus(event, 400, 'ProjectName too short');
|
if (newProjectName.length < 2) return setResponseStatus(event, 400, 'ProjectName too short');
|
||||||
|
|||||||
@@ -7,9 +7,7 @@ export default defineEventHandler(async event => {
|
|||||||
if (!userData?.logged) return [];
|
if (!userData?.logged) return [];
|
||||||
|
|
||||||
|
|
||||||
const members = await TeamMemberModel.find({
|
const members = await TeamMemberModel.find({ user_id: userData.id, pending: false });
|
||||||
user_id: userData.id
|
|
||||||
});
|
|
||||||
|
|
||||||
const projects: TProject[] = [];
|
const projects: TProject[] = [];
|
||||||
|
|
||||||
|
|||||||
@@ -10,6 +10,8 @@ export default defineEventHandler(async event => {
|
|||||||
const { project_id } = body;
|
const { project_id } = body;
|
||||||
if (!project_id) return setResponseStatus(event, 400, 'project_id is required');
|
if (!project_id) return setResponseStatus(event, 400, 'project_id is required');
|
||||||
|
|
||||||
|
console.log({ project_id, user_id: data.user.id });
|
||||||
|
|
||||||
const member = await TeamMemberModel.findOne({ project_id, user_id: data.user.id });
|
const member = await TeamMemberModel.findOne({ project_id, user_id: data.user.id });
|
||||||
if (!member) return setResponseStatus(event, 400, 'member not found');
|
if (!member) return setResponseStatus(event, 400, 'member not found');
|
||||||
|
|
||||||
|
|||||||
@@ -9,12 +9,16 @@ export default defineEventHandler(async event => {
|
|||||||
const data = await getRequestData(event, [], ['OWNER']);
|
const data = await getRequestData(event, [], ['OWNER']);
|
||||||
if (!data) return;
|
if (!data) return;
|
||||||
|
|
||||||
const { project_id, project } = data;
|
const { project_id, project, user } = data;
|
||||||
|
|
||||||
const { email } = await readBody(event);
|
const { email } = await readBody(event);
|
||||||
|
|
||||||
const targetUser = await UserModel.findOne({ email });
|
const targetUser = await UserModel.findOne({ email });
|
||||||
|
|
||||||
|
if (targetUser && targetUser._id.toString() === project.owner.toString()) {
|
||||||
|
return setResponseStatus(event, 400, 'You cannot invite yourself');
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
const link = `http://127.0.0.1:3000/accept_invite?project_id=${project_id.toString()}`;
|
const link = `http://127.0.0.1:3000/accept_invite?project_id=${project_id.toString()}`;
|
||||||
|
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { TeamMemberModel } from "@schema/TeamMemberSchema";
|
|||||||
|
|
||||||
export default defineEventHandler(async event => {
|
export default defineEventHandler(async event => {
|
||||||
|
|
||||||
const data = await getRequestData(event, []);
|
const data = await getRequestData(event, [], []);
|
||||||
if (!data) return;
|
if (!data) return;
|
||||||
|
|
||||||
const { project_id, user } = data;
|
const { project_id, user } = data;
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ export type MemberWithPermissions = {
|
|||||||
|
|
||||||
export default defineEventHandler(async event => {
|
export default defineEventHandler(async event => {
|
||||||
|
|
||||||
const data = await getRequestData(event);
|
const data = await getRequestData(event, [], ['OWNER']);
|
||||||
if (!data) return;
|
if (!data) return;
|
||||||
|
|
||||||
const { project_id, project, user } = data;
|
const { project_id, project, user } = data;
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ export default defineEventHandler(async event => {
|
|||||||
const { name: newSnapshotName, from, to, color: snapshotColor } = body;
|
const { name: newSnapshotName, from, to, color: snapshotColor } = body;
|
||||||
|
|
||||||
if (!newSnapshotName) return setResponseStatus(event, 400, 'SnapshotName too short');
|
if (!newSnapshotName) return setResponseStatus(event, 400, 'SnapshotName too short');
|
||||||
if (newSnapshotName.length == 0) return setResponseStatus(event, 400, 'SnapshotName too short');
|
if (newSnapshotName.trim().length == 0) return setResponseStatus(event, 400, 'SnapshotName too short');
|
||||||
|
|
||||||
if (!from) return setResponseStatus(event, 400, 'from is required');
|
if (!from) return setResponseStatus(event, 400, 'from is required');
|
||||||
if (!to) return setResponseStatus(event, 400, 'to is required');
|
if (!to) return setResponseStatus(event, 400, 'to is required');
|
||||||
@@ -26,7 +26,7 @@ export default defineEventHandler(async event => {
|
|||||||
|
|
||||||
|
|
||||||
const newSnapshot = await ProjectSnapshotModel.create({
|
const newSnapshot = await ProjectSnapshotModel.create({
|
||||||
name: newSnapshotName,
|
name: newSnapshotName.trim(),
|
||||||
from: new Date(from),
|
from: new Date(from),
|
||||||
to: new Date(to),
|
to: new Date(to),
|
||||||
color: snapshotColor,
|
color: snapshotColor,
|
||||||
|
|||||||
@@ -12,17 +12,18 @@ export default defineEventHandler(async event => {
|
|||||||
const cacheKey = `timeline:visits:${pid}:${slice}:${from}:${to}:${domain}`;
|
const cacheKey = `timeline:visits:${pid}:${slice}:${from}:${to}:${domain}`;
|
||||||
const cacheExp = 20;
|
const cacheExp = 20;
|
||||||
|
|
||||||
return await Redis.useCacheV2(cacheKey, cacheExp, async () => {
|
// return await Redis.useCacheV2(cacheKey, cacheExp, async () => {
|
||||||
const timelineData = await executeAdvancedTimelineAggregation({
|
const timelineData = await executeAdvancedTimelineAggregation({
|
||||||
projectId: project_id,
|
projectId: project_id,
|
||||||
model: VisitModel,
|
model: VisitModel,
|
||||||
from, to, slice, timeOffset, domain
|
from, to, slice, timeOffset, domain,
|
||||||
});
|
debug: true
|
||||||
|
|
||||||
return timelineData;
|
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
return timelineData;
|
||||||
|
|
||||||
|
// });
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -80,10 +80,11 @@ export async function executeAdvancedTimelineAggregation<T = {}>(options: Advanc
|
|||||||
range: {
|
range: {
|
||||||
step: 1,
|
step: 1,
|
||||||
unit: granularity,
|
unit: granularity,
|
||||||
bounds: [
|
bounds: 'full'
|
||||||
new Date(new Date(options.from).getTime() - (timeOffset * 1000 * 60)),
|
// [
|
||||||
new Date(new Date(options.to).getTime() - (timeOffset * 1000 * 60) + 1),
|
// new Date(new Date(options.from).getTime() - (timeOffset * 1000 * 60)),
|
||||||
]
|
// new Date(new Date(options.to).getTime() - (timeOffset * 1000 * 60) + 1),
|
||||||
|
// ]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -90,6 +90,8 @@ app.post('/send/welcome', express.json(), async (req, res) => {
|
|||||||
|
|
||||||
app.post('/send/purchase', express.json(), async (req, res) => {
|
app.post('/send/purchase', express.json(), async (req, res) => {
|
||||||
try {
|
try {
|
||||||
|
console.log('PURCHASE EMAIL DISABLED')
|
||||||
|
return;
|
||||||
const { target, projectName } = req.body;
|
const { target, projectName } = req.body;
|
||||||
const ok = await EmailService.sendPurchaseEmail(target, projectName);
|
const ok = await EmailService.sendPurchaseEmail(target, projectName);
|
||||||
res.json({ ok });
|
res.json({ ok });
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { model, Schema, Types } from 'mongoose';
|
import { model, Schema, Types } from 'mongoose';
|
||||||
|
|
||||||
export type TeamMemberRole = 'ADMIN' | 'GUEST';
|
export type TeamMemberRole = 'OWNER' | 'GUEST' | 'MANAGER';
|
||||||
|
|
||||||
|
|
||||||
export type TPermission = {
|
export type TPermission = {
|
||||||
|
|||||||
@@ -53,17 +53,16 @@ class DateService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
canUseSliceFromDays(days: number, slice: Slice): [false, string] | [true, number] {
|
canUseSliceFromDays(days: number, slice: Slice): [false, string] | [true, number] {
|
||||||
|
// HOUR - 3 DAYS - 72 SAMPLES
|
||||||
// 3 Days
|
|
||||||
if (slice === 'hour' && (days > 3)) return [false, 'Date gap too big for this slice'];
|
if (slice === 'hour' && (days > 3)) return [false, 'Date gap too big for this slice'];
|
||||||
// 3 Weeks
|
// DAY - 2 MONTHS - 62 SAMPLES
|
||||||
if (slice === 'day' && (days > 31)) return [false, 'Date gap too big for this slice'];
|
if (slice === 'day' && (days > 31 * 2)) return [false, 'Date gap too big for this slice'];
|
||||||
// 3 Years
|
// MONTH - 4 YEARS - 60 SAMPLES
|
||||||
if (slice === 'month' && (days > 365 * 3)) return [false, 'Date gap too big for this slice'];
|
if (slice === 'month' && (days > 365 * 4)) return [false, 'Date gap too big for this slice'];
|
||||||
|
|
||||||
// 2 days
|
// DAY - 2 DAYS - 2 SAMPLES
|
||||||
if (slice === 'day' && (days < 2)) return [false, 'Date gap too small for this slice'];
|
if (slice === 'day' && (days < 2)) return [false, 'Date gap too small for this slice'];
|
||||||
// 2 month
|
// MONTH - 2 MONTHS - 2 SAMPLES
|
||||||
if (slice === 'month' && (days < 31 * 2)) return [false, 'Date gap too small for this slice'];
|
if (slice === 'month' && (days < 31 * 2)) return [false, 'Date gap too small for this slice'];
|
||||||
|
|
||||||
return [true, days]
|
return [true, days]
|
||||||
|
|||||||
Reference in New Issue
Block a user