Compare commits
No commits in common. "main" and "0.0.15" have entirely different histories.
@ -1,7 +1 @@
|
|||||||
NEXT_PUBLIC_SITE_URL=https://westsoundhall.org
|
NEXT_PUBLIC_SITE_URL=https://example.com
|
||||||
STRIPE_SECRET_KEY=sk_XXXX
|
|
||||||
NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY=pk_XXXX
|
|
||||||
POSTMARK_SERVER_TOKEN=XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX
|
|
||||||
FROM_ADDRESS=support@westsoundhall.org
|
|
||||||
ADMIN_ADDRESS=board@westsoundhall.org
|
|
||||||
CALENDAR_ADDR=https://calendar.google.com/calendar/ical/westsoundcommunityclub%40gmail.com/private-XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX/basic.ics
|
|
||||||
|
@ -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@v3
|
uses: docker/setup-buildx-action@v2
|
||||||
|
|
||||||
- name: Login to Gitea Docker registry
|
- name: Login to Gitea Docker registry
|
||||||
uses: docker/login-action@v3
|
uses: docker/login-action@v2
|
||||||
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@v6
|
uses: docker/build-push-action@v4
|
||||||
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
|
||||||
|
|
||||||
|
19
README.md
@ -6,7 +6,10 @@ https://westsoundhall.org
|
|||||||
|
|
||||||
## Running
|
## Running
|
||||||
|
|
||||||
Pre-build containers are created whenever a version is tagged in this repository. Pull the [latest version](https://git.grosinger.net/tgrosinger/-/packages/container/west-sound-hall/) and run on a server with Docker available.
|
Pre-build containers are created whenever a version is tagged in this
|
||||||
|
repository. Pull the [latest
|
||||||
|
version](https://git.grosinger.net/tgrosinger/-/packages/container/west-sound-hall/)
|
||||||
|
and run on a server with Docker available.
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
docker run -p 3000:3000 git.grosinger.net/tgrosinger/west-sound-hall:0.0.14
|
docker run -p 3000:3000 git.grosinger.net/tgrosinger/west-sound-hall:0.0.14
|
||||||
@ -14,11 +17,11 @@ docker run -p 3000:3000 git.grosinger.net/tgrosinger/west-sound-hall:0.0.14
|
|||||||
|
|
||||||
## Updating
|
## Updating
|
||||||
|
|
||||||
### Events on the Homepage and the Calendar
|
### Events on the Homepage
|
||||||
|
|
||||||
The events on the calendar are loaded from the westsoundcommunityclub@gmail.com Google Calendar.
|
The homepage has a list of upcoming events. This list is created from [`src/app/upcoming-events.json`](https://git.grosinger.net/tgrosinger/west-sound-hall/src/branch/main/src/app/upcoming-events.json). To update the events listed, modify that file, tag a new version, and then update the running container to the latest version.
|
||||||
|
|
||||||
Please note that all events on the calendar will be displayed. If an event should not reveal the title to the public, add the word "Private" to the event description (not the title).
|
Events in the past will be automatically hidden from view.
|
||||||
|
|
||||||
### News Posts
|
### News Posts
|
||||||
|
|
||||||
@ -38,7 +41,11 @@ To get started, first install the npm dependencies:
|
|||||||
npm install
|
npm install
|
||||||
```
|
```
|
||||||
|
|
||||||
Next, copy the `.env.example` file from this directory and call it `.env.local`. Fill in the values that have been redacted with their actual secrets. Be sure to use the test environment key from Stripe unless you are setting up production.
|
Next, create a `.env.local` file in the root of your project and set the `NEXT_PUBLIC_SITE_URL` variable to your site's public URL:
|
||||||
|
|
||||||
|
```
|
||||||
|
NEXT_PUBLIC_SITE_URL=https://example.com
|
||||||
|
```
|
||||||
|
|
||||||
Next, run the development server:
|
Next, run the development server:
|
||||||
|
|
||||||
@ -52,4 +59,4 @@ Finally, open [http://localhost:3000](http://localhost:3000) in your browser to
|
|||||||
|
|
||||||
This site is based off of the Spotlight template from Tailwind, and licensed under the [Tailwind UI license](https://tailwindui.com/license).
|
This site is based off of the Spotlight template from Tailwind, and licensed under the [Tailwind UI license](https://tailwindui.com/license).
|
||||||
|
|
||||||
It was purchased by [Tony Grosinger](mailto:tony@grosinger.net).
|
It was purchased by Tony Grosinger.
|
||||||
|
712
package-lock.json
generated
16
package.json
@ -10,39 +10,37 @@
|
|||||||
},
|
},
|
||||||
"browserslist": "defaults, not ie <= 11",
|
"browserslist": "defaults, not ie <= 11",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@headlessui/react": "1.7.19",
|
"@headlessui/react": "1.7.17",
|
||||||
"@heroicons/react": "^2.0.18",
|
"@heroicons/react": "^2.0.18",
|
||||||
"@mapbox/rehype-prism": "^0.8.0",
|
"@mapbox/rehype-prism": "^0.8.0",
|
||||||
"@mdx-js/loader": "^2.3.0",
|
"@mdx-js/loader": "^2.3.0",
|
||||||
"@mdx-js/react": "^2.3.0",
|
"@mdx-js/react": "^2.3.0",
|
||||||
"@next/mdx": "14.2.2",
|
"@next/mdx": "13.4.16",
|
||||||
"@stripe/react-stripe-js": "2.4.0",
|
"@stripe/react-stripe-js": "2.4.0",
|
||||||
"@stripe/stripe-js": "2.2.1",
|
"@stripe/stripe-js": "2.2.1",
|
||||||
"@tailwindcss/forms": "0.5.7",
|
"@tailwindcss/forms": "0.5.7",
|
||||||
"@tailwindcss/typography": "^0.5.4",
|
"@tailwindcss/typography": "^0.5.4",
|
||||||
"@types/ical": "^0.8.3",
|
"@types/node": "20.4.7",
|
||||||
"@types/react": "18.2.18",
|
"@types/react": "18.2.18",
|
||||||
"@types/react-dom": "18.2.7",
|
"@types/react-dom": "18.2.7",
|
||||||
"@types/webpack-env": "^1.18.1",
|
"@types/webpack-env": "^1.18.1",
|
||||||
"autoprefixer": "^10.4.12",
|
"autoprefixer": "^10.4.12",
|
||||||
"cheerio": "^1.0.0-rc.12",
|
"cheerio": "^1.0.0-rc.12",
|
||||||
"clsx": "^1.2.1",
|
"clsx": "^1.2.1",
|
||||||
"dayjs": "^1.11.10",
|
|
||||||
"fast-glob": "^3.2.11",
|
"fast-glob": "^3.2.11",
|
||||||
"feed": "^4.2.2",
|
"feed": "^4.2.2",
|
||||||
"ical": "^0.8.0",
|
"next": "13.4.16",
|
||||||
"next": "14.2.2",
|
"next-themes": "^0.2.1",
|
||||||
"postmark": "4.0.2",
|
"postmark": "4.0.2",
|
||||||
"react": "18.2.0",
|
"react": "18.2.0",
|
||||||
"react-dom": "18.2.0",
|
"react-dom": "18.2.0",
|
||||||
"remark-gfm": "^3.0.1",
|
"remark-gfm": "^3.0.1",
|
||||||
"sharp": "0.32.6",
|
"sharp": "0.32.6",
|
||||||
"stripe": "14.9.0",
|
"stripe": "14.9.0",
|
||||||
"tailwindcss": "3.4.3",
|
"tailwindcss": "^3.3.3",
|
||||||
"typescript": "5.4.5"
|
"typescript": "5.1.6"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/node": "20.4.7",
|
|
||||||
"eslint": "8.45.0",
|
"eslint": "8.45.0",
|
||||||
"eslint-config-next": "13.4.16",
|
"eslint-config-next": "13.4.16",
|
||||||
"prettier": "^3.0.1",
|
"prettier": "^3.0.1",
|
||||||
|
@ -12,10 +12,7 @@ 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,
|
||||||
|
@ -1,124 +0,0 @@
|
|||||||
|
|
||||||
import { type Metadata } from 'next'
|
|
||||||
import Link from 'next/link'
|
|
||||||
import clsx from 'clsx'
|
|
||||||
|
|
||||||
import { Container } from '@/components/Container'
|
|
||||||
import { UserPlusIcon, GiftIcon, EnvelopeIcon } from '@heroicons/react/24/solid'
|
|
||||||
import { TableCell, TableHeading, TableLeftHeading } from '@/components/Table'
|
|
||||||
|
|
||||||
function SocialLink({
|
|
||||||
className,
|
|
||||||
href,
|
|
||||||
children,
|
|
||||||
icon: Icon,
|
|
||||||
}: {
|
|
||||||
className?: string
|
|
||||||
href: string
|
|
||||||
icon: React.ComponentType<{ className?: string }>
|
|
||||||
children: React.ReactNode
|
|
||||||
}) {
|
|
||||||
return (
|
|
||||||
<li className={clsx(className, 'flex')}>
|
|
||||||
<Link
|
|
||||||
href={href}
|
|
||||||
className="group flex text-sm font-medium text-zinc-800 transition hover:text-teal-500"
|
|
||||||
>
|
|
||||||
<Icon className="h-6 w-6 flex-none fill-zinc-500 transition group-hover:fill-teal-500" />
|
|
||||||
<span className="ml-4">{children}</span>
|
|
||||||
</Link>
|
|
||||||
</li>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export const metadata: Metadata = {
|
|
||||||
title: 'West Sound Community Club - Board of Directory',
|
|
||||||
description:
|
|
||||||
'The West Sound Community Club on Orcas Island.',
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
export default function Club() {
|
|
||||||
return (
|
|
||||||
<Container className="mt-16 sm:mt-32">
|
|
||||||
<div className="grid grid-cols-1 gap-y-16 lg:grid-cols-2 lg:grid-rows-[auto_1fr] lg:gap-y-12">
|
|
||||||
<div className="lg:order-first lg:row-span-2">
|
|
||||||
<h1 className="text-4xl font-bold tracking-tight text-zinc-800 sm:text-5xl">
|
|
||||||
The Board of Directors
|
|
||||||
</h1>
|
|
||||||
<div className="mt-6 space-y-7 text-base text-zinc-600">
|
|
||||||
<p>
|
|
||||||
Elections for the Board of Directors are held annually at the October member meeting and potluck.
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<p>
|
|
||||||
If you are interested in being one the ballot at the upcoming
|
|
||||||
election, please
|
|
||||||
<a href="mailto:board@westsoundhall.org"
|
|
||||||
className="pl-1 text-blue-600 visited:text-purple-600 hover:underline">
|
|
||||||
contact the board
|
|
||||||
</a>.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="overflow-x-auto -mx-4 sm:-mx-0">
|
|
||||||
<div className="inline-block min-w-full py-2 align-middle">
|
|
||||||
<table className="min-w-full divide-y divide-gray-300">
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<TableHeading>Name</TableHeading>
|
|
||||||
<TableHeading>Position</TableHeading>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody className="divide-y divide-gray-200 ">
|
|
||||||
<tr>
|
|
||||||
<TableLeftHeading>Lisa Pedersen</TableLeftHeading>
|
|
||||||
<TableCell>President</TableCell>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<TableLeftHeading>Betsy Wareham</TableLeftHeading>
|
|
||||||
<TableCell>Vice President</TableCell>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<TableLeftHeading>Beth Gasser</TableLeftHeading>
|
|
||||||
<TableCell>Secretary</TableCell>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<TableLeftHeading>Tony Grosinger</TableLeftHeading>
|
|
||||||
<TableCell>Treasurer</TableCell>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<TableLeftHeading>Leslie Brown</TableLeftHeading>
|
|
||||||
<TableCell>Director</TableCell>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<TableLeftHeading>Linn Hulley</TableLeftHeading>
|
|
||||||
<TableCell>Director</TableCell>
|
|
||||||
</tr>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="lg:pl-20">
|
|
||||||
<ul role="list">
|
|
||||||
<SocialLink
|
|
||||||
href="mailto:contact@westsoundhall.org"
|
|
||||||
icon={EnvelopeIcon}
|
|
||||||
className="mt-4 border-zinc-100"
|
|
||||||
>
|
|
||||||
contact@westsoundhall.org
|
|
||||||
</SocialLink>
|
|
||||||
<SocialLink
|
|
||||||
href="mailto:contact@westsoundhall.org"
|
|
||||||
icon={EnvelopeIcon}
|
|
||||||
className="mt-4 border-zinc-100"
|
|
||||||
>
|
|
||||||
board@westsoundhall.org
|
|
||||||
</SocialLink>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Container>
|
|
||||||
)
|
|
||||||
}
|
|
@ -1,239 +0,0 @@
|
|||||||
'use client'
|
|
||||||
|
|
||||||
import {
|
|
||||||
ChevronLeftIcon,
|
|
||||||
ChevronRightIcon,
|
|
||||||
ClockIcon,
|
|
||||||
} from '@heroicons/react/20/solid'
|
|
||||||
|
|
||||||
import dayjs from "dayjs";
|
|
||||||
import React from 'react';
|
|
||||||
import { Calendar, Event } from './page';
|
|
||||||
import Link from 'next/link';
|
|
||||||
|
|
||||||
interface day {
|
|
||||||
date: dayjs.Dayjs;
|
|
||||||
isCurrentMonth: boolean;
|
|
||||||
isToday: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
function classNames(...classes: string[]): string {
|
|
||||||
return classes.filter(Boolean).join(' ');
|
|
||||||
}
|
|
||||||
|
|
||||||
const getEvents = (cal: Calendar, d: dayjs.Dayjs): Event[] => {
|
|
||||||
const year = cal[d.year()];
|
|
||||||
if (!year) {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
|
|
||||||
const month = year[d.month()];
|
|
||||||
if (!month) {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
|
|
||||||
return month[d.date()] || [];
|
|
||||||
}
|
|
||||||
|
|
||||||
export const CalendarComponent: React.FC<{ calendar: Calendar }> = ({ calendar }) => {
|
|
||||||
const [selectedDay, setSelectedDay] = React.useState(dayjs().startOf('day'));
|
|
||||||
const [selectedMonth, setSelectedMonth] = React.useState(dayjs().startOf('month'));
|
|
||||||
|
|
||||||
const days = React.useMemo(() => {
|
|
||||||
// Number of greyed days shown before the beginning of the current month.
|
|
||||||
const renderedDaysBeforeMonth = selectedMonth.day();
|
|
||||||
const firstDay = selectedMonth.subtract(renderedDaysBeforeMonth, 'day');
|
|
||||||
const month = selectedMonth.month();
|
|
||||||
const today = dayjs().startOf('day');
|
|
||||||
|
|
||||||
let current = firstDay;
|
|
||||||
const days: day[] = [];
|
|
||||||
|
|
||||||
// Fill out the first week with days from the previous month.
|
|
||||||
for (let i = 0; i < renderedDaysBeforeMonth; i++) {
|
|
||||||
days.push({
|
|
||||||
date: current,
|
|
||||||
isCurrentMonth: false,
|
|
||||||
isToday: current.isSame(today),
|
|
||||||
});
|
|
||||||
current = current.add(1, 'day');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add the days for the selected month.
|
|
||||||
while (current.month() === month) {
|
|
||||||
days.push({
|
|
||||||
date: current,
|
|
||||||
isCurrentMonth: true,
|
|
||||||
isToday: current.isSame(today),
|
|
||||||
});
|
|
||||||
current = current.add(1, 'day');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Finish out the week with days from the following month.
|
|
||||||
while (current.day() > 0) {
|
|
||||||
days.push({
|
|
||||||
date: current,
|
|
||||||
isCurrentMonth: false,
|
|
||||||
isToday: current.isSame(today),
|
|
||||||
});
|
|
||||||
current = current.add(1, 'day');
|
|
||||||
}
|
|
||||||
|
|
||||||
return days;
|
|
||||||
}, [selectedMonth]);
|
|
||||||
|
|
||||||
const isSelectedDay = (d: day): boolean => {
|
|
||||||
return d.date.isSame(selectedDay);
|
|
||||||
}
|
|
||||||
|
|
||||||
const selectedDayEvents = React.useMemo(() => {
|
|
||||||
return getEvents(calendar, selectedDay);
|
|
||||||
}, [calendar, selectedDay]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="lg:flex lg:h-full lg:flex-col">
|
|
||||||
<header className="flex items-center justify-between border-b border-zinc-100 py-4 lg:flex-none">
|
|
||||||
<h1 className="text-base font-semibold leading-6 text-zinc-800">
|
|
||||||
<time dateTime={selectedMonth.format('YYYY-MM')}>{selectedMonth.format('MMMM YYYY')}</time>
|
|
||||||
</h1>
|
|
||||||
<div className="flex items-center">
|
|
||||||
<div className="relative flex items-center rounded-md shadow-sm md:items-stretch">
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={() => { setSelectedMonth(selectedMonth.subtract(1, 'month')) }}
|
|
||||||
className="flex h-9 w-12 items-center justify-center rounded-l-md border-y border-l border-zinc-100 pr-1 text-gray-400 hover:text-gray-500 focus:relative md:w-9 md:pr-0 md:hover:bg-gray-50"
|
|
||||||
>
|
|
||||||
<span className="sr-only">Previous month</span>
|
|
||||||
<ChevronLeftIcon className="h-5 w-5" aria-hidden="true" />
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={() => {
|
|
||||||
setSelectedDay(dayjs().startOf('day'))
|
|
||||||
setSelectedMonth(dayjs().startOf('month'));
|
|
||||||
}}
|
|
||||||
className="hidden border-y border-zinc-100 px-3.5 text-sm font-semibold text-zinc-800 hover:bg-gray-50 focus:relative md:block"
|
|
||||||
>
|
|
||||||
Today
|
|
||||||
</button>
|
|
||||||
<span className="relative -mx-px h-5 w-px bg-gray-300 md:hidden" />
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={() => { setSelectedMonth(selectedMonth.add(1, 'month')) }}
|
|
||||||
className="flex h-9 w-12 items-center justify-center rounded-r-md border-y border-r border-zinc-100 pl-1 text-gray-400 hover:text-gray-500 focus:relative md:w-9 md:pl-0 md:hover:bg-gray-50"
|
|
||||||
>
|
|
||||||
<span className="sr-only">Next month</span>
|
|
||||||
<ChevronRightIcon className="h-5 w-5" aria-hidden="true" />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</header>
|
|
||||||
<div className="shadow ring-1 ring-black ring-opacity-5 lg:flex lg:flex-auto lg:flex-col">
|
|
||||||
<div className="grid grid-cols-7 gap-px border-b border-zinc-100 bg-gray-200 text-center text-xs font-semibold leading-6 text-gray-700 lg:flex-none">
|
|
||||||
{[['S', 'un'], ['M', 'on'], ['T', 'ue'], ['W', 'ed'], ['T', 'hu'], ['F', 'ri'], ['S', 'at']].map((d) => (
|
|
||||||
<div key={d[0] + d[1]} className="bg-white py-2">
|
|
||||||
{d[0]}<span className="sr-only sm:not-sr-only">{d[1]}</span>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
<div className="flex bg-gray-200 text-xs leading-6 text-gray-700 lg:flex-auto">
|
|
||||||
<div className="isolate grid w-full grid-cols-7 gap-px">
|
|
||||||
{days.map((day) => {
|
|
||||||
const events = getEvents(calendar, day.date);
|
|
||||||
const isSelected = isSelectedDay(day);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<button
|
|
||||||
key={day.date.format('YYYY-MM-DD')}
|
|
||||||
type="button"
|
|
||||||
onClick={() => { setSelectedDay(day.date) }}
|
|
||||||
className={classNames(
|
|
||||||
day.isCurrentMonth ? 'bg-white' : 'bg-gray-50',
|
|
||||||
isSelected || day.isToday ? 'font-semibold' : '',
|
|
||||||
isSelected ? 'text-white' : '',
|
|
||||||
!isSelected && day.isToday ? 'text-indigo-600' : '',
|
|
||||||
!isSelected && day.isCurrentMonth && !day.isToday ? 'text-gray-900' : '',
|
|
||||||
!isSelected && !day.isCurrentMonth && !day.isToday ? 'text-gray-500' : '',
|
|
||||||
'flex h-12 sm:h-14 md:h-16 lg:h-24 flex-col px-3 py-2 hover:bg-gray-100 focus:z-10'
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<time
|
|
||||||
dateTime={day.date.format('YYYY-MM-DD')}
|
|
||||||
className={classNames(
|
|
||||||
isSelected ? 'flex h-6 w-6 items-center justify-center rounded-full' : '',
|
|
||||||
isSelected && day.isToday ? 'bg-indigo-600' : '',
|
|
||||||
isSelected && !day.isToday ? 'bg-gray-900' : '',
|
|
||||||
'ml-auto'
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
{day.date.date()}
|
|
||||||
</time>
|
|
||||||
<span className="sr-only">{events.length} events</span>
|
|
||||||
{events.length > 0 && (
|
|
||||||
<>
|
|
||||||
<span className="-mx-0.5 mt-auto flex flex-wrap-reverse lg:hidden">
|
|
||||||
{events.map((event) => (
|
|
||||||
<span key={event.id} className="mx-0.5 mb-1 h-1.5 w-1.5 rounded-full bg-gray-400" />
|
|
||||||
))}
|
|
||||||
</span>
|
|
||||||
<ol className="mt-2 hidden lg:block">
|
|
||||||
{events.slice(0, 2).map((event) => (
|
|
||||||
<li key={event.id}>
|
|
||||||
<p className="flex-auto truncate font-medium text-gray-900 group-hover:text-indigo-600">
|
|
||||||
{event.name}
|
|
||||||
</p>
|
|
||||||
</li>
|
|
||||||
))}
|
|
||||||
{events.length > 2 && <li className="text-gray-500">+ {events.length - 2} more</li>}
|
|
||||||
</ol>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</button>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="pt-10">
|
|
||||||
<h2 className="text-base font-semibold leading-6 text-zinc-800">
|
|
||||||
Events on <time dateTime={selectedDay.format('YYYY-MM-DD')}>{selectedDay.format('MMMM D')}</time>
|
|
||||||
</h2>
|
|
||||||
{selectedDayEvents.length > 0 && (
|
|
||||||
<div className="py-5">
|
|
||||||
<ol className="divide-y divide-gray-100 overflow-hidden rounded-lg bg-white text-sm shadow ring-1 ring-black ring-opacity-5">
|
|
||||||
{selectedDayEvents.map((event) => {
|
|
||||||
const start = dayjs(event.start);
|
|
||||||
return (
|
|
||||||
<li key={event.id} className="group flex p-4 pr-6 focus-within:bg-gray-50 hover:bg-gray-50">
|
|
||||||
<div className="flex-auto">
|
|
||||||
<p className="font-semibold text-gray-900">{event.name}</p>
|
|
||||||
<time dateTime={start.format('YYYY-MM-DD HH:mm')} className="mt-2 flex items-center text-gray-700">
|
|
||||||
<ClockIcon className="mr-2 h-5 w-5 text-gray-400" aria-hidden="true" />
|
|
||||||
{event.allDay
|
|
||||||
? "All day"
|
|
||||||
: start.format("h:mm a")
|
|
||||||
}
|
|
||||||
</time>
|
|
||||||
</div>
|
|
||||||
</li>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</ol>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
{selectedDayEvents.length === 0 && (
|
|
||||||
<p className="py-6 text-sm font-semibold text-gray-600">No events on this day</p>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<p>
|
|
||||||
If you are interested in reserving the hall, please see the
|
|
||||||
<Link href="/rental" className="pl-1 text-blue-600 hover:underline"
|
|
||||||
>hall rental page
|
|
||||||
</Link>.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
@ -1,132 +0,0 @@
|
|||||||
import * as ical from "ical";
|
|
||||||
import dayjs from "dayjs";
|
|
||||||
import React from 'react';
|
|
||||||
import { CalendarComponent } from "./calendar";
|
|
||||||
import { Container } from "@/components/Container";
|
|
||||||
import { Metadata } from "next";
|
|
||||||
|
|
||||||
const icalAddr = process.env.CALENDAR_ADDR || '';
|
|
||||||
|
|
||||||
export type Calendar = Record<number, MonthEvents>;
|
|
||||||
type MonthEvents = Record<number, DayEvents>;
|
|
||||||
type DayEvents = Record<number, Event[]>;
|
|
||||||
|
|
||||||
let calendar: Calendar;
|
|
||||||
let upcomingEvents: Event[] = [];
|
|
||||||
let calendarLastRefresh: dayjs.Dayjs;
|
|
||||||
|
|
||||||
export async function getCalendar(): Promise<Calendar> {
|
|
||||||
const now = dayjs();
|
|
||||||
if (!calendar || !calendarLastRefresh || now.diff(calendarLastRefresh, 'hours') >= 1) {
|
|
||||||
await loadCalendar();
|
|
||||||
calendarLastRefresh = now;
|
|
||||||
}
|
|
||||||
|
|
||||||
return calendar;
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function getUpcomingEvents(): Promise<Event[]> {
|
|
||||||
await getCalendar();
|
|
||||||
return upcomingEvents;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Event must be a "plain object" so that it can be passed to the Calendar client component. */
|
|
||||||
export interface Event {
|
|
||||||
id: string;
|
|
||||||
name: string;
|
|
||||||
allDay: boolean;
|
|
||||||
start: Date;
|
|
||||||
end: Date;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function loadCalendar(): Promise<void> {
|
|
||||||
if (icalAddr === '') {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log("Refreshing iCal from Google Calendar")
|
|
||||||
|
|
||||||
// For some reason Google Calendar is sending different content based on who is requesting it.
|
|
||||||
const headers = new Headers();
|
|
||||||
headers.set('User-Agent', 'curl/7.54.1');
|
|
||||||
|
|
||||||
const icalContents = await (await fetch(icalAddr, { headers })).text();
|
|
||||||
const events = ical.parseICS(icalContents);
|
|
||||||
const thisMonth = dayjs().startOf('month');
|
|
||||||
const yesterday = dayjs().startOf('day').subtract(1, 'day');
|
|
||||||
const upcoming: Event[] = []
|
|
||||||
|
|
||||||
const cal: Calendar = {};
|
|
||||||
for (const id in events) {
|
|
||||||
if (!events.hasOwnProperty(id)) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
const event = events[id];
|
|
||||||
const start = dayjs(event.start);
|
|
||||||
const end = dayjs(event.end);
|
|
||||||
const allDay = start.hour() === 0 && start.minute() === 0 && end.diff(start, 'hours') === 24;
|
|
||||||
const privateEvent = event.description?.toLowerCase().includes("private");
|
|
||||||
|
|
||||||
const converted = {
|
|
||||||
id: event.uid || "",
|
|
||||||
name: privateEvent ? "Private event" : (event.summary || ""),
|
|
||||||
allDay: allDay,
|
|
||||||
start: start.toDate(),
|
|
||||||
end: end.toDate(),
|
|
||||||
}
|
|
||||||
|
|
||||||
// Don't spend any more time on events that aren't going to be displayed.
|
|
||||||
if (start.isBefore(thisMonth)) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (upcoming.length < 4 && start.isAfter(yesterday) && !privateEvent) {
|
|
||||||
upcoming.push(converted);
|
|
||||||
}
|
|
||||||
|
|
||||||
const year = start.year();
|
|
||||||
const month = start.month();
|
|
||||||
const day = start.date();
|
|
||||||
|
|
||||||
if (!(year in cal)) {
|
|
||||||
cal[year] = {};
|
|
||||||
}
|
|
||||||
const yearMap = cal[year];
|
|
||||||
|
|
||||||
if (!(month in yearMap)) {
|
|
||||||
yearMap[month] = {};
|
|
||||||
}
|
|
||||||
const monthMap = yearMap[month];
|
|
||||||
|
|
||||||
if (day in monthMap) {
|
|
||||||
monthMap[day].push(converted);
|
|
||||||
} else {
|
|
||||||
monthMap[day] = [converted];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
calendar = cal;
|
|
||||||
upcomingEvents = upcoming;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const metadata: Metadata = {
|
|
||||||
title: 'Events Calendar',
|
|
||||||
description:
|
|
||||||
'Upcoming events and rentals ast the West Sound Hall.',
|
|
||||||
}
|
|
||||||
|
|
||||||
export default async function Calendar() {
|
|
||||||
const calendar = await getCalendar();
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Container className="mt-16 sm:mt-32">
|
|
||||||
<header className="max-w-2xl">
|
|
||||||
<h1 className="text-4xl font-bold tracking-tight text-zinc-800 sm:text-5xl">
|
|
||||||
West Sound Hall Calendar
|
|
||||||
</h1>
|
|
||||||
</header>
|
|
||||||
<CalendarComponent calendar={calendar} />
|
|
||||||
</Container>
|
|
||||||
);
|
|
||||||
}
|
|
@ -1,37 +0,0 @@
|
|||||||
"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,12 +1,13 @@
|
|||||||
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 { Container } from '@/components/Container';
|
import { type Metadata } from 'next'
|
||||||
import { EnvelopeIcon, UserGroupIcon } from '@heroicons/react/24/solid';
|
import Image from 'next/image'
|
||||||
import interiorEmptyImage from '@/images/photos/interior-empty.jpg';
|
import Link from 'next/link'
|
||||||
|
import clsx from 'clsx'
|
||||||
|
|
||||||
|
import { Container } from '@/components/Container'
|
||||||
|
import { UserPlusIcon, GiftIcon, EnvelopeIcon } from '@heroicons/react/24/solid'
|
||||||
|
import interiorEmptyImage from '@/images/photos/interior-empty.jpg'
|
||||||
|
import { ClubPayment } from './payment';
|
||||||
|
|
||||||
function SocialLink({
|
function SocialLink({
|
||||||
className,
|
className,
|
||||||
@ -14,47 +15,34 @@ 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')}>
|
||||||
<Link
|
<Link
|
||||||
href={href}
|
href={href}
|
||||||
className="group flex text-sm font-medium text-zinc-800 transition hover:text-teal-500"
|
className="group flex text-sm font-medium text-zinc-800 transition hover:text-teal-500 dark:text-zinc-200 dark:hover:text-teal-500"
|
||||||
>
|
>
|
||||||
<Icon className="h-6 w-6 flex-none fill-zinc-500 transition group-hover:fill-teal-500" />
|
<Icon className="h-6 w-6 flex-none fill-zinc-500 transition group-hover:fill-teal-500" />
|
||||||
<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: 'West Sound Community Club',
|
||||||
description: 'The West Sound Community Club on Orcas Island.',
|
description:
|
||||||
};
|
'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`
|
|
||||||
const ClubPayment = dynamic(
|
|
||||||
() => {
|
|
||||||
return import('./payment');
|
|
||||||
},
|
|
||||||
{ ssr: false },
|
|
||||||
);
|
|
||||||
|
|
||||||
const MailingListSignupForm = dynamic(
|
|
||||||
() => {
|
|
||||||
return import('./mailinglist');
|
|
||||||
},
|
|
||||||
{ ssr: false },
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Container className="mt-16 sm:mt-32">
|
<Container className="mt-16 sm:mt-32">
|
||||||
<div className="grid grid-cols-1 gap-y-16 lg:grid-cols-2 lg:grid-rows-[auto_1fr] lg:gap-y-12">
|
<div className="grid grid-cols-1 gap-y-16 lg:grid-cols-2 lg:grid-rows-[auto_1fr] lg:gap-y-12">
|
||||||
@ -64,63 +52,61 @@ export default function Club() {
|
|||||||
src={interiorEmptyImage}
|
src={interiorEmptyImage}
|
||||||
alt=""
|
alt=""
|
||||||
sizes="(min-width: 1024px) 32rem, 20rem"
|
sizes="(min-width: 1024px) 32rem, 20rem"
|
||||||
className="aspect-square rounded-2xl bg-zinc-100 object-cover"
|
className="aspect-square rounded-2xl bg-zinc-100 object-cover dark:bg-zinc-800"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="lg:order-first lg:row-span-2">
|
<div className="lg:order-first lg:row-span-2">
|
||||||
<h1 className="text-4xl font-bold tracking-tight text-zinc-800 sm:text-5xl">
|
<h1 className="text-4xl font-bold tracking-tight text-zinc-800 dark:text-zinc-100 sm:text-5xl">
|
||||||
West Sound Community Club
|
West Sound Community 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 dark:text-zinc-400">
|
||||||
<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
|
||||||
Together we steward the West Sound Hall, and gather monthly for
|
living in the West Sound area. Together we steward the West Sound
|
||||||
potlucks and other community events.
|
Hall, and gather monthly for 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 125, the hall has provided continuity
|
community center. For over one hundred years, the hall has
|
||||||
for the residents and guests of the West Sound community. The
|
provided continuity for the residents and guests of the West Sound
|
||||||
current residents continue the commitment to maintain this unique
|
community. The current residents continue the commitment to
|
||||||
gathering place out of care and respect for past and future
|
maintain this unique gathering place out of care and respect for
|
||||||
generations.
|
past and future generations.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<p>
|
<p>
|
||||||
The West Sound Community Club is a nonprofit, tax-exempt
|
The West Sound Community Club is a nonprofit, tax-exempt corporation
|
||||||
corporation under Section 501(c)3 of the Internal Revenue Code.
|
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">
|
||||||
<MailingListSignupForm />
|
<SocialLink href="/WSCC-Membership-Form.pdf" icon={UserPlusIcon}>
|
||||||
|
Membership Form
|
||||||
|
</SocialLink>
|
||||||
|
<SocialLink href="#" icon={GiftIcon} className="mt-4">
|
||||||
|
Donations (Coming soon)
|
||||||
|
</SocialLink>
|
||||||
<SocialLink
|
<SocialLink
|
||||||
href="mailto:contact@westsoundhall.org"
|
href="mailto:contact@westsoundhall.org"
|
||||||
icon={EnvelopeIcon}
|
icon={EnvelopeIcon}
|
||||||
className="mt-8 border-t border-zinc-100 pt-8"
|
className="mt-8 border-t border-zinc-100 pt-8 dark:border-zinc-700/40"
|
||||||
>
|
>
|
||||||
contact@westsoundhall.org
|
contact@westsoundhall.org
|
||||||
</SocialLink>
|
</SocialLink>
|
||||||
<SocialLink
|
|
||||||
href="/board-of-directors"
|
|
||||||
icon={UserGroupIcon}
|
|
||||||
className="mt-4"
|
|
||||||
>
|
|
||||||
Board of Directors
|
|
||||||
</SocialLink>
|
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<h2 className="mb-4 text-2xl font-semibold leading-6 text-gray-900">
|
<h2 className="text-2xl mb-4 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,19 +1,14 @@
|
|||||||
'use client';
|
"use client"
|
||||||
|
|
||||||
import React, { useEffect, useState, FormEvent, useCallback } from 'react';
|
import React, { useEffect, useState, FormEvent } from 'react';
|
||||||
import { AddressElement, Elements } from '@stripe/react-stripe-js';
|
import { AddressElement, Elements } from '@stripe/react-stripe-js';
|
||||||
import {
|
import { Appearance, StripeAddressElementOptions, StripeElementsOptions, loadStripe } from '@stripe/stripe-js';
|
||||||
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 || '');
|
||||||
|
|
||||||
@ -25,15 +20,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 = [
|
||||||
@ -48,40 +43,20 @@ function classNames(...classes: string[]) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function Spinner(): React.JSX.Element {
|
function Spinner(): React.JSX.Element {
|
||||||
return (
|
return (<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
|
||||||
<svg
|
<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">
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
<animateTransform attributeName="transform" dur="1s" repeatCount="indefinite" type="rotate" values="0 12 12;360 12 12" />
|
||||||
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>
|
</path>
|
||||||
</svg>
|
</svg>);
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function CheckoutForm({
|
function CheckoutForm({
|
||||||
paymentIntentID,
|
paymentIntentID
|
||||||
}: {
|
}: {
|
||||||
paymentIntentID: string;
|
paymentIntentID: string
|
||||||
}): React.JSX.Element {
|
}): React.JSX.Element {
|
||||||
const [selectedMembershipLevel, setSelectedMembershipLevel] = useState(
|
const [selectedMembershipLevel, setSelectedMembershipLevel] = useState(membershipLevels[0]);
|
||||||
membershipLevels[0],
|
const [selectedAdditionalDonation, setSelectedAdditionalDonation] = useState<number | null>(null);
|
||||||
);
|
|
||||||
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);
|
||||||
@ -97,7 +72,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) {
|
||||||
@ -123,19 +98,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);
|
||||||
|
|
||||||
@ -146,17 +121,11 @@ 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();
|
||||||
@ -193,17 +162,10 @@ function CheckoutForm({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<form
|
<form id="payment-form" onSubmit={handleSubmit} className="m-auto space-y-4">
|
||||||
id="payment-form"
|
|
||||||
onSubmit={handleSubmit}
|
|
||||||
className="m-auto space-y-4"
|
|
||||||
>
|
|
||||||
{/* Membership Type */}
|
{/* Membership Type */}
|
||||||
<RadioGroup
|
<RadioGroup value={selectedMembershipLevel} onChange={setSelectedMembershipLevel} className="space-y-3">
|
||||||
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>
|
||||||
@ -215,10 +177,8 @@ function CheckoutForm({
|
|||||||
value={membership}
|
value={membership}
|
||||||
className={({ active }) =>
|
className={({ active }) =>
|
||||||
classNames(
|
classNames(
|
||||||
active
|
active ? 'border-indigo-600 ring-2 ring-indigo-600' : 'border-gray-300',
|
||||||
? 'border-indigo-600 ring-2 ring-indigo-600'
|
'relative flex cursor-pointer rounded-lg border bg-white p-4 shadow-sm focus:outline-none'
|
||||||
: 'border-gray-200',
|
|
||||||
'relative flex cursor-pointer rounded-lg border bg-white p-4 shadow-sm hover:bg-gray-50 focus:outline-none',
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
@ -226,26 +186,15 @@ 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
|
<RadioGroup.Label as="span" className="block text-sm font-medium text-gray-900">
|
||||||
as="span"
|
|
||||||
className="block text-sm font-medium text-gray-900"
|
|
||||||
>
|
|
||||||
{membership.title}
|
{membership.title}
|
||||||
</RadioGroup.Label>
|
</RadioGroup.Label>
|
||||||
<RadioGroup.Description
|
<RadioGroup.Description as="span" className="mt-1 flex items-center text-sm text-gray-500">
|
||||||
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
|
<RadioGroup.Description as="span" className="ml-8 text-sm font-medium">
|
||||||
as="span"
|
<span className="text-gray-900">${membership.price}</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>
|
||||||
@ -253,7 +202,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"
|
||||||
/>
|
/>
|
||||||
@ -265,11 +214,7 @@ function CheckoutForm({
|
|||||||
</RadioGroup>
|
</RadioGroup>
|
||||||
|
|
||||||
{/* Additional donation */}
|
{/* Additional donation */}
|
||||||
<RadioGroup
|
<RadioGroup value={selectedAdditionalDonation} onChange={setSelectedAdditionalDonation} className="space-y-3">
|
||||||
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>
|
||||||
@ -283,17 +228,16 @@ function CheckoutForm({
|
|||||||
'cursor-pointer focus:outline-none',
|
'cursor-pointer focus:outline-none',
|
||||||
option === -1 ? 'col-span-2' : '',
|
option === -1 ? 'col-span-2' : '',
|
||||||
checked
|
checked
|
||||||
? 'ring-2 ring-indigo-600'
|
? 'ring-2 ring-indigo-600 ring-offset-2'
|
||||||
: 'text-gray-900 ring-1 ring-inset ring-gray-200 hover:bg-gray-50',
|
: 'ring-1 ring-inset ring-gray-300 bg-white text-gray-900 hover:bg-gray-50',
|
||||||
'flex items-center justify-center rounded-md bg-white px-3 py-3 text-sm font-semibold sm:flex-1',
|
'flex items-center justify-center rounded-md py-3 px-3 text-sm font-semibold sm:flex-1'
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
{option === -1 ? (
|
|
||||||
<RadioGroup.Label
|
{option === -1
|
||||||
as="span"
|
? (
|
||||||
className="flex flex-col items-center"
|
<RadioGroup.Label as="span" className="flex flex-col items-center">
|
||||||
>
|
|
||||||
<div className="relative rounded-md shadow-sm">
|
<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="pointer-events-none absolute inset-y-0 left-0 flex items-center pl-3">
|
||||||
<span className="text-gray-500 sm:text-sm">$</span>
|
<span className="text-gray-500 sm:text-sm">$</span>
|
||||||
@ -306,21 +250,19 @@ function CheckoutForm({
|
|||||||
min="0"
|
min="0"
|
||||||
step="1"
|
step="1"
|
||||||
onChange={(e) => setCustomAmount(e.target.value)}
|
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"
|
className="block w-full rounded-md border-0 py-1.5 pl-7 pr-12 text-gray-900 ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 focus:ring-1 focus:ring-inset focus:ring-gray-500 sm:text-sm sm:leading-6"
|
||||||
placeholder="Custom"
|
placeholder="Custom"
|
||||||
aria-describedby="price-currency"
|
aria-describedby="price-currency"
|
||||||
/>
|
/>
|
||||||
<div className="pointer-events-none absolute inset-y-0 right-0 flex items-center pr-3">
|
<div className="pointer-events-none absolute inset-y-0 right-0 flex items-center pr-3">
|
||||||
<span
|
<span className="text-gray-500 sm:text-sm" id="price-currency">
|
||||||
className="text-gray-500 sm:text-sm"
|
|
||||||
id="price-currency"
|
|
||||||
>
|
|
||||||
/yr
|
/yr
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</RadioGroup.Label>
|
</RadioGroup.Label>
|
||||||
) : (
|
)
|
||||||
|
: (
|
||||||
<RadioGroup.Label as="span">
|
<RadioGroup.Label as="span">
|
||||||
<span>${option}</span>
|
<span>${option}</span>
|
||||||
<span className="text-gray-500">/yr</span>
|
<span className="text-gray-500">/yr</span>
|
||||||
@ -336,11 +278,8 @@ function CheckoutForm({
|
|||||||
About you
|
About you
|
||||||
</h2>
|
</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">
|
<div className="rounded-md mb-3 px-3 pb-1.5 pt-2.5 shadow-sm ring-1 ring-inset ring-gray-300 focus-within:ring-2 focus-within:ring-indigo-600">
|
||||||
<label
|
<label htmlFor="name" className="block text-xs font-medium text-gray-900">
|
||||||
htmlFor="email"
|
|
||||||
className="block text-xs font-medium text-gray-900"
|
|
||||||
>
|
|
||||||
Email
|
Email
|
||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
@ -362,15 +301,9 @@ 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{' '}
|
to pay by cash or check, please instead <a className="underline
|
||||||
<a
|
mx-1" href="/WSCC-Membership-Form.pdf">fill out a paper form</a> and
|
||||||
className="mx-1
|
mail to the address on the form.
|
||||||
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>
|
||||||
@ -378,34 +311,27 @@ function CheckoutForm({
|
|||||||
{/* TODO: Automatically renew toggle? */}
|
{/* TODO: Automatically renew toggle? */}
|
||||||
|
|
||||||
<button
|
<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"
|
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"
|
||||||
disabled={isLoading || !stripe || !elements}
|
disabled={isLoading || !stripe || !elements}
|
||||||
id="submit"
|
id="submit"
|
||||||
>
|
>
|
||||||
{isLoading ? <Spinner /> : 'Pay $' + totalAmount}
|
{isLoading ? (
|
||||||
|
<Spinner />
|
||||||
|
) : (
|
||||||
|
'Pay $' + totalAmount
|
||||||
|
)}
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
{/* Show any error or success messages */}
|
{/* Show any error or success messages */}
|
||||||
{message && (
|
{message && <div className="text-red-500" id="payment-message">{message}</div>}
|
||||||
<div className="text-red-500" id="payment-message">
|
|
||||||
{message}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</form>
|
</form>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const DEFAULT_OPTIONS = {
|
export function ClubPayment() {
|
||||||
config: { attributes: true, childList: true, subtree: true },
|
|
||||||
};
|
|
||||||
|
|
||||||
export default function ClubPayment() {
|
|
||||||
const [clientSecret, setClientSecret] = useState('');
|
const [clientSecret, setClientSecret] = useState('');
|
||||||
const [paymentIntent, setPaymentIntent] = useState('');
|
const [paymentIntent, setPaymentIntent] = useState('');
|
||||||
|
|
||||||
const htmlEl = document.getElementsByTagName('html')[0];
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// Create PaymentIntent as soon as the page loads using our local API
|
// Create PaymentIntent as soon as the page loads using our local API
|
||||||
fetch('api/stripe_intent', {
|
fetch('api/stripe_intent', {
|
||||||
@ -423,29 +349,23 @@ export default function ClubPayment() {
|
|||||||
});
|
});
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const styles = getComputedStyle(htmlEl);
|
|
||||||
|
|
||||||
const appearance: Appearance = {
|
const appearance: Appearance = {
|
||||||
theme: 'stripe',
|
theme: 'stripe',
|
||||||
variables: {
|
|
||||||
colorBackground: styles.getPropertyValue('--stripe-background'),
|
|
||||||
colorText: styles.getPropertyValue('--stripe-foreground'),
|
|
||||||
},
|
|
||||||
labels: 'floating',
|
labels: 'floating',
|
||||||
};
|
};
|
||||||
|
|
||||||
const options: StripeElementsOptions = {
|
const options: StripeElementsOptions = {
|
||||||
clientSecret,
|
clientSecret,
|
||||||
appearance,
|
appearance
|
||||||
};
|
}
|
||||||
|
return <>
|
||||||
return (
|
|
||||||
<>
|
|
||||||
{clientSecret && (
|
{clientSecret && (
|
||||||
<Elements options={options} stripe={stripe}>
|
<Elements options={options}
|
||||||
|
stripe={stripe}>
|
||||||
<CheckoutForm paymentIntentID={paymentIntent} />
|
<CheckoutForm paymentIntentID={paymentIntent} />
|
||||||
</Elements>
|
</Elements>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -1,46 +0,0 @@
|
|||||||
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>
|
|
||||||
);
|
|
||||||
}
|
|
@ -1,341 +0,0 @@
|
|||||||
'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>
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
@ -1,31 +0,0 @@
|
|||||||
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,22 +14,22 @@ 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')}>
|
||||||
<Link
|
<Link
|
||||||
href={href}
|
href={href}
|
||||||
className="group flex text-sm font-medium text-zinc-800 transition hover:text-teal-500"
|
className="group flex text-sm font-medium text-zinc-800 transition hover:text-teal-500 dark:text-zinc-200 dark:hover:text-teal-500"
|
||||||
>
|
>
|
||||||
<Icon className="h-6 w-6 flex-none fill-zinc-500 transition group-hover:fill-teal-500" />
|
<Icon className="h-6 w-6 flex-none fill-zinc-500 transition group-hover:fill-teal-500" />
|
||||||
<span className="ml-4">{children}</span>
|
<span className="ml-4">{children}</span>
|
||||||
</Link>
|
</Link>
|
||||||
</li>
|
</li>
|
||||||
);
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
function TimelineEntry({
|
function TimelineEntry({
|
||||||
@ -38,41 +38,51 @@ 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 ? <Card.Eyebrow decorate>{event}</Card.Eyebrow> : null}
|
{event
|
||||||
|
? <Card.Eyebrow decorate>{event}</Card.Eyebrow>
|
||||||
|
: null}
|
||||||
<Card.Description>{description}</Card.Description>
|
<Card.Description>{description}</Card.Description>
|
||||||
{cta ? <Card.Cta>{cta}</Card.Cta> : null}
|
{cta
|
||||||
|
? <Card.Cta>{cta}</Card.Cta>
|
||||||
|
: null}
|
||||||
{children}
|
{children}
|
||||||
</Card>
|
</Card>
|
||||||
);
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
function DateListItem({ year, value }: { year?: string; value: string }) {
|
function DateListItem({
|
||||||
|
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: 'History',
|
||||||
description: 'The history of the West Sound Community Hall on Orcas Island.',
|
description:
|
||||||
};
|
'The history of the West Sound Community Hall on Orcas Island.',
|
||||||
|
}
|
||||||
|
|
||||||
export default function About() {
|
export default function About() {
|
||||||
return (
|
return (
|
||||||
@ -84,29 +94,29 @@ export default function About() {
|
|||||||
src={originalDeedImage}
|
src={originalDeedImage}
|
||||||
alt=""
|
alt=""
|
||||||
sizes="(min-width: 1024px) 32rem, 20rem"
|
sizes="(min-width: 1024px) 32rem, 20rem"
|
||||||
className="rotate-3 rounded-2xl bg-zinc-100 object-cover"
|
className="rotate-3 rounded-2xl bg-zinc-100 object-cover dark:bg-zinc-800"
|
||||||
/>
|
/>
|
||||||
<p className="text-sm text-zinc-400">
|
<p className="text-sm text-zinc-400 dark:text-zinc-500">
|
||||||
Original property deed from Alexander Chalmers.
|
Original property deed from Alexander Chalmers.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="lg:order-first lg:row-span-2">
|
<div className="lg:order-first lg:row-span-2">
|
||||||
<h1 className="text-4xl font-bold tracking-tight text-zinc-800 sm:text-5xl">
|
<h1 className="text-4xl font-bold tracking-tight text-zinc-800 dark:text-zinc-100 sm:text-5xl">
|
||||||
Hall History
|
Hall History
|
||||||
</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 dark:text-zinc-400">
|
||||||
<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
|
of Orcas Island. Members of the nonprofit West Sound Community Club,
|
||||||
Club, as stewards of the Hall, strive to maintain the integrity of
|
as stewards of the Hall, strive to maintain the integrity of the
|
||||||
the Hall as a unique gathering place for future generations.
|
Hall as a unique gathering place for future generations.
|
||||||
</p>
|
</p>
|
||||||
<p>
|
<p>
|
||||||
In 1902 volunteers began building the Hall with materials supplied
|
In 1902 volunteers began building the Hall with materials supplied by George
|
||||||
by George Adkins. The building site was donated two years earlier
|
Adkins. The building site was donated two years earlier by Alexander Chalmers.
|
||||||
by Alexander Chalmers. The Hall was erected to serve as a central
|
The Hall was erected to serve as a central meeting place for residents of the
|
||||||
meeting place for residents of the West Sound area.
|
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
|
||||||
@ -120,7 +130,7 @@ export default function About() {
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="mt-8 space-y-8">
|
<div className="mt-8 space-y-8">
|
||||||
<h2 className="text-2xl font-bold tracking-tight text-zinc-800 sm:text-5xl">
|
<h2 className="text-2xl font-bold tracking-tight text-zinc-800 dark:text-zinc-100 sm:text-5xl">
|
||||||
Detailed History
|
Detailed History
|
||||||
</h2>
|
</h2>
|
||||||
|
|
||||||
@ -133,105 +143,120 @@ 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 dark:text-zinc-400">
|
||||||
<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
|
||||||
year="1912"
|
value='West Sound Literary Society'
|
||||||
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">
|
||||||
@ -246,5 +271,5 @@ export default function About() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Container>
|
</Container>
|
||||||
);
|
)
|
||||||
}
|
}
|
||||||
|
@ -27,7 +27,7 @@ export default function RootLayout({
|
|||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<html lang="en" className="h-full antialiased" suppressHydrationWarning>
|
<html lang="en" className="h-full antialiased" suppressHydrationWarning>
|
||||||
<body className="flex h-full bg-zinc-50">
|
<body className="flex h-full bg-zinc-50 dark:bg-black">
|
||||||
<Providers>
|
<Providers>
|
||||||
<div className="flex w-full">
|
<div className="flex w-full">
|
||||||
<Layout>{children}</Layout>
|
<Layout>{children}</Layout>
|
||||||
|
Before Width: | Height: | Size: 307 KiB |
Before Width: | Height: | Size: 474 KiB |
Before Width: | Height: | Size: 413 KiB |
@ -1,58 +0,0 @@
|
|||||||
import { ArticleLayout } from '@/components/ArticleLayout'
|
|
||||||
import {MdxImage as Image } from '@/components/MdxImage'
|
|
||||||
import boddingtonStore from './Boddingtons_Store_West_Sound.jpg'
|
|
||||||
import westSoundMainStreet from './West_Sound_Main_Street.jpg'
|
|
||||||
import westSound from './West_Sound.jpg'
|
|
||||||
|
|
||||||
export const article = {
|
|
||||||
author: 'Tony Grosinger',
|
|
||||||
date: '2024-02-17',
|
|
||||||
title: 'February 2024 Potluck',
|
|
||||||
description: "The second potluck of the year, with a slideshow of Orcas history from John Wachter.",
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
export const metadata = {
|
|
||||||
title: article.title,
|
|
||||||
description: article.description,
|
|
||||||
}
|
|
||||||
|
|
||||||
export default (props) => <ArticleLayout article={article} {...props} />
|
|
||||||
|
|
||||||
This month after the potluck dinner, the community club was treated to a
|
|
||||||
slideshow presentation from John Wachter. He showed us almost 100 photos from
|
|
||||||
all over Orcas Island, many of which dated back to the early 1900's.
|
|
||||||
|
|
||||||
<div className="not-prose flex flex-col items-center">
|
|
||||||
<Image src={boddingtonStore} alt="Boddington's Store at the end of Crow Valley Road" />
|
|
||||||
<span className="mt-2 text-sm text-zinc-400">
|
|
||||||
Boddington's Store at the end of Crow Valley Road.
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
Many of these images were available to us thanks to the [Orcas Island Historical
|
|
||||||
Society](https://www.orcasmuseums.org/), and several of the photos were also
|
|
||||||
passed down by John's family.
|
|
||||||
|
|
||||||
<div className="not-prose flex flex-col items-center">
|
|
||||||
<Image src={westSoundMainStreet} alt="West Sound Main Street" />
|
|
||||||
<span className="mt-2 text-sm text-zinc-400">
|
|
||||||
West Sound Main Street
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
A recurring observation was both how many more buildings there were in West
|
|
||||||
Sound, and how few trees there were on the island!
|
|
||||||
|
|
||||||
<div className="not-prose flex flex-col items-center">
|
|
||||||
<Image src={westSound} alt="West Sound and the south end of Turtleback" />
|
|
||||||
<span className="mt-2 text-sm text-zinc-400">
|
|
||||||
West Sound and the south end of Turtleback
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
If you have ideas for presentations or activities at upcoming potlucks, please <a
|
|
||||||
target="_blank" href="mailto:board@westsoundhall.org">send us a message</a>.
|
|
||||||
|
|
||||||
Thank you John, the Historical Society, and Peter Fisher for preserving and
|
|
||||||
sharing this history.
|
|
@ -1,132 +0,0 @@
|
|||||||
import { ArticleLayout } from '@/components/ArticleLayout'
|
|
||||||
import {MdxImage as Image } from '@/components/MdxImage'
|
|
||||||
import townHallMeeting from './town-hall-meeting.jpg'
|
|
||||||
|
|
||||||
export const article = {
|
|
||||||
author: 'Tony Grosinger',
|
|
||||||
date: '2024-03-06',
|
|
||||||
title: 'March 2024 Town Hall',
|
|
||||||
description: "A community gathering to brainstorm ideas for the club's future.",
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
export const metadata = {
|
|
||||||
title: article.title,
|
|
||||||
description: article.description,
|
|
||||||
}
|
|
||||||
|
|
||||||
export default (props) => <ArticleLayout article={article} {...props} />
|
|
||||||
|
|
||||||
On March 6th 2024 approximately 25 members of the club gathered in the hall to
|
|
||||||
do some brainstorming. The goal was just to get ideas down on paper so that they
|
|
||||||
can be prioritized and planned in the future.
|
|
||||||
|
|
||||||
<div className="not-prose flex flex-col items-center">
|
|
||||||
<Image src={townHallMeeting} alt="Town Hall Meeting" />
|
|
||||||
<span className="mt-2 text-sm text-zinc-400">
|
|
||||||
Town Hall Meeting
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
The board of directors put together the list of categories and the community
|
|
||||||
filled in the all the ideas.
|
|
||||||
|
|
||||||
Thank you to Mark Gasser and Joe Mentor for facilitating.
|
|
||||||
|
|
||||||
- Activities
|
|
||||||
- Movie night
|
|
||||||
- Ping pong
|
|
||||||
- Use activities to attract people and build membership
|
|
||||||
- Piggy-back on the OIYC pancake breakfast in May
|
|
||||||
- Linkage with waterfront
|
|
||||||
- Lack of parking – have to pay OIYC $50
|
|
||||||
- Calendaring improvements – double booking with YC
|
|
||||||
- BBQ (community outreach)
|
|
||||||
- Olga days
|
|
||||||
- Garage sale / rummage sale (Fidelis used to do this)
|
|
||||||
- Deer Harbor had once mid-winter that was a huge success
|
|
||||||
- Dance
|
|
||||||
- Karaoke
|
|
||||||
- End of season tool cleaning and sharpening
|
|
||||||
- Jim Hamilton volunteered to bring his sharpener
|
|
||||||
- Sip and paint
|
|
||||||
- Guest lectures
|
|
||||||
- Car & boat show
|
|
||||||
- Cooking
|
|
||||||
- Joint event with DHCC
|
|
||||||
- Approved for-profit events.
|
|
||||||
- We are in a smaller county so there are some exceptions.
|
|
||||||
- Art classes, music classes, dance classes
|
|
||||||
- A Saturday market might be allowed too.
|
|
||||||
- Petitioning the county can be a strategy for adding more allowed activities.
|
|
||||||
- Arthur mentioned that if you go through Parks and Recreation we might be able to do a dance.
|
|
||||||
- Nancy had more information from her time as VP.
|
|
||||||
- Building Improvements
|
|
||||||
- A deck on the east side of the building extending to ground level on the north end.
|
|
||||||
- Replace the kitchen.
|
|
||||||
- A paint job in the kitchen would also go a long way.
|
|
||||||
- Three compartment sink. Commercial dishwasher.
|
|
||||||
- Joint project with the OIYC
|
|
||||||
- Infrared heading panels or heat pump
|
|
||||||
- Floor
|
|
||||||
- Engineering study (or just an inspection)
|
|
||||||
- Turn on the heat from your phone.
|
|
||||||
- Replace the single pane windows.
|
|
||||||
- Alternatively, adding shutters could protect the existing windows and improve their insulation.
|
|
||||||
- Grant mentions that staying with in the purview of maintenance is easier to get through county approval rather than remodel.
|
|
||||||
- New storage closet on the north end of the building.
|
|
||||||
- Replace the hot water heater.
|
|
||||||
- Restroom improvements.
|
|
||||||
- Building Maintenance
|
|
||||||
- Replacing siding on the north end of the building.
|
|
||||||
- Address the leak between the kitchen and the north end of the building.
|
|
||||||
- Use the compressor the blow out the water lines on cold days.
|
|
||||||
- Funding
|
|
||||||
- Could the historical society help us with fundraising since we’re a historic building?
|
|
||||||
- Similarly, could the historical society host their meetings at our hall?
|
|
||||||
- Grants
|
|
||||||
- Bill Bangs
|
|
||||||
- Need a needs analysis first
|
|
||||||
- The county has a historic building preservation fund that can fund 50% of projects.
|
|
||||||
- Convert Sail Orcas into a separate renter rather than being under the YC umbrella.
|
|
||||||
- Advertise for rental of the hall. Chamber of commerce?
|
|
||||||
- OICF bi-annual funding campaigns
|
|
||||||
- Raise member dues
|
|
||||||
- Sliding scale
|
|
||||||
- Introductory rate, or the opposite - grandfather existing members at lower
|
|
||||||
- Different funds that members can donate to
|
|
||||||
- Membership capital campaign – direct asking
|
|
||||||
- Increase membership
|
|
||||||
- Make household double individual rate
|
|
||||||
- Sell bricks
|
|
||||||
- Governance
|
|
||||||
- Build out more committees
|
|
||||||
- Architectural
|
|
||||||
- Colin Baden
|
|
||||||
- Arthur
|
|
||||||
- Membership / Welcoming
|
|
||||||
- Encourage external groups to be committees, for example historical society or fidelis
|
|
||||||
- Invite DHCC to one of our meetings
|
|
||||||
- Review bylaws and articles of incorporation.
|
|
||||||
- Membership
|
|
||||||
- Solicit the OIYC to become WSCC members (add-on to your OIYC membership?)
|
|
||||||
- We should make it more clear that membership is open to more than WS residents.
|
|
||||||
- Must attract younger people to become members.
|
|
||||||
- DHCC has had success with this. Talk to them about what they did.
|
|
||||||
- Work with realtors to have them introduce new people to the club
|
|
||||||
- Arthur asks to have a physical invitation he can hand out to people he meets.
|
|
||||||
- OIYC Partnership
|
|
||||||
- Kitchen remodel
|
|
||||||
- Articulate shared goals and vision
|
|
||||||
- Ensure the OIYC liaison attends our meetings
|
|
||||||
- One set of common glassware, dishes, etc.
|
|
||||||
- Use/Expectations
|
|
||||||
- Update kitchen instructions (e.g. don’t rinse food down the drain).
|
|
||||||
- Update cleaning rules in hall rent agreement.
|
|
||||||
- Discourage or ban single use plastics during events.
|
|
||||||
- Vision/Goals/Mission/Objectives
|
|
||||||
- Keep the building standing
|
|
||||||
- Grow the organization
|
|
||||||
- Group activity - have members write down a list of what the hall means to them.
|
|
||||||
- Set by example what community can be.
|
|
||||||
|
|
Before Width: | Height: | Size: 1.5 MiB |
Before Width: | Height: | Size: 4.2 MiB |
@ -1,54 +0,0 @@
|
|||||||
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.
|
|
||||||
|
|
Before Width: | Height: | Size: 239 KiB |
Before Width: | Height: | Size: 245 KiB |
@ -1,44 +0,0 @@
|
|||||||
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!
|
|
||||||
|
|
@ -47,7 +47,7 @@ export default async function ArticlesIndex() {
|
|||||||
title="West Sound Hall News"
|
title="West Sound Hall News"
|
||||||
intro="History, Announcements, and more from the West Sound Hall and Community Club."
|
intro="History, Announcements, and more from the West Sound Hall and Community Club."
|
||||||
>
|
>
|
||||||
<div className="md:border-l md:border-zinc-100 md:pl-6">
|
<div className="md:border-l md:border-zinc-100 md:pl-6 md:dark:border-zinc-700/40">
|
||||||
<div className="flex max-w-3xl flex-col space-y-16">
|
<div className="flex max-w-3xl flex-col space-y-16">
|
||||||
{articles.map((article) => (
|
{articles.map((article) => (
|
||||||
<Article key={article.slug} article={article} />
|
<Article key={article.slug} article={article} />
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import { ArticleLayout } from '@/components/ArticleLayout'
|
import { ArticleLayout } from '@/components/ArticleLayout'
|
||||||
import {MdxImage as Image } from '@/components/MdxImage'
|
import Image from 'next/image'
|
||||||
import originalWebsite from './westsoundhall-original-website.png'
|
import originalWebsite from './westsoundhall-original-website.png'
|
||||||
|
|
||||||
export const article = {
|
export const article = {
|
||||||
@ -31,7 +31,7 @@ years.
|
|||||||
|
|
||||||
<div className="not-prose flex flex-col items-center">
|
<div className="not-prose flex flex-col items-center">
|
||||||
<Image src={originalWebsite} alt="Screenshot of the original West Sound Hall website" />
|
<Image src={originalWebsite} alt="Screenshot of the original West Sound Hall website" />
|
||||||
<span className="mt-2 text-sm text-zinc-400">
|
<span className="mt-2 text-sm text-zinc-400 dark:text-zinc-500">
|
||||||
Screenshot of the original West Sound Hall website.
|
Screenshot of the original West Sound Hall website.
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
@ -41,8 +41,8 @@ the hall available for rental by the community, and even added the hall to the
|
|||||||
WA Heritage Register. There's so much more to the hall than just potlucks,
|
WA Heritage Register. There's so much more to the hall than just potlucks,
|
||||||
and this website hopes to share this with the West Sound Community.
|
and this website hopes to share this with the West Sound Community.
|
||||||
|
|
||||||
Thanks for being a part of the West Sound Community!
|
If you'd like to follow along, please <a target="_blank"
|
||||||
|
href="https://orcas.community/mailinglists/7">sign up for email
|
||||||
|
announcements</a> when new blog posts are published.
|
||||||
|
|
||||||
For posterity, the source code for this website along with instructions for
|
Thanks for being a part of the West Sound Community!
|
||||||
developing and updating it can be found
|
|
||||||
[here](https://git.grosinger.net/tgrosinger/west-sound-hall).
|
|
||||||
|
@ -5,13 +5,13 @@ export default function NotFound() {
|
|||||||
return (
|
return (
|
||||||
<Container className="flex h-full items-center pt-16 sm:pt-32">
|
<Container className="flex h-full items-center pt-16 sm:pt-32">
|
||||||
<div className="flex flex-col items-center">
|
<div className="flex flex-col items-center">
|
||||||
<p className="text-base font-semibold text-zinc-400">
|
<p className="text-base font-semibold text-zinc-400 dark:text-zinc-500">
|
||||||
404
|
404
|
||||||
</p>
|
</p>
|
||||||
<h1 className="mt-4 text-4xl font-bold tracking-tight text-zinc-800 sm:text-5xl">
|
<h1 className="mt-4 text-4xl font-bold tracking-tight text-zinc-800 dark:text-zinc-100 sm:text-5xl">
|
||||||
Page not found
|
Page not found
|
||||||
</h1>
|
</h1>
|
||||||
<p className="mt-4 text-base text-zinc-600">
|
<p className="mt-4 text-base text-zinc-600 dark:text-zinc-400">
|
||||||
Sorry, we couldn’t find the page you’re looking for.
|
Sorry, we couldn’t find the page you’re looking for.
|
||||||
</p>
|
</p>
|
||||||
<Button href="/" variant="secondary" className="mt-4">
|
<Button href="/" variant="secondary" className="mt-4">
|
||||||
|
170
src/app/page.tsx
@ -1,107 +1,129 @@
|
|||||||
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 { promises as fs } from 'fs';
|
||||||
|
|
||||||
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 bg-sky-300 px-3 py-2 text-center font-semibold transition hover:bg-sky-400"
|
className="rounded-md px-3 py-2 font-semibold text-center transition dark:hover:text-teal-400 bg-sky-300 hover:bg-sky-400 dark:bg-zinc-700/40 dark:hover:bg-zinc-600/40"
|
||||||
>
|
>
|
||||||
{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}`}>{article.title}</Card.Title>
|
<Card.Title href={`/news/${article.slug}`}>
|
||||||
|
{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 }) {
|
interface Meeting {
|
||||||
const start = dayjs(event.start);
|
title: string
|
||||||
const end = dayjs(event.end);
|
date: string
|
||||||
|
startTime: string
|
||||||
const date = start.format('YYYY-MM-DD');
|
endTime?: string
|
||||||
|
notes?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
function MeetingListItem({ meeting }: { meeting: Meeting }) {
|
||||||
return (
|
return (
|
||||||
<li className="flex gap-4">
|
<li className="flex gap-4">
|
||||||
<dl className="flex flex-auto flex-wrap gap-x-2">
|
<dl className="flex flex-auto flex-wrap gap-x-2">
|
||||||
<dt className="sr-only">Title</dt>
|
<dt className="sr-only">Title</dt>
|
||||||
<dd className="w-full flex-none text-sm font-medium text-zinc-900">
|
<dd className="w-full flex-none text-sm font-medium text-zinc-900 dark:text-zinc-100">
|
||||||
{event.name}
|
{meeting.title}
|
||||||
</dd>
|
</dd>
|
||||||
<dt className="sr-only">Date</dt>
|
<dt className="sr-only">Date</dt>
|
||||||
<dd className="text-xs text-zinc-500">{date}</dd>
|
<dd className="text-xs text-zinc-500 dark:text-zinc-400">
|
||||||
|
{meeting.date}
|
||||||
|
</dd>
|
||||||
<dt className="sr-only">Time</dt>
|
<dt className="sr-only">Time</dt>
|
||||||
{event.allDay ? (
|
{meeting.endTime
|
||||||
<dd className="ml-auto text-xs text-zinc-400" aria-label="All day">
|
? <dd
|
||||||
All day
|
className="ml-auto text-xs text-zinc-400 dark:text-zinc-500"
|
||||||
</dd>
|
aria-label={`${meeting.startTime} until ${meeting.endTime}`}
|
||||||
) : event.end ? (
|
|
||||||
<dd
|
|
||||||
className="ml-auto text-xs text-zinc-400"
|
|
||||||
aria-label={`${start.format('YYYY-MM-DD HH:mm')} until ${end.format(
|
|
||||||
'YYYY-MM-DD HH-mm',
|
|
||||||
)}`}
|
|
||||||
>
|
>
|
||||||
<time dateTime={start.format('YYYY-MM-DD HH:mm')}>
|
<time dateTime={meeting.date + ' ' + meeting.startTime}>{meeting.startTime}</time>{' '}
|
||||||
{start.format('h:mm a')}
|
|
||||||
</time>{' '}
|
|
||||||
<span aria-hidden="true">—</span>{' '}
|
<span aria-hidden="true">—</span>{' '}
|
||||||
<time dateTime={end.format('YYYY-MM-DD HH-mm')}>
|
<time dateTime={meeting.date + ' ' + meeting.endTime}>{meeting.endTime}</time>{' '}
|
||||||
{end.format('h:mm a')}
|
|
||||||
</time>{' '}
|
|
||||||
</dd>
|
</dd>
|
||||||
) : (
|
: <dd
|
||||||
<dd
|
className="ml-auto text-xs text-zinc-400 dark:text-zinc-500"
|
||||||
className="ml-auto text-xs text-zinc-400"
|
aria-label={`${meeting.startTime}`}
|
||||||
aria-label={start.format('YYYY-MM-DD HH-mm')}
|
|
||||||
>
|
>
|
||||||
<time dateTime={start.format('YYYY-MM-DD HH-mm')}>
|
<time dateTime={meeting.date + ' ' + meeting.startTime}>{meeting.startTime}</time>{' '}
|
||||||
{start.format('h:mm a')}
|
|
||||||
</time>{' '}
|
|
||||||
</dd>
|
</dd>
|
||||||
)}
|
}
|
||||||
</dl>
|
</dl>
|
||||||
</li>
|
</li>
|
||||||
);
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
async function Events() {
|
async function Events() {
|
||||||
const events = await getUpcomingEvents();
|
const now = new Date();
|
||||||
|
const nowYear = now.getFullYear();
|
||||||
|
const nowMonth = now.getMonth() + 1;
|
||||||
|
const nowDay = now.getDate();
|
||||||
|
|
||||||
|
const file = await fs.readFile(process.cwd() + '/src/app/upcoming-events.json', 'utf8');
|
||||||
|
const allEvents: Array<Meeting> = JSON.parse(file);
|
||||||
|
|
||||||
|
// Remove any events in the past.
|
||||||
|
const events = allEvents.filter((e) => {
|
||||||
|
const [year, month, day] = e.date.split('-');
|
||||||
|
|
||||||
|
const parsedYear = parseInt(year)
|
||||||
|
if (parsedYear > nowYear) {
|
||||||
|
return true
|
||||||
|
} else if (parsedYear < nowYear) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
const parsedMonth = parseInt(month)
|
||||||
|
if (parsedMonth > nowMonth) {
|
||||||
|
return true
|
||||||
|
} else if (parsedMonth < nowMonth) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
const parsedDay = parseInt(day)
|
||||||
|
return parsedDay >= nowDay
|
||||||
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="rounded-2xl border border-zinc-100 p-6">
|
<div className="rounded-2xl border border-zinc-100 p-6 dark:border-zinc-700/40">
|
||||||
<h2 className="flex text-sm font-semibold text-zinc-900">
|
<h2 className="flex text-sm font-semibold text-zinc-900 dark:text-zinc-100">
|
||||||
<CalendarDaysIcon className="h-6 w-6 flex-none fill-zinc-100 stroke-zinc-400" />
|
<CalendarDaysIcon className="h-6 w-6 flex-none fill-zinc-100 stroke-zinc-400 dark:fill-zinc-100/10 dark:stroke-zinc-500" />
|
||||||
<span className="ml-3">Upcoming Events</span>
|
<span className="ml-3">Upcoming Events</span>
|
||||||
</h2>
|
</h2>
|
||||||
<ol className="mt-6 space-y-4">
|
<ol className="mt-6 space-y-4">
|
||||||
{events.map((event, idx) => (
|
{events.map((meeting, idx) => (
|
||||||
<EventListItem key={idx} event={event} />
|
<MeetingListItem key={idx} meeting={meeting} />
|
||||||
))}
|
))}
|
||||||
</ol>
|
</ol>
|
||||||
{/*
|
{/*
|
||||||
@ -110,44 +132,40 @@ 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 mt-24 max-w-7xl px-6 lg:px-8">
|
<div className="mx-auto max-w-7xl px-6 mt-24 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 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">
|
<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">
|
||||||
<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-gray-900 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 dark:text-zinc-400">
|
||||||
The West Sound Community Hall is located in the hamlet of West
|
The West Sound Community Hall is located in the hamlet of West Sound
|
||||||
Sound on Orcas Island, about 10 minutes from the ferry landing
|
on Orcas Island, about 10 minutes from the ferry landing and
|
||||||
and Eastsound. It has served as a public assembly hall since it
|
Eastsound. It has served as a public assembly hall since it was
|
||||||
was built by volunteers in 1902.
|
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 dark:text-zinc-400">
|
||||||
Facing West Sound, the Hall is at the heart of the West Sound
|
Facing West Sound, the Hall is at the heart of the West Sound community.
|
||||||
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="w-full max-w-xl rounded-2xl object-cover lg:row-span-2 lg:max-w-none"
|
className="lg:row-span-2 w-full max-w-xl rounded-2xl object-cover 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 dark:border-zinc-700/40">
|
||||||
<div className="flex flex-col gap-y-4">
|
<div className="flex flex-col gap-y-4">
|
||||||
<LinkButton href="/club">
|
<LinkButton href='/club'>Join or Renew your Membership</LinkButton>
|
||||||
Join or Renew your Membership
|
<LinkButton href='/rental'>Rent the Hall</LinkButton>
|
||||||
</LinkButton>
|
|
||||||
<LinkButton href="/donate">Donate</LinkButton>
|
|
||||||
<LinkButton href="/rental">Rent the Hall</LinkButton>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -166,5 +184,5 @@ export default async function Home() {
|
|||||||
</div>
|
</div>
|
||||||
</Container>
|
</Container>
|
||||||
</>
|
</>
|
||||||
);
|
)
|
||||||
}
|
}
|
||||||
|
99
src/app/projects/page.tsx
Normal file
@ -0,0 +1,99 @@
|
|||||||
|
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 dark:border dark:border-zinc-700/50 dark:bg-zinc-800 dark:ring-0">
|
||||||
|
<Image
|
||||||
|
src={project.logo}
|
||||||
|
alt=""
|
||||||
|
className="h-8 w-8"
|
||||||
|
unoptimized
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<h2 className="mt-6 text-base font-semibold text-zinc-800 dark:text-zinc-100">
|
||||||
|
<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 dark:text-zinc-200">
|
||||||
|
<LinkIcon className="h-6 w-6 flex-none" />
|
||||||
|
<span className="ml-2">{project.link.label}</span>
|
||||||
|
</p>
|
||||||
|
</Card>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</SimpleLayout>
|
||||||
|
)
|
||||||
|
}
|
@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
import { createContext, useEffect, useRef } from 'react'
|
import { createContext, useEffect, useRef } from 'react'
|
||||||
import { usePathname } from 'next/navigation'
|
import { usePathname } from 'next/navigation'
|
||||||
|
import { ThemeProvider, useTheme } from 'next-themes'
|
||||||
|
|
||||||
function usePrevious<T>(value: T) {
|
function usePrevious<T>(value: T) {
|
||||||
let ref = useRef<T>()
|
let ref = useRef<T>()
|
||||||
@ -13,6 +14,30 @@ function usePrevious<T>(value: T) {
|
|||||||
return ref.current
|
return ref.current
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function ThemeWatcher() {
|
||||||
|
let { resolvedTheme, setTheme } = useTheme()
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
let media = window.matchMedia('(prefers-color-scheme: dark)')
|
||||||
|
|
||||||
|
function onMediaChange() {
|
||||||
|
let systemTheme = media.matches ? 'dark' : 'light'
|
||||||
|
if (resolvedTheme === systemTheme) {
|
||||||
|
setTheme('system')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onMediaChange()
|
||||||
|
media.addEventListener('change', onMediaChange)
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
media.removeEventListener('change', onMediaChange)
|
||||||
|
}
|
||||||
|
}, [resolvedTheme, setTheme])
|
||||||
|
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
export const AppContext = createContext<{ previousPathname?: string }>({})
|
export const AppContext = createContext<{ previousPathname?: string }>({})
|
||||||
|
|
||||||
export function Providers({ children }: { children: React.ReactNode }) {
|
export function Providers({ children }: { children: React.ReactNode }) {
|
||||||
@ -21,7 +46,10 @@ export function Providers({ children }: { children: React.ReactNode }) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<AppContext.Provider value={{ previousPathname }}>
|
<AppContext.Provider value={{ previousPathname }}>
|
||||||
|
<ThemeProvider attribute="class" disableTransitionOnChange>
|
||||||
|
<ThemeWatcher />
|
||||||
{children}
|
{children}
|
||||||
|
</ThemeProvider>
|
||||||
</AppContext.Provider>
|
</AppContext.Provider>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -6,8 +6,6 @@ import clsx from 'clsx'
|
|||||||
import { Container } from '@/components/Container'
|
import { Container } from '@/components/Container'
|
||||||
import exteriorFront from '@/images/photos/exterior-front.png'
|
import exteriorFront from '@/images/photos/exterior-front.png'
|
||||||
import { EnvelopeIcon, PencilSquareIcon, QuestionMarkCircleIcon } from '@heroicons/react/24/solid'
|
import { EnvelopeIcon, PencilSquareIcon, QuestionMarkCircleIcon } from '@heroicons/react/24/solid'
|
||||||
import React from 'react'
|
|
||||||
import { TableCell, TableHeading, TableLeftHeading } from '@/components/Table'
|
|
||||||
|
|
||||||
function SocialLink({
|
function SocialLink({
|
||||||
className,
|
className,
|
||||||
@ -24,7 +22,7 @@ function SocialLink({
|
|||||||
<li className={clsx(className, 'flex')}>
|
<li className={clsx(className, 'flex')}>
|
||||||
<Link
|
<Link
|
||||||
href={href}
|
href={href}
|
||||||
className="group flex text-sm font-medium text-zinc-800 transition hover:text-teal-500"
|
className="group flex text-sm font-medium text-zinc-800 transition hover:text-teal-500 dark:text-zinc-200 dark:hover:text-teal-500"
|
||||||
>
|
>
|
||||||
<Icon className="h-6 w-6 flex-none fill-zinc-500 transition group-hover:fill-teal-500" />
|
<Icon className="h-6 w-6 flex-none fill-zinc-500 transition group-hover:fill-teal-500" />
|
||||||
<span className="ml-4">{children}</span>
|
<span className="ml-4">{children}</span>
|
||||||
@ -34,7 +32,7 @@ function SocialLink({
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const metadata: Metadata = {
|
export const metadata: Metadata = {
|
||||||
title: 'Hall Rental',
|
title: 'West Sound Hall Rental',
|
||||||
description:
|
description:
|
||||||
'Rental information for the West Sound Hall.',
|
'Rental information for the West Sound Hall.',
|
||||||
}
|
}
|
||||||
@ -49,15 +47,15 @@ export default function Rental() {
|
|||||||
src={exteriorFront}
|
src={exteriorFront}
|
||||||
alt=""
|
alt=""
|
||||||
sizes="(min-width: 1024px) 32rem, 20rem"
|
sizes="(min-width: 1024px) 32rem, 20rem"
|
||||||
className="aspect-square rounded-2xl bg-zinc-100 object-cover"
|
className="aspect-square rounded-2xl bg-zinc-100 object-cover dark:bg-zinc-800"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="lg:order-first lg:row-span-2">
|
<div className="lg:order-first lg:row-span-2">
|
||||||
<h1 className="text-4xl font-bold tracking-tight text-zinc-800 sm:text-5xl">
|
<h1 className="text-4xl font-bold tracking-tight text-zinc-800 dark:text-zinc-100 sm:text-5xl">
|
||||||
West Sound Hall Rental
|
Hall Rental
|
||||||
</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 dark:text-zinc-400">
|
||||||
<p>
|
<p>
|
||||||
The West Sound Community Hall is a public assembly hall, which has
|
The West Sound Community Hall is a public assembly hall, which has
|
||||||
been in continuous operation since it was built in 1902. In 1999,
|
been in continuous operation since it was built in 1902. In 1999,
|
||||||
@ -80,12 +78,6 @@ export default function Rental() {
|
|||||||
damage to the Hall or cleaning is necessary, deductions will be
|
damage to the Hall or cleaning is necessary, deductions will be
|
||||||
made at the discretion of the Board.
|
made at the discretion of the Board.
|
||||||
</p>
|
</p>
|
||||||
<p>
|
|
||||||
Please check the
|
|
||||||
<Link href="/calendar" className="pl-1 text-blue-600 hover:underline"
|
|
||||||
>calendar
|
|
||||||
</Link> for availability.
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="order-last sm:order-none lg:pl-20">
|
<div className="order-last sm:order-none lg:pl-20">
|
||||||
@ -99,14 +91,14 @@ export default function Rental() {
|
|||||||
<SocialLink
|
<SocialLink
|
||||||
href="mailto:contact@westsoundhall.org"
|
href="mailto:contact@westsoundhall.org"
|
||||||
icon={EnvelopeIcon}
|
icon={EnvelopeIcon}
|
||||||
className="mt-8 border-t border-zinc-100 pt-8"
|
className="mt-8 border-t border-zinc-100 pt-8 dark:border-zinc-700/40"
|
||||||
>
|
>
|
||||||
contact@westsoundhall.org
|
contact@westsoundhall.org
|
||||||
</SocialLink>
|
</SocialLink>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
<div className="lg:col-span-2">
|
<div className="lg:col-span-2">
|
||||||
<h2 className="text-2xl font-bold tracking-tight text-zinc-800 sm:text-2xl">
|
<h2 className="text-2xl font-bold tracking-tight text-zinc-800 dark:text-zinc-100 sm:text-2xl">
|
||||||
Rental Rates
|
Rental Rates
|
||||||
</h2>
|
</h2>
|
||||||
|
|
||||||
@ -115,50 +107,50 @@ export default function Rental() {
|
|||||||
<table className="min-w-full divide-y divide-gray-300">
|
<table className="min-w-full divide-y divide-gray-300">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th scope="col" className="py-3.5 pl-4 pr-3 sm:pl-0"></th>
|
<th scope="col" className="py-3.5 pl-4 pr-3 text-left text-sm font-semibold text-gray-900 sm:pl-0"></th>
|
||||||
<TableHeading>Half day (4 hr)</TableHeading>
|
<th scope="col" className="px-3 py-3.5 text-left text-sm font-semibold text-gray-900">Half day (4 hr)</th>
|
||||||
<TableHeading>All day</TableHeading>
|
<th scope="col" className="px-3 py-3.5 text-left text-sm font-semibold text-gray-900">All day</th>
|
||||||
<TableHeading>Deposit</TableHeading>
|
<th scope="col" className="px-3 py-3.5 text-left text-sm font-semibold text-gray-900">Deposit</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody className="divide-y divide-gray-200 ">
|
<tbody className="divide-y divide-gray-200">
|
||||||
<tr>
|
<tr>
|
||||||
<TableLeftHeading>Orcas non-profit organizations</TableLeftHeading>
|
<td className="whitespace-nowrap py-4 pl-4 pr-3 text-sm font-medium text-gray-900 sm:pl-0">Orcas non-profit organizations</td>
|
||||||
<TableCell>$50 or 2 hours for $30*</TableCell>
|
<td className="whitespace-nowrap px-3 py-4 text-sm text-gray-500">$50 or 2 hours for $30*</td>
|
||||||
<TableCell>$100</TableCell>
|
<td className="whitespace-nowrap px-3 py-4 text-sm text-gray-500">$100</td>
|
||||||
<TableCell>None†</TableCell>
|
<td className="whitespace-nowrap px-3 py-4 text-sm text-gray-500">None†</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<TableLeftHeading>WSCC and OIYC Members</TableLeftHeading>
|
<td className="whitespace-nowrap py-4 pl-4 pr-3 text-sm font-medium text-gray-900 sm:pl-0">WSCC and OIYC Members</td>
|
||||||
<TableCell>$50 or 2 hours for $30*</TableCell>
|
<td className="whitespace-nowrap px-3 py-4 text-sm text-gray-500">$50 or 2 hours for $30*</td>
|
||||||
<TableCell>$100</TableCell>
|
<td className="whitespace-nowrap px-3 py-4 text-sm text-gray-500">$100</td>
|
||||||
<TableCell>None†</TableCell>
|
<td className="whitespace-nowrap px-3 py-4 text-sm text-gray-500">None†</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<TableLeftHeading>Off-island non-profit organizations</TableLeftHeading>
|
<td className="whitespace-nowrap py-4 pl-4 pr-3 text-sm font-medium text-gray-900 sm:pl-0">Off-island non-profit organizations</td>
|
||||||
<TableCell>$50</TableCell>
|
<td className="whitespace-nowrap px-3 py-4 text-sm text-gray-500">$50</td>
|
||||||
<TableCell>$100</TableCell>
|
<td className="whitespace-nowrap px-3 py-4 text-sm text-gray-500">$100</td>
|
||||||
<TableCell>$300</TableCell>
|
<td className="whitespace-nowrap px-3 py-4 text-sm text-gray-500">$300</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<TableLeftHeading>Individuals and non-public use</TableLeftHeading>
|
<td className="whitespace-nowrap py-4 pl-4 pr-3 text-sm font-medium text-gray-900 sm:pl-0">Individuals and non-public use</td>
|
||||||
<TableCell>$40 per hour</TableCell>
|
<td className="whitespace-nowrap px-3 py-4 text-sm text-gray-500">$40 per hour</td>
|
||||||
<TableCell>$300</TableCell>
|
<td className="whitespace-nowrap px-3 py-4 text-sm text-gray-500">$300</td>
|
||||||
<TableCell>$300</TableCell>
|
<td className="whitespace-nowrap px-3 py-4 text-sm text-gray-500">$300</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<TableLeftHeading>Government sponsored activities</TableLeftHeading>
|
<td className="whitespace-nowrap py-4 pl-4 pr-3 text-sm font-medium text-gray-900 sm:pl-0">Government sponsored activities</td>
|
||||||
<TableCell>$50</TableCell>
|
<td className="whitespace-nowrap px-3 py-4 text-sm text-gray-500">$50</td>
|
||||||
<TableCell>$50</TableCell>
|
<td className="whitespace-nowrap px-3 py-4 text-sm text-gray-500">$50</td>
|
||||||
<TableCell>None†</TableCell>
|
<td className="whitespace-nowrap px-3 py-4 text-sm text-gray-500">None†</td>
|
||||||
</tr>
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="mt-6 space-y-5 text-base text-zinc-600">
|
<div className="mt-6 space-y-5 text-base text-zinc-600 dark:text-zinc-400">
|
||||||
<h2 className="text-2xl font-bold tracking-tight text-zinc-800 sm:text-2xl">
|
<h2 className="text-2xl font-bold tracking-tight text-zinc-800 dark:text-zinc-100 sm:text-2xl">
|
||||||
Use Restrictions
|
Use Restrictions
|
||||||
</h2>
|
</h2>
|
||||||
|
|
||||||
@ -176,7 +168,6 @@ export default function Rental() {
|
|||||||
The West Sound Community Hall may not be used for either personal or
|
The West Sound Community Hall may not be used for either personal or
|
||||||
organizational monetary gain or to promote business activities.
|
organizational monetary gain or to promote business activities.
|
||||||
The only exceptions to this restriction are:
|
The only exceptions to this restriction are:
|
||||||
</p>
|
|
||||||
|
|
||||||
<ol className="list-decimal ml-8 mt-5">
|
<ol className="list-decimal ml-8 mt-5">
|
||||||
<li>
|
<li>
|
||||||
@ -192,6 +183,7 @@ export default function Rental() {
|
|||||||
sponsoring nonprofit organization.)
|
sponsoring nonprofit organization.)
|
||||||
</li>
|
</li>
|
||||||
</ol>
|
</ol>
|
||||||
|
</p>
|
||||||
|
|
||||||
<p>
|
<p>
|
||||||
Any individual or organizational nonpublic use of the hall which
|
Any individual or organizational nonpublic use of the hall which
|
||||||
@ -222,8 +214,8 @@ export default function Rental() {
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="mt-6 space-y-5 text-base text-zinc-600">
|
<div className="mt-6 space-y-5 text-base text-zinc-600 dark:text-zinc-400">
|
||||||
<h2 className="text-2xl font-bold tracking-tight text-zinc-800 sm:text-2xl">
|
<h2 className="text-2xl font-bold tracking-tight text-zinc-800 dark:text-zinc-100 sm:text-2xl">
|
||||||
Capacity
|
Capacity
|
||||||
</h2>
|
</h2>
|
||||||
|
|
||||||
@ -233,8 +225,8 @@ export default function Rental() {
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="mt-6 space-y-5 text-base text-zinc-600">
|
<div className="mt-6 space-y-5 text-base text-zinc-600 dark:text-zinc-400">
|
||||||
<h2 className="text-2xl font-bold tracking-tight text-zinc-800 sm:text-2xl">
|
<h2 className="text-2xl font-bold tracking-tight text-zinc-800 dark:text-zinc-100 sm:text-2xl">
|
||||||
Parking
|
Parking
|
||||||
</h2>
|
</h2>
|
||||||
|
|
||||||
@ -255,8 +247,8 @@ export default function Rental() {
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="mt-6 space-y-5 text-base text-zinc-600">
|
<div className="mt-6 space-y-5 text-base text-zinc-600 dark:text-zinc-400">
|
||||||
<h2 className="text-2xl font-bold tracking-tight text-zinc-800 sm:text-2xl">
|
<h2 className="text-2xl font-bold tracking-tight text-zinc-800 dark:text-zinc-100 sm:text-2xl">
|
||||||
Accessibility
|
Accessibility
|
||||||
</h2>
|
</h2>
|
||||||
|
|
||||||
|
@ -1,17 +1,18 @@
|
|||||||
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
|
||||||
@ -22,11 +23,9 @@ 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="text-base text-zinc-600">
|
<p className="mt-6 text-base text-zinc-600 dark:text-zinc-400">
|
||||||
Your financial support helps us perserve this historic building and to
|
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.
|
||||||
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>
|
||||||
);
|
)
|
||||||
}
|
}
|
||||||
|
24
src/app/upcoming-events.json
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
[
|
||||||
|
{
|
||||||
|
"title": "January Potluck",
|
||||||
|
"date": "2024-01-20",
|
||||||
|
"startTime": "6:00pm"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "February Potluck",
|
||||||
|
"date": "2024-02-17",
|
||||||
|
"startTime": "6:00pm",
|
||||||
|
"notes": "With historic West Sound slide show."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "Town Hall Meeting",
|
||||||
|
"date": "2024-03-06",
|
||||||
|
"startTime": "6:00pm",
|
||||||
|
"endTime": "8:00pm"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "March Potluck",
|
||||||
|
"date": "2024-03-16",
|
||||||
|
"startTime": "6:00pm"
|
||||||
|
}
|
||||||
|
]
|
@ -41,24 +41,24 @@ export function ArticleLayout({
|
|||||||
type="button"
|
type="button"
|
||||||
onClick={() => router.back()}
|
onClick={() => router.back()}
|
||||||
aria-label="Go back to articles"
|
aria-label="Go back to articles"
|
||||||
className="group mb-8 flex h-10 w-10 items-center justify-center rounded-full bg-white shadow-md shadow-zinc-800/5 ring-1 ring-zinc-900/5 transition lg:absolute lg:-left-5 lg:-mt-2 lg:mb-0 xl:-top-1.5 xl:left-0 xl:mt-0"
|
className="group mb-8 flex h-10 w-10 items-center justify-center rounded-full bg-white shadow-md shadow-zinc-800/5 ring-1 ring-zinc-900/5 transition dark:border dark:border-zinc-700/50 dark:bg-zinc-800 dark:ring-0 dark:ring-white/10 dark:hover:border-zinc-700 dark:hover:ring-white/20 lg:absolute lg:-left-5 lg:-mt-2 lg:mb-0 xl:-top-1.5 xl:left-0 xl:mt-0"
|
||||||
>
|
>
|
||||||
<ArrowLeftIcon className="h-4 w-4 stroke-zinc-500 transition group-hover:stroke-zinc-700" />
|
<ArrowLeftIcon className="h-4 w-4 stroke-zinc-500 transition group-hover:stroke-zinc-700 dark:stroke-zinc-500 dark:group-hover:stroke-zinc-400" />
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
<article>
|
<article>
|
||||||
<header className="flex flex-col">
|
<header className="flex flex-col">
|
||||||
<h1 className="mt-6 text-4xl font-bold tracking-tight text-zinc-800 sm:text-5xl">
|
<h1 className="mt-6 text-4xl font-bold tracking-tight text-zinc-800 dark:text-zinc-100 sm:text-5xl">
|
||||||
{article.title}
|
{article.title}
|
||||||
</h1>
|
</h1>
|
||||||
<time
|
<time
|
||||||
dateTime={article.date}
|
dateTime={article.date}
|
||||||
className="order-first flex items-center text-base text-zinc-400"
|
className="order-first flex items-center text-base text-zinc-400 dark:text-zinc-500"
|
||||||
>
|
>
|
||||||
<span className="h-4 w-0.5 rounded-full bg-zinc-200" />
|
<span className="h-4 w-0.5 rounded-full bg-zinc-200 dark:bg-zinc-500" />
|
||||||
<span className="ml-3">{formatDate(article.date)}</span>
|
<span className="ml-3">{formatDate(article.date)}</span>
|
||||||
</time>
|
</time>
|
||||||
<span id="byline" className="mt-4 text-base text-zinc-400">by {article.author}</span>
|
<span id="byline" className='mt-4 text-base text-zinc-400 dark:text-zinc-500'>by {article.author}</span>
|
||||||
</header>
|
</header>
|
||||||
<Prose className="mt-8" data-mdx-content>
|
<Prose className="mt-8" data-mdx-content>
|
||||||
{children}
|
{children}
|
||||||
|
@ -3,9 +3,9 @@ import clsx from 'clsx'
|
|||||||
|
|
||||||
const variantStyles = {
|
const variantStyles = {
|
||||||
primary:
|
primary:
|
||||||
'bg-zinc-800 font-semibold text-zinc-100 hover:bg-zinc-700 active:bg-zinc-800 active:text-zinc-100/70',
|
'bg-zinc-800 font-semibold text-zinc-100 hover:bg-zinc-700 active:bg-zinc-800 active:text-zinc-100/70 dark:bg-zinc-700 dark:hover:bg-zinc-600 dark:active:bg-zinc-700 dark:active:text-zinc-100/70',
|
||||||
secondary:
|
secondary:
|
||||||
'bg-zinc-50 font-medium text-zinc-900 hover:bg-zinc-100 active:bg-zinc-100 active:text-zinc-900/60',
|
'bg-zinc-50 font-medium text-zinc-900 hover:bg-zinc-100 active:bg-zinc-100 active:text-zinc-900/60 dark:bg-zinc-800/50 dark:text-zinc-300 dark:hover:bg-zinc-800 dark:hover:text-zinc-50 dark:active:bg-zinc-800/50 dark:active:text-zinc-50/70',
|
||||||
}
|
}
|
||||||
|
|
||||||
type ButtonProps = {
|
type ButtonProps = {
|
||||||
@ -13,7 +13,7 @@ type ButtonProps = {
|
|||||||
} & (
|
} & (
|
||||||
| (React.ComponentPropsWithoutRef<'button'> & { href?: undefined })
|
| (React.ComponentPropsWithoutRef<'button'> & { href?: undefined })
|
||||||
| React.ComponentPropsWithoutRef<typeof Link>
|
| React.ComponentPropsWithoutRef<typeof Link>
|
||||||
)
|
)
|
||||||
|
|
||||||
export function Button({
|
export function Button({
|
||||||
variant = 'primary',
|
variant = 'primary',
|
||||||
|
@ -39,7 +39,7 @@ Card.Link = function CardLink({
|
|||||||
}: React.ComponentPropsWithoutRef<typeof Link>) {
|
}: React.ComponentPropsWithoutRef<typeof Link>) {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className="absolute -inset-x-4 -inset-y-6 z-0 scale-95 bg-zinc-50 opacity-0 transition group-hover:scale-100 group-hover:opacity-100 sm:-inset-x-6 sm:rounded-2xl" />
|
<div className="absolute -inset-x-4 -inset-y-6 z-0 scale-95 bg-zinc-50 opacity-0 transition group-hover:scale-100 group-hover:opacity-100 dark:bg-zinc-800/50 sm:-inset-x-6 sm:rounded-2xl" />
|
||||||
<Link {...props}>
|
<Link {...props}>
|
||||||
<span className="absolute -inset-x-4 -inset-y-6 z-20 sm:-inset-x-6 sm:rounded-2xl" />
|
<span className="absolute -inset-x-4 -inset-y-6 z-20 sm:-inset-x-6 sm:rounded-2xl" />
|
||||||
<span className="relative z-10">{children}</span>
|
<span className="relative z-10">{children}</span>
|
||||||
@ -59,7 +59,7 @@ Card.Title = function CardTitle<T extends React.ElementType = 'h2'>({
|
|||||||
let Component = as ?? 'h2'
|
let Component = as ?? 'h2'
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Component className="text-base font-semibold tracking-tight text-zinc-800">
|
<Component className="text-base font-semibold tracking-tight text-zinc-800 dark:text-zinc-100">
|
||||||
{href ? <Card.Link href={href}>{children}</Card.Link> : children}
|
{href ? <Card.Link href={href}>{children}</Card.Link> : children}
|
||||||
</Component>
|
</Component>
|
||||||
)
|
)
|
||||||
@ -71,7 +71,7 @@ Card.Description = function CardDescription({
|
|||||||
children: React.ReactNode
|
children: React.ReactNode
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<p className="relative z-10 mt-2 text-sm text-zinc-600">
|
<p className="relative z-10 mt-2 text-sm text-zinc-600 dark:text-zinc-400">
|
||||||
{children}
|
{children}
|
||||||
</p>
|
</p>
|
||||||
)
|
)
|
||||||
@ -105,7 +105,7 @@ Card.Eyebrow = function CardEyebrow<T extends React.ElementType = 'p'>({
|
|||||||
<Component
|
<Component
|
||||||
className={clsx(
|
className={clsx(
|
||||||
className,
|
className,
|
||||||
"relative z-10 order-first mb-3 flex items-center text-sm text-zinc-400",
|
'relative z-10 order-first mb-3 flex items-center text-sm text-zinc-400 dark:text-zinc-500',
|
||||||
decorate && 'pl-3.5',
|
decorate && 'pl-3.5',
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
@ -115,7 +115,7 @@ Card.Eyebrow = function CardEyebrow<T extends React.ElementType = 'p'>({
|
|||||||
className="absolute inset-y-0 left-0 flex items-center"
|
className="absolute inset-y-0 left-0 flex items-center"
|
||||||
aria-hidden="true"
|
aria-hidden="true"
|
||||||
>
|
>
|
||||||
<span className="h-4 w-0.5 rounded-full bg-zinc-200" />
|
<span className="h-4 w-0.5 rounded-full bg-zinc-200 dark:bg-zinc-500" />
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
{children}
|
{children}
|
||||||
|
@ -12,7 +12,7 @@ function NavLink({
|
|||||||
return (
|
return (
|
||||||
<Link
|
<Link
|
||||||
href={href}
|
href={href}
|
||||||
className="transition hover:text-teal-500"
|
className="transition hover:text-teal-500 dark:hover:text-teal-400"
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
</Link>
|
</Link>
|
||||||
@ -23,24 +23,18 @@ export function Footer() {
|
|||||||
return (
|
return (
|
||||||
<footer className="mt-32 flex-none">
|
<footer className="mt-32 flex-none">
|
||||||
<ContainerOuter>
|
<ContainerOuter>
|
||||||
<div className="border-t border-zinc-100 pb-16 pt-10">
|
<div className="border-t border-zinc-100 pb-16 pt-10 dark:border-zinc-700/40">
|
||||||
<ContainerInner>
|
<ContainerInner>
|
||||||
<div className="flex flex-col items-center justify-between gap-6 sm:flex-row">
|
<div className="flex flex-col items-center justify-between gap-6 sm:flex-row">
|
||||||
<div className="flex flex-wrap justify-center gap-x-6 gap-y-1 text-sm font-medium text-zinc-800">
|
<div className="flex flex-wrap justify-center gap-x-6 gap-y-1 text-sm font-medium text-zinc-800 dark:text-zinc-200">
|
||||||
<NavLink href="/hall-history">History</NavLink>
|
<NavLink href="/hall-history">History</NavLink>
|
||||||
<NavLink href="/news">News</NavLink>
|
<NavLink href="/news">News</NavLink>
|
||||||
<NavLink href="/calendar">Calendar</NavLink>
|
|
||||||
<NavLink href="/rental">Rental</NavLink>
|
<NavLink href="/rental">Rental</NavLink>
|
||||||
<NavLink href="/club">Club</NavLink>
|
<NavLink href="/club">Club</NavLink>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<p className="text-sm text-zinc-400 dark:text-zinc-500">
|
||||||
<p className="text-sm text-zinc-400">
|
|
||||||
© {new Date().getFullYear()} West Sound Community Club. All rights reserved.
|
© {new Date().getFullYear()} West Sound Community Club. All rights reserved.
|
||||||
</p>
|
</p>
|
||||||
<p className="text-sm text-zinc-400">
|
|
||||||
WSCC is a 501c3 nonprofit organization - 91-1283768
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</ContainerInner>
|
</ContainerInner>
|
||||||
</div>
|
</div>
|
||||||
|
@ -1,14 +1,47 @@
|
|||||||
'use client'
|
'use client'
|
||||||
|
|
||||||
import { Fragment, useEffect, useRef } from 'react'
|
import { Fragment, useEffect, useRef, useState } from 'react'
|
||||||
import Link from 'next/link'
|
import Link from 'next/link'
|
||||||
import { usePathname } from 'next/navigation'
|
import { usePathname } from 'next/navigation'
|
||||||
|
import { useTheme } from 'next-themes'
|
||||||
import { Popover, Transition } from '@headlessui/react'
|
import { Popover, Transition } from '@headlessui/react'
|
||||||
import clsx from 'clsx'
|
import clsx from 'clsx'
|
||||||
import { XMarkIcon, ChevronDownIcon } from '@heroicons/react/24/solid'
|
import { XMarkIcon, ChevronDownIcon } from '@heroicons/react/24/solid'
|
||||||
|
|
||||||
import { Container } from '@/components/Container'
|
import { Container } from '@/components/Container'
|
||||||
|
|
||||||
|
function SunIcon(props: React.ComponentPropsWithoutRef<'svg'>) {
|
||||||
|
return (
|
||||||
|
<svg
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
strokeWidth="1.5"
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
aria-hidden="true"
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<path d="M8 12.25A4.25 4.25 0 0 1 12.25 8v0a4.25 4.25 0 0 1 4.25 4.25v0a4.25 4.25 0 0 1-4.25 4.25v0A4.25 4.25 0 0 1 8 12.25v0Z" />
|
||||||
|
<path
|
||||||
|
d="M12.25 3v1.5M21.5 12.25H20M18.791 18.791l-1.06-1.06M18.791 5.709l-1.06 1.06M12.25 20v1.5M4.5 12.25H3M6.77 6.77 5.709 5.709M6.77 17.73l-1.061 1.061"
|
||||||
|
fill="none"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function MoonIcon(props: React.ComponentPropsWithoutRef<'svg'>) {
|
||||||
|
return (
|
||||||
|
<svg viewBox="0 0 24 24" aria-hidden="true" {...props}>
|
||||||
|
<path
|
||||||
|
d="M17.25 16.22a6.937 6.937 0 0 1-9.47-9.47 7.451 7.451 0 1 0 9.47 9.47ZM12.75 7C17 7 17 2.75 17 2.75S17 7 21.25 7C17 7 17 11.25 17 11.25S17 7 12.75 7Z"
|
||||||
|
strokeWidth="1.5"
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
function MobileNavItem({
|
function MobileNavItem({
|
||||||
href,
|
href,
|
||||||
children,
|
children,
|
||||||
@ -30,9 +63,9 @@ function MobileNavigation(
|
|||||||
) {
|
) {
|
||||||
return (
|
return (
|
||||||
<Popover {...props}>
|
<Popover {...props}>
|
||||||
<Popover.Button className="group flex items-center rounded-full bg-white/90 px-4 py-2 text-sm font-medium text-zinc-800 shadow-lg shadow-zinc-800/5 ring-1 ring-zinc-900/5 backdrop-blur">
|
<Popover.Button className="group flex items-center rounded-full bg-white/90 px-4 py-2 text-sm font-medium text-zinc-800 shadow-lg shadow-zinc-800/5 ring-1 ring-zinc-900/5 backdrop-blur dark:bg-zinc-800/90 dark:text-zinc-200 dark:ring-white/10 dark:hover:ring-white/20">
|
||||||
Menu
|
Menu
|
||||||
<ChevronDownIcon className="ml-3 h-auto w-2 stroke-zinc-500 group-hover:stroke-zinc-700" />
|
<ChevronDownIcon className="ml-3 h-auto w-2 stroke-zinc-500 group-hover:stroke-zinc-700 dark:group-hover:stroke-zinc-400" />
|
||||||
</Popover.Button>
|
</Popover.Button>
|
||||||
<Transition.Root>
|
<Transition.Root>
|
||||||
<Transition.Child
|
<Transition.Child
|
||||||
@ -44,7 +77,7 @@ function MobileNavigation(
|
|||||||
leaveFrom="opacity-100"
|
leaveFrom="opacity-100"
|
||||||
leaveTo="opacity-0"
|
leaveTo="opacity-0"
|
||||||
>
|
>
|
||||||
<Popover.Overlay className="fixed inset-0 z-50 bg-zinc-800/40 backdrop-blur-sm" />
|
<Popover.Overlay className="fixed inset-0 z-50 bg-zinc-800/40 backdrop-blur-sm dark:bg-black/80" />
|
||||||
</Transition.Child>
|
</Transition.Child>
|
||||||
<Transition.Child
|
<Transition.Child
|
||||||
as={Fragment}
|
as={Fragment}
|
||||||
@ -57,22 +90,21 @@ function MobileNavigation(
|
|||||||
>
|
>
|
||||||
<Popover.Panel
|
<Popover.Panel
|
||||||
focus
|
focus
|
||||||
className="fixed inset-x-4 top-8 z-50 origin-top rounded-3xl bg-white p-8 ring-1 ring-zinc-900/5"
|
className="fixed inset-x-4 top-8 z-50 origin-top rounded-3xl bg-white p-8 ring-1 ring-zinc-900/5 dark:bg-zinc-900 dark:ring-zinc-800"
|
||||||
>
|
>
|
||||||
<div className="flex flex-row-reverse items-center justify-between">
|
<div className="flex flex-row-reverse items-center justify-between">
|
||||||
<Popover.Button aria-label="Close menu" className="-m-1 p-1">
|
<Popover.Button aria-label="Close menu" className="-m-1 p-1">
|
||||||
<XMarkIcon className="h-6 w-6 text-zinc-500" />
|
<XMarkIcon className="h-6 w-6 text-zinc-500 dark:text-zinc-400" />
|
||||||
</Popover.Button>
|
</Popover.Button>
|
||||||
<h2 className="text-sm font-medium text-zinc-600">
|
<h2 className="text-sm font-medium text-zinc-600 dark:text-zinc-400">
|
||||||
Navigation
|
Navigation
|
||||||
</h2>
|
</h2>
|
||||||
</div>
|
</div>
|
||||||
<nav className="mt-6">
|
<nav className="mt-6">
|
||||||
<ul className="-my-2 divide-y divide-zinc-100 text-base text-zinc-800">
|
<ul className="-my-2 divide-y divide-zinc-100 text-base text-zinc-800 dark:divide-zinc-100/5 dark:text-zinc-300">
|
||||||
<MobileNavItem href="/">Home</MobileNavItem>
|
<MobileNavItem href="/">Home</MobileNavItem>
|
||||||
<MobileNavItem href="/hall-history">Hall History</MobileNavItem>
|
<MobileNavItem href="/hall-history">Hall History</MobileNavItem>
|
||||||
<MobileNavItem href="/news">News</MobileNavItem>
|
<MobileNavItem href="/news">News</MobileNavItem>
|
||||||
<MobileNavItem href="/calendar">Calendar</MobileNavItem>
|
|
||||||
<MobileNavItem href="/rental">Rental</MobileNavItem>
|
<MobileNavItem href="/rental">Rental</MobileNavItem>
|
||||||
<MobileNavItem href="/club">Club</MobileNavItem>
|
<MobileNavItem href="/club">Club</MobileNavItem>
|
||||||
</ul>
|
</ul>
|
||||||
@ -100,13 +132,13 @@ function NavItem({
|
|||||||
className={clsx(
|
className={clsx(
|
||||||
'relative block px-3 py-2 transition',
|
'relative block px-3 py-2 transition',
|
||||||
isActive
|
isActive
|
||||||
? "text-teal-500"
|
? 'text-teal-500 dark:text-teal-400'
|
||||||
: "hover:text-teal-500",
|
: 'hover:text-teal-500 dark:hover:text-teal-400',
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
{isActive && (
|
{isActive && (
|
||||||
<span className="absolute inset-x-1 -bottom-px h-px bg-gradient-to-r from-teal-500/0 via-teal-500/40 to-teal-500/0" />
|
<span className="absolute inset-x-1 -bottom-px h-px bg-gradient-to-r from-teal-500/0 via-teal-500/40 to-teal-500/0 dark:from-teal-400/0 dark:via-teal-400/40 dark:to-teal-400/0" />
|
||||||
)}
|
)}
|
||||||
</Link>
|
</Link>
|
||||||
</li>
|
</li>
|
||||||
@ -116,11 +148,10 @@ function NavItem({
|
|||||||
function DesktopNavigation(props: React.ComponentPropsWithoutRef<'nav'>) {
|
function DesktopNavigation(props: React.ComponentPropsWithoutRef<'nav'>) {
|
||||||
return (
|
return (
|
||||||
<nav {...props}>
|
<nav {...props}>
|
||||||
<ul className="flex rounded-full bg-white/90 px-3 text-sm font-medium text-zinc-800 shadow-lg shadow-zinc-800/5 ring-1 ring-zinc-900/5 backdrop-blur">
|
<ul className="flex rounded-full bg-white/90 px-3 text-sm font-medium text-zinc-800 shadow-lg shadow-zinc-800/5 ring-1 ring-zinc-900/5 backdrop-blur dark:bg-zinc-800/90 dark:text-zinc-200 dark:ring-white/10">
|
||||||
<NavItem href="/">Home</NavItem>
|
<NavItem href="/">Home</NavItem>
|
||||||
<NavItem href="/hall-history">History</NavItem>
|
<NavItem href="/hall-history">History</NavItem>
|
||||||
<NavItem href="/news">News</NavItem>
|
<NavItem href="/news">News</NavItem>
|
||||||
<NavItem href="/calendar">Calendar</NavItem>
|
|
||||||
<NavItem href="/rental">Rental</NavItem>
|
<NavItem href="/rental">Rental</NavItem>
|
||||||
<NavItem href="/club">Club</NavItem>
|
<NavItem href="/club">Club</NavItem>
|
||||||
</ul>
|
</ul>
|
||||||
@ -128,6 +159,28 @@ function DesktopNavigation(props: React.ComponentPropsWithoutRef<'nav'>) {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function ThemeToggle() {
|
||||||
|
let { resolvedTheme, setTheme } = useTheme()
|
||||||
|
let otherTheme = resolvedTheme === 'dark' ? 'light' : 'dark'
|
||||||
|
let [mounted, setMounted] = useState(false)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setMounted(true)
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
aria-label={mounted ? `Switch to ${otherTheme} theme` : 'Toggle theme'}
|
||||||
|
className="group rounded-full bg-white/90 px-3 py-2 shadow-lg shadow-zinc-800/5 ring-1 ring-zinc-900/5 backdrop-blur transition dark:bg-zinc-800/90 dark:ring-white/10 dark:hover:ring-white/20"
|
||||||
|
onClick={() => setTheme(otherTheme)}
|
||||||
|
>
|
||||||
|
<SunIcon className="h-6 w-6 fill-zinc-100 stroke-zinc-500 transition group-hover:fill-zinc-200 group-hover:stroke-zinc-700 dark:hidden [@media(prefers-color-scheme:dark)]:fill-teal-50 [@media(prefers-color-scheme:dark)]:stroke-teal-500 [@media(prefers-color-scheme:dark)]:group-hover:fill-teal-50 [@media(prefers-color-scheme:dark)]:group-hover:stroke-teal-600" />
|
||||||
|
<MoonIcon className="hidden h-6 w-6 fill-zinc-700 stroke-zinc-500 transition dark:block [@media(prefers-color-scheme:dark)]:group-hover:stroke-zinc-400 [@media_not_(prefers-color-scheme:dark)]:fill-teal-400/10 [@media_not_(prefers-color-scheme:dark)]:stroke-teal-500" />
|
||||||
|
</button>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
function clamp(number: number, a: number, b: number) {
|
function clamp(number: number, a: number, b: number) {
|
||||||
let min = Math.min(a, b)
|
let min = Math.min(a, b)
|
||||||
let max = Math.max(a, b)
|
let max = Math.max(a, b)
|
||||||
@ -230,10 +283,18 @@ export function Header() {
|
|||||||
'var(--header-inner-position)' as React.CSSProperties['position'],
|
'var(--header-inner-position)' as React.CSSProperties['position'],
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
<div className="relative flex gap-4">
|
||||||
|
<div className="flex flex-1"></div>
|
||||||
<div className="flex flex-1 justify-end md:justify-center">
|
<div className="flex flex-1 justify-end md:justify-center">
|
||||||
<MobileNavigation className="pointer-events-auto md:hidden" />
|
<MobileNavigation className="pointer-events-auto md:hidden" />
|
||||||
<DesktopNavigation className="pointer-events-auto hidden md:block" />
|
<DesktopNavigation className="pointer-events-auto hidden md:block" />
|
||||||
</div>
|
</div>
|
||||||
|
<div className="flex justify-end md:flex-1">
|
||||||
|
<div className="pointer-events-auto">
|
||||||
|
<ThemeToggle />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</Container>
|
</Container>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
@ -6,7 +6,7 @@ export function Layout({ children }: { children: React.ReactNode }) {
|
|||||||
<>
|
<>
|
||||||
<div className="fixed inset-0 flex justify-center sm:px-8">
|
<div className="fixed inset-0 flex justify-center sm:px-8">
|
||||||
<div className="flex w-full max-w-7xl lg:px-8">
|
<div className="flex w-full max-w-7xl lg:px-8">
|
||||||
<div className="w-full bg-white ring-1 ring-zinc-100" />
|
<div className="w-full bg-white ring-1 ring-zinc-100 dark:bg-zinc-900 dark:ring-zinc-300/20" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="relative flex w-full flex-col">
|
<div className="relative flex w-full flex-col">
|
||||||
|
@ -1,7 +0,0 @@
|
|||||||
import React from 'react'
|
|
||||||
import Image, { ImageProps } from 'next/image'
|
|
||||||
|
|
||||||
// Workaround for https://github.com/hashicorp/next-mdx-remote/issues/405
|
|
||||||
export function MdxImage(props: ImageProps) {
|
|
||||||
return <Image {...props} alt={props.alt} />;
|
|
||||||
}
|
|
@ -5,6 +5,6 @@ export function Prose({
|
|||||||
...props
|
...props
|
||||||
}: React.ComponentPropsWithoutRef<'div'>) {
|
}: React.ComponentPropsWithoutRef<'div'>) {
|
||||||
return (
|
return (
|
||||||
<div className={clsx(className, 'prose')} {...props} />
|
<div className={clsx(className, 'prose dark:prose-invert')} {...props} />
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -12,12 +12,12 @@ export function Section({
|
|||||||
return (
|
return (
|
||||||
<section
|
<section
|
||||||
aria-labelledby={id}
|
aria-labelledby={id}
|
||||||
className="md:border-l md:border-zinc-100 md:pl-6"
|
className="md:border-l md:border-zinc-100 md:pl-6 md:dark:border-zinc-700/40"
|
||||||
>
|
>
|
||||||
<div className="grid max-w-3xl grid-cols-1 items-baseline gap-y-8 md:grid-cols-4">
|
<div className="grid max-w-3xl grid-cols-1 items-baseline gap-y-8 md:grid-cols-4">
|
||||||
<h2
|
<h2
|
||||||
id={id}
|
id={id}
|
||||||
className="text-sm font-semibold text-zinc-800"
|
className="text-sm font-semibold text-zinc-800 dark:text-zinc-100"
|
||||||
>
|
>
|
||||||
{title}
|
{title}
|
||||||
</h2>
|
</h2>
|
||||||
|
@ -12,10 +12,10 @@ export function SimpleLayout({
|
|||||||
return (
|
return (
|
||||||
<Container className="mt-16 sm:mt-32">
|
<Container className="mt-16 sm:mt-32">
|
||||||
<header className="max-w-2xl">
|
<header className="max-w-2xl">
|
||||||
<h1 className="text-4xl font-bold tracking-tight text-zinc-800 sm:text-5xl">
|
<h1 className="text-4xl font-bold tracking-tight text-zinc-800 dark:text-zinc-100 sm:text-5xl">
|
||||||
{title}
|
{title}
|
||||||
</h1>
|
</h1>
|
||||||
<p className="mt-6 text-base text-zinc-600">
|
<p className="mt-6 text-base text-zinc-600 dark:text-zinc-400">
|
||||||
{intro}
|
{intro}
|
||||||
</p>
|
</p>
|
||||||
</header>
|
</header>
|
||||||
|
@ -1,25 +0,0 @@
|
|||||||
|
|
||||||
export function TableHeading({
|
|
||||||
children
|
|
||||||
}: {
|
|
||||||
children: React.ReactNode
|
|
||||||
}) {
|
|
||||||
return <th scope="col" className="px-3 py-3.5 text-left text-sm font-semibold text-gray-900">{children}</th>
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
export function TableLeftHeading({
|
|
||||||
children
|
|
||||||
}: {
|
|
||||||
children: React.ReactNode
|
|
||||||
}) {
|
|
||||||
return <td className="whitespace-nowrap py-4 pl-4 pr-3 text-sm font-medium text-gray-900 sm:pl-0">{children}</td>
|
|
||||||
}
|
|
||||||
|
|
||||||
export function TableCell({
|
|
||||||
children
|
|
||||||
}: {
|
|
||||||
children: React.ReactNode
|
|
||||||
}) {
|
|
||||||
return <td className="whitespace-nowrap px-3 py-4 text-sm text-gray-500">{children}</td>
|
|
||||||
}
|
|
Before Width: | Height: | Size: 91 KiB |
12
src/images/logos/animaginary.svg
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
<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>
|
After Width: | Height: | Size: 625 B |
25
src/images/logos/cosmos.svg
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
<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>
|
After Width: | Height: | Size: 1.5 KiB |
5
src/images/logos/helio-stream.svg
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
<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>
|
After Width: | Height: | Size: 339 B |
21
src/images/logos/open-shuttle.svg
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
<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>
|
After Width: | Height: | Size: 1.2 KiB |
24
src/images/logos/planetaria.svg
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
<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>
|
After Width: | Height: | Size: 3.2 KiB |
@ -6,8 +6,6 @@ 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,
|
||||||
@ -15,8 +13,6 @@ export function emailNotification({
|
|||||||
phone,
|
phone,
|
||||||
address,
|
address,
|
||||||
}: {
|
}: {
|
||||||
subject: string;
|
|
||||||
bodyPrefix: string;
|
|
||||||
name: string;
|
name: string;
|
||||||
type: string;
|
type: string;
|
||||||
amount: number;
|
amount: number;
|
||||||
@ -27,9 +23,9 @@ export function emailNotification({
|
|||||||
client.sendEmail({
|
client.sendEmail({
|
||||||
From: fromAddr,
|
From: fromAddr,
|
||||||
To: toAddr,
|
To: toAddr,
|
||||||
Subject: subject,
|
Subject: 'New WSCC Membership',
|
||||||
TextBody: [
|
TextBody: [
|
||||||
bodyPrefix,
|
'New WSCC member:',
|
||||||
'',
|
'',
|
||||||
'Name: ' + name,
|
'Name: ' + name,
|
||||||
'Type: ' + type,
|
'Type: ' + type,
|
||||||
|
@ -2,10 +2,3 @@
|
|||||||
@import 'tailwindcss/components';
|
@import 'tailwindcss/components';
|
||||||
@import './prism.css';
|
@import './prism.css';
|
||||||
@import 'tailwindcss/utilities';
|
@import 'tailwindcss/utilities';
|
||||||
|
|
||||||
:root {
|
|
||||||
/* Your default theme */
|
|
||||||
|
|
||||||
--stripe-background: #FFFFFF;
|
|
||||||
--stripe-foreground: #000000;
|
|
||||||
}
|
|
@ -170,8 +170,8 @@ export default function typographyStyles({ theme }: PluginUtils) {
|
|||||||
paddingLeft: theme('spacing.6'),
|
paddingLeft: theme('spacing.6'),
|
||||||
},
|
},
|
||||||
li: {
|
li: {
|
||||||
// marginTop: theme('spacing.2'),
|
marginTop: theme('spacing.6'),
|
||||||
// marginBottom: theme('spacing.2'),
|
marginBottom: theme('spacing.6'),
|
||||||
paddingLeft: theme('spacing[3.5]'),
|
paddingLeft: theme('spacing[3.5]'),
|
||||||
},
|
},
|
||||||
'li::marker': {
|
'li::marker': {
|
||||||
@ -184,14 +184,14 @@ export default function typographyStyles({ theme }: PluginUtils) {
|
|||||||
'ul > li::marker': {
|
'ul > li::marker': {
|
||||||
color: 'var(--tw-prose-bullets)',
|
color: 'var(--tw-prose-bullets)',
|
||||||
},
|
},
|
||||||
// 'li :is(ol, ul)': {
|
'li :is(ol, ul)': {
|
||||||
// marginTop: theme('spacing.2'),
|
marginTop: theme('spacing.4'),
|
||||||
// marginBottom: theme('spacing.2'),
|
marginBottom: theme('spacing.4'),
|
||||||
// },
|
},
|
||||||
// 'li :is(li, p)': {
|
'li :is(li, p)': {
|
||||||
// marginTop: theme('spacing.3'),
|
marginTop: theme('spacing.3'),
|
||||||
// marginBottom: theme('spacing.3'),
|
marginBottom: theme('spacing.3'),
|
||||||
// },
|
},
|
||||||
|
|
||||||
// Code blocks
|
// Code blocks
|
||||||
pre: {
|
pre: {
|
||||||
|