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"
},
"dependencies": {
"@cloudscape-design/components": "^3.0.1132",
"@cloudscape-design/global-styles": "^1.0.47",
"@emotion/react": "^11.14.0",
"@emotion/styled": "^11.14.1",
"@mui/material": "^7.3.5",
"@mui/x-date-pickers": "^8.19.0",
"axios": "^1.13.2",
"dayjs": "^1.11.19",
"react": "^19.2.0",
"react-dom": "^19.2.0",
"react-intl": "^7.1.14",

View File

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

View File

@ -1,9 +1,5 @@
import "./App.css";
import { Login } from "./pages/Login.tsx";
import {
ContentLayout,
SpaceBetween
} from "@cloudscape-design/components";
import icon from "./paella-icon.png";
import { Outlet, Route, Routes } from "react-router";
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 { NotFound } from "./pages/NotFound.tsx";
import { BuyReturn } from "./pages/BuyReturn.tsx";
import { Grid, Paper } from "@mui/material";
function App() {
const locallyStoredUserId = window.localStorage.getItem("paysplit-user-id");
const locallyStoredUserIdAsNumber = locallyStoredUserId ? parseInt(locallyStoredUserId) : null
const [userId, setUserId] = useState<number | null>(locallyStoredUserIdAsNumber);
const locallyStoredUserIdAsNumber = locallyStoredUserId
? parseInt(locallyStoredUserId)
: null;
const [userId, setUserId] = useState<number | null>(
locallyStoredUserIdAsNumber,
);
const storeUserId = (id: number) => {
window.localStorage.setItem("paysplit-user-id", id.toString());
setUserId(id);
}
};
return (
<ContentLayout defaultPadding maxContentWidth={500}>
<SpaceBetween size="m">
<Paper
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" />
<Routes>
<Route
@ -52,8 +62,8 @@ function App() {
</Route>
<Route path="*" element={<NotFound />} />
</Routes>
</SpaceBetween>
</ContentLayout>
</Grid>
</Paper>
);
}

View File

@ -3,12 +3,16 @@ 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={{}}>
<App />
<LocalizationProvider dateAdapter={AdapterDayjs}>
<App />
</LocalizationProvider>
</IntlProvider>
</BrowserRouter>
</StrictMode>,

View File

@ -1,33 +1,76 @@
import {
Button,
Container,
Header,
Input,
SpaceBetween,
} from "@cloudscape-design/components";
import { useContext, useState } from "react";
import { startBuyFlow } from "../api/client";
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 = () => {
const [quantity, setQuantity] = useState(0);
const { userId } = useContext(LoggedInPageContext)!;
const navigate = useNavigate();
const handleBuyFlow = () =>
startBuyFlow(userId, quantity).then(
(response) => (window.location.href = response.data.url),
);
return (
<Container>
<SpaceBetween size="s">
<Header>Buy tokens</Header>
<Input
type="number"
<Grid container direction="column" spacing={2} alignItems="center">
<Typography variant="h3">Buy tokens</Typography>
<Paper
variant="outlined"
sx={{
p: 5,
bgcolor: "aliceblue",
borderRadius: 4,
}}
>
<RadioGroup
value={quantity.toString()}
onChange={(e) => setQuantity(parseInt(e.detail.value))}
/>
<Button onClick={handleBuyFlow}>Buy</Button>
</SpaceBetween>
</Container>
onChange={(e) => setQuantity(parseInt(e.target.value))}
>
<FormControlLabel
value="1"
control={<Radio />}
label="1 token - 4€"
/>
<FormControlLabel
value="5"
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,18 +1,18 @@
import { Header, SpaceBetween, Spinner } from "@cloudscape-design/components";
import { useContext } from "react";
import { completeBuyFlow } from "../api/client";
import { LoggedInPageContext } from "../context/logged-in-page-context";
import { CircularProgress, Grid, Typography } from "@mui/material";
export const BuyReturn = () => {
const { userId } = useContext(LoggedInPageContext)!;
completeBuyFlow(userId).then(
(response) => (window.location.href = response.data.url),
);
(response) => (window.location.href = response.data.url),
);
return (
<SpaceBetween size="m" direction="horizontal">
<Spinner size="big" />
<Header>We are processing your request</Header>
</SpaceBetween>
<Grid container direction="column" spacing={6} alignItems="center">
<Typography variant="h5">We are processing your request</Typography>
<CircularProgress size={50} color="inherit" />
</Grid>
);
};

View File

@ -1,14 +1,15 @@
import {
Button,
Input,
Checkbox,
Header,
SpaceBetween,
} from "@cloudscape-design/components";
import { useContext, useState } from "react";
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 {
Button,
Checkbox,
FormControlLabel,
Grid,
TextField,
Typography,
} from "@mui/material";
export const Login = () => {
const [userName, setUserName] = useState("");
@ -25,31 +26,44 @@ export const Login = () => {
});
return (
<SpaceBetween size="s" alignItems={"center"}>
<Header>Login</Header>
<Input
<Grid container direction="column" spacing={2} alignItems="center">
<Typography variant="h3">Login</Typography>
<TextField
value={userName}
onChange={(e) => setUserName(e.detail.value)}
onChange={(e) => setUserName(e.target.value)}
placeholder="User"
/>
<Input
<TextField
type="password"
value={password}
onChange={(e) => setPassword(e.detail.value)}
onChange={(e) => setPassword(e.target.value)}
placeholder="Password"
/>
<Checkbox
checked={rememberMe}
onChange={() => setRememberMe(!rememberMe)}
<FormControlLabel
control={
<Checkbox
checked={rememberMe}
onChange={() => setRememberMe(!rememberMe)}
/>
}
label="Remember me"
/>
<Button
onClick={handleLoginFlow}
variant="contained"
color="primary"
sx={{ width: "100%" }}
>
Remember me
</Checkbox>
<Button onClick={handleLoginFlow}>Login</Button>
<Button>
<Link to="/register" style={{ textDecoration: "none" }}>
Create an account
</Link>
Login
</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 * as zod from "zod";
import { registerUser } from "../api/client";
import { useNavigate } from "react-router";
import { PreLoginPageContext } from "../context/pre-login-page-context";
import {
Alert,
Button,
Header,
Input,
SpaceBetween,
} from "@cloudscape-design/components";
import * as zod from "zod";
import { registerUser } from "../api/client";
import { Link, useNavigate } from "react-router";
import { PreLoginPageContext } from "../context/pre-login-page-context";
Grid,
Paper,
TextField,
Typography,
} from "@mui/material";
export const Register = () => {
const [userName, setUserName] = useState("");
@ -57,39 +58,62 @@ export const Register = () => {
});
return (
<SpaceBetween size="s" alignItems={"center"}>
<Header>Register</Header>
<Input
value={userName}
onChange={(e) => setUserName(e.detail.value)}
placeholder="User name"
/>
<Input
type="password"
value={password}
onChange={(e) => setPassword(e.detail.value)}
placeholder="Password"
/>
<Input
type="password"
value={passwordConfirm}
onChange={(e) => setPasswordConfirm(e.detail.value)}
placeholder="Confirm password"
/>
{passwordErrors.map((error) => (
<Alert type="error">{error}</Alert>
))}
<Button
disabled={passwordErrors.length !== 0}
onClick={handleRegisterFlow}
>
Register
</Button>
<Button>
<Link to="/login" style={{ textDecoration: "none" }}>
Login with existing account
</Link>
</Button>
</SpaceBetween>
<>
<Grid container direction="column" spacing={2} alignItems="center">
<Typography variant="h3">Register</Typography>
<TextField
value={userName}
onChange={(e) => setUserName(e.target.value)}
placeholder="User name"
/>
<TextField
type="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
placeholder="Password"
/>
<TextField
type="password"
value={passwordConfirm}
onChange={(e) => setPasswordConfirm(e.target.value)}
placeholder="Confirm password"
/>
<Button
disabled={passwordErrors.length !== 0}
variant="contained"
color="primary"
sx={{ width: "100%" }}
onClick={handleRegisterFlow}
>
Register
</Button>
<Button
onClick={() => navigate("/login")}
variant="contained"
sx={{ width: "100%" }}
color="secondary"
>
Login
</Button>
</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 {
Calendar,
Button,
SpaceBetween,
Container,
Header,
} from "@cloudscape-design/components";
import { Button, Grid, Paper, Typography } from "@mui/material";
import { DateCalendar } from "@mui/x-date-pickers";
import { useState } from "react";
import { FormattedDate } from "react-intl";
import { Link } from "react-router";
import { useNavigate } from "react-router";
import { Dayjs } from "dayjs";
export const SignUp = () => {
const [selectedDate, setSelectedDate] = useState<string | null>(null);
const [selectedDate, setSelectedDate] = useState<Dayjs | null>(null);
const navigate = useNavigate();
const tuesday = 2;
const thursday = 4;
const isDateEnabled = (date: Date) =>
[tuesday, thursday].includes(date.getDay());
const isDateDisabled = (day: Dayjs) =>
![tuesday, thursday].includes(day.day());
const availableSlots: number = 3;
const availableTokens: number = 0;
return (
<SpaceBetween size="s" alignItems={"center"}>
<Container>
<Header>Pick a date</Header>
<Calendar
value={selectedDate || ""}
onChange={(e) => setSelectedDate(e.detail.value)}
isDateEnabled={isDateEnabled}
<Grid container direction="column" spacing={2} alignItems="center">
<Typography variant="h3">Pick a date</Typography>
<Paper
variant="outlined"
sx={{
p: 1,
bgcolor: "aliceblue",
borderRadius: 4,
}}
>
<DateCalendar
value={selectedDate}
onChange={setSelectedDate}
shouldDisableDate={isDateDisabled}
slotProps={{
day: {
sx: {
fontSize: "0.9rem",
},
},
}}
/>
{selectedDate && (
<>
<Header>
<FormattedDate
value={selectedDate}
day="numeric"
month="long"
year="numeric"
/>
</Header>
{availableSlots === 0 ? (
<p>There are no available slots for this date.</p>
) : (
<>
<p>There are {availableSlots} available slots for this date.</p>
<Button disabled={!availableTokens}>Sign up</Button>
</>
)}
</>
)}
</Container>
{!availableTokens && (
<Container>
<p>
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>
</Container>
</Paper>
{selectedDate && (
<Paper
variant="outlined"
sx={{
p: 3,
bgcolor: "aliceblue",
borderRadius: 4,
}}
>
<Typography variant="h5">
<FormattedDate
value={selectedDate.day()}
day="numeric"
month="long"
year="numeric"
/>
</Typography>
{availableSlots === 0 ? (
<Typography variant="subtitle1">
There are no available slots for this date.
</Typography>
) : (
<>
<Typography variant="subtitle1">
There are {availableSlots} available slots for this date.
</Typography>
<Button
variant="contained"
color="primary"
sx={{ width: "100%" }}
disabled={!availableTokens}
>
Sign up
</Button>
</>
)}
</Paper>
)}
</SpaceBetween>
{!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({
server: {
...baseServerConfigBuilder("localhost"),
host: true
host: true,
},
preview: baseServerConfigBuilder("paysplit-app"),
plugins: [react()],