From caef51a2d208101533927f79f90d00fe70c23e1d Mon Sep 17 00:00:00 2001 From: Adetoye Adewoye Date: Tue, 7 Oct 2025 16:29:35 +0100 Subject: [PATCH 01/25] update sidebar ui --- public/icons/user-group.svg | 8 +++ src/app/layout.tsx | 2 +- src/components/Header/AppSidebar.tsx | 71 ++++++++++++------------ src/components/Header/Header.module.scss | 2 +- src/global/globals.scss | 2 +- src/global/themeColors.ts | 14 +++-- tailwind.config.ts | 2 +- 7 files changed, 58 insertions(+), 43 deletions(-) create mode 100644 public/icons/user-group.svg diff --git a/public/icons/user-group.svg b/public/icons/user-group.svg new file mode 100644 index 0000000..64d2d2c --- /dev/null +++ b/public/icons/user-group.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/src/app/layout.tsx b/src/app/layout.tsx index 8654cb4..f772a7c 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -24,7 +24,7 @@ export default function RootLayout({ children, modal }: { children: ReactNode; m
-
+
diff --git a/src/components/Header/AppSidebar.tsx b/src/components/Header/AppSidebar.tsx index 8832859..ab87be3 100644 --- a/src/components/Header/AppSidebar.tsx +++ b/src/components/Header/AppSidebar.tsx @@ -10,27 +10,18 @@ import Image from 'next/image'; import { Listbox, ListboxItem } from '@nextui-org/listbox'; import { usePathname, useRouter } from 'next/navigation'; import { useApiContext, useUserDetailsContext } from '@/contexts'; -import dynamic from 'next/dynamic'; +// import dynamic from 'next/dynamic'; import styles from './Header.module.scss'; import LinkWithNetwork from '../Misc/LinkWithNetwork'; -const JoinFellowshipButton = dynamic(() => import('./JoinFellowshipButton'), { ssr: false }); +// const JoinFellowshipButton = dynamic(() => import('./JoinFellowshipButton'), { ssr: false }); function ListboxItemStartContent({ isParentItem = false, isCurrentRoute, icon }: { isParentItem: boolean; isCurrentRoute: boolean; icon?: string }) { return ( - - {isCurrentRoute && !isParentItem && ( - border-image - )} + {icon && ( icon - Login Icon
- +
+
+
+ Collectives +
+

Collectives

+
+
+ Governance by +
+ Polkassembly +
+
+
+ {/* */} {loginAddress && fellows.map((fellow) => fellow.address).includes(loginAddress) && ( { @@ -187,9 +194,11 @@ function AppSidebar() { return ( {navItem.label} ) : ( {navItem.label} @@ -217,12 +226,6 @@ function AppSidebar() {
- Polkassembly Logo
+
+ +
diff --git a/src/components/Header/RightSidebar/QuickActions.tsx b/src/components/Header/RightSidebar/QuickActions.tsx new file mode 100644 index 0000000..84843b4 --- /dev/null +++ b/src/components/Header/RightSidebar/QuickActions.tsx @@ -0,0 +1,50 @@ +// Copyright 2019-2025 @polkassembly/fellowship authors & contributors +// This software may be modified and distributed under the terms +// of the Apache-2.0 license. See the LICENSE file for details. + +'use client'; + +import React from 'react'; +import { FileText, DollarSign, UserPlus, Plus } from 'lucide-react'; +import { Button } from '@nextui-org/button'; + +export default function QuickActions() { + return ( +
+

Quick Actions

+
+ + + + + + + +
+
+ ); +} diff --git a/src/components/Header/RightSidebar/RightSidebar.tsx b/src/components/Header/RightSidebar/RightSidebar.tsx new file mode 100644 index 0000000..e12da42 --- /dev/null +++ b/src/components/Header/RightSidebar/RightSidebar.tsx @@ -0,0 +1,55 @@ +// Copyright 2019-2025 @polkassembly/fellowship authors & contributors +// This software may be modified and distributed under the terms +// of the Apache-2.0 license. See the LICENSE file for details. + +'use client'; + +import React, { useMemo } from 'react'; +import { useApiContext, useUserDetailsContext } from '@/contexts'; +import { UserPlus } from 'lucide-react'; +import { Button } from '@nextui-org/button'; +import LinkWithNetwork from '../../Misc/LinkWithNetwork'; +import getSubstrateAddress from '@/utils/getSubstrateAddress'; +import QuickActions from './QuickActions'; +// import Treasury from './Treasury'; +// import ActivityFeed from './ActivityFeed'; + +export default function RightSidebar() { + const { fellows } = useApiContext(); + const { loginAddress } = useUserDetailsContext(); + + // Check if current user is a fellow + const isFellow = useMemo(() => { + if (!loginAddress || !fellows?.length) return false; + const substrateAddress = getSubstrateAddress(loginAddress); + return fellows.find((f: any) => f.address === substrateAddress) !== undefined; + }, [loginAddress, fellows]); + + return ( + + ); +} diff --git a/src/global/globals.scss b/src/global/globals.scss index c0b0bb6..be8a345 100644 --- a/src/global/globals.scss +++ b/src/global/globals.scss @@ -15,7 +15,7 @@ } #main-section { - @apply w-full max-w-[1208px] lg:ml-[296px]; + @apply w-full max-w-[1208px] lg:ml-[296px] lg:mr-[332px]; } ::-webkit-scrollbar { diff --git a/yarn.lock b/yarn.lock index 0ebb673..6f803d7 100644 --- a/yarn.lock +++ b/yarn.lock @@ -570,6 +570,20 @@ "@react-types/overlays" "3.8.11" "@react-types/shared" "3.26.0" +"@nextui-org/aria-utils@2.2.7": + version "2.2.7" + resolved "https://registry.yarnpkg.com/@nextui-org/aria-utils/-/aria-utils-2.2.7.tgz#38b2ce13e652d78f874311c74f6c72c6d282ddf7" + integrity sha512-QgMZ8fii6BCI/+ZIkgXgkm/gMNQ92pQJn83q90fBT6DF+6j4hsCpJwLNCF5mIJkX/cQ/4bHDsDaj7w1OzkhQNg== + dependencies: + "@nextui-org/react-rsc-utils" "2.1.1" + "@nextui-org/shared-utils" "2.1.2" + "@nextui-org/system" "2.4.6" + "@react-aria/utils" "3.26.0" + "@react-stately/collections" "3.12.0" + "@react-stately/overlays" "3.6.12" + "@react-types/overlays" "3.8.11" + "@react-types/shared" "3.26.0" + "@nextui-org/avatar@^2.0.21": version "2.2.4" resolved "https://registry.npmjs.org/@nextui-org/avatar/-/avatar-2.2.4.tgz" @@ -582,6 +596,14 @@ "@react-aria/interactions" "3.22.5" "@react-aria/utils" "3.26.0" +"@nextui-org/badge@^2.2.5": + version "2.2.5" + resolved "https://registry.yarnpkg.com/@nextui-org/badge/-/badge-2.2.5.tgz#999ee8e2e42ff6dd40315d128cddfa7f2e0ce6fe" + integrity sha512-8pLbuY+RVCzI/00CzNudc86BiuXByPFz2yHh00djKvZAXbT0lfjvswClJxSC2FjUXlod+NtE+eHmlhSMo3gmpw== + dependencies: + "@nextui-org/react-utils" "2.1.3" + "@nextui-org/shared-utils" "2.1.2" + "@nextui-org/button@2.2.7", "@nextui-org/button@^2.0.21": version "2.2.7" resolved "https://registry.npmjs.org/@nextui-org/button/-/button-2.2.7.tgz" @@ -599,6 +621,23 @@ "@react-types/button" "3.10.1" "@react-types/shared" "3.26.0" +"@nextui-org/button@2.2.9": + version "2.2.9" + resolved "https://registry.yarnpkg.com/@nextui-org/button/-/button-2.2.9.tgz#c0205fa89ea2daceebba7fc6311e8de47c31d4c9" + integrity sha512-RrfjAZHoc6nmaqoLj40M0Qj3tuDdv2BMGCgggyWklOi6lKwtOaADPvxEorDwY3GnN54Xej+9SWtUwE8Oc3SnOg== + dependencies: + "@nextui-org/react-utils" "2.1.3" + "@nextui-org/ripple" "2.2.7" + "@nextui-org/shared-utils" "2.1.2" + "@nextui-org/spinner" "2.2.6" + "@nextui-org/use-aria-button" "2.2.4" + "@react-aria/button" "3.11.0" + "@react-aria/focus" "3.19.0" + "@react-aria/interactions" "3.22.5" + "@react-aria/utils" "3.26.0" + "@react-types/button" "3.10.1" + "@react-types/shared" "3.26.0" + "@nextui-org/calendar@^2.0.7": version "2.2.7" resolved "https://registry.npmjs.org/@nextui-org/calendar/-/calendar-2.2.7.tgz" @@ -684,6 +723,16 @@ "@nextui-org/system-rsc" "2.3.4" "@react-types/shared" "3.26.0" +"@nextui-org/divider@2.2.5": + version "2.2.5" + resolved "https://registry.yarnpkg.com/@nextui-org/divider/-/divider-2.2.5.tgz#d4531f70ebdfb534f9c9379e9585bffee64a464e" + integrity sha512-OB8b3CU4nQ5ARIGL48izhzrAHR0mnwws+Kd5LqRCZ/1R9uRMqsq7L0gpG9FkuV2jf2FuA7xa/GLOLKbIl4CEww== + dependencies: + "@nextui-org/react-rsc-utils" "2.1.1" + "@nextui-org/shared-utils" "2.1.2" + "@nextui-org/system-rsc" "2.3.5" + "@react-types/shared" "3.26.0" + "@nextui-org/dom-animation@2.1.1": version "2.1.1" resolved "https://registry.npmjs.org/@nextui-org/dom-animation/-/dom-animation-2.1.1.tgz" @@ -720,6 +769,20 @@ "@react-types/form" "3.7.8" "@react-types/shared" "3.26.0" +"@nextui-org/form@2.1.8": + version "2.1.8" + resolved "https://registry.yarnpkg.com/@nextui-org/form/-/form-2.1.8.tgz#a29bca617c124b852740d37a9027285f266e267b" + integrity sha512-Xn/dUO5zDG7zukbql1MDYh4Xwe1vnIVMRTHgckbkBtXXVNqgoTU09TTfy8WOJ0pMDX4GrZSBAZ86o37O+IHbaA== + dependencies: + "@nextui-org/react-utils" "2.1.3" + "@nextui-org/shared-utils" "2.1.2" + "@nextui-org/system" "2.4.6" + "@nextui-org/theme" "2.4.5" + "@react-aria/utils" "3.26.0" + "@react-stately/form" "3.1.0" + "@react-types/form" "3.7.8" + "@react-types/shared" "3.26.0" + "@nextui-org/framer-utils@2.1.4": version "2.1.4" resolved "https://registry.npmjs.org/@nextui-org/framer-utils/-/framer-utils-2.1.4.tgz" @@ -729,6 +792,15 @@ "@nextui-org/system" "2.4.4" "@nextui-org/use-measure" "2.1.1" +"@nextui-org/framer-utils@2.1.6": + version "2.1.6" + resolved "https://registry.yarnpkg.com/@nextui-org/framer-utils/-/framer-utils-2.1.6.tgz#425a25a85e1e5712c68d1018420b1ca231448dd3" + integrity sha512-b+BxKFox8j9rNAaL+CRe2ZMb1/SKjz9Kl2eLjDSsq3q82K/Hg7lEjlpgE8cu41wIGjH1unQxtP+btiJgl067Ow== + dependencies: + "@nextui-org/shared-utils" "2.1.2" + "@nextui-org/system" "2.4.6" + "@nextui-org/use-measure" "2.1.1" + "@nextui-org/input@^2.1.9": version "2.4.6" resolved "https://registry.npmjs.org/@nextui-org/input/-/input-2.4.6.tgz" @@ -748,6 +820,25 @@ "@react-types/textfield" "3.10.0" react-textarea-autosize "^8.5.3" +"@nextui-org/listbox@2.3.9": + version "2.3.9" + resolved "https://registry.yarnpkg.com/@nextui-org/listbox/-/listbox-2.3.9.tgz#181969b3adf4e8edbb0c3ef7430223da616434ba" + integrity sha512-iGJ8xwkXf8K7chk1iZgC05KGpHiWJXY1dnV7ytIJ7yu4BbsRIHb0QknK5j8A74YeGpouJQ9+jsmCERmySxlqlg== + dependencies: + "@nextui-org/aria-utils" "2.2.7" + "@nextui-org/divider" "2.2.5" + "@nextui-org/react-utils" "2.1.3" + "@nextui-org/shared-utils" "2.1.2" + "@nextui-org/use-is-mobile" "2.2.2" + "@react-aria/focus" "3.19.0" + "@react-aria/interactions" "3.22.5" + "@react-aria/listbox" "3.13.6" + "@react-aria/utils" "3.26.0" + "@react-stately/list" "3.11.1" + "@react-types/menu" "3.9.13" + "@react-types/shared" "3.26.0" + "@tanstack/react-virtual" "3.11.2" + "@nextui-org/listbox@^2.1.10": version "2.3.7" resolved "https://registry.npmjs.org/@nextui-org/listbox/-/listbox-2.3.7.tgz" @@ -846,6 +937,28 @@ "@react-types/button" "3.10.1" "@react-types/overlays" "3.8.11" +"@nextui-org/popover@2.3.9": + version "2.3.9" + resolved "https://registry.yarnpkg.com/@nextui-org/popover/-/popover-2.3.9.tgz#d29d7c1804ec56bcc96bc0a9b09248c5f2722538" + integrity sha512-glLYKlFJ4EkFrNMBC3ediFPpQwKzaFlzKoaMum2G3HUtmC4d1HLTSOQJOd2scUzZxD3/K9dp1XHYbEcCnCrYpQ== + dependencies: + "@nextui-org/aria-utils" "2.2.7" + "@nextui-org/button" "2.2.9" + "@nextui-org/dom-animation" "2.1.1" + "@nextui-org/framer-utils" "2.1.6" + "@nextui-org/react-utils" "2.1.3" + "@nextui-org/shared-utils" "2.1.2" + "@nextui-org/use-aria-button" "2.2.4" + "@nextui-org/use-safe-layout-effect" "2.1.1" + "@react-aria/dialog" "3.5.20" + "@react-aria/focus" "3.19.0" + "@react-aria/interactions" "3.22.5" + "@react-aria/overlays" "3.24.0" + "@react-aria/utils" "3.26.0" + "@react-stately/overlays" "3.6.12" + "@react-types/button" "3.10.1" + "@react-types/overlays" "3.8.11" + "@nextui-org/progress@^2.0.24": version "2.2.4" resolved "https://registry.npmjs.org/@nextui-org/progress/-/progress-2.2.4.tgz" @@ -889,6 +1002,14 @@ "@nextui-org/react-rsc-utils" "2.1.1" "@nextui-org/shared-utils" "2.1.1" +"@nextui-org/react-utils@2.1.3": + version "2.1.3" + resolved "https://registry.yarnpkg.com/@nextui-org/react-utils/-/react-utils-2.1.3.tgz#444f1b8b4f4f50efdb72abd201660c908a46ba88" + integrity sha512-o61fOS+S8p3KtgLLN7ub5gR0y7l517l9eZXJabUdnVcZzZjTqEijWjzjIIIyAtYAlL4d+WTXEOROuc32sCmbqw== + dependencies: + "@nextui-org/react-rsc-utils" "2.1.1" + "@nextui-org/shared-utils" "2.1.2" + "@nextui-org/ripple@2.2.5": version "2.2.5" resolved "https://registry.npmjs.org/@nextui-org/ripple/-/ripple-2.2.5.tgz" @@ -898,6 +1019,24 @@ "@nextui-org/react-utils" "2.1.1" "@nextui-org/shared-utils" "2.1.1" +"@nextui-org/ripple@2.2.7": + version "2.2.7" + resolved "https://registry.yarnpkg.com/@nextui-org/ripple/-/ripple-2.2.7.tgz#69a3ebc48f9eb1c34982e33c69c511e741489f43" + integrity sha512-cphzlvCjdROh1JWQhO/wAsmBdlU9kv/UA2YRQS4viaWcA3zO+qOZVZ9/YZMan6LBlOLENCaE9CtV2qlzFtVpEg== + dependencies: + "@nextui-org/dom-animation" "2.1.1" + "@nextui-org/react-utils" "2.1.3" + "@nextui-org/shared-utils" "2.1.2" + +"@nextui-org/scroll-shadow@2.3.5": + version "2.3.5" + resolved "https://registry.yarnpkg.com/@nextui-org/scroll-shadow/-/scroll-shadow-2.3.5.tgz#18260dd8b5d6d9fcaa8486552dad940ad393c786" + integrity sha512-2H5qro6RHcWo6ZfcG2hHZHsR1LrV3FMZP5Lkc9ZwJdWPg4dXY4erGRE4U+B7me6efj5tBOFmZkIpxVUyMBLtZg== + dependencies: + "@nextui-org/react-utils" "2.1.3" + "@nextui-org/shared-utils" "2.1.2" + "@nextui-org/use-data-scroll-overflow" "2.2.2" + "@nextui-org/scroll-shadow@^2.1.12": version "2.3.3" resolved "https://registry.npmjs.org/@nextui-org/scroll-shadow/-/scroll-shadow-2.3.3.tgz" @@ -907,6 +1046,31 @@ "@nextui-org/shared-utils" "2.1.1" "@nextui-org/use-data-scroll-overflow" "2.2.1" +"@nextui-org/select@^2.4.9": + version "2.4.9" + resolved "https://registry.yarnpkg.com/@nextui-org/select/-/select-2.4.9.tgz#7a220c0a0f090343fda37bc6ac229c3130c36c39" + integrity sha512-R8HHKDH7dA4Dv73Pl80X7qfqdyl+Fw4gi/9bmyby0QJG8LN2zu51xyjjKphmWVkAiE3O35BRVw7vMptHnWFUgQ== + dependencies: + "@nextui-org/aria-utils" "2.2.7" + "@nextui-org/form" "2.1.8" + "@nextui-org/listbox" "2.3.9" + "@nextui-org/popover" "2.3.9" + "@nextui-org/react-utils" "2.1.3" + "@nextui-org/scroll-shadow" "2.3.5" + "@nextui-org/shared-icons" "2.1.1" + "@nextui-org/shared-utils" "2.1.2" + "@nextui-org/spinner" "2.2.6" + "@nextui-org/use-aria-button" "2.2.4" + "@nextui-org/use-aria-multiselect" "2.4.3" + "@nextui-org/use-safe-layout-effect" "2.1.1" + "@react-aria/focus" "3.19.0" + "@react-aria/form" "3.0.11" + "@react-aria/interactions" "3.22.5" + "@react-aria/utils" "3.26.0" + "@react-aria/visually-hidden" "3.8.18" + "@react-types/shared" "3.26.0" + "@tanstack/react-virtual" "3.11.2" + "@nextui-org/shared-icons@2.1.1": version "2.1.1" resolved "https://registry.npmjs.org/@nextui-org/shared-icons/-/shared-icons-2.1.1.tgz" @@ -917,6 +1081,11 @@ resolved "https://registry.npmjs.org/@nextui-org/shared-utils/-/shared-utils-2.1.1.tgz" integrity sha512-qE8gZO63GqUX1ljOi/4PlwGzE84dhUS3zFIq+10/N6ePAaNjM4DwtL4ocucG3abCz4iRUueYKLIxTO2+eYyAfw== +"@nextui-org/shared-utils@2.1.2": + version "2.1.2" + resolved "https://registry.yarnpkg.com/@nextui-org/shared-utils/-/shared-utils-2.1.2.tgz#ab35095d17a2fd53afa29558e03464c2a6b89466" + integrity sha512-5n0D+AGB4P9lMD1TxwtdRSuSY0cWgyXKO9mMU11Xl3zoHNiAz/SbCSTc4VBJdQJ7Y3qgNXvZICzf08+bnjjqqA== + "@nextui-org/skeleton@^2.0.22": version "2.2.3" resolved "https://registry.npmjs.org/@nextui-org/skeleton/-/skeleton-2.2.3.tgz" @@ -943,6 +1112,15 @@ "@nextui-org/shared-utils" "2.1.1" "@nextui-org/system-rsc" "2.3.4" +"@nextui-org/spinner@2.2.6": + version "2.2.6" + resolved "https://registry.yarnpkg.com/@nextui-org/spinner/-/spinner-2.2.6.tgz#df9db9fc0a00196f4990aca54771b101561135f0" + integrity sha512-0V0H8jVpgRolgLnCuKDbrQCSK0VFPAZYiyGOE1+dfyIezpta+Nglh+uEl2sEFNh6B9Z8mARB8YEpRnTcA0ePDw== + dependencies: + "@nextui-org/react-utils" "2.1.3" + "@nextui-org/shared-utils" "2.1.2" + "@nextui-org/system-rsc" "2.3.5" + "@nextui-org/switch@^2.0.33": version "2.2.6" resolved "https://registry.npmjs.org/@nextui-org/switch/-/switch-2.2.6.tgz" @@ -967,6 +1145,14 @@ "@react-types/shared" "3.26.0" clsx "^1.2.1" +"@nextui-org/system-rsc@2.3.5": + version "2.3.5" + resolved "https://registry.yarnpkg.com/@nextui-org/system-rsc/-/system-rsc-2.3.5.tgz#f1beadfa7ce8d7c5f99298126adccbccf08f698e" + integrity sha512-DpVLNV9LkeP1yDULFCXm2mxA9m4ygS7XYy3lwgcF9M1A8QAWB+ut+FcP+8a6va50oSHOqwvUwPDUslgXTPMBfQ== + dependencies: + "@react-types/shared" "3.26.0" + clsx "^1.2.1" + "@nextui-org/system@2.4.4", "@nextui-org/system@^2.2.2": version "2.4.4" resolved "https://registry.npmjs.org/@nextui-org/system/-/system-2.4.4.tgz" @@ -981,6 +1167,20 @@ "@react-stately/utils" "3.10.5" "@react-types/datepicker" "3.9.0" +"@nextui-org/system@2.4.6": + version "2.4.6" + resolved "https://registry.yarnpkg.com/@nextui-org/system/-/system-2.4.6.tgz#1af5b9001131cdda23c4fca0d3ec6139cd763cb9" + integrity sha512-6ujAriBZMfQ16n6M6Ad9g32KJUa1CzqIVaHN/tymadr/3m8hrr7xDw6z50pVjpCRq2PaaA1hT8Hx7EFU3f2z3Q== + dependencies: + "@internationalized/date" "3.6.0" + "@nextui-org/react-utils" "2.1.3" + "@nextui-org/system-rsc" "2.3.5" + "@react-aria/i18n" "3.12.4" + "@react-aria/overlays" "3.24.0" + "@react-aria/utils" "3.26.0" + "@react-stately/utils" "3.10.5" + "@react-types/datepicker" "3.9.0" + "@nextui-org/table@^2.0.28": version "2.2.6" resolved "https://registry.npmjs.org/@nextui-org/table/-/table-2.2.6.tgz" @@ -1035,6 +1235,20 @@ tailwind-merge "^2.5.2" tailwind-variants "^0.1.20" +"@nextui-org/theme@2.4.5": + version "2.4.5" + resolved "https://registry.yarnpkg.com/@nextui-org/theme/-/theme-2.4.5.tgz#12668ea604e721563255382436f8f32d57427ee1" + integrity sha512-c7Y17n+hBGiFedxMKfg7Qyv93iY5MteamLXV4Po4c1VF1qZJI6I+IKULFh3FxPWzAoz96r6NdYT7OLFjrAJdWg== + dependencies: + "@nextui-org/shared-utils" "2.1.2" + clsx "^1.2.1" + color "^4.2.3" + color2k "^2.0.2" + deepmerge "4.3.1" + flat "^5.0.2" + tailwind-merge "^2.5.2" + tailwind-variants "^0.1.20" + "@nextui-org/tooltip@^2.0.24": version "2.2.5" resolved "https://registry.npmjs.org/@nextui-org/tooltip/-/tooltip-2.2.5.tgz" @@ -1079,6 +1293,18 @@ "@react-types/button" "3.10.1" "@react-types/shared" "3.26.0" +"@nextui-org/use-aria-button@2.2.4": + version "2.2.4" + resolved "https://registry.yarnpkg.com/@nextui-org/use-aria-button/-/use-aria-button-2.2.4.tgz#9237999b277ea68c4b7132ea16a992ea89a6cf71" + integrity sha512-Bz8l4JGzRKh6V58VX8Laq4rKZDppsnVuNCBHpMJuLo2F9ht7UKvZAEJwXcdbUZ87aui/ZC+IPYqgjvT+d8QlQg== + dependencies: + "@nextui-org/shared-utils" "2.1.2" + "@react-aria/focus" "3.19.0" + "@react-aria/interactions" "3.22.5" + "@react-aria/utils" "3.26.0" + "@react-types/button" "3.10.1" + "@react-types/shared" "3.26.0" + "@nextui-org/use-aria-modal-overlay@2.2.3": version "2.2.3" resolved "https://registry.npmjs.org/@nextui-org/use-aria-modal-overlay/-/use-aria-modal-overlay-2.2.3.tgz" @@ -1089,6 +1315,26 @@ "@react-stately/overlays" "3.6.12" "@react-types/shared" "3.26.0" +"@nextui-org/use-aria-multiselect@2.4.3": + version "2.4.3" + resolved "https://registry.yarnpkg.com/@nextui-org/use-aria-multiselect/-/use-aria-multiselect-2.4.3.tgz#3406607644c49398dcfe86d5693ceddef81a6463" + integrity sha512-PwDA4Y5DOx0SMxc277JeZi8tMtaINTwthPhk8SaDrtOBhP+r9owS3T/W9t37xKnmrTerHwaEq4ADGQtm5/VMXQ== + dependencies: + "@react-aria/i18n" "3.12.4" + "@react-aria/interactions" "3.22.5" + "@react-aria/label" "3.7.13" + "@react-aria/listbox" "3.13.6" + "@react-aria/menu" "3.16.0" + "@react-aria/selection" "3.21.0" + "@react-aria/utils" "3.26.0" + "@react-stately/form" "3.1.0" + "@react-stately/list" "3.11.1" + "@react-stately/menu" "3.9.0" + "@react-types/button" "3.10.1" + "@react-types/overlays" "3.8.11" + "@react-types/select" "3.9.8" + "@react-types/shared" "3.26.0" + "@nextui-org/use-callback-ref@2.1.1": version "2.1.1" resolved "https://registry.npmjs.org/@nextui-org/use-callback-ref/-/use-callback-ref-2.1.1.tgz" @@ -1103,6 +1349,13 @@ dependencies: "@nextui-org/shared-utils" "2.1.1" +"@nextui-org/use-data-scroll-overflow@2.2.2": + version "2.2.2" + resolved "https://registry.yarnpkg.com/@nextui-org/use-data-scroll-overflow/-/use-data-scroll-overflow-2.2.2.tgz#5ea3cb0c8771e537bdded68c2f375764c77a72f0" + integrity sha512-TFB6BuaLOsE++K1UEIPR9StkBgj9Cvvc+ccETYpmn62B7pK44DmxjkwhK0ei59wafJPIyytZ3DgdVDblfSyIXA== + dependencies: + "@nextui-org/shared-utils" "2.1.2" + "@nextui-org/use-disclosure@2.2.2": version "2.2.2" resolved "https://registry.npmjs.org/@nextui-org/use-disclosure/-/use-disclosure-2.2.2.tgz" @@ -2121,7 +2374,7 @@ "@swc/helpers" "^0.5.0" clsx "^2.0.0" -"@react-aria/form@^3.0.11": +"@react-aria/form@3.0.11", "@react-aria/form@^3.0.11": version "3.0.11" resolved "https://registry.npmjs.org/@react-aria/form/-/form-3.0.11.tgz" integrity sha512-oXzjTiwVuuWjZ8muU0hp3BrDH5qjVctLOF50mjPvqUbvXQTHhoDxWweyIXPQjGshaqBd2w4pWaE4A2rG2O/apw== @@ -2175,7 +2428,7 @@ "@react-types/shared" "^3.26.0" "@swc/helpers" "^0.5.0" -"@react-aria/label@^3.7.13": +"@react-aria/label@3.7.13", "@react-aria/label@^3.7.13": version "3.7.13" resolved "https://registry.npmjs.org/@react-aria/label/-/label-3.7.13.tgz" integrity sha512-brSAXZVTey5RG/Ex6mTrV/9IhGSQFU4Al34qmjEDho+Z2qT4oPwf8k7TRXWWqzOU0ugYxekYbsLd2zlN3XvWcg== @@ -2730,6 +2983,13 @@ dependencies: "@react-types/shared" "^3.26.0" +"@react-types/select@3.9.8": + version "3.9.8" + resolved "https://registry.yarnpkg.com/@react-types/select/-/select-3.9.8.tgz#2443b82549b65821f85876a5b803e6d04ae6343e" + integrity sha512-RGsYj2oFjXpLnfcvWMBQnkcDuKkwT43xwYWZGI214/gp/B64tJiIUgTM5wFTRAeGDX23EePkhCQF+9ctnqFd6g== + dependencies: + "@react-types/shared" "^3.26.0" + "@react-types/shared@3.26.0", "@react-types/shared@^3.26.0": version "3.26.0" resolved "https://registry.npmjs.org/@react-types/shared/-/shared-3.26.0.tgz" @@ -2852,11 +3112,23 @@ dependencies: "@tanstack/virtual-core" "3.10.9" +"@tanstack/react-virtual@3.11.2": + version "3.11.2" + resolved "https://registry.yarnpkg.com/@tanstack/react-virtual/-/react-virtual-3.11.2.tgz#d6b9bd999c181f0a2edce270c87a2febead04322" + integrity sha512-OuFzMXPF4+xZgx8UzJha0AieuMihhhaWG0tCqpp6tDzlFwOmNBPYMuLOtMJ1Tr4pXLHmgjcWhG6RlknY2oNTdQ== + dependencies: + "@tanstack/virtual-core" "3.11.2" + "@tanstack/virtual-core@3.10.9": version "3.10.9" resolved "https://registry.npmjs.org/@tanstack/virtual-core/-/virtual-core-3.10.9.tgz" integrity sha512-kBknKOKzmeR7lN+vSadaKWXaLS0SZZG+oqpQ/k80Q6g9REn6zRHS/ZYdrIzHnpHgy/eWs00SujveUN/GJT2qTw== +"@tanstack/virtual-core@3.11.2": + version "3.11.2" + resolved "https://registry.yarnpkg.com/@tanstack/virtual-core/-/virtual-core-3.11.2.tgz#00409e743ac4eea9afe5b7708594d5fcebb00212" + integrity sha512-vTtpNt7mKCiZ1pwU9hfKPhpdVO2sVzFQsxoVBGtOSHxlrRRzYr8iQ2TlwbAcRYCcEiZ9ECAM8kBzH0v2+VzfKw== + "@tootallnate/once@2": version "2.0.0" resolved "https://registry.npmjs.org/@tootallnate/once/-/once-2.0.0.tgz" From cd3ab8cf80b8a72e028f746b038133f6ada5f73d Mon Sep 17 00:00:00 2001 From: Adetoye Adewoye Date: Wed, 8 Oct 2025 00:08:50 +0100 Subject: [PATCH 04/25] add activity feed to right sidebar --- .../Header/RightSidebar/ActivityFeed.tsx | 164 ++++++++++++++++++ .../Header/RightSidebar/RightSidebar.tsx | 9 +- 2 files changed, 168 insertions(+), 5 deletions(-) create mode 100644 src/components/Header/RightSidebar/ActivityFeed.tsx diff --git a/src/components/Header/RightSidebar/ActivityFeed.tsx b/src/components/Header/RightSidebar/ActivityFeed.tsx new file mode 100644 index 0000000..8686be5 --- /dev/null +++ b/src/components/Header/RightSidebar/ActivityFeed.tsx @@ -0,0 +1,164 @@ +// Copyright 2019-2025 @polkassembly/fellowship authors & contributors +// This software may be modified and distributed under the terms +// of the Apache-2.0 license. See the LICENSE file for details. + +'use client'; + +import React, { useEffect, useState } from 'react'; +import { UserPlus, TrendingUp, FileText, Vote, CircleCheckBig, DollarSign, TrendingDown, Settings } from 'lucide-react'; +import { Button } from '@nextui-org/button'; +import { Card, CardBody, CardHeader } from '@nextui-org/card'; +import { ActivityFeedItem, EActivityFeed, SubsquidActivityType } from '@/global/types'; +import { useApiContext } from '@/contexts'; +import LinkWithNetwork from '../../Misc/LinkWithNetwork'; +import getActivityFeed from '@/app/api/v1/feed/getActivityFeed'; +import getOriginUrl from '@/utils/getOriginUrl'; +import dayjs from '@/services/dayjs-init'; +import LoadingSpinner from '../../Misc/LoadingSpinner'; +import Address from '../../Profile/Address'; + +// Simple function to get activity details +const getActivityDetails = (feedItem: ActivityFeedItem) => { + switch (feedItem.type) { + case SubsquidActivityType.Inducted: + return { icon: UserPlus, color: 'text-green-600', title: 'Member Inducted', description: 'was inducted into the fellowship' }; + case SubsquidActivityType.Promoted: + return { icon: TrendingUp, color: 'text-green-600', title: 'Member Promoted', description: `was promoted to Rank ${feedItem.rank || 0}` }; + case SubsquidActivityType.Demoted: + return { icon: TrendingDown, color: 'text-red-600', title: 'Member Demoted', description: `was demoted to Rank ${feedItem.rank || 0}` }; + case SubsquidActivityType.Retained: + return { icon: CircleCheckBig, color: 'text-green-600', title: 'Member Retained', description: `was retained at Rank ${feedItem.rank || 0}` }; + case SubsquidActivityType.EvidenceSubmitted: + return { icon: FileText, color: 'text-purple-600', title: 'Evidence Submitted', description: 'submitted new evidence' }; + case SubsquidActivityType.GeneralProposal: + case SubsquidActivityType.RFC: + return { icon: Vote, color: 'text-blue-600', title: 'Proposal Created', description: 'created a new proposal' }; + case SubsquidActivityType.Payout: + return { icon: DollarSign, color: 'text-yellow-600', title: 'Salary Payout', description: 'received salary payout' }; + case SubsquidActivityType.Voted: + return { + icon: Vote, + color: 'text-blue-600', + title: 'Vote Cast', + description: `voted ${feedItem.vote?.decision || 'unknown'} on proposal #${feedItem.vote?.proposalIndex || 'unknown'}` + }; + case SubsquidActivityType.OffBoarded: + return { icon: TrendingDown, color: 'text-red-600', title: 'Member Off-boarded', description: 'was off-boarded from the fellowship' }; + case SubsquidActivityType.ActivityChanged: + return { icon: Settings, color: 'text-gray-600', title: 'Status Changed', description: `status changed to ${feedItem.isActive ? 'active' : 'inactive'}` }; + default: + return { icon: CircleCheckBig, color: 'text-blue-600', title: 'Activity', description: 'performed an activity' }; + } +}; + +export default function ActivityFeed() { + const { network } = useApiContext(); + const [feedItems, setFeedItems] = useState([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + + useEffect(() => { + const fetchActivities = async () => { + try { + setLoading(true); + setError(null); + + const originUrl = getOriginUrl(); + const feedItems = (await getActivityFeed({ + feedType: EActivityFeed.ALL, + originUrl, + page: 1, + network + })) as ActivityFeedItem[]; + + if (Array.isArray(feedItems)) { + // Sort by created_at date first (most recent first) and limit to 5 items + const sortedFeedItems = feedItems.toSorted((a, b) => new Date(b.created_at).getTime() - new Date(a.created_at).getTime()).slice(0, 5); + setFeedItems(sortedFeedItems); + } else { + setFeedItems([]); + } + } catch (err) { + console.error('Error fetching activities:', err); + setError('Failed to load activities'); + setFeedItems([]); + } finally { + setLoading(false); + } + }; + + fetchActivities(); + }, [network]); + + return ( +
+ + +
+
What's Happening
+
+
+ + {loading && ( +
+ +
+ )} + + {error &&
{error}
} + + {!loading && !error && ( +
+ {feedItems.length > 0 ? ( + feedItems.map((feedItem) => { + const details = getActivityDetails(feedItem); + const IconComponent = details.icon; + const timestamp = dayjs(feedItem.created_at).fromNow(); + + return ( +
+
+ +
+
+

{details.title}

+
+
+ {details.description} +
+

{timestamp}

+
+
+ ); + }) + ) : ( +
No recent activities found.
+ )} +
+ )} + +
+ +
+
+
+
+ ); +} diff --git a/src/components/Header/RightSidebar/RightSidebar.tsx b/src/components/Header/RightSidebar/RightSidebar.tsx index e12da42..3b48299 100644 --- a/src/components/Header/RightSidebar/RightSidebar.tsx +++ b/src/components/Header/RightSidebar/RightSidebar.tsx @@ -12,7 +12,7 @@ import LinkWithNetwork from '../../Misc/LinkWithNetwork'; import getSubstrateAddress from '@/utils/getSubstrateAddress'; import QuickActions from './QuickActions'; // import Treasury from './Treasury'; -// import ActivityFeed from './ActivityFeed'; +import ActivityFeed from './ActivityFeed'; export default function RightSidebar() { const { fellows } = useApiContext(); @@ -44,12 +44,11 @@ export default function RightSidebar() { )} - {/* Scrollable Content + {/* Scrollable Content */}
- {isFellow && } - + {/* {isFellow && } */} -
*/} + ); } From ea2699145c2d73631fd0c0054273821e8bd6f2cb Mon Sep 17 00:00:00 2001 From: Adetoye Adewoye Date: Wed, 8 Oct 2025 01:01:30 +0100 Subject: [PATCH 05/25] add treasury data --- src/app/api/v1/treasury/route.ts | 142 +++++++++++++++ .../Header/RightSidebar/RightSidebar.tsx | 4 +- .../Header/RightSidebar/Treasury.tsx | 168 ++++++++++++++++++ 3 files changed, 312 insertions(+), 2 deletions(-) create mode 100644 src/app/api/v1/treasury/route.ts create mode 100644 src/components/Header/RightSidebar/Treasury.tsx diff --git a/src/app/api/v1/treasury/route.ts b/src/app/api/v1/treasury/route.ts new file mode 100644 index 0000000..10d2ad7 --- /dev/null +++ b/src/app/api/v1/treasury/route.ts @@ -0,0 +1,142 @@ +// Copyright 2019-2025 @polkassembly/fellowship authors & contributors +// This software may be modified and distributed under the terms +// of the Apache-2.0 license. See the LICENSE file for details. + +import { NextRequest, NextResponse } from 'next/server'; +import { headers } from 'next/headers'; +import getNetworkFromHeaders from '@/app/api/api-utils/getNetworkFromHeaders'; +import withErrorHandling from '@/app/api/api-utils/withErrorHandling'; +import { API_ERROR_CODE } from '@/global/constants/errorCodes'; +import { APIError } from '@/global/exceptions'; +import MESSAGES from '@/global/messages'; +import { ApiPromise, WsProvider } from '@polkadot/api'; +import networkConstants from '@/global/networkConstants'; + +interface TreasuryData { + cycleIndex: number; + cycleProgress: number; + totalCycleDays: number; + daysRemaining: number; + lastPayoutDaysAgo: number; + nextPayoutDays: number; + treasurySpendPeriod: number; + currentSpendingPeriod: number; + spendingProgress: number; +} + +async function getCurrentBlock(api: ApiPromise) { + return await api.rpc.chain.getHeader(); +} + +async function getTreasuryData(api: ApiPromise): Promise { + try { + // Check if treasury module exists + if (!api.consts.treasury) { + // Check for fellowship salary cycle instead + if (api.consts.fellowshipSalary) { + const registrationPeriod = api.consts.fellowshipSalary.registrationPeriod as any; + const registrationPeriodValue = registrationPeriod?.toJSON() || 0; + + // Get current block + const currentBlock = await getCurrentBlock(api); + + // Use registration period as cycle length (this is an approximation) + const cycleLength = registrationPeriodValue * 10; // Multiply by 10 for longer cycles + const currentCycle = Math.floor(currentBlock.number.toNumber() / cycleLength); + const cycleProgress = ((currentBlock.number.toNumber() % cycleLength) / cycleLength) * 100; + + // Convert blocks to days (assuming 6 second block time for collectives) + const blocksPerDay = (24 * 60 * 60) / 6; // 14400 blocks per day + const daysRemaining = Math.ceil((cycleLength - (currentBlock.number.toNumber() % cycleLength)) / blocksPerDay); + const totalCycleDays = Math.ceil(cycleLength / blocksPerDay); + + return { + cycleIndex: currentCycle, + cycleProgress: Math.round(cycleProgress), + totalCycleDays, + daysRemaining, + lastPayoutDaysAgo: 0, + nextPayoutDays: daysRemaining, + treasurySpendPeriod: cycleLength, + currentSpendingPeriod: currentCycle, + spendingProgress: Math.round(cycleProgress) + }; + } + + // Return default values if no modules found + return { + cycleIndex: 0, + cycleProgress: 0, + totalCycleDays: 0, + daysRemaining: 0, + lastPayoutDaysAgo: 0, + nextPayoutDays: 0, + treasurySpendPeriod: 0, + currentSpendingPeriod: 0, + spendingProgress: 0 + }; + } + + // Get treasury spend period information + const spendPeriod = api.consts.treasury.spendPeriod as any; + const treasurySpendPeriod = (spendPeriod?.toJSON() as number) || 0; + + // Get current block + const currentBlock = await getCurrentBlock(api); + + // Calculate current spending period + const currentSpendingPeriod = Math.floor(currentBlock.number.toNumber() / treasurySpendPeriod); + const spendingProgress = ((currentBlock.number.toNumber() % treasurySpendPeriod) / treasurySpendPeriod) * 100; + + // Convert blocks to days (assuming 6 second block time) + const blocksPerDay = (24 * 60 * 60) / 6; // 14400 blocks per day + const daysRemaining = Math.ceil((treasurySpendPeriod - (currentBlock.number.toNumber() % treasurySpendPeriod)) / blocksPerDay); + const totalCycleDays = Math.ceil(treasurySpendPeriod / blocksPerDay); + + return { + cycleIndex: currentSpendingPeriod, + cycleProgress: Math.round(spendingProgress), + totalCycleDays, + daysRemaining, + lastPayoutDaysAgo: 0, // This would need to be fetched from historical data + nextPayoutDays: daysRemaining, + treasurySpendPeriod, + currentSpendingPeriod, + spendingProgress: Math.round(spendingProgress) + }; + } catch (error) { + console.error('Error fetching treasury data:', error); + throw error; + } +} + +export const GET = withErrorHandling(async (req: NextRequest) => { + const headersList = headers(); + const network = getNetworkFromHeaders(headersList); + + if (!network) { + throw new APIError(`${MESSAGES.INVALID_PARAMS_ERROR}`, 500, API_ERROR_CODE.INVALID_PARAMS_ERROR); + } + + try { + // Create API connection + const wsProvider = networkConstants[String(network)]?.rpcEndpoints?.[0]?.key; + if (!wsProvider) { + throw new APIError('No RPC endpoint available for network', 500, 'RPC_ERROR'); + } + + const provider = new WsProvider(wsProvider); + const api = new ApiPromise({ provider }); + + await api.isReady; + + const treasuryData = await getTreasuryData(api); + + await api.disconnect(); + + return NextResponse.json(treasuryData); + } catch (error) { + console.error('Error in treasury API:', error); + throw new APIError('Failed to fetch treasury data', 500, 'TREASURY_FETCH_ERROR'); + } +}); diff --git a/src/components/Header/RightSidebar/RightSidebar.tsx b/src/components/Header/RightSidebar/RightSidebar.tsx index 3b48299..925db6f 100644 --- a/src/components/Header/RightSidebar/RightSidebar.tsx +++ b/src/components/Header/RightSidebar/RightSidebar.tsx @@ -11,7 +11,7 @@ import { Button } from '@nextui-org/button'; import LinkWithNetwork from '../../Misc/LinkWithNetwork'; import getSubstrateAddress from '@/utils/getSubstrateAddress'; import QuickActions from './QuickActions'; -// import Treasury from './Treasury'; +import Treasury from './Treasury'; import ActivityFeed from './ActivityFeed'; export default function RightSidebar() { @@ -46,7 +46,7 @@ export default function RightSidebar() { {/* Scrollable Content */}
- {/* {isFellow && } */} +
diff --git a/src/components/Header/RightSidebar/Treasury.tsx b/src/components/Header/RightSidebar/Treasury.tsx new file mode 100644 index 0000000..91ce3e4 --- /dev/null +++ b/src/components/Header/RightSidebar/Treasury.tsx @@ -0,0 +1,168 @@ +// Copyright 2019-2025 @polkassembly/fellowship authors & contributors +// This software may be modified and distributed under the terms +// of the Apache-2.0 license. See the LICENSE file for details. + +'use client'; + +import React, { useEffect, useState } from 'react'; +import { Wallet, Calendar } from 'lucide-react'; +import { Button } from '@nextui-org/button'; +import { Card, CardBody, CardHeader } from '@nextui-org/card'; +import { Progress } from '@nextui-org/progress'; +import { useApiContext, useUserDetailsContext } from '@/contexts'; +import nextApiClientFetch from '@/utils/nextApiClientFetch'; +import formatSalary from '@/utils/formatSalary'; +import getSubstrateAddress from '@/utils/getSubstrateAddress'; +import LinkWithNetwork from '../../Misc/LinkWithNetwork'; +import LoadingSpinner from '../../Misc/LoadingSpinner'; + +interface TreasuryData { + cycleIndex: number; + cycleProgress: number; + totalCycleDays: number; + daysRemaining: number; + lastPayoutDaysAgo: number; + nextPayoutDays: number; + treasurySpendPeriod: number; + currentSpendingPeriod: number; + spendingProgress: number; +} + +export default function Treasury({ isFellow }: Readonly<{ isFellow: boolean }>) { + const { network, fellows } = useApiContext(); + const { addresses } = useUserDetailsContext(); + + const [treasuryData, setTreasuryData] = useState(null); + const [userSalary, setUserSalary] = useState('0'); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + + useEffect(() => { + const fetchData = async () => { + if (!network) { + return; + } + + try { + setLoading(true); + setError(null); + + // Fetch treasury data + const { data: treasury, error: treasuryError } = await nextApiClientFetch({ + network, + url: 'api/v1/treasury', + isPolkassemblyAPI: false + }); + + if (treasuryError || !treasury) { + console.error('Error fetching treasury data:', treasuryError); + setError('Failed to load treasury data'); + return; + } + + setTreasuryData(treasury); + + // Get user's salary if they are a fellow (use same logic as members table) + if (isFellow && addresses && addresses.length > 0) { + const userAddress = getSubstrateAddress(addresses[0] || ''); + const fellow = fellows?.find((f) => f.address === userAddress); + + if (fellow) { + // Use the same salary value as in the members table (fellow.salary) + setUserSalary(fellow.salary || '0'); + } + } + } catch (err) { + console.error('Error fetching treasury data:', err); + setError('Failed to load treasury data'); + } finally { + setLoading(false); + } + }; + + fetchData(); + }, [network, isFellow, addresses, fellows]); + + return ( +
+ + +
+ + Treasury & Salary +
+
+ + {loading && ( +
+ +
+ )} + + {error &&
{error}
} + + {!loading && !error && ( + <> +
+
+ Treasury Cycle + Cycle #{treasuryData?.cycleIndex || 0} +
+
+
+ Progress + + {(treasuryData?.totalCycleDays || 0) - (treasuryData?.daysRemaining || 0)}/{treasuryData?.totalCycleDays || 0} days + +
+ +
+
+ +
+ {isFellow && userSalary !== '0' && ( +
+ Your Salary + {formatSalary(userSalary)} +
+ )} +
+ {(treasuryData?.lastPayoutDaysAgo || 0) > 0 ? `Last payout: ${treasuryData?.lastPayoutDaysAgo} days ago` : 'No recent payouts'} +
+
+ +
+ +
+ +
+
+ Next payout: + {treasuryData?.nextPayoutDays || 0} days +
+ {isFellow && userSalary !== '0' && ( +
+ Estimated amount: + {formatSalary(userSalary)} +
+ )} +
+ + )} +
+
+
+ ); +} From 38a44059fc46e2751ed1b827bb99facaf9ad9e00 Mon Sep 17 00:00:00 2001 From: Adetoye Adewoye Date: Wed, 8 Oct 2025 02:37:54 +0100 Subject: [PATCH 06/25] Add submit evidence flow --- src/app/(site)/submit-evidence/page.tsx | 68 ++++ .../@modal/(site)/(.)submit-evidence/page.tsx | 97 +++++ .../Header/RightSidebar/QuickActions.tsx | 3 + src/components/Misc/AddressSwitch.tsx | 209 +++++++++++ .../SubmitEvidence/SubmitEvidenceForm.tsx | 341 ++++++++++++++++++ 5 files changed, 718 insertions(+) create mode 100644 src/app/(site)/submit-evidence/page.tsx create mode 100644 src/app/@modal/(site)/(.)submit-evidence/page.tsx create mode 100644 src/components/Misc/AddressSwitch.tsx create mode 100644 src/components/SubmitEvidence/SubmitEvidenceForm.tsx diff --git a/src/app/(site)/submit-evidence/page.tsx b/src/app/(site)/submit-evidence/page.tsx new file mode 100644 index 0000000..1c37290 --- /dev/null +++ b/src/app/(site)/submit-evidence/page.tsx @@ -0,0 +1,68 @@ +// Copyright 2019-2025 @polkassembly/fellowship authors & contributors +// This software may be modified and distributed under the terms +// of the Apache-2.0 license. See the LICENSE file for details. + +'use client'; + +import SubmitEvidenceForm from '@/components/SubmitEvidence/SubmitEvidenceForm'; +import { Button } from '@nextui-org/button'; +import { useRef, useState } from 'react'; +import { useUserDetailsContext } from '@/contexts'; +import LinkWithNetwork from '@/components/Misc/LinkWithNetwork'; + +export default function SubmitEvidence() { + const { id } = useUserDetailsContext(); + + const formRef = useRef(null); + const [isFormValid, setIsFormValid] = useState(false); + const [isFormLoading, setIsFormLoading] = useState(false); + + const handleSubmit = () => { + if (formRef.current) { + formRef?.current?.requestSubmit(); + } + }; + + return ( +
+

