Search
Search

Transaction: DNEofmh...3gqm

Receiver
Status
Succeeded
Transaction Fee
0.00943 
Deposit Value
3.32028 
Gas Used
94 Tgas
Attached Gas
300 Tgas
Created
July 01, 2024 at 9:04:46am
Hash
DNEofmhqppt4oZMbUXnobsDzVSVYRwSGofa4Fo8V3gqm

Actions

Called method: 'set' in contract: social.near
Arguments:
{ "data": { "bos.forum.potlock.near": { "widget": { "components.organism.Footer": { "": "const page = props.page;\nconst Footer = styled.div`\n width: 100%;\n background-color: #000;\n color: white !important;\n padding: 1rem;\n margin-top: 2rem;\n width: 100vw;\n position: relative;\n left: 50%;\n right: 50%;\n margin-left: -50vw;\n margin-right: -50vw;\n @media screen and (max-width: 768px) {\n padding: 1rem;\n }\n svg {\n fill: white;\n }\n path {\n fill: white;\n }\n`;\nconst XIcon = () => {\n return (\n <svg\n width=\"20\"\n height=\"16\"\n version=\"1.1\"\n xmlns=\"http://www.w3.org/2000/svg\"\n viewBox=\"0 0 300 300\"\n >\n <path d=\"M178.57 127.15 290.27 0h-26.46l-97.03 110.38L89.34 0H0l117.13 166.93L0 300.25h26.46l102.4-116.59 81.8 116.59h89.34M36.01 19.54H76.66l187.13 262.13h-40.66\" />\n </svg>\n );\n};\nconst TelegramIcon = () => {\n return (\n <svg\n xmlns=\"http://www.w3.org/2000/svg\"\n width=\"20\"\n height=\"16\"\n viewBox=\"0 0 20 16\"\n fill=\"none\"\n >\n <path\n d=\"M19.7398 1.45657L16.8608 15.0342C16.6436 15.9924 16.0771 16.2309 15.2721 15.7796L10.8854 12.5469L8.76879 14.5828C8.53463 14.817 8.33866 15.0129 7.8872 15.0129L8.20233 10.5452L16.3327 3.19847C16.6862 2.88334 16.256 2.70869 15.7833 3.02386L5.73217 9.35266L1.40507 7.99835C0.463838 7.70445 0.446834 7.05707 1.60095 6.60566L18.526 0.085202C19.3096 -0.208647 19.9954 0.25977 19.7398 1.45657Z\"\n fill=\"#151515\"\n />\n </svg>\n );\n};\nconst YoutubeIcon = () => {\n return (\n <svg\n xmlns=\"http://www.w3.org/2000/svg\"\n width=\"24\"\n height=\"16\"\n viewBox=\"0 0 24 16\"\n fill=\"none\"\n >\n <path\n d=\"M23.1744 2.49854C22.9115 1.51517 22.1369 0.740571 21.1535 0.477714C19.3712 -1.21102e-07 12.2235 0 12.2235 0C12.2235 0 5.07581 -1.21102e-07 3.29346 0.477714C2.3101 0.740571 1.53549 1.51517 1.27264 2.49854C0.794922 4.28089 0.794922 8 0.794922 8C0.794922 8 0.794922 11.7191 1.27264 13.5015C1.53549 14.4848 2.3101 15.2594 3.29346 15.5223C5.07581 16 12.2235 16 12.2235 16C12.2235 16 19.3712 16 21.1535 15.5223C22.1369 15.2594 22.9115 14.4848 23.1744 13.5015C23.6521 11.7191 23.6521 8 23.6521 8C23.6521 8 23.6521 4.28089 23.1744 2.49854ZM9.93778 11.4286V4.57143L15.8761 8L9.93778 11.4286Z\"\n fill=\"#151515\"\n />\n </svg>\n );\n};\nconst SocialLinksContainer = () => {\n return (\n <div className=\"d-flex gap-md-4 gap-2 align-items-center\">\n <a href=\"https://x.com/potlock\" target=\"_blank\">\n <XIcon />\n </a>\n <a href=\"https://aipgf.com/telegram\" target=\"_blank\">\n <TelegramIcon />\n </a>\n </div>\n );\n};\nreturn (\n <Footer className=\"d-flex gap-2 justify-content-center align-items-center\">\n <SocialLinksContainer />\n </Footer>\n);\n" }, "components.molecule.Markdown": { "": "const Container = styled.div`\n font-family: menlo monspace !important;\n p {\n white-space: pre-line; // This ensures text breaks to new line\n span {\n white-space: normal; // and this ensures profile links look normal\n }\n }\n blockquote {\n margin: 1em 0;\n padding-left: 1.5em;\n border-left: 4px solid #ccc;\n color: #666;\n font-style: italic;\n font-size: inherit;\n }\n pre {\n background-color: #f4f4f4;\n border: 1px solid #ddd;\n border-radius: 4px;\n padding: 1em;\n overflow-x: auto;\n font-family: \"Courier New\", Courier, monospace;\n }\n a {\n color: #3c697d;\n font-weight: 500 !important;\n }\n`;\nreturn (\n <Container>\n <Markdown text={props.content} />\n </Container>\n);\n" }, "components.organism.Navbar": { "": "const page = props.page;\nconst [showMenu, setShowMenu] = useState(false);\nconst { href: linkHref } = VM.require(`devhub.near/widget/core.lib.url`);\nlinkHref || (linkHref = () => {});\nconst Logo = () => {\n const Wrapper = styled.div`\n .text-lg {\n font-size: 22px;\n }\n a:hover {\n text-decoration: none;\n }\n `;\n return (\n <Wrapper>\n <Link\n to={linkHref({\n widgetSrc: `bos.forum.potlock.near/widget/app`,\n params: { page: \"proposals\" },\n })}\n >\n <div className=\"d-flex gap-2 align-items-center\">\n <b className=\"text-lg\">AI-PGF</b>\n </div>\n </Link>\n </Wrapper>\n );\n};\nconst ProfileIcon = () => {\n const Wrapper = styled.svg`\n padding: 0.25rem;\n @media screen and (max-width: 768px) {\n display: none;\n }\n `;\n return (\n <Widget\n src={`bos.forum.potlock.near/widget/components.molecule.Profile`}\n props={{\n accountId: context.accountId,\n }}\n />\n );\n};\nconst MenuIcon = () => (\n <svg\n xmlns=\"http://www.w3.org/2000/svg\"\n width=\"16\"\n height=\"16\"\n viewBox=\"0 0 16 16\"\n fill=\"none\"\n style={{ height: 20, width: 20 }}\n >\n <path\n fill-rule=\"evenodd\"\n clip-rule=\"evenodd\"\n d=\"M2 12.2986H14V13.3732H2V12.2986ZM2 9.07471H14V10.1493H2V9.07471ZM2 5.85083H14V6.92546H2V5.85083ZM2 2.62695H14V3.70158H2V2.62695Z\"\n fill=\"#818181\"\n />\n </svg>\n);\nconst Navbar = styled.div`\n padding: 1.5rem 0rem;\n display: flex;\n flex-direction: row;\n justify-content: space-between;\n align-items: center;\n background: #f4f4f4;\n @media screen and (max-width: 768px) {\n padding: 1.875rem 1.375rem;\n }\n`;\nconst LinksContainer = styled.div`\n display: flex;\n flex-direction: row;\n align-items: center;\n gap: 1.5rem;\n @media screen and (max-width: 768px) {\n display: none;\n }\n`;\nconst MobileMenu = styled.button`\n all: unset;\n display: none;\n @media screen and (max-width: 768px) {\n display: block;\n }\n`;\nlet links = [\n {\n title: \"Proposals\",\n href: \"proposals\",\n links: [],\n },\n {\n title: \"RFPs\",\n href: \"rfps\",\n links: [],\n },\n {\n title: \"About\",\n href: \"https://aipgf.com/about\",\n links: [],\n },\n {\n title: \"Ideas\",\n href: \"https://aipgf.com/ideas\",\n links: [],\n },\n];\nconst isModerator = Near.view(\n \"forum.potlock.near\",\n \"is_allowed_to_write_rfps\",\n {\n editor: context.accountId,\n }\n);\nif (isModerator) {\n links = [\n {\n title: \"Admin\",\n href: \"admin\",\n links: [],\n },\n ...links,\n ];\n}\nconst MobileNav = styled.div`\n display: none;\n @media screen and (max-width: 768px) {\n display: flex;\n }\n position: absolute;\n top: 0;\n right: 0;\n width: 207px;\n padding: 24px 36px 36px 16px;\n flex-direction: column;\n align-items: flex-end;\n gap: 2.5rem;\n flex-shrink: 0;\n border-radius: 0px 0px 0px 16px;\n background: rgba(41, 41, 41, 0.6);\n backdrop-filter: blur(5px);\n z-index: 50;\n`;\nconst MobileLink = styled.a`\n color: #f4f4f4 !important;\n font-size: 20px;\n font-style: normal;\n font-weight: 400;\n line-height: 20px; /* 100% */\n margin-bottom: 1rem;\n &.active {\n color: #00ec97 !important;\n }\n &:hover {\n text-decoration: none;\n color: #00ec97 !important;\n }\n`;\nreturn (\n <Navbar className=\"position-relative\">\n <Logo />\n <div className=\"d-flex gap-3 align-items-center\">\n <LinksContainer>\n {links.map((link) => (\n <Widget\n src={`bos.forum.potlock.near/widget/components.molecule.NavbarDropdown`}\n props={{\n title: link.title,\n href: link.href,\n links: link.links,\n page: page,\n }}\n />\n ))}\n </LinksContainer>\n {context.accountId && <ProfileIcon />}\n <MobileMenu onClick={() => setShowMenu(!showMenu)}>\n <MenuIcon />\n </MobileMenu>\n </div>\n {showMenu && (\n <MobileNav>\n <div\n onClick={() => setShowMenu(!showMenu)}\n style={{ cursor: \"pointer\" }}\n >\n <i className=\"bi bi-x\" style={{ fontSize: 20, color: \"#F4F4F4\" }}></i>\n </div>\n <div className=\"d-flex flex-column gap-2\">\n {links.map((link, idx) =>\n link.href ? (\n <MobileLink\n key={`mobile-link-${idx}`}\n className={link.href === props.page && \"active\"}\n href={`/bos.forum.potlock.near/widget/app?page=${link.href}`}\n >\n {link.title}\n </MobileLink>\n ) : (\n link.links.map((it, idx) =>\n it.href.startsWith(\"http://\") ||\n it.href.startsWith(\"https://\") ? (\n <MobileLink\n key={`nested-link-${idx}`}\n className={link.href === props.page && \"active\"}\n href={it.href}\n target=\"no_blank\"\n >\n /{it.title}\n </MobileLink>\n ) : (\n <MobileLink\n key={`nested-link-${idx}`}\n className={link.href === props.page && \"active\"}\n href={`/bos.forum.potlock.near/widget/app?page=${it.href}`}\n >\n /{it.title}\n </MobileLink>\n )\n )\n )\n )}\n </div>\n </MobileNav>\n )}\n </Navbar>\n);\n" }, "components.molecule.SimpleMDE": { "": "/**\n * iframe embedding a SimpleMDE component\n * https://github.com/sparksuite/simplemde-markdown-editor\n */\nconst { getLinkUsingCurrentGateway } = VM.require(\n `bos.forum.potlock.near/widget/core.common`\n) || { getLinkUsingCurrentGateway: () => {} };\nconst data = props.data;\nconst onChange = props.onChange ?? (() => {});\nconst onChangeKeyup = props.onChangeKeyup ?? (() => {}); // in case where we want immediate action\nconst height = props.height ?? \"390\";\nconst className = props.className ?? \"w-100\";\nconst embeddCSS = props.embeddCSS;\nState.init({\n iframeHeight: height,\n message: props.data,\n});\nconst profilesData = Social.get(\"*/profile/name\", \"final\");\nconst followingData = Social.get(\n `${context.accountId}/graph/follow/**`,\n \"final\"\n);\n// SIMPLEMDE CONFIG //\nconst fontFamily = props.fontFamily ?? \"sans-serif\";\nconst alignToolItems = props.alignToolItems ?? \"right\";\nconst placeholder = props.placeholder ?? \"\";\nconst showAccountAutoComplete = props.showAutoComplete ?? false;\nconst showProposalIdAutoComplete = props.showProposalIdAutoComplete ?? false;\nconst showRfpIdAutoComplete = props.showRfpIdAutoComplete ?? false;\nconst autoFocus = props.autoFocus ?? false;\nconst proposalQueryName =\n \"bos_forum_potlock_near_ai_pgf_indexer_proposals_with_latest_snapshot\";\nconst proposalLink = getLinkUsingCurrentGateway(\n `bos.forum.potlock.near/widget/app?page=proposal&id=`\n);\nconst proposalQuery = `query GetLatestSnapshot($offset: Int = 0, $limit: Int = 10, $where: ${proposalQueryName}_bool_exp = {}) {\n${proposalQueryName}(\n offset: $offset\n limit: $limit\n order_by: {proposal_id: desc}\n where: $where\n) {\n name\n proposal_id\n}\n}`;\nconst rfpQueryName =\n \"bos_forum_potlock_near_ai_pgf_indexer_rfps_with_latest_snapshot\";\nconst rfpLink = getLinkUsingCurrentGateway(\n `bos.forum.potlock.near/widget/app?page=rfp&id=`\n);\nconst rfpQuery = `query GetLatestSnapshot($offset: Int = 0, $limit: Int = 10, $where: ${rfpQueryName}_bool_exp = {}) {\n${rfpQueryName}(\n offset: $offset\n limit: $limit\n order_by: {rfp_id: desc}\n where: $where\n) {\n rfp_id\n name\n}\n}`;\nconst code = `\n<!doctype html>\n<html>\n <head>\n <meta charset=\"utf-8\">\n <meta name=\"viewport\" content=\"width=device-width, initial-scale=1, shrink-to-fit=no\">\n <style>\n body { \n margin: auto;\n font-family: menlo monspace !important;\n overflow: visible;\n font-size:14px !important;\n }\n @media screen and (max-width: 768px) {\n body {\n font-size: 12px;\n }\n }\n \n .cursor-pointer {\n cursor: pointer;\n }\n .text-wrap {\n overflow: hidden;\n white-space: normal;\n }\n .dropdown-item:hover,\n .dropdown-item:focus {\n background-color:rgb(0, 236, 151) !important;\n color:white !important;\n outline: none !important;\n }\n .editor-toolbar {\n text-align: ${alignToolItems};\n }\n \n .CodeMirror {\n min-height:200px !important; // for autocomplete to be visble \n }\n .CodeMirror-scroll {\n min-height:200px !important; // for autocomplete to be visble \n }\n ${embeddCSS}\n </style>\n <link rel=\"stylesheet\" href=\"https://cdn.jsdelivr.net/simplemde/latest/simplemde.min.css\">\n <link rel=\"stylesheet\" href=\"https://cdn.jsdelivr.net/highlight.js/latest/styles/github.min.css\">\n <link href=\"https://cdn.jsdelivr.net/npm/bootstrap@5.2.3/dist/css/bootstrap.min.css\" rel=\"stylesheet\" integrity=\"sha384-rbsA2VBKQhggwzxH7pPCaAqO46MgnOM80zW1RWuH61DGLwZJEdK2Kadq2F9CUG65\" crossorigin=\"anonymous\">\n</head>\n<body>\n<div class=\"dropdown\">\n <button style=\"display: none\" type=\"button\" data-bs-toggle=\"dropdown\">\n Dropdown button\n </button>\n <ul class=\"dropdown-menu\" id=\"mentiondropdown\" style=\"position: absolute;\">\n</div>\n<div class=\"dropdown\">\n <button style=\"display: none\" type=\"button\" data-bs-toggle=\"dropdown\">\n Dropdown button\n </button>\n <ul class=\"dropdown-menu\" id=\"referencedropdown\" style=\"position: absolute;\">\n</div>\n</ul>\n<textarea></textarea>\n<script src=\"https://cdn.jsdelivr.net/simplemde/latest/simplemde.min.js\"></script>\n<script src=\"https://cdn.jsdelivr.net/npm/@popperjs/core@2.11.6/dist/umd/popper.min.js\" integrity=\"sha384-oBqDVmMz9ATKxIep9tiCxS/Z9fNfEXiDAYTujMAeBAsjFuCZSmKbSSUnQlmh/jp3\" crossorigin=\"anonymous\"></script>\n<script src=\"https://cdn.jsdelivr.net/npm/bootstrap@5.2.3/dist/js/bootstrap.min.js\" integrity=\"sha384-cuYeSxntonz0PPNlHhBs68uyIAVpIIOZZ5JqeqvYYIcEL727kskC66kF92t6Xl2V\" crossorigin=\"anonymous\"></script>\n<script>\nlet codeMirrorInstance;\nlet isEditorInitialized = false;\nlet followingData = {};\nlet profilesData = {};\nlet proposalQuery = '';\nlet proposalLink = '';\nlet proposalQueryName = '';\nlet rfpQuery = '';\nlet rfpLink = '';\nlet rfpQueryName = '';\nlet showAccountAutoComplete = ${showAccountAutoComplete};\nlet showProposalIdAutoComplete = ${showProposalIdAutoComplete};\nlet showRfpIdAutoComplete = ${showRfpIdAutoComplete}\nfunction getSuggestedAccounts(term) {\n let results = [];\n term = (term || \"\").replace(/\\W/g, \"\").toLowerCase();\n const limit = 5;\n const profiles = Object.entries(profilesData);\n for (let i = 0; i < profiles.length; i++) {\n let score = 0;\n const accountId = profiles[i][0];\n const accountIdSearch = profiles[i][0].replace(/\\W/g, \"\").toLowerCase();\n const nameSearch = (profiles[i][1]?.profile?.name || \"\")\n .replace(/\\W/g, \"\")\n .toLowerCase();\n const accountIdSearchIndex = accountIdSearch.indexOf(term);\n const nameSearchIndex = nameSearch.indexOf(term);\n if (accountIdSearchIndex > -1 || nameSearchIndex > -1) {\n score += 10;\n if (accountIdSearchIndex === 0) {\n score += 10;\n }\n if (nameSearchIndex === 0) {\n score += 10;\n }\n if (followingData[accountId] === \"\") {\n score += 30;\n }\n results.push({\n accountId,\n score,\n });\n }\n }\n results.sort((a, b) => b.score - a.score);\n results = results.slice(0, limit);\n return results;\n}\nasync function asyncFetch(endpoint, { method, headers, body }) {\n try {\n const response = await fetch(endpoint, {\n method: method,\n headers: headers,\n body: body\n });\n if (!response.ok) {\n throw new Error(\"HTTP error!\");\n }\n return await response.json();\n } catch (error) {\n console.error('Error fetching data:', error);\n throw error;\n }\n}\nfunction extractNumbers(str) {\n let numbers = \"\";\n for (let i = 0; i < str.length; i++) {\n if (!isNaN(str[i])) {\n numbers += str[i];\n }\n }\n return numbers;\n};\nasync function getSuggestedRfps(id) {\n let results = [];\n const variables = {\n limit: 3,\n offset: 0,\n where: {},\n };\n if (id) {\n const rfpId = extractNumbers(id);\n if (rfpId) {\n variables[\"where\"] = { rfp_id: { _eq: id } };\n } else {\n variables[\"where\"] = {\n _or: [\n { name: { _iregex: id } },\n { summary: { _iregex: id } },\n { description: { _iregex: id} },\n ]\n };\n }\n }\n await asyncFetch(\"https://near-queryapi.api.pagoda.co/v1/graphql\", {\n method: \"POST\",\n headers: { \"x-hasura-role\": \"bos_forum_potlock_near\" },\n body: JSON.stringify({\n query: rfpQuery,\n variables: variables,\n operationName: \"GetLatestSnapshot\",\n }),\n })\n .then((res) => {\n const rfps =\n res?.data?.[\n rfpQueryName\n ];\n results = rfps;\n })\n .catch((error) => {\n console.error(error);\n });\n return results;\n};\nasync function getSuggestedProposals(id) {\n let results = [];\n const variables = {\n limit: 3,\n offset: 0,\n where: {},\n };\n if (id) {\n const proposalId = extractNumbers(id);\n if (proposalId) {\n variables[\"where\"] = { proposal_id: { _eq: id } };\n } else {\n variables[\"where\"] = {\n _or: [\n { name: { _iregex: id } },\n { summary: { _iregex: id } },\n { description: { _iregex: id} },\n ]\n };\n }\n }\n await asyncFetch(\"https://near-queryapi.api.pagoda.co/v1/graphql\", {\n method: \"POST\",\n headers: { \"x-hasura-role\": \"bos_forum_potlock_near\" },\n body: JSON.stringify({\n query: proposalQuery,\n variables: variables,\n operationName: \"GetLatestSnapshot\",\n }),\n })\n .then((res) => {\n const proposals =\n res?.data?.[\n proposalQueryName\n ];\n results = proposals;\n })\n .catch((error) => {\n console.error(error);\n });\n return results;\n};\n// Initializes SimpleMDE element and attaches to text-area\nconst simplemde = new SimpleMDE({\n forceSync: true,\n toolbar: [\n \"heading\",\n \"bold\",\n \"italic\",\n \"|\", // adding | creates a divider in the toolbar\n \"quote\",\n \"code\",\n \"link\",\n ],\n placeholder: \\`${placeholder}\\`,\n initialValue: \"\",\n insertTexts: {\n link: [\"[\", \"]()\"],\n },\n spellChecker: false,\n renderingConfig: {\n\t\tsingleLineBreaks: false,\n\t\tcodeSyntaxHighlighting: true,\n\t},\n autofocus:${autoFocus}\n});\ncodeMirrorInstance = simplemde.codemirror;\n/**\n * Sends message to Widget to update content\n */\nconst updateContent = () => {\n const content = simplemde.value();\n window.parent.postMessage({ handler: \"update\", content }, \"*\");\n};\n/**\n * Sends message to Widget to update iframe height\n */\nconst updateIframeHeight = () => {\n const iframeHeight = document.body.scrollHeight;\n window.parent.postMessage({ handler: \"resize\", height: iframeHeight }, \"*\");\n};\n// On Change\nsimplemde.codemirror.on('blur', () => {\n updateContent();\n});\nsimplemde.codemirror.on('keyup', () => {\n updateIframeHeight();\n const content = simplemde.value();\n window.parent.postMessage({ handler: \"updateOnKeyup\", content }, \"*\");\n});\nif (showAccountAutoComplete) {\n let mentionToken;\n let mentionCursorStart;\n const dropdown = document.getElementById(\"mentiondropdown\");\n simplemde.codemirror.on(\"keydown\", () => {\n if (mentionToken && event.key === 'ArrowDown') {\n dropdown.querySelector('button').focus();\n event.preventDefault();\n return false;\n }\n });\n simplemde.codemirror.on(\"keyup\", (cm, event) => {\n const cursor = cm.getCursor();\n const token = cm.getTokenAt(cursor);\n const createMentionDropDownOptions = () => {\n const mentionInput = cm.getRange(mentionCursorStart, cursor);\n dropdown.innerHTML = getSuggestedAccounts(mentionInput)\n .map(\n (item) =>\n '<li><button class=\"dropdown-item cursor-pointer w-100 text-wrap\">' + item?.accountId + '</button></li>'\n )\n .join(\"\");\n dropdown.querySelectorAll(\"li\").forEach((li) => {\n li.addEventListener(\"click\", () => {\n const selectedText = li.textContent.trim();\n simplemde.codemirror.replaceRange(selectedText, mentionCursorStart, cursor);\n mentionToken = null;\n dropdown.classList.remove(\"show\");\n cm.focus();\n });\n });\n }\n // show dropwdown only when @ is at first place or when there is a space before @\n if (!mentionToken && (token.string === \"@\" && cursor.ch === 1 || token.string === \"@\" && cm.getTokenAt({line:cursor.line, ch: cursor.ch - 1}).string == ' ')) {\n mentionToken = token;\n mentionCursorStart = cursor;\n // Calculate cursor position relative to the iframe's viewport\n const rect = cm.charCoords(cursor);\n const x = rect.left;\n const y = rect.bottom;\n // Create dropdown with options\n dropdown.style.top = y + \"px\";\n dropdown.style.left = x + \"px\";\n createMentionDropDownOptions();\n dropdown.classList.add(\"show\");\n // Close dropdown on outside click\n document.addEventListener(\"click\", function(event) {\n if (!dropdown.contains(event.target)) {\n mentionToken = null;\n dropdown.classList.remove(\"show\");\n }\n });\n } else if (mentionToken && token.string.match(/[^@a-z0-9.]/)) {\n mentionToken = null;\n dropdown.classList.remove(\"show\");\n } else if (mentionToken) {\n createMentionDropDownOptions();\n }\n});\n}\nif (showProposalIdAutoComplete) {\n let proposalId;\n let referenceCursorStart;\n const dropdown = document.getElementById(\"referencedropdown\");\n const loader = document.createElement('div');\n loader.className = 'loader';\n loader.textContent = 'Loading...';\n simplemde.codemirror.on(\"keydown\", () => {\n if (proposalId && event.key === 'ArrowDown') {\n dropdown.querySelector('button').focus();\n event.preventDefault();\n return false;\n }\n });\n simplemde.codemirror.on(\"keyup\", (cm, event) => {\n const cursor = cm.getCursor();\n const token = cm.getTokenAt(cursor);\n const createReferenceDropDownOptions = async () => {\n try {\n const input = cm.getRange(referenceCursorStart, cursor);\n dropdown.innerHTML = ''; // Clear previous content\n dropdown.appendChild(loader); // Show loader\n const suggestedProposals = await getSuggestedProposals(input);\n const suggestedRFPs = await getSuggestedRfps(input);\n const proposalItems = suggestedProposals\n .map(\n (item) =>\n '<li><button class=\"dropdown-item cursor-pointer w-100 text-wrap\">' + \"#\" + item?.proposal_id + \" Proposal: \" + item.name + '</button></li>'\n )\n .join(\"\");\n const rfpItems = suggestedRFPs\n .map(\n (item) =>\n '<li><button class=\"dropdown-item cursor-pointer w-100 text-wrap\">' + \"#\" + item?.rfp_id + \" RFP: \" + \" \" + item.name + '</button></li>'\n )\n .join(\"\");\n dropdown.innerHTML = proposalItems + rfpItems;\n dropdown.querySelectorAll(\"li\").forEach((li) => {\n li.addEventListener(\"click\", () => {\n const selectedText = li.textContent.trim();\n const startIndex = selectedText.indexOf('#') + 1; \n const endIndex = selectedText.indexOf(' ', startIndex);\n const id = endIndex !== -1 ? selectedText.substring(startIndex, endIndex) : selectedText.substring(startIndex);\n const link = (selectedText.includes(\"RFP:\") ? rfpLink : proposalLink) + id;\n const adjustedStart = {\n line: referenceCursorStart.line,\n ch: referenceCursorStart.ch - 1\n };\n simplemde.codemirror.replaceRange(\"[\" + selectedText + \"]\" + \"(\" + link + \")\", adjustedStart, cursor);\n proposalId = null;\n dropdown.classList.remove(\"show\");\n cm.focus();\n });\n });\n } catch (error) {\n console.error('Error fetching data:', error);\n // Handle error: Remove loader\n dropdown.innerHTML = ''; // Clear previous content\n } finally {\n // Remove loader\n dropdown.removeChild(loader);\n }\n }\n // show dropwdown only when there is space before # or it's first char\n if (!proposalId && (token.string === \"#\" && cursor.ch === 1 || token.string === \"#\" && cm.getTokenAt({line:cursor.line, ch: cursor.ch - 1}).string == ' ')) {\n proposalId = token;\n referenceCursorStart = cursor;\n // Calculate cursor position relative to the iframe's viewport\n const rect = cm.charCoords(cursor);\n const x = rect.left;\n const y = rect.bottom;\n // Create dropdown with options\n dropdown.style.top = y + \"px\";\n dropdown.style.left = x + \"px\";\n createReferenceDropDownOptions();\n dropdown.classList.add(\"show\");\n // Close dropdown on outside click\n document.addEventListener(\"click\", function(event) {\n if (!dropdown.contains(event.target)) {\n proposalId = null;\n dropdown.classList.remove(\"show\");\n }\n });\n } else if (proposalId && (token.string.match(/[^#a-z0-9.]/) || !token.string)) {\n proposalId = null;\n dropdown.classList.remove(\"show\");\n } else if (proposalId) {\n createReferenceDropDownOptions();\n }\n});\n}\nwindow.addEventListener(\"message\", (event) => {\n if (!isEditorInitialized && event.data !== \"\") {\n simplemde.value(event.data.content);\n isEditorInitialized = true;\n } else {\n if (event.data.handler === 'refreshEditor') {\n codeMirrorInstance.getDoc().setValue(event.data.content);\n }\n }\n if (event.data.followingData) {\n followingData = event.data.followingData;\n }\n if (event.data.profilesData) {\n profilesData = JSON.parse(event.data.profilesData);\n }\n if (event.data.proposalQuery) {\n proposalQuery = event.data.proposalQuery;\n }\n if (event.data.proposalQueryName) {\n proposalQueryName = event.data.proposalQueryName;\n }\n if (event.data.proposalLink) {\n proposalLink = event.data.proposalLink;\n }\n if (event.data.rfpQuery) {\n rfpQuery = event.data.rfpQuery;\n }\n if (event.data.rfpQueryName) {\n rfpQueryName = event.data.rfpQueryName;\n }\n if (event.data.rfpLink) {\n rfpLink = event.data.rfpLink;\n }\n \n});\n</script>\n</body>\n</html>\n`;\nreturn (\n <iframe\n className={className}\n style={{\n height: `${state.iframeHeight}px`,\n maxHeight: \"410px\",\n minHeight: \"250px\",\n }}\n srcDoc={code}\n message={{\n content: props.data?.content ?? \"\",\n followingData,\n profilesData: JSON.stringify(profilesData),\n proposalQuery: proposalQuery,\n proposalQueryName: proposalQueryName,\n proposalLink: proposalLink,\n rfpQuery: rfpQuery,\n rfpQueryName: rfpQueryName,\n rfpLink: rfpLink,\n handler: props.data.handler,\n }}\n onMessage={(e) => {\n switch (e.handler) {\n case \"update\":\n {\n onChange(e.content);\n }\n break;\n case \"resize\":\n {\n const offset = 10;\n State.update({ iframeHeight: e.height + offset });\n }\n break;\n case \"updateOnKeyup\":\n {\n onChangeKeyup(e.content);\n }\n break;\n }\n }}\n />\n);\n" }, "components.molecule.Compose": { "": "const EmbeddCSS = `\n .CodeMirror {\n margin-inline:10px;\n border-radius:5px;\n }\n .editor-toolbar {\n border: none !important;\n }\n`;\nconst Wrapper = styled.div`\n .nav-link {\n color: inherit !important;\n }\n .card-header {\n padding-bottom: 0px !important;\n }\n`;\nconst Compose = ({\n data,\n onChange,\n autocompleteEnabled,\n placeholder,\n height,\n embeddCSS,\n showProposalIdAutoComplete,\n onChangeKeyup,\n handler,\n}) => {\n State.init({\n data: data,\n selectedTab: \"editor\",\n autoFocus: false,\n });\n useEffect(() => {\n if (typeof onChange === \"function\") {\n onChange(state.data);\n }\n }, [state.data]);\n useEffect(() => {\n // for clearing editor after txn approval/ showing draft state\n if (data !== state.data || handler !== state.handler) {\n State.update({ data: data, handler: handler });\n }\n }, [data, handler]);\n return (\n <Wrapper>\n <div className=\"card\">\n <div className=\"card-header\" style={{ position: \"relative\" }}>\n <div>\n <ul class=\"nav nav-tabs\">\n <li class=\"nav-item\">\n <button\n class={`nav-link ${\n state.selectedTab === \"editor\" ? \"active\" : \"\"\n }`}\n onClick={() =>\n State.update({ selectedTab: \"editor\", autoFocus: true })\n }\n >\n Write\n </button>\n </li>\n <li class=\"nav-item\">\n <button\n class={`nav-link ${\n state.selectedTab === \"preview\" ? \"active\" : \"\"\n }`}\n onClick={() => State.update({ selectedTab: \"preview\" })}\n >\n Preview\n </button>\n </li>\n </ul>\n </div>\n </div>\n {state.selectedTab === \"editor\" ? (\n <>\n <Widget\n src={`bos.forum.potlock.near/widget/components.molecule.SimpleMDE`}\n props={{\n data: { handler: state.handler, content: state.data },\n onChange: (content) => {\n State.update({ data: content, handler: \"update\" });\n },\n placeholder: placeholder,\n height,\n embeddCSS: embeddCSS || EmbeddCSS,\n showAutoComplete: autocompleteEnabled,\n showProposalIdAutoComplete: showProposalIdAutoComplete,\n autoFocus: state.autoFocus,\n onChangeKeyup: onChangeKeyup,\n }}\n />\n </>\n ) : (\n <div className=\"card-body\">\n <Widget\n src={`devhub.near/widget/devhub.components.molecule.MarkdownViewer`}\n props={{\n text: state.data,\n }}\n />\n </div>\n )}\n </div>\n </Wrapper>\n );\n};\nreturn Compose(props);\n" }, "components.molecule.AccountInput": { "": "const value = props.value;\nconst placeholder = props.placeholder;\nconst onUpdate = props.onUpdate;\nconst [account, setAccount] = useState(value);\nconst [showAccountAutocomplete, setAutoComplete] = useState(false);\nconst [isValidAccount, setValidAccount] = useState(true);\nconst AutoComplete = styled.div`\n margin-top: 1rem;\n`;\nuseEffect(() => {\n if (value !== account) {\n setAccount(value);\n }\n}, [value]);\nuseEffect(() => {\n if (value !== account) {\n onUpdate(account);\n }\n}, [account]);\nuseEffect(() => {\n const handler = setTimeout(() => {\n const valid = account.length === 64 || (account ?? \"\").includes(\".near\");\n setValidAccount(valid);\n setAutoComplete(!valid);\n }, 100);\n return () => {\n clearTimeout(handler);\n };\n}, [account]);\nreturn (\n <div>\n <Widget\n src={`devhub.near/widget/devhub.components.molecule.Input`}\n props={{\n className: \"flex-grow-1\",\n value: account,\n onChange: (e) => {\n setAccount(e.target.value);\n },\n skipPaddingGap: true,\n placeholder: placeholder,\n inputProps: {\n max: 64,\n prefix: \"@\",\n },\n }}\n />\n {account && !isValidAccount && (\n <div style={{ color: \"red\" }} className=\"text-sm mt-1\">\n Please enter valid account ID\n </div>\n )}\n {showAccountAutocomplete && (\n <AutoComplete>\n <Widget\n src={`devhub.near/widget/devhub.components.molecule.AccountAutocomplete`}\n props={{\n term: account,\n onSelect: (id) => {\n setAccount(id);\n setAutoComplete(false);\n },\n onClose: () => setAutoComplete(false),\n }}\n />\n </AutoComplete>\n )}\n </div>\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://near.social/near/widget/ProfilePage?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.template.AppLayout": { "": "const data = fetch(`https://httpbin.org/headers`);\nconst gatewayURL = data?.body?.headers?.Origin ?? \"\";\n// we need fixed positioning for near social and not for org\nconst ParentContainer = gatewayURL.includes(\"near.org\")\n ? styled.div`\n width: 100%;\n min-height: 90vh;\n background: #f4f4f4;\n padding-bottom: 1rem;\n `\n : styled.div`\n position: fixed;\n inset: 73px 0px 0px;\n width: 100%;\n overflow-y: scroll;\n min-height: 90vh;\n background: #f4f4f4;\n `;\nconst Theme = styled.div`\n display: flex;\n flex-direction: column;\n padding-top: calc(-1 * var(--body-top-padding));\n background: #f4f4f4;\n .container-xl {\n padding-inline: 0px !important;\n }\n font-family: menlo monspace !important;\n`;\nconst Container = styled.div`\n width: 100%;\n`;\nconst ContentContainer = styled.div`\n flex: 1;\n display: flex;\n flex-direction: column;\n align-items: center;\n width: 100%;\n`;\nconst AppHeader = ({ page }) => (\n <Widget\n src={`bos.forum.potlock.near/widget/components.organism.Navbar`}\n props={{\n page: page,\n ...props,\n }}\n />\n);\nconst Footer = (props) => {\n return (\n <Widget\n src=\"bos.forum.potlock.near/widget/components.organism.Footer\"\n props={{\n ...props,\n }}\n />\n );\n};\nconst AppLayout = ({ page, children }) => {\n return (\n <ParentContainer>\n <Theme>\n <Container className=\"container-xl p-3\">\n <AppHeader page={page} />\n <ContentContainer className=\"content-container\">\n {children}\n </ContentContainer>\n </Container>\n <Footer />\n </Theme>\n </ParentContainer>\n );\n};\nreturn { AppLayout };\n" }, "app": { "": "/**\n * This is the main entry point for the RFP application.\n * Page route gets passed in through params, along with all other page props.\n */\nconst { page, ...passProps } = props;\n// Import our modules\nconst { AppLayout } = VM.require(\n `bos.forum.potlock.near/widget/components.template.AppLayout`\n);\nif (!AppLayout) {\n return <p>Loading modules...</p>;\n}\n// CSS styles to be used across the app.\n// Define fonts here, as well as any other global styles.\nconst Theme = styled.div`\n a {\n color: inherit;\n }\n .attractable {\n box-shadow: 0 0.125rem 0.25rem rgba(0, 0, 0, 0.075) !important;\n transition: box-shadow 0.6s;\n &:hover {\n box-shadow: 0 0.5rem 1rem rgba(0, 0, 0, 0.15) !important;\n }\n }\n`;\nif (!page) {\n // If no page is specified, we default to the feed page TEMP\n page = \"proposals\";\n}\n// This is our navigation, rendering the page based on the page parameter\nfunction Page() {\n const routes = page.split(\".\");\n switch (routes[0]) {\n case \"rfps\": {\n return (\n <Widget\n src={`bos.forum.potlock.near/widget/components.rfps.Feed`}\n props={passProps}\n />\n );\n }\n case \"rfp\": {\n return (\n <Widget\n src={`bos.forum.potlock.near/widget/components.rfps.Rfp`}\n props={passProps}\n />\n );\n }\n case \"create-rfp\": {\n return (\n <Widget\n src={`bos.forum.potlock.near/widget/components.rfps.Editor`}\n props={passProps}\n />\n );\n }\n case \"create-proposal\": {\n return (\n <Widget\n src={`bos.forum.potlock.near/widget/components.proposals.Editor`}\n props={{ ...passProps }}\n />\n );\n }\n case \"ideas\": {\n return (\n <Widget\n src={`bos.forum.potlock.near/widget/components.proposals.Feed`}\n props={passProps}\n />\n );\n }\n case \"proposals\": {\n return (\n <Widget\n src={`bos.forum.potlock.near/widget/components.proposals.Feed`}\n props={passProps}\n />\n );\n }\n case \"proposal\": {\n return (\n <Widget\n src={`bos.forum.potlock.near/widget/components.proposals.Proposal`}\n props={passProps}\n />\n );\n }\n case \"admin\": {\n return (\n <Widget\n src={`bos.forum.potlock.near/widget/components.pages.admin`}\n props={passProps}\n />\n );\n }\n default: {\n // TODO: 404 page\n return <p>404</p>;\n }\n }\n}\nreturn (\n <Theme>\n <AppLayout page={page}>\n <Page />\n </AppLayout>\n </Theme>\n);\n" }, "components.molecule.FilterByLabel": { "": "const availableOptions = props.availableOptions;\nconst options =\n (availableOptions ?? []).map((i) => {\n return { label: i.title, value: i.value };\n }) ?? [];\noptions.unshift({ label: \"All\", value: null });\nconst setSelected = props.onStateChange ?? (() => {});\nreturn (\n <div>\n <Widget\n src={`devhub.near/widget/devhub.components.molecule.DropDown`}\n props={{\n options: options,\n label: \"Category\",\n onUpdate: (v) => {\n setSelected(v);\n },\n }}\n />\n </div>\n);\n" }, "components.rfps.StageDropdown": { "": "const { RFP_TIMELINE_STATUS } = VM.require(\n `bos.forum.potlock.near/widget/core.common`\n) || { RFP_TIMELINE_STATUS: {} };\nconst setSelected = props.onStateChange ?? (() => {});\nconst timelineStatusArray = Object.entries(RFP_TIMELINE_STATUS).map(\n ([key, value]) => ({\n label: key\n .split(\"_\")\n .map((word) => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase())\n .join(\" \"),\n value,\n })\n);\ntimelineStatusArray.unshift({ label: \"All\", value: null });\nreturn (\n <div>\n <Widget\n src={`devhub.near/widget/devhub.components.molecule.DropDown`}\n props={{\n options: timelineStatusArray,\n label: \"Timeline\",\n onUpdate: (v) => {\n setSelected(v);\n },\n }}\n />\n </div>\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 src={`devhub.near/widget/devhub.entity.proposal.LoginScreen`} />\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\">\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.pages.admin": { "": "const accessControlInfo =\n Near.view(\"forum.potlock.near\", \"get_access_control_info\") ?? null;\nif (!accessControlInfo.members_list) {\n return (\n <Widget src={`devhub.near/widget/devhub.components.molecule.Spinner`} />\n );\n}\nconst rootMembers = Near.view(\"forum.potlock.near\", \"get_root_members\") ?? null;\nconst teamNames = Object.keys(rootMembers || {});\nconst isModerator = Near.view(\n \"forum.potlock.near\",\n \"is_allowed_to_write_rfps\",\n {\n editor: context.accountId,\n }\n);\nconst noPermissionBanner = (\n <div className=\"d-flex flex-column justify-content-center align-items-center\">\n <h2 className=\"alert alert-danger p-3 h6\">\n Your account does not have administration permissions.\n </h2>\n </div>\n);\nif (!isModerator) {\n return noPermissionBanner;\n}\nfunction createEditTeam({\n teamName,\n members,\n description,\n contractCall, // typescript edit_member || add_member\n}) {\n let txn = [];\n const membersAndTeams = Object.keys(accessControlInfo.members_list);\n members.forEach((member) => {\n // if Contract panic member does not exist in the members_list\n if (!membersAndTeams.includes(member)) {\n // Add member\n txn.push({\n contractName: \"forum.potlock.near\",\n methodName: \"add_member\",\n args: {\n member: member,\n metadata: {\n member_metadata_version: \"V0\",\n description: description,\n permissions: {\n \"*\": [\"use-labels\", \"edit-post\"],\n },\n children: [],\n parents: [],\n },\n },\n gas: Big(10).pow(14),\n });\n }\n });\n // Check edit team\n Near.call([\n ...txn,\n {\n contractName: \"forum.potlock.near\",\n methodName: contractCall, // add_member || edit_member\n args: {\n member: `team:${teamName}`,\n metadata: {\n member_metadata_version: \"V0\",\n description: description,\n permissions: {\n \"*\": [\"use-labels\", \"edit-post\"],\n },\n children: members,\n parents: [],\n },\n },\n gas: Big(10).pow(14),\n },\n ]);\n}\nconst Container = styled.div`\n width: 100%;\n margin: 0 auto;\n padding: 20px;\n text-align: left;\n min-height: 65vh;\n`;\nconst Tab = styled.button`\n color: rgb(0, 236, 151);\n &:hover {\n color: rgba(0, 236, 151, 0.5);\n }\n`;\nreturn (\n <Container>\n <div className=\"d-flex flex-column gap-4 p-4\">\n <ul class=\"nav nav-tabs\" id=\"myTab\" role=\"tablist\">\n <li class=\"nav-item\" role=\"presentation\">\n <Tab\n className=\"nav-link active\"\n id=\"profile-tab\"\n data-bs-toggle=\"tab\"\n data-bs-target=\"#profile\"\n type=\"button\"\n role=\"tab\"\n aria-controls=\"profile\"\n aria-selected=\"false\"\n >\n Moderators\n </Tab>\n </li>\n <li class=\"nav-item\" role=\"presentation\">\n <Tab\n className=\"nav-link\"\n id=\"announcement-tab\"\n data-bs-toggle=\"tab\"\n data-bs-target=\"#announcement\"\n type=\"button\"\n role=\"tab\"\n aria-controls=\"announcement\"\n aria-selected=\"false\"\n >\n Announcement\n </Tab>\n </li>\n </ul>\n <div class=\"tab-content\" id=\"myTabContent\">\n <div\n class=\"tab-pane fade show active\"\n id=\"profile\"\n role=\"tabpanel\"\n aria-labelledby=\"profile-tab\"\n >\n <Widget\n src={`bos.forum.potlock.near/widget/components.admin.ModeratorsConfigurator`}\n props={{\n accessControlInfo,\n createEditTeam,\n }}\n />\n </div>\n <div\n class=\"tab-pane fade\"\n id=\"announcement\"\n role=\"tabpanel\"\n aria-labelledby=\"announcement-tab\"\n >\n <Widget\n src={`bos.forum.potlock.near/widget/components.admin.AnnouncementConfigurator`}\n props={{\n accessControlInfo,\n createEditTeam,\n }}\n />\n </div>\n </div>\n </div>\n </Container>\n);\n" }, "components.admin.ModeratorsConfigurator": { "": "const { Tile } = VM.require(\n `devhub.near/widget/devhub.components.molecule.Tile`\n) || { Tile: () => <></> };\nconst { accessControlInfo, createEditTeam } = props;\nconst [editModerators, setEditModerators] = useState(false);\nconst [moderators, setModerators] = useState(\n accessControlInfo.members_list[\"team:moderators\"].children || []\n);\nconst handleEditModerators = () => {\n createEditTeam({\n teamName: \"moderators\",\n description:\n \"The moderator group has permissions to create and edit RFPs, edit and manage proposals, and manage admins.\",\n members: moderators,\n contractCall: \"edit_member\",\n });\n};\nconst handleCancelModerators = () => {\n setEditModerators(false);\n setModerators(accessControlInfo.members_list[\"team:moderators\"].children);\n};\nreturn (\n <>\n <h3>Moderators</h3>\n <div className=\"card-body\">\n <h6>\n The moderator group has permissions to create and edit RFPs, edit and\n manage proposals, and manage admins.\n </h6>\n <Widget\n src={`devhub.near/widget/devhub.components.molecule.PostControls`}\n props={{\n icon: \"bi bi-gear-wide-connected\",\n className: \"mb-3\",\n title: \"Edit members\",\n onClick: () => setEditModerators(!editModerators),\n testId: \"edit-members\",\n }}\n />\n </div>\n <Tile className=\"p-3\" style={{ background: \"white\" }}>\n {editModerators ? (\n <>\n <Widget\n src={`bos.forum.potlock.near/widget/components.admin.AccountsEditor`}\n props={{\n data: {\n maxLength: 100,\n placeholder: \"member.near\",\n list: moderators,\n },\n setList: setModerators,\n // Could add a check to see if it is an valid account id.\n validate: (newItem) => true,\n invalidate: () => null,\n }}\n />\n <div\n className={\n \"d-flex align-items-center justify-content-end gap-3 mt-4\"\n }\n >\n <Widget\n src={`devhub.near/widget/devhub.components.molecule.Button`}\n props={{\n classNames: {\n root: \"btn-outline-danger shadow-none border-0\",\n },\n label: \"Cancel\",\n onClick: handleCancelModerators,\n }}\n />\n <Widget\n src={`devhub.near/widget/devhub.components.molecule.Button`}\n props={{\n classNames: { root: \"btn-success\" },\n icon: {\n type: \"bootstrap_icon\",\n variant: \"bi-check-circle-fill\",\n },\n label: \"Submit\",\n onClick: handleEditModerators,\n }}\n />\n </div>\n </>\n ) : (\n <>\n <div class=\"pt-4\">Members</div>\n {moderators && (\n <div class=\"vstack\">\n {moderators.length ? (\n moderators.map((child) => (\n <Tile className=\"w-25 p-3 m-1\" minHeight={10}>\n <Widget\n src={`devhub.near/widget/devhub.components.molecule.ProfileLine`}\n props={{ accountId: child }}\n />\n </Tile>\n ))\n ) : (\n <div>No moderators</div>\n )}\n </div>\n )}\n </>\n )}\n </Tile>\n </>\n);\n" }, "components.molecule.LinkedRfps": { "": "const { readableDate } = VM.require(`devhub.near/widget/core.lib.common`) || {\n readableDate: () => {},\n};\nconst { href } = VM.require(`devhub.near/widget/core.lib.url`) || {\n href: () => {},\n};\nconst linkedRfpIds = props.linkedRfpIds ?? [];\nconst linkedRfpsData = [];\nlinkedRfpIds.map((item) => {\n const data = Near.view(\"forum.potlock.near\", \"get_rfp\", {\n rfp_id: item,\n });\n if (data !== null) {\n linkedRfpsData.push(data);\n }\n});\nconst Container = styled.div`\n a {\n &:hover {\n text-decoration: none !important;\n }\n }\n`;\nreturn (\n <Container className=\"d-flex flex-column gap-3\">\n {linkedRfpsData.map((item) => {\n return (\n <a\n href={href({\n widgetSrc: `bos.forum.potlock.near/widget/app`,\n params: {\n page: \"rfp\",\n id: item.id,\n },\n })}\n target=\"_blank\"\n rel=\"noopener noreferrer\"\n >\n <div className=\"d-flex gap-2\">\n <Widget\n src={`bos.forum.potlock.near/widget/components.molecule.Profile`}\n props={{\n accountId: item.author_id,\n }}\n />\n <div className=\"d-flex flex-column\" style={{ maxWidth: 250 }}>\n <b className=\"text-truncate\">{item.snapshot.name}</b>\n <div className=\"text-sm text-muted\">\n created on {readableDate(item.snapshot.timestamp / 1000000)}\n </div>\n </div>\n </div>\n </a>\n );\n })}\n </Container>\n);\n" }, "components.molecule.ComposeComment": { "": "const proposalId = props.proposalId;\nconst rfpId = props.rfpId;\nconst draftKey = \"AI_PGF_COMMENT_DRAFT\" + proposalId;\nlet draftComment = \"\";\nconst ComposeEmbeddCSS = `\n .CodeMirror {\n border: none !important;\n min-height: 50px !important;\n }\n .editor-toolbar {\n border: none !important;\n }\n .CodeMirror-scroll{\n min-height: 50px !important;\n max-height: 300px !important;\n }\n`;\nconst notifyAccountIds = props.notifyAccountIds ?? [];\nconst accountId = context.accountId;\nconst item = props.item;\nconst [allowGetDraft, setAllowGetDraft] = useState(true);\nconst [comment, setComment] = useState(null);\nconst [isTxnCreated, setTxnCreated] = useState(false);\nconst [handler, setHandler] = useState(\"update\"); // to update editor state on draft and txn approval\nconst [showCommentToast, setCommentToast] = useState(false);\nif (allowGetDraft) {\n draftComment = Storage.privateGet(draftKey);\n}\nuseEffect(() => {\n if (draftComment) {\n setComment(draftComment);\n setAllowGetDraft(false);\n setHandler(\"refreshEditor\");\n }\n}, [draftComment]);\nuseEffect(() => {\n if (draftComment === comment) {\n return;\n }\n const handler = setTimeout(() => {\n Storage.privateSet(draftKey, comment);\n }, 1000);\n return () => {\n clearTimeout(handler);\n };\n}, [comment]);\nuseEffect(() => {\n if (handler === \"update\") {\n return;\n }\n const handler = setTimeout(() => {\n setHandler(\"update\");\n }, 3000);\n return () => {\n clearTimeout(handler);\n };\n}, [handler]);\nif (!accountId) {\n return (\n <div\n style={{\n marginLeft: 10,\n backgroundColor: \"#ECF8FB\",\n border: \"1px solid #E2E6EC\",\n }}\n className=\"d-flex align-items-center gap-1 p-4 rounded-2 flex-wrap flex-md-nowrap\"\n >\n <Link to=\"https://near.org/signup\">\n <Widget\n src={`devhub.near/widget/devhub.components.molecule.Button`}\n props={{\n classNames: { root: \"grey-btn\" },\n label: \"Sign up\",\n }}\n />\n </Link>\n <div className=\"fw-bold\">to join this conversation.</div>\n <div>Already have an account?</div>\n <a className=\"text-decoration-underline\" href=\"https://near.org/signin\">\n Log in to comment\n </a>\n </div>\n );\n}\nfunction extractMentions(text) {\n const mentionRegex =\n /@((?:(?:[a-z\\d]+[-_])*[a-z\\d]+\\.)*(?:[a-z\\d]+[-_])*[a-z\\d]+)/gi;\n mentionRegex.lastIndex = 0;\n const accountIds = new Set();\n for (const match of text.matchAll(mentionRegex)) {\n if (\n !/[\\w`]/.test(match.input.charAt(match.index - 1)) &&\n !/[/\\w`]/.test(match.input.charAt(match.index + match[0].length)) &&\n match[1].length >= 2 &&\n match[1].length <= 64\n ) {\n accountIds.add(match[1].toLowerCase());\n }\n }\n return [...accountIds];\n}\nfunction extractTagNotifications(text, item) {\n return extractMentions(text || \"\")\n .filter((accountId) => accountId !== context.accountId)\n .map((accountId) => ({\n key: accountId,\n value: {\n type: \"mention\",\n item,\n },\n }));\n}\nfunction composeData() {\n setTxnCreated(true);\n const data = {\n post: {\n comment: JSON.stringify({\n type: \"md\",\n text: comment,\n item,\n }),\n },\n index: {\n comment: JSON.stringify({\n key: item,\n value: {\n type: \"md\",\n },\n }),\n },\n };\n const notifications = extractTagNotifications(comment, {\n type: \"social\",\n path: `${accountId}/post/comment`,\n });\n if (notifyAccountIds.length > 0) {\n notifyAccountIds.map((account) => {\n if (account !== context.accountId) {\n notifications.push({\n key: account,\n value: proposalId\n ? {\n type: \"proposal/reply\",\n item,\n proposal: proposalId,\n widgetAccountId: \"bos.forum.potlock.near\",\n }\n : {\n type: \"rfp/reply\",\n item,\n rfp: rfpId,\n widgetAccountId: \"bos.forum.potlock.near\",\n },\n });\n }\n });\n }\n if (notifications.length) {\n data.index.notify = JSON.stringify(\n notifications.length > 1 ? notifications : notifications[0]\n );\n }\n Social.set(data, {\n force: true,\n onCommit: () => {\n setCommentToast(true);\n setComment(\"\");\n setHandler(\"refreshEditor\");\n setTxnCreated(false);\n },\n onCancel: () => {\n setTxnCreated(false);\n },\n });\n}\nuseEffect(() => {\n if (props.transactionHashes && comment) {\n setComment(\"\");\n }\n}, [props.transactionHashes]);\nconst LoadingButtonSpinner = (\n <span\n class=\"comment-btn-spinner spinner-border spinner-border-sm\"\n role=\"status\"\n aria-hidden=\"true\"\n ></span>\n);\nconst Compose = useMemo(() => {\n return (\n <Widget\n src={`bos.forum.potlock.near/widget/components.molecule.Compose`}\n props={{\n data: comment,\n onChangeKeyup: setComment,\n autocompleteEnabled: true,\n placeholder: \"Add your comment here...\",\n height: \"250\",\n embeddCSS: ComposeEmbeddCSS,\n handler: handler,\n showProposalIdAutoComplete: true,\n }}\n />\n );\n}, [draftComment, handler]);\nreturn (\n <div className=\"d-flex gap-2\">\n <Widget\n src={`near/widget/DIG.Toast`}\n props={{\n title: \"Comment Submitted Successfully\",\n type: \"success\",\n open: showCommentToast,\n onOpenChange: (v) => setCommentToast(v),\n trigger: <></>,\n providerProps: { duration: 3000 },\n }}\n />\n <Widget\n src={`bos.forum.potlock.near/widget/components.molecule.Profile`}\n props={{\n accountId: accountId,\n }}\n />\n <div className=\"d-flex flex-column gap-2 w-100\">\n <b className=\"mt-1\">Add a comment</b>\n {Compose}\n <div className=\"d-flex gap-2 align-content-center justify-content-end\">\n <Widget\n src={`devhub.near/widget/devhub.components.molecule.Button`}\n props={{\n label: isTxnCreated ? LoadingButtonSpinner : \"Comment\",\n [\"data-testid\"]: \"compose-comment\",\n disabled: !comment || isTxnCreated,\n classNames: { root: \"green-btn btn-sm\" },\n onClick: () => {\n composeData();\n },\n }}\n />\n </div>\n </div>\n </div>\n);\n" }, "components.admin.AnnouncementConfigurator": { "": "const { Tile } = VM.require(\n `devhub.near/widget/devhub.components.molecule.Tile`\n) || { Tile: () => <></> };\nconst item = {\n path: `forum.potlock.near/profile/**`,\n};\nconst profile = Social.get(item.path);\nif (!profile.announcement) {\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}\nconst initialData = profile.announcement;\nconst [content, setContent] = useState(null);\nconst [showCommentToast, setCommentToast] = useState(false);\nconst [handler, setHandler] = useState(null);\nconst [isTxnCreated, setTxnCreated] = useState(false);\nconst Container = styled.div`\n width: 100%;\n margin: 0 auto;\n padding: 20px;\n text-align: left;\n`;\nconst hasDataChanged = () => {\n return content !== initialData;\n};\nconst handlePublish = () => {\n setTxnCreated(true);\n Near.call([\n {\n contractName: \"forum.potlock.near\",\n methodName: \"set_social_db_profile_announcement\",\n args: { announcement: content },\n gas: 270000000000000,\n },\n ]);\n};\nuseEffect(() => {\n if (isTxnCreated) {\n const checkForAnnouncementInSocialDB = () => {\n Near.asyncView(REPL_SOCIAL_CONTRACT, \"get\", {\n keys: [item.path],\n }).then((result) => {\n try {\n const submittedAboutText = content;\n const lastAboutTextFromSocialDB =\n result[\"forum.potlock.near\"].profile.description;\n if (submittedAboutText === lastAboutTextFromSocialDB) {\n setTxnCreated(false);\n setCommentToast(true);\n return;\n }\n } catch (e) {}\n setTimeout(() => checkForAnnouncementInSocialDB(), 2000);\n });\n };\n checkForAnnouncementInSocialDB();\n }\n}, [isTxnCreated]);\nuseEffect(() => {\n if (!content && initialData) {\n setContent(initialData);\n setHandler(\"update\");\n }\n}, [initialData]);\nfunction Preview() {\n return (\n <Tile className=\"p-3\" style={{ background: \"white\" }}>\n <Widget\n src={`bos.forum.potlock.near/widget/components.molecule.Markdown`}\n props={{\n content: content,\n }}\n />\n </Tile>\n );\n}\nreturn (\n <Container>\n <Widget\n src={`near/widget/DIG.Toast`}\n props={{\n title: \"Announcement updated successfully\",\n type: \"success\",\n open: showCommentToast,\n onOpenChange: (v) => setCommentToast(v),\n trigger: <></>,\n providerProps: { duration: 3000 },\n }}\n />\n <ul className=\"nav nav-tabs\" id=\"editPreviewTabs\" role=\"tablist\">\n <li className=\"nav-item\" role=\"presentation\">\n <button\n className=\"nav-link active\"\n id=\"edit-tab\"\n data-bs-toggle=\"tab\"\n data-bs-target=\"#edit\"\n type=\"button\"\n role=\"tab\"\n aria-controls=\"edit\"\n aria-selected=\"true\"\n >\n Edit\n </button>\n </li>\n <li className=\"nav-item\" role=\"presentation\">\n <button\n className=\"nav-link\"\n id=\"preview-tab\"\n data-bs-toggle=\"tab\"\n data-bs-target=\"#preview\"\n type=\"button\"\n role=\"tab\"\n aria-controls=\"preview\"\n aria-selected=\"false\"\n >\n Preview\n </button>\n </li>\n </ul>\n <div className=\"tab-content\" id=\"editPreviewTabsContent\">\n <div\n className=\"tab-pane show active py-4\"\n id=\"edit\"\n role=\"tabpanel\"\n aria-labelledby=\"edit-tab\"\n >\n <Widget\n src={`bos.forum.potlock.near/widget/components.molecule.SimpleMDE`}\n props={{\n data: { handler: handler, content: content },\n onChangeKeyup: (v) => {\n setContent(v);\n },\n showAutoComplete: true,\n }}\n />\n <div\n className={\"d-flex align-items-center justify-content-end gap-3 mt-4\"}\n >\n <Widget\n src={`devhub.near/widget/devhub.components.molecule.Button`}\n props={{\n classNames: { root: \"btn-success\" },\n disabled: !hasDataChanged(),\n icon: {\n type: \"bootstrap_icon\",\n variant: \"bi-check-circle-fill\",\n },\n label: \"Publish\",\n onClick: handlePublish,\n }}\n />\n </div>\n </div>\n <div\n className=\"tab-pane\"\n id=\"preview\"\n role=\"tabpanel\"\n aria-labelledby=\"preview-tab\"\n style={{ position: \"relative\" }}\n >\n <div className=\"w-100 h-100 py-4\">\n <Preview />\n </div>\n </div>\n </div>\n </Container>\n);\n" }, "components.rfps.Feed": { "": "const { fetchGraphQL } = VM.require(\n `bos.forum.potlock.near/widget/core.common`\n) || { fetchGraphQL: () => {} };\nconst { href } = VM.require(`devhub.near/widget/core.lib.url`);\nhref || (href = () => {});\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 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 .card.no-border {\n border-left: none !important;\n border-right: none !important;\n margin-bottom: -3.5rem;\n }\n @media screen and (max-width: 768px) {\n font-size: 13px;\n }\n .text-sm {\n font-size: 13px;\n }\n .bg-blue {\n background-image: linear-gradient(to bottom, #4b7a93, #213236);\n color: white;\n }\n .border-bottom {\n border-bottom: 1px solid grey;\n }\n .cursor-pointer {\n cursor: pointer;\n }\n .rfp-card {\n border-left: none !important;\n border-right: none !important;\n border-bottom: none !important;\n &:hover {\n background-color: #f4f4f4;\n }\n }\n .blue-btn {\n background-color: #3c697d !important;\n border: none;\n color: white;\n &:active {\n color: white;\n }\n }\n .bg-grey {\n background: #e2e6ec;\n }\n @media screen and (max-width: 768px) {\n .blue-btn {\n padding: 0.5rem 0.8rem !important;\n min-height: 32px;\n }\n }\n a.no-space {\n display: inline-block;\n }\n .text-wrap {\n overflow: hidden;\n white-space: normal;\n }\n .fw-semi-bold {\n font-weight: 500;\n }\n`;\nconst Heading = styled.div`\n font-size: 24px;\n font-weight: 700;\n width: 100%;\n .text-normal {\n font-weight: normal !important;\n }\n @media screen and (max-width: 768px) {\n font-size: 18px;\n }\n`;\nconst rfpLabelOptions = getGlobalLabels();\nconst FeedItem = ({ rfp, index }) => {\n const accountId = rfp.author_id;\n const profile = Social.get(`${accountId}/profile/**`, \"final\");\n // We will have to get the rfp from the contract to get the block height.\n const blockHeight = parseInt(rfp.social_db_post_block_height);\n const item = {\n type: \"social\",\n path: `forum.potlock.near/post/main`,\n blockHeight: blockHeight,\n };\n return (\n <a\n href={href({\n widgetSrc: `bos.forum.potlock.near/widget/app`,\n params: {\n page: \"rfp\",\n id: rfp.rfp_id,\n },\n })}\n onClick={(e) => e.stopPropagation()}\n style={{ textDecoration: \"none\" }}\n >\n <div\n className={\n \"rfp-card d-flex justify-content-between gap-2 text-muted cursor-pointer p-3 w-100 flex-wrap flex-sm-nowrap \" +\n (index !== 0 && \" border\")\n }\n >\n <div className=\"d-flex gap-4 w-100\">\n <Widget\n src={`bos.forum.potlock.near/widget/components.molecule.Profile`}\n props={{\n accountId: rfp.author_id,\n }}\n />\n <div className=\"d-flex flex-column gap-2 w-100 text-wrap\">\n <div className=\"d-flex gap-2 align-items-center flex-wrap w-100\">\n <div className=\"h6 mb-0 text-black\">{rfp.name}</div>\n <Widget\n src={`bos.forum.potlock.near/widget/components.molecule.MultiSelectCategoryDropdown`}\n props={{\n selected: rfp.labels,\n disabled: true,\n hideDropdown: true,\n onChange: () => {},\n availableOptions: rfpLabelOptions,\n }}\n />\n </div>\n <div className=\"d-flex gap-2 align-items-center flex-wrap flex-sm-nowrap text-sm w-100\">\n <div>#{rfp.rfp_id} ・ Created</div>\n <Widget\n src={`near/widget/TimeAgo`}\n props={{\n blockHeight,\n blockTimestamp: rfp.timestamp,\n }}\n />\n </div>\n <div className=\"d-flex gap-4 flex-wrap flex-sm-nowrap text-sm w-100 text-muted my-2\">\n <div\n className=\"d-flex flex-column gap-1\"\n style={{ maxWidth: \"70%\" }}\n >\n <div className=\"fw-semi-bold\">Summay</div>\n <div>{rfp.summary}</div>\n </div>\n <div style={{ width: \"1px\" }} className=\"bg-grey\"></div>\n <div className=\"d-flex flex-column gap-1\">\n <div className=\"fw-semi-bold\">Submission Deadline</div>\n <h6 className=\"mb-0 text-black\">\n {readableDate(rfp.submission_deadline / 1000000)}\n </h6>\n </div>\n </div>\n <div className=\"d-flex gap-2 align-items-center text-sm\">\n <div>\n <img\n src=\"https://ipfs.near.social/ipfs/bafkreif4p376f3qvpb2ewwsmi6fkcm3jalhuuzuxbgvehgl552agqw47ju\"\n height={30}\n width={30}\n />\n {rfp.linked_proposals.length ?? 0}\n proposals\n </div>\n <div className=\"d-flex align-items-center\">\n <Widget\n src={`devhub.near/widget/devhub.entity.proposal.CommentIcon`}\n props={{\n item,\n showOverlay: false,\n onClick: () => {},\n }}\n />\n comments\n </div>\n </div>\n </div>\n </div>\n <div className=\"align-self-center\" style={{ minWidth: \"fit-content\" }}>\n <Widget\n src={`bos.forum.potlock.near/widget/components.rfps.StatusTag`}\n props={{\n timelineStatus: rfp.timeline.status,\n }}\n />\n </div>\n </div>\n </a>\n );\n};\nconst getRfp = (rfp_id) => {\n return Near.asyncView(\"forum.potlock.near\", \"get_rfp\", {\n rfp_id,\n });\n};\nconst FeedPage = () => {\n State.init({\n data: [],\n cachedItems: {},\n stage: \"\",\n sort: \"\",\n label: \"\",\n input: \"\",\n loading: false,\n loadingMore: false,\n aggregatedCount: null,\n currentlyDisplaying: 0,\n isFiltered: false,\n });\n const queryName =\n \"bos_forum_potlock_near_ai_pgf_indexer_rfps_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: {rfp_id: desc}\n where: $where\n ) {\n author_id\n block_height\n name\n summary\n editor_id\n rfp_id\n timeline\n views\n labels\n submission_deadline\n linked_proposals\n }\n ${queryName}_aggregate(\n order_by: {rfp_id: desc}\n where: $where\n ) {\n aggregate {\n count\n }\n }\n }`;\n function separateNumberAndText(str) {\n const numberRegex = /\\d+/;\n if (numberRegex.test(str)) {\n const number = str.match(numberRegex)[0];\n const text = str.replace(numberRegex, \"\").trim();\n return { number: parseInt(number), text };\n } else {\n return { number: null, text: str.trim() };\n }\n }\n const buildWhereClause = () => {\n let where = {};\n if (state.label) {\n where = { labels: { _contains: state.label }, ...where };\n }\n if (state.stage) {\n // timeline is stored as jsonb\n where = {\n timeline: { _cast: { String: { _regex: `${state.stage}` } } },\n ...where,\n };\n }\n if (state.input) {\n const { number, text } = separateNumberAndText(state.input);\n if (number) {\n where = { rfp_id: { _eq: number }, ...where };\n }\n if (text) {\n where = {\n _or: [\n { name: { _iregex: `${text}` } },\n { summary: { _iregex: `${text}` } },\n { description: { _iregex: `${text}` } },\n ],\n ...where,\n };\n }\n }\n State.update({ isFiltered: Object.keys(where).length > 0 });\n return where;\n };\n const buildOrderByClause = () => {\n /**\n * TODO\n * Most commented -> edit contract and indexer\n * Unanswered -> 0 comments\n */\n };\n const makeMoreItems = () => {\n if (state.aggregatedCount <= state.currentlyDisplaying) return;\n fetchRfps(state.data.length);\n };\n const fetchRfps = (offset) => {\n if (!offset) {\n offset = 0;\n }\n if (state.loading) return;\n const FETCH_LIMIT = 10;\n const variables = {\n limit: FETCH_LIMIT,\n offset,\n where: buildWhereClause(),\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 totalResult = result.body.data?.[`${queryName}_aggregate`];\n State.update({ aggregatedCount: totalResult.aggregate.count });\n // Parse timeline\n fetchBlockHeights(data, offset);\n }\n }\n });\n };\n const renderItem = (item, index) => (\n <div\n key={item.rfp_id}\n className={\n (index !== state.data.length - 1 && \"border-bottom \") + index === 0 &&\n \" rounded-top-2 rfp-item-container\"\n }\n >\n <FeedItem rfp={item} index={index} />\n </div>\n );\n const cachedRenderItem = (item, index) => {\n if (props.term) {\n return renderItem(item, {\n searchKeywords: [props.term],\n });\n }\n const key = JSON.stringify(item);\n if (!(key in state.cachedItems)) {\n state.cachedItems[key] = renderItem(item, index);\n State.update();\n }\n return state.cachedItems[key];\n };\n useEffect(() => {\n fetchRfps();\n }, [state.input, state.sort, state.label, state.stage]);\n const mergeItems = (newItems) => {\n const items = [\n ...new Set([...newItems, ...state.data].map((i) => JSON.stringify(i))),\n ].map((i) => JSON.parse(i));\n // Sorting in the front end\n if (state.sort === \"rfp_id\" || state.sort === \"\") {\n items.sort((a, b) => b.rfp_id - a.rfp_id);\n } else if (state.sort === \"views\") {\n items.sort((a, b) => b.views - a.views);\n }\n return items;\n };\n const fetchBlockHeights = (data, offset) => {\n let promises = data.map((item) => getRfp(item.rfp_id));\n Promise.all(promises).then((blockHeights) => {\n data = data.map((item, index) => ({\n ...item,\n timeline: JSON.parse(item.timeline),\n social_db_post_block_height:\n blockHeights[index].social_db_post_block_height,\n }));\n if (offset) {\n let newData = mergeItems(data);\n State.update({\n data: newData,\n currentlyDisplaying: newData.length,\n loading: false,\n });\n } else {\n State.update({\n data,\n currentlyDisplaying: data.length,\n loading: false,\n });\n }\n });\n };\n const loader = (\n <div className=\"d-flex justify-content-center align-items-center w-100\">\n <Widget src={`devhub.near/widget/devhub.components.molecule.Spinner`} />\n </div>\n );\n const renderedItems = state.data ? state.data.map(cachedRenderItem) : null;\n const isAllowedToWriteRfp = Near.view(\n \"forum.potlock.near\",\n \"is_allowed_to_write_rfps\",\n {\n editor: context.accountId,\n }\n );\n return (\n <Container className=\"w-100 py-4 px-2 d-flex flex-column gap-3\">\n <div className=\"d-flex justify-content-between flex-wrap gap-2 align-items-center\">\n <Heading>\n RFPs\n <span className=\"text-muted text-normal\">\n ({state.aggregatedCount ?? state.data.length}){\" \"}\n </span>\n </Heading>\n <div className=\"d-flex flex-wrap gap-4 align-items-center\">\n <Widget\n src={`devhub.near/widget/devhub.feature.proposal-search.by-input`}\n props={{\n search: state.input,\n className: \"w-xs-100\",\n onSearch: (input) => {\n State.update({ input });\n fetchRfps();\n },\n onEnter: () => {\n fetchRfps();\n },\n }}\n />\n <Widget\n src={`devhub.near/widget/devhub.feature.proposal-search.by-sort`}\n props={{\n onStateChange: (select) => {\n State.update({ sort: select.value });\n },\n }}\n />\n <Widget\n src={`bos.forum.potlock.near/widget/components.molecule.FilterByLabel`}\n props={{\n onStateChange: (select) => {\n State.update({ label: select.value });\n },\n availableOptions: rfpLabelOptions,\n }}\n />\n <div className=\"d-flex gap-4 align-items-center\">\n <Widget\n src={`bos.forum.potlock.near/widget/components.rfps.StageDropdown`}\n props={{\n onStateChange: (select) => {\n State.update({ stage: select.value });\n },\n }}\n />\n </div>\n </div>\n {isAllowedToWriteRfp && (\n <div className=\"mt-2 mt-xs-0\">\n <Link\n to={href({\n widgetSrc: `bos.forum.potlock.near/widget/app`,\n params: { page: \"create-rfp\" },\n })}\n >\n <Widget\n src={`devhub.near/widget/devhub.components.molecule.Button`}\n props={{\n label: (\n <div className=\"d-flex gap-2 align-items-center\">\n <div>\n <i className=\"bi bi-plus-circle-fill\"></i>\n </div>\n Create RFP\n </div>\n ),\n classNames: { root: \"blue-btn\" },\n }}\n />\n </Link>\n </div>\n )}\n </div>\n <div style={{ minHeight: \"50vh\" }}>\n {!Array.isArray(state.data) ? (\n loader\n ) : (\n <div className=\"card no-border rounded-0 mt-4 py-3 full-width-div\">\n <div className=\"container-xl\">\n <div>\n <Widget\n src={`bos.forum.potlock.near/widget/components.pages.announcement`}\n loading=\"\"\n />\n </div>\n <div className=\"mt-4 border rounded-2\">\n {state.aggregatedCount === 0 ? (\n <div className=\"m-2\">\n {state.isFiltered ? (\n <div class=\"alert alert-danger\" role=\"alert\">\n No RFP found for selected filter.\n </div>\n ) : (\n <div class=\"alert alert-secondary\" role=\"alert\">\n No RFP has been created yet.\n </div>\n )}\n </div>\n ) : state.aggregatedCount > 0 ? (\n <InfiniteScroll\n pageStart={0}\n loadMore={makeMoreItems}\n hasMore={state.aggregatedCount > state.data.length}\n loader={loader}\n useWindow={false}\n threshold={100}\n >\n {renderedItems}\n </InfiniteScroll>\n ) : (\n loader\n )}\n </div>\n </div>\n </div>\n )}\n </div>\n </Container>\n );\n};\nreturn FeedPage(props);\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://near.social/near/widget/ProfilePage?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.LinkedProposalsDropdown": { "": "const { fetchGraphQL } = VM.require(\n `bos.forum.potlock.near/widget/core.common`\n) || { fetchGraphQL: () => {} };\nconst { href } = VM.require(`devhub.near/widget/core.lib.url`);\nhref || (href = () => {});\nconst linkedProposals = props.linkedProposals;\nconst onChange = props.onChange;\nconst [selectedProposals, setSelectedProposals] = useState(linkedProposals);\nconst [proposalsOptions, setProposalsOptions] = useState([]);\nconst [searchProposalId, setSearchProposalId] = useState(\"\");\nconst queryName =\n \"bos_forum_potlock_near_ai_pgf_indexer_proposals_with_latest_snapshot\";\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: {proposal_id: desc}\n where: $where\n) {\n name\n proposal_id\n}\n}`;\nuseEffect(() => {\n if (JSON.stringify(linkedProposals) !== JSON.stringify(selectedProposals)) {\n setSelectedProposals(linkedProposals);\n }\n}, [linkedProposals]);\nuseEffect(() => {\n if (JSON.stringify(linkedProposals) !== JSON.stringify(selectedProposals)) {\n onChange(selectedProposals);\n }\n}, [selectedProposals]);\nfunction separateNumberAndText(str) {\n const numberRegex = /\\d+/;\n if (numberRegex.test(str)) {\n const number = str.match(numberRegex)[0];\n const text = str.replace(numberRegex, \"\").trim();\n return { number: parseInt(number), text };\n } else {\n return { number: null, text: str.trim() };\n }\n}\nconst buildWhereClause = () => {\n let where = {};\n const { number, text } = separateNumberAndText(searchProposalId);\n if (number) {\n where = { proposal_id: { _eq: number }, ...where };\n }\n if (text) {\n where = {\n _or: [\n { name: { _iregex: `${text}` } },\n { summary: { _iregex: `${text}` } },\n { description: { _iregex: `${text}` } },\n ],\n ...where,\n };\n }\n return where;\n};\nconst fetchProposals = () => {\n const FETCH_LIMIT = 30;\n const variables = {\n limit: FETCH_LIMIT,\n offset: 0,\n where: buildWhereClause(),\n };\n fetchGraphQL(query, \"GetLatestSnapshot\", variables).then(async (result) => {\n if (result.status === 200) {\n if (result.body.data) {\n const proposalsData = result.body.data?.[queryName];\n const data = [];\n for (const prop of proposalsData) {\n data.push({\n label: \"# \" + prop.proposal_id + \" : \" + prop.name,\n value: prop.proposal_id,\n });\n }\n setProposalsOptions(data);\n }\n }\n });\n};\nuseEffect(() => {\n fetchProposals();\n}, [searchProposalId]);\nreturn (\n <>\n {selectedProposals.map((proposal) => {\n return (\n <div className=\"d-flex gap-2 align-items-center\">\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: proposal.value,\n },\n })}\n target=\"_blank\"\n rel=\"noopener noreferrer\"\n >\n {proposal.label}\n </a>\n <div\n className=\"cursor-pointer\"\n onClick={() => {\n const updatedLinkedProposals = selectedProposals.filter(\n (item) => item.value !== proposal.value\n );\n setSelectedProposals(updatedLinkedProposals);\n }}\n >\n <i className=\"bi bi-trash3-fill\"></i>\n </div>\n </div>\n );\n })}\n <Widget\n src={`bos.forum.potlock.near/widget/components.molecule.DropDownWithSearch`}\n props={{\n selectedValue: selectedProposals,\n onChange: (v) => {\n if (!selectedProposals.some((item) => item.value === v.value)) {\n setSelectedProposals([...selectedProposals, v]);\n }\n },\n options: proposalsOptions,\n showSearch: true,\n searchInputPlaceholder: \"Search by Id\",\n defaultLabel: \"Search proposals\",\n searchByValue: true,\n onSearch: (value) => {\n setSearchProposalId(value);\n },\n }}\n />\n </>\n);\n" }, "components.admin.AccountsEditor": { "": "const { data, setList, validate, invalidate } = props;\nconst [newItem, setNewItem] = useState(\"\");\nconst handleAddItem = () => {\n if (validate(newItem)) {\n setList([...data.list, newItem]);\n setNewItem(\"\");\n } else {\n return invalidate();\n }\n};\nconst handleDeleteItem = (index) => {\n const updatedData = [...data.list];\n updatedData.splice(index, 1);\n setList(updatedData);\n};\nconst Item = styled.div`\n padding: 10px;\n margin: 5px;\n display: flex;\n align-items: center;\n flex-direction: row;\n gap: 10px;\n`;\nreturn (\n <>\n {data.list.map((item, index) => (\n <Item key={index}>\n <div className=\"flex-grow-1\">\n <Widget\n src={`devhub.near/widget/devhub.components.molecule.Input`}\n props={{\n className: \"flex-grow-1\",\n value: item,\n skipPaddingGap: true,\n placeholder: data.placeholder,\n inputProps: {\n prefix: data.prefix,\n disabled: true,\n },\n }}\n />\n </div>\n <button\n className=\"btn btn-outline-danger\"\n onClick={() => handleDeleteItem(index)}\n >\n <i className=\"bi bi-trash-fill\" />\n </button>\n </Item>\n ))}\n {data.list.length < data.maxLength && (\n <Item>\n <div className=\"flex-grow-1\">\n <Widget\n src={`bos.forum.potlock.near/widget/components.molecule.AccountInput`}\n props={{\n onUpdate: (value) => setNewItem(value),\n value: newItem,\n placeholder: data.placeholder,\n }}\n />\n </div>\n <button\n className=\"btn btn-success add-member\"\n onClick={handleAddItem}\n disabled={newItem === \"\"}\n data-testid=\"add-to-list\"\n >\n <i className=\"bi bi-plus\" />\n </button>\n </Item>\n )}\n </>\n);\n" }, "components.molecule.MultiSelectCategoryDropdown": { "": "const { href } = VM.require(`devhub.near/widget/core.lib.url`);\nhref || (href = () => {});\nconst {\n selected,\n onChange,\n disabled,\n availableOptions,\n hideDropdown,\n linkedRfp,\n} = props;\nconst [selectedOptions, setSelectedOptions] = useState([]);\nconst [isOpen, setIsOpen] = useState(false);\nconst [initialStateApplied, setInitialState] = useState(false);\nconst toggleDropdown = () => {\n setIsOpen(!isOpen);\n};\nuseEffect(() => {\n if (JSON.stringify(selectedOptions) !== JSON.stringify(selected)) {\n if (availableOptions.length > 0) {\n if ((selected ?? []).some((i) => !i.value)) {\n setSelectedOptions(\n selected.map((i) => availableOptions.find((t) => t.value === i))\n );\n } else {\n setSelectedOptions(selected);\n }\n setInitialState(true);\n }\n } else {\n setInitialState(true);\n }\n}, [selected, availableOptions]);\nuseEffect(() => {\n if (\n JSON.stringify(selectedOptions) !== JSON.stringify(selected) &&\n initialStateApplied\n ) {\n onChange(selectedOptions);\n }\n}, [selectedOptions, initialStateApplied]);\nconst Container = styled.div`\n .drop-btn {\n width: 100%;\n text-align: left;\n padding-inline: 10px;\n }\n .dropdown-toggle:after {\n position: absolute;\n top: 46%;\n right: 2%;\n }\n .dropdown-menu {\n width: 100%;\n }\n .dropdown-item.active,\n .dropdown-item:active {\n background-color: #f0f0f0 !important;\n color: black;\n }\n .disabled {\n background-color: #f8f8f8 !important;\n cursor: not-allowed !important;\n border-radius: 5px;\n opacity: inherit !important;\n }\n .disabled.dropdown-toggle::after {\n display: none !important;\n }\n .custom-select {\n position: relative;\n }\n .selected {\n background-color: #f0f0f0;\n }\n .cursor-pointer {\n cursor: pointer;\n }\n .text-wrap {\n overflow: hidden;\n white-space: normal;\n }\n`;\nconst handleOptionClick = (option) => {\n if (!selectedOptions.some((item) => item.value === option.value)) {\n setSelectedOptions([...selectedOptions, option]);\n }\n setIsOpen(false);\n};\nconst Item = ({ option }) => {\n return <div> {option.title}</div>;\n};\nreturn (\n <>\n <div className=\"d-flex gap-2 align-items-center\">\n {(selectedOptions ?? []).map((option) => {\n return (\n <div\n style={{\n color: \"white\",\n backgroundColor: `rgb(${option.color})`,\n width: \"max-content\",\n }}\n className=\"d-flex gap-2 align-items-center badge rounded-lg p-2 h6 mb-0\"\n >\n {option.title}\n {!disabled && (\n <div\n className=\"cursor-pointer\"\n onClick={() => {\n const updatedOptions = selectedOptions.filter(\n (item) => item.value !== option.value\n );\n setSelectedOptions(updatedOptions);\n }}\n >\n <i className=\"bi bi-trash3-fill\"></i>\n </div>\n )}\n </div>\n );\n })}\n </div>\n {!hideDropdown && (\n <Container>\n <div\n className=\"custom-select w-100\"\n tabIndex=\"0\"\n onBlur={() => setIsOpen(false)}\n >\n <div\n className={\n \"dropdown-toggle bg-white border rounded-2 btn drop-btn w-100 \" +\n (disabled ? \"disabled\" : \"\")\n }\n onClick={!disabled && toggleDropdown}\n >\n <div className={`selected-option`}>\n {linkedRfp ? (\n <span className=\"text-sm d-flex gap-2 align-items-center\">\n <i class=\"bi bi-lock-fill\"></i>\n These categories match the chosen RFP and cannot be changed.\n To use different categories, unlink the RFP.\n </span>\n ) : (\n <span>Select Category </span>\n )}\n </div>\n </div>\n {isOpen && (\n <div className=\"dropdown-menu rounded-2 dropdown-menu-end dropdown-menu-lg-start px-2 shadow show w-100\">\n <div>\n {(availableOptions ?? []).map((option) => (\n <div\n key={option.value}\n className={`dropdown-item cursor-pointer w-100 my-1 ${\n (selectedOptions ?? []).find(\n (item) => item.value === option.value\n )\n ? \"selected\"\n : \"\"\n }`}\n onClick={() => handleOptionClick(option)}\n >\n <Item option={option} />\n </div>\n ))}\n </div>\n </div>\n )}\n </div>\n </Container>\n )}\n </>\n);\n" }, "components.molecule.LikeButton": { "": "const item = props.item;\nconst proposalId = props.proposalId;\nconst rfpId = props.rfpId;\nconst notifyAccountIds = props.notifyAccountIds ?? [];\nif (!item) {\n return \"\";\n}\nconst likes = Social.index(\"like\", item);\nconst dataLoading = likes === null;\nconst likesByUsers = {};\n(likes || []).forEach((like) => {\n if (like.value.type === \"like\") {\n likesByUsers[like.accountId] = like;\n } else if (like.value.type === \"unlike\") {\n delete likesByUsers[like.accountId];\n }\n});\nif (state.hasLike === true) {\n likesByUsers[context.accountId] = {\n accountId: context.accountId,\n };\n} else if (state.hasLike === false) {\n delete likesByUsers[context.accountId];\n}\nconst accountsWithLikes = Object.keys(likesByUsers);\nconst hasLike = context.accountId && !!likesByUsers[context.accountId];\nconst hasLikeOptimistic =\n state.hasLikeOptimistic === undefined ? hasLike : state.hasLikeOptimistic;\nconst totalLikes =\n accountsWithLikes.length +\n (hasLike === false && state.hasLikeOptimistic === true ? 1 : 0) -\n (hasLike === true && state.hasLikeOptimistic === false ? 1 : 0);\nconst LikeButton = styled.button`\n border: 0;\n display: inline-flex;\n align-items: center;\n gap: 6px;\n color: #687076;\n font-weight: 400;\n font-size: 14px;\n line-height: 17px;\n cursor: pointer;\n background: none;\n padding: 6px;\n transition: color 200ms;\n i {\n font-size: 16px;\n transition: color 200ms;\n &.bi-heart-fill {\n color: #e5484d !important;\n }\n }\n &:hover,\n &:focus {\n outline: none;\n color: #11181c;\n }\n`;\nconst likeClick = (e) => {\n e.preventDefault();\n e.stopPropagation();\n if (state.loading) {\n return;\n }\n State.update({\n loading: true,\n hasLikeOptimistic: !hasLike,\n });\n const data = {\n index: {\n like: JSON.stringify({\n key: item,\n value: {\n type: hasLike ? \"unlike\" : \"like\",\n },\n }),\n },\n };\n if (!hasLike && notifyAccountIds.length > 0) {\n const notifyData = notifyAccountIds.map((account) => {\n if (account !== context.accountId) {\n return {\n key: account,\n value: proposalId\n ? {\n type: \"proposal/like\",\n item,\n proposal: proposalId,\n widgetAccountId: \"bos.forum.potlock.near\",\n }\n : {\n type: \"rfp/like\",\n item,\n rfp: rfpId,\n widgetAccountId: \"bos.forum.potlock.near\",\n },\n };\n }\n });\n if (notifyData.length > 0) {\n data.index.notify = notifyData;\n }\n }\n Social.set(data, {\n onCommit: () => State.update({ loading: false, hasLike: !hasLike }),\n onCancel: () =>\n State.update({\n loading: false,\n hasLikeOptimistic: !state.hasLikeOptimistic,\n }),\n });\n};\nconst title = hasLike ? \"Unlike\" : \"Like\";\nreturn (\n <LikeButton\n disabled={state.loading || dataLoading || !context.accountId}\n title={title}\n onClick={likeClick}\n >\n <i className={`${hasLikeOptimistic ? \"bi-heart-fill\" : \"bi-heart\"}`} />\n {Object.values(likesByUsers ?? {}).length > 0 ? (\n <span className={`count ${hasLike ? \"liked\" : \"\"}`}>\n <Widget\n loading={likeCount || \"\"}\n src=\"mob.near/widget/N.Overlay.Faces\"\n props={{ accounts: likesByUsers, limit: 10 }}\n />\n </span>\n ) : (\n \"0\"\n )}\n </LikeButton>\n);\n" }, "components.molecule.NavbarDropdown": { "": "const title = props.title;\nconst links = props.links;\nconst href = props.href;\nconst [showMenu, setShowMenu] = useState(false);\nconst { href: linkHref } = VM.require(`devhub.near/widget/core.lib.url`);\nlinkHref || (linkHref = () => {});\nconst Dropdown = styled.div`\n position: relative;\n display: flex;\n flex-direction: column;\n align-items: center;\n p {\n &.active {\n color: #fff;\n &:hover {\n text-decoration: none;\n color: #096d50 !important;\n }\n }\n }\n`;\nconst DropdownMenu = styled.div`\n z-index: 50;\n position: absolute;\n top: 2.25rem;\n &.active {\n padding: 0.5rem 1rem;\n padding-top: 1rem;\n border-radius: 1rem;\n background: rgba(217, 217, 217, 0.7);\n backdrop-filter: blur(5px);\n width: max-content;\n animation: slide-down 300ms ease;\n transform-origin: top center;\n }\n @keyframes slide-down {\n 0% {\n transform: scaleY(0);\n }\n 100% {\n transform: scaleY(1);\n }\n }\n`;\nconst DropdownLink = styled.div`\n color: inherit;\n text-decoration: none;\n &.active {\n color: #555555;\n }\n &:hover {\n text-decoration: none;\n color: #096d50 !important;\n }\n`;\nconst isOutsideLink = (href ?? \"\").includes(\"https\");\nreturn (\n <Dropdown\n onMouseEnter={() => setShowMenu(true)}\n onMouseLeave={() => setShowMenu(false)}\n >\n {href ? (\n <DropdownLink className={href === props.page && \"active\"} href={href}>\n <Link\n style={{ textDecoration: \"none\" }}\n to={\n isOutsideLink\n ? href\n : linkHref({\n widgetSrc: `bos.forum.potlock.near/widget/app`,\n params: { page: href },\n })\n }\n target={isOutsideLink ? \"_blank\" : \"\"}\n >\n {title}\n </Link>\n </DropdownLink>\n ) : (\n <p className={`m-0 py-2 nav-dropdown`} style={{ cursor: \"default\" }}>\n {title} ↓\n </p>\n )}\n {showMenu && links.length !== 0 && (\n <DropdownMenu className={`${showMenu && \"active\"}`}>\n <div className=\"d-flex flex-column gap-3\">\n {links.map((link) => (\n // Check if the link is external\n <DropdownLink\n className={link.href === props.page && \"active\"}\n key={`${link.title}-${link.href}`}\n >\n {link.href.startsWith(\"http://\") ||\n link.href.startsWith(\"https://\") ? (\n // External link: Render an <a> tag\n <a\n href={link.href}\n style={{ textDecoration: \"none\" }}\n target=\"_blank\"\n rel=\"noopener noreferrer\"\n >\n {link.title}\n </a>\n ) : (\n // Internal link: Render the <Link> component\n <Link\n style={{ textDecoration: \"none\" }}\n to={linkHref({\n widgetSrc: `bos.forum.potlock.near/widget/app`,\n params: { page: link.href },\n })}\n >\n {link.title}\n </Link>\n )}\n </DropdownLink>\n ))}\n </div>\n </DropdownMenu>\n )}\n </Dropdown>\n);\n" }, "components.rfps.ConfirmCancelModal": { "": "const { CANCEL_RFP_OPTIONS } = VM.require(\n `bos.forum.potlock.near/widget/core.common`\n) || { CANCEL_RFP_OPTIONS: {} };\nconst isOpen = props.isOpen;\nconst onCancelClick = props.onCancelClick;\nconst onConfirmClick = props.onConfirmClick;\nconst linkedProposalIds = props.linkedProposalIds;\nconst Modal = styled.div`\n display: ${({ hidden }) => (hidden ? \"none\" : \"flex\")};\n position: fixed;\n inset: 0;\n justify-content: center;\n align-items: center;\n opacity: 1;\n z-index: 999;\n .black-btn {\n background-color: #000 !important;\n border: none;\n color: white;\n &:active {\n color: white;\n }\n }\n @media screen and (max-width: 768px) {\n h5 {\n font-size: 16px !important;\n }\n }\n .btn {\n font-size: 14px;\n }\n .bg-grey {\n background: rgb(244, 244, 244) !important;\n max-height: 300px;\n overflow-y: auto;\n }\n`;\nconst ModalBackdrop = styled.div`\n position: absolute;\n inset: 0;\n background-color: rgba(0, 0, 0, 0.5);\n opacity: 0.4;\n`;\nconst ModalDialog = styled.div`\n padding: 2em;\n z-index: 999;\n overflow-y: auto;\n max-height: 85%;\n margin-top: 5%;\n width: 50%;\n @media screen and (max-width: 768px) {\n margin: 2rem;\n width: 100%;\n }\n`;\nconst ModalHeader = styled.div`\n display: flex;\n flex-direction: row;\n justify-content: space-between;\n align-items: center;\n padding-bottom: 4px;\n`;\nconst ModalFooter = styled.div`\n padding-top: 4px;\n display: flex;\n flex-direction: row;\n justify-content: space-between;\n align-items: items-center;\n`;\nconst CloseButton = styled.button`\n display: flex;\n align-items: center;\n justify-content: center;\n background-color: white;\n padding: 0.5em;\n border-radius: 6px;\n border: 0;\n color: #344054;\n &:hover {\n background-color: #d3d3d3;\n }\n`;\nconst ConfirmButton = styled.button`\n padding: 0.7em;\n border-radius: 6px;\n border: 0;\n box-shadow: 0px 1px 2px rgba(16, 24, 40, 0.05);\n background-color: #12b76a;\n color: white;\n &:hover {\n background-color: #0e9f5d;\n }\n`;\nconst ModalContent = styled.div`\n flex: 1;\n font-size: 14px;\n margin-top: 4px;\n margin-bottom: 4px;\n overflow-y: auto;\n max-height: 50%;\n @media screen and (max-width: 768px) {\n font-size: 12px !important;\n }\n`;\nconst NoButton = styled.button`\n background: transparent;\n border: none;\n padding: 0;\n margin: 0;\n box-shadow: none;\n`;\nconst [proposalStatus, setProposalStatus] = useState(null);\nconst OptionForm = useMemo(() => {\n return (\n <div className=\"d-flex flex-column gap-1 pl-2\">\n <Widget\n src={`bos.forum.potlock.near/widget/components.molecule.RadioButton`}\n props={{\n value: CANCEL_RFP_OPTIONS.CANCEL_PROPOSALS,\n label: (\n <div>\n <span className=\"fw-bold\">Option 1: </span>Cancel all linked\n proposals\n </div>\n ),\n isChecked: proposalStatus === CANCEL_RFP_OPTIONS.CANCEL_PROPOSALS,\n onClick: (v) => {\n if (v) {\n setProposalStatus(CANCEL_RFP_OPTIONS.CANCEL_PROPOSALS);\n }\n },\n }}\n />\n <Widget\n src={`bos.forum.potlock.near/widget/components.molecule.RadioButton`}\n props={{\n value: CANCEL_RFP_OPTIONS.UNLINK_PROPOSALS,\n label: (\n <div>\n <span className=\"fw-bold\">Option 2: </span> Unlink all linked\n proposals (maintain their status)\n </div>\n ),\n isChecked: proposalStatus === CANCEL_RFP_OPTIONS.UNLINK_PROPOSALS,\n onClick: (v) => {\n if (v) {\n setProposalStatus(CANCEL_RFP_OPTIONS.UNLINK_PROPOSALS);\n }\n },\n }}\n />\n <Widget\n src={`bos.forum.potlock.near/widget/components.molecule.RadioButton`}\n props={{\n value: CANCEL_RFP_OPTIONS.NONE,\n label: (\n <div>\n <span className=\"fw-bold\">Option 3: </span> Leave all linked\n proposals as they are\n </div>\n ),\n isChecked: proposalStatus === CANCEL_RFP_OPTIONS.NONE,\n onClick: (v) => {\n if (v) {\n setProposalStatus(CANCEL_RFP_OPTIONS.NONE);\n }\n },\n }}\n />\n </div>\n );\n}, [proposalStatus]);\nreturn (\n <>\n <Modal hidden={!isOpen}>\n <ModalBackdrop />\n <ModalDialog className=\"card\">\n <ModalHeader>\n <h5 className=\"mb-0\">Are you sure you want to cancel this RFP?</h5>\n </ModalHeader>\n <ModalContent className=\"text-muted d-flex flex-column gap-2\">\n The RFP status will change to “Cancelled” and it will no longer be\n active or relevant. Comments will remain open.\n <div className=\"bg-grey d-flex flex-column p-3 rounded-1 text-black\">\n <div className=\"h6\">\n Linked Proposals ({linkedProposalIds.length})\n </div>\n <Widget\n src={`bos.forum.potlock.near/widget/components.molecule.LinkedProposals`}\n props={{\n linkedProposalIds: linkedProposalIds,\n showStatus: true,\n }}\n />\n </div>\n <div className=\"text-muted d-flex flex-column gap-2\">\n <div className=\"text-lg\">\n What would you like to do with the linked proposals?\n </div>\n {OptionForm}\n </div>\n <div className=\"text-sm mt-2\">\n Note: To take specific actions on individual proposals, please\n manage them from their respective pages.\n </div>\n </ModalContent>\n <div className=\"d-flex gap-2 align-items-center justify-content-end mt-2\">\n <Widget\n src={`devhub.near/widget/devhub.components.molecule.Button`}\n props={{\n classNames: { root: \"btn-outline-secondary\" },\n label: \"Cancel\",\n onClick: onCancelClick,\n }}\n />\n <Widget\n src={`devhub.near/widget/devhub.components.molecule.Button`}\n props={{\n classNames: { root: \"btn-danger\" },\n disabled: !proposalStatus,\n label: \"Ready to Cancel\",\n onClick: () => onConfirmClick(proposalStatus),\n }}\n />\n </div>\n </ModalDialog>\n </Modal>\n </>\n);\n" }, "components.molecule.LinkedRfpDropdown": { "": "const { RFP_TIMELINE_STATUS, fetchGraphQL, parseJSON } = VM.require(\n `bos.forum.potlock.near/widget/core.common`\n) || { RFP_TIMELINE_STATUS: {}, fetchGraphQL: () => {}, parseJSON: () => {} };\nconst { href } = VM.require(`devhub.near/widget/core.lib.url`);\nhref || (href = () => {});\nconst { linkedRfp, onChange, disabled, onDeleteRfp } = props;\nconst isModerator = Near.view(\n \"forum.potlock.near\",\n \"is_allowed_to_write_rfps\",\n {\n editor: context.accountId,\n }\n);\nconst [selectedRFP, setSelectedRFP] = useState(null);\nconst [acceptingRfpsOptions, setAcceptingRfpsOption] = useState([]);\nconst [allRfpOptions, setAllRfpOptions] = useState([]);\nconst [searchRFPId, setSearchRfpId] = useState(\"\");\nconst [initialStateApplied, setInitialState] = useState(false);\nconst queryName =\n \"bos_forum_potlock_near_ai_pgf_indexer_rfps_with_latest_snapshot\";\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: {rfp_id: desc}\n where: $where\n ) {\n name\n rfp_id\n timeline\n }\n }`;\nfunction separateNumberAndText(str) {\n const numberRegex = /\\d+/;\n if (numberRegex.test(str)) {\n const number = str.match(numberRegex)[0];\n const text = str.replace(numberRegex, \"\").trim();\n return { number: parseInt(number), text };\n } else {\n return { number: null, text: str.trim() };\n }\n}\nconst buildWhereClause = () => {\n // show only accepting submissions stage rfps\n let where = {};\n const { number, text } = separateNumberAndText(searchRFPId);\n if (number) {\n where = { rfp_id: { _eq: number }, ...where };\n }\n if (text) {\n where = {\n _or: [\n { name: { _iregex: `${text}` } },\n { summary: { _iregex: `${text}` } },\n { description: { _iregex: `${text}` } },\n ],\n ...where,\n };\n }\n return where;\n};\nconst fetchRfps = () => {\n const FETCH_LIMIT = 30;\n const variables = {\n limit: FETCH_LIMIT,\n offset: 0,\n where: buildWhereClause(),\n };\n fetchGraphQL(query, \"GetLatestSnapshot\", variables).then(async (result) => {\n if (result.status === 200) {\n if (result.body.data) {\n const rfpsData = result.body.data?.[queryName];\n const data = [];\n const acceptingData = [];\n for (const prop of rfpsData) {\n const timeline = parseJSON(prop.timeline);\n const label = \"# \" + prop.rfp_id + \" : \" + prop.name;\n const value = prop.rfp_id;\n if (timeline.status === RFP_TIMELINE_STATUS.ACCEPTING_SUBMISSIONS) {\n acceptingData.push({\n label,\n value,\n });\n }\n data.push({\n label,\n value,\n });\n }\n setAcceptingRfpsOption(acceptingData);\n setAllRfpOptions(data);\n }\n }\n });\n};\nuseEffect(() => {\n fetchRfps();\n}, [searchRFPId]);\nuseEffect(() => {\n if (JSON.stringify(linkedRfp) !== JSON.stringify(selectedRFP)) {\n if (allRfpOptions.length > 0) {\n if (typeof linkedRfp !== \"object\") {\n setSelectedRFP(allRfpOptions.find((i) => linkedRfp === i.value));\n } else {\n setSelectedRFP(linkedRfp);\n }\n setInitialState(true);\n }\n } else {\n setInitialState(true);\n }\n}, [linkedRfp, allRfpOptions]);\nuseEffect(() => {\n if (\n JSON.stringify(linkedRfp) !== JSON.stringify(selectedRFP) &&\n initialStateApplied\n ) {\n onChange(selectedRFP);\n }\n}, [selectedRFP, initialStateApplied]);\nreturn (\n <>\n {selectedRFP && (\n <div className=\"d-flex gap-2 align-items-center\">\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: selectedRFP.value,\n },\n })}\n target=\"_blank\"\n rel=\"noopener noreferrer\"\n >\n {selectedRFP.label}\n </a>\n {!disabled && (\n <div\n className=\"cursor-pointer\"\n onClick={() => {\n onDeleteRfp();\n setSelectedRFP(null);\n }}\n >\n <i className=\"bi bi-trash3-fill\"></i>\n </div>\n )}\n </div>\n )}\n <Widget\n src={`bos.forum.potlock.near/widget/components.molecule.DropDownWithSearch`}\n props={{\n disabled: disabled,\n selectedValue: selectedRFP.value,\n onChange: (v) => {\n setSelectedRFP(v);\n },\n options: isModerator ? allRfpOptions : acceptingRfpsOptions,\n showSearch: true,\n searchInputPlaceholder: \"Search by Id\",\n defaultLabel: \"Search RFP\",\n searchByValue: true,\n onSearch: (value) => {\n setSearchRfpId(value);\n },\n }}\n />\n </>\n);\n" }, "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://near.social/near/widget/ProfilePage?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.molecule.LinkedProposals": { "": "const { 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 linkedProposalIds = props.linkedProposalIds ?? [];\nconst linkedProposalsData = [];\nconst showStatus = props.showStatus ?? false;\n// using contract instead of indexer, since indexer doesn't return timestamp\nlinkedProposalIds.map((item) => {\n const data = Near.view(\"forum.potlock.near\", \"get_proposal\", {\n proposal_id: item,\n });\n if (data !== null) {\n linkedProposalsData.push(data);\n }\n});\nconst Container = styled.div`\n a {\n &:hover {\n text-decoration: none !important;\n }\n }\n`;\nreturn (\n <Container className=\"d-flex flex-column gap-3\">\n {linkedProposalsData.map((item) => {\n return (\n <a\n href={href({\n widgetSrc: `bos.forum.potlock.near/widget/app`,\n params: {\n page: \"proposal\",\n id: item.id,\n },\n })}\n target=\"_blank\"\n rel=\"noopener noreferrer\"\n >\n <div className=\"d-flex gap-2\">\n <Widget\n src={`bos.forum.potlock.near/widget/components.molecule.Profile`}\n props={{\n accountId: item.snapshot.editor_id,\n }}\n />\n <div className=\"d-flex flex-column\" style={{ maxWidth: 250 }}>\n <b className=\"text-truncate\">{item.snapshot.name}</b>\n <div className=\"text-sm text-muted\">\n created on {readableDate(item.snapshot.timestamp / 1000000)}\n </div>\n {showStatus && (\n <div style={{ width: \"fit-content\" }} className=\"mt-1\">\n <Widget\n src={`devhub.near/widget/devhub.entity.proposal.StatusTag`}\n props={{\n timelineStatus: item.snapshot.timeline.status,\n }}\n />\n </div>\n )}\n </div>\n </div>\n </a>\n );\n })}\n </Container>\n);\n" }, "components.pages.announcement": { "": "const profile = Social.getr(`forum.potlock.near/profile`, \"final\", {\n subscribe: true,\n});\nif (!profile || !profile.announcement) {\n return <></>;\n}\nconst Container = styled.div`\n width: -webkit-fill-available;\n background-image: linear-gradient(to bottom, #4b7a93, #213236);\n color: white;\n a {\n color: white !important;\n }\n`;\nreturn (\n <Container className=\"p-2 px-3 rounded-3\">\n <Widget\n src={`bos.forum.potlock.near/widget/components.molecule.Markdown`}\n props={{\n content: profile.announcement,\n }}\n />\n </Container>\n);\n" }, "components.rfps.StatusTag": { "": "const { RFP_TIMELINE_STATUS } = VM.require(\n `bos.forum.potlock.near/widget/core.common`\n) || { RFP_TIMELINE_STATUS: {} };\nconst timelineStatus = props.timelineStatus;\nconst size = props.size ?? \"md\";\nconst getClassNameByStatus = () => {\n switch (timelineStatus) {\n case RFP_TIMELINE_STATUS.CANCELLED:\n return \"grey\";\n case RFP_TIMELINE_STATUS.PROPOSAL_SELECTED:\n return \"green\";\n case RFP_TIMELINE_STATUS.EVALUATION:\n return \"orange\";\n default:\n return \"black\";\n }\n};\nconst Container = styled.div`\n font-size: ${({ size }) => {\n switch (size) {\n case \"sm\":\n return \"10px\";\n case \"lg\":\n return \"14px\";\n default:\n return \"12px\";\n }\n }};\n min-width: fit-content;\n .orange-tag {\n border: 1px solid #ff7a00 !important;\n color: #ff7a00 !important;\n }\n .black-tag {\n border: 1px solid #000 !important;\n color: #000 !important;\n }\n .grey-tag {\n border: 1px solid #979797 !important;\n color: #979797 !important;\n }\n .green-tag {\n border: 1px solid #04a46e !important;\n color: #04a46e !important;\n }\n .fw-bold {\n font-weight: 600 !important;\n }\n`;\nreturn (\n <Container size={size}>\n <div className={getClassNameByStatus() + \"-tag fw-bold rounded-2 p-1 px-2\"}>\n {(timelineStatus ?? \"\").replace(\"_\", \" \")}\n </div>\n </Container>\n);\n" }, "components.core.lib.contract": { "": "function ensureOtherIsLast(arr) {\n const otherIndex = (arr ?? []).findIndex((item) => item.value === \"Other\");\n if (otherIndex !== -1) {\n const [otherItem] = arr.splice(otherIndex, 1);\n arr.push(otherItem);\n }\n return arr;\n}\nfunction getGlobalLabels() {\n let labels = Near.view(\"forum.potlock.near\", \"get_global_labels\");\n if (labels !== null) {\n labels = ensureOtherIsLast(labels);\n }\n return labels ?? null;\n}\nreturn {\n getGlobalLabels,\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 src={`devhub.near/widget/devhub.entity.proposal.LoginScreen`} />\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\">\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.proposals.Feed": { "": "const { fetchGraphQL, parseJSON, isNumber } = VM.require(\n `bos.forum.potlock.near/widget/core.common`\n) || { fetchGraphQL: () => {}, 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 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 .card.no-border {\n border-left: none !important;\n border-right: none !important;\n margin-bottom: -3.5rem;\n }\n @media screen and (max-width: 768px) {\n font-size: 13px;\n }\n .text-sm {\n font-size: 13px;\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 border-left: none !important;\n border-right: none !important;\n border-bottom: none !important;\n &:hover {\n background-color: #f4f4f4;\n }\n }\n .blue-btn {\n background-color: #3c697d !important;\n border: none;\n color: white;\n &:active {\n color: white;\n }\n }\n @media screen and (max-width: 768px) {\n .blue-btn {\n padding: 0.5rem 0.8rem !important;\n min-height: 32px;\n }\n }\n a.no-space {\n display: inline-block;\n }\n .text-wrap {\n overflow: hidden;\n white-space: normal;\n }\n .bg-blue {\n background-image: linear-gradient(to bottom, #4b7a93, #213236);\n color: white;\n }\n`;\nconst Heading = styled.div`\n font-size: 24px;\n font-weight: 700;\n width: 100%;\n .text-normal {\n font-weight: normal !important;\n }\n @media screen and (max-width: 768px) {\n font-size: 18px;\n }\n`;\nconst rfpLabelOptions = getGlobalLabels();\nconst FeedItem = ({ proposal, index }) => {\n const accountId = proposal.author_id;\n proposal.timeline = parseJSON(proposal.timeline);\n const profile = Social.get(`${accountId}/profile/**`, \"final\");\n // We will have to get the proposal from the contract to get the block height.\n const blockHeight = parseInt(proposal.social_db_post_block_height);\n const item = {\n type: \"social\",\n path: `forum.potlock.near/post/main`,\n blockHeight: blockHeight,\n };\n const isLinked = isNumber(proposal.linked_rfp);\n const rfpData = proposal.rfpData;\n return (\n <a\n href={href({\n widgetSrc: `bos.forum.potlock.near/widget/app`,\n params: {\n page: \"proposal\",\n id: proposal.proposal_id,\n },\n })}\n onClick={(e) => e.stopPropagation()}\n style={{ textDecoration: \"none\" }}\n >\n <div\n className={\n \"proposal-card d-flex justify-content-between gap-2 text-muted cursor-pointer p-3 w-100 flex-wrap flex-sm-nowrap \" +\n (index !== 0 && \" border\")\n }\n >\n <div className=\"d-flex gap-4 w-100\">\n <Widget\n src={`bos.forum.potlock.near/widget/components.molecule.Profile`}\n props={{\n accountId,\n }}\n />\n <div className=\"d-flex flex-column gap-2 w-100 text-wrap\">\n <div className=\"d-flex gap-2 align-items-center flex-wrap w-100\">\n <div className=\"h6 mb-0 text-black\">{proposal.name}</div>\n <Widget\n src={`bos.forum.potlock.near/widget/components.molecule.MultiSelectCategoryDropdown`}\n props={{\n selected: proposal.labels,\n disabled: true,\n hideDropdown: true,\n onChange: () => {},\n availableOptions: rfpLabelOptions,\n }}\n />\n </div>\n {isLinked && rfpData && (\n <div className=\"text-sm text-muted d-flex gap-1 align-items-center\">\n <i class=\"bi bi-link-45deg\"></i>\n In response to RFP :\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: rfpData.rfp_id,\n },\n })}\n target=\"_blank\"\n rel=\"noopener noreferrer\"\n >\n {rfpData.name}\n </a>\n </div>\n )}\n <div className=\"d-flex gap-2 align-items-center flex-wrap flex-sm-nowrap text-sm w-100\">\n <div>#{proposal.proposal_id} ・ </div>\n <div className=\"text-truncate\">\n By {profile.name ?? accountId} ・{\" \"}\n </div>\n <Widget\n src={`near/widget/TimeAgo`}\n props={{\n blockHeight,\n blockTimestamp: proposal.timestamp,\n }}\n />\n </div>\n <div className=\"d-flex gap-2 align-items-center\">\n <Widget\n src={`devhub.near/widget/devhub.entity.proposal.LikeButton`}\n props={{\n item,\n proposalId: proposal.id,\n notifyAccountId: accountId,\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 </div>\n </div>\n </div>\n <div className=\"align-self-center\" style={{ minWidth: \"fit-content\" }}>\n <Widget\n src={`devhub.near/widget/devhub.entity.proposal.StatusTag`}\n props={{\n timelineStatus: proposal.timeline.status,\n }}\n />\n </div>\n </div>\n </a>\n );\n};\nconst getProposal = (proposal_id) => {\n return Near.asyncView(`forum.potlock.near`, \"get_proposal\", {\n proposal_id,\n });\n};\nconst FeedPage = () => {\n State.init({\n data: [],\n cachedItems: {},\n stage: \"\",\n sort: \"\",\n label: \"\",\n input: \"\",\n loading: false,\n loadingMore: false,\n aggregatedCount: null,\n currentlyDisplaying: 0,\n isFiltered: false,\n });\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 author_id\n block_height\n name\n labels\n summary\n editor_id\n proposal_id\n ts\n timeline\n views\n linked_rfp\n }\n ${queryName}_aggregate(\n order_by: {proposal_id: desc}\n where: $where\n ) {\n aggregate {\n count\n }\n }\n }`;\n const rfpQueryName =\n \"bos_forum_potlock_near_ai_pgf_indexer_rfps_with_latest_snapshot\";\n const rfpQuery = `query GetLatestSnapshot($offset: Int = 0, $limit: Int = 10, $where: ${rfpQueryName}_bool_exp = {}) {\n ${rfpQueryName}(\n offset: $offset\n limit: $limit\n order_by: {rfp_id: desc}\n where: $where\n ) {\n name\n rfp_id\n }\n }`;\n function separateNumberAndText(str) {\n const numberRegex = /\\d+/;\n if (numberRegex.test(str)) {\n const number = str.match(numberRegex)[0];\n const text = str.replace(numberRegex, \"\").trim();\n return { number: parseInt(number), text };\n } else {\n return { number: null, text: str.trim() };\n }\n }\n const buildWhereClause = () => {\n let where = {};\n if (state.label) {\n where = { labels: { _contains: state.label }, ...where };\n }\n if (state.stage) {\n // timeline is stored as jsonb\n where = {\n timeline: { _cast: { String: { _regex: `${state.stage}` } } },\n ...where,\n };\n }\n if (state.input) {\n const { number, text } = separateNumberAndText(state.input);\n if (number) {\n where = { proposal_id: { _eq: number }, ...where };\n }\n if (text) {\n where = {\n _or: [\n { name: { _iregex: `${text}` } },\n { summary: { _iregex: `${text}` } },\n { description: { _iregex: `${text}` } },\n ],\n ...where,\n };\n }\n }\n State.update({ isFiltered: Object.keys(where).length > 0 });\n return where;\n };\n const buildOrderByClause = () => {\n /**\n * TODO\n * Most commented -> edit contract and indexer\n * Unanswered -> 0 comments\n */\n };\n const makeMoreItems = () => {\n if (state.aggregatedCount <= state.currentlyDisplaying) return;\n fetchProposals(state.data.length);\n };\n const fetchProposals = (offset) => {\n if (!offset) {\n offset = 0;\n }\n if (state.loading) return;\n const FETCH_LIMIT = 10;\n const variables = {\n limit: FETCH_LIMIT,\n offset,\n where: buildWhereClause(),\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 totalResult = result.body.data?.[`${queryName}_aggregate`];\n const promises = data.map((item) => {\n if (isNumber(item.linked_rfp)) {\n return fetchGraphQL(rfpQuery, \"GetLatestSnapshot\", {}).then(\n (result) => {\n const rfpData = result.body.data?.[rfpQueryName];\n return { ...item, rfpData: rfpData[0] };\n }\n );\n } else {\n return Promise.resolve(item);\n }\n });\n Promise.all(promises).then((res) => {\n State.update({ aggregatedCount: totalResult.aggregate.count });\n fetchBlockHeights(res, offset);\n });\n }\n }\n });\n };\n const renderItem = (item, index) => (\n <div\n key={item.proposal_id}\n className={\n (index !== state.data.length - 1 && \"border-bottom \") + index === 0 &&\n \" rounded-top-2\"\n }\n >\n <FeedItem proposal={item} index={index} />\n </div>\n );\n const cachedRenderItem = (item, index) => {\n if (props.term) {\n return renderItem(item, {\n searchKeywords: [props.term],\n });\n }\n const key = JSON.stringify(item);\n if (!(key in state.cachedItems)) {\n state.cachedItems[key] = renderItem(item, index);\n State.update();\n }\n return state.cachedItems[key];\n };\n useEffect(() => {\n fetchProposals();\n }, [state.sort, state.label, state.stage]);\n const mergeItems = (newItems) => {\n const items = [\n ...new Set([...newItems, ...state.data].map((i) => JSON.stringify(i))),\n ].map((i) => JSON.parse(i));\n // Sorting in the front end\n if (state.sort === \"proposal_id\" || state.sort === \"\") {\n items.sort((a, b) => b.proposal_id - a.proposal_id);\n } else if (state.sort === \"views\") {\n items.sort((a, b) => b.views - a.views);\n }\n return items;\n };\n const fetchBlockHeights = (data, offset) => {\n let promises = data.map((item) => getProposal(item.proposal_id));\n Promise.all(promises).then((blockHeights) => {\n data = data.map((item, index) => ({\n ...item,\n timeline: JSON.parse(item.timeline),\n social_db_post_block_height:\n blockHeights[index].social_db_post_block_height,\n }));\n if (offset) {\n let newData = mergeItems(data);\n State.update({\n data: newData,\n currentlyDisplaying: newData.length,\n loading: false,\n });\n } else {\n State.update({\n data,\n currentlyDisplaying: data.length,\n loading: false,\n });\n }\n });\n };\n const loader = (\n <div className=\"d-flex justify-content-center align-items-center w-100\">\n <Widget src={`devhub.near/widget/devhub.components.molecule.Spinner`} />\n </div>\n );\n const renderedItems = state.data ? state.data.map(cachedRenderItem) : null;\n return (\n <Container className=\"w-100 py-4 px-2 d-flex flex-column gap-3\">\n <div className=\"d-flex justify-content-between flex-wrap gap-2 align-items-center\">\n <Heading>\n Proposals\n <span className=\"text-muted text-normal\">\n ({state.aggregatedCount ?? state.data.length}){\" \"}\n </span>\n </Heading>\n <div className=\"d-flex flex-wrap gap-4 align-items-center\">\n <Widget\n src={`devhub.near/widget/devhub.feature.proposal-search.by-input`}\n props={{\n search: state.input,\n className: \"w-xs-100\",\n onSearch: (input) => {\n State.update({ input });\n fetchProposals();\n },\n onEnter: () => {\n fetchProposals();\n },\n }}\n />\n <Widget\n src={`devhub.near/widget/devhub.feature.proposal-search.by-sort`}\n props={{\n onStateChange: (select) => {\n State.update({ sort: select.value });\n },\n }}\n />\n <div className=\"d-flex gap-4 align-items-center\">\n <Widget\n src={`bos.forum.potlock.near/widget/components.molecule.FilterByLabel`}\n props={{\n onStateChange: (select) => {\n State.update({ label: select.value });\n },\n availableOptions: rfpLabelOptions,\n }}\n />\n <Widget\n src={`devhub.near/widget/devhub.feature.proposal-search.by-stage`}\n props={{\n onStateChange: (select) => {\n State.update({ stage: select.value });\n },\n }}\n />\n </div>\n </div>\n <div className=\"mt-2 mt-xs-0\">\n <Link\n to={href({\n widgetSrc: `bos.forum.potlock.near/widget/app`,\n params: { page: \"create-proposal\" },\n })}\n >\n <Widget\n src={`devhub.near/widget/devhub.components.molecule.Button`}\n props={{\n label: (\n <div className=\"d-flex gap-2 align-items-center\">\n <div>\n <i className=\"bi bi-plus-circle-fill\"></i>\n </div>\n Submit Proposal\n </div>\n ),\n classNames: { root: \"blue-btn\" },\n }}\n />\n </Link>\n </div>\n </div>\n <div style={{ minHeight: \"50vh\" }}>\n {!Array.isArray(state.data) ? (\n loader\n ) : (\n <div className=\"card no-border rounded-0 mt-4 py-3 full-width-div\">\n <div className=\"container-xl\">\n <div>\n <Widget\n src={`bos.forum.potlock.near/widget/components.pages.announcement`}\n loading=\"\"\n />\n </div>\n <div className=\"mt-4 border rounded-2\">\n {state.aggregatedCount === 0 ? (\n <div className=\"m-2\">\n {state.isFiltered ? (\n <div class=\"alert alert-danger\" role=\"alert\">\n No proposal found for selected filter.\n </div>\n ) : (\n <div class=\"alert alert-secondary\" role=\"alert\">\n No proposal has been created yet.\n </div>\n )}\n </div>\n ) : state.aggregatedCount > 0 ? (\n <InfiniteScroll\n pageStart={0}\n loadMore={makeMoreItems}\n hasMore={state.aggregatedCount > state.data.length}\n loader={loader}\n useWindow={false}\n threshold={100}\n >\n {renderedItems}\n </InfiniteScroll>\n ) : (\n loader\n )}\n </div>\n </div>\n </div>\n )}\n </div>\n </Container>\n );\n};\nreturn FeedPage(props);\n" }, "components.rfps.TimelineConfigurator": { "": "const { RFP_TIMELINE_STATUS } = VM.require(\n `bos.forum.potlock.near/widget/core.common`\n) || { RFP_TIMELINE_STATUS: {} };\nconst stepsArray = [1, 2, 3];\nconst timeline = props.timeline;\nconst disabled = props.disabled;\nconst setTimeline = props.setTimeline ?? (() => {});\nconst TimelineStatusOptions = [\n {\n label: \"Accepting Submissions\",\n value: { status: RFP_TIMELINE_STATUS.ACCEPTING_SUBMISSIONS },\n },\n {\n label: \"Evaluation\",\n value: {\n status: RFP_TIMELINE_STATUS.EVALUATION,\n },\n },\n {\n label: \"Proposal Selected\",\n value: {\n status: RFP_TIMELINE_STATUS.PROPOSAL_SELECTED,\n },\n },\n {\n label: \"Cancelled\",\n value: {\n status: RFP_TIMELINE_STATUS.CANCELLED,\n },\n },\n];\nconst Container = styled.div`\n .circle-lg {\n width: 15px;\n height: 15px;\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: 85px;\n background-color: lightgrey;\n }\n @media screen and (max-width: 970px) {\n .vertical-line {\n height: 70px !important;\n }\n }\n @media screen and (max-width: 570px) {\n .vertical-line {\n height: 65px !important;\n }\n }\n`;\nconst selectedTimelineStatusIndex = useMemo(\n () =>\n TimelineStatusOptions.findIndex((i) => i.value.status === timeline.status),\n [timeline]\n);\nconst TimelineItems = ({ title, children, value, values }) => {\n const indexOfCurrentItem = TimelineStatusOptions.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 = selectedTimelineStatusIndex;\n // index 2,3 is of decision\n if (selectedTimelineStatusIndex === 3) {\n statusIndex = 2;\n }\n if (statusIndex === indexOfCurrentItem) {\n color = \"#FEF6EE\";\n }\n if (statusIndex > indexOfCurrentItem) {\n color = \"#EEFEF0\";\n }\n // cancelled\n if (\n statusIndex === 2 &&\n (values ?? []).includes(RFP_TIMELINE_STATUS.CANCELLED) &&\n timeline.status === RFP_TIMELINE_STATUS.CANCELLED\n ) {\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};\nreturn (\n <Container className=\"d-flex flex-column gap-2\">\n {!disabled && (\n <div className=\"d-flex flex-column gap-2\">\n <h6 className=\"mb-0\">Status</h6>\n <Widget\n src={`bos.forum.potlock.near/widget/components.molecule.DropDown`}\n props={{\n options: TimelineStatusOptions,\n selectedValue: TimelineStatusOptions.find(\n (i) => i.value.status === timeline.status\n ),\n onUpdate: (v) => {\n setTimeline({ status: v.value.status });\n },\n }}\n />\n </div>\n )}\n <div className=\"d-flex gap-3 mt-2\">\n <div className=\"d-flex flex-column mt-4\">\n {stepsArray.map((_, index) => {\n const indexOfCurrentItem = index;\n let color = \"\";\n let statusIndex = selectedTimelineStatusIndex;\n // index 2,3 is of decision\n if (selectedTimelineStatusIndex === 3) {\n statusIndex = 2;\n }\n const current = statusIndex === indexOfCurrentItem;\n const completed =\n statusIndex > indexOfCurrentItem ||\n timeline.status === RFP_TIMELINE_STATUS.PROPOSAL_SELECTED ||\n timeline.status === RFP_TIMELINE_STATUS.CANCELLED;\n return (\n <div className=\"d-flex flex-column align-items-center gap-1\">\n <div\n className={\n \"circle-lg \" +\n (completed && \" green-fill \") +\n (current && !completed && \" yellow-fill \")\n }\n >\n {completed && (\n <div\n className=\"d-flex justify-content-center align-items-center\"\n style={{ height: \"120%\" }}\n >\n <i className=\"bi bi-check\"></i>\n </div>\n )}\n </div>\n {index !== stepsArray.length - 1 && (\n <div\n className={\n \"vertical-line \" +\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) Accepting Submissions\"\n value={RFP_TIMELINE_STATUS.ACCEPTING_SUBMISSIONS}\n >\n <div>During this stage, the RFP is still open for submissions.</div>\n </TimelineItems>\n <TimelineItems\n title=\"2) Evaluation\"\n value={RFP_TIMELINE_STATUS.EVALUATION}\n >\n <div>\n This RFP is closed for submissions. All submitted proposals are\n under review.\n </div>\n </TimelineItems>\n <TimelineItems\n title=\"3) Decision\"\n values={[\n RFP_TIMELINE_STATUS.PROPOSAL_SELECTED,\n RFP_TIMELINE_STATUS.CANCELLED,\n ]}\n >\n <div className=\"d-flex flex-column gap-2\">\n <div>Sponsor makes a final decision:</div>\n <Widget\n src={`bos.forum.potlock.near/widget/components.molecule.RadioButton`}\n props={{\n value: \"\",\n label: <div className=\"fw-bold\">Proposal Selected</div>,\n disabled: disabled,\n isChecked:\n timeline.status === RFP_TIMELINE_STATUS.PROPOSAL_SELECTED,\n onClick: (v) => {\n if (v) {\n setTimeline({\n status: RFP_TIMELINE_STATUS.PROPOSAL_SELECTED,\n });\n }\n },\n }}\n />\n <Widget\n src={`bos.forum.potlock.near/widget/components.molecule.RadioButton`}\n props={{\n value: \"\",\n disabled: disabled,\n label: <div className=\"fw-bold\">RFP Cancelled</div>,\n isChecked: timeline.status === RFP_TIMELINE_STATUS.CANCELLED,\n onClick: (v) => {\n if (v) {\n setTimeline({\n status: RFP_TIMELINE_STATUS.CANCELLED,\n });\n }\n },\n }}\n />\n </div>\n </TimelineItems>\n </div>\n </div>\n </Container>\n);\n" }, "components.rfps.ViewRfpModal": { "": "const { href: linkHref } = VM.require(`devhub.near/widget/core.lib.url`);\nlinkHref || (linkHref = () => {});\nconst isOpen = props.isOpen;\nconst isEdit = props.isEdit;\nconst rfpId = props.rfpId;\nconst Modal = styled.div`\n display: ${({ hidden }) => (hidden ? \"none\" : \"flex\")};\n position: fixed;\n inset: 0;\n justify-content: center;\n align-items: center;\n opacity: 1;\n z-index: 999;\n .black-btn {\n background-color: #000 !important;\n border: none;\n color: white;\n &:active {\n color: white;\n }\n }\n @media screen and (max-width: 768px) {\n h5 {\n font-size: 16px !important;\n }\n }\n .btn {\n font-size: 14px;\n }\n .btn-blue {\n background-image: linear-gradient(to bottom, #4b7a93, #213236);\n color: white;\n }\n`;\nconst ModalBackdrop = styled.div`\n position: absolute;\n inset: 0;\n background-color: rgba(0, 0, 0, 0.9);\n opacity: 0.9;\n`;\nconst ModalDialog = styled.div`\n padding: 2em;\n z-index: 999;\n overflow-y: auto;\n max-height: 85%;\n margin-top: 5%;\n width: 30%;\n @media screen and (max-width: 768px) {\n margin: 2rem;\n width: 100%;\n }\n`;\nconst ModalHeader = styled.div`\n display: flex;\n flex-direction: row;\n justify-content: space-between;\n align-items: center;\n padding-bottom: 4px;\n`;\nconst ModalFooter = styled.div`\n padding-top: 4px;\n display: flex;\n flex-direction: row;\n justify-content: space-between;\n align-items: items-center;\n`;\nconst CloseButton = styled.button`\n display: flex;\n align-items: center;\n justify-content: center;\n background-color: white;\n padding: 0.5em;\n border-radius: 6px;\n border: 0;\n color: #344054;\n &:hover {\n background-color: #d3d3d3;\n }\n`;\nconst ConfirmButton = styled.button`\n padding: 0.7em;\n border-radius: 6px;\n border: 0;\n box-shadow: 0px 1px 2px rgba(16, 24, 40, 0.05);\n background-color: #12b76a;\n color: white;\n &:hover {\n background-color: #0e9f5d;\n }\n`;\nconst ModalContent = styled.div`\n flex: 1;\n font-size: 14px;\n margin-top: 4px;\n margin-bottom: 4px;\n overflow-y: auto;\n max-height: 50%;\n @media screen and (max-width: 768px) {\n font-size: 12px !important;\n }\n`;\nconst NoButton = styled.button`\n background: transparent;\n border: none;\n padding: 0;\n margin: 0;\n box-shadow: none;\n`;\nreturn (\n <>\n <Modal hidden={!isOpen}>\n <ModalBackdrop />\n <ModalDialog className=\"card\">\n <ModalHeader>\n <h5 className=\"mb-0\">\n Your RFP has been successfully {!isEdit ? \"published\" : \"edited\"}.\n </h5>\n </ModalHeader>\n <div className=\"d-flex gap-2 justify-content-center mt-2\">\n <Link\n to={linkHref({\n widgetSrc: `bos.forum.potlock.near/widget/app`,\n params: { page: \"rfp\", id: rfpId },\n })}\n >\n <Widget\n src={`devhub.near/widget/devhub.components.molecule.Button`}\n props={{\n classNames: { root: \"btn-blue\" },\n label: \"View RFP\",\n onClick: () => {},\n }}\n />\n </Link>\n </div>\n </ModalDialog>\n </Modal>\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://near.social/near/widget/ProfilePage?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 className=\"d-none d-sm-flex\">\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.molecule.DropDownWithSearch": { "": "const {\n selectedValue,\n onChange,\n options,\n defaultLabel,\n showSearch,\n searchInputPlaceholder,\n searchByLabel,\n searchByValue,\n onSearch,\n disabled,\n} = props;\nconst [searchTerm, setSearchTerm] = useState(\"\");\nconst [filteredOptions, setFilteredOptions] = useState(options);\nconst [isOpen, setIsOpen] = useState(false);\nconst [selectedOption, setSelectedOption] = useState({\n label:\n options?.find((item) => item.value === selectedValue)?.label ??\n defaultLabel,\n value: defaultLabel,\n});\nuseEffect(() => {\n if (selectedOption.value !== selectedValue) {\n setSelectedOption({\n label:\n options?.find((item) => item.value === selectedValue)?.label ??\n defaultLabel,\n value: defaultLabel,\n });\n }\n}, [selectedValue]);\nuseEffect(() => {\n setFilteredOptions(options);\n}, [options]);\nconst handleSearch = (event) => {\n const term = event.target.value.toLowerCase();\n setSearchTerm(term);\n if (typeof onSearch === \"function\") {\n onSearch(term);\n return;\n }\n const filteredOptions = options.filter((option) => {\n if (searchByLabel) {\n return option.label.toLowerCase().includes(term);\n }\n if (searchByValue) {\n return option.value.toString().toLowerCase().includes(term);\n }\n });\n setFilteredOptions(filteredOptions);\n};\nconst toggleDropdown = () => {\n setIsOpen(!isOpen);\n};\nconst handleOptionClick = (option) => {\n setSelectedOption(option);\n setIsOpen(false);\n onChange(option);\n};\nconst Container = styled.div`\n .drop-btn {\n width: 100%;\n text-align: left;\n padding-inline: 10px;\n }\n .dropdown-toggle:after {\n position: absolute;\n top: 46%;\n right: 5%;\n }\n .dropdown-menu {\n width: 100%;\n }\n .dropdown-item.active,\n .dropdown-item:active {\n background-color: #f0f0f0 !important;\n color: black;\n }\n .custom-select {\n position: relative;\n }\n .scroll-box {\n max-height: 200px;\n overflow-y: scroll;\n }\n .selected {\n background-color: #f0f0f0;\n }\n input {\n background-color: #f8f9fa;\n }\n .cursor-pointer {\n cursor: pointer;\n }\n .text-wrap {\n overflow: hidden;\n white-space: normal;\n }\n .disabled {\n background-color: #f8f8f8 !important;\n cursor: not-allowed !important;\n border-radius: 5px;\n opacity: inherit !important;\n }\n .disabled.dropdown-toggle::after {\n display: none !important;\n }\n`;\nlet searchFocused = false;\nreturn (\n <Container>\n <div\n className=\"custom-select w-100\"\n tabIndex=\"0\"\n onBlur={() => {\n setTimeout(() => {\n setIsOpen(searchFocused || false);\n }, 0);\n }}\n >\n <div\n className={\n \"dropdown-toggle bg-white border rounded-2 btn drop-btn w-100 \" +\n (disabled ? \"disabled\" : \"\")\n }\n >\n <div\n className={`selected-option w-100 text-wrap ${\n selectedOption.label === defaultLabel ? \"text-muted\" : \"\"\n }`}\n onClick={!disabled && toggleDropdown}\n >\n {selectedOption.label}\n </div>\n </div>\n {isOpen && (\n <div className=\"dropdown-menu dropdown-menu-end dropdown-menu-lg-start px-2 shadow show\">\n {showSearch && (\n <input\n type=\"text\"\n className=\"form-control mb-2\"\n placeholder={searchInputPlaceholder ?? \"Search options\"}\n value={searchTerm}\n onChange={handleSearch}\n onFocus={() => {\n searchFocused = true;\n }}\n onBlur={() => {\n setTimeout(() => {\n searchFocused = false;\n }, 0);\n }}\n />\n )}\n <div className=\"scroll-box\">\n {filteredOptions.map((option) => (\n <div\n key={option.value}\n className={`dropdown-item cursor-pointer w-100 text-wrap ${\n selectedOption.value === option.value ? \"selected\" : \"\"\n }`}\n onClick={() => handleOptionClick(option)}\n >\n {option.label}\n </div>\n ))}\n </div>\n </div>\n )}\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://near.social/near/widget/ProfilePage?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 className=\"d-none d-sm-flex\">\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" }, "components.rfps.WarningModal": { "": "const isOpen = props.isOpen;\nconst onConfirmClick = props.onConfirmClick;\nconst Modal = styled.div`\n display: ${({ hidden }) => (hidden ? \"none\" : \"flex\")};\n position: fixed;\n inset: 0;\n justify-content: center;\n align-items: center;\n opacity: 1;\n z-index: 999;\n .black-btn {\n background-color: #000 !important;\n border: none;\n color: white;\n &:active {\n color: white;\n }\n }\n @media screen and (max-width: 768px) {\n h5 {\n font-size: 16px !important;\n }\n }\n .btn {\n font-size: 14px;\n }\n`;\nconst ModalBackdrop = styled.div`\n position: absolute;\n inset: 0;\n background-color: rgba(0, 0, 0, 0.5);\n opacity: 0.4;\n`;\nconst ModalDialog = styled.div`\n padding: 2em;\n z-index: 999;\n overflow-y: auto;\n max-height: 85%;\n margin-top: 5%;\n width: 50%;\n @media screen and (max-width: 768px) {\n margin: 2rem;\n width: 100%;\n }\n`;\nconst ModalHeader = styled.div`\n display: flex;\n flex-direction: row;\n justify-content: space-between;\n align-items: center;\n padding-bottom: 4px;\n`;\nconst ModalFooter = styled.div`\n padding-top: 4px;\n display: flex;\n flex-direction: row;\n justify-content: space-between;\n align-items: items-center;\n`;\nconst CloseButton = styled.button`\n display: flex;\n align-items: center;\n justify-content: center;\n background-color: white;\n padding: 0.5em;\n border-radius: 6px;\n border: 0;\n color: #344054;\n &:hover {\n background-color: #d3d3d3;\n }\n`;\nconst ConfirmButton = styled.button`\n padding: 0.7em;\n border-radius: 6px;\n border: 0;\n box-shadow: 0px 1px 2px rgba(16, 24, 40, 0.05);\n background-color: #12b76a;\n color: white;\n &:hover {\n background-color: #0e9f5d;\n }\n`;\nconst ModalContent = styled.div`\n flex: 1;\n font-size: 14px;\n margin-top: 4px;\n margin-bottom: 4px;\n overflow-y: auto;\n max-height: 50%;\n @media screen and (max-width: 768px) {\n font-size: 12px !important;\n }\n`;\nconst NoButton = styled.button`\n background: transparent;\n border: none;\n padding: 0;\n margin: 0;\n box-shadow: none;\n`;\nreturn (\n <>\n <Modal hidden={!isOpen}>\n <ModalBackdrop />\n <ModalDialog className=\"card\">\n <ModalHeader>\n <h5 className=\"mb-0\">Warning: No approved proposal found!</h5>\n </ModalHeader>\n <ModalContent>\n You haven't approved any proposals linked to the RFP. Please approve a\n proposal to proceed to the proposal selection phase.\n </ModalContent>\n <div className=\"d-flex gap-2 align-items-center justify-content-end mt-2\">\n <Widget\n src={`devhub.near/widget/devhub.components.molecule.Button`}\n props={{\n classNames: { root: \"btn-outline-secondary\" },\n label: \"Dismiss\",\n onClick: onConfirmClick,\n }}\n />\n </div>\n </ModalDialog>\n </Modal>\n </>\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://near.social/near/widget/ProfilePage?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" }, "core.common": { "": "const RFP_TIMELINE_STATUS = {\n ACCEPTING_SUBMISSIONS: \"ACCEPTING_SUBMISSIONS\",\n EVALUATION: \"EVALUATION\",\n PROPOSAL_SELECTED: \"PROPOSAL_SELECTED\",\n CANCELLED: \"CANCELLED\",\n};\nconst PROPOSAL_TIMELINE_STATUS = {\n DRAFT: \"DRAFT\",\n REVIEW: \"REVIEW\",\n APPROVED: \"APPROVED\",\n REJECTED: \"REJECTED\",\n CANCELED: \"CANCELLED\",\n APPROVED_CONDITIONALLY: \"APPROVED_CONDITIONALLY\",\n PAYMENT_PROCESSING: \"PAYMENT_PROCESSING\",\n FUNDED: \"FUNDED\",\n};\nconst QUERYAPI_ENDPOINT = `https://near-queryapi.api.pagoda.co/v1/graphql`;\nasync function fetchGraphQL(operationsDoc, operationName, variables) {\n return asyncFetch(QUERYAPI_ENDPOINT, {\n method: \"POST\",\n headers: { \"x-hasura-role\": \"bos_forum_potlock_near\" },\n body: JSON.stringify({\n query: operationsDoc,\n variables: variables,\n operationName: operationName,\n }),\n });\n}\nconst CANCEL_RFP_OPTIONS = {\n CANCEL_PROPOSALS: \"CANCEL_PROPOSALS\",\n UNLINK_PROPOSALS: \"UNLINK_PROPOSALSS\",\n NONE: \"NONE\",\n};\nfunction parseJSON(json) {\n if (typeof json === \"string\") {\n try {\n return JSON.parse(json);\n } catch (error) {\n return json;\n }\n } else {\n return json;\n }\n}\nfunction isNumber(value) {\n return typeof value === \"number\";\n}\nconst PROPOSALS_APPROVED_STATUS_ARRAY = [\n PROPOSAL_TIMELINE_STATUS.APPROVED,\n PROPOSAL_TIMELINE_STATUS.APPROVED_CONDITIONALLY,\n PROPOSAL_TIMELINE_STATUS.PAYMENT_PROCESSING,\n PROPOSAL_TIMELINE_STATUS.FUNDED,\n];\nfunction getLinkUsingCurrentGateway(url) {\n const data = fetch(`https://httpbin.org/headers`);\n const gatewayURL = data?.body?.headers?.Origin ?? \"\";\n return `https://${\n gatewayURL.includes(\"near.org\") ? \"dev.near.org\" : \"near.social\"\n }/${url}`;\n}\nreturn {\n RFP_TIMELINE_STATUS,\n PROPOSAL_TIMELINE_STATUS,\n fetchGraphQL,\n CANCEL_RFP_OPTIONS,\n parseJSON,\n isNumber,\n PROPOSALS_APPROVED_STATUS_ARRAY,\n getLinkUsingCurrentGateway,\n};\n" }, "components.proposals.ViewProposalModal": { "": "const { href: linkHref } = VM.require(`devhub.near/widget/core.lib.url`);\nlinkHref || (linkHref = () => {});\nconst isOpen = props.isOpen;\nconst isEdit = props.isEdit;\nconst proposalId = props.proposalId;\nconst Modal = styled.div`\n display: ${({ hidden }) => (hidden ? \"none\" : \"flex\")};\n position: fixed;\n inset: 0;\n justify-content: center;\n align-items: center;\n opacity: 1;\n z-index: 999;\n .black-btn {\n background-color: #000 !important;\n border: none;\n color: white;\n &:active {\n color: white;\n }\n }\n @media screen and (max-width: 768px) {\n h5 {\n font-size: 16px !important;\n }\n }\n .btn {\n font-size: 14px;\n }\n .btn-blue {\n background-image: linear-gradient(to bottom, #4b7a93, #213236);\n color: white;\n }\n`;\nconst ModalBackdrop = styled.div`\n position: absolute;\n inset: 0;\n background-color: rgba(0, 0, 0, 0.9);\n opacity: 0.9;\n`;\nconst ModalDialog = styled.div`\n padding: 2em;\n z-index: 999;\n overflow-y: auto;\n max-height: 85%;\n margin-top: 5%;\n width: 30%;\n @media screen and (max-width: 768px) {\n margin: 2rem;\n width: 100%;\n }\n`;\nconst ModalHeader = styled.div`\n display: flex;\n flex-direction: row;\n justify-content: space-between;\n align-items: center;\n padding-bottom: 4px;\n`;\nconst ModalFooter = styled.div`\n padding-top: 4px;\n display: flex;\n flex-direction: row;\n justify-content: space-between;\n align-items: items-center;\n`;\nconst CloseButton = styled.button`\n display: flex;\n align-items: center;\n justify-content: center;\n background-color: white;\n padding: 0.5em;\n border-radius: 6px;\n border: 0;\n color: #344054;\n &:hover {\n background-color: #d3d3d3;\n }\n`;\nconst ConfirmButton = styled.button`\n padding: 0.7em;\n border-radius: 6px;\n border: 0;\n box-shadow: 0px 1px 2px rgba(16, 24, 40, 0.05);\n background-color: #12b76a;\n color: white;\n &:hover {\n background-color: #0e9f5d;\n }\n`;\nconst ModalContent = styled.div`\n flex: 1;\n font-size: 14px;\n margin-top: 4px;\n margin-bottom: 4px;\n overflow-y: auto;\n max-height: 50%;\n @media screen and (max-width: 768px) {\n font-size: 12px !important;\n }\n`;\nconst NoButton = styled.button`\n background: transparent;\n border: none;\n padding: 0;\n margin: 0;\n box-shadow: none;\n`;\nreturn (\n <>\n <Modal hidden={!isOpen}>\n <ModalBackdrop />\n <ModalDialog className=\"card\">\n <ModalHeader>\n <h5 className=\"mb-0\">\n Your Proposal has been successfully{\" \"}\n {!isEdit ? \"published\" : \"edited\"}.\n </h5>\n </ModalHeader>\n <div className=\"d-flex gap-2 justify-content-center mt-2\">\n <Link\n to={linkHref({\n widgetSrc: `bos.forum.potlock.near/widget/app`,\n params: { page: \"proposal\", id: proposalId },\n })}\n >\n <Widget\n src={`devhub.near/widget/devhub.components.molecule.Button`}\n props={{\n classNames: { root: \"btn-blue\" },\n label: \"View Proposal\",\n onClick: () => {},\n }}\n />\n </Link>\n </div>\n </ModalDialog>\n </Modal>\n </>\n);\n" }, "components.molecule.RadioButton": { "": "const RadioButton = ({ value, isChecked, label, onClick, disabled }) => {\n const [checked, setChecked] = useState(isChecked);\n useEffect(() => {\n if (isChecked !== checked) {\n setChecked(isChecked);\n }\n }, [isChecked]);\n useEffect(() => {\n onClick(checked);\n }, [checked]);\n return (\n <div className=\"d-flex gap-2 align-items-center\">\n <input\n className=\"form-check-input\"\n type=\"radio\"\n disabled={disabled}\n value={value}\n checked={checked}\n onChange={(e) => setChecked(e.target.checked)}\n />\n <label className=\"form-check-label text-sm\">{label}</label>\n </div>\n );\n};\nreturn RadioButton(props);\n" }, "components.molecule.DropDown": { "": "const options = props.options; // [{label:\"\",value:\"\"}]\nconst label = props.label;\nconst onUpdate = props.onUpdate ?? (() => {});\nconst selectedValue = props.selectedValue;\nconst [selected, setSelected] = useState(selectedValue);\nuseEffect(() => {\n if (JSON.stringify(selectedValue) !== JSON.stringify(selected)) {\n setSelected(selectedValue);\n }\n}, [selectedValue]);\nconst StyledDropdown = styled.div`\n .drop-btn {\n width: 100%;\n max-width: 200px;\n text-align: left;\n padding-inline: 10px;\n }\n .dropdown-item.active,\n .dropdown-item:active {\n background-color: #f0f0f0 !important;\n color: black;\n }\n .cursor-pointer {\n cursor: pointer;\n }\n`;\nuseEffect(() => {\n onUpdate(selected);\n}, [selected]);\nreturn (\n <div>\n <div className=\"dropdown w-100\">\n <StyledDropdown>\n <button\n className=\"btn drop-btn text-truncate dropdown-toggle bg-white border rounded-2\"\n type=\"button\"\n data-bs-toggle=\"dropdown\"\n aria-expanded=\"false\"\n >\n {label} {selected && label && \": \"} {selected.label}\n </button>\n <ul className=\"dropdown-menu dropdown-menu-end dropdown-menu-lg-start px-2 shadow\">\n {options.map((item) => (\n <li\n style={{ borderRadius: \"5px\" }}\n className=\"dropdown-item cursor-pointer link-underline link-underline-opacity-0\"\n onClick={() => {\n if (selected.label !== item.label) {\n setSelected(item);\n }\n }}\n >\n {item.label}\n </li>\n ))}\n </ul>\n </StyledDropdown>\n </div>\n </div>\n);\n" } } } } }

Transaction Execution Plan

Convert Transaction To Receipt
Gas Burned:
1 Tgas
Tokens Burned:
0.0001 
Receipt:
Predecessor ID:
Receiver ID:
Gas Burned:
93 Tgas
Tokens Burned:
0.00933 
Called method: 'set' in contract: social.near
Arguments:
{ "data": { "bos.forum.potlock.near": { "widget": { "components.organism.Footer": { "": "const page = props.page;\nconst Footer = styled.div`\n width: 100%;\n background-color: #000;\n color: white !important;\n padding: 1rem;\n margin-top: 2rem;\n width: 100vw;\n position: relative;\n left: 50%;\n right: 50%;\n margin-left: -50vw;\n margin-right: -50vw;\n @media screen and (max-width: 768px) {\n padding: 1rem;\n }\n svg {\n fill: white;\n }\n path {\n fill: white;\n }\n`;\nconst XIcon = () => {\n return (\n <svg\n width=\"20\"\n height=\"16\"\n version=\"1.1\"\n xmlns=\"http://www.w3.org/2000/svg\"\n viewBox=\"0 0 300 300\"\n >\n <path d=\"M178.57 127.15 290.27 0h-26.46l-97.03 110.38L89.34 0H0l117.13 166.93L0 300.25h26.46l102.4-116.59 81.8 116.59h89.34M36.01 19.54H76.66l187.13 262.13h-40.66\" />\n </svg>\n );\n};\nconst TelegramIcon = () => {\n return (\n <svg\n xmlns=\"http://www.w3.org/2000/svg\"\n width=\"20\"\n height=\"16\"\n viewBox=\"0 0 20 16\"\n fill=\"none\"\n >\n <path\n d=\"M19.7398 1.45657L16.8608 15.0342C16.6436 15.9924 16.0771 16.2309 15.2721 15.7796L10.8854 12.5469L8.76879 14.5828C8.53463 14.817 8.33866 15.0129 7.8872 15.0129L8.20233 10.5452L16.3327 3.19847C16.6862 2.88334 16.256 2.70869 15.7833 3.02386L5.73217 9.35266L1.40507 7.99835C0.463838 7.70445 0.446834 7.05707 1.60095 6.60566L18.526 0.085202C19.3096 -0.208647 19.9954 0.25977 19.7398 1.45657Z\"\n fill=\"#151515\"\n />\n </svg>\n );\n};\nconst YoutubeIcon = () => {\n return (\n <svg\n xmlns=\"http://www.w3.org/2000/svg\"\n width=\"24\"\n height=\"16\"\n viewBox=\"0 0 24 16\"\n fill=\"none\"\n >\n <path\n d=\"M23.1744 2.49854C22.9115 1.51517 22.1369 0.740571 21.1535 0.477714C19.3712 -1.21102e-07 12.2235 0 12.2235 0C12.2235 0 5.07581 -1.21102e-07 3.29346 0.477714C2.3101 0.740571 1.53549 1.51517 1.27264 2.49854C0.794922 4.28089 0.794922 8 0.794922 8C0.794922 8 0.794922 11.7191 1.27264 13.5015C1.53549 14.4848 2.3101 15.2594 3.29346 15.5223C5.07581 16 12.2235 16 12.2235 16C12.2235 16 19.3712 16 21.1535 15.5223C22.1369 15.2594 22.9115 14.4848 23.1744 13.5015C23.6521 11.7191 23.6521 8 23.6521 8C23.6521 8 23.6521 4.28089 23.1744 2.49854ZM9.93778 11.4286V4.57143L15.8761 8L9.93778 11.4286Z\"\n fill=\"#151515\"\n />\n </svg>\n );\n};\nconst SocialLinksContainer = () => {\n return (\n <div className=\"d-flex gap-md-4 gap-2 align-items-center\">\n <a href=\"https://x.com/potlock\" target=\"_blank\">\n <XIcon />\n </a>\n <a href=\"https://aipgf.com/telegram\" target=\"_blank\">\n <TelegramIcon />\n </a>\n </div>\n );\n};\nreturn (\n <Footer className=\"d-flex gap-2 justify-content-center align-items-center\">\n <SocialLinksContainer />\n </Footer>\n);\n" }, "components.molecule.Markdown": { "": "const Container = styled.div`\n font-family: menlo monspace !important;\n p {\n white-space: pre-line; // This ensures text breaks to new line\n span {\n white-space: normal; // and this ensures profile links look normal\n }\n }\n blockquote {\n margin: 1em 0;\n padding-left: 1.5em;\n border-left: 4px solid #ccc;\n color: #666;\n font-style: italic;\n font-size: inherit;\n }\n pre {\n background-color: #f4f4f4;\n border: 1px solid #ddd;\n border-radius: 4px;\n padding: 1em;\n overflow-x: auto;\n font-family: \"Courier New\", Courier, monospace;\n }\n a {\n color: #3c697d;\n font-weight: 500 !important;\n }\n`;\nreturn (\n <Container>\n <Markdown text={props.content} />\n </Container>\n);\n" }, "components.organism.Navbar": { "": "const page = props.page;\nconst [showMenu, setShowMenu] = useState(false);\nconst { href: linkHref } = VM.require(`devhub.near/widget/core.lib.url`);\nlinkHref || (linkHref = () => {});\nconst Logo = () => {\n const Wrapper = styled.div`\n .text-lg {\n font-size: 22px;\n }\n a:hover {\n text-decoration: none;\n }\n `;\n return (\n <Wrapper>\n <Link\n to={linkHref({\n widgetSrc: `bos.forum.potlock.near/widget/app`,\n params: { page: \"proposals\" },\n })}\n >\n <div className=\"d-flex gap-2 align-items-center\">\n <b className=\"text-lg\">AI-PGF</b>\n </div>\n </Link>\n </Wrapper>\n );\n};\nconst ProfileIcon = () => {\n const Wrapper = styled.svg`\n padding: 0.25rem;\n @media screen and (max-width: 768px) {\n display: none;\n }\n `;\n return (\n <Widget\n src={`bos.forum.potlock.near/widget/components.molecule.Profile`}\n props={{\n accountId: context.accountId,\n }}\n />\n );\n};\nconst MenuIcon = () => (\n <svg\n xmlns=\"http://www.w3.org/2000/svg\"\n width=\"16\"\n height=\"16\"\n viewBox=\"0 0 16 16\"\n fill=\"none\"\n style={{ height: 20, width: 20 }}\n >\n <path\n fill-rule=\"evenodd\"\n clip-rule=\"evenodd\"\n d=\"M2 12.2986H14V13.3732H2V12.2986ZM2 9.07471H14V10.1493H2V9.07471ZM2 5.85083H14V6.92546H2V5.85083ZM2 2.62695H14V3.70158H2V2.62695Z\"\n fill=\"#818181\"\n />\n </svg>\n);\nconst Navbar = styled.div`\n padding: 1.5rem 0rem;\n display: flex;\n flex-direction: row;\n justify-content: space-between;\n align-items: center;\n background: #f4f4f4;\n @media screen and (max-width: 768px) {\n padding: 1.875rem 1.375rem;\n }\n`;\nconst LinksContainer = styled.div`\n display: flex;\n flex-direction: row;\n align-items: center;\n gap: 1.5rem;\n @media screen and (max-width: 768px) {\n display: none;\n }\n`;\nconst MobileMenu = styled.button`\n all: unset;\n display: none;\n @media screen and (max-width: 768px) {\n display: block;\n }\n`;\nlet links = [\n {\n title: \"Proposals\",\n href: \"proposals\",\n links: [],\n },\n {\n title: \"RFPs\",\n href: \"rfps\",\n links: [],\n },\n {\n title: \"About\",\n href: \"https://aipgf.com/about\",\n links: [],\n },\n {\n title: \"Ideas\",\n href: \"https://aipgf.com/ideas\",\n links: [],\n },\n];\nconst isModerator = Near.view(\n \"forum.potlock.near\",\n \"is_allowed_to_write_rfps\",\n {\n editor: context.accountId,\n }\n);\nif (isModerator) {\n links = [\n {\n title: \"Admin\",\n href: \"admin\",\n links: [],\n },\n ...links,\n ];\n}\nconst MobileNav = styled.div`\n display: none;\n @media screen and (max-width: 768px) {\n display: flex;\n }\n position: absolute;\n top: 0;\n right: 0;\n width: 207px;\n padding: 24px 36px 36px 16px;\n flex-direction: column;\n align-items: flex-end;\n gap: 2.5rem;\n flex-shrink: 0;\n border-radius: 0px 0px 0px 16px;\n background: rgba(41, 41, 41, 0.6);\n backdrop-filter: blur(5px);\n z-index: 50;\n`;\nconst MobileLink = styled.a`\n color: #f4f4f4 !important;\n font-size: 20px;\n font-style: normal;\n font-weight: 400;\n line-height: 20px; /* 100% */\n margin-bottom: 1rem;\n &.active {\n color: #00ec97 !important;\n }\n &:hover {\n text-decoration: none;\n color: #00ec97 !important;\n }\n`;\nreturn (\n <Navbar className=\"position-relative\">\n <Logo />\n <div className=\"d-flex gap-3 align-items-center\">\n <LinksContainer>\n {links.map((link) => (\n <Widget\n src={`bos.forum.potlock.near/widget/components.molecule.NavbarDropdown`}\n props={{\n title: link.title,\n href: link.href,\n links: link.links,\n page: page,\n }}\n />\n ))}\n </LinksContainer>\n {context.accountId && <ProfileIcon />}\n <MobileMenu onClick={() => setShowMenu(!showMenu)}>\n <MenuIcon />\n </MobileMenu>\n </div>\n {showMenu && (\n <MobileNav>\n <div\n onClick={() => setShowMenu(!showMenu)}\n style={{ cursor: \"pointer\" }}\n >\n <i className=\"bi bi-x\" style={{ fontSize: 20, color: \"#F4F4F4\" }}></i>\n </div>\n <div className=\"d-flex flex-column gap-2\">\n {links.map((link, idx) =>\n link.href ? (\n <MobileLink\n key={`mobile-link-${idx}`}\n className={link.href === props.page && \"active\"}\n href={`/bos.forum.potlock.near/widget/app?page=${link.href}`}\n >\n {link.title}\n </MobileLink>\n ) : (\n link.links.map((it, idx) =>\n it.href.startsWith(\"http://\") ||\n it.href.startsWith(\"https://\") ? (\n <MobileLink\n key={`nested-link-${idx}`}\n className={link.href === props.page && \"active\"}\n href={it.href}\n target=\"no_blank\"\n >\n /{it.title}\n </MobileLink>\n ) : (\n <MobileLink\n key={`nested-link-${idx}`}\n className={link.href === props.page && \"active\"}\n href={`/bos.forum.potlock.near/widget/app?page=${it.href}`}\n >\n /{it.title}\n </MobileLink>\n )\n )\n )\n )}\n </div>\n </MobileNav>\n )}\n </Navbar>\n);\n" }, "components.molecule.SimpleMDE": { "": "/**\n * iframe embedding a SimpleMDE component\n * https://github.com/sparksuite/simplemde-markdown-editor\n */\nconst { getLinkUsingCurrentGateway } = VM.require(\n `bos.forum.potlock.near/widget/core.common`\n) || { getLinkUsingCurrentGateway: () => {} };\nconst data = props.data;\nconst onChange = props.onChange ?? (() => {});\nconst onChangeKeyup = props.onChangeKeyup ?? (() => {}); // in case where we want immediate action\nconst height = props.height ?? \"390\";\nconst className = props.className ?? \"w-100\";\nconst embeddCSS = props.embeddCSS;\nState.init({\n iframeHeight: height,\n message: props.data,\n});\nconst profilesData = Social.get(\"*/profile/name\", \"final\");\nconst followingData = Social.get(\n `${context.accountId}/graph/follow/**`,\n \"final\"\n);\n// SIMPLEMDE CONFIG //\nconst fontFamily = props.fontFamily ?? \"sans-serif\";\nconst alignToolItems = props.alignToolItems ?? \"right\";\nconst placeholder = props.placeholder ?? \"\";\nconst showAccountAutoComplete = props.showAutoComplete ?? false;\nconst showProposalIdAutoComplete = props.showProposalIdAutoComplete ?? false;\nconst showRfpIdAutoComplete = props.showRfpIdAutoComplete ?? false;\nconst autoFocus = props.autoFocus ?? false;\nconst proposalQueryName =\n \"bos_forum_potlock_near_ai_pgf_indexer_proposals_with_latest_snapshot\";\nconst proposalLink = getLinkUsingCurrentGateway(\n `bos.forum.potlock.near/widget/app?page=proposal&id=`\n);\nconst proposalQuery = `query GetLatestSnapshot($offset: Int = 0, $limit: Int = 10, $where: ${proposalQueryName}_bool_exp = {}) {\n${proposalQueryName}(\n offset: $offset\n limit: $limit\n order_by: {proposal_id: desc}\n where: $where\n) {\n name\n proposal_id\n}\n}`;\nconst rfpQueryName =\n \"bos_forum_potlock_near_ai_pgf_indexer_rfps_with_latest_snapshot\";\nconst rfpLink = getLinkUsingCurrentGateway(\n `bos.forum.potlock.near/widget/app?page=rfp&id=`\n);\nconst rfpQuery = `query GetLatestSnapshot($offset: Int = 0, $limit: Int = 10, $where: ${rfpQueryName}_bool_exp = {}) {\n${rfpQueryName}(\n offset: $offset\n limit: $limit\n order_by: {rfp_id: desc}\n where: $where\n) {\n rfp_id\n name\n}\n}`;\nconst code = `\n<!doctype html>\n<html>\n <head>\n <meta charset=\"utf-8\">\n <meta name=\"viewport\" content=\"width=device-width, initial-scale=1, shrink-to-fit=no\">\n <style>\n body { \n margin: auto;\n font-family: menlo monspace !important;\n overflow: visible;\n font-size:14px !important;\n }\n @media screen and (max-width: 768px) {\n body {\n font-size: 12px;\n }\n }\n \n .cursor-pointer {\n cursor: pointer;\n }\n .text-wrap {\n overflow: hidden;\n white-space: normal;\n }\n .dropdown-item:hover,\n .dropdown-item:focus {\n background-color:rgb(0, 236, 151) !important;\n color:white !important;\n outline: none !important;\n }\n .editor-toolbar {\n text-align: ${alignToolItems};\n }\n \n .CodeMirror {\n min-height:200px !important; // for autocomplete to be visble \n }\n .CodeMirror-scroll {\n min-height:200px !important; // for autocomplete to be visble \n }\n ${embeddCSS}\n </style>\n <link rel=\"stylesheet\" href=\"https://cdn.jsdelivr.net/simplemde/latest/simplemde.min.css\">\n <link rel=\"stylesheet\" href=\"https://cdn.jsdelivr.net/highlight.js/latest/styles/github.min.css\">\n <link href=\"https://cdn.jsdelivr.net/npm/bootstrap@5.2.3/dist/css/bootstrap.min.css\" rel=\"stylesheet\" integrity=\"sha384-rbsA2VBKQhggwzxH7pPCaAqO46MgnOM80zW1RWuH61DGLwZJEdK2Kadq2F9CUG65\" crossorigin=\"anonymous\">\n</head>\n<body>\n<div class=\"dropdown\">\n <button style=\"display: none\" type=\"button\" data-bs-toggle=\"dropdown\">\n Dropdown button\n </button>\n <ul class=\"dropdown-menu\" id=\"mentiondropdown\" style=\"position: absolute;\">\n</div>\n<div class=\"dropdown\">\n <button style=\"display: none\" type=\"button\" data-bs-toggle=\"dropdown\">\n Dropdown button\n </button>\n <ul class=\"dropdown-menu\" id=\"referencedropdown\" style=\"position: absolute;\">\n</div>\n</ul>\n<textarea></textarea>\n<script src=\"https://cdn.jsdelivr.net/simplemde/latest/simplemde.min.js\"></script>\n<script src=\"https://cdn.jsdelivr.net/npm/@popperjs/core@2.11.6/dist/umd/popper.min.js\" integrity=\"sha384-oBqDVmMz9ATKxIep9tiCxS/Z9fNfEXiDAYTujMAeBAsjFuCZSmKbSSUnQlmh/jp3\" crossorigin=\"anonymous\"></script>\n<script src=\"https://cdn.jsdelivr.net/npm/bootstrap@5.2.3/dist/js/bootstrap.min.js\" integrity=\"sha384-cuYeSxntonz0PPNlHhBs68uyIAVpIIOZZ5JqeqvYYIcEL727kskC66kF92t6Xl2V\" crossorigin=\"anonymous\"></script>\n<script>\nlet codeMirrorInstance;\nlet isEditorInitialized = false;\nlet followingData = {};\nlet profilesData = {};\nlet proposalQuery = '';\nlet proposalLink = '';\nlet proposalQueryName = '';\nlet rfpQuery = '';\nlet rfpLink = '';\nlet rfpQueryName = '';\nlet showAccountAutoComplete = ${showAccountAutoComplete};\nlet showProposalIdAutoComplete = ${showProposalIdAutoComplete};\nlet showRfpIdAutoComplete = ${showRfpIdAutoComplete}\nfunction getSuggestedAccounts(term) {\n let results = [];\n term = (term || \"\").replace(/\\W/g, \"\").toLowerCase();\n const limit = 5;\n const profiles = Object.entries(profilesData);\n for (let i = 0; i < profiles.length; i++) {\n let score = 0;\n const accountId = profiles[i][0];\n const accountIdSearch = profiles[i][0].replace(/\\W/g, \"\").toLowerCase();\n const nameSearch = (profiles[i][1]?.profile?.name || \"\")\n .replace(/\\W/g, \"\")\n .toLowerCase();\n const accountIdSearchIndex = accountIdSearch.indexOf(term);\n const nameSearchIndex = nameSearch.indexOf(term);\n if (accountIdSearchIndex > -1 || nameSearchIndex > -1) {\n score += 10;\n if (accountIdSearchIndex === 0) {\n score += 10;\n }\n if (nameSearchIndex === 0) {\n score += 10;\n }\n if (followingData[accountId] === \"\") {\n score += 30;\n }\n results.push({\n accountId,\n score,\n });\n }\n }\n results.sort((a, b) => b.score - a.score);\n results = results.slice(0, limit);\n return results;\n}\nasync function asyncFetch(endpoint, { method, headers, body }) {\n try {\n const response = await fetch(endpoint, {\n method: method,\n headers: headers,\n body: body\n });\n if (!response.ok) {\n throw new Error(\"HTTP error!\");\n }\n return await response.json();\n } catch (error) {\n console.error('Error fetching data:', error);\n throw error;\n }\n}\nfunction extractNumbers(str) {\n let numbers = \"\";\n for (let i = 0; i < str.length; i++) {\n if (!isNaN(str[i])) {\n numbers += str[i];\n }\n }\n return numbers;\n};\nasync function getSuggestedRfps(id) {\n let results = [];\n const variables = {\n limit: 3,\n offset: 0,\n where: {},\n };\n if (id) {\n const rfpId = extractNumbers(id);\n if (rfpId) {\n variables[\"where\"] = { rfp_id: { _eq: id } };\n } else {\n variables[\"where\"] = {\n _or: [\n { name: { _iregex: id } },\n { summary: { _iregex: id } },\n { description: { _iregex: id} },\n ]\n };\n }\n }\n await asyncFetch(\"https://near-queryapi.api.pagoda.co/v1/graphql\", {\n method: \"POST\",\n headers: { \"x-hasura-role\": \"bos_forum_potlock_near\" },\n body: JSON.stringify({\n query: rfpQuery,\n variables: variables,\n operationName: \"GetLatestSnapshot\",\n }),\n })\n .then((res) => {\n const rfps =\n res?.data?.[\n rfpQueryName\n ];\n results = rfps;\n })\n .catch((error) => {\n console.error(error);\n });\n return results;\n};\nasync function getSuggestedProposals(id) {\n let results = [];\n const variables = {\n limit: 3,\n offset: 0,\n where: {},\n };\n if (id) {\n const proposalId = extractNumbers(id);\n if (proposalId) {\n variables[\"where\"] = { proposal_id: { _eq: id } };\n } else {\n variables[\"where\"] = {\n _or: [\n { name: { _iregex: id } },\n { summary: { _iregex: id } },\n { description: { _iregex: id} },\n ]\n };\n }\n }\n await asyncFetch(\"https://near-queryapi.api.pagoda.co/v1/graphql\", {\n method: \"POST\",\n headers: { \"x-hasura-role\": \"bos_forum_potlock_near\" },\n body: JSON.stringify({\n query: proposalQuery,\n variables: variables,\n operationName: \"GetLatestSnapshot\",\n }),\n })\n .then((res) => {\n const proposals =\n res?.data?.[\n proposalQueryName\n ];\n results = proposals;\n })\n .catch((error) => {\n console.error(error);\n });\n return results;\n};\n// Initializes SimpleMDE element and attaches to text-area\nconst simplemde = new SimpleMDE({\n forceSync: true,\n toolbar: [\n \"heading\",\n \"bold\",\n \"italic\",\n \"|\", // adding | creates a divider in the toolbar\n \"quote\",\n \"code\",\n \"link\",\n ],\n placeholder: \\`${placeholder}\\`,\n initialValue: \"\",\n insertTexts: {\n link: [\"[\", \"]()\"],\n },\n spellChecker: false,\n renderingConfig: {\n\t\tsingleLineBreaks: false,\n\t\tcodeSyntaxHighlighting: true,\n\t},\n autofocus:${autoFocus}\n});\ncodeMirrorInstance = simplemde.codemirror;\n/**\n * Sends message to Widget to update content\n */\nconst updateContent = () => {\n const content = simplemde.value();\n window.parent.postMessage({ handler: \"update\", content }, \"*\");\n};\n/**\n * Sends message to Widget to update iframe height\n */\nconst updateIframeHeight = () => {\n const iframeHeight = document.body.scrollHeight;\n window.parent.postMessage({ handler: \"resize\", height: iframeHeight }, \"*\");\n};\n// On Change\nsimplemde.codemirror.on('blur', () => {\n updateContent();\n});\nsimplemde.codemirror.on('keyup', () => {\n updateIframeHeight();\n const content = simplemde.value();\n window.parent.postMessage({ handler: \"updateOnKeyup\", content }, \"*\");\n});\nif (showAccountAutoComplete) {\n let mentionToken;\n let mentionCursorStart;\n const dropdown = document.getElementById(\"mentiondropdown\");\n simplemde.codemirror.on(\"keydown\", () => {\n if (mentionToken && event.key === 'ArrowDown') {\n dropdown.querySelector('button').focus();\n event.preventDefault();\n return false;\n }\n });\n simplemde.codemirror.on(\"keyup\", (cm, event) => {\n const cursor = cm.getCursor();\n const token = cm.getTokenAt(cursor);\n const createMentionDropDownOptions = () => {\n const mentionInput = cm.getRange(mentionCursorStart, cursor);\n dropdown.innerHTML = getSuggestedAccounts(mentionInput)\n .map(\n (item) =>\n '<li><button class=\"dropdown-item cursor-pointer w-100 text-wrap\">' + item?.accountId + '</button></li>'\n )\n .join(\"\");\n dropdown.querySelectorAll(\"li\").forEach((li) => {\n li.addEventListener(\"click\", () => {\n const selectedText = li.textContent.trim();\n simplemde.codemirror.replaceRange(selectedText, mentionCursorStart, cursor);\n mentionToken = null;\n dropdown.classList.remove(\"show\");\n cm.focus();\n });\n });\n }\n // show dropwdown only when @ is at first place or when there is a space before @\n if (!mentionToken && (token.string === \"@\" && cursor.ch === 1 || token.string === \"@\" && cm.getTokenAt({line:cursor.line, ch: cursor.ch - 1}).string == ' ')) {\n mentionToken = token;\n mentionCursorStart = cursor;\n // Calculate cursor position relative to the iframe's viewport\n const rect = cm.charCoords(cursor);\n const x = rect.left;\n const y = rect.bottom;\n // Create dropdown with options\n dropdown.style.top = y + \"px\";\n dropdown.style.left = x + \"px\";\n createMentionDropDownOptions();\n dropdown.classList.add(\"show\");\n // Close dropdown on outside click\n document.addEventListener(\"click\", function(event) {\n if (!dropdown.contains(event.target)) {\n mentionToken = null;\n dropdown.classList.remove(\"show\");\n }\n });\n } else if (mentionToken && token.string.match(/[^@a-z0-9.]/)) {\n mentionToken = null;\n dropdown.classList.remove(\"show\");\n } else if (mentionToken) {\n createMentionDropDownOptions();\n }\n});\n}\nif (showProposalIdAutoComplete) {\n let proposalId;\n let referenceCursorStart;\n const dropdown = document.getElementById(\"referencedropdown\");\n const loader = document.createElement('div');\n loader.className = 'loader';\n loader.textContent = 'Loading...';\n simplemde.codemirror.on(\"keydown\", () => {\n if (proposalId && event.key === 'ArrowDown') {\n dropdown.querySelector('button').focus();\n event.preventDefault();\n return false;\n }\n });\n simplemde.codemirror.on(\"keyup\", (cm, event) => {\n const cursor = cm.getCursor();\n const token = cm.getTokenAt(cursor);\n const createReferenceDropDownOptions = async () => {\n try {\n const input = cm.getRange(referenceCursorStart, cursor);\n dropdown.innerHTML = ''; // Clear previous content\n dropdown.appendChild(loader); // Show loader\n const suggestedProposals = await getSuggestedProposals(input);\n const suggestedRFPs = await getSuggestedRfps(input);\n const proposalItems = suggestedProposals\n .map(\n (item) =>\n '<li><button class=\"dropdown-item cursor-pointer w-100 text-wrap\">' + \"#\" + item?.proposal_id + \" Proposal: \" + item.name + '</button></li>'\n )\n .join(\"\");\n const rfpItems = suggestedRFPs\n .map(\n (item) =>\n '<li><button class=\"dropdown-item cursor-pointer w-100 text-wrap\">' + \"#\" + item?.rfp_id + \" RFP: \" + \" \" + item.name + '</button></li>'\n )\n .join(\"\");\n dropdown.innerHTML = proposalItems + rfpItems;\n dropdown.querySelectorAll(\"li\").forEach((li) => {\n li.addEventListener(\"click\", () => {\n const selectedText = li.textContent.trim();\n const startIndex = selectedText.indexOf('#') + 1; \n const endIndex = selectedText.indexOf(' ', startIndex);\n const id = endIndex !== -1 ? selectedText.substring(startIndex, endIndex) : selectedText.substring(startIndex);\n const link = (selectedText.includes(\"RFP:\") ? rfpLink : proposalLink) + id;\n const adjustedStart = {\n line: referenceCursorStart.line,\n ch: referenceCursorStart.ch - 1\n };\n simplemde.codemirror.replaceRange(\"[\" + selectedText + \"]\" + \"(\" + link + \")\", adjustedStart, cursor);\n proposalId = null;\n dropdown.classList.remove(\"show\");\n cm.focus();\n });\n });\n } catch (error) {\n console.error('Error fetching data:', error);\n // Handle error: Remove loader\n dropdown.innerHTML = ''; // Clear previous content\n } finally {\n // Remove loader\n dropdown.removeChild(loader);\n }\n }\n // show dropwdown only when there is space before # or it's first char\n if (!proposalId && (token.string === \"#\" && cursor.ch === 1 || token.string === \"#\" && cm.getTokenAt({line:cursor.line, ch: cursor.ch - 1}).string == ' ')) {\n proposalId = token;\n referenceCursorStart = cursor;\n // Calculate cursor position relative to the iframe's viewport\n const rect = cm.charCoords(cursor);\n const x = rect.left;\n const y = rect.bottom;\n // Create dropdown with options\n dropdown.style.top = y + \"px\";\n dropdown.style.left = x + \"px\";\n createReferenceDropDownOptions();\n dropdown.classList.add(\"show\");\n // Close dropdown on outside click\n document.addEventListener(\"click\", function(event) {\n if (!dropdown.contains(event.target)) {\n proposalId = null;\n dropdown.classList.remove(\"show\");\n }\n });\n } else if (proposalId && (token.string.match(/[^#a-z0-9.]/) || !token.string)) {\n proposalId = null;\n dropdown.classList.remove(\"show\");\n } else if (proposalId) {\n createReferenceDropDownOptions();\n }\n});\n}\nwindow.addEventListener(\"message\", (event) => {\n if (!isEditorInitialized && event.data !== \"\") {\n simplemde.value(event.data.content);\n isEditorInitialized = true;\n } else {\n if (event.data.handler === 'refreshEditor') {\n codeMirrorInstance.getDoc().setValue(event.data.content);\n }\n }\n if (event.data.followingData) {\n followingData = event.data.followingData;\n }\n if (event.data.profilesData) {\n profilesData = JSON.parse(event.data.profilesData);\n }\n if (event.data.proposalQuery) {\n proposalQuery = event.data.proposalQuery;\n }\n if (event.data.proposalQueryName) {\n proposalQueryName = event.data.proposalQueryName;\n }\n if (event.data.proposalLink) {\n proposalLink = event.data.proposalLink;\n }\n if (event.data.rfpQuery) {\n rfpQuery = event.data.rfpQuery;\n }\n if (event.data.rfpQueryName) {\n rfpQueryName = event.data.rfpQueryName;\n }\n if (event.data.rfpLink) {\n rfpLink = event.data.rfpLink;\n }\n \n});\n</script>\n</body>\n</html>\n`;\nreturn (\n <iframe\n className={className}\n style={{\n height: `${state.iframeHeight}px`,\n maxHeight: \"410px\",\n minHeight: \"250px\",\n }}\n srcDoc={code}\n message={{\n content: props.data?.content ?? \"\",\n followingData,\n profilesData: JSON.stringify(profilesData),\n proposalQuery: proposalQuery,\n proposalQueryName: proposalQueryName,\n proposalLink: proposalLink,\n rfpQuery: rfpQuery,\n rfpQueryName: rfpQueryName,\n rfpLink: rfpLink,\n handler: props.data.handler,\n }}\n onMessage={(e) => {\n switch (e.handler) {\n case \"update\":\n {\n onChange(e.content);\n }\n break;\n case \"resize\":\n {\n const offset = 10;\n State.update({ iframeHeight: e.height + offset });\n }\n break;\n case \"updateOnKeyup\":\n {\n onChangeKeyup(e.content);\n }\n break;\n }\n }}\n />\n);\n" }, "components.molecule.Compose": { "": "const EmbeddCSS = `\n .CodeMirror {\n margin-inline:10px;\n border-radius:5px;\n }\n .editor-toolbar {\n border: none !important;\n }\n`;\nconst Wrapper = styled.div`\n .nav-link {\n color: inherit !important;\n }\n .card-header {\n padding-bottom: 0px !important;\n }\n`;\nconst Compose = ({\n data,\n onChange,\n autocompleteEnabled,\n placeholder,\n height,\n embeddCSS,\n showProposalIdAutoComplete,\n onChangeKeyup,\n handler,\n}) => {\n State.init({\n data: data,\n selectedTab: \"editor\",\n autoFocus: false,\n });\n useEffect(() => {\n if (typeof onChange === \"function\") {\n onChange(state.data);\n }\n }, [state.data]);\n useEffect(() => {\n // for clearing editor after txn approval/ showing draft state\n if (data !== state.data || handler !== state.handler) {\n State.update({ data: data, handler: handler });\n }\n }, [data, handler]);\n return (\n <Wrapper>\n <div className=\"card\">\n <div className=\"card-header\" style={{ position: \"relative\" }}>\n <div>\n <ul class=\"nav nav-tabs\">\n <li class=\"nav-item\">\n <button\n class={`nav-link ${\n state.selectedTab === \"editor\" ? \"active\" : \"\"\n }`}\n onClick={() =>\n State.update({ selectedTab: \"editor\", autoFocus: true })\n }\n >\n Write\n </button>\n </li>\n <li class=\"nav-item\">\n <button\n class={`nav-link ${\n state.selectedTab === \"preview\" ? \"active\" : \"\"\n }`}\n onClick={() => State.update({ selectedTab: \"preview\" })}\n >\n Preview\n </button>\n </li>\n </ul>\n </div>\n </div>\n {state.selectedTab === \"editor\" ? (\n <>\n <Widget\n src={`bos.forum.potlock.near/widget/components.molecule.SimpleMDE`}\n props={{\n data: { handler: state.handler, content: state.data },\n onChange: (content) => {\n State.update({ data: content, handler: \"update\" });\n },\n placeholder: placeholder,\n height,\n embeddCSS: embeddCSS || EmbeddCSS,\n showAutoComplete: autocompleteEnabled,\n showProposalIdAutoComplete: showProposalIdAutoComplete,\n autoFocus: state.autoFocus,\n onChangeKeyup: onChangeKeyup,\n }}\n />\n </>\n ) : (\n <div className=\"card-body\">\n <Widget\n src={`devhub.near/widget/devhub.components.molecule.MarkdownViewer`}\n props={{\n text: state.data,\n }}\n />\n </div>\n )}\n </div>\n </Wrapper>\n );\n};\nreturn Compose(props);\n" }, "components.molecule.AccountInput": { "": "const value = props.value;\nconst placeholder = props.placeholder;\nconst onUpdate = props.onUpdate;\nconst [account, setAccount] = useState(value);\nconst [showAccountAutocomplete, setAutoComplete] = useState(false);\nconst [isValidAccount, setValidAccount] = useState(true);\nconst AutoComplete = styled.div`\n margin-top: 1rem;\n`;\nuseEffect(() => {\n if (value !== account) {\n setAccount(value);\n }\n}, [value]);\nuseEffect(() => {\n if (value !== account) {\n onUpdate(account);\n }\n}, [account]);\nuseEffect(() => {\n const handler = setTimeout(() => {\n const valid = account.length === 64 || (account ?? \"\").includes(\".near\");\n setValidAccount(valid);\n setAutoComplete(!valid);\n }, 100);\n return () => {\n clearTimeout(handler);\n };\n}, [account]);\nreturn (\n <div>\n <Widget\n src={`devhub.near/widget/devhub.components.molecule.Input`}\n props={{\n className: \"flex-grow-1\",\n value: account,\n onChange: (e) => {\n setAccount(e.target.value);\n },\n skipPaddingGap: true,\n placeholder: placeholder,\n inputProps: {\n max: 64,\n prefix: \"@\",\n },\n }}\n />\n {account && !isValidAccount && (\n <div style={{ color: \"red\" }} className=\"text-sm mt-1\">\n Please enter valid account ID\n </div>\n )}\n {showAccountAutocomplete && (\n <AutoComplete>\n <Widget\n src={`devhub.near/widget/devhub.components.molecule.AccountAutocomplete`}\n props={{\n term: account,\n onSelect: (id) => {\n setAccount(id);\n setAutoComplete(false);\n },\n onClose: () => setAutoComplete(false),\n }}\n />\n </AutoComplete>\n )}\n </div>\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://near.social/near/widget/ProfilePage?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.template.AppLayout": { "": "const data = fetch(`https://httpbin.org/headers`);\nconst gatewayURL = data?.body?.headers?.Origin ?? \"\";\n// we need fixed positioning for near social and not for org\nconst ParentContainer = gatewayURL.includes(\"near.org\")\n ? styled.div`\n width: 100%;\n min-height: 90vh;\n background: #f4f4f4;\n padding-bottom: 1rem;\n `\n : styled.div`\n position: fixed;\n inset: 73px 0px 0px;\n width: 100%;\n overflow-y: scroll;\n min-height: 90vh;\n background: #f4f4f4;\n `;\nconst Theme = styled.div`\n display: flex;\n flex-direction: column;\n padding-top: calc(-1 * var(--body-top-padding));\n background: #f4f4f4;\n .container-xl {\n padding-inline: 0px !important;\n }\n font-family: menlo monspace !important;\n`;\nconst Container = styled.div`\n width: 100%;\n`;\nconst ContentContainer = styled.div`\n flex: 1;\n display: flex;\n flex-direction: column;\n align-items: center;\n width: 100%;\n`;\nconst AppHeader = ({ page }) => (\n <Widget\n src={`bos.forum.potlock.near/widget/components.organism.Navbar`}\n props={{\n page: page,\n ...props,\n }}\n />\n);\nconst Footer = (props) => {\n return (\n <Widget\n src=\"bos.forum.potlock.near/widget/components.organism.Footer\"\n props={{\n ...props,\n }}\n />\n );\n};\nconst AppLayout = ({ page, children }) => {\n return (\n <ParentContainer>\n <Theme>\n <Container className=\"container-xl p-3\">\n <AppHeader page={page} />\n <ContentContainer className=\"content-container\">\n {children}\n </ContentContainer>\n </Container>\n <Footer />\n </Theme>\n </ParentContainer>\n );\n};\nreturn { AppLayout };\n" }, "app": { "": "/**\n * This is the main entry point for the RFP application.\n * Page route gets passed in through params, along with all other page props.\n */\nconst { page, ...passProps } = props;\n// Import our modules\nconst { AppLayout } = VM.require(\n `bos.forum.potlock.near/widget/components.template.AppLayout`\n);\nif (!AppLayout) {\n return <p>Loading modules...</p>;\n}\n// CSS styles to be used across the app.\n// Define fonts here, as well as any other global styles.\nconst Theme = styled.div`\n a {\n color: inherit;\n }\n .attractable {\n box-shadow: 0 0.125rem 0.25rem rgba(0, 0, 0, 0.075) !important;\n transition: box-shadow 0.6s;\n &:hover {\n box-shadow: 0 0.5rem 1rem rgba(0, 0, 0, 0.15) !important;\n }\n }\n`;\nif (!page) {\n // If no page is specified, we default to the feed page TEMP\n page = \"proposals\";\n}\n// This is our navigation, rendering the page based on the page parameter\nfunction Page() {\n const routes = page.split(\".\");\n switch (routes[0]) {\n case \"rfps\": {\n return (\n <Widget\n src={`bos.forum.potlock.near/widget/components.rfps.Feed`}\n props={passProps}\n />\n );\n }\n case \"rfp\": {\n return (\n <Widget\n src={`bos.forum.potlock.near/widget/components.rfps.Rfp`}\n props={passProps}\n />\n );\n }\n case \"create-rfp\": {\n return (\n <Widget\n src={`bos.forum.potlock.near/widget/components.rfps.Editor`}\n props={passProps}\n />\n );\n }\n case \"create-proposal\": {\n return (\n <Widget\n src={`bos.forum.potlock.near/widget/components.proposals.Editor`}\n props={{ ...passProps }}\n />\n );\n }\n case \"ideas\": {\n return (\n <Widget\n src={`bos.forum.potlock.near/widget/components.proposals.Feed`}\n props={passProps}\n />\n );\n }\n case \"proposals\": {\n return (\n <Widget\n src={`bos.forum.potlock.near/widget/components.proposals.Feed`}\n props={passProps}\n />\n );\n }\n case \"proposal\": {\n return (\n <Widget\n src={`bos.forum.potlock.near/widget/components.proposals.Proposal`}\n props={passProps}\n />\n );\n }\n case \"admin\": {\n return (\n <Widget\n src={`bos.forum.potlock.near/widget/components.pages.admin`}\n props={passProps}\n />\n );\n }\n default: {\n // TODO: 404 page\n return <p>404</p>;\n }\n }\n}\nreturn (\n <Theme>\n <AppLayout page={page}>\n <Page />\n </AppLayout>\n </Theme>\n);\n" }, "components.molecule.FilterByLabel": { "": "const availableOptions = props.availableOptions;\nconst options =\n (availableOptions ?? []).map((i) => {\n return { label: i.title, value: i.value };\n }) ?? [];\noptions.unshift({ label: \"All\", value: null });\nconst setSelected = props.onStateChange ?? (() => {});\nreturn (\n <div>\n <Widget\n src={`devhub.near/widget/devhub.components.molecule.DropDown`}\n props={{\n options: options,\n label: \"Category\",\n onUpdate: (v) => {\n setSelected(v);\n },\n }}\n />\n </div>\n);\n" }, "components.rfps.StageDropdown": { "": "const { RFP_TIMELINE_STATUS } = VM.require(\n `bos.forum.potlock.near/widget/core.common`\n) || { RFP_TIMELINE_STATUS: {} };\nconst setSelected = props.onStateChange ?? (() => {});\nconst timelineStatusArray = Object.entries(RFP_TIMELINE_STATUS).map(\n ([key, value]) => ({\n label: key\n .split(\"_\")\n .map((word) => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase())\n .join(\" \"),\n value,\n })\n);\ntimelineStatusArray.unshift({ label: \"All\", value: null });\nreturn (\n <div>\n <Widget\n src={`devhub.near/widget/devhub.components.molecule.DropDown`}\n props={{\n options: timelineStatusArray,\n label: \"Timeline\",\n onUpdate: (v) => {\n setSelected(v);\n },\n }}\n />\n </div>\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 src={`devhub.near/widget/devhub.entity.proposal.LoginScreen`} />\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\">\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.pages.admin": { "": "const accessControlInfo =\n Near.view(\"forum.potlock.near\", \"get_access_control_info\") ?? null;\nif (!accessControlInfo.members_list) {\n return (\n <Widget src={`devhub.near/widget/devhub.components.molecule.Spinner`} />\n );\n}\nconst rootMembers = Near.view(\"forum.potlock.near\", \"get_root_members\") ?? null;\nconst teamNames = Object.keys(rootMembers || {});\nconst isModerator = Near.view(\n \"forum.potlock.near\",\n \"is_allowed_to_write_rfps\",\n {\n editor: context.accountId,\n }\n);\nconst noPermissionBanner = (\n <div className=\"d-flex flex-column justify-content-center align-items-center\">\n <h2 className=\"alert alert-danger p-3 h6\">\n Your account does not have administration permissions.\n </h2>\n </div>\n);\nif (!isModerator) {\n return noPermissionBanner;\n}\nfunction createEditTeam({\n teamName,\n members,\n description,\n contractCall, // typescript edit_member || add_member\n}) {\n let txn = [];\n const membersAndTeams = Object.keys(accessControlInfo.members_list);\n members.forEach((member) => {\n // if Contract panic member does not exist in the members_list\n if (!membersAndTeams.includes(member)) {\n // Add member\n txn.push({\n contractName: \"forum.potlock.near\",\n methodName: \"add_member\",\n args: {\n member: member,\n metadata: {\n member_metadata_version: \"V0\",\n description: description,\n permissions: {\n \"*\": [\"use-labels\", \"edit-post\"],\n },\n children: [],\n parents: [],\n },\n },\n gas: Big(10).pow(14),\n });\n }\n });\n // Check edit team\n Near.call([\n ...txn,\n {\n contractName: \"forum.potlock.near\",\n methodName: contractCall, // add_member || edit_member\n args: {\n member: `team:${teamName}`,\n metadata: {\n member_metadata_version: \"V0\",\n description: description,\n permissions: {\n \"*\": [\"use-labels\", \"edit-post\"],\n },\n children: members,\n parents: [],\n },\n },\n gas: Big(10).pow(14),\n },\n ]);\n}\nconst Container = styled.div`\n width: 100%;\n margin: 0 auto;\n padding: 20px;\n text-align: left;\n min-height: 65vh;\n`;\nconst Tab = styled.button`\n color: rgb(0, 236, 151);\n &:hover {\n color: rgba(0, 236, 151, 0.5);\n }\n`;\nreturn (\n <Container>\n <div className=\"d-flex flex-column gap-4 p-4\">\n <ul class=\"nav nav-tabs\" id=\"myTab\" role=\"tablist\">\n <li class=\"nav-item\" role=\"presentation\">\n <Tab\n className=\"nav-link active\"\n id=\"profile-tab\"\n data-bs-toggle=\"tab\"\n data-bs-target=\"#profile\"\n type=\"button\"\n role=\"tab\"\n aria-controls=\"profile\"\n aria-selected=\"false\"\n >\n Moderators\n </Tab>\n </li>\n <li class=\"nav-item\" role=\"presentation\">\n <Tab\n className=\"nav-link\"\n id=\"announcement-tab\"\n data-bs-toggle=\"tab\"\n data-bs-target=\"#announcement\"\n type=\"button\"\n role=\"tab\"\n aria-controls=\"announcement\"\n aria-selected=\"false\"\n >\n Announcement\n </Tab>\n </li>\n </ul>\n <div class=\"tab-content\" id=\"myTabContent\">\n <div\n class=\"tab-pane fade show active\"\n id=\"profile\"\n role=\"tabpanel\"\n aria-labelledby=\"profile-tab\"\n >\n <Widget\n src={`bos.forum.potlock.near/widget/components.admin.ModeratorsConfigurator`}\n props={{\n accessControlInfo,\n createEditTeam,\n }}\n />\n </div>\n <div\n class=\"tab-pane fade\"\n id=\"announcement\"\n role=\"tabpanel\"\n aria-labelledby=\"announcement-tab\"\n >\n <Widget\n src={`bos.forum.potlock.near/widget/components.admin.AnnouncementConfigurator`}\n props={{\n accessControlInfo,\n createEditTeam,\n }}\n />\n </div>\n </div>\n </div>\n </Container>\n);\n" }, "components.admin.ModeratorsConfigurator": { "": "const { Tile } = VM.require(\n `devhub.near/widget/devhub.components.molecule.Tile`\n) || { Tile: () => <></> };\nconst { accessControlInfo, createEditTeam } = props;\nconst [editModerators, setEditModerators] = useState(false);\nconst [moderators, setModerators] = useState(\n accessControlInfo.members_list[\"team:moderators\"].children || []\n);\nconst handleEditModerators = () => {\n createEditTeam({\n teamName: \"moderators\",\n description:\n \"The moderator group has permissions to create and edit RFPs, edit and manage proposals, and manage admins.\",\n members: moderators,\n contractCall: \"edit_member\",\n });\n};\nconst handleCancelModerators = () => {\n setEditModerators(false);\n setModerators(accessControlInfo.members_list[\"team:moderators\"].children);\n};\nreturn (\n <>\n <h3>Moderators</h3>\n <div className=\"card-body\">\n <h6>\n The moderator group has permissions to create and edit RFPs, edit and\n manage proposals, and manage admins.\n </h6>\n <Widget\n src={`devhub.near/widget/devhub.components.molecule.PostControls`}\n props={{\n icon: \"bi bi-gear-wide-connected\",\n className: \"mb-3\",\n title: \"Edit members\",\n onClick: () => setEditModerators(!editModerators),\n testId: \"edit-members\",\n }}\n />\n </div>\n <Tile className=\"p-3\" style={{ background: \"white\" }}>\n {editModerators ? (\n <>\n <Widget\n src={`bos.forum.potlock.near/widget/components.admin.AccountsEditor`}\n props={{\n data: {\n maxLength: 100,\n placeholder: \"member.near\",\n list: moderators,\n },\n setList: setModerators,\n // Could add a check to see if it is an valid account id.\n validate: (newItem) => true,\n invalidate: () => null,\n }}\n />\n <div\n className={\n \"d-flex align-items-center justify-content-end gap-3 mt-4\"\n }\n >\n <Widget\n src={`devhub.near/widget/devhub.components.molecule.Button`}\n props={{\n classNames: {\n root: \"btn-outline-danger shadow-none border-0\",\n },\n label: \"Cancel\",\n onClick: handleCancelModerators,\n }}\n />\n <Widget\n src={`devhub.near/widget/devhub.components.molecule.Button`}\n props={{\n classNames: { root: \"btn-success\" },\n icon: {\n type: \"bootstrap_icon\",\n variant: \"bi-check-circle-fill\",\n },\n label: \"Submit\",\n onClick: handleEditModerators,\n }}\n />\n </div>\n </>\n ) : (\n <>\n <div class=\"pt-4\">Members</div>\n {moderators && (\n <div class=\"vstack\">\n {moderators.length ? (\n moderators.map((child) => (\n <Tile className=\"w-25 p-3 m-1\" minHeight={10}>\n <Widget\n src={`devhub.near/widget/devhub.components.molecule.ProfileLine`}\n props={{ accountId: child }}\n />\n </Tile>\n ))\n ) : (\n <div>No moderators</div>\n )}\n </div>\n )}\n </>\n )}\n </Tile>\n </>\n);\n" }, "components.molecule.LinkedRfps": { "": "const { readableDate } = VM.require(`devhub.near/widget/core.lib.common`) || {\n readableDate: () => {},\n};\nconst { href } = VM.require(`devhub.near/widget/core.lib.url`) || {\n href: () => {},\n};\nconst linkedRfpIds = props.linkedRfpIds ?? [];\nconst linkedRfpsData = [];\nlinkedRfpIds.map((item) => {\n const data = Near.view(\"forum.potlock.near\", \"get_rfp\", {\n rfp_id: item,\n });\n if (data !== null) {\n linkedRfpsData.push(data);\n }\n});\nconst Container = styled.div`\n a {\n &:hover {\n text-decoration: none !important;\n }\n }\n`;\nreturn (\n <Container className=\"d-flex flex-column gap-3\">\n {linkedRfpsData.map((item) => {\n return (\n <a\n href={href({\n widgetSrc: `bos.forum.potlock.near/widget/app`,\n params: {\n page: \"rfp\",\n id: item.id,\n },\n })}\n target=\"_blank\"\n rel=\"noopener noreferrer\"\n >\n <div className=\"d-flex gap-2\">\n <Widget\n src={`bos.forum.potlock.near/widget/components.molecule.Profile`}\n props={{\n accountId: item.author_id,\n }}\n />\n <div className=\"d-flex flex-column\" style={{ maxWidth: 250 }}>\n <b className=\"text-truncate\">{item.snapshot.name}</b>\n <div className=\"text-sm text-muted\">\n created on {readableDate(item.snapshot.timestamp / 1000000)}\n </div>\n </div>\n </div>\n </a>\n );\n })}\n </Container>\n);\n" }, "components.molecule.ComposeComment": { "": "const proposalId = props.proposalId;\nconst rfpId = props.rfpId;\nconst draftKey = \"AI_PGF_COMMENT_DRAFT\" + proposalId;\nlet draftComment = \"\";\nconst ComposeEmbeddCSS = `\n .CodeMirror {\n border: none !important;\n min-height: 50px !important;\n }\n .editor-toolbar {\n border: none !important;\n }\n .CodeMirror-scroll{\n min-height: 50px !important;\n max-height: 300px !important;\n }\n`;\nconst notifyAccountIds = props.notifyAccountIds ?? [];\nconst accountId = context.accountId;\nconst item = props.item;\nconst [allowGetDraft, setAllowGetDraft] = useState(true);\nconst [comment, setComment] = useState(null);\nconst [isTxnCreated, setTxnCreated] = useState(false);\nconst [handler, setHandler] = useState(\"update\"); // to update editor state on draft and txn approval\nconst [showCommentToast, setCommentToast] = useState(false);\nif (allowGetDraft) {\n draftComment = Storage.privateGet(draftKey);\n}\nuseEffect(() => {\n if (draftComment) {\n setComment(draftComment);\n setAllowGetDraft(false);\n setHandler(\"refreshEditor\");\n }\n}, [draftComment]);\nuseEffect(() => {\n if (draftComment === comment) {\n return;\n }\n const handler = setTimeout(() => {\n Storage.privateSet(draftKey, comment);\n }, 1000);\n return () => {\n clearTimeout(handler);\n };\n}, [comment]);\nuseEffect(() => {\n if (handler === \"update\") {\n return;\n }\n const handler = setTimeout(() => {\n setHandler(\"update\");\n }, 3000);\n return () => {\n clearTimeout(handler);\n };\n}, [handler]);\nif (!accountId) {\n return (\n <div\n style={{\n marginLeft: 10,\n backgroundColor: \"#ECF8FB\",\n border: \"1px solid #E2E6EC\",\n }}\n className=\"d-flex align-items-center gap-1 p-4 rounded-2 flex-wrap flex-md-nowrap\"\n >\n <Link to=\"https://near.org/signup\">\n <Widget\n src={`devhub.near/widget/devhub.components.molecule.Button`}\n props={{\n classNames: { root: \"grey-btn\" },\n label: \"Sign up\",\n }}\n />\n </Link>\n <div className=\"fw-bold\">to join this conversation.</div>\n <div>Already have an account?</div>\n <a className=\"text-decoration-underline\" href=\"https://near.org/signin\">\n Log in to comment\n </a>\n </div>\n );\n}\nfunction extractMentions(text) {\n const mentionRegex =\n /@((?:(?:[a-z\\d]+[-_])*[a-z\\d]+\\.)*(?:[a-z\\d]+[-_])*[a-z\\d]+)/gi;\n mentionRegex.lastIndex = 0;\n const accountIds = new Set();\n for (const match of text.matchAll(mentionRegex)) {\n if (\n !/[\\w`]/.test(match.input.charAt(match.index - 1)) &&\n !/[/\\w`]/.test(match.input.charAt(match.index + match[0].length)) &&\n match[1].length >= 2 &&\n match[1].length <= 64\n ) {\n accountIds.add(match[1].toLowerCase());\n }\n }\n return [...accountIds];\n}\nfunction extractTagNotifications(text, item) {\n return extractMentions(text || \"\")\n .filter((accountId) => accountId !== context.accountId)\n .map((accountId) => ({\n key: accountId,\n value: {\n type: \"mention\",\n item,\n },\n }));\n}\nfunction composeData() {\n setTxnCreated(true);\n const data = {\n post: {\n comment: JSON.stringify({\n type: \"md\",\n text: comment,\n item,\n }),\n },\n index: {\n comment: JSON.stringify({\n key: item,\n value: {\n type: \"md\",\n },\n }),\n },\n };\n const notifications = extractTagNotifications(comment, {\n type: \"social\",\n path: `${accountId}/post/comment`,\n });\n if (notifyAccountIds.length > 0) {\n notifyAccountIds.map((account) => {\n if (account !== context.accountId) {\n notifications.push({\n key: account,\n value: proposalId\n ? {\n type: \"proposal/reply\",\n item,\n proposal: proposalId,\n widgetAccountId: \"bos.forum.potlock.near\",\n }\n : {\n type: \"rfp/reply\",\n item,\n rfp: rfpId,\n widgetAccountId: \"bos.forum.potlock.near\",\n },\n });\n }\n });\n }\n if (notifications.length) {\n data.index.notify = JSON.stringify(\n notifications.length > 1 ? notifications : notifications[0]\n );\n }\n Social.set(data, {\n force: true,\n onCommit: () => {\n setCommentToast(true);\n setComment(\"\");\n setHandler(\"refreshEditor\");\n setTxnCreated(false);\n },\n onCancel: () => {\n setTxnCreated(false);\n },\n });\n}\nuseEffect(() => {\n if (props.transactionHashes && comment) {\n setComment(\"\");\n }\n}, [props.transactionHashes]);\nconst LoadingButtonSpinner = (\n <span\n class=\"comment-btn-spinner spinner-border spinner-border-sm\"\n role=\"status\"\n aria-hidden=\"true\"\n ></span>\n);\nconst Compose = useMemo(() => {\n return (\n <Widget\n src={`bos.forum.potlock.near/widget/components.molecule.Compose`}\n props={{\n data: comment,\n onChangeKeyup: setComment,\n autocompleteEnabled: true,\n placeholder: \"Add your comment here...\",\n height: \"250\",\n embeddCSS: ComposeEmbeddCSS,\n handler: handler,\n showProposalIdAutoComplete: true,\n }}\n />\n );\n}, [draftComment, handler]);\nreturn (\n <div className=\"d-flex gap-2\">\n <Widget\n src={`near/widget/DIG.Toast`}\n props={{\n title: \"Comment Submitted Successfully\",\n type: \"success\",\n open: showCommentToast,\n onOpenChange: (v) => setCommentToast(v),\n trigger: <></>,\n providerProps: { duration: 3000 },\n }}\n />\n <Widget\n src={`bos.forum.potlock.near/widget/components.molecule.Profile`}\n props={{\n accountId: accountId,\n }}\n />\n <div className=\"d-flex flex-column gap-2 w-100\">\n <b className=\"mt-1\">Add a comment</b>\n {Compose}\n <div className=\"d-flex gap-2 align-content-center justify-content-end\">\n <Widget\n src={`devhub.near/widget/devhub.components.molecule.Button`}\n props={{\n label: isTxnCreated ? LoadingButtonSpinner : \"Comment\",\n [\"data-testid\"]: \"compose-comment\",\n disabled: !comment || isTxnCreated,\n classNames: { root: \"green-btn btn-sm\" },\n onClick: () => {\n composeData();\n },\n }}\n />\n </div>\n </div>\n </div>\n);\n" }, "components.admin.AnnouncementConfigurator": { "": "const { Tile } = VM.require(\n `devhub.near/widget/devhub.components.molecule.Tile`\n) || { Tile: () => <></> };\nconst item = {\n path: `forum.potlock.near/profile/**`,\n};\nconst profile = Social.get(item.path);\nif (!profile.announcement) {\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}\nconst initialData = profile.announcement;\nconst [content, setContent] = useState(null);\nconst [showCommentToast, setCommentToast] = useState(false);\nconst [handler, setHandler] = useState(null);\nconst [isTxnCreated, setTxnCreated] = useState(false);\nconst Container = styled.div`\n width: 100%;\n margin: 0 auto;\n padding: 20px;\n text-align: left;\n`;\nconst hasDataChanged = () => {\n return content !== initialData;\n};\nconst handlePublish = () => {\n setTxnCreated(true);\n Near.call([\n {\n contractName: \"forum.potlock.near\",\n methodName: \"set_social_db_profile_announcement\",\n args: { announcement: content },\n gas: 270000000000000,\n },\n ]);\n};\nuseEffect(() => {\n if (isTxnCreated) {\n const checkForAnnouncementInSocialDB = () => {\n Near.asyncView(REPL_SOCIAL_CONTRACT, \"get\", {\n keys: [item.path],\n }).then((result) => {\n try {\n const submittedAboutText = content;\n const lastAboutTextFromSocialDB =\n result[\"forum.potlock.near\"].profile.description;\n if (submittedAboutText === lastAboutTextFromSocialDB) {\n setTxnCreated(false);\n setCommentToast(true);\n return;\n }\n } catch (e) {}\n setTimeout(() => checkForAnnouncementInSocialDB(), 2000);\n });\n };\n checkForAnnouncementInSocialDB();\n }\n}, [isTxnCreated]);\nuseEffect(() => {\n if (!content && initialData) {\n setContent(initialData);\n setHandler(\"update\");\n }\n}, [initialData]);\nfunction Preview() {\n return (\n <Tile className=\"p-3\" style={{ background: \"white\" }}>\n <Widget\n src={`bos.forum.potlock.near/widget/components.molecule.Markdown`}\n props={{\n content: content,\n }}\n />\n </Tile>\n );\n}\nreturn (\n <Container>\n <Widget\n src={`near/widget/DIG.Toast`}\n props={{\n title: \"Announcement updated successfully\",\n type: \"success\",\n open: showCommentToast,\n onOpenChange: (v) => setCommentToast(v),\n trigger: <></>,\n providerProps: { duration: 3000 },\n }}\n />\n <ul className=\"nav nav-tabs\" id=\"editPreviewTabs\" role=\"tablist\">\n <li className=\"nav-item\" role=\"presentation\">\n <button\n className=\"nav-link active\"\n id=\"edit-tab\"\n data-bs-toggle=\"tab\"\n data-bs-target=\"#edit\"\n type=\"button\"\n role=\"tab\"\n aria-controls=\"edit\"\n aria-selected=\"true\"\n >\n Edit\n </button>\n </li>\n <li className=\"nav-item\" role=\"presentation\">\n <button\n className=\"nav-link\"\n id=\"preview-tab\"\n data-bs-toggle=\"tab\"\n data-bs-target=\"#preview\"\n type=\"button\"\n role=\"tab\"\n aria-controls=\"preview\"\n aria-selected=\"false\"\n >\n Preview\n </button>\n </li>\n </ul>\n <div className=\"tab-content\" id=\"editPreviewTabsContent\">\n <div\n className=\"tab-pane show active py-4\"\n id=\"edit\"\n role=\"tabpanel\"\n aria-labelledby=\"edit-tab\"\n >\n <Widget\n src={`bos.forum.potlock.near/widget/components.molecule.SimpleMDE`}\n props={{\n data: { handler: handler, content: content },\n onChangeKeyup: (v) => {\n setContent(v);\n },\n showAutoComplete: true,\n }}\n />\n <div\n className={\"d-flex align-items-center justify-content-end gap-3 mt-4\"}\n >\n <Widget\n src={`devhub.near/widget/devhub.components.molecule.Button`}\n props={{\n classNames: { root: \"btn-success\" },\n disabled: !hasDataChanged(),\n icon: {\n type: \"bootstrap_icon\",\n variant: \"bi-check-circle-fill\",\n },\n label: \"Publish\",\n onClick: handlePublish,\n }}\n />\n </div>\n </div>\n <div\n className=\"tab-pane\"\n id=\"preview\"\n role=\"tabpanel\"\n aria-labelledby=\"preview-tab\"\n style={{ position: \"relative\" }}\n >\n <div className=\"w-100 h-100 py-4\">\n <Preview />\n </div>\n </div>\n </div>\n </Container>\n);\n" }, "components.rfps.Feed": { "": "const { fetchGraphQL } = VM.require(\n `bos.forum.potlock.near/widget/core.common`\n) || { fetchGraphQL: () => {} };\nconst { href } = VM.require(`devhub.near/widget/core.lib.url`);\nhref || (href = () => {});\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 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 .card.no-border {\n border-left: none !important;\n border-right: none !important;\n margin-bottom: -3.5rem;\n }\n @media screen and (max-width: 768px) {\n font-size: 13px;\n }\n .text-sm {\n font-size: 13px;\n }\n .bg-blue {\n background-image: linear-gradient(to bottom, #4b7a93, #213236);\n color: white;\n }\n .border-bottom {\n border-bottom: 1px solid grey;\n }\n .cursor-pointer {\n cursor: pointer;\n }\n .rfp-card {\n border-left: none !important;\n border-right: none !important;\n border-bottom: none !important;\n &:hover {\n background-color: #f4f4f4;\n }\n }\n .blue-btn {\n background-color: #3c697d !important;\n border: none;\n color: white;\n &:active {\n color: white;\n }\n }\n .bg-grey {\n background: #e2e6ec;\n }\n @media screen and (max-width: 768px) {\n .blue-btn {\n padding: 0.5rem 0.8rem !important;\n min-height: 32px;\n }\n }\n a.no-space {\n display: inline-block;\n }\n .text-wrap {\n overflow: hidden;\n white-space: normal;\n }\n .fw-semi-bold {\n font-weight: 500;\n }\n`;\nconst Heading = styled.div`\n font-size: 24px;\n font-weight: 700;\n width: 100%;\n .text-normal {\n font-weight: normal !important;\n }\n @media screen and (max-width: 768px) {\n font-size: 18px;\n }\n`;\nconst rfpLabelOptions = getGlobalLabels();\nconst FeedItem = ({ rfp, index }) => {\n const accountId = rfp.author_id;\n const profile = Social.get(`${accountId}/profile/**`, \"final\");\n // We will have to get the rfp from the contract to get the block height.\n const blockHeight = parseInt(rfp.social_db_post_block_height);\n const item = {\n type: \"social\",\n path: `forum.potlock.near/post/main`,\n blockHeight: blockHeight,\n };\n return (\n <a\n href={href({\n widgetSrc: `bos.forum.potlock.near/widget/app`,\n params: {\n page: \"rfp\",\n id: rfp.rfp_id,\n },\n })}\n onClick={(e) => e.stopPropagation()}\n style={{ textDecoration: \"none\" }}\n >\n <div\n className={\n \"rfp-card d-flex justify-content-between gap-2 text-muted cursor-pointer p-3 w-100 flex-wrap flex-sm-nowrap \" +\n (index !== 0 && \" border\")\n }\n >\n <div className=\"d-flex gap-4 w-100\">\n <Widget\n src={`bos.forum.potlock.near/widget/components.molecule.Profile`}\n props={{\n accountId: rfp.author_id,\n }}\n />\n <div className=\"d-flex flex-column gap-2 w-100 text-wrap\">\n <div className=\"d-flex gap-2 align-items-center flex-wrap w-100\">\n <div className=\"h6 mb-0 text-black\">{rfp.name}</div>\n <Widget\n src={`bos.forum.potlock.near/widget/components.molecule.MultiSelectCategoryDropdown`}\n props={{\n selected: rfp.labels,\n disabled: true,\n hideDropdown: true,\n onChange: () => {},\n availableOptions: rfpLabelOptions,\n }}\n />\n </div>\n <div className=\"d-flex gap-2 align-items-center flex-wrap flex-sm-nowrap text-sm w-100\">\n <div>#{rfp.rfp_id} ・ Created</div>\n <Widget\n src={`near/widget/TimeAgo`}\n props={{\n blockHeight,\n blockTimestamp: rfp.timestamp,\n }}\n />\n </div>\n <div className=\"d-flex gap-4 flex-wrap flex-sm-nowrap text-sm w-100 text-muted my-2\">\n <div\n className=\"d-flex flex-column gap-1\"\n style={{ maxWidth: \"70%\" }}\n >\n <div className=\"fw-semi-bold\">Summay</div>\n <div>{rfp.summary}</div>\n </div>\n <div style={{ width: \"1px\" }} className=\"bg-grey\"></div>\n <div className=\"d-flex flex-column gap-1\">\n <div className=\"fw-semi-bold\">Submission Deadline</div>\n <h6 className=\"mb-0 text-black\">\n {readableDate(rfp.submission_deadline / 1000000)}\n </h6>\n </div>\n </div>\n <div className=\"d-flex gap-2 align-items-center text-sm\">\n <div>\n <img\n src=\"https://ipfs.near.social/ipfs/bafkreif4p376f3qvpb2ewwsmi6fkcm3jalhuuzuxbgvehgl552agqw47ju\"\n height={30}\n width={30}\n />\n {rfp.linked_proposals.length ?? 0}\n proposals\n </div>\n <div className=\"d-flex align-items-center\">\n <Widget\n src={`devhub.near/widget/devhub.entity.proposal.CommentIcon`}\n props={{\n item,\n showOverlay: false,\n onClick: () => {},\n }}\n />\n comments\n </div>\n </div>\n </div>\n </div>\n <div className=\"align-self-center\" style={{ minWidth: \"fit-content\" }}>\n <Widget\n src={`bos.forum.potlock.near/widget/components.rfps.StatusTag`}\n props={{\n timelineStatus: rfp.timeline.status,\n }}\n />\n </div>\n </div>\n </a>\n );\n};\nconst getRfp = (rfp_id) => {\n return Near.asyncView(\"forum.potlock.near\", \"get_rfp\", {\n rfp_id,\n });\n};\nconst FeedPage = () => {\n State.init({\n data: [],\n cachedItems: {},\n stage: \"\",\n sort: \"\",\n label: \"\",\n input: \"\",\n loading: false,\n loadingMore: false,\n aggregatedCount: null,\n currentlyDisplaying: 0,\n isFiltered: false,\n });\n const queryName =\n \"bos_forum_potlock_near_ai_pgf_indexer_rfps_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: {rfp_id: desc}\n where: $where\n ) {\n author_id\n block_height\n name\n summary\n editor_id\n rfp_id\n timeline\n views\n labels\n submission_deadline\n linked_proposals\n }\n ${queryName}_aggregate(\n order_by: {rfp_id: desc}\n where: $where\n ) {\n aggregate {\n count\n }\n }\n }`;\n function separateNumberAndText(str) {\n const numberRegex = /\\d+/;\n if (numberRegex.test(str)) {\n const number = str.match(numberRegex)[0];\n const text = str.replace(numberRegex, \"\").trim();\n return { number: parseInt(number), text };\n } else {\n return { number: null, text: str.trim() };\n }\n }\n const buildWhereClause = () => {\n let where = {};\n if (state.label) {\n where = { labels: { _contains: state.label }, ...where };\n }\n if (state.stage) {\n // timeline is stored as jsonb\n where = {\n timeline: { _cast: { String: { _regex: `${state.stage}` } } },\n ...where,\n };\n }\n if (state.input) {\n const { number, text } = separateNumberAndText(state.input);\n if (number) {\n where = { rfp_id: { _eq: number }, ...where };\n }\n if (text) {\n where = {\n _or: [\n { name: { _iregex: `${text}` } },\n { summary: { _iregex: `${text}` } },\n { description: { _iregex: `${text}` } },\n ],\n ...where,\n };\n }\n }\n State.update({ isFiltered: Object.keys(where).length > 0 });\n return where;\n };\n const buildOrderByClause = () => {\n /**\n * TODO\n * Most commented -> edit contract and indexer\n * Unanswered -> 0 comments\n */\n };\n const makeMoreItems = () => {\n if (state.aggregatedCount <= state.currentlyDisplaying) return;\n fetchRfps(state.data.length);\n };\n const fetchRfps = (offset) => {\n if (!offset) {\n offset = 0;\n }\n if (state.loading) return;\n const FETCH_LIMIT = 10;\n const variables = {\n limit: FETCH_LIMIT,\n offset,\n where: buildWhereClause(),\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 totalResult = result.body.data?.[`${queryName}_aggregate`];\n State.update({ aggregatedCount: totalResult.aggregate.count });\n // Parse timeline\n fetchBlockHeights(data, offset);\n }\n }\n });\n };\n const renderItem = (item, index) => (\n <div\n key={item.rfp_id}\n className={\n (index !== state.data.length - 1 && \"border-bottom \") + index === 0 &&\n \" rounded-top-2 rfp-item-container\"\n }\n >\n <FeedItem rfp={item} index={index} />\n </div>\n );\n const cachedRenderItem = (item, index) => {\n if (props.term) {\n return renderItem(item, {\n searchKeywords: [props.term],\n });\n }\n const key = JSON.stringify(item);\n if (!(key in state.cachedItems)) {\n state.cachedItems[key] = renderItem(item, index);\n State.update();\n }\n return state.cachedItems[key];\n };\n useEffect(() => {\n fetchRfps();\n }, [state.input, state.sort, state.label, state.stage]);\n const mergeItems = (newItems) => {\n const items = [\n ...new Set([...newItems, ...state.data].map((i) => JSON.stringify(i))),\n ].map((i) => JSON.parse(i));\n // Sorting in the front end\n if (state.sort === \"rfp_id\" || state.sort === \"\") {\n items.sort((a, b) => b.rfp_id - a.rfp_id);\n } else if (state.sort === \"views\") {\n items.sort((a, b) => b.views - a.views);\n }\n return items;\n };\n const fetchBlockHeights = (data, offset) => {\n let promises = data.map((item) => getRfp(item.rfp_id));\n Promise.all(promises).then((blockHeights) => {\n data = data.map((item, index) => ({\n ...item,\n timeline: JSON.parse(item.timeline),\n social_db_post_block_height:\n blockHeights[index].social_db_post_block_height,\n }));\n if (offset) {\n let newData = mergeItems(data);\n State.update({\n data: newData,\n currentlyDisplaying: newData.length,\n loading: false,\n });\n } else {\n State.update({\n data,\n currentlyDisplaying: data.length,\n loading: false,\n });\n }\n });\n };\n const loader = (\n <div className=\"d-flex justify-content-center align-items-center w-100\">\n <Widget src={`devhub.near/widget/devhub.components.molecule.Spinner`} />\n </div>\n );\n const renderedItems = state.data ? state.data.map(cachedRenderItem) : null;\n const isAllowedToWriteRfp = Near.view(\n \"forum.potlock.near\",\n \"is_allowed_to_write_rfps\",\n {\n editor: context.accountId,\n }\n );\n return (\n <Container className=\"w-100 py-4 px-2 d-flex flex-column gap-3\">\n <div className=\"d-flex justify-content-between flex-wrap gap-2 align-items-center\">\n <Heading>\n RFPs\n <span className=\"text-muted text-normal\">\n ({state.aggregatedCount ?? state.data.length}){\" \"}\n </span>\n </Heading>\n <div className=\"d-flex flex-wrap gap-4 align-items-center\">\n <Widget\n src={`devhub.near/widget/devhub.feature.proposal-search.by-input`}\n props={{\n search: state.input,\n className: \"w-xs-100\",\n onSearch: (input) => {\n State.update({ input });\n fetchRfps();\n },\n onEnter: () => {\n fetchRfps();\n },\n }}\n />\n <Widget\n src={`devhub.near/widget/devhub.feature.proposal-search.by-sort`}\n props={{\n onStateChange: (select) => {\n State.update({ sort: select.value });\n },\n }}\n />\n <Widget\n src={`bos.forum.potlock.near/widget/components.molecule.FilterByLabel`}\n props={{\n onStateChange: (select) => {\n State.update({ label: select.value });\n },\n availableOptions: rfpLabelOptions,\n }}\n />\n <div className=\"d-flex gap-4 align-items-center\">\n <Widget\n src={`bos.forum.potlock.near/widget/components.rfps.StageDropdown`}\n props={{\n onStateChange: (select) => {\n State.update({ stage: select.value });\n },\n }}\n />\n </div>\n </div>\n {isAllowedToWriteRfp && (\n <div className=\"mt-2 mt-xs-0\">\n <Link\n to={href({\n widgetSrc: `bos.forum.potlock.near/widget/app`,\n params: { page: \"create-rfp\" },\n })}\n >\n <Widget\n src={`devhub.near/widget/devhub.components.molecule.Button`}\n props={{\n label: (\n <div className=\"d-flex gap-2 align-items-center\">\n <div>\n <i className=\"bi bi-plus-circle-fill\"></i>\n </div>\n Create RFP\n </div>\n ),\n classNames: { root: \"blue-btn\" },\n }}\n />\n </Link>\n </div>\n )}\n </div>\n <div style={{ minHeight: \"50vh\" }}>\n {!Array.isArray(state.data) ? (\n loader\n ) : (\n <div className=\"card no-border rounded-0 mt-4 py-3 full-width-div\">\n <div className=\"container-xl\">\n <div>\n <Widget\n src={`bos.forum.potlock.near/widget/components.pages.announcement`}\n loading=\"\"\n />\n </div>\n <div className=\"mt-4 border rounded-2\">\n {state.aggregatedCount === 0 ? (\n <div className=\"m-2\">\n {state.isFiltered ? (\n <div class=\"alert alert-danger\" role=\"alert\">\n No RFP found for selected filter.\n </div>\n ) : (\n <div class=\"alert alert-secondary\" role=\"alert\">\n No RFP has been created yet.\n </div>\n )}\n </div>\n ) : state.aggregatedCount > 0 ? (\n <InfiniteScroll\n pageStart={0}\n loadMore={makeMoreItems}\n hasMore={state.aggregatedCount > state.data.length}\n loader={loader}\n useWindow={false}\n threshold={100}\n >\n {renderedItems}\n </InfiniteScroll>\n ) : (\n loader\n )}\n </div>\n </div>\n </div>\n )}\n </div>\n </Container>\n );\n};\nreturn FeedPage(props);\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://near.social/near/widget/ProfilePage?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.LinkedProposalsDropdown": { "": "const { fetchGraphQL } = VM.require(\n `bos.forum.potlock.near/widget/core.common`\n) || { fetchGraphQL: () => {} };\nconst { href } = VM.require(`devhub.near/widget/core.lib.url`);\nhref || (href = () => {});\nconst linkedProposals = props.linkedProposals;\nconst onChange = props.onChange;\nconst [selectedProposals, setSelectedProposals] = useState(linkedProposals);\nconst [proposalsOptions, setProposalsOptions] = useState([]);\nconst [searchProposalId, setSearchProposalId] = useState(\"\");\nconst queryName =\n \"bos_forum_potlock_near_ai_pgf_indexer_proposals_with_latest_snapshot\";\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: {proposal_id: desc}\n where: $where\n) {\n name\n proposal_id\n}\n}`;\nuseEffect(() => {\n if (JSON.stringify(linkedProposals) !== JSON.stringify(selectedProposals)) {\n setSelectedProposals(linkedProposals);\n }\n}, [linkedProposals]);\nuseEffect(() => {\n if (JSON.stringify(linkedProposals) !== JSON.stringify(selectedProposals)) {\n onChange(selectedProposals);\n }\n}, [selectedProposals]);\nfunction separateNumberAndText(str) {\n const numberRegex = /\\d+/;\n if (numberRegex.test(str)) {\n const number = str.match(numberRegex)[0];\n const text = str.replace(numberRegex, \"\").trim();\n return { number: parseInt(number), text };\n } else {\n return { number: null, text: str.trim() };\n }\n}\nconst buildWhereClause = () => {\n let where = {};\n const { number, text } = separateNumberAndText(searchProposalId);\n if (number) {\n where = { proposal_id: { _eq: number }, ...where };\n }\n if (text) {\n where = {\n _or: [\n { name: { _iregex: `${text}` } },\n { summary: { _iregex: `${text}` } },\n { description: { _iregex: `${text}` } },\n ],\n ...where,\n };\n }\n return where;\n};\nconst fetchProposals = () => {\n const FETCH_LIMIT = 30;\n const variables = {\n limit: FETCH_LIMIT,\n offset: 0,\n where: buildWhereClause(),\n };\n fetchGraphQL(query, \"GetLatestSnapshot\", variables).then(async (result) => {\n if (result.status === 200) {\n if (result.body.data) {\n const proposalsData = result.body.data?.[queryName];\n const data = [];\n for (const prop of proposalsData) {\n data.push({\n label: \"# \" + prop.proposal_id + \" : \" + prop.name,\n value: prop.proposal_id,\n });\n }\n setProposalsOptions(data);\n }\n }\n });\n};\nuseEffect(() => {\n fetchProposals();\n}, [searchProposalId]);\nreturn (\n <>\n {selectedProposals.map((proposal) => {\n return (\n <div className=\"d-flex gap-2 align-items-center\">\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: proposal.value,\n },\n })}\n target=\"_blank\"\n rel=\"noopener noreferrer\"\n >\n {proposal.label}\n </a>\n <div\n className=\"cursor-pointer\"\n onClick={() => {\n const updatedLinkedProposals = selectedProposals.filter(\n (item) => item.value !== proposal.value\n );\n setSelectedProposals(updatedLinkedProposals);\n }}\n >\n <i className=\"bi bi-trash3-fill\"></i>\n </div>\n </div>\n );\n })}\n <Widget\n src={`bos.forum.potlock.near/widget/components.molecule.DropDownWithSearch`}\n props={{\n selectedValue: selectedProposals,\n onChange: (v) => {\n if (!selectedProposals.some((item) => item.value === v.value)) {\n setSelectedProposals([...selectedProposals, v]);\n }\n },\n options: proposalsOptions,\n showSearch: true,\n searchInputPlaceholder: \"Search by Id\",\n defaultLabel: \"Search proposals\",\n searchByValue: true,\n onSearch: (value) => {\n setSearchProposalId(value);\n },\n }}\n />\n </>\n);\n" }, "components.admin.AccountsEditor": { "": "const { data, setList, validate, invalidate } = props;\nconst [newItem, setNewItem] = useState(\"\");\nconst handleAddItem = () => {\n if (validate(newItem)) {\n setList([...data.list, newItem]);\n setNewItem(\"\");\n } else {\n return invalidate();\n }\n};\nconst handleDeleteItem = (index) => {\n const updatedData = [...data.list];\n updatedData.splice(index, 1);\n setList(updatedData);\n};\nconst Item = styled.div`\n padding: 10px;\n margin: 5px;\n display: flex;\n align-items: center;\n flex-direction: row;\n gap: 10px;\n`;\nreturn (\n <>\n {data.list.map((item, index) => (\n <Item key={index}>\n <div className=\"flex-grow-1\">\n <Widget\n src={`devhub.near/widget/devhub.components.molecule.Input`}\n props={{\n className: \"flex-grow-1\",\n value: item,\n skipPaddingGap: true,\n placeholder: data.placeholder,\n inputProps: {\n prefix: data.prefix,\n disabled: true,\n },\n }}\n />\n </div>\n <button\n className=\"btn btn-outline-danger\"\n onClick={() => handleDeleteItem(index)}\n >\n <i className=\"bi bi-trash-fill\" />\n </button>\n </Item>\n ))}\n {data.list.length < data.maxLength && (\n <Item>\n <div className=\"flex-grow-1\">\n <Widget\n src={`bos.forum.potlock.near/widget/components.molecule.AccountInput`}\n props={{\n onUpdate: (value) => setNewItem(value),\n value: newItem,\n placeholder: data.placeholder,\n }}\n />\n </div>\n <button\n className=\"btn btn-success add-member\"\n onClick={handleAddItem}\n disabled={newItem === \"\"}\n data-testid=\"add-to-list\"\n >\n <i className=\"bi bi-plus\" />\n </button>\n </Item>\n )}\n </>\n);\n" }, "components.molecule.MultiSelectCategoryDropdown": { "": "const { href } = VM.require(`devhub.near/widget/core.lib.url`);\nhref || (href = () => {});\nconst {\n selected,\n onChange,\n disabled,\n availableOptions,\n hideDropdown,\n linkedRfp,\n} = props;\nconst [selectedOptions, setSelectedOptions] = useState([]);\nconst [isOpen, setIsOpen] = useState(false);\nconst [initialStateApplied, setInitialState] = useState(false);\nconst toggleDropdown = () => {\n setIsOpen(!isOpen);\n};\nuseEffect(() => {\n if (JSON.stringify(selectedOptions) !== JSON.stringify(selected)) {\n if (availableOptions.length > 0) {\n if ((selected ?? []).some((i) => !i.value)) {\n setSelectedOptions(\n selected.map((i) => availableOptions.find((t) => t.value === i))\n );\n } else {\n setSelectedOptions(selected);\n }\n setInitialState(true);\n }\n } else {\n setInitialState(true);\n }\n}, [selected, availableOptions]);\nuseEffect(() => {\n if (\n JSON.stringify(selectedOptions) !== JSON.stringify(selected) &&\n initialStateApplied\n ) {\n onChange(selectedOptions);\n }\n}, [selectedOptions, initialStateApplied]);\nconst Container = styled.div`\n .drop-btn {\n width: 100%;\n text-align: left;\n padding-inline: 10px;\n }\n .dropdown-toggle:after {\n position: absolute;\n top: 46%;\n right: 2%;\n }\n .dropdown-menu {\n width: 100%;\n }\n .dropdown-item.active,\n .dropdown-item:active {\n background-color: #f0f0f0 !important;\n color: black;\n }\n .disabled {\n background-color: #f8f8f8 !important;\n cursor: not-allowed !important;\n border-radius: 5px;\n opacity: inherit !important;\n }\n .disabled.dropdown-toggle::after {\n display: none !important;\n }\n .custom-select {\n position: relative;\n }\n .selected {\n background-color: #f0f0f0;\n }\n .cursor-pointer {\n cursor: pointer;\n }\n .text-wrap {\n overflow: hidden;\n white-space: normal;\n }\n`;\nconst handleOptionClick = (option) => {\n if (!selectedOptions.some((item) => item.value === option.value)) {\n setSelectedOptions([...selectedOptions, option]);\n }\n setIsOpen(false);\n};\nconst Item = ({ option }) => {\n return <div> {option.title}</div>;\n};\nreturn (\n <>\n <div className=\"d-flex gap-2 align-items-center\">\n {(selectedOptions ?? []).map((option) => {\n return (\n <div\n style={{\n color: \"white\",\n backgroundColor: `rgb(${option.color})`,\n width: \"max-content\",\n }}\n className=\"d-flex gap-2 align-items-center badge rounded-lg p-2 h6 mb-0\"\n >\n {option.title}\n {!disabled && (\n <div\n className=\"cursor-pointer\"\n onClick={() => {\n const updatedOptions = selectedOptions.filter(\n (item) => item.value !== option.value\n );\n setSelectedOptions(updatedOptions);\n }}\n >\n <i className=\"bi bi-trash3-fill\"></i>\n </div>\n )}\n </div>\n );\n })}\n </div>\n {!hideDropdown && (\n <Container>\n <div\n className=\"custom-select w-100\"\n tabIndex=\"0\"\n onBlur={() => setIsOpen(false)}\n >\n <div\n className={\n \"dropdown-toggle bg-white border rounded-2 btn drop-btn w-100 \" +\n (disabled ? \"disabled\" : \"\")\n }\n onClick={!disabled && toggleDropdown}\n >\n <div className={`selected-option`}>\n {linkedRfp ? (\n <span className=\"text-sm d-flex gap-2 align-items-center\">\n <i class=\"bi bi-lock-fill\"></i>\n These categories match the chosen RFP and cannot be changed.\n To use different categories, unlink the RFP.\n </span>\n ) : (\n <span>Select Category </span>\n )}\n </div>\n </div>\n {isOpen && (\n <div className=\"dropdown-menu rounded-2 dropdown-menu-end dropdown-menu-lg-start px-2 shadow show w-100\">\n <div>\n {(availableOptions ?? []).map((option) => (\n <div\n key={option.value}\n className={`dropdown-item cursor-pointer w-100 my-1 ${\n (selectedOptions ?? []).find(\n (item) => item.value === option.value\n )\n ? \"selected\"\n : \"\"\n }`}\n onClick={() => handleOptionClick(option)}\n >\n <Item option={option} />\n </div>\n ))}\n </div>\n </div>\n )}\n </div>\n </Container>\n )}\n </>\n);\n" }, "components.molecule.LikeButton": { "": "const item = props.item;\nconst proposalId = props.proposalId;\nconst rfpId = props.rfpId;\nconst notifyAccountIds = props.notifyAccountIds ?? [];\nif (!item) {\n return \"\";\n}\nconst likes = Social.index(\"like\", item);\nconst dataLoading = likes === null;\nconst likesByUsers = {};\n(likes || []).forEach((like) => {\n if (like.value.type === \"like\") {\n likesByUsers[like.accountId] = like;\n } else if (like.value.type === \"unlike\") {\n delete likesByUsers[like.accountId];\n }\n});\nif (state.hasLike === true) {\n likesByUsers[context.accountId] = {\n accountId: context.accountId,\n };\n} else if (state.hasLike === false) {\n delete likesByUsers[context.accountId];\n}\nconst accountsWithLikes = Object.keys(likesByUsers);\nconst hasLike = context.accountId && !!likesByUsers[context.accountId];\nconst hasLikeOptimistic =\n state.hasLikeOptimistic === undefined ? hasLike : state.hasLikeOptimistic;\nconst totalLikes =\n accountsWithLikes.length +\n (hasLike === false && state.hasLikeOptimistic === true ? 1 : 0) -\n (hasLike === true && state.hasLikeOptimistic === false ? 1 : 0);\nconst LikeButton = styled.button`\n border: 0;\n display: inline-flex;\n align-items: center;\n gap: 6px;\n color: #687076;\n font-weight: 400;\n font-size: 14px;\n line-height: 17px;\n cursor: pointer;\n background: none;\n padding: 6px;\n transition: color 200ms;\n i {\n font-size: 16px;\n transition: color 200ms;\n &.bi-heart-fill {\n color: #e5484d !important;\n }\n }\n &:hover,\n &:focus {\n outline: none;\n color: #11181c;\n }\n`;\nconst likeClick = (e) => {\n e.preventDefault();\n e.stopPropagation();\n if (state.loading) {\n return;\n }\n State.update({\n loading: true,\n hasLikeOptimistic: !hasLike,\n });\n const data = {\n index: {\n like: JSON.stringify({\n key: item,\n value: {\n type: hasLike ? \"unlike\" : \"like\",\n },\n }),\n },\n };\n if (!hasLike && notifyAccountIds.length > 0) {\n const notifyData = notifyAccountIds.map((account) => {\n if (account !== context.accountId) {\n return {\n key: account,\n value: proposalId\n ? {\n type: \"proposal/like\",\n item,\n proposal: proposalId,\n widgetAccountId: \"bos.forum.potlock.near\",\n }\n : {\n type: \"rfp/like\",\n item,\n rfp: rfpId,\n widgetAccountId: \"bos.forum.potlock.near\",\n },\n };\n }\n });\n if (notifyData.length > 0) {\n data.index.notify = notifyData;\n }\n }\n Social.set(data, {\n onCommit: () => State.update({ loading: false, hasLike: !hasLike }),\n onCancel: () =>\n State.update({\n loading: false,\n hasLikeOptimistic: !state.hasLikeOptimistic,\n }),\n });\n};\nconst title = hasLike ? \"Unlike\" : \"Like\";\nreturn (\n <LikeButton\n disabled={state.loading || dataLoading || !context.accountId}\n title={title}\n onClick={likeClick}\n >\n <i className={`${hasLikeOptimistic ? \"bi-heart-fill\" : \"bi-heart\"}`} />\n {Object.values(likesByUsers ?? {}).length > 0 ? (\n <span className={`count ${hasLike ? \"liked\" : \"\"}`}>\n <Widget\n loading={likeCount || \"\"}\n src=\"mob.near/widget/N.Overlay.Faces\"\n props={{ accounts: likesByUsers, limit: 10 }}\n />\n </span>\n ) : (\n \"0\"\n )}\n </LikeButton>\n);\n" }, "components.molecule.NavbarDropdown": { "": "const title = props.title;\nconst links = props.links;\nconst href = props.href;\nconst [showMenu, setShowMenu] = useState(false);\nconst { href: linkHref } = VM.require(`devhub.near/widget/core.lib.url`);\nlinkHref || (linkHref = () => {});\nconst Dropdown = styled.div`\n position: relative;\n display: flex;\n flex-direction: column;\n align-items: center;\n p {\n &.active {\n color: #fff;\n &:hover {\n text-decoration: none;\n color: #096d50 !important;\n }\n }\n }\n`;\nconst DropdownMenu = styled.div`\n z-index: 50;\n position: absolute;\n top: 2.25rem;\n &.active {\n padding: 0.5rem 1rem;\n padding-top: 1rem;\n border-radius: 1rem;\n background: rgba(217, 217, 217, 0.7);\n backdrop-filter: blur(5px);\n width: max-content;\n animation: slide-down 300ms ease;\n transform-origin: top center;\n }\n @keyframes slide-down {\n 0% {\n transform: scaleY(0);\n }\n 100% {\n transform: scaleY(1);\n }\n }\n`;\nconst DropdownLink = styled.div`\n color: inherit;\n text-decoration: none;\n &.active {\n color: #555555;\n }\n &:hover {\n text-decoration: none;\n color: #096d50 !important;\n }\n`;\nconst isOutsideLink = (href ?? \"\").includes(\"https\");\nreturn (\n <Dropdown\n onMouseEnter={() => setShowMenu(true)}\n onMouseLeave={() => setShowMenu(false)}\n >\n {href ? (\n <DropdownLink className={href === props.page && \"active\"} href={href}>\n <Link\n style={{ textDecoration: \"none\" }}\n to={\n isOutsideLink\n ? href\n : linkHref({\n widgetSrc: `bos.forum.potlock.near/widget/app`,\n params: { page: href },\n })\n }\n target={isOutsideLink ? \"_blank\" : \"\"}\n >\n {title}\n </Link>\n </DropdownLink>\n ) : (\n <p className={`m-0 py-2 nav-dropdown`} style={{ cursor: \"default\" }}>\n {title} ↓\n </p>\n )}\n {showMenu && links.length !== 0 && (\n <DropdownMenu className={`${showMenu && \"active\"}`}>\n <div className=\"d-flex flex-column gap-3\">\n {links.map((link) => (\n // Check if the link is external\n <DropdownLink\n className={link.href === props.page && \"active\"}\n key={`${link.title}-${link.href}`}\n >\n {link.href.startsWith(\"http://\") ||\n link.href.startsWith(\"https://\") ? (\n // External link: Render an <a> tag\n <a\n href={link.href}\n style={{ textDecoration: \"none\" }}\n target=\"_blank\"\n rel=\"noopener noreferrer\"\n >\n {link.title}\n </a>\n ) : (\n // Internal link: Render the <Link> component\n <Link\n style={{ textDecoration: \"none\" }}\n to={linkHref({\n widgetSrc: `bos.forum.potlock.near/widget/app`,\n params: { page: link.href },\n })}\n >\n {link.title}\n </Link>\n )}\n </DropdownLink>\n ))}\n </div>\n </DropdownMenu>\n )}\n </Dropdown>\n);\n" }, "components.rfps.ConfirmCancelModal": { "": "const { CANCEL_RFP_OPTIONS } = VM.require(\n `bos.forum.potlock.near/widget/core.common`\n) || { CANCEL_RFP_OPTIONS: {} };\nconst isOpen = props.isOpen;\nconst onCancelClick = props.onCancelClick;\nconst onConfirmClick = props.onConfirmClick;\nconst linkedProposalIds = props.linkedProposalIds;\nconst Modal = styled.div`\n display: ${({ hidden }) => (hidden ? \"none\" : \"flex\")};\n position: fixed;\n inset: 0;\n justify-content: center;\n align-items: center;\n opacity: 1;\n z-index: 999;\n .black-btn {\n background-color: #000 !important;\n border: none;\n color: white;\n &:active {\n color: white;\n }\n }\n @media screen and (max-width: 768px) {\n h5 {\n font-size: 16px !important;\n }\n }\n .btn {\n font-size: 14px;\n }\n .bg-grey {\n background: rgb(244, 244, 244) !important;\n max-height: 300px;\n overflow-y: auto;\n }\n`;\nconst ModalBackdrop = styled.div`\n position: absolute;\n inset: 0;\n background-color: rgba(0, 0, 0, 0.5);\n opacity: 0.4;\n`;\nconst ModalDialog = styled.div`\n padding: 2em;\n z-index: 999;\n overflow-y: auto;\n max-height: 85%;\n margin-top: 5%;\n width: 50%;\n @media screen and (max-width: 768px) {\n margin: 2rem;\n width: 100%;\n }\n`;\nconst ModalHeader = styled.div`\n display: flex;\n flex-direction: row;\n justify-content: space-between;\n align-items: center;\n padding-bottom: 4px;\n`;\nconst ModalFooter = styled.div`\n padding-top: 4px;\n display: flex;\n flex-direction: row;\n justify-content: space-between;\n align-items: items-center;\n`;\nconst CloseButton = styled.button`\n display: flex;\n align-items: center;\n justify-content: center;\n background-color: white;\n padding: 0.5em;\n border-radius: 6px;\n border: 0;\n color: #344054;\n &:hover {\n background-color: #d3d3d3;\n }\n`;\nconst ConfirmButton = styled.button`\n padding: 0.7em;\n border-radius: 6px;\n border: 0;\n box-shadow: 0px 1px 2px rgba(16, 24, 40, 0.05);\n background-color: #12b76a;\n color: white;\n &:hover {\n background-color: #0e9f5d;\n }\n`;\nconst ModalContent = styled.div`\n flex: 1;\n font-size: 14px;\n margin-top: 4px;\n margin-bottom: 4px;\n overflow-y: auto;\n max-height: 50%;\n @media screen and (max-width: 768px) {\n font-size: 12px !important;\n }\n`;\nconst NoButton = styled.button`\n background: transparent;\n border: none;\n padding: 0;\n margin: 0;\n box-shadow: none;\n`;\nconst [proposalStatus, setProposalStatus] = useState(null);\nconst OptionForm = useMemo(() => {\n return (\n <div className=\"d-flex flex-column gap-1 pl-2\">\n <Widget\n src={`bos.forum.potlock.near/widget/components.molecule.RadioButton`}\n props={{\n value: CANCEL_RFP_OPTIONS.CANCEL_PROPOSALS,\n label: (\n <div>\n <span className=\"fw-bold\">Option 1: </span>Cancel all linked\n proposals\n </div>\n ),\n isChecked: proposalStatus === CANCEL_RFP_OPTIONS.CANCEL_PROPOSALS,\n onClick: (v) => {\n if (v) {\n setProposalStatus(CANCEL_RFP_OPTIONS.CANCEL_PROPOSALS);\n }\n },\n }}\n />\n <Widget\n src={`bos.forum.potlock.near/widget/components.molecule.RadioButton`}\n props={{\n value: CANCEL_RFP_OPTIONS.UNLINK_PROPOSALS,\n label: (\n <div>\n <span className=\"fw-bold\">Option 2: </span> Unlink all linked\n proposals (maintain their status)\n </div>\n ),\n isChecked: proposalStatus === CANCEL_RFP_OPTIONS.UNLINK_PROPOSALS,\n onClick: (v) => {\n if (v) {\n setProposalStatus(CANCEL_RFP_OPTIONS.UNLINK_PROPOSALS);\n }\n },\n }}\n />\n <Widget\n src={`bos.forum.potlock.near/widget/components.molecule.RadioButton`}\n props={{\n value: CANCEL_RFP_OPTIONS.NONE,\n label: (\n <div>\n <span className=\"fw-bold\">Option 3: </span> Leave all linked\n proposals as they are\n </div>\n ),\n isChecked: proposalStatus === CANCEL_RFP_OPTIONS.NONE,\n onClick: (v) => {\n if (v) {\n setProposalStatus(CANCEL_RFP_OPTIONS.NONE);\n }\n },\n }}\n />\n </div>\n );\n}, [proposalStatus]);\nreturn (\n <>\n <Modal hidden={!isOpen}>\n <ModalBackdrop />\n <ModalDialog className=\"card\">\n <ModalHeader>\n <h5 className=\"mb-0\">Are you sure you want to cancel this RFP?</h5>\n </ModalHeader>\n <ModalContent className=\"text-muted d-flex flex-column gap-2\">\n The RFP status will change to “Cancelled” and it will no longer be\n active or relevant. Comments will remain open.\n <div className=\"bg-grey d-flex flex-column p-3 rounded-1 text-black\">\n <div className=\"h6\">\n Linked Proposals ({linkedProposalIds.length})\n </div>\n <Widget\n src={`bos.forum.potlock.near/widget/components.molecule.LinkedProposals`}\n props={{\n linkedProposalIds: linkedProposalIds,\n showStatus: true,\n }}\n />\n </div>\n <div className=\"text-muted d-flex flex-column gap-2\">\n <div className=\"text-lg\">\n What would you like to do with the linked proposals?\n </div>\n {OptionForm}\n </div>\n <div className=\"text-sm mt-2\">\n Note: To take specific actions on individual proposals, please\n manage them from their respective pages.\n </div>\n </ModalContent>\n <div className=\"d-flex gap-2 align-items-center justify-content-end mt-2\">\n <Widget\n src={`devhub.near/widget/devhub.components.molecule.Button`}\n props={{\n classNames: { root: \"btn-outline-secondary\" },\n label: \"Cancel\",\n onClick: onCancelClick,\n }}\n />\n <Widget\n src={`devhub.near/widget/devhub.components.molecule.Button`}\n props={{\n classNames: { root: \"btn-danger\" },\n disabled: !proposalStatus,\n label: \"Ready to Cancel\",\n onClick: () => onConfirmClick(proposalStatus),\n }}\n />\n </div>\n </ModalDialog>\n </Modal>\n </>\n);\n" }, "components.molecule.LinkedRfpDropdown": { "": "const { RFP_TIMELINE_STATUS, fetchGraphQL, parseJSON } = VM.require(\n `bos.forum.potlock.near/widget/core.common`\n) || { RFP_TIMELINE_STATUS: {}, fetchGraphQL: () => {}, parseJSON: () => {} };\nconst { href } = VM.require(`devhub.near/widget/core.lib.url`);\nhref || (href = () => {});\nconst { linkedRfp, onChange, disabled, onDeleteRfp } = props;\nconst isModerator = Near.view(\n \"forum.potlock.near\",\n \"is_allowed_to_write_rfps\",\n {\n editor: context.accountId,\n }\n);\nconst [selectedRFP, setSelectedRFP] = useState(null);\nconst [acceptingRfpsOptions, setAcceptingRfpsOption] = useState([]);\nconst [allRfpOptions, setAllRfpOptions] = useState([]);\nconst [searchRFPId, setSearchRfpId] = useState(\"\");\nconst [initialStateApplied, setInitialState] = useState(false);\nconst queryName =\n \"bos_forum_potlock_near_ai_pgf_indexer_rfps_with_latest_snapshot\";\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: {rfp_id: desc}\n where: $where\n ) {\n name\n rfp_id\n timeline\n }\n }`;\nfunction separateNumberAndText(str) {\n const numberRegex = /\\d+/;\n if (numberRegex.test(str)) {\n const number = str.match(numberRegex)[0];\n const text = str.replace(numberRegex, \"\").trim();\n return { number: parseInt(number), text };\n } else {\n return { number: null, text: str.trim() };\n }\n}\nconst buildWhereClause = () => {\n // show only accepting submissions stage rfps\n let where = {};\n const { number, text } = separateNumberAndText(searchRFPId);\n if (number) {\n where = { rfp_id: { _eq: number }, ...where };\n }\n if (text) {\n where = {\n _or: [\n { name: { _iregex: `${text}` } },\n { summary: { _iregex: `${text}` } },\n { description: { _iregex: `${text}` } },\n ],\n ...where,\n };\n }\n return where;\n};\nconst fetchRfps = () => {\n const FETCH_LIMIT = 30;\n const variables = {\n limit: FETCH_LIMIT,\n offset: 0,\n where: buildWhereClause(),\n };\n fetchGraphQL(query, \"GetLatestSnapshot\", variables).then(async (result) => {\n if (result.status === 200) {\n if (result.body.data) {\n const rfpsData = result.body.data?.[queryName];\n const data = [];\n const acceptingData = [];\n for (const prop of rfpsData) {\n const timeline = parseJSON(prop.timeline);\n const label = \"# \" + prop.rfp_id + \" : \" + prop.name;\n const value = prop.rfp_id;\n if (timeline.status === RFP_TIMELINE_STATUS.ACCEPTING_SUBMISSIONS) {\n acceptingData.push({\n label,\n value,\n });\n }\n data.push({\n label,\n value,\n });\n }\n setAcceptingRfpsOption(acceptingData);\n setAllRfpOptions(data);\n }\n }\n });\n};\nuseEffect(() => {\n fetchRfps();\n}, [searchRFPId]);\nuseEffect(() => {\n if (JSON.stringify(linkedRfp) !== JSON.stringify(selectedRFP)) {\n if (allRfpOptions.length > 0) {\n if (typeof linkedRfp !== \"object\") {\n setSelectedRFP(allRfpOptions.find((i) => linkedRfp === i.value));\n } else {\n setSelectedRFP(linkedRfp);\n }\n setInitialState(true);\n }\n } else {\n setInitialState(true);\n }\n}, [linkedRfp, allRfpOptions]);\nuseEffect(() => {\n if (\n JSON.stringify(linkedRfp) !== JSON.stringify(selectedRFP) &&\n initialStateApplied\n ) {\n onChange(selectedRFP);\n }\n}, [selectedRFP, initialStateApplied]);\nreturn (\n <>\n {selectedRFP && (\n <div className=\"d-flex gap-2 align-items-center\">\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: selectedRFP.value,\n },\n })}\n target=\"_blank\"\n rel=\"noopener noreferrer\"\n >\n {selectedRFP.label}\n </a>\n {!disabled && (\n <div\n className=\"cursor-pointer\"\n onClick={() => {\n onDeleteRfp();\n setSelectedRFP(null);\n }}\n >\n <i className=\"bi bi-trash3-fill\"></i>\n </div>\n )}\n </div>\n )}\n <Widget\n src={`bos.forum.potlock.near/widget/components.molecule.DropDownWithSearch`}\n props={{\n disabled: disabled,\n selectedValue: selectedRFP.value,\n onChange: (v) => {\n setSelectedRFP(v);\n },\n options: isModerator ? allRfpOptions : acceptingRfpsOptions,\n showSearch: true,\n searchInputPlaceholder: \"Search by Id\",\n defaultLabel: \"Search RFP\",\n searchByValue: true,\n onSearch: (value) => {\n setSearchRfpId(value);\n },\n }}\n />\n </>\n);\n" }, "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://near.social/near/widget/ProfilePage?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.molecule.LinkedProposals": { "": "const { 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 linkedProposalIds = props.linkedProposalIds ?? [];\nconst linkedProposalsData = [];\nconst showStatus = props.showStatus ?? false;\n// using contract instead of indexer, since indexer doesn't return timestamp\nlinkedProposalIds.map((item) => {\n const data = Near.view(\"forum.potlock.near\", \"get_proposal\", {\n proposal_id: item,\n });\n if (data !== null) {\n linkedProposalsData.push(data);\n }\n});\nconst Container = styled.div`\n a {\n &:hover {\n text-decoration: none !important;\n }\n }\n`;\nreturn (\n <Container className=\"d-flex flex-column gap-3\">\n {linkedProposalsData.map((item) => {\n return (\n <a\n href={href({\n widgetSrc: `bos.forum.potlock.near/widget/app`,\n params: {\n page: \"proposal\",\n id: item.id,\n },\n })}\n target=\"_blank\"\n rel=\"noopener noreferrer\"\n >\n <div className=\"d-flex gap-2\">\n <Widget\n src={`bos.forum.potlock.near/widget/components.molecule.Profile`}\n props={{\n accountId: item.snapshot.editor_id,\n }}\n />\n <div className=\"d-flex flex-column\" style={{ maxWidth: 250 }}>\n <b className=\"text-truncate\">{item.snapshot.name}</b>\n <div className=\"text-sm text-muted\">\n created on {readableDate(item.snapshot.timestamp / 1000000)}\n </div>\n {showStatus && (\n <div style={{ width: \"fit-content\" }} className=\"mt-1\">\n <Widget\n src={`devhub.near/widget/devhub.entity.proposal.StatusTag`}\n props={{\n timelineStatus: item.snapshot.timeline.status,\n }}\n />\n </div>\n )}\n </div>\n </div>\n </a>\n );\n })}\n </Container>\n);\n" }, "components.pages.announcement": { "": "const profile = Social.getr(`forum.potlock.near/profile`, \"final\", {\n subscribe: true,\n});\nif (!profile || !profile.announcement) {\n return <></>;\n}\nconst Container = styled.div`\n width: -webkit-fill-available;\n background-image: linear-gradient(to bottom, #4b7a93, #213236);\n color: white;\n a {\n color: white !important;\n }\n`;\nreturn (\n <Container className=\"p-2 px-3 rounded-3\">\n <Widget\n src={`bos.forum.potlock.near/widget/components.molecule.Markdown`}\n props={{\n content: profile.announcement,\n }}\n />\n </Container>\n);\n" }, "components.rfps.StatusTag": { "": "const { RFP_TIMELINE_STATUS } = VM.require(\n `bos.forum.potlock.near/widget/core.common`\n) || { RFP_TIMELINE_STATUS: {} };\nconst timelineStatus = props.timelineStatus;\nconst size = props.size ?? \"md\";\nconst getClassNameByStatus = () => {\n switch (timelineStatus) {\n case RFP_TIMELINE_STATUS.CANCELLED:\n return \"grey\";\n case RFP_TIMELINE_STATUS.PROPOSAL_SELECTED:\n return \"green\";\n case RFP_TIMELINE_STATUS.EVALUATION:\n return \"orange\";\n default:\n return \"black\";\n }\n};\nconst Container = styled.div`\n font-size: ${({ size }) => {\n switch (size) {\n case \"sm\":\n return \"10px\";\n case \"lg\":\n return \"14px\";\n default:\n return \"12px\";\n }\n }};\n min-width: fit-content;\n .orange-tag {\n border: 1px solid #ff7a00 !important;\n color: #ff7a00 !important;\n }\n .black-tag {\n border: 1px solid #000 !important;\n color: #000 !important;\n }\n .grey-tag {\n border: 1px solid #979797 !important;\n color: #979797 !important;\n }\n .green-tag {\n border: 1px solid #04a46e !important;\n color: #04a46e !important;\n }\n .fw-bold {\n font-weight: 600 !important;\n }\n`;\nreturn (\n <Container size={size}>\n <div className={getClassNameByStatus() + \"-tag fw-bold rounded-2 p-1 px-2\"}>\n {(timelineStatus ?? \"\").replace(\"_\", \" \")}\n </div>\n </Container>\n);\n" }, "components.core.lib.contract": { "": "function ensureOtherIsLast(arr) {\n const otherIndex = (arr ?? []).findIndex((item) => item.value === \"Other\");\n if (otherIndex !== -1) {\n const [otherItem] = arr.splice(otherIndex, 1);\n arr.push(otherItem);\n }\n return arr;\n}\nfunction getGlobalLabels() {\n let labels = Near.view(\"forum.potlock.near\", \"get_global_labels\");\n if (labels !== null) {\n labels = ensureOtherIsLast(labels);\n }\n return labels ?? null;\n}\nreturn {\n getGlobalLabels,\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 src={`devhub.near/widget/devhub.entity.proposal.LoginScreen`} />\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\">\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.proposals.Feed": { "": "const { fetchGraphQL, parseJSON, isNumber } = VM.require(\n `bos.forum.potlock.near/widget/core.common`\n) || { fetchGraphQL: () => {}, 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 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 .card.no-border {\n border-left: none !important;\n border-right: none !important;\n margin-bottom: -3.5rem;\n }\n @media screen and (max-width: 768px) {\n font-size: 13px;\n }\n .text-sm {\n font-size: 13px;\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 border-left: none !important;\n border-right: none !important;\n border-bottom: none !important;\n &:hover {\n background-color: #f4f4f4;\n }\n }\n .blue-btn {\n background-color: #3c697d !important;\n border: none;\n color: white;\n &:active {\n color: white;\n }\n }\n @media screen and (max-width: 768px) {\n .blue-btn {\n padding: 0.5rem 0.8rem !important;\n min-height: 32px;\n }\n }\n a.no-space {\n display: inline-block;\n }\n .text-wrap {\n overflow: hidden;\n white-space: normal;\n }\n .bg-blue {\n background-image: linear-gradient(to bottom, #4b7a93, #213236);\n color: white;\n }\n`;\nconst Heading = styled.div`\n font-size: 24px;\n font-weight: 700;\n width: 100%;\n .text-normal {\n font-weight: normal !important;\n }\n @media screen and (max-width: 768px) {\n font-size: 18px;\n }\n`;\nconst rfpLabelOptions = getGlobalLabels();\nconst FeedItem = ({ proposal, index }) => {\n const accountId = proposal.author_id;\n proposal.timeline = parseJSON(proposal.timeline);\n const profile = Social.get(`${accountId}/profile/**`, \"final\");\n // We will have to get the proposal from the contract to get the block height.\n const blockHeight = parseInt(proposal.social_db_post_block_height);\n const item = {\n type: \"social\",\n path: `forum.potlock.near/post/main`,\n blockHeight: blockHeight,\n };\n const isLinked = isNumber(proposal.linked_rfp);\n const rfpData = proposal.rfpData;\n return (\n <a\n href={href({\n widgetSrc: `bos.forum.potlock.near/widget/app`,\n params: {\n page: \"proposal\",\n id: proposal.proposal_id,\n },\n })}\n onClick={(e) => e.stopPropagation()}\n style={{ textDecoration: \"none\" }}\n >\n <div\n className={\n \"proposal-card d-flex justify-content-between gap-2 text-muted cursor-pointer p-3 w-100 flex-wrap flex-sm-nowrap \" +\n (index !== 0 && \" border\")\n }\n >\n <div className=\"d-flex gap-4 w-100\">\n <Widget\n src={`bos.forum.potlock.near/widget/components.molecule.Profile`}\n props={{\n accountId,\n }}\n />\n <div className=\"d-flex flex-column gap-2 w-100 text-wrap\">\n <div className=\"d-flex gap-2 align-items-center flex-wrap w-100\">\n <div className=\"h6 mb-0 text-black\">{proposal.name}</div>\n <Widget\n src={`bos.forum.potlock.near/widget/components.molecule.MultiSelectCategoryDropdown`}\n props={{\n selected: proposal.labels,\n disabled: true,\n hideDropdown: true,\n onChange: () => {},\n availableOptions: rfpLabelOptions,\n }}\n />\n </div>\n {isLinked && rfpData && (\n <div className=\"text-sm text-muted d-flex gap-1 align-items-center\">\n <i class=\"bi bi-link-45deg\"></i>\n In response to RFP :\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: rfpData.rfp_id,\n },\n })}\n target=\"_blank\"\n rel=\"noopener noreferrer\"\n >\n {rfpData.name}\n </a>\n </div>\n )}\n <div className=\"d-flex gap-2 align-items-center flex-wrap flex-sm-nowrap text-sm w-100\">\n <div>#{proposal.proposal_id} ・ </div>\n <div className=\"text-truncate\">\n By {profile.name ?? accountId} ・{\" \"}\n </div>\n <Widget\n src={`near/widget/TimeAgo`}\n props={{\n blockHeight,\n blockTimestamp: proposal.timestamp,\n }}\n />\n </div>\n <div className=\"d-flex gap-2 align-items-center\">\n <Widget\n src={`devhub.near/widget/devhub.entity.proposal.LikeButton`}\n props={{\n item,\n proposalId: proposal.id,\n notifyAccountId: accountId,\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 </div>\n </div>\n </div>\n <div className=\"align-self-center\" style={{ minWidth: \"fit-content\" }}>\n <Widget\n src={`devhub.near/widget/devhub.entity.proposal.StatusTag`}\n props={{\n timelineStatus: proposal.timeline.status,\n }}\n />\n </div>\n </div>\n </a>\n );\n};\nconst getProposal = (proposal_id) => {\n return Near.asyncView(`forum.potlock.near`, \"get_proposal\", {\n proposal_id,\n });\n};\nconst FeedPage = () => {\n State.init({\n data: [],\n cachedItems: {},\n stage: \"\",\n sort: \"\",\n label: \"\",\n input: \"\",\n loading: false,\n loadingMore: false,\n aggregatedCount: null,\n currentlyDisplaying: 0,\n isFiltered: false,\n });\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 author_id\n block_height\n name\n labels\n summary\n editor_id\n proposal_id\n ts\n timeline\n views\n linked_rfp\n }\n ${queryName}_aggregate(\n order_by: {proposal_id: desc}\n where: $where\n ) {\n aggregate {\n count\n }\n }\n }`;\n const rfpQueryName =\n \"bos_forum_potlock_near_ai_pgf_indexer_rfps_with_latest_snapshot\";\n const rfpQuery = `query GetLatestSnapshot($offset: Int = 0, $limit: Int = 10, $where: ${rfpQueryName}_bool_exp = {}) {\n ${rfpQueryName}(\n offset: $offset\n limit: $limit\n order_by: {rfp_id: desc}\n where: $where\n ) {\n name\n rfp_id\n }\n }`;\n function separateNumberAndText(str) {\n const numberRegex = /\\d+/;\n if (numberRegex.test(str)) {\n const number = str.match(numberRegex)[0];\n const text = str.replace(numberRegex, \"\").trim();\n return { number: parseInt(number), text };\n } else {\n return { number: null, text: str.trim() };\n }\n }\n const buildWhereClause = () => {\n let where = {};\n if (state.label) {\n where = { labels: { _contains: state.label }, ...where };\n }\n if (state.stage) {\n // timeline is stored as jsonb\n where = {\n timeline: { _cast: { String: { _regex: `${state.stage}` } } },\n ...where,\n };\n }\n if (state.input) {\n const { number, text } = separateNumberAndText(state.input);\n if (number) {\n where = { proposal_id: { _eq: number }, ...where };\n }\n if (text) {\n where = {\n _or: [\n { name: { _iregex: `${text}` } },\n { summary: { _iregex: `${text}` } },\n { description: { _iregex: `${text}` } },\n ],\n ...where,\n };\n }\n }\n State.update({ isFiltered: Object.keys(where).length > 0 });\n return where;\n };\n const buildOrderByClause = () => {\n /**\n * TODO\n * Most commented -> edit contract and indexer\n * Unanswered -> 0 comments\n */\n };\n const makeMoreItems = () => {\n if (state.aggregatedCount <= state.currentlyDisplaying) return;\n fetchProposals(state.data.length);\n };\n const fetchProposals = (offset) => {\n if (!offset) {\n offset = 0;\n }\n if (state.loading) return;\n const FETCH_LIMIT = 10;\n const variables = {\n limit: FETCH_LIMIT,\n offset,\n where: buildWhereClause(),\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 totalResult = result.body.data?.[`${queryName}_aggregate`];\n const promises = data.map((item) => {\n if (isNumber(item.linked_rfp)) {\n return fetchGraphQL(rfpQuery, \"GetLatestSnapshot\", {}).then(\n (result) => {\n const rfpData = result.body.data?.[rfpQueryName];\n return { ...item, rfpData: rfpData[0] };\n }\n );\n } else {\n return Promise.resolve(item);\n }\n });\n Promise.all(promises).then((res) => {\n State.update({ aggregatedCount: totalResult.aggregate.count });\n fetchBlockHeights(res, offset);\n });\n }\n }\n });\n };\n const renderItem = (item, index) => (\n <div\n key={item.proposal_id}\n className={\n (index !== state.data.length - 1 && \"border-bottom \") + index === 0 &&\n \" rounded-top-2\"\n }\n >\n <FeedItem proposal={item} index={index} />\n </div>\n );\n const cachedRenderItem = (item, index) => {\n if (props.term) {\n return renderItem(item, {\n searchKeywords: [props.term],\n });\n }\n const key = JSON.stringify(item);\n if (!(key in state.cachedItems)) {\n state.cachedItems[key] = renderItem(item, index);\n State.update();\n }\n return state.cachedItems[key];\n };\n useEffect(() => {\n fetchProposals();\n }, [state.sort, state.label, state.stage]);\n const mergeItems = (newItems) => {\n const items = [\n ...new Set([...newItems, ...state.data].map((i) => JSON.stringify(i))),\n ].map((i) => JSON.parse(i));\n // Sorting in the front end\n if (state.sort === \"proposal_id\" || state.sort === \"\") {\n items.sort((a, b) => b.proposal_id - a.proposal_id);\n } else if (state.sort === \"views\") {\n items.sort((a, b) => b.views - a.views);\n }\n return items;\n };\n const fetchBlockHeights = (data, offset) => {\n let promises = data.map((item) => getProposal(item.proposal_id));\n Promise.all(promises).then((blockHeights) => {\n data = data.map((item, index) => ({\n ...item,\n timeline: JSON.parse(item.timeline),\n social_db_post_block_height:\n blockHeights[index].social_db_post_block_height,\n }));\n if (offset) {\n let newData = mergeItems(data);\n State.update({\n data: newData,\n currentlyDisplaying: newData.length,\n loading: false,\n });\n } else {\n State.update({\n data,\n currentlyDisplaying: data.length,\n loading: false,\n });\n }\n });\n };\n const loader = (\n <div className=\"d-flex justify-content-center align-items-center w-100\">\n <Widget src={`devhub.near/widget/devhub.components.molecule.Spinner`} />\n </div>\n );\n const renderedItems = state.data ? state.data.map(cachedRenderItem) : null;\n return (\n <Container className=\"w-100 py-4 px-2 d-flex flex-column gap-3\">\n <div className=\"d-flex justify-content-between flex-wrap gap-2 align-items-center\">\n <Heading>\n Proposals\n <span className=\"text-muted text-normal\">\n ({state.aggregatedCount ?? state.data.length}){\" \"}\n </span>\n </Heading>\n <div className=\"d-flex flex-wrap gap-4 align-items-center\">\n <Widget\n src={`devhub.near/widget/devhub.feature.proposal-search.by-input`}\n props={{\n search: state.input,\n className: \"w-xs-100\",\n onSearch: (input) => {\n State.update({ input });\n fetchProposals();\n },\n onEnter: () => {\n fetchProposals();\n },\n }}\n />\n <Widget\n src={`devhub.near/widget/devhub.feature.proposal-search.by-sort`}\n props={{\n onStateChange: (select) => {\n State.update({ sort: select.value });\n },\n }}\n />\n <div className=\"d-flex gap-4 align-items-center\">\n <Widget\n src={`bos.forum.potlock.near/widget/components.molecule.FilterByLabel`}\n props={{\n onStateChange: (select) => {\n State.update({ label: select.value });\n },\n availableOptions: rfpLabelOptions,\n }}\n />\n <Widget\n src={`devhub.near/widget/devhub.feature.proposal-search.by-stage`}\n props={{\n onStateChange: (select) => {\n State.update({ stage: select.value });\n },\n }}\n />\n </div>\n </div>\n <div className=\"mt-2 mt-xs-0\">\n <Link\n to={href({\n widgetSrc: `bos.forum.potlock.near/widget/app`,\n params: { page: \"create-proposal\" },\n })}\n >\n <Widget\n src={`devhub.near/widget/devhub.components.molecule.Button`}\n props={{\n label: (\n <div className=\"d-flex gap-2 align-items-center\">\n <div>\n <i className=\"bi bi-plus-circle-fill\"></i>\n </div>\n Submit Proposal\n </div>\n ),\n classNames: { root: \"blue-btn\" },\n }}\n />\n </Link>\n </div>\n </div>\n <div style={{ minHeight: \"50vh\" }}>\n {!Array.isArray(state.data) ? (\n loader\n ) : (\n <div className=\"card no-border rounded-0 mt-4 py-3 full-width-div\">\n <div className=\"container-xl\">\n <div>\n <Widget\n src={`bos.forum.potlock.near/widget/components.pages.announcement`}\n loading=\"\"\n />\n </div>\n <div className=\"mt-4 border rounded-2\">\n {state.aggregatedCount === 0 ? (\n <div className=\"m-2\">\n {state.isFiltered ? (\n <div class=\"alert alert-danger\" role=\"alert\">\n No proposal found for selected filter.\n </div>\n ) : (\n <div class=\"alert alert-secondary\" role=\"alert\">\n No proposal has been created yet.\n </div>\n )}\n </div>\n ) : state.aggregatedCount > 0 ? (\n <InfiniteScroll\n pageStart={0}\n loadMore={makeMoreItems}\n hasMore={state.aggregatedCount > state.data.length}\n loader={loader}\n useWindow={false}\n threshold={100}\n >\n {renderedItems}\n </InfiniteScroll>\n ) : (\n loader\n )}\n </div>\n </div>\n </div>\n )}\n </div>\n </Container>\n );\n};\nreturn FeedPage(props);\n" }, "components.rfps.TimelineConfigurator": { "": "const { RFP_TIMELINE_STATUS } = VM.require(\n `bos.forum.potlock.near/widget/core.common`\n) || { RFP_TIMELINE_STATUS: {} };\nconst stepsArray = [1, 2, 3];\nconst timeline = props.timeline;\nconst disabled = props.disabled;\nconst setTimeline = props.setTimeline ?? (() => {});\nconst TimelineStatusOptions = [\n {\n label: \"Accepting Submissions\",\n value: { status: RFP_TIMELINE_STATUS.ACCEPTING_SUBMISSIONS },\n },\n {\n label: \"Evaluation\",\n value: {\n status: RFP_TIMELINE_STATUS.EVALUATION,\n },\n },\n {\n label: \"Proposal Selected\",\n value: {\n status: RFP_TIMELINE_STATUS.PROPOSAL_SELECTED,\n },\n },\n {\n label: \"Cancelled\",\n value: {\n status: RFP_TIMELINE_STATUS.CANCELLED,\n },\n },\n];\nconst Container = styled.div`\n .circle-lg {\n width: 15px;\n height: 15px;\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: 85px;\n background-color: lightgrey;\n }\n @media screen and (max-width: 970px) {\n .vertical-line {\n height: 70px !important;\n }\n }\n @media screen and (max-width: 570px) {\n .vertical-line {\n height: 65px !important;\n }\n }\n`;\nconst selectedTimelineStatusIndex = useMemo(\n () =>\n TimelineStatusOptions.findIndex((i) => i.value.status === timeline.status),\n [timeline]\n);\nconst TimelineItems = ({ title, children, value, values }) => {\n const indexOfCurrentItem = TimelineStatusOptions.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 = selectedTimelineStatusIndex;\n // index 2,3 is of decision\n if (selectedTimelineStatusIndex === 3) {\n statusIndex = 2;\n }\n if (statusIndex === indexOfCurrentItem) {\n color = \"#FEF6EE\";\n }\n if (statusIndex > indexOfCurrentItem) {\n color = \"#EEFEF0\";\n }\n // cancelled\n if (\n statusIndex === 2 &&\n (values ?? []).includes(RFP_TIMELINE_STATUS.CANCELLED) &&\n timeline.status === RFP_TIMELINE_STATUS.CANCELLED\n ) {\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};\nreturn (\n <Container className=\"d-flex flex-column gap-2\">\n {!disabled && (\n <div className=\"d-flex flex-column gap-2\">\n <h6 className=\"mb-0\">Status</h6>\n <Widget\n src={`bos.forum.potlock.near/widget/components.molecule.DropDown`}\n props={{\n options: TimelineStatusOptions,\n selectedValue: TimelineStatusOptions.find(\n (i) => i.value.status === timeline.status\n ),\n onUpdate: (v) => {\n setTimeline({ status: v.value.status });\n },\n }}\n />\n </div>\n )}\n <div className=\"d-flex gap-3 mt-2\">\n <div className=\"d-flex flex-column mt-4\">\n {stepsArray.map((_, index) => {\n const indexOfCurrentItem = index;\n let color = \"\";\n let statusIndex = selectedTimelineStatusIndex;\n // index 2,3 is of decision\n if (selectedTimelineStatusIndex === 3) {\n statusIndex = 2;\n }\n const current = statusIndex === indexOfCurrentItem;\n const completed =\n statusIndex > indexOfCurrentItem ||\n timeline.status === RFP_TIMELINE_STATUS.PROPOSAL_SELECTED ||\n timeline.status === RFP_TIMELINE_STATUS.CANCELLED;\n return (\n <div className=\"d-flex flex-column align-items-center gap-1\">\n <div\n className={\n \"circle-lg \" +\n (completed && \" green-fill \") +\n (current && !completed && \" yellow-fill \")\n }\n >\n {completed && (\n <div\n className=\"d-flex justify-content-center align-items-center\"\n style={{ height: \"120%\" }}\n >\n <i className=\"bi bi-check\"></i>\n </div>\n )}\n </div>\n {index !== stepsArray.length - 1 && (\n <div\n className={\n \"vertical-line \" +\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) Accepting Submissions\"\n value={RFP_TIMELINE_STATUS.ACCEPTING_SUBMISSIONS}\n >\n <div>During this stage, the RFP is still open for submissions.</div>\n </TimelineItems>\n <TimelineItems\n title=\"2) Evaluation\"\n value={RFP_TIMELINE_STATUS.EVALUATION}\n >\n <div>\n This RFP is closed for submissions. All submitted proposals are\n under review.\n </div>\n </TimelineItems>\n <TimelineItems\n title=\"3) Decision\"\n values={[\n RFP_TIMELINE_STATUS.PROPOSAL_SELECTED,\n RFP_TIMELINE_STATUS.CANCELLED,\n ]}\n >\n <div className=\"d-flex flex-column gap-2\">\n <div>Sponsor makes a final decision:</div>\n <Widget\n src={`bos.forum.potlock.near/widget/components.molecule.RadioButton`}\n props={{\n value: \"\",\n label: <div className=\"fw-bold\">Proposal Selected</div>,\n disabled: disabled,\n isChecked:\n timeline.status === RFP_TIMELINE_STATUS.PROPOSAL_SELECTED,\n onClick: (v) => {\n if (v) {\n setTimeline({\n status: RFP_TIMELINE_STATUS.PROPOSAL_SELECTED,\n });\n }\n },\n }}\n />\n <Widget\n src={`bos.forum.potlock.near/widget/components.molecule.RadioButton`}\n props={{\n value: \"\",\n disabled: disabled,\n label: <div className=\"fw-bold\">RFP Cancelled</div>,\n isChecked: timeline.status === RFP_TIMELINE_STATUS.CANCELLED,\n onClick: (v) => {\n if (v) {\n setTimeline({\n status: RFP_TIMELINE_STATUS.CANCELLED,\n });\n }\n },\n }}\n />\n </div>\n </TimelineItems>\n </div>\n </div>\n </Container>\n);\n" }, "components.rfps.ViewRfpModal": { "": "const { href: linkHref } = VM.require(`devhub.near/widget/core.lib.url`);\nlinkHref || (linkHref = () => {});\nconst isOpen = props.isOpen;\nconst isEdit = props.isEdit;\nconst rfpId = props.rfpId;\nconst Modal = styled.div`\n display: ${({ hidden }) => (hidden ? \"none\" : \"flex\")};\n position: fixed;\n inset: 0;\n justify-content: center;\n align-items: center;\n opacity: 1;\n z-index: 999;\n .black-btn {\n background-color: #000 !important;\n border: none;\n color: white;\n &:active {\n color: white;\n }\n }\n @media screen and (max-width: 768px) {\n h5 {\n font-size: 16px !important;\n }\n }\n .btn {\n font-size: 14px;\n }\n .btn-blue {\n background-image: linear-gradient(to bottom, #4b7a93, #213236);\n color: white;\n }\n`;\nconst ModalBackdrop = styled.div`\n position: absolute;\n inset: 0;\n background-color: rgba(0, 0, 0, 0.9);\n opacity: 0.9;\n`;\nconst ModalDialog = styled.div`\n padding: 2em;\n z-index: 999;\n overflow-y: auto;\n max-height: 85%;\n margin-top: 5%;\n width: 30%;\n @media screen and (max-width: 768px) {\n margin: 2rem;\n width: 100%;\n }\n`;\nconst ModalHeader = styled.div`\n display: flex;\n flex-direction: row;\n justify-content: space-between;\n align-items: center;\n padding-bottom: 4px;\n`;\nconst ModalFooter = styled.div`\n padding-top: 4px;\n display: flex;\n flex-direction: row;\n justify-content: space-between;\n align-items: items-center;\n`;\nconst CloseButton = styled.button`\n display: flex;\n align-items: center;\n justify-content: center;\n background-color: white;\n padding: 0.5em;\n border-radius: 6px;\n border: 0;\n color: #344054;\n &:hover {\n background-color: #d3d3d3;\n }\n`;\nconst ConfirmButton = styled.button`\n padding: 0.7em;\n border-radius: 6px;\n border: 0;\n box-shadow: 0px 1px 2px rgba(16, 24, 40, 0.05);\n background-color: #12b76a;\n color: white;\n &:hover {\n background-color: #0e9f5d;\n }\n`;\nconst ModalContent = styled.div`\n flex: 1;\n font-size: 14px;\n margin-top: 4px;\n margin-bottom: 4px;\n overflow-y: auto;\n max-height: 50%;\n @media screen and (max-width: 768px) {\n font-size: 12px !important;\n }\n`;\nconst NoButton = styled.button`\n background: transparent;\n border: none;\n padding: 0;\n margin: 0;\n box-shadow: none;\n`;\nreturn (\n <>\n <Modal hidden={!isOpen}>\n <ModalBackdrop />\n <ModalDialog className=\"card\">\n <ModalHeader>\n <h5 className=\"mb-0\">\n Your RFP has been successfully {!isEdit ? \"published\" : \"edited\"}.\n </h5>\n </ModalHeader>\n <div className=\"d-flex gap-2 justify-content-center mt-2\">\n <Link\n to={linkHref({\n widgetSrc: `bos.forum.potlock.near/widget/app`,\n params: { page: \"rfp\", id: rfpId },\n })}\n >\n <Widget\n src={`devhub.near/widget/devhub.components.molecule.Button`}\n props={{\n classNames: { root: \"btn-blue\" },\n label: \"View RFP\",\n onClick: () => {},\n }}\n />\n </Link>\n </div>\n </ModalDialog>\n </Modal>\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://near.social/near/widget/ProfilePage?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 className=\"d-none d-sm-flex\">\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.molecule.DropDownWithSearch": { "": "const {\n selectedValue,\n onChange,\n options,\n defaultLabel,\n showSearch,\n searchInputPlaceholder,\n searchByLabel,\n searchByValue,\n onSearch,\n disabled,\n} = props;\nconst [searchTerm, setSearchTerm] = useState(\"\");\nconst [filteredOptions, setFilteredOptions] = useState(options);\nconst [isOpen, setIsOpen] = useState(false);\nconst [selectedOption, setSelectedOption] = useState({\n label:\n options?.find((item) => item.value === selectedValue)?.label ??\n defaultLabel,\n value: defaultLabel,\n});\nuseEffect(() => {\n if (selectedOption.value !== selectedValue) {\n setSelectedOption({\n label:\n options?.find((item) => item.value === selectedValue)?.label ??\n defaultLabel,\n value: defaultLabel,\n });\n }\n}, [selectedValue]);\nuseEffect(() => {\n setFilteredOptions(options);\n}, [options]);\nconst handleSearch = (event) => {\n const term = event.target.value.toLowerCase();\n setSearchTerm(term);\n if (typeof onSearch === \"function\") {\n onSearch(term);\n return;\n }\n const filteredOptions = options.filter((option) => {\n if (searchByLabel) {\n return option.label.toLowerCase().includes(term);\n }\n if (searchByValue) {\n return option.value.toString().toLowerCase().includes(term);\n }\n });\n setFilteredOptions(filteredOptions);\n};\nconst toggleDropdown = () => {\n setIsOpen(!isOpen);\n};\nconst handleOptionClick = (option) => {\n setSelectedOption(option);\n setIsOpen(false);\n onChange(option);\n};\nconst Container = styled.div`\n .drop-btn {\n width: 100%;\n text-align: left;\n padding-inline: 10px;\n }\n .dropdown-toggle:after {\n position: absolute;\n top: 46%;\n right: 5%;\n }\n .dropdown-menu {\n width: 100%;\n }\n .dropdown-item.active,\n .dropdown-item:active {\n background-color: #f0f0f0 !important;\n color: black;\n }\n .custom-select {\n position: relative;\n }\n .scroll-box {\n max-height: 200px;\n overflow-y: scroll;\n }\n .selected {\n background-color: #f0f0f0;\n }\n input {\n background-color: #f8f9fa;\n }\n .cursor-pointer {\n cursor: pointer;\n }\n .text-wrap {\n overflow: hidden;\n white-space: normal;\n }\n .disabled {\n background-color: #f8f8f8 !important;\n cursor: not-allowed !important;\n border-radius: 5px;\n opacity: inherit !important;\n }\n .disabled.dropdown-toggle::after {\n display: none !important;\n }\n`;\nlet searchFocused = false;\nreturn (\n <Container>\n <div\n className=\"custom-select w-100\"\n tabIndex=\"0\"\n onBlur={() => {\n setTimeout(() => {\n setIsOpen(searchFocused || false);\n }, 0);\n }}\n >\n <div\n className={\n \"dropdown-toggle bg-white border rounded-2 btn drop-btn w-100 \" +\n (disabled ? \"disabled\" : \"\")\n }\n >\n <div\n className={`selected-option w-100 text-wrap ${\n selectedOption.label === defaultLabel ? \"text-muted\" : \"\"\n }`}\n onClick={!disabled && toggleDropdown}\n >\n {selectedOption.label}\n </div>\n </div>\n {isOpen && (\n <div className=\"dropdown-menu dropdown-menu-end dropdown-menu-lg-start px-2 shadow show\">\n {showSearch && (\n <input\n type=\"text\"\n className=\"form-control mb-2\"\n placeholder={searchInputPlaceholder ?? \"Search options\"}\n value={searchTerm}\n onChange={handleSearch}\n onFocus={() => {\n searchFocused = true;\n }}\n onBlur={() => {\n setTimeout(() => {\n searchFocused = false;\n }, 0);\n }}\n />\n )}\n <div className=\"scroll-box\">\n {filteredOptions.map((option) => (\n <div\n key={option.value}\n className={`dropdown-item cursor-pointer w-100 text-wrap ${\n selectedOption.value === option.value ? \"selected\" : \"\"\n }`}\n onClick={() => handleOptionClick(option)}\n >\n {option.label}\n </div>\n ))}\n </div>\n </div>\n )}\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://near.social/near/widget/ProfilePage?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 className=\"d-none d-sm-flex\">\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" }, "components.rfps.WarningModal": { "": "const isOpen = props.isOpen;\nconst onConfirmClick = props.onConfirmClick;\nconst Modal = styled.div`\n display: ${({ hidden }) => (hidden ? \"none\" : \"flex\")};\n position: fixed;\n inset: 0;\n justify-content: center;\n align-items: center;\n opacity: 1;\n z-index: 999;\n .black-btn {\n background-color: #000 !important;\n border: none;\n color: white;\n &:active {\n color: white;\n }\n }\n @media screen and (max-width: 768px) {\n h5 {\n font-size: 16px !important;\n }\n }\n .btn {\n font-size: 14px;\n }\n`;\nconst ModalBackdrop = styled.div`\n position: absolute;\n inset: 0;\n background-color: rgba(0, 0, 0, 0.5);\n opacity: 0.4;\n`;\nconst ModalDialog = styled.div`\n padding: 2em;\n z-index: 999;\n overflow-y: auto;\n max-height: 85%;\n margin-top: 5%;\n width: 50%;\n @media screen and (max-width: 768px) {\n margin: 2rem;\n width: 100%;\n }\n`;\nconst ModalHeader = styled.div`\n display: flex;\n flex-direction: row;\n justify-content: space-between;\n align-items: center;\n padding-bottom: 4px;\n`;\nconst ModalFooter = styled.div`\n padding-top: 4px;\n display: flex;\n flex-direction: row;\n justify-content: space-between;\n align-items: items-center;\n`;\nconst CloseButton = styled.button`\n display: flex;\n align-items: center;\n justify-content: center;\n background-color: white;\n padding: 0.5em;\n border-radius: 6px;\n border: 0;\n color: #344054;\n &:hover {\n background-color: #d3d3d3;\n }\n`;\nconst ConfirmButton = styled.button`\n padding: 0.7em;\n border-radius: 6px;\n border: 0;\n box-shadow: 0px 1px 2px rgba(16, 24, 40, 0.05);\n background-color: #12b76a;\n color: white;\n &:hover {\n background-color: #0e9f5d;\n }\n`;\nconst ModalContent = styled.div`\n flex: 1;\n font-size: 14px;\n margin-top: 4px;\n margin-bottom: 4px;\n overflow-y: auto;\n max-height: 50%;\n @media screen and (max-width: 768px) {\n font-size: 12px !important;\n }\n`;\nconst NoButton = styled.button`\n background: transparent;\n border: none;\n padding: 0;\n margin: 0;\n box-shadow: none;\n`;\nreturn (\n <>\n <Modal hidden={!isOpen}>\n <ModalBackdrop />\n <ModalDialog className=\"card\">\n <ModalHeader>\n <h5 className=\"mb-0\">Warning: No approved proposal found!</h5>\n </ModalHeader>\n <ModalContent>\n You haven't approved any proposals linked to the RFP. Please approve a\n proposal to proceed to the proposal selection phase.\n </ModalContent>\n <div className=\"d-flex gap-2 align-items-center justify-content-end mt-2\">\n <Widget\n src={`devhub.near/widget/devhub.components.molecule.Button`}\n props={{\n classNames: { root: \"btn-outline-secondary\" },\n label: \"Dismiss\",\n onClick: onConfirmClick,\n }}\n />\n </div>\n </ModalDialog>\n </Modal>\n </>\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://near.social/near/widget/ProfilePage?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" }, "core.common": { "": "const RFP_TIMELINE_STATUS = {\n ACCEPTING_SUBMISSIONS: \"ACCEPTING_SUBMISSIONS\",\n EVALUATION: \"EVALUATION\",\n PROPOSAL_SELECTED: \"PROPOSAL_SELECTED\",\n CANCELLED: \"CANCELLED\",\n};\nconst PROPOSAL_TIMELINE_STATUS = {\n DRAFT: \"DRAFT\",\n REVIEW: \"REVIEW\",\n APPROVED: \"APPROVED\",\n REJECTED: \"REJECTED\",\n CANCELED: \"CANCELLED\",\n APPROVED_CONDITIONALLY: \"APPROVED_CONDITIONALLY\",\n PAYMENT_PROCESSING: \"PAYMENT_PROCESSING\",\n FUNDED: \"FUNDED\",\n};\nconst QUERYAPI_ENDPOINT = `https://near-queryapi.api.pagoda.co/v1/graphql`;\nasync function fetchGraphQL(operationsDoc, operationName, variables) {\n return asyncFetch(QUERYAPI_ENDPOINT, {\n method: \"POST\",\n headers: { \"x-hasura-role\": \"bos_forum_potlock_near\" },\n body: JSON.stringify({\n query: operationsDoc,\n variables: variables,\n operationName: operationName,\n }),\n });\n}\nconst CANCEL_RFP_OPTIONS = {\n CANCEL_PROPOSALS: \"CANCEL_PROPOSALS\",\n UNLINK_PROPOSALS: \"UNLINK_PROPOSALSS\",\n NONE: \"NONE\",\n};\nfunction parseJSON(json) {\n if (typeof json === \"string\") {\n try {\n return JSON.parse(json);\n } catch (error) {\n return json;\n }\n } else {\n return json;\n }\n}\nfunction isNumber(value) {\n return typeof value === \"number\";\n}\nconst PROPOSALS_APPROVED_STATUS_ARRAY = [\n PROPOSAL_TIMELINE_STATUS.APPROVED,\n PROPOSAL_TIMELINE_STATUS.APPROVED_CONDITIONALLY,\n PROPOSAL_TIMELINE_STATUS.PAYMENT_PROCESSING,\n PROPOSAL_TIMELINE_STATUS.FUNDED,\n];\nfunction getLinkUsingCurrentGateway(url) {\n const data = fetch(`https://httpbin.org/headers`);\n const gatewayURL = data?.body?.headers?.Origin ?? \"\";\n return `https://${\n gatewayURL.includes(\"near.org\") ? \"dev.near.org\" : \"near.social\"\n }/${url}`;\n}\nreturn {\n RFP_TIMELINE_STATUS,\n PROPOSAL_TIMELINE_STATUS,\n fetchGraphQL,\n CANCEL_RFP_OPTIONS,\n parseJSON,\n isNumber,\n PROPOSALS_APPROVED_STATUS_ARRAY,\n getLinkUsingCurrentGateway,\n};\n" }, "components.proposals.ViewProposalModal": { "": "const { href: linkHref } = VM.require(`devhub.near/widget/core.lib.url`);\nlinkHref || (linkHref = () => {});\nconst isOpen = props.isOpen;\nconst isEdit = props.isEdit;\nconst proposalId = props.proposalId;\nconst Modal = styled.div`\n display: ${({ hidden }) => (hidden ? \"none\" : \"flex\")};\n position: fixed;\n inset: 0;\n justify-content: center;\n align-items: center;\n opacity: 1;\n z-index: 999;\n .black-btn {\n background-color: #000 !important;\n border: none;\n color: white;\n &:active {\n color: white;\n }\n }\n @media screen and (max-width: 768px) {\n h5 {\n font-size: 16px !important;\n }\n }\n .btn {\n font-size: 14px;\n }\n .btn-blue {\n background-image: linear-gradient(to bottom, #4b7a93, #213236);\n color: white;\n }\n`;\nconst ModalBackdrop = styled.div`\n position: absolute;\n inset: 0;\n background-color: rgba(0, 0, 0, 0.9);\n opacity: 0.9;\n`;\nconst ModalDialog = styled.div`\n padding: 2em;\n z-index: 999;\n overflow-y: auto;\n max-height: 85%;\n margin-top: 5%;\n width: 30%;\n @media screen and (max-width: 768px) {\n margin: 2rem;\n width: 100%;\n }\n`;\nconst ModalHeader = styled.div`\n display: flex;\n flex-direction: row;\n justify-content: space-between;\n align-items: center;\n padding-bottom: 4px;\n`;\nconst ModalFooter = styled.div`\n padding-top: 4px;\n display: flex;\n flex-direction: row;\n justify-content: space-between;\n align-items: items-center;\n`;\nconst CloseButton = styled.button`\n display: flex;\n align-items: center;\n justify-content: center;\n background-color: white;\n padding: 0.5em;\n border-radius: 6px;\n border: 0;\n color: #344054;\n &:hover {\n background-color: #d3d3d3;\n }\n`;\nconst ConfirmButton = styled.button`\n padding: 0.7em;\n border-radius: 6px;\n border: 0;\n box-shadow: 0px 1px 2px rgba(16, 24, 40, 0.05);\n background-color: #12b76a;\n color: white;\n &:hover {\n background-color: #0e9f5d;\n }\n`;\nconst ModalContent = styled.div`\n flex: 1;\n font-size: 14px;\n margin-top: 4px;\n margin-bottom: 4px;\n overflow-y: auto;\n max-height: 50%;\n @media screen and (max-width: 768px) {\n font-size: 12px !important;\n }\n`;\nconst NoButton = styled.button`\n background: transparent;\n border: none;\n padding: 0;\n margin: 0;\n box-shadow: none;\n`;\nreturn (\n <>\n <Modal hidden={!isOpen}>\n <ModalBackdrop />\n <ModalDialog className=\"card\">\n <ModalHeader>\n <h5 className=\"mb-0\">\n Your Proposal has been successfully{\" \"}\n {!isEdit ? \"published\" : \"edited\"}.\n </h5>\n </ModalHeader>\n <div className=\"d-flex gap-2 justify-content-center mt-2\">\n <Link\n to={linkHref({\n widgetSrc: `bos.forum.potlock.near/widget/app`,\n params: { page: \"proposal\", id: proposalId },\n })}\n >\n <Widget\n src={`devhub.near/widget/devhub.components.molecule.Button`}\n props={{\n classNames: { root: \"btn-blue\" },\n label: \"View Proposal\",\n onClick: () => {},\n }}\n />\n </Link>\n </div>\n </ModalDialog>\n </Modal>\n </>\n);\n" }, "components.molecule.RadioButton": { "": "const RadioButton = ({ value, isChecked, label, onClick, disabled }) => {\n const [checked, setChecked] = useState(isChecked);\n useEffect(() => {\n if (isChecked !== checked) {\n setChecked(isChecked);\n }\n }, [isChecked]);\n useEffect(() => {\n onClick(checked);\n }, [checked]);\n return (\n <div className=\"d-flex gap-2 align-items-center\">\n <input\n className=\"form-check-input\"\n type=\"radio\"\n disabled={disabled}\n value={value}\n checked={checked}\n onChange={(e) => setChecked(e.target.checked)}\n />\n <label className=\"form-check-label text-sm\">{label}</label>\n </div>\n );\n};\nreturn RadioButton(props);\n" }, "components.molecule.DropDown": { "": "const options = props.options; // [{label:\"\",value:\"\"}]\nconst label = props.label;\nconst onUpdate = props.onUpdate ?? (() => {});\nconst selectedValue = props.selectedValue;\nconst [selected, setSelected] = useState(selectedValue);\nuseEffect(() => {\n if (JSON.stringify(selectedValue) !== JSON.stringify(selected)) {\n setSelected(selectedValue);\n }\n}, [selectedValue]);\nconst StyledDropdown = styled.div`\n .drop-btn {\n width: 100%;\n max-width: 200px;\n text-align: left;\n padding-inline: 10px;\n }\n .dropdown-item.active,\n .dropdown-item:active {\n background-color: #f0f0f0 !important;\n color: black;\n }\n .cursor-pointer {\n cursor: pointer;\n }\n`;\nuseEffect(() => {\n onUpdate(selected);\n}, [selected]);\nreturn (\n <div>\n <div className=\"dropdown w-100\">\n <StyledDropdown>\n <button\n className=\"btn drop-btn text-truncate dropdown-toggle bg-white border rounded-2\"\n type=\"button\"\n data-bs-toggle=\"dropdown\"\n aria-expanded=\"false\"\n >\n {label} {selected && label && \": \"} {selected.label}\n </button>\n <ul className=\"dropdown-menu dropdown-menu-end dropdown-menu-lg-start px-2 shadow\">\n {options.map((item) => (\n <li\n style={{ borderRadius: \"5px\" }}\n className=\"dropdown-item cursor-pointer link-underline link-underline-opacity-0\"\n onClick={() => {\n if (selected.label !== item.label) {\n setSelected(item);\n }\n }}\n >\n {item.label}\n </li>\n ))}\n </ul>\n </StyledDropdown>\n </div>\n </div>\n);\n" } } } } }
Result:
{ "block_height": "122380822" }
No logs
Receipt:
Predecessor ID:
Gas Burned:
223 Ggas
Tokens Burned:
0 
Transferred 0.1792  to bos.forum.potlock.near
Empty result
No logs