From 090a57f4dd8931e55c56b15d644029fb7b1be6ed Mon Sep 17 00:00:00 2001 From: Dylan De Faoite Date: Tue, 3 Mar 2026 15:29:21 +0000 Subject: [PATCH 01/24] build: add frontend to docker --- docker-compose.dev.yml | 12 ++++++++++++ frontend/Dockerfile | 13 +++++++++++++ 2 files changed, 25 insertions(+) create mode 100644 frontend/Dockerfile diff --git a/docker-compose.dev.yml b/docker-compose.dev.yml index ea5e341..96c3430 100644 --- a/docker-compose.dev.yml +++ b/docker-compose.dev.yml @@ -56,5 +56,17 @@ services: count: 1 capabilities: [gpu] + frontend: + build: + context: ./frontend + container_name: crosspost_frontend + volumes: + - ./frontend:/app + - /app/node_modules + ports: + - "5173:5173" + depends_on: + - backend + volumes: model_cache: \ No newline at end of file diff --git a/frontend/Dockerfile b/frontend/Dockerfile new file mode 100644 index 0000000..e8ea84e --- /dev/null +++ b/frontend/Dockerfile @@ -0,0 +1,13 @@ +FROM node:20-alpine + +WORKDIR /app + +COPY package.json package-lock.json* ./ +RUN npm install + +# Copy rest of the app +COPY . . + +EXPOSE 5173 + +CMD ["npm", "run", "dev", "--", "--host"] \ No newline at end of file -- 2.49.1 From 8ac5207a11bc85ec1f49e21c48266e9cf8b7822e Mon Sep 17 00:00:00 2001 From: Dylan De Faoite Date: Tue, 3 Mar 2026 15:55:01 +0000 Subject: [PATCH 02/24] feat: add login page --- frontend/src/pages/Login.tsx | 168 +++++++++++++++++++++++++++++++++++ 1 file changed, 168 insertions(+) create mode 100644 frontend/src/pages/Login.tsx diff --git a/frontend/src/pages/Login.tsx b/frontend/src/pages/Login.tsx new file mode 100644 index 0000000..2c67511 --- /dev/null +++ b/frontend/src/pages/Login.tsx @@ -0,0 +1,168 @@ +import { useEffect, useState } from "react"; +import axios from "axios"; +import { useNavigate } from "react-router-dom"; +import StatsStyling from "../styles/stats_styling"; + +const styles = StatsStyling; +const API_BASE_URL = "http://localhost:5000"; + +const LoginPage = () => { + const navigate = useNavigate(); + + const [isRegisterMode, setIsRegisterMode] = useState(false); + const [username, setUsername] = useState(""); + const [email, setEmail] = useState(""); + const [password, setPassword] = useState(""); + const [loading, setLoading] = useState(false); + const [error, setError] = useState(""); + const [info, setInfo] = useState(""); + + useEffect(() => { + const token = localStorage.getItem("access_token"); + if (!token) { + return; + } + + axios.defaults.headers.common.Authorization = `Bearer ${token}`; + axios + .get(`${API_BASE_URL}/profile`) + .then(() => { + navigate("/upload", { replace: true }); + }) + .catch(() => { + localStorage.removeItem("access_token"); + delete axios.defaults.headers.common.Authorization; + }); + }, [navigate]); + + const handleSubmit = async (event: React.FormEvent) => { + event.preventDefault(); + setError(""); + setInfo(""); + setLoading(true); + + try { + if (isRegisterMode) { + await axios.post(`${API_BASE_URL}/register`, { username, email, password }); + setInfo("Account created. You can now sign in."); + setIsRegisterMode(false); + } else { + const response = await axios.post<{ access_token: string }>( + `${API_BASE_URL}/login`, + { username, password } + ); + + const token = response.data.access_token; + localStorage.setItem("access_token", token); + axios.defaults.headers.common.Authorization = `Bearer ${token}`; + navigate("/upload"); + } + } catch (requestError: unknown) { + if (axios.isAxiosError(requestError)) { + setError( + String(requestError.response?.data?.error || requestError.message || "Request failed") + ); + } else { + setError("Unexpected error occurred."); + } + } finally { + setLoading(false); + } + }; + + return ( +
+
+
+
+

