Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
c51b579
fix(deps): update dependency sharp to v0.34.4
renovate[bot] Sep 17, 2025
6d7aebb
fix(deps): update dependency @types/node to v24.5.2
renovate[bot] Sep 18, 2025
7cdc26c
fix(deps): update dependency framer-motion to v12.23.16
renovate[bot] Sep 19, 2025
644cf6e
Merge pull request #343 from Hack-PSU/renovate/sharp-0.x-lockfile
kensac Sep 19, 2025
c6103e6
Merge pull request #344 from Hack-PSU/renovate/framer-motion-12.x-loc…
kensac Sep 19, 2025
de80009
Merge pull request #345 from Hack-PSU/renovate/node-24.x-lockfile
kensac Sep 19, 2025
0c9f505
fix(deps): update dependency @posthog/nextjs-config to v1.3.1
renovate[bot] Sep 19, 2025
049f705
fix(deps): update dependency swiper to v12.0.2
renovate[bot] Sep 19, 2025
f118aee
Merge pull request #346 from Hack-PSU/renovate/swiper-12.x-lockfile
kensac Sep 19, 2025
7f9127e
Merge pull request #347 from Hack-PSU/renovate/posthog-nextjs-config-…
kensac Sep 19, 2025
337eb23
fix(deps): update dependency posthog-js to v1.266.2
renovate[bot] Sep 19, 2025
f52802b
fix(deps): update dependency posthog-node to v5.8.5
renovate[bot] Sep 19, 2025
ea68751
fix(deps): update dependency eslint to v9.36.0
renovate[bot] Sep 19, 2025
f6d8884
fix(deps): update dependency firebase to v12.3.0
renovate[bot] Sep 19, 2025
60160bc
Merge pull request #348 from Hack-PSU/renovate/posthog-js-1.x-lockfile
kensac Sep 19, 2025
882de86
Merge pull request #349 from Hack-PSU/renovate/posthog-node-5.x-lockfile
kensac Sep 19, 2025
e6dd015
Merge pull request #350 from Hack-PSU/renovate/eslint-monorepo
kensac Sep 19, 2025
b9954f8
Merge pull request #351 from Hack-PSU/renovate/firebase-12.x-lockfile
kensac Sep 19, 2025
22c675e
fix(deps): update dependency react-hook-form to v7.63.0
renovate[bot] Sep 19, 2025
2e25bf2
fix(deps): update dependency posthog-js to v1.266.3
renovate[bot] Sep 20, 2025
19c7a91
fix(deps): update dependency posthog-node to v5.8.6
renovate[bot] Sep 20, 2025
bac10e8
Merge pull request #352 from Hack-PSU/renovate/react-hook-form-7.x-lo…
kensac Sep 20, 2025
936232c
Merge pull request #353 from Hack-PSU/renovate/posthog-js-1.x-lockfile
kensac Sep 20, 2025
8ec95ea
Merge pull request #354 from Hack-PSU/renovate/posthog-node-5.x-lockfile
kensac Sep 20, 2025
613e45e
new page
kensac Sep 29, 2025
fca17a3
we test in prod
kensac Sep 29, 2025
2a94cd8
Merge pull request #363 from Hack-PSU/kanishk/extra-credit-fr
kensac Sep 29, 2025
1cb1976
change ui
kensac Sep 29, 2025
0e036ed
Merge pull request #364 from Hack-PSU/kanishk/extra-credit-fr
kensac Sep 29, 2025
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
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"cmdk": "^1.1.1",
"eslint": "9.35.0",
"eslint": "9.36.0",
"eslint-config-next": "15.5.3",
"firebase": "^12.0.0",
"form-data": "^4.0.0",
Expand Down
230 changes: 230 additions & 0 deletions src/app/(protected)/extra-credit/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,230 @@
"use client";

import { useState } from "react";
import { useRouter } from "next/navigation";
import { useFirebase } from "@/lib/providers/FirebaseProvider";
import { useUserInfoMe } from "@/lib/api/user/hook";
import { useAllExtraCreditClasses } from "@/lib/api/extra-credit/hook";
import {
useUserExtraCreditClasses,
useAssignExtraCreditClass,
useUnassignExtraCreditClass,
} from "@/lib/api/user/hook";
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from "@/components/ui/card";
import { Button } from "@/components/ui/button";
import { Badge } from "@/components/ui/badge";
import { toast } from "sonner";
import { GraduationCap, Loader2, ArrowLeft, Check, Plus, X } from "lucide-react";

