Merge pull request #1911 from SkynetLabs/dashboard-v2-upgrade-page

Dashboard v2 upgrade page
This commit is contained in:
Karol Wypchło 2022-03-24 11:59:40 +01:00 committed by GitHub
commit b2a7a2a8a4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 176 additions and 15 deletions

View File

@ -1,3 +1,4 @@
import cn from "classnames";
import PropTypes from "prop-types";
import styled from "styled-components";
@ -6,9 +7,12 @@ import styled from "styled-components";
*/
export const Button = styled.button.attrs(({ disabled, $primary }) => ({
type: "button",
className: `px-6 py-2.5 rounded-full font-sans uppercase text-xs tracking-wide text-palette-600 transition-[filter]
${$primary ? "bg-primary" : "bg-white border-2 border-black"}
${disabled ? "saturate-50 brightness-125 cursor-default text-palette-400" : "hover:brightness-90"}`,
className: cn("px-6 py-2.5 rounded-full font-sans uppercase text-xs tracking-wide transition-[opacity_filter]", {
"bg-primary text-palette-600": $primary,
"bg-white border-2 border-black text-palette-600": !$primary,
"cursor-not-allowed opacity-60": disabled,
"hover:brightness-90": !disabled,
}),
}))``;
Button.propTypes = {
/**

View File

@ -1,6 +1,8 @@
import PropTypes from "prop-types";
import { withIconProps } from "../withIconProps";
export const CircledCheckmarkIcon = withIconProps(({ size, ...props }) => (
export const CheckmarkIcon = withIconProps(({ size, circled, ...props }) => (
<svg
width={size}
height={size}
@ -9,10 +11,15 @@ export const CircledCheckmarkIcon = withIconProps(({ size, ...props }) => (
shapeRendering="geometricPrecision"
{...props}
>
<circle cx="16" cy="16" r="15" fill="transparent" stroke="currentColor" strokeWidth="2" />
{circled && <circle cx="16" cy="16" r="15" fill="transparent" stroke="currentColor" strokeWidth="2" />}
<polygon
fill="currentColor"
points="22.45 11.19 23.86 12.61 14.44 22.03 9.69 17.28 11.1 15.86 14.44 19.2 22.45 11.19"
/>
</svg>
));
CheckmarkIcon.propTypes = {
...CheckmarkIcon.propTypes,
circled: PropTypes.bool,
};

View File

@ -4,7 +4,7 @@ export * from "./icons/LockClosedIcon";
export * from "./icons/SkynetLogoIcon";
export * from "./icons/ArrowRightIcon";
export * from "./icons/InfoIcon";
export * from "./icons/CircledCheckmarkIcon";
export * from "./icons/CheckmarkIcon";
export * from "./icons/CircledErrorIcon";
export * from "./icons/CircledProgressIcon";
export * from "./icons/CircledArrowUpIcon";

View File

@ -6,7 +6,7 @@ export default function Bullets({ visibleSlides, activeIndex, allSlides, changeS
}
return (
<div className="flex gap-3 pt-6">
<div className="flex gap-3 pt-6 justify-center sm:justify-start">
{Array(allSlides - visibleSlides + 1)
.fill(null)
.map((_, index) => (

View File

@ -2,7 +2,7 @@ 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"}`,
className: `slider-slide transition-opacity ${isVisible ? "" : "opacity-50 cursor-pointer select-none"}`,
}))``;
Slide.propTypes = {

View File

@ -22,12 +22,12 @@ const scrollableStyles = css`
`;
const Scroller = styled.div.attrs({
className: "slider-scroller grid gap-4 transition-transform",
className: "slider-scroller grid transition-transform",
})`
${({ $scrollable }) => ($scrollable ? scrollableStyles : "")}
`;
const Slider = ({ slides, breakpoints }) => {
const Slider = ({ slides, breakpoints, scrollerClassName, className }) => {
const { visibleSlides, scrollable } = useActiveBreakpoint(breakpoints);
const [activeIndex, setActiveIndex] = React.useState(0);
const changeSlide = React.useCallback(
@ -49,12 +49,13 @@ const Slider = ({ slides, breakpoints }) => {
}, [slides.length, visibleSlides, activeIndex]);
return (
<Container>
<Container className={className}>
<Scroller
$visibleSlides={visibleSlides}
$allSlides={slides.length}
$activeIndex={activeIndex}
$scrollable={scrollable}
className={scrollerClassName}
>
{slides.map((slide, index) => {
const isVisible = index >= activeIndex && index < activeIndex + visibleSlides;
@ -63,7 +64,11 @@ const Slider = ({ slides, breakpoints }) => {
<div key={`slide-${index}`}>
<Slide
isVisible={isVisible || !scrollable}
onClickCapture={scrollable && !isVisible ? (event) => changeSlide(event, index) : null}
onClickCapture={
scrollable && !isVisible
? (event) => changeSlide(event, index > activeIndex ? activeIndex + 1 : activeIndex - 1)
: null
}
>
{slide}
</Slide>
@ -101,6 +106,14 @@ Slider.propTypes = {
* If set to false, all slides will be visible & rendered in a column.
*/
scrollable: PropTypes.bool.isRequired,
/**
* Additional class names to apply to the <Scroller /> element.
*/
scrollerClassName: PropTypes.string,
/**
* Additional class names to apply to the <Container /> element.
*/
className: PropTypes.string,
})
),
};
@ -123,6 +136,8 @@ Slider.defaultProps = {
visibleSlides: 1,
},
],
scrollerClassName: "gap-4",
className: "",
};
export default Slider;

