diff --git a/app/about/executives/page.tsx b/app/about/executives/page.tsx index 822d4cd..fe758cb 100644 --- a/app/about/executives/page.tsx +++ b/app/about/executives/page.tsx @@ -4,7 +4,7 @@ import Text from "@/components/common/Text"; import Divider from "@/components/common/Divider"; import Link from "@/components/common/Link"; import Image from "next/image"; -import { fetchExecutivesData, Executive } from "@/lib/executives"; +import { fetchExecutivesData } from "@/lib/executives"; export async function generateStaticParams() { try { diff --git a/app/get-involved/upcoming/page.tsx b/app/get-involved/upcoming/page.tsx index 921401c..2cf65a8 100644 --- a/app/get-involved/upcoming/page.tsx +++ b/app/get-involved/upcoming/page.tsx @@ -1,114 +1,74 @@ import React from "react"; import HeroText from "../../../components/sections/HeroText"; import BlogList from "@/components/common/BlogList"; +import { fetchUpcomingPosts } from "@/lib/posts"; + +export async function generateStaticParams() { + try { + const posts = await fetchUpcomingPosts(); + console.log(`Fetched ${posts.length} upcoming posts at build time`); + return [{}]; + } catch (error) { + console.error("Error in generateStaticParams:", error); + return [{}]; + } +} + +export default async function Upcoming() { + // Fetch upcoming posts data at build time + const allPosts = await fetchUpcomingPosts(); + + // Get current date (at start of day for comparison) + const today = new Date(); + today.setHours(0, 0, 0, 0); + + // Separate upcoming and past events + const upcomingPosts = allPosts.filter((post) => { + // Clone the date to avoid mutating the original + const postDate = new Date(post.dateObj.getTime()); + postDate.setHours(0, 0, 0, 0); + return postDate >= today; + }); + + const pastPosts = allPosts.filter((post) => { + // Clone the date to avoid mutating the original + const postDate = new Date(post.dateObj.getTime()); + postDate.setHours(0, 0, 0, 0); + return postDate < today; + }); + + // Sort upcoming posts by date (earliest first) and past posts by date (newest first) + upcomingPosts.sort((a, b) => a.dateObj.getTime() - b.dateObj.getTime()); + pastPosts.sort((a, b) => b.dateObj.getTime() - a.dateObj.getTime()); + + // Remove dateObj before passing to BlogList + const upcomingPostsClean = upcomingPosts.map( + ({ dateObj: _dateObj, ...post }) => post + ); + const pastPostsClean = pastPosts.map( + ({ dateObj: _dateObj, ...post }) => post + ); -const eventPosts = [ - { - image: "/images/involved-upcoming-1.webp", - date: "March 4, 2025", - title: "Test Raffle Contest", - description: `Our famous test library exists solely because students donate their term tests. Help us update our test library by donating yours - your name will be entered in a draw to win one of two pairs of AirPod 3s that we are raffling away! - - You can donate in-person in our office in Sid Smith or by email: students.assu@utoronto.ca. - - Deadline to submit is April 9th.`, - }, - { - image: "/images/involved-upcoming-2.webp", - date: "Feb 27, 2025", - title: "Join Our Team", - description: `ASSU Election Nominations are now open! Are you looking to get more involved in your education? Getting involved with the ASSU Executive is a great way to make your mark on academic life here in at the University of Toronto. - - We are accepting nominations for candidates to fill the roles of President (1) and Executive (4). Candidates will be elected during our March 21st ASSU Council Meeting. All full-time students in the Faculty of Arts and Science at the St. George Campus are eligible to run for our elections. - - For more information or questions please email: students.assu@utoronto.ca - - Nomination forms are due by Thursday March 13th at 5pm`, - }, - { - image: "/images/involved-upcoming-3.webp", - date: "Feb 25, 2025", - title: "2025 URC Schedule", - description: `URC is happening THIS FRIDAY! ๐ŸŽ‰โœจ Join us for a day celebrating incredible undergraduate research across the Faculty of Arts & Sciencesโ€”Sciences, Social Sciences, and Humanities! Donโ€™t miss out on exciting guest speakers and groundbreaking student projects! ๐Ÿ’ก๐Ÿ™Œ`, - }, - { - image: "/images/involved-upcoming-4.webp", - date: "Feb 13, 2025", - title: "ASSU Open Mic Night", - description: `Itโ€™s soon that time of year for ASSU Open Mic Night!!๐Ÿ•บ๐Ÿฝ๐ŸŽค๐ŸŽญ๐ŸŽธ - March 25th 6-9pm at the Cats Eye in Victoria College - Register to perform and RSVP to attend with the links in our bio!!!! - FREE ticket, FREE food&drink, LIVE talent - We hope to see you all there!!!! - - If you have any questions please dm us or reach out to the emails listed on the signup forms :))`, - }, - { - image: "/images/involved-upcoming-5.webp", - date: "Feb 13, 2025", - title: "Reading Week Office Closure", - description: `The ASSU Office in Sid Smith will be closed next week for reading week! We will be back open for in-person service on Feb 24th at 10am. In the meantime you can always reach us by DM or email: students.assu@utoronto.ca - - Have a great reading week and we'll see you back on campus soon!`, - }, - { - image: "/images/involved-upcoming-6.webp", - date: "Feb 4, 2025", - title: "ASSU Student Initiative Award", - description: `๐Ÿ“ข New Award Alert ๐Ÿ“ข - - Are you involved in a NEW club or initiative on campus? ASSU is looking to award trailblazers on campus who have started something new. - - This new Student Initiative Award will be conferred to those who have established a new organization, during the 23/24 or 24/25 academic year, at the University of Toronto St. George campus and/or within the wider community. The organization must be centred - around a topic/subject which has previously received little attention on our campus from other organizations, or the organization may be focused on a pre-existing topic/subject but conducts events and initiatives on a significant and unprecedented scale.`, - }, - { - image: "/images/involved-upcoming-7.webp", - date: "Jan 17, 2025", - title: "ASSU Leadership Awards", - description: `If you are a student who has made a difference on campus or in your community make sure you apply for our ASSU Leadership awards! These awards recognize the exceptional students from across our Faculty who make this place what it is - - The deadline is January 24th and all the information about about how to apply and the different awards available can be found at assu.ca.`, - }, - { - image: "/images/involved-upcoming-8.webp", - date: "Jan 17, 2025", - title: "2025 URC on Friday, Feb 28", - description: `Mark your calendars! ๐Ÿ—“๏ธ - - Undergraduate Research Conference - February 28, 2025 - Sidney Smith Hall - - Stay tuned for the schedule and selected speakers!!!`, - }, - { - image: "/images/involved-upcoming-9.webp", - date: "Jan 16, 2025", - title: "Past-test Library Grand Prize Winner", - description: `๐ŸŽ‰ Congratulations to the winner of our Fall semester past-test library grand prize raffle winner, Rei! - - Rei was one of the dozens of students who donated their tests last semester to keep our library up to date. For more than 20 years, Arts and Science students have been using our past-test library as a way to help them study and prepare for tests. - - You can do your part by donating your tests today and getting entered into our prize raffle! Come by our office in Sid Smith, room 1068 to learn more`, - }, - { - image: "/images/involved-upcoming-10.webp", - date: "Jan 13, 2025", - title: "URC: Abstract Submissions are EXTENDED", - description: `๐ŸŽ“โœจ Hey Arts Sci! Did you know every year ASSU hosts the Undergraduate Research Conference (URC) to showcase the amazing research of students across Arts and Science? ๐Ÿ’ก๐Ÿ“š -Interested in presenting at URC? Submit an abstract! ๐Ÿ“ The deadline for abstract submissions has been extended to January 24th, 2025!`, - }, -]; + console.log( + `Filtered posts: ${upcomingPostsClean.length} upcoming, ${pastPostsClean.length} past` + ); -const Upcoming = () => { return ( <> + {upcomingPostsClean.length > 0 && } + {upcomingPostsClean.length === 0 && ( +

+ No upcoming events at this time. +

+ )} - + {pastPostsClean.length > 0 && } + {pastPostsClean.length === 0 && ( +

+ No past events to display. +

+ )} ); -}; - -export default Upcoming; +} diff --git a/lib/posts.ts b/lib/posts.ts index 6d3721d..8c48035 100644 --- a/lib/posts.ts +++ b/lib/posts.ts @@ -1,37 +1,305 @@ -import fs from "fs"; -import path from "path"; -import matter from "gray-matter"; -import { remark } from "remark"; -import html from "remark-html"; -import breaks from "remark-breaks"; +// Import cheerio only on server side +import * as cheerio from "cheerio"; -const eventsDirectory = path.join(process.cwd(), "events", "upcoming"); - -export interface EventPost { - slug: string; - contentHtml: string; - title: string; +// Define the data shape for blog posts +export interface BlogPost { + image: string; date: string; + title: string; description: string; - image?: string; + dateObj: Date; // Keep original date object for filtering } -export async function getEventBySlug(slug: string): Promise { - const fullPath = path.join(eventsDirectory, `${slug}.md`); - const fileContents = fs.readFileSync(fullPath, "utf8"); +// Define the WordPress API response structure for posts +interface WordPressPost { + id: number; + date: string; + date_gmt: string; + slug: string; + status: string; + title: { + rendered: string; + }; + content: { + rendered: string; + }; + excerpt: { + rendered: string; + }; + featured_media: number; + link: string; + _links?: { + "wp:featuredmedia"?: Array<{ href: string }>; + }; +} - const { data, content } = matter(fileContents); - const processedContent = await remark().use(breaks).use(html).process(content); - const contentHtml = processedContent.toString(); +// Define the WordPress Media API response structure +interface WordPressMedia { + id: number; + source_url: string; + media_details?: { + sizes?: { + [key: string]: { + source_url: string; + width: number; + height: number; + }; + }; + }; +} - const { title, date, description, image } = data; +// Function to clean and normalize text +function cleanText(text: string): string { + return text + .trim() + .replace(/\s+/g, " ") // Replace multiple spaces with single space + .replace(/\n+/g, "\n") // Preserve intentional newlines + .replace(/ /g, " ") // Replace HTML non-breaking spaces + .replace(/&/g, "&") // Replace HTML entities + .replace(/</g, "<") + .replace(/>/g, ">") + .replace(/"/g, '"') + .replace(/'/g, "'") + .replace(/’/g, "'") // Right single quotation mark + .replace(/–/g, "-") // En dash + .replace(/—/g, "--") // Em dash + .replace(/'/g, "'") + .replace(/&/g, "&"); +} - return { - slug, - contentHtml, - title, - date, - description, - image, - }; +// Function to format date from ISO string to readable format +function formatDate(dateString: string): string { + try { + const date = new Date(dateString); + const options: Intl.DateTimeFormatOptions = { + year: "numeric", + month: "long", + day: "numeric", + }; + return date.toLocaleDateString("en-US", options); + } catch (error) { + console.error("Error formatting date:", error); + return dateString; + } +} + +// Function to extract text from HTML +function extractTextFromHtml(html: string): string { + const $ = cheerio.load(html); + // Remove script and style tags + $("script, style").remove(); + // Get text content + let text = $("body").text() || $.text(); + // Clean up the text + text = cleanText(text); + // Replace multiple newlines with double newline + text = text.replace(/\n\s*\n\s*\n/g, "\n\n"); + return text.trim(); +} + +// Function to extract image from content HTML +function extractImageFromContent(html: string): string | null { + const $ = cheerio.load(html); + // Look for images in the content + const $images = $("img"); + + if ($images.length > 0) { + // Get the first image src + const firstImage = $images.first(); + const src = firstImage.attr("src"); + + if (src) { + // Clean and normalize the image URL + if (src.startsWith("http")) { + return src; + } else if (src.startsWith("/")) { + return `https://assu.ca${src}`; + } else if (src.startsWith("../")) { + return `https://assu.ca/wp/${src.replace(/^\.\.\//, "")}`; + } else { + return `https://assu.ca/wp/${src}`; + } + } + } + + return null; +} + +// Function to fetch featured media URL +async function fetchFeaturedMediaUrl( + mediaId: number, + featuredMediaLink?: string +): Promise { + try { + const mediaUrl = featuredMediaLink; + + // If we have a direct link, use it + if (mediaUrl && mediaUrl.includes("/wp/v2/media/")) { + const controller = new AbortController(); + const timeoutId = setTimeout(() => controller.abort(), 5000); + + const response = await fetch(mediaUrl, { + signal: controller.signal, + headers: { + "User-Agent": "ASSU-Website/1.0", + }, + }); + + clearTimeout(timeoutId); + + if (response.ok) { + const media: WordPressMedia = await response.json(); + return media.source_url; + } + } + + // Fallback: try direct media endpoint + if (mediaId > 0) { + const controller = new AbortController(); + const timeoutId = setTimeout(() => controller.abort(), 5000); + + const response = await fetch( + `https://assu.ca/wp/wp-json/wp/v2/media/${mediaId}`, + { + signal: controller.signal, + headers: { + "User-Agent": "ASSU-Website/1.0", + }, + } + ); + + clearTimeout(timeoutId); + + if (response.ok) { + const media: WordPressMedia = await response.json(); + return media.source_url; + } + } + } catch (error) { + console.error("Error fetching featured media:", error); + } + + return null; +} + +// Main function to fetch and parse upcoming posts +export async function fetchUpcomingPosts(): Promise { + try { + // Only run on server side + if (typeof window !== "undefined") { + console.warn("fetchUpcomingPosts should only be called on server side"); + return getFallbackPosts(); + } + + console.log("Fetching upcoming posts from WordPress API..."); + + const controller = new AbortController(); + const timeoutId = setTimeout(() => controller.abort(), 10000); + + const response = await fetch( + "https://assu.ca/wp/wp-json/wp/v2/posts?categories=24", + { + signal: controller.signal, + headers: { + "User-Agent": "ASSU-Website/1.0", + }, + } + ); + + clearTimeout(timeoutId); + + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`); + } + + const data: WordPressPost[] = await response.json(); + console.log("WordPress API response received:", data.length, "posts found"); + + if (!data || data.length === 0) { + console.warn("No posts found"); + return getFallbackPosts(); + } + + // Sort posts by date (newest first) before processing + const sortedData = [...data].sort((a, b) => { + const dateA = new Date(a.date).getTime(); + const dateB = new Date(b.date).getTime(); + return dateB - dateA; + }); + + // Process each post + const posts: BlogPost[] = await Promise.all( + sortedData.map(async (post) => { + // Extract title + const title = cleanText(post.title.rendered); + + // Format date + const date = formatDate(post.date); + + // Extract description from excerpt + let description = extractTextFromHtml(post.excerpt.rendered); + + // If excerpt is empty or too short, try content + if (!description || description.length < 50) { + description = extractTextFromHtml(post.content.rendered); + // Limit description length + if (description.length > 500) { + description = description.substring(0, 500) + "..."; + } + } + + // Get image + let image: string | null = null; + + // Try to fetch featured media first + if (post.featured_media > 0) { + const featuredMediaLink = + post._links?.["wp:featuredmedia"]?.[0]?.href; + image = await fetchFeaturedMediaUrl( + post.featured_media, + featuredMediaLink + ); + } + + // If no featured media, try extracting from content + if (!image) { + image = extractImageFromContent(post.content.rendered); + } + + // Fallback to default image + if (!image) { + image = "/images/involved-upcoming-1.webp"; + } + + return { + image, + date, + title, + description, + dateObj: new Date(post.date), + }; + }) + ); + + console.log("Processed", posts.length, "upcoming posts"); + + return posts; + } catch (error) { + console.error("Error fetching upcoming posts:", error); + return getFallbackPosts(); + } +} + +// Helper function to get fallback posts +function getFallbackPosts(): BlogPost[] { + return [ + { + image: "/images/involved-upcoming-1.webp", + date: "March 4, 2025", + title: "Test Raffle Contest", + description: + "Our famous test library exists solely because students donate their term tests. Help us update our test library by donating yours - your name will be entered in a draw to win one of two pairs of AirPod 3s that we are raffling away!", + dateObj: new Date("2025-03-04"), + }, + ]; }