aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--Timeline/ClientApp/package.json2
-rw-r--r--Timeline/ClientApp/src/app/service-worker.tsx10
-rw-r--r--Timeline/ClientApp/src/app/views/about/index.tsx4
-rw-r--r--Timeline/ClientApp/src/app/views/admin/Admin.tsx25
-rw-r--r--Timeline/ClientApp/src/app/views/admin/UserAdmin.tsx47
-rw-r--r--Timeline/ClientApp/src/app/views/common/AppBar.tsx2
-rw-r--r--Timeline/ClientApp/src/app/views/common/FileInput.tsx36
-rw-r--r--Timeline/ClientApp/src/app/views/common/OperationDialog.tsx93
-rw-r--r--Timeline/ClientApp/src/app/views/common/alert/AlertHost.tsx9
-rw-r--r--Timeline/ClientApp/src/app/views/login/index.tsx53
-rw-r--r--Timeline/ClientApp/src/app/views/timeline-common/TimelineItem.tsx28
-rw-r--r--Timeline/ClientApp/src/app/views/timeline-common/TimelineMember.tsx22
-rw-r--r--Timeline/ClientApp/src/app/views/timeline-common/TimelinePageTemplate.tsx6
-rw-r--r--Timeline/ClientApp/src/app/views/timeline-common/TimelinePageTemplateUI.tsx18
-rw-r--r--Timeline/ClientApp/src/app/views/timeline-common/TimelinePostEdit.tsx23
-rw-r--r--Timeline/ClientApp/src/app/views/timeline-common/timeline-common.sass25
-rw-r--r--Timeline/ClientApp/src/app/views/timeline/TimelineInfoCard.tsx46
-rw-r--r--Timeline/ClientApp/src/app/views/timeline/timeline.sass14
-rw-r--r--Timeline/ClientApp/src/app/views/user/ChangeAvatarDialog.tsx93
-rw-r--r--Timeline/ClientApp/src/app/views/user/UserInfoCard.tsx48
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>
)}