diff --git a/src/app/admin/page.tsx b/src/app/admin/page.tsx index 14e58ffe..357e818a 100644 --- a/src/app/admin/page.tsx +++ b/src/app/admin/page.tsx @@ -16,7 +16,7 @@ import { doc, updateDoc } from "firebase/firestore"; import { db } from "@/lib/firebase"; import { Button } from "@/components/ui/button"; -const apClasses = apClassesData.apClasses; +const apClasses = [...apClassesData.apClasses, "AP Porting"]; const Page = () => { const { user } = useUser(); diff --git a/src/app/admin/subject/[slug]/page.tsx b/src/app/admin/subject/[slug]/page.tsx index 3fb60a73..374ac4c4 100644 --- a/src/app/admin/subject/[slug]/page.tsx +++ b/src/app/admin/subject/[slug]/page.tsx @@ -20,6 +20,7 @@ import { cn, formatSlug } from "@/lib/utils"; import short from "short-uuid"; import type { Subject, Unit } from "@/types/firestore"; import UnitComponent from "./_components/unit"; +import { DEFAULT_PORTING_SUBJECT } from "@/lib/apPortingDefaults"; const translator = short(short.constants.flickrBase58); @@ -29,7 +30,7 @@ function generateShortId() { return timestamp + randomPart; } -const apClasses = apClassesData.apClasses; +const apClasses = [...apClassesData.apClasses, "AP Porting"]; // Default empty subject structure const emptyData: Subject = { @@ -68,6 +69,7 @@ export default function Page({ params }: { params: { slug: string } }) { const { user, error, setError, setLoading } = useUser(); const [subjectTitle, setSubjectTitle] = useState(""); const [units, setUnits] = useState([]); + const [resetting, setResetting] = useState(false); const [subjectLoading, setSubjectLoading] = useState(true); const [unsavedChanges, setUnsavedChanges] = useState(false); @@ -172,6 +174,85 @@ export default function Page({ params }: { params: { slug: string } }) { * If you dont want this, use the commented out handleSave. ****************************************************/ + const handleReset = async () => { + if (!confirm("Are you sure you want to reset the AP Porting course? This will restore all articles, MCQs, and structure to their default values and delete any custom changes you made.")) { + return; + } + setResetting(true); + try { + // 1. Fetch and delete existing structure/articles/tests under /subjects/porting + const unitsCollectionRef = collection(db, "subjects", "porting", "units"); + const unitsSnap = await getDocs(unitsCollectionRef); + + const batch = writeBatch(db); + + for (const unitDoc of unitsSnap.docs) { + const chaptersCollectionRef = collection(db, "subjects", "porting", "units", unitDoc.id, "chapters"); + const chaptersSnap = await getDocs(chaptersCollectionRef); + chaptersSnap.forEach((chapterDoc) => { + batch.delete(chapterDoc.ref); + }); + + const testsCollectionRef = collection(db, "subjects", "porting", "units", unitDoc.id, "tests"); + const testsSnap = await getDocs(testsCollectionRef); + testsSnap.forEach((testDoc) => { + batch.delete(testDoc.ref); + }); + + batch.delete(unitDoc.ref); + } + + batch.delete(doc(db, "subjects", "porting")); + await batch.commit(); + + // 2. Write defaults + const writeBatchObj = writeBatch(db); + writeBatchObj.set(doc(db, "subjects", "porting"), { + title: DEFAULT_PORTING_SUBJECT.title, + units: DEFAULT_PORTING_SUBJECT.units, + }); + + for (const unit of DEFAULT_PORTING_SUBJECT.units) { + const unitRef = doc(db, "subjects", "porting", "units", unit.id); + writeBatchObj.set(unitRef, unit); + + for (const chapter of unit.chapters) { + const chapterRef = doc(db, "subjects", "porting", "units", unit.id, "chapters", chapter.id); + writeBatchObj.set(chapterRef, { + id: chapter.id, + createdAt: new Date(), + author: "System Default", + title: `${unit.id}/chapter/${chapter.id}`, + isPublic: chapter.isPublic ?? true, + }); + } + + if (unit.tests) { + for (const test of unit.tests) { + const testRef = doc(db, "subjects", "porting", "units", unit.id, "tests", test.id); + writeBatchObj.set(testRef, { + ...test, + instanceId: `porting_${unit.id}_test_${test.id}`, + }); + } + } + } + + await writeBatchObj.commit(); + + setSubjectTitle(DEFAULT_PORTING_SUBJECT.title); + setUnits(DEFAULT_PORTING_SUBJECT.units); + setUnsavedChanges(false); + + alert("AP Porting course reset successfully."); + } catch (error) { + console.error("Error resetting subject:", error); + alert("Error resetting subject. Check console for details."); + } finally { + setResetting(false); + } + }; + // Replaces save const handleSave = async () => { // Rebuild the Subject object from current state @@ -353,17 +434,28 @@ export default function Page({ params }: { params: { slug: string } }) { Return to Admin Dashboard - {(!subjectLoading || unsavedChanges) && ( - - )} +
+ {params.slug === "porting" && ( + + )} + {(!subjectLoading || unsavedChanges) && ( + + )} +
{/* Subject Title */} diff --git a/src/app/sitemap.ts b/src/app/sitemap.ts index 0a7f68a3..170b52c4 100644 --- a/src/app/sitemap.ts +++ b/src/app/sitemap.ts @@ -20,7 +20,7 @@ export default async function sitemap(): Promise { priority, })); - const subjects = await getAllSubjects(); + const subjects = (await getAllSubjects()).filter((s) => s.slug !== "porting"); const subjectRoutes: MetadataRoute.Sitemap = []; for (const subject of subjects) { diff --git a/src/app/subject/[slug]/(sidebar)/layout.tsx b/src/app/subject/[slug]/(sidebar)/layout.tsx index 198a1f89..dd37cfa0 100644 --- a/src/app/subject/[slug]/(sidebar)/layout.tsx +++ b/src/app/subject/[slug]/(sidebar)/layout.tsx @@ -24,8 +24,14 @@ export default function Layout({ const [subject, setSubject] = useState(null); useEffect(() => { + if (user === undefined) return; + const fetchSubject = async () => { try { + const isAuthorized = user && (user.access === "admin" || user.access === "member" || user.access === "grader"); + if (params.slug === "porting" && !isAuthorized) { + return; + } const docRef = doc(db, "subjects", params.slug); const docSnap = await getDoc(docRef); if (docSnap.exists()) { @@ -39,7 +45,7 @@ export default function Layout({ fetchSubject().catch((error) => { console.error(error); }); - }, [params.slug]); + }, [params.slug, user]); // `{children}` must always render: this layout wraps server-rendered chapter // pages, and short-circuiting on the client `loading`/`error` state (which is diff --git a/src/app/subject/[slug]/(sidebar)/page.tsx b/src/app/subject/[slug]/(sidebar)/page.tsx index 9b37c680..e41dc81b 100644 --- a/src/app/subject/[slug]/(sidebar)/page.tsx +++ b/src/app/subject/[slug]/(sidebar)/page.tsx @@ -23,6 +23,12 @@ const Page = ({ params }: { params: { slug: string } }) => { useEffect(() => { const fetchSubject = async () => { try { + const isAuthorized = user && (user.access === "admin" || user.access === "member" || user.access === "grader"); + if (params.slug === "porting" && !isAuthorized) { + setError("Subject not found. That's probably us, not you."); + setLoading(false); + return; + } const docRef = doc(db, "subjects", params.slug); const docSnap = await getDoc(docRef); if (docSnap.exists()) { diff --git a/src/lib/apPortingDefaults.ts b/src/lib/apPortingDefaults.ts new file mode 100644 index 00000000..92cf8589 --- /dev/null +++ b/src/lib/apPortingDefaults.ts @@ -0,0 +1,9 @@ +import type { Subject } from "@/types/firestore"; +import type { OutputData } from "@editorjs/editorjs"; + +export const DEFAULT_PORTING_SUBJECT: Subject = { + title: "AP Porting", + units: [] +}; + + diff --git a/src/lib/search/guideSearch.ts b/src/lib/search/guideSearch.ts index ebbe8d89..1e7d20ae 100644 --- a/src/lib/search/guideSearch.ts +++ b/src/lib/search/guideSearch.ts @@ -345,8 +345,10 @@ export const loadGuideSearchItems = async ( const subjectsSnapshot = await getDocs(collection(db, "subjects")); const itemsBySubject = await Promise.all( - subjectsSnapshot.docs.map(async (subjectDoc) => { - const subjectData = subjectDoc.data() as Subject; + subjectsSnapshot.docs + .filter((subjectDoc) => subjectDoc.id !== "porting") + .map(async (subjectDoc) => { + const subjectData = subjectDoc.data() as Subject; try { return await mapSubjectToSearchItems(subjectDoc.id, subjectData); } catch (error) {