silly features in place

This commit is contained in:
counterweight 2025-12-18 22:51:43 +01:00
parent c5d3c7f4c9
commit 322bdd3e6e
Signed by: counterweight
GPG key ID: 883EDBAA726BD96C
5 changed files with 997 additions and 6 deletions

View file

@ -1,11 +1,12 @@
from contextlib import asynccontextmanager from contextlib import asynccontextmanager
from fastapi import FastAPI, Depends, HTTPException, Response, status from fastapi import FastAPI, Depends, HTTPException, Response, status, Query
from fastapi.middleware.cors import CORSMiddleware from fastapi.middleware.cors import CORSMiddleware
from sqlalchemy import select from sqlalchemy import select, func, desc
from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.ext.asyncio import AsyncSession
from pydantic import BaseModel
from database import engine, get_db, Base from database import engine, get_db, Base
from models import Counter, User from models import Counter, User, SumRecord, CounterRecord
from auth import ( from auth import (
ACCESS_TOKEN_EXPIRE_MINUTES, ACCESS_TOKEN_EXPIRE_MINUTES,
COOKIE_NAME, COOKIE_NAME,
@ -129,10 +130,175 @@ async def get_counter(
@app.post("/api/counter/increment") @app.post("/api/counter/increment")
async def increment_counter( async def increment_counter(
db: AsyncSession = Depends(get_db), db: AsyncSession = Depends(get_db),
_current_user: User = Depends(get_current_user), current_user: User = Depends(get_current_user),
): ):
counter = await get_or_create_counter(db) counter = await get_or_create_counter(db)
value_before = counter.value
counter.value += 1 counter.value += 1
record = CounterRecord(
user_id=current_user.id,
value_before=value_before,
value_after=counter.value,
)
db.add(record)
await db.commit() await db.commit()
return {"value": counter.value} return {"value": counter.value}
# Sum endpoints
class SumRequest(BaseModel):
a: float
b: float
class SumResponse(BaseModel):
a: float
b: float
result: float
@app.post("/api/sum", response_model=SumResponse)
async def calculate_sum(
data: SumRequest,
db: AsyncSession = Depends(get_db),
current_user: User = Depends(get_current_user),
):
result = data.a + data.b
record = SumRecord(
user_id=current_user.id,
a=data.a,
b=data.b,
result=result,
)
db.add(record)
await db.commit()
return SumResponse(a=data.a, b=data.b, result=result)
# Audit endpoints
from datetime import datetime
from typing import List
class CounterRecordResponse(BaseModel):
id: int
user_email: str
value_before: int
value_after: int
created_at: datetime
class SumRecordResponse(BaseModel):
id: int
user_email: str
a: float
b: float
result: float
created_at: datetime
class PaginatedCounterRecords(BaseModel):
records: List[CounterRecordResponse]
total: int
page: int
per_page: int
total_pages: int
class PaginatedSumRecords(BaseModel):
records: List[SumRecordResponse]
total: int
page: int
per_page: int
total_pages: int
@app.get("/api/audit/counter", response_model=PaginatedCounterRecords)
async def get_counter_records(
page: int = Query(1, ge=1),
per_page: int = Query(10, ge=1, le=100),
db: AsyncSession = Depends(get_db),
_current_user: User = Depends(get_current_user),
):
# Get total count
count_result = await db.execute(select(func.count(CounterRecord.id)))
total = count_result.scalar() or 0
total_pages = (total + per_page - 1) // per_page if total > 0 else 1
# Get paginated records with user email
offset = (page - 1) * per_page
query = (
select(CounterRecord, User.email)
.join(User, CounterRecord.user_id == User.id)
.order_by(desc(CounterRecord.created_at))
.offset(offset)
.limit(per_page)
)
result = await db.execute(query)
rows = result.all()
records = [
CounterRecordResponse(
id=record.id,
user_email=email,
value_before=record.value_before,
value_after=record.value_after,
created_at=record.created_at,
)
for record, email in rows
]
return PaginatedCounterRecords(
records=records,
total=total,
page=page,
per_page=per_page,
total_pages=total_pages,
)
@app.get("/api/audit/sum", response_model=PaginatedSumRecords)
async def get_sum_records(
page: int = Query(1, ge=1),
per_page: int = Query(10, ge=1, le=100),
db: AsyncSession = Depends(get_db),
_current_user: User = Depends(get_current_user),
):
# Get total count
count_result = await db.execute(select(func.count(SumRecord.id)))
total = count_result.scalar() or 0
total_pages = (total + per_page - 1) // per_page if total > 0 else 1
# Get paginated records with user email
offset = (page - 1) * per_page
query = (
select(SumRecord, User.email)
.join(User, SumRecord.user_id == User.id)
.order_by(desc(SumRecord.created_at))
.offset(offset)
.limit(per_page)
)
result = await db.execute(query)
rows = result.all()
records = [
SumRecordResponse(
id=record.id,
user_email=email,
a=record.a,
b=record.b,
result=record.result,
created_at=record.created_at,
)
for record, email in rows
]
return PaginatedSumRecords(
records=records,
total=total,
page=page,
per_page=per_page,
total_pages=total_pages,
)

View file

@ -1,4 +1,5 @@
from sqlalchemy import Integer, String from datetime import datetime
from sqlalchemy import Integer, String, Float, DateTime, ForeignKey
from sqlalchemy.orm import Mapped, mapped_column from sqlalchemy.orm import Mapped, mapped_column
from database import Base from database import Base
@ -17,3 +18,24 @@ class User(Base):
email: Mapped[str] = mapped_column(String(255), unique=True, nullable=False, index=True) email: Mapped[str] = mapped_column(String(255), unique=True, nullable=False, index=True)
hashed_password: Mapped[str] = mapped_column(String(255), nullable=False) hashed_password: Mapped[str] = mapped_column(String(255), nullable=False)
class SumRecord(Base):
__tablename__ = "sum_records"
id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
user_id: Mapped[int] = mapped_column(Integer, ForeignKey("users.id"), nullable=False, index=True)
a: Mapped[float] = mapped_column(Float, nullable=False)
b: Mapped[float] = mapped_column(Float, nullable=False)
result: Mapped[float] = mapped_column(Float, nullable=False)
created_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow)
class CounterRecord(Base):
__tablename__ = "counter_records"
id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
user_id: Mapped[int] = mapped_column(Integer, ForeignKey("users.id"), nullable=False, index=True)
value_before: Mapped[int] = mapped_column(Integer, nullable=False)
value_after: Mapped[int] = mapped_column(Integer, nullable=False)
created_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow)

428
frontend/app/audit/page.tsx Normal file
View file

@ -0,0 +1,428 @@
"use client";
import { useEffect, useState } from "react";
import { useRouter } from "next/navigation";
import { useAuth } from "../auth-context";
import { API_URL } from "../config";
interface CounterRecord {
id: number;
user_email: string;
value_before: number;
value_after: number;
created_at: string;
}
interface SumRecord {
id: number;
user_email: string;
a: number;
b: number;
result: number;
created_at: string;
}
interface PaginatedResponse<T> {
records: T[];
total: number;
page: number;
per_page: number;
total_pages: number;
}
export default function AuditPage() {
const [counterData, setCounterData] = useState<PaginatedResponse<CounterRecord> | null>(null);
const [sumData, setSumData] = useState<PaginatedResponse<SumRecord> | null>(null);
const [counterPage, setCounterPage] = useState(1);
const [sumPage, setSumPage] = useState(1);
const { user, isLoading, logout } = useAuth();
const router = useRouter();
useEffect(() => {
if (!isLoading && !user) {
router.push("/login");
}
}, [isLoading, user, router]);
useEffect(() => {
if (user) {
fetchCounterRecords(counterPage);
}
}, [user, counterPage]);
useEffect(() => {
if (user) {
fetchSumRecords(sumPage);
}
}, [user, sumPage]);
const fetchCounterRecords = async (page: number) => {
try {
const res = await fetch(`${API_URL}/api/audit/counter?page=${page}&per_page=10`, {
credentials: "include",
});
const data = await res.json();
setCounterData(data);
} catch {
setCounterData(null);
}
};
const fetchSumRecords = async (page: number) => {
try {
const res = await fetch(`${API_URL}/api/audit/sum?page=${page}&per_page=10`, {
credentials: "include",
});
const data = await res.json();
setSumData(data);
} catch {
setSumData(null);
}
};
const handleLogout = async () => {
await logout();
router.push("/login");
};
const formatDate = (dateStr: string) => {
return new Date(dateStr).toLocaleString();
};
if (isLoading) {
return (
<main style={styles.main}>
<div style={styles.loader}>Loading...</div>
</main>
);
}
if (!user) {
return null;
}
return (
<main style={styles.main}>
<div style={styles.header}>
<div style={styles.nav}>
<a href="/" style={styles.navLink}>Counter</a>
<span style={styles.navDivider}></span>
<a href="/sum" style={styles.navLink}>Sum</a>
<span style={styles.navDivider}></span>
<span style={styles.navCurrent}>Audit</span>
</div>
<div style={styles.userInfo}>
<span style={styles.userEmail}>{user.email}</span>
<button onClick={handleLogout} style={styles.logoutBtn}>
Sign out
</button>
</div>
</div>
<div style={styles.content}>
<div style={styles.tablesContainer}>
{/* Counter Records Table */}
<div style={styles.tableCard}>
<div style={styles.tableHeader}>
<h2 style={styles.tableTitle}>Counter Activity</h2>
<span style={styles.totalCount}>
{counterData?.total ?? 0} records
</span>
</div>
<div style={styles.tableWrapper}>
<table style={styles.table}>
<thead>
<tr>
<th style={styles.th}>User</th>
<th style={styles.th}>Before</th>
<th style={styles.th}>After</th>
<th style={styles.th}>Date</th>
</tr>
</thead>
<tbody>
{counterData?.records.map((record) => (
<tr key={record.id} style={styles.tr}>
<td style={styles.td}>{record.user_email}</td>
<td style={styles.tdNum}>{record.value_before}</td>
<td style={styles.tdNum}>{record.value_after}</td>
<td style={styles.tdDate}>{formatDate(record.created_at)}</td>
</tr>
))}
{(!counterData || counterData.records.length === 0) && (
<tr>
<td colSpan={4} style={styles.emptyRow}>No records yet</td>
</tr>
)}
</tbody>
</table>
</div>
{counterData && counterData.total_pages > 1 && (
<div style={styles.pagination}>
<button
onClick={() => setCounterPage((p) => Math.max(1, p - 1))}
disabled={counterPage === 1}
style={styles.pageBtn}
>
</button>
<span style={styles.pageInfo}>
{counterPage} / {counterData.total_pages}
</span>
<button
onClick={() => setCounterPage((p) => Math.min(counterData.total_pages, p + 1))}
disabled={counterPage === counterData.total_pages}
style={styles.pageBtn}
>
</button>
</div>
)}
</div>
{/* Sum Records Table */}
<div style={styles.tableCard}>
<div style={styles.tableHeader}>
<h2 style={styles.tableTitle}>Sum Activity</h2>
<span style={styles.totalCount}>
{sumData?.total ?? 0} records
</span>
</div>
<div style={styles.tableWrapper}>
<table style={styles.table}>
<thead>
<tr>
<th style={styles.th}>User</th>
<th style={styles.th}>A</th>
<th style={styles.th}>B</th>
<th style={styles.th}>Result</th>
<th style={styles.th}>Date</th>
</tr>
</thead>
<tbody>
{sumData?.records.map((record) => (
<tr key={record.id} style={styles.tr}>
<td style={styles.td}>{record.user_email}</td>
<td style={styles.tdNum}>{record.a}</td>
<td style={styles.tdNum}>{record.b}</td>
<td style={styles.tdResult}>{record.result}</td>
<td style={styles.tdDate}>{formatDate(record.created_at)}</td>
</tr>
))}
{(!sumData || sumData.records.length === 0) && (
<tr>
<td colSpan={5} style={styles.emptyRow}>No records yet</td>
</tr>
)}
</tbody>
</table>
</div>
{sumData && sumData.total_pages > 1 && (
<div style={styles.pagination}>
<button
onClick={() => setSumPage((p) => Math.max(1, p - 1))}
disabled={sumPage === 1}
style={styles.pageBtn}
>
</button>
<span style={styles.pageInfo}>
{sumPage} / {sumData.total_pages}
</span>
<button
onClick={() => setSumPage((p) => Math.min(sumData.total_pages, p + 1))}
disabled={sumPage === sumData.total_pages}
style={styles.pageBtn}
>
</button>
</div>
)}
</div>
</div>
</div>
</main>
);
}
const styles: Record<string, React.CSSProperties> = {
main: {
minHeight: "100vh",
background: "linear-gradient(135deg, #0f0f23 0%, #1a1a3e 50%, #2d1b4e 100%)",
display: "flex",
flexDirection: "column",
},
loader: {
flex: 1,
display: "flex",
alignItems: "center",
justifyContent: "center",
fontFamily: "'DM Sans', system-ui, sans-serif",
color: "rgba(255, 255, 255, 0.5)",
fontSize: "1.125rem",
},
header: {
padding: "1.5rem 2rem",
borderBottom: "1px solid rgba(255, 255, 255, 0.06)",
display: "flex",
justifyContent: "space-between",
alignItems: "center",
},
nav: {
display: "flex",
alignItems: "center",
gap: "0.75rem",
},
navLink: {
fontFamily: "'DM Sans', system-ui, sans-serif",
color: "rgba(255, 255, 255, 0.5)",
fontSize: "0.875rem",
textDecoration: "none",
transition: "color 0.2s",
},
navDivider: {
color: "rgba(255, 255, 255, 0.2)",
fontSize: "0.75rem",
},
navCurrent: {
fontFamily: "'DM Sans', system-ui, sans-serif",
color: "#a78bfa",
fontSize: "0.875rem",
fontWeight: 600,
},
userInfo: {
display: "flex",
alignItems: "center",
gap: "1rem",
},
userEmail: {
fontFamily: "'DM Sans', system-ui, sans-serif",
color: "rgba(255, 255, 255, 0.6)",
fontSize: "0.875rem",
},
logoutBtn: {
fontFamily: "'DM Sans', system-ui, sans-serif",
padding: "0.5rem 1rem",
fontSize: "0.875rem",
fontWeight: 500,
background: "rgba(255, 255, 255, 0.05)",
color: "rgba(255, 255, 255, 0.7)",
border: "1px solid rgba(255, 255, 255, 0.1)",
borderRadius: "8px",
cursor: "pointer",
transition: "all 0.2s",
},
content: {
flex: 1,
padding: "2rem",
overflowY: "auto",
},
tablesContainer: {
display: "flex",
flexDirection: "column",
gap: "2rem",
maxWidth: "1200px",
margin: "0 auto",
},
tableCard: {
background: "rgba(255, 255, 255, 0.03)",
backdropFilter: "blur(10px)",
border: "1px solid rgba(255, 255, 255, 0.08)",
borderRadius: "20px",
padding: "1.5rem",
boxShadow: "0 25px 50px -12px rgba(0, 0, 0, 0.5)",
},
tableHeader: {
display: "flex",
justifyContent: "space-between",
alignItems: "center",
marginBottom: "1rem",
},
tableTitle: {
fontFamily: "'Instrument Serif', Georgia, serif",
fontSize: "1.5rem",
fontWeight: 400,
color: "#fff",
margin: 0,
},
totalCount: {
fontFamily: "'DM Sans', system-ui, sans-serif",
fontSize: "0.875rem",
color: "rgba(255, 255, 255, 0.4)",
},
tableWrapper: {
overflowX: "auto",
},
table: {
width: "100%",
borderCollapse: "collapse",
fontFamily: "'DM Sans', system-ui, sans-serif",
},
th: {
textAlign: "left",
padding: "0.75rem 1rem",
fontSize: "0.75rem",
fontWeight: 600,
color: "rgba(255, 255, 255, 0.4)",
textTransform: "uppercase",
letterSpacing: "0.05em",
borderBottom: "1px solid rgba(255, 255, 255, 0.08)",
},
tr: {
borderBottom: "1px solid rgba(255, 255, 255, 0.04)",
},
td: {
padding: "0.875rem 1rem",
fontSize: "0.875rem",
color: "rgba(255, 255, 255, 0.7)",
},
tdNum: {
padding: "0.875rem 1rem",
fontSize: "0.875rem",
color: "rgba(255, 255, 255, 0.9)",
fontFamily: "'DM Sans', monospace",
},
tdResult: {
padding: "0.875rem 1rem",
fontSize: "0.875rem",
color: "#a78bfa",
fontWeight: 600,
fontFamily: "'DM Sans', monospace",
},
tdDate: {
padding: "0.875rem 1rem",
fontSize: "0.75rem",
color: "rgba(255, 255, 255, 0.4)",
},
emptyRow: {
padding: "2rem 1rem",
textAlign: "center",
color: "rgba(255, 255, 255, 0.3)",
fontSize: "0.875rem",
},
pagination: {
display: "flex",
justifyContent: "center",
alignItems: "center",
gap: "1rem",
marginTop: "1rem",
paddingTop: "1rem",
borderTop: "1px solid rgba(255, 255, 255, 0.06)",
},
pageBtn: {
fontFamily: "'DM Sans', system-ui, sans-serif",
padding: "0.5rem 1rem",
fontSize: "1rem",
background: "rgba(255, 255, 255, 0.05)",
color: "rgba(255, 255, 255, 0.7)",
border: "1px solid rgba(255, 255, 255, 0.1)",
borderRadius: "8px",
cursor: "pointer",
transition: "all 0.2s",
},
pageInfo: {
fontFamily: "'DM Sans', system-ui, sans-serif",
fontSize: "0.875rem",
color: "rgba(255, 255, 255, 0.5)",
},
};

View file

@ -56,6 +56,13 @@ export default function Home() {
return ( return (
<main style={styles.main}> <main style={styles.main}>
<div style={styles.header}> <div style={styles.header}>
<div style={styles.nav}>
<span style={styles.navCurrent}>Counter</span>
<span style={styles.navDivider}></span>
<a href="/sum" style={styles.navLink}>Sum</a>
<span style={styles.navDivider}></span>
<a href="/audit" style={styles.navLink}>Audit</a>
</div>
<div style={styles.userInfo}> <div style={styles.userInfo}>
<span style={styles.userEmail}>{user.email}</span> <span style={styles.userEmail}>{user.email}</span>
<button onClick={handleLogout} style={styles.logoutBtn}> <button onClick={handleLogout} style={styles.logoutBtn}>
@ -97,11 +104,35 @@ const styles: Record<string, React.CSSProperties> = {
header: { header: {
padding: "1.5rem 2rem", padding: "1.5rem 2rem",
borderBottom: "1px solid rgba(255, 255, 255, 0.06)", borderBottom: "1px solid rgba(255, 255, 255, 0.06)",
display: "flex",
justifyContent: "space-between",
alignItems: "center",
},
nav: {
display: "flex",
alignItems: "center",
gap: "0.75rem",
},
navLink: {
fontFamily: "'DM Sans', system-ui, sans-serif",
color: "rgba(255, 255, 255, 0.5)",
fontSize: "0.875rem",
textDecoration: "none",
transition: "color 0.2s",
},
navDivider: {
color: "rgba(255, 255, 255, 0.2)",
fontSize: "0.75rem",
},
navCurrent: {
fontFamily: "'DM Sans', system-ui, sans-serif",
color: "#a78bfa",
fontSize: "0.875rem",
fontWeight: 600,
}, },
userInfo: { userInfo: {
display: "flex", display: "flex",
alignItems: "center", alignItems: "center",
justifyContent: "flex-end",
gap: "1rem", gap: "1rem",
}, },
userEmail: { userEmail: {

344
frontend/app/sum/page.tsx Normal file
View file

@ -0,0 +1,344 @@
"use client";
import { useEffect, useState } from "react";
import { useRouter } from "next/navigation";
import { useAuth } from "../auth-context";
import { API_URL } from "../config";
export default function SumPage() {
const [a, setA] = useState("");
const [b, setB] = useState("");
const [result, setResult] = useState<number | null>(null);
const [showResult, setShowResult] = useState(false);
const { user, isLoading, logout } = useAuth();
const router = useRouter();
useEffect(() => {
if (!isLoading && !user) {
router.push("/login");
}
}, [isLoading, user, router]);
const handleSum = async () => {
const numA = parseFloat(a) || 0;
const numB = parseFloat(b) || 0;
try {
const res = await fetch(`${API_URL}/api/sum`, {
method: "POST",
headers: { "Content-Type": "application/json" },
credentials: "include",
body: JSON.stringify({ a: numA, b: numB }),
});
const data = await res.json();
setResult(data.result);
setShowResult(true);
} catch {
// Fallback to local calculation if API fails
setResult(numA + numB);
setShowResult(true);
}
};
const handleReset = () => {
setA("");
setB("");
setResult(null);
setShowResult(false);
};
const handleLogout = async () => {
await logout();
router.push("/login");
};
if (isLoading) {
return (
<main style={styles.main}>
<div style={styles.loader}>Loading...</div>
</main>
);
}
if (!user) {
return null;
}
return (
<main style={styles.main}>
<div style={styles.header}>
<div style={styles.nav}>
<a href="/" style={styles.navLink}>Counter</a>
<span style={styles.navDivider}></span>
<span style={styles.navCurrent}>Sum</span>
<span style={styles.navDivider}></span>
<a href="/audit" style={styles.navLink}>Audit</a>
</div>
<div style={styles.userInfo}>
<span style={styles.userEmail}>{user.email}</span>
<button onClick={handleLogout} style={styles.logoutBtn}>
Sign out
</button>
</div>
</div>
<div style={styles.content}>
<div style={styles.card}>
<span style={styles.label}>Sum Calculator</span>
{!showResult ? (
<div style={styles.inputSection}>
<div style={styles.inputRow}>
<div style={styles.inputWrapper}>
<input
type="number"
value={a}
onChange={(e) => setA(e.target.value)}
placeholder="0"
style={styles.input}
aria-label="First number"
/>
</div>
<span style={styles.operator}>+</span>
<div style={styles.inputWrapper}>
<input
type="number"
value={b}
onChange={(e) => setB(e.target.value)}
placeholder="0"
style={styles.input}
aria-label="Second number"
/>
</div>
</div>
<button
onClick={handleSum}
style={styles.sumBtn}
disabled={a === "" && b === ""}
>
<span style={styles.equalsIcon}>=</span>
Calculate
</button>
</div>
) : (
<div style={styles.resultSection}>
<div style={styles.equation}>
<span style={styles.equationNum}>{parseFloat(a) || 0}</span>
<span style={styles.equationOp}>+</span>
<span style={styles.equationNum}>{parseFloat(b) || 0}</span>
<span style={styles.equationOp}>=</span>
</div>
<h1 style={styles.result}>{result}</h1>
<button onClick={handleReset} style={styles.resetBtn}>
<span style={styles.resetIcon}></span>
Try Again
</button>
</div>
)}
</div>
</div>
</main>
);
}
const styles: Record<string, React.CSSProperties> = {
main: {
minHeight: "100vh",
background: "linear-gradient(135deg, #0f0f23 0%, #1a1a3e 50%, #2d1b4e 100%)",
display: "flex",
flexDirection: "column",
},
loader: {
flex: 1,
display: "flex",
alignItems: "center",
justifyContent: "center",
fontFamily: "'DM Sans', system-ui, sans-serif",
color: "rgba(255, 255, 255, 0.5)",
fontSize: "1.125rem",
},
header: {
padding: "1.5rem 2rem",
borderBottom: "1px solid rgba(255, 255, 255, 0.06)",
display: "flex",
justifyContent: "space-between",
alignItems: "center",
},
nav: {
display: "flex",
alignItems: "center",
gap: "0.75rem",
},
navLink: {
fontFamily: "'DM Sans', system-ui, sans-serif",
color: "rgba(255, 255, 255, 0.5)",
fontSize: "0.875rem",
textDecoration: "none",
transition: "color 0.2s",
},
navDivider: {
color: "rgba(255, 255, 255, 0.2)",
fontSize: "0.75rem",
},
navCurrent: {
fontFamily: "'DM Sans', system-ui, sans-serif",
color: "#a78bfa",
fontSize: "0.875rem",
fontWeight: 600,
},
userInfo: {
display: "flex",
alignItems: "center",
gap: "1rem",
},
userEmail: {
fontFamily: "'DM Sans', system-ui, sans-serif",
color: "rgba(255, 255, 255, 0.6)",
fontSize: "0.875rem",
},
logoutBtn: {
fontFamily: "'DM Sans', system-ui, sans-serif",
padding: "0.5rem 1rem",
fontSize: "0.875rem",
fontWeight: 500,
background: "rgba(255, 255, 255, 0.05)",
color: "rgba(255, 255, 255, 0.7)",
border: "1px solid rgba(255, 255, 255, 0.1)",
borderRadius: "8px",
cursor: "pointer",
transition: "all 0.2s",
},
content: {
flex: 1,
display: "flex",
alignItems: "center",
justifyContent: "center",
padding: "2rem",
},
card: {
background: "rgba(255, 255, 255, 0.03)",
backdropFilter: "blur(10px)",
border: "1px solid rgba(255, 255, 255, 0.08)",
borderRadius: "32px",
padding: "3rem 4rem",
textAlign: "center",
boxShadow: "0 25px 50px -12px rgba(0, 0, 0, 0.5)",
minWidth: "400px",
},
label: {
fontFamily: "'DM Sans', system-ui, sans-serif",
display: "block",
color: "rgba(255, 255, 255, 0.4)",
fontSize: "0.875rem",
textTransform: "uppercase",
letterSpacing: "0.1em",
marginBottom: "2rem",
},
inputSection: {
display: "flex",
flexDirection: "column",
alignItems: "center",
gap: "2rem",
},
inputRow: {
display: "flex",
alignItems: "center",
gap: "1rem",
},
inputWrapper: {
position: "relative",
},
input: {
fontFamily: "'Instrument Serif', Georgia, serif",
fontSize: "3rem",
fontWeight: 400,
width: "120px",
padding: "0.75rem 1rem",
textAlign: "center",
background: "rgba(255, 255, 255, 0.05)",
border: "2px solid rgba(255, 255, 255, 0.1)",
borderRadius: "16px",
color: "#fff",
outline: "none",
transition: "border-color 0.2s, box-shadow 0.2s",
},
operator: {
fontFamily: "'Instrument Serif', Georgia, serif",
fontSize: "3rem",
color: "#a78bfa",
fontWeight: 400,
},
sumBtn: {
fontFamily: "'DM Sans', system-ui, sans-serif",
padding: "1rem 2.5rem",
fontSize: "1.125rem",
fontWeight: 600,
background: "linear-gradient(135deg, #6366f1 0%, #8b5cf6 100%)",
color: "#fff",
border: "none",
borderRadius: "16px",
cursor: "pointer",
display: "inline-flex",
alignItems: "center",
gap: "0.5rem",
transition: "transform 0.2s, box-shadow 0.2s",
boxShadow: "0 4px 14px rgba(99, 102, 241, 0.4)",
},
equalsIcon: {
fontSize: "1.5rem",
fontWeight: 400,
},
resultSection: {
display: "flex",
flexDirection: "column",
alignItems: "center",
},
equation: {
display: "flex",
alignItems: "center",
gap: "0.5rem",
marginBottom: "0.5rem",
},
equationNum: {
fontFamily: "'DM Sans', system-ui, sans-serif",
fontSize: "1.25rem",
color: "rgba(255, 255, 255, 0.6)",
},
equationOp: {
fontFamily: "'DM Sans', system-ui, sans-serif",
fontSize: "1.25rem",
color: "rgba(139, 92, 246, 0.8)",
},
result: {
fontFamily: "'Instrument Serif', Georgia, serif",
fontSize: "7rem",
fontWeight: 400,
color: "#fff",
margin: 0,
lineHeight: 1,
background: "linear-gradient(135deg, #fff 0%, #a78bfa 100%)",
WebkitBackgroundClip: "text",
WebkitTextFillColor: "transparent",
backgroundClip: "text",
},
resetBtn: {
fontFamily: "'DM Sans', system-ui, sans-serif",
marginTop: "2rem",
padding: "0.875rem 2rem",
fontSize: "1rem",
fontWeight: 500,
background: "rgba(255, 255, 255, 0.05)",
color: "rgba(255, 255, 255, 0.7)",
border: "1px solid rgba(255, 255, 255, 0.1)",
borderRadius: "12px",
cursor: "pointer",
display: "inline-flex",
alignItems: "center",
gap: "0.5rem",
transition: "all 0.2s",
},
resetIcon: {
fontSize: "1.25rem",
},
};