From 30e183ea1f25119ffa578bf461c76b995b33762f Mon Sep 17 00:00:00 2001 From: Thomas Date: Tue, 18 Jun 2024 15:37:34 -0500 Subject: [PATCH 01/32] added config json file --- instances/events-committee.near/.gitignore | 2 ++ instances/events-committee.near/aliases.mainnet.json | 11 +++++++++++ instances/events-committee.near/bos.config.json | 6 ++++++ instances/events-committee.near/data.json | 3 +++ package.json | 2 ++ 5 files changed, 24 insertions(+) create mode 100644 instances/events-committee.near/.gitignore create mode 100644 instances/events-committee.near/aliases.mainnet.json create mode 100644 instances/events-committee.near/bos.config.json create mode 100644 instances/events-committee.near/data.json diff --git a/instances/events-committee.near/.gitignore b/instances/events-committee.near/.gitignore new file mode 100644 index 000000000..9d0b71a3c --- /dev/null +++ b/instances/events-committee.near/.gitignore @@ -0,0 +1,2 @@ +build +dist diff --git a/instances/events-committee.near/aliases.mainnet.json b/instances/events-committee.near/aliases.mainnet.json new file mode 100644 index 000000000..aadaf50b5 --- /dev/null +++ b/instances/events-committee.near/aliases.mainnet.json @@ -0,0 +1,11 @@ +{ + "REPL_DEVHUB": "events-committee.near", + "REPL_DEVHUB_LEGACY": "devgovgigs.near", + "REPL_DEVHUB_CONTRACT": "events-committee.near", + "REPL_NEAR": "near", + "REPL_MOB": "mob.near", + "REPL_EFIZ": "efiz.near", + "REPL_DEVS": "devs.near", + "REPL_SOCIAL_CONTRACT": "social.near", + "REPL_RPC_URL": "https://rpc.mainnet.near.org" +} diff --git a/instances/events-committee.near/bos.config.json b/instances/events-committee.near/bos.config.json new file mode 100644 index 000000000..a382469fb --- /dev/null +++ b/instances/events-committee.near/bos.config.json @@ -0,0 +1,6 @@ +{ + "account": "events-committee.near", + "aliasPrefix": "REPL", + "aliasesContainsPrefix": true, + "aliases": ["./aliases.mainnet.json"] +} diff --git a/instances/events-committee.near/data.json b/instances/events-committee.near/data.json new file mode 100644 index 000000000..9c8e62299 --- /dev/null +++ b/instances/events-committee.near/data.json @@ -0,0 +1,3 @@ +{ + "event-committee.near": {} +} diff --git a/package.json b/package.json index fd477a0bb..cb26db0a1 100644 --- a/package.json +++ b/package.json @@ -16,7 +16,9 @@ "fmt:check": "prettier --check '**/*.{js,jsx,ts,tsx,json}'", "build": "npm run fmt", "bw:dev:devhub": "bw dev instances/devhub.near", + "bw:dev:events": "bw dev instances/events-committee.near", "bw:build:devhub": "bw build instances/devhub.near", + "bw:build:events": "bw build instances/events-committee.near", "gateway:devhub": "node scripts/dev-gateway.mjs devhub.near", "test": "npx playwright test", "test:watch:codespaces": "npm test -- --ui-host=0.0.0.0", From 96f57e92314daeae4fa0aecf9bfd7e2cc09700ca Mon Sep 17 00:00:00 2001 From: Thomas Date: Tue, 18 Jun 2024 16:49:13 -0500 Subject: [PATCH 02/32] added widget folder + symlink --- instances/events-committee.near/data.json | 2 +- instances/events-committee.near/src | 1 + .../widget/DevGov/Notification/Item/Left.jsx | 34 + .../widget/DevGov/Notification/Item/Right.jsx | 20 + .../events-committee.near/widget/app.jsx | 106 ++ .../widget/devhub/components/atom/Alert.jsx | 15 + .../widget/devhub/components/atom/Icon.jsx | 45 + .../widget/devhub/components/atom/Tag.jsx | 13 + .../widget/devhub/components/atom/Toggle.jsx | 77 + .../components/feed/MergedIndexFeed.jsx | 265 +++ .../devhub/components/feed/SubscribedFeed.jsx | 179 ++ .../devhub/components/island/banner.jsx | 100 ++ .../devhub/components/island/connect.jsx | 332 ++++ .../devhub/components/island/explore.jsx | 193 +++ .../widget/devhub/components/island/hero.jsx | 212 +++ .../devhub/components/island/home-section.jsx | 63 + .../devhub/components/island/participate.jsx | 207 +++ .../devhub/components/island/support.jsx | 134 ++ .../components/layout/LikeButton/Faces.jsx | 122 ++ .../molecule/AccountAutocomplete.jsx | 147 ++ .../components/molecule/BadgeDetails.jsx | 78 + .../devhub/components/molecule/BadgesList.jsx | 63 + .../components/molecule/BlogControl.jsx | 16 + .../devhub/components/molecule/Button.jsx | 168 ++ .../devhub/components/molecule/Checkbox.jsx | 28 + .../components/molecule/CommunityControl.jsx | 51 + .../devhub/components/molecule/Compose.jsx | 120 ++ .../devhub/components/molecule/DropDown.jsx | 60 + .../molecule/DropDownWithSearch.jsx | 176 ++ .../devhub/components/molecule/Input.jsx | 227 +++ .../devhub/components/molecule/ListEditor.jsx | 85 + .../components/molecule/MarkdownEditor.jsx | 14 + .../components/molecule/MarkdownViewer.jsx | 60 + .../components/molecule/NavbarDropdown.jsx | 129 ++ .../components/molecule/PostControls.jsx | 51 + .../components/molecule/ProfileCard.jsx | 109 ++ .../components/molecule/ProfileLine.jsx | 21 + .../devhub/components/molecule/Select.jsx | 52 + .../devhub/components/molecule/SimpleMDE.jsx | 538 ++++++ .../devhub/components/molecule/Spinner.jsx | 7 + .../devhub/components/molecule/Switch.jsx | 39 + .../devhub/components/molecule/Tile.jsx | 20 + .../components/organism/Configurator.jsx | 316 ++++ .../devhub/components/organism/Feed.jsx | 130 ++ .../components/organism/Feed/NearQueryApi.jsx | 275 +++ .../devhub/components/organism/Navbar.jsx | 276 ++++ .../devhub/components/organism/NewsLetter.jsx | 146 ++ .../devhub/components/templates/AppLayout.jsx | 64 + .../widget/devhub/entity/addon/blog/Card.jsx | 77 + .../devhub/entity/addon/blog/Configurator.jsx | 45 + .../widget/devhub/entity/addon/blog/Feed.jsx | 142 ++ .../widget/devhub/entity/addon/blog/Page.jsx | 129 ++ .../devhub/entity/addon/blog/Viewer.jsx | 79 + .../entity/addon/blog/editor/content.jsx | 263 +++ .../devhub/entity/addon/blog/editor/form.jsx | 169 ++ .../devhub/entity/addon/blog/editor/index.jsx | 81 + .../entity/addon/blog/editor/layout.jsx | 36 + .../entity/addon/blog/editor/provider.jsx | 140 ++ .../entity/addon/blog/editor/sidebar.jsx | 50 + .../devhub/entity/addon/blogv2/Blog.jsx | 13 + .../devhub/entity/addon/blogv2/Card.jsx | 77 + .../entity/addon/blogv2/Configurator.jsx | 84 + .../devhub/entity/addon/blogv2/Page.jsx | 143 ++ .../devhub/entity/addon/blogv2/Viewer.jsx | 274 +++ .../addon/blogv2/editor/BlogOverview.jsx | 106 ++ .../addon/blogv2/editor/BlogPostSettings.jsx | 17 + .../addon/blogv2/editor/CategoryDropdown.jsx | 153 ++ .../addon/blogv2/editor/ConfirmModal.jsx | 150 ++ .../entity/addon/blogv2/editor/content.jsx | 693 ++++++++ .../entity/addon/blogv2/editor/form.jsx | 209 +++ .../entity/addon/blogv2/editor/index.jsx | 90 + .../entity/addon/blogv2/editor/layout.jsx | 148 ++ .../entity/addon/blogv2/editor/provider.jsx | 120 ++ .../entity/addon/github/Configurator.jsx | 603 +++++++ .../devhub/entity/addon/github/Viewer.jsx | 38 + .../entity/addon/github/kanban_board.jsx | 232 +++ .../entity/addon/github/kanban_ticket.jsx | 123 ++ .../entity/addon/kanban/Configurator.jsx | 478 ++++++ .../devhub/entity/addon/kanban/Viewer.jsx | 34 + .../devhub/entity/addon/kanban/post_board.jsx | 194 +++ .../entity/addon/kanban/post_ticket.jsx | 188 +++ .../entity/addon/telegram/Configurator.jsx | 119 ++ .../devhub/entity/addon/telegram/Viewer.jsx | 56 + .../devhub/entity/addon/wiki/Configurator.jsx | 163 ++ .../devhub/entity/addon/wiki/Viewer.jsx | 95 ++ .../devhub/entity/community/Activity.jsx | 85 + .../devhub/entity/community/Announcements.jsx | 210 +++ .../widget/devhub/entity/community/Card.jsx | 71 + .../devhub/entity/community/Compose.jsx | 478 ++++++ .../devhub/entity/community/Discussions.jsx | 276 ++++ .../devhub/entity/community/Provider.jsx | 72 + .../devhub/entity/community/Sidebar.jsx | 70 + .../devhub/entity/community/Spawner.jsx | 100 ++ .../widget/devhub/entity/community/Teams.jsx | 62 + .../widget/devhub/entity/community/Tile.jsx | 90 + .../configuration/AboutConfigurator.jsx | 64 + .../AccessControlConfigurator.jsx | 174 ++ .../configuration/AddonsConfigurator.jsx | 336 ++++ .../configuration/BrandingConfigurator.jsx | 165 ++ .../configuration/ConfigurationSection.jsx | 49 + .../configuration/InformationConfigurator.jsx | 78 + .../widget/devhub/entity/post/History.jsx | 130 ++ .../widget/devhub/entity/post/List.jsx | 400 +++++ .../widget/devhub/entity/post/Panel.jsx | 742 +++++++++ .../widget/devhub/entity/post/Post.jsx | 924 +++++++++++ .../widget/devhub/entity/post/PostEditor.jsx | 932 +++++++++++ .../widget/devhub/entity/post/Postv2.jsx | 20 + .../widget/devhub/entity/post/draft.jsx | 10 + .../devhub/entity/proposal/AccountInput.jsx | 76 + .../entity/proposal/CategoryDropdown.jsx | 184 +++ .../devhub/entity/proposal/CategoryTag.jsx | 67 + .../devhub/entity/proposal/CommentIcon.jsx | 47 + .../entity/proposal/CommentsAndLogs.jsx | 463 ++++++ .../devhub/entity/proposal/ComposeComment.jsx | 251 +++ .../entity/proposal/ConfirmCancelModal.jsx | 156 ++ .../entity/proposal/ConfirmReviewModal.jsx | 153 ++ .../widget/devhub/entity/proposal/Editor.jsx | 1320 +++++++++++++++ .../widget/devhub/entity/proposal/Feed.jsx | 589 +++++++ .../widget/devhub/entity/proposal/History.jsx | 124 ++ .../devhub/entity/proposal/LikeButton.jsx | 146 ++ .../proposal/LinkedProposalsDropdown.jsx | 160 ++ .../devhub/entity/proposal/LoginScreen.jsx | 65 + .../proposal/MultiSelectLabelsDropdown.jsx | 182 ++ .../widget/devhub/entity/proposal/Profile.jsx | 55 + .../devhub/entity/proposal/Proposal.jsx | 1471 +++++++++++++++++ .../devhub/entity/proposal/StatusTag.jsx | 74 + .../entity/proposal/VerificationStatus.jsx | 252 +++ .../devhub/entity/team/Configurator.jsx | 247 +++ .../devhub/entity/team/LabelPermissions.jsx | 35 + .../widget/devhub/entity/team/LabelRow.jsx | 264 +++ .../devhub/feature/post-search/by-author.jsx | 30 + .../devhub/feature/post-search/by-tag.jsx | 30 + .../devhub/feature/post-search/panel.jsx | 218 +++ .../feature/proposal-search/by-author.jsx | 42 + .../feature/proposal-search/by-category.jsx | 51 + .../feature/proposal-search/by-input.jsx | 39 + .../feature/proposal-search/by-sort.jsx | 27 + .../feature/proposal-search/by-stage.jsx | 30 + .../widget/devhub/notification/Item.jsx | 22 + .../widget/devhub/notification/LR.jsx | 26 + .../widget/devhub/notification/Left.jsx | 34 + .../widget/devhub/notification/Right.jsx | 20 + .../widget/devhub/page/about.jsx | 170 ++ .../widget/devhub/page/addon.jsx | 170 ++ .../widget/devhub/page/admin/index.jsx | 139 ++ .../devhub/page/admin/moderatorsTab.jsx | 125 ++ .../widget/devhub/page/announcements.jsx | 151 ++ .../widget/devhub/page/blog.jsx | 111 ++ .../widget/devhub/page/blogv2.jsx | 146 ++ .../widget/devhub/page/communities.jsx | 222 +++ .../devhub/page/community/configuration.jsx | 189 +++ .../widget/devhub/page/community/index.jsx | 365 ++++ .../widget/devhub/page/contribute.jsx | 204 +++ .../widget/devhub/page/feed.jsx | 76 + .../widget/devhub/page/home.jsx | 12 + .../widget/devhub/page/post.jsx | 22 + .../widget/devhub/page/profile.jsx | 11 + .../widget/devhub/page/proposals.jsx | 6 + 158 files changed, 26003 insertions(+), 1 deletion(-) create mode 120000 instances/events-committee.near/src create mode 100644 instances/events-committee.near/widget/DevGov/Notification/Item/Left.jsx create mode 100644 instances/events-committee.near/widget/DevGov/Notification/Item/Right.jsx create mode 100644 instances/events-committee.near/widget/app.jsx create mode 100644 instances/events-committee.near/widget/devhub/components/atom/Alert.jsx create mode 100644 instances/events-committee.near/widget/devhub/components/atom/Icon.jsx create mode 100644 instances/events-committee.near/widget/devhub/components/atom/Tag.jsx create mode 100644 instances/events-committee.near/widget/devhub/components/atom/Toggle.jsx create mode 100644 instances/events-committee.near/widget/devhub/components/feed/MergedIndexFeed.jsx create mode 100644 instances/events-committee.near/widget/devhub/components/feed/SubscribedFeed.jsx create mode 100644 instances/events-committee.near/widget/devhub/components/island/banner.jsx create mode 100644 instances/events-committee.near/widget/devhub/components/island/connect.jsx create mode 100644 instances/events-committee.near/widget/devhub/components/island/explore.jsx create mode 100644 instances/events-committee.near/widget/devhub/components/island/hero.jsx create mode 100644 instances/events-committee.near/widget/devhub/components/island/home-section.jsx create mode 100644 instances/events-committee.near/widget/devhub/components/island/participate.jsx create mode 100644 instances/events-committee.near/widget/devhub/components/island/support.jsx create mode 100644 instances/events-committee.near/widget/devhub/components/layout/LikeButton/Faces.jsx create mode 100644 instances/events-committee.near/widget/devhub/components/molecule/AccountAutocomplete.jsx create mode 100644 instances/events-committee.near/widget/devhub/components/molecule/BadgeDetails.jsx create mode 100644 instances/events-committee.near/widget/devhub/components/molecule/BadgesList.jsx create mode 100644 instances/events-committee.near/widget/devhub/components/molecule/BlogControl.jsx create mode 100644 instances/events-committee.near/widget/devhub/components/molecule/Button.jsx create mode 100644 instances/events-committee.near/widget/devhub/components/molecule/Checkbox.jsx create mode 100644 instances/events-committee.near/widget/devhub/components/molecule/CommunityControl.jsx create mode 100644 instances/events-committee.near/widget/devhub/components/molecule/Compose.jsx create mode 100644 instances/events-committee.near/widget/devhub/components/molecule/DropDown.jsx create mode 100644 instances/events-committee.near/widget/devhub/components/molecule/DropDownWithSearch.jsx create mode 100644 instances/events-committee.near/widget/devhub/components/molecule/Input.jsx create mode 100644 instances/events-committee.near/widget/devhub/components/molecule/ListEditor.jsx create mode 100644 instances/events-committee.near/widget/devhub/components/molecule/MarkdownEditor.jsx create mode 100644 instances/events-committee.near/widget/devhub/components/molecule/MarkdownViewer.jsx create mode 100644 instances/events-committee.near/widget/devhub/components/molecule/NavbarDropdown.jsx create mode 100644 instances/events-committee.near/widget/devhub/components/molecule/PostControls.jsx create mode 100644 instances/events-committee.near/widget/devhub/components/molecule/ProfileCard.jsx create mode 100644 instances/events-committee.near/widget/devhub/components/molecule/ProfileLine.jsx create mode 100644 instances/events-committee.near/widget/devhub/components/molecule/Select.jsx create mode 100644 instances/events-committee.near/widget/devhub/components/molecule/SimpleMDE.jsx create mode 100644 instances/events-committee.near/widget/devhub/components/molecule/Spinner.jsx create mode 100644 instances/events-committee.near/widget/devhub/components/molecule/Switch.jsx create mode 100644 instances/events-committee.near/widget/devhub/components/molecule/Tile.jsx create mode 100644 instances/events-committee.near/widget/devhub/components/organism/Configurator.jsx create mode 100644 instances/events-committee.near/widget/devhub/components/organism/Feed.jsx create mode 100644 instances/events-committee.near/widget/devhub/components/organism/Feed/NearQueryApi.jsx create mode 100644 instances/events-committee.near/widget/devhub/components/organism/Navbar.jsx create mode 100644 instances/events-committee.near/widget/devhub/components/organism/NewsLetter.jsx create mode 100644 instances/events-committee.near/widget/devhub/components/templates/AppLayout.jsx create mode 100644 instances/events-committee.near/widget/devhub/entity/addon/blog/Card.jsx create mode 100644 instances/events-committee.near/widget/devhub/entity/addon/blog/Configurator.jsx create mode 100644 instances/events-committee.near/widget/devhub/entity/addon/blog/Feed.jsx create mode 100644 instances/events-committee.near/widget/devhub/entity/addon/blog/Page.jsx create mode 100644 instances/events-committee.near/widget/devhub/entity/addon/blog/Viewer.jsx create mode 100644 instances/events-committee.near/widget/devhub/entity/addon/blog/editor/content.jsx create mode 100644 instances/events-committee.near/widget/devhub/entity/addon/blog/editor/form.jsx create mode 100644 instances/events-committee.near/widget/devhub/entity/addon/blog/editor/index.jsx create mode 100644 instances/events-committee.near/widget/devhub/entity/addon/blog/editor/layout.jsx create mode 100644 instances/events-committee.near/widget/devhub/entity/addon/blog/editor/provider.jsx create mode 100644 instances/events-committee.near/widget/devhub/entity/addon/blog/editor/sidebar.jsx create mode 100644 instances/events-committee.near/widget/devhub/entity/addon/blogv2/Blog.jsx create mode 100644 instances/events-committee.near/widget/devhub/entity/addon/blogv2/Card.jsx create mode 100644 instances/events-committee.near/widget/devhub/entity/addon/blogv2/Configurator.jsx create mode 100644 instances/events-committee.near/widget/devhub/entity/addon/blogv2/Page.jsx create mode 100644 instances/events-committee.near/widget/devhub/entity/addon/blogv2/Viewer.jsx create mode 100644 instances/events-committee.near/widget/devhub/entity/addon/blogv2/editor/BlogOverview.jsx create mode 100644 instances/events-committee.near/widget/devhub/entity/addon/blogv2/editor/BlogPostSettings.jsx create mode 100644 instances/events-committee.near/widget/devhub/entity/addon/blogv2/editor/CategoryDropdown.jsx create mode 100644 instances/events-committee.near/widget/devhub/entity/addon/blogv2/editor/ConfirmModal.jsx create mode 100644 instances/events-committee.near/widget/devhub/entity/addon/blogv2/editor/content.jsx create mode 100644 instances/events-committee.near/widget/devhub/entity/addon/blogv2/editor/form.jsx create mode 100644 instances/events-committee.near/widget/devhub/entity/addon/blogv2/editor/index.jsx create mode 100644 instances/events-committee.near/widget/devhub/entity/addon/blogv2/editor/layout.jsx create mode 100644 instances/events-committee.near/widget/devhub/entity/addon/blogv2/editor/provider.jsx create mode 100644 instances/events-committee.near/widget/devhub/entity/addon/github/Configurator.jsx create mode 100644 instances/events-committee.near/widget/devhub/entity/addon/github/Viewer.jsx create mode 100644 instances/events-committee.near/widget/devhub/entity/addon/github/kanban_board.jsx create mode 100644 instances/events-committee.near/widget/devhub/entity/addon/github/kanban_ticket.jsx create mode 100644 instances/events-committee.near/widget/devhub/entity/addon/kanban/Configurator.jsx create mode 100644 instances/events-committee.near/widget/devhub/entity/addon/kanban/Viewer.jsx create mode 100644 instances/events-committee.near/widget/devhub/entity/addon/kanban/post_board.jsx create mode 100644 instances/events-committee.near/widget/devhub/entity/addon/kanban/post_ticket.jsx create mode 100644 instances/events-committee.near/widget/devhub/entity/addon/telegram/Configurator.jsx create mode 100644 instances/events-committee.near/widget/devhub/entity/addon/telegram/Viewer.jsx create mode 100644 instances/events-committee.near/widget/devhub/entity/addon/wiki/Configurator.jsx create mode 100644 instances/events-committee.near/widget/devhub/entity/addon/wiki/Viewer.jsx create mode 100644 instances/events-committee.near/widget/devhub/entity/community/Activity.jsx create mode 100644 instances/events-committee.near/widget/devhub/entity/community/Announcements.jsx create mode 100644 instances/events-committee.near/widget/devhub/entity/community/Card.jsx create mode 100644 instances/events-committee.near/widget/devhub/entity/community/Compose.jsx create mode 100644 instances/events-committee.near/widget/devhub/entity/community/Discussions.jsx create mode 100644 instances/events-committee.near/widget/devhub/entity/community/Provider.jsx create mode 100644 instances/events-committee.near/widget/devhub/entity/community/Sidebar.jsx create mode 100644 instances/events-committee.near/widget/devhub/entity/community/Spawner.jsx create mode 100644 instances/events-committee.near/widget/devhub/entity/community/Teams.jsx create mode 100644 instances/events-committee.near/widget/devhub/entity/community/Tile.jsx create mode 100644 instances/events-committee.near/widget/devhub/entity/community/configuration/AboutConfigurator.jsx create mode 100644 instances/events-committee.near/widget/devhub/entity/community/configuration/AccessControlConfigurator.jsx create mode 100644 instances/events-committee.near/widget/devhub/entity/community/configuration/AddonsConfigurator.jsx create mode 100644 instances/events-committee.near/widget/devhub/entity/community/configuration/BrandingConfigurator.jsx create mode 100644 instances/events-committee.near/widget/devhub/entity/community/configuration/ConfigurationSection.jsx create mode 100644 instances/events-committee.near/widget/devhub/entity/community/configuration/InformationConfigurator.jsx create mode 100644 instances/events-committee.near/widget/devhub/entity/post/History.jsx create mode 100644 instances/events-committee.near/widget/devhub/entity/post/List.jsx create mode 100644 instances/events-committee.near/widget/devhub/entity/post/Panel.jsx create mode 100644 instances/events-committee.near/widget/devhub/entity/post/Post.jsx create mode 100644 instances/events-committee.near/widget/devhub/entity/post/PostEditor.jsx create mode 100644 instances/events-committee.near/widget/devhub/entity/post/Postv2.jsx create mode 100644 instances/events-committee.near/widget/devhub/entity/post/draft.jsx create mode 100644 instances/events-committee.near/widget/devhub/entity/proposal/AccountInput.jsx create mode 100644 instances/events-committee.near/widget/devhub/entity/proposal/CategoryDropdown.jsx create mode 100644 instances/events-committee.near/widget/devhub/entity/proposal/CategoryTag.jsx create mode 100644 instances/events-committee.near/widget/devhub/entity/proposal/CommentIcon.jsx create mode 100644 instances/events-committee.near/widget/devhub/entity/proposal/CommentsAndLogs.jsx create mode 100644 instances/events-committee.near/widget/devhub/entity/proposal/ComposeComment.jsx create mode 100644 instances/events-committee.near/widget/devhub/entity/proposal/ConfirmCancelModal.jsx create mode 100644 instances/events-committee.near/widget/devhub/entity/proposal/ConfirmReviewModal.jsx create mode 100644 instances/events-committee.near/widget/devhub/entity/proposal/Editor.jsx create mode 100644 instances/events-committee.near/widget/devhub/entity/proposal/Feed.jsx create mode 100644 instances/events-committee.near/widget/devhub/entity/proposal/History.jsx create mode 100644 instances/events-committee.near/widget/devhub/entity/proposal/LikeButton.jsx create mode 100644 instances/events-committee.near/widget/devhub/entity/proposal/LinkedProposalsDropdown.jsx create mode 100644 instances/events-committee.near/widget/devhub/entity/proposal/LoginScreen.jsx create mode 100644 instances/events-committee.near/widget/devhub/entity/proposal/MultiSelectLabelsDropdown.jsx create mode 100644 instances/events-committee.near/widget/devhub/entity/proposal/Profile.jsx create mode 100644 instances/events-committee.near/widget/devhub/entity/proposal/Proposal.jsx create mode 100644 instances/events-committee.near/widget/devhub/entity/proposal/StatusTag.jsx create mode 100644 instances/events-committee.near/widget/devhub/entity/proposal/VerificationStatus.jsx create mode 100644 instances/events-committee.near/widget/devhub/entity/team/Configurator.jsx create mode 100644 instances/events-committee.near/widget/devhub/entity/team/LabelPermissions.jsx create mode 100644 instances/events-committee.near/widget/devhub/entity/team/LabelRow.jsx create mode 100644 instances/events-committee.near/widget/devhub/feature/post-search/by-author.jsx create mode 100644 instances/events-committee.near/widget/devhub/feature/post-search/by-tag.jsx create mode 100644 instances/events-committee.near/widget/devhub/feature/post-search/panel.jsx create mode 100644 instances/events-committee.near/widget/devhub/feature/proposal-search/by-author.jsx create mode 100644 instances/events-committee.near/widget/devhub/feature/proposal-search/by-category.jsx create mode 100644 instances/events-committee.near/widget/devhub/feature/proposal-search/by-input.jsx create mode 100644 instances/events-committee.near/widget/devhub/feature/proposal-search/by-sort.jsx create mode 100644 instances/events-committee.near/widget/devhub/feature/proposal-search/by-stage.jsx create mode 100644 instances/events-committee.near/widget/devhub/notification/Item.jsx create mode 100644 instances/events-committee.near/widget/devhub/notification/LR.jsx create mode 100644 instances/events-committee.near/widget/devhub/notification/Left.jsx create mode 100644 instances/events-committee.near/widget/devhub/notification/Right.jsx create mode 100644 instances/events-committee.near/widget/devhub/page/about.jsx create mode 100644 instances/events-committee.near/widget/devhub/page/addon.jsx create mode 100644 instances/events-committee.near/widget/devhub/page/admin/index.jsx create mode 100644 instances/events-committee.near/widget/devhub/page/admin/moderatorsTab.jsx create mode 100644 instances/events-committee.near/widget/devhub/page/announcements.jsx create mode 100644 instances/events-committee.near/widget/devhub/page/blog.jsx create mode 100644 instances/events-committee.near/widget/devhub/page/blogv2.jsx create mode 100644 instances/events-committee.near/widget/devhub/page/communities.jsx create mode 100644 instances/events-committee.near/widget/devhub/page/community/configuration.jsx create mode 100644 instances/events-committee.near/widget/devhub/page/community/index.jsx create mode 100644 instances/events-committee.near/widget/devhub/page/contribute.jsx create mode 100644 instances/events-committee.near/widget/devhub/page/feed.jsx create mode 100644 instances/events-committee.near/widget/devhub/page/home.jsx create mode 100644 instances/events-committee.near/widget/devhub/page/post.jsx create mode 100644 instances/events-committee.near/widget/devhub/page/profile.jsx create mode 100644 instances/events-committee.near/widget/devhub/page/proposals.jsx diff --git a/instances/events-committee.near/data.json b/instances/events-committee.near/data.json index 9c8e62299..38f1b5f96 100644 --- a/instances/events-committee.near/data.json +++ b/instances/events-committee.near/data.json @@ -1,3 +1,3 @@ { - "event-committee.near": {} + "events-committee.near": {} } diff --git a/instances/events-committee.near/src b/instances/events-committee.near/src new file mode 120000 index 000000000..0301008ca --- /dev/null +++ b/instances/events-committee.near/src @@ -0,0 +1 @@ +widget \ No newline at end of file diff --git a/instances/events-committee.near/widget/DevGov/Notification/Item/Left.jsx b/instances/events-committee.near/widget/DevGov/Notification/Item/Left.jsx new file mode 100644 index 000000000..e238a2143 --- /dev/null +++ b/instances/events-committee.near/widget/DevGov/Notification/Item/Left.jsx @@ -0,0 +1,34 @@ +const { href } = VM.require("${REPL_DEVHUB}/widget/core.lib.url") || (() => {}); + +if (!props.type) { + return "Loading ..."; +} + +const type = props.type.split("/")[1]; +return props.type ? ( + <> + {type == "like" + ? "liked your" + : type == "reply" + ? "replied to your" + : type == "edit" + ? "edited your" + : type == "mention" + ? "mentioned you in their" + : "???"}{" "} + + DevHub post + + +) : ( + "Loading ..." +); diff --git a/instances/events-committee.near/widget/DevGov/Notification/Item/Right.jsx b/instances/events-committee.near/widget/DevGov/Notification/Item/Right.jsx new file mode 100644 index 000000000..b5c19d982 --- /dev/null +++ b/instances/events-committee.near/widget/DevGov/Notification/Item/Right.jsx @@ -0,0 +1,20 @@ +const { href } = VM.require("${REPL_DEVHUB}/widget/core.lib.url") || (() => {}); + +return props.post === undefined ? ( + "Loading ..." +) : ( + <> + + View DevHub post + + +); diff --git a/instances/events-committee.near/widget/app.jsx b/instances/events-committee.near/widget/app.jsx new file mode 100644 index 000000000..0a58ebad0 --- /dev/null +++ b/instances/events-committee.near/widget/app.jsx @@ -0,0 +1,106 @@ +/** + * This is the main entry point for the DevHub application. + * Page route gets passed in through params, along with all other page props. + */ + +const { onDraftStateChange } = VM.require( + "${REPL_DEVHUB}/widget/devhub.entity.post.draft" +); + +const { page, ...passProps } = props; + +// Import our modules +const { AppLayout } = VM.require( + "${REPL_DEVHUB}/widget/devhub.components.templates.AppLayout" +); + +if (!AppLayout) { + return

