added CategoriesModal EmojiPickerButton CategorySign AddCategoryModal
updated NavigationBar AnalyticsModal SettingsModal
This commit is contained in:
@@ -9,6 +9,8 @@
|
||||
"@testing-library/user-event": "^13.5.0",
|
||||
"bootstrap": "^5.3.7",
|
||||
"chart.js": "^4.5.0",
|
||||
"classnames": "^2.5.1",
|
||||
"emoji-picker-react": "^4.13.2",
|
||||
"faker": "^6.6.6",
|
||||
"react": "^19.1.0",
|
||||
"react-bootstrap": "^2.10.10",
|
||||
|
||||
@@ -0,0 +1,69 @@
|
||||
import { useTheme } from "../../contexts/ThemeContext";
|
||||
import { Button } from "react-bootstrap";
|
||||
import styles from "./CategorySign.module.css";
|
||||
import classNames from "classnames";
|
||||
|
||||
const CategorySign = ({ icon, title, subcategories }) => {
|
||||
const { theme } = useTheme();
|
||||
return (
|
||||
<Button variant={theme} className="w-100 mt-3">
|
||||
<div>
|
||||
<div className="d-flex flex-row gap-3 p-2">
|
||||
<div
|
||||
className="p-2 justify-content-start align-items-start d-flex flex-column"
|
||||
style={{ fontSize: "30px" }}
|
||||
>
|
||||
<div>{icon}</div>
|
||||
</div>
|
||||
<div className="d-flex flex-column p-0 justify-content-center align-items-start w-100">
|
||||
<h3
|
||||
className="mb-0 fw-bolder"
|
||||
style={{ fontSize: "1.1rem", fontWeight: 700 }}
|
||||
>
|
||||
{title}
|
||||
</h3>
|
||||
<div
|
||||
className="text-muted small mb-1"
|
||||
style={{ fontSize: "0.8rem" }}
|
||||
>
|
||||
{subcategories.length === 0
|
||||
? "No subcategories"
|
||||
: `Subcategories: ${subcategories.length}`}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{subcategories.length === 0 ? (
|
||||
""
|
||||
) : (
|
||||
<div
|
||||
className="d-flex align-items-center gap-3 p-0"
|
||||
style={{ margin: "0 20px" }}
|
||||
>
|
||||
<div
|
||||
className="p-2 justify-content-start align-items-start d-flex flex-column"
|
||||
// style={{ fontSize: "30px" }}
|
||||
>
|
||||
<div>➞</div>
|
||||
</div>
|
||||
<div
|
||||
className={classNames("d-flex gap-3", styles.hideScrollbar)}
|
||||
style={{ width: "100%", overflowX: "auto" }}
|
||||
>
|
||||
{subcategories.map((subcategory) => (
|
||||
<Button
|
||||
variant="secondary"
|
||||
style={{ borderRadius: "25px", padding: "0 15px" }}
|
||||
key={subcategory.id}
|
||||
>
|
||||
{subcategory.name}
|
||||
</Button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</Button>
|
||||
);
|
||||
};
|
||||
|
||||
export default CategorySign;
|
||||
@@ -0,0 +1,7 @@
|
||||
.hideScrollbar {
|
||||
scrollbar-width: none;
|
||||
-ms-overflow-style: none;
|
||||
}
|
||||
.hideScrollbar::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
import React, { useState, useRef } from "react";
|
||||
import { Button, Overlay, Popover, InputGroup } from "react-bootstrap";
|
||||
import EmojiPicker from "emoji-picker-react";
|
||||
import { useTheme } from "../../contexts/ThemeContext";
|
||||
|
||||
const EmojiPickerButton = () => {
|
||||
const { theme } = useTheme();
|
||||
const [showPicker, setShowPicker] = useState(false);
|
||||
const [inputValue, setInputValue] = useState("😀");
|
||||
const target = useRef(null);
|
||||
|
||||
const handleEmojiSelect = (emoji) => {
|
||||
setInputValue(emoji.emoji);
|
||||
setShowPicker(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="p-3">
|
||||
<InputGroup>
|
||||
<Button
|
||||
ref={target}
|
||||
variant="secondary"
|
||||
style={{
|
||||
borderRadius: "50%",
|
||||
width: "100px",
|
||||
height: "100px",
|
||||
fontSize: "45px",
|
||||
}}
|
||||
onClick={() => setShowPicker(!showPicker)}
|
||||
>
|
||||
{inputValue}
|
||||
</Button>
|
||||
|
||||
<Overlay
|
||||
show={showPicker}
|
||||
target={target.current}
|
||||
placement="bottom"
|
||||
rootClose
|
||||
onHide={() => setShowPicker(false)}
|
||||
>
|
||||
<Popover id="emoji-popover">
|
||||
<Popover.Body className="p-0">
|
||||
<EmojiPicker
|
||||
onEmojiClick={handleEmojiSelect}
|
||||
height={350}
|
||||
width="100%"
|
||||
previewConfig={{ showPreview: false }}
|
||||
theme={theme}
|
||||
/>
|
||||
</Popover.Body>
|
||||
</Popover>
|
||||
</Overlay>
|
||||
</InputGroup>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default EmojiPickerButton;
|
||||
@@ -4,14 +4,14 @@ import SettingsModal from "../../modals/SettingsModal/SettingsModal";
|
||||
|
||||
const NavigationBar = () => {
|
||||
return (
|
||||
<Navbar
|
||||
<div
|
||||
expand="lg"
|
||||
className="justify-content-between"
|
||||
className="d-flex align-items-center justify-content-between"
|
||||
style={{ padding: "1rem 1rem 0 1rem" }}
|
||||
>
|
||||
<Navbar.Brand href="/">Wallet</Navbar.Brand>
|
||||
<SettingsModal />
|
||||
</Navbar>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -0,0 +1,70 @@
|
||||
import { useState } from "react";
|
||||
import { useTheme } from "../../contexts/ThemeContext";
|
||||
import { Modal, Button, Card, Form } from "react-bootstrap";
|
||||
import EmojiPickerButton from "../../components/EmojiPickerButton/EmojiPickerButton";
|
||||
|
||||
const AddCategoryModal = ({ variant, ...props }) => {
|
||||
const { theme } = useTheme();
|
||||
const [show, setShow] = useState(false);
|
||||
const handleClose = () => setShow(false);
|
||||
const handleShow = () => setShow(true);
|
||||
const [title, setTitle] = useState("");
|
||||
return (
|
||||
<>
|
||||
<Button variant={theme} onClick={handleShow} className="m-0" {...props}>
|
||||
New Category
|
||||
</Button>
|
||||
<Modal show={show} onHide={handleClose} fullscreen={true}>
|
||||
<Modal.Header className="d-flex align-items-center justify-content-between">
|
||||
<spacer />
|
||||
<Modal.Title>New Category</Modal.Title>
|
||||
<Button
|
||||
variant={theme}
|
||||
type="button"
|
||||
onClick={handleClose}
|
||||
style={{
|
||||
borderRadius: "50%",
|
||||
width: "30px",
|
||||
height: "30px",
|
||||
padding: "0",
|
||||
}}
|
||||
>
|
||||
✕
|
||||
</Button>
|
||||
</Modal.Header>
|
||||
<Modal.Body className="p-3">
|
||||
<div className="d-flex align-items-center justify-content-center">
|
||||
<EmojiPickerButton />
|
||||
</div>
|
||||
<Form.Control
|
||||
type="text"
|
||||
placeholder="Title"
|
||||
value={title}
|
||||
onChange={(e) => setTitle(e.target.value)}
|
||||
className="mb-3"
|
||||
/>
|
||||
<Card>
|
||||
<Card.Body>
|
||||
<div
|
||||
className="text-muted small mb-1"
|
||||
style={{ fontSize: "0.8rem" }}
|
||||
>
|
||||
SUBCATEGORIES
|
||||
</div>
|
||||
<Button
|
||||
variant={"secondary"}
|
||||
onClick={() => {}}
|
||||
className="w-100 text-start mb-3"
|
||||
>
|
||||
Add subcategory
|
||||
</Button>
|
||||
<spacer />
|
||||
<Button className="w-100">Add</Button>
|
||||
</Card.Body>
|
||||
</Card>
|
||||
</Modal.Body>
|
||||
</Modal>
|
||||
</>
|
||||
);
|
||||
};
|
||||
export default AddCategoryModal;
|
||||
@@ -145,7 +145,7 @@ const AnalyticsModal = () => {
|
||||
<spacer />
|
||||
</Modal.Header>
|
||||
<Modal.Body style={{ paddingLeft: "0", paddingRight: "0" }}>
|
||||
<Tabs defaultActiveKey="Expenses" id="history-tabs">
|
||||
<Tabs defaultActiveKey="Expenses" id="analytics-tabs">
|
||||
<Tab eventKey="Incomes" title="Incomes"></Tab>
|
||||
<Tab eventKey="Expenses" title="Expenses" style={{ padding: "0" }}>
|
||||
<div className="d-flex mt-2 justify-content-center">
|
||||
|
||||
@@ -0,0 +1,113 @@
|
||||
import { useState } from "react";
|
||||
import { useTheme } from "../../contexts/ThemeContext";
|
||||
import {
|
||||
Modal,
|
||||
Button,
|
||||
Form,
|
||||
InputGroup,
|
||||
Tabs,
|
||||
Tab,
|
||||
Dropdown,
|
||||
DropdownButton,
|
||||
} from "react-bootstrap";
|
||||
import styles from "./CategoriesModal.module.css";
|
||||
import CategorySign from "../../components/CategorySign/CategorySign";
|
||||
import AddCategoryModal from "../../modals/AddCategoryModal/AddCategoryModal";
|
||||
|
||||
const CategoriesModal = ({ variant }) => {
|
||||
const { theme } = useTheme();
|
||||
const [show, setShow] = useState(false);
|
||||
const handleClose = () => setShow(false);
|
||||
const handleShow = () => setShow(true);
|
||||
return (
|
||||
<>
|
||||
<Button
|
||||
variant={theme}
|
||||
onClick={handleShow}
|
||||
className="w-100 d-flex justify-content-between"
|
||||
>
|
||||
<div>© Categories</div>
|
||||
<div>❯</div>
|
||||
</Button>
|
||||
<Modal show={show} onHide={handleClose} fullscreen={true}>
|
||||
<Modal.Header>
|
||||
<div className="d-flex flex-column w-100 gap-2">
|
||||
<div className="d-flex align-items-center justify-content-between">
|
||||
<Button
|
||||
variant={theme}
|
||||
onClick={handleClose}
|
||||
className="d-flex justify-content-between gap-2"
|
||||
>
|
||||
<div style={{ transform: "rotate(180deg)" }}>❯</div>
|
||||
<div>Settings</div>
|
||||
</Button>
|
||||
<Modal.Title>Categories</Modal.Title>
|
||||
<spacer />
|
||||
<DropdownButton title="☰" variant={theme} drop="start">
|
||||
<Dropdown.Item className="p-0">
|
||||
<AddCategoryModal
|
||||
className="w-100"
|
||||
style={{ paddingLeft: "0" }}
|
||||
/>
|
||||
</Dropdown.Item>
|
||||
<Dropdown.Item>Reorder</Dropdown.Item>
|
||||
</DropdownButton>
|
||||
</div>
|
||||
<InputGroup className="mb-3">
|
||||
<InputGroup.Text id="basic-addon1">
|
||||
<div style={{ transform: "rotate(90deg)" }}>☌</div>
|
||||
</InputGroup.Text>
|
||||
<Form.Control
|
||||
placeholder="Search"
|
||||
aria-label="Search"
|
||||
aria-describedby="basic-addon1"
|
||||
/>
|
||||
</InputGroup>
|
||||
</div>
|
||||
</Modal.Header>
|
||||
<Modal.Body className="p-0">
|
||||
<Tabs
|
||||
defaultActiveKey="Expenses"
|
||||
id="categories-tabs"
|
||||
className="nav nav-tabs"
|
||||
>
|
||||
<Tab eventKey="Incomes" title="Incomes">
|
||||
asdf
|
||||
</Tab>
|
||||
<Tab eventKey="Expenses" title="Expenses">
|
||||
<div
|
||||
className={styles.scrollableContent}
|
||||
style={{ padding: "10px" }}
|
||||
>
|
||||
<CategorySign
|
||||
icon="😀"
|
||||
title="Cash Loan"
|
||||
subcategories={[]}
|
||||
/>
|
||||
<CategorySign
|
||||
icon="😀"
|
||||
title="Cash Loan"
|
||||
subcategories={[
|
||||
{ id: 0, name: "Tbank" },
|
||||
{ id: 1, name: "Sber" },
|
||||
{ id: 2, name: "VTB" },
|
||||
{ id: 0, name: "Tbank" },
|
||||
{ id: 1, name: "Sber" },
|
||||
{ id: 2, name: "VTB" },
|
||||
{ id: 0, name: "Tbank" },
|
||||
{ id: 1, name: "Sber" },
|
||||
{ id: 2, name: "VTB" },
|
||||
{ id: 0, name: "Tbank" },
|
||||
{ id: 1, name: "Sber" },
|
||||
{ id: 2, name: "VTB" },
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
</Tab>
|
||||
</Tabs>
|
||||
</Modal.Body>
|
||||
</Modal>
|
||||
</>
|
||||
);
|
||||
};
|
||||
export default CategoriesModal;
|
||||
@@ -0,0 +1,11 @@
|
||||
.scrollableContent {
|
||||
height: calc(100vh - 220px);
|
||||
bottom: 0;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.scrollableContent {
|
||||
height: calc(100vh - 280px);
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,7 @@ import { useState } from "react";
|
||||
import { useTheme } from "../../contexts/ThemeContext";
|
||||
import { ListGroup, Modal, Button } from "react-bootstrap";
|
||||
import ThemeToggle from "../../components/ThemeToggle";
|
||||
import CategoriesModal from "../../modals/CategoriesModal/CategoriesModal";
|
||||
|
||||
const SettingsModal = () => {
|
||||
const { theme } = useTheme();
|
||||
@@ -39,6 +40,17 @@ const SettingsModal = () => {
|
||||
</Button>
|
||||
</Modal.Header>
|
||||
<Modal.Body>
|
||||
<h3 className="text-muted small m-1" style={{ fontSize: "0.8rem" }}>
|
||||
DATA
|
||||
</h3>
|
||||
<ListGroup>
|
||||
<ListGroup.Item className="p-0">
|
||||
<CategoriesModal />
|
||||
</ListGroup.Item>
|
||||
</ListGroup>
|
||||
<h3 className="text-muted small m-1" style={{ fontSize: "0.8rem" }}>
|
||||
APPLICATION
|
||||
</h3>
|
||||
<ListGroup>
|
||||
<ListGroup.Item>
|
||||
<ThemeToggle />
|
||||
|
||||
Reference in New Issue
Block a user