mirror of
https://github.com/rowyio/rowy.git
synced 2025-12-29 00:16:39 +01:00
consolidate Navigation, add settings pages stubs
This commit is contained in:
@@ -18,7 +18,7 @@
|
||||
"@material-ui/icons": "^5.0.0-beta.4",
|
||||
"@material-ui/lab": "^5.0.0-alpha.43",
|
||||
"@material-ui/styles": "^5.0.0-beta.4",
|
||||
"@mdi/js": "^5.8.55",
|
||||
"@mdi/js": "^5.9.55",
|
||||
"@monaco-editor/react": "^4.1.0",
|
||||
"@tinymce/tinymce-react": "^3.4.0",
|
||||
"algoliasearch": "^4.8.6",
|
||||
|
||||
61
src/App.tsx
61
src/App.tsx
@@ -1,5 +1,5 @@
|
||||
import { lazy, Suspense } from "react";
|
||||
import { Route, Switch, Link } from "react-router-dom";
|
||||
import { Route, Switch, Link, Redirect } from "react-router-dom";
|
||||
|
||||
import { StyledEngineProvider } from "@material-ui/core/styles";
|
||||
import { Button } from "@material-ui/core";
|
||||
@@ -25,28 +25,22 @@ import TestView from "pages/Test";
|
||||
import Favicon from "assets/Favicon";
|
||||
import "analytics";
|
||||
|
||||
const AuthSetupGuidePage = lazy(
|
||||
() => import("pages/Auth/SetupGuide" /* webpackChunkName: "AuthSetupGuide" */)
|
||||
);
|
||||
// prettier-ignore
|
||||
const AuthSetupGuidePage = lazy(() => import("pages/Auth/SetupGuide" /* webpackChunkName: "AuthSetupGuide" */));
|
||||
// prettier-ignore
|
||||
const ImpersonatorAuthPage = lazy(() => import("./pages/Auth/ImpersonatorAuth" /* webpackChunkName: "ImpersonatorAuthPage" */));
|
||||
// prettier-ignore
|
||||
const JwtAuthPage = lazy(() => import("./pages/Auth/JwtAuth" /* webpackChunkName: "JwtAuthPage" */));
|
||||
|
||||
const HomePage = lazy(
|
||||
() => import("./pages/Home" /* webpackChunkName: "HomePage" */)
|
||||
);
|
||||
const TablePage = lazy(
|
||||
() => import("./pages/Table" /* webpackChunkName: "TablePage" */)
|
||||
);
|
||||
const ImpersonatorAuthPage = lazy(
|
||||
() =>
|
||||
import(
|
||||
"./pages/Auth/ImpersonatorAuth" /* webpackChunkName: "ImpersonatorAuthPage" */
|
||||
)
|
||||
);
|
||||
const JwtAuthPage = lazy(
|
||||
() => import("./pages/Auth/JwtAuth" /* webpackChunkName: "JwtAuthPage" */)
|
||||
);
|
||||
// const GridView = lazy(
|
||||
// () => import("./views/GridView" /* webpackChunkName: "GridView" */)
|
||||
// );
|
||||
// prettier-ignore
|
||||
const HomePage = lazy(() => import("./pages/Home" /* webpackChunkName: "HomePage" */));
|
||||
// prettier-ignore
|
||||
const TablePage = lazy(() => import("./pages/Table" /* webpackChunkName: "TablePage" */));
|
||||
|
||||
// prettier-ignore
|
||||
const ProjectSettingsPage = lazy(() => import("./pages/Settings/ProjectSettings" /* webpackChunkName: "ProjectSettingsPage" */));
|
||||
// prettier-ignore
|
||||
const UserSettingsPage = lazy(() => import("./pages/Settings/UserSettings" /* webpackChunkName: "UserSettingsPage" */));
|
||||
|
||||
export default function App() {
|
||||
return (
|
||||
@@ -85,7 +79,9 @@ export default function App() {
|
||||
path={routes.signOut}
|
||||
render={() => <SignOutView />}
|
||||
/>
|
||||
|
||||
<Route exact path={"/test"} render={() => <TestView />} />
|
||||
|
||||
<PrivateRoute
|
||||
exact
|
||||
path={[
|
||||
@@ -93,6 +89,9 @@ export default function App() {
|
||||
routes.tableWithId,
|
||||
routes.tableGroupWithId,
|
||||
routes.gridWithId,
|
||||
routes.settings,
|
||||
routes.projectSettings,
|
||||
routes.userSettings,
|
||||
]}
|
||||
render={() => (
|
||||
<RowyContextProvider>
|
||||
@@ -110,6 +109,24 @@ export default function App() {
|
||||
path={routes.tableGroupWithId}
|
||||
render={() => <TablePage />}
|
||||
/>
|
||||
|
||||
<PrivateRoute
|
||||
exact
|
||||
path={routes.settings}
|
||||
render={() => (
|
||||
<Redirect to={routes.userSettings} />
|
||||
)}
|
||||
/>
|
||||
<PrivateRoute
|
||||
exact
|
||||
path={routes.projectSettings}
|
||||
render={() => <ProjectSettingsPage />}
|
||||
/>
|
||||
<PrivateRoute
|
||||
exact
|
||||
path={routes.userSettings}
|
||||
render={() => <UserSettingsPage />}
|
||||
/>
|
||||
</Switch>
|
||||
</RowyContextProvider>
|
||||
)}
|
||||
|
||||
10
src/assets/icons/Firebase.tsx
Normal file
10
src/assets/icons/Firebase.tsx
Normal file
@@ -0,0 +1,10 @@
|
||||
import SvgIcon, { SvgIconProps } from "@material-ui/core/SvgIcon";
|
||||
import { mdiFirebase } from '@mdi/js';
|
||||
|
||||
export default function AddColumn(props: SvgIconProps) {
|
||||
return (
|
||||
<SvgIcon {...props}>
|
||||
<path d={mdiFirebase} />
|
||||
</SvgIcon>
|
||||
);
|
||||
}
|
||||
10
src/assets/icons/ProjectSettings.tsx
Normal file
10
src/assets/icons/ProjectSettings.tsx
Normal file
@@ -0,0 +1,10 @@
|
||||
import SvgIcon, { SvgIconProps } from "@material-ui/core/SvgIcon";
|
||||
import { mdiWrenchOutline } from "@mdi/js";
|
||||
|
||||
export default function ProjectSettings(props: SvgIconProps) {
|
||||
return (
|
||||
<SvgIcon {...props}>
|
||||
<path d={mdiWrenchOutline} />
|
||||
</SvgIcon>
|
||||
);
|
||||
}
|
||||
@@ -83,8 +83,8 @@ export default function AuthLayout({ children, loading }: IAuthLayoutProps) {
|
||||
<Div100vh className={classes.root} style={{ minHeight: "100rvh" }}>
|
||||
<Paper className={classes.paper}>
|
||||
<Logo />
|
||||
<Typography variant="overline" className={classes.projectName}>
|
||||
{process.env.REACT_APP_FIREBASE_PROJECT_ID}
|
||||
<Typography variant="body2" className={classes.projectName}>
|
||||
Project: {process.env.REACT_APP_FIREBASE_PROJECT_ID}
|
||||
</Typography>
|
||||
{children}
|
||||
|
||||
|
||||
@@ -1,239 +0,0 @@
|
||||
import React, { useState, useEffect } from "react";
|
||||
import { SearchIndex } from "algoliasearch/lite";
|
||||
import { FacetHit } from "@algolia/client-search";
|
||||
import useAlgolia from "use-algolia";
|
||||
import { useDebouncedCallback } from "use-debounce";
|
||||
|
||||
import { makeStyles, createStyles } from "@material-ui/styles";
|
||||
import {
|
||||
Grid,
|
||||
Typography,
|
||||
Button,
|
||||
TextField,
|
||||
InputAdornment,
|
||||
ListItemSecondaryAction,
|
||||
} from "@material-ui/core";
|
||||
import SearchIcon from "@material-ui/icons/Search";
|
||||
|
||||
import MultiSelect from "@antlerengineering/multiselect";
|
||||
|
||||
const useStyles = makeStyles((theme) =>
|
||||
createStyles({
|
||||
resetFilters: { marginRight: -theme.spacing(1) },
|
||||
|
||||
filterGrid: {
|
||||
marginTop: 0,
|
||||
marginBottom: theme.spacing(3),
|
||||
},
|
||||
|
||||
listItemText: { whiteSpace: "pre-line" },
|
||||
count: {
|
||||
position: "static",
|
||||
marginLeft: "auto",
|
||||
paddingLeft: theme.spacing(1.5),
|
||||
transform: "none",
|
||||
color: theme.palette.text.disabled,
|
||||
},
|
||||
})
|
||||
);
|
||||
|
||||
/**
|
||||
* Generates the string to dispatch as filters for the query
|
||||
* @param filterValues The user-selected filters
|
||||
* @param requiredFilters Filters not selected by the user
|
||||
*/
|
||||
const generateFiltersString = (
|
||||
filterValues: Record<string, string[]>,
|
||||
requiredFilters?: string
|
||||
) => {
|
||||
if (Object.keys(filterValues).length === 0) return null;
|
||||
|
||||
let filtersString = Object.entries(filterValues)
|
||||
.filter(([, values]) => values.length > 0)
|
||||
.map(
|
||||
([facet, values]) =>
|
||||
`(${values
|
||||
.map((value) => `${facet}:"${value.replace(/"/g, '\\"')}"`)
|
||||
.join(" OR ")})`
|
||||
)
|
||||
.join(" AND ");
|
||||
|
||||
if (requiredFilters) {
|
||||
if (filtersString)
|
||||
filtersString = requiredFilters + " AND " + filtersString;
|
||||
else filtersString = requiredFilters;
|
||||
}
|
||||
|
||||
return filtersString;
|
||||
};
|
||||
|
||||
export interface IAlgoliaFiltersProps {
|
||||
index: SearchIndex;
|
||||
request: ReturnType<typeof useAlgolia>[0]["request"];
|
||||
requestDispatch: ReturnType<typeof useAlgolia>[1];
|
||||
requiredFilters?: string;
|
||||
label: string;
|
||||
filters: {
|
||||
label: string;
|
||||
facet: string;
|
||||
labelTransformer?: (value: string) => string;
|
||||
}[];
|
||||
search?: boolean;
|
||||
}
|
||||
|
||||
export default function AlgoliaFilters({
|
||||
index,
|
||||
request,
|
||||
requestDispatch,
|
||||
requiredFilters,
|
||||
label,
|
||||
filters,
|
||||
search = true,
|
||||
}: IAlgoliaFiltersProps) {
|
||||
const classes = useStyles();
|
||||
|
||||
// Store filter values
|
||||
const [filterValues, setFilterValues] = useState<Record<string, string[]>>(
|
||||
{}
|
||||
);
|
||||
// Push filter values to dispatch
|
||||
useEffect(() => {
|
||||
const filtersString = generateFiltersString(filterValues, requiredFilters);
|
||||
if (filtersString === null) return;
|
||||
requestDispatch({ filters: filtersString });
|
||||
}, [filterValues]);
|
||||
|
||||
// Store facet values
|
||||
const [facetValues, setFacetValues] = useState<
|
||||
Record<string, readonly FacetHit[]>
|
||||
>({});
|
||||
// Get facet values
|
||||
useEffect(() => {
|
||||
if (!index) return;
|
||||
|
||||
filters.forEach((filter) => {
|
||||
const params = { ...request, maxFacetHits: 100 };
|
||||
// Ignore current user-selected value for these filters so all options
|
||||
// continue to show up
|
||||
params.filters =
|
||||
generateFiltersString(
|
||||
{ ...filterValues, [filter.facet]: [] },
|
||||
requiredFilters
|
||||
) ?? "";
|
||||
|
||||
index
|
||||
.searchForFacetValues(filter.facet, "", params)
|
||||
.then(({ facetHits }) =>
|
||||
setFacetValues((other) => ({ ...other, [filter.facet]: facetHits }))
|
||||
);
|
||||
});
|
||||
}, [filters, index, filterValues, requiredFilters]);
|
||||
|
||||
// Reset filters
|
||||
const handleResetFilters = () => {
|
||||
setFilterValues({});
|
||||
setQuery("");
|
||||
requestDispatch({ filters: requiredFilters ?? "", query: "" });
|
||||
};
|
||||
|
||||
// Store search query
|
||||
const [query, setQuery] = useState("");
|
||||
const [handleQueryChange] = useDebouncedCallback(
|
||||
(query: string) => requestDispatch({ query }),
|
||||
500
|
||||
);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Grid container spacing={1} alignItems="center">
|
||||
<Grid item xs>
|
||||
<Typography variant="overline">
|
||||
Filter{label ? " " + label : "s"}
|
||||
</Typography>
|
||||
</Grid>
|
||||
|
||||
<Grid item>
|
||||
<Button
|
||||
color="primary"
|
||||
onClick={handleResetFilters}
|
||||
className={classes.resetFilters}
|
||||
disabled={query === "" && Object.keys(filterValues).length === 0}
|
||||
>
|
||||
Reset Filters
|
||||
</Button>
|
||||
</Grid>
|
||||
</Grid>
|
||||
|
||||
<Grid
|
||||
container
|
||||
spacing={2}
|
||||
alignItems="center"
|
||||
className={classes.filterGrid}
|
||||
>
|
||||
{search && (
|
||||
<Grid item xs={12} md={4} lg={3}>
|
||||
<TextField
|
||||
value={query}
|
||||
onChange={(e) => {
|
||||
setQuery(e.target.value);
|
||||
handleQueryChange(e.target.value);
|
||||
}}
|
||||
variant="filled"
|
||||
type="search"
|
||||
InputProps={{
|
||||
startAdornment: (
|
||||
<InputAdornment position="start">
|
||||
<SearchIcon />
|
||||
</InputAdornment>
|
||||
),
|
||||
}}
|
||||
aria-label={`Search${label ? " " + label : ""}`}
|
||||
placeholder={`Search${label ? " " + label : ""}`}
|
||||
hiddenLabel
|
||||
fullWidth
|
||||
/>
|
||||
</Grid>
|
||||
)}
|
||||
|
||||
{filters.map((filter) => (
|
||||
<Grid item key={filter.facet} xs={12} sm={6} md={4} lg={3}>
|
||||
<MultiSelect
|
||||
label={filter.label}
|
||||
value={filterValues[filter.facet] ?? []}
|
||||
onChange={(value) =>
|
||||
setFilterValues((other) => ({
|
||||
...other,
|
||||
[filter.facet]: value,
|
||||
}))
|
||||
}
|
||||
options={
|
||||
facetValues[filter.facet]?.map((item) => ({
|
||||
value: item.value,
|
||||
label: filter.labelTransformer
|
||||
? filter.labelTransformer(item.value)
|
||||
: item.value,
|
||||
count: item.count,
|
||||
})) ?? []
|
||||
}
|
||||
itemRenderer={(option) => (
|
||||
<React.Fragment key={option.value}>
|
||||
{option.label}
|
||||
<ListItemSecondaryAction className={classes.count}>
|
||||
<Typography
|
||||
variant="body2"
|
||||
color="inherit"
|
||||
component="span"
|
||||
>
|
||||
{(option as any).count}
|
||||
</Typography>
|
||||
</ListItemSecondaryAction>
|
||||
</React.Fragment>
|
||||
)}
|
||||
searchable={facetValues[filter.facet]?.length > 10}
|
||||
/>
|
||||
</Grid>
|
||||
))}
|
||||
</Grid>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,249 +0,0 @@
|
||||
import React, { useState } from "react";
|
||||
import clsx from "clsx";
|
||||
|
||||
import {
|
||||
Card,
|
||||
Grid,
|
||||
Typography,
|
||||
Button,
|
||||
CardActions,
|
||||
CardContent,
|
||||
CardMedia,
|
||||
Divider,
|
||||
Tabs,
|
||||
Tab,
|
||||
} from "@material-ui/core";
|
||||
import { ButtonProps } from "@material-ui/core/Button";
|
||||
import GoIcon from "assets/icons/Go";
|
||||
|
||||
import useStyles from "./styles";
|
||||
export interface ICardProps {
|
||||
className?: string;
|
||||
style?: React.CSSProperties;
|
||||
|
||||
overline?: React.ReactNode;
|
||||
title?: React.ReactNode;
|
||||
imageSource?: string;
|
||||
imageShape?: "square" | "circle";
|
||||
imageClassName?: string;
|
||||
|
||||
tabs?: {
|
||||
label: string;
|
||||
content: React.ReactNode;
|
||||
disabled?: boolean;
|
||||
}[];
|
||||
bodyContent?: React.ReactNode;
|
||||
|
||||
primaryButton?: { label: string } & Partial<ButtonProps>;
|
||||
primaryLink?: {
|
||||
href?: string;
|
||||
target?: string;
|
||||
rel?: string;
|
||||
label: string;
|
||||
} & Partial<ButtonProps>;
|
||||
secondaryAction?: React.ReactNode;
|
||||
}
|
||||
|
||||
const a11yProps = (index: number) => ({
|
||||
id: `full-width-tab-${index}`,
|
||||
"aria-controls": `full-width-tabpanel-${index}`,
|
||||
});
|
||||
|
||||
export default function BasicCard({
|
||||
className,
|
||||
style,
|
||||
|
||||
overline,
|
||||
title,
|
||||
imageSource,
|
||||
imageShape = "square",
|
||||
imageClassName,
|
||||
|
||||
tabs,
|
||||
bodyContent,
|
||||
|
||||
primaryButton,
|
||||
primaryLink,
|
||||
secondaryAction,
|
||||
}: ICardProps) {
|
||||
const classes = useStyles();
|
||||
|
||||
const [tab, setTab] = useState(0);
|
||||
|
||||
const handleChangeTab = (event: React.ChangeEvent<{}>, newValue: number) =>
|
||||
setTab(newValue);
|
||||
|
||||
return (
|
||||
<Card className={clsx(classes.root, className)} style={style}>
|
||||
<Grid
|
||||
container
|
||||
direction="column"
|
||||
wrap="nowrap"
|
||||
className={classes.container}
|
||||
>
|
||||
<Grid item xs className={classes.cardContentContainer}>
|
||||
<CardContent className={clsx(classes.container, classes.cardContent)}>
|
||||
<Grid
|
||||
container
|
||||
direction="column"
|
||||
wrap="nowrap"
|
||||
className={classes.container}
|
||||
>
|
||||
{(overline || title || imageSource) && (
|
||||
<Grid item className={classes.headerContainer}>
|
||||
<Grid container spacing={3}>
|
||||
<Grid item xs>
|
||||
{overline && (
|
||||
<Typography
|
||||
variant="overline"
|
||||
className={classes.overline}
|
||||
>
|
||||
{overline}
|
||||
</Typography>
|
||||
)}
|
||||
{title && (
|
||||
<Typography variant="h5" className={classes.title}>
|
||||
{title}
|
||||
</Typography>
|
||||
)}
|
||||
</Grid>
|
||||
|
||||
{imageSource && (
|
||||
<Grid item>
|
||||
<CardMedia
|
||||
className={clsx(
|
||||
classes.image,
|
||||
imageShape === "circle" && classes.imageCircle,
|
||||
imageClassName
|
||||
)}
|
||||
image={imageSource}
|
||||
title={typeof title === "string" ? title : ""}
|
||||
/>
|
||||
</Grid>
|
||||
)}
|
||||
</Grid>
|
||||
</Grid>
|
||||
)}
|
||||
|
||||
{tabs && (
|
||||
<Grid item className={classes.tabsContainer}>
|
||||
<Tabs
|
||||
className={classes.tabs}
|
||||
value={tab}
|
||||
onChange={handleChangeTab}
|
||||
indicatorColor="primary"
|
||||
textColor="primary"
|
||||
variant="fullWidth"
|
||||
aria-label="full width tabs"
|
||||
>
|
||||
{tabs?.map((tab, index) => (
|
||||
<Tab
|
||||
key={`card-tab-${index}`}
|
||||
className={classes.tab}
|
||||
label={tab.label}
|
||||
disabled={tab.disabled}
|
||||
{...a11yProps(index)}
|
||||
/>
|
||||
))}
|
||||
</Tabs>
|
||||
<Divider className={clsx(classes.tabs, classes.tabDivider)} />
|
||||
</Grid>
|
||||
)}
|
||||
|
||||
{(tabs || bodyContent) && (
|
||||
<Grid item xs className={classes.contentContainer}>
|
||||
{tabs && (
|
||||
<div className={classes.tabSection}>
|
||||
{tabs[tab].content && Array.isArray(tabs[tab].content) ? (
|
||||
<Grid
|
||||
container
|
||||
direction="column"
|
||||
wrap="nowrap"
|
||||
justifyContent="space-between"
|
||||
spacing={3}
|
||||
className={classes.tabContentGrid}
|
||||
>
|
||||
{(tabs[tab].content as React.ReactNode[]).map(
|
||||
(element, index) => (
|
||||
<Grid item key={`tab-content-${index}`}>
|
||||
{element}
|
||||
</Grid>
|
||||
)
|
||||
)}
|
||||
</Grid>
|
||||
) : (
|
||||
tabs[tab].content
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{bodyContent && Array.isArray(bodyContent) ? (
|
||||
<Grid
|
||||
container
|
||||
direction="column"
|
||||
wrap="nowrap"
|
||||
justifyContent="space-between"
|
||||
className={classes.container}
|
||||
>
|
||||
{bodyContent.map((element, i) => (
|
||||
<Grid item key={i}>
|
||||
{element}
|
||||
</Grid>
|
||||
))}
|
||||
</Grid>
|
||||
) : (
|
||||
bodyContent
|
||||
)}
|
||||
</Grid>
|
||||
)}
|
||||
</Grid>
|
||||
</CardContent>
|
||||
</Grid>
|
||||
|
||||
{(primaryButton || primaryLink || secondaryAction) && (
|
||||
<Grid item>
|
||||
<Divider className={classes.divider} />
|
||||
<CardActions className={classes.cardActions}>
|
||||
<Grid item>
|
||||
{primaryButton && (
|
||||
<Button
|
||||
{...primaryButton}
|
||||
color={primaryButton.color || "primary"}
|
||||
disabled={!!primaryButton.disabled}
|
||||
endIcon={
|
||||
primaryButton.endIcon === undefined ? (
|
||||
<GoIcon />
|
||||
) : (
|
||||
primaryButton.endIcon
|
||||
)
|
||||
}
|
||||
>
|
||||
{primaryButton.label}
|
||||
</Button>
|
||||
)}
|
||||
{primaryLink && (
|
||||
<Button
|
||||
classes={{ label: classes.primaryLinkLabel }}
|
||||
{...(primaryLink as any)}
|
||||
color={primaryLink.color || "primary"}
|
||||
component="a"
|
||||
endIcon={
|
||||
primaryLink.endIcon === undefined ? (
|
||||
<GoIcon />
|
||||
) : (
|
||||
primaryLink.endIcon
|
||||
)
|
||||
}
|
||||
>
|
||||
{primaryLink.label}
|
||||
</Button>
|
||||
)}
|
||||
</Grid>
|
||||
{secondaryAction && <Grid item>{secondaryAction}</Grid>}
|
||||
</CardActions>
|
||||
</Grid>
|
||||
)}
|
||||
</Grid>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
@@ -1,58 +0,0 @@
|
||||
import { makeStyles, createStyles } from "@material-ui/styles";
|
||||
|
||||
const useStyles = makeStyles((theme) =>
|
||||
createStyles({
|
||||
root: { width: "100%", height: "100%" },
|
||||
container: { height: "100%" },
|
||||
|
||||
cardContentContainer: {
|
||||
"&:last-child": { paddingBottom: theme.spacing(3) },
|
||||
},
|
||||
cardContent: {
|
||||
"&:last-child": { paddingBottom: 0 },
|
||||
},
|
||||
|
||||
headerContainer: {},
|
||||
tabsContainer: { "$headerContainer + &": { marginTop: theme.spacing(2) } },
|
||||
contentContainer: {
|
||||
"$headerContainer + &": { marginTop: theme.spacing(2) },
|
||||
},
|
||||
|
||||
overline: {
|
||||
marginBottom: theme.spacing(3),
|
||||
color: theme.palette.text.disabled,
|
||||
wordBreak: "break-word",
|
||||
},
|
||||
title: {
|
||||
whiteSpace: "pre-line",
|
||||
wordBreak: "break-word",
|
||||
},
|
||||
image: {
|
||||
width: 80,
|
||||
height: 80,
|
||||
borderRadius: theme.shape.borderRadius,
|
||||
},
|
||||
imageCircle: { borderRadius: "50%" },
|
||||
|
||||
tabs: { margin: theme.spacing(0, -2) },
|
||||
tab: { minWidth: 0 },
|
||||
tabDivider: { marginTop: -1 },
|
||||
|
||||
tabSection: { paddingTop: theme.spacing(2), height: "100%" },
|
||||
tabContentGrid: { height: `calc(100% + ${theme.spacing(3)})` },
|
||||
|
||||
divider: {
|
||||
margin: theme.spacing(2),
|
||||
marginBottom: 0,
|
||||
},
|
||||
cardActions: {
|
||||
padding: theme.spacing(0.75, 1),
|
||||
display: "flex",
|
||||
justifyContent: "space-between",
|
||||
},
|
||||
|
||||
primaryLinkLabel: { whiteSpace: "nowrap" },
|
||||
})
|
||||
);
|
||||
|
||||
export default useStyles;
|
||||
@@ -1,81 +0,0 @@
|
||||
import { Grid as MuiGrid } from "@material-ui/core";
|
||||
import Card from "./Card";
|
||||
import useAlgolia from "use-algolia";
|
||||
import AlgoliaFilters from "./AlgoliaFilters";
|
||||
import _get from "lodash/get";
|
||||
const advisorsFilters = [
|
||||
{ label: "Type", facet: "type" },
|
||||
{ label: "Experience (Industry)", facet: "expertise" },
|
||||
{ label: "Location", facet: "location" },
|
||||
];
|
||||
|
||||
interface IGridProps {
|
||||
collection: string;
|
||||
filters: any[];
|
||||
}
|
||||
export const replacer = (data: any) => (m: string, key: string) => {
|
||||
const objKey = key.split(":")[0];
|
||||
const defaultValue = key.split(":")[1] || "";
|
||||
return _get(data, objKey, defaultValue);
|
||||
};
|
||||
|
||||
const CARD_CONFIG = {
|
||||
title: `{{firstName}} {{lastName}}`,
|
||||
image: "{{profilePhoto[0].downloadURL}}",
|
||||
body: "{{bio}}",
|
||||
};
|
||||
export default function Grid({ collection }: IGridProps) {
|
||||
const [algoliaState, requestDispatch, ,] = useAlgolia(
|
||||
process.env.REACT_APP_ALGOLIA_APP_ID!,
|
||||
process.env.REACT_APP_ALGOLIA_SEARCH_API_KEY!,
|
||||
collection,
|
||||
{ hitsPerPage: 100 }
|
||||
);
|
||||
|
||||
const isLoading = algoliaState.loading || !algoliaState.index;
|
||||
const noResults = algoliaState.hits.length === 0;
|
||||
const requiredFilters = ``;
|
||||
const isEmpty =
|
||||
noResults &&
|
||||
algoliaState.request?.query === undefined &&
|
||||
algoliaState.request?.filters === requiredFilters;
|
||||
return (
|
||||
<>
|
||||
{algoliaState.index && !isEmpty && (
|
||||
<AlgoliaFilters
|
||||
index={algoliaState.index}
|
||||
request={algoliaState.request}
|
||||
requestDispatch={requestDispatch}
|
||||
requiredFilters={requiredFilters}
|
||||
label={collection}
|
||||
filters={[]}
|
||||
search
|
||||
/>
|
||||
)}
|
||||
<MuiGrid container spacing={4}>
|
||||
{" "}
|
||||
{algoliaState.hits.map((hit) => {
|
||||
return (
|
||||
<MuiGrid key={hit.objectID} item lg={4} md={6} xs={12}>
|
||||
{" "}
|
||||
<Card
|
||||
bodyContent={CARD_CONFIG.body.replace(
|
||||
/\{\{(.*?)\}\}/g,
|
||||
replacer(hit)
|
||||
)}
|
||||
title={CARD_CONFIG.title.replace(
|
||||
/\{\{(.*?)\}\}/g,
|
||||
replacer(hit)
|
||||
)}
|
||||
imageSource={CARD_CONFIG.image.replace(
|
||||
/\{\{(.*?)\}\}/g,
|
||||
replacer(hit)
|
||||
)}
|
||||
/>{" "}
|
||||
</MuiGrid>
|
||||
);
|
||||
})}
|
||||
</MuiGrid>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -1,131 +0,0 @@
|
||||
import { makeStyles, createStyles } from "@material-ui/styles";
|
||||
import {
|
||||
useTheme,
|
||||
useMediaQuery,
|
||||
Drawer,
|
||||
DrawerProps,
|
||||
Grid,
|
||||
IconButton,
|
||||
List,
|
||||
MenuItem,
|
||||
ListItemIcon,
|
||||
ListItemText,
|
||||
} from "@material-ui/core";
|
||||
import CloseIcon from "assets/icons/Backburger";
|
||||
import AddIcon from "@material-ui/icons/Add";
|
||||
|
||||
import { APP_BAR_HEIGHT } from ".";
|
||||
import Logo from "assets/Logo";
|
||||
|
||||
import { useRowyContext } from "contexts/RowyContext";
|
||||
import useRouter from "hooks/useRouter";
|
||||
|
||||
export const NAV_DRAWER_WIDTH = 300;
|
||||
|
||||
const useStyles = makeStyles((theme) =>
|
||||
createStyles({
|
||||
paper: {
|
||||
width: NAV_DRAWER_WIDTH,
|
||||
overflowX: "hidden",
|
||||
backgroundColor: theme.palette.background.paper,
|
||||
},
|
||||
|
||||
logoRow: {
|
||||
height: APP_BAR_HEIGHT,
|
||||
marginTop: 0,
|
||||
marginBottom: theme.spacing(1),
|
||||
|
||||
padding: theme.spacing(0, 2),
|
||||
},
|
||||
logo: { marginLeft: theme.spacing(1.5) },
|
||||
|
||||
nav: { height: "100%" },
|
||||
list: {
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
flexWrap: "nowrap",
|
||||
|
||||
height: "100%",
|
||||
},
|
||||
|
||||
createTable: { marginTop: "auto" },
|
||||
})
|
||||
);
|
||||
|
||||
export interface INavDrawerProps extends DrawerProps {
|
||||
handleCreateTable: () => void;
|
||||
}
|
||||
|
||||
export default function NavDrawer({
|
||||
handleCreateTable,
|
||||
...props
|
||||
}: INavDrawerProps) {
|
||||
const classes = useStyles();
|
||||
const theme = useTheme();
|
||||
const isSm = useMediaQuery(theme.breakpoints.down("md"));
|
||||
|
||||
const { sections } = useRowyContext();
|
||||
const { location } = useRouter();
|
||||
const { hash } = location;
|
||||
|
||||
return (
|
||||
<Drawer
|
||||
open
|
||||
variant={isSm ? "temporary" : "persistent"}
|
||||
{...props}
|
||||
classes={{ paper: classes.paper }}
|
||||
>
|
||||
<Grid
|
||||
container
|
||||
spacing={1}
|
||||
wrap="nowrap"
|
||||
alignItems="center"
|
||||
className={classes.logoRow}
|
||||
>
|
||||
<Grid item>
|
||||
<IconButton
|
||||
aria-label="Close navigation drawer"
|
||||
onClick={props.onClose as any}
|
||||
edge="start"
|
||||
size="large"
|
||||
>
|
||||
<CloseIcon />
|
||||
</IconButton>
|
||||
</Grid>
|
||||
|
||||
<Grid item className={classes.logo}>
|
||||
<Logo />
|
||||
</Grid>
|
||||
</Grid>
|
||||
|
||||
<nav className={classes.nav}>
|
||||
<List className={classes.list}>
|
||||
{sections &&
|
||||
Object.keys(sections).map((section) => (
|
||||
<li key={section}>
|
||||
<MenuItem
|
||||
component="a"
|
||||
href={`#${section}`}
|
||||
selected={
|
||||
section === decodeURIComponent(hash.replace("#", ""))
|
||||
}
|
||||
onClick={isSm ? (props.onClose as any) : undefined}
|
||||
>
|
||||
<ListItemText primary={section} />
|
||||
</MenuItem>
|
||||
</li>
|
||||
))}
|
||||
|
||||
<li className={classes.createTable}>
|
||||
<MenuItem onClick={handleCreateTable}>
|
||||
<ListItemIcon>
|
||||
<AddIcon />
|
||||
</ListItemIcon>
|
||||
<ListItemText primary="Create Table" />
|
||||
</MenuItem>
|
||||
</li>
|
||||
</List>
|
||||
</nav>
|
||||
</Drawer>
|
||||
);
|
||||
}
|
||||
@@ -1,173 +0,0 @@
|
||||
import clsx from "clsx";
|
||||
|
||||
import { makeStyles, createStyles } from "@material-ui/styles";
|
||||
import {
|
||||
useScrollTrigger,
|
||||
Grid,
|
||||
AppBar,
|
||||
Toolbar,
|
||||
IconButton,
|
||||
} from "@material-ui/core";
|
||||
import MenuIcon from "@material-ui/icons/Menu";
|
||||
|
||||
import Logo from "assets/Logo";
|
||||
import NavDrawer, { NAV_DRAWER_WIDTH } from "./NavDrawer";
|
||||
import UserMenu from "components/Navigation/UserMenu";
|
||||
|
||||
export const APP_BAR_HEIGHT = 56;
|
||||
|
||||
const useStyles = makeStyles((theme) =>
|
||||
createStyles({
|
||||
open: {},
|
||||
|
||||
navDrawerContainer: {
|
||||
[theme.breakpoints.up("md")]: {
|
||||
width: 0,
|
||||
transition: theme.transitions.create("width", {
|
||||
easing: theme.transitions.easing.sharp,
|
||||
duration: theme.transitions.duration.leavingScreen,
|
||||
}),
|
||||
|
||||
"$open &": {
|
||||
width: NAV_DRAWER_WIDTH,
|
||||
transition: theme.transitions.create("width", {
|
||||
easing: theme.transitions.easing.easeOut,
|
||||
duration: theme.transitions.duration.enteringScreen,
|
||||
}),
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
appBar: {
|
||||
height: APP_BAR_HEIGHT,
|
||||
|
||||
[theme.breakpoints.down("md")]: { paddingRight: 0 },
|
||||
|
||||
[theme.breakpoints.up("md")]: {
|
||||
transition:
|
||||
theme.transitions.create("width", {
|
||||
easing: theme.transitions.easing.sharp,
|
||||
duration: theme.transitions.duration.leavingScreen,
|
||||
}) +
|
||||
", " +
|
||||
theme.transitions.create(["box-shadow", "background-color"]),
|
||||
"$open &": {
|
||||
width: `calc(100% - ${NAV_DRAWER_WIDTH}px)`,
|
||||
transition:
|
||||
theme.transitions.create("width", {
|
||||
easing: theme.transitions.easing.easeOut,
|
||||
duration: theme.transitions.duration.enteringScreen,
|
||||
}) +
|
||||
", " +
|
||||
theme.transitions.create(["box-shadow", "background-color"]),
|
||||
},
|
||||
},
|
||||
|
||||
// Elevation 8
|
||||
backgroundImage:
|
||||
"linear-gradient(rgba(255, 255, 255, 0.09), rgba(255, 255, 255, 0.09))",
|
||||
"&::before": {
|
||||
content: "''",
|
||||
display: "block",
|
||||
position: "absolute",
|
||||
top: 0,
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
left: 0,
|
||||
|
||||
backgroundColor: theme.palette.background.default,
|
||||
transition: theme.transitions.create("opacity"),
|
||||
},
|
||||
},
|
||||
appBarScrolled: {
|
||||
"&::before": {
|
||||
opacity: 0,
|
||||
},
|
||||
},
|
||||
toolbar: {
|
||||
height: APP_BAR_HEIGHT,
|
||||
minHeight: "auto",
|
||||
minWidth: 0,
|
||||
maxWidth: "none",
|
||||
padding: theme.spacing(0, 2),
|
||||
},
|
||||
|
||||
openButton: {
|
||||
opacity: 1,
|
||||
transition: theme.transitions.create("opacity"),
|
||||
"$open &": { opacity: 0 },
|
||||
},
|
||||
logo: {
|
||||
flex: 1,
|
||||
textAlign: "center",
|
||||
|
||||
opacity: 1,
|
||||
transition: theme.transitions.create("opacity"),
|
||||
"$open &": { opacity: 0 },
|
||||
},
|
||||
})
|
||||
);
|
||||
|
||||
export interface IHomeNavigationProps {
|
||||
children: React.ReactNode;
|
||||
|
||||
open: boolean;
|
||||
setOpen: React.Dispatch<React.SetStateAction<boolean>>;
|
||||
|
||||
handleCreateTable: () => void;
|
||||
}
|
||||
|
||||
export default function HomeNavigation({
|
||||
children,
|
||||
open,
|
||||
setOpen,
|
||||
handleCreateTable,
|
||||
}: IHomeNavigationProps) {
|
||||
const classes = useStyles();
|
||||
const trigger = useScrollTrigger({ disableHysteresis: true, threshold: 0 });
|
||||
|
||||
return (
|
||||
<Grid
|
||||
container
|
||||
wrap="nowrap"
|
||||
alignItems="flex-start"
|
||||
className={clsx(open && classes.open)}
|
||||
>
|
||||
<Grid item className={classes.navDrawerContainer}>
|
||||
<NavDrawer
|
||||
open={open}
|
||||
onClose={() => setOpen(false)}
|
||||
handleCreateTable={handleCreateTable}
|
||||
/>
|
||||
</Grid>
|
||||
|
||||
<Grid item xs>
|
||||
<AppBar
|
||||
color="inherit"
|
||||
elevation={trigger ? 1 : 0}
|
||||
className={clsx(classes.appBar, trigger && classes.appBarScrolled)}
|
||||
>
|
||||
<Toolbar className={classes.toolbar}>
|
||||
<IconButton
|
||||
aria-label="Open navigation drawer"
|
||||
onClick={() => setOpen(true)}
|
||||
edge="start"
|
||||
size="large"
|
||||
className={classes.openButton}
|
||||
>
|
||||
<MenuIcon />
|
||||
</IconButton>
|
||||
|
||||
<div className={classes.logo}>
|
||||
<Logo />
|
||||
</div>
|
||||
|
||||
<UserMenu />
|
||||
</Toolbar>
|
||||
</AppBar>
|
||||
|
||||
{children}
|
||||
</Grid>
|
||||
</Grid>
|
||||
);
|
||||
}
|
||||
@@ -13,6 +13,7 @@ import {
|
||||
} from "@material-ui/core";
|
||||
import HomeIcon from "@material-ui/icons/HomeOutlined";
|
||||
import SettingsIcon from "@material-ui/icons/SettingsOutlined";
|
||||
import ProjectSettingsIcon from "assets/icons/ProjectSettings";
|
||||
import CloseIcon from "assets/icons/Backburger";
|
||||
|
||||
import { APP_BAR_HEIGHT } from ".";
|
||||
@@ -26,19 +27,21 @@ export const NAV_DRAWER_WIDTH = 256;
|
||||
|
||||
export interface INavDrawerProps extends DrawerProps {
|
||||
currentSection?: string;
|
||||
currentTable: string;
|
||||
currentTable?: string;
|
||||
onClose: NonNullable<DrawerProps["onClose"]>;
|
||||
}
|
||||
|
||||
export default function NavDrawer({
|
||||
currentSection,
|
||||
currentTable,
|
||||
currentTable = "",
|
||||
...props
|
||||
}: INavDrawerProps) {
|
||||
const { sections } = useRowyContext();
|
||||
const { userClaims, sections } = useRowyContext();
|
||||
|
||||
const closeDrawer = (e: {}) => props.onClose(e, "escapeKeyDown");
|
||||
|
||||
return (
|
||||
<Drawer
|
||||
open
|
||||
{...props}
|
||||
sx={{ "& .MuiDrawer-paper": { minWidth: NAV_DRAWER_WIDTH } }}
|
||||
>
|
||||
@@ -62,7 +65,7 @@ export default function NavDrawer({
|
||||
<nav>
|
||||
<List disablePadding>
|
||||
<li>
|
||||
<MenuItem component={Link} to={routes.home}>
|
||||
<MenuItem component={Link} to={routes.home} onClick={closeDrawer}>
|
||||
<ListItemIcon>
|
||||
<HomeIcon />
|
||||
</ListItemIcon>
|
||||
@@ -70,13 +73,31 @@ export default function NavDrawer({
|
||||
</MenuItem>
|
||||
</li>
|
||||
<li>
|
||||
<MenuItem component={Link} to={routes.settings}>
|
||||
<MenuItem
|
||||
component={Link}
|
||||
to={routes.settings}
|
||||
onClick={closeDrawer}
|
||||
>
|
||||
<ListItemIcon>
|
||||
<SettingsIcon />
|
||||
</ListItemIcon>
|
||||
<ListItemText primary="Settings" />
|
||||
</MenuItem>
|
||||
</li>
|
||||
{userClaims?.roles?.includes("ADMIN") && (
|
||||
<li>
|
||||
<MenuItem
|
||||
component={Link}
|
||||
to={routes.projectSettings}
|
||||
onClick={closeDrawer}
|
||||
>
|
||||
<ListItemIcon>
|
||||
<ProjectSettingsIcon />
|
||||
</ListItemIcon>
|
||||
<ListItemText primary="Project Settings" />
|
||||
</MenuItem>
|
||||
</li>
|
||||
)}
|
||||
|
||||
<Divider variant="middle" sx={{ mt: 1, mb: 1 }} />
|
||||
|
||||
@@ -88,6 +109,7 @@ export default function NavDrawer({
|
||||
tables={tables}
|
||||
currentSection={currentSection}
|
||||
currentTable={currentTable}
|
||||
closeDrawer={closeDrawer}
|
||||
/>
|
||||
))}
|
||||
</List>
|
||||
|
||||
@@ -53,6 +53,8 @@ export interface INavDrawerItemProps {
|
||||
|
||||
currentSection?: string;
|
||||
currentTable: string;
|
||||
|
||||
closeDrawer: (e: {}) => void;
|
||||
}
|
||||
|
||||
export default function NavDrawerItem({
|
||||
@@ -60,6 +62,7 @@ export default function NavDrawerItem({
|
||||
tables,
|
||||
currentSection,
|
||||
currentTable,
|
||||
closeDrawer,
|
||||
}: INavDrawerItemProps) {
|
||||
const classes = useStyles();
|
||||
|
||||
@@ -111,6 +114,7 @@ export default function NavDrawerItem({
|
||||
"~2F"
|
||||
)}`
|
||||
}
|
||||
onClick={closeDrawer}
|
||||
>
|
||||
<ListItemText
|
||||
primary={table.name}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { useState, useRef } from "react";
|
||||
import { Link } from "react-router-dom";
|
||||
|
||||
import { makeStyles, createStyles } from "@material-ui/styles";
|
||||
import {
|
||||
IconButton,
|
||||
IconButtonProps,
|
||||
@@ -14,59 +13,19 @@ import {
|
||||
ListItemSecondaryAction,
|
||||
Link as MuiLink,
|
||||
Divider,
|
||||
Badge,
|
||||
} from "@material-ui/core";
|
||||
import AccountCircleIcon from "@material-ui/icons/AccountCircle";
|
||||
import ArrowRightIcon from "@material-ui/icons/ArrowRight";
|
||||
|
||||
import UpdateChecker, { useLatestUpdateState } from "./UpdateChecker";
|
||||
import { useAppContext } from "contexts/AppContext";
|
||||
import routes from "constants/routes";
|
||||
import { projectId } from "@src/firebase";
|
||||
import meta from "@root/package.json";
|
||||
|
||||
const useStyles = makeStyles((theme) =>
|
||||
createStyles({
|
||||
spacer: {
|
||||
width: 48,
|
||||
height: 48,
|
||||
},
|
||||
|
||||
iconButton: {},
|
||||
avatar: {
|
||||
"$iconButton &": {
|
||||
width: 24,
|
||||
height: 24,
|
||||
},
|
||||
},
|
||||
|
||||
paper: { minWidth: 160 },
|
||||
|
||||
// divider: { margin: theme.spacing(1, 2) },
|
||||
|
||||
secondaryAction: { pointerEvents: "none" },
|
||||
secondaryIcon: {
|
||||
display: "block",
|
||||
color: theme.palette.action.active,
|
||||
},
|
||||
|
||||
subMenu: { marginTop: theme.spacing(-0.5) },
|
||||
|
||||
version: {
|
||||
userSelect: "none",
|
||||
color: theme.palette.text.disabled,
|
||||
margin: 0,
|
||||
},
|
||||
})
|
||||
);
|
||||
|
||||
export default function UserMenu(props: IconButtonProps) {
|
||||
const classes = useStyles();
|
||||
|
||||
const anchorEl = useRef<HTMLButtonElement>(null);
|
||||
const [open, setOpen] = useState(false);
|
||||
const [themeSubMenu, setThemeSubMenu] = useState<EventTarget | null>(null);
|
||||
const [latestUpdate] = useLatestUpdateState<null | Record<string, any>>();
|
||||
|
||||
const {
|
||||
currentUser,
|
||||
@@ -77,14 +36,14 @@ export default function UserMenu(props: IconButtonProps) {
|
||||
setThemeOverridden,
|
||||
} = useAppContext();
|
||||
if (!currentUser || !userDoc || !userDoc?.state?.doc)
|
||||
return <div className={classes.spacer} />;
|
||||
return <div style={{ width: 48, height: 48 }} />;
|
||||
|
||||
const displayName = userDoc?.state?.doc?.user?.displayName;
|
||||
const avatarUrl = userDoc?.state?.doc?.user?.photoURL;
|
||||
const email = userDoc?.state?.doc?.user?.email;
|
||||
|
||||
const avatar = avatarUrl ? (
|
||||
<Avatar src={avatarUrl} className={classes.avatar} />
|
||||
<Avatar src={avatarUrl} />
|
||||
) : (
|
||||
<AccountCircleIcon color="secondary" />
|
||||
);
|
||||
@@ -122,15 +81,9 @@ export default function UserMenu(props: IconButtonProps) {
|
||||
{...props}
|
||||
ref={anchorEl}
|
||||
onClick={() => setOpen(true)}
|
||||
className={classes.iconButton}
|
||||
sx={{ "& .MuiAvatar-root": { width: 24, height: 24 } }}
|
||||
>
|
||||
{latestUpdate?.tag_name > "v" + meta.version ? (
|
||||
<Badge color="primary" overlap="circular" variant="dot">
|
||||
{avatar}
|
||||
</Badge>
|
||||
) : (
|
||||
avatar
|
||||
)}
|
||||
{avatar}
|
||||
</IconButton>
|
||||
|
||||
<Menu
|
||||
@@ -141,19 +94,19 @@ export default function UserMenu(props: IconButtonProps) {
|
||||
transformOrigin={{ vertical: "top", horizontal: "right" }}
|
||||
open={open}
|
||||
onClose={() => setOpen(false)}
|
||||
classes={{ paper: classes.paper }}
|
||||
sx={{ "& .MuiPaper-root": { minWidth: 160 } }}
|
||||
>
|
||||
<ListItem>
|
||||
<ListItem style={{ cursor: "default" }}>
|
||||
<ListItemAvatar>{avatar}</ListItemAvatar>
|
||||
<ListItemText primary={displayName} secondary={email} />
|
||||
</ListItem>
|
||||
|
||||
<Divider variant="middle" />
|
||||
<Divider variant="middle" sx={{ mt: 0.5, mb: 0.5 }} />
|
||||
|
||||
<MenuItem onClick={(e) => setThemeSubMenu(e.target)}>
|
||||
Theme
|
||||
<ListItemSecondaryAction className={classes.secondaryAction}>
|
||||
<ArrowRightIcon className={classes.secondaryIcon} />
|
||||
<ListItemSecondaryAction style={{ pointerEvents: "none" }}>
|
||||
<ArrowRightIcon style={{ display: "block" }} />
|
||||
</ListItemSecondaryAction>
|
||||
</MenuItem>
|
||||
|
||||
@@ -165,7 +118,7 @@ export default function UserMenu(props: IconButtonProps) {
|
||||
transformOrigin={{ vertical: "top", horizontal: "right" }}
|
||||
open
|
||||
onClose={() => setThemeSubMenu(null)}
|
||||
classes={{ paper: classes.subMenu }}
|
||||
sx={{ "& .MuiPaper-root": { mt: -0.5 } }}
|
||||
>
|
||||
<MenuItem
|
||||
onClick={() => changeTheme("system")}
|
||||
@@ -189,7 +142,7 @@ export default function UserMenu(props: IconButtonProps) {
|
||||
)}
|
||||
|
||||
<MenuItem component={Link} to={routes.userSettings} disabled>
|
||||
User settings
|
||||
Settings
|
||||
</MenuItem>
|
||||
|
||||
<Divider variant="middle" />
|
||||
@@ -200,12 +153,6 @@ export default function UserMenu(props: IconButtonProps) {
|
||||
|
||||
<Divider variant="middle" />
|
||||
|
||||
<MenuItem component={Link} to={routes.projectSettings} disabled>
|
||||
Project settings
|
||||
</MenuItem>
|
||||
|
||||
<UpdateChecker />
|
||||
|
||||
<ListItem>
|
||||
<ListItemText
|
||||
primary={
|
||||
@@ -237,7 +184,11 @@ export default function UserMenu(props: IconButtonProps) {
|
||||
}
|
||||
primaryTypographyProps={{ variant: "caption", color: "inherit" }}
|
||||
secondaryTypographyProps={{ variant: "caption", color: "inherit" }}
|
||||
className={classes.version}
|
||||
sx={{
|
||||
userSelect: "none",
|
||||
color: "text.disabled",
|
||||
margin: 0,
|
||||
}}
|
||||
/>
|
||||
</ListItem>
|
||||
</Menu>
|
||||
|
||||
@@ -1,72 +1,29 @@
|
||||
import React, { useState, useEffect } from "react";
|
||||
import _find from "lodash/find";
|
||||
import { ReactNode, useState } from "react";
|
||||
|
||||
import { makeStyles, createStyles } from "@material-ui/styles";
|
||||
import { AppBar, Toolbar, IconButton } from "@material-ui/core";
|
||||
import {
|
||||
useScrollTrigger,
|
||||
AppBar,
|
||||
Toolbar,
|
||||
IconButton,
|
||||
Box,
|
||||
Typography,
|
||||
} from "@material-ui/core";
|
||||
import MenuIcon from "@material-ui/icons/Menu";
|
||||
|
||||
import Breadcrumbs from "./Breadcrumbs";
|
||||
import NavDrawer from "./NavDrawer";
|
||||
|
||||
// import { DRAWER_COLLAPSED_WIDTH } from "components/SideDrawer";
|
||||
import { useRowyContext } from "contexts/RowyContext";
|
||||
import UserMenu from "./UserMenu";
|
||||
|
||||
import { name } from "@root/package.json";
|
||||
import { projectId } from "@src/firebase";
|
||||
|
||||
export const APP_BAR_HEIGHT = 56;
|
||||
|
||||
const useStyles = makeStyles((theme) =>
|
||||
createStyles({
|
||||
appBar: {
|
||||
// paddingRight: DRAWER_COLLAPSED_WIDTH,
|
||||
height: APP_BAR_HEIGHT,
|
||||
[theme.breakpoints.down("md")]: { paddingRight: 0 },
|
||||
|
||||
backgroundColor: theme.palette.background.default,
|
||||
},
|
||||
|
||||
toolbar: {
|
||||
height: APP_BAR_HEIGHT,
|
||||
minHeight: "auto",
|
||||
minWidth: 0,
|
||||
maxWidth: "none",
|
||||
padding: theme.spacing(0, 2),
|
||||
},
|
||||
|
||||
breadcrumbs: { flex: 1 },
|
||||
})
|
||||
);
|
||||
|
||||
export default function Navigation({
|
||||
children,
|
||||
tableCollection,
|
||||
}: React.PropsWithChildren<{ tableCollection: string }>) {
|
||||
const { tables } = useRowyContext();
|
||||
const classes = useStyles();
|
||||
export interface INavigationProps {
|
||||
children: ReactNode;
|
||||
title?: ReactNode;
|
||||
}
|
||||
|
||||
export default function Navigation({ children, title }: INavigationProps) {
|
||||
const [open, setOpen] = useState(false);
|
||||
useEffect(() => {
|
||||
setOpen(false);
|
||||
}, [tableCollection]);
|
||||
|
||||
// Find the matching section for the current route
|
||||
const currentSection = _find(tables, [
|
||||
"collection",
|
||||
tableCollection?.split("/")[0],
|
||||
])?.section;
|
||||
const currentTable = tableCollection?.split("/")[0];
|
||||
|
||||
useEffect(() => {
|
||||
const tableName =
|
||||
_find(tables, ["collection", currentTable])?.name || currentTable;
|
||||
document.title = `${tableName} | ${projectId} | ${name}`;
|
||||
|
||||
return () => {
|
||||
document.title = `${projectId} | ${name}`;
|
||||
};
|
||||
}, [currentTable]);
|
||||
const trigger = useScrollTrigger({ disableHysteresis: true, threshold: 0 });
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -74,9 +31,44 @@ export default function Navigation({
|
||||
position="sticky"
|
||||
color="inherit"
|
||||
elevation={0}
|
||||
className={classes.appBar}
|
||||
className={trigger ? "scrolled" : ""}
|
||||
sx={{
|
||||
height: APP_BAR_HEIGHT, // Elevation 8
|
||||
backgroundImage:
|
||||
"linear-gradient(rgba(255, 255, 255, 0.09), rgba(255, 255, 255, 0.09))",
|
||||
|
||||
"&::before": {
|
||||
content: "''",
|
||||
display: "block",
|
||||
position: "absolute",
|
||||
top: 0,
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
left: 0,
|
||||
|
||||
bgcolor: "background.default",
|
||||
transition: (theme) => theme.transitions.create("opacity"),
|
||||
},
|
||||
|
||||
"&:hover, &.scrolled": {
|
||||
boxShadow: 1,
|
||||
"&::before": { opacity: 0 },
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Toolbar className={classes.toolbar}>
|
||||
<Toolbar
|
||||
sx={{
|
||||
height: APP_BAR_HEIGHT,
|
||||
minWidth: 0,
|
||||
maxWidth: "none",
|
||||
"&&": {
|
||||
minHeight: APP_BAR_HEIGHT,
|
||||
p: 0,
|
||||
pl: 2,
|
||||
pr: 2,
|
||||
},
|
||||
}}
|
||||
>
|
||||
<IconButton
|
||||
aria-label="Open navigation drawer"
|
||||
onClick={() => setOpen(true)}
|
||||
@@ -86,19 +78,22 @@ export default function Navigation({
|
||||
<MenuIcon />
|
||||
</IconButton>
|
||||
|
||||
<Breadcrumbs className={classes.breadcrumbs} />
|
||||
<Box sx={{ flex: 1, ml: 20 / 8, mr: 20 / 8, userSelect: "none" }}>
|
||||
{typeof title === "string" ? (
|
||||
<Typography variant="h6" component="h1">
|
||||
{title}
|
||||
</Typography>
|
||||
) : (
|
||||
title
|
||||
)}
|
||||
</Box>
|
||||
|
||||
<UserMenu />
|
||||
{/* <Notifications /> */}
|
||||
</Toolbar>
|
||||
</AppBar>
|
||||
|
||||
<NavDrawer
|
||||
currentSection={currentSection}
|
||||
currentTable={currentTable}
|
||||
open={open}
|
||||
onClose={() => setOpen(false)}
|
||||
/>
|
||||
<NavDrawer open={open} onClose={() => setOpen(false)} />
|
||||
|
||||
{children}
|
||||
</>
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { makeStyles, createStyles } from "@material-ui/styles";
|
||||
import { Stack, Button } from "@material-ui/core";
|
||||
|
||||
import { isCollectionGroup } from "utils/fns";
|
||||
@@ -20,35 +19,6 @@ import { FieldType } from "constants/fields";
|
||||
|
||||
export const TABLE_HEADER_HEIGHT = 44;
|
||||
|
||||
const useStyles = makeStyles((theme) =>
|
||||
createStyles({
|
||||
root: {
|
||||
width: "100%",
|
||||
margin: 0,
|
||||
padding: theme.spacing(0, 2, 1.5, 1),
|
||||
height: TABLE_HEADER_HEIGHT,
|
||||
|
||||
overflowX: "auto",
|
||||
whiteSpace: "nowrap",
|
||||
|
||||
userSelect: "none",
|
||||
|
||||
[theme.breakpoints.down("md")]: {
|
||||
width: "100%",
|
||||
paddingRight: theme.spacing(1),
|
||||
},
|
||||
|
||||
"& > *": { paddingTop: "0 !important" },
|
||||
},
|
||||
|
||||
spacer: { width: theme.spacing(2) },
|
||||
midSpacer: { minWidth: theme.spacing(8) },
|
||||
})
|
||||
);
|
||||
|
||||
/**
|
||||
* TODO: Make this properly mobile responsive, not just horizontally scrolling
|
||||
*/
|
||||
export default function TableHeader() {
|
||||
const { currentUser } = useAppContext();
|
||||
const { tableActions, tableState, userClaims } = useRowyContext();
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import React, { useEffect, useState, useContext } from "react";
|
||||
import { auth, db } from "../firebase";
|
||||
import { projectId, auth, db } from "@src/firebase";
|
||||
import firebase from "firebase/app";
|
||||
import useDoc from "hooks/useDoc";
|
||||
import createPersistedState from "use-persisted-state";
|
||||
@@ -13,6 +13,7 @@ import {
|
||||
import Themes from "Themes";
|
||||
|
||||
import ErrorBoundary from "components/ErrorBoundary";
|
||||
import { name } from "@root/package.json";
|
||||
|
||||
const useThemeState = createPersistedState("__ROWY__THEME");
|
||||
const useThemeOverriddenState = createPersistedState(
|
||||
@@ -50,6 +51,10 @@ export const AppProvider: React.FC = ({ children }) => {
|
||||
});
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
document.title = `${projectId} | ${name}`;
|
||||
}, []);
|
||||
|
||||
// Store matching userDoc
|
||||
const [userDoc, dispatchUserDoc] = useDoc({});
|
||||
// Get userDoc
|
||||
|
||||
@@ -1,32 +1,38 @@
|
||||
import { Typography, Button } from "@material-ui/core";
|
||||
import { Button } from "@material-ui/core";
|
||||
import OpenInNewIcon from "@material-ui/icons/OpenInNew";
|
||||
|
||||
import AuthLayout from "components/Auth/AuthLayout";
|
||||
import EmptyState from "components/EmptyState";
|
||||
import FirebaseIcon from "assets/icons/Firebase";
|
||||
|
||||
import WIKI_LINKS from "constants/wikiLinks";
|
||||
import { name } from "@root/package.json";
|
||||
|
||||
export default function AuthSetupGuide() {
|
||||
return (
|
||||
<AuthLayout>
|
||||
<div>
|
||||
<Typography variant="h6" component="h2" gutterBottom>
|
||||
Firebase Authentication Not Set Up
|
||||
</Typography>
|
||||
|
||||
<Typography variant="body1" color="textSecondary">
|
||||
Firebase Authentication must be enabled to sign in to Rowy.
|
||||
</Typography>
|
||||
</div>
|
||||
|
||||
<Button
|
||||
variant="contained"
|
||||
endIcon={<OpenInNewIcon />}
|
||||
component="a"
|
||||
href={WIKI_LINKS.setUpAuth}
|
||||
target="_blank"
|
||||
rel="noopener"
|
||||
>
|
||||
Set-Up Guide
|
||||
</Button>
|
||||
<EmptyState
|
||||
Icon={FirebaseIcon}
|
||||
message="Set Up Firebase Authentication"
|
||||
description={
|
||||
<>
|
||||
<span>
|
||||
To sign in to {name}, set up Firebase Authentication in the
|
||||
Firebase Console.
|
||||
</span>
|
||||
<Button
|
||||
variant="contained"
|
||||
color="primary"
|
||||
endIcon={<OpenInNewIcon />}
|
||||
href={WIKI_LINKS.setUpAuth}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
Setup Guide
|
||||
</Button>
|
||||
</>
|
||||
}
|
||||
/>
|
||||
</AuthLayout>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,38 +0,0 @@
|
||||
import queryString from "query-string";
|
||||
|
||||
import { Hidden } from "@material-ui/core";
|
||||
|
||||
import Navigation from "components/Navigation";
|
||||
import Grid from "components/Grid";
|
||||
import SideDrawer from "components/SideDrawer";
|
||||
|
||||
import { RowyFilter } from "hooks/useRowy";
|
||||
import useRouter from "hooks/useRouter";
|
||||
|
||||
export default function GridPage() {
|
||||
const router = useRouter();
|
||||
const tableCollection = decodeURIComponent(router.match.params.id);
|
||||
|
||||
let filters: RowyFilter[] = [];
|
||||
const parsed = queryString.parse(router.location.search);
|
||||
if (typeof parsed.filters === "string") {
|
||||
// decoded
|
||||
//[{"key":"cohort","operator":"==","value":"AMS1"}]
|
||||
filters = JSON.parse(parsed.filters);
|
||||
//TODO: json schema validator
|
||||
}
|
||||
|
||||
return (
|
||||
<Navigation tableCollection={tableCollection}>
|
||||
<Grid
|
||||
key={tableCollection}
|
||||
collection={tableCollection}
|
||||
filters={filters}
|
||||
/>
|
||||
|
||||
<Hidden smDown>
|
||||
<SideDrawer />
|
||||
</Hidden>
|
||||
</Navigation>
|
||||
);
|
||||
}
|
||||
@@ -19,7 +19,8 @@ import EditIcon from "@material-ui/icons/Edit";
|
||||
import Favorite from "@material-ui/icons/Favorite";
|
||||
import FavoriteBorder from "@material-ui/icons/FavoriteBorder";
|
||||
|
||||
import HomeNavigation from "components/HomeNavigation";
|
||||
import Navigation from "components/Navigation";
|
||||
import Logo from "assets/Logo";
|
||||
import StyledCard from "components/StyledCard";
|
||||
|
||||
import routes from "constants/routes";
|
||||
@@ -132,7 +133,6 @@ export default function HomePage() {
|
||||
mode: TableSettingsDialogModes.create,
|
||||
data: null,
|
||||
});
|
||||
const [open, setOpen] = useState(false);
|
||||
const [openProjectSettings, setOpenProjectSettings] = useState(false);
|
||||
const [openBuilderInstaller, setOpenBuilderInstaller] = useState(false);
|
||||
|
||||
@@ -178,15 +178,7 @@ export default function HomePage() {
|
||||
const TableCard = ({ table }) => {
|
||||
const checked = Boolean(_find(favs, table));
|
||||
return (
|
||||
<Grid
|
||||
key={table.name}
|
||||
item
|
||||
xs={12}
|
||||
sm={6}
|
||||
md={open ? 6 : 4}
|
||||
lg={4}
|
||||
xl={3}
|
||||
>
|
||||
<Grid key={table.name} item xs={12} sm={6} md={4} lg={4} xl={3}>
|
||||
<StyledCard
|
||||
className={classes.card}
|
||||
overline={table.section}
|
||||
@@ -236,10 +228,12 @@ export default function HomePage() {
|
||||
};
|
||||
|
||||
return (
|
||||
<HomeNavigation
|
||||
open={open}
|
||||
setOpen={setOpen}
|
||||
handleCreateTable={handleCreateTable}
|
||||
<Navigation
|
||||
title={
|
||||
<div style={{ textAlign: "center" }}>
|
||||
<Logo />
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<main className={classes.root}>
|
||||
{sections && Object.keys(sections).length > 0 ? (
|
||||
@@ -353,6 +347,6 @@ export default function HomePage() {
|
||||
{openBuilderInstaller && (
|
||||
<BuilderInstaller handleClose={() => setOpenBuilderInstaller(false)} />
|
||||
)}
|
||||
</HomeNavigation>
|
||||
</Navigation>
|
||||
);
|
||||
}
|
||||
|
||||
15
src/pages/Settings/ProjectSettings.tsx
Normal file
15
src/pages/Settings/ProjectSettings.tsx
Normal file
@@ -0,0 +1,15 @@
|
||||
import { Container, Typography } from "@material-ui/core";
|
||||
|
||||
import Navigation from "components/Navigation";
|
||||
|
||||
export default function ProjectSettings() {
|
||||
return (
|
||||
<Navigation title="Project Settings">
|
||||
<Container style={{ height: "200vh" }}>
|
||||
<Typography component="h2" variant="h4">
|
||||
Project Settings
|
||||
</Typography>
|
||||
</Container>
|
||||
</Navigation>
|
||||
);
|
||||
}
|
||||
15
src/pages/Settings/UserSettings.tsx
Normal file
15
src/pages/Settings/UserSettings.tsx
Normal file
@@ -0,0 +1,15 @@
|
||||
import { Container, Typography } from "@material-ui/core";
|
||||
|
||||
import Navigation from "components/Navigation";
|
||||
|
||||
export default function UserSettings() {
|
||||
return (
|
||||
<Navigation title="Settings">
|
||||
<Container>
|
||||
<Typography component="h2" variant="h4">
|
||||
Settings
|
||||
</Typography>
|
||||
</Container>
|
||||
</Navigation>
|
||||
);
|
||||
}
|
||||
@@ -1,10 +1,12 @@
|
||||
import { useEffect } from "react";
|
||||
import queryString from "query-string";
|
||||
import _isEmpty from "lodash/isEmpty";
|
||||
import _find from "lodash/find";
|
||||
|
||||
import { Hidden } from "@material-ui/core";
|
||||
|
||||
import Navigation from "components/Navigation";
|
||||
import Breadcrumbs from "components/Navigation/Breadcrumbs";
|
||||
import Table from "components/Table";
|
||||
import SideDrawer from "components/SideDrawer";
|
||||
import TableHeaderSkeleton from "components/Table/Skeleton/TableHeaderSkeleton";
|
||||
@@ -18,13 +20,33 @@ import useRouter from "hooks/useRouter";
|
||||
import { DocActions } from "hooks/useDoc";
|
||||
import ActionParamsProvider from "components/fields/Action/FormDialog/Provider";
|
||||
|
||||
import { name } from "@root/package.json";
|
||||
import { projectId } from "@src/firebase";
|
||||
|
||||
export default function TablePage() {
|
||||
const router = useRouter();
|
||||
const tableCollection = decodeURIComponent(router.match.params.id);
|
||||
|
||||
const { tableState, tableActions, sideDrawerRef } = useRowyContext();
|
||||
const { tableState, tableActions, sideDrawerRef, tables } = useRowyContext();
|
||||
const { userDoc } = useAppContext();
|
||||
|
||||
// Find the matching section for the current route
|
||||
const currentSection = _find(tables, [
|
||||
"collection",
|
||||
tableCollection?.split("/")[0],
|
||||
])?.section;
|
||||
const currentTable = tableCollection?.split("/")[0];
|
||||
|
||||
useEffect(() => {
|
||||
const tableName =
|
||||
_find(tables, ["collection", currentTable])?.name || currentTable;
|
||||
document.title = `${tableName} | ${projectId} | ${name}`;
|
||||
|
||||
return () => {
|
||||
document.title = `${projectId} | ${name}`;
|
||||
};
|
||||
}, [currentTable]);
|
||||
|
||||
let filters: RowyFilter[] = [];
|
||||
const parsed = queryString.parse(router.location.search);
|
||||
if (typeof parsed.filters === "string") {
|
||||
@@ -54,7 +76,7 @@ export default function TablePage() {
|
||||
if (!tableState) return null;
|
||||
|
||||
return (
|
||||
<Navigation tableCollection={tableCollection}>
|
||||
<Navigation title={<Breadcrumbs />}>
|
||||
<ActionParamsProvider>
|
||||
{tableState.loadingColumns && (
|
||||
<>
|
||||
|
||||
@@ -75,7 +75,7 @@ export default function TestView() {
|
||||
const handleTabChange = (_, newTab) => setTab(newTab);
|
||||
|
||||
return (
|
||||
<Navigation tableCollection="">
|
||||
<Navigation title="Theme Test">
|
||||
<Container style={{ margin: "24px 0 200px" }}>
|
||||
<Stack spacing={8}>
|
||||
<Table stickyHeader>
|
||||
|
||||
@@ -2504,7 +2504,7 @@
|
||||
prop-types "^15.7.2"
|
||||
react-is "^17.0.2"
|
||||
|
||||
"@mdi/js@^5.8.55", "@mdi/js@^5.9.55":
|
||||
"@mdi/js@^5.9.55":
|
||||
version "5.9.55"
|
||||
resolved "https://registry.yarnpkg.com/@mdi/js/-/js-5.9.55.tgz#8f5bc4d924c23f30dab20545ddc768e778bbc882"
|
||||
integrity sha512-BbeHMgeK2/vjdJIRnx12wvQ6s8xAYfvMmEAVsUx9b+7GiQGQ9Za8jpwp17dMKr9CgKRvemlAM4S7S3QOtEbp4A==
|
||||
|
||||
Reference in New Issue
Block a user