Application skeleton

main
MiguelMLorente 2025-11-16 16:25:11 +01:00
parent 68bb680abd
commit de7e79207c
19 changed files with 4147 additions and 0 deletions

24
.gitignore vendored Normal file
View File

@ -0,0 +1,24 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
dist
dist-ssr
*.local
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?

3
.prettierignore Normal file
View File

@ -0,0 +1,3 @@
# Ignore artifacts:
build
coverage

1
.prettierrc Normal file
View File

@ -0,0 +1 @@
{}

23
eslint.config.js Normal file
View File

@ -0,0 +1,23 @@
import js from "@eslint/js";
import globals from "globals";
import reactHooks from "eslint-plugin-react-hooks";
import reactRefresh from "eslint-plugin-react-refresh";
import tseslint from "typescript-eslint";
import { defineConfig, globalIgnores } from "eslint/config";
export default defineConfig([
globalIgnores(["dist"]),
{
files: ["**/*.{ts,tsx}"],
extends: [
js.configs.recommended,
tseslint.configs.recommended,
reactHooks.configs.flat.recommended,
reactRefresh.configs.vite,
],
languageOptions: {
ecmaVersion: 2020,
globals: globals.browser,
},
},
]);

12
index.html Normal file
View File

@ -0,0 +1,12 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>paysplit</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>

3770
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

39
package.json Normal file
View File

@ -0,0 +1,39 @@
{
"name": "paysplit",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "tsc -b && vite build",
"lint": "eslint paysplit",
"preview": "vite preview",
"format": "prettier . --write"
},
"dependencies": {
"@cloudscape-design/components": "^3.0.1132",
"@cloudscape-design/global-styles": "^1.0.47",
"react": "^19.2.0",
"react-dom": "^19.2.0",
"react-intl": "^7.1.14",
"react-router": "^7.9.6"
},
"devDependencies": {
"@eslint/js": "^9.39.1",
"@types/node": "^24.10.0",
"@types/react": "^19.2.2",
"@types/react-dom": "^19.2.2",
"@vitejs/plugin-react": "^5.1.0",
"eslint": "^9.39.1",
"eslint-plugin-react-hooks": "^7.0.1",
"eslint-plugin-react-refresh": "^0.4.24",
"globals": "^16.5.0",
"prettier": "3.6.2",
"typescript": "~5.9.3",
"typescript-eslint": "^8.46.3",
"vite": "npm:rolldown-vite@7.2.2"
},
"overrides": {
"vite": "npm:rolldown-vite@7.2.2"
}
}

4
src/App.css Normal file
View File

@ -0,0 +1,4 @@
.app-logo {
max-width: 40%;
margin: 0 30%;
}

24
src/App.tsx Normal file
View File

@ -0,0 +1,24 @@
import "./App.css";
import { Login } from "./pages/Login.tsx";
import { ContentLayout, SpaceBetween } from "@cloudscape-design/components";
import icon from "./paella-icon.png";
import { Route, Routes } from "react-router";
import { SignUp } from "./pages/SignUp.tsx";
import { Register } from "./pages/Register.tsx";
function App() {
return (
<ContentLayout defaultPadding maxContentWidth={500}>
<SpaceBetween size="m">
<img className="app-logo" src={icon} alt="App logo" />
<Routes>
<Route path="/" element={<Login />} />
<Route path="/signup" element={<SignUp />} />
<Route path="/register" element={<Register />} />
</Routes>
</SpaceBetween>
</ContentLayout>
);
}
export default App;

15
src/main.tsx Normal file
View File

@ -0,0 +1,15 @@
import { StrictMode } from "react";
import { createRoot } from "react-dom/client";
import App from "./App.tsx";
import { BrowserRouter } from "react-router";
import { IntlProvider } from "react-intl";
createRoot(document.getElementById("root")!).render(
<StrictMode>
<BrowserRouter>
<IntlProvider locale="es-ES" messages={{}}>
<App />
</IntlProvider>
</BrowserRouter>
</StrictMode>,
);

BIN
src/paella-icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 672 KiB

1
src/pages/Buy.tsx Normal file
View File

@ -0,0 +1 @@
export const Buy = () => {};

33
src/pages/Login.tsx Normal file
View File

@ -0,0 +1,33 @@
import {
Button,
Input,
Checkbox,
Header,
SpaceBetween,
} from "@cloudscape-design/components";
import { useState } from "react";
export const Login = () => {
const [userName, setUserName] = useState("");
const [password, setPassword] = useState("");
const [rememberMe, setRememberMe] = useState(false);
return (
<SpaceBetween size="s" alignItems={"center"}>
<Header>Login</Header>
<Input value={userName} onChange={(e) => setUserName(e.detail.value)} />
<Input
type="password"
value={password}
onChange={(e) => setPassword(e.detail.value)}
/>
<Checkbox
checked={rememberMe}
onChange={() => setRememberMe(!rememberMe)}
>
Remember me
</Checkbox>
<Button>Login</Button>
</SpaceBetween>
);
};

83
src/pages/Register.tsx Normal file
View File

