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 b04ca65..b7dd78f 100644 Binary files a/.env.gpg and b/.env.gpg differ diff --git a/.gitignore b/.gitignore index c049d89..89636b4 100644 --- a/.gitignore +++ b/.gitignore @@ -3,6 +3,8 @@ prisma/**/* !prisma/schema.prisma .env +encrypt_secrets.sh + supabase/ !/lib/supabase diff --git a/app/actions/subscribe.ts b/app/actions/subscribe.ts new file mode 100644 index 0000000..101c4bb --- /dev/null +++ b/app/actions/subscribe.ts @@ -0,0 +1,32 @@ +'use server' + +import {z} from 'zod' +import {addSubscriber} from '@/lib/listmonk' + +export async function subscribe(prevState: { message: string }, formData: FormData) { + const schema = z.object({ + email: z.string().email() + }) + + const {email} = { + email: formData.get('email') + } + + const parse = schema.safeParse({email}) + if (!parse.success) { + return {message: 'Error, your email address is invalid.'} + } + + try { + await addSubscriber(email) + } catch (error) { + let errorMessage + if (error instanceof Error) errorMessage = error.message + else errorMessage = String(error) + + console.error(error) + return {message: errorMessage} + } + + return {message: 'Subscribed'} +} \ No newline at end of file diff --git a/components/layouts/ArticleLayout.tsx b/components/layouts/ArticleLayout.tsx index 5009755..d18c0b2 100644 --- a/components/layouts/ArticleLayout.tsx +++ b/components/layouts/ArticleLayout.tsx @@ -6,6 +6,7 @@ import {Views} from '@/components/ui/Views' import {ArrowDownIcon} from '@/components/icons/ArrowDownIcon' import ArticleNav from '@/components/ui/ArticleNav' import {Comments} from '@/components/ui/Comments' +import {Subscribe} from '@/components/ui/Subscribe' import {getAllArticles} from '@/lib/getAllArticles' import {getComments} from '@/lib/getComments' import {format} from 'date-fns' @@ -83,6 +84,7 @@ export async function ArticleLayout({ {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