refactor: replace inline styles with new CSS utility classes across various pages.

This commit is contained in:
2025-12-20 16:08:11 +05:30
parent d9f6694b2f
commit c8b4beafff
8 changed files with 364 additions and 93 deletions

View File

@@ -110,6 +110,224 @@ a:hover {
outline-offset: 2px; outline-offset: 2px;
} }
/* =====================================================
UTILITY CLASSES
===================================================== */
/* Spacing - Margin Bottom */
.mb-xs {
margin-bottom: var(--space-xs);
}
.mb-sm {
margin-bottom: var(--space-sm);
}
.mb-md {
margin-bottom: var(--space-md);
}
.mb-lg {
margin-bottom: var(--space-lg);
}
.mb-xl {
margin-bottom: var(--space-xl);
}
/* Spacing - Margin Top */
.mt-xs {
margin-top: var(--space-xs);
}
.mt-sm {
margin-top: var(--space-sm);
}
.mt-md {
margin-top: var(--space-md);
}
.mt-lg {
margin-top: var(--space-lg);
}
.mt-xl {
margin-top: var(--space-xl);
}
/* Spacing - Margin Left */
.ml-xs {
margin-left: var(--space-xs);
}
.ml-sm {
margin-left: var(--space-sm);
}
.ml-md {
margin-left: var(--space-md);
}
/* Spacing - Padding */
.p-sm {
padding: var(--space-sm);
}
.p-md {
padding: var(--space-md);
}
.p-lg {
padding: var(--space-lg);
}
.p-xl {
padding: var(--space-xl);
}
/* Flex Layouts */
.flex {
display: flex;
}
.flex-col {
display: flex;
flex-direction: column;
}
.flex-center {
display: flex;
align-items: center;
justify-content: center;
}
.flex-between {
display: flex;
align-items: center;
justify-content: space-between;
}
.items-center {
align-items: center;
}
.justify-center {
justify-content: center;
}
/* Gaps */
.gap-xs {
gap: var(--space-xs);
}
.gap-sm {
gap: var(--space-sm);
}
.gap-md {
gap: var(--space-md);
}
.gap-lg {
gap: var(--space-lg);
}
/* Text Utilities */
.text-center {
text-align: center;
}
.text-xs {
font-size: 12px;
}
.text-sm {
font-size: 14px;
}
.font-medium {
font-weight: 500;
}
.font-semibold {
font-weight: 600;
}
.uppercase {
text-transform: uppercase;
}
.truncate {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
/* Modal */
.modal-overlay {
position: fixed;
inset: 0;
background-color: rgba(0, 0, 0, 0.5);
display: flex;
align-items: center;
justify-content: center;
z-index: 1000;
}
.modal-content {
max-width: 400px;
text-align: center;
}
/* Display */
.hidden {
display: none;
}
.block {
display: block;
}
/* Width */
.w-full {
width: 100%;
}
.max-w-sm {
max-width: 400px;
}
.max-w-md {
max-width: 600px;
}
.max-w-lg {
max-width: 800px;
}
.max-w-xl {
max-width: 1000px;
}
/* Min Width / Flex */
.min-w-0 {
min-width: 0;
}
.flex-1 {
flex: 1;
}
/* Border */
.border-t {
border-top: 1px solid var(--border);
}
/* =====================================================
END UTILITY CLASSES
===================================================== */
/* Button Base */ /* Button Base */
.btn { .btn {
display: inline-flex; display: inline-flex;
@@ -463,7 +681,7 @@ select.input {
} }
.biomarker-info { .biomarker-info {
flex: 0 0 240px; flex: 0 0 320px;
min-width: 0; min-width: 0;
display: flex; display: flex;
flex-direction: row; flex-direction: row;
@@ -473,13 +691,11 @@ select.input {
.biomarker-info .biomarker-name { .biomarker-info .biomarker-name {
font-size: 14px; font-size: 14px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
} }
.biomarker-info .biomarker-unit { .biomarker-info .biomarker-unit {
font-size: 11px; font-size: 11px;
flex-shrink: 0;
} }
/* Biomarker Scale Bar */ /* Biomarker Scale Bar */
@@ -785,4 +1001,97 @@ select.input {
.page-loading { .page-loading {
padding: var(--space-lg); padding: var(--space-lg);
color: var(--text-secondary); color: var(--text-secondary);
}
/* =====================================================
SOURCES PAGE
===================================================== */
/* Upload Zone */
.upload-zone {
border: 2px dashed var(--border);
border-radius: var(--radius-md);
padding: var(--space-xl);
text-align: center;
cursor: pointer;
transition: all 0.2s;
}
.upload-zone:hover {
border-color: var(--accent);
background-color: color-mix(in srgb, var(--accent) 5%, var(--bg-secondary));
}
.upload-zone.drag-over {
border-color: var(--accent);
background-color: color-mix(in srgb, var(--accent) 5%, var(--bg-secondary));
}
.upload-zone.uploading {
cursor: wait;
}
/* Upload Icon */
.upload-icon {
width: 36px;
height: 36px;
display: block;
margin: 0 auto;
}
/* Source Item */
.source-item {
padding: var(--space-sm) 0;
border-bottom: 1px solid var(--border);
}
/* Status Parsed */
.status-parsed {
color: var(--indicator-normal);
}
/* Icon sizes */
.icon-sm {
width: 14px;
height: 14px;
}
/* =====================================================
DASHBOARD PAGE
===================================================== */
/* Category Card */
.category-card {
padding: 0;
}
/* Category Name */
.category-name {
font-size: 16px;
font-weight: 600;
text-transform: uppercase;
}
/* Collapsible Header Button */
.collapsible-header {
background: none;
border: none;
cursor: pointer;
text-align: left;
color: var(--text-primary);
}
/* Collapse Icon */
.collapse-icon {
width: 18px;
height: 18px;
transition: transform 0.2s;
}
/* =====================================================
INSIGHTS PAGE
===================================================== */
.coming-soon-icon {
font-size: 48px;
} }

View File

@@ -121,7 +121,7 @@ export function AdminPage() {
const isConfigAdmin = (user: User) => user.username === CONFIG_ADMIN_USERNAME const isConfigAdmin = (user: User) => user.username === CONFIG_ADMIN_USERNAME
if (loading) { if (loading) {
return <div style={{ padding: 'var(--space-lg)' }}>Loading...</div> return <div className="p-lg">Loading...</div>
} }
if (!currentUser || currentUser.role !== 'admin') { if (!currentUser || currentUser.role !== 'admin') {
@@ -129,10 +129,10 @@ export function AdminPage() {
} }
return ( return (
<div style={{ padding: 'var(--space-lg)', maxWidth: '1000px', margin: '0 auto' }}> <div className="admin-page p-lg max-w-xl">
<header style={{ marginBottom: 'var(--space-xl)' }}> <header className="mb-xl">
<Link to="/" className="text-secondary text-sm"> Back to Dashboard</Link> <Link to="/" className="text-secondary text-sm"> Back to Dashboard</Link>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginTop: 'var(--space-sm)' }}> <div className="flex-between mt-sm">
<h1>User Management</h1> <h1>User Management</h1>
<button className="btn btn-primary" onClick={() => setShowCreateModal(true)}> <button className="btn btn-primary" onClick={() => setShowCreateModal(true)}>
+ New User + New User
@@ -172,7 +172,7 @@ export function AdminPage() {
</td> </td>
<td className="text-secondary text-sm">{new Date(user.created_at).toLocaleDateString()}</td> <td className="text-secondary text-sm">{new Date(user.created_at).toLocaleDateString()}</td>
<td> <td>
<div style={{ display: 'flex', gap: 'var(--space-xs)' }}> <div className="flex gap-xs">
<button <button
className="btn btn-secondary btn-sm" className="btn btn-secondary btn-sm"
onClick={() => setEditingUser(user)} onClick={() => setEditingUser(user)}
@@ -243,7 +243,7 @@ export function AdminPage() {
<option value="admin">Admin</option> <option value="admin">Admin</option>
</select> </select>
</div> </div>
<div style={{ display: 'flex', gap: 'var(--space-sm)', marginTop: 'var(--space-lg)' }}> <div className="flex gap-sm mt-lg">
<button type="submit" className="btn btn-primary">Create</button> <button type="submit" className="btn btn-primary">Create</button>
<button type="button" className="btn btn-secondary" onClick={() => setShowCreateModal(false)}>Cancel</button> <button type="button" className="btn btn-secondary" onClick={() => setShowCreateModal(false)}>Cancel</button>
</div> </div>
@@ -327,7 +327,7 @@ function EditUserModal({ user, onClose, onSave, setMessage }: EditUserModalProps
placeholder="Full name" placeholder="Full name"
/> />
</div> </div>
<div style={{ display: 'flex', gap: 'var(--space-sm)', marginTop: 'var(--space-lg)' }}> <div className="flex gap-sm mt-lg">
<button type="submit" className="btn btn-primary" disabled={saving}> <button type="submit" className="btn btn-primary" disabled={saving}>
{saving ? 'Saving...' : 'Save'} {saving ? 'Saving...' : 'Save'}
</button> </button>
@@ -419,7 +419,7 @@ function ResetPasswordModal({ user, onClose, onSave, setMessage }: ResetPassword
/> />
</div> </div>
{error && <div className="error-message">{error}</div>} {error && <div className="error-message">{error}</div>}
<div style={{ display: 'flex', gap: 'var(--space-sm)', marginTop: 'var(--space-lg)' }}> <div className="flex gap-sm mt-lg">
<button type="submit" className="btn btn-primary" disabled={saving}> <button type="submit" className="btn btn-primary" disabled={saving}>
{saving ? 'Resetting...' : 'Reset Password'} {saving ? 'Resetting...' : 'Reset Password'}
</button> </button>

View File

@@ -60,53 +60,38 @@ export function DashboardPage() {
</header> </header>
<section> <section>
<h2 style={{ marginBottom: 'var(--space-md)' }}>Biomarker Categories</h2> <h2 className="mb-md">Biomarker Categories</h2>
<div style={{ display: 'flex', flexDirection: 'column', gap: 'var(--space-sm)' }}> <div className="flex-col gap-sm">
{categories.map(category => { {categories.map(category => {
const categoryBiomarkers = getBiomarkersForCategory(category.id) const categoryBiomarkers = getBiomarkersForCategory(category.id)
const isExpanded = expandedCategories.has(category.id) const isExpanded = expandedCategories.has(category.id)
return ( return (
<div key={category.id} className="card" style={{ padding: 0 }}> <div key={category.id} className="card category-card">
<button <button
className="collapsible-header" className="collapsible-header w-full p-md flex-between"
onClick={() => toggleCategory(category.id)} onClick={() => toggleCategory(category.id)}
style={{
width: '100%',
padding: 'var(--space-md)',
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
background: 'none',
border: 'none',
cursor: 'pointer',
textAlign: 'left',
color: 'var(--text-primary)',
}}
> >
<div> <div>
<span style={{ fontSize: '16px', fontWeight: 600, textTransform: 'uppercase' }}>{category.name}</span> <span className="category-name">{category.name}</span>
<span className="text-secondary text-sm" style={{ marginLeft: 'var(--space-sm)' }}> <span className="text-secondary text-sm ml-sm">
({categoryBiomarkers.length} biomarkers) ({categoryBiomarkers.length} biomarkers)
</span> </span>
</div> </div>
<img <img
src="/icons/general/icons8-collapse-arrow-50.png" src="/icons/general/icons8-collapse-arrow-50.png"
alt="expand" alt="expand"
className="theme-icon" className="theme-icon collapse-icon"
style={{ style={{
width: 18,
height: 18,
transition: 'transform 0.2s',
transform: isExpanded ? 'rotate(180deg)' : 'rotate(0)' transform: isExpanded ? 'rotate(180deg)' : 'rotate(0)'
}} }}
/> />
</button> </button>
{isExpanded && ( {isExpanded && (
<div style={{ borderTop: '1px solid var(--border)', padding: 'var(--space-sm)' }}> <div className="category-content border-t p-sm">
{categoryBiomarkers.length === 0 ? ( {categoryBiomarkers.length === 0 ? (
<p className="text-secondary text-sm" style={{ padding: 'var(--space-sm)' }}> <p className="text-secondary text-sm p-sm">
No biomarkers in this category No biomarkers in this category
</p> </p>
) : ( ) : (

View File

@@ -6,8 +6,8 @@ export function InsightsPage() {
<p className="text-secondary">AI-powered analysis of your health data</p> <p className="text-secondary">AI-powered analysis of your health data</p>
</header> </header>
<div className="card" style={{ textAlign: 'center', padding: 'var(--space-xl)' }}> <div className="card text-center p-xl">
<div style={{ fontSize: '48px', marginBottom: 'var(--space-md)' }}>🚀</div> <div className="coming-soon-icon mb-md">🚀</div>
<h3>Coming Soon</h3> <h3>Coming Soon</h3>
<p className="text-secondary"> <p className="text-secondary">
AI-generated health insights and recommendations based on your biomarker data. AI-generated health insights and recommendations based on your biomarker data.

View File

@@ -91,7 +91,7 @@ export function LoginPage() {
</button> </button>
</form> </form>
<p className="text-secondary text-sm" style={{ marginTop: 'var(--space-md)', textAlign: 'center' }}> <p className="text-secondary text-sm mt-md text-center">
Don't have an account? <Link to="/signup">Sign up</Link> Don't have an account? <Link to="/signup">Sign up</Link>
</p> </p>
</div> </div>

View File

@@ -123,7 +123,7 @@ export function ProfilePage() {
} }
return ( return (
<div className="page" style={{ maxWidth: '600px' }}> <div className="page max-w-md">
<header className="page-header"> <header className="page-header">
<h1>Profile</h1> <h1>Profile</h1>
<p className="text-secondary">Manage your account and health information</p> <p className="text-secondary">Manage your account and health information</p>
@@ -131,8 +131,8 @@ export function ProfilePage() {
<form onSubmit={handleSubmit}> <form onSubmit={handleSubmit}>
{/* Account Info */} {/* Account Info */}
<div className="card" style={{ marginBottom: 'var(--space-lg)' }}> <div className="card mb-lg">
<h3 style={{ marginBottom: 'var(--space-md)' }}>Account</h3> <h3 className="mb-md">Account</h3>
<div className="form-group"> <div className="form-group">
<label>Username</label> <label>Username</label>
<input type="text" className="input" value={username} disabled /> <input type="text" className="input" value={username} disabled />
@@ -165,8 +165,8 @@ export function ProfilePage() {
</div> </div>
{/* Physical Info */} {/* Physical Info */}
<div className="card" style={{ marginBottom: 'var(--space-lg)' }}> <div className="card mb-lg">
<h3 style={{ marginBottom: 'var(--space-md)' }}>Physical Info</h3> <h3 className="mb-md">Physical Info</h3>
<div className="form-group"> <div className="form-group">
<label htmlFor="height">Height (cm)</label> <label htmlFor="height">Height (cm)</label>
@@ -214,8 +214,8 @@ export function ProfilePage() {
</div> </div>
{/* Lifestyle */} {/* Lifestyle */}
<div className="card" style={{ marginBottom: 'var(--space-lg)' }}> <div className="card mb-lg">
<h3 style={{ marginBottom: 'var(--space-md)' }}>Lifestyle</h3> <h3 className="mb-md">Lifestyle</h3>
<div className="form-group"> <div className="form-group">
<label htmlFor="diet">Diet</label> <label htmlFor="diet">Diet</label>

View File

@@ -98,7 +98,7 @@ export function SignupPage() {
</button> </button>
</form> </form>
<p className="text-secondary text-sm" style={{ marginTop: 'var(--space-md)', textAlign: 'center' }}> <p className="text-secondary text-sm mt-md text-center">
Already have an account? <Link to="/login">Sign in</Link> Already have an account? <Link to="/login">Sign in</Link>
</p> </p>
</div> </div>

View File

@@ -117,13 +117,13 @@ export function SourcesPage() {
</header> </header>
<div className="card"> <div className="card">
<h3 style={{ marginBottom: 'var(--space-md)' }}>Upload Data</h3> <h3 className="mb-md">Upload Data</h3>
<p className="text-secondary text-sm" style={{ marginBottom: 'var(--space-lg)' }}> <p className="text-secondary text-sm mb-lg">
Upload lab reports in PDF, CSV, or Excel format to import your biomarker data. Upload lab reports in PDF, CSV, or Excel format to import your biomarker data.
</p> </p>
{error && ( {error && (
<div className="alert alert-error" style={{ marginBottom: 'var(--space-md)' }}> <div className="alert alert-error mb-md">
{error} {error}
</div> </div>
)} )}
@@ -131,7 +131,7 @@ export function SourcesPage() {
<input <input
type="file" type="file"
ref={fileInputRef} ref={fileInputRef}
style={{ display: 'none' }} className="hidden"
multiple multiple
accept=".pdf,.csv,.xlsx,.xls,.jpg,.jpeg,.png" accept=".pdf,.csv,.xlsx,.xls,.jpg,.jpeg,.png"
onChange={(e) => handleUpload(e.target.files)} onChange={(e) => handleUpload(e.target.files)}
@@ -139,15 +139,6 @@ export function SourcesPage() {
<div <div
className={`upload-zone ${dragOver ? 'drag-over' : ''}`} className={`upload-zone ${dragOver ? 'drag-over' : ''}`}
style={{
border: `2px dashed ${dragOver ? 'var(--accent)' : 'var(--border)'}`,
borderRadius: 'var(--radius-md)',
padding: 'var(--space-xl)',
textAlign: 'center',
cursor: uploading ? 'wait' : 'pointer',
backgroundColor: dragOver ? 'color-mix(in srgb, var(--accent) 5%, var(--bg-secondary))' : 'transparent',
transition: 'all 0.2s',
}}
onClick={() => !uploading && fileInputRef.current?.click()} onClick={() => !uploading && fileInputRef.current?.click()}
onDragOver={(e) => { e.preventDefault(); setDragOver(true) }} onDragOver={(e) => { e.preventDefault(); setDragOver(true) }}
onDragLeave={() => setDragOver(false)} onDragLeave={() => setDragOver(false)}
@@ -159,20 +150,20 @@ export function SourcesPage() {
> >
{uploading ? ( {uploading ? (
<> <>
<div style={{ marginBottom: 'var(--space-sm)', textAlign: 'center' }}> <div className="mb-sm text-center">
<img src="/icons/general/icons8-clock-50.png" alt="Uploading" className="theme-icon" style={{ width: 36, height: 36, display: 'block', margin: '0 auto' }} /> <img src="/icons/general/icons8-clock-50.png" alt="Uploading" className="upload-icon theme-icon" />
</div> </div>
<p className="text-secondary">Uploading...</p> <p className="text-secondary">Uploading...</p>
</> </>
) : ( ) : (
<> <>
<div style={{ marginBottom: 'var(--space-sm)', textAlign: 'center' }}> <div className="mb-sm text-center">
<img src="/icons/general/icons8-upload-to-the-cloud-50.png" alt="Upload" className="theme-icon" style={{ width: 36, height: 36, display: 'block', margin: '0 auto' }} /> <img src="/icons/general/icons8-upload-to-the-cloud-50.png" alt="Upload" className="upload-icon theme-icon" />
</div> </div>
<p className="text-secondary"> <p className="text-secondary">
Drag & drop files here, or click to browse Drag & drop files here, or click to browse
</p> </p>
<p className="text-secondary text-xs" style={{ marginTop: 'var(--space-sm)' }}> <p className="text-secondary text-xs mt-sm">
Supported: PDF, CSV, XLSX, Images Supported: PDF, CSV, XLSX, Images
</p> </p>
</> </>
@@ -180,8 +171,8 @@ export function SourcesPage() {
</div> </div>
</div> </div>
<div className="card" style={{ marginTop: 'var(--space-lg)' }}> <div className="card mt-lg">
<h3 style={{ marginBottom: 'var(--space-md)' }}>Recent Uploads</h3> <h3 className="mb-md">Recent Uploads</h3>
{loading ? ( {loading ? (
<p className="text-secondary text-sm">Loading...</p> <p className="text-secondary text-sm">Loading...</p>
@@ -190,28 +181,22 @@ export function SourcesPage() {
) : ( ) : (
<div className="sources-list"> <div className="sources-list">
{sources.map(source => ( {sources.map(source => (
<div key={source.id} className="source-item" style={{ <div key={source.id} className="source-item flex-between">
display: 'flex', <div className="flex-1 min-w-0">
alignItems: 'center', <div className="font-medium truncate">
justifyContent: 'space-between',
padding: 'var(--space-sm) 0',
borderBottom: '1px solid var(--border)',
}}>
<div style={{ flex: 1, minWidth: 0 }}>
<div style={{ fontWeight: 500, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>
{source.name} {source.name}
</div> </div>
<div className="text-secondary text-xs"> <div className="text-secondary text-xs">
{source.file_type} {formatFileSize(source.file_size)} {formatDate(source.uploaded_at)} {source.file_type} {formatFileSize(source.file_size)} {formatDate(source.uploaded_at)}
</div> </div>
</div> </div>
<div style={{ display: 'flex', gap: 'var(--space-sm)' }}> <div className="flex gap-sm items-center">
{source.ocr_data ? ( {source.ocr_data ? (
<span style={{ color: 'var(--success)', fontSize: '12px', display: 'flex', alignItems: 'center', gap: '4px' }}> <span className="status-parsed flex items-center gap-xs text-xs">
<img src="/icons/general/icons8-checkmark-50.png" alt="Parsed" style={{ width: 14, height: 14 }} /> Parsed <img src="/icons/general/icons8-checkmark-50.png" alt="Parsed" className="icon-sm" /> Parsed
</span> </span>
) : ( ) : (
<span style={{ color: 'var(--text-secondary)', fontSize: '12px' }}>Pending</span> <span className="text-secondary text-xs">Pending</span>
)} )}
<button <button
className="btn btn-danger btn-sm" className="btn btn-danger btn-sm"
@@ -228,21 +213,13 @@ export function SourcesPage() {
{/* Delete Confirmation Modal */} {/* Delete Confirmation Modal */}
{deleteConfirmId !== null && ( {deleteConfirmId !== null && (
<div style={{ <div className="modal-overlay">
position: 'fixed', <div className="card modal-content">
inset: 0, <h3 className="mb-md">Delete File?</h3>
backgroundColor: 'rgba(0,0,0,0.5)', <p className="text-secondary mb-lg">
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
zIndex: 1000,
}}>
<div className="card" style={{ maxWidth: 400, textAlign: 'center' }}>
<h3 style={{ marginBottom: 'var(--space-md)' }}>Delete File?</h3>
<p className="text-secondary" style={{ marginBottom: 'var(--space-lg)' }}>
Are you sure you want to delete this file? This action cannot be undone. Are you sure you want to delete this file? This action cannot be undone.
</p> </p>
<div style={{ display: 'flex', gap: 'var(--space-sm)', justifyContent: 'center' }}> <div className="flex gap-sm justify-center">
<button className="btn" onClick={() => setDeleteConfirmId(null)}> <button className="btn" onClick={() => setDeleteConfirmId(null)}>
Cancel Cancel
</button> </button>