export default function ExtraCredit() {
const { user, isAuthenticated, isLoading: authLoading } = useFirebase();
const router = useRouter();
const { data: userInfo, isLoading: userInfoLoading } = useUserInfoMe();
const { data: allClasses, isLoading: allClassesLoading } =
useAllExtraCreditClasses();
const { data: assignedClasses, isLoading: assignedClassesLoading } =
useUserExtraCreditClasses(user?.uid || "");

const { mutateAsync: assignClass, isPending: isAssigning } =
useAssignExtraCreditClass();
const { mutateAsync: unassignClass, isPending: isUnassigning } =
useUnassignExtraCreditClass();

const [processingClassId, setProcessingClassId] = useState<number | null>(
null
);

const isClassAssigned = (classId: number) => {
return assignedClasses?.some((c) => c.id === classId) || false;
};

const handleToggleClass = async (
classId: number,
isCurrentlyAssigned: boolean
) => {
if (!user?.uid) {
toast.error("Please sign in to manage extra credit classes");
return;
}

setProcessingClassId(classId);
try {
if (isCurrentlyAssigned) {
await unassignClass({ userId: user.uid, classId });
toast.success("Extra credit class removed");
} else {
await assignClass({ userId: user.uid, classId });
toast.success("Extra credit class assigned");
}
} catch (error: any) {
console.error("Error toggling class:", error);
const errorMessage =
error?.message || "Failed to update extra credit class";
toast.error(errorMessage);
} finally {
setProcessingClassId(null);
}
};

if (authLoading || userInfoLoading) {
return (
<div className="flex min-h-screen items-center justify-center">
<div className="flex items-center space-x-2">
<Loader2 className="h-6 w-6 animate-spin" />
<span className="text-lg">Loading...</span>
</div>
</div>
);
}

if (!isAuthenticated || !user) {
router.push("/");
return null;
}

return (
<div className="min-h-screen bg-transparent py-8 px-4">
<div className="mx-auto max-w-4xl space-y-6">
<Button
variant="ghost"
onClick={() => router.push("/profile")}
className="mb-4"
>
<ArrowLeft className="mr-2 h-4 w-4" />
Back to Profile
</Button>

<Card className="border-2 border-red-500 bg-gradient-to-r from-slate-900 to-slate-800 text-white">
<CardHeader className="text-center">
<div className="flex justify-center mb-4">
<GraduationCap className="h-16 w-16 text-red-400" />
</div>
<CardTitle className="text-2xl md:text-3xl font-bold">
Extra Credit Classes
</CardTitle>
<CardDescription className="text-slate-300">
Select the classes you want to receive extra credit for attending
HackPSU
</CardDescription>
</CardHeader>
</Card>

<Card>
<CardHeader>
<div className="flex items-center justify-between">
<div>
<CardTitle className="flex items-center space-x-2">
<GraduationCap className="h-6 w-6" />
<span>Extra Credit Classes</span>
</CardTitle>
<CardDescription>
Click to add or remove classes for extra credit
</CardDescription>
</div>
{assignedClasses && assignedClasses.length > 0 && (
<Badge variant="secondary" className="text-sm">
{assignedClasses.length} selected
</Badge>
)}
</div>
</CardHeader>
<CardContent>
{allClassesLoading || assignedClassesLoading ? (
<div className="flex items-center justify-center py-8">
<Loader2 className="h-6 w-6 animate-spin" />
<span className="ml-2">Loading classes...</span>
</div>
) : allClasses && allClasses.length > 0 ? (
<div className="space-y-3">
{allClasses.map((classItem) => {
const assigned = isClassAssigned(classItem.id);
const isProcessing = processingClassId === classItem.id;

return (
<div
key={classItem.id}
className={`flex items-center justify-between p-4 border-2 rounded-lg transition-all cursor-pointer ${
assigned
? "border-green-500 bg-green-50 hover:bg-green-100"
: "border-gray-200 hover:border-gray-300 hover:bg-gray-50"
} ${
isProcessing || isAssigning || isUnassigning
? "opacity-50 cursor-not-allowed"
: ""
}`}
onClick={() =>
!isProcessing &&
!isAssigning &&
!isUnassigning &&
handleToggleClass(classItem.id, assigned)
}
>
<div className="flex items-center space-x-3">
<div
className={`flex items-center justify-center w-6 h-6 rounded-full border-2 ${
assigned
? "border-green-600 bg-green-600"
: "border-gray-300"
}`}
>
{assigned && <Check className="h-4 w-4 text-white" />}
</div>
<div>
<div
className={`font-medium ${
assigned ? "text-green-900" : "text-gray-900"
}`}
>
{classItem.name}
</div>
</div>
</div>
<div className="flex items-center space-x-2">
{isProcessing ? (
<Loader2 className="h-5 w-5 animate-spin text-gray-400" />
) : (
<Button
variant={assigned ? "destructive" : "default"}
size="sm"
onClick={(e) => {
e.stopPropagation();
handleToggleClass(classItem.id, assigned);
}}
disabled={isAssigning || isUnassigning}
>
{assigned ? (
<>
<X className="h-4 w-4 mr-1" />
Remove
</>
) : (
<>
<Plus className="h-4 w-4 mr-1" />
Add
</>
)}
</Button>
)}
</div>
</div>
);
})}
</div>
) : (
<div className="text-center py-8 text-gray-500">
<GraduationCap className="h-12 w-12 mx-auto mb-2 opacity-50" />
<p>No extra credit classes available at this time</p>
</div>
)}
</CardContent>
</Card>
</div>
</div>
);
}
15 changes: 15 additions & 0 deletions src/app/(protected)/profile/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ import {
FolderOpen,
Users,
Lock,
GraduationCap,
} from "lucide-react";

