Skip to content

Commit e349062

Browse files
committed
Minor bugfixes
1 parent 9f11983 commit e349062

4 files changed

Lines changed: 62 additions & 10 deletions

File tree

resources/js/components/Notifications.vue

Lines changed: 24 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,25 @@
11
<script setup lang="ts">
22
import { Dropdown, DropdownContent, DropdownTrigger } from '@/components/dropdown';
3-
import { notifications } from '@/composables/useAuth';
3+
import { useGlobalProps } from '@/composables/useGlobalProps';
44
import { _ } from '@/composables/useTranslations';
55
import { Link } from '@inertiajs/vue3';
66
import axios from 'axios';
77
import { Bell } from 'lucide-vue-next';
88
import { route as routeUri } from 'ziggy-js';
99
10+
const globalProps = useGlobalProps();
11+
12+
// Computed reference to notifications from global props
13+
const notifications = computed(() => globalProps.auth?.notifications);
14+
1015
const markAsRead = (notification: Notification, close: () => void) => {
11-
if (!notification.is_read) axios.post(routeUri('notifications.read', notification.id));
16+
if (!notification.is_read) {
17+
axios.post(routeUri('notifications.read', notification.id));
18+
// Decrement unread count when marking a single notification as read
19+
if (globalProps.auth?.notifications) {
20+
globalProps.auth.notifications.unread = Math.max(0, globalProps.auth.notifications.unread - 1);
21+
}
22+
}
1223
1324
notification.is_read = true;
1425
setTimeout(() => close(), 100);
@@ -17,8 +28,12 @@ const markAsRead = (notification: Notification, close: () => void) => {
1728
const markAllAsRead = () => {
1829
axios.post(routeUri('notifications.readAll'));
1930
20-
// Optimistically mark all as read
21-
notifications.value.recent.forEach((n: Notification) => (n.is_read = true));
31+
// Optimistically mark all as read using the mutable global props copy
32+
if (globalProps.auth?.notifications) {
33+
globalProps.auth.notifications.recent.forEach((n: Notification) => (n.is_read = true));
34+
// Reset unread count to 0
35+
globalProps.auth.notifications.unread = 0;
36+
}
2237
};
2338
</script>
2439

@@ -32,21 +47,21 @@ const markAllAsRead = () => {
3247

3348
<!-- Unread count badge -->
3449
<span
35-
v-if="notifications.unread"
50+
v-if="notifications?.unread"
3651
class="absolute -top-0.5 -right-0.5 flex size-5 items-center justify-center rounded-full bg-accent-foreground text-xs font-bold text-background"
3752
>
3853
{{ notifications.unread > 9 ? '9+' : notifications.unread }}
3954
</span>
4055
</DropdownTrigger>
4156

4257
<DropdownContent
43-
class="top-10 right-0 mr-3 w-80 origin-top overflow-hidden rounded-xl border border-border bg-surface-elevated shadow shadow-accent/70 backdrop-blur-2xl"
58+
class="top-10 right-0 mr-3 w-80 origin-top overflow-hidden rounded-xl border border-border bg-surface-elevated shadow shadow-accent/70 backdrop-blur-2xl max-xs:fixed max-xs:inset-x max-xs:top-16"
4459
>
4560
<!-- Header -->
4661
<div class="flex items-center justify-between border-b border-border/30 px-4 py-3">
4762
<h3 class="text-sm font-semibold text-text">{{ _('Notifications') }}</h3>
4863
<button
49-
v-if="notifications.unread"
64+
v-if="notifications?.unread"
5065
class="text-xs text-text-subtle hocus:text-text"
5166
@click="markAllAsRead"
5267
>
@@ -57,8 +72,8 @@ const markAllAsRead = () => {
5772
<!-- Notifications list -->
5873
<div class="max-h-96 divide-y divide-border/70 overflow-y-auto">
5974
<component
75+
v-for="notification in notifications?.recent || []"
6076
:is="notification.data?.action ? Link : 'div'"
61-
v-for="notification in notifications.recent"
6277
:key="notification.id"
6378
:href="notification.data?.action"
6479
class="flex items-start gap-3 px-4 py-3 transition-colors"
@@ -85,7 +100,7 @@ const markAllAsRead = () => {
85100

86101
<!-- Empty state -->
87102
<div
88-
v-if="notifications.recent.length === 0"
103+
v-if="!notifications?.recent || notifications.recent.length === 0"
89104
class="flex flex-col items-center justify-center py-12 text-text-subtle"
90105
>
91106
<Bell class="mb-3 size-12 opacity-30" />

resources/js/components/layout/NavMain.vue

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import {
44
SidebarMenu,
55
SidebarMenuButton,
66
SidebarMenuItem,
7+
useSidebar,
78
} from '@/components/layout/sidebar';
89
import { can } from '@/composables/useAuth';
910
import { currentRoute } from '@/composables/useRoute';
@@ -30,6 +31,7 @@ interface NavItem {
3031
}
3132
3233
const { groupMembers } = useMemberGrouping();
34+
const { setOpenMobile } = useSidebar();
3335
3436
const allNavItems: NavItem[] = [
3537
{
@@ -106,6 +108,7 @@ const activeNavItem = computed(() =>
106108
:href="route(toValue(item.route))"
107109
:class="{ 'pointer-events-none': item.route === currentRoute }"
108110
prefetch
111+
@click="setOpenMobile(false)"
109112
>
110113
<component :is="item.icon" />
111114
<span>{{ _(toValue(item.label)) }}</span>

resources/js/components/layout/header/AppSidebarHeader.vue

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ onAnyMessage((message) => {
3333
<template>
3434
<header class="@container/header m-0 mb-base @md:px-base">
3535
<div
36-
class="relative z-20 flex items-center justify-between gap-base border-t border-r border-border bg-linear-to-r from-sidebar/70 to-background to-70% px-8 py-4 shadow-lg shadow-foreground/10 transition-[width,height] ease-linear @md:mt-4 @md:mb-wide @md:rounded-lg @md:px-wide @md:py-3"
36+
class="relative z-20 flex items-center justify-between gap-base border-t border-r border-border bg-linear-to-r from-sidebar/70 to-background to-70% px-4 xs:px-8 py-4 shadow-lg shadow-foreground/10 transition-[width,height] ease-linear @md:mt-4 mb-4 @md:mb-wide @md:rounded-lg @md:px-wide @md:py-3"
3737
>
3838
<div class="flex flex-1 items-center gap-4">
3939
<SidebarTrigger />
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
// Copilot - Pending review
2+
import { usePage } from '@inertiajs/vue3';
3+
4+
/**
5+
* Composable that maintains a reactive, mutable copy of Inertia props.
6+
* Useful for optimistic updates without requiring a full page reload.
7+
*
8+
* The returned object maintains the same structure as usePage().props,
9+
* but is a reactive copy that can be mutated to update UI optimistically.
10+
* Nested mutations persist until the next Inertia update.
11+
*
12+
* @returns Reactive copy of Inertia props
13+
*/
14+
export function useGlobalProps() {
15+
const page = usePage();
16+
const globalProps = reactive<Record<string, any>>({});
17+
18+
// Initialize with current props immediately
19+
Object.assign(globalProps, page.props);
20+
21+
// Sync whenever props change
22+
watchEffect(() => {
23+
// Remove stale props that no longer exist in the current Inertia props
24+
for (const key in globalProps) {
25+
if (!(key in page.props)) {
26+
delete globalProps[key];
27+
}
28+
}
29+
// Update with current props (adds new ones, updates existing)
30+
Object.assign(globalProps, page.props);
31+
});
32+
33+
return globalProps;
34+
}

0 commit comments

Comments
 (0)