Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion web/components/Notifications/Indicator.vue
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ const icon = computed<{ component: Component; color: string } | null>(() => {

<template>
<div class="relative">
<BellIcon class="w-6 h-6" />
<BellIcon class="w-6 h-6 text-header-text-primary" />
<div
v-if="indicatorLevel === 'UNREAD'"
class="absolute top-0 right-0 size-2.5 rounded-full border border-neutral-800 bg-unraid-green"
Expand Down
35 changes: 27 additions & 8 deletions web/components/Notifications/Item.vue
Original file line number Diff line number Diff line change
Expand Up @@ -63,33 +63,52 @@ const deleteNotification = reactive(
const mutationError = computed(() => {
return archive.error?.message ?? deleteNotification.error?.message;
});

const reformattedTimestamp = computed<string>(() => {
if (!props.timestamp) return '';
const userLocale = navigator.language ?? 'en-US'; // Get the user's browser language (e.g., 'en-US', 'fr-FR')

const reformattedDate = new Intl.DateTimeFormat(userLocale, {
localeMatcher: 'best fit',
month: 'short',
day: 'numeric',
hour: '2-digit',
minute: '2-digit',
hour12: ['AM', 'PM'].some((period) => (props.formattedTimestamp ?? 'AM').includes(period)),
}).format(new Date(props.timestamp));
return reformattedDate;
});
</script>

<template>
<!-- fixed width hack ensures alignment with other elements regardless of scrollbar presence or width -->
<div class="group/item relative py-5 flex flex-col gap-2 text-base w-[487px]">
<header class="w-full flex flex-row items-baseline justify-between gap-2 -translate-y-1">
<h3 class="tracking-normal flex flex-row items-baseline gap-2 uppercase font-bold">
<div class="group/item relative py-5 flex flex-col gap-2 text-base">
<header class="flex flex-row items-baseline justify-between gap-2 -translate-y-1">
<h3
class="tracking-normal flex flex-row items-baseline gap-2 uppercase font-bold overflow-x-hidden"
>
<!-- the `translate` compensates for extra space added by the `svg` element when rendered -->
<component
:is="icon.component"
v-if="icon"
class="size-5 shrink-0 translate-y-1"
:class="icon.color"
/>
<span>{{ title }}</span>
<span class="truncate flex-1" :title="title">{{ title }}</span>
</h3>

<div class="shrink-0 flex flex-row items-baseline justify-end gap-2 mt-1">
<p class="text-gray-500 text-sm">{{ formattedTimestamp }}</p>
<div
class="shrink-0 flex flex-row items-baseline justify-end gap-2 mt-1"
:title="formattedTimestamp ?? reformattedTimestamp"
>
<p class="text-secondary-foreground text-sm">{{ reformattedTimestamp }}</p>
</div>
</header>

<h4 class="font-bold">
{{ subject }}
</h4>

<div class="w-full flex flex-row items-center justify-between gap-2">
<div class="flex flex-row items-center justify-between gap-2">
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Display flex defaults to flex row can remove flex-row.

<div class="" v-html="descriptionMarkup" />
</div>

Expand Down
6 changes: 4 additions & 2 deletions web/components/Notifications/List.vue
Original file line number Diff line number Diff line change
Expand Up @@ -67,11 +67,10 @@ async function onLoadMore() {
</script>

<template>
<!-- The horizontal padding here adjusts for the scrollbar offset -->
<div
v-if="notifications?.length > 0"
v-infinite-scroll="[onLoadMore, { canLoadMore: () => canLoadMore }]"
class="divide-y divide-gray-200 overflow-y-auto h-full pl-7"
class="divide-y divide-gray-200 px-7 flex flex-col overflow-y-scroll flex-1 min-h-0"
>
<NotificationsItem
v-for="notification in notifications"
Expand All @@ -81,6 +80,9 @@ async function onLoadMore() {
<div v-if="loading" class="py-5 grid place-content-center">
<LoadingSpinner />
</div>
<div v-if="!canLoadMore" class="py-5 grid place-content-center text-secondary-foreground">
You've reached the end...
</div>
</div>

<LoadingError v-else :loading="loading" :error="offlineError ?? error" @retry="refetch">
Expand Down
148 changes: 73 additions & 75 deletions web/components/Notifications/Sidebar.vue
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<script setup lang="ts">
import { Button } from '@/components/shadcn/button';
import { Sheet, SheetContent, SheetHeader, SheetTitle, SheetTrigger } from '@/components/shadcn/sheet';
import { useMutation, useQuery } from '@vue/apollo-composable';
import { Button } from '@/components/shadcn/button';
// eslint-disable-next-line @typescript-eslint/consistent-type-imports -- false positive :(
import { Importance, NotificationType } from '~/composables/gql/graphql';
import {
Expand Down Expand Up @@ -47,86 +47,84 @@ const overview = computed(() => {
<span class="sr-only">Notifications</span>
<NotificationsIndicator :overview="overview" />
</SheetTrigger>

<!-- We remove the horizontal padding from the container to keep the NotificationList's scrollbar in the right place -->
<SheetContent
:to="teleportTarget"
class="w-full max-w-[100vw] sm:max-w-[540px] h-screen px-0"
class="w-full max-w-[100vw] sm:max-w-[540px] max-h-screen h-screen min-h-screen px-0 flex flex-col gap-5 pb-0"
>
<div class="flex flex-col h-full gap-5">
<SheetHeader class="ml-1 px-6 flex items-baseline gap-1">
<SheetTitle class="text-2xl">Notifications</SheetTitle>
<a href="/Settings/Notifications">
<Button variant="link" size="sm" class="p-0 h-auto"> Edit Settings </Button>
</a>
</SheetHeader>
<div class="relative flex flex-col h-full w-full">
<SheetHeader class="ml-1 px-6 items-baseline gap-1 pb-2">
<SheetTitle class="text-2xl">Notifications</SheetTitle>
<a href="/Settings/Notifications">
<Button variant="link" size="sm" class="p-0 h-auto">Edit Settings</Button>
</a>
</SheetHeader>
<Tabs
default-value="unread"
class="flex flex-1 flex-col min-h-0"
aria-label="Notification filters"
>
<div class="flex flex-row justify-between items-center flex-wrap gap-5 px-6">
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unneeded flex-row.

<TabsList class="flex" aria-label="Filter notifications by status">
<TabsTrigger value="unread">
Unread <span v-if="overview">({{ overview.unread.total }})</span>
</TabsTrigger>
<TabsTrigger value="archived">
Archived <span v-if="overview">({{ overview.archive.total }})</span>
</TabsTrigger>
</TabsList>
<TabsContent value="unread" class="flex-col items-end">
<Button
:disabled="loadingArchiveAll"
variant="link"
size="sm"
class="text-foreground hover:text-destructive transition-none"
@click="confirmAndArchiveAll"
>
Archive All
</Button>
</TabsContent>
<TabsContent value="archived" class="flex-col items-end">
<Button
:disabled="loadingDeleteAll"
variant="link"
size="sm"
class="text-foreground hover:text-destructive transition-none"
@click="confirmAndDeleteArchives"
>
Delete All
</Button>
</TabsContent>

<!-- min-h-0 prevents the flex container from expanding beyond its containing bounds. -->
<!-- this is necessary because flex items have a default min-height: auto, -->
<!-- which means they won't shrink below the height of their content, even if you use flex-1 or other flex properties. -->
<Tabs default-value="unread" class="flex-1 flex flex-col min-h-0" activation-mode="manual">
<div class="flex flex-row justify-between items-center flex-wrap gap-5 px-6">
<TabsList class="ml-[1px]">
<TabsTrigger value="unread">
Unread <span v-if="overview">({{ overview.unread.total }})</span>
</TabsTrigger>
<TabsTrigger value="archived">
Archived <span v-if="overview">({{ overview.archive.total }})</span>
</TabsTrigger>
</TabsList>
<TabsContent value="unread">
<Button
:disabled="loadingArchiveAll"
variant="link"
size="sm"
class="text-foreground hover:text-destructive transition-none"
@click="confirmAndArchiveAll"
>
Archive All
</Button>
</TabsContent>
<TabsContent value="archived">
<Button
:disabled="loadingDeleteAll"
variant="link"
size="sm"
class="text-foreground hover:text-destructive transition-none"
@click="confirmAndDeleteArchives"
>
Delete All
</Button>
</TabsContent>

<Select
@update:model-value="
(val) => {
importance = val === 'all' ? undefined : (val as Importance);
}
"
>
<SelectTrigger class="h-auto">
<SelectValue class="text-gray-400 leading-6" placeholder="Filter By" />
</SelectTrigger>
<SelectContent :to="teleportTarget">
<SelectGroup>
<SelectLabel>Notification Types</SelectLabel>
<SelectItem value="all">All Types</SelectItem>
<SelectItem :value="Importance.Alert"> Alert </SelectItem>
<SelectItem :value="Importance.Info">Info</SelectItem>
<SelectItem :value="Importance.Warning">Warning</SelectItem>
</SelectGroup>
</SelectContent>
</Select>
</div>
<Select
@update:model-value="
(val) => {
importance = val === 'all' ? undefined : (val as Importance);
}
"
>
<SelectTrigger class="h-auto">
<SelectValue class="text-gray-400 leading-6" placeholder="Filter By" />
</SelectTrigger>
<SelectContent :to="teleportTarget">
<SelectGroup>
<SelectLabel>Notification Types</SelectLabel>
<SelectItem value="all">All Types</SelectItem>
<SelectItem :value="Importance.Alert"> Alert </SelectItem>
<SelectItem :value="Importance.Info">Info</SelectItem>
<SelectItem :value="Importance.Warning">Warning</SelectItem>
</SelectGroup>
</SelectContent>
</Select>
</div>

<TabsContent value="unread" class="flex-1 min-h-0 mt-5">
<NotificationsList :importance="importance" :type="NotificationType.Unread" />
</TabsContent>
<TabsContent value="unread" class="flex-col flex-1 min-h-0">
<NotificationsList :importance="importance" :type="NotificationType.Unread" />
</TabsContent>

<TabsContent value="archived" class="flex-1 min-h-0 mt-5">
<NotificationsList :importance="importance" :type="NotificationType.Archive" />
</TabsContent>
</Tabs>
<TabsContent value="archived" class="flex-col flex-1 min-h-0">
<NotificationsList :importance="importance" :type="NotificationType.Archive" />
</TabsContent>
</Tabs>
</div>
</SheetContent>
</Sheet>
Expand Down
41 changes: 36 additions & 5 deletions web/components/shadcn/sheet/Sheet.vue
Original file line number Diff line number Diff line change
@@ -1,14 +1,45 @@
<script setup lang="ts">
import { DialogRoot, type DialogRootEmits, type DialogRootProps, useForwardPropsEmits } from 'radix-vue'
import { DialogRoot, useForwardPropsEmits, type DialogRootEmits, type DialogRootProps } from 'radix-vue';

const props = defineProps<DialogRootProps & { class?: string }>()
const emits = defineEmits<DialogRootEmits>()
const MOBILE_VIEWPORT = 'width=device-width, initial-scale=1, maximum-scale=1, user-scalable=0' as const;

const forwarded = useForwardPropsEmits(props, emits)
const props = defineProps<DialogRootProps & { class?: string }>();
const emits = defineEmits<DialogRootEmits>();

const getViewport = (): string => {
return document.querySelector('meta[name="viewport"]')?.getAttribute('content') ?? 'width=1300';
};
const updateViewport = (viewport: string): void => {
if (window.innerWidth < 500) {
const meta = document.querySelector('meta[name="viewport"]');
if (meta) {
meta.setAttribute('content', viewport);
} else {
const meta = document.createElement('meta');
meta.name = 'viewport';
meta.content = viewport;
document.head.appendChild(meta);
}
}
};

const forwarded = useForwardPropsEmits(props, emits);
const initialViewport = ref(getViewport());
const openListener = (opened: boolean) => {
if (opened) {
updateViewport(MOBILE_VIEWPORT);
} else {
updateViewport(initialViewport.value);
}
};

onUnmounted(() => {
updateViewport(initialViewport.value);
});
</script>

<template>
<DialogRoot v-bind="forwarded">
<DialogRoot v-bind="forwarded" @update:open="openListener">
<slot />
</DialogRoot>
</template>
23 changes: 15 additions & 8 deletions web/components/shadcn/tabs/TabsContent.vue
Original file line number Diff line number Diff line change
@@ -1,21 +1,28 @@
<script setup lang="ts">
import { type HTMLAttributes, computed } from 'vue'
import { TabsContent, type TabsContentProps } from 'radix-vue'
import { cn } from '~/components/shadcn/utils'
import { cn } from '~/components/shadcn/utils';
import { TabsContent, type TabsContentProps } from 'radix-vue';
import { computed, type HTMLAttributes } from 'vue';

const props = defineProps<TabsContentProps & { class?: HTMLAttributes['class'] }>()
const props = defineProps<TabsContentProps & { class?: HTMLAttributes['class'] }>();

const delegatedProps = computed(() => {
const { class: _, ...delegated } = props
const { class: _, ...delegated } = props;
return delegated;
});

return delegated
})
</script>

<template>
<TabsContent
:class="cn('mt-2 ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2', props.class)"

v-bind="delegatedProps"
:class="
cn(
'flex mt-2 ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2',
props.class
)
"
class="data-[state=active]:flex data-[state=inactive]:hidden"
>
<slot />
</TabsContent>
Expand Down
Loading
Loading