Replace cloudscape on behalf of MUI

main
MiguelMLorente 2025-11-29 21:21:25 +01:00
parent 26b228197e
commit 44b3ef8b70
11 changed files with 1035 additions and 408 deletions

986
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -11,9 +11,12 @@
"format": "prettier . --write" "format": "prettier . --write"
}, },
"dependencies": { "dependencies": {
"@cloudscape-design/components": "^3.0.1132", "@emotion/react": "^11.14.0",
"@cloudscape-design/global-styles": "^1.0.47", "@emotion/styled": "^11.14.1",
"@mui/material": "^7.3.5",
"@mui/x-date-pickers": "^8.19.0",
"axios": "^1.13.2", "axios": "^1.13.2",
"dayjs": "^1.11.19",
"react": "^19.2.0", "react": "^19.2.0",
"react-dom": "^19.2.0", "react-dom": "^19.2.0",
"react-intl": "^7.1.14", "react-intl": "^7.1.14",

View File

@ -1,4 +1,5 @@
.app-logo { .app-logo {
max-width: 40%; max-width: 200px;
margin: 0 30%; border-radius: 10%;
margin: auto;
} }

View File

@ -1,9 +1,5 @@
import "./App.css"; import "./App.css";
import { Login } from "./pages/Login.tsx"; import { Login } from "./pages/Login.tsx";
import {
ContentLayout,
SpaceBetween
} from "@cloudscape-design/components";
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 { SignUp } from "./pages/SignUp.tsx";
@ -14,19 +10,33 @@ import { PreLoginPageContext } from "./context/pre-login-page-context.ts";
import { LoggedInPageContext } from "./context/logged-in-page-context.ts"; import { LoggedInPageContext } from "./context/logged-in-page-context.ts";
import { NotFound } from "./pages/NotFound.tsx"; import { NotFound } from "./pages/NotFound.tsx";
import { BuyReturn } from "./pages/BuyReturn.tsx"; import { BuyReturn } from "./pages/BuyReturn.tsx";
import { Grid, Paper } from "@mui/material";
function App() { function App() {
const locallyStoredUserId = window.localStorage.getItem("paysplit-user-id"); const locallyStoredUserId = window.localStorage.getItem("paysplit-user-id");
const locallyStoredUserIdAsNumber = locallyStoredUserId ? parseInt(locallyStoredUserId) : null const locallyStoredUserIdAsNumber = locallyStoredUserId
const [userId, setUserId] = useState<number | null>(locallyStoredUserIdAsNumber); ? parseInt(locallyStoredUserId)
: null;
const [userId, setUserId] = useState<number | null>(
locallyStoredUserIdAsNumber,
);
const storeUserId = (id: number) => { const storeUserId = (id: number) => {
window.localStorage.setItem("paysplit-user-id", id.toString()); window.localStorage.setItem("paysplit-user-id", id.toString());
setUserId(id); setUserId(id);
} };
return ( return (
<ContentLayout defaultPadding maxContentWidth={500}> <Paper
<SpaceBetween size="m"> elevation={2}
sx={{
padding: "50px 20px",
bgcolor: "lightblue",
borderRadius: 4,
maxWidth: "400px",
margin: "auto",
}}
>
<Grid container direction="column" alignContent="center" spacing={2}>
<img className="app-logo" src={icon} alt="App logo" /> <img className="app-logo" src={icon} alt="App logo" />
<Routes> <Routes>
<Route <Route
@ -52,8 +62,8 @@ function App() {
</Route> </Route>
<Route path="*" element={<NotFound />} /> <Route path="*" element={<NotFound />} />
</Routes> </Routes>
</SpaceBetween> </Grid>
</ContentLayout> </Paper>
); );
} }

View File

@ -3,12 +3,16 @@ 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,33 +1,76 @@
import {
Button,
Container,
Header,
Input,
SpaceBetween,
} from "@cloudscape-design/components";
import { useContext, useState } from "react"; import { useContext, useState } from "react";
import { startBuyFlow } from "../api/client"; import { startBuyFlow } from "../api/client";
import { LoggedInPageContext } from "../context/logged-in-page-context"; import { LoggedInPageContext } from "../context/logged-in-page-context";
import {
Button,
FormControlLabel,
Grid,
Paper,
Radio,
RadioGroup,
Typography,
} from "@mui/material";
import { useNavigate } from "react-router";
export const Buy = () => { export const Buy = () => {
const [quantity, setQuantity] = useState(0); const [quantity, setQuantity] = useState(0);
const { userId } = useContext(LoggedInPageContext)!; const { userId } = useContext(LoggedInPageContext)!;
const navigate = useNavigate();
const handleBuyFlow = () => const handleBuyFlow = () =>
startBuyFlow(userId, quantity).then( startBuyFlow(userId, quantity).then(
(response) => (window.location.href = response.data.url), (response) => (window.location.href = response.data.url),
); );
return ( return (
<Container> <Grid container direction="column" spacing={2} alignItems="center">
<SpaceBetween size="s"> <Typography variant="h3">Buy tokens</Typography>
<Header>Buy tokens</Header>
<Input <Paper
type="number" variant="outlined"
sx={{
p: 5,
bgcolor: "aliceblue",
borderRadius: 4,
}}
>
<RadioGroup
value={quantity.toString()} value={quantity.toString()}
onChange={(e) => setQuantity(parseInt(e.detail.value))} onChange={(e) => setQuantity(parseInt(e.target.value))}
>
<FormControlLabel
value="1"
control={<Radio />}
label="1 token - 4€"
/> />
<Button onClick={handleBuyFlow}>Buy</Button> <FormControlLabel
</SpaceBetween> value="5"
</Container> control={<Radio />}
label="5 tokens - 20€"
/>
<FormControlLabel
value="10"
control={<Radio />}
label="10 tokens - 40"
/>
</RadioGroup>
</Paper>
<Button
onClick={handleBuyFlow}
variant="contained"
color="primary"
sx={{ width: "100%" }}
>
Buy
</Button>
<Button
onClick={() => navigate("/signup")}
variant="contained"
color="secondary"
sx={{ width: "100%" }}
>
Back
</Button>
</Grid>
); );
}; };

View File

@ -1,7 +1,7 @@
import { Header, SpaceBetween, Spinner } from "@cloudscape-design/components";
import { useContext } from "react"; import { useContext } from "react";
import { completeBuyFlow } from "../api/client"; import { completeBuyFlow } from "../api/client";
import { LoggedInPageContext } from "../context/logged-in-page-context"; import { LoggedInPageContext } from "../context/logged-in-page-context";
import { CircularProgress, Grid, Typography } from "@mui/material";
export const BuyReturn = () => { export const BuyReturn = () => {
const { userId } = useContext(LoggedInPageContext)!; const { userId } = useContext(LoggedInPageContext)!;
@ -10,9 +10,9 @@ export const BuyReturn = () => {
); );
return ( return (
<SpaceBetween size="m" direction="horizontal"> <Grid container direction="column" spacing={6} alignItems="center">
<Spinner size="big" /> <Typography variant="h5">We are processing your request</Typography>
<Header>We are processing your request</Header> <CircularProgress size={50} color="inherit" />
</SpaceBetween> </Grid>
); );
}; };

View File

@ -1,14 +1,15 @@
import {
Button,
Input,
Checkbox,
Header,
SpaceBetween,
} from "@cloudscape-design/components";
import { useContext, useState } from "react"; import { useContext, useState } from "react";
import { login } from "../api/client"; import { login } from "../api/client";
import { Link, useNavigate } from "react-router"; import { useNavigate } from "react-router";
import { PreLoginPageContext } from "../context/pre-login-page-context"; import { PreLoginPageContext } from "../context/pre-login-page-context";
import {
Button,
Checkbox,
FormControlLabel,
Grid,
TextField,
Typography,
} from "@mui/material";
export const Login = () => { export const Login = () => {
const [userName, setUserName] = useState(""); const [userName, setUserName] = useState("");
@ -25,31 +26,44 @@ export const Login = () => {
}); });
return ( return (
<SpaceBetween size="s" alignItems={"center"}> <Grid container direction="column" spacing={2} alignItems="center">
<Header>Login</Header> <Typography variant="h3">Login</Typography>
<Input <TextField
value={userName} value={userName}
onChange={(e) => setUserName(e.detail.value)} onChange={(e) => setUserName(e.target.value)}
placeholder="User" placeholder="User"
/> />
<Input <TextField
type="password" type="password"
value={password} value={password}
onChange={(e) => setPassword(e.detail.value)} onChange={(e) => setPassword(e.target.value)}
placeholder="Password" placeholder="Password"
/> />
<FormControlLabel
control={
<Checkbox <Checkbox
checked={rememberMe} checked={rememberMe}
onChange={() => setRememberMe(!rememberMe)} onChange={() => setRememberMe(!rememberMe)}
/>
}
label="Remember me"
/>
<Button
onClick={handleLoginFlow}
variant="contained"
color="primary"
sx={{ width: "100%" }}
> >
Remember me Login
</Checkbox>
<Button onClick={handleLoginFlow}>Login</Button>
<Button>
<Link to="/register" style={{ textDecoration: "none" }}>
Create an account
</Link>
</Button> </Button>
</SpaceBetween> <Button
variant="contained"
color="secondary"
sx={{ width: "100%" }}
onClick={() => navigate("/register")}
>
Create an account
</Button>
</Grid>
); );
}; };

