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
228 changes: 144 additions & 84 deletions src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,45 +6,29 @@ import ExamRegistration from './components/ExamRegistration';
import StudyStrategies from './components/StudyStrategies';
import ExamResultSubmission from './components/ExamResultSubmission';
import StarBackground from './components/StarBackground';
import { ExamRegistration as ExamRegistrationType, ExamQuestion, ExamResult } from './types';

// Mock exam questions for demo
const MOCK_QUESTIONS: ExamQuestion[] = [
{
id: '1',
examType: 'usmle-step1',
state: 'CA',
question: 'A 45-year-old patient presents with chest pain. What is the most likely diagnosis?',
options: ['Myocardial infarction', 'Gastroesophageal reflux', 'Pulmonary embolism', 'Anxiety disorder', 'Costochondritis'],
correctAnswer: 0,
explanation: 'Given the age and presentation, myocardial infarction should be the primary consideration.',
difficulty: 'medium',
timeAllowed: 90,
category: 'cardiology',
verified: true,
createdAt: new Date(),
},
{
id: '2',
examType: 'bar-exam',
state: 'NY',
question: 'In contract law, what constitutes a valid offer?',
options: ['Intent to be bound', 'Definite terms', 'Communication to offeree', 'All of the above', 'None of the above'],
correctAnswer: 3,
explanation: 'A valid offer requires intent, definite terms, and communication to the offeree.',
difficulty: 'easy',
timeAllowed: 120,
category: 'contracts',
verified: true,
createdAt: new Date(),
},
];
import { ExamRegistration as ExamRegistrationType, ExamResult, UserAnswer } from './types';
import { validateApiKey } from './utils/api';