Loading modules...

; +} + +// CSS styles to be used across the app. +// Define fonts here, as well as any other global styles. +const Theme = styled.div` + a { + color: inherit; + } + + .attractable { + box-shadow: 0 0.125rem 0.25rem rgba(0, 0, 0, 0.075) !important; + transition: box-shadow 0.6s; + + &:hover { + box-shadow: 0 0.5rem 1rem rgba(0, 0, 0, 0.15) !important; + } + } +`; + +if (!page) { + // If no page is specified, we default to the feed page TEMP + page = "home"; +} + +// This is our navigation, rendering the page based on the page parameter +function Page() { + const routes = page.split("."); + switch (routes[0]) { + case "create-proposal": { + return ( + + ); + } + + case "proposals": { + return ( + + ); + } + case "proposal": { + return ( + + ); + } + // ?page=about + case "about": { + return ( + + ); + } + case "admin": { + return ( + + ); + } + default: { + return ( + + ); + } + } +} + +return ( + + + + + +); diff --git a/instances/events-committee.near/widget/devhub/components/atom/Alert.jsx b/instances/events-committee.near/widget/devhub/components/atom/Alert.jsx new file mode 100644 index 000000000..1f4470539 --- /dev/null +++ b/instances/events-committee.near/widget/devhub/components/atom/Alert.jsx @@ -0,0 +1,15 @@ +const Alert = ({ onClose, message }) => + message && ( + + ); + +return Alert(props); diff --git a/instances/events-committee.near/widget/devhub/components/atom/Icon.jsx b/instances/events-committee.near/widget/devhub/components/atom/Icon.jsx new file mode 100644 index 000000000..b9e425dfa --- /dev/null +++ b/instances/events-committee.near/widget/devhub/components/atom/Icon.jsx @@ -0,0 +1,45 @@ +const svgIconsByVariant = { + floppy_drive: (elementProps) => ( + + + + + + ), +}; + +const iconsByType = { + bootstrap_icon: ({ className, variant, ...otherProps }) => ( + + ), + + svg_icon: ({ variant, ...elementProps }) => + svgIconsByVariant[variant](elementProps), +}; + +const Icon = ({ type, ...otherProps }) => + typeof iconsByType[type] === "function" + ? iconsByType[type](otherProps) + : null; + +return Icon(props); diff --git a/instances/events-committee.near/widget/devhub/components/atom/Tag.jsx b/instances/events-committee.near/widget/devhub/components/atom/Tag.jsx new file mode 100644 index 000000000..d4a8d3216 --- /dev/null +++ b/instances/events-committee.near/widget/devhub/components/atom/Tag.jsx @@ -0,0 +1,13 @@ +const black = props.black; + +const Span = styled.span` + color: ${black ? "#818181" : "#00ec97"}; + font-size: 16px; + font-style: normal; + font-weight: 700; + line-height: 20px; /* 125% */ +`; + +const Tag = ({ tag }) => {tag}; + +return Tag(props); diff --git a/instances/events-committee.near/widget/devhub/components/atom/Toggle.jsx b/instances/events-committee.near/widget/devhub/components/atom/Toggle.jsx new file mode 100644 index 000000000..f91f10cf0 --- /dev/null +++ b/instances/events-committee.near/widget/devhub/components/atom/Toggle.jsx @@ -0,0 +1,77 @@ +const ToggleRoot = styled.div` + justify-content: space-between; + width: fit-content; + max-width: 100%; +`; + +const ToggleSwitchRoot = styled("Switch.Root")` + all: unset; + display: block; + width: 42px; + height: 25px; + background-color: #d1d1d1; + border-radius: 9999px; + position: relative; + box-shadow: 0 2px 10px var(--blackA7); + + &[data-state="checked"] { + background-color: #00d084; + } + + &[data-disabled=""] { + opacity: 0.7; + } +`; + +const ToggleSwitchThumb = styled("Switch.Thumb")` + all: unset; + display: block; + width: 21px; + height: 21px; + border-radius: 9999px; + transition: transform 100ms; + transform: translateX(2px); + will-change: transform; + + &[data-state="checked"] { + transform: translateX(19px); + } +`; + +const ToggleLabel = styled.label` + white-space: nowrap; +`; + +const Toggle = ({ + className, + direction, + disabled, + inputProps, + key, + label, + onChange, + value: checked, + ...rest +}) => ( + + {label} + + + {!disabled && } + + +); + +return Toggle(props); diff --git a/instances/events-committee.near/widget/devhub/components/feed/MergedIndexFeed.jsx b/instances/events-committee.near/widget/devhub/components/feed/MergedIndexFeed.jsx new file mode 100644 index 000000000..1c7567ad2 --- /dev/null +++ b/instances/events-committee.near/widget/devhub/components/feed/MergedIndexFeed.jsx @@ -0,0 +1,265 @@ +if (!props.index) { + return "props.index is not defined"; +} +const indices = JSON.parse( + JSON.stringify(Array.isArray(props.index) ? props.index : [props.index]) +); + +const filter = props.filter; + +const renderItem = + props.renderItem ?? + ((item) => ( +
+ #{item.blockHeight}: {JSON.stringify(item)} +
+ )); +const cachedRenderItem = (item, i) => { + const key = JSON.stringify(item); + + if (!(key in state.cachedItems)) { + state.cachedItems[key] = renderItem(item, i); + State.update(); + } + return state.cachedItems[key]; +}; + +const initialRenderLimit = props.initialRenderLimit ?? 10; +const addDisplayCount = props.nextLimit ?? initialRenderLimit; +const reverse = !!props.reverse; + +const computeFetchFrom = (items, limit, desc) => { + if (!items || items.length < limit) { + return false; + } + const blockHeight = items[items.length - 1].blockHeight; + return desc ? blockHeight - 1 : blockHeight + 1; +}; + +const mergeItems = (iIndex, oldItems, newItems, desc) => { + const index = indices[iIndex]; + const items = [ + ...new Set( + [ + ...newItems.map((item) => ({ + ...item, + action: index.action, + key: index.key, + index: iIndex, + })), + ...oldItems, + ].map((i) => JSON.stringify(i)) + ), + ].map((i) => JSON.parse(i)); + items.sort((a, b) => a.blockHeight - b.blockHeight); + if (desc) { + items.reverse(); + } + return items; +}; + +const jIndices = JSON.stringify(indices); +if (jIndices !== state.jIndices) { + State.update({ + jIndices, + feeds: indices.map(() => ({})), + items: [], + displayCount: initialRenderLimit, + cachedItems: {}, + }); +} + +let stateChanged = false; +for (let iIndex = 0; iIndex < indices.length; ++iIndex) { + const index = indices[iIndex]; + const feed = state.feeds[iIndex]; + let feedChanged = false; + index.options = index.options || {}; + index.options.limit = Math.min( + Math.max(initialRenderLimit + addDisplayCount * 2, index.options.limit), + 100 + ); + const desc = index.options.order === "desc"; + + const initialItems = Social.index( + index.action, + index.key, + index.options, + index.cacheOptions + ); + if (initialItems === null) { + continue; + } + + const jInitialItems = JSON.stringify(initialItems); + const nextFetchFrom = computeFetchFrom( + initialItems, + index.options.limit, + desc + ); + if (feed.jInitialItems !== jInitialItems) { + feed.jInitialItems = jInitialItems; + feedChanged = true; + if (nextFetchFrom !== feed.initialNextFetchFrom) { + feed.fetchFrom = false; + feed.items = mergeItems(iIndex, [], initialItems, desc); + feed.initialNextFetchFrom = nextFetchFrom; + feed.nextFetchFrom = nextFetchFrom; + } else { + feed.items = mergeItems(iIndex, feed.items, initialItems, desc); + } + } + + feed.usedCount = 0; + + if (feedChanged) { + state.feeds[iIndex] = feed; + stateChanged = true; + } +} + +// Construct merged feed and compute usage per feed. + +const filteredItems = []; +while (filteredItems.length < state.displayCount) { + let bestItem = null; + for (let iIndex = 0; iIndex < indices.length; ++iIndex) { + const index = indices[iIndex]; + const feed = state.feeds[iIndex]; + const desc = index.options.order === "desc"; + if (!feed.items) { + continue; + } + const item = feed.items[feed.usedCount]; + if (!item) { + continue; + } + if ( + bestItem === null || + (desc + ? item.blockHeight > bestItem.blockHeight + : item.blockHeight < bestItem.blockHeight) + ) { + bestItem = item; + } + } + if (!bestItem) { + break; + } + state.feeds[bestItem.index].usedCount++; + if (filter) { + if (filter.ignore) { + if (bestItem.accountId in filter.ignore) { + continue; + } + } + if (filter.require) { + if (!(bestItem.accountId in filter.require)) { + continue; + } + } + } + filteredItems.push(bestItem); +} + +// Fetch new items for feeds that don't have enough items. +for (let iIndex = 0; iIndex < indices.length; ++iIndex) { + const index = indices[iIndex]; + const feed = state.feeds[iIndex]; + const desc = index.options.order === "desc"; + let feedChanged = false; + + if ( + (feed.items.length || 0) - feed.usedCount < addDisplayCount * 2 && + !feed.fetchFrom && + feed.nextFetchFrom && + feed.nextFetchFrom !== feed.fetchFrom + ) { + feed.fetchFrom = feed.nextFetchFrom; + feedChanged = true; + } + + if (feed.fetchFrom) { + const limit = addDisplayCount; + const newItems = Social.index( + index.action, + index.key, + Object.assign({}, index.options, { + from: feed.fetchFrom, + subscribe: undefined, + limit, + }) + ); + if (newItems !== null) { + feed.items = mergeItems(iIndex, feed.items, newItems, desc); + feed.fetchFrom = false; + feed.nextFetchFrom = computeFetchFrom(newItems, limit, desc); + feedChanged = true; + } + } + + if (feedChanged) { + state.feeds[iIndex] = feed; + stateChanged = true; + } +} + +if (stateChanged) { + State.update(); +} + +const makeMoreItems = () => { + State.update({ + displayCount: state.displayCount + addDisplayCount, + }); +}; + +const loader = ( +
+
+); + +const fetchMore = + props.manual && + (state.feeds.some((f) => !!f.fetchFrom) && + filteredItems.length < state.displayCount + ? loader + : state.displayCount < filteredItems.length && ( + + )); + +const items = filteredItems ? filteredItems.slice(0, state.displayCount) : []; +if (reverse) { + items.reverse(); +} + +const renderedItems = items.map(cachedRenderItem); + +return props.manual ? ( + <> + {reverse && fetchMore} + {renderedItems} + {!reverse && fetchMore} + +) : ( + + {renderedItems} + +); diff --git a/instances/events-committee.near/widget/devhub/components/feed/SubscribedFeed.jsx b/instances/events-committee.near/widget/devhub/components/feed/SubscribedFeed.jsx new file mode 100644 index 000000000..cc21407f2 --- /dev/null +++ b/instances/events-committee.near/widget/devhub/components/feed/SubscribedFeed.jsx @@ -0,0 +1,179 @@ +const indexKey = props.indexKey ?? "main"; +const groupId = props.groupId; +const permissions = props.permissions; + +const index = [ + { + action: "post", + key: indexKey, + options: { + limit: 10, + order: props.sort ? props.sort : "desc", + subscribe: true, + accountId: props.accounts, + }, + cacheOptions: { + ignoreCache: true, + }, + }, + { + action: "repost", + key: indexKey, + options: { + limit: 10, + order: props.sort ? props.sort : "desc", + subscribe: true, + accountId: props.accounts, + }, + cacheOptions: { + ignoreCache: true, + }, + }, +]; + +const isPremiumFeed = props.isPremiumFeed; +const commentAccounts = props.commentAccounts; +const renderedPosts = {}; + +const makePostItem = (a) => ({ + type: "social", + path: `${a.accountId}/post/main`, + blockHeight: a.blockHeight, +}); + +const renderPost = (a) => { + if (a.value.type !== "md") { + return false; + } + const item = JSON.stringify(makePostItem(a)); + if (item in renderedPosts) { + return false; + } + renderedPosts[item] = true; + + return ( +
+ } + src="mob.near/widget/MainPage.N.Post" + props={{ + accountId: a.accountId, + blockHeight: a.blockHeight, + isPremiumFeed, + commentAccounts, + indexKey, + groupId, + permissions, + }} + /> +
+ ); +}; + +const repostSvg = ( + + + + +); + +const extractParentPost = (item) => { + if (!item || item.type !== "social" || !item.path || !item.blockHeight) { + return undefined; + } + const accountId = item.path.split("/")[0]; + return `${accountId}/post/main` === item.path + ? { accountId, blockHeight: item.blockHeight } + : undefined; +}; + +const renderRepost = (a) => { + if (a.value.type !== "repost") { + return false; + } + const post = extractParentPost(a.value.item); + if (!post) { + return false; + } + const item = JSON.stringify(makePostItem(post)); + if (item in renderedPosts) { + return false; + } + renderedPosts[item] = true; + + return ( +
+
+ {repostSvg}{" "} + + Reposted by{" "} + + +
+ } + src="mob.near/widget/MainPage.N.Post" + props={{ + accountId: post.accountId, + blockHeight: post.blockHeight, + reposted: true, + isPremiumFeed, + commentAccounts, + indexKey, + groupId, + permissions, + }} + /> +
+ ); +}; + +const renderItem = (item) => + item.action === "post" ? renderPost(item) : renderRepost(item); +return ( + +); diff --git a/instances/events-committee.near/widget/devhub/components/island/banner.jsx b/instances/events-committee.near/widget/devhub/components/island/banner.jsx new file mode 100644 index 000000000..e48754ced --- /dev/null +++ b/instances/events-committee.near/widget/devhub/components/island/banner.jsx @@ -0,0 +1,100 @@ +const imageLink = + "https://ipfs.near.social/ipfs/bafybeiap2mzwsly4apaldxguiunx4rjwqyadksj5yxuzwrww3kue3ao5qe"; + +const HeroSection = styled.div` + position: relative; + height: auto; + z-index: 3; + width: 70%; + background: #00ec97; + clip-path: polygon(0 0, 100% 0%, 75% 100%, 0% 100%); + + padding-top: 2rem; + padding-bottom: 2rem; + padding-left: 3.375rem; + + @media screen and (max-width: 768px) { + width: 100%; + padding: 1rem 1.5rem; + clip-path: none; + } +`; + +const Title = styled.h1` + color: #f4f4f4; + font-size: 4rem; + font-style: normal; + font-weight: 700; + line-height: 100%; /* 88px */ + letter-spacing: -1.76px; + + @media screen and (max-width: 768px) { + font-size: 2.25rem; + letter-spacing: -0.72px; + margin: 0; + } +`; + +const Container = styled.div` + position: relative; + width: 100%; + height: max-content; + overflow: hidden; + + @media screen and (max-width: 768px) { + background: #f4f4f4; + } +`; + +const ImageContainer = styled.div` + width: 100%; + height: 100%; + position: absolute; + top: 0; + right: 0; + z-index: 1; + background: transparent; + + @media screen and (max-width: 768px) { + display: none; + } +`; + +const Image = styled.img` + margin-left: 15.625rem; + height: 100%; + width: 100%; + filter: grayscale(100%); + object-fit: cover; +`; + +const MobileImage = styled.img` + display: none; + + width: 100%; + height: 196px; + + width: 100%; + object-fit: cover; + filter: grayscale(1); + + @media screen and (max-width: 768px) { + display: block; + } +`; + +return ( + + + + The decentralized <br /> + <span style={{ color: "#101820" }}>home base</span> <br /> + for NEAR builders + + + + + + + +); diff --git a/instances/events-committee.near/widget/devhub/components/island/connect.jsx b/instances/events-committee.near/widget/devhub/components/island/connect.jsx new file mode 100644 index 000000000..d1ff30a58 --- /dev/null +++ b/instances/events-committee.near/widget/devhub/components/island/connect.jsx @@ -0,0 +1,332 @@ +const { getFeaturedCommunities } = VM.require( + "${REPL_DEVHUB}/widget/core.adapter.devhub-contract" +); + +if (!getFeaturedCommunities) { + return

Loading modules...

; +} + +const communities = getFeaturedCommunities(); + +if (!communities) { + return

Loading communities...

; +} + +const [startIndex, setStartIndex] = useState(0); +const [endIndex, setEndIndex] = useState(2); + +const DescriptionHeader = styled.h2` + color: #f4f4f4; + font-size: 1.75rem; + font-style: normal; + font-weight: 700; + line-height: 120%; /* 43.2px */ + + @media screen and (max-width: 786px) { + font-size: 1.5rem; + } +`; + +const Description = styled.p` + color: #f4f4f4; + text-shadow: 0px 4px 4px rgba(0, 0, 0, 0.25); + font-size: 1.25rem; + font-style: normal; + font-weight: 400; + line-height: 120%; /* 28.8px */ + + @media screen and (max-width: 786px) { + font-size: 1rem; + } +`; + +const imageSource = + "https://ipfs.near.social/ipfs/bafkreic7wxhocbnxoo63uh6n2ur3otykbzouymobt3ebgd2b4dmdiu3764"; + +const CardBody = styled.div` + border-radius: 1rem; + border: 1px solid #00ec97; + background: #3f4040; + + display: flex; + max-width: 31.5%; + height: 12rem; + padding: 1.5rem; + flex-direction: column; + justify-content: center; + align-items: flex-start; + gap: 0.5rem; + flex-shrink: 0; + align-self: stretch; + + h3 { + color: #00ec97; + font-size: 1.5rem; + font-style: normal; + font-weight: 700; + line-height: 100%; /* 40px */ + } + + p { + color: #818181; + font-size: 1.125rem; + font-style: normal; + font-weight: 400; + line-height: 120%; /* 28.8px */ + } + + a { + color: #00ec97; + font-size: 1.25rem; + font-style: normal; + font-weight: 700; + line-height: 120%; /* 28.8px */ + + &:hover { + text-decoration: none; + } + } + + @media screen and (max-width: 768px) { + max-width: 80%; + } +`; + +const Card = ({ title, description, href }) => { + return ( + +

{title}

+

{description}

+ Learn more → +
+ ); +}; + +const Cards = communities.map((com) => { + return { + title: com.name, + description: com.description, + href: "/${REPL_DEVHUB}/widget/app?page=community&handle=" + com.handle, + }; +}); + +const ForwardButton = styled.button` + all: unset; + position: absolute; + right: 0; + + margin: 1rem; + + &:hover, + &:active { + border: none; + outline: none; + } + + ${endIndex >= Cards.length - 1 && "svg {transform: rotate(180deg);}"} +`; + +const handleForward = () => { + if (endIndex <= Cards.length - 1) { + setStartIndex(endIndex + 1); + setEndIndex(endIndex + 3); + } else { + setStartIndex(0); + setEndIndex(2); + } +}; + +const CTA = styled.a` + color: #00ec97 !important; + font-size: 1.5rem; + font-style: normal; + font-weight: 700; + line-height: 120%; /* 28.8px */ + + @media screen and (max-width: 768px) { + font-size: 1.25rem; + } +`; + +const Subheading = styled.h3` + color: #8a8e93; + font-size: 2.25rem; + font-style: normal; + font-weight: 700; + line-height: 120%; /* 43.2px */ + + padding: 3rem; + padding-top: 0; + + @media screen and (max-width: 786px) { + padding: 1rem; + padding-top: 0; + font-size: 1.5rem; + } +`; + +const Container = styled.div` + width: 100%; + display: flex; + position: relative; + align-items: center; + + @media screen and (max-width: 768px) { + flex-direction: column; + } +`; + +const DescriptionContainer = styled.div` + padding: 3rem; + width: 55%; + + @media screen and (max-width: 768px) { + padding: 1rem; + width: 100%; + } +`; + +const ImageContainer = styled.div` + position: absolute; + top: 25%; + right: 0; + + width: 50%; + height: 65%; + + @media screen and (max-width: 768px) { + position: relative; + padding: 0 1rem; + + height: 225px; + width: 100%; + } +`; + +const Image = styled.img` + width: 100%; + height: 90%; + object-fit: cover; + clip-path: polygon(15% 0, 100% 0%, 100% 100%, 0% 100%); + + @media screen and (max-width: 768px) { + clip-path: none; + } +`; + +const CardsContainer = styled.div` + padding: 3rem; + padding-top: 0; + + position: relative; + display: flex; + flex-direction: row; + gap: 1rem; + width: 100%; + align-items: center; + + @media screen and (max-width: 768px) { + display: none; + } +`; + +const ArrowIcon = () => { + return ( + + + + ); +}; + +const CTAContainer = styled.div` + padding: 3rem; + padding-top: 0; + + @media screen and (max-width: 786px) { + padding: 1rem; + padding-top: 0; + } +`; + +const MobileCards = styled.div` + display: none; + + @media screen and (max-width: 768px) { + display: flex; + padding: 1rem; + flex-direction: row; + overflow-x: auto; + gap: 1rem; + } +`; + +const Content = ( + <> + + + + Communities are the lifeblood of /dev/hub + + + We believe that communities are the foundation of a decentralized + ecosystem. Explore and engage with our diverse range of communities + today. + + + + + + + Featured Communities + + {Cards.slice(startIndex, endIndex + 1).map((card, idx) => ( + + ))} + + + + + + {Cards.map((card, idx) => ( + + ))} + + + + Explore all communities → + + + +); + +return ( + +); diff --git a/instances/events-committee.near/widget/devhub/components/island/explore.jsx b/instances/events-committee.near/widget/devhub/components/island/explore.jsx new file mode 100644 index 000000000..c94fb5b73 --- /dev/null +++ b/instances/events-committee.near/widget/devhub/components/island/explore.jsx @@ -0,0 +1,193 @@ +const Card = styled.div` + display: flex; + max-width: 20rem; + max-height: 17.5rem; + padding: 1.5rem; + flex-direction: column; + justify-content: space-between; + align-items: center; + + background: #fff; + border-radius: 1rem; + + h3 { + color: #555555; //#00ec97; + text-align: center; + font-size: 1.5rem; + font-style: normal; + font-weight: 700; + line-height: 120%; /* 28.8px */ + } + + p { + color: #555; + text-align: center; + font-size: 1rem; + font-style: normal; + font-weight: 400; + line-height: 120%; /* 24px */ + } + + a { + color: #555555; //#00ec97; + text-align: center; + font-size: 1.25rem; + font-style: normal; + font-weight: 700; + line-height: 120%; /* 24px */ + } + + @media screen and (max-width: 768px) { + h3 { + font-size: 1.5rem; + } + + p, + a { + font-size: 1rem; + } + + padding: 1rem; + } +`; + +const SectionCard = ({ title, description, href }) => { + return ( + +

{title}

+

{description}

+ Learn more → +
+ ); +}; + +const Cards = [ + { + title: "217 Validators", + description: + "to ensure security, liveness, and fault tolerance of the network", + href: "https://nearscope.net/", + }, + { + title: "<1s, <1¢", + description: + "Fast and cost-efficient transactions. 1s to update values with minimal fees", + href: "https://nearblocks.io/", + }, + { + title: "Awesome DevEx", + description: + "NEAR lets developers innovate with familiar tools: TypeScript, Rust, Solidity", + href: "https://docs.near.org/", + }, + { + title: "Horizontal Scaling", + description: + "Nightshade ensures maximum performance thanks to its sharded design", + href: "https://docs.near.org/concepts/advanced/papers", + }, +]; + +const CTA = styled.a` + display: flex; + padding: 0.875rem 1rem; + align-items: center; + gap: 0.5rem; + + border-radius: 1rem; + background: #00ec97; + + color: #f4f4f4 !important; + font-size: 1.5rem; + font-style: normal; + font-weight: 700; + line-height: 120%; /* 28.8px */ + letter-spacing: -0.03rem; + + width: max-content; + text-decoration: none; + + &:hover { + text-decoration: none; + background: #555555; + } + + @media screen and (max-width: 768px) { + color: #f4f4f4 !important; + font-size: 1.125rem; + font-style: normal; + font-weight: 700; + line-height: 120%; /* 24px */ + letter-spacing: -0.4px; + + margin-left: auto; + margin-right: auto; + + display: flex; + padding: 14px 16px; + flex-direction: column; + justify-content: center; + align-items: flex-start; + gap: 8px; + + border-radius: 16px; + background: #00ec97; + } +`; + +const Container = styled.div` + display: flex; + flex-direction: column; + gap: 3rem; + + padding: 3rem; + padding-top: 0; + + @media screen and (max-width: 768px) { + padding: 1.5rem; + padding-top: 0; + } +`; + +const CardsContainer = styled.div` + display: flex; + justify-content: space-between; + gap: 1.5rem; + + @media screen and (max-width: 768px) { + display: grid; + grid-template-columns: 1fr 1fr; + /* gap: 2.25rem; */ + } +`; + +const Content = ( + + + {Cards.map((card) => ( + + ))} + + + Explore the Open Web on NEAR → + + +); + +return ( + +); diff --git a/instances/events-committee.near/widget/devhub/components/island/hero.jsx b/instances/events-committee.near/widget/devhub/components/island/hero.jsx new file mode 100644 index 000000000..f8298b794 --- /dev/null +++ b/instances/events-committee.near/widget/devhub/components/island/hero.jsx @@ -0,0 +1,212 @@ +const { href } = VM.require("${REPL_DEVHUB}/widget/core.lib.url"); + +href || (href = () => {}); + +const imageLink = + "https://ipfs.near.social/ipfs/bafybeiap2mzwsly4apaldxguiunx4rjwqyadksj5yxuzwrww3kue3ao5qe"; + +const HeroSection = styled.div` + position: relative; + height: auto; + z-index: 3; + width: 70%; + background: #00ec97; + clip-path: polygon(0 0, 100% 0%, 75% 100%, 0% 100%); + + padding-top: 2rem; + padding-bottom: 2rem; + padding-left: 3.375rem; + + @media screen and (max-width: 768px) { + width: 100%; + padding: 1rem 1.5rem; + clip-path: none; + } +`; + +const Title = styled.h1` + color: #f4f4f4; + font-size: 4rem; + font-style: normal; + font-weight: 700; + line-height: 100%; /* 88px */ + letter-spacing: -1.76px; + + @media screen and (max-width: 768px) { + font-size: 2.25rem; + letter-spacing: -0.72px; + margin: 0; + } +`; + +const Lead = styled.p` + color: #151515; + font-size: 1.75rem; + font-style: normal; + font-weight: 400; + line-height: 110%; /* 39.6px */ + + width: 70%; + + @media screen and (max-width: 768px) { + font-size: 1.5rem; + width: 100%; + } +`; + +const CTA = styled.a` + display: inline-flex; + padding: 0.875rem 1rem; + align-items: center; + gap: 0.5rem; + + border-radius: 1rem; + border: 1px solid #151515; + + color: #151515 !important; + font-size: 1.25rem; + font-style: normal; + font-weight: 700; + line-height: 120%; /* 28.8px */ + letter-spacing: -0.48px; + + &:hover { + background: #151515; + color: #f4f4f4 !important; + text-decoration: none; // Remove underline on hover + } + + @media screen and (max-width: 768px) { + display: inline-flex; + padding: 8px 16px; + align-items: center; + gap: 8px; + + border-radius: 16px; + background: #00ec97; + + border: none; + + color: #f4f4f4 !important; + font-size: 16px; + font-style: normal; + font-weight: 700; + line-height: 120%; /* 19.2px */ + letter-spacing: -0.32px; + + &:hover { + background: #151515; + color: #f4f4f4; + text-decoration: none; // Remove underline on hover + } + } +`; + +const Container = styled.div` + position: relative; + width: 100%; + height: max-content; + overflow: hidden; + + @media screen and (max-width: 768px) { + background: #f4f4f4; + } +`; + +const ImageContainer = styled.div` + width: 100%; + height: 100%; + position: absolute; + top: 0; + right: 0; + z-index: 1; + background: transparent; + + @media screen and (max-width: 768px) { + display: none; + } +`; + +const Image = styled.img` + margin-left: 15.625rem; + height: 100%; + width: 100%; + filter: grayscale(100%); + object-fit: cover; +`; + +const DesktopDescription = styled.div` + @media screen and (max-width: 786px) { + display: none; + } +`; + +const MobileImage = styled.img` + display: none; + + width: 100%; + height: 196px; + + width: 100%; + object-fit: cover; + filter: grayscale(1); + + @media screen and (max-width: 768px) { + display: block; + } +`; + +const MobileDescription = styled.div` + display: none; + padding: 24px 16px; + + width: 100%; + + @media screen and (max-width: 768px) { + display: block; + } +`; + +return ( + + + + The decentralized <br /> + <span style={{ color: "#101820" }}>home base</span> <br /> + for NEAR builders + + + + Join a vibrant community of innovators shaping the open web. + + + Read more → + + + + + + + + + Join a vibrant community of innovators shaping the open web. + + Read more → + + + +); diff --git a/instances/events-committee.near/widget/devhub/components/island/home-section.jsx b/instances/events-committee.near/widget/devhub/components/island/home-section.jsx new file mode 100644 index 000000000..240f40f3e --- /dev/null +++ b/instances/events-committee.near/widget/devhub/components/island/home-section.jsx @@ -0,0 +1,63 @@ +const title = props.title; +const titleColor = props.titleColor; +const description = props.description; +const children = props.children; +const background = props.background; + +const Section = styled.div` + ${background && "background: #292929;"} + ${background && "color: #F4F4F4;"} + width: 100%; +`; + +const SectionHeader = styled.h2` + color: ${titleColor || "#00ec97"}; + font-size: 1.5rem; + font-style: normal; + font-weight: 400; + line-height: 120%; /* 28.8px */ + letter-spacing: -0.24px; + + @media screen and (max-width: 768px) { + font-size: 1.25rem; + } +`; + +const SectionDescription = styled.h3` + color: #151515; + font-size: 2.25rem; + font-style: normal; + font-weight: 700; + line-height: 110%; /* 39.6px */ + letter-spacing: -0.72px; + + margin-bottom: 2.25rem; + margin-top: 2.25rem; + + max-width: 40rem; + + @media screen and (max-width: 768px) { + margin-bottom: 1.5rem; + margin-top: 1.5rem; + font-size: 1.5rem; + } +`; + +const Container = styled.div` + padding: 3rem; + padding-bottom: 0; + + @media screen and (max-width: 768px) { + padding: 1rem; + } +`; + +return ( +
+ + {title} + {description && {description}} + + {children} +
+); diff --git a/instances/events-committee.near/widget/devhub/components/island/participate.jsx b/instances/events-committee.near/widget/devhub/components/island/participate.jsx new file mode 100644 index 000000000..53cecb78e --- /dev/null +++ b/instances/events-committee.near/widget/devhub/components/island/participate.jsx @@ -0,0 +1,207 @@ +const { href } = VM.require("${REPL_DEVHUB}/widget/core.lib.url"); + +href || (href = () => {}); + +const Container = styled.div` + margin-top: 2.25rem; + + display: flex; + flex-direction: row; + justify-content: space-between; + width: 100%; + + @media screen and (max-width: 768px) { + flex-direction: column; + margin-top: 0; + } +`; + +const LinkItem = styled.a` + color: #00ec97; + font-size: 1.25rem; + font-style: normal; + font-weight: 400; + line-height: 120%; /* 28.8px */ + + display: flex; + align-items: center; + + &:hover { + text-decoration: none; + color: #096d50; + } +`; + +const Links = [ + { + links: [ + { + title: "Ideate on DevHub", + href: "/devhub.near/widget/app?page=blog&id=2029", + count: 1, + }, + { + title: "Post a Proposal", + href: "/devhub.near/widget/app?page=blog&id=2035", + count: 2, + }, + { + title: "Host an Event", + href: "/devhub.near/widget/app?page=community&handle=hacks&tab=wiki-202", + count: 3, + }, + ], + }, + { + links: [ + { + title: "Improve NEAR Docs", + href: "https://github.com/near/docs", + count: 4, + }, + { + title: "Join the Fellowship", + href: "/devhub.near/widget/app?page=community&handle=fellowship&tab=wiki-201", + count: 5, + }, + { + title: "Join NEAR Campus", + href: "/devhub.near/widget/app?page=community&handle=near-campus", + count: 6, + }, + ], + }, + { + links: [ + { + title: "Dive into Hackbox", + href: "/hackbox.near/widget/home", + count: 7, + }, + ], + }, +]; + +const CTA = styled.a` + display: flex; + padding: 0.875rem 1rem; + align-items: center; + gap: 0.5rem; + + border-radius: 1rem; + background: #00ec97; + + color: #f4f4f4 !important; + font-size: 1.5rem; + font-style: normal; + font-weight: 700; + line-height: 120%; /* 28.8px */ + letter-spacing: -0.03rem; + + width: max-content; + margin-top: 1.5rem; + + &:hover { + background: #555555; + text-decoration: none !important; + } + + @media screen and (max-width: 768px) { + color: #f4f4f4 !important; + font-size: 20px; + font-style: normal; + font-weight: 700; + line-height: 120%; /* 24px */ + + display: flex; + padding: 14px 16px; + align-items: center; + gap: 8px; + + border-radius: 16px; + background: #555555; + + &:hover { + //background: #555555; + text-decoration: none; + } + } +`; + +const SectionPadding = styled.div` + padding: 3rem; + padding-top: 0; + + display: flex; + flex-direction: column; + + @media screen and (max-width: 768px) { + padding: 1rem; + padding-top: 0; + } +`; + +const LinksContainer = styled.div` + width: 30%; + + @media screen and (max-width: 768px) { + width: 100%; + margin-bottom: 1rem; + } +`; + +const Content = ( + + + {Links.map((it) => ( + +
+ {it.links.map((link) => ( +
+ + {link.count} + {" "} + + {link.title} + +
+ ))} +
+
+ ))} +
+ + + Learn more → + +
+); + +return ( + +); diff --git a/instances/events-committee.near/widget/devhub/components/island/support.jsx b/instances/events-committee.near/widget/devhub/components/island/support.jsx new file mode 100644 index 000000000..7a5e66fb5 --- /dev/null +++ b/instances/events-committee.near/widget/devhub/components/island/support.jsx @@ -0,0 +1,134 @@ +const Items = [ + { + heading: ( + <> + Developer +
+ Resources + + ), + description: "Learn the fundamentals of NEAR and build with confidence", + cta: { + href: "https://docs.near.org", + title: "Read ↗", + }, + }, + { + heading: <>Office Hours, + description: ( + <> + Need some help? +
DevRel contributors are available to answer your technical + questions + + ), + cta: { + href: "/devhub.near/widget/app?page=community&handle=devrel&tab=office-hours-support", + title: "Join ↗", + }, + }, + { + heading: <>Get Funding, + description: + "Explore funding opportunities from DevHub to fuel your vision", + cta: { + href: "/devhub.near/widget/app?page=community&handle=developer-dao&tab=funding", + title: "Learn more ↗", + }, + }, +]; + +const Circle = styled.div` + display: flex; + width: 18.75rem; + height: 18.75rem; + padding: 2.25rem; + flex-direction: column; + justify-content: center; + align-items: center; + gap: 1rem; + flex-shrink: 0; + + border-radius: 22.5rem; + border: 1px solid #00ec97; + background: #f4f4f4; + + h3 { + color: #101820; + text-align: center; + font-size: 1.75rem; + font-style: normal; + font-weight: 700; + line-height: 100%; /* 36px */ + } + + p { + color: #101820; + text-align: center; + font-size: 1.125rem; + font-style: normal; + font-weight: 400; + line-height: 120%; /* 28.8px */ + letter-spacing: -0.72px; + } + + a { + color: #555555; + font-size: 1.125rem; + font-style: normal; + font-weight: 700; + line-height: 120%; /* 28.8px */ + } +`; + +const Container = styled.div` + padding: 3rem; + padding-top: 0; + margin-top: 1.5rem; + + @media screen and (max-width: 786px) { + padding: 1.5rem; + padding-top: 0; + } +`; + +const ItemsContainer = styled.div` + display: flex; + flex-direction: row; + justify-content: center; + align-items: center; + width: 100%; + + flex-wrap: wrap; + gap: 3rem; + + @media screen and (max-width: 768px) { + flex-direction: column; + gap: 1rem; + } +`; + +const Content = ( + + + {Items.map((it) => ( + +

{it.heading}

+

{it.description}

+ {it.cta.title} +
+ ))} +
+
+); + +return ( + +); diff --git a/instances/events-committee.near/widget/devhub/components/layout/LikeButton/Faces.jsx b/instances/events-committee.near/widget/devhub/components/layout/LikeButton/Faces.jsx new file mode 100644 index 000000000..694463cbd --- /dev/null +++ b/instances/events-committee.near/widget/devhub/components/layout/LikeButton/Faces.jsx @@ -0,0 +1,122 @@ +const accountId = context.accountId; +const likesByUsers = props.likesByUsers || {}; +const limit = props.limit ?? 3; + +let likes = Object.keys(likesByUsers).reverse(); + +const graphLikes = []; +const nonGraph = []; + +const graph = + (accountId && + Social.keys(`${accountId}/graph/follow/*`, "final")[accountId].graph + .follow) || + {}; + +likes.forEach((accountId) => { + if (accountId in graph) { + graphLikes.push(accountId); + } else { + nonGraph.push(accountId); + } +}); + +let faces = [...graphLikes, ...nonGraph]; + +if (faces.length < limit + 3) { + limit = faces.length; +} + +const renderFaces = faces.splice(0, limit); + +const Faces = styled.span` + .face { + display: inline-block; + position: relative; + margin: -0.1em; + height: 1.5em; + width: 1.5em; + min-width: 1.5em; + vertical-align: top; + img { + object-fit: cover; + border-radius: 50%; + width: 100%; + height: 100%; + } + } +`; + +const Others = styled.span` + &:hover { + color: white !important; + } +`; + +const numLikes = likes.length - limit; + +return ( + <> + + {renderFaces.map((accountId, i) => ( + + + ), + }} + /> + + ))} + + {numLikes > 0 ? ( + +
+ {faces.slice(0, 10).map((accountId, i) => ( + + +
+
+ ))} + {faces.length > 10 ? "..." : ""} +
+ + } + > + + and {numLikes} other{numLikes === 1 ? "" : "s"} + +
+ ) : ( + "" + )} + +); diff --git a/instances/events-committee.near/widget/devhub/components/molecule/AccountAutocomplete.jsx b/instances/events-committee.near/widget/devhub/components/molecule/AccountAutocomplete.jsx new file mode 100644 index 000000000..725fa4854 --- /dev/null +++ b/instances/events-committee.near/widget/devhub/components/molecule/AccountAutocomplete.jsx @@ -0,0 +1,147 @@ +if (!context.accountId || !props.term) return <>; + +let results = []; +const filterAccounts = props.filterAccounts ?? []; // hide certain accounts from the list +const profilesData = Social.get("*/profile/name", "final") || {}; +const followingData = Social.get( + `${context.accountId}/graph/follow/**`, + "final" +); +if (!profilesData) return <>; +const profiles = Object.entries(profilesData); +const term = (props.term || "").replace(/\W/g, "").toLowerCase(); +const limit = 5; + +for (let i = 0; i < profiles.length; i++) { + let score = 0; + const accountId = profiles[i][0]; + const accountIdSearch = profiles[i][0].replace(/\W/g, "").toLowerCase(); + const nameSearch = (profiles[i][1]?.profile?.name || "") + .replace(/\W/g, "") + .toLowerCase(); + const accountIdSearchIndex = accountIdSearch.indexOf(term); + const nameSearchIndex = nameSearch.indexOf(term); + + if (accountIdSearchIndex > -1 || nameSearchIndex > -1) { + score += 10; + + if (accountIdSearchIndex === 0) { + score += 10; + } + if (nameSearchIndex === 0) { + score += 10; + } + if (followingData[accountId] === "") { + score += 30; + } + + results.push({ + accountId, + score, + }); + } +} + +results.sort((a, b) => b.score - a.score); +results = results.slice(0, limit); +if (filterAccounts?.length > 0) { + results = results.filter((item) => !filterAccounts?.includes(item.accountId)); +} + +function onResultClick(id) { + props.onSelect && props.onSelect(id); +} + +const Wrapper = styled.div` + position: relative; + background: #eceef0; + + &::before { + content: ""; + display: block; + position: absolute; + right: 0; + width: 6px; + height: 100%; + background: linear-gradient( + to left, + rgba(236, 238, 240, 1), + rgba(236, 238, 240, 0) + ); + z-index: 10; + } +`; + +const Scroller = styled.div` + position: relative; + display: flex; + padding: 6px; + gap: 6px; + overflow: auto; + scroll-behavior: smooth; + align-items: center; + scrollbar-width: none; + -ms-overflow-style: none; + &::-webkit-scrollbar { + display: none; + } + + > * { + max-width: 175px; + flex-grow: 0; + flex-shrink: 0; + + button { + border: 1px solid #eceef0; + background: #fff !important; + border-radius: 6px; + padding: 3px 6px; + transition: all 200ms; + + &:focus, + &:hover { + border-color: #687076; + } + } + } +`; + +const CloseButton = styled.button` + background: none; + border: none; + display: block; + padding: 12px; + color #687076; + transition: all 200ms; + + &:hover { + color: #000; + } +`; + +if (results.length === 0) return <>; + +return ( + + + + + + + {results.map((result) => { + return ( + + ); + })} + + +); diff --git a/instances/events-committee.near/widget/devhub/components/molecule/BadgeDetails.jsx b/instances/events-committee.near/widget/devhub/components/molecule/BadgeDetails.jsx new file mode 100644 index 000000000..c3a8c2d27 --- /dev/null +++ b/instances/events-committee.near/widget/devhub/components/molecule/BadgeDetails.jsx @@ -0,0 +1,78 @@ +const nearDevGovBadgesContractId = "neardevgov.near"; + +let badgeId, ownerAccountId; +if (props.tokenId) { + let [_badgeId, _ownerAccountId] = props.tokenId.split(":", 2); + badgeId = _badgeId; + ownerAccountId = _ownerAccountId; +} else { + badgeId = props.badgeId; +} + +if (!badgeId) { + return ( + <> + Please, provide
badgeId
or
tokenId
to the + DevGovBadgeDetails component + + ); +} + +let badgeMetadata = + props.badgeMetadata ?? + Near.view(nearDevGovBadgesContractId, "get_badge", { + badge_id: badgeId, + }).badge_metadata; + +if (!badgeMetadata) { + return <>Loading...; +} + +return ( + <> +
+
+
+ +
+
+
+
+
+
+
+

+ {badgeMetadata.title} +

+
+
+
+
+ Awarded to + {badgeMetadata.copies} + developers +
+
+
+ +
+
+
+
+
+ +); diff --git a/instances/events-committee.near/widget/devhub/components/molecule/BadgesList.jsx b/instances/events-committee.near/widget/devhub/components/molecule/BadgesList.jsx new file mode 100644 index 000000000..be2875ce2 --- /dev/null +++ b/instances/events-committee.near/widget/devhub/components/molecule/BadgesList.jsx @@ -0,0 +1,63 @@ +const { href } = VM.require("${REPL_DEVHUB}/widget/core.lib.url") || (() => {}); + +const nearDevGovBadgesContractId = "neardevgov.near"; + +let badges = props.badges; +const mode = props.mode || "normal"; + +if (!badges) { + const accountId = props.accountId || context.accountId; + const ownedBadges = Near.view( + nearDevGovBadgesContractId, + "nft_tokens_for_owner", + { + account_id: accountId, + } + ); + if (!ownedBadges) { + return <>{mode === "compact" ? "" : "Loading..."}; + } + badges = ownedBadges; +} + +let style; +if (mode === "normal") { + style = { width: "3em", height: "3em" }; +} else if (mode === "compact") { + style = { width: "1.5em", height: "1.5em" }; +} +if (props.style) { + style = props.style; +} + +const renderedBadgesList = badges.map(({ token_id: tokenId, metadata }) => ( + + + {mode === "compact" ? null : metadata.title} + +)); + +if (mode === "compact") { + return <>{renderedBadgesList}; +} else { + return ( +
    + {renderedBadgesList.map((renderedBadge) => ( +
  • {renderedBadge}
  • + ))} +
+ ); +} diff --git a/instances/events-committee.near/widget/devhub/components/molecule/BlogControl.jsx b/instances/events-committee.near/widget/devhub/components/molecule/BlogControl.jsx new file mode 100644 index 000000000..9a1e57a4e --- /dev/null +++ b/instances/events-committee.near/widget/devhub/components/molecule/BlogControl.jsx @@ -0,0 +1,16 @@ +const { title, onClick } = props; + +return ( + +); diff --git a/instances/events-committee.near/widget/devhub/components/molecule/Button.jsx b/instances/events-committee.near/widget/devhub/components/molecule/Button.jsx new file mode 100644 index 000000000..c7cdfae46 --- /dev/null +++ b/instances/events-committee.near/widget/devhub/components/molecule/Button.jsx @@ -0,0 +1,168 @@ +const styles = ` + padding: 0.5rem 1.2rem !important; + min-height: 36px; + line-height: 1.5; + text-decoration: none !important; + + .disabled { + pointer-events: none; + } + + &:not(.shadow-none) { + box-shadow: 0 0.125rem 0.25rem rgba(0, 0, 0, 0.075) !important; + transition: box-shadow 0.6s; + } + + &.btn-sm { + padding: 0.5rem 0.8rem !important; + min-height: 32px; + line-height: 1; + } + + &.btn-lg { + padding: 1rem 1.5rem !important; + min-height: 48px; + } + + &.btn-primary { + border: none; + --bs-btn-color: #ffffff; + --bs-btn-bg: #087990; + --bs-btn-border-color: #087990; + --bs-btn-hover-color: #ffffff; + --bs-btn-hover-bg: #055160; + --bs-btn-hover-border-color: #055160; + --bs-btn-focus-shadow-rgb: 49, 132, 253; + --bs-btn-active-color: #ffffff; + --bs-btn-active-bg: #055160; + --bs-btn-active-border-color: #055160; + --bs-btn-active-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125); + --bs-btn-disabled-color: #ffffff; + --bs-btn-disabled-bg: #0551604a; + } + + &.btn-outline-primary { + --bs-btn-color: #087990; + --bs-btn-border-color: #087990; + --bs-btn-hover-color: #ffffff; + --bs-btn-hover-bg: #087990; + --bs-btn-hover-border-color: #087990; + --bs-btn-focus-shadow-rgb: 49, 132, 253; + --bs-btn-active-color: #ffffff; + --bs-btn-active-bg: #087990; + --bs-btn-active-border-color: #087990; + --bs-btn-active-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125); + --bs-btn-disabled-border-color: #0551604a; + } + + &[class*="btn-outline-"] { + border-width: 2px; + } + + &.btn-outline-primary { + --bs-btn-disabled-color: #6c757d8f; + } + + &.btn-secondary { + border: none; + } + + &.btn-outline-secondary { + --bs-btn-disabled-color: #6c757d8f; + } + + &.btn-success { + border: none; + --bs-btn-disabled-bg: #35482a4a; + } + + &.btn-outline-success { + --bs-btn-disabled-color: #6c757d8f; + } + + &.btn-danger { + border: none; + } + + &.btn-outline-danger { + --bs-btn-disabled-color: #6c757d8f; + } + + &.btn-warning { + border: none; + } + + &.btn-outline-warning { + --bs-btn-disabled-color: #6c757d8f; + } + + &.btn-info { + border: none; + } + + &.btn-outline-info { + --bs-btn-disabled-color: #6c757d8f; + } +`; + +const LoadingButtonSpinner = ( + +); + +const rootElementByType = (type) => + type === "link" + ? styled.a` + ${styles} + ` + : styled.button` + ${styles} + `; + +const Button = ({ + classNames, + icon: iconProps, + label, + type, + isHidden, + notRounded, + loading, + disabled, + ...restProps +}) => { + const ButtonRoot = rootElementByType(type); + + return ( + + {iconProps !== null && + !props.loading && + typeof iconProps === "object" && + !Array.isArray(iconProps) && ( + + )} + {props.loading ? LoadingButtonSpinner : null} + + {label} + + + ); +}; + +return Button(props); diff --git a/instances/events-committee.near/widget/devhub/components/molecule/Checkbox.jsx b/instances/events-committee.near/widget/devhub/components/molecule/Checkbox.jsx new file mode 100644 index 000000000..236fc94de --- /dev/null +++ b/instances/events-committee.near/widget/devhub/components/molecule/Checkbox.jsx @@ -0,0 +1,28 @@ +const CheckBox = ({ value, isChecked, label, onClick }) => { + const [checked, setChecked] = useState(isChecked); + + useEffect(() => { + if (isChecked !== checked) { + setChecked(isChecked); + } + }, [isChecked]); + + useEffect(() => { + onClick(checked); + }, [checked]); + + return ( +
+ setChecked(e.target.checked)} + /> + +
+ ); +}; + +return CheckBox(props); diff --git a/instances/events-committee.near/widget/devhub/components/molecule/CommunityControl.jsx b/instances/events-committee.near/widget/devhub/components/molecule/CommunityControl.jsx new file mode 100644 index 000000000..97c02bae2 --- /dev/null +++ b/instances/events-committee.near/widget/devhub/components/molecule/CommunityControl.jsx @@ -0,0 +1,51 @@ +const { className, title, icon, href, onClick } = props; + +const Button = styled.button` + display: flex; + align-items: center; + gap: 8px; + + border-radius: 4px; + background: #04a46e; + + color: #f4f4f4; + font-size: 16px; + font-style: normal; + font-weight: 700; + line-height: 20px; /* 125% */ + + padding: 0.5rem 1rem; + + &:hover { + background: #555555; + text-decoration: none !important; + } + + outline: none; + border: none; +`; + +return ( +
+ {props.href ? ( + + + + ) : ( + + )} +
+); diff --git a/instances/events-committee.near/widget/devhub/components/molecule/Compose.jsx b/instances/events-committee.near/widget/devhub/components/molecule/Compose.jsx new file mode 100644 index 000000000..ebd9452a2 --- /dev/null +++ b/instances/events-committee.near/widget/devhub/components/molecule/Compose.jsx @@ -0,0 +1,120 @@ +const EmbeddCSS = ` + .CodeMirror { + margin-inline:10px; + border-radius:5px; + } + + .editor-toolbar { + border: none !important; + } +`; + +const Wrapper = styled.div` + .nav-link { + color: inherit !important; + } + + .card-header { + padding-bottom: 0px !important; + } +`; + +const Compose = ({ + data, + onChange, + autocompleteEnabled, + placeholder, + height, + embeddCSS, + showProposalIdAutoComplete, + onChangeKeyup, + handler, +}) => { + State.init({ + data: data, + selectedTab: "editor", + autoFocus: false, + }); + + useEffect(() => { + if (typeof onChange === "function") { + onChange(state.data); + } + }, [state.data]); + + useEffect(() => { + // for clearing editor after txn approval/ showing draft state + if (data !== state.data || handler !== state.handler) { + State.update({ data: data, handler: handler }); + } + }, [data, handler]); + + return ( + +
+
+
+ +
+
+ + {state.selectedTab === "editor" ? ( + <> + { + State.update({ data: content, handler: "update" }); + }, + placeholder: placeholder, + height, + embeddCSS: embeddCSS || EmbeddCSS, + showAutoComplete: autocompleteEnabled, + showProposalIdAutoComplete: showProposalIdAutoComplete, + autoFocus: state.autoFocus, + onChangeKeyup: onChangeKeyup, + }} + /> + + ) : ( +
+ +
+ )} +
+
+ ); +}; + +return Compose(props); diff --git a/instances/events-committee.near/widget/devhub/components/molecule/DropDown.jsx b/instances/events-committee.near/widget/devhub/components/molecule/DropDown.jsx new file mode 100644 index 000000000..984ace45d --- /dev/null +++ b/instances/events-committee.near/widget/devhub/components/molecule/DropDown.jsx @@ -0,0 +1,60 @@ +const options = props.options; // [{label:"",value:""}] +const label = props.label; +const onUpdate = props.onUpdate ?? (() => {}); +const selectedValue = props.selectedValue; +const [selected, setSelected] = useState(selectedValue); + +const StyledDropdown = styled.div` + .drop-btn { + width: 100%; + max-width: 200px; + text-align: left; + padding-inline: 10px; + } + + .dropdown-item.active, + .dropdown-item:active { + background-color: #f0f0f0 !important; + color: black; + } + + .cursor-pointer { + cursor: pointer; + } +`; + +useEffect(() => { + onUpdate(selected); +}, [selected]); + +return ( +
+ +
+); diff --git a/instances/events-committee.near/widget/devhub/components/molecule/DropDownWithSearch.jsx b/instances/events-committee.near/widget/devhub/components/molecule/DropDownWithSearch.jsx new file mode 100644 index 000000000..5a558656f --- /dev/null +++ b/instances/events-committee.near/widget/devhub/components/molecule/DropDownWithSearch.jsx @@ -0,0 +1,176 @@ +const { + selectedValue, + onChange, + options, + defaultLabel, + showSearch, + searchInputPlaceholder, + searchByLabel, + searchByValue, + onSearch, +} = props; + +const [searchTerm, setSearchTerm] = useState(""); +const [filteredOptions, setFilteredOptions] = useState(options); +const [isOpen, setIsOpen] = useState(false); +const [selectedOption, setSelectedOption] = useState({ + label: + options?.find((item) => item.value === selectedValue)?.label ?? + defaultLabel, + value: defaultLabel, +}); + +useEffect(() => { + if (selectedOption.value !== selectedValue) { + setSelectedOption({ + label: + options?.find((item) => item.value === selectedValue)?.label ?? + defaultLabel, + value: defaultLabel, + }); + } +}, [selectedValue]); + +useEffect(() => { + setFilteredOptions(options); +}, [options]); + +const handleSearch = (event) => { + const term = event.target.value.toLowerCase(); + setSearchTerm(term); + if (typeof onSearch === "function") { + onSearch(term); + return; + } + + const filteredOptions = options.filter((option) => { + if (searchByLabel) { + return option.label.toLowerCase().includes(term); + } + if (searchByValue) { + return option.value.toString().toLowerCase().includes(term); + } + }); + + setFilteredOptions(filteredOptions); +}; + +const toggleDropdown = () => { + setIsOpen(!isOpen); +}; + +const handleOptionClick = (option) => { + setSelectedOption(option); + setIsOpen(false); + onChange(option); +}; + +const Container = styled.div` + .drop-btn { + width: 100%; + text-align: left; + padding-inline: 10px; + } + + .dropdown-toggle:after { + position: absolute; + top: 46%; + right: 5%; + } + + .dropdown-menu { + width: 100%; + } + + .dropdown-item.active, + .dropdown-item:active { + background-color: #f0f0f0 !important; + color: black; + } + + .custom-select { + position: relative; + } + + .scroll-box { + max-height: 200px; + overflow-y: scroll; + } + + .selected { + background-color: #f0f0f0; + } + + input { + background-color: #f8f9fa; + } + + .cursor-pointer { + cursor: pointer; + } + + .text-wrap { + overflow: hidden; + white-space: normal; + } +`; +let searchFocused = false; +return ( + +
{ + setTimeout(() => { + setIsOpen(searchFocused || false); + }, 0); + }} + > +
+
+ {selectedOption.label} +
+
+ + {isOpen && ( +
+ {showSearch && ( + { + searchFocused = true; + }} + onBlur={() => { + setTimeout(() => { + searchFocused = false; + }, 0); + }} + /> + )} +
+ {filteredOptions.map((option) => ( +
handleOptionClick(option)} + > + {option.label} +
+ ))} +
+
+ )} +
+
+); diff --git a/instances/events-committee.near/widget/devhub/components/molecule/Input.jsx b/instances/events-committee.near/widget/devhub/components/molecule/Input.jsx new file mode 100644 index 000000000..2db3b5b3b --- /dev/null +++ b/instances/events-committee.near/widget/devhub/components/molecule/Input.jsx @@ -0,0 +1,227 @@ +const TextInput = ({ + className, + format, + inputProps: { className: inputClassName, ...inputProps }, + key, + label, + multiline, + onChange, + debounceTimeout, + placeholder, + type, + value, + skipPaddingGap, + style, + error, + ...otherProps +}) => { + onChange = typeof onChange === "function" ? onChange : () => {}; + State.init({ + data: value, + error: error, + }); + + function convertToString(value) { + return typeof value === "number" ? value.toFixed() : value ?? ""; + } + + useEffect(() => { + const inputError = ""; + if (value !== state.data) { + const isNumeric = inputProps.inputmode === "numeric"; + let inputValue = state.data; + // check for input number error (since type: number doesn't work on firefox/safari) + if (isNumeric) { + if (!inputValue) { + return; + } + inputValue = convertToString(state.data).replace(/,/g, ""); + let isValidInteger = /^[1-9][0-9]*$/.test(inputValue); + if (!isValidInteger) { + inputError = "Please enter the nearest positive whole number."; + } + + State.update({ error: inputError }); + } + const handler = setTimeout(() => { + onChange({ target: { value: inputValue }, error: inputError }); + }, debounceTimeout || 30); + + return () => { + clearTimeout(handler); + }; + } + }, [state.data]); + + useEffect(() => { + if (value !== state.data) { + State.update({ data: value }); + } + }, [value]); + + useEffect(() => { + if (error !== state.error) { + State.update({ error: error }); + } + }, [error]); + + const typeAttribute = + type === "text" || + type === "password" || + type === "number" || + type === "date" + ? type + : "text"; + + const isValid = () => { + if (!state.data || state.data.length === 0) { + return !inputProps.required; + } else if (inputProps.min && inputProps.min > state.data?.length) { + return false; + } else if (inputProps.max && inputProps.max < state.data?.length) { + return false; + } else if ( + inputProps.allowCommaAndSpace === false && + /^[^,\s]*$/.test(state.data) === false + ) { + return false; + } else if ( + inputProps.validUrl === true && + /^(www\.)?[-a-zA-Z0-9@:%._\+~#=]{2,256}\.[a-z]{2,6}\b([-a-zA-Z0-9@:%_\+.~#?&//=]*)$/.test( + state.data + ) === false + ) { + return false; + } + return true; + }; + + const renderedLabels = [ + (label?.length ?? 0) > 0 ? ( + + {label} + + {inputProps.required ? * : null} + + ) : null, + + format === "markdown" ? ( + + ) : null, + + format === "comma-separated" ? ( + + {format} + + ) : null, + + (inputProps.max ?? null) !== null ? ( + {`${state.data?.length ?? 0} / ${inputProps.max}`} + ) : null, + ].filter((label) => label !== null); + + const onKeyDown = props.onKeyDown ?? (() => {}); + + const getFormattedData = useCallback(() => { + if (inputProps.inputmode === "numeric") { + const number = parseFloat(convertToString(state.data).replace(/,/g, "")); + if (!isNaN(number)) { + // Format the number for display + return number.toLocaleString("en-US"); + } + } + return state.data; + }, [state.data]); + + return ( +
+ {renderedLabels.length > 0 ? ( + + {renderedLabels.map((label) => label)} + + ) : null} + + {!multiline ? ( +
+
+ {inputProps.prefix && ( + + {inputProps.prefix} + + )} + State.update({ data: e.target.value })} + onBlur={(e) => { + if (typeof onBlur === "function") { + onBlur({ target: { value: e.target.value } }); + } + }} + onKeyDown={onKeyDown} + {...{ placeholder, ...inputProps }} + /> +
+ {state.error && ( +
+ {state.error} +
+ )} +
+ ) : ( + + + + + + + + +`; + +return ( + + + + + + + ); + })} +
+ ); +} diff --git a/instances/events-committee.near/widget/devhub/entity/addon/wiki/Configurator.jsx b/instances/events-committee.near/widget/devhub/entity/addon/wiki/Configurator.jsx new file mode 100644 index 000000000..75bcb4a30 --- /dev/null +++ b/instances/events-committee.near/widget/devhub/entity/addon/wiki/Configurator.jsx @@ -0,0 +1,163 @@ +const { data, onSubmit } = props; + +const initialData = data; +const [content, setContent] = useState(data.content || ""); +const [title, setTitle] = useState(data.title || ""); +const [subtitle, setSubtitle] = useState(data.subtitle || ""); + +const [textAlign, setTextAlign] = useState(data.textAlign || "left"); + +const Container = styled.div` + width: 100%; + margin: 0 auto; + padding: 20px; + text-align: left; +`; + +const FormContainer = styled.div` + padding-top: 1rem; + + & > *:not(:last-child) { + margin-bottom: 1rem; + } +`; + +const hasDataChanged = () => { + return ( + content !== initialData.content || + title !== initialData.title || + subtitle !== initialData.subtitle || + textAlign !== initialData.textAlign + ); +}; + +const handleSubmit = () => { + if (title) onSubmit({ title, subtitle, content, textAlign }); +}; + +return ( + +
    +
  • + +
  • +
  • + +
  • +
+
+
+
+ setTextAlign(e.target.value), + options: [ + { label: "Left", value: "left" }, + { label: "Center", value: "center" }, + { label: "Right", value: "right" }, + ], + }} + /> +
+ +
+ setTitle(e.target.value), + value: title, + inputProps: { + min: 2, + max: 60, + required: true, + }, + }} + /> +
+
+ setSubtitle(e.target.value), + value: subtitle, + inputProps: { + min: 2, + max: 250, + }, + }} + /> +
+ +
+
+ +
+
+
+
+ +
+
+
+
+); diff --git a/instances/events-committee.near/widget/devhub/entity/addon/wiki/Viewer.jsx b/instances/events-committee.near/widget/devhub/entity/addon/wiki/Viewer.jsx new file mode 100644 index 000000000..bf4774d5c --- /dev/null +++ b/instances/events-committee.near/widget/devhub/entity/addon/wiki/Viewer.jsx @@ -0,0 +1,95 @@ +const { content, title, subtitle, textAlign } = props; + +const Container = styled.div` + display: flex; + flex-direction: column; + width: 100%; + + padding: 0 3rem; + + margin: 0 auto; + text-align: ${(p) => p.textAlign ?? "left"}; + + h1 { + color: #151515; + font-size: 3.5rem; + font-style: normal; + font-weight: 700; + line-height: 100%; /* 88px */ + margin: 1rem 0; + } + + p.subtitle { + color: #555; + font-size: 1.5rem; + font-style: normal; + font-weight: 400; + line-height: 110%; /* 35.2px */ + margin: 0; + } + + @media screen and (max-width: 768px) { + padding: 0 1rem; + + span.category { + font-size: 0.75rem; + } + + h1 { + font-size: 2rem; + } + + p.subtitle { + font-size: 1rem; + } + } + + a { + color: #0000ee; + } +`; + +const Content = styled.div` + margin: 20px 0; + text-align: left; +`; + +const Title = styled.h1` + margin-bottom: 10px; +`; + +const Subtitle = styled.p` + margin-bottom: 20px; +`; + +const CenteredMessage = styled.div` + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + width: 100%; + height: ${(p) => p.height ?? "100%"}; +`; + +if (!content) { + return ( + +

No Wiki Configured

+
+ ); +} else { + return ( + +

{title}

+

{subtitle}

+ + + +
+ ); +} diff --git a/instances/events-committee.near/widget/devhub/entity/community/Activity.jsx b/instances/events-committee.near/widget/devhub/entity/community/Activity.jsx new file mode 100644 index 000000000..ae4885512 --- /dev/null +++ b/instances/events-committee.near/widget/devhub/entity/community/Activity.jsx @@ -0,0 +1,85 @@ +const { handle } = props; + +const { getCommunity } = VM.require( + "${REPL_DEVHUB}/widget/core.adapter.devhub-contract" +); +const { href } = VM.require("${REPL_DEVHUB}/widget/core.lib.url"); + +getCommunity = getCommunity || (() => <>); +href || (href = () => {}); + +if (!handle) { + return

Handle not defined

; +} + +if (!href) { + return

Loading modules...

; +} + +// TODO: Why do we need to get community data again? Isn't the tag the handle... +const communityData = getCommunity({ handle }); + +if (communityData === null) { + return
Loading...
; +} + +const MainContent = styled.div` + flex-grow: 1; + max-width: 75%; + + @media screen and (max-width: 960px) { + max-width: 100%; + } +`; + +const SidebarContainer = styled.div` + max-width: 25%; + margin-right: 1.5rem; + + @media screen and (max-width: 960px) { + display: none; + } +`; + +return ( +
+
+
+ + + ), + recency, + transactionHashes: props.transactionHashes, + }} + /> + + + + +
+
+
+); diff --git a/instances/events-committee.near/widget/devhub/entity/community/Announcements.jsx b/instances/events-committee.near/widget/devhub/entity/community/Announcements.jsx new file mode 100644 index 000000000..77f06eb39 --- /dev/null +++ b/instances/events-committee.near/widget/devhub/entity/community/Announcements.jsx @@ -0,0 +1,210 @@ +const { handle } = props; +const { getCommunity, setCommunitySocialDB } = VM.require( + "${REPL_DEVHUB}/widget/core.adapter.devhub-contract" +); + +getCommunity = getCommunity || (() => <>); +setCommunitySocialDB = setCommunitySocialDB || (() => <>); + +const communityData = getCommunity({ handle }); +const [postsExists, setPostExists] = useState(false); +const [newUnseenPosts, setNewUnseenPosts] = useState([]); +const [lastQueryRequestTimestamp, setLastQueryRequestTimestamp] = useState( + new Date().getTime() +); +const [submittedAnnouncementData, setSubmittedAnnouncementData] = + useState(null); +const communityAccountId = `${handle}.community.${REPL_DEVHUB_CONTRACT}`; + +let checkIndexerInterval; +const onNewUnseenPosts = (newUnseenPosts) => { + if (newUnseenPosts.length > 0) { + clearInterval(checkIndexerInterval); + } +}; + +useEffect(() => { + if (submittedAnnouncementData) { + const checkForAnnouncementInSocialDB = () => { + Near.asyncView("${REPL_SOCIAL_CONTRACT}", "get", { + keys: [`${communityAccountId}/post/**`], + }).then((result) => { + try { + const submittedAnnouncementText = JSON.parse( + submittedAnnouncementData.post.main + ).text; + const lastAnnouncementTextFromSocialDB = JSON.parse( + result[communityAccountId].post.main + ).text; + if (submittedAnnouncementText === lastAnnouncementTextFromSocialDB) { + setSubmittedAnnouncementData(null); + checkIndexerInterval = setInterval(() => { + setLastQueryRequestTimestamp(new Date().getTime()); + }, 500); + return; + } + } catch (e) {} + setTimeout(() => checkForAnnouncementInSocialDB(), 1000); + }); + }; + checkForAnnouncementInSocialDB(); + } +}, [submittedAnnouncementData]); + +const MainContent = styled.div` + padding-left: 2rem; + flex: 3; + @media screen and (max-width: 960px) { + padding-left: 0rem; + } + .post:hover { + background-color: inherit !important; + } +`; + +const SidebarContainer = styled.div` + flex: 1; +`; + +const Heading = styled.div` + font-size: 19px; + font-weight: 600; +`; + +const SubHeading = styled.div` + font-size: 15px; + font-weight: 600; +`; + +const Container = styled.div` + flex-wrap: no-wrap; + max-width: 100%; + + .max-width-100 { + max-width: 100%; + } + @media screen and (max-width: 960px) { + flex-wrap: wrap; + } + + .card { + border-radius: 1rem !important; + } + + .display-none { + display: none; + } +`; + +const Tag = styled.div` + border-top-right-radius: 50px; + border-bottom-right-radius: 50px; + border-top-left-radius: 50px; + border-bottom-left-radius: 50px; + padding-inline: 0.8rem; + padding-block: 0.3rem; + display: flex; + gap: 0.5rem; + border-width: 1px; + border-style: solid; + font-size: 14px; + color: rgba(0, 236, 151, 1); + font-weight: 800; +`; + +const [sort, setSort] = useState("desc"); + +return ( +
+ + +
+ {context.accountId && + (communityData?.admins ?? []).includes(context.accountId) && ( +
+ { + setSubmittedAnnouncementData(v); + setCommunitySocialDB({ handle, data: v }); + }, + profileAccountId: `${handle}.community.${REPL_DEVHUB_CONTRACT}`, + isFinished: () => submittedAnnouncementData === null, + }} + /> +
+ )} +
+ Announcements +
+ +
+
+ {!postsExists && ( +
+
No announcements exists.
+
+ )} +
+ +
+
+
+ +
+
+
{communityData?.description}
+
+ {communityData?.tag} +
+
+
+ Community Admins + {(communityData?.admins ?? []).map((accountId) => ( +
+ +
+ ))} +
+
+
+
+
+); diff --git a/instances/events-committee.near/widget/devhub/entity/community/Card.jsx b/instances/events-committee.near/widget/devhub/entity/community/Card.jsx new file mode 100644 index 000000000..6f1ffa1ab --- /dev/null +++ b/instances/events-committee.near/widget/devhub/entity/community/Card.jsx @@ -0,0 +1,71 @@ +const { href } = VM.require("${REPL_DEVHUB}/widget/core.lib.url"); + +if (!href) { + return <>; +} + +const Card = styled.div` + cursor: pointer; + background-color: white; + border-radius: 0.5rem; + padding: 1.5rem; + gap: 1rem; + height: 100%; + min-height: 12rem; + + display: flex; + align-items: center; + justify-content: flex-start; + transition: all 300ms; + box-shadow: 0 1px 3px 0 rgb(0 0 0 / 0.1), 0 1px 2px -1px rgb(0 0 0 / 0.1); + + &:hover { + box-shadow: 0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1); + } + + img.logo { + height: 6rem; + width: 6rem; + border-radius: 50%; + + object-fit: cover; + } + + h3, + p { + margin: 0; + } + + h3 { + font-size: 1.25rem; + font-weight: 600; + } + + p { + font-size: 1rem; + font-weight: 400; + } +`; + +const CommunityCard = ({ metadata }) => { + const { handle, logo_url, name, description } = metadata; + const link = href({ + widgetSrc: "${REPL_DEVHUB}/widget/app", + params: { page: "community", handle: handle }, + }); + + return ( + + + + +
+

{name}

+

{description}

+
+
+ + ); +}; + +return CommunityCard(props); diff --git a/instances/events-committee.near/widget/devhub/entity/community/Compose.jsx b/instances/events-committee.near/widget/devhub/entity/community/Compose.jsx new file mode 100644 index 000000000..cbff5c749 --- /dev/null +++ b/instances/events-committee.near/widget/devhub/entity/community/Compose.jsx @@ -0,0 +1,478 @@ +const profileAccountId = props.profileAccountId; + +if (!profileAccountId) { + return <>; +} + +State.init({ + image: {}, + text: "", + showPreview: false, + mentionInput: "", // text next to @ tag + mentionsArray: [], // all the mentions in the description +}); + +const [isSubmittingTransaction, setIsSubmittingTransaction] = useState(false); +const profile = Social.getr(`${profileAccountId}/profile`); +const autocompleteEnabled = true; + +const content = { + type: "md", + image: state.image.cid ? { ipfs_cid: state.image.cid } : undefined, + text: state.text, +}; + +function extractMentions(text) { + const mentionRegex = + /@((?:(?:[a-z\d]+[-_])*[a-z\d]+\.)*(?:[a-z\d]+[-_])*[a-z\d]+)/gi; + mentionRegex.lastIndex = 0; + const accountIds = new Set(); + for (const match of text.matchAll(mentionRegex)) { + if ( + !/[\w`]/.test(match.input.charAt(match.index - 1)) && + !/[/\w`]/.test(match.input.charAt(match.index + match[0].length)) && + match[1].length >= 2 && + match[1].length <= 64 + ) { + accountIds.add(match[1].toLowerCase()); + } + } + return [...accountIds]; +} + +function extractTagNotifications(text, item) { + return extractMentions(text || "") + .filter((accountId) => accountId !== profileAccountId) + .map((accountId) => ({ + key: accountId, + value: { + type: "mention", + item, + }, + })); +} + +function composeData() { + const data = { + post: { + main: JSON.stringify(content), + }, + index: { + post: JSON.stringify({ + key: "main", + value: { + type: "md", + }, + }), + }, + }; + + const notifications = extractTagNotifications(state.text, { + type: "social", + path: `${profileAccountId}/post/main`, + }); + + if (notifications.length) { + data.index.notify = JSON.stringify( + notifications.length > 1 ? notifications : notifications[0] + ); + } + + return data; +} + +const handleSubmit = () => { + const data = composeData(); + if (props.onSubmit) { + props.onSubmit(data); + } + if (props.isFinished) { + setIsSubmittingTransaction(true); + } +}; + +function resetState() { + State.update({ + image: {}, + text: "", + }); +} + +useEffect(() => { + if (props.isFinished && props.isFinished() && isSubmittingTransaction) { + resetState(); + setIsSubmittingTransaction(false); + } +}, [props.isFinished]); + +function textareaInputHandler(value) { + const words = value.split(/\s+/); + const allMentiones = words + .filter((word) => word.startsWith("@")) + .map((mention) => mention.slice(1)); + const newMentiones = allMentiones.filter( + (item) => !state.mentionsArray.includes(item) + ); + + State.update((lastKnownState) => ({ + ...lastKnownState, + text: value, + showAccountAutocomplete: newMentiones?.length > 0, + mentionsArray: allMentiones, + mentionInput: newMentiones?.[0] ?? "", + })); +} + +function autoCompleteAccountId(id) { + // to make sure we update the @ at correct index + let currentIndex = 0; + const updatedDescription = state.text.replace( + /(?:^|\s)(@[^\s]*)/g, + (match) => { + if (currentIndex === state.mentionsArray.indexOf(state.mentionInput)) { + currentIndex++; + return ` @${id}`; + } else { + currentIndex++; + return match; + } + } + ); + State.update((lastKnownState) => ({ + ...lastKnownState, + text: updatedDescription, + showAccountAutocomplete: false, + })); +} + +const Wrapper = styled.div` + --padding: 24px; + position: relative; + + @media (max-width: 1024px) { + --padding: 12px; + } +`; + +const LoadingButtonSpinner = ( + +); + +const Avatar = styled.div` + width: 40px; + height: 40px; + pointer-events: none; + position: absolute; + top: var(--padding); + left: var(--padding); + + img { + object-fit: cover; + border-radius: 40px; + width: 100%; + height: 100%; + } + + @media (max-width: 992px) { + display: none; + } +`; + +const Textarea = styled.div` + display: grid; + vertical-align: top; + align-items: center; + position: relative; + align-items: stretch; + + &::after, + textarea { + width: 100%; + min-width: 1em; + height: unset; + min-height: 164px; + font: inherit; + padding: var(--padding) var(--padding) calc(40px + (var(--padding) * 2)) + calc(40px + (var(--padding) * 2)); + margin: 0; + resize: none; + background: none; + appearance: none; + border: none; + grid-area: 1 / 1; + overflow: hidden; + outline: none; + + @media (max-width: 1024px) { + min-height: 124px; + } + + @media (max-width: 992px) { + padding-left: var(--padding); + } + } + + &::after { + content: attr(data-value) " "; + visibility: hidden; + white-space: pre-wrap; + } + + textarea { + transition: all 200ms; + + &::placeholder { + opacity: 1; + color: #687076; + } + + &:empty + p { + display: block; + } + } +`; + +const TextareaDescription = styled.p` + position: absolute; + top: calc(var(--padding) + 24px); + left: calc(42px + (var(--padding) * 2)); + right: var(--padding); + font-size: 10px; + line-height: 18px; + font-weight: 400; + color: #687076; + pointer-events: none; + display: none; + + a { + color: #000; + outline: none; + font-weight: 600; + pointer-events: auto; + + &:hover, + &:focus { + color: #000; + text-decoration: underline; + } + } + + @media (max-width: 992px) { + left: var(--padding); + } +`; + +const Actions = styled.div` + display: inline-flex; + gap: 12px; + position: absolute; + bottom: var(--padding); + right: var(--padding); + + .commit-post-button, + .preview-post-button { + background: #59e692; + color: #09342e; + border-radius: 40px; + height: 40px; + padding: 0 35px; + font-weight: 600; + font-size: 14px; + border: none; + cursor: pointer; + transition: background 200ms, opacity 200ms; + + &:hover, + &:focus { + background: rgb(112 242 164); + outline: none; + } + + &:disabled { + opacity: 0.5; + pointer-events: none; + } + } + + .preview-post-button { + color: #11181c; + background: #f1f3f5; + padding: 0; + width: 40px; + + &:hover, + &:focus { + background: #d7dbde; + outline: none; + } + } + + .upload-image-button { + display: flex; + align-items: center; + justify-content: center; + background: #f1f3f5; + color: #11181c; + border-radius: 40px; + height: 40px; + min-width: 40px; + font-size: 0; + border: none; + cursor: pointer; + transition: background 200ms, opacity 200ms; + + &::before { + font-size: 16px; + } + + &:hover, + &:focus { + background: #d7dbde; + outline: none; + } + + &:disabled { + opacity: 0.5; + pointer-events: none; + } + + span { + margin-left: 12px; + } + } + + .d-inline-block { + display: flex !important; + gap: 12px; + margin: 0 !important; + + .overflow-hidden { + width: 40px !important; + height: 40px !important; + } + } +`; + +const PreviewWrapper = styled.div` + position: relative; + padding: var(--padding); + padding-bottom: calc(40px + (var(--padding) * 2)); +`; + +const AutoComplete = styled.div` + position: absolute; + z-index: 5; + bottom: 0; + left: 0; + right: 0; + + > div > div { + padding: calc(var(--padding) / 2); + } +`; + +return ( + + {state.showPreview ? ( + + } + props={{ + accountId: profileAccountId, + blockHeight: "now", + content, + }} + /> + + ) : ( + <> + + + + + + + )} + + {autocompleteEnabled && state.showAccountAutocomplete && ( + + State.update({ showAccountAutocomplete: false }), + }} + /> + + )} + + + {!state.showPreview && ( + + )} + + + + + + +); diff --git a/instances/events-committee.near/widget/devhub/entity/community/Discussions.jsx b/instances/events-committee.near/widget/devhub/entity/community/Discussions.jsx new file mode 100644 index 000000000..5ce67ecfb --- /dev/null +++ b/instances/events-committee.near/widget/devhub/entity/community/Discussions.jsx @@ -0,0 +1,276 @@ +const NEW_DISCUSSION_POSTED_CONTENT_STORAGE_KEY = + "new_discussion_posted_content"; +const { handle } = props; +const { getCommunity, setCommunitySocialDB } = VM.require( + "${REPL_DEVHUB}/widget/core.adapter.devhub-contract" +); + +getCommunity = getCommunity || (() => <>); +setCommunitySocialDB = setCommunitySocialDB || (() => <>); + +const communityData = getCommunity({ handle }); + +const MainContent = styled.div` + padding-left: 2rem; + flex: 3; + @media screen and (max-width: 960px) { + padding-left: 0rem; + } + .post:hover { + background-color: inherit !important; + } +`; + +const SidebarContainer = styled.div` + flex: 1; +`; + +const Heading = styled.div` + font-size: 19px; + font-weight: 600; +`; + +const SubHeading = styled.div` + font-size: 15px; + font-weight: 600; +`; + +const Container = styled.div` + flex-wrap: no-wrap; + max-width: 100%; + + .max-width-100 { + max-width: 100%; + } + @media screen and (max-width: 960px) { + flex-wrap: wrap; + } + + .card { + border-radius: 1rem !important; + } + + .display-none { + display: none; + } +`; + +const Tag = styled.div` + border-top-right-radius: 50px; + border-bottom-right-radius: 50px; + border-top-left-radius: 50px; + border-bottom-left-radius: 50px; + padding-inline: 0.8rem; + padding-block: 0.3rem; + display: flex; + gap: 0.5rem; + border-width: 1px; + border-style: solid; + font-size: 14px; + color: rgba(0, 236, 151, 1); + font-weight: 800; +`; + +const [sort, setSort] = useState("desc"); +const [isTransactionFinished, setIsTransactionFinished] = useState(false); + +const discussionsAccountId = + "discussions." + handle + ".community.${REPL_DEVHUB_CONTRACT}"; + +function checkIfReposted(blockHeight) { + Near.asyncView("${REPL_SOCIAL_CONTRACT}", "get", { + keys: [`${discussionsAccountId}/index/**`], + }) + .then((response) => { + const repost = response[discussionsAccountId].index.repost; + + if (repost && repost.indexOf(`"blockHeight":${blockHeight}`) > -1) { + setIsTransactionFinished(true); + } else { + setTimeout(() => checkIfReposted(), 500); + } + }) + .catch((error) => { + console.error( + "DevHub Error [Discussions]: checkIfReposted failed", + error + ); + }); +} + +function repostOnDiscussions(blockHeight) { + Near.call([ + { + contractName: "${REPL_DEVHUB_CONTRACT}", + methodName: "create_discussion", + args: { + handle, + block_height: blockHeight, + }, + gas: Big(10).pow(14), + }, + ]); + checkIfReposted(blockHeight); +} + +async function checkHashes() { + if (props.transactionHashes) { + asyncFetch("${REPL_RPC_URL}", { + method: "POST", + headers: { + "content-type": "application/json", + }, + body: JSON.stringify({ + jsonrpc: "2.0", + id: "dontcare", + method: "tx", + params: [props.transactionHashes, context.accountId], + }), + }).then((transaction) => { + if (transaction !== null) { + const transaction_method_name = + transaction?.body?.result?.transaction?.actions[0].FunctionCall + .method_name; + + if (transaction_method_name === "set") { + getBlockHeightAndRepost(); + } + + // show the latest created post to user + if (transaction_method_name === "create_discussion") { + console.log("Discussions created in the last call, show it to user."); + } + } + }); + } +} + +function getBlockHeightAndRepost() { + const newDiscussionPostedContent = Storage.get( + NEW_DISCUSSION_POSTED_CONTENT_STORAGE_KEY + ); + console.log("new discussion content", newDiscussionPostedContent); + + Near.asyncView("${REPL_SOCIAL_CONTRACT}", "get", { + keys: [`${context.accountId}/post/**`], + options: { + with_block_height: true, + }, + }) + .then((response) => { + const post_main = response[context.accountId].post.main; + const content = post_main[""]; + if (content === newDiscussionPostedContent) { + const blockHeight = post_main[":block"]; + console.log("content matches", blockHeight, post_main); + repostOnDiscussions(blockHeight); + } else { + console.log( + "content does not match (yet)", + post_main, + newDiscussionPostedContent + ); + setTimeout(() => getBlockHeightAndRepost(), 500); + } + }) + .catch((error) => { + console.log( + "DevHub Error [Discussions]: getBlockHeightAndRepost failed", + error + ); + }); +} + +checkHashes(); + +return ( +
+ + +
+ {context.accountId && ( +
+ isTransactionFinished, + onSubmit: (v) => { + console.log("ON SUBMIT"); + Storage.set( + NEW_DISCUSSION_POSTED_CONTENT_STORAGE_KEY, + v.post.main + ); + + Social.set(v, { + force: true, + onCommit: () => { + getBlockHeightAndRepost(); + }, + }); + }, + profileAccountId: context.accountId, + }} + /> +
+ )} +
+ Discussions +
+ +
+
+
+ +
+
+
+ +
+
+
{communityData?.description}
+
+ {communityData?.tag} +
+
+
+ Community Admins + {(communityData?.admins ?? []).map((accountId) => ( +
+ +
+ ))} +
+
+
+
+
+); diff --git a/instances/events-committee.near/widget/devhub/entity/community/Provider.jsx b/instances/events-committee.near/widget/devhub/entity/community/Provider.jsx new file mode 100644 index 000000000..03689c164 --- /dev/null +++ b/instances/events-committee.near/widget/devhub/entity/community/Provider.jsx @@ -0,0 +1,72 @@ +const { handle, Children } = props; + +const { + getAccountCommunityPermissions, + createCommunity, + updateCommunity, + deleteCommunity, + getCommunity, + setCommunityAddons, +} = VM.require("${REPL_DEVHUB}/widget/core.adapter.devhub-contract"); + +if ( + !getCommunity || + !getAccountCommunityPermissions || + !createCommunity || + !updateCommunity || + !deleteCommunity || + !setCommunityAddons +) { + return

Loading modules...

; +} + +const CenteredMessage = styled.div` + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + width: 100%; + height: ${(p) => p.height ?? "100%"}; +`; + +const [isLoading, setIsLoading] = useState(false); +const [error, setError] = useState(null); + +const community = getCommunity({ handle }); + +const permissions = getAccountCommunityPermissions({ + account_id: context.accountId, + community_handle: handle, +}) || { + can_configure: false, + can_delete: false, +}; + +if (isLoading) { + return ( + +

Loading...

+
+ ); +} else if (!community) { + return ( + +

{`Community with handle "${handle}" not found.`}

+
+ ); +} + +function handleUpdateCommunity(v) { + updateCommunity(v); +} + +return ( + +); diff --git a/instances/events-committee.near/widget/devhub/entity/community/Sidebar.jsx b/instances/events-committee.near/widget/devhub/entity/community/Sidebar.jsx new file mode 100644 index 000000000..182c44cb8 --- /dev/null +++ b/instances/events-committee.near/widget/devhub/entity/community/Sidebar.jsx @@ -0,0 +1,70 @@ +const { href } = VM.require("${REPL_DEVHUB}/widget/core.lib.url"); + +if (!href) { + return

Loading modules...

; +} + +const { community } = props; + +const CommunitySummary = () => { + return ( + <> + + + + + + + + ); +}; + +return community === null ? ( +
Loading...
+) : ( +
+ , + style: { marginTop: "0.5rem" }, + }} + /> + + ( +
+ +
+ )), + + fullWidth: true, + minHeight: 0, + noBorder: true, + }} + /> +
+); diff --git a/instances/events-committee.near/widget/devhub/entity/community/Spawner.jsx b/instances/events-committee.near/widget/devhub/entity/community/Spawner.jsx new file mode 100644 index 000000000..84c1a94c1 --- /dev/null +++ b/instances/events-committee.near/widget/devhub/entity/community/Spawner.jsx @@ -0,0 +1,100 @@ +const { typeMatch } = VM.require("${REPL_DEVHUB}/widget/core.lib.struct"); + +if (!typeMatch) { + return

Loading modules...

; +} + +const { data, onSubmit, onCancel } = props; + +const CommunityInputsPartialSchema = { + handle: { + inputProps: { + min: 2, + max: 40, + allowCommaAndSpace: false, + placeholder: + "Choose unique URL handle for your community. Example: zero-knowledge.", + required: true, + }, + + label: "URL handle", + order: 3, + }, + + name: { + inputProps: { + min: 2, + max: 30, + placeholder: "Community name.", + required: true, + }, + + label: "Name", + order: 1, + }, + + tag: { + inputProps: { + min: 2, + max: 30, + allowCommaAndSpace: false, + placeholder: + "Any posts with this tag will show up in your community feed.", + + required: true, + }, + + label: "Tag", + order: 4, + }, + + description: { + inputProps: { + min: 2, + max: 60, + + placeholder: + "Describe your community in one short sentence that will appear in the communities discovery page.", + + required: true, + }, + + label: "Description", + order: 2, + }, +}; + +const communityInputsValidator = (formValues) => + typeMatch(formValues) && + Object.values(formValues).every( + (value) => typeof value === "string" && value.length > 0 + ); + +const CommunityInputsDefaults = { + handle: "", + name: "", + tag: "", + description: "", +}; + +return ( + +); diff --git a/instances/events-committee.near/widget/devhub/entity/community/Teams.jsx b/instances/events-committee.near/widget/devhub/entity/community/Teams.jsx new file mode 100644 index 000000000..06c23bc6f --- /dev/null +++ b/instances/events-committee.near/widget/devhub/entity/community/Teams.jsx @@ -0,0 +1,62 @@ +const { handle } = props; + +const { Tile } = + VM.require("${REPL_DEVHUB}/widget/devhub.components.molecule.Tile") || + (() => <>); + +if (!Tile) { + return
Loading...
; +} + +const { getCommunity } = VM.require( + "${REPL_DEVHUB}/widget/core.adapter.devhub-contract" +); + +const communityData = getCommunity({ handle }); + +if (communityData === null) { + return
Loading...
; +} + +const UserList = ({ name, users }) => ( +
+ {(users ?? []).map((user, i) => ( +
+
+ {name + " #" + (i + 1)} +
+ +
+ + + +
+
+ ))} +
+); + +return ( +
+ +
+
+
+ Community Admins +
+
+ +
+
+
+); diff --git a/instances/events-committee.near/widget/devhub/entity/community/Tile.jsx b/instances/events-committee.near/widget/devhub/entity/community/Tile.jsx new file mode 100644 index 000000000..7e5a0b472 --- /dev/null +++ b/instances/events-committee.near/widget/devhub/entity/community/Tile.jsx @@ -0,0 +1,90 @@ +/* INCLUDE: "core/lib/gui/attractable" */ +const AttractableDiv = styled.div` + box-shadow: 0 0.125rem 0.25rem rgba(0, 0, 0, 0.075) !important; + transition: box-shadow 0.6s; + + &:hover { + box-shadow: 0 0.5rem 1rem rgba(0, 0, 0, 0.15) !important; + } +`; + +const AttractableLink = styled.a` + box-shadow: 0 0.125rem 0.25rem rgba(0, 0, 0, 0.075) !important; + transition: box-shadow 0.6s; + + &:hover { + box-shadow: 0 0.5rem 1rem rgba(0, 0, 0, 0.15) !important; + } +`; + +const AttractableImage = styled.img` + box-shadow: 0 0.125rem 0.25rem rgba(0, 0, 0, 0.075) !important; + transition: box-shadow 0.6s; + + &:hover { + box-shadow: 0 0.5rem 1rem rgba(0, 0, 0, 0.15) !important; + } +`; +/* END_INCLUDE: "core/lib/gui/attractable" */ + +const Tile = ({ + children, + borderRadius, + className, + fullWidth, + headerSlotRight, + heading, + headingAdornment, + id, + isHidden, + noBorder, + noFrame, + minHeight, + style, +}) => ( + + { +
+
+ {headingAdornment} + {heading} +
+ + {headerSlotRight} +
+ } + + {children} +
+); + +return Tile(props); diff --git a/instances/events-committee.near/widget/devhub/entity/community/configuration/AboutConfigurator.jsx b/instances/events-committee.near/widget/devhub/entity/community/configuration/AboutConfigurator.jsx new file mode 100644 index 000000000..e0c60eafd --- /dev/null +++ b/instances/events-committee.near/widget/devhub/entity/community/configuration/AboutConfigurator.jsx @@ -0,0 +1,64 @@ +const CommunityAboutSchema = { + bio_markdown: { + format: "markdown", + + inputProps: { + min: 3, + max: 200, + + placeholder: + "Tell people about your community. This will appear on your community’s homepage.", + required: true, + resize: "none", + }, + + label: "Bio", + multiline: true, + order: 1, + }, + + twitter_handle: { + inputProps: { prefix: "https://twitter.com/", min: 2, max: 60 }, + label: "Twitter", + order: 2, + }, + + github_handle: { + inputProps: { prefix: "https://github.com/", min: 2, max: 60 }, + label: "Github", + order: 3, + }, + + telegram_handle: { + inputProps: { prefix: "https://t.me/", min: 2, max: 60 }, + format: "comma-separated", + label: "Telegram", + order: 4, + }, + + website_url: { + inputProps: { prefix: "https://", min: 2, max: 60, validUrl: true }, + label: "Website", + order: 5, + }, +}; + +const { data, onSubmit, onCancel, setIsActive, isActive } = props; + +function handleOnSubmit(v) { + onSubmit(v); + setIsActive(false); +} + +return ( + +); diff --git a/instances/events-committee.near/widget/devhub/entity/community/configuration/AccessControlConfigurator.jsx b/instances/events-committee.near/widget/devhub/entity/community/configuration/AccessControlConfigurator.jsx new file mode 100644 index 000000000..61ef162d8 --- /dev/null +++ b/instances/events-committee.near/widget/devhub/entity/community/configuration/AccessControlConfigurator.jsx @@ -0,0 +1,174 @@ +const CommunityAccessControlSchema = { + admins: { + format: "comma-separated", + inputProps: { required: true }, + label: "Admins", + order: 1, + }, +}; + +const Struct = VM.require("${REPL_DEVHUB}/widget/core.lib.struct"); + +if (!Struct) { + return

Loading modules...

; +} + +const AutoComplete = styled.div` + z-index: 5; + + > div > div { + padding: calc(var(--padding) / 2); + } +`; + +const Wrapper = styled.div` + .container { + display: flex; + align-items: center; + flex-wrap: wrap; + gap: 0.5em; + } + + .admins-item { + display: inline-block; + padding: 0.6em 0.8em; + border-radius: 10px; + border: 1px solid lightgray; + position: relative; + } + + .admins-item .remove { + position: absolute; + right: 5px; + top: 0; + font-size: 18px; + color: grey; + cursor: pointer; + } + + .admins-input { + flex-grow: 1; + border: none; + outline: none; + } + + input[type="text"]:disabled { + all: inherit; + } + + input::placeholder { + font-size: 16px; + } +`; + +const { data, onSubmit, onCancel, setIsActive, isActive } = props; +const initialValues = Struct.typeMatch(CommunityAccessControlSchema) + ? Struct.pick(data ?? {}, Object.keys(CommunityAccessControlSchema)) + : {}; + +const [admins, setAdmins] = useState(initialValues?.admins ?? []); +const [text, setText] = useState(""); +const [showAccountAutocomplete, setShowAutoAutocomplete] = useState(false); + +function handleKeyDown(e) { + if (e.key !== "Enter") return; + const value = e.target.value; + if (!value.trim()) return; + // Add the value to the admins array + setAdmins([...admins, value]); + setText(""); +} + +const onCancelClick = () => { + setAdmins(initialValues?.admins ?? []); + setIsActive(false); +}; + +const onSubmitClick = () => { + onSubmit({ admins: admins.map((admin) => admin.trim()) }); + setIsActive(false); +}; + +function autoCompleteAccountId(id) { + setAdmins([...admins, id]); + setText(""); + setShowAutoAutocomplete(false); +} + +return ( + +
+ {admins.map((admin, index) => ( +
+ + {/* don't allow removal if only 1 admin is added */} + {admins.length > 1 && isActive && ( + setAdmins(admins.filter((item) => item !== admin))} + > + × + + )} +
+ ))} + { + setShowAutoAutocomplete(true); + setText(v.target.value); + }} + onKeyDown={handleKeyDown} + type="text" + className="admins-input" + placeholder={isActive && "Add Admins here..."} + /> +
+ {showAccountAutocomplete && ( + + setShowAutoAutocomplete(false), + filterAccounts: admins, + }} + /> + + )} + {isActive && ( +
+ + +
+ )} +
+); diff --git a/instances/events-committee.near/widget/devhub/entity/community/configuration/AddonsConfigurator.jsx b/instances/events-committee.near/widget/devhub/entity/community/configuration/AddonsConfigurator.jsx new file mode 100644 index 000000000..e54a32164 --- /dev/null +++ b/instances/events-committee.near/widget/devhub/entity/community/configuration/AddonsConfigurator.jsx @@ -0,0 +1,336 @@ +const { getAllAddons } = + VM.require("${REPL_DEVHUB}/widget/core.adapter.devhub-contract") || + (() => {}); + +const { generateRandom6CharUUID } = VM.require( + "${REPL_DEVHUB}/widget/core.lib.stringUtils" +); + +generateRandom6CharUUID || (generateRandom6CharUUID = () => {}); + +const { href } = VM.require("${REPL_DEVHUB}/widget/core.lib.url") || (() => {}); + +const availableAddons = getAllAddons() || []; + +const isActive = props.isActive; + +const Container = styled.div` + display: flex; + flex-direction: column; + width: 100%; +`; + +const Item = styled.div` + padding: 10px; + margin: 5px; + display: flex; + align-items: center; + gap: 10px; +`; + +const Icon = styled.span` + margin-right: 10px; +`; + +const EditableField = styled.input` + flex: 1; +`; + +const ToggleButton = styled.input` + margin-left: 10px; +`; + +const Table = styled.table` + width: 100%; + border-collapse: collapse; +`; + +const Header = styled.thead` + background-color: #f0f0f0; +`; + +const HeaderCell = styled.th` + padding: 10px; + text-align: left; +`; + +const Row = styled.tr``; + +const Cell = styled.td` + padding: 10px; +`; + +const AddonItem = ({ + data, + onUpdate, + onMove, + onRemove, + index, + isTop, + isBottom, +}) => { + const handleNameChange = (event) => { + const newName = event.target.value; + onUpdate({ ...data, display_name: newName }); + }; + + const handleEnableChange = () => { + onUpdate({ ...data, enabled: !data.enabled }); + }; + + const moveItemUp = () => { + if (!isTop) { + onMove(index, index - 1); + } + }; + + const moveItemDown = () => { + if (!isBottom) { + onMove(index, index + 1); + } + }; + + const removeItem = () => { + onRemove(data.id); + }; + + const addonMatch = + availableAddons.find((it) => it.id === data.addon_id) ?? null; + + return ( + + +
+ + +
+
+ +
{addonMatch.title}
+
+ + + + +
+ +
+
+ +
+ {isActive && ( + + )} +
+
+
+ ); +}; + +function arraysAreEqual(arr1, arr2) { + if (arr1.length !== arr2.length) { + return false; + } + for (let i = 0; i < arr1.length; i++) { + if (arr1[i] !== arr2[i]) { + return false; + } + } + return true; +} + +const AddonsConfigurator = ({ data, onSubmit }) => { + const [originalList, setOriginalList] = useState(data); + const [list, setList] = useState(data); + const [changesMade, setChangesMade] = useState(false); + + useEffect(() => { + setOriginalList(data); + }, [data]); + + const updateItem = (updatedItem) => { + const updatedList = list.map((item) => + item.id === updatedItem.id ? updatedItem : item + ); + setList(updatedList); + setChangesMade(!arraysAreEqual(originalList, updatedList)); + }; + + const moveItem = (fromIndex, toIndex) => { + const updatedList = [...list]; + const [movedItem] = updatedList.splice(fromIndex, 1); + updatedList.splice(toIndex, 0, movedItem); + setList(updatedList); + setChangesMade(!arraysAreEqual(originalList, updatedList)); + }; + + const [selectedAddon, setSelectedAddon] = useState(null); + + /** + * Necessary solution to migrate the old blogs to the new blogv2. + * Since the blogs are migrated before the addon instance is created. + */ + const getRandomIdExceptFirstBlogV2Instance = (selectedAddonId) => { + if (selectedAddonId !== "blogv2") { + return generateRandom6CharUUID(); + } + const firstBlogV2Addon = availableAddons.find( + (addon) => addon.id === "blogv2" + ); + if (!firstBlogV2Addon) { + // If no blogv2 addon is found, return a static id + // "first-blogv2-no-random-id"; + return "blogv2"; + } + return `blogv2-id-${generateRandom6CharUUID()}`; + }; + + const handleAddItem = () => { + const newItem = { + id: getRandomIdExceptFirstBlogV2Instance(selectedAddon.id), + addon_id: selectedAddon.id, + display_name: selectedAddon.title, + enabled: true, + parameters: "{}", + }; + const updatedList = [...list, newItem]; + setList(updatedList); + setChangesMade(!arraysAreEqual(originalList, updatedList)); + setSelectedAddon(null); + }; + + const removeItem = (id) => { + const updatedList = list.filter((item) => item.id !== id); + setList(updatedList); + setChangesMade(!arraysAreEqual(originalList, updatedList)); + }; + + return ( + +

+ Add or remove custom tabs, which will appear in your community's + navigation bar. +
+ You can customize them on each page. +

+ {list.length > 0 && ( + +
+ + Order + Tab Type + Tab Name + Enabled + {isActive && ( + Actions + )} + +
+ + {list.map((item, index) => ( + + ))} + +
+ )} + {isActive && availableAddons && ( +
+
+ ({ + label: addon.title, + value: addon.id, + })), + value: selectedAddon.id ?? "", + onChange: (e) => + setSelectedAddon( + availableAddons.find((addon) => addon.id === e.target.value) + ), + placeholder: "Select an addon", + }} + /> + +
+
+ )} + {isActive && ( +
+ onSubmit(list), + }} + /> +
+ )} +
+ ); +}; + +return AddonsConfigurator(props); diff --git a/instances/events-committee.near/widget/devhub/entity/community/configuration/BrandingConfigurator.jsx b/instances/events-committee.near/widget/devhub/entity/community/configuration/BrandingConfigurator.jsx new file mode 100644 index 000000000..d06d99b3f --- /dev/null +++ b/instances/events-committee.near/widget/devhub/entity/community/configuration/BrandingConfigurator.jsx @@ -0,0 +1,165 @@ +const Banner = styled.div` + border-top-left-radius: var(--bs-border-radius-xl) !important; + border-top-right-radius: var(--bs-border-radius-xl) !important; + height: calc(100% - 100px); + + & > div :not(.btn) { + position: absolute; + display: none; + margin: 0 !important; + width: 0 !important; + height: 0 !important; + } + + .btn { + padding: 0.5rem 0.75rem !important; + min-height: 32; + line-height: 1; + + border: none; + border-radius: 50px; + --bs-btn-color: #ffffff; + --bs-btn-bg: #087990; + --bs-btn-border-color: #087990; + --bs-btn-hover-color: #ffffff; + --bs-btn-hover-bg: #055160; + --bs-btn-hover-border-color: #055160; + --bs-btn-focus-shadow-rgb: 49, 132, 253; + --bs-btn-active-color: #ffffff; + --bs-btn-active-bg: #055160; + --bs-btn-active-border-color: #055160; + --bs-btn-active-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125); + opacity: 0.8; + + &:hover { + opacity: 1; + } + } +`; + +const Logo = styled.div` + & > div :not(.btn) { + position: absolute; + display: none; + margin: 0 !important; + width: 0 !important; + height: 0 !important; + } + + .btn { + padding: 0.5rem 0.75rem !important; + min-height: 32; + line-height: 1; + + border: none; + border-radius: 50px; + --bs-btn-color: #ffffff; + --bs-btn-bg: #087990; + --bs-btn-border-color: #087990; + --bs-btn-hover-color: #ffffff; + --bs-btn-hover-bg: #055160; + --bs-btn-hover-border-color: #055160; + --bs-btn-focus-shadow-rgb: 49, 132, 253; + --bs-btn-active-color: #ffffff; + --bs-btn-active-bg: #055160; + --bs-btn-active-border-color: #055160; + --bs-btn-active-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125); + opacity: 0.8; + + &:hover { + opacity: 1; + } + } +`; + +const cidToURL = (cid) => `https://ipfs.near.social/ipfs/${cid}`; + +const { data, onSubmit, hasConfigurePermissions, link } = props; + +const initialInput = { banner: null, logo: null }; + +const initialValues = { + banner: { cid: data.banner_url.split("/").at(-1) }, + logo: { cid: data.logo_url.split("/").at(-1) }, +}; + +State.init({ + input: initialInput, +}); + +const hasUnsubmittedChanges = Object.values(state.input).some( + (value) => value !== null +); + +const isSynced = state.input === initialValues; + +if (hasUnsubmittedChanges && !isSynced) { + onSubmit({ + banner_url: cidToURL(state.input.banner?.cid ?? initialValues.banner.cid), + logo_url: cidToURL(state.input.logo?.cid ?? initialValues.logo.cid), + }); + + State.update((lastKnownState) => ({ + ...lastKnownState, + input: initialInput, + })); +} + +return ( +
+ + {hasConfigurePermissions && ( + + )} + + + {hasConfigurePermissions && } + + +
+
+ {typeof link === "string" && link.length > 0 ? ( + {data.name} + ) : ( + data.name + )} +
+ +

+ {data.description} +

+
+
+); diff --git a/instances/events-committee.near/widget/devhub/entity/community/configuration/ConfigurationSection.jsx b/instances/events-committee.near/widget/devhub/entity/community/configuration/ConfigurationSection.jsx new file mode 100644 index 000000000..585ed16dd --- /dev/null +++ b/instances/events-committee.near/widget/devhub/entity/community/configuration/ConfigurationSection.jsx @@ -0,0 +1,49 @@ +const { + title, + hasConfigurePermissions, + Configurator, + Preview, + headerRight, + forceEditActive, +} = props; + +const [isEditActive, setEditActive] = useState(forceEditActive || false); + +function SectionHeader() { + return ( +
+
+ {title} +
+ {headerRight || + (hasConfigurePermissions && ( + setEditActive(!isEditActive), + }} + /> + ))} +
+ ); +} + +return ( +
+ + setEditActive(!isEditActive)} + /> +
+); diff --git a/instances/events-committee.near/widget/devhub/entity/community/configuration/InformationConfigurator.jsx b/instances/events-committee.near/widget/devhub/entity/community/configuration/InformationConfigurator.jsx new file mode 100644 index 000000000..44a7e707c --- /dev/null +++ b/instances/events-committee.near/widget/devhub/entity/community/configuration/InformationConfigurator.jsx @@ -0,0 +1,78 @@ +const CommunityInformationSchema = { + name: { + inputProps: { + min: 2, + max: 30, + placeholder: "Community name.", + required: true, + }, + + label: "Name", + order: 1, + }, + + description: { + inputProps: { + min: 2, + max: 60, + + placeholder: + "Describe your community in one short sentence that will appear in the communities discovery page.", + + required: true, + }, + + label: "Description", + order: 2, + }, + + handle: { + inputProps: { + min: 2, + max: 40, + allowCommaAndSpace: false, + placeholder: + "Choose unique URL handle for your community. Example: zero-knowledge.", + + required: true, + }, + + label: "URL handle", + order: 3, + }, + + tag: { + inputProps: { + min: 2, + max: 30, + allowCommaAndSpace: false, + placeholder: + "Any posts with this tag will show up in your community feed.", + + required: true, + }, + + label: "Tag", + order: 4, + }, +}; + +const { data, onSubmit, onCancel, setIsActive, isActive } = props; + +function handleOnSubmit(v) { + onSubmit(v); + setIsActive(false); +} + +return ( + +); diff --git a/instances/events-committee.near/widget/devhub/entity/post/History.jsx b/instances/events-committee.near/widget/devhub/entity/post/History.jsx new file mode 100644 index 000000000..1eb7a749a --- /dev/null +++ b/instances/events-committee.near/widget/devhub/entity/post/History.jsx @@ -0,0 +1,130 @@ +/* +---props--- +props.post: {}; +props.id: number; +props.newTab: boolean; +props.timestamp: number; +props.referral: any; +*/ +const { href } = VM.require("${REPL_DEVHUB}/widget/core.lib.url") || (() => {}); + +const postId = props.post.id ?? (props.id ? parseInt(props.id) : 0); +const post = + props.post ?? + Near.view(nearDevGovGigsContractAccountId, "get_post", { + post_id: postId, + }); +if (!post || !post.snapshot_history) { + return
; +} +const referral = props.referral; + +function readableDate(timestamp) { + var a = new Date(timestamp); + return ( + a.toDateString() + + " " + + a.toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" }) + ).substring(4); +} + +const currentTimestamp = props.timestamp ?? post.snapshot.timestamp; +const snapshot = post.snapshot; +const snapshotHistory = post.snapshot_history + ? Array.from(post.snapshot_history) + : []; + +snapshotHistory.push(snapshot); +snapshotHistory.reverse(); + +const history = ( +
+ + +
+); + +return history; diff --git a/instances/events-committee.near/widget/devhub/entity/post/List.jsx b/instances/events-committee.near/widget/devhub/entity/post/List.jsx new file mode 100644 index 000000000..972a3d78d --- /dev/null +++ b/instances/events-committee.near/widget/devhub/entity/post/List.jsx @@ -0,0 +1,400 @@ +// This component implementation was forked from [IndexFeed], but it does not fully implement lazy loading. +// While this component uses InfiniteScroll, it still loads the whole list of Post IDs in one view call. +// The contract will need to be extended with pagination support, yet, even in the current state the page loads much faster. +// [IndexFeed]: https://near.social/#/mob.near/widget/WidgetSource?src=mob.near/widget/IndexFeed + +const { href } = VM.require("${REPL_DEVHUB}/widget/core.lib.url"); + +const { draftState, onDraftStateChange } = VM.require( + "${REPL_DEVHUB}/widget/devhub.entity.post.draft" +); + +if (!href) { + return

Loading modules...

; +} + +const QUERYAPI_ENDPOINT = `https://near-queryapi.api.pagoda.co/v1/graphql/`; + +const queryName = + props.queryName ?? `bo_near_devhub_v38_posts_with_latest_snapshot`; +const totalQueryName = + props.totalQueryName ?? + "bo_near_devhub_v38_posts_with_latest_snapshot_aggregate"; +const query = `query DevhubPostsQuery($limit: Int = 100, $offset: Int = 0, $where: ${queryName}_bool_exp = {}) { + ${queryName}( + limit: $limit + offset: $offset + order_by: {ts: desc} + where: $where + ) { + post_id + } + } +`; + +const totalQuery = `query DevhubTotalPostsQuery($where: ${queryName}_bool_exp = {}) { + ${totalQueryName}( + where: $where + ) { + aggregate { + count + } + } + } +`; + +function fetchGraphQL(operationsDoc, operationName, variables) { + return asyncFetch(QUERYAPI_ENDPOINT, { + method: "POST", + headers: { "x-hasura-role": `bo_near` }, + body: JSON.stringify({ + query: operationsDoc, + variables: variables, + operationName: operationName, + }), + }); +} + +function searchConditionChanged() { + return ( + props.author != state.author || + props.term != state.term || + props.tag != state.tag || + props.recency != state.recency + ); +} + +function updateSearchCondition() { + State.update({ + author: props.author, + term: props.term, + tag: props.tag, + recency: props.recency, + loading: true, + }); +} + +const initialRenderLimit = props.initialRenderLimit ?? 3; +const addDisplayCount = props.nextLimit ?? initialRenderLimit; + +State.init({ + period: "week", + totalItems: 0, + displayCount: initialRenderLimit, +}); + +function getPostIds(tag, offset) { + if (searchConditionChanged()) { + updateSearchCondition(); + } + let where = {}; + let authorId = props.author; + let label = tag || props.tag; + if (authorId) { + where = { author_id: { _eq: authorId }, ...where }; + } + if (props.term) { + where = { description: { _ilike: `%${props.term}%` }, ...where }; + } + if (label) { + if (typeof label === "string") { + // Handle a single label + where = { labels: { _contains: label }, ...where }; + } else if (Array.isArray(label)) { + // Handle an array of labels + where = { + labels: { + _containsAny: label, + }, + ...where, + }; + } + } + if (!props.recency) { + // show only top level posts + where = { parent_id: { _is_null: true }, ...where }; + } + + // Don't show blog and devhub-test posts + where = { + _and: [ + { + _not: { + labels: { _contains: "blog" }, + parent_id: { _is_null: true }, + post_type: { _eq: "Comment" }, + }, + }, + { + _not: { + labels: { _contains: "devhub-test" }, + }, + }, + ], + ...where, + }; + + if (!offset) { + fetchGraphQL(totalQuery, "DevhubTotalPostsQuery", { + where, + }).then((result) => { + const data = result.body.data[totalQueryName]; + State.update({ + totalItems: data.aggregate.count, + }); + }); + } + + fetchGraphQL(query, "DevhubPostsQuery", { + limit: 50, + offset: offset ?? 0, + where, + }).then((result) => { + if (result.status === 200) { + if (result.body.data) { + const data = result.body.data[queryName]; + if (offset) { + State.update({ + postIds: state.postIds.concat(data.map((p) => p.post_id)), + loading: false, + }); + } else { + State.update({ + postIds: data.map((p) => p.post_id), + loading: false, + }); + } + } + } else { + State.update({ loading: false }); + } + }); +} + +if (!state.items || searchConditionChanged()) { + getPostIds(); +} + +function defaultRenderItem(postId, additionalProps) { + if (!additionalProps) { + additionalProps = {}; + } + // It is important to have a non-zero-height element as otherwise InfiniteScroll loads too many items on initial load + return ( +
+ { + if (typeof props.updateTagInput === "function") { + props.updateTagInput(tag); + } + getPostIds(tag); + }, + transactionHashes: props.transactionHashes, + }} + /> +
+ ); +} + +const renderItem = props.renderItem ?? defaultRenderItem; + +const cachedRenderItem = (item, i) => { + if (props.term) { + return renderItem(item, { + searchKeywords: [props.term], + }); + } + + const key = JSON.stringify(item); + + if (!(key in state.cachedItems)) { + state.cachedItems[key] = renderItem(item); + State.update(); + } + return state.cachedItems[key]; +}; + +const ONE_DAY = 60 * 60 * 24 * 1000; +const ONE_WEEK = 60 * 60 * 24 * 1000 * 7; +const ONE_MONTH = 60 * 60 * 24 * 1000 * 30; + +function getHotnessScore(post) { + //post.id - shows the age of the post, should grow exponentially, since newer posts are more important + //post.likes.length - linear value + const age = Math.pow(post.id, 5); + const comments = post.comments; + const commentAge = comments.reduce((sum, age) => sum + Math.pow(age, 5), 0); + const totalAge = age + commentAge; + //use log functions to make likes score and exponentially big age score close to each other + return Math.log10(post.likes.length) + Math.log(Math.log10(totalAge)); +} + +const getPeriodText = (period) => { + let text = "Last 24 hours"; + if (period === "week") { + text = "Last week"; + } + if (period === "month") { + text = "Last month"; + } + return text; +}; + +let postIds = state.postIds ?? null; + +const loader = ( +
+
+); + +if (postIds === null) { + return loader; +} +const initialItems = postIds; + +const jInitialItems = JSON.stringify(initialItems); +if (state.jInitialItems !== jInitialItems) { + // const jIndex = JSON.stringify(index); + // if (jIndex !== state.jIndex) { + State.update({ + jIndex, + jInitialItems, + items: initialItems, + cachedItems: {}, + }); +} + +const makeMoreItems = () => { + State.update({ + displayCount: state.displayCount + addDisplayCount, + }); + if ( + state.items.length - state.displayCount < addDisplayCount * 5 && + !state.loading + ) { + State.update({ loading: true }); + getPostIds(null, state.items.length); + } +}; + +const items = state.items ? state.items.slice(0, state.displayCount) : []; +const renderedItems = items.map(cachedRenderItem); + +const Head = + props.recency == "hot" ? ( +
+
+ + Hottest Posts +
+ +
+ ) : ( + <> + ); + +return ( + <> + {Head} + {state.loading ? loader : null} + {is_edit_or_add_post_transaction ? ( +

+ Post {transaction_method_name == "edit_post" ? "edited" : "added"}{" "} + successfully. Back to{" "} + + feed + +

+ ) : state.items.length > 0 ? ( +
+ state.items.length} + loader={loader} + useWindow={false} + > + {renderedItems} + +
+ ) : ( +

+ No posts{" "} + {props.term || props.tag || props.author ? "matches search" : ""} + {props.recency === "hot" + ? " in " + getPeriodText(state.period).toLowerCase() + : ""} +

+ )} + +); diff --git a/instances/events-committee.near/widget/devhub/entity/post/Panel.jsx b/instances/events-committee.near/widget/devhub/entity/post/Panel.jsx new file mode 100644 index 000000000..0638a5ae8 --- /dev/null +++ b/instances/events-committee.near/widget/devhub/entity/post/Panel.jsx @@ -0,0 +1,742 @@ +////////////////////////////////////////////////////////////////////// +///STOPWORDS////////////////////////////////////////////////////////// +const stopWords = [ + "about", + "above", + "after", + "again", + "against", + "all", + "and", + "any", + "are", + "because", + "been", + "before", + "being", + "below", + "between", + "both", + "but", + "can", + "cannot", + "could", + "did", + "does", + "doing", + "down", + "during", + "each", + "etc", + "few", + "for", + "from", + "further", + "had", + "has", + "have", + "having", + "her", + "hers", + "herself", + "him", + "himself", + "his", + "how", + "into", + "its", + "itself", + "just", + "more", + "most", + "myself", + "nor", + "not", + "now", + "off", + "once", + "only", + "other", + "our", + "ours", + "ourselves", + "out", + "over", + "own", + "same", + "she", + "should", + "some", + "still", + "such", + "than", + "that", + "the", + "their", + "theirs", + "them", + "themselves", + "then", + "there", + "these", + "they", + "this", + "those", + "through", + "too", + "under", + "until", + "very", + "was", + "were", + "what", + "when", + "where", + "which", + "while", + "who", + "whom", + "why", + "will", + "with", + "you", + "your", + "yours", + "yourself", + "yourselves", + "www", + "http", + "com", +]; + +const stopWordsDictionary = {}; +for (let i = 0; i < stopWords.length; i++) { + stopWordsDictionary[stopWords[i]] = true; +} + +function isStopWord(word) { + return stopWordsDictionary.hasOwnProperty(word.toLowerCase()); +} +////////////////////////////////////////////////////////////////////// +///SYNONYMS/////////////////////////////////////////////////////////// +const synonyms = { + ether: "ethereum", + eth: "ethereum", + either: "ethereum", + app: "application", + cryptocyrrency: "crypto", + developerdao: "devdao", + dev: "develop", + doc: "document", + lib: "librari", + saw: "see", + seen: "see", + tweet: "twitter", + paid: "pai", + src: "sourc", +}; + +const applySynonym = (word) => { + if (synonyms.hasOwnProperty(word.toLowerCase())) { + return synonyms[word]; + } + return word; +}; +////////////////////////////////////////////////////////////////////// +///STEMMING/////////////////////////////////////////////////////////// +const step2list = { + ational: "ate", + tional: "tion", + enci: "ence", + anci: "ance", + izer: "ize", + bli: "ble", + alli: "al", + entli: "ent", + eli: "e", + ousli: "ous", + ization: "ize", + ation: "ate", + ator: "ate", + alism: "al", + iveness: "ive", + fulness: "ful", + ousness: "ous", + aliti: "al", + iviti: "ive", + biliti: "ble", + logi: "log", +}; + +/** @type {Record} */ +const step3list = { + icate: "ic", + ative: "", + alize: "al", + iciti: "ic", + ical: "ic", + ful: "", + ness: "", +}; + +const gt0 = /^([^aeiou][^aeiouy]*)?([aeiouy][aeiou]*)([^aeiou][^aeiouy]*)/; +const eq1 = + /^([^aeiou][^aeiouy]*)?([aeiouy][aeiou]*)([^aeiou][^aeiouy]*)([aeiouy][aeiou]*)?$/; +const gt1 = + /^([^aeiou][^aeiouy]*)?(([aeiouy][aeiou]*)([^aeiou][^aeiouy]*)){2,}/; +const vowelInStem = /^([^aeiou][^aeiouy]*)?[aeiouy]/; +const consonantLike = /^([^aeiou][^aeiouy]*)[aeiouy][^aeiouwxy]$/; + +// Exception expressions. +const sfxLl = /ll$/; +const sfxE = /^(.+?)e$/; +const sfxY = /^(.+?)y$/; +const sfxIon = /^(.+?(s|t))(ion)$/; +const sfxEdOrIng = /^(.+?)(ed|ing)$/; +const sfxAtOrBlOrIz = /(at|bl|iz)$/; +const sfxEED = /^(.+?)eed$/; +const sfxS = /^.+?[^s]s$/; +const sfxSsesOrIes = /^.+?(ss|i)es$/; +const sfxMultiConsonantLike = /([^aeiouylsz])\1$/; +const step2 = + /^(.+?)(ational|tional|enci|anci|izer|bli|alli|entli|eli|ousli|ization|ation|ator|alism|iveness|fulness|ousness|aliti|iviti|biliti|logi)$/; +const step3 = /^(.+?)(icate|ative|alize|iciti|ical|ful|ness)$/; +const step4 = + /^(.+?)(al|ance|ence|er|ic|able|ible|ant|ement|ment|ent|ou|ism|ate|iti|ous|ive|ize)$/; + +/** + * Get the stem from a given value. + * + * @param {string} value + * Value to stem. + * @returns {string} + * Stem for `value` + */ +// eslint-disable-next-line complexity +function stemmer(value) { + let result = value.toLowerCase(); + + // Exit early. + if (result.length < 3) { + return result; + } + + /** @type {boolean} */ + let firstCharacterWasLowerCaseY = false; + + // Detect initial `y`, make sure it never matches. + if ( + result.codePointAt(0) === 121 // Lowercase Y + ) { + firstCharacterWasLowerCaseY = true; + result = "Y" + result.slice(1); + } + + // Step 1a. + if (sfxSsesOrIes.test(result)) { + // Remove last two characters. + result = result.slice(0, -2); + } else if (sfxS.test(result)) { + // Remove last character. + result = result.slice(0, -1); + } + + /** @type {RegExpMatchArray|null} */ + let match; + + // Step 1b. + if ((match = sfxEED.exec(result))) { + if (gt0.test(match[1])) { + // Remove last character. + result = result.slice(0, -1); + } + } else if ((match = sfxEdOrIng.exec(result)) && vowelInStem.test(match[1])) { + result = match[1]; + + if (sfxAtOrBlOrIz.test(result)) { + // Append `e`. + result += "e"; + } else if (sfxMultiConsonantLike.test(result)) { + // Remove last character. + result = result.slice(0, -1); + } else if (consonantLike.test(result)) { + // Append `e`. + result += "e"; + } + } + + // Step 1c. + if ((match = sfxY.exec(result)) && vowelInStem.test(match[1])) { + // Remove suffixing `y` and append `i`. + result = match[1] + "i"; + } + + // Step 2. + if ((match = step2.exec(result)) && gt0.test(match[1])) { + result = match[1] + step2list[match[2]]; + } + + // Step 3. + if ((match = step3.exec(result)) && gt0.test(match[1])) { + result = match[1] + step3list[match[2]]; + } + + // Step 4. + if ((match = step4.exec(result))) { + if (gt1.test(match[1])) { + result = match[1]; + } + } else if ((match = sfxIon.exec(result)) && gt1.test(match[1])) { + result = match[1]; + } + + // Step 5. + if ( + (match = sfxE.exec(result)) && + (gt1.test(match[1]) || + (eq1.test(match[1]) && !consonantLike.test(match[1]))) + ) { + result = match[1]; + } + + if (sfxLl.test(result) && gt1.test(result)) { + result = result.slice(0, -1); + } + + // Turn initial `Y` back to `y`. + if (firstCharacterWasLowerCaseY) { + result = "y" + result.slice(1); + } + + return result; +} + +////////////////////////////////////////////////////////////////////// +///SPELLCHECK///////////////////////////////////////////////////////// +function levenshteinDistance(s, t, threshold) { + const BIG_NUMBER = 10000; + if (s == null || t == null) { + return BIG_NUMBER; + } + if (threshold < 0) { + return BIG_NUMBER; + } + let n = s.length; + let m = t.length; + if (Math.abs(n - m) >= threshold) { + return BIG_NUMBER; + } + + // if one string is empty, the edit distance is necessarily the length of the other + if (n == 0) { + return m <= threshold ? m : BIG_NUMBER; + } else if (m == 0) { + return n <= threshold ? n : BIG_NUMBER; + } + + if (n > m) { + // swap the two strings to consume less memory + let temp = s; + s = t; + t = temp; + let tempSize = n; + n = m; + m = tempSize; + } + + let p = Array.from({ length: n + 1 }, () => 0); // 'previous' cost array, horizontally + let d = Array.from({ length: n + 1 }, () => 0); // cost array, horizontally + let _d; // placeholder to assist in swapping p and d + + // fill in starting table values + const boundary = Math.min(n, threshold) + 1; + for (let i = 0; i < boundary; i++) { + p[i] = i; + } + // these fills ensure that the value above the rightmost entry of our + // stripe will be ignored in following loop iterations + for (let i = boundary; i < p.length; i++) { + p[i] = BIG_NUMBER; + } + for (let i = 0; i < d.length; i++) { + d[i] = BIG_NUMBER; + } + + // iterates through t + for (let j = 1; j <= m; j++) { + const t_j = t.charAt(j - 1); // jth character of t + d[0] = j; + + // compute stripe indices, constrain to array size + const min = Math.max(1, j - threshold); + const max = j > BIG_NUMBER - threshold ? n : Math.min(n, j + threshold); + + // the stripe may lead off of the table if s and t are of different sizes + if (min > max) { + return BIG_NUMBER; + } + + // ignore entry left of leftmost + if (min > 1) { + d[min - 1] = BIG_NUMBER; + } + + // iterates through [min, max] in s + for (let i = min; i <= max; i++) { + if (s.charAt(i - 1) == t_j) { + // diagonally left and up + d[i] = p[i - 1]; + } else { + // 1 + minimum of cell to the left, to the top, diagonally left and up + d[i] = 1 + Math.min(Math.min(d[i - 1], p[i]), p[i - 1]); + } + } + + // copy current distance counts to 'previous row' distance counts + _d = p; + p = d; + d = _d; + } + // we don't need to check for threshold here because we did it inside the loop + return p[n] <= threshold ? p[n] : BIG_NUMBER; +} + +const spellcheckQueryProcessing = (query, dictionary) => { + // Split text document into words + const words = stemAndFilterQuery(query); + const dictionaryArray = Object.keys(dictionary); + // Iterate over each word in the text + for (let i = 0; i < words.length; i++) { + let word = words[i].toLowerCase().replace(/[^a-z0-9]/g, ""); + + // If the word is not in the dictionary, find the closest match + if (!dictionary.hasOwnProperty(word)) { + let closestMatch = undefined; + let closestDistance = word.length; + let allowedDistance = Math.min(word.length - 1, 3); + // Iterate over each word in the dictionary + if (word.length > 1) { + for (let j = 0; j < dictionaryArray.length; j++) { + let dictWord = dictionaryArray[j]; + let distance = levenshteinDistance(word, dictWord, allowedDistance); + + // If the distance is less than the closest distance, update the closest match + if (distance <= allowedDistance && distance < closestDistance) { + closestMatch = dictWord; + closestDistance = distance; + } + } + } + // Replace the misspelled word with the closest match + words[i] = closestMatch; + } + } + return words.filter((word) => !!word); +}; + +////////////////////////////////////////////////////////////////////// +///INDEXER&SEARCH///////////////////////////////////////////////////// +const fillDictionaryWith = (dict, text, id) => { + let word = ""; + for (let i = 0; i < text.length; i++) { + const char = text.charAt(i); + const nextChar = text.charAt(i + 1); + if (/\w/.test(char) || (char === "." && /\w/.test(nextChar))) { + word += char.toLowerCase(); + } else if (word.length > 0) { + const processedWord = applySynonym(stemmer(word)); + if (processedWord.length > 1 && !isStopWord(processedWord)) { + const oldValue = dict[processedWord] || []; + dict[processedWord] = [...oldValue, id]; + } + word = ""; + } + } + const processedWord = applySynonym(stemmer(word)); + if (processedWord.length > 1 && !isStopWord(processedWord)) { + const oldValue = dict[stemmer(processedWord)] || []; + dict[stemmer(processedWord)] = [...oldValue, id]; + } + return dict; +}; + +const buildIndex = (posts) => { + let index = {}; + + posts.forEach((post) => { + const title = post.snapshot.name; + const labels = post.snapshot.labels.join(" "); + const text = post.snapshot.description; + const postType = post.snapshot.post_type; + const authorId = post.author_id; + const postText = `${authorId} ${postType} ${title} ${labels} ${text}`; + index = fillDictionaryWith(index, postText, post.id); + }); + return index; +}; + +const stemAndFilterQuery = (query) => { + return Object.keys(fillDictionaryWith({}, query)); +}; + +const sortSearchResult = (searchResult) => { + // create a map to count the frequency of each element + const freq = new Map(); + for (const num of searchResult) { + freq.set(num, (freq.get(num) || 0) + 1); + } + + // define a custom comparison function to sort the array + function compare(a, b) { + // compare the frequency of the two elements + const freqDiff = freq.get(b) - freq.get(a); + if (freqDiff !== 0) { + return freqDiff; // if they have different frequency, sort by frequency + } else { + return 0; // if they have the same frequency, leave as it is. Will be sorted by search term, by date + } + } + + // sort the array using the custom comparison function + searchResult.sort(compare); + return searchResult.filter( + (elem, index) => searchResult.indexOf(elem) === index + ); +}; + +const search = (processedQueryArray, index) => { + return sortSearchResult( + processedQueryArray.flatMap((queryWord) => { + const termSearchRes = index[queryWord].reverse(); + const termSortedSearchRes = sortSearchResult(termSearchRes); + return termSortedSearchRes; + }) + ); +}; + +////////////////////////////////////////////////////////////////////// +///UI&UX////////////////////////////////////////////////////////////// +//Run search and spelling computation every time the search bar modified +//but no more frequent than 1 time per 1.5 seconds +const amountOfResultsToShowFirst = 5; + +const buildPostsIndex = () => { + return Near.asyncView("${REPL_DEVHUB_LEGACY}", "get_posts").then((posts) => { + const index = buildIndex(posts); + const data = posts.reduce((acc, post) => { + acc[post.id] = post; + return acc; + }, {}); + return { index, data }; + }); +}; + +const getProcessedPostsCached = () => { + return useCache(() => buildPostsIndex(), "processedPostsCached"); +}; + +if (!state.interval) { + let termStorage = ""; + Storage.privateSet("term", ""); + setInterval(() => { + const currentInput = Storage.privateGet("term"); + if (currentInput !== termStorage) { + termStorage = currentInput; + computeResults(termStorage); + } + }, 1500); + State.update({ + interval: true, + }); +} + +const computeResults = (term) => { + const start = new Date().getTime(); + const processedPostsCached = useCache( + () => + buildPostsIndex().then((processedPosts) => { + // Run query first time posts retrieved + const query = term; + const processedQuery = spellcheckQueryProcessing( + query, + processedPosts.index + ); + const searchResult = search(processedQuery, processedPosts.index); + console.log(processedQuery); + console.log(searchResult); + State.update({ + searchResult, + shownSearchResults: searchResult.slice(0, amountOfResultsToShowFirst), + processedQuery, + loading: false, + }); + return processedPosts; + }), + "processedPostsCached" + ); + if (processedPostsCached) { + // Run query every other time after data retrieved and cached + const query = term; + const processedQuery = spellcheckQueryProcessing( + query, + processedPostsCached.index + ); + const searchResult = search(processedQuery, processedPostsCached.index); + console.log(processedQuery); + console.log(searchResult); + State.update({ + searchResult, + shownSearchResults: searchResult.slice(0, 10), + processedQuery, + loading: false, + }); + } + const end = new Date().getTime(); + console.log("search time: ", end - start); +}; + +const updateInput = (term) => { + Storage.privateSet("term", term); + State.update({ + term, + loading: true, + }); +}; + +const getSearchResultsKeywordsFor = (postId) => { + const index = getProcessedPostsCached().index; + return state.processedQuery.filter((queryWord) => { + return index[queryWord].includes(postId); + }); +}; + +const showMoreSearchResults = () => { + const shownSearchResults = state.shownSearchResults || []; + const newShownSearchResults = state.searchResult.slice( + 0, + shownSearchResults.length + amountOfResultsToShowFirst + ); + State.update({ shownSearchResults: newShownSearchResults }); +}; + +return ( + <> +
+
+
+ {state.loading ? ( +
+ updateInput(e.target.value)} + placeholder={props.placeholder ?? `Search Posts`} + /> +
+ + +
+ +
+
+ {props.children} +
+
+ {state.processedQuery && + state.processedQuery.length > 0 && + state.term.toLowerCase().trim() !== state.processedQuery.join(" ") && ( +
+ Looking for + {state.processedQuery.join(" ")}: +
+ )} + {state.term && state.term.length > 1 && state.searchResult ? ( + { + return [postId, getSearchResultsKeywordsFor(postId)]; + }) + ), + }, + recency: props.recency, + tag: props.tag, + author: props.author, + }} + key={key} + /> + ) : ( + + )} + +); diff --git a/instances/events-committee.near/widget/devhub/entity/post/Post.jsx b/instances/events-committee.near/widget/devhub/entity/post/Post.jsx new file mode 100644 index 000000000..8c0234d70 --- /dev/null +++ b/instances/events-committee.near/widget/devhub/entity/post/Post.jsx @@ -0,0 +1,924 @@ +// Ideally, this would be a page + +const { href } = VM.require("${REPL_DEVHUB}/widget/core.lib.url"); +const { getDepositAmountForWriteAccess } = VM.require( + "${REPL_DEVHUB}/widget/core.lib.common" +); + +getDepositAmountForWriteAccess || (getDepositAmountForWriteAccess = () => {}); +const { draftState, onDraftStateChange } = VM.require( + "${REPL_DEVHUB}/widget/devhub.entity.post.draft" +); + +if (!href) { + return

Loading modules...

; +} + +const ButtonWithHover = styled.button` + background-color: #fff; + transition: all 300ms; + border-radius: 0.5rem; + + &:hover { + background-color: #e9ecef; + color: #000; + } + + &:disabled { + background-color: #fff; + color: #b7b7b7; + } +`; + +const LikeLoadingSpinner = ( +