diff --git a/src/common/CustomProgressBar.tsx b/src/common/CustomProgressBar.tsx
new file mode 100644
index 0000000..037259d
--- /dev/null
+++ b/src/common/CustomProgressBar.tsx
@@ -0,0 +1,41 @@
+import { Box, colors } from "@mui/material";
+import { makeStyles } from "@mui/styles";
+
+interface Props {
+ colour: string;
+ completed: number;
+}
+
+const useStyles = makeStyles(() => ({
+ container: {
+ height: 20,
+ width: "100%",
+ backgroundColor: "#e0e0de",
+ borderRadius: 50
+ },
+ filler: {
+ height: "100%",
+ // width: `${completed}%`,
+ // backgroundColor: bgcolor,
+ borderRadius: "inherit",
+ textAlign: "right"
+ },
+ label: {
+ padding: 5,
+ color: colors.grey[900],
+ fontWeight: "bold"
+ }
+ })
+);
+
+export default function CustomProgressBar(props: Props) {
+ const { colour, completed } = props;
+ const classes = useStyles();
+ return (
+
+
+ {completed}%
+
+
+ );
+}
diff --git a/src/common/ImageViewer.tsx b/src/common/ImageViewer.tsx
new file mode 100644
index 0000000..4759185
--- /dev/null
+++ b/src/common/ImageViewer.tsx
@@ -0,0 +1,52 @@
+import { Box } from "@mui/material";
+import { pond } from "protobuf-ts/pond";
+import { useFileControllerAPI } from "providers";
+import React, { useEffect, useState } from "react";
+
+interface Props {
+ image: pond.FileReference;
+ height?: number | string;
+ width?: number | string;
+}
+
+export default function ImageViewer(props: Props) {
+ const { image, height, width } = props;
+ const fileController = useFileControllerAPI();
+ // const [contentType, setContentType] = useState("")
+ const [imageData, setImageData] = useState("");
+
+ useEffect(() => {
+ //check local storage for the image
+ let imageURL = localStorage.getItem(image.uuid);
+ if (imageURL !== null) {
+ setImageData(imageURL);
+ } else {
+ //if its not there retrieve it
+ fileController
+ .retrieveFile(image.spacesPath)
+ .then(resp => {
+ let url = URL.createObjectURL(resp.data);
+ setImageData(url);
+ let reader = new FileReader();
+ reader.addEventListener("load", () => {
+ //store the file url in base64 in local storage so it doesn't need to be loaded everytime
+ localStorage.setItem(image.uuid, reader.result as string);
+ });
+ reader.readAsDataURL(resp.data);
+ })
+ .catch(err => {
+ //console.log("There were errors retrieving the file");
+ });
+ }
+ }, [image, fileController]);
+
+ //get the file from digital ocean by using our backend as a proxy
+
+ return (
+
+
+
+
+
+ );
+}
diff --git a/src/contract/constractActions.tsx b/src/contract/constractActions.tsx
new file mode 100644
index 0000000..3c1e036
--- /dev/null
+++ b/src/contract/constractActions.tsx
@@ -0,0 +1,107 @@
+import { Box, Card, Typography, useTheme } from "@mui/material";
+import GrainDescriber from "grain/GrainDescriber";
+import { Contract } from "models/Contract";
+import { Label, Pie, PieChart, ResponsiveContainer, Text } from "recharts";
+
+interface Props {
+ contract: Contract;
+}
+
+export default function ContractVisualizer(props: Props) {
+ const { contract } = props;
+ const theme = useTheme();
+
+ return (
+
+
+
+ {GrainDescriber(contract.grain()).name}
+
+
+ {contract.deliveredInPreferredUnit().toFixed(2)}/
+ {contract.sizeInPreferredUnit().toFixed(2)} {contract.unit}
+
+
+
+
+ {/*
+
+
+ );
+}
diff --git a/src/contract/contractActions.tsx b/src/contract/contractActions.tsx
new file mode 100644
index 0000000..7d13123
--- /dev/null
+++ b/src/contract/contractActions.tsx
@@ -0,0 +1,219 @@
+import {
+ IconButton,
+ lighten,
+ ListItemIcon,
+ ListItemText,
+ Menu,
+ MenuItem,
+ Theme,
+ Tooltip
+} from "@mui/material";
+import SettingsIcon from "@mui/icons-material/Settings";
+import MoreIcon from "@mui/icons-material/MoreVert";
+import React, { useState } from "react";
+import ObjectUsersIcon from "@mui/icons-material/AccountCircle";
+import ObjectTeamsIcon from "@mui/icons-material/SupervisedUserCircle";
+import ObjectUsers from "user/ObjectUsers";
+import { pond } from "protobuf-ts/pond";
+import { Scope } from "models";
+import ObjectTeams from "teams/ObjectTeams";
+import RemoveSelfFromObject from "user/RemoveSelfFromObject";
+import ShareObject from "user/ShareObject";
+import { blue } from "@mui/material/colors";
+import RemoveSelfIcon from "@mui/icons-material/ExitToApp";
+import { Share } from "@mui/icons-material";
+import { Contract } from "models/Contract";
+import ContractSettings from "./contractSettings";
+import { makeStyles } from "@mui/styles";
+
+const useStyles = makeStyles((theme: Theme) => ({
+ shareIcon: {
+ color: blue["500"],
+ "&:hover": {
+ color: blue["600"]
+ }
+ },
+ removeIcon: {
+ color: "var(--status-alert)"
+ },
+ red: {
+ color: "var(--status-alert)"
+ },
+ blueIcon: {
+ color: blue["500"],
+ "&:hover": {
+ color: blue["600"]
+ }
+ },
+ menuButton: {
+ cursor: "pointer",
+ "&:hover": {
+ color: lighten(theme.palette.background.default, 0.2),
+ }
+ }
+ })
+);
+interface OpenState {
+ users: boolean;
+ teams: boolean;
+ settings: boolean;
+ removeSelf: boolean;
+ share: boolean;
+}
+
+interface Props {
+ contract: Contract;
+ refreshCallback: () => void;
+ permissions: pond.Permission[];
+}
+
+export default function ContractActions(props: Props) {
+ const classes = useStyles();
+ const { contract, refreshCallback, permissions } = props;
+ const [anchorEl, setAnchorEl] = React.useState(null);
+ const [openState, setOpenState] = useState({
+ users: false,
+ teams: false,
+ settings: false,
+ removeSelf: false,
+ share: false
+ });
+
+ const groupMenu = () => {
+ return (
+
+ );
+ };
+
+ const dialogs = () => {
+ const key = contract.key();
+ const label = contract.name();
+ return (
+
+ {
+ setOpenState({ ...openState, settings: false });
+ }}
+ />
+ setOpenState({ ...openState, share: false })}
+ />
+ setOpenState({ ...openState, users: false })}
+ refreshCallback={refreshCallback}
+ />
+ setOpenState({ ...openState, teams: false })}
+ refreshCallback={refreshCallback}
+ />
+ {
+ setOpenState({ ...openState, removeSelf: false });
+ }}
+ />
+
+ );
+ };
+
+ return (
+
+ {permissions.includes(pond.Permission.PERMISSION_WRITE) && (
+
+ setOpenState({ ...openState, settings: true })}>
+
+
+
+ )}
+ ) => setAnchorEl(event.currentTarget)}>
+
+
+ {dialogs()}
+ {groupMenu()}
+
+ );
+}
diff --git a/src/contract/contractList.tsx b/src/contract/contractList.tsx
new file mode 100644
index 0000000..345ccc6
--- /dev/null
+++ b/src/contract/contractList.tsx
@@ -0,0 +1,509 @@
+import {
+ Accordion,
+ AccordionDetails,
+ AccordionSummary,
+ Box,
+ Table,
+ TableBody,
+ TableCell,
+ TableHead,
+ TableRow,
+ Button,
+ Typography,
+ Theme,
+ DialogContent,
+ DialogActions,
+ DialogTitle,
+ useTheme,
+ List,
+ ListItem,
+ Divider,
+ Grid2 as Grid,
+ Tabs,
+ Tab,
+ colors,
+ lighten
+} from "@mui/material";
+import { ExpandMore } from "@mui/icons-material";
+import CustomProgressBar from "common/CustomProgressBar";
+import ResponsiveDialog from "common/ResponsiveDialog";
+import GrainDescriber from "grain/GrainDescriber";
+import GrainTransaction from "grain/GrainTransaction";
+import { useMobile } from "hooks";
+import { Contract } from "models/Contract";
+import { pond } from "protobuf-ts/pond";
+import { useContractAPI } from "providers/pond/contractAPI";
+import React, { useEffect, useState } from "react";
+// import { useHistory } from "react-router";
+import { Label, Pie, PieChart, ResponsiveContainer, Text } from "recharts";
+import { stringToMaterialColour } from "utils";
+import { makeStyles } from "@mui/styles";
+import { useNavigate } from "react-router-dom";
+
+interface Props {
+ contracts: Contract[];
+ updateList: (newContractList: Contract[]) => void;
+ contractPermissions: Map;
+}
+
+const useStyles = makeStyles((theme: Theme) => ({
+ contractRow: {
+ cursor: "pointer",
+ "&:hover": {
+ backgroundColor: lighten(theme.palette.background.default, 0.25)
+ }
+ }
+ })
+);
+
+export default function ContractsList(props: Props) {
+ const { contracts, contractPermissions, updateList } = props;
+ const theme = useTheme();
+ //will need map for contracts that have supported grain types
+ const [supportedTypes, setSupportedTypes] = useState