@ -0,0 +1,83 @@
import { useState } from "react";
import {
Button,
Header,
Input,
SpaceBetween,
} from "@cloudscape-design/components";
const PASSWORD_MAX_LENGTH = 64;
const PASSWORD_MIN_LENGTH = 8;
const specialChars = ["|", "#", "%", "&", "/", ",", "(", ")", "="];
const numChars = ["1", "2", "3", "4", "5", "6", "7", "8", "9", "0"];
enum PasswordErrors {
PasswordTooShort,
PasswordTooLong,
MissingNumbers,
MissingSpecialChars,
PasswordDoNotMatch,
}
const passwordMessages: Record<PasswordErrors, string> = {
[PasswordErrors.PasswordTooShort]: `La contraseña debe tener al menos ${PASSWORD_MIN_LENGTH} letras`,
[PasswordErrors.PasswordTooLong]: `La contraseña no puede tener más de ${PASSWORD_MAX_LENGTH} letras`,
[PasswordErrors.MissingNumbers]: `La contraseña debe tener al menos un número`,
[PasswordErrors.MissingSpecialChars]: `La contraseña debe tener al menos un simbolito`,
[PasswordErrors.PasswordDoNotMatch]: "Las contraseñas deben coincidir",
};
export const Register = () => {
const [userName, setUserName] = useState("");
const [password, setPassword] = useState("");
const [passwordConfirm, setPasswordConfirm] = useState("");
const passwordErrors: PasswordErrors[] = [];
const stringContainsSpecialChars = (stringToCheck: string) => {
return specialChars.some((char) => stringToCheck.includes(char));
};
const stringContainsNumber = (stringToCheck: string) => {
return numChars.some((char) => stringToCheck.includes(char));
};
if (password.length < PASSWORD_MIN_LENGTH) {
passwordErrors.push(PasswordErrors.PasswordTooShort);
}
if (password.length > PASSWORD_MAX_LENGTH) {
passwordErrors.push(PasswordErrors.PasswordTooLong);
}
if (!stringContainsSpecialChars(password)) {
passwordErrors.push(PasswordErrors.MissingSpecialChars);
}
if (!stringContainsNumber(password)) {
passwordErrors.push(PasswordErrors.MissingNumbers);
}
if (password !== passwordConfirm) {
passwordErrors.push(PasswordErrors.PasswordDoNotMatch);
}
return (
<SpaceBetween size="s" alignItems={"center"}>
<Header>Register</Header>
<Input value={userName} onChange={(e) => setUserName(e.detail.value)} />
<Input
type="password"
value={password}
onChange={(e) => setPassword(e.detail.value)}
/>
<Input
type="password"
value={passwordConfirm}
onChange={(e) => setPasswordConfirm(e.detail.value)}
/>
{passwordErrors.map((error) => (
<span>{passwordMessages[error]}</span>
))}
<Button>Register</Button>
</SpaceBetween>
);
};

48
src/pages/SignUp.tsx Normal file
View File

@ -0,0 +1,48 @@
import {
Calendar,
Button,
SpaceBetween,
Container,
} from "@cloudscape-design/components";
import { useState } from "react";
import { FormattedDate } from "react-intl";
export const SignUp = () => {
const [selectedDate, setSelectedDate] = useState<string | null>(null);
const tuesday = 2;
const thursday = 4;
const isDateEnabled = (date: Date) =>
[tuesday, thursday].includes(date.getDay());
const availableSlots: number = 3;
return (
<SpaceBetween size="s" alignItems={"center"}>
<Calendar
value={selectedDate || ""}
onChange={(e) => setSelectedDate(e.detail.value)}
isDateEnabled={isDateEnabled}
/>
{selectedDate && (
<Container
header={
<FormattedDate
value={selectedDate}
day="numeric"
month="long"
year="numeric"
/>
}
>
{availableSlots === 0 ? (
<p>There are no available slots for this date.</p>
) : (
<>
<p>There are {availableSlots} available slots for this date.</p>
<Button>Sign up</Button>
</>
)}
</Container>
)}
</SpaceBetween>
);
};

27
tsconfig.app.json Normal file
View File

@ -0,0 +1,27 @@
{
"compilerOptions": {
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
"target": "ES2022",
"useDefineForClassFields": true,
"lib": ["ES2022", "DOM", "DOM.Iterable"],
"module": "ESNext",
"types": ["vite/client"],
"skipLibCheck": true,
/* Bundler mode */
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"verbatimModuleSyntax": true,
"moduleDetection": "force",
"noEmit": true,
"jsx": "react-jsx",
/* Linting */
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true,
"noUncheckedSideEffectImports": true
},
"include": ["src"]
}

7
tsconfig.json Normal file
View File

@ -0,0 +1,7 @@
{
"files": [],
"references": [
{ "path": "./tsconfig.app.json" },
{ "path": "./tsconfig.node.json" }
]
}

26
tsconfig.node.json Normal file
View File

@ -0,0 +1,26 @@
{
"compilerOptions": {
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
"target": "ES2023",
"lib": ["ES2023"],
"module": "ESNext",
"types": ["node"],
"skipLibCheck": true,
/* Bundler mode */
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"verbatimModuleSyntax": true,
"moduleDetection": "force",
"noEmit": true,
/* Linting */
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"erasableSyntaxOnly": false,
"noFallthroughCasesInSwitch": true,
"noUncheckedSideEffectImports": true
},
"include": ["vite.config.ts"]
}

7
vite.config.ts Normal file
View File

@ -0,0 +1,7 @@
import { defineConfig } from "vite";
import react from "@vitejs/plugin-react";
// https://vite.dev/config/
export default defineConfig({
plugins: [react()],
});