From be2a0ae651a3782992e3075ea54d765b1903e722 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 -> 950 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 | 25 ++++++++++++++ 8 files changed, 120 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..20b0bf6d75e0f8fdfc1aa3cfa5e323193c97b074 100644 GIT binary patch literal 950 zcmV;n14;ah4Fm@R0!_bJqqPysa{tol0rSIowM6P;hwnFZT-UkR5Fd2>|Hu~?=`Jm8 zFBNzltUqmbP!Rt|4yXQPHw`?5u(hk3$O}Cf633O}5mw-mqXoyC-^d9!^@~0Su_5O= z8>f`QeN{t((o!R#it(m!L!bDWK6z&|w?S_Ou?1FCBjS5$b-#K|$5WKTHxws!x4LIl z2C!~e+Mu$$0EhWX@rz)jwLvA!BkpxbIaN?iRy2BaWx*5*(%NJ9<;z-iOuSJ*pc3cF z4YvjFxyC*^)mIqbtL0a^TEAhS@W%Us!cG9?s_lo*>!CLrCqZNyAA`iG_yzDCPV8@nx}*=8l^i z(L@IARi<_ljsJMrUE~iVcV6tuQbc%HU7QtflzH-F8ly#A)Hb6-o#FMvkj(!cZM}uR zWBRN_zQ66yi~m_lWn_wZoUDjn=X0w{JSn`C@9PtoB9*Vo6PD$T*Z<+7f}CrN?ds#S z^rn?seJ!ic!kaJCT40qj7R#KOf!3_o>UD|wVF<*dhy-JrGn}6&*PM2uL~snVcQZs{ zrM5vhZflu)vi{%lLBS)O1KckM_%RfbTOe*Ztq>jS$!vOcPEWfEqA+&HW0Q;9$beMkmdHCC0|7Tm0(Fn8-> zmK>E*Hnjuk-si&LMosK~YQ*^7(n3rP(?tqjE?`*?eM zM>(A4Y`^yTQ1msu7RP$g<{B@$pjUbH?5WTZx+%5%=a1;CB0LQ&gbs>L_Jf2r85t{2 z*UB?nSj>a3%i-Rx9=h}rdi=4TlPODWUKs8efdG+ z?&%-L*7Z#hkpZxPTfR3EzT;t0lS~=o&b>3s$pu7 YU3JnRuQ)7~Ws816^2AgJq*}ONXo6qo0RR91 literal 882 zcmV-&1C9KQ4Fm@R0wLPk=wjDQ=>O8`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..b4907d0 --- /dev/null +++ b/lib/listmonk.ts @@ -0,0 +1,25 @@ +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') + if (response.status >= 500) throw new Error('Server error') + } +} \ No newline at end of file