Search
Search

Transaction: GamCXSu...BH7D

Receiver
Status
Succeeded
Transaction Fee
0.00393 
Deposit Value
0.00242 
Gas Used
39 Tgas
Attached Gas
300 Tgas
Created
July 01, 2024 at 5:26:17pm
Hash
GamCXSu16vs6RfHpkshdHjCTMX7NkbBcJHN8uSXWBH7D

Actions

Called method: 'set' in contract: social.near
Arguments:
{ "data": { "bos.forum.potlock.near": { "widget": { "components.proposals.CommentsAndLogs": { "": "const { PROPOSAL_TIMELINE_STATUS, isNumber, getLinkUsingCurrentGateway } =\n VM.require(`bos.forum.potlock.near/widget/core.common`) || {\n PROPOSAL_TIMELINE_STATUS: {},\n isNumber: () => {},\n getLinkUsingCurrentGateway: () => {},\n };\nconst { href } = VM.require(`devhub.near/widget/core.lib.url`);\nhref || (href = () => {});\nconst snapshotHistory = props.snapshotHistory;\nconst latestSnapshot = props.latestSnapshot;\nconst Wrapper = styled.div`\n position: relative;\n .log-line {\n position: absolute;\n left: 7%;\n top: -30px;\n bottom: 0;\n z-index: 1;\n width: 1px;\n background-color: var(--bs-border-color);\n z-index: 1;\n }\n .text-wrap {\n overflow: hidden;\n white-space: normal;\n }\n .fw-bold {\n font-weight: 600 !important;\n }\n .inline-flex {\n display: -webkit-inline-box !important;\n align-items: center !important;\n gap: 0.25rem !important;\n margin-right: 2px;\n flex-wrap: wrap;\n }\n`;\nconst CommentContainer = styled.div`\n border: 1px solid lightgrey;\n overflow: auto;\n`;\nconst Header = styled.div`\n position: relative;\n background-color: #f4f4f4;\n height: 50px;\n .menu {\n position: absolute;\n right: 10px;\n top: 4px;\n font-size: 30px;\n }\n`;\n// check snapshot history all keys and values for differences\nfunction getDifferentKeysWithValues(obj1, obj2) {\n return Object.keys(obj1)\n .filter((key) => {\n if (key !== \"editor_id\" && obj2.hasOwnProperty(key)) {\n const value1 = obj1[key];\n const value2 = obj2[key];\n if (Array.isArray(value1) && Array.isArray(value2)) {\n const sortedValue1 = [...value1].sort();\n const sortedValue2 = [...value2].sort();\n return JSON.stringify(sortedValue1) !== JSON.stringify(sortedValue2);\n } else if (typeof value1 === \"object\" && typeof value2 === \"object\") {\n return JSON.stringify(value1) !== JSON.stringify(value2);\n } else {\n return value1 !== value2;\n }\n }\n return false;\n })\n .map((key) => ({\n key,\n originalValue: obj1[key],\n modifiedValue: obj2[key],\n }));\n}\nState.init({\n data: null,\n socialComments: null,\n changedKeysListWithValues: null,\n});\nfunction sortTimelineAndComments() {\n const comments = Social.index(\"comment\", props.item, { subscribe: true });\n if (state.changedKeysListWithValues === null) {\n const changedKeysListWithValues = snapshotHistory\n .slice(1)\n .map((item, index) => {\n const startingPoint = snapshotHistory[index]; // Set comparison to the previous item\n return {\n editorId: item.editor_id,\n ...getDifferentKeysWithValues(startingPoint, item),\n };\n });\n State.update({ changedKeysListWithValues });\n }\n // sort comments and timeline logs by time\n const snapShotTimeStamp = Array.isArray(snapshotHistory)\n ? snapshotHistory.map((i) => {\n return { blockHeight: null, timestamp: parseFloat(i.timestamp / 1e6) };\n })\n : [];\n const commentsTimeStampPromise = Array.isArray(comments)\n ? Promise.all(\n comments.map((item) => {\n return asyncFetch(\n `https://api.near.social/time?blockHeight=${item.blockHeight}`\n ).then((res) => {\n const timeMs = parseFloat(res.body);\n return {\n blockHeight: item.blockHeight,\n timestamp: timeMs,\n };\n });\n })\n ).then((res) => res)\n : Promise.resolve([]);\n commentsTimeStampPromise.then((commentsTimeStamp) => {\n const combinedArray = [...snapShotTimeStamp, ...commentsTimeStamp];\n combinedArray.sort((a, b) => a.timestamp - b.timestamp);\n State.update({ data: combinedArray, socialComments: comments });\n });\n}\nif ((snapshotHistory ?? []).length > 0) {\n sortTimelineAndComments();\n}\nconst Comment = ({ commentItem }) => {\n const { accountId, blockHeight } = commentItem;\n const item = {\n type: \"social\",\n path: `${accountId}/post/comment`,\n blockHeight,\n };\n const content = JSON.parse(Social.get(item.path, blockHeight) ?? \"null\");\n const link = getLinkUsingCurrentGateway(\n `bos.forum.potlock.near/widget/app?page=proposal&id=${props.id}&accountId=${accountId}&blockHeight=${blockHeight}`\n );\n const hightlightComment =\n parseInt(props.blockHeight ?? \"\") === blockHeight &&\n props.accountId === accountId;\n return (\n <div style={{ zIndex: 99, background: \"white\" }}>\n <div className=\"d-flex gap-2 flex-1\">\n <div className=\"d-none d-sm-flex\">\n <Widget\n src={`bos.forum.potlock.near/widget/components.molecule.Profile`}\n props={{\n accountId: accountId,\n }}\n />\n </div>\n <CommentContainer\n style={{ border: hightlightComment ? \"2px solid black\" : \"\" }}\n className=\"rounded-2 flex-1\"\n >\n <Header className=\"d-flex gap-3 align-items-center p-2 px-3\">\n <div className=\"text-muted\">\n <a\n rel=\"noopener noreferrer\"\n target=\"_blank\"\n href={`https://bos.potlock.org/?tab=profile&accountId=${accountId}`}\n >\n <span className=\"fw-bold text-black\">{accountId}</span>\n </a>\n commented ・{\" \"}\n <Widget\n src={`near/widget/TimeAgo`}\n props={{\n blockHeight: blockHeight,\n }}\n />\n </div>\n {context.accountId && (\n <div className=\"menu\">\n <Widget\n src={`near/widget/Posts.Menu`}\n props={{\n accountId: accountId,\n blockHeight: blockHeight,\n contentPath: `/post/comment`,\n contentType: \"comment\",\n }}\n />\n </div>\n )}\n </Header>\n <div className=\"p-2 px-3\">\n <Widget\n src={`devhub.near/widget/devhub.components.molecule.MarkdownViewer`}\n props={{\n text: content.text,\n }}\n />\n <div className=\"d-flex gap-2 align-items-center mt-4\">\n <Widget\n src={`devhub.near/widget/devhub.entity.proposal.LikeButton`}\n props={{\n item: item,\n notifyAccountId: accountId,\n }}\n />\n <Widget\n src={`near/widget/CopyUrlButton`}\n props={{\n url: link,\n }}\n />\n </div>\n </div>\n </CommentContainer>\n </div>\n </div>\n );\n};\nfunction capitalizeFirstLetter(string) {\n const updated = string.replace(\"_\", \" \");\n return updated.charAt(0).toUpperCase() + updated.slice(1).toLowerCase();\n}\nfunction parseTimelineKeyAndValue(timeline, originalValue, modifiedValue) {\n const oldValue = originalValue[timeline];\n const newValue = modifiedValue[timeline];\n switch (timeline) {\n case \"status\": {\n if (\n (newValue === PROPOSAL_TIMELINE_STATUS.APPROVED ||\n newValue === PROPOSAL_TIMELINE_STATUS.APPROVED_CONDITIONALLY) &&\n latestSnapshot.linked_rfp\n ) {\n return (\n <span className=\"inline-flex\">\n moved proposal to{\" \"}\n <Widget\n src={`devhub.near/widget/devhub.entity.proposal.StatusTag`}\n props={{\n timelineStatus: newValue,\n }}\n />\n ・ this proposal is selected for RFP{\" \"}\n <LinkToRfp id={latestSnapshot.linked_rfp}>\n #{latestSnapshot.linked_rfp}\n </LinkToRfp>\n </span>\n );\n } else\n return (\n oldValue !== newValue && (\n <span className=\"inline-flex\">\n moved proposal from{\" \"}\n <Widget\n src={`devhub.near/widget/devhub.entity.proposal.StatusTag`}\n props={{\n timelineStatus: oldValue,\n }}\n />\n to{\" \"}\n <Widget\n src={`devhub.near/widget/devhub.entity.proposal.StatusTag`}\n props={{\n timelineStatus: newValue,\n }}\n />\n stage\n </span>\n )\n );\n }\n case \"sponsor_requested_review\":\n return !oldValue && newValue && <span>completed review</span>;\n case \"reviewer_completed_attestation\":\n return !oldValue && newValue && <span>completed attestation</span>;\n case \"kyc_verified\":\n return !oldValue && newValue && <span>verified KYC/KYB</span>;\n case \"test_transaction_sent\":\n return (\n !oldValue &&\n newValue && (\n <span>\n confirmed sponsorship and shared funding steps with recipient\n </span>\n )\n );\n case \"payouts\":\n return <span>updated the funding payment links.</span>;\n default:\n return null;\n }\n}\nconst AccountProfile = ({ accountId }) => {\n return (\n <span className=\"inline-flex fw-bold text-black\">\n <Widget\n src={`bos.forum.potlock.near/widget/components.molecule.Profile`}\n props={{\n accountId: accountId,\n size: \"sm\",\n showAccountId: true,\n }}\n />\n </span>\n );\n};\nconst LinkToRfp = ({ id, children }) => {\n return (\n <a\n className=\"text-decoration-underline flex-1\"\n href={href({\n widgetSrc: `bos.forum.potlock.near/widget/app`,\n params: {\n page: \"rfp\",\n id: id,\n },\n })}\n target=\"_blank\"\n rel=\"noopener noreferrer\"\n >\n {children}\n </a>\n );\n};\nconst parseProposalKeyAndValue = (key, modifiedValue, originalValue) => {\n switch (key) {\n case \"name\":\n return <span>changed title</span>;\n case \"summary\":\n case \"description\":\n return <span>changed {key}</span>;\n case \"labels\":\n return <span>changed labels to {(modifiedValue ?? []).join(\", \")}</span>;\n case \"category\":\n return (\n <span>\n changed category from {originalValue} to {modifiedValue}\n </span>\n );\n case \"linked_proposals\":\n return <span>updated linked proposals</span>;\n case \"linked_rfp\": {\n const isUnlinked = isNumber(originalValue) && !isNumber(modifiedValue);\n const actionText = isUnlinked ? \"unlinked\" : \"linked\";\n const rfpId = originalValue ?? modifiedValue;\n return (\n <span>\n {actionText} an RFP <LinkToRfp id={rfpId}>#{rfpId}</LinkToRfp>\n </span>\n );\n }\n case \"requested_sponsorship_usd_amount\":\n return (\n <span>\n changed sponsorship amount from {originalValue} to {modifiedValue}\n </span>\n );\n case \"requested_sponsorship_paid_in_currency\":\n return (\n <span>\n changed sponsorship currency from {originalValue} to {modifiedValue}\n </span>\n );\n case \"receiver_account\":\n return (\n <span className=\"inline-flex\">\n changed receiver account from{\" \"}\n <AccountProfile accountId={originalValue} />\n to <AccountProfile accountId={modifiedValue} />\n </span>\n );\n case \"supervisor\":\n return !originalValue && modifiedValue ? (\n <span className=\"inline-flex\">\n added\n <AccountProfile accountId={modifiedValue} />\n as project coordinator\n </span>\n ) : (\n <span className=\"inline-flex\">\n changed project coordinator from{\" \"}\n <AccountProfile accountId={originalValue} />\n to <AccountProfile accountId={modifiedValue} />\n </span>\n );\n case \"timeline\": {\n const modifiedKeys = Object.keys(modifiedValue);\n const originalKeys = Object.keys(originalValue);\n return modifiedKeys.map((i, index) => {\n const text = parseTimelineKeyAndValue(i, originalValue, modifiedValue);\n return (\n text && (\n <span key={index} className=\"inline-flex\">\n {text}\n {text && \"・\"}\n </span>\n )\n );\n });\n }\n default:\n return null;\n }\n};\nconst LogIconContainer = styled.div`\n margin-left: 50px;\n z-index: 99;\n @media screen and (max-width: 768px) {\n margin-left: 10px;\n }\n`;\nconst Log = ({ timestamp }) => {\n const updatedData = useMemo(\n () =>\n state.changedKeysListWithValues.find((obj) =>\n Object.values(obj).some(\n (value) =>\n value && parseFloat(value.modifiedValue / 1e6) === timestamp\n )\n ),\n [state.changedKeysListWithValues, timestamp]\n );\n const editorId = updatedData.editorId;\n const valuesArray = Object.values(updatedData ?? {});\n // if valuesArray length is 2 that means it only has timestamp and editorId\n if (!updatedData || valuesArray.length === 2) {\n return <></>;\n }\n return valuesArray.map((i, index) => {\n if (i.key && i.key !== \"timestamp\") {\n return (\n <LogIconContainer\n className=\"d-flex gap-3 align-items-center\"\n key={index}\n >\n <img\n src=\"https://ipfs.near.social/ipfs/bafkreiffqrxdi4xqu7erf46gdlwuodt6dm6rji2jtixs3iionjvga6rhdi\"\n height={30}\n />\n <div\n className={\n \"flex-1 gap-1 w-100 text-wrap text-muted align-items-center \" +\n (i.key === \"timeline\" &&\n Object.keys(i.originalValue ?? {}).length > 1\n ? \"\"\n : \"inline-flex\")\n }\n >\n <span className=\"inline-flex fw-bold text-black\">\n <AccountProfile accountId={editorId} showAccountId={true} />{\" \"}\n </span>\n {parseProposalKeyAndValue(i.key, i.modifiedValue, i.originalValue)}\n {i.key !== \"timeline\" && \"・\"}\n <Widget\n src={`near/widget/TimeAgo`}\n props={{\n blockTimestamp: timestamp * 1000000,\n }}\n />\n </div>\n </LogIconContainer>\n );\n }\n });\n};\nif (Array.isArray(state.data)) {\n return (\n <Wrapper>\n <div\n className=\"log-line\"\n style={{ height: state.data.length > 2 ? \"110%\" : \"150%\" }}\n ></div>\n <div className=\"d-flex flex-column gap-4\">\n {state.data.map((i, index) => {\n if (i.blockHeight) {\n const item = state.socialComments.find(\n (t) => t.blockHeight === i.blockHeight\n );\n return <Comment commentItem={item} />;\n } else {\n return <Log timestamp={i.timestamp} key={index} />;\n }\n })}\n </div>\n </Wrapper>\n );\n}\n" }, "components.proposals.Proposal": { "": "const {\n PROPOSAL_TIMELINE_STATUS,\n fetchGraphQL,\n parseJSON,\n isNumber,\n getLinkUsingCurrentGateway,\n} = VM.require(`bos.forum.potlock.near/widget/core.common`) || {\n PROPOSAL_TIMELINE_STATUS: {},\n fetchGraphQL: () => {},\n parseJSON: () => {},\n isNumber: () => {},\n getLinkUsingCurrentGateway: () => {},\n};\nconst { href } = VM.require(`devhub.near/widget/core.lib.url`);\nhref || (href = () => {});\nconst { getGlobalLabels } = VM.require(\n `bos.forum.potlock.near/widget/components.core.lib.contract`\n) || { getGlobalLabels: () => {} };\nconst { readableDate } = VM.require(`devhub.near/widget/core.lib.common`) || {\n readableDate: () => {},\n};\nconst accountId = context.accountId;\n/*\n ---props---\n props.id: number;\n props.timestamp: number; optional\n accountId: string\n blockHeight:number\n */\nconst DecisionStage = [\n PROPOSAL_TIMELINE_STATUS.APPROVED,\n PROPOSAL_TIMELINE_STATUS.REJECTED,\n PROPOSAL_TIMELINE_STATUS.APPROVED_CONDITIONALLY,\n];\nconst Container = styled.div`\n .full-width-div {\n width: 100vw;\n position: relative;\n left: 50%;\n right: 50%;\n margin-left: -50vw;\n margin-right: -50vw;\n }\n .fw-bold {\n font-weight: 600 !important;\n }\n .card.no-border {\n border-left: none !important;\n border-right: none !important;\n margin-bottom: -3.5rem;\n }\n .description-box {\n font-size: 14px;\n }\n .draft-info-container {\n background-color: #ecf8fb;\n }\n .review-info-container {\n background-color: #fef6ee;\n }\n .text-sm {\n font-size: 13px !important;\n }\n .flex-1 {\n flex: 1;\n }\n .flex-3 {\n flex: 3;\n }\n .circle {\n width: 20px;\n height: 20px;\n border-radius: 50%;\n border: 1px solid grey;\n }\n .green-fill {\n background-color: rgb(4, 164, 110) !important;\n border-color: rgb(4, 164, 110) !important;\n color: white !important;\n }\n .yellow-fill {\n border-color: #ff7a00 !important;\n }\n .vertical-line {\n width: 2px;\n height: 180px;\n background-color: lightgrey;\n }\n @media screen and (max-width: 970px) {\n .vertical-line {\n height: 135px !important;\n }\n .vertical-line-sm {\n height: 70px !important;\n }\n .gap-6 {\n gap: 0.5rem !important;\n }\n }\n @media screen and (max-width: 570px) {\n .vertical-line {\n height: 180px !important;\n }\n .vertical-line-sm {\n height: 75px !important;\n }\n .gap-6 {\n gap: 0.5rem !important;\n }\n }\n .vertical-line-sm {\n width: 2px;\n height: 70px;\n background-color: lightgrey;\n }\n .form-check-input:disabled ~ .form-check-label,\n .form-check-input[disabled] ~ .form-check-label {\n opacity: 1;\n }\n .form-check-input {\n border-color: black !important;\n }\n .grey-btn {\n background-color: #687076;\n border: none;\n color: white;\n }\n .form-check-input:checked {\n background-color: #04a46e !important;\n border-color: #04a46e !important;\n }\n .dropdown-toggle:after {\n position: absolute;\n top: 46%;\n right: 5%;\n }\n .drop-btn {\n max-width: none !important;\n }\n .dropdown-menu {\n width: 100%;\n border-radius: 0.375rem !important;\n }\n .green-btn {\n background-color: #03ba16 !important;\n border: none;\n color: white;\n &:active {\n color: white;\n }\n }\n .gap-6 {\n gap: 2.5rem;\n }\n .border-vertical {\n border-top: var(--bs-border-width) var(--bs-border-style)\n var(--bs-border-color) !important;\n border-bottom: var(--bs-border-width) var(--bs-border-style)\n var(--bs-border-color) !important;\n }\n button.px-0 {\n padding-inline: 0px !important;\n }\n red-icon i {\n color: red;\n }\n input[type=\"radio\"] {\n min-width: 13px;\n }\n`;\nconst ProposalContainer = styled.div`\n border: 1px solid lightgrey;\n overflow: auto;\n`;\nconst Header = styled.div`\n position: relative;\n background-color: #f4f4f4;\n height: 50px;\n .menu {\n position: absolute;\n right: 10px;\n top: 4px;\n font-size: 30px;\n }\n`;\nconst Text = styled.p`\n display: block;\n margin: 0;\n font-size: 14px;\n line-height: 20px;\n font-weight: 400;\n color: #687076;\n white-space: nowrap;\n`;\nconst Actions = styled.div`\n display: flex;\n align-items: center;\n gap: 12px;\n margin: -6px -6px 6px;\n`;\nconst Avatar = styled.div`\n width: 40px;\n height: 40px;\n pointer-events: none;\n img {\n object-fit: cover;\n border-radius: 40px;\n width: 100%;\n height: 100%;\n }\n`;\nconst LinkProfile = ({ account, children }) => {\n return (\n <a\n rel=\"noopener noreferrer\"\n target=\"_blank\"\n href={`https://bos.potlock.org/?tab=profile&accountId=${account}`}\n >\n {children}\n </a>\n );\n};\nconst stepsArray = [1, 2, 3, 4, 5];\nconst { id, timestamp } = props;\nconst proposal = Near.view(\"forum.potlock.near\", \"get_proposal\", {\n proposal_id: parseInt(id),\n});\nconst [snapshotHistory, setSnapshotHistory] = useState([]);\nconst queryName = \"bos_forum_potlock_near_ai_pgf_indexer_proposal_snapshots\";\nconst query = `query GetLatestSnapshot($offset: Int = 0, $limit: Int = 10, $where: ${queryName}_bool_exp = {}) {\n ${queryName}(\n offset: $offset\n limit: $limit\n order_by: {ts: asc}\n where: $where\n ) {\n editor_id\n name\n summary\n description\n ts\n proposal_id\n timeline\n labels\n linked_proposals\n linked_rfp\n requested_sponsorship_usd_amount\n requested_sponsorship_paid_in_currency\n receiver_account\n requested_sponsor\n supervisor\n }\n}`;\nconst fetchSnapshotHistory = () => {\n const variables = {\n where: { proposal_id: { _eq: id } },\n };\n fetchGraphQL(query, \"GetLatestSnapshot\", variables).then(async (result) => {\n if (result.status === 200) {\n if (result.body.data) {\n const data = result.body.data?.[queryName];\n const history = data.map((item) => {\n const proposalData = {\n ...item,\n timestamp: item.ts,\n timeline: parseJSON(item.timeline),\n };\n delete proposalData.ts;\n return proposalData;\n });\n setSnapshotHistory(history);\n }\n }\n });\n};\nuseEffect(() => {\n fetchSnapshotHistory();\n}, [id]);\nif (!proposal) {\n return (\n <div\n style={{ height: \"50vh\" }}\n className=\"d-flex justify-content-center align-items-center w-100\"\n >\n <Widget src={`devhub.near/widget/devhub.components.molecule.Spinner`} />\n </div>\n );\n}\nif (timestamp && proposal) {\n proposal.snapshot =\n snapshotHistory.find((item) => item.timestamp === timestamp) ??\n proposal.snapshot;\n}\nconst { snapshot } = proposal;\nsnapshot.timeline = parseJSON(snapshot.timeline);\nconst authorId = proposal.author_id;\nconst blockHeight = parseInt(proposal.social_db_post_block_height);\nconst item = {\n type: \"social\",\n path: `forum.potlock.near/post/main`,\n blockHeight,\n};\nconst proposalURL = getLinkUsingCurrentGateway(\n `bos.forum.potlock.near/widget/app?page=proposal&id=${proposal.id}&timestamp=${snapshot.timestamp}`\n);\nconst SidePanelItem = ({ title, children, hideBorder, ishidden }) => {\n return (\n <div\n style={{ gap: \"8px\" }}\n className={\n ishidden\n ? \"d-none\"\n : \"d-flex flex-column pb-3 \" + (!hideBorder && \" border-bottom\")\n }\n >\n <div className=\"h6 mb-0\">{title} </div>\n <div className=\"text-muted\">{children}</div>\n </div>\n );\n};\nconst rfpLabelOptions = getGlobalLabels();\nconst proposalStatusOptions = [\n {\n label: \"Draft\",\n value: { status: PROPOSAL_TIMELINE_STATUS.DRAFT },\n },\n {\n label: \"Review\",\n value: {\n status: PROPOSAL_TIMELINE_STATUS.REVIEW,\n sponsor_requested_review: false,\n reviewer_completed_attestation: false,\n },\n },\n {\n label: \"Approved\",\n value: {\n status: PROPOSAL_TIMELINE_STATUS.APPROVED,\n sponsor_requested_review: true,\n reviewer_completed_attestation: false,\n },\n },\n {\n label: \"Approved-Conditionally\",\n value: {\n status: PROPOSAL_TIMELINE_STATUS.APPROVED_CONDITIONALLY,\n sponsor_requested_review: true,\n reviewer_completed_attestation: false,\n },\n },\n {\n label: \"Rejected\",\n value: {\n status: PROPOSAL_TIMELINE_STATUS.REJECTED,\n sponsor_requested_review: true,\n reviewer_completed_attestation: false,\n },\n },\n {\n label: \"Canceled\",\n value: {\n status: PROPOSAL_TIMELINE_STATUS.CANCELED,\n sponsor_requested_review: false,\n reviewer_completed_attestation: false,\n },\n },\n {\n label: \"Payment-processing\",\n value: {\n status: PROPOSAL_TIMELINE_STATUS.PAYMENT_PROCESSING,\n kyc_verified: false,\n test_transaction_sent: false,\n request_for_trustees_created: false,\n sponsor_requested_review: true,\n reviewer_completed_attestation: false,\n },\n },\n {\n label: \"Funded\",\n value: {\n status: PROPOSAL_TIMELINE_STATUS.FUNDED,\n trustees_released_payment: true,\n kyc_verified: true,\n test_transaction_sent: true,\n request_for_trustees_created: true,\n sponsor_requested_review: true,\n reviewer_completed_attestation: false,\n },\n },\n];\nconst CheckBox = ({ value, isChecked, label, disabled, onClick }) => {\n return (\n <div className=\"d-flex gap-2 align-items-center\">\n <input\n class=\"form-check-input\"\n type=\"checkbox\"\n value={value}\n checked={isChecked}\n disabled={!isModerator || !showTimelineSetting || disabled}\n onChange={(e) => onClick(e.target.checked)}\n />\n <label style={{ width: \"90%\" }} class=\"form-check-label text-black\">\n {label}\n </label>\n </div>\n );\n};\nconst RadioButton = ({ value, isChecked, label }) => {\n return (\n <div className=\"d-flex gap-2 align-items-center\">\n <input\n class=\"form-check-input\"\n type=\"radio\"\n value={value}\n checked={isChecked}\n disabled={true}\n />\n <label class=\"form-check-label text-black\">{label}</label>\n </div>\n );\n};\nconst isAllowedToEditProposal = Near.view(\n \"forum.potlock.near\",\n \"is_allowed_to_edit_proposal\",\n {\n proposal_id: proposal.id,\n editor: accountId,\n }\n);\nconst isModerator = Near.view(\n \"forum.potlock.near\",\n \"is_allowed_to_write_rfps\",\n {\n editor: context.accountId,\n }\n);\nconst editProposal = ({ timeline }) => {\n const body = {\n proposal_body_version: \"V1\",\n name: snapshot.name,\n description: snapshot.description,\n category: snapshot.category,\n summary: snapshot.summary,\n linked_proposals: snapshot.linked_proposals,\n requested_sponsorship_usd_amount: snapshot.requested_sponsorship_usd_amount,\n requested_sponsorship_paid_in_currency:\n snapshot.requested_sponsorship_paid_in_currency,\n receiver_account: snapshot.receiver_account,\n requested_sponsor: snapshot.requested_sponsor,\n timeline: timeline,\n linked_rfp: snapshot.linked_rfp,\n supervisor: supervisor ?? snapshot.supervisor,\n };\n const args = {\n labels: typeof snapshot.linked_rfp === \"number\" ? [] : snapshot.labels,\n body: body,\n id: proposal.id,\n };\n Near.call([\n {\n contractName: \"forum.potlock.near\",\n methodName: \"edit_proposal\",\n args: args,\n gas: 270000000000000,\n },\n ]);\n};\nconst editProposalStatus = ({ timeline }) => {\n Near.call([\n {\n contractName: \"forum.potlock.near\",\n methodName: \"edit_proposal_timeline\",\n args: {\n id: proposal.id,\n timeline: timeline,\n },\n gas: 270000000000000,\n },\n ]);\n};\nconst [isReviewModalOpen, setReviewModal] = useState(false);\nconst [isCancelModalOpen, setCancelModal] = useState(false);\nconst [showTimelineSetting, setShowTimelineSetting] = useState(false);\nconst proposalStatus = useCallback(\n () =>\n proposalStatusOptions.find(\n (i) => i.value.status === snapshot.timeline.status\n ),\n [snapshot]\n);\nconst [updatedProposalStatus, setUpdatedProposalStatus] = useState({});\nuseEffect(() => {\n setUpdatedProposalStatus({\n ...proposalStatus(),\n value: { ...proposalStatus().value, ...snapshot.timeline },\n });\n}, [proposal]);\nconst [paymentHashes, setPaymentHashes] = useState([\"\"]);\nconst [supervisor, setSupervisor] = useState(snapshot.supervisor);\nconst selectedStatusIndex = useMemo(\n () =>\n proposalStatusOptions.findIndex((i) => {\n return updatedProposalStatus.value.status === i.value.status;\n }),\n [updatedProposalStatus]\n);\nconst TimelineItems = ({ title, children, value, values }) => {\n const indexOfCurrentItem = proposalStatusOptions.findIndex((i) =>\n Array.isArray(values)\n ? values.includes(i.value.status)\n : value === i.value.status\n );\n let color = \"transparent\";\n let statusIndex = selectedStatusIndex;\n // index 2,3,4,5 is of decision\n if (selectedStatusIndex === 3 || selectedStatusIndex === 2) {\n statusIndex = 2;\n }\n if (statusIndex === indexOfCurrentItem) {\n color = \"#FEF6EE\";\n }\n if (\n statusIndex > indexOfCurrentItem ||\n updatedProposalStatus.value.status === PROPOSAL_TIMELINE_STATUS.FUNDED\n ) {\n color = \"#EEFEF0\";\n }\n // reject\n if (statusIndex === 4 && indexOfCurrentItem === 2) {\n color = \"#FF7F7F\";\n }\n // cancelled\n if (statusIndex === 5 && indexOfCurrentItem === 2) {\n color = \"#F4F4F4\";\n }\n return (\n <div\n className=\"p-2 rounded-3\"\n style={{\n backgroundColor: color,\n }}\n >\n <div className=\"h6 text-black\"> {title}</div>\n <div className=\"text-sm\">{children}</div>\n </div>\n );\n};\nconst link = href({\n widgetSrc: `bos.forum.potlock.near/widget/app`,\n params: {\n page: \"create-proposal\",\n id: proposal.id,\n timestamp: timestamp,\n },\n});\nconst createdDate = snapshotHistory[0]?.timestamp ?? snapshot.timestamp;\nreturn (\n <Container className=\"d-flex flex-column gap-2 w-100 mt-4\">\n <Widget\n src={`devhub.near/widget/devhub.entity.proposal.ConfirmReviewModal`}\n props={{\n isOpen: isReviewModalOpen,\n onCancelClick: () => setReviewModal(false),\n onReviewClick: () => {\n setReviewModal(false);\n editProposalStatus({ timeline: proposalStatusOptions[1].value });\n },\n }}\n />\n <Widget\n src={`devhub.near/widget/devhub.entity.proposal.ConfirmCancelModal`}\n props={{\n isOpen: isCancelModalOpen,\n onCancelClick: () => setCancelModal(false),\n onConfirmClick: () => {\n setCancelModal(false);\n editProposalStatus({ timeline: proposalStatusOptions[5].value });\n },\n }}\n />\n <div className=\"d-flex px-3 px-lg-0 justify-content-between\">\n <div className=\"d-flex gap-2 align-items-center h3\">\n <div>{snapshot.name}</div>\n <div className=\"text-muted\">#{proposal.id}</div>\n </div>\n <div className=\"d-flex gap-2 align-items-center\">\n <Widget\n src={`near/widget/ShareButton`}\n props={{\n postType: \"post\",\n url: proposalURL,\n }}\n />\n {((isAllowedToEditProposal &&\n snapshot.timeline.status === PROPOSAL_TIMELINE_STATUS.DRAFT) ||\n isModerator) && (\n <Link to={link} style={{ textDecoration: \"none\" }}>\n <Widget\n src={`devhub.near/widget/devhub.components.molecule.Button`}\n props={{\n label: \"Edit\",\n classNames: { root: \"grey-btn btn-sm\" },\n }}\n />\n </Link>\n )}\n </div>\n </div>\n <div className=\"d-flex flex-wrap flex-md-nowrap px-3 px-lg-0 gap-2 align-items-center text-sm pb-3 w-100\">\n <Widget\n src={`devhub.near/widget/devhub.entity.proposal.StatusTag`}\n props={{\n timelineStatus: snapshot.timeline.status,\n size: \"sm\",\n }}\n />\n <div className=\"w-100 d-flex flex-wrap flex-md-nowrap gap-1 align-items-center\">\n <div className=\"fw-bold text-truncate\">\n <LinkProfile account={authorId}>{authorId}</LinkProfile>\n </div>\n <div>created on {readableDate(createdDate / 1000000)}</div>\n </div>\n </div>\n <div className=\"card no-border rounded-0 full-width-div px-3 px-lg-0\">\n <div className=\"container-xl py-4\">\n {snapshot.timeline.status === PROPOSAL_TIMELINE_STATUS.DRAFT &&\n isAllowedToEditProposal && (\n <div className=\"draft-info-container p-3 p-sm-4 d-flex flex-wrap flex-sm-nowrap justify-content-between align-items-center gap-2 rounded-2\">\n <div style={{ minWidth: \"300px\" }}>\n <b>\n This proposal is in draft mode and open for community\n comments.\n </b>\n <p className=\"text-sm text-muted mt-2\">\n The author can still refine the proposal and build consensus\n before sharing it with sponsors. Click “Ready for review” when\n you want to start the official review process. This will lock\n the editing function, but comments are still open.\n </p>\n </div>\n <div style={{ minWidth: \"fit-content\" }}>\n <Widget\n src={`devhub.near/widget/devhub.components.molecule.Button`}\n props={{\n label: \"Ready for review\",\n classNames: { root: \"grey-btn btn-sm\" },\n onClick: () => setReviewModal(true),\n }}\n />\n </div>\n </div>\n )}\n {snapshot.timeline.status === PROPOSAL_TIMELINE_STATUS.REVIEW &&\n isAllowedToEditProposal && (\n <div className=\"review-info-container p-3 p-sm-4 d-flex flex-wrap flex-sm-nowrap justify-content-between align-items-center gap-2 rounded-2\">\n <div style={{ minWidth: \"300px\" }}>\n <b>\n This proposal is in review mode and still open for community\n comments.\n </b>\n <p className=\"text-sm text-muted mt-2\">\n You can’t edit the proposal, but comments are open. Only\n moderators can make changes. Click “Cancel Proposal” to cancel\n your proposal. This changes the status to Canceled, signaling\n to sponsors that it’s no longer active or relevant.\n </p>\n </div>\n <div style={{ minWidth: \"fit-content\" }}>\n <Widget\n src={`devhub.near/widget/devhub.components.molecule.Button`}\n props={{\n label: (\n <div className=\"d-flex align-items-center gap-1\">\n <i class=\"bi bi-trash3\"></i> Cancel Proposal\n </div>\n ),\n classNames: { root: \"btn-outline-danger btn-sm\" },\n onClick: () => setCancelModal(true),\n }}\n />\n </div>\n </div>\n )}\n <div className=\"my-4\">\n <div className=\"d-flex flex-wrap gap-6\">\n <div\n style={{ minWidth: \"350px\" }}\n className=\"flex-3 order-2 order-md-1\"\n >\n <div\n className=\"d-flex gap-2 flex-1\"\n style={{\n zIndex: 99,\n background: \"white\",\n position: \"relative\",\n }}\n >\n <div\n className=\"d-none d-sm-flex\"\n style={{ height: \"max-content\" }}\n >\n <Widget\n src={`bos.forum.potlock.near/widget/components.molecule.Profile`}\n props={{\n accountId: authorId,\n }}\n />\n </div>\n <ProposalContainer className=\"rounded-2 flex-1\">\n <Header className=\"d-flex gap-1 align-items-center p-2 px-3 \">\n <div\n className=\"fw-bold text-truncate\"\n style={{ maxWidth: \"60%\" }}\n >\n <LinkProfile account={authorId}>{authorId}</LinkProfile>\n </div>\n <div\n className=\"text-muted\"\n style={{ minWidth: \"fit-content\" }}\n >\n ・{\" \"}\n <Widget\n src={`near/widget/TimeAgo`}\n props={{\n blockHeight,\n blockTimestamp: createdDate,\n }}\n />\n {context.accountId && (\n <div className=\"menu\">\n <Widget\n src={`near/widget/Posts.Menu`}\n props={{\n accountId: authorId,\n blockHeight: blockHeight,\n }}\n />\n </div>\n )}\n </div>\n </Header>\n <div className=\"d-flex flex-column gap-1 p-2 px-3 description-box\">\n <div className=\"text-muted h6 border-bottom pb-1 mt-3\">\n PROPOSAL CATEGORY\n </div>\n <div>\n <Widget\n src={`bos.forum.potlock.near/widget/components.molecule.MultiSelectCategoryDropdown`}\n props={{\n selected: snapshot.labels,\n disabled: true,\n hideDropdown: true,\n onChange: () => {},\n availableOptions: rfpLabelOptions,\n }}\n />\n </div>\n <div className=\"text-muted h6 border-bottom pb-1 mt-3\">\n SUMMARY\n </div>\n <div>{snapshot.summary}</div>\n <div className=\"text-muted h6 border-bottom pb-1 mt-3 mb-4\">\n DESCRIPTION\n </div>\n <Widget\n src={`devhub.near/widget/devhub.components.molecule.MarkdownViewer`}\n props={{ text: snapshot.description }}\n />\n <div className=\"d-flex gap-2 align-items-center mt-4\">\n <Widget\n src={`bos.forum.potlock.near/widget/components.molecule.LikeButton`}\n props={{\n item,\n proposalId: proposal.id,\n notifyAccountIds: [authorId],\n }}\n />\n <Widget\n src={`devhub.near/widget/devhub.entity.proposal.CommentIcon`}\n props={{\n item,\n showOverlay: false,\n onClick: () => {},\n }}\n />\n <Widget\n src={`near/widget/CopyUrlButton`}\n props={{\n url: proposalURL,\n }}\n />\n </div>\n </div>\n </ProposalContainer>\n </div>\n <div className=\"border-bottom pb-4 mt-4\">\n <Widget\n src={`bos.forum.potlock.near/widget/components.proposals.CommentsAndLogs`}\n props={{\n ...props,\n id: proposal.id,\n item: item,\n snapshotHistory: snapshotHistory,\n latestSnapshot: snapshot,\n }}\n />\n </div>\n <div\n style={{\n position: \"relative\",\n zIndex: 99,\n backgroundColor: \"white\",\n }}\n className=\"pt-4\"\n >\n <Widget\n src={`bos.forum.potlock.near/widget/components.molecule.ComposeComment`}\n props={{\n ...props,\n item: item,\n notifyAccountIds: [authorId],\n proposalId: proposal.id,\n }}\n />\n </div>\n </div>\n <div\n style={{ minWidth: \"350px\" }}\n className=\"d-flex flex-column gap-4 flex-1 order-1 order-md-2\"\n >\n <SidePanelItem title=\"Author\">\n {console.log({ authorId })}\n <Widget\n src={`bos.forum.potlock.near/widget/components.molecule.AccountProfile`}\n props={{\n accountId: authorId,\n noOverlay: true,\n }}\n />\n </SidePanelItem>\n <SidePanelItem\n title={\"Linked RFP\"}\n ishidden={!isNumber(snapshot.linked_rfp)}\n >\n <Widget\n src={`bos.forum.potlock.near/widget/components.molecule.LinkedRfps`}\n props={{\n linkedRfpIds: [snapshot.linked_rfp],\n }}\n />\n </SidePanelItem>\n <SidePanelItem\n title={\n \"Linked Proposals \" + `(${snapshot.linked_proposals.length})`\n }\n ishidden={!snapshot.linked_proposals.length}\n >\n <Widget\n src={`bos.forum.potlock.near/widget/components.molecule.LinkedProposals`}\n props={{\n linkedProposalIds: snapshot.linked_proposals,\n }}\n />\n </SidePanelItem>\n <SidePanelItem title=\"Funding Ask\">\n <div className=\"h4 text-black\">\n {snapshot.requested_sponsorship_usd_amount && (\n <div className=\"d-flex flex-column gap-1\">\n <div>\n {parseInt(\n snapshot.requested_sponsorship_usd_amount\n ).toLocaleString()}{\" \"}\n USD\n </div>\n <div className=\"text-sm text-muted\">\n Requested in{\" \"}\n {snapshot.requested_sponsorship_paid_in_currency}\n </div>\n </div>\n )}\n </div>\n </SidePanelItem>\n <SidePanelItem title=\"Recipient Wallet Address\">\n <Widget\n src={`bos.forum.potlock.near/widget/components.molecule.AccountProfile`}\n props={{\n accountId: snapshot.receiver_account,\n noOverlay: true,\n }}\n />\n </SidePanelItem>\n <SidePanelItem title=\"Recipient Verification Status\">\n <Widget\n src={`devhub.near/widget/devhub.entity.proposal.VerificationStatus`}\n props={{\n receiverAccount: snapshot.receiver_account,\n showGetVerifiedBtn:\n accountId === snapshot.receiver_account ||\n accountId === authorId,\n }}\n />\n </SidePanelItem>\n <SidePanelItem\n title=\"Project Coordinator\"\n ishidden={!snapshot.supervisor}\n >\n <Widget\n src={`bos.forum.potlock.near/widget/components.molecule.AccountProfile`}\n props={{\n accountId: snapshot.supervisor,\n noOverlay: true,\n }}\n />\n </SidePanelItem>\n <SidePanelItem\n hideBorder={true}\n title={\n <div>\n <div className=\"d-flex justify-content-between align-content-center\">\n Timeline\n {isModerator && (\n <div onClick={() => setShowTimelineSetting(true)}>\n <i class=\"bi bi-gear\"></i>\n </div>\n )}\n </div>\n {showTimelineSetting && (\n <div className=\"mt-2 d-flex flex-column gap-2\">\n <h6 className=\"mb-0\">Proposal Status</h6>\n <Widget\n src={`devhub.near/widget/devhub.components.molecule.DropDown`}\n props={{\n options: proposalStatusOptions,\n selectedValue: updatedProposalStatus,\n onUpdate: (v) => {\n setUpdatedProposalStatus({\n ...v,\n value: {\n ...v.value,\n ...updatedProposalStatus.value,\n status: v.value.status,\n },\n });\n },\n }}\n />\n </div>\n )}\n </div>\n }\n >\n <div className=\"d-flex flex-column gap-2\">\n <div className=\"d-flex gap-3 mt-2\">\n <div className=\"d-flex flex-column\">\n {stepsArray.map((_, index) => {\n const indexOfCurrentItem = index;\n let color = \"\";\n let statusIndex = selectedStatusIndex;\n // index 2,3,4 is of decision\n if (\n selectedStatusIndex === 3 ||\n selectedStatusIndex === 2 ||\n selectedStatusIndex === 4 ||\n selectedStatusIndex === 5\n ) {\n statusIndex = 2;\n }\n if (selectedStatusIndex === 6) {\n statusIndex = 3;\n }\n const current = statusIndex === indexOfCurrentItem;\n const completed =\n statusIndex > indexOfCurrentItem ||\n updatedProposalStatus.value.status ===\n PROPOSAL_TIMELINE_STATUS.FUNDED;\n return (\n <div className=\"d-flex flex-column align-items-center gap-1\">\n <div\n className={\n \"circle \" +\n (completed && \" green-fill \") +\n (current && \" yellow-fill \")\n }\n >\n {completed && (\n <div\n className=\"d-flex justify-content-center align-items-center\"\n style={{ height: \"110%\" }}\n >\n <i class=\"bi bi-check\"></i>\n </div>\n )}\n </div>\n {index !== stepsArray.length - 1 && (\n <div\n className={\n \"vertical-line\" +\n (index === stepsArray.length - 2\n ? \"-sm \"\n : \" \") +\n (completed && \" green-fill \") +\n (current && \" yellow-fill \")\n }\n ></div>\n )}\n </div>\n );\n })}\n </div>\n <div className=\"d-flex flex-column gap-3\">\n <TimelineItems\n title=\"1) Draft\"\n value={PROPOSAL_TIMELINE_STATUS.DRAFT}\n >\n <div>\n Once an author submits a proposal, it is in draft mode\n and open for community comments. The author can still\n make changes to the proposal during this stage and\n submit it for official review when ready.\n </div>\n </TimelineItems>\n <TimelineItems\n title=\"2) Review\"\n value={PROPOSAL_TIMELINE_STATUS.REVIEW}\n >\n <div className=\"d-flex flex-column gap-2\">\n Sponsors who agree to consider the proposal may\n request attestations from work groups.\n <CheckBox\n value=\"\"\n disabled={selectedStatusIndex !== 1}\n onClick={(value) =>\n setUpdatedProposalStatus((prevState) => ({\n ...prevState,\n value: {\n ...prevState.value,\n sponsor_requested_review: value,\n },\n }))\n }\n label=\"Sponsor provides feedback or requests reviews\"\n isChecked={\n updatedProposalStatus.value\n .sponsor_requested_review\n }\n />\n <CheckBox\n value=\"\"\n disabled={selectedStatusIndex !== 1}\n label=\"Reviewer completes attestations (Optional)\"\n onClick={(value) =>\n setUpdatedProposalStatus((prevState) => ({\n ...prevState,\n value: {\n ...prevState.value,\n reviewer_completed_attestation: value,\n },\n }))\n }\n isChecked={\n updatedProposalStatus.value\n .reviewer_completed_attestation\n }\n />\n </div>\n </TimelineItems>\n <TimelineItems\n title=\"3) Decision\"\n values={[\n PROPOSAL_TIMELINE_STATUS.APPROVED,\n PROPOSAL_TIMELINE_STATUS.APPROVED_CONDITIONALLY,\n PROPOSAL_TIMELINE_STATUS.REJECTED,\n ]}\n >\n <div className=\"d-flex flex-column gap-2\">\n <div>Sponsor makes a final decision:</div>\n <RadioButton\n value=\"\"\n label={<div className=\"fw-bold\">Approved</div>}\n isChecked={\n updatedProposalStatus.value.status ===\n PROPOSAL_TIMELINE_STATUS.APPROVED ||\n updatedProposalStatus.value.status ===\n PROPOSAL_TIMELINE_STATUS.PAYMENT_PROCESSING ||\n updatedProposalStatus.value.status ===\n PROPOSAL_TIMELINE_STATUS.FUNDED\n }\n />\n <RadioButton\n value=\"\"\n label={\n <>\n <div className=\"fw-bold\">\n Approved - Conditional{\" \"}\n </div>\n <span>\n Requires follow up from recipient. Moderators\n will provide further details.\n </span>\n </>\n }\n isChecked={\n updatedProposalStatus.value.status ===\n PROPOSAL_TIMELINE_STATUS.APPROVED_CONDITIONALLY\n }\n />\n <RadioButton\n value=\"Reject\"\n label={<div className=\"fw-bold\">Rejected</div>}\n isChecked={\n updatedProposalStatus.value.status ===\n PROPOSAL_TIMELINE_STATUS.REJECTED\n }\n />\n <RadioButton\n value=\"Canceled\"\n label={<div className=\"fw-bold\">Canceled</div>}\n isChecked={\n updatedProposalStatus.value.status ===\n PROPOSAL_TIMELINE_STATUS.CANCELED\n }\n />\n </div>\n </TimelineItems>\n <TimelineItems\n title=\"4) Payment Processing\"\n value={PROPOSAL_TIMELINE_STATUS.PAYMENT_PROCESSING}\n >\n <div className=\"d-flex flex-column gap-2\">\n <CheckBox\n value={updatedProposalStatus.value.kyc_verified}\n label=\"Sponsor verifies KYC/KYB\"\n disabled={selectedStatusIndex !== 6}\n onClick={(value) =>\n setUpdatedProposalStatus((prevState) => ({\n ...prevState,\n value: {\n ...prevState.value,\n kyc_verified: value,\n },\n }))\n }\n isChecked={updatedProposalStatus.value.kyc_verified}\n />\n <CheckBox\n value={\n updatedProposalStatus.value.test_transaction_sent\n }\n disabled={selectedStatusIndex !== 6}\n label=\"Sponsor confirmed sponsorship and shared funding steps with recipient\"\n onClick={(value) =>\n setUpdatedProposalStatus((prevState) => ({\n ...prevState,\n value: {\n ...prevState.value,\n test_transaction_sent: value,\n },\n }))\n }\n isChecked={\n updatedProposalStatus.value.test_transaction_sent\n }\n />\n </div>\n </TimelineItems>\n <TimelineItems\n title=\"5) Funded\"\n value={PROPOSAL_TIMELINE_STATUS.FUNDED}\n >\n <div className=\"d-flex flex-column gap-2\">\n {paymentHashes?.length > 1 ? (\n paymentHashes.slice(0, -1).map((link, index) => (\n <a\n key={index}\n href={link}\n className=\"text-decoration-underline\"\n target=\"_blank\"\n rel=\"noopener noreferrer\"\n >\n Payment Link\n <i className=\"bi bi-arrow-up-right\"></i>\n </a>\n ))\n ) : updatedProposalStatus.value.payouts.length > 0 ? (\n <div>\n {updatedProposalStatus.value.payouts.map(\n (link) => {\n return (\n <a\n href={link}\n className=\"text-decoration-underline\"\n target=\"_blank\"\n rel=\"noopener noreferrer\"\n >\n Payment Link\n <i class=\"bi bi-arrow-up-right\"></i>\n </a>\n );\n }\n )}\n </div>\n ) : (\n \"No Payouts yet\"\n )}\n </div>\n </TimelineItems>\n </div>\n </div>\n {showTimelineSetting && (\n <div className=\"d-flex flex-column gap-2\">\n <div className=\"border-vertical py-3 my-2\">\n <label className=\"text-black h6\">\n Project Coordinator\n </label>\n <Widget\n src={`devhub.near/widget/devhub.entity.proposal.AccountInput`}\n props={{\n value: supervisor,\n placeholder: \"\",\n onUpdate: setSupervisor,\n }}\n />\n </div>\n {updatedProposalStatus.value.status ===\n PROPOSAL_TIMELINE_STATUS.FUNDED && (\n <div className=\"border-vertical py-3 my-2\">\n <label className=\"text-black h6\">Payment Link</label>\n <div className=\"d-flex flex-column gap-2\">\n {paymentHashes.map((item, index) => (\n <div className=\"d-flex gap-2 justify-content-between align-items-center\">\n <Widget\n src={`devhub.near/widget/devhub.components.molecule.Input`}\n props={{\n className: \"flex-grow-1\",\n value: item,\n onChange: (e) => {\n const updatedHashes = [...paymentHashes];\n updatedHashes[index] = e.target.value;\n setPaymentHashes(updatedHashes);\n },\n skipPaddingGap: true,\n placeholder: \"Enter URL\",\n }}\n />\n <div style={{ minWidth: 20 }}>\n {index !== paymentHashes.length - 1 ? (\n <Widget\n src={`devhub.near/widget/devhub.components.molecule.Button`}\n props={{\n classNames: {\n root: \"btn-outline-danger shadow-none w-100\",\n },\n label: <i class=\"bi bi-trash3 h6\"></i>,\n onClick: () => {\n const updatedHashes = [\n ...paymentHashes,\n ];\n updatedHashes.splice(index, 1);\n setPaymentHashes(updatedHashes);\n },\n }}\n />\n ) : (\n <Widget\n src={`devhub.near/widget/devhub.components.molecule.Button`}\n props={{\n classNames: {\n root: \"green-btn shadow-none border-0 w-100\",\n },\n disabled: !item,\n label: <i class=\"bi bi-plus-lg\"></i>,\n onClick: () =>\n setPaymentHashes([\n ...paymentHashes,\n \"\",\n ]),\n }}\n />\n )}\n </div>\n </div>\n ))}\n </div>\n </div>\n )}\n <div className=\"d-flex gap-2 align-items-center justify-content-end text-sm\">\n <Widget\n src={`devhub.near/widget/devhub.components.molecule.Button`}\n props={{\n label: \"Cancel\",\n classNames: {\n root: \"btn-outline-danger border-0 shadow-none btn-sm\",\n },\n onClick: () => {\n setShowTimelineSetting(false);\n setUpdatedProposalStatus(proposalStatus);\n },\n }}\n />\n <Widget\n src={`devhub.near/widget/devhub.components.molecule.Button`}\n props={{\n label: \"Save\",\n disabled:\n !supervisor &&\n DecisionStage.includes(\n updatedProposalStatus.value.status\n ),\n classNames: { root: \"green-btn btn-sm\" },\n onClick: () => {\n if (snapshot.supervisor !== supervisor) {\n editProposal({\n timeline: updatedProposalStatus.value,\n });\n } else if (\n updatedProposalStatus.value.status ===\n PROPOSAL_TIMELINE_STATUS.FUNDED\n ) {\n editProposalStatus({\n timeline: {\n ...updatedProposalStatus.value,\n payouts: !paymentHashes[0]\n ? []\n : paymentHashes.filter(\n (item) => item !== \"\"\n ),\n },\n });\n } else {\n editProposalStatus({\n timeline: updatedProposalStatus.value,\n });\n }\n setShowTimelineSetting(false);\n },\n }}\n />\n </div>\n </div>\n )}\n </div>\n </SidePanelItem>\n </div>\n </div>\n </div>\n </div>\n </div>\n </Container>\n);\n" }, "components.rfps.CommentsAndLogs": { "": "const { RFP_TIMELINE_STATUS, getLinkUsingCurrentGateway } = VM.require(\n `bos.forum.potlock.near/widget/core.common`\n) || { RFP_TIMELINE_STATUS: {}, getLinkUsingCurrentGateway: () => {} };\nconst { href } = VM.require(`devhub.near/widget/core.lib.url`);\nhref || (href = () => {});\nconst snapshotHistory = props.snapshotHistory;\nconst approvedProposals = props.approvedProposals ?? [];\nconst Wrapper = styled.div`\n position: relative;\n .log-line {\n position: absolute;\n left: 7%;\n top: -30px;\n bottom: 0;\n z-index: 1;\n width: 1px;\n background-color: var(--bs-border-color);\n z-index: 1;\n }\n .text-wrap {\n overflow: hidden;\n white-space: normal;\n }\n .fw-bold {\n font-weight: 600 !important;\n }\n .inline-flex {\n display: -webkit-inline-box !important;\n align-items: center !important;\n gap: 0.25rem !important;\n margin-right: 2px;\n flex-wrap: wrap;\n }\n`;\nconst CommentContainer = styled.div`\n border: 1px solid lightgrey;\n overflow: auto;\n`;\nconst Header = styled.div`\n position: relative;\n background-color: #f4f4f4;\n height: 50px;\n .menu {\n position: absolute;\n right: 10px;\n top: 4px;\n font-size: 30px;\n }\n`;\n// check snapshot history all keys and values for differences\nfunction getDifferentKeysWithValues(obj1, obj2) {\n return Object.keys(obj1)\n .filter((key) => {\n if (key !== \"editor_id\" && obj2.hasOwnProperty(key)) {\n const value1 = obj1[key];\n const value2 = obj2[key];\n if (Array.isArray(value1) && Array.isArray(value2)) {\n const sortedValue1 = [...value1].sort();\n const sortedValue2 = [...value2].sort();\n return JSON.stringify(sortedValue1) !== JSON.stringify(sortedValue2);\n } else if (typeof value1 === \"object\" && typeof value2 === \"object\") {\n return JSON.stringify(value1) !== JSON.stringify(value2);\n } else {\n return value1 !== value2;\n }\n }\n return false;\n })\n .map((key) => ({\n key,\n originalValue: obj1[key],\n modifiedValue: obj2[key],\n }));\n}\nState.init({\n data: null,\n socialComments: null,\n changedKeysListWithValues: null,\n});\nfunction sortTimelineAndComments() {\n const comments = Social.index(\"comment\", props.item);\n if (state.changedKeysListWithValues === null) {\n const changedKeysListWithValues = snapshotHistory\n .slice(1)\n .map((item, index) => {\n const startingPoint = snapshotHistory[index]; // Set comparison to the previous item\n return {\n editorId: item.editor_id,\n ...getDifferentKeysWithValues(startingPoint, item),\n };\n });\n State.update({ changedKeysListWithValues });\n }\n // sort comments and timeline logs by time\n const snapShotTimeStamp = Array.isArray(snapshotHistory)\n ? snapshotHistory.map((i) => {\n return { blockHeight: null, timestamp: parseFloat(i.timestamp / 1e6) };\n })\n : [];\n const commentsTimeStampPromise = Array.isArray(comments)\n ? Promise.all(\n comments.map((item) => {\n return asyncFetch(\n `https://api.near.social/time?blockHeight=${item.blockHeight}`\n ).then((res) => {\n const timeMs = parseFloat(res.body);\n return {\n blockHeight: item.blockHeight,\n timestamp: timeMs,\n };\n });\n })\n ).then((res) => res)\n : Promise.resolve([]);\n commentsTimeStampPromise.then((commentsTimeStamp) => {\n const combinedArray = [...snapShotTimeStamp, ...commentsTimeStamp];\n combinedArray.sort((a, b) => a.timestamp - b.timestamp);\n State.update({ data: combinedArray, socialComments: comments });\n });\n}\nif ((snapshotHistory ?? []).length > 0) {\n sortTimelineAndComments();\n}\nconst Comment = ({ commentItem }) => {\n const { accountId, blockHeight } = commentItem;\n const item = {\n type: \"social\",\n path: `${accountId}/post/comment`,\n blockHeight,\n };\n const content = JSON.parse(Social.get(item.path, blockHeight) ?? \"null\");\n const link = getLinkUsingCurrentGateway(\n `bos.forum.potlock.near/widget/app?page=rfp&id=${props.id}&accountId=${accountId}&blockHeight=${blockHeight}`\n );\n function getHighlightCommentStyle() {\n const highlightComment =\n parseInt(props.blockHeight ?? \"\") === blockHeight &&\n props.accountId === accountId;\n return {\n border: highlightComment ? \"2px solid black\" : \"\",\n };\n }\n return (\n <div style={{ zIndex: 99, background: \"white\" }}>\n <div className=\"d-flex gap-2 flex-1\">\n <div className=\"d-none d-sm-flex\">\n <Widget\n src={`bos.forum.potlock.near/widget/components.molecule.Profile`}\n props={{\n accountId: accountId,\n }}\n />\n </div>\n <CommentContainer\n style={getHighlightCommentStyle()}\n className=\"rounded-2 flex-1\"\n >\n <Header className=\"d-flex gap-3 align-items-center p-2 px-3\">\n <div className=\"text-muted\">\n <a\n rel=\"noopener noreferrer\"\n target=\"_blank\"\n href={`https://bos.potlock.org/?tab=profile&accountId=${accountId}`}\n >\n <span className=\"fw-bold text-black\">{accountId}</span>\n </a>\n commented ・{\" \"}\n <Widget\n src={`near/widget/TimeAgo`}\n props={{\n blockHeight: blockHeight,\n }}\n />\n </div>\n {context.accountId && (\n <div className=\"menu\">\n <Widget\n src={`near/widget/Posts.Menu`}\n props={{\n accountId: accountId,\n blockHeight: blockHeight,\n contentPath: `/post/comment`,\n contentType: \"comment\",\n }}\n />\n </div>\n )}\n </Header>\n <div className=\"p-2 px-3\">\n <Widget\n src={`devhub.near/widget/devhub.components.molecule.MarkdownViewer`}\n props={{\n text: content.text,\n }}\n />\n <div className=\"d-flex gap-2 align-items-center mt-4\">\n <Widget\n src={`devhub.near/widget/devhub.entity.proposal.LikeButton`}\n props={{\n item: item,\n notifyAccountId: accountId,\n }}\n />\n <Widget\n src={`near/widget/CopyUrlButton`}\n props={{\n url: link,\n }}\n />\n </div>\n </div>\n </CommentContainer>\n </div>\n </div>\n );\n};\nfunction capitalizeFirstLetter(string) {\n const updated = string.replace(\"_\", \" \");\n return updated.charAt(0).toUpperCase() + updated.slice(1).toLowerCase();\n}\nfunction parseTimelineKeyAndValue(timeline, originalValue, modifiedValue) {\n const oldValue = originalValue[timeline];\n const newValue = modifiedValue[timeline];\n switch (timeline) {\n case \"status\":\n if (newValue === RFP_TIMELINE_STATUS.PROPOSAL_SELECTED) {\n return (\n <span className=\"inline-flex\">\n moved RFP to{\" \"}\n <Widget\n src={`bos.forum.potlock.near/widget/components.rfps.StatusTag`}\n props={{\n timelineStatus: newValue,\n }}\n />\n ・ selected proposal(s) are{\" \"}\n {approvedProposals.map((i, index) => (\n <span>\n <LinkToProposal id={i.proposal_id}>\n {\" \"}\n #{i.proposal_id} {i.name}\n </LinkToProposal>\n {index < approvedProposals.length - 1 && \", \"}\n </span>\n ))}\n </span>\n );\n }\n return (\n oldValue !== newValue && (\n <span className=\"inline-flex\">\n moved RFP from{\" \"}\n <Widget\n src={`bos.forum.potlock.near/widget/components.rfps.StatusTag`}\n props={{\n timelineStatus: oldValue,\n }}\n />\n to{\" \"}\n <Widget\n src={`bos.forum.potlock.near/widget/components.rfps.StatusTag`}\n props={{\n timelineStatus: newValue,\n }}\n />\n stage\n </span>\n )\n );\n default:\n return null;\n }\n}\nconst AccountProfile = ({ accountId }) => {\n return (\n <span className=\"inline-flex fw-bold text-black\">\n <Widget\n src={`bos.forum.potlock.near/widget/components.molecule.Profile`}\n props={{\n accountId: accountId,\n size: \"sm\",\n showAccountId: true,\n }}\n />\n </span>\n );\n};\nfunction symmetricDifference(arr1, arr2) {\n const diffA = arr1.filter((item) => !arr2.includes(item));\n const diffB = arr2.filter((item) => !arr1.includes(item));\n return [...diffA, ...diffB];\n}\nconst LinkToProposal = ({ id, children }) => {\n return (\n <a\n className=\"text-decoration-underline flex-1\"\n href={href({\n widgetSrc: `bos.forum.potlock.near/widget/app`,\n params: {\n page: \"proposal\",\n id: id,\n },\n })}\n target=\"_blank\"\n rel=\"noopener noreferrer\"\n >\n {children}\n </a>\n );\n};\nconst parseProposalKeyAndValue = (key, modifiedValue, originalValue) => {\n switch (key) {\n case \"name\":\n return <span>changed title</span>;\n case \"summary\":\n case \"description\":\n return <span>changed {key}</span>;\n case \"labels\":\n return <span>changed labels to {(modifiedValue ?? []).join(\", \")}</span>;\n case \"linked_proposals\": {\n const newProposals = modifiedValue || [];\n const oldProposals = originalValue || [];\n const difference = symmetricDifference(oldProposals, newProposals).join(\n \",\"\n );\n const isUnlinked = oldProposals.length > newProposals.length;\n const actionText = isUnlinked\n ? \"unlinked a proposal\"\n : \"linked a proposal\";\n return (\n <span>\n {actionText}{\" \"}\n <LinkToProposal id={difference}> #{difference}</LinkToProposal>\n </span>\n );\n }\n case \"timeline\": {\n const modifiedKeys = Object.keys(modifiedValue);\n const originalKeys = Object.keys(originalValue);\n return modifiedKeys.map((i, index) => {\n const text = parseTimelineKeyAndValue(i, originalValue, modifiedValue);\n return (\n text && (\n <span key={index} className=\"inline-flex\">\n {text}\n {text &&\n originalKeys.length > 1 &&\n index < modifiedKeys.length - 1 &&\n \"・\"}\n </span>\n )\n );\n });\n }\n default:\n return null;\n }\n};\nconst LogIconContainer = styled.div`\n margin-left: 50px;\n z-index: 99;\n @media screen and (max-width: 768px) {\n margin-left: 10px;\n }\n`;\nconst Log = ({ timestamp }) => {\n const updatedData = useMemo(\n () =>\n state.changedKeysListWithValues.find((obj) =>\n Object.values(obj).some(\n (value) =>\n value && parseFloat(value.modifiedValue / 1e6) === timestamp\n )\n ),\n [state.changedKeysListWithValues, timestamp]\n );\n const editorId = updatedData.editorId;\n const valuesArray = Object.values(updatedData ?? {});\n // if valuesArray length is 2 that means it only has timestamp and editorId\n if (!updatedData || valuesArray.length === 2) {\n return <></>;\n }\n return valuesArray.map((i, index) => {\n if (i.key && i.key !== \"timestamp\") {\n return (\n <LogIconContainer\n className=\"d-flex gap-3 align-items-center\"\n key={index}\n >\n <img\n src=\"https://ipfs.near.social/ipfs/bafkreiffqrxdi4xqu7erf46gdlwuodt6dm6rji2jtixs3iionjvga6rhdi\"\n height={30}\n />\n <div\n className={\n \"flex-1 gap-1 w-100 text-wrap text-muted align-items-center \" +\n (i.key === \"timeline\" &&\n Object.keys(i.originalValue ?? {}).length > 1\n ? \"\"\n : \"inline-flex\")\n }\n >\n <span className=\"inline-flex fw-bold text-black\">\n <AccountProfile accountId={editorId} showAccountId={true} />\n </span>\n {parseProposalKeyAndValue(i.key, i.modifiedValue, i.originalValue)}\n ・\n <Widget\n src={`near/widget/TimeAgo`}\n props={{\n blockTimestamp: timestamp * 1000000,\n }}\n />\n </div>\n </LogIconContainer>\n );\n }\n });\n};\nif (Array.isArray(state.data)) {\n return (\n <Wrapper>\n <div\n className=\"log-line\"\n style={{ height: state.data.length > 2 ? \"110%\" : \"150%\" }}\n ></div>\n <div className=\"d-flex flex-column gap-4\">\n {state.data.map((i, index) => {\n if (i.blockHeight) {\n const item = state.socialComments.find(\n (t) => t.blockHeight === i.blockHeight\n );\n return <Comment commentItem={item} />;\n } else {\n return <Log timestamp={i.timestamp} key={index} />;\n }\n })}\n </div>\n </Wrapper>\n );\n}\n" }, "components.molecule.AccountProfile": { "": "let {\n accountId,\n blockHeight,\n blockTimestamp,\n profile,\n verifications,\n showFlagAccountFeature,\n} = props;\naccountId = accountId || context.accountId;\nshowFlagAccountFeature = showFlagAccountFeature ?? false;\nprofile = profile || Social.get(`${accountId}/profile/**`, \"final\");\nconst profileUrl = `https://bos.potlock.org/?tab=profile&accountId=${accountId}`;\nconst Wrapper = styled.a`\n display: inline-grid;\n width: 100%;\n align-items: center;\n gap: 12px;\n grid-template-columns: auto 1fr;\n cursor: pointer;\n margin: 0;\n color: #687076 !important;\n outline: none;\n text-decoration: none !important;\n background: none !important;\n border: none;\n text-align: left;\n padding: 0;\n > * {\n min-width: 0;\n }\n &:hover,\n &:focus {\n div:first-child {\n border-color: #d0d5dd;\n }\n }\n`;\nconst Text = styled.p`\n margin: 0;\n font-size: 14px;\n line-height: 20px;\n color: ${(p) => (p.bold ? \"#11181C\" : \"#687076\")};\n font-weight: ${(p) => (p.bold ? \"600\" : \"400\")};\n font-size: ${(p) => (p.small ? \"10px\" : \"14px\")};\n overflow: ${(p) => (p.ellipsis ? \"hidden\" : \"\")};\n text-overflow: ${(p) => (p.ellipsis ? \"ellipsis\" : \"\")};\n white-space: nowrap !important;\n`;\nconst Avatar = styled.div`\n width: ${props.avatarSize || \"40px\"};\n height: ${props.avatarSize || \"40px\"};\n flex-shrink: 0;\n border: 1px solid #eceef0;\n overflow: hidden;\n border-radius: 40px;\n transition: border-color 200ms;\n img {\n object-fit: cover;\n width: 100%;\n height: 100%;\n margin: 0 !important;\n }\n`;\nconst VerifiedBadge = styled.div`\n position: absolute;\n left: 24px;\n top: 22px;\n`;\nconst Name = styled.div`\n display: flex;\n gap: 6px;\n align-items: center;\n`;\nconst AccountProfile = (\n <Wrapper\n href={!props.onClick && profileUrl}\n target=\"_blank\"\n rel=\"noopener noreferrer\"\n >\n <Avatar>\n <Widget\n src=\"mob.near/widget/Image\"\n props={{\n image: profile.image,\n alt: profile.name,\n fallbackUrl:\n \"https://ipfs.near.social/ipfs/bafkreibiyqabm3kl24gcb2oegb7pmwdi6wwrpui62iwb44l7uomnn3lhbi\",\n }}\n />\n </Avatar>\n {verifications && (\n <VerifiedBadge>\n <Widget\n src=\"near/widget/Settings.Identity.Verifications.Icon\"\n props={{ type: \"base\" }}\n />\n </VerifiedBadge>\n )}\n <div>\n <div>\n <div>{profile.name || accountId.split(\".near\")[0]}</div>\n {props.inlineContent}\n {props.blockHeight && (\n <div style={{ marginLeft: \"auto\" }}>\n Joined{\" \"}\n <Widget\n src=\"near/widget/TimeAgo\"\n props={{ blockHeight, blockTimestamp }}\n />\n ago\n </div>\n )}\n </div>\n {!props.hideAccountId && <div>@{accountId}</div>}\n </div>\n </Wrapper>\n);\nif (props.noOverlay) return AccountProfile;\nreturn (\n <div>dfdsfs</div>\n // <Widget\n // src=\"near/widget/AccountProfileOverlay\"\n // props={{\n // accountId,\n // profile,\n // children: AccountProfile,\n // placement: props.overlayPlacement,\n // verifications,\n // showFlagAccountFeature,\n // }}\n // />\n);\n" }, "components.proposals.Editor": { "": "const { RFP_TIMELINE_STATUS, parseJSON, isNumber } = VM.require(\n `bos.forum.potlock.near/widget/core.common`\n) || { RFP_TIMELINE_STATUS: {}, parseJSON: () => {}, isNumber: () => {} };\nconst { href } = VM.require(`devhub.near/widget/core.lib.url`);\nhref || (href = () => {});\nconst { getGlobalLabels } = VM.require(\n `bos.forum.potlock.near/widget/components.core.lib.contract`\n) || { getGlobalLabels: () => {} };\nconst { id, timestamp, rfp_id } = props;\nconst isEditPage = typeof id === \"string\";\nconst author = context.accountId;\nconst ToCDocs = \"https://aipgf.com/terms\";\nconst CoCDocs = \"https://aipgf.com/conduct\";\nif (!author) {\n return (\n <Widget\n src={`bos.forum.potlock.near/widget/components.molecule.LoginScreen`}\n />\n );\n}\nlet editProposalData = null;\nlet draftProposalData = null;\nconst draftKey = \"AI_PGF_PROPOSAL_EDIT\";\nconst rfpLabelOptions = getGlobalLabels();\nif (isEditPage) {\n editProposalData = Near.view(\"forum.potlock.near\", \"get_proposal\", {\n proposal_id: parseInt(id),\n });\n}\nconst Container = styled.div`\n input {\n font-size: 14px !important;\n }\n .card.no-border {\n border-left: none !important;\n border-right: none !important;\n margin-bottom: -3.5rem;\n }\n textarea {\n font-size: 14px !important;\n }\n .full-width-div {\n width: 100vw;\n position: relative;\n left: 50%;\n right: 50%;\n margin-left: -50vw;\n margin-right: -50vw;\n }\n .text-sm {\n font-size: 13px;\n }\n @media screen and (max-width: 768px) {\n .h6 {\n font-size: 14px !important;\n }\n .h5 {\n font-size: 16px !important;\n }\n .text-sm {\n font-size: 11px;\n }\n .gap-6 {\n gap: 0.5rem !important;\n }\n }\n .border-bottom {\n border-bottom: var(--bs-card-border-width) solid var(--bs-card-border-color);\n }\n .text-xs {\n font-size: 10px;\n }\n .flex-2 {\n flex: 2;\n }\n .flex-1 {\n flex: 1;\n }\n .bg-grey {\n background-color: #f4f4f4;\n }\n .border-bottom {\n border-bottom: 1px solid grey;\n }\n .cursor-pointer {\n cursor: pointer;\n }\n .proposal-card {\n &:hover {\n background-color: #f4f4f4;\n }\n }\n .border-1 {\n border: 1px solid #e2e6ec;\n }\n .green-btn {\n background-color: #03ba16 !important;\n border: none;\n color: white;\n &:active {\n color: white;\n }\n }\n .black-btn {\n background-color: #000 !important;\n border: none;\n color: white;\n &:active {\n color: white;\n }\n }\n .dropdown-toggle:after {\n position: absolute;\n top: 46%;\n right: 5%;\n }\n .drop-btn {\n max-width: none !important;\n }\n .dropdown-menu {\n width: 100%;\n border-radius: 0.375rem !important;\n }\n .input-icon {\n display: flex;\n height: 100%;\n align-items: center;\n border-right: 1px solid #dee2e6;\n padding-right: 10px;\n }\n /* Tooltip container */\n .custom-tooltip {\n position: relative;\n display: inline-block;\n }\n /* Tooltip text */\n .custom-tooltip .tooltiptext {\n visibility: hidden;\n width: 250px;\n background-color: #fff;\n color: #6c757d;\n text-align: center;\n padding: 10px;\n border-radius: 6px;\n font-size: 12px;\n border: 0.2px solid #6c757d;\n /* Position the tooltip text */\n position: absolute;\n z-index: 1;\n bottom: 125%;\n left: -30px;\n /* Fade in tooltip */\n opacity: 0;\n transition: opacity 0.3s;\n }\n /* Tooltip arrow */\n .custom-tooltip .tooltiptext::after {\n content: \"\";\n position: absolute;\n top: 100%;\n left: 15%;\n margin-left: -5px;\n border-width: 5px;\n border-style: solid;\n border-color: #555 transparent transparent transparent;\n }\n /* Show the tooltip text when you mouse over the tooltip container */\n .custom-tooltip:hover .tooltiptext {\n visibility: visible;\n opacity: 1;\n }\n .form-check-input:checked {\n background-color: #04a46e !important;\n border-color: #04a46e !important;\n }\n .gap-6 {\n gap: 2.5rem;\n }\n a.no-space {\n display: inline-block;\n }\n`;\nconst Heading = styled.div`\n font-size: 24px;\n font-weight: 700;\n @media screen and (max-width: 768px) {\n font-size: 18px;\n }\n`;\nconst tokensOptions = [\n { label: \"NEAR\", value: \"NEAR\" },\n { label: \"USDT\", value: \"USDT\" },\n {\n label: \"USDC\",\n value: \"USDC\",\n },\n {\n label: \"Other\",\n value: \"OTHER\",\n },\n];\nconst [linkedRfp, setLinkedRfp] = useState(rfp_id ? parseInt(rfp_id) : null);\nconst [labels, setLabels] = useState([]);\nconst [title, setTitle] = useState(null);\nconst [description, setDescription] = useState(null);\nconst [summary, setSummary] = useState(null);\nconst [consent, setConsent] = useState({ toc: false, coc: false });\nconst [linkedProposals, setLinkedProposals] = useState([]);\nconst [receiverAccount, setReceiverAccount] = useState(context.accountId);\nconst [requestedSponsorshipAmount, setRequestedSponsorshipAmount] =\n useState(null);\nconst [requestedSponsorshipToken, setRequestedSponsorshipToken] = useState(\n tokensOptions[2]\n);\nconst [allowDraft, setAllowDraft] = useState(true);\nconst [loading, setLoading] = useState(true);\nconst [disabledSubmitBtn, setDisabledSubmitBtn] = useState(false);\nconst [isDraftBtnOpen, setDraftBtnOpen] = useState(false);\nconst [selectedStatus, setSelectedStatus] = useState(\"draft\");\nconst [isReviewModalOpen, setReviewModal] = useState(false);\nconst [isCancelModalOpen, setCancelModal] = useState(false);\nconst [showProposalViewModal, setShowProposalViewModal] = useState(false); // when user creates/edit a proposal and confirm the txn, this is true\nconst [proposalId, setProposalId] = useState(null);\nconst [proposalIdsArray, setProposalIdsArray] = useState(null);\nconst [isTxnCreated, setCreateTxn] = useState(false);\nconst [oldProposalData, setOldProposalData] = useState(null);\nconst [supervisor, setSupervisor] = useState(null);\nif (allowDraft) {\n draftProposalData = Storage.privateGet(draftKey);\n}\nconst isModerator = Near.view(\n \"forum.potlock.near\",\n \"is_allowed_to_write_rfps\",\n {\n editor: context.accountId,\n }\n);\nconst memoizedDraftData = useMemo(\n () => ({\n id: editProposalData.id ?? null,\n snapshot: {\n linked_rfp: linkedRfp,\n name: title,\n description: description,\n labels: labels,\n summary: summary,\n requested_sponsorship_usd_amount: requestedSponsorshipAmount,\n requested_sponsorship_paid_in_currency: requestedSponsorshipToken.value,\n receiver_account: receiverAccount,\n },\n }),\n [\n linkedRfp,\n title,\n summary,\n description,\n labels,\n requestedSponsorshipAmount,\n requestedSponsorshipToken,\n receiverAccount,\n ]\n);\nuseEffect(() => {\n if (allowDraft) {\n let data = editProposalData || JSON.parse(draftProposalData);\n let snapshot = data.snapshot;\n if (data) {\n if (timestamp) {\n snapshot =\n data.snapshot_history.find((item) => item.timestamp === timestamp) ??\n data.snapshot;\n }\n if (\n draftProposalData &&\n editProposalData &&\n editProposalData.id === JSON.parse(draftProposalData).id\n ) {\n snapshot = {\n ...editProposalData.snapshot,\n ...JSON.parse(draftProposalData).snapshot,\n };\n }\n if (!isNumber(linkedRfp)) {\n setLinkedRfp(snapshot.linked_rfp);\n }\n setLabels(snapshot.labels ?? []);\n setTitle(snapshot.name);\n setSummary(snapshot.summary);\n setDescription(snapshot.description);\n setReceiverAccount(snapshot.receiver_account);\n setRequestedSponsorshipAmount(snapshot.requested_sponsorship_usd_amount);\n setSupervisor(snapshot.supervisor);\n const token = tokensOptions.find(\n (item) => item.value === snapshot.requested_sponsorship_paid_in_currency\n );\n setRequestedSponsorshipToken(token ?? tokensOptions[2]);\n if (isEditPage) {\n setConsent({ toc: true, coc: true });\n }\n }\n }\n}, [editProposalData, draftProposalData, allowDraft]);\n// show loader until LS data is set in state\nuseEffect(() => {\n const handler = setTimeout(() => {\n setAllowDraft(false);\n setLoading(false);\n }, 500);\n return () => clearTimeout(handler);\n}, []);\nuseEffect(() => {\n if (showProposalViewModal) {\n return;\n }\n setDisabledSubmitBtn(\n isTxnCreated ||\n !title ||\n !description ||\n !summary ||\n !(labels ?? []).length ||\n !requestedSponsorshipAmount ||\n !receiverAccount ||\n !consent.toc ||\n !consent.coc\n );\n const handler = setTimeout(() => {\n Storage.privateSet(draftKey, JSON.stringify(memoizedDraftData));\n }, 10000);\n return () => clearTimeout(handler);\n}, [\n memoizedDraftData,\n draftKey,\n draftProposalData,\n consent,\n isTxnCreated,\n showProposalViewModal,\n]);\n// set RFP labels, disable link rfp change when linked rfp is past accepting stage\nconst [disabledLinkRFP, setDisableLinkRFP] = useState(false);\nuseEffect(() => {\n if (linkedRfp) {\n Near.asyncView(\"forum.potlock.near\", \"get_rfp\", {\n rfp_id: linkedRfp.value ?? linkedRfp,\n }).then((i) => {\n const timeline = parseJSON(i.snapshot.timeline);\n setDisableLinkRFP(\n !isModerator &&\n timeline.status !== RFP_TIMELINE_STATUS.ACCEPTING_SUBMISSIONS\n );\n setLabels(i.snapshot.labels);\n });\n }\n}, [linkedRfp]);\nuseEffect(() => {\n if (\n editProposalData &&\n editProposalData?.snapshot?.linked_proposals?.length > 0\n ) {\n editProposalData.snapshot.linked_proposals.map((item) => {\n useCache(\n () =>\n Near.asyncView(\"forum.potlock.near\", \"get_proposal\", {\n proposal_id: parseInt(item),\n }).then((proposal) => {\n setLinkedProposals([\n ...linkedProposals,\n {\n label: \"# \" + proposal.id + \" : \" + proposal.snapshot.name,\n value: proposal.id,\n },\n ]);\n }),\n item + \"linked_proposals\",\n { subscribe: false }\n );\n });\n }\n}, [editProposalData]);\nconst InputContainer = ({ heading, description, children }) => {\n return (\n <div className=\"d-flex flex-column gap-1 gap-sm-2 w-100\">\n <b className=\"h6 mb-0\">{heading}</b>\n {description && (\n <div className=\"text-muted w-100 text-sm\">{description}</div>\n )}\n {children}\n </div>\n );\n};\n// show proposal created after txn approval for popup wallet\nuseEffect(() => {\n if (isTxnCreated) {\n if (editProposalData) {\n setOldProposalData(editProposalData);\n if (\n editProposalData &&\n typeof editProposalData === \"object\" &&\n oldProposalData &&\n typeof oldProposalData === \"object\" &&\n JSON.stringify(editProposalData) !== JSON.stringify(oldProposalData)\n ) {\n setCreateTxn(false);\n setProposalId(editProposalData.id);\n setShowProposalViewModal(true);\n }\n } else {\n const proposalIds = Near.view(\n \"forum.potlock.near\",\n \"get_all_proposal_ids\"\n );\n if (Array.isArray(proposalIds) && !proposalIdsArray) {\n setProposalIdsArray(proposalIds);\n }\n if (\n Array.isArray(proposalIds) &&\n Array.isArray(proposalIdsArray) &&\n proposalIds.length !== proposalIdsArray.length\n ) {\n setCreateTxn(false);\n setProposalId(proposalIds[proposalIds.length - 1]);\n setShowProposalViewModal(true);\n }\n }\n }\n setLoading(false);\n});\nuseEffect(() => {\n if (props.transactionHashes) {\n setLoading(true);\n useCache(\n () =>\n asyncFetch(\"https://rpc.mainnet.near.org\", {\n method: \"POST\",\n headers: {\n \"content-type\": \"application/json\",\n },\n body: JSON.stringify({\n jsonrpc: \"2.0\",\n id: \"dontcare\",\n method: \"tx\",\n params: [props.transactionHashes, context.accountId],\n }),\n }).then((transaction) => {\n const transaction_method_name =\n transaction?.body?.result?.transaction?.actions[0].FunctionCall\n .method_name;\n const is_edit_or_add_post_transaction =\n transaction_method_name == \"add_proposal\" ||\n transaction_method_name == \"edit_proposal\";\n if (is_edit_or_add_post_transaction) {\n setShowProposalViewModal(true);\n Storage.privateSet(draftKey, null);\n }\n // show the latest created proposal to user\n if (transaction_method_name == \"add_proposal\") {\n useCache(\n () =>\n Near.asyncView(\n \"forum.potlock.near\",\n \"get_all_proposal_ids\"\n ).then((proposalIdsArray) => {\n setProposalId(\n proposalIdsArray?.[proposalIdsArray?.length - 1]\n );\n }),\n props.transactionHashes + \"proposalIds\",\n { subscribe: false }\n );\n } else {\n setProposalId(id);\n }\n setLoading(false);\n }),\n props.transactionHashes + context.accountId,\n { subscribe: false }\n );\n } else {\n if (showProposalViewModal) {\n setShowProposalViewModal(false);\n }\n }\n}, [props.transactionHashes]);\nconst DropdowntBtnContainer = styled.div`\n font-size: 13px;\n min-width: 150px;\n .custom-select {\n position: relative;\n }\n .select-header {\n display: flex;\n justify-content: space-between;\n align-items: center;\n border: 1px solid #ccc;\n border-radius-top: 5px;\n cursor: pointer;\n background-color: #fff;\n border-radius: 5px;\n }\n .no-border {\n border: none !important;\n }\n .options-card {\n position: absolute;\n top: 100%;\n left: 0;\n width: 200%;\n border: 1px solid #ccc;\n background-color: #fff;\n padding: 0.5rem;\n z-index: 99;\n font-size: 13px;\n border-radius:0.375rem !important;\n }\n .left {\n right: 0 !important;\n left: auto !important;\n }\n @media screen and (max-width: 768px) {\n .options-card {\n right: 0 !important;\n left: auto !important;\n }\n }\n .option {\n margin-block: 5px;\n padding: 10px;\n cursor: pointer;\n border-bottom: 1px solid #f0f0f0;\n transition: background-color 0.3s ease;\n border-radius: 0.375rem !important;\n }\n .option:hover {\n background-color: #f0f0f0; /* Custom hover effect color */\n }\n .option:last-child {\n border-bottom: none;\n }\n .selected {\n background-color: #f0f0f0;\n }\n .disabled {\n background-color: #f4f4f4 !important;\n cursor: not-allowed !important;\n font-weight: 500;\n color: #b3b3b3;\n }\n .disabled .circle {\n opacity: 0.5;\n }\n .circle {\n width: 8px;\n height: 8px;\n border-radius: 50%;\n }\n .grey {\n background-color: #818181;\n }\n .green {\n background-color: #04a46e;\n }\n a:hover {\n text-decoration: none;\n }\n}\n`;\nconst LoadingButtonSpinner = (\n <span\n class=\"submit-proposal-draft-loading-indicator spinner-border spinner-border-sm\"\n role=\"status\"\n aria-hidden=\"true\"\n ></span>\n);\nconst SubmitBtn = () => {\n const btnOptions = [\n {\n iconColor: \"grey\",\n label: \"Submit Draft\",\n description:\n \"The author can still edit the proposal and build consensus before sharing it with sponsors.\",\n value: \"draft\",\n },\n {\n iconColor: \"green\",\n label: \"Ready for Review\",\n description:\n \"Start the official review process with sponsors. This will lock the editing function, but comments are still open.\",\n value: \"review\",\n },\n ];\n const handleOptionClick = (option) => {\n setDraftBtnOpen(false);\n setSelectedStatus(option.value);\n handleSubmit(option.value);\n };\n const toggleDropdown = () => {\n setDraftBtnOpen(!isDraftBtnOpen);\n };\n const handleSubmit = (status) => {\n const isDraft = status === \"draft\";\n if (isDraft) {\n onSubmit({ isDraft });\n cleanDraft();\n } else {\n setReviewModal(true);\n }\n };\n const selectedOption = btnOptions.find((i) => i.value === selectedStatus);\n return (\n <DropdowntBtnContainer>\n <div\n className=\"custom-select\"\n tabIndex=\"0\"\n onBlur={() => setDraftBtnOpen(false)}\n >\n <div\n className={\n \"select-header d-flex gap-1 align-items-center submit-draft-button \" +\n (disabledSubmitBtn && \"disabled\")\n }\n >\n <div\n onClick={() => !disabledSubmitBtn && handleSubmit(selectedStatus)}\n className=\"p-2 d-flex gap-2 align-items-center \"\n >\n {isTxnCreated ? (\n LoadingButtonSpinner\n ) : (\n <div className={\"circle \" + selectedOption.iconColor}></div>\n )}\n <div className={`selected-option`}>{selectedOption.label}</div>\n </div>\n <div\n className=\"h-100 p-2\"\n style={{ borderLeft: \"1px solid #ccc\" }}\n onClick={!disabledSubmitBtn && toggleDropdown}\n >\n <i class={`bi bi-chevron-${isDraftBtnOpen ? \"up\" : \"down\"}`}></i>\n </div>\n </div>\n {isDraftBtnOpen && (\n <div className=\"options-card\">\n {btnOptions.map((option) => (\n <div\n key={option.value}\n className={`option ${\n selectedOption.value === option.value ? \"selected\" : \"\"\n }`}\n onClick={() => handleOptionClick(option)}\n >\n <div className={`d-flex gap-2 align-items-center`}>\n <div className={\"circle \" + option.iconColor}></div>\n <div className=\"fw-bold\">{option.label}</div>\n </div>\n <div className=\"text-muted text-xs\">{option.description}</div>\n </div>\n ))}\n </div>\n )}\n </div>\n </DropdowntBtnContainer>\n );\n};\nconst onSubmit = ({ isDraft, isCancel }) => {\n setCreateTxn(true);\n console.log(\"submitting transaction\");\n const linkedProposalsIds = linkedProposals.map((item) => item.value) ?? [];\n const body = {\n proposal_body_version: \"V1\",\n linked_rfp: linkedRfp?.value,\n category: \"AI PGF\",\n name: title,\n description: description,\n summary: summary,\n linked_proposals: linkedProposalsIds,\n requested_sponsorship_usd_amount: requestedSponsorshipAmount,\n requested_sponsorship_paid_in_currency: requestedSponsorshipToken.value,\n receiver_account: receiverAccount,\n requested_sponsor: \"impact.sputnik-dao.near\",\n supervisor: supervisor,\n timeline: isCancel\n ? {\n status: \"CANCELLED\",\n sponsor_requested_review: false,\n reviewer_completed_attestation: false,\n }\n : isDraft\n ? { status: \"DRAFT\" }\n : {\n status: \"REVIEW\",\n sponsor_requested_review: false,\n reviewer_completed_attestation: false,\n },\n };\n const args = {\n labels: linkedRfp ? [] : (labels ?? []).map((i) => i.value ?? i),\n body: body,\n };\n if (isEditPage) {\n args[\"id\"] = editProposalData.id;\n }\n Near.call([\n {\n contractName: \"forum.potlock.near\",\n methodName: isEditPage ? \"edit_proposal\" : \"add_proposal\",\n args: args,\n gas: 270000000000000,\n deposit: \"100000000000000000000000\",\n },\n ]);\n};\nfunction cleanDraft() {\n Storage.privateSet(draftKey, null);\n}\nif (loading) {\n return (\n <div\n style={{ height: \"50vh\" }}\n className=\"d-flex justify-content-center align-items-center w-100\"\n >\n <Widget src={`devhub.near/widget/devhub.components.molecule.Spinner`} />\n </div>\n );\n}\nconst [collapseState, setCollapseState] = useState({});\nconst CollapsibleContainer = ({ title, children, noPaddingTop }) => {\n return (\n <div\n className={\n \"border-bottom py-4 \" +\n (noPaddingTop && \"pt-0 \") +\n (collapseState[title] && \" pb-0\")\n }\n >\n <div className={\"d-flex justify-content-between \"}>\n <div className=\"h5 text-muted mb-2 mb-sm-3\">{title}</div>\n <div\n className=\"d-flex d-sm-none cursor-pointer\"\n onClick={() =>\n setCollapseState((prevState) => ({\n ...prevState,\n [title]: !prevState[title],\n }))\n }\n >\n {!collapseState[title] ? (\n <i class=\"bi bi-chevron-up h4\"></i>\n ) : (\n <i class=\"bi bi-chevron-down h4\"></i>\n )}\n </div>\n </div>\n <div className={!collapseState[title] ? \"\" : \"d-none\"}>{children}</div>\n </div>\n );\n};\nconst CategoryDropdown = useMemo(() => {\n return (\n <Widget\n src={`bos.forum.potlock.near/widget/components.molecule.MultiSelectCategoryDropdown`}\n props={{\n selected: labels,\n onChange: (v) => setLabels(v),\n disabled: linkedRfp, // when RFP is linked, labels are disabled\n linkedRfp: linkedRfp,\n availableOptions: rfpLabelOptions,\n }}\n />\n );\n}, [draftProposalData, linkedRfp, labels]);\nconst TitleComponent = useMemo(() => {\n return (\n <Widget\n src={`devhub.near/widget/devhub.components.molecule.Input`}\n props={{\n className: \"flex-grow-1\",\n value: title,\n onBlur: (e) => {\n setTitle(e.target.value);\n },\n skipPaddingGap: true,\n inputProps: {\n max: 80,\n },\n }}\n />\n );\n}, [draftProposalData]);\nconst SummaryComponent = useMemo(() => {\n return (\n <Widget\n src={`devhub.near/widget/devhub.components.molecule.Input`}\n props={{\n className: \"flex-grow-1\",\n value: summary,\n multiline: true,\n onBlur: (e) => {\n setSummary(e.target.value);\n },\n skipPaddingGap: true,\n inputProps: {\n max: 500,\n },\n }}\n />\n );\n}, [draftProposalData]);\nconst DescriptionComponent = useMemo(() => {\n return (\n <Widget\n src={`bos.forum.potlock.near/widget/components.molecule.Compose`}\n props={{\n data: description,\n onChange: setDescription,\n autocompleteEnabled: true,\n autoFocus: false,\n showProposalIdAutoComplete: true,\n }}\n />\n );\n}, [draftProposalData]);\nconst ConsentComponent = useMemo(() => {\n return (\n <div className=\"d-flex flex-column gap-2\">\n <Widget\n src={`devhub.near/widget/devhub.components.molecule.Checkbox`}\n props={{\n value: \"toc\",\n label: (\n <>\n I’ve agree to{\" \"}\n <a\n href={ToCDocs}\n className=\"text-decoration-underline\"\n target=\"_blank\"\n rel=\"noopener noreferrer\"\n >\n AI PGF's Terms and Conditions\n </a>\n and commit to honoring it\n </>\n ),\n isChecked: consent.toc,\n onClick: (value) =>\n setConsent((prevConsent) => ({\n ...prevConsent,\n toc: value,\n })),\n }}\n />\n <Widget\n src={`devhub.near/widget/devhub.components.molecule.Checkbox`}\n props={{\n value: \"coc\",\n label: (\n <>\n I’ve read{\" \"}\n <a\n href={CoCDocs}\n className=\"text-decoration-underline\"\n target=\"_blank\"\n rel=\"noopener noreferrer\"\n >\n AI PGF's Code of Conduct\n </a>\n and commit to honoring it\n </>\n ),\n isChecked: consent.coc,\n onClick: (value) =>\n setConsent((prevConsent) => ({\n ...prevConsent,\n coc: value,\n })),\n }}\n />\n </div>\n );\n}, [draftProposalData]);\nconst ProfileComponent = useMemo(() => {\n return (\n <Widget\n src=\"mob.near/widget/Profile.ShortInlineBlock\"\n props={{\n accountId: author,\n }}\n />\n );\n}, []);\nconst LinkRFPComponent = useMemo(() => {\n return (\n <div className=\"d-flex flex-column gap-1\">\n <Widget\n src={`bos.forum.potlock.near/widget/components.molecule.LinkedRfpDropdown`}\n props={{\n onChange: setLinkedRfp,\n linkedRfp: linkedRfp,\n disabled: disabledLinkRFP,\n onDeleteRfp: () => setLabels([]),\n }}\n />\n </div>\n );\n}, [draftProposalData, disabledLinkRFP]);\nconst LinkedProposalsComponent = useMemo(() => {\n return (\n <div className=\"d-flex flex-column gap-1\">\n <div className=\"text-muted w-100 text-sm\">\n Link any relevant proposals (e.g. previous milestones).\n </div>\n <Widget\n src={`bos.forum.potlock.near/widget/components.molecule.LinkedProposalsDropdown`}\n props={{\n onChange: setLinkedProposals,\n linkedProposals: linkedProposals,\n }}\n />\n </div>\n );\n}, [draftProposalData]);\nconst ReceiverAccountComponent = useMemo(() => {\n return (\n <Widget\n src={`devhub.near/widget/devhub.entity.proposal.AccountInput`}\n props={{\n value: receiverAccount,\n placeholder: \"Enter Address\",\n onUpdate: setReceiverAccount,\n }}\n />\n );\n}, [draftProposalData]);\nconst AmountComponent = useMemo(() => {\n return (\n <Widget\n src={`devhub.near/widget/devhub.components.molecule.Input`}\n props={{\n className: \"flex-grow-1\",\n value: requestedSponsorshipAmount,\n onChange: (e) => {\n setRequestedSponsorshipAmount(e.target.value);\n },\n skipPaddingGap: true,\n inputProps: {\n type: \"text\",\n prefix: \"$\",\n inputmode: \"numeric\",\n pattern: \"[0-9]*\",\n },\n }}\n />\n );\n}, [draftProposalData]);\nconst CurrencyComponent = useMemo(() => {\n return (\n <Widget\n src={`devhub.near/widget/devhub.components.molecule.DropDown`}\n props={{\n options: tokensOptions,\n selectedValue: requestedSponsorshipToken,\n onUpdate: (v) => {\n setRequestedSponsorshipToken(v);\n },\n }}\n />\n );\n}, [draftProposalData]);\nreturn (\n <Container className=\"w-100 py-4 px-0 px-sm-2 d-flex flex-column gap-3\">\n <Heading className=\"px-2 px-sm-0\">\n {isEditPage ? \"Edit\" : \"Create\"} Proposal\n </Heading>\n <Widget\n src={`bos.forum.potlock.near/widget/components.proposals.ViewProposalModal`}\n props={{\n isOpen: showProposalViewModal,\n isEdit: isEditPage,\n proposalId: proposalId,\n }}\n />\n <Widget\n src={`devhub.near/widget/devhub.entity.proposal.ConfirmReviewModal`}\n props={{\n isOpen: isReviewModalOpen,\n onCancelClick: () => setReviewModal(false),\n onReviewClick: () => {\n setReviewModal(false);\n cleanDraft();\n onSubmit({ isDraft: false });\n },\n }}\n />\n <Widget\n src={`devhub.near/widget/devhub.entity.proposal.ConfirmCancelModal`}\n props={{\n isOpen: isCancelModalOpen,\n onCancelClick: () => setCancelModal(false),\n onConfirmClick: () => {\n setCancelModal(false);\n onSubmit({ isCancel: true });\n },\n }}\n />\n <div className=\"card no-border rounded-0 px-2 p-lg-0 full-width-div\">\n <div className=\"container-xl py-4 d-flex flex-wrap gap-6 w-100\">\n <div\n style={{ minWidth: \"350px\" }}\n className=\"flex-2 w-100 order-2 order-md-1\"\n >\n <div className=\"d-flex gap-2 w-100\">\n <div className=\"d-none d-sm-flex\" style={{ height: \"max-content\" }}>\n <Widget\n src={`bos.forum.potlock.near/widget/components.molecule.Profile`}\n props={{\n accountId: author,\n }}\n />\n </div>\n <div className=\"d-flex flex-column gap-4 w-100\">\n <div className=\"border-bottom pb-4\">\n <InputContainer\n heading=\"Link RFP (Optional)\"\n description={\n \"Link this proposal if it is a response to a specific RFP. You can only link to active RFPs in the “Accepting Submission” stage. You can only link to one RFP.\"\n }\n >\n {LinkRFPComponent}\n </InputContainer>\n </div>\n <InputContainer\n heading=\"Category\"\n description={\n <>\n Select the relevant categories that best align with your\n contribution to the NEAR developer community.\n </>\n }\n >\n {CategoryDropdown}\n </InputContainer>\n <InputContainer\n heading=\"Title\"\n description=\"Highlight the essence of your proposal in a few words. This will appear on your proposal’s detail page and the main proposal feed. Keep it short, please :)\"\n >\n {TitleComponent}\n </InputContainer>\n <InputContainer\n heading=\"Summary\"\n description=\"Explain your proposal briefly. This is your chance to make a good first impression on the community. Include what needs or goals your work will address, your solution, and the benefit for the NEAR developer community.\"\n >\n {SummaryComponent}\n </InputContainer>\n <InputContainer\n heading=\"Description\"\n description={\n <>\n Expand on your summary with any relevant details like your\n contribution timeline, key milestones, team background, and\n a clear breakdown of how the funds will be used. Proposals\n should be simple and clear (e.g. 1 month). For more complex\n projects, treat each milestone as a separate proposal.\n </>\n }\n >\n {DescriptionComponent}\n </InputContainer>\n <InputContainer heading=\"Final Consent\">\n {ConsentComponent}\n </InputContainer>\n <div className=\"d-flex justify-content-between gap-2 align-items-center\">\n <div>\n {isEditPage && (\n <Widget\n src={`devhub.near/widget/devhub.components.molecule.Button`}\n props={{\n classNames: {\n root: \"btn-outline-danger shadow-none border-0 btn-sm\",\n },\n label: (\n <div className=\"d-flex align-items-center gap-1\">\n <i class=\"bi bi-trash3\"></i> Cancel Proposal\n </div>\n ),\n onClick: () => setCancelModal(true),\n }}\n />\n )}\n </div>\n <div\n className=\"d-flex gap-2\"\n style={{\n height: isDraftBtnOpen ? \"25vh\" : \"auto\",\n alignItems: isDraftBtnOpen ? \"flex-start\" : \"center\",\n }}\n >\n <Link\n to={\n isEditPage\n ? href({\n widgetSrc: `bos.forum.potlock.near/widget/app`,\n params: {\n page: \"proposal\",\n id: parseInt(id),\n },\n })\n : href({\n widgetSrc: `bos.forum.potlock.near/widget/app`,\n params: {\n page: \"proposals\",\n },\n })\n }\n >\n <Widget\n src={`devhub.near/widget/devhub.components.molecule.Button`}\n props={{\n classNames: {\n root: \"d-flex h-100 text-muted fw-bold btn-outline shadow-none border-0 btn-sm\",\n },\n label: \"Discard Changes\",\n onClick: cleanDraft,\n }}\n />\n </Link>\n <SubmitBtn />\n </div>\n </div>\n </div>\n </div>\n </div>\n <div\n style={{ minWidth: \"350px\" }}\n className=\"flex-1 w-100 order-1 order-md-2\"\n >\n <CollapsibleContainer noPaddingTop={true} title=\"Author Details\">\n <div className=\"d-flex flex-column gap-3 gap-sm-4\">\n <InputContainer heading=\"Author\">\n {ProfileComponent}\n </InputContainer>\n </div>\n </CollapsibleContainer>\n <div className=\"my-2\">\n <CollapsibleContainer title=\"Link Proposals (Optional)\">\n {LinkedProposalsComponent}\n </CollapsibleContainer>\n </div>\n <div className=\"my-2\">\n <CollapsibleContainer title=\"Funding Details\">\n <div className=\"d-flex flex-column gap-3 gap-sm-4\">\n <InputContainer\n heading=\"Recipient NEAR Wallet Address\"\n description=\"Enter the address that will receive the funds. We’ll need this to send a test transaction once your proposal is approved.\"\n >\n {ReceiverAccountComponent}\n </InputContainer>\n <InputContainer\n heading={\n <div className=\"d-flex gap-2 align-items-center\">\n Recipient Verification Status\n <div className=\"custom-tooltip\">\n <i class=\"bi bi-info-circle-fill\"></i>\n <span class=\"tooltiptext\">\n To get approved and receive payments on our platform,\n you must complete KYC/KYB verification using Fractal,\n a trusted identity verification solution. This helps\n others trust transactions with your account. Click\n \"Get Verified\" to start. <br />\n <br />\n Once verified, your profile will display a badge,\n which is valid for 365 days from the date of your\n verification. You must renew your verification upon\n expiration OR if any of your personal information\n changes.\n </span>\n </div>\n </div>\n }\n description=\"\"\n >\n <div className=\"border border-1 p-3 rounded-2\">\n <Widget\n src={`devhub.near/widget/devhub.entity.proposal.VerificationStatus`}\n props={{\n receiverAccount: receiverAccount,\n showGetVerifiedBtn: true,\n imageSize: 30,\n }}\n />\n </div>\n </InputContainer>\n <InputContainer\n heading=\"Total Amount (USD)\"\n description={<>Enter the exact amount you are seeking.</>}\n >\n {AmountComponent}\n </InputContainer>\n <InputContainer\n heading=\"Currency\"\n description=\"Select your preferred currency for receiving funds. Note: The exchange rate for NEAR tokens will be the closing rate at the day of the invoice.\"\n >\n {CurrencyComponent}\n </InputContainer>\n </div>\n </CollapsibleContainer>\n </div>\n </div>\n </div>\n </div>\n </Container>\n);\n" }, "components.molecule.Profile": { "": "const accountId = props.accountId;\nconst size = props.size ?? \"md\";\nconst showAccountId = props.showAccountId;\nconst Avatar = styled.div`\n &.sm {\n min-width: 30px;\n max-width: 30px;\n min-height: 30px;\n max-height: 30px;\n }\n &.md {\n min-width: 40px;\n max-width: 40px;\n min-height: 40px;\n max-height: 40px;\n }\n pointer-events: none;\n flex-shrink: 0;\n border: 1px solid #eceef0;\n overflow: hidden;\n border-radius: 40px;\n transition: border-color 200ms;\n img {\n object-fit: cover;\n width: 100%;\n height: 100%;\n margin: 0 !important;\n }\n`;\nconst profile = Social.get(`${accountId}/profile/**`, \"final\");\nconst profileUrl = `https://bos.potlock.org/?tab=profile&accountId=${accountId}`;\nreturn (\n <a rel=\"noopener noreferrer\" target=\"_blank\" href={profileUrl}>\n <div className=\"d-flex gap-2 align-items-center\">\n <Avatar className={size}>\n <Widget\n src=\"mob.near/widget/Image\"\n props={{\n image: profile.image,\n alt: profile.name,\n fallbackUrl:\n \"https://ipfs.near.social/ipfs/bafkreibiyqabm3kl24gcb2oegb7pmwdi6wwrpui62iwb44l7uomnn3lhbi\",\n }}\n />\n </Avatar>\n {showAccountId && (\n <div>\n {(accountId ?? \"\").substring(0, 20)}\n {(accountId ?? \"\").length > 20 ? \"...\" : \"\"}\n </div>\n )}\n </div>\n </a>\n);\n" }, "components.rfps.Editor": { "": "const { RFP_TIMELINE_STATUS, parseJSON } = VM.require(\n `bos.forum.potlock.near/widget/core.common`\n) || { RFP_TIMELINE_STATUS: {}, parseJSON: () => {} };\nconst { href } = VM.require(`devhub.near/widget/core.lib.url`);\nconst draftKey = \"AI_PGF_RFP_EDIT\";\nhref || (href = () => {});\nconst { getGlobalLabels } = VM.require(\n `bos.forum.potlock.near/widget/components.core.lib.contract`\n) || { getGlobalLabels: () => {} };\nconst { id, timestamp } = props;\nconst isEditPage = typeof id === \"string\";\nconst author = context.accountId;\nconst ToCDocs = \"https://aipgf.com/terms\";\nconst CoCDocs = \"https://aipgf.com/conduct\";\nconst rfpLabelOptions = getGlobalLabels();\nconst isAllowedToWriteRfp = Near.view(\n \"forum.potlock.near\",\n \"is_allowed_to_write_rfps\",\n {\n editor: context.accountId,\n }\n);\nif (!author || !isAllowedToWriteRfp) {\n return (\n <Widget\n src={`bos.forum.potlock.near/widget/components.molecule.LoginScreen`}\n />\n );\n}\nlet editRfpData = null;\nlet draftRfpData = null;\nif (isEditPage) {\n editRfpData = Near.view(`forum.potlock.near`, \"get_rfp\", {\n rfp_id: parseInt(id),\n });\n}\nconst Container = styled.div`\n input {\n font-size: 14px !important;\n }\n .card.no-border {\n border-left: none !important;\n border-right: none !important;\n margin-bottom: -3.5rem;\n }\n textarea {\n font-size: 14px !important;\n }\n .full-width-div {\n width: 100vw;\n position: relative;\n left: 50%;\n right: 50%;\n margin-left: -50vw;\n margin-right: -50vw;\n }\n .text-sm {\n font-size: 13px;\n }\n .h5 {\n font-size: 18px !important;\n }\n @media screen and (max-width: 768px) {\n .h6 {\n font-size: 14px !important;\n }\n .h5 {\n font-size: 16px !important;\n }\n .text-sm {\n font-size: 11px;\n }\n .gap-6 {\n gap: 0.5rem !important;\n }\n }\n .border-bottom {\n border-bottom: var(--bs-card-border-width) solid var(--bs-card-border-color);\n }\n .text-xs {\n font-size: 10px;\n }\n .flex-2 {\n flex: 2;\n }\n .flex-1 {\n flex: 1;\n }\n .bg-grey {\n background-color: #f4f4f4;\n }\n .border-bottom {\n border-bottom: 1px solid grey;\n }\n .cursor-pointer {\n cursor: pointer;\n }\n .border-1 {\n border: 1px solid #e2e6ec;\n }\n .black-btn {\n background-color: #000 !important;\n border: none;\n color: white;\n &:active {\n color: white;\n }\n }\n .dropdown-toggle:after {\n position: absolute;\n top: 46%;\n right: 5%;\n }\n .drop-btn {\n max-width: none !important;\n }\n .dropdown-menu {\n width: 100%;\n border-radius: 0.375rem !important;\n }\n .input-icon {\n display: flex;\n height: 100%;\n align-items: center;\n border-right: 1px solid #dee2e6;\n padding-right: 10px;\n }\n /* Tooltip container */\n .custom-tooltip {\n position: relative;\n display: inline-block;\n }\n /* Tooltip text */\n .custom-tooltip .tooltiptext {\n visibility: hidden;\n width: 250px;\n background-color: #fff;\n color: #6c757d;\n text-align: center;\n padding: 10px;\n border-radius: 6px;\n font-size: 12px;\n border: 0.2px solid #6c757d;\n /* Position the tooltip text */\n position: absolute;\n z-index: 1;\n bottom: 125%;\n left: -30px;\n /* Fade in tooltip */\n opacity: 0;\n transition: opacity 0.3s;\n }\n /* Tooltip arrow */\n .custom-tooltip .tooltiptext::after {\n content: \"\";\n position: absolute;\n top: 100%;\n left: 15%;\n margin-left: -5px;\n border-width: 5px;\n border-style: solid;\n border-color: #555 transparent transparent transparent;\n }\n /* Show the tooltip text when you mouse over the tooltip container */\n .custom-tooltip:hover .tooltiptext {\n visibility: visible;\n opacity: 1;\n }\n .form-check-input:checked {\n background-color: #04a46e !important;\n border-color: #04a46e !important;\n }\n .gap-6 {\n gap: 2.5rem;\n }\n a.no-space {\n display: inline-block;\n }\n .fw-light-bold {\n font-weight: 600 !important;\n }\n .disabled .circle {\n opacity: 0.5;\n }\n .circle {\n width: 6px;\n height: 6px;\n border-radius: 50%;\n }\n .grey {\n background-color: #818181;\n }\n @media screen and (max-width: 970px) {\n .gap-6 {\n gap: 1.5rem !important;\n }\n }\n @media screen and (max-width: 570px) {\n .gap-6 {\n gap: 0.5rem !important;\n }\n }\n`;\nconst Heading = styled.div`\n font-size: 24px;\n font-weight: 700;\n @media screen and (max-width: 768px) {\n font-size: 18px;\n }\n`;\nfunction getTimestamp(date) {\n // in nanoseconds\n const parsedDate = date ? new Date(date) : new Date();\n return Math.floor(parsedDate.getTime() * 1000000).toString();\n}\nfunction getDate(timestamp) {\n const stamp =\n !timestamp || timestamp === \"0\" || timestamp === \"NaN\" ? null : timestamp;\n return new Date(parseFloat(stamp / 1000000)).toISOString().split(\"T\")[0];\n}\nconst [labels, setLabels] = useState([]);\nconst [title, setTitle] = useState(null);\nconst [description, setDescription] = useState(null);\nconst [summary, setSummary] = useState(null);\nconst [consent, setConsent] = useState({ toc: false, coc: false });\nconst [submissionDeadline, setSubmissionDeadline] = useState(null);\nconst [allowDraft, setAllowDraft] = useState(true);\nconst [loading, setLoading] = useState(true);\nconst [disabledSubmitBtn, setDisabledSubmitBtn] = useState(false);\nconst [isDraftBtnOpen, setDraftBtnOpen] = useState(false);\nconst [showRfpViewModal, setShowRfpViewModal] = useState(false); // when user creates/edit a RFP and confirm the txn, this is true\nconst [rfpId, setRfpId] = useState(null);\nconst [rfpIdsArray, setRfpIdsArray] = useState(null);\nconst [isTxnCreated, setCreateTxn] = useState(false);\nconst [oldRfpData, setOldRfpData] = useState(null);\nconst [timeline, setTimeline] = useState({\n status: RFP_TIMELINE_STATUS.ACCEPTING_SUBMISSIONS,\n});\nif (allowDraft) {\n draftRfpData = Storage.privateGet(draftKey);\n}\nconst memoizedDraftData = useMemo(\n () => ({\n id: editRfpData.id ?? null,\n snapshot: {\n name: title,\n description: description,\n labels: labels,\n summary: summary,\n submission_deadline: getTimestamp(submissionDeadline),\n },\n }),\n [title, summary, description, submissionDeadline, labels]\n);\nuseEffect(() => {\n if (allowDraft) {\n let data = editRfpData || JSON.parse(draftRfpData);\n let snapshot = data.snapshot;\n if (data) {\n if (timestamp) {\n snapshot =\n data.snapshot_history.find((item) => item.timestamp === timestamp) ??\n data.snapshot;\n }\n if (\n draftRfpData &&\n editRfpData &&\n editRfpData.id === JSON.parse(draftRfpData).id\n ) {\n snapshot = {\n ...editRfpData.snapshot,\n ...JSON.parse(draftRfpData).snapshot,\n };\n }\n setRfpId(data.id);\n setLabels(snapshot.labels);\n setTitle(snapshot.name);\n setSummary(snapshot.summary);\n setDescription(snapshot.description);\n setSubmissionDeadline(getDate(snapshot.submission_deadline));\n setTimeline(parseJSON(snapshot.timeline));\n if (isEditPage) {\n setConsent({ toc: true, coc: true });\n }\n }\n }\n}, [editRfpData, draftRfpData, allowDraft]);\n// show loader until LS data is set in state\nuseEffect(() => {\n const handler = setTimeout(() => {\n setAllowDraft(false);\n setLoading(false);\n }, 200);\n return () => clearTimeout(handler);\n}, []);\nuseEffect(() => {\n if (showRfpViewModal) {\n return;\n }\n setDisabledSubmitBtn(\n !title ||\n !description ||\n !summary ||\n !(labels ?? []).length ||\n !submissionDeadline ||\n !consent.toc ||\n !consent.coc\n );\n const handler = setTimeout(() => {\n Storage.privateSet(draftKey, JSON.stringify(memoizedDraftData));\n }, 10000);\n return () => clearTimeout(handler);\n}, [\n memoizedDraftData,\n draftKey,\n draftRfpData,\n consent,\n isTxnCreated,\n showRfpViewModal,\n]);\nconst InputContainer = ({ heading, description, children }) => {\n return (\n <div className=\"d-flex flex-column gap-1 gap-sm-2 w-100\">\n <b className=\"h6 mb-0\">{heading}</b>\n {description && (\n <div className=\"text-muted w-100 text-sm\">{description}</div>\n )}\n {children}\n </div>\n );\n};\n// show RFP created after txn approval for popup wallet\nuseEffect(() => {\n if (isTxnCreated) {\n if (editRfpData) {\n setOldRfpData(editRfpData);\n if (\n editRfpData &&\n typeof editRfpData === \"object\" &&\n oldRfpData &&\n typeof oldRfpData === \"object\" &&\n JSON.stringify(editRfpData) !== JSON.stringify(oldRfpData)\n ) {\n setCreateTxn(false);\n setRfpId(editRfpData.id);\n setShowRfpViewModal(true);\n }\n } else {\n const rfpIds = Near.view(\"forum.potlock.near\", \"get_all_rfp_ids\");\n if (Array.isArray(rfpIds) && !rfpIdsArray) {\n setRfpIdsArray(rfpIds);\n }\n if (\n Array.isArray(rfpIds) &&\n Array.isArray(rfpIdsArray) &&\n rfpIds.length !== rfpIdsArray.length\n ) {\n setCreateTxn(false);\n setRfpId(rfpIds[rfpIds.length - 1]);\n setShowRfpViewModal(true);\n }\n }\n }\n});\nuseEffect(() => {\n if (props.transactionHashes) {\n setLoading(true);\n useCache(\n () =>\n asyncFetch(\"https://rpc.mainnet.near.org\", {\n method: \"POST\",\n headers: {\n \"content-type\": \"application/json\",\n },\n body: JSON.stringify({\n jsonrpc: \"2.0\",\n id: \"dontcare\",\n method: \"tx\",\n params: [props.transactionHashes, context.accountId],\n }),\n }).then((transaction) => {\n const transaction_method_name =\n transaction?.body?.result?.transaction?.actions[0].FunctionCall\n .method_name;\n const is_edit_or_add_rfp_transaction =\n transaction_method_name == \"add_rfp\" ||\n transaction_method_name == \"edit_rfp\";\n if (is_edit_or_add_rfp_transaction) {\n setShowRfpViewModal(true);\n Storage.privateSet(draftKey, null);\n }\n // show the latest created rfp to user\n if (transaction_method_name == \"add_rfp\") {\n useCache(\n () =>\n Near.asyncView(\"forum.potlock.near\", \"get_all_rfp_ids\").then(\n (rfpIdsArray) => {\n setRfpId(rfpIdsArray?.[rfpIdsArray?.length - 1]);\n }\n ),\n props.transactionHashes + \"rfpIds\",\n { subscribe: false }\n );\n } else {\n setRfpId(id);\n }\n setLoading(false);\n }),\n props.transactionHashes + context.accountId,\n { subscribe: false }\n );\n } else {\n if (showRfpViewModal) {\n setShowRfpViewModal(false);\n }\n }\n}, [props.transactionHashes]);\nconst LoadingButtonSpinner = (\n <span\n className=\"submit-rfp-loading-indicator spinner-border spinner-border-sm\"\n role=\"status\"\n aria-hidden=\"true\"\n ></span>\n);\nconst onSubmit = () => {\n setCreateTxn(true);\n const body = {\n rfp_body_version: \"V0\",\n name: title,\n description: description,\n summary: summary,\n submission_deadline: getTimestamp(submissionDeadline),\n timeline: timeline,\n };\n const args = { labels: (labels ?? []).map((i) => i.value), body: body };\n if (isEditPage) {\n args[\"id\"] = editRfpData.id;\n }\n Near.call([\n {\n contractName: \"forum.potlock.near\",\n methodName: isEditPage ? \"edit_rfp\" : \"add_rfp\",\n args: args,\n gas: 270000000000000,\n deposit: \"100000000000000000000000\",\n },\n ]);\n};\nfunction cleanDraft() {\n Storage.privateSet(draftKey, null);\n}\nif (loading) {\n return (\n <div\n style={{ height: \"50vh\" }}\n className=\"d-flex justify-content-center align-items-center w-100\"\n >\n <Widget src={`devhub.near/widget/devhub.components.molecule.Spinner`} />\n </div>\n );\n}\nconst [collapseState, setCollapseState] = useState({});\nconst CollapsibleContainer = ({ title, children, noPaddingTop }) => {\n return (\n <div\n className={\n \"border-bottom py-4 \" +\n (noPaddingTop && \"pt-0 \") +\n (collapseState[title] && \" pb-0\")\n }\n >\n <div className={\"d-flex justify-content-between \"}>\n <div className=\"h5 text-muted mb-2 mb-sm-3\">{title}</div>\n <div\n className=\"d-flex d-sm-none cursor-pointer\"\n onClick={() =>\n setCollapseState((prevState) => ({\n ...prevState,\n [title]: !prevState[title],\n }))\n }\n >\n {!collapseState[title] ? (\n <i className=\"bi bi-chevron-up h4\"></i>\n ) : (\n <i className=\"bi bi-chevron-down h4\"></i>\n )}\n </div>\n </div>\n <div className={!collapseState[title] ? \"\" : \"d-none\"}>{children}</div>\n </div>\n );\n};\nconst CategoryDropdown = useMemo(() => {\n return (\n <Widget\n src={`bos.forum.potlock.near/widget/components.molecule.MultiSelectCategoryDropdown`}\n props={{\n selected: labels,\n onChange: (v) => setLabels(v),\n disabled: false,\n availableOptions: rfpLabelOptions,\n }}\n />\n );\n}, [draftRfpData]);\nconst TitleComponent = useMemo(() => {\n return (\n <Widget\n src={`devhub.near/widget/devhub.components.molecule.Input`}\n props={{\n className: \"flex-grow-1\",\n value: title,\n onBlur: (e) => {\n setTitle(e.target.value);\n },\n skipPaddingGap: true,\n inputProps: {\n max: 80,\n },\n }}\n />\n );\n}, [draftRfpData]);\nconst SummaryComponent = useMemo(() => {\n return (\n <Widget\n src={`devhub.near/widget/devhub.components.molecule.Input`}\n props={{\n className: \"flex-grow-1\",\n value: summary,\n multiline: true,\n onBlur: (e) => {\n setSummary(e.target.value);\n },\n skipPaddingGap: true,\n inputProps: {\n max: 500,\n },\n }}\n />\n );\n}, [draftRfpData]);\nconst DescriptionComponent = useMemo(() => {\n return (\n <Widget\n src={`bos.forum.potlock.near/widget/components.molecule.Compose`}\n props={{\n data: description,\n onChange: setDescription,\n autocompleteEnabled: true,\n autoFocus: false,\n }}\n />\n );\n}, [draftRfpData]);\nconst ConsentComponent = useMemo(() => {\n return (\n <div className=\"d-flex flex-column gap-2\">\n <Widget\n src={`devhub.near/widget/devhub.components.molecule.Checkbox`}\n props={{\n value: \"toc\",\n label: (\n <>\n I’ve agree to{\" \"}\n <a\n href={ToCDocs}\n className=\"text-decoration-underline\"\n target=\"_blank\"\n rel=\"noopener noreferrer\"\n >\n AI PGF's Terms and Conditions\n </a>\n and commit to honoring it\n </>\n ),\n isChecked: consent.toc,\n onClick: (value) =>\n setConsent((prevConsent) => ({\n ...prevConsent,\n toc: value,\n })),\n }}\n />\n <Widget\n src={`devhub.near/widget/devhub.components.molecule.Checkbox`}\n props={{\n value: \"coc\",\n label: (\n <>\n I’ve read{\" \"}\n <a\n href={CoCDocs}\n className=\"text-decoration-underline\"\n target=\"_blank\"\n rel=\"noopener noreferrer\"\n >\n AI PGF's Code of Conduct\n </a>\n and commit to honoring it\n </>\n ),\n isChecked: consent.coc,\n onClick: (value) =>\n setConsent((prevConsent) => ({\n ...prevConsent,\n coc: value,\n })),\n }}\n />\n </div>\n );\n}, [draftRfpData]);\nconst SubmissionDeadline = useMemo(() => {\n return (\n <Widget\n src={`devhub.near/widget/devhub.components.molecule.Input`}\n props={{\n className: \"flex-grow-1\",\n value: submissionDeadline,\n onBlur: (e) => {\n setSubmissionDeadline(e.target.value);\n },\n skipPaddingGap: true,\n type: \"date\",\n inputProps: {\n required: true,\n },\n }}\n />\n );\n}, [draftRfpData]);\nreturn (\n <Container className=\"w-100 py-2 px-0 px-sm-2 d-flex flex-column gap-3\">\n <Widget\n src={`bos.forum.potlock.near/widget/components.rfps.ViewRfpModal`}\n props={{\n isOpen: showRfpViewModal,\n isEdit: isEditPage,\n rfpId: rfpId,\n }}\n />\n <Widget\n src={`bos.forum.potlock.near/widget/components.rfps.ConfirmCancelModal`}\n props={{\n isOpen: isCancelModalOpen,\n onCancelClick: () => {\n setCancelModal(false);\n setTimeline({ status: RFP_TIMELINE_STATUS.EVALUATION });\n },\n onConfirmClick: (value) => {\n setCancelModal(false);\n onCancelRFP(value);\n },\n linkedProposalIds: editRfpData.snapshot.linked_proposals,\n }}\n />\n <Widget\n src={`bos.forum.potlock.near/widget/components.rfps.WarningModal`}\n props={{\n isOpen: isWarningModalOpen,\n onConfirmClick: () => {\n setWarningModal(false);\n setTimeline({ status: RFP_TIMELINE_STATUS.EVALUATION });\n },\n }}\n />\n <Heading className=\"px-2 px-sm-0\">\n {isEditPage ? \"Edit\" : \"Create\"} RFP\n </Heading>\n <div className=\"card no-border rounded-0 px-2 p-lg-0 full-width-div\">\n <div className=\"container-xl py-4 d-flex flex-wrap gap-6 w-100\">\n <div\n style={{ minWidth: \"350px\" }}\n className=\"flex-2 w-100 order-2 order-md-1\"\n >\n <div className=\"d-flex gap-3 w-100\">\n <div className=\"d-none d-sm-flex\" style={{ height: \"max-content\" }}>\n <Widget\n src={`bos.forum.potlock.near/widget/components.molecule.Profile`}\n props={{\n accountId: author,\n }}\n />\n </div>\n <div className=\"d-flex flex-column gap-4 w-100\">\n <InputContainer\n heading=\"Category\"\n description={\n <>\n Select the relevant categories to help users quickly\n understand the nature of the need.\n </>\n }\n >\n {CategoryDropdown}\n </InputContainer>\n <InputContainer\n heading=\"Title\"\n description=\"Highlight the essence of your RFP in a few words. This will appear on your RFP’s detail page and the main RFP feed. Keep it short, please :)\"\n >\n {TitleComponent}\n </InputContainer>\n <InputContainer\n heading=\"Summary\"\n description=\"Explain your RFP briefly. What is the problem or need, desired outcome, and benefit to the NEAR developer community.\"\n >\n {SummaryComponent}\n </InputContainer>\n <InputContainer\n heading=\"Description\"\n description={\n \"Expand on your summary with any relevant details like a detailed explanation of the problem and the expected solution, scope, and deliverables. Also include an estimate range for the project if you have a specific budget. And the selection criteria.\"\n }\n >\n {DescriptionComponent}\n </InputContainer>\n <InputContainer heading=\"Final Consent\">\n {ConsentComponent}\n </InputContainer>\n <div className=\"d-flex justify-content-end gap-2 align-items-center\">\n <Link\n to={\n isEditPage\n ? href({\n widgetSrc: `bos.forum.potlock.near/widget/app`,\n params: {\n page: \"rfp\",\n id: parseInt(id),\n },\n })\n : href({\n widgetSrc: `bos.forum.potlock.near/widget/app`,\n params: {\n page: \"rfps\",\n },\n })\n }\n >\n <Widget\n src={`devhub.near/widget/devhub.components.molecule.Button`}\n props={{\n classNames: {\n root: \"d-flex h-100 text-muted fw-bold btn-outline shadow-none border-0 btn-sm\",\n },\n label: \"Discard Changes\",\n onClick: cleanDraft,\n }}\n />\n </Link>\n <Widget\n src={`devhub.near/widget/devhub.components.molecule.Button`}\n props={{\n classNames: {\n root: \"d-flex h-100 fw-light-bold btn-outline shadow-none border-1\",\n },\n label: (\n <div className=\"d-flex align-items-center gap-2\">\n <div className=\"circle grey\"></div> <div>Submit</div>\n </div>\n ),\n onClick: onSubmit,\n disabled: disabledSubmitBtn,\n }}\n />\n </div>\n </div>\n </div>\n </div>\n <div\n style={{ minWidth: \"350px\" }}\n className=\"flex-1 w-100 order-1 order-md-2\"\n >\n <CollapsibleContainer noPaddingTop={true}>\n <div className=\"d-flex flex-column gap-3 gap-sm-4\">\n <InputContainer\n heading=\"Submission Deadline\"\n description=\"Enter the deadline for submitting proposals.\"\n >\n {SubmissionDeadline}\n </InputContainer>\n </div>\n </CollapsibleContainer>\n <div className=\"my-2\">\n <CollapsibleContainer title=\"Timeline\">\n <Widget\n src={`bos.forum.potlock.near/widget/components.rfps.TimelineConfigurator`}\n props={{\n timeline: timeline,\n setTimeline: (v) => {\n if (editRfpData.snapshot.timeline.status === v.status) {\n return;\n }\n // if proposal selected timeline is selected and no approved proposals exist, show warning\n if (\n v.status === RFP_TIMELINE_STATUS.PROPOSAL_SELECTED &&\n Array.isArray(approvedProposals) &&\n !approvedProposals.length\n ) {\n setWarningModal(true);\n }\n if (v.status === RFP_TIMELINE_STATUS.CANCELLED) {\n setCancelModal(true);\n }\n setTimeline(v);\n },\n disabled: isEditPage ? false : true,\n }}\n />\n </CollapsibleContainer>\n </div>\n </div>\n </div>\n </div>\n </Container>\n);\n" }, "components.rfps.Rfp": { "": "const {\n RFP_TIMELINE_STATUS,\n fetchGraphQL,\n CANCEL_RFP_OPTIONS,\n parseJSON,\n PROPOSALS_APPROVED_STATUS_ARRAY,\n getLinkUsingCurrentGateway,\n} = VM.require(`bos.forum.potlock.near/widget/core.common`) || {\n RFP_TIMELINE_STATUS: {},\n fetchGraphQL: () => {},\n CANCEL_RFP_OPTIONS: {},\n parseJSON: () => {},\n PROPOSALS_APPROVED_STATUS_ARRAY: {},\n getLinkUsingCurrentGateway: () => {},\n};\nconst { href } = VM.require(`devhub.near/widget/core.lib.url`) || {\n href: () => {},\n};\nconst { readableDate } = VM.require(`devhub.near/widget/core.lib.common`) || {\n readableDate: () => {},\n};\nconst { getGlobalLabels } = VM.require(\n `bos.forum.potlock.near/widget/components.core.lib.contract`\n) || { getGlobalLabels: () => {} };\nconst accountId = context.accountId;\n/*\n ---props---\n props.id: number;\n props.timestamp: number; optional\n accountId: string\n blockHeight:number\n */\nconst { id, timestamp } = props;\nconst Container = styled.div`\n .full-width-div {\n width: 100vw;\n position: relative;\n left: 50%;\n right: 50%;\n margin-left: -50vw;\n margin-right: -50vw;\n }\n .fw-bold {\n font-weight: 600 !important;\n }\n .card.no-border {\n border-left: none !important;\n border-right: none !important;\n margin-bottom: -3.5rem;\n }\n .description-box {\n font-size: 14px;\n }\n .accept-submission-info-container {\n background-color: #ecf8fb;\n }\n .text-sm {\n font-size: 13px !important;\n }\n .flex-1 {\n flex: 1;\n }\n .flex-3 {\n flex: 3;\n }\n .circle {\n width: 20px;\n height: 20px;\n border-radius: 50%;\n border: 1px solid grey;\n }\n .green-fill {\n background-color: rgb(4, 164, 110) !important;\n border-color: rgb(4, 164, 110) !important;\n color: white !important;\n }\n .yellow-fill {\n border-color: #ff7a00 !important;\n }\n .vertical-line {\n width: 2px;\n height: 180px;\n background-color: lightgrey;\n }\n @media screen and (max-width: 970px) {\n .vertical-line {\n height: 135px !important;\n }\n .vertical-line-sm {\n height: 70px !important;\n }\n .gap-6 {\n gap: 0.5rem !important;\n }\n }\n @media screen and (max-width: 570px) {\n .vertical-line {\n height: 180px !important;\n }\n .vertical-line-sm {\n height: 75px !important;\n }\n .gap-6 {\n gap: 0.5rem !important;\n }\n }\n .vertical-line-sm {\n width: 2px;\n height: 70px;\n background-color: lightgrey;\n }\n .form-check-input:disabled ~ .form-check-label,\n .form-check-input[disabled] ~ .form-check-label {\n opacity: 1;\n }\n .form-check-input {\n border-color: black !important;\n }\n .grey-btn {\n background-color: #687076;\n border: none;\n color: white;\n }\n .blue-btn {\n background-color: #3c697d;\n border: none;\n color: white;\n }\n .form-check-input:checked {\n background-color: #3c697d !important;\n border-color: #3c697d !important;\n }\n .dropdown-toggle:after {\n position: absolute;\n top: 46%;\n right: 5%;\n }\n .drop-btn {\n max-width: none !important;\n }\n .dropdown-menu {\n width: 100%;\n border-radius: 0.375rem !important;\n }\n .green-btn {\n background-color: #04a46e !important;\n border: none;\n color: white;\n &:active {\n color: white;\n }\n }\n .gap-6 {\n gap: 2.5rem;\n }\n .border-vertical {\n border-top: var(--bs-border-width) var(--bs-border-style)\n var(--bs-border-color) !important;\n border-bottom: var(--bs-border-width) var(--bs-border-style)\n var(--bs-border-color) !important;\n }\n button.px-0 {\n padding-inline: 0px !important;\n }\n red-icon i {\n color: red;\n }\n input[type=\"radio\"] {\n min-width: 13px;\n }\n`;\nconst RfpContainer = styled.div`\n border: 1px solid lightgrey;\n overflow: auto;\n`;\nconst Header = styled.div`\n position: relative;\n background-color: #f4f4f4;\n height: 50px;\n .menu {\n position: absolute;\n right: 10px;\n top: 4px;\n font-size: 30px;\n }\n`;\nconst Text = styled.p`\n display: block;\n margin: 0;\n font-size: 14px;\n line-height: 20px;\n font-weight: 400;\n color: #687076;\n white-space: nowrap;\n`;\nconst Actions = styled.div`\n display: flex;\n align-items: center;\n gap: 12px;\n margin: -6px -6px 6px;\n`;\nconst Avatar = styled.div`\n width: 40px;\n height: 40px;\n pointer-events: none;\n img {\n object-fit: cover;\n border-radius: 40px;\n width: 100%;\n height: 100%;\n }\n`;\nconst rfpLabelOptions = getGlobalLabels();\nconst LinkProfile = ({ account, children }) => {\n return (\n <a\n rel=\"noopener noreferrer\"\n target=\"_blank\"\n href={`https://bos.potlock.org/?tab=profile&accountId=${account}`}\n >\n {children}\n </a>\n );\n};\nconst [snapshotHistory, setSnapshotHistory] = useState([]);\nconst rfp = Near.view(\"forum.potlock.near\", \"get_rfp\", {\n rfp_id: parseInt(id),\n});\nconst queryName = \"bos_forum_potlock_near_ai_pgf_indexer_rfp_snapshots\";\nconst query = `query GetLatestSnapshot($offset: Int = 0, $limit: Int = 10, $where: ${queryName}_bool_exp = {}) {\n ${queryName}(\n offset: $offset\n limit: $limit\n order_by: {ts: asc}\n where: $where\n ) {\n editor_id\n name\n summary\n description\n ts\n rfp_id\n timeline\n labels\n submission_deadline\n linked_proposals\n }\n}`;\nconst fetchSnapshotHistory = () => {\n const variables = {\n where: { rfp_id: { _eq: id } },\n };\n fetchGraphQL(query, \"GetLatestSnapshot\", variables).then(async (result) => {\n if (result.status === 200) {\n if (result.body.data) {\n const data = result.body.data?.[queryName];\n const history = data.map((item) => {\n const rfpData = {\n ...item,\n timestamp: item.ts,\n timeline: parseJSON(item.timeline),\n };\n delete rfpData.ts;\n return rfpData;\n });\n setSnapshotHistory(history);\n }\n }\n });\n};\nuseEffect(() => {\n fetchSnapshotHistory();\n}, [id]);\nif (!rfp) {\n return (\n <div\n style={{ height: \"50vh\" }}\n className=\"d-flex justify-content-center align-items-center w-100\"\n >\n <Widget src={`devhub.near/widget/devhub.components.molecule.Spinner`} />\n </div>\n );\n}\nif (timestamp && rfp) {\n rfp.snapshot =\n snapshotHistory.find((item) => item.timestamp === timestamp) ??\n rfp.snapshot;\n}\nconst { snapshot } = rfp;\nsnapshot.timeline = parseJSON(snapshot.timeline);\nconst authorId = rfp.author_id;\nconst blockHeight = parseInt(rfp.social_db_post_block_height);\nconst item = {\n type: \"social\",\n path: `forum.potlock.near/post/main`,\n blockHeight,\n};\nconst rfpURL = getLinkUsingCurrentGateway(\n `bos.forum.potlock.near/widget/app?page=rfp&id=${rfp.id}&timestamp=${snapshot.timestamp}`\n);\nconst SidePanelItem = ({ title, children, hideBorder, ishidden }) => {\n return (\n <div\n style={{ gap: \"8px\" }}\n className={\n ishidden\n ? \"d-none\"\n : \"d-flex flex-column pb-3 \" + (!hideBorder && \" border-bottom\")\n }\n >\n <div className=\"h6 mb-0\">{title} </div>\n <div className=\"text-muted\">{children}</div>\n </div>\n );\n};\nconst isAllowedToWriteRfp = Near.view(\n \"forum.potlock.near\",\n \"is_allowed_to_write_rfps\",\n {\n editor: accountId,\n }\n);\nconst link = href({\n widgetSrc: `bos.forum.potlock.near/widget/app`,\n params: {\n page: \"create-rfp\",\n id: rfp.id,\n timestamp: timestamp,\n },\n});\nconst createdDate = snapshotHistory[0].timestamp ?? snapshot.timestamp;\nconst [approvedProposals, setApprovedProposals] = useState([]);\nconst [isCancelModalOpen, setCancelModal] = useState(false);\nconst [isWarningModalOpen, setWarningModal] = useState(false);\nconst [timeline, setTimeline] = useState(null);\nconst [showTimelineSetting, setShowTimelineSetting] = useState(false);\nuseEffect(() => {\n if (!timeline) {\n setTimeline(snapshot.timeline);\n }\n}, [snapshot]);\nfunction fetchApprovedRfpProposals() {\n const queryName =\n \"bos_forum_potlock_near_ai_pgf_indexer_proposals_with_latest_snapshot\";\n const query = `query GetLatestSnapshot($offset: Int = 0, $limit: Int = 10, $where: ${queryName}_bool_exp = {}) {\n ${queryName}(\n offset: $offset\n limit: $limit\n order_by: {proposal_id: desc}\n where: $where\n ) {\n proposal_id\n name\n timeline\n }\n }`;\n const FETCH_LIMIT = 50;\n const variables = {\n limit: FETCH_LIMIT,\n offset,\n where: {\n proposal_id: { _in: rfp.snapshot.linked_proposals },\n },\n };\n fetchGraphQL(query, \"GetLatestSnapshot\", variables).then(async (result) => {\n if (result.status === 200) {\n if (result.body.data) {\n const data = result.body.data?.[queryName];\n const approved = [];\n data.map((item) => {\n const timeline = parseJSON(item.timeline);\n if (PROPOSALS_APPROVED_STATUS_ARRAY.includes(timeline.status)) {\n approved.push(item);\n }\n });\n setApprovedProposals(approved);\n }\n }\n });\n}\nconst editRFPStatus = () => {\n Near.call([\n {\n contractName: \"forum.potlock.near\",\n methodName: \"edit_rfp_timeline\",\n args: {\n id: rfp.id,\n timeline: timeline,\n },\n gas: 270000000000000,\n },\n ]);\n};\nconst onCancelRFP = (value) => {\n Near.call([\n {\n contractName: \"forum.potlock.near\",\n methodName: \"cancel_rfp\",\n args: {\n id: rfp.id,\n proposals_to_cancel:\n value === CANCEL_RFP_OPTIONS.CANCEL_PROPOSALS\n ? snapshot.linked_proposals\n : [],\n proposals_to_unlink:\n value === CANCEL_RFP_OPTIONS.UNLINK_PROPOSALS\n ? snapshot.linked_proposals\n : [],\n },\n gas: 270000000000000,\n },\n ]);\n};\nconst accessControlInfo =\n Near.view(\"forum.potlock.near\", \"get_access_control_info\") ?? null;\nconst moderatorList =\n accessControlInfo?.members_list?.[\"team:moderators\"]?.children;\nfetchApprovedRfpProposals();\nconst SubmitProposalBtn = () => {\n return (\n <div style={{ minWidth: \"fit-content\" }}>\n <Link\n to={href({\n widgetSrc: `bos.forum.potlock.near/widget/app`,\n params: { page: \"create-proposal\", rfp_id: rfp.id },\n })}\n >\n <Widget\n src={`devhub.near/widget/devhub.components.molecule.Button`}\n props={{\n label: (\n <div className=\"d-flex align-items-center gap-2\">\n <i className=\"bi bi-plus-circle\"></i>Submit Proposal\n </div>\n ),\n classNames: { root: \"blue-btn\" },\n }}\n />\n </Link>\n </div>\n );\n};\nreturn (\n <Container className=\"d-flex flex-column gap-2 w-100 mt-4\">\n <Widget\n src={`bos.forum.potlock.near/widget/components.rfps.ConfirmCancelModal`}\n props={{\n isOpen: isCancelModalOpen,\n onCancelClick: () => {\n setCancelModal(false);\n setTimeline({ status: RFP_TIMELINE_STATUS.EVALUATION });\n },\n onConfirmClick: (value) => {\n setCancelModal(false);\n onCancelRFP(value);\n },\n linkedProposalIds: snapshot.linked_proposals,\n }}\n />\n <Widget\n src={`bos.forum.potlock.near/widget/components.rfps.WarningModal`}\n props={{\n isOpen: isWarningModalOpen,\n onConfirmClick: () => {\n setWarningModal(false);\n setTimeline({ status: RFP_TIMELINE_STATUS.EVALUATION });\n },\n }}\n />\n <div className=\"d-flex px-3 px-lg-0 justify-content-between\">\n <div className=\"d-flex gap-2 align-items-center h3\">\n <div>{snapshot.name}</div>\n <div className=\"text-muted\">#{rfp.id}</div>\n </div>\n <div className=\"d-flex gap-2 align-items-center\">\n <Widget\n src={`near/widget/ShareButton`}\n props={{\n postType: \"post\",\n url: rfpURL,\n }}\n />\n {isAllowedToWriteRfp && (\n <Link to={link} style={{ textDecoration: \"none\" }}>\n <Widget\n src={`devhub.near/widget/devhub.components.molecule.Button`}\n props={{\n label: \"Edit\",\n classNames: { root: \"grey-btn btn-sm\" },\n }}\n />\n </Link>\n )}\n </div>\n </div>\n <div className=\"d-flex flex-wrap flex-md-nowrap px-3 px-lg-0 gap-2 align-items-center text-sm pb-3 w-100\">\n <Widget\n src={`bos.forum.potlock.near/widget/components.rfps.StatusTag`}\n props={{\n timelineStatus: snapshot.timeline.status,\n size: \"sm\",\n }}\n />\n <div className=\"w-100 d-flex flex-wrap flex-md-nowrap gap-1 align-items-center\">\n <div className=\"fw-bold text-truncate\">\n <LinkProfile account={authorId}>{authorId}</LinkProfile>\n </div>\n <div>created on {readableDate(createdDate / 1000000)}</div>\n </div>\n </div>\n <div className=\"card no-border rounded-0 full-width-div px-3 px-lg-0\">\n <div className=\"container-xl py-4\">\n {snapshot.timeline.status ===\n RFP_TIMELINE_STATUS.ACCEPTING_SUBMISSIONS && (\n <div className=\"accept-submission-info-container p-3 p-sm-4 d-flex flex-wrap flex-sm-nowrap justify-content-between align-items-center gap-2 rounded-2\">\n <div style={{ minWidth: \"300px\" }}>\n <b>This RFP is accepting submissions.</b>\n <p className=\"text-sm text-muted mt-2\">\n Click Submit Proposal if you want to submit a proposal.\n </p>\n </div>\n <SubmitProposalBtn />\n </div>\n )}\n <div className=\"my-4\">\n <div className=\"d-flex flex-wrap gap-6\">\n <div\n style={{ minWidth: \"350px\" }}\n className=\"flex-3 order-2 order-md-1\"\n >\n <div\n className=\"d-flex gap-2 flex-1\"\n style={{\n zIndex: 99,\n background: \"white\",\n position: \"relative\",\n }}\n >\n <div\n className=\"d-none d-sm-flex\"\n style={{ height: \"max-content\" }}\n >\n <Widget\n src={`bos.forum.potlock.near/widget/components.molecule.Profile`}\n props={{\n accountId: authorId,\n }}\n />\n </div>\n <RfpContainer className=\"rounded-2 flex-1\">\n <Header className=\"d-flex gap-1 align-items-center p-2 px-3 \">\n <div\n className=\"fw-bold text-truncate\"\n style={{ maxWidth: \"60%\" }}\n >\n <LinkProfile account={authorId}>{authorId}</LinkProfile>\n </div>\n <div\n className=\"text-muted\"\n style={{ minWidth: \"fit-content\" }}\n >\n ・{\" \"}\n <Widget\n src={`near/widget/TimeAgo`}\n props={{\n blockHeight,\n blockTimestamp: createdDate,\n }}\n />\n {context.accountId && (\n <div className=\"menu\">\n <Widget\n src={`near/widget/Posts.Menu`}\n props={{\n accountId: authorId,\n blockHeight: blockHeight,\n }}\n />\n </div>\n )}\n </div>\n </Header>\n <div className=\"d-flex flex-column gap-1 p-2 px-3 description-box\">\n <div className=\"text-muted h6 border-bottom pb-1 mt-3\">\n RFP CATEGORY\n <div className=\"my-2\">\n <Widget\n src={`bos.forum.potlock.near/widget/components.molecule.MultiSelectCategoryDropdown`}\n props={{\n selected: snapshot.labels,\n disabled: true,\n hideDropdown: true,\n onChange: () => {},\n availableOptions: rfpLabelOptions,\n }}\n />\n </div>\n </div>\n <div className=\"text-muted h6 border-bottom pb-1 mt-3\">\n SUMMARY\n </div>\n <div>{snapshot.summary}</div>\n <div className=\"text-muted h6 border-bottom pb-1 mt-3 mb-4\">\n DESCRIPTION\n </div>\n <Widget\n src={`devhub.near/widget/devhub.components.molecule.MarkdownViewer`}\n props={{ text: snapshot.description }}\n />\n <div className=\"d-flex gap-2 align-items-center mt-4\">\n <Widget\n src={`bos.forum.potlock.near/widget/components.molecule.LikeButton`}\n props={{\n item,\n rfpId: rfp.id,\n notifyAccountIds: moderatorList,\n }}\n />\n <Widget\n src={`devhub.near/widget/devhub.entity.proposal.CommentIcon`}\n props={{\n item,\n showOverlay: false,\n onClick: () => {},\n }}\n />\n <Widget\n src={`near/widget/CopyUrlButton`}\n props={{\n url: rfpURL,\n }}\n />\n </div>\n </div>\n </RfpContainer>\n </div>\n <div className=\"border-bottom pb-4 mt-4\">\n <Widget\n src={`bos.forum.potlock.near/widget/components.rfps.CommentsAndLogs`}\n props={{\n ...props,\n id: rfp.id,\n item: item,\n approvedProposals: approvedProposals,\n snapshotHistory: snapshotHistory,\n }}\n />\n </div>\n <div\n style={{\n position: \"relative\",\n zIndex: 99,\n backgroundColor: \"white\",\n }}\n className=\"pt-4\"\n >\n <Widget\n src={`bos.forum.potlock.near/widget/components.molecule.ComposeComment`}\n props={{\n ...props,\n item: item,\n notifyAccountIds: moderatorList,\n rfpId: rfp.id,\n }}\n />\n {snapshot.timeline.status ===\n RFP_TIMELINE_STATUS.ACCEPTING_SUBMISSIONS && (\n <div className=\"accept-submission-info-container mt-3 p-3 p-sm-4 d-flex flex-wrap flex-md-nowrap justify-content-between align-items-center gap-2 rounded-2\">\n <div style={{ minWidth: \"350px\" }}>\n <b>Want to respond to this RFP? </b> This RFP is accepting\n submissions.\n </div>\n <SubmitProposalBtn />\n </div>\n )}\n </div>\n </div>\n <div\n style={{ minWidth: \"300px\" }}\n className=\"d-flex flex-column gap-4 flex-1 order-1 order-md-2\"\n >\n <SidePanelItem title=\"Submission Deadline\">\n <h5 className=\"text-black\">\n {readableDate(\n parseFloat(snapshot.submission_deadline / 1000000)\n )}\n </h5>\n </SidePanelItem>\n <SidePanelItem\n title={\n <div>\n <div className=\"d-flex justify-content-between align-content-center\">\n Timeline\n {isAllowedToWriteRfp && (\n <div\n data-testid=\"setting-btn\"\n onClick={() => setShowTimelineSetting(true)}\n >\n <i class=\"bi bi-gear\"></i>\n </div>\n )}\n </div>\n </div>\n }\n >\n <Widget\n src={`bos.forum.potlock.near/widget/components.rfps.TimelineConfigurator`}\n props={{\n timeline: timeline,\n setTimeline: (v) => {\n if (\n snapshot.timeline.status === v.status &&\n timeline.status === v.status\n ) {\n return;\n }\n // if proposal selected timeline is selected and no approved proposals exist, show warning\n if (\n v.status === RFP_TIMELINE_STATUS.PROPOSAL_SELECTED &&\n Array.isArray(approvedProposals) &&\n !approvedProposals.length\n ) {\n setWarningModal(true);\n }\n if (v.status === RFP_TIMELINE_STATUS.CANCELLED) {\n setCancelModal(true);\n }\n setTimeline(v);\n },\n disabled: showTimelineSetting ? false : true,\n }}\n />\n {showTimelineSetting && (\n <div className=\"d-flex gap-2 align-items-center justify-content-end text-sm mt-2\">\n <Widget\n src={`devhub.near/widget/devhub.components.molecule.Button`}\n props={{\n label: \"Cancel\",\n classNames: {\n root: \"btn-outline-danger border-0 shadow-none btn-sm\",\n },\n onClick: () => {\n setShowTimelineSetting(false);\n setTimeline(snapshot.timeline);\n },\n }}\n />\n <Widget\n src={`devhub.near/widget/devhub.components.molecule.Button`}\n props={{\n label: \"Save\",\n classNames: { root: \"blue-btn btn-sm\" },\n onClick: () => {\n editRFPStatus();\n setShowTimelineSetting(false);\n },\n }}\n />\n </div>\n )}\n </SidePanelItem>\n <SidePanelItem\n title={\n \"Selected Proposal\" + \" (\" + approvedProposals.length + \")\"\n }\n ishidden={!approvedProposals.length}\n >\n <Widget\n src={`bos.forum.potlock.near/widget/components.molecule.LinkedProposals`}\n props={{\n linkedProposalIds: (approvedProposals ?? []).map(\n (i) => i.proposal_id\n ),\n showStatus: false,\n }}\n />\n </SidePanelItem>\n <SidePanelItem\n title={\n \"All Proposals\" +\n \" (\" +\n snapshot.linked_proposals.length +\n \")\"\n }\n ishidden={!snapshot.linked_proposals.length}\n >\n <Widget\n src={`bos.forum.potlock.near/widget/components.molecule.LinkedProposals`}\n props={{\n linkedProposalIds: snapshot.linked_proposals,\n showStatus:\n snapshot.timeline.status !==\n RFP_TIMELINE_STATUS.PROPOSAL_SELECTED,\n }}\n />\n </SidePanelItem>\n </div>\n </div>\n </div>\n </div>\n </div>\n </Container>\n);\n" } } } } }

