mirror of
https://github.com/dat515-2025/Group-8.git
synced 2026-03-22 06:57:47 +01:00
Merge pull request #44 from dat515-2025/43-fix-the-ui-layout-in-chrome
Fixed the layout issues for Chrome-based browsers, added options for users modifying transactions in the UI and implemented mobile friendly UI responsiveness
This commit is contained in:
@@ -19,6 +19,17 @@ export type Transaction = {
|
|||||||
date?: string | null; // ISO date (YYYY-MM-DD)
|
date?: string | null; // ISO date (YYYY-MM-DD)
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export async function deleteTransaction(id: number): Promise<void> {
|
||||||
|
const res = await fetch(`${getBaseUrl()}/transactions/${id}/delete`, {
|
||||||
|
method: 'DELETE',
|
||||||
|
headers: getHeaders('none'),
|
||||||
|
});
|
||||||
|
if (!res.ok) {
|
||||||
|
const text = await res.text();
|
||||||
|
throw new Error(text || 'Failed to delete transaction');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function getBaseUrl() {
|
function getBaseUrl() {
|
||||||
const base = BACKEND_URL?.replace(/\/$/, '') || '';
|
const base = BACKEND_URL?.replace(/\/$/, '') || '';
|
||||||
return base || '';
|
return base || '';
|
||||||
|
|||||||
@@ -13,9 +13,9 @@ export function applyTheme(theme: Theme) {
|
|||||||
export function applyFontSize(size: FontSize) {
|
export function applyFontSize(size: FontSize) {
|
||||||
const root = document.documentElement;
|
const root = document.documentElement;
|
||||||
const map: Record<FontSize, string> = {
|
const map: Record<FontSize, string> = {
|
||||||
small: '14px',
|
small: '12px',
|
||||||
medium: '18px',
|
medium: '15px',
|
||||||
large: '22px',
|
large: '21px',
|
||||||
};
|
};
|
||||||
root.style.fontSize = map[size];
|
root.style.fontSize = map[size];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,8 +24,6 @@ a:hover {
|
|||||||
|
|
||||||
body {
|
body {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
display: flex;
|
|
||||||
place-items: center;
|
|
||||||
min-width: 320px;
|
min-width: 320px;
|
||||||
min-height: 100vh;
|
min-height: 100vh;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
// src/BalanceChart.tsx
|
// src/BalanceChart.tsx
|
||||||
import { LineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip, Legend, ResponsiveContainer } from 'recharts';
|
import { useEffect, useRef, useState } from 'react';
|
||||||
|
import { LineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip, Legend } from 'recharts';
|
||||||
import { type BalancePoint } from '../api';
|
import { type BalancePoint } from '../api';
|
||||||
|
|
||||||
function formatAmount(n: number) {
|
function formatAmount(n: number) {
|
||||||
@@ -10,37 +11,56 @@ function formatDate(dateStr: string) {
|
|||||||
return new Date(dateStr).toLocaleDateString(undefined, { month: 'short', day: 'numeric' });
|
return new Date(dateStr).toLocaleDateString(undefined, { month: 'short', day: 'numeric' });
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function BalanceChart({ data }: { data: BalancePoint[] }) {
|
type Props = { data: BalancePoint[]; pxPerPoint?: number };
|
||||||
|
|
||||||
|
export default function BalanceChart({ data, pxPerPoint = 40 }: Props) {
|
||||||
|
const wrapRef = useRef<HTMLDivElement | null>(null);
|
||||||
|
const [containerWidth, setContainerWidth] = useState(0);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
function measure() {
|
||||||
|
if (!wrapRef.current) return;
|
||||||
|
setContainerWidth(wrapRef.current.clientWidth);
|
||||||
|
}
|
||||||
|
measure();
|
||||||
|
const obs = new ResizeObserver(measure);
|
||||||
|
if (wrapRef.current) obs.observe(wrapRef.current);
|
||||||
|
return () => obs.disconnect();
|
||||||
|
}, []);
|
||||||
|
|
||||||
if (data.length === 0) {
|
if (data.length === 0) {
|
||||||
return <div>No data to display</div>;
|
return <div>No data to display</div>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const desiredWidth = Math.max(containerWidth, Math.max(600, data.length * pxPerPoint));
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ResponsiveContainer width="100%" height={300}>
|
<div ref={wrapRef} className="chart-scroll">
|
||||||
<LineChart
|
<div className="chart-inner" style={{ minWidth: desiredWidth, paddingBottom: 8 }}>
|
||||||
data={data}
|
<LineChart
|
||||||
// Increased 'left' margin to create more space for the Y-axis label and tick values
|
width={desiredWidth}
|
||||||
margin={{ top: 5, right: 30, left: 50, bottom: 5 }} // <-- Change this line
|
height={300}
|
||||||
>
|
data={data}
|
||||||
<CartesianGrid strokeDasharray="3 3" />
|
margin={{ top: 5, right: 30, left: 50, bottom: 5 }}
|
||||||
<XAxis
|
>
|
||||||
dataKey="date"
|
<CartesianGrid strokeDasharray="3 3" />
|
||||||
tickFormatter={formatDate}
|
<XAxis
|
||||||
label={{ value: 'Date', position: 'insideBottom', offset: -5 }}
|
dataKey="date"
|
||||||
/>
|
tickFormatter={formatDate}
|
||||||
<YAxis
|
label={{ value: 'Date', position: 'insideBottom', offset: -5 }}
|
||||||
tickFormatter={(value) => formatAmount(value as number)}
|
/>
|
||||||
// Adjusted 'offset' for the Y-axis label.
|
<YAxis
|
||||||
// A negative offset moves it further away from the axis.
|
tickFormatter={(value) => formatAmount(value as number)}
|
||||||
label={{ value: 'Balance', angle: -90, position: 'insideLeft', offset: -30 }} // <-- Change this line
|
label={{ value: 'Balance', angle: -90, position: 'insideLeft', offset: -30 }}
|
||||||
/>
|
/>
|
||||||
<Tooltip
|
<Tooltip
|
||||||
labelFormatter={formatDate}
|
labelFormatter={formatDate}
|
||||||
formatter={(value) => [formatAmount(value as number), 'Balance']}
|
formatter={(value) => [formatAmount(value as number), 'Balance']}
|
||||||
/>
|
/>
|
||||||
<Legend />
|
<Legend />
|
||||||
<Line type="monotone" dataKey="balance" stroke="#3b82f6" strokeWidth={2} activeDot={{ r: 8 }} />
|
<Line type="monotone" dataKey="balance" stroke="#3b82f6" strokeWidth={2} activeDot={{ r: 8 }} />
|
||||||
</LineChart>
|
</LineChart>
|
||||||
</ResponsiveContainer>
|
</div>
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -92,9 +92,13 @@ export default function CategoryPieCharts({ transactions, categories }: { transa
|
|||||||
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div style={{ display: 'flex', flexWrap: 'wrap', gap: '20px', justifyContent: 'center' }}>
|
<div className="pie-grid" >
|
||||||
<SinglePieChart data={expensesData} title="Expenses by Category" />
|
<div className="pie-card">
|
||||||
<SinglePieChart data={earningsData} title="Earnings by Category" />
|
<SinglePieChart data={expensesData} title="Expenses by Category" />
|
||||||
|
</div>
|
||||||
|
<div className="pie-card">
|
||||||
|
<SinglePieChart data={earningsData} title="Earnings by Category" />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
import { useEffect, useMemo, useState } from 'react';
|
import { useEffect, useMemo, useState } from 'react';
|
||||||
import { type Category, type Transaction, type BalancePoint, getCategories, getTransactions, createTransaction, updateTransaction, getBalanceSeries } from '../api';
|
import { type Category, type Transaction, type BalancePoint, deleteTransaction, getCategories, getTransactions, createTransaction, updateTransaction, getBalanceSeries } from '../api';
|
||||||
import AccountPage from './AccountPage';
|
import AccountPage from './AccountPage';
|
||||||
import AppearancePage from './AppearancePage';
|
import AppearancePage from './AppearancePage';
|
||||||
import BalanceChart from './BalanceChart';
|
import BalanceChart from './BalanceChart';
|
||||||
@@ -161,9 +161,17 @@ export default function Dashboard({ onLogout }: { onLogout: () => void }) {
|
|||||||
|
|
||||||
// Manual forms moved to ManualManagement page
|
// Manual forms moved to ManualManagement page
|
||||||
|
|
||||||
// Inline edit state for transaction categories
|
// Inline edit state for transaction editing
|
||||||
const [editingTxId, setEditingTxId] = useState<number | null>(null);
|
const [editingTxId, setEditingTxId] = useState<number | null>(null);
|
||||||
const [editingCategoryIds, setEditingCategoryIds] = useState<number[]>([]);
|
const [editingCategoryIds, setEditingCategoryIds] = useState<number[]>([]);
|
||||||
|
const [editingAmount, setEditingAmount] = useState<string>('');
|
||||||
|
const [editingDescription, setEditingDescription] = useState<string>('');
|
||||||
|
const [editingDate, setEditingDate] = useState<string>(''); // YYYY-MM-DD
|
||||||
|
|
||||||
|
// Sidebar toggle for mobile
|
||||||
|
const [sidebarOpen, setSidebarOpen] = useState(false);
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
async function loadAll() {
|
async function loadAll() {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
@@ -259,31 +267,59 @@ export default function Dashboard({ onLogout }: { onLogout: () => void }) {
|
|||||||
function categoryNameById(id: number) { return categories.find(c => c.id === id)?.name || `#${id}`; }
|
function categoryNameById(id: number) { return categories.find(c => c.id === id)?.name || `#${id}`; }
|
||||||
|
|
||||||
|
|
||||||
function beginEditCategories(t: Transaction) {
|
function beginEditTransaction(t: Transaction) {
|
||||||
setEditingTxId(t.id);
|
setEditingTxId(t.id);
|
||||||
setEditingCategoryIds([...(t.category_ids || [])]);
|
setEditingCategoryIds([...(t.category_ids || [])]);
|
||||||
|
setEditingAmount(String(t.amount));
|
||||||
|
setEditingDescription(t.description || '');
|
||||||
|
setEditingDate(t.date || '');
|
||||||
}
|
}
|
||||||
function cancelEditCategories() {
|
function cancelEditTransaction() {
|
||||||
setEditingTxId(null);
|
setEditingTxId(null);
|
||||||
setEditingCategoryIds([]);
|
setEditingCategoryIds([]);
|
||||||
|
setEditingAmount('');
|
||||||
|
setEditingDescription('');
|
||||||
|
setEditingDate('');
|
||||||
}
|
}
|
||||||
async function saveEditCategories() {
|
async function saveEditTransaction() {
|
||||||
if (editingTxId == null) return;
|
if (editingTxId == null) return;
|
||||||
|
const amountNum = Number(editingAmount);
|
||||||
|
if (Number.isNaN(amountNum)) {
|
||||||
|
alert('Amount must be a number.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
try {
|
try {
|
||||||
const updated = await updateTransaction(editingTxId, { category_ids: editingCategoryIds });
|
const updated = await updateTransaction(editingTxId, {
|
||||||
|
amount: amountNum,
|
||||||
|
description: editingDescription,
|
||||||
|
date: editingDate || undefined,
|
||||||
|
category_ids: editingCategoryIds,
|
||||||
|
});
|
||||||
setTransactions(prev => prev.map(p => (p.id === updated.id ? updated : p)));
|
setTransactions(prev => prev.map(p => (p.id === updated.id ? updated : p)));
|
||||||
cancelEditCategories();
|
// Optionally refresh balance series to reflect changes immediately
|
||||||
|
try { setBalanceSeries(await getBalanceSeries(startDate || undefined, endDate || undefined)); } catch {}
|
||||||
|
cancelEditTransaction();
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
alert(err?.message || 'Failed to update transaction categories');
|
alert(err?.message || 'Failed to update transaction');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
async function handleDeleteTransaction(id: number) {
|
||||||
|
if (!confirm('Delete this transaction? This cannot be undone.')) return;
|
||||||
|
try {
|
||||||
|
await deleteTransaction(id);
|
||||||
|
setTransactions(prev => prev.filter(t => t.id !== id));
|
||||||
|
try { setBalanceSeries(await getBalanceSeries(startDate || undefined, endDate || undefined)); } catch {}
|
||||||
|
} catch (err: any) {
|
||||||
|
alert(err?.message || 'Failed to delete transaction');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="app-layout">
|
<div className={`app-layout ${sidebarOpen ? 'sidebar-open' : ''}`}>
|
||||||
<aside className="sidebar" style={{ display: 'flex', flexDirection: 'column' }}>
|
<aside className="sidebar" style={{ display: 'flex', flexDirection: 'column' }}>
|
||||||
<div>
|
<div>
|
||||||
<div className="logo">7Project</div>
|
<div className="logo">7Project</div>
|
||||||
<nav className="nav">
|
<nav className="nav" onClick={() => setSidebarOpen(false)}>
|
||||||
<button className={current === 'home' ? 'active' : ''} onClick={() => setCurrent('home')}>Home</button>
|
<button className={current === 'home' ? 'active' : ''} onClick={() => setCurrent('home')}>Home</button>
|
||||||
<button className={current === 'manual' ? 'active' : ''} onClick={() => setCurrent('manual')}>Manual management</button>
|
<button className={current === 'manual' ? 'active' : ''} onClick={() => setCurrent('manual')}>Manual management</button>
|
||||||
<button className={current === 'account' ? 'active' : ''} onClick={() => setCurrent('account')}>Account</button>
|
<button className={current === 'account' ? 'active' : ''} onClick={() => setCurrent('account')}>Account</button>
|
||||||
@@ -296,6 +332,12 @@ export default function Dashboard({ onLogout }: { onLogout: () => void }) {
|
|||||||
</aside>
|
</aside>
|
||||||
<div className="content">
|
<div className="content">
|
||||||
<div className="topbar">
|
<div className="topbar">
|
||||||
|
<button
|
||||||
|
className="icon-btn hamburger"
|
||||||
|
aria-label="Open menu"
|
||||||
|
aria-expanded={sidebarOpen}
|
||||||
|
onClick={() => setSidebarOpen(true)}
|
||||||
|
>☰</button>
|
||||||
<h2 style={{ margin: 0 }}>{current === 'home' ? 'Dashboard' : current === 'manual' ? 'Manual management' : current === 'account' ? 'Account' : 'Appearance'}</h2>
|
<h2 style={{ margin: 0 }}>{current === 'home' ? 'Dashboard' : current === 'manual' ? 'Manual management' : current === 'account' ? 'Account' : 'Appearance'}</h2>
|
||||||
<div className="actions">
|
<div className="actions">
|
||||||
<span className="user muted">Signed in</span>
|
<span className="user muted">Signed in</span>
|
||||||
@@ -376,39 +418,98 @@ export default function Dashboard({ onLogout }: { onLogout: () => void }) {
|
|||||||
<button className="btn primary" disabled={page >= totalPages - 1} onClick={() => setPage(p => Math.min(totalPages - 1, p + 1))}>Next</button>
|
<button className="btn primary" disabled={page >= totalPages - 1} onClick={() => setPage(p => Math.min(totalPages - 1, p + 1))}>Next</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<table className="table">
|
<table className="table responsive">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th>Date</th>
|
<th>Date</th>
|
||||||
<th style={{ textAlign: 'right' }}>Amount</th>
|
<th style={{ textAlign: 'right' }}>Amount</th>
|
||||||
<th>Description</th>
|
<th>Description</th>
|
||||||
<th>Categories</th>
|
<th>Categories</th>
|
||||||
|
<th>Actions</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{visible.map(t => (
|
{visible.map(t => (
|
||||||
<tr key={t.id}>
|
<tr key={t.id}>
|
||||||
<td>{t.date || ''}</td>
|
{/* Date cell */}
|
||||||
<td className="amount">{formatAmount(t.amount)}</td>
|
<td data-label="Date">
|
||||||
<td>{t.description || ''}</td>
|
|
||||||
<td>
|
|
||||||
{editingTxId === t.id ? (
|
{editingTxId === t.id ? (
|
||||||
<div className="space-y" style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
|
<input
|
||||||
<select multiple className="input" value={editingCategoryIds.map(String)} onChange={(e) => {
|
className="input"
|
||||||
const opts = Array.from(e.currentTarget.selectedOptions).map(o => Number(o.value));
|
type="date"
|
||||||
setEditingCategoryIds(opts);
|
value={editingDate}
|
||||||
}}>
|
onChange={(e) => setEditingDate(e.target.value)}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
t.date || ''
|
||||||
|
)}
|
||||||
|
</td>
|
||||||
|
|
||||||
|
{/* Amount cell */}
|
||||||
|
<td data-label="Amount" className="amount" style={{ textAlign: 'right' }}>
|
||||||
|
{editingTxId === t.id ? (
|
||||||
|
<input
|
||||||
|
className="input"
|
||||||
|
type="number"
|
||||||
|
step="0.01"
|
||||||
|
value={editingAmount}
|
||||||
|
onChange={(e) => setEditingAmount(e.target.value)}
|
||||||
|
style={{ textAlign: 'right' }}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
formatAmount(t.amount)
|
||||||
|
)}
|
||||||
|
</td>
|
||||||
|
|
||||||
|
{/* Description cell */}
|
||||||
|
<td data-label="Description">
|
||||||
|
{editingTxId === t.id ? (
|
||||||
|
<input
|
||||||
|
className="input"
|
||||||
|
type="text"
|
||||||
|
value={editingDescription}
|
||||||
|
onChange={(e) => setEditingDescription(e.target.value)}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
t.description || ''
|
||||||
|
)}
|
||||||
|
</td>
|
||||||
|
|
||||||
|
{/* Categories cell */}
|
||||||
|
<td data-label="Categories">
|
||||||
|
{editingTxId === t.id ? (
|
||||||
|
<div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
|
||||||
|
<select
|
||||||
|
multiple
|
||||||
|
className="input"
|
||||||
|
value={editingCategoryIds.map(String)}
|
||||||
|
onChange={(e) => {
|
||||||
|
const opts = Array.from(e.currentTarget.selectedOptions).map(o => Number(o.value));
|
||||||
|
setEditingCategoryIds(opts);
|
||||||
|
}}
|
||||||
|
>
|
||||||
{categories.map(c => (
|
{categories.map(c => (
|
||||||
<option key={c.id} value={c.id}>{c.name}</option>
|
<option key={c.id} value={c.id}>{c.name}</option>
|
||||||
))}
|
))}
|
||||||
</select>
|
</select>
|
||||||
<button className="btn small" onClick={saveEditCategories}>Save</button>
|
|
||||||
<button className="btn small" onClick={cancelEditCategories}>Cancel</button>
|
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="space-x" style={{ display: 'flex', alignItems: 'center', gap: 8, justifyContent: 'space-between' }}>
|
<span>{t.category_ids.map(id => categoryNameById(id)).join(', ') || '—'}</span>
|
||||||
<span>{t.category_ids.map(id => categoryNameById(id)).join(', ') || '—'}</span>
|
)}
|
||||||
<button className="btn small" onClick={() => beginEditCategories(t)}>Change</button>
|
</td>
|
||||||
|
|
||||||
|
{/* Actions cell */}
|
||||||
|
<td data-label="Actions">
|
||||||
|
{editingTxId === t.id ? (
|
||||||
|
<div className="actions" style={{ display: 'flex', gap: 8, justifyContent: 'flex-end' }}>
|
||||||
|
<button className="btn small" onClick={saveEditTransaction}>Save</button>
|
||||||
|
<button className="btn small" onClick={cancelEditTransaction}>Cancel</button>
|
||||||
|
<button className="btn small" onClick={() => handleDeleteTransaction(t.id)}>Delete</button>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="actions" style={{ display: 'flex', gap: 8, justifyContent: 'flex-end' }}>
|
||||||
|
<button className="btn small" onClick={() => beginEditTransaction(t)}>Edit</button>
|
||||||
|
<button className="btn small" onClick={() => handleDeleteTransaction(t.id)}>Delete</button>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</td>
|
</td>
|
||||||
@@ -447,6 +548,7 @@ export default function Dashboard({ onLogout }: { onLogout: () => void }) {
|
|||||||
onClose={() => setMockModalOpen(false)}
|
onClose={() => setMockModalOpen(false)}
|
||||||
onGenerate={handleGenerateMockTransactions}
|
onGenerate={handleGenerateMockTransactions}
|
||||||
/>
|
/>
|
||||||
|
{sidebarOpen && <div className="backdrop" onClick={() => setSidebarOpen(false)} />}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -80,7 +80,7 @@ export default function LoginRegisterPage({ onLoggedIn }: { onLoggedIn: () => vo
|
|||||||
<input className="input" type="password" required value={password} onChange={(e) => setPassword(e.target.value)} />
|
<input className="input" type="password" required value={password} onChange={(e) => setPassword(e.target.value)} />
|
||||||
</div>
|
</div>
|
||||||
{mode === 'register' && (
|
{mode === 'register' && (
|
||||||
<div className="form-row">
|
<div className="space-y">
|
||||||
<div>
|
<div>
|
||||||
<label className="muted">First name (optional)</label>
|
<label className="muted">First name (optional)</label>
|
||||||
<input className="input" type="text" value={firstName} onChange={(e) => setFirstName(e.target.value)} />
|
<input className="input" type="text" value={firstName} onChange={(e) => setFirstName(e.target.value)} />
|
||||||
|
|||||||
@@ -48,26 +48,49 @@ body[data-theme="dark"] {
|
|||||||
.card h3 { margin: 0 0 12px; }
|
.card h3 { margin: 0 0 12px; }
|
||||||
|
|
||||||
/* Forms */
|
/* Forms */
|
||||||
.input, select, textarea {
|
/* Common field styles (no custom arrow here) */
|
||||||
|
.input, textarea {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
padding: 10px 12px;
|
padding: 10px 12px;
|
||||||
border-radius: 10px;
|
border-radius: 10px;
|
||||||
border: 1px solid var(--border);
|
border: 1px solid var(--border);
|
||||||
background-color: var(--panel);
|
background-color: var(--panel);
|
||||||
color: var(--muted);
|
color: var(--muted);
|
||||||
|
}
|
||||||
|
|
||||||
/* Add these properties specifically for the select element */
|
/* Select-only: show custom dropdown arrow */
|
||||||
|
select.input {
|
||||||
-webkit-appearance: none;
|
-webkit-appearance: none;
|
||||||
-moz-appearance: none;
|
-moz-appearance: none;
|
||||||
appearance: none;
|
appearance: none;
|
||||||
|
|
||||||
padding-right: 32px; /* Add space for the custom arrow */
|
padding-right: 32px; /* room for the arrow */
|
||||||
background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 20 20'%3e%3cpath stroke='%236b7280' stroke-linecap='round' stroke-linejoin='round' stroke-width='1.5' d='M6 8l4 4 4-4'/%3e%3c/svg%3e");
|
background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 20 20'%3e%3cpath stroke='%236b7280' stroke-linecap='round' stroke-linejoin='round' stroke-width='1.5' d='M6 8l4 4 4-4'/%3e%3c/svg%3e");
|
||||||
background-position: right 0.5rem center;
|
background-position: right 0.5rem center;
|
||||||
background-repeat: no-repeat;
|
background-repeat: no-repeat;
|
||||||
background-size: 1.5em 1.5em;
|
background-size: 1.5em 1.5em;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.pie-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||||
|
gap: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 900px) {
|
||||||
|
.pie-grid {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Make charts scale nicely within the cards */
|
||||||
|
.pie-card canvas, .pie-card svg {
|
||||||
|
max-width: 100%;
|
||||||
|
height: auto;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
.input:focus, select:focus, textarea:focus {
|
.input:focus, select:focus, textarea:focus {
|
||||||
outline: 2px solid var(--primary);
|
outline: 2px solid var(--primary);
|
||||||
outline-offset: 2px;
|
outline-offset: 2px;
|
||||||
@@ -151,3 +174,117 @@ body.auth-page #root {
|
|||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/* Responsive enhancements */
|
||||||
|
|
||||||
|
/* Off-canvas sidebar + hamburger for mobile */
|
||||||
|
@media (max-width: 900px) {
|
||||||
|
.app-layout {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
min-height: 100dvh;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
.sidebar {
|
||||||
|
position: fixed;
|
||||||
|
inset: 0 auto 0 0;
|
||||||
|
width: 80vw;
|
||||||
|
max-width: 320px;
|
||||||
|
transform: translateX(-100%);
|
||||||
|
transition: transform 200ms ease;
|
||||||
|
z-index: 1000;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
.app-layout.sidebar-open .sidebar {
|
||||||
|
transform: translateX(0);
|
||||||
|
}
|
||||||
|
.hamburger {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 40px;
|
||||||
|
height: 40px;
|
||||||
|
margin-right: 8px;
|
||||||
|
}
|
||||||
|
.topbar { position: sticky; top: 0; z-index: 500; }
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: 901px) {
|
||||||
|
.hamburger { display: none; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Backdrop when sidebar is open */
|
||||||
|
.backdrop {
|
||||||
|
position: fixed;
|
||||||
|
inset: 0;
|
||||||
|
background: rgba(0,0,0,0.45);
|
||||||
|
z-index: 900;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Responsive table: convert to card list on small screens */
|
||||||
|
.table.responsive { width: 100%; }
|
||||||
|
@media (max-width: 700px) {
|
||||||
|
.table.responsive thead { display: none; }
|
||||||
|
.table.responsive tbody tr {
|
||||||
|
display: block;
|
||||||
|
border: 1px solid var(--border, #2a2f45);
|
||||||
|
border-radius: 8px;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
overflow: hidden;
|
||||||
|
background: var(--panel);
|
||||||
|
}
|
||||||
|
.table.responsive tbody td {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 12px;
|
||||||
|
padding: 10px 12px;
|
||||||
|
border-bottom: 1px solid var(--border);
|
||||||
|
text-align: left !important; /* override any right align */
|
||||||
|
}
|
||||||
|
.table.responsive tbody td:last-child { border-bottom: 0; }
|
||||||
|
.table.responsive tbody td::before {
|
||||||
|
content: attr(data-label);
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--muted);
|
||||||
|
}
|
||||||
|
.table.responsive .actions { width: 100%; justify-content: flex-end; }
|
||||||
|
.table.responsive .amount { font-weight: 600; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Filters and controls wrapping */
|
||||||
|
@media (max-width: 900px) {
|
||||||
|
.form-row { grid-template-columns: repeat(2, minmax(0, 1fr)); }
|
||||||
|
}
|
||||||
|
@media (max-width: 700px) {
|
||||||
|
.form-row { grid-template-columns: 1fr; }
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-controls { gap: 12px; }
|
||||||
|
@media (max-width: 700px) {
|
||||||
|
.table-controls { flex-direction: column; align-items: stretch; }
|
||||||
|
.table-controls .actions { width: 100%; }
|
||||||
|
.table-controls .actions .btn { flex: 1 0 auto; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Touch-friendly sizes */
|
||||||
|
.btn, .input, select.input { min-height: 40px; }
|
||||||
|
.btn.small { min-height: 36px; }
|
||||||
|
|
||||||
|
/* Connection rows on mobile */
|
||||||
|
@media (max-width: 700px) {
|
||||||
|
.connection-row { flex-direction: column; align-items: stretch; gap: 8px; }
|
||||||
|
.connection-row .btn { width: 100%; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Charts should scale to container */
|
||||||
|
.card canvas, .card svg { max-width: 100%; height: auto; display: block; }
|
||||||
|
|
||||||
|
|
||||||
|
/* Horizontal scroll container for wide charts */
|
||||||
|
.chart-scroll {
|
||||||
|
overflow-x: auto;
|
||||||
|
overflow-y: hidden;
|
||||||
|
-webkit-overflow-scrolling: touch; /* momentum scroll on iOS */
|
||||||
|
}
|
||||||
|
.chart-inner { min-width: 900px; }
|
||||||
|
|||||||
Reference in New Issue
Block a user