59 Commits

Author SHA1 Message Date
72dcb42147 Update action workflow versions
Some checks failed
Build Production Image / Build Production Image (push) Failing after 3m2s
2025-03-17 21:31:48 -07:00
64c80c11ba Donation page
Some checks failed
Build Production Image / Build Production Image (push) Failing after 2m28s
2025-03-16 22:10:16 -07:00
3e8fada7c7 Remove unused projects page 2025-03-16 22:09:58 -07:00
45cc2e856c Remove unused useMutationObservable 2025-03-16 22:09:58 -07:00
01c3912ba9 Add new entries to the hall timeline 2025-03-16 22:09:58 -07:00
5ed921ec05 Small copy corrections 2025-03-16 22:09:58 -07:00
d1c7de593b NFC: Formatting 2025-03-16 22:09:58 -07:00
4acf298044 Remove year from membership form pdf 2025-03-16 16:34:04 -07:00
a7ec7bf5eb Update Dockerfile env format 2024-12-19 21:52:54 -08:00
78d996a989 Move listmonk form into separate module and add captcha
Some checks failed
Build Production Image / Build Production Image (push) Failing after 2m21s
2024-12-19 18:53:40 -08:00
f342b0fa25 Add blog post about 2024-10-20 potluck
All checks were successful
Build Production Image / Build Production Image (push) Successful in 1m22s
2024-10-26 19:10:48 -07:00
8b57fd492d Add listmonk mailing list form
All checks were successful
Build Production Image / Build Production Image (push) Successful in 2m2s
2024-10-22 19:49:37 -07:00
f8971e812e Update board members
All checks were successful
Build Production Image / Build Production Image (push) Successful in 2m1s
2024-10-20 18:32:08 -07:00
e420bb7ca2 Add blog post about 2024-09-21 potluck
All checks were successful
Build Production Image / Build Production Image (push) Successful in 2m22s
2024-09-22 18:41:24 -07:00
41f688fd84 Fix typo 2024-09-22 18:41:03 -07:00
55aa6af239 Correctly start weeks with Sunday
All checks were successful
Build Production Image / Build Production Image (push) Successful in 1m36s
The days of the week already reflected this, but the headers did not
match.
2024-08-23 18:40:58 -07:00
922a8d24dd Set user agent when requesting iCal
All checks were successful
Build Production Image / Build Production Image (push) Successful in 1m5s
2024-05-21 19:57:31 -07:00
7192f527e9 Add Linn as director
All checks were successful
Build Production Image / Build Production Image (push) Successful in 1m15s
2024-04-27 14:22:31 -07:00
4cf7878202 Upgrade dependencies
All checks were successful
Build Production Image / Build Production Image (push) Successful in 55s
2024-04-21 08:42:25 -07:00
346df14a25 Fix page titles
They have West Sound Hall added to the end already.
2024-04-21 08:29:52 -07:00
b88febfc64 Add full name to titles
All checks were successful
Build Production Image / Build Production Image (push) Successful in 54s
2024-04-21 08:19:57 -07:00
a60cba2762 Move type deps back into runtime since it doesn't work without them
All checks were successful
Build Production Image / Build Production Image (push) Successful in 54s
2024-04-20 18:01:47 -07:00
3481adfcab Remove dark mode
All checks were successful
Build Production Image / Build Production Image (push) Successful in 1m0s
2024-04-20 17:52:55 -07:00
18365af5dc Add calendar pulling from Google Calendar 2024-04-20 15:56:48 -07:00
768fa39cf2 Fix escaping in blog post summary
All checks were successful
Build Production Image / Build Production Image (push) Successful in 1m13s
2024-03-24 09:06:18 -07:00
94e316daf6 Update package lock
All checks were successful
Build Production Image / Build Production Image (push) Successful in 1m38s
2024-03-24 09:00:10 -07:00
76d3e0a64d Add upcoming events 2024-03-24 09:00:00 -07:00
75c750c493 Add notes from town hall meeting 2024-03-24 08:59:46 -07:00
823ac4f5c8 Add news post for February 2024 potluck
All checks were successful
Build Production Image / Build Production Image (push) Successful in 59s
2024-02-27 19:59:42 -08:00
292cefb945 Update events
All checks were successful
Build Production Image / Build Production Image (push) Successful in 54s
2024-02-03 14:33:30 -08:00
7f3499aba1 Fix table heading on board of directors 2024-02-01 19:59:28 -08:00
f62e434dff Fix club membership form in dark mode
All checks were successful
Build Production Image / Build Production Image (push) Successful in 53s
2024-02-01 19:55:11 -08:00
7190b306e7 Add EIN to footer
All checks were successful
Build Production Image / Build Production Image (push) Successful in 1m3s
2024-01-31 20:29:43 -08:00
d9225e36d7 Fix rental table styling in dark mode 2024-01-31 20:29:27 -08:00
c957b156d8 Fix invisible heading in dark mode 2024-01-31 20:29:03 -08:00
f936bae555 Add board of directors page 2024-01-31 20:28:13 -08:00
52ec6d7b81 Add instructions for updating content to the README.md
All checks were successful
Build Production Image / Build Production Image (push) Successful in 57s
2024-01-28 10:04:51 -08:00
49751bb433 Reorganize homepage layout
Switch from 5 small photos to one larger photo on the right
2024-01-28 10:04:46 -08:00
18f711444f Load upcoming events from json file 2024-01-28 10:04:06 -08:00
c263bc4573 Remove logo avatar from top left corner 2024-01-28 10:03:17 -08:00
c4b5d24186 Remove checkbox for credit card fees 2024-01-28 10:02:07 -08:00
b532de646d Rename blog to news 2024-01-28 10:01:52 -08:00
283b5d8a49 Add homepage links for membership and rental
All checks were successful
Build Production Image / Build Production Image (push) Successful in 56s
2024-01-15 10:43:21 -08:00
e666588cc3 Production build at container run time
All checks were successful
Build Production Image / Build Production Image (push) Successful in 58s
2024-01-14 12:39:49 -08:00
d98da909ae Testing actions
All checks were successful
Build Production Image / Build Production Image (push) Successful in 1m18s
2024-01-11 14:59:13 -08:00
c7d3f92558 Dockerize for running in production 2024-01-08 18:42:56 -08:00
591ccc361b Add first blog post 2024-01-08 14:43:29 -08:00
d935608d0c Remove image rotation on club page 2024-01-08 14:43:13 -08:00
0ed3a4df64 Update event calendar 2024-01-08 14:43:03 -08:00
9287b4da1a Use Stripe Address and email admin on membership creation 2023-12-25 19:04:15 -08:00
896bb5242a Add Stripe membership form 2023-12-25 15:34:39 -08:00
9a32b8dfc0 Update club and rental pages
Add forms and update text
2023-12-15 18:33:37 -08:00
a9437816e1 Add rental information 2023-12-10 21:21:32 -08:00
db3d9fa9f4 Update images 2023-12-02 16:20:07 -08:00
7430c165aa Update avatar image to WSCC logo svg 2023-12-01 21:18:02 -08:00
1766431695 Convert more pages to hero icons 2023-12-01 21:17:46 -08:00
879a56d98e Convert resume into events calendar 2023-11-29 20:29:32 -08:00
982a376264 Add hero icons 2023-11-29 20:21:02 -08:00
7cad2d9c13 Add support for dynamic authors on blog posts 2023-11-26 10:14:10 -08:00
91 changed files with 3304 additions and 2013 deletions

8
.dockerignore Normal file
View File

@@ -0,0 +1,8 @@
# Don't copy node_modules
node_modules
# Ignore common folders that we do not need
.git
.next
.github
.vscode

View File

@@ -1 +1,7 @@
NEXT_PUBLIC_SITE_URL=https://example.com
NEXT_PUBLIC_SITE_URL=https://westsoundhall.org
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

View File

@@ -0,0 +1,32 @@
name: Build Production Image
run-name: $GITHUB_REF_TYPE Production Build
on:
push:
tags:
- '*.*.*'
jobs:
Build Production Image:
runs-on: ubuntu-latest
container:
image: git.grosinger.net/tgrosinger/runner-image:0.0.3
steps:
- name: Check out repository code
uses: actions/checkout@v3
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Login to Gitea Docker registry
uses: docker/login-action@v3
with:
registry: git.grosinger.net
username: tgrosinger
password: ${{ secrets.CONTAINER_REGISTRY_ACCESS_TOKEN }}
- name: Build and push
uses: docker/build-push-action@v6
with:
context: .
push: true
tags: git.grosinger.net/tgrosinger/west-sound-hall:${{ gitea.ref_name }}

20
Dockerfile Normal file
View File

@@ -0,0 +1,20 @@
FROM docker.io/library/node:18-alpine AS build-env
ENV NODE_ENV=production
ENV PORT=3000
ENV NEXT_TELEMETRY_DISABLED=1
WORKDIR /app
# Building app
COPY package*.json ./
# Install node modules
# Note: We also install dev deps as TypeScript may be needed
RUN npm install
# Copy files. Use dockerignore to avoid copying node_modules
COPY . .
# Run app command
CMD ["/bin/ash", "/app/entrypoint.sh"]

View File

