Compare commits

...

10 Commits
0.0.33 ... main

Author SHA1 Message Date
72dcb42147 Update action workflow versions
Some checks failed
Build Production Image / Build Production Image (push) Failing after 3m2s
2025-03-17 21:31:48 -07:00
64c80c11ba Donation page
Some checks failed
Build Production Image / Build Production Image (push) Failing after 2m28s
2025-03-16 22:10:16 -07:00
3e8fada7c7 Remove unused projects page 2025-03-16 22:09:58 -07:00
45cc2e856c Remove unused useMutationObservable 2025-03-16 22:09:58 -07:00
01c3912ba9 Add new entries to the hall timeline 2025-03-16 22:09:58 -07:00
5ed921ec05 Small copy corrections 2025-03-16 22:09:58 -07:00
d1c7de593b NFC: Formatting 2025-03-16 22:09:58 -07:00
4acf298044 Remove year from membership form pdf 2025-03-16 16:34:04 -07:00
a7ec7bf5eb Update Dockerfile env format 2024-12-19 21:52:54 -08:00
78d996a989 Move listmonk form into separate module and add captcha
Some checks failed
Build Production Image / Build Production Image (push) Failing after 2m21s
2024-12-19 18:53:40 -08:00
21 changed files with 832 additions and 556 deletions

View File

@ -15,17 +15,17 @@ jobs:
uses: actions/checkout@v3 uses: actions/checkout@v3
- name: Set up Docker Buildx - name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2 uses: docker/setup-buildx-action@v3
- name: Login to Gitea Docker registry - name: Login to Gitea Docker registry
uses: docker/login-action@v2 uses: docker/login-action@v3
with: with:
registry: git.grosinger.net registry: git.grosinger.net
username: tgrosinger username: tgrosinger
password: ${{ secrets.CONTAINER_REGISTRY_ACCESS_TOKEN }} password: ${{ secrets.CONTAINER_REGISTRY_ACCESS_TOKEN }}
- name: Build and push - name: Build and push
uses: docker/build-push-action@v4 uses: docker/build-push-action@v6
with: with:
context: . context: .
push: true push: true

View File

@ -1,8 +1,8 @@
FROM docker.io/library/node:18-alpine AS build-env FROM docker.io/library/node:18-alpine AS build-env
ENV NODE_ENV production ENV NODE_ENV=production
ENV PORT 3000 ENV PORT=3000
ENV NEXT_TELEMETRY_DISABLED 1 ENV NEXT_TELEMETRY_DISABLED=1
WORKDIR /app WORKDIR /app

Binary file not shown.

View File

