Fix the frontend API calls and implement logins on frontend #7
@@ -91,36 +91,24 @@ const AppLayout = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div style={styles.appShell}>
|
||||||
style={{
|
<div style={{ ...styles.container, ...styles.appHeaderWrap }}>
|
||||||
minHeight: "100vh",
|
|
||||||
background: "#f6f7fb",
|
|
||||||
fontFamily: styles.page.fontFamily,
|
|
||||||
color: "#111827",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<div style={{ ...styles.container, padding: "16px 24px 0" }}>
|
|
||||||
<div style={{ ...styles.card, ...styles.headerBar }}>
|
<div style={{ ...styles.card, ...styles.headerBar }}>
|
||||||
<div style={{ display: "flex", alignItems: "center", gap: 10, flexWrap: "wrap" }}>
|
<div style={styles.appHeaderBrandRow}>
|
||||||
<span style={{ margin: 0, color: "#111827", fontSize: 18, fontWeight: 700 }}>
|
<span style={styles.appTitle}>
|
||||||
Ethnograph View
|
Ethnograph View
|
||||||
</span>
|
</span>
|
||||||
<span
|
<span
|
||||||
style={{
|
style={{
|
||||||
padding: "4px 10px",
|
...styles.authStatusBadge,
|
||||||
borderRadius: 999,
|
...(isSignedIn ? styles.authStatusSignedIn : styles.authStatusSignedOut),
|
||||||
fontSize: 12,
|
|
||||||
fontWeight: 700,
|
|
||||||
fontFamily: styles.page.fontFamily,
|
|
||||||
background: isSignedIn ? "#dcfce7" : "#fee2e2",
|
|
||||||
color: isSignedIn ? "#166534" : "#991b1b",
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{isSignedIn ? `Signed in: ${getUserLabel(currentUser)}` : "Not signed in"}
|
{isSignedIn ? `Signed in: ${getUserLabel(currentUser)}` : "Not signed in"}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div style={{ ...styles.controls, flexWrap: "wrap" }}>
|
<div style={styles.controlsWrapped}>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
style={location.pathname === "/upload" ? styles.buttonPrimary : styles.buttonSecondary}
|
style={location.pathname === "/upload" ? styles.buttonPrimary : styles.buttonSecondary}
|
||||||
|
|||||||
@@ -1,4 +1,7 @@
|
|||||||
import type { CSSProperties } from "react";
|
import type { CSSProperties } from "react";
|
||||||
|
import StatsStyling from "../styles/stats_styling";
|
||||||
|
|
||||||
|
const styles = StatsStyling;
|
||||||
|
|
||||||
const Card = (props: {
|
const Card = (props: {
|
||||||
label: string;
|
label: string;
|
||||||
@@ -8,45 +11,17 @@ const Card = (props: {
|
|||||||
style?: CSSProperties
|
style?: CSSProperties
|
||||||
}) => {
|
}) => {
|
||||||
return (
|
return (
|
||||||
<div style={{
|
<div style={{ ...styles.cardBase, ...props.style }}>
|
||||||
background: "rgba(255,255,255,0.85)",
|
<div style={styles.cardTopRow}>
|
||||||
border: "1px solid rgba(15,23,42,0.08)",
|
<div style={styles.cardLabel}>
|
||||||
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"
|
|
||||||
}}>
|
|
||||||
{props.label}
|
{props.label}
|
||||||
</div>
|
</div>
|
||||||
{props.rightSlot ? <div>{props.rightSlot}</div> : null}
|
{props.rightSlot ? <div>{props.rightSlot}</div> : null}
|
||||||
</div>
|
</div>
|
||||||
<div style={{
|
<div style={styles.cardValue}>{props.value}</div>
|
||||||
fontSize: 22,
|
{props.sublabel ? <div style={styles.cardSubLabel}>{props.sublabel}</div> : null}
|
||||||
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>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default Card;
|
export default Card;
|
||||||
|
|||||||
@@ -66,11 +66,11 @@ const EmotionalStats = ({contentData}: EmotionalStatsProps) => {
|
|||||||
<div style={{ ...styles.container, ...styles.card, marginTop: 16 }}>
|
<div style={{ ...styles.container, ...styles.card, marginTop: 16 }}>
|
||||||
<h2 style={styles.sectionTitle}>Average Emotion by Topic</h2>
|
<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>
|
<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 }}>
|
<div style={styles.emotionalSummaryRow}>
|
||||||
<span><strong style={{ color: "#111827" }}>Topics:</strong> {strongestPerTopic.length}</span>
|
<span><strong style={{ color: "#24292f" }}>Topics:</strong> {strongestPerTopic.length}</span>
|
||||||
<span><strong style={{ color: "#111827" }}>Median Sample:</strong> {medianSampleSize} events</span>
|
<span><strong style={{ color: "#24292f" }}>Median Sample:</strong> {medianSampleSize} events</span>
|
||||||
<span><strong style={{ color: "#111827" }}>Low Sample (<{lowSampleThreshold}):</strong> {lowSampleTopics}</span>
|
<span><strong style={{ color: "#24292f" }}>Low Sample (<{lowSampleThreshold}):</strong> {lowSampleTopics}</span>
|
||||||
<span><strong style={{ color: "#111827" }}>Stable Sample ({stableSampleThreshold}+):</strong> {stableSampleTopics}</span>
|
<span><strong style={{ color: "#24292f" }}>Stable Sample ({stableSampleThreshold}+):</strong> {stableSampleTopics}</span>
|
||||||
</div>
|
</div>
|
||||||
<p style={{ ...styles.sectionSubtitle, marginTop: 10, marginBottom: 0 }}>
|
<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.
|
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) => (
|
{strongestPerTopic.map((topic) => (
|
||||||
<div key={topic.topic} style={{ ...styles.card, gridColumn: "span 4" }}>
|
<div key={topic.topic} style={{ ...styles.card, gridColumn: "span 4" }}>
|
||||||
<h3 style={{ ...styles.sectionTitle, marginBottom: 6 }}>{topic.topic}</h3>
|
<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
|
Top Emotion
|
||||||
</div>
|
</div>
|
||||||
<div style={{ fontSize: 24, fontWeight: 800, marginTop: 4, lineHeight: 1.2 }}>
|
<div style={styles.emotionalTopicValue}>
|
||||||
{formatEmotion(topic.emotion)}
|
{formatEmotion(topic.emotion)}
|
||||||
</div>
|
</div>
|
||||||
<div style={{ display: "flex", justifyContent: "space-between", alignItems: "center", marginTop: 10, fontSize: 13, color: "#6b7280" }}>
|
<div style={styles.emotionalMetricRow}>
|
||||||
<span>Confidence</span>
|
<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>
|
||||||
<div style={{ display: "flex", justifyContent: "space-between", alignItems: "center", marginTop: 4, fontSize: 13, color: "#6b7280" }}>
|
<div style={styles.emotionalMetricRowCompact}>
|
||||||
<span>Sample Size</span>
|
<span>Sample Size</span>
|
||||||
<span style={{ fontWeight: 700, color: "#111827" }}>{topic.count} events</span>
|
<span style={styles.emotionalMetricValue}>{topic.count} events</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
|
|||||||
@@ -13,26 +13,11 @@ type Props = {
|
|||||||
|
|
||||||
export default function UserModal({ open, onClose, userData, username }: Props) {
|
export default function UserModal({ open, onClose, userData, username }: Props) {
|
||||||
return (
|
return (
|
||||||
<Dialog open={open} onClose={onClose} style={{ position: "relative", zIndex: 50 }}>
|
<Dialog open={open} onClose={onClose} style={styles.modalRoot}>
|
||||||
<div
|
<div style={styles.modalBackdrop} />
|
||||||
style={{
|
|
||||||
position: "fixed",
|
|
||||||
inset: 0,
|
|
||||||
background: "rgba(0,0,0,0.45)",
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<div
|
<div style={styles.modalContainer}>
|
||||||
style={{
|
<DialogPanel style={{ ...styles.card, ...styles.modalPanel }}>
|
||||||
position: "fixed",
|
|
||||||
inset: 0,
|
|
||||||
display: "flex",
|
|
||||||
alignItems: "center",
|
|
||||||
justifyContent: "center",
|
|
||||||
padding: 16,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<DialogPanel style={{ ...styles.card, width: "min(520px, 95vw)" }}>
|
|
||||||
<div style={styles.headerBar}>
|
<div style={styles.headerBar}>
|
||||||
<div>
|
<div>
|
||||||
<DialogTitle style={styles.sectionTitle}>{username}</DialogTitle>
|
<DialogTitle style={styles.sectionTitle}>{username}</DialogTitle>
|
||||||
|
|||||||
@@ -1,4 +1,8 @@
|
|||||||
:root {
|
:root {
|
||||||
|
--bg-default: #f6f8fa;
|
||||||
|
--text-default: #24292f;
|
||||||
|
--border-default: #d0d7de;
|
||||||
|
--focus-ring: rgba(9, 105, 218, 0.22);
|
||||||
font-synthesis: none;
|
font-synthesis: none;
|
||||||
text-rendering: optimizeLegibility;
|
text-rendering: optimizeLegibility;
|
||||||
-webkit-font-smoothing: antialiased;
|
-webkit-font-smoothing: antialiased;
|
||||||
@@ -14,4 +18,27 @@ body,
|
|||||||
|
|
||||||
body {
|
body {
|
||||||
margin: 0;
|
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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -78,9 +78,9 @@ const DatasetStatusPage = () => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div style={styles.page}>
|
<div style={styles.page}>
|
||||||
<div style={{ ...styles.container, maxWidth: 720 }}>
|
<div style={styles.containerNarrow}>
|
||||||
<div style={{ ...styles.card, marginTop: 28 }}>
|
<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"}
|
{isProcessing ? "Processing dataset..." : isError ? "Dataset processing failed" : "Dataset ready"}
|
||||||
</h1>
|
</h1>
|
||||||
|
|
||||||
@@ -94,11 +94,10 @@ const DatasetStatusPage = () => {
|
|||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
...styles.card,
|
...styles.card,
|
||||||
marginTop: 12,
|
...styles.statusMessageCard,
|
||||||
borderColor: isError ? "rgba(185, 28, 28, 0.28)" : "rgba(0,0,0,0.06)",
|
borderColor: isError ? "rgba(185, 28, 28, 0.28)" : "rgba(0,0,0,0.06)",
|
||||||
background: isError ? "#fff5f5" : "#ffffff",
|
background: isError ? "#fff5f5" : "#ffffff",
|
||||||
color: isError ? "#991b1b" : "#374151",
|
color: isError ? "#991b1b" : "#374151",
|
||||||
boxShadow: "none",
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{statusMessage || (isProcessing ? "Waiting for updates from the worker queue..." : "No details provided.")}
|
{statusMessage || (isProcessing ? "Waiting for updates from the worker queue..." : "No details provided.")}
|
||||||
|
|||||||
@@ -55,11 +55,11 @@ const DatasetsPage = () => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div style={styles.page}>
|
<div style={styles.page}>
|
||||||
<div style={{ ...styles.container, maxWidth: 1100 }}>
|
<div style={styles.containerWide}>
|
||||||
<div style={{ ...styles.card, ...styles.headerBar }}>
|
<div style={{ ...styles.card, ...styles.headerBar }}>
|
||||||
<div>
|
<div>
|
||||||
<h1 style={{ margin: 0, color: "#111827", fontSize: 28 }}>My Datasets</h1>
|
<h1 style={styles.sectionHeaderTitle}>My Datasets</h1>
|
||||||
<p style={{ margin: "8px 0 0", color: "#6b7280", fontSize: 14 }}>
|
<p style={styles.sectionHeaderSubtitle}>
|
||||||
View and reopen datasets you previously uploaded.
|
View and reopen datasets you previously uploaded.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
@@ -91,7 +91,7 @@ const DatasetsPage = () => {
|
|||||||
|
|
||||||
{!error && datasets.length > 0 && (
|
{!error && datasets.length > 0 && (
|
||||||
<div style={{ ...styles.card, marginTop: 14, padding: 0, overflow: "hidden" }}>
|
<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) => {
|
{datasets.map((dataset) => {
|
||||||
const isComplete = dataset.status === "complete";
|
const isComplete = dataset.status === "complete";
|
||||||
const targetPath = isComplete
|
const targetPath = isComplete
|
||||||
@@ -101,24 +101,17 @@ const DatasetsPage = () => {
|
|||||||
return (
|
return (
|
||||||
<li
|
<li
|
||||||
key={dataset.id}
|
key={dataset.id}
|
||||||
style={{
|
style={styles.datasetListItem}
|
||||||
display: "flex",
|
|
||||||
alignItems: "center",
|
|
||||||
justifyContent: "space-between",
|
|
||||||
gap: 12,
|
|
||||||
padding: "14px 16px",
|
|
||||||
borderBottom: "1px solid rgba(0,0,0,0.06)",
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
<div style={{ minWidth: 0 }}>
|
<div style={{ minWidth: 0 }}>
|
||||||
<div style={{ fontWeight: 700, color: "#111827" }}>
|
<div style={styles.datasetName}>
|
||||||
{dataset.name || `Dataset #${dataset.id}`}
|
{dataset.name || `Dataset #${dataset.id}`}
|
||||||
</div>
|
</div>
|
||||||
<div style={{ fontSize: 13, color: "#6b7280", marginTop: 4 }}>
|
<div style={styles.datasetMeta}>
|
||||||
ID #{dataset.id} • Status: {dataset.status || "unknown"}
|
ID #{dataset.id} • Status: {dataset.status || "unknown"}
|
||||||
</div>
|
</div>
|
||||||
{dataset.status_message && (
|
{dataset.status_message && (
|
||||||
<div style={{ fontSize: 13, color: "#6b7280", marginTop: 2 }}>
|
<div style={styles.datasetMetaSecondary}>
|
||||||
{dataset.status_message}
|
{dataset.status_message}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -6,11 +6,6 @@ import StatsStyling from "../styles/stats_styling";
|
|||||||
const API_BASE_URL = import.meta.env.VITE_BACKEND_URL
|
const API_BASE_URL = import.meta.env.VITE_BACKEND_URL
|
||||||
|
|
||||||
const styles = StatsStyling;
|
const styles = StatsStyling;
|
||||||
const controlStyle = {
|
|
||||||
width: "100%",
|
|
||||||
maxWidth: "100%",
|
|
||||||
boxSizing: "border-box" as const,
|
|
||||||
};
|
|
||||||
|
|
||||||
const LoginPage = () => {
|
const LoginPage = () => {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
@@ -77,54 +72,44 @@ const LoginPage = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div style={{ ...styles.container, maxWidth: 560, padding: "48px 24px" }}>
|
<div style={styles.containerAuth}>
|
||||||
<div
|
<div style={{ ...styles.card, ...styles.authCard }}>
|
||||||
style={{
|
<div style={styles.headingBlock}>
|
||||||
...styles.card,
|
<h1 style={styles.headingXl}>
|
||||||
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 }}>
|
|
||||||
{isRegisterMode ? "Create your account" : "Welcome back"}
|
{isRegisterMode ? "Create your account" : "Welcome back"}
|
||||||
</h1>
|
</h1>
|
||||||
<p style={{ margin: "8px 0 0", color: "#6b7280", fontSize: 14 }}>
|
<p style={styles.mutedText}>
|
||||||
{isRegisterMode
|
{isRegisterMode
|
||||||
? "Register to start uploading and exploring your dataset insights."
|
? "Register to start uploading and exploring your dataset insights."
|
||||||
: "Sign in to continue to your analytics workspace."}
|
: "Sign in to continue to your analytics workspace."}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<form
|
<form onSubmit={handleSubmit} style={styles.authForm}>
|
||||||
onSubmit={handleSubmit}
|
|
||||||
style={{ display: "grid", gap: 12, maxWidth: 380, margin: "0 auto" }}
|
|
||||||
>
|
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
placeholder="Username"
|
placeholder="Username"
|
||||||
style={{ ...styles.input, ...controlStyle }}
|
style={{ ...styles.input, ...styles.authControl }}
|
||||||
value={username}
|
value={username}
|
||||||
onChange={(event) => setUsername(event.target.value)}
|
onChange={(event) => setUsername(event.target.value)}
|
||||||
required
|
required
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{isRegisterMode && (
|
{isRegisterMode && (
|
||||||
<input
|
<input
|
||||||
type="email"
|
type="email"
|
||||||
placeholder="Email"
|
placeholder="Email"
|
||||||
style={{ ...styles.input, ...controlStyle }}
|
style={{ ...styles.input, ...styles.authControl }}
|
||||||
value={email}
|
value={email}
|
||||||
onChange={(event) => setEmail(event.target.value)}
|
onChange={(event) => setEmail(event.target.value)}
|
||||||
required
|
required
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<input
|
<input
|
||||||
type="password"
|
type="password"
|
||||||
placeholder="Password"
|
placeholder="Password"
|
||||||
style={{ ...styles.input, ...controlStyle }}
|
style={{ ...styles.input, ...styles.authControl }}
|
||||||
value={password}
|
value={password}
|
||||||
onChange={(event) => setPassword(event.target.value)}
|
onChange={(event) => setPassword(event.target.value)}
|
||||||
required
|
required
|
||||||
@@ -132,7 +117,7 @@ const LoginPage = () => {
|
|||||||
|
|
||||||
<button
|
<button
|
||||||
type="submit"
|
type="submit"
|
||||||
style={{ ...styles.buttonPrimary, ...controlStyle, marginTop: 2 }}
|
style={{ ...styles.buttonPrimary, ...styles.authControl, marginTop: 2 }}
|
||||||
disabled={loading}
|
disabled={loading}
|
||||||
>
|
>
|
||||||
{loading
|
{loading
|
||||||
@@ -144,57 +129,24 @@ const LoginPage = () => {
|
|||||||
</form>
|
</form>
|
||||||
|
|
||||||
{error && (
|
{error && (
|
||||||
<p
|
<p style={styles.authErrorText}>
|
||||||
style={{
|
|
||||||
color: "#b91c1c",
|
|
||||||
margin: "12px auto 0",
|
|
||||||
fontSize: 14,
|
|
||||||
maxWidth: 380,
|
|
||||||
textAlign: "center",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{error}
|
{error}
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{info && (
|
{info && (
|
||||||
<p
|
<p style={styles.authInfoText}>
|
||||||
style={{
|
|
||||||
color: "#166534",
|
|
||||||
margin: "12px auto 0",
|
|
||||||
fontSize: 14,
|
|
||||||
maxWidth: 380,
|
|
||||||
textAlign: "center",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{info}
|
{info}
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<div
|
<div style={styles.authSwitchRow}>
|
||||||
style={{
|
<span style={styles.authSwitchLabel}>
|
||||||
marginTop: 16,
|
|
||||||
display: "flex",
|
|
||||||
alignItems: "center",
|
|
||||||
justifyContent: "center",
|
|
||||||
gap: 8,
|
|
||||||
flexWrap: "wrap",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<span style={{ color: "#6b7280", fontSize: 14 }}>
|
|
||||||
{isRegisterMode ? "Already have an account?" : "New here?"}
|
{isRegisterMode ? "Already have an account?" : "New here?"}
|
||||||
</span>
|
</span>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
style={{
|
style={styles.authSwitchButton}
|
||||||
border: "none",
|
|
||||||
background: "transparent",
|
|
||||||
color: "#2563eb",
|
|
||||||
fontSize: 14,
|
|
||||||
fontWeight: 600,
|
|
||||||
cursor: "pointer",
|
|
||||||
padding: 0,
|
|
||||||
}}
|
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setError("");
|
setError("");
|
||||||
setInfo("");
|
setInfo("");
|
||||||
|
|||||||
@@ -174,11 +174,11 @@ return (
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div style={{ fontSize: 13, color: "#6b7280" }}>Analytics Dashboard</div>
|
<div style={styles.dashboardMeta}>Analytics Dashboard</div>
|
||||||
<div style={{ fontSize: 13, color: "#6b7280" }}>Dataset #{datasetId ?? "-"}</div>
|
<div style={styles.dashboardMeta}>Dataset #{datasetId ?? "-"}</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div style={{ ...styles.container, display: "flex", gap: 8, marginTop: 12 }}>
|
<div style={{ ...styles.container, ...styles.tabsRow }}>
|
||||||
<button
|
<button
|
||||||
onClick={() => setActiveView("summary")}
|
onClick={() => setActiveView("summary")}
|
||||||
style={activeView === "summary" ? styles.buttonPrimary : styles.buttonSecondary}
|
style={activeView === "summary" ? styles.buttonPrimary : styles.buttonSecondary}
|
||||||
|
|||||||
@@ -70,11 +70,11 @@ const UploadPage = () => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div style={styles.page}>
|
<div style={styles.page}>
|
||||||
<div style={{ ...styles.container, maxWidth: 1100 }}>
|
<div style={styles.containerWide}>
|
||||||
<div style={{ ...styles.card, ...styles.headerBar }}>
|
<div style={{ ...styles.card, ...styles.headerBar }}>
|
||||||
<div>
|
<div>
|
||||||
<h1 style={{ margin: 0, color: "#111827", fontSize: 28 }}>Upload Dataset</h1>
|
<h1 style={styles.sectionHeaderTitle}>Upload Dataset</h1>
|
||||||
<p style={{ margin: "8px 0 0", color: "#6b7280", fontSize: 14 }}>
|
<p style={styles.sectionHeaderSubtitle}>
|
||||||
Name your dataset, then upload posts and topic map files to generate analytics.
|
Name your dataset, then upload posts and topic map files to generate analytics.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
@@ -96,10 +96,10 @@ const UploadPage = () => {
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div style={{ ...styles.card, gridColumn: "auto" }}>
|
<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>
|
<p style={styles.sectionSubtitle}>Use a clear label so you can identify this upload later.</p>
|
||||||
<input
|
<input
|
||||||
style={{ ...styles.input, width: "100%", maxWidth: "100%", boxSizing: "border-box" }}
|
style={{ ...styles.input, ...styles.inputFullWidth }}
|
||||||
type="text"
|
type="text"
|
||||||
placeholder="Example: Cork Discussions - Jan 2026"
|
placeholder="Example: Cork Discussions - Jan 2026"
|
||||||
value={datasetName}
|
value={datasetName}
|
||||||
@@ -108,29 +108,29 @@ const UploadPage = () => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div style={{ ...styles.card, gridColumn: "auto" }}>
|
<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>
|
<p style={styles.sectionSubtitle}>Upload the raw post records export.</p>
|
||||||
<input
|
<input
|
||||||
style={{ ...styles.input, width: "100%", maxWidth: "100%", boxSizing: "border-box" }}
|
style={{ ...styles.input, ...styles.inputFullWidth }}
|
||||||
type="file"
|
type="file"
|
||||||
accept=".jsonl"
|
accept=".jsonl"
|
||||||
onChange={(event) => setPostFile(event.target.files?.[0] ?? null)}
|
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"}
|
{postFile ? `Selected: ${postFile.name}` : "No file selected"}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div style={{ ...styles.card, gridColumn: "auto" }}>
|
<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>
|
<p style={styles.sectionSubtitle}>Upload your topic bucket mapping file.</p>
|
||||||
<input
|
<input
|
||||||
style={{ ...styles.input, width: "100%", maxWidth: "100%", boxSizing: "border-box" }}
|
style={{ ...styles.input, ...styles.inputFullWidth }}
|
||||||
type="file"
|
type="file"
|
||||||
accept=".json"
|
accept=".json"
|
||||||
onChange={(event) => setTopicBucketFile(event.target.files?.[0] ?? null)}
|
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"}
|
{topicBucketFile ? `Selected: ${topicBucketFile.name}` : "No file selected"}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
@@ -140,10 +140,7 @@ const UploadPage = () => {
|
|||||||
style={{
|
style={{
|
||||||
...styles.card,
|
...styles.card,
|
||||||
marginTop: 14,
|
marginTop: 14,
|
||||||
borderColor: hasError ? "rgba(185, 28, 28, 0.28)" : "rgba(0,0,0,0.06)",
|
...(hasError ? styles.alertCardError : styles.alertCardInfo),
|
||||||
background: hasError ? "#fff5f5" : "#ffffff",
|
|
||||||
color: hasError ? "#991b1b" : "#374151",
|
|
||||||
fontSize: 14,
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{returnMessage || "After upload, your dataset is queued for processing and you'll land on stats."}
|
{returnMessage || "After upload, your dataset is queued for processing and you'll land on stats."}
|
||||||
|
|||||||
42
frontend/src/styles/stats/appLayout.ts
Normal file
42
frontend/src/styles/stats/appLayout.ts
Normal 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,
|
||||||
|
},
|
||||||
|
};
|
||||||
92
frontend/src/styles/stats/auth.ts
Normal file
92
frontend/src/styles/stats/auth.ts
Normal 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,
|
||||||
|
},
|
||||||
|
};
|
||||||
42
frontend/src/styles/stats/cards.ts
Normal file
42
frontend/src/styles/stats/cards.ts
Normal 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,
|
||||||
|
},
|
||||||
|
};
|
||||||
55
frontend/src/styles/stats/datasets.ts
Normal file
55
frontend/src/styles/stats/datasets.ts
Normal 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,
|
||||||
|
},
|
||||||
|
};
|
||||||
51
frontend/src/styles/stats/emotional.ts
Normal file
51
frontend/src/styles/stats/emotional.ts
Normal 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,
|
||||||
|
},
|
||||||
|
};
|
||||||
34
frontend/src/styles/stats/feedback.ts
Normal file
34
frontend/src/styles/stats/feedback.ts
Normal 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,
|
||||||
|
},
|
||||||
|
};
|
||||||
157
frontend/src/styles/stats/foundations.ts
Normal file
157
frontend/src/styles/stats/foundations.ts
Normal 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",
|
||||||
|
},
|
||||||
|
};
|
||||||
28
frontend/src/styles/stats/modal.ts
Normal file
28
frontend/src/styles/stats/modal.ts
Normal 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)",
|
||||||
|
},
|
||||||
|
};
|
||||||
26
frontend/src/styles/stats/palette.ts
Normal file
26
frontend/src/styles/stats/palette.ts
Normal 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;
|
||||||
3
frontend/src/styles/stats/types.ts
Normal file
3
frontend/src/styles/stats/types.ts
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
import type { CSSProperties } from "react";
|
||||||
|
|
||||||
|
export type StyleMap = Record<string, CSSProperties>;
|
||||||
@@ -1,136 +1,22 @@
|
|||||||
import type { CSSProperties } from "react";
|
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> = {
|
const StatsStyling: Record<string, CSSProperties> = {
|
||||||
page: {
|
...foundationStyles,
|
||||||
width: "100%",
|
...appLayoutStyles,
|
||||||
minHeight: "100vh",
|
...authStyles,
|
||||||
padding: 24,
|
...datasetStyles,
|
||||||
background: "#f6f7fb",
|
...feedbackStyles,
|
||||||
fontFamily:
|
...cardStyles,
|
||||||
'-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Inter, Arial, sans-serif',
|
...emotionalStyles,
|
||||||
color: "#111827",
|
...modalStyles,
|
||||||
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",
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export default StatsStyling;
|
export default StatsStyling;
|
||||||
|
|||||||
Reference in New Issue
Block a user