From 7b9242da2bcc3fed1d43a799ac44c1910dbe7624 Mon Sep 17 00:00:00 2001 From: Megha-Dev-19 Date: Thu, 7 Dec 2023 15:42:14 +0530 Subject: [PATCH 01/14] fix solution action --- src/devhub/entity/post/PostEditor.jsx | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/devhub/entity/post/PostEditor.jsx b/src/devhub/entity/post/PostEditor.jsx index cff6067ea..4e55daefd 100644 --- a/src/devhub/entity/post/PostEditor.jsx +++ b/src/devhub/entity/post/PostEditor.jsx @@ -1,3 +1,10 @@ +const cleanDescription = (description) => { + return description ? description.replace( + /###### Requested amount: .+?\n###### Requested sponsor: @[^\s]+\n/g, + "" + ) : description +}; + initState({ seekingFunding: props.seekingFunding ?? false, author_id: context.accountId, @@ -89,13 +96,6 @@ const labels = labelStrings.map((s) => { return { name: s }; }); -const cleanDescription = (description) => { - return description.replace( - /###### Requested amount: .+?\n###### Requested sponsor: @[^\s]+\n/g, - "" - ); -}; - if (!state.draftStateApplied && props.draftState) { State.update({ ...props.draftState, draftStateApplied: true }); } From c31e5ca8c883c74f5523003fb9ef75c12718961b Mon Sep 17 00:00:00 2001 From: Megha-Dev-19 Date: Mon, 22 Apr 2024 15:21:54 +0530 Subject: [PATCH 02/14] fix preview for proposals --- src/devhub/components/molecule/Compose.jsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/devhub/components/molecule/Compose.jsx b/src/devhub/components/molecule/Compose.jsx index 059ae8cda..b681af491 100644 --- a/src/devhub/components/molecule/Compose.jsx +++ b/src/devhub/components/molecule/Compose.jsx @@ -95,7 +95,7 @@ const Compose = ({ "${REPL_DEVHUB}/widget/devhub.components.molecule.MarkdownViewer" } props={{ - text: data, + text: state.data, }} /> From be83d05ed580e0abf170b82150a8edebf866a923 Mon Sep 17 00:00:00 2001 From: Megha-Dev-19 <100185149+Megha-Dev-19@users.noreply.github.com> Date: Thu, 14 Nov 2024 15:15:29 +0530 Subject: [PATCH 03/14] setup new portal instance for templar --- instances/treasury-templar.near/.gitignore | 2 + .../aliases.mainnet.json | 14 + .../treasury-templar.near/bos.config.json | 6 + instances/treasury-templar.near/data.json | 3 + instances/treasury-templar.near/src | 1 + .../treasury-templar.near/widget/app.jsx | 164 ++ .../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 | 158 ++ .../components/molecule/LinkedRfpDropdown.jsx | 188 +++ .../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 | 692 ++++++++ .../widget/components/organism/Navbar.jsx | 264 +++ .../widget/components/pages/about.jsx | 27 + .../widget/components/pages/admin.jsx | 170 ++ .../widget/components/proposals/Editor.jsx | 1321 +++++++++++++++ .../widget/components/proposals/Feed.jsx | 676 ++++++++ .../widget/components/proposals/Proposal.jsx | 1441 +++++++++++++++++ .../widget/components/proposals/Proposals.jsx | 8 + .../proposals/ViewProposalModal.jsx | 161 ++ .../components/rfps/CommentsAndLogs.jsx | 467 ++++++ .../components/rfps/ConfirmCancelModal.jsx | 250 +++ .../widget/components/rfps/Editor.jsx | 873 ++++++++++ .../widget/components/rfps/Feed.jsx | 569 +++++++ .../widget/components/rfps/Rfp.jsx | 878 ++++++++++ .../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/config/css.jsx | 23 + .../widget/config/data.jsx | 49 + .../widget/config/theme.jsx | 5 + .../widget/core/common.jsx | 79 + 48 files changed, 10825 insertions(+) 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/app.jsx 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/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/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/config/css.jsx create mode 100644 instances/treasury-templar.near/widget/config/data.jsx create mode 100644 instances/treasury-templar.near/widget/config/theme.jsx create mode 100644 instances/treasury-templar.near/widget/core/common.jsx 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..83048af46 --- /dev/null +++ b/instances/treasury-templar.near/aliases.mainnet.json @@ -0,0 +1,14 @@ +{ + "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_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", + "REPL_INDEXER_HASURA_ROLE": "polyprogrammist_near" +} 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/app.jsx b/instances/treasury-templar.near/widget/app.jsx new file mode 100644 index 000000000..b7dabb3d4 --- /dev/null +++ b/instances/treasury-templar.near/widget/app.jsx @@ -0,0 +1,164 @@ +/** + * 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_TREASURY_TEMPLAR}/widget/components.template.AppLayout` +); + +const { Theme } = VM.require(`${REPL_TREASURY_TEMPLAR}/widget/config.theme`); + +const { CssContainer } = VM.require( + `${REPL_TREASURY_TEMPLAR}/widget/config.css` +); + +if (!AppLayout || !Theme || !CssContainer) { + return

Loading modules...

; +} + +if (!page) { + // If no page is specified, we default to the feed page TEMP + page = "about"; +} + +const propsToSend = { ...passProps, instance: "${REPL_TREASURY_TEMPLAR}" }; + +// This is our navigation, rendering the page based on the page parameter +function Page() { + const routes = page.split("."); + switch (routes[0]) { + 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 ( + + ); + } + // FOR TREASURY, SINCE WE USE SAME ACCOUNT TO DEPLOY BOTH + case "dashboard": { + return ( + + ); + } + case "settings": { + return ( + + ); + } + case "payments": { + return ( + + ); + } + + case "stake-delegation": { + return ( + + ); + } + + case "asset-exchange": { + return ( + + ); + } + default: { + return ; + } + } +} + +return ( + + + + + + + +); 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..3d629d59a --- /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..4c2b063cf --- /dev/null +++ b/instances/treasury-templar.near/widget/components/molecule/LinkedProposalsDropdown.jsx @@ -0,0 +1,158 @@ +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(""); + +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(), + }; + if (typeof fetchGraphQL !== "function") { + return; + } + 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/treasury-templar.near/widget/components/molecule/LinkedRfpDropdown.jsx b/instances/treasury-templar.near/widget/components/molecule/LinkedRfpDropdown.jsx new file mode 100644 index 000000000..e80ea2a91 --- /dev/null +++ b/instances/treasury-templar.near/widget/components/molecule/LinkedRfpDropdown.jsx @@ -0,0 +1,188 @@ +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); + +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(), + }; + if (typeof fetchGraphQL !== "function") { + return; + } + 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 && ( +
+ + {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..b57b46c90 --- /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..5dcef5c25 --- /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..0ae9f861f --- /dev/null +++ b/instances/treasury-templar.near/widget/components/molecule/SimpleMDE.jsx @@ -0,0 +1,692 @@ +/** + * 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 || []; + +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_TREASURY_TEMPLAR}/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_TREASURY_TEMPLAR}/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 = ` + + + + + + + + + + + + + + + + + + + + + + + +`; + +return ( +