Submit Evidence

+ +
+ {!id ? ( +
+ Please{' '} + + login + {' '} + to submit evidence. +
+ ) : ( +
+ { + // Optionally redirect or show success message + }} + onFormStateChange={(isValid, isLoading) => { + setIsFormValid(isValid); + setIsFormLoading(isLoading); + }} + /> + + +
+ )} +
+
+ ); +} diff --git a/src/app/@modal/(site)/(.)submit-evidence/page.tsx b/src/app/@modal/(site)/(.)submit-evidence/page.tsx new file mode 100644 index 0000000..e1efa49 --- /dev/null +++ b/src/app/@modal/(site)/(.)submit-evidence/page.tsx @@ -0,0 +1,97 @@ +// Copyright 2019-2025 @polkassembly/fellowship authors & contributors +// This software may be modified and distributed under the terms +// of the Apache-2.0 license. See the LICENSE file for details. + +'use client'; + +import SubmitEvidenceForm from '@/components/SubmitEvidence/SubmitEvidenceForm'; +import { Button } from '@nextui-org/button'; +import { Modal, ModalContent, ModalHeader, ModalBody, ModalFooter } from '@nextui-org/modal'; +import { useRouter } from 'next/navigation'; +import React, { useRef, useState } from 'react'; +import { Divider } from '@nextui-org/divider'; +import { useUserDetailsContext } from '@/contexts'; +import LinkWithNetwork from '@/components/Misc/LinkWithNetwork'; +import { FileText } from 'lucide-react'; + +function SubmitEvidenceModal() { + const router = useRouter(); + const { id } = useUserDetailsContext(); + + const formRef = useRef(null); + const [isModalOpen, setIsModalOpen] = useState(true); + const [isFormValid, setIsFormValid] = useState(false); + const [isFormLoading, setIsFormLoading] = useState(false); + + const handleOnClose = () => { + router.back(); + }; + + const handleSubmit = () => { + if (formRef.current) { + formRef?.current?.requestSubmit(); + } + }; + + return ( + + + {() => + id ? ( + <> + + +

