Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
efcaffd
Meeting Code
lenguyenduyphuc May 28, 2025
cb5a4bf
Create README.md
XuanGiaHanNguyen May 28, 2025
a6f0f6c
Update README.md
XuanGiaHanNguyen May 28, 2025
700a50e
Update README.md
XuanGiaHanNguyen May 29, 2025
7b9c140
Updated
lenguyenduyphuc Jun 4, 2025
cb2a128
Updated and cleaned the backend
lenguyenduyphuc Jun 4, 2025
8bff0ac
Payment + My Meetings
XuanGiaHanNguyen Jun 5, 2025
f5ad52c
Finialize code DuyPhuc
lenguyenduyphuc Jun 5, 2025
6a5b5f7
Passed pre commit
lenguyenduyphuc Jun 5, 2025
9311c44
Pass CI/CD
lenguyenduyphuc Jun 5, 2025
f2dcb6b
Finalize
lenguyenduyphuc Jun 5, 2025
9967390
payment + my schedule features (passed pre-commit checks)
XuanGiaHanNguyen Jun 5, 2025
778a846
fixed requirement.txt
XuanGiaHanNguyen Jun 5, 2025
cf98e94
supabase issues
XuanGiaHanNguyen Jun 5, 2025
7b82241
changed supabase requirement
XuanGiaHanNguyen Jun 5, 2025
f45daf2
supabase version issues
XuanGiaHanNguyen Jun 5, 2025
fac5e57
fixed reuqirement.txt
XuanGiaHanNguyen Jun 5, 2025
51fd814
.
XuanGiaHanNguyen Jun 5, 2025
9487b56
.
XuanGiaHanNguyen Jun 5, 2025
f6cd3bb
.
XuanGiaHanNguyen Jun 5, 2025
87cc751
..
XuanGiaHanNguyen Jun 5, 2025
2b95cfc
...
XuanGiaHanNguyen Jun 5, 2025
faa7bcb
....
XuanGiaHanNguyen Jun 5, 2025
496a03e
Merging Phuc's booking meeting features with Han's payment + schedule…
XuanGiaHanNguyen Jun 8, 2025
100797b
Fixed frontend and backend
XuanGiaHanNguyen Jun 8, 2025
628aed9
security issues still unresolved
XuanGiaHanNguyen Jun 8, 2025
3323884
.
XuanGiaHanNguyen Jun 8, 2025
f626667
..
XuanGiaHanNguyen Jun 8, 2025
6be1456
Remove .env file from tracking
XuanGiaHanNguyen Jun 8, 2025
9eedc47
removed env file
XuanGiaHanNguyen Jun 8, 2025
bd4212e
pre-commit testing again
XuanGiaHanNguyen Jun 8, 2025
4df7d6f
macbook is not case-sensitive, hence caused mixup in naming
XuanGiaHanNguyen Jun 8, 2025
169c88d
Stripe issue resolved
XuanGiaHanNguyen Jun 9, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
101 changes: 101 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
# Toast Tutor 🍞📚

<div align="center">
<img width="1280" alt="Screenshot 2025-05-28 at 9 49 06 PM" src="https://github.com/user-attachments/assets/22bcb795-5c01-4e10-a853-26e29d4048eb" />
</div>



A web application that connects tutors with students based on personalized learning needs, availability, and teaching preferences.

## ✨ Features

- **Authentication**: Secure login with role-based access for students and tutors
- **Smart Matching**: Intelligent tutor-student pairing based on subject, level, timing, and teaching style
- **Customizable Profiles**: Detailed profiles for both students and tutors with preferences and expertise
- **Booking System**: Easy session scheduling with flexible timing and subject selection
- **Payment Integration**: Secure Stripe payment processing with automated billing
- **Rating System**: Comprehensive feedback and review system with performance metrics
- **Tutor Dashboard**: Session management, earnings tracking, and student progress monitoring
- **Real-time Notifications**: Toast notifications for bookings, messages, and updates

## 🚀 Setup

**Prerequisites**: Python 3.8+, Node.js 14+, Redis, Supabase account, Stripe account

1. **Clone and Install**
```bash
git clone https://github.com/ttrang87/toast-tutor.git
cd toast-tutor

# Backend
cd backend
python -m venv venv
source venv/bin/activate
pip install -r requirements.txt

# Frontend
cd ../frontend
npm install
```

