Implement session selector and summary

main
MiguelMLorente 2025-12-06 11:29:02 +01:00
parent f96d20eaf2
commit 97fee33264
9 changed files with 325 additions and 149 deletions

View File

@ -2,7 +2,7 @@ import "./App.css";
import { Login } from "./pages/Login.tsx"; import { Login } from "./pages/Login.tsx";
import icon from "./paella-icon.png"; import icon from "./paella-icon.png";
import { Outlet, Route, Routes } from "react-router"; import { Outlet, Route, Routes } from "react-router";
import { SignUp } from "./pages/SignUp.tsx"; import { SignUpPage } from "./pages/signup/SignUpPage.tsx";
import { Register } from "./pages/Register.tsx"; import { Register } from "./pages/Register.tsx";
import { Buy } from "./pages/Buy.tsx"; import { Buy } from "./pages/Buy.tsx";
import { useState } from "react"; import { useState } from "react";
@ -25,10 +25,11 @@ function App() {
<Paper <Paper
elevation={2} elevation={2}
sx={{ sx={{
padding: "50px 20px", padding: "50px 30px",
bgcolor: "lightblue", bgcolor: "lightblue",
borderRadius: 4, borderRadius: 4,
maxWidth: "400px", maxWidth: "500px",
width: "fit-content",
margin: "auto", margin: "auto",
}} }}
> >
@ -52,7 +53,7 @@ function App() {
</LoggedInPageContext.Provider> </LoggedInPageContext.Provider>
} }
> >
<Route path="/signup" element={<SignUp />} /> <Route path="/signup" element={<SignUpPage />} />
<Route path="/buy" element={<Buy />} /> <Route path="/buy" element={<Buy />} />
<Route path="/buy/return" element={<BuyReturn />} /> <Route path="/buy/return" element={<BuyReturn />} />
</Route> </Route>

View File

@ -31,6 +31,7 @@ export interface Session {
size: number; size: number;
userCount: number; userCount: number;
includesRequester: boolean; includesRequester: boolean;
status: "SCHEDULED" | "OPEN" | "CLOSED" | "CANCELLED";
date: string; date: string;
} }
@ -52,3 +53,15 @@ export const leaveSession = (userId: string, sessionId: string) =>
AxiosResponse<Session[]> AxiosResponse<Session[]>
> >
).then((response) => response.data); ).then((response) => response.data);
export interface Tokens {
available: number;
purchased: number;
consumed: number;
used: number;
}
export const getAvailableTokenCount = (userId: string) =>
(
axios.get(`/tokens?userId=${userId}`) as Promise<AxiosResponse<Tokens>>
).then((response) => response.data);

View File

