silly features in place
This commit is contained in:
parent
c5d3c7f4c9
commit
322bdd3e6e
5 changed files with 997 additions and 6 deletions
174
backend/main.py
174
backend/main.py
|
|
@ -1,11 +1,12 @@
|
|||
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 sqlalchemy import select
|
||||
from sqlalchemy import select, func, desc
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from pydantic import BaseModel
|
||||
from database import engine, get_db, Base
|
||||
from models import Counter, User
|
||||
from models import Counter, User, SumRecord, CounterRecord
|
||||
from auth import (
|
||||
ACCESS_TOKEN_EXPIRE_MINUTES,
|
||||
COOKIE_NAME,
|
||||
|
|
@ -129,10 +130,175 @@ async def get_counter(
|
|||
@app.post("/api/counter/increment")
|
||||
async def increment_counter(
|
||||
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)
|
||||
value_before = counter.value
|
||||
counter.value += 1
|
||||
|
||||
record = CounterRecord(
|
||||
user_id=current_user.id,
|
||||
value_before=value_before,
|
||||
value_after=counter.value,
|
||||
)
|
||||
db.add(record)
|
||||
await db.commit()
|
||||
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,
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -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 database import Base
|
||||
|
||||
|
|
@ -17,3 +18,24 @@ class User(Base):
|
|||
email: Mapped[str] = mapped_column(String(255), unique=True, nullable=False, index=True)
|
||||
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
428
frontend/app/audit/page.tsx
Normal 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)",
|
||||
},
|
||||
};
|
||||
|
||||
|
|
@ -56,6 +56,13 @@ export default function Home() {
|
|||
return (
|
||||
<main style={styles.main}>
|
||||
<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}>
|
||||
<span style={styles.userEmail}>{user.email}</span>
|
||||
<button onClick={handleLogout} style={styles.logoutBtn}>
|
||||
|
|
@ -97,11 +104,35 @@ const styles: Record<string, React.CSSProperties> = {
|
|||
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",
|
||||
justifyContent: "flex-end",
|
||||
gap: "1rem",
|
||||
},
|
||||
userEmail: {
|
||||
|
|
|
|||
344
frontend/app/sum/page.tsx
Normal file
344
frontend/app/sum/page.tsx
Normal 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",
|
||||
},
|
||||
};
|
||||
|
||||
Loading…
Add table
Add a link
Reference in a new issue