@@ -140,6 +148,7 @@ return (
handle: "developer-dao",
hideTitle: true,
communityAddonId: "blogv2",
+ data: config,
}}
/>
diff --git a/instances/devhub.near/widget/devhub/page/community/index.jsx b/instances/devhub.near/widget/devhub/page/community/index.jsx
index 3d7fd93e9..b4d3a0764 100644
--- a/instances/devhub.near/widget/devhub/page/community/index.jsx
+++ b/instances/devhub.near/widget/devhub/page/community/index.jsx
@@ -56,31 +56,6 @@ const [isLinkCopied, setLinkCopied] = useState(false);
const [addonView, setAddonView] = useState("viewer");
-// CommunityAddOn
-const blogv2 = {
- addon_id: "blogv2",
- display_name: "BlogV2",
- enabled: true,
- id: "blogv2",
- parameters:
- '{"title":"My blog page title",\
- "subtitle":"Classic subtitle",\
- "auhtorEnabled": "enabled",\
- "searchEnabled": "enabled",\
- "orderBy": "timedesc",\
- "categoriesEnabled": "enabled",\
- "categories": ["news", "guide", "reference"],\
- "categoryRequired": false}',
-};
-
-const blogv2instance2 = {
- addon_id: "blogv2",
- display_name: "BlogV2",
- enabled: true,
- id: "blogv2instance2",
- parameters: "{}",
-};
-
const tabs = [];
(community.addons || []).map((addon) => {
diff --git a/instances/events-committee.near/aliases.mainnet.json b/instances/events-committee.near/aliases.mainnet.json
index 246aad77b..aadaf50b5 100644
--- a/instances/events-committee.near/aliases.mainnet.json
+++ b/instances/events-committee.near/aliases.mainnet.json
@@ -8,4 +8,4 @@
"REPL_DEVS": "devs.near",
"REPL_SOCIAL_CONTRACT": "social.near",
"REPL_RPC_URL": "https://rpc.mainnet.near.org"
-}
\ No newline at end of file
+}
diff --git a/instances/infrastructure-committee.near/.gitignore b/instances/infrastructure-committee.near/.gitignore
new file mode 100644
index 000000000..9d0b71a3c
--- /dev/null
+++ b/instances/infrastructure-committee.near/.gitignore
@@ -0,0 +1,2 @@
+build
+dist
diff --git a/instances/infrastructure-committee.near/aliases.mainnet.json b/instances/infrastructure-committee.near/aliases.mainnet.json
new file mode 100644
index 000000000..a26f38ef0
--- /dev/null
+++ b/instances/infrastructure-committee.near/aliases.mainnet.json
@@ -0,0 +1,12 @@
+{
+ "REPL_DEVHUB": "devhub.near",
+ "REPL_INFRASTRUCTURE_COMMITTEE": "infrastructure-committee.near",
+ "REPL_INFRASTRUCTURE_COMMITTEE_CONTRACT": "infrastructure-committee.near",
+ "REPL_NEAR": "near",
+ "REPL_RPC_URL": "https://rpc.mainnet.near.org",
+ "REPL_RFP_IMAGE": "https://ipfs.near.social/ipfs/bafkreicbygt4kajytlxij24jj6tkg2ppc2dw3dlqhkermkjjfgdfnlizzy",
+ "REPL_RFP_FEED_INDEXER_QUERY_NAME": "polyprogrammist_near_devhub_ic_v1_rfps_with_latest_snapshot",
+ "REPL_RFP_INDEXER_QUERY_NAME": "polyprogrammist_near_devhub_ic_v1_rfp_snapshots",
+ "REPL_PROPOSAL_FEED_INDEXER_QUERY_NAME": "polyprogrammist_near_devhub_ic_v1_proposals_with_latest_snapshot",
+ "REPL_PROPOSAL_QUERY_NAME": "polyprogrammist_near_devhub_ic_v1_proposal_snapshots"
+}
diff --git a/instances/infrastructure-committee.near/bos.config.json b/instances/infrastructure-committee.near/bos.config.json
new file mode 100644
index 000000000..cdab1132b
--- /dev/null
+++ b/instances/infrastructure-committee.near/bos.config.json
@@ -0,0 +1,6 @@
+{
+ "account": "infrastructure-committee.near",
+ "aliasPrefix": "REPL",
+ "aliasesContainsPrefix": true,
+ "aliases": ["./aliases.mainnet.json"]
+}
diff --git a/instances/infrastructure-committee.near/data.json b/instances/infrastructure-committee.near/data.json
new file mode 100644
index 000000000..b3569fc6c
--- /dev/null
+++ b/instances/infrastructure-committee.near/data.json
@@ -0,0 +1,3 @@
+{
+ "infrastructure-committee.near": {}
+}
diff --git a/instances/infrastructure-committee.near/src b/instances/infrastructure-committee.near/src
new file mode 120000
index 000000000..0301008ca
--- /dev/null
+++ b/instances/infrastructure-committee.near/src
@@ -0,0 +1 @@
+widget
\ No newline at end of file
diff --git a/instances/infrastructure-committee.near/widget/app.jsx b/instances/infrastructure-committee.near/widget/app.jsx
new file mode 100644
index 000000000..a07dce898
--- /dev/null
+++ b/instances/infrastructure-committee.near/widget/app.jsx
@@ -0,0 +1,129 @@
+/**
+ * This is the main entry point for the RFP application.
+ * Page route gets passed in through params, along with all other page props.
+ */
+
+const { page, ...passProps } = props;
+
+// Import our modules
+const { AppLayout } = VM.require(
+ `${REPL_INFRASTRUCTURE_COMMITTEE}/widget/components.template.AppLayout`
+);
+
+if (!AppLayout) {
+ return
Loading modules...
;
+}
+
+// CSS styles to be used across the app.
+// Define fonts here, as well as any other global styles.
+const Theme = styled.div`
+ a {
+ color: inherit;
+ }
+
+ .attractable {
+ box-shadow: 0 0.125rem 0.25rem rgba(0, 0, 0, 0.075) !important;
+ transition: box-shadow 0.6s;
+
+ &:hover {
+ box-shadow: 0 0.5rem 1rem rgba(0, 0, 0, 0.15) !important;
+ }
+ }
+`;
+
+if (!page) {
+ // If no page is specified, we default to the feed page TEMP
+ page = "about";
+}
+
+// This is our navigation, rendering the page based on the page parameter
+function Page() {
+ const routes = page.split(".");
+ switch (routes[0]) {
+ case "about": {
+ return (
+
+ );
+ }
+ case "rfps": {
+ return (
+
+ );
+ }
+ case "rfp": {
+ return (
+
+ );
+ }
+ case "create-rfp": {
+ return (
+
+ );
+ }
+ case "create-proposal": {
+ return (
+
+ );
+ }
+
+ case "proposals": {
+ return (
+
+ );
+ }
+ case "proposal": {
+ return (
+
+ );
+ }
+ case "about": {
+ return (
+
+ );
+ }
+ case "admin": {
+ return (
+
+ );
+ }
+ default: {
+ // TODO: 404 page
+ return
404
;
+ }
+ }
+}
+
+return (
+
+
+
+
+
+);
diff --git a/instances/infrastructure-committee.near/widget/components/admin/AboutConfigurator.jsx b/instances/infrastructure-committee.near/widget/components/admin/AboutConfigurator.jsx
new file mode 100644
index 000000000..5d8fbe526
--- /dev/null
+++ b/instances/infrastructure-committee.near/widget/components/admin/AboutConfigurator.jsx
@@ -0,0 +1,185 @@
+const { Tile } = VM.require(
+ `${REPL_DEVHUB}/widget/devhub.components.molecule.Tile`
+) || { Tile: () => <>> };
+
+const item = {
+ path: `${REPL_INFRASTRUCTURE_COMMITTEE_CONTRACT}/profile/**`,
+};
+
+const profile = Social.get(item.path);
+
+if (!profile.description) {
+
+
+
;
+}
+
+const initialData = profile.description;
+const [content, setContent] = useState(null);
+const [showCommentToast, setCommentToast] = useState(false);
+const [handler, setHandler] = useState(null);
+const [isTxnCreated, setTxnCreated] = useState(false);
+
+const Container = styled.div`
+ width: 100%;
+ margin: 0 auto;
+ padding: 20px;
+ text-align: left;
+`;
+
+const hasDataChanged = () => {
+ return content !== initialData;
+};
+
+const handlePublish = () => {
+ setTxnCreated(true);
+ Near.call([
+ {
+ contractName: "${REPL_INFRASTRUCTURE_COMMITTEE_CONTRACT}",
+ methodName: "set_social_db_profile_description",
+ args: { description: content },
+ gas: 270000000000000,
+ },
+ ]);
+};
+
+useEffect(() => {
+ if (isTxnCreated) {
+ const checkForAboutInSocialDB = () => {
+ Near.asyncView(REPL_SOCIAL_CONTRACT, "get", {
+ keys: [item.path],
+ }).then((result) => {
+ try {
+ const submittedAboutText = content;
+ const lastAboutTextFromSocialDB =
+ result["${REPL_INFRASTRUCTURE_COMMITTEE_CONTRACT}"].profile
+ .description;
+ if (submittedAboutText === lastAboutTextFromSocialDB) {
+ setTxnCreated(false);
+ setCommentToast(true);
+ return;
+ }
+ } catch (e) {}
+ setTimeout(() => checkForAboutInSocialDB(), 2000);
+ });
+ };
+ checkForAboutInSocialDB();
+ }
+}, [isTxnCreated]);
+
+useEffect(() => {
+ if (!content && initialData) {
+ setContent(initialData);
+ setHandler("update");
+ }
+}, [initialData]);
+
+function Preview() {
+ return (
+
+
+
+ );
+}
+
+return (
+
+ setCommentToast(v),
+ trigger: <>>,
+ providerProps: { duration: 3000 },
+ }}
+ />
+
+
+
+ Edit
+
+
+
+
+ Preview
+
+
+
+
+
+
{
+ setContent(v);
+ },
+ showAutoComplete: true,
+ }}
+ />
+
+
+
+
+
+
+
+
+);
diff --git a/instances/infrastructure-committee.near/widget/components/admin/AccountsEditor.jsx b/instances/infrastructure-committee.near/widget/components/admin/AccountsEditor.jsx
new file mode 100644
index 000000000..ebe769ca1
--- /dev/null
+++ b/instances/infrastructure-committee.near/widget/components/admin/AccountsEditor.jsx
@@ -0,0 +1,79 @@
+const { data, setList, validate, invalidate } = props;
+
+const [newItem, setNewItem] = useState("");
+
+const handleAddItem = () => {
+ if (validate(newItem)) {
+ setList([...data.list, newItem]);
+ setNewItem("");
+ } else {
+ return invalidate();
+ }
+};
+
+const handleDeleteItem = (index) => {
+ const updatedData = [...data.list];
+ updatedData.splice(index, 1);
+ setList(updatedData);
+};
+
+const Item = styled.div`
+ padding: 10px;
+ margin: 5px;
+ display: flex;
+ align-items: center;
+ flex-direction: row;
+ gap: 10px;
+`;
+
+return (
+ <>
+ {data.list.map((item, index) => (
+
-
+
+
+
+ handleDeleteItem(index)}
+ >
+
+
+
+ ))}
+ {data.list.length < data.maxLength && (
+
-
+
+ setNewItem(value),
+ value: newItem,
+ placeholder: data.placeholder,
+ }}
+ />
+
+
+
+
+
+ )}
+ >
+);
diff --git a/instances/infrastructure-committee.near/widget/components/admin/ModeratorsConfigurator.jsx b/instances/infrastructure-committee.near/widget/components/admin/ModeratorsConfigurator.jsx
new file mode 100644
index 000000000..22aad5644
--- /dev/null
+++ b/instances/infrastructure-committee.near/widget/components/admin/ModeratorsConfigurator.jsx
@@ -0,0 +1,116 @@
+const { Tile } = VM.require(
+ `${REPL_DEVHUB}/widget/devhub.components.molecule.Tile`
+) || { Tile: () => <>> };
+
+const { accessControlInfo, createEditTeam } = props;
+
+const [editModerators, setEditModerators] = useState(false);
+const [moderators, setModerators] = useState(
+ accessControlInfo.members_list["team:moderators"].children || []
+);
+
+const handleEditModerators = () => {
+ createEditTeam({
+ teamName: "moderators",
+ description:
+ "The moderator group has permissions to create and edit RFPs, edit and manage proposals, and manage admins.",
+ members: moderators,
+ contractCall: "edit_member",
+ });
+};
+
+const handleCancelModerators = () => {
+ setEditModerators(false);
+ setModerators(accessControlInfo.members_list["team:moderators"].children);
+};
+
+return (
+ <>
+
Moderators
+
+
+ The moderator group has permissions to create and edit RFPs, edit and
+ manage proposals, and manage admins.
+
+ setEditModerators(!editModerators),
+ testId: "edit-members",
+ }}
+ />
+
+
+ {editModerators ? (
+ <>
+ true,
+ invalidate: () => null,
+ }}
+ />
+
+
+
+
+ >
+ ) : (
+ <>
+ Members
+ {moderators && (
+
+ {moderators.length ? (
+ moderators.map((child) => (
+
+
+
+ ))
+ ) : (
+
No moderators
+ )}
+
+ )}
+ >
+ )}
+
+ >
+);
diff --git a/instances/infrastructure-committee.near/widget/components/core/lib/contract.jsx b/instances/infrastructure-committee.near/widget/components/core/lib/contract.jsx
new file mode 100644
index 000000000..35a1510d5
--- /dev/null
+++ b/instances/infrastructure-committee.near/widget/components/core/lib/contract.jsx
@@ -0,0 +1,24 @@
+function ensureOtherIsLast(arr) {
+ const otherIndex = (arr ?? []).findIndex((item) => item.value === "Other");
+
+ if (otherIndex !== -1) {
+ const [otherItem] = arr.splice(otherIndex, 1);
+ arr.push(otherItem);
+ }
+ return arr;
+}
+
+function getGlobalLabels() {
+ let labels = Near.view(
+ "${REPL_INFRASTRUCTURE_COMMITTEE_CONTRACT}",
+ "get_global_labels"
+ );
+ if (labels !== null) {
+ labels = ensureOtherIsLast(labels);
+ }
+ return labels ?? null;
+}
+
+return {
+ getGlobalLabels,
+};
diff --git a/instances/infrastructure-committee.near/widget/components/molecule/AccountInput.jsx b/instances/infrastructure-committee.near/widget/components/molecule/AccountInput.jsx
new file mode 100644
index 000000000..1e821eb56
--- /dev/null
+++ b/instances/infrastructure-committee.near/widget/components/molecule/AccountInput.jsx
@@ -0,0 +1,75 @@
+const value = props.value;
+const placeholder = props.placeholder;
+const onUpdate = props.onUpdate;
+
+const [account, setAccount] = useState(value);
+const [showAccountAutocomplete, setAutoComplete] = useState(false);
+const [isValidAccount, setValidAccount] = useState(true);
+const AutoComplete = styled.div`
+ margin-top: 1rem;
+`;
+
+useEffect(() => {
+ if (value !== account) {
+ setAccount(value);
+ }
+}, [value]);
+
+useEffect(() => {
+ if (value !== account) {
+ onUpdate(account);
+ }
+}, [account]);
+
+useEffect(() => {
+ const handler = setTimeout(() => {
+ const valid = account.length === 64 || (account ?? "").includes(".near");
+ setValidAccount(valid);
+ setAutoComplete(!valid);
+ }, 100);
+
+ return () => {
+ clearTimeout(handler);
+ };
+}, [account]);
+
+return (
+
+
{
+ setAccount(e.target.value);
+ },
+ skipPaddingGap: true,
+ placeholder: placeholder,
+ inputProps: {
+ max: 64,
+ prefix: "@",
+ },
+ }}
+ />
+ {account && !isValidAccount && (
+
+ Please enter valid account ID
+
+ )}
+ {showAccountAutocomplete && (
+
+ {
+ setAccount(id);
+ setAutoComplete(false);
+ },
+ onClose: () => setAutoComplete(false),
+ }}
+ />
+
+ )}
+
+);
diff --git a/instances/infrastructure-committee.near/widget/components/molecule/Compose.jsx b/instances/infrastructure-committee.near/widget/components/molecule/Compose.jsx
new file mode 100644
index 000000000..b6afed8a8
--- /dev/null
+++ b/instances/infrastructure-committee.near/widget/components/molecule/Compose.jsx
@@ -0,0 +1,118 @@
+const EmbeddCSS = `
+ .CodeMirror {
+ margin-inline:10px;
+ border-radius:5px;
+ }
+
+ .editor-toolbar {
+ border: none !important;
+ }
+`;
+
+const Wrapper = styled.div`
+ .nav-link {
+ color: inherit !important;
+ }
+
+ .card-header {
+ padding-bottom: 0px !important;
+ }
+`;
+
+const Compose = ({
+ data,
+ onChange,
+ autocompleteEnabled,
+ placeholder,
+ height,
+ embeddCSS,
+ showProposalIdAutoComplete,
+ onChangeKeyup,
+ handler,
+}) => {
+ State.init({
+ data: data,
+ selectedTab: "editor",
+ autoFocus: false,
+ });
+
+ useEffect(() => {
+ if (typeof onChange === "function") {
+ onChange(state.data);
+ }
+ }, [state.data]);
+
+ useEffect(() => {
+ // for clearing editor after txn approval/ showing draft state
+ if (data !== state.data || handler !== state.handler) {
+ State.update({ data: data, handler: handler });
+ }
+ }, [data, handler]);
+
+ return (
+
+
+
+
+
+
+
+ State.update({ selectedTab: "editor", autoFocus: true })
+ }
+ >
+ Write
+
+
+
+ State.update({ selectedTab: "preview" })}
+ >
+ Preview
+
+
+
+
+
+
+ {state.selectedTab === "editor" ? (
+ <>
+
{
+ State.update({ data: content, handler: "update" });
+ },
+ placeholder: placeholder,
+ height,
+ embeddCSS: embeddCSS || EmbeddCSS,
+ showAutoComplete: autocompleteEnabled,
+ showProposalIdAutoComplete: showProposalIdAutoComplete,
+ autoFocus: state.autoFocus,
+ onChangeKeyup: onChangeKeyup,
+ }}
+ />
+ >
+ ) : (
+
+
+
+ )}
+
+
+ );
+};
+
+return Compose(props);
diff --git a/instances/infrastructure-committee.near/widget/components/molecule/ComposeComment.jsx b/instances/infrastructure-committee.near/widget/components/molecule/ComposeComment.jsx
new file mode 100644
index 000000000..b69786402
--- /dev/null
+++ b/instances/infrastructure-committee.near/widget/components/molecule/ComposeComment.jsx
@@ -0,0 +1,265 @@
+const proposalId = props.proposalId;
+const rfpId = props.rfpId;
+const draftKey = "INFRA_COMMENT_DRAFT" + proposalId;
+let draftComment = "";
+
+const ComposeEmbeddCSS = `
+ .CodeMirror {
+ border: none !important;
+ min-height: 50px !important;
+ }
+
+ .editor-toolbar {
+ border: none !important;
+ }
+
+ .CodeMirror-scroll{
+ min-height: 50px !important;
+ max-height: 300px !important;
+ }
+`;
+
+const notifyAccountIds = props.notifyAccountIds ?? [];
+const accountId = context.accountId;
+const item = props.item;
+const [allowGetDraft, setAllowGetDraft] = useState(true);
+const [comment, setComment] = useState(null);
+const [isTxnCreated, setTxnCreated] = useState(false);
+const [handler, setHandler] = useState("update"); // to update editor state on draft and txn approval
+const [showCommentToast, setCommentToast] = useState(false);
+
+if (allowGetDraft) {
+ draftComment = Storage.privateGet(draftKey);
+}
+
+useEffect(() => {
+ if (draftComment) {
+ setComment(draftComment);
+ setAllowGetDraft(false);
+ setHandler("refreshEditor");
+ }
+}, [draftComment]);
+
+useEffect(() => {
+ if (draftComment === comment) {
+ return;
+ }
+ const handler = setTimeout(() => {
+ Storage.privateSet(draftKey, comment);
+ }, 1000);
+
+ return () => {
+ clearTimeout(handler);
+ };
+}, [comment]);
+
+useEffect(() => {
+ if (handler === "update") {
+ return;
+ }
+ const handler = setTimeout(() => {
+ setHandler("update");
+ }, 3000);
+
+ return () => {
+ clearTimeout(handler);
+ };
+}, [handler]);
+
+if (!accountId) {
+ return (
+
+ );
+}
+
+function extractMentions(text) {
+ const mentionRegex =
+ /@((?:(?:[a-z\d]+[-_])*[a-z\d]+\.)*(?:[a-z\d]+[-_])*[a-z\d]+)/gi;
+ mentionRegex.lastIndex = 0;
+ const accountIds = new Set();
+ for (const match of text.matchAll(mentionRegex)) {
+ if (
+ !/[\w`]/.test(match.input.charAt(match.index - 1)) &&
+ !/[/\w`]/.test(match.input.charAt(match.index + match[0].length)) &&
+ match[1].length >= 2 &&
+ match[1].length <= 64
+ ) {
+ accountIds.add(match[1].toLowerCase());
+ }
+ }
+ return [...accountIds];
+}
+
+function extractTagNotifications(text, item) {
+ return extractMentions(text || "")
+ .filter((accountId) => accountId !== context.accountId)
+ .map((accountId) => ({
+ key: accountId,
+ value: {
+ type: "mention",
+ item,
+ },
+ }));
+}
+
+function composeData() {
+ setTxnCreated(true);
+ const data = {
+ post: {
+ comment: JSON.stringify({
+ type: "md",
+ text: comment,
+ item,
+ }),
+ },
+ index: {
+ comment: JSON.stringify({
+ key: item,
+ value: {
+ type: "md",
+ },
+ }),
+ },
+ };
+
+ const notifications = extractTagNotifications(comment, {
+ type: "social",
+ path: `${accountId}/post/comment`,
+ });
+
+ if (notifyAccountIds.length > 0) {
+ notifyAccountIds.map((account) => {
+ if (account !== context.accountId) {
+ notifications.push({
+ key: account,
+ value: proposalId
+ ? {
+ type: "proposal/reply",
+ item,
+ proposal: proposalId,
+ widgetAccountId: "${REPL_INFRASTRUCTURE_COMMITTEE}",
+ }
+ : {
+ type: "rfp/reply",
+ item,
+ rfp: rfpId,
+ widgetAccountId: "${REPL_INFRASTRUCTURE_COMMITTEE}",
+ },
+ });
+ }
+ });
+ }
+
+ if (notifications.length) {
+ data.index.notify = JSON.stringify(
+ notifications.length > 1 ? notifications : notifications[0]
+ );
+ }
+
+ Social.set(data, {
+ force: true,
+ onCommit: () => {
+ setCommentToast(true);
+ setComment("");
+ setHandler("refreshEditor");
+ setTxnCreated(false);
+ },
+ onCancel: () => {
+ setTxnCreated(false);
+ },
+ });
+}
+
+useEffect(() => {
+ if (props.transactionHashes && comment) {
+ setComment("");
+ }
+}, [props.transactionHashes]);
+
+const LoadingButtonSpinner = (
+
+);
+
+const Compose = useMemo(() => {
+ return (
+
+ );
+}, [draftComment, handler]);
+
+return (
+
+
setCommentToast(v),
+ trigger: <>>,
+ providerProps: { duration: 3000 },
+ }}
+ />
+
+
+
Add a comment
+ {Compose}
+
+ {
+ composeData();
+ },
+ }}
+ />
+
+
+
+);
diff --git a/instances/infrastructure-committee.near/widget/components/molecule/DropDown.jsx b/instances/infrastructure-committee.near/widget/components/molecule/DropDown.jsx
new file mode 100644
index 000000000..23a1bb819
--- /dev/null
+++ b/instances/infrastructure-committee.near/widget/components/molecule/DropDown.jsx
@@ -0,0 +1,66 @@
+const options = props.options; // [{label:"",value:""}]
+const label = props.label;
+const onUpdate = props.onUpdate ?? (() => {});
+const selectedValue = props.selectedValue;
+const [selected, setSelected] = useState(selectedValue);
+
+useEffect(() => {
+ if (JSON.stringify(selectedValue) !== JSON.stringify(selected)) {
+ setSelected(selectedValue);
+ }
+}, [selectedValue]);
+
+const StyledDropdown = styled.div`
+ .drop-btn {
+ width: 100%;
+ max-width: 200px;
+ text-align: left;
+ padding-inline: 10px;
+ }
+
+ .dropdown-item.active,
+ .dropdown-item:active {
+ background-color: #f0f0f0 !important;
+ color: black;
+ }
+
+ .cursor-pointer {
+ cursor: pointer;
+ }
+`;
+
+useEffect(() => {
+ onUpdate(selected);
+}, [selected]);
+
+return (
+
+
+
+
+ {label} {selected && label && ": "} {selected.label}
+
+
+ {options.map((item) => (
+ {
+ if (selected.label !== item.label) {
+ setSelected(item);
+ }
+ }}
+ >
+ {item.label}
+
+ ))}
+
+
+
+
+);
diff --git a/instances/infrastructure-committee.near/widget/components/molecule/DropDownWithSearch.jsx b/instances/infrastructure-committee.near/widget/components/molecule/DropDownWithSearch.jsx
new file mode 100644
index 000000000..c4365a553
--- /dev/null
+++ b/instances/infrastructure-committee.near/widget/components/molecule/DropDownWithSearch.jsx
@@ -0,0 +1,193 @@
+const {
+ selectedValue,
+ onChange,
+ options,
+ defaultLabel,
+ showSearch,
+ searchInputPlaceholder,
+ searchByLabel,
+ searchByValue,
+ onSearch,
+ disabled,
+} = props;
+
+const [searchTerm, setSearchTerm] = useState("");
+const [filteredOptions, setFilteredOptions] = useState(options);
+const [isOpen, setIsOpen] = useState(false);
+const [selectedOption, setSelectedOption] = useState({
+ label:
+ options?.find((item) => item.value === selectedValue)?.label ??
+ defaultLabel,
+ value: defaultLabel,
+});
+
+useEffect(() => {
+ if (selectedOption.value !== selectedValue) {
+ setSelectedOption({
+ label:
+ options?.find((item) => item.value === selectedValue)?.label ??
+ defaultLabel,
+ value: defaultLabel,
+ });
+ }
+}, [selectedValue]);
+
+useEffect(() => {
+ setFilteredOptions(options);
+}, [options]);
+
+const handleSearch = (event) => {
+ const term = event.target.value.toLowerCase();
+ setSearchTerm(term);
+ if (typeof onSearch === "function") {
+ onSearch(term);
+ return;
+ }
+
+ const filteredOptions = options.filter((option) => {
+ if (searchByLabel) {
+ return option.label.toLowerCase().includes(term);
+ }
+ if (searchByValue) {
+ return option.value.toString().toLowerCase().includes(term);
+ }
+ });
+
+ setFilteredOptions(filteredOptions);
+};
+
+const toggleDropdown = () => {
+ setIsOpen(!isOpen);
+};
+
+const handleOptionClick = (option) => {
+ setSelectedOption(option);
+ setIsOpen(false);
+ onChange(option);
+};
+
+const Container = styled.div`
+ .drop-btn {
+ width: 100%;
+ text-align: left;
+ padding-inline: 10px;
+ }
+
+ .dropdown-toggle:after {
+ position: absolute;
+ top: 46%;
+ right: 5%;
+ }
+
+ .dropdown-menu {
+ width: 100%;
+ }
+
+ .dropdown-item.active,
+ .dropdown-item:active {
+ background-color: #f0f0f0 !important;
+ color: black;
+ }
+
+ .custom-select {
+ position: relative;
+ }
+
+ .scroll-box {
+ max-height: 200px;
+ overflow-y: scroll;
+ }
+
+ .selected {
+ background-color: #f0f0f0;
+ }
+
+ input {
+ background-color: #f8f9fa;
+ }
+
+ .cursor-pointer {
+ cursor: pointer;
+ }
+
+ .text-wrap {
+ overflow: hidden;
+ white-space: normal;
+ }
+
+ .disabled {
+ background-color: #f8f8f8 !important;
+ cursor: not-allowed !important;
+ border-radius: 5px;
+ opacity: inherit !important;
+ }
+
+ .disabled.dropdown-toggle::after {
+ display: none !important;
+ }
+`;
+let searchFocused = false;
+return (
+
+ {
+ setTimeout(() => {
+ setIsOpen(searchFocused || false);
+ }, 0);
+ }}
+ >
+
+
+ {selectedOption.label}
+
+
+
+ {isOpen && (
+
+ {showSearch && (
+
{
+ searchFocused = true;
+ }}
+ onBlur={() => {
+ setTimeout(() => {
+ searchFocused = false;
+ }, 0);
+ }}
+ />
+ )}
+
+ {filteredOptions.map((option) => (
+
handleOptionClick(option)}
+ >
+ {option.label}
+
+ ))}
+
+
+ )}
+
+
+);
diff --git a/instances/infrastructure-committee.near/widget/components/molecule/FilterByLabel.jsx b/instances/infrastructure-committee.near/widget/components/molecule/FilterByLabel.jsx
new file mode 100644
index 000000000..35eb464b6
--- /dev/null
+++ b/instances/infrastructure-committee.near/widget/components/molecule/FilterByLabel.jsx
@@ -0,0 +1,22 @@
+const availableOptions = props.availableOptions;
+const options =
+ (availableOptions ?? []).map((i) => {
+ return { label: i.title, value: i.value };
+ }) ?? [];
+options.unshift({ label: "All", value: null });
+const setSelected = props.onStateChange ?? (() => {});
+
+return (
+
+ {
+ setSelected(v);
+ },
+ }}
+ />
+
+);
diff --git a/instances/infrastructure-committee.near/widget/components/molecule/LikeButton.jsx b/instances/infrastructure-committee.near/widget/components/molecule/LikeButton.jsx
new file mode 100644
index 000000000..02949f0e1
--- /dev/null
+++ b/instances/infrastructure-committee.near/widget/components/molecule/LikeButton.jsx
@@ -0,0 +1,148 @@
+const item = props.item;
+const proposalId = props.proposalId;
+const rfpId = props.rfpId;
+const notifyAccountIds = props.notifyAccountIds ?? [];
+if (!item) {
+ return "";
+}
+
+const likes = Social.index("like", item);
+
+const dataLoading = likes === null;
+
+const likesByUsers = {};
+
+(likes || []).forEach((like) => {
+ if (like.value.type === "like") {
+ likesByUsers[like.accountId] = like;
+ } else if (like.value.type === "unlike") {
+ delete likesByUsers[like.accountId];
+ }
+});
+if (state.hasLike === true) {
+ likesByUsers[context.accountId] = {
+ accountId: context.accountId,
+ };
+} else if (state.hasLike === false) {
+ delete likesByUsers[context.accountId];
+}
+
+const accountsWithLikes = Object.keys(likesByUsers);
+const hasLike = context.accountId && !!likesByUsers[context.accountId];
+const hasLikeOptimistic =
+ state.hasLikeOptimistic === undefined ? hasLike : state.hasLikeOptimistic;
+const totalLikes =
+ accountsWithLikes.length +
+ (hasLike === false && state.hasLikeOptimistic === true ? 1 : 0) -
+ (hasLike === true && state.hasLikeOptimistic === false ? 1 : 0);
+
+const LikeButton = styled.button`
+ border: 0;
+ display: inline-flex;
+ align-items: center;
+ gap: 6px;
+ color: #687076;
+ font-weight: 400;
+ font-size: 14px;
+ line-height: 17px;
+ cursor: pointer;
+ background: none;
+ padding: 6px;
+ transition: color 200ms;
+
+ i {
+ font-size: 16px;
+ transition: color 200ms;
+
+ &.bi-heart-fill {
+ color: #e5484d !important;
+ }
+ }
+
+ &:hover,
+ &:focus {
+ outline: none;
+ color: #11181c;
+ }
+`;
+
+const likeClick = (e) => {
+ e.preventDefault();
+ e.stopPropagation();
+ if (state.loading) {
+ return;
+ }
+
+ State.update({
+ loading: true,
+ hasLikeOptimistic: !hasLike,
+ });
+
+ const data = {
+ index: {
+ like: JSON.stringify({
+ key: item,
+ value: {
+ type: hasLike ? "unlike" : "like",
+ },
+ }),
+ },
+ };
+
+ if (!hasLike && notifyAccountIds.length > 0) {
+ const notifyData = notifyAccountIds.map((account) => {
+ if (account !== context.accountId) {
+ return {
+ key: account,
+ value: proposalId
+ ? {
+ type: "proposal/like",
+ item,
+ proposal: proposalId,
+ widgetAccountId: "${REPL_INFRASTRUCTURE_COMMITTEE}",
+ }
+ : {
+ type: "rfp/like",
+ item,
+ rfp: rfpId,
+ widgetAccountId: "${REPL_INFRASTRUCTURE_COMMITTEE}",
+ },
+ };
+ }
+ });
+ if (notifyData.length > 0) {
+ data.index.notify = notifyData;
+ }
+ }
+ Social.set(data, {
+ onCommit: () => State.update({ loading: false, hasLike: !hasLike }),
+ onCancel: () =>
+ State.update({
+ loading: false,
+ hasLikeOptimistic: !state.hasLikeOptimistic,
+ }),
+ });
+};
+
+const title = hasLike ? "Unlike" : "Like";
+
+return (
+
+
+ {Object.values(likesByUsers ?? {}).length > 0 ? (
+
+
+
+ ) : (
+ "0"
+ )}
+
+);
diff --git a/instances/infrastructure-committee.near/widget/components/molecule/LinkedProposals.jsx b/instances/infrastructure-committee.near/widget/components/molecule/LinkedProposals.jsx
new file mode 100644
index 000000000..c08ca6b65
--- /dev/null
+++ b/instances/infrastructure-committee.near/widget/components/molecule/LinkedProposals.jsx
@@ -0,0 +1,78 @@
+const { href } = VM.require(`${REPL_DEVHUB}/widget/core.lib.url`) || {
+ href: () => {},
+};
+
+const { readableDate } = VM.require(
+ `${REPL_DEVHUB}/widget/core.lib.common`
+) || { readableDate: () => {} };
+
+const linkedProposalIds = props.linkedProposalIds ?? [];
+const linkedProposalsData = [];
+const showStatus = props.showStatus ?? false;
+
+// using contract instead of indexer, since indexer doesn't return timestamp
+linkedProposalIds.map((item) => {
+ const data = Near.view(
+ "${REPL_INFRASTRUCTURE_COMMITTEE_CONTRACT}",
+ "get_proposal",
+ {
+ proposal_id: item,
+ }
+ );
+ if (data !== null) {
+ linkedProposalsData.push(data);
+ }
+});
+
+const Container = styled.div`
+ a {
+ &:hover {
+ text-decoration: none !important;
+ }
+ }
+`;
+
+return (
+
+ {linkedProposalsData.map((item) => {
+ return (
+
+
+
+
+
{item.snapshot.name}
+
+ created on {readableDate(item.snapshot.timestamp / 1000000)}
+
+ {showStatus && (
+
+
+
+ )}
+
+
+
+ );
+ })}
+
+);
diff --git a/instances/infrastructure-committee.near/widget/components/molecule/LinkedProposalsDropdown.jsx b/instances/infrastructure-committee.near/widget/components/molecule/LinkedProposalsDropdown.jsx
new file mode 100644
index 000000000..b7252f090
--- /dev/null
+++ b/instances/infrastructure-committee.near/widget/components/molecule/LinkedProposalsDropdown.jsx
@@ -0,0 +1,155 @@
+const { fetchGraphQL } = VM.require(
+ `${REPL_INFRASTRUCTURE_COMMITTEE}/widget/core.common`
+) || { fetchGraphQL: () => {} };
+
+const { href } = VM.require(`${REPL_DEVHUB}/widget/core.lib.url`);
+href || (href = () => {});
+
+const linkedProposals = props.linkedProposals;
+const onChange = props.onChange;
+const [selectedProposals, setSelectedProposals] = useState(linkedProposals);
+const [proposalsOptions, setProposalsOptions] = useState([]);
+const [searchProposalId, setSearchProposalId] = useState("");
+
+const queryName = "${REPL_PROPOSAL_FEED_INDEXER_QUERY_NAME}";
+const query = `query GetLatestSnapshot($offset: Int = 0, $limit: Int = 10, $where: ${queryName}_bool_exp = {}) {
+${queryName}(
+ offset: $offset
+ limit: $limit
+ order_by: {proposal_id: desc}
+ where: $where
+) {
+ name
+ proposal_id
+}
+}`;
+
+useEffect(() => {
+ if (JSON.stringify(linkedProposals) !== JSON.stringify(selectedProposals)) {
+ setSelectedProposals(linkedProposals);
+ }
+}, [linkedProposals]);
+
+useEffect(() => {
+ if (JSON.stringify(linkedProposals) !== JSON.stringify(selectedProposals)) {
+ onChange(selectedProposals);
+ }
+}, [selectedProposals]);
+
+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 = {};
+ const { number, text } = separateNumberAndText(searchProposalId);
+
+ 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 fetchProposals = () => {
+ const FETCH_LIMIT = 30;
+ const variables = {
+ limit: FETCH_LIMIT,
+ offset: 0,
+ where: buildWhereClause(),
+ };
+ fetchGraphQL(query, "GetLatestSnapshot", variables).then(async (result) => {
+ if (result.status === 200) {
+ if (result.body.data) {
+ const proposalsData = result.body.data?.[queryName];
+ const data = [];
+ for (const prop of proposalsData) {
+ data.push({
+ label: "# " + prop.proposal_id + " : " + prop.name,
+ value: prop.proposal_id,
+ });
+ }
+ setProposalsOptions(data);
+ }
+ }
+ });
+};
+
+useEffect(() => {
+ fetchProposals();
+}, [searchProposalId]);
+
+return (
+ <>
+ {selectedProposals.map((proposal) => {
+ return (
+
+
+ {proposal.label}
+
+
{
+ const updatedLinkedProposals = selectedProposals.filter(
+ (item) => item.value !== proposal.value
+ );
+ setSelectedProposals(updatedLinkedProposals);
+ }}
+ >
+
+
+
+ );
+ })}
+
+
{
+ if (!selectedProposals.some((item) => item.value === v.value)) {
+ setSelectedProposals([...selectedProposals, v]);
+ }
+ },
+ options: proposalsOptions,
+ showSearch: true,
+ searchInputPlaceholder: "Search by Id",
+ defaultLabel: "Search proposals",
+ searchByValue: true,
+ onSearch: (value) => {
+ setSearchProposalId(value);
+ },
+ }}
+ />
+ >
+);
diff --git a/instances/infrastructure-committee.near/widget/components/molecule/LinkedRfpDropdown.jsx b/instances/infrastructure-committee.near/widget/components/molecule/LinkedRfpDropdown.jsx
new file mode 100644
index 000000000..eca60224d
--- /dev/null
+++ b/instances/infrastructure-committee.near/widget/components/molecule/LinkedRfpDropdown.jsx
@@ -0,0 +1,185 @@
+const { RFP_TIMELINE_STATUS, fetchGraphQL, parseJSON } = VM.require(
+ `${REPL_INFRASTRUCTURE_COMMITTEE}/widget/core.common`
+) || { RFP_TIMELINE_STATUS: {}, fetchGraphQL: () => {}, parseJSON: () => {} };
+const { href } = VM.require(`${REPL_DEVHUB}/widget/core.lib.url`);
+href || (href = () => {});
+
+const { linkedRfp, onChange, disabled, onDeleteRfp } = props;
+
+const isModerator = Near.view(
+ "${REPL_INFRASTRUCTURE_COMMITTEE_CONTRACT}",
+ "is_allowed_to_write_rfps",
+ {
+ editor: context.accountId,
+ }
+);
+
+const [selectedRFP, setSelectedRFP] = useState(null);
+const [acceptingRfpsOptions, setAcceptingRfpsOption] = useState([]);
+const [allRfpOptions, setAllRfpOptions] = useState([]);
+const [searchRFPId, setSearchRfpId] = useState("");
+const [initialStateApplied, setInitialState] = useState(false);
+
+const queryName = "${REPL_RFP_FEED_INDEXER_QUERY_NAME}";
+const query = `query GetLatestSnapshot($offset: Int = 0, $limit: Int = 10, $where: ${queryName}_bool_exp = {}) {
+ ${queryName}(
+ offset: $offset
+ limit: $limit
+ order_by: {rfp_id: desc}
+ where: $where
+ ) {
+ name
+ rfp_id
+ timeline
+ }
+ }`;
+
+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 = () => {
+ // show only accepting submissions stage rfps
+ let where = {};
+ const { number, text } = separateNumberAndText(searchRFPId);
+
+ if (number) {
+ where = { rfp_id: { _eq: number }, ...where };
+ }
+
+ if (text) {
+ where = {
+ _or: [
+ { name: { _iregex: `${text}` } },
+ { summary: { _iregex: `${text}` } },
+ { description: { _iregex: `${text}` } },
+ ],
+ ...where,
+ };
+ }
+
+ return where;
+};
+
+const fetchRfps = () => {
+ const FETCH_LIMIT = 30;
+ const variables = {
+ limit: FETCH_LIMIT,
+ offset: 0,
+ where: buildWhereClause(),
+ };
+ fetchGraphQL(query, "GetLatestSnapshot", variables).then(async (result) => {
+ if (result.status === 200) {
+ if (result.body.data) {
+ const rfpsData = result.body.data?.[queryName];
+ const data = [];
+ const acceptingData = [];
+ for (const prop of rfpsData) {
+ const timeline = parseJSON(prop.timeline);
+ const label = "# " + prop.rfp_id + " : " + prop.name;
+ const value = prop.rfp_id;
+ if (timeline.status === RFP_TIMELINE_STATUS.ACCEPTING_SUBMISSIONS) {
+ acceptingData.push({
+ label,
+ value,
+ });
+ }
+ data.push({
+ label,
+ value,
+ });
+ }
+ setAcceptingRfpsOption(acceptingData);
+ setAllRfpOptions(data);
+ }
+ }
+ });
+};
+
+useEffect(() => {
+ fetchRfps();
+}, [searchRFPId]);
+
+useEffect(() => {
+ if (JSON.stringify(linkedRfp) !== JSON.stringify(selectedRFP)) {
+ if (allRfpOptions.length > 0) {
+ if (typeof linkedRfp !== "object") {
+ setSelectedRFP(allRfpOptions.find((i) => linkedRfp === i.value));
+ } else {
+ setSelectedRFP(linkedRfp);
+ }
+ setInitialState(true);
+ }
+ } else {
+ setInitialState(true);
+ }
+}, [linkedRfp, allRfpOptions]);
+
+useEffect(() => {
+ if (
+ JSON.stringify(linkedRfp) !== JSON.stringify(selectedRFP) &&
+ initialStateApplied
+ ) {
+ onChange(selectedRFP);
+ }
+}, [selectedRFP, initialStateApplied]);
+
+return (
+ <>
+ {selectedRFP && (
+
+ )}
+ {
+ setSelectedRFP(v);
+ },
+ options: isModerator ? allRfpOptions : acceptingRfpsOptions,
+ showSearch: true,
+ searchInputPlaceholder: "Search by Id",
+ defaultLabel: "Search RFP",
+ searchByValue: true,
+ onSearch: (value) => {
+ setSearchRfpId(value);
+ },
+ }}
+ />
+ >
+);
diff --git a/instances/infrastructure-committee.near/widget/components/molecule/LinkedRfps.jsx b/instances/infrastructure-committee.near/widget/components/molecule/LinkedRfps.jsx
new file mode 100644
index 000000000..777162356
--- /dev/null
+++ b/instances/infrastructure-committee.near/widget/components/molecule/LinkedRfps.jsx
@@ -0,0 +1,61 @@
+const { readableDate } = VM.require(
+ `${REPL_DEVHUB}/widget/core.lib.common`
+) || { readableDate: () => {} };
+
+const { href } = VM.require(`${REPL_DEVHUB}/widget/core.lib.url`) || {
+ href: () => {},
+};
+
+const linkedRfpIds = props.linkedRfpIds ?? [];
+const linkedRfpsData = [];
+
+linkedRfpIds.map((item) => {
+ const data = Near.view(
+ "${REPL_INFRASTRUCTURE_COMMITTEE_CONTRACT}",
+ "get_rfp",
+ {
+ rfp_id: item,
+ }
+ );
+ if (data !== null) {
+ linkedRfpsData.push(data);
+ }
+});
+
+const Container = styled.div`
+ a {
+ &:hover {
+ text-decoration: none !important;
+ }
+ }
+`;
+
+return (
+
+ {linkedRfpsData.map((item) => {
+ return (
+
+
+
+
+
{item.snapshot.name}
+
+ created on {readableDate(item.snapshot.timestamp / 1000000)}
+
+
+
+
+ );
+ })}
+
+);
diff --git a/instances/infrastructure-committee.near/widget/components/molecule/Markdown.jsx b/instances/infrastructure-committee.near/widget/components/molecule/Markdown.jsx
new file mode 100644
index 000000000..26d9f4c6c
--- /dev/null
+++ b/instances/infrastructure-committee.near/widget/components/molecule/Markdown.jsx
@@ -0,0 +1,38 @@
+const Container = styled.div`
+ p {
+ white-space: pre-line; // This ensures text breaks to new line
+
+ span {
+ white-space: normal; // and this ensures profile links look normal
+ }
+ }
+
+ blockquote {
+ margin: 1em 0;
+ padding-left: 1.5em;
+ border-left: 4px solid #ccc;
+ color: #666;
+ font-style: italic;
+ font-size: inherit;
+ }
+
+ pre {
+ background-color: #f4f4f4;
+ border: 1px solid #ddd;
+ border-radius: 4px;
+ padding: 1em;
+ overflow-x: auto;
+ font-family: "Courier New", Courier, monospace;
+ }
+
+ a {
+ color: #3c697d;
+ font-weight: 500 !important;
+ }
+`;
+
+return (
+
+
+
+);
diff --git a/instances/infrastructure-committee.near/widget/components/molecule/MultiSelectCategoryDropdown.jsx b/instances/infrastructure-committee.near/widget/components/molecule/MultiSelectCategoryDropdown.jsx
new file mode 100644
index 000000000..358984ee7
--- /dev/null
+++ b/instances/infrastructure-committee.near/widget/components/molecule/MultiSelectCategoryDropdown.jsx
@@ -0,0 +1,193 @@
+const { href } = VM.require(`${REPL_DEVHUB}/widget/core.lib.url`);
+href || (href = () => {});
+
+const {
+ selected,
+ onChange,
+ disabled,
+ availableOptions,
+ hideDropdown,
+ linkedRfp,
+} = props;
+
+const [selectedOptions, setSelectedOptions] = useState([]);
+const [isOpen, setIsOpen] = useState(false);
+const [initialStateApplied, setInitialState] = useState(false);
+
+const toggleDropdown = () => {
+ setIsOpen(!isOpen);
+};
+
+useEffect(() => {
+ if (JSON.stringify(selectedOptions) !== JSON.stringify(selected)) {
+ if (availableOptions.length > 0) {
+ if ((selected ?? []).some((i) => !i.value)) {
+ setSelectedOptions(
+ selected.map((i) => availableOptions.find((t) => t.value === i))
+ );
+ } else {
+ setSelectedOptions(selected);
+ }
+ setInitialState(true);
+ }
+ } else {
+ setInitialState(true);
+ }
+}, [selected, availableOptions]);
+
+useEffect(() => {
+ if (
+ JSON.stringify(selectedOptions) !== JSON.stringify(selected) &&
+ initialStateApplied
+ ) {
+ onChange(selectedOptions);
+ }
+}, [selectedOptions, initialStateApplied]);
+
+const Container = styled.div`
+ .drop-btn {
+ width: 100%;
+ text-align: left;
+ padding-inline: 10px;
+ }
+
+ .dropdown-toggle:after {
+ position: absolute;
+ top: 46%;
+ right: 2%;
+ }
+
+ .dropdown-menu {
+ width: 100%;
+ }
+
+ .dropdown-item.active,
+ .dropdown-item:active {
+ background-color: #f0f0f0 !important;
+ color: black;
+ }
+
+ .disabled {
+ background-color: #f8f8f8 !important;
+ cursor: not-allowed !important;
+ border-radius: 5px;
+ opacity: inherit !important;
+ }
+
+ .disabled.dropdown-toggle::after {
+ display: none !important;
+ }
+
+ .custom-select {
+ position: relative;
+ }
+
+ .selected {
+ background-color: #f0f0f0;
+ }
+
+ .cursor-pointer {
+ cursor: pointer;
+ }
+
+ .text-wrap {
+ overflow: hidden;
+ white-space: normal;
+ }
+`;
+
+const handleOptionClick = (option) => {
+ if (!selectedOptions.some((item) => item.value === option.value)) {
+ setSelectedOptions([...selectedOptions, option]);
+ }
+ setIsOpen(false);
+};
+
+const Item = ({ option }) => {
+ return {option.title}
;
+};
+
+return (
+ <>
+
+ {(selectedOptions ?? []).map((option) => {
+ return (
+
+ {option.title}
+ {!disabled && (
+
{
+ const updatedOptions = selectedOptions.filter(
+ (item) => item.value !== option.value
+ );
+ setSelectedOptions(updatedOptions);
+ }}
+ >
+
+
+ )}
+
+ );
+ })}
+
+ {!hideDropdown && (
+
+ setIsOpen(false)}
+ >
+
+
+ {linkedRfp ? (
+
+
+ These categories match the chosen RFP and cannot be changed.
+ To use different categories, unlink the RFP.
+
+ ) : (
+ Select Category
+ )}
+
+
+
+ {isOpen && (
+
+
+ {(availableOptions ?? []).map((option) => (
+
item.value === option.value
+ )
+ ? "selected"
+ : ""
+ }`}
+ onClick={() => handleOptionClick(option)}
+ >
+
+
+ ))}
+
+
+ )}
+
+
+ )}
+ >
+);
diff --git a/instances/infrastructure-committee.near/widget/components/molecule/NavbarDropdown.jsx b/instances/infrastructure-committee.near/widget/components/molecule/NavbarDropdown.jsx
new file mode 100644
index 000000000..156284ccb
--- /dev/null
+++ b/instances/infrastructure-committee.near/widget/components/molecule/NavbarDropdown.jsx
@@ -0,0 +1,129 @@
+const title = props.title;
+const links = props.links;
+const href = props.href;
+
+const [showMenu, setShowMenu] = useState(false);
+
+const { href: linkHref } = VM.require(`${REPL_DEVHUB}/widget/core.lib.url`);
+
+linkHref || (linkHref = () => {});
+
+const Dropdown = styled.div`
+ position: relative;
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+
+ p {
+ &.active {
+ color: #fff;
+
+ &:hover {
+ text-decoration: none;
+ color: #096d50 !important;
+ }
+ }
+ }
+`;
+
+const DropdownMenu = styled.div`
+ z-index: 50;
+ position: absolute;
+ top: 2.25rem;
+
+ &.active {
+ padding: 0.5rem 1rem;
+ padding-top: 1rem;
+ border-radius: 1rem;
+ background: rgba(217, 217, 217, 0.7);
+ backdrop-filter: blur(5px);
+ width: max-content;
+ animation: slide-down 300ms ease;
+ transform-origin: top center;
+ }
+
+ @keyframes slide-down {
+ 0% {
+ transform: scaleY(0);
+ }
+ 100% {
+ transform: scaleY(1);
+ }
+ }
+`;
+
+const DropdownLink = styled.div`
+ color: inherit;
+ text-decoration: none;
+
+ &.active {
+ color: #555555;
+ }
+
+ &:hover {
+ text-decoration: none;
+ color: #096d50 !important;
+ }
+`;
+
+return (
+ setShowMenu(true)}
+ onMouseLeave={() => setShowMenu(false)}
+ >
+ {href ? (
+
+
+ {title}
+
+
+ ) : (
+
+ {title} ↓
+
+ )}
+ {showMenu && links.length !== 0 && (
+
+
+ {links.map((link) => (
+ // Check if the link is external
+
+ {link.href.startsWith("http://") ||
+ link.href.startsWith("https://") ? (
+ // External link: Render an tag
+
+ {link.title}
+
+ ) : (
+ // Internal link: Render the component
+
+ {link.title}
+
+ )}
+
+ ))}
+
+
+ )}
+
+);
diff --git a/instances/infrastructure-committee.near/widget/components/molecule/RadioButton.jsx b/instances/infrastructure-committee.near/widget/components/molecule/RadioButton.jsx
new file mode 100644
index 000000000..b08dcd10d
--- /dev/null
+++ b/instances/infrastructure-committee.near/widget/components/molecule/RadioButton.jsx
@@ -0,0 +1,29 @@
+const RadioButton = ({ value, isChecked, label, onClick, disabled }) => {
+ const [checked, setChecked] = useState(isChecked);
+
+ useEffect(() => {
+ if (isChecked !== checked) {
+ setChecked(isChecked);
+ }
+ }, [isChecked]);
+
+ useEffect(() => {
+ onClick(checked);
+ }, [checked]);
+
+ return (
+
+ setChecked(e.target.checked)}
+ />
+ {label}
+
+ );
+};
+
+return RadioButton(props);
diff --git a/instances/infrastructure-committee.near/widget/components/molecule/SimpleMDE.jsx b/instances/infrastructure-committee.near/widget/components/molecule/SimpleMDE.jsx
new file mode 100644
index 000000000..a2157e055
--- /dev/null
+++ b/instances/infrastructure-committee.near/widget/components/molecule/SimpleMDE.jsx
@@ -0,0 +1,629 @@
+/**
+ * iframe embedding a SimpleMDE component
+ * https://github.com/sparksuite/simplemde-markdown-editor
+ */
+const { getLinkUsingCurrentGateway } = VM.require(
+ `${REPL_INFRASTRUCTURE_COMMITTEE}/widget/core.common`
+) || { getLinkUsingCurrentGateway: () => {} };
+const data = props.data;
+const onChange = props.onChange ?? (() => {});
+const onChangeKeyup = props.onChangeKeyup ?? (() => {}); // in case where we want immediate action
+const height = props.height ?? "390";
+const className = props.className ?? "w-100";
+const embeddCSS = props.embeddCSS;
+
+State.init({
+ iframeHeight: height,
+ message: props.data,
+});
+
+const profilesData = Social.get("*/profile/name", "final");
+const followingData = Social.get(
+ `${context.accountId}/graph/follow/**`,
+ "final"
+);
+
+// SIMPLEMDE CONFIG //
+const fontFamily = props.fontFamily ?? "sans-serif";
+const alignToolItems = props.alignToolItems ?? "right";
+const placeholder = props.placeholder ?? "";
+const showAccountAutoComplete = props.showAutoComplete ?? false;
+const showProposalIdAutoComplete = props.showProposalIdAutoComplete ?? false;
+const showRfpIdAutoComplete = props.showRfpIdAutoComplete ?? false;
+const autoFocus = props.autoFocus ?? false;
+
+const proposalQueryName = "${REPL_PROPOSAL_FEED_INDEXER_QUERY_NAME}";
+const proposalLink = getLinkUsingCurrentGateway(
+ `${REPL_INFRASTRUCTURE_COMMITTEE}/widget/app?page=proposal&id=`
+);
+const proposalQuery = `query GetLatestSnapshot($offset: Int = 0, $limit: Int = 10, $where: ${proposalQueryName}_bool_exp = {}) {
+${proposalQueryName}(
+ offset: $offset
+ limit: $limit
+ order_by: {proposal_id: desc}
+ where: $where
+) {
+ name
+ proposal_id
+}
+}`;
+
+const rfpQueryName = "${REPL_RFP_FEED_INDEXER_QUERY_NAME}";
+const rfpLink = getLinkUsingCurrentGateway(
+ `${REPL_INFRASTRUCTURE_COMMITTEE}/widget/app?page=rfp&id=`
+);
+const rfpQuery = `query GetLatestSnapshot($offset: Int = 0, $limit: Int = 10, $where: ${rfpQueryName}_bool_exp = {}) {
+${rfpQueryName}(
+ offset: $offset
+ limit: $limit
+ order_by: {rfp_id: desc}
+ where: $where
+) {
+ rfp_id
+ name
+}
+}`;
+
+const code = `
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Dropdown button
+
+
+
+
+
+ Dropdown button
+
+
+
+
+
+
+
+
+
+
+
+
+`;
+
+return (
+
+
+
+);
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..8d46ae84f
--- /dev/null
+++ b/instances/infrastructure-committee.near/widget/components/proposals/Feed.jsx
@@ -0,0 +1,609 @@
+const { fetchGraphQL, parseJSON, isNumber } = VM.require(
+ `${REPL_INFRASTRUCTURE_COMMITTEE}/widget/core.common`
+) || { fetchGraphQL: () => {}, parseJSON: () => {}, isNumber: () => {} };
+
+const { href } = VM.require(`${REPL_DEVHUB}/widget/core.lib.url`);
+href || (href = () => {});
+
+const { getGlobalLabels } = VM.require(
+ `${REPL_INFRASTRUCTURE_COMMITTEE}/widget/components.core.lib.contract`
+) || { getGlobalLabels: () => {} };
+
+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;
+ }
+ }
+
+ .blue-btn {
+ background-color: #3c697d !important;
+ border: none;
+ color: white;
+
+ &:active {
+ color: white;
+ }
+ }
+
+ @media screen and (max-width: 768px) {
+ .blue-btn {
+ padding: 0.5rem 0.8rem !important;
+ min-height: 32px;
+ }
+ }
+
+ a.no-space {
+ display: inline-block;
+ }
+
+ .text-wrap {
+ overflow: hidden;
+ white-space: normal;
+ }
+
+ .bg-blue {
+ background-image: linear-gradient(to bottom, #4b7a93, #213236);
+ color: white;
+ }
+`;
+
+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 rfpLabelOptions = getGlobalLabels();
+
+const FeedItem = ({ proposal, index }) => {
+ const accountId = proposal.author_id;
+ proposal.timeline = parseJSON(proposal.timeline);
+ 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: `${REPL_INFRASTRUCTURE_COMMITTEE_CONTRACT}/post/main`,
+ blockHeight: blockHeight,
+ };
+
+ const isLinked = isNumber(proposal.linked_rfp);
+ const rfpData = proposal.rfpData;
+
+ return (
+