@ -3,16 +3,12 @@ import { createRoot } from "react-dom/client";
import App from "./App.tsx"; import App from "./App.tsx";
import { BrowserRouter } from "react-router"; import { BrowserRouter } from "react-router";
import { IntlProvider } from "react-intl"; import { IntlProvider } from "react-intl";
import { LocalizationProvider } from "@mui/x-date-pickers";
import { AdapterDayjs } from "@mui/x-date-pickers/AdapterDayjs";
createRoot(document.getElementById("root")!).render( createRoot(document.getElementById("root")!).render(
<StrictMode> <StrictMode>
<BrowserRouter> <BrowserRouter>
<IntlProvider locale="es-ES" messages={{}}> <IntlProvider locale="es-ES" messages={{}}>
<LocalizationProvider dateAdapter={AdapterDayjs}>
<App /> <App />
</LocalizationProvider>
</IntlProvider> </IntlProvider>
</BrowserRouter> </BrowserRouter>
</StrictMode>, </StrictMode>,

View File

@ -1,140 +0,0 @@
import {
Alert,
Button,
Grid,
Paper,
Table,
TableBody,
TableCell,
TableContainer,
TableHead,
TableRow,
Typography,
} from "@mui/material";
import { useContext, useMemo } from "react";
import { FormattedDate } from "react-intl";
import { useNavigate } from "react-router";
import { Async } from "react-async";
import {
getAllSessions,
joinSession,
leaveSession,
type Session,
} from "../api/client";
import { LoggedInPageContext } from "../context/logged-in-page-context";
export const SignUp = () => {
const navigate = useNavigate();
const { userId } = useContext(LoggedInPageContext)!;
const sessionsPromise = useMemo(() => getAllSessions(userId), []);
const availableTokens: number = 0;
return (
<Grid container direction="column" spacing={2} alignItems="center">
<Typography variant="h3">Pick a date</Typography>
<Paper
variant="outlined"
sx={{
p: 3,
bgcolor: "aliceblue",
borderRadius: 4,
}}
>
<Async promise={sessionsPromise}>
<Async.Loading>Loading</Async.Loading>
<Async.Fulfilled<Session[]>>
{(sessions) =>
sessions.length === 0 ? (
<Alert severity="warning">No sessions are available</Alert>
) : (
<TableContainer component={Paper}>
<Table>
<TableHead>
<TableRow>
<TableCell>Date</TableCell>
<TableCell>Slots</TableCell>
<TableCell>Sign-in/out</TableCell>
</TableRow>
</TableHead>
<TableBody>
{sessions.map((session) => (
<TableRow key={session.id}>
<TableCell>
<FormattedDate
value={new Date(session.date)}
day="numeric"
month="short"
year="numeric"
/>
</TableCell>
<TableCell>
{session.userCount}/{session.size}
</TableCell>
<TableCell>
{session.includesRequester ? (
<Button
variant="contained"
color="primary"
sx={{ width: "100%" }}
onClick={async () => {
await leaveSession(userId, session.id);
window.location.reload();
}}
>
Sign out
</Button>
) : session.userCount >= session.size ? (
"UNAVAILABLE"
) : (
<Button
variant="contained"
color="primary"
sx={{ width: "100%" }}
onClick={async () => {
await joinSession(userId, session.id);
window.location.reload();
}}
>
Sign in
</Button>
)}
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</TableContainer>
)
}
</Async.Fulfilled>
<Async.Rejected>
<Alert severity="error">Something went wrong</Alert>
</Async.Rejected>
</Async>
</Paper>
{!availableTokens && (
<Paper
variant="outlined"
sx={{
p: 3,
bgcolor: "aliceblue",
borderRadius: 4,
}}
>
<Typography variant="subtitle1">
You don't have any tokens to sign up
</Typography>
<Button
variant="contained"
color="secondary"
sx={{ width: "100%" }}
onClick={() => navigate("/buy")}
>
Buy
</Button>
</Paper>
)}
</Grid>
);
};

View File

@ -0,0 +1,106 @@
import { Button } from "@mui/material";
import {
joinSession,
leaveSession,
type Session,
type Tokens,
} from "../../api/client";
import { LoggedInPageContext } from "../../context/logged-in-page-context";
import { useContext } from "react";
import { Async } from "react-async";
import { useNavigate } from "react-router";
export const SessionAction = (props: {
session: Session;
tokensPromise: Promise<Tokens>;
}) => {
const { session, tokensPromise } = props;
const { userId } = useContext(LoggedInPageContext)!;
const navigate = useNavigate();
if (session.status !== "OPEN") {
return (
<Button
variant="contained"
disabled
color="primary"
sx={{ width: "100%" }}
>
SESSION {session.status}
</Button>
);
}
if (session.includesRequester) {
return (
<Button
variant="contained"
color="primary"
sx={{ width: "100%" }}
onClick={async () => {
await leaveSession(userId, session.id);
window.location.reload();
}}
>
Sign out
</Button>
);
}
if (session.userCount >= session.size) {
return (
<Button
variant="contained"
disabled
color="primary"
sx={{ width: "100%" }}
>
SESSION FULL
</Button>
);
}
const renderLoadingButton = () => (
<Button variant="contained" color="primary" disabled sx={{ width: "100%" }}>
LOADING
</Button>
);
const renderBuyTokensButton = () => (
<Button
variant="contained"
color="primary"
sx={{ width: "100%" }}
onClick={() => navigate("/buy")}
>
BUY TOKENS
</Button>
);
const renderSignInButton = () => (
<Button
variant="contained"
color="primary"
sx={{ width: "100%" }}
onClick={async () => {
await joinSession(userId, session.id);
window.location.reload();
}}
>
SIGN UP
</Button>
);
return (
<Async promise={tokensPromise}>
<Async.Loading>{renderLoadingButton()}</Async.Loading>
<Async.Fulfilled<Tokens>>
{(tokens) =>
tokens.available === 0
? renderBuyTokensButton()
: renderSignInButton()
}
</Async.Fulfilled>
</Async>
);
};

View File

@ -0,0 +1,62 @@
import {
Paper,
Table,
TableBody,
TableCell,
TableContainer,
TableHead,
TableRow,
} from "@mui/material";
import { FormattedDate } from "react-intl";
import { type Session, type Tokens } from "../../api/client";
import { SessionAction } from "./SessionAction";
export const SessionPicker = (props: {
sessions: Session[];
tokensPromise: Promise<Tokens>;
}) => {
const { sessions, tokensPromise } = props;
return (
<TableContainer component={Paper}>
<Table>
<TableHead>
<TableRow>
<TableCell align="center" sx={{ fontWeight: "bold" }}>
Date
</TableCell>
<TableCell align="center" sx={{ fontWeight: "bold" }}>
Slots
</TableCell>
<TableCell align="center" sx={{ fontWeight: "bold" }}>
Sign-in/out
</TableCell>
</TableRow>
</TableHead>
<TableBody>
{sessions.map((session) => (
<TableRow key={session.id}>
<TableCell align="center">
<FormattedDate
value={new Date(session.date)}
day="numeric"
month="short"
year="numeric"
/>
</TableCell>
<TableCell align="center">
{session.userCount}/{session.size}
</TableCell>
<TableCell align="center">
<SessionAction
session={session}
tokensPromise={tokensPromise}
/>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</TableContainer>
);
};

View File

@ -0,0 +1,59 @@
import { Alert, Grid, Paper, Typography } from "@mui/material";
import { useContext, useMemo } from "react";
import { Async } from "react-async";
import {
getAllSessions,
getAvailableTokenCount,
type Session,
type Tokens,
} from "../../api/client";
import { LoggedInPageContext } from "../../context/logged-in-page-context";
import { SessionPicker } from "./SessionPicker";
import { TokensSummary } from "./TokensSummary";
export const SignUpPage = () => {
const { userId } = useContext(LoggedInPageContext)!;
const sessionsPromise = useMemo(() => getAllSessions(userId), []);
const tokensPromise = getAvailableTokenCount(userId);
return (
<Grid container direction="column" spacing={2} alignItems="center">
<Typography variant="h3">Pick a date</Typography>
<Paper
variant="outlined"
sx={{
p: 3,
bgcolor: "aliceblue",
borderRadius: 4,
}}
>
<Async promise={sessionsPromise}>
<Async.Loading>Loading</Async.Loading>
<Async.Fulfilled<Session[]>>
{(sessions) =>
sessions.length === 0 ? (
<Alert severity="warning">No sessions are available</Alert>
) : (
<SessionPicker
sessions={sessions}
tokensPromise={tokensPromise}
/>
)
}
</Async.Fulfilled>
<Async.Rejected>
<Alert severity="error">Something went wrong</Alert>
</Async.Rejected>
</Async>
</Paper>
<Async promise={tokensPromise}>
<Async.Fulfilled<Tokens>>
{(tokens) => <TokensSummary tokens={tokens} />}
</Async.Fulfilled>
<Async.Rejected>
<Alert severity="error">Something went wrong</Alert>
</Async.Rejected>
</Async>
</Grid>
);
};

View File

@ -0,0 +1,78 @@
import { useNavigate } from "react-router";
import type { Tokens } from "../../api/client";
import {
Button,
Grid,
Paper,
Table,
TableBody,
TableCell,
TableContainer,
TableHead,
TableRow,
Typography,
} from "@mui/material";
export const TokensSummary = (props: { tokens: Tokens }) => {
const { tokens } = props;
const { purchased, consumed, used, available } = tokens;
const navigate = useNavigate();
return (
<Paper
variant="outlined"
sx={{
p: 3,
bgcolor: "aliceblue",
borderRadius: 4,
}}
>
<Grid container direction="column" spacing={3} alignItems="center">
<Typography variant="h6">Your tokens summary</Typography>
<TableContainer component={Paper}>
<Table>
<TableHead>
<TableRow>
<TableCell align="center" sx={{ fontWeight: "bold" }}>
Purchased
</TableCell>
<TableCell align="center" sx={{ fontWeight: "bold" }}>
Consumed
</TableCell>
<TableCell align="center" sx={{ fontWeight: "bold" }}>
Allocated
</TableCell>
<TableCell align="center" sx={{ fontWeight: "bold" }}>
Available
</TableCell>
</TableRow>
</TableHead>
<TableBody>
<TableRow>
<TableCell align="center">{purchased}</TableCell>
<TableCell align="center">{consumed}</TableCell>
<TableCell align="center">{used}</TableCell>
<TableCell align="center">{available}</TableCell>
</TableRow>
</TableBody>
</Table>
</TableContainer>
<Button
variant="contained"
color="primary"
sx={{ width: "100%" }}
onClick={() => navigate("/buy")}
>
Buy tokens
</Button>
<Button
variant="contained"
color="secondary"
sx={{ width: "100%" }}
onClick={() => navigate("/invoices")}
>
See all purchases
</Button>
</Grid>
</Paper>
);
};

View File

@ -7,6 +7,7 @@ const serverRoutes = [
"/buy/complete", "/buy/complete",
"/buy/cancel", "/buy/cancel",
"/session", "/session",
"/tokens",
]; ];
const baseServerConfigBuilder = (domain: string) => ({ const baseServerConfigBuilder = (domain: string) => ({
proxy: Object.fromEntries( proxy: Object.fromEntries(