Compare commits

..

No commits in common. "52ec6d7b8169d3e4635141ffe48372d29d938bd0" and "283b5d8a49b6eddb0a35fe26050d008565a43af7" have entirely different histories.

11 changed files with 275 additions and 146 deletions

View File

@ -1,41 +1,10 @@
# West Sound Hall Website # Spotlight
This is the website for the West Sound Hall and Community Club on Orcas Island, WA. Spotlight is a [Tailwind UI](https://tailwindui.com) site template built using [Tailwind CSS](https://tailwindcss.com) and [Next.js](https://nextjs.org).
https://westsoundhall.org ## Getting started
## Running To get started with this template, first install the npm dependencies:
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
The homepage has a list of upcoming events. This list is created from [`src/app/upcoming-events.json`](https://git.grosinger.net/tgrosinger/west-sound-hall/src/branch/main/src/app/upcoming-events.json). To update the events listed, modify that file, tag a new version, and then update the running container to the latest version.
Events in the past will be automatically hidden from view.
### 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 ```bash
npm install npm install
@ -55,8 +24,19 @@ npm run dev
Finally, open [http://localhost:3000](http://localhost:3000) in your browser to view the website. 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 ## License
This site is based off of the Spotlight template from Tailwind, and licensed under the [Tailwind UI license](https://tailwindui.com/license). This site template is a commercial product and is licensed under the [Tailwind UI license](https://tailwindui.com/license).
It was purchased by Tony Grosinger. ## 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

View File

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

View File

@ -59,6 +59,7 @@ function CheckoutForm({
const [selectedAdditionalDonation, setSelectedAdditionalDonation] = useState<number | null>(null); const [selectedAdditionalDonation, setSelectedAdditionalDonation] = useState<number | null>(null);
const [customAmount, setCustomAmount] = useState(''); const [customAmount, setCustomAmount] = useState('');
const [email, setEmail] = useState(''); const [email, setEmail] = useState('');
const [offsetFees, setOffsetFees] = useState(true);
const [totalAmount, setTotalAmount] = useState(300); const [totalAmount, setTotalAmount] = useState(300);
const [message, setMessage] = useState<string>(''); const [message, setMessage] = useState<string>('');
const [isLoading, setIsLoading] = useState(false); const [isLoading, setIsLoading] = useState(false);
@ -110,7 +111,9 @@ function CheckoutForm({
} }
} }
subtotal = Math.ceil(subtotal * 1.03) if (offsetFees) {
subtotal = Math.ceil(subtotal * 1.03)
}
setTotalAmount(subtotal); setTotalAmount(subtotal);
@ -125,7 +128,7 @@ function CheckoutForm({
}, },
}), }),
}); });
}, [paymentIntentID, selectedMembershipLevel, selectedAdditionalDonation, customAmount, email]) }, [paymentIntentID, selectedMembershipLevel, selectedAdditionalDonation, customAmount, offsetFees, email])
const handleSubmit = async (e: FormEvent) => { const handleSubmit = async (e: FormEvent) => {
e.preventDefault(); e.preventDefault();
@ -300,16 +303,33 @@ function CheckoutForm({
Payment Payment
</h2> </h2>
<div className="mt-1 text-sm text-gray-500"> <div className="mt-1 text-sm text-gray-500">
Credit card fees included. If you would like to avoid these fees or If you would like to pay by cash or check, please instead
to pay by cash or check, please instead <a className="underline <a className="underline mx-1" href="/WSCC-Membership-Form.pdf">fill out a paper form</a>
mx-1" href="/WSCC-Membership-Form.pdf">fill out a paper form</a> and and mail to the address on the form.
mail to the address on the form.
</div> </div>
<PaymentElement id="payment-element" /> <PaymentElement id="payment-element" />
</div> </div>
{/* TODO: Automatically renew toggle? */} {/* TODO: Automatically renew toggle? */}
<div className="relative flex gap-x-3">
<div className="flex h-6 items-center">
<input
id="offsetFees"
name="offsetFees"
type="checkbox"
checked={offsetFees}
onChange={(e) => setOffsetFees(e.target.checked)}
className="h-4 w-4 rounded border-gray-300 text-indigo-600 focus:ring-indigo-600"
/>
</div>
<div className="text-sm leading-6">
<label htmlFor="offsetFees" className="font-medium text-gray-900">
Help offset credit card fees
</label>
</div>
</div>
<button <button
className="mt-6 w-full rounded-md flex justify-center border border-transparent bg-indigo-600 px-4 py-2 text-sm font-medium text-white shadow-sm hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2" className="mt-6 w-full rounded-md flex justify-center border border-transparent bg-indigo-600 px-4 py-2 text-sm font-medium text-white shadow-sm hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2"
disabled={isLoading || !stripe || !elements} disabled={isLoading || !stripe || !elements}

View File

@ -36,17 +36,17 @@ export async function GET(req: Request) {
}); });
let articleIds = require let articleIds = require
.context('../news', true, /\/page\.mdx$/) .context('../blog', true, /\/page\.mdx$/)
.keys() .keys()
.filter((key) => key.startsWith('./')) .filter((key) => key.startsWith('./'))
.map((key) => key.slice(2).replace(/\/page\.mdx$/, '')); .map((key) => key.slice(2).replace(/\/page\.mdx$/, ''));
for (let id of articleIds) { for (let id of articleIds) {
let url = String(new URL(`/news/${id}`, req.url)); let url = String(new URL(`/blog/${id}`, req.url));
let html = await (await fetch(url)).text(); let html = await (await fetch(url)).text();
let $ = cheerio.load(html); let $ = cheerio.load(html);
let publicUrl = `${siteUrl}/news/${id}`; let publicUrl = `${siteUrl}/blog/${id}`;
let article = $('article').first(); let article = $('article').first();
let title = article.find('h1').first().text(); let title = article.find('h1').first().text();
let date = article.find('time').first().attr('datetime'); let date = article.find('time').first().attr('datetime');

View File

@ -1,14 +1,17 @@
import Image from 'next/image' import Image from 'next/image'
import clsx from 'clsx'
import Link from 'next/link' import Link from 'next/link'
import { Card } from '@/components/Card' import { Card } from '@/components/Card'
import { Container } from '@/components/Container' import { Container } from '@/components/Container'
import { CalendarDaysIcon } from '@heroicons/react/24/solid' import { CalendarDaysIcon, EnvelopeIcon } from '@heroicons/react/24/solid'
import exteriorFrontImage from '@/images/photos/exterior-front.png' import exteriorFrontImage from '@/images/photos/exterior-front.png'
import stageImage from '@/images/photos/stage.jpg'
import exteriorSoutheastImage from '@/images/photos/exterior-southeast.jpg'
import interorEmptyImage from '@/images/photos/interior-empty.jpg'
import kitchenImage from '@/images/photos/kitchen.jpg'
import { type BlogPostWithSlug, getAllBlogPosts } from '@/lib/articles' import { type BlogPostWithSlug, getAllBlogPosts } from '@/lib/articles'
import { formatDate } from '@/lib/formatDate' import { formatDate } from '@/lib/formatDate'
import { promises as fs } from 'fs';
function LinkButton({ function LinkButton({
href, href,
@ -20,7 +23,9 @@ function LinkButton({
return ( return (
<Link <Link
href={href} href={href}
className="rounded-md px-3 py-2 font-semibold text-center transition dark:hover:text-teal-400 bg-sky-300 hover:bg-sky-400 dark:bg-zinc-700/40 dark:hover:bg-zinc-600/40" className="rounded-md px-3 py-2 font-semibold text-center transition hover:text-teal-500 dark:hover:text-teal-400 bg-zinc-200 hover:bg-zinc-100 dark:bg-zinc-700/40 dark:hover:bg-zinc-600/40"
> >
{children} {children}
</Link> </Link>
@ -30,7 +35,7 @@ function LinkButton({
function Article({ article }: { article: BlogPostWithSlug }) { function Article({ article }: { article: BlogPostWithSlug }) {
return ( return (
<Card as="article"> <Card as="article">
<Card.Title href={`/news/${article.slug}`}> <Card.Title href={`/blog/${article.slug}`}>
{article.title} {article.title}
</Card.Title> </Card.Title>
<Card.Eyebrow as="time" dateTime={article.date} decorate> <Card.Eyebrow as="time" dateTime={article.date} decorate>
@ -42,6 +47,17 @@ function Article({ article }: { article: BlogPostWithSlug }) {
) )
} }
function Newsletter() {
return (
<div className="rounded-2xl border border-zinc-100 p-6 dark:border-zinc-700/40">
<div className="flex flex-col gap-y-4">
<LinkButton href='/club'>Join or Renew your Membership</LinkButton>
<LinkButton href='/rental'>Rent the Hall</LinkButton>
</div>
</div>
)
}
interface Meeting { interface Meeting {
title: string title: string
date: string date: string
@ -84,36 +100,21 @@ function MeetingListItem({ meeting }: { meeting: Meeting }) {
) )
} }
async function Events() { function Events() {
const now = new Date(); let events: Array<Meeting> = [
const nowYear = now.getFullYear(); {
const nowMonth = now.getMonth() + 1; title: 'January Potluck',
const nowDay = now.getDate(); date: '2024-01-20',
startTime: '6:00pm',
const file = await fs.readFile(process.cwd() + '/src/app/upcoming-events.json', 'utf8'); },
const allEvents: Array<Meeting> = JSON.parse(file); {
title: 'February Potluck',
// Remove any events in the past. date: '2024-02-17',
const events = allEvents.filter((e) => { startTime: '6:00pm',
const [year, month, day] = e.date.split('-'); // endTime: '8:00pm',
// notes: 'Bring your own chair.'
const parsedYear = parseInt(year) },
if (parsedYear > nowYear) { ]
return true
} else if (parsedYear < nowYear) {
return false
}
const parsedMonth = parseInt(month)
if (parsedMonth > nowMonth) {
return true
} else if (parsedMonth < nowMonth) {
return false
}
const parsedDay = parseInt(day)
return parsedDay >= nowDay
});
return ( return (
<div className="rounded-2xl border border-zinc-100 p-6 dark:border-zinc-700/40"> <div className="rounded-2xl border border-zinc-100 p-6 dark:border-zinc-700/40">
@ -135,42 +136,55 @@ async function Events() {
) )
} }
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">
{[exteriorSoutheastImage, stageImage, exteriorFrontImage, interorEmptyImage, kitchenImage].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() { export default async function Home() {
let articles = (await getAllBlogPosts()).slice(0, 4) let articles = (await getAllBlogPosts()).slice(0, 4)
return ( return (
<> <>
<div className="mx-auto max-w-7xl px-6 mt-24 lg:px-8"> <Container className="mt-9">
<div className="relative px-4 sm:px-8 lg:px-12"> <div className="max-w-2xl">
<div className="mx-auto max-w-2xl lg:mx-0 grid lg:max-w-none grid-cols-1 lg:grid-cols-2 lg:gap-x-8 gap-y-10"> <h1 className="text-4xl font-bold tracking-tight text-zinc-800 dark:text-zinc-100 sm:text-5xl">
<h1 className="text-4xl font-bold tracking-tight text-gray-900 sm:text-6xl lg:col-span-2"> West Sound Community Hall
West Sound Community Hall </h1>
</h1> <p className="mt-6 text-base text-zinc-600 dark:text-zinc-400">
<div className="max-w-xl"> The West Sound Community Hall is located in the hamlet of West Sound
<p className="mt-6 text-base text-zinc-600 dark:text-zinc-400"> on Orcas Island, about 10 minutes from the ferry landing and
The West Sound Community Hall is located in the hamlet of West Sound Eastsound. It has served as a public assembly hall since it was
on Orcas Island, about 10 minutes from the ferry landing and built by volunteers in 1902.
Eastsound. It has served as a public assembly hall since it was </p>
built by volunteers in 1902. <p className="mt-6 text-base text-zinc-600 dark:text-zinc-400">
</p> Facing West Sound, the Hall is at the heart of the West Sound community.
<p className="mt-6 text-base text-zinc-600 dark:text-zinc-400"> </p>
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="lg:row-span-2 w-full max-w-xl rounded-2xl object-cover lg:max-w-none"
/>
<div className="h-fit max-w-xl rounded-2xl border border-zinc-100 p-6 dark:border-zinc-700/40">
<div className="flex flex-col gap-y-4">
<LinkButton href='/club'>Join or Renew your Membership</LinkButton>
<LinkButton href='/rental'>Rent the Hall</LinkButton>
</div>
</div>
</div>
</div> </div>
</div> </Container>
<Photos />
<Container className="mt-24 md:mt-28"> <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="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"> <div className="flex flex-col gap-16">
@ -178,7 +192,8 @@ export default async function Home() {
<Article key={article.slug} article={article} /> <Article key={article.slug} article={article} />
))} ))}
</div> </div>
<div className="lg:pl-16 xl:pl-24"> <div className="space-y-10 lg:pl-16 xl:pl-24">
<Newsletter />
<Events /> <Events />
</div> </div>
</div> </div>

View File

@ -1,24 +0,0 @@
[
{
"title": "January Potluck",
"date": "2024-01-20",
"startTime": "6:00pm"
},
{
"title": "February Potluck",
"date": "2024-02-17",
"startTime": "6:00pm",
"notes": "With historic West Sound slide show."
},
{
"title": "Town Hall Meeting",
"date": "2024-03-06",
"startTime": "6:00pm",
"endTime": "8:00pm"
},
{
"title": "March Potluck",
"date": "2024-03-16",
"startTime": "6:00pm"
}
]

View File

@ -28,7 +28,6 @@ export function Footer() {
<div className="flex flex-col items-center justify-between gap-6 sm:flex-row"> <div className="flex flex-col items-center justify-between gap-6 sm:flex-row">
<div className="flex flex-wrap justify-center gap-x-6 gap-y-1 text-sm font-medium text-zinc-800 dark:text-zinc-200"> <div className="flex flex-wrap justify-center gap-x-6 gap-y-1 text-sm font-medium text-zinc-800 dark:text-zinc-200">
<NavLink href="/hall-history">History</NavLink> <NavLink href="/hall-history">History</NavLink>
<NavLink href="/news">News</NavLink>
<NavLink href="/rental">Rental</NavLink> <NavLink href="/rental">Rental</NavLink>
<NavLink href="/club">Club</NavLink> <NavLink href="/club">Club</NavLink>
</div> </div>

File diff suppressed because one or more lines are too long

View File

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