Skip to content

Commit f02dd13

Browse files
committed
Fix TypeScript errors and remove comments
1 parent 94c61dd commit f02dd13

File tree

16 files changed

+507
-174
lines changed

16 files changed

+507
-174
lines changed

.github/workflows/ci.yml

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
name: CI
2+
3+
on: [push, pull_request]
4+
5+
jobs:
6+
build:
7+
runs-on: ubuntu-latest
8+
9+
steps:
10+
- uses: actions/checkout@v4
11+
- name: Use Node.js
12+
uses: actions/setup-node@v4
13+
with:
14+
node-version: '20.x'
15+
- run: npm ci
16+
- run: npm run build
17+
- run: npx tsc --noEmit

backend/app.py

Lines changed: 49 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,31 +1,66 @@
1+
import os
2+
import numpy as np
13
from flask import Flask, request, jsonify
24
from flask_cors import CORS
3-
import numpy as np
45
from sklearn.datasets import load_breast_cancer
56
from sklearn.ensemble import GradientBoostingClassifier
7+
from pydantic import BaseModel, ValidationError, conlist
8+
from typing import List
9+
from waitress import serve
610

711
app = Flask(__name__)
8-
CORS(app, resources={r"/*": {"origins": [
9-
"http://localhost:5173",
10-
"http://127.0.0.1:5173",
11-
"http://localhost:3000",
12-
"http://127.0.0.1:3000",
13-
]}})
1412

13+
# CORS Configuration via Env Vars
14+
ALLOWED_ORIGINS = os.getenv("FRONTEND_URL", "http://localhost:5173").split(",")
15+
CORS(app, resources={r"/*": {"origins": ALLOWED_ORIGINS}})
16+
17+
# Train Model on Startup
1518
data = load_breast_cancer()
1619
X, y = data.data, data.target
1720
model = GradientBoostingClassifier(n_estimators=100, random_state=42)
1821
model.fit(X, y)
1922

23+
# Input Validation Schema
24+
class PredictionInput(BaseModel):
25+
data: List[float]
2026

21-
@app.route("/predict", methods=["POST", "OPTIONS"])
27+
@app.route("/predict", methods=["POST"])
2228
def predict():
23-
body = request.get_json(force=True)
24-
features = np.array(body["data"]).reshape(1, -1)
25-
prediction = model.predict(features)
26-
result = "Benign" if prediction[0] == 1 else "Malignant"
27-
return jsonify({"result": result})
29+
try:
30+
# 1. Parse & Validate Input
31+
body = request.get_json(force=True)
32+
if not body:
33+
return jsonify({"error": "Empty request body"}), 400
34+
35+
# Pydantic validation
36+
input_data = PredictionInput(**body)
37+
38+
# 2. Check Feature Count
39+
features = np.array(input_data.data).reshape(1, -1)
40+
if features.shape[1] != X.shape[1]:
41+
return jsonify({
42+
"error": f"Invalid feature count. Expected {X.shape[1]}, got {features.shape[1]}"
43+
}), 400
44+
45+
# 3. Prediction
46+
prediction = model.predict(features)
47+
result = "Benign" if prediction[0] == 1 else "Malignant"
48+
49+
return jsonify({"result": result})
50+
51+
except ValidationError as e:
52+
return jsonify({"error": "Validation Error", "details": e.errors()}), 400
53+
except Exception as e:
54+
# Log error in production (print for now)
55+
print(f"Prediction Error: {e}")
56+
return jsonify({"error": "Internal Server Error"}), 500
2857

58+
@app.route("/health", methods=["GET"])
59+
def health():
60+
return jsonify({"status": "healthy"}), 200
2961

3062
if __name__ == "__main__":
31-
app.run(debug=True, host="127.0.0.1", port=5000)
63+
# Production Server
64+
port = int(os.environ.get("PORT", 5000))
65+
print(f"Starting production server on port {port}...")
66+
serve(app, host="0.0.0.0", port=port)

backend/requirements.txt

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
1-
Flask==2.3.3
2-
Flask-CORS==4.0.0
3-
numpy==1.24.3
4-
scikit-learn==1.3.0
1+
flask
2+
flask-cors
3+
numpy
4+
scikit-learn
5+
pydantic
6+
waitress

render.yaml

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
services:
2+
- type: web
3+
name: edcare-backend
4+
env: python
5+
buildCommand: pip install -r backend/requirements.txt
6+
startCommand: python backend/app.py
7+
envVars:
8+
- key: PYTHON_VERSION
9+
value: 3.11.0
10+
- key: FRONTEND_URL
11+
value: https://your-frontend-url.vercel.app

