Merge pull request #1911 from SkynetLabs/dashboard-v2-upgrade-page
Dashboard v2 upgrade page
This commit is contained in:
commit
b2a7a2a8a4
|
@ -1,3 +1,4 @@
|
||||||
|
import cn from "classnames";
|
||||||
import PropTypes from "prop-types";
|
import PropTypes from "prop-types";
|
||||||
import styled from "styled-components";
|
import styled from "styled-components";
|
||||||
|
|
||||||
|
@ -6,9 +7,12 @@ import styled from "styled-components";
|
||||||
*/
|
*/
|
||||||
export const Button = styled.button.attrs(({ disabled, $primary }) => ({
|
export const Button = styled.button.attrs(({ disabled, $primary }) => ({
|
||||||
type: "button",
|
type: "button",
|
||||||
className: `px-6 py-2.5 rounded-full font-sans uppercase text-xs tracking-wide text-palette-600 transition-[filter]
|
className: cn("px-6 py-2.5 rounded-full font-sans uppercase text-xs tracking-wide transition-[opacity_filter]", {
|
||||||
${$primary ? "bg-primary" : "bg-white border-2 border-black"}
|
"bg-primary text-palette-600": $primary,
|
||||||
${disabled ? "saturate-50 brightness-125 cursor-default text-palette-400" : "hover:brightness-90"}`,
|
"bg-white border-2 border-black text-palette-600": !$primary,
|
||||||
|
"cursor-not-allowed opacity-60": disabled,
|
||||||
|
"hover:brightness-90": !disabled,
|
||||||
|
}),
|
||||||
}))``;
|
}))``;
|
||||||
Button.propTypes = {
|
Button.propTypes = {
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -1,6 +1,8 @@
|
||||||
|
import PropTypes from "prop-types";
|
||||||
|
|
||||||
import { withIconProps } from "../withIconProps";
|
import { withIconProps } from "../withIconProps";
|
||||||
|
|
||||||
export const CircledCheckmarkIcon = withIconProps(({ size, ...props }) => (
|
export const CheckmarkIcon = withIconProps(({ size, circled, ...props }) => (
|
||||||
<svg
|
<svg
|
||||||
width={size}
|
width={size}
|
||||||
height={size}
|
height={size}
|
||||||
|
@ -9,10 +11,15 @@ export const CircledCheckmarkIcon = withIconProps(({ size, ...props }) => (
|
||||||
shapeRendering="geometricPrecision"
|
shapeRendering="geometricPrecision"
|
||||||
{...props}
|
{...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
|
<polygon
|
||||||
fill="currentColor"
|
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"
|
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>
|
</svg>
|
||||||
));
|
));
|
||||||
|
|
||||||
|
CheckmarkIcon.propTypes = {
|
||||||
|
...CheckmarkIcon.propTypes,
|
||||||
|
circled: PropTypes.bool,
|
||||||
|
};
|
|
@ -4,7 +4,7 @@ export * from "./icons/LockClosedIcon";
|
||||||
export * from "./icons/SkynetLogoIcon";
|
export * from "./icons/SkynetLogoIcon";
|
||||||
export * from "./icons/ArrowRightIcon";
|
export * from "./icons/ArrowRightIcon";
|
||||||
export * from "./icons/InfoIcon";
|
export * from "./icons/InfoIcon";
|
||||||
export * from "./icons/CircledCheckmarkIcon";
|
export * from "./icons/CheckmarkIcon";
|
||||||
export * from "./icons/CircledErrorIcon";
|
export * from "./icons/CircledErrorIcon";
|
||||||
export * from "./icons/CircledProgressIcon";
|
export * from "./icons/CircledProgressIcon";
|
||||||
export * from "./icons/CircledArrowUpIcon";
|
export * from "./icons/CircledArrowUpIcon";
|
||||||
|
|
|
@ -6,7 +6,7 @@ export default function Bullets({ visibleSlides, activeIndex, allSlides, changeS
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex gap-3 pt-6">
|
<div className="flex gap-3 pt-6 justify-center sm:justify-start">
|
||||||
{Array(allSlides - visibleSlides + 1)
|
{Array(allSlides - visibleSlides + 1)
|
||||||
.fill(null)
|
.fill(null)
|
||||||
.map((_, index) => (
|
.map((_, index) => (
|
||||||
|
|
|
@ -2,7 +2,7 @@ import styled from "styled-components";
|
||||||
import PropTypes from "prop-types";
|
import PropTypes from "prop-types";
|
||||||
|
|
||||||
const Slide = styled.div.attrs(({ isVisible }) => ({
|
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 = {
|
Slide.propTypes = {
|
||||||
|
|
|
@ -22,12 +22,12 @@ const scrollableStyles = css`
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const Scroller = styled.div.attrs({
|
const Scroller = styled.div.attrs({
|
||||||
className: "slider-scroller grid gap-4 transition-transform",
|
className: "slider-scroller grid transition-transform",
|
||||||
})`
|
})`
|
||||||
${({ $scrollable }) => ($scrollable ? scrollableStyles : "")}
|
${({ $scrollable }) => ($scrollable ? scrollableStyles : "")}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const Slider = ({ slides, breakpoints }) => {
|
const Slider = ({ slides, breakpoints, scrollerClassName, className }) => {
|
||||||
const { visibleSlides, scrollable } = useActiveBreakpoint(breakpoints);
|
const { visibleSlides, scrollable } = useActiveBreakpoint(breakpoints);
|
||||||
const [activeIndex, setActiveIndex] = React.useState(0);
|
const [activeIndex, setActiveIndex] = React.useState(0);
|
||||||
const changeSlide = React.useCallback(
|
const changeSlide = React.useCallback(
|
||||||
|
@ -49,12 +49,13 @@ const Slider = ({ slides, breakpoints }) => {
|
||||||
}, [slides.length, visibleSlides, activeIndex]);
|
}, [slides.length, visibleSlides, activeIndex]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Container>
|
<Container className={className}>
|
||||||
<Scroller
|
<Scroller
|
||||||
$visibleSlides={visibleSlides}
|
$visibleSlides={visibleSlides}
|
||||||
$allSlides={slides.length}
|
$allSlides={slides.length}
|
||||||
$activeIndex={activeIndex}
|
$activeIndex={activeIndex}
|
||||||
$scrollable={scrollable}
|
$scrollable={scrollable}
|
||||||
|
className={scrollerClassName}
|
||||||
>
|
>
|
||||||
{slides.map((slide, index) => {
|
{slides.map((slide, index) => {
|
||||||
const isVisible = index >= activeIndex && index < activeIndex + visibleSlides;
|
const isVisible = index >= activeIndex && index < activeIndex + visibleSlides;
|
||||||
|
@ -63,7 +64,11 @@ const Slider = ({ slides, breakpoints }) => {
|
||||||
<div key={`slide-${index}`}>
|
<div key={`slide-${index}`}>
|
||||||
<Slide
|
<Slide
|
||||||
isVisible={isVisible || !scrollable}
|
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}
|
||||||
</Slide>
|
</Slide>
|
||||||
|
@ -101,6 +106,14 @@ Slider.propTypes = {
|
||||||
* If set to false, all slides will be visible & rendered in a column.
|
* If set to false, all slides will be visible & rendered in a column.
|
||||||
*/
|
*/
|
||||||
scrollable: PropTypes.bool.isRequired,
|
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,
|
visibleSlides: 1,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
scrollerClassName: "gap-4",
|
||||||
|
className: "",
|
||||||
};
|
};
|
||||||
|
|
||||||
export default Slider;
|
export default Slider;
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import cn from "classnames";
|
import cn from "classnames";
|
||||||
|
|
||||||
import { CircledCheckmarkIcon, CircledErrorIcon, CircledProgressIcon, CircledArrowUpIcon } from "../Icons";
|
import { CheckmarkIcon, CircledErrorIcon, CircledProgressIcon, CircledArrowUpIcon } from "../Icons";
|
||||||
|
|
||||||
export default function UploaderItemIcon({ status }) {
|
export default function UploaderItemIcon({ status }) {
|
||||||
switch (status) {
|
switch (status) {
|
||||||
|
@ -11,7 +11,7 @@ export default function UploaderItemIcon({ status }) {
|
||||||
case "processing":
|
case "processing":
|
||||||
return <CircledProgressIcon className="animate-[spin_3s_linear_infinite]" />;
|
return <CircledProgressIcon className="animate-[spin_3s_linear_infinite]" />;
|
||||||
case "complete":
|
case "complete":
|
||||||
return <CircledCheckmarkIcon />;
|
return <CheckmarkIcon circled />;
|
||||||
case "error":
|
case "error":
|
||||||
return <CircledErrorIcon className="text-error" />;
|
return <CircledErrorIcon className="text-error" />;
|
||||||
default:
|
default:
|
||||||
|
|
|
@ -4,7 +4,7 @@ import freeTier from "../lib/tiers";
|
||||||
import { usePlans } from "../contexts/plans";
|
import { usePlans } from "../contexts/plans";
|
||||||
|
|
||||||
export default function useActivePlan(user) {
|
export default function useActivePlan(user) {
|
||||||
const { plans, error } = usePlans();
|
const { plans, loading, error } = usePlans();
|
||||||
|
|
||||||
const [activePlan, setActivePlan] = useState(freeTier);
|
const [activePlan, setActivePlan] = useState(freeTier);
|
||||||
|
|
||||||
|
@ -17,6 +17,7 @@ export default function useActivePlan(user) {
|
||||||
return {
|
return {
|
||||||
error,
|
error,
|
||||||
plans,
|
plans,
|
||||||
|
loading,
|
||||||
activePlan,
|
activePlan,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
@ -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;
|
Reference in New Issue