Transaction Execution Plan

Convert Transaction To Receipt
Gas Burned:
695 Ggas
Tokens Burned:
0.00007 
Receipt:
Predecessor ID:
Receiver ID:
Gas Burned:
38 Tgas
Tokens Burned:
0.00386 
Called method: 'set' in contract: social.near
Arguments:
{ "data": { "bos.forum.potlock.near": { "widget": { "components.proposals.CommentsAndLogs": { "": "const { PROPOSAL_TIMELINE_STATUS, isNumber, getLinkUsingCurrentGateway } =\n VM.require(`bos.forum.potlock.near/widget/core.common`) || {\n PROPOSAL_TIMELINE_STATUS: {},\n isNumber: () => {},\n getLinkUsingCurrentGateway: () => {},\n };\nconst { href } = VM.require(`devhub.near/widget/core.lib.url`);\nhref || (href = () => {});\nconst snapshotHistory = props.snapshotHistory;\nconst latestSnapshot = props.latestSnapshot;\nconst Wrapper = styled.div`\n position: relative;\n .log-line {\n position: absolute;\n left: 7%;\n top: -30px;\n bottom: 0;\n z-index: 1;\n width: 1px;\n background-color: var(--bs-border-color);\n z-index: 1;\n }\n .text-wrap {\n overflow: hidden;\n white-space: normal;\n }\n .fw-bold {\n font-weight: 600 !important;\n }\n .inline-flex {\n display: -webkit-inline-box !important;\n align-items: center !important;\n gap: 0.25rem !important;\n margin-right: 2px;\n flex-wrap: wrap;\n }\n`;\nconst CommentContainer = styled.div`\n border: 1px solid lightgrey;\n overflow: auto;\n`;\nconst Header = styled.div`\n position: relative;\n background-color: #f4f4f4;\n height: 50px;\n .menu {\n position: absolute;\n right: 10px;\n top: 4px;\n font-size: 30px;\n }\n`;\n// check snapshot history all keys and values for differences\nfunction getDifferentKeysWithValues(obj1, obj2) {\n return Object.keys(obj1)\n .filter((key) => {\n if (key !== \"editor_id\" && obj2.hasOwnProperty(key)) {\n const value1 = obj1[key];\n const value2 = obj2[key];\n if (Array.isArray(value1) && Array.isArray(value2)) {\n const sortedValue1 = [...value1].sort();\n const sortedValue2 = [...value2].sort();\n return JSON.stringify(sortedValue1) !== JSON.stringify(sortedValue2);\n } else if (typeof value1 === \"object\" && typeof value2 === \"object\") {\n return JSON.stringify(value1) !== JSON.stringify(value2);\n } else {\n return value1 !== value2;\n }\n }\n return false;\n })\n .map((key) => ({\n key,\n originalValue: obj1[key],\n modifiedValue: obj2[key],\n }));\n}\nState.init({\n data: null,\n socialComments: null,\n changedKeysListWithValues: null,\n});\nfunction sortTimelineAndComments() {\n const comments = Social.index(\"comment\", props.item, { subscribe: true });\n if (state.changedKeysListWithValues === null) {\n const changedKeysListWithValues = snapshotHistory\n .slice(1)\n .map((item, index) => {\n const startingPoint = snapshotHistory[index]; // Set comparison to the previous item\n return {\n editorId: item.editor_id,\n ...getDifferentKeysWithValues(startingPoint, item),\n };\n });\n State.update({ changedKeysListWithValues });\n }\n // sort comments and timeline logs by time\n const snapShotTimeStamp = Array.isArray(snapshotHistory)\n ? snapshotHistory.map((i) => {\n return { blockHeight: null, timestamp: parseFloat(i.timestamp / 1e6) };\n })\n : [];\n const commentsTimeStampPromise = Array.isArray(comments)\n ? Promise.all(\n comments.map((item) => {\n return asyncFetch(\n `https://api.near.social/time?blockHeight=${item.blockHeight}`\n ).then((res) => {\n const timeMs = parseFloat(res.body);\n return {\n blockHeight: item.blockHeight,\n timestamp: timeMs,\n };\n });\n })\n ).then((res) => res)\n : Promise.resolve([]);\n commentsTimeStampPromise.then((commentsTimeStamp) => {\n const combinedArray = [...snapShotTimeStamp, ...commentsTimeStamp];\n combinedArray.sort((a, b) => a.timestamp - b.timestamp);\n State.update({ data: combinedArray, socialComments: comments });\n });\n}\nif ((snapshotHistory ?? []).length > 0) {\n sortTimelineAndComments();\n}\nconst Comment = ({ commentItem }) => {\n const { accountId, blockHeight } = commentItem;\n const item = {\n type: \"social\",\n path: `${accountId}/post/comment`,\n blockHeight,\n };\n const content = JSON.parse(Social.get(item.path, blockHeight) ?? \"null\");\n const link = getLinkUsingCurrentGateway(\n `bos.forum.potlock.near/widget/app?page=proposal&id=${props.id}&accountId=${accountId}&blockHeight=${blockHeight}`\n );\n const hightlightComment =\n parseInt(props.blockHeight ?? \"\") === blockHeight &&\n props.accountId === accountId;\n return (\n <div style={{ zIndex: 99, background: \"white\" }}>\n <div className=\"d-flex gap-2 flex-1\">\n <div className=\"d-none d-sm-flex\">\n <Widget\n src={`bos.forum.potlock.near/widget/components.molecule.Profile`}\n props={{\n accountId: accountId,\n }}\n />\n </div>\n <CommentContainer\n style={{ border: hightlightComment ? \"2px solid black\" : \"\" }}\n className=\"rounded-2 flex-1\"\n >\n <Header className=\"d-flex gap-3 align-items-center p-2 px-3\">\n <div className=\"text-muted\">\n <a\n rel=\"noopener noreferrer\"\n target=\"_blank\"\n href={`https://bos.potlock.org/?tab=profile&accountId=${accountId}`}\n >\n <span className=\"fw-bold text-black\">{accountId}</span>\n </a>\n commented ・{\" \"}\n <Widget\n src={`near/widget/TimeAgo`}\n props={{\n blockHeight: blockHeight,\n }}\n />\n </div>\n {context.accountId && (\n <div className=\"menu\">\n <Widget\n src={`near/widget/Posts.Menu`}\n props={{\n accountId: accountId,\n blockHeight: blockHeight,\n contentPath: `/post/comment`,\n contentType: \"comment\",\n }}\n />\n </div>\n )}\n </Header>\n <div className=\"p-2 px-3\">\n <Widget\n src={`devhub.near/widget/devhub.components.molecule.MarkdownViewer`}\n props={{\n text: content.text,\n }}\n />\n <div className=\"d-flex gap-2 align-items-center mt-4\">\n <Widget\n src={`devhub.near/widget/devhub.entity.proposal.LikeButton`}\n props={{\n item: item,\n notifyAccountId: accountId,\n }}\n />\n <Widget\n src={`near/widget/CopyUrlButton`}\n props={{\n url: link,\n }}\n />\n </div>\n </div>\n </CommentContainer>\n </div>\n </div>\n );\n};\nfunction capitalizeFirstLetter(string) {\n const updated = string.replace(\"_\", \" \");\n return updated.charAt(0).toUpperCase() + updated.slice(1).toLowerCase();\n}\nfunction parseTimelineKeyAndValue(timeline, originalValue, modifiedValue) {\n const oldValue = originalValue[timeline];\n const newValue = modifiedValue[timeline];\n switch (timeline) {\n case \"status\": {\n if (\n (newValue === PROPOSAL_TIMELINE_STATUS.APPROVED ||\n newValue === PROPOSAL_TIMELINE_STATUS.APPROVED_CONDITIONALLY) &&\n latestSnapshot.linked_rfp\n ) {\n return (\n <span className=\"inline-flex\">\n moved proposal to{\" \"}\n <Widget\n src={`devhub.near/widget/devhub.entity.proposal.StatusTag`}\n props={{\n timelineStatus: newValue,\n }}\n />\n ・ this proposal is selected for RFP{\" \"}\n <LinkToRfp id={latestSnapshot.linked_rfp}>\n #{latestSnapshot.linked_rfp}\n </LinkToRfp>\n </span>\n );\n } else\n return (\n oldValue !== newValue && (\n <span className=\"inline-flex\">\n moved proposal from{\" \"}\n <Widget\n src={`devhub.near/widget/devhub.entity.proposal.StatusTag`}\n props={{\n timelineStatus: oldValue,\n }}\n />\n to{\" \"}\n <Widget\n src={`devhub.near/widget/devhub.entity.proposal.StatusTag`}\n props={{\n timelineStatus: newValue,\n }}\n />\n stage\n </span>\n )\n );\n }\n case \"sponsor_requested_review\":\n return !oldValue && newValue && <span>completed review</span>;\n case \"reviewer_completed_attestation\":\n return !oldValue && newValue && <span>completed attestation</span>;\n case \"kyc_verified\":\n return !oldValue && newValue && <span>verified KYC/KYB</span>;\n case \"test_transaction_sent\":\n return (\n !oldValue &&\n newValue && (\n <span>\n confirmed sponsorship and shared funding steps with recipient\n </span>\n )\n );\n case \"payouts\":\n return <span>updated the funding payment links.</span>;\n default:\n return null;\n }\n}\nconst AccountProfile = ({ accountId }) => {\n return (\n <span className=\"inline-flex fw-bold text-black\">\n <Widget\n src={`bos.forum.potlock.near/widget/components.molecule.Profile`}\n props={{\n accountId: accountId,\n size: \"sm\",\n showAccountId: true,\n }}\n />\n </span>\n );\n};\nconst LinkToRfp = ({ id, children }) => {\n return (\n <a\n className=\"text-decoration-underline flex-1\"\n href={href({\n widgetSrc: `bos.forum.potlock.near/widget/app`,\n params: {\n page: \"rfp\",\n id: id,\n },\n })}\n target=\"_blank\"\n rel=\"noopener noreferrer\"\n >\n {children}\n </a>\n );\n};\nconst parseProposalKeyAndValue = (key, modifiedValue, originalValue) => {\n switch (key) {\n case \"name\":\n return <span>changed title</span>;\n case \"summary\":\n case \"description\":\n return <span>changed {key}</span>;\n case \"labels\":\n return <span>changed labels to {(modifiedValue ?? []).join(\", \")}</span>;\n case \"category\":\n return (\n <span>\n changed category from {originalValue} to {modifiedValue}\n </span>\n );\n case \"linked_proposals\":\n return <span>updated linked proposals</span>;\n case \"linked_rfp\": {\n const isUnlinked = isNumber(originalValue) && !isNumber(modifiedValue);\n const actionText = isUnlinked ? \"unlinked\" : \"linked\";\n const rfpId = originalValue ?? modifiedValue;\n return (\n <span>\n {actionText} an RFP <LinkToRfp id={rfpId}>#{rfpId}</LinkToRfp>\n </span>\n );\n }\n case \"requested_sponsorship_usd_amount\":\n return (\n <span>\n changed sponsorship amount from {originalValue} to {modifiedValue}\n </span>\n );\n case \"requested_sponsorship_paid_in_currency\":\n return (\n <span>\n changed sponsorship currency from {originalValue} to {modifiedValue}\n </span>\n );\n case \"receiver_account\":\n return (\n <span className=\"inline-flex\">\n changed receiver account from{\" \"}\n <AccountProfile accountId={originalValue} />\n to <AccountProfile accountId={modifiedValue} />\n </span>\n );\n case \"supervisor\":\n return !originalValue && modifiedValue ? (\n <span className=\"inline-flex\">\n added\n <AccountProfile accountId={modifiedValue} />\n as project coordinator\n </span>\n ) : (\n <span className=\"inline-flex\">\n changed project coordinator from{\" \"}\n <AccountProfile accountId={originalValue} />\n to <AccountProfile accountId={modifiedValue} />\n </span>\n );\n case \"timeline\": {\n const modifiedKeys = Object.keys(modifiedValue);\n const originalKeys = Object.keys(originalValue);\n return modifiedKeys.map((i, index) => {\n const text = parseTimelineKeyAndValue(i, originalValue, modifiedValue);\n return (\n text && (\n <span key={index} className=\"inline-flex\">\n {text}\n {text && \"・\"}\n </span>\n )\n );\n });\n }\n default:\n return null;\n }\n};\nconst LogIconContainer = styled.div`\n margin-left: 50px;\n z-index: 99;\n @media screen and (max-width: 768px) {\n margin-left: 10px;\n }\n`;\nconst Log = ({ timestamp }) => {\n const updatedData = useMemo(\n () =>\n state.changedKeysListWithValues.find((obj) =>\n Object.values(obj).some(\n (value) =>\n value && parseFloat(value.modifiedValue / 1e6) === timestamp\n )\n ),\n [state.changedKeysListWithValues, timestamp]\n );\n const editorId = updatedData.editorId;\n const valuesArray = Object.values(updatedData ?? {});\n // if valuesArray length is 2 that means it only has timestamp and editorId\n if (!updatedData || valuesArray.length === 2) {\n return <></>;\n }\n return valuesArray.map((i, index) => {\n if (i.key && i.key !== \"timestamp\") {\n return (\n <LogIconContainer\n className=\"d-flex gap-3 align-items-center\"\n key={index}\n >\n <img\n src=\"https://ipfs.near.social/ipfs/bafkreiffqrxdi4xqu7erf46gdlwuodt6dm6rji2jtixs3iionjvga6rhdi\"\n height={30}\n />\n <div\n className={\n \"flex-1 gap-1 w-100 text-wrap text-muted align-items-center \" +\n (i.key === \"timeline\" &&\n Object.keys(i.originalValue ?? {}).length > 1\n ? \"\"\n : \"inline-flex\")\n }\n >\n <span className=\"inline-flex fw-bold text-black\">\n <AccountProfile accountId={editorId} showAccountId={true} />{\" \"}\n </span>\n {parseProposalKeyAndValue(i.key, i.modifiedValue, i.originalValue)}\n {i.key !== \"timeline\" && \"・\"}\n <Widget\n src={`near/widget/TimeAgo`}\n props={{\n blockTimestamp: timestamp * 1000000,\n }}\n />\n </div>\n </LogIconContainer>\n );\n }\n });\n};\nif (Array.isArray(state.data)) {\n return (\n <Wrapper>\n <div\n className=\"log-line\"\n style={{ height: state.data.length > 2 ? \"110%\" : \"150%\" }}\n ></div>\n <div className=\"d-flex flex-column gap-4\">\n {state.data.map((i, index) => {\n if (i.blockHeight) {\n const item = state.socialComments.find(\n (t) => t.blockHeight === i.blockHeight\n );\n return <Comment commentItem={item} />;\n } else {\n return <Log timestamp={i.timestamp} key={index} />;\n }\n })}\n </div>\n </Wrapper>\n );\n}\n" }, "components.proposals.Proposal": { "": "const {\n PROPOSAL_TIMELINE_STATUS,\n fetchGraphQL,\n parseJSON,\n isNumber,\n getLinkUsingCurrentGateway,\n} = VM.require(`bos.forum.potlock.near/widget/core.common`) || {\n PROPOSAL_TIMELINE_STATUS: {},\n fetchGraphQL: () => {},\n parseJSON: () => {},\n isNumber: () => {},\n getLinkUsingCurrentGateway: () => {},\n};\nconst { href } = VM.require(`devhub.near/widget/core.lib.url`);\nhref || (href = () => {});\nconst { getGlobalLabels } = VM.require(\n `bos.forum.potlock.near/widget/components.core.lib.contract`\n) || { getGlobalLabels: () => {} };\nconst { readableDate } = VM.require(`devhub.near/widget/core.lib.common`) || {\n readableDate: () => {},\n};\nconst accountId = context.accountId;\n/*\n ---props---\n props.id: number;\n props.timestamp: number; optional\n accountId: string\n blockHeight:number\n */\nconst DecisionStage = [\n PROPOSAL_TIMELINE_STATUS.APPROVED,\n PROPOSAL_TIMELINE_STATUS.REJECTED,\n PROPOSAL_TIMELINE_STATUS.APPROVED_CONDITIONALLY,\n];\nconst Container = styled.div`\n .full-width-div {\n width: 100vw;\n position: relative;\n left: 50%;\n right: 50%;\n margin-left: -50vw;\n margin-right: -50vw;\n }\n .fw-bold {\n font-weight: 600 !important;\n }\n .card.no-border {\n border-left: none !important;\n border-right: none !important;\n margin-bottom: -3.5rem;\n }\n .description-box {\n font-size: 14px;\n }\n .draft-info-container {\n background-color: #ecf8fb;\n }\n .review-info-container {\n background-color: #fef6ee;\n }\n .text-sm {\n font-size: 13px !important;\n }\n .flex-1 {\n flex: 1;\n }\n .flex-3 {\n flex: 3;\n }\n .circle {\n width: 20px;\n height: 20px;\n border-radius: 50%;\n border: 1px solid grey;\n }\n .green-fill {\n background-color: rgb(4, 164, 110) !important;\n border-color: rgb(4, 164, 110) !important;\n color: white !important;\n }\n .yellow-fill {\n border-color: #ff7a00 !important;\n }\n .vertical-line {\n width: 2px;\n height: 180px;\n background-color: lightgrey;\n }\n @media screen and (max-width: 970px) {\n .vertical-line {\n height: 135px !important;\n }\n .vertical-line-sm {\n height: 70px !important;\n }\n .gap-6 {\n gap: 0.5rem !important;\n }\n }\n @media screen and (max-width: 570px) {\n .vertical-line {\n height: 180px !important;\n }\n .vertical-line-sm {\n height: 75px !important;\n }\n .gap-6 {\n gap: 0.5rem !important;\n }\n }\n .vertical-line-sm {\n width: 2px;\n height: 70px;\n background-color: lightgrey;\n }\n .form-check-input:disabled ~ .form-check-label,\n .form-check-input[disabled] ~ .form-check-label {\n opacity: 1;\n }\n .form-check-input {\n border-color: black !important;\n }\n .grey-btn {\n background-color: #687076;\n border: none;\n color: white;\n }\n .form-check-input:checked {\n background-color: #04a46e !important;\n border-color: #04a46e !important;\n }\n .dropdown-toggle:after {\n position: absolute;\n top: 46%;\n right: 5%;\n }\n .drop-btn {\n max-width: none !important;\n }\n .dropdown-menu {\n width: 100%;\n border-radius: 0.375rem !important;\n }\n .green-btn {\n background-color: #03ba16 !important;\n border: none;\n color: white;\n &:active {\n color: white;\n }\n }\n .gap-6 {\n gap: 2.5rem;\n }\n .border-vertical {\n border-top: var(--bs-border-width) var(--bs-border-style)\n var(--bs-border-color) !important;\n border-bottom: var(--bs-border-width) var(--bs-border-style)\n var(--bs-border-color) !important;\n }\n button.px-0 {\n padding-inline: 0px !important;\n }\n red-icon i {\n color: red;\n }\n input[type=\"radio\"] {\n min-width: 13px;\n }\n`;\nconst ProposalContainer = styled.div`\n border: 1px solid lightgrey;\n overflow: auto;\n`;\nconst Header = styled.div`\n position: relative;\n background-color: #f4f4f4;\n height: 50px;\n .menu {\n position: absolute;\n right: 10px;\n top: 4px;\n font-size: 30px;\n }\n`;\nconst Text = styled.p`\n display: block;\n margin: 0;\n font-size: 14px;\n line-height: 20px;\n font-weight: 400;\n color: #687076;\n white-space: nowrap;\n`;\nconst Actions = styled.div`\n display: flex;\n align-items: center;\n gap: 12px;\n margin: -6px -6px 6px;\n`;\nconst Avatar = styled.div`\n width: 40px;\n height: 40px;\n pointer-events: none;\n img {\n object-fit: cover;\n border-radius: 40px;\n width: 100%;\n height: 100%;\n }\n`;\nconst LinkProfile = ({ account, children }) => {\n return (\n <a\n rel=\"noopener noreferrer\"\n target=\"_blank\"\n href={`https://bos.potlock.org/?tab=profile&accountId=${account}`}\n >\n {children}\n </a>\n );\n};\nconst stepsArray = [1, 2, 3, 4, 5];\nconst { id, timestamp } = props;\nconst proposal = Near.view(\"forum.potlock.near\", \"get_proposal\", {\n proposal_id: parseInt(id),\n});\nconst [snapshotHistory, setSnapshotHistory] = useState([]);\nconst queryName = \"bos_forum_potlock_near_ai_pgf_indexer_proposal_snapshots\";\nconst query = `query GetLatestSnapshot($offset: Int = 0, $limit: Int = 10, $where: ${queryName}_bool_exp = {}) {\n ${queryName}(\n offset: $offset\n limit: $limit\n order_by: {ts: asc}\n where: $where\n ) {\n editor_id\n name\n summary\n description\n ts\n proposal_id\n timeline\n labels\n linked_proposals\n linked_rfp\n requested_sponsorship_usd_amount\n requested_sponsorship_paid_in_currency\n receiver_account\n requested_sponsor\n supervisor\n }\n}`;\nconst fetchSnapshotHistory = () => {\n const variables = {\n where: { proposal_id: { _eq: id } },\n };\n fetchGraphQL(query, \"GetLatestSnapshot\", variables).then(async (result) => {\n if (result.status === 200) {\n if (result.body.data) {\n const data = result.body.data?.[queryName];\n const history = data.map((item) => {\n const proposalData = {\n ...item,\n timestamp: item.ts,\n timeline: parseJSON(item.timeline),\n };\n delete proposalData.ts;\n return proposalData;\n });\n setSnapshotHistory(history);\n }\n }\n });\n};\nuseEffect(() => {\n fetchSnapshotHistory();\n}, [id]);\nif (!proposal) {\n return (\n <div\n style={{ height: \"50vh\" }}\n className=\"d-flex justify-content-center align-items-center w-100\"\n >\n <Widget src={`devhub.near/widget/devhub.components.molecule.Spinner`} />\n </div>\n );\n}\nif (timestamp && proposal) {\n proposal.snapshot =\n snapshotHistory.find((item) => item.timestamp === timestamp) ??\n proposal.snapshot;\n}\nconst { snapshot } = proposal;\nsnapshot.timeline = parseJSON(snapshot.timeline);\nconst authorId = proposal.author_id;\nconst blockHeight = parseInt(proposal.social_db_post_block_height);\nconst item = {\n type: \"social\",\n path: `forum.potlock.near/post/main`,\n blockHeight,\n};\nconst proposalURL = getLinkUsingCurrentGateway(\n `bos.forum.potlock.near/widget/app?page=proposal&id=${proposal.id}&timestamp=${snapshot.timestamp}`\n);\nconst SidePanelItem = ({ title, children, hideBorder, ishidden }) => {\n return (\n <div\n style={{ gap: \"8px\" }}\n className={\n ishidden\n ? \"d-none\"\n : \"d-flex flex-column pb-3 \" + (!hideBorder && \" border-bottom\")\n }\n >\n <div className=\"h6 mb-0\">{title} </div>\n <div className=\"text-muted\">{children}</div>\n </div>\n );\n};\nconst rfpLabelOptions = getGlobalLabels();\nconst proposalStatusOptions = [\n {\n label: \"Draft\",\n value: { status: PROPOSAL_TIMELINE_STATUS.DRAFT },\n },\n {\n label: \"Review\",\n value: {\n status: PROPOSAL_TIMELINE_STATUS.REVIEW,\n sponsor_requested_review: false,\n reviewer_completed_attestation: false,\n },\n },\n {\n label: \"Approved\",\n value: {\n status: PROPOSAL_TIMELINE_STATUS.APPROVED,\n sponsor_requested_review: true,\n reviewer_completed_attestation: false,\n },\n },\n {\n label: \"Approved-Conditionally\",\n value: {\n status: PROPOSAL_TIMELINE_STATUS.APPROVED_CONDITIONALLY,\n sponsor_requested_review: true,\n reviewer_completed_attestation: false,\n },\n },\n {\n label: \"Rejected\",\n value: {\n status: PROPOSAL_TIMELINE_STATUS.REJECTED,\n sponsor_requested_review: true,\n reviewer_completed_attestation: false,\n },\n },\n {\n label: \"Canceled\",\n value: {\n status: PROPOSAL_TIMELINE_STATUS.CANCELED,\n sponsor_requested_review: false,\n reviewer_completed_attestation: false,\n },\n },\n {\n label: \"Payment-processing\",\n value: {\n status: PROPOSAL_TIMELINE_STATUS.PAYMENT_PROCESSING,\n kyc_verified: false,\n test_transaction_sent: false,\n request_for_trustees_created: false,\n sponsor_requested_review: true,\n reviewer_completed_attestation: false,\n },\n },\n {\n label: \"Funded\",\n value: {\n status: PROPOSAL_TIMELINE_STATUS.FUNDED,\n trustees_released_payment: true,\n kyc_verified: true,\n test_transaction_sent: true,\n request_for_trustees_created: true,\n sponsor_requested_review: true,\n reviewer_completed_attestation: false,\n },\n },\n];\nconst CheckBox = ({ value, isChecked, label, disabled, onClick }) => {\n return (\n <div className=\"d-flex gap-2 align-items-center\">\n <input\n class=\"form-check-input\"\n type=\"checkbox\"\n value={value}\n checked={isChecked}\n disabled={!isModerator || !showTimelineSetting || disabled}\n onChange={(e) => onClick(e.target.checked)}\n />\n <label style={{ width: \"90%\" }} class=\"form-check-label text-black\">\n {label}\n </label>\n </div>\n );\n};\nconst RadioButton = ({ value, isChecked, label }) => {\n return (\n <div className=\"d-flex gap-2 align-items-center\">\n <input\n class=\"form-check-input\"\n type=\"radio\"\n value={value}\n checked={isChecked}\n disabled={true}\n />\n <label class=\"form-check-label text-black\">{label}</label>\n </div>\n );\n};\nconst isAllowedToEditProposal = Near.view(\n \"forum.potlock.near\",\n \"is_allowed_to_edit_proposal\",\n {\n proposal_id: proposal.id,\n editor: accountId,\n }\n);\nconst isModerator = Near.view(\n \"forum.potlock.near\",\n \"is_allowed_to_write_rfps\",\n {\n editor: context.accountId,\n }\n);\nconst editProposal = ({ timeline }) => {\n const body = {\n proposal_body_version: \"V1\",\n name: snapshot.name,\n description: snapshot.description,\n category: snapshot.category,\n summary: snapshot.summary,\n linked_proposals: snapshot.linked_proposals,\n requested_sponsorship_usd_amount: snapshot.requested_sponsorship_usd_amount,\n requested_sponsorship_paid_in_currency:\n snapshot.requested_sponsorship_paid_in_currency,\n receiver_account: snapshot.receiver_account,\n requested_sponsor: snapshot.requested_sponsor,\n timeline: timeline,\n linked_rfp: snapshot.linked_rfp,\n supervisor: supervisor ?? snapshot.supervisor,\n };\n const args = {\n labels: typeof snapshot.linked_rfp === \"number\" ? [] : snapshot.labels,\n body: body,\n id: proposal.id,\n };\n Near.call([\n {\n contractName: \"forum.potlock.near\",\n methodName: \"edit_proposal\",\n args: args,\n gas: 270000000000000,\n },\n ]);\n};\nconst editProposalStatus = ({ timeline }) => {\n Near.call([\n {\n contractName: \"forum.potlock.near\",\n methodName: \"edit_proposal_timeline\",\n args: {\n id: proposal.id,\n timeline: timeline,\n },\n gas: 270000000000000,\n },\n ]);\n};\nconst [isReviewModalOpen, setReviewModal] = useState(false);\nconst [isCancelModalOpen, setCancelModal] = useState(false);\nconst [showTimelineSetting, setShowTimelineSetting] = useState(false);\nconst proposalStatus = useCallback(\n () =>\n proposalStatusOptions.find(\n (i) => i.value.status === snapshot.timeline.status\n ),\n [snapshot]\n);\nconst [updatedProposalStatus, setUpdatedProposalStatus] = useState({});\nuseEffect(() => {\n setUpdatedProposalStatus({\n ...proposalStatus(),\n value: { ...proposalStatus().value, ...snapshot.timeline },\n });\n}, [proposal]);\nconst [paymentHashes, setPaymentHashes] = useState([\"\"]);\nconst [supervisor, setSupervisor] = useState(snapshot.supervisor);\nconst selectedStatusIndex = useMemo(\n () =>\n proposalStatusOptions.findIndex((i) => {\n return updatedProposalStatus.value.status === i.value.status;\n }),\n [updatedProposalStatus]\n);\nconst TimelineItems = ({ title, children, value, values }) => {\n const indexOfCurrentItem = proposalStatusOptions.findIndex((i) =>\n Array.isArray(values)\n ? values.includes(i.value.status)\n : value === i.value.status\n );\n let color = \"transparent\";\n let statusIndex = selectedStatusIndex;\n // index 2,3,4,5 is of decision\n if (selectedStatusIndex === 3 || selectedStatusIndex === 2) {\n statusIndex = 2;\n }\n if (statusIndex === indexOfCurrentItem) {\n color = \"#FEF6EE\";\n }\n if (\n statusIndex > indexOfCurrentItem ||\n updatedProposalStatus.value.status === PROPOSAL_TIMELINE_STATUS.FUNDED\n ) {\n color = \"#EEFEF0\";\n }\n // reject\n if (statusIndex === 4 && indexOfCurrentItem === 2) {\n color = \"#FF7F7F\";\n }\n // cancelled\n if (statusIndex === 5 && indexOfCurrentItem === 2) {\n color = \"#F4F4F4\";\n }\n return (\n <div\n className=\"p-2 rounded-3\"\n style={{\n backgroundColor: color,\n }}\n >\n <div className=\"h6 text-black\"> {title}</div>\n <div className=\"text-sm\">{children}</div>\n </div>\n );\n};\nconst link = href({\n widgetSrc: `bos.forum.potlock.near/widget/app`,\n params: {\n page: \"create-proposal\",\n id: proposal.id,\n timestamp: timestamp,\n },\n});\nconst createdDate = snapshotHistory[0]?.timestamp ?? snapshot.timestamp;\nreturn (\n <Container className=\"d-flex flex-column gap-2 w-100 mt-4\">\n <Widget\n src={`devhub.near/widget/devhub.entity.proposal.ConfirmReviewModal`}\n props={{\n isOpen: isReviewModalOpen,\n onCancelClick: () => setReviewModal(false),\n onReviewClick: () => {\n setReviewModal(false);\n editProposalStatus({ timeline: proposalStatusOptions[1].value });\n },\n }}\n />\n <Widget\n src={`devhub.near/widget/devhub.entity.proposal.ConfirmCancelModal`}\n props={{\n isOpen: isCancelModalOpen,\n onCancelClick: () => setCancelModal(false),\n onConfirmClick: () => {\n setCancelModal(false);\n editProposalStatus({ timeline: proposalStatusOptions[5].value });\n },\n }}\n />\n <div className=\"d-flex px-3 px-lg-0 justify-content-between\">\n <div className=\"d-flex gap-2 align-items-center h3\">\n <div>{snapshot.name}</div>\n <div className=\"text-muted\">#{proposal.id}</div>\n </div>\n <div className=\"d-flex gap-2 align-items-center\">\n <Widget\n src={`near/widget/ShareButton`}\n props={{\n postType: \"post\",\n url: proposalURL,\n }}\n />\n {((isAllowedToEditProposal &&\n snapshot.timeline.status === PROPOSAL_TIMELINE_STATUS.DRAFT) ||\n isModerator) && (\n <Link to={link} style={{ textDecoration: \"none\" }}>\n <Widget\n src={`devhub.near/widget/devhub.components.molecule.Button`}\n props={{\n label: \"Edit\",\n classNames: { root: \"grey-btn btn-sm\" },\n }}\n />\n </Link>\n )}\n </div>\n </div>\n <div className=\"d-flex flex-wrap flex-md-nowrap px-3 px-lg-0 gap-2 align-items-center text-sm pb-3 w-100\">\n <Widget\n src={`devhub.near/widget/devhub.entity.proposal.StatusTag`}\n props={{\n timelineStatus: snapshot.timeline.status,\n size: \"sm\",\n }}\n />\n <div className=\"w-100 d-flex flex-wrap flex-md-nowrap gap-1 align-items-center\">\n <div className=\"fw-bold text-truncate\">\n <LinkProfile account={authorId}>{authorId}</LinkProfile>\n </div>\n <div>created on {readableDate(createdDate / 1000000)}</div>\n </div>\n </div>\n <div className=\"card no-border rounded-0 full-width-div px-3 px-lg-0\">\n <div className=\"container-xl py-4\">\n {snapshot.timeline.status === PROPOSAL_TIMELINE_STATUS.DRAFT &&\n isAllowedToEditProposal && (\n <div className=\"draft-info-container p-3 p-sm-4 d-flex flex-wrap flex-sm-nowrap justify-content-between align-items-center gap-2 rounded-2\">\n <div style={{ minWidth: \"300px\" }}>\n <b>\n This proposal is in draft mode and open for community\n comments.\n </b>\n <p className=\"text-sm text-muted mt-2\">\n The author can still refine the proposal and build consensus\n before sharing it with sponsors. Click “Ready for review” when\n you want to start the official review process. This will lock\n the editing function, but comments are still open.\n </p>\n </div>\n <div style={{ minWidth: \"fit-content\" }}>\n <Widget\n src={`devhub.near/widget/devhub.components.molecule.Button`}\n props={{\n label: \"Ready for review\",\n classNames: { root: \"grey-btn btn-sm\" },\n onClick: () => setReviewModal(true),\n }}\n />\n </div>\n </div>\n )}\n {snapshot.timeline.status === PROPOSAL_TIMELINE_STATUS.REVIEW &&\n isAllowedToEditProposal && (\n <div className=\"review-info-container p-3 p-sm-4 d-flex flex-wrap flex-sm-nowrap justify-content-between align-items-center gap-2 rounded-2\">\n <div style={{ minWidth: \"300px\" }}>\n <b>\n This proposal is in review mode and still open for community\n comments.\n </b>\n <p className=\"text-sm text-muted mt-2\">\n You can’t edit the proposal, but comments are open. Only\n moderators can make changes. Click “Cancel Proposal” to cancel\n your proposal. This changes the status to Canceled, signaling\n to sponsors that it’s no longer active or relevant.\n </p>\n </div>\n <div style={{ minWidth: \"fit-content\" }}>\n <Widget\n src={`devhub.near/widget/devhub.components.molecule.Button`}\n props={{\n label: (\n <div className=\"d-flex align-items-center gap-1\">\n <i class=\"bi bi-trash3\"></i> Cancel Proposal\n </div>\n ),\n classNames: { root: \"btn-outline-danger btn-sm\" },\n onClick: () => setCancelModal(true),\n }}\n />\n </div>\n </div>\n )}\n <div className=\"my-4\">\n <div className=\"d-flex flex-wrap gap-6\">\n <div\n style={{ minWidth: \"350px\" }}\n className=\"flex-3 order-2 order-md-1\"\n >\n <div\n className=\"d-flex gap-2 flex-1\"\n style={{\n zIndex: 99,\n background: \"white\",\n position: \"relative\",\n }}\n >\n <div\n className=\"d-none d-sm-flex\"\n style={{ height: \"max-content\" }}\n >\n <Widget\n src={`bos.forum.potlock.near/widget/components.molecule.Profile`}\n props={{\n accountId: authorId,\n }}\n />\n </div>\n <ProposalContainer className=\"rounded-2 flex-1\">\n <Header className=\"d-flex gap-1 align-items-center p-2 px-3 \">\n <div\n className=\"fw-bold text-truncate\"\n style={{ maxWidth: \"60%\" }}\n >\n <LinkProfile account={authorId}>{authorId}</LinkProfile>\n </div>\n <div\n className=\"text-muted\"\n style={{ minWidth: \"fit-content\" }}\n >\n ・{\" \"}\n <Widget\n src={`near/widget/TimeAgo`}\n props={{\n blockHeight,\n blockTimestamp: createdDate,\n }}\n />\n {context.accountId && (\n <div className=\"menu\">\n <Widget\n src={`near/widget/Posts.Menu`}\n props={{\n accountId: authorId,\n blockHeight: blockHeight,\n }}\n />\n </div>\n )}\n </div>\n </Header>\n <div className=\"d-flex flex-column gap-1 p-2 px-3 description-box\">\n <div className=\"text-muted h6 border-bottom pb-1 mt-3\">\n PROPOSAL CATEGORY\n </div>\n <div>\n <Widget\n src={`bos.forum.potlock.near/widget/components.molecule.MultiSelectCategoryDropdown`}\n props={{\n selected: snapshot.labels,\n disabled: true,\n hideDropdown: true,\n onChange: () => {},\n availableOptions: rfpLabelOptions,\n }}\n />\n </div>\n <div className=\"text-muted h6 border-bottom pb-1 mt-3\">\n SUMMARY\n </div>\n <div>{snapshot.summary}</div>\n <div className=\"text-muted h6 border-bottom pb-1 mt-3 mb-4\">\n DESCRIPTION\n </div>\n <Widget\n src={`devhub.near/widget/devhub.components.molecule.MarkdownViewer`}\n props={{ text: snapshot.description }}\n />\n <div className=\"d-flex gap-2 align-items-center mt-4\">\n <Widget\n src={`bos.forum.potlock.near/widget/components.molecule.LikeButton`}\n props={{\n item,\n proposalId: proposal.id,\n notifyAccountIds: [authorId],\n }}\n />\n <Widget\n src={`devhub.near/widget/devhub.entity.proposal.CommentIcon`}\n props={{\n item,\n showOverlay: false,\n onClick: () => {},\n }}\n />\n <Widget\n src={`near/widget/CopyUrlButton`}\n props={{\n url: proposalURL,\n }}\n />\n </div>\n </div>\n </ProposalContainer>\n </div>\n <div className=\"border-bottom pb-4 mt-4\">\n <Widget\n src={`bos.forum.potlock.near/widget/components.proposals.CommentsAndLogs`}\n props={{\n ...props,\n id: proposal.id,\n item: item,\n snapshotHistory: snapshotHistory,\n latestSnapshot: snapshot,\n }}\n />\n </div>\n <div\n style={{\n position: \"relative\",\n zIndex: 99,\n backgroundColor: \"white\",\n }}\n className=\"pt-4\"\n >\n <Widget\n src={`bos.forum.potlock.near/widget/components.molecule.ComposeComment`}\n props={{\n ...props,\n item: item,\n notifyAccountIds: [authorId],\n proposalId: proposal.id,\n }}\n />\n </div>\n </div>\n <div\n style={{ minWidth: \"350px\" }}\n className=\"d-flex flex-column gap-4 flex-1 order-1 order-md-2\"\n >\n <SidePanelItem title=\"Author\">\n {console.log({ authorId })}\n <Widget\n src={`bos.forum.potlock.near/widget/components.molecule.AccountProfile`}\n props={{\n accountId: authorId,\n noOverlay: true,\n }}\n />\n </SidePanelItem>\n <SidePanelItem\n title={\"Linked RFP\"}\n ishidden={!isNumber(snapshot.linked_rfp)}\n >\n <Widget\n src={`bos.forum.potlock.near/widget/components.molecule.LinkedRfps`}\n props={{\n linkedRfpIds: [snapshot.linked_rfp],\n }}\n />\n </SidePanelItem>\n <SidePanelItem\n title={\n \"Linked Proposals \" + `(${snapshot.linked_proposals.length})`\n }\n ishidden={!snapshot.linked_proposals.length}\n >\n <Widget\n src={`bos.forum.potlock.near/widget/components.molecule.LinkedProposals`}\n props={{\n linkedProposalIds: snapshot.linked_proposals,\n }}\n />\n </SidePanelItem>\n <SidePanelItem title=\"Funding Ask\">\n <div className=\"h4 text-black\">\n {snapshot.requested_sponsorship_usd_amount && (\n <div className=\"d-flex flex-column gap-1\">\n <div>\n {parseInt(\n snapshot.requested_sponsorship_usd_amount\n ).toLocaleString()}{\" \"}\n USD\n </div>\n <div className=\"text-sm text-muted\">\n Requested in{\" \"}\n {snapshot.requested_sponsorship_paid_in_currency}\n </div>\n </div>\n )}\n </div>\n </SidePanelItem>\n <SidePanelItem title=\"Recipient Wallet Address\">\n <Widget\n src={`bos.forum.potlock.near/widget/components.molecule.AccountProfile`}\n props={{\n accountId: snapshot.receiver_account,\n noOverlay: true,\n }}\n />\n </SidePanelItem>\n <SidePanelItem title=\"Recipient Verification Status\">\n <Widget\n src={`devhub.near/widget/devhub.entity.proposal.VerificationStatus`}\n props={{\n receiverAccount: snapshot.receiver_account,\n showGetVerifiedBtn:\n accountId === snapshot.receiver_account ||\n accountId === authorId,\n }}\n />\n </SidePanelItem>\n <SidePanelItem\n title=\"Project Coordinator\"\n ishidden={!snapshot.supervisor}\n >\n <Widget\n src={`bos.forum.potlock.near/widget/components.molecule.AccountProfile`}\n props={{\n accountId: snapshot.supervisor,\n noOverlay: true,\n }}\n />\n </SidePanelItem>\n <SidePanelItem\n hideBorder={true}\n title={\n <div>\n <div className=\"d-flex justify-content-between align-content-center\">\n Timeline\n {isModerator && (\n <div onClick={() => setShowTimelineSetting(true)}>\n <i class=\"bi bi-gear\"></i>\n </div>\n )}\n </div>\n {showTimelineSetting && (\n <div className=\"mt-2 d-flex flex-column gap-2\">\n <h6 className=\"mb-0\">Proposal Status</h6>\n <Widget\n src={`devhub.near/widget/devhub.components.molecule.DropDown`}\n props={{\n options: proposalStatusOptions,\n selectedValue: updatedProposalStatus,\n onUpdate: (v) => {\n setUpdatedProposalStatus({\n ...v,\n value: {\n ...v.value,\n ...updatedProposalStatus.value,\n status: v.value.status,\n },\n });\n },\n }}\n />\n </div>\n )}\n </div>\n }\n >\n <div className=\"d-flex flex-column gap-2\">\n <div className=\"d-flex gap-3 mt-2\">\n <div className=\"d-flex flex-column\">\n {stepsArray.map((_, index) => {\n const indexOfCurrentItem = index;\n let color = \"\";\n let statusIndex = selectedStatusIndex;\n // index 2,3,4 is of decision\n if (\n selectedStatusIndex === 3 ||\n selectedStatusIndex === 2 ||\n selectedStatusIndex === 4 ||\n selectedStatusIndex === 5\n ) {\n statusIndex = 2;\n }\n if (selectedStatusIndex === 6) {\n statusIndex = 3;\n }\n const current = statusIndex === indexOfCurrentItem;\n const completed =\n statusIndex > indexOfCurrentItem ||\n updatedProposalStatus.value.status ===\n PROPOSAL_TIMELINE_STATUS.FUNDED;\n return (\n <div className=\"d-flex flex-column align-items-center gap-1\">\n <div\n className={\n \"circle \" +\n (completed && \" green-fill \") +\n (current && \" yellow-fill \")\n }\n >\n {completed && (\n <div\n className=\"d-flex justify-content-center align-items-center\"\n style={{ height: \"110%\" }}\n >\n <i class=\"bi bi-check\"></i>\n </div>\n )}\n </div>\n {index !== stepsArray.length - 1 && (\n <div\n className={\n \"vertical-line\" +\n (index === stepsArray.length - 2\n ? \"-sm \"\n : \" \") +\n (completed && \" green-fill \") +\n (current && \" yellow-fill \")\n }\n ></div>\n )}\n </div>\n );\n })}\n </div>\n <div className=\"d-flex flex-column gap-3\">\n <TimelineItems\n title=\"1) Draft\"\n value={PROPOSAL_TIMELINE_STATUS.DRAFT}\n >\n <div>\n Once an author submits a proposal, it is in draft mode\n and open for community comments. The author can still\n make changes to the proposal during this stage and\n submit it for official review when ready.\n </div>\n </TimelineItems>\n <TimelineItems\n title=\"2) Review\"\n value={PROPOSAL_TIMELINE_STATUS.REVIEW}\n >\n <div className=\"d-flex flex-column gap-2\">\n Sponsors who agree to consider the proposal may\n request attestations from work groups.\n <CheckBox\n value=\"\"\n disabled={selectedStatusIndex !== 1}\n onClick={(value) =>\n setUpdatedProposalStatus((prevState) => ({\n ...prevState,\n value: {\n ...prevState.value,\n sponsor_requested_review: value,\n },\n }))\n }\n label=\"Sponsor provides feedback or requests reviews\"\n isChecked={\n updatedProposalStatus.value\n .sponsor_requested_review\n }\n />\n <CheckBox\n value=\"\"\n disabled={selectedStatusIndex !== 1}\n label=\"Reviewer completes attestations (Optional)\"\n onClick={(value) =>\n setUpdatedProposalStatus((prevState) => ({\n ...prevState,\n value: {\n ...prevState.value,\n reviewer_completed_attestation: value,\n },\n }))\n }\n isChecked={\n updatedProposalStatus.value\n .reviewer_completed_attestation\n }\n />\n </div>\n </TimelineItems>\n <TimelineItems\n title=\"3) Decision\"\n values={[\n PROPOSAL_TIMELINE_STATUS.APPROVED,\n PROPOSAL_TIMELINE_STATUS.APPROVED_CONDITIONALLY,\n PROPOSAL_TIMELINE_STATUS.REJECTED,\n ]}\n >\n <div className=\"d-flex flex-column gap-2\">\n <div>Sponsor makes a final decision:</div>\n <RadioButton\n value=\"\"\n label={<div className=\"fw-bold\">Approved</div>}\n isChecked={\n updatedProposalStatus.value.status ===\n PROPOSAL_TIMELINE_STATUS.APPROVED ||\n updatedProposalStatus.value.status ===\n PROPOSAL_TIMELINE_STATUS.PAYMENT_PROCESSING ||\n updatedProposalStatus.value.status ===\n PROPOSAL_TIMELINE_STATUS.FUNDED\n }\n />\n <RadioButton\n value=\"\"\n label={\n <>\n <div className=\"fw-bold\">\n Approved - Conditional{\" \"}\n </div>\n <span>\n Requires follow up from recipient. Moderators\n will provide further details.\n </span>\n </>\n }\n isChecked={\n updatedProposalStatus.value.status ===\n PROPOSAL_TIMELINE_STATUS.APPROVED_CONDITIONALLY\n }\n />\n <RadioButton\n value=\"Reject\"\n label={<div className=\"fw-bold\">Rejected</div>}\n isChecked={\n updatedProposalStatus.value.status ===\n PROPOSAL_TIMELINE_STATUS.REJECTED\n }\n />\n <RadioButton\n value=\"Canceled\"\n label={<div className=\"fw-bold\">Canceled</div>}\n isChecked={\n updatedProposalStatus.value.status ===\n PROPOSAL_TIMELINE_STATUS.CANCELED\n }\n />\n </div>\n </TimelineItems>\n <TimelineItems\n title=\"4) Payment Processing\"\n value={PROPOSAL_TIMELINE_STATUS.PAYMENT_PROCESSING}\n >\n <div className=\"d-flex flex-column gap-2\">\n <CheckBox\n value={updatedProposalStatus.value.kyc_verified}\n label=\"Sponsor verifies KYC/KYB\"\n disabled={selectedStatusIndex !== 6}\n onClick={(value) =>\n setUpdatedProposalStatus((prevState) => ({\n ...prevState,\n value: {\n ...prevState.value,\n kyc_verified: value,\n },\n }))\n }\n isChecked={updatedProposalStatus.value.kyc_verified}\n />\n <CheckBox\n value={\n updatedProposalStatus.value.test_transaction_sent\n }\n disabled={selectedStatusIndex !== 6}\n label=\"Sponsor confirmed sponsorship and shared funding steps with recipient\"\n onClick={(value) =>\n setUpdatedProposalStatus((prevState) => ({\n ...prevState,\n value: {\n ...prevState.value,\n test_transaction_sent: value,\n },\n }))\n }\n isChecked={\n updatedProposalStatus.value.test_transaction_sent\n }\n />\n </div>\n </TimelineItems>\n <TimelineItems\n title=\"5) Funded\"\n value={PROPOSAL_TIMELINE_STATUS.FUNDED}\n >\n <div className=\"d-flex flex-column gap-2\">\n {paymentHashes?.length > 1 ? (\n paymentHashes.slice(0, -1).map((link, index) => (\n <a\n key={index}\n href={link}\n className=\"text-decoration-underline\"\n target=\"_blank\"\n rel=\"noopener noreferrer\"\n >\n Payment Link\n <i className=\"bi bi-arrow-up-right\"></i>\n </a>\n ))\n ) : updatedProposalStatus.value.payouts.length > 0 ? (\n <div>\n {updatedProposalStatus.value.payouts.map(\n (link) => {\n return (\n <a\n href={link}\n className=\"text-decoration-underline\"\n target=\"_blank\"\n rel=\"noopener noreferrer\"\n >\n Payment Link\n <i class=\"bi bi-arrow-up-right\"></i>\n </a>\n );\n }\n )}\n </div>\n ) : (\n \"No Payouts yet\"\n )}\n </div>\n </TimelineItems>\n </div>\n </div>\n {showTimelineSetting && (\n <div className=\"d-flex flex-column gap-2\">\n <div className=\"border-vertical py-3 my-2\">\n <label className=\"text-black h6\">\n Project Coordinator\n </label>\n <Widget\n src={`devhub.near/widget/devhub.entity.proposal.AccountInput`}\n props={{\n value: supervisor,\n placeholder: \"\",\n onUpdate: setSupervisor,\n }}\n />\n </div>\n {updatedProposalStatus.value.status ===\n PROPOSAL_TIMELINE_STATUS.FUNDED && (\n <div className=\"border-vertical py-3 my-2\">\n <label className=\"text-black h6\">Payment Link</label>\n <div className=\"d-flex flex-column gap-2\">\n {paymentHashes.map((item, index) => (\n <div className=\"d-flex gap-2 justify-content-between align-items-center\">\n <Widget\n src={`devhub.near/widget/devhub.components.molecule.Input`}\n props={{\n className: \"flex-grow-1\",\n value: item,\n onChange: (e) => {\n const updatedHashes = [...paymentHashes];\n updatedHashes[index] = e.target.value;\n setPaymentHashes(updatedHashes);\n },\n skipPaddingGap: true,\n placeholder: \"Enter URL\",\n }}\n />\n <div style={{ minWidth: 20 }}>\n {index !== paymentHashes.length - 1 ? (\n <Widget\n src={`devhub.near/widget/devhub.components.molecule.Button`}\n props={{\n classNames: {\n root: \"btn-outline-danger shadow-none w-100\",\n },\n label: <i class=\"bi bi-trash3 h6\"></i>,\n onClick: () => {\n const updatedHashes = [\n ...paymentHashes,\n ];\n updatedHashes.splice(index, 1);\n setPaymentHashes(updatedHashes);\n },\n }}\n />\n ) : (\n <Widget\n src={`devhub.near/widget/devhub.components.molecule.Button`}\n props={{\n classNames: {\n root: \"green-btn shadow-none border-0 w-100\",\n },\n disabled: !item,\n label: <i class=\"bi bi-plus-lg\"></i>,\n onClick: () =>\n setPaymentHashes([\n ...paymentHashes,\n \"\",\n ]),\n }}\n />\n )}\n </div>\n </div>\n ))}\n </div>\n </div>\n )}\n <div className=\"d-flex gap-2 align-items-center justify-content-end text-sm\">\n <Widget\n src={`devhub.near/widget/devhub.components.molecule.Button`}\n props={{\n label: \"Cancel\",\n classNames: {\n root: \"btn-outline-danger border-0 shadow-none btn-sm\",\n },\n onClick: () => {\n setShowTimelineSetting(false);\n setUpdatedProposalStatus(proposalStatus);\n },\n }}\n />\n <Widget\n src={`devhub.near/widget/devhub.components.molecule.Button`}\n props={{\n label: \"Save\",\n disabled:\n !supervisor &&\n DecisionStage.includes(\n updatedProposalStatus.value.status\n ),\n classNames: { root: \"green-btn btn-sm\" },\n onClick: () => {\n if (snapshot.supervisor !== supervisor) {\n editProposal({\n timeline: updatedProposalStatus.value,\n });\n } else if (\n updatedProposalStatus.value.status ===\n PROPOSAL_TIMELINE_STATUS.FUNDED\n ) {\n editProposalStatus({\n timeline: {\n ...updatedProposalStatus.value,\n payouts: !paymentHashes[0]\n ? []\n : paymentHashes.filter(\n (item) => item !== \"\"\n ),\n },\n });\n } else {\n editProposalStatus({\n timeline: updatedProposalStatus.value,\n });\n }\n setShowTimelineSetting(false);\n },\n }}\n />\n </div>\n </div>\n )}\n </div>\n </SidePanelItem>\n </div>\n </div>\n </div>\n </div>\n </div>\n </Container>\n);\n" }, "components.rfps.CommentsAndLogs": { "": "const { RFP_TIMELINE_STATUS, getLinkUsingCurrentGateway } = VM.require(\n `bos.forum.potlock.near/widget/core.common`\n) || { RFP_TIMELINE_STATUS: {}, getLinkUsingCurrentGateway: () => {} };\nconst { href } = VM.require(`devhub.near/widget/core.lib.url`);\nhref || (href = () => {});\nconst snapshotHistory = props.snapshotHistory;\nconst approvedProposals = props.approvedProposals ?? [];\nconst Wrapper = styled.div`\n position: relative;\n .log-line {\n position: absolute;\n left: 7%;\n top: -30px;\n bottom: 0;\n z-index: 1;\n width: 1px;\n background-color: var(--bs-border-color);\n z-index: 1;\n }\n .text-wrap {\n overflow: hidden;\n white-space: normal;\n }\n .fw-bold {\n font-weight: 600 !important;\n }\n .inline-flex {\n display: -webkit-inline-box !important;\n align-items: center !important;\n gap: 0.25rem !important;\n margin-right: 2px;\n flex-wrap: wrap;\n }\n`;\nconst CommentContainer = styled.div`\n border: 1px solid lightgrey;\n overflow: auto;\n`;\nconst Header = styled.div`\n position: relative;\n background-color: #f4f4f4;\n height: 50px;\n .menu {\n position: absolute;\n right: 10px;\n top: 4px;\n font-size: 30px;\n }\n`;\n// check snapshot history all keys and values for differences\nfunction getDifferentKeysWithValues(obj1, obj2) {\n return Object.keys(obj1)\n .filter((key) => {\n if (key !== \"editor_id\" && obj2.hasOwnProperty(key)) {\n const value1 = obj1[key];\n const value2 = obj2[key];\n if (Array.isArray(value1) && Array.isArray(value2)) {\n const sortedValue1 = [...value1].sort();\n const sortedValue2 = [...value2].sort();\n return JSON.stringify(sortedValue1) !== JSON.stringify(sortedValue2);\n } else if (typeof value1 === \"object\" && typeof value2 === \"object\") {\n return JSON.stringify(value1) !== JSON.stringify(value2);\n } else {\n return value1 !== value2;\n }\n }\n return false;\n })\n .map((key) => ({\n key,\n originalValue: obj1[key],\n modifiedValue: obj2[key],\n }));\n}\nState.init({\n data: null,\n socialComments: null,\n changedKeysListWithValues: null,\n});\nfunction sortTimelineAndComments() {\n const comments = Social.index(\"comment\", props.item);\n if (state.changedKeysListWithValues === null) {\n const changedKeysListWithValues = snapshotHistory\n .slice(1)\n .map((item, index) => {\n const startingPoint = snapshotHistory[index]; // Set comparison to the previous item\n return {\n editorId: item.editor_id,\n ...getDifferentKeysWithValues(startingPoint, item),\n };\n });\n State.update({ changedKeysListWithValues });\n }\n // sort comments and timeline logs by time\n const snapShotTimeStamp = Array.isArray(snapshotHistory)\n ? snapshotHistory.map((i) => {\n return { blockHeight: null, timestamp: parseFloat(i.timestamp / 1e6) };\n })\n : [];\n const commentsTimeStampPromise = Array.isArray(comments)\n ? Promise.all(\n comments.map((item) => {\n return asyncFetch(\n `https://api.near.social/time?blockHeight=${item.blockHeight}`\n ).then((res) => {\n const timeMs = parseFloat(res.body);\n return {\n blockHeight: item.blockHeight,\n timestamp: timeMs,\n };\n });\n })\n ).then((res) => res)\n : Promise.resolve([]);\n commentsTimeStampPromise.then((commentsTimeStamp) => {\n const combinedArray = [...snapShotTimeStamp, ...commentsTimeStamp];\n combinedArray.sort((a, b) => a.timestamp - b.timestamp);\n State.update({ data: combinedArray, socialComments: comments });\n });\n}\nif ((snapshotHistory ?? []).length > 0) {\n sortTimelineAndComments();\n}\nconst Comment = ({ commentItem }) => {\n const { accountId, blockHeight } = commentItem;\n const item = {\n type: \"social\",\n path: `${accountId}/post/comment`,\n blockHeight,\n };\n const content = JSON.parse(Social.get(item.path, blockHeight) ?? \"null\");\n const link = getLinkUsingCurrentGateway(\n `bos.forum.potlock.near/widget/app?page=rfp&id=${props.id}&accountId=${accountId}&blockHeight=${blockHeight}`\n );\n function getHighlightCommentStyle() {\n const highlightComment =\n parseInt(props.blockHeight ?? \"\") === blockHeight &&\n props.accountId === accountId;\n return {\n border: highlightComment ? \"2px solid black\" : \"\",\n };\n }\n return (\n <div style={{ zIndex: 99, background: \"white\" }}>\n <div className=\"d-flex gap-2 flex-1\">\n <div className=\"d-none d-sm-flex\">\n <Widget\n src={`bos.forum.potlock.near/widget/components.molecule.Profile`}\n props={{\n accountId: accountId,\n }}\n />\n </div>\n <CommentContainer\n style={getHighlightCommentStyle()}\n className=\"rounded-2 flex-1\"\n >\n <Header className=\"d-flex gap-3 align-items-center p-2 px-3\">\n <div className=\"text-muted\">\n <a\n rel=\"noopener noreferrer\"\n target=\"_blank\"\n href={`https://bos.potlock.org/?tab=profile&accountId=${accountId}`}\n >\n <span className=\"fw-bold text-black\">{accountId}</span>\n </a>\n commented ・{\" \"}\n <Widget\n src={`near/widget/TimeAgo`}\n props={{\n blockHeight: blockHeight,\n }}\n />\n </div>\n {context.accountId && (\n <div className=\"menu\">\n <Widget\n src={`near/widget/Posts.Menu`}\n props={{\n accountId: accountId,\n blockHeight: blockHeight,\n contentPath: `/post/comment`,\n contentType: \"comment\",\n }}\n />\n </div>\n )}\n </Header>\n <div className=\"p-2 px-3\">\n <Widget\n src={`devhub.near/widget/devhub.components.molecule.MarkdownViewer`}\n props={{\n text: content.text,\n }}\n />\n <div className=\"d-flex gap-2 align-items-center mt-4\">\n <Widget\n src={`devhub.near/widget/devhub.entity.proposal.LikeButton`}\n props={{\n item: item,\n notifyAccountId: accountId,\n }}\n />\n <Widget\n src={`near/widget/CopyUrlButton`}\n props={{\n url: link,\n }}\n />\n </div>\n </div>\n </CommentContainer>\n </div>\n </div>\n );\n};\nfunction capitalizeFirstLetter(string) {\n const updated = string.replace(\"_\", \" \");\n return updated.charAt(0).toUpperCase() + updated.slice(1).toLowerCase();\n}\nfunction parseTimelineKeyAndValue(timeline, originalValue, modifiedValue) {\n const oldValue = originalValue[timeline];\n const newValue = modifiedValue[timeline];\n switch (timeline) {\n case \"status\":\n if (newValue === RFP_TIMELINE_STATUS.PROPOSAL_SELECTED) {\n return (\n <span className=\"inline-flex\">\n moved RFP to{\" \"}\n <Widget\n src={`bos.forum.potlock.near/widget/components.rfps.StatusTag`}\n props={{\n timelineStatus: newValue,\n }}\n />\n ・ selected proposal(s) are{\" \"}\n {approvedProposals.map((i, index) => (\n <span>\n <LinkToProposal id={i.proposal_id}>\n {\" \"}\n #{i.proposal_id} {i.name}\n </LinkToProposal>\n {index < approvedProposals.length - 1 && \", \"}\n </span>\n ))}\n </span>\n );\n }\n return (\n oldValue !== newValue && (\n <span className=\"inline-flex\">\n moved RFP from{\" \"}\n <Widget\n src={`bos.forum.potlock.near/widget/components.rfps.StatusTag`}\n props={{\n timelineStatus: oldValue,\n }}\n />\n to{\" \"}\n <Widget\n src={`bos.forum.potlock.near/widget/components.rfps.StatusTag`}\n props={{\n timelineStatus: newValue,\n }}\n />\n stage\n </span>\n )\n );\n default:\n return null;\n }\n}\nconst AccountProfile = ({ accountId }) => {\n return (\n <span className=\"inline-flex fw-bold text-black\">\n <Widget\n src={`bos.forum.potlock.near/widget/components.molecule.Profile`}\n props={{\n accountId: accountId,\n size: \"sm\",\n showAccountId: true,\n }}\n />\n </span>\n );\n};\nfunction symmetricDifference(arr1, arr2) {\n const diffA = arr1.filter((item) => !arr2.includes(item));\n const diffB = arr2.filter((item) => !arr1.includes(item));\n return [...diffA, ...diffB];\n}\nconst LinkToProposal = ({ id, children }) => {\n return (\n <a\n className=\"text-decoration-underline flex-1\"\n href={href({\n widgetSrc: `bos.forum.potlock.near/widget/app`,\n params: {\n page: \"proposal\",\n id: id,\n },\n })}\n target=\"_blank\"\n rel=\"noopener noreferrer\"\n >\n {children}\n </a>\n );\n};\nconst parseProposalKeyAndValue = (key, modifiedValue, originalValue) => {\n switch (key) {\n case \"name\":\n return <span>changed title</span>;\n case \"summary\":\n case \"description\":\n return <span>changed {key}</span>;\n case \"labels\":\n return <span>changed labels to {(modifiedValue ?? []).join(\", \")}</span>;\n case \"linked_proposals\": {\n const newProposals = modifiedValue || [];\n const oldProposals = originalValue || [];\n const difference = symmetricDifference(oldProposals, newProposals).join(\n \",\"\n );\n const isUnlinked = oldProposals.length > newProposals.length;\n const actionText = isUnlinked\n ? \"unlinked a proposal\"\n : \"linked a proposal\";\n return (\n <span>\n {actionText}{\" \"}\n <LinkToProposal id={difference}> #{difference}</LinkToProposal>\n </span>\n );\n }\n case \"timeline\": {\n const modifiedKeys = Object.keys(modifiedValue);\n const originalKeys = Object.keys(originalValue);\n return modifiedKeys.map((i, index) => {\n const text = parseTimelineKeyAndValue(i, originalValue, modifiedValue);\n return (\n text && (\n <span key={index} className=\"inline-flex\">\n {text}\n {text &&\n originalKeys.length > 1 &&\n index < modifiedKeys.length - 1 &&\n \"・\"}\n </span>\n )\n );\n });\n }\n default:\n return null;\n }\n};\nconst LogIconContainer = styled.div`\n margin-left: 50px;\n z-index: 99;\n @media screen and (max-width: 768px) {\n margin-left: 10px;\n }\n`;\nconst Log = ({ timestamp }) => {\n const updatedData = useMemo(\n () =>\n state.changedKeysListWithValues.find((obj) =>\n Object.values(obj).some(\n (value) =>\n value && parseFloat(value.modifiedValue / 1e6) === timestamp\n )\n ),\n [state.changedKeysListWithValues, timestamp]\n );\n const editorId = updatedData.editorId;\n const valuesArray = Object.values(updatedData ?? {});\n // if valuesArray length is 2 that means it only has timestamp and editorId\n if (!updatedData || valuesArray.length === 2) {\n return <></>;\n }\n return valuesArray.map((i, index) => {\n if (i.key && i.key !== \"timestamp\") {\n return (\n <LogIconContainer\n className=\"d-flex gap-3 align-items-center\"\n key={index}\n >\n <img\n src=\"https://ipfs.near.social/ipfs/bafkreiffqrxdi4xqu7erf46gdlwuodt6dm6rji2jtixs3iionjvga6rhdi\"\n height={30}\n />\n <div\n className={\n \"flex-1 gap-1 w-100 text-wrap text-muted align-items-center \" +\n (i.key === \"timeline\" &&\n Object.keys(i.originalValue ?? {}).length > 1\n ? \"\"\n : \"inline-flex\")\n }\n >\n <span className=\"inline-flex fw-bold text-black\">\n <AccountProfile accountId={editorId} showAccountId={true} />\n </span>\n {parseProposalKeyAndValue(i.key, i.modifiedValue, i.originalValue)}\n ・\n <Widget\n src={`near/widget/TimeAgo`}\n props={{\n blockTimestamp: timestamp * 1000000,\n }}\n />\n </div>\n </LogIconContainer>\n );\n }\n });\n};\nif (Array.isArray(state.data)) {\n return (\n <Wrapper>\n <div\n className=\"log-line\"\n style={{ height: state.data.length > 2 ? \"110%\" : \"150%\" }}\n ></div>\n <div className=\"d-flex flex-column gap-4\">\n {state.data.map((i, index) => {\n if (i.blockHeight) {\n const item = state.socialComments.find(\n (t) => t.blockHeight === i.blockHeight\n );\n return <Comment commentItem={item} />;\n } else {\n return <Log timestamp={i.timestamp} key={index} />;\n }\n })}\n </div>\n </Wrapper>\n );\n}\n" }, "components.molecule.AccountProfile": { "": "let {\n accountId,\n blockHeight,\n blockTimestamp,\n profile,\n verifications,\n showFlagAccountFeature,\n} = props;\naccountId = accountId || context.accountId;\nshowFlagAccountFeature = showFlagAccountFeature ?? false;\nprofile = profile || Social.get(`${accountId}/profile/**`, \"final\");\nconst profileUrl = `https://bos.potlock.org/?tab=profile&accountId=${accountId}`;\nconst Wrapper = styled.a`\n display: inline-grid;\n width: 100%;\n align-items: center;\n gap: 12px;\n grid-template-columns: auto 1fr;\n cursor: pointer;\n margin: 0;\n color: #687076 !important;\n outline: none;\n text-decoration: none !important;\n background: none !important;\n border: none;\n text-align: left;\n padding: 0;\n > * {\n min-width: 0;\n }\n &:hover,\n &:focus {\n div:first-child {\n border-color: #d0d5dd;\n }\n }\n`;\nconst Text = styled.p`\n margin: 0;\n font-size: 14px;\n line-height: 20px;\n color: ${(p) => (p.bold ? \"#11181C\" : \"#687076\")};\n font-weight: ${(p) => (p.bold ? \"600\" : \"400\")};\n font-size: ${(p) => (p.small ? \"10px\" : \"14px\")};\n overflow: ${(p) => (p.ellipsis ? \"hidden\" : \"\")};\n text-overflow: ${(p) => (p.ellipsis ? \"ellipsis\" : \"\")};\n white-space: nowrap !important;\n`;\nconst Avatar = styled.div`\n width: ${props.avatarSize || \"40px\"};\n height: ${props.avatarSize || \"40px\"};\n flex-shrink: 0;\n border: 1px solid #eceef0;\n overflow: hidden;\n border-radius: 40px;\n transition: border-color 200ms;\n img {\n object-fit: cover;\n width: 100%;\n height: 100%;\n margin: 0 !important;\n }\n`;\nconst VerifiedBadge = styled.div`\n position: absolute;\n left: 24px;\n top: 22px;\n`;\nconst Name = styled.div`\n display: flex;\n gap: 6px;\n align-items: center;\n`;\nconst AccountProfile = (\n <Wrapper\n href={!props.onClick && profileUrl}\n target=\"_blank\"\n rel=\"noopener noreferrer\"\n >\n <Avatar>\n <Widget\n src=\"mob.near/widget/Image\"\n props={{\n image: profile.image,\n alt: profile.name,\n fallbackUrl:\n \"https://ipfs.near.social/ipfs/bafkreibiyqabm3kl24gcb2oegb7pmwdi6wwrpui62iwb44l7uomnn3lhbi\",\n }}\n />\n </Avatar>\n {verifications && (\n <VerifiedBadge>\n <Widget\n src=\"near/widget/Settings.Identity.Verifications.Icon\"\n props={{ type: \"base\" }}\n />\n </VerifiedBadge>\n )}\n <div>\n <div>\n <div>{profile.name || accountId.split(\".near\")[0]}</div>\n {props.inlineContent}\n {props.blockHeight && (\n <div style={{ marginLeft: \"auto\" }}>\n Joined{\" \"}\n <Widget\n src=\"near/widget/TimeAgo\"\n props={{ blockHeight, blockTimestamp }}\n />\n ago\n </div>\n )}\n </div>\n {!props.hideAccountId && <div>@{accountId}</div>}\n </div>\n </Wrapper>\n);\nif (props.noOverlay) return AccountProfile;\nreturn (\n <div>dfdsfs</div>\n // <Widget\n // src=\"near/widget/AccountProfileOverlay\"\n // props={{\n // accountId,\n // profile,\n // children: AccountProfile,\n // placement: props.overlayPlacement,\n // verifications,\n // showFlagAccountFeature,\n // }}\n // />\n);\n" }, "components.proposals.Editor": { "": "const { RFP_TIMELINE_STATUS, parseJSON, isNumber } = VM.require(\n `bos.forum.potlock.near/widget/core.common`\n) || { RFP_TIMELINE_STATUS: {}, parseJSON: () => {}, isNumber: () => {} };\nconst { href } = VM.require(`devhub.near/widget/core.lib.url`);\nhref || (href = () => {});\nconst { getGlobalLabels } = VM.require(\n `bos.forum.potlock.near/widget/components.core.lib.contract`\n) || { getGlobalLabels: () => {} };\nconst { id, timestamp, rfp_id } = props;\nconst isEditPage = typeof id === \"string\";\nconst author = context.accountId;\nconst ToCDocs = \"https://aipgf.com/terms\";\nconst CoCDocs = \"https://aipgf.com/conduct\";\nif (!author) {\n return (\n <Widget\n src={`bos.forum.potlock.near/widget/components.molecule.LoginScreen`}\n />\n );\n}\nlet editProposalData = null;\nlet draftProposalData = null;\nconst draftKey = \"AI_PGF_PROPOSAL_EDIT\";\nconst rfpLabelOptions = getGlobalLabels();\nif (isEditPage) {\n editProposalData = Near.view(\"forum.potlock.near\", \"get_proposal\", {\n proposal_id: parseInt(id),\n });\n}\nconst Container = styled.div`\n input {\n font-size: 14px !important;\n }\n .card.no-border {\n border-left: none !important;\n border-right: none !important;\n margin-bottom: -3.5rem;\n }\n textarea {\n font-size: 14px !important;\n }\n .full-width-div {\n width: 100vw;\n position: relative;\n left: 50%;\n right: 50%;\n margin-left: -50vw;\n margin-right: -50vw;\n }\n .text-sm {\n font-size: 13px;\n }\n @media screen and (max-width: 768px) {\n .h6 {\n font-size: 14px !important;\n }\n .h5 {\n font-size: 16px !important;\n }\n .text-sm {\n font-size: 11px;\n }\n .gap-6 {\n gap: 0.5rem !important;\n }\n }\n .border-bottom {\n border-bottom: var(--bs-card-border-width) solid var(--bs-card-border-color);\n }\n .text-xs {\n font-size: 10px;\n }\n .flex-2 {\n flex: 2;\n }\n .flex-1 {\n flex: 1;\n }\n .bg-grey {\n background-color: #f4f4f4;\n }\n .border-bottom {\n border-bottom: 1px solid grey;\n }\n .cursor-pointer {\n cursor: pointer;\n }\n .proposal-card {\n &:hover {\n background-color: #f4f4f4;\n }\n }\n .border-1 {\n border: 1px solid #e2e6ec;\n }\n .green-btn {\n background-color: #03ba16 !important;\n border: none;\n color: white;\n &:active {\n color: white;\n }\n }\n .black-btn {\n background-color: #000 !important;\n border: none;\n color: white;\n &:active {\n color: white;\n }\n }\n .dropdown-toggle:after {\n position: absolute;\n top: 46%;\n right: 5%;\n }\n .drop-btn {\n max-width: none !important;\n }\n .dropdown-menu {\n width: 100%;\n border-radius: 0.375rem !important;\n }\n .input-icon {\n display: flex;\n height: 100%;\n align-items: center;\n border-right: 1px solid #dee2e6;\n padding-right: 10px;\n }\n /* Tooltip container */\n .custom-tooltip {\n position: relative;\n display: inline-block;\n }\n /* Tooltip text */\n .custom-tooltip .tooltiptext {\n visibility: hidden;\n width: 250px;\n background-color: #fff;\n color: #6c757d;\n text-align: center;\n padding: 10px;\n border-radius: 6px;\n font-size: 12px;\n border: 0.2px solid #6c757d;\n /* Position the tooltip text */\n position: absolute;\n z-index: 1;\n bottom: 125%;\n left: -30px;\n /* Fade in tooltip */\n opacity: 0;\n transition: opacity 0.3s;\n }\n /* Tooltip arrow */\n .custom-tooltip .tooltiptext::after {\n content: \"\";\n position: absolute;\n top: 100%;\n left: 15%;\n margin-left: -5px;\n border-width: 5px;\n border-style: solid;\n border-color: #555 transparent transparent transparent;\n }\n /* Show the tooltip text when you mouse over the tooltip container */\n .custom-tooltip:hover .tooltiptext {\n visibility: visible;\n opacity: 1;\n }\n .form-check-input:checked {\n background-color: #04a46e !important;\n border-color: #04a46e !important;\n }\n .gap-6 {\n gap: 2.5rem;\n }\n a.no-space {\n display: inline-block;\n }\n`;\nconst Heading = styled.div`\n font-size: 24px;\n font-weight: 700;\n @media screen and (max-width: 768px) {\n font-size: 18px;\n }\n`;\nconst tokensOptions = [\n { label: \"NEAR\", value: \"NEAR\" },\n { label: \"USDT\", value: \"USDT\" },\n {\n label: \"USDC\",\n value: \"USDC\",\n },\n {\n label: \"Other\",\n value: \"OTHER\",\n },\n];\nconst [linkedRfp, setLinkedRfp] = useState(rfp_id ? parseInt(rfp_id) : null);\nconst [labels, setLabels] = useState([]);\nconst [title, setTitle] = useState(null);\nconst [description, setDescription] = useState(null);\nconst [summary, setSummary] = useState(null);\nconst [consent, setConsent] = useState({ toc: false, coc: false });\nconst [linkedProposals, setLinkedProposals] = useState([]);\nconst [receiverAccount, setReceiverAccount] = useState(context.accountId);\nconst [requestedSponsorshipAmount, setRequestedSponsorshipAmount] =\n useState(null);\nconst [requestedSponsorshipToken, setRequestedSponsorshipToken] = useState(\n tokensOptions[2]\n);\nconst [allowDraft, setAllowDraft] = useState(true);\nconst [loading, setLoading] = useState(true);\nconst [disabledSubmitBtn, setDisabledSubmitBtn] = useState(false);\nconst [isDraftBtnOpen, setDraftBtnOpen] = useState(false);\nconst [selectedStatus, setSelectedStatus] = useState(\"draft\");\nconst [isReviewModalOpen, setReviewModal] = useState(false);\nconst [isCancelModalOpen, setCancelModal] = useState(false);\nconst [showProposalViewModal, setShowProposalViewModal] = useState(false); // when user creates/edit a proposal and confirm the txn, this is true\nconst [proposalId, setProposalId] = useState(null);\nconst [proposalIdsArray, setProposalIdsArray] = useState(null);\nconst [isTxnCreated, setCreateTxn] = useState(false);\nconst [oldProposalData, setOldProposalData] = useState(null);\nconst [supervisor, setSupervisor] = useState(null);\nif (allowDraft) {\n draftProposalData = Storage.privateGet(draftKey);\n}\nconst isModerator = Near.view(\n \"forum.potlock.near\",\n \"is_allowed_to_write_rfps\",\n {\n editor: context.accountId,\n }\n);\nconst memoizedDraftData = useMemo(\n () => ({\n id: editProposalData.id ?? null,\n snapshot: {\n linked_rfp: linkedRfp,\n name: title,\n description: description,\n labels: labels,\n summary: summary,\n requested_sponsorship_usd_amount: requestedSponsorshipAmount,\n requested_sponsorship_paid_in_currency: requestedSponsorshipToken.value,\n receiver_account: receiverAccount,\n },\n }),\n [\n linkedRfp,\n title,\n summary,\n description,\n labels,\n requestedSponsorshipAmount,\n requestedSponsorshipToken,\n receiverAccount,\n ]\n);\nuseEffect(() => {\n if (allowDraft) {\n let data = editProposalData || JSON.parse(draftProposalData);\n let snapshot = data.snapshot;\n if (data) {\n if (timestamp) {\n snapshot =\n data.snapshot_history.find((item) => item.timestamp === timestamp) ??\n data.snapshot;\n }\n if (\n draftProposalData &&\n editProposalData &&\n editProposalData.id === JSON.parse(draftProposalData).id\n ) {\n snapshot = {\n ...editProposalData.snapshot,\n ...JSON.parse(draftProposalData).snapshot,\n };\n }\n if (!isNumber(linkedRfp)) {\n setLinkedRfp(snapshot.linked_rfp);\n }\n setLabels(snapshot.labels ?? []);\n setTitle(snapshot.name);\n setSummary(snapshot.summary);\n setDescription(snapshot.description);\n setReceiverAccount(snapshot.receiver_account);\n setRequestedSponsorshipAmount(snapshot.requested_sponsorship_usd_amount);\n setSupervisor(snapshot.supervisor);\n const token = tokensOptions.find(\n (item) => item.value === snapshot.requested_sponsorship_paid_in_currency\n );\n setRequestedSponsorshipToken(token ?? tokensOptions[2]);\n if (isEditPage) {\n setConsent({ toc: true, coc: true });\n }\n }\n }\n}, [editProposalData, draftProposalData, allowDraft]);\n// show loader until LS data is set in state\nuseEffect(() => {\n const handler = setTimeout(() => {\n setAllowDraft(false);\n setLoading(false);\n }, 500);\n return () => clearTimeout(handler);\n}, []);\nuseEffect(() => {\n if (showProposalViewModal) {\n return;\n }\n setDisabledSubmitBtn(\n isTxnCreated ||\n !title ||\n !description ||\n !summary ||\n !(labels ?? []).length ||\n !requestedSponsorshipAmount ||\n !receiverAccount ||\n !consent.toc ||\n !consent.coc\n );\n const handler = setTimeout(() => {\n Storage.privateSet(draftKey, JSON.stringify(memoizedDraftData));\n }, 10000);\n return () => clearTimeout(handler);\n}, [\n memoizedDraftData,\n draftKey,\n draftProposalData,\n consent,\n isTxnCreated,\n showProposalViewModal,\n]);\n// set RFP labels, disable link rfp change when linked rfp is past accepting stage\nconst [disabledLinkRFP, setDisableLinkRFP] = useState(false);\nuseEffect(() => {\n if (linkedRfp) {\n Near.asyncView(\"forum.potlock.near\", \"get_rfp\", {\n rfp_id: linkedRfp.value ?? linkedRfp,\n }).then((i) => {\n const timeline = parseJSON(i.snapshot.timeline);\n setDisableLinkRFP(\n !isModerator &&\n timeline.status !== RFP_TIMELINE_STATUS.ACCEPTING_SUBMISSIONS\n );\n setLabels(i.snapshot.labels);\n });\n }\n}, [linkedRfp]);\nuseEffect(() => {\n if (\n editProposalData &&\n editProposalData?.snapshot?.linked_proposals?.length > 0\n ) {\n editProposalData.snapshot.linked_proposals.map((item) => {\n useCache(\n () =>\n Near.asyncView(\"forum.potlock.near\", \"get_proposal\", {\n proposal_id: parseInt(item),\n }).then((proposal) => {\n setLinkedProposals([\n ...linkedProposals,\n {\n label: \"# \" + proposal.id + \" : \" + proposal.snapshot.name,\n value: proposal.id,\n },\n ]);\n }),\n item + \"linked_proposals\",\n { subscribe: false }\n );\n });\n }\n}, [editProposalData]);\nconst InputContainer = ({ heading, description, children }) => {\n return (\n <div className=\"d-flex flex-column gap-1 gap-sm-2 w-100\">\n <b className=\"h6 mb-0\">{heading}</b>\n {description && (\n <div className=\"text-muted w-100 text-sm\">{description}</div>\n )}\n {children}\n </div>\n );\n};\n// show proposal created after txn approval for popup wallet\nuseEffect(() => {\n if (isTxnCreated) {\n if (editProposalData) {\n setOldProposalData(editProposalData);\n if (\n editProposalData &&\n typeof editProposalData === \"object\" &&\n oldProposalData &&\n typeof oldProposalData === \"object\" &&\n JSON.stringify(editProposalData) !== JSON.stringify(oldProposalData)\n ) {\n setCreateTxn(false);\n setProposalId(editProposalData.id);\n setShowProposalViewModal(true);\n }\n } else {\n const proposalIds = Near.view(\n \"forum.potlock.near\",\n \"get_all_proposal_ids\"\n );\n if (Array.isArray(proposalIds) && !proposalIdsArray) {\n setProposalIdsArray(proposalIds);\n }\n if (\n Array.isArray(proposalIds) &&\n Array.isArray(proposalIdsArray) &&\n proposalIds.length !== proposalIdsArray.length\n ) {\n setCreateTxn(false);\n setProposalId(proposalIds[proposalIds.length - 1]);\n setShowProposalViewModal(true);\n }\n }\n }\n setLoading(false);\n});\nuseEffect(() => {\n if (props.transactionHashes) {\n setLoading(true);\n useCache(\n () =>\n asyncFetch(\"https://rpc.mainnet.near.org\", {\n method: \"POST\",\n headers: {\n \"content-type\": \"application/json\",\n },\n body: JSON.stringify({\n jsonrpc: \"2.0\",\n id: \"dontcare\",\n method: \"tx\",\n params: [props.transactionHashes, context.accountId],\n }),\n }).then((transaction) => {\n const transaction_method_name =\n transaction?.body?.result?.transaction?.actions[0].FunctionCall\n .method_name;\n const is_edit_or_add_post_transaction =\n transaction_method_name == \"add_proposal\" ||\n transaction_method_name == \"edit_proposal\";\n if (is_edit_or_add_post_transaction) {\n setShowProposalViewModal(true);\n Storage.privateSet(draftKey, null);\n }\n // show the latest created proposal to user\n if (transaction_method_name == \"add_proposal\") {\n useCache(\n () =>\n Near.asyncView(\n \"forum.potlock.near\",\n \"get_all_proposal_ids\"\n ).then((proposalIdsArray) => {\n setProposalId(\n proposalIdsArray?.[proposalIdsArray?.length - 1]\n );\n }),\n props.transactionHashes + \"proposalIds\",\n { subscribe: false }\n );\n } else {\n setProposalId(id);\n }\n setLoading(false);\n }),\n props.transactionHashes + context.accountId,\n { subscribe: false }\n );\n } else {\n if (showProposalViewModal) {\n setShowProposalViewModal(false);\n }\n }\n}, [props.transactionHashes]);\nconst DropdowntBtnContainer = styled.div`\n font-size: 13px;\n min-width: 150px;\n .custom-select {\n position: relative;\n }\n .select-header {\n display: flex;\n justify-content: space-between;\n align-items: center;\n border: 1px solid #ccc;\n border-radius-top: 5px;\n cursor: pointer;\n background-color: #fff;\n border-radius: 5px;\n }\n .no-border {\n border: none !important;\n }\n .options-card {\n position: absolute;\n top: 100%;\n left: 0;\n width: 200%;\n border: 1px solid #ccc;\n background-color: #fff;\n padding: 0.5rem;\n z-index: 99;\n font-size: 13px;\n border-radius:0.375rem !important;\n }\n .left {\n right: 0 !important;\n left: auto !important;\n }\n @media screen and (max-width: 768px) {\n .options-card {\n right: 0 !important;\n left: auto !important;\n }\n }\n .option {\n margin-block: 5px;\n padding: 10px;\n cursor: pointer;\n border-bottom: 1px solid #f0f0f0;\n transition: background-color 0.3s ease;\n border-radius: 0.375rem !important;\n }\n .option:hover {\n background-color: #f0f0f0; /* Custom hover effect color */\n }\n .option:last-child {\n border-bottom: none;\n }\n .selected {\n background-color: #f0f0f0;\n }\n .disabled {\n background-color: #f4f4f4 !important;\n cursor: not-allowed !important;\n font-weight: 500;\n color: #b3b3b3;\n }\n .disabled .circle {\n opacity: 0.5;\n }\n .circle {\n width: 8px;\n height: 8px;\n border-radius: 50%;\n }\n .grey {\n background-color: #818181;\n }\n .green {\n background-color: #04a46e;\n }\n a:hover {\n text-decoration: none;\n }\n}\n`;\nconst LoadingButtonSpinner = (\n <span\n class=\"submit-proposal-draft-loading-indicator spinner-border spinner-border-sm\"\n role=\"status\"\n aria-hidden=\"true\"\n ></span>\n);\nconst SubmitBtn = () => {\n const btnOptions = [\n {\n iconColor: \"grey\",\n label: \"Submit Draft\",\n description:\n \"The author can still edit the proposal and build consensus before sharing it with sponsors.\",\n value: \"draft\",\n },\n {\n iconColor: \"green\",\n label: \"Ready for Review\",\n description:\n \"Start the official review process with sponsors. This will lock the editing function, but comments are still open.\",\n value: \"review\",\n },\n ];\n const handleOptionClick = (option) => {\n setDraftBtnOpen(false);\n setSelectedStatus(option.value);\n handleSubmit(option.value);\n };\n const toggleDropdown = () => {\n setDraftBtnOpen(!isDraftBtnOpen);\n };\n const handleSubmit = (status) => {\n const isDraft = status === \"draft\";\n if (isDraft) {\n onSubmit({ isDraft });\n cleanDraft();\n } else {\n setReviewModal(true);\n }\n };\n const selectedOption = btnOptions.find((i) => i.value === selectedStatus);\n return (\n <DropdowntBtnContainer>\n <div\n className=\"custom-select\"\n tabIndex=\"0\"\n onBlur={() => setDraftBtnOpen(false)}\n >\n <div\n className={\n \"select-header d-flex gap-1 align-items-center submit-draft-button \" +\n (disabledSubmitBtn && \"disabled\")\n }\n >\n <div\n onClick={() => !disabledSubmitBtn && handleSubmit(selectedStatus)}\n className=\"p-2 d-flex gap-2 align-items-center \"\n >\n {isTxnCreated ? (\n LoadingButtonSpinner\n ) : (\n <div className={\"circle \" + selectedOption.iconColor}></div>\n )}\n <div className={`selected-option`}>{selectedOption.label}</div>\n </div>\n <div\n className=\"h-100 p-2\"\n style={{ borderLeft: \"1px solid #ccc\" }}\n onClick={!disabledSubmitBtn && toggleDropdown}\n >\n <i class={`bi bi-chevron-${isDraftBtnOpen ? \"up\" : \"down\"}`}></i>\n </div>\n </div>\n {isDraftBtnOpen && (\n <div className=\"options-card\">\n {btnOptions.map((option) => (\n <div\n key={option.value}\n className={`option ${\n selectedOption.value === option.value ? \"selected\" : \"\"\n }`}\n onClick={() => handleOptionClick(option)}\n >\n <div className={`d-flex gap-2 align-items-center`}>\n <div className={\"circle \" + option.iconColor}></div>\n <div className=\"fw-bold\">{option.label}</div>\n </div>\n <div className=\"text-muted text-xs\">{option.description}</div>\n </div>\n ))}\n </div>\n )}\n </div>\n </DropdowntBtnContainer>\n );\n};\nconst onSubmit = ({ isDraft, isCancel }) => {\n setCreateTxn(true);\n console.log(\"submitting transaction\");\n const linkedProposalsIds = linkedProposals.map((item) => item.value) ?? [];\n const body = {\n proposal_body_version: \"V1\",\n linked_rfp: linkedRfp?.value,\n category: \"AI PGF\",\n name: title,\n description: description,\n summary: summary,\n linked_proposals: linkedProposalsIds,\n requested_sponsorship_usd_amount: requestedSponsorshipAmount,\n requested_sponsorship_paid_in_currency: requestedSponsorshipToken.value,\n receiver_account: receiverAccount,\n requested_sponsor: \"impact.sputnik-dao.near\",\n supervisor: supervisor,\n timeline: isCancel\n ? {\n status: \"CANCELLED\",\n sponsor_requested_review: false,\n reviewer_completed_attestation: false,\n }\n : isDraft\n ? { status: \"DRAFT\" }\n : {\n status: \"REVIEW\",\n sponsor_requested_review: false,\n reviewer_completed_attestation: false,\n },\n };\n const args = {\n labels: linkedRfp ? [] : (labels ?? []).map((i) => i.value ?? i),\n body: body,\n };\n if (isEditPage) {\n args[\"id\"] = editProposalData.id;\n }\n Near.call([\n {\n contractName: \"forum.potlock.near\",\n methodName: isEditPage ? \"edit_proposal\" : \"add_proposal\",\n args: args,\n gas: 270000000000000,\n deposit: \"100000000000000000000000\",\n },\n ]);\n};\nfunction cleanDraft() {\n Storage.privateSet(draftKey, null);\n}\nif (loading) {\n return (\n <div\n style={{ height: \"50vh\" }}\n className=\"d-flex justify-content-center align-items-center w-100\"\n >\n <Widget src={`devhub.near/widget/devhub.components.molecule.Spinner`} />\n </div>\n );\n}\nconst [collapseState, setCollapseState] = useState({});\nconst CollapsibleContainer = ({ title, children, noPaddingTop }) => {\n return (\n <div\n className={\n \"border-bottom py-4 \" +\n (noPaddingTop && \"pt-0 \") +\n (collapseState[title] && \" pb-0\")\n }\n >\n <div className={\"d-flex justify-content-between \"}>\n <div className=\"h5 text-muted mb-2 mb-sm-3\">{title}</div>\n <div\n className=\"d-flex d-sm-none cursor-pointer\"\n onClick={() =>\n setCollapseState((prevState) => ({\n ...prevState,\n [title]: !prevState[title],\n }))\n }\n >\n {!collapseState[title] ? (\n <i class=\"bi bi-chevron-up h4\"></i>\n ) : (\n <i class=\"bi bi-chevron-down h4\"></i>\n )}\n </div>\n </div>\n <div className={!collapseState[title] ? \"\" : \"d-none\"}>{children}</div>\n </div>\n );\n};\nconst CategoryDropdown = useMemo(() => {\n return (\n <Widget\n src={`bos.forum.potlock.near/widget/components.molecule.MultiSelectCategoryDropdown`}\n props={{\n selected: labels,\n onChange: (v) => setLabels(v),\n disabled: linkedRfp, // when RFP is linked, labels are disabled\n linkedRfp: linkedRfp,\n availableOptions: rfpLabelOptions,\n }}\n />\n );\n}, [draftProposalData, linkedRfp, labels]);\nconst TitleComponent = useMemo(() => {\n return (\n <Widget\n src={`devhub.near/widget/devhub.components.molecule.Input`}\n props={{\n className: \"flex-grow-1\",\n value: title,\n onBlur: (e) => {\n setTitle(e.target.value);\n },\n skipPaddingGap: true,\n inputProps: {\n max: 80,\n },\n }}\n />\n );\n}, [draftProposalData]);\nconst SummaryComponent = useMemo(() => {\n return (\n <Widget\n src={`devhub.near/widget/devhub.components.molecule.Input`}\n props={{\n className: \"flex-grow-1\",\n value: summary,\n multiline: true,\n onBlur: (e) => {\n setSummary(e.target.value);\n },\n skipPaddingGap: true,\n inputProps: {\n max: 500,\n },\n }}\n />\n );\n}, [draftProposalData]);\nconst DescriptionComponent = useMemo(() => {\n return (\n <Widget\n src={`bos.forum.potlock.near/widget/components.molecule.Compose`}\n props={{\n data: description,\n onChange: setDescription,\n autocompleteEnabled: true,\n autoFocus: false,\n showProposalIdAutoComplete: true,\n }}\n />\n );\n}, [draftProposalData]);\nconst ConsentComponent = useMemo(() => {\n return (\n <div className=\"d-flex flex-column gap-2\">\n <Widget\n src={`devhub.near/widget/devhub.components.molecule.Checkbox`}\n props={{\n value: \"toc\",\n label: (\n <>\n I’ve agree to{\" \"}\n <a\n href={ToCDocs}\n className=\"text-decoration-underline\"\n target=\"_blank\"\n rel=\"noopener noreferrer\"\n >\n AI PGF's Terms and Conditions\n </a>\n and commit to honoring it\n </>\n ),\n isChecked: consent.toc,\n onClick: (value) =>\n setConsent((prevConsent) => ({\n ...prevConsent,\n toc: value,\n })),\n }}\n />\n <Widget\n src={`devhub.near/widget/devhub.components.molecule.Checkbox`}\n props={{\n value: \"coc\",\n label: (\n <>\n I’ve read{\" \"}\n <a\n href={CoCDocs}\n className=\"text-decoration-underline\"\n target=\"_blank\"\n rel=\"noopener noreferrer\"\n >\n AI PGF's Code of Conduct\n </a>\n and commit to honoring it\n </>\n ),\n isChecked: consent.coc,\n onClick: (value) =>\n setConsent((prevConsent) => ({\n ...prevConsent,\n coc: value,\n })),\n }}\n />\n </div>\n );\n}, [draftProposalData]);\nconst ProfileComponent = useMemo(() => {\n return (\n <Widget\n src=\"mob.near/widget/Profile.ShortInlineBlock\"\n props={{\n accountId: author,\n }}\n />\n );\n}, []);\nconst LinkRFPComponent = useMemo(() => {\n return (\n <div className=\"d-flex flex-column gap-1\">\n <Widget\n src={`bos.forum.potlock.near/widget/components.molecule.LinkedRfpDropdown`}\n props={{\n onChange: setLinkedRfp,\n linkedRfp: linkedRfp,\n disabled: disabledLinkRFP,\n onDeleteRfp: () => setLabels([]),\n }}\n />\n </div>\n );\n}, [draftProposalData, disabledLinkRFP]);\nconst LinkedProposalsComponent = useMemo(() => {\n return (\n <div className=\"d-flex flex-column gap-1\">\n <div className=\"text-muted w-100 text-sm\">\n Link any relevant proposals (e.g. previous milestones).\n </div>\n <Widget\n src={`bos.forum.potlock.near/widget/components.molecule.LinkedProposalsDropdown`}\n props={{\n onChange: setLinkedProposals,\n linkedProposals: linkedProposals,\n }}\n />\n </div>\n );\n}, [draftProposalData]);\nconst ReceiverAccountComponent = useMemo(() => {\n return (\n <Widget\n src={`devhub.near/widget/devhub.entity.proposal.AccountInput`}\n props={{\n value: receiverAccount,\n placeholder: \"Enter Address\",\n onUpdate: setReceiverAccount,\n }}\n />\n );\n}, [draftProposalData]);\nconst AmountComponent = useMemo(() => {\n return (\n <Widget\n src={`devhub.near/widget/devhub.components.molecule.Input`}\n props={{\n className: \"flex-grow-1\",\n value: requestedSponsorshipAmount,\n onChange: (e) => {\n setRequestedSponsorshipAmount(e.target.value);\n },\n skipPaddingGap: true,\n inputProps: {\n type: \"text\",\n prefix: \"$\",\n inputmode: \"numeric\",\n pattern: \"[0-9]*\",\n },\n }}\n />\n );\n}, [draftProposalData]);\nconst CurrencyComponent = useMemo(() => {\n return (\n <Widget\n src={`devhub.near/widget/devhub.components.molecule.DropDown`}\n props={{\n options: tokensOptions,\n selectedValue: requestedSponsorshipToken,\n onUpdate: (v) => {\n setRequestedSponsorshipToken(v);\n },\n }}\n />\n );\n}, [draftProposalData]);\nreturn (\n <Container className=\"w-100 py-4 px-0 px-sm-2 d-flex flex-column gap-3\">\n <Heading className=\"px-2 px-sm-0\">\n {isEditPage ? \"Edit\" : \"Create\"} Proposal\n </Heading>\n <Widget\n src={`bos.forum.potlock.near/widget/components.proposals.ViewProposalModal`}\n props={{\n isOpen: showProposalViewModal,\n isEdit: isEditPage,\n proposalId: proposalId,\n }}\n />\n <Widget\n src={`devhub.near/widget/devhub.entity.proposal.ConfirmReviewModal`}\n props={{\n isOpen: isReviewModalOpen,\n onCancelClick: () => setReviewModal(false),\n onReviewClick: () => {\n setReviewModal(false);\n cleanDraft();\n onSubmit({ isDraft: false });\n },\n }}\n />\n <Widget\n src={`devhub.near/widget/devhub.entity.proposal.ConfirmCancelModal`}\n props={{\n isOpen: isCancelModalOpen,\n onCancelClick: () => setCancelModal(false),\n onConfirmClick: () => {\n setCancelModal(false);\n onSubmit({ isCancel: true });\n },\n }}\n />\n <div className=\"card no-border rounded-0 px-2 p-lg-0 full-width-div\">\n <div className=\"container-xl py-4 d-flex flex-wrap gap-6 w-100\">\n <div\n style={{ minWidth: \"350px\" }}\n className=\"flex-2 w-100 order-2 order-md-1\"\n >\n <div className=\"d-flex gap-2 w-100\">\n <div className=\"d-none d-sm-flex\" style={{ height: \"max-content\" }}>\n <Widget\n src={`bos.forum.potlock.near/widget/components.molecule.Profile`}\n props={{\n accountId: author,\n }}\n />\n </div>\n <div className=\"d-flex flex-column gap-4 w-100\">\n <div className=\"border-bottom pb-4\">\n <InputContainer\n heading=\"Link RFP (Optional)\"\n description={\n \"Link this proposal if it is a response to a specific RFP. You can only link to active RFPs in the “Accepting Submission” stage. You can only link to one RFP.\"\n }\n >\n {LinkRFPComponent}\n </InputContainer>\n </div>\n <InputContainer\n heading=\"Category\"\n description={\n <>\n Select the relevant categories that best align with your\n contribution to the NEAR developer community.\n </>\n }\n >\n {CategoryDropdown}\n </InputContainer>\n <InputContainer\n heading=\"Title\"\n description=\"Highlight the essence of your proposal in a few words. This will appear on your proposal’s detail page and the main proposal feed. Keep it short, please :)\"\n >\n {TitleComponent}\n </InputContainer>\n <InputContainer\n heading=\"Summary\"\n description=\"Explain your proposal briefly. This is your chance to make a good first impression on the community. Include what needs or goals your work will address, your solution, and the benefit for the NEAR developer community.\"\n >\n {SummaryComponent}\n </InputContainer>\n <InputContainer\n heading=\"Description\"\n description={\n <>\n Expand on your summary with any relevant details like your\n contribution timeline, key milestones, team background, and\n a clear breakdown of how the funds will be used. Proposals\n should be simple and clear (e.g. 1 month). For more complex\n projects, treat each milestone as a separate proposal.\n </>\n }\n >\n {DescriptionComponent}\n </InputContainer>\n <InputContainer heading=\"Final Consent\">\n {ConsentComponent}\n </InputContainer>\n <div className=\"d-flex justify-content-between gap-2 align-items-center\">\n <div>\n {isEditPage && (\n <Widget\n src={`devhub.near/widget/devhub.components.molecule.Button`}\n props={{\n classNames: {\n root: \"btn-outline-danger shadow-none border-0 btn-sm\",\n },\n label: (\n <div className=\"d-flex align-items-center gap-1\">\n <i class=\"bi bi-trash3\"></i> Cancel Proposal\n </div>\n ),\n onClick: () => setCancelModal(true),\n }}\n />\n )}\n </div>\n <div\n className=\"d-flex gap-2\"\n style={{\n height: isDraftBtnOpen ? \"25vh\" : \"auto\",\n alignItems: isDraftBtnOpen ? \"flex-start\" : \"center\",\n }}\n >\n <Link\n to={\n isEditPage\n ? href({\n widgetSrc: `bos.forum.potlock.near/widget/app`,\n params: {\n page: \"proposal\",\n id: parseInt(id),\n },\n })\n : href({\n widgetSrc: `bos.forum.potlock.near/widget/app`,\n params: {\n page: \"proposals\",\n },\n })\n }\n >\n <Widget\n src={`devhub.near/widget/devhub.components.molecule.Button`}\n props={{\n classNames: {\n root: \"d-flex h-100 text-muted fw-bold btn-outline shadow-none border-0 btn-sm\",\n },\n label: \"Discard Changes\",\n onClick: cleanDraft,\n }}\n />\n </Link>\n <SubmitBtn />\n </div>\n </div>\n </div>\n </div>\n </div>\n <div\n style={{ minWidth: \"350px\" }}\n className=\"flex-1 w-100 order-1 order-md-2\"\n >\n <CollapsibleContainer noPaddingTop={true} title=\"Author Details\">\n <div className=\"d-flex flex-column gap-3 gap-sm-4\">\n <InputContainer heading=\"Author\">\n {ProfileComponent}\n </InputContainer>\n </div>\n </CollapsibleContainer>\n <div className=\"my-2\">\n <CollapsibleContainer title=\"Link Proposals (Optional)\">\n {LinkedProposalsComponent}\n </CollapsibleContainer>\n </div>\n <div className=\"my-2\">\n <CollapsibleContainer title=\"Funding Details\">\n <div className=\"d-flex flex-column gap-3 gap-sm-4\">\n <InputContainer\n heading=\"Recipient NEAR Wallet Address\"\n description=\"Enter the address that will receive the funds. We’ll need this to send a test transaction once your proposal is approved.\"\n >\n {ReceiverAccountComponent}\n </InputContainer>\n <InputContainer\n heading={\n <div className=\"d-flex gap-2 align-items-center\">\n Recipient Verification Status\n <div className=\"custom-tooltip\">\n <i class=\"bi bi-info-circle-fill\"></i>\n <span class=\"tooltiptext\">\n To get approved and receive payments on our platform,\n you must complete KYC/KYB verification using Fractal,\n a trusted identity verification solution. This helps\n others trust transactions with your account. Click\n \"Get Verified\" to start. <br />\n <br />\n Once verified, your profile will display a badge,\n which is valid for 365 days from the date of your\n verification. You must renew your verification upon\n expiration OR if any of your personal information\n changes.\n </span>\n </div>\n </div>\n }\n description=\"\"\n >\n <div className=\"border border-1 p-3 rounded-2\">\n <Widget\n src={`devhub.near/widget/devhub.entity.proposal.VerificationStatus`}\n props={{\n receiverAccount: receiverAccount,\n showGetVerifiedBtn: true,\n imageSize: 30,\n }}\n />\n </div>\n </InputContainer>\n <InputContainer\n heading=\"Total Amount (USD)\"\n description={<>Enter the exact amount you are seeking.</>}\n >\n {AmountComponent}\n </InputContainer>\n <InputContainer\n heading=\"Currency\"\n description=\"Select your preferred currency for receiving funds. Note: The exchange rate for NEAR tokens will be the closing rate at the day of the invoice.\"\n >\n {CurrencyComponent}\n </InputContainer>\n </div>\n </CollapsibleContainer>\n </div>\n </div>\n </div>\n </div>\n </Container>\n);\n" }, "components.molecule.Profile": { "": "const accountId = props.accountId;\nconst size = props.size ?? \"md\";\nconst showAccountId = props.showAccountId;\nconst Avatar = styled.div`\n &.sm {\n min-width: 30px;\n max-width: 30px;\n min-height: 30px;\n max-height: 30px;\n }\n &.md {\n min-width: 40px;\n max-width: 40px;\n min-height: 40px;\n max-height: 40px;\n }\n pointer-events: none;\n flex-shrink: 0;\n border: 1px solid #eceef0;\n overflow: hidden;\n border-radius: 40px;\n transition: border-color 200ms;\n img {\n object-fit: cover;\n width: 100%;\n height: 100%;\n margin: 0 !important;\n }\n`;\nconst profile = Social.get(`${accountId}/profile/**`, \"final\");\nconst profileUrl = `https://bos.potlock.org/?tab=profile&accountId=${accountId}`;\nreturn (\n <a rel=\"noopener noreferrer\" target=\"_blank\" href={profileUrl}>\n <div className=\"d-flex gap-2 align-items-center\">\n <Avatar className={size}>\n <Widget\n src=\"mob.near/widget/Image\"\n props={{\n image: profile.image,\n alt: profile.name,\n fallbackUrl:\n \"https://ipfs.near.social/ipfs/bafkreibiyqabm3kl24gcb2oegb7pmwdi6wwrpui62iwb44l7uomnn3lhbi\",\n }}\n />\n </Avatar>\n {showAccountId && (\n <div>\n {(accountId ?? \"\").substring(0, 20)}\n {(accountId ?? \"\").length > 20 ? \"...\" : \"\"}\n </div>\n )}\n </div>\n </a>\n);\n" }, "components.rfps.Editor": { "": "const { RFP_TIMELINE_STATUS, parseJSON } = VM.require(\n `bos.forum.potlock.near/widget/core.common`\n) || { RFP_TIMELINE_STATUS: {}, parseJSON: () => {} };\nconst { href } = VM.require(`devhub.near/widget/core.lib.url`);\nconst draftKey = \"AI_PGF_RFP_EDIT\";\nhref || (href = () => {});\nconst { getGlobalLabels } = VM.require(\n `bos.forum.potlock.near/widget/components.core.lib.contract`\n) || { getGlobalLabels: () => {} };\nconst { id, timestamp } = props;\nconst isEditPage = typeof id === \"string\";\nconst author = context.accountId;\nconst ToCDocs = \"https://aipgf.com/terms\";\nconst CoCDocs = \"https://aipgf.com/conduct\";\nconst rfpLabelOptions = getGlobalLabels();\nconst isAllowedToWriteRfp = Near.view(\n \"forum.potlock.near\",\n \"is_allowed_to_write_rfps\",\n {\n editor: context.accountId,\n }\n);\nif (!author || !isAllowedToWriteRfp) {\n return (\n <Widget\n src={`bos.forum.potlock.near/widget/components.molecule.LoginScreen`}\n />\n );\n}\nlet editRfpData = null;\nlet draftRfpData = null;\nif (isEditPage) {\n editRfpData = Near.view(`forum.potlock.near`, \"get_rfp\", {\n rfp_id: parseInt(id),\n });\n}\nconst Container = styled.div`\n input {\n font-size: 14px !important;\n }\n .card.no-border {\n border-left: none !important;\n border-right: none !important;\n margin-bottom: -3.5rem;\n }\n textarea {\n font-size: 14px !important;\n }\n .full-width-div {\n width: 100vw;\n position: relative;\n left: 50%;\n right: 50%;\n margin-left: -50vw;\n margin-right: -50vw;\n }\n .text-sm {\n font-size: 13px;\n }\n .h5 {\n font-size: 18px !important;\n }\n @media screen and (max-width: 768px) {\n .h6 {\n font-size: 14px !important;\n }\n .h5 {\n font-size: 16px !important;\n }\n .text-sm {\n font-size: 11px;\n }\n .gap-6 {\n gap: 0.5rem !important;\n }\n }\n .border-bottom {\n border-bottom: var(--bs-card-border-width) solid var(--bs-card-border-color);\n }\n .text-xs {\n font-size: 10px;\n }\n .flex-2 {\n flex: 2;\n }\n .flex-1 {\n flex: 1;\n }\n .bg-grey {\n background-color: #f4f4f4;\n }\n .border-bottom {\n border-bottom: 1px solid grey;\n }\n .cursor-pointer {\n cursor: pointer;\n }\n .border-1 {\n border: 1px solid #e2e6ec;\n }\n .black-btn {\n background-color: #000 !important;\n border: none;\n color: white;\n &:active {\n color: white;\n }\n }\n .dropdown-toggle:after {\n position: absolute;\n top: 46%;\n right: 5%;\n }\n .drop-btn {\n max-width: none !important;\n }\n .dropdown-menu {\n width: 100%;\n border-radius: 0.375rem !important;\n }\n .input-icon {\n display: flex;\n height: 100%;\n align-items: center;\n border-right: 1px solid #dee2e6;\n padding-right: 10px;\n }\n /* Tooltip container */\n .custom-tooltip {\n position: relative;\n display: inline-block;\n }\n /* Tooltip text */\n .custom-tooltip .tooltiptext {\n visibility: hidden;\n width: 250px;\n background-color: #fff;\n color: #6c757d;\n text-align: center;\n padding: 10px;\n border-radius: 6px;\n font-size: 12px;\n border: 0.2px solid #6c757d;\n /* Position the tooltip text */\n position: absolute;\n z-index: 1;\n bottom: 125%;\n left: -30px;\n /* Fade in tooltip */\n opacity: 0;\n transition: opacity 0.3s;\n }\n /* Tooltip arrow */\n .custom-tooltip .tooltiptext::after {\n content: \"\";\n position: absolute;\n top: 100%;\n left: 15%;\n margin-left: -5px;\n border-width: 5px;\n border-style: solid;\n border-color: #555 transparent transparent transparent;\n }\n /* Show the tooltip text when you mouse over the tooltip container */\n .custom-tooltip:hover .tooltiptext {\n visibility: visible;\n opacity: 1;\n }\n .form-check-input:checked {\n background-color: #04a46e !important;\n border-color: #04a46e !important;\n }\n .gap-6 {\n gap: 2.5rem;\n }\n a.no-space {\n display: inline-block;\n }\n .fw-light-bold {\n font-weight: 600 !important;\n }\n .disabled .circle {\n opacity: 0.5;\n }\n .circle {\n width: 6px;\n height: 6px;\n border-radius: 50%;\n }\n .grey {\n background-color: #818181;\n }\n @media screen and (max-width: 970px) {\n .gap-6 {\n gap: 1.5rem !important;\n }\n }\n @media screen and (max-width: 570px) {\n .gap-6 {\n gap: 0.5rem !important;\n }\n }\n`;\nconst Heading = styled.div`\n font-size: 24px;\n font-weight: 700;\n @media screen and (max-width: 768px) {\n font-size: 18px;\n }\n`;\nfunction getTimestamp(date) {\n // in nanoseconds\n const parsedDate = date ? new Date(date) : new Date();\n return Math.floor(parsedDate.getTime() * 1000000).toString();\n}\nfunction getDate(timestamp) {\n const stamp =\n !timestamp || timestamp === \"0\" || timestamp === \"NaN\" ? null : timestamp;\n return new Date(parseFloat(stamp / 1000000)).toISOString().split(\"T\")[0];\n}\nconst [labels, setLabels] = useState([]);\nconst [title, setTitle] = useState(null);\nconst [description, setDescription] = useState(null);\nconst [summary, setSummary] = useState(null);\nconst [consent, setConsent] = useState({ toc: false, coc: false });\nconst [submissionDeadline, setSubmissionDeadline] = useState(null);\nconst [allowDraft, setAllowDraft] = useState(true);\nconst [loading, setLoading] = useState(true);\nconst [disabledSubmitBtn, setDisabledSubmitBtn] = useState(false);\nconst [isDraftBtnOpen, setDraftBtnOpen] = useState(false);\nconst [showRfpViewModal, setShowRfpViewModal] = useState(false); // when user creates/edit a RFP and confirm the txn, this is true\nconst [rfpId, setRfpId] = useState(null);\nconst [rfpIdsArray, setRfpIdsArray] = useState(null);\nconst [isTxnCreated, setCreateTxn] = useState(false);\nconst [oldRfpData, setOldRfpData] = useState(null);\nconst [timeline, setTimeline] = useState({\n status: RFP_TIMELINE_STATUS.ACCEPTING_SUBMISSIONS,\n});\nif (allowDraft) {\n draftRfpData = Storage.privateGet(draftKey);\n}\nconst memoizedDraftData = useMemo(\n () => ({\n id: editRfpData.id ?? null,\n snapshot: {\n name: title,\n description: description,\n labels: labels,\n summary: summary,\n submission_deadline: getTimestamp(submissionDeadline),\n },\n }),\n [title, summary, description, submissionDeadline, labels]\n);\nuseEffect(() => {\n if (allowDraft) {\n let data = editRfpData || JSON.parse(draftRfpData);\n let snapshot = data.snapshot;\n if (data) {\n if (timestamp) {\n snapshot =\n data.snapshot_history.find((item) => item.timestamp === timestamp) ??\n data.snapshot;\n }\n if (\n draftRfpData &&\n editRfpData &&\n editRfpData.id === JSON.parse(draftRfpData).id\n ) {\n snapshot = {\n ...editRfpData.snapshot,\n ...JSON.parse(draftRfpData).snapshot,\n };\n }\n setRfpId(data.id);\n setLabels(snapshot.labels);\n setTitle(snapshot.name);\n setSummary(snapshot.summary);\n setDescription(snapshot.description);\n setSubmissionDeadline(getDate(snapshot.submission_deadline));\n setTimeline(parseJSON(snapshot.timeline));\n if (isEditPage) {\n setConsent({ toc: true, coc: true });\n }\n }\n }\n}, [editRfpData, draftRfpData, allowDraft]);\n// show loader until LS data is set in state\nuseEffect(() => {\n const handler = setTimeout(() => {\n setAllowDraft(false);\n setLoading(false);\n }, 200);\n return () => clearTimeout(handler);\n}, []);\nuseEffect(() => {\n if (showRfpViewModal) {\n return;\n }\n setDisabledSubmitBtn(\n !title ||\n !description ||\n !summary ||\n !(labels ?? []).length ||\n !submissionDeadline ||\n !consent.toc ||\n !consent.coc\n );\n const handler = setTimeout(() => {\n Storage.privateSet(draftKey, JSON.stringify(memoizedDraftData));\n }, 10000);\n return () => clearTimeout(handler);\n}, [\n memoizedDraftData,\n draftKey,\n draftRfpData,\n consent,\n isTxnCreated,\n showRfpViewModal,\n]);\nconst InputContainer = ({ heading, description, children }) => {\n return (\n <div className=\"d-flex flex-column gap-1 gap-sm-2 w-100\">\n <b className=\"h6 mb-0\">{heading}</b>\n {description && (\n <div className=\"text-muted w-100 text-sm\">{description}</div>\n )}\n {children}\n </div>\n );\n};\n// show RFP created after txn approval for popup wallet\nuseEffect(() => {\n if (isTxnCreated) {\n if (editRfpData) {\n setOldRfpData(editRfpData);\n if (\n editRfpData &&\n typeof editRfpData === \"object\" &&\n oldRfpData &&\n typeof oldRfpData === \"object\" &&\n JSON.stringify(editRfpData) !== JSON.stringify(oldRfpData)\n ) {\n setCreateTxn(false);\n setRfpId(editRfpData.id);\n setShowRfpViewModal(true);\n }\n } else {\n const rfpIds = Near.view(\"forum.potlock.near\", \"get_all_rfp_ids\");\n if (Array.isArray(rfpIds) && !rfpIdsArray) {\n setRfpIdsArray(rfpIds);\n }\n if (\n Array.isArray(rfpIds) &&\n Array.isArray(rfpIdsArray) &&\n rfpIds.length !== rfpIdsArray.length\n ) {\n setCreateTxn(false);\n setRfpId(rfpIds[rfpIds.length - 1]);\n setShowRfpViewModal(true);\n }\n }\n }\n});\nuseEffect(() => {\n if (props.transactionHashes) {\n setLoading(true);\n useCache(\n () =>\n asyncFetch(\"https://rpc.mainnet.near.org\", {\n method: \"POST\",\n headers: {\n \"content-type\": \"application/json\",\n },\n body: JSON.stringify({\n jsonrpc: \"2.0\",\n id: \"dontcare\",\n method: \"tx\",\n params: [props.transactionHashes, context.accountId],\n }),\n }).then((transaction) => {\n const transaction_method_name =\n transaction?.body?.result?.transaction?.actions[0].FunctionCall\n .method_name;\n const is_edit_or_add_rfp_transaction =\n transaction_method_name == \"add_rfp\" ||\n transaction_method_name == \"edit_rfp\";\n if (is_edit_or_add_rfp_transaction) {\n setShowRfpViewModal(true);\n Storage.privateSet(draftKey, null);\n }\n // show the latest created rfp to user\n if (transaction_method_name == \"add_rfp\") {\n useCache(\n () =>\n Near.asyncView(\"forum.potlock.near\", \"get_all_rfp_ids\").then(\n (rfpIdsArray) => {\n setRfpId(rfpIdsArray?.[rfpIdsArray?.length - 1]);\n }\n ),\n props.transactionHashes + \"rfpIds\",\n { subscribe: false }\n );\n } else {\n setRfpId(id);\n }\n setLoading(false);\n }),\n props.transactionHashes + context.accountId,\n { subscribe: false }\n );\n } else {\n if (showRfpViewModal) {\n setShowRfpViewModal(false);\n }\n }\n}, [props.transactionHashes]);\nconst LoadingButtonSpinner = (\n <span\n className=\"submit-rfp-loading-indicator spinner-border spinner-border-sm\"\n role=\"status\"\n aria-hidden=\"true\"\n ></span>\n);\nconst onSubmit = () => {\n setCreateTxn(true);\n const body = {\n rfp_body_version: \"V0\",\n name: title,\n description: description,\n summary: summary,\n submission_deadline: getTimestamp(submissionDeadline),\n timeline: timeline,\n };\n const args = { labels: (labels ?? []).map((i) => i.value), body: body };\n if (isEditPage) {\n args[\"id\"] = editRfpData.id;\n }\n Near.call([\n {\n contractName: \"forum.potlock.near\",\n methodName: isEditPage ? \"edit_rfp\" : \"add_rfp\",\n args: args,\n gas: 270000000000000,\n deposit: \"100000000000000000000000\",\n },\n ]);\n};\nfunction cleanDraft() {\n Storage.privateSet(draftKey, null);\n}\nif (loading) {\n return (\n <div\n style={{ height: \"50vh\" }}\n className=\"d-flex justify-content-center align-items-center w-100\"\n >\n <Widget src={`devhub.near/widget/devhub.components.molecule.Spinner`} />\n </div>\n );\n}\nconst [collapseState, setCollapseState] = useState({});\nconst CollapsibleContainer = ({ title, children, noPaddingTop }) => {\n return (\n <div\n className={\n \"border-bottom py-4 \" +\n (noPaddingTop && \"pt-0 \") +\n (collapseState[title] && \" pb-0\")\n }\n >\n <div className={\"d-flex justify-content-between \"}>\n <div className=\"h5 text-muted mb-2 mb-sm-3\">{title}</div>\n <div\n className=\"d-flex d-sm-none cursor-pointer\"\n onClick={() =>\n setCollapseState((prevState) => ({\n ...prevState,\n [title]: !prevState[title],\n }))\n }\n >\n {!collapseState[title] ? (\n <i className=\"bi bi-chevron-up h4\"></i>\n ) : (\n <i className=\"bi bi-chevron-down h4\"></i>\n )}\n </div>\n </div>\n <div className={!collapseState[title] ? \"\" : \"d-none\"}>{children}</div>\n </div>\n );\n};\nconst CategoryDropdown = useMemo(() => {\n return (\n <Widget\n src={`bos.forum.potlock.near/widget/components.molecule.MultiSelectCategoryDropdown`}\n props={{\n selected: labels,\n onChange: (v) => setLabels(v),\n disabled: false,\n availableOptions: rfpLabelOptions,\n }}\n />\n );\n}, [draftRfpData]);\nconst TitleComponent = useMemo(() => {\n return (\n <Widget\n src={`devhub.near/widget/devhub.components.molecule.Input`}\n props={{\n className: \"flex-grow-1\",\n value: title,\n onBlur: (e) => {\n setTitle(e.target.value);\n },\n skipPaddingGap: true,\n inputProps: {\n max: 80,\n },\n }}\n />\n );\n}, [draftRfpData]);\nconst SummaryComponent = useMemo(() => {\n return (\n <Widget\n src={`devhub.near/widget/devhub.components.molecule.Input`}\n props={{\n className: \"flex-grow-1\",\n value: summary,\n multiline: true,\n onBlur: (e) => {\n setSummary(e.target.value);\n },\n skipPaddingGap: true,\n inputProps: {\n max: 500,\n },\n }}\n />\n );\n}, [draftRfpData]);\nconst DescriptionComponent = useMemo(() => {\n return (\n <Widget\n src={`bos.forum.potlock.near/widget/components.molecule.Compose`}\n props={{\n data: description,\n onChange: setDescription,\n autocompleteEnabled: true,\n autoFocus: false,\n }}\n />\n );\n}, [draftRfpData]);\nconst ConsentComponent = useMemo(() => {\n return (\n <div className=\"d-flex flex-column gap-2\">\n <Widget\n src={`devhub.near/widget/devhub.components.molecule.Checkbox`}\n props={{\n value: \"toc\",\n label: (\n <>\n I’ve agree to{\" \"}\n <a\n href={ToCDocs}\n className=\"text-decoration-underline\"\n target=\"_blank\"\n rel=\"noopener noreferrer\"\n >\n AI PGF's Terms and Conditions\n </a>\n and commit to honoring it\n </>\n ),\n isChecked: consent.toc,\n onClick: (value) =>\n setConsent((prevConsent) => ({\n ...prevConsent,\n toc: value,\n })),\n }}\n />\n <Widget\n src={`devhub.near/widget/devhub.components.molecule.Checkbox`}\n props={{\n value: \"coc\",\n label: (\n <>\n I’ve read{\" \"}\n <a\n href={CoCDocs}\n className=\"text-decoration-underline\"\n target=\"_blank\"\n rel=\"noopener noreferrer\"\n >\n AI PGF's Code of Conduct\n </a>\n and commit to honoring it\n </>\n ),\n isChecked: consent.coc,\n onClick: (value) =>\n setConsent((prevConsent) => ({\n ...prevConsent,\n coc: value,\n })),\n }}\n />\n </div>\n );\n}, [draftRfpData]);\nconst SubmissionDeadline = useMemo(() => {\n return (\n <Widget\n src={`devhub.near/widget/devhub.components.molecule.Input`}\n props={{\n className: \"flex-grow-1\",\n value: submissionDeadline,\n onBlur: (e) => {\n setSubmissionDeadline(e.target.value);\n },\n skipPaddingGap: true,\n type: \"date\",\n inputProps: {\n required: true,\n },\n }}\n />\n );\n}, [draftRfpData]);\nreturn (\n <Container className=\"w-100 py-2 px-0 px-sm-2 d-flex flex-column gap-3\">\n <Widget\n src={`bos.forum.potlock.near/widget/components.rfps.ViewRfpModal`}\n props={{\n isOpen: showRfpViewModal,\n isEdit: isEditPage,\n rfpId: rfpId,\n }}\n />\n <Widget\n src={`bos.forum.potlock.near/widget/components.rfps.ConfirmCancelModal`}\n props={{\n isOpen: isCancelModalOpen,\n onCancelClick: () => {\n setCancelModal(false);\n setTimeline({ status: RFP_TIMELINE_STATUS.EVALUATION });\n },\n onConfirmClick: (value) => {\n setCancelModal(false);\n onCancelRFP(value);\n },\n linkedProposalIds: editRfpData.snapshot.linked_proposals,\n }}\n />\n <Widget\n src={`bos.forum.potlock.near/widget/components.rfps.WarningModal`}\n props={{\n isOpen: isWarningModalOpen,\n onConfirmClick: () => {\n setWarningModal(false);\n setTimeline({ status: RFP_TIMELINE_STATUS.EVALUATION });\n },\n }}\n />\n <Heading className=\"px-2 px-sm-0\">\n {isEditPage ? \"Edit\" : \"Create\"} RFP\n </Heading>\n <div className=\"card no-border rounded-0 px-2 p-lg-0 full-width-div\">\n <div className=\"container-xl py-4 d-flex flex-wrap gap-6 w-100\">\n <div\n style={{ minWidth: \"350px\" }}\n className=\"flex-2 w-100 order-2 order-md-1\"\n >\n <div className=\"d-flex gap-3 w-100\">\n <div className=\"d-none d-sm-flex\" style={{ height: \"max-content\" }}>\n <Widget\n src={`bos.forum.potlock.near/widget/components.molecule.Profile`}\n props={{\n accountId: author,\n }}\n />\n </div>\n <div className=\"d-flex flex-column gap-4 w-100\">\n <InputContainer\n heading=\"Category\"\n description={\n <>\n Select the relevant categories to help users quickly\n understand the nature of the need.\n </>\n }\n >\n {CategoryDropdown}\n </InputContainer>\n <InputContainer\n heading=\"Title\"\n description=\"Highlight the essence of your RFP in a few words. This will appear on your RFP’s detail page and the main RFP feed. Keep it short, please :)\"\n >\n {TitleComponent}\n </InputContainer>\n <InputContainer\n heading=\"Summary\"\n description=\"Explain your RFP briefly. What is the problem or need, desired outcome, and benefit to the NEAR developer community.\"\n >\n {SummaryComponent}\n </InputContainer>\n <InputContainer\n heading=\"Description\"\n description={\n \"Expand on your summary with any relevant details like a detailed explanation of the problem and the expected solution, scope, and deliverables. Also include an estimate range for the project if you have a specific budget. And the selection criteria.\"\n }\n >\n {DescriptionComponent}\n </InputContainer>\n <InputContainer heading=\"Final Consent\">\n {ConsentComponent}\n </InputContainer>\n <div className=\"d-flex justify-content-end gap-2 align-items-center\">\n <Link\n to={\n isEditPage\n ? href({\n widgetSrc: `bos.forum.potlock.near/widget/app`,\n params: {\n page: \"rfp\",\n id: parseInt(id),\n },\n })\n : href({\n widgetSrc: `bos.forum.potlock.near/widget/app`,\n params: {\n page: \"rfps\",\n },\n })\n }\n >\n <Widget\n src={`devhub.near/widget/devhub.components.molecule.Button`}\n props={{\n classNames: {\n root: \"d-flex h-100 text-muted fw-bold btn-outline shadow-none border-0 btn-sm\",\n },\n label: \"Discard Changes\",\n onClick: cleanDraft,\n }}\n />\n </Link>\n <Widget\n src={`devhub.near/widget/devhub.components.molecule.Button`}\n props={{\n classNames: {\n root: \"d-flex h-100 fw-light-bold btn-outline shadow-none border-1\",\n },\n label: (\n <div className=\"d-flex align-items-center gap-2\">\n <div className=\"circle grey\"></div> <div>Submit</div>\n </div>\n ),\n onClick: onSubmit,\n disabled: disabledSubmitBtn,\n }}\n />\n </div>\n </div>\n </div>\n </div>\n <div\n style={{ minWidth: \"350px\" }}\n className=\"flex-1 w-100 order-1 order-md-2\"\n >\n <CollapsibleContainer noPaddingTop={true}>\n <div className=\"d-flex flex-column gap-3 gap-sm-4\">\n <InputContainer\n heading=\"Submission Deadline\"\n description=\"Enter the deadline for submitting proposals.\"\n >\n {SubmissionDeadline}\n </InputContainer>\n </div>\n </CollapsibleContainer>\n <div className=\"my-2\">\n <CollapsibleContainer title=\"Timeline\">\n <Widget\n src={`bos.forum.potlock.near/widget/components.rfps.TimelineConfigurator`}\n props={{\n timeline: timeline,\n setTimeline: (v) => {\n if (editRfpData.snapshot.timeline.status === v.status) {\n return;\n }\n // if proposal selected timeline is selected and no approved proposals exist, show warning\n if (\n v.status === RFP_TIMELINE_STATUS.PROPOSAL_SELECTED &&\n Array.isArray(approvedProposals) &&\n !approvedProposals.length\n ) {\n setWarningModal(true);\n }\n if (v.status === RFP_TIMELINE_STATUS.CANCELLED) {\n setCancelModal(true);\n }\n setTimeline(v);\n },\n disabled: isEditPage ? false : true,\n }}\n />\n </CollapsibleContainer>\n </div>\n </div>\n </div>\n </div>\n </Container>\n);\n" }, "components.rfps.Rfp": { "": "const {\n RFP_TIMELINE_STATUS,\n fetchGraphQL,\n CANCEL_RFP_OPTIONS,\n parseJSON,\n PROPOSALS_APPROVED_STATUS_ARRAY,\n getLinkUsingCurrentGateway,\n} = VM.require(`bos.forum.potlock.near/widget/core.common`) || {\n RFP_TIMELINE_STATUS: {},\n fetchGraphQL: () => {},\n CANCEL_RFP_OPTIONS: {},\n parseJSON: () => {},\n PROPOSALS_APPROVED_STATUS_ARRAY: {},\n getLinkUsingCurrentGateway: () => {},\n};\nconst { href } = VM.require(`devhub.near/widget/core.lib.url`) || {\n href: () => {},\n};\nconst { readableDate } = VM.require(`devhub.near/widget/core.lib.common`) || {\n readableDate: () => {},\n};\nconst { getGlobalLabels } = VM.require(\n `bos.forum.potlock.near/widget/components.core.lib.contract`\n) || { getGlobalLabels: () => {} };\nconst accountId = context.accountId;\n/*\n ---props---\n props.id: number;\n props.timestamp: number; optional\n accountId: string\n blockHeight:number\n */\nconst { id, timestamp } = props;\nconst Container = styled.div`\n .full-width-div {\n width: 100vw;\n position: relative;\n left: 50%;\n right: 50%;\n margin-left: -50vw;\n margin-right: -50vw;\n }\n .fw-bold {\n font-weight: 600 !important;\n }\n .card.no-border {\n border-left: none !important;\n border-right: none !important;\n margin-bottom: -3.5rem;\n }\n .description-box {\n font-size: 14px;\n }\n .accept-submission-info-container {\n background-color: #ecf8fb;\n }\n .text-sm {\n font-size: 13px !important;\n }\n .flex-1 {\n flex: 1;\n }\n .flex-3 {\n flex: 3;\n }\n .circle {\n width: 20px;\n height: 20px;\n border-radius: 50%;\n border: 1px solid grey;\n }\n .green-fill {\n background-color: rgb(4, 164, 110) !important;\n border-color: rgb(4, 164, 110) !important;\n color: white !important;\n }\n .yellow-fill {\n border-color: #ff7a00 !important;\n }\n .vertical-line {\n width: 2px;\n height: 180px;\n background-color: lightgrey;\n }\n @media screen and (max-width: 970px) {\n .vertical-line {\n height: 135px !important;\n }\n .vertical-line-sm {\n height: 70px !important;\n }\n .gap-6 {\n gap: 0.5rem !important;\n }\n }\n @media screen and (max-width: 570px) {\n .vertical-line {\n height: 180px !important;\n }\n .vertical-line-sm {\n height: 75px !important;\n }\n .gap-6 {\n gap: 0.5rem !important;\n }\n }\n .vertical-line-sm {\n width: 2px;\n height: 70px;\n background-color: lightgrey;\n }\n .form-check-input:disabled ~ .form-check-label,\n .form-check-input[disabled] ~ .form-check-label {\n opacity: 1;\n }\n .form-check-input {\n border-color: black !important;\n }\n .grey-btn {\n background-color: #687076;\n border: none;\n color: white;\n }\n .blue-btn {\n background-color: #3c697d;\n border: none;\n color: white;\n }\n .form-check-input:checked {\n background-color: #3c697d !important;\n border-color: #3c697d !important;\n }\n .dropdown-toggle:after {\n position: absolute;\n top: 46%;\n right: 5%;\n }\n .drop-btn {\n max-width: none !important;\n }\n .dropdown-menu {\n width: 100%;\n border-radius: 0.375rem !important;\n }\n .green-btn {\n background-color: #04a46e !important;\n border: none;\n color: white;\n &:active {\n color: white;\n }\n }\n .gap-6 {\n gap: 2.5rem;\n }\n .border-vertical {\n border-top: var(--bs-border-width) var(--bs-border-style)\n var(--bs-border-color) !important;\n border-bottom: var(--bs-border-width) var(--bs-border-style)\n var(--bs-border-color) !important;\n }\n button.px-0 {\n padding-inline: 0px !important;\n }\n red-icon i {\n color: red;\n }\n input[type=\"radio\"] {\n min-width: 13px;\n }\n`;\nconst RfpContainer = styled.div`\n border: 1px solid lightgrey;\n overflow: auto;\n`;\nconst Header = styled.div`\n position: relative;\n background-color: #f4f4f4;\n height: 50px;\n .menu {\n position: absolute;\n right: 10px;\n top: 4px;\n font-size: 30px;\n }\n`;\nconst Text = styled.p`\n display: block;\n margin: 0;\n font-size: 14px;\n line-height: 20px;\n font-weight: 400;\n color: #687076;\n white-space: nowrap;\n`;\nconst Actions = styled.div`\n display: flex;\n align-items: center;\n gap: 12px;\n margin: -6px -6px 6px;\n`;\nconst Avatar = styled.div`\n width: 40px;\n height: 40px;\n pointer-events: none;\n img {\n object-fit: cover;\n border-radius: 40px;\n width: 100%;\n height: 100%;\n }\n`;\nconst rfpLabelOptions = getGlobalLabels();\nconst LinkProfile = ({ account, children }) => {\n return (\n <a\n rel=\"noopener noreferrer\"\n target=\"_blank\"\n href={`https://bos.potlock.org/?tab=profile&accountId=${account}`}\n >\n {children}\n </a>\n );\n};\nconst [snapshotHistory, setSnapshotHistory] = useState([]);\nconst rfp = Near.view(\"forum.potlock.near\", \"get_rfp\", {\n rfp_id: parseInt(id),\n});\nconst queryName = \"bos_forum_potlock_near_ai_pgf_indexer_rfp_snapshots\";\nconst query = `query GetLatestSnapshot($offset: Int = 0, $limit: Int = 10, $where: ${queryName}_bool_exp = {}) {\n ${queryName}(\n offset: $offset\n limit: $limit\n order_by: {ts: asc}\n where: $where\n ) {\n editor_id\n name\n summary\n description\n ts\n rfp_id\n timeline\n labels\n submission_deadline\n linked_proposals\n }\n}`;\nconst fetchSnapshotHistory = () => {\n const variables = {\n where: { rfp_id: { _eq: id } },\n };\n fetchGraphQL(query, \"GetLatestSnapshot\", variables).then(async (result) => {\n if (result.status === 200) {\n if (result.body.data) {\n const data = result.body.data?.[queryName];\n const history = data.map((item) => {\n const rfpData = {\n ...item,\n timestamp: item.ts,\n timeline: parseJSON(item.timeline),\n };\n delete rfpData.ts;\n return rfpData;\n });\n setSnapshotHistory(history);\n }\n }\n });\n};\nuseEffect(() => {\n fetchSnapshotHistory();\n}, [id]);\nif (!rfp) {\n return (\n <div\n style={{ height: \"50vh\" }}\n className=\"d-flex justify-content-center align-items-center w-100\"\n >\n <Widget src={`devhub.near/widget/devhub.components.molecule.Spinner`} />\n </div>\n );\n}\nif (timestamp && rfp) {\n rfp.snapshot =\n snapshotHistory.find((item) => item.timestamp === timestamp) ??\n rfp.snapshot;\n}\nconst { snapshot } = rfp;\nsnapshot.timeline = parseJSON(snapshot.timeline);\nconst authorId = rfp.author_id;\nconst blockHeight = parseInt(rfp.social_db_post_block_height);\nconst item = {\n type: \"social\",\n path: `forum.potlock.near/post/main`,\n blockHeight,\n};\nconst rfpURL = getLinkUsingCurrentGateway(\n `bos.forum.potlock.near/widget/app?page=rfp&id=${rfp.id}&timestamp=${snapshot.timestamp}`\n);\nconst SidePanelItem = ({ title, children, hideBorder, ishidden }) => {\n return (\n <div\n style={{ gap: \"8px\" }}\n className={\n ishidden\n ? \"d-none\"\n : \"d-flex flex-column pb-3 \" + (!hideBorder && \" border-bottom\")\n }\n >\n <div className=\"h6 mb-0\">{title} </div>\n <div className=\"text-muted\">{children}</div>\n </div>\n );\n};\nconst isAllowedToWriteRfp = Near.view(\n \"forum.potlock.near\",\n \"is_allowed_to_write_rfps\",\n {\n editor: accountId,\n }\n);\nconst link = href({\n widgetSrc: `bos.forum.potlock.near/widget/app`,\n params: {\n page: \"create-rfp\",\n id: rfp.id,\n timestamp: timestamp,\n },\n});\nconst createdDate = snapshotHistory[0].timestamp ?? snapshot.timestamp;\nconst [approvedProposals, setApprovedProposals] = useState([]);\nconst [isCancelModalOpen, setCancelModal] = useState(false);\nconst [isWarningModalOpen, setWarningModal] = useState(false);\nconst [timeline, setTimeline] = useState(null);\nconst [showTimelineSetting, setShowTimelineSetting] = useState(false);\nuseEffect(() => {\n if (!timeline) {\n setTimeline(snapshot.timeline);\n }\n}, [snapshot]);\nfunction fetchApprovedRfpProposals() {\n const queryName =\n \"bos_forum_potlock_near_ai_pgf_indexer_proposals_with_latest_snapshot\";\n const query = `query GetLatestSnapshot($offset: Int = 0, $limit: Int = 10, $where: ${queryName}_bool_exp = {}) {\n ${queryName}(\n offset: $offset\n limit: $limit\n order_by: {proposal_id: desc}\n where: $where\n ) {\n proposal_id\n name\n timeline\n }\n }`;\n const FETCH_LIMIT = 50;\n const variables = {\n limit: FETCH_LIMIT,\n offset,\n where: {\n proposal_id: { _in: rfp.snapshot.linked_proposals },\n },\n };\n fetchGraphQL(query, \"GetLatestSnapshot\", variables).then(async (result) => {\n if (result.status === 200) {\n if (result.body.data) {\n const data = result.body.data?.[queryName];\n const approved = [];\n data.map((item) => {\n const timeline = parseJSON(item.timeline);\n if (PROPOSALS_APPROVED_STATUS_ARRAY.includes(timeline.status)) {\n approved.push(item);\n }\n });\n setApprovedProposals(approved);\n }\n }\n });\n}\nconst editRFPStatus = () => {\n Near.call([\n {\n contractName: \"forum.potlock.near\",\n methodName: \"edit_rfp_timeline\",\n args: {\n id: rfp.id,\n timeline: timeline,\n },\n gas: 270000000000000,\n },\n ]);\n};\nconst onCancelRFP = (value) => {\n Near.call([\n {\n contractName: \"forum.potlock.near\",\n methodName: \"cancel_rfp\",\n args: {\n id: rfp.id,\n proposals_to_cancel:\n value === CANCEL_RFP_OPTIONS.CANCEL_PROPOSALS\n ? snapshot.linked_proposals\n : [],\n proposals_to_unlink:\n value === CANCEL_RFP_OPTIONS.UNLINK_PROPOSALS\n ? snapshot.linked_proposals\n : [],\n },\n gas: 270000000000000,\n },\n ]);\n};\nconst accessControlInfo =\n Near.view(\"forum.potlock.near\", \"get_access_control_info\") ?? null;\nconst moderatorList =\n accessControlInfo?.members_list?.[\"team:moderators\"]?.children;\nfetchApprovedRfpProposals();\nconst SubmitProposalBtn = () => {\n return (\n <div style={{ minWidth: \"fit-content\" }}>\n <Link\n to={href({\n widgetSrc: `bos.forum.potlock.near/widget/app`,\n params: { page: \"create-proposal\", rfp_id: rfp.id },\n })}\n >\n <Widget\n src={`devhub.near/widget/devhub.components.molecule.Button`}\n props={{\n label: (\n <div className=\"d-flex align-items-center gap-2\">\n <i className=\"bi bi-plus-circle\"></i>Submit Proposal\n </div>\n ),\n classNames: { root: \"blue-btn\" },\n }}\n />\n </Link>\n </div>\n );\n};\nreturn (\n <Container className=\"d-flex flex-column gap-2 w-100 mt-4\">\n <Widget\n src={`bos.forum.potlock.near/widget/components.rfps.ConfirmCancelModal`}\n props={{\n isOpen: isCancelModalOpen,\n onCancelClick: () => {\n setCancelModal(false);\n setTimeline({ status: RFP_TIMELINE_STATUS.EVALUATION });\n },\n onConfirmClick: (value) => {\n setCancelModal(false);\n onCancelRFP(value);\n },\n linkedProposalIds: snapshot.linked_proposals,\n }}\n />\n <Widget\n src={`bos.forum.potlock.near/widget/components.rfps.WarningModal`}\n props={{\n isOpen: isWarningModalOpen,\n onConfirmClick: () => {\n setWarningModal(false);\n setTimeline({ status: RFP_TIMELINE_STATUS.EVALUATION });\n },\n }}\n />\n <div className=\"d-flex px-3 px-lg-0 justify-content-between\">\n <div className=\"d-flex gap-2 align-items-center h3\">\n <div>{snapshot.name}</div>\n <div className=\"text-muted\">#{rfp.id}</div>\n </div>\n <div className=\"d-flex gap-2 align-items-center\">\n <Widget\n src={`near/widget/ShareButton`}\n props={{\n postType: \"post\",\n url: rfpURL,\n }}\n />\n {isAllowedToWriteRfp && (\n <Link to={link} style={{ textDecoration: \"none\" }}>\n <Widget\n src={`devhub.near/widget/devhub.components.molecule.Button`}\n props={{\n label: \"Edit\",\n classNames: { root: \"grey-btn btn-sm\" },\n }}\n />\n </Link>\n )}\n </div>\n </div>\n <div className=\"d-flex flex-wrap flex-md-nowrap px-3 px-lg-0 gap-2 align-items-center text-sm pb-3 w-100\">\n <Widget\n src={`bos.forum.potlock.near/widget/components.rfps.StatusTag`}\n props={{\n timelineStatus: snapshot.timeline.status,\n size: \"sm\",\n }}\n />\n <div className=\"w-100 d-flex flex-wrap flex-md-nowrap gap-1 align-items-center\">\n <div className=\"fw-bold text-truncate\">\n <LinkProfile account={authorId}>{authorId}</LinkProfile>\n </div>\n <div>created on {readableDate(createdDate / 1000000)}</div>\n </div>\n </div>\n <div className=\"card no-border rounded-0 full-width-div px-3 px-lg-0\">\n <div className=\"container-xl py-4\">\n {snapshot.timeline.status ===\n RFP_TIMELINE_STATUS.ACCEPTING_SUBMISSIONS && (\n <div className=\"accept-submission-info-container p-3 p-sm-4 d-flex flex-wrap flex-sm-nowrap justify-content-between align-items-center gap-2 rounded-2\">\n <div style={{ minWidth: \"300px\" }}>\n <b>This RFP is accepting submissions.</b>\n <p className=\"text-sm text-muted mt-2\">\n Click Submit Proposal if you want to submit a proposal.\n </p>\n </div>\n <SubmitProposalBtn />\n </div>\n )}\n <div className=\"my-4\">\n <div className=\"d-flex flex-wrap gap-6\">\n <div\n style={{ minWidth: \"350px\" }}\n className=\"flex-3 order-2 order-md-1\"\n >\n <div\n className=\"d-flex gap-2 flex-1\"\n style={{\n zIndex: 99,\n background: \"white\",\n position: \"relative\",\n }}\n >\n <div\n className=\"d-none d-sm-flex\"\n style={{ height: \"max-content\" }}\n >\n <Widget\n src={`bos.forum.potlock.near/widget/components.molecule.Profile`}\n props={{\n accountId: authorId,\n }}\n />\n </div>\n <RfpContainer className=\"rounded-2 flex-1\">\n <Header className=\"d-flex gap-1 align-items-center p-2 px-3 \">\n <div\n className=\"fw-bold text-truncate\"\n style={{ maxWidth: \"60%\" }}\n >\n <LinkProfile account={authorId}>{authorId}</LinkProfile>\n </div>\n <div\n className=\"text-muted\"\n style={{ minWidth: \"fit-content\" }}\n >\n ・{\" \"}\n <Widget\n src={`near/widget/TimeAgo`}\n props={{\n blockHeight,\n blockTimestamp: createdDate,\n }}\n />\n {context.accountId && (\n <div className=\"menu\">\n <Widget\n src={`near/widget/Posts.Menu`}\n props={{\n accountId: authorId,\n blockHeight: blockHeight,\n }}\n />\n </div>\n )}\n </div>\n </Header>\n <div className=\"d-flex flex-column gap-1 p-2 px-3 description-box\">\n <div className=\"text-muted h6 border-bottom pb-1 mt-3\">\n RFP CATEGORY\n <div className=\"my-2\">\n <Widget\n src={`bos.forum.potlock.near/widget/components.molecule.MultiSelectCategoryDropdown`}\n props={{\n selected: snapshot.labels,\n disabled: true,\n hideDropdown: true,\n onChange: () => {},\n availableOptions: rfpLabelOptions,\n }}\n />\n </div>\n </div>\n <div className=\"text-muted h6 border-bottom pb-1 mt-3\">\n SUMMARY\n </div>\n <div>{snapshot.summary}</div>\n <div className=\"text-muted h6 border-bottom pb-1 mt-3 mb-4\">\n DESCRIPTION\n </div>\n <Widget\n src={`devhub.near/widget/devhub.components.molecule.MarkdownViewer`}\n props={{ text: snapshot.description }}\n />\n <div className=\"d-flex gap-2 align-items-center mt-4\">\n <Widget\n src={`bos.forum.potlock.near/widget/components.molecule.LikeButton`}\n props={{\n item,\n rfpId: rfp.id,\n notifyAccountIds: moderatorList,\n }}\n />\n <Widget\n src={`devhub.near/widget/devhub.entity.proposal.CommentIcon`}\n props={{\n item,\n showOverlay: false,\n onClick: () => {},\n }}\n />\n <Widget\n src={`near/widget/CopyUrlButton`}\n props={{\n url: rfpURL,\n }}\n />\n </div>\n </div>\n </RfpContainer>\n </div>\n <div className=\"border-bottom pb-4 mt-4\">\n <Widget\n src={`bos.forum.potlock.near/widget/components.rfps.CommentsAndLogs`}\n props={{\n ...props,\n id: rfp.id,\n item: item,\n approvedProposals: approvedProposals,\n snapshotHistory: snapshotHistory,\n }}\n />\n </div>\n <div\n style={{\n position: \"relative\",\n zIndex: 99,\n backgroundColor: \"white\",\n }}\n className=\"pt-4\"\n >\n <Widget\n src={`bos.forum.potlock.near/widget/components.molecule.ComposeComment`}\n props={{\n ...props,\n item: item,\n notifyAccountIds: moderatorList,\n rfpId: rfp.id,\n }}\n />\n {snapshot.timeline.status ===\n RFP_TIMELINE_STATUS.ACCEPTING_SUBMISSIONS && (\n <div className=\"accept-submission-info-container mt-3 p-3 p-sm-4 d-flex flex-wrap flex-md-nowrap justify-content-between align-items-center gap-2 rounded-2\">\n <div style={{ minWidth: \"350px\" }}>\n <b>Want to respond to this RFP? </b> This RFP is accepting\n submissions.\n </div>\n <SubmitProposalBtn />\n </div>\n )}\n </div>\n </div>\n <div\n style={{ minWidth: \"300px\" }}\n className=\"d-flex flex-column gap-4 flex-1 order-1 order-md-2\"\n >\n <SidePanelItem title=\"Submission Deadline\">\n <h5 className=\"text-black\">\n {readableDate(\n parseFloat(snapshot.submission_deadline / 1000000)\n )}\n </h5>\n </SidePanelItem>\n <SidePanelItem\n title={\n <div>\n <div className=\"d-flex justify-content-between align-content-center\">\n Timeline\n {isAllowedToWriteRfp && (\n <div\n data-testid=\"setting-btn\"\n onClick={() => setShowTimelineSetting(true)}\n >\n <i class=\"bi bi-gear\"></i>\n </div>\n )}\n </div>\n </div>\n }\n >\n <Widget\n src={`bos.forum.potlock.near/widget/components.rfps.TimelineConfigurator`}\n props={{\n timeline: timeline,\n setTimeline: (v) => {\n if (\n snapshot.timeline.status === v.status &&\n timeline.status === v.status\n ) {\n return;\n }\n // if proposal selected timeline is selected and no approved proposals exist, show warning\n if (\n v.status === RFP_TIMELINE_STATUS.PROPOSAL_SELECTED &&\n Array.isArray(approvedProposals) &&\n !approvedProposals.length\n ) {\n setWarningModal(true);\n }\n if (v.status === RFP_TIMELINE_STATUS.CANCELLED) {\n setCancelModal(true);\n }\n setTimeline(v);\n },\n disabled: showTimelineSetting ? false : true,\n }}\n />\n {showTimelineSetting && (\n <div className=\"d-flex gap-2 align-items-center justify-content-end text-sm mt-2\">\n <Widget\n src={`devhub.near/widget/devhub.components.molecule.Button`}\n props={{\n label: \"Cancel\",\n classNames: {\n root: \"btn-outline-danger border-0 shadow-none btn-sm\",\n },\n onClick: () => {\n setShowTimelineSetting(false);\n setTimeline(snapshot.timeline);\n },\n }}\n />\n <Widget\n src={`devhub.near/widget/devhub.components.molecule.Button`}\n props={{\n label: \"Save\",\n classNames: { root: \"blue-btn btn-sm\" },\n onClick: () => {\n editRFPStatus();\n setShowTimelineSetting(false);\n },\n }}\n />\n </div>\n )}\n </SidePanelItem>\n <SidePanelItem\n title={\n \"Selected Proposal\" + \" (\" + approvedProposals.length + \")\"\n }\n ishidden={!approvedProposals.length}\n >\n <Widget\n src={`bos.forum.potlock.near/widget/components.molecule.LinkedProposals`}\n props={{\n linkedProposalIds: (approvedProposals ?? []).map(\n (i) => i.proposal_id\n ),\n showStatus: false,\n }}\n />\n </SidePanelItem>\n <SidePanelItem\n title={\n \"All Proposals\" +\n \" (\" +\n snapshot.linked_proposals.length +\n \")\"\n }\n ishidden={!snapshot.linked_proposals.length}\n >\n <Widget\n src={`bos.forum.potlock.near/widget/components.molecule.LinkedProposals`}\n props={{\n linkedProposalIds: snapshot.linked_proposals,\n showStatus:\n snapshot.timeline.status !==\n RFP_TIMELINE_STATUS.PROPOSAL_SELECTED,\n }}\n />\n </SidePanelItem>\n </div>\n </div>\n </div>\n </div>\n </div>\n </Container>\n);\n" } } } } }
Result:
{ "block_height": "122405820" }
No logs
Receipt:
Predecessor ID:
Gas Burned:
223 Ggas
Tokens Burned:
0 
Transferred 0.18445  to bos.forum.potlock.near
Empty result
No logs