feat: set up announcements page

This commit is contained in:
Naomi Carrigan 2025-01-22 22:40:19 -08:00
parent 1d2f8061fe
commit 9d81ce456b
Signed by: naomi
SSH Key Fingerprint: SHA256:rca1iUI2OhAM6n4FIUaFcZcicmri0jgocqKiTTAfrt8
12 changed files with 2938 additions and 152 deletions

View File

@ -1,16 +1,17 @@
import { dirname } from "path"; import NaomisConfig from "@nhcarrigan/eslint-config";
import { fileURLToPath } from "url";
import { FlatCompat } from "@eslint/eslintrc";
const __filename = fileURLToPath(import.meta.url); export default [
const __dirname = dirname(__filename); ...NaomisConfig,
{
const compat = new FlatCompat({ rules: {
baseDirectory: __dirname, "@typescript-eslint/naming-convention": "off",
}); "unicorn/filename-case": "off",
"max-lines": "off",
const eslintConfig = [ "max-lines-per-function": "off",
...compat.extends("next/core-web-vitals", "next/typescript"), complexity: "off",
"import/no-default-export": "off",
"import/extensions": ["warn", "never"],
"stylistic/max-len": "off",
},
},
]; ];
export default eslintConfig;

View File

@ -1,7 +1,16 @@
import type { NextConfig } from "next"; import type { NextConfig } from "next";
const nextConfig: NextConfig = { const nextConfig: NextConfig = {
/* config options here */ eslint: {
ignoreDuringBuilds: true,
},
images: {
remotePatterns: [
{
hostname: "cdn.nhcarrigan.com",
},
],
},
}; };
export default nextConfig; export default nextConfig;

View File

@ -6,22 +6,26 @@
"dev": "next dev", "dev": "next dev",
"build": "next build", "build": "next build",
"start": "next start", "start": "next start",
"lint": "next lint" "lint": "eslint src --max-warnings 0"
}, },
"dependencies": { "dependencies": {
"gray-matter": "4.0.3",
"next": "15.1.6",
"react": "^19.0.0", "react": "^19.0.0",
"react-dom": "^19.0.0", "react-dom": "^19.0.0",
"next": "15.1.6" "react-markdown": "9.0.3",
"remark-gfm": "4.0.0"
}, },
"devDependencies": { "devDependencies": {
"typescript": "^5", "@eslint/eslintrc": "^3",
"@nhcarrigan/eslint-config": "5.1.0",
"@types/node": "^20", "@types/node": "^20",
"@types/react": "^19", "@types/react": "^19",
"@types/react-dom": "^19", "@types/react-dom": "^19",
"postcss": "^8",
"tailwindcss": "^3.4.1",
"eslint": "^9", "eslint": "^9",
"eslint-config-next": "15.1.6", "eslint-config-next": "15.1.6",
"@eslint/eslintrc": "^3" "postcss": "^8",
"tailwindcss": "^3.4.1",
"typescript": "^5"
} }
} }