Submit Evidence

+
+ + + + setIsModalOpen(false)} + onFormStateChange={(isValid, isLoading) => { + setIsFormValid(isValid); + setIsFormLoading(isLoading); + }} + /> + + + + + + + + + ) : ( +
+ Please{' '} + + login + {' '} + to submit evidence. +
+ ) + } +
+
+ ); +} + +export default SubmitEvidenceModal; diff --git a/src/components/Header/RightSidebar/QuickActions.tsx b/src/components/Header/RightSidebar/QuickActions.tsx index 84843b4..cfcf5e4 100644 --- a/src/components/Header/RightSidebar/QuickActions.tsx +++ b/src/components/Header/RightSidebar/QuickActions.tsx @@ -7,6 +7,7 @@ import React from 'react'; import { FileText, DollarSign, UserPlus, Plus } from 'lucide-react'; import { Button } from '@nextui-org/button'; +import LinkWithNetwork from '../../Misc/LinkWithNetwork'; export default function QuickActions() { return ( @@ -16,6 +17,8 @@ export default function QuickActions() { + + + + + {() => ( + <> + + +

Switch Wallet & Address

+
+ + + +
+

Select Wallet

+ setTempSelectedWallet(wallet)} + /> +
+ + {tempSelectedWallet && ( +
+

Select Address

+ +
+ )} +
+ + + + + + + + + )} +
+
+ + ); +} + +export default AddressSwitch; diff --git a/src/components/SubmitEvidence/SubmitEvidenceForm.tsx b/src/components/SubmitEvidence/SubmitEvidenceForm.tsx new file mode 100644 index 0000000..6496615 --- /dev/null +++ b/src/components/SubmitEvidence/SubmitEvidenceForm.tsx @@ -0,0 +1,341 @@ +// Copyright 2019-2025 @polkassembly/fellowship authors & contributors +// This software may be modified and distributed under the terms +// of the Apache-2.0 license. See the LICENSE file for details. + +'use client'; + +import React, { useState } from 'react'; +import { useForm, Controller } from 'react-hook-form'; +import { Select, SelectItem } from '@nextui-org/select'; +import { useApiContext, useUserDetailsContext } from '@/contexts'; +import { Wallet } from '@/global/types'; +import { InjectedAccount } from '@polkadot/extension-inject/types'; +import queueNotification from '@/utils/queueNotification'; +import LoadingSpinner from '@/components/Misc/LoadingSpinner'; +import AlertCard from '@/components/Misc/AlertCard'; +import AddressSwitch from '@/components/Misc/AddressSwitch'; +import getSubstrateAddress from '@/utils/getSubstrateAddress'; +import executeTx from '@/utils/executeTx'; +import MarkdownEditor from '@/components/TextEditor/MarkdownEditor'; + +interface Props { + readonly formRef: React.RefObject; + readonly onSuccess?: () => void; + readonly onFormStateChange?: (isValid: boolean, isLoading: boolean) => void; +} + +export enum FellowshipWish { + RETENTION = 'Retention', + PROMOTION = 'Promotion' +} + +interface FormData { + wish: FellowshipWish; + evidence: string; +} + +function SubmitEvidenceForm({ formRef, onSuccess, onFormStateChange }: Props) { + const { api, apiReady, network, fellows } = useApiContext(); + const { id, addresses } = useUserDetailsContext(); + + const { + formState: { errors }, + control, + handleSubmit + } = useForm({ + defaultValues: { + wish: FellowshipWish.RETENTION, + evidence: '' + } + }); + + const [loading, setLoading] = useState(false); + const [error, setError] = useState(''); + const [selectedWallet, setSelectedWallet] = useState(null); + const [selectedAddress, setSelectedAddress] = useState(null); + const [txStatus, setTxStatus] = useState(''); + + // Form validation state + const isFormValid = Boolean(selectedWallet && selectedAddress && api && apiReady && !loading); + + // Notify parent component of form state changes + React.useEffect(() => { + onFormStateChange?.(isFormValid, loading); + }, [isFormValid, loading, onFormStateChange]); + + const currentFellow = fellows?.find((fellow) => fellow.address === getSubstrateAddress(addresses?.[0] || '')); + + // Validation function to check if form can be submitted + const validateFormData = (wish: FellowshipWish, evidence: string): string | null => { + if (!wish || !Object.values(FellowshipWish).includes(wish)) { + return 'Please select a valid wish type.'; + } + + if (!evidence || typeof evidence !== 'string') { + return 'Evidence is required.'; + } + + const trimmedEvidence = evidence.trim(); + if (trimmedEvidence.length === 0) { + return 'Evidence cannot be empty.'; + } + + if (trimmedEvidence.length < 10) { + return 'Evidence must be at least 10 characters long.'; + } + + if (trimmedEvidence.length > 10000) { + return 'Evidence is too long. Please keep it under 10,000 characters.'; + } + + return null; + }; + + const submitForm = async ({ wish, evidence }: FormData) => { + // Early return if basic conditions are not met + if (!id) { + setError('Please login to submit evidence.'); + return; + } + + if (loading) { + setError('Transaction is already in progress.'); + return; + } + + if (!api || !apiReady) { + setError('API is not ready. Please try again.'); + return; + } + + if (!selectedWallet) { + setError('Please select a wallet.'); + return; + } + + if (!selectedAddress) { + setError('Please select an address.'); + return; + } + + // Validate selected address + if (!selectedAddress.address) { + setError('Selected address is invalid.'); + return; + } + + const fellowAddress = getSubstrateAddress(selectedAddress.address); + if (!fellowAddress) { + setError('Invalid address format. Please select a valid address.'); + return; + } + + // Validate fellow status + if (!fellows || fellows.length === 0) { + setError('Fellows data is not available. Please try again.'); + return; + } + + const fellow = fellows.find((f) => f.address === fellowAddress); + if (!fellow) { + setError('Selected address is not a fellow. Only fellows can submit evidence.'); + return; + } + + // Validate form data using validation function + const formValidationError = validateFormData(wish, evidence); + if (formValidationError) { + setError(formValidationError); + return; + } + + const trimmedEvidence = evidence.trim(); + + // Clear any previous errors + setError(''); + + setLoading(true); + + try { + // Convert wish to the expected format for the extrinsic + const wishValue = wish === FellowshipWish.PROMOTION ? 'Promotion' : 'Retention'; + + // Convert evidence to bytes (using trimmed evidence) + const evidenceBytes = new TextEncoder().encode(trimmedEvidence); + + // Create the submitEvidence transaction + const tx = api.tx.coreFellowship.submitEvidence(wishValue, evidenceBytes); + + const onFailed = (message: string) => { + setLoading(false); + queueNotification({ + header: 'Submit Evidence Transaction Failed!', + message, + status: 'error' + }); + setTxStatus(''); + }; + + const onSuccessCallback = async () => { + setLoading(false); + queueNotification({ + header: 'Evidence Submitted Successfully!', + message: `Your ${wish.toLowerCase()} evidence has been submitted for ${fellow.rank} rank.`, + status: 'success' + }); + setTxStatus(''); + onSuccess?.(); + }; + + await executeTx({ + address: selectedAddress.address, + api, + apiReady, + errorMessageFallback: 'Error while executing transaction. Please try again.', + network, + onFailed, + onSuccess: onSuccessCallback, + setStatus: (status: string) => setTxStatus(status), + tx + }); + } catch (err) { + console.error('Error submitting evidence:', err); + const errorMessage = err instanceof Error ? err.message : 'Failed to submit evidence. Please try again.'; + setError(`Transaction failed: ${errorMessage}`); + setLoading(false); + setTxStatus(''); + } + }; + + return ( +
{ + e.preventDefault(); + if (!isFormValid) { + setError('Please ensure all required fields are filled and a valid wallet/address is selected.'); + return; + } + handleSubmit(submitForm)(e); + }} + className='flex flex-col gap-4' + > + + + {currentFellow && ( +
+
+ Current Rank: {currentFellow.rank} +
+
+ )} + +
+
+ Wish Type* +
+ ( + + )} + /> + {errors.wish?.message && ( + + {errors.wish.message} + + )} +
+ +
+
+ Evidence* +
+ ( + + )} + /> + {errors.evidence?.message && ( + + {errors.evidence.message} + + )} +
+ + {error && ( + + )} + + {txStatus && ( + + + {txStatus} + + } + /> + )} + + ); +} + +export default SubmitEvidenceForm; From 1f553f42fb1d264a33793dd42d0b15f5ae146cd7 Mon Sep 17 00:00:00 2001 From: Adetoye Adewoye Date: Wed, 8 Oct 2025 02:58:18 +0100 Subject: [PATCH 07/25] add create proposal form --- src/app/(site)/create-proposal/page.tsx | 68 +++ .../@modal/(site)/(.)create-proposal/page.tsx | 97 ++++ .../CreateProposal/CreateProposalForm.tsx | 450 ++++++++++++++++++ .../Header/RightSidebar/QuickActions.tsx | 2 + src/components/Misc/EvidenceSelector.tsx | 159 +++++++ .../SubmitEvidence/SubmitEvidenceForm.tsx | 66 ++- 6 files changed, 841 insertions(+), 1 deletion(-) create mode 100644 src/app/(site)/create-proposal/page.tsx create mode 100644 src/app/@modal/(site)/(.)create-proposal/page.tsx create mode 100644 src/components/CreateProposal/CreateProposalForm.tsx create mode 100644 src/components/Misc/EvidenceSelector.tsx diff --git a/src/app/(site)/create-proposal/page.tsx b/src/app/(site)/create-proposal/page.tsx new file mode 100644 index 0000000..c74a476 --- /dev/null +++ b/src/app/(site)/create-proposal/page.tsx @@ -0,0 +1,68 @@ +// Copyright 2019-2025 @polkassembly/fellowship authors & contributors +// This software may be modified and distributed under the terms +// of the Apache-2.0 license. See the LICENSE file for details. + +'use client'; + +import CreateProposalForm from '@/components/CreateProposal/CreateProposalForm'; +import { Button } from '@nextui-org/button'; +import { useRef, useState } from 'react'; +import { useUserDetailsContext } from '@/contexts'; +import LinkWithNetwork from '@/components/Misc/LinkWithNetwork'; + +export default function CreateProposal() { + const { id } = useUserDetailsContext(); + + const formRef = useRef(null); + const [isFormValid, setIsFormValid] = useState(false); + const [isFormLoading, setIsFormLoading] = useState(false); + + const handleSubmit = () => { + if (formRef.current) { + formRef?.current?.requestSubmit(); + } + }; + + return ( +
+

