Compare commits
15 Commits
Author | SHA1 | Date | |
---|---|---|---|
72dcb42147 | |||
64c80c11ba | |||
3e8fada7c7 | |||
45cc2e856c | |||
01c3912ba9 | |||
5ed921ec05 | |||
d1c7de593b | |||
4acf298044 | |||
a7ec7bf5eb | |||
78d996a989 | |||
f342b0fa25 | |||
8b57fd492d | |||
f8971e812e | |||
e420bb7ca2 | |||
41f688fd84 |
@ -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
|
||||||
|
@ -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
|
||||||
|
|
||||||
|
@ -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,
|
||||||
|
@ -80,17 +80,13 @@ export default function Club() {
|
|||||||
<TableCell>Vice President</TableCell>
|
<TableCell>Vice President</TableCell>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<TableLeftHeading>Tony Grosinger</TableLeftHeading>
|
<TableLeftHeading>Beth Gasser</TableLeftHeading>
|
||||||
<TableCell>Secretary</TableCell>
|
<TableCell>Secretary</TableCell>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<TableLeftHeading>Temporarily performed by Secretary</TableLeftHeading>
|
<TableLeftHeading>Tony Grosinger</TableLeftHeading>
|
||||||
<TableCell>Treasurer</TableCell>
|
<TableCell>Treasurer</TableCell>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
|
||||||
<TableLeftHeading>Mark Gasser</TableLeftHeading>
|
|
||||||
<TableCell>Director</TableCell>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
<tr>
|
||||||
<TableLeftHeading>Leslie Brown</TableLeftHeading>
|
<TableLeftHeading>Leslie Brown</TableLeftHeading>
|
||||||
<TableCell>Director</TableCell>
|
<TableCell>Director</TableCell>
|
||||||
|
37
src/app/club/mailinglist.tsx
Normal 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>);
|
||||||
|
}
|
@ -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,38 +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">
|
||||||
<SocialLink href="/WSCC-Membership-Form.pdf" icon={UserPlusIcon}>
|
<MailingListSignupForm />
|
||||||
Membership Form
|
|
||||||
</SocialLink>
|
|
||||||
<SocialLink href="https://groups.orcashub.org/invites/YkP79vjDtz" icon={AtSymbolIcon} className="mt-4">
|
|
||||||
Club Member Mailing List
|
|
||||||
</SocialLink>
|
|
||||||
<SocialLink href="https://groups.orcashub.org/invites/Lg87tP3G2V" icon={AtSymbolIcon} className="mt-4">
|
|
||||||
West Sound Neighbor to Neighbor Mailing List
|
|
||||||
</SocialLink>
|
|
||||||
<SocialLink
|
<SocialLink
|
||||||
href="mailto:contact@westsoundhall.org"
|
href="mailto:contact@westsoundhall.org"
|
||||||
icon={EnvelopeIcon}
|
icon={EnvelopeIcon}
|
||||||
@ -110,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>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
@ -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="name" 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
@ -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
@ -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>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
31
src/app/donate/thank-you/page.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
@ -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'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'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'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'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'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>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
@ -8,7 +8,7 @@ export const article = {
|
|||||||
author: 'Tony Grosinger',
|
author: 'Tony Grosinger',
|
||||||
date: '2024-02-17',
|
date: '2024-02-17',
|
||||||
title: 'February 2024 Potluck',
|
title: 'February 2024 Potluck',
|
||||||
description: "The second potluck of the year, with a slidshow of Orcas history from John Wachter.",
|
description: "The second potluck of the year, with a slideshow of Orcas history from John Wachter.",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
BIN
src/app/news/2024-09-21-potluck/Land_Bank_Presentation.jpg
Normal file
After Width: | Height: | Size: 4.2 MiB |
54
src/app/news/2024-09-21-potluck/page.mdx
Normal file
@ -0,0 +1,54 @@
|
|||||||
|
import { ArticleLayout } from '@/components/ArticleLayout'
|
||||||
|
import {MdxImage as Image } from '@/components/MdxImage'
|
||||||
|
import landBankPresentation from './Land_Bank_Presentation.jpg'
|
||||||
|
|
||||||
|
export const article = {
|
||||||
|
author: 'Tony Grosinger',
|
||||||
|
date: '2024-09-21',
|
||||||
|
title: 'September 2024 Potluck',
|
||||||
|
description: "Our first potluck after the summer break, with Board of Director nominations for the election next month.",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export const metadata = {
|
||||||
|
title: article.title,
|
||||||
|
description: article.description,
|
||||||
|
}
|
||||||
|
|
||||||
|
export default (props) => <ArticleLayout article={article} {...props} />
|
||||||
|
|
||||||
|
Summer is wrapping up and the monthly potlucks at the West Sound Hall have
|
||||||
|
returned! There are several exciting developments to share with you. Here's a
|
||||||
|
quick update on what was discussed at our recent community potluck:
|
||||||
|
|
||||||
|
In October, a contractor will be repairing the north end of the hall using the
|
||||||
|
funds we raised earlier this year with Give Orcas.
|
||||||
|
|
||||||
|
Also in October, we will be electing the new Board of Directors. Our nominating
|
||||||
|
committee - Cathy and Grant - have announced the slate of candidates. This
|
||||||
|
year's nominees are:
|
||||||
|
|
||||||
|
- Lisa Pedersen for President
|
||||||
|
- Betsy Wareham for Vice President
|
||||||
|
- Beth Gasser for Secretary
|
||||||
|
- Tony Grosinger for Treasurer
|
||||||
|
- Leslie Brown for Director 1
|
||||||
|
- Linn Hulley for Director 2
|
||||||
|
|
||||||
|
No additional nominations were made from the floor during the meeting. We
|
||||||
|
encourage all members to join us at the October 19th potluck to vote for the
|
||||||
|
new board.
|
||||||
|
|
||||||
|
<div className="not-prose flex flex-col items-center">
|
||||||
|
<Image src={landBankPresentation} alt="Lincoln Bormann from the SJC Land Bank presenting at the potluck." />
|
||||||
|
<span className="mt-2 text-sm text-zinc-400">
|
||||||
|
Lincoln Bormann from the SJC Land Bank presenting at the potluck.
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
After the potluck dinner, Lincoln Bormann and Brian Wiese from the San Juan
|
||||||
|
County Land Bank gave a presentation outlining the work they do, how they make
|
||||||
|
decisions about what land to acquire, and provided updates for newly opening
|
||||||
|
parks. The Land Bank will be up for election this fall and you can learn more
|
||||||
|
on their website - https://sjclandbank.org.
|
||||||
|
|
BIN
src/app/news/2024-10-20-elections/N_Wall_Lower.jpg
Normal file
After Width: | Height: | Size: 239 KiB |
BIN
src/app/news/2024-10-20-elections/N_Wall_Upper.jpg
Normal file
After Width: | Height: | Size: 245 KiB |
44
src/app/news/2024-10-20-elections/page.mdx
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
import { ArticleLayout } from '@/components/ArticleLayout'
|
||||||
|
import {MdxImage as Image } from '@/components/MdxImage'
|
||||||
|
import wallUpper from './N_Wall_Upper.jpg'
|
||||||
|
import wallLower from './N_Wall_Lower.jpg'
|
||||||
|
|
||||||
|
export const article = {
|
||||||
|
author: 'Tony Grosinger',
|
||||||
|
date: '2024-10-20',
|
||||||
|
title: 'Election and Building Repairs',
|
||||||
|
description: "Board of director elections took place, as well as some updates on building maintenance.",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export const metadata = {
|
||||||
|
title: article.title,
|
||||||
|
description: article.description,
|
||||||
|
}
|
||||||
|
|
||||||
|
export default (props) => <ArticleLayout article={article} {...props} />
|
||||||
|
|
||||||
|
At the October potluck we accepted votes for the 2024-2025 Board of Directors. Between those present and votes emailed prior to the potluck, we had enough participants to make a quorum and approve the slate.
|
||||||
|
|
||||||
|
There are also several new updates related to hall maintenance. First, repairs to the north wall ar under way! This maintenance work was made possible by generous donations during the Spring Give Orcas campaign where we raised $10,000.
|
||||||
|
|
||||||
|
<div className="not-prose flex flex-col items-center">
|
||||||
|
<Image src={wallUpper} alt="Repairs to the upper portion of the north wall." />
|
||||||
|
<span className="mt-2 text-sm text-zinc-400">
|
||||||
|
Repairs to the upper portion of the north wall.
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="not-prose flex flex-col items-center">
|
||||||
|
<Image src={wallLower} alt="Repairs to the lower portion of the north wall." />
|
||||||
|
<span className="mt-2 text-sm text-zinc-400">
|
||||||
|
Repairs to the lower portion of the north wall.
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
By the end of the month this wall should have brand new siding that will keep out the rain and breeze.
|
||||||
|
|
||||||
|
We also learned this month that our well water is not potable. After working with multiple experts and performing several rounds of chlorination treatment of the well and storage tanks, it has been determined that the best route forward is likely to abandon this water system and to move towards joining West Sound Water Association.
|
||||||
|
|
||||||
|
There is still lots of discussion and planning necessary before we can even break ground on making the hook up, but every party is working very hard to restore drinkable water to the hall as soon as possible!
|
||||||
|
|
123
src/app/page.tsx
@ -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>
|
||||||
</>
|
</>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
@ -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 I’ve made trying to put my dent in the universe.',
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function Projects() {
|
|
||||||
return (
|
|
||||||
<SimpleLayout
|
|
||||||
title="Things I’ve made trying to put my dent in the universe."
|
|
||||||
intro="I’ve worked on tons of little projects over the years but these are the ones that I’m 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>
|
|
||||||
)
|
|
||||||
}
|
|
@ -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'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'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
After Width: | Height: | Size: 91 KiB |
@ -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 |
@ -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 |
@ -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 |
@ -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 |
@ -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 |
@ -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,
|
||||||
|