Merge pull request #1760 from SkynetLabs/dashboard-v2-main-screen

Dashboard V2: main screen
This commit is contained in:
Karol Wypchło 2022-03-01 11:18:28 +01:00 committed by GitHub
commit 8e648a32a0
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
30 changed files with 717 additions and 65 deletions

View File

@ -3,6 +3,7 @@ module.exports = {
addons: [ addons: [
"@storybook/addon-links", "@storybook/addon-links",
"@storybook/addon-essentials", "@storybook/addon-essentials",
"storybook-addon-gatsby",
{ {
name: "@storybook/addon-postcss", name: "@storybook/addon-postcss",
options: { options: {

View File

@ -9,6 +9,7 @@ module.exports = {
"gatsby-plugin-react-helmet", "gatsby-plugin-react-helmet",
"gatsby-plugin-sharp", "gatsby-plugin-sharp",
"gatsby-transformer-sharp", "gatsby-transformer-sharp",
"gatsby-plugin-styled-components",
"gatsby-plugin-postcss", "gatsby-plugin-postcss",
{ {
resolve: "gatsby-source-filesystem", resolve: "gatsby-source-filesystem",

View File

@ -0,0 +1,13 @@
import * as React from "react";
import "@fontsource/sora/300.css"; // light
import "@fontsource/sora/400.css"; // normal
import "@fontsource/sora/500.css"; // medium
import "@fontsource/sora/600.css"; // semibold
import "@fontsource/source-sans-pro/400.css"; // normal
import "@fontsource/source-sans-pro/600.css"; // semibold
import "./src/styles/global.css";
export function wrapPageElement({ element, props }) {
const Layout = element.type.Layout ?? React.Fragment;
return <Layout {...props}>{element}</Layout>;
}

View File

@ -24,6 +24,7 @@
"gatsby": "^4.6.2", "gatsby": "^4.6.2",
"gatsby-plugin-postcss": "^5.7.0", "gatsby-plugin-postcss": "^5.7.0",
"postcss": "^8.4.6", "postcss": "^8.4.6",
"pretty-bytes": "^6.0.0",
"react": "^17.0.1", "react": "^17.0.1",
"react-dom": "^17.0.1", "react-dom": "^17.0.1",
"react-helmet": "^6.1.0", "react-helmet": "^6.1.0",
@ -44,20 +45,23 @@
"autoprefixer": "^10.4.2", "autoprefixer": "^10.4.2",
"babel-eslint": "^10.1.0", "babel-eslint": "^10.1.0",
"babel-loader": "^8.2.3", "babel-loader": "^8.2.3",
"babel-plugin-preval": "^5.1.0",
"babel-plugin-styled-components": "^2.0.2", "babel-plugin-styled-components": "^2.0.2",
"eslint": "^8.9.0", "eslint": "^8.9.0",
"eslint-config-react-app": "^7.0.0", "eslint-config-react-app": "^7.0.0",
"eslint-plugin-storybook": "^0.5.6", "eslint-plugin-storybook": "^0.5.6",
"gatsby-plugin-alias-imports": "^1.0.5", "gatsby-plugin-alias-imports": "^1.0.5",
"gatsby-plugin-image": "^2.6.0", "gatsby-plugin-image": "^2.6.0",
"gatsby-plugin-preval": "^1.0.0",
"gatsby-plugin-provide-react": "^1.0.2", "gatsby-plugin-provide-react": "^1.0.2",
"gatsby-plugin-react-helmet": "^5.6.0", "gatsby-plugin-react-helmet": "^5.6.0",
"gatsby-plugin-sharp": "^4.6.0", "gatsby-plugin-sharp": "^4.6.0",
"gatsby-plugin-styled-components": "^5.7.0", "gatsby-plugin-styled-components": "^5.8.0",
"gatsby-source-filesystem": "^4.6.0", "gatsby-source-filesystem": "^4.6.0",
"gatsby-transformer-sharp": "^4.6.0", "gatsby-transformer-sharp": "^4.6.0",
"prettier": "2.5.1", "prettier": "2.5.1",
"react-is": "^17.0.2", "react-is": "^17.0.2",
"storybook-addon-gatsby": "^0.0.5",
"styled-components": "^5.3.3" "styled-components": "^5.3.3"
} }
} }

View File

@ -0,0 +1,71 @@
import * as React from "react";
import fileSize from "pretty-bytes";
import { Link } from "gatsby";
import { GraphBar } from "./GraphBar";
import { UsageGraph } from "./UsageGraph";
// TODO: get real data
const useUsageData = () => ({
files: {
used: 19_521,
limit: 20_000,
},
storage: {
used: 23_000_000_000,
limit: 1_000_000_000_000,
},
});
const size = (bytes) => {
const text = fileSize(bytes, { maximumFractionDigits: 1 });
const [value, unit] = text.split(" ");
return {
text,
value,
unit,
};
};
export default function CurrentUsage() {
const { files, storage } = useUsageData();
const storageUsage = size(storage.used);
const storageLimit = size(storage.limit);
const filesUsedLabel = React.useMemo(() => ({ value: files.used, unit: "files" }), [files.used]);
return (
<>
<h4>
{storageUsage.text} of {storageLimit.text}
</h4>
<p className="text-palette-400">
{files.used} of {files.limit} files
</p>
<div className="relative mt-7 font-sans uppercase text-xs">
<div className="flex place-content-between">
<span>Storage</span>
<span>{storageLimit.text}</span>
</div>
<UsageGraph>
<GraphBar value={storage.used} limit={storage.limit} label={storageUsage} />
<GraphBar value={files.used} limit={files.limit} label={filesUsedLabel} />
</UsageGraph>
<div className="flex place-content-between">
<span>Files</span>
<span className="inline-flex place-content-between w-[37%]">
<Link
to="/upgrade"
className="text-primary underline-offset-3 decoration-dotted hover:text-primary-light hover:underline"
>
UPGRADE
</Link>{" "}
{/* TODO: proper URL */}
<span>{files.limit}</span>
</span>
</div>
</div>
</>
);
}

View File

@ -0,0 +1,35 @@
import styled from "styled-components";
const Bar = styled.div.attrs({
className: `relative flex justify-end h-4 bg-primary rounded-l rounded-r-lg`,
})`
min-width: 1rem;
width: ${({ $percentage }) => $percentage}%;
`;
const BarTip = styled.span.attrs({
className: "relative w-4 h-4 border-2 rounded-full bg-white border-primary",
})``;
const BarLabel = styled.span.attrs({
className: "bg-white rounded border-2 border-palette-200 px-3 whitespace-nowrap absolute shadow",
})`
right: max(0%, ${({ $percentage }) => 100 - $percentage}%);
top: -0.5rem;
transform: translateX(50%);
`;
export const GraphBar = ({ value, limit, label }) => {
const percentage = typeof limit !== "number" || limit === 0 ? 0 : (value / limit) * 100;
return (
<div className="relative flex items-center">
<Bar $percentage={percentage}>
<BarTip />
</Bar>
<BarLabel $percentage={percentage}>
<span className="font-sora text-lg">{label.value}</span> <span>{label.unit}</span>
</BarLabel>
</div>
);
};

View File

@ -0,0 +1,9 @@
import styled from "styled-components";
export const UsageGraph = styled.div.attrs({
className: "w-full my-3 grid grid-flow-row grid-rows-2",
})`
height: 146px;
background: url(/images/usage-graph-bg.svg) no-repeat;
background-size: cover;
`;

View File

@ -0,0 +1,3 @@
import CurrentUsage from "./CurrentUsage";
export default CurrentUsage;

View File

@ -0,0 +1,22 @@
import * as React from "react";
import { Table, TableBody, TableCell, TableRow } from "../Table";
export default function ActivityTable({ data }) {
return (
<Table style={{ tableLayout: "fixed" }}>
<TableBody>
{data.map(({ name, type, size, uploaded, skylink }) => (
<TableRow key={skylink}>
<TableCell>{name}</TableCell>
<TableCell className="w-[80px]">{type}</TableCell>
<TableCell className="w-[80px]" align="right">
{size}
</TableCell>
<TableCell className="w-[180px]">{uploaded}</TableCell>
<TableCell>{skylink}</TableCell>
</TableRow>
))}
</TableBody>
</Table>
);
}

View File

@ -0,0 +1,24 @@
import * as React from "react";
import { Panel } from "../Panel";
import { Tab, TabPanel, Tabs } from "../Tabs";
import ActivityTable from "./ActivityTable";
import useRecentActivityData from "./useActivityData";
export default function LatestActivity() {
const { downloads, uploads } = useRecentActivityData();
return (
<Panel title="Latest activity">
<Tabs>
<Tab id="uploads" title="Uploads" />
<Tab id="downloads" title="Downloads" />
<TabPanel tabId="uploads" className="pt-4">
<ActivityTable data={uploads} />
</TabPanel>
<TabPanel tabId="downloads" className="pt-4">
<ActivityTable data={downloads} />
</TabPanel>
</Tabs>
</Panel>
);
}

View File

@ -0,0 +1,3 @@
import LatestActivity from "./LatestActivity";
export default LatestActivity;

View File

@ -0,0 +1,54 @@
const UPLOADS_DATA = [
{
name: "At_vereo_eos_censes",
type: ".mp4",
size: "2.45 MB",
uploaded: "a few seconds ago",
skylink: "_HyFqH632Rmy99c93idTtBVXeRDgaDAAWg6Bmm5P1izriu",
},
{
name: "Miriam Klein IV",
type: ".pdf",
size: "7.52 MB",
uploaded: "01/04/2021; 17:11",
skylink: "_izriuHyFqH632Rmy99c93idTtBVXeRDgaDAAWg6Bmm5P1",
},
{
name: "tmp/QmWR6eVDVkwhAYq7X99w4xT9KNKBzwK39Fj1PDmr4ZnzMm/QmWR6eVDVkwhAYq7X99w4xT9KNKBzwK39Fj1PDmr4ZnzMm",
type: ".doc",
size: "8.15 MB",
uploaded: "10/26/2020; 7:21",
skylink: "_VXeRDgaDAAWg6Bmm5P1izriuHyFqH632Rmy99c93idTtB",
},
{
name: "Perm_London",
type: ".avi",
size: "225.6 MB",
uploaded: "09/12/2020; 19:28",
skylink: "_eRDgaDAAWg6Bmm5P1izriuHyFqH632Rmy99c93idTtBVX",
},
{
name: "Santa_Clara",
type: ".pdf",
size: "7.52 MB",
uploaded: "09/12/2020; 19:23",
skylink: "_AWg6Bmm5P1izriuHyFqH632Rmy99c93idTtBVXeRDgaDA",
},
{
name: "Marysa_Labrone",
type: ".doc",
size: "8.15 MB",
uploaded: "09/12/2020; 19:21",
skylink: "_P1izriuHyFqH632Rmy99c93idTtBVXeRDgaDAAWg6Bmm5",
},
];
const DOWNLOADS_DATA = UPLOADS_DATA.slice().reverse();
// TODO: get real data
export default function useRecentActivityData() {
return {
uploads: UPLOADS_DATA,
downloads: DOWNLOADS_DATA,
};
}

View File

@ -1,21 +1,78 @@
import { Link } from "gatsby";
import styled from "styled-components"; import styled from "styled-components";
import { screen } from "../../lib/cssHelpers";
import { DropdownMenu, DropdownMenuLink } from "../DropdownMenu";
import { CogIcon, LockClosedIcon, SkynetLogoIcon } from "../Icons";
import { PageContainer } from "../PageContainer"; import { PageContainer } from "../PageContainer";
import { NavBarLink, NavBarSection } from ".";
const NavBarContainer = styled.div.attrs({ const NavBarContainer = styled.div.attrs({
className: `grid sticky top-0 bg-white`, className: `grid sticky top-0 bg-white`,
})``; })``;
const NavBarBody = styled.nav.attrs({ const NavBarBody = styled.nav.attrs({
className: "grid h-[80px] font-sans font-light text-sm", className: "grid font-sans font-light text-xs sm:text-sm",
})` })`
height: 100px;
grid-template-columns: 1fr 1fr;
grid-template-rows: 60px 40px;
grid-template-areas:
"logo dropdown"
"navigation navigation";
${screen(
"sm",
`
height: 80px;
grid-template-columns: auto max-content 1fr; grid-template-columns: auto max-content 1fr;
grid-template-areas: "logo navigation dropdown";
grid-template-rows: auto;
`
)}
.navigation-area {
grid-area: navigation;
display: grid;
grid-template-columns: repeat(3, 1fr);
}
.logo-area {
grid-area: logo;
justify-content: start;
}
.dropdown-area {
grid-area: dropdown;
}
`; `;
export const NavBar = (props) => ( export const NavBar = () => (
<NavBarContainer> <NavBarContainer>
<PageContainer> <PageContainer className="px-0">
<NavBarBody {...props} /> <NavBarBody>
<NavBarSection className="logo-area pl-2 pr-4 md:px-0 md:w-[110px] justify-center sm:justify-start">
<SkynetLogoIcon size={48} />
</NavBarSection>
<NavBarSection className="navigation-area border-t border-palette-100">
<NavBarLink to="/" as={Link} activeClassName="!border-b-primary">
Dashboard
</NavBarLink>
<NavBarLink to="/files" as={Link} activeClassName="!border-b-primary">
Files
</NavBarLink>
<NavBarLink to="/payments" as={Link} activeClassName="!border-b-primary">
Payments
</NavBarLink>
</NavBarSection>
<NavBarSection className="dropdown-area justify-end">
<DropdownMenu title="My account">
<DropdownMenuLink href="/settings" icon={CogIcon} label="Settings" />
<DropdownMenuLink href="/logout" icon={LockClosedIcon} label="Log out" />
</DropdownMenu>
</NavBarSection>
</NavBarBody>
</PageContainer> </PageContainer>
</NavBarContainer> </NavBarContainer>
); );

View File

@ -9,17 +9,7 @@ export default {
}, },
}; };
const Template = (props) => ( const Template = (props) => <NavBar {...props} />;
<NavBar {...props}>
<NavBarSection>
<NavBarLink href="/dashboard" active>
Dashboard
</NavBarLink>
<NavBarLink href="/files">Files</NavBarLink>
<NavBarLink href="/payments">Payments</NavBarLink>
</NavBarSection>
</NavBar>
);
export const DashboardTopNavigation = Template.bind({}); export const DashboardTopNavigation = Template.bind({});
DashboardTopNavigation.args = {}; DashboardTopNavigation.args = {};

View File

@ -3,7 +3,7 @@ import styled from "styled-components";
export const NavBarLink = styled.a.attrs(({ active }) => ({ export const NavBarLink = styled.a.attrs(({ active }) => ({
className: ` className: `
min-w-[168px] sm:min-w-[133px] lg:min-w-[168px]
flex h-full items-center justify-center flex h-full items-center justify-center
border-x border-x-palette-100 border-b-2 border-x border-x-palette-100 border-b-2
text-palette-600 transition-colors hover:bg-palette-100/50 text-palette-600 transition-colors hover:bg-palette-100/50

View File

@ -2,7 +2,7 @@ import PropTypes from "prop-types";
import styled from "styled-components"; import styled from "styled-components";
export const PageContainer = styled.div.attrs({ export const PageContainer = styled.div.attrs({
className: `mx-auto w-page md:w-page-md lg:w-page-lg xl:w-page-xl`, className: `mx-auto w-page lg:w-page-lg xl:w-page-xl px-2 md:px-16 lg:px-0`,
})``; })``;
PageContainer.propTypes = { PageContainer.propTypes = {

View File

@ -25,7 +25,7 @@ Panel.propTypes = {
/** /**
* Label of the panel * Label of the panel
*/ */
title: PropTypes.string, title: PropTypes.oneOfType([PropTypes.string, PropTypes.node]),
}; };
Panel.defaultProps = { Panel.defaultProps = {

View File

@ -0,0 +1,29 @@
import PropTypes from "prop-types";
export default function Bullets({ visibleSlides, activeIndex, allSlides, changeSlide }) {
if (allSlides <= visibleSlides) {
return null;
}
return (
<div className="flex gap-3 pt-6">
{Array(allSlides - visibleSlides + 1)
.fill(null)
.map((_, index) => (
<button
key={index}
type="button"
className={`rounded-full w-3 h-3 ${activeIndex === index ? "bg-primary" : "border-2 cursor-pointer"}`}
onClick={() => changeSlide(index)}
/>
))}
</div>
);
}
Bullets.propTypes = {
allSlides: PropTypes.number.isRequired,
activeIndex: PropTypes.number.isRequired,
visibleSlides: PropTypes.number.isRequired,
changeSlide: PropTypes.func.isRequired,
};

View File

@ -0,0 +1,12 @@
import styled from "styled-components";
import PropTypes from "prop-types";
const Slide = styled.div.attrs(({ isVisible }) => ({
className: `slider-slide transition-opacity ${isVisible ? "" : "opacity-50 cursor-pointer"}`,
}))``;
Slide.propTypes = {
isVisible: PropTypes.bool.isRequired,
};
export default Slide;

View File

@ -0,0 +1,127 @@
import * as React from "react";
import PropTypes from "prop-types";
import styled, { css } from "styled-components";
import theme from "../../lib/theme";
import useActiveBreakpoint from "./useActiveBreakpoint";
import Bullets from "./Bullets";
import Slide from "./Slide";
const Container = styled.div.attrs({
className: "slider w-full",
})``;
/**
* Styles applied to the movable element when the number of slide elements
* exceeds the number of visible slides for the current breakpoint
* */
const scrollableStyles = css`
${({ $allSlides, $visibleSlides, $activeIndex }) => `
transform: translateX(calc(-1 * ${$activeIndex} * ((100% + 1rem) / ${$visibleSlides})));
grid-template-columns: repeat(${$allSlides}, calc((100% - ${$visibleSlides - 1}rem) / ${$visibleSlides}));
`}
`;
const Scroller = styled.div.attrs({
className: "slider-scroller grid gap-4 transition-transform",
})`
${({ $scrollable }) => ($scrollable ? scrollableStyles : "")}
`;
const Slider = ({ slides, breakpoints }) => {
const { visibleSlides, scrollable } = useActiveBreakpoint(breakpoints);
const [activeIndex, setActiveIndex] = React.useState(0);
const changeSlide = React.useCallback(
(index) => {
setActiveIndex(Math.min(index, slides.length - visibleSlides)); // Don't let it scroll too far
},
[slides, visibleSlides, setActiveIndex]
);
React.useEffect(() => {
const maxIndex = slides.length - visibleSlides;
// Make sure to not scroll too far when screen size changes.
if (activeIndex > maxIndex) {
setActiveIndex(maxIndex);
}
}, [slides.length, visibleSlides, activeIndex]);
return (
<Container>
<Scroller
$visibleSlides={visibleSlides}
$allSlides={slides.length}
$activeIndex={activeIndex}
$scrollable={scrollable}
>
{slides.map((slide, index) => {
const isVisible = index >= activeIndex && index < activeIndex + visibleSlides;
return (
<div key={`slide-${index}`}>
<Slide
isVisible={isVisible || !scrollable}
onClick={scrollable && !isVisible ? () => changeSlide(index) : null}
>
{slide}
</Slide>
</div>
);
})}
</Scroller>
{scrollable && (
<Bullets
activeIndex={activeIndex}
allSlides={slides.length}
visibleSlides={visibleSlides}
changeSlide={changeSlide}
/>
)}
</Container>
);
};
Slider.propTypes = {
slides: PropTypes.arrayOf(PropTypes.node.isRequired),
breakpoints: PropTypes.arrayOf(
PropTypes.shape({
/**
* Breakpoint name as defined in Tailwind config. If not defined, config
* will be applied to all non-configured screen sizes.
*/
name: PropTypes.string,
/**
* Number of slides visible for a given breakpoint.
*/
visibleSlides: PropTypes.number.isRequired,
/**
* Whether or not the list should be scrollable horizontally at the given breakpoint.
* If set to false, all slides will be visible & rendered in a column.
*/
scrollable: PropTypes.bool.isRequired,
})
),
};
Slider.defaultProps = {
breakpoints: [
{
name: "xl",
scrollable: true,
visibleSlides: 3,
},
{
name: "md",
scrollable: true,
visibleSlides: 2,
},
{
// For the smallest screens, we won't scroll but instead stack the slides vertically.
scrollable: false,
visibleSlides: 1,
},
],
};
export default Slider;

View File

@ -0,0 +1 @@
export * from "./Slider";

View File

@ -0,0 +1,38 @@
import { useEffect, useMemo, useCallback, useState } from "react";
import { useWindowSize } from "react-use";
import theme from "../../lib/theme";
const { screens } = theme;
export default function useActiveBreakpoint(breakpoints) {
const { width: windowWidth } = useWindowSize();
const monitoredBreakpoints = useMemo(
() =>
breakpoints
.slice()
// Map breakpoint names to their min-width configured in Tailwind
.map(({ name, ...config }) => {
// If breakpoint name is not configured,
// we'll apply this config to all unmatched breakpoints.
const minWidth = screens[name] ? parseInt(screens[name], 10) : -Infinity;
return { minWidth, ...config };
})
// Since our breakpoints are setup with min-width rule, we need to sort them from largest to smallest
.sort(({ minWidth: widthA }, { minWidth: widthB }) => widthB - widthA),
[breakpoints]
);
const findActiveBreakpoint = useCallback(
() => monitoredBreakpoints.find((breakpoint) => windowWidth >= breakpoint.minWidth),
[monitoredBreakpoints, windowWidth]
);
const [activeBreakpoint, setActiveBreakpoint] = useState(findActiveBreakpoint());
useEffect(() => {
setActiveBreakpoint(findActiveBreakpoint());
}, [findActiveBreakpoint]);
return activeBreakpoint;
}

View File

@ -4,7 +4,7 @@ import styled from "styled-components";
* Accepts all HMTL attributes a `<td>` element does. * Accepts all HMTL attributes a `<td>` element does.
*/ */
export const TableCell = styled.td.attrs({ export const TableCell = styled.td.attrs({
className: `px-6 py-4 h-tableRow truncate className: `first:pl-6 last:pr-6 px-2 py-4 h-tableRow truncate
text-palette-600 even:text-palette-400 text-palette-600 even:text-palette-400
first:rounded-l-sm last:rounded-r-sm`, first:rounded-l-sm last:rounded-r-sm`,
})` })`

View File

@ -1,6 +1,7 @@
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import PropTypes from "prop-types"; import PropTypes from "prop-types";
import styled from "styled-components"; import styled from "styled-components";
import { useWindowSize } from "react-use";
const Wrapper = styled.div.attrs({ const Wrapper = styled.div.attrs({
className: "absolute left-0 bottom-0 w-full h-0.5 bg-palette-200", className: "absolute left-0 bottom-0 w-full h-0.5 bg-palette-200",
@ -15,6 +16,7 @@ const Indicator = styled.div.attrs({
export const ActiveTabIndicator = ({ tabRef }) => { export const ActiveTabIndicator = ({ tabRef }) => {
const [position, setPosition] = useState(0); const [position, setPosition] = useState(0);
const [width, setWidth] = useState(0); const [width, setWidth] = useState(0);
const { width: windowWidth } = useWindowSize();
useEffect(() => { useEffect(() => {
if (!tabRef?.current) { if (!tabRef?.current) {
@ -24,7 +26,7 @@ export const ActiveTabIndicator = ({ tabRef }) => {
const { offsetLeft, offsetWidth } = tabRef.current; const { offsetLeft, offsetWidth } = tabRef.current;
setPosition(offsetLeft); setPosition(offsetLeft);
setWidth(offsetWidth); setWidth(offsetWidth);
}, [tabRef]); }, [tabRef, windowWidth]);
return ( return (
<Wrapper> <Wrapper>
@ -33,6 +35,9 @@ export const ActiveTabIndicator = ({ tabRef }) => {
); );
}; };
// Needed, because we're using an Element constant here which Gatsby doesn't recognize during build time.
if (typeof window !== "undefined") {
ActiveTabIndicator.propTypes = { ActiveTabIndicator.propTypes = {
tabRef: PropTypes.oneOfType([PropTypes.func, PropTypes.shape({ current: PropTypes.instanceOf(Element) })]), tabRef: PropTypes.oneOfType([PropTypes.func, PropTypes.shape({ current: PropTypes.instanceOf(Element) })]),
}; };
}

View File

@ -1,50 +1,23 @@
import * as React from "react"; import * as React from "react";
import { Link } from "gatsby";
import styled from "styled-components"; import styled from "styled-components";
import { DropdownMenu, DropdownMenuLink } from "../components/DropdownMenu";
import { PageContainer } from "../components/PageContainer"; import { PageContainer } from "../components/PageContainer";
import { CogIcon, SkynetLogoIcon, LockClosedIcon } from "../components/Icons"; import { NavBar } from "../components/Navbar";
import { NavBar, NavBarLink, NavBarSection } from "../components/Navbar";
import { Footer } from "../components/Footer"; import { Footer } from "../components/Footer";
const Layout = styled.div.attrs({ const Layout = styled.div.attrs({
className: "h-screen overflow-hidden", className: "min-h-screen overflow-hidden",
})` })`
background-image: url(/images/dashboard-bg.svg); background-image: url(/images/dashboard-bg.svg);
background-position: -300px -280px; background-position: center -280px;
background-repeat: no-repeat;
.navbar {
grid-template-columns: auto max-content 1fr;
}
`; `;
const DashboardLayout = ({ children }) => { const DashboardLayout = ({ children }) => {
return ( return (
<> <>
<Layout> <Layout>
<NavBar className="navbar"> <NavBar />
<NavBarSection className="w-[110px] justify-start">
<SkynetLogoIcon size={48} />
</NavBarSection>
<NavBarSection>
<NavBarLink to="/" as={Link} activeClassName="!border-b-primary">
Dashboard
</NavBarLink>
<NavBarLink to="/files" as={Link} activeClassName="!border-b-primary">
Files
</NavBarLink>
<NavBarLink to="/payments" as={Link} activeClassName="!border-b-primary">
Payments
</NavBarLink>
</NavBarSection>
<NavBarSection className="justify-end">
<DropdownMenu title="My account">
<DropdownMenuLink href="/settings" icon={CogIcon} label="Settings" />
<DropdownMenuLink href="/logout" icon={LockClosedIcon} label="Log out" />
</DropdownMenu>
</NavBarSection>
</NavBar>
<PageContainer> <PageContainer>
<main className="mt-14">{children}</main> <main className="mt-14">{children}</main>
</PageContainer> </PageContainer>

View File

@ -0,0 +1,21 @@
import { css } from "styled-components";
import theme from "./theme";
export const screen = (breakpoint, style) => {
const { screens } = theme;
const minWidth = screens[breakpoint];
if (typeof minWidth === "undefined") {
throw ReferenceError(
`Screen "${breakpoint}" is not defined in Tailwind config. Available values are: ${Object.keys(screens).join(
", "
)}.`
);
}
return css`
@media (min-width: ${minWidth}) {
${style}
}
`;
};

View File

@ -0,0 +1,8 @@
// @preval
// This file is pre-evaluated on build-time, so the config is only resolved once
// and then included in the bundle for us to use in dynamic styles.
import resolveConfig from "tailwindcss/resolveConfig";
import tailwindConfig from "../../tailwind.config.js";
export default resolveConfig(tailwindConfig).theme;

View File

@ -1,9 +1,67 @@
import * as React from "react"; import * as React from "react";
import { useMedia } from "react-use";
import theme from "../lib/theme";
import { ArrowRightIcon } from "../components/Icons";
import { Panel } from "../components/Panel";
import { Tab, TabPanel, Tabs } from "../components/Tabs";
import LatestActivity from "../components/LatestActivity/LatestActivity";
import DashboardLayout from "../layouts/DashboardLayout"; import DashboardLayout from "../layouts/DashboardLayout";
import Slider from "../components/Slider/Slider";
import CurrentUsage from "../components/CurrentUsage";
const IndexPage = () => { const IndexPage = () => {
return <>Dashboard</>; const showRecentActivity = useMedia(`(min-width: ${theme.screens.md})`);
return (
<>
<div className="w-full">
<Slider
slides={[
<Panel title="Upload">
<Tabs variant="fill">
<Tab id="files" title="Files" />
<Tab id="directory" title="Directory" />
<TabPanel tabId="files">
<div className="w-full py-16 bg-palette-100/50 text-center">Upload files...</div>
</TabPanel>
<TabPanel tabId="directory">
<div className="w-full py-16 bg-palette-100/50 text-center">Upload a directory...</div>
</TabPanel>
</Tabs>
</Panel>,
<Panel
title={
<>
<ArrowRightIcon /> Usage
</>
}
>
<CurrentUsage />
</Panel>,
<Panel
title={
<>
<ArrowRightIcon /> Current plan
</>
}
>
<ul>
<li>Current</li>
<li>Plan</li>
<li>Info</li>
</ul>
</Panel>,
]}
/>
</div>
{showRecentActivity && (
<div className="mt-10">
<LatestActivity />
</div>
)}
</>
);
}; };
IndexPage.Layout = DashboardLayout; IndexPage.Layout = DashboardLayout;

View File

@ -0,0 +1,35 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 378 146" preserveAspectRatio="none">
<defs>
<pattern id="diagonalHatch" patternUnits="userSpaceOnUse" width="8" height="8">
<path d="M-2,2 l6,-6
M0,8 l8,-8
M7,10 l6,-6" stroke="#d4dddb" />
</pattern>
<style>.cls-1{fill:#f5f7f7;}.cls-2{fill:#d4dddb; opacity: 0.5;}.cls-3{fill:url(#diagonalHatch);}.cls-4{fill:#80e3af;}</style>
</defs>
<g id="Layer_2" data-name="Layer 2">
<g id="Layer_1-2" data-name="Layer 1">
<rect class="cls-1" width="2" height="146" rx="1"/>
<rect class="cls-1" x="4" width="4" height="146" rx="2"/>
<rect class="cls-1" x="10" width="6" height="146" rx="2"/>
<rect class="cls-1" x="18" width="8" height="146" rx="2"/>
<rect class="cls-1" x="28" width="10" height="146" rx="2"/>
<rect class="cls-1" x="40" width="12" height="146" rx="2"/>
<rect class="cls-1" x="54" width="14" height="146" rx="2"/>
<rect class="cls-1" x="70" width="16" height="146" rx="2"/>
<rect class="cls-1" x="88" width="18" height="146" rx="2"/>
<rect class="cls-1" x="108" width="20" height="146" rx="2"/>
<rect class="cls-1" x="130" width="22" height="146" rx="2"/>
<rect class="cls-1" x="154" width="24" height="146" rx="2"/>
<rect class="cls-1" x="180" width="26" height="146" rx="2"/>
<rect class="cls-1" x="208" width="28" height="146" rx="2"/>
<rect class="cls-2" x="238" width="30" height="146" rx="2"/>
<rect class="cls-2" x="270" width="32" height="146" rx="2"/>
<rect class="cls-2" x="304" width="34" height="146" rx="2"/>
<rect class="cls-3" x="340" width="38" height="146" rx="2"/>
<path class="cls-4" d="M304,144h34a0,0,0,0,1,0,0v0a2,2,0,0,1-2,2H306a2,2,0,0,1-2-2v0A0,0,0,0,1,304,144Z"/>
<path class="cls-4" d="M270,144h32a0,0,0,0,1,0,0v0a2,2,0,0,1-2,2H272a2,2,0,0,1-2-2v0A0,0,0,0,1,270,144Z"/>
<path class="cls-4" d="M238,144h30a0,0,0,0,1,0,0v0a2,2,0,0,1-2,2H240a2,2,0,0,1-2-2v0A0,0,0,0,1,238,144Z"/>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.0 KiB

View File

@ -338,7 +338,7 @@
chalk "^2.0.0" chalk "^2.0.0"
js-tokens "^4.0.0" js-tokens "^4.0.0"
"@babel/parser@^7.12.11", "@babel/parser@^7.12.7", "@babel/parser@^7.14.7", "@babel/parser@^7.15.5", "@babel/parser@^7.16.7", "@babel/parser@^7.17.3", "@babel/parser@^7.7.0": "@babel/parser@^7.1.0", "@babel/parser@^7.12.11", "@babel/parser@^7.12.7", "@babel/parser@^7.14.7", "@babel/parser@^7.15.5", "@babel/parser@^7.16.7", "@babel/parser@^7.17.3", "@babel/parser@^7.7.0":
version "7.17.3" version "7.17.3"
resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.17.3.tgz#b07702b982990bf6fdc1da5049a23fece4c5c3d0" resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.17.3.tgz#b07702b982990bf6fdc1da5049a23fece4c5c3d0"
integrity sha512-7yJPvPV+ESz2IUTPbOL+YkIGyCqOyNIzdguKQuJGnH7bg1WTIifuM21YqokFt/THWh1AkCRn9IgoykTRCBVpzA== integrity sha512-7yJPvPV+ESz2IUTPbOL+YkIGyCqOyNIzdguKQuJGnH7bg1WTIifuM21YqokFt/THWh1AkCRn9IgoykTRCBVpzA==
@ -1139,7 +1139,7 @@
debug "^4.1.0" debug "^4.1.0"
globals "^11.1.0" globals "^11.1.0"
"@babel/types@^7.0.0-beta.49", "@babel/types@^7.12.11", "@babel/types@^7.12.7", "@babel/types@^7.15.4", "@babel/types@^7.16.0", "@babel/types@^7.16.7", "@babel/types@^7.16.8", "@babel/types@^7.17.0", "@babel/types@^7.2.0", "@babel/types@^7.4.4", "@babel/types@^7.7.0": "@babel/types@^7.0.0", "@babel/types@^7.0.0-beta.49", "@babel/types@^7.12.11", "@babel/types@^7.12.7", "@babel/types@^7.15.4", "@babel/types@^7.16.0", "@babel/types@^7.16.7", "@babel/types@^7.16.8", "@babel/types@^7.17.0", "@babel/types@^7.2.0", "@babel/types@^7.3.0", "@babel/types@^7.4.4", "@babel/types@^7.7.0":
version "7.17.0" version "7.17.0"
resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.17.0.tgz#a826e368bccb6b3d84acd76acad5c0d87342390b" resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.17.0.tgz#a826e368bccb6b3d84acd76acad5c0d87342390b"
integrity sha512-TmKSNO4D5rzhL5bjWFcVHHLETzfQ/AmbKpKPOSjlP0WoHZ6L911fgoOKY4Alp/emzG4cHJdyN49zpgkbXFEHHw== integrity sha512-TmKSNO4D5rzhL5bjWFcVHHLETzfQ/AmbKpKPOSjlP0WoHZ6L911fgoOKY4Alp/emzG4cHJdyN49zpgkbXFEHHw==
@ -3156,6 +3156,39 @@
resolved "https://registry.yarnpkg.com/@types/aria-query/-/aria-query-4.2.2.tgz#ed4e0ad92306a704f9fb132a0cfcf77486dbe2bc" resolved "https://registry.yarnpkg.com/@types/aria-query/-/aria-query-4.2.2.tgz#ed4e0ad92306a704f9fb132a0cfcf77486dbe2bc"
integrity sha512-HnYpAE1Y6kRyKM/XkEuiRQhTHvkzMBurTHnpFLYLBGPIylZNPs9jJcuOOYWxPLJCSEtmZT0Y8rHDokKN7rRTig== integrity sha512-HnYpAE1Y6kRyKM/XkEuiRQhTHvkzMBurTHnpFLYLBGPIylZNPs9jJcuOOYWxPLJCSEtmZT0Y8rHDokKN7rRTig==
"@types/babel__core@^7.1.12":
version "7.1.18"
resolved "https://registry.yarnpkg.com/@types/babel__core/-/babel__core-7.1.18.tgz#1a29abcc411a9c05e2094c98f9a1b7da6cdf49f8"
integrity sha512-S7unDjm/C7z2A2R9NzfKCK1I+BAALDtxEmsJBwlB3EzNfb929ykjL++1CK9LO++EIp2fQrC8O+BwjKvz6UeDyQ==
dependencies:
"@babel/parser" "^7.1.0"
"@babel/types" "^7.0.0"
"@types/babel__generator" "*"
"@types/babel__template" "*"
"@types/babel__traverse" "*"
"@types/babel__generator@*":
version "7.6.4"
resolved "https://registry.yarnpkg.com/@types/babel__generator/-/babel__generator-7.6.4.tgz#1f20ce4c5b1990b37900b63f050182d28c2439b7"
integrity sha512-tFkciB9j2K755yrTALxD44McOrk+gfpIpvC3sxHjRawj6PfnQxrse4Clq5y/Rq+G3mrBurMax/lG8Qn2t9mSsg==
dependencies:
"@babel/types" "^7.0.0"
"@types/babel__template@*":
version "7.4.1"
resolved "https://registry.yarnpkg.com/@types/babel__template/-/babel__template-7.4.1.tgz#3d1a48fd9d6c0edfd56f2ff578daed48f36c8969"
integrity sha512-azBFKemX6kMg5Io+/rdGT0dkGreboUVR0Cdm3fz9QJWpaQGJRQXl7C+6hOTCZcMll7KFyEQpgbYI2lHdsS4U7g==
dependencies:
"@babel/parser" "^7.1.0"
"@babel/types" "^7.0.0"
"@types/babel__traverse@*":
version "7.14.2"
resolved "https://registry.yarnpkg.com/@types/babel__traverse/-/babel__traverse-7.14.2.tgz#ffcd470bbb3f8bf30481678fb5502278ca833a43"
integrity sha512-K2waXdXBi2302XUdcHcR1jCeU0LL4TD9HRs/gk0N2Xvrht+G/BfJa4QObBQZfhMdxiCpV3COl5Nfq4uKTeTnJA==
dependencies:
"@babel/types" "^7.3.0"
"@types/cacheable-request@^6.0.1": "@types/cacheable-request@^6.0.1":
version "6.0.2" version "6.0.2"
resolved "https://registry.yarnpkg.com/@types/cacheable-request/-/cacheable-request-6.0.2.tgz#c324da0197de0a98a2312156536ae262429ff6b9" resolved "https://registry.yarnpkg.com/@types/cacheable-request/-/cacheable-request-6.0.2.tgz#c324da0197de0a98a2312156536ae262429ff6b9"
@ -4679,6 +4712,16 @@ babel-plugin-polyfill-regenerator@^0.3.0:
dependencies: dependencies:
"@babel/helper-define-polyfill-provider" "^0.3.1" "@babel/helper-define-polyfill-provider" "^0.3.1"
babel-plugin-preval@^5.1.0:
version "5.1.0"
resolved "https://registry.yarnpkg.com/babel-plugin-preval/-/babel-plugin-preval-5.1.0.tgz#6efb89bf6b97af592cd1400c6df49c0e9e6ab027"
integrity sha512-G5R+xmo5LS41A4UyZjOjV0mp9AvkuCyUOAJ6TOv/jTZS+VKh7L7HUDRcCSOb0YCM/u0fFarh7Diz0wjY8rFNFg==
dependencies:
"@babel/runtime" "^7.12.5"
"@types/babel__core" "^7.1.12"
babel-plugin-macros "^3.0.1"
require-from-string "^2.0.2"
babel-plugin-react-docgen@^4.2.1: babel-plugin-react-docgen@^4.2.1:
version "4.2.1" version "4.2.1"
resolved "https://registry.yarnpkg.com/babel-plugin-react-docgen/-/babel-plugin-react-docgen-4.2.1.tgz#7cc8e2f94e8dc057a06e953162f0810e4e72257b" resolved "https://registry.yarnpkg.com/babel-plugin-react-docgen/-/babel-plugin-react-docgen-4.2.1.tgz#7cc8e2f94e8dc057a06e953162f0810e4e72257b"
@ -8159,6 +8202,11 @@ gatsby-plugin-postcss@^5.7.0:
"@babel/runtime" "^7.15.4" "@babel/runtime" "^7.15.4"
postcss-loader "^4.3.0" postcss-loader "^4.3.0"
gatsby-plugin-preval@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/gatsby-plugin-preval/-/gatsby-plugin-preval-1.0.0.tgz#b0e9dcc9ef568cb6ca998f7211b5365824b97201"
integrity sha512-HpPp2bdA4nZsuD3R++GRhM9BPlFp8+ilkXIo53hNr14OlHXxrgimh9lqVVvP3q4JriHh+bYcvmfqm6msIsxxLg==
gatsby-plugin-provide-react@^1.0.2: gatsby-plugin-provide-react@^1.0.2:
version "1.0.2" version "1.0.2"
resolved "https://registry.yarnpkg.com/gatsby-plugin-provide-react/-/gatsby-plugin-provide-react-1.0.2.tgz#e50bb311cd8ef5855c6d94f708266ab117c77a15" resolved "https://registry.yarnpkg.com/gatsby-plugin-provide-react/-/gatsby-plugin-provide-react-1.0.2.tgz#e50bb311cd8ef5855c6d94f708266ab117c77a15"
@ -8196,10 +8244,10 @@ gatsby-plugin-sharp@^4.6.0:
svgo "1.3.2" svgo "1.3.2"
uuid "3.4.0" uuid "3.4.0"
gatsby-plugin-styled-components@^5.7.0: gatsby-plugin-styled-components@^5.8.0:
version "5.7.0" version "5.8.0"
resolved "https://registry.yarnpkg.com/gatsby-plugin-styled-components/-/gatsby-plugin-styled-components-5.7.0.tgz#8ba7b4ddb1722dcd0efd4fc6f1a8e62f47be012c" resolved "https://registry.yarnpkg.com/gatsby-plugin-styled-components/-/gatsby-plugin-styled-components-5.8.0.tgz#5d8c81802ed9266435aa1145451bfb3ac582ad37"
integrity sha512-mX8N4nqIX0Ow/pUSORUb8WlKvgX7foCoWZ0AifyBOFnhBCbRWYTsXFWwiea6jCnST5V61b2TOFpjIHcvHvc9aQ== integrity sha512-4ma9PgOr3U5TUX6uwAqFW+VX+fDxmt1y4oM3ArfZufaiQvZJ52cuf/uiyI+Tx1DJebcypEpR5dXYVl9ZX1bUHg==
dependencies: dependencies:
"@babel/runtime" "^7.15.4" "@babel/runtime" "^7.15.4"
@ -12402,6 +12450,11 @@ pretty-bytes@^5.4.1:
resolved "https://registry.yarnpkg.com/pretty-bytes/-/pretty-bytes-5.6.0.tgz#356256f643804773c82f64723fe78c92c62beaeb" resolved "https://registry.yarnpkg.com/pretty-bytes/-/pretty-bytes-5.6.0.tgz#356256f643804773c82f64723fe78c92c62beaeb"
integrity sha512-FFw039TmrBqFK8ma/7OL3sDz/VytdtJr044/QUJtH0wK9lb9jLq9tJyIxUwtQJHwar2BqtiA4iCWSwo9JLkzFg== integrity sha512-FFw039TmrBqFK8ma/7OL3sDz/VytdtJr044/QUJtH0wK9lb9jLq9tJyIxUwtQJHwar2BqtiA4iCWSwo9JLkzFg==
pretty-bytes@^6.0.0:
version "6.0.0"
resolved "https://registry.yarnpkg.com/pretty-bytes/-/pretty-bytes-6.0.0.tgz#928be2ad1f51a2e336add8ba764739f9776a8140"
integrity sha512-6UqkYefdogmzqAZWzJ7laYeJnaXDy2/J+ZqiiMtS7t7OfpXWTlaeGMwX8U6EFvPV/YWWEKRkS8hKS4k60WHTOg==
pretty-error@^2.1.1, pretty-error@^2.1.2: pretty-error@^2.1.1, pretty-error@^2.1.2:
version "2.1.2" version "2.1.2"
resolved "https://registry.yarnpkg.com/pretty-error/-/pretty-error-2.1.2.tgz#be89f82d81b1c86ec8fdfbc385045882727f93b6" resolved "https://registry.yarnpkg.com/pretty-error/-/pretty-error-2.1.2.tgz#be89f82d81b1c86ec8fdfbc385045882727f93b6"
@ -14061,6 +14114,11 @@ store2@^2.12.0:
resolved "https://registry.yarnpkg.com/store2/-/store2-2.13.1.tgz#fae7b5bb9d35fc53dc61cd262df3abb2f6e59022" resolved "https://registry.yarnpkg.com/store2/-/store2-2.13.1.tgz#fae7b5bb9d35fc53dc61cd262df3abb2f6e59022"
integrity sha512-iJtHSGmNgAUx0b/MCS6ASGxb//hGrHHRgzvN+K5bvkBTN7A9RTpPSf1WSp+nPGvWCJ1jRnvY7MKnuqfoi3OEqg== integrity sha512-iJtHSGmNgAUx0b/MCS6ASGxb//hGrHHRgzvN+K5bvkBTN7A9RTpPSf1WSp+nPGvWCJ1jRnvY7MKnuqfoi3OEqg==
storybook-addon-gatsby@^0.0.5:
version "0.0.5"
resolved "https://registry.yarnpkg.com/storybook-addon-gatsby/-/storybook-addon-gatsby-0.0.5.tgz#94f5b67bab8659d0248b65e60dabc3702818ce8b"
integrity sha512-18f8Kc6mx8mEFfqY2DgF9ayDfmM58+9IjJqIxGV4bA4r2EtB/Q1LDNELIJmpLLyA5NrSvECxCqzLu7jNBlWgmA==
stream-browserify@^2.0.1: stream-browserify@^2.0.1:
version "2.0.2" version "2.0.2"
resolved "https://registry.yarnpkg.com/stream-browserify/-/stream-browserify-2.0.2.tgz#87521d38a44aa7ee91ce1cd2a47df0cb49dd660b" resolved "https://registry.yarnpkg.com/stream-browserify/-/stream-browserify-2.0.2.tgz#87521d38a44aa7ee91ce1cd2a47df0cb49dd660b"