Skip to content

Commit 5855d25

Browse files
committed
update wishes
1 parent cb883ea commit 5855d25

11 files changed

Lines changed: 195 additions & 21 deletions

File tree

apps/web/actions/wishes.test.ts

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
import { beforeEach, describe, expect, it, vi } from "vitest";
22

3+
vi.hoisted(() => {
4+
process.env.NTFY_WISHES_ID = "baby-wishes";
5+
});
6+
37
vi.mock("@workspace/flags", () => ({
48
enableShareWishes: vi.fn(),
59
}));
@@ -13,13 +17,19 @@ vi.mock("@workspace/database", () => ({
1317
},
1418
}));
1519

20+
vi.mock("@workspace/ntfy", () => ({
21+
publish: vi.fn(),
22+
}));
23+
1624
import { db } from "@workspace/database";
1725
import { enableShareWishes } from "@workspace/flags";
26+
import { publish } from "@workspace/ntfy";
1827
import { getPublicWishes, submitWish } from "./wishes";
1928

2029
const mockSubmit = vi.mocked(db.wishes.submit);
2130
const mockReadPublic = vi.mocked(db.wishes.readPublic);
2231
const mockEnableShareWishes = vi.mocked(enableShareWishes);
32+
const mockPublish = vi.mocked(publish);
2333