+ {isRegisterMode ? "Create your account" : "Welcome back"} +

+

+ {isRegisterMode + ? "Register to start uploading and exploring your dataset insights." + : "Sign in to continue to your analytics workspace."} +

+
+ +
+ setUsername(event.target.value)} + required + /> + + {isRegisterMode && ( + setEmail(event.target.value)} + required + /> + )} + + setPassword(event.target.value)} + required + /> + + +
+ + {error && ( +

+ {error} +

+ )} + + {info && ( +

+ {info} +

+ )} + +
+ + {isRegisterMode ? "Already have an account?" : "New here?"} + + +
+
+
+
+ ); +}; + +export default LoginPage; -- 2.49.1 From 64783e764d9d19f641ef423b05d36d0d86a381c9 Mon Sep 17 00:00:00 2001 From: Dylan De Faoite Date: Tue, 3 Mar 2026 16:57:10 +0000 Subject: [PATCH 03/24] fix: remove unnecessary styling in index.css --- frontend/src/index.css | 61 ++++-------------------------------------- 1 file changed, 5 insertions(+), 56 deletions(-) diff --git a/frontend/src/index.css b/frontend/src/index.css index 08a3ac9..87e53e4 100644 --- a/frontend/src/index.css +++ b/frontend/src/index.css @@ -1,68 +1,17 @@ :root { - font-family: system-ui, Avenir, Helvetica, Arial, sans-serif; - line-height: 1.5; - font-weight: 400; - - color-scheme: light dark; - color: rgba(255, 255, 255, 0.87); - background-color: #242424; - font-synthesis: none; text-rendering: optimizeLegibility; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; } -a { - font-weight: 500; - color: #646cff; - text-decoration: inherit; -} -a:hover { - color: #535bf2; +html, +body, +#root { + width: 100%; + height: 100%; } body { margin: 0; - display: flex; - place-items: center; - min-width: 320px; - min-height: 100vh; -} - -h1 { - font-size: 3.2em; - line-height: 1.1; -} - -button { - border-radius: 8px; - border: 1px solid transparent; - padding: 0.6em 1.2em; - font-size: 1em; - font-weight: 500; - font-family: inherit; - background-color: #1a1a1a; - cursor: pointer; - transition: border-color 0.25s; -} -button:hover { - border-color: #646cff; -} -button:focus, -button:focus-visible { - outline: 4px auto -webkit-focus-ring-color; -} - -@media (prefers-color-scheme: light) { - :root { - color: #213547; - background-color: #ffffff; - } - a:hover { - color: #747bff; - } - button { - background-color: #f9f9f9; - } } -- 2.49.1 From 4b33f17b4b1daf299f2fc08a4e36899175aa73ce Mon Sep 17 00:00:00 2001 From: Dylan De Faoite Date: Tue, 3 Mar 2026 17:07:31 +0000 Subject: [PATCH 04/24] fix: inconsistent styling in login page --- frontend/src/pages/Login.tsx | 73 ++++++++++++++++++++++++++++-------- 1 file changed, 58 insertions(+), 15 deletions(-) diff --git a/frontend/src/pages/Login.tsx b/frontend/src/pages/Login.tsx index 2c67511..4916eee 100644 --- a/frontend/src/pages/Login.tsx +++ b/frontend/src/pages/Login.tsx @@ -5,6 +5,11 @@ import StatsStyling from "../styles/stats_styling"; const styles = StatsStyling; const API_BASE_URL = "http://localhost:5000"; +const controlStyle = { + width: "100%", + maxWidth: "100%", + boxSizing: "border-box" as const, +}; const LoginPage = () => { const navigate = useNavigate(); @@ -71,18 +76,17 @@ const LoginPage = () => { }; return ( -
-
+
-
-

+
+

{isRegisterMode ? "Create your account" : "Welcome back"}

@@ -92,11 +96,14 @@ const LoginPage = () => {

-
+ setUsername(event.target.value)} required @@ -106,7 +113,7 @@ const LoginPage = () => { setEmail(event.target.value)} required @@ -116,13 +123,17 @@ const LoginPage = () => { setPassword(event.target.value)} required /> -

-
); }; -- 2.49.1 From 53105686317b971fac3ce9f17c2d1ca0ffce21e2 Mon Sep 17 00:00:00 2001 From: Dylan De Faoite Date: Tue, 3 Mar 2026 17:17:57 +0000 Subject: [PATCH 05/24] feat: add React layout and a topbar allowing for easy logins --- frontend/src/App.tsx | 12 ++- frontend/src/components/AppLayout.tsx | 125 ++++++++++++++++++++++++++ server/app.py | 1 - 3 files changed, 134 insertions(+), 4 deletions(-) create mode 100644 frontend/src/components/AppLayout.tsx diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index b7b1726..2f728d6 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -1,12 +1,18 @@ -import { Routes, Route } from "react-router-dom"; +import { Navigate, Route, Routes } from "react-router-dom"; +import AppLayout from "./components/AppLayout"; +import LoginPage from "./pages/Login"; import UploadPage from "./pages/Upload"; import StatPage from "./pages/Stats"; function App() { return ( - } /> - } /> + }> + } /> + } /> + } /> + } /> + ); } diff --git a/frontend/src/components/AppLayout.tsx b/frontend/src/components/AppLayout.tsx new file mode 100644 index 0000000..272539c --- /dev/null +++ b/frontend/src/components/AppLayout.tsx @@ -0,0 +1,125 @@ +import { useCallback, useEffect, useState } from "react"; +import axios from "axios"; +import { Outlet, useLocation, useNavigate } from "react-router-dom"; +import StatsStyling from "../styles/stats_styling"; + +type ProfileResponse = { + user?: Record; +}; + +const styles = StatsStyling; +const API_BASE_URL = "http://localhost:5000"; + +const getUserLabel = (user: Record | null) => { + if (!user) { + return "Signed in"; + } + + const username = user.username; + if (typeof username === "string" && username.length > 0) { + return username; + } + + const email = user.email; + if (typeof email === "string" && email.length > 0) { + return email; + } + + return "Signed in"; +}; + +const AppLayout = () => { + const location = useLocation(); + const navigate = useNavigate(); + const [isSignedIn, setIsSignedIn] = useState(false); + const [currentUser, setCurrentUser] = useState | null>(null); + + const syncAuthState = useCallback(async () => { + const token = localStorage.getItem("access_token"); + + if (!token) { + setIsSignedIn(false); + setCurrentUser(null); + delete axios.defaults.headers.common.Authorization; + return; + } + + axios.defaults.headers.common.Authorization = `Bearer ${token}`; + + try { + const response = await axios.get(`${API_BASE_URL}/profile`); + setIsSignedIn(true); + setCurrentUser(response.data.user ?? null); + } catch { + localStorage.removeItem("access_token"); + delete axios.defaults.headers.common.Authorization; + setIsSignedIn(false); + setCurrentUser(null); + } + }, []); + + useEffect(() => { + void syncAuthState(); + }, [location.pathname, syncAuthState]); + + const onAuthButtonClick = () => { + if (isSignedIn) { + localStorage.removeItem("access_token"); + delete axios.defaults.headers.common.Authorization; + setIsSignedIn(false); + setCurrentUser(null); + navigate("/login", { replace: true }); + return; + } + + navigate("/login"); + }; + + return ( +
+
+
+
+ + Ethnograph View + + + {isSignedIn ? `Signed in: ${getUserLabel(currentUser)}` : "Not signed in"} + +
+ +
+ +
+
+
+ + +
+ ); +}; + +export default AppLayout; diff --git a/server/app.py b/server/app.py index a92f4a7..49feae8 100644 --- a/server/app.py +++ b/server/app.py @@ -15,7 +15,6 @@ from flask_jwt_extended import ( ) from server.analysis.stat_gen import StatGen -from server.analysis.enrichment import DatasetEnrichment from server.exceptions import NotAuthorisedException, NonExistentDatasetException from server.db.database import PostgresConnector from server.core.auth import AuthManager -- 2.49.1 From b6de100a1734347950b0d6886c761c975143d493 Mon Sep 17 00:00:00 2001 From: Dylan De Faoite Date: Tue, 3 Mar 2026 17:18:09 +0000 Subject: [PATCH 06/24] feat: overhaul upload page styling --- frontend/src/pages/Upload.tsx | 156 +++++++++++++++++++++++++--------- 1 file changed, 116 insertions(+), 40 deletions(-) diff --git a/frontend/src/pages/Upload.tsx b/frontend/src/pages/Upload.tsx index 2218231..74a9b18 100644 --- a/frontend/src/pages/Upload.tsx +++ b/frontend/src/pages/Upload.tsx @@ -1,56 +1,132 @@ -import axios from 'axios' -import './../App.css' -import { useState } from 'react' -import { useNavigate } from 'react-router-dom' +import axios from "axios"; +import { useState } from "react"; +import { useNavigate } from "react-router-dom"; import StatsStyling from "../styles/stats_styling"; const styles = StatsStyling; +const API_BASE_URL = "http://localhost:5000"; const UploadPage = () => { - let postFile: File | undefined; - let topicBucketFile: File | undefined; - const [returnMessage, setReturnMessage] = useState('') - const navigate = useNavigate() + const [postFile, setPostFile] = useState(null); + const [topicBucketFile, setTopicBucketFile] = useState(null); + const [returnMessage, setReturnMessage] = useState(""); + const [isSubmitting, setIsSubmitting] = useState(false); + const [hasError, setHasError] = useState(false); + const navigate = useNavigate(); const uploadFiles = async () => { if (!postFile || !topicBucketFile) { - alert('Please upload all files before uploading.') - return + setHasError(true); + setReturnMessage("Please upload both files before continuing."); + return; } - const formData = new FormData() - formData.append('posts', postFile) - formData.append('topics', topicBucketFile) + const formData = new FormData(); + formData.append("posts", postFile); + formData.append("topics", topicBucketFile); try { - const response = await axios.post('http://localhost:5000/upload', formData, { - headers: { - 'Content-Type': 'multipart/form-data', - }, - }) - console.log('Files uploaded successfully:', response.data) - setReturnMessage(`Upload successful! Posts: ${response.data.posts_count}, Comments: ${response.data.comments_count}`) - navigate('/stats') - } catch (error) { - console.error('Error uploading files:', error) - setReturnMessage('Error uploading files. Error details: ' + error) - } - } - return ( -
-
-

Posts File

- postFile = e.target.files?.[0]}> -
-
-

Topic Buckets File

- topicBucketFile = e.target.files?.[0]}> -
- + setIsSubmitting(true); + setHasError(false); + setReturnMessage(""); -

