diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 0c01554..cd53745 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -1,9 +1,6 @@ import { BrowserRouter as Router, Routes, Route, Navigate } from 'react-router-dom' -import Navbar from '@/components/ui/Navbar' -import ProtectedRoute from '@/components/ProtectedRoute' -import StatusDashboard from '@/components/StatusDashboard' -import Home from '@/components/Home' -import Welcome from '@/components/Welcome' +import StatusDashboard from '@/ui/StatusDashboard' +import Welcome from '@/ui/Welcome' import { SignupForm, SigninForm, @@ -12,24 +9,18 @@ import { NewPassword, ForgotPassword, } from '@/features/auth/components' -import AdminDashboard2 from '@/components/dashboards/AdminDashboard2' -import JoinATeam from '@/components/JoinATeam' -import CreateOrJoinTeam from '@/components/CreateOrJoinTeam' -import InterviewerAvailability from '@/components/InterviewerAvailability' +import Dashboard from '@/features/dashboard/Dashboard' +import JoinATeam from '@/features/candidate/components/JoinATeam' +import CreateOrJoinTeam from '@/features/auth/CreateOrJoinTeam' +import Availability from '@/features/dashboard/components/Availability' import { AuthProvider } from '@/provider/AuthProvider' -import AdminDashboard from '@/components/dashboards/AdminDashboard' -import InterviewerDashboard from '@/components/dashboards/InterviewerDashboard' -import CandidateDashboard from '@/components/dashboards/CandidateDashboard' -import AdminSettings from './components/AdminSettings' function App() { return ( - } /> - } /> } /> } /> } /> @@ -39,14 +30,10 @@ function App() { } /> } /> } /> - } /> - } /> - - {/* Role-Based Dashboards - Protected */} - } /> - } /> - } /> - } /> + } /> + + {/* Dashboard */} + } /> {/* Catch-all - must be last */} } /> diff --git a/frontend/src/components/AdminSettings.tsx b/frontend/src/components/AdminSettings.tsx deleted file mode 100644 index 25063ef..0000000 --- a/frontend/src/components/AdminSettings.tsx +++ /dev/null @@ -1,414 +0,0 @@ -import { useState } from "react"; -import { Link } from "react-router-dom"; -import { CalendarDays, LayoutDashboard, Users, Settings, Plus, User, X } from "lucide-react"; - -interface Moderator { - id: string; - name: string; - email: string; - isMain?: boolean; -} - -interface Role { - id: string; - name: string; -} - -interface Department { - id: string; - name: string; -} - -export default function AdminSettings() { - const [moderators, setModerators] = useState([ - { id: "1", name: "Jason Van-Humbeek", email: "jason@example.com", isMain: true }, - { id: "2", name: "Vincenzo Milano", email: "vincenzo@example.com" }, - ]); - - const [roles, setRoles] = useState([ - { id: "1", name: "Software Engineer" }, - { id: "2", name: "Academic Coordinator" }, - ]); - - const [departments, setDepartments] = useState([ - { id: "1", name: "Eng" }, - { id: "2", name: "Academics" }, - ]); - - const [interviewersPerInterviewee, setInterviewersPerInterviewee] = useState(2); - const [maxInterviewsPerDay, setMaxInterviewsPerDay] = useState(5); - - // Input visibility states - const [showRoleInput, setShowRoleInput] = useState(false); - const [newRoleName, setNewRoleName] = useState(""); - const [showDeptInput, setShowDeptInput] = useState(false); - const [newDeptName, setNewDeptName] = useState(""); - const [showModeratorInput, setShowModeratorInput] = useState(false); - const [newModeratorEmail, setNewModeratorEmail] = useState(""); - - const removeModerator = (id: string) => { - setModerators(moderators.filter((mod) => mod.id !== id)); - }; - - const removeRole = (id: string) => { - setRoles(roles.filter((role) => role.id !== id)); - }; - - const removeDepartment = (id: string) => { - setDepartments(departments.filter((dept) => dept.id !== id)); - }; - - const addRole = () => { - if (newRoleName.trim()) { - setRoles([...roles, { id: Date.now().toString(), name: newRoleName.trim() }]); - setNewRoleName(""); - setShowRoleInput(false); - } - }; - - const addDepartment = () => { - if (newDeptName.trim()) { - setDepartments([...departments, { id: Date.now().toString(), name: newDeptName.trim() }]); - setNewDeptName(""); - setShowDeptInput(false); - } - }; - - const inviteModerator = () => { - if (newModeratorEmail.trim()) { - // This would typically make an API call to send an invite email - console.log("Inviting moderator:", newModeratorEmail); - // For demo purposes, add them to the list - setModerators([ - ...moderators, - { - id: Date.now().toString(), - name: "Pending", - email: newModeratorEmail.trim(), - }, - ]); - setNewModeratorEmail(""); - setShowModeratorInput(false); - alert(`Invite sent to ${newModeratorEmail}`); - } - }; - - const handleSave = () => { - // This would typically make an API call to save all settings - console.log("Saving settings:", { - moderators, - roles, - departments, - interviewersPerInterviewee, - maxInterviewsPerDay, - }); - alert("Settings saved successfully!"); - }; - - return ( -
- {/* Left Sidebar */} - - - {/* Main Content Area */} -
- {/* Header / Top Right */} -
-

Admin Settings

-
- -
- Name - Admin -
-
-
- - {/* Settings Content */} -
- {/* Moderators Section */} -
-
-

Team Moderators

- -
- - {showModeratorInput && ( -
- setNewModeratorEmail(e.target.value)} - placeholder="Enter moderator email" - className="flex-1 px-3 py-2 text-sm border border-gray-300 rounded-md focus:ring-blue-500 focus:border-blue-500" - onKeyPress={(e) => e.key === "Enter" && inviteModerator()} - /> - - -
- )} - -
- {moderators.map((moderator) => ( -
- - {moderator.name} - {moderator.isMain && ( - (main) - )} - - {!moderator.isMain && ( - - )} -
- ))} -
-
- - {/* Auto Scheduling Preferences Section */} -
-

- Auto Scheduling Preferences -

-
-
- - setInterviewersPerInterviewee(parseInt(e.target.value) || 0)} - className="w-full md:w-48 px-3 py-2 border border-gray-300 rounded-md focus:ring-blue-500 focus:border-blue-500" - placeholder="e.g., 2" - /> -

- Set how many interviewers should meet with each candidate -

-
-
- - setMaxInterviewsPerDay(parseInt(e.target.value) || 0)} - className="w-full md:w-48 px-3 py-2 border border-gray-300 rounded-md focus:ring-blue-500 focus:border-blue-500" - placeholder="e.g., 5" - /> -

- Limit daily interviews to prevent interviewer burnout -

-
-
-
- - {/* Two Column Layout for Roles and Departments */} -
- {/* Manage Roles Section */} -
-
-

Interview Roles

- -
-

Roles available for interviewees

- - {showRoleInput && ( -
- setNewRoleName(e.target.value)} - placeholder="Enter role name" - className="flex-1 px-3 py-1.5 text-sm border border-gray-300 rounded-md focus:ring-blue-500 focus:border-blue-500" - onKeyPress={(e) => e.key === "Enter" && addRole()} - /> - - -
- )} - -
- {roles.map((role) => ( -
- {role.name} - -
- ))} -
-
- - {/* Manage Departments Section */} -
-
-

Departments

- -
-

Departments for interviewers

