diff --git a/README.md b/README.md new file mode 100644 index 0000000..6ff1e2a --- /dev/null +++ b/README.md @@ -0,0 +1,31 @@ +# MyChart + +Thin-slice Django demo for a future multi-user video calling app. + +## What this currently does + +- Renders a lobby page where a user can enter a display name and room name. +- Redirects users into a shareable room URL. +- Shows the selected room and display name on the room page. +- Provides a visible placeholder for the future video stream area. + +## What this does not do yet + +- No Algora token integration yet. +- No WebRTC / real-time media transport. +- No presence tracking or multi-user synchronization. +- No production deployment or auth model. + +## Local development + +1. Create a Python environment. +2. Install Django 4.x. +3. Run `python manage.py migrate`. +4. Run `python manage.py runserver`. + +## Recommended next thin slices + +1. Add a server-side endpoint that issues Algora-compatible room or session tokens. +2. Connect `static/js/stream.js` to actual room join/presence logic. +3. Replace the placeholder video tile with a real WebRTC client implementation. +4. Add tests for token issuance, room authorization, and presence behavior. diff --git a/base/templates/base/lobby.html b/base/templates/base/lobby.html index a390c32..4399cd7 100644 --- a/base/templates/base/lobby.html +++ b/base/templates/base/lobby.html @@ -5,10 +5,22 @@
-
+

Welcome to MyChart

-

A group calling application for you

+

Create a shareable room link for a lightweight multi-user call demo.

+ +
+ + + + + + + +
+ +

Thin slice only: this routes people into a shared room URL and surfaces room/member identity in the UI, but it does not implement real-time media yet.

{% endblock content %} \ No newline at end of file diff --git a/base/templates/base/main.html b/base/templates/base/main.html index 95642cb..6217c0b 100644 --- a/base/templates/base/main.html +++ b/base/templates/base/main.html @@ -1,19 +1,15 @@ - {% load static %} - + - - + MyChart Demo + - + -

Main Template

- {% block content %} - - {% endblock content %} + {% block content %}{% endblock content %} \ No newline at end of file diff --git a/base/templates/base/room.html b/base/templates/base/room.html index 2061dec..0a8abe9 100644 --- a/base/templates/base/room.html +++ b/base/templates/base/room.html @@ -1,18 +1,35 @@ {% extends 'base/main.html' %} {% block content %} +
+
+
+

Demo room

+

{{ room_name }}

+

Signed in as {{ display_name }}

+
+ Back to lobby +
-
-
-

Room Name:

+
+
+ Room slug + {{ room_name }} +
+
+ Invite URL + {{ request.build_absolute_uri }} +
-
My Name
-
-
+
{{ display_name }}
+
+

Camera stream placeholder