{returnMessage}

+ const response = await axios.post(`${API_BASE_URL}/upload`, formData, { + headers: { + "Content-Type": "multipart/form-data", + }, + }); + + setReturnMessage( + `Upload queued successfully (dataset #${response.data.dataset_id}). Redirecting to insights...` + ); + + setTimeout(() => { + navigate("/stats"); + }, 400); + } catch (error: unknown) { + setHasError(true); + if (axios.isAxiosError(error)) { + const message = String(error.response?.data?.error || error.message || "Upload failed."); + setReturnMessage(`Upload failed: ${message}`); + } else { + setReturnMessage("Upload failed due to an unexpected error."); + } + } finally { + setIsSubmitting(false); + } + }; + + return ( +
+
+
+
+

Upload Dataset

+

+ Add your posts and topic map files to generate fresh analytics. +

+
+ +
+ +
+
+

Posts File (.jsonl)

+

Upload the raw post records export.

+ setPostFile(event.target.files?.[0] ?? null)} + /> +

+ {postFile ? `Selected: ${postFile.name}` : "No file selected"} +

+
+ +
+

Topics File (.json)

+

Upload your topic bucket mapping file.

+ setTopicBucketFile(event.target.files?.[0] ?? null)} + /> +

+ {topicBucketFile ? `Selected: ${topicBucketFile.name}` : "No file selected"} +