Create Proposal

+ +
+ {!id ? ( +
+ Please{' '} + + login + {' '} + to create a proposal. +
+ ) : ( +
+ { + // Optionally redirect or show success message + }} + onFormStateChange={(isValid, isLoading) => { + setIsFormValid(isValid); + setIsFormLoading(isLoading); + }} + /> + + +
+ )} +
+
+ ); +} diff --git a/src/app/@modal/(site)/(.)create-proposal/page.tsx b/src/app/@modal/(site)/(.)create-proposal/page.tsx new file mode 100644 index 0000000..ba64aa9 --- /dev/null +++ b/src/app/@modal/(site)/(.)create-proposal/page.tsx @@ -0,0 +1,97 @@ +// Copyright 2019-2025 @polkassembly/fellowship authors & contributors +// This software may be modified and distributed under the terms +// of the Apache-2.0 license. See the LICENSE file for details. + +'use client'; + +import CreateProposalForm from '@/components/CreateProposal/CreateProposalForm'; +import { Button } from '@nextui-org/button'; +import { Modal, ModalContent, ModalHeader, ModalBody, ModalFooter } from '@nextui-org/modal'; +import { useRouter } from 'next/navigation'; +import React, { useRef, useState } from 'react'; +import { Divider } from '@nextui-org/divider'; +import { useUserDetailsContext } from '@/contexts'; +import LinkWithNetwork from '@/components/Misc/LinkWithNetwork'; +import { FileText } from 'lucide-react'; + +function CreateProposalModal() { + const router = useRouter(); + const { id } = useUserDetailsContext(); + + const formRef = useRef(null); + const [isModalOpen, setIsModalOpen] = useState(true); + const [isFormValid, setIsFormValid] = useState(false); + const [isFormLoading, setIsFormLoading] = useState(false); + + const handleOnClose = () => { + router.back(); + }; + + const handleSubmit = () => { + if (formRef.current) { + formRef?.current?.requestSubmit(); + } + }; + + return ( + + + {() => + id ? ( + <> + + +

