remove bottstrap

pull/118/head
maxime 1 year ago
parent fa7339b0f1
commit 42c0300ced

@ -5,11 +5,12 @@
<link rel="icon" type="image/svg+xml" href="/src/assets/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>IQBall</title>
</head>
<body>
<div id="root"></div>
<script src="https://cdn.jsdelivr.net/npm/react/umd/react.production.min.js" crossorigin></script>
<script
src="https://cdn.jsdelivr.net/npm/react/umd/react.production.min.js"
crossorigin></script>
<script
src="https://cdn.jsdelivr.net/npm/react-dom/umd/react-dom.production.min.js"
@ -19,7 +20,9 @@
src="https://cdn.jsdelivr.net/npm/react-bootstrap@next/dist/react-bootstrap.min.js"
crossorigin></script>
<script>var Alert = ReactBootstrap.Alert;</script>
<script>
var Alert = ReactBootstrap.Alert
</script>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>

@ -9,10 +9,8 @@
"@testing-library/user-event": "^13.5.0",
"@types/react": "^18.2.31",
"@types/react-dom": "^18.2.14",
"bootstrap": "^5.3.3",
"eslint-plugin-react-refresh": "^0.4.5",
"react": "^18.2.0",
"react-bootstrap": "^2.10.2",
"react-dom": "^18.2.0",
"react-draggable": "^4.4.6",
"react-router-dom": "^6.22.0",

