diff options
20 files changed, 247 insertions, 357 deletions
diff --git a/Timeline/ClientApp/package.json b/Timeline/ClientApp/package.json index 81232c71..5ae6a608 100644 --- a/Timeline/ClientApp/package.json +++ b/Timeline/ClientApp/package.json @@ -27,7 +27,6 @@ "react-router": "^5.2.0", "react-router-bootstrap": "^0.25.0", "react-router-dom": "^5.2.0", - "reactstrap": "^8.5.1", "regenerator-runtime": "^0.13.7", "rxjs": "^6.6.2", "workbox-precaching": "^5.1.3", @@ -75,7 +74,6 @@ "@types/react-router": "^5.1.8", "@types/react-router-bootstrap": "^0.24.5", "@types/react-router-dom": "^5.1.5", - "@types/reactstrap": "^8.5.1", "@types/webpack-env": "^1.15.2", "@types/xregexp": "^4.3.0", "@typescript-eslint/eslint-plugin": "^3.10.1", diff --git a/Timeline/ClientApp/src/app/service-worker.tsx b/Timeline/ClientApp/src/app/service-worker.tsx index e629995a..3be54bc1 100644 --- a/Timeline/ClientApp/src/app/service-worker.tsx +++ b/Timeline/ClientApp/src/app/service-worker.tsx @@ -1,6 +1,6 @@ import React from "react"; -import { Button } from "reactstrap"; import { useTranslation } from "react-i18next"; +import { Button } from "react-bootstrap"; import { pushAlert } from "./services/alert"; @@ -39,7 +39,11 @@ if ("serviceWorker" in navigator) { return ( <> {t("serviceWorker.externalActivatedPrompt")} - <Button color="success" size="sm" onClick={upgradeReload} outline> + <Button + variant="outline-success" + size="sm" + onClick={upgradeReload} + > {t("serviceWorker.reloadNow")} </Button> </> @@ -83,7 +87,7 @@ if ("serviceWorker" in navigator) { return ( <> {t("serviceWorker.upgradePrompt")} - <Button color="success" size="sm" onClick={upgrade} outline> + <Button variant="outline-success" size="sm" onClick={upgrade}> {t("serviceWorker.upgradeNow")} </Button> </> diff --git a/Timeline/ClientApp/src/app/views/about/index.tsx b/Timeline/ClientApp/src/app/views/about/index.tsx index 21c487da..78cffb5f 100644 --- a/Timeline/ClientApp/src/app/views/about/index.tsx +++ b/Timeline/ClientApp/src/app/views/about/index.tsx @@ -23,8 +23,8 @@ const frontendCredits: { url: "https://getbootstrap.com", }, { - name: "reactstrap", - url: "https://reactstrap.github.io", + name: "react-bootstrap", + url: "https://react-bootstrap.github.io", }, { name: "babeljs", diff --git a/Timeline/ClientApp/src/app/views/admin/Admin.tsx b/Timeline/ClientApp/src/app/views/admin/Admin.tsx index 51dc5a3c..e0f59b0f 100644 --- a/Timeline/ClientApp/src/app/views/admin/Admin.tsx +++ b/Timeline/ClientApp/src/app/views/admin/Admin.tsx @@ -1,5 +1,4 @@ import React, { Fragment } from "react"; -import { Nav, NavItem, NavLink } from "reactstrap"; import { Redirect, Route, @@ -7,7 +6,7 @@ import { useRouteMatch, useHistory, } from "react-router"; -import classnames from "classnames"; +import { Nav } from "react-bootstrap"; import AppBar from "../common/AppBar"; import { UserWithToken } from "@/services/user"; @@ -37,27 +36,27 @@ const Admin: React.FC<AdminProps> = (props) => { <Route path={`${match.path}/${name}`}> <AppBar /> <div style={{ height: 56 }} className="flex-fix-length" /> - <Nav tabs> - <NavItem> - <NavLink - className={classnames({ active: tabName === "users" })} + <Nav variant="tabs"> + <Nav.Item> + <Nav.Link + active={tabName === "users"} onClick={() => { toggle("users"); }} > Users - </NavLink> - </NavItem> - <NavItem> - <NavLink - className={classnames({ active: tabName === "more" })} + </Nav.Link> + </Nav.Item> + <Nav.Item> + <Nav.Link + active={tabName === "more"} onClick={() => { toggle("more"); }} > More - </NavLink> - </NavItem> + </Nav.Link> + </Nav.Item> </Nav> {body} </Route> diff --git a/Timeline/ClientApp/src/app/views/admin/UserAdmin.tsx b/Timeline/ClientApp/src/app/views/admin/UserAdmin.tsx index bde6b3af..18b77ca8 100644 --- a/Timeline/ClientApp/src/app/views/admin/UserAdmin.tsx +++ b/Timeline/ClientApp/src/app/views/admin/UserAdmin.tsx @@ -1,16 +1,13 @@ import React, { useState, useEffect } from "react"; +import axios from "axios"; import { - ListGroupItem, + ListGroup, Row, Col, - UncontrolledDropdown, - DropdownToggle, - DropdownMenu, - DropdownItem, + Dropdown, Spinner, Button, -} from "reactstrap"; -import axios from "axios"; +} from "react-bootstrap"; import OperationDialog from "../common/OperationDialog"; import { User, UserWithToken } from "@/services/user"; @@ -101,7 +98,7 @@ const UserItem: React.FC<UserCardProps> = (props) => { }; return ( - <ListGroupItem className="container"> + <ListGroup.Item className="container"> <Row className="align-items-center"> <Col> <p className="mb-0 text-primary">{user.username}</p> @@ -112,31 +109,31 @@ const UserItem: React.FC<UserCardProps> = (props) => { </small> </Col> <Col className="col-auto"> - <UncontrolledDropdown> - <DropdownToggle color="warning" className="text-light" caret> + <Dropdown> + <Dropdown.Toggle variant="warning" className="text-light"> Manage - </DropdownToggle> - <DropdownMenu> - <DropdownItem onClick={createClickCallback(kChangeUsername)}> + </Dropdown.Toggle> + <Dropdown.Menu> + <Dropdown.Item onClick={createClickCallback(kChangeUsername)}> Change Username - </DropdownItem> - <DropdownItem onClick={createClickCallback(kChangePassword)}> + </Dropdown.Item> + <Dropdown.Item onClick={createClickCallback(kChangePassword)}> Change Password - </DropdownItem> - <DropdownItem onClick={createClickCallback(kChangePermission)}> + </Dropdown.Item> + <Dropdown.Item onClick={createClickCallback(kChangePermission)}> Change Permission - </DropdownItem> - <DropdownItem + </Dropdown.Item> + <Dropdown.Item className="text-danger" onClick={createClickCallback(kDelete)} > Delete - </DropdownItem> - </DropdownMenu> - </UncontrolledDropdown> + </Dropdown.Item> + </Dropdown.Menu> + </Dropdown> </Col> </Row> - </ListGroupItem> + </ListGroup.Item> ); }; @@ -441,7 +438,7 @@ const UserAdmin: React.FC<UserAdminProps> = (props) => { return ( <> <Button - color="success" + variant="success" onClick={() => setDialog({ type: "create", @@ -456,7 +453,7 @@ const UserAdmin: React.FC<UserAdminProps> = (props) => { </> ); } else { - return <Spinner />; + return <Spinner animation="border" />; } }; diff --git a/Timeline/ClientApp/src/app/views/common/AppBar.tsx b/Timeline/ClientApp/src/app/views/common/AppBar.tsx index 464747c0..ee4ead8f 100644 --- a/Timeline/ClientApp/src/app/views/common/AppBar.tsx +++ b/Timeline/ClientApp/src/app/views/common/AppBar.tsx @@ -17,7 +17,7 @@ const AppBar: React.FC = (_) => { const isAdministrator = user && user.administrator; return ( - <Navbar bg="primary" variant="dark" expand="md"> + <Navbar bg="primary" variant="dark" expand="md" sticky="top"> <LinkContainer to="/"> <Navbar.Brand className="d-flex align-items-center"> <TimelineLogo style={{ height: "1em" }} /> diff --git a/Timeline/ClientApp/src/app/views/common/FileInput.tsx b/Timeline/ClientApp/src/app/views/common/FileInput.tsx deleted file mode 100644 index 7b053d5c..00000000 --- a/Timeline/ClientApp/src/app/views/common/FileInput.tsx +++ /dev/null @@ -1,36 +0,0 @@ -import React from "react"; -import clsx from "clsx"; - -export interface FileInputProps - extends Omit<React.InputHTMLAttributes<HTMLInputElement>, "type" | "id"> { - inputId?: string; - labelText: string; - color?: string; - className?: string; -} - -const FileInput: React.FC<FileInputProps> = (props) => { - const { inputId, labelText, color, className, ...otherProps } = props; - - const realInputId = React.useMemo<string>(() => { - if (inputId != null) return inputId; - return ( - "file-input-" + - (((1 + Math.random()) * 0x10000) | 0).toString(16).substring(1) - ); - }, [inputId]); - - return ( - <> - <input className="d-none" type="file" id={realInputId} {...otherProps} /> - <label - htmlFor={realInputId} - className={clsx("btn", "btn-" + (color ?? "primary"), className)} - > - {labelText} - </label> - </> - ); -}; - -export default FileInput; diff --git a/Timeline/ClientApp/src/app/views/common/OperationDialog.tsx b/Timeline/ClientApp/src/app/views/common/OperationDialog.tsx index 402ffbec..6f97eb15 100644 --- a/Timeline/ClientApp/src/app/views/common/OperationDialog.tsx +++ b/Timeline/ClientApp/src/app/views/common/OperationDialog.tsx @@ -1,26 +1,13 @@ import React, { useState } from "react"; import { useTranslation } from "react-i18next"; -import { - Spinner, - Container, - ModalBody, - Label, - Input, - FormGroup, - FormFeedback, - ModalFooter, - Button, - Modal, - ModalHeader, - FormText, -} from "reactstrap"; +import { Spinner, Container, Form, Button, Modal } from "react-bootstrap"; import { UiLogicError } from "@/common"; const DefaultProcessPrompt: React.FC = (_) => { return ( <Container className="justify-content-center align-items-center"> - <Spinner /> + <Spinner animation="border" variant="success" /> </Container> ); }; @@ -233,7 +220,7 @@ const OperationDialog: React.FC<OperationDialogProps> = (props) => { body = ( <> - <ModalBody> + <Modal.Body> {inputPrompt} {inputScheme.map((item, index) => { const value = values[index]; @@ -242,9 +229,9 @@ const OperationDialog: React.FC<OperationDialogProps> = (props) => { if (item.type === "text") { return ( - <FormGroup key={index}> - {item.label && <Label>{t(item.label)}</Label>} - <Input + <Form.Group key={index}> + {item.label && <Form.Label>{t(item.label)}</Form.Label>} + <Form.Control type={item.password === true ? "password" : "text"} value={value as string} onChange={(e) => { @@ -258,35 +245,35 @@ const OperationDialog: React.FC<OperationDialogProps> = (props) => { ) ); }} - invalid={error != null} - {...item.textFieldProps} + isInvalid={error != null} /> - {error != null && <FormFeedback>{error}</FormFeedback>} - {item.helperText && <FormText>{t(item.helperText)}</FormText>} - </FormGroup> + {error != null && ( + <Form.Control.Feedback>{error}</Form.Control.Feedback> + )} + {item.helperText && ( + <Form.Text>{t(item.helperText)}</Form.Text> + )} + </Form.Group> ); } else if (item.type === "bool") { return ( - <FormGroup check key={index}> - <Input + <Form.Group key={index}> + <Form.Check<"input"> type="checkbox" - value={value as string} - onChange={(e) => { - updateValue( - index, - (e.target as HTMLInputElement).checked - ); + checked={value as boolean} + onChange={(event) => { + updateValue(index, event.currentTarget.checked); }} + label={t(item.label)} /> - <Label check>{t(item.label)}</Label> - </FormGroup> + </Form.Group> ); } else if (item.type === "select") { return ( - <FormGroup key={index}> - <Label>{t(item.label)}</Label> - <Input - type="select" + <Form.Group key={index}> + <Form.Label>{t(item.label)}</Form.Label> + <Form.Control + as="select" value={value as string} onChange={(event) => { updateValue(index, event.target.value); @@ -300,18 +287,18 @@ const OperationDialog: React.FC<OperationDialogProps> = (props) => { </option> ); })} - </Input> - </FormGroup> + </Form.Control> + </Form.Group> ); } })} - </ModalBody> - <ModalFooter> - <Button color="secondary" onClick={close}> + </Modal.Body> + <Modal.Footer> + <Button variant="secondary" onClick={close}> {t("operationDialog.cancel")} </Button> <Button - color="primary" + variant="primary" disabled={testErrorInfo(inputError)} onClick={() => { if (validateAll()) { @@ -321,14 +308,14 @@ const OperationDialog: React.FC<OperationDialogProps> = (props) => { > {t("operationDialog.confirm")} </Button> - </ModalFooter> + </Modal.Footer> </> ); } else if (step === "process") { body = ( - <ModalBody> + <Modal.Body> {props.processPrompt?.() ?? <DefaultProcessPrompt />} - </ModalBody> + </Modal.Body> ); } else { let content: React.ReactNode; @@ -345,12 +332,12 @@ const OperationDialog: React.FC<OperationDialogProps> = (props) => { } body = ( <> - <ModalBody>{content}</ModalBody> - <ModalFooter> - <Button color="primary" onClick={close}> + <Modal.Body>{content}</Modal.Body> + <Modal.Footer> + <Button variant="primary" onClick={close}> {t("operationDialog.ok")} </Button> - </ModalFooter> + </Modal.Footer> </> ); } @@ -359,7 +346,7 @@ const OperationDialog: React.FC<OperationDialogProps> = (props) => { return ( <Modal isOpen={props.open} toggle={close}> - <ModalHeader + <Modal.Header className={ props.titleColor != null ? "text-" + @@ -372,7 +359,7 @@ const OperationDialog: React.FC<OperationDialogProps> = (props) => { } > {title} - </ModalHeader> + </Modal.Header> {body} </Modal> ); diff --git a/Timeline/ClientApp/src/app/views/common/alert/AlertHost.tsx b/Timeline/ClientApp/src/app/views/common/alert/AlertHost.tsx index 31c0fb86..c74f18e2 100644 --- a/Timeline/ClientApp/src/app/views/common/alert/AlertHost.tsx +++ b/Timeline/ClientApp/src/app/views/common/alert/AlertHost.tsx @@ -1,8 +1,8 @@ import React, { useCallback } from "react"; -import { Alert } from "reactstrap"; import without from "lodash/without"; import concat from "lodash/concat"; import { useTranslation } from "react-i18next"; +import { Alert } from "react-bootstrap"; import { alertService, @@ -37,7 +37,12 @@ export const AutoCloseAlert: React.FC<AutoCloseAlertProps> = (props) => { }, [dismissTime, props.close]); return ( - <Alert className="m-3" color={alert.type ?? "primary"} toggle={props.close}> + <Alert + className="m-3" + variant={alert.type ?? "primary"} + onClose={props.close} + dismissible + > {(() => { const { message } = alert; if (typeof message === "function") { diff --git a/Timeline/ClientApp/src/app/views/login/index.tsx b/Timeline/ClientApp/src/app/views/login/index.tsx index e53d0002..5d1e8f06 100644 --- a/Timeline/ClientApp/src/app/views/login/index.tsx +++ b/Timeline/ClientApp/src/app/views/login/index.tsx @@ -1,15 +1,7 @@ import React, { Fragment, useState, useEffect } from "react"; import { useHistory } from "react-router"; import { useTranslation } from "react-i18next"; -import { - Label, - FormGroup, - Input, - Form, - FormFeedback, - Spinner, - Button, -} from "reactstrap"; +import { Form, Spinner, Button } from "react-bootstrap"; import { useUser, userService } from "@/services/user"; @@ -84,9 +76,9 @@ const LoginPage: React.FC = (_) => { <div className="container login-container mt-appbar"> <h1>{t("welcome")}</h1> <Form> - <FormGroup> - <Label for="username">{t("user.username")}</Label> - <Input + <Form.Group> + <Form.Label htmlFor="username">{t("user.username")}</Form.Label> + <Form.Control id="username" disabled={process} onChange={(e) => { @@ -94,15 +86,17 @@ const LoginPage: React.FC = (_) => { setUsernameDirty(true); }} value={username} - invalid={usernameDirty && username === ""} + isInvalid={usernameDirty && username === ""} /> {usernameDirty && username === "" && ( - <FormFeedback>{t("login.emptyUsername")}</FormFeedback> + <Form.Control.Feedback> + {t("login.emptyUsername")} + </Form.Control.Feedback> )} - </FormGroup> - <FormGroup> - <Label for="password">{t("user.password")}</Label> - <Input + </Form.Group> + <Form.Group> + <Form.Label htmlFor="password">{t("user.password")}</Form.Label> + <Form.Control id="password" type="password" disabled={process} @@ -111,30 +105,31 @@ const LoginPage: React.FC = (_) => { setPasswordDirty(true); }} value={password} - invalid={passwordDirty && password === ""} + isInvalid={passwordDirty && password === ""} /> {passwordDirty && password === "" && ( - <FormFeedback>{t("login.emptyPassword")}</FormFeedback> + <Form.Control.Feedback> + {t("login.emptyPassword")} + </Form.Control.Feedback> )} - </FormGroup> - <FormGroup check> - <Input + </Form.Group> + <Form.Group> + <Form.Check<"input"> id="remember-me" type="checkbox" checked={rememberMe} onChange={(e) => { - const v = (e.target as HTMLInputElement).checked; - setRememberMe(v); + setRememberMe(e.target.checked); }} + label={t("user.rememberMe")} /> - <Label for="remember-me">{t("user.rememberMe")}</Label> - </FormGroup> + </Form.Group> {error ? <p className="text-error">{t(error)}</p> : null} <div> {process ? ( - <Spinner /> + <Spinner animation="border" /> ) : ( - <Button color="primary" onClick={onSubmit}> + <Button variant="primary" onClick={onSubmit}> {t("user.login")} </Button> )} diff --git a/Timeline/ClientApp/src/app/views/timeline-common/TimelineItem.tsx b/Timeline/ClientApp/src/app/views/timeline-common/TimelineItem.tsx index f2441612..ce371015 100644 --- a/Timeline/ClientApp/src/app/views/timeline-common/TimelineItem.tsx +++ b/Timeline/ClientApp/src/app/views/timeline-common/TimelineItem.tsx @@ -1,19 +1,11 @@ import React from "react"; import clsx from "clsx"; -import { - Row, - Col, - Modal, - ModalHeader, - ModalBody, - ModalFooter, - Button, -} from "reactstrap"; import { Link } from "react-router-dom"; import { useTranslation } from "react-i18next"; import Svg from "react-inlinesvg"; import chevronDownIcon from "bootstrap-icons/icons/chevron-down.svg"; import trashIcon from "bootstrap-icons/icons/trash.svg"; +import { Row, Col, Modal, Button } from "react-bootstrap"; import { useAvatar } from "@/services/user"; import { TimelinePostInfo } from "@/services/timeline"; @@ -28,16 +20,18 @@ const TimelinePostDeleteConfirmDialog: React.FC<{ return ( <Modal toggle={toggle} isOpen centered> - <ModalHeader className="text-danger"> - {t("timeline.post.deleteDialog.title")} - </ModalHeader> - <ModalBody>{t("timeline.post.deleteDialog.prompt")}</ModalBody> - <ModalFooter> - <Button color="secondary" onClick={toggle}> + <Modal.Header> + <Modal.Title className="text-danger"> + {t("timeline.post.deleteDialog.title")} + </Modal.Title> + </Modal.Header> + <Modal.Body>{t("timeline.post.deleteDialog.prompt")}</Modal.Body> + <Modal.Footer> + <Button variant="secondary" onClick={toggle}> {t("operationDialog.cancel")} </Button> <Button - color="danger" + variant="danger" onClick={() => { onConfirm(); toggle(); @@ -45,7 +39,7 @@ const TimelinePostDeleteConfirmDialog: React.FC<{ > {t("operationDialog.confirm")} </Button> - </ModalFooter> + </Modal.Footer> </Modal> ); }; diff --git a/Timeline/ClientApp/src/app/views/timeline-common/TimelineMember.tsx b/Timeline/ClientApp/src/app/views/timeline-common/TimelineMember.tsx index 99605922..67a8543a 100644 --- a/Timeline/ClientApp/src/app/views/timeline-common/TimelineMember.tsx +++ b/Timeline/ClientApp/src/app/views/timeline-common/TimelineMember.tsx @@ -1,14 +1,6 @@ import React, { useState } from "react"; import { useTranslation } from "react-i18next"; -import { - Container, - ListGroup, - ListGroupItem, - Modal, - Row, - Col, - Button, -} from "reactstrap"; +import { Container, ListGroup, Modal, Row, Col, Button } from "react-bootstrap"; import { User, useAvatar } from "@/services/user"; @@ -25,9 +17,9 @@ const TimelineMemberItem: React.FC<{ const avatar = useAvatar(user.username); return ( - <ListGroupItem className="container"> + <ListGroup.Item className="container"> <Row> - <Col className="col-auto"> + <Col xs="auto"> <BlobImage blob={avatar} className="avatar small" /> </Col> <Col> @@ -46,7 +38,7 @@ const TimelineMemberItem: React.FC<{ return ( <Button className="align-self-center" - color="danger" + variant="danger" onClick={() => { onRemove(user.username); }} @@ -56,7 +48,7 @@ const TimelineMemberItem: React.FC<{ ); })()} </Row> - </ListGroupItem> + </ListGroup.Item> ); }; @@ -169,7 +161,7 @@ const TimelineMember: React.FC<TimelineMemberProps> = (props) => { </Row> </Col> <Button - color="primary" + variant="primary" className="align-self-center" disabled={!addable} onClick={() => { @@ -212,7 +204,7 @@ export const TimelineMemberDialog: React.FC<TimelineMemberDialogProps> = ( props ) => { return ( - <Modal isOpen={props.open} toggle={props.onClose}> + <Modal show centered onHide={props.onClose}> <TimelineMember {...props} /> </Modal> ); diff --git a/Timeline/ClientApp/src/app/views/timeline-common/TimelinePageTemplate.tsx b/Timeline/ClientApp/src/app/views/timeline-common/TimelinePageTemplate.tsx index 1b03d5c7..d5c91622 100644 --- a/Timeline/ClientApp/src/app/views/timeline-common/TimelinePageTemplate.tsx +++ b/Timeline/ClientApp/src/app/views/timeline-common/TimelinePageTemplate.tsx @@ -160,10 +160,6 @@ export default function TimelinePageTemplate<TManageItem>( [onManageProp] ); - const onMember = React.useCallback(() => { - setDialog("member"); - }, []); - return ( <> <UiComponent @@ -181,7 +177,7 @@ export default function TimelinePageTemplate<TManageItem>( ? onManage : undefined } - onMember={onMember} + onMember={() => setDialog("member")} /> {dialogElement} </> diff --git a/Timeline/ClientApp/src/app/views/timeline-common/TimelinePageTemplateUI.tsx b/Timeline/ClientApp/src/app/views/timeline-common/TimelinePageTemplateUI.tsx index 7af11efa..e25ed962 100644 --- a/Timeline/ClientApp/src/app/views/timeline-common/TimelinePageTemplateUI.tsx +++ b/Timeline/ClientApp/src/app/views/timeline-common/TimelinePageTemplateUI.tsx @@ -1,9 +1,9 @@ import React, { CSSProperties } from "react"; -import { Spinner } from "reactstrap"; +import clsx from "clsx"; import { useTranslation } from "react-i18next"; import { fromEvent } from "rxjs"; import Svg from "react-inlinesvg"; -import clsx from "clsx"; +import { Spinner } from "react-bootstrap"; import arrowsAngleContractIcon from "bootstrap-icons/icons/arrows-angle-contract.svg"; import arrowsAngleExpandIcon from "bootstrap-icons/icons/arrows-angle-expand.svg"; @@ -262,7 +262,7 @@ export default function TimelinePageTemplateUI<TManageItems>( } else { timelineBody = ( <div className="full-viewport-center-child"> - <Spinner color="primary" type="grow" /> + <Spinner variant="primary" animation="grow" /> </div> ); } @@ -271,7 +271,7 @@ export default function TimelinePageTemplateUI<TManageItems>( body = ( <> <div - className="fixed-top mt-appbar info-card-container" + className="info-card-container" data-collapse={infoCardCollapse ? "true" : "false"} > <Svg @@ -304,7 +304,7 @@ export default function TimelinePageTemplateUI<TManageItems>( } else { body = ( <div className="full-viewport-center-child"> - <Spinner color="primary" type="grow" /> + <Spinner variant="primary" animation="grow" /> </div> ); } @@ -313,13 +313,7 @@ export default function TimelinePageTemplateUI<TManageItems>( return ( <> <AppBar /> - <div> - <div - style={{ height: 56 + cardHeight }} - className="timeline-page-top-space flex-fix-length" - /> - {body} - </div> + {body} </> ); } diff --git a/Timeline/ClientApp/src/app/views/timeline-common/TimelinePostEdit.tsx b/Timeline/ClientApp/src/app/views/timeline-common/TimelinePostEdit.tsx index 6a8bb000..42f83b52 100644 --- a/Timeline/ClientApp/src/app/views/timeline-common/TimelinePostEdit.tsx +++ b/Timeline/ClientApp/src/app/views/timeline-common/TimelinePostEdit.tsx @@ -1,7 +1,7 @@ import React from "react"; -import { Button, Spinner, Row, Col } from "reactstrap"; import { useTranslation } from "react-i18next"; import Svg from "react-inlinesvg"; +import { Button, Spinner, Row, Col, Form } from "react-bootstrap"; import textIcon from "bootstrap-icons/icons/card-text.svg"; import imageIcon from "bootstrap-icons/icons/image.svg"; @@ -10,8 +10,6 @@ import { UiLogicError } from "@/common"; import { pushAlert } from "@/services/alert"; import { TimelineCreatePostRequest } from "@/services/timeline"; -import FileInput from "../common/FileInput"; - interface TimelinePostEditImageProps { onSelect: (blob: Blob | null) => void; } @@ -59,11 +57,11 @@ const TimelinePostEditImage: React.FC<TimelinePostEditImageProps> = (props) => { return ( <> - <FileInput - labelText={t("chooseImage")} + <Form.File + label={t("chooseImage")} onChange={onInputChange} accept="image/*" - className="mx-3 my-1" + className="mx-3 my-1 d-inline-block" /> {fileUrl && error == null && ( <img @@ -189,7 +187,8 @@ const TimelinePostEdit: React.FC<TimelinePostEditProps> = (props) => { <Row> <Col className="px-1 py-1"> {kind === "text" ? ( - <textarea + <Form.Control + as="textarea" className="w-100 h-100 timeline-post-edit" value={text} disabled={state === "process"} @@ -203,7 +202,7 @@ const TimelinePostEdit: React.FC<TimelinePostEditProps> = (props) => { <TimelinePostEditImage onSelect={onImageSelect} /> )} </Col> - <Col sm="col-auto align-self-end m-1"> + <Col xs="auto" className="align-self-end m-1"> {(() => { if (state === "input") { return ( @@ -216,13 +215,17 @@ const TimelinePostEdit: React.FC<TimelinePostEditProps> = (props) => { onClick={toggleKind} /> </div> - <Button color="primary" onClick={onSend} disabled={!canSend}> + <Button + variant="primary" + onClick={onSend} + disabled={!canSend} + > {t("timeline.send")} </Button> </> ); } else { - return <Spinner />; + return <Spinner variant="primary" animation="border" />; } })()} </Col> diff --git a/Timeline/ClientApp/src/app/views/timeline-common/timeline-common.sass b/Timeline/ClientApp/src/app/views/timeline-common/timeline-common.sass index 960c992d..1862de02 100644 --- a/Timeline/ClientApp/src/app/views/timeline-common/timeline-common.sass +++ b/Timeline/ClientApp/src/app/views/timeline-common/timeline-common.sass @@ -119,14 +119,6 @@ $timeline-line-color-current: #36c2e6 background: change-color($color: white, $alpha: 0.8) z-index: 100 -textarea.timeline-post-edit - @extend .border-primary - @extend .rounded - - &:focus - outline: none - box-shadow: 0 0 5px 0 $primary - .timeline-page-top-space transition: height 0.5s @@ -147,3 +139,20 @@ textarea.timeline-post-edit border-radius: 50% vertical-align: middle margin-right: 0.6em + +.info-card-container + position: sticky + z-index: 1 + + .info-card-collapse-button + z-index: 1 + position: relative + + .info-card-content + width: 100% + transform-origin: right top + transition: transform 0.5s + + &[data-collapse='true'] + .info-card-content + transform: scale(0) diff --git a/Timeline/ClientApp/src/app/views/timeline/TimelineInfoCard.tsx b/Timeline/ClientApp/src/app/views/timeline/TimelineInfoCard.tsx index e3e89057..bf5c3105 100644 --- a/Timeline/ClientApp/src/app/views/timeline/TimelineInfoCard.tsx +++ b/Timeline/ClientApp/src/app/views/timeline/TimelineInfoCard.tsx @@ -1,14 +1,8 @@ import React from "react"; import clsx from "clsx"; -import { - Dropdown, - DropdownToggle, - DropdownMenu, - DropdownItem, - Button, -} from "reactstrap"; import { useTranslation } from "react-i18next"; import { fromEvent } from "rxjs"; +import { Dropdown, Button } from "react-bootstrap"; import { useAvatar } from "@/services/user"; import { timelineVisibilityTooltipTranslationMap } from "@/services/timeline"; @@ -23,7 +17,7 @@ export type TimelineInfoCardProps = TimelineCardComponentProps< >; const TimelineInfoCard: React.FC<TimelineInfoCardProps> = (props) => { - const { onHeight, onManage } = props; + const { onHeight, onMember, onManage } = props; const { t } = useTranslation(); @@ -43,18 +37,10 @@ const TimelineInfoCard: React.FC<TimelineInfoCardProps> = (props) => { return () => subscription.unsubscribe(); }); - const [manageDropdownOpen, setManageDropdownOpen] = React.useState<boolean>( - false - ); - const toggleManageDropdown = React.useCallback( - (): void => setManageDropdownOpen((old) => !old), - [] - ); - return ( <div ref={containerRef} - className={clsx("rounded border p-2 bg-light", props.className)} + className={clsx("rounded border p-2 bg-light clearfix", props.className)} onTransitionEnd={notifyHeight} > <h3 className="text-primary mx-3 d-inline-block align-middle"> @@ -77,28 +63,28 @@ const TimelineInfoCard: React.FC<TimelineInfoCardProps> = (props) => { </small> <div className="text-right mt-2"> {onManage != null ? ( - <Dropdown isOpen={manageDropdownOpen} toggle={toggleManageDropdown}> - <DropdownToggle outline color="primary"> + <Dropdown> + <Dropdown.Toggle variant="outline-primary"> {t("timeline.manage")} - </DropdownToggle> - <DropdownMenu> - <DropdownItem onClick={() => onManage("property")}> + </Dropdown.Toggle> + <Dropdown.Menu> + <Dropdown.Item onClick={() => onManage("property")}> {t("timeline.manageItem.property")} - </DropdownItem> - <DropdownItem onClick={props.onMember}> + </Dropdown.Item> + <Dropdown.Item onClick={onMember}> {t("timeline.manageItem.member")} - </DropdownItem> - <DropdownItem divider /> - <DropdownItem + </Dropdown.Item> + <Dropdown.Divider /> + <Dropdown.Item className="text-danger" onClick={() => onManage("delete")} > {t("timeline.manageItem.delete")} - </DropdownItem> - </DropdownMenu> + </Dropdown.Item> + </Dropdown.Menu> </Dropdown> ) : ( - <Button color="primary" outline onClick={props.onMember}> + <Button variant="outline-primary" onClick={onMember}> {t("timeline.memberButton")} </Button> )} diff --git a/Timeline/ClientApp/src/app/views/timeline/timeline.sass b/Timeline/ClientApp/src/app/views/timeline/timeline.sass index 0eeec73a..e69de29b 100644 --- a/Timeline/ClientApp/src/app/views/timeline/timeline.sass +++ b/Timeline/ClientApp/src/app/views/timeline/timeline.sass @@ -1,14 +0,0 @@ -.info-card-container - .info-card-collapse-button - z-index: 1 - position: relative - - .info-card-content - width: 100% - position: absolute - transform-origin: right top - transition: transform 0.5s - - &[data-collapse='true'] - .info-card-content - transform: scale(0) diff --git a/Timeline/ClientApp/src/app/views/user/ChangeAvatarDialog.tsx b/Timeline/ClientApp/src/app/views/user/ChangeAvatarDialog.tsx index 1dd2ee8b..ffa2218b 100644 --- a/Timeline/ClientApp/src/app/views/user/ChangeAvatarDialog.tsx +++ b/Timeline/ClientApp/src/app/views/user/ChangeAvatarDialog.tsx @@ -1,14 +1,7 @@ import React, { useState, useEffect } from "react"; import { useTranslation } from "react-i18next"; -import { - Modal, - ModalHeader, - Row, - Button, - ModalBody, - ModalFooter, -} from "reactstrap"; import { AxiosError } from "axios"; +import { Modal, Row, Button } from "react-bootstrap"; import { UiLogicError } from "@/common"; @@ -56,7 +49,7 @@ const ChangeAvatarDialog: React.FC<ChangeAvatarDialogProps> = (props) => { const closeDialog = props.close; - const toggle = React.useCallback((): void => { + const close = React.useCallback((): void => { if (!(state === "uploading")) { closeDialog(); } @@ -163,23 +156,25 @@ const ChangeAvatarDialog: React.FC<ChangeAvatarDialogProps> = (props) => { }; return ( - <Modal isOpen={props.open} toggle={toggle}> - <ModalHeader> {t("userPage.dialogChangeAvatar.title")}</ModalHeader> + <Modal show={props.open} onHide={close}> + <Modal.Header> + <Modal.Title> {t("userPage.dialogChangeAvatar.title")}</Modal.Title> + </Modal.Header> {(() => { if (state === "select") { return ( <> - <ModalBody className="container"> + <Modal.Body className="container"> <Row>{t("userPage.dialogChangeAvatar.prompt.select")}</Row> <Row> <input type="file" accept="image/*" onChange={onSelectFile} /> </Row> - </ModalBody> - <ModalFooter> - <Button color="secondary" onClick={toggle}> + </Modal.Body> + <Modal.Footer> + <Button variant="secondary" onClick={close}> {t("operationDialog.cancel")} </Button> - </ModalFooter> + </Modal.Footer> </> ); } else if (state === "crop") { @@ -188,7 +183,7 @@ const ChangeAvatarDialog: React.FC<ChangeAvatarDialogProps> = (props) => { } return ( <> - <ModalBody className="container"> + <Modal.Body className="container"> <Row className="justify-content-center"> <ImageCropper clip={clip} @@ -198,12 +193,12 @@ const ChangeAvatarDialog: React.FC<ChangeAvatarDialogProps> = (props) => { /> </Row> <Row>{t("userPage.dialogChangeAvatar.prompt.crop")}</Row> - </ModalBody> - <ModalFooter> - <Button color="secondary" onClick={toggle}> + </Modal.Body> + <Modal.Footer> + <Button variant="secondary" onClick={close}> {t("operationDialog.cancel")} </Button> - <Button color="secondary" onClick={onCropPrevious}> + <Button variant="secondary" onClick={onCropPrevious}> {t("operationDialog.previousStep")} </Button> <Button @@ -215,87 +210,87 @@ const ChangeAvatarDialog: React.FC<ChangeAvatarDialogProps> = (props) => { > {t("operationDialog.nextStep")} </Button> - </ModalFooter> + </Modal.Footer> </> ); } else if (state === "processcrop") { return ( <> - <ModalBody className="container"> + <Modal.Body className="container"> <Row> {t("userPage.dialogChangeAvatar.prompt.processingCrop")} </Row> - </ModalBody> - <ModalFooter> - <Button color="secondary" onClick={toggle}> + </Modal.Body> + <Modal.Footer> + <Button variant="secondary" onClick={close}> {t("operationDialog.cancel")} </Button> - <Button color="secondary" onClick={onPreviewPrevious}> + <Button variant="secondary" onClick={onPreviewPrevious}> {t("operationDialog.previousStep")} </Button> - </ModalFooter> + </Modal.Footer> </> ); } else if (state === "preview") { return ( <> - <ModalBody className="container"> + <Modal.Body className="container"> {createPreviewRow()} <Row>{t("userPage.dialogChangeAvatar.prompt.preview")}</Row> - </ModalBody> - <ModalFooter> - <Button color="secondary" onClick={toggle}> + </Modal.Body> + <Modal.Footer> + <Button variant="secondary" onClick={close}> {t("operationDialog.cancel")} </Button> - <Button color="secondary" onClick={onPreviewPrevious}> + <Button variant="secondary" onClick={onPreviewPrevious}> {t("operationDialog.previousStep")} </Button> - <Button color="primary" onClick={upload}> + <Button variant="primary" onClick={upload}> {t("userPage.dialogChangeAvatar.upload")} </Button> - </ModalFooter> + </Modal.Footer> </> ); } else if (state === "uploading") { return ( <> - <ModalBody className="container"> + <Modal.Body className="container"> {createPreviewRow()} <Row>{t("userPage.dialogChangeAvatar.prompt.uploading")}</Row> - </ModalBody> - <ModalFooter></ModalFooter> + </Modal.Body> + <Modal.Footer></Modal.Footer> </> ); } else if (state === "success") { return ( <> - <ModalBody className="container"> + <Modal.Body className="container"> <Row className="p-4 text-success"> {t("operationDialog.success")} </Row> - </ModalBody> - <ModalFooter> - <Button color="success" onClick={toggle}> + </Modal.Body> + <Modal.Footer> + <Button variant="success" onClick={close}> {t("operationDialog.ok")} </Button> - </ModalFooter> + </Modal.Footer> </> ); } else { return ( <> - <ModalBody className="container"> + <Modal.Body className="container"> {createPreviewRow()} <Row className="text-danger">{trueMessage}</Row> - </ModalBody> - <ModalFooter> - <Button color="secondary" onClick={toggle}> + </Modal.Body> + <Modal.Footer> + <Button variant="secondary" onClick={close}> {t("operationDialog.cancel")} </Button> - <Button color="primary" onClick={upload}> + <Button variant="primary" onClick={upload}> {t("operationDialog.retry")} </Button> - </ModalFooter> + </Modal.Footer> </> ); } diff --git a/Timeline/ClientApp/src/app/views/user/UserInfoCard.tsx b/Timeline/ClientApp/src/app/views/user/UserInfoCard.tsx index 1a111877..f1878b5c 100644 --- a/Timeline/ClientApp/src/app/views/user/UserInfoCard.tsx +++ b/Timeline/ClientApp/src/app/views/user/UserInfoCard.tsx @@ -1,14 +1,8 @@ import React from "react"; import clsx from "clsx"; -import { - Dropdown, - DropdownToggle, - DropdownMenu, - DropdownItem, - Button, -} from "reactstrap"; import { useTranslation } from "react-i18next"; import { fromEvent } from "rxjs"; +import { Dropdown, Button } from "react-bootstrap"; import { timelineVisibilityTooltipTranslationMap } from "@/services/timeline"; import { useAvatar } from "@/services/user"; @@ -42,24 +36,16 @@ const UserInfoCard: React.FC<UserInfoCardProps> = (props) => { return () => subscription.unsubscribe(); }); - const [manageDropdownOpen, setManageDropdownOpen] = React.useState<boolean>( - false - ); - const toggleManageDropdown = React.useCallback( - (): void => setManageDropdownOpen((old) => !old), - [] - ); - return ( <div ref={containerRef} - className={clsx("rounded border bg-light p-2", props.className)} + className={clsx("rounded border bg-light p-2 clearfix", props.className)} onTransitionEnd={notifyHeight} > <BlobImage blob={avatar} onLoad={notifyHeight} - className="avatar large mr-2 mb-2 rounded-circle float-left" + className="avatar large mr-2 rounded-circle float-left" /> <div> {props.timeline.owner.nickname} @@ -73,27 +59,27 @@ const UserInfoCard: React.FC<UserInfoCardProps> = (props) => { </small> <div className="text-right mt-2"> {onManage != null ? ( - <Dropdown isOpen={manageDropdownOpen} toggle={toggleManageDropdown}> - <DropdownToggle outline color="primary"> + <Dropdown> + <Dropdown.Toggle variant="outline-primary"> {t("timeline.manage")} - </DropdownToggle> - <DropdownMenu> - <DropdownItem onClick={() => onManage("nickname")}> + </Dropdown.Toggle> + <Dropdown.Menu> + <Dropdown.Item onClick={() => onManage("nickname")}> {t("timeline.manageItem.nickname")} - </DropdownItem> - <DropdownItem onClick={() => onManage("avatar")}> + </Dropdown.Item> + <Dropdown.Item onClick={() => onManage("avatar")}> {t("timeline.manageItem.avatar")} - </DropdownItem> - <DropdownItem onClick={() => onManage("property")}> + </Dropdown.Item> + <Dropdown.Item onClick={() => onManage("property")}> {t("timeline.manageItem.property")} - </DropdownItem> - <DropdownItem onClick={props.onMember}> + </Dropdown.Item> + <Dropdown.Item onClick={props.onMember}> {t("timeline.manageItem.member")} - </DropdownItem> - </DropdownMenu> + </Dropdown.Item> + </Dropdown.Menu> </Dropdown> ) : ( - <Button color="primary" outline onClick={props.onMember}> + <Button variant="outline-primary" onClick={props.onMember}> {t("timeline.memberButton")} </Button> )} |