Implement session selector and summary
parent
f96d20eaf2
commit
97fee33264
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
|
|
||||||
|
|
@ -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>,
|
||||||
|
|
|
||||||
|
|
@ -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>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
@ -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>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
@ -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>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
@ -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>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
@ -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>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
@ -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(
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue