@apptegy/nuxt-navigation 0.1.97 → 0.1.99

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,79 @@
1
+ export type NotificationType = 'announcement' | 'message' | 'comment' | 'mention' | 'reply' | 'reaction' | 'school-post' | 'system' | 'unknown';
2
+ export type NotificationApiState = 'unread' | 'read' | 'dismissed';
3
+ export interface INotificationApiActor {
4
+ avatar_url?: string;
5
+ first_name: string;
6
+ last_name: string;
7
+ }
8
+ export interface INotificationApiRecord {
9
+ id: number | string;
10
+ source_record_type: string;
11
+ title: string;
12
+ summary?: string;
13
+ actors: INotificationApiActor[];
14
+ detail_link?: string;
15
+ notification_state: NotificationApiState | string;
16
+ created_at: string;
17
+ }
18
+ export interface INotificationsPagination {
19
+ page: number;
20
+ per_page: number;
21
+ total_count: number;
22
+ total_pages: number;
23
+ }
24
+ export interface INotificationsApiResponse {
25
+ data: INotificationApiRecord[];
26
+ errors: unknown[];
27
+ warnings: unknown[];
28
+ pagination: INotificationsPagination;
29
+ }
30
+ export interface INotificationActor {
31
+ name: string;
32
+ avatarUrl?: string;
33
+ }
34
+ export interface INotificationItem {
35
+ id: string;
36
+ type: NotificationType;
37
+ actors: INotificationActor[];
38
+ actionText: string;
39
+ timestampLabel: string;
40
+ /** ISO-8601 timestamp from the API — used to derive group labels and relative time. */
41
+ createdAt?: string;
42
+ contextLabel?: string;
43
+ /** When false, the notification counts toward the bell badge and appears in the New tab. */
44
+ dismissed?: boolean;
45
+ /** Read receipt — announcement and message types only. */
46
+ read?: boolean;
47
+ /** Optional edge-case state chip shown at the bottom of the card. */
48
+ stateLabel?: string;
49
+ href?: string;
50
+ groupCount?: number;
51
+ /** @deprecated Use dismissed instead. Kept for backward compatibility. */
52
+ unread?: boolean;
53
+ }
54
+ export interface INotificationGroup {
55
+ id: string;
56
+ label: string;
57
+ notifications: INotificationItem[];
58
+ }
59
+ export interface INotificationTab {
60
+ id: string;
61
+ label: string;
62
+ count?: number;
63
+ /** When true, only in-badge notifications are shown for this tab. */
64
+ unreadOnly?: boolean;
65
+ }
66
+ export interface INotificationFilter {
67
+ id: string;
68
+ label: string;
69
+ count?: number;
70
+ /** Notification types included in this filter. Omit or leave empty to include all types. */
71
+ types?: NotificationType[];
72
+ }
73
+ export interface INotificationsPanelData {
74
+ retentionLabel?: string;
75
+ tabs: INotificationTab[];
76
+ /** Optional — when omitted, filter pills are derived from notification results. */
77
+ filters?: INotificationFilter[];
78
+ groups: INotificationGroup[];
79
+ }
File without changes
@@ -0,0 +1,26 @@
1
+ import type { INotificationApiActor, INotificationApiRecord, INotificationGroup, INotificationItem, INotificationsApiResponse, INotificationsPanelData, INotificationTab, NotificationType } from '../types/index.js';
2
+ import { type INotificationDateLabels } from './notification-dates.js';
3
+ export declare const DEFAULT_NOTIFICATION_TABS: INotificationTab[];
4
+ export declare function mapSourceRecordType(sourceRecordType: string): NotificationType;
5
+ export declare function mapNotificationApiActor(actor: INotificationApiActor): {
6
+ name: string;
7
+ avatarUrl: string | undefined;
8
+ };
9
+ export declare function mapNotificationApiRecord(record: INotificationApiRecord, options: {
10
+ locale?: string;
11
+ dateLabels: INotificationDateLabels;
12
+ now?: Date;
13
+ }): INotificationItem;
14
+ export declare function groupNotificationItems(items: INotificationItem[], options: {
15
+ locale?: string;
16
+ dateLabels: INotificationDateLabels;
17
+ now?: Date;
18
+ }): INotificationGroup[];
19
+ export declare function mapNotificationsApiResponse(response: INotificationsApiResponse, options: {
20
+ locale?: string;
21
+ dateLabels: INotificationDateLabels;
22
+ retentionLabel?: string;
23
+ tabs?: INotificationTab[];
24
+ now?: Date;
25
+ }): INotificationsPanelData;
26
+ export declare function getNotificationsTotalCount(response: INotificationsApiResponse | null | undefined): number;
@@ -0,0 +1,83 @@
1
+ import {
2
+ compareNotificationDateGroups,
3
+ formatNotificationGroupLabel,
4
+ formatNotificationTimeAgo,
5
+ getNotificationDateGroupKey
6
+ } from "./notification-dates.js";
7
+ const SOURCE_RECORD_TYPE_MAP = {
8
+ announcement: "announcement",
9
+ message: "message",
10
+ comment: "comment",
11
+ mention: "mention",
12
+ reply: "reply",
13
+ reaction: "reaction",
14
+ school_post: "school-post",
15
+ "school-post": "school-post",
16
+ system: "system"
17
+ };
18
+ export const DEFAULT_NOTIFICATION_TABS = [
19
+ { id: "new", label: "New", unreadOnly: true },
20
+ { id: "all", label: "All" }
21
+ ];
22
+ export function mapSourceRecordType(sourceRecordType) {
23
+ return SOURCE_RECORD_TYPE_MAP[sourceRecordType] ?? "unknown";
24
+ }
25
+ export function mapNotificationApiActor(actor) {
26
+ const name = [actor.first_name, actor.last_name].filter(Boolean).join(" ").trim();
27
+ return {
28
+ name: name || "Unknown",
29
+ avatarUrl: actor.avatar_url
30
+ };
31
+ }
32
+ export function mapNotificationApiRecord(record, options) {
33
+ const state = record.notification_state;
34
+ return {
35
+ id: String(record.id),
36
+ type: mapSourceRecordType(record.source_record_type),
37
+ actors: record.actors.map(mapNotificationApiActor),
38
+ actionText: record.title,
39
+ timestampLabel: formatNotificationTimeAgo(record.created_at, {
40
+ locale: options.locale,
41
+ now: options.now,
42
+ labels: {
43
+ justNow: options.dateLabels.justNow,
44
+ yesterday: options.dateLabels.yesterdayTimeAgo
45
+ }
46
+ }),
47
+ createdAt: record.created_at,
48
+ contextLabel: record.summary,
49
+ dismissed: state === "dismissed",
50
+ read: state === "read",
51
+ unread: state === "unread",
52
+ href: record.detail_link
53
+ };
54
+ }
55
+ export function groupNotificationItems(items, options) {
56
+ const groups = /* @__PURE__ */ new Map();
57
+ for (const item of items) {
58
+ const createdAt = item.createdAt;
59
+ if (!createdAt) {
60
+ continue;
61
+ }
62
+ const groupKey = getNotificationDateGroupKey(createdAt, options.now);
63
+ const bucket = groups.get(groupKey) ?? [];
64
+ bucket.push(item);
65
+ groups.set(groupKey, bucket);
66
+ }
67
+ return [...groups.entries()].sort(([a], [b]) => compareNotificationDateGroups(a, b)).map(([groupKey, notifications]) => ({
68
+ id: groupKey,
69
+ label: formatNotificationGroupLabel(groupKey, options.dateLabels, options.locale),
70
+ notifications
71
+ }));
72
+ }
73
+ export function mapNotificationsApiResponse(response, options) {
74
+ const items = response.data.map((record) => mapNotificationApiRecord(record, options));
75
+ return {
76
+ retentionLabel: options.retentionLabel,
77
+ tabs: options.tabs ?? DEFAULT_NOTIFICATION_TABS,
78
+ groups: groupNotificationItems(items, options)
79
+ };
80
+ }
81
+ export function getNotificationsTotalCount(response) {
82
+ return response?.pagination?.total_count ?? 0;
83
+ }
@@ -0,0 +1,15 @@
1
+ export interface INotificationDateLabels {
2
+ today: string;
3
+ yesterday: string;
4
+ yesterdayTimeAgo: string;
5
+ justNow: string;
6
+ }
7
+ /** Calendar bucket id used to group notifications (today, yesterday, or YYYY-MM-DD). */
8
+ export declare function getNotificationDateGroupKey(createdAt: string, now?: Date): string;
9
+ export declare function compareNotificationDateGroups(a: string, b: string): number;
10
+ export declare function formatNotificationGroupLabel(groupKey: string, labels: INotificationDateLabels, locale?: string): string;
11
+ export declare function formatNotificationTimeAgo(createdAt: string, options?: {
12
+ locale?: string;
13
+ now?: Date;
14
+ labels?: Pick<INotificationDateLabels, 'justNow' | 'yesterdayTimeAgo'>;
15
+ }): string;
@@ -0,0 +1,81 @@
1
+ function startOfDay(date) {
2
+ const value = new Date(date);
3
+ value.setHours(0, 0, 0, 0);
4
+ return value;
5
+ }
6
+ export function getNotificationDateGroupKey(createdAt, now = /* @__PURE__ */ new Date()) {
7
+ const created = new Date(createdAt);
8
+ const today = startOfDay(now);
9
+ const yesterday = new Date(today);
10
+ yesterday.setDate(yesterday.getDate() - 1);
11
+ const createdDay = startOfDay(created);
12
+ if (createdDay.getTime() === today.getTime()) {
13
+ return "today";
14
+ }
15
+ if (createdDay.getTime() === yesterday.getTime()) {
16
+ return "yesterday";
17
+ }
18
+ const year = createdDay.getFullYear();
19
+ const month = String(createdDay.getMonth() + 1).padStart(2, "0");
20
+ const day = String(createdDay.getDate()).padStart(2, "0");
21
+ return `${year}-${month}-${day}`;
22
+ }
23
+ const GROUP_SORT_ORDER = {
24
+ today: 0,
25
+ yesterday: 1
26
+ };
27
+ export function compareNotificationDateGroups(a, b) {
28
+ const orderA = GROUP_SORT_ORDER[a] ?? 2;
29
+ const orderB = GROUP_SORT_ORDER[b] ?? 2;
30
+ if (orderA !== orderB) {
31
+ return orderA - orderB;
32
+ }
33
+ if (orderA === 2 && orderB === 2) {
34
+ return b.localeCompare(a);
35
+ }
36
+ return 0;
37
+ }
38
+ export function formatNotificationGroupLabel(groupKey, labels, locale = "en") {
39
+ if (groupKey === "today") {
40
+ return labels.today;
41
+ }
42
+ if (groupKey === "yesterday") {
43
+ return labels.yesterday;
44
+ }
45
+ const date = /* @__PURE__ */ new Date(`${groupKey}T12:00:00`);
46
+ return date.toLocaleDateString(locale, { month: "long", day: "numeric" }).toUpperCase();
47
+ }
48
+ export function formatNotificationTimeAgo(createdAt, options = {}) {
49
+ const now = options.now ?? /* @__PURE__ */ new Date();
50
+ const created = new Date(createdAt);
51
+ const locale = options.locale ?? "en";
52
+ const labels = {
53
+ justNow: options.labels?.justNow ?? "just now",
54
+ yesterday: options.labels?.yesterdayTimeAgo ?? "Yesterday"
55
+ };
56
+ const diffMs = now.getTime() - created.getTime();
57
+ const diffMinutes = Math.floor(diffMs / 6e4);
58
+ const diffHours = Math.floor(diffMs / 36e5);
59
+ const today = startOfDay(now);
60
+ const createdDay = startOfDay(created);
61
+ const yesterday = new Date(today);
62
+ yesterday.setDate(yesterday.getDate() - 1);
63
+ if (diffMinutes < 1) {
64
+ return labels.justNow;
65
+ }
66
+ const relativeTime = new Intl.RelativeTimeFormat(locale, { numeric: "always" });
67
+ if (createdDay.getTime() === today.getTime()) {
68
+ if (diffMinutes < 60) {
69
+ return relativeTime.format(-diffMinutes, "minute");
70
+ }
71
+ return relativeTime.format(-diffHours, "hour");
72
+ }
73
+ if (createdDay.getTime() === yesterday.getTime()) {
74
+ return labels.yesterday;
75
+ }
76
+ const diffDays = Math.floor((today.getTime() - createdDay.getTime()) / 864e5);
77
+ if (diffDays < 7) {
78
+ return relativeTime.format(-diffDays, "day");
79
+ }
80
+ return created.toLocaleDateString(locale, { month: "short", day: "numeric" });
81
+ }
@@ -0,0 +1,21 @@
1
+ import type { INotificationFilter, INotificationGroup, INotificationItem, INotificationTab, NotificationType } from '../types/index.js';
2
+ export declare function flattenNotifications(groups: INotificationGroup[]): INotificationItem[];
3
+ /** In-badge notifications (New tab scope): unread only. */
4
+ export declare function isInBadge(notification: INotificationItem): boolean;
5
+ export declare function getInBadgeNotifications(notifications: INotificationItem[]): INotificationItem[];
6
+ export declare function getInBadgeCount(notifications: INotificationItem[]): number;
7
+ export declare function notificationMatchesFilter(notification: INotificationItem, filter: INotificationFilter): boolean;
8
+ export declare function getTabBadgeCount(notifications: INotificationItem[], tab: INotificationTab): number;
9
+ export declare function getFilterBadgeCount(notifications: INotificationItem[], filter: INotificationFilter): number;
10
+ export declare function getScopedNotifications(notifications: INotificationItem[], tab: INotificationTab | undefined): INotificationItem[];
11
+ /**
12
+ * Builds filter pills from notification results.
13
+ * Returns an empty array when fewer than 2 categories have content (per NC v2 spec).
14
+ */
15
+ export declare function buildFiltersFromNotifications(notifications: INotificationItem[], tab: INotificationTab | undefined): INotificationFilter[];
16
+ export declare function resolveNotificationFilters(notifications: INotificationItem[], tab: INotificationTab | undefined, filters?: INotificationFilter[]): INotificationFilter[];
17
+ export declare function filterHasContent(notifications: INotificationItem[], filter: INotificationFilter, tab: INotificationTab | undefined): boolean;
18
+ export declare function getVisibleFilters(notifications: INotificationItem[], filters: INotificationFilter[], tab: INotificationTab | undefined): INotificationFilter[];
19
+ export declare function shouldShowFilterChips(notifications: INotificationItem[], filters: INotificationFilter[], tab: INotificationTab | undefined, usesDynamicFilters?: boolean): boolean;
20
+ export declare function formatNotificationCount(count: number): string;
21
+ export declare function getTypeAccent(type: NotificationType): string;
@@ -0,0 +1,117 @@
1
+ import { NOTIFICATION_FILTER_CATEGORIES } from "../constants/notification-types.js";
2
+ export function flattenNotifications(groups) {
3
+ return groups.flatMap((group) => group.notifications);
4
+ }
5
+ export function isInBadge(notification) {
6
+ if (notification.dismissed === true) {
7
+ return false;
8
+ }
9
+ if (notification.unread === true) {
10
+ return true;
11
+ }
12
+ if (notification.read === true) {
13
+ return false;
14
+ }
15
+ if (notification.dismissed !== void 0) {
16
+ return !notification.dismissed;
17
+ }
18
+ return notification.unread === true;
19
+ }
20
+ export function getInBadgeNotifications(notifications) {
21
+ return notifications.filter(isInBadge);
22
+ }
23
+ export function getInBadgeCount(notifications) {
24
+ return getInBadgeNotifications(notifications).length;
25
+ }
26
+ export function notificationMatchesFilter(notification, filter) {
27
+ if (!filter.types?.length) {
28
+ return true;
29
+ }
30
+ return filter.types.includes(notification.type);
31
+ }
32
+ export function getTabBadgeCount(notifications, tab) {
33
+ if (tab.unreadOnly || tab.id === "new") {
34
+ return getInBadgeCount(notifications);
35
+ }
36
+ return 0;
37
+ }
38
+ export function getFilterBadgeCount(notifications, filter) {
39
+ if (!filter.types?.length || filter.id === "all-types") {
40
+ return 0;
41
+ }
42
+ return getInBadgeNotifications(notifications).filter(
43
+ (notification) => notificationMatchesFilter(notification, filter)
44
+ ).length;
45
+ }
46
+ export function getScopedNotifications(notifications, tab) {
47
+ if (tab?.unreadOnly || tab?.id === "new") {
48
+ return getInBadgeNotifications(notifications);
49
+ }
50
+ return notifications;
51
+ }
52
+ export function buildFiltersFromNotifications(notifications, tab) {
53
+ const scoped = getScopedNotifications(notifications, tab);
54
+ const typesPresent = new Set(scoped.map((notification) => notification.type));
55
+ const categories = NOTIFICATION_FILTER_CATEGORIES.filter(
56
+ (category) => category.types.some((type) => typesPresent.has(type))
57
+ );
58
+ if (categories.length < 2) {
59
+ return [];
60
+ }
61
+ return [
62
+ { id: "all-types", label: "All types" },
63
+ ...categories
64
+ ];
65
+ }
66
+ export function resolveNotificationFilters(notifications, tab, filters) {
67
+ if (filters?.length) {
68
+ return filters;
69
+ }
70
+ return buildFiltersFromNotifications(notifications, tab);
71
+ }
72
+ export function filterHasContent(notifications, filter, tab) {
73
+ const scoped = getScopedNotifications(notifications, tab);
74
+ return scoped.some((notification) => notificationMatchesFilter(notification, filter));
75
+ }
76
+ export function getVisibleFilters(notifications, filters, tab) {
77
+ return filters.filter((filter) => {
78
+ if (filter.id === "other") {
79
+ return notifications.some((notification) => notification.type === "unknown");
80
+ }
81
+ return true;
82
+ }).filter((filter) => {
83
+ if (filter.id === "all-types") {
84
+ return true;
85
+ }
86
+ return filterHasContent(notifications, filter, tab);
87
+ });
88
+ }
89
+ export function shouldShowFilterChips(notifications, filters, tab, usesDynamicFilters = false) {
90
+ if (usesDynamicFilters) {
91
+ return filters.length > 0;
92
+ }
93
+ const categoriesWithContent = filters.filter(
94
+ (filter) => filter.id !== "all-types" && filter.id !== "other" && filterHasContent(notifications, filter, tab)
95
+ );
96
+ return categoriesWithContent.length >= 2;
97
+ }
98
+ export function formatNotificationCount(count) {
99
+ if (count > 99) {
100
+ return "99+";
101
+ }
102
+ return String(count);
103
+ }
104
+ export function getTypeAccent(type) {
105
+ const accents = {
106
+ announcement: "#D67A11",
107
+ message: "#1372B3",
108
+ comment: "#139A4F",
109
+ mention: "#5A2496",
110
+ reply: "#1372B3",
111
+ reaction: "#62626E",
112
+ "school-post": "#1E3FBA",
113
+ system: "#62626E",
114
+ unknown: "#62626E"
115
+ };
116
+ return accents[type];
117
+ }
package/locale/en.json CHANGED
@@ -15,6 +15,27 @@
15
15
  "userDropdown": "User settings",
