add noti
This commit is contained in:
2
components.d.ts
vendored
2
components.d.ts
vendored
@@ -28,6 +28,7 @@ declare module 'vue' {
|
||||
Layout: typeof import('./src/components/icons/Layout.vue')['default']
|
||||
LinkIcon: typeof import('./src/components/icons/LinkIcon.vue')['default']
|
||||
Message: typeof import('primevue/message')['default']
|
||||
NotificationPopover: typeof import('./src/components/NotificationPopover.vue')['default']
|
||||
PageHeader: typeof import('./src/components/dashboard/PageHeader.vue')['default']
|
||||
Password: typeof import('primevue/password')['default']
|
||||
RootLayout: typeof import('./src/components/RootLayout.vue')['default']
|
||||
@@ -61,6 +62,7 @@ declare global {
|
||||
const Layout: typeof import('./src/components/icons/Layout.vue')['default']
|
||||
const LinkIcon: typeof import('./src/components/icons/LinkIcon.vue')['default']
|
||||
const Message: typeof import('primevue/message')['default']
|
||||
const NotificationPopover: typeof import('./src/components/NotificationPopover.vue')['default']
|
||||
const PageHeader: typeof import('./src/components/dashboard/PageHeader.vue')['default']
|
||||
const Password: typeof import('primevue/password')['default']
|
||||
const RootLayout: typeof import('./src/components/RootLayout.vue')['default']
|
||||
|
||||
@@ -29,12 +29,6 @@ export const customFetch = (url: string, options: RequestInit) => {
|
||||
// res.headers.forEach((value, key) => {
|
||||
// c.header(key, value);
|
||||
// });
|
||||
return fetch(apiUrl, options).then(res => {
|
||||
// Forward response headers to client (especially Set-Cookie)
|
||||
res.headers.forEach((value, key) => {
|
||||
c.header(key, value);
|
||||
});
|
||||
return res;
|
||||
});
|
||||
return fetch(apiUrl, options)
|
||||
}
|
||||
}
|
||||
@@ -5,8 +5,9 @@ import Home from "@/components/icons/Home.vue";
|
||||
import Video from "@/components/icons/Video.vue";
|
||||
import Credit from "@/components/icons/Credit.vue";
|
||||
import Upload from "./icons/Upload.vue";
|
||||
import NotificationPopover from "./NotificationPopover.vue";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { createStaticVNode } from "vue";
|
||||
import { createStaticVNode, ref } from "vue";
|
||||
|
||||
const className = ":uno: w-12 h-12 p-2 rounded-2xl hover:bg-primary/15 flex press-animated items-center justify-center";
|
||||
const homeHoist = createStaticVNode(`<img class="h-8 w-8" src="/apple-touch-icon.png" alt="Logo" />`, 1);
|
||||
@@ -20,19 +21,46 @@ const links = [
|
||||
{ href: "/upload", label: "Upload", icon: Upload, type: "a", className },
|
||||
{ href: "/video", label: "Video", icon: Video, type: "a", className },
|
||||
{ href: "/payments-and-plans", label: "Payments & Plans", icon: Credit, type: "a", className },
|
||||
{ href: "/notification", label: "Notification", icon: Bell, type: "a", className },
|
||||
{ href: "#notification", label: "Notification", icon: Bell, type: "notification", className },
|
||||
{ href: "/profile", label: "Profile", icon: profileHoist, type: "a", className: 'w-12 h-12 rounded-2xl hover:bg-primary/15 flex' },
|
||||
];
|
||||
|
||||
const notificationPopover = ref<InstanceType<typeof NotificationPopover>>();
|
||||
|
||||
const handleNotificationClick = (event: Event) => {
|
||||
notificationPopover.value?.toggle(event);
|
||||
};
|
||||
</script>
|
||||
<template>
|
||||
<header
|
||||
class=":uno: fixed left-0 w-18 flex flex-col items-center pt-4 gap-6 z-41 max-h-screen h-screen border-r border-gray-200 bg-white">
|
||||
<component :is="i.type === 'a' ? 'router-link' : 'div'" v-for="i in links" :key="i.label"
|
||||
v-bind="i.type === 'a' ? { to: i.href } : {}" v-tooltip="i.label"
|
||||
:class="cn(i.className, $route.path === i.href && 'bg-primary/15')">
|
||||
<component :is="i.icon" class="w-6 h-6" :filled="$route.path === i.href" />
|
||||
</component>
|
||||
<template v-for="i in links" :key="i.label">
|
||||
<!-- Notification button with popover -->
|
||||
<button
|
||||
v-if="i.type === 'notification'"
|
||||
@click="handleNotificationClick"
|
||||
v-tooltip="i.label"
|
||||
:class="cn(i.className, 'relative')"
|
||||
>
|
||||
<component :is="i.icon" class="w-6 h-6" />
|
||||
<!-- Unread badge -->
|
||||
<span class="absolute top-1 right-1 w-2.5 h-2.5 bg-red-500 rounded-full border-2 border-white"></span>
|
||||
</button>
|
||||
<!-- Regular links -->
|
||||
<component
|
||||
v-else
|
||||
:is="i.type === 'a' ? 'router-link' : 'div'"
|
||||
v-bind="i.type === 'a' ? { to: i.href } : {}"
|
||||
v-tooltip="i.label"
|
||||
:class="cn(i.className, $route.path === i.href && 'bg-primary/15')"
|
||||
>
|
||||
<component :is="i.icon" class="w-6 h-6" :filled="$route.path === i.href" />
|
||||
</component>
|
||||
</template>
|
||||
</header>
|
||||
|
||||
<NotificationPopover ref="notificationPopover" />
|
||||
|
||||
<main class="flex flex-1 overflow-hidden md:ps-18">
|
||||
<div class="flex-1 overflow-auto p-4 bg-white rounded-lg md:(mr-2 mb-2) min-h-[calc(100vh-8rem)]">
|
||||
<router-view v-slot="{ Component }">
|
||||
@@ -48,3 +76,4 @@ const links = [
|
||||
</div>
|
||||
</main>
|
||||
</template>
|
||||
|
||||
|
||||
159
src/components/NotificationPopover.vue
Normal file
159
src/components/NotificationPopover.vue
Normal file
@@ -0,0 +1,159 @@
|
||||
<script setup lang="ts">
|
||||
import Popover from 'primevue/popover';
|
||||
import { computed, ref } from 'vue';
|
||||
import NotificationItem from '@/routes/notification/components/NotificationItem.vue';
|
||||
|
||||
type NotificationType = 'info' | 'success' | 'warning' | 'error' | 'video' | 'payment' | 'system';
|
||||
|
||||
interface Notification {
|
||||
id: string;
|
||||
type: NotificationType;
|
||||
title: string;
|
||||
message: string;
|
||||
time: string;
|
||||
read: boolean;
|
||||
actionUrl?: string;
|
||||
actionLabel?: string;
|
||||
}
|
||||
|
||||
const popover = ref<InstanceType<typeof Popover>>();
|
||||
|
||||
// Mock notifications data
|
||||
const notifications = ref<Notification[]>([
|
||||
{
|
||||
id: '1',
|
||||
type: 'video',
|
||||
title: 'Video processing complete',
|
||||
message: 'Your video "Summer Vacation 2024" has been successfully processed.',
|
||||
time: '2 min ago',
|
||||
read: false,
|
||||
actionUrl: '/video',
|
||||
actionLabel: 'View'
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
type: 'payment',
|
||||
title: 'Payment successful',
|
||||
message: 'Your subscription to Pro Plan has been renewed successfully.',
|
||||
time: '1 hour ago',
|
||||
read: false,
|
||||
actionUrl: '/payments-and-plans',
|
||||
actionLabel: 'Receipt'
|
||||
},
|
||||
{
|
||||
id: '3',
|
||||
type: 'warning',
|
||||
title: 'Storage almost full',
|
||||
message: 'You have used 85% of your storage quota.',
|
||||
time: '3 hours ago',
|
||||
read: false,
|
||||
actionUrl: '/payments-and-plans',
|
||||
actionLabel: 'Upgrade'
|
||||
},
|
||||
{
|
||||
id: '4',
|
||||
type: 'success',
|
||||
title: 'Upload successful',
|
||||
message: 'Your video "Product Demo v2" has been uploaded.',
|
||||
time: '1 day ago',
|
||||
read: true
|
||||
}
|
||||
]);
|
||||
|
||||
const unreadCount = computed(() => notifications.value.filter(n => !n.read).length);
|
||||
|
||||
const toggle = (event: Event) => {
|
||||
popover.value?.toggle(event);
|
||||
};
|
||||
|
||||
const handleMarkRead = (id: string) => {
|
||||
const notification = notifications.value.find(n => n.id === id);
|
||||
if (notification) notification.read = true;
|
||||
};
|
||||
|
||||
const handleDelete = (id: string) => {
|
||||
notifications.value = notifications.value.filter(n => n.id !== id);
|
||||
};
|
||||
|
||||
const handleMarkAllRead = () => {
|
||||
notifications.value.forEach(n => n.read = true);
|
||||
};
|
||||
|
||||
defineExpose({ toggle });
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Popover ref="popover" appendTo="body" :pt="{
|
||||
root: { class: 'notification-popover' },
|
||||
content: { class: 'p-0' }
|
||||
}">
|
||||
<div class="w-[380px] max-h-[480px] flex flex-col">
|
||||
<!-- Header -->
|
||||
<div class="flex items-center justify-between p-4 border-b border-gray-100">
|
||||
<div class="flex items-center gap-2">
|
||||
<h3 class="font-semibold text-gray-900">Notifications</h3>
|
||||
<span
|
||||
v-if="unreadCount > 0"
|
||||
class="px-2 py-0.5 text-xs font-medium bg-primary text-white rounded-full"
|
||||
>
|
||||
{{ unreadCount }}
|
||||
</span>
|
||||
</div>
|
||||
<button
|
||||
v-if="unreadCount > 0"
|
||||
@click="handleMarkAllRead"
|
||||
class="text-sm text-primary hover:underline font-medium"
|
||||
>
|
||||
Mark all read
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Notification List -->
|
||||
<div class="flex-1 overflow-y-auto">
|
||||
<template v-if="notifications.length > 0">
|
||||
<div
|
||||
v-for="notification in notifications"
|
||||
:key="notification.id"
|
||||
class="border-b border-gray-50 last:border-0"
|
||||
>
|
||||
<NotificationItem
|
||||
:notification="notification"
|
||||
@mark-read="handleMarkRead"
|
||||
@delete="handleDelete"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- Empty state -->
|
||||
<div v-else class="py-12 text-center">
|
||||
<span class="i-lucide-bell-off w-12 h-12 text-gray-300 mx-auto block mb-3"></span>
|
||||
<p class="text-gray-500 text-sm">No notifications</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Footer -->
|
||||
<div v-if="notifications.length > 0" class="p-3 border-t border-gray-100 bg-gray-50/50">
|
||||
<router-link
|
||||
to="/notification"
|
||||
class="block w-full text-center text-sm text-primary font-medium hover:underline"
|
||||
@click="popover?.hide()"
|
||||
>
|
||||
View all notifications
|
||||
</router-link>
|
||||
</div>
|
||||
</div>
|
||||
</Popover>
|
||||
</template>
|
||||
|
||||
<style>
|
||||
.notification-popover {
|
||||
border-radius: 16px !important;
|
||||
box-shadow: 0 10px 40px rgba(0, 0, 0, 0.12) !important;
|
||||
border: 1px solid rgba(0, 0, 0, 0.08) !important;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.notification-popover .p-popover-content {
|
||||
padding: 0 !important;
|
||||
}
|
||||
</style>
|
||||
@@ -104,6 +104,6 @@ const bgClass = computed(() => {
|
||||
|
||||
<style scoped>
|
||||
.notification-item {
|
||||
position: relative;
|
||||
border-radius: 8px;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -43,7 +43,7 @@ const currentPlan = computed(() => {
|
||||
return plans.value.find(p => p.id === currentPlanId.value);
|
||||
});
|
||||
|
||||
const { data, isLoading, mutate: mutatePlans } = useSWRV("r/plans", () => client.plans.plansList())
|
||||
const { data, isLoading, mutate: mutatePlans } = useSWRV("r/plans", client.plans.plansList)
|
||||
|
||||
watch(data, (newValue) => {
|
||||
if (newValue) {
|
||||
|
||||
Reference in New Issue
Block a user