mirror of
https://github.com/Litlyx/litlyx
synced 2025-12-10 07:48:37 +01:00
new selfhosted version
This commit is contained in:
30
dashboard/components/dialog/CancelPlan.vue
Normal file
30
dashboard/components/dialog/CancelPlan.vue
Normal file
@@ -0,0 +1,30 @@
|
||||
<script lang="ts" setup>
|
||||
import { LoaderCircle } from 'lucide-vue-next';
|
||||
|
||||
const emits = defineEmits<{ (event: 'confirm'): void }>();
|
||||
const { close } = useDialog();
|
||||
|
||||
const loading = ref<boolean>(false);
|
||||
|
||||
function onConfirm() {
|
||||
loading.value = true;
|
||||
emits('confirm')
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div v-if="!loading" class="flex flex-col gap-4">
|
||||
<div>
|
||||
Are you sure to cancel your current plan?
|
||||
</div>
|
||||
<div class="flex justify-end gap-2">
|
||||
<Button variant="secondary" @click="close()"> Back </Button>
|
||||
<Button variant="destructive" @click="onConfirm()"> Cancel </Button>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="loading" class="flex items-center justify-center my-4">
|
||||
<LoaderCircle class="size-10 animate-[spin_1s_ease-in-out_infinite] duration-500">
|
||||
</LoaderCircle>
|
||||
</div>
|
||||
</template>
|
||||
@@ -1,38 +0,0 @@
|
||||
<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>
|
||||
@@ -1,30 +1,23 @@
|
||||
<script lang="ts" setup>
|
||||
|
||||
const { closeDialog } = useCustomDialog();
|
||||
import type { DateRange } from 'reka-ui'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover'
|
||||
import { RangeCalendar } from '@/components/ui/range-calendar'
|
||||
import { CalendarIcon } from 'lucide-vue-next'
|
||||
import { CalendarDate, DateFormatter, getLocalTimeZone } from '@internationalized/date'
|
||||
|
||||
import { sub, format, isSameDay, type Duration, startOfDay, endOfDay } from 'date-fns'
|
||||
|
||||
const ranges = [
|
||||
{ label: 'Last 7 days', duration: { days: 7 } },
|
||||
{ label: 'Last 14 days', duration: { days: 14 } },
|
||||
{ label: 'Last 30 days', duration: { days: 30 } },
|
||||
{ label: 'Last 3 months', duration: { months: 3 } },
|
||||
{ label: 'Last 6 months', duration: { months: 6 } },
|
||||
{ label: 'Last year', duration: { years: 1 } }
|
||||
]
|
||||
const selected = ref<{ start: Date, end: Date }>({ start: sub(new Date(), { days: 14 }), end: new Date() })
|
||||
|
||||
function isRangeSelected(duration: Duration) {
|
||||
return isSameDay(selected.value.start, sub(new Date(), duration)) && isSameDay(selected.value.end, new Date())
|
||||
}
|
||||
|
||||
function selectRange(duration: Duration) {
|
||||
selected.value = { start: sub(new Date(), duration), end: new Date() }
|
||||
}
|
||||
const emits = defineEmits<{
|
||||
(event: 'confirm', data: { name: string, color: string, from: string, to: string }): void
|
||||
}>();
|
||||
const { close } = useDialog();
|
||||
|
||||
const currentColor = ref<string>("#5680F8");
|
||||
|
||||
const colorpicker = ref<HTMLInputElement | null>(null);
|
||||
const colorpicker = useTemplateRef<HTMLInputElement>('colorpicker');
|
||||
|
||||
const snapshotName = ref<string>("");
|
||||
|
||||
function showColorPicker() {
|
||||
colorpicker.value?.click();
|
||||
@@ -34,83 +27,64 @@ function onColorChange() {
|
||||
currentColor.value = colorpicker.value?.value || '#000000';
|
||||
}
|
||||
|
||||
const snapshotName = ref<string>("");
|
||||
const value = ref<DateRange>({
|
||||
start: new CalendarDate(new Date().getFullYear(), new Date().getUTCMonth(), 1),
|
||||
end: new CalendarDate(new Date().getFullYear(), new Date().getUTCMonth(), new Date().getDate())
|
||||
}) as Ref<DateRange>;
|
||||
|
||||
const { updateSnapshots, snapshot, snapshots } = useSnapshot();
|
||||
const { createAlert } = useAlert()
|
||||
const canCreate = computed(() => {
|
||||
return snapshotName.value.trim().length > 2 && snapshotName.value.trim().length < 22 && value.value.start && value.value.end
|
||||
})
|
||||
|
||||
async function confirmSnapshot() {
|
||||
await $fetch("/api/snapshot/create", {
|
||||
method: 'POST',
|
||||
headers: useComputedHeaders({ useSnapshotDates: false }).value,
|
||||
body: JSON.stringify({
|
||||
name: snapshotName.value,
|
||||
color: currentColor.value,
|
||||
from: startOfDay(selected.value.start),
|
||||
to: endOfDay(selected.value.end)
|
||||
})
|
||||
});
|
||||
const df = new DateFormatter('en-US', { dateStyle: 'medium' })
|
||||
|
||||
await updateSnapshots();
|
||||
closeDialog();
|
||||
createAlert('Timeframe created', 'Timeframe created successfully', 'far fa-circle-check', 5000);
|
||||
const newSnapshot = snapshots.value.at(-1);
|
||||
if (newSnapshot) snapshot.value = newSnapshot;
|
||||
|
||||
}
|
||||
const popoverOpen = ref<boolean>(false);
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="w-full h-full flex flex-col">
|
||||
<div class="flex flex-col gap-4">
|
||||
|
||||
<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">
|
||||
<div :style="`background-color: ${currentColor};`" @click="showColorPicker"
|
||||
class="w-6 h-6 rounded-full aspect-[1/1] relative cursor-pointer">
|
||||
<div class="relative flex items-center gap-2">
|
||||
<div @click="showColorPicker" :style="`background-color:${currentColor};`"
|
||||
class="absolute left-2 shrink-0 size-4 rounded-full">
|
||||
<input @input="onColorChange" ref="colorpicker" class="relative w-0 h-0 z-[-100]" type="color">
|
||||
</div>
|
||||
<div class="grow">
|
||||
<LyxUiInput placeholder="Timeframe name" v-model="snapshotName" class="px-4 py-1 w-full"></LyxUiInput>
|
||||
</div>
|
||||
<Input v-model="snapshotName" class="pl-7" placeholder="Timeframe name"></Input>
|
||||
</div>
|
||||
|
||||
<div class="mt-4 justify-center flex w-full">
|
||||
<UPopover class="w-full" :popper="{ placement: 'bottom' }">
|
||||
<UButton class="w-full" color="primary" variant="solid">
|
||||
<div class="flex items-center justify-center w-full gap-2">
|
||||
<i class="i-heroicons-calendar-days-20-solid"></i>
|
||||
{{ selected.start.toLocaleDateString() }} - {{ selected.end.toLocaleDateString() }}
|
||||
</div>
|
||||
</UButton>
|
||||
<template #panel="{ close }">
|
||||
<div class="flex items-center sm:divide-x divide-gray-200 dark:divide-gray-800">
|
||||
<div class="hidden sm:flex flex-col py-4">
|
||||
<UButton v-for="(range, index) in ranges" :key="index" :label="range.label" color="gray"
|
||||
variant="ghost" class="rounded-none px-6"
|
||||
:class="[isRangeSelected(range.duration) ? 'bg-gray-100 dark:bg-gray-800' : 'hover:bg-gray-50 dark:hover:bg-gray-800/50']"
|
||||
truncate @click="selectRange(range.duration)" />
|
||||
</div>
|
||||
<Popover v-model:open="popoverOpen">
|
||||
<PopoverTrigger as-child>
|
||||
<Button variant="outline">
|
||||
<CalendarIcon class="mr-2 h-4 w-4" />
|
||||
<template v-if="value.start">
|
||||
<template v-if="value.end">
|
||||
{{ df.format(value.start.toDate(getLocalTimeZone())) }} - {{
|
||||
df.format(value.end.toDate(getLocalTimeZone())) }}
|
||||
</template>
|
||||
|
||||
<DatePicker v-model="selected" @close="close" />
|
||||
</div>
|
||||
</template>
|
||||
</UPopover>
|
||||
<template v-else>
|
||||
{{ df.format(value.start.toDate(getLocalTimeZone())) }}
|
||||
</template>
|
||||
</template>
|
||||
<template v-else>
|
||||
Pick a date
|
||||
</template>
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent class="w-auto p-4 flex flex-col items-end relative z-[90]">
|
||||
<RangeCalendar v-model="value" initial-focus :number-of-months="2"
|
||||
@update:start-value="(startDate) => value.start = startDate" />
|
||||
<Button @click="popoverOpen = false;"> Confirm </Button>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
|
||||
<div class="flex justify-end gap-2">
|
||||
<Button variant="secondary" @click="close()"> Back </Button>
|
||||
<Button :disabled="!canCreate"
|
||||
@click="(value.start && value.end) ? emits('confirm', { name: snapshotName, color: currentColor, from: value.start.toString(), to: value.end.toString() }) : null">
|
||||
Create
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div class="grow"></div>
|
||||
<div class="flex items-center justify-around gap-4">
|
||||
<LyxUiButton @click="closeDialog()" type="secondary" class="w-full text-center">
|
||||
Cancel
|
||||
</LyxUiButton>
|
||||
<LyxUiButton @click="confirmSnapshot()" type="primary" class="w-full text-center"
|
||||
:disabled="snapshotName.trim().length == 0">
|
||||
Confirm
|
||||
</LyxUiButton>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</template>
|
||||
20
dashboard/components/dialog/DangerGeneric.vue
Normal file
20
dashboard/components/dialog/DangerGeneric.vue
Normal file
@@ -0,0 +1,20 @@
|
||||
<script lang="ts" setup>
|
||||
|
||||
const emits = defineEmits<{ (event: 'confirm', data?: any): void }>();
|
||||
const props = defineProps<{ data: { label: string, confirm?: string, back?: string, metadata?: any } }>();
|
||||
const { close } = useDialog();
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex flex-col gap-4">
|
||||
<div>
|
||||
{{ data.label }}
|
||||
</div>
|
||||
<div class="flex justify-end gap-2">
|
||||
<Button variant="secondary" @click="close()"> {{ data.back ?? 'Back' }}</Button>
|
||||
<Button variant="destructive" @click="emits('confirm', data.metadata)"> {{ data.confirm ?? 'Delete' }}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
31
dashboard/components/dialog/DeleteAccount.vue
Normal file
31
dashboard/components/dialog/DeleteAccount.vue
Normal file
@@ -0,0 +1,31 @@
|
||||
<script lang="ts" setup>
|
||||
import { LoaderCircle } from 'lucide-vue-next';
|
||||
|
||||
|
||||
const emits = defineEmits<{ (event: 'confirm'): void }>();
|
||||
const { close } = useDialog();
|
||||
|
||||
const loading = ref<boolean>(false);
|
||||
|
||||
function onConfirm() {
|
||||
loading.value = true;
|
||||
emits('confirm')
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div v-if="!loading" class="flex flex-col gap-4">
|
||||
<div>
|
||||
Are you sure to delete your account?
|
||||
</div>
|
||||
<div class="flex justify-end gap-2">
|
||||
<Button variant="secondary" @click="close()"> Back </Button>
|
||||
<Button variant="destructive" @click="onConfirm()"> Delete </Button>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="loading" class="flex items-center justify-center my-4">
|
||||
<LoaderCircle class="size-10 animate-[spin_1s_ease-in-out_infinite] duration-500">
|
||||
</LoaderCircle>
|
||||
</div>
|
||||
</template>
|
||||
@@ -1,84 +1,19 @@
|
||||
<script lang="ts" setup>
|
||||
import type { ButtonType } from '../LyxUi/Button.vue';
|
||||
|
||||
const emit = defineEmits(['success', 'cancel'])
|
||||
|
||||
const props = defineProps<{
|
||||
buttonType: ButtonType,
|
||||
message: string,
|
||||
deleteData: { isAll: boolean, visits: boolean, sessions: boolean, events: boolean, domain: string }
|
||||
}>();
|
||||
|
||||
const isDone = ref<boolean>(false);
|
||||
const canDelete = ref<boolean>(false);
|
||||
|
||||
async function deleteData() {
|
||||
|
||||
try {
|
||||
if (props.deleteData.isAll) {
|
||||
await $fetch('/api/settings/delete_all', {
|
||||
method: 'DELETE',
|
||||
headers: useComputedHeaders({ useSnapshotDates: false }).value,
|
||||
})
|
||||
} else {
|
||||
await $fetch('/api/settings/delete_domain', {
|
||||
method: 'DELETE',
|
||||
headers: useComputedHeaders({ useSnapshotDates: false, custom: { 'Content-Type': 'application/json' } }).value,
|
||||
body: JSON.stringify({
|
||||
domain: props.deleteData.domain,
|
||||
visits: props.deleteData.visits,
|
||||
sessions: props.deleteData.sessions,
|
||||
events: props.deleteData.events,
|
||||
})
|
||||
})
|
||||
}
|
||||
} catch (ex) {
|
||||
alert('Something went wrong');
|
||||
console.error(ex);
|
||||
}
|
||||
|
||||
isDone.value = true;
|
||||
}
|
||||
const emits = defineEmits<{ (event: 'confirm'): void }>();
|
||||
const props = defineProps<{ data: { domain: string } }>();
|
||||
const { close } = useDialog();
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<UModal :ui="{
|
||||
strategy: 'override',
|
||||
overlay: {
|
||||
background: 'bg-lyx-background/85'
|
||||
},
|
||||
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">
|
||||
|
||||
<div class="font-semibold text-[1.2rem]"> {{ isDone ? "Data Deletion Scheduled" : "Are you sure ?" }}</div>
|
||||
|
||||
<div v-if="!isDone">
|
||||
{{ message }}
|
||||
</div>
|
||||
|
||||
<div v-if="isDone">
|
||||
Your data deletion request is being processed and will be reflected in your project dashboard within a
|
||||
few minutes.
|
||||
</div>
|
||||
|
||||
<div class="grow"></div>
|
||||
|
||||
<div v-if="!isDone">
|
||||
<UCheckbox v-model="canDelete" label="Confirm data delete"></UCheckbox>
|
||||
</div>
|
||||
|
||||
<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>
|
||||
</div>
|
||||
|
||||
<div v-if="isDone" class="flex justify-end w-full">
|
||||
<LyxUiButton type="secondary" @click="emit('success')"> Dismiss </LyxUiButton>
|
||||
</div>
|
||||
<div class="flex flex-col gap-4">
|
||||
<div>
|
||||
Are you sure to delete data from domain {{ props.data.domain }}?
|
||||
</div>
|
||||
</UModal>
|
||||
<div class="flex justify-end gap-2">
|
||||
<Button variant="secondary" @click="close()"> Back </Button>
|
||||
<Button variant="destructive" @click="emits('confirm')"> Delete </Button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
19
dashboard/components/dialog/DeleteProject.vue
Normal file
19
dashboard/components/dialog/DeleteProject.vue
Normal file
@@ -0,0 +1,19 @@
|
||||
<script lang="ts" setup>
|
||||
|
||||
const emits = defineEmits<{ (event: 'confirm'): void }>();
|
||||
const props = defineProps<{ data: { project_id: string, project_name: string } }>();
|
||||
const { close } = useDialog();
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex flex-col gap-4">
|
||||
<div>
|
||||
Are you sure to delete the project: {{ props.data.project_name }}?
|
||||
</div>
|
||||
<div class="flex justify-end gap-2">
|
||||
<Button variant="secondary" @click="close()"> Back </Button>
|
||||
<Button variant="destructive" @click="emits('confirm')"> Delete </Button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
19
dashboard/components/dialog/DeleteSnapshot.vue
Normal file
19
dashboard/components/dialog/DeleteSnapshot.vue
Normal file
@@ -0,0 +1,19 @@
|
||||
<script lang="ts" setup>
|
||||
|
||||
const emits = defineEmits<{ (event: 'confirm'): void }>();
|
||||
const props = defineProps<{ data: { snapshot: GenericSnapshot } }>();
|
||||
const { close } = useDialog();
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex flex-col gap-4">
|
||||
<div>
|
||||
Are you sure to delete the snapshot: {{ props.data.snapshot.name }}?
|
||||
</div>
|
||||
<div class="flex justify-end gap-2">
|
||||
<Button variant="secondary" @click="close()"> Back </Button>
|
||||
<Button variant="destructive" @click="emits('confirm')"> Delete </Button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -1,56 +0,0 @@
|
||||
<script lang="ts" setup>
|
||||
|
||||
const { createAlert } = useAlert();
|
||||
const { close } = useModal()
|
||||
|
||||
const text = ref<string>("");
|
||||
|
||||
async function sendFeedback() {
|
||||
if (text.value.length < 5) return;
|
||||
try {
|
||||
|
||||
const res = await $fetch('/api/feedback/add', {
|
||||
headers: useComputedHeaders({
|
||||
useSnapshotDates: false,
|
||||
custom: { 'Content-Type': 'application/json' }
|
||||
}).value,
|
||||
method:'POST',
|
||||
body: JSON.stringify({ text: text.value })
|
||||
});
|
||||
|
||||
createAlert('Success', 'Feedback sent successfully.', 'far fa-circle-check', 5000);
|
||||
|
||||
close();
|
||||
} catch (ex) {
|
||||
console.error(ex);
|
||||
createAlert('Error', 'Error sending feedback. Please try again later', 'far fa-triangle-exclamation', 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> Share everything with us. </div>
|
||||
<textarea v-model="text" placeholder="Leave your feedback"
|
||||
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>
|
||||
<LyxUiButton :disabled="text.length < 5" @click="sendFeedback()" type="primary"> Send </LyxUiButton>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</UModal>
|
||||
|
||||
</template>
|
||||
@@ -1,58 +0,0 @@
|
||||
<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>
|
||||
@@ -1,86 +0,0 @@
|
||||
<script lang="ts" setup>
|
||||
import Accept_invite from '~/pages/accept_invite.vue';
|
||||
|
||||
|
||||
const { createAlert } = useAlert();
|
||||
const { close } = useModal()
|
||||
|
||||
const emit = defineEmits(['success', 'cancel'])
|
||||
|
||||
const props = defineProps<{
|
||||
invites: {
|
||||
project_name: string, 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');
|
||||
}
|
||||
}
|
||||
|
||||
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>
|
||||
|
||||
<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-8 p-6">
|
||||
|
||||
<div class="flex flex-col gap-6" v-for="invite of invites">
|
||||
|
||||
<div class="dark:text-lyx-text text-lyx-lightmode-text">
|
||||
You are invited to join
|
||||
<span class="font-semibold">{{ invite.project_name }}</span>.
|
||||
Do you accept?
|
||||
</div>
|
||||
|
||||
<div class="flex gap-4 w-full justify-end">
|
||||
<LyxUiButton @click="declineInvite(invite.project_id)" type="secondary"> Decline </LyxUiButton>
|
||||
<LyxUiButton @click="acceptInvite(invite.project_id)" type="primary"> Accept </LyxUiButton>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
</div>
|
||||
</UModal>
|
||||
</template>
|
||||
19
dashboard/components/dialog/KickUser.vue
Normal file
19
dashboard/components/dialog/KickUser.vue
Normal file
@@ -0,0 +1,19 @@
|
||||
<script lang="ts" setup>
|
||||
|
||||
const emits = defineEmits<{ (event: 'confirm'): void }>();
|
||||
const props = defineProps<{ data: { email: string } }>();
|
||||
const { close } = useDialog();
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex flex-col gap-4">
|
||||
<div>
|
||||
Are you sure to remove {{ props.data.email }}?
|
||||
</div>
|
||||
<div class="flex justify-end gap-2">
|
||||
<Button variant="secondary" @click="close()"> Undo </Button>
|
||||
<Button variant="destructive" @click="emits('confirm')"> Confirm </Button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
93
dashboard/components/dialog/ManagePermissions.vue
Normal file
93
dashboard/components/dialog/ManagePermissions.vue
Normal file
@@ -0,0 +1,93 @@
|
||||
<script lang="ts" setup>
|
||||
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'
|
||||
import type { MemberWithPermissions } from '~/server/api/members/list';
|
||||
import type { TPermission } from '~/shared/schema/TeamMemberSchema'
|
||||
import { Toggle } from '@/components/ui/toggle'
|
||||
|
||||
const emits = defineEmits<{ (event: 'confirm', data: TPermission): void }>();
|
||||
const props = defineProps<{ data: { member: MemberWithPermissions } }>();
|
||||
const { close } = useDialog();
|
||||
|
||||
const domainStore = useDomainStore();
|
||||
|
||||
const currentPermission = ref<TPermission>({
|
||||
webAnalytics: props.data.member.permission.webAnalytics,
|
||||
events: props.data.member.permission.events,
|
||||
ai: props.data.member.permission.ai,
|
||||
domains: props.data.member.permission.domains
|
||||
})
|
||||
|
||||
|
||||
function onCheckboxClick(domain: string) {
|
||||
if (currentPermission.value.domains.includes(domain)) {
|
||||
const index = currentPermission.value.domains.indexOf(domain);
|
||||
currentPermission.value.domains.splice(index, 1);
|
||||
} else {
|
||||
currentPermission.value.domains.push(domain)
|
||||
}
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex flex-col gap-4">
|
||||
<div> Select user permissions </div>
|
||||
|
||||
<div class="flex gap-3 justify-center">
|
||||
<Toggle v-model="currentPermission.webAnalytics" class="flex-1" variant="outline">
|
||||
<div> Web Analytics </div>
|
||||
</Toggle>
|
||||
<Toggle v-model="currentPermission.events" class="flex-1" variant="outline">
|
||||
<div> Events </div>
|
||||
</Toggle>
|
||||
<Toggle v-model="currentPermission.ai" class="flex-1" variant="outline">
|
||||
<div> AI </div>
|
||||
</Toggle>
|
||||
</div>
|
||||
|
||||
<div> Select what domains is allowed to see </div>
|
||||
|
||||
<div class="flex justify-center">
|
||||
<Tabs default-value="all" class="w-full">
|
||||
<TabsList class="flex justify-center w-full">
|
||||
<TabsTrigger @click="currentPermission.domains = []" value="none">
|
||||
No domains
|
||||
</TabsTrigger>
|
||||
<TabsTrigger @click="currentPermission.domains = ['*']" value="all">
|
||||
All domains
|
||||
</TabsTrigger>
|
||||
<TabsTrigger @click="currentPermission.domains = []" value="custom">
|
||||
Custom domains
|
||||
</TabsTrigger>
|
||||
</TabsList>
|
||||
<TabsContent value="none">
|
||||
<div class="text-center">
|
||||
The user cannot access any domain.
|
||||
</div>
|
||||
</TabsContent>
|
||||
<TabsContent value="all">
|
||||
<div class="text-center">
|
||||
The user can access all domains.
|
||||
</div>
|
||||
</TabsContent>
|
||||
<TabsContent value="custom">
|
||||
<ScrollArea type="always" class="h-[15rem] flex flex-col gap-1 w-full mt-3">
|
||||
<div @click="onCheckboxClick(domain.name)"
|
||||
class="flex items-center gap-2 w-fit cursor-pointer select-none"
|
||||
v-for="domain of domainStore.domains.filter(e => e._id !== '*')">
|
||||
<Checkbox :model-value="currentPermission.domains.includes(domain.name)"></Checkbox>
|
||||
<div> {{ truncateText(domain.name, 35) }} </div>
|
||||
</div>
|
||||
</ScrollArea>
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="flex justify-end gap-2">
|
||||
<Button variant="secondary" @click="close()"> Cancel </Button>
|
||||
<Button @click="emits('confirm', currentPermission)"> Save </Button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -1,118 +0,0 @@
|
||||
<script lang="ts" setup>
|
||||
import { useSelectMenuStyle } from '~/composables/ui/useSelectMenuStyle';
|
||||
import type { TTeamMember } from '~/shared/schema/TeamMemberSchema';
|
||||
|
||||
const emit = defineEmits(['success', 'cancel'])
|
||||
|
||||
const props = defineProps<{ member_id: string }>();
|
||||
|
||||
const { domainList, domain, setActiveDomain, refreshDomains, refreshingDomains } = useDomain();
|
||||
|
||||
const { data: member } = useFetch<TTeamMember>(`/api/project/members/get?member_id=${props.member_id}`, {
|
||||
headers: useComputedHeaders({})
|
||||
})
|
||||
|
||||
const { createAlert } = useAlert()
|
||||
|
||||
async function save(member_id: string) {
|
||||
if (!member.value) return;
|
||||
const res = await $fetch('/api/project/members/edit', {
|
||||
method: 'POST',
|
||||
headers: useComputedHeaders({ custom: { 'Content-Type': 'application/json' } }).value,
|
||||
body: JSON.stringify({
|
||||
member_id,
|
||||
webAnalytics: member.value.permission.webAnalytics,
|
||||
events: member.value.permission.events,
|
||||
ai: member.value.permission.ai,
|
||||
domains: member.value.permission.domains
|
||||
})
|
||||
});
|
||||
createAlert('Saved', 'Permission saved successfully', 'fas fa-check', 2500);
|
||||
emit('success')
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
|
||||
|
||||
<template>
|
||||
<UModal :ui="{
|
||||
strategy: 'override',
|
||||
overlay: {
|
||||
background: 'bg-lyx-background/85'
|
||||
},
|
||||
background: 'bg-lyx-lightmode-widget dark:bg-lyx-widget',
|
||||
ring: 'border-solid border-[1px] border-[#262626]'
|
||||
}">
|
||||
<div class="p-8">
|
||||
<div v-if="member" class="manage flex flex-col gap-4">
|
||||
|
||||
<div class="flex flex-col gap-1">
|
||||
<div class="poppins text-[1.1rem]"> Manage permissions </div>
|
||||
<div class="poppins text-[.9rem] dark:text-lyx-text-dark"> Choose what this member can do on this project. </div>
|
||||
</div>
|
||||
|
||||
<LyxUiSeparator></LyxUiSeparator>
|
||||
|
||||
<div class="flex flex-col gap-1">
|
||||
<div>
|
||||
<div class="mb-1"> Select what domain is allowed to see: </div>
|
||||
<div class="mb-1">
|
||||
<USelectMenu v-model="member.permission.domains" :options="domainList" multiple
|
||||
value-attribute="_id">
|
||||
<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>
|
||||
{{
|
||||
member.permission.domains.length > 2 ?
|
||||
`${member.permission.domains.length} domains` :
|
||||
(member.permission.domains.map(e => e).join(' & ') || 'No domains')
|
||||
}}
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</USelectMenu>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<UCheckbox v-model="member.permission.webAnalytics"></UCheckbox>
|
||||
<div> Allow web analytics page </div>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<UCheckbox v-model="member.permission.events"></UCheckbox>
|
||||
<div> Allow events page </div>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<UCheckbox v-model="member.permission.ai"></UCheckbox>
|
||||
<div> Allow to use AI data analyst </div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
|
||||
<div class="flex gap-2 justify-end mt-8">
|
||||
<LyxUiButton class="!w-[6rem] text-center" type="secondary" @click="emit('cancel')"> Cancel </LyxUiButton>
|
||||
<LyxUiButton class="!w-[6rem] text-center" v-if="member?.permission" @click="save(member._id.toString())" type="primary">
|
||||
Save
|
||||
</LyxUiButton>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</UModal>
|
||||
|
||||
</template>
|
||||
18
dashboard/components/dialog/TestDialog.vue
Normal file
18
dashboard/components/dialog/TestDialog.vue
Normal file
@@ -0,0 +1,18 @@
|
||||
<script lang="ts" setup>
|
||||
|
||||
const emits = defineEmits<{
|
||||
(event: 'confirm', data: number): void
|
||||
}>()
|
||||
|
||||
const props = defineProps<{ data: { contentText: string } }>();
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
TEST DIALOG CONTENT
|
||||
<Button @click="emits('confirm', 123)">
|
||||
{{ data.contentText }}
|
||||
</Button>
|
||||
</div>
|
||||
</template>
|
||||
33
dashboard/components/dialog/shields/AddAddress.vue
Normal file
33
dashboard/components/dialog/shields/AddAddress.vue
Normal file
@@ -0,0 +1,33 @@
|
||||
<script lang="ts" setup>
|
||||
|
||||
const address = ref<string>('');
|
||||
const description = ref<string>('');
|
||||
const canAdd = computed(() => address.value.length > 0);
|
||||
const loading = ref<boolean>(false);
|
||||
|
||||
const emits = defineEmits<{ (event: 'confirm', data: { address: string, description: string }): void }>();
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex flex-col gap-2">
|
||||
<Label class="text-base"> Add IP to Block List</Label>
|
||||
|
||||
<Label> IP Address </Label>
|
||||
<Input v-model="address"></Input>
|
||||
<Label> Description </Label>
|
||||
<Input v-model="description"></Input>
|
||||
|
||||
<div class="text-sm text-muted-foreground">
|
||||
Once added, we will start rejecting traffic from this IP within a few minutes.
|
||||
</div>
|
||||
|
||||
<Button v-if="loading" disabled>
|
||||
<Loader class="!size-6"></Loader>
|
||||
</Button>
|
||||
|
||||
<Button v-else @click="emits('confirm', { address, description }), loading = true" :disabled="!canAdd">
|
||||
Add IP Address
|
||||
</Button>
|
||||
</div>
|
||||
</template>
|
||||
27
dashboard/components/dialog/shields/AddDomain.vue
Normal file
27
dashboard/components/dialog/shields/AddDomain.vue
Normal file
@@ -0,0 +1,27 @@
|
||||
<script lang="ts" setup>
|
||||
|
||||
const domain = ref<string>('');
|
||||
const canAdd = computed(() => domain.value.length > 0);
|
||||
const loading = ref<boolean>(false);
|
||||
|
||||
const emits = defineEmits<{ (event: 'confirm', domain: string): void }>();
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex flex-col gap-2">
|
||||
<Label class="text-base"> Add Domain to Allow List</Label>
|
||||
<Input v-model="domain"></Input>
|
||||
<div class="text-sm text-muted-foreground">
|
||||
You can use a wildcard (*) to match multiple hostnames.
|
||||
For example, *.domain.com will only record traffic on the main domain and all the subdomains.
|
||||
</div>
|
||||
<div class="text-sm text-muted-foreground">
|
||||
NB: Once added, we will start allowing traffic only from matching hostnames within a few minutes.
|
||||
</div>
|
||||
<Button v-if="loading" disabled>
|
||||
<Loader class="!size-6"></Loader>
|
||||
</Button>
|
||||
<Button v-else @click="emits('confirm', domain), loading = true" :disabled="!canAdd"> Add domain </Button>
|
||||
</div>
|
||||
</template>
|
||||
22
dashboard/components/dialog/shields/DeleteAddress.vue
Normal file
22
dashboard/components/dialog/shields/DeleteAddress.vue
Normal file
@@ -0,0 +1,22 @@
|
||||
<script lang="ts" setup>
|
||||
|
||||
const { close } = useDialog();
|
||||
const emits = defineEmits<{ (event: 'confirm'): void }>();
|
||||
const props = defineProps<{ data: { address: string } }>();
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex flex-col gap-2">
|
||||
<Label class="text-base"> Address delete </Label>
|
||||
|
||||
<div class="text-base">
|
||||
Are you sure to delete the blacklisted IP address <b>{{ props.data.address }}</b>
|
||||
</div>
|
||||
|
||||
<div class="flex justify-end gap-2">
|
||||
<Button variant="secondary" @click="close()"> Back </Button>
|
||||
<Button variant="destructive" @click="emits('confirm')"> Delete </Button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
22
dashboard/components/dialog/shields/DeleteDomain.vue
Normal file
22
dashboard/components/dialog/shields/DeleteDomain.vue
Normal file
@@ -0,0 +1,22 @@
|
||||
<script lang="ts" setup>
|
||||
|
||||
const { close } = useDialog();
|
||||
const emits = defineEmits<{ (event: 'confirm'): void }>();
|
||||
const props = defineProps<{ data: { domain: string } }>();
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex flex-col gap-2">
|
||||
<Label class="text-base"> Domain delete </Label>
|
||||
|
||||
<div class="text-base">
|
||||
Are you sure to delete the whitelisted domain <b>{{ props.data.domain }}</b>
|
||||
</div>
|
||||
|
||||
<div class="flex justify-end gap-2">
|
||||
<Button variant="secondary" @click="close()"> Back </Button>
|
||||
<Button variant="destructive" @click="emits('confirm')"> Delete </Button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
Reference in New Issue
Block a user