Implement session selector and summary
parent
f96d20eaf2
commit
97fee33264
|
|
@ -2,7 +2,7 @@ import "./App.css";
|
|||
import { Login } from "./pages/Login.tsx";
|
||||
import icon from "./paella-icon.png";
|
||||
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 { Buy } from "./pages/Buy.tsx";
|
||||
import { useState } from "react";
|
||||
|
|
@ -25,10 +25,11 @@ function App() {
|
|||
<Paper
|
||||
elevation={2}
|
||||
sx={{
|
||||
padding: "50px 20px",
|
||||
padding: "50px 30px",
|
||||
bgcolor: "lightblue",
|
||||
borderRadius: 4,
|
||||
maxWidth: "400px",
|
||||
maxWidth: "500px",
|
||||
width: "fit-content",
|
||||
margin: "auto",
|
||||
}}
|
||||
>
|
||||
|
|
@ -52,7 +53,7 @@ function App() {
|
|||
</LoggedInPageContext.Provider>
|
||||
}
|
||||
>
|
||||
<Route path="/signup" element={<SignUp />} />
|
||||
<Route path="/signup" element={<SignUpPage />} />
|
||||
<Route path="/buy" element={<Buy />} />
|
||||
<Route path="/buy/return" element={<BuyReturn />} />
|
||||
</Route>
|
||||
|
|
|
|||
|
|
@ -31,6 +31,7 @@ export interface Session {
|
|||
size: number;
|
||||
userCount: number;
|
||||
includesRequester: boolean;
|
||||
status: "SCHEDULED" | "OPEN" | "CLOSED" | "CANCELLED";
|
||||
date: string;
|
||||
}
|
||||
|
||||
|
|
@ -52,3 +53,15 @@ export const leaveSession = (userId: string, sessionId: string) =>
|
|||
AxiosResponse<Session[]>
|
||||
>
|
||||
).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 { BrowserRouter } from "react-router";
|
||||
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(
|
||||
<StrictMode>
|
||||
<BrowserRouter>
|
||||
<IntlProvider locale="es-ES" messages={{}}>
|
||||
<LocalizationProvider dateAdapter={AdapterDayjs}>
|
||||
<App />
|
||||
</LocalizationProvider>
|
||||
</IntlProvider>
|
||||
</BrowserRouter>
|
||||
</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/cancel",
|
||||
"/session",
|
||||
"/tokens",
|
||||
];
|
||||
const baseServerConfigBuilder = (domain: string) => ({
|
||||
proxy: Object.fromEntries(
|
||||
|
|
|
|||
Loading…
Reference in New Issue