export default function Profile() {
Expand Down Expand Up @@ -141,6 +142,10 @@ export default function Profile() {
router.push("/project");
};

const handleExtraCredit = () => {
router.push("/extra-credit");
};

// Find user's team
const userTeam = teams?.find((team) =>
[
Expand Down Expand Up @@ -431,6 +436,16 @@ export default function Profile() {
Submit Reimbursement Form
</Button>

<Button
onClick={handleExtraCredit}
className="w-full"
variant="default"
size="lg"
>
<GraduationCap className="mr-2 h-4 w-4" />
Manage Extra Credit
</Button>

<Separator />

<Button
Expand Down
6 changes: 6 additions & 0 deletions src/lib/api/apiClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,12 @@ export async function apiFetch<T>(
if (responseType === "blob") {
return (await response.blob()) as unknown as T;
}

// Handle 204 No Content responses
if (response.status === 204) {
return undefined as unknown as T;
}

return (await response.json()) as T;
}

Expand Down
11 changes: 11 additions & 0 deletions src/lib/api/user/entity.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,3 +24,14 @@ export interface UserCreateEntity extends Omit<UserEntity, "id" | "resume"> {
export interface UserInfoMe extends UserEntity {
registration: RegistrationEntity;
}

export interface ExtraCreditClass {
id: number;
name: string;
hackathonId: string | null;
}

export interface ExtraCreditAssignment {
userId: string;
classId: number;
}
45 changes: 44 additions & 1 deletion src/lib/api/user/hook.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,14 +9,23 @@ import {
deleteUser,
getUserResume,
getUserInfoMe,
getUserExtraCreditClasses,
assignExtraCreditClass,
unassignExtraCreditClass,
} from "./provider";
import { UserCreateEntity, UserEntity, UserInfoMe } from "./entity";
import {
UserCreateEntity,
UserEntity,
UserInfoMe,
ExtraCreditClass,
} from "./entity";

export const userQueryKeys = {
all: ["users"] as const,
detail: (id: string) => ["user", id] as const,
resume: (id: string) => ["user", id, "resume"] as const,
me: ["user", "info", "me"] as const,
extraCredit: (id: string) => ["user", id, "extra-credit"] as const,
};

export function useAllUsers(active?: boolean) {
Expand Down Expand Up @@ -105,3 +114,37 @@ export function useUserInfoMe() {
retryDelay: 1000,
});
}

export function useUserExtraCreditClasses(userId: string) {
return useQuery<ExtraCreditClass[]>({
queryKey: userQueryKeys.extraCredit(userId),
queryFn: () => getUserExtraCreditClasses(userId),
enabled: Boolean(userId),
});
}

export function useAssignExtraCreditClass() {
const qc = useQueryClient();
return useMutation({
mutationFn: (opts: { userId: string; classId: number }) =>
assignExtraCreditClass(opts.userId, opts.classId),
onSuccess: (_, variables) => {
qc.invalidateQueries({
queryKey: userQueryKeys.extraCredit(variables.userId),
});
},
});
}

export function useUnassignExtraCreditClass() {
const qc = useQueryClient();
return useMutation({
mutationFn: (opts: { userId: string; classId: number }) =>
unassignExtraCreditClass(opts.userId, opts.classId),
onSuccess: (_, variables) => {
qc.invalidateQueries({
queryKey: userQueryKeys.extraCredit(variables.userId),
});
},
});
}
Loading