mirror of
https://github.com/Litlyx/litlyx
synced 2025-12-10 15:58:38 +01:00
new selfhosted version
This commit is contained in:
@@ -1,51 +1,86 @@
|
||||
<script lang="ts" setup>
|
||||
<script setup lang="ts">
|
||||
|
||||
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuLabel, DropdownMenuSeparator, DropdownMenuTrigger } from '@/components/ui/dropdown-menu'
|
||||
import { SidebarMenu, SidebarMenuButton, SidebarMenuItem, useSidebar } from '@/components/ui/sidebar'
|
||||
|
||||
import { ChevronsUpDown, Settings } from 'lucide-vue-next'
|
||||
|
||||
const domainStore = useDomainStore();
|
||||
|
||||
|
||||
const filterDomains = ref<string>('');
|
||||
const domainsFiltered = computed(() => {
|
||||
if (filterDomains.value.length == 0) return domainStore.domains;
|
||||
return domainStore.domains.filter(e => e.name.includes(filterDomains.value));
|
||||
})
|
||||
|
||||
const { containerProps, wrapperProps, list } = useVirtualList(domainsFiltered, { itemHeight: 36 });
|
||||
|
||||
|
||||
const { isMobile } = useSidebar()
|
||||
|
||||
const router = useRouter();
|
||||
|
||||
|
||||
const { domainList, domain, setActiveDomain, refreshDomains, refreshingDomains } = useDomain();
|
||||
|
||||
function onChange(e: string) {
|
||||
setActiveDomain(e);
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex gap-2">
|
||||
<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" :loading="refreshingDomains" value-attribute="_id" :options="domainList">
|
||||
<SidebarMenu>
|
||||
<SidebarMenuItem>
|
||||
<Skeleton v-if="!domainStore.activeDomain" class="w-full h-12 px-2"></Skeleton>
|
||||
<DropdownMenu v-if="domainStore.activeDomain">
|
||||
<DropdownMenuTrigger as-child>
|
||||
<SidebarMenuButton size="lg"
|
||||
class="px-4 cursor-pointer data-[state=open]:bg-sidebar-accent data-[state=open]:text-sidebar-accent-foreground">
|
||||
<!-- <div
|
||||
class="flex aspect-square size-8 items-center justify-center rounded-lg bg-sidebar-primary text-sidebar-primary-foreground">
|
||||
<Cat></Cat>
|
||||
</div> -->
|
||||
<div class="grid flex-1 text-left text-sm leading-tight">
|
||||
<span v-if="domainStore.activeDomain" class="truncate font-medium">
|
||||
{{ domainStore.activeDomain.name }}
|
||||
</span>
|
||||
<!-- <span v-if="snapshotStore.from && snapshotStore.to" class="truncate text-xs text-gray-400">
|
||||
{{ new Date(snapshotStore.from).toLocaleDateString() }} to {{ new Date(snapshotStore.to).toLocaleDateString() }}
|
||||
</span> -->
|
||||
</div>
|
||||
<ChevronsUpDown class="ml-auto" />
|
||||
</SidebarMenuButton>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent class="w-[23rem] rounded-lg" align="start"
|
||||
:side="isMobile ? 'bottom' : 'right'" :side-offset="4">
|
||||
|
||||
<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>
|
||||
<DropdownMenuLabel class="text-xs text-gray-500 dark:text-gray-400">
|
||||
Domains
|
||||
</DropdownMenuLabel>
|
||||
|
||||
<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 class="my-2">
|
||||
<Input v-model="filterDomains" placeholder="Filter Domains" @keydown.stop type="text"></Input>
|
||||
</div>
|
||||
<div>
|
||||
{{ refreshingDomains ? 'Loading...' : (domain || '-') }}
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</USelectMenu>
|
||||
|
||||
<UTooltip text="Manage domains">
|
||||
<NuxtLink to="/settings?tab=domains"
|
||||
class="flex items-center hover:rotate-[60deg] transition-all duration-200 ease-in-out cursor-pointer">
|
||||
<i class="far fa-gear"></i>
|
||||
</NuxtLink>
|
||||
</UTooltip>
|
||||
</div>
|
||||
</template>
|
||||
<div v-bind="containerProps" class="max-h-[20rem]">
|
||||
<div class="flex flex-col" v-bind="wrapperProps">
|
||||
<DropdownMenuItem class="h-[36px]" v-for="item in list" :key="item.data.name"
|
||||
:class="{ 'bg-sidebar-accent/50': item.data.name === domainStore.activeDomain.name }"
|
||||
@click="domainStore.setActive(item.data._id.toString())">
|
||||
{{ item.data.name }}
|
||||
</DropdownMenuItem>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem class="gap-2 p-2" @click="router.push('/settings?tab=domains')">
|
||||
<div
|
||||
class="flex size-6 items-center justify-center rounded-md border border-gray-200 bg-transparent dark:border-gray-800">
|
||||
<Settings class="size-4" />
|
||||
</div>
|
||||
<div class="font-medium">
|
||||
Manage domains
|
||||
</div>
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</SidebarMenuItem>
|
||||
</SidebarMenu>
|
||||
</template>
|
||||
|
||||
51
dashboard/components/selector/ImageSelector.vue
Normal file
51
dashboard/components/selector/ImageSelector.vue
Normal file
@@ -0,0 +1,51 @@
|
||||
<script setup lang="ts">
|
||||
|
||||
|
||||
const emits = defineEmits<{
|
||||
(event: 'file_selected', value: File): void;
|
||||
}>();
|
||||
|
||||
const fileInput = ref<HTMLElement | null>(null)
|
||||
const isDragging = ref(false)
|
||||
|
||||
const triggerFileSelect = () => { (fileInput.value as any).click() }
|
||||
|
||||
const handleFileChange = (event: any) => {
|
||||
const file = event.target.files[0]
|
||||
if (file) emits('file_selected', file);
|
||||
|
||||
}
|
||||
|
||||
const handleDrop = (event: any) => {
|
||||
const file = event.dataTransfer.files[0]
|
||||
isDragging.value = false
|
||||
if (file) emits('file_selected', file);
|
||||
}
|
||||
|
||||
const handleDragOver = () => {
|
||||
isDragging.value = true
|
||||
}
|
||||
|
||||
const handleDragLeave = () => {
|
||||
isDragging.value = false
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
|
||||
|
||||
<template>
|
||||
|
||||
<div id="drop-area"
|
||||
class="w-full select-none max-w-md border-2 border-dashed border-gray-600 rounded-lg p-12 text-center cursor-pointer hover:border-blue-500 transition"
|
||||
@click="triggerFileSelect" @dragover.prevent="handleDragOver" @dragleave="handleDragLeave"
|
||||
@drop.prevent="handleDrop" :class="{ 'border-blue-500': isDragging }">
|
||||
<p class="text-gray-400">
|
||||
Drag & drop an image here
|
||||
<br>
|
||||
or click to select a file
|
||||
</p>
|
||||
<input ref="fileInput" type="file" accept="image/*" class="hidden" @change="handleFileChange" />
|
||||
</div>
|
||||
|
||||
</template>
|
||||
@@ -1,55 +0,0 @@
|
||||
<script lang="ts" setup>
|
||||
|
||||
import type { TProject } from '@schema/project/ProjectSchema';
|
||||
|
||||
const { user } = useLoggedUser()
|
||||
|
||||
const { projectList, guestProjectList, allProjectList, actions, project } = useProject();
|
||||
const { setActiveDomain } = useDomain();
|
||||
|
||||
function isProjectMine(owner?: string) {
|
||||
if (!owner) return false;
|
||||
if (!user.value) return false;
|
||||
if (!user.value.logged) return;
|
||||
return user.value.id == owner;
|
||||
}
|
||||
|
||||
function onChange(e: TProject) {
|
||||
actions.setActiveProject(e._id.toString());
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
||||
<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',
|
||||
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'
|
||||
}
|
||||
}" class="w-full" v-if="allProjectList" @change="onChange" :value="project" :options="allProjectList">
|
||||
|
||||
<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.name }} {{ !isProjectMine(option.owner) ? '(Guest)' : '' }}</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template #label>
|
||||
<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>
|
||||
{{ project?.name || '-' }}
|
||||
{{ !isProjectMine(project?.owner?.toString()) ? '(Guest)' : '' }}
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</USelectMenu>
|
||||
|
||||
</template>
|
||||
150
dashboard/components/selector/SnapshotSelector.vue
Normal file
150
dashboard/components/selector/SnapshotSelector.vue
Normal file
@@ -0,0 +1,150 @@
|
||||
<script setup lang="ts">
|
||||
|
||||
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuLabel, DropdownMenuSeparator, DropdownMenuTrigger } from '@/components/ui/dropdown-menu'
|
||||
import { SidebarMenu, SidebarMenuButton, SidebarMenuItem, useSidebar } from '@/components/ui/sidebar'
|
||||
|
||||
import { ChevronsUpDown, Plus, Trash } from 'lucide-vue-next'
|
||||
import DeleteSnapshot from '../dialog/DeleteSnapshot.vue';
|
||||
|
||||
import CreateSnapshot from '../dialog/CreateSnapshot.vue';
|
||||
|
||||
const snapshotStore = useSnapshotStore();
|
||||
const domainStore = useDomainStore();
|
||||
|
||||
const dialog = useDialog();
|
||||
|
||||
const open = ref<boolean>(false);
|
||||
const { isMobile } = useSidebar()
|
||||
|
||||
|
||||
function showCreateSnapshotDialog() {
|
||||
open.value = false;
|
||||
dialog.open({
|
||||
body: CreateSnapshot,
|
||||
title: 'Create timeframe',
|
||||
async onSuccess(data, close) {
|
||||
|
||||
await useCatch({
|
||||
toast: true,
|
||||
async action() {
|
||||
return await useAuthFetchSync<void>('/api/snapshot/create', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: { name: data.name, color: data.color, from: new Date(data.from), to: new Date(data.to) }
|
||||
});
|
||||
},
|
||||
async onSuccess(_, toast) {
|
||||
toast('Snapshot created', { description: `Snapshot ${data.name} created` });
|
||||
await snapshotStore.fetchSnapshots({ activateLast: true });
|
||||
}
|
||||
});
|
||||
|
||||
close();
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function showDialog(snapshot: GenericSnapshot) {
|
||||
open.value = false;
|
||||
dialog.open({
|
||||
body: DeleteSnapshot,
|
||||
title: 'Delete timeframe',
|
||||
props: { snapshot },
|
||||
async onSuccess(_, close) {
|
||||
|
||||
await useCatch({
|
||||
toast: true,
|
||||
async action() {
|
||||
return await useAuthFetchSync<void>('/api/snapshot/delete', {
|
||||
method: 'DELETE',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: { id: snapshot._id.toString() }
|
||||
});
|
||||
},
|
||||
async onSuccess(data, toast) {
|
||||
toast('Timeframe deleted', { description: `Timeframe ${snapshot.name} deleted` });
|
||||
await snapshotStore.fetchSnapshots()
|
||||
}
|
||||
});
|
||||
|
||||
close();
|
||||
},
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<SidebarMenu>
|
||||
<SidebarMenuItem>
|
||||
<Skeleton v-if="!snapshotStore.activeSnapshot || !domainStore.activeDomain" class="w-full h-12 p-2">
|
||||
</Skeleton>
|
||||
<DropdownMenu v-model:open="open" v-if="snapshotStore.activeSnapshot && domainStore.activeDomain">
|
||||
<DropdownMenuTrigger as-child>
|
||||
<SidebarMenuButton size="lg"
|
||||
class="px-4 cursor-pointer data-[state=open]:bg-sidebar-accent/50 data-[state=open]:text-sidebar-accent-foreground">
|
||||
<div class="grid flex-1 text-left text-sm leading-tight">
|
||||
<div class="flex items-center">
|
||||
<div :style="`background-color:${snapshotStore.activeSnapshot.color};`"
|
||||
class="flex size-3 mr-[.4rem] rounded-full"> </div>
|
||||
<div v-if="snapshotStore.activeSnapshot" class="truncate font-medium">
|
||||
{{ snapshotStore.activeSnapshot.name }}
|
||||
</div>
|
||||
</div>
|
||||
<span v-if="snapshotStore.from && snapshotStore.to" class="truncate text-xs text-gray-400">
|
||||
{{ new Date(snapshotStore.from).toLocaleDateString() }}
|
||||
to
|
||||
{{ new Date(snapshotStore.to).toLocaleDateString() }}
|
||||
</span>
|
||||
</div>
|
||||
<ChevronsUpDown class="ml-auto" />
|
||||
</SidebarMenuButton>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent class="w-[--reka-dropdown-menu-trigger-width] min-w-56 rounded-lg" align="start"
|
||||
:side="isMobile ? 'bottom' : 'right'" :side-offset="4">
|
||||
<DropdownMenuLabel class="text-xs text-gray-500 dark:text-gray-400">
|
||||
Timeframes
|
||||
</DropdownMenuLabel>
|
||||
|
||||
<div class="overflow-y-auto h-[20rem]">
|
||||
|
||||
<div class="flex items-center" v-for="item in snapshotStore.snapshots">
|
||||
<DropdownMenuItem :key="item.name" class="flex gap-1 w-full p-0 my-0.5"
|
||||
:class="{'bg-sidebar-accent/50':item.name === snapshotStore.activeSnapshot.name}"
|
||||
>
|
||||
<div @click="snapshotStore.setActive(item._id.toString())"
|
||||
class="flex gap-2 grow p-2 items-center">
|
||||
<div :style="`background-color:${item.color};`"
|
||||
class="flex size-4 rounded-full border">
|
||||
</div>
|
||||
<div class="grow">
|
||||
{{ item.name }}
|
||||
</div>
|
||||
</div>
|
||||
</DropdownMenuItem>
|
||||
<div class="py-2 px-1" v-if="!item._id.toString().startsWith('__')">
|
||||
<Trash @click="showDialog(item)" class="size-4 hover:text-red-200 cursor-pointer">
|
||||
</Trash>
|
||||
</div>
|
||||
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem class="gap-2 p-2" @click="showCreateSnapshotDialog()">
|
||||
<div
|
||||
class="flex size-6 items-center justify-center rounded-md border border-gray-200 bg-transparent dark:border-gray-800">
|
||||
<Plus class="size-4" />
|
||||
</div>
|
||||
<div class="font-medium">
|
||||
Add timeframe
|
||||
</div>
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</SidebarMenuItem>
|
||||
</SidebarMenu>
|
||||
</template>
|
||||
Reference in New Issue
Block a user