src/components/ErrorBoundary.tsx

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
import React, { Component, ErrorInfo, ReactNode } from 'react';
2+
import { Button } from './ui/Button';
3+
4+
interface Props {
5+
children?: ReactNode;
6+
}
7+
8+
interface State {
9+
hasError: boolean;
10+
error?: Error;
11+
}
12+
13+
class ErrorBoundary extends Component<Props, State> {
14+
public state: State = {
15+
hasError: false
16+
};
17+
18+
public static getDerivedStateFromError(error: Error): State {
19+
return { hasError: true, error };
20+
}
21+
22+
public componentDidCatch(error: Error, errorInfo: ErrorInfo) {
23+
console.error("Uncaught error:", error, errorInfo);
24+
}
25+
26+
public render() {
27+
if (this.state.hasError) {
28+
return (
29+
<div className="flex flex-col items-center justify-center min-h-screen bg-slate-50 p-4 text-center">
30+
<div className="bg-white p-8 rounded-lg shadow-lg max-w-md w-full">
31+
<h1 className="text-2xl font-bold text-red-600 mb-4">Something went wrong</h1>
32+
<p className="text-slate-600 mb-6">
33+
An unexpected error has occurred. Our team has been notified.
34+
</p>
35+
<div className="bg-slate-100 p-4 rounded mb-6 text-left overflow-auto max-h-40 text-xs font-mono text-slate-700">
36+
{this.state.error?.toString()}
37+
</div>
38+
<Button onClick={() => window.location.reload()} className="w-full">
39+
Reload Application
40+
</Button>
41+
</div>
42+
</div>
43+
);
44+
}
45+
46+
return this.props.children;
47+
}
48+
}
49+
50+
export default ErrorBoundary;

src/components/ui/Skeleton.tsx

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import { cn } from "@/lib/utils"
2+
3+
function Skeleton({
4+
className,
5+
...props
6+
}: React.HTMLAttributes<HTMLDivElement>) {
7+
return (
8+
<div
9+
className={cn("animate-pulse rounded-md bg-slate-100/10", className)}
10+
{...props}
11+
/>
12+
)
13+
}
14+
15+
export { Skeleton }

