diff --git a/.eslintignore b/.eslintignore index 8b64b1b13..5240f9ef2 100644 --- a/.eslintignore +++ b/.eslintignore @@ -1 +1,2 @@ **/*.generated.* +**/*.json diff --git a/.github/ISSUE_TEMPLATE/provide_feedback.yml b/.github/ISSUE_TEMPLATE/provide_feedback.yml index f3f2fd673..a79941f77 100644 --- a/.github/ISSUE_TEMPLATE/provide_feedback.yml +++ b/.github/ISSUE_TEMPLATE/provide_feedback.yml @@ -1,22 +1,22 @@ --- name: Provide Feedback description: Questions and general feedback goes here! -labels: ["feedback", "beta"] +labels: ['feedback', 'beta'] body: - type: checkboxes attributes: label: App description: For which app do want to provide feedback? options: - - label: "[Admin](https://admin.daohaus.fun)" - - label: "[Summon](https://summon.daohaus.fun)" - - label: "[Hub](https://hub.daohaus.fun)" - - label: "Toolbox" + - label: '[Admin](https://admin.daohaus.fun)' + - label: '[Summon](https://summon.daohaus.fun)' + - label: '[Hub](https://hub.daohaus.fun)' + - label: '[Toolbox](https://toolbox.daohaus.fun/)' validations: required: false - type: checkboxes attributes: - label: Feedback type + label: Feedback Type options: - label: Comments - label: Questions @@ -31,9 +31,9 @@ body: required: true - type: textarea attributes: - label: Screenshots and context + label: Screenshots and Context placeholder: | - Place additional information that will help understand feedback. - You can add screenshots by pasting from your clipboard. + Add additional information that will help us understand your feedback. + You can also add screenshots by pasting from your clipboard. validations: required: false diff --git a/.github/ISSUE_TEMPLATE/report_bug.yml b/.github/ISSUE_TEMPLATE/report_bug.yml index 507bc852d..f447c8252 100644 --- a/.github/ISSUE_TEMPLATE/report_bug.yml +++ b/.github/ISSUE_TEMPLATE/report_bug.yml @@ -1,7 +1,7 @@ --- name: Report Bug description: Report a reproducible bug or issue -labels: ["bug", "beta"] +labels: ['bug', 'beta'] body: - type: input attributes: @@ -11,32 +11,32 @@ body: required: true - type: checkboxes attributes: - label: App Affected + label: App description: In which app are you encountering this bug? options: - - label: "[Admin](https://admin.daohaus.fun)" - - label: "[Summon](https://summon.daohaus.fun)" - - label: "[Hub](https://hub.daohaus.fun)" - - label: "Toolbox" + - label: '[Admin](https://admin.daohaus.fun)' + - label: '[Summon](https://summon.daohaus.fun)' + - label: '[Hub](https://hub.daohaus.fun)' + - label: '[Toolbox](https://toolbox.daohaus.fun/)' validations: required: false - type: textarea attributes: - label: Expected behavior + label: Expected Behavior description: What did you expect to happen with this feature? validations: required: true - type: textarea attributes: - label: Actual behavior + label: Actual Behavior description: What happened that didn't meet your expectations? validations: required: true - type: textarea attributes: - label: Steps to reproduce + label: Steps to Reproduce description: | - If applicable, can you lay out the steps needed to reproduce this issue? + If applicable, can you lay out the steps required to reproduce this issue? placeholder: | 1. 2. @@ -46,7 +46,7 @@ body: - type: dropdown attributes: label: Browser - description: Which browser is this issue occuring in? + description: Which browser is this issue occurring in? multiple: true options: - N/A @@ -58,9 +58,9 @@ body: - Other - type: textarea attributes: - label: Screenshots and context + label: Screenshots and Context placeholder: | - Place additional information that will help address this issue here. + Add additional information that will help us address this issue. You can add screenshots by pasting from your clipboard. validations: required: false diff --git a/.github/ISSUE_TEMPLATE/request_feature.yml b/.github/ISSUE_TEMPLATE/request_feature.yml index b3f016c0b..1a1517757 100644 --- a/.github/ISSUE_TEMPLATE/request_feature.yml +++ b/.github/ISSUE_TEMPLATE/request_feature.yml @@ -1,17 +1,17 @@ --- name: Feature Request description: Share your ideas for future development -labels: ["enhancement", "beta"] +labels: ['enhancement', 'beta'] body: - type: checkboxes attributes: label: App description: What app would you like to help improve? options: - - label: "[Admin](https://admin.daohaus.fun)" - - label: "[Summon](https://summon.daohaus.fun)" - - label: "[Hub](https://hub.daohaus.fun)" - - label: "Toolbox" + - label: '[Admin](https://admin.daohaus.fun)' + - label: '[Summon](https://summon.daohaus.fun)' + - label: '[Hub](https://hub.daohaus.fun)' + - label: '[Toolbox](https://toolbox.daohaus.fun/)' validations: required: false - type: checkboxes @@ -20,22 +20,22 @@ body: description: | Is your request a new feature or an enhancement to an existing feature? options: - - label: "New Feature" - - label: "Enhancement" + - label: 'New Feature' + - label: 'Enhancement' validations: required: true - type: textarea attributes: label: Feature description: Please describe the feature request or enhancement. - placeholder: "Example: killer feature nobody thought of before" + placeholder: 'Example: killer feature nobody thought of before' validations: required: true - type: textarea attributes: - label: Screenshots and context + label: Screenshots and Context placeholder: | - Place additional information that will help explain the feature. + Add additional information that will help explain the feature. You can add screenshots by pasting from your clipboard. validations: required: false diff --git a/.github/workflows/ci_develop.yaml b/.github/workflows/ci_develop.yaml index 9f11d695d..c2db7b6c9 100644 --- a/.github/workflows/ci_develop.yaml +++ b/.github/workflows/ci_develop.yaml @@ -16,6 +16,7 @@ env: DAOHAUS_HUB_DEVELOP_SKYNET_SEED: ${{ secrets.DAOHAUS_HUB_DEVELOP_SKYNET_SEED }} DAOHAUS_CORE_UI_DEVELOP_SKYNET_SEED: ${{ secrets.DAOHAUS_CORE_UI_DEVELOP_SKYNET_SEED }} DAOHAUS_SUMMON_SAFE_DEVELOP_SKYNET_SEED: ${{ secrets.DAOHAUS_SUMMON_SAFE_DEVELOP_SKYNET_SEED }} + DAOHAUS_UI_DEVELOP_SKYNET_SEED: ${{ secrets.DAOHAUS_UI_DEVELOP_SKYNET_SEED }} jobs: build: @@ -43,17 +44,19 @@ jobs: uses: docker/setup-qemu-action@v1 - name: Set up Docker Buildx uses: docker/setup-buildx-action@v1 - - name: Install doctl - uses: digitalocean/action-doctl@v2 - with: - token: ${{ secrets.DIGITALOCEAN_ACCESS_TOKEN }} - - name: Save kubeconfig - run: doctl kubernetes cluster kubeconfig save daohaus-staging-nyc3-1653325548883 + # - name: Install doctl + # uses: digitalocean/action-doctl@v2 + # with: + # token: ${{ secrets.DIGITALOCEAN_ACCESS_TOKEN }} + # - name: Save kubeconfig + # run: doctl kubernetes cluster kubeconfig save daohaus-staging-nyc3-1653325548883 - name: Create env file run: | touch .env echo NX_RIVET_KEY=${{ secrets.RIVET_KEY }} >> .env echo VITE_RIVET_KEY=${{ secrets.RIVET_KEY }} >> .env + echo VITE_INFURA_PROJECT_ID=${{ secrets.INFURA_PROJECT_ID }} >> .env + echo VITE_ETHERSCAN_KEY=${{ secrets.ETHERSCAN_KEY }} >> .env echo CERAMIC_NETWORK=testnet-clay >> .env echo NODE_ENV=production >> .env - run: yarn install --frozen-lockfile diff --git a/.github/workflows/ci_master.yaml b/.github/workflows/ci_master.yaml index 3721ec875..47f24932f 100644 --- a/.github/workflows/ci_master.yaml +++ b/.github/workflows/ci_master.yaml @@ -39,6 +39,8 @@ jobs: run: | touch .env echo NX_RIVET_KEY=${{ secrets.RIVET_KEY }} >> .env + echo VITE_RIVET_KEY=${{ secrets.RIVET_KEY }} >> .env + echo VITE_INFURA_PROJECT_ID=${{ secrets.INFURA_PROJECT_ID }} >> .env echo CERAMIC_NETWORK=${{ secrets.CERAMIC_NETWORK }} >> .env echo NODE_ENV=production >> .env - run: yarn install --frozen-lockfile diff --git a/apps/core-app/public/hub-illustration.svg b/apps/core-app/public/hub-illustration.svg new file mode 100644 index 000000000..9b5d125a9 --- /dev/null +++ b/apps/core-app/public/hub-illustration.svg @@ -0,0 +1,75 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/apps/core-app/src/Routes.tsx b/apps/core-app/src/Routes.tsx index e605604aa..71d597df2 100644 --- a/apps/core-app/src/Routes.tsx +++ b/apps/core-app/src/Routes.tsx @@ -1,43 +1,95 @@ -import React from 'react'; -import { Routes as RoutesDom, Route } from 'react-router-dom'; +import { + Routes as RoutesDom, + Route, + Outlet, + useLocation, + useParams, + useNavigate, + matchPath, +} from 'react-router-dom'; -import { DaoContextProvider } from './contexts/DaoContext'; import Home from './pages/Home'; -import Dao from './pages/Dao'; import DaoOverview from './pages/DaoOverview'; import Members from './pages/Members'; import Member from './pages/Member'; import Proposals from './pages/Proposals'; -import Vaults from './pages/Vaults'; +import Safes from './pages/Safes'; import FormTest from './pages/FormTest'; import Settings from './pages/Settings'; import NewProposal from './pages/NewProposal'; import UpdateSettings from './pages/UpdateSettings'; +import ProposalDetails from './pages/ProposalDetails'; +import { DaoContainer } from './pages/DaoContainer'; +import { Banner } from '@daohaus/ui'; +import RageQuit from './pages/RageQuit'; +import { + HausConnectProvider, + HausLayout, + useHausConnect, +} from '@daohaus/daohaus-connect-feature'; +import { useEffect, useLayoutEffect, useState } from 'react'; + +const HomeContainer = () => { + const location = useLocation(); + const navigate = useNavigate(); + const { isConnected, address } = useHausConnect(); + const { profile } = useParams(); + + useLayoutEffect(() => { + if (isConnected && address && profile) { + return; + } + if (isConnected && !profile) { + navigate(`/${address}`); + // window.location.href = `/${address}`; + } + }, [isConnected, address, profile, navigate]); + return ( + + + + ); +}; const Routes = () => { + const [daoChainId, setDaoChainId] = useState(); + const location = useLocation(); + const pathMatch = matchPath('molochv3/:daochain/:daoid/*', location.pathname); + + useEffect(() => { + if (pathMatch?.params?.daochain) { + setDaoChainId(pathMatch?.params?.daochain); + } + if (daoChainId && !pathMatch?.params?.daochain) { + setDaoChainId(undefined); + } + }, [pathMatch?.params?.daochain, setDaoChainId, daoChainId]); + return ( - - } /> - - - - } - > - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - - + + + + }> + } /> + + }> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + + + ); }; diff --git a/apps/core-app/src/assets/wallet_connect.svg b/apps/core-app/src/assets/wallet_connect.svg new file mode 100644 index 000000000..e57796bce --- /dev/null +++ b/apps/core-app/src/assets/wallet_connect.svg @@ -0,0 +1,11 @@ + + + WalletConnect + Created with Sketch. + + + + + + + diff --git a/apps/core-app/src/components/ActionDisplay.tsx b/apps/core-app/src/components/ActionDisplay.tsx new file mode 100644 index 000000000..d68ef0740 --- /dev/null +++ b/apps/core-app/src/components/ActionDisplay.tsx @@ -0,0 +1,160 @@ +import { + ArgType, + isEthAddress, + isValidNetwork, + ValidNetwork, +} from '@daohaus/common-utilities'; +import { DecodedMultiTX, isActionError } from '@daohaus/tx-builder-feature'; +import { + AddressDisplay, + Bold, + DataSm, + Divider, + H4, + useBreakpoint, + widthQuery, +} from '@daohaus/ui'; +import { useParams } from 'react-router-dom'; +import styled from 'styled-components'; + +const DisplayContainer = styled.div` + margin-top: 2rem; + + .display-segment { + display: flex; + flex-direction: column; + } + + .data { + word-break: break-all; + margin-bottom: 2rem; + .space, + .address-display { + margin-bottom: 2rem; + } + } + .value-box { + display: flex; + } +`; + +export const ActionDisplay = ({ actions }: { actions: DecodedMultiTX }) => { + const { daochain } = useParams(); + const network = isValidNetwork(daochain) ? daochain : undefined; + const isMobile = useBreakpoint(widthQuery.sm); + + return ( + + {actions.map((action, index) => { + if (isActionError(action)) { + return ( +
+

Action {index + 1}: Error

+ {action.message} + + + HEX DATA: + + {action.data} +
+ ); + } + return ( +
+
+

+ Action {index + 1}: {action.name} +

+ + TARGET + + + + VALUE + + {action.value} + +
+ {action.params.map((arg, index) => { + return ( +
+ + + PARAM + {index + 1}:{' '} + + {arg.name} + + + TYPE: + {arg.type} + + + + VALUE: + + + +
+ ); + })} +
+ ); + })} +
+ ); +}; + +const ValueDisplay = ({ + argValue, + network, + isMobile, +}: { + argValue: ArgType; + network?: ValidNetwork; + isMobile?: boolean; +}) => { + if (Array.isArray(argValue)) { + return ( + <> + {argValue.map((value, index) => { + return ( +
+ + {index + 1 < argValue?.length && } +
+ ); + })} + + ); + } + if (isEthAddress(argValue)) { + return ( + + ); + } + if (typeof argValue === 'boolean') { + return {`${argValue}`}; + } + if (typeof argValue === 'string' || typeof argValue === 'number') { + return {argValue}; + } + + return {argValue.toString()}; +}; diff --git a/apps/core-app/src/components/ButtonLink.tsx b/apps/core-app/src/components/ButtonLink.tsx new file mode 100644 index 000000000..c10f5f7f9 --- /dev/null +++ b/apps/core-app/src/components/ButtonLink.tsx @@ -0,0 +1,43 @@ +import { Button, Link } from '@daohaus/ui'; +import React, { ComponentProps } from 'react'; + +type ProfileLinkProps = { + href?: string; + to?: string; + selected?: boolean; + disabled?: boolean; + linkType?: 'internal' | 'external' | 'no-icon-external'; + hideIcon?: boolean; + target?: string; + rel?: string; +} & Partial>; + +export const ButtonLink = ({ + href, + to, + selected, + target, + disabled, + children, + linkType, + hideIcon, + rel, + ...buttonProps +}: ProfileLinkProps) => { + return ( + + + + ); +}; diff --git a/apps/core-app/src/components/CancelProposal.tsx b/apps/core-app/src/components/CancelProposal.tsx new file mode 100644 index 000000000..672280604 --- /dev/null +++ b/apps/core-app/src/components/CancelProposal.tsx @@ -0,0 +1,112 @@ +import React, { useMemo } from 'react'; +import { useParams } from 'react-router-dom'; +import { + handleErrorMessage, + isGovernor, + PROPOSAL_STATUS, + TXLego, +} from '@daohaus/common-utilities'; +import { ITransformedProposal } from '@daohaus/dao-data'; +import { useHausConnect } from '@daohaus/daohaus-connect-feature'; +import { useTxBuilder } from '@daohaus/tx-builder-feature'; +import { Spinner, useToast } from '@daohaus/ui'; +import { useDao } from '@daohaus/dao-context'; + +import { ACTION_TX } from '../legos/tx'; +import { GatedButton } from './proposalCards/GatedButton'; + +export const CancelProposal = ({ + proposal, + onSuccess, +}: { + proposal: ITransformedProposal; + onSuccess: () => void; +}) => { + const { daochain } = useParams(); + const { fireTransaction } = useTxBuilder(); + const { chainId, address } = useHausConnect(); + const { errorToast, defaultToast, successToast } = useToast(); + const [isLoading, setIsLoading] = React.useState(false); + const { dao, refreshAll } = useDao(); + + const handleCancel = () => { + const { proposalId } = proposal; + + if (!proposalId) return; + setIsLoading(true); + fireTransaction({ + tx: { ...ACTION_TX.CANCEL, staticArgs: [proposalId] } as TXLego, + lifeCycleFns: { + onTxError: (error) => { + const errMsg = handleErrorMessage({ + error, + }); + errorToast({ title: 'Cancel Failed', description: errMsg }); + setIsLoading(false); + }, + onTxSuccess: () => { + defaultToast({ + title: 'Cancel Success', + description: 'Please wait for subgraph to sync', + }); + }, + onPollError: (error) => { + const errMsg = handleErrorMessage({ + error, + }); + errorToast({ title: 'Poll Error', description: errMsg }); + setIsLoading(false); + }, + onPollSuccess: () => { + successToast({ + title: 'Cancel Success', + description: 'Proposal cancelled', + }); + setIsLoading(false); + refreshAll(); + onSuccess(); + }, + }, + }); + }; + + const isConnectedToDao = + chainId === daochain + ? true + : 'You are not connected to the same network as the DAO'; + + const addressCanCancel = useMemo(() => { + const isProposer = + proposal.createdBy.toLowerCase() === address?.toLowerCase(); + + const sponsorBelowThreshold = + Number(proposal.sponsorMembership?.shares) < + Number(dao?.sponsorThreshold); + + const isGovernorShaman = dao?.shamen?.some((shaman) => { + return ( + shaman.shamanAddress.toLowerCase() === address?.toLowerCase() && + isGovernor(shaman.permissions) + ); + }); + + return isProposer || sponsorBelowThreshold || isGovernorShaman + ? true + : `Proposal can only be cancelled by the proposer, by a governance shaman or if the sponsor's shares have fallen below the sponsor threshold`; + }, [proposal, address, dao]); + + if (proposal.status !== PROPOSAL_STATUS.voting) { + return null; + } + + return ( + + {isLoading ? : 'Cancel'} + + ); +}; diff --git a/apps/core-app/src/components/DaoCard.tsx b/apps/core-app/src/components/DaoCard.tsx new file mode 100644 index 000000000..33640945d --- /dev/null +++ b/apps/core-app/src/components/DaoCard.tsx @@ -0,0 +1,153 @@ +import styled from 'styled-components'; + +import { + charLimit, + getNetworkName, + readableNumbers, +} from '@daohaus/common-utilities'; +import { ITransformedMembership } from '@daohaus/dao-data'; +import { + Badge, + Bold, + border, + ParLg, + ParMd, + ProfileAvatar, + Tag, +} from '@daohaus/ui'; + +import { ButtonLink } from './ButtonLink'; + +const StyledDaoCard = styled.div` + background-color: ${(props) => props.theme.card.bg}; + display: flex; + flex-direction: column; + width: 100%; + max-width: 34rem; + min-width: 26rem; + border: 1px solid ${(props) => props.theme.card.border}; + padding: 2.4rem; + border-radius: ${border.radius}; + .top-box { + display: flex; + justify-content: space-between; + margin-bottom: 1.3rem; + } + + .badge { + transform: translateX(-0.8rem); + } + .stats-box { + display: flex; + flex-direction: column; + margin-bottom: 2.4rem; + p { + margin-bottom: 0.6rem; + } + } + .tag-box { + font-size: 1.4rem; + margin-bottom: 2.4rem; + div { + margin-right: 1.5rem; + } + } +`; + +export const DaoCard = ({ + isDelegate, + dao, + activeMemberCount, + fiatTotal, + activeProposalCount, + totalProposalCount, + votingPower, + name, + networkId, + contractType, +}: ITransformedMembership) => { + return ( + +
+
+ + {activeProposalCount > 0 && ( + + )} +
+ {isDelegate && Delegate} +
+ + {name ? charLimit(name, 21) : charLimit(dao, 21)}{' '} + +
+ {activeMemberCount && ( + + + {readableNumbers.toNumber({ value: activeMemberCount })} + {' '} + {parseInt( + readableNumbers.toNumber({ value: activeMemberCount }) + ) === 1 + ? 'Member' + : 'Members'} + + )} + {fiatTotal != null && ( + + + {readableNumbers.toDollars({ + value: fiatTotal, + unit: 'USD', + separator: ' ', + })} + + + )} + {totalProposalCount && ( + + + {readableNumbers.toNumber({ value: totalProposalCount })} + {' '} + {parseInt( + readableNumbers.toNumber({ value: totalProposalCount }) + ) === 1 + ? 'Proposal' + : 'Proposals'} + + )} + {votingPower > 0 ? ( + + + {readableNumbers.toPercentDecimals({ + value: votingPower, + separator: '', + })} + {' '} + Voting Power + + ) : ( + No Voting Power + )} +
+
+ {getNetworkName(networkId)} + {contractType} +
+ + Go + +
+ ); +}; diff --git a/apps/core-app/src/components/DaoFilterDropdown.tsx b/apps/core-app/src/components/DaoFilterDropdown.tsx new file mode 100644 index 000000000..7244a4cbb --- /dev/null +++ b/apps/core-app/src/components/DaoFilterDropdown.tsx @@ -0,0 +1,119 @@ +import { NETWORK_DATA } from '@daohaus/common-utilities'; +import { + Button, + Dropdown, + DropdownButton, + DropdownMenuItem, + DropdownMenuLabel, + ParSm, +} from '@daohaus/ui'; +import { indigoDark } from '@radix-ui/colors'; +import { MouseEvent } from 'react'; +import { RiCheckLine, RiFilterFill } from 'react-icons/ri'; +import styled, { useTheme } from 'styled-components'; +import { FILTER_TYPE } from '../utils/hub'; + +// HOW CAN THIS BE GENERALIZED? + +type DAOFilterDropdownProps = { + filterNetworks: Record; + toggleNetworkFilter: (event: MouseEvent) => void; + filterDelegate: string; + toggleDelegateFilter: (event: MouseEvent) => void; +}; + +const IconFilter = styled(RiFilterFill)` + height: 1.8rem; + width: 1.8rem; + display: flex; + // USE THEME + fill: ${indigoDark.indigo10}; + :hover { + fill: ${indigoDark.indigo10}; + } +`; + +export const DAOFilterDropdown = ({ + filterNetworks, + toggleNetworkFilter, + filterDelegate, + toggleDelegateFilter, +}: DAOFilterDropdownProps) => { + const theme = useTheme(); + const networkButtons = Object.values(NETWORK_DATA).map((network) => { + const isActive = filterNetworks[network.chainId]; + + return ( + + +
{network.name}
+
+
+ ); + }); + return ( + + Filters + + } + > + + Networks + + {networkButtons} + + Delegation + + + +
I am a Delegate
+
+
+ + +
I have a Delegate
+
+
+
+ ); +}; diff --git a/apps/core-app/src/components/DaoList.tsx b/apps/core-app/src/components/DaoList.tsx new file mode 100644 index 000000000..9af183cce --- /dev/null +++ b/apps/core-app/src/components/DaoList.tsx @@ -0,0 +1,46 @@ +import styled from 'styled-components'; + +import { ITransformedMembership } from '@daohaus/dao-data'; +import { breakpoints } from '@daohaus/ui'; + +import { DaoCard } from './DaoCard'; +import { ListType } from './HomeDashboard'; +import { DaoTable } from './DaoTable'; + +export const DaoList = ({ + daoData, + isMobile, + listType, +}: { + daoData: ITransformedMembership[]; + isMobile: boolean; + listType: ListType; +}) => { + if (isMobile) { + return ; + } + + if (listType === ListType.Cards) return ; + if (listType === ListType.Table) return ; + + return null; +}; + +const CardListBox = styled.div` + display: flex; + flex-wrap: wrap; + column-gap: 4rem; + row-gap: 2rem; + justify-content: center; + @media (min-width: ${breakpoints.xs}) { + justify-content: flex-start; + } +`; + +const DaoCards = ({ daoData }: { daoData: ITransformedMembership[] }) => ( + + {daoData.map((dao) => ( + + ))} + +); diff --git a/apps/core-app/src/components/DaoProfile.tsx b/apps/core-app/src/components/DaoProfile.tsx index da29bc54b..db9c388d1 100644 --- a/apps/core-app/src/components/DaoProfile.tsx +++ b/apps/core-app/src/components/DaoProfile.tsx @@ -15,8 +15,8 @@ import { Button, Link, } from '@daohaus/ui'; +import { TDao } from '@daohaus/dao-context'; -import { TDao } from '../contexts/DaoContext'; import { TagList } from './TagList'; import { missingDaoProfileData } from '../utils/general'; diff --git a/apps/core-app/src/components/DaoTable.tsx b/apps/core-app/src/components/DaoTable.tsx new file mode 100644 index 000000000..3355bc7a1 --- /dev/null +++ b/apps/core-app/src/components/DaoTable.tsx @@ -0,0 +1,237 @@ +import React from 'react'; +import { useTable, Column, UseTableRowProps } from 'react-table'; +import styled from 'styled-components'; +import { indigoDark } from '@radix-ui/colors'; + +import { ITransformedMembership } from '@daohaus/dao-data'; +import { ProfileAvatar, Tag } from '@daohaus/ui'; +import { + charLimit, + getNetworkName, + readableNumbers, + truncateAddress, +} from '@daohaus/common-utilities'; + +// REVIEW NOTES +// Can this be refactored to use DaoHaus Table? + +interface IDaoTableData { + daoData: ITransformedMembership[]; +} + +const Table = styled.table` + width: 100%; + font-size: 1.6rem; + line-height: 2.4rem; + border-collapse: collapse; +`; + +const Thead = styled.thead``; + +const Th = styled.th` + color: ${indigoDark.indigo11}; + border-bottom: 1px solid ${indigoDark.indigo5}; + padding: 0.5rem; + text-align: left; +`; + +const Tr = styled.tr``; + +const Td = styled.td` + text-align: center; + padding: 1.5rem; +`; + +const TBody = styled.tbody``; + +const Highlight = styled.p` + text-align: left; + color: ${indigoDark.indigo9}; +`; + +const FirstHeader = styled.p` + text-align: left; + padding-left: 1.6rem; +`; + +const FirstCell = styled.div` + text-align: left; + display: flex; + gap: 1.2rem; + align-items: center; +`; + +const StyledLink = styled.a` + text-decoration: none; + color: unset; +`; + +type HubTableType = Omit & { + name: { name?: string; address: string; networkId?: string }; +}; + +export const DaoTable = ({ daoData }: IDaoTableData) => { + const tableData = React.useMemo( + () => + daoData.map((dao: ITransformedMembership) => ({ + name: { + name: charLimit(dao.name, 21), + address: dao.dao, + networkId: dao.networkId, + }, + activeProposalCount: dao.activeProposalCount, + fiatTotal: dao.fiatTotal, + activeMemberCount: dao.activeMemberCount, + votingPower: dao.votingPower, + networkId: dao.networkId, + delegatingTo: dao.delegatingTo, + memberAddress: dao.memberAddress, + safeAddress: dao.safeAddress, + dao: dao.dao, + isDelegate: dao.isDelegate, + totalProposalCount: dao.totalProposalCount, + contractType: dao.contractType, + })), + [daoData] + ); + + const exampleColumns = React.useMemo[]>( + () => [ + { + accessor: 'name', // accessor is the "key" in the data + Cell: ({ + value, + row, + }: { + value: { name?: string; address: string; networkId?: string }; + row: UseTableRowProps; + }) => { + return ( + + + + {charLimit(value.name, 21) || truncateAddress(value.address)} + + {row.original.isDelegate && ( + + Delegate + + )} + + ); + }, + Header: () => { + return ( + + {daoData?.length} {daoData?.length === 1 ? 'DAO' : 'DAOs'} + + ); + }, + }, + { + Header: 'Active Proposals', + accessor: 'activeProposalCount', + Cell: ({ value }: { value: string | number }) => { + return ( + + {readableNumbers.toNumberShort({ value, decimals: 1 })} + + ); + }, + }, + { + Header: 'Vaults', + accessor: 'fiatTotal', + Cell: ({ value }: { value?: number }) => { + return ( + + {value != null + ? readableNumbers.toDollars({ value, separator: ' ' }) + : '--'} + + ); + }, + }, + { + Header: 'Members', + accessor: 'activeMemberCount', + Cell: ({ value }: { value: string | number }) => { + return ( + + {readableNumbers.toNumberShort({ value, decimals: 1 })} + + ); + }, + }, + { + Header: 'Power', + accessor: 'votingPower', + Cell: ({ value }: { value: string | number }) => { + return ( + + {readableNumbers.toPercentDecimals({ + value, + separator: '', + })} + + ); + }, + }, + { + Header: 'Network', + accessor: 'networkId', + Cell: ({ value }: { value: string | undefined }) => { + return {getNetworkName(value)}; + }, + }, + { + Header: 'Delegate', + accessor: 'delegatingTo', + Cell: ({ value }: { value: string | undefined }) => { + return ( + + {value === undefined ? '--' : truncateAddress(value)} + + ); + }, + }, + ], + [daoData?.length] + ); + + const { getTableProps, getTableBodyProps, headerGroups, rows, prepareRow } = + useTable({ + columns: exampleColumns, + data: tableData, + }); + + return ( + + + {headerGroups.map((headerGroup) => ( + + {headerGroup.headers.map((column) => ( + + ))} + + ))} + + + {rows.map((row, i) => { + prepareRow(row); + return ( + + {row.cells.map((cell) => { + return ; + })} + + ); + })} + +
{column.render('Header')}
{cell.render('Cell')}
+ ); +}; diff --git a/apps/core-app/src/components/FakeMarkdown.tsx b/apps/core-app/src/components/FakeMarkdown.tsx index 75eba0fdc..a435c559d 100644 --- a/apps/core-app/src/components/FakeMarkdown.tsx +++ b/apps/core-app/src/components/FakeMarkdown.tsx @@ -7,7 +7,7 @@ import { SuccessMessage, WrappedTextArea, } from '@daohaus/ui'; -import { useDao } from '../contexts/DaoContext'; +import { useDao } from '@daohaus/dao-context'; import { useParams } from 'react-router-dom'; import { ENDPOINTS, @@ -60,6 +60,7 @@ const handleFetchPubId = async ( const result = await request(subgraphUrl, query, { safeId, }); + const pubId = result?.permissions?.[0]?.publication?.id; if (!shouldUpdate) return; if (pubId) { diff --git a/apps/core-app/src/components/FilterDropdown.tsx b/apps/core-app/src/components/FilterDropdown.tsx index 7bfc9b0fd..0f74577b8 100644 --- a/apps/core-app/src/components/FilterDropdown.tsx +++ b/apps/core-app/src/components/FilterDropdown.tsx @@ -39,7 +39,7 @@ const FilterDropdown = ({ filter, toggleFilter }: FilterDropdownProps) => { spacing=".6rem" trigger={ } > diff --git a/apps/core-app/src/components/GovernanceSettings.tsx b/apps/core-app/src/components/GovernanceSettings.tsx index b1c131bc0..a2e7b4edb 100644 --- a/apps/core-app/src/components/GovernanceSettings.tsx +++ b/apps/core-app/src/components/GovernanceSettings.tsx @@ -1,7 +1,16 @@ import styled from 'styled-components'; -import { H3, H4, DataIndicator, ParSm, widthQuery, Theme } from '@daohaus/ui'; +import { + H3, + H4, + DataIndicator, + ParSm, + widthQuery, + Theme, + Button, + Link, +} from '@daohaus/ui'; -import { TDao } from '../contexts/DaoContext'; +import { TDao } from '@daohaus/dao-context'; import { charLimit, formatPeriods, @@ -9,6 +18,7 @@ import { fromWei, getNetwork, INFO_COPY, + lowerCaseLootToken, } from '@daohaus/common-utilities'; import { useMemo } from 'react'; import { useParams } from 'react-router-dom'; @@ -22,7 +32,6 @@ const GovernanceContainer = styled.div` } `; -// putting this in place for when we bring in the action button const GovernanceCardHeader = styled.div` display: flex; justify-content: space-between; @@ -31,6 +40,14 @@ const GovernanceCardHeader = styled.div` margin-bottom: 3rem; `; +const TokensHeader = styled.div` + display: flex; + justify-content: space-between; + align-items: flex-end; + flex-wrap: wrap; + margin-top: 1rem; +`; + const DataGrid = styled.div` display: flex; flex-wrap: wrap; @@ -55,6 +72,15 @@ const TokenDataGrid = styled(DataGrid)` const StyledLink = styled.a` text-decoration: none; color: ${({ theme }: { theme: Theme }) => theme.link.color}; + :hover { + text-decoration: none; + } +`; + +const StyledButtonLink = styled(Link)` + :hover { + text-decoration: none; + } `; type GovernanceSettingsProps = { @@ -62,21 +88,43 @@ type GovernanceSettingsProps = { }; export const GovernanceSettings = ({ dao }: GovernanceSettingsProps) => { - const { daochain } = useParams(); + const { daochain, daoid } = useParams(); const networkData = useMemo(() => { if (!daochain) return null; return getNetwork(daochain); }, [daochain]); + const defaultValues = useMemo(() => { + if (!dao) return null; + return { + votingPeriod: dao.votingPeriod, + gracePeriod: dao.gracePeriod, + proposalOffering: dao.proposalOffering, + quorumPercent: dao.quorumPercent, + minRetention: dao.minRetentionPercent, + sponsorThreshold: dao.sponsorThreshold, + newOffering: dao.proposalOffering, + vStake: dao.sharesPaused, + nvStake: dao.lootPaused, + }; + }, [dao]); + return (

Governance Settings

+ + +
@@ -102,7 +150,7 @@ export const GovernanceSettings = ({ dao }: GovernanceSettingsProps) => { @@ -125,16 +173,27 @@ export const GovernanceSettings = ({ dao }: GovernanceSettingsProps) => { -

DAO Tokens

+ +

DAO Tokens

+ + + +

Voting

{ { { + // const { address } = useHausConnect(); + const { profile } = useParams(); + const isMobile = useBreakpoint(widthQuery.sm); + + const [daoData, setDaoData] = useState([]); + const [filterNetworks, setFilterNetworks] = + useState>(defaultNetworks); + const [filterDelegate, setFilterDelegate] = useState(''); + const [sortBy, setSortBy] = useState(DEFAULT_SORT_KEY); + const [searchTerm, setSearchTerm] = useState(''); + const [loading, setLoading] = useState(true); + const [listType, setListType] = useState(ListType.Cards); + + const debouncedSearchTerm = useDebounce(searchTerm, 500); + + useEffect(() => { + let shouldUpdate = true; + const getDaos = async (address: string) => { + setLoading(true); + try { + const haus = Haus.create(); + const query = await haus.profile.listDaosByMember({ + memberAddress: address, + networkIds: Object.keys(filterNetworks) as ValidNetwork[], + includeTokens: true, + daoFilter: { name_contains_nocase: debouncedSearchTerm }, + memberFilter: getDelegateFilter(filterDelegate, address), + ordering: SORT_FIELDS[sortBy].ordering, + }); + if (query.data?.daos && shouldUpdate) { + setDaoData(query.data.daos); + } + } catch (error) { + const errMsg = handleErrorMessage({ + error, + fallback: 'Error loading DAOs', + }); + console.error(errMsg); + } finally { + setLoading(false); + } + }; + if (!profile) return; + getDaos(profile); + return () => { + shouldUpdate = false; + }; + }, [profile, filterNetworks, filterDelegate, sortBy, debouncedSearchTerm]); + + const toggleNetworkFilter = (event: MouseEvent) => { + const network = event.currentTarget.value; + if (network && isValidNetwork(network)) { + filterNetworks[network] + ? setFilterNetworks((prevState) => { + delete prevState[network]; + return { ...prevState }; + }) + : setFilterNetworks((prevState) => ({ + ...prevState, + [network]: network, + })); + } + }; + const toggleDelegateFilter = (event: MouseEvent) => { + setFilterDelegate((prevState) => + prevState === event.currentTarget.value ? '' : event.currentTarget.value + ); + }; + const switchSortBy = (event: ChangeEvent) => { + setSortBy(event.target.value); + }; + + const toggleListType = () => { + setListType((prevState) => + prevState === ListType.Cards ? ListType.Table : ListType.Cards + ); + }; + + const tableControlProps = { + toggleNetworkFilter, + toggleDelegateFilter, + toggleListType, + switchSortBy, + setSearchTerm, + filterNetworks, + filterDelegate, + sortBy, + listType, + searchTerm, + totalDaos: daoData.length, + noun: { + singular: 'DAO', + plural: 'DAOs', + }, + }; + + if (loading) { + return ( + + + + ); + } + if (!daoData.length) { + return ( + + + + ); + } + + return ( + + + + ); +}; + +const CenterFrame = styled.div` + height: 30rem; + width: 100%; + display: flex; + align-items: center; + justify-content: center; + .inner { + position: absolute; + } +`; + +const Loading = ({ isMobile }: { isMobile: boolean }) => ( + +
+ +
+
+); +const NoDaosFound = () => ( + +

No Daos Found

+
+); diff --git a/apps/core-app/src/components/HomeNotConnected.tsx b/apps/core-app/src/components/HomeNotConnected.tsx new file mode 100644 index 000000000..703d11d2f --- /dev/null +++ b/apps/core-app/src/components/HomeNotConnected.tsx @@ -0,0 +1,96 @@ +import { breakpoints, H1, Italic, ParMd, ParXl } from '@daohaus/ui'; +import styled from 'styled-components'; + +const ViewBox = styled.div` + grid-area: body; + width: 100%; + height: 70rem; + display: flex; + background-image: url('hub-illustration.svg'); + background-size: auto 60rem; + background-repeat: no-repeat; + background-position: -10% 180%; + margin-top: 6.3rem; + .text-section { + width: 100%; + max-width: 40rem; + min-width: 28rem; + } + .hero { + font-size: 6rem; + font-weight: 900; + } + .tag-line { + font-size: 1.6rem; + margin-bottom: 3.2rem; + font-weight: 700; + } + ul { + margin-top: 2.4rem; + padding-inline-start: 2.4rem; + margin-top: 2.4rem; + } + li { + font-size: 1.6rem; + } + @media (min-width: ${breakpoints.xs}) { + height: 80rem; + background-size: auto 70rem; + background-position: 6rem 8rem; + } + @media (min-width: ${breakpoints.sm}) { + height: 90rem; + background-size: auto 80rem; + background-position: 20rem 10rem; + .hero { + font-size: 6.6rem; + } + .connect { + font-size: 3.6rem; + } + .tag-line { + font-size: 3.2rem; + } + ul { + margin-top: 3.2rem; + } + li { + font-size: 2.4rem; + margin-bottom: 1.6rem; + } + } + @media (min-width: ${breakpoints.md}) { + .text-section { + max-width: 52rem; + } + height: 100rem; + background-size: auto 90rem; + background-position: 110% 30%; + } +`; +export const HomeNotConnected = () => { + return ( + +
+

HUB

+ + Schelling point for all your DAO activity + + + Connect a wallet to: + +
    + +
  • See all your DAOs
  • +
    + +
  • View Active Proposals
  • +
    + +
  • Manage your shared profile
  • +
    +
+
+
+ ); +}; diff --git a/apps/core-app/src/components/ListActions.tsx b/apps/core-app/src/components/ListActions.tsx new file mode 100644 index 000000000..082c57596 --- /dev/null +++ b/apps/core-app/src/components/ListActions.tsx @@ -0,0 +1,121 @@ +import { Noun } from '@daohaus/common-utilities'; +import { + Button, + SingleColumnLayout, + useBreakpoint, + widthQuery, +} from '@daohaus/ui'; +import { indigoDark } from '@radix-ui/colors'; +import { ChangeEvent, MouseEvent, ReactNode } from 'react'; +import { RiGridFill, RiListCheck } from 'react-icons/ri'; +import styled from 'styled-components'; +import { sortOptions } from '../utils/hub'; +import { DAOFilterDropdown } from './DaoFilterDropdown'; +import { ListType } from './HomeDashboard'; +import SearchInput from './SearchInput'; +import { SortDropdown } from './SortDropdown'; + +type ListActionsProps = { + children: ReactNode; + searchTerm: string; + filterNetworks: Record; + listType: ListType; + toggleListType: () => void; + toggleNetworkFilter: (event: MouseEvent) => void; + filterDelegate: string; + toggleDelegateFilter: (event: MouseEvent) => void; + sortBy: string; + switchSortBy: (event: ChangeEvent) => void; + setSearchTerm: (term: string) => void; + totalDaos: number; + noun: Noun; +}; + +const IconGrid = styled(RiGridFill)` + height: 1.8rem; + width: 1.8rem; + display: flex; + fill: ${indigoDark.indigo10}; + :hover { + fill: ${indigoDark.indigo10}; + } +`; + +const IconList = styled(RiListCheck)` + height: 1.8rem; + width: 1.8rem; + display: flex; + fill: ${indigoDark.indigo10}; + :hover { + fill: ${indigoDark.indigo10}; + } +`; + +const ControlBarBox = styled.div` + display: flex; + width: 100%; + margin-bottom: 3rem; + flex-wrap: wrap; + gap: 1.6rem; + .list-toggle { + margin-right: auto; + } + @media ${widthQuery.sm} { + flex-direction: column; + } +`; + +export const ListActions = ({ + children, + searchTerm, + setSearchTerm, + totalDaos, + noun, + filterNetworks, + filterDelegate, + toggleNetworkFilter, + toggleDelegateFilter, + toggleListType, + listType, + sortBy, + switchSortBy, +}: ListActionsProps) => { + const isMobile = useBreakpoint(widthQuery.sm); + + return ( + + + + + {isMobile || ( + + )} + + + {children} + + ); +}; diff --git a/apps/core-app/src/components/ManageDelegate.tsx b/apps/core-app/src/components/ManageDelegate.tsx new file mode 100644 index 000000000..efb923577 --- /dev/null +++ b/apps/core-app/src/components/ManageDelegate.tsx @@ -0,0 +1,44 @@ +import { useMemo } from 'react'; +import { FormBuilder } from '@daohaus/haus-form-builder'; +import { useConnectedMembership, useDao } from '@daohaus/dao-context'; + +import { CustomFields } from '../legos/config'; +import { COMMON_FORMS } from '../legos/form'; + +type ManageDelegateProps = { + defaultMember?: string; +}; + +export const ManageDelegate = ({ defaultMember }: ManageDelegateProps) => { + const { connectedMembership } = useConnectedMembership(); + const { refreshAll } = useDao(); + + const defaultValues = useMemo(() => { + if (defaultMember) { + return { delegatingTo: defaultMember }; + } + if ( + connectedMembership && + connectedMembership.delegatingTo !== connectedMembership.memberAddress + ) { + return connectedMembership; + } + }, [connectedMembership, defaultMember]); + + const onFormComplete = () => { + refreshAll?.(); + }; + + if (!connectedMembership) return null; + + return ( + + ); +}; + +export default ManageDelegate; diff --git a/apps/core-app/src/components/MemberProfileMenu.tsx b/apps/core-app/src/components/MemberProfileMenu.tsx new file mode 100644 index 000000000..2c810024b --- /dev/null +++ b/apps/core-app/src/components/MemberProfileMenu.tsx @@ -0,0 +1,148 @@ +import { useMemo } from 'react'; +import { RiMore2Fill } from 'react-icons/ri'; +import { useParams } from 'react-router-dom'; +import styled from 'styled-components'; +import { useConnectedMembership } from '@daohaus/dao-context'; +import { + Dropdown, + DropdownMenuItem, + DropdownButton, + font, + Theme, + Dialog, + DialogTrigger, + DialogContent, + DropdownLink, + DropdownText, +} from '@daohaus/ui'; + +import ManageDelegate from './ManageDelegate'; + +export const ProfileMenuTrigger = styled(DropdownButton)` + padding: 0 4px 0 4px; + + &[data-state='open'] { + border-top-left-radius: 0; + border-bottom-left-radius: 0; + } + + svg.icon-right { + color: ${({ theme }: { theme: Theme }) => theme.primary}; + } + + svg.icon-left { + margin-right: 0; + } +`; + +export const ProfileMenuLink = styled(DropdownLink)` + font-weight: ${font.weight.bold}; +`; + +const ProfileMenuText = styled(DropdownText)` + border-radius: 2px; + color: ${(props) => props.theme.dropdown.text}; + font-weight: ${font.weight.bold}; + cursor: pointer; + display: flex; + padding: 1rem; + transition: 0.2s all; + width: 100%; + font-size: ${font.size.md}; + + svg { + margin-left: 0.3rem; + } + + :hover { + background-color: ${(props) => props.theme.navTabs.hoverNavLinkDropdownBg}; + border-color: ${(props) => props.theme.navTabs.hoverNavLinkDropdownBorder}; + text-decoration: none; + } + + &.disabled { + color: ${(props) => props.theme.dropdown.textDisabled}; + } +`; + +type MemberProfileMenuProps = { + memberAddress: string; +}; + +export const MemberProfileMenu = ({ + memberAddress, +}: MemberProfileMenuProps) => { + const { daoid, daochain } = useParams(); + const { connectedMembership } = useConnectedMembership(); + + const enableActions = useMemo(() => { + return ( + connectedMembership && + connectedMembership?.memberAddress !== memberAddress && + Number(connectedMembership.shares) > 0 + ); + }, [connectedMembership, memberAddress]); + + const isMenuForConnectedMember = useMemo(() => { + return connectedMembership?.memberAddress === memberAddress; + }, [connectedMembership, memberAddress]); + + return ( + } + side="left" + > + {isMenuForConnectedMember && ( + <> + + + + Delegate + + + + + + + + + Rage Quit + + + + )} + + {!isMenuForConnectedMember && ( + <> + + + + + Delegate To + + + + + + + + + + Guild Kick + + + + )} + + ); +}; diff --git a/apps/core-app/src/components/MembersOverview.tsx b/apps/core-app/src/components/MembersOverview.tsx index 16693d802..d1afdc5b2 100644 --- a/apps/core-app/src/components/MembersOverview.tsx +++ b/apps/core-app/src/components/MembersOverview.tsx @@ -1,13 +1,19 @@ import styled from 'styled-components'; import { Card, Theme, DataIndicator, widthQuery } from '@daohaus/ui'; -import { TDao } from '../contexts/DaoContext'; -import { charLimit, formatValueTo, fromWei } from '@daohaus/common-utilities'; +import { TDao } from '@daohaus/dao-context'; +import { + charLimit, + formatValueTo, + fromWei, + lowerCaseLootToken, +} from '@daohaus/common-utilities'; const MembersOverviewCard = styled(Card)` background-color: ${({ theme }: { theme: Theme }) => theme.card.hoverBg}; border: none; padding: 3rem; + width: 100%; `; const DataGrid = styled.div` @@ -33,7 +39,7 @@ export const MembersOverview = ({ dao }: MembersOverviewProps) => { return ( - + { })} /> { const { daochain, daoid } = useParams(); const { connectedMembership } = useConnectedMembership(); + const isMobile = useBreakpoint(widthQuery.sm); return ( <> @@ -62,7 +65,7 @@ export const MetadataSettings = ({ dao }: MetadataSettingsProps) => {

Metadata

{connectedMembership && Number(connectedMembership.shares) && ( - + )} @@ -100,6 +103,7 @@ export const MetadataSettings = ({ dao }: MetadataSettingsProps) => { address={dao.id} copy explorerNetworkId={daochain as keyof Keychain} + truncate={isMobile} />
@@ -107,6 +111,7 @@ export const MetadataSettings = ({ dao }: MetadataSettingsProps) => {
@@ -115,6 +120,7 @@ export const MetadataSettings = ({ dao }: MetadataSettingsProps) => { @@ -123,6 +129,7 @@ export const MetadataSettings = ({ dao }: MetadataSettingsProps) => { diff --git a/apps/core-app/src/components/Profile.tsx b/apps/core-app/src/components/Profile.tsx index 4f7a7c500..e6762f95f 100644 --- a/apps/core-app/src/components/Profile.tsx +++ b/apps/core-app/src/components/Profile.tsx @@ -1,7 +1,5 @@ -import React from 'react'; import styled from 'styled-components'; import { indigoDark } from '@radix-ui/colors'; -import { BiDotsVerticalRounded } from 'react-icons/bi'; import { breakpoints, @@ -21,8 +19,9 @@ import { fromWei, votingPowerPercentage, } from '@daohaus/common-utilities'; +import { TMembership, useDao } from '@daohaus/dao-context'; -import { TMembership, useDao } from '../contexts/DaoContext'; +import { MemberProfileMenu } from './MemberProfileMenu'; const AvatarLarge = styled(Avatar)` height: 12rem; @@ -70,10 +69,6 @@ const Container = styled.div` gap: 1rem; `; -const StyledMenuIcon = styled(BiDotsVerticalRounded)` - font-size: 1.8rem; -`; - const DataGrid = styled.div` display: flex; flex-wrap: wrap; @@ -140,7 +135,9 @@ export const Profile = ({ profile, membership }: ProfileProps) => { )} - + {membership && ( + + )} {membership && dao && ( diff --git a/apps/core-app/src/components/ProfileLink.tsx b/apps/core-app/src/components/ProfileLink.tsx index 5b5331de5..27f92944d 100644 --- a/apps/core-app/src/components/ProfileLink.tsx +++ b/apps/core-app/src/components/ProfileLink.tsx @@ -1,26 +1,21 @@ import { Button, Link } from '@daohaus/ui'; +import { ComponentProps } from 'react'; import { useParams } from 'react-router-dom'; type ProfileLinkProps = { memberAddress: string; - sm?: boolean; - lg?: boolean; - buttonText?: string; -}; +} & Partial>; export const ProfileLink = ({ memberAddress, - sm = false, - lg = false, - buttonText = 'Profile', + children, + ...rest }: ProfileLinkProps) => { const { daoid, daochain } = useParams(); return ( - + ); }; diff --git a/apps/core-app/src/components/ProposalCardOverview.tsx b/apps/core-app/src/components/ProposalCardOverview.tsx index a54c15f30..1e5d14590 100644 --- a/apps/core-app/src/components/ProposalCardOverview.tsx +++ b/apps/core-app/src/components/ProposalCardOverview.tsx @@ -1,30 +1,66 @@ import { useParams } from 'react-router-dom'; -import styled from 'styled-components'; -import { AddressDisplay, Button, ParLg, ParMd } from '@daohaus/ui'; +import styled, { useTheme } from 'styled-components'; import { + AddressDisplay, + Button, + ParLg, + ParMd, + Link, + useBreakpoint, + widthQuery, + Tooltip, + TintSecondary, +} from '@daohaus/ui'; +import { + charLimit, + dynamicDecimals, formatShortDateTimeFromSeconds, + formatValueTo, + fromWei, Keychain, + NETWORK_DATA, + ValidNetwork, } from '@daohaus/common-utilities'; -import { TProposals } from '../contexts/DaoContext'; +import { TProposals } from '@daohaus/dao-context'; +import { getProposalTypeLabel } from '../utils/general'; +import { ITransformedProposal } from '@daohaus/dao-data'; +import { RiTimeLine } from 'react-icons/ri'; -const OverviewContainer = styled.div` +const OverviewBox = styled.div` display: flex; flex-direction: column; - gap: 1.2rem; + margin-bottom: 1.1rem; + height: 100%; + .title { + margin-bottom: 2rem; + } + .description { + margin-bottom: auto; + } + @media ${widthQuery.md} { + .description { + margin-bottom: 2rem; + } + } `; -const OverviewHeader = styled.div` - width: 100%; +const SubmittedContainer = styled.div` display: flex; - justify-content: space-between; - align-items: flex-start; + + margin-top: 2rem; + .submitted-by { + margin-right: 1rem; + } + @media ${widthQuery.sm} { + flex-direction: column; + } `; -const SubmittedContainer = styled.div` - display: flex; - gap: 1rem; - margin-top: 2.1rem; +const StyledLink = styled(Link)` + :hover { + text-decoration: none; + } `; type ProposalCardOverviewProps = { @@ -34,21 +70,46 @@ type ProposalCardOverviewProps = { export const ProposalCardOverview = ({ proposal, }: ProposalCardOverviewProps) => { - const { daochain } = useParams(); + const { daochain, daoid } = useParams(); + const theme = useTheme(); + const isMobile = useBreakpoint(widthQuery.sm); + const isMd = useBreakpoint(widthQuery.md); return ( - - + + + {proposal.title} + + {charLimit(proposal.description, 145)} + + {Number(proposal.proposalOffering) > 0 && ( - {proposal.proposalType} |{' '} - {formatShortDateTimeFromSeconds(proposal.createdAt)} + Offering:{' '} + + {formatValueTo({ + value: fromWei(proposal.proposalOffering), + format: 'number', + unit: NETWORK_DATA[daochain as ValidNetwork]?.symbol, + decimals: dynamicDecimals({ + baseUnits: Number(proposal.proposalOffering), + }), + })} + - - - {proposal.title} - {proposal.description} + )} + {isMd && ( + + + + )} - Submitted by + + Submitted by:{' '} + + + ); +}; + +const OverviewContainer = styled.div` + width: 100%; + display: flex; + justify-content: space-between; + align-items: flex-start; + @media ${widthQuery.md} { + align-items: center; + margin-bottom: 2rem; + } +`; + +export const OverviewHeader = ({ + proposal, +}: { + proposal: ITransformedProposal; +}) => { + const { daochain, daoid } = useParams(); + + const theme = useTheme(); + const isMobile = useBreakpoint(widthQuery.md); + return ( + + {isMobile ? ( + <> + + {getProposalTypeLabel(proposal.proposalType)} + + } + /> + + ) : ( + <> + + {getProposalTypeLabel(proposal.proposalType)} |{' '} + {formatShortDateTimeFromSeconds(proposal.createdAt)} + + + + + + )} ); }; diff --git a/apps/core-app/src/components/ProposalDetailsGuts.tsx b/apps/core-app/src/components/ProposalDetailsGuts.tsx new file mode 100644 index 000000000..55faa4836 --- /dev/null +++ b/apps/core-app/src/components/ProposalDetailsGuts.tsx @@ -0,0 +1,111 @@ +import { useParams } from 'react-router-dom'; +import styled from 'styled-components'; +import { + AddressDisplay, + ParMd, + Link, + Theme, + border, + DataIndicator, +} from '@daohaus/ui'; +import { + dynamicDecimals, + formatShortDateTimeFromSeconds, + formatValueTo, + fromWei, + Keychain, + NETWORK_DATA, + ValidNetwork, +} from '@daohaus/common-utilities'; + +import { TProposals } from '@daohaus/dao-context'; +import { ProposalWarning } from './ProposalWarning'; + +const OverviewContainer = styled.div` + display: flex; + flex-direction: column; + margin-top: 1.2rem; + width: 100%; + padding: 2.8rem 3.6rem; + + border-radius: ${border.radius}; + border: 1px ${({ theme }: { theme: Theme }) => theme.card.border} solid; + background-color: ${({ theme }: { theme: Theme }) => theme.card.hoverBg}; +`; + +const DataContainer = styled.div` + display: flex; + flex-direction: row; + justify-content: space-between; + flex-wrap: wrap; + margin-top: 3rem; +`; + +const SpacedAddressDisplay = styled(AddressDisplay)` + margin-top: 1rem; + margin-bottom: 2rem; +`; + +const Spacer = styled.div` + margin-bottom: 2rem; +`; + +type ProposalDetailsGutsProps = { + proposal: TProposals[number]; +}; + +export const ProposalDetailsGuts = ({ proposal }: ProposalDetailsGutsProps) => { + const { daochain } = useParams(); + + return ( + + {proposal.description} + {proposal.contentURI && ( + + Link + + )} + +
+ Submitted by + +
+ +
+ + {Number(proposal.proposalOffering) > 0 && ( + + )} + + +
+ ); +}; diff --git a/apps/core-app/src/components/ProposalHistory.tsx b/apps/core-app/src/components/ProposalHistory.tsx new file mode 100644 index 000000000..9edce551a --- /dev/null +++ b/apps/core-app/src/components/ProposalHistory.tsx @@ -0,0 +1,46 @@ +import { useMemo } from 'react'; +import { useParams } from 'react-router-dom'; +import styled from 'styled-components'; +import { getNetwork } from '@daohaus/common-utilities'; + +import { TProposals } from '@daohaus/dao-context'; +import { ProposalHistoryCard } from './ProposalHistoryCard'; +import { + buildProposalHistory, + ProposalHistoryElement, +} from '../utils/historyHelpers'; + +const HistoryContainer = styled.div` + margin-top: 3rem; +`; + +type ProposalHistoryProps = { + proposal?: TProposals[number]; +}; + +export const ProposalHistory = ({ proposal }: ProposalHistoryProps) => { + const { daochain } = useParams(); + const historyData: ProposalHistoryElement[] | null = useMemo(() => { + if (!proposal || !daochain) return null; + return buildProposalHistory({ + proposal, + networkData: getNetwork(daochain), + }); + }, [proposal, daochain]); + + if (!historyData) return null; + + return ( + + {historyData.map((element) => { + return ( + + ); + })} + + ); +}; diff --git a/apps/core-app/src/components/ProposalHistoryCard.tsx b/apps/core-app/src/components/ProposalHistoryCard.tsx new file mode 100644 index 000000000..7cd788111 --- /dev/null +++ b/apps/core-app/src/components/ProposalHistoryCard.tsx @@ -0,0 +1,182 @@ +import { useParams } from 'react-router-dom'; +import { MouseEvent, useState } from 'react'; +import { RiArrowUpSLine, RiArrowDownSLine } from 'react-icons/ri'; +import styled from 'styled-components'; + +import { + Bold, + Theme, + ParMd, + AddressDisplay, + DataIndicator, + Button, + Dialog, + DialogTrigger, + DialogContent, + ParLg, +} from '@daohaus/ui'; +import { Keychain } from '@daohaus/common-utilities'; +import { ExplorerLink } from '@daohaus/daohaus-connect-feature'; + +import { + ProposalHistoryElement, + ProposalHistoryElementData, +} from '../utils/historyHelpers'; +import { TProposals } from '@daohaus/dao-context'; +import { VoteList } from './VoteList'; + +const ElementContainer = styled.div` + display: flex; + flex-direction: column; + justify-content: flex-start; + padding: 2rem 0; + border-bottom: 1px solid #ffffff16; +`; + +const VisibleContainer = styled.div` + display: flex; + flex-direction: row; + justify-content: space-between; + align-items: center; +`; + +const ContentContainer = styled.div` + display: flex; + flex-direction: column; + justify-content: flex-start; + gap: 1rem; +`; + +const StyledTitle = styled(Bold)` + color: ${({ theme, active }: { theme: Theme; active: boolean }) => + active && theme.link.color}; +`; + +const StyledUpArrow = styled(RiArrowUpSLine)` + font-size: 4.8rem; + font-weight: 900; + color: ${({ theme }: { theme: Theme }) => theme.link.color}; +`; + +const StyledDownArrow = styled(RiArrowDownSLine)` + font-size: 4.8rem; + font-weight: 900; + color: ${({ theme }: { theme: Theme }) => theme.link.color}; +`; + +const DataGrid = styled.div` + display: flex; + flex-direction: row; + justify-content: space-between; + margin-top: 2.4rem; +`; + +const SpacedAddressDisplay = styled(AddressDisplay)` + margin-top: 1rem; +`; + +const LinkContainer = styled.div` + display: flex; + justify-content: flex-end; + width: 100%; + margin-top: 2.5rem; +`; + +const DataPoint = ({ + data, + daochain, +}: { + data: ProposalHistoryElementData; + daochain?: string; +}) => { + if (data.dataType === 'member') { + return ( +
+ {data.label} + +
+ ); + } + + if (data.dataType === 'dataIndicator') { + return ; + } + + return null; +}; + +type ProposalHistoryCardProps = { + element: ProposalHistoryElement; + proposal?: TProposals[number]; +}; + +export const ProposalHistoryCard = ({ + element, + proposal, +}: ProposalHistoryCardProps) => { + const { daochain } = useParams(); + const [open, setOpen] = useState(false); + + const handleToggle = (event: MouseEvent) => { + setOpen((prevState) => !prevState); + }; + + const hasProposalVotes = + proposal && proposal.votes && proposal.votes.length > 0; + + return ( + + + + + {element.title} + + {element.text && {element.text}} + + {element.canExpand && open && ( +
+ +
+ )} + {element.canExpand && !open && ( +
+ +
+ )} + {element.showVotesButton && hasProposalVotes && ( + + + + + + + + + )} +
+ {element.canExpand && open && ( + <> + + {element.dataElements && + element.dataElements.map((data) => ( + + ))} + + + {element.txHash && ( + + + View Transaction + + + )} + + )} +
+ ); +}; diff --git a/apps/core-app/src/components/ProposalWarning.tsx b/apps/core-app/src/components/ProposalWarning.tsx new file mode 100644 index 000000000..a8b9c0a73 --- /dev/null +++ b/apps/core-app/src/components/ProposalWarning.tsx @@ -0,0 +1,63 @@ +import styled from 'styled-components'; +import { Card, ParXs, Theme } from '@daohaus/ui'; +import { useMemo } from 'react'; +import { PROPOSAL_TYPE_WARNINGS } from '../utils/constants'; +import { ExplorerLink } from '@daohaus/daohaus-connect-feature'; + +const WarningContainer = styled(Card)` + width: 100%; + background-color: ${({ theme, error }: { theme: Theme; error: boolean }) => + error && theme.card.warningBg}; + border-color: ${({ theme, error }: { theme: Theme; error: boolean }) => + error && theme.card.warningBorder}; +`; + +const StyledParXs = styled(ParXs)` + color: ${({ theme, error }: { theme: Theme; error: boolean }) => + error && theme.card.warningText}; +`; + +const Spacer = styled.div` + margin-top: 2rem; +`; + +type ProposalWarningProps = { + proposalType: string | undefined; + decodeError: boolean; + txHash: string; +}; + +export const ProposalWarning = ({ + proposalType, + decodeError, + txHash, +}: ProposalWarningProps) => { + const warningMessage: string = useMemo(() => { + if (decodeError) { + return PROPOSAL_TYPE_WARNINGS.ERROR_CANNOT_DECODE; + } else { + return ( + (proposalType && PROPOSAL_TYPE_WARNINGS[proposalType]) || + PROPOSAL_TYPE_WARNINGS.ERROR_UNKOWN + ); + } + }, [proposalType, decodeError]); + + const hasError = + decodeError || warningMessage === PROPOSAL_TYPE_WARNINGS.ERROR_UNKOWN; + + return ( + + {warningMessage} + {decodeError || + (hasError && ( + <> + + + View Details + + + ))} + + ); +}; diff --git a/apps/core-app/src/components/SearchInput.tsx b/apps/core-app/src/components/SearchInput.tsx index 3100cf8c7..eeb716e81 100644 --- a/apps/core-app/src/components/SearchInput.tsx +++ b/apps/core-app/src/components/SearchInput.tsx @@ -1,40 +1,25 @@ import { ChangeEvent, useEffect, useState } from 'react'; -import styled from 'styled-components'; -import { indigoDark } from '@radix-ui/colors'; import { BiSearch } from 'react-icons/bi'; -import { Input } from '@daohaus/ui'; +import { Field, Input } from '@daohaus/ui'; import useDebounce from '../utils/debounceHook'; - -const StyledInput = styled(Input)` - background: ${indigoDark.indigo3}; - color: ${indigoDark.indigo11}; - margin-right: 2rem; - ::placeholder { - color: ${indigoDark.indigo11}; - } - :focus { - background: ${indigoDark.indigo3}; - color: ${indigoDark.indigo11}; - } -`; - -const IconSearch = styled(BiSearch)` - fill: ${indigoDark.indigo11}; - :hover { - fill: ${indigoDark.indigo11}; - } -`; +import { Noun } from '@daohaus/common-utilities'; type SearchInputProps = { searchTerm: string; setSearchTerm: (term: string) => void; totalItems: number; -}; + noun?: Noun; +} & Partial; const SearchInput = ({ searchTerm, setSearchTerm, totalItems, + noun = { + singular: 'proposal', + plural: 'proposals', + }, + ...inputProps }: SearchInputProps) => { const [localSearchTerm, setLocalSearchTerm] = useState(''); @@ -55,14 +40,15 @@ const SearchInput = ({ }; return ( - ); }; diff --git a/apps/core-app/src/components/ShamanList.tsx b/apps/core-app/src/components/ShamanList.tsx index a7e6d5571..3449b32f0 100644 --- a/apps/core-app/src/components/ShamanList.tsx +++ b/apps/core-app/src/components/ShamanList.tsx @@ -1,7 +1,7 @@ import styled from 'styled-components'; -import { AddressDisplay, Button, DataSm, widthQuery } from '@daohaus/ui'; +import { AddressDisplay, Button, DataSm, Link, widthQuery } from '@daohaus/ui'; -import { TDao } from '../contexts/DaoContext'; +import { TDao } from '@daohaus/dao-context'; import { useParams } from 'react-router-dom'; import { Keychain } from '@daohaus/common-utilities'; @@ -28,12 +28,18 @@ const ShamanContainer = styled.div` } `; +const StyledLink = styled(Link)` + :hover { + text-decoration: none; + } +`; + type ShamanListProps = { shamen: TDao['shamen']; }; export const ShamanList = ({ shamen }: ShamanListProps) => { - const { daochain } = useParams(); + const { daochain, daoid } = useParams(); return ( <> @@ -55,7 +61,18 @@ export const ShamanList = ({ shamen }: ShamanListProps) => {
{shaman.permissions} - + + +
))} diff --git a/apps/core-app/src/components/ShamanSettings.tsx b/apps/core-app/src/components/ShamanSettings.tsx index e22a369ea..bad9c7aba 100644 --- a/apps/core-app/src/components/ShamanSettings.tsx +++ b/apps/core-app/src/components/ShamanSettings.tsx @@ -1,7 +1,7 @@ import styled from 'styled-components'; import { H3, ParSm } from '@daohaus/ui'; -import { TDao } from '../contexts/DaoContext'; +import { TDao } from '@daohaus/dao-context'; import { ShamanList } from './ShamanList'; const ShamanContainer = styled.div` @@ -34,9 +34,10 @@ export const ShamanSettings = ({ dao }: ShamanSettingsProps) => {
- Contracts that can adjust governance, shares, and memberships without - proposals. Be careful with adding new and it’s recommended that the - DAO removes any that aren’t needed any longer. + Shamans are contracts that can adjust governance, shares, and + memberships without proposals. Because shamans can affect the security + of the DAO, be cautious when adding new shamans, and remove any that + are no longer needed.
diff --git a/apps/core-app/src/components/SortDropdown.tsx b/apps/core-app/src/components/SortDropdown.tsx new file mode 100644 index 000000000..f704de27e --- /dev/null +++ b/apps/core-app/src/components/SortDropdown.tsx @@ -0,0 +1,45 @@ +import { Label, Select, SelectProps, widthQuery } from '@daohaus/ui'; +import styled from 'styled-components'; + +// SENDTO UI LIBRARY + +const SelectBox = styled.div` + display: flex; + align-items: center; + width: 32rem; + label { + display: block; + width: 10rem; + } + @media ${widthQuery.sm} { + width: 100%; + flex-direction: column; + align-items: flex-start; + label { + margin-bottom: 1rem; + } + } +`; +type SortDropdownProps = SelectProps; + +export const SortDropdown = ({ + id, + label = 'Sort By', + options, + ...props +}: SortDropdownProps) => { + return ( + + +