diff --git a/.gitignore b/.gitignore index 818e9fa..90d4de3 100644 --- a/.gitignore +++ b/.gitignore @@ -21,3 +21,4 @@ next-env.d.ts # Nix flake.lock +.env*.local diff --git a/app/events/page.tsx b/app/events/page.tsx index 33466f7..225eade 100644 --- a/app/events/page.tsx +++ b/app/events/page.tsx @@ -14,7 +14,7 @@ import styles from './Events.module.css' type CompleteEventDetails = EventDetails & { end: number, link: string | null } export type EventFrontmatter = Omit & { event_details: CompleteEventDetails } -async function getEvents() { +export async function getEvents() { const markdownFiles = await generateUnmodifiedSlugsFromMarkdownFiles('app/events') const events: Frontmatter[] = [] for (const { slug } of markdownFiles) { diff --git a/app/page.tsx b/app/page.tsx index b15fb3d..16c7f23 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -1,12 +1,17 @@ import { NavItems } from "@/components/NavItems" import { HomeLoop } from "@/components/HomeLoop" +import { HomepageBanner } from "@/components/HomepageBanner" +import { getEvents } from "./events/page" import styles from "./Home.module.css" -export default function Home() { +export default async function Home() { + const events = await getEvents() + return ( <>
+

compsigh

compsigh is the social computer science club at USFCA for meeting cool diff --git a/components/HomepageBanner/HomepageBanner.module.css b/components/HomepageBanner/HomepageBanner.module.css new file mode 100644 index 0000000..d798fd6 --- /dev/null +++ b/components/HomepageBanner/HomepageBanner.module.css @@ -0,0 +1,19 @@ +div.banner { + /*font-size: 1.2rem;*/ + + /*a { + display: flex; + white-space: pre; + text-decoration: none; + padding: 4px 8px; + }*/ +} + +@media (max-width: 700px) { + div.banner { + margin-left: -24px; + /*a { + flex-direction: column; + }*/ + } +} diff --git a/components/HomepageBanner/HomepageBanner.test.tsx b/components/HomepageBanner/HomepageBanner.test.tsx new file mode 100644 index 0000000..d0b82a9 --- /dev/null +++ b/components/HomepageBanner/HomepageBanner.test.tsx @@ -0,0 +1,33 @@ +import { render, screen } from "@testing-library/react" +import { HomepageBanner } from "./HomepageBanner" +import { EventFrontmatter } from "@/app/events/page" + +describe("Next Component HomepageBanner", () => { + it("Renders when an event is coming up", () => { + const currentTime = Math.floor(new Date().getTime() / 1000) + + const testing_UpComingEvent: EventFrontmatter[] = [ + { + title: "DAVE", + description: "register!", + event_details: { + start: currentTime + 60, + end: currentTime + 90, + location: "The Hive", + cover_image: "/events/2025-11-07/deploy25.png", + pictures: [], + link: "https://touch-grass.tech/" + }, + slug: "events/2025-11-07/deploy25" + } + ] + + render() + + const banner = screen.getByText((content) => + content.includes("Next event:") + ) + + expect(banner).toBeInTheDocument() + }) +}) diff --git a/components/HomepageBanner/HomepageBanner.tsx b/components/HomepageBanner/HomepageBanner.tsx new file mode 100644 index 0000000..6b126db --- /dev/null +++ b/components/HomepageBanner/HomepageBanner.tsx @@ -0,0 +1,63 @@ +"use client" + +// Components +import { LinkBar } from "@/components/LinkBar" + +// Functions +import { isValidURL } from "./isValidURL" + +// Types +import { EventFrontmatter } from "@/app/events/page" + +// Styles +import styles from "./HomepageBanner.module.css" + +export function HomepageBanner({ events }: { events: EventFrontmatter[] }) { + const currentTime = Math.floor(new Date().getTime() / 1000) + if (events.length === 0) return <> + + const updatedEvents: EventFrontmatter[] = [ + ...events, + { + title: "compsigh night v2026.01.20 6 7", + description: "register!", + event_details: { + start: 1770420908, + end: 1770420968, + location: "The Hive", + cover_image: "/events/2025-11-07/deploy25.png", + pictures: [], + link: "https://touch-grass.tech/" + }, + slug: "events/2025-11-07/deploy25" + } + ] + + const upcomingEvents: EventFrontmatter[] = [] + for (const event of updatedEvents) + if (currentTime < event.event_details.start) upcomingEvents.push(event) + + if (upcomingEvents.length === 0) return <> + const nearestUpcomingEvent = upcomingEvents[0] + const title = nearestUpcomingEvent.title + const eventLink = nearestUpcomingEvent.event_details.link! + + if (!isValidURL(eventLink)) return <> + + return ( +