@@ -1,20 +1,44 @@
# Spotlight
# West Sound Hall Website
Spotlight is a [Tailwind UI](https://tailwindui.com) site template built using [Tailwind CSS](https://tailwindcss.com) and [Next.js](https://nextjs.org).
This is the website for the West Sound Hall and Community Club on Orcas Island, WA.
## Getting started
https://westsoundhall.org
To get started with this template, first install the npm dependencies:
## 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.
```sh
docker run -p 3000:3000 git.grosinger.net/tgrosinger/west-sound-hall:0.0.14
```
## Updating
### Events on the Homepage and the Calendar
The events on the calendar are loaded from the westsoundcommunityclub@gmail.com Google Calendar.
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).
### News Posts
News posts are written in [`src/app/news`](https://git.grosinger.net/tgrosinger/west-sound-hall/src/branch/main/src/app/news). Each post requires a directory within this folder, and the directory title will become the last segment of the news post URL.
To create a new post, create a new directory in that folder, then within that folder create a `page.mdx`. Use an existing news post as a template by copying its `page.mdx` then modify the author, date, title, description, and the body of the post as needed.
Posts are written in [mdx](https://mdxjs.com/) however for most news posts you can just consider the body of the post to be [standard markdown](https://www.markdownguide.org/basic-syntax/).
Photos can be added in the same directory as the `page.mdx` file. Refer to another news post for an example of how to embed them.
## Developing
To get started, first install the npm dependencies:
```bash
npm install
```
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, 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, run the development server:
@@ -24,19 +48,8 @@ npm run dev
Finally, open [http://localhost:3000](http://localhost:3000) in your browser to view the website.
## Customizing
You can start editing this template by modifying the files in the `/src` folder. The site will auto-update as you edit these files.
## License
This site template is a commercial product and is 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).
## Learn more
To learn more about the technologies used in this site template, see the following resources:
- [Tailwind CSS](https://tailwindcss.com/docs) - the official Tailwind CSS documentation
- [Next.js](https://nextjs.org/docs) - the official Next.js documentation
- [Headless UI](https://headlessui.dev) - the official Headless UI documentation
- [MDX](https://mdxjs.com) - the MDX documentation
It was purchased by [Tony Grosinger](mailto:tony@grosinger.net).

7
entrypoint.sh Executable file
View File

@@ -0,0 +1,7 @@
#!/bin/bash
# The build step hard-codes variables from the environment into HTML, so it must
# be done with the correct .env.local for runtime.
./node_modules/next/dist/bin/next build
./node_modules/next/dist/bin/next start

1013
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -10,34 +10,42 @@
},
"browserslist": "defaults, not ie <= 11",
"dependencies": {
"@headlessui/react": "^1.7.13",
"@headlessui/react": "1.7.19",
"@heroicons/react": "^2.0.18",
"@mapbox/rehype-prism": "^0.8.0",
"@mdx-js/loader": "^2.3.0",
"@mdx-js/react": "^2.3.0",
"@next/mdx": "13.4.16",
"@next/mdx": "14.2.2",
"@stripe/react-stripe-js": "2.4.0",
"@stripe/stripe-js": "2.2.1",
"@tailwindcss/forms": "0.5.7",
"@tailwindcss/typography": "^0.5.4",
"@types/node": "20.4.7",
"@types/ical": "^0.8.3",
"@types/react": "18.2.18",
"@types/react-dom": "18.2.7",
"@types/webpack-env": "^1.18.1",
"autoprefixer": "^10.4.12",
"cheerio": "^1.0.0-rc.12",
"clsx": "^1.2.1",
"dayjs": "^1.11.10",
"fast-glob": "^3.2.11",
"feed": "^4.2.2",
"next": "13.4.16",
"next-themes": "^0.2.1",
"ical": "^0.8.0",
"next": "14.2.2",
"postmark": "4.0.2",
"react": "18.2.0",
"react-dom": "18.2.0",
"remark-gfm": "^3.0.1",
"tailwindcss": "^3.3.3",
"typescript": "5.1.6"
"sharp": "0.32.6",
"stripe": "14.9.0",
"tailwindcss": "3.4.3",
"typescript": "5.4.5"
},
"devDependencies": {
"@types/node": "20.4.7",
"eslint": "8.45.0",
"eslint-config-next": "13.4.16",
"prettier": "^3.0.1",
"prettier-plugin-tailwindcss": "^0.5.2",
"sharp": "^0.32.0"
"prettier-plugin-tailwindcss": "^0.5.2"
}
}
}

Binary file not shown.

BIN
public/WSCC-Hall-Rental-QA.pdf Executable file

Binary file not shown.

BIN
public/WSCC-Membership-Form.pdf Executable file

Binary file not shown.

View File

@@ -0,0 +1,62 @@
import { NextRequest, NextResponse } from 'next/server';
import Stripe from 'stripe';
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY || '');
export async function POST(request: NextRequest): Promise<NextResponse> {
const { amount, payment_intent_id, metadata } = await request.json();
if (payment_intent_id) {
try {
// If a payment_intent_id is passed, retrieve the paymentIntent
const current_intent = await stripe.paymentIntents.retrieve(
payment_intent_id,
);
// If a paymentIntent is retrieved update its amount
if (current_intent) {
const updated_intent = await stripe.paymentIntents.update(
payment_intent_id,
{
amount: amount,
metadata: metadata || {},
},
);
return NextResponse.json(updated_intent, { status: 200 });
}
} catch (e) {
// Catch any error and return a status 500
if ((e as any).code !== 'resource_missing') {
const errorMessage =
e instanceof Error ? e.message : 'Internal server error';
return NextResponse.json(
{ statusCode: 500, message: errorMessage },
{ status: 500 },
);
}
}
}
try {
// Create PaymentIntent
const params: Stripe.PaymentIntentCreateParams = {
amount: amount,
currency: 'usd',
payment_method_types: ['card'],
automatic_payment_methods: {
enabled: false,
},
metadata: metadata || {},
};
const payment_intent = await stripe.paymentIntents.create(params);
//Return the payment_intent object
return NextResponse.json(payment_intent, { status: 200 });
} catch (err) {
const errorMessage =
err instanceof Error ? err.message : 'Internal server error';
return NextResponse.json(
{ statusCode: 500, message: errorMessage },
{ status: 500 },
);
}
}

View File

@@ -0,0 +1,32 @@
import { emailNotification } from '@/lib/email';
import { NextRequest } from 'next/server';
export async function POST(request: NextRequest): Promise<Response> {
const body = await request.json();
if (body.type === 'charge.succeeded') {
const data = body.data.object;
const billing = data.billing_details;
const cityStateZip = `${billing.address.city}, ${billing.address.state} ${billing.address.postal_code}`;
const address = billing.address.line2
? `${billing.address.line1}\n${cityStateZip}`
: `${billing.address.line1}\n${billing.address.line2}\n${cityStateZip}`;
const isDonation = data.metadata.type === 'donation';
emailNotification({
subject: isDonation ? 'New WSCC Donation' : 'New WSCC Membership',
bodyPrefix: isDonation ? 'New donation:' : 'New WSCC member:',
name: billing.name,
type: data.metadata.type,
amount: data.amount,
email: billing.email,
phone: billing.phone,
address,
});
} else {
console.log('Another type of event');
console.log(body);
}
return new Response(null, { status: 200 });
}

View File

@@ -1,91 +0,0 @@
import { ArticleLayout } from '@/components/ArticleLayout'
import Image from 'next/image'
import designSystem from './planetaria-design-system.png'
export const article = {
author: 'Adam Wathan',
date: '2022-09-05',
title: 'Crafting a design system for a multiplanetary future',
description:
'Most companies try to stay ahead of the curve when it comes to visual design, but for Planetaria we needed to create a brand that would still inspire us 100 years from now when humanity has spread across our entire solar system.',
}
export const metadata = {
title: article.title,
description: article.description,
}
export default (props) => <ArticleLayout article={article} {...props} />
Most companies try to stay ahead of the curve when it comes to visual design, but for Planetaria we needed to create a brand that would still inspire us 100 years from now when humanity has spread across our entire solar system.
<Image src={designSystem} alt="" />
I knew that to get it right I was going to have to replicate the viewing conditions of someone from the future, so I grabbed my space helmet from the closet, created a new Figma document, and got to work.
## Sermone fata
Lorem markdownum, bracchia in redibam! Terque unda puppi nec, linguae posterior
in utraque respicere candidus Mimasque formae; quae conantem cervice. Parcite
variatus, redolentia adeunt. Tyrioque dies, naufraga sua adit partibus celanda
torquere temptata, erit maneat et ramos, [iam](#) ait dominari
potitus! Tibi litora matremque fumantia condi radicibus opusque.
Deus feram verumque, fecit, ira tamen, terras per alienae victum. Mutantur
levitate quas ubi arcum ripas oculos abest. Adest [commissaque
victae](#) in gemitus nectareis ire diva
dotibus ora, et findi huic invenit; fatis? Fractaque dare superinposita
nimiumque simulatoremque sanguine, at voce aestibus diu! Quid veterum hausit tu
nil utinam paternos ima, commentaque.
```c
exbibyte_wins = gigahertz(3);
grayscaleUtilityClient = control_uat;
pcmciaHibernate = oop_virus_console(text_mountain);
if (stateWaisFirewire >= -2) {
jfs = 647065 / ldapVrml(tutorialRestore, 85);
metal_runtime_parse = roomComputingResolution - toolbarUpload +
ipx_nvram_open;
} else {
maximizeSidebar *= suffix_url(flatbed + 2, requirements_encoding_node +
only_qbe_media, minicomputer);
}
```
Aere repetiti cognataque natus. Habebat vela solutis saepe munus nondum adhuc
oscula nomina pignora corpus deserat.
## Lethaei Pindumve me quae dinumerat Pavor
Idem se saxa fata pollentibus geminos; quos pedibus. Est urnis Herses omnes nec
divite: et ille illa furit sim verbis Cyllenius.
1. Captus inpleverunt collo
2. Nec nam placebant
3. Siquos vulgus
4. Dictis carissime fugae
5. A tacitos nulla viginti
Ungues fistula annoso, ille addit linoque motatque uberior verso
[rubuerunt](#) confine desuetaque. _Sanguine_ anteit
emerguntque expugnacior est pennas iniqui ecce **haeret** genus: peiora imagine
fossas Cephisos formosa! Refugitque amata [refelli](#)
supplex. Summa brevis vetuere tenebas, hostes vetantis, suppressit, arreptum
regna. Postquam conpescit iuvenis habet corpus, et erratica, perdere, tot mota
ars talis.
```c
digital.webcam_dual_frequency = webmasterMms;
if (5 + language_standalone_google) {
cc_inbox_layout *= file_character;
task += p;
lockUnicode += enterprise_monochrome(tokenFunctionPersonal, keyVirtual,
adf);
}
windows_binary_esports(87734, array(restoreRomTopology, adRaw(407314),
dongleBashThumbnail), interpreter);
```
Sit volat naturam; motu Cancri. Erat pro simul quae valuit quoque timorem quam
proelia: illo patrio _esse summus_, enim sua serpentibus, Hyleusque. Est coniuge
recuso; refert Coroniden ignotos manat, adfectu.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 51 KiB

View File

@@ -1,106 +0,0 @@
import { ArticleLayout } from '@/components/ArticleLayout'
export const article = {
author: 'Adam Wathan',
date: '2022-09-02',
title: 'Introducing Animaginary: High performance web animations',
description:
'When youre building a website for a company as ambitious as Planetaria, you need to make an impression. I wanted people to visit our website and see animations that looked more realistic than reality itself.',
}
export const metadata = {
title: article.title,
description: article.description,
}
export default (props) => <ArticleLayout article={article} {...props} />
When youre building a website for a company as ambitious as Planetaria, you need to make an impression. I wanted people to visit our website and see animations that looked more realistic than reality itself.
To make this possible, we needed to squeeze every drop of performance out of the browser possible. And so Animaginary was born.
```js
import { animate } from '@planetaria/animaginary'
export function MyComponent({ open, children }) {
return (
<animate.div
in={open}
animateFrom="opacity-0 scale-95"
animateTo="opacity-100 scale-100"
duration={350}
>
{children}
</animate.div>
)
}
```
Animaginary is our new web animation library that redefines what you thought was possible on the web. Hand-written in optimized WASM, Animaginary can even animate the `height` property of an element at 60fps.
## Sermone fata
Lorem markdownum, bracchia in redibam! Terque unda puppi nec, linguae posterior
in utraque respicere candidus Mimasque formae; quae conantem cervice. Parcite
variatus, redolentia adeunt. Tyrioque dies, naufraga sua adit partibus celanda
torquere temptata, erit maneat et ramos, [iam](#) ait dominari
potitus! Tibi litora matremque fumantia condi radicibus opusque.
Deus feram verumque, fecit, ira tamen, terras per alienae victum. Mutantur
levitate quas ubi arcum ripas oculos abest. Adest [commissaque
victae](#) in gemitus nectareis ire diva
dotibus ora, et findi huic invenit; fatis? Fractaque dare superinposita
nimiumque simulatoremque sanguine, at voce aestibus diu! Quid veterum hausit tu
nil utinam paternos ima, commentaque.
```c
exbibyte_wins = gigahertz(3);
grayscaleUtilityClient = control_uat;
pcmciaHibernate = oop_virus_console(text_mountain);
if (stateWaisFirewire >= -2) {
jfs = 647065 / ldapVrml(tutorialRestore, 85);
metal_runtime_parse = roomComputingResolution - toolbarUpload +
ipx_nvram_open;
} else {
maximizeSidebar *= suffix_url(flatbed + 2, requirements_encoding_node +
only_qbe_media, minicomputer);
}
```
Aere repetiti cognataque natus. Habebat vela solutis saepe munus nondum adhuc
oscula nomina pignora corpus deserat.
## Lethaei Pindumve me quae dinumerat Pavor
Idem se saxa fata pollentibus geminos; quos pedibus. Est urnis Herses omnes nec
divite: et ille illa furit sim verbis Cyllenius.
1. Captus inpleverunt collo
2. Nec nam placebant
3. Siquos vulgus
4. Dictis carissime fugae
5. A tacitos nulla viginti
Ungues fistula annoso, ille addit linoque motatque uberior verso
[rubuerunt](#) confine desuetaque. _Sanguine_ anteit
emerguntque expugnacior est pennas iniqui ecce **haeret** genus: peiora imagine
fossas Cephisos formosa! Refugitque amata [refelli](#)
supplex. Summa brevis vetuere tenebas, hostes vetantis, suppressit, arreptum
regna. Postquam conpescit iuvenis habet corpus, et erratica, perdere, tot mota
ars talis.
```c
digital.webcam_dual_frequency = webmasterMms;
if (5 + language_standalone_google) {
cc_inbox_layout *= file_character;
task += p;
lockUnicode += enterprise_monochrome(tokenFunctionPersonal, keyVirtual,
adf);
}
windows_binary_esports(87734, array(restoreRomTopology, adRaw(407314),
dongleBashThumbnail), interpreter);
```
Sit volat naturam; motu Cancri. Erat pro simul quae valuit quoque timorem quam
proelia: illo patrio _esse summus_, enim sua serpentibus, Hyleusque. Est coniuge
recuso; refert Coroniden ignotos manat, adfectu.

View File

@@ -1,101 +0,0 @@
import { ArticleLayout } from '@/components/ArticleLayout'
export const article = {
author: 'Adam Wathan',
date: '2022-07-14',
title: 'Rewriting the cosmOS kernel in Rust',
description:
'When we released the first version of cosmOS last year, it was written in Go. Go is a wonderful programming language, but its been a while since Ive seen an article on the front page of Hacker News about rewriting some important tool in Go and I see articles on there about rewriting things in Rust every single week.',
}
export const metadata = {
title: article.title,
description: article.description,
}
export default (props) => <ArticleLayout article={article} {...props} />
When we released the first version of cosmOS last year, it was written in Go. Go is a wonderful programming language with a lot of benefits, but its been a while since Ive seen an article on the front page of Hacker News about rewriting some important tool in Go and I see articles on there about rewriting things in Rust every single week.
```rust
use ferris_says::say;
use std::io::{stdout, BufWriter};
fn main() {
let stdout = stdout();
let message = String::from("Hello fellow hackers");
let width = message.chars().count();
let mut writer = BufWriter::new(stdout.lock());
say(message.as_bytes(), width, &mut writer).unwrap();
}
```
I derive a large amount of my self-worth from whether or not Hacker News is impressed with the work I'm doing, so when I realized this, I cancelled all of our existing projects and started migrating everything to Rust immediately.
## Sermone fata
Lorem markdownum, bracchia in redibam! Terque unda puppi nec, linguae posterior
in utraque respicere candidus Mimasque formae; quae conantem cervice. Parcite
variatus, redolentia adeunt. Tyrioque dies, naufraga sua adit partibus celanda
torquere temptata, erit maneat et ramos, [iam](#) ait dominari
potitus! Tibi litora matremque fumantia condi radicibus opusque.
Deus feram verumque, fecit, ira tamen, terras per alienae victum. Mutantur
levitate quas ubi arcum ripas oculos abest. Adest [commissaque
victae](#) in gemitus nectareis ire diva
dotibus ora, et findi huic invenit; fatis? Fractaque dare superinposita
nimiumque simulatoremque sanguine, at voce aestibus diu! Quid veterum hausit tu
nil utinam paternos ima, commentaque.
```c
exbibyte_wins = gigahertz(3);
grayscaleUtilityClient = control_uat;
pcmciaHibernate = oop_virus_console(text_mountain);
if (stateWaisFirewire >= -2) {
jfs = 647065 / ldapVrml(tutorialRestore, 85);
metal_runtime_parse = roomComputingResolution - toolbarUpload +
ipx_nvram_open;
} else {
maximizeSidebar *= suffix_url(flatbed + 2, requirements_encoding_node +
only_qbe_media, minicomputer);
}
```
Aere repetiti cognataque natus. Habebat vela solutis saepe munus nondum adhuc
oscula nomina pignora corpus deserat.
## Lethaei Pindumve me quae dinumerat Pavor
Idem se saxa fata pollentibus geminos; quos pedibus. Est urnis Herses omnes nec
divite: et ille illa furit sim verbis Cyllenius.
1. Captus inpleverunt collo
2. Nec nam placebant
3. Siquos vulgus
4. Dictis carissime fugae
5. A tacitos nulla viginti
Ungues fistula annoso, ille addit linoque motatque uberior verso
[rubuerunt](#) confine desuetaque. _Sanguine_ anteit
emerguntque expugnacior est pennas iniqui ecce **haeret** genus: peiora imagine
fossas Cephisos formosa! Refugitque amata [refelli](#)
supplex. Summa brevis vetuere tenebas, hostes vetantis, suppressit, arreptum
regna. Postquam conpescit iuvenis habet corpus, et erratica, perdere, tot mota
ars talis.
```c
digital.webcam_dual_frequency = webmasterMms;
if (5 + language_standalone_google) {
cc_inbox_layout *= file_character;
task += p;
lockUnicode += enterprise_monochrome(tokenFunctionPersonal, keyVirtual,
adf);
}
windows_binary_esports(87734, array(restoreRomTopology, adRaw(407314),
dongleBashThumbnail), interpreter);
```
Sit volat naturam; motu Cancri. Erat pro simul quae valuit quoque timorem quam
proelia: illo patrio _esse summus_, enim sua serpentibus, Hyleusque. Est coniuge
recuso; refert Coroniden ignotos manat, adfectu.

View File

@@ -0,0 +1,124 @@
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>
)
}

View File

@@ -0,0 +1,239 @@
'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>
)
}

132
src/app/calendar/page.tsx Normal file
View File

@@ -0,0 +1,132 @@
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>
);
}

View File

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

View File

@@ -1,14 +1,12 @@
import { type Metadata } from 'next'
import Image from 'next/image'
import Link from 'next/link'
import clsx from 'clsx'
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 {
InstagramIcon,
TwitterIcon,
} from '@/components/SocialIcons'
import portraitImage from '@/images/portrait.jpg'
import { Container } from '@/components/Container';
import { EnvelopeIcon, UserGroupIcon } from '@heroicons/react/24/solid';
import interiorEmptyImage from '@/images/photos/interior-empty.jpg';
function SocialLink({
className,
@@ -16,86 +14,113 @@ function SocialLink({
children,
icon: Icon,
}: {
className?: string
href: string
icon: React.ComponentType<{ className?: string }>
children: React.ReactNode
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 dark:text-zinc-200 dark:hover:text-teal-500"
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>
)
}
function MailIcon(props: React.ComponentPropsWithoutRef<'svg'>) {
return (
<svg viewBox="0 0 24 24" aria-hidden="true" {...props}>
<path
fillRule="evenodd"
d="M6 5a3 3 0 0 0-3 3v8a3 3 0 0 0 3 3h12a3 3 0 0 0 3-3V8a3 3 0 0 0-3-3H6Zm.245 2.187a.75.75 0 0 0-.99 1.126l6.25 5.5a.75.75 0 0 0 .99 0l6.25-5.5a.75.75 0 0 0-.99-1.126L12 12.251 6.245 7.187Z"
/>
</svg>
)
);
}
export const metadata: Metadata = {
title: 'History',
description:
'The West Sound Community Club on Orcas Island.',
}
title: 'Community Club',
description: 'The West Sound Community Club on Orcas Island.',
};
// TODO: Replace interiorEmptyImage with a photo from a potluck
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 (
<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:pl-20">
<div className="max-w-xs px-2.5 lg:max-w-none">
<Image
src={portraitImage}
src={interiorEmptyImage}
alt=""
sizes="(min-width: 1024px) 32rem, 20rem"
className="aspect-square rotate-3 rounded-2xl bg-zinc-100 object-cover dark:bg-zinc-800"
className="aspect-square rounded-2xl bg-zinc-100 object-cover"
/>
</div>
</div>
<div className="lg:order-first lg:row-span-2">
<h1 className="text-4xl font-bold tracking-tight text-zinc-800 dark:text-zinc-100 sm:text-5xl">
<h1 className="text-4xl font-bold tracking-tight text-zinc-800 sm:text-5xl">
West Sound Community Club
</h1>
<div className="mt-6 space-y-7 text-base text-zinc-600 dark:text-zinc-400">
<div className="mt-6 space-y-7 text-base text-zinc-600">
<p>
The West Sound Community Club, a nonprofit, tax-exempt corporation
under Section 501(c)3 of the Internal Revenue Code, owns and manages
the Hall. The Club sponsors monthly community potlucks at the Hall
from September through May.
The West Sound Community Club is a group of neighbors and friends.
Together we steward the West Sound Hall, and gather monthly for
potlucks and other community events.
</p>
<p>
Members of the West Sound community, past and present, have
invested time, money, and talent in creating a true old-fashioned
community center. For over 125, the hall has provided continuity
for the residents and guests of the West Sound community. The
current residents continue the commitment to maintain this unique
gathering place out of care and respect for past and future
generations.
</p>
<p>
The West Sound Community Club is a nonprofit, tax-exempt
corporation under Section 501(c)3 of the Internal Revenue Code.
</p>
</div>
</div>
<div className="lg:pl-20">
<ul role="list">
<SocialLink href="#" icon={TwitterIcon}>
Membership Form
</SocialLink>
<SocialLink href="#" icon={InstagramIcon} className="mt-4">
Donations
</SocialLink>
<MailingListSignupForm />
<SocialLink
href="mailto:contact@westsoundhall.org"
icon={MailIcon}
className="mt-8 border-t border-zinc-100 pt-8 dark:border-zinc-700/40"
icon={EnvelopeIcon}
className="mt-8 border-t border-zinc-100 pt-8"
>
contact@westsoundhall.org
</SocialLink>
<SocialLink
href="/board-of-directors"
icon={UserGroupIcon}
className="mt-4"
>
Board of Directors
</SocialLink>
</ul>
</div>
<div>
<h2 className="mb-4 text-2xl font-semibold leading-6 text-gray-900">
Join or Renew your Membership
</h2>
<ClubPayment />
</div>
</div>
</Container>
)
);
}

451
src/app/club/payment.tsx Normal file
View File

@@ -0,0 +1,451 @@
'use client';
import React, { useEffect, useState, FormEvent, useCallback } 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 membershipLevels = [
{ id: 1, title: 'Individual', description: 'One vote', price: 20 },
{ id: 2, title: 'Household', description: 'Two votes', price: 30 },
];
const additionalDonationLevels = [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 [selectedMembershipLevel, setSelectedMembershipLevel] = useState(
membershipLevels[0],
);
const [selectedAdditionalDonation, setSelectedAdditionalDonation] = useState<
number | null
>(null);
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 = selectedMembershipLevel.price;
if (selectedAdditionalDonation && selectedAdditionalDonation !== -1) {
subtotal += selectedAdditionalDonation;
} else if (customAmount !== '') {
try {
subtotal += parseFloat(customAmount);
} catch {
console.error('');
}
}
subtotal = Math.ceil(subtotal * 1.03);
setTotalAmount(subtotal);
fetch('api/stripe_intent', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
amount: subtotal * 100,
payment_intent_id: paymentIntentID,
metadata: {
type: selectedMembershipLevel.title,
},
}),
});
}, [
paymentIntentID,
selectedMembershipLevel,
selectedAdditionalDonation,
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 + '/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"
>
{/* Membership Type */}
<RadioGroup
value={selectedMembershipLevel}
onChange={setSelectedMembershipLevel}
className="space-y-3"
>
<RadioGroup.Label className="text-base font-semibold leading-6 text-gray-900">
Select a membership type
</RadioGroup.Label>
<div className="grid grid-cols-1 gap-y-6 sm:grid-cols-2 sm:gap-x-4">
{membershipLevels.map((membership) => (
<RadioGroup.Option
key={membership.id}
value={membership}
className={({ active }) =>
classNames(
active
? 'border-indigo-600 ring-2 ring-indigo-600'
: 'border-gray-200',
'relative flex cursor-pointer rounded-lg border bg-white p-4 shadow-sm hover:bg-gray-50 focus:outline-none',
)
}
>
{({ checked, active }) => (
<>
<span className="flex flex-1 items-center justify-between ">
<span className="flex flex-col">
<RadioGroup.Label
as="span"
className="block text-sm font-medium text-gray-900"
>
{membership.title}
</RadioGroup.Label>
<RadioGroup.Description
as="span"
className="mt-1 flex items-center text-sm text-gray-500"
>
{membership.description}
</RadioGroup.Description>
</span>
<RadioGroup.Description
as="span"
className="ml-8 text-sm font-medium"
>
<span className="text-gray-900">
${membership.price}
</span>
<span className="text-gray-500">/yr</span>
</RadioGroup.Description>
</span>
<span
className={classNames(
active ? 'border' : 'border-2',
checked ? 'border-indigo-600' : 'border-transparent',
'pointer-events-none absolute -inset-px rounded-lg',
)}
aria-hidden="true"
/>
</>
)}
</RadioGroup.Option>
))}
</div>
</RadioGroup>
{/* Additional donation */}
<RadioGroup
value={selectedAdditionalDonation}
onChange={setSelectedAdditionalDonation}
className="space-y-3"
>
<RadioGroup.Label className="text-base font-semibold leading-6 text-gray-900">
Additional donation
</RadioGroup.Label>
<div className="grid grid-cols-3 gap-3 sm:grid-cols-5">
{additionalDonationLevels.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-12 text-gray-900 ring-1 ring-inset ring-gray-200 placeholder:text-gray-400 focus:ring-1 focus:ring-inset focus:ring-gray-500 sm:text-sm sm:leading-6"
placeholder="Custom"
aria-describedby="price-currency"
/>
<div className="pointer-events-none absolute inset-y-0 right-0 flex items-center pr-3">
<span
className="text-gray-500 sm:text-sm"
id="price-currency"
>
/yr
</span>
</div>
</div>
</RadioGroup.Label>
) : (
<RadioGroup.Label as="span">
<span>${option}</span>
<span className="text-gray-500">/yr</span>
</RadioGroup.Label>
)}
</RadioGroup.Option>
))}
</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>
<div className="mt-1 text-sm text-gray-500">
Credit card fees included. If you would like to avoid these fees or
to pay by cash or check, please instead{' '}
<a
className="mx-1
underline"
href="/WSCC-Membership-Form.pdf"
>
fill out a paper form
</a>{' '}
and mail to the address on the form.
</div>
<PaymentElement id="payment-element" />
</div>
{/* TODO: Automatically renew toggle? */}
<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 ClubPayment() {
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>
)}
</>
);
}

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

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

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

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