- - {showDeptInput && ( -
- setNewDeptName(e.target.value)} - placeholder="Enter department name" - className="flex-1 px-3 py-1.5 text-sm border border-gray-300 rounded-md focus:ring-blue-500 focus:border-blue-500" - onKeyPress={(e) => e.key === "Enter" && addDepartment()} - /> - - -
- )} - -
- {departments.map((dept) => ( -
- {dept.name} - -
- ))} -
-
-
- - {/* Save Button */} -
- -
-
-
-
- ); -} \ No newline at end of file diff --git a/frontend/src/components/Home.tsx b/frontend/src/components/Home.tsx deleted file mode 100644 index ff6bbd5..0000000 --- a/frontend/src/components/Home.tsx +++ /dev/null @@ -1,25 +0,0 @@ -import { Button } from '@/components/ui/button' -import { Link } from 'react-router-dom' - -const Home = () => { - return ( -
-

schedule app

-

- welcome to the schedule app. -

- -
-
- -
-
-
- ) -} - -export default Home \ No newline at end of file diff --git a/frontend/src/components/InterviewerAvailability.tsx b/frontend/src/components/InterviewerAvailability.tsx deleted file mode 100644 index 6450121..0000000 --- a/frontend/src/components/InterviewerAvailability.tsx +++ /dev/null @@ -1,292 +0,0 @@ -import { useState } from "react"; -import { Link } from "react-router-dom"; -import { ArrowLeft } from "lucide-react"; -import { Checkbox } from "@/components/ui/checkbox"; - -interface AvailabilityData { - [dateKey: string]: string[]; // dateKey format: "YYYY-MM-DD", value: array of time slots -} - -export default function InterviewerAvailability() { - const [selectedDate, setSelectedDate] = useState(null); - const [availability, setAvailability] = useState({}); - const [currentMonth, setCurrentMonth] = useState(new Date().getMonth()); - const [currentYear, setCurrentYear] = useState(new Date().getFullYear()); - - // Time slots from 8am to 11pm - const timeSlots = [ - "8:00 am", - "9:00 am", - "10:00 am", - "11:00 am", - "12:00 pm", - "1:00 pm", - "2:00 pm", - "3:00 pm", - "4:00 pm", - "5:00 pm", - "6:00 pm", - "7:00 pm", - "8:00 pm", - "9:00 pm", - "10:00 pm", - "11:00 pm", - ]; - - // Get days in month - const getDaysInMonth = (month: number, year: number) => { - return new Date(year, month + 1, 0).getDate(); - }; - - // Get first day of month (0 = Sunday, 1 = Monday, etc.) - const getFirstDayOfMonth = (month: number, year: number) => { - return new Date(year, month, 1).getDay(); - }; - - // Format date as YYYY-MM-DD - const formatDateKey = (date: Date): string => { - const year = date.getFullYear(); - const month = String(date.getMonth() + 1).padStart(2, "0"); - const day = String(date.getDate()).padStart(2, "0"); - return `${year}-${month}-${day}`; - }; - - // Get month name - const getMonthName = (month: number) => { - const months = [ - "January", - "February", - "March", - "April", - "May", - "June", - "July", - "August", - "September", - "October", - "November", - "December", - ]; - return months[month]; - }; - - // Handle date selection - const handleDateClick = (day: number) => { - const date = new Date(currentYear, currentMonth, day); - setSelectedDate(date); - }; - - // Handle time slot toggle - const handleTimeSlotToggle = (timeSlot: string, checked: boolean) => { - if (!selectedDate) return; - - const dateKey = formatDateKey(selectedDate); - const currentTimes = availability[dateKey] || []; - - if (checked) { - setAvailability({ - ...availability, - [dateKey]: [...currentTimes, timeSlot], - }); - } else { - setAvailability({ - ...availability, - [dateKey]: currentTimes.filter((time) => time !== timeSlot), - }); - } - }; - - // Check if time slot is selected for current date - const isTimeSlotSelected = (timeSlot: string): boolean => { - if (!selectedDate) return false; - const dateKey = formatDateKey(selectedDate); - return availability[dateKey]?.includes(timeSlot) || false; - }; - - // Check if date has availability - const dateHasAvailability = (day: number): boolean => { - const date = new Date(currentYear, currentMonth, day); - const dateKey = formatDateKey(date); - return availability[dateKey]?.length > 0 || false; - }; - - // Handle submit - const handleSubmit = async () => { - // TODO: Connect to backend API - console.log("Submitting availability:", availability); - - // Example API call structure: - // const response = await apiFetch('/availability', { - // method: 'POST', - // headers: { 'Content-Type': 'application/json' }, - // body: JSON.stringify(availability) - // }); - - alert("Availability submitted successfully!"); - }; - - // Generate calendar days - const daysInMonth = getDaysInMonth(currentMonth, currentYear); - const firstDay = getFirstDayOfMonth(currentMonth, currentYear); - const days: (number | null)[] = []; - - // Add empty cells for days before the first day of the month - for (let i = 0; i < firstDay; i++) { - days.push(null); - } - - // Add all days of the month - for (let day = 1; day <= daysInMonth; day++) { - days.push(day); - } - - const isDateSelected = (day: number): boolean => { - if (!selectedDate) return false; - return ( - selectedDate.getDate() === day && - selectedDate.getMonth() === currentMonth && - selectedDate.getFullYear() === currentYear - ); - }; - - // Navigation functions - const goToPreviousMonth = () => { - if (currentMonth === 0) { - setCurrentMonth(11); - setCurrentYear(currentYear - 1); - } else { - setCurrentMonth(currentMonth - 1); - } - setSelectedDate(null); - }; - - const goToNextMonth = () => { - if (currentMonth === 11) { - setCurrentMonth(0); - setCurrentYear(currentYear + 1); - } else { - setCurrentMonth(currentMonth + 1); - } - setSelectedDate(null); - }; - - return ( -
-
- {/* Back Button */} - - - Back to Dashboard - - - {/* Instructions */} -
-

- Please select all the times you're available to meet for an interview. We'll use this information to schedule your interview. -

-

- • Select a day, fill in available times, and then repeat process for all available days. -

-
- - {/* Calendar */} -
-
- -

- {getMonthName(currentMonth)} {currentYear} -

- -
- - {/* Calendar Grid */} -
- {/* Day headers */} - {["S", "M", "T", "W", "T", "F", "S"].map((day, index) => ( -
- {day} -
- ))} - - {/* Calendar days */} - {days.map((day, index) => { - if (day === null) { - return
; - } - - const isSelected = isDateSelected(day); - const hasAvailability = dateHasAvailability(day); - - return ( - - ); - })} -
-
- - {/* Time Slot Selector */} - {selectedDate && ( -
- -
- {timeSlots.map((timeSlot) => ( - - ))} -
-
- )} - - {/* Submit Button */} - -
-
- ); -} - diff --git a/frontend/src/components/dashboards/AdminDashboard.tsx b/frontend/src/components/dashboards/AdminDashboard.tsx deleted file mode 100644 index e404878..0000000 --- a/frontend/src/components/dashboards/AdminDashboard.tsx +++ /dev/null @@ -1,113 +0,0 @@ -import { useAuth } from "@/features/auth/hooks/useAuth"; - -export default function AdminDashboard() { - const { user } = useAuth(); - - return ( -
- {/* Header */} -
-
-

Admin Dashboard

-
-
- - {/* Main Content */} -
-
- {/* Welcome Card */} -
-
-

- Welcome back, {user?.name}! 👋 -

-

Role: Administrator

-

Email: {user?.email}

-
-
- - {/* Admin Features Grid */} -
- {/* Users Management */} -
-
-
-
- - - -
-
-
-
- User Management -
-
- Manage all users -
-
-
-
-
-
- - {/* Teams Management */} -
-
-
-
- - - -
-
-
-
- Teams -
-
- Manage teams -
-
-
-
-
-
- - {/* Schedule Overview */} -
-
-
-
- - - -
-
-
-
- Schedules -
-
- View all schedules -
-
-
-
-
-
-
- - {/* Info Box */} -
-

- 🎉 Authentication Working! You're logged in as an Administrator. - This dashboard is a placeholder - actual admin features will be implemented next. -

-
-
-
-
- ); -} - diff --git a/frontend/src/components/dashboards/AdminDashboard2.tsx b/frontend/src/components/dashboards/AdminDashboard2.tsx deleted file mode 100644 index da446c9..0000000 --- a/frontend/src/components/dashboards/AdminDashboard2.tsx +++ /dev/null @@ -1,259 +0,0 @@ -import { useState } from "react"; -import { Link } from "react-router-dom"; -import { CalendarDays, LayoutDashboard, User, Plus, Users, Settings } from "lucide-react"; -import CalendarCard from "@/components/ui/CalendarCard"; - -export default function InterviewerSchedule() { - const [activeTab, setActiveTab] = useState("This week"); - - const tabs = ["Today", "Tomorrow", "This week", "Next week"]; - const currentDate = new Date().getDate(); // get current date - - // Function to extract day number from date string - const getDayFromDate = (dateString: string): number => { - const match = dateString.match(/(\d+)/); - return match ? parseInt(match[1]) : 0; - }; - - // list containing the upcoming interviews, uses the CalendarCard componenet to allow for dynamically added interviews - // add an object to this list an it will appear as a new interview - const interviews = [ - { - id: 1, - role: "Frontend Developer", - interviewer: "Interviewer #2", - date: "Thursday, Oct 9, 2:00 PM" - }, - { - id: 2, - role: "Backend Developer", - interviewer: "Interviewer #3", - date: " Thursday, Oct 9, 4:00 PM" - }, - { - id: 3, - role: "Full Stack Developer", - interviewer: "Interviewer #1", - date: "Saturday, Oct 10, 3:30 PM" - }, - { - id: 4, - role: "Full Stack Developer", - interviewer: "Interviewer #2", - date: "Monday, Oct 20, 3:30 PM" - }, - { - id: 5, - role: "Full Stack Developer", - interviewer: "Interviewer #4", - date: "monday, Oct 11, 3:30 PM" - }, - - ]; - - // TODO: create a system that will schedule interviews and connect it to this list - - - // Function to categorize interviews based on current date - const categorizeInterviews = (interviewsList: typeof interviews) => { - - // create a list with all interviews happening today - const todayInterviews = interviewsList.filter(interview => { - const interviewDay = getDayFromDate(interview.date); - return interviewDay === currentDate; - }); - - // create a list with all interviews happening this week - const thisWeekInterviews = interviewsList.filter(interview => { - const interviewDay = getDayFromDate(interview.date); - return interviewDay >= currentDate && interviewDay <= currentDate + 6; - }); - - // create a list with all interviews happening in the next 7 days - const next7DaysInterviews = interviewsList.filter(interview => { - const interviewDay = getDayFromDate(interview.date); - return interviewDay >= currentDate && interviewDay <= currentDate + 7; - }); - - // return these three in a json format - will become stats - return { - today: todayInterviews.length, - thisWeek: thisWeekInterviews.length, - next7Days: next7DaysInterviews.length - }; - }; - - const stats = categorizeInterviews(interviews); - - const calendarStats = [ - { title: "Today's Interviews", count: stats.today }, - { title: "This week's interviews", count: stats.thisWeek }, - { title: "Next 7 days", count: stats.next7Days } - ]; - - // Function to filter interviews based on active tab - const getFilteredInterviews = (interviewsList: typeof interviews) => { - - switch (activeTab) { - - case "Today": - return interviewsList.filter(interview => { - const interviewDay = getDayFromDate(interview.date); - return interviewDay === currentDate; - }); - - case "Tomorrow": - return interviewsList.filter(interview => { - const interviewDay = getDayFromDate(interview.date); - return interviewDay === currentDate + 1; - }); - - case "This week": - return interviewsList.filter(interview => { - const interviewDay = getDayFromDate(interview.date); - return interviewDay >= currentDate && interviewDay <= currentDate + 6; - }); - - case "Next week": - return interviewsList.filter(interview => { - const interviewDay = getDayFromDate(interview.date); - return interviewDay >= currentDate + 7 && interviewDay <= currentDate + 13; - }); - - default: - return interviewsList; - } - - }; - - return ( - - // left side bar -
- - - {/* Main Content Area */} -
- - {/* Header / Top Right */} -
-
- -
- Name - Interviewer -
-
-
- - {/* Calendar at a glance */} -
-

Welcome back, Name!

-

Your calendar at a glance

-
- {calendarStats.map((stat, index) => ( - - ))} -
-
- - {/* Interview Schedule Section */} -
-

Interview Schedule

- {/* Tabs */} -
- {tabs.map((tab) => ( - - ))} -
- - {/* Interviews List */} -
-

Interviews

-
- {getFilteredInterviews(interviews).map((interview) => ( -
- -
-

- {interview.role} | with {interview.interviewer} -

-

{interview.date}

-
-
- ))} -
-
-
-
-
- ); -} diff --git a/frontend/src/components/dashboards/CandidateDashboard.tsx b/frontend/src/components/dashboards/CandidateDashboard.tsx deleted file mode 100644 index 4074508..0000000 --- a/frontend/src/components/dashboards/CandidateDashboard.tsx +++ /dev/null @@ -1,90 +0,0 @@ -import { useAuth } from "@/features/auth/hooks/useAuth"; - -export default function CandidateDashboard() { - const { user } = useAuth(); - - return ( -
- {/* Header */} -
-
-

Candidate Dashboard

-
-
- - {/* Main Content */} -
-
- {/* Welcome Card */} -
-
-

- Welcome, {user?.name}! 👋 -

-

Role: Candidate

-

Email: {user?.email}

-
-
- - {/* Candidate Features Grid */} -
- {/* My Interviews */} -
-
-
-
- - - -
-
-
-
- My Interviews -
-
- View upcoming interviews -
-
-
-
-
-
- - {/* Schedule Request */} -
-
-
-
- - - -
-
-
-
- Schedule Interview -
-
- Request new interview -
-
-
-
-
-
-
- - {/* Info Box */} -
-

- 🎉 Authentication Working! You're logged in as a Candidate. - This dashboard is a placeholder - actual candidate features will be implemented next. -

-
-
-
-
- ); -} - diff --git a/frontend/src/components/dashboards/InterviewerDashboard.tsx b/frontend/src/components/dashboards/InterviewerDashboard.tsx deleted file mode 100644 index 0e2a1dc..0000000 --- a/frontend/src/components/dashboards/InterviewerDashboard.tsx +++ /dev/null @@ -1,90 +0,0 @@ -import { useAuth } from "@/features/auth/hooks/useAuth"; - -export default function InterviewerDashboard() { - const { user } = useAuth(); - - return ( -
- {/* Header */} -
-
-

Interviewer Dashboard

-
-
- - {/* Main Content */} -
-
- {/* Welcome Card */} -
-
-

- Welcome back, {user?.name}! 👋 -

-

Role: Interviewer

-

Email: {user?.email}

-
-
- - {/* Interviewer Features Grid */} -
- {/* Availability */} -
-
-
-
- - - -
-
-
-
- My Availability -
-
- Set your schedule -
-
-
-
-
-
- - {/* Scheduled Interviews */} -
-
-
-
- - - -
-
-
-
- My Interviews -
-
- View scheduled interviews -
-
-
-
-
-
-
- - {/* Info Box */} -
-

- 🎉 Authentication Working! You're logged in as an Interviewer. - This dashboard is a placeholder - actual interviewer features will be implemented next. -

-
-
-
-
- ); -} - diff --git a/frontend/src/components/ui/CalendarCard.tsx b/frontend/src/components/ui/CalendarCard.tsx deleted file mode 100644 index 83777c0..0000000 --- a/frontend/src/components/ui/CalendarCard.tsx +++ /dev/null @@ -1,14 +0,0 @@ -interface CalendarCardProps { - title: string; - count: number; - className?: string; -} - -export default function CalendarCard({ title, count, className = "" }: CalendarCardProps) { - return ( -
-

{title}

-

{count}

-
- ); -} diff --git a/frontend/src/components/ui/Navbar.tsx b/frontend/src/components/ui/Navbar.tsx deleted file mode 100644 index fe07950..0000000 --- a/frontend/src/components/ui/Navbar.tsx +++ /dev/null @@ -1,64 +0,0 @@ -import { Button } from '@/components/ui/button' -import { Link, useNavigate } from 'react-router-dom' -import { useAuth } from '@/features/auth/hooks/useAuth' - -export default function Navbar() { - const { isAuthenticated, user, logout } = useAuth() - const navigate = useNavigate() - - const handleLogout = async () => { - await logout() - navigate('/') - } - - return ( - - ) -} \ No newline at end of file diff --git a/frontend/src/components/ui/alert.tsx b/frontend/src/components/ui/alert.tsx deleted file mode 100644 index 5afd41d..0000000 --- a/frontend/src/components/ui/alert.tsx +++ /dev/null @@ -1,59 +0,0 @@ -import * as React from "react" -import { cva, type VariantProps } from "class-variance-authority" - -import { cn } from "@/lib/utils" - -const alertVariants = cva( - "relative w-full rounded-lg border px-4 py-3 text-sm [&>svg+div]:translate-y-[-3px] [&>svg]:absolute [&>svg]:left-4 [&>svg]:top-4 [&>svg]:text-foreground [&>svg~*]:pl-7", - { - variants: { - variant: { - default: "bg-background text-foreground", - destructive: - "border-destructive/50 text-destructive dark:border-destructive [&>svg]:text-destructive", - }, - }, - defaultVariants: { - variant: "default", - }, - } -) - -const Alert = React.forwardRef< - HTMLDivElement, - React.HTMLAttributes & VariantProps ->(({ className, variant, ...props }, ref) => ( -
-)) -Alert.displayName = "Alert" - -const AlertTitle = React.forwardRef< - HTMLParagraphElement, - React.HTMLAttributes ->(({ className, ...props }, ref) => ( -
-)) -AlertTitle.displayName = "AlertTitle" - -const AlertDescription = React.forwardRef< - HTMLParagraphElement, - React.HTMLAttributes ->(({ className, ...props }, ref) => ( -
-)) -AlertDescription.displayName = "AlertDescription" - -export { Alert, AlertTitle, AlertDescription } diff --git a/frontend/src/components/ui/badge.tsx b/frontend/src/components/ui/badge.tsx deleted file mode 100644 index 644e9dd..0000000 --- a/frontend/src/components/ui/badge.tsx +++ /dev/null @@ -1,37 +0,0 @@ -import * as React from "react" -import { cva, type VariantProps } from "class-variance-authority" - -import { cn } from "@/lib/utils" - -const badgeVariants = cva( - "inline-flex items-center rounded-md border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2", - { - variants: { - variant: { - default: - "border-transparent bg-primary text-primary-foreground shadow hover:bg-primary/80", - secondary: - "border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80", - destructive: - "border-transparent bg-destructive text-destructive-foreground shadow hover:bg-destructive/80", - outline: "text-foreground", - }, - }, - defaultVariants: { - variant: "default", - }, - } -) - -export interface BadgeProps - extends React.HTMLAttributes, - VariantProps {} - -function Badge({ className, variant, ...props }: BadgeProps) { - return ( -
- ) -} - -// eslint-disable-next-line react-refresh/only-export-components -export { Badge, badgeVariants } diff --git a/frontend/src/components/ui/card.tsx b/frontend/src/components/ui/card.tsx deleted file mode 100644 index cabfbfc..0000000 --- a/frontend/src/components/ui/card.tsx +++ /dev/null @@ -1,76 +0,0 @@ -import * as React from "react" - -import { cn } from "@/lib/utils" - -const Card = React.forwardRef< - HTMLDivElement, - React.HTMLAttributes ->(({ className, ...props }, ref) => ( -
-)) -Card.displayName = "Card" - -const CardHeader = React.forwardRef< - HTMLDivElement, - React.HTMLAttributes ->(({ className, ...props }, ref) => ( -
-)) -CardHeader.displayName = "CardHeader" - -const CardTitle = React.forwardRef< - HTMLDivElement, - React.HTMLAttributes ->(({ className, ...props }, ref) => ( -
-)) -CardTitle.displayName = "CardTitle" - -const CardDescription = React.forwardRef< - HTMLDivElement, - React.HTMLAttributes ->(({ className, ...props }, ref) => ( -
-)) -CardDescription.displayName = "CardDescription" - -const CardContent = React.forwardRef< - HTMLDivElement, - React.HTMLAttributes ->(({ className, ...props }, ref) => ( -
-)) -CardContent.displayName = "CardContent" - -const CardFooter = React.forwardRef< - HTMLDivElement, - React.HTMLAttributes ->(({ className, ...props }, ref) => ( -
-)) -CardFooter.displayName = "CardFooter" - -export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent } diff --git a/frontend/src/components/ui/checkbox.tsx b/frontend/src/components/ui/checkbox.tsx deleted file mode 100644 index e00a7d3..0000000 --- a/frontend/src/components/ui/checkbox.tsx +++ /dev/null @@ -1,28 +0,0 @@ -import * as React from "react" -import * as CheckboxPrimitive from "@radix-ui/react-checkbox" -import { Check } from "lucide-react" - -import { cn } from "@/lib/utils" - -const Checkbox = React.forwardRef< - React.ElementRef, - React.ComponentPropsWithoutRef ->(({ className, ...props }, ref) => ( - - - - - -)) -Checkbox.displayName = CheckboxPrimitive.Root.displayName - -export { Checkbox } diff --git a/frontend/src/components/ui/input.tsx b/frontend/src/components/ui/input.tsx deleted file mode 100644 index 69b64fb..0000000 --- a/frontend/src/components/ui/input.tsx +++ /dev/null @@ -1,22 +0,0 @@ -import * as React from "react" - -import { cn } from "@/lib/utils" - -const Input = React.forwardRef>( - ({ className, type, ...props }, ref) => { - return ( - - ) - } -) -Input.displayName = "Input" - -export { Input } diff --git a/frontend/src/features/admin/components/AdminSettings.tsx b/frontend/src/features/admin/components/AdminSettings.tsx new file mode 100644 index 0000000..2a357a0 --- /dev/null +++ b/frontend/src/features/admin/components/AdminSettings.tsx @@ -0,0 +1,350 @@ +import { useState } from "react"; +import { X } from "lucide-react"; + +interface Moderator { + id: string; + name: string; + email: string; + isMain?: boolean; +} + +interface Role { + id: string; + name: string; +} + +interface Department { + id: string; + name: string; +} + +export default function AdminSettings() { + const [moderators, setModerators] = useState([ + { id: "1", name: "Jason Van-Humbeek", email: "jason@example.com", isMain: true }, + { id: "2", name: "Vincenzo Milano", email: "vincenzo@example.com" }, + ]); + + const [roles, setRoles] = useState([ + { id: "1", name: "Software Engineer" }, + { id: "2", name: "Academic Coordinator" }, + ]); + + const [departments, setDepartments] = useState([ + { id: "1", name: "Eng" }, + { id: "2", name: "Academics" }, + ]); + + const [interviewersPerInterviewee, setInterviewersPerInterviewee] = useState(2); + const [maxInterviewsPerDay, setMaxInterviewsPerDay] = useState(5); + + // Input visibility states + const [showRoleInput, setShowRoleInput] = useState(false); + const [newRoleName, setNewRoleName] = useState(""); + const [showDeptInput, setShowDeptInput] = useState(false); + const [newDeptName, setNewDeptName] = useState(""); + const [showModeratorInput, setShowModeratorInput] = useState(false); + const [newModeratorEmail, setNewModeratorEmail] = useState(""); + + const removeModerator = (id: string) => { + setModerators(moderators.filter((mod) => mod.id !== id)); + }; + + const removeRole = (id: string) => { + setRoles(roles.filter((role) => role.id !== id)); + }; + + const removeDepartment = (id: string) => { + setDepartments(departments.filter((dept) => dept.id !== id)); + }; + + const addRole = () => { + if (newRoleName.trim()) { + setRoles([...roles, { id: Date.now().toString(), name: newRoleName.trim() }]); + setNewRoleName(""); + setShowRoleInput(false); + } + }; + + const addDepartment = () => { + if (newDeptName.trim()) { + setDepartments([...departments, { id: Date.now().toString(), name: newDeptName.trim() }]); + setNewDeptName(""); + setShowDeptInput(false); + } + }; + + const inviteModerator = () => { + if (newModeratorEmail.trim()) { + // This would typically make an API call to send an invite email + console.log("Inviting moderator:", newModeratorEmail); + // For demo purposes, add them to the list + setModerators([ + ...moderators, + { + id: Date.now().toString(), + name: "Pending", + email: newModeratorEmail.trim(), + }, + ]); + setNewModeratorEmail(""); + setShowModeratorInput(false); + alert(`Invite sent to ${newModeratorEmail}`); + } + }; + + const handleSave = () => { + // This would typically make an API call to save all settings + console.log("Saving settings:", { + moderators, + roles, + departments, + interviewersPerInterviewee, + maxInterviewsPerDay, + }); + alert("Settings saved successfully!"); + }; + + return ( +
+ {/* Header */} +

Admin Settings

+ + {/* Moderators Section */} +
+
+

Team Moderators

+ +
+ + {showModeratorInput && ( +
+ setNewModeratorEmail(e.target.value)} + placeholder="Enter moderator email" + className="flex-1 px-3 py-2 text-sm border border-gray-300 rounded-md focus:ring-blue-500 focus:border-blue-500" + onKeyPress={(e) => e.key === "Enter" && inviteModerator()} + /> + + +
+ )} + +
+ {moderators.map((moderator) => ( +
+ + {moderator.name} + {moderator.isMain && ( + (main) + )} + + {!moderator.isMain && ( + + )} +
+ ))} +
+
+ + {/* Auto Scheduling Preferences Section */} +
+

