Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
27 changes: 27 additions & 0 deletions jportal/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions jportal/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@
"sonner": "^2.0.7",
"tailwind-merge": "^2.5.4",
"tailwindcss-animate": "^1.0.7",
"web-haptics": "^0.0.6",
"zod": "^3.23.8",
"zustand": "^5.0.8"
},
Expand Down
18 changes: 14 additions & 4 deletions jportal/src/components/AttendanceCard.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { Card } from "@/components/ui/card";
import { Sheet, SheetContent, SheetHeader, SheetTitle } from "@/components/ui/sheet";
import { Calendar } from "@/components/ui/calendar";
import { LineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer } from "recharts";
import { useHaptics } from "@/hooks/useHaptics";

const AttendanceCard = ({
subject,
Expand All @@ -21,7 +22,9 @@ const AttendanceCard = ({
const [isLoading, setIsLoading] = useState(false);
const [selectedDate, setSelectedDate] = useState(null);

const haptics = useHaptics();
const handleClick = async () => {
haptics.tap();
setSelectedSubject(subject);
if (!subjectAttendanceData[subject.name]) {
setIsLoading(true);
Expand Down Expand Up @@ -152,9 +155,12 @@ const AttendanceCard = ({

<Sheet
open={selectedSubject?.name === subject.name}
onOpenChange={() => {
setSelectedSubject(null);
setSelectedDate(null);
onOpenChange={(open) => {
if (!open) {
haptics.tap();
setSelectedSubject(null);
setSelectedDate(null);
}
}}
>
<SheetContent side="bottom" className="h-[70vh] bg-background text-foreground border-0 overflow-hidden">
Expand Down Expand Up @@ -268,7 +274,11 @@ const AttendanceCard = ({
},
}}
selected={selectedDate}
onSelect={(date) => setSelectedDate(date)}
onSelect={(date) => {
if (date) haptics.selection();
setSelectedDate(date);
}}
onMonthChange={() => haptics.selection()}
className={`pb-2 text-foreground ${isLoading ? "animate-pulse" : ""} w-full shrink-0 max-w-full`}
classNames={{
months: "flex flex-col sm:flex-row space-y-4 sm:space-x-4 sm:space-y-0",
Expand Down
3 changes: 3 additions & 0 deletions jportal/src/components/Header.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import LogoutIcon from "@/../public/icons/logout.svg?react";
import { Link } from "react-router-dom";
import { ChartNoAxesCombined } from "lucide-react";
import { useState, useEffect, useRef } from "react";
import { useHaptics } from "@/hooks/useHaptics";

const FairyLights = () => {
const [textMetrics, setTextMetrics] = useState(null);
Expand Down Expand Up @@ -216,8 +217,10 @@ const FairyLights = () => {

const Header = ({ setIsAuthenticated, setIsDemoMode }) => {
const navigate = useNavigate();
const haptics = useHaptics();

const handleLogout = () => {
haptics.tap();
localStorage.removeItem("username");
localStorage.removeItem("password");
localStorage.removeItem("attendanceData");
Expand Down
15 changes: 15 additions & 0 deletions jportal/src/components/InteractiveGPAChart.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,10 @@ import { CHART_CONFIG, DRAG_CONFIG } from "@/utils/chartConstants";
import DraggableDot from "./DraggableDot";
import GPAChartTooltip from "./GPAChartTooltip";
import { Card } from "@/components/ui/card";
import { useHaptics } from "@/hooks/useHaptics";

export default function InteractiveGPAChart({ semesterData, onDataChange }) {
const haptics = useHaptics();
const [chartData, setChartData] = useState(semesterData);
const [dragState, setDragState] = useState({
isDragging: false,
Expand All @@ -29,6 +31,7 @@ export default function InteractiveGPAChart({ semesterData, onDataChange }) {
// Don't allow dragging during intro animation
if (isAnimating) return;

haptics.tap();
const semesterValue = chartData[index].sgpa;
setDragState({
isDragging: true,
Expand Down Expand Up @@ -61,6 +64,10 @@ export default function InteractiveGPAChart({ semesterData, onDataChange }) {
// Round to specified precision
newValue = Math.round(newValue / DRAG_CONFIG.precision) * DRAG_CONFIG.precision;

if (newValue !== chartData[dragState.draggedIndex].sgpa) {
haptics.selection();
}

// Recalculate CGPA based on new SGPA
const updatedData = recalculateCGPA(chartData, dragState.draggedIndex, newValue);
setChartData(updatedData);
Expand All @@ -72,6 +79,7 @@ export default function InteractiveGPAChart({ semesterData, onDataChange }) {
};

const handleDragEnd = () => {
haptics.tap();
setDragState({
isDragging: false,
draggedIndex: null,
Expand All @@ -85,6 +93,7 @@ export default function InteractiveGPAChart({ semesterData, onDataChange }) {
// Don't allow dragging during intro animation
if (isAnimating) return;

haptics.tap();
setCardDragState({
isDragging: true,
draggedIndex: index,
Expand All @@ -106,6 +115,10 @@ export default function InteractiveGPAChart({ semesterData, onDataChange }) {
// Round to specified precision
newValue = Math.round(newValue / DRAG_CONFIG.precision) * DRAG_CONFIG.precision;

if (newValue !== chartData[index].sgpa) {
haptics.selection();
}

// Recalculate CGPA based on new SGPA
const updatedData = recalculateCGPA(chartData, index, newValue);
setChartData(updatedData);
Expand All @@ -123,6 +136,7 @@ export default function InteractiveGPAChart({ semesterData, onDataChange }) {
};

const handleCardDragEnd = () => {
haptics.tap();
setCardDragState({
isDragging: false,
draggedIndex: null,
Expand Down Expand Up @@ -176,6 +190,7 @@ export default function InteractiveGPAChart({ semesterData, onDataChange }) {
// Animation complete - reset to original values
setChartData(originalDataRef.current);
setIsAnimating(false);
haptics.success();
if (onDataChange) {
onDataChange(originalDataRef.current);
}
Expand Down
7 changes: 7 additions & 0 deletions jportal/src/components/Login.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { toast } from "sonner";
import { Eye, EyeOff } from "lucide-react";
import { LoginError } from "https://cdn.jsdelivr.net/npm/jsjiit@0.0.23/dist/jsjiit.esm.js";
import PublicHeader from "./PublicHeader";
import { useHaptics } from "@/hooks/useHaptics";

// Define the form schema
const formSchema = z.object({
Expand All @@ -27,6 +28,7 @@ export default function Login({ onLoginSuccess, onDemoLogin, w }) {
isLoading: false,
credentials: null,
});
const haptics = useHaptics();

// Initialize form
const form = useForm({
Expand All @@ -39,6 +41,7 @@ export default function Login({ onLoginSuccess, onDemoLogin, w }) {

// Handle demo login
const handleDemoLogin = () => {
haptics.success();
onDemoLogin();
};

Expand All @@ -55,6 +58,7 @@ export default function Login({ onLoginSuccess, onDemoLogin, w }) {
localStorage.setItem("password", loginStatus.credentials.password);

console.log("Login successful");
haptics.success();
setLoginStatus((prev) => ({
...prev,
isLoading: false,
Expand All @@ -67,11 +71,14 @@ export default function Login({ onLoginSuccess, onDemoLogin, w }) {
error.message.includes("JIIT Web Portal server is temporarily unavailable")
) {
console.error("JIIT Web Portal server is temporarily unavailable");
haptics.error();
toast.error("JIIT Web Portal server is temporarily unavailable. Please try again later.");
} else if (error instanceof LoginError && error.message.includes("Failed to fetch")) {
haptics.error();
toast.error("Please check your internet connection. If connected, JIIT Web Portal server is unavailable.");
} else {
console.error("Login failed:", error);
haptics.error();
toast.error("Login failed. Please check your credentials.");
}
setLoginStatus((prev) => ({
Expand Down
4 changes: 4 additions & 0 deletions jportal/src/components/Navbar.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,11 @@ import GradesIcon from "@/../public/icons/grades.svg?react";
import ExamsIcon from "@/../public/icons/exams.svg?react";
import SubjectsIcon from "@/../public/icons/subjects1.svg?react";
import ProfileIcon from "@/../public/icons/profile.svg?react";
import { useHaptics } from "@/hooks/useHaptics";

function Navbar() {
const haptics = useHaptics();

const navItems = [
{ name: "ATTENDANCE", path: "/attendance", IconComponent: AttendanceIcon },
{ name: " GRADES ", path: "/grades", IconComponent: GradesIcon },
Expand All @@ -20,6 +23,7 @@ function Navbar() {
<NavLink
key={item.name}
to={item.path}
onClick={() => haptics.tap()}
className={({ isActive }) => `
flex-1 text-md text-muted-foreground data-[state=active]:bg-background data-[state=active]:text-foreground text-clip overflow-hidden whitespace-nowrap
${isActive ? "opacity-100" : "opacity-70"}
Expand Down
3 changes: 3 additions & 0 deletions jportal/src/components/theme-selector.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,21 @@ import { defaultPresets } from "../utils/theme-presets";
import { useThemeStore } from "../stores/theme-store";
import { Button } from "./ui/button";
import { cn } from "../lib/utils";
import { useHaptics } from "../hooks/useHaptics";

interface ThemeSelectorProps {
className?: string;
}

export function ThemeSelector({ className }: ThemeSelectorProps) {
const { themeState, setThemeState } = useThemeStore();
const haptics = useHaptics();

const handleThemeSelect = (presetKey: string) => {
const preset = defaultPresets[presetKey];
if (!preset) return;

haptics.success();
setThemeState({
...themeState,
preset: presetKey,
Expand Down
14 changes: 13 additions & 1 deletion jportal/src/components/ui/button.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { Slot } from "@radix-ui/react-slot"
import { cva } from "class-variance-authority";

import { cn } from "@/lib/utils"
import { useHaptics } from "@/hooks/useHaptics"

const buttonVariants = cva(
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
Expand Down Expand Up @@ -35,11 +36,22 @@ const buttonVariants = cva(

const Button = React.forwardRef(({ className, variant, size, asChild = false, ...props }, ref) => {
const Comp = asChild ? Slot : "button"
const haptics = useHaptics();

const handleClick = (e) => {
// Only trigger haptic if button is not disabled
if (!props.disabled) {
haptics.tap();
}
props.onClick?.(e);
};

return (
(<Comp
className={cn(buttonVariants({ variant, size, className }))}
ref={ref}
{...props} />)
{...props}
onClick={handleClick} />)
);
})
Button.displayName = "Button"
Expand Down
28 changes: 19 additions & 9 deletions jportal/src/components/ui/dialog.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import * as DialogPrimitive from "@radix-ui/react-dialog"
import { X } from "lucide-react"

import { cn } from "@/lib/utils"
import { useHaptics } from "@/hooks/useHaptics"

const Dialog = DialogPrimitive.Root

Expand All @@ -12,15 +13,24 @@ const DialogPortal = DialogPrimitive.Portal

const DialogClose = DialogPrimitive.Close

const DialogOverlay = React.forwardRef(({ className, ...props }, ref) => (
<DialogPrimitive.Overlay
ref={ref}
className={cn(
"fixed inset-0 z-50 bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
className
)}
{...props} />
))
const DialogOverlay = React.forwardRef(({ className, ...props }, ref) => {
const haptics = useHaptics();

// Trigger haptic when overlay appears (dialog opens)
React.useEffect(() => {
haptics.tap();
}, []);

return (
<DialogPrimitive.Overlay
ref={ref}
className={cn(
"fixed inset-0 z-50 bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
className
)}
{...props} />
);
})
DialogOverlay.displayName = DialogPrimitive.Overlay.displayName

const DialogContent = React.forwardRef(({ className, children, ...props }, ref) => (
Expand Down
Loading