import React, { useState, useEffect } from 'react';
import { Trash2, Plus, Save, Upload, BookOpen } from 'lucide-react';
// GPA scale - standard US 4.0 scale
const gradePoints = {
'A+': 4.0, 'A': 4.0, 'A-': 3.7,
'B+': 3.3, 'B': 3.0, 'B-': 2.7,
'C+': 2.3, 'C': 2.0, 'C-': 1.7,
'D+': 1.3, 'D': 1.0, 'D-': 0.7,
'F': 0.0
};
const GPACalculator = () => {
const [semesters, setSemesters] = useState([{
id: 1,
name: 'Semester 1',
courses: [{ id: 1, name: '', credits: 3, grade: 'A' }]
}]);
const [activeTab, setActiveTab] = useState('current');
// Load data from localStorage on component mount
useEffect(() => {
const savedData = localStorage.getItem('gpaCalculatorData');
if (savedData) {
try {
setSemesters(JSON.parse(savedData));
} catch (e) {
console.error('Failed to load saved data', e);
}
}
}, []);
const addCourse = (semesterId) => {
setSemesters(semesters.map(semester => {
if (semester.id === semesterId) {
return {
...semester,
courses: [
...semester.courses,
{
id: Math.max(0, ...semester.courses.map(c => c.id)) + 1,
name: '',
credits: 3,
grade: 'A'
}
]
};
}
return semester;
}));
};
const removeCourse = (semesterId, courseId) => {
setSemesters(semesters.map(semester => {
if (semester.id === semesterId) {
return {
...semester,
courses: semester.courses.filter(course => course.id !== courseId)
};
}
return semester;
}));
};
const updateCourse = (semesterId, courseId, field, value) => {
setSemesters(semesters.map(semester => {
if (semester.id === semesterId) {
return {
...semester,
courses: semester.courses.map(course => {
if (course.id === courseId) {
return { ...course, [field]: value };
}
return course;
})
};
}
return semester;
}));
};
const addSemester = () => {
const newId = Math.max(0, ...semesters.map(s => s.id)) + 1;
setSemesters([
...semesters,
{
id: newId,
name: `Semester ${newId}`,
courses: [{ id: 1, name: '', credits: 3, grade: 'A' }]
}
]);
};
const removeSemester = (semesterId) => {
setSemesters(semesters.filter(semester => semester.id !== semesterId));
};
const updateSemesterName = (semesterId, name) => {
setSemesters(semesters.map(semester => {
if (semester.id === semesterId) {
return { ...semester, name };
}
return semester;
}));
};
const calculateSemesterGPA = (courses) => {
if (courses.length === 0) return 0;
let totalPoints = 0;
let totalCredits = 0;
courses.forEach(course => {
const credits = parseFloat(course.credits) || 0;
totalPoints += (gradePoints[course.grade] || 0) * credits;
totalCredits += credits;
});
return totalCredits > 0 ? (totalPoints / totalCredits).toFixed(2) : 0;
};
const calculateCumulativeGPA = () => {
let totalPoints = 0;
let totalCredits = 0;
semesters.forEach(semester => {
semester.courses.forEach(course => {
const credits = parseFloat(course.credits) || 0;
totalPoints += (gradePoints[course.grade] || 0) * credits;
totalCredits += credits;
});
});
return totalCredits > 0 ? (totalPoints / totalCredits).toFixed(2) : 0;
};
const saveData = () => {
localStorage.setItem('gpaCalculatorData', JSON.stringify(semesters));
alert('Your GPA data has been saved!');
};
const exportData = () => {
const dataStr = JSON.stringify(semesters, null, 2);
const dataUri = 'data:application/json;charset=utf-8,'+ encodeURIComponent(dataStr);
const exportFileDefaultName = 'gpa-data.json';
const linkElement = document.createElement('a');
linkElement.setAttribute('href', dataUri);
linkElement.setAttribute('download', exportFileDefaultName);
linkElement.click();
};
const importData = (event) => {
const file = event.target.files[0];
if (!file) return;
const reader = new FileReader();
reader.onload = (e) => {
try {
const data = JSON.parse(e.target.result);
setSemesters(data);
localStorage.setItem('gpaCalculatorData', JSON.stringify(data));
alert('GPA data imported successfully!');
} catch (error) {
alert('Error importing data. Please check your file format.');
console.error('Import error:', error);
}
};
reader.readAsText(file);
};
return (
<div className="p-4 max-w-4xl mx-auto bg-white rounded-lg shadow">
<div className="flex items-center mb-6">
<BookOpen className="text-blue-600 mr-2" size={24} />
<h1 className="text-2xl font-bold text-gray-800">College GPA Calculator</h1>
</div>
<div className="mb-6">
<div className="flex border-b">
<button
className={`py-2 px-4 ${activeTab === 'current' ? 'border-b-2 border-blue-500 text-blue-600' : 'text-gray-600'}`}
onClick={() => setActiveTab('current')}
>
Current Semester
</button>
<button
className={`py-2 px-4 ${activeTab === 'cumulative' ? 'border-b-2 border-blue-500 text-blue-600' : 'text-gray-600'}`}
onClick={() => setActiveTab('cumulative')}
>
Cumulative GPA
</button>
</div>
</div>
{activeTab === 'current' && (
<div>
{semesters.map((semester) => (
<div key={semester.id} className="mb-8 p-4 border rounded-lg bg-gray-50">
<div className="flex justify-between items-center mb-4">
<input
type="text"
value={semester.name}
onChange={(e) => updateSemesterName(semester.id, e.target.value)}
className="text-xl font-semibold bg-transparent border-b border-gray-300 focus:border-blue-500 outline-none"
/>
<button
onClick={() => removeSemester(semester.id)}
className="text-red-500 hover:text-red-700"
disabled={semesters.length === 1}
>
<Trash2 size={18} />
</button>
</div>
<div className="overflow-x-auto">
<table className="w-full mb-4">
<thead>
<tr className="bg-gray-100">
<th className="p-2 text-left">Course Name</th>
<th className="p-2 text-center">Credits</th>
<th className="p-2 text-center">Grade</th>
<th className="p-2 text-center">Action</th>
</tr>
</thead>
<tbody>
{semester.courses.map((course) => (
<tr key={course.id} className="border-b">
<td className="p-2">
<input
type="text"
value={course.name}
onChange={(e) => updateCourse(semester.id, course.id, 'name', e.target.value)}
placeholder="Course name"
className="w-full p-1 border rounded"
/>
</td>
<td className="p-2">
<select
value={course.credits}
onChange={(e) => updateCourse(semester.id, course.id, 'credits', e.target.value)}
className="w-full p-1 border rounded text-center"
>
{[0.5, 1, 1.5, 2, 2.5, 3, 3.5, 4, 4.5, 5].map(credit => (
<option key={credit} value={credit}>{credit}</option>
))}
</select>
</td>
<td className="p-2">
<select
value={course.grade}
onChange={(e) => updateCourse(semester.id, course.id, 'grade', e.target.value)}
className="w-full p-1 border rounded text-center"
>
{Object.keys(gradePoints).map(grade => (
<option key={grade} value={grade}>{grade}</option>
))}
</select>
</td>
<td className="p-2 text-center">
<button
onClick={() => removeCourse(semester.id, course.id)}
className="text-red-500 hover:text-red-700"
disabled={semester.courses.length === 1}
>
<Trash2 size={16} />
</button>
</td>
</tr>
))}
</tbody>
</table>
</div>
<div className="flex justify-between items-center">
<button
onClick={() => addCourse(semester.id)}
className="flex items-center text-blue-600 hover:text-blue-800"
>
<Plus size={16} className="mr-1" /> Add Course
</button>
<div className="text-right">
<div className="text-sm text-gray-600">Semester GPA</div>
<div className="text-2xl font-bold">{calculateSemesterGPA(semester.courses)}</div>
</div>
</div>
</div>
))}
<div className="mb-6">
<button
onClick={addSemester}
className="flex items-center bg-blue-600 text-white px-4 py-2 rounded hover:bg-blue-700"
>
<Plus size={16} className="mr-1" /> Add Semester
</button>
</div>
</div>
)}
{activeTab === 'cumulative' && (
<div className="mb-8">
<div className="bg-gray-50 p-6 rounded-lg border">
<h2 className="text-xl font-semibold mb-4">Cumulative GPA Summary</h2>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 mb-6">
<div className="bg-white p-4 rounded-lg border shadow-sm">
<div className="text-sm text-gray-600 mb-1">Overall GPA</div>
<div className="text-3xl font-bold text-blue-600">{calculateCumulativeGPA()}</div>
</div>
<div className="bg-white p-4 rounded-lg border shadow-sm">
<div className="text-sm text-gray-600 mb-1">Total Credits</div>
<div className="text-3xl font-bold text-green-600">
{semesters.reduce((total, semester) =>
total + semester.courses.reduce((sum, course) =>
sum + (parseFloat(course.credits) || 0), 0), 0)}
</div>
</div>
</div>
<h3 className="text-lg font-medium mb-3">Semester Breakdown</h3>
<div className="overflow-x-auto">
<table className="w-full">
<thead>
<tr className="bg-gray-100">
<th className="p-2 text-left">Semester</th>
<th className="p-2 text-center">Credits</th>
<th className="p-2 text-center">GPA</th>
</tr>
</thead>
<tbody>
{semesters.map((semester) => {
const credits = semester.courses.reduce((sum, course) =>
sum + (parseFloat(course.credits) || 0), 0);
return (
<tr key={semester.id} className="border-b">
<td className="p-2">{semester.name}</td>
<td className="p-2 text-center">{credits}</td>
<td className="p-2 text-center">{calculateSemesterGPA(semester.courses)}</td>
</tr>
);
})}
</tbody>
</table>
</div>
</div>
</div>
)}
<div className="flex flex-wrap gap-2 justify-between mt-6">
<div className="flex gap-2">
<button
onClick={saveData}
className="flex items-center bg-green-600 text-white px-4 py-2 rounded hover:bg-green-700"
>
<Save size={16} className="mr-1" /> Save Data
</button>
<button
onClick={exportData}
className="flex items-center bg-purple-600 text-white px-4 py-2 rounded hover:bg-purple-700"
>
<Save size={16} className="mr-1" /> Export JSON
</button>
<div className="relative">
<input
type="file"
id="import-file"
accept=".json"
onChange={importData}
className="absolute inset-0 w-full h-full opacity-0 cursor-pointer"
/>
<button className="flex items-center bg-yellow-600 text-white px-4 py-2 rounded hover:bg-yellow-700">
<Upload size={16} className="mr-1" /> Import Data
</button>
</div>
</div>
</div>
</div>
);
};
export default GPACalculator;
Home Test
