mirror of https://github.com/certusone/oyster.git
feat: mobile layout
This commit is contained in:
parent
bbeaf5c420
commit
d4a7ebd19a
|
@ -1,13 +1,10 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import './../../App.less';
|
import './../../App.less';
|
||||||
import { Divider, Layout, Menu } from 'antd';
|
import { Breadcrumb, Layout } from 'antd';
|
||||||
import { Link, useLocation } from 'react-router-dom';
|
import { Link, useLocation } from 'react-router-dom';
|
||||||
|
|
||||||
import { LABELS } from '../../constants';
|
import { LABELS } from '../../constants';
|
||||||
import config from './../../../package.json';
|
|
||||||
import { contexts, components } from '@oyster/common';
|
import { contexts, components } from '@oyster/common';
|
||||||
import { NewProposalMenuItem } from '../../views/proposal/new';
|
|
||||||
import { RegisterGovernanceMenuItem } from '../../views/governance/register';
|
|
||||||
import { Content, Header } from 'antd/lib/layout/layout';
|
import { Content, Header } from 'antd/lib/layout/layout';
|
||||||
import Logo from './dark-horizontal-combined-rainbow.inline.svg';
|
import Logo from './dark-horizontal-combined-rainbow.inline.svg';
|
||||||
|
|
||||||
|
@ -18,16 +15,30 @@ export const AppLayout = React.memo((props: any) => {
|
||||||
const { env } = useConnectionConfig();
|
const { env } = useConnectionConfig();
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
|
|
||||||
const paths: { [key: string]: string } = {
|
const breadcrumbNameMap: any = {
|
||||||
'/': '1',
|
'/governance': 'Governance',
|
||||||
'/dashboard': '2',
|
'/apps/1': 'Application1',
|
||||||
|
'/apps/2': 'Application2',
|
||||||
|
'/apps/1/detail': 'Detail',
|
||||||
|
'/apps/2/detail': 'Detail',
|
||||||
};
|
};
|
||||||
|
|
||||||
const current =
|
const pathSnippets = location.pathname.split('/').filter(i => i);
|
||||||
[...Object.keys(paths)].find(key => location.pathname.startsWith(key)) ||
|
const extraBreadcrumbItems = pathSnippets.map((_, index) => {
|
||||||
'';
|
const url = `/${pathSnippets.slice(0, index + 1).join('/')}`;
|
||||||
const defaultKey = paths[current] || '1';
|
return (
|
||||||
const theme = 'dark';
|
<Breadcrumb.Item key={url}>
|
||||||
|
<Link to={url}>{breadcrumbNameMap[url]}</Link>
|
||||||
|
</Breadcrumb.Item>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
const breadcrumbItems = [
|
||||||
|
<Breadcrumb.Item key="home">
|
||||||
|
<Link to="/">Home</Link>
|
||||||
|
</Breadcrumb.Item>,
|
||||||
|
].concat(extraBreadcrumbItems);
|
||||||
|
|
||||||
|
// TODO: add breadcrumb
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="App">
|
<div className="App">
|
||||||
|
@ -46,7 +57,8 @@ export const AppLayout = React.memo((props: any) => {
|
||||||
useWalletBadge={true}
|
useWalletBadge={true}
|
||||||
/>
|
/>
|
||||||
</Header>
|
</Header>
|
||||||
<Content style={{ padding: '0 50px', flexDirection: 'column' }}>
|
<Content>
|
||||||
|
{/* <Breadcrumb>{breadcrumbItems}</Breadcrumb> */}
|
||||||
{props.children}
|
{props.children}
|
||||||
</Content>
|
</Content>
|
||||||
</Layout>
|
</Layout>
|
||||||
|
|
|
@ -23,7 +23,7 @@ export function Routes() {
|
||||||
<Switch>
|
<Switch>
|
||||||
<Route exact path="/" component={() => <HomeView />} />
|
<Route exact path="/" component={() => <HomeView />} />
|
||||||
<Route path="/proposal/:id" children={<ProposalView />} />
|
<Route path="/proposal/:id" children={<ProposalView />} />
|
||||||
<Route path="/proposals/:id" children={<ProposalsView />} />
|
<Route path="/governance/:id" children={<ProposalsView />} />
|
||||||
|
|
||||||
<Route
|
<Route
|
||||||
exact
|
exact
|
||||||
|
|
|
@ -1,58 +1,92 @@
|
||||||
import { Col, List, Row } from 'antd';
|
import { Badge, Col, List, Row, Statistic } from 'antd';
|
||||||
import React, { useMemo, useState } from 'react';
|
import React, { useMemo, useState } from 'react';
|
||||||
import { useProposals } from '../../contexts/proposals';
|
import { useProposals } from '../../contexts/proposals';
|
||||||
import './style.less'; // Don't remove this line, it will break dark mode if you do due to weird transpiling conditions
|
import './style.less'; // Don't remove this line, it will break dark mode if you do due to weird transpiling conditions
|
||||||
import { TokenIcon } from '@oyster/common';
|
import { TokenIcon, useConnectionConfig, useWallet } from '@oyster/common';
|
||||||
import { Background } from './../../components/Background';
|
import { Background } from './../../components/Background';
|
||||||
import { useHistory } from 'react-router-dom';
|
import { useHistory } from 'react-router-dom';
|
||||||
import { RegisterGovernanceMenuItem } from '../governance/register';
|
import { RegisterGovernanceMenuItem } from '../governance/register';
|
||||||
|
import { TimelockStateStatus } from '../../models/timelock';
|
||||||
|
|
||||||
export const HomeView = () => {
|
export const HomeView = () => {
|
||||||
const history = useHistory();
|
const history = useHistory();
|
||||||
const { configs } = useProposals();
|
const { configs, proposals } = useProposals();
|
||||||
const [page, setPage] = useState(0);
|
const { connected } = useWallet();
|
||||||
const listData = useMemo(() => {
|
const listData = useMemo(() => {
|
||||||
const newListData: any[] = [];
|
const newListData: any[] = [];
|
||||||
|
|
||||||
Object.keys(configs).forEach(key => {
|
Object.keys(configs).forEach(configKey => {
|
||||||
const config = configs[key];
|
const config = configs[configKey];
|
||||||
|
const mint = config.info.governanceMint.toBase58();
|
||||||
|
|
||||||
|
const proposalCount = Object.keys(proposals).reduce((acc, proposalKey) => {
|
||||||
|
let proposal = proposals[proposalKey];
|
||||||
|
if(proposal.info.config.toBase58() == configKey) {
|
||||||
|
acc.active = acc.active + (
|
||||||
|
proposal.info.state.status === TimelockStateStatus.Voting ||
|
||||||
|
proposal.info.state.status === TimelockStateStatus.Draft ? 1 : 0);
|
||||||
|
|
||||||
|
acc.total = acc.total +1;
|
||||||
|
}
|
||||||
|
|
||||||
|
return acc;
|
||||||
|
}, {
|
||||||
|
active: 0,
|
||||||
|
total: 0,
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
newListData.push({
|
newListData.push({
|
||||||
href: '/proposals/' + key,
|
href: '/governance/' + configKey,
|
||||||
title: config.info.name,
|
title: config.info.name,
|
||||||
badge: <TokenIcon mintAddress={config.info.governanceMint} size={40} />,
|
badge: <Badge count={proposalCount.active}>
|
||||||
|
<TokenIcon mintAddress={mint} size={40} />
|
||||||
|
</Badge>,
|
||||||
|
proposalCount,
|
||||||
|
configKey,
|
||||||
config,
|
config,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
return newListData;
|
return newListData;
|
||||||
}, [configs]);
|
}, [configs, proposals]);
|
||||||
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Row>
|
<>
|
||||||
<Col flex="auto">
|
<Background />
|
||||||
<Background />
|
<Row>
|
||||||
<div className="governance-title">
|
<Col flex="auto" xxl={15} xs={24} className="governance-container">
|
||||||
<h1>Governance</h1>
|
<div className="governance-title">
|
||||||
<RegisterGovernanceMenuItem style={{ marginLeft: 'auto', marginRight: 0 }} />
|
<h1>Governance</h1>
|
||||||
</div>
|
<RegisterGovernanceMenuItem style={{ marginLeft: 'auto', marginRight: 0 }} disabled={!connected} />
|
||||||
<List
|
</div>
|
||||||
itemLayout="vertical"
|
<List
|
||||||
size="large"
|
itemLayout="vertical"
|
||||||
loading={listData.length === 0}
|
size="large"
|
||||||
pagination={false}
|
loading={listData.length === 0}
|
||||||
dataSource={listData}
|
pagination={false}
|
||||||
renderItem={item => (
|
dataSource={listData}
|
||||||
<List.Item key={item.title} className="governance-item" onClick={() => history.push(item.href)}>
|
|
||||||
<List.Item.Meta
|
renderItem={item => (
|
||||||
avatar={item.badge}
|
<List.Item key={item.title} className="governance-item" onClick={() => history.push(item.href)}
|
||||||
title={item.title}
|
extra={
|
||||||
description={item.description}
|
<>
|
||||||
/>
|
<Statistic title="Proposals" value={item.proposalCount.total} />
|
||||||
</List.Item>
|
</>
|
||||||
)}
|
}>
|
||||||
/>
|
<List.Item.Meta
|
||||||
</Col>
|
avatar={item.badge}
|
||||||
</Row>
|
title={item.title}
|
||||||
|
description={item.description}
|
||||||
|
>
|
||||||
|
</List.Item.Meta>
|
||||||
|
|
||||||
|
|
||||||
|
</List.Item>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,17 +1,23 @@
|
||||||
@import '~antd/dist/antd.dark.less';
|
@import '~antd/dist/antd.dark.less';
|
||||||
|
|
||||||
|
.governance-container {
|
||||||
|
margin-left: auto;
|
||||||
|
margin-right: auto;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
.governance-title {
|
.governance-title {
|
||||||
font-family: "FF Oxide Solid";
|
font-family: "FF Oxide Solid";
|
||||||
font-style: normal;
|
font-style: normal;
|
||||||
font-weight: 300;
|
font-weight: 300;
|
||||||
letter-spacing: 0px;
|
letter-spacing: 0px;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 240px;
|
top: -40px;
|
||||||
left: calc(20% + 10px);
|
|
||||||
z-index: 2;
|
z-index: 2;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
right: calc(20% + 10px);
|
width: 100%;
|
||||||
|
padding: 0px 10px;
|
||||||
|
|
||||||
h1 {
|
h1 {
|
||||||
font-size: 28px;
|
font-size: 28px;
|
||||||
|
@ -24,13 +30,32 @@
|
||||||
background-color: #202020;
|
background-color: #202020;
|
||||||
padding: 0px !important;
|
padding: 0px !important;
|
||||||
border-radius: 15px;
|
border-radius: 15px;
|
||||||
margin-left: 20%;
|
|
||||||
margin-right: 20%;
|
|
||||||
|
|
||||||
:hover {
|
.ant-badge-count {
|
||||||
cursor: pointer;
|
top: 8px !important;
|
||||||
background-color: #252425;
|
right: 12px !important;
|
||||||
border-radius: 15px;
|
}
|
||||||
|
|
||||||
|
.ant-list-item-extra {
|
||||||
|
user-select: none;
|
||||||
|
pointer-events: none;
|
||||||
|
display: flex;
|
||||||
|
margin-left: 0px !important;
|
||||||
|
margin-right: auto;
|
||||||
|
|
||||||
|
.ant-statistic {
|
||||||
|
margin-top: 5px;
|
||||||
|
padding: 0px 20px;
|
||||||
|
|
||||||
|
.ant-statistic-content {
|
||||||
|
font-size: 20px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-list-item-main {
|
||||||
|
flex: 0 !important;
|
||||||
|
min-width: 250px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.ant-list-item-meta {
|
.ant-list-item-meta {
|
||||||
|
@ -46,3 +71,9 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.governance-item:hover {
|
||||||
|
cursor: pointer;
|
||||||
|
background-color: #252425;
|
||||||
|
border-radius: 15px;
|
||||||
|
}
|
||||||
|
|
|
@ -2,10 +2,9 @@ import { Col, List, Row } from 'antd';
|
||||||
import React, { useMemo, useState } from 'react';
|
import React, { useMemo, useState } from 'react';
|
||||||
import { useConfig, useProposals } from '../../contexts/proposals';
|
import { useConfig, useProposals } from '../../contexts/proposals';
|
||||||
import './style.less'; // Don't remove this line, it will break dark mode if you do due to weird transpiling conditions
|
import './style.less'; // Don't remove this line, it will break dark mode if you do due to weird transpiling conditions
|
||||||
import { StateBadge, StateBadgeRibbon } from '../../components/Proposal/StateBadge';
|
import { StateBadge } from '../../components/Proposal/StateBadge';
|
||||||
import { urlRegex } from '../proposal';
|
|
||||||
import { useHistory, useParams } from 'react-router-dom';
|
import { useHistory, useParams } from 'react-router-dom';
|
||||||
import { TokenIcon } from '@oyster/common';
|
import { TokenIcon, useConnectionConfig, useWallet } from '@oyster/common';
|
||||||
import { NewProposalMenuItem } from '../proposal/new';
|
import { NewProposalMenuItem } from '../proposal/new';
|
||||||
const PAGE_SIZE = 10;
|
const PAGE_SIZE = 10;
|
||||||
|
|
||||||
|
@ -15,6 +14,13 @@ export const ProposalsView = () => {
|
||||||
const { proposals } = useProposals();
|
const { proposals } = useProposals();
|
||||||
const config = useConfig(id);
|
const config = useConfig(id);
|
||||||
const [page, setPage] = useState(0);
|
const [page, setPage] = useState(0);
|
||||||
|
const { tokenMap } = useConnectionConfig();
|
||||||
|
const { connected } = useWallet();
|
||||||
|
const token = tokenMap.get(config?.info.governanceMint.toBase58() || '') as any;
|
||||||
|
const tokenBackground = token?.extensions?.background || 'https://solana.com/static/8c151e179d2d7e80255bdae6563209f2/6833b/validators.webp';
|
||||||
|
|
||||||
|
const mint = config?.info.governanceMint.toBase58() || '';
|
||||||
|
|
||||||
const listData = useMemo(() => {
|
const listData = useMemo(() => {
|
||||||
const newListData: any[] = [];
|
const newListData: any[] = [];
|
||||||
|
|
||||||
|
@ -24,21 +30,12 @@ export const ProposalsView = () => {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
newListData.push({
|
newListData.push({
|
||||||
href: '/proposal/' + key,
|
href: '/proposal/' + key,
|
||||||
title: proposal.info.state.name,
|
title: proposal.info.state.name,
|
||||||
badge: <TokenIcon mintAddress={config?.info.governanceMint} size={30} />,
|
badge: <TokenIcon mintAddress={mint} size={30} />,
|
||||||
status: proposal.info.state.status,
|
status: proposal.info.state.status,
|
||||||
proposal,
|
proposal,
|
||||||
description: proposal.info.state.descLink.match(urlRegex) ? (
|
|
||||||
<a href={proposal.info.state.descLink} target={'_blank'}>
|
|
||||||
Link to markdown
|
|
||||||
</a>
|
|
||||||
) : (
|
|
||||||
proposal.info.state.descLink
|
|
||||||
),
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
return newListData;
|
return newListData;
|
||||||
|
@ -46,13 +43,18 @@ export const ProposalsView = () => {
|
||||||
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Row>
|
<Row style={{ background: `url(${tokenBackground})`, height: '100%' }}>
|
||||||
<Col flex="auto">
|
<Col flex="auto" xxl={15} xs={24} className="proposals-container">
|
||||||
<div className="proposals-header">
|
<div className="proposals-header">
|
||||||
<TokenIcon mintAddress={config?.info.governanceMint} size={40} />
|
<TokenIcon mintAddress={config?.info.governanceMint} size={60} style={{ marginRight: 20 }} />
|
||||||
<h1>{config?.info.name}</h1>
|
<div>
|
||||||
|
<h1>{config?.info.name}</h1>
|
||||||
|
<a href={tokenMap.get(mint)?.extensions?.website} target="_blank">
|
||||||
|
{tokenMap.get(mint)?.extensions?.website}
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
<NewProposalMenuItem className="proposals-new-btn" />
|
<NewProposalMenuItem className="proposals-new-btn" disabled={!connected} />
|
||||||
</div>
|
</div>
|
||||||
<List
|
<List
|
||||||
itemLayout="vertical"
|
itemLayout="vertical"
|
||||||
|
|
|
@ -1,10 +1,16 @@
|
||||||
@import '~antd/dist/antd.dark.less';
|
@import '~antd/dist/antd.dark.less';
|
||||||
|
|
||||||
|
.proposals-container {
|
||||||
|
margin-left: auto;
|
||||||
|
margin-right: auto;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
.proposals-header {
|
.proposals-header {
|
||||||
margin-left: 20%;
|
|
||||||
margin-right: 20%;
|
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
padding: 0px 10px;
|
||||||
|
text-align: left;
|
||||||
|
|
||||||
h1 {
|
h1 {
|
||||||
font-size: 28px;
|
font-size: 28px;
|
||||||
|
@ -22,8 +28,6 @@
|
||||||
background-color: #202020;
|
background-color: #202020;
|
||||||
padding: 0px !important;
|
padding: 0px !important;
|
||||||
border-radius: 15px;
|
border-radius: 15px;
|
||||||
margin-left: 20%;
|
|
||||||
margin-right: 20%;
|
|
||||||
|
|
||||||
:hover {
|
:hover {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
|
Loading…
Reference in New Issue