Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 30 additions & 0 deletions src/components/global/Breadcrumbs.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import React from 'react';
import { Link } from 'react-router-dom';

/**
* Simple breadcrumb navigation
* @param {Array<{label: string, to?: string}>} items
*/
const Breadcrumbs = ({ items = [] }) => {
if (!items.length) return null;
return (
<nav className="breadcrumbs" aria-label="Breadcrumb">
{items.map((item, idx) => {
const isLast = idx === items.length - 1;
return (
<span key={idx} className={isLast ? 'breadcrumb-current' : 'breadcrumb-item'}>
{item.to && !isLast ? (
<Link to={item.to}>{item.label}</Link>
) : (
<span aria-current={isLast ? 'page' : undefined}>{item.label}</span>
)}
{!isLast && <span className="breadcrumb-sep" aria-hidden>›</span>}
</span>
);
})}
</nav>
);
};

export default Breadcrumbs;

14 changes: 10 additions & 4 deletions src/components/governance/AddressList.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -181,9 +181,12 @@ const AddressList = ({proposal, vote, onAfterVote}) => {
// })
})
.catch(err => {
const message = (voteData === 'Invalid network version')
? 'Invalid network version'
: (err?.response?.data?.message || err?.message || 'Unknown error');
addressErrorVote.push({
name: address.name,
err: (voteData === 'Invalid network version') ? 'Invalid network version' : err.response.data.message
err: message
})
});
}
Expand Down Expand Up @@ -233,9 +236,12 @@ const AddressList = ({proposal, vote, onAfterVote}) => {
</>
) : (
<>
{
loadingAddress && <p>Loading...</p>
}
{loadingAddress && (
<div className="loading loading--center" role="status" aria-live="polite">
<div className="spinner" aria-hidden></div>
<span className="loading__label">Loading addresses…</span>
</div>
)}
{
(!loadingAddress && addressList.length === 0) && <p>You don't have a voting address please add one</p>
}
Expand Down
66 changes: 52 additions & 14 deletions src/components/governance/ProposalCard.jsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import React, { useEffect, useState } from "react";
import React, { useEffect, useState, useRef } from "react";
import { useTranslation } from "react-i18next";
import { Collapse } from "react-collapse";
import swal from "sweetalert2";

import { useUser } from "../../context/user-context";
import { VOTE_OUTCOME } from "../../constants/votes";
import ProposalCardInfo from "./ProposalCardInfo";
import CustomModal from "../global/CustomModal";
import AddressList from "./AddressList";
Expand Down Expand Up @@ -35,6 +36,9 @@ function ProposalCard({ proposal, enabled, userInfo, onLoadProposals }) {
const [month_remaining, setMonth_remaining] = useState(0);
const [payment_type, setPayment_type] = useState("");
const [vote, setVote] = useState("");
const titleButtonRef = useRef(null);
const touchStartX = useRef(null);
const touchStartY = useRef(null);

/**
* UseEffect that calculates the approx payment dates of the current proposal
Expand Down Expand Up @@ -74,6 +78,8 @@ function ProposalCard({ proposal, enabled, userInfo, onLoadProposals }) {
};
}, [proposal]);



/**
* Function that receives a string of a number with , as separator and returns it without it
* @function
Expand Down Expand Up @@ -157,7 +163,7 @@ function ProposalCard({ proposal, enabled, userInfo, onLoadProposals }) {
};

return (
<div className="proposal">
<div className={`proposal ${useCollapse ? 'proposal--expanded' : ''}`} role="region" aria-labelledby={`proposal-title-${proposal.Hash}`}>
<div className="vote-count">
<span className="yes">{proposal.YesCount}</span>
<span className="no">{proposal.NoCount}</span>
Expand All @@ -171,13 +177,36 @@ function ProposalCard({ proposal, enabled, userInfo, onLoadProposals }) {
<div className="description">
<div className="date">{proposalDate(proposal.CreationTime)}</div>

<span
title="more info"
style={{ cursor: "pointer" }}
<button
id={`proposal-title-${proposal.Hash}`}
ref={titleButtonRef}
className={`proposal-title ${useCollapse ? 'expanded' : ''}`}
aria-expanded={useCollapse}
aria-controls={`proposal-details-${proposal.Hash}`}
onClick={() => setUseCollapse(!useCollapse)}
onKeyDown={(e) => {
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault();
setUseCollapse(!useCollapse);
}
}}
onTouchStart={(e) => {
const t = e.changedTouches[0];
touchStartX.current = t.clientX;
touchStartY.current = t.clientY;
}}
onTouchEnd={(e) => {
const t = e.changedTouches[0];
const dx = t.clientX - (touchStartX.current ?? 0);
const dy = t.clientY - (touchStartY.current ?? 0);
if (Math.abs(dx) > 40 && Math.abs(dy) < 30) {
setUseCollapse((prev) => !prev);
}
}}
>
{proposal.title || proposal.name}
</span>
<span className="proposal-title__text">{proposal.title || proposal.name}</span>
<span className="proposal-title__icon" aria-hidden>{useCollapse ? '▾' : '▸'}</span>
</button>

<br />

Expand All @@ -191,47 +220,56 @@ function ProposalCard({ proposal, enabled, userInfo, onLoadProposals }) {
isOpened={useCollapse}
initialStyle={{ height: 0, overflow: "hidden" }}
>
<div className={"ReactCollapse--collapse"}>
<div className={"ReactCollapse--collapse"} id={`proposal-details-${proposal.Hash}`}>
<ProposalCardInfo
proposal={proposal}
days_remaining={days_remaining}
month_remaining={month_remaining}
payment_type={payment_type}
/>
<div className="proposal-back">
<button
className="btn btn--ghost"
onClick={() => {
setUseCollapse(false);
titleButtonRef.current && titleButtonRef.current.focus();
}}
aria-label={t('govlist.back_to_proposals', 'Back to Proposals')}
>
← {t('govlist.back_to_proposals', 'Back to Proposals')}
</button>
</div>
</div>
</Collapse>
</div>
{user && (
<div className="actions">
<button
style={{ border: "none", outline: "none" }}
className="vote vote--yes"
title={t("govlist.vote.yes_tooltip", "Vote YES - Support this proposal")}
aria-label={t("govlist.vote.yes_aria", "Vote yes for this proposal")}
disabled={userInfo ? false : true}
onClick={() => openMnVote(1)}
onClick={() => openMnVote(VOTE_OUTCOME.YES)}
>
<span className="vote-emoji">👍</span>
<span className="vote-label">{t("govlist.vote.yes", "Vote Yes")}</span>
</button>
<button
style={{ border: "none", outline: "none" }}
className="vote vote--abstain"
title={t("govlist.vote.abstain_tooltip", "ABSTAIN - Neutral vote")}
aria-label={t("govlist.vote.abstain_aria", "Abstain from voting on this proposal")}
disabled={userInfo ? false : true}
onClick={() => openMnVote(3)}
onClick={() => openMnVote(VOTE_OUTCOME.ABSTAIN)}
>
<span className="vote-emoji">➖</span>
<span className="vote-label">{t("govlist.vote.abstain", "Abstain")}</span>
</button>
<button
style={{ border: "none", outline: "none" }}
className="vote vote--no"
title={t("govlist.vote.no_tooltip", "Vote NO - Reject this proposal")}
aria-label={t("govlist.vote.no_aria", "Vote no for this proposal")}
disabled={userInfo ? false : true}
onClick={() => openMnVote(2)}
onClick={() => openMnVote(VOTE_OUTCOME.NO)}
>
<span className="vote-emoji">👎</span>
<span className="vote-label">{t("govlist.vote.no", "Vote No")}</span>
Expand Down
25 changes: 23 additions & 2 deletions src/components/governance/ProposalCardInfo.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -101,15 +101,36 @@ function ProposalCardInfo({
} Remaining)`}</span>
)}
</p>
<span style={{ lineHeight: "1.5" }}>Voting string:</span>
<span style={{ lineHeight: "1.5" }}>Voting strings (CLI):</span>
<div className="input-form">
<div className="form-group">
<div className="form-group" style={{marginBottom: '6px'}}>
<textarea
type="text"
className="styled"
style={{ resize: "none" }}
value={`gobject_vote_many ${proposal.Key} funding yes`}
disabled={true}
aria-label="CLI vote yes command"
/>
</div>
<div className="form-group" style={{marginBottom: '6px'}}>
<textarea
type="text"
className="styled"
style={{ resize: "none" }}
value={`gobject_vote_many ${proposal.Key} funding no`}
disabled={true}
aria-label="CLI vote no command"
/>
</div>
<div className="form-group">
<textarea
type="text"
className="styled"
style={{ resize: "none" }}
value={`gobject_vote_many ${proposal.Key} funding abstain`}
disabled={true}
aria-label="CLI vote abstain command"
/>
</div>
</div>
Expand Down
21 changes: 20 additions & 1 deletion src/components/governance/ProposalsList.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import React, { useEffect, useState, useCallback, useMemo, useRef } from 'react'
import axios from 'axios';
import { withTranslation } from "react-i18next";
import { useUser } from '../../context/user-context';
import swal from 'sweetalert2';

import { list, getUserInfo } from '../../utils/request';

Expand Down Expand Up @@ -83,12 +84,30 @@ function ProposalsList(props) {
}
}, [cancelSource, loadProposals, loadUserInfo]);

useEffect(() => {
if (dataload === 2) {
swal.fire({
toast: true,
position: 'top-end',
icon: 'error',
title: t('govlist.error_fetch', 'Failed to fetch proposals'),
showConfirmButton: false,
timer: 2500
});
}
}, [dataload, t]);


return (
<>
<SubTitle heading={t('govlist.table.title')} />
{
(dataload === 0) && <p className="text-center">{t('govlist.loading')}</p>
(dataload === 0) && (
<div className="loading loading--center" role="status" aria-live="polite">
<div className="spinner" aria-hidden></div>
<span className="loading__label">{t('govlist.loading', 'Loading proposals...')}</span>
</div>
)
}
{
(dataload === 1 && proposals.length > 0) && <div className="proposals">
Expand Down
15 changes: 15 additions & 0 deletions src/constants/votes.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
export const VOTE_SIGNAL = {
NONE: 0,
FUNDING: 1,
VALID: 2,
DELETE: 3,
ENDORSED: 4,
};

export const VOTE_OUTCOME = {
NONE: 0,
YES: 1,
NO: 2,
ABSTAIN: 3,
};

6 changes: 6 additions & 0 deletions src/pages/Governance.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import BackgroundInner from "../components/global/BackgroundInner";
import BannerImage from "../components/global/BannerImage";
import GovDetails from "../components/governance/GovDetails";
import ProposalsList from "../components/governance/ProposalsList";
import Breadcrumbs from "../components/global/Breadcrumbs";

const API_URI = process.env.REACT_APP_SYS_API_URI;

Expand Down Expand Up @@ -155,6 +156,10 @@ class Governance extends Component {
<div className="shell-large">
<div className="section__body">
<div className="articles">
<Breadcrumbs items={[
{ label: t('header.home', 'Home'), to: '/' },
{ label: t('header.governance', 'Governance') }
]} />
<BannerImage
heading={t("governance.heading")}
direction="top-right"
Expand All @@ -168,6 +173,7 @@ class Governance extends Component {
<div className="cols">
<div className="col col--size12">
<div className="article__content">
<h2 className="section-heading" id="proposals-heading">{t('govlist.table.title')}</h2>
<ProposalsList
statsData={this.state.statsData.stats.mn_stats}
/>
Expand Down
Loading