+ Next step: wire this room into Algora/WebRTC token issuance and peer presence. +
+
- {% endblock content %} diff --git a/base/tests.py b/base/tests.py index 7ce503c..327b618 100644 --- a/base/tests.py +++ b/base/tests.py @@ -1,3 +1,26 @@ from django.test import TestCase +from django.urls import reverse -# Create your tests here. + +class LobbyFlowTests(TestCase): + def test_lobby_page_renders_join_form(self): + response = self.client.get(reverse('lobby')) + + self.assertEqual(response.status_code, 200) + self.assertContains(response, 'Join room') + self.assertContains(response, 'Display name') + self.assertContains(response, 'Room name') + + def test_lobby_redirects_to_room_when_room_is_provided(self): + response = self.client.get(reverse('lobby'), {'name': 'Alice', 'room': 'demo-room'}) + + self.assertEqual(response.status_code, 302) + self.assertEqual(response.url, '/room/?room=demo-room&name=Alice') + + def test_room_page_displays_room_and_name_context(self): + response = self.client.get(reverse('room'), {'name': 'Alice', 'room': 'demo-room'}) + + self.assertEqual(response.status_code, 200) + self.assertContains(response, 'demo-room') + self.assertContains(response, 'Alice') + self.assertContains(response, 'Camera stream placeholder') diff --git a/base/urls.py b/base/urls.py index eee9bea..423f744 100644 --- a/base/urls.py +++ b/base/urls.py @@ -1,7 +1,8 @@ -from django.urls import path -from . import views +from django.urls import path + +from . import views urlpatterns = [ - path("", views.lobby), - path("room/", views.room), + path('', views.lobby, name='lobby'), + path('room/', views.room, name='room'), ] diff --git a/base/views.py b/base/views.py index bf673c6..6ffe213 100644 --- a/base/views.py +++ b/base/views.py @@ -1,7 +1,24 @@ -from django.shortcuts import render +from django.shortcuts import redirect, render + def lobby(request): + room_name = request.GET.get('room', '').strip() + display_name = request.GET.get('name', '').strip() + + if room_name: + query = f"?room={room_name}" + if display_name: + query += f"&name={display_name}" + return redirect(f"/room/{query}") + return render(request, 'base/lobby.html') + def room(request): - return render(request, 'base/room.html') + room_name = request.GET.get('room', '').strip() or 'demo-room' + display_name = request.GET.get('name', '').strip() or 'Guest' + context = { + 'room_name': room_name, + 'display_name': display_name, + } + return render(request, 'base/room.html', context) diff --git a/static/styles/main.css b/static/styles/main.css index 698e4b0..bc9036e 100644 --- a/static/styles/main.css +++ b/static/styles/main.css @@ -1,47 +1,202 @@ -:root { - --shadow:0 4px 6px -1px rgb(0,0,0,0.1), 0 2px 4px -1px rgb(0,0,0,0,06); +:root { + --shadow: 0 10px 30px rgba(15, 23, 42, 0.12); + --bg: #e8e9ef; + --surface: #ffffff; + --surface-muted: #f8fafc; + --text: #0f172a; + --text-muted: #475569; + --primary: #4b5fac; + --primary-dark: #33468c; + --border: #cbd5e1; } +* { + box-sizing: border-box; +} body { - background-color: rgba(232,233,239,1); + margin: 0; + min-height: 100vh; + font-family: Arial, sans-serif; + color: var(--text); + background: linear-gradient(180deg, #eef2ff 0%, var(--bg) 100%); +} + +main { + width: min(960px, calc(100% - 32px)); + margin: 0 auto; + padding: 32px 0 48px; } #logo { display: block; width: 100px; - margin: 0 auto; + margin: 0 auto 16px; } #form-container { - width: 400px; + width: min(460px, 100%); + box-shadow: var(--shadow); + background-color: var(--surface); + padding: 32px; + border-radius: 16px; + margin: 6vh auto 0; +} + +.hero-copy { + text-align: center; + margin-bottom: 24px; +} + +.hero-copy h1 { + margin-bottom: 8px; +} + +.hero-copy p, +.hint, +.room-header p, +.meta-label, +code, +small { + color: var(--text-muted); +} + +#join-form { + display: grid; + gap: 12px; +} + +label { + font-size: 0.95rem; + font-weight: 600; +} + +input { + width: 100%; + padding: 12px 14px; + border: 1px solid var(--border); + border-radius: 10px; + font-size: 1rem; +} + +button, +.secondary-link { + display: inline-flex; + justify-content: center; + align-items: center; + border-radius: 10px; + padding: 12px 16px; + font-weight: 600; + text-decoration: none; +} + +button { + border: 0; + cursor: pointer; + background: var(--primary); + color: white; +} + +button:hover { + background: var(--primary-dark); +} + +.secondary-link { + border: 1px solid var(--border); + color: var(--text); + background: var(--surface); +} + +.hint { + margin-top: 18px; + font-size: 0.95rem; +} + +.room-page { + padding-top: 40px; +} + +.room-header, +.room-meta { + display: flex; + gap: 16px; + justify-content: space-between; + align-items: flex-start; + margin-bottom: 20px; + flex-wrap: wrap; +} + +.eyebrow { + text-transform: uppercase; + letter-spacing: 0.08em; + font-size: 0.8rem; + margin-bottom: 8px; +} + +.meta-card { + flex: 1 1 280px; + background: var(--surface); + border: 1px solid var(--border); + border-radius: 14px; + padding: 16px; box-shadow: var(--shadow); - background-color: #fff; - padding: 30px; - border-radius: 5px; - position: fixed; - top: 50%; - left: 50%; - transform: translate(-50%, -50%); +} + +.meta-label { + display: block; + font-size: 0.8rem; + text-transform: uppercase; + letter-spacing: 0.05em; + margin-bottom: 6px; +} + +code { + display: block; + white-space: normal; + word-break: break-all; } #video-streams { display: flex; flex-wrap: wrap; - height: 85vh; - width: 75%; - margin: 0 auto; + gap: 16px; } .video-container { flex-basis: 500px; flex-grow: 1; - max-height: 100%; - min-height: 350px; + min-height: 350px; + border: 1px solid var(--primary); + border-radius: 16px; + overflow: hidden; + background-color: #c6cadb; + box-shadow: var(--shadow); +} - border: 1px solid rgb(75, 95, 172, 1); - border-radius: 5px; - margin: 2px; - background-color: rgba(198,202,219,1); +.username-wrapper { + padding: 12px 16px; + background: rgba(255, 255, 255, 0.9); + border-bottom: 1px solid var(--border); +} -} \ No newline at end of file +.video-player { + min-height: 280px; +} + +.placeholder-state { + display: grid; + place-items: center; + text-align: center; + padding: 32px; + background: linear-gradient(135deg, rgba(75, 95, 172, 0.12), rgba(255, 255, 255, 0.9)); +} + +@media (max-width: 640px) { + #form-container { + padding: 24px; + } + + main { + width: min(100%, calc(100% - 24px)); + } +}