Compare commits

...

6 Commits

Author SHA1 Message Date
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
11 changed files with 146 additions and 275 deletions

View File

@ -1,10 +1,41 @@
# 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
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
npm install
@ -24,19 +55,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.

View File

@ -59,7 +59,6 @@ function CheckoutForm({
const [selectedAdditionalDonation, setSelectedAdditionalDonation] = useState<number | null>(null);
const [customAmount, setCustomAmount] = useState('');
const [email, setEmail] = useState('');
const [offsetFees, setOffsetFees] = useState(true);
const [totalAmount, setTotalAmount] = useState(300);
const [message, setMessage] = useState<string>('');
const [isLoading, setIsLoading] = useState(false);
@ -111,9 +110,7 @@ function CheckoutForm({
}
}
if (offsetFees) {
subtotal = Math.ceil(subtotal * 1.03)
}
setTotalAmount(subtotal);
@ -128,7 +125,7 @@ function CheckoutForm({
},
}),
});
}, [paymentIntentID, selectedMembershipLevel, selectedAdditionalDonation, customAmount, offsetFees, email])
}, [paymentIntentID, selectedMembershipLevel, selectedAdditionalDonation, customAmount, email])
const handleSubmit = async (e: FormEvent) => {
e.preventDefault();
@ -303,33 +300,16 @@ function CheckoutForm({
Payment
</h2>
<div className="mt-1 text-sm text-gray-500">
If you would like to pay by cash or check, please instead
<a className="underline mx-1" href="/WSCC-Membership-Form.pdf">fill out a paper form</a>
and mail to the address on the form.
Credit card fees included. If you would like to avoid these fees or
to pay by cash or check, please instead <a className="underline
mx-1" 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? */}
<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
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}

View File

@ -36,17 +36,17 @@ 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');

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,7 +44,7 @@ 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">

View File

@ -1,17 +1,14 @@
import Image from 'next/image'
import clsx from 'clsx'
import Link from 'next/link'
import { Card } from '@/components/Card'
import { Container } from '@/components/Container'
import { CalendarDaysIcon, EnvelopeIcon } from '@heroicons/react/24/solid'
import { CalendarDaysIcon } from '@heroicons/react/24/solid'
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 { formatDate } from '@/lib/formatDate'
import { promises as fs } from 'fs';
function LinkButton({
href,
@ -23,9 +20,7 @@ function LinkButton({
return (
<Link
href={href}
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"
className="rounded-md px-3 py-2 font-semibold text-center transition dark:hover:text-teal-400 bg-sky-300 hover:bg-sky-400 dark:bg-zinc-700/40 dark:hover:bg-zinc-600/40"
>
{children}
</Link>
@ -35,7 +30,7 @@ function LinkButton({
function Article({ article }: { article: BlogPostWithSlug }) {
return (
<Card as="article">
<Card.Title href={`/blog/${article.slug}`}>
<Card.Title href={`/news/${article.slug}`}>
{article.title}
</Card.Title>
<Card.Eyebrow as="time" dateTime={article.date} decorate>
@ -47,17 +42,6 @@ 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 {
title: string
date: string
@ -100,21 +84,36 @@ function MeetingListItem({ meeting }: { meeting: Meeting }) {
)
}
function Events() {
let events: Array<Meeting> = [
{
title: 'January Potluck',
date: '2024-01-20',
startTime: '6:00pm',
},
{
title: 'February Potluck',
date: '2024-02-17',
startTime: '6:00pm',
// endTime: '8:00pm',
// notes: 'Bring your own chair.'
},
]
async function Events() {
const now = new Date();
const nowYear = now.getFullYear();
const nowMonth = now.getMonth() + 1;
const nowDay = now.getDate();
const file = await fs.readFile(process.cwd() + '/src/app/upcoming-events.json', 'utf8');
const allEvents: Array<Meeting> = JSON.parse(file);
// Remove any events in the past.
const events = allEvents.filter((e) => {
const [year, month, day] = e.date.split('-');
const parsedYear = parseInt(year)
if (parsedYear > nowYear) {
return true
} else if (parsedYear < nowYear) {
return false
}
const parsedMonth = parseInt(month)
if (parsedMonth > nowMonth) {
return true
} else if (parsedMonth < nowMonth) {
return false
}
const parsedDay = parseInt(day)
return parsedDay >= nowDay
});
return (
<div className="rounded-2xl border border-zinc-100 p-6 dark:border-zinc-700/40">
@ -136,43 +135,18 @@ 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() {
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">
<div className="mx-auto max-w-7xl px-6 mt-24 lg:px-8">
<div className="relative px-4 sm:px-8 lg:px-12">
<div className="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-gray-900 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 dark:text-zinc-400">
The West Sound Community Hall is located in the hamlet of West Sound
on Orcas Island, about 10 minutes from the ferry landing and
@ -183,8 +157,20 @@ export default async function Home() {
Facing West Sound, the Hall is at the heart of the West Sound community.
</p>
</div>
</Container>
<Photos />
<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>
<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">
@ -192,8 +178,7 @@ export default async function Home() {
<Article key={article.slug} article={article} />
))}
</div>
<div className="space-y-10 lg:pl-16 xl:pl-24">
<Newsletter />
<div className="lg:pl-16 xl:pl-24">
<Events />
</div>
</div>

View File

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

View File

@ -28,6 +28,7 @@ export function Footer() {
<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">
<NavLink href="/hall-history">History</NavLink>
<NavLink href="/news">News</NavLink>
<NavLink href="/rental">Rental</NavLink>
<NavLink href="/club">Club</NavLink>
</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> {
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));