2. **Environment Setup**
```bash
# Backend .env
SUPABASE_URL=your_supabase_url
SUPABASE_KEY=your_supabase_key
STRIPE_SECRET_KEY=your_stripe_secret
REDIS_URL=redis://localhost:6379

# Frontend .env.local
REACT_APP_SUPABASE_URL=your_supabase_url
REACT_APP_SUPABASE_ANON_KEY=your_supabase_key
REACT_APP_STRIPE_PUBLISHABLE_KEY=your_stripe_public_key
```

3. **Run**
```bash
# Start Redis
redis-server

# Backend
cd backend && python manage.py runserver

# Frontend
cd frontend && npm start
```

Access: Frontend at `http://localhost:3000`, API at `http://localhost:8000`

## 🏗️ Tech Stack

**Frontend**: React.js + Tailwind CSS
**Backend**: Django (Python) + Supabase
**Database**: PostgreSQL (via Supabase)
**Payments**: Stripe
**Caching**: Redis
**Notifications**: Toast

## 📱 Usage

**Students**: Create account → Browse tutors → Book sessions → Make payment → Attend & rate
**Tutors**: Create profile → Set availability → Receive bookings → Track earnings → Manage sessions

## 🔧 API

Key endpoints: `/api/auth/`, `/api/users/`, `/api/tutors/`, `/api/bookings/`, `/api/payments/`, `/api/reviews/`

Authentication: Include JWT token in Authorization header: `Bearer <token>`

## 🤝 Contributing

1. Fork the repo
2. Create feature branch (`git checkout -b feature/amazing-feature`)
3. Commit changes (`git commit -m 'Add amazing feature'`)
4. Push to branch (`git push origin feature/amazing-feature`)
5. Open Pull Request

## 📄 License

MIT License - see [LICENSE.md](LICENSE.md)

6 changes: 5 additions & 1 deletion backend/backend/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,10 @@
"django.middleware.clickjacking.XFrameOptionsMiddleware",
]

CSRF_TRUSTED_ORIGINS = [
"http://localhost:5173",
]

CORS_ALLOWED_ORIGINS = [
"http://localhost:5173", # If using Vite
"http://127.0.0.1:8000", # Alternative localhost format
Expand Down Expand Up @@ -185,7 +189,7 @@
# Stripe
STRIPE_PUBLIC_KEY = os.getenv("PUBLIC_KEY")
STRIPE_SECRET_KEY = os.getenv("SECRET_KEY")
STRIPE_WEBHOOK_SECRET = ""
STRIPE_WEBHOOK_SECRET = os.getenv("WEBHOOK_SECRET")


# EMAIL_BACKEND = 'django.core.mail.backends.smtp.EmailBackend'
Expand Down
Binary file modified backend/requirements.txt
Binary file not shown.
171 changes: 171 additions & 0 deletions backend/toast_tutor/controller/meeting.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,171 @@
from django.utils import timezone
from django.shortcuts import get_object_or_404
from rest_framework.decorators import api_view
from rest_framework.response import Response
from rest_framework import status

from ..serializers import MeetingSerializer
from django.db.models import Q
from ..models import Meeting, User


# pk = primary key
@api_view(["GET"])
def get_meeting(request, pk):
meeting = get_object_or_404(Meeting, pk=pk)
# Pass request context to serializer
return Response(
MeetingSerializer(meeting, context={"request": request}).data, status=status.HTTP_200_OK
)


@api_view(["GET"])
def get_meetings(request):
meetings = Meeting.objects.all().order_by("start_time")
# Pass request context to serializer
return Response(
MeetingSerializer(meetings, many=True, context={"request": request}).data, status=200
)


@api_view(["GET"])
def get_meetings_by_tutor(request, tutor_id):
meetings = Meeting.objects.filter(organizer_id=tutor_id, status="scheduled").order_by(
"start_time"
)
# Pass request context to serializer
return Response(
MeetingSerializer(meetings, many=True, context={"request": request}).data, status=200
)


@api_view(["POST"])
def create_meeting(request):
serializer = MeetingSerializer(data=request.data, context={"request": request})
if serializer.is_valid():
meeting = serializer.save()
return Response(
MeetingSerializer(meeting, context={"request": request}).data,
status=status.HTTP_201_CREATED,
)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)


