feat(notifications): added notification system w/ interwoven refactoring
This commit is contained in:
@@ -1,17 +0,0 @@
|
||||
<template>
|
||||
<div class="transition inline-flex items-center justify-center cursor-pointer rounded-sm px-4 py-2 bg-zinc-900 text-zinc-600 hover:bg-zinc-800 hover:text-zinc-300 relative">
|
||||
<slot />
|
||||
<div v-if="props.notifications !== undefined"
|
||||
class="text-zinc-900 absolute top-0 right-0 translate-x-[30%] translate-y-[-30%] text-xs bg-blue-300 rounded-full w-3.5 h-3.5 text-center">
|
||||
{{ props.notifications }}
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
const props = defineProps<{
|
||||
notifications?: number
|
||||
}>();
|
||||
|
||||
|
||||
</script>
|
||||
@@ -0,0 +1,57 @@
|
||||
<template>
|
||||
<div class="pointer-events-auto w-full bg-zinc-950/40 rounded">
|
||||
<div class="p-4">
|
||||
<div class="flex items-start">
|
||||
<div class="w-0 flex-1 pt-0.5">
|
||||
<p class="text-sm font-medium text-zinc-100">
|
||||
{{ notification.title }}
|
||||
</p>
|
||||
<p class="mt-1 text-sm text-zinc-600 line-clamp-3">
|
||||
{{ notification.description }}
|
||||
</p>
|
||||
<div
|
||||
v-if="notification.actions.length > 0"
|
||||
class="mt-3 flex space-x-7"
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
class="rounded-md bg-white text-sm font-medium text-blue-600 hover:text-blue-500 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2"
|
||||
>
|
||||
Undo
|
||||
</button>
|
||||
<!-- todo -->
|
||||
</div>
|
||||
</div>
|
||||
<div class="ml-4 flex shrink-0">
|
||||
<button
|
||||
@click="() => deleteMe()"
|
||||
type="button"
|
||||
class="inline-flex rounded-md text-zinc-400 hover:text-zinc-500 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2"
|
||||
>
|
||||
<span class="sr-only">Close</span>
|
||||
<XMarkIcon class="size-5" aria-hidden="true" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { XMarkIcon } from "@heroicons/vue/24/solid";
|
||||
import type { Notification } from "@prisma/client";
|
||||
|
||||
const props = defineProps<{ notification: Notification }>();
|
||||
|
||||
async function deleteMe() {
|
||||
await $fetch(`/api/v1/notifications/${props.notification.id}`, {
|
||||
method: "DELETE",
|
||||
});
|
||||
const notifications = useNotifications();
|
||||
const indexOfMe = notifications.value.findIndex(
|
||||
(e) => e.id === props.notification.id
|
||||
);
|
||||
// Delete me
|
||||
notifications.value.splice(indexOfMe, 1);
|
||||
}
|
||||
</script>
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<div class="flex rounded-sm px-2 py-2 bg-zinc-900 text-zinc-600">
|
||||
<div class="flex rounded px-2 py-2 bg-zinc-900 text-zinc-600">
|
||||
<slot />
|
||||
</div>
|
||||
</template>
|
||||
@@ -21,15 +21,39 @@
|
||||
</div>
|
||||
<div class="inline-flex items-center">
|
||||
<ol class="inline-flex gap-3">
|
||||
<li v-for="(item, itemIdx) in quickActions">
|
||||
<HeaderWidget
|
||||
@click="item.action"
|
||||
:notifications="item.notifications"
|
||||
>
|
||||
<component class="h-5" :is="item.icon" />
|
||||
</HeaderWidget>
|
||||
<li>
|
||||
<UserHeaderWidget>
|
||||
<UserGroupIcon class="h-5" />
|
||||
</UserHeaderWidget>
|
||||
</li>
|
||||
<HeaderUserWidget />
|
||||
|
||||
<li>
|
||||
<Menu as="div" class="relative inline-block">
|
||||
<MenuButton>
|
||||
<UserHeaderWidget :notifications="unreadNotifications.length">
|
||||
<BellIcon class="h-5" />
|
||||
</UserHeaderWidget>
|
||||
</MenuButton>
|
||||
|
||||
<transition
|
||||
enter-active-class="transition ease-out duration-100"
|
||||
enter-from-class="transform opacity-0 scale-95"
|
||||
enter-to-class="transform opacity-100 scale-100"
|
||||
leave-active-class="transition ease-in duration-75"
|
||||
leave-from-class="transform opacity-100 scale-100"
|
||||
leave-to-class="transform opacity-0 scale-95"
|
||||
>
|
||||
<MenuItems
|
||||
class="absolute inset-x-0 -translate-x-1/2 top-10 z-50 w-96 focus:outline-none shadow-md"
|
||||
>
|
||||
<UserHeaderNotificationWidgetPanel
|
||||
:notifications="unreadNotifications"
|
||||
/>
|
||||
</MenuItems>
|
||||
</transition>
|
||||
</Menu>
|
||||
</li>
|
||||
<UserHeaderUserWidget />
|
||||
</ol>
|
||||
</div>
|
||||
</div>
|
||||
@@ -41,7 +65,7 @@
|
||||
<div class="flex gap-x-4 lg:gap-x-6">
|
||||
<div class="flex items-center gap-x-3">
|
||||
<!-- Profile dropdown -->
|
||||
<HeaderUserWidget />
|
||||
<UserHeaderUserWidget />
|
||||
|
||||
<button
|
||||
type="button"
|
||||
@@ -111,7 +135,7 @@
|
||||
<Logo class="h-8 w-auto" />
|
||||
</NuxtLink>
|
||||
|
||||
<HeaderUserWidget />
|
||||
<UserHeaderUserWidget />
|
||||
</div>
|
||||
<nav class="flex flex-1 flex-col gap-y-8">
|
||||
<ol class="flex flex-col gap-y-3">
|
||||
@@ -130,14 +154,18 @@
|
||||
</ol>
|
||||
<div class="h-px w-full bg-zinc-700" />
|
||||
<div class="flex flex-row gap-x-4 justify-stretch">
|
||||
<li class="w-full" v-for="(item, itemIdx) in quickActions">
|
||||
<HeaderWidget
|
||||
class="w-full"
|
||||
@click="item.action"
|
||||
:notifications="item.notifications"
|
||||
<li class="w-full">
|
||||
<UserHeaderWidget class="w-full">
|
||||
<UserGroupIcon class="h-5" />
|
||||
</UserHeaderWidget>
|
||||
</li>
|
||||
<li class="w-full">
|
||||
<UserHeaderWidget
|
||||
class="w-full"
|
||||
:notifications="unreadNotifications.length"
|
||||
>
|
||||
<component class="h-5" :is="item.icon" />
|
||||
</HeaderWidget>
|
||||
<BellIcon class="h-5" />
|
||||
</UserHeaderWidget>
|
||||
</li>
|
||||
</div>
|
||||
</nav>
|
||||
@@ -156,9 +184,11 @@ import {
|
||||
DialogPanel,
|
||||
TransitionChild,
|
||||
TransitionRoot,
|
||||
Menu,
|
||||
MenuButton,
|
||||
MenuItems,
|
||||
} from "@headlessui/vue";
|
||||
import type { NavigationItem, QuickActionNav } from "../composables/types";
|
||||
import HeaderWidget from "./HeaderWidget.vue";
|
||||
import type { NavigationItem } from "../composables/types";
|
||||
import { Bars3Icon } from "@heroicons/vue/24/outline";
|
||||
import { XMarkIcon } from "@heroicons/vue/24/solid";
|
||||
|
||||
@@ -189,16 +219,10 @@ const navigation: Array<NavigationItem> = [
|
||||
|
||||
const currentPageIndex = useCurrentNavigationIndex(navigation);
|
||||
|
||||
const quickActions: Array<QuickActionNav> = [
|
||||
{
|
||||
icon: UserGroupIcon,
|
||||
action: async () => {},
|
||||
},
|
||||
{
|
||||
icon: BellIcon,
|
||||
action: async () => {},
|
||||
},
|
||||
];
|
||||
const notifications = useNotifications();
|
||||
const unreadNotifications = computed(() =>
|
||||
notifications.value.filter((e) => !e.read)
|
||||
);
|
||||
|
||||
const sidebarOpen = ref(false);
|
||||
router.afterEach(() => (sidebarOpen.value = false));
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
<template>
|
||||
<PanelWidget class="flex-col gap-y-2">
|
||||
<div class="border-b border-zinc-700 pb-3 p-2">
|
||||
<div
|
||||
class="-ml-4 -mt-2 flex flex-wrap items-center justify-between sm:flex-nowrap"
|
||||
>
|
||||
<div class="ml-4 mt-2">
|
||||
<h3 class="text-base font-semibold text-zinc-100 text-sm">
|
||||
Unread notifications
|
||||
</h3>
|
||||
</div>
|
||||
<div class="ml-4 mt-2 shrink-0">
|
||||
<NuxtLink
|
||||
to="/account/notifications"
|
||||
type="button"
|
||||
class="text-sm text-zinc-500"
|
||||
>
|
||||
View all →
|
||||
</NuxtLink>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col gap-y-2 max-h-[300px] overflow-y-scroll">
|
||||
<Notification
|
||||
v-for="notification in props.notifications"
|
||||
:notification="notification"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
v-if="props.notifications.length == 0"
|
||||
class="text-sm text-zinc-600 p-3 text-center w-full"
|
||||
>
|
||||
No notifications
|
||||
</div>
|
||||
</PanelWidget>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { Notification } from "@prisma/client";
|
||||
|
||||
const props = defineProps<{ notifications: Array<Notification> }>();
|
||||
</script>
|
||||
+3
-4
@@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<Menu v-if="user" as="div" class="relative inline-block">
|
||||
<MenuButton>
|
||||
<HeaderWidget>
|
||||
<UserHeaderWidget>
|
||||
<div class="inline-flex items-center text-zinc-300 hover:text-white">
|
||||
<img
|
||||
:src="useObject(user.profilePicture)"
|
||||
@@ -10,7 +10,7 @@
|
||||
<span class="ml-2 text-sm font-bold">{{ user.displayName }}</span>
|
||||
<ChevronDownIcon class="ml-3 h-4" />
|
||||
</div>
|
||||
</HeaderWidget>
|
||||
</UserHeaderWidget>
|
||||
</MenuButton>
|
||||
|
||||
<transition
|
||||
@@ -60,9 +60,8 @@
|
||||
<script setup lang="ts">
|
||||
import { Menu, MenuButton, MenuItem, MenuItems } from "@headlessui/vue";
|
||||
import { ChevronDownIcon } from "@heroicons/vue/16/solid";
|
||||
import type { NavigationItem } from "../composables/types";
|
||||
import HeaderWidget from "./HeaderWidget.vue";
|
||||
import { useObject } from "~/composables/objects";
|
||||
import type { NavigationItem } from "~/composables/types";
|
||||
|
||||
const user = useUser();
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
<template>
|
||||
<div
|
||||
:class="[
|
||||
'transition inline-flex items-center justify-center cursor-pointer rounded-sm px-4 py-2 relative',
|
||||
showNotifications
|
||||
? 'bg-blue-300 text-zinc-900 hover:bg-blue-200 hover:text-zinc-950'
|
||||
: 'bg-zinc-900 text-zinc-600 hover:bg-zinc-800 hover:text-zinc-300',
|
||||
]"
|
||||
>
|
||||
<slot />
|
||||
<TransitionRoot
|
||||
:show="showNotifications"
|
||||
enter="ease-out duration-150"
|
||||
enter-from="opacity-0"
|
||||
enter-to="opacity-100"
|
||||
leave="ease-in duration-150"
|
||||
leave-from="opacity-100"
|
||||
leave-to="opacity-0"
|
||||
>
|
||||
<div
|
||||
class="w-4 h-4 absolute top-0 right-0 translate-x-[30%] translate-y-[-30%] rounded-full bg-blue-300 inline-flex items-center justify-center text-xs text-zinc-900 font-bold"
|
||||
>
|
||||
{{ props.notifications }}
|
||||
</div>
|
||||
</TransitionRoot>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { TransitionRoot } from "@headlessui/vue";
|
||||
|
||||
const props = defineProps<{
|
||||
notifications?: number;
|
||||
}>();
|
||||
|
||||
const showNotifications = computed(() => !!props.notifications);
|
||||
</script>
|
||||
Reference in New Issue
Block a user