src/components/ui/Toast.tsx

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ const toastVariants = cva(
1212
success: "border-l-4 border-emerald-500",
1313
error: "border-l-4 border-red-500",
1414
warning: "border-l-4 border-amber-500",
15+
info: "border-l-4 border-blue-500",
1516
},
1617
position: {
1718
"top-right": "top-5 right-5",
@@ -41,12 +42,12 @@ export function Toast({ id, message, description, variant, position, duration =
4142
const [isVisible, setIsVisible] = useState(false);
4243

4344
useEffect(() => {
44-
45+
4546
const timer = setTimeout(() => setIsVisible(true), 10);
4647

4748
const dismissTimer = setTimeout(() => {
4849
setIsVisible(false);
49-
setTimeout(() => onDismiss(id), 300);
50+
setTimeout(() => onDismiss(id), 300);
5051
}, duration);
5152

5253
return () => {
@@ -60,6 +61,7 @@ export function Toast({ id, message, description, variant, position, duration =
6061
case 'success': return <CheckCircle className="w-5 h-5 text-emerald-500" />;
6162
case 'error': return <AlertCircle className="w-5 h-5 text-red-500" />;
6263
case 'warning': return <AlertTriangle className="w-5 h-5 text-amber-500" />;
64+
case 'info': return <Info className="w-5 h-5 text-blue-500" />;
6365
default: return <Info className="w-5 h-5 text-blue-500" />;
6466
}
6567
};

src/hooks/useAppointments.ts

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
2+
import { supabase } from '@/lib/supabase';
3+
import { Appointment, AppointmentStatus } from '@/types/app';
4+
import { Database } from '@/types/database.types';
5+
import { useToast } from '@/contexts/ToastContext';
6+
7+
export const useAppointments = (userId: string | undefined, role: 'doctor' | 'patient') => {
8+
const queryClient = useQueryClient();
9+
const { toast } = useToast();
10+
11+
const fetchAppointments = async () => {
12+
if (!userId) return [];
13+
14+
let query = supabase
15+
.from('appointments')
16+
.select(`
17+
*,
18+
profiles:user_id ( full_name, avatar_url ),
19+
doctors:doctor_id ( name, specialty )
20+
`)
21+
.order('appointment_time', { ascending: true });
22+
23+
if (role === 'doctor') {
24+
query = query.eq('doctor_id', userId);
25+
} else {
26+
query = query.eq('user_id', userId);
27+
}
28+
29+
const { data, error } = await query;
30+
if (error) throw error;
31+
return data as Appointment[];
32+
};
33+
34+
const { data: appointments, isLoading, error } = useQuery({
35+
queryKey: ['appointments', userId, role],
36+
queryFn: fetchAppointments,
37+
enabled: !!userId,
38+
staleTime: 1000 * 60 * 5, // 5 minutes
39+
});
40+
41+
const updateStatusMutation = useMutation({
42+
mutationFn: async ({ id, status, appointment_time, reschedule_reason }: { id: number; status: AppointmentStatus; appointment_time?: string, reschedule_reason?: string }) => {
43+
const updateData: Database['public']['Tables']['appointments']['Update'] = { status };
44+
if (appointment_time) updateData.appointment_time = appointment_time;
45+
if (reschedule_reason) updateData.reschedule_reason = reschedule_reason;
46+
47+
const { error } = await (supabase
48+
.from('appointments') as any)
49+
.update(updateData)
50+
.eq('id', id);
51+
52+
if (error) throw error;
53+
},
54+
onMutate: async (newStatus) => {
55+
await queryClient.cancelQueries({ queryKey: ['appointments', userId, role] });
56+
const previousAppointments = queryClient.getQueryData(['appointments', userId, role]);
57+
58+
queryClient.setQueryData(['appointments', userId, role], (old: Appointment[] | undefined) => {
59+
return old ? old.map((apt) => apt.id === newStatus.id ? { ...apt, ...newStatus } : apt) : [];
60+
});
61+
62+
return { previousAppointments };
63+
},
64+
onError: (err, _newStatus, context: any) => {
65+
queryClient.setQueryData(['appointments', userId, role], context.previousAppointments);
66+
toast({ message: "Failed to update appointment", description: err.message || "An error occurred", variant: "error" });
67+
},
68+
onSettled: () => {
69+
queryClient.invalidateQueries({ queryKey: ['appointments', userId, role] });
70+
toast({ message: "Appointment updated successfully", variant: "success" });
71+
},
72+
});
73+
74+
return {
75+
appointments,
76+
isLoading,
77+
error,
78+
updateStatus: updateStatusMutation,
79+
};
80+
};

src/hooks/usePatients.ts

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
import { useQuery } from '@tanstack/react-query';
2+
import { supabase } from '@/lib/supabase';
3+
import { Profile, AppointmentStatus } from '@/types/app';
4+
5+
export interface PatientData extends Profile {
6+
lastVisit: string;
7+
lastSnapshot: any;
8+
status: AppointmentStatus;
9+
}
10+
11+
export const usePatients = (doctorId: string | undefined) => {
12+
const fetchPatients = async () => {
13+
if (!doctorId) return [];
14+
15+
const { data, error } = await supabase
16+
.from('appointments')
17+
.select(`
18+
user_id,
19+
profiles:user_id ( id, full_name, gender, phone ),
20+
health_data_snapshot,
21+
appointment_time,
22+
status
23+
`)
24+
.eq('doctor_id', doctorId)
25+
.order('appointment_time', { ascending: false });
26+
27+
if (error) throw error;
28+
29+
const uniquePatients = new Map<string, PatientData>();
30+
31+
data?.forEach((apt: any) => {
32+
if (apt.user_id && !uniquePatients.has(apt.user_id)) {
33+
34+
const profile = Array.isArray(apt.profiles) ? apt.profiles[0] : apt.profiles;
35+
36+
if (profile) {
37+
uniquePatients.set(apt.user_id, {
38+
...profile,
39+
lastVisit: apt.appointment_time,
40+
lastSnapshot: apt.health_data_snapshot,
41+
status: apt.status
42+
});
43+
}
44+
}
45+
});
46+
47+
return Array.from(uniquePatients.values());
48+
};
49+
50+
return useQuery({
51+
queryKey: ['doctor_patients', doctorId],
52+
queryFn: fetchPatients,
53+
enabled: !!doctorId,
54+
staleTime: 1000 * 60 * 5,
55+
});
56+
};

src/lib/stripe.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
export const stripePromise = Promise.resolve("pk_test_mock_key");
2+
3+
export const createCheckoutSession = async (priceId: string, userId: string) => {
4+
5+
console.log(`Creating checkout session for item ${priceId} user ${userId}`);
6+
7+
8+
await new Promise(resolve => setTimeout(resolve, 1000));
9+
10+
return {
11+
sessionId: "cs_test_mock_session_id",
12+
url: "https://checkout.stripe.com/pay/cs_test_mock"
13+
};
14+
};

0 commit comments

Comments
 (0)