diff --git a/.vscode/extensions.json b/.vscode/extensions.json index 908dc5d..65628d9 100644 --- a/.vscode/extensions.json +++ b/.vscode/extensions.json @@ -7,7 +7,8 @@ "golang.go", "bradlc.vscode-tailwindcss", "craigrbroughton.htmx-attributes", - "nefrob.vscode-just-syntax" + "nefrob.vscode-just-syntax", + "prisma.prisma" ], "unwantedRecommendations": [] } diff --git a/web/package-lock.json b/web/package-lock.json index 04dd2ab..d45782b 100644 --- a/web/package-lock.json +++ b/web/package-lock.json @@ -11,7 +11,7 @@ "@astrojs/check": "0.9.4", "@astrojs/db": "0.15.0", "@astrojs/mdx": "4.3.0", - "@astrojs/node": "9.2.2", + "@astrojs/node": "9.3.0", "@astrojs/rss": "4.0.12", "@astrojs/sitemap": "3.4.1", "@fontsource-variable/space-grotesk": "5.2.8", @@ -22,7 +22,7 @@ "@types/mime-types": "3.0.1", "@types/pg": "8.15.4", "@vercel/og": "0.6.8", - "astro": "5.10.1", + "astro": "5.11.0", "astro-loading-indicator": "0.7.0", "astro-remote": "0.3.4", "astro-seo-schema": "5.0.0", @@ -51,8 +51,8 @@ "web-push": "3.6.7" }, "devDependencies": { - "@eslint/js": "9.30.0", - "@faker-js/faker": "9.8.0", + "@eslint/js": "9.30.1", + "@faker-js/faker": "9.9.0", "@iconify-json/material-symbols": "1.2.28", "@iconify-json/mdi": "1.2.3", "@iconify-json/ri": "1.2.5", @@ -72,12 +72,12 @@ "astro-icon": "1.1.5", "date-fns": "4.1.0", "esbuild": "0.25.5", - "eslint": "9.30.0", + "eslint": "9.30.1", "eslint-import-resolver-typescript": "4.4.4", "eslint-plugin-astro": "1.3.1", "eslint-plugin-import": "2.32.0", "eslint-plugin-jsx-a11y": "6.10.2", - "globals": "16.2.0", + "globals": "16.3.0", "prettier": "3.6.2", "prettier-plugin-astro": "0.14.1", "prettier-plugin-tailwindcss": "0.6.13", @@ -297,9 +297,9 @@ } }, "node_modules/@astrojs/node": { - "version": "9.2.2", - "resolved": "https://registry.npmjs.org/@astrojs/node/-/node-9.2.2.tgz", - "integrity": "sha512-PtLPuuojmcl9O3CEvXqL/D+wB4x5DlbrGOvP0MeTAh/VfKFprYAzgw1+45xsnTO+QvPWb26l1cT+ZQvvohmvMw==", + "version": "9.3.0", + "resolved": "https://registry.npmjs.org/@astrojs/node/-/node-9.3.0.tgz", + "integrity": "sha512-IV8NzGStHAsKBz1ljxxD8PBhBfnw/BEx/PZfsncTNXg9D4kQtZbSy+Ak0LvDs+rPmK0VeXLNn0HAdWuHCVg8cw==", "license": "MIT", "dependencies": { "@astrojs/internal-helpers": "0.6.1", @@ -2641,9 +2641,9 @@ } }, "node_modules/@eslint/js": { - "version": "9.30.0", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.30.0.tgz", - "integrity": "sha512-Wzw3wQwPvc9sHM+NjakWTcPx11mbZyiYHuwWa/QfZ7cIRX7WK54PSk7bdyXDaoaopUcMatv1zaQvOAAO8hCdww==", + "version": "9.30.1", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.30.1.tgz", + "integrity": "sha512-zXhuECFlyep42KZUhWjfvsmXGX39W8K8LFb8AWXM9gSV9dQB+MrJGLKvW6Zw0Ggnbpw0VHTtrhFXYe3Gym18jg==", "dev": true, "license": "MIT", "engines": { @@ -2678,9 +2678,9 @@ } }, "node_modules/@faker-js/faker": { - "version": "9.8.0", - "resolved": "https://registry.npmjs.org/@faker-js/faker/-/faker-9.8.0.tgz", - "integrity": "sha512-U9wpuSrJC93jZBxx/Qq2wPjCuYISBueyVUGK7qqdmj7r/nxaxwW8AQDCLeRO7wZnjj94sh3p246cAYjUKuqgfg==", + "version": "9.9.0", + "resolved": "https://registry.npmjs.org/@faker-js/faker/-/faker-9.9.0.tgz", + "integrity": "sha512-OEl393iCOoo/z8bMezRlJu+GlRGlsKbUAN7jKB6LhnKoqKve5DXRpalbItIIcwnCjs1k/FOPjFzcA6Qn+H+YbA==", "dev": true, "funding": [ { @@ -6333,9 +6333,9 @@ } }, "node_modules/astro": { - "version": "5.10.1", - "resolved": "https://registry.npmjs.org/astro/-/astro-5.10.1.tgz", - "integrity": "sha512-DJVmt+51jU1xmgmAHCDwuUgcG/5aVFSU+tcX694acAZqPVt8EMUAmUZcJDX36Z7/EztnPph9HR3pm72jS2EgHQ==", + "version": "5.11.0", + "resolved": "https://registry.npmjs.org/astro/-/astro-5.11.0.tgz", + "integrity": "sha512-MEICntERthUxJPSSDsDiZuwiCMrsaYy3fnDhp4c6ScUfldCB8RBnB/myYdpTFXpwYBy6SgVsHQ1H4MuuA7ro/Q==", "license": "MIT", "dependencies": { "@astrojs/compiler": "^2.12.2", @@ -9039,9 +9039,9 @@ } }, "node_modules/eslint": { - "version": "9.30.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.30.0.tgz", - "integrity": "sha512-iN/SiPxmQu6EVkf+m1qpBxzUhE12YqFLOSySuOyVLJLEF9nzTf+h/1AJYc1JWzCnktggeNrjvQGLngDzXirU6g==", + "version": "9.30.1", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.30.1.tgz", + "integrity": "sha512-zmxXPNMOXmwm9E0yQLi5uqXHs7uq2UIiqEKo3Gq+3fwo1XrJ+hijAZImyF7hclW3E6oHz43Yk3RP8at6OTKflQ==", "dev": true, "license": "MIT", "dependencies": { @@ -9051,7 +9051,7 @@ "@eslint/config-helpers": "^0.3.0", "@eslint/core": "^0.14.0", "@eslint/eslintrc": "^3.3.1", - "@eslint/js": "9.30.0", + "@eslint/js": "9.30.1", "@eslint/plugin-kit": "^0.3.1", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", @@ -10401,9 +10401,9 @@ } }, "node_modules/globals": { - "version": "16.2.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-16.2.0.tgz", - "integrity": "sha512-O+7l9tPdHCU320IigZZPj5zmRCFG9xHmx9cU8FqU2Rp+JN714seHV+2S9+JslCpY4gJwU2vOGox0wzgae/MCEg==", + "version": "16.3.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-16.3.0.tgz", + "integrity": "sha512-bqWEnJ1Nt3neqx2q5SFfGS8r/ahumIakg3HcwtNlrVlwXIeNumWn/c7Pn/wKzGhf6SaW6H6uWXLqC30STCMchQ==", "dev": true, "license": "MIT", "engines": { diff --git a/web/package.json b/web/package.json index 89995c6..9d48f86 100644 --- a/web/package.json +++ b/web/package.json @@ -27,7 +27,7 @@ "@astrojs/check": "0.9.4", "@astrojs/db": "0.15.0", "@astrojs/mdx": "4.3.0", - "@astrojs/node": "9.2.2", + "@astrojs/node": "9.3.0", "@astrojs/rss": "4.0.12", "@astrojs/sitemap": "3.4.1", "@fontsource-variable/space-grotesk": "5.2.8", @@ -38,7 +38,7 @@ "@types/mime-types": "3.0.1", "@types/pg": "8.15.4", "@vercel/og": "0.6.8", - "astro": "5.10.1", + "astro": "5.11.0", "astro-loading-indicator": "0.7.0", "astro-remote": "0.3.4", "astro-seo-schema": "5.0.0", @@ -67,8 +67,8 @@ "web-push": "3.6.7" }, "devDependencies": { - "@eslint/js": "9.30.0", - "@faker-js/faker": "9.8.0", + "@eslint/js": "9.30.1", + "@faker-js/faker": "9.9.0", "@iconify-json/material-symbols": "1.2.28", "@iconify-json/mdi": "1.2.3", "@iconify-json/ri": "1.2.5", @@ -88,12 +88,12 @@ "astro-icon": "1.1.5", "date-fns": "4.1.0", "esbuild": "0.25.5", - "eslint": "9.30.0", + "eslint": "9.30.1", "eslint-import-resolver-typescript": "4.4.4", "eslint-plugin-astro": "1.3.1", "eslint-plugin-import": "2.32.0", "eslint-plugin-jsx-a11y": "6.10.2", - "globals": "16.2.0", + "globals": "16.3.0", "prettier": "3.6.2", "prettier-plugin-astro": "0.14.1", "prettier-plugin-tailwindcss": "0.6.13", diff --git a/web/prisma/triggers/01_karma_tx.sql b/web/prisma/triggers/01_karma_tx.sql index 600dcdf..7500e68 100644 --- a/web/prisma/triggers/01_karma_tx.sql +++ b/web/prisma/triggers/01_karma_tx.sql @@ -275,7 +275,7 @@ CREATE OR REPLACE FUNCTION handle_suggestion_status_change() RETURNS TRIGGER AS $$ DECLARE service_name TEXT; - service_visibility "serviceVisibility"; + service_visibility "ServiceVisibility"; is_user_admin_or_moderator BOOLEAN; BEGIN -- Award karma for first approval @@ -283,7 +283,7 @@ BEGIN -- and ensure it wasn't already APPROVED. IF OLD.status IS DISTINCT FROM 'APPROVED' AND NEW.status = 'APPROVED' THEN -- Fetch service details for the description - SELECT name, serviceVisibility INTO service_name, service_visibility FROM "Service" WHERE id = NEW."serviceId"; + SELECT name, "serviceVisibility" INTO service_name, service_visibility FROM "Service" WHERE id = NEW."serviceId"; -- Only award karma if the service is public IF service_visibility = 'PUBLIC' THEN diff --git a/web/src/constants/readStatus.ts b/web/src/constants/readStatus.ts new file mode 100644 index 0000000..c9fd48d --- /dev/null +++ b/web/src/constants/readStatus.ts @@ -0,0 +1,33 @@ +import { makeHelpersForOptions } from '../lib/makeHelpersForOptions' +import { transformCase } from '../lib/strings' + +type ReadStatusInfo = { + id: T + label: string + readValue: boolean +} + +export const { + dataArray: readStatuses, + getFn: getReadStatus, + zodEnumById: readStatusZodEnum, +} = makeHelpersForOptions( + 'id', + (id): ReadStatusInfo => ({ + id, + label: id ? transformCase(id, 'title') : String(id), + readValue: false, + }), + [ + { + id: 'unread', + label: 'Unread', + readValue: false, + }, + { + id: 'read', + label: 'Read', + readValue: true, + }, + ] as const satisfies ReadStatusInfo[] +) diff --git a/web/src/pages/notifications.astro b/web/src/pages/notifications.astro index 3131829..c6aa562 100644 --- a/web/src/pages/notifications.astro +++ b/web/src/pages/notifications.astro @@ -5,10 +5,12 @@ import { actions } from 'astro:actions' import Button from '../components/Button.astro' import CopyButton from '../components/CopyButton.astro' +import Pagination from '../components/Pagination.astro' import PushNotificationBanner from '../components/PushNotificationBanner.astro' import TimeFormatted from '../components/TimeFormatted.astro' import Tooltip from '../components/Tooltip.astro' import { getNotificationTypeInfo } from '../constants/notificationTypes' +import { getReadStatus, readStatusZodEnum } from '../constants/readStatus' import BaseLayout from '../layouts/BaseLayout.astro' import { cn } from '../lib/cn' import { getOrCreateNotificationPreferences } from '../lib/notificationPreferences' @@ -16,6 +18,10 @@ import { makeNotificationActions, makeNotificationContent, makeNotificationTitle import { zodParseQueryParamsStoringErrors } from '../lib/parseUrlFilters' import { prisma } from '../lib/prisma' import { makeLoginUrl } from '../lib/redirectUrls' +import { transformCase } from '../lib/strings' +import { urlWithParams } from '../lib/urls' + +import type { Prisma } from '@prisma/client' const user = Astro.locals.user if (!user) return Astro.redirect(makeLoginUrl(Astro.url)) @@ -25,20 +31,26 @@ const PAGE_SIZE = 20 const { data: params } = zodParseQueryParamsStoringErrors( { page: z.coerce.number().int().min(1).default(1), + readStatus: readStatusZodEnum.optional(), }, Astro ) const skip = (params.page - 1) * PAGE_SIZE +const readStatusInfo = params.readStatus ? getReadStatus(params.readStatus) : undefined + +const notificationWhereClause: Prisma.NotificationWhereInput = { + userId: user.id, + read: readStatusInfo?.readValue, +} + const [dbNotifications, notificationPreferences, totalNotifications, pushSubscriptions] = await Astro.locals.banners.tryMany([ [ 'Error while fetching notifications', () => prisma.notification.findMany({ - where: { - userId: user.id, - }, + where: notificationWhereClause, orderBy: { createdAt: 'desc', }, @@ -153,7 +165,7 @@ const [dbNotifications, notificationPreferences, totalNotifications, pushSubscri ], [ 'Error while fetching total notifications', - () => prisma.notification.count({ where: { userId: user.id } }), + () => prisma.notification.count({ where: notificationWhereClause }), 0, ], [ @@ -227,13 +239,18 @@ const notifications = dbNotifications.map((notification) => ({ ) } -
+

- Notifications + + {transformCase(`${readStatusInfo?.label ?? ''} notifications`.trim(), 'sentence')}

-
+
@@ -323,31 +359,12 @@ const notifications = dbNotifications.map((notification) => ({ ))} {totalPages > 1 && ( -
-
- - -
- - Page {params.page} of {totalPages} - -
- - -
-
+ )}
)