Fix the frontend API calls and implement logins on frontend #7

Merged
dylan merged 24 commits from feat/update-frontend-api-calls into main 2026-03-04 20:20:50 +00:00
21 changed files with 652 additions and 320 deletions
Showing only changes of commit bd0e1a9050 - Show all commits

View File

@@ -91,36 +91,24 @@ const AppLayout = () => {
};
return (
<div
style={{
minHeight: "100vh",
background: "#f6f7fb",
fontFamily: styles.page.fontFamily,
color: "#111827",
}}
>
<div style={{ ...styles.container, padding: "16px 24px 0" }}>
<div style={styles.appShell}>
<div style={{ ...styles.container, ...styles.appHeaderWrap }}>
<div style={{ ...styles.card, ...styles.headerBar }}>
<div style={{ display: "flex", alignItems: "center", gap: 10, flexWrap: "wrap" }}>
<span style={{ margin: 0, color: "#111827", fontSize: 18, fontWeight: 700 }}>
<div style={styles.appHeaderBrandRow}>
<span style={styles.appTitle}>
Ethnograph View
</span>
<span
style={{
padding: "4px 10px",
borderRadius: 999,
fontSize: 12,
fontWeight: 700,
fontFamily: styles.page.fontFamily,
background: isSignedIn ? "#dcfce7" : "#fee2e2",
color: isSignedIn ? "#166534" : "#991b1b",
...styles.authStatusBadge,
...(isSignedIn ? styles.authStatusSignedIn : styles.authStatusSignedOut),
}}
>
{isSignedIn ? `Signed in: ${getUserLabel(currentUser)}` : "Not signed in"}
</span>
</div>
<div style={{ ...styles.controls, flexWrap: "wrap" }}>
<div style={styles.controlsWrapped}>
<button
type="button"
style={location.pathname === "/upload" ? styles.buttonPrimary : styles.buttonSecondary}

View File

@@ -1,4 +1,7 @@
import type { CSSProperties } from "react";
import StatsStyling from "../styles/stats_styling";
const styles = StatsStyling;
const Card = (props: {
label: string;
@@ -8,45 +11,17 @@ const Card = (props: {
style?: CSSProperties
}) => {
return (
<div style={{
background: "rgba(255,255,255,0.85)",
border: "1px solid rgba(15,23,42,0.08)",
borderRadius: 16,
padding: 14,
boxShadow: "0 12px 30px rgba(15,23,42,0.06)",
minHeight: 88,
...props.style
}}>
<div style={ {
display: "flex",
justifyContent: "space-between",
alignItems: "center",
gap: 10,
}}>
<div style={{
fontSize: 12,
fontWeight: 700,
color: "rgba(15, 23, 42, 0.65)",
letterSpacing: "0.02em",
textTransform: "uppercase"
}}>
<div style={{ ...styles.cardBase, ...props.style }}>
<div style={styles.cardTopRow}>
<div style={styles.cardLabel}>
{props.label}
</div>
{props.rightSlot ? <div>{props.rightSlot}</div> : null}
</div>
<div style={{
fontSize: 22,
fontWeight: 850,
marginTop: 6,
letterSpacing: "-0.02em",
}}>{props.value}</div>
{props.sublabel ? <div style={{
marginTop: 6,
fontSize: 12,
color: "rgba(15, 23, 42, 0.55)",
}}>{props.sublabel}</div> : null}
<div style={styles.cardValue}>{props.value}</div>
{props.sublabel ? <div style={styles.cardSubLabel}>{props.sublabel}</div> : null}
</div>
);
}
export default Card;
export default Card;

View File

@@ -66,11 +66,11 @@ const EmotionalStats = ({contentData}: EmotionalStatsProps) => {
<div style={{ ...styles.container, ...styles.card, marginTop: 16 }}>
<h2 style={styles.sectionTitle}>Average Emotion by Topic</h2>
<p style={styles.sectionSubtitle}>Read confidence together with sample size. Topics with fewer than {lowSampleThreshold} events are usually noisy and less reliable.</p>
<div style={{ display: "flex", flexWrap: "wrap", gap: 10, fontSize: 13, color: "#4b5563", marginTop: 6 }}>
<span><strong style={{ color: "#111827" }}>Topics:</strong> {strongestPerTopic.length}</span>
<span><strong style={{ color: "#111827" }}>Median Sample:</strong> {medianSampleSize} events</span>
<span><strong style={{ color: "#111827" }}>Low Sample (&lt;{lowSampleThreshold}):</strong> {lowSampleTopics}</span>
<span><strong style={{ color: "#111827" }}>Stable Sample ({stableSampleThreshold}+):</strong> {stableSampleTopics}</span>
<div style={styles.emotionalSummaryRow}>
<span><strong style={{ color: "#24292f" }}>Topics:</strong> {strongestPerTopic.length}</span>
<span><strong style={{ color: "#24292f" }}>Median Sample:</strong> {medianSampleSize} events</span>
<span><strong style={{ color: "#24292f" }}>Low Sample (&lt;{lowSampleThreshold}):</strong> {lowSampleTopics}</span>
<span><strong style={{ color: "#24292f" }}>Stable Sample ({stableSampleThreshold}+):</strong> {stableSampleTopics}</span>
</div>
<p style={{ ...styles.sectionSubtitle, marginTop: 10, marginBottom: 0 }}>
Confidence reflects how strongly one emotion leads within a topic, not model accuracy. Use larger samples for stronger conclusions.
@@ -81,19 +81,19 @@ const EmotionalStats = ({contentData}: EmotionalStatsProps) => {
{strongestPerTopic.map((topic) => (
<div key={topic.topic} style={{ ...styles.card, gridColumn: "span 4" }}>
<h3 style={{ ...styles.sectionTitle, marginBottom: 6 }}>{topic.topic}</h3>
<div style={{ fontSize: 12, fontWeight: 700, color: "#6b7280", letterSpacing: "0.02em", textTransform: "uppercase" }}>
<div style={styles.emotionalTopicLabel}>
Top Emotion
</div>
<div style={{ fontSize: 24, fontWeight: 800, marginTop: 4, lineHeight: 1.2 }}>
<div style={styles.emotionalTopicValue}>
{formatEmotion(topic.emotion)}
</div>
<div style={{ display: "flex", justifyContent: "space-between", alignItems: "center", marginTop: 10, fontSize: 13, color: "#6b7280" }}>
<div style={styles.emotionalMetricRow}>
<span>Confidence</span>
<span style={{ fontWeight: 700, color: "#111827" }}>{topic.value.toFixed(3)}</span>
<span style={styles.emotionalMetricValue}>{topic.value.toFixed(3)}</span>
</div>
<div style={{ display: "flex", justifyContent: "space-between", alignItems: "center", marginTop: 4, fontSize: 13, color: "#6b7280" }}>
<div style={styles.emotionalMetricRowCompact}>
<span>Sample Size</span>
<span style={{ fontWeight: 700, color: "#111827" }}>{topic.count} events</span>
<span style={styles.emotionalMetricValue}>{topic.count} events</span>
</div>
</div>
))}

View File

@@ -13,26 +13,11 @@ type Props = {
export default function UserModal({ open, onClose, userData, username }: Props) {
return (
<Dialog open={open} onClose={onClose} style={{ position: "relative", zIndex: 50 }}>
<div
style={{
position: "fixed",
inset: 0,
background: "rgba(0,0,0,0.45)",
}}
/>
<Dialog open={open} onClose={onClose} style={styles.modalRoot}>
<div style={styles.modalBackdrop} />
<div
style={{
position: "fixed",
inset: 0,
display: "flex",
alignItems: "center",
justifyContent: "center",
padding: 16,
}}
>
<DialogPanel style={{ ...styles.card, width: "min(520px, 95vw)" }}>
<div style={styles.modalContainer}>
<DialogPanel style={{ ...styles.card, ...styles.modalPanel }}>
<div style={styles.headerBar}>
<div>
<DialogTitle style={styles.sectionTitle}>{username}</DialogTitle>

View File

@@ -1,4 +1,8 @@
:root {
--bg-default: #f6f8fa;
--text-default: #24292f;
--border-default: #d0d7de;
--focus-ring: rgba(9, 105, 218, 0.22);
font-synthesis: none;
text-rendering: optimizeLegibility;
-webkit-font-smoothing: antialiased;
@@ -14,4 +18,27 @@ body,
body {
margin: 0;
background: var(--bg-default);
color: var(--text-default);
font-family: "IBM Plex Sans", "Noto Sans", "Liberation Sans", "Segoe UI", sans-serif;
}
* {
box-sizing: border-box;
}
button,
input,
select,
textarea {
font: inherit;
}
input:focus,
button:focus-visible,
select:focus,
textarea:focus {
border-color: #0969da;
box-shadow: 0 0 0 3px var(--focus-ring);
outline: none;
}

View File

@@ -78,9 +78,9 @@ const DatasetStatusPage = () => {
return (
<div style={styles.page}>
<div style={{ ...styles.container, maxWidth: 720 }}>
<div style={styles.containerNarrow}>
<div style={{ ...styles.card, marginTop: 28 }}>
<h1 style={{ margin: 0, fontSize: 28, color: "#111827" }}>
<h1 style={styles.sectionHeaderTitle}>
{isProcessing ? "Processing dataset..." : isError ? "Dataset processing failed" : "Dataset ready"}
</h1>
@@ -94,11 +94,10 @@ const DatasetStatusPage = () => {
<div
style={{
...styles.card,
marginTop: 12,
...styles.statusMessageCard,
borderColor: isError ? "rgba(185, 28, 28, 0.28)" : "rgba(0,0,0,0.06)",
background: isError ? "#fff5f5" : "#ffffff",
color: isError ? "#991b1b" : "#374151",
boxShadow: "none",
}}
>
{statusMessage || (isProcessing ? "Waiting for updates from the worker queue..." : "No details provided.")}

View File

@@ -55,11 +55,11 @@ const DatasetsPage = () => {
return (
<div style={styles.page}>
<div style={{ ...styles.container, maxWidth: 1100 }}>
<div style={styles.containerWide}>
<div style={{ ...styles.card, ...styles.headerBar }}>
<div>
<h1 style={{ margin: 0, color: "#111827", fontSize: 28 }}>My Datasets</h1>
<p style={{ margin: "8px 0 0", color: "#6b7280", fontSize: 14 }}>
<h1 style={styles.sectionHeaderTitle}>My Datasets</h1>
<p style={styles.sectionHeaderSubtitle}>
View and reopen datasets you previously uploaded.
</p>
</div>
@@ -91,7 +91,7 @@ const DatasetsPage = () => {
{!error && datasets.length > 0 && (
<div style={{ ...styles.card, marginTop: 14, padding: 0, overflow: "hidden" }}>
<ul style={{ listStyle: "none", margin: 0, padding: 0 }}>
<ul style={styles.listNoBullets}>
{datasets.map((dataset) => {
const isComplete = dataset.status === "complete";
const targetPath = isComplete
@@ -101,24 +101,17 @@ const DatasetsPage = () => {
return (
<li
key={dataset.id}
style={{
display: "flex",
alignItems: "center",
justifyContent: "space-between",
gap: 12,
padding: "14px 16px",
borderBottom: "1px solid rgba(0,0,0,0.06)",
}}
style={styles.datasetListItem}
>
<div style={{ minWidth: 0 }}>
<div style={{ fontWeight: 700, color: "#111827" }}>
<div style={styles.datasetName}>
{dataset.name || `Dataset #${dataset.id}`}
</div>
<div style={{ fontSize: 13, color: "#6b7280", marginTop: 4 }}>
<div style={styles.datasetMeta}>
ID #{dataset.id} Status: {dataset.status || "unknown"}
</div>
{dataset.status_message && (
<div style={{ fontSize: 13, color: "#6b7280", marginTop: 2 }}>
<div style={styles.datasetMetaSecondary}>
{dataset.status_message}
</div>
)}

View File

@@ -6,11 +6,6 @@ import StatsStyling from "../styles/stats_styling";
const API_BASE_URL = import.meta.env.VITE_BACKEND_URL
const styles = StatsStyling;
const controlStyle = {
width: "100%",
maxWidth: "100%",
boxSizing: "border-box" as const,
};
const LoginPage = () => {
const navigate = useNavigate();
@@ -77,54 +72,44 @@ const LoginPage = () => {
};
return (
<div style={{ ...styles.container, maxWidth: 560, padding: "48px 24px" }}>
<div
style={{
...styles.card,
padding: 28,
background:
"linear-gradient(180deg, rgba(255,255,255,1) 0%, rgba(248,250,255,1) 100%)",
}}
>
<div style={{ marginBottom: 22, textAlign: "center" }}>
<h1 style={{ margin: 0, color: "#111827", fontSize: 30, lineHeight: 1.1 }}>
<div style={styles.containerAuth}>
<div style={{ ...styles.card, ...styles.authCard }}>
<div style={styles.headingBlock}>
<h1 style={styles.headingXl}>
{isRegisterMode ? "Create your account" : "Welcome back"}
</h1>
<p style={{ margin: "8px 0 0", color: "#6b7280", fontSize: 14 }}>
<p style={styles.mutedText}>
{isRegisterMode
? "Register to start uploading and exploring your dataset insights."
: "Sign in to continue to your analytics workspace."}
</p>
</div>
<form
onSubmit={handleSubmit}
style={{ display: "grid", gap: 12, maxWidth: 380, margin: "0 auto" }}
>
<form onSubmit={handleSubmit} style={styles.authForm}>
<input
type="text"
placeholder="Username"
style={{ ...styles.input, ...controlStyle }}
style={{ ...styles.input, ...styles.authControl }}
value={username}
onChange={(event) => setUsername(event.target.value)}
required
/>
{isRegisterMode && (
<input
type="email"
placeholder="Email"
style={{ ...styles.input, ...controlStyle }}
value={email}
onChange={(event) => setEmail(event.target.value)}
required
<input
type="email"
placeholder="Email"
style={{ ...styles.input, ...styles.authControl }}
value={email}
onChange={(event) => setEmail(event.target.value)}
required
/>
)}
<input
type="password"
placeholder="Password"
style={{ ...styles.input, ...controlStyle }}
style={{ ...styles.input, ...styles.authControl }}
value={password}
onChange={(event) => setPassword(event.target.value)}
required
@@ -132,7 +117,7 @@ const LoginPage = () => {
<button
type="submit"
style={{ ...styles.buttonPrimary, ...controlStyle, marginTop: 2 }}
style={{ ...styles.buttonPrimary, ...styles.authControl, marginTop: 2 }}
disabled={loading}
>
{loading
@@ -144,57 +129,24 @@ const LoginPage = () => {
</form>
{error && (
<p
style={{
color: "#b91c1c",
margin: "12px auto 0",
fontSize: 14,
maxWidth: 380,
textAlign: "center",
}}
>
<p style={styles.authErrorText}>
{error}
</p>
)}
{info && (
<p
style={{
color: "#166534",
margin: "12px auto 0",
fontSize: 14,
maxWidth: 380,
textAlign: "center",
}}
>
<p style={styles.authInfoText}>
{info}
</p>
)}
<div
style={{
marginTop: 16,
display: "flex",
alignItems: "center",
justifyContent: "center",
gap: 8,
flexWrap: "wrap",
}}
>
<span style={{ color: "#6b7280", fontSize: 14 }}>
<div style={styles.authSwitchRow}>
<span style={styles.authSwitchLabel}>
{isRegisterMode ? "Already have an account?" : "New here?"}
</span>
<button
type="button"
style={{
border: "none",
background: "transparent",
color: "#2563eb",
fontSize: 14,
fontWeight: 600,
cursor: "pointer",
padding: 0,
}}
style={styles.authSwitchButton}
onClick={() => {
setError("");
setInfo("");

View File

@@ -174,11 +174,11 @@ return (
</button>
</div>
<div style={{ fontSize: 13, color: "#6b7280" }}>Analytics Dashboard</div>
<div style={{ fontSize: 13, color: "#6b7280" }}>Dataset #{datasetId ?? "-"}</div>
<div style={styles.dashboardMeta}>Analytics Dashboard</div>
<div style={styles.dashboardMeta}>Dataset #{datasetId ?? "-"}</div>
</div>
<div style={{ ...styles.container, display: "flex", gap: 8, marginTop: 12 }}>
<div style={{ ...styles.container, ...styles.tabsRow }}>
<button
onClick={() => setActiveView("summary")}
style={activeView === "summary" ? styles.buttonPrimary : styles.buttonSecondary}

View File

@@ -70,11 +70,11 @@ const UploadPage = () => {
return (
<div style={styles.page}>
<div style={{ ...styles.container, maxWidth: 1100 }}>
<div style={styles.containerWide}>
<div style={{ ...styles.card, ...styles.headerBar }}>
<div>
<h1 style={{ margin: 0, color: "#111827", fontSize: 28 }}>Upload Dataset</h1>
<p style={{ margin: "8px 0 0", color: "#6b7280", fontSize: 14 }}>
<h1 style={styles.sectionHeaderTitle}>Upload Dataset</h1>
<p style={styles.sectionHeaderSubtitle}>
Name your dataset, then upload posts and topic map files to generate analytics.
</p>
</div>
@@ -96,10 +96,10 @@ const UploadPage = () => {
}}
>
<div style={{ ...styles.card, gridColumn: "auto" }}>
<h2 style={{ ...styles.sectionTitle, color: "#111827" }}>Dataset Name</h2>
<h2 style={{ ...styles.sectionTitle, color: "#24292f" }}>Dataset Name</h2>
<p style={styles.sectionSubtitle}>Use a clear label so you can identify this upload later.</p>
<input
style={{ ...styles.input, width: "100%", maxWidth: "100%", boxSizing: "border-box" }}
style={{ ...styles.input, ...styles.inputFullWidth }}
type="text"
placeholder="Example: Cork Discussions - Jan 2026"
value={datasetName}
@@ -108,29 +108,29 @@ const UploadPage = () => {
</div>
<div style={{ ...styles.card, gridColumn: "auto" }}>
<h2 style={{ ...styles.sectionTitle, color: "#111827" }}>Posts File (.jsonl)</h2>
<h2 style={{ ...styles.sectionTitle, color: "#24292f" }}>Posts File (.jsonl)</h2>
<p style={styles.sectionSubtitle}>Upload the raw post records export.</p>
<input
style={{ ...styles.input, width: "100%", maxWidth: "100%", boxSizing: "border-box" }}
style={{ ...styles.input, ...styles.inputFullWidth }}
type="file"
accept=".jsonl"
onChange={(event) => setPostFile(event.target.files?.[0] ?? null)}
/>
<p style={{ margin: "10px 0 0", fontSize: 13, color: "#374151" }}>
<p style={styles.subtleBodyText}>
{postFile ? `Selected: ${postFile.name}` : "No file selected"}
</p>
</div>
<div style={{ ...styles.card, gridColumn: "auto" }}>
<h2 style={{ ...styles.sectionTitle, color: "#111827" }}>Topics File (.json)</h2>
<h2 style={{ ...styles.sectionTitle, color: "#24292f" }}>Topics File (.json)</h2>
<p style={styles.sectionSubtitle}>Upload your topic bucket mapping file.</p>
<input
style={{ ...styles.input, width: "100%", maxWidth: "100%", boxSizing: "border-box" }}
style={{ ...styles.input, ...styles.inputFullWidth }}
type="file"
accept=".json"
onChange={(event) => setTopicBucketFile(event.target.files?.[0] ?? null)}
/>
<p style={{ margin: "10px 0 0", fontSize: 13, color: "#374151" }}>
<p style={styles.subtleBodyText}>
{topicBucketFile ? `Selected: ${topicBucketFile.name}` : "No file selected"}
</p>
</div>
@@ -140,10 +140,7 @@ const UploadPage = () => {
style={{
...styles.card,
marginTop: 14,
borderColor: hasError ? "rgba(185, 28, 28, 0.28)" : "rgba(0,0,0,0.06)",
background: hasError ? "#fff5f5" : "#ffffff",
color: hasError ? "#991b1b" : "#374151",
fontSize: 14,
...(hasError ? styles.alertCardError : styles.alertCardInfo),
}}
>
{returnMessage || "After upload, your dataset is queued for processing and you'll land on stats."}

View File

@@ -0,0 +1,42 @@
import { palette } from "./palette";
import type { StyleMap } from "./types";
export const appLayoutStyles: StyleMap = {
appHeaderWrap: {
padding: "16px 24px 0",
},
appHeaderBrandRow: {
display: "flex",
alignItems: "center",
gap: 10,
flexWrap: "wrap",
},
appTitle: {
margin: 0,
color: palette.textPrimary,
fontSize: 18,
fontWeight: 600,
},
authStatusBadge: {
padding: "3px 8px",
borderRadius: 6,
fontSize: 12,
fontWeight: 600,
fontFamily: '"IBM Plex Sans", "Noto Sans", "Liberation Sans", "Segoe UI", sans-serif',
},
authStatusSignedIn: {
border: `1px solid ${palette.statusPositiveBorder}`,
background: palette.statusPositiveBg,
color: palette.statusPositiveText,
},
authStatusSignedOut: {
border: `1px solid ${palette.statusNegativeBorder}`,
background: palette.statusNegativeBg,
color: palette.statusNegativeText,
},
};

View File

@@ -0,0 +1,92 @@
import { palette } from "./palette";
import type { StyleMap } from "./types";
export const authStyles: StyleMap = {
containerAuth: {
maxWidth: 560,
margin: "0 auto",
padding: "48px 24px",
},
headingXl: {
margin: 0,
color: palette.textPrimary,
fontSize: 28,
fontWeight: 600,
lineHeight: 1.1,
},
headingBlock: {
marginBottom: 22,
textAlign: "center",
},
mutedText: {
margin: "8px 0 0",
color: palette.textSecondary,
fontSize: 14,
},
authCard: {
padding: 28,
},
authForm: {
display: "grid",
gap: 12,
maxWidth: 380,
margin: "0 auto",
},
inputFullWidth: {
width: "100%",
maxWidth: "100%",
boxSizing: "border-box",
},
authControl: {
width: "100%",
maxWidth: "100%",
boxSizing: "border-box",
},
authErrorText: {
color: palette.dangerText,
margin: "12px auto 0",
fontSize: 14,
maxWidth: 380,
textAlign: "center",
},
authInfoText: {
color: palette.successText,
margin: "12px auto 0",
fontSize: 14,
maxWidth: 380,
textAlign: "center",
},
authSwitchRow: {
marginTop: 16,
display: "flex",
alignItems: "center",
justifyContent: "center",
gap: 8,
flexWrap: "wrap",
},
authSwitchLabel: {
color: palette.textSecondary,
fontSize: 14,
},
authSwitchButton: {
border: "none",
background: "transparent",
color: palette.brandGreenBorder,
fontSize: 14,
fontWeight: 600,
cursor: "pointer",
padding: 0,
},
};

View File

@@ -0,0 +1,42 @@
import { palette } from "./palette";
import type { StyleMap } from "./types";
export const cardStyles: StyleMap = {
cardBase: {
background: palette.surface,
border: `1px solid ${palette.borderDefault}`,
borderRadius: 8,
padding: 14,
boxShadow: `0 1px 0 ${palette.shadowSubtle}`,
minHeight: 88,
},
cardTopRow: {
display: "flex",
justifyContent: "space-between",
alignItems: "center",
gap: 10,
},
cardLabel: {
fontSize: 12,
fontWeight: 600,
color: palette.textSecondary,
letterSpacing: "0.02em",
textTransform: "uppercase",
},
cardValue: {
fontSize: 24,
fontWeight: 700,
marginTop: 6,
letterSpacing: "-0.02em",
color: palette.textPrimary,
},
cardSubLabel: {
marginTop: 6,
fontSize: 12,
color: palette.textSecondary,
},
};

View File

@@ -0,0 +1,55 @@
import { palette } from "./palette";
import type { StyleMap } from "./types";
export const datasetStyles: StyleMap = {
sectionHeaderTitle: {
margin: 0,
color: palette.textPrimary,
fontSize: 28,
fontWeight: 600,
},
sectionHeaderSubtitle: {
margin: "8px 0 0",
color: palette.textSecondary,
fontSize: 14,
},
listNoBullets: {
listStyle: "none",
margin: 0,
padding: 0,
},
datasetListItem: {
display: "flex",
alignItems: "center",
justifyContent: "space-between",
gap: 12,
padding: "14px 16px",
borderBottom: `1px solid ${palette.borderMuted}`,
},
datasetName: {
fontWeight: 600,
color: palette.textPrimary,
},
datasetMeta: {
fontSize: 13,
color: palette.textSecondary,
marginTop: 4,
},
datasetMetaSecondary: {
fontSize: 13,
color: palette.textSecondary,
marginTop: 2,
},
subtleBodyText: {
margin: "10px 0 0",
fontSize: 13,
color: palette.textBody,
},
};

View File

@@ -0,0 +1,51 @@
import { palette } from "./palette";
import type { StyleMap } from "./types";
export const emotionalStyles: StyleMap = {
emotionalSummaryRow: {
display: "flex",
flexWrap: "wrap",
gap: 10,
fontSize: 13,
color: palette.textTertiary,
marginTop: 6,
},
emotionalTopicLabel: {
fontSize: 12,
fontWeight: 600,
color: palette.textSecondary,
letterSpacing: "0.02em",
textTransform: "uppercase",
},
emotionalTopicValue: {
fontSize: 24,
fontWeight: 800,
marginTop: 4,
lineHeight: 1.2,
},
emotionalMetricRow: {
display: "flex",
justifyContent: "space-between",
alignItems: "center",
marginTop: 10,
fontSize: 13,
color: palette.textSecondary,
},
emotionalMetricRowCompact: {
display: "flex",
justifyContent: "space-between",
alignItems: "center",
marginTop: 4,
fontSize: 13,
color: palette.textSecondary,
},
emotionalMetricValue: {
fontWeight: 600,
color: palette.textPrimary,
},
};

View File

@@ -0,0 +1,34 @@
import { palette } from "./palette";
import type { StyleMap } from "./types";
export const feedbackStyles: StyleMap = {
alertCardError: {
borderColor: palette.alertErrorBorder,
background: palette.alertErrorBg,
color: palette.alertErrorText,
fontSize: 14,
},
alertCardInfo: {
borderColor: palette.alertInfoBorder,
background: palette.surface,
color: palette.textBody,
fontSize: 14,
},
statusMessageCard: {
marginTop: 12,
boxShadow: "none",
},
dashboardMeta: {
fontSize: 13,
color: palette.textSecondary,
},
tabsRow: {
display: "flex",
gap: 8,
marginTop: 12,
},
};

View File

@@ -0,0 +1,157 @@
import { palette } from "./palette";
import type { StyleMap } from "./types";
export const foundationStyles: StyleMap = {
appShell: {
minHeight: "100vh",
background: palette.canvas,
fontFamily: '"IBM Plex Sans", "Noto Sans", "Liberation Sans", "Segoe UI", sans-serif',
color: palette.textPrimary,
},
page: {
width: "100%",
minHeight: "100vh",
padding: 20,
background: palette.canvas,
fontFamily: '"IBM Plex Sans", "Noto Sans", "Liberation Sans", "Segoe UI", sans-serif',
color: palette.textPrimary,
overflowX: "hidden",
boxSizing: "border-box",
},
container: {
maxWidth: 1240,
margin: "0 auto",
},
containerWide: {
maxWidth: 1100,
margin: "0 auto",
},
containerNarrow: {
maxWidth: 720,
margin: "0 auto",
},
card: {
background: palette.surface,
borderRadius: 8,
padding: 16,
border: `1px solid ${palette.borderDefault}`,
boxShadow: `0 1px 0 ${palette.shadowSubtle}`,
},
headerBar: {
display: "flex",
flexWrap: "wrap",
alignItems: "center",
justifyContent: "space-between",
gap: 10,
},
controls: {
display: "flex",
gap: 8,
alignItems: "center",
},
controlsWrapped: {
display: "flex",
gap: 8,
alignItems: "center",
flexWrap: "wrap",
},
input: {
width: 280,
maxWidth: "70vw",
padding: "8px 10px",
borderRadius: 6,
border: `1px solid ${palette.borderDefault}`,
outline: "none",
fontSize: 14,
background: palette.surface,
color: palette.textPrimary,
},
buttonPrimary: {
padding: "8px 12px",
borderRadius: 6,
border: `1px solid ${palette.brandGreenBorder}`,
background: palette.brandGreen,
color: palette.surface,
fontWeight: 600,
cursor: "pointer",
boxShadow: "none",
},
buttonSecondary: {
padding: "8px 12px",
borderRadius: 6,
border: `1px solid ${palette.borderDefault}`,
background: palette.canvas,
color: palette.textPrimary,
fontWeight: 600,
cursor: "pointer",
},
grid: {
marginTop: 12,
display: "grid",
gridTemplateColumns: "repeat(12, 1fr)",
gap: 12,
},
sectionTitle: {
margin: 0,
fontSize: 17,
fontWeight: 600,
},
sectionSubtitle: {
margin: "6px 0 14px",
fontSize: 13,
color: palette.textSecondary,
},
chartWrapper: {
width: "100%",
height: 350,
},
heatmapWrapper: {
width: "100%",
height: 320,
},
topUsersList: {
display: "flex",
flexDirection: "column",
gap: 10,
},
topUserItem: {
padding: "10px 12px",
borderRadius: 8,
background: palette.canvas,
border: `1px solid ${palette.borderMuted}`,
},
topUserName: {
fontWeight: 600,
fontSize: 14,
color: palette.textPrimary,
},
topUserMeta: {
fontSize: 13,
color: palette.textSecondary,
},
scrollArea: {
maxHeight: 420,
overflowY: "auto",
},
};

View File

@@ -0,0 +1,28 @@
import { palette } from "./palette";
import type { StyleMap } from "./types";
export const modalStyles: StyleMap = {
modalRoot: {
position: "relative",
zIndex: 50,
},
modalBackdrop: {
position: "fixed",
inset: 0,
background: palette.modalBackdrop,
},
modalContainer: {
position: "fixed",
inset: 0,
display: "flex",
alignItems: "center",
justifyContent: "center",
padding: 16,
},
modalPanel: {
width: "min(520px, 95vw)",
},
};

View File

@@ -0,0 +1,26 @@
export const palette = {
canvas: "#f6f8fa",
surface: "#ffffff",
textPrimary: "#24292f",
textSecondary: "#57606a",
textTertiary: "#4b5563",
textBody: "#374151",
borderDefault: "#d0d7de",
borderMuted: "#d8dee4",
shadowSubtle: "rgba(27, 31, 36, 0.04)",
brandGreen: "#2da44e",
brandGreenBorder: "#1f883d",
statusPositiveBorder: "#b7dfc8",
statusPositiveBg: "#edf9f1",
statusPositiveText: "#1f6f43",
statusNegativeBorder: "#f3c1c1",
statusNegativeBg: "#fff2f2",
statusNegativeText: "#9a2929",
dangerText: "#b91c1c",
successText: "#166534",
alertErrorBorder: "rgba(185, 28, 28, 0.28)",
alertErrorBg: "#fff5f5",
alertErrorText: "#991b1b",
alertInfoBorder: "rgba(0,0,0,0.06)",
modalBackdrop: "rgba(0,0,0,0.45)",
} as const;

View File

@@ -0,0 +1,3 @@
import type { CSSProperties } from "react";
export type StyleMap = Record<string, CSSProperties>;

View File

@@ -1,136 +1,22 @@
import type { CSSProperties } from "react";
import { appLayoutStyles } from "./stats/appLayout";
import { authStyles } from "./stats/auth";
import { cardStyles } from "./stats/cards";
import { datasetStyles } from "./stats/datasets";
import { emotionalStyles } from "./stats/emotional";
import { feedbackStyles } from "./stats/feedback";
import { foundationStyles } from "./stats/foundations";
import { modalStyles } from "./stats/modal";
const StatsStyling: Record<string, CSSProperties> = {
page: {
width: "100%",
minHeight: "100vh",
padding: 24,
background: "#f6f7fb",
fontFamily:
'-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Inter, Arial, sans-serif',
color: "#111827",
overflowX: "hidden",
boxSizing: "border-box"
},
container: {
maxWidth: 1400,
margin: "0 auto",
},
card: {
background: "white",
borderRadius: 16,
padding: 16,
border: "1px solid rgba(0,0,0,0.06)",
boxShadow: "0 6px 20px rgba(0,0,0,0.06)",
},
headerBar: {
display: "flex",
flexWrap: "wrap",
alignItems: "center",
justifyContent: "space-between",
gap: 12,
},
controls: {
display: "flex",
gap: 10,
alignItems: "center",
},
input: {
width: 320,
maxWidth: "70vw",
padding: "10px 12px",
borderRadius: 12,
border: "1px solid rgba(0,0,0,0.12)",
outline: "none",
fontSize: 14,
background: "#fff",
color: "black"
},
buttonPrimary: {
padding: "10px 14px",
borderRadius: 12,
border: "1px solid rgba(0,0,0,0.08)",
background: "#2563eb",
color: "white",
fontWeight: 600,
cursor: "pointer",
boxShadow: "0 6px 16px rgba(37,99,235,0.25)",
},
buttonSecondary: {
padding: "10px 14px",
borderRadius: 12,
border: "1px solid rgba(0,0,0,0.12)",
background: "#fff",
color: "#111827",
fontWeight: 600,
cursor: "pointer",
},
grid: {
marginTop: 18,
display: "grid",
gridTemplateColumns: "repeat(12, 1fr)",
gap: 16,
},
sectionTitle: {
margin: 0,
fontSize: 16,
fontWeight: 700,
},
sectionSubtitle: {
margin: "6px 0 14px",
fontSize: 13,
color: "#6b7280",
},
chartWrapper: {
width: "100%",
height: 350,
},
heatmapWrapper: {
width: "100%",
height: 320,
},
topUsersList: {
display: "flex",
flexDirection: "column",
gap: 10,
},
topUserItem: {
padding: "10px 12px",
borderRadius: 12,
background: "#f9fafb",
border: "1px solid rgba(0,0,0,0.06)",
},
topUserName: {
fontWeight: 700,
fontSize: 14,
color: "black"
},
topUserMeta: {
fontSize: 13,
color: "#6b7280",
},
scrollArea: {
maxHeight: 450,
overflowY: "auto",
},
...foundationStyles,
...appLayoutStyles,
...authStyles,
...datasetStyles,
...feedbackStyles,
...cardStyles,
...emotionalStyles,
...modalStyles,
};
export default StatsStyling;
export default StatsStyling;