Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
d7585bd
all mentors view
heryandjaruma Jul 16, 2025
defd87f
minor layouting
heryandjaruma Jul 16, 2025
574a478
add specialization filter view
heryandjaruma Jul 16, 2025
5def4d5
add mentoring availability
heryandjaruma Jul 16, 2025
e1af786
add mentor item component
heryandjaruma Jul 16, 2025
28e5730
better item component
heryandjaruma Jul 16, 2025
d04699b
add back button
heryandjaruma Jul 16, 2025
74a3e63
init mentor detail
heryandjaruma Jul 16, 2025
a5963ca
init mentorings time slots
heryandjaruma Jul 16, 2025
21b1978
available mentorings
heryandjaruma Jul 16, 2025
f671fd7
add mentoring slots for mentor
heryandjaruma Jul 16, 2025
8ffb4bd
use mentoring container
heryandjaruma Jul 16, 2025
7a030b5
improve ui
heryandjaruma Jul 16, 2025
6ca6b89
define mentorship slot
heryandjaruma Jul 16, 2025
6261d4f
better draw mentorship slot
heryandjaruma Jul 16, 2025
d028dbc
better mentorship schedule view
heryandjaruma Jul 16, 2025
a9fcb90
better layouting
heryandjaruma Jul 16, 2025
6e2ad26
change mentorship impl; edit type
heryandjaruma Jul 17, 2025
4c86686
add better layouting
heryandjaruma Jul 17, 2025
cdedab9
change mentor layouting
heryandjaruma Jul 17, 2025
fd462a7
add mentorship page
heryandjaruma Jul 17, 2025
d725daf
better add mentorship slot ui
heryandjaruma Jul 17, 2025
1e07e8f
proper add mentorship slots
heryandjaruma Jul 17, 2025
f79b5d8
add batch add slot
heryandjaruma Jul 17, 2025
643e277
remove unused code
heryandjaruma Jul 17, 2025
b8ab959
add delete mentorship appointment
heryandjaruma Jul 17, 2025
a6702ff
better mentor layouting
heryandjaruma Jul 17, 2025
a7b013c
add image support
heryandjaruma Jul 17, 2025
8db90d6
minor fix mentorship
heryandjaruma Jul 17, 2025
2515fde
better mentorship layout
heryandjaruma Jul 17, 2025
344fdda
Merge pull request #14 from GarudaHacks/feat/mentoring-system
heryandjaruma Jul 17, 2025
5875b62
add local dev config
heryandjaruma Jul 19, 2025
4b1fb88
minor fix for optional fields
heryandjaruma Jul 19, 2025
caeaab6
define emulator
heryandjaruma Jul 19, 2025
f8cecb5
Merge pull request #15 from GarudaHacks/feat/mentoring-system
heryandjaruma Jul 19, 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
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -39,3 +39,7 @@ yarn-error.log*
# typescript
*.tsbuildinfo
next-env.d.ts

