refactor: 🔥 Remove old code

This commit is contained in:
Jesse Wierzbinski 2024-12-02 22:55:36 +01:00
parent 42e0b38fd8
commit 5b3e9ce8b3
No known key found for this signature in database
12 changed files with 371 additions and 651 deletions

View file

@ -10,7 +10,6 @@
<NuxtLayout>
<NuxtPage />
</NuxtLayout>
<NotificationsRenderer />
<ConfirmationModal />
<!-- pointer-events-auto fixes https://github.com/unovue/shadcn-vue/issues/462 -->
<Toaster class="pointer-events-auto" />
@ -24,11 +23,8 @@ import "~/styles/index.css";
import { convert } from "html-to-text";
import "iconify-icon";
import ConfirmationModal from "./components/modals/confirm.vue";
import NotificationsRenderer from "./components/notifications/notifications-renderer.vue";
import { Toaster } from "./components/ui/sonner";
import { SettingIds } from "./settings";
// Use SSR-safe IDs for Headless UI
provideHeadlessUseId(() => useId());
const code = useRequestURL().searchParams.get("code");
const appData = useAppData();

BIN
bun.lockb

Binary file not shown.

View file

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

View file

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

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

View file

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

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

View file

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

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

View file

@ -1,18 +1,4 @@
<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 {
Breadcrumb,
BreadcrumbItem,
@ -21,235 +7,19 @@ import {
BreadcrumbPage,
BreadcrumbSeparator,
} 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 {
Sidebar,
SidebarContent,
SidebarFooter,
SidebarGroup,
SidebarGroupLabel,
SidebarHeader,
SidebarInset,
SidebarMenu,
SidebarMenuButton,
SidebarMenuItem,
SidebarMenuSub,
SidebarMenuSubButton,
SidebarMenuSubItem,
SidebarProvider,
SidebarRail,
SidebarTrigger,
} from "~/components/ui/sidebar";
import NotificationsTimeline from "../timelines/notifications.vue";
import { Button } from "../ui/button";
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();
import LeftSidebar from "./left-sidebar.vue";
import RightSidebar from "./right-sidebar.vue";
</script>
<template>
<SidebarProvider>
<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
: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>
<LeftSidebar />
<SidebarInset>
<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">
@ -275,11 +45,6 @@ const instance = useInstance();
<slot />
</div>
</SidebarInset>
<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>
<RightSidebar />
</SidebarProvider>
</template>

View file

@ -3,7 +3,6 @@ export default defineNuxtConfig({
modules: [
"@nuxtjs/tailwindcss",
"@vueuse/nuxt",
"nuxt-headlessui",
"@nuxt/fonts",
"@vee-validate/nuxt",
"nuxt-security",

View file

@ -51,7 +51,6 @@
"mitt": "^3.0.1",
"nanoid": "^5.0.9",
"nuxt": "^3.14.1592",
"nuxt-headlessui": "^1.2.0",
"nuxt-security": "^2.1.4",
"nuxt-shiki": "^0.3.0",
"overlayscrollbars": "^2.10.1",