2692
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,23 @@
---
title: "Migrating Our Community to Self-Hosted Platforms"
date: "2025-01-22"
summary: "Why we chose to move away from anything and everything that we can't control ourselves."
---
Hey friends,
We've been watching events unfold in our political climate, including tech leaders supporting a president that aims to oppress members of our LGBT+ community. As such, we've made the decision to migrate away from public community platforms and start self-hosting our own instead.
## What does this mean?
Well, there's a few things:
- First and foremost, the bulk of our communication is being moved to our [forum](https://chat.nhcarrigan.com). If you need to get in touch with us for *anything*, this is probably the best place to start. Additionally, conversations here are searchable and indexed by many crawlers.
- Our chat is migrating over to [IRC](https://irc.nhcarrigan.com). To protect the privacy of our community members, we do *not* run a bouncer. Which means anything you send can only be seen by users who are *currently online*. This ephemeral nature makes it nice for informal chatter, but leaves the forum as the best place for long-term conversation.
- For personal updates, we maintain our own [blog](https://blog.nhcarrigan.com). We also have a self-hosted [Sharkey instance](https://fedi.nhcarrigan.com) for smaller posts. Sign-ups are restricted to our family and polycule, but you can connect with us via any Mastodon-compliant instance!
## What if I don't want to join any of those?
That's fine! We'll still maintain a minimal presence on platforms like Discord and Bluesky. But our core focus for building our community is going to be on our self-hosted solutions, where we know our marginalised members are safe and can be protected.
We do hope to see you in our new home on the forum! 💜

Binary file not shown.

Before

Width:  |  Height:  |  Size: 25 KiB

View File

@ -2,20 +2,33 @@
@tailwind components; @tailwind components;
@tailwind utilities; @tailwind utilities;
:root { * {
--background: #ffffff; font-family: "OpenDyslexic", monospace;
--foreground: #171717;
} }
@media (prefers-color-scheme: dark) { h1 {
:root { @apply text-4xl;
--background: #0a0a0a;
--foreground: #ededed;
}
} }
body { h2 {
color: var(--foreground); @apply text-2xl;
background: var(--background); }
font-family: Arial, Helvetica, sans-serif;
a {
@apply underline;
}
li {
@apply list-disc;
@apply list-inside;
@apply text-left;
}
p {
@apply text-justify;
}
@layer utilities {
.text-balance {
text-wrap: balance;
}
} }

View File

@ -1,34 +1,64 @@
/**
* @copyright nhcarrigan
* @license Naomi's Public License
* @author Naomi Carrigan
*/
import { Inter } from "next/font/google";
import Script from "next/script";
import type { Metadata } from "next"; import type { Metadata } from "next";
import { Geist, Geist_Mono } from "next/font/google"; import type { JSX, ReactNode } from "react";
// eslint-disable-next-line import/no-unassigned-import -- Import global styles.
import "./globals.css"; import "./globals.css";
const geistSans = Geist({ // eslint-disable-next-line new-cap -- This is a function call.
variable: "--font-geist-sans", const inter = Inter({ subsets: [ "latin" ] });
subsets: ["latin"],
});
const geistMono = Geist_Mono({ const metadata: Metadata = {
variable: "--font-geist-mono", description:
subsets: ["latin"], "This page tells you everything you could ever want to know about Naomi and her consulting firm nhcarrigan.",
}); openGraph: {
images: "https://cdn.nhcarrigan.com/og-image.png",
export const metadata: Metadata = { },
title: "Create Next App", title: "NHCarrigan Announcements",
description: "Generated by create next app", twitter: {
card: "summary_large_image",
images: "https://cdn.nhcarrigan.com/og-image.png",
site: "@naomi_lgbt",
},
}; };
export default function RootLayout({ /**
* The top-level wrapper for the React application.
* Handles mounting the shadow DOM.
* @param opts - The rendering options.
* @param opts.children - The children elements to render.
* @returns A JSX element.
*/
const RootLayout = ({
children, children,
}: Readonly<{ }: Readonly<{
children: React.ReactNode; children: ReactNode;
}>) { }>): JSX.Element => {
return ( return (
<html lang="en"> <html lang="en">
<body <Script
className={`${geistSans.variable} ${geistMono.variable} antialiased`} async={true}
> defer={true}
src="https://cdn.nhcarrigan.com/headers/index.js"
strategy={"afterInteractive"}
type="text/javascript"
></Script>
<link
href="https://cdn.nhcarrigan.com/logo.png"
rel="icon"
sizes="any"
/>
<body className={inter.className}>
{children} {children}
</body> </body>
</html> </html>
); );
} };
export { metadata };
export default RootLayout;

View File

@ -1,101 +1,26 @@
import Image from "next/image"; /**
* @copyright nhcarrigan
* @license Naomi's Public License
* @author Naomi Carrigan
*/
import { Rule } from "@/components/rule";
import { getSortedPostsData } from "@/lib/posts";
export default function Home() { export default function Home() {
const posts = getSortedPostsData();
return ( return (
<div className="grid grid-rows-[20px_1fr_20px] items-center justify-items-center min-h-screen p-8 pb-20 gap-16 sm:p-20 font-[family-name:var(--font-geist-sans)]"> <main>
<main className="flex flex-col gap-8 row-start-2 items-center sm:items-start"> <h1>{"Announcements"}</h1>
<Image <p>{"This page documents all of our organisation's announcements, in reverse chronological order."}</p>
className="dark:invert" {posts.map((post) => {
src="/next.svg" return <div key={post.slug}>
alt="Next.js logo" <Rule />
width={180} <h2><a className="underline" href={`/post/${post.slug}`}>{post.data.title}</a></h2>
height={38} <p className="italic text-center">{post.data.date.toLocaleDateString("en-GB",{ year: "numeric", month: "long", day: "numeric"})}</p>
priority <p>{post.data.summary}</p>
/> </div>;
<ol className="list-inside list-decimal text-sm text-center sm:text-left font-[family-name:var(--font-geist-mono)]"> })}
<li className="mb-2"> </main>
Get started by editing{" "}
<code className="bg-black/[.05] dark:bg-white/[.06] px-1 py-0.5 rounded font-semibold">
src/app/page.tsx
</code>
.
</li>
<li>Save and see your changes instantly.</li>
</ol>
<div className="flex gap-4 items-center flex-col sm:flex-row">
<a
className="rounded-full border border-solid border-transparent transition-colors flex items-center justify-center bg-foreground text-background gap-2 hover:bg-[#383838] dark:hover:bg-[#ccc] text-sm sm:text-base h-10 sm:h-12 px-4 sm:px-5"
href="https://vercel.com/new?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
target="_blank"
rel="noopener noreferrer"
>
<Image
className="dark:invert"
src="/vercel.svg"
alt="Vercel logomark"
width={20}
height={20}
/>
Deploy now
</a>
<a
className="rounded-full border border-solid border-black/[.08] dark:border-white/[.145] transition-colors flex items-center justify-center hover:bg-[#f2f2f2] dark:hover:bg-[#1a1a1a] hover:border-transparent text-sm sm:text-base h-10 sm:h-12 px-4 sm:px-5 sm:min-w-44"
href="https://nextjs.org/docs?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
target="_blank"
rel="noopener noreferrer"
>
Read our docs
</a>
</div>
</main>
<footer className="row-start-3 flex gap-6 flex-wrap items-center justify-center">
<a
className="flex items-center gap-2 hover:underline hover:underline-offset-4"
href="https://nextjs.org/learn?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
target="_blank"
rel="noopener noreferrer"
>
<Image
aria-hidden
src="/file.svg"
alt="File icon"
width={16}
height={16}
/>
Learn
</a>
<a
className="flex items-center gap-2 hover:underline hover:underline-offset-4"
href="https://vercel.com/templates?framework=next.js&utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
target="_blank"
rel="noopener noreferrer"
>
<Image
aria-hidden
src="/window.svg"
alt="Window icon"
width={16}
height={16}
/>
Examples
</a>
<a
className="flex items-center gap-2 hover:underline hover:underline-offset-4"
href="https://nextjs.org?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
target="_blank"
rel="noopener noreferrer"
>
<Image
aria-hidden
src="/globe.svg"
alt="Globe icon"
width={16}
height={16}
/>
Go to nextjs.org
</a>
</footer>
</div>
); );
} }

View File

@ -0,0 +1,23 @@
import { Rule } from "@/components/rule";
import { getPostData } from "@/lib/posts";
import Markdown from "react-markdown";
import remarkGfm from "remark-gfm";
export default async function Page({
params
}: {
params: Promise<{slug: string}>
}) {
const { slug } = await params;
const post = getPostData(slug);
return (
<main>
<h1>{post.data.title}</h1>
<p className="italic text-center">{`Published ${post.data.date.toLocaleDateString("en-GB", { weekday: "long", year: "numeric", month: "long", day: "numeric"})}`}</p>
<Rule />
<Markdown remarkPlugins={[remarkGfm]}>{post.content}</Markdown>
<Rule />
<a href="/">{"← Back to home"}</a>
</main>
)
}

14
src/components/rule.tsx Normal file
View File

@ -0,0 +1,14 @@
/**
* @copyright nhcarrigan
* @license Naomi's Public License
* @author Naomi Carrigan
*/
import type { JSX } from "react";
/**
* Renders a customised horizontal rule.
* @returns A JSX element.
*/
export const Rule = (): JSX.Element => {
return <hr className="border-dashed border-2 border-[--primary-color]"></hr>;
};

58
src/lib/posts.ts Normal file
View File

@ -0,0 +1,58 @@
/**
* @copyright nhcarrigan
* @license Naomi's Public License
* @author Naomi Carrigan
*/
import { readFileSync, readdirSync } from "node:fs";
import { join } from "node:path";
import matter from "gray-matter";
const postsDirectory = join(process.cwd(), "posts");
export const getSortedPostsData = () => {
const fileNames = readdirSync(postsDirectory);
const allPostsData = fileNames.map((fileName) => {
const slug = fileName.replace(/\.md$/, "");
const fullPath = join(postsDirectory, fileName);
const fileContents = readFileSync(fullPath, "utf8");
const matterResult = matter(fileContents) as unknown as {
content: string;
data: { title: string; date: string; summary: string };
};
return {
slug: slug,
data: {
title: matterResult.data.title,
date: new Date(matterResult.data.date),
summary: matterResult.data.summary,
},
content: matterResult.content,
};
});
return allPostsData.sort((a, b) => {
if (a.data.date < b.data.date) {
return 1;
} else {
return -1;
}
});
};
export const getPostData = (slug: string) => {
const fullPath = join(postsDirectory, slug + ".md");
const fileContents = readFileSync(fullPath, "utf8");
const matterResult = matter(fileContents) as unknown as {
content: string;
data: { title: string; date: string; summary: string };
};
return {
slug: slug,
data: {
title: matterResult.data.title,
date: new Date(matterResult.data.date),
summary: matterResult.data.summary,
},
content: matterResult.content,
};
}