# for dev only - Ryan
/app/api/create-mentors/**
*adminsdk*.*
89 changes: 89 additions & 0 deletions app/mentorship/[mentorId]/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
"use client"

import MentorshipAppointmentCardComponent from "@/components/MentorshipAppointmentCardComponent"
import { fetchMentorshipAppointmentsByMentorId, fetchMentorById, getMentorProfilePicture } from "@/lib/firebaseUtils"
import { FirestoreMentor, MentorshipAppointment } from "@/lib/types"
import { Plus } from "lucide-react"
import Image from "next/image"
import { useParams, useRouter } from "next/navigation"
import { useEffect, useState } from "react"
import ghq from "@/public/assets/ghq.png"

export default function MentorDetailPage() {
const params = useParams<{ mentorId: string }>()
const router = useRouter()
const [mentor, setMentor] = useState<FirestoreMentor>()
const [mentorshipAppointments, setMentorshipAppointments] = useState<MentorshipAppointment[]>()
const [mentorUrl, setMentorUrl] = useState<string>('')

useEffect(() => {
fetchMentorById(params.mentorId).then((m) => {
if (m) {
setMentor(m)

getMentorProfilePicture(m.name).then((pp) => {
if (pp) {
setMentorUrl(pp)
}
})
}
})

fetchMentorshipAppointmentsByMentorId(params.mentorId).then((m) => {
if (m) {
setMentorshipAppointments(m)
}
})


}, [params.mentorId])

const handleOnClickAddAppointment = (mentorId: string) => {
router.push(`/mentorship/add?mentorId=${mentorId}`)
}

return (
<div className="flex flex-col gap-4">
<div className="flex flex-col gap-4">
<h1 className="text-xl font-bold">Mentor Details</h1>
<div className="flex flex-col gap-2 border p-4 rounded-xl">
<Image
src={mentorUrl || "https://garudahacks.com/images/logo/ghq.png"}
alt={`Profile picture of ${mentor?.name || 'mentor'}`}
width={200}
height={200}
onError={() => {
setMentorUrl(ghq.src)
}}
className="rounded-full w-64 aspect-square"
/>
{mentor?.name && <h2 className="text-2xl font-bold">{mentor?.name}</h2>}
{mentor?.email && <h3 className="text-muted-foreground">{mentor?.email}</h3>}
{mentor?.discordUsername && <p className="">Discord: <span className="text-muted-foreground font-mono w-fit p-1 rounded-full text-sm">{mentor?.discordUsername}</span></p>}
{mentor?.specialization && <p className="">Specialization: {mentor?.specialization.toUpperCase()}</p>}
<div>
<p className="font-semibold text-muted-foreground">Intro</p>
<p className="text-sm">{mentor?.intro}</p>
</div>
</div>
</div>

<div className="flex flex-col gap-4">
<div className="flex justify-between items-center">
<h2 className="font-bold">Mentoring Schedule ({mentorshipAppointments?.length})</h2>
<button className="flex items-center gap-1 text-sm border rounded-full px-3 py-1 hover:bg-primary/90"
onClick={() => handleOnClickAddAppointment(params.mentorId)}
>Add Schedule
<Plus />
</button>
</div>

<div className="flex flex-col gap-4">
{mentorshipAppointments?.map((mentorshipAppointment) => (
<MentorshipAppointmentCardComponent key={mentorshipAppointment.id} mentorshipAppointment={mentorshipAppointment} />
))}
</div>
</div>
</div>
)
}
135 changes: 135 additions & 0 deletions app/mentorship/add/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
"use client"

import { ONE_SLOT_INTERVAL_MINUTES } from "@/config"
import { addMentorshipAppointment, fetchMentorById } from "@/lib/firebaseUtils"
import { dateToStringTime, epochToStringDate } from "@/lib/helpers"
import { FirestoreMentor, MentorshipAppointment } from "@/lib/types"
import { Loader2 } from "lucide-react"
import { useRouter, useSearchParams } from "next/navigation"
import { useEffect, useState } from "react"
import DatePicker from "react-datepicker"
import "react-datepicker/dist/react-datepicker.css";

export default function AddMentorshipAppointmentPage() {
const router = useRouter()
const TIME_INTERVAL = 15
const params = useSearchParams()
const mentorId = params.get('mentorId')

const [mentor, setMentor] = useState<FirestoreMentor | null>()
const [mentorName, setMentorName] = useState<string | undefined>('')
const [startDate, setStartDate] = useState<Date | null>(new Date());
const [nTime, setNTime] = useState<number>(1)
const [appointmentType, setAppointmentType] = useState<string>('online');
const [error, setError] = useState('')

const [loading, setLoading] = useState(false)

const handleTypeChange = (event: React.ChangeEvent<HTMLInputElement>) => {
setAppointmentType(event.target.value);
};

const handleSubmit = async () => {
setLoading(true)

try {
if (nTime <= 0) {
setError('Must create minimum 1 slot')
return
} else {
setError('')
}

if (!startDate) {
setError('Start date cannot be empty')
return
}

if (!mentorId) {
setError('Mentor Id cannot be empty')
return
}

if (!location) {
setError('Location cannot be empty')
return
}
for (let index = 0; index < nTime; index++) {
const INTERVALS_SECONDS = ONE_SLOT_INTERVAL_MINUTES * 60 * index;
addMentorshipAppointment((startDate.getTime() / 1000) + INTERVALS_SECONDS, mentorId, appointmentType).then((res) => {
router.replace(`/mentorship/${mentorId}`)
})
}
} catch (error) {
console.log(error)
} finally {
setLoading(false)
}
}

useEffect(() => {
if (mentorId) {
fetchMentorById(mentorId).then((result) => {
setMentor(result)
setMentorName(result?.name)
})
}
}, [mentorId])


return (
<div className="flex flex-col gap-4">
<div>
<h1 className="text-xl">Add Mentorship Appointment Slot</h1>
<p className="text-muted-foreground">Add mentorship slots that can be booked immediately by hackers.</p>
</div>

<div className="flex flex-col gap-4 max-w-xl">
<div className="flex flex-col gap-2">
<span className="font-semibold text-sm">Mentor Name</span>
<input value={mentorName} onChange={e => setMentorName(e.target.value)} disabled type="text" className="p-2 rounded-xl bg-zinc-50/20" />
</div>

<div className="flex flex-col gap-2">
<span className="font-semibold text-sm">Start Time</span>
<div className="flex flex-row items-center gap-4">
<DatePicker
selected={startDate}
onChange={(date: Date | null) => date && setStartDate(date)}
showTimeSelect
timeIntervals={15}
className="bg-zinc-50/20 p-2 rounded-lg"
/>
<span>{dateToStringTime(startDate || new Date())}</span>
</div>
</div>

<div className="flex flex-col gap-2">
<span className="font-semibold text-sm">Create For N Times</span>
<input value={nTime} onChange={e => setNTime(Number(e.target.value))} type="number" className="p-2 rounded-xl bg-zinc-50/20" />
{startDate && <p className="text-muted-foreground text-sm">This will create {nTime} slot(s) with interval {TIME_INTERVAL} minutes starting from {epochToStringDate(startDate.getTime() / 1000)}</p>}

</div>

<div>
<div className="flex flex-row gap-1">
<input type="radio" name="appointmentType" value={"online"} id="online" checked={appointmentType === 'online'} onChange={handleTypeChange} />
<label htmlFor="online">Online</label>
</div>
<div className="flex flex-row gap-1">
<input type="radio" name="appointmentType" value={"offline"} id="offline" checked={appointmentType === 'offline'} onChange={handleTypeChange} />
<label htmlFor="offline">Offline</label>
</div>
</div>

<div className="flex flex-col gap-2 text-center">
{error && <span className="text-red-500 text-sm">{error}</span>}
<button onClick={handleSubmit} className="border p-2 rounded-xl text-sm flex flex-row items-center justify-center gap-1" type="button">
{loading && <Loader2 className="" />}
Add Slot
</button>
</div>
</div>
</div>
)
}
23 changes: 23 additions & 0 deletions app/mentorship/layout.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
"use client"

import { ChevronLeft } from "lucide-react";
import { useRouter } from "next/navigation";
import { ReactNode } from "react";

export default function MentorDetailLayout(
{ children }: { children: ReactNode }
) {
const router = useRouter()

return (
<div className="flex flex-col gap-4">
<div>
<button className="flex flex-row items-center justify-center gap-1 border p-2 rounded-lg text-md" onClick={() => router.back()}>
<ChevronLeft />
<span>Back</span>
</button>
</div>
{children}
</div>
)
}
12 changes: 7 additions & 5 deletions app/mentorship/page.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
"use client";

import MentorListComponent from "@/components/MentorList";
import PageHeader from "@/components/PageHeader";
import UnderConstruction from "@/components/UnderConstruction";
import { useAuth } from "@/contexts/AuthContext";

export default function Mentorship() {
return (
Expand All @@ -8,10 +11,9 @@ export default function Mentorship() {
title="Mentorship"
subtitle="Connect mentors with participants and manage mentorship programs."
/>
<UnderConstruction
feature="Mentorship System"
description="This section will allow you to manage mentor assignments, track mentorship sessions, and facilitate connections between mentors and participants during Garuda Hacks 6.0."
/>
<div>
<MentorListComponent />
</div>
</div>
);
}
50 changes: 50 additions & 0 deletions components/DrawMentorshipSlot.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import { EPOCH_START_MENTORING, PIXELS_PER_MINUTE } from "@/config";
import { epochToStringDate } from "@/lib/helpers";
import { ChevronDown, ChevronUp } from "lucide-react";
import { ReactNode, useState } from "react";

interface DrawMentorshipSlotProps {
index: number
startTime: number
endTime: number
children?: ReactNode
asBooking?: boolean
}

export default function DrawMentorshipSlot(
{ startTime, endTime, index, children, asBooking }: DrawMentorshipSlotProps
) {
const startOffset = (startTime - EPOCH_START_MENTORING) / 60; // minutes from start
const duration = (endTime - startTime) / 60; // duration in minutes
const topPosition = startOffset * PIXELS_PER_MINUTE;
const height = duration * PIXELS_PER_MINUTE;

const [openDetail, setOpenDetail] = useState(false)

return (
<div
key={index}
className={`absolute flex flex-col text-sm items-center gap-2 px-2 py-1 border opacity-90 rounded-lg cursor-pointer transition-colors z-10 ${asBooking ? 'bg-primary' : 'bg-zinc-700 border-dashed rounded-l-none'}`}
style={{
top: `${topPosition}px`,
height: `${height}px`,
minHeight: '24px',
width: `${asBooking ? '50%' : '20%'}`,
right: `${asBooking ? '20%' : '0'}`
}}
onClick={() => setOpenDetail(!openDetail)}
>
<div className="flex flex-col gap-2">
<div className="flex justify-between gap-2 items-center w-full">
<p>{asBooking && <span className="font-bold">Booked{" -- "}</span> }{epochToStringDate(startTime)} - {epochToStringDate(endTime)} WIB</p>
<button >
{openDetail ? <ChevronUp /> : <ChevronDown />}
</button>
</div>
<div className={`${openDetail ? 'block' : 'hidden'} bg-gray-700 max-w-xl p-4 rounded-xl`}>
{children}
</div>
</div>
</div>
);
}
18 changes: 18 additions & 0 deletions components/MentorItemComponent.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { epochToStringDate } from "@/lib/helpers"
import { FirestoreMentor } from "@/lib/types"
import Link from "next/link"

export default function MentorItemComponent({ mentor: m }: { mentor: FirestoreMentor }) {
return (
<Link href={`/mentorship/${m.id}`} key={m.id} className="border rounded-xl border-gray-400 p-4 flex justify-between">
<div className="flex flex-col gap-2">
<p className="font-semibold">{m.name}</p>
<p className="text-muted-foreground">{m.email}</p>
<p className="">{m.specialization.toUpperCase()}</p>
</div>
<button className="font-semibold text-sm hover:underline">
View
</button>
</Link>
)
}
Loading