diff --git a/packages/prop-house-webapp/src/components/HousePropCard/HousePropCard.module.css b/packages/prop-house-webapp/src/components/HousePropCard/HousePropCard.module.css new file mode 100644 index 000000000..350028038 --- /dev/null +++ b/packages/prop-house-webapp/src/components/HousePropCard/HousePropCard.module.css @@ -0,0 +1,46 @@ +.propCard { + height: 300px; + cursor: pointer; + border: 1px solid var(--border-med); +} +.propCardHeader { + display: flex; + align-items: center; + font-weight: bold; + color: var(--brand-gray); + justify-content: space-between; +} +.voteDisplay { + display: flex; + align-items: center; +} +.voteDisplay svg { + margin-right: 6px; +} +.proposer { + display: flex; + align-items: center; + font-weight: bold; + font-size: 16px; +} +.propTitle { + margin: 12px 0 6px 0; + display: -webkit-box; + -webkit-line-clamp: 2; + -webkit-box-orient: vertical; + overflow: hidden; + text-overflow: ellipsis; +} + +.propImgContainer { +} +.propImgContainer img { + width: 100%; + height: 100%; + object-fit: cover; + min-height: 120px; + min-width: 120px; + max-height: 140px; + border-radius: 6px; + outline: 1px solid var(--border-light); +} diff --git a/packages/prop-house-webapp/src/components/HousePropCard/index.tsx b/packages/prop-house-webapp/src/components/HousePropCard/index.tsx new file mode 100644 index 000000000..cb2d82e4b --- /dev/null +++ b/packages/prop-house-webapp/src/components/HousePropCard/index.tsx @@ -0,0 +1,55 @@ +import React, { useState, useEffect } from 'react'; +import { useNavigate } from 'react-router-dom'; +import { Proposal } from '@prophouse/sdk-react'; +import Card, { CardBgColor, CardBorderRadius } from '../Card'; +import EthAddress from '../EthAddress'; +import classes from './HousePropCard.module.css'; +import { replaceIpfsGateway } from '../../utils/ipfs'; +import getFirstImageFromProp from '../../utils/getFirstImageFromProp'; +import { MdHowToVote } from 'react-icons/md'; + +const HousePropCard: React.FC<{ proposal: Proposal }> = ({ proposal }) => { + const navigate = useNavigate(); + + const [imgUrlFromProp, setImgUrlFromProp] = useState(undefined); + + useEffect(() => { + let imgUrl; + const getImg = async () => { + imgUrl = await getFirstImageFromProp(proposal); + setImgUrlFromProp(imgUrl); + }; + getImg(); + }, [proposal]); + return ( +
navigate(`/${proposal.round}/${proposal.id}`)}> + +
+ +
+ + {proposal.votingPower} +
+
+
{proposal.title}
+ + {imgUrlFromProp ? ( +
+ propCardImage +
+ ) : ( +

{proposal.tldr}

+ )} +
+
+ ); +}; +export default HousePropCard; diff --git a/packages/prop-house-webapp/src/components/HouseTabBar/HouseTabBar.module.css b/packages/prop-house-webapp/src/components/HouseTabBar/HouseTabBar.module.css new file mode 100644 index 000000000..25d6e482f --- /dev/null +++ b/packages/prop-house-webapp/src/components/HouseTabBar/HouseTabBar.module.css @@ -0,0 +1,47 @@ +.stickyContainer { + background: white; + top: 0; + position: sticky !important; + z-index: 1; + border-bottom: 1px var(--border-light) solid; +} + +.tabBarContainer { + display: flex; +} + +.tabOption { + display: flex; + gap: 10px; + border-bottom: 2px solid transparent; + padding-bottom: 12px; + margin-right: 20px; +} +.tabOption.selected { + border-bottom: 2px solid var(--border-dark); +} +.tabOption:hover { + cursor: pointer; + border-bottom: 2px solid var(--border-dark); +} + +.tabOptionName { + font-weight: 600; + color: var(--brand-gray); +} +.tabOptionName.selected, +.tabOptionName:hover { + color: var(--brand-black) !important; +} + +.tabOptionNumber { + font-weight: 700; + font-size: 14px; + border-radius: 6px; + padding: 0px 2px; + align-self: center; + background: var(--border-light); + color: var(--brand-gray); + min-width: 20px; + text-align: center; +} diff --git a/packages/prop-house-webapp/src/components/HouseTabBar/index.tsx b/packages/prop-house-webapp/src/components/HouseTabBar/index.tsx new file mode 100644 index 000000000..c0649114c --- /dev/null +++ b/packages/prop-house-webapp/src/components/HouseTabBar/index.tsx @@ -0,0 +1,42 @@ +import clsx from 'clsx'; +import classes from './HouseTabBar.module.css'; +import { Proposal, Round } from '@prophouse/sdk-react'; + +export enum SelectedTab { + Rounds, + Props, +} + +const HouseTabBar: React.FC<{ + rounds: Round[]; + proposals: Proposal[]; + selectedTab: SelectedTab; + setSelectedTab: React.Dispatch>; +}> = ({ rounds, proposals, selectedTab, setSelectedTab }) => { + const propsIsSelected = selectedTab === SelectedTab.Props; + const roundsIsSelected = selectedTab === SelectedTab.Rounds; + return ( +
+
setSelectedTab(SelectedTab.Rounds)} + > + + Rounds + + {rounds?.length} +
+
setSelectedTab(SelectedTab.Props)} + > + + Awarded Proposals + + {proposals?.length} +
+
+ ); +}; + +export default HouseTabBar; diff --git a/packages/prop-house-webapp/src/components/PropActivityItem/index.tsx b/packages/prop-house-webapp/src/components/PropActivityItem/index.tsx index d90df8ba8..31e0ac6c2 100644 --- a/packages/prop-house-webapp/src/components/PropActivityItem/index.tsx +++ b/packages/prop-house-webapp/src/components/PropActivityItem/index.tsx @@ -20,16 +20,22 @@ const PropActivityItem: React.FC<{ proposal: Proposal }> = ({ proposal }) => { }, [proposal]); return (
navigate(`/${proposal.round}/${proposal.id}`)}> - proposed  - {proposal.title} - {imgUrlFromProp && ( -
- propCardImage -
+ {imgUrlFromProp ? ( + <> + {proposal.title} +
+ propCardImage +
+ + ) : ( + <> + proposed  + {proposal.title} + )}
); diff --git a/packages/prop-house-webapp/src/pages/House/House.module.css b/packages/prop-house-webapp/src/pages/House/House.module.css index 008f81b22..2331d3db2 100644 --- a/packages/prop-house-webapp/src/pages/House/House.module.css +++ b/packages/prop-house-webapp/src/pages/House/House.module.css @@ -1,6 +1,5 @@ .houseContainer { background: var(--bg-dark); - min-height: 350px; padding: 1rem; } @@ -10,8 +9,7 @@ top: 0; position: sticky !important; z-index: 1; - padding-top: 12px; - border-bottom: 1px solid var(--border-light); + border-bottom: 1px var(--border-light) solid; } @media (max-width: 575px) { @@ -20,6 +18,6 @@ padding-left: 0 !important; } .stickyContainer { - padding: 12px 0px 8px 0px; + padding: 12px 0px 0px 0px; } } diff --git a/packages/prop-house-webapp/src/pages/House/index.tsx b/packages/prop-house-webapp/src/pages/House/index.tsx index 36e34518a..135460c4f 100644 --- a/packages/prop-house-webapp/src/pages/House/index.tsx +++ b/packages/prop-house-webapp/src/pages/House/index.tsx @@ -3,105 +3,54 @@ import { useAppSelector } from '../../hooks'; import HouseHeader from '../../components/HouseHeader'; import React, { useEffect, useState } from 'react'; import { Col, Container, Row } from 'react-bootstrap'; -import HouseUtilityBar from '../../components/HouseUtilityBar'; -import ErrorMessageCard from '../../components/ErrorMessageCard'; -import NoSearchResults from '../../components/NoSearchResults'; -import { sortRoundByStatus } from '../../utils/sortRoundByStatus'; -import { RoundStatus } from '../../components/StatusFilters'; -import { useTranslation } from 'react-i18next'; -import { Round, Timed, usePropHouse } from '@prophouse/sdk-react'; -import RoundCard from '../../components/RoundCard'; +import { Proposal, Round, usePropHouse } from '@prophouse/sdk-react'; import { CardType, cardServiceUrl } from '../../utils/cardServiceUrl'; import OpenGraphElements from '../../components/OpenGraphElements'; import { COMPLETED_ROUND_OVERRIDES, HIDDEN_ROUND_OVERRIDES } from '../../utils/roundOverrides'; import { removeHtmlFromString } from '../../utils/removeHtmlFromString'; +import JumboRoundCard from '../../components/JumboRoundCard'; +import HouseTabBar, { SelectedTab } from '../../components/HouseTabBar'; +import HousePropCard from '../../components/HousePropCard'; const House: React.FC<{}> = () => { const propHouse = usePropHouse(); const house = useAppSelector(state => state.propHouse.activeHouse); const [rounds, setRounds] = useState(); - const [loadingRounds, setLoadingRounds] = useState(false); - const [failedLoadingRounds, setFailedLoadingRounds] = useState(false); - const [roundsOnDisplay, setRoundsOnDisplay] = useState([]); - const [currentRoundStatus, setCurrentRoundStatus] = useState(RoundStatus.AllRounds); - const [input, setInput] = useState(''); - - const { t } = useTranslation(); - - const [numberOfRoundsPerStatus, setNumberOfRoundsPerStatus] = useState([]); + const [props, setProps] = useState(); + const [selectedTab, setSelectedTab] = useState(SelectedTab.Rounds); // fetch rounds useEffect(() => { if (!house || rounds) return; const fetchRounds = async () => { - setLoadingRounds(true); - try { - const rounds = (await propHouse.query.getRoundsForHouse(house.address)).map(round => { + const rounds = (await propHouse.query.getRoundsForHouse(house.address)) + .map(round => { if (COMPLETED_ROUND_OVERRIDES[round.address]) { round.state = COMPLETED_ROUND_OVERRIDES[round.address].state; } return round; }) .filter(round => !HIDDEN_ROUND_OVERRIDES.includes(round.address)); - setRounds(rounds); - - // Number of rounds under a certain status type in a House - setNumberOfRoundsPerStatus([ - // number of active rounds (proposing & voting) - rounds.filter( - r => - r.state === Timed.RoundState.IN_PROPOSING_PERIOD || - r.state === Timed.RoundState.IN_VOTING_PERIOD, - ).length, - rounds.length, - ]); - - // if there are no active rounds, default filter by all rounds - rounds.filter( - r => - r.state === Timed.RoundState.IN_PROPOSING_PERIOD || - r.state === Timed.RoundState.IN_VOTING_PERIOD, - ).length === 0 && setCurrentRoundStatus(RoundStatus.AllRounds); - - setLoadingRounds(false); - } catch (e) { - setLoadingRounds(false); - setFailedLoadingRounds(true); - } + setRounds(rounds); }; fetchRounds(); }, [house, propHouse.query, rounds]); - // search functionality + // fetch winning props useEffect(() => { - rounds && - // check if searching via input - (input.length === 0 - ? // if a filter has been clicked that isn't "All rounds" (default) - currentRoundStatus !== RoundStatus.Active - ? // filter by all rounds - setRoundsOnDisplay(rounds) - : // filter by active rounds (proposing & voting) - setRoundsOnDisplay( - rounds.filter( - r => - r.state === Timed.RoundState.IN_PROPOSING_PERIOD || - r.state === Timed.RoundState.IN_VOTING_PERIOD, - ), - ) - : // filter by search input that matches round title or description - setRoundsOnDisplay( - rounds.filter(round => { - const query = input.toLowerCase(); - return ( - round.title.toLowerCase().indexOf(query) >= 0 || - round.description?.toLowerCase().indexOf(query) >= 0 - ); - }), - )); - }, [input, currentRoundStatus, rounds, house, propHouse.query]); + if (!rounds || props) return; + + const fetchWinningProps = async () => { + const props = await propHouse.query.getProposals({ + where: { round_: { sourceChainRound_in: rounds.map(r => r.address) }, isWinner: true }, + }); + setProps(props); + }; + + fetchWinningProps(); + }); return ( <> @@ -118,15 +67,13 @@ const House: React.FC<{}> = () => { -
-
@@ -134,23 +81,19 @@ const House: React.FC<{}> = () => {
- {loadingRounds ? ( - <> - ) : !loadingRounds && failedLoadingRounds ? ( - - ) : roundsOnDisplay.length > 0 ? ( - sortRoundByStatus(roundsOnDisplay).map((round, index) => ( - - - - )) - ) : input === '' ? ( - - - - ) : ( - - )} + {selectedTab === SelectedTab.Rounds + ? rounds && + rounds.map((round, index) => ( + + + + )) + : props && + props.map((prop, i) => ( + + + + ))}
diff --git a/packages/prop-house-webapp/src/pages/Proposal/index.tsx b/packages/prop-house-webapp/src/pages/Proposal/index.tsx index db19d0abc..164981dca 100644 --- a/packages/prop-house-webapp/src/pages/Proposal/index.tsx +++ b/packages/prop-house-webapp/src/pages/Proposal/index.tsx @@ -21,6 +21,7 @@ import { useAccountModal } from '@rainbow-me/rainbowkit'; import useVotingPower from '../../hooks/useVotingPower'; import VoteConfirmationModal from '../../components/VoteConfirmationModal'; import { Timed, usePropHouse } from '@prophouse/sdk-react'; +import Skeleton from 'react-loading-skeleton'; const Proposal = () => { const params = useParams(); @@ -46,6 +47,7 @@ const Proposal = () => { useEffect(() => { const isSameProp = proposal && proposal.id === Number(id) && round && round.address === roundAddress; + if (!isSameProp) dispatch(setOnchainActiveProposal()); if (!roundAddress || !id || isSameProp) return; const fetchProposal = async () => { try { @@ -137,7 +139,12 @@ const Proposal = () => { ) : failedFetch ? ( ) : ( - + <> + + + + + )} {votingBar}