@ -1,8 +1,25 @@
import { BrowserRouter, Navigate, Outlet, Route, Routes, useLocation } from "react-router-dom"
import {
BrowserRouter,
Navigate,
Outlet,
Route,
Routes,
useLocation,
} from "react-router-dom"
import { Header } from "./pages/template/Header.tsx"
import "./style/app.css"
import { createContext, lazy, ReactNode, Suspense, useCallback, useContext, useEffect, useMemo, useState } from "react"
import {
createContext,
lazy,
ReactNode,
Suspense,
useCallback,
useContext,
useEffect,
useMemo,
useState,
} from "react"
import { BASE } from "./Constants.ts"
import { Authentication, Fetcher } from "./app/Fetcher.ts"
import { User } from "./model/User.ts"
@ -32,18 +49,19 @@ export default function App() {
const fetcher = useMemo(() => new Fetcher(storedAuth), [storedAuth])
const [user, setUser] = useState<User | null>(null)
const handleAuthSuccess = useCallback(async (auth: Authentication) => {
fetcher.updateAuthentication(auth)
const user = await fetchUser(fetcher)
setUser(user)
storeAuthentication(auth)
}, [fetcher])
const handleAuthSuccess = useCallback(
async (auth: Authentication) => {
fetcher.updateAuthentication(auth)
const user = await fetchUser(fetcher)
setUser(user)
storeAuthentication(auth)
},
[fetcher],
)
useEffect(() => {
const interval = setInterval(() => {
fetcher.fetchAPIGet("auth/keep-alive")
console.log("KEPT ALIVE !")
}, TOKEN_REFRESH_INTERVAL_MS)
return () => clearInterval(interval)
@ -52,93 +70,86 @@ export default function App() {
return (
<div id="app">
<FetcherContext.Provider value={fetcher}>
<SignedInUserContext.Provider value={{
user,
setUser,
}}>
<SignedInUserContext.Provider
value={{
user,
setUser,
}}>
<BrowserRouter basename={BASE}>
<Outlet />
<Routes>
<Route
path={"/login"}
element={suspense(<LoginPage onSuccess={handleAuthSuccess} />)}
element={suspense(
<LoginPage onSuccess={handleAuthSuccess} />,
)}
/>
<Route
path={"/register"}
element={suspense(<RegisterPage onSuccess={handleAuthSuccess} />)}
element={suspense(
<RegisterPage
onSuccess={handleAuthSuccess}
/>,
)}
/>
<Route path={"/"} element={suspense(<AppLayout />)}>
<Route path={"/"} element={
suspense(
<Route
path={"/"}
element={suspense(
<LoggedInPage>
<HomePage />
</LoggedInPage>,
)
}
)}
/>
<Route
path={"/home"}
element={
suspense(
<LoggedInPage>
<HomePage />
</LoggedInPage>,
)
}
element={suspense(
<LoggedInPage>
<HomePage />
</LoggedInPage>,
)}
/>
<Route
path={"/settings"}
element={
suspense(
<LoggedInPage>
<Settings />
</LoggedInPage>,
)
}
element={suspense(
<LoggedInPage>
<Settings />
</LoggedInPage>,
)}
/>
<Route
path={"/team/new"}
element={
suspense(<CreateTeamPage />)
}
element={suspense(<CreateTeamPage />)}
/>
<Route
path={"/team/:teamId"}
element={
suspense(
<LoggedInPage>
<TeamPanelPage />
</LoggedInPage>,
)
}
element={suspense(
<LoggedInPage>
<TeamPanelPage />
</LoggedInPage>,
)}
/>
<Route
path={"/tactic/new"}
element={
suspense(
<LoggedInPage>
<NewTacticPage />
</LoggedInPage>,
)
}
element={suspense(<NewTacticPage />)}
/>
<Route
path={"/tactic/:tacticId/edit"}
element={
suspense(
<LoggedInPage>
<Editor guestMode={false} />
</LoggedInPage>,
)
}
element={suspense(
<LoggedInPage>
<Editor guestMode={false} />
</LoggedInPage>,
)}
/>
<Route
path={"/tactic/edit-guest"}
element={suspense(<Editor guestMode={true} />)}
element={suspense(
<Editor guestMode={true} />,
)}
/>
<Route
@ -158,7 +169,9 @@ async function fetchUser(fetcher: Fetcher): Promise<User> {
const response = await fetcher.fetchAPIGet("user")
if (!response.ok) {
throw Error("Could not retrieve user information : " + await response.text())
throw Error(
"Could not retrieve user information : " + (await response.text()),
)
}
return await response.json()
@ -182,13 +195,15 @@ interface LoggedInPageProps {
enum UserFetchingState {
FETCHING,
FETCHED,
ERROR
ERROR,
}
function LoggedInPage({ children }: LoggedInPageProps) {
const [user, setUser] = useUser()
const fetcher = useAppFetcher()
const [userFetchingState, setUserFetchingState] = useState(user === null ? UserFetchingState.FETCHING : UserFetchingState.FETCHED)
const [userFetchingState, setUserFetchingState] = useState(
user === null ? UserFetchingState.FETCHING : UserFetchingState.FETCHED,
)
const location = useLocation()
useEffect(() => {
@ -200,16 +215,20 @@ function LoggedInPage({ children }: LoggedInPageProps) {
} catch (e) {
setUserFetchingState(UserFetchingState.ERROR)
}
}
if (userFetchingState === UserFetchingState.FETCHING)
initUser()
if (userFetchingState === UserFetchingState.FETCHING) initUser()
}, [fetcher, setUser, userFetchingState])
switch (userFetchingState) {
case UserFetchingState.ERROR:
return <Navigate to={"/login"} replace state={{ from: location.pathname }} />
return (
<Navigate
to={"/login"}
replace
state={{ from: location.pathname }}
/>
)
case UserFetchingState.FETCHED:
return children
case UserFetchingState.FETCHING:
@ -242,5 +261,3 @@ export function useUser(): [User | null, (user: User) => void] {
const { user, setUser } = useContext(SignedInUserContext)!
return [user, setUser]
}

@ -1,13 +1,11 @@
import { API } from "../Constants.ts"
export interface Authentication {
token: string
expirationDate: Date
}
export class Fetcher {
private auth?: Authentication
public constructor(auth?: Authentication) {
@ -63,10 +61,7 @@ export class Fetcher {
this.auth = auth
}
async handleResponse(
response: Response,
): Promise<Response> {
async handleResponse(response: Response): Promise<Response> {
// if we provided a token but still unauthorized, the token has expired
if (!response.ok) {
return response
@ -83,5 +78,3 @@ export class Fetcher {
return response
}
}

@ -1,4 +1,13 @@
import { CSSProperties, RefObject, SetStateAction, useCallback, useEffect, useMemo, useRef, useState } from "react"
import {
CSSProperties,
RefObject,
SetStateAction,
useCallback,
useEffect,
useMemo,
useRef,
useState,
} from "react"
import "../style/editor.css"
import TitleInput from "../components/TitleInput"
import PlainCourt from "../assets/court/full_court.svg?react"
@ -9,9 +18,18 @@ import { BallPiece } from "../components/editor/BallPiece"
import { Rack } from "../components/Rack"
import { PlayerPiece } from "../components/editor/PlayerPiece"
import { ComponentId, CourtType, StepContent, StepInfoNode, TacticComponent } from "../model/tactic/Tactic"
import SavingState, { SaveState, SaveStates } from "../components/editor/SavingState"
import {
ComponentId,
CourtType,
StepContent,
StepInfoNode,
TacticComponent,
} from "../model/tactic/Tactic"
import SavingState, {
SaveState,
SaveStates,
} from "../components/editor/SavingState"
import { BALL_TYPE } from "../model/tactic/CourtObjects"
import { CourtAction } from "../components/editor/CourtAction"
@ -34,10 +52,19 @@ import {
updateComponent,
} from "../editor/TacticContentDomains"
import { BallState, Player, PlayerInfo, PlayerLike, PlayerTeam } from "../model/tactic/Player"
import {
BallState,
Player,
PlayerInfo,
PlayerLike,
PlayerTeam,
} from "../model/tactic/Player"
import { RackedCourtObject, RackedPlayer } from "../editor/RackedItems"
import { CourtPlayer, EditableCourtPlayer } from "../components/editor/CourtPlayer.tsx"
import {
CourtPlayer,
EditableCourtPlayer,
} from "../components/editor/CourtPlayer.tsx"
import {
createAction,
getActionKind,
@ -49,10 +76,19 @@ import ArrowAction from "../components/actions/ArrowAction"
import { middlePos, Pos, ratioWithinBase } from "../geo/Pos"
import { Action, ActionKind } from "../model/tactic/Action"
import BallAction from "../components/actions/BallAction"
import { computePhantomPositioning, getOrigin, removePlayer } from "../editor/PlayerDomains"
import {
computePhantomPositioning,
getOrigin,
removePlayer,
} from "../editor/PlayerDomains"
import { CourtBall } from "../components/editor/CourtBall"
import StepsTree from "../components/editor/StepsTree"
import { addStepNode, getParent, getStepNode, removeStepNode } from "../editor/StepsDomain"
import {
addStepNode,
getParent,
getStepNode,
removeStepNode,
} from "../editor/StepsDomain"
import SplitLayout from "../components/SplitLayout.tsx"
import { ServiceError, TacticService } from "../service/TacticService.ts"
import { LocalStorageTacticService } from "../service/LocalStorageTacticService.ts"
@ -102,7 +138,11 @@ function EditorPortal({ guestMode }: EditorPageProps) {
return <EditorPageWrapper service={LocalStorageTacticService.init()} />
}
return <EditorPageWrapper service={new APITacticService(fetcher, parseInt(idStr))} />
return (
<EditorPageWrapper
service={new APITacticService(fetcher, parseInt(idStr))}
/>
)
}
function EditorPageWrapper({ service }: { service: TacticService }) {
@ -210,7 +250,7 @@ function EditorPageWrapper({ service }: { service: TacticService }) {
if (typeof contextResult === "string") {
setPanicMessage(
"There has been an error retrieving the editor initial context : " +
contextResult,
contextResult,
)
return
}
@ -225,7 +265,7 @@ function EditorPageWrapper({ service }: { service: TacticService }) {
if (typeof contentResult === "string") {
setPanicMessage(
"There has been an error retrieving the tactic's root step content : " +
contentResult,
contentResult,
)
return
}
@ -488,15 +528,15 @@ function EditorPage({
/>
),
!isFrozen &&
(info.ballState === BallState.HOLDS_ORIGIN ||
info.ballState === BallState.PASSED_ORIGIN) && (
<BallAction
key={2}
onDrop={(ballBounds) => {
doMoveBall(ballBounds, player)
}}
/>
),
(info.ballState === BallState.HOLDS_ORIGIN ||
info.ballState === BallState.PASSED_ORIGIN) && (
<BallAction
key={2}
onDrop={(ballBounds) => {
doMoveBall(ballBounds, player)
}}
/>
),
]
},
[
@ -810,12 +850,12 @@ interface EditorStepsTreeProps {
}
function EditorStepsTree({
selectedStepId,
root,
onAddChildren,
onRemoveNode,
onStepSelected,
}: EditorStepsTreeProps) {
selectedStepId,
root,
onAddChildren,
onRemoveNode,
onStepSelected,
}: EditorStepsTreeProps) {
return (
<div id="steps-div">
<StepsTree
@ -840,12 +880,12 @@ interface PlayerRackProps {
}
function PlayerRack({
id,
objects,
setObjects,
courtRef,
setComponents,
}: PlayerRackProps) {
id,
objects,
setObjects,
courtRef,
setComponents,
}: PlayerRackProps) {
const courtBounds = useCallback(
() => courtRef.current!.getBoundingClientRect(),
[courtRef],
@ -899,15 +939,15 @@ interface CourtPlayerArrowActionProps {
}
function CourtPlayerArrowAction({
playerInfo,
player,
isInvalid,
playerInfo,
player,
isInvalid,
content,
setContent,
setPreviewAction,
courtRef,
}: CourtPlayerArrowActionProps) {
content,
setContent,
setPreviewAction,
courtRef,
}: CourtPlayerArrowActionProps) {
const courtBounds = useCallback(
() => courtRef.current!.getBoundingClientRect(),
[courtRef],

@ -9,7 +9,7 @@ export interface LoginAppProps {
onSuccess: (auth: Authentication) => void
}
export default function LoginApp({onSuccess}: LoginAppProps) {
export default function LoginApp({ onSuccess }: LoginAppProps) {
const [errors, setErrors] = useState<Failure[]>([])
const fetcher = useAppFetcher()

@ -1,129 +1,144 @@
import "bootstrap/dist/css/bootstrap.min.css"
import Button from "react-bootstrap/Button"
import Form from "react-bootstrap/Form"
import Image from "react-bootstrap/Image"
import Container from "react-bootstrap/Container"
import Row from "react-bootstrap/Row"
import Col from "react-bootstrap/Col"
import Modal from "react-bootstrap/Modal"
import { Stack } from "react-bootstrap"
import { useRef, useState } from "react"
import { FormEvent, useCallback, useRef, useState } from "react"
import "../style/settings.css"
import { useAppFetcher, useUser } from "../App.tsx"
import { Fetcher } from "../app/Fetcher.ts"
export default function Settings() {
return (
<div id="settings-page">
<div id="content">
<AccountSettings />
</div>
</div>
)
}
function AccountSettings() {
return (
<div id="account">
<ProfilSettings/>
</div>
)
}
function ProfilSettings() {
const nameRef = useRef<HTMLInputElement>(null)
const emailRef = useRef<HTMLInputElement>(null)
const passwordRef = useRef<HTMLInputElement>(null)
const confirmPasswordRef = useRef<HTMLInputElement>(null)
export default function ProfileSettings() {
const fetcher = useAppFetcher()
const [user, setUser] = useUser()
const [errorMessages, setErrorMessages] = useState<string[]>([])
const [success, setSuccess] = useState(false)
const formRef = useRef<HTMLFormElement | null>(null)
const submitForm = useCallback(
async (e: FormEvent) => {
e.preventDefault()
const { name, email, password, confirmPassword } =
Object.fromEntries<string>(
new FormData(formRef.current!) as Iterable<
[PropertyKey, string]
>,
)
if (password !== confirmPassword) {
setErrorMessages(["Les mots de passe ne correspondent pas !"])
return
}
const req: AccountUpdateRequest = {
name: name,
email: email,
}
if (password && password.length !== 0) {
req.password = password
}
const errors = await updateAccount(fetcher, req)
if (errors.length !== 0) {
setErrorMessages(errors)
setSuccess(false)
return
}
setUser({ ...user!, email, name })
setSuccess(true)
formRef.current!.reset()
setErrorMessages([])
},
[fetcher, setUser, user],
)
const [modalShow, setModalShow] = useState(false)
const width = 150
const profilePicture = user!.profilePicture
return (
<Container>
{errorMessages.map(msg => (<div key={msg} className={"error-message"}>{msg}</div>))}
<Row>
<Col>
<Stack>
<div id="profile-picture">
<Image src={profilePicture} width={width} height={width} roundedCircle />
<div id="settings-page">
<div id="settings-content">
{errorMessages.map((msg) => (
<div key={msg} className={"error-message"}>
{msg}
</div>
))}
{success && (
<p className={"success-message"}>
Modifications sauvegardées
</p>
)}
<div id="settings-inputs">
<div id="settings-profile-picture">
<div>
<div id="profile-picture">
<img
id="profile-picture-img"
src={profilePicture}
width={width}
height={width}
alt="profile-picture"
/>
</div>
<button
className="settings-button"
onClick={() => setModalShow(true)}>
Changer photo de profil
</button>
<ProfileImageInputPopup
show={modalShow}
onHide={() => setModalShow(false)}
/>
</div>
<Button variant="outline-primary" onClick={() => setModalShow(true)}>
Changer photo de profil
</Button>
<ProfileImageInputPopup
show={modalShow}
onHide={() => setModalShow(false)}
/>
</Stack>
</Col>
<Col>
<Form>
<Form.Group className="mb-3" controlId="formUsername">
<Form.Label className="content">Nom d'utilisateur</Form.Label>
<Form.Control ref={nameRef} size="sm" defaultValue={user!.name} />
</Form.Group>
<Form.Group className="mb-3" controlId="formEmail">
<Form.Label className="content">Adresse mail</Form.Label>
<Form.Control ref={emailRef} size="sm" defaultValue={user!.email} type="email"
placeholder="Addresse email" />
</Form.Group>
<Form.Group className="mb-3" controlId="formPassword">
<Form.Label className="content">Mot de passe</Form.Label>
<Form.Control ref={passwordRef}
size="sm"
type="password"
placeholder="Mot de passe" />
</Form.Group>
<Form.Group className="mb-3" controlId="formConfirmPassword">
<Form.Label className="content">Confirmez le mot de passe</Form.Label>
<Form.Control ref={confirmPasswordRef}
size="sm"
type="password"
placeholder="Confirmation du mot de passe" />
</Form.Group>
<Button variant="outline-primary" type="button"
onClick={async () => {
const name = nameRef.current!.value
const email = emailRef.current!.value
const password = passwordRef.current?.value
const confirmPassword = confirmPasswordRef.current?.value
if (password !== confirmPassword) {
setErrorMessages(["Les mots de passe ne correspondent pas !"])
return
}
const req: AccountUpdateRequest = {
name,
email,
}
if (password && password.length !== 0) {
req.password = password
}
const errors = await updateAccount(fetcher, req)
if (errors.length !== 0) {
setErrorMessages(errors)
return
}
setUser({...user!, email, name})
}}>Mettre
à jour</Button>
</Form>
</Col>
</Row>
</Container>
</div>
<div>
<form
ref={formRef}
id="credentials-form"
onSubmit={submitForm}>
<p>Nom d'utilisateur</p>
<input
className="settings-input"
name="name"
type="text"
placeholder={"Nom d'utilisateur"}
defaultValue={user!.name}
/>
<p>Addresse email</p>
<input
className="settings-input"
name="email"
type="email"
placeholder={"Addresse email"}
defaultValue={user!.email}
/>
<p>Mot de passe</p>
<input
className="settings-input"
name="password"
type="password"
placeholder={"Mot de passe"}
/>
<p>Confirmez le mot de passe</p>
<input
className="settings-input"
name="confirmPassword"
type="password"
placeholder={"Confirmation du mot de passe"}
/>
<button
className="settings-button"
type="submit"
onClick={submitForm}>
Mettre à jour
</button>
</form>
</div>
</div>
</div>
</div>
)
}
@ -134,13 +149,16 @@ interface AccountUpdateRequest {
password?: string
}
async function updateAccount(fetcher: Fetcher, req: AccountUpdateRequest): Promise<string[]> {
async function updateAccount(
fetcher: Fetcher,
req: AccountUpdateRequest,
): Promise<string[]> {
const response = await fetcher.fetchAPI("user", req, "PUT")
if (response.ok)
return []
if (response.ok) return []
const body = await response.json()
return Object.entries(body)
.flatMap(([kind, messages]) => (messages as string[]).map(msg => `${kind}: ${msg}`))
return Object.entries(body).flatMap(([kind, messages]) =>
(messages as string[]).map((msg) => `${kind}: ${msg}`),
)
}
interface ProfileImageInputPopupProps {
@ -155,57 +173,76 @@ function ProfileImageInputPopup({ show, onHide }: ProfileImageInputPopupProps) {
const fetcher = useAppFetcher()
const [user, setUser] = useUser()
if (!show) return <></>
return (
<Modal
show={show}
onHide={onHide}
size="lg"
aria-labelledby="title-modal"
centered
>
<Modal.Header>
<Modal.Title id="title-modal">
Nouvelle photo de profil
</Modal.Title>
</Modal.Header>
<Modal.Body>
{errorMessages?.map(msg => <div key={msg} className="error-message">{msg}</div>)}
<Form.Label>Saisissez le lien vers votre nouvelle photo de profil</Form.Label>
<Form.Control isInvalid={errorMessages?.length !== 0} ref={urlRef} type="input"
placeholder={"lien vers une image"} />
</Modal.Body>
<Modal.Footer>
<Button onClick={onHide}>Annuler</Button>
<Button onClick={async () => {
const url = urlRef.current!.value
const exists = await imageExists(url)
setErrorMessages(["Le lien ne renvoie vers aucune image !"])
if (!exists) {
return
<div id="profile-picture-popup">
<div id="profile-picture-popup-content">
<div id="profile-picture-popup-header">
<p id="profile-picture-popup-title">
Nouvelle photo de profil
</p>
</div>
{errorMessages?.map((msg) => (
<div key={msg} className="error-message">
{msg}
</div>
))}
<p id="profile-picture-popup-subtitle">
Saisissez le lien vers votre nouvelle photo de profil
</p>
<input
className={
`settings-input ` +
((errorMessages?.length ?? 0) === 0
? ""
: "invalid-input")
}
const errors = await updateAccount(fetcher, { profilePicture: url })
if (errors.length !== 0) {
setErrorMessages(errors)
return
}
setUser({...user!, profilePicture: url})
onHide()
}
}>Valider</Button>
</Modal.Footer>
</Modal>
ref={urlRef}
type="input"
placeholder={"lien vers une image"}
/>
<div id="profile-picture-popup-footer">
<button className={"settings-button"} onClick={onHide}>
Annuler
</button>
<button
className={"settings-button"}
onClick={async () => {
const url = urlRef.current!.value
const exists = await imageExists(url)
setErrorMessages([
"Le lien ne renvoie vers aucune image !",
])
if (!exists) {
return
}
const errors = await updateAccount(fetcher, {
profilePicture: url,
})
if (errors.length !== 0) {
setErrorMessages(errors)
return
}
setUser({ ...user!, profilePicture: url })
setErrorMessages([])
onHide()
}}>
Valider
</button>
</div>
</div>
</div>
)
}
async function imageExists(imageLink: string) {
try {
const response = await fetch(imageLink)
const response = await fetch(imageLink, { mode: "no-cors" })
if (response.ok) {
const contentType = response.headers.get("content-type")
const contentType = response.headers.get("Content-type")
return contentType?.startsWith("image/") ?? false
}
return false
@ -214,4 +251,3 @@ async function imageExists(imageLink: string) {
return false
}
}

@ -5,11 +5,17 @@ import { useNavigate } from "react-router-dom"
export function Header() {
const navigate = useNavigate()
const [user, ] = useUser()
const [user] = useUser()
const accountImage = user?.profilePicture
? <img id="img-account" src={user.profilePicture} alt={"profile-picture"} />
: <AccountSvg id="img-account" />
const accountImage = user?.profilePicture ? (
<img
id="img-account"
src={user.profilePicture}
alt={"profile-picture"}
/>
) : (
<AccountSvg id="img-account" />
)
return (
<div id="header">

@ -12,10 +12,14 @@ export class APITacticService implements TacticService {
}
async getContext(): Promise<TacticContext | ServiceError> {
const infoResponsePromise = this.fetcher.fetchAPIGet(`tactics/${this.tacticId}`)
const infoResponsePromise = this.fetcher.fetchAPIGet(
`tactics/${this.tacticId}`,
)
const infoResponse = await infoResponsePromise
const treeResponsePromise = this.fetcher.fetchAPIGet(`tactics/${this.tacticId}/tree`)
const treeResponsePromise = this.fetcher.fetchAPIGet(
`tactics/${this.tacticId}/tree`,
)
const treeResponse = await treeResponsePromise
if (infoResponse.status == 401 || treeResponse.status == 401) {
@ -31,10 +35,13 @@ export class APITacticService implements TacticService {
parent: StepInfoNode,
content: StepContent,
): Promise<StepInfoNode | ServiceError> {
const response = await this.fetcher.fetchAPI(`tactics/${this.tacticId}/steps`, {
parentId: parent.id,
content,
})
const response = await this.fetcher.fetchAPI(
`tactics/${this.tacticId}/steps`,
{
parentId: parent.id,
content,
},
)
if (response.status == 404) return ServiceError.NOT_FOUND
if (response.status == 401) return ServiceError.UNAUTHORIZED

@ -1,5 +1,9 @@
.error-message {
color: red
color: red;
}
.success-message {
color: green;
}
#settings-page {
@ -13,4 +17,104 @@
#profile-picture {
display: flex;
justify-content: center;
}
}
#settings-content {
padding: 30px;
border: 1px solid gray;
border-radius: 15px;
background-color: #e1e1e1;
}
#settings-inputs {
display: flex;
gap: 20px;
}
.settings-button {
background-color: white;
border: solid 1px #0d6efd;
border-radius: 5px;
color: #0d6efd;
padding: 5px;
transition: background-color 250ms;
}
.settings-button:hover {
background-color: #0d6efd;
color: white;
cursor: pointer;
}
.settings-input {
border: solid 1px lightgray;
border-radius: 5px;
padding: 5px;
outline: #5f8fee;
}
.settings-input:focus {
box-shadow: #0056b3;
}
#credentials-form {
display: flex;
flex-direction: column;
}
#profile-picture-popup {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
position: absolute;
top: 0;
left: 0;
z-index: 100000000000000000000000000000000;
width: 100%;
height: 100%;
background-color: rgba(84, 78, 78, 0.33);
}
#profile-picture-popup-content {
display: flex;
flex-direction: column;
row-gap: 20px;
background-color: white;
padding: 25px;
border-radius: 25px;
}
#profile-picture-popup-title {
font-size: 25px;
}
#profile-picture-popup-subtitle,
#profile-picture-popup-title {
margin: 0;
}
#profile-picture-popup-header {
border: 0 solid;
border-bottom-width: 1px;
}
#profile-picture-popup-footer {
display: flex;
width: 100%;
gap: 5px;
}
#profile-picture-img {
user-select: none;
-webkit-user-drag: none;
border-radius: 2000px;
}
.invalid-input {
border: 1px solid red;
}

Loading…
Cancel
Save