From e8641219765c0e4b12a069af5ba4c95d0e1b21fc Mon Sep 17 00:00:00 2001 From: Megha <100185149+Megha-Dev-19@users.noreply.github.com> Date: Tue, 10 Dec 2024 23:21:58 +0530 Subject: [PATCH] Setup new portal instance for templar (#983) * fix solution action * fix preview for proposals * setup new portal instance for templar * add indexer * changes as per indexer * fix filter * fix search filter * rename `app` to `portal` * hide about * added templar t&c * updated terms and condition * fix issues with snapshot history * skip announcement related tests * skip sunset tests --- instances/devhub.near/widget/config/data.jsx | 1 + .../devhub/entity/proposal/AcceptedTerms.jsx | 4 +- .../entity/proposal/CommentsAndLogs.jsx | 12 +- .../widget/config/data.jsx | 1 + .../widget/components/proposals/Proposal.jsx | 3 +- .../components/rfps/CommentsAndLogs.jsx | 30 +- .../widget/components/rfps/Rfp.jsx | 2 +- .../widget/config/data.jsx | 1 + instances/treasury-templar.near/.gitignore | 2 + .../aliases.mainnet.json | 10 + .../treasury-templar.near/bos.config.json | 6 + instances/treasury-templar.near/data.json | 3 + instances/treasury-templar.near/src | 1 + .../components/admin/AboutConfigurator.jsx | 185 ++ .../components/admin/AccountsEditor.jsx | 79 + .../admin/ModeratorsConfigurator.jsx | 115 ++ .../widget/components/core/lib/contract.jsx | 24 + .../components/molecule/AccountInput.jsx | 78 + .../widget/components/molecule/Compose.jsx | 120 ++ .../components/molecule/ComposeComment.jsx | 267 +++ .../widget/components/molecule/DropDown.jsx | 66 + .../components/molecule/FilterByLabel.jsx | 22 + .../widget/components/molecule/LikeButton.jsx | 148 ++ .../components/molecule/LinkedProposals.jsx | 74 + .../molecule/LinkedProposalsDropdown.jsx | 114 ++ .../components/molecule/LinkedRfpDropdown.jsx | 153 ++ .../widget/components/molecule/LinkedRfps.jsx | 57 + .../widget/components/molecule/Markdown.jsx | 38 + .../molecule/MultiSelectCategoryDropdown.jsx | 193 ++ .../components/molecule/NavbarDropdown.jsx | 129 ++ .../components/molecule/RadioButton.jsx | 29 + .../widget/components/molecule/SimpleMDE.jsx | 589 ++++++ .../widget/components/organism/Navbar.jsx | 264 +++ .../widget/components/pages/about.jsx | 27 + .../widget/components/pages/admin.jsx | 170 ++ .../components/proposals/AcceptedTerms.jsx | 15 + .../widget/components/proposals/Editor.jsx | 1323 ++++++++++++++ .../widget/components/proposals/Feed.jsx | 634 +++++++ .../widget/components/proposals/Proposal.jsx | 1424 +++++++++++++++ .../widget/components/proposals/Proposals.jsx | 8 + .../proposals/TermsAndConditions.jsx | 1584 +++++++++++++++++ .../proposals/ViewProposalModal.jsx | 161 ++ .../components/rfps/CommentsAndLogs.jsx | 487 +++++ .../components/rfps/ConfirmCancelModal.jsx | 250 +++ .../widget/components/rfps/Editor.jsx | 875 +++++++++ .../widget/components/rfps/Feed.jsx | 532 ++++++ .../widget/components/rfps/Rfp.jsx | 835 +++++++++ .../widget/components/rfps/StageDropdown.jsx | 30 + .../widget/components/rfps/StatusTag.jsx | 66 + .../components/rfps/TimelineConfigurator.jsx | 245 +++ .../widget/components/rfps/ViewRfpModal.jsx | 160 ++ .../widget/components/rfps/WarningModal.jsx | 144 ++ .../widget/components/template/AppLayout.jsx | 67 + .../widget/core/common.jsx | 79 + .../widget/portal-config/css.jsx | 23 + .../widget/portal-config/data.jsx | 13 + .../widget/portal-config/theme.jsx | 5 + .../treasury-templar.near/widget/portal.jsx | 115 ++ package.json | 2 + .../community/announcements.links.spec.js | 10 +- .../tests/community/announcements.spec.js | 8 +- playwright-tests/tests/sunset/blog.spec.js | 10 +- playwright-tests/tests/sunset/feed.spec.js | 10 +- playwright-tests/tests/sunset/search.spec.js | 4 +- 64 files changed, 12105 insertions(+), 31 deletions(-) create mode 100644 instances/treasury-templar.near/.gitignore create mode 100644 instances/treasury-templar.near/aliases.mainnet.json create mode 100644 instances/treasury-templar.near/bos.config.json create mode 100644 instances/treasury-templar.near/data.json create mode 120000 instances/treasury-templar.near/src create mode 100644 instances/treasury-templar.near/widget/components/admin/AboutConfigurator.jsx create mode 100644 instances/treasury-templar.near/widget/components/admin/AccountsEditor.jsx create mode 100644 instances/treasury-templar.near/widget/components/admin/ModeratorsConfigurator.jsx create mode 100644 instances/treasury-templar.near/widget/components/core/lib/contract.jsx create mode 100644 instances/treasury-templar.near/widget/components/molecule/AccountInput.jsx create mode 100644 instances/treasury-templar.near/widget/components/molecule/Compose.jsx create mode 100644 instances/treasury-templar.near/widget/components/molecule/ComposeComment.jsx create mode 100644 instances/treasury-templar.near/widget/components/molecule/DropDown.jsx create mode 100644 instances/treasury-templar.near/widget/components/molecule/FilterByLabel.jsx create mode 100644 instances/treasury-templar.near/widget/components/molecule/LikeButton.jsx create mode 100644 instances/treasury-templar.near/widget/components/molecule/LinkedProposals.jsx create mode 100644 instances/treasury-templar.near/widget/components/molecule/LinkedProposalsDropdown.jsx create mode 100644 instances/treasury-templar.near/widget/components/molecule/LinkedRfpDropdown.jsx create mode 100644 instances/treasury-templar.near/widget/components/molecule/LinkedRfps.jsx create mode 100644 instances/treasury-templar.near/widget/components/molecule/Markdown.jsx create mode 100644 instances/treasury-templar.near/widget/components/molecule/MultiSelectCategoryDropdown.jsx create mode 100644 instances/treasury-templar.near/widget/components/molecule/NavbarDropdown.jsx create mode 100644 instances/treasury-templar.near/widget/components/molecule/RadioButton.jsx create mode 100644 instances/treasury-templar.near/widget/components/molecule/SimpleMDE.jsx create mode 100644 instances/treasury-templar.near/widget/components/organism/Navbar.jsx create mode 100644 instances/treasury-templar.near/widget/components/pages/about.jsx create mode 100644 instances/treasury-templar.near/widget/components/pages/admin.jsx create mode 100644 instances/treasury-templar.near/widget/components/proposals/AcceptedTerms.jsx create mode 100644 instances/treasury-templar.near/widget/components/proposals/Editor.jsx create mode 100644 instances/treasury-templar.near/widget/components/proposals/Feed.jsx create mode 100644 instances/treasury-templar.near/widget/components/proposals/Proposal.jsx create mode 100644 instances/treasury-templar.near/widget/components/proposals/Proposals.jsx create mode 100644 instances/treasury-templar.near/widget/components/proposals/TermsAndConditions.jsx create mode 100644 instances/treasury-templar.near/widget/components/proposals/ViewProposalModal.jsx create mode 100644 instances/treasury-templar.near/widget/components/rfps/CommentsAndLogs.jsx create mode 100644 instances/treasury-templar.near/widget/components/rfps/ConfirmCancelModal.jsx create mode 100644 instances/treasury-templar.near/widget/components/rfps/Editor.jsx create mode 100644 instances/treasury-templar.near/widget/components/rfps/Feed.jsx create mode 100644 instances/treasury-templar.near/widget/components/rfps/Rfp.jsx create mode 100644 instances/treasury-templar.near/widget/components/rfps/StageDropdown.jsx create mode 100644 instances/treasury-templar.near/widget/components/rfps/StatusTag.jsx create mode 100644 instances/treasury-templar.near/widget/components/rfps/TimelineConfigurator.jsx create mode 100644 instances/treasury-templar.near/widget/components/rfps/ViewRfpModal.jsx create mode 100644 instances/treasury-templar.near/widget/components/rfps/WarningModal.jsx create mode 100644 instances/treasury-templar.near/widget/components/template/AppLayout.jsx create mode 100644 instances/treasury-templar.near/widget/core/common.jsx create mode 100644 instances/treasury-templar.near/widget/portal-config/css.jsx create mode 100644 instances/treasury-templar.near/widget/portal-config/data.jsx create mode 100644 instances/treasury-templar.near/widget/portal-config/theme.jsx create mode 100644 instances/treasury-templar.near/widget/portal.jsx diff --git a/instances/devhub.near/widget/config/data.jsx b/instances/devhub.near/widget/config/data.jsx index b6abb2da2..98cffd2db 100644 --- a/instances/devhub.near/widget/config/data.jsx +++ b/instances/devhub.near/widget/config/data.jsx @@ -87,6 +87,7 @@ const categoryOptions = [ ]; return { + portalName: "Devhub", contract: "devhub.near", proposalFeedIndexerQueryName: "polyprogrammist_near_devhub_prod_v1_proposals_with_latest_snapshot", diff --git a/instances/devhub.near/widget/devhub/entity/proposal/AcceptedTerms.jsx b/instances/devhub.near/widget/devhub/entity/proposal/AcceptedTerms.jsx index c91ff7f84..71bf59f97 100644 --- a/instances/devhub.near/widget/devhub/entity/proposal/AcceptedTerms.jsx +++ b/instances/devhub.near/widget/devhub/entity/proposal/AcceptedTerms.jsx @@ -8,7 +8,7 @@ State.init({ const instance = props.instance ?? ""; -const { cacheUrl } = VM.require(`${instance}/widget/config.data`); +const { cacheUrl, portalName } = VM.require(`${instance}/widget/config.data`); const fetchAndSetProposalSnapshot = () => { if (!props.proposalId) { @@ -81,6 +81,6 @@ return ( target="_blank" rel="noopener noreferrer" > - DevHub’s Terms and Conditions + {portalName}'s Terms and Conditions ); diff --git a/instances/devhub.near/widget/devhub/entity/proposal/CommentsAndLogs.jsx b/instances/devhub.near/widget/devhub/entity/proposal/CommentsAndLogs.jsx index e1e716355..42a81de09 100644 --- a/instances/devhub.near/widget/devhub/entity/proposal/CommentsAndLogs.jsx +++ b/instances/devhub.near/widget/devhub/entity/proposal/CommentsAndLogs.jsx @@ -4,6 +4,12 @@ const { getLinkUsingCurrentGateway } = VM.require( const snapshotHistory = props.snapshotHistory; const proposalId = props.id; const instanceAccount = props.item.path.split("/")[0]; +const acceptedTermsComponent = props.acceptedTermsComponent ?? ( + +); const Wrapper = styled.div` position: relative; @@ -95,6 +101,7 @@ function sortTimelineAndComments() { .map((item, index) => { const startingPoint = snapshotHistory[index]; // Set comparison to the previous item // we don't show timeline_version in logs + delete startingPoint.block_height; delete startingPoint.timeline.timeline_version; delete item.timeline.timeline_version; if ( @@ -332,10 +339,7 @@ const parseProposalKeyAndValue = (key, modifiedValue, originalValue) => { return ( accepted - + {acceptedTermsComponent} ); } diff --git a/instances/events-committee.near/widget/config/data.jsx b/instances/events-committee.near/widget/config/data.jsx index f9445cfb1..03eaf1843 100644 --- a/instances/events-committee.near/widget/config/data.jsx +++ b/instances/events-committee.near/widget/config/data.jsx @@ -42,6 +42,7 @@ const categoryOptions = [ ]; return { + portalName: "Events Committee", contract: "events-committee.near", proposalFeedIndexerQueryName: "thomasguntenaar_near_event_committee_prod_v1_proposals_with_latest_snapshot", diff --git a/instances/infrastructure-committee.near/widget/components/proposals/Proposal.jsx b/instances/infrastructure-committee.near/widget/components/proposals/Proposal.jsx index b8f0ca934..44f5878f2 100644 --- a/instances/infrastructure-committee.near/widget/components/proposals/Proposal.jsx +++ b/instances/infrastructure-committee.near/widget/components/proposals/Proposal.jsx @@ -861,8 +861,7 @@ return ( ...props, id: proposal.id, item: item, - snapshotHistory: snapshotHistory, - latestSnapshot: snapshot, + snapshotHistory: [...proposal.snapshot_history, snapshot], }} /> diff --git a/instances/infrastructure-committee.near/widget/components/rfps/CommentsAndLogs.jsx b/instances/infrastructure-committee.near/widget/components/rfps/CommentsAndLogs.jsx index 4f5e13655..2f4d491bb 100644 --- a/instances/infrastructure-committee.near/widget/components/rfps/CommentsAndLogs.jsx +++ b/instances/infrastructure-committee.near/widget/components/rfps/CommentsAndLogs.jsx @@ -92,11 +92,12 @@ State.init({ function sortTimelineAndComments() { const comments = Social.index("comment", props.item, { subscribe: true }); - if (state.changedKeysListWithValues === null) { + if (state.changedKeysListWithValues === null && snapshotHistory.length > 0) { const changedKeysListWithValues = snapshotHistory .slice(1) .map((item, index) => { const startingPoint = snapshotHistory[index]; // Set comparison to the previous item + delete startingPoint.block_height; return { editorId: item.editor_id, ...getDifferentKeysWithValues(startingPoint, item), @@ -135,9 +136,11 @@ function sortTimelineAndComments() { }); } -if (Array.isArray(snapshotHistory)) { - sortTimelineAndComments(); -} +useEffect(() => { + if (Array.isArray(snapshotHistory)) { + sortTimelineAndComments(); + } +}, [snapshotHistory]); const Comment = ({ commentItem }) => { const { accountId, blockHeight } = commentItem; @@ -288,7 +291,10 @@ function parseTimelineKeyAndValue(timeline, originalValue, modifiedValue) { const AccountProfile = ({ accountId }) => { return ( - + { ); }; +function formatDate(nanoseconds) { + const milliseconds = nanoseconds / 1_000_000; // Convert nanoseconds to milliseconds + const date = new Date(milliseconds); + + return date.toISOString().split("T")[0]; +} + const parseProposalKeyAndValue = (key, modifiedValue, originalValue) => { switch (key) { case "name": @@ -335,6 +348,13 @@ const parseProposalKeyAndValue = (key, modifiedValue, originalValue) => { return changed {key}; case "labels": return changed labels to {(modifiedValue ?? []).join(", ")}; + case "submission_deadline": + return ( + + changed submission deadline to {formatDate(modifiedValue)} to{" "} + {formatDate(originalValue)}{" "} + + ); case "linked_proposals": { const newProposals = modifiedValue || []; const oldProposals = originalValue || []; diff --git a/instances/infrastructure-committee.near/widget/components/rfps/Rfp.jsx b/instances/infrastructure-committee.near/widget/components/rfps/Rfp.jsx index b4bd6f2a7..94c676fcc 100644 --- a/instances/infrastructure-committee.near/widget/components/rfps/Rfp.jsx +++ b/instances/infrastructure-committee.near/widget/components/rfps/Rfp.jsx @@ -299,7 +299,7 @@ const fetchSnapshotHistory = () => { delete rfpData.ts; return rfpData; }); - setSnapshotHistory(history); + setSnapshotHistory([...history].reverse()); }); }; diff --git a/instances/infrastructure-committee.near/widget/config/data.jsx b/instances/infrastructure-committee.near/widget/config/data.jsx index ee263b1ca..17656ade4 100644 --- a/instances/infrastructure-committee.near/widget/config/data.jsx +++ b/instances/infrastructure-committee.near/widget/config/data.jsx @@ -59,6 +59,7 @@ const categoryOptions = [ ]; return { + portalName: "Infrastructure Committee", contract: "infrastructure-committee.near", proposalFeedIndexerQueryName: "polyprogrammist_near_devhub_ic_v1_proposals_with_latest_snapshot", diff --git a/instances/treasury-templar.near/.gitignore b/instances/treasury-templar.near/.gitignore new file mode 100644 index 000000000..9d0b71a3c --- /dev/null +++ b/instances/treasury-templar.near/.gitignore @@ -0,0 +1,2 @@ +build +dist diff --git a/instances/treasury-templar.near/aliases.mainnet.json b/instances/treasury-templar.near/aliases.mainnet.json new file mode 100644 index 000000000..34b8bf0aa --- /dev/null +++ b/instances/treasury-templar.near/aliases.mainnet.json @@ -0,0 +1,10 @@ +{ + "REPL_DEVHUB": "devhub.near", + "REPL_TREASURY_TEMPLAR": "treasury-templar.near", + "REPL_TREASURY_TEMPLAR_CONTRACT": "treasury-templar.near", + "REPL_TREASURY_BASE_DEPLOYMENT_ACCOUNT": "treasury-templar.near", + "REPL_NEAR": "near", + "REPL_RPC_URL": "https://rpc.mainnet.near.org", + "REPL_RFP_IMAGE": "https://ipfs.near.social/ipfs/bafkreicbygt4kajytlxij24jj6tkg2ppc2dw3dlqhkermkjjfgdfnlizzy", + "REPL_CACHE_URL": "https://templar-cache-api-rs.fly.dev" +} diff --git a/instances/treasury-templar.near/bos.config.json b/instances/treasury-templar.near/bos.config.json new file mode 100644 index 000000000..236e1e806 --- /dev/null +++ b/instances/treasury-templar.near/bos.config.json @@ -0,0 +1,6 @@ +{ + "account": "treasury-templar.near", + "aliasPrefix": "REPL", + "aliasesContainsPrefix": true, + "aliases": ["./aliases.mainnet.json"] +} diff --git a/instances/treasury-templar.near/data.json b/instances/treasury-templar.near/data.json new file mode 100644 index 000000000..a0a335dd1 --- /dev/null +++ b/instances/treasury-templar.near/data.json @@ -0,0 +1,3 @@ +{ + "treasury-templar.near": {} +} diff --git a/instances/treasury-templar.near/src b/instances/treasury-templar.near/src new file mode 120000 index 000000000..0301008ca --- /dev/null +++ b/instances/treasury-templar.near/src @@ -0,0 +1 @@ +widget \ No newline at end of file diff --git a/instances/treasury-templar.near/widget/components/admin/AboutConfigurator.jsx b/instances/treasury-templar.near/widget/components/admin/AboutConfigurator.jsx new file mode 100644 index 000000000..28e742f6e --- /dev/null +++ b/instances/treasury-templar.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_TREASURY_TEMPLAR_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_TREASURY_TEMPLAR_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_TREASURY_TEMPLAR_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 }, + }} + /> +
    +
  • + +
  • +
  • + +
  • +
+
+
+ { + setContent(v); + }, + showAutoComplete: true, + }} + /> + +
+ +
+
+
+
+ +
+
+
+
+); diff --git a/instances/treasury-templar.near/widget/components/admin/AccountsEditor.jsx b/instances/treasury-templar.near/widget/components/admin/AccountsEditor.jsx new file mode 100644 index 000000000..f79110866 --- /dev/null +++ b/instances/treasury-templar.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) => ( + +
+ +
+ +
+ ))} + {data.list.length < data.maxLength && ( + +
+ setNewItem(value), + value: newItem, + placeholder: data.placeholder, + }} + /> +
+ +
+ )} + +); diff --git a/instances/treasury-templar.near/widget/components/admin/ModeratorsConfigurator.jsx b/instances/treasury-templar.near/widget/components/admin/ModeratorsConfigurator.jsx new file mode 100644 index 000000000..fa20b1c78 --- /dev/null +++ b/instances/treasury-templar.near/widget/components/admin/ModeratorsConfigurator.jsx @@ -0,0 +1,115 @@ +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/treasury-templar.near/widget/components/core/lib/contract.jsx b/instances/treasury-templar.near/widget/components/core/lib/contract.jsx new file mode 100644 index 000000000..1fc260d27 --- /dev/null +++ b/instances/treasury-templar.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_TREASURY_TEMPLAR_CONTRACT}", + "get_global_labels" + ); + if (labels !== null) { + labels = ensureOtherIsLast(labels); + } + return labels ?? null; +} + +return { + getGlobalLabels, +}; diff --git a/instances/treasury-templar.near/widget/components/molecule/AccountInput.jsx b/instances/treasury-templar.near/widget/components/molecule/AccountInput.jsx new file mode 100644 index 000000000..6800ce26e --- /dev/null +++ b/instances/treasury-templar.near/widget/components/molecule/AccountInput.jsx @@ -0,0 +1,78 @@ +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") || + (account ?? "").includes(".tg"); + 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/treasury-templar.near/widget/components/molecule/Compose.jsx b/instances/treasury-templar.near/widget/components/molecule/Compose.jsx new file mode 100644 index 000000000..048858a92 --- /dev/null +++ b/instances/treasury-templar.near/widget/components/molecule/Compose.jsx @@ -0,0 +1,120 @@ +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, + sortedRelevantUsers, +}) => { + 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.selectedTab === "editor" ? ( + <> + { + State.update({ data: content, handler: "update" }); + }, + placeholder: placeholder, + height, + embeddCSS: embeddCSS || EmbeddCSS, + showAutoComplete: autocompleteEnabled, + showProposalIdAutoComplete: showProposalIdAutoComplete, + autoFocus: state.autoFocus, + onChangeKeyup: onChangeKeyup, + sortedRelevantUsers, + }} + /> + + ) : ( +
+ +
+ )} +
+
+ ); +}; + +return Compose(props); diff --git a/instances/treasury-templar.near/widget/components/molecule/ComposeComment.jsx b/instances/treasury-templar.near/widget/components/molecule/ComposeComment.jsx new file mode 100644 index 000000000..878ab39d1 --- /dev/null +++ b/instances/treasury-templar.near/widget/components/molecule/ComposeComment.jsx @@ -0,0 +1,267 @@ +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 ( +
+ + + +
to join this conversation.
+
Already have an account?
+ + Log in to comment + +
+ ); +} + +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_TREASURY_TEMPLAR}", + } + : { + type: "rfp/reply", + item, + rfp: rfpId, + widgetAccountId: "${REPL_TREASURY_TEMPLAR}", + }, + }); + } + }); + } + + if (notifications.length) { + data.index.notify = JSON.stringify( + notifications.length > 1 ? notifications : notifications[0] + ); + } + + Social.set(data, { + force: true, + onCommit: () => { + setCommentToast(true); + setComment(""); + Storage.privateSet(draftKey, ""); + setHandler("committed"); + setTxnCreated(false); + }, + onCancel: () => { + setTxnCreated(false); + }, + }); +} + +useEffect(() => { + if (props.transactionHashes && comment) { + setComment(""); + } +}, [props.transactionHashes]); + +const LoadingButtonSpinner = ( + +); + +const Compose = useMemo(() => { + return ( + + ); +}, [draftComment, handler, props.sortedRelevantUsers]); + +return ( +
+ setCommentToast(v), + trigger: <>, + providerProps: { duration: 3000 }, + }} + /> + +
+ Add a comment + {Compose} +
+ { + composeData(); + }, + }} + /> +
+
+
+); diff --git a/instances/treasury-templar.near/widget/components/molecule/DropDown.jsx b/instances/treasury-templar.near/widget/components/molecule/DropDown.jsx new file mode 100644 index 000000000..23a1bb819 --- /dev/null +++ b/instances/treasury-templar.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 ( +
+
+ + +
    + {options.map((item) => ( +
  • { + if (selected.label !== item.label) { + setSelected(item); + } + }} + > + {item.label} +
  • + ))} +
