From 6e55492c1684f60a5dcacc00d6ff2b271e6d430a Mon Sep 17 00:00:00 2001 From: Heryan Djaruma Date: Mon, 23 Jun 2025 21:20:29 +0700 Subject: [PATCH 01/25] better show modal --- app/applications/page.tsx | 59 +---------------- components/ApplicationAcceptModal.tsx | 93 +++++++++++++++++++++++++++ 2 files changed, 95 insertions(+), 57 deletions(-) create mode 100644 components/ApplicationAcceptModal.tsx diff --git a/app/applications/page.tsx b/app/applications/page.tsx index 7a671e8..c8e5c62 100644 --- a/app/applications/page.tsx +++ b/app/applications/page.tsx @@ -14,6 +14,7 @@ import { getQuestionText, } from "@/lib/firebaseUtils"; import { CombinedApplicationData, APPLICATION_STATUS } from "@/lib/types"; +import ApplicationAcceptModal from "@/components/ApplicationAcceptModal"; export default function Applications() { const [applications, setApplications] = useState( @@ -865,63 +866,7 @@ export default function Applications() { {showAcceptModal && ( -
-
-
-

- Accept Participants -

- -
- -
-

- Select criteria for automatically accepting participants: -

- -
-

Coming Soon

-

- This feature will allow you to bulk accept participants based - on scores, status, and other criteria. The implementation is - in progress. -

-
-
- -
- - -
-
-
+ )} ); diff --git a/components/ApplicationAcceptModal.tsx b/components/ApplicationAcceptModal.tsx new file mode 100644 index 0000000..e30264a --- /dev/null +++ b/components/ApplicationAcceptModal.tsx @@ -0,0 +1,93 @@ +import { useState } from "react" + +interface ApplicationAcceptModalProps { + setShowAcceptModal: (value: boolean) => void +} + +export default function ApplicationAcceptModal({ setShowAcceptModal }: ApplicationAcceptModalProps) { + const [minScore, setMinScore] = useState(0) + const [minScoreError, setMinScoreError] = useState("") + + const onChangeMinScore = (e: React.ChangeEvent) => { + const value = Number(e.target.value) + if (value < 0 || value > 10) { + setMinScoreError("Score must be between 0 and 10") + } else { + setMinScore(value) + setMinScoreError("") + } + } + + return ( +
+
+
+

+ Accept Participants +

+ +
+ +
+
+

+ Score threshold to accept applicants: +

+
+ + {minScoreError} +
+
+ +
+

Coming Soon

+

+ This feature will allow you to bulk accept participants based + on scores, status, and other criteria. The implementation is + in progress. +

+
+
+ +
+ + +
+
+
+ ) +} \ No newline at end of file From 0bede51d19ead6e66ed280bd17a70eeb825c5162 Mon Sep 17 00:00:00 2001 From: Heryan Djaruma Date: Tue, 24 Jun 2025 00:41:12 +0700 Subject: [PATCH 02/25] add filtering in application accept modal --- components/ApplicationAcceptModal.tsx | 31 +++++++++++++++++---------- lib/firebaseUtils.ts | 31 +++++++++++++++++++++------ 2 files changed, 45 insertions(+), 17 deletions(-) diff --git a/components/ApplicationAcceptModal.tsx b/components/ApplicationAcceptModal.tsx index e30264a..6786271 100644 --- a/components/ApplicationAcceptModal.tsx +++ b/components/ApplicationAcceptModal.tsx @@ -1,12 +1,14 @@ -import { useState } from "react" +import { useEffect, useState } from "react" +import { CombinedApplicationData, fetchApplicationsWithUsers } from "@/lib/firebaseUtils" interface ApplicationAcceptModalProps { setShowAcceptModal: (value: boolean) => void } export default function ApplicationAcceptModal({ setShowAcceptModal }: ApplicationAcceptModalProps) { - const [minScore, setMinScore] = useState(0) + const [minScore, setMinScore] = useState(undefined) const [minScoreError, setMinScoreError] = useState("") + const [combinedApplications, setCombinedApplications] = useState([]) const onChangeMinScore = (e: React.ChangeEvent) => { const value = Number(e.target.value) @@ -18,13 +20,23 @@ export default function ApplicationAcceptModal({ setShowAcceptModal }: Applicati } } + useEffect(() => { + const scoreFilter = minScore === 0 ? undefined : minScore; + fetchApplicationsWithUsers("submitted", scoreFilter).then((applications) => { + setCombinedApplications(applications.filter(app => app.score !== undefined)) + }) + }, [minScore]) + return (
-

- Accept Participants -

+
+

+ Accept Participants +

+

Bulk accept participants based on scores, status, and other criteria.

+
-

Coming Soon

-

- This feature will allow you to bulk accept participants based - on scores, status, and other criteria. The implementation is - in progress. -

+ {combinedApplications.map((application) => ( +
{application.id}
+ ))}
diff --git a/lib/firebaseUtils.ts b/lib/firebaseUtils.ts index aa09930..1b9d855 100644 --- a/lib/firebaseUtils.ts +++ b/lib/firebaseUtils.ts @@ -7,6 +7,7 @@ import { query, orderBy, Timestamp, + where, } from "firebase/firestore"; import { db, auth } from "./firebase"; import { @@ -50,10 +51,11 @@ export async function fetchAllApplications(): Promise { /** * Fetches all users from Firestore */ -export async function fetchAllUsers(): Promise { +export async function fetchAllUsers(status?: string): Promise { try { const usersRef = collection(db, 'users'); - const querySnapshot = await getDocs(usersRef); + const firebaseQuery = status ? query(usersRef, where('status', '==', status)) : usersRef; + const querySnapshot = await getDocs(firebaseQuery); const users: FirestoreUser[] = []; querySnapshot.forEach((doc) => { @@ -93,13 +95,30 @@ export async function fetchUserById(userId: string): Promise { +export async function fetchApplicationsWithUsers(status?: string, minScore?: number): Promise { try { - const [applications, users] = await Promise.all([ + let [applications, users] = await Promise.all([ fetchAllApplications(), - fetchAllUsers() + fetchAllUsers(status) ]); - + + if (minScore !== undefined) { + applications = applications.filter(application => application.score !== undefined && application.score >= minScore); + } + else if (minScore === undefined || minScore === 0) { + applications = applications.filter(application => application.score === undefined); + } + + applications.sort((a, b) => { + if (a.score === undefined) { + return 1; + } + if (b.score === undefined) { + return -1; + } + return b.score - a.score; + }); + const usersMap = new Map(); users.forEach(user => { usersMap.set(user.id, user); From 759e7f282f12ef35e14e3a365204c0a134eb244e Mon Sep 17 00:00:00 2001 From: Heryan Djaruma Date: Tue, 24 Jun 2025 01:03:44 +0700 Subject: [PATCH 03/25] add loading spinner and better row ui --- components/ApplicationAcceptModal.tsx | 32 ++++++++++--- components/lists/AcceptingApplicationRow.tsx | 49 ++++++++++++++++++++ 2 files changed, 74 insertions(+), 7 deletions(-) create mode 100644 components/lists/AcceptingApplicationRow.tsx diff --git a/components/ApplicationAcceptModal.tsx b/components/ApplicationAcceptModal.tsx index 6786271..8c00b34 100644 --- a/components/ApplicationAcceptModal.tsx +++ b/components/ApplicationAcceptModal.tsx @@ -1,11 +1,14 @@ import { useEffect, useState } from "react" import { CombinedApplicationData, fetchApplicationsWithUsers } from "@/lib/firebaseUtils" +import AcceptingApplicationRowComponent from "./lists/AcceptingApplicationRow" +import LoadingSpinner from "./LoadingSpinner" interface ApplicationAcceptModalProps { setShowAcceptModal: (value: boolean) => void } export default function ApplicationAcceptModal({ setShowAcceptModal }: ApplicationAcceptModalProps) { + const [isLoading, setIsLoading] = useState(true) const [minScore, setMinScore] = useState(undefined) const [minScoreError, setMinScoreError] = useState("") const [combinedApplications, setCombinedApplications] = useState([]) @@ -21,15 +24,17 @@ export default function ApplicationAcceptModal({ setShowAcceptModal }: Applicati } useEffect(() => { + setIsLoading(true) const scoreFilter = minScore === 0 ? undefined : minScore; fetchApplicationsWithUsers("submitted", scoreFilter).then((applications) => { setCombinedApplications(applications.filter(app => app.score !== undefined)) }) + setIsLoading(false) }, [minScore]) return (
-
+

@@ -57,7 +62,7 @@ export default function ApplicationAcceptModal({ setShowAcceptModal }: Applicati

-
+

Score threshold to accept applicants: @@ -75,11 +80,24 @@ export default function ApplicationAcceptModal({ setShowAcceptModal }: Applicati

-
- {combinedApplications.map((application) => ( -
{application.id}
- ))} -
+ {isLoading ? ( +
+ +
+ ) : ( + <> +

Showing {combinedApplications.length} applications passing the score threshold

+ +
+ {combinedApplications.map((application) => ( +
+ +
+ ))} +
+ + )} +
diff --git a/components/lists/AcceptingApplicationRow.tsx b/components/lists/AcceptingApplicationRow.tsx new file mode 100644 index 0000000..694d10b --- /dev/null +++ b/components/lists/AcceptingApplicationRow.tsx @@ -0,0 +1,49 @@ +import { APPLICATION_STATUS, CombinedApplicationData } from "@/lib/types"; + +interface AcceptingApplicationRowComponentProps { + application: CombinedApplicationData +} + +export default function AcceptingApplicationRowComponent( + { application }: AcceptingApplicationRowComponentProps +) { + const getDisplayStatus = (application: CombinedApplicationData): string => { + if ( + application.status === APPLICATION_STATUS.SUBMITTED && + application.score + ) { + return APPLICATION_STATUS.GRADED; + } + return application.status; + }; + return ( +
+
+

+ {application.firstName} +

+
+ {application.score ? ( +
+ {application.score}/10 +
+ ) : ( +
+ Not scored +
+ )} +
+
+
+ + {getDisplayStatus(application)} + +
+
+ ) +} \ No newline at end of file From 13039ddfa04500a0e663b04bc67ee8a1d065a4ba Mon Sep 17 00:00:00 2001 From: Heryan Djaruma Date: Tue, 24 Jun 2025 01:17:33 +0700 Subject: [PATCH 04/25] add lucide dependency --- package.json | 1 + 1 file changed, 1 insertion(+) diff --git a/package.json b/package.json index 91d0cb3..7f7f1cc 100644 --- a/package.json +++ b/package.json @@ -11,6 +11,7 @@ "dependencies": { "@vercel/analytics": "^1.5.0", "firebase": "^11.8.1", + "lucide-react": "^0.522.0", "next": "15.1.6", "nodemailer": "^7.0.3", "react": "^19.0.0", From 42625921e8e2ec7969b9eda5cb454b4859538f41 Mon Sep 17 00:00:00 2001 From: Heryan Djaruma Date: Tue, 24 Jun 2025 01:17:39 +0700 Subject: [PATCH 05/25] better app row ui --- components/lists/AcceptingApplicationRow.tsx | 29 +++++++------------- 1 file changed, 10 insertions(+), 19 deletions(-) diff --git a/components/lists/AcceptingApplicationRow.tsx b/components/lists/AcceptingApplicationRow.tsx index 694d10b..b8758af 100644 --- a/components/lists/AcceptingApplicationRow.tsx +++ b/components/lists/AcceptingApplicationRow.tsx @@ -1,4 +1,5 @@ -import { APPLICATION_STATUS, CombinedApplicationData } from "@/lib/types"; +import { CombinedApplicationData } from "@/lib/types"; +import { SquareArrowOutUpRight } from "lucide-react"; interface AcceptingApplicationRowComponentProps { application: CombinedApplicationData @@ -7,22 +8,14 @@ interface AcceptingApplicationRowComponentProps { export default function AcceptingApplicationRowComponent( { application }: AcceptingApplicationRowComponentProps ) { - const getDisplayStatus = (application: CombinedApplicationData): string => { - if ( - application.status === APPLICATION_STATUS.SUBMITTED && - application.score - ) { - return APPLICATION_STATUS.GRADED; - } - return application.status; - }; + return (
-
-

+
+

{application.firstName}

@@ -36,13 +29,11 @@ export default function AcceptingApplicationRowComponent(
)}
-

-
- - {getDisplayStatus(application)} - +
) From 31d1b3856ee8bc7380563758d1f6e7cdd3e1b96a Mon Sep 17 00:00:00 2001 From: Heryan Djaruma Date: Tue, 24 Jun 2025 01:32:41 +0700 Subject: [PATCH 06/25] add preview in accepting application --- app/applications/page.tsx | 19 +- components/ApplicationAcceptModal.tsx | 509 +++++++++++++++---- components/lists/AcceptingApplicationRow.tsx | 12 +- lib/evaluator.ts | 17 + 4 files changed, 423 insertions(+), 134 deletions(-) create mode 100644 lib/evaluator.ts diff --git a/app/applications/page.tsx b/app/applications/page.tsx index c8e5c62..5df04f2 100644 --- a/app/applications/page.tsx +++ b/app/applications/page.tsx @@ -15,6 +15,7 @@ import { } from "@/lib/firebaseUtils"; import { CombinedApplicationData, APPLICATION_STATUS } from "@/lib/types"; import ApplicationAcceptModal from "@/components/ApplicationAcceptModal"; +import { calculateAge } from "@/lib/evaluator"; export default function Applications() { const [applications, setApplications] = useState( @@ -341,24 +342,6 @@ export default function Applications() { } }; - const calculateAge = (dateOfBirth: string): number => { - try { - const birth = new Date(dateOfBirth); - const today = new Date(); - let age = today.getFullYear() - birth.getFullYear(); - const monthDiff = today.getMonth() - birth.getMonth(); - if ( - monthDiff < 0 || - (monthDiff === 0 && today.getDate() < birth.getDate()) - ) { - age--; - } - return age; - } catch { - return 0; - } - }; - if (loading) { return (
diff --git a/components/ApplicationAcceptModal.tsx b/components/ApplicationAcceptModal.tsx index 8c00b34..77ef4b7 100644 --- a/components/ApplicationAcceptModal.tsx +++ b/components/ApplicationAcceptModal.tsx @@ -1,120 +1,407 @@ import { useEffect, useState } from "react" -import { CombinedApplicationData, fetchApplicationsWithUsers } from "@/lib/firebaseUtils" +import { APPLICATION_STATUS, CombinedApplicationData, fetchApplicationsWithUsers, formatApplicationDate, getEducationLevel, getQuestionText, getYearSuffix } from "@/lib/firebaseUtils" import AcceptingApplicationRowComponent from "./lists/AcceptingApplicationRow" import LoadingSpinner from "./LoadingSpinner" +import { X } from "lucide-react" +import { calculateAge } from "@/lib/evaluator" interface ApplicationAcceptModalProps { - setShowAcceptModal: (value: boolean) => void + setShowAcceptModal: (value: boolean) => void } export default function ApplicationAcceptModal({ setShowAcceptModal }: ApplicationAcceptModalProps) { - const [isLoading, setIsLoading] = useState(true) - const [minScore, setMinScore] = useState(undefined) - const [minScoreError, setMinScoreError] = useState("") - const [combinedApplications, setCombinedApplications] = useState([]) - - const onChangeMinScore = (e: React.ChangeEvent) => { - const value = Number(e.target.value) - if (value < 0 || value > 10) { - setMinScoreError("Score must be between 0 and 10") - } else { - setMinScore(value) - setMinScoreError("") - } - } - - useEffect(() => { - setIsLoading(true) - const scoreFilter = minScore === 0 ? undefined : minScore; - fetchApplicationsWithUsers("submitted", scoreFilter).then((applications) => { - setCombinedApplications(applications.filter(app => app.score !== undefined)) - }) - setIsLoading(false) - }, [minScore]) - - return ( -
-
-
-
-

- Accept Participants -

-

Bulk accept participants based on scores, status, and other criteria.

-
- -
- -
-
-

- Score threshold to accept applicants: -

-
- - {minScoreError} -
-
- - {isLoading ? ( -
- -
- ) : ( - <> -

Showing {combinedApplications.length} applications passing the score threshold

- -
- {combinedApplications.map((application) => ( -
- -
- ))} -
- - )} - -
- -
- - -
-
-
- ) + const [isLoading, setIsLoading] = useState(true) + const [minScore, setMinScore] = useState(undefined) + const [minScoreError, setMinScoreError] = useState("") + const [combinedApplications, setCombinedApplications] = useState([]) + const [previewModalActive, setPreviewModalActive] = useState(false) + const [currentApplicationPreview, setCurrentApplicationPreview] = useState(undefined) + + const [questionTexts, setQuestionTexts] = useState<{ + motivation: string; + bigProblem: string; + interestingProject: string; + }>({ + motivation: "Motivation", + bigProblem: "Problem to Solve", + interestingProject: "Interesting Project", + }); + + const loadQuestionTexts = async () => { + try { + const [motivationText, bigProblemText, interestingProjectText] = + await Promise.all([ + getQuestionText("motivation"), + getQuestionText("bigProblem"), + getQuestionText("interestingProject"), + ]); + + setQuestionTexts({ + motivation: motivationText, + bigProblem: bigProblemText, + interestingProject: interestingProjectText, + }); + } catch (error) { + console.error("Error loading question texts:", error); + } + }; + + const onChangeMinScore = (e: React.ChangeEvent) => { + const value = Number(e.target.value) + if (value < 0 || value > 10) { + setMinScoreError("Score must be between 0 and 10") + } else { + setMinScore(value) + setMinScoreError("") + } + } + + const onPreviewApplication = (application: CombinedApplicationData) => { + setCurrentApplicationPreview(application) + setPreviewModalActive(true) + } + + + useEffect(() => { + loadQuestionTexts() + }, []) + + useEffect(() => { + setIsLoading(true) + const scoreFilter = minScore === 0 ? undefined : minScore; + fetchApplicationsWithUsers("submitted", scoreFilter).then((applications) => { + setCombinedApplications(applications.filter(app => app.score !== undefined)) + }) + setIsLoading(false) + }, [minScore]) + + return ( +
+
+
+
+

+ Accept Participants +

+

Bulk accept participants based on scores, status, and other criteria.

+
+ +
+ +
+
+

+ Score threshold to accept applicants: +

+
+ + {minScoreError} +
+
+ + {isLoading ? ( +
+ +
+ ) : ( + <> +

Showing {combinedApplications.length} applications passing the score threshold

+ +
+ {combinedApplications.map((application) => ( +
+ +
+ ))} +
+ + )} + +
+ +
+ + +
+
+ + {previewModalActive && ( +
+
+
setPreviewModalActive(false)}> + +
+ +
+

+ Application Preview +

+ + {currentApplicationPreview && ( + +
+
+
+
+

+ {currentApplicationPreview.firstName} +

+
+

+ Email:{" "} + {currentApplicationPreview.email} +

+

+ School:{" "} + {currentApplicationPreview.school} +

+

+ Education:{" "} + {getEducationLevel(currentApplicationPreview.education)} +

+

+ Year:{" "} + {getYearSuffix(currentApplicationPreview.year)} +

+

+ Age:{" "} + {calculateAge(currentApplicationPreview.date_of_birth)} +

+

+ Hackathons:{" "} + {currentApplicationPreview.hackathonCount} previous +

+

+ Desired Roles:{" "} + {currentApplicationPreview.desiredRoles} +

+
+
+
+
+ Links & Documents +
+
+ {currentApplicationPreview.resume && ( + + + + + Resume (PDF) + + )} + {currentApplicationPreview.portfolio && + currentApplicationPreview.portfolio !== "X" && ( + + + + + Portfolio + + )} + {currentApplicationPreview.github && + currentApplicationPreview.github !== "X" && ( + + + + + GitHub + + )} + {currentApplicationPreview.linkedin && + currentApplicationPreview.linkedin !== "X" && ( + + + + + LinkedIn + + )} +
+
+
+ +
+
+ {questionTexts.motivation} +
+