View File

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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 15 KiB

View File

@@ -2,6 +2,18 @@ import assert from 'assert';
import * as cheerio from 'cheerio';
import { Feed } from 'feed';
interface Author {
name: string;
email: string;
}
const authors: Record<string, Author> = {
'Tony Grosinger': {
name: 'Tony Grosinger',
email: 'tony@grosinger.net',
},
};
export async function GET(req: Request) {
let siteUrl = process.env.NEXT_PUBLIC_SITE_URL;
@@ -9,15 +21,10 @@ export async function GET(req: Request) {
throw Error('Missing NEXT_PUBLIC_SITE_URL environment variable');
}
let author = {
name: 'Spencer Sharp',
email: 'spencer@planetaria.tech',
};
let feed = new Feed({
title: author.name,
description: 'Your blog description',
author,
title: 'West Sound Community Hall',
description:
'History, Announcements, and more from the West Sound Hall and Community Club.',
id: siteUrl,
link: siteUrl,
image: `${siteUrl}/favicon.ico`,
@@ -29,20 +36,21 @@ export async function GET(req: Request) {
});
let articleIds = require
.context('../blog', true, /\/page\.mdx$/)
.context('../news', true, /\/page\.mdx$/)
.keys()
.filter((key) => key.startsWith('./'))
.map((key) => key.slice(2).replace(/\/page\.mdx$/, ''));
for (let id of articleIds) {
let url = String(new URL(`/blog/${id}`, req.url));
let url = String(new URL(`/news/${id}`, req.url));
let html = await (await fetch(url)).text();
let $ = cheerio.load(html);
let publicUrl = `${siteUrl}/blog/${id}`;
let publicUrl = `${siteUrl}/news/${id}`;
let article = $('article').first();
let title = article.find('h1').first().text();
let date = article.find('time').first().attr('datetime');
let author = article.find('#byline').first().text().slice(3); // Remove "by " from beginning
let content = article.find('[data-mdx-content]').first().html();
assert(typeof title === 'string');
@@ -54,8 +62,8 @@ export async function GET(req: Request) {
id: publicUrl,
link: publicUrl,
content,
author: [author],
contributor: [author],
author: [authors[author]],
contributor: [authors[author]],
date: new Date(date),
});
}

View File

@@ -1,17 +1,12 @@
import { type Metadata } from 'next'
import Image from 'next/image'
import Link from 'next/link'
import clsx from 'clsx'
import { Card } from '@/components/Card'
import { type Metadata } from 'next';
import Image from 'next/image';
import Link from 'next/link';
import clsx from 'clsx';
import { Card } from '@/components/Card';
import { Container } from '@/components/Container'
import {
GitHubIcon,
InstagramIcon,
LinkedInIcon,
TwitterIcon,
} from '@/components/SocialIcons'
import portraitImage from '@/images/portrait.jpg'
import { Container } from '@/components/Container';
import originalDeedImage from '@/images/original-deed.png';
import { EnvelopeIcon } from '@heroicons/react/24/solid';
function SocialLink({
className,
@@ -19,33 +14,22 @@ function SocialLink({
children,
icon: Icon,
}: {
className?: string
href: string
icon: React.ComponentType<{ className?: string }>
children: React.ReactNode
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 dark:text-zinc-200 dark:hover:text-teal-500"
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>
)
}
function MailIcon(props: React.ComponentPropsWithoutRef<'svg'>) {
return (
<svg viewBox="0 0 24 24" aria-hidden="true" {...props}>
<path
fillRule="evenodd"
d="M6 5a3 3 0 0 0-3 3v8a3 3 0 0 0 3 3h12a3 3 0 0 0 3-3V8a3 3 0 0 0-3-3H6Zm.245 2.187a.75.75 0 0 0-.99 1.126l6.25 5.5a.75.75 0 0 0 .99 0l6.25-5.5a.75.75 0 0 0-.99-1.126L12 12.251 6.245 7.187Z"
/>
</svg>
)
);
}
function TimelineEntry({
@@ -54,51 +38,41 @@ function TimelineEntry({
event,
cta,
href,
children
children,
}: {
title: string
description: string
event?: string
cta?: string
href?: string
children?: React.ReactNode
title: string;
description: string;
event?: string;
cta?: string;
href?: string;
children?: React.ReactNode;
}) {
return (
<Card as="article">
<Card.Title as="h3" href={href}>
{title}
</Card.Title>
{event
? <Card.Eyebrow decorate>{event}</Card.Eyebrow>
: null}
{event ? <Card.Eyebrow decorate>{event}</Card.Eyebrow> : null}
<Card.Description>{description}</Card.Description>
{cta
? <Card.Cta>{cta}</Card.Cta>
: null}
{cta ? <Card.Cta>{cta}</Card.Cta> : null}
{children}
</Card>
)
);
}
function DateListItem({
year, value
}: {
year?: string
value: string
}) {
function DateListItem({ year, value }: { year?: string; value: string }) {
return (
<div className='flex space-x-4'>
<div className='w-10'>{year}</div>
<div className="flex space-x-4">
<div className="w-10">{year}</div>
<div>{value}</div>
</div>
)
);
}
export const metadata: Metadata = {
title: 'History',
description:
'The history of the West Sound Community Hall on Orcas Island.',
}
title: 'Hall History',
description: 'The history of the West Sound Community Hall on Orcas Island.',
};
export default function About() {
return (
@@ -107,29 +81,32 @@ export default function About() {
<div className="lg:pl-20">
<div className="max-w-xs px-2.5 lg:max-w-none">
<Image
src={portraitImage}
src={originalDeedImage}
alt=""
sizes="(min-width: 1024px) 32rem, 20rem"
className="aspect-square rotate-3 rounded-2xl bg-zinc-100 object-cover dark:bg-zinc-800"
className="rotate-3 rounded-2xl bg-zinc-100 object-cover"
/>
<p className="text-sm text-zinc-400">
Original property deed from Alexander Chalmers.
</p>
</div>
</div>
<div className="lg:order-first lg:row-span-2">
<h1 className="text-4xl font-bold tracking-tight text-zinc-800 dark:text-zinc-100 sm:text-5xl">
<h1 className="text-4xl font-bold tracking-tight text-zinc-800 sm:text-5xl">
Hall History
</h1>
<div className="mt-6 space-y-7 text-base text-zinc-600 dark:text-zinc-400">
<div className="mt-6 space-y-7 text-base text-zinc-600">
<p>
The West Sound Community Hall represents the history and character
of Orcas Island. Members of the nonprofit West Sound Community Club,
as stewards of the Hall, strive to maintain the integrity of the
Hall as a unique gathering place for future generations.
of Orcas Island. Members of the nonprofit West Sound Community
Club, as stewards of the Hall, strive to maintain the integrity of
the Hall as a unique gathering place for future generations.
</p>
<p>
In 1902 volunteers began building the Hall with materials supplied by George
Adkins. The building site was donated two years earlier by Alexander Chalmers.
The Hall was erected to serve as a central meeting place for residents of the
West Sound area.
In 1902 volunteers began building the Hall with materials supplied
by George Adkins. The building site was donated two years earlier
by Alexander Chalmers. The Hall was erected to serve as a central
meeting place for residents of the West Sound area.
</p>
<p>
Over the years the original one-room schoolhouse design has
@@ -143,7 +120,7 @@ export default function About() {
</p>
</div>
<div className="mt-8 space-y-8">
<h2 className="text-2xl font-bold tracking-tight text-zinc-800 dark:text-zinc-100 sm:text-5xl">
<h2 className="text-2xl font-bold tracking-tight text-zinc-800 sm:text-5xl">
Detailed History
</h2>
@@ -156,140 +133,112 @@ 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.'
/>
<TimelineEntry
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.'
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."
/>
<TimelineEntry
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.'
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."
/>
<TimelineEntry
title='1903 - 1935'
description='During this period the following organizations were regular users of the Hall:'
title="1903 - 1935"
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 dark:text-zinc-400">
<div className="relative z-10 ml-4 mt-2 text-sm text-zinc-600">
<DateListItem
year='1903'
value='Women&apos;s Christian Temperance Union Woodmen Lodge'
year="1903"
value="Women's Christian Temperance Union Woodmen Lodge"
/>
<DateListItem value="West Sound Literary Society" />
<DateListItem year="1911" value="West Sound Boy's Band" />
<DateListItem
value='West Sound Literary Society'
/>
<DateListItem
year='1911'
value='West Sound Boy&apos;s Band'
/>
<DateListItem
year='1912'
value='West Sound Grange (130 members in 1913)'
/>
<DateListItem
value='Odd Fellows'
/>
<DateListItem
value='West Sound Athletic Club'
/>
<DateListItem
year='1913'
value='West Sound Orchestra'
/>
<DateListItem
value='West Sound Baseball Team'
/>
<DateListItem
year='1922'
value='Farm Bureau'
/>
<DateListItem
year='1925'
value='Fidelis Circle'
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" />
</div>
</TimelineEntry>
<TimelineEntry
title='1905 - 1910'
description='Sometime during this period the Hall was increased in size by adding twelve feet to the south end.'
title="1905 - 1910"
description="Sometime during this period the Hall was increased in size by adding twelve feet to the south end."
/>
<TimelineEntry
title='1935'
description='West Sound Hall Company is reorganized as the West Sound Community Club.'
title="1935"
description="West Sound Hall Company is reorganized as the West Sound Community Club."
/>
<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.'
/>
<TimelineEntry
title='1940&apos;s'
description='During the Second World War, Fidelis Circle cared for and managed the Hall.'
title="1940's"
description="During the Second World War, Fidelis Circle cared for and managed the Hall."
/>
<TimelineEntry
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.'
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."
/>
<TimelineEntry
title='1956'
description='The Orcas Island Yacht Club was founded and began using the Hall as a clubhouse.'
title="1956"
description="The Orcas Island Yacht Club was founded and began using the Hall as a clubhouse."
/>
<TimelineEntry
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.'
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."
/>
<TimelineEntry
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.'
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."
/>
<TimelineEntry
title='1979'
description='The property on which the hall sits was increased in size by donation a strip of land surrounding the hall structure.'
title="1979"
description="The property on which the hall sits was increased in size by donation a strip of land surrounding the hall structure."
/>
<TimelineEntry
title='1981'
description='Major foundation repairs of a temporary nature were made.'
title="1981"
description="Major foundation repairs of a temporary nature were made."
/>
<TimelineEntry
title='1996'
description='The entry stairs and deck were rebuilt.'
title="1996"
description="The entry stairs and deck were rebuilt."
/>
<TimelineEntry
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.'
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."
/>
<TimelineEntry
title='1999 - October 18th'
description='The West Sound Community Hall was listed in the Washington Heritage Register.'
title="1999 - October 18th"
description="The West Sound Community Hall was listed in the Washington Heritage Register."
/>
<TimelineEntry
title='2000 - June 12th'
title="2000 - June 12th"
description='The Washington Department of Revenue approved the West Sound Community Club&apos;s application for property tax exemption for the West Sound Community Hall as a "public assembly hall".'
/>
<TimelineEntry
title='2000 - August 31st'
description='West Sound Community Hall web site first launched.'
title="2000 - August 31st"
description="West Sound Community Hall web site first launched."
/>
<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.'
/>
<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 className="lg:pl-20">
<ul role="list">
<SocialLink href="#" icon={TwitterIcon}>
Follow on Twitter
</SocialLink>
<SocialLink href="#" icon={InstagramIcon} className="mt-4">
Follow on Instagram
</SocialLink>
<SocialLink href="#" icon={GitHubIcon} className="mt-4">
Follow on GitHub
</SocialLink>
<SocialLink href="#" icon={LinkedInIcon} className="mt-4">
Follow on LinkedIn
</SocialLink>
<SocialLink
href="mailto:contact@westsoundhall.org"
icon={MailIcon}
className="mt-8 border-t border-zinc-100 pt-8 dark:border-zinc-700/40"
icon={EnvelopeIcon}
>
contact@westsoundhall.org
</SocialLink>
@@ -297,5 +246,5 @@ export default function About() {
</div>
</div>
</Container>
)
);
}

View File

@@ -7,12 +7,12 @@ import '@/styles/tailwind.css'
export const metadata: Metadata = {
title: {
template: '%s - Spencer Sharp',
template: '%s - West Sound Hall',
default:
'Spencer Sharp - Software designer, founder, and amateur astronaut',
'West Sound Hall - A community space on Orcas Island',
},
description:
'Im Spencer, a software designer and entrepreneur based in New York City. Im the founder and CEO of Planetaria, where we develop technologies that empower regular people to explore space on their own terms.',
'West Sound Community Hall, located at 884 Deer Harbor Road in the hamlet of West Sound, has served as a public assembly hall since it was built by volunteers in 1902.',
alternates: {
types: {
'application/rss+xml': `${process.env.NEXT_PUBLIC_SITE_URL}/feed.xml`,
@@ -27,7 +27,7 @@ export default function RootLayout({
}) {
return (
<html lang="en" className="h-full antialiased" suppressHydrationWarning>
<body className="flex h-full bg-zinc-50 dark:bg-black">
<body className="flex h-full bg-zinc-50">
<Providers>
<div className="flex w-full">
<Layout>{children}</Layout>

Binary file not shown.

After

Width:  |  Height:  |  Size: 307 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 474 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 413 KiB

View File

@@ -0,0 +1,58 @@
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.

View File

@@ -0,0 +1,132 @@
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 were 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. dont 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.

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 MiB

View File

@@ -0,0 +1,54 @@
import { ArticleLayout } from '@/components/ArticleLayout'
import {MdxImage as Image } from '@/components/MdxImage'
import landBankPresentation from './Land_Bank_Presentation.jpg'
export const article = {
author: 'Tony Grosinger',
date: '2024-09-21',
title: 'September 2024 Potluck',
description: "Our first potluck after the summer break, with Board of Director nominations for the election next month.",
}
export const metadata = {
title: article.title,
description: article.description,
}
export default (props) => <ArticleLayout article={article} {...props} />
Summer is wrapping up and the monthly potlucks at the West Sound Hall have
returned! There are several exciting developments to share with you. Here's a
quick update on what was discussed at our recent community potluck:
In October, a contractor will be repairing the north end of the hall using the
funds we raised earlier this year with Give Orcas.
Also in October, we will be electing the new Board of Directors. Our nominating
committee - Cathy and Grant - have announced the slate of candidates. This
year's nominees are:
- Lisa Pedersen for President
- Betsy Wareham for Vice President
- Beth Gasser for Secretary
- Tony Grosinger for Treasurer
- Leslie Brown for Director 1
- Linn Hulley for Director 2
No additional nominations were made from the floor during the meeting. We
encourage all members to join us at the October 19th potluck to vote for the
new board.
<div className="not-prose flex flex-col items-center">
<Image src={landBankPresentation} alt="Lincoln Bormann from the SJC Land Bank presenting at the potluck." />
<span className="mt-2 text-sm text-zinc-400">
Lincoln Bormann from the SJC Land Bank presenting at the potluck.
</span>
</div>
After the potluck dinner, Lincoln Bormann and Brian Wiese from the San Juan
County Land Bank gave a presentation outlining the work they do, how they make
decisions about what land to acquire, and provided updates for newly opening
parks. The Land Bank will be up for election this fall and you can learn more
on their website - https://sjclandbank.org.

Binary file not shown.

After

Width:  |  Height:  |  Size: 239 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 245 KiB

View File

@@ -0,0 +1,44 @@
import { ArticleLayout } from '@/components/ArticleLayout'
import {MdxImage as Image } from '@/components/MdxImage'
import wallUpper from './N_Wall_Upper.jpg'
import wallLower from './N_Wall_Lower.jpg'
export const article = {
author: 'Tony Grosinger',
date: '2024-10-20',
title: 'Election and Building Repairs',
description: "Board of director elections took place, as well as some updates on building maintenance.",
}
export const metadata = {
title: article.title,
description: article.description,
}
export default (props) => <ArticleLayout article={article} {...props} />
At the October potluck we accepted votes for the 2024-2025 Board of Directors. Between those present and votes emailed prior to the potluck, we had enough participants to make a quorum and approve the slate.
There are also several new updates related to hall maintenance. First, repairs to the north wall ar under way! This maintenance work was made possible by generous donations during the Spring Give Orcas campaign where we raised $10,000.
<div className="not-prose flex flex-col items-center">
<Image src={wallUpper} alt="Repairs to the upper portion of the north wall." />
<span className="mt-2 text-sm text-zinc-400">
Repairs to the upper portion of the north wall.
</span>
</div>
<div className="not-prose flex flex-col items-center">
<Image src={wallLower} alt="Repairs to the lower portion of the north wall." />
<span className="mt-2 text-sm text-zinc-400">
Repairs to the lower portion of the north wall.
</span>
</div>
By the end of the month this wall should have brand new siding that will keep out the rain and breeze.
We also learned this month that our well water is not potable. After working with multiple experts and performing several rounds of chlorination treatment of the well and storage tanks, it has been determined that the best route forward is likely to abandon this water system and to move towards joining West Sound Water Association.
There is still lots of discussion and planning necessary before we can even break ground on making the hook up, but every party is working very hard to restore drinkable water to the hall as soon as possible!

View File

@@ -9,7 +9,7 @@ function Article({ article }: { article: BlogPostWithSlug }) {
return (
<article className="md:grid md:grid-cols-4 md:items-baseline">
<Card className="md:col-span-3">
<Card.Title href={`/blog/${article.slug}`}>
<Card.Title href={`/news/${article.slug}`}>
{article.title}
</Card.Title>
<Card.Eyebrow
@@ -35,7 +35,7 @@ function Article({ article }: { article: BlogPostWithSlug }) {
}
export const metadata: Metadata = {
title: 'Blog',
title: 'Club News',
description: 'History, Announcements, and more from the West Sound Hall and Community Club.',
}
@@ -44,10 +44,10 @@ export default async function ArticlesIndex() {
return (
<SimpleLayout
title="West Sound Hall Blog"
title="West Sound Hall News"
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 md:dark:border-zinc-700/40">
<div className="md:border-l md:border-zinc-100 md:pl-6">
<div className="flex max-w-3xl flex-col space-y-16">
{articles.map((article) => (
<Article key={article.slug} article={article} />

View File

@@ -0,0 +1,48 @@
import { ArticleLayout } from '@/components/ArticleLayout'
import {MdxImage as Image } from '@/components/MdxImage'
import originalWebsite from './westsoundhall-original-website.png'
export const article = {
author: 'Tony Grosinger',
date: '2024-01-01',
title: 'Welcome to the New WSCC Website',
description: "In the early 2000's, the West Sound Community Club leadership put a massive effort into improving the hall's physical foundation, however it's easy to overlook the incredible amount of work that was also put into less tangible aspects of the hall.",
}
export const metadata = {
title: article.title,
description: article.description,
}
export default (props) => <ArticleLayout article={article} {...props} />
In the early 2000&apos;s, the West Sound Community Club leadership put a massive
effort into improving the hall&apos;s physical foundation, however it&apos;s
easy to overlook the incredible amount of work that was also put into less
tangible aspects of the hall.
The original version of this website was launched in 2004. It was one of the
first digital archives of the history of West Sound and how central the hall was
to this community. This updated version of the website preserves this historical
collection and aims to expand it with information collected by the Club over the
years.
<div className="not-prose flex flex-col items-center">
<Image src={originalWebsite} alt="Screenshot of the original West Sound Hall website" />
<span className="mt-2 text-sm text-zinc-400">
Screenshot of the original West Sound Hall website.
</span>
</div>
Along with the website, past Boards have worked to incorporate the Club, make
the hall available for rental by the community, and even added the hall to the
WA Heritage Register. There&apos;s so much more to the hall than just potlucks,
and this website hopes to share this with the West Sound Community.
Thanks for being a part of the West Sound Community!
For posterity, the source code for this website along with instructions for
developing and updating it can be found
[here](https://git.grosinger.net/tgrosinger/west-sound-hall).

Binary file not shown.

After

Width:  |  Height:  |  Size: 644 KiB

View File

@@ -5,13 +5,13 @@ export default function NotFound() {
return (
<Container className="flex h-full items-center pt-16 sm:pt-32">
<div className="flex flex-col items-center">
<p className="text-base font-semibold text-zinc-400 dark:text-zinc-500">
<p className="text-base font-semibold text-zinc-400">
404
</p>
<h1 className="mt-4 text-4xl font-bold tracking-tight text-zinc-800 dark:text-zinc-100 sm:text-5xl">
<h1 className="mt-4 text-4xl font-bold tracking-tight text-zinc-800 sm:text-5xl">
Page not found
</h1>
<p className="mt-4 text-base text-zinc-600 dark:text-zinc-400">
<p className="mt-4 text-base text-zinc-600">
Sorry, we couldnt find the page youre looking for.
</p>
<Button href="/" variant="secondary" className="mt-4">

View File

@@ -1,311 +1,158 @@
import Image, { type ImageProps } from 'next/image'
import Link from 'next/link'
import clsx from 'clsx'
import Image from 'next/image';
import { Button } from '@/components/Button'
import { Card } from '@/components/Card'
import { Container } from '@/components/Container'
import {
GitHubIcon,
InstagramIcon,
LinkedInIcon,
TwitterIcon,
} from '@/components/SocialIcons'
import logoAirbnb from '@/images/logos/airbnb.svg'
import logoFacebook from '@/images/logos/facebook.svg'
import logoPlanetaria from '@/images/logos/planetaria.svg'
import logoStarbucks from '@/images/logos/starbucks.svg'
import image1 from '@/images/photos/image-1.jpg'
import image2 from '@/images/photos/image-2.jpg'
import image3 from '@/images/photos/image-3.jpg'
import image4 from '@/images/photos/image-4.jpg'
import image5 from '@/images/photos/image-5.jpg'
import { type BlogPostWithSlug, getAllBlogPosts } from '@/lib/articles'
import { formatDate } from '@/lib/formatDate'
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 MailIcon(props: React.ComponentPropsWithoutRef<'svg'>) {
function LinkButton({
href,
children,
}: {
href: string;
children: React.ReactNode;
}) {
return (
<svg
viewBox="0 0 24 24"
fill="none"
strokeWidth="1.5"
strokeLinecap="round"
strokeLinejoin="round"
aria-hidden="true"
{...props}
<Link
href={href}
className="rounded-md bg-sky-300 px-3 py-2 text-center font-semibold transition hover:bg-sky-400"
>
<path
d="M2.75 7.75a3 3 0 0 1 3-3h12.5a3 3 0 0 1 3 3v8.5a3 3 0 0 1-3 3H5.75a3 3 0 0 1-3-3v-8.5Z"
className="fill-zinc-100 stroke-zinc-400 dark:fill-zinc-100/10 dark:stroke-zinc-500"
/>
<path
d="m4 6 6.024 5.479a2.915 2.915 0 0 0 3.952 0L20 6"
className="stroke-zinc-400 dark:stroke-zinc-500"
/>
</svg>
)
}
function BriefcaseIcon(props: React.ComponentPropsWithoutRef<'svg'>) {
return (
<svg
viewBox="0 0 24 24"
fill="none"
strokeWidth="1.5"
strokeLinecap="round"
strokeLinejoin="round"
aria-hidden="true"
{...props}
>
<path
d="M2.75 9.75a3 3 0 0 1 3-3h12.5a3 3 0 0 1 3 3v8.5a3 3 0 0 1-3 3H5.75a3 3 0 0 1-3-3v-8.5Z"
className="fill-zinc-100 stroke-zinc-400 dark:fill-zinc-100/10 dark:stroke-zinc-500"
/>
<path
d="M3 14.25h6.249c.484 0 .952-.002 1.316.319l.777.682a.996.996 0 0 0 1.316 0l.777-.682c.364-.32.832-.319 1.316-.319H21M8.75 6.5V4.75a2 2 0 0 1 2-2h2.5a2 2 0 0 1 2 2V6.5"
className="stroke-zinc-400 dark:stroke-zinc-500"
/>
</svg>
)
}
function ArrowDownIcon(props: React.ComponentPropsWithoutRef<'svg'>) {
return (
<svg viewBox="0 0 16 16" fill="none" aria-hidden="true" {...props}>
<path
d="M4.75 8.75 8 12.25m0 0 3.25-3.5M8 12.25v-8.5"
strokeWidth="1.5"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
)
{children}
</Link>
);
}
function Article({ article }: { article: BlogPostWithSlug }) {
return (
<Card as="article">
<Card.Title href={`/blog/${article.slug}`}>
{article.title}
</Card.Title>
<Card.Title href={`/news/${article.slug}`}>{article.title}</Card.Title>
<Card.Eyebrow as="time" dateTime={article.date} decorate>
{formatDate(article.date)}
</Card.Eyebrow>
<Card.Description>{article.description}</Card.Description>
<Card.Cta>Read article</Card.Cta>
</Card>
)
);
}
function SocialLink({
icon: Icon,
...props
}: React.ComponentPropsWithoutRef<typeof Link> & {
icon: React.ComponentType<{ className?: string }>
}) {
return (
<Link className="group -m-1 p-1" {...props}>
<Icon className="h-6 w-6 fill-zinc-500 transition group-hover:fill-zinc-600 dark:fill-zinc-400 dark:group-hover:fill-zinc-300" />
</Link>
)
}
function EventListItem({ event }: { event: Event }) {
const start = dayjs(event.start);
const end = dayjs(event.end);
function Newsletter() {
return (
<form
action="/thank-you"
className="rounded-2xl border border-zinc-100 p-6 dark:border-zinc-700/40"
>
<h2 className="flex text-sm font-semibold text-zinc-900 dark:text-zinc-100">
<MailIcon className="h-6 w-6 flex-none" />
<span className="ml-3">Stay up to date</span>
</h2>
<p className="mt-2 text-sm text-zinc-600 dark:text-zinc-400">
Get notified when I publish something new, and unsubscribe at any time.
</p>
<div className="mt-6 flex">
<input
type="email"
placeholder="Email address"
aria-label="Email address"
required
className="min-w-0 flex-auto appearance-none rounded-md border border-zinc-900/10 bg-white px-3 py-[calc(theme(spacing.2)-1px)] shadow-md shadow-zinc-800/5 placeholder:text-zinc-400 focus:border-teal-500 focus:outline-none focus:ring-4 focus:ring-teal-500/10 dark:border-zinc-700 dark:bg-zinc-700/[0.15] dark:text-zinc-200 dark:placeholder:text-zinc-500 dark:focus:border-teal-400 dark:focus:ring-teal-400/10 sm:text-sm"
/>
<Button type="submit" className="ml-4 flex-none">
Join
</Button>
</div>
</form>
)
}
interface Role {
company: string
title: string
logo: ImageProps['src']
start: string | { label: string; dateTime: string }
end: string | { label: string; dateTime: string }
}
function Role({ role }: { role: Role }) {
let startLabel =
typeof role.start === 'string' ? role.start : role.start.label
let startDate =
typeof role.start === 'string' ? role.start : role.start.dateTime
let endLabel = typeof role.end === 'string' ? role.end : role.end.label
let endDate = typeof role.end === 'string' ? role.end : role.end.dateTime
const date = start.format('YYYY-MM-DD');
return (
<li className="flex gap-4">
<div className="relative mt-1 flex h-10 w-10 flex-none items-center justify-center rounded-full 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={role.logo} alt="" className="h-7 w-7" unoptimized />
</div>
<dl className="flex flex-auto flex-wrap gap-x-2">
<dt className="sr-only">Company</dt>
<dd className="w-full flex-none text-sm font-medium text-zinc-900 dark:text-zinc-100">
{role.company}
</dd>
<dt className="sr-only">Role</dt>
<dd className="text-xs text-zinc-500 dark:text-zinc-400">
{role.title}
<dt className="sr-only">Title</dt>
<dd className="w-full flex-none text-sm font-medium text-zinc-900">
{event.name}
</dd>
<dt className="sr-only">Date</dt>
<dd
className="ml-auto text-xs text-zinc-400 dark:text-zinc-500"
aria-label={`${startLabel} until ${endLabel}`}
>
<time dateTime={startDate}>{startLabel}</time>{' '}
<span aria-hidden="true"></span>{' '}
<time dateTime={endDate}>{endLabel}</time>
</dd>
<dd className="text-xs text-zinc-500">{date}</dd>
<dt className="sr-only">Time</dt>
{event.allDay ? (
<dd className="ml-auto text-xs text-zinc-400" aria-label="All day">
All day
</dd>
) : 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')}>
{start.format('h:mm a')}
</time>{' '}
<span aria-hidden="true"></span>{' '}
<time dateTime={end.format('YYYY-MM-DD HH-mm')}>
{end.format('h:mm a')}
</time>{' '}
</dd>
) : (
<dd
className="ml-auto text-xs text-zinc-400"
aria-label={start.format('YYYY-MM-DD HH-mm')}
>
<time dateTime={start.format('YYYY-MM-DD HH-mm')}>
{start.format('h:mm a')}
</time>{' '}
</dd>
)}
</dl>
</li>
)
);
}
function Resume() {
let resume: Array<Role> = [
{
company: 'Planetaria',
title: 'CEO',
logo: logoPlanetaria,
start: '2019',
end: {
label: 'Present',
dateTime: new Date().getFullYear().toString(),
},
},
{
company: 'Airbnb',
title: 'Product Designer',
logo: logoAirbnb,
start: '2014',
end: '2019',
},
{
company: 'Facebook',
title: 'iOS Software Engineer',
logo: logoFacebook,
start: '2011',
end: '2014',
},
{
company: 'Starbucks',
title: 'Shift Supervisor',
logo: logoStarbucks,
start: '2008',
end: '2011',
},
]
async function Events() {
const events = await getUpcomingEvents();
return (
<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 dark:text-zinc-100">
<BriefcaseIcon className="h-6 w-6 flex-none" />
<span className="ml-3">Work</span>
<div className="rounded-2xl border border-zinc-100 p-6">
<h2 className="flex text-sm font-semibold text-zinc-900">
<CalendarDaysIcon className="h-6 w-6 flex-none fill-zinc-100 stroke-zinc-400" />
<span className="ml-3">Upcoming Events</span>
</h2>
<ol className="mt-6 space-y-4">
{resume.map((role, roleIndex) => (
<Role key={roleIndex} role={role} />
{events.map((event, idx) => (
<EventListItem key={idx} event={event} />
))}
</ol>
{/*
<Button href="#" variant="secondary" className="group mt-6 w-full">
Download CV
<ArrowDownIcon className="h-4 w-4 stroke-zinc-400 transition group-active:stroke-zinc-600 dark:group-hover:stroke-zinc-50 dark:group-active:stroke-zinc-50" />
Full Calendar
</Button>
*/}
</div>
)
}
function Photos() {
let rotations = ['rotate-2', '-rotate-2', 'rotate-2', 'rotate-2', '-rotate-2']
return (
<div className="mt-16 sm:mt-20">
<div className="-my-4 flex justify-center gap-5 overflow-hidden py-4 sm:gap-8">
{[image1, image2, image3, image4, image5].map((image, imageIndex) => (
<div
key={image.src}
className={clsx(
'relative aspect-[9/10] w-44 flex-none overflow-hidden rounded-xl bg-zinc-100 dark:bg-zinc-800 sm:w-72 sm:rounded-2xl',
rotations[imageIndex % rotations.length],
)}
>
<Image
src={image}
alt=""
sizes="(min-width: 640px) 18rem, 11rem"
className="absolute inset-0 h-full w-full object-cover"
/>
</div>
))}
</div>
</div>
)
);
}
export default async function Home() {
let articles = (await getAllBlogPosts()).slice(0, 4)
let articles = (await getAllBlogPosts()).slice(0, 4);
return (
<>
<Container className="mt-9">
<div className="max-w-2xl">
<h1 className="text-4xl font-bold tracking-tight text-zinc-800 dark:text-zinc-100 sm:text-5xl">
West Sound Community Hall
</h1>
<p className="mt-6 text-base text-zinc-600 dark:text-zinc-400">
WestSound Community Hall, located at 884 Deer Harbor Road in the hamlet of West Sound, has served as a public assembly hall since it was built by volunteers in 1902.
</p>
<p className="mt-6 text-base text-zinc-600 dark:text-zinc-400">
Facing West Sound, the Hall is at the heart of the WestSound community.
</p>
<div className="mt-6 flex gap-6">
<SocialLink
href="https://twitter.com"
aria-label="Follow on Twitter"
icon={TwitterIcon}
/>
<SocialLink
href="https://instagram.com"
aria-label="Follow on Instagram"
icon={InstagramIcon}
/>
<SocialLink
href="https://github.com"
aria-label="Follow on GitHub"
icon={GitHubIcon}
/>
<SocialLink
href="https://linkedin.com"
aria-label="Follow on LinkedIn"
icon={LinkedInIcon}
<div className="mx-auto mt-24 max-w-7xl px-6 lg:px-8">
<div className="relative px-4 sm:px-8 lg:px-12">
<div className="mx-auto grid max-w-2xl grid-cols-1 gap-y-10 lg:mx-0 lg:max-w-none lg:grid-cols-2 lg:gap-x-8">
<h1 className="text-4xl font-bold tracking-tight text-zinc-800 sm:text-6xl lg:col-span-2">
West Sound Community Hall
</h1>
<div className="max-w-xl">
<p className="mt-6 text-base text-zinc-600">
The West Sound Community Hall is located in the hamlet of West
Sound on Orcas Island, about 10 minutes from the ferry landing
and Eastsound. It has served as a public assembly hall since it
was built by volunteers in 1902.
</p>
<p className="mt-6 text-base text-zinc-600">
Facing West Sound, the Hall is at the heart of the West Sound
community.
</p>
</div>
<Image
src={exteriorFrontImage}
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"
/>
<div className="h-fit max-w-xl rounded-2xl border border-zinc-100 p-6">
<div className="flex flex-col gap-y-4">
<LinkButton href="/club">
Join or Renew your Membership
</LinkButton>
<LinkButton href="/donate">Donate</LinkButton>
<LinkButton href="/rental">Rent the Hall</LinkButton>
</div>
</div>
</div>
</div>
</Container>
<Photos />
</div>
<Container className="mt-24 md:mt-28">
<div className="mx-auto grid max-w-xl grid-cols-1 gap-y-20 lg:max-w-none lg:grid-cols-2">
<div className="flex flex-col gap-16">
@@ -313,12 +160,11 @@ export default async function Home() {
<Article key={article.slug} article={article} />
))}
</div>
<div className="space-y-10 lg:pl-16 xl:pl-24">
<Newsletter />
<Resume />
<div className="lg:pl-16 xl:pl-24">
<Events />
</div>
</div>
</Container>
</>
)
);
}

View File

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

View File

@@ -2,7 +2,6 @@
import { createContext, useEffect, useRef } from 'react'
import { usePathname } from 'next/navigation'
import { ThemeProvider, useTheme } from 'next-themes'
function usePrevious<T>(value: T) {
let ref = useRef<T>()
@@ -14,30 +13,6 @@ function usePrevious<T>(value: T) {
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 function Providers({ children }: { children: React.ReactNode }) {
@@ -46,10 +21,7 @@ export function Providers({ children }: { children: React.ReactNode }) {
return (
<AppContext.Provider value={{ previousPathname }}>
<ThemeProvider attribute="class" disableTransitionOnChange>
<ThemeWatcher />
{children}
</ThemeProvider>
{children}
</AppContext.Provider>
)
}

View File

@@ -4,13 +4,10 @@ import Link from 'next/link'
import clsx from 'clsx'
import { Container } from '@/components/Container'
import {
GitHubIcon,
InstagramIcon,
LinkedInIcon,
TwitterIcon,
} from '@/components/SocialIcons'
import portraitImage from '@/images/portrait.jpg'
import exteriorFront from '@/images/photos/exterior-front.png'
import { EnvelopeIcon, PencilSquareIcon, QuestionMarkCircleIcon } from '@heroicons/react/24/solid'
import React from 'react'
import { TableCell, TableHeading, TableLeftHeading } from '@/components/Table'
function SocialLink({
className,
@@ -27,7 +24,7 @@ function SocialLink({
<li className={clsx(className, 'flex')}>
<Link
href={href}
className="group flex text-sm font-medium text-zinc-800 transition hover:text-teal-500 dark:text-zinc-200 dark:hover:text-teal-500"
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>
@@ -36,21 +33,10 @@ function SocialLink({
)
}
function MailIcon(props: React.ComponentPropsWithoutRef<'svg'>) {
return (
<svg viewBox="0 0 24 24" aria-hidden="true" {...props}>
<path
fillRule="evenodd"
d="M6 5a3 3 0 0 0-3 3v8a3 3 0 0 0 3 3h12a3 3 0 0 0 3-3V8a3 3 0 0 0-3-3H6Zm.245 2.187a.75.75 0 0 0-.99 1.126l6.25 5.5a.75.75 0 0 0 .99 0l6.25-5.5a.75.75 0 0 0-.99-1.126L12 12.251 6.245 7.187Z"
/>
</svg>
)
}
export const metadata: Metadata = {
title: 'History',
title: 'Hall Rental',
description:
'The history of the West Sound Community Hall on Orcas Island.',
'Rental information for the West Sound Hall.',
}
export default function Rental() {
@@ -60,20 +46,20 @@ export default function Rental() {
<div className="lg:pl-20">
<div className="max-w-xs px-2.5 lg:max-w-none">
<Image
src={portraitImage}
src={exteriorFront}
alt=""
sizes="(min-width: 1024px) 32rem, 20rem"
className="aspect-square rotate-3 rounded-2xl bg-zinc-100 object-cover dark:bg-zinc-800"
className="aspect-square rounded-2xl bg-zinc-100 object-cover"
/>
</div>
</div>
<div className="lg:order-first lg:row-span-2">
<h1 className="text-4xl font-bold tracking-tight text-zinc-800 dark:text-zinc-100 sm:text-5xl">
Hall Rental
<h1 className="text-4xl font-bold tracking-tight text-zinc-800 sm:text-5xl">
West Sound Hall Rental
</h1>
<div className="mt-6 space-y-7 text-base text-zinc-600 dark:text-zinc-400">
<div className="mt-6 space-y-7 text-base text-zinc-600">
<p>
The WestSound 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,
the Hall was listed on the Washington Heritage Register. The West
Sound Community Club, as steward of the Hall, makes the Hall
@@ -82,7 +68,7 @@ export default function Rental() {
</p>
<p>
Rental and deposit fees must be paid prior to obtaining access for
set-up. Rental fees include use of the meeting room, projector
set-up. Rental fees include use of the meeting room, projector
and stage, chairs and tables, kitchen, rest rooms, water, and
lights. It does not include trash removal. Cleanup of the Hall
including removal of trash from the property the same day as the
@@ -94,25 +80,192 @@ export default function Rental() {
damage to the Hall or cleaning is necessary, deductions will be
made at the discretion of the Board.
</p>
<p>
Please check the
<Link href="/calendar" className="pl-1 text-blue-600 hover:underline"
>calendar
</Link> for availability.
</p>
</div>
</div>
<div className="lg:pl-20">
<div className="order-last sm:order-none lg:pl-20">
<ul role="list">
<SocialLink href="#" icon={TwitterIcon}>
<SocialLink href="/WSCC-Hall-Rental-Application.pdf" icon={PencilSquareIcon}>
Rental Application Form
</SocialLink>
<SocialLink href="#" icon={InstagramIcon} className="mt-4">
<SocialLink href='/WSCC-Hall-Rental-QA.pdf' icon={QuestionMarkCircleIcon} className="mt-4">
Rental Q&A
</SocialLink>
<SocialLink
href="mailto:contact@westsoundhall.org"
icon={MailIcon}
className="mt-8 border-t border-zinc-100 pt-8 dark:border-zinc-700/40"
icon={EnvelopeIcon}
className="mt-8 border-t border-zinc-100 pt-8"
>
contact@westsoundhall.org
</SocialLink>
</ul>
</div>
<div className="lg:col-span-2">
<h2 className="text-2xl font-bold tracking-tight text-zinc-800 sm:text-2xl">
Rental Rates
</h2>
<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>
<th scope="col" className="py-3.5 pl-4 pr-3 sm:pl-0"></th>
<TableHeading>Half day (4 hr)</TableHeading>
<TableHeading>All day</TableHeading>
<TableHeading>Deposit</TableHeading>
</tr>
</thead>
<tbody className="divide-y divide-gray-200 ">
<tr>
<TableLeftHeading>Orcas non-profit organizations</TableLeftHeading>
<TableCell>$50 or 2 hours for $30*</TableCell>
<TableCell>$100</TableCell>
<TableCell>None</TableCell>
</tr>
<tr>
<TableLeftHeading>WSCC and OIYC Members</TableLeftHeading>
<TableCell>$50 or 2 hours for $30*</TableCell>
<TableCell>$100</TableCell>
<TableCell>None</TableCell>
</tr>
<tr>
<TableLeftHeading>Off-island non-profit organizations</TableLeftHeading>
<TableCell>$50</TableCell>
<TableCell>$100</TableCell>
<TableCell>$300</TableCell>
</tr>
<tr>
<TableLeftHeading>Individuals and non-public use</TableLeftHeading>
<TableCell>$40 per hour</TableCell>
<TableCell>$300</TableCell>
<TableCell>$300</TableCell>
</tr>
<tr>
<TableLeftHeading>Government sponsored activities</TableLeftHeading>
<TableCell>$50</TableCell>
<TableCell>$50</TableCell>
<TableCell>None</TableCell>
</tr>
</tbody>
</table>
</div>
</div>
<div className="mt-6 space-y-5 text-base text-zinc-600">
<h2 className="text-2xl font-bold tracking-tight text-zinc-800 sm:text-2xl">
Use Restrictions
</h2>
<p>
The West Sound Community Hall is a non-smoking facility.
</p>
<p>
The West Sound Community Club Board of Directors reserves the right
to deny use of the West Sound Community Hall to any individual or
organization without exception.
</p>
<p>
The West Sound Community Hall may not be used for either personal or
organizational monetary gain or to promote business activities.
The only exceptions to this restriction are:
</p>
<ol className="list-decimal ml-8 mt-5">
<li>
Use for fund raising activities sponsored by the West Sound
Community Club, a nonprofit corporation, for purposes specified in
Article II of the Bylaws of the West Sound Community Club.
</li>
<li>
Use for fund raising activities sponsored by qualified nonprofit
organizations with prior approval by the President of the West Sound
Community Club. (Such activities must be less than five days in
length and more than 50% of the proceeds must be distributed to the
sponsoring nonprofit organization.)
</li>
</ol>
<p>
Any individual or organizational nonpublic use of the hall which
requires payment of a fee by those attending or offers items for
sale must be approved in advance by the Board of Directors of the
West Sound Community Club.
</p>
<p>
If liquor is to be served, renter is responsible for obtaining the
appropriate license from the
<Link href="https://lcb.wa.gov/"
target="_blank" className="pl-1 text-blue-600 visited:text-purple-600 hover:underline"
>
Washington State Liquor Control Board
</Link>.
</p>
<p>
Use of the Hall is subject to renter completing a Hall Rental
Application which includes an agreement to abide by certain rules
and restrictions.
</p>
<p>
The Hall must be left clean and renter must remove trash from Hall
property on the day of the event.
</p>
</div>
<div className="mt-6 space-y-5 text-base text-zinc-600">
<h2 className="text-2xl font-bold tracking-tight text-zinc-800 sm:text-2xl">
Capacity
</h2>
<p>
Maximum occupancy of the Hall is restricted to 126 people.
Maximum table seating is 84 people.
</p>
</div>
<div className="mt-6 space-y-5 text-base text-zinc-600">
<h2 className="text-2xl font-bold tracking-tight text-zinc-800 sm:text-2xl">
Parking
</h2>
<p>
There is limited public parking available along both sides of Deer
Harbor Road west of the Hall. However, additional parking for Hall
events may be arranged on the grounds of the Orcas Island Yacht Club
picnic shelter located east of the Hall. (Yacht Club events have
priority on the use of this parking area.) When this parking area is
available, use for most nonpublic events requires a $50 donation to
the Yacht Club. Use of the Yacht Club parking area for activities
other than parking, such as catering activities, bonfires and
picnics is not allowed.
</p>
<p>
Contact the Property Manager for details on parking.
</p>
</div>
<div className="mt-6 space-y-5 text-base text-zinc-600">
<h2 className="text-2xl font-bold tracking-tight text-zinc-800 sm:text-2xl">
Accessibility
</h2>
<p>
The Hall is accessible by wheelchair using a ramp on the west side.
The bathrooms are not handicap accessible.
</p>
</div>
</div>
</div>
</Container>
)

View File

@@ -1,98 +0,0 @@
import { type Metadata } from 'next'
import { Card } from '@/components/Card'
import { Section } from '@/components/Section'
import { SimpleLayout } from '@/components/SimpleLayout'
function SpeakingSection({
children,
...props
}: React.ComponentPropsWithoutRef<typeof Section>) {
return (
<Section {...props}>
<div className="space-y-16">{children}</div>
</Section>
)
}
function Appearance({
title,
description,
event,
cta,
href,
}: {
title: string
description: string
event: string
cta: string
href: string
}) {
return (
<Card as="article">
<Card.Title as="h3" href={href}>
{title}
</Card.Title>
<Card.Eyebrow decorate>{event}</Card.Eyebrow>
<Card.Description>{description}</Card.Description>
<Card.Cta>{cta}</Card.Cta>
</Card>
)
}
export const metadata: Metadata = {
title: 'Speaking',
description:
'Ive spoken at events all around the world and been interviewed for many podcasts.',
}
export default function Speaking() {
return (
<SimpleLayout
title="Ive spoken at events all around the world and been interviewed for many podcasts."
intro="One of my favorite ways to share my ideas is live on stage, where theres so much more communication bandwidth than there is in writing, and I love podcast interviews because they give me the opportunity to answer questions instead of just present my opinions."
>
<div className="space-y-20">
<SpeakingSection title="Conferences">
<Appearance
href="#"
title="In space, no one can watch you stream — until now"
description="A technical deep-dive into HelioStream, the real-time streaming library I wrote for transmitting live video back to Earth."
event="SysConf 2021"
cta="Watch video"
/>
<Appearance
href="#"
title="Lessons learned from our first product recall"
description="They say that if youre not embarassed by your first version, youre doing it wrong. Well when youre selling DIY space shuttle kits it turns out its a bit more complicated."
event="Business of Startups 2020"
cta="Watch video"
/>
</SpeakingSection>
<SpeakingSection title="Podcasts">
<Appearance
href="#"
title="Using design as a competitive advantage"
description="How we used world-class visual design to attract a great team, win over customers, and get more press for Planetaria."
event="Encoding Design, July 2022"
cta="Listen to podcast"
/>
<Appearance
href="#"
title="Bootstrapping an aerospace company to $17M ARR"
description="The story of how we built one of the most promising space startups in the world without taking any capital from investors."
event="The Escape Velocity Show, March 2022"
cta="Listen to podcast"
/>
<Appearance
href="#"
title="Programming your company operating system"
description="On the importance of creating systems and processes for running your business so that everyone on the team knows how to make the right decision no matter the situation."
event="How They Work Radio, September 2021"
cta="Listen to podcast"
/>
</SpeakingSection>
</div>
</SimpleLayout>
)
}

View File

@@ -1,17 +1,32 @@
import { type Metadata } from 'next'
import { type Metadata } from 'next';
import { SimpleLayout } from '@/components/SimpleLayout'
import { SimpleLayout } from '@/components/SimpleLayout';
export const metadata: Metadata = {
title: 'Youre subscribed',
description: 'Thanks for subscribing to my newsletter.',
}
title: 'Thank You',
description: 'Thanks for becoming a member.',
};
export default async function ThankYou({
searchParams,
}: {
searchParams: { [key: string]: string | string[] | undefined };
}) {
const { redirect_status } = searchParams;
if (redirect_status !== 'succeeded') {
// TODO: Display error
}
export default function ThankYou() {
return (
<SimpleLayout
title="Thanks for subscribing."
intro="Ill send you an email any time I publish a new blog post, release a new project, or have anything interesting to share that I think youd want to hear about. You can unsubscribe at any time, no hard feelings."
/>
)
title="Thanks for becoming a member."
intro="Thank you for joining 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. We&apos;ll add you to our member mailing
list so you receive announcement emails about upcoming events.
</p>
</SimpleLayout>
);
}

View File

@@ -1,123 +0,0 @@
import { Card } from '@/components/Card'
import { Section } from '@/components/Section'
import { SimpleLayout } from '@/components/SimpleLayout'
function ToolsSection({
children,
...props
}: React.ComponentPropsWithoutRef<typeof Section>) {
return (
<Section {...props}>
<ul role="list" className="space-y-16">
{children}
</ul>
</Section>
)
}
function Tool({
title,
href,
children,
}: {
title: string
href?: string
children: React.ReactNode
}) {
return (
<Card as="li">
<Card.Title as="h3" href={href}>
{title}
</Card.Title>
<Card.Description>{children}</Card.Description>
</Card>
)
}
export const metadata = {
title: 'Uses',
description: 'Software I use, gadgets I love, and other things I recommend.',
}
export default function Uses() {
return (
<SimpleLayout
title="Software I use, gadgets I love, and other things I recommend."
intro="I get asked a lot about the things I use to build software, stay productive, or buy to fool myself into thinking Im being productive when Im really just procrastinating. Heres a big list of all of my favorite stuff."
>
<div className="space-y-20">
<ToolsSection title="Workstation">
<Tool title="16” MacBook Pro, M1 Max, 64GB RAM (2021)">
I was using an Intel-based 16 MacBook Pro prior to this and the
difference is night and day. Ive never heard the fans turn on a
single time, even under the incredibly heavy loads I put it through
with our various launch simulations.
</Tool>
<Tool title="Apple Pro Display XDR (Standard Glass)">
The only display on the market if you want something HiDPI and
bigger than 27. When youre working at planetary scale, every pixel
you can get counts.
</Tool>
<Tool title="IBM Model M SSK Industrial Keyboard">
They dont make keyboards the way they used to. I buy these any time
I see them go up for sale and keep them in storage in case I need
parts or need to retire my main.
</Tool>
<Tool title="Apple Magic Trackpad">
Something about all the gestures makes me feel like a wizard with
special powers. I really like feeling like a wizard with special
powers.
</Tool>
<Tool title="Herman Miller Aeron Chair">
If Im going to slouch in the worst ergonomic position imaginable
all day, I might as well do it in an expensive chair.
</Tool>
</ToolsSection>
<ToolsSection title="Development tools">
<Tool title="Sublime Text 4">
I dont care if its missing all of the fancy IDE features everyone
else relies on, Sublime Text is still the best text editor ever
made.
</Tool>
<Tool title="iTerm2">
Im honestly not even sure what features I get with this that arent
just part of the macOS Terminal but its what I use.
</Tool>
<Tool title="TablePlus">
Great software for working with databases. Has saved me from
building about a thousand admin interfaces for my various projects
over the years.
</Tool>
</ToolsSection>
<ToolsSection title="Design">
<Tool title="Figma">
We started using Figma as just a design tool but now its become our
virtual whiteboard for the entire company. Never would have expected
the collaboration features to be the real hook.
</Tool>
</ToolsSection>
<ToolsSection title="Productivity">
<Tool title="Alfred">
Its not the newest kid on the block but its still the fastest. The
Sublime Text of the application launcher world.
</Tool>
<Tool title="Reflect">
Using a daily notes system instead of trying to keep things
organized by topics has been super powerful for me. And with
Reflect, its still easy for me to keep all of that stuff
discoverable by topic even though all of my writing happens in the
daily note.
</Tool>
<Tool title="SavvyCal">
Great tool for scheduling meetings while protecting my calendar and
making sure I still have lots of time for deep work during the week.
</Tool>
<Tool title="Focus">
Simple tool for blocking distracting websites when I need to just do
the work and get some momentum going.
</Tool>
</ToolsSection>
</div>
</SimpleLayout>
)
}

View File

@@ -41,23 +41,24 @@ export function ArticleLayout({
type="button"
onClick={() => router.back()}
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 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"
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"
>
<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" />
<ArrowLeftIcon className="h-4 w-4 stroke-zinc-500 transition group-hover:stroke-zinc-700" />
</button>
)}
<article>
<header className="flex flex-col">
<h1 className="mt-6 text-4xl font-bold tracking-tight text-zinc-800 dark:text-zinc-100 sm:text-5xl">
<h1 className="mt-6 text-4xl font-bold tracking-tight text-zinc-800 sm:text-5xl">
{article.title}
</h1>
<time
dateTime={article.date}
className="order-first flex items-center text-base text-zinc-400 dark:text-zinc-500"
className="order-first flex items-center text-base text-zinc-400"
>
<span className="h-4 w-0.5 rounded-full bg-zinc-200 dark:bg-zinc-500" />
<span className="h-4 w-0.5 rounded-full bg-zinc-200" />
<span className="ml-3">{formatDate(article.date)}</span>
</time>
<span id="byline" className="mt-4 text-base text-zinc-400">by {article.author}</span>
</header>
<Prose className="mt-8" data-mdx-content>
{children}

View File

@@ -3,17 +3,17 @@ import clsx from 'clsx'
const variantStyles = {
primary:
'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',
'bg-zinc-800 font-semibold text-zinc-100 hover:bg-zinc-700 active:bg-zinc-800 active:text-zinc-100/70',
secondary:
'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',
'bg-zinc-50 font-medium text-zinc-900 hover:bg-zinc-100 active:bg-zinc-100 active:text-zinc-900/60',
}
type ButtonProps = {
variant?: keyof typeof variantStyles
} & (
| (React.ComponentPropsWithoutRef<'button'> & { href?: undefined })
| React.ComponentPropsWithoutRef<typeof Link>
)
| (React.ComponentPropsWithoutRef<'button'> & { href?: undefined })
| React.ComponentPropsWithoutRef<typeof Link>
)
export function Button({
variant = 'primary',

View File

@@ -39,7 +39,7 @@ Card.Link = function CardLink({
}: React.ComponentPropsWithoutRef<typeof Link>) {
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 dark:bg-zinc-800/50 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 sm:-inset-x-6 sm:rounded-2xl" />
<Link {...props}>
<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>
@@ -59,7 +59,7 @@ Card.Title = function CardTitle<T extends React.ElementType = 'h2'>({
let Component = as ?? 'h2'
return (
<Component className="text-base font-semibold tracking-tight text-zinc-800 dark:text-zinc-100">
<Component className="text-base font-semibold tracking-tight text-zinc-800">
{href ? <Card.Link href={href}>{children}</Card.Link> : children}
</Component>
)
@@ -71,7 +71,7 @@ Card.Description = function CardDescription({
children: React.ReactNode
}) {
return (
<p className="relative z-10 mt-2 text-sm text-zinc-600 dark:text-zinc-400">
<p className="relative z-10 mt-2 text-sm text-zinc-600">
{children}
</p>
)
@@ -105,7 +105,7 @@ Card.Eyebrow = function CardEyebrow<T extends React.ElementType = 'p'>({
<Component
className={clsx(
className,
'relative z-10 order-first mb-3 flex items-center text-sm text-zinc-400 dark:text-zinc-500',
"relative z-10 order-first mb-3 flex items-center text-sm text-zinc-400",
decorate && 'pl-3.5',
)}
{...props}
@@ -115,7 +115,7 @@ Card.Eyebrow = function CardEyebrow<T extends React.ElementType = 'p'>({
className="absolute inset-y-0 left-0 flex items-center"
aria-hidden="true"
>
<span className="h-4 w-0.5 rounded-full bg-zinc-200 dark:bg-zinc-500" />
<span className="h-4 w-0.5 rounded-full bg-zinc-200" />
</span>
)}
{children}

View File

@@ -12,7 +12,7 @@ function NavLink({
return (
<Link
href={href}
className="transition hover:text-teal-500 dark:hover:text-teal-400"
className="transition hover:text-teal-500"
>
{children}
</Link>
@@ -23,18 +23,24 @@ export function Footer() {
return (
<footer className="mt-32 flex-none">
<ContainerOuter>
<div className="border-t border-zinc-100 pb-16 pt-10 dark:border-zinc-700/40">
<div className="border-t border-zinc-100 pb-16 pt-10">
<ContainerInner>
<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 dark:text-zinc-200">
<div className="flex flex-wrap justify-center gap-x-6 gap-y-1 text-sm font-medium text-zinc-800">
<NavLink href="/hall-history">History</NavLink>
<NavLink href="/news">News</NavLink>
<NavLink href="/calendar">Calendar</NavLink>
<NavLink href="/rental">Rental</NavLink>
<NavLink href="/club">Club</NavLink>
</div>
<p className="text-sm text-zinc-400 dark:text-zinc-500">
&copy; {new Date().getFullYear()} Spencer Sharp. All rights
reserved.
</p>
<div>
<p className="text-sm text-zinc-400">
&copy; {new Date().getFullYear()} West Sound Community Club. All rights reserved.
</p>
<p className="text-sm text-zinc-400">
WSCC is a 501c3 nonprofit organization - 91-1283768
</p>
</div>
</div>
</ContainerInner>
</div>

View File

@@ -1,76 +1,13 @@
'use client'
import { Fragment, useEffect, useRef, useState } from 'react'
import Image from 'next/image'
import { Fragment, useEffect, useRef } from 'react'
import Link from 'next/link'
import { usePathname } from 'next/navigation'
import { useTheme } from 'next-themes'
import { Popover, Transition } from '@headlessui/react'
import clsx from 'clsx'
import { XMarkIcon, ChevronDownIcon } from '@heroicons/react/24/solid'
import { Container } from '@/components/Container'
import avatarImage from '@/images/avatar.jpg'
function CloseIcon(props: React.ComponentPropsWithoutRef<'svg'>) {
return (
<svg viewBox="0 0 24 24" aria-hidden="true" {...props}>
<path
d="m17.25 6.75-10.5 10.5M6.75 6.75l10.5 10.5"
fill="none"
stroke="currentColor"
strokeWidth="1.5"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
)
}
function ChevronDownIcon(props: React.ComponentPropsWithoutRef<'svg'>) {
return (
<svg viewBox="0 0 8 6" aria-hidden="true" {...props}>
<path
d="M1.75 1.75 4 4.25l2.25-2.5"
fill="none"
strokeWidth="1.5"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
)
}
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({
href,
@@ -93,9 +30,9 @@ function MobileNavigation(
) {
return (
<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 dark:bg-zinc-800/90 dark:text-zinc-200 dark:ring-white/10 dark:hover:ring-white/20">
<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">
Menu
<ChevronDownIcon className="ml-3 h-auto w-2 stroke-zinc-500 group-hover:stroke-zinc-700 dark:group-hover:stroke-zinc-400" />
<ChevronDownIcon className="ml-3 h-auto w-2 stroke-zinc-500 group-hover:stroke-zinc-700" />
</Popover.Button>
<Transition.Root>
<Transition.Child
@@ -107,7 +44,7 @@ function MobileNavigation(
leaveFrom="opacity-100"
leaveTo="opacity-0"
>
<Popover.Overlay className="fixed inset-0 z-50 bg-zinc-800/40 backdrop-blur-sm dark:bg-black/80" />
<Popover.Overlay className="fixed inset-0 z-50 bg-zinc-800/40 backdrop-blur-sm" />
</Transition.Child>
<Transition.Child
as={Fragment}
@@ -120,20 +57,22 @@ function MobileNavigation(
>
<Popover.Panel
focus
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"
className="fixed inset-x-4 top-8 z-50 origin-top rounded-3xl bg-white p-8 ring-1 ring-zinc-900/5"
>
<div className="flex flex-row-reverse items-center justify-between">
<Popover.Button aria-label="Close menu" className="-m-1 p-1">
<CloseIcon className="h-6 w-6 text-zinc-500 dark:text-zinc-400" />
<XMarkIcon className="h-6 w-6 text-zinc-500" />
</Popover.Button>
<h2 className="text-sm font-medium text-zinc-600 dark:text-zinc-400">
<h2 className="text-sm font-medium text-zinc-600">
Navigation
</h2>
</div>
<nav className="mt-6">
<ul className="-my-2 divide-y divide-zinc-100 text-base text-zinc-800 dark:divide-zinc-100/5 dark:text-zinc-300">
<ul className="-my-2 divide-y divide-zinc-100 text-base text-zinc-800">
<MobileNavItem href="/">Home</MobileNavItem>
<MobileNavItem href="/hall-history">Hall History</MobileNavItem>
<MobileNavItem href="/blog">Blog</MobileNavItem>
<MobileNavItem href="/news">News</MobileNavItem>
<MobileNavItem href="/calendar">Calendar</MobileNavItem>
<MobileNavItem href="/rental">Rental</MobileNavItem>
<MobileNavItem href="/club">Club</MobileNavItem>
</ul>
@@ -161,13 +100,13 @@ function NavItem({
className={clsx(
'relative block px-3 py-2 transition',
isActive
? 'text-teal-500 dark:text-teal-400'
: 'hover:text-teal-500 dark:hover:text-teal-400',
? "text-teal-500"
: "hover:text-teal-500",
)}
>
{children}
{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 dark:from-teal-400/0 dark:via-teal-400/40 dark:to-teal-400/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" />
)}
</Link>
</li>
@@ -177,9 +116,11 @@ function NavItem({
function DesktopNavigation(props: React.ComponentPropsWithoutRef<'nav'>) {
return (
<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 dark:bg-zinc-800/90 dark:text-zinc-200 dark:ring-white/10">
<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">
<NavItem href="/">Home</NavItem>
<NavItem href="/hall-history">History</NavItem>
<NavItem href="/blog">Blog</NavItem>
<NavItem href="/news">News</NavItem>
<NavItem href="/calendar">Calendar</NavItem>
<NavItem href="/rental">Rental</NavItem>
<NavItem href="/club">Club</NavItem>
</ul>
@@ -187,86 +128,20 @@ 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) {
let min = Math.min(a, b)
let max = Math.max(a, b)
return Math.min(Math.max(number, min), max)
}
function AvatarContainer({
className,
...props
}: React.ComponentPropsWithoutRef<'div'>) {
return (
<div
className={clsx(
className,
'h-10 w-10 rounded-full bg-white/90 p-0.5 shadow-lg shadow-zinc-800/5 ring-1 ring-zinc-900/5 backdrop-blur dark:bg-zinc-800/90 dark:ring-white/10',
)}
{...props}
/>
)
}
function Avatar({
large = false,
className,
...props
}: Omit<React.ComponentPropsWithoutRef<typeof Link>, 'href'> & {
large?: boolean
}) {
return (
<Link
href="/"
aria-label="Home"
className={clsx(className, 'pointer-events-auto')}
{...props}
>
<Image
src={avatarImage}
alt=""
sizes={large ? '4rem' : '2.25rem'}
className={clsx(
'rounded-full bg-zinc-100 object-cover dark:bg-zinc-800',
large ? 'h-16 w-16' : 'h-9 w-9',
)}
priority
/>
</Link>
)
}
export function Header() {
let isHomePage = usePathname() === '/'
let headerRef = useRef<React.ElementRef<'div'>>(null)
let avatarRef = useRef<React.ElementRef<'div'>>(null)
let isInitial = useRef(true)
useEffect(() => {
let downDelay = avatarRef.current?.offsetTop ?? 0
let downDelay = 0
let upDelay = 64
function setProperty(property: string, value: string) {
@@ -310,48 +185,14 @@ export function Header() {
if (top === 0 && scrollY > 0 && scrollY >= downDelay) {
setProperty('--header-inner-position', 'fixed')
removeProperty('--header-top')
removeProperty('--avatar-top')
} else {
removeProperty('--header-inner-position')
setProperty('--header-top', '0px')
setProperty('--avatar-top', '0px')
}
}
function updateAvatarStyles() {
if (!isHomePage) {
return
}
let fromScale = 1
let toScale = 36 / 64
let fromX = 0
let toX = 2 / 16
let scrollY = downDelay - window.scrollY
let scale = (scrollY * (fromScale - toScale)) / downDelay + toScale
scale = clamp(scale, fromScale, toScale)
let x = (scrollY * (fromX - toX)) / downDelay + toX
x = clamp(x, fromX, toX)
setProperty(
'--avatar-image-transform',
`translate3d(${x}rem, 0, 0) scale(${scale})`,
)
let borderScale = 1 / (toScale / scale)
let borderX = (-toX + x) * borderScale
let borderTransform = `translate3d(${borderX}rem, 0, 0) scale(${borderScale})`
setProperty('--avatar-border-transform', borderTransform)
setProperty('--avatar-border-opacity', scale === toScale ? '1' : '0')
}
function updateStyles() {
updateHeaderStyles()
updateAvatarStyles()
isInitial.current = false
}
@@ -374,44 +215,6 @@ export function Header() {
marginBottom: 'var(--header-mb)',
}}
>
{isHomePage && (
<>
<div
ref={avatarRef}
className="order-last mt-[calc(theme(spacing.16)-theme(spacing.3))]"
/>
<Container
className="top-0 order-last -mb-3 pt-3"
style={{
position:
'var(--header-position)' as React.CSSProperties['position'],
}}
>
<div
className="top-[var(--avatar-top,theme(spacing.3))] w-full"
style={{
position:
'var(--header-inner-position)' as React.CSSProperties['position'],
}}
>
<div className="relative">
<AvatarContainer
className="absolute left-0 top-3 origin-left transition-opacity"
style={{
opacity: 'var(--avatar-border-opacity, 0)',
transform: 'var(--avatar-border-transform)',
}}
/>
<Avatar
large
className="block h-16 w-16 origin-left"
style={{ transform: 'var(--avatar-image-transform)' }}
/>
</div>
</div>
</Container>
</>
)}
<div
ref={headerRef}
className="top-0 z-10 h-16 pt-6"
@@ -427,23 +230,9 @@ export function Header() {
'var(--header-inner-position)' as React.CSSProperties['position'],
}}
>
<div className="relative flex gap-4">
<div className="flex flex-1">
{!isHomePage && (
<AvatarContainer>
<Avatar />
</AvatarContainer>
)}
</div>
<div className="flex flex-1 justify-end md:justify-center">
<MobileNavigation className="pointer-events-auto md:hidden" />
<DesktopNavigation className="pointer-events-auto hidden md:block" />
</div>
<div className="flex justify-end md:flex-1">
<div className="pointer-events-auto">
<ThemeToggle />
</div>
</div>
<div className="flex flex-1 justify-end md:justify-center">
<MobileNavigation className="pointer-events-auto md:hidden" />
<DesktopNavigation className="pointer-events-auto hidden md:block" />
</div>
</Container>
</div>

View File

@@ -6,7 +6,7 @@ export function Layout({ children }: { children: React.ReactNode }) {
<>
<div className="fixed inset-0 flex justify-center sm:px-8">
<div className="flex w-full max-w-7xl lg:px-8">
<div className="w-full bg-white ring-1 ring-zinc-100 dark:bg-zinc-900 dark:ring-zinc-300/20" />
<div className="w-full bg-white ring-1 ring-zinc-100" />
</div>
</div>
<div className="relative flex w-full flex-col">

View File

@@ -0,0 +1,7 @@
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} />;
}

View File

@@ -5,6 +5,6 @@ export function Prose({
...props
}: React.ComponentPropsWithoutRef<'div'>) {
return (
<div className={clsx(className, 'prose dark:prose-invert')} {...props} />
<div className={clsx(className, 'prose')} {...props} />
)
}

View File

@@ -12,12 +12,12 @@ export function Section({
return (
<section
aria-labelledby={id}
className="md:border-l md:border-zinc-100 md:pl-6 md:dark:border-zinc-700/40"
className="md:border-l md:border-zinc-100 md:pl-6"
>
<div className="grid max-w-3xl grid-cols-1 items-baseline gap-y-8 md:grid-cols-4">
<h2
id={id}
className="text-sm font-semibold text-zinc-800 dark:text-zinc-100"
className="text-sm font-semibold text-zinc-800"
>
{title}
</h2>

View File

@@ -12,10 +12,10 @@ export function SimpleLayout({
return (
<Container className="mt-16 sm:mt-32">
<header className="max-w-2xl">
<h1 className="text-4xl font-bold tracking-tight text-zinc-800 dark:text-zinc-100 sm:text-5xl">
<h1 className="text-4xl font-bold tracking-tight text-zinc-800 sm:text-5xl">
{title}
</h1>
<p className="mt-6 text-base text-zinc-600 dark:text-zinc-400">
<p className="mt-6 text-base text-zinc-600">
{intro}
</p>
</header>

View File

@@ -1,36 +0,0 @@
export function TwitterIcon(props: React.ComponentPropsWithoutRef<'svg'>) {
return (
<svg viewBox="0 0 24 24" aria-hidden="true" {...props}>
<path d="M20.055 7.983c.011.174.011.347.011.523 0 5.338-3.92 11.494-11.09 11.494v-.003A10.755 10.755 0 0 1 3 18.186c.308.038.618.057.928.058a7.655 7.655 0 0 0 4.841-1.733c-1.668-.032-3.13-1.16-3.642-2.805a3.753 3.753 0 0 0 1.76-.07C5.07 13.256 3.76 11.6 3.76 9.676v-.05a3.77 3.77 0 0 0 1.77.505C3.816 8.945 3.288 6.583 4.322 4.737c1.98 2.524 4.9 4.058 8.034 4.22a4.137 4.137 0 0 1 1.128-3.86A3.807 3.807 0 0 1 19 5.274a7.657 7.657 0 0 0 2.475-.98c-.29.934-.9 1.729-1.713 2.233A7.54 7.54 0 0 0 22 5.89a8.084 8.084 0 0 1-1.945 2.093Z" />
</svg>
)
}
export function InstagramIcon(props: React.ComponentPropsWithoutRef<'svg'>) {
return (
<svg viewBox="0 0 24 24" aria-hidden="true" {...props}>
<path d="M12 3c-2.444 0-2.75.01-3.71.054-.959.044-1.613.196-2.185.418A4.412 4.412 0 0 0 4.51 4.511c-.5.5-.809 1.002-1.039 1.594-.222.572-.374 1.226-.418 2.184C3.01 9.25 3 9.556 3 12s.01 2.75.054 3.71c.044.959.196 1.613.418 2.185.23.592.538 1.094 1.039 1.595.5.5 1.002.808 1.594 1.038.572.222 1.226.374 2.184.418C9.25 20.99 9.556 21 12 21s2.75-.01 3.71-.054c.959-.044 1.613-.196 2.185-.419a4.412 4.412 0 0 0 1.595-1.038c.5-.5.808-1.002 1.038-1.594.222-.572.374-1.226.418-2.184.044-.96.054-1.267.054-3.711s-.01-2.75-.054-3.71c-.044-.959-.196-1.613-.419-2.185A4.412 4.412 0 0 0 19.49 4.51c-.5-.5-1.002-.809-1.594-1.039-.572-.222-1.226-.374-2.184-.418C14.75 3.01 14.444 3 12 3Zm0 1.622c2.403 0 2.688.009 3.637.052.877.04 1.354.187 1.67.31.421.163.72.358 1.036.673.315.315.51.615.673 1.035.123.317.27.794.31 1.671.043.95.052 1.234.052 3.637s-.009 2.688-.052 3.637c-.04.877-.187 1.354-.31 1.67-.163.421-.358.72-.673 1.036a2.79 2.79 0 0 1-1.035.673c-.317.123-.794.27-1.671.31-.95.043-1.234.052-3.637.052s-2.688-.009-3.637-.052c-.877-.04-1.354-.187-1.67-.31a2.789 2.789 0 0 1-1.036-.673 2.79 2.79 0 0 1-.673-1.035c-.123-.317-.27-.794-.31-1.671-.043-.95-.052-1.234-.052-3.637s.009-2.688.052-3.637c.04-.877.187-1.354.31-1.67.163-.421.358-.72.673-1.036.315-.315.615-.51 1.035-.673.317-.123.794-.27 1.671-.31.95-.043 1.234-.052 3.637-.052Z" />
<path d="M12 15a3 3 0 1 1 0-6 3 3 0 0 1 0 6Zm0-7.622a4.622 4.622 0 1 0 0 9.244 4.622 4.622 0 0 0 0-9.244Zm5.884-.182a1.08 1.08 0 1 1-2.16 0 1.08 1.08 0 0 1 2.16 0Z" />
</svg>
)
}
export function GitHubIcon(props: React.ComponentPropsWithoutRef<'svg'>) {
return (
<svg viewBox="0 0 24 24" aria-hidden="true" {...props}>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M12 2C6.475 2 2 6.588 2 12.253c0 4.537 2.862 8.369 6.838 9.727.5.09.687-.218.687-.487 0-.243-.013-1.05-.013-1.91C7 20.059 6.35 18.957 6.15 18.38c-.113-.295-.6-1.205-1.025-1.448-.35-.192-.85-.667-.013-.68.788-.012 1.35.744 1.538 1.051.9 1.551 2.338 1.116 2.912.846.088-.666.35-1.115.638-1.371-2.225-.256-4.55-1.14-4.55-5.062 0-1.115.387-2.038 1.025-2.756-.1-.256-.45-1.307.1-2.717 0 0 .837-.269 2.75 1.051.8-.23 1.65-.346 2.5-.346.85 0 1.7.115 2.5.346 1.912-1.333 2.75-1.05 2.75-1.05.55 1.409.2 2.46.1 2.716.637.718 1.025 1.628 1.025 2.756 0 3.934-2.337 4.806-4.562 5.062.362.32.675.936.675 1.897 0 1.371-.013 2.473-.013 2.82 0 .268.188.589.688.486a10.039 10.039 0 0 0 4.932-3.74A10.447 10.447 0 0 0 22 12.253C22 6.588 17.525 2 12 2Z"
/>
</svg>
)
}
export function LinkedInIcon(props: React.ComponentPropsWithoutRef<'svg'>) {
return (
<svg viewBox="0 0 24 24" aria-hidden="true" {...props}>
<path d="M18.335 18.339H15.67v-4.177c0-.996-.02-2.278-1.39-2.278-1.389 0-1.601 1.084-1.601 2.205v4.25h-2.666V9.75h2.56v1.17h.035c.358-.674 1.228-1.387 2.528-1.387 2.7 0 3.2 1.778 3.2 4.091v4.715zM7.003 8.575a1.546 1.546 0 01-1.548-1.549 1.548 1.548 0 111.547 1.549zm1.336 9.764H5.666V9.75H8.34v8.589zM19.67 3H4.329C3.593 3 3 3.58 3 4.297v15.406C3 20.42 3.594 21 4.328 21h15.338C20.4 21 21 20.42 21 19.703V4.297C21 3.58 20.4 3 19.666 3h.003z" />
</svg>
)
}

25
src/components/Table.tsx Normal file
View File

@@ -0,0 +1,25 @@
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>
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 237 KiB

BIN
src/images/logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 91 KiB

View File

@@ -1,13 +0,0 @@
<svg width="28" height="28" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect width="28" height="28" rx="14" fill="#FF5A5F" />
<g clip-path="url(#a)">
<path
d="M14.001 18.183c-.902-1.131-1.432-2.122-1.609-2.971-.175-.685-.106-1.232.194-1.643.318-.474.792-.704 1.414-.704.622 0 1.096.23 1.414.708.297.407.372.955.19 1.644-.194.865-.723 1.856-1.608 2.972l.005-.006Zm6.4.76c-.123.831-.689 1.52-1.466 1.856-1.502.653-2.989-.389-4.261-1.803 2.104-2.634 2.493-4.685 1.59-6.012-.53-.76-1.289-1.13-2.263-1.13-1.963 0-3.042 1.66-2.618 3.588.247 1.043.901 2.229 1.945 3.555-.654.723-1.274 1.237-1.822 1.555-.424.23-.83.372-1.218.406-1.786.266-3.186-1.467-2.55-3.253.088-.23.263-.654.563-1.308l.017-.035c.976-2.119 2.161-4.527 3.523-7.197l.035-.088.387-.744c.3-.548.423-.793.9-1.095.231-.14.514-.21.831-.21.636 0 1.132.372 1.344.671.106.16.23.372.388.636l.372.726.054.106c1.36 2.669 2.547 5.072 3.52 7.196l.016.016.356.814.212.509c.162.409.196.815.142 1.239l.004.002Zm.814-1.593c-.124-.389-.337-.847-.6-1.396v-.02a235.28 235.28 0 0 0-3.538-7.23l-.074-.108C16.212 6.974 15.646 6 14.001 6c-1.627 0-2.317 1.13-3.023 2.599l-.054.106a221.31 221.31 0 0 0-3.536 7.232v.035l-.372.813c-.14.336-.212.512-.23.565-.9 2.477.955 4.65 3.201 4.65.018 0 .088 0 .177-.018h.248c1.166-.142 2.37-.883 3.59-2.211 1.218 1.326 2.422 2.069 3.587 2.211h.248c.089.018.16.018.177.018 2.246.002 4.101-2.174 3.201-4.65Z"
fill="#fff" />
</g>
<defs>
<clipPath id="a">
<path fill="#fff" transform="translate(6 6)" d="M0 0h16v16H0z" />
</clipPath>
</defs>
</svg>

Before

Width:  |  Height:  |  Size: 1.5 KiB

View File

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

Before

Width:  |  Height:  |  Size: 625 B

View File

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

Before

Width:  |  Height:  |  Size: 1.5 KiB

View File

@@ -1,18 +0,0 @@
<svg width="28" height="28" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#a)">
<rect width="28" height="28" rx="14" fill="#fff" />
<g clip-path="url(#b)">
<path
d="M29 14.09c0-8.283-6.716-15-15-15-8.284 0-15 6.717-15 15 0 7.488 5.485 13.693 12.656 14.818v-10.48H7.847V14.09h3.81v-3.303c0-3.759 2.24-5.836 5.666-5.836 1.64 0 3.357.294 3.357.294v3.69h-1.892c-1.864 0-2.445 1.157-2.445 2.343v2.813h4.16l-.666 4.337h-3.494V28.91C23.515 27.783 29 21.577 29 14.09Z"
fill="#1877F2" />
</g>
</g>
<defs>
<clipPath id="a">
<rect width="28" height="28" rx="14" fill="#fff" />
</clipPath>
<clipPath id="b">
<path fill="#fff" transform="translate(-1 -1)" d="M0 0h30v30H0z" />
</clipPath>
</defs>
</svg>

Before

Width:  |  Height:  |  Size: 781 B

View File

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

Before

Width:  |  Height:  |  Size: 339 B

View File

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

Before

Width:  |  Height:  |  Size: 1.2 KiB

View File

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

Before

Width:  |  Height:  |  Size: 3.2 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 6.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 81 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.8 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.4 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.7 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 57 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 43 KiB

BIN
src/images/photos/stage.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 54 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 286 KiB

View File

@@ -12,7 +12,7 @@ export interface BlogPostWithSlug extends BlogPost {
}
async function importBlogPost(filename: string): Promise<BlogPostWithSlug> {
let { article } = (await import(`../app/blog/${filename}`)) as {
let { article } = (await import(`../app/news/${filename}`)) as {
default: React.ComponentType;
article: BlogPost;
};
@@ -25,7 +25,7 @@ async function importBlogPost(filename: string): Promise<BlogPostWithSlug> {
export async function getAllBlogPosts() {
let articleFilenames = await glob('*/page.mdx', {
cwd: './src/app/blog',
cwd: './src/app/news',
});
const posts = await Promise.all(articleFilenames.map(importBlogPost));

42
src/lib/email.ts Normal file
View File

@@ -0,0 +1,42 @@
import { ServerClient } from 'postmark';
const fromAddr = process.env.FROM_ADDRESS || '';
const toAddr = process.env.ADMIN_ADDRESS || '';
const serverToken = process.env.POSTMARK_SERVER_TOKEN || '';
const client = new ServerClient(serverToken);
export function emailNotification({
subject,
bodyPrefix,
name,
type,
amount,
email,
phone,
address,
}: {
subject: string;
bodyPrefix: string;
name: string;
type: string;
amount: number;
email: string;
phone: string;
address: string;
}) {
client.sendEmail({
From: fromAddr,
To: toAddr,
Subject: subject,
TextBody: [
bodyPrefix,
'',
'Name: ' + name,
'Type: ' + type,
'Amount: ' + amount,
'Email: ' + email,
'Phone: ' + phone,
'Address: ' + address,
].join('\n'),
});
}

View File

@@ -2,3 +2,10 @@
@import 'tailwindcss/components';
@import './prism.css';
@import 'tailwindcss/utilities';
:root {
/* Your default theme */
--stripe-background: #FFFFFF;
--stripe-foreground: #000000;
}

View File

@@ -1,4 +1,5 @@
import typographyPlugin from '@tailwindcss/typography'
import formsPlugin from '@tailwindcss/forms';
import { type Config } from 'tailwindcss'
import typographyStyles from './typography'
@@ -6,7 +7,10 @@ import typographyStyles from './typography'
export default {
content: ['./src/**/*.{js,jsx,ts,tsx}'],
darkMode: 'class',
plugins: [typographyPlugin],
plugins: [
typographyPlugin,
formsPlugin,
],
theme: {
fontSize: {
xs: ['0.8125rem', { lineHeight: '1.5rem' }],

View File

@@ -170,8 +170,8 @@ export default function typographyStyles({ theme }: PluginUtils) {
paddingLeft: theme('spacing.6'),
},
li: {
marginTop: theme('spacing.6'),
marginBottom: theme('spacing.6'),
// marginTop: theme('spacing.2'),
// marginBottom: theme('spacing.2'),
paddingLeft: theme('spacing[3.5]'),
},
'li::marker': {
@@ -184,14 +184,14 @@ export default function typographyStyles({ theme }: PluginUtils) {
'ul > li::marker': {
color: 'var(--tw-prose-bullets)',
},
'li :is(ol, ul)': {
marginTop: theme('spacing.4'),
marginBottom: theme('spacing.4'),
},
'li :is(li, p)': {
marginTop: theme('spacing.3'),
marginBottom: theme('spacing.3'),
},
// 'li :is(ol, ul)': {
// marginTop: theme('spacing.2'),
// marginBottom: theme('spacing.2'),
// },
// 'li :is(li, p)': {
// marginTop: theme('spacing.3'),
// marginBottom: theme('spacing.3'),
// },
// Code blocks
pre: {