v3: correctly using SITE_NAME across the app, added notifications on comments
All checks were successful
Build and Publish Docker Image / build-and-push (push) Successful in 44s
All checks were successful
Build and Publish Docker Image / build-and-push (push) Successful in 44s
This commit is contained in:
@@ -6,10 +6,10 @@
|
||||
# Defaults to http://localhost:GERBEUR_PORT for local development.
|
||||
# GERBEUR_PUBLIC_URL=https://example.com
|
||||
|
||||
# Site name used in OG meta tags
|
||||
# Site name shown in the browser tab, app header, OG meta tags, and emails.
|
||||
GERBEUR_SITE_NAME=gerbeur
|
||||
|
||||
# Port the API server listens on (the container's internal port)
|
||||
# Port the API server listens on (the container's internal port).
|
||||
GERBEUR_PORT=8000
|
||||
|
||||
# Network interface Oak binds to. Default: 0.0.0.0 (all interfaces, required for Docker).
|
||||
|
||||
@@ -12,9 +12,7 @@ COPY public/ ./public/
|
||||
COPY scripts/ ./scripts/
|
||||
COPY src/ ./src/
|
||||
|
||||
# In same-origin deployments (API serves the frontend), no build args are needed
|
||||
# — the frontend uses relative URLs at runtime. Only set VITE_API_HOSTNAME if
|
||||
# the API lives on a different host than the frontend (e.g. a separate API server).
|
||||
# VITE_API_* are only needed when the API lives on a different host than the frontend.
|
||||
ARG VITE_API_PROTOCOL
|
||||
ARG VITE_API_HOSTNAME
|
||||
ARG VITE_API_PORT
|
||||
|
||||
@@ -46,7 +46,7 @@ See [`.env.example`](.env.example) for the full list with descriptions. Key vari
|
||||
| `GERBEUR_LISTEN_HOST` | Network interface Oak binds to; use `127.0.0.1` to restrict to loopback | `0.0.0.0` |
|
||||
| `GERBEUR_FRONTEND_URL` | Frontend base URL for email links and CORS; auto-added to allowed origins — the only variable needed when the frontend runs on a separate host | `GERBEUR_PUBLIC_URL` |
|
||||
| `GERBEUR_ALLOWED_ORIGINS` | Comma-separated extra origins for CORS/WebSocket; `PUBLIC_URL` and `FRONTEND_URL` are always included — typically only needed in dev for the Vite server | `""` (empty) |
|
||||
| `GERBEUR_SITE_NAME` | Site name used in OG meta tags and emails | `gerbeur` |
|
||||
| `GERBEUR_SITE_NAME` | Site name used in the browser tab, app header, OG meta tags, and emails | `gerbeur` |
|
||||
| `GERBEUR_SMTPS_URL` | SMTPS connection URL for outgoing email (`smtps://user:pass@host:465`) | unset |
|
||||
| `GERBEUR_FROM_EMAIL` | Sender address for outgoing emails — required when `GERBEUR_SMTPS_URL` is set | unset |
|
||||
| `GERBEUR_WELCOME_EMAIL_BODY` | Markdown body for the account-creation welcome email; supports `{{username}}` and `{{site_name}}` | built-in template |
|
||||
@@ -63,12 +63,15 @@ The standard deployment runs API and frontend in a single container. The API ser
|
||||
```sh
|
||||
docker build -t gerbeur .
|
||||
|
||||
docker build -t gerbeur .
|
||||
|
||||
docker run -d \
|
||||
-p 8000:8000 \
|
||||
-v gerbeur-db:/app/api/sql \
|
||||
-v gerbeur-uploads:/app/api/uploads \
|
||||
-e GERBEUR_JWT_SECRET=$(openssl rand -hex 32) \
|
||||
-e GERBEUR_PUBLIC_URL=https://example.com \
|
||||
-e GERBEUR_SITE_NAME=mysite \
|
||||
--name gerbeur \
|
||||
gerbeur
|
||||
```
|
||||
|
||||
@@ -1,13 +1,30 @@
|
||||
import { Context, Next, send } from "@oak/oak";
|
||||
import { OG_SITE_NAME } from "../config.ts";
|
||||
|
||||
async function serveIndexHtml(
|
||||
context: Context<Record<string, object>>,
|
||||
root: string,
|
||||
) {
|
||||
const filePath = `${root}/index.html`;
|
||||
const raw = await Deno.readTextFile(filePath);
|
||||
const html = raw.replaceAll("__SITE_NAME__", OG_SITE_NAME);
|
||||
context.response.type = "text/html";
|
||||
context.response.body = html;
|
||||
}
|
||||
|
||||
export function routeStaticFilesFrom(staticPaths: string[]) {
|
||||
return async (context: Context<Record<string, object>>, next: Next) => {
|
||||
const pathname = context.request.url.pathname;
|
||||
|
||||
// Serve index.html with runtime placeholder replacement
|
||||
if (pathname === "/" || pathname === "/index.html") {
|
||||
await serveIndexHtml(context, staticPaths[0]);
|
||||
return;
|
||||
}
|
||||
|
||||
for (const path of staticPaths) {
|
||||
try {
|
||||
await send(context, context.request.url.pathname, {
|
||||
root: path,
|
||||
index: "index.html",
|
||||
});
|
||||
await send(context, pathname, { root: path });
|
||||
return;
|
||||
} catch {
|
||||
continue;
|
||||
@@ -15,7 +32,7 @@ export function routeStaticFilesFrom(staticPaths: string[]) {
|
||||
}
|
||||
|
||||
// SPA fallback: serve index.html so client-side routes work on direct navigation
|
||||
await send(context, "/index.html", { root: staticPaths[0] });
|
||||
await serveIndexHtml(context, staticPaths[0]);
|
||||
await next();
|
||||
};
|
||||
}
|
||||
|
||||
@@ -617,7 +617,8 @@ export type NotificationType =
|
||||
| "user_dump_posted"
|
||||
| "playlist_dump_added"
|
||||
| "dump_upvoted"
|
||||
| "user_mentioned";
|
||||
| "user_mentioned"
|
||||
| "dump_commented";
|
||||
|
||||
export interface PlaylistFollowedData {
|
||||
followerId: string;
|
||||
@@ -661,13 +662,22 @@ export interface UserMentionedData {
|
||||
dumpId?: string;
|
||||
}
|
||||
|
||||
export interface DumpCommentedData {
|
||||
commenterId: string;
|
||||
commenterUsername: string;
|
||||
commentId: string;
|
||||
dumpId: string;
|
||||
dumpTitle: string;
|
||||
}
|
||||
|
||||
export type NotificationData =
|
||||
| PlaylistFollowedData
|
||||
| UserFollowedData
|
||||
| UserDumpPostedData
|
||||
| PlaylistDumpAddedData
|
||||
| DumpUpvotedData
|
||||
| UserMentionedData;
|
||||
| UserMentionedData
|
||||
| DumpCommentedData;
|
||||
|
||||
export interface Notification {
|
||||
id: string;
|
||||
|
||||
@@ -5,7 +5,10 @@ import {
|
||||
} from "../model/interfaces.ts";
|
||||
import { type SQLOutputValue } from "node:sqlite";
|
||||
import { commentRowToApi, db, isCommentRow } from "../model/db.ts";
|
||||
import { notifyMentions } from "./notification-service.ts";
|
||||
import {
|
||||
notifyDumpOwnerNewComment,
|
||||
notifyMentions,
|
||||
} from "./notification-service.ts";
|
||||
import { linkAttachments } from "./attachment-service.ts";
|
||||
|
||||
const SELECT_COLS =
|
||||
@@ -66,6 +69,7 @@ export function createComment(
|
||||
const dumpRow = db.prepare(`SELECT title FROM dumps WHERE id = ?;`).get(
|
||||
dumpId,
|
||||
) as { title: string } | undefined;
|
||||
notifyDumpOwnerNewComment(userId, id, dumpId);
|
||||
notifyMentions(userId, body, "comment", id, dumpRow?.title ?? "", dumpId);
|
||||
linkAttachments(body, id);
|
||||
return comment;
|
||||
|
||||
@@ -276,6 +276,36 @@ export function notifyMentions(
|
||||
}
|
||||
}
|
||||
|
||||
export function notifyDumpOwnerNewComment(
|
||||
commenterId: string,
|
||||
commentId: string,
|
||||
dumpId: string,
|
||||
): void {
|
||||
const commenterRow = db.prepare(
|
||||
`SELECT username FROM users WHERE id = ?;`,
|
||||
).get(commenterId) as { username: string } | undefined;
|
||||
|
||||
const dumpRow = db.prepare(
|
||||
`SELECT title, user_id FROM dumps WHERE id = ?;`,
|
||||
).get(dumpId) as { title: string; user_id: string } | undefined;
|
||||
|
||||
if (!commenterRow || !dumpRow) return;
|
||||
if (commenterId === dumpRow.user_id) return; // no self-notification
|
||||
|
||||
createNotification(
|
||||
dumpRow.user_id,
|
||||
"dump_commented",
|
||||
{
|
||||
commenterId,
|
||||
commenterUsername: commenterRow.username,
|
||||
commentId,
|
||||
dumpId,
|
||||
dumpTitle: dumpRow.title,
|
||||
},
|
||||
`comment:${commentId}`,
|
||||
);
|
||||
}
|
||||
|
||||
export function notifyPlaylistFollowersNewDump(
|
||||
playlistId: string,
|
||||
playlistTitle: string,
|
||||
|
||||
@@ -8,9 +8,10 @@
|
||||
<link href="https://fonts.googleapis.com/css2?family=Saira:ital,wght@0,100..900;1,100..900&family=Space+Grotesk:wght@400;700;900&display=swap" rel="stylesheet">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<meta name="theme-color" content="#111827" />
|
||||
<meta name="site-name" content="__SITE_NAME__" />
|
||||
<link rel="manifest" href="/manifest.webmanifest" />
|
||||
<link rel="apple-touch-icon" href="/favicon.svg" />
|
||||
<title>Dumps</title>
|
||||
<title>__SITE_NAME__</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
|
||||
@@ -29,7 +29,8 @@ export function AppHeader(
|
||||
className={`app-header${centerSlot ? " app-header--has-center" : ""}`}
|
||||
>
|
||||
<Link to="/?tab=hot" className="app-header-brand">
|
||||
🚚 gerbeur
|
||||
🚚 {document.querySelector<HTMLMetaElement>('meta[name="site-name"]')
|
||||
?.content ?? "gerbeur"}
|
||||
</Link>
|
||||
|
||||
{centerSlot && <div className="app-header-center">{centerSlot}</div>}
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -92,45 +92,51 @@ msgstr "+ New playlist"
|
||||
msgid "+ Playlist"
|
||||
msgstr "+ Playlist"
|
||||
|
||||
#. placeholder {0}: d.commenterUsername
|
||||
#. placeholder {1}: d.dumpTitle
|
||||
#: src/pages/Notifications.tsx:171
|
||||
msgid "<0>{0}</0> commented on <1>{1}</1>"
|
||||
msgstr "<0>{0}</0> commented on <1>{1}</1>"
|
||||
|
||||
#. placeholder {0}: d.followerUsername
|
||||
#. placeholder {1}: d.playlistTitle
|
||||
#: src/pages/Notifications.tsx:124
|
||||
#: src/pages/Notifications.tsx:131
|
||||
msgid "<0>{0}</0> followed your playlist <1>{1}</1>"
|
||||
msgstr "<0>{0}</0> followed your playlist <1>{1}</1>"
|
||||
|
||||
#. placeholder {0}: d.mentionerUsername
|
||||
#: src/pages/Notifications.tsx:166
|
||||
#: src/pages/Notifications.tsx:183
|
||||
msgid "<0>{0}</0> mentioned you in <1>{where}</1>"
|
||||
msgstr "<0>{0}</0> mentioned you in <1>{where}</1>"
|
||||
|
||||
#. placeholder {0}: d.dumperUsername
|
||||
#. placeholder {1}: d.dumpTitle
|
||||
#: src/pages/Notifications.tsx:134
|
||||
#: src/pages/Notifications.tsx:141
|
||||
msgid "<0>{0}</0> posted <1>{1}</1>"
|
||||
msgstr "<0>{0}</0> posted <1>{1}</1>"
|
||||
|
||||
#. placeholder {0}: d.followerUsername
|
||||
#: src/pages/Notifications.tsx:115
|
||||
#: src/pages/Notifications.tsx:122
|
||||
msgid "<0>{0}</0> started following you"
|
||||
msgstr "<0>{0}</0> started following you"
|
||||
|
||||
#. placeholder {0}: d.voterUsername
|
||||
#. placeholder {1}: d.dumpTitle
|
||||
#: src/pages/Notifications.tsx:154
|
||||
#: src/pages/Notifications.tsx:161
|
||||
msgid "<0>{0}</0> upvoted <1>{1}</1>"
|
||||
msgstr "<0>{0}</0> upvoted <1>{1}</1>"
|
||||
|
||||
#. placeholder {0}: d.dumpTitle
|
||||
#. placeholder {1}: d.playlistTitle
|
||||
#: src/pages/Notifications.tsx:144
|
||||
#: src/pages/Notifications.tsx:151
|
||||
msgid "<0>{0}</0> was added to <1>{1}</1>"
|
||||
msgstr "<0>{0}</0> was added to <1>{1}</1>"
|
||||
|
||||
#: src/pages/Notifications.tsx:164
|
||||
#: src/pages/Notifications.tsx:181
|
||||
msgid "a comment"
|
||||
msgstr "a comment"
|
||||
|
||||
#: src/pages/Notifications.tsx:164
|
||||
#: src/pages/Notifications.tsx:181
|
||||
msgid "a post"
|
||||
msgstr "a post"
|
||||
|
||||
@@ -373,7 +379,7 @@ msgstr "Dumps"
|
||||
msgid "Dumps ({0}{1})"
|
||||
msgstr "Dumps ({0}{1})"
|
||||
|
||||
#: src/pages/Notifications.tsx:349
|
||||
#: src/pages/Notifications.tsx:360
|
||||
msgid "Earlier"
|
||||
msgstr "Earlier"
|
||||
|
||||
@@ -432,6 +438,7 @@ msgstr "Failed to generate invite"
|
||||
#: src/pages/index/HotFeed.tsx:36
|
||||
#: src/pages/index/JournalFeed.tsx:48
|
||||
#: src/pages/index/NewFeed.tsx:36
|
||||
#: src/pages/Notifications.tsx:334
|
||||
#: src/pages/UserPublicProfile.tsx:1106
|
||||
#: src/pages/UserPublicProfile.tsx:1148
|
||||
#: src/pages/UserPublicProfile.tsx:1193
|
||||
@@ -623,7 +630,7 @@ msgstr "Live updates are temporarily disconnected. Trying to reconnect…"
|
||||
msgid "Live updates unavailable."
|
||||
msgstr "Live updates unavailable."
|
||||
|
||||
#: src/pages/Notifications.tsx:390
|
||||
#: src/pages/Notifications.tsx:407
|
||||
msgid "Load more"
|
||||
msgstr "Load more"
|
||||
|
||||
@@ -658,8 +665,8 @@ msgstr "Loading profile…"
|
||||
#: src/pages/index/HotFeed.tsx:32
|
||||
#: src/pages/index/JournalFeed.tsx:44
|
||||
#: src/pages/index/NewFeed.tsx:32
|
||||
#: src/pages/Notifications.tsx:313
|
||||
#: src/pages/Notifications.tsx:389
|
||||
#: src/pages/Notifications.tsx:330
|
||||
#: src/pages/Notifications.tsx:406
|
||||
#: src/pages/UserDumps.tsx:51
|
||||
#: src/pages/UserPlaylists.tsx:342
|
||||
#: src/pages/UserPublicProfile.tsx:1100
|
||||
@@ -692,7 +699,7 @@ msgstr "Login failed"
|
||||
msgid "Max 50 MB"
|
||||
msgstr "Max 50 MB"
|
||||
|
||||
#: src/pages/Notifications.tsx:306
|
||||
#: src/pages/Notifications.tsx:323
|
||||
msgid "new"
|
||||
msgstr "new"
|
||||
|
||||
@@ -762,7 +769,7 @@ msgstr "No users match \"{q}\"."
|
||||
msgid "Not following anyone yet."
|
||||
msgstr "Not following anyone yet."
|
||||
|
||||
#: src/pages/Notifications.tsx:324
|
||||
#: src/pages/Notifications.tsx:341
|
||||
#: src/pages/UserDumps.tsx:123
|
||||
#: src/pages/UserPublicProfile.tsx:1340
|
||||
#: src/pages/UserPublicProfile.tsx:1463
|
||||
@@ -771,7 +778,7 @@ msgid "Nothing here yet."
|
||||
msgstr "Nothing here yet."
|
||||
|
||||
#: src/components/NotificationBell.tsx:42
|
||||
#: src/pages/Notifications.tsx:302
|
||||
#: src/pages/Notifications.tsx:319
|
||||
msgid "Notifications"
|
||||
msgstr "Notifications"
|
||||
|
||||
@@ -1021,7 +1028,7 @@ msgstr "This reset link is missing or malformed."
|
||||
msgid "Title"
|
||||
msgstr "Title"
|
||||
|
||||
#: src/pages/Notifications.tsx:346
|
||||
#: src/pages/Notifications.tsx:357
|
||||
msgid "Today"
|
||||
msgstr "Today"
|
||||
|
||||
@@ -1115,11 +1122,11 @@ msgstr "Why are you dumping this?"
|
||||
msgid "Write a reply…"
|
||||
msgstr "Write a reply…"
|
||||
|
||||
#: src/pages/Notifications.tsx:348
|
||||
#: src/pages/Notifications.tsx:359
|
||||
msgid "Yesterday"
|
||||
msgstr "Yesterday"
|
||||
|
||||
#: src/pages/Notifications.tsx:327
|
||||
#: src/pages/Notifications.tsx:344
|
||||
msgid "You'll be notified when someone follows your playlists, upvotes your dumps, or posts new content."
|
||||
msgstr "You'll be notified when someone follows your playlists, upvotes your dumps, or posts new content."
|
||||
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -92,45 +92,51 @@ msgstr "+ Nouvelle collection"
|
||||
msgid "+ Playlist"
|
||||
msgstr "+ Collection"
|
||||
|
||||
#. placeholder {0}: d.commenterUsername
|
||||
#. placeholder {1}: d.dumpTitle
|
||||
#: src/pages/Notifications.tsx:171
|
||||
msgid "<0>{0}</0> commented on <1>{1}</1>"
|
||||
msgstr ""
|
||||
|
||||
#. placeholder {0}: d.followerUsername
|
||||
#. placeholder {1}: d.playlistTitle
|
||||
#: src/pages/Notifications.tsx:124
|
||||
#: src/pages/Notifications.tsx:131
|
||||
msgid "<0>{0}</0> followed your playlist <1>{1}</1>"
|
||||
msgstr "<0>{0}</0> a suivi votre collection <1>{1}</1>"
|
||||
|
||||
#. placeholder {0}: d.mentionerUsername
|
||||
#: src/pages/Notifications.tsx:166
|
||||
#: src/pages/Notifications.tsx:183
|
||||
msgid "<0>{0}</0> mentioned you in <1>{where}</1>"
|
||||
msgstr "<0>{0}</0> vous a mentionné dans <1>{where}</1>"
|
||||
|
||||
#. placeholder {0}: d.dumperUsername
|
||||
#. placeholder {1}: d.dumpTitle
|
||||
#: src/pages/Notifications.tsx:134
|
||||
#: src/pages/Notifications.tsx:141
|
||||
msgid "<0>{0}</0> posted <1>{1}</1>"
|
||||
msgstr "<0>{0}</0> a publié <1>{1}</1>"
|
||||
|
||||
#. placeholder {0}: d.followerUsername
|
||||
#: src/pages/Notifications.tsx:115
|
||||
#: src/pages/Notifications.tsx:122
|
||||
msgid "<0>{0}</0> started following you"
|
||||
msgstr "<0>{0}</0> a commencé à vous suivre"
|
||||
|
||||
#. placeholder {0}: d.voterUsername
|
||||
#. placeholder {1}: d.dumpTitle
|
||||
#: src/pages/Notifications.tsx:154
|
||||
#: src/pages/Notifications.tsx:161
|
||||
msgid "<0>{0}</0> upvoted <1>{1}</1>"
|
||||
msgstr "<0>{0}</0> a voté pour <1>{1}</1>"
|
||||
|
||||
#. placeholder {0}: d.dumpTitle
|
||||
#. placeholder {1}: d.playlistTitle
|
||||
#: src/pages/Notifications.tsx:144
|
||||
#: src/pages/Notifications.tsx:151
|
||||
msgid "<0>{0}</0> was added to <1>{1}</1>"
|
||||
msgstr "<0>{0}</0> a été ajouté à <1>{1}</1>"
|
||||
|
||||
#: src/pages/Notifications.tsx:164
|
||||
#: src/pages/Notifications.tsx:181
|
||||
msgid "a comment"
|
||||
msgstr "un commentaire"
|
||||
|
||||
#: src/pages/Notifications.tsx:164
|
||||
#: src/pages/Notifications.tsx:181
|
||||
msgid "a post"
|
||||
msgstr "une publication"
|
||||
|
||||
@@ -357,7 +363,7 @@ msgstr "Recos"
|
||||
msgid "Dumps ({0}{1})"
|
||||
msgstr "Recos ({0}{1})"
|
||||
|
||||
#: src/pages/Notifications.tsx:349
|
||||
#: src/pages/Notifications.tsx:360
|
||||
msgid "Earlier"
|
||||
msgstr "Plus tôt"
|
||||
|
||||
@@ -416,6 +422,7 @@ msgstr "Impossible de générer une invitation"
|
||||
#: src/pages/index/HotFeed.tsx:36
|
||||
#: src/pages/index/JournalFeed.tsx:48
|
||||
#: src/pages/index/NewFeed.tsx:36
|
||||
#: src/pages/Notifications.tsx:334
|
||||
#: src/pages/UserPublicProfile.tsx:1106
|
||||
#: src/pages/UserPublicProfile.tsx:1148
|
||||
#: src/pages/UserPublicProfile.tsx:1193
|
||||
@@ -570,7 +577,7 @@ msgstr "Les mises à jour en direct sont temporairement interrompues. Tentative
|
||||
msgid "Live updates unavailable."
|
||||
msgstr "Mises à jour en direct indisponibles."
|
||||
|
||||
#: src/pages/Notifications.tsx:390
|
||||
#: src/pages/Notifications.tsx:407
|
||||
msgid "Load more"
|
||||
msgstr "Charger plus"
|
||||
|
||||
@@ -605,8 +612,8 @@ msgstr "Chargement du profil…"
|
||||
#: src/pages/index/HotFeed.tsx:32
|
||||
#: src/pages/index/JournalFeed.tsx:44
|
||||
#: src/pages/index/NewFeed.tsx:32
|
||||
#: src/pages/Notifications.tsx:313
|
||||
#: src/pages/Notifications.tsx:389
|
||||
#: src/pages/Notifications.tsx:330
|
||||
#: src/pages/Notifications.tsx:406
|
||||
#: src/pages/UserDumps.tsx:51
|
||||
#: src/pages/UserPlaylists.tsx:342
|
||||
#: src/pages/UserPublicProfile.tsx:1100
|
||||
@@ -639,7 +646,7 @@ msgstr "Connexion échouée"
|
||||
msgid "Max 50 MB"
|
||||
msgstr "Max 50 Mo"
|
||||
|
||||
#: src/pages/Notifications.tsx:306
|
||||
#: src/pages/Notifications.tsx:323
|
||||
msgid "new"
|
||||
msgstr "nouveau"
|
||||
|
||||
@@ -705,7 +712,7 @@ msgstr "Aucun utilisateur ne correspond à « {q} »."
|
||||
msgid "Not following anyone yet."
|
||||
msgstr "Aucun abonnement pour le moment."
|
||||
|
||||
#: src/pages/Notifications.tsx:324
|
||||
#: src/pages/Notifications.tsx:341
|
||||
#: src/pages/UserDumps.tsx:123
|
||||
#: src/pages/UserPublicProfile.tsx:1340
|
||||
#: src/pages/UserPublicProfile.tsx:1463
|
||||
@@ -714,7 +721,7 @@ msgid "Nothing here yet."
|
||||
msgstr "Rien ici pour l'instant."
|
||||
|
||||
#: src/components/NotificationBell.tsx:42
|
||||
#: src/pages/Notifications.tsx:302
|
||||
#: src/pages/Notifications.tsx:319
|
||||
msgid "Notifications"
|
||||
msgstr "Notifications"
|
||||
|
||||
@@ -952,7 +959,7 @@ msgstr "Ce lien de réinitialisation est absent ou malformé."
|
||||
msgid "Title"
|
||||
msgstr "Titre"
|
||||
|
||||
#: src/pages/Notifications.tsx:346
|
||||
#: src/pages/Notifications.tsx:357
|
||||
msgid "Today"
|
||||
msgstr "Aujourd'hui"
|
||||
|
||||
@@ -1034,11 +1041,11 @@ msgstr "Pourquoi recommandez-vous ça ?"
|
||||
msgid "Write a reply…"
|
||||
msgstr "Écrire une réponse…"
|
||||
|
||||
#: src/pages/Notifications.tsx:348
|
||||
#: src/pages/Notifications.tsx:359
|
||||
msgid "Yesterday"
|
||||
msgstr "Hier"
|
||||
|
||||
#: src/pages/Notifications.tsx:327
|
||||
#: src/pages/Notifications.tsx:344
|
||||
msgid "You'll be notified when someone follows your playlists, upvotes your dumps, or posts new content."
|
||||
msgstr "Vous serez notifié lorsque quelqu'un suit vos collections, vote pour vos recos ou publie du nouveau contenu."
|
||||
|
||||
|
||||
14
src/model.ts
14
src/model.ts
@@ -248,7 +248,8 @@ export type NotificationType =
|
||||
| "user_dump_posted"
|
||||
| "playlist_dump_added"
|
||||
| "dump_upvoted"
|
||||
| "user_mentioned";
|
||||
| "user_mentioned"
|
||||
| "dump_commented";
|
||||
|
||||
export interface PlaylistFollowedData {
|
||||
followerId: string;
|
||||
@@ -292,13 +293,22 @@ export interface UserMentionedData {
|
||||
dumpId?: string;
|
||||
}
|
||||
|
||||
export interface DumpCommentedData {
|
||||
commenterId: string;
|
||||
commenterUsername: string;
|
||||
commentId: string;
|
||||
dumpId: string;
|
||||
dumpTitle: string;
|
||||
}
|
||||
|
||||
export type NotificationData =
|
||||
| PlaylistFollowedData
|
||||
| UserFollowedData
|
||||
| UserDumpPostedData
|
||||
| PlaylistDumpAddedData
|
||||
| DumpUpvotedData
|
||||
| UserMentionedData;
|
||||
| UserMentionedData
|
||||
| DumpCommentedData;
|
||||
|
||||
export interface Notification {
|
||||
id: string;
|
||||
|
||||
@@ -10,6 +10,7 @@ import { ErrorCard } from "../components/ErrorCard.tsx";
|
||||
import { Tooltip } from "../components/Tooltip.tsx";
|
||||
import { useWS } from "../hooks/useWS.ts";
|
||||
import type {
|
||||
DumpCommentedData,
|
||||
DumpUpvotedData,
|
||||
Notification,
|
||||
NotificationData,
|
||||
@@ -36,7 +37,13 @@ type State =
|
||||
loadingMore: boolean;
|
||||
};
|
||||
|
||||
type NotifIconKind = "upvote" | "follow" | "dump" | "playlist" | "mention";
|
||||
type NotifIconKind =
|
||||
| "upvote"
|
||||
| "follow"
|
||||
| "dump"
|
||||
| "playlist"
|
||||
| "mention"
|
||||
| "comment";
|
||||
|
||||
function notifIconKind(type: Notification["type"]): NotifIconKind {
|
||||
switch (type) {
|
||||
@@ -52,6 +59,8 @@ function notifIconKind(type: Notification["type"]): NotifIconKind {
|
||||
return "playlist";
|
||||
case "user_mentioned":
|
||||
return "mention";
|
||||
case "dump_commented":
|
||||
return "comment";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -75,6 +84,7 @@ function NotifIcon({ type }: { type: Notification["type"] }) {
|
||||
dump: "🚚",
|
||||
playlist: "📜",
|
||||
mention: "@",
|
||||
comment: "💬",
|
||||
};
|
||||
return (
|
||||
<span className={`notif-icon notif-icon--${kind}`}>
|
||||
@@ -96,6 +106,10 @@ function notificationLink(n: Notification): string {
|
||||
return `/dumps/${(data as PlaylistDumpAddedData).dumpId}`;
|
||||
case "dump_upvoted":
|
||||
return `/dumps/${(data as DumpUpvotedData).dumpId}`;
|
||||
case "dump_commented":
|
||||
return `/dumps/${(data as DumpCommentedData).dumpId}#comment-${
|
||||
(data as DumpCommentedData).commentId
|
||||
}`;
|
||||
case "user_mentioned": {
|
||||
const d = data as UserMentionedData;
|
||||
if (d.contextType === "comment") {
|
||||
@@ -159,6 +173,16 @@ function notificationContent(n: Notification): React.ReactNode {
|
||||
</Trans>
|
||||
);
|
||||
}
|
||||
case "dump_commented": {
|
||||
const d = data as DumpCommentedData;
|
||||
return (
|
||||
<Trans>
|
||||
<strong>{d.commenterUsername}</strong>
|
||||
{" commented on "}
|
||||
<strong>{d.dumpTitle}</strong>
|
||||
</Trans>
|
||||
);
|
||||
}
|
||||
case "user_mentioned": {
|
||||
const d = data as UserMentionedData;
|
||||
const where = d.contextTitle ||
|
||||
|
||||
@@ -4,6 +4,8 @@ import { lingui } from "@lingui/vite-plugin";
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
|
||||
const SITE_NAME = process.env.GERBEUR_SITE_NAME || "gerbeur";
|
||||
|
||||
function manifestPlugin(): Plugin {
|
||||
const cssPath = path.resolve("src/index.css");
|
||||
const outPath = path.resolve("public/manifest.webmanifest");
|
||||
@@ -21,8 +23,8 @@ function manifestPlugin(): Plugin {
|
||||
const bgColor = cssVar(rootBlock, "--color-bg") ?? "#111827";
|
||||
|
||||
const manifest = {
|
||||
name: "gerbeur",
|
||||
short_name: "gerbeur",
|
||||
name: SITE_NAME,
|
||||
short_name: SITE_NAME,
|
||||
start_url: "/",
|
||||
display: "standalone",
|
||||
background_color: bgColor,
|
||||
@@ -73,6 +75,18 @@ export default defineConfig({
|
||||
},
|
||||
plugins: [
|
||||
manifestPlugin(),
|
||||
{
|
||||
// In dev the API server isn't serving index.html, so replace the placeholder here.
|
||||
// In production the Oak server does the replacement at runtime (see api/lib/static.ts).
|
||||
name: "inject-site-name",
|
||||
transformIndexHtml: {
|
||||
order: "pre",
|
||||
handler: (html, ctx) =>
|
||||
ctx.server
|
||||
? html.replaceAll("__SITE_NAME__", SITE_NAME)
|
||||
: html,
|
||||
},
|
||||
},
|
||||
lingui(),
|
||||
react({
|
||||
plugins: [["@lingui/swc-plugin", {}]],
|
||||
|
||||
Reference in New Issue
Block a user