new selfhosted version

This commit is contained in:
antonio
2025-11-28 14:11:51 +01:00
parent afda29997d
commit 951860f67e
1046 changed files with 72586 additions and 574750 deletions

View File

@@ -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>

View 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>

View File

@@ -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>

View 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>