diff --git a/app/App.tsx b/app/App.tsx
index 2467250..faa41d5 100644
--- a/app/App.tsx
+++ b/app/App.tsx
@@ -7,6 +7,7 @@ import Wagmi from "~/providers/Wagmi";
import { AppLayout } from "~/components/AppLayout";
import { Outlet } from "@remix-run/react";
import { RosetteStone } from "./providers/RosetteStone";
+import { FnEntriesFilters } from "./providers/FnEntriesFilters";
export type AppContext = {
displayTopBar(display: boolean): void;
@@ -24,17 +25,19 @@ export const App = () => {
-
-
-
+
+
+
+
+
diff --git a/app/components/EntriesFilters/DateRangeCustom/index.tsx b/app/components/EntriesFilters/DateRangeCustom/index.tsx
new file mode 100644
index 0000000..b977032
--- /dev/null
+++ b/app/components/EntriesFilters/DateRangeCustom/index.tsx
@@ -0,0 +1,47 @@
+import React, { useCallback, useRef } from "react";
+import { DateRange } from "react-date-range";
+import styled from "styled-components";
+import { useOnClickOutside } from "~/hooks/useOutsideAlerter";
+
+type DataRangeCustomProps = {
+ show: boolean;
+ setShow: (show: boolean) => void;
+ onChange: (dateRange: any) => void;
+ ranges: [{ startDate: Date; endDate: Date; key: string }];
+};
+
+const DateRangeCustom = ({
+ show,
+ setShow,
+ onChange,
+ ranges = [{ startDate: new Date(), endDate: new Date(), key: "selection" }],
+}: DataRangeCustomProps) => {
+ const ref = useRef();
+
+ const handleClose = useCallback(() => {
+ setShow(false);
+ }, [setShow]);
+
+ useOnClickOutside(ref, handleClose);
+
+ return (
+
+
+
+ );
+};
+
+const DateRangeContainer = styled.div`
+ z-index: 1;
+ position: absolute;
+ display: ${({ show }: { show: boolean }) => (show ? "block" : "none")};
+ text-align: center;
+ margin-top: 60px;
+ background-color: rgba(0, 0, 0, 1);
+`;
+
+export default DateRangeCustom;
diff --git a/app/components/EntriesFilters/DateRangeCustom/style.css b/app/components/EntriesFilters/DateRangeCustom/style.css
new file mode 100644
index 0000000..177057e
--- /dev/null
+++ b/app/components/EntriesFilters/DateRangeCustom/style.css
@@ -0,0 +1,26 @@
+.rdrCalendarWrapper {
+ background-color: #FDE9BC;
+ border: 2px solid #CFC7B5;
+ border-radius: 12px;
+}
+
+.rdrDateDisplayWrapper {
+ background-color: #A2957A;
+ border-radius: 10px 10px 0 0 ;
+}
+
+.rdrDateDisplayItemActive {
+ border: 2px solid #F86E38;
+}
+
+.rdrDateDisplayItem input {
+ background-color: #2B2727;
+ color: #FDE9BC;
+ font-size: 16px;
+ border-radius: 3px;
+}
+
+.rdrDateDisplayItem {
+ box-shadow: none;
+ background-color: transparent;
+}
\ No newline at end of file
diff --git a/app/components/EntriesFilters/index.tsx b/app/components/EntriesFilters/index.tsx
new file mode 100644
index 0000000..a612072
--- /dev/null
+++ b/app/components/EntriesFilters/index.tsx
@@ -0,0 +1,389 @@
+import React, { useEffect, useRef, useState } from "react";
+import {
+ Button,
+ GU,
+ SearchInput,
+ TextInput,
+ useTheme,
+} from "@blossom-labs/rosette-ui";
+import styled from "styled-components";
+import Select from "react-select";
+import DateRangeCustom from "~/components/EntriesFilters/DateRangeCustom";
+import { useFnEntriesFilters } from "~/providers/FnEntriesFilters";
+import { useOnClickOutside } from "~/hooks/useOutsideAlerter";
+
+type EntriesFiltersProps = {
+ compactMode: boolean;
+ tabletMode: boolean;
+};
+
+type SubmitterFilterProps = {
+ tabletMode: boolean;
+ compactMode: boolean;
+ placeholder: string;
+ value: string;
+ onChange: (e: any) => void;
+};
+
+export const EntriesFilters = ({
+ compactMode,
+ tabletMode,
+}: EntriesFiltersProps) => {
+ const theme = useTheme();
+ const [toggleDate, setToggleDate] = useState(false);
+ const { externalFilters, internalFilters, clearValues } =
+ useFnEntriesFilters();
+
+ const { submitterFilter, statusFilter, dateRangeFilter, searchFilter } =
+ externalFilters;
+
+ const { sortingOption } = internalFilters;
+
+ const [submitterFilterValue, setSubmitterFilter] = submitterFilter;
+ const [, setStatusFilter, statusRef] = statusFilter;
+ const [dateRange, setDateRange] = dateRangeFilter;
+ const [search, setSearch] = searchFilter;
+
+ const [, setSorting, sortingRef] = sortingOption;
+
+ const handleStatusFilterChange = (selected: any) => {
+ if (selected) {
+ setStatusFilter(selected.value);
+ }
+ };
+
+ const handleSortingOptionChange = (selected: any) => {
+ if (selected) {
+ setSorting(selected.value);
+ }
+ };
+
+ const handleSubmitterFilterChange = (e: any) => {
+ setSubmitterFilter(e.target.value);
+ };
+
+ const handleDateRangeChange = (ranges: any) => {
+ if (ranges.selection || ranges.range1) {
+ const range = ranges.selection || ranges.range1;
+ setDateRange({
+ startDate: range.startDate,
+ endDate: range.endDate,
+ active: true,
+ });
+ }
+ };
+
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ clearValues()}
+ >
+ Clear
+
+
+
+ );
+};
+
+const SubmitterFilter = ({
+ tabletMode,
+ compactMode,
+ placeholder,
+ value,
+ onChange,
+}: SubmitterFilterProps) => {
+ const [showInput, setShowInput] = useState(false);
+ const ref = useRef(null);
+
+ useOnClickOutside(ref, () => setShowInput(false));
+
+ useEffect(() => {
+ const focusOnElement = (ref: any): void => {
+ ref.current.focus();
+ };
+ if (showInput === true) {
+ focusOnElement(ref);
+ }
+ }, [showInput]);
+
+ return (
+ <>
+ setShowInput(true)}
+ >
+ Submitter
+ {showInput && (
+
+
+
+ )}
+
+ >
+ );
+};
+
+const TextInputContainer = styled.div`
+ position: absolute;
+ top: 55px;
+ left: 10px;
+ height: 48px;
+ z-index: 1;
+`;
+
+const SubmitterContainer = styled.div<{
+ tabletMode: boolean;
+ compactMode: boolean;
+ showInput: boolean;
+}>`
+ position: relative;
+ background: #0e0d0d80;
+ height: 48px;
+ width: ${({ tabletMode, compactMode }) =>
+ tabletMode || compactMode ? "100%" : "140px"};
+ padding-left: ${({ tabletMode, compactMode }) =>
+ tabletMode || compactMode ? "30px" : "10px"};
+
+ border: ${({ showInput, theme }) =>
+ showInput ? `2px solid ${theme.focus}` : `1px solid ${theme.borderDark}`};
+};
+ border-radius: 8px;
+ display: flex;
+ color: ${({ theme }) => theme.positiveContent};
+ justify-content: start;
+ align-items: center;
+ cursor: pointer;
+`;
+
+const ClearButton = styled(Button)<{
+ tabletMode: boolean;
+ compactMode: boolean;
+}>`
+ color: ${({ theme }) => theme.positiveContent};
+ width: ${({ tabletMode, compactMode }) =>
+ tabletMode || compactMode ? "100%" : "140px"};
+ height: 48px;
+ background: linear-gradient(#0e0d0d, #0e0d0d) padding-box,
+ linear-gradient(45deg, #fac758, #f7513e) border-box;
+ border: 2px solid transparent;
+ border-radius: 8px;
+`;
+
+const Filter = styled.div`
+ display: flex;
+ justify-content: start;
+`;
+
+const Filters = styled.div<{ compactMode: boolean; tabletMode: boolean }>`
+ display: ${({ compactMode, tabletMode }) =>
+ compactMode || tabletMode ? "grid" : "flex"};
+ grid-gap: ${({ compactMode, tabletMode }) =>
+ compactMode || tabletMode ? 1 * GU : 0}px;
+ grid-template-columns: ${({ compactMode, tabletMode }) =>
+ `repeat(${compactMode ? "1" : tabletMode ? "2" : "1"}, ${
+ compactMode ? "300" : tabletMode ? `336` : ""
+ }px)`};
+ justify-content: ${({ compactMode, tabletMode }) =>
+ compactMode || tabletMode ? "start" : "space-between"};
+ width: ${({ compactMode, tabletMode }) =>
+ compactMode || tabletMode ? "" : "1200px"};
+ margin-bottom: ${5 * GU}px;
+`;
+
+const FiltersContainer = styled.div`
+ display: flex;
+ justify-content: center;
+ width: 1130px;
+ padding-top: ${8 * GU}px;
+`;
+
+// should tweak this to look acording to the theme
+const getSelectStyles = (
+ compactMode: boolean,
+ tabletMode: boolean,
+ theme: any
+) => {
+ const customStyles = {
+ option: (base: any, state: any) => ({
+ ...base,
+ backgroundColor: state.isSelected
+ ? "rgba(138, 128, 105, 0.7)"
+ : "rgba(14, 13, 13)",
+ borderColor: state.isSelected ? "#f5f5f5" : "#ddd",
+ borderWidth: 1,
+ borderRadius: 4,
+ padding: "5px 10px",
+ fontSize: 18,
+ fontWeight: "bold",
+ color: `${theme.positiveContent}`,
+ cursor: "pointer",
+ "&:hover": {
+ backgroundColor: state.isSelected
+ ? "rgba(138, 128, 105, 0.7)"
+ : "rgba(138, 128, 105, 0.2)",
+ },
+ }),
+ menu: (base: any) => ({
+ ...base,
+ border: "1px solid #ccc",
+ boxShadow: "0 2px 12px rgba(0, 0, 0, 0.1)",
+ borderRadius: 8,
+ backgroundColor: "rgba(14, 13, 13)",
+ padding: "5px 0",
+ width: "110%",
+ }),
+ control: (base: any, state: any) => ({
+ ...base,
+ height: "48px",
+ cursor: "pointer",
+ border: state.isFocused
+ ? `2px solid ${theme.focus}`
+ : `1px solid ${theme.borderDark}`,
+ borderRadius: "8px",
+ boxShadow: "0",
+ background: "rgba(14, 13, 13, 0.5)",
+ "&:hover": {
+ borderColor: "none",
+ },
+ }),
+ placeholder: (base: any) => ({
+ ...base,
+ color: `${theme.content}`,
+ }),
+ indicatorSeparator: (base: any) => ({
+ ...base,
+ display: "none",
+ }),
+ singleValue: (base: any) => ({
+ ...base,
+ color: `${theme.content}`,
+ }),
+ };
+
+ if (tabletMode) {
+ return {
+ ...customStyles,
+ control: (base: any, state: any) => ({
+ ...base,
+ padding: " 0 0 0 20px",
+ width: "335px",
+ height: "48px",
+ cursor: "pointer",
+ border: state.isFocused
+ ? `2px solid ${theme.focus}`
+ : `1px solid ${theme.borderDark}`,
+ borderRadius: "8px",
+ boxShadow: "0",
+ background: "rgba(14, 13, 13, 0.5)",
+ "&:hover": {
+ borderColor: "none",
+ },
+ }),
+ };
+ } else if (compactMode) {
+ return {
+ ...customStyles,
+ control: (base: any, state: any) => ({
+ ...base,
+ padding: " 0 0 0 20px",
+ width: "300px",
+ height: "48px",
+ cursor: "pointer",
+ border: state.isFocused
+ ? `2px solid ${theme.focus}`
+ : `1px solid ${theme.borderDark}`,
+ borderRadius: "8px",
+ boxShadow: "0",
+ background: "rgba(14, 13, 13, 0.5)",
+ "&:hover": {
+ borderColor: "none",
+ },
+ }),
+ };
+ } else {
+ return customStyles;
+ }
+};
diff --git a/app/components/EntryScreen/StatusModule/index.tsx b/app/components/EntryScreen/StatusModule/index.tsx
new file mode 100644
index 0000000..e848735
--- /dev/null
+++ b/app/components/EntryScreen/StatusModule/index.tsx
@@ -0,0 +1,76 @@
+import { useEffect, useState } from "react";
+import { Button, GU } from "@blossom-labs/rosette-ui";
+import styled from "styled-components";
+
+type StatusModuleProps = {
+ status: string | undefined;
+};
+
+export const StatusModule = ({ status }: StatusModuleProps) => {
+ const [currentStatus, setCurrentStatus] = useState();
+
+ useEffect(() => {
+ if (status) {
+ setCurrentStatus(status);
+ }
+ }, [status]);
+
+ const renderDynamicSection = () => {
+ switch (status) {
+ case "challenged":
+ return ;
+ case "added":
+ return ;
+ case "pending":
+ return Timer
;
+ }
+ };
+
+ return (
+
+
+
+ {currentStatus}
+
+ {renderDynamicSection()}
+
+ );
+};
+
+const Container = styled.div`
+ display: flex;
+ flex-direction: column;
+ width: 363px;
+ height: 200px;
+ justify-content: space-around;
+ border: 1px solid #8a8069;
+ border-radius: 20px;
+ outline: 0;
+`;
+
+const StatusContainer = styled.div`
+ display: flex;
+ flex-direction: column;
+ gap: ${2 * GU}px;
+ padding: ${1 * GU}px;
+`;
+
+const DynamicSection = styled.div`
+ display: flex;
+ flex-direction: column;
+ width: ${35 * GU}px;
+ margin: auto;
+`;
+
+const Label = styled.div`
+ font-size: 16px;
+ color: #a2957a;
+ margin-left: ${4 * GU}px;
+ margin-top: ${3 * GU}px;
+`;
+const StatusTitle = styled.div`
+ font-weight: 400;
+ font-size: 20px;
+ color: #fde9bc;
+ margin-left: ${4 * GU}px;
+`;
diff --git a/app/hooks/useOutsideAlerter.ts b/app/hooks/useOutsideAlerter.ts
new file mode 100644
index 0000000..bc5293f
--- /dev/null
+++ b/app/hooks/useOutsideAlerter.ts
@@ -0,0 +1,21 @@
+import { useEffect } from 'react';
+
+export function useOnClickOutside(ref: any, handler: any) {
+ useEffect(
+ () => {
+ const listener = (event: any) => {
+ if (!ref.current || ref.current.contains(event.target)) {
+ return;
+ }
+ handler(event);
+ };
+ document.addEventListener("mousedown", listener);
+ document.addEventListener("touchstart", listener);
+ return () => {
+ document.removeEventListener("mousedown", listener);
+ document.removeEventListener("touchstart", listener);
+ };
+ },
+ [ref, handler]
+ );
+}
\ No newline at end of file
diff --git a/app/providers/FnEntriesFilters.tsx b/app/providers/FnEntriesFilters.tsx
new file mode 100644
index 0000000..2dc15e1
--- /dev/null
+++ b/app/providers/FnEntriesFilters.tsx
@@ -0,0 +1,233 @@
+import React, {
+ useCallback,
+ useContext,
+ useEffect,
+ useRef,
+ useState,
+} from "react";
+import { FnDescriptionStatus } from "~/types";
+import type { FnEntry } from "~/types";
+
+export type ExternalFilters = {
+ submitterFilter: [string, (submitter: string) => void];
+ statusFilter: [
+ FnDescriptionStatus | "",
+ (status: FnDescriptionStatus | "") => void,
+ React.Ref
+ ];
+ dateRangeFilter: [any, (dateRange: any) => void];
+ searchFilter: [string, (searchTerm: string) => void];
+};
+
+type DateRangeSelection = {
+ startDate: Date;
+ endDate: Date;
+ key: string;
+ active: boolean;
+};
+
+type FnEntriesFiltersContextProps = {
+ entries: FnEntry[];
+ setEntries: (entries: FnEntry[]) => void;
+ filteredEntries: FnEntry[];
+ externalFilters: ExternalFilters;
+ internalFilters: {
+ sortingOption: [string | null, (option: string) => void, React.Ref];
+ };
+ clearValues: () => void;
+ loading: boolean;
+};
+
+const FnEntriesFiltersContext =
+ React.createContext(
+ {} as FnEntriesFiltersContextProps
+ );
+
+export function FnEntriesFilters({ children }: { children: React.ReactNode }) {
+ const [entries, setEntries] = useState([]);
+ const [sortingOption, setSortingOption] = useState(null);
+ const [dateRange, setDateRange] = useState({
+ startDate: new Date(),
+ endDate: new Date(),
+ key: "selection",
+ active: false,
+ });
+
+ // todo: add types
+ const [submitterFilter, setSubmitterFilter] = useState("");
+ const [statusFilter, setStatusFilter] = useState(
+ ""
+ );
+ const [abiSearchFilter, setAbiSearchFilter] = useState("");
+
+ const [filteredEntries, setFilteredEntries] = useState([]);
+
+ const sortingRef = useRef();
+ const statusRef = useRef();
+
+ useEffect(() => {
+ let filtered = entries;
+
+ const statusFiltering = () => {
+ switch (statusFilter) {
+ case FnDescriptionStatus.Added:
+ filtered = filtered.filter(
+ (e) => e.status === FnDescriptionStatus.Added
+ );
+ break;
+ case FnDescriptionStatus.Available:
+ filtered = filtered.filter(
+ (e) => e.status === FnDescriptionStatus.Available
+ );
+ break;
+ case FnDescriptionStatus.Pending:
+ filtered = filtered.filter(
+ (e) => e.status === FnDescriptionStatus.Pending
+ );
+ break;
+ case FnDescriptionStatus.Challenged:
+ filtered = filtered.filter(
+ (e) => e.status === FnDescriptionStatus.Challenged
+ );
+ break;
+ default:
+ filtered = entries;
+ break;
+ }
+ };
+
+ const searchFiltering = () => {
+ if (abiSearchFilter) {
+ filtered = filtered.filter((e) => {
+ return e.abi.toLowerCase().includes(abiSearchFilter.toLowerCase());
+ });
+ }
+ };
+ const sorting = () => {
+ switch (sortingOption) {
+ case "newest":
+ filtered = filtered.sort((a, b) => {
+ return b.upsertAt - a.upsertAt;
+ });
+ break;
+ // how to sort by relevance?
+ case "relevance":
+ filtered = filtered.slice().sort((a, b) => {
+ if (a.abi > b.abi) {
+ return 1;
+ }
+ if (a.abi < b.abi) {
+ return -1;
+ }
+ return 0;
+ });
+ break;
+ }
+ };
+ const submitterFiltering = () => {
+ if (submitterFilter !== "") {
+ filtered = filtered.filter((entry) => {
+ return entry.submitter
+ .toLowerCase()
+ .includes(submitterFilter.toString().toLowerCase());
+ });
+ }
+ };
+ const rangeFiltering = () => {
+ if (
+ dateRange &&
+ dateRange.active &&
+ dateRange.startDate &&
+ dateRange.endDate
+ ) {
+ filtered = filtered.filter((entry) => {
+ return (
+ entry.upsertAt >= dateRange.startDate.getTime() &&
+ entry.upsertAt <= dateRange.endDate.getTime()
+ );
+ });
+ }
+ };
+
+ statusFiltering();
+ submitterFiltering();
+ rangeFiltering();
+ searchFiltering();
+ sorting();
+ setFilteredEntries(filtered);
+ }, [
+ entries,
+ statusFilter,
+ sortingOption,
+ setFilteredEntries,
+ submitterFilter,
+ dateRange,
+ abiSearchFilter,
+ ]);
+
+ const handleSortingOptionChange = useCallback((option: string) => {
+ setSortingOption(option);
+ }, []);
+
+ const handleSubmitterFilterChange = useCallback((newSubmitter: any) => {
+ setSubmitterFilter(newSubmitter);
+ }, []);
+
+ const handleStatusFilterChange = useCallback(
+ (status: FnDescriptionStatus | "") => {
+ setStatusFilter(status);
+ },
+ []
+ );
+
+ const handleSearchChange = useCallback((newSearch: any) => {
+ setAbiSearchFilter(newSearch);
+ }, []);
+
+ const clearValues = () => {
+ setSubmitterFilter("");
+ setDateRange({
+ startDate: new Date(),
+ endDate: new Date(),
+ key: "selection",
+ active: false,
+ });
+ handleStatusFilterChange("");
+ setAbiSearchFilter("");
+ setFilteredEntries(entries);
+ setSortingOption("");
+ if (sortingRef.current) {
+ sortingRef.current.clearValue();
+ }
+ if (statusRef.current) {
+ statusRef.current.clearValue();
+ }
+ };
+
+ return (
+
+ {children}
+
+ );
+}
+
+export const useFnEntriesFilters = () => {
+ return useContext(FnEntriesFiltersContext);
+};
diff --git a/app/routes/entries/$entry.tsx b/app/routes/entries/$entry.tsx
index db431bf..4d2908d 100644
--- a/app/routes/entries/$entry.tsx
+++ b/app/routes/entries/$entry.tsx
@@ -1,10 +1,20 @@
+import {
+ BackButton,
+ textStyle,
+ IdentityBadge,
+ GU,
+ useViewport,
+} from "@blossom-labs/rosette-ui";
+import { useEffect, useState } from "react";
import type { LoaderFunction } from "@remix-run/node";
import { json } from "@remix-run/node";
-import { useLoaderData } from "@remix-run/react";
+import { useLoaderData, useNavigate } from "@remix-run/react";
import styled from "styled-components";
import { AppScreen } from "~/components/AppLayout/AppScreen";
-import type { FnEntrySubgraphData } from "~/types";
+import { StatusModule } from "~/components/EntryScreen/StatusModule";
+import type { FnEntryMetadata, FnEntrySubgraphData } from "~/types";
import { fetchFnEntry } from "~/utils/server/subgraph.server";
+import { fetchEntryMetadata } from "~/utils/server/entries-data.server";
export const loader: LoaderFunction = async ({ params }) => {
const entryId = params.entry;
@@ -18,28 +28,151 @@ export const loader: LoaderFunction = async ({ params }) => {
const entry = await fetchFnEntry(entryId);
- return json({ entry });
+ if (!entry) {
+ throw new Response("Entry not found", {
+ status: 404,
+ statusText: "Entry not found",
+ });
+ }
+
+ const entryMetadata = await fetchEntryMetadata(entry.cid);
+
+ if (entryMetadata.abi) {
+ const abi = formatAbi(entryMetadata.abi);
+ entryMetadata.abi = abi;
+ }
+
+ return json({ entry, entryMetadata });
};
type LoaderData = {
entry?: FnEntrySubgraphData;
+ entryMetadata?: FnEntryMetadata;
};
export default function EntryRoute() {
- const { entry } = useLoaderData();
+ const { entry, entryMetadata } = useLoaderData();
+ const navigate = useNavigate();
+ const [date, setDate] = useState();
+ const { below } = useViewport();
+ const compactMode = below("large");
+
+ useEffect(() => {
+ if (entry) {
+ const rawDate = new Date(entry.upsertAt);
+ setDate(formatRawDate(rawDate));
+ }
+ }, [entry]);
return (
- {entry?.sigHash}
+
+
+ navigate(`/entries`)} />
+ {entryMetadata?.abi}
+ {`Contract ${""}`}
+ {`Publicated on ${date?.toString()}`}
+
+ Submitter
+
+
+
+ Description
+ {entryMetadata?.notice}
+
+
+
+
);
}
-const Container = styled.div`
+function formatRawDate(rawDate: Date) {
+ const string = rawDate.toString();
+ const dateArray = string.split(" ");
+ const formattedDate = `${dateArray[1]} ${dateArray[2]}, ${dateArray[3]}`;
+ return formattedDate;
+}
+
+function formatAbi(abi: string) {
+ const formattedAbi = abi.replace("function ", "");
+ return formattedAbi;
+}
+
+const DescriptionContainer = styled.div`
display: flex;
- justify-content: center;
+ flex-direction: column;
+`;
+
+const DescriptionContent = styled.div`
+ font-family: "Avenir";
+ font-style: normal;
+ ${textStyle("title4")};
+ color: ${({ theme }) => theme.positiveContent};
+`;
+
+const DescriptionTitle = styled.div`
+ display: flex;
+ justify-content: start;
+ align-items: start;
+ height: 100%;
+ width: 100%;
+ ${textStyle("title3")};
+ color: ${({ theme }) => theme.surfaceOpened};
+`;
+
+const SubmitterTitle = styled.div`
+ margin-bottom: ${1 * GU}px;
+ ${textStyle("body2")};
+ color: ${({ theme }) => theme.surfaceOpened};
+`;
+
+const SubmitterContainer = styled.div`
+ display: flex;
+ flex-direction: column;
+`;
+
+const DateTitle = styled.div`
+ ${textStyle("title4")};
+ color: ${({ theme }) => theme.surfaceOpened};
+`;
+
+const ContractTitle = styled.div`
+ display: flex;
+ justify-content: start;
align-items: start;
height: 100%;
width: 100%;
+ ${textStyle("title4")};
color: ${({ theme }) => theme.content};
`;
+
+const EntryTitle = styled.div`
+ font-weight: 400;
+ font-size: 36px;
+ color: ${({ theme }) => theme.content};};
+`;
+
+const EntryInfoContainer = styled.div`
+ display: flex;
+ flex-direction: column;
+ justify-content: start;
+ align-items: start;
+ width: 650px;
+ gap: ${2 * GU}px;
+`;
+
+const EntryContainer = styled.div<{
+ compactMode: boolean;
+ tabletMode: boolean;
+}>`
+ display: flex;
+ flex-direction: ${({ compactMode }) => (compactMode ? "column" : "row")};
+ justify-content: center;
+ align-items: ${({ compactMode }) => (compactMode ? "center" : "start")};
+ height: 100%;
+ width: 100%;
+ grid-gap: ${10 * GU}px;
+ padding-top: ${({ compactMode, tabletMode }) =>
+ compactMode ? 3 * GU : tabletMode ? 5 * GU : 9 * GU}px;
+`;
diff --git a/app/routes/entries/index.tsx b/app/routes/entries/index.tsx
index dfa110f..9550a85 100644
--- a/app/routes/entries/index.tsx
+++ b/app/routes/entries/index.tsx
@@ -1,4 +1,5 @@
import { GU, textStyle, useViewport } from "@blossom-labs/rosette-ui";
+import { useEffect } from "react";
import type { LoaderFunction } from "@remix-run/node";
import { json } from "@remix-run/node";
import { useLoaderData, useNavigate } from "@remix-run/react";
@@ -6,10 +7,25 @@ import styled from "styled-components";
import { AppScreen } from "~/components/AppLayout/AppScreen";
import { SmoothDisplayContainer } from "~/components/SmoothDisplayContainer";
import { StatusLabel } from "~/components/StatusLabel";
+import { EntriesFilters } from "~/components/EntriesFilters";
+import { useFnEntriesFilters } from "~/providers/FnEntriesFilters";
import type { FnEntry } from "~/types";
import { fetchEntries } from "~/utils/server/entries-data.server";
import { fetchFnEntries } from "~/utils/server/subgraph.server";
+// date range styles
+import dateStyles from "react-date-range/dist/styles.css";
+import defaultDateStyles from "react-date-range/dist/theme/default.css";
+import customDateStyles from "../../components/EntriesFilters/DateRangeCustom/style.css";
+
+export function links() {
+ return [
+ { rel: "stylesheet", href: dateStyles },
+ { rel: "stylesheet", href: defaultDateStyles },
+ { rel: "stylesheet", href: customDateStyles },
+ ];
+}
+
export const loader: LoaderFunction = async () => {
const fnsSubgraphData = await fetchFnEntries();
@@ -26,16 +42,23 @@ export default function Entries() {
const { fns } = useLoaderData();
const { below, within } = useViewport();
+ const { setEntries, filteredEntries } = useFnEntriesFilters();
+
const compactMode = below("medium");
const tabletMode = within("medium", "large");
+ useEffect(() => {
+ setEntries(fns);
+ }, [fns, setEntries]);
+
return (
- {fns.length ? (
+
+ {filteredEntries.length ? (
- {fns.map((f) => (
+ {filteredEntries.map((f) => (
))}
@@ -64,36 +87,37 @@ function EntryCard({ fn }: { fn: FnEntry }) {
const Container = styled.div<{ compactMode: boolean; tabletMode: boolean }>`
display: flex;
- justify-content: center;
- align-items: start;
- padding-top: ${({ compactMode, tabletMode }) =>
- compactMode ? 3 * GU : tabletMode ? 5 * GU : 9 * GU}px;
+ flex-direction: column;
+ justify-content: start;
+ align-items: center;
height: 100%;
width: 100%;
+ padding-top: ${({ compactMode, tabletMode }) =>
+ compactMode ? 3 * GU : tabletMode ? 5 * GU : 9 * GU}px;
`;
const ListContainer = styled.div<{ compactMode: boolean; tabletMode: boolean }>`
display: grid;
- grid-gap: ${({ compactMode, tabletMode }) =>
- compactMode ? 3 * GU : tabletMode ? 3 * GU : 5 * GU}px;
grid-template-columns: ${({ compactMode, tabletMode }) =>
`repeat(${compactMode ? "1" : tabletMode ? "2" : "3"}, ${
compactMode ? "327" : tabletMode ? "336" : "350"
}px)`};
+ grid-gap: ${({ compactMode, tabletMode }) =>
+ compactMode ? 3 * GU : tabletMode ? 3 * GU : 5 * GU}px;
margin-bottom: ${5 * GU}px;
`;
const EntryContainer = styled.div`
+ display: flex;
+ flex-direction: column;
+ justify-content: space-between;
+ height: 223px;
padding: ${3 * GU}px;
background: ${(props) => props.theme.surface};
border: 1px solid ${(props) => props.theme.border};
border-radius: ${2.5 * GU}px;
box-shadow: 0px 1px 2px rgba(0, 0, 0, 0.15);
cursor: pointer;
- display: flex;
- flex-direction: column;
- justify-content: space-between;
- height: 223px;
`;
const NoticeContainer = styled.div`
@@ -110,8 +134,8 @@ const InfoContainer = styled.div`
display: flex;
justify-content: space-between;
align-items: center;
- border-top: 1px solid ${(props) => props.theme.border};
padding-top: ${4 * GU}px;
+ border-top: 1px solid ${(props) => props.theme.border};
`;
const Hash = styled.div`
diff --git a/app/types.ts b/app/types.ts
index 5321e0c..9f8c1d4 100644
--- a/app/types.ts
+++ b/app/types.ts
@@ -4,6 +4,8 @@ export type ValueOrArray = T | ValueOrArray[];
export enum FnDescriptionStatus {
Available = "available",
+ Challenged = "challenged",
+ Pending = "pending",
Added = "added",
}
diff --git a/app/utils/client/icons.client.ts b/app/utils/client/icons.client.ts
index 86b2fea..3179f36 100644
--- a/app/utils/client/icons.client.ts
+++ b/app/utils/client/icons.client.ts
@@ -1,4 +1,4 @@
-import { IconCheck } from "@blossom-labs/rosette-ui";
+import { IconCheck, IconClock, IconWarning } from "@blossom-labs/rosette-ui";
import type { FunctionComponent } from "react";
import ethereum from "~/assets/ethereum.svg";
import gnosisChain from "~/assets/gnosis-chain.svg";
@@ -35,5 +35,17 @@ export const getFnEntryStatusIconData = (
Icon: IconCheck,
color: "#8DCE3A",
};
+ case FnDescriptionStatus.Pending:
+ return {
+ Icon: IconClock,
+ color: "#F5A623",
+ }
+ case FnDescriptionStatus.Challenged:
+ return {
+ Icon: IconWarning,
+ color: "#F7513E"
+ }
+ case FnDescriptionStatus.Available:
+ return undefined;
}
};
diff --git a/package.json b/package.json
index 29a63e0..ffb5fd8 100644
--- a/package.json
+++ b/package.json
@@ -25,12 +25,15 @@
"@uniswap/widgets": "^1.1.1",
"@vercel/node": "^2.4.0",
"clipboard-polyfill": "^4.0.0-rc1",
+ "date-fns": "^2.29.1",
"ethers": "^5.6.1",
"ipfs-http-client": "56.0.3",
"lodash.debounce": "^4.0.8",
"react": "^17.0.2",
+ "react-date-range": "^1.4.0",
"react-dom": "^17.0.2",
"react-redux": "^8.0.2",
+ "react-select": "^5.4.0",
"redux": "^4.2.0",
"styled-components": "^5.3.3",
"wagmi": "^0.2.28",
@@ -42,6 +45,7 @@
"@remix-run/serve": "^1.6.3",
"@types/lodash.debounce": "^4.0.7",
"@types/react": "^17.0.24",
+ "@types/react-date-range": "^1.4.4",
"@types/react-dom": "^17.0.9",
"@types/styled-components": "^5.1.24",
"@types/stylis": "^4.0.2",