Skip to content
Merged
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
32 changes: 32 additions & 0 deletions src/components/AnalyticsDashboard.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
// AnalyticsDashboard.tsx
import QuestionBreakdown from './QuestionBreakdown';
import TopicPerformance from './TopicPerformance';
import ScoreChart from './ScoreChart';
import ExportButton from './ExportButton';
import { groupByTopic } from '../utils/analytics.helpers';

const AnalyticsDashboard = ({ questions }) => {
const topics = groupByTopic(questions);

return (
<div className="bg-gray-900 text-white p-6 rounded-xl space-y-6">
<div className="flex justify-between items-center">
<h2 className="text-xl font-semibold">
Test Analytics
</h2>
<ExportButton data={questions} />
</div>

{/* 🔹 Chart */}
<ScoreChart data={topics} />

{/* 🔹 Topic Performance */}
<TopicPerformance topics={topics} />

{/* 🔹 Question Breakdown */}
<QuestionBreakdown questions={questions} />
</div>
);
};

export default AnalyticsDashboard;
37 changes: 37 additions & 0 deletions src/components/HintBox.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
// HintBox.tsx
import React from 'react';

interface Props {
hint?: string;
revealed: boolean;
onReveal: () => void;
penalty?: number;
}

const HintBox: React.FC<Props> = ({
hint,
revealed,
onReveal,
penalty = 0,
}) => {
if (!hint) return null;

return (
<div className="mt-3">
{!revealed ? (
<button
onClick={onReveal}
className="text-sm text-yellow-400 hover:underline"
>
💡 Show Hint {penalty > 0 && `( -${penalty} pts )`}
</button>
) : (
<div className="bg-yellow-500/10 border border-yellow-500 p-3 rounded-md text-sm text-yellow-200">
{hint}
</div>
)}
</div>
);
};

export default HintBox;
100 changes: 100 additions & 0 deletions src/components/QuizResult.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
import React from 'react';

type QuestionResult = {
question: string;
options: string[];
correctAnswer: string;
selectedAnswer: string;
explanation: string;
};

interface Props {
results: QuestionResult[];
}

const QuizResult: React.FC<Props> = ({ results }) => {
const total = results.length;
const correctCount = results.filter(
(q) => q.selectedAnswer === q.correctAnswer
).length;

const scorePercent = Math.round((correctCount / total) * 100);

return (
<div className="bg-gray-900 text-white p-6 rounded-xl max-w-3xl mx-auto">
{/* 🔹 Score Summary */}
<div className="mb-6 border-b border-gray-700 pb-4">
<h2 className="text-xl font-semibold">Quiz Results</h2>
<p className="text-sm text-gray-400 mt-1">
You got <strong>{correctCount}</strong> out of{' '}
<strong>{total}</strong> correct
</p>

<div className="mt-3">
<div className="w-full bg-gray-700 rounded-full h-3">
<div
className="bg-green-500 h-3 rounded-full"
style={{ width: `${scorePercent}%` }}
/>
</div>
<p className="text-sm mt-1">{scorePercent}% Score</p>
</div>
</div>

{/* 🔹 Questions Review */}
<div className="space-y-6">
{results.map((q, index) => {
const isCorrect = q.selectedAnswer === q.correctAnswer;

return (
<div
key={index}
className="p-4 rounded-lg border border-gray-700"
>
<p className="font-medium mb-3">
{index + 1}. {q.question}
</p>

<div className="space-y-2">
{q.options.map((option, i) => {
const isSelected = option === q.selectedAnswer;
const isCorrectOption = option === q.correctAnswer;

let className = 'block px-3 py-2 rounded-md text-sm';

if (isCorrectOption) {
className += ' bg-green-600/30 border border-green-500';
} else if (isSelected && !isCorrect) {
className += ' bg-red-600/30 border border-red-500';
} else {
className += ' bg-gray-800';
}

return (
<div key={i} className={className}>
{option}
</div>
);
})}
</div>

{/* 🔹 Explanation */}
<div className="mt-3 text-sm text-gray-300">
<p>
<strong>
{isCorrect ? 'Correct ✅' : 'Incorrect ❌'}
</strong>
</p>
<p className="mt-1 text-gray-400">
{q.explanation}
</p>
</div>
</div>
);
})}
</div>
</div>
);
};

export default QuizResult;
91 changes: 91 additions & 0 deletions src/components/follow/follow.api.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
import React, { useEffect, useState } from 'react';

interface Props {
userId: string; // target user
}

const FollowCard: React.FC<Props> = ({ userId }) => {
const [isFollowing, setIsFollowing] = useState(false);
const [followers, setFollowers] = useState(0);
const [loading, setLoading] = useState(false);

// 🔹 Fetch initial data
useEffect(() => {
const fetchData = async () => {
try {
const [statusRes, countRes] = await Promise.all([
fetch(`/api/follow/status/${userId}`),
fetch(`/api/follow/count/${userId}`),
]);

const status = await statusRes.json();
const counts = await countRes.json();

setIsFollowing(status);
setFollowers(counts.followers);
} catch (err) {
console.error(err);
}
};

fetchData();
}, [userId]);

// 🔹 Handle follow/unfollow
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
setLoading(true);

try {
if (isFollowing) {
await fetch(`/api/follow/${userId}`, {
method: 'DELETE',
});

setIsFollowing(false);
setFollowers((prev) => prev - 1); // optimistic
} else {
await fetch(`/api/follow`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ userId }),
});

setIsFollowing(true);
setFollowers((prev) => prev + 1); // optimistic
}
} catch (err) {
console.error(err);
} finally {
setLoading(false);
}
};

return (
<div className="bg-gray-900 text-white p-4 rounded-xl w-fit">
<p className="text-sm mb-2">
<strong>{followers}</strong> Followers
</p>

<form onSubmit={handleSubmit}>
<button
type="submit"
disabled={loading}
className={`px-4 py-2 rounded-lg text-sm font-medium transition ${
isFollowing
? 'bg-gray-700'
: 'bg-green-600 hover:bg-green-500'
}`}
>
{loading
? 'Processing...'
: isFollowing
? 'Unfollow'
: 'Follow'}
</button>
</form>
</div>
);
};

export default FollowCard;
Loading