Create Proposal

+
+ + + + setIsModalOpen(false)} + onFormStateChange={(isValid, isLoading) => { + setIsFormValid(isValid); + setIsFormLoading(isLoading); + }} + /> + + + + + + + + + ) : ( +
+ Please{' '} + + login + {' '} + to create a proposal. +
+ ) + } +
+
+ ); +} + +export default CreateProposalModal; diff --git a/src/components/CreateProposal/CreateProposalForm.tsx b/src/components/CreateProposal/CreateProposalForm.tsx new file mode 100644 index 0000000..ac416d8 --- /dev/null +++ b/src/components/CreateProposal/CreateProposalForm.tsx @@ -0,0 +1,450 @@ +// Copyright 2019-2025 @polkassembly/fellowship authors & contributors +// This software may be modified and distributed under the terms +// of the Apache-2.0 license. See the LICENSE file for details. + +'use client'; + +import React, { useState } from 'react'; +import { useForm, Controller } from 'react-hook-form'; +import { Select, SelectItem } from '@nextui-org/select'; +import { Input, Textarea } from '@nextui-org/input'; +import { useApiContext, useUserDetailsContext } from '@/contexts'; +import { Wallet } from '@/global/types'; +import { InjectedAccount } from '@polkadot/extension-inject/types'; +import { useSearchParams } from 'next/navigation'; +import queueNotification from '@/utils/queueNotification'; +import LoadingSpinner from '@/components/Misc/LoadingSpinner'; +import AlertCard from '@/components/Misc/AlertCard'; +import AddressSwitch from '@/components/Misc/AddressSwitch'; +import EvidenceSelector from '@/components/Misc/EvidenceSelector'; +import getSubstrateAddress from '@/utils/getSubstrateAddress'; +import MarkdownEditor from '@/components/TextEditor/MarkdownEditor'; + +interface Props { + readonly formRef: React.RefObject; + readonly onSuccess?: () => void; + readonly onFormStateChange?: (isValid: boolean, isLoading: boolean) => void; +} + +export enum ProposalType { + PROMOTION = 'Promotion', + RETENTION = 'Retention' +} + +interface FormData { + title: string; + proposalType: ProposalType; + motivation: string; + interest: string; + evidenceId: string; +} + +function CreateProposalForm({ formRef, onSuccess, onFormStateChange }: Props) { + const { api, apiReady, fellows } = useApiContext(); + const { id, addresses } = useUserDetailsContext(); + const searchParams = useSearchParams(); + + const { + formState: { errors }, + control, + handleSubmit + } = useForm({ + defaultValues: { + title: '', + proposalType: ProposalType.PROMOTION, + motivation: '', + interest: '', + evidenceId: searchParams?.get('evidenceId') || '' + } + }); + + const [loading, setLoading] = useState(false); + const [error, setError] = useState(''); + const [selectedWallet, setSelectedWallet] = useState(null); + const [selectedAddress, setSelectedAddress] = useState(null); + const [txStatus, setTxStatus] = useState(''); + + // Form validation state + const isFormValid = Boolean(selectedWallet && selectedAddress && api && apiReady && !loading); + + // Notify parent component of form state changes + React.useEffect(() => { + onFormStateChange?.(isFormValid, loading); + }, [isFormValid, loading, onFormStateChange]); + + const currentFellow = fellows?.find((fellow) => fellow.address === getSubstrateAddress(addresses?.[0] || '')); + + // Validation function to check if form can be submitted + const validateFormData = (data: FormData): string | null => { + if (!data.title || data.title.trim().length === 0) { + return 'Title is required.'; + } + + if (data.title.trim().length < 5) { + return 'Title must be at least 5 characters long.'; + } + + if (data.title.trim().length > 200) { + return 'Title must be less than 200 characters.'; + } + + if (!data.proposalType || !Object.values(ProposalType).includes(data.proposalType)) { + return 'Please select a valid proposal type.'; + } + + if (!data.motivation || data.motivation.trim().length === 0) { + return 'Motivation is required.'; + } + + if (data.motivation.trim().length < 20) { + return 'Motivation must be at least 20 characters long.'; + } + + if (data.motivation.trim().length > 5000) { + return 'Motivation must be less than 5,000 characters.'; + } + + if (!data.interest || data.interest.trim().length === 0) { + return 'Interest is required.'; + } + + if (data.interest.trim().length < 10) { + return 'Interest must be at least 10 characters long.'; + } + + if (data.interest.trim().length > 2000) { + return 'Interest must be less than 2,000 characters.'; + } + + if (!data.evidenceId || data.evidenceId.trim().length === 0) { + return 'Evidence is required.'; + } + + return null; + }; + + const submitForm = async (data: FormData) => { + // Early return if basic conditions are not met + if (!id) { + setError('Please login to create a proposal.'); + return; + } + + if (loading) { + setError('Transaction is already in progress.'); + return; + } + + if (!api || !apiReady) { + setError('API is not ready. Please try again.'); + return; + } + + if (!selectedWallet) { + setError('Please select a wallet.'); + return; + } + + if (!selectedAddress) { + setError('Please select an address.'); + return; + } + + // Validate selected address + if (!selectedAddress.address) { + setError('Selected address is invalid.'); + return; + } + + const fellowAddress = getSubstrateAddress(selectedAddress.address); + if (!fellowAddress) { + setError('Invalid address format. Please select a valid address.'); + return; + } + + // Validate fellow status + if (!fellows || fellows.length === 0) { + setError('Fellows data is not available. Please try again.'); + return; + } + + const fellow = fellows.find((f) => f.address === fellowAddress); + if (!fellow) { + setError('Selected address is not a fellow. Only fellows can create proposals.'); + return; + } + + // Validate form data using validation function + const formValidationError = validateFormData(data); + if (formValidationError) { + setError(formValidationError); + return; + } + + // Clear any previous errors + setError(''); + + setLoading(true); + + try { + // TODO: Implement actual proposal creation logic + // This would typically involve: + // 1. Creating a preimage + // 2. Submitting the proposal + // 3. Handling the transaction + + // For now, simulate a successful transaction + await new Promise((resolve) => setTimeout(resolve, 2000)); + + queueNotification({ + header: 'Proposal Created Successfully!', + message: `Your ${data.proposalType.toLowerCase()} proposal has been created.`, + status: 'success' + }); + + setLoading(false); + onSuccess?.(); + } catch (err) { + console.error('Error creating proposal:', err); + const errorMessage = err instanceof Error ? err.message : 'Failed to create proposal. Please try again.'; + setError(`Transaction failed: ${errorMessage}`); + setLoading(false); + setTxStatus(''); + } + }; + + return ( +
{ + e.preventDefault(); + if (!isFormValid) { + setError('Please ensure all required fields are filled and a valid wallet/address is selected.'); + return; + } + handleSubmit(submitForm)(e); + }} + className='flex flex-col gap-4' + > + + + {currentFellow && ( +
+
+ Current Rank: {currentFellow.rank} +
+
+ )} + +
+
+ Title* +
+ ( + + )} + /> + {errors.title?.message && ( + + {errors.title.message} + + )} +
+ +
+
+ Proposal Type* +
+ ( + + )} + /> + {errors.proposalType?.message && ( + + {errors.proposalType.message} + + )} +
+ +
+
+ Motivation* +
+ ( +