@ -12,7 +12,10 @@ export async function POST(request: NextRequest): Promise<Response> {
? `${billing.address.line1}\n${cityStateZip}` ? `${billing.address.line1}\n${cityStateZip}`
: `${billing.address.line1}\n${billing.address.line2}\n${cityStateZip}`; : `${billing.address.line1}\n${billing.address.line2}\n${cityStateZip}`;
const isDonation = data.metadata.type === 'donation';
emailNotification({ emailNotification({
subject: isDonation ? 'New WSCC Donation' : 'New WSCC Membership',
bodyPrefix: isDonation ? 'New donation:' : 'New WSCC member:',
name: billing.name, name: billing.name,
type: data.metadata.type, type: data.metadata.type,
amount: data.amount, amount: data.amount,

View File

@ -0,0 +1,37 @@
"use client"
export default function MailingListSignupForm(): React.JSX.Element {
return (<form method="post" action="https://lists.orcas.community/subscription/form" className="listmonk-form">
<div className="flex flex-col gap-y-2">
<h3 className="text-lg font-semibold text-gray-900">Join our mailing list</h3>
<input type="hidden" name="nonce" />
<div>
<label htmlFor="email" className="sr-only">Email</label>
<input
type="email"
name="email"
id="email"
className="block w-full rounded-md border-0 py-1.5 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-indigo-600 sm:text-sm sm:leading-6"
placeholder="you@example.com" />
</div>
<div>
<label htmlFor="name" className="sr-only">Name</label>
<input
type="text"
name="name"
id="name"
className="block w-full rounded-md border-0 py-1.5 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-indigo-600 sm:text-sm sm:leading-6"
placeholder="Jane Doe" />
</div>
<input className="hidden" type="checkbox" name="l" checked value="ea5f1e67-2ff0-4762-8893-0645e93a8306" />
<div className="h-captcha flex justify-center" data-sitekey="77aeddb4-cfda-4a3e-b262-0288a4e4664a"></div>
<script src="https://js.hcaptcha.com/1/api.js" async defer></script>
<input
className="w-full rounded-md flex justify-center border border-transparent bg-indigo-600 px-4 py-2 text-sm font-medium text-white shadow-sm hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2"
id="submit"
type="submit"
value="Subscribe"
/>
</div>
</form>);
}

View File

@ -1,13 +1,12 @@
import { type Metadata } from 'next';
import Image from 'next/image';
import Link from 'next/link';
import dynamic from 'next/dynamic';
import clsx from 'clsx';
import { type Metadata } from 'next' import { Container } from '@/components/Container';
import Image from 'next/image' import { EnvelopeIcon, UserGroupIcon } from '@heroicons/react/24/solid';
import Link from 'next/link' import interiorEmptyImage from '@/images/photos/interior-empty.jpg';
import dynamic from "next/dynamic";
import clsx from 'clsx'
import { Container } from '@/components/Container'
import { UserPlusIcon, AtSymbolIcon, EnvelopeIcon, UserGroupIcon } from '@heroicons/react/24/solid'
import interiorEmptyImage from '@/images/photos/interior-empty.jpg'
function SocialLink({ function SocialLink({
className, className,
@ -15,10 +14,10 @@ function SocialLink({
children, children,
icon: Icon, icon: Icon,
}: { }: {
className?: string className?: string;
href: string href: string;
icon: React.ComponentType<{ className?: string }> icon: React.ComponentType<{ className?: string }>;
children: React.ReactNode children: React.ReactNode;
}) { }) {
return ( return (
<li className={clsx(className, 'flex')}> <li className={clsx(className, 'flex')}>
@ -30,26 +29,30 @@ function SocialLink({
<span className="ml-4">{children}</span> <span className="ml-4">{children}</span>
</Link> </Link>
</li> </li>
) );
} }
export const metadata: Metadata = { export const metadata: Metadata = {
title: 'Community Club', title: 'Community Club',
description: description: 'The West Sound Community Club on Orcas Island.',
'The West Sound Community Club on Orcas Island.', };
}
// TODO: Replace interiorEmptyImage with a photo from a potluck // TODO: Replace interiorEmptyImage with a photo from a potluck
export default function Club() { export default function Club() {
// Dynamic import since ClubPayment uses `document` // Dynamic import since ClubPayment uses `document`
const ClubPayment = dynamic( const ClubPayment = dynamic(
() => { () => {
return import("./payment"); return import('./payment');
}, },
{ ssr: false } { ssr: false },
);
const MailingListSignupForm = dynamic(
() => {
return import('./mailinglist');
},
{ ssr: false },
); );
return ( return (
@ -71,60 +74,30 @@ export default function Club() {
</h1> </h1>
<div className="mt-6 space-y-7 text-base text-zinc-600"> <div className="mt-6 space-y-7 text-base text-zinc-600">
<p> <p>
The West Sound Community Club is a group of neighbors and friends The West Sound Community Club is a group of neighbors and friends.
living in the West Sound area. Together we steward the West Sound Together we steward the West Sound Hall, and gather monthly for
Hall, and gather monthly for potlucks and other community events. potlucks and other community events.
</p> </p>
<p> <p>
Members of the West Sound community, past and present, have Members of the West Sound community, past and present, have
invested time, money, and talent in creating a true old-fashioned invested time, money, and talent in creating a true old-fashioned
community center. For over one hundred years, the hall has community center. For over 125, the hall has provided continuity
provided continuity for the residents and guests of the West Sound for the residents and guests of the West Sound community. The
community. The current residents continue the commitment to current residents continue the commitment to maintain this unique
maintain this unique gathering place out of care and respect for gathering place out of care and respect for past and future
past and future generations. generations.
</p> </p>
<p> <p>
The West Sound Community Club is a nonprofit, tax-exempt corporation The West Sound Community Club is a nonprofit, tax-exempt
under Section 501(c)3 of the Internal Revenue Code. corporation under Section 501(c)3 of the Internal Revenue Code.
</p> </p>
</div> </div>
</div> </div>
<div className="lg:pl-20"> <div className="lg:pl-20">
<ul role="list"> <ul role="list">
<form method="post" action="https://lists.orcas.community/subscription/form" className="listmonk-form"> <MailingListSignupForm />
<div className="flex flex-col gap-y-2">
<h3 className="text-lg font-semibold text-gray-900">Join our mailing list</h3>
<input type="hidden" name="nonce" />
<div>
<label htmlFor="email" className="sr-only">Email</label>
<input
type="email"
name="email"
id="email"
className="block w-full rounded-md border-0 py-1.5 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-indigo-600 sm:text-sm sm:leading-6"
placeholder="you@example.com" />
</div>
<div>
<label htmlFor="name" className="sr-only">Name</label>
<input
type="text"
name="name"
id="name"
className="block w-full rounded-md border-0 py-1.5 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-indigo-600 sm:text-sm sm:leading-6"
placeholder="Jane Doe" />
</div>
<input className="hidden" type="checkbox" name="l" checked value="ea5f1e67-2ff0-4762-8893-0645e93a8306" />
<input
className="w-full rounded-md flex justify-center border border-transparent bg-indigo-600 px-4 py-2 text-sm font-medium text-white shadow-sm hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2"
id="submit"
type="submit"
value="Subscribe"
/>
</div>
</form>
<SocialLink <SocialLink
href="mailto:contact@westsoundhall.org" href="mailto:contact@westsoundhall.org"
icon={EnvelopeIcon} icon={EnvelopeIcon}
@ -132,18 +105,22 @@ export default function Club() {
> >
contact@westsoundhall.org contact@westsoundhall.org
</SocialLink> </SocialLink>
<SocialLink href="/board-of-directors" icon={UserGroupIcon} className="mt-4"> <SocialLink
href="/board-of-directors"
icon={UserGroupIcon}
className="mt-4"
>
Board of Directors Board of Directors
</SocialLink> </SocialLink>
</ul> </ul>
</div> </div>
<div> <div>
<h2 className="text-2xl mb-4 font-semibold leading-6 text-gray-900"> <h2 className="mb-4 text-2xl font-semibold leading-6 text-gray-900">
Join or Renew your Membership Join or Renew your Membership
</h2> </h2>
<ClubPayment /> <ClubPayment />
</div> </div>
</div> </div>
</Container> </Container>
) );
} }

View File

@ -1,14 +1,19 @@
"use client" 'use client';
import React, { useEffect, useState, FormEvent, useCallback } from 'react'; import React, { useEffect, useState, FormEvent, useCallback } from 'react';
import { AddressElement, Elements } from '@stripe/react-stripe-js'; import { AddressElement, Elements } from '@stripe/react-stripe-js';
import { Appearance, StripeAddressElementOptions, StripeElementsOptions, loadStripe } from '@stripe/stripe-js'; import {
Appearance,
StripeAddressElementOptions,
StripeElementsOptions,
loadStripe,
} from '@stripe/stripe-js';
import { import {
PaymentElement, PaymentElement,
useStripe, useStripe,
useElements, useElements,
} from '@stripe/react-stripe-js'; } from '@stripe/react-stripe-js';
import { RadioGroup } from '@headlessui/react' import { RadioGroup } from '@headlessui/react';
const stripe = loadStripe(process.env.NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY || ''); const stripe = loadStripe(process.env.NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY || '');
@ -20,15 +25,15 @@ const addressOptions: StripeAddressElementOptions = {
}, },
validation: { validation: {
phone: { phone: {
required: 'always' required: 'always',
} },
}, },
defaultValues: { defaultValues: {
address: { address: {
state: 'WA', state: 'WA',
country: 'US', country: 'US',
}, },
} },
}; };
const membershipLevels = [ const membershipLevels = [
@ -43,20 +48,40 @@ function classNames(...classes: string[]) {
} }
function Spinner(): React.JSX.Element { function Spinner(): React.JSX.Element {
return (<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"> return (
<path fill="currentColor" d="M10.72,19.9a8,8,0,0,1-6.5-9.79A7.77,7.77,0,0,1,10.4,4.16a8,8,0,0,1,9.49,6.52A1.54,1.54,0,0,0,21.38,12h.13a1.37,1.37,0,0,0,1.38-1.54,11,11,0,1,0-12.7,12.39A1.54,1.54,0,0,0,12,21.34h0A1.47,1.47,0,0,0,10.72,19.9Z"> <svg
<animateTransform attributeName="transform" dur="1s" repeatCount="indefinite" type="rotate" values="0 12 12;360 12 12" /> xmlns="http://www.w3.org/2000/svg"
</path> width="24"
</svg>); height="24"
viewBox="0 0 24 24"
>
<path
fill="currentColor"
d="M10.72,19.9a8,8,0,0,1-6.5-9.79A7.77,7.77,0,0,1,10.4,4.16a8,8,0,0,1,9.49,6.52A1.54,1.54,0,0,0,21.38,12h.13a1.37,1.37,0,0,0,1.38-1.54,11,11,0,1,0-12.7,12.39A1.54,1.54,0,0,0,12,21.34h0A1.47,1.47,0,0,0,10.72,19.9Z"
>
<animateTransform
attributeName="transform"
dur="1s"
repeatCount="indefinite"
type="rotate"
values="0 12 12;360 12 12"
/>
</path>
</svg>
);
} }
function CheckoutForm({ function CheckoutForm({
paymentIntentID paymentIntentID,
}: { }: {
paymentIntentID: string paymentIntentID: string;
}): React.JSX.Element { }): React.JSX.Element {
const [selectedMembershipLevel, setSelectedMembershipLevel] = useState(membershipLevels[0]); const [selectedMembershipLevel, setSelectedMembershipLevel] = useState(
const [selectedAdditionalDonation, setSelectedAdditionalDonation] = useState<number | null>(null); membershipLevels[0],
);
const [selectedAdditionalDonation, setSelectedAdditionalDonation] = useState<
number | null
>(null);
const [customAmount, setCustomAmount] = useState(''); const [customAmount, setCustomAmount] = useState('');
const [email, setEmail] = useState(''); const [email, setEmail] = useState('');
const [totalAmount, setTotalAmount] = useState(300); const [totalAmount, setTotalAmount] = useState(300);
@ -72,7 +97,7 @@ function CheckoutForm({
//Grab the client secret from url params //Grab the client secret from url params
const clientSecret = new URLSearchParams(window.location.search).get( const clientSecret = new URLSearchParams(window.location.search).get(
'payment_intent_client_secret' 'payment_intent_client_secret',
); );
if (!clientSecret) { if (!clientSecret) {
@ -98,19 +123,19 @@ function CheckoutForm({
}, [stripe]); }, [stripe]);
useEffect(() => { useEffect(() => {
let subtotal = selectedMembershipLevel.price let subtotal = selectedMembershipLevel.price;
if (selectedAdditionalDonation && selectedAdditionalDonation !== -1) { if (selectedAdditionalDonation && selectedAdditionalDonation !== -1) {
subtotal += selectedAdditionalDonation subtotal += selectedAdditionalDonation;
} else if (customAmount !== '') { } else if (customAmount !== '') {
try { try {
subtotal += parseFloat(customAmount) subtotal += parseFloat(customAmount);
} catch { } catch {
console.error('') console.error('');
} }
} }
subtotal = Math.ceil(subtotal * 1.03) subtotal = Math.ceil(subtotal * 1.03);
setTotalAmount(subtotal); setTotalAmount(subtotal);
@ -121,11 +146,17 @@ function CheckoutForm({
amount: subtotal * 100, amount: subtotal * 100,
payment_intent_id: paymentIntentID, payment_intent_id: paymentIntentID,
metadata: { metadata: {
'type': selectedMembershipLevel.title, type: selectedMembershipLevel.title,
}, },
}), }),
}); });
}, [paymentIntentID, selectedMembershipLevel, selectedAdditionalDonation, customAmount, email]) }, [
paymentIntentID,
selectedMembershipLevel,
selectedAdditionalDonation,
customAmount,
email,
]);
const handleSubmit = async (e: FormEvent) => { const handleSubmit = async (e: FormEvent) => {
e.preventDefault(); e.preventDefault();
@ -162,10 +193,17 @@ function CheckoutForm({
return ( return (
<> <>
<form id="payment-form" onSubmit={handleSubmit} className="m-auto space-y-4"> <form
id="payment-form"
onSubmit={handleSubmit}
className="m-auto space-y-4"
>
{/* Membership Type */} {/* Membership Type */}
<RadioGroup value={selectedMembershipLevel} onChange={setSelectedMembershipLevel} className="space-y-3"> <RadioGroup
value={selectedMembershipLevel}
onChange={setSelectedMembershipLevel}
className="space-y-3"
>
<RadioGroup.Label className="text-base font-semibold leading-6 text-gray-900"> <RadioGroup.Label className="text-base font-semibold leading-6 text-gray-900">
Select a membership type Select a membership type
</RadioGroup.Label> </RadioGroup.Label>
@ -177,8 +215,10 @@ function CheckoutForm({
value={membership} value={membership}
className={({ active }) => className={({ active }) =>
classNames( classNames(
active ? 'border-indigo-600 ring-2 ring-indigo-600' : 'border-gray-200', active
'relative flex cursor-pointer rounded-lg border bg-white hover:bg-gray-50 p-4 shadow-sm focus:outline-none' ? 'border-indigo-600 ring-2 ring-indigo-600'
: 'border-gray-200',
'relative flex cursor-pointer rounded-lg border bg-white p-4 shadow-sm hover:bg-gray-50 focus:outline-none',
) )
} }
> >
@ -186,15 +226,26 @@ function CheckoutForm({
<> <>
<span className="flex flex-1 items-center justify-between "> <span className="flex flex-1 items-center justify-between ">
<span className="flex flex-col"> <span className="flex flex-col">
<RadioGroup.Label as="span" className="block text-sm font-medium text-gray-900"> <RadioGroup.Label
as="span"
className="block text-sm font-medium text-gray-900"
>
{membership.title} {membership.title}
</RadioGroup.Label> </RadioGroup.Label>
<RadioGroup.Description as="span" className="mt-1 flex items-center text-sm text-gray-500"> <RadioGroup.Description
as="span"
className="mt-1 flex items-center text-sm text-gray-500"
>
{membership.description} {membership.description}
</RadioGroup.Description> </RadioGroup.Description>
</span> </span>
<RadioGroup.Description as="span" className="ml-8 text-sm font-medium"> <RadioGroup.Description
<span className="text-gray-900">${membership.price}</span> as="span"
className="ml-8 text-sm font-medium"
>
<span className="text-gray-900">
${membership.price}
</span>
<span className="text-gray-500">/yr</span> <span className="text-gray-500">/yr</span>
</RadioGroup.Description> </RadioGroup.Description>
</span> </span>
@ -202,7 +253,7 @@ function CheckoutForm({
className={classNames( className={classNames(
active ? 'border' : 'border-2', active ? 'border' : 'border-2',
checked ? 'border-indigo-600' : 'border-transparent', checked ? 'border-indigo-600' : 'border-transparent',
'pointer-events-none absolute -inset-px rounded-lg' 'pointer-events-none absolute -inset-px rounded-lg',
)} )}
aria-hidden="true" aria-hidden="true"
/> />
@ -214,7 +265,11 @@ function CheckoutForm({
</RadioGroup> </RadioGroup>
{/* Additional donation */} {/* Additional donation */}
<RadioGroup value={selectedAdditionalDonation} onChange={setSelectedAdditionalDonation} className="space-y-3"> <RadioGroup
value={selectedAdditionalDonation}
onChange={setSelectedAdditionalDonation}
className="space-y-3"
>
<RadioGroup.Label className="text-base font-semibold leading-6 text-gray-900"> <RadioGroup.Label className="text-base font-semibold leading-6 text-gray-900">
Additional donation Additional donation
</RadioGroup.Label> </RadioGroup.Label>
@ -229,45 +284,48 @@ function CheckoutForm({
option === -1 ? 'col-span-2' : '', option === -1 ? 'col-span-2' : '',
checked checked
? 'ring-2 ring-indigo-600' ? 'ring-2 ring-indigo-600'
: 'ring-1 ring-inset ring-gray-200 text-gray-900 hover:bg-gray-50', : 'text-gray-900 ring-1 ring-inset ring-gray-200 hover:bg-gray-50',
'flex items-center justify-center rounded-md py-3 px-3 text-sm font-semibold sm:flex-1 bg-white' 'flex items-center justify-center rounded-md bg-white px-3 py-3 text-sm font-semibold sm:flex-1',
) )
} }
> >
{option === -1 ? (
{option === -1 <RadioGroup.Label
? ( as="span"
<RadioGroup.Label as="span" className="flex flex-col items-center"> className="flex flex-col items-center"
<div className="relative rounded-md shadow-sm"> >
<div className="pointer-events-none absolute inset-y-0 left-0 flex items-center pl-3"> <div className="relative rounded-md shadow-sm">
<span className="text-gray-500 sm:text-sm">$</span> <div className="pointer-events-none absolute inset-y-0 left-0 flex items-center pl-3">
</div> <span className="text-gray-500 sm:text-sm">$</span>
<input
type="number"
name="custom-amount"
id="custom-amount"
value={customAmount}
min="0"
step="1"
onChange={(e) => setCustomAmount(e.target.value)}
className="block w-full rounded-md border-0 py-1.5 pl-7 pr-12 text-gray-900 ring-1 ring-inset ring-gray-200 placeholder:text-gray-400 focus:ring-1 focus:ring-inset focus:ring-gray-500 sm:text-sm sm:leading-6"
placeholder="Custom"
aria-describedby="price-currency"
/>
<div className="pointer-events-none absolute inset-y-0 right-0 flex items-center pr-3">
<span className="text-gray-500 sm:text-sm" id="price-currency">
/yr
</span>
</div>
</div> </div>
</RadioGroup.Label> <input
) type="number"
: ( name="custom-amount"
<RadioGroup.Label as="span"> id="custom-amount"
<span>${option}</span> value={customAmount}
<span className="text-gray-500">/yr</span> min="0"
</RadioGroup.Label> step="1"
)} onChange={(e) => setCustomAmount(e.target.value)}
className="block w-full rounded-md border-0 py-1.5 pl-7 pr-12 text-gray-900 ring-1 ring-inset ring-gray-200 placeholder:text-gray-400 focus:ring-1 focus:ring-inset focus:ring-gray-500 sm:text-sm sm:leading-6"
placeholder="Custom"
aria-describedby="price-currency"
/>
<div className="pointer-events-none absolute inset-y-0 right-0 flex items-center pr-3">
<span
className="text-gray-500 sm:text-sm"
id="price-currency"
>
/yr
</span>
</div>
</div>
</RadioGroup.Label>
) : (
<RadioGroup.Label as="span">
<span>${option}</span>
<span className="text-gray-500">/yr</span>
</RadioGroup.Label>
)}
</RadioGroup.Option> </RadioGroup.Option>
))} ))}
</div> </div>
@ -278,8 +336,11 @@ function CheckoutForm({
About you About you
</h2> </h2>
<div className="rounded-md mb-3 px-3 pb-1.5 pt-2.5 shadow-sm ring-1 ring-inset ring-gray-200 focus-within:ring-2 focus-within:ring-indigo-600"> <div className="mb-3 rounded-md px-3 pb-1.5 pt-2.5 shadow-sm ring-1 ring-inset ring-gray-200 focus-within:ring-2 focus-within:ring-indigo-600">
<label htmlFor="email" className="block text-xs font-medium text-gray-900"> <label
htmlFor="email"
className="block text-xs font-medium text-gray-900"
>
Email Email
</label> </label>
<input <input
@ -301,9 +362,15 @@ function CheckoutForm({
</h2> </h2>
<div className="mt-1 text-sm text-gray-500"> <div className="mt-1 text-sm text-gray-500">
Credit card fees included. If you would like to avoid these fees or Credit card fees included. If you would like to avoid these fees or
to pay by cash or check, please instead <a className="underline to pay by cash or check, please instead{' '}
mx-1" href="/WSCC-Membership-Form.pdf">fill out a paper form</a> and <a
mail to the address on the form. className="mx-1
underline"
href="/WSCC-Membership-Form.pdf"
>
fill out a paper form
</a>{' '}
and mail to the address on the form.
</div> </div>
<PaymentElement id="payment-element" /> <PaymentElement id="payment-element" />
</div> </div>
@ -311,19 +378,19 @@ function CheckoutForm({
{/* TODO: Automatically renew toggle? */} {/* TODO: Automatically renew toggle? */}
<button <button
className="mt-6 w-full rounded-md flex justify-center border border-transparent bg-indigo-600 px-4 py-2 text-sm font-medium text-white shadow-sm hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2" className="mt-6 flex w-full justify-center rounded-md border border-transparent bg-indigo-600 px-4 py-2 text-sm font-medium text-white shadow-sm hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2"
disabled={isLoading || !stripe || !elements} disabled={isLoading || !stripe || !elements}
id="submit" id="submit"
> >
{isLoading ? ( {isLoading ? <Spinner /> : 'Pay $' + totalAmount}
<Spinner />
) : (
'Pay $' + totalAmount
)}
</button> </button>
{/* Show any error or success messages */} {/* Show any error or success messages */}
{message && <div className="text-red-500" id="payment-message">{message}</div>} {message && (
<div className="text-red-500" id="payment-message">
{message}
</div>
)}
</form> </form>
</> </>
); );
@ -333,26 +400,6 @@ const DEFAULT_OPTIONS = {
config: { attributes: true, childList: true, subtree: true }, config: { attributes: true, childList: true, subtree: true },
}; };
function useMutationObservable(targetEl: Node, cb: MutationCallback, options = DEFAULT_OPTIONS) {
const [observer, setObserver] = useState<MutationObserver | null>(null);
useEffect(() => {
const obs = new MutationObserver(cb);
setObserver(obs);
}, [cb, options, setObserver]);
useEffect(() => {
if (!observer) return;
const { config } = options;
observer.observe(targetEl, config);
return () => {
if (observer) {
observer.disconnect();
}
};
}, [observer, targetEl, options]);
}
export default function ClubPayment() { export default function ClubPayment() {
const [clientSecret, setClientSecret] = useState(''); const [clientSecret, setClientSecret] = useState('');
const [paymentIntent, setPaymentIntent] = useState(''); const [paymentIntent, setPaymentIntent] = useState('');
@ -390,16 +437,15 @@ export default function ClubPayment() {
const options: StripeElementsOptions = { const options: StripeElementsOptions = {
clientSecret, clientSecret,
appearance, appearance,
} };
return <> return (
{clientSecret && ( <>
<Elements options={options} {clientSecret && (
stripe={stripe}> <Elements options={options} stripe={stripe}>
<CheckoutForm paymentIntentID={paymentIntent} /> <CheckoutForm paymentIntentID={paymentIntent} />
</Elements> </Elements>
)} )}
</> </>
);
} }

46
src/app/donate/page.tsx Normal file
View File

@ -0,0 +1,46 @@
import { type Metadata } from 'next';
import Image from 'next/image';
import dynamic from 'next/dynamic';
import { Container } from '@/components/Container';
import logoImage from '@/images/logo.png';
export const metadata: Metadata = {
title: 'Donate',
description: 'Donate to the West Sound Community Club.',
};
export default function Donate() {
// Dynamic import since DontationPayment uses `document`
const DontationPayment = dynamic(
() => {
return import('./payment');
},
{ ssr: false },
);
return (
<Container className="mx-auto mt-16 max-w-3xl sm:mt-32">
<div className="px-2.5">
<Image
src={logoImage}
alt=""
sizes="(min-width: 1024px) 32rem, 20rem"
/>
</div>
<div className="my-6 space-y-7 text-base text-zinc-600">
<p>
Your donation to the West Sound Community Club helps keep the hall
alive for the next generation.
</p>
<p>
The West Sound Community Club is a nonprofit, tax-exempt corporation
under Section 501(c)3 of the Internal Revenue Code. Your donations are
tax-deductible.
</p>
</div>
<DontationPayment />
</Container>
);
}

341
src/app/donate/payment.tsx Normal file
View File

@ -0,0 +1,341 @@
'use client';
import React, { useEffect, useState, FormEvent } from 'react';
import { AddressElement, Elements } from '@stripe/react-stripe-js';
import {
Appearance,
StripeAddressElementOptions,
StripeElementsOptions,
loadStripe,
} from '@stripe/stripe-js';
import {
PaymentElement,
useStripe,
useElements,
} from '@stripe/react-stripe-js';
import { RadioGroup } from '@headlessui/react';
const stripe = loadStripe(process.env.NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY || '');
const addressOptions: StripeAddressElementOptions = {
mode: 'billing',
allowedCountries: ['US'],
fields: {
phone: 'always',
},
validation: {
phone: {
required: 'always',
},
},
defaultValues: {
address: {
state: 'WA',
country: 'US',
},
},
};
const donationLevels = [10, 50, 100, -1];
function classNames(...classes: string[]) {
return classes.filter(Boolean).join(' ');
}
function Spinner(): React.JSX.Element {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
>
<path
fill="currentColor"
d="M10.72,19.9a8,8,0,0,1-6.5-9.79A7.77,7.77,0,0,1,10.4,4.16a8,8,0,0,1,9.49,6.52A1.54,1.54,0,0,0,21.38,12h.13a1.37,1.37,0,0,0,1.38-1.54,11,11,0,1,0-12.7,12.39A1.54,1.54,0,0,0,12,21.34h0A1.47,1.47,0,0,0,10.72,19.9Z"
>
<animateTransform
attributeName="transform"
dur="1s"
repeatCount="indefinite"
type="rotate"
values="0 12 12;360 12 12"
/>
</path>
</svg>
);
}
function CheckoutForm({
paymentIntentID,
}: {
paymentIntentID: string;
}): React.JSX.Element {
const [selectedDonation, setSelectedDonation] = useState<number>(10);
const [customAmount, setCustomAmount] = useState('');
const [email, setEmail] = useState('');
const [totalAmount, setTotalAmount] = useState(300);
const [message, setMessage] = useState<string>('');
const [isLoading, setIsLoading] = useState(false);
const stripe = useStripe();
const elements = useElements();
useEffect(() => {
if (!stripe) {
return;
}
//Grab the client secret from url params
const clientSecret = new URLSearchParams(window.location.search).get(
'payment_intent_client_secret',
);
if (!clientSecret) {
return;
}
stripe.retrievePaymentIntent(clientSecret).then(({ paymentIntent }) => {
switch (paymentIntent?.status) {
case 'succeeded':
setMessage('Payment succeeded!');
break;
case 'processing':
setMessage('Your payment is processing.');
break;
case 'requires_payment_method':
setMessage('Your payment was not successful, please try again.');
break;
default:
setMessage('Something went wrong.');
break;
}
});
}, [stripe]);
useEffect(() => {
let subtotal = 0;
if (selectedDonation && selectedDonation !== -1) {
subtotal += selectedDonation;
} else if (customAmount !== '') {
try {
subtotal += parseFloat(customAmount);
} catch {
console.error('');
}
}
setTotalAmount(subtotal);
fetch('api/stripe_intent', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
amount: subtotal * 100,
payment_intent_id: paymentIntentID,
}),
});
}, [paymentIntentID, selectedDonation, customAmount, email]);
const handleSubmit = async (e: FormEvent) => {
e.preventDefault();
if (!stripe || !elements) {
// Stripe.js has not yet loaded.
return;
}
setIsLoading(true);
const { error } = await stripe.confirmPayment({
elements,
confirmParams: {
return_url: window.location.origin + '/donate/thank-you',
receipt_email: email,
payment_method_data: {
billing_details: {
// Other details are filled automatically by address form.
email,
},
},
},
});
if (error.type === 'card_error' || error.type === 'validation_error') {
setMessage(error.message || '');
} else {
setMessage('An unexpected error occured.');
}
setIsLoading(false);
};
return (
<>
<form
id="payment-form"
onSubmit={handleSubmit}
className="m-auto space-y-4"
>
{/* Amount */}
<RadioGroup
value={selectedDonation}
onChange={setSelectedDonation}
className="space-y-3"
>
<RadioGroup.Label className="text-base font-semibold leading-6 text-gray-900">
Donation amount
</RadioGroup.Label>
<div className="grid grid-cols-3 gap-3 sm:grid-cols-5">
{donationLevels.map((option) => (
<RadioGroup.Option
key={option}
value={option}
className={({ active, checked }) =>
classNames(
'cursor-pointer focus:outline-none',
option === -1 ? 'col-span-2' : '',
checked
? 'ring-2 ring-indigo-600'
: 'text-gray-900 ring-1 ring-inset ring-gray-200 hover:bg-gray-50',
'flex items-center justify-center rounded-md bg-white px-3 py-3 text-sm font-semibold sm:flex-1',
)
}
>
{option === -1 ? (
<RadioGroup.Label
as="span"
className="flex flex-col items-center"
>
<div className="relative rounded-md shadow-sm">
<div className="pointer-events-none absolute inset-y-0 left-0 flex items-center pl-3">
<span className="text-gray-500 sm:text-sm">$</span>
</div>
<input
type="number"
name="custom-amount"
id="custom-amount"
value={customAmount}
min="0"
step="1"
onChange={(e) => setCustomAmount(e.target.value)}
className="block w-full rounded-md border-0 py-1.5 pl-7 pr-2 text-gray-900 ring-1 ring-inset ring-gray-200 placeholder:text-gray-400 focus:ring-1 focus:ring-inset focus:ring-gray-500 sm:text-sm sm:leading-6"
placeholder="Custom"
aria-describedby="price-currency"
/>
</div>
</RadioGroup.Label>
) : (
<RadioGroup.Label as="span">
<span>${option}</span>
</RadioGroup.Label>
)}
</RadioGroup.Option>
))}
</div>
</RadioGroup>
<div className="space-y-3">
<h2 className="text-base font-semibold leading-6 text-gray-900">
About you
</h2>
<div className="mb-3 rounded-md px-3 pb-1.5 pt-2.5 shadow-sm ring-1 ring-inset ring-gray-200 focus-within:ring-2 focus-within:ring-indigo-600">
<label
htmlFor="email"
className="block text-xs font-medium text-gray-900"
>
Email
</label>
<input
type="email"
name="email"
id="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
className="block w-full border-0 p-0 text-gray-900 placeholder:text-gray-400 focus:ring-0 sm:text-sm sm:leading-6"
placeholder="you@example.com"
/>
</div>
<AddressElement options={addressOptions} />
</div>
<div className="space-y-3">
<h2 className="text-base font-semibold leading-6 text-gray-900">
Payment
</h2>
<PaymentElement id="payment-element" />
</div>
<button
className="mt-6 flex w-full justify-center rounded-md border border-transparent bg-indigo-600 px-4 py-2 text-sm font-medium text-white shadow-sm hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2"
disabled={isLoading || !stripe || !elements}
id="submit"
>
{isLoading ? <Spinner /> : 'Pay $' + totalAmount}
</button>
{/* Show any error or success messages */}
{message && (
<div className="text-red-500" id="payment-message">
{message}
</div>
)}
</form>
</>
);
}
const DEFAULT_OPTIONS = {
config: { attributes: true, childList: true, subtree: true },
};
export default function DontationPayment() {
const [clientSecret, setClientSecret] = useState('');
const [paymentIntent, setPaymentIntent] = useState('');
const htmlEl = document.getElementsByTagName('html')[0];
useEffect(() => {
// Create PaymentIntent as soon as the page loads using our local API
fetch('api/stripe_intent', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
amount: 2000,
payment_intent_id: '',
}),
})
.then((res) => res.json())
.then((data) => {
setClientSecret(data.client_secret);
setPaymentIntent(data.id);
});
}, []);
const styles = getComputedStyle(htmlEl);
const appearance: Appearance = {
theme: 'stripe',
variables: {
colorBackground: styles.getPropertyValue('--stripe-background'),
colorText: styles.getPropertyValue('--stripe-foreground'),
},
labels: 'floating',
};
const options: StripeElementsOptions = {
clientSecret,
appearance,
};
return (
<>
{clientSecret && (
<Elements options={options} stripe={stripe}>
<CheckoutForm paymentIntentID={paymentIntent} />
</Elements>
)}
</>
);
}

View File

@ -0,0 +1,31 @@
import { type Metadata } from 'next';
import { SimpleLayout } from '@/components/SimpleLayout';
export const metadata: Metadata = {
title: 'Thank You',
description: 'Thanks for donating.',
};
export default async function ThankYou({
searchParams,
}: {
searchParams: { [key: string]: string | string[] | undefined };
}) {
const { redirect_status } = searchParams;
if (redirect_status !== 'succeeded') {
// TODO: Display error
}
return (
<SimpleLayout
title="Thanks for donating."
intro="Thank you for donating to the West Sound Community Club."
>
<p className="text-base text-zinc-600">
Your financial support helps us perserve this historic building and to
host events for the community.
</p>
</SimpleLayout>
);
}

View File

@ -1,12 +1,12 @@
import { type Metadata } from 'next' import { type Metadata } from 'next';
import Image from 'next/image' import Image from 'next/image';
import Link from 'next/link' import Link from 'next/link';
import clsx from 'clsx' import clsx from 'clsx';
import { Card } from '@/components/Card' import { Card } from '@/components/Card';
import { Container } from '@/components/Container' import { Container } from '@/components/Container';
import originalDeedImage from '@/images/original-deed.png' import originalDeedImage from '@/images/original-deed.png';
import { EnvelopeIcon } from '@heroicons/react/24/solid' import { EnvelopeIcon } from '@heroicons/react/24/solid';
function SocialLink({ function SocialLink({
className, className,
@ -14,10 +14,10 @@ function SocialLink({
children, children,
icon: Icon, icon: Icon,
}: { }: {
className?: string className?: string;
href: string href: string;
icon: React.ComponentType<{ className?: string }> icon: React.ComponentType<{ className?: string }>;
children: React.ReactNode children: React.ReactNode;
}) { }) {
return ( return (
<li className={clsx(className, 'flex')}> <li className={clsx(className, 'flex')}>
@ -29,7 +29,7 @@ function SocialLink({
<span className="ml-4">{children}</span> <span className="ml-4">{children}</span>
</Link> </Link>
</li> </li>
) );
} }
function TimelineEntry({ function TimelineEntry({
@ -38,51 +38,41 @@ function TimelineEntry({
event, event,
cta, cta,
href, href,
children children,
}: { }: {
title: string title: string;
description: string description: string;
event?: string event?: string;
cta?: string cta?: string;
href?: string href?: string;
children?: React.ReactNode children?: React.ReactNode;
}) { }) {
return ( return (
<Card as="article"> <Card as="article">
<Card.Title as="h3" href={href}> <Card.Title as="h3" href={href}>
{title} {title}
</Card.Title> </Card.Title>
{event {event ? <Card.Eyebrow decorate>{event}</Card.Eyebrow> : null}
? <Card.Eyebrow decorate>{event}</Card.Eyebrow>
: null}
<Card.Description>{description}</Card.Description> <Card.Description>{description}</Card.Description>
{cta {cta ? <Card.Cta>{cta}</Card.Cta> : null}
? <Card.Cta>{cta}</Card.Cta>
: null}
{children} {children}
</Card> </Card>
) );
} }
function DateListItem({ function DateListItem({ year, value }: { year?: string; value: string }) {
year, value
}: {
year?: string
value: string
}) {
return ( return (
<div className='flex space-x-4'> <div className="flex space-x-4">
<div className='w-10'>{year}</div> <div className="w-10">{year}</div>
<div>{value}</div> <div>{value}</div>
</div> </div>
) );
} }
export const metadata: Metadata = { export const metadata: Metadata = {
title: 'Hall History', title: 'Hall History',
description: description: 'The history of the West Sound Community Hall on Orcas Island.',
'The history of the West Sound Community Hall on Orcas Island.', };
}
export default function About() { export default function About() {
return ( return (
@ -108,15 +98,15 @@ export default function About() {
<div className="mt-6 space-y-7 text-base text-zinc-600"> <div className="mt-6 space-y-7 text-base text-zinc-600">
<p> <p>
The West Sound Community Hall represents the history and character The West Sound Community Hall represents the history and character
of Orcas Island. Members of the nonprofit West Sound Community Club, of Orcas Island. Members of the nonprofit West Sound Community
as stewards of the Hall, strive to maintain the integrity of the Club, as stewards of the Hall, strive to maintain the integrity of
Hall as a unique gathering place for future generations. the Hall as a unique gathering place for future generations.
</p> </p>
<p> <p>
In 1902 volunteers began building the Hall with materials supplied by George In 1902 volunteers began building the Hall with materials supplied
Adkins. The building site was donated two years earlier by Alexander Chalmers. by George Adkins. The building site was donated two years earlier
The Hall was erected to serve as a central meeting place for residents of the by Alexander Chalmers. The Hall was erected to serve as a central
West Sound area. meeting place for residents of the West Sound area.
</p> </p>
<p> <p>
Over the years the original one-room schoolhouse design has Over the years the original one-room schoolhouse design has
@ -143,120 +133,105 @@ export default function About() {
description='Alexander Chalmers donated "property to be used for a site for a public hall" to the West Sound Hall Company. The donated land was originally part of a large parcel homesteaded by Peter LaPlante in 1884.' description='Alexander Chalmers donated "property to be used for a site for a public hall" to the West Sound Hall Company. The donated land was originally part of a large parcel homesteaded by Peter LaPlante in 1884.'
/> />
<TimelineEntry <TimelineEntry
title='1902 - 1903' title="1902 - 1903"
description='The West Sound Community Hall was constructed. Funds for building materials were donated by George Adkins. The Hall was constructed by Adkins, Omer Freel, Joe Verrier, Peter LaPlante, Gus Smedberg and others. Mr. Adkins, who moved to West Sound in 1896, founded the West Sound Trading Company which was located on a wharf opposite the Hall.' description="The West Sound Community Hall was constructed. Funds for building materials were donated by George Adkins. The Hall was constructed by Adkins, Omer Freel, Joe Verrier, Peter LaPlante, Gus Smedberg and others. Mr. Adkins, who moved to West Sound in 1896, founded the West Sound Trading Company which was located on a wharf opposite the Hall."
/> />
<TimelineEntry <TimelineEntry
title='1904' title="1904"
description='The Chase brothers, who owned the Chase Bros. Mill in West Sound, donated lumber and built an addition for a stage on the north end of the Hall.' description="The Chase brothers, who owned the Chase Bros. Mill in West Sound, donated lumber and built an addition for a stage on the north end of the Hall."
/> />
<TimelineEntry <TimelineEntry
title='1903 - 1935' title="1903 - 1935"
description='During this period the following organizations were regular users of the Hall:' description="During this period the following organizations were regular users of the Hall:"
> >
<div className="relative z-10 ml-4 mt-2 text-sm text-zinc-600"> <div className="relative z-10 ml-4 mt-2 text-sm text-zinc-600">
<DateListItem <DateListItem
year='1903' year="1903"
value='Women&apos;s Christian Temperance Union Woodmen Lodge' value="Women's Christian Temperance Union Woodmen Lodge"
/> />
<DateListItem value="West Sound Literary Society" />
<DateListItem year="1911" value="West Sound Boy's Band" />
<DateListItem <DateListItem
value='West Sound Literary Society' year="1912"
/> value="West Sound Grange (130 members in 1913)"
<DateListItem
year='1911'
value='West Sound Boy&apos;s Band'
/>
<DateListItem
year='1912'
value='West Sound Grange (130 members in 1913)'
/>
<DateListItem
value='Odd Fellows'
/>
<DateListItem
value='West Sound Athletic Club'
/>
<DateListItem
year='1913'
value='West Sound Orchestra'
/>
<DateListItem
value='West Sound Baseball Team'
/>
<DateListItem
year='1922'
value='Farm Bureau'
/>
<DateListItem
year='1925'
value='Fidelis Circle'
/> />
<DateListItem value="Odd Fellows" />
<DateListItem value="West Sound Athletic Club" />
<DateListItem year="1913" value="West Sound Orchestra" />
<DateListItem value="West Sound Baseball Team" />
<DateListItem year="1922" value="Farm Bureau" />
<DateListItem year="1925" value="Fidelis Circle" />
</div> </div>
</TimelineEntry> </TimelineEntry>
<TimelineEntry <TimelineEntry
title='1905 - 1910' title="1905 - 1910"
description='Sometime during this period the Hall was increased in size by adding twelve feet to the south end.' description="Sometime during this period the Hall was increased in size by adding twelve feet to the south end."
/> />
<TimelineEntry <TimelineEntry
title='1935' title="1935"
description='West Sound Hall Company is reorganized as the West Sound Community Club.' description="West Sound Hall Company is reorganized as the West Sound Community Club."
/> />
<TimelineEntry <TimelineEntry
title='Sometime after 1935' title="Sometime after 1935"
description='The stage, then located at the north end of the hall, was remodeled " to make it as wide as the Hall and 22 feet back providing room for stage, kitchen and rest room - cost $122.85." Logs were salvaged from local beaches and the Thatcher Mill sawed them into lumber for use in remodel.' description='The stage, then located at the north end of the hall, was remodeled " to make it as wide as the Hall and 22 feet back providing room for stage, kitchen and rest room - cost $122.85." Logs were salvaged from local beaches and the Thatcher Mill sawed them into lumber for use in remodel.'
/> />
<TimelineEntry <TimelineEntry
title='1940&apos;s' title="1940's"
description='During the Second World War, Fidelis Circle cared for and managed the Hall.' description="During the Second World War, Fidelis Circle cared for and managed the Hall."
/> />
<TimelineEntry <TimelineEntry
title='Sometime after 1947' title="Sometime after 1947"
description='When it was determined the foundation under the stage was failing, the stage was converted to a kitchen and the floor was lowered to match the level of the main Hall floor. The current stage on the east side of the Hall was probably added at the same time.' description="When it was determined the foundation under the stage was failing, the stage was converted to a kitchen and the floor was lowered to match the level of the main Hall floor. The current stage on the east side of the Hall was probably added at the same time."
/> />
<TimelineEntry <TimelineEntry
title='1956' title="1956"
description='The Orcas Island Yacht Club was founded and began using the Hall as a clubhouse.' description="The Orcas Island Yacht Club was founded and began using the Hall as a clubhouse."
/> />
<TimelineEntry <TimelineEntry
title='1957' title="1957"
description='The south entry was moved to the present location on the east side of the Hall. The old entry and scaffolding for the new entry are shown in a photo taken in April 1957. This is the only known photo showing in detail how the exterior of the Hall looked prior to 1957.' description="The south entry was moved to the present location on the east side of the Hall. The old entry and scaffolding for the new entry are shown in a photo taken in April 1957. This is the only known photo showing in detail how the exterior of the Hall looked prior to 1957."
/> />
<TimelineEntry <TimelineEntry
title='1972' title="1972"
description='Members of the Orcas Island Yacht Club made major repairs to the building structure and the interior was completely remodeled. Prior to the remodel the interior walls were burlap cloth and there was no ceiling in the hall.' description="Members of the Orcas Island Yacht Club made major repairs to the building structure and the interior was completely remodeled. Prior to the remodel the interior walls were burlap cloth and there was no ceiling in the hall."
/> />
<TimelineEntry <TimelineEntry
title='1979' title="1979"
description='The property on which the hall sits was increased in size by donation a strip of land surrounding the hall structure.' description="The property on which the hall sits was increased in size by donation a strip of land surrounding the hall structure."
/> />
<TimelineEntry <TimelineEntry
title='1981' title="1981"
description='Major foundation repairs of a temporary nature were made.' description="Major foundation repairs of a temporary nature were made."
/> />
<TimelineEntry <TimelineEntry
title='1996' title="1996"
description='The entry stairs and deck were rebuilt.' description="The entry stairs and deck were rebuilt."
/> />
<TimelineEntry <TimelineEntry
title='1999' title="1999"
description='Realizing a major renovation of the Hall was needed, the officers of the West Sound Community Club reorganized the club as a nonprofit corporation and applied for 501(c)(3) status. A Centennial Building Committee was created to manage the renovation. The goal was to complete the renovation prior to the 100th anniversary of the Hall&appos;s construction in 1902. Con Russell Construction was selected as the contractor and construction started in February 2000. Major repairs, including a new foundation, metal roof, insulation, and painting the exterior, were completed by October of 2000. To date, the Club has received contributions and pledges of more than $81,000 to pay for the renovation and establish an endowment for future maintenance of the Hall.' description="Realizing a major renovation of the Hall was needed, the officers of the West Sound Community Club reorganized the club as a nonprofit corporation and applied for 501(c)(3) status. A Centennial Building Committee was created to manage the renovation. The goal was to complete the renovation prior to the 100th anniversary of the Hall&appos;s construction in 1902. Con Russell Construction was selected as the contractor and construction started in February 2000. Major repairs, including a new foundation, metal roof, insulation, and painting the exterior, were completed by October of 2000. To date, the Club has received contributions and pledges of more than $81,000 to pay for the renovation and establish an endowment for future maintenance of the Hall."
/> />
<TimelineEntry <TimelineEntry
title='1999 - October 18th' title="1999 - October 18th"
description='The West Sound Community Hall was listed in the Washington Heritage Register.' description="The West Sound Community Hall was listed in the Washington Heritage Register."
/> />
<TimelineEntry <TimelineEntry
title='2000 - June 12th' title="2000 - June 12th"
description='The Washington Department of Revenue approved the West Sound Community Club&apos;s application for property tax exemption for the West Sound Community Hall as a "public assembly hall".' description='The Washington Department of Revenue approved the West Sound Community Club&apos;s application for property tax exemption for the West Sound Community Hall as a "public assembly hall".'
/> />
<TimelineEntry <TimelineEntry
title='2000 - August 31st' title="2000 - August 31st"
description='West Sound Community Hall web site first launched.' description="West Sound Community Hall web site first launched."
/> />
<TimelineEntry <TimelineEntry
title='2001 - April 21st' title="2001 - April 21st"
description='A new "West Sound Community Hall" sign was placed on the front of the Hall in a ceremony attended by more than twenty members of the West Sound Community Club. Placement of the sign marked the completion of a major renovation of the Hall started in February 2000.' description='A new "West Sound Community Hall" sign was placed on the front of the Hall in a ceremony attended by more than twenty members of the West Sound Community Club. Placement of the sign marked the completion of a major renovation of the Hall started in February 2000.'
/> />
<TimelineEntry title="2018" description="The hall is repainted" />
<TimelineEntry
title="2025"
description="Various repairs and maintenance are performed including residing the North exterior wall. The well is found to contain ecoli and the board of directors begins working on hooking up to West Sound Water Association."
/>
</div> </div>
</div> </div>
<div className="lg:pl-20"> <div className="lg:pl-20">
@ -271,5 +246,5 @@ export default function About() {
</div> </div>
</div> </div>
</Container> </Container>
) );
} }

View File

@ -1,46 +1,43 @@
import Image from 'next/image' import Image from 'next/image';
import Link from 'next/link'
import { Card } from '@/components/Card'
import { Container } from '@/components/Container'
import { CalendarDaysIcon } from '@heroicons/react/24/solid'
import exteriorFrontImage from '@/images/photos/exterior-front.png'
import { type BlogPostWithSlug, getAllBlogPosts } from '@/lib/articles'
import { formatDate } from '@/lib/formatDate'
import { getUpcomingEvents, Event } from './calendar/page'
import dayjs from 'dayjs'
import Link from 'next/link';
import { Card } from '@/components/Card';
import { Container } from '@/components/Container';
import { CalendarDaysIcon } from '@heroicons/react/24/solid';
import exteriorFrontImage from '@/images/photos/exterior-front.png';
import { type BlogPostWithSlug, getAllBlogPosts } from '@/lib/articles';
import { formatDate } from '@/lib/formatDate';
import { getUpcomingEvents, Event } from './calendar/page';
import dayjs from 'dayjs';
function LinkButton({ function LinkButton({
href, href,
children, children,
}: { }: {
href: string href: string;
children: React.ReactNode children: React.ReactNode;
}) { }) {
return ( return (
<Link <Link
href={href} href={href}
className="rounded-md px-3 py-2 font-semibold text-center transition bg-sky-300 hover:bg-sky-400" className="rounded-md bg-sky-300 px-3 py-2 text-center font-semibold transition hover:bg-sky-400"
> >
{children} {children}
</Link> </Link>
) );
} }
function Article({ article }: { article: BlogPostWithSlug }) { function Article({ article }: { article: BlogPostWithSlug }) {
return ( return (
<Card as="article"> <Card as="article">
<Card.Title href={`/news/${article.slug}`}> <Card.Title href={`/news/${article.slug}`}>{article.title}</Card.Title>
{article.title}
</Card.Title>
<Card.Eyebrow as="time" dateTime={article.date} decorate> <Card.Eyebrow as="time" dateTime={article.date} decorate>
{formatDate(article.date)} {formatDate(article.date)}
</Card.Eyebrow> </Card.Eyebrow>
<Card.Description>{article.description}</Card.Description> <Card.Description>{article.description}</Card.Description>
<Card.Cta>Read article</Card.Cta> <Card.Cta>Read article</Card.Cta>
</Card> </Card>
) );
} }
function EventListItem({ event }: { event: Event }) { function EventListItem({ event }: { event: Event }) {
@ -57,40 +54,40 @@ function EventListItem({ event }: { event: Event }) {
{event.name} {event.name}
</dd> </dd>
<dt className="sr-only">Date</dt> <dt className="sr-only">Date</dt>
<dd className="text-xs text-zinc-500"> <dd className="text-xs text-zinc-500">{date}</dd>
{date}
</dd>
<dt className="sr-only">Time</dt> <dt className="sr-only">Time</dt>
{event.allDay {event.allDay ? (
? <dd <dd className="ml-auto text-xs text-zinc-400" aria-label="All day">
className="ml-auto text-xs text-zinc-400"
aria-label="All day"
>
All day All day
</dd> </dd>
: (event.end ) : event.end ? (
? ( <dd
<dd className="ml-auto text-xs text-zinc-400"
className="ml-auto text-xs text-zinc-400" aria-label={`${start.format('YYYY-MM-DD HH:mm')} until ${end.format(
aria-label={`${start.format('YYYY-MM-DD HH:mm')} until ${end.format('YYYY-MM-DD HH-mm')}`} 'YYYY-MM-DD HH-mm',
> )}`}
<time dateTime={start.format('YYYY-MM-DD HH:mm')}>{start.format('h:mm a')}</time>{' '} >
<span aria-hidden="true"></span>{' '} <time dateTime={start.format('YYYY-MM-DD HH:mm')}>
<time dateTime={end.format('YYYY-MM-DD HH-mm')}>{end.format('h:mm a')}</time>{' '} {start.format('h:mm a')}
</dd> </time>{' '}
) : ( <span aria-hidden="true"></span>{' '}
<dd <time dateTime={end.format('YYYY-MM-DD HH-mm')}>
className="ml-auto text-xs text-zinc-400" {end.format('h:mm a')}
aria-label={start.format('YYYY-MM-DD HH-mm')} </time>{' '}
> </dd>
<time dateTime={start.format('YYYY-MM-DD HH-mm')}>{start.format('h:mm a')}</time>{' '} ) : (
</dd> <dd
) className="ml-auto text-xs text-zinc-400"
) aria-label={start.format('YYYY-MM-DD HH-mm')}
} >
<time dateTime={start.format('YYYY-MM-DD HH-mm')}>
{start.format('h:mm a')}
</time>{' '}
</dd>
)}
</dl> </dl>
</li> </li>
) );
} }
async function Events() { async function Events() {
@ -113,40 +110,44 @@ async function Events() {
</Button> </Button>
*/} */}
</div> </div>
) );
} }
export default async function Home() { export default async function Home() {
let articles = (await getAllBlogPosts()).slice(0, 4) let articles = (await getAllBlogPosts()).slice(0, 4);
return ( return (
<> <>
<div className="mx-auto max-w-7xl px-6 mt-24 lg:px-8"> <div className="mx-auto mt-24 max-w-7xl px-6 lg:px-8">
<div className="relative px-4 sm:px-8 lg:px-12"> <div className="relative px-4 sm:px-8 lg:px-12">
<div className="mx-auto max-w-2xl lg:mx-0 grid lg:max-w-none grid-cols-1 lg:grid-cols-2 lg:gap-x-8 gap-y-10"> <div className="mx-auto grid max-w-2xl grid-cols-1 gap-y-10 lg:mx-0 lg:max-w-none lg:grid-cols-2 lg:gap-x-8">
<h1 className="text-4xl font-bold tracking-tight text-zinc-800 sm:text-6xl lg:col-span-2"> <h1 className="text-4xl font-bold tracking-tight text-zinc-800 sm:text-6xl lg:col-span-2">
West Sound Community Hall West Sound Community Hall
</h1> </h1>
<div className="max-w-xl"> <div className="max-w-xl">
<p className="mt-6 text-base text-zinc-600"> <p className="mt-6 text-base text-zinc-600">
The West Sound Community Hall is located in the hamlet of West Sound The West Sound Community Hall is located in the hamlet of West
on Orcas Island, about 10 minutes from the ferry landing and Sound on Orcas Island, about 10 minutes from the ferry landing
Eastsound. It has served as a public assembly hall since it was and Eastsound. It has served as a public assembly hall since it
built by volunteers in 1902. was built by volunteers in 1902.
</p> </p>
<p className="mt-6 text-base text-zinc-600"> <p className="mt-6 text-base text-zinc-600">
Facing West Sound, the Hall is at the heart of the West Sound community. Facing West Sound, the Hall is at the heart of the West Sound
community.
</p> </p>
</div> </div>
<Image <Image
src={exteriorFrontImage} src={exteriorFrontImage}
alt="Exterior front of the West Sound Hall" alt="Exterior front of the West Sound Hall"
className="lg:row-span-2 w-full max-w-xl rounded-2xl object-cover lg:max-w-none" className="w-full max-w-xl rounded-2xl object-cover lg:row-span-2 lg:max-w-none"
/> />
<div className="h-fit max-w-xl rounded-2xl border border-zinc-100 p-6"> <div className="h-fit max-w-xl rounded-2xl border border-zinc-100 p-6">
<div className="flex flex-col gap-y-4"> <div className="flex flex-col gap-y-4">
<LinkButton href='/club'>Join or Renew your Membership</LinkButton> <LinkButton href="/club">
<LinkButton href='/rental'>Rent the Hall</LinkButton> Join or Renew your Membership
</LinkButton>
<LinkButton href="/donate">Donate</LinkButton>
<LinkButton href="/rental">Rent the Hall</LinkButton>
</div> </div>
</div> </div>
</div> </div>
@ -165,5 +166,5 @@ export default async function Home() {
</div> </div>
</Container> </Container>
</> </>
) );
} }

View File

@ -1,99 +0,0 @@
import { type Metadata } from 'next'
import Image from 'next/image'
import { Card } from '@/components/Card'
import { SimpleLayout } from '@/components/SimpleLayout'
import logoAnimaginary from '@/images/logos/animaginary.svg'
import logoCosmos from '@/images/logos/cosmos.svg'
import logoHelioStream from '@/images/logos/helio-stream.svg'
import logoOpenShuttle from '@/images/logos/open-shuttle.svg'
import logoPlanetaria from '@/images/logos/planetaria.svg'
const projects = [
{
name: 'Planetaria',
description:
'Creating technology to empower civilians to explore space on their own terms.',
link: { href: 'http://planetaria.tech', label: 'planetaria.tech' },
logo: logoPlanetaria,
},
{
name: 'Animaginary',
description:
'High performance web animation library, hand-written in optimized WASM.',
link: { href: '#', label: 'github.com' },
logo: logoAnimaginary,
},
{
name: 'HelioStream',
description:
'Real-time video streaming library, optimized for interstellar transmission.',
link: { href: '#', label: 'github.com' },
logo: logoHelioStream,
},
{
name: 'cosmOS',
description:
'The operating system that powers our Planetaria space shuttles.',
link: { href: '#', label: 'github.com' },
logo: logoCosmos,
},
{
name: 'OpenShuttle',
description:
'The schematics for the first rocket I designed that successfully made it to orbit.',
link: { href: '#', label: 'github.com' },
logo: logoOpenShuttle,
},
]
function LinkIcon(props: React.ComponentPropsWithoutRef<'svg'>) {
return (
<svg viewBox="0 0 24 24" aria-hidden="true" {...props}>
<path
d="M15.712 11.823a.75.75 0 1 0 1.06 1.06l-1.06-1.06Zm-4.95 1.768a.75.75 0 0 0 1.06-1.06l-1.06 1.06Zm-2.475-1.414a.75.75 0 1 0-1.06-1.06l1.06 1.06Zm4.95-1.768a.75.75 0 1 0-1.06 1.06l1.06-1.06Zm3.359.53-.884.884 1.06 1.06.885-.883-1.061-1.06Zm-4.95-2.12 1.414-1.415L12 6.344l-1.415 1.413 1.061 1.061Zm0 3.535a2.5 2.5 0 0 1 0-3.536l-1.06-1.06a4 4 0 0 0 0 5.656l1.06-1.06Zm4.95-4.95a2.5 2.5 0 0 1 0 3.535L17.656 12a4 4 0 0 0 0-5.657l-1.06 1.06Zm1.06-1.06a4 4 0 0 0-5.656 0l1.06 1.06a2.5 2.5 0 0 1 3.536 0l1.06-1.06Zm-7.07 7.07.176.177 1.06-1.06-.176-.177-1.06 1.06Zm-3.183-.353.884-.884-1.06-1.06-.884.883 1.06 1.06Zm4.95 2.121-1.414 1.414 1.06 1.06 1.415-1.413-1.06-1.061Zm0-3.536a2.5 2.5 0 0 1 0 3.536l1.06 1.06a4 4 0 0 0 0-5.656l-1.06 1.06Zm-4.95 4.95a2.5 2.5 0 0 1 0-3.535L6.344 12a4 4 0 0 0 0 5.656l1.06-1.06Zm-1.06 1.06a4 4 0 0 0 5.657 0l-1.061-1.06a2.5 2.5 0 0 1-3.535 0l-1.061 1.06Zm7.07-7.07-.176-.177-1.06 1.06.176.178 1.06-1.061Z"
fill="currentColor"
/>
</svg>
)
}
export const metadata: Metadata = {
title: 'Projects',
description: 'Things Ive made trying to put my dent in the universe.',
}
export default function Projects() {
return (
<SimpleLayout
title="Things Ive made trying to put my dent in the universe."
intro="Ive worked on tons of little projects over the years but these are the ones that Im most proud of. Many of them are open-source, so if you see something that piques your interest, check out the code and contribute if you have ideas for how it can be improved."
>
<ul
role="list"
className="grid grid-cols-1 gap-x-12 gap-y-16 sm:grid-cols-2 lg:grid-cols-3"
>
{projects.map((project) => (
<Card as="li" key={project.name}>
<div className="relative z-10 flex h-12 w-12 items-center justify-center rounded-full bg-white shadow-md shadow-zinc-800/5 ring-1 ring-zinc-900/5">
<Image
src={project.logo}
alt=""
className="h-8 w-8"
unoptimized
/>
</div>
<h2 className="mt-6 text-base font-semibold text-zinc-800">
<Card.Link href={project.link.href}>{project.name}</Card.Link>
</h2>
<Card.Description>{project.description}</Card.Description>
<p className="relative z-10 mt-6 flex text-sm font-medium text-zinc-400 transition group-hover:text-teal-500">
<LinkIcon className="h-6 w-6 flex-none" />
<span className="ml-2">{project.link.label}</span>
</p>
</Card>
))}
</ul>
</SimpleLayout>
)
}

View File

@ -1,18 +1,17 @@
import { type Metadata } from 'next' import { type Metadata } from 'next';
import { SimpleLayout } from '@/components/SimpleLayout' import { SimpleLayout } from '@/components/SimpleLayout';
export const metadata: Metadata = { export const metadata: Metadata = {
title: 'Thank You', title: 'Thank You',
description: 'Thanks for becoming a member.', description: 'Thanks for becoming a member.',
} };
export default async function ThankYou({ export default async function ThankYou({
searchParams searchParams,
}: { }: {
searchParams: { [key: string]: string | string[] | undefined } searchParams: { [key: string]: string | string[] | undefined };
}) { }) {
const { redirect_status } = searchParams; const { redirect_status } = searchParams;
if (redirect_status !== 'succeeded') { if (redirect_status !== 'succeeded') {
// TODO: Display error // TODO: Display error
@ -23,9 +22,11 @@ export default async function ThankYou({
title="Thanks for becoming a member." title="Thanks for becoming a member."
intro="Thank you for joining the West Sound Community Club." intro="Thank you for joining the West Sound Community Club."
> >
<p className="mt-6 text-base text-zinc-600"> <p className="text-base text-zinc-600">
Your financial support helps us perserve this historic building and to host events for the community. We&apos;ll add you to our member mailing list so you receive announcement emails about upcoming events. Your financial support helps us perserve this historic building and to
host events for the community. We&apos;ll add you to our member mailing
list so you receive announcement emails about upcoming events.
</p> </p>
</SimpleLayout> </SimpleLayout>
) );
} }

BIN
src/images/logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 91 KiB

View File

@ -1,12 +0,0 @@
<svg width="32" height="32" fill="none" xmlns="http://www.w3.org/2000/svg">
<circle cx="16" cy="16" r="16" fill="url(#a)"/>
<rect x="16" y="8" width="8" height="8" rx="2" fill="#fff" fill-opacity=".4"/>
<rect x="12" y="12" width="8" height="8" rx="2" fill="#fff" fill-opacity=".5"/>
<rect x="8" y="16" width="8" height="8" rx="2" fill="#fff"/>
<defs>
<radialGradient id="a" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="matrix(0 18.5 -18.5 0 16 13.5)">
<stop offset=".169" stop-color="#22D3EE"/>
<stop offset="1" stop-color="#7451FF"/>
</radialGradient>
</defs>
</svg>

Before

Width:  |  Height:  |  Size: 625 B

View File

@ -1,25 +0,0 @@
<svg width="32" height="32" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#a)">
<rect width="32" height="32" rx="16" fill="#001120"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M10 13a1 1 0 0 0-1 1v4a1 1 0 0 0 1 1h2a1 1 0 0 0 1-1v-4a1 1 0 0 0-1-1h-2Zm-3 1a3 3 0 0 1 3-3h2a3 3 0 0 1 3 3v4a3 3 0 0 1-3 3h-2a3 3 0 0 1-3-3v-4Zm10-.257A2.743 2.743 0 0 1 19.743 11H22a3 3 0 0 1 3 3 1 1 0 1 1-2 0 1 1 0 0 0-1-1h-2.257a.743.743 0 0 0-.235 1.449l3.616 1.205A2.743 2.743 0 0 1 22.257 21H20a3 3 0 0 1-3-3 1 1 0 1 1 2 0 1 1 0 0 0 1 1h2.257a.743.743 0 0 0 .235-1.449l-3.616-1.205A2.743 2.743 0 0 1 17 13.743Z" fill="url(#b)"/>
<path fill="#fff" fill-opacity=".1" d="M0 23h32v1H0z"/>
<path fill="#fff" fill-opacity=".1" d="M5 0v32H4V0z"/>
<path fill="#fff" fill-opacity=".1" d="M0 8h32v1H0z"/>
<path fill="url(#c)" d="M0 23h32v1H0z"/>
<path fill="#fff" fill-opacity=".1" d="M28 0v32h-1V0z"/>
</g>
<defs>
<linearGradient id="b" x1="11" y1="12" x2="11" y2="20" gradientUnits="userSpaceOnUse">
<stop stop-color="#fff"/>
<stop offset="1" stop-color="#E3E8ED"/>
</linearGradient>
<linearGradient id="c" x1="1.5" y1="23.5" x2="30.5" y2="23.5" gradientUnits="userSpaceOnUse">
<stop stop-color="#fff" stop-opacity="0"/>
<stop offset=".486" stop-color="#fff"/>
<stop offset="1" stop-color="#fff" stop-opacity="0"/>
</linearGradient>
<clipPath id="a">
<rect width="32" height="32" rx="16" fill="#fff"/>
</clipPath>
</defs>
</svg>

Before

Width:  |  Height:  |  Size: 1.5 KiB

View File

@ -1,5 +0,0 @@
<svg width="32" height="32" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect width="32" height="32" rx="16" fill="#F43F5E" />
<path d="M9 11a2 2 0 0 1 2-2h2v12a2 2 0 0 1-2 2H9V11ZM19 11a2 2 0 0 1 2-2h2v12a2 2 0 0 1-2 2h-2V11Z" fill="#fff" />
<path d="M15.447 16.106A2 2 0 0 1 17.237 15H21v2h-6l.447-.894Z" fill="#fff" />
</svg>

Before

Width:  |  Height:  |  Size: 339 B

View File

@ -1,21 +0,0 @@
<svg width="32" height="32" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#a)">
<rect width="32" height="32" rx="16" fill="#0085FF" />
<path fill="#fff" fill-opacity=".2" d="M0 26h32v1H0z" />
<path fill="#fff" fill-opacity=".2" d="M8 0v32H7V0zM16.5 0v32h-1V0z" />
<path fill="#fff" fill-opacity=".2" d="M0 5h32v1H0zM0 15.5h32v1H0z" />
<path fill="#fff" fill-opacity=".2" d="M25 0v32h-1V0z" />
<path
d="M13 21v-5.485c0-2.959-.228-4.866 1.24-7.435l.892-1.56a1 1 0 0 1 1.736 0l.892 1.56C19.228 10.65 19 12.556 19 15.515V21l3.293 3.293c.63.63.184 1.707-.707 1.707H10.414c-.89 0-1.337-1.077-.707-1.707L13 21Z"
fill="#fff" />
<path
d="M13 26v-5m0 5h6m-6 0h-2.586c-.89 0-1.337-1.077-.707-1.707L13 21m0 0v-5.485c0-2.959-.228-4.866 1.24-7.435l.892-1.56a1 1 0 0 1 1.736 0l.892 1.56C19.228 10.65 19 12.556 19 15.515V21m0 5h2.586c.89 0 1.337-1.077.707-1.707L19 21m0 5v-5"
stroke="#fff" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" />
<path d="M16 13v2" stroke="#0085FF" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" />
</g>
<defs>
<clipPath id="a">
<rect width="32" height="32" rx="16" fill="#fff" />
</clipPath>
</defs>
</svg>

Before

Width:  |  Height:  |  Size: 1.2 KiB

View File

@ -1,24 +0,0 @@
<svg width="32" height="32" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#a)">
<rect width="32" height="32" rx="16" fill="url(#b)"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M12.755 30.136c.051-4.308.484-8.167 1.147-10.985.355-1.509.766-2.677 1.196-3.45.466-.838.801-.951.902-.951.1 0 .436.113.902.95.43.774.841 1.942 1.196 3.451.663 2.818 1.096 6.677 1.147 10.985.508-.117 1.007-.26 1.494-.428-.074-4.224-.508-8.042-1.18-10.9-.327-1.389-.718-2.584-1.169-3.497 1.498.715 2.887 2.097 4.035 4.048 1.257 2.138 2.179 4.894 2.589 8 .47-.374.916-.775 1.335-1.203-.488-2.878-1.402-5.467-2.631-7.557-.652-1.107-1.402-2.09-2.236-2.901a16.296 16.296 0 0 1 7.851 6.01c.236-.55.439-1.117.606-1.699C26.688 15.892 21.653 13.25 16 13.25c-5.653 0-10.688 2.642-13.939 6.759.167.582.37 1.15.606 1.7a16.295 16.295 0 0 1 7.85-6.011c-.833.811-1.583 1.794-2.235 2.9-1.23 2.091-2.143 4.68-2.631 7.558.42.428.866.83 1.335 1.203.41-3.106 1.332-5.862 2.59-8 1.147-1.95 2.536-3.333 4.034-4.048-.451.913-.842 2.108-1.168 3.496-.673 2.86-1.107 6.677-1.18 10.901.486.168.984.311 1.493.428Zm-.002 1.534a15.857 15.857 0 0 1-1.502-.387c.017 4.623.465 8.825 1.19 11.91.327 1.388.718 2.583 1.169 3.496-1.498-.715-2.887-2.097-4.035-4.048C7.845 39.698 6.75 35.584 6.75 31c0-.643.022-1.276.063-1.899a16.074 16.074 0 0 1-1.41-1.113c-.1.98-.153 1.986-.153 3.012 0 4.804 1.144 9.19 3.032 12.401.652 1.107 1.402 2.09 2.236 2.901C4.24 44.052-.25 38.051-.25 31c0-2.814.715-5.46 1.974-7.768a15.914 15.914 0 0 1-.741-1.699A17.667 17.667 0 0 0-1.75 31c0 9.803 7.947 17.75 17.75 17.75S33.75 40.803 33.75 31c0-3.48-1.002-6.727-2.733-9.467-.214.583-.462 1.15-.74 1.699A16.178 16.178 0 0 1 32.25 31c0 7.05-4.49 13.053-10.768 15.302.834-.811 1.584-1.794 2.236-2.9C25.605 40.19 26.75 35.803 26.75 31c0-1.026-.052-2.032-.152-3.012-.45.396-.92.768-1.411 1.113.041.623.063 1.256.063 1.899 0 4.584-1.095 8.698-2.825 11.64-1.148 1.952-2.537 3.334-4.035 4.049.451-.913.842-2.108 1.168-3.496.726-3.085 1.174-7.287 1.192-11.91-.492.153-.993.282-1.503.387-.04 4.386-.476 8.319-1.149 11.179-.355 1.509-.766 2.677-1.196 3.45-.466.838-.801.951-.902.951-.1 0-.436-.113-.902-.95-.43-.774-.841-1.942-1.196-3.451-.673-2.86-1.108-6.793-1.149-11.179Z" fill="url(#c)"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M19.894 5.553a1 1 0 0 1-.447 1.342l-9.341 3.552c-.494.247.246.494 0 0-.247-.494-.494.247 0 0l8.447-5.341a1 1 0 0 1 1.341.447Z" fill="url(#d)"/>
</g>
<defs>
<linearGradient id="b" x1="16" y1="0" x2="16" y2="33" gradientUnits="userSpaceOnUse">
<stop stop-color="#00172C"/>
<stop offset=".803" stop-color="#5900EB"/>
</linearGradient>
<linearGradient id="d" x1="19" y1="6" x2="11.5" y2="10" gradientUnits="userSpaceOnUse">
<stop stop-color="#9969E8"/>
<stop offset="1" stop-color="#FFA0D2" stop-opacity=".32"/>
</linearGradient>
<radialGradient id="c" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="matrix(0 35.75 -57.5 0 16 13)">
<stop offset=".14" stop-color="#fff"/>
<stop offset=".514" stop-color="#fff" stop-opacity="0"/>
</radialGradient>
<clipPath id="a">
<rect width="32" height="32" rx="16" fill="#fff"/>
</clipPath>
</defs>
</svg>

Before

Width:  |  Height:  |  Size: 3.2 KiB

View File

@ -6,6 +6,8 @@ const serverToken = process.env.POSTMARK_SERVER_TOKEN || '';
const client = new ServerClient(serverToken); const client = new ServerClient(serverToken);
export function emailNotification({ export function emailNotification({
subject,
bodyPrefix,
name, name,
type, type,
amount, amount,
@ -13,6 +15,8 @@ export function emailNotification({
phone, phone,
address, address,
}: { }: {
subject: string;
bodyPrefix: string;
name: string; name: string;
type: string; type: string;
amount: number; amount: number;
@ -23,9 +27,9 @@ export function emailNotification({
client.sendEmail({ client.sendEmail({
From: fromAddr, From: fromAddr,
To: toAddr, To: toAddr,
Subject: 'New WSCC Membership', Subject: subject,
TextBody: [ TextBody: [
'New WSCC member:', bodyPrefix,
'', '',
'Name: ' + name, 'Name: ' + name,
'Type: ' + type, 'Type: ' + type,