mirror of
https://github.com/versia-pub/frontend.git
synced 2025-12-06 08:28:20 +01:00
refactor: 🔥 Remove old code
This commit is contained in:
parent
42e0b38fd8
commit
5b3e9ce8b3
4
app.vue
4
app.vue
|
|
@ -10,7 +10,6 @@
|
||||||
<NuxtLayout>
|
<NuxtLayout>
|
||||||
<NuxtPage />
|
<NuxtPage />
|
||||||
</NuxtLayout>
|
</NuxtLayout>
|
||||||
<NotificationsRenderer />
|
|
||||||
<ConfirmationModal />
|
<ConfirmationModal />
|
||||||
<!-- pointer-events-auto fixes https://github.com/unovue/shadcn-vue/issues/462 -->
|
<!-- pointer-events-auto fixes https://github.com/unovue/shadcn-vue/issues/462 -->
|
||||||
<Toaster class="pointer-events-auto" />
|
<Toaster class="pointer-events-auto" />
|
||||||
|
|
@ -24,11 +23,8 @@ import "~/styles/index.css";
|
||||||
import { convert } from "html-to-text";
|
import { convert } from "html-to-text";
|
||||||
import "iconify-icon";
|
import "iconify-icon";
|
||||||
import ConfirmationModal from "./components/modals/confirm.vue";
|
import ConfirmationModal from "./components/modals/confirm.vue";
|
||||||
import NotificationsRenderer from "./components/notifications/notifications-renderer.vue";
|
|
||||||
import { Toaster } from "./components/ui/sonner";
|
import { Toaster } from "./components/ui/sonner";
|
||||||
import { SettingIds } from "./settings";
|
import { SettingIds } from "./settings";
|
||||||
// Use SSR-safe IDs for Headless UI
|
|
||||||
provideHeadlessUseId(() => useId());
|
|
||||||
|
|
||||||
const code = useRequestURL().searchParams.get("code");
|
const code = useRequestURL().searchParams.get("code");
|
||||||
const appData = useAppData();
|
const appData = useAppData();
|
||||||
|
|
|
||||||
|
|
@ -1,42 +0,0 @@
|
||||||
<template>
|
|
||||||
<Teleport to="body">
|
|
||||||
<Toaster :toaster="toaster" v-slot="toast">
|
|
||||||
<Toast.Root
|
|
||||||
class="rounded-lg w-[calc(100vw-2rem)] sm:w-80 bg-dark-500 duration-200 shadow-lg ring-1 ring-white/10 p-4 [&:nth-child(n+5)]:opacity-0 data-[stack]:!opacity-100 scale-[--scale,100%] translate-x-[--x] translate-y-[--y] z-[--z-index] will-change-transform">
|
|
||||||
<div class="grid grid-cols-[auto_1fr_auto]">
|
|
||||||
<div class="shrink-0 h-6 w-6">
|
|
||||||
<iconify-icon v-if="toast.type === 'success'" icon="tabler:check" height="none"
|
|
||||||
class="h-6 w-6 text-green-400" aria-hidden="true" />
|
|
||||||
<iconify-icon v-else-if="toast.type === 'error'" icon="tabler:alert-triangle" height="none"
|
|
||||||
class="h-6 w-6 text-red-400" aria-hidden="true" />
|
|
||||||
<iconify-icon v-else-if="toast.type === 'loading'" icon="tabler:loader" height="none"
|
|
||||||
class="h-6 w-6 text-primary2-500 animate-spin" aria-hidden="true" />
|
|
||||||
<iconify-icon v-else-if="toast.type === 'info'" icon="tabler:info-circle" height="none"
|
|
||||||
class="h-6 w-6 text-blue-500" aria-hidden="true" />
|
|
||||||
</div>
|
|
||||||
<div class="ml-3 flex-1 pt-0.5 shrink-0 min-w-48">
|
|
||||||
<Toast.Title class="text-sm font-semibold text-gray-50">{{ toast.title }}</Toast.Title>
|
|
||||||
<Toast.Description class="mt-1 text-sm text-gray-400">{{
|
|
||||||
toast.description }}</Toast.Description>
|
|
||||||
</div>
|
|
||||||
<div class="ml-4 flex shrink-0">
|
|
||||||
<Toast.CloseTrigger type="button" title="Close this notification"
|
|
||||||
class="inline-flex rounded-md text-gray-400 hover:text-gray-300 duration-200">
|
|
||||||
<iconify-icon icon="tabler:x" class="h-5 w-5" aria-hidden="true" />
|
|
||||||
</Toast.CloseTrigger>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Toast.Root>
|
|
||||||
</Toaster>
|
|
||||||
</Teleport>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="tsx">
|
|
||||||
import { Toast, Toaster, createToaster } from "@ark-ui/vue";
|
|
||||||
|
|
||||||
const toaster = createToaster({ placement: "top-end", overlap: true, gap: 24 });
|
|
||||||
|
|
||||||
useListen("notification:new", (notification) => {
|
|
||||||
toaster.create(notification);
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
|
|
@ -1,100 +0,0 @@
|
||||||
<template>
|
|
||||||
<AdaptiveDropdown>
|
|
||||||
<template #button>
|
|
||||||
<slot>
|
|
||||||
<div class="rounded text-left flex flex-row gap-x-2 hover:scale-[95%] duration-100"
|
|
||||||
v-if="identity">
|
|
||||||
<div class="shrink-0">
|
|
||||||
<Avatar class="size-12 rounded ring-1 ring-white/5" :src="identity.account.avatar"
|
|
||||||
:alt="`${identity.account.acct}'s avatar'`" />
|
|
||||||
</div>
|
|
||||||
<div class="flex flex-col items-start p-1 justify-around grow overflow-hidden">
|
|
||||||
<div class="flex flex-row items-center justify-between w-full">
|
|
||||||
<div class="font-semibold text-gray-200 text-sm line-clamp-1 break-all">
|
|
||||||
{{
|
|
||||||
identity.account.display_name }}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<span class="text-gray-400 text-xs line-clamp-1 break-all w-full">
|
|
||||||
Change account
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<ButtonBase theme="secondary" v-else class="w-full !justify-start overflow-hidden">
|
|
||||||
<Icon icon="tabler:login" class="!size-6" />
|
|
||||||
<span class="shrink-0 line-clamp-1">Sign In</span>
|
|
||||||
</ButtonBase>
|
|
||||||
</slot>
|
|
||||||
</template>
|
|
||||||
<template #items>
|
|
||||||
<div class="p-2">
|
|
||||||
<h3 class="text-gray-400 text-xs text-center md:text-left uppercase font-semibold">Switch to account
|
|
||||||
</h3>
|
|
||||||
</div>
|
|
||||||
<div class="px-2 py-4 md:py-2 flex flex-col gap-3 max-w-[100vw]">
|
|
||||||
<Menu.Item value="" v-for="identity of identities" class="hover:scale-[95%] duration-100">
|
|
||||||
<div class="flex flex-row gap-x-4">
|
|
||||||
<div class="shrink-0" data-part="item" @click="useEvent('identity:change', identity)">
|
|
||||||
<Avatar class="h-12 w-12 rounded ring-1 ring-white/5" :src="identity.account.avatar"
|
|
||||||
:alt="`${identity.account.acct}'s avatar'`" />
|
|
||||||
</div>
|
|
||||||
<div data-part="item" class="flex flex-col items-start justify-around grow overflow-hidden"
|
|
||||||
@click="useEvent('identity:change', identity)">
|
|
||||||
<div class="flex flex-row items-center justify-between w-full">
|
|
||||||
<div class="font-semibold text-gray-200 line-clamp-1 break-all">
|
|
||||||
{{
|
|
||||||
identity.account.display_name }}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<span class="text-gray-400 text-sm line-clamp-1 break-all w-full">
|
|
||||||
@{{
|
|
||||||
identity.account.acct
|
|
||||||
}}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<button data-part="item"
|
|
||||||
class="shrink-0 ml-6 size-12 ring-white/5 ring-1 flex items-center justify-center rounded"
|
|
||||||
@click="$emit('signOut', identity.id)">
|
|
||||||
<iconify-icon icon="tabler:logout" class="size-6 text-gray-200" width="none" />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</Menu.Item>
|
|
||||||
<Menu.Item value="" v-if="identity">
|
|
||||||
<NuxtLink href="/settings" class="w-full">
|
|
||||||
<ButtonBase theme="ghost" class="w-full !justify-start">
|
|
||||||
<Icon icon="tabler:adjustments" class="!size-6" />
|
|
||||||
<span class="shrink-0 line-clamp-1">Settings</span>
|
|
||||||
</ButtonBase>
|
|
||||||
</NuxtLink>
|
|
||||||
</Menu.Item>
|
|
||||||
<Menu.Item value="">
|
|
||||||
<ButtonBase @click="$emit('signIn')" theme="ghost" class="w-full !justify-start">
|
|
||||||
<Icon icon="tabler:user-plus" class="!size-6" />
|
|
||||||
<span class="shrink-0 line-clamp-1">Add new account</span>
|
|
||||||
</ButtonBase>
|
|
||||||
</Menu.Item>
|
|
||||||
<Menu.Item value="" v-if="!identity">
|
|
||||||
<NuxtLink href="/register" class="w-full">
|
|
||||||
<ButtonBase theme="outline" class="w-full !justify-start">
|
|
||||||
<Icon icon="tabler:certificate" class="!size-6" />
|
|
||||||
<span class="shrink-0 line-clamp-1">Create new account</span>
|
|
||||||
</ButtonBase>
|
|
||||||
</NuxtLink>
|
|
||||||
</Menu.Item>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
</AdaptiveDropdown>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script lang="ts" setup>
|
|
||||||
import { Menu } from "@ark-ui/vue";
|
|
||||||
import ButtonBase from "~/packages/ui/components/buttons/button.vue";
|
|
||||||
import Icon from "~/packages/ui/components/icons/icon.vue";
|
|
||||||
import Avatar from "../avatars/avatar.vue";
|
|
||||||
import AdaptiveDropdown from "../dropdowns/AdaptiveDropdown.vue";
|
|
||||||
|
|
||||||
defineEmits<{
|
|
||||||
signIn: [];
|
|
||||||
signOut: [identityId: string];
|
|
||||||
}>();
|
|
||||||
</script>
|
|
||||||
152
components/sidebars/account-switcher.vue
Normal file
152
components/sidebars/account-switcher.vue
Normal file
|
|
@ -0,0 +1,152 @@
|
||||||
|
<template>
|
||||||
|
<DropdownMenu>
|
||||||
|
<DropdownMenuTrigger as-child>
|
||||||
|
<SidebarMenuButton size="lg"
|
||||||
|
class="data-[state=open]:bg-sidebar-accent data-[state=open]:text-sidebar-accent-foreground">
|
||||||
|
<Avatar v-if="identity" shape="square" class="size-8">
|
||||||
|
<AvatarImage :src="identity?.account.avatar" alt="" />
|
||||||
|
<AvatarFallback class="rounded-lg"> AA </AvatarFallback>
|
||||||
|
</Avatar>
|
||||||
|
<Avatar v-else shape="square" class="size-8">
|
||||||
|
<AvatarFallback class="rounded-lg"> AB </AvatarFallback>
|
||||||
|
</Avatar>
|
||||||
|
<div class="grid flex-1 text-left text-sm leading-tight">
|
||||||
|
<span class="truncate font-semibold" v-render-emojis="identity?.account.emojis">{{
|
||||||
|
identity?.account.display_name ?? "Not signed in"
|
||||||
|
}}</span>
|
||||||
|
<span class="truncate text-xs" v-if="identity">@{{ identity?.account.acct }}</span>
|
||||||
|
</div>
|
||||||
|
<ChevronsUpDown class="ml-auto size-4" />
|
||||||
|
</SidebarMenuButton>
|
||||||
|
</DropdownMenuTrigger>
|
||||||
|
<DropdownMenuContent class="w-[--radix-dropdown-menu-trigger-width] min-w-56 rounded-lg" side="bottom"
|
||||||
|
align="end" :side-offset="4">
|
||||||
|
<DropdownMenuLabel class="p-0 font-normal">
|
||||||
|
<div v-for="identity of identities" class="flex items-center gap-2 px-1 py-1.5 text-left text-sm">
|
||||||
|
<Avatar shape="square" class="size-8">
|
||||||
|
<AvatarImage :src="identity.account.avatar" alt="" />
|
||||||
|
<AvatarFallback class="rounded-lg"> AA </AvatarFallback>
|
||||||
|
</Avatar>
|
||||||
|
<div class="grid flex-1 text-left text-sm leading-tight">
|
||||||
|
<span class="truncate font-semibold" v-render-emojis="identity.account.emojis">{{
|
||||||
|
identity.account.display_name
|
||||||
|
}}</span>
|
||||||
|
<span class="truncate text-xs">@{{
|
||||||
|
identity.account.acct
|
||||||
|
}}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<DropdownMenuItem @click="signIn()">
|
||||||
|
<UserPlus />
|
||||||
|
Add account
|
||||||
|
</DropdownMenuItem>
|
||||||
|
</DropdownMenuLabel>
|
||||||
|
<DropdownMenuSeparator v-if="identity" />
|
||||||
|
<DropdownMenuGroup v-if="identity">
|
||||||
|
<DropdownMenuItem>
|
||||||
|
<BadgeCheck />
|
||||||
|
Account
|
||||||
|
</DropdownMenuItem>
|
||||||
|
</DropdownMenuGroup>
|
||||||
|
<DropdownMenuSeparator />
|
||||||
|
<DropdownMenuItem @click="signOut()">
|
||||||
|
<LogOut />
|
||||||
|
Log out
|
||||||
|
</DropdownMenuItem>
|
||||||
|
</DropdownMenuContent>
|
||||||
|
</DropdownMenu>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { BadgeCheck, ChevronsUpDown, LogOut, UserPlus } from "lucide-vue-next";
|
||||||
|
import { toast } from "vue-sonner";
|
||||||
|
import { Avatar, AvatarFallback, AvatarImage } from "../ui/avatar";
|
||||||
|
import {
|
||||||
|
DropdownMenu,
|
||||||
|
DropdownMenuContent,
|
||||||
|
DropdownMenuGroup,
|
||||||
|
DropdownMenuItem,
|
||||||
|
DropdownMenuLabel,
|
||||||
|
DropdownMenuSeparator,
|
||||||
|
DropdownMenuTrigger,
|
||||||
|
} from "../ui/dropdown-menu";
|
||||||
|
import { SidebarMenuButton } from "../ui/sidebar";
|
||||||
|
|
||||||
|
const appData = useAppData();
|
||||||
|
|
||||||
|
const signIn = async () => {
|
||||||
|
const id = toast.loading("Signing in...");
|
||||||
|
|
||||||
|
const output = await client.value.createApp("Versia", {
|
||||||
|
scopes: ["read", "write", "follow", "push"],
|
||||||
|
redirect_uris: new URL("/", useRequestURL().origin).toString(),
|
||||||
|
website: useBaseUrl().value,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!output?.data) {
|
||||||
|
toast.dismiss(id);
|
||||||
|
toast.error("Failed to create app");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
appData.value = output.data;
|
||||||
|
|
||||||
|
const url = await client.value.generateAuthUrl(
|
||||||
|
output.data.client_id,
|
||||||
|
output.data.client_secret,
|
||||||
|
{
|
||||||
|
scopes: ["read", "write", "follow", "push"],
|
||||||
|
redirect_uri: new URL("/", useRequestURL().origin).toString(),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!url) {
|
||||||
|
toast.dismiss(id);
|
||||||
|
toast.error("Failed to generate auth URL");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
window.location.href = url;
|
||||||
|
};
|
||||||
|
|
||||||
|
const signOut = async (userId?: string) => {
|
||||||
|
const id = toast.loading("Signing out...");
|
||||||
|
|
||||||
|
if (!(appData.value && identity.value)) {
|
||||||
|
toast.dismiss(id);
|
||||||
|
toast.error("No app or identity data to sign out");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const identityToRevoke = userId
|
||||||
|
? identities.value.find((i) => i.account.id === userId)
|
||||||
|
: identity.value;
|
||||||
|
|
||||||
|
if (!identityToRevoke) {
|
||||||
|
toast.dismiss(id);
|
||||||
|
toast.error("No identity to revoke");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Don't do anything on error, as Versia Server doesn't implement the revoke endpoint yet
|
||||||
|
await client.value
|
||||||
|
?.revokeToken(
|
||||||
|
appData.value.client_id,
|
||||||
|
identityToRevoke.tokens.access_token,
|
||||||
|
identityToRevoke.tokens.access_token,
|
||||||
|
)
|
||||||
|
.catch(() => {
|
||||||
|
// Do nothing
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!userId) {
|
||||||
|
identity.value = null;
|
||||||
|
await navigateTo("/");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
identities.value = identities.value.filter((i) => i.id !== userId);
|
||||||
|
toast.dismiss(id);
|
||||||
|
toast.success("Signed out");
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
@ -1,41 +0,0 @@
|
||||||
<template>
|
|
||||||
<aside v-bind="$props" role="complementary" :aria-expanded="open ? 'true' : 'false'"
|
|
||||||
:class="['flex max-h-dvh overflow-hidden duration-200', open ? enterClass : leaveClass, direction === 'left' ? 'flex-row' : 'flex-row-reverse']">
|
|
||||||
<OverlayScrollbarsComponent :defer="true"
|
|
||||||
class="bg-dark-700 ring-1 ring-white/10 h-full overflow-y-auto w-full">
|
|
||||||
<slot />
|
|
||||||
</OverlayScrollbarsComponent>
|
|
||||||
<button @click="open = !open" aria-label="Toggle sidebar"
|
|
||||||
class="h-full bg-dark-700 hover:bg-dark-400 hover:cursor-pointer duration-200 py-4 px-0.5 flex items-center justify-center w-4 shrink-0">
|
|
||||||
<iconify-icon icon="tabler:chevron-right"
|
|
||||||
:class="['text-gray-200 duration-200', direction === 'left' ? open ? 'rotate-180' : 'rotate-0' : open ? 'rotate-0' : 'rotate-180']"
|
|
||||||
aria-hidden="true" />
|
|
||||||
</button>
|
|
||||||
</aside>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script lang="ts" setup>
|
|
||||||
import { OverlayScrollbarsComponent } from "overlayscrollbars-vue";
|
|
||||||
// slides in and out from the left or right
|
|
||||||
import type { HTMLAttributes } from "vue";
|
|
||||||
|
|
||||||
interface Props extends /* @vue-ignore */ HTMLAttributes {
|
|
||||||
direction?: "left" | "right";
|
|
||||||
initial?: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
const props = withDefaults(defineProps<Props>(), {
|
|
||||||
direction: "left",
|
|
||||||
initial: false,
|
|
||||||
});
|
|
||||||
|
|
||||||
const leaveClass = computed(() =>
|
|
||||||
props.direction === "left"
|
|
||||||
? "-left-[calc(28rem-6rem)]"
|
|
||||||
: "-right-[calc(28rem-1rem)]",
|
|
||||||
);
|
|
||||||
const enterClass = computed(() =>
|
|
||||||
props.direction === "left" ? "left-0" : "right-0",
|
|
||||||
);
|
|
||||||
const open = ref(props.initial);
|
|
||||||
</script>
|
|
||||||
202
components/sidebars/left-sidebar.vue
Normal file
202
components/sidebars/left-sidebar.vue
Normal file
|
|
@ -0,0 +1,202 @@
|
||||||
|
<template>
|
||||||
|
<Sidebar variant="inset" collapsible="icon">
|
||||||
|
<SidebarHeader>
|
||||||
|
<SidebarMenu>
|
||||||
|
<SidebarMenuItem>
|
||||||
|
<NuxtLink href="/">
|
||||||
|
<SidebarMenuButton size="lg"
|
||||||
|
class="data-[state=open]:bg-sidebar-accent data-[state=open]:text-sidebar-accent-foreground">
|
||||||
|
<Avatar shape="square" class="size-8">
|
||||||
|
<AvatarImage :src="instance?.thumbnail.url ??
|
||||||
|
'https://cdn.versia.pub/branding/icon.svg'
|
||||||
|
" alt="" />
|
||||||
|
</Avatar>
|
||||||
|
<div class="grid flex-1 text-left text-sm leading-tight">
|
||||||
|
<span class="truncate font-semibold">{{ instance?.title ?? 'Versia Server' }}</span>
|
||||||
|
<span class="truncate text-xs">{{ "A Versia Server instance" }}</span>
|
||||||
|
</div>
|
||||||
|
<!-- <ChevronsUpDown class="ml-auto" /> -->
|
||||||
|
</SidebarMenuButton>
|
||||||
|
</NuxtLink>
|
||||||
|
</SidebarMenuItem>
|
||||||
|
</SidebarMenu>
|
||||||
|
</SidebarHeader>
|
||||||
|
<SidebarContent>
|
||||||
|
<SidebarGroup>
|
||||||
|
<SidebarGroupLabel>Navigation</SidebarGroupLabel>
|
||||||
|
<SidebarMenu>
|
||||||
|
<SidebarMenuItem v-for="item in data.other" :key="item.name">
|
||||||
|
<SidebarMenuButton as-child>
|
||||||
|
<NuxtLink :href="item.url">
|
||||||
|
<component :is="item.icon" />
|
||||||
|
<span>{{ item.name }}</span>
|
||||||
|
</NuxtLink>
|
||||||
|
</SidebarMenuButton>
|
||||||
|
</SidebarMenuItem>
|
||||||
|
</SidebarMenu>
|
||||||
|
</SidebarGroup>
|
||||||
|
<SidebarGroup class="mt-auto">
|
||||||
|
<SidebarGroupLabel>More</SidebarGroupLabel>
|
||||||
|
<SidebarMenu>
|
||||||
|
<Collapsible v-for="item in data.navMain" :key="item.title" as-child
|
||||||
|
class="group/collapsible">
|
||||||
|
<SidebarMenuItem>
|
||||||
|
<CollapsibleTrigger as-child>
|
||||||
|
<SidebarMenuButton :tooltip="item.title">
|
||||||
|
<component :is="item.icon" />
|
||||||
|
<span>{{ item.title }}</span>
|
||||||
|
<ChevronRight
|
||||||
|
class="ml-auto transition-transform duration-200 group-data-[state=open]/collapsible:rotate-90" />
|
||||||
|
</SidebarMenuButton>
|
||||||
|
</CollapsibleTrigger>
|
||||||
|
<CollapsibleContent>
|
||||||
|
<SidebarMenuSub>
|
||||||
|
<SidebarMenuSubItem v-for="subItem in item.items" :key="subItem.title">
|
||||||
|
<SidebarMenuSubButton as-child>
|
||||||
|
<NuxtLink :href="subItem.url">
|
||||||
|
<span>{{ subItem.title }}</span>
|
||||||
|
</NuxtLink>
|
||||||
|
</SidebarMenuSubButton>
|
||||||
|
</SidebarMenuSubItem>
|
||||||
|
</SidebarMenuSub>
|
||||||
|
</CollapsibleContent>
|
||||||
|
</SidebarMenuItem>
|
||||||
|
</Collapsible>
|
||||||
|
</SidebarMenu>
|
||||||
|
</SidebarGroup>
|
||||||
|
</SidebarContent>
|
||||||
|
<SidebarFooter>
|
||||||
|
<SidebarMenu class="gap-3">
|
||||||
|
<SidebarMenuItem>
|
||||||
|
<ThemeSwitcher />
|
||||||
|
</SidebarMenuItem>
|
||||||
|
<SidebarMenuItem>
|
||||||
|
<AccountSwitcher />
|
||||||
|
</SidebarMenuItem>
|
||||||
|
<SidebarMenuItem class="flex flex-col gap-2">
|
||||||
|
<Button variant="default" size="lg" class="w-full group-data-[collapsible=icon]:px-4"
|
||||||
|
v-if="identity"
|
||||||
|
@click="useEvent('composer:open')">
|
||||||
|
<Pen />
|
||||||
|
<span class="group-data-[collapsible=icon]:hidden">Compose</span>
|
||||||
|
</Button>
|
||||||
|
<Button variant="destructive" size="lg" class="w-full group-data-[collapsible=icon]:px-4" v-if="$pwa?.needRefresh" @click="$pwa?.updateServiceWorker(true)">
|
||||||
|
<DownloadCloud />
|
||||||
|
<span class="group-data-[collapsible=icon]:hidden">Update</span>
|
||||||
|
</Button>
|
||||||
|
</SidebarMenuItem>
|
||||||
|
</SidebarMenu>
|
||||||
|
</SidebarFooter>
|
||||||
|
<SidebarRail />
|
||||||
|
</Sidebar>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import {
|
||||||
|
BadgeCheck,
|
||||||
|
BedSingle,
|
||||||
|
Bell,
|
||||||
|
ChevronRight,
|
||||||
|
ChevronsUpDown,
|
||||||
|
DownloadCloud,
|
||||||
|
Globe,
|
||||||
|
House,
|
||||||
|
LogOut,
|
||||||
|
MapIcon,
|
||||||
|
Pen,
|
||||||
|
RefreshCcw,
|
||||||
|
Settings2,
|
||||||
|
} from "lucide-vue-next";
|
||||||
|
import { toast } from "vue-sonner";
|
||||||
|
import { Avatar, AvatarFallback, AvatarImage } from "~/components/ui/avatar";
|
||||||
|
import {
|
||||||
|
Collapsible,
|
||||||
|
CollapsibleContent,
|
||||||
|
CollapsibleTrigger,
|
||||||
|
} from "~/components/ui/collapsible";
|
||||||
|
import {
|
||||||
|
DropdownMenu,
|
||||||
|
DropdownMenuContent,
|
||||||
|
DropdownMenuGroup,
|
||||||
|
DropdownMenuItem,
|
||||||
|
DropdownMenuLabel,
|
||||||
|
DropdownMenuSeparator,
|
||||||
|
DropdownMenuTrigger,
|
||||||
|
} from "~/components/ui/dropdown-menu";
|
||||||
|
import {
|
||||||
|
Sidebar,
|
||||||
|
SidebarContent,
|
||||||
|
SidebarFooter,
|
||||||
|
SidebarGroup,
|
||||||
|
SidebarGroupLabel,
|
||||||
|
SidebarHeader,
|
||||||
|
SidebarMenu,
|
||||||
|
SidebarMenuButton,
|
||||||
|
SidebarMenuItem,
|
||||||
|
SidebarMenuSub,
|
||||||
|
SidebarMenuSubButton,
|
||||||
|
SidebarMenuSubItem,
|
||||||
|
SidebarRail,
|
||||||
|
} from "~/components/ui/sidebar";
|
||||||
|
import { Button } from "../ui/button";
|
||||||
|
import AccountSwitcher from "./account-switcher.vue";
|
||||||
|
import ThemeSwitcher from "./theme-switcher.vue";
|
||||||
|
|
||||||
|
const data = {
|
||||||
|
navMain: [
|
||||||
|
{
|
||||||
|
title: "Preferences",
|
||||||
|
url: "/preferences",
|
||||||
|
icon: Settings2,
|
||||||
|
items: [
|
||||||
|
{
|
||||||
|
title: "Appearance",
|
||||||
|
url: "/preferences/appearance",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Behaviour",
|
||||||
|
url: "/preferences/behaviour",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Emojis",
|
||||||
|
url: "/preferences/emojis",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Roles",
|
||||||
|
url: "/preferences/roles",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
other: [
|
||||||
|
{
|
||||||
|
name: "Home",
|
||||||
|
url: "/home",
|
||||||
|
icon: House,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Public",
|
||||||
|
url: "/public",
|
||||||
|
icon: MapIcon,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Local",
|
||||||
|
url: "/local",
|
||||||
|
icon: BedSingle,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Global",
|
||||||
|
url: "/global",
|
||||||
|
icon: Globe,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Notifications",
|
||||||
|
url: "/notifications",
|
||||||
|
icon: Bell,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
const instance = useInstance();
|
||||||
|
const { $pwa } = useNuxtApp();
|
||||||
|
</script>
|
||||||
|
|
@ -1,223 +0,0 @@
|
||||||
<template>
|
|
||||||
<aside
|
|
||||||
class="fixed h-dvh z-10 md:flex hidden flex-col p-4 bg-dark-800 gap-10 max-w-[80px] hover:max-w-72 w-full duration-200 group ring-1 ring-dark-500"
|
|
||||||
aria-label="Navigation" role="complementary">
|
|
||||||
<NuxtLink href="/">
|
|
||||||
<img crossorigin="anonymous" class="size-11 rounded ring-1 ring-white/10 hover:scale-105 duration-200"
|
|
||||||
:src="instance?.thumbnail.url ?? 'https://cdn.versia.pub/branding/icon.svg'"
|
|
||||||
alt="Logo of your instance" />
|
|
||||||
</NuxtLink>
|
|
||||||
|
|
||||||
<div class="flex flex-col gap-3">
|
|
||||||
<h3 class="font-semibold text-gray-300 text-xs uppercase opacity-0 group-hover:opacity-100 duration-200">
|
|
||||||
Timelines</h3>
|
|
||||||
|
|
||||||
<NuxtLink v-for="timeline in visibleTimelines" :key="timeline.href" :to="timeline.href">
|
|
||||||
<ButtonBase theme="ghost" class="w-full !justify-start overflow-hidden rounded-sm">
|
|
||||||
<Icon :icon="timeline.icon" class="!size-6" />
|
|
||||||
<span class="shrink-0 line-clamp-1">{{ timeline.name }}</span>
|
|
||||||
</ButtonBase>
|
|
||||||
</NuxtLink>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="flex flex-col gap-3 mt-auto">
|
|
||||||
<h3 class="font-semibold text-gray-300 text-xs uppercase opacity-0 group-hover:opacity-100 duration-200">
|
|
||||||
Account</h3>
|
|
||||||
|
|
||||||
<AccountPicker @sign-in="signIn().finally(() => loadingAuth = false)"
|
|
||||||
@sign-out="id => signOut(id).finally(() => loadingAuth = false)" />
|
|
||||||
<NuxtLink href="/register" v-if="!identity">
|
|
||||||
<ButtonBase theme="ghost" class="w-full !justify-start overflow-hidden rounded-sm">
|
|
||||||
<Icon icon="tabler:certificate" class="!size-6" />
|
|
||||||
<span class="shrink-0 line-clamp-1">Register</span>
|
|
||||||
</ButtonBase>
|
|
||||||
</NuxtLink>
|
|
||||||
<NuxtLink href="/settings" v-if="identity">
|
|
||||||
<ButtonBase @click="$emit('signIn')" theme="secondary" class="w-full !justify-start overflow-hidden">
|
|
||||||
<Icon icon="tabler:adjustments" class="!size-6" />
|
|
||||||
<span class="shrink-0 line-clamp-1">Settings</span>
|
|
||||||
</ButtonBase>
|
|
||||||
</NuxtLink>
|
|
||||||
<h3 v-if="identity"
|
|
||||||
class="font-semibold text-gray-300 text-xs uppercase opacity-0 group-hover:opacity-100 duration-200">
|
|
||||||
Posts</h3>
|
|
||||||
<ButtonBase v-if="identity" @click="compose" title="Open composer (shortcut: n)" theme="gradient"
|
|
||||||
class="!justify-start overflow-hidden">
|
|
||||||
<Icon icon="tabler:writing" class="!size-6" />
|
|
||||||
<span class="shrink-0 line-clamp-1">Compose</span>
|
|
||||||
<kbd class="text-xs font-semibold rounded bg-dark-500 font-mono px-1 flex flex-row ml-auto">
|
|
||||||
<iconify-icon icon="tabler:keyboard" height="1rem" width="1rem" class="inline" aria-hidden="true" />
|
|
||||||
<iconify-icon icon="tabler:letter-n-small" height="1rem" width="1rem" class="inline -mr-1"
|
|
||||||
aria-hidden="true" />
|
|
||||||
</kbd>
|
|
||||||
</ButtonBase>
|
|
||||||
<ButtonBase v-if="$pwa?.needRefresh" @click="$pwa?.updateServiceWorker()" title="Update service worker"
|
|
||||||
theme="primary" class="w-full !justify-start overflow-hidden">
|
|
||||||
<Icon icon="tabler:refresh" class="!size-6" />
|
|
||||||
<span class="shrink-0 line-clamp-1">Update</span>
|
|
||||||
</ButtonBase>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
</aside>
|
|
||||||
<!-- Mobile bottom navbar -->
|
|
||||||
<nav
|
|
||||||
:class="['fixed bottom-0 left-0 right-0 z-20 h-16 md:hidden grid gap-3 p-2 *:shadow-xl bg-dark-900 ring-1 ring-white/10 text-gray-200', !!identity ? 'grid-cols-4' : 'grid-cols-3']">
|
|
||||||
|
|
||||||
<AdaptiveDropdown>
|
|
||||||
<template #button>
|
|
||||||
<ButtonMobileNavbar icon="tabler:home" text="Timelines" />
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<template #items>
|
|
||||||
<Menu.Item value="" v-for="timeline in visibleTimelines" :key="timeline.href">
|
|
||||||
<NuxtLink :href="timeline.href">
|
|
||||||
<ButtonDropdown :icon="timeline.icon" class="w-full">
|
|
||||||
{{ timeline.name }}
|
|
||||||
</ButtonDropdown>
|
|
||||||
</NuxtLink>
|
|
||||||
</Menu.Item>
|
|
||||||
</template>
|
|
||||||
</AdaptiveDropdown>
|
|
||||||
<NuxtLink href="/notifications" class="w-full">
|
|
||||||
<ButtonMobileNavbar icon="tabler:bell" text="Notifications" />
|
|
||||||
</NuxtLink>
|
|
||||||
<ButtonMobileNavbar v-if="$pwa?.needRefresh" @click="$pwa?.updateServiceWorker(true)" icon="tabler:refresh"
|
|
||||||
text="Update" />
|
|
||||||
<AccountPicker v-else @sign-in="signIn().finally(() => loadingAuth = false)"
|
|
||||||
@sign-out="id => signOut(id).finally(() => loadingAuth = false)">
|
|
||||||
<ButtonMobileNavbar icon="tabler:user" text="Account" />
|
|
||||||
</AccountPicker>
|
|
||||||
<button @click="compose" v-if="identity"
|
|
||||||
class="flex flex-col items-center justify-center p-2 rounded bg-gradient-to-tr from-[theme(colors.primary.300/70%)] via-purple-300/70 to-indigo-400/70">
|
|
||||||
<iconify-icon icon="tabler:writing" class="text-2xl" />
|
|
||||||
<span class="text-xs hidden md:inline">Compose</span>
|
|
||||||
</button>
|
|
||||||
</nav>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script lang="ts" setup>
|
|
||||||
import { Menu } from "@ark-ui/vue";
|
|
||||||
import ButtonBase from "~/packages/ui/components/buttons/button.vue";
|
|
||||||
import Icon from "~/packages/ui/components/icons/icon.vue";
|
|
||||||
import ButtonDropdown from "../buttons/button-dropdown.vue";
|
|
||||||
import ButtonMobileNavbar from "../buttons/button-mobile-navbar.vue";
|
|
||||||
import AdaptiveDropdown from "../dropdowns/AdaptiveDropdown.vue";
|
|
||||||
import AccountPicker from "./account-picker.vue";
|
|
||||||
const { $pwa } = useNuxtApp();
|
|
||||||
|
|
||||||
const timelines = ref([
|
|
||||||
{
|
|
||||||
href: "/home",
|
|
||||||
name: "Home",
|
|
||||||
icon: "tabler:home",
|
|
||||||
requiresAuth: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
href: "/public",
|
|
||||||
name: "Public",
|
|
||||||
icon: "tabler:world",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
href: "/local",
|
|
||||||
name: "Local",
|
|
||||||
icon: "tabler:home",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
href: "/notifications",
|
|
||||||
name: "Notifications",
|
|
||||||
icon: "tabler:bell",
|
|
||||||
requiresAuth: true,
|
|
||||||
},
|
|
||||||
]);
|
|
||||||
|
|
||||||
const visibleTimelines = computed(() =>
|
|
||||||
timelines.value.filter(
|
|
||||||
(timeline) => !timeline.requiresAuth || identity.value,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
const loadingAuth = ref(false);
|
|
||||||
|
|
||||||
const appData = useAppData();
|
|
||||||
const instance = useInstance();
|
|
||||||
|
|
||||||
const compose = () => {
|
|
||||||
useEvent("composer:open");
|
|
||||||
};
|
|
||||||
|
|
||||||
const signIn = async () => {
|
|
||||||
loadingAuth.value = true;
|
|
||||||
|
|
||||||
const output = await client.value.createApp("Versia", {
|
|
||||||
scopes: ["read", "write", "follow", "push"],
|
|
||||||
redirect_uris: new URL("/", useRequestURL().origin).toString(),
|
|
||||||
website: useBaseUrl().value,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!output?.data) {
|
|
||||||
alert("Failed to create app");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
appData.value = output.data;
|
|
||||||
|
|
||||||
const url = await client.value.generateAuthUrl(
|
|
||||||
output.data.client_id,
|
|
||||||
output.data.client_secret,
|
|
||||||
{
|
|
||||||
scopes: ["read", "write", "follow", "push"],
|
|
||||||
redirect_uri: new URL("/", useRequestURL().origin).toString(),
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!url) {
|
|
||||||
alert("Failed to generate auth URL");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
window.location.href = url;
|
|
||||||
};
|
|
||||||
|
|
||||||
const signOut = async (id?: string) => {
|
|
||||||
loadingAuth.value = true;
|
|
||||||
|
|
||||||
if (!(appData.value && identity.value)) {
|
|
||||||
console.error("No app or identity data to sign out");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const identityToRevoke = id
|
|
||||||
? identities.value.find((i) => i.id === id)
|
|
||||||
: identity.value;
|
|
||||||
|
|
||||||
if (!identityToRevoke) {
|
|
||||||
console.error("No identity to revoke");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Don't do anything on error, as Versia Server doesn't implement the revoke endpoint yet
|
|
||||||
await client.value
|
|
||||||
?.revokeToken(
|
|
||||||
appData.value.client_id,
|
|
||||||
identityToRevoke.tokens.access_token,
|
|
||||||
identityToRevoke.tokens.access_token,
|
|
||||||
)
|
|
||||||
.catch(() => {
|
|
||||||
// Do nothing
|
|
||||||
});
|
|
||||||
|
|
||||||
if (id === identity.value.id) {
|
|
||||||
identity.value = null;
|
|
||||||
await navigateTo("/");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
identities.value = identities.value.filter((i) => i.id !== id);
|
|
||||||
await useEvent("notification:new", {
|
|
||||||
type: "success",
|
|
||||||
title: "Signed out",
|
|
||||||
description: "Account signed out successfully",
|
|
||||||
});
|
|
||||||
};
|
|
||||||
</script>
|
|
||||||
13
components/sidebars/right-sidebar.vue
Normal file
13
components/sidebars/right-sidebar.vue
Normal file
|
|
@ -0,0 +1,13 @@
|
||||||
|
<template>
|
||||||
|
<Sidebar variant="inset" collapsible="none" side="right" class="[--sidebar-width:24rem] hidden lg:flex">
|
||||||
|
<SidebarContent class="p-2 overflow-y-auto">
|
||||||
|
<NotificationsTimeline />
|
||||||
|
</SidebarContent>
|
||||||
|
<SidebarRail />
|
||||||
|
</Sidebar>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import NotificationsTimeline from "../timelines/notifications.vue";
|
||||||
|
import { Sidebar, SidebarContent, SidebarRail } from "../ui/sidebar";
|
||||||
|
</script>
|
||||||
|
|
@ -1,18 +1,4 @@
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import {
|
|
||||||
BadgeCheck,
|
|
||||||
BedSingle,
|
|
||||||
Bell,
|
|
||||||
ChevronRight,
|
|
||||||
ChevronsUpDown,
|
|
||||||
Globe,
|
|
||||||
House,
|
|
||||||
LogOut,
|
|
||||||
MapIcon,
|
|
||||||
Pen,
|
|
||||||
Settings2,
|
|
||||||
} from "lucide-vue-next";
|
|
||||||
import { Avatar, AvatarFallback, AvatarImage } from "~/components/ui/avatar";
|
|
||||||
import {
|
import {
|
||||||
Breadcrumb,
|
Breadcrumb,
|
||||||
BreadcrumbItem,
|
BreadcrumbItem,
|
||||||
|
|
@ -21,235 +7,19 @@ import {
|
||||||
BreadcrumbPage,
|
BreadcrumbPage,
|
||||||
BreadcrumbSeparator,
|
BreadcrumbSeparator,
|
||||||
} from "~/components/ui/breadcrumb";
|
} from "~/components/ui/breadcrumb";
|
||||||
import {
|
|
||||||
Collapsible,
|
|
||||||
CollapsibleContent,
|
|
||||||
CollapsibleTrigger,
|
|
||||||
} from "~/components/ui/collapsible";
|
|
||||||
import {
|
|
||||||
DropdownMenu,
|
|
||||||
DropdownMenuContent,
|
|
||||||
DropdownMenuGroup,
|
|
||||||
DropdownMenuItem,
|
|
||||||
DropdownMenuLabel,
|
|
||||||
DropdownMenuSeparator,
|
|
||||||
DropdownMenuTrigger,
|
|
||||||
} from "~/components/ui/dropdown-menu";
|
|
||||||
import { Separator } from "~/components/ui/separator";
|
import { Separator } from "~/components/ui/separator";
|
||||||
import {
|
import {
|
||||||
Sidebar,
|
|
||||||
SidebarContent,
|
|
||||||
SidebarFooter,
|
|
||||||
SidebarGroup,
|
|
||||||
SidebarGroupLabel,
|
|
||||||
SidebarHeader,
|
|
||||||
SidebarInset,
|
SidebarInset,
|
||||||
SidebarMenu,
|
|
||||||
SidebarMenuButton,
|
|
||||||
SidebarMenuItem,
|
|
||||||
SidebarMenuSub,
|
|
||||||
SidebarMenuSubButton,
|
|
||||||
SidebarMenuSubItem,
|
|
||||||
SidebarProvider,
|
SidebarProvider,
|
||||||
SidebarRail,
|
|
||||||
SidebarTrigger,
|
SidebarTrigger,
|
||||||
} from "~/components/ui/sidebar";
|
} from "~/components/ui/sidebar";
|
||||||
import NotificationsTimeline from "../timelines/notifications.vue";
|
import LeftSidebar from "./left-sidebar.vue";
|
||||||
import { Button } from "../ui/button";
|
import RightSidebar from "./right-sidebar.vue";
|
||||||
import ThemeSwitcher from "./theme-switcher.vue";
|
|
||||||
|
|
||||||
const data = {
|
|
||||||
navMain: [
|
|
||||||
{
|
|
||||||
title: "Preferences",
|
|
||||||
url: "/preferences",
|
|
||||||
icon: Settings2,
|
|
||||||
items: [
|
|
||||||
{
|
|
||||||
title: "Appearance",
|
|
||||||
url: "/preferences/appearance",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: "Behaviour",
|
|
||||||
url: "/preferences/behaviour",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: "Emojis",
|
|
||||||
url: "/preferences/emojis",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: "Roles",
|
|
||||||
url: "/preferences/roles",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
other: [
|
|
||||||
{
|
|
||||||
name: "Home",
|
|
||||||
url: "/home",
|
|
||||||
icon: House,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Public",
|
|
||||||
url: "/public",
|
|
||||||
icon: MapIcon,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Local",
|
|
||||||
url: "/local",
|
|
||||||
icon: BedSingle,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Global",
|
|
||||||
url: "/global",
|
|
||||||
icon: Globe,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Notifications",
|
|
||||||
url: "/notifications",
|
|
||||||
icon: Bell,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
};
|
|
||||||
|
|
||||||
const instance = useInstance();
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<SidebarProvider>
|
<SidebarProvider>
|
||||||
<Sidebar variant="inset" collapsible="icon">
|
<LeftSidebar />
|
||||||
<SidebarHeader>
|
|
||||||
<SidebarMenu>
|
|
||||||
<SidebarMenuItem>
|
|
||||||
<NuxtLink href="/">
|
|
||||||
<SidebarMenuButton size="lg"
|
|
||||||
class="data-[state=open]:bg-sidebar-accent data-[state=open]:text-sidebar-accent-foreground">
|
|
||||||
<Avatar shape="square" class="size-8">
|
|
||||||
<AvatarImage :src="instance?.thumbnail.url ??
|
|
||||||
'https://cdn.versia.pub/branding/icon.svg'
|
|
||||||
" alt="" />
|
|
||||||
</Avatar>
|
|
||||||
<div class="grid flex-1 text-left text-sm leading-tight">
|
|
||||||
<span class="truncate font-semibold">{{ instance?.title ?? 'Versia Server' }}</span>
|
|
||||||
<span class="truncate text-xs">{{ "A Versia Server instance" }}</span>
|
|
||||||
</div>
|
|
||||||
<!-- <ChevronsUpDown class="ml-auto" /> -->
|
|
||||||
</SidebarMenuButton>
|
|
||||||
</NuxtLink>
|
|
||||||
</SidebarMenuItem>
|
|
||||||
</SidebarMenu>
|
|
||||||
</SidebarHeader>
|
|
||||||
<SidebarContent>
|
|
||||||
<SidebarGroup>
|
|
||||||
<SidebarGroupLabel>Navigation</SidebarGroupLabel>
|
|
||||||
<SidebarMenu>
|
|
||||||
<SidebarMenuItem v-for="item in data.other" :key="item.name">
|
|
||||||
<SidebarMenuButton as-child>
|
|
||||||
<NuxtLink :href="item.url">
|
|
||||||
<component :is="item.icon" />
|
|
||||||
<span>{{ item.name }}</span>
|
|
||||||
</NuxtLink>
|
|
||||||
</SidebarMenuButton>
|
|
||||||
</SidebarMenuItem>
|
|
||||||
</SidebarMenu>
|
|
||||||
</SidebarGroup>
|
|
||||||
<SidebarGroup class="mt-auto">
|
|
||||||
<SidebarGroupLabel>More</SidebarGroupLabel>
|
|
||||||
<SidebarMenu>
|
|
||||||
<Collapsible v-for="item in data.navMain" :key="item.title" as-child
|
|
||||||
:default-open="item.isActive" class="group/collapsible">
|
|
||||||
<SidebarMenuItem>
|
|
||||||
<CollapsibleTrigger as-child>
|
|
||||||
<SidebarMenuButton :tooltip="item.title">
|
|
||||||
<component :is="item.icon" />
|
|
||||||
<span>{{ item.title }}</span>
|
|
||||||
<ChevronRight
|
|
||||||
class="ml-auto transition-transform duration-200 group-data-[state=open]/collapsible:rotate-90" />
|
|
||||||
</SidebarMenuButton>
|
|
||||||
</CollapsibleTrigger>
|
|
||||||
<CollapsibleContent>
|
|
||||||
<SidebarMenuSub>
|
|
||||||
<SidebarMenuSubItem v-for="subItem in item.items" :key="subItem.title">
|
|
||||||
<SidebarMenuSubButton as-child>
|
|
||||||
<NuxtLink :href="subItem.url">
|
|
||||||
<span>{{ subItem.title }}</span>
|
|
||||||
</NuxtLink>
|
|
||||||
</SidebarMenuSubButton>
|
|
||||||
</SidebarMenuSubItem>
|
|
||||||
</SidebarMenuSub>
|
|
||||||
</CollapsibleContent>
|
|
||||||
</SidebarMenuItem>
|
|
||||||
</Collapsible>
|
|
||||||
</SidebarMenu>
|
|
||||||
</SidebarGroup>
|
|
||||||
</SidebarContent>
|
|
||||||
<SidebarFooter>
|
|
||||||
<SidebarMenu class="gap-3">
|
|
||||||
<SidebarMenuItem>
|
|
||||||
<ThemeSwitcher />
|
|
||||||
</SidebarMenuItem>
|
|
||||||
<SidebarMenuItem>
|
|
||||||
<DropdownMenu>
|
|
||||||
<DropdownMenuTrigger as-child>
|
|
||||||
<SidebarMenuButton size="lg"
|
|
||||||
class="data-[state=open]:bg-sidebar-accent data-[state=open]:text-sidebar-accent-foreground">
|
|
||||||
<Avatar shape="square" class="size-8">
|
|
||||||
<AvatarImage :src="identity?.account.avatar" alt="" />
|
|
||||||
<AvatarFallback class="rounded-lg"> AA </AvatarFallback>
|
|
||||||
</Avatar>
|
|
||||||
<div class="grid flex-1 text-left text-sm leading-tight">
|
|
||||||
<span class="truncate font-semibold" v-render-emojis="identity?.account.emojis">{{
|
|
||||||
identity?.account.display_name
|
|
||||||
}}</span>
|
|
||||||
<span class="truncate text-xs">@{{ identity?.account.acct }}</span>
|
|
||||||
</div>
|
|
||||||
<ChevronsUpDown class="ml-auto size-4" />
|
|
||||||
</SidebarMenuButton>
|
|
||||||
</DropdownMenuTrigger>
|
|
||||||
<DropdownMenuContent class="w-[--radix-dropdown-menu-trigger-width] min-w-56 rounded-lg"
|
|
||||||
side="bottom" align="end" :side-offset="4">
|
|
||||||
<DropdownMenuLabel class="p-0 font-normal">
|
|
||||||
<div class="flex items-center gap-2 px-1 py-1.5 text-left text-sm">
|
|
||||||
<Avatar shape="square" class="size-8">
|
|
||||||
<AvatarImage :src="identity?.account.avatar" alt="" />
|
|
||||||
<AvatarFallback class="rounded-lg"> AA </AvatarFallback>
|
|
||||||
</Avatar>
|
|
||||||
<div class="grid flex-1 text-left text-sm leading-tight">
|
|
||||||
<span class="truncate font-semibold" v-render-emojis="identity?.account.emojis">{{
|
|
||||||
identity?.account.display_name
|
|
||||||
}}</span>
|
|
||||||
<span class="truncate text-xs">@{{
|
|
||||||
identity?.account.acct
|
|
||||||
}}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</DropdownMenuLabel>
|
|
||||||
<DropdownMenuSeparator />
|
|
||||||
<DropdownMenuGroup>
|
|
||||||
<DropdownMenuItem>
|
|
||||||
<BadgeCheck />
|
|
||||||
Account
|
|
||||||
</DropdownMenuItem>
|
|
||||||
</DropdownMenuGroup>
|
|
||||||
<DropdownMenuSeparator />
|
|
||||||
<DropdownMenuItem>
|
|
||||||
<LogOut />
|
|
||||||
Log out
|
|
||||||
</DropdownMenuItem>
|
|
||||||
</DropdownMenuContent>
|
|
||||||
</DropdownMenu>
|
|
||||||
</SidebarMenuItem>
|
|
||||||
<SidebarMenuItem>
|
|
||||||
<Button variant="default" size="lg" class="w-full group-data-[collapsible=icon]:px-4" @click="useEvent('composer:open')">
|
|
||||||
<Pen />
|
|
||||||
<span class="group-data-[collapsible=icon]:hidden">Compose</span>
|
|
||||||
</Button>
|
|
||||||
</SidebarMenuItem>
|
|
||||||
</SidebarMenu>
|
|
||||||
</SidebarFooter>
|
|
||||||
<SidebarRail />
|
|
||||||
</Sidebar>
|
|
||||||
<SidebarInset>
|
<SidebarInset>
|
||||||
<header
|
<header
|
||||||
class="flex h-16 shrink-0 items-center gap-2 transition-[width,height] ease-linear group-has-[[data-collapsible=icon]]/sidebar-wrapper:h-12 overflow-hidden">
|
class="flex h-16 shrink-0 items-center gap-2 transition-[width,height] ease-linear group-has-[[data-collapsible=icon]]/sidebar-wrapper:h-12 overflow-hidden">
|
||||||
|
|
@ -275,11 +45,6 @@ const instance = useInstance();
|
||||||
<slot />
|
<slot />
|
||||||
</div>
|
</div>
|
||||||
</SidebarInset>
|
</SidebarInset>
|
||||||
<Sidebar variant="inset" collapsible="none" side="right" class="[--sidebar-width:24rem] hidden lg:flex">
|
<RightSidebar />
|
||||||
<SidebarContent class="p-2 overflow-y-auto">
|
|
||||||
<NotificationsTimeline />
|
|
||||||
</SidebarContent>
|
|
||||||
<SidebarRail />
|
|
||||||
</Sidebar>
|
|
||||||
</SidebarProvider>
|
</SidebarProvider>
|
||||||
</template>
|
</template>
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,6 @@ export default defineNuxtConfig({
|
||||||
modules: [
|
modules: [
|
||||||
"@nuxtjs/tailwindcss",
|
"@nuxtjs/tailwindcss",
|
||||||
"@vueuse/nuxt",
|
"@vueuse/nuxt",
|
||||||
"nuxt-headlessui",
|
|
||||||
"@nuxt/fonts",
|
"@nuxt/fonts",
|
||||||
"@vee-validate/nuxt",
|
"@vee-validate/nuxt",
|
||||||
"nuxt-security",
|
"nuxt-security",
|
||||||
|
|
|
||||||
|
|
@ -51,7 +51,6 @@
|
||||||
"mitt": "^3.0.1",
|
"mitt": "^3.0.1",
|
||||||
"nanoid": "^5.0.9",
|
"nanoid": "^5.0.9",
|
||||||
"nuxt": "^3.14.1592",
|
"nuxt": "^3.14.1592",
|
||||||
"nuxt-headlessui": "^1.2.0",
|
|
||||||
"nuxt-security": "^2.1.4",
|
"nuxt-security": "^2.1.4",
|
||||||
"nuxt-shiki": "^0.3.0",
|
"nuxt-shiki": "^0.3.0",
|
||||||
"overlayscrollbars": "^2.10.1",
|
"overlayscrollbars": "^2.10.1",
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue