From 36532bc1f1b417a1d7870e9c0670b60cde4310b5 Mon Sep 17 00:00:00 2001 From: Ryan Freeman Date: Tue, 22 Apr 2025 16:48:06 +0100 Subject: [PATCH] Add subscription feature --- .env.example | 6 +++- .env.gpg | Bin 882 -> 962 bytes .gitignore | 2 ++ app/actions/subscribe.ts | 32 +++++++++++++++++ components/layouts/ArticleLayout.tsx | 2 ++ components/ui/StatusMessage.tsx | 7 ++-- components/ui/Subscribe.tsx | 50 +++++++++++++++++++++++++++ lib/listmonk.ts | 28 +++++++++++++++ 8 files changed, 123 insertions(+), 4 deletions(-) create mode 100644 app/actions/subscribe.ts create mode 100644 components/ui/Subscribe.tsx create mode 100644 lib/listmonk.ts diff --git a/.env.example b/.env.example index 7af1733..ac3f5b9 100644 --- a/.env.example +++ b/.env.example @@ -13,4 +13,8 @@ AUTH_SECRET= AUTH_TRUST_HOST= AUTH_REDIRECT_PROXY_URL= NTFY_URL= -NTFY_TOKEN= \ No newline at end of file +NTFY_TOKEN= +LISTMONK_URL= +LISTMONK_LIST_ID= +LISTMONK_USERNAME= +LISTMONK_TOKEN= \ No newline at end of file diff --git a/.env.gpg b/.env.gpg index b04ca65097d64b5ee848696b2ef2aa7875b36329..b7dd78f2eecb7f5a677880dbf89402bd4f143c2a 100644 GIT binary patch literal 962 zcmV;z13mnV4Fm@R0@!N;!Mg{3?*G#10WT-3i^$O^z<(8}eNd@6ZU*R>p=+{b>tDa_ z0g~MaP6m?(P?yRlL;?miF+VxylakI?k~kqs3jNIBW$p26iM%>q5A^WF{q?OCk4L?i z(%~Vf`=55lH9--ouoBOF%Hdal13n`opT`@4eLfGay})l+O>@yHZO1~LwdC2Gwt3p| zHPKpGl=6RY0gLChRbbUqe>xDSwoI}|q*0XOD0gJ4Nym?nW#!**E6RP2vQ=IZ?EL%z zZoVCju13#7kJ7A~%hI)J)5KtU40{nKa;E{k`?54etkp@82>QQN)6ha8i`1@EMnbysi21ly>9m4#Q zqd-!9f-V+Q=o#KfEnN!fb3$Zi-z`#EW+Q%AQBI^j0OQR}!d7cJrGf|J@i8@7 zXWUSJ8mFHBlcna(W>reGrQEVfUG^}+MT;hBy9gp_5*(|(9ZddQy7~%#U>n495Nf<( zcaE$^pu>Bz0bX6Tr5+rc!0++a0dA7-F}g!n><q5xI6u`q`Gp3wCd4UUwMD6p5bvpL#qjftXQJi{ABo$TMp zDM1IDBS*OVhm>jiM&zs6$C;(!ueijW*2rKx6^`OV=6YhItsz`TL z=FfqlqI=KL|4Kvt$K0s!IGOxvnZPGlUxjgl`@Q4bTim@DF}Y*!yMgbHrrLR+CfAJE ztiAFrs~!&@c3lnWcaOjqcv{mzXWNV3V<(n)R9{TiWWfmBPMN+j{c=o#bH?N8W)eE(zYEgcK*bqO8`0mpZ7blOBG~gW6ps zLL%NTSOWTf72qwU33pNn!0G(Uvg#}GS)xg&XW*`?M?&nLM}Q9qIGW7^4U1@3-FcDS zKm?25nb7b%R|PW%oD>I-=zEi;v>u5@t}^PU!L55Q!hk&k z7wMynQM|&tqkxE%8v4{!8*I&JG@tQxk1nPMAC)4(2v@d3#eO1|?&Y#g$9hv$_~#iblj#4j_n(TycybOjx%6EoPq2Dzw6!1R|qkX%O60jlbhc#vS^w z?mwHx%QOoGIHhlnwS<|{(4{X)q{~QfH6A%&$o9BrmFlO7j4%X~A&Iy#U*(~2Jrqv` z*Pc&ax^PZ`@C(7=nkhQg^U#vxgNEr z0iUf*lT+y>G9sL88d*fjq+nSob9K1>cc>ubT*(}Tq@<4`xga0NK<@`UQE5Qi-2rAA zn^AV*h4TIf0`yvV1t>sqzf*W|j(+c2RA!NcrFB*uOOo?yV2rEYG&Rz@KUKEiXn3Xb_u;I4$6h2Go?sEuM(6PSjgBvmn`4LL}No z9qeo+ywlB%9-`1??hE}xK2crMB{me#*XO?l(VISECf@KOUI=3@_^n)P!o4T{X#V~2 z(&xW#hK1?V#wu@K3xViy$U^AiIjR93TO}CKW#)0la{?Gh^OG{PXIqFFu?DIcpx5swZvXH9{j7F}Jd+}HEq@<}zY zgZGe9^`I$U!l=gAWxU<~=EP-%Nga@dd}Cg#K0PO+TOy_2+<_F8q^&4`dY~2kB|5wIQQct1yh`4<;%f zTH+O=NdfE4E*s*acYH0GZ-b9g6 {children} + diff --git a/components/ui/StatusMessage.tsx b/components/ui/StatusMessage.tsx index 841df0c..10b6166 100644 --- a/components/ui/StatusMessage.tsx +++ b/components/ui/StatusMessage.tsx @@ -6,11 +6,12 @@ import {CrossIcon} from '@/components/icons/CrossIcon' type StatusMessageProps = { children: ReactNode className?: string + errorConditions?: string[] } -export function StatusMessage({children, className}: StatusMessageProps) { - const errorConditions = ['error', 'problem'] - const isError = errorConditions.some(condition => children?.toString().toLowerCase().includes(condition)) +export function StatusMessage({children, className, errorConditions}: StatusMessageProps) { + const _errorConditions = ['error', 'problem', ...errorConditions ?? []] + const isError = _errorConditions.some(condition => children?.toString().toLowerCase().includes(condition)) return ( <> diff --git a/components/ui/Subscribe.tsx b/components/ui/Subscribe.tsx new file mode 100644 index 0000000..805fffd --- /dev/null +++ b/components/ui/Subscribe.tsx @@ -0,0 +1,50 @@ +'use client' + +import React, {useActionState} from 'react' +import {Button} from '@/components/ui/Button' +import {InboxIcon} from '@/components/icons/InboxIcon' +import {subscribe} from '@/app/actions/subscribe' +import {StatusMessage} from '@/components/ui/StatusMessage' + +type InitialState = { + message: string +} + +const initialState: InitialState = { + message: '' +} + +export function Subscribe() { + const [state, formAction, pending] = useActionState(subscribe, initialState) + + const handleKeyDown = (e: React.KeyboardEvent) => { + if ((e.ctrlKey || e.metaKey) && (e.key === 'Enter' || e.key === 'NumpadEnter')) { + e.preventDefault() + e.currentTarget.form?.requestSubmit() + } + } + + return ( +
+
+

+ + Stay in the loop +

+

+ Subscribe to my newsletter and get notified when I publish more content like this. +

+
+ + + {state?.message} +
+
+
+ ) +} \ No newline at end of file diff --git a/lib/listmonk.ts b/lib/listmonk.ts new file mode 100644 index 0000000..95b40e9 --- /dev/null +++ b/lib/listmonk.ts @@ -0,0 +1,28 @@ +const LISTMONK_URL = process.env.LISTMONK_URL ?? '' +const LISTMONK_LIST_ID = process.env.LISTMONK_LIST_ID ?? '' +const LISTMONK_USERNAME = process.env.LISTMONK_USERNAME ?? '' +const LISTMONK_TOKEN = process.env.LISTMONK_TOKEN ?? '' + +export async function addSubscriber(email: FormDataEntryValue | null) { + if (email !== null && LISTMONK_URL !== '') { + const headers = new Headers() + headers.append('Content-Type', 'application/json') + headers.append('Authorization', 'Basic ' + Buffer.from(LISTMONK_USERNAME + ':' + LISTMONK_TOKEN).toString('base64')) + + const response = await fetch(LISTMONK_URL, { + method: 'POST', + headers: headers, + body: JSON.stringify({ + email: email, + lists: [parseInt(LISTMONK_LIST_ID)], + preconfirm_subscriptions: true + }) + }) + + if (response.status === 409) { + throw new Error('Already subscribed') + } else if (response.status !== 200) { + throw new Error('Server error') + } + } +} \ No newline at end of file