16
16
  "logoAlt": "Thrillshare - Return Home",
17
17
  "brandLogoAlt": "Logo",
18
- "website": "School Website"
18
+ "website": "School Website",
19
+ "notifications": "Notifications",
20
+ "notificationsPanel": {
21
+ "retentionLabel": "Kept 30 days",
22
+ "tabs": {
23
+ "new": "New",
24
+ "all": "All"
25
+ },
26
+ "tabsLabel": "Notification views",
27
+ "scrollFilters": "Scroll filters",
28
+ "clearAll": "Clear all",
29
+ "dismiss": "Dismiss notification",
30
+ "close": "Close notifications",
31
+ "loading": "Loading notifications...",
32
+ "empty": "You're all caught up.",
33
+ "justNow": "just now",
34
+ "yesterday": "Yesterday",
35
+ "dateGroups": {
36
+ "today": "TODAY",
37
+ "yesterday": "YESTERDAY"
38
+ }
39
+ }
19
40
  }
20
41
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@apptegy/nuxt-navigation",
3
- "version": "0.1.97",
3
+ "version": "0.1.99",
4
4
  "type": "module",
5
5
  "exports": {
6
6
  ".": {
@@ -32,19 +32,18 @@
32
32
  "@vueuse/core": "^12.0.0"
33
33
  },
34
34
  "devDependencies": {
35
- "@nuxt/devtools": "^2.6.2",
36
- "@nuxt/eslint-config": "^1.6.0",
37
- "@nuxt/module-builder": "^1.0.1",
38
- "@nuxt/schema": "^3.17.0",
39
- "@nuxt/test-utils": "^3.17.0",
40
- "@types/node": "^22.0.0",
41
- "eslint": "^9.31.0",
42
- "nuxt": "^3.17.0",
43
- "sass": "^1.89.2",
44
- "typescript": "~5.8.0",
45
- "vitest": "^3.2.4",
46
- "vue": "^3.5.13",
47
- "vue-tsc": "^3.0.3"
48
- },
49
- "gitHead": "9b9589aa135b0930ffa4c4b6c178520108e9ad7b"
35
+ "@nuxt/devtools": "catalog:",
36
+ "@nuxt/eslint-config": "catalog:",
37
+ "@nuxt/module-builder": "catalog:",
38
+ "@nuxt/schema": "catalog:",
39
+ "@nuxt/test-utils": "catalog:",
40
+ "@types/node": "catalog:",
41
+ "eslint": "catalog:",
42
+ "nuxt": "catalog:",
43
+ "sass": "catalog:",
44
+ "typescript": "catalog:",
45
+ "vitest": "catalog:",
46
+ "vue": "catalog:",
47
+ "vue-tsc": "catalog:"
48
+ }
50
49
  }