View File

@ -1,15 +1,16 @@
import { useContext, useState } from "react"; import { useContext, useState } from "react";
import * as zod from "zod";
import { registerUser } from "../api/client";
import { useNavigate } from "react-router";
import { PreLoginPageContext } from "../context/pre-login-page-context";
import { import {
Alert, Alert,
Button, Button,
Header, Grid,
Input, Paper,
SpaceBetween, TextField,
} from "@cloudscape-design/components"; Typography,
import * as zod from "zod"; } from "@mui/material";
import { registerUser } from "../api/client";
import { Link, useNavigate } from "react-router";
import { PreLoginPageContext } from "../context/pre-login-page-context";
export const Register = () => { export const Register = () => {
const [userName, setUserName] = useState(""); const [userName, setUserName] = useState("");
@ -57,39 +58,62 @@ export const Register = () => {
}); });
return ( return (
<SpaceBetween size="s" alignItems={"center"}> <>
<Header>Register</Header> <Grid container direction="column" spacing={2} alignItems="center">
<Input <Typography variant="h3">Register</Typography>
<TextField
value={userName} value={userName}
onChange={(e) => setUserName(e.detail.value)} onChange={(e) => setUserName(e.target.value)}
placeholder="User name" placeholder="User name"
/> />
<Input <TextField
type="password" type="password"
value={password} value={password}
onChange={(e) => setPassword(e.detail.value)} onChange={(e) => setPassword(e.target.value)}
placeholder="Password" placeholder="Password"
/> />
<Input <TextField
type="password" type="password"
value={passwordConfirm} value={passwordConfirm}
onChange={(e) => setPasswordConfirm(e.detail.value)} onChange={(e) => setPasswordConfirm(e.target.value)}
placeholder="Confirm password" placeholder="Confirm password"
/> />
{passwordErrors.map((error) => (
<Alert type="error">{error}</Alert>
))}
<Button <Button
disabled={passwordErrors.length !== 0} disabled={passwordErrors.length !== 0}
variant="contained"
color="primary"
sx={{ width: "100%" }}
onClick={handleRegisterFlow} onClick={handleRegisterFlow}
> >
Register Register
</Button> </Button>
<Button> <Button
<Link to="/login" style={{ textDecoration: "none" }}> onClick={() => navigate("/login")}
Login with existing account variant="contained"
</Link> sx={{ width: "100%" }}
color="secondary"
>
Login
</Button> </Button>
</SpaceBetween> </Grid>
{!!passwordErrors.length && (
<Paper
elevation={2}
sx={{
p: 1,
bgcolor: "white",
borderRadius: 4,
maxWidth: "400px",
margin: "auto",
}}
>
<Grid container direction="column" spacing={2}>
{passwordErrors.map((error) => (
<Alert severity="error">{error}</Alert>
))}
</Grid>
</Paper>
)}
</>
); );
}; };