+ Auto Scheduling Preferences +

+
+
+ + setInterviewersPerInterviewee(parseInt(e.target.value) || 0)} + className="w-full md:w-48 px-3 py-2 border border-gray-300 rounded-md focus:ring-blue-500 focus:border-blue-500" + placeholder="e.g., 2" + /> +

+ Set how many interviewers should meet with each candidate +

+
+
+ + setMaxInterviewsPerDay(parseInt(e.target.value) || 0)} + className="w-full md:w-48 px-3 py-2 border border-gray-300 rounded-md focus:ring-blue-500 focus:border-blue-500" + placeholder="e.g., 5" + /> +

+ Limit daily interviews to prevent interviewer burnout +

+
+
+
+ + {/* Two Column Layout for Roles and Departments */} +
+ {/* Manage Roles Section */} +
+
+

Interview Roles

+ +
+

Roles available for interviewees

+ + {showRoleInput && ( +
+ setNewRoleName(e.target.value)} + placeholder="Enter role name" + className="flex-1 px-3 py-1.5 text-sm border border-gray-300 rounded-md focus:ring-blue-500 focus:border-blue-500" + onKeyPress={(e) => e.key === "Enter" && addRole()} + /> + + +
+ )} + +
+ {roles.map((role) => ( +
+ {role.name} + +
+ ))} +
+
+ + {/* Manage Departments Section */} +
+
+

