diff --git a/instances/devhub.near/widget/devhub/components/organism/Navbar.jsx b/instances/devhub.near/widget/devhub/components/organism/Navbar.jsx
index a5ed91d5a..90a237d84 100644
--- a/instances/devhub.near/widget/devhub/components/organism/Navbar.jsx
+++ b/instances/devhub.near/widget/devhub/components/organism/Navbar.jsx
@@ -4,9 +4,9 @@ const [showMenu, setShowMenu] = useState(false);
const { href: linkHref } = VM.require("${REPL_DEVHUB}/widget/core.lib.url");
-const { hasModerator } =
- VM.require("${REPL_DEVHUB}/widget/core.adapter.devhub-contract") ||
- (() => {});
+const { hasModerator } = VM.require(
+ "${REPL_DEVHUB}/widget/core.adapter.devhub-contract"
+) || { hasModerator: () => {} };
linkHref || (linkHref = () => {});
diff --git a/instances/events-committee.near/widget/devhub/components/organism/Navbar.jsx b/instances/events-committee.near/widget/devhub/components/organism/Navbar.jsx
index d5e3e37c9..330e4aaaf 100644
--- a/instances/events-committee.near/widget/devhub/components/organism/Navbar.jsx
+++ b/instances/events-committee.near/widget/devhub/components/organism/Navbar.jsx
@@ -4,9 +4,9 @@ const [showMenu, setShowMenu] = useState(false);
const { href: linkHref } = VM.require("${REPL_DEVHUB}/widget/core.lib.url");
-const { hasModerator } =
- VM.require("${REPL_EVENTS}/widget/core.adapter.devhub-contract") ||
- (() => {});
+const { hasModerator } = VM.require(
+ "${REPL_EVENTS}/widget/core.adapter.devhub-contract"
+) || { hasModerator: () => {} };
linkHref || (linkHref = () => {});
diff --git a/instances/infrastructure-committee.near/widget/components/proposals/Feed.jsx b/instances/infrastructure-committee.near/widget/components/proposals/Feed.jsx
new file mode 100644
index 000000000..5e9dfe196
--- /dev/null
+++ b/instances/infrastructure-committee.near/widget/components/proposals/Feed.jsx
@@ -0,0 +1,678 @@
+const { href } = VM.require("${REPL_DEVHUB}/widget/core.lib.url") || {
+ href: () => {},
+};
+
+const instance = props.instance ?? "";
+
+const {
+ contract,
+ rfpFeedIndexerQueryName,
+ proposalFeedAnnouncement,
+ availableCategoryOptions,
+ proposalFeedIndexerQueryName,
+ indexerHasuraRole,
+ isDevhub,
+ isInfra,
+ isEvents,
+} = VM.require(`${instance}/widget/config.data`);
+
+const loader = (
+
+
+
+);
+
+if (!contract) {
+ return loader;
+}
+
+function isNumber(v) {
+ return typeof v === "number";
+}
+
+const Container = styled.div`
+ .full-width-div {
+ width: 100vw;
+ position: relative;
+ left: 50%;
+ right: 50%;
+ margin-left: -50vw;
+ margin-right: -50vw;
+ }
+
+ .card.no-border {
+ border-left: none !important;
+ border-right: none !important;
+ margin-bottom: -3.5rem;
+ }
+
+ @media screen and (max-width: 768px) {
+ font-size: 13px;
+ }
+
+ .text-sm {
+ font-size: 13px;
+ }
+
+ .bg-grey {
+ background-color: #f4f4f4;
+ }
+
+ .border-bottom {
+ border-bottom: 1px solid grey;
+ }
+
+ .cursor-pointer {
+ cursor: pointer;
+ }
+
+ .proposal-card {
+ border-left: none !important;
+ border-right: none !important;
+ border-bottom: none !important;
+ &:hover {
+ background-color: #f4f4f4;
+ }
+ }
+
+ @media screen and (max-width: 768px) {
+ .theme-btn {
+ padding: 0.5rem 0.8rem !important;
+ min-height: 32px;
+ }
+ }
+
+ a.no-space {
+ display: inline-block;
+ }
+
+ .text-wrap {
+ overflow: hidden;
+ white-space: normal;
+ }
+
+ .text-center {
+ text-align: center;
+ }
+
+ .btn-grey-outline {
+ background-color: #fafafa;
+ border: 1px solid #e6e8eb;
+ color: #11181c;
+
+ &:hover {
+ background-color: #e6e8eb;
+ }
+
+ &:active {
+ border: 2px solid #e6e8eb;
+ }
+ }
+`;
+
+const Heading = styled.div`
+ font-size: 24px;
+ font-weight: 700;
+ width: 100%;
+
+ .text-normal {
+ font-weight: normal !important;
+ }
+
+ @media screen and (max-width: 768px) {
+ font-size: 18px;
+ }
+`;
+
+const FeedItem = ({ proposal, index }) => {
+ const accountId = proposal.author_id;
+ const profile = Social.get(`${accountId}/profile/**`, "final");
+ // We will have to get the proposal from the contract to get the block height.
+ const blockHeight = parseInt(proposal.social_db_post_block_height);
+ const item = {
+ type: "social",
+ path: `${contract}/post/main`,
+ blockHeight: blockHeight,
+ };
+
+ const isLinked = isNumber(proposal.linked_rfp);
+ const rfpData = proposal.rfpData;
+
+ return (
+ e.stopPropagation()}
+ style={{ textDecoration: "none" }}
+ >
+
+
+
+
+
+
{proposal.name}
+ {(isInfra || isEvents) && (
+
{},
+ availableOptions: availableCategoryOptions,
+ }}
+ />
+ )}
+ {isDevhub && (
+
+ )}
+
+ {isLinked && rfpData && (
+
+ )}
+
+
#{proposal.proposal_id} ・
+
+ By {profile.name ?? accountId} ・{" "}
+
+
+
+
+
+
+ {},
+ }}
+ />
+
+
+
+
+
+
+
+
+ );
+};
+
+const getProposal = (proposal_id) => {
+ return Near.asyncView(contract, "get_proposal", {
+ proposal_id,
+ });
+};
+
+const FeedPage = () => {
+ const QUERYAPI_ENDPOINT = `https://near-queryapi.api.pagoda.co/v1/graphql`;
+
+ State.init({
+ data: [],
+ author: "",
+ stage: "",
+ sort: "",
+ category: "",
+ input: "",
+ loading: false,
+ searchLoader: false,
+ makeMoreLoader: false,
+ aggregatedCount: null,
+ currentlyDisplaying: 0,
+ });
+
+ const query = `query GetLatestSnapshot($offset: Int = 0, $limit: Int = 20, $where: ${proposalFeedIndexerQueryName}_bool_exp = {}) {
+ ${proposalFeedIndexerQueryName}(
+ offset: $offset
+ limit: $limit
+ order_by: {proposal_id: desc}
+ where: $where
+ ) {
+ author_id
+ block_height
+ name
+ category
+ summary
+ editor_id
+ proposal_id
+ ts
+ timeline
+ views
+ labels
+ linked_rfp
+ }
+ ${proposalFeedIndexerQueryName}_aggregate(
+ order_by: {proposal_id: desc}
+ where: $where
+ ) {
+ aggregate {
+ count
+ }
+ }
+ }`;
+
+ const rfpQuery = `query GetLatestSnapshot($offset: Int = 0, $limit: Int = 20, $where: ${rfpFeedIndexerQueryName}_bool_exp = {}) {
+ ${rfpFeedIndexerQueryName}(
+ offset: $offset
+ limit: $limit
+ order_by: {rfp_id: desc}
+ where: $where
+ ) {
+ name
+ rfp_id
+ }
+ }`;
+
+ function fetchGraphQL(operationsDoc, operationName, variables) {
+ return asyncFetch(QUERYAPI_ENDPOINT, {
+ method: "POST",
+ headers: { "x-hasura-role": indexerHasuraRole },
+ body: JSON.stringify({
+ query: operationsDoc,
+ variables: variables,
+ operationName: operationName,
+ }),
+ });
+ }
+
+ function separateNumberAndText(str) {
+ const numberRegex = /\d+/;
+
+ if (numberRegex.test(str)) {
+ const number = str.match(numberRegex)[0];
+ const text = str.replace(numberRegex, "").trim();
+ return { number: parseInt(number), text };
+ } else {
+ return { number: null, text: str.trim() };
+ }
+ }
+
+ const buildWhereClause = () => {
+ let where = {};
+ if (state.author) {
+ where = { author_id: { _eq: state.author }, ...where };
+ }
+
+ if (state.category) {
+ if (isInfra || isEvents) {
+ where = { labels: { _contains: state.category }, ...where };
+ } else {
+ where = { category: { _eq: state.category }, ...where };
+ }
+ }
+
+ if (state.stage) {
+ // timeline is stored as jsonb
+ where = {
+ timeline: { _cast: { String: { _regex: `${state.stage}` } } },
+ ...where,
+ };
+ }
+ if (state.input) {
+ const { number, text } = separateNumberAndText(state.input);
+ if (number) {
+ where = { proposal_id: { _eq: number }, ...where };
+ }
+
+ if (text) {
+ where = {
+ _or: [
+ { name: { _iregex: `${text}` } },
+ { summary: { _iregex: `${text}` } },
+ { description: { _iregex: `${text}` } },
+ ],
+ ...where,
+ };
+ }
+ }
+
+ return where;
+ };
+
+ const makeMoreItems = () => {
+ State.update({ makeMoreLoader: true });
+ fetchProposals(state.data.length);
+ };
+
+ const statusOrder = {
+ APPROVED: -1,
+ REVIEW: 0,
+ CANCELLED: 1,
+ REJECTED: 1,
+ };
+
+ const fetchProposals = (offset) => {
+ if (!offset) {
+ offset = 0;
+ }
+ if (state.loading) return;
+ State.update({ loading: true });
+ const FETCH_LIMIT = 20;
+ const variables = {
+ limit: FETCH_LIMIT,
+ offset,
+ where: buildWhereClause(),
+ };
+ fetchGraphQL(query, "GetLatestSnapshot", variables).then(async (result) => {
+ if (result.status === 200) {
+ if (result.body.data) {
+ const data = result.body.data[proposalFeedIndexerQueryName];
+ const totalResult =
+ result.body.data[`${proposalFeedIndexerQueryName}_aggregate`];
+ const promises = data.map((item) => {
+ if (isNumber(item.linked_rfp)) {
+ return fetchGraphQL(rfpQuery, "GetLatestSnapshot", {
+ where: { rfp_id: { _eq: item.linked_rfp } },
+ }).then((result) => {
+ const rfpData = result.body.data?.[rfpFeedIndexerQueryName];
+ return { ...item, rfpData: rfpData[0] };
+ });
+ } else {
+ return Promise.resolve(item);
+ }
+ });
+ Promise.all(promises).then((res) => {
+ State.update({ aggregatedCount: totalResult.aggregate.count });
+ fetchBlockHeights(res, offset);
+ });
+ }
+ }
+ });
+ };
+
+ useEffect(() => {
+ State.update({ searchLoader: true });
+ fetchProposals();
+ }, [state.author, state.sort, state.category, state.stage]);
+
+ const mergeItems = (newItems) => {
+ const items = [
+ ...new Set([...newItems, ...state.data].map((i) => JSON.stringify(i))),
+ ].map((i) => JSON.parse(i));
+ // Sorting in the front end
+ if (state.sort === "proposal_id" || state.sort === "") {
+ items.sort((a, b) => b.proposal_id - a.proposal_id);
+ } else if (state.sort === "views") {
+ items.sort((a, b) => b.views - a.views);
+ }
+
+ items.sort((a, b) => {
+ return statusOrder[a.timeline.status] - statusOrder[b.timeline.status];
+ });
+
+ return items;
+ };
+
+ const fetchBlockHeights = (data, offset) => {
+ let promises = data.map((item) => getProposal(item.proposal_id));
+ Promise.all(promises).then((blockHeights) => {
+ data = data.map((item, index) => ({
+ ...item,
+ timeline: JSON.parse(item.timeline),
+ social_db_post_block_height:
+ blockHeights[index].social_db_post_block_height,
+ }));
+ if (offset) {
+ let newData = mergeItems(data);
+ State.update({
+ data: newData,
+ currentlyDisplaying: newData.length,
+ loading: false,
+ searchLoader: false,
+ makeMoreLoader: false,
+ });
+ } else {
+ let sorted = [...data].sort((a, b) => {
+ return (
+ statusOrder[a.timeline.status] - statusOrder[b.timeline.status]
+ );
+ });
+ State.update({
+ data: sorted,
+ currentlyDisplaying: data.length,
+ loading: false,
+ searchLoader: false,
+ makeMoreLoader: false,
+ });
+ }
+ });
+ };
+
+ useEffect(() => {
+ const handler = setTimeout(() => {
+ fetchProposals();
+ }, 1000);
+
+ return () => {
+ clearTimeout(handler);
+ };
+ }, [state.input]);
+
+ return (
+
+
+
+ Proposals
+
+ ({state.aggregatedCount ?? state.data.length}){" "}
+
+
+
+
{
+ State.update({ input });
+ },
+ onEnter: () => {
+ fetchProposals();
+ },
+ }}
+ />
+ {
+ State.update({ sort: select.value });
+ },
+ }}
+ />
+
+ {!isInfra && (
+ {
+ State.update({ category: select.value });
+ },
+ }}
+ />
+ )}
+ {
+ State.update({ stage: select.value });
+ },
+ }}
+ />
+ {
+ State.update({ author: select.value });
+ },
+ }}
+ />
+
+
+
+
+
+
+
+
+ Submit Proposal
+
+ ),
+ classNames: { root: "theme-btn" },
+ }}
+ />
+
+
+
+
+ {!Array.isArray(state.data) ? (
+ loader
+ ) : (
+
+
+ {proposalFeedAnnouncement}
+
+ {state.aggregatedCount === 0 ? (
+
+ No proposals found for selected filter.{" "}
+
+ ) : state.searchLoader ? (
+ loader
+ ) : state.aggregatedCount > 0 ? (
+ state.data.map((item, index) => {
+ return (
+
+
+
+ );
+ })
+ ) : (
+ loader
+ )}
+
+ {state.aggregatedCount > 0 &&
+ state.aggregatedCount > state.data.length && (
+
+ {state.makeMoreLoader ? (
+ loader
+ ) : (
+
+ {!state.loading && (
+
+ makeMoreItems(),
+ }}
+ />
+
+ )}
+
+ )}
+
+ )}
+
+
+ )}
+
+
+ );
+};
+
+return FeedPage(props);
diff --git a/instances/infrastructure-committee.near/widget/components/proposals/Proposals.jsx b/instances/infrastructure-committee.near/widget/components/proposals/Proposals.jsx
index eb97875f5..5691d05c4 100644
--- a/instances/infrastructure-committee.near/widget/components/proposals/Proposals.jsx
+++ b/instances/infrastructure-committee.near/widget/components/proposals/Proposals.jsx
@@ -1,6 +1,6 @@
return (