+ {/* + Next event: + `} /> + */} + + Next event: {title} + +
+ ) +} diff --git a/components/HomepageBanner/index.ts b/components/HomepageBanner/index.ts new file mode 100644 index 0000000..086986a --- /dev/null +++ b/components/HomepageBanner/index.ts @@ -0,0 +1 @@ +export { HomepageBanner } from './HomepageBanner' \ No newline at end of file diff --git a/components/HomepageBanner/isValidURL.test.ts b/components/HomepageBanner/isValidURL.test.ts new file mode 100644 index 0000000..87ffc20 --- /dev/null +++ b/components/HomepageBanner/isValidURL.test.ts @@ -0,0 +1,89 @@ +import { isValidURL } from "./isValidURL" + +interface Test { + name: string + input: string + expected: boolean +} + +const tests: Test[] = [ + { + name: "Return true for valid URL string", + input: "https://touch-grass.tech/", + expected: true + }, + { + name: "File Transfer Protocol", + input: "ftp://example.com", + expected: true + }, + { + name: "WebSocket Secure Protocol", + input: "wss://example.com", + expected: true + }, + { + name: "Email Protocol", + input: "mailto://johnpork@yahoo.com", + expected: true + }, + { + name: "Torrent Protocol", + input: + "magnet:?xt=urn:btih:6D5B5F0E8E6E4F5D8B7A9C1E2D3F4A5B6C7D8E9F&dn=example-file.zip&tr=udp://tracker.example.com:6969/announce", + expected: true + }, + { + name: "Missing protocol", + input: "example.com", + expected: false + }, + { + name: "Fake protocol", + input: "67://example.com", + expected: false + }, + { + name: "Space in domain name", + input: "https://example .com", + expected: false + }, + { + name: "Space in domain", + input: "https://exam ple.com/path", + expected: false + }, + { + name: "No colon after protocol", + input: "ftp//example.com", + expected: false + }, + { + name: "Missing one slash", + input: "https:/example.com", + expected: true + }, + { + name: "Extra slash", + input: "https:////////////////example.com", + expected: true + }, + { + name: "Non-numeric port", + input: "https://exam[ple].com", + expected: false + }, + { + name: "Unencoded spaces in path", + input: "https://tungtungtung.com/path with spaces", + expected: true + } +] + +describe("TS function IsValidURL", () => { + tests.forEach((config) => { + it(config.name, () => { + expect(isValidURL(config.input)).toBe(config.expected) + }) + }) +}) diff --git a/components/HomepageBanner/isValidURL.ts b/components/HomepageBanner/isValidURL.ts new file mode 100644 index 0000000..6a5416e --- /dev/null +++ b/components/HomepageBanner/isValidURL.ts @@ -0,0 +1,8 @@ +export function isValidURL(url: string): boolean { + try { + new URL(url) + return true + } catch { + return false + } +} diff --git a/components/LinkBar/LinkBar.module.css b/components/LinkBar/LinkBar.module.css index 58c53d9..7237b95 100644 --- a/components/LinkBar/LinkBar.module.css +++ b/components/LinkBar/LinkBar.module.css @@ -11,12 +11,23 @@ div#container { width: 100%; height: 64px; display: flex; - justify-content: flex-end; align-items: center; font-family: var(--font-tronica-mono); } -div#container.previous { +div#container.alignment-start { + justify-content: left; +} + +div#container.alignment-end { + justify-content: right; +} + +div#container.text-first { + flex-direction: row; +} + +div#container.arrow-first { flex-direction: row-reverse; } diff --git a/components/LinkBar/LinkBar.tsx b/components/LinkBar/LinkBar.tsx index 03bd771..09187be 100644 --- a/components/LinkBar/LinkBar.tsx +++ b/components/LinkBar/LinkBar.tsx @@ -1,23 +1,36 @@ -import Link, { type LinkProps } from 'next/link' +import Link, { type LinkProps } from "next/link" -import styles from './LinkBar.module.css' +import styles from "./LinkBar.module.css" interface LinkBarProps extends LinkProps { children: React.ReactNode - type?: 'previous' | 'next' + arrowDirection?: "back" | "forward" + alignment?: "start" | "end" + order?: "text-first" | "arrow-first" } -export function LinkBar({ children, type = 'next', ...props }: LinkBarProps) { +export function LinkBar({ + children, + arrowDirection = "forward", + alignment = "end", + order = "text-first", + ...props +}: LinkBarProps) { return (