diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c8efa67..8ce5711 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -12,8 +12,6 @@ jobs: - uses: actions/checkout@v4 - uses: pnpm/action-setup@v4 - with: - version: 9 - uses: actions/setup-node@v4 with: diff --git a/README.md b/README.md index fab960b..ff6b56e 100644 --- a/README.md +++ b/README.md @@ -31,7 +31,8 @@ pnpm dev | `pnpm test` | Run tests in watch mode | | `pnpm test:run` | Run tests once (CI) | | `pnpm lint` | Lint with ESLint | -| `pnpm generate:og-image` | Regenerate `public/og-image.png` from SVG | + +The Open Graph image at `/og-image.png` is generated at build time from `public/og-image.svg` and `public/logo.png` via a [static file endpoint](https://docs.astro.build/en/guides/endpoints/#static-file-endpoints) (`src/pages/og-image.png.ts`). ## Environment variables diff --git a/astro.config.mjs b/astro.config.mjs index 832e8d0..8f165be 100644 --- a/astro.config.mjs +++ b/astro.config.mjs @@ -1,6 +1,6 @@ // @ts-check -import { fileURLToPath } from 'node:url'; -import { defineConfig } from 'astro/config'; +import { fileURLToPath, URL } from 'node:url'; +import { defineConfig, envField } from 'astro/config'; import react from '@astrojs/react'; import tailwindcss from '@tailwindcss/vite'; @@ -8,7 +8,21 @@ import tailwindcss from '@tailwindcss/vite'; // Static output — deploy dist/ to Cloudflare Pages without an adapter. // Add @astrojs/cloudflare when switching to output: 'server'. export default defineConfig({ - site: process.env.PUBLIC_SITE_URL, + site: process.env.PUBLIC_SITE_URL ?? 'https://webdevdesign.example.com', + env: { + schema: { + PUBLIC_SITE_URL: envField.string({ + context: 'client', + access: 'public', + default: 'https://webdevdesign.example.com', + }), + PUBLIC_DISCORD_INVITE_URL: envField.string({ + context: 'client', + access: 'public', + default: 'https://discord.gg/webdevdesign', + }), + }, + }, output: 'static', integrations: [react()], vite: { diff --git a/package.json b/package.json index 0524871..3499d6f 100644 --- a/package.json +++ b/package.json @@ -14,8 +14,7 @@ "lint": "eslint .", "test": "vitest", "test:run": "vitest run", - "test:coverage": "vitest run --coverage", - "generate:og-image": "node scripts/generate-og-image.mjs" + "test:coverage": "vitest run --coverage" }, "dependencies": { "@astrojs/react": "^5.0.7", diff --git a/public/favicon.ico b/public/favicon.ico deleted file mode 100644 index 7f48a94..0000000 Binary files a/public/favicon.ico and /dev/null differ diff --git a/public/favicon.svg b/public/favicon.svg deleted file mode 100644 index a5cbf81..0000000 --- a/public/favicon.svg +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/public/logo.png b/public/logo.png new file mode 100644 index 0000000..50fbb99 Binary files /dev/null and b/public/logo.png differ diff --git a/public/og-image.png b/public/og-image.png deleted file mode 100644 index 2ec5bc2..0000000 Binary files a/public/og-image.png and /dev/null differ diff --git a/public/og-image.svg b/public/og-image.svg index d739909..fe6296e 100644 --- a/public/og-image.svg +++ b/public/og-image.svg @@ -1,8 +1,14 @@ - - - Web Dev & Design - A Discord community for web developers and designers - - Join Discord + + + + + + + + WELCOME TO + Web Dev & Design + A place to learn, help, and belong + + Join Discord diff --git a/scripts/generate-og-image.mjs b/scripts/generate-og-image.mjs deleted file mode 100644 index 32c4a1b..0000000 --- a/scripts/generate-og-image.mjs +++ /dev/null @@ -1,8 +0,0 @@ -import { readFileSync } from 'node:fs'; -import { dirname, join } from 'node:path'; -import { fileURLToPath } from 'node:url'; -import sharp from 'sharp'; - -const root = join(dirname(fileURLToPath(import.meta.url)), '..'); -const svg = readFileSync(join(root, 'public/og-image.svg')); -await sharp(svg).png().toFile(join(root, 'public/og-image.png')); diff --git a/src/components/astro/CommunityCallout.astro b/src/components/astro/CommunityCallout.astro new file mode 100644 index 0000000..1a72ff6 --- /dev/null +++ b/src/components/astro/CommunityCallout.astro @@ -0,0 +1,21 @@ +--- +--- + + diff --git a/src/components/astro/CommunityCallout.astro.test.ts b/src/components/astro/CommunityCallout.astro.test.ts new file mode 100644 index 0000000..5f05eef --- /dev/null +++ b/src/components/astro/CommunityCallout.astro.test.ts @@ -0,0 +1,14 @@ +import { describe, expect, it } from 'vitest'; +import { createTestContainer } from '../../test/createTestContainer'; +import CommunityCallout from './CommunityCallout.astro'; + +describe('CommunityCallout', () => { + it('clarifies the server is not a job board', async () => { + const container = await createTestContainer(); + const html = await container.renderToString(CommunityCallout, { props: {} }); + + expect(html).toContain('Not a job board'); + expect(html).toContain('not a place for hiring'); + expect(html).toContain('/code-of-conduct'); + }); +}); diff --git a/src/components/astro/DiscordCTA.astro b/src/components/astro/DiscordCTA.astro index bb74f0f..c25bbd8 100644 --- a/src/components/astro/DiscordCTA.astro +++ b/src/components/astro/DiscordCTA.astro @@ -1,19 +1,19 @@ --- interface Props { href: string; - variant?: 'primary' | 'secondary'; + variant?: "primary" | "secondary"; class?: string; } -const { href, variant = 'primary', class: className = '' } = Astro.props; +const { href, variant = "primary", class: className = "" } = Astro.props; const base = - 'inline-flex items-center justify-center rounded-lg px-5 py-2.5 text-sm font-semibold transition-colors focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2'; + "inline-flex items-center justify-center rounded-lg px-5 py-2.5 text-sm font-semibold transition-colors focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2"; const variants = { - primary: 'bg-brand-600 text-white hover:bg-brand-700 focus-visible:outline-brand-500', + primary: "bg-mod text-black hover:bg-mod-700 focus-visible:outline-mod", secondary: - 'border border-brand-600 text-brand-700 hover:bg-brand-50 focus-visible:outline-brand-500', + "border border-admin text-admin hover:bg-admin/15 focus-visible:outline-admin", }; --- diff --git a/src/components/astro/DiscordWidget.astro b/src/components/astro/DiscordWidget.astro new file mode 100644 index 0000000..4606ae9 --- /dev/null +++ b/src/components/astro/DiscordWidget.astro @@ -0,0 +1,183 @@ +--- +import { + countMembersByStatus, + formatPresenceCount, + sortMembersByStatus, +} from "../../lib/discordWidget"; +import type { DiscordWidgetData } from "../../schemas/discordWidget"; +import DiscordCTA from "./DiscordCTA.astro"; + +interface Props { + widget: DiscordWidgetData | null; + inviteUrl: string; +} + +const { widget, inviteUrl } = Astro.props; + +const statusDot = { + online: "bg-mod", + idle: "bg-admin", + dnd: "bg-red-500", + offline: "bg-ink-muted", +} as const; + +const displayedMembers = widget + ? sortMembersByStatus(widget.members).slice(0, 12) + : []; +const statusCounts = widget ? countMembersByStatus(widget.members) : null; +const joinUrl = widget?.instant_invite ?? inviteUrl; +const sortedChannels = widget + ? [...widget.channels].sort((a, b) => a.position - b.position) + : []; +--- + +
+
+
+

+ See who's online +

+

+ Live from the server — members hanging out right now. +

+
+ + { + widget ? ( +
+ + ) : ( +
+

+ Couldn't load live server data right now. +

+
+ +
+
+ ) + } +
+
diff --git a/src/components/astro/DiscordWidget.astro.test.ts b/src/components/astro/DiscordWidget.astro.test.ts new file mode 100644 index 0000000..0f8ac25 --- /dev/null +++ b/src/components/astro/DiscordWidget.astro.test.ts @@ -0,0 +1,63 @@ +import { describe, expect, it } from 'vitest'; +import { createTestContainer } from '../../test/createTestContainer'; +import DiscordWidget from './DiscordWidget.astro'; +import type { DiscordWidgetData } from '../../schemas/discordWidget'; + +const mockWidget: DiscordWidgetData = { + id: '434487340535382016', + name: 'Web Dev & Design', + instant_invite: 'https://discord.com/invite/example', + presence_count: 5348, + channels: [ + { id: '1', name: 'event-stage', position: 1 }, + { id: '2', name: 'AFK', position: 7 }, + ], + members: [ + { + id: '1', + username: 'Yash', + status: 'online', + avatar_url: 'https://cdn.discordapp.com/widget-avatars/example/1', + game: { name: 'Visual Studio Code' }, + }, + { + id: '2', + username: 'Battousai', + status: 'online', + avatar_url: 'https://cdn.discordapp.com/widget-avatars/example/2', + }, + ], +}; + +describe('DiscordWidget', () => { + it('renders live server stats and members from widget JSON', async () => { + const container = await createTestContainer(); + const html = await container.renderToString(DiscordWidget, { + props: { + widget: mockWidget, + inviteUrl: 'https://discord.gg/example', + }, + }); + + expect(html).toContain('5,348 online'); + expect(html).toContain('Web Dev & Design'); + expect(html).toContain('event-stage'); + expect(html).toContain('Yash'); + expect(html).toContain('Visual Studio Code'); + expect(html).toContain('Join Discord'); + expect(html).not.toContain(' { + const container = await createTestContainer(); + const html = await container.renderToString(DiscordWidget, { + props: { + widget: null, + inviteUrl: 'https://discord.gg/example', + }, + }); + + expect(html).toContain("Couldn't load live server data"); + expect(html).toContain('https://discord.gg/example'); + }); +}); diff --git a/src/components/astro/Footer.astro b/src/components/astro/Footer.astro index e77d400..882cec9 100644 --- a/src/components/astro/Footer.astro +++ b/src/components/astro/Footer.astro @@ -11,7 +11,7 @@ const { siteName, discordUrl, githubUrl } = Astro.props; const year = new Date().getFullYear(); --- -