mirror of
https://github.com/r-freeman/portfolio.git
synced 2025-04-24 08:54:36 +00:00
Add subscription feature
All checks were successful
Build And Publish / BuildAndPublish (push) Successful in 3m11s
All checks were successful
Build And Publish / BuildAndPublish (push) Successful in 3m11s
This commit is contained in:
parent
272a5cecab
commit
36532bc1f1
@ -13,4 +13,8 @@ AUTH_SECRET=
|
||||
AUTH_TRUST_HOST=
|
||||
AUTH_REDIRECT_PROXY_URL=
|
||||
NTFY_URL=
|
||||
NTFY_TOKEN=
|
||||
NTFY_TOKEN=
|
||||
LISTMONK_URL=
|
||||
LISTMONK_LIST_ID=
|
||||
LISTMONK_USERNAME=
|
||||
LISTMONK_TOKEN=
|
2
.gitignore
vendored
2
.gitignore
vendored
@ -3,6 +3,8 @@ prisma/**/*
|
||||
!prisma/schema.prisma
|
||||
.env
|
||||
|
||||
encrypt_secrets.sh
|
||||
|
||||
supabase/
|
||||
!/lib/supabase
|
||||
|
||||
|
32
app/actions/subscribe.ts
Normal file
32
app/actions/subscribe.ts
Normal file
@ -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'}
|
||||
}
|
@ -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({
|
||||
</header>
|
||||
<Prose className="mt-8" data-mdx-content>{children}</Prose>
|
||||
</article>
|
||||
<Subscribe/>
|
||||
<Comments slug={slug} comments={comments}/>
|
||||
<ArticleNav prev={prev} next={next}/>
|
||||
</div>
|
||||
|
@ -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 (
|
||||
<>
|
||||
|
50
components/ui/Subscribe.tsx
Normal file
50
components/ui/Subscribe.tsx
Normal file
@ -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<HTMLInputElement>) => {
|
||||
if ((e.ctrlKey || e.metaKey) && (e.key === 'Enter' || e.key === 'NumpadEnter')) {
|
||||
e.preventDefault()
|
||||
e.currentTarget.form?.requestSubmit()
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="mt-24 rounded-2xl p-px dark:bg-gradient-to-r dark:from-pink-500 dark:via-red-500 dark:to-yellow-500">
|
||||
<div className="rounded-2xl border border-zinc-100 p-6 dark:border-zinc-700/40 bg-white dark:bg-gray-950">
|
||||
<h2 className="flex items-center text-base font-semibold text-zinc-900 dark:text-zinc-100">
|
||||
<InboxIcon className="size-6"/>
|
||||
<span className="ml-3">Stay in the loop</span>
|
||||
</h2>
|
||||
<p className="mt-2 text-base text-zinc-600 dark:text-gray-400">
|
||||
Subscribe to my newsletter and get notified when I publish more content like this.
|
||||
</p>
|
||||
<form className="mt-6 flex flex-col sm:flex-row gap-x-2"
|
||||
action={formAction}>
|
||||
<input type="email" name="email" id="email" required onKeyDown={handleKeyDown}
|
||||
placeholder="Your email address"
|
||||
className="rounded-md px-3 py-1.5 text-base text-zinc-600 dark:text-zinc-400 bg-[#fafafa] dark:bg-[#121212] border-[1px] dark:border-zinc-700/40 -outline-offset-1 focus:outline focus:outline-2 focus:-outline-offset-2 focus:outline-indigo-600 focus:dark:outline-indigo-600"/>
|
||||
<Button variant="secondary" className="mt-2 sm:mt-0" disabled={pending}>
|
||||
Subscribe
|
||||
</Button>
|
||||
<StatusMessage className="mt-2 sm:mt-0 sm:ml-2" errorConditions={['already']}>{state?.message}</StatusMessage>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
28
lib/listmonk.ts
Normal file
28
lib/listmonk.ts
Normal file
@ -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')
|
||||
}
|
||||
}
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user