2434
function createFormData(data: {
2535
name?: string;
@@ -39,6 +49,7 @@ describe("wishes actions", () => {
3949
beforeEach(() => {
4050
vi.clearAllMocks();
4151
mockEnableShareWishes.mockResolvedValue(true);
52+
mockPublish.mockResolvedValue(new Response());
4253
});
4354

4455
describe("submitWish", () => {
@@ -60,6 +71,12 @@ describe("wishes actions", () => {
6071
message: "Congratulations!",
6172
isPublic: true,
6273
});
74+
expect(mockPublish).toHaveBeenCalledWith({
75+
topic: "baby-wishes",
76+
title: "New wish from John Doe (john@example.com)",
77+
message: "Congratulations!",
78+
tags: ["baby", "heart"],
79+
});
6380
});
6481

6582
it("submits a private wish when isPublic is not checked", async () => {
@@ -93,6 +110,7 @@ describe("wishes actions", () => {
93110
).rejects.toThrow("Missing required fields");
94111

95112
expect(mockSubmit).not.toHaveBeenCalled();
113+
expect(mockPublish).not.toHaveBeenCalled();
96114
});
97115

98116
it("throws error when email is missing", async () => {
@@ -106,6 +124,7 @@ describe("wishes actions", () => {
106124
).rejects.toThrow("Missing required fields");
107125

108126
expect(mockSubmit).not.toHaveBeenCalled();
127+
expect(mockPublish).not.toHaveBeenCalled();
109128
});
110129

111130
it("throws error when message is missing", async () => {
@@ -119,6 +138,7 @@ describe("wishes actions", () => {
119138
).rejects.toThrow("Missing required fields");
120139

121140
expect(mockSubmit).not.toHaveBeenCalled();
141+
expect(mockPublish).not.toHaveBeenCalled();
122142
});
123143

124144
it("propagates database errors", async () => {
@@ -148,6 +168,7 @@ describe("wishes actions", () => {
148168
);
149169

150170
expect(mockSubmit).not.toHaveBeenCalled();
171+
expect(mockPublish).not.toHaveBeenCalled();
151172
});
152173
});
153174

apps/web/actions/wishes.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,9 @@
33
import type { PublicWish } from "@workspace/database";
44
import { db } from "@workspace/database";
55
import { enableShareWishes } from "@workspace/flags";
6+
import { publish } from "@workspace/ntfy";
7+
8+
const NTFY_TOPIC = process.env.NTFY_WISHES_ID;
69

710
export async function submitWish(formData: FormData): Promise<void> {
811
const isShareWishesEnabled = await enableShareWishes();
@@ -23,6 +26,15 @@ export async function submitWish(formData: FormData): Promise<void> {
2326
message,
2427
isPublic,
2528
});
29+
30+
if (NTFY_TOPIC) {
31+
await publish({
32+
topic: NTFY_TOPIC,
33+
title: `New wish from ${name} (${email})`,
34+
message,
35+
tags: ["baby", "heart"],
36+
});
37+
}
2638
}
2739

2840
export async function getPublicWishes(): Promise<PublicWish[]> {

apps/web/app/(news)/baby/_components/born-view.tsx

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,6 @@ export function BornView() {
5555

5656
<ExpandableImage
5757
alt="Baby Sarah Janet Somai shortly after birth"
58-
className="rounded-lg"
5958
src="https://7civhc6kzuyy90te.public.blob.vercel-storage.com/baby/baby_mon-DRtNwVBsw2DW1rciuXGI4goGpcfHp1"
6059
/>
6160
</div>

apps/web/app/(news)/baby/_components/signbook.tsx

Lines changed: 0 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -86,26 +86,6 @@ export function SignBook() {
8686
rows={3}
8787
/>
8888
</div>
89-
90-
<div className="space-y-1">
91-
<div className="flex items-center gap-2">
92-
<input
93-
className="size-4 accent-primary"
94-
id="isPublic"
95-
name="isPublic"
96-
type="checkbox"
97-
/>
98-
<Label
99-
className="text-muted-foreground text-xs"
100-
htmlFor="isPublic"
101-
>
102-
Share publicly on the page
103-
</Label>
104-
</div>
105-
<p className="pl-6 text-muted-foreground/70 text-xs">
106-
Public messages are reviewed before being displayed
107-
</p>
108-
</div>
10989
</div>
11090

11191
<AlertDialogFooter className="mt-6">

apps/web/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
"@vercel/speed-insights": "1.3.1",
2121
"@vercel/toolbar": "0.1.41",
2222
"@workspace/database": "workspace:*",
23+
"@workspace/ntfy": "workspace:*",
2324
"@workspace/emailing": "workspace:*",
2425
"@workspace/flags": "workspace:*",
2526
"@workspace/ui": "workspace:*",

packages/ntfy/index.test.ts

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
2+
import { publish } from "./index";
3+
4+
const mockFetch = vi.fn();
5+
6+
beforeEach(() => {
7+
vi.stubGlobal("fetch", mockFetch);
8+
mockFetch.mockResolvedValue(new Response());
9+
});
10+
11+
afterEach(() => {
12+
vi.restoreAllMocks();
13+
});
14+
15+
describe("publish", () => {
16+
it("sends a POST request to the default ntfy URL", async () => {
17+
await publish({ topic: "test", message: "hello" });
18+
19+
expect(mockFetch).toHaveBeenCalledWith("https://ntfy.sh", {
20+
method: "POST",
21+
headers: { "Content-Type": "application/json" },
22+
body: JSON.stringify({
23+
topic: "test",
24+
title: undefined,
25+
message: "hello",
26+
priority: undefined,
27+
tags: undefined,
28+
}),
29+
});
30+
});
31+
32+
it("sends to a custom base URL when provided", async () => {
33+
await publish(
34+
{ topic: "test", message: "hello" },
35+
{ baseUrl: "https://ntfy.example.com" }
36+
);
37+
38+
expect(mockFetch).toHaveBeenCalledWith(
39+
"https://ntfy.example.com",
40+
expect.objectContaining({ method: "POST" })
41+
);
42+
});
43+
44+
it("includes all optional fields in the request body", async () => {
45+
await publish({
46+
topic: "alerts",
47+
title: "Alert",
48+
message: "Something happened",
49+
priority: 5,
50+
tags: ["warning", "skull"],
51+
});
52+
53+
const body = JSON.parse(mockFetch.mock.calls[0][1].body as string);
54+
expect(body).toEqual({
55+
topic: "alerts",
56+
title: "Alert",
57+
message: "Something happened",
58+
priority: 5,
59+
tags: ["warning", "skull"],
60+
});
61+
});
62+
63+
it("returns the fetch Response", async () => {
64+
const expected = new Response("ok", { status: 200 });
65+
mockFetch.mockResolvedValue(expected);
66+
67+
const result = await publish({ topic: "test", message: "hi" });
68+
69+
expect(result).toBe(expected);
70+
});
71+
72+
it("propagates fetch errors", async () => {
73+
mockFetch.mockRejectedValue(new Error("Network error"));
74+
75+
await expect(publish({ topic: "test", message: "hi" })).rejects.toThrow(
76+
"Network error"
77+
);
78+
});
79+
});

packages/ntfy/index.ts

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
const DEFAULT_BASE_URL = "https://ntfy.sh";
2+
3+
export interface NtfyMessage {
4+
topic: string;
5+
title?: string;
6+
message: string;
7+
priority?: 1 | 2 | 3 | 4 | 5;
8+
tags?: string[];
9+
}
10+
11+
export interface NtfyOptions {
12+
baseUrl?: string;
13+
}
14+
15+
export function publish(
16+
msg: NtfyMessage,
17+
options?: NtfyOptions
18+
): Promise<Response> {
19+
const baseUrl = options?.baseUrl ?? DEFAULT_BASE_URL;
20+
21+
return fetch(baseUrl, {
22+
method: "POST",
23+
headers: { "Content-Type": "application/json" },
24+
body: JSON.stringify({
25+
topic: msg.topic,
26+
title: msg.title,
27+
message: msg.message,
28+
priority: msg.priority,
29+
tags: msg.tags,
30+
}),
31+
});
32+
}

packages/ntfy/package.json

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
{
2+
"name": "@workspace/ntfy",
3+
"version": "0.0.0",
4+
"private": true,
5+
"exports": {
6+
".": "./index.ts"
7+
},
8+
"scripts": {
9+
"clean": "git clean -xdf .cache .turbo dist node_modules",
10+
"test": "vitest run",
11+
"test:watch": "vitest",
12+
"typecheck": "tsc --noEmit --emitDeclarationOnly false"
13+
},
14+
"devDependencies": {
15+
"@workspace/typescript-config": "workspace:*",
16+
"typescript": "5.9.3",
17+
"vitest": "3.2.4"
18+
}
19+
}

packages/ntfy/tsconfig.json

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
{
2+
"extends": "@workspace/typescript-config/base.json",
3+
"compilerOptions": {
4+
"baseUrl": "."
5+
},
6+
"include": ["**/*.ts"],
7+
"exclude": ["node_modules"]
8+
}

packages/ntfy/vitest.config.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
import { defineConfig } from "vitest/config";
2+
3+
export default defineConfig({
4+
test: {
5+
environment: "node",
6+
include: ["**/*.test.ts"],
7+
},
8+
});

0 commit comments

Comments
 (0)