From f2ea6e1d0fd1f352c83c697c39ec75f171485542 Mon Sep 17 00:00:00 2001 From: ddnthemc Date: Sun, 20 Apr 2025 11:21:24 +0200 Subject: [PATCH] Initial Commit --- .gitignore | 4 + .idea/.gitignore | 3 + .idea/TrackAtt.iml | 8 ++ .../inspectionProfiles/profiles_settings.xml | 6 + .idea/misc.xml | 7 + .idea/modules.xml | 8 ++ .idea/vcs.xml | 6 + README.md | 4 + activities_old.json | 15 +++ backend.py | 114 ++++++++++++++++ json_extensions.py | 39 ++++++ static/script.js | 106 +++++++++++++++ static/style.css | 127 ++++++++++++++++++ static/style2.css | 4 + templates/index.html | 28 ++++ templates/index2.html | 41 ++++++ 16 files changed, 520 insertions(+) create mode 100644 .gitignore create mode 100644 .idea/.gitignore create mode 100644 .idea/TrackAtt.iml create mode 100644 .idea/inspectionProfiles/profiles_settings.xml create mode 100644 .idea/misc.xml create mode 100644 .idea/modules.xml create mode 100644 .idea/vcs.xml create mode 100644 README.md create mode 100644 activities_old.json create mode 100644 backend.py create mode 100644 json_extensions.py create mode 100644 static/script.js create mode 100644 static/style.css create mode 100644 static/style2.css create mode 100644 templates/index.html create mode 100644 templates/index2.html diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..653ff56 --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +/log/ +/.idea/ +/__pycache__/ + diff --git a/.idea/.gitignore b/.idea/.gitignore new file mode 100644 index 0000000..26d3352 --- /dev/null +++ b/.idea/.gitignore @@ -0,0 +1,3 @@ +# Default ignored files +/shelf/ +/workspace.xml diff --git a/.idea/TrackAtt.iml b/.idea/TrackAtt.iml new file mode 100644 index 0000000..d0876a7 --- /dev/null +++ b/.idea/TrackAtt.iml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/.idea/inspectionProfiles/profiles_settings.xml b/.idea/inspectionProfiles/profiles_settings.xml new file mode 100644 index 0000000..105ce2d --- /dev/null +++ b/.idea/inspectionProfiles/profiles_settings.xml @@ -0,0 +1,6 @@ + + + + \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml new file mode 100644 index 0000000..a6218fe --- /dev/null +++ b/.idea/misc.xml @@ -0,0 +1,7 @@ + + + + + + \ No newline at end of file diff --git a/.idea/modules.xml b/.idea/modules.xml new file mode 100644 index 0000000..07ead32 --- /dev/null +++ b/.idea/modules.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml new file mode 100644 index 0000000..94a25f7 --- /dev/null +++ b/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..50b52b2 --- /dev/null +++ b/README.md @@ -0,0 +1,4 @@ +ActivityTrackerWeb is a web full stack implementation of an activity tracker. +Currently just a playground to test concepts and possible user experiences. + +It's a *Work in Progress!* diff --git a/activities_old.json b/activities_old.json new file mode 100644 index 0000000..c5f2804 --- /dev/null +++ b/activities_old.json @@ -0,0 +1,15 @@ +[ + "Dormire", + "Guida Auto", + "Work", + "Cesso", + "Giardino", + "Cena/Pranzo", + "Cucina", + "Allenamento", + "Messa", + "Duolinguo", + "Online", + "SelfStudy", + "Puzza" +] \ No newline at end of file diff --git a/backend.py b/backend.py new file mode 100644 index 0000000..f041a51 --- /dev/null +++ b/backend.py @@ -0,0 +1,114 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +from fastapi import FastAPI, WebSocket, WebSocketDisconnect, Request +from fastapi.responses import JSONResponse, HTMLResponse +from fastapi.staticfiles import StaticFiles +from fastapi.templating import Jinja2Templates +from fastapi.middleware.cors import CORSMiddleware +from typing import List +import uvicorn +import time +from pathlib import Path +from json_extensions import load_from_json_file, save_to_json_file + + +Base_Log = Path('./log') + +Activities_File = Path('activities.json') + +Base_Log.mkdir(exist_ok=True, parents=True) +app = FastAPI() + +# Mount static files like CSS/JS/images +app.mount("/static", StaticFiles(directory="static"), name="static") + +# Set up templates directory +templates = Jinja2Templates(directory="templates") + +# Dummy shared state +current_activity = "No activity yet" +if Activities_File.is_file(): + activities = load_from_json_file(Activities_File) +else: + activities = [ + 'Dormire', + 'Guida Auto', + 'Work', + 'Cesso', + 'Giardino', + 'Cena/Pranzo', + 'Cucina', + 'Allenamento', + 'Messa', + 'Duolinguo', + 'Online', + 'SelfStudy', + ] + save_to_json_file(Activities_File, activities) +# for i in range(99): +# activities.append(f'Act {i}') +connections: List[WebSocket] = [] + + + +@app.get("/", response_class=HTMLResponse) +async def get_homepage(request: Request, u: str = 'Nobody'): + return templates.TemplateResponse("index2.html", {"request": request, 'user': u}) + + +@app.get("/api/init") +async def get_initial_data(): + return JSONResponse(content={ + "current": current_activity, + "activities": activities + }) + + +@app.websocket("/ws/activity") +async def activity_websocket(websocket: WebSocket, u: str = 'Unk'): + await websocket.accept() + connections.append(websocket) + try: + while True: + data = await websocket.receive_json() + print(data) + if data.get("action") == "select": + now = time.time() + data['user'] = u + data['epoch'] = now + dump_data(data) + global current_activity + current_activity = data["activity"] + if current_activity not in activities: + activities.append(current_activity) + save_to_json_file(Activities_File, activities) + await broadcast({ + "type": "activity_update", + "activity": current_activity + }) + except WebSocketDisconnect: + connections.remove(websocket) + + +def dump_data(dt: dict): + user = dt.get('user', 'Ukn') + epoch = dt.get('epoch', 0.0) + human_epoch = time.strftime('%Y_%m_%d_%H%M%S', time.localtime(epoch)) + outfile = Base_Log / f'Act_{user}_{int(1000.0 * epoch)}_{human_epoch}.json' + save_to_json_file(outfile, dt) + + +async def broadcast(message: dict): + for conn in connections: + try: + await conn.send_json(message) + except Exception: + connections.remove(conn) + + +def main(): + pass + + +if __name__ == '__main__': + uvicorn.run(app, host="127.0.0.1", port=8000) diff --git a/json_extensions.py b/json_extensions.py new file mode 100644 index 0000000..9dcddd8 --- /dev/null +++ b/json_extensions.py @@ -0,0 +1,39 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +import json +from decimal import Decimal +# from task_row_x import TaskRow, SimpleTaskRow + + +def load_from_json_file(infile): + with open(infile, 'r') as fin: + obj = json.load(fin) + return obj + + +class SpecialEncoder(json.JSONEncoder): + def default(self, obj): + if isinstance(obj, Decimal): + return str(obj) # Or you could return str(obj) depending on your needs + # if isinstance(obj, TaskRow) or isinstance(obj, SimpleTaskRow): + if hasattr(obj, 'get_dict') and callable(getattr(obj, 'get_dict')): + return obj.get_dict() + if isinstance(obj, set): + return [str(v) for v in obj] + return super().default(obj) + + +def save_to_json_file(out_name, obj): + with open(out_name, 'w', encoding='utf-8', newline='\n') as fout: + json.dump(obj, fout, indent=4, cls=SpecialEncoder) + + +def save_to_json_str(obj): + return json.dumps(obj, indent=4, cls=SpecialEncoder) + +def main(): + pass + + +if __name__ == '__main__': + main() diff --git a/static/script.js b/static/script.js new file mode 100644 index 0000000..b02610b --- /dev/null +++ b/static/script.js @@ -0,0 +1,106 @@ +let currentActivity = ''; +let socket; + +function renderActivitiesOld(activities) { +const list = document.getElementById('activity-list'); +list.innerHTML = ''; +activities.forEach(activity => { + const div = document.createElement('div'); + div.textContent = activity; + div.className = 'activity-item' + (activity === currentActivity ? ' selected' : ''); + div.onclick = () => selectActivity(activity); + list.appendChild(div); +}); +} + +function renderActivities(activities) { + const list = document.getElementById('activity-list'); + list.innerHTML = ''; + + activities.forEach(activity => { + const id = `activity-${activity.replace(/\s+/g, '-')}`; + + const label = document.createElement('label'); + label.className = 'activity-option'; + + const radio = document.createElement('input'); + radio.type = 'radio'; + radio.name = 'activity'; + radio.value = activity; + radio.id = id; + if (activity === currentActivity) { + radio.checked = true; + } + + radio.addEventListener('change', () => { + socket.send(JSON.stringify({ action: 'select', activity })); + }); + + const span = document.createElement('span'); + span.textContent = activity; + + label.appendChild(radio); + label.appendChild(span); + list.appendChild(label); + }); +} + + + +function selectActivity(activity) { +currentActivity = activity; +document.getElementById('current-activity').textContent = activity; +renderActivities(activityList); +socket.send(JSON.stringify({ action: 'select', activity })); +} + +function addNewActivity() { +const input = document.getElementById('new-activity'); +const newAct = input.value.trim(); +if (newAct && !activityList.includes(newAct)) { + activityList.push(newAct); + selectActivity(newAct); + input.value = ''; +} +} + +let activityList = []; + +async function initializePage() { +// Fetch initial data from server +const response = await fetch(`/api/init?u=${user}`); +const data = await response.json(); +currentActivity = data.current; +activityList = data.activities; +document.getElementById('current-activity').textContent = currentActivity; +renderActivities(activityList); +} + +function setupWebSocket() { +socket = new WebSocket(`ws://${window.location.host}/ws/activity?u=${user}`); + +socket.onopen = () => { + console.log('WebSocket connected'); + socket.send(JSON.stringify({ action: 'hello' })); +}; + +socket.onmessage = (event) => { + const msg = JSON.parse(event.data); + if (msg.type === 'activity_update') { + currentActivity = msg.activity; + document.getElementById('current-activity').textContent = currentActivity; + renderActivities(activityList); + } +}; + +socket.onclose = () => { + console.log('WebSocket closed, retrying...'); + setTimeout(setupWebSocket, 1000); +}; +} + +// Initialize on load +window.onload = () => { +initializePage(); +setupWebSocket(); +}; \ No newline at end of file diff --git a/static/style.css b/static/style.css new file mode 100644 index 0000000..af1870b --- /dev/null +++ b/static/style.css @@ -0,0 +1,127 @@ +:root { + --primary: #0066cc; + --accent: #e0f0ff; + --text: #222; + --bg: #f9f9f9; + --border-radius: 0.5rem; +} + +* { + box-sizing: border-box; + margin: 0; + padding: 0; + font-family: "Segoe UI", sans-serif; +} + +body { + background-color: var(--bg); + color: var(--text); + padding: 1rem; + display: flex; + justify-content: center; + align-items: center; + min-height: 100vh; +} + +.container { + width: 100%; + max-width: 600px; + background-color: white; + padding: 2rem; + border-radius: var(--border-radius); + box-shadow: 0 0 20px rgba(0, 0, 0, 0.05); + display: flex; + flex-direction: column; + gap: 2rem; +} + +h1, h2 { + color: var(--primary); +} + +#current-activity { + font-size: 1.2rem; + padding: 1rem; + background-color: var(--accent); + border-radius: var(--border-radius); + margin-top: 0.5rem; +} + +#activity-list { + list-style-type: none; + margin-top: 1rem; + display: flex; + flex-direction: column; + gap: 0.5rem; +} + +#activity-list li { + padding: 0.6rem 1rem; + background-color: #f0f0f0; + border-radius: var(--border-radius); + cursor: pointer; + transition: background 0.2s; +} + +#activity-list li:hover { + background-color: #dceeff; +} + +input[type="text"] { + width: 100%; + padding: 0.6rem; + font-size: 1rem; + margin-top: 0.5rem; + border: 1px solid #ccc; + border-radius: var(--border-radius); +} + +button { + margin-top: 0.5rem; + padding: 0.6rem 1.2rem; + font-size: 1rem; + background-color: var(--primary); + color: white; + border: none; + border-radius: var(--border-radius); + cursor: pointer; + transition: background 0.3s ease; +} + +button:hover { + background-color: #004a99; +} + +footer { + text-align: center; + font-size: 0.9rem; + color: #666; + margin-top: 2rem; +} + +#activity-list { + display: flex; + flex-direction: column; + gap: 0.75rem; +} + +.activity-option { + display: flex; + align-items: center; + gap: 0.75rem; + padding: 0.6rem 1rem; + background-color: #f0f0f0; + border-radius: var(--border-radius); + cursor: pointer; + transition: background 0.2s; +} + +.activity-option:hover { + background-color: #e6f2ff; +} + +.activity-option input[type="radio"] { + accent-color: var(--primary); + width: 1.2rem; + height: 1.2rem; +} \ No newline at end of file diff --git a/static/style2.css b/static/style2.css new file mode 100644 index 0000000..5fe67c6 --- /dev/null +++ b/static/style2.css @@ -0,0 +1,4 @@ + body { font-family: sans-serif; margin: 2rem; } + .activity-list { margin-top: 1rem; } + .activity-item { cursor: pointer; padding: 0.5rem; border: 1px solid #ccc; margin-bottom: 0.5rem; } + .activity-item.selected { background-color: #def; } diff --git a/templates/index.html b/templates/index.html new file mode 100644 index 0000000..5f4eb7c --- /dev/null +++ b/templates/index.html @@ -0,0 +1,28 @@ + + + + + Activity Tracker + + + + +

Select Your Activity

+ +
+ Current Activity: Loading... +
+ +
+ +
+ +
+ + +
+ + + + + diff --git a/templates/index2.html b/templates/index2.html new file mode 100644 index 0000000..1edb5ed --- /dev/null +++ b/templates/index2.html @@ -0,0 +1,41 @@ + + + + + + Activity Tracker + + + +
+
+

Che stai AffĂ 

+
+ +
+

{{ user }} Current Activity:

+
Loading...
+
+ +
+

Select Existing Activity:

+
    +
    + +
    +

    Or Add a New One:

    + + +
    + +
    +

    Connected via WebSocket | All changes sync in real-time

    +
    +
    + + + + +