View File

@ -1,65 +1,105 @@
import { import { Button, Grid, Paper, Typography } from "@mui/material";
Calendar, import { DateCalendar } from "@mui/x-date-pickers";
Button,
SpaceBetween,
Container,
Header,
} from "@cloudscape-design/components";
import { useState } from "react"; import { useState } from "react";
import { FormattedDate } from "react-intl"; import { FormattedDate } from "react-intl";
import { Link } from "react-router"; import { useNavigate } from "react-router";
import { Dayjs } from "dayjs";
export const SignUp = () => { export const SignUp = () => {
const [selectedDate, setSelectedDate] = useState<string | null>(null); const [selectedDate, setSelectedDate] = useState<Dayjs | null>(null);
const navigate = useNavigate();
const tuesday = 2; const tuesday = 2;
const thursday = 4; const thursday = 4;
const isDateEnabled = (date: Date) => const isDateDisabled = (day: Dayjs) =>
[tuesday, thursday].includes(date.getDay()); ![tuesday, thursday].includes(day.day());
const availableSlots: number = 3; const availableSlots: number = 3;
const availableTokens: number = 0; const availableTokens: number = 0;
return ( return (
<SpaceBetween size="s" alignItems={"center"}> <Grid container direction="column" spacing={2} alignItems="center">
<Container> <Typography variant="h3">Pick a date</Typography>
<Header>Pick a date</Header> <Paper
<Calendar variant="outlined"
value={selectedDate || ""} sx={{
onChange={(e) => setSelectedDate(e.detail.value)} p: 1,
isDateEnabled={isDateEnabled} bgcolor: "aliceblue",
/> borderRadius: 4,
{selectedDate && ( }}
<> >
<Header> <DateCalendar
<FormattedDate
value={selectedDate} value={selectedDate}
onChange={setSelectedDate}
shouldDisableDate={isDateDisabled}
slotProps={{
day: {
sx: {
fontSize: "0.9rem",
},
},
}}
/>
</Paper>
{selectedDate && (
<Paper
variant="outlined"
sx={{
p: 3,
bgcolor: "aliceblue",
borderRadius: 4,
}}
>
<Typography variant="h5">
<FormattedDate
value={selectedDate.day()}
day="numeric" day="numeric"
month="long" month="long"
year="numeric" year="numeric"
/> />
</Header> </Typography>
{availableSlots === 0 ? ( {availableSlots === 0 ? (
<p>There are no available slots for this date.</p> <Typography variant="subtitle1">
There are no available slots for this date.
</Typography>
) : ( ) : (
<> <>
<p>There are {availableSlots} available slots for this date.</p> <Typography variant="subtitle1">
<Button disabled={!availableTokens}>Sign up</Button> There are {availableSlots} available slots for this date.
</> </Typography>
)} <Button
</> variant="contained"
)} color="primary"
</Container> sx={{ width: "100%" }}
{!availableTokens && ( disabled={!availableTokens}
<Container> >
<p> Sign up
You don't have any tokens to sign up to a slot, please buy here.
</p>
<Button>
<Link to="/buy" style={{ textDecoration: "none" }}>
Buy
</Link>
</Button> </Button>
</Container> </>
)} )}
</SpaceBetween> </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

@ -21,7 +21,7 @@ const baseServerConfigBuilder = (domain: string) => ({
export default defineConfig({ export default defineConfig({
server: { server: {
...baseServerConfigBuilder("localhost"), ...baseServerConfigBuilder("localhost"),
host: true host: true,
}, },
preview: baseServerConfigBuilder("paysplit-app"), preview: baseServerConfigBuilder("paysplit-app"),
plugins: [react()], plugins: [react()],