+
+
+ +
+ {returnMessage || "After upload, your dataset is queued for processing and you'll land on stats."} +
+
- ) -} + ); +}; export default UploadPage; -- 2.49.1 From 772205d3dfa6ff0f3b607454b8ee4f7d48c8d2f5 Mon Sep 17 00:00:00 2001 From: Dylan De Faoite Date: Tue, 3 Mar 2026 17:25:00 +0000 Subject: [PATCH 07/24] feat(api): add ability to fetch all datasets by a user --- server/app.py | 10 ++++++++-- server/core/datasets.py | 4 ++++ 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/server/app.py b/server/app.py index 49feae8..c0d28ef 100644 --- a/server/app.py +++ b/server/app.py @@ -104,15 +104,21 @@ def profile(): message="Access granted", user=auth_manager.get_user_by_id(current_user) ), 200 +@app.route("/user/datasets") +@jwt_required() +def get_user_datasets(): + current_user = int(get_jwt_identity()) + return jsonify(dataset_manager.get_user_datasets(current_user)), 200 @app.route("/upload", methods=["POST"]) @jwt_required() def upload_data(): - if "posts" not in request.files or "topics" not in request.files: + if "posts" not in request.files or "topics" not in request.files or "name" not in request.form: return jsonify({"error": "Missing required files or form data"}), 400 post_file = request.files["posts"] topic_file = request.files["topics"] + dataset_name = request.form["name"] if post_file.filename == "" or topic_file.filename == "": return jsonify({"error": "Empty filename"}), 400 @@ -129,7 +135,7 @@ def upload_data(): posts_df = pd.read_json(post_file, lines=True, convert_dates=False) topics = json.load(topic_file) - dataset_id = dataset_manager.save_dataset_info(current_user, f"dataset_{current_user}", topics) + dataset_id = dataset_manager.save_dataset_info(current_user, dataset_name, topics) process_dataset.delay( dataset_id, diff --git a/server/core/datasets.py b/server/core/datasets.py index 541db5d..2f0e35f 100644 --- a/server/core/datasets.py +++ b/server/core/datasets.py @@ -17,6 +17,10 @@ class DatasetManager: return False return True + + def get_user_datasets(self, user_id: int) -> list[dict]: + query = "SELECT * FROM datasets WHERE user_id = %s" + return self.db.execute(query, (user_id, ), fetch=True) def get_dataset_content(self, dataset_id: int) -> pd.DataFrame: query = "SELECT * FROM events WHERE dataset_id = %s" -- 2.49.1 From 207c4b67da84e351095411b3dcd7cadccaf03173 Mon Sep 17 00:00:00 2001 From: Dylan De Faoite Date: Tue, 3 Mar 2026 17:28:46 +0000 Subject: [PATCH 08/24] feat(frontend): add dataset name requirements to the upload page --- frontend/src/pages/Upload.tsx | 28 +++++++++++++++++++++++++--- server/app.py | 20 ++++++++++---------- 2 files changed, 35 insertions(+), 13 deletions(-) diff --git a/frontend/src/pages/Upload.tsx b/frontend/src/pages/Upload.tsx index 74a9b18..712d853 100644 --- a/frontend/src/pages/Upload.tsx +++ b/frontend/src/pages/Upload.tsx @@ -7,6 +7,7 @@ const styles = StatsStyling; const API_BASE_URL = "http://localhost:5000"; const UploadPage = () => { + const [datasetName, setDatasetName] = useState(""); const [postFile, setPostFile] = useState(null); const [topicBucketFile, setTopicBucketFile] = useState(null); const [returnMessage, setReturnMessage] = useState(""); @@ -15,6 +16,14 @@ const UploadPage = () => { const navigate = useNavigate(); const uploadFiles = async () => { + const normalizedDatasetName = datasetName.trim(); + + if (!normalizedDatasetName) { + setHasError(true); + setReturnMessage("Please add a dataset name before continuing."); + return; + } + if (!postFile || !topicBucketFile) { setHasError(true); setReturnMessage("Please upload both files before continuing."); @@ -22,6 +31,7 @@ const UploadPage = () => { } const formData = new FormData(); + formData.append("name", normalizedDatasetName); formData.append("posts", postFile); formData.append("topics", topicBucketFile); @@ -63,7 +73,7 @@ const UploadPage = () => {

Upload Dataset

- Add your posts and topic map files to generate fresh analytics. + Name your dataset, then upload posts and topic map files to generate analytics.

-
Analytics Dashboard
-
+
Analytics Dashboard
+
Dataset #{datasetId ?? "-"}
+
+ + + + + + +
+ + {error && ( +
+ {error} +
+ )} + + {!error && datasets.length === 0 && ( +
+ No datasets yet. Upload one to get started. +
+ )} + + {!error && datasets.length > 0 && ( +
+
    + {datasets.map((dataset) => { + const isComplete = dataset.status === "complete"; + const targetPath = isComplete + ? `/dataset/${dataset.id}/stats` + : `/dataset/${dataset.id}/status`; + + return ( +
  • +
    +
    + {dataset.name || `Dataset #${dataset.id}`} +
    +
    + ID #{dataset.id} • Status: {dataset.status || "unknown"} +
    + {dataset.status_message && ( +
    + {dataset.status_message} +
    + )} +
    + + +
  • + ); + })} +
+
+ )} + + + ); +}; + +export default DatasetsPage; diff --git a/frontend/src/utils/documentTitle.ts b/frontend/src/utils/documentTitle.ts index 779d9b2..904a6a8 100644 --- a/frontend/src/utils/documentTitle.ts +++ b/frontend/src/utils/documentTitle.ts @@ -3,7 +3,7 @@ const DEFAULT_TITLE = "Ethnograph View"; const STATIC_TITLES: Record = { "/login": "Sign In", "/upload": "Upload Dataset", - "/stats": "Stats", + "/datasets": "My Datasets", }; export const getDocumentTitle = (pathname: string) => { -- 2.49.1 From f3b48525e21be76737699d8be435bf61f74864e0 Mon Sep 17 00:00:00 2001 From: Dylan De Faoite Date: Tue, 3 Mar 2026 20:09:57 +0000 Subject: [PATCH 14/24] feat(backend): increase default JWT expiration --- example.env | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/example.env b/example.env index 5e544ef..ac126d8 100644 --- a/example.env +++ b/example.env @@ -11,7 +11,7 @@ POSTGRES_DIR= # JWT JWT_SECRET_KEY= -JWT_ACCESS_TOKEN_EXPIRES=1200 +JWT_ACCESS_TOKEN_EXPIRES=28800 # Models HF_HOME=/models/huggingface -- 2.49.1 From e2ac4495fd76212847247291e18cd4444b959736 Mon Sep 17 00:00:00 2001 From: Dylan De Faoite Date: Tue, 3 Mar 2026 20:13:13 +0000 Subject: [PATCH 15/24] chore(frontend): add extra types to frontend --- frontend/src/types/ApiTypes.ts | 20 +++++++++++++------- server/analysis/stat_gen.py | 4 +--- 2 files changed, 14 insertions(+), 10 deletions(-) diff --git a/frontend/src/types/ApiTypes.ts b/frontend/src/types/ApiTypes.ts index 7153720..6bf9c2d 100644 --- a/frontend/src/types/ApiTypes.ts +++ b/frontend/src/types/ApiTypes.ts @@ -10,12 +10,6 @@ type FrequencyWord = { count: number; } -type AverageEmotionByTopic = { - topic: string; - n: number; - [emotion: string]: string | number; -} - type Vocab = { author: string; events: number; @@ -58,13 +52,25 @@ type HeatmapCell = { type TimeAnalysisResponse = { events_per_day: EventsPerDay[]; weekday_hour_heatmap: HeatmapCell[]; - burstiness: number; } // Content Analysis +type NGram = { + count: number; + ngram: string; +} + +type AverageEmotionByTopic = { + topic: string; + n: number; + [emotion: string]: string | number; +} + type ContentAnalysisResponse = { word_frequencies: FrequencyWord[]; average_emotion_by_topic: AverageEmotionByTopic[]; + common_three_phrases: NGram[]; + common_two_phrases: NGram[]; } // Summary diff --git a/server/analysis/stat_gen.py b/server/analysis/stat_gen.py index f9d8344..458cb6b 100644 --- a/server/analysis/stat_gen.py +++ b/server/analysis/stat_gen.py @@ -108,9 +108,7 @@ class StatGen: return { "top_users": self.interaction_analysis.top_users(filtered_df), "users": self.interaction_analysis.per_user_analysis(filtered_df), - "interaction_graph": self.interaction_analysis.interaction_graph( - filtered_df - ), + "interaction_graph": self.interaction_analysis.interaction_graph(filtered_df) } def get_interactional_analysis(self, df: pd.DataFrame, filters: dict | None = None) -> dict: -- 2.49.1 From bd0e1a9050a823603950f020f07aa326cd141faa Mon Sep 17 00:00:00 2001 From: Dylan De Faoite Date: Tue, 3 Mar 2026 20:28:23 +0000 Subject: [PATCH 16/24] refactor(frontend): move stylings out of logic into centralized file --- frontend/src/components/AppLayout.tsx | 26 +--- frontend/src/components/Card.tsx | 43 ++---- frontend/src/components/EmotionalStats.tsx | 22 +-- frontend/src/components/UserModal.tsx | 23 +-- frontend/src/index.css | 27 ++++ frontend/src/pages/DatasetStatus.tsx | 7 +- frontend/src/pages/Datasets.tsx | 23 ++- frontend/src/pages/Login.tsx | 90 +++--------- frontend/src/pages/Stats.tsx | 6 +- frontend/src/pages/Upload.tsx | 27 ++-- frontend/src/styles/stats/appLayout.ts | 42 ++++++ frontend/src/styles/stats/auth.ts | 92 ++++++++++++ frontend/src/styles/stats/cards.ts | 42 ++++++ frontend/src/styles/stats/datasets.ts | 55 ++++++++ frontend/src/styles/stats/emotional.ts | 51 +++++++ frontend/src/styles/stats/feedback.ts | 34 +++++ frontend/src/styles/stats/foundations.ts | 157 +++++++++++++++++++++ frontend/src/styles/stats/modal.ts | 28 ++++ frontend/src/styles/stats/palette.ts | 26 ++++ frontend/src/styles/stats/types.ts | 3 + frontend/src/styles/stats_styling.tsx | 148 +++---------------- 21 files changed, 652 insertions(+), 320 deletions(-) create mode 100644 frontend/src/styles/stats/appLayout.ts create mode 100644 frontend/src/styles/stats/auth.ts create mode 100644 frontend/src/styles/stats/cards.ts create mode 100644 frontend/src/styles/stats/datasets.ts create mode 100644 frontend/src/styles/stats/emotional.ts create mode 100644 frontend/src/styles/stats/feedback.ts create mode 100644 frontend/src/styles/stats/foundations.ts create mode 100644 frontend/src/styles/stats/modal.ts create mode 100644 frontend/src/styles/stats/palette.ts create mode 100644 frontend/src/styles/stats/types.ts diff --git a/frontend/src/components/AppLayout.tsx b/frontend/src/components/AppLayout.tsx index 832515f..5aea6f2 100644 --- a/frontend/src/components/AppLayout.tsx +++ b/frontend/src/components/AppLayout.tsx @@ -91,36 +91,24 @@ const AppLayout = () => { }; return ( -
-
+
+
-
- +
+ Ethnograph View {isSignedIn ? `Signed in: ${getUserLabel(currentUser)}` : "Not signed in"}
-
+
-
Analytics Dashboard
-
Dataset #{datasetId ?? "-"}
+
Analytics Dashboard
+
Dataset #{datasetId ?? "-"}
-
+
- - -
- + }
@@ -239,8 +239,8 @@ return (
)} - {activeView === "interaction" && userData && ( - + {activeView === "user" && userData && ( + )}
-- 2.49.1