diff --git a/src/containers/App.jsx b/src/containers/App.jsx
index 2ed4b68..f5f7a50 100644
--- a/src/containers/App.jsx
+++ b/src/containers/App.jsx
@@ -2,26 +2,19 @@ import React from "react";
import { CssBaseline, ThemeProvider, Container, Box } from "@material-ui/core";
import { ThemeProvider as StyledComponentsThemeProvider } from "styled-components";
import {
- useFilters,
useTitle,
+ useTabs,
theme,
- useShare,
- useCalculation,
} from "../utils";
-import { Title, Filter, Footer } from "../containers";
-import { ShareDialog, Chart, Table } from "../components";
+import { Title, Footer, IslandTabs, Calculator } from "../containers";
const App = () => {
useTitle();
- const { inputFilters, filters, saveFilters } = useFilters();
- const {
- onCloseShareModal,
- showShareDialog,
- openShareDialog,
- shareFilters,
- } = useShare(filters);
+ const {tabs, addTab, deleteTab, value, handleTabChange} = useTabs();
- const result = useCalculation({ filters });
+ const panelMarkup = tabs.map((tab, index) => (
+
+ ));
return (
@@ -30,21 +23,17 @@ const App = () => {
-
-
-
-
-
+
+ {panelMarkup}
+
+
-
);
diff --git a/src/containers/Calculator.jsx b/src/containers/Calculator.jsx
new file mode 100644
index 0000000..78db731
--- /dev/null
+++ b/src/containers/Calculator.jsx
@@ -0,0 +1,41 @@
+import React from "react";
+import PropTypes from "prop-types";
+import { useFilters, useShare, useCalculation } from "../utils";
+import { Filter, TabPanel } from "../containers";
+import { ShareDialog, Chart, Table } from "../components";
+
+const Calculator = ({ value, index, filterKey }) => {
+ const { inputFilters, filters, saveFilters } = useFilters(filterKey);
+ const {
+ onCloseShareModal,
+ showShareDialog,
+ openShareDialog,
+ shareFilters,
+ } = useShare(filters);
+ const result = useCalculation({ filters });
+
+ return (
+
+
+
+
+
+
+ );
+};
+
+Calculator.propTypes = {
+ value: PropTypes.number.isRequired,
+ index: PropTypes.number.isRequired,
+ filterKey: PropTypes.string.isRequired,
+};
+
+export default Calculator;
diff --git a/src/containers/IslandTabs.jsx b/src/containers/IslandTabs.jsx
new file mode 100644
index 0000000..e0630de
--- /dev/null
+++ b/src/containers/IslandTabs.jsx
@@ -0,0 +1,87 @@
+import React from "react";
+import PropTypes from "prop-types";
+import { useTranslation } from "react-i18next";
+import { makeStyles } from "@material-ui/styles";
+import { Tab, Tabs } from "@material-ui/core";
+import { Add, Close } from "@material-ui/icons";
+
+const useTabsStyles = makeStyles(({ spacing }) => ({
+ root: {
+ marginLeft: spacing(2),
+ },
+ indicator: {
+ backgroundColor: "#18A558",
+ },
+}));
+
+const useTabStyles = makeStyles(({ breakpoints, spacing }) => ({
+ root: {
+ textTransform: "initial",
+ padding: spacing(-1, 2),
+ [breakpoints.up("md")]: {
+ minWidth: 120,
+ },
+ "&:hover": {
+ backgroundColor: "rgba(13, 152, 186, 0.1)",
+ "& .MuiTab-label": {
+ color: "#18A558",
+ },
+ },
+ "&$selected": {
+ "& *": {
+ color: "#18A558",
+ },
+ },
+ },
+ selected: {},
+ textColorInherit: {
+ opacity: 1,
+ },
+ wrapper: {
+ flexDirection: "row-reverse",
+ letterSpacing: 0.5,
+ '& svg, .material-icons': {
+ marginLeft: 12,
+ marginTop: 7,
+ },
+ },
+}));
+
+const IslandTabs = ({ tabs, onAdd, onDelete, onChange, value }) => {
+ const { t } = useTranslation()
+ const tabClasses = useTabStyles();
+ const tabsClasses = useTabsStyles();
+
+ return (
+
+ {tabs.map((tab, index) => (
+ }
+ classes={{
+ ...tabClasses,
+ wrapper: `${tabClasses.wrapper} MuiTab-label`,
+ }}
+ />
+ ))}
+ } />
+
+ );
+};
+
+IslandTabs.propTypes = {
+ tabs: PropTypes.arrayOf(
+ PropTypes.shape({
+ id: PropTypes.number.isRequired,
+ key: PropTypes.string.isRequired,
+ }),
+ ),
+ value: PropTypes.number.isRequired,
+ onChange: PropTypes.func.isRequired,
+ onAdd: PropTypes.func.isRequired,
+ onDelete: PropTypes.func.isRequired,
+};
+
+export default IslandTabs;
diff --git a/src/containers/TabPanel.jsx b/src/containers/TabPanel.jsx
new file mode 100644
index 0000000..8e5b2f9
--- /dev/null
+++ b/src/containers/TabPanel.jsx
@@ -0,0 +1,24 @@
+import React from "react";
+import PropTypes from "prop-types";
+
+// Manual implementation of https://material-ui.com/api/tab-panel/
+// TabPanel not yet available in @material-ui/core
+const TabPanel = ({ value, index, children }) => {
+ return (
+
+ {value === index && children}
+
+ );
+};
+
+TabPanel.propTypes = {
+ value: PropTypes.number.isRequired,
+ index: PropTypes.number.isRequired,
+ children: PropTypes.node,
+};
+
+export default TabPanel;
diff --git a/src/containers/index.js b/src/containers/index.js
index cfd2c46..189872b 100644
--- a/src/containers/index.js
+++ b/src/containers/index.js
@@ -1,4 +1,7 @@
export { default as App } from "./App";
+export { default as IslandTabs } from "./IslandTabs";
+export { default as TabPanel } from "./TabPanel";
+export { default as Calculator } from "./Calculator";
export { default as Filter } from "./Filter";
export { default as Footer } from "./Footer";
export { default as Localizer } from "./Localizer";
diff --git a/src/locales/en/translation.json b/src/locales/en/translation.json
index 3d1dbd1..3aaf024 100644
--- a/src/locales/en/translation.json
+++ b/src/locales/en/translation.json
@@ -33,5 +33,7 @@
"Chance": "Chance",
"Pattern": "Pattern",
"patternNames": "Fluctuating_High Spike_Decreasing_Small Spike",
- "All Patterns": "All Patterns"
+ "All Patterns": "All Patterns",
+ "Add Island": "Add Island",
+ "Island": "Island"
}
diff --git a/src/utils/index.js b/src/utils/index.js
index 2cf4679..37a6fa5 100644
--- a/src/utils/index.js
+++ b/src/utils/index.js
@@ -5,3 +5,4 @@ export { default as useShare } from "./useShare";
export { default as useTitle } from "./useTitle";
export { default as useWeekDays } from "./useWeekDays";
export { default as useCalculation } from "./useCalculation";
+export { default as useTabs } from "./useTabs";
diff --git a/src/utils/useFilters.js b/src/utils/useFilters.js
index ff8307f..f797b51 100644
--- a/src/utils/useFilters.js
+++ b/src/utils/useFilters.js
@@ -1,8 +1,8 @@
import { useEffect, useMemo } from "react";
import { useLocalStorage } from "react-use";
-const useFilters = () => {
- const [filters, saveFilters] = useLocalStorage("filters", []);
+const useFilters = (key) => {
+ const [filters, saveFilters] = useLocalStorage(key, []);
// Array of strings
const inputFilters = useMemo(
diff --git a/src/utils/useTabs.js b/src/utils/useTabs.js
new file mode 100644
index 0000000..6cc7711
--- /dev/null
+++ b/src/utils/useTabs.js
@@ -0,0 +1,79 @@
+import { useState, useEffect, useCallback } from "react";
+import { useLocalStorage } from "react-use";
+
+const DEFAULT_TAB_LIST = [
+ {
+ id: 0,
+ key: "filters-0",
+ },
+];
+
+const useTabs = () => {
+ const [value, setValue] = useState(0);
+ const [tabs, saveTabs] = useLocalStorage("tablist", DEFAULT_TAB_LIST);
+
+ useEffect(() => {
+ if (!Array.isArray(tabs)) {
+ saveTabs(DEFAULT_TAB_LIST);
+ }
+ }, [tabs, saveTabs]);
+
+ const addTab = useCallback(() => {
+ let id = 0
+ for (id = 0; id < tabs.length; id++) {
+ if (tabs[id].id !== id) {
+ break;
+ }
+ }
+ setValue(id);
+ saveTabs([...tabs.slice(0, id), {
+ id,
+ key: `filters-${id}`,
+ }, ...tabs.slice(id)]);
+ }, [tabs, saveTabs]);
+
+ const deleteTab = useCallback((event) => {
+ // Prevent MaterialUI from switching tabs
+ event.stopPropagation();
+
+ // Prevent deleting the last tab
+ if (tabs.length === 1) {
+ return;
+ }
+
+ let deletedTabIndex = 0;
+ const tabId = parseInt(event.target.id, 10);
+
+ const tabList = tabs.filter((tab, index) => {
+ if (tab.id === tabId) {
+ deletedTabIndex = index;
+ localStorage.removeItem(tab.key);
+ }
+ return tab.id !== tabId;
+ });
+
+ if (deletedTabIndex !== 0) {
+ setValue(deletedTabIndex - 1)
+ }
+
+ saveTabs(tabList);
+ }, [tabs, saveTabs]);
+
+ const handleTabChange = useCallback((_event, newValue) => {
+ if (newValue === tabs.length) {
+ addTab();
+ } else {
+ setValue(newValue);
+ }
+ }, [tabs, addTab]);
+
+ return {
+ value,
+ tabs,
+ addTab,
+ deleteTab,
+ handleTabChange,
+ };
+};
+
+export default useTabs;