@api_view(["PATCH"])
def update_meeting(request, pk):
meeting = get_object_or_404(Meeting, pk=pk)
serializer = MeetingSerializer(
meeting, data=request.data, partial=True, context={"request": request}
)
serializer.is_valid(raise_exception=True)
serializer.save()
return Response(serializer.data, status=status.HTTP_200_OK)


@api_view(["DELETE"])
def delete_meeting(request, pk):
meeting = get_object_or_404(Meeting, pk=pk)
meeting.delete()
return Response(status=status.HTTP_204_NO_CONTENT)


@api_view(["POST"])
def book_meeting(request, pk):
meeting = get_object_or_404(Meeting, pk=pk, status="scheduled")
student_id = request.data.get("student")
if not student_id:
return Response({"detail": "student id required"}, status=status.HTTP_400_BAD_REQUEST)
if meeting.organizer_id == int(student_id):
return Response(
{"detail": "organizer cannot book own meeting"}, status=status.HTTP_400_BAD_REQUEST
)

meeting.set_pending(student_id)
# Pass request context to serializer - this was missing!
return Response(
MeetingSerializer(meeting, context={"request": request}).data, status=status.HTTP_200_OK
)


@api_view(["POST"])
def confirm_payment(request, pk):

meeting = get_object_or_404(Meeting, pk=pk, status="pending")

if meeting.payment_expires_at and timezone.now() > meeting.payment_expires_at:
meeting.student = None
meeting.status = "scheduled"
meeting.payment_expires_at = None
meeting.save(update_fields=["student", "status", "payment_expires_at"])
return Response({"detail": "payment window expired"}, status=status.HTTP_400_BAD_REQUEST)

# if confirmed payment
meeting.status = "booked"
meeting.payment_expires_at = None
meeting.save(update_fields=["status", "payment_expires_at"])
# Pass request context to serializer
return Response(
MeetingSerializer(meeting, context={"request": request}).data, status=status.HTTP_200_OK
)


@api_view(["POST"])
def cancel_payment(request, pk):
meeting = get_object_or_404(Meeting, pk=pk)
if meeting.status != "pending":
return Response({"detail": "nothing to cancel"}, status=status.HTTP_400_BAD_REQUEST)

meeting.student = None
meeting.status = "scheduled"
meeting.payment_expires_at = None
# Note: google_event_id and google_meet_link are preserved when canceling
meeting.save(update_fields=["student", "status", "payment_expires_at"])
# Pass request context to serializer
return Response(
MeetingSerializer(meeting, context={"request": request}).data, status=status.HTTP_200_OK
)


@api_view(["GET"])
def get_user_meetings(request, user_id):
"""
Fetch all meetings for a specific user (as organizer or student)
"""
try:
user = User.objects.get(id=user_id)
except User.DoesNotExist:
return Response({"error": "User not found"}, status=status.HTTP_404_NOT_FOUND)

# Get meetings where user is either organizer or student
meetings = Meeting.objects.filter(Q(organizer=user) | Q(student=user)).order_by("start_time")

# Pass context with request and the specific user
serializer = MeetingSerializer(meetings, many=True, context={"request": request, "user": user})
return Response(serializer.data, status=status.HTTP_200_OK)


@api_view(["GET"])
def get_user_meetings_by_status(request, user_id, meeting_status):
"""
Fetch meetings for a specific user filtered by status
"""
try:
user = User.objects.get(id=user_id)
except User.DoesNotExist:
return Response({"error": "User not found"}, status=status.HTTP_404_NOT_FOUND)

# Validate status
valid_statuses = ["scheduled", "booked", "completed"]
if meeting_status not in valid_statuses:
return Response(
{"error": "Invalid status. Valid options: scheduled, booked, completed"},
status=status.HTTP_400_BAD_REQUEST,
)

meetings = Meeting.objects.filter(
Q(organizer=user) | Q(student=user), status=meeting_status
).order_by("start_time")

# Pass context with request and the specific user
serializer = MeetingSerializer(meetings, many=True, context={"request": request, "user": user})
return Response(serializer.data, status=status.HTTP_200_OK)
Loading