diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index d58278b..d90d374 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -11,12 +11,12 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Check out repository
- uses: actions/checkout@v2
+ uses: actions/checkout@v4
- name: Set up Node
- uses: actions/setup-node@v1
+ uses: actions/setup-node@v4
with:
- node-version: 12
+ node-version: 18
- name: Install dependencies
run: npm i
@@ -28,12 +28,12 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Check out repository
- uses: actions/checkout@v2
+ uses: actions/checkout@v4
- name: Set up Node
- uses: actions/setup-node@v1
+ uses: actions/setup-node@v4
with:
- node-version: 12
+ node-version: 18
- name: Install dependencies
run: npm i
@@ -45,12 +45,12 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Check out repository
- uses: actions/checkout@v2
+ uses: actions/checkout@v4
- name: Set up Node
- uses: actions/setup-node@v1
+ uses: actions/setup-node@v4
with:
- node-version: 12
+ node-version: 18
- name: Install dependencies
run: npm i
diff --git a/components/clients/ProjectQuiz.jsx b/_archive/clients/ProjectQuiz.jsx
similarity index 100%
rename from components/clients/ProjectQuiz.jsx
rename to _archive/clients/ProjectQuiz.jsx
diff --git a/pages/clients/quizpage.jsx b/_archive/clients/quizpage.jsx
similarity index 100%
rename from pages/clients/quizpage.jsx
rename to _archive/clients/quizpage.jsx
diff --git a/pages/api/saveQuizData.js b/_archive/clients/saveQuizData.js
similarity index 100%
rename from pages/api/saveQuizData.js
rename to _archive/clients/saveQuizData.js
diff --git a/components/about/ourValues.jsx b/components/about/ourValues.jsx
index 52aaa2b..a21f288 100644
--- a/components/about/ourValues.jsx
+++ b/components/about/ourValues.jsx
@@ -4,8 +4,43 @@ import styles from './styles.module.scss';
import classNames from 'classnames';
import { IoMdArrowDropright as ArrowIcon } from 'react-icons/io';
-const OurValues = ({ content }) => {
- const [selected, setSelected] = useState(3);
+const content = [
+ {
+ header: 'Engage in your community.',
+ body: 'Our community makes us special. We are a group of friends who care deeply about our organization and each other. The strength of our community comes from the contributions of its members. We welcome new members with warmth, and we make the effort to know each other beyond superficial details.',
+ },
+ {
+ header: 'Be dependable.',
+ body: 'As an organization, we trust every single member to hold themselves and others accountable. We know that our performance affects others. If we all are engaged and follow through on our commitments, then we can be confident that others have our backs.',
+ },
+ {
+ header: 'Develop with care.',
+ body: 'We build with others in mind. Empathy and compassion are crucial to serving our partner organizations and members. When we embark on projects, we work to deeply understand the people who we are helping. We write software that meets the highest standards of development. When we deliver our final products, we ensure that others can understand how to use our code and build on it.',
+ },
+ {
+ header: 'Go beyond technology.',
+ body: 'Technology is only one tool we use in our greater mission for social impact. It is easy to fall into the trap of believing technology can solve all problems. We know that technology alone is not enough. We learn from, work with, and are inspired by other organizations and individuals who are tackling social problems using a multitude of tools.',
+ },
+ {
+ header: 'Go for it.',
+ body: 'Sometimes, the only way to learn if something works is to run with it. If we waited until our plans were perfect, we would never get anywhere. Organizational roles do not determine the value of an idea: great ideas can come from anyone and anywhere. We grow because we are always trying something new.',
+ },
+ {
+ header: 'Learn what you don’t know.',
+ body: 'There is a lot to learn. In order to improve the world, we need to know about it. We recognize that we have barely scratched the surface of what is out there. We are humble about our gaps in knowledge, and are willing to answer questions with “I don’t know.” We are hungry learners, and we teach each other whenever possible.',
+ },
+ {
+ header: 'Be open-minded.',
+ body: 'Our process depends on openness to different people, topics, and perspectives. We embrace difference and work against intolerance to foster an inclusive environment. Our goal is to expose our members to the vast opportunities and daunting challenges in our work.',
+ },
+ {
+ header: 'Be critical.',
+ body: 'Criticism helps us grow. We are critical about the world we live in, our organization, and ourselves. Our community has an obligation to question the assumptions and decisions made by those around it. Our leaders constantly strive to improve our model by assessing our impact. Our members proactively ask for feedback and are forthcoming with feedback for others.',
+ },
+];
+
+const OurValues = () => {
+ const [selected, setSelected] = useState(() => Math.min(3, content.length - 1));
const [displayValues, setDisplayValues] = useState([]);
const [moveToNext, setMoveToNext] = useState(false);
const autoScrollInterval = useRef(null);
@@ -30,6 +65,10 @@ const OurValues = ({ content }) => {
useEffect(() => {
// whenever we need to move to the next element...
if (moveToNext) {
+ if (content.length === 0) {
+ setMoveToNext(false);
+ return;
+ }
// change which card is focused (wrapping to the start of the list if necessary)
setSelected((selected + 1) % content.length);
// once we're done scrolling to our focused card...
@@ -56,15 +95,15 @@ const OurValues = ({ content }) => {
className={classNames(styles.value_card_container, {
[styles.move]: moveToNext,
})}>
- {displayValues.map(({ header, body, image }) => (
+ {displayValues.map(({ header, body }) => (
{header}
-
{body.json.content[0].content[0].value}
+
{body}
))}
diff --git a/components/apply/nonprofit/servicesDetail.jsx b/components/apply/nonprofit/servicesDetail.jsx
index 77b0c0e..7aa8ff0 100644
--- a/components/apply/nonprofit/servicesDetail.jsx
+++ b/components/apply/nonprofit/servicesDetail.jsx
@@ -1,7 +1,6 @@
import React from 'react';
import Section from '../../section';
import { Container, Row } from 'reactstrap';
-import ContentBlock from '../../ContentBlock';
export default function ServicesDetail({ content, title }) {
return (
@@ -12,7 +11,7 @@ export default function ServicesDetail({ content, title }) {
-
+
{content}
diff --git a/components/apply/timeline.jsx b/components/apply/timeline.jsx
index a7afc7c..59107b4 100644
--- a/components/apply/timeline.jsx
+++ b/components/apply/timeline.jsx
@@ -1,6 +1,6 @@
import React from 'react';
import { VerticalTimeline, VerticalTimelineElement } from 'react-vertical-timeline-component';
-import ContentBlock from '../ContentBlock';
+import RichText from '../richText';
function Icon({ src, alt }) {
return (
@@ -19,19 +19,28 @@ function Icon({ src, alt }) {
function Timeline({ steps }) {
return (
-
- {steps &&
- steps.map(({ header, body, image }) => (
- }
- key={header}>
-
@@ -135,17 +136,12 @@ const NonprofitScroll = () => {
hack4impact has built for other nonprofits.
-
-
- Start
-
-
-
{/* Back to top button */}
diff --git a/components/faqQuestion.jsx b/components/faqQuestion.jsx
index 4e82d33..766bf7a 100644
--- a/components/faqQuestion.jsx
+++ b/components/faqQuestion.jsx
@@ -1,6 +1,6 @@
import React from 'react';
import { Collapse } from 'reactstrap';
-import ContentBlock from './ContentBlock';
+import RichText from './richText';
function FaqQuestion({ question, answer }) {
const [isOpen, setIsOpen] = React.useState(false);
@@ -15,7 +15,9 @@ function FaqQuestion({ question, answer }) {
{/* eslint-enable */}
-
+
+
+
@@ -48,6 +50,9 @@ function FaqQuestion({ question, answer }) {
margin: 0 10px;
color: var(--gray);
}
+ .answer p {
+ white-space: pre-line;
+ }
`}
>
);
diff --git a/components/homePageProject.jsx b/components/homePageProject.jsx
index a74c3bb..d99febf 100644
--- a/components/homePageProject.jsx
+++ b/components/homePageProject.jsx
@@ -1,6 +1,5 @@
import React from 'react';
import Link from 'next/link';
-import ContentBlock from './ContentBlock';
function HomePageProject({ title, description, thumbnail, urlSlug }) {
return (
@@ -9,7 +8,7 @@ function HomePageProject({ title, description, thumbnail, urlSlug }) {
{title}
-
+
{description}
);
diff --git a/components/projects/featureSlider.jsx b/components/projects/featureSlider.jsx
index 9bfd2dc..0bc2df5 100644
--- a/components/projects/featureSlider.jsx
+++ b/components/projects/featureSlider.jsx
@@ -80,7 +80,7 @@ function FeatureSlider({ features }) {
.feature-slider-btn:hover h3 {
color: var(--primary-blue) !important;
}
- .feature-slider-btn:hover 0 {
+ .feature-slider-btn:hover p {
color: black !important;
}
.is-active h3 {
diff --git a/components/quote.jsx b/components/quote.jsx
index 5e0c465..64f17c2 100644
--- a/components/quote.jsx
+++ b/components/quote.jsx
@@ -3,7 +3,6 @@ import React from 'react';
import QuoteSection from './quoteSection';
import { Container, Row, Col } from 'reactstrap';
-import ContentBlock from './ContentBlock';
const Quote = ({ quote, source, sourceTitle }) => (
@@ -12,7 +11,7 @@ const Quote = ({ quote, source, sourceTitle }) => (
-
+
{quote}
@@ -23,7 +23,7 @@ function Home({ chapterLogos, previewProjects }) {
-
+
{/* */}
>
);
@@ -32,23 +32,35 @@ function Home({ chapterLogos, previewProjects }) {
export default Home;
export async function getStaticProps() {
- const {
- chapterCollection,
- pennWebsiteLayout: { projectsCollection },
- } = await fetchNotionContent('homepage');
-
+ let chapterLogos = [];
+ let previewProjects = [];
+ let partners = [];
+ try {
+ const {
+ chapterCollection,
+ pennWebsiteLayout: { projectsCollection },
+ partnerCollection,
+ } = await fetchNotionContent('homepage');
+
+ chapterLogos = chapterCollection.items.map(
+ ({ websiteLink, socialMediaLink, codeRepoLink, ...chapter }) => ({
+ ...chapter,
+ // not all chapters have a website,
+ // so we need to have some solid fallbacks
+ link: websiteLink ?? socialMediaLink ?? codeRepoLink ?? 'https://hack4impact.org',
+ }),
+ );
+ previewProjects = projectsCollection.items;
+ partners = partnerCollection.items;
+ } catch (error) {
+ console.error('Error fetching homepage content:', error);
+ }
+
return {
props: {
- chapterLogos: chapterCollection.items.map(
- ({ websiteLink, socialMediaLink, codeRepoLink, ...chapter }) => ({
- ...chapter,
- // not all chapters have a website,
- // so we need to have some solid fallbacks
- link: websiteLink ?? socialMediaLink ?? codeRepoLink ?? 'https://hack4impact.org',
- }),
- ),
- previewProjects: projectsCollection.items,
+ chapterLogos,
+ previewProjects,
+ partners,
},
- revalidate: 60,
};
}
diff --git a/pages/projects/[projectSlug].jsx b/pages/projects/[projectSlug].jsx
index d90a849..959fe77 100644
--- a/pages/projects/[projectSlug].jsx
+++ b/pages/projects/[projectSlug].jsx
@@ -155,7 +155,6 @@ export async function getStaticProps({ params: { projectSlug } }) {
return {
props: formattedProject,
- revalidate: 60,
};
} catch (error) {
console.error('Error fetching project detail:', error);
diff --git a/pages/projects/index.jsx b/pages/projects/index.jsx
index 85a4229..e5a8716 100644
--- a/pages/projects/index.jsx
+++ b/pages/projects/index.jsx
@@ -50,7 +50,6 @@ export async function getStaticProps() {
props: {
projects: projectsCollection.items,
},
- revalidate: 60,
};
} catch (error) {
console.error('Error fetching projects from Notion:', error);
@@ -58,7 +57,6 @@ export async function getStaticProps() {
props: {
projects: [],
},
- revalidate: 60,
};
}
}
\ No newline at end of file
diff --git a/pages/team/[memberSlug].jsx b/pages/team/[memberSlug].jsx
index f7754dd..2cccf37 100644
--- a/pages/team/[memberSlug].jsx
+++ b/pages/team/[memberSlug].jsx
@@ -132,7 +132,6 @@ export async function getStaticProps({ params: { memberSlug } }) {
return {
props: memberData,
- revalidate: 60,
};
} catch (error) {
console.error('Error fetching member detail:', error);
diff --git a/public/images/placeholder.webp b/public/images/placeholder.webp
new file mode 100644
index 0000000..8687432
Binary files /dev/null and b/public/images/placeholder.webp differ
diff --git a/utils/fallbackApplicationContent.js b/utils/fallbackApplicationContent.js
new file mode 100644
index 0000000..1d86bdf
--- /dev/null
+++ b/utils/fallbackApplicationContent.js
@@ -0,0 +1,686 @@
+// Snapshot of Notion Timeline/FAQ/Applications content as of 2026-08-10, used only when the live Notion fetch fails or returns no entries for that applicationType.
+// This is a fallback, not a source of truth — update Notion first; re-sync this file only if it drifts noticeably out of date.
+
+const FALLBACK_APPLICATION_CONTENT = {
+ Students: {
+ application: {
+ applicationLink: '',
+ openRolesLink: '',
+ description:
+ 'If you have programming experience at or above the level of an introductory level computer science class (such as CIS 1100 or AP Computer Science), and you want to apply your classroom knowledge to the real world — making a concrete improvement in people’s lives — then Hack4Impact is the club for you.\n\nWe place a strong emphasis on peer mentorship and learning, so we’ll teach you everything you need to know about designing and building web applications. No experience beyond programming fundamentals is required to join Hack4Impact.\n\nAs part of the team, you will learn how to identify user needs in order to design and build a working product. You’ll have a chance to experiment with and build new technologies.',
+ },
+ timeline: [
+ {
+ header: 'Info Sessions',
+ body: [
+ {
+ text: 'At the beginning of the fall semester we will hold info sessions and be active at the SAC and ESAC club fairs, so come say hi and learn more about Hack4Impact!\n\n',
+ bold: false,
+ italic: false,
+ strikethrough: false,
+ code: false,
+ href: null,
+ },
+ {
+ text: 'Fall 2026 Important Recruiting Dates: TBD',
+ bold: true,
+ italic: false,
+ strikethrough: false,
+ code: false,
+ href: null,
+ },
+ ],
+ image: {
+ url: 'https://images.ctfassets.net/dz50cburkkql/1aiDmihyyZvDKqpH8s88W3/4c3915eebaf05dce640c6c156594e4ee/info-xxl.png',
+ description: '',
+ },
+ },
+ {
+ header: 'Written Application',
+ body: [
+ {
+ text: 'We will release a written application that should take about an hour to complete. We want to learn about what you have done, what you hope to do, and why you would be a good fit for Hack4Impact. Please include any side projects, open source contributions, or anything else you think we should see.',
+ bold: false,
+ italic: false,
+ strikethrough: false,
+ code: false,
+ href: null,
+ },
+ ],
+ image: {
+ url: 'https://images.ctfassets.net/dz50cburkkql/4W9MVoK6p2153qdoy9m5JD/7506f7ef1dab3929cc198364c66ef52d/pencil-paper.svg',
+ description: '',
+ },
+ },
+ {
+ header: 'Behavioral Interview',
+ body: [
+ {
+ text: 'If your written application is accepted, we will contact you to schedule a behavioral interview with two Hack4Impact members. We want to know more about you, your passion for social impact, and what you can bring to the Hack4Impact community. This is also a great time to ask questions and get a sense of what being a member of Hack4Impact is like from your interviewers.',
+ bold: false,
+ italic: false,
+ strikethrough: false,
+ code: false,
+ href: null,
+ },
+ ],
+ image: {
+ url: 'https://images.ctfassets.net/dz50cburkkql/5fL8iR8PK6yqKMiNKIyGut/35533f4b493168fa237fb00f1b33b77b/talk-xxl.png',
+ description: '',
+ },
+ },
+ {
+ header: 'Technical Interview',
+ body: [
+ {
+ text: "The last portion of the application process is a technical interview with two Hack4Impact members. Don't worry about grinding Leetcode for this one, we only require a CIS 110 level of knowledge, but it will focus on Web Development (the actual technical work that we do!) We want to get a better idea of how you approach problems, implement solutions, and explain your work.",
+ bold: false,
+ italic: false,
+ strikethrough: false,
+ code: false,
+ href: null,
+ },
+ ],
+ image: {
+ url: 'https://images.ctfassets.net/dz50cburkkql/4XgI5xF1nUSRnKeC0jVOc2/47b53f321eef3eac83eff622600b75e4/code-xxl.png',
+ description: '',
+ },
+ },
+ ],
+ faqs: [
+ {
+ question: 'Why should I join?',
+ answer: [
+ {
+ text: "As a Hack4Impact member, you can shape the organization's initiatives (or start your own!) in areas such as impact assessment, diversity & inclusion, career development, and more. As an organization and as a community, we care about and actively support the individual development of each member, be it professional or personal. In addition to our social impact projects, we focus on building a tight-knit community of students passionate about social change. As a member of the Hack4Impact family, you will bond with the community and become part of the Hack4Impact family, receive mentorship and guidance from a strong network of accomplished and amazing alumni, watch movies, join in for board game nights, and make memories with your Hack4Impact friends 😊",
+ bold: false,
+ italic: false,
+ strikethrough: false,
+ code: false,
+ href: null,
+ },
+ ],
+ },
+ {
+ question: 'Who should apply?',
+ answer: [
+ {
+ text: 'Class of 2028, 2029, and 2030. You do NOT have to be a CS major to apply!',
+ bold: false,
+ italic: false,
+ strikethrough: false,
+ code: false,
+ href: null,
+ },
+ ],
+ },
+ {
+ question: 'What is the time commitment?',
+ answer: [
+ {
+ text: '1hr full group (mandatory) meeting every Wednesday. ~4 hours outside of that inclusive of social and technical development activities!',
+ bold: false,
+ italic: false,
+ strikethrough: false,
+ code: false,
+ href: null,
+ },
+ ],
+ },
+ {
+ question: 'Do you recruit in the Fall or Spring semester?',
+ answer: [
+ {
+ text: 'We usually do not recruit for any positions in the Spring, but we will be recruiting in Fall 2026. Check back in August for any updates about recruitment!',
+ bold: false,
+ italic: false,
+ strikethrough: false,
+ code: false,
+ href: null,
+ },
+ ],
+ },
+ {
+ question: 'Can grad students apply?',
+ answer: [
+ {
+ text: 'Yes, grad students are welcome to apply as long as you are not Class of 2027. On the application form, please indicate your latest graduation year from Penn as your "class year".',
+ bold: false,
+ italic: false,
+ strikethrough: false,
+ code: false,
+ href: null,
+ },
+ ],
+ },
+ {
+ question: 'How much software experience do I need to be a technical developer?',
+ answer: [
+ {
+ text: 'If you have taken CIS 110 or AP Computer Science, you are qualified to join Hack4Impact! We have a huge focus on education and will make sure that you have the resources and mentorship opportunities to learn how to build software products. If you are eager to learn the technologies, and are passionate about social impact and community-building, we are excited for you to apply! Developers with web development knowledge are a plus, but please apply even if you don’t know!',
+ bold: false,
+ italic: false,
+ strikethrough: false,
+ code: false,
+ href: null,
+ },
+ ],
+ },
+ {
+ question: 'What are you looking for in an applicant?',
+ answer: [
+ {
+ text: 'We are looking for people who are excited about working in social impact and tech and who resonate deeply with our mission and values: ',
+ bold: false,
+ italic: false,
+ strikethrough: false,
+ code: false,
+ href: null,
+ },
+ {
+ text: 'http://hack4impact.org/about',
+ bold: false,
+ italic: false,
+ strikethrough: false,
+ code: false,
+ href: 'http://hack4impact.org/about',
+ },
+ {
+ text: "! If you are passionate about software and social impact, a self-starter, want to learn, and are eager to apply your skills to affect real lives, please submit an application! If you are on the fence, please reach out to us. We're super friendly ❤️",
+ bold: false,
+ italic: false,
+ strikethrough: false,
+ code: false,
+ href: null,
+ },
+ ],
+ },
+ {
+ question: 'What does the technical interview look like?',
+ answer: [
+ {
+ text: 'This year, we are changing up the technical interview from years past. We will providing complete instructions with what to expect after the first round interview.',
+ bold: false,
+ italic: false,
+ strikethrough: false,
+ code: false,
+ href: null,
+ },
+ ],
+ },
+ {
+ question: 'I applied to H4I last year but did not get in. Can I re-apply again this year?',
+ answer: [
+ {
+ text: 'YES! Please re-apply. So many of our members did not get in the first time, and we strongly encourage you to submit another application!',
+ bold: false,
+ italic: false,
+ strikethrough: false,
+ code: false,
+ href: null,
+ },
+ ],
+ },
+ {
+ question:
+ "Should I still apply if I haven't necessarily worked on a structured side project?",
+ answer: [
+ {
+ text: "No worries if you don't have a super structured project that you've worked on. Feel free to write about some of the work that you've done in the courses or through self-learning. The point of the project question is for you to explain and demonstrate the work that you've done with CS, and often, people will write about \"projects\"/assignments they've completed as part of their classes as examples of it. Also, if you feel like this isn't the best time for you to apply and want to wait to get a bit more experience/work, we 100% encourage re-applying or applying the following year. A lot of our members entered as sophomores and juniors, and even one of our current co-directors applied twice! Though, we would encourage you to submit an application this year, EVEN if you are unsure about your skills/experience!",
+ bold: false,
+ italic: false,
+ strikethrough: false,
+ code: false,
+ href: null,
+ },
+ ],
+ },
+ {
+ question:
+ 'Some members of H4I mentioned they went on Figma a lot... do some developers design as well?',
+ answer: [
+ {
+ text: "We currently don't have an official design position, but many of our developers do have extra skills they like to work on outside of developing. Design is definitely one of them, and this year, internally, we will be putting together a design committee to build out a lasting team. That said, we do not recruit for designers. For those of you who are interested but not too knowledgable about design, Figma is a collaborative design tool similar to photoshop/illustrator that is ~free~. (Also, a ton of our alum work there so we love it 🥺 )",
+ bold: false,
+ italic: false,
+ strikethrough: false,
+ code: false,
+ href: null,
+ },
+ ],
+ },
+ {
+ question: 'What does a technical developer/software engineer do in Hack4Impact?',
+ answer: [
+ {
+ text: 'Technical developers drive forward Hack4Impact’s mission by building software that meets important social and humanitarian needs. As a technical developer, you will have the opportunity to develop and ship a project with tangible impact each semester. Within the organization, you can also work on various open-source software initiatives and learn from mentors around you to gain skills necessary to becoming a full-stack developer.',
+ bold: false,
+ italic: false,
+ strikethrough: false,
+ code: false,
+ href: null,
+ },
+ ],
+ },
+ {
+ question: 'How does the project structure work?',
+ answer: [
+ {
+ text: 'We source and work on new projects every semester normally in teams of 5-6 people. Each team has a PM (project manager) who handles the majority of client communications and a TL (tech lead) who helps develop and distribute tasks weekly.',
+ bold: false,
+ italic: false,
+ strikethrough: false,
+ code: false,
+ href: null,
+ },
+ ],
+ },
+ ],
+ },
+ Organizations: {
+ application: {
+ applicationLink:
+ 'https://docs.google.com/forms/d/1kSSmmI2ksisl-DlRPDFrTh5M90xVUcaOpNMBxdilhqk/viewform?edit_requested=true',
+ openRolesLink: '',
+ description:
+ '\nThat’s awesome! We’d love to talk with you, so please email penn@hack4impact.org and we’ll get back to you as soon as we can.\nWe want to help maximize your nonprofit’s impact, and we’re serious about project quality. We’ll communicate frequently and work with you closely to ensure we’re building exactly what you need.\nWe generally charge $1,000 per project. Our pricing is need-based, and if your organization is not able to afford the cost, we will be happy to lower or even waive the fee. All client contributions feed right back into Hack4Impact to pay for professional development events, speakers, organization retreats, and operating costs.',
+ },
+ timeline: [
+ {
+ header: 'Brainstorming',
+ body: [
+ {
+ text: 'Come up with an idea for application that might be useful to your organization.\nMake sure also to visit our ',
+ bold: false,
+ italic: false,
+ strikethrough: false,
+ code: false,
+ href: null,
+ },
+ {
+ text: 'completed projects page',
+ bold: false,
+ italic: false,
+ strikethrough: false,
+ code: false,
+ href: 'https://upenn.hack4impact.org/projects',
+ },
+ {
+ text: ' to see what types of applications we’ve built in the past.',
+ bold: false,
+ italic: false,
+ strikethrough: false,
+ code: false,
+ href: null,
+ },
+ ],
+ image: {
+ url: 'https://images.ctfassets.net/dz50cburkkql/2ToYCXBHebKTvGhv80QgLD/b03943c5f04ca515be345cb7a60c1e87/solutions-xxl.png',
+ description: '',
+ },
+ },
+ {
+ header: 'Interest Period',
+ body: [
+ {
+ text: 'Once you have an idea in mind, you should fill out the nonprofit interest form. Describe your organization and your project idea. This will help us get an initial sense of your goals, needs, and expectations. The form can be found ',
+ bold: false,
+ italic: false,
+ strikethrough: false,
+ code: false,
+ href: null,
+ },
+ {
+ text: 'here',
+ bold: false,
+ italic: false,
+ strikethrough: false,
+ code: false,
+ href: 'https://docs.google.com/forms/d/e/1FAIpQLSc6HT8WyxjWdugR1OULl80xE1Aaz1-dMOo5c179DE4EawOYHg/viewform',
+ },
+ {
+ text: '.',
+ bold: false,
+ italic: false,
+ strikethrough: false,
+ code: false,
+ href: null,
+ },
+ ],
+ image: {
+ url: 'https://images.ctfassets.net/dz50cburkkql/5rFBCfoZEzPVsli7X8Bxm2/601f8676f4f4a78c0ea762cf6178a22b/list-ingredients-xxl.png',
+ description: '',
+ },
+ },
+ {
+ header: 'Introduction Call',
+ body: [
+ {
+ text: 'A member of Hack4Impact will reach out to you to schedule a phone call. We’ll use this phone call to learn more about your organization, project idea, and how Hack4Impact can help. Please use this phone call to ask further questions about Hack4Impact.',
+ bold: false,
+ italic: false,
+ strikethrough: false,
+ code: false,
+ href: null,
+ },
+ ],
+ image: {
+ url: 'https://images.ctfassets.net/dz50cburkkql/1hv5I5e9zZCMWm0oKLAcCw/c3f86bb6f8e053a5299bd822a69afddf/phone-xxl.png',
+ description: '',
+ },
+ },
+ {
+ header: 'Incubation Period',
+ body: [
+ {
+ text: 'Up to a week before the application deadline, we will work with you to define, clarify, and tighten your project specifications. While submitting a draft of your application for feedback isn’t required, we strongly encourage applicant organizations to do so.',
+ bold: false,
+ italic: false,
+ strikethrough: false,
+ code: false,
+ href: null,
+ },
+ ],
+ image: {
+ url: 'https://images.ctfassets.net/dz50cburkkql/2ToYCXBHebKTvGhv80QgLD/b03943c5f04ca515be345cb7a60c1e87/solutions-xxl.png',
+ description: '',
+ },
+ },
+ {
+ header: 'Application Period',
+ body: [
+ {
+ text: 'Submit the project application! Each fall and spring semester, we choose three to four nonprofits to partner with. We will notify you within a few weeks of the application deadline whether you have been selected for the upcoming semester.',
+ bold: false,
+ italic: false,
+ strikethrough: false,
+ code: false,
+ href: null,
+ },
+ ],
+ image: {
+ url: 'https://images.ctfassets.net/dz50cburkkql/5rFBCfoZEzPVsli7X8Bxm2/601f8676f4f4a78c0ea762cf6178a22b/list-ingredients-xxl.png',
+ description: '',
+ },
+ },
+ {
+ header: 'Scoping and Ideation',
+ body: [
+ {
+ text: 'If your application is successful, you will be paired with a Hack4Impact project manager (PM), a tech lead (TL) and developer team. Your PM and TL will reach out to better understand the project and to create a plan for the features and timeline of the project in a Project Requirement Document (PRD).',
+ bold: false,
+ italic: false,
+ strikethrough: false,
+ code: false,
+ href: null,
+ },
+ ],
+ image: {
+ url: 'https://images.ctfassets.net/dz50cburkkql/4W9MVoK6p2153qdoy9m5JD/7506f7ef1dab3929cc198364c66ef52d/pencil-paper.svg',
+ description: '',
+ },
+ },
+ {
+ header: 'Development',
+ body: [
+ {
+ text: 'Once the PRD is finalized, we will begin development on the project. Your PM, TL, and development team will work with you over a period of 3 months to build an app. The PM and TL will work with you to ensure that the project is completed on-time and to your specifications.',
+ bold: false,
+ italic: false,
+ strikethrough: false,
+ code: false,
+ href: null,
+ },
+ ],
+ image: {
+ url: 'https://images.ctfassets.net/dz50cburkkql/4XgI5xF1nUSRnKeC0jVOc2/47b53f321eef3eac83eff622600b75e4/code-xxl.png',
+ description: '',
+ },
+ },
+ {
+ header: 'Hand Off',
+ body: [
+ {
+ text: 'At the end of the semester, we will officially hand off the project source code to you and your organization for you to begin using! Depending on your technical requirements and resources, we can also help you set up a web host to run the application, or you can integrate it into your existing infrastructure yourself.',
+ bold: false,
+ italic: false,
+ strikethrough: false,
+ code: false,
+ href: null,
+ },
+ ],
+ image: {
+ url: 'https://images.ctfassets.net/dz50cburkkql/f9mRdZEBAduwVSyZtLOxN/9665ce8b95b4b19b5b950d09836a8a3a/handshake-xxl.png',
+ description: '',
+ },
+ },
+ {
+ header: 'Testing Period',
+ body: [
+ {
+ text: 'After we hand off the project, you will have a trial period of two months to test out the application. We’ll continue to provide technical support during this period, so if anything breaks or you discover bugs, we’ll happily fix them.',
+ bold: false,
+ italic: false,
+ strikethrough: false,
+ code: false,
+ href: null,
+ },
+ ],
+ image: {
+ url: 'https://images.ctfassets.net/dz50cburkkql/4XgI5xF1nUSRnKeC0jVOc2/47b53f321eef3eac83eff622600b75e4/code-xxl.png',
+ description: '',
+ },
+ },
+ ],
+ faqs: [
+ {
+ question: 'What types of projects do you work on?',
+ answer: [
+ {
+ text: 'We work on a variety web based applications. These can range from volunteer management systems to automated SMS chatbots. Some examples of tools to consider include:\n• A platform that allows you to keep track of the organization’s activities and see where cost-cutting can occur.\n• A database system that allows you to more easily search through previous financial, client, or service records.\n• An application to keep track of your clients and better assess your overall impact.\nMake sure also to visit our ',
+ bold: false,
+ italic: false,
+ strikethrough: false,
+ code: false,
+ href: null,
+ },
+ {
+ text: 'completed projects page',
+ bold: false,
+ italic: false,
+ strikethrough: false,
+ code: false,
+ href: 'https://upenn.hack4impact.org/projects',
+ },
+ {
+ text: ' to see what types of applications we’ve built in the past.\nOur main focus is on software engineering and developer education, so our current offerings ',
+ bold: false,
+ italic: false,
+ strikethrough: false,
+ code: false,
+ href: null,
+ },
+ {
+ text: 'do not',
+ bold: true,
+ italic: false,
+ strikethrough: false,
+ code: false,
+ href: null,
+ },
+ {
+ text: ' include:\n• ',
+ bold: false,
+ italic: false,
+ strikethrough: false,
+ code: false,
+ href: null,
+ },
+ {
+ text: 'Basic website design and development.',
+ bold: true,
+ italic: false,
+ strikethrough: false,
+ code: false,
+ href: null,
+ },
+ {
+ text: '\nWe do not build websites that are purely content-based, such as Wordpress blogs or landing pages.\n• ',
+ bold: false,
+ italic: false,
+ strikethrough: false,
+ code: false,
+ href: null,
+ },
+ {
+ text: 'Technology integration or consulting.',
+ bold: true,
+ italic: false,
+ strikethrough: false,
+ code: false,
+ href: null,
+ },
+ {
+ text: '\nWe do not set up or integrate existing solutions such as Salesforce or Drupal.',
+ bold: false,
+ italic: false,
+ strikethrough: false,
+ code: false,
+ href: null,
+ },
+ ],
+ },
+ {
+ question: 'When can I apply?',
+ answer: [
+ {
+ text: 'We source projects for both the Fall and Spring semesters around June and November respectively. Please fill out our ',
+ bold: false,
+ italic: false,
+ strikethrough: false,
+ code: false,
+ href: null,
+ },
+ {
+ text: 'nonprofit interest form',
+ bold: false,
+ italic: false,
+ strikethrough: false,
+ code: false,
+ href: 'https://docs.google.com/forms/d/e/1FAIpQLSc6HT8WyxjWdugR1OULl80xE1Aaz1-dMOo5c179DE4EawOYHg/viewform',
+ },
+ {
+ text: ' and keep an eye on this page for exact dates.',
+ bold: false,
+ italic: false,
+ strikethrough: false,
+ code: false,
+ href: null,
+ },
+ ],
+ },
+ {
+ question: 'How much does it cost?',
+ answer: [
+ {
+ text: 'We generally charge $1000 per project. Our pricing is need-based, and if your organization is not able to afford the cost, we will be happy to lower or even waive the fee. All client contributions feed right back into Hack4Impact to pay for professional development events, speakers, organization retreats, and operating costs.',
+ bold: false,
+ italic: false,
+ strikethrough: false,
+ code: false,
+ href: null,
+ },
+ ],
+ },
+ {
+ question: 'How long do projects take?',
+ answer: [
+ {
+ text: 'Most projects run for about a semester, so about 3 months of development. For the month leading up to that development period, your tech lead and project manager will with you to plan the project. If you are interested in doing a larger project, share your idea to ',
+ bold: false,
+ italic: false,
+ strikethrough: false,
+ code: false,
+ href: null,
+ },
+ {
+ text: 'penn@hack4impat.org',
+ bold: false,
+ italic: false,
+ strikethrough: false,
+ code: false,
+ href: 'mailto:penn@hack4impact.org',
+ },
+ {
+ text: ' and we will see what we can do!',
+ bold: false,
+ italic: false,
+ strikethrough: false,
+ code: false,
+ href: null,
+ },
+ ],
+ },
+ {
+ question: 'What technologies do you use?',
+ answer: [
+ {
+ text: 'Currently, most of our projects are build using React and Typescript/Node.js with an Express and MongoDB backend. Feel free to check out our TypeScript boilerplate ',
+ bold: false,
+ italic: false,
+ strikethrough: false,
+ code: false,
+ href: null,
+ },
+ {
+ text: 'here',
+ bold: false,
+ italic: false,
+ strikethrough: false,
+ code: false,
+ href: 'https://github.com/Orang-utan/ts-boilerplate',
+ },
+ {
+ text: '.',
+ bold: false,
+ italic: false,
+ strikethrough: false,
+ code: false,
+ href: null,
+ },
+ ],
+ },
+ {
+ question: 'Do you build mobile apps?',
+ answer: [
+ {
+ text: 'Historically the Penn chapter of Hack4Impact has focused on mainly web development focused projects. If your project specifically needs a mobile app feel free to contact us at ',
+ bold: false,
+ italic: false,
+ strikethrough: false,
+ code: false,
+ href: null,
+ },
+ {
+ text: 'penn@hack4impact.org',
+ bold: false,
+ italic: false,
+ strikethrough: false,
+ code: false,
+ href: 'mailto:penn@hack4impact.org',
+ },
+ {
+ text: ' and we can connect you with another chapter.',
+ bold: false,
+ italic: false,
+ strikethrough: false,
+ code: false,
+ href: null,
+ },
+ ],
+ },
+ ],
+ },
+};
+
+export default FALLBACK_APPLICATION_CONTENT;
diff --git a/utils/fetchContent.js b/utils/fetchContent.js
index b49a88e..cdf3a9a 100644
--- a/utils/fetchContent.js
+++ b/utils/fetchContent.js
@@ -1,4 +1,5 @@
import { Client } from '@notionhq/client';
+import FALLBACK_APPLICATION_CONTENT from './fallbackApplicationContent';
// Initialize the Notion client
const notion = new Client({
@@ -8,6 +9,8 @@ const notion = new Client({
const space = process.env.NEXT_PUBLIC_CONTENTFUL_SPACE_ID;
const accessToken = process.env.NEXT_PUBLIC_CONTENTFUL_ACCESS_TOKEN;
+const PLACEHOLDER_IMAGE = '/images/placeholder.webp';
+
const safeExtract = (value, fallback = '') => {
const result = extractPlainText(value || []);
return result || fallback;
@@ -16,7 +19,20 @@ const safeExtract = (value, fallback = '') => {
// Helper function to extract plain text from Notion rich text
function extractPlainText(richTextArray) {
if (!richTextArray || !Array.isArray(richTextArray)) return '';
- return richTextArray.map(item => item.plain_text || '').join('');
+ return richTextArray.map((item) => item.plain_text || '').join('');
+}
+
+// Preserves per-segment formatting (bold, italic, etc.) that extractPlainText discards
+function extractRichTextSegments(richTextArray) {
+ if (!richTextArray || !Array.isArray(richTextArray)) return [];
+ return richTextArray.map((item) => ({
+ text: item.plain_text || '',
+ bold: item.annotations?.bold || false,
+ italic: item.annotations?.italic || false,
+ strikethrough: item.annotations?.strikethrough || false,
+ code: item.annotations?.code || false,
+ href: item.href || null,
+ }));
}
// Helper function to convert Notion page properties for members/chapters
@@ -24,59 +40,113 @@ function formatMemberData(page) {
const properties = page.properties;
return {
name: safeExtract(properties.name?.title),
- title: properties.title?.select?.name,
+ title: properties.title?.select?.name || '',
image: {
- url: properties.image?.url || ''
+ url: properties.image?.url || PLACEHOLDER_IMAGE,
},
linkedIn: properties.linkedIn?.url || properties.linkedin?.url || '',
classOf: properties.class?.select?.name || '',
- urlSlug: safeExtract(properties.urlSlug?.rich_text || properties.url_slug?.rich_text || []) ||
- safeExtract(properties.name?.title || properties.Name?.title || []).toLowerCase().replace(/\s+/g, '-'),
+ urlSlug:
+ safeExtract(properties.urlSlug?.rich_text || properties.url_slug?.rich_text || []) ||
+ safeExtract(properties.name?.title || properties.Name?.title || [])
+ .toLowerCase()
+ .replace(/\s+/g, '-'),
status: properties.status?.status?.name || '',
email: properties.email?.email || '',
joined: properties.joined?.select?.name || '',
- bio: safeExtract(properties.bio?.rich_text || [])
+ bio: safeExtract(properties.bio?.rich_text || []),
};
}
function formatProjectData(page) {
const properties = page.properties;
-
+
return {
title: safeExtract(properties.name?.title),
- team: properties.team?.people?.map(person => person.name || person.id).join(', ') || '',
+ team: properties.team?.people?.map((person) => person.name || person.id).join(', ') || '',
description: safeExtract(properties.description?.rich_text || []),
thumbnail: {
- url: properties.thumbnail?.url || '',
- description: safeExtract(properties.thumbnailDescription?.rich_text) || ''
+ url: properties.thumbnail?.url || PLACEHOLDER_IMAGE,
+ description: safeExtract(properties.thumbnailDescription?.rich_text) || '',
},
- urlSlug: safeExtract(properties.urlSlug?.rich_text) ||
- safeExtract(properties.name?.title).toLowerCase().replace(/\s+/g, '-') || '',
- completedIn: properties.semester?.select?.name || 'Unknown'
+ urlSlug:
+ safeExtract(properties.urlSlug?.rich_text) ||
+ safeExtract(properties.name?.title).toLowerCase().replace(/\s+/g, '-') ||
+ '',
+ completedIn: properties.semester?.select?.name || 'Unknown',
+ };
+}
+
+function formatPartnerData(page) {
+ const properties = page.properties;
+ return {
+ name: safeExtract(properties.name?.title),
+ logoUrl: properties.logoUrl?.url || PLACEHOLDER_IMAGE,
+ link: properties.link?.url || '',
+ tier: properties.tier?.select?.name || '',
+ order: properties.order?.number ?? 0,
};
}
function formatRichTextForContentBlock(richTextArray) {
if (!richTextArray || !Array.isArray(richTextArray)) return '';
-
+
return safeExtract(richTextArray);
}
+// Helper function to read a database row's title property regardless of its column name
+function getTitleText(properties) {
+ const titleProp = Object.values(properties).find((prop) => prop.type === 'title');
+ return safeExtract(titleProp?.title || []);
+}
+
+function formatTimelineStep(page) {
+ const properties = page.properties;
+ return {
+ header: safeExtract(properties.header?.rich_text || []),
+ body: extractRichTextSegments(properties.bodyText?.rich_text || []),
+ image: {
+ url: properties.imageUrl?.url || PLACEHOLDER_IMAGE,
+ description: safeExtract(properties.imageDescription?.rich_text || []),
+ },
+ };
+}
+
+function formatTestimonial(page) {
+ const properties = page.properties;
+ return {
+ author: safeExtract(properties.author?.rich_text || []),
+ quote: safeExtract(properties.quote?.rich_text || []),
+ };
+}
+
+function formatFaq(page) {
+ const properties = page.properties;
+ return {
+ question: safeExtract(properties.question?.rich_text || []),
+ answer: extractRichTextSegments(properties.answer?.rich_text || []),
+ };
+}
+
// Helper function to parse comma-separated feature data
function parseFeatureCollection(featureImagesText, featureDescriptionsText) {
if (!featureImagesText) return [];
-
- const imageUrls = featureImagesText.split(',').map(url => url.trim()).filter(url => url);
- const descriptions = featureDescriptionsText ?
- featureDescriptionsText.split(',').map(desc => desc.trim()) : [];
-
+
+ const imageUrls = featureImagesText
+ .split(',')
+ .map((url) => url.trim())
+ .filter((url) => url);
+ const descriptions = featureDescriptionsText
+ ? featureDescriptionsText.split(',').map((desc) => desc.trim())
+ : [];
+
return imageUrls.map((url, index) => ({
header: `Feature ${index + 1}`,
image: {
url: url,
- description: descriptions[index] || `Feature ${index + 1} screenshot`
+ description: descriptions[index] || `Feature ${index + 1} screenshot`,
},
- body: descriptions[index] || `Description for feature ${index + 1}` // ← Return plain text, not wrapped in json
+ body: descriptions[index] || `Description for feature ${index + 1}`, // ← Return plain text, not wrapped in json
}));
}
@@ -87,11 +157,11 @@ export async function fetchProjectDetail(urlSlug) {
filter: {
property: 'urlSlug',
rich_text: {
- equals: urlSlug
- }
- }
+ equals: urlSlug,
+ },
+ },
});
-
+
if (!projectResponse.results.length) {
return null;
}
@@ -103,36 +173,44 @@ export async function fetchProjectDetail(urlSlug) {
title: safeExtract(properties.title?.title || properties.name?.title || []),
description: safeExtract(properties.description?.rich_text || []),
thumbnail: {
- url: properties.thumbnail?.url || properties.thumbnail?.files?.[0]?.file?.url || '',
- description: safeExtract(properties.thumbnail_description?.rich_text || [])
+ url:
+ properties.thumbnail?.url ||
+ properties.thumbnail?.files?.[0]?.file?.url ||
+ PLACEHOLDER_IMAGE,
+ description: safeExtract(properties.thumbnail_description?.rich_text || []),
},
finalProductLink: properties.final_product_link?.url || '',
codeRepoLink: properties.code_repo_link?.url || '',
- technologiesUsed: properties.technologies_used?.multi_select?.map(tech => tech.name).join(', ') || '',
+ technologiesUsed:
+ properties.technologies_used?.multi_select?.map((tech) => tech.name).join(', ') || '',
project: formatRichTextForContentBlock(properties.about_project?.rich_text || []),
- client: formatRichTextForContentBlock(properties.client?.rich_text || [{ plain_text: properties.client?.url || '' }]),
+ client: formatRichTextForContentBlock(
+ properties.client?.rich_text || [{ plain_text: properties.client?.url || '' }],
+ ),
impact: formatRichTextForContentBlock(properties.impact?.rich_text),
featuresCollection: {
items: parseFeatureCollection(
safeExtract(properties.feature_images?.rich_text || []),
- safeExtract(properties.feature_descriptions?.rich_text || [])
- )
+ safeExtract(properties.feature_descriptions?.rich_text || []),
+ ),
},
testimonialsCollection: {
- items: []
+ items: [],
},
pmtlCollection: {
- items: properties.pmtl?.people?.map(person => ({
- name: person.name || 'Team Member',
- image: { url: person.avatar_url || '' },
- })) || []
+ items:
+ properties.pmtl?.people?.map((person) => ({
+ name: person.name || 'Team Member',
+ image: { url: person.avatar_url || PLACEHOLDER_IMAGE },
+ })) || [],
},
teamMembersCollection: {
- items: properties.team?.people?.map(person => ({
- name: person.name || 'Team Member',
- image: { url: person.avatar_url || '' },
- })) || []
- }
+ items:
+ properties.team?.people?.map((person) => ({
+ name: person.name || 'Team Member',
+ image: { url: person.avatar_url || PLACEHOLDER_IMAGE },
+ })) || [],
+ },
};
return formattedProject;
@@ -142,6 +220,92 @@ export async function fetchProjectDetail(urlSlug) {
}
}
+// applicationType is either 'Students' or 'Organizations', matching the title
+// values in the Applications database and the applicationType select options
+// in the Timeline/Testimonials/FAQs databases.
+export async function fetchApplicationContent(applicationType) {
+ const empty = {
+ applicationLink: '',
+ openRolesLink: '',
+ description: '',
+ timelineCollection: { items: [] },
+ testimonialsCollection: { items: [] },
+ faqsCollection: { items: [] },
+ };
+
+ const [applicationResult, timelineResult, testimonialsResult, faqsResult] =
+ await Promise.allSettled([
+ notion.databases.query({ database_id: process.env.NOTION_APPLICATIONS_DATABASE_ID }),
+ notion.databases.query({
+ database_id: process.env.NOTION_TIMELINE_DATABASE_ID,
+ filter: { property: 'applicationType', select: { equals: applicationType } },
+ sorts: [{ property: 'order', direction: 'ascending' }],
+ }),
+ notion.databases.query({
+ database_id: process.env.NOTION_TESTIMONIALS_DATABASE_ID,
+ filter: { property: 'applicationType', select: { equals: applicationType } },
+ sorts: [{ property: 'order', direction: 'ascending' }],
+ }),
+ notion.databases.query({
+ database_id: process.env.NOTION_FAQS_DATABASE_ID,
+ filter: { property: 'applicationType', select: { equals: applicationType } },
+ sorts: [{ property: 'order', direction: 'ascending' }],
+ }),
+ ]);
+
+ const appPage =
+ applicationResult.status === 'fulfilled' &&
+ applicationResult.value.results.find(
+ (page) => getTitleText(page.properties) === applicationType,
+ );
+
+ if (appPage) {
+ const properties = appPage.properties;
+ empty.applicationLink = properties.applicationLink?.url || '';
+ empty.openRolesLink = properties.openRolesLink?.url || '';
+ empty.description = safeExtract(properties.description?.rich_text || []);
+ } else {
+ if (applicationResult.status === 'rejected') {
+ console.error(
+ `Error fetching application info for ${applicationType}:`,
+ applicationResult.reason,
+ );
+ }
+ const fallback = FALLBACK_APPLICATION_CONTENT[applicationType]?.application;
+ if (fallback) {
+ empty.applicationLink = fallback.applicationLink;
+ empty.openRolesLink = fallback.openRolesLink;
+ empty.description = fallback.description;
+ }
+ }
+
+ if (timelineResult.status === 'fulfilled' && timelineResult.value.results.length > 0) {
+ empty.timelineCollection.items = timelineResult.value.results.map(formatTimelineStep);
+ } else {
+ if (timelineResult.status === 'rejected') {
+ console.error(`Error fetching timeline for ${applicationType}:`, timelineResult.reason);
+ }
+ empty.timelineCollection.items = FALLBACK_APPLICATION_CONTENT[applicationType]?.timeline || [];
+ }
+
+ if (testimonialsResult.status === 'fulfilled') {
+ empty.testimonialsCollection.items = testimonialsResult.value.results.map(formatTestimonial);
+ } else {
+ console.error(`Error fetching testimonials for ${applicationType}:`, testimonialsResult.reason);
+ }
+
+ if (faqsResult.status === 'fulfilled' && faqsResult.value.results.length > 0) {
+ empty.faqsCollection.items = faqsResult.value.results.map(formatFaq);
+ } else {
+ if (faqsResult.status === 'rejected') {
+ console.error(`Error fetching FAQs for ${applicationType}:`, faqsResult.reason);
+ }
+ empty.faqsCollection.items = FALLBACK_APPLICATION_CONTENT[applicationType]?.faqs || [];
+ }
+
+ return empty;
+}
+
export async function fetchMemberDetail(urlSlug) {
try {
const memberResponse = await notion.databases.query({
@@ -149,31 +313,31 @@ export async function fetchMemberDetail(urlSlug) {
filter: {
property: 'urlSlug',
rich_text: {
- equals: urlSlug
- }
- }
+ equals: urlSlug,
+ },
+ },
});
-
+
if (!memberResponse.results.length) {
return null;
}
const memberData = memberResponse.results[0];
const properties = memberData.properties;
-
+
// Format the data for the member detail page
const formattedMember = {
name: safeExtract(properties.name?.title || []),
title: properties.title?.select?.name || '',
image: {
- url: properties.image?.url || '',
- description: safeExtract(properties.name?.title || []) + ' profile photo'
+ url: properties.image?.url || PLACEHOLDER_IMAGE,
+ description: safeExtract(properties.name?.title || []) + ' profile photo',
},
linkedIn: properties.linkedin?.url || '',
bio: safeExtract(properties.bio?.rich_text || []),
classOf: properties.class?.select?.name || '',
email: properties.email?.email || '',
- github: properties.github?.url || ''
+ github: properties.github?.url || '',
};
return formattedMember;
@@ -194,8 +358,8 @@ export async function fetchNotionContent(type, options = {}) {
});
return {
memberCollection: {
- items: memberResponse.results.map(formatMemberData)
- }
+ items: memberResponse.results.map(formatMemberData),
+ },
};
case 'projects':
@@ -204,26 +368,57 @@ export async function fetchNotionContent(type, options = {}) {
sort: {
property: 'completion',
direction: 'descending',
- }
+ },
});
return {
pennWebsiteLayout: {
projectsCollection: {
- items: projectResponse.results.map(formatProjectData)
- }
- }
+ items: projectResponse.results.map(formatProjectData),
+ },
+ },
+ };
+
+ case 'partners':
+ const partnerResponse = await notion.databases.query({
+ database_id: process.env.NOTION_PARTNERS_DATABASE_ID,
+ });
+ return {
+ partnerCollection: {
+ items: partnerResponse.results.map(formatPartnerData),
+ },
};
case 'homepage':
- // Fetch both chapters and projects for homepage
- const [chaptersData, projectsData] = await Promise.all([
+ // Fetch chapters, projects, and partners for homepage.
+ // allSettled so one failing (e.g. partners not configured) can't blank out the other two.
+ const [chaptersResult, projectsResult, partnersResult] = await Promise.allSettled([
fetchNotionContent('members'),
- fetchNotionContent('projects')
+ fetchNotionContent('projects'),
+ fetchNotionContent('partners'),
]);
+ if (chaptersResult.status === 'rejected') {
+ console.error('Error fetching members for homepage:', chaptersResult.reason);
+ }
+ if (projectsResult.status === 'rejected') {
+ console.error('Error fetching projects for homepage:', projectsResult.reason);
+ }
+ if (partnersResult.status === 'rejected') {
+ console.error('Error fetching partners for homepage:', partnersResult.reason);
+ }
return {
- chapterCollection: chaptersData.memberCollection,
- pennWebsiteLayout: projectsData.pennWebsiteLayout
+ chapterCollection:
+ chaptersResult.status === 'fulfilled'
+ ? chaptersResult.value.memberCollection
+ : { items: [] },
+ pennWebsiteLayout:
+ projectsResult.status === 'fulfilled'
+ ? projectsResult.value.pennWebsiteLayout
+ : { projectsCollection: { items: [] } },
+ partnerCollection:
+ partnersResult.status === 'fulfilled'
+ ? partnersResult.value.partnerCollection
+ : { items: [] },
};
default:
@@ -234,7 +429,7 @@ export async function fetchNotionContent(type, options = {}) {
message: error.message,
code: error.code,
status: error.status,
- stack: error.stack
+ stack: error.stack,
});
throw error;
}
@@ -244,7 +439,7 @@ export async function fetchNotionContent(type, options = {}) {
export async function fetchContent(query) {
try {
const res = await fetch(
- `https://graphql.contentful.com/content/v1/spaces/${space}/environments/master`,
+ `https://graphql.contentful.com/content/v1/spaces/${space}/environments/main`,
{
method: 'POST',
headers: {