Departments

+ +
+

Departments for interviewers

+ + {showDeptInput && ( +
+ setNewDeptName(e.target.value)} + placeholder="Enter department name" + className="flex-1 px-3 py-1.5 text-sm border border-gray-300 rounded-md focus:ring-blue-500 focus:border-blue-500" + onKeyPress={(e) => e.key === "Enter" && addDepartment()} + /> + + +
+ )} + +
+ {departments.map((dept) => ( +
+ {dept.name} + +
+ ))} +
+
+
+ + {/* Save Button */} +
+ +
+
+ ); +} diff --git a/frontend/src/features/admin/components/index.ts b/frontend/src/features/admin/components/index.ts new file mode 100644 index 0000000..f30aa06 --- /dev/null +++ b/frontend/src/features/admin/components/index.ts @@ -0,0 +1 @@ +export { default as AdminSettings } from "./AdminSettings"; \ No newline at end of file diff --git a/frontend/src/features/admin/hooks/index.ts b/frontend/src/features/admin/hooks/index.ts new file mode 100644 index 0000000..ab0c014 --- /dev/null +++ b/frontend/src/features/admin/hooks/index.ts @@ -0,0 +1 @@ +// \ No newline at end of file diff --git a/frontend/src/features/admin/services/index.ts b/frontend/src/features/admin/services/index.ts new file mode 100644 index 0000000..ab0c014 --- /dev/null +++ b/frontend/src/features/admin/services/index.ts @@ -0,0 +1 @@ +// \ No newline at end of file diff --git a/frontend/src/features/admin/types/index.ts b/frontend/src/features/admin/types/index.ts new file mode 100644 index 0000000..ab0c014 --- /dev/null +++ b/frontend/src/features/admin/types/index.ts @@ -0,0 +1 @@ +// \ No newline at end of file diff --git a/frontend/src/features/admin/utils/index.ts b/frontend/src/features/admin/utils/index.ts new file mode 100644 index 0000000..ab0c014 --- /dev/null +++ b/frontend/src/features/admin/utils/index.ts @@ -0,0 +1 @@ +// \ No newline at end of file diff --git a/frontend/src/components/CreateOrJoinTeam.tsx b/frontend/src/features/auth/CreateOrJoinTeam.tsx similarity index 97% rename from frontend/src/components/CreateOrJoinTeam.tsx rename to frontend/src/features/auth/CreateOrJoinTeam.tsx index 4ce8ae7..e14ed8c 100644 --- a/frontend/src/components/CreateOrJoinTeam.tsx +++ b/frontend/src/features/auth/CreateOrJoinTeam.tsx @@ -1,4 +1,4 @@ -import { Button } from "./ui/button"; +import { Button } from "../../ui/button"; import { Link } from "react-router-dom"; export default function CreateOrJoinTeam() { diff --git a/frontend/src/features/auth/components/ForgotPassword.tsx b/frontend/src/features/auth/components/ForgotPassword.tsx index f9f115f..cdd8c97 100644 --- a/frontend/src/features/auth/components/ForgotPassword.tsx +++ b/frontend/src/features/auth/components/ForgotPassword.tsx @@ -1,6 +1,6 @@ import { useState } from "react"; import { Link, useNavigate } from "react-router-dom"; -import { Button } from "@/components/ui/button"; +import { Button } from "@/ui/button"; import { ArrowLeft } from "lucide-react"; import { useFormValidation } from "../hooks/useFormValidation"; import { forgotPassword } from "../services/authApi"; diff --git a/frontend/src/features/auth/components/NewPassword.tsx b/frontend/src/features/auth/components/NewPassword.tsx index 949673c..f667df5 100644 --- a/frontend/src/features/auth/components/NewPassword.tsx +++ b/frontend/src/features/auth/components/NewPassword.tsx @@ -1,6 +1,6 @@ import { useState } from "react"; import { useNavigate } from "react-router-dom"; -import { Button } from "@/components/ui/button"; +import { Button } from "@/ui/button"; import { usePasswordValidation, isPasswordValid } from "../hooks/usePasswordValidation"; import { useFormValidation } from "../hooks/useFormValidation"; import { resetPassword } from "../services/authApi"; diff --git a/frontend/src/features/auth/components/NewPasswordMade.tsx b/frontend/src/features/auth/components/NewPasswordMade.tsx index 0481e34..f08cfbe 100644 --- a/frontend/src/features/auth/components/NewPasswordMade.tsx +++ b/frontend/src/features/auth/components/NewPasswordMade.tsx @@ -1,4 +1,4 @@ -import { Button } from "@/components/ui/button"; +import { Button } from "@/ui/button"; import { useNavigate } from "react-router-dom"; export default function NewPasswordMade() { diff --git a/frontend/src/features/auth/components/TwoFactorAuth.tsx b/frontend/src/features/auth/components/TwoFactorAuth.tsx index ad165b7..8915f72 100644 --- a/frontend/src/features/auth/components/TwoFactorAuth.tsx +++ b/frontend/src/features/auth/components/TwoFactorAuth.tsx @@ -1,7 +1,6 @@ import { useState, useEffect } from "react"; import { Link, useNavigate } from "react-router-dom"; -import { Button } from "@/components/ui/button"; -import { Input } from "@/components/ui/input"; +import { Button } from "@/ui/button"; import { ArrowLeft } from "lucide-react"; import { verifyResetCode, verifyEmail, setTokens, getCurrentUser } from "../services/authApi"; import { useAuth } from "../hooks/useAuth"; @@ -187,24 +186,16 @@ export default function TwoFactorAuth() { /> {/* 2FA Code input */} -
- - handleCodeChange(e.target.value)} - placeholder="Enter 6-digit code" - maxLength={6} - required - disabled={success} - /> -
- + handleCodeChange(e.target.value)} + placeholder="Enter 6-digit code" + required + disabled={success} + /> {/* Back link */}
>(( + { className, type, ...props }, + ref +) => { + return ( + + ) +}); +Input.displayName = "Input"; export default function JoinATeam() { const [email, setEmail] = useState(""); diff --git a/frontend/src/features/candidate/components/index.ts b/frontend/src/features/candidate/components/index.ts new file mode 100644 index 0000000..677b843 --- /dev/null +++ b/frontend/src/features/candidate/components/index.ts @@ -0,0 +1 @@ +export { default as JoinATeam } from "./JoinATeam"; \ No newline at end of file diff --git a/frontend/src/features/candidate/hooks/index.ts b/frontend/src/features/candidate/hooks/index.ts new file mode 100644 index 0000000..ab0c014 --- /dev/null +++ b/frontend/src/features/candidate/hooks/index.ts @@ -0,0 +1 @@ +// \ No newline at end of file diff --git a/frontend/src/features/candidate/services/index.ts b/frontend/src/features/candidate/services/index.ts new file mode 100644 index 0000000..ab0c014 --- /dev/null +++ b/frontend/src/features/candidate/services/index.ts @@ -0,0 +1 @@ +// \ No newline at end of file diff --git a/frontend/src/features/candidate/types/index.ts b/frontend/src/features/candidate/types/index.ts new file mode 100644 index 0000000..ab0c014 --- /dev/null +++ b/frontend/src/features/candidate/types/index.ts @@ -0,0 +1 @@ +// \ No newline at end of file diff --git a/frontend/src/features/candidate/utils/index.ts b/frontend/src/features/candidate/utils/index.ts new file mode 100644 index 0000000..ab0c014 --- /dev/null +++ b/frontend/src/features/candidate/utils/index.ts @@ -0,0 +1 @@ +// \ No newline at end of file diff --git a/frontend/src/features/dashboard/Dashboard.tsx b/frontend/src/features/dashboard/Dashboard.tsx new file mode 100644 index 0000000..b72ddb5 --- /dev/null +++ b/frontend/src/features/dashboard/Dashboard.tsx @@ -0,0 +1,118 @@ +import { useState } from "react"; +import { + DashboardSidebar, + DashboardHeader, + CalendarStats, + InterviewScheduleSection, + Availability, +} from "./components"; +import { AdminSettings } from "../admin/components"; + +export default function Dashboard() { + const [activePage, setActivePage] = useState("dashboard"); + const currentDate = new Date().getDate(); // get current date + + // Function to extract day number from date string + const getDayFromDate = (dateString: string): number => { + const match = dateString.match(/(\d+)/); + return match ? parseInt(match[1]) : 0; + }; + + // list containing the upcoming interviews, uses the CalendarCard componenet to allow for dynamically added interviews + // add an object to this list an it will appear as a new interview + const interviews = [ + { + id: 1, + role: "Frontend Developer", + interviewer: "Interviewer #2", + date: "Thursday, Oct 9, 2:00 PM", + }, + { + id: 2, + role: "Backend Developer", + interviewer: "Interviewer #3", + date: " Thursday, Oct 9, 4:00 PM", + }, + { + id: 3, + role: "Full Stack Developer", + interviewer: "Interviewer #1", + date: "Saturday, Oct 10, 3:30 PM", + }, + { + id: 4, + role: "Full Stack Developer", + interviewer: "Interviewer #2", + date: "Monday, Oct 20, 3:30 PM", + }, + { + id: 5, + role: "Full Stack Developer", + interviewer: "Interviewer #4", + date: "monday, Oct 11, 3:30 PM", + }, + ]; + + // TODO: create a system that will schedule interviews and connect it to this list + + // Function to categorize interviews based on current date + const categorizeInterviews = (interviewsList: typeof interviews) => { + // create a list with all interviews happening today + const todayInterviews = interviewsList.filter((interview) => { + const interviewDay = getDayFromDate(interview.date); + return interviewDay === currentDate; + }); + + // create a list with all interviews happening this week + const thisWeekInterviews = interviewsList.filter((interview) => { + const interviewDay = getDayFromDate(interview.date); + return interviewDay >= currentDate && interviewDay <= currentDate + 6; + }); + + // create a list with all interviews happening in the next 7 days + const next7DaysInterviews = interviewsList.filter((interview) => { + const interviewDay = getDayFromDate(interview.date); + return interviewDay >= currentDate && interviewDay <= currentDate + 7; + }); + + // return these three in a json format - will become stats + return { + today: todayInterviews.length, + thisWeek: thisWeekInterviews.length, + next7Days: next7DaysInterviews.length, + }; + }; + + const stats = categorizeInterviews(interviews); + + const calendarStats = [ + { title: "Today's Interviews", count: stats.today }, + { title: "This week's interviews", count: stats.thisWeek }, + { title: "Next 7 days", count: stats.next7Days }, + ]; + + return ( +
+ + + {/* Main Content Area */} +
+ + {activePage === "dashboard" ? ( + <> + + + + ) : activePage === "admin-settings" ? ( + + ) : activePage === "availability" ? ( + + ) : null} +
+
+ ); +} diff --git a/frontend/src/features/dashboard/components/Availability.tsx b/frontend/src/features/dashboard/components/Availability.tsx new file mode 100644 index 0000000..f18dada --- /dev/null +++ b/frontend/src/features/dashboard/components/Availability.tsx @@ -0,0 +1,165 @@ +import * as React from "react"; +import { useState } from "react"; +import { Check } from "lucide-react"; +import * as CheckboxPrimitive from "@radix-ui/react-checkbox"; + +interface AvailabilityData { + [dateKey: string]: string[]; +} + +const Checkbox = React.forwardRef< + React.ComponentRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + + + + + +)); +Checkbox.displayName = CheckboxPrimitive.Root.displayName; + +const MONTHS = ["January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"]; +const TIME_SLOTS = ["8:00 am", "9:00 am", "10:00 am", "11:00 am", "12:00 pm", "1:00 pm", "2:00 pm", "3:00 pm", "4:00 pm", "5:00 pm", "6:00 pm", "7:00 pm", "8:00 pm", "9:00 pm", "10:00 pm", "11:00 pm"]; + +export default function Availability() { + const [selectedDate, setSelectedDate] = useState(null); + const [availability, setAvailability] = useState({}); + const [currentMonth, setCurrentMonth] = useState(new Date().getMonth()); + const [currentYear, setCurrentYear] = useState(new Date().getFullYear()); + + const formatDateKey = (date: Date): string => { + const year = date.getFullYear(); + const month = String(date.getMonth() + 1).padStart(2, "0"); + const day = String(date.getDate()).padStart(2, "0"); + return `${year}-${month}-${day}`; + }; + + const getDaysInMonth = (month: number, year: number) => new Date(year, month + 1, 0).getDate(); + const getFirstDayOfMonth = (month: number, year: number) => new Date(year, month, 1).getDay(); + + const handleDateClick = (day: number) => setSelectedDate(new Date(currentYear, currentMonth, day)); + + const handleTimeSlotToggle = (timeSlot: string, checked: boolean) => { + if (!selectedDate) return; + const dateKey = formatDateKey(selectedDate); + const currentTimes = availability[dateKey] || []; + setAvailability({ + ...availability, + [dateKey]: checked ? [...currentTimes, timeSlot] : currentTimes.filter((time) => time !== timeSlot), + }); + }; + + const isTimeSlotSelected = (timeSlot: string): boolean => { + if (!selectedDate) return false; + return availability[formatDateKey(selectedDate)]?.includes(timeSlot) || false; + }; + + const dateHasAvailability = (day: number): boolean => { + const dateKey = formatDateKey(new Date(currentYear, currentMonth, day)); + return availability[dateKey]?.length > 0 || false; + }; + + const isDateSelected = (day: number): boolean => + selectedDate?.getDate() === day && selectedDate?.getMonth() === currentMonth && selectedDate?.getFullYear() === currentYear; + + const handleMonthChange = (direction: number) => { + let newMonth = currentMonth + direction; + let newYear = currentYear; + if (newMonth < 0) { + newMonth = 11; + newYear--; + } else if (newMonth > 11) { + newMonth = 0; + newYear++; + } + setCurrentMonth(newMonth); + setCurrentYear(newYear); + setSelectedDate(null); + }; + + const generateCalendarDays = () => { + const days: (number | null)[] = []; + const firstDay = getFirstDayOfMonth(currentMonth, currentYear); + for (let i = 0; i < firstDay; i++) days.push(null); + for (let day = 1; day <= getDaysInMonth(currentMonth, currentYear); day++) days.push(day); + return days; + }; + + const handleSubmit = async () => { + console.log("Submitting availability:", availability); + alert("Availability submitted successfully!"); + }; + + const days = generateCalendarDays(); + + return ( +
+

Your Availability

+ +
+
+

Please select all the times you're available to meet for an interview. We'll use this information to schedule your interview.

+

• Select a day, fill in available times, and then repeat process for all available days.

+
+ +
+
+ +

{MONTHS[currentMonth]} {currentYear}

+ +
+ +
+ {["S", "M", "T", "W", "T", "F", "S"].map((day, i) => ( +
+ {day} +
+ ))} + {days.map((day, i) => ( +
+ {day === null ? null : ( + + )} +
+ ))} +
+
+
+ + {selectedDate && ( +
+ +
+ {TIME_SLOTS.map((timeSlot) => ( + + ))} +
+
+ )} + + +
+ ); +} + diff --git a/frontend/src/features/dashboard/components/CalendarStats.tsx b/frontend/src/features/dashboard/components/CalendarStats.tsx new file mode 100644 index 0000000..ae007d2 --- /dev/null +++ b/frontend/src/features/dashboard/components/CalendarStats.tsx @@ -0,0 +1,25 @@ +interface CalendarStat { + title: string; + count: number; +} + +interface CalendarStatsProps { + stats: CalendarStat[]; +} + +export default function CalendarStats({ stats }: CalendarStatsProps) { + return ( +
+

Welcome back, Name!

+

Your calendar at a glance

+
+ {stats.map((stat, index) => ( +
+

{stat.title}

+

{stat.count}

+
+ ))} +
+
+ ); +} diff --git a/frontend/src/features/dashboard/components/Header.tsx b/frontend/src/features/dashboard/components/Header.tsx new file mode 100644 index 0000000..51ba740 --- /dev/null +++ b/frontend/src/features/dashboard/components/Header.tsx @@ -0,0 +1,15 @@ +import { User } from "lucide-react"; + +export default function DashboardHeader() { + return ( +
+
+ +
+ Name + Interviewer +
+
+
+ ); +} diff --git a/frontend/src/features/dashboard/components/InterviewScheduleSection.tsx b/frontend/src/features/dashboard/components/InterviewScheduleSection.tsx new file mode 100644 index 0000000..fbf4f98 --- /dev/null +++ b/frontend/src/features/dashboard/components/InterviewScheduleSection.tsx @@ -0,0 +1,98 @@ +import { useState } from "react"; +import { User } from "lucide-react"; + +interface Interview { + id: number; + role: string; + interviewer: string; + date: string; +} + +interface InterviewScheduleSectionProps { + interviews: Interview[]; + getDayFromDate: (dateString: string) => number; + currentDate: number; +} + +export default function InterviewScheduleSection({ + interviews, + getDayFromDate, + currentDate, +}: InterviewScheduleSectionProps) { + const [activeTab, setActiveTab] = useState("This week"); + const tabs = ["Today", "Tomorrow", "This week", "Next week"]; + + const getFilteredInterviews = (interviewsList: Interview[]) => { + switch (activeTab) { + case "Today": + return interviewsList.filter((interview) => { + const interviewDay = getDayFromDate(interview.date); + return interviewDay === currentDate; + }); + + case "Tomorrow": + return interviewsList.filter((interview) => { + const interviewDay = getDayFromDate(interview.date); + return interviewDay === currentDate + 1; + }); + + case "This week": + return interviewsList.filter((interview) => { + const interviewDay = getDayFromDate(interview.date); + return interviewDay >= currentDate && interviewDay <= currentDate + 6; + }); + + case "Next week": + return interviewsList.filter((interview) => { + const interviewDay = getDayFromDate(interview.date); + return interviewDay >= currentDate + 7 && interviewDay <= currentDate + 13; + }); + + default: + return interviewsList; + } + }; + + return ( +
+

Interview Schedule

+ {/* Tabs */} +
+ {tabs.map((tab) => ( + + ))} +
+ + {/* Interviews List */} +
+

Interviews

+
+ {getFilteredInterviews(interviews).map((interview) => ( +
+ +
+

+ {interview.role} | with {interview.interviewer} +

+

{interview.date}

+
+
+ ))} +
+
+
+ ); +} diff --git a/frontend/src/features/dashboard/components/Sidebar.tsx b/frontend/src/features/dashboard/components/Sidebar.tsx new file mode 100644 index 0000000..fa83761 --- /dev/null +++ b/frontend/src/features/dashboard/components/Sidebar.tsx @@ -0,0 +1,79 @@ +import { CalendarDays, LayoutDashboard, Users, Settings, Plus } from "lucide-react"; + +interface DashboardSidebarProps { + activePage?: string; + onPageChange?: (page: string) => void; +} + +export default function DashboardSidebar({ activePage = "dashboard", onPageChange }: DashboardSidebarProps) { + const handleAdminSettingsClick = (e: React.MouseEvent) => { + e.preventDefault(); + onPageChange?.("admin-settings"); + }; + + const handleAvailabilityClick = (e: React.MouseEvent) => { + e.preventDefault(); + onPageChange?.("availability"); + }; + + return ( + + ); +} diff --git a/frontend/src/features/dashboard/components/index.ts b/frontend/src/features/dashboard/components/index.ts new file mode 100644 index 0000000..ce86211 --- /dev/null +++ b/frontend/src/features/dashboard/components/index.ts @@ -0,0 +1,5 @@ +export { default as DashboardSidebar } from "./Sidebar"; +export { default as DashboardHeader } from "./Header"; +export { default as CalendarStats } from "./CalendarStats"; +export { default as InterviewScheduleSection } from "./InterviewScheduleSection"; +export { default as Availability } from "./Availability"; \ No newline at end of file diff --git a/frontend/src/features/dashboard/hooks/index.ts b/frontend/src/features/dashboard/hooks/index.ts new file mode 100644 index 0000000..ab0c014 --- /dev/null +++ b/frontend/src/features/dashboard/hooks/index.ts @@ -0,0 +1 @@ +// \ No newline at end of file diff --git a/frontend/src/features/dashboard/services/index.ts b/frontend/src/features/dashboard/services/index.ts new file mode 100644 index 0000000..ab0c014 --- /dev/null +++ b/frontend/src/features/dashboard/services/index.ts @@ -0,0 +1 @@ +// \ No newline at end of file diff --git a/frontend/src/features/dashboard/types/index.ts b/frontend/src/features/dashboard/types/index.ts new file mode 100644 index 0000000..ab0c014 --- /dev/null +++ b/frontend/src/features/dashboard/types/index.ts @@ -0,0 +1 @@ +// \ No newline at end of file diff --git a/frontend/src/features/dashboard/utils/index.ts b/frontend/src/features/dashboard/utils/index.ts new file mode 100644 index 0000000..ab0c014 --- /dev/null +++ b/frontend/src/features/dashboard/utils/index.ts @@ -0,0 +1 @@ +// \ No newline at end of file diff --git a/frontend/src/features/interviewer/components/index.ts b/frontend/src/features/interviewer/components/index.ts new file mode 100644 index 0000000..ab0c014 --- /dev/null +++ b/frontend/src/features/interviewer/components/index.ts @@ -0,0 +1 @@ +// \ No newline at end of file diff --git a/frontend/src/features/interviewer/hooks/index.ts b/frontend/src/features/interviewer/hooks/index.ts new file mode 100644 index 0000000..ab0c014 --- /dev/null +++ b/frontend/src/features/interviewer/hooks/index.ts @@ -0,0 +1 @@ +// \ No newline at end of file diff --git a/frontend/src/features/interviewer/services/index.ts b/frontend/src/features/interviewer/services/index.ts new file mode 100644 index 0000000..ab0c014 --- /dev/null +++ b/frontend/src/features/interviewer/services/index.ts @@ -0,0 +1 @@ +// \ No newline at end of file diff --git a/frontend/src/features/interviewer/types/index.ts b/frontend/src/features/interviewer/types/index.ts new file mode 100644 index 0000000..ab0c014 --- /dev/null +++ b/frontend/src/features/interviewer/types/index.ts @@ -0,0 +1 @@ +// \ No newline at end of file diff --git a/frontend/src/features/interviewer/utils/index.ts b/frontend/src/features/interviewer/utils/index.ts new file mode 100644 index 0000000..ab0c014 --- /dev/null +++ b/frontend/src/features/interviewer/utils/index.ts @@ -0,0 +1 @@ +// \ No newline at end of file diff --git a/frontend/src/lib/utils.ts b/frontend/src/lib/utils.ts deleted file mode 100644 index bd0c391..0000000 --- a/frontend/src/lib/utils.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { clsx, type ClassValue } from "clsx" -import { twMerge } from "tailwind-merge" - -export function cn(...inputs: ClassValue[]) { - return twMerge(clsx(inputs)) -} diff --git a/frontend/src/components/StatusDashboard.tsx b/frontend/src/ui/StatusDashboard.tsx similarity index 69% rename from frontend/src/components/StatusDashboard.tsx rename to frontend/src/ui/StatusDashboard.tsx index 35ba66b..65bba64 100644 --- a/frontend/src/components/StatusDashboard.tsx +++ b/frontend/src/ui/StatusDashboard.tsx @@ -1,11 +1,140 @@ import { useState, useEffect, useCallback } from 'react' -import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card" -import { Badge } from "@/components/ui/badge" -import { Button } from "@/components/ui/button" -import { Alert, AlertDescription } from "@/components/ui/alert" +import * as React from "react" +import { cva, type VariantProps } from "class-variance-authority" +import { Button } from "@/ui/button" import { CheckCircle, XCircle, Clock, Terminal } from "lucide-react" import { apiFetch } from '@/utils/api' +// ===== CARD COMPONENTS ===== +const Card = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
+)) +Card.displayName = "Card" + +const CardHeader = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
+)) +CardHeader.displayName = "CardHeader" + +const CardTitle = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
+)) +CardTitle.displayName = "CardTitle" + +const CardDescription = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
+)) +CardDescription.displayName = "CardDescription" + +const CardContent = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
+)) +CardContent.displayName = "CardContent" + +// ===== BADGE COMPONENTS ===== +const badgeVariants = cva( + "inline-flex items-center rounded-md border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2", + { + variants: { + variant: { + default: + "border-transparent bg-primary text-primary-foreground shadow hover:bg-primary/80", + secondary: + "border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80", + destructive: + "border-transparent bg-destructive text-destructive-foreground shadow hover:bg-destructive/80", + outline: "text-foreground", + }, + }, + defaultVariants: { + variant: "default", + }, + } +) + +interface BadgeProps + extends React.HTMLAttributes, + VariantProps {} + +function Badge({ className, variant, ...props }: BadgeProps) { + return ( +
+ ) +} + +// ===== ALERT COMPONENTS ===== +const alertVariants = cva( + "relative w-full rounded-lg border px-4 py-3 text-sm [&>svg+div]:translate-y-[-3px] [&>svg]:absolute [&>svg]:left-4 [&>svg]:top-4 [&>svg]:text-foreground [&>svg~*]:pl-7", + { + variants: { + variant: { + default: "bg-background text-foreground", + destructive: + "border-destructive/50 text-destructive dark:border-destructive [&>svg]:text-destructive", + }, + }, + defaultVariants: { + variant: "default", + }, + } +) + +const Alert = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes & VariantProps +>(({ className, variant, ...props }, ref) => ( +
+)) +Alert.displayName = "Alert" + +const AlertDescription = React.forwardRef< + HTMLParagraphElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
+)) +AlertDescription.displayName = "AlertDescription" + // types for our status checks interface StatusCheck { name: string diff --git a/frontend/src/components/Welcome.tsx b/frontend/src/ui/Welcome.tsx similarity index 99% rename from frontend/src/components/Welcome.tsx rename to frontend/src/ui/Welcome.tsx index 1235182..f556380 100644 --- a/frontend/src/components/Welcome.tsx +++ b/frontend/src/ui/Welcome.tsx @@ -1,4 +1,4 @@ -import { Button } from '@/components/ui/button' +import { Button } from '@/ui/button' import { Link } from 'react-router-dom' const Welcome = () => { diff --git a/frontend/src/components/ui/button.tsx b/frontend/src/ui/button.tsx similarity index 94% rename from frontend/src/components/ui/button.tsx rename to frontend/src/ui/button.tsx index d8f7202..0be8260 100644 --- a/frontend/src/components/ui/button.tsx +++ b/frontend/src/ui/button.tsx @@ -2,8 +2,6 @@ import * as React from "react" import { Slot } from "@radix-ui/react-slot" import { cva, type VariantProps } from "class-variance-authority" -import { cn } from "@/lib/utils" - const buttonVariants = cva( "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0", { @@ -45,7 +43,7 @@ const Button = React.forwardRef( const Comp = asChild ? Slot : "button" return ( diff --git a/frontend/src/components/ProtectedRoute.tsx b/frontend/src/utils/ProtectedRoute.tsx similarity index 60% rename from frontend/src/components/ProtectedRoute.tsx rename to frontend/src/utils/ProtectedRoute.tsx index 647b627..6d8fe48 100644 --- a/frontend/src/components/ProtectedRoute.tsx +++ b/frontend/src/utils/ProtectedRoute.tsx @@ -1,43 +1,54 @@ -import { ReactNode } from 'react'; -import { Navigate } from 'react-router-dom'; -import { useAuth } from '@/features/auth/hooks/useAuth'; - -interface ProtectedRouteProps { - children: ReactNode; -} - -/** - * ProtectedRoute Component - * Protects dashboard routes by ensuring: - * 1. User is authenticated - * 2. User's email is verified - * - * If either condition fails, redirects to appropriate page. - */ -export default function ProtectedRoute({ children }: ProtectedRouteProps) { - const { user, isLoading } = useAuth(); - - // Still loading auth state - if (isLoading) { - return ( -
-
-

