feat: home page with MUI components

- Rewrote frontend from JSX to TypeScript (TSX)
- AppLayout with MUI AppBar, ThemeProvider, CssBaseline
- HomePage at / with dashboard metrics and recent updates
- MetricsPanel with 4 metric cards (total, interviews, offers, rejection rate)
- UpdateCard for each job update with status chip
- LoadingSkeleton during data fetch
- Error Alert on API failure
- Create New Job Application button navigating to /applications/new
- Vite proxy for /api → backend on :8000
- useDashboardData custom hook with concurrent fetch

Closes #2
This commit is contained in:
Marko (Hermes Implementer)
2026-05-26 04:38:49 +00:00
parent b91b7c364e
commit 811045daca
32 changed files with 2083 additions and 34 deletions
+67
View File
@@ -0,0 +1,67 @@
from django.db.models import Count, Q
from rest_framework import permissions, viewsets
from rest_framework.decorators import action
from rest_framework.request import Request
from rest_framework.response import Response
from .models import JobApplication, JobUpdate
from .serializers import (
DashboardMetricsSerializer,
JobApplicationSerializer,
JobUpdateSerializer,
)
class JobApplicationViewSet(viewsets.ModelViewSet):
queryset = JobApplication.objects.all().prefetch_related("updates")
serializer_class = JobApplicationSerializer
permission_classes = [permissions.AllowAny]
class JobUpdateViewSet(viewsets.ModelViewSet):
queryset = JobUpdate.objects.select_related("job_application").all()
serializer_class = JobUpdateSerializer
permission_classes = [permissions.AllowAny]
@action(detail=False, methods=["get"])
def latest(self, request: Request) -> Response:
"""Return the 3 most recent job updates with job info and metrics."""
updates = (
self.get_queryset()
.select_related("job_application")
.order_by("-created_at")[:3]
)
serializer = self.get_serializer(updates, many=True)
return Response(serializer.data)
@action(detail=False, methods=["get"])
def metrics(self, request: Request) -> Response:
"""Return dashboard metrics: total apps, status breakdown, interview/offer counts."""
total = JobApplication.objects.count()
status_counts = dict(
JobApplication.objects.values("status")
.annotate(count=Count("id"))
.values_list("status", "count")
)
interview_count = JobApplication.objects.filter(
Q(status="INTERVIEW") | Q(status="OFFER")
).count()
offer_count = JobApplication.objects.filter(status="OFFER").count()
rejection_rate = (
round(
status_counts.get("REJECTED", 0) / total * 100, 1
)
if total > 0
else 0.0
)
serializer = DashboardMetricsSerializer(
instance={
"total_applications": total,
"status_breakdown": status_counts,
"interviews_count": interview_count,
"offers_count": offer_count,
"rejection_rate": rejection_rate,
}
)
return Response(serializer.data)