Compare commits
6 Commits
49114c30a6
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 6c04f690d3 | |||
| 02fe1cffaf | |||
| 9afb8b2de1 | |||
| 156ecf75d3 | |||
| 3b84e1effc | |||
| a40721aba4 |
@@ -9,5 +9,8 @@ __pycache__/
|
|||||||
.ruff_cache/
|
.ruff_cache/
|
||||||
.uvicorn.pid
|
.uvicorn.pid
|
||||||
|
|
||||||
docs/
|
docs/images/
|
||||||
|
docs/screenshots/
|
||||||
|
docs/*.pdf
|
||||||
|
docs/presentation.md
|
||||||
README.md
|
README.md
|
||||||
|
|||||||
@@ -6,9 +6,11 @@ ENV PYTHONDONTWRITEBYTECODE=1 \
|
|||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
COPY requirements.txt .
|
COPY requirements.txt .
|
||||||
RUN pip install --no-cache-dir -r requirements.txt
|
RUN pip install --no-cache-dir --upgrade pip setuptools wheel -i https://package-mirror.liara.ir/repository/pypi/simple \
|
||||||
|
&& pip install --no-cache-dir -r requirements.txt -i https://package-mirror.liara.ir/repository/pypi/simple
|
||||||
|
|
||||||
COPY app ./app
|
COPY app ./app
|
||||||
|
COPY docs/slides ./docs/slides
|
||||||
|
|
||||||
EXPOSE 8000
|
EXPOSE 8000
|
||||||
|
|
||||||
|
|||||||
14
app/main.py
14
app/main.py
@@ -1,7 +1,7 @@
|
|||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
from fastapi import FastAPI
|
from fastapi import FastAPI
|
||||||
from fastapi.responses import FileResponse
|
from fastapi.responses import FileResponse, RedirectResponse
|
||||||
from fastapi.staticfiles import StaticFiles
|
from fastapi.staticfiles import StaticFiles
|
||||||
|
|
||||||
from app.websocket.chat import router as chat_router
|
from app.websocket.chat import router as chat_router
|
||||||
@@ -9,6 +9,7 @@ from app.websocket.manager import manager
|
|||||||
|
|
||||||
BASE_DIR = Path(__file__).resolve().parent
|
BASE_DIR = Path(__file__).resolve().parent
|
||||||
STATIC_DIR = BASE_DIR / "static"
|
STATIC_DIR = BASE_DIR / "static"
|
||||||
|
SLIDES_DIR = BASE_DIR.parent / "docs" / "slides"
|
||||||
|
|
||||||
app = FastAPI(title="Simple WebSocket Chatroom")
|
app = FastAPI(title="Simple WebSocket Chatroom")
|
||||||
|
|
||||||
@@ -20,6 +21,17 @@ app.include_router(chat_router)
|
|||||||
app.mount("/static", StaticFiles(directory=STATIC_DIR), name="static")
|
app.mount("/static", StaticFiles(directory=STATIC_DIR), name="static")
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/slides", include_in_schema=False)
|
||||||
|
async def slides_redirect():
|
||||||
|
"""Redirect to the static slides directory so relative asset paths work."""
|
||||||
|
return RedirectResponse(url="/slides/")
|
||||||
|
|
||||||
|
|
||||||
|
# Serves the presentation and its relative assets.
|
||||||
|
# This keeps docs/slides/index.html openable directly from the file system too.
|
||||||
|
app.mount("/slides", StaticFiles(directory=SLIDES_DIR, html=True), name="slides")
|
||||||
|
|
||||||
|
|
||||||
@app.get("/")
|
@app.get("/")
|
||||||
async def home():
|
async def home():
|
||||||
"""Serve the chatroom GUI."""
|
"""Serve the chatroom GUI."""
|
||||||
|
|||||||
BIN
docs/slides/Vazirmatn[wght].woff2
Normal file
BIN
docs/slides/Vazirmatn[wght].woff2
Normal file
Binary file not shown.
@@ -119,6 +119,326 @@
|
|||||||
<aside class="notes">Explain the code structure: FastAPI serves the frontend, the WebSocket endpoint accepts connections, and the manager stores active clients by room before broadcasting messages.</aside>
|
<aside class="notes">Explain the code structure: FastAPI serves the frontend, the WebSocket endpoint accepts connections, and the manager stores active clients by room before broadcasting messages.</aside>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
<section class="slide code-slide" data-title="Backend Router">
|
||||||
|
<h2>Backend Codes</h2>
|
||||||
|
<p class="subtitle">WebSocket router setup in FastAPI.</p>
|
||||||
|
<div class="code-window">
|
||||||
|
<div class="code-titlebar"><span></span><span></span><span></span><strong>app/websocket/chat.py</strong></div>
|
||||||
|
<pre><code><span class="line"><span class="kw">import</span> json</span>
|
||||||
|
<span class="line"><span class="kw">from</span> datetime <span class="kw">import</span> datetime, timezone</span>
|
||||||
|
<span class="line"></span>
|
||||||
|
<span class="line"><span class="kw">from</span> fastapi <span class="kw">import</span> APIRouter, WebSocket, WebSocketDisconnect</span>
|
||||||
|
<span class="line"></span>
|
||||||
|
<span class="line"><span class="kw">from</span> app.websocket.manager <span class="kw">import</span> manager</span>
|
||||||
|
<span class="line"></span>
|
||||||
|
<span class="line">router = APIRouter()</span></code></pre>
|
||||||
|
</div>
|
||||||
|
<aside class="notes">You can start by explaining that this module owns the WebSocket endpoint and delegates connection storage to the shared manager.</aside>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="slide code-slide" data-title="Backend Payload">
|
||||||
|
<h2>Message Payload Shape</h2>
|
||||||
|
<p class="subtitle">Every server message uses one small JSON structure.</p>
|
||||||
|
<div class="code-window">
|
||||||
|
<div class="code-titlebar"><span></span><span></span><span></span><strong>app/websocket/chat.py</strong></div>
|
||||||
|
<pre><code><span class="line"><span class="kw">def</span> <span class="fn">now_iso</span>() -> str:</span>
|
||||||
|
<span class="line"> <span class="kw">return</span> datetime.now(timezone.utc).isoformat()</span>
|
||||||
|
<span class="line"></span>
|
||||||
|
<span class="line"><span class="kw">def</span> <span class="fn">event</span>(event_type, room_id, client_id, content):</span>
|
||||||
|
<span class="line"> <span class="kw">return</span> {</span>
|
||||||
|
<span class="line"> <span class="str">"type"</span>: event_type,</span>
|
||||||
|
<span class="line"> <span class="str">"room_id"</span>: room_id,</span>
|
||||||
|
<span class="line"> <span class="str">"client_id"</span>: client_id,</span>
|
||||||
|
<span class="line"> <span class="str">"content"</span>: content,</span>
|
||||||
|
<span class="line"> <span class="str">"timestamp"</span>: now_iso(),</span>
|
||||||
|
<span class="line"> }</span></code></pre>
|
||||||
|
</div>
|
||||||
|
<aside class="notes">This slide shows the server-to-client contract: message type, room, sender, content, and timestamp.</aside>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="slide code-slide" data-title="Backend Endpoint">
|
||||||
|
<h2>WebSocket Endpoint</h2>
|
||||||
|
<p class="subtitle">The route parameters select the room and the user identity.</p>
|
||||||
|
<div class="code-window">
|
||||||
|
<div class="code-titlebar"><span></span><span></span><span></span><strong>app/websocket/chat.py</strong></div>
|
||||||
|
<pre><code><span class="line">@router.websocket(<span class="str">"/ws/{room_id}/{client_id}"</span>)</span>
|
||||||
|
<span class="line"><span class="kw">async def</span> <span class="fn">websocket_endpoint</span>(</span>
|
||||||
|
<span class="line"> websocket: WebSocket,</span>
|
||||||
|
<span class="line"> room_id: str,</span>
|
||||||
|
<span class="line"> client_id: str,</span>
|
||||||
|
<span class="line">):</span>
|
||||||
|
<span class="line"> room_id = clean_value(room_id, <span class="str">"general"</span>)</span>
|
||||||
|
<span class="line"> client_id = clean_value(client_id, <span class="str">"anonymous"</span>)</span>
|
||||||
|
<span class="line"></span>
|
||||||
|
<span class="line"> <span class="kw">await</span> manager.connect(websocket, room_id, client_id)</span></code></pre>
|
||||||
|
</div>
|
||||||
|
<aside class="notes">The browser URL maps directly to this route. FastAPI receives the HTTP Upgrade request and the manager accepts the socket.</aside>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="slide code-slide" data-title="Backend Presence">
|
||||||
|
<h2>Join and Presence Events</h2>
|
||||||
|
<p class="subtitle">After connection, the server informs the room and refreshes online users.</p>
|
||||||
|
<div class="code-window">
|
||||||
|
<div class="code-titlebar"><span></span><span></span><span></span><strong>app/websocket/chat.py</strong></div>
|
||||||
|
<pre><code><span class="line"><span class="kw">await</span> manager.broadcast_json(</span>
|
||||||
|
<span class="line"> event(<span class="str">"system"</span>, room_id, <span class="str">"system"</span>, <span class="str">f"{client_id} joined {room_id}."</span>),</span>
|
||||||
|
<span class="line"> room_id,</span>
|
||||||
|
<span class="line">)</span>
|
||||||
|
<span class="line"></span>
|
||||||
|
<span class="line"><span class="kw">await</span> manager.broadcast_json({</span>
|
||||||
|
<span class="line"> <span class="str">"type"</span>: <span class="str">"presence"</span>,</span>
|
||||||
|
<span class="line"> <span class="str">"users"</span>: manager.room_users(room_id),</span>
|
||||||
|
<span class="line"> <span class="str">"timestamp"</span>: now_iso(),</span>
|
||||||
|
<span class="line">}, room_id)</span></code></pre>
|
||||||
|
</div>
|
||||||
|
<aside class="notes">Presence is separate from normal chat messages, so the frontend can update the sidebar without adding a chat bubble.</aside>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="slide code-slide" data-title="Backend Receive Loop">
|
||||||
|
<h2>Receive Loop and Broadcast</h2>
|
||||||
|
<p class="subtitle">The socket stays open and every valid message is broadcast to the room.</p>
|
||||||
|
<div class="code-window">
|
||||||
|
<div class="code-titlebar"><span></span><span></span><span></span><strong>app/websocket/chat.py</strong></div>
|
||||||
|
<pre><code><span class="line"><span class="kw">while</span> True:</span>
|
||||||
|
<span class="line"> raw_data = <span class="kw">await</span> websocket.receive_text()</span>
|
||||||
|
<span class="line"></span>
|
||||||
|
<span class="line"> <span class="kw">try</span>:</span>
|
||||||
|
<span class="line"> data = json.loads(raw_data)</span>
|
||||||
|
<span class="line"> message = str(data.get(<span class="str">"content"</span>, <span class="str">""</span>)).strip()</span>
|
||||||
|
<span class="line"> <span class="kw">except</span> json.JSONDecodeError:</span>
|
||||||
|
<span class="line"> message = raw_data.strip()</span>
|
||||||
|
<span class="line"></span>
|
||||||
|
<span class="line"> <span class="kw">if</span> message:</span>
|
||||||
|
<span class="line"> <span class="kw">await</span> manager.broadcast_json(event(<span class="str">"message"</span>, room_id, client_id, message), room_id)</span></code></pre>
|
||||||
|
</div>
|
||||||
|
<aside class="notes">This is the main real-time loop: receive text, parse JSON if possible, ignore empty messages, then broadcast.</aside>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="slide code-slide" data-title="WebSocket Disconnect Handling">
|
||||||
|
<h2>Connection Teardown and Presence Update</h2>
|
||||||
|
<p class="subtitle">Handles user disconnects, cleans up the room, and updates presence for remaining clients.</p>
|
||||||
|
|
||||||
|
<div class="code-window">
|
||||||
|
<div class="code-titlebar">
|
||||||
|
<span></span><span></span><span></span>
|
||||||
|
<strong>app/websocket/chat.py</strong>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<pre><code><span class="line"><span class="kw">except</span> WebSocketDisconnect:</span>
|
||||||
|
<span class="line"> manager.disconnect(websocket, room_id)</span>
|
||||||
|
<span class="line"> <span class="kw">await</span> manager.broadcast_json(</span>
|
||||||
|
<span class="line"> event(<span class="str">"system"</span>, room_id, <span class="str">"سیستم"</span>, </span>
|
||||||
|
<span class="line"> f<span class="str">"{client_id} از اتاق {room_id} خارج شد."</span>),</span>
|
||||||
|
<span class="line"> room_id,</span>
|
||||||
|
<span class="line"> )</span>
|
||||||
|
<span class="line"> <span class="kw">await</span> manager.broadcast_json(</span>
|
||||||
|
<span class="line"> {</span>
|
||||||
|
<span class="line"> <span class="str">"type"</span>: <span class="str">"presence"</span>,</span>
|
||||||
|
<span class="line"> <span class="str">"room_id"</span>: room_id,</span>
|
||||||
|
<span class="line"> <span class="str">"client_id"</span>: <span class="str">"سیستم"</span>,</span>
|
||||||
|
<span class="line"> <span class="str">"content"</span>: <span class="str">"presence-updated"</span>,</span>
|
||||||
|
<span class="line"> <span class="str">"users"</span>: manager.room_users(room_id),</span>
|
||||||
|
<span class="line"> <span class="str">"timestamp"</span>: now_iso(),</span>
|
||||||
|
<span class="line"> },</span>
|
||||||
|
<span class="line"> room_id,</span>
|
||||||
|
<span class="line"> )</span></code></pre>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<aside class="notes">
|
||||||
|
This block handles client disconnection: it removes the websocket from the room,
|
||||||
|
broadcasts a system message announcing the departure, and then sends an updated
|
||||||
|
presence payload so all clients can refresh the active user list.
|
||||||
|
</aside>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="slide code-slide" data-title="Backend Connection Manager">
|
||||||
|
<h2>ConnectionManager</h2>
|
||||||
|
<p class="subtitle">Manages WebSocket connections grouped by room and cleans up disconnected clients.</p>
|
||||||
|
|
||||||
|
<div class="code-window">
|
||||||
|
<div class="code-titlebar">
|
||||||
|
<span></span><span></span><span></span>
|
||||||
|
<strong>app/websocket/manager.py</strong>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<pre><code><span class="line"><span class="kw">class</span> <span class="fn">ConnectionManager</span>:</span>
|
||||||
|
<span class="line"> <span class="kw">def</span> <span class="fn">__init__</span>(self):</span>
|
||||||
|
<span class="line"> self.active_connections: dict[str, list[ClientConnection]] = {}</span>
|
||||||
|
<span class="line"></span>
|
||||||
|
<span class="line"> <span class="kw">async def</span> <span class="fn">connect</span>(self, websocket: WebSocket, room_id: str, client_id: str):</span>
|
||||||
|
<span class="line"> <span class="kw">await</span> websocket.accept()</span>
|
||||||
|
<span class="line"> self.active_connections.setdefault(room_id, [])</span>
|
||||||
|
<span class="line"> self.active_connections[room_id].append(ClientConnection(websocket=websocket, client_id=client_id))</span>
|
||||||
|
<span class="line"></span>
|
||||||
|
<span class="line"> <span class="kw">def</span> <span class="fn">disconnect</span>(self, websocket: WebSocket, room_id: str):</span>
|
||||||
|
<span class="line"> <span class="kw">if</span> room_id <span class="kw">not</span> <span class="kw">in</span> self.active_connections: <span class="kw">return</span></span>
|
||||||
|
<span class="line"></span>
|
||||||
|
<span class="line"> self.active_connections[room_id] = [</span>
|
||||||
|
<span class="line"> connection</span>
|
||||||
|
<span class="line"> <span class="kw">for</span> connection <span class="kw">in</span> self.active_connections[room_id] <span class="kw">if</span> connection.websocket <span class="kw">is</span> <span class="kw">not</span> websocket]</span>
|
||||||
|
<span class="line"></span>
|
||||||
|
<span class="line"> <span class="kw">if</span> <span class="kw">not</span> self.active_connections[room_id]:</span>
|
||||||
|
<span class="line"> <span class="kw">del</span> self.active_connections[room_id]</span></code></pre>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<aside class="notes">
|
||||||
|
This slide shows how the ConnectionManager tracks active users per room, handles new WebSocket
|
||||||
|
connections, and removes clients when they disconnect. Empty rooms are automatically cleaned up.
|
||||||
|
</aside>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="slide code-slide" data-title="Backend Broadcast Manager">
|
||||||
|
<h2>Broadcast</h2>
|
||||||
|
<p class="subtitle">Sends messages to all clients in a room and removes failed WebSocket connections safely.</p>
|
||||||
|
|
||||||
|
<div class="code-window">
|
||||||
|
<div class="code-titlebar">
|
||||||
|
<span></span><span></span><span></span>
|
||||||
|
<strong>app/websocket/manager.py</strong>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<pre><code><span class="line"><span class="kw">class</span> <span class="fn">ConnectionManager</span>:</span>
|
||||||
|
<span class="line"></span>
|
||||||
|
<span class="line"> <span class="kw">async def</span> <span class="fn">broadcast_json</span>(self, payload: <span class="kw">dict</span>[str, Any], room_id: str):</span>
|
||||||
|
<span class="line"> <span class="kw">if</span> room_id <span class="kw">not</span> <span class="kw">in</span> self.active_connections:</span>
|
||||||
|
<span class="line"> <span class="kw">return</span></span>
|
||||||
|
<span class="line"></span>
|
||||||
|
<span class="line"> dead_connections: <span class="kw">list</span>[WebSocket] = []</span>
|
||||||
|
<span class="line"></span>
|
||||||
|
<span class="line"> <span class="com"># Iterate over a copy so disconnected clients can be removed safely later.</span></span>
|
||||||
|
<span class="line"> <span class="kw">for</span> connection <span class="kw">in</span> <span class="kw">list</span>(self.active_connections[room_id]):</span>
|
||||||
|
<span class="line"> <span class="kw">try</span>:</span>
|
||||||
|
<span class="line"> <span class="kw">await</span> connection.websocket.send_json(payload)</span>
|
||||||
|
<span class="line"> <span class="kw">except</span> Exception:</span>
|
||||||
|
<span class="line"> dead_connections.append(connection.websocket)</span>
|
||||||
|
<span class="line"></span>
|
||||||
|
<span class="line"> <span class="kw">for</span> websocket <span class="kw">in</span> dead_connections:</span>
|
||||||
|
<span class="line"> self.disconnect(websocket, room_id)</span></code></pre>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<aside class="notes">
|
||||||
|
This method broadcasts a JSON payload to all clients in a room.
|
||||||
|
It safely handles broken connections by collecting failed sockets and removing them after iteration,
|
||||||
|
preventing runtime errors while modifying the connection list.
|
||||||
|
</aside>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="slide code-slide" data-title="Frontend State">
|
||||||
|
<h2>Frontend Codes</h2>
|
||||||
|
<p class="subtitle">browser state and DOM references.</p>
|
||||||
|
<div class="code-window">
|
||||||
|
<div class="code-titlebar"><span></span><span></span><span></span><strong>app/static/app.js</strong></div>
|
||||||
|
<pre><code><span class="line"><span class="kw">const</span> usernameInput = document.querySelector(<span class="str">'#username'</span>);</span>
|
||||||
|
<span class="line"><span class="kw">const</span> roomInput = document.querySelector(<span class="str">'#room'</span>);</span>
|
||||||
|
<span class="line"><span class="kw">const</span> connectBtn = document.querySelector(<span class="str">'#connectBtn'</span>);</span>
|
||||||
|
<span class="line"><span class="kw">const</span> messageForm = document.querySelector(<span class="str">'#messageForm'</span>);</span>
|
||||||
|
<span class="line"><span class="kw">const</span> messages = document.querySelector(<span class="str">'#messages'</span>);</span>
|
||||||
|
<span class="line"><span class="kw">const</span> usersList = document.querySelector(<span class="str">'#usersList'</span>);</span>
|
||||||
|
<span class="line"></span>
|
||||||
|
<span class="line"><span class="kw">let</span> socket = <span class="num">null</span>;</span>
|
||||||
|
<span class="line"><span class="kw">let</span> currentUser = <span class="str">''</span>;</span>
|
||||||
|
<span class="line"><span class="kw">let</span> currentRoom = <span class="str">''</span>;</span></code></pre>
|
||||||
|
</div>
|
||||||
|
<aside class="notes">You can introduce the frontend file by showing that the UI stores one WebSocket object and the current room/user state.</aside>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="slide code-slide" data-title="Frontend URL">
|
||||||
|
<h2>Building the WebSocket URL</h2>
|
||||||
|
<p class="subtitle">The browser automatically switches between ws and wss.</p>
|
||||||
|
<div class="code-window">
|
||||||
|
<div class="code-titlebar"><span></span><span></span><span></span><strong>app/static/app.js</strong></div>
|
||||||
|
<pre><code><span class="line"><span class="kw">function</span> <span class="fn">connect</span>() {</span>
|
||||||
|
<span class="line"> currentUser = safeName(usernameInput.value, <span class="str">'anonymous'</span>);</span>
|
||||||
|
<span class="line"> currentRoom = safeName(roomInput.value, <span class="str">'general'</span>);</span>
|
||||||
|
<span class="line"></span>
|
||||||
|
<span class="line"> <span class="kw">const</span> wsProtocol = window.location.protocol === <span class="str">'https:'</span> ? <span class="str">'wss'</span> : <span class="str">'ws'</span>;</span>
|
||||||
|
<span class="line"> <span class="kw">const</span> room = encodeURIComponent(currentRoom);</span>
|
||||||
|
<span class="line"> <span class="kw">const</span> user = encodeURIComponent(currentUser);</span>
|
||||||
|
<span class="line"> <span class="kw">const</span> wsUrl = <span class="str">`${wsProtocol}://${window.location.host}/ws/${room}/${user}`</span>;</span>
|
||||||
|
<span class="line"></span>
|
||||||
|
<span class="line"> socket = <span class="kw">new</span> WebSocket(wsUrl);</span>
|
||||||
|
<span class="line">}</span></code></pre>
|
||||||
|
</div>
|
||||||
|
<aside class="notes">This is why the same frontend works locally and behind HTTPS on the server.</aside>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="slide code-slide" data-title="Frontend Open Event">
|
||||||
|
<h2>Connection Open Handler</h2>
|
||||||
|
<p class="subtitle">When the socket opens, the UI moves into connected mode.</p>
|
||||||
|
<div class="code-window">
|
||||||
|
<div class="code-titlebar"><span></span><span></span><span></span><strong>app/static/app.js</strong></div>
|
||||||
|
<pre><code><span class="line">socket.addEventListener(<span class="str">'open'</span>, () => {</span>
|
||||||
|
<span class="line"> setConnectedState(<span class="num">true</span>);</span>
|
||||||
|
<span class="line"></span>
|
||||||
|
<span class="line"> roomTitle.textContent = <span class="str">`Room: ${currentRoom}`</span>;</span>
|
||||||
|
<span class="line"> roomSubtitle.textContent = <span class="str">`You are chatting as ${currentUser}.`</span>;</span>
|
||||||
|
<span class="line"></span>
|
||||||
|
<span class="line"> messageInput.focus();</span>
|
||||||
|
<span class="line">});</span></code></pre>
|
||||||
|
</div>
|
||||||
|
<aside class="notes">This slide connects WebSocket state to UI state: buttons, labels, and the message input change after the connection opens.</aside>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="slide code-slide" data-title="Frontend Message Event">
|
||||||
|
<h2>Handling Incoming Messages</h2>
|
||||||
|
<p class="subtitle">Presence updates and chat messages are separated by type.</p>
|
||||||
|
<div class="code-window">
|
||||||
|
<div class="code-titlebar"><span></span><span></span><span></span><strong>app/static/app.js</strong></div>
|
||||||
|
<pre><code><span class="line">socket.addEventListener(<span class="str">'message'</span>, (event) => {</span>
|
||||||
|
<span class="line"> <span class="kw">const</span> payload = JSON.parse(event.data);</span>
|
||||||
|
<span class="line"></span>
|
||||||
|
<span class="line"> <span class="kw">if</span> (payload.type === <span class="str">'presence'</span>) {</span>
|
||||||
|
<span class="line"> updateUsers(payload.users || []);</span>
|
||||||
|
<span class="line"> <span class="kw">return</span>;</span>
|
||||||
|
<span class="line"> }</span>
|
||||||
|
<span class="line"></span>
|
||||||
|
<span class="line"> addMessage(payload);</span>
|
||||||
|
<span class="line">});</span></code></pre>
|
||||||
|
</div>
|
||||||
|
<aside class="notes">The frontend treats presence as sidebar data and normal messages as chat feed data.</aside>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="slide code-slide" data-title="Frontend Render">
|
||||||
|
<h2>Rendering a Chat Message</h2>
|
||||||
|
<p class="subtitle">The browser creates message elements without reloading the page.</p>
|
||||||
|
<div class="code-window">
|
||||||
|
<div class="code-titlebar"><span></span><span></span><span></span><strong>app/static/app.js</strong></div>
|
||||||
|
<pre><code><span class="line"><span class="kw">function</span> <span class="fn">addMessage</span>(payload) {</span>
|
||||||
|
<span class="line"> clearEmptyState();</span>
|
||||||
|
<span class="line"></span>
|
||||||
|
<span class="line"> <span class="kw">const</span> card = document.createElement(<span class="str">'article'</span>);</span>
|
||||||
|
<span class="line"> card.className = <span class="str">'message'</span>;</span>
|
||||||
|
<span class="line"></span>
|
||||||
|
<span class="line"> <span class="kw">if</span> (payload.client_id === currentUser) card.classList.add(<span class="str">'mine'</span>);</span>
|
||||||
|
<span class="line"></span>
|
||||||
|
<span class="line"> content.textContent = payload.content || <span class="str">''</span>;</span>
|
||||||
|
<span class="line"> card.append(meta, content);</span>
|
||||||
|
<span class="line"> messages.appendChild(card);</span>
|
||||||
|
<span class="line"> messages.scrollTop = messages.scrollHeight;</span>
|
||||||
|
<span class="line">}</span></code></pre>
|
||||||
|
</div>
|
||||||
|
<aside class="notes">This demonstrates DOM rendering and auto-scrolling. The UI changes because WebSocket data arrives, not because the page refreshes.</aside>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="slide code-slide" data-title="Frontend Send">
|
||||||
|
<h2>Sending Messages</h2>
|
||||||
|
<p class="subtitle">The form sends JSON only when the socket is open.</p>
|
||||||
|
<div class="code-window">
|
||||||
|
<div class="code-titlebar"><span></span><span></span><span></span><strong>app/static/app.js</strong></div>
|
||||||
|
<pre><code><span class="line">messageForm.addEventListener(<span class="str">'submit'</span>, (event) => {</span>
|
||||||
|
<span class="line"> event.preventDefault();</span>
|
||||||
|
<span class="line"></span>
|
||||||
|
<span class="line"> <span class="kw">const</span> content = messageInput.value.trim();</span>
|
||||||
|
<span class="line"> <span class="kw">if</span> (!content || !socket || socket.readyState !== WebSocket.OPEN) <span class="kw">return</span>;</span>
|
||||||
|
<span class="line"></span>
|
||||||
|
<span class="line"> socket.send(JSON.stringify({ content }));</span>
|
||||||
|
<span class="line"> messageInput.value = <span class="str">''</span>;</span>
|
||||||
|
<span class="line"> messageInput.focus();</span>
|
||||||
|
<span class="line">});</span></code></pre>
|
||||||
|
</div>
|
||||||
|
<aside class="notes">This is the browser-to-server direction. The server receives this JSON and broadcasts the content to the room.</aside>
|
||||||
|
</section>
|
||||||
|
|
||||||
<section class="slide" data-title="Takeaways">
|
<section class="slide" data-title="Takeaways">
|
||||||
<h2>Keynotes and Takeaways</h2>
|
<h2>Keynotes and Takeaways</h2>
|
||||||
<p class="subtitle">The essential points to remember from the project.</p>
|
<p class="subtitle">The essential points to remember from the project.</p>
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
@font-face {
|
@font-face {
|
||||||
font-family: "Vazirmatn";
|
font-family: "Vazirmatn";
|
||||||
src: url("../../app/static/Vazirmatn[wght].woff2") format("woff2");
|
src: url("Vazirmatn[wght].woff2") format("woff2");
|
||||||
font-weight: 100 900;
|
font-weight: 100 900;
|
||||||
font-style: normal;
|
font-style: normal;
|
||||||
font-display: swap;
|
font-display: swap;
|
||||||
@@ -93,7 +93,8 @@ body {
|
|||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
.slide.active.protocol-slide,
|
.slide.active.protocol-slide,
|
||||||
.slide.active.image-slide {
|
.slide.active.image-slide,
|
||||||
|
.slide.active.code-slide {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-rows: auto auto minmax(0, 1fr);
|
grid-template-rows: auto auto minmax(0, 1fr);
|
||||||
}
|
}
|
||||||
@@ -131,7 +132,8 @@ h2 { font-size: clamp(27px, calc(46px * var(--slide-scale)), 66px); }
|
|||||||
letter-spacing: .01em;
|
letter-spacing: .01em;
|
||||||
}
|
}
|
||||||
.protocol-slide .subtitle,
|
.protocol-slide .subtitle,
|
||||||
.image-slide .subtitle {
|
.image-slide .subtitle,
|
||||||
|
.code-slide .subtitle {
|
||||||
margin-bottom: clamp(10px, calc(14px * var(--slide-scale)), 22px);
|
margin-bottom: clamp(10px, calc(14px * var(--slide-scale)), 22px);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -635,6 +637,89 @@ pre {
|
|||||||
}
|
}
|
||||||
.ltr { direction: ltr; text-align: left; }
|
.ltr { direction: ltr; text-align: left; }
|
||||||
|
|
||||||
|
.code-window {
|
||||||
|
min-height: 0;
|
||||||
|
border-radius: clamp(10px, calc(14px * var(--slide-scale)), 18px);
|
||||||
|
background: #0d1117;
|
||||||
|
border: 1px solid rgba(255,255,255,.13);
|
||||||
|
box-shadow: 0 24px 70px rgba(0,0,0,.34), inset 0 1px 0 rgba(255,255,255,.08);
|
||||||
|
overflow: hidden;
|
||||||
|
direction: ltr;
|
||||||
|
display: grid;
|
||||||
|
grid-template-rows: auto minmax(0, 1fr);
|
||||||
|
}
|
||||||
|
.code-titlebar {
|
||||||
|
height: clamp(38px, calc(44px * var(--slide-scale)), 56px);
|
||||||
|
background: linear-gradient(180deg, #1f2430, #151a24);
|
||||||
|
border-bottom: 1px solid rgba(255,255,255,.09);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
padding: 0 clamp(14px, calc(18px * var(--slide-scale)), 26px);
|
||||||
|
}
|
||||||
|
.code-titlebar span {
|
||||||
|
width: clamp(10px, calc(12px * var(--slide-scale)), 15px);
|
||||||
|
height: clamp(10px, calc(12px * var(--slide-scale)), 15px);
|
||||||
|
border-radius: 50%;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
.code-titlebar span:nth-child(1) { background: #ff5f57; }
|
||||||
|
.code-titlebar span:nth-child(2) { background: #febc2e; }
|
||||||
|
.code-titlebar span:nth-child(3) { background: #28c840; }
|
||||||
|
.code-titlebar strong {
|
||||||
|
margin-left: 10px;
|
||||||
|
color: #c9d1d9;
|
||||||
|
font-family: "Cascadia Code", "Consolas", "SFMono-Regular", monospace;
|
||||||
|
font-size: clamp(12px, calc(14px * var(--slide-scale)), 19px);
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
.code-window pre {
|
||||||
|
min-height: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
background:
|
||||||
|
linear-gradient(90deg, rgba(125, 211, 252, .035), transparent 36%),
|
||||||
|
#0d1117;
|
||||||
|
border-radius: 0;
|
||||||
|
box-shadow: none;
|
||||||
|
padding: clamp(14px, calc(20px * var(--slide-scale)), 28px) 0;
|
||||||
|
white-space: pre;
|
||||||
|
font-size: clamp(11px, calc(14px * var(--slide-scale)), 19px);
|
||||||
|
line-height: 0;
|
||||||
|
}
|
||||||
|
.code-window code {
|
||||||
|
display: block;
|
||||||
|
background: transparent;
|
||||||
|
border-radius: 0;
|
||||||
|
color: #d6deeb;
|
||||||
|
padding: 0;
|
||||||
|
font-family: "Cascadia Code", "Consolas", "SFMono-Regular", monospace;
|
||||||
|
direction: ltr;
|
||||||
|
unicode-bidi: normal;
|
||||||
|
}
|
||||||
|
.code-window .line {
|
||||||
|
counter-increment: code-line;
|
||||||
|
display: block;
|
||||||
|
min-height: 1.55em;
|
||||||
|
line-height: 1.45;
|
||||||
|
padding: 0 clamp(18px, calc(28px * var(--slide-scale)), 42px) 0 0;
|
||||||
|
}
|
||||||
|
.code-window code {
|
||||||
|
counter-reset: code-line;
|
||||||
|
}
|
||||||
|
.code-window .line::before {
|
||||||
|
content: counter(code-line);
|
||||||
|
display: inline-block;
|
||||||
|
width: clamp(34px, calc(44px * var(--slide-scale)), 56px);
|
||||||
|
margin-right: clamp(12px, calc(18px * var(--slide-scale)), 26px);
|
||||||
|
color: #6e7681;
|
||||||
|
text-align: right;
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
.code-window .kw { color: #ff7b72; }
|
||||||
|
.code-window .fn { color: #d2a8ff; }
|
||||||
|
.code-window .str { color: #a5d6ff; }
|
||||||
|
.code-window .num { color: #79c0ff; }
|
||||||
|
|
||||||
.version-row {
|
.version-row {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(3, 1fr);
|
grid-template-columns: repeat(3, 1fr);
|
||||||
@@ -666,7 +751,7 @@ pre {
|
|||||||
}
|
}
|
||||||
.detailed-version-row li {
|
.detailed-version-row li {
|
||||||
font-size: clamp(10px, calc(13px * var(--slide-scale)), 17px);
|
font-size: clamp(10px, calc(13px * var(--slide-scale)), 17px);
|
||||||
line-height: 1.42;
|
/* line-height: 1.42; */
|
||||||
}
|
}
|
||||||
.detailed-version-row code {
|
.detailed-version-row code {
|
||||||
color: #ffe8aa;
|
color: #ffe8aa;
|
||||||
@@ -1007,7 +1092,8 @@ code {
|
|||||||
.subtitle { font-size: clamp(14px, 4vw, 18px); }
|
.subtitle { font-size: clamp(14px, 4vw, 18px); }
|
||||||
.kicker { font-size: clamp(12px, 3.7vw, 16px); }
|
.kicker { font-size: clamp(12px, 3.7vw, 16px); }
|
||||||
.slide.active.protocol-slide,
|
.slide.active.protocol-slide,
|
||||||
.slide.active.image-slide { display: block; }
|
.slide.active.image-slide,
|
||||||
|
.slide.active.code-slide { display: block; }
|
||||||
.hero-grid, .split, .demo-layout, .version-row, .resource-grid, .mechanism-grid, .takeaway-grid { grid-template-columns: 1fr; }
|
.hero-grid, .split, .demo-layout, .version-row, .resource-grid, .mechanism-grid, .takeaway-grid { grid-template-columns: 1fr; }
|
||||||
.takeaway-grid { grid-template-rows: none; min-height: 0; }
|
.takeaway-grid { grid-template-rows: none; min-height: 0; }
|
||||||
.hero-grid, .split, .demo-layout, .mechanism-grid { gap: 18px; }
|
.hero-grid, .split, .demo-layout, .mechanism-grid { gap: 18px; }
|
||||||
@@ -1047,6 +1133,19 @@ code {
|
|||||||
.mini-points span { font-size: 12px; padding: 7px 10px; }
|
.mini-points span { font-size: 12px; padding: 7px 10px; }
|
||||||
.quote { font-size: 15px; }
|
.quote { font-size: 15px; }
|
||||||
pre { font-size: clamp(10px, 2.8vw, 13px); padding: 14px; }
|
pre { font-size: clamp(10px, 2.8vw, 13px); padding: 14px; }
|
||||||
|
.code-window { display: block; }
|
||||||
|
.code-window pre {
|
||||||
|
overflow-x: auto;
|
||||||
|
font-size: clamp(10px, 2.7vw, 13px);
|
||||||
|
padding: 12px 0;
|
||||||
|
}
|
||||||
|
.code-window .line {
|
||||||
|
padding-right: 14px;
|
||||||
|
}
|
||||||
|
.code-window .line::before {
|
||||||
|
width: 32px;
|
||||||
|
margin-right: 12px;
|
||||||
|
}
|
||||||
ol { font-size: 14px; line-height: 1.9; }
|
ol { font-size: 14px; line-height: 1.9; }
|
||||||
.controls { left: 14px; bottom: 14px; }
|
.controls { left: 14px; bottom: 14px; }
|
||||||
.controls button { width: 36px; height: 36px; font-size: 24px; }
|
.controls button { width: 36px; height: 36px; font-size: 24px; }
|
||||||
|
|||||||
Reference in New Issue
Block a user