+
+
+
+); diff --git a/instances/treasury-templar.near/widget/components/molecule/FilterByLabel.jsx b/instances/treasury-templar.near/widget/components/molecule/FilterByLabel.jsx new file mode 100644 index 000000000..35eb464b6 --- /dev/null +++ b/instances/treasury-templar.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/treasury-templar.near/widget/components/molecule/LikeButton.jsx b/instances/treasury-templar.near/widget/components/molecule/LikeButton.jsx new file mode 100644 index 000000000..d23b9009b --- /dev/null +++ b/instances/treasury-templar.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_TREASURY_TEMPLAR}", + } + : { + type: "rfp/like", + item, + rfp: rfpId, + widgetAccountId: "${REPL_TREASURY_TEMPLAR}", + }, + }; + } + }); + 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/treasury-templar.near/widget/components/molecule/LinkedProposals.jsx b/instances/treasury-templar.near/widget/components/molecule/LinkedProposals.jsx new file mode 100644 index 000000000..5b0d19c6a --- /dev/null +++ b/instances/treasury-templar.near/widget/components/molecule/LinkedProposals.jsx @@ -0,0 +1,74 @@ +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_TREASURY_TEMPLAR_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/treasury-templar.near/widget/components/molecule/LinkedProposalsDropdown.jsx b/instances/treasury-templar.near/widget/components/molecule/LinkedProposalsDropdown.jsx new file mode 100644 index 000000000..ed70906d4 --- /dev/null +++ b/instances/treasury-templar.near/widget/components/molecule/LinkedProposalsDropdown.jsx @@ -0,0 +1,114 @@ +const { fetchGraphQL } = VM.require( + `${REPL_TREASURY_TEMPLAR}/widget/core.common` +); + +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(""); + +useEffect(() => { + if (JSON.stringify(linkedProposals) !== JSON.stringify(selectedProposals)) { + setSelectedProposals(linkedProposals); + } +}, [linkedProposals]); + +useEffect(() => { + if (JSON.stringify(linkedProposals) !== JSON.stringify(selectedProposals)) { + onChange(selectedProposals); + } +}, [selectedProposals]); + +function searchProposals() { + const ENDPOINT = "${REPL_CACHE_URL}"; + + let searchInput = encodeURI(searchProposalId); + let searchUrl = searchInput + ? `${ENDPOINT}/proposals/search/${searchInput}` + : `${ENDPOINT}/proposals`; + + return asyncFetch(searchUrl, { + method: "GET", + headers: { + accept: "application/json", + }, + }) + .then((result) => { + const proposalsData = result.body.records; + const data = []; + for (const prop of proposalsData) { + data.push({ + label: "# " + prop.proposal_id + " : " + prop.name, + value: prop.proposal_id, + }); + } + setProposalsOptions(data); + }) + .catch((error) => { + console.log("Error searching cache api", error); + }); +} + +useEffect(() => { + searchProposals(); +}, [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/treasury-templar.near/widget/components/molecule/LinkedRfpDropdown.jsx b/instances/treasury-templar.near/widget/components/molecule/LinkedRfpDropdown.jsx new file mode 100644 index 000000000..2056155e9 --- /dev/null +++ b/instances/treasury-templar.near/widget/components/molecule/LinkedRfpDropdown.jsx @@ -0,0 +1,153 @@ +const { RFP_TIMELINE_STATUS, fetchGraphQL, parseJSON } = VM.require( + `${REPL_TREASURY_TEMPLAR}/widget/core.common` +) || { RFP_TIMELINE_STATUS: {}, parseJSON: () => {} }; +const { href } = VM.require(`${REPL_DEVHUB}/widget/core.lib.url`); +href || (href = () => {}); + +const { linkedRfp, onChange, disabled, onDeleteRfp } = props; + +const isModerator = Near.view( + "${REPL_TREASURY_TEMPLAR_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); + +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() }; + } +} + +function searchRfps() { + const ENDPOINT = "${REPL_CACHE_URL}"; + let searchInput = encodeURI(searchRFPId); + let searchUrl = searchInput + ? `${ENDPOINT}/rfps/search/${searchInput}` + : `${ENDPOINT}/rfps`; + + return asyncFetch(searchUrl, { + method: "GET", + headers: { + accept: "application/json", + }, + }) + .then((result) => { + const rfpsData = result.body.records; + 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); + }) + .catch((error) => { + console.log("Error searching cache api", error); + }); +} + +useEffect(() => { + searchRfps(); +}, [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 && ( +
+ + {selectedRFP.label} + + {!disabled && ( +
{ + onDeleteRfp(); + setSelectedRFP(null); + }} + > + +
+ )} +
+ )} + { + 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/treasury-templar.near/widget/components/molecule/LinkedRfps.jsx b/instances/treasury-templar.near/widget/components/molecule/LinkedRfps.jsx new file mode 100644 index 000000000..850f6e387 --- /dev/null +++ b/instances/treasury-templar.near/widget/components/molecule/LinkedRfps.jsx @@ -0,0 +1,57 @@ +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_TREASURY_TEMPLAR_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/treasury-templar.near/widget/components/molecule/Markdown.jsx b/instances/treasury-templar.near/widget/components/molecule/Markdown.jsx new file mode 100644 index 000000000..020f1f0cd --- /dev/null +++ b/instances/treasury-templar.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: #8942d9; + font-weight: 500 !important; + } +`; + +return ( + + + +); diff --git a/instances/treasury-templar.near/widget/components/molecule/MultiSelectCategoryDropdown.jsx b/instances/treasury-templar.near/widget/components/molecule/MultiSelectCategoryDropdown.jsx new file mode 100644 index 000000000..358984ee7 --- /dev/null +++ b/instances/treasury-templar.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/treasury-templar.near/widget/components/molecule/NavbarDropdown.jsx b/instances/treasury-templar.near/widget/components/molecule/NavbarDropdown.jsx new file mode 100644 index 000000000..a0cd3ea56 --- /dev/null +++ b/instances/treasury-templar.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/treasury-templar.near/widget/components/molecule/RadioButton.jsx b/instances/treasury-templar.near/widget/components/molecule/RadioButton.jsx new file mode 100644 index 000000000..b08dcd10d --- /dev/null +++ b/instances/treasury-templar.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)} + /> + +
+ ); +}; + +return RadioButton(props); diff --git a/instances/treasury-templar.near/widget/components/molecule/SimpleMDE.jsx b/instances/treasury-templar.near/widget/components/molecule/SimpleMDE.jsx new file mode 100644 index 000000000..ee922e873 --- /dev/null +++ b/instances/treasury-templar.near/widget/components/molecule/SimpleMDE.jsx @@ -0,0 +1,589 @@ +/** + * iframe embedding a SimpleMDE component + * https://github.com/sparksuite/simplemde-markdown-editor + */ +const { getLinkUsingCurrentGateway } = VM.require( + `${REPL_TREASURY_TEMPLAR}/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; +const sortedRelevantUsers = props.sortedRelevantUsers || []; +const cacheUrl = "${REPL_CACHE_URL}"; +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 = false; +const autoFocus = props.autoFocus ?? false; + +const proposalLink = getLinkUsingCurrentGateway( + `${REPL_TREASURY_TEMPLAR}/widget/app?page=proposal&id=` +); + +const code = ` + + + + + + + + + + + + + + + + + + + + + + + +`; + +return ( +