Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
928f81f
feat(faq-page): added model and controller for faq
willjinjin Apr 27, 2026
67ca058
feat(faq-backend): added route for faq
willjinjin Apr 27, 2026
fb4e8e2
feat(faq-backend): added route and updated index to accomodate faq
willjinjin Apr 27, 2026
0375cd4
feat(faq-backend): added try catch error handling
willjinjin Apr 27, 2026
d7094ac
feat(faq-backend_: updated faq to fetch faq data from mongodb instead…
willjinjin Apr 27, 2026
cd5744a
Merge branch 'feat/faq-page' into feat/faq-backend
willjinjin May 3, 2026
1964756
chore(faq-backend): merged main into faq-backend
willjinjin May 3, 2026
86766b9
chore(faq-backend): ran prettier on faq backend related pages
willjinjin May 3, 2026
0fb0bcf
chore(faq-backend): merged main into branch
willjinjin May 10, 2026
27f140a
chore(faq-backend): removed placeholder for faq
willjinjin May 10, 2026
ba4bf13
feat(faq-backend): implemented timer-based caching for faq page
willjinjin May 10, 2026
07178bd
chore(faq-backend): changed faq function name to getFaqs, also change…
willjinjin May 10, 2026
1d73ab3
feat(faq-backend): created createFaq function for faqs
willjinjin May 10, 2026
780da47
feat(faq-backend): created updateFaq function for faq page
willjinjin May 10, 2026
3f2948b
feat(faq-backend): created deleteFaq function for faq page
willjinjin May 10, 2026
dfb8103
fix(faq-backend): fixed a route error where all 4 used get
willjinjin May 10, 2026
175e18c
feat(faq-frontend): updated frontend to accomodate new faq schema by …
willjinjin May 10, 2026
455726c
chore(faq-backend): updated fetch to use axios
willjinjin May 17, 2026
8de9879
fix(faq-backend): fixed faq route to correctly handle requests
willjinjin May 19, 2026
ef2b4ca
feat(faq-backend): added new function to clear all faqs
willjinjin May 19, 2026
000f1fd
fix(faq-backend): updated faq cache expire time to 1 week
willjinjin May 24, 2026
65523f0
feat(faq-backend): added flag to signal cache to refresh when update …
willjinjin May 24, 2026
1ec1f64
feat: add content backend with model, controller and routes
harrywu23 Jun 1, 2026
d3311b6
Merge branch 'main' into feat/content-backend
RLee64 Jun 2, 2026
8fa469c
global .env ignore
RLee64 Jun 2, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
/node_modules
.DS_Store
.env
2 changes: 0 additions & 2 deletions client/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -22,5 +22,3 @@ dist-ssr
*.njsproj
*.sln
*.sw?

.env
36 changes: 34 additions & 2 deletions client/src/pages/Faq.tsx
Original file line number Diff line number Diff line change
@@ -1,15 +1,47 @@
import React, { useState, useEffect } from "react";
import Collapsible from "../components/Collapsible";
import api from "../api";

import "../style/faq.css";
import "../style/common.css";
import faqs from "../placeholders/faqs.json";

interface Faq {
question: string;
answer: string;
}

function parseFaq(content: string): Faq[] {
const lines = content.split("\n");
const faqs: Faq[] = [];
let current: Faq | null = null;

for (const line of lines) {
if (line.startsWith("#")) {
if (current) faqs.push(current);
current = { question: line.slice(1).trim(), answer: "" };
} else if (current) {
current.answer += (current.answer ? "\n" : "") + line;
}
}

if (current) faqs.push(current);
return faqs;
}

const Faq = () => {
const [faqs, setFaqs] = useState<Faq[]>([]);

useEffect(() => {
api
.get<{ content: string }[]>("/faqs")
.then((res) => setFaqs(res.data.flatMap((faq) => parseFaq(faq.content))))
.catch((err) => console.error("Failed to fetch faqs:", err));
}, []);

return (
<div className="faq-container bg-yellow-light">
{/** title */}
<section className="faq-title">F&nbsp;A&nbsp;Q&nbsp;s</section>

{/** faq items */}
<section className="faq-list">
{faqs.map((item, index) => (
Expand Down
26 changes: 0 additions & 26 deletions client/src/placeholders/faqs.json

This file was deleted.

1 change: 0 additions & 1 deletion server/.gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,2 @@
node_modules
build
.env
88 changes: 88 additions & 0 deletions server/src/controllers/contentController.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
import { RequestHandler } from "express";
import { Content } from "../model/content";

const CACHE_TTL_MS = 1000 * 60 * 60 * 24 * 7;

let contentCache: { data: unknown[]; expiresAt: number } | null = null;
let contentCacheStale = false;

const markContentCacheStale = () => {
contentCacheStale = true;
};

export const getContents: RequestHandler = async (req, res) => {
try {
if (
contentCache &&
!contentCacheStale &&
Date.now() < contentCache.expiresAt
) {
res.json(contentCache.data);
return;
}
const contents = await Content.find().lean();
contentCache = { data: contents, expiresAt: Date.now() + CACHE_TTL_MS };
contentCacheStale = false;
res.json(contents);
} catch (error) {
console.error("Error fetching contents:", error);
res.status(500).json({ message: "Failed to fetch contents" });
}
};

export const createContent: RequestHandler = async (req, res) => {
try {
const content = await Content.create(req.body);
markContentCacheStale();
res.status(201).json(content);
} catch (error) {
console.error("Error creating content:", error);
res.status(500).json({ message: "Failed to create content" });
}
};

export const updateContent: RequestHandler = async (req, res) => {
try {
const updated = await Content.findByIdAndUpdate(req.params.id, req.body, {
returnDocument: "after",
}).lean();
if (!updated) {
res.status(404).json({ message: "Content not found" });
return;
}
markContentCacheStale();
res.json(updated);
} catch (error) {
console.error("Error updating content: ", error);
res.status(500).json({ message: "Failed to update content" });
}
};

export const deleteContent: RequestHandler = async (req, res) => {
try {
const deleted = await Content.findByIdAndDelete(req.params.id);
if (!deleted) {
res.status(404).json({ message: "Content not found" });
return;
}
markContentCacheStale();
res.json({ message: "Content deleted successfully" });
} catch (error) {
console.error("Error deleting content: ", error);
res.status(500).json({ message: "Failed to delete content" });
}
};

export const deleteAllContents: RequestHandler = async (req, res) => {
try {
const result = await Content.deleteMany({});
markContentCacheStale();
res.json({
message: "All contents deleted successfully",
deletedCount: result.deletedCount,
});
} catch (error) {
console.error("Error deleting all contents: ", error);
res.status(500).json({ message: "Failed to delete all contents" });
}
};
81 changes: 81 additions & 0 deletions server/src/controllers/faqController.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
import { RequestHandler } from "express";
import { Faq } from "../model/faq";

const CACHE_TTL_MS = 1000 * 60 * 60 * 24 * 7;

let faqCache: { data: unknown[]; expiresAt: number } | null = null;
let faqCacheStale = false;

const markFaqCacheStale = () => {
faqCacheStale = true;
};

export const getFaqs: RequestHandler = async (req, res) => {
try {
if (faqCache && !faqCacheStale && Date.now() < faqCache.expiresAt) {
res.json(faqCache.data);
return;
}
const faqs = await Faq.find().lean();
faqCache = { data: faqs, expiresAt: Date.now() + CACHE_TTL_MS };
faqCacheStale = false;
res.json(faqs);
} catch (error) {
console.error("Error fetching FAQs:", error);
res.status(500).json({ message: "Failed to fetch FAQs" });
}
};

export const createFaq: RequestHandler = async (req, res) => {
try {
const faq = await Faq.create(req.body);
markFaqCacheStale();
res.status(201).json(faq);
} catch (error) {
console.error("Error creating FAQ:", error);
res.status(500).json({ message: "Failed to create FAQ" });
}
};

export const updateFaq: RequestHandler = async (req, res) => {
try {
const updated = await Faq.findByIdAndUpdate(req.params.id, req.body, {
returnDocument: "after",
}).lean();
if (!updated) {
res.status(404).json({ message: "FAQ not found" });
return;
}
markFaqCacheStale();
res.json(updated);
} catch (error) {
console.error("Error updating FAQ: ", error);
res.status(500).json({ message: "Failed to update FAQ" });
}
};

export const deleteFaq: RequestHandler = async (req, res) => {
try {
const deleted = await Faq.findByIdAndDelete(req.params.id);
if (!deleted) {
res.status(404).json({ message: "FAQ not found" });
return;
}
markFaqCacheStale();
res.json({ message: "FAQ deleted successfully" });
} catch (error) {
console.error("Error deleting FAQ: ", error);
res.status(500).json({ message: "Failed to delete FAQ" });
}
};

export const deleteAllFaqs: RequestHandler = async(req, res) => {
try {
const result = await Faq.deleteMany({});
markFaqCacheStale();
res.json({message: "All FAQs deleted successfully", deletedCount: result.deletedCount });
} catch (error) {
console.error("Error deleting all FAQs: ", error);
res.status(500).json({message: "Failed to delete all FAQs"});
}
};
4 changes: 4 additions & 0 deletions server/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ import { Strategy as GoogleStrategy } from "passport-google-oauth20";
import authRoutes from "./routes/authRoutes";
import imageRoutes from "./routes/imageRoutes";
import executivesRoutes from "./routes/executivesRoutes";
import faqRoutes from "./routes/faqRoutes";
import contentRoutes from "./routes/contentRoutes";
import paymentRoutes from "./routes/paymentRoutes";
import webhookRoutes from "./routes/webhookRoutes";
import sponsorRoutes from "./routes/sponsorRoutes";
Expand Down Expand Up @@ -68,6 +70,8 @@ app.use(express.json());
app.use("/api/auth", authRoutes);
app.use("/api/images", imageRoutes);
app.use("/api/executives", executivesRoutes);
app.use("/api/faqs", faqRoutes);
app.use("/api/contents", contentRoutes);
app.use("/api/payments", paymentRoutes);
app.use("/api/sponsors", sponsorRoutes);
app.use("/api/contacts", contactRoutes);
Expand Down
12 changes: 12 additions & 0 deletions server/src/model/content.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import mongoose from "mongoose";
const { Schema, model } = mongoose;

const contentSchema = new Schema(
{
_id: { type: String, required: true },
content: { type: String, required: true },
},
{ versionKey: false, timestamps: true }
);

export const Content = model("Content", contentSchema);
11 changes: 11 additions & 0 deletions server/src/model/faq.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import mongoose from "mongoose";
const { Schema, model } = mongoose;

const faqSchema = new Schema(
{
content: { type: String, required: true },
},
{ versionKey: false, timestamps: true }
);

export const Faq = model("Faq", faqSchema);
17 changes: 17 additions & 0 deletions server/src/routes/contentRoutes.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import express from "express";
import {
getContents,
createContent,
updateContent,
deleteContent,
deleteAllContents,
} from "../controllers/contentController";

const router = express.Router();
router.get("/", getContents);
router.post("/", createContent);
router.put("/:id", updateContent);
router.delete("/:id", deleteContent);
router.delete("/", deleteAllContents);

export default router;
17 changes: 17 additions & 0 deletions server/src/routes/faqRoutes.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import express from "express";
import {
getFaqs,
createFaq,
updateFaq,
deleteFaq,
deleteAllFaqs,
} from "../controllers/faqController";

const router = express.Router();
router.get("/", getFaqs);
router.post("/", createFaq);
router.put("/:id", updateFaq);
router.delete("/:id", deleteFaq);
router.delete("/", deleteAllFaqs);

export default router;
Loading