Loading...

-
-
- ); - } - - // Not authenticated - if (!user) { - return ; - } - - // Email not verified (for signup flow) - if (!user.isEmailVerified) { - return ; - } - - // All checks passed, render the protected component - return <>{children}; -} +import { ReactNode } from 'react'; +import { Navigate } from 'react-router-dom'; +import { useAuth } from '@/features/auth/hooks/useAuth'; +import { UserRole } from '@/features/auth/services/authApi'; + +interface ProtectedRouteProps { + children: ReactNode; + requiredRole?: UserRole; +} + +/** + * ProtectedRoute Component + * Protects dashboard routes by ensuring: + * 1. User is authenticated + * 2. User's email is verified + * 3. User has the required role (if specified) + * + * If any condition fails, redirects to appropriate page. + * + * @param children - The component to render if all checks pass + * @param requiredRole - Optional role requirement (admin, interviewer, candidate) + */ +export default function ProtectedRoute({ children, requiredRole }: ProtectedRouteProps) { + const { user, isLoading } = useAuth(); + + // Still loading auth state + if (isLoading) { + return ( +
+
+

Loading...

+
+
+ ); + } + + // Not authenticated + if (!user) { + return ; + } + + // Email not verified (for signup flow) + if (!user.isEmailVerified) { + return ; + } + + // Check role if required + if (requiredRole && user.role !== requiredRole) { + return ; + } + + // All checks passed, render the protected component + return <>{children}; +}