function App() {
const [currentView, setCurrentView] = useState<'landing' | 'registration' | 'study' | 'results'>('landing');
const [currentRegistration, setCurrentRegistration] = useState<ExamRegistrationType | null>(null);
const [darkMode, setDarkMode] = useState(true);
const [examExpired, setExamExpired] = useState(false);
const [apiKeyValid, setApiKeyValid] = useState<boolean | null>(null);

// Validate API key on app start
useEffect(() => {
const checkApiKey = async () => {
try {
const isValid = await validateApiKey();
setApiKeyValid(isValid);
} catch {
setApiKeyValid(false);
}
};

checkApiKey();
}, []);

useEffect(() => {
// Check if exam date has passed
Expand All @@ -63,42 +47,72 @@ function App() {
setExamExpired(false);
setCurrentView('study');

// If failed, could load questions from successful candidates here
if (result.actualExamResult === 'fail') {
console.log('Loading questions from successful candidates...');
// Update registration with result
if (currentRegistration) {
const updatedRegistration = {
...currentRegistration,
examResult: result.actualExamResult,
updatedAt: new Date(),
};
setCurrentRegistration(updatedRegistration);
localStorage.setItem('exam-registration', JSON.stringify(updatedRegistration));
}
};

const handleStudySessionComplete = (answers: any[], score: number) => {
console.log('Study session completed:', { answers, score });
const handleStudySessionComplete = (answers: UserAnswer[], score: number) => {
console.log('Study session completed:', {
answers: answers.length,
score,
examType: currentRegistration?.examType.name,
state: currentRegistration?.state.name
});

// Save session results to localStorage for analytics
const sessionResult = {
timestamp: new Date(),
examType: currentRegistration?.examType.id,
state: currentRegistration?.state.code,
score,
totalQuestions: answers.length,
correctAnswers: answers.filter(a => a.isCorrect).length,
};

const savedResults = JSON.parse(localStorage.getItem('study-sessions') || '[]');
savedResults.push(sessionResult);
localStorage.setItem('study-sessions', JSON.stringify(savedResults));
};

// Check if user needs to report exam results
// Load registration from localStorage
useEffect(() => {
if (examExpired && currentRegistration && !currentRegistration.examResult) {
setCurrentView('results');
}
}, []);

useEffect(() => {
// Load registration from localStorage
const saved = localStorage.getItem('exam-registration');
if (saved) {
const registration = JSON.parse(saved);
setCurrentRegistration(registration);
if (registration.isActive) {
setCurrentView('study');
try {
const registration = JSON.parse(saved);
setCurrentRegistration(registration);
if (registration.isActive && registration.paymentStatus === 'completed') {
setCurrentView('study');
}
} catch (error) {
console.error('Failed to load registration from localStorage:', error);
localStorage.removeItem('exam-registration');
}
}
}, []);

// Save registration to localStorage
useEffect(() => {
// Save registration to localStorage
if (currentRegistration) {
localStorage.setItem('exam-registration', JSON.stringify(currentRegistration));
}
}, [currentRegistration]);

// Check if user needs to report exam results
useEffect(() => {
if (examExpired && currentRegistration && !currentRegistration.examResult) {
setCurrentView('results');
}
}, [examExpired, currentRegistration]);

return (
<CivicAuthProvider clientId="7f8e5073-ab64-46df-825e-07d489f612ff">
<AppContent
Expand All @@ -108,6 +122,7 @@ function App() {
setDarkMode={setDarkMode}
currentRegistration={currentRegistration}
examExpired={examExpired}
apiKeyValid={apiKeyValid}
onRegistrationComplete={handleRegistrationComplete}
onResultSubmitted={handleResultSubmitted}
onStudySessionComplete={handleStudySessionComplete}
Expand All @@ -123,9 +138,10 @@ interface AppContentProps {
setDarkMode: (dark: boolean) => void;
currentRegistration: ExamRegistrationType | null;
examExpired: boolean;
apiKeyValid: boolean | null;
onRegistrationComplete: (registration: ExamRegistrationType) => void;
onResultSubmitted: (result: ExamResult) => void;
onStudySessionComplete: (answers: any[], score: number) => void;
onStudySessionComplete: (answers: UserAnswer[], score: number) => void;
}

const AppContent: React.FC<AppContentProps> = ({
Expand All @@ -135,13 +151,31 @@ const AppContent: React.FC<AppContentProps> = ({
setDarkMode,
currentRegistration,
examExpired,
apiKeyValid,
onRegistrationComplete,
onResultSubmitted,
onStudySessionComplete,
}) => {
const { user, isLoading, error, signIn } = useUser();

const renderContent = () => {
// Show API key error if invalid
if (apiKeyValid === false) {
return (
<div className="text-center py-12">
<div className="bg-red-900/50 border border-red-500 rounded-xl p-8 max-w-md mx-auto">
<h2 className="text-2xl font-bold text-red-300 mb-4">Configuration Error</h2>
<p className="text-red-200 mb-4">
Groq API key is not configured or invalid. Please check your environment variables.
</p>
<p className="text-sm text-red-300">
Add your Groq API key to the .env file as VITE_GROQ_API_KEY
</p>
</div>
</div>
);
}

switch (currentView) {
case 'landing':
return <LandingPage onGetStarted={() => setCurrentView('registration')} />;
Expand All @@ -151,22 +185,45 @@ const AppContent: React.FC<AppContentProps> = ({
if (examExpired) {
return (
<div className="text-center py-12">
<h2 className="text-2xl font-bold text-white mb-4">Study Features Deactivated</h2>
<p className="text-gray-400 mb-6">
Your exam date has passed. Please report your results to reactivate your study features.
</p>
<button
onClick={() => setCurrentView('results')}
className="px-6 py-3 bg-purple-600 text-white rounded-lg hover:bg-purple-700"
>
Report Exam Results
</button>
<div className="bg-orange-900/50 border border-orange-500 rounded-xl p-8 max-w-md mx-auto">
<h2 className="text-2xl font-bold text-orange-300 mb-4">Study Features Deactivated</h2>
<p className="text-orange-200 mb-6">
Your exam date has passed. Please report your results to reactivate your study features.
</p>
<button
onClick={() => setCurrentView('results')}
className="px-6 py-3 bg-purple-600 text-white rounded-lg hover:bg-purple-700"
>
Report Exam Results
</button>
</div>
</div>
);
}

if (!currentRegistration) {
return (
<div className="text-center py-12">
<div className="bg-gray-800/50 border border-gray-700 rounded-xl p-8 max-w-md mx-auto">
<h2 className="text-2xl font-bold text-white mb-4">No Active Registration</h2>
<p className="text-gray-300 mb-6">
Please complete your exam registration to access study features.
</p>
<button
onClick={() => setCurrentView('registration')}
className="px-6 py-3 bg-purple-600 text-white rounded-lg hover:bg-purple-700"
>
Register for Exam
</button>
</div>
</div>
);
}

return (
<StudyStrategies
questions={MOCK_QUESTIONS}
examType={currentRegistration.examType.id}
state={currentRegistration.state.code}
onSessionComplete={onStudySessionComplete}
/>
);
Expand All @@ -182,49 +239,52 @@ const AppContent: React.FC<AppContentProps> = ({
}
};

// If authentication is loading, show a loading state
if (isLoading) {
// Loading state
if (isLoading || apiKeyValid === null) {
return (
<div className={`min-h-screen flex items-center justify-center transition-colors duration-300 ${darkMode ? 'bg-gray-900 text-white' : 'bg-gray-50 text-gray-900'}`}>
<StarBackground darkMode={darkMode} />
<div className="text-center">
<div className="text-center relative z-10">
<div className="animate-spin rounded-full h-12 w-12 border-t-2 border-b-2 border-purple-500 mx-auto"></div>
<p className="mt-4 text-lg">Loading...</p>
</div>
</div>
);
}

// If there's an error, show it
// Authentication error
if (error) {
return (
<div className={`min-h-screen flex items-center justify-center transition-colors duration-300 ${darkMode ? 'bg-gray-900 text-white' : 'bg-gray-50 text-gray-900'}`}>
<StarBackground darkMode={darkMode} />
<div className="text-center">
<p className="text-red-500 text-lg">Error: {error.message}</p>
<button
onClick={() => {
try {
signIn();
} catch (err) {
console.error('Sign-in error:', err);
}
}}
className="mt-4 px-6 py-2 rounded-lg bg-gradient-to-r from-purple-600 to-pink-600 text-white font-medium hover:scale-105 transition-all"
>
Try Again
</button>
<div className="text-center relative z-10">
<div className="bg-red-900/50 border border-red-500 rounded-xl p-8 max-w-md mx-auto">
<p className="text-red-300 text-lg mb-4">Authentication Error</p>
<p className="text-red-200 text-sm mb-4">{error.message}</p>
<button
onClick={() => {
try {
signIn();
} catch (err) {
console.error('Sign-in error:', err);
}
}}
className="px-6 py-2 rounded-lg bg-gradient-to-r from-purple-600 to-pink-600 text-white font-medium hover:scale-105 transition-all"
>
Try Again
</button>
</div>
</div>
</div>
);
}

// If no user is authenticated, show a login prompt
// Not authenticated
if (!user) {
return (
<div className={`min-h-screen flex items-center justify-center transition-colors duration-300 ${darkMode ? 'bg-gray-900 text-white' : 'bg-gray-50 text-gray-900'}`}>
<StarBackground darkMode={darkMode} />
<div className="text-center p-8 rounded-lg bg-opacity-80 backdrop-blur-md ${darkMode ? 'bg-gray-800/80' : 'bg-white/80'}">
<div className="text-center p-8 rounded-lg bg-opacity-80 backdrop-blur-md bg-gray-800/80 relative z-10 max-w-md mx-auto">
<h1 className="text-3xl font-bold bg-gradient-to-r from-purple-400 to-pink-400 bg-clip-text text-transparent mb-4">
Welcome to TutorBot
</h1>
Expand All @@ -246,7 +306,7 @@ const AppContent: React.FC<AppContentProps> = ({
);
}

// If authenticated, render the full app
// Authenticated - render full app
return (
<div className={`min-h-screen transition-colors duration-300 ${darkMode ? 'bg-gray-900 text-white' : 'bg-gray-50 text-gray-900'}`}>
<StarBackground darkMode={darkMode} />
Expand Down
Loading