View File

@ -1,6 +1,6 @@
import cn from "classnames";
import { CircledCheckmarkIcon, CircledErrorIcon, CircledProgressIcon, CircledArrowUpIcon } from "../Icons";
import { CheckmarkIcon, CircledErrorIcon, CircledProgressIcon, CircledArrowUpIcon } from "../Icons";
export default function UploaderItemIcon({ status }) {
switch (status) {
@ -11,7 +11,7 @@ export default function UploaderItemIcon({ status }) {
case "processing":
return <CircledProgressIcon className="animate-[spin_3s_linear_infinite]" />;
case "complete":
return <CircledCheckmarkIcon />;
return <CheckmarkIcon circled />;
case "error":
return <CircledErrorIcon className="text-error" />;
default:

View File

@ -4,7 +4,7 @@ import freeTier from "../lib/tiers";
import { usePlans } from "../contexts/plans";
export default function useActivePlan(user) {
const { plans, error } = usePlans();
const { plans, loading, error } = usePlans();
const [activePlan, setActivePlan] = useState(freeTier);
@ -17,6 +17,7 @@ export default function useActivePlan(user) {
return {
error,
plans,
loading,
activePlan,
};
}

View File

@ -0,0 +1,134 @@
import * as React from "react";
import bytes from "pretty-bytes";
import styled from "styled-components";
import { useUser } from "../contexts/user";
import { PlansProvider } from "../contexts/plans/PlansProvider";
import useActivePlan from "../hooks/useActivePlan";
import DashboardLayout from "../layouts/DashboardLayout";
import { Panel } from "../components/Panel";
import Slider from "../components/Slider/Slider";
import { CheckmarkIcon } from "../components/Icons";
import { Button } from "../components/Button";
const SLIDER_BREAKPOINTS = [
{
name: "xl",
scrollable: true,
visibleSlides: 4,
},
{
name: "lg",
scrollable: true,
visibleSlides: 3,
},
{
name: "sm",
scrollable: true,
visibleSlides: 2,
},
{
scrollable: true,
visibleSlides: 1,
},
];
const PlanSummaryItem = ({ children }) => (
<li className="flex items-start gap-1 my-2">
<CheckmarkIcon size={32} className="text-primary shrink-0" />
<div className="mt-1">{children}</div>
</li>
);
const Description = styled.div`
display: -webkit-box;
-webkit-line-clamp: 4;
-webkit-box-orient: vertical;
overflow: hidden;
flex-shrink: 0;
height: 6rem;
`;
const Price = ({ price }) => (
<div className="my-8 text-center">
<h2>
<sup className="text-lg -top-4">$</sup>
{price}
</h2>
<p className="uppercase text-sm font-light -mt-2">per month</p>
</div>
);
const bandwidth = (value) => `${bytes(value, { bits: true })}/s`;
const storage = (value) => bytes(value, { binary: true });
const localizedNumber = (value) => value.toLocaleString();
const PlansSlider = () => {
const { user, error: userError } = useUser();
const { plans, loading, activePlan, error: plansError } = useActivePlan(user);
if (userError || plansError) {
return (
<div className="flex flex-col items-center justify-center">
<h3>Oooops!</h3>
<p>Something went wrong, please try again later.</p>
</div>
);
}
return (
<div className="w-full mb-24">
{!loading && (
<Slider
slides={plans.map((plan) => {
const isHigherThanCurrent = plan.tier > activePlan?.tier;
const isCurrent = plan.tier === activePlan?.tier;
return (
<Panel className="min-h-[620px] px-6 py-10 flex flex-col">
<h3>{plan.name}</h3>
<Description>{plan.description}</Description>
<Price price={plan.price} />
<div className="text-center my-6">
<Button $primary={isHigherThanCurrent} disabled={isCurrent}>
{isHigherThanCurrent && "Upgrade"}
{isCurrent && "Current"}
{!isHigherThanCurrent && !isCurrent && "Choose"}
</Button>
</div>
{plan.limits && (
<ul className="-ml-2">
<PlanSummaryItem>
Pin up to {storage(plan.limits.storageLimit)} of censorship-resistant storage
</PlanSummaryItem>
<PlanSummaryItem>
Support for up to {localizedNumber(plan.limits.maxNumberUploads)} files
</PlanSummaryItem>
<PlanSummaryItem>{bandwidth(plan.limits.uploadBandwidth)} upload bandwidth</PlanSummaryItem>
<PlanSummaryItem>{bandwidth(plan.limits.downloadBandwidth)} download bandwidth</PlanSummaryItem>
</ul>
)}
</Panel>
);
})}
breakpoints={SLIDER_BREAKPOINTS}
scrollerClassName="gap-4 xl:gap-8"
className="px-8 sm:px-4 md:px-0 lg:px-0"
/>
)}
</div>
);
};
const UpgradePage = () => (
<PlansProvider>
<PlansSlider />
</PlansProvider>
);
UpgradePage.Layout = DashboardLayout;
export default UpgradePage;