Dashboard v2 lighthouse scores improvements (#1972)

* Metadata improvements

* Accessibility improvements

* Improve performance on mobile
This commit is contained in:
Michał Leszczyk 2022-04-06 11:10:28 +02:00 committed by GitHub
parent d077a27fa9
commit 7bd54359e6
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
27 changed files with 258 additions and 150 deletions

View File

@ -2,8 +2,8 @@ const { createProxyMiddleware } = require("http-proxy-middleware");
module.exports = {
siteMetadata: {
title: `Accounts Dashboard`,
siteUrl: `https://www.yourdomain.tld`,
title: "Skynet Account",
siteUrl: `https://account.${process.env.GATSBY_PORTAL_DOMAIN}/`,
},
trailingSlash: "never",
plugins: [

View File

@ -83,13 +83,19 @@ export const APIKey = ({ apiKey, onRemoved, onEdited, onRemovalError }) => {
{isPublic && (
<button
title="Add or remove skylinks"
aria-label="Add or remove skylinks"
className="p-1 transition-colors hover:text-primary"
onClick={promptEdit}
>
<CogIcon size={22} />
</button>
)}
<button title="Delete this API key" className="p-1 transition-colors hover:text-error" onClick={promptRemoval}>
<button
title="Delete this API key"
aria-label="Delete this API key"
className="p-1 transition-colors hover:text-error"
onClick={promptRemoval}
>
<TrashIcon size={16} />
</button>
</span>
@ -121,7 +127,11 @@ export const APIKey = ({ apiKey, onRemoved, onEdited, onRemovalError }) => {
<code className="whitespace-nowrap select-all truncate bg-palette-100 odd:bg-white p-1">
{skylink}
</code>
<button className="p-1 transition-colors hover:text-error" onClick={() => removeSkylink(skylink)}>
<button
className="p-1 transition-colors hover:text-error"
onClick={() => removeSkylink(skylink)}
aria-label="Remove skylink"
>
<TrashIcon size={16} />
</button>
</li>

View File

@ -22,7 +22,7 @@ const TooltipContent = styled.div.attrs({
className: "bg-primary-light/10 text-palette-600 py-2 px-4 ",
})``;
export const CopyButton = ({ value, className }) => {
export const CopyButton = ({ value, className, ariaLabel = "Copy" }) => {
const containerRef = useRef();
const [copied, setCopied] = useState(false);
const [timer, setTimer] = useState(null);
@ -39,7 +39,7 @@ export const CopyButton = ({ value, className }) => {
return (
<div ref={containerRef} className={`inline-flex relative overflow-visible pr-2 ${className ?? ""}`}>
<Button onClick={handleCopy} className={copied ? "text-primary" : ""}>
<Button onClick={handleCopy} className={copied ? "text-primary" : ""} aria-label={ariaLabel}>
<CopyIcon size={16} />
</Button>
<TooltipContainer $visible={copied}>

View File

@ -101,7 +101,6 @@ export default function CurrentUsage() {
>
UPGRADE
</Link>{" "}
{/* TODO: proper URL */}
<span>{usage.filesLimit}</span>
</span>
</div>

View File

@ -84,19 +84,19 @@ export default function FileTable({ items }) {
<TableCell className="w-[180px]">{date}</TableCell>
<TableCell className="hidden lg:table-cell pr-6 !overflow-visible">
<div className="flex items-center">
<CopyButton value={skylink} className="mr-2" />
<CopyButton value={skylink} className="mr-2" aria-label="Copy skylink" />
<span className="w-full inline-block truncate">{skylink}</span>
</div>
</TableCell>
<TableCell className="w-[100px] !overflow-visible">
<div className="flex text-palette-600 gap-4">
<PopoverMenu options={buildShareMenu(item)} openClassName="text-primary">
<button>
<button aria-label="Share this skylink">
<ShareIcon size={22} />
</button>
</PopoverMenu>
<PopoverMenu options={buildOptionsMenu(item)} openClassName="text-primary">
<button>
<button aria-label="Manage this skylink">
<CogIcon />
</button>
</PopoverMenu>

View File

@ -0,0 +1,27 @@
import { Helmet } from "react-helmet";
import { graphql, useStaticQuery } from "gatsby";
export const Metadata = ({ children }) => {
const { site } = useStaticQuery(
graphql`
query Q {
site {
siteMetadata {
title
}
}
}
`
);
const { title } = site.siteMetadata;
return (
<Helmet htmlAttributes={{ lang: "en" }} titleTemplate={`%s | ${title}`} defaultTitle={title}>
<link rel="icon" href="/favicon.ico" type="image/x-icon" />
<meta name="description" content="Manage your Skynet uploads, account subscription, settings and API keys" />
<link rel="preconnect" href={`https://${process.env.GATSBY_PORTAL_DOMAIN}/`} />
{children}
</Helmet>
);
};

View File

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

View File

@ -11,7 +11,7 @@ export const Modal = ({ children, className, onClose }) => (
<ModalPortal>
<Overlay onClick={onClose}>
<div className={cn("relative w-modal max-w-modal shadow-sm rounded")}>
<button onClick={onClose} className="absolute top-[20px] right-[20px]">
<button onClick={onClose} className="absolute top-[20px] right-[20px]" aria-label="Close">
<PlusIcon size={14} className="rotate-45" />
</button>
<Panel className={cn("px-8 py-6 sm:px-12 sm:py-10", className)}>{children}</Panel>

View File

@ -94,6 +94,7 @@ export const NavBar = () => {
partiallyActive
/>
<DropdownMenuLink
as="button"
onClick={onLogout}
activeClassName="text-primary"
className="cursor-pointer"

View File

@ -12,6 +12,7 @@ export default function Bullets({ visibleSlides, activeIndex, allSlides, changeS
.map((_, index) => (
<button
key={index}
aria-label={`Slide ${index + 1}`}
type="button"
className={`rounded-full w-3 h-3 ${activeIndex === index ? "bg-primary" : "border-2 cursor-pointer"}`}
onClick={(event) => changeSlide(event, index)}

View File

@ -105,7 +105,11 @@ const Uploader = ({ mode }) => {
</div>
) : (
<div className="p-5">
<Button $primary className="w-[40px] h-[40px] !p-0 inline-flex justify-center items-center">
<Button
$primary
className="w-[40px] h-[40px] !p-0 inline-flex justify-center items-center"
aria-label="Upload new file"
>
<PlusIcon size={12} />
</Button>
<span className="ml-4">Add, or drop your files here</span>

View File

@ -44,7 +44,7 @@ export const AddAPIKeyForm = forwardRef(({ onSuccess, type }, ref) => {
<code className="p-2 rounded border border-palette-200 text-xs selection:bg-primary/30 truncate">
{generatedKey}
</code>
<CopyButton value={generatedKey} className="whitespace-nowrap" />
<CopyButton value={generatedKey} className="whitespace-nowrap" aria-label="Copy the new API key" />
</div>
</Alert>
)}
@ -94,7 +94,7 @@ export const AddAPIKeyForm = forwardRef(({ onSuccess, type }, ref) => {
{isSubmitting ? (
<CircledProgressIcon size={38} className="text-palette-300 animate-[spin_3s_linear_infinite]" />
) : (
<Button type="submit" className="px-2.5">
<Button type="submit" className="px-2.5" aria-label="Create general API key">
<PlusIcon size={14} />
</Button>
)}

View File

@ -59,7 +59,7 @@ export const AddPublicAPIKeyForm = forwardRef(({ onSuccess }, ref) => {
<code className="p-2 rounded border border-palette-200 text-xs selection:bg-primary/30 truncate">
{generatedKey}
</code>
<CopyButton value={generatedKey} className="whitespace-nowrap" />
<CopyButton value={generatedKey} className="whitespace-nowrap" aria-label="Copy the new public API key" />
</div>
</Alert>
)}
@ -137,7 +137,7 @@ export const AddPublicAPIKeyForm = forwardRef(({ onSuccess }, ref) => {
touched={skylinksTouched[index]}
/>
<span className="w-[24px] shrink-0 mt-3">
<button type="button" onClick={() => remove(index)}>
<button type="button" onClick={() => remove(index)} aria-label="Remove this skylink">
<TrashIcon size={16} />
</button>
</span>
@ -160,6 +160,7 @@ export const AddPublicAPIKeyForm = forwardRef(({ onSuccess }, ref) => {
/>
<button
type="button"
aria-label="Add this skylink"
onClick={() => appendSkylink(values.nextSkylink)}
className={cn("shrink-0 mt-1.5 w-[24px] h-[24px]", {
"text-palette-300 cursor-not-allowed": isNextSkylinkInvalid,

View File

@ -48,7 +48,7 @@ export const AddSkylinkToAPIKeyForm = ({ addSkylink }) => (
{isSubmitting ? (
<CircledProgressIcon size={38} className="text-palette-300 animate-[spin_3s_linear_infinite]" />
) : (
<Button type="submit" className="px-2.5">
<Button type="submit" className="px-2.5" aria-label="Add this skylink">
<PlusIcon size={14} />
</Button>
)}

View File

@ -4,6 +4,7 @@ import { navigate } from "gatsby";
import AuthLayout from "../../layouts/AuthLayout";
import { LoginForm } from "../../components/forms";
import { useUser } from "../../contexts/user";
import { Metadata } from "../../components/Metadata";
const LoginPage = ({ location }) => {
const { user, mutate: refreshUserState } = useUser();
@ -17,16 +18,21 @@ const LoginPage = ({ location }) => {
}, [user, redirectTo]);
return (
<div className="bg-white px-8 py-10 md:py-32 lg:px-16 xl:px-28 min-h-screen">
<div className="mb-4 md:mb-16">
<img src="/images/logo-black-text.svg" alt="Skynet" />
<>
<Metadata>
<title>Sign In</title>
</Metadata>
<div className="bg-white px-8 py-10 md:py-32 lg:px-16 xl:px-28 min-h-screen">
<div className="mb-4 md:mb-16">
<img src="/images/logo-black-text.svg" alt="Skynet" />
</div>
<LoginForm
onSuccess={async () => {
await refreshUserState();
}}
/>
</div>
<LoginForm
onSuccess={async () => {
await refreshUserState();
}}
/>
</div>
</>
);
};

View File

@ -4,6 +4,7 @@ import AuthLayout from "../../layouts/AuthLayout";
import { RecoveryForm } from "../../components/forms/RecoveryForm";
import HighlightedLink from "../../components/HighlightedLink";
import { Metadata } from "../../components/Metadata";
const State = {
Pure: "PURE",
@ -15,31 +16,36 @@ const ResetPasswordPage = () => {
const [state, setState] = useState(State.Pure);
return (
<div className="bg-white px-8 py-10 md:py-32 lg:px-16 xl:px-28 min-h-screen">
<div className="mb-4 md:mb-16">
<img src="/images/logo-black-text.svg" alt="Skynet" />
<>
<Metadata>
<title>Reset Password</title>
</Metadata>
<div className="bg-white px-8 py-10 md:py-32 lg:px-16 xl:px-28 min-h-screen">
<div className="mb-4 md:mb-16">
<img src="/images/logo-black-text.svg" alt="Skynet" />
</div>
{state !== State.Success && (
<RecoveryForm onSuccess={() => setState(State.Success)} onFailure={() => setState(State.Failure)} />
)}
{state === State.Success && (
<p className="text-primary text-center font-semibold">Please check your inbox for further instructions.</p>
)}
{state === State.Failure && (
<p className="text-error text-center">Something went wrong, please try again later.</p>
)}
<div className="text-sm text-center mt-8">
<p>
Suddenly remembered your password? <HighlightedLink to="/auth/login">Sign in</HighlightedLink>
</p>
<p>
Don't actually have an account? <HighlightedLink to="/auth/signup">Create one!</HighlightedLink>
</p>
</div>
</div>
{state !== State.Success && (
<RecoveryForm onSuccess={() => setState(State.Success)} onFailure={() => setState(State.Failure)} />
)}
{state === State.Success && (
<p className="text-primary text-center font-semibold">Please check your inbox for further instructions.</p>
)}
{state === State.Failure && (
<p className="text-error text-center">Something went wrong, please try again later.</p>
)}
<div className="text-sm text-center mt-8">
<p>
Suddenly remembered your password? <HighlightedLink to="/auth/login">Sign in</HighlightedLink>
</p>
<p>
Don't actually have an account? <HighlightedLink to="/auth/signup">Create one!</HighlightedLink>
</p>
</div>
</div>
</>
);
};

View File

@ -9,6 +9,7 @@ import HighlightedLink from "../../components/HighlightedLink";
import { SignUpForm } from "../../components/forms/SignUpForm";
import { usePortalSettings } from "../../contexts/portal-settings";
import { PlansProvider, usePlans } from "../../contexts/plans";
import { Metadata } from "../../components/Metadata";
const FreePortalHeader = () => {
const { plans } = usePlans();
@ -57,6 +58,9 @@ const SignUpPage = () => {
return (
<PlansProvider>
<Metadata>
<title>Sign Up</title>
</Metadata>
<div className="bg-white px-8 py-10 md:py-32 lg:px-16 xl:px-28 min-h-screen">
<div className="mb-4 md:mb-16">
<img src="/images/logo-black-text.svg" alt="Skynet" />

View File

@ -1,28 +1,34 @@
import * as React from "react";
import { useSearchParam } from "react-use";
import DashboardLayout from "../layouts/DashboardLayout";
import { Panel } from "../components/Panel";
import { Tab, TabPanel, Tabs } from "../components/Tabs";
import { Metadata } from "../components/Metadata";
import FileList from "../components/FileList/FileList";
import { useSearchParam } from "react-use";
const FilesPage = () => {
const defaultTab = useSearchParam("tab");
return (
<Panel title="Files">
<Tabs defaultTab={defaultTab || "uploads"}>
<Tab id="uploads" title="Uploads" />
<Tab id="downloads" title="Downloads" />
<TabPanel tabId="uploads" className="pt-4">
<FileList type="uploads" />
</TabPanel>
<TabPanel tabId="downloads" className="pt-4">
<FileList type="downloads" />
</TabPanel>
</Tabs>
</Panel>
<>
<Metadata>
<title>My Files</title>
</Metadata>
<Panel title="Files">
<Tabs defaultTab={defaultTab || "uploads"}>
<Tab id="uploads" title="Uploads" />
<Tab id="downloads" title="Downloads" />
<TabPanel tabId="uploads" className="pt-4">
<FileList type="uploads" />
</TabPanel>
<TabPanel tabId="downloads" className="pt-4">
<FileList type="downloads" />
</TabPanel>
</Tabs>
</Panel>
</>
);
};

View File

@ -13,6 +13,7 @@ import CurrentUsage from "../components/CurrentUsage";
import Uploader from "../components/Uploader/Uploader";
import CurrentPlan from "../components/CurrentPlan";
import { FullScreenLoadingIndicator } from "../components/LoadingIndicator";
import { Metadata } from "../components/Metadata";
import useUpgradeRedirect from "../hooks/useUpgradeRedirect";
const IndexPage = () => {
@ -24,51 +25,60 @@ const IndexPage = () => {
}
return (
<PlansProvider>
<div className="w-full">
<Slider
slides={[
<Panel title="Upload" className="h-[330px]">
<Tabs variant="fill">
<Tab id="files" title="Files" />
<Tab id="directory" title="Directory" />
<TabPanel tabId="files" className="h-full overflow-y-auto">
<Uploader mode="file" />
</TabPanel>
<TabPanel tabId="directory" className="h-full overflow-y-auto">
<Uploader mode="directory" />
</TabPanel>
</Tabs>
</Panel>,
<Panel
title={
<>
<ArrowRightIcon /> Usage
</>
}
className="h-[330px]"
>
<CurrentUsage />
</Panel>,
<Panel
title={
<>
<ArrowRightIcon /> Current plan
</>
}
className="h-[330px]"
>
<CurrentPlan />
</Panel>,
]}
/>
</div>
{showRecentActivity && (
<div className="mt-10">
<LatestActivity />
</div>
<>
<Metadata>
<title>Dashboard</title>
</Metadata>
{verifyingSubscription ? (
<FullScreenLoadingIndicator />
) : (
<PlansProvider>
<div className="w-full">
<Slider
slides={[
<Panel title="Upload" className="h-[330px]">
<Tabs variant="fill">
<Tab id="files" title="Files" />
<Tab id="directory" title="Directory" />
<TabPanel tabId="files" className="h-full overflow-y-auto">
<Uploader mode="file" />
</TabPanel>
<TabPanel tabId="directory" className="h-full overflow-y-auto">
<Uploader mode="directory" />
</TabPanel>
</Tabs>
</Panel>,
<Panel
title={
<>
<ArrowRightIcon /> Usage
</>
}
className="h-[330px]"
>
<CurrentUsage />
</Panel>,
<Panel
title={
<>
<ArrowRightIcon /> Current plan
</>
}
className="h-[330px]"
>
<CurrentPlan />
</Panel>,
]}
/>
</div>
{showRecentActivity && (
<div className="mt-10">
<LatestActivity />
</div>
)}
</PlansProvider>
)}
</PlansProvider>
</>
);
};

View File

@ -7,6 +7,7 @@ import { AddAPIKeyForm, APIKeyType } from "../../components/forms/AddAPIKeyForm"
import { APIKeyList } from "../../components/APIKeyList/APIKeyList";
import { Alert } from "../../components/Alert";
import { AddPublicAPIKeyForm } from "../../components/forms/AddPublicAPIKeyForm";
import { Metadata } from "../../components/Metadata";
const APIKeysPage = () => {
const { data: apiKeys = [], mutate: reloadKeys, error } = useSWR("user/apikeys");
@ -29,6 +30,9 @@ const APIKeysPage = () => {
return (
<>
<Metadata>
<title>API Keys</title>
</Metadata>
<div className="flex flex-col xl:flex-row">
<div className="flex flex-col gap-10 lg:shrink-0 lg:max-w-[576px] xl:max-w-[524px]">
<div>

View File

@ -4,6 +4,7 @@ import UserSettingsLayout from "../../layouts/UserSettingsLayout";
import { Switch } from "../../components/Switch";
import { Button } from "../../components/Button";
import { Metadata } from "../../components/Metadata";
const useExportOptions = () => {
const [pinnedFiles, setPinnedFiles] = useState(false);
@ -29,6 +30,9 @@ const ExportPage = () => {
return (
<>
<Metadata>
<title>Export</title>
</Metadata>
<div className="flex flex-col xl:flex-row">
<div className="flex flex-col gap-10 lg:shrink-0 lg:max-w-[576px] xl:max-w-[524px]">
<section>

View File

@ -7,6 +7,7 @@ import { AccountSettingsForm } from "../../components/forms/AccountSettingsForm"
import { Modal } from "../../components/Modal/Modal";
import { AccountRemovalForm } from "../../components/forms/AccountRemovalForm";
import { Alert } from "../../components/Alert";
import { Metadata } from "../../components/Metadata";
const State = {
Pure: "PURE",
@ -39,6 +40,9 @@ const AccountPage = () => {
return (
<>
<Metadata>
<title>Settings</title>
</Metadata>
<div className="flex flex-col xl:flex-row">
<div className="flex flex-col gap-10 lg:shrink-0 lg:max-w-[576px] xl:max-w-[524px]">
<section>

View File

@ -1,13 +1,17 @@
import * as React from "react";
import { StaticImage } from "gatsby-plugin-image";
import UserSettingsLayout from "../../layouts/UserSettingsLayout";
import { Switch } from "../../components/Switch";
import { StaticImage } from "gatsby-plugin-image";
import { Metadata } from "../../components/Metadata";
const NotificationsPage = () => {
return (
<>
<Metadata>
<title>Notifications</title>
</Metadata>
<div className="flex">
<div className="flex flex-col gap-10 lg:shrink-0 lg:max-w-[576px] xl:max-w-[524px]">
<h4>Notifications</h4>

View File

@ -13,6 +13,7 @@ import { Button } from "../components/Button";
import { usePortalSettings } from "../contexts/portal-settings";
import { Alert } from "../components/Alert";
import HighlightedLink from "../components/HighlightedLink";
import { Metadata } from "../components/Metadata";
const PAID_PORTAL_BREAKPOINTS = [
{
@ -88,6 +89,9 @@ const PlansSlider = () => {
return (
<div className="w-full mb-24">
<Metadata>
<title>Upgrade</title>
</Metadata>
{settings.isSubscriptionRequired && !activePlan && (
<Alert $variant="info" className="mb-6">
<p className="font-semibold mt-0">This Skynet portal requires a paid subscription.</p>

View File

@ -5,6 +5,7 @@ import { AllUsersAuthLayout } from "../../layouts/AuthLayout";
import HighlightedLink from "../../components/HighlightedLink";
import accountsService from "../../services/accountsService";
import { Metadata } from "../../components/Metadata";
const State = {
Pure: "PURE",
@ -52,24 +53,29 @@ const EmailConfirmationPage = ({ location }) => {
}, [token]);
return (
<div className="bg-white px-8 py-10 md:py-32 lg:px-16 xl:px-28 min-h-screen">
<div className="mb-4 md:mb-16">
<img src="/images/logo-black-text.svg" alt="Skynet" />
</div>
<div className="text-center">
{state === State.Pure && <p>Please wait while we verify your account...</p>}
<>
<Metadata>
<title>Confirm E-mail Address</title>
</Metadata>
<div className="bg-white px-8 py-10 md:py-32 lg:px-16 xl:px-28 min-h-screen">
<div className="mb-4 md:mb-16">
<img src="/images/logo-black-text.svg" alt="Skynet" />
</div>
<div className="text-center">
{state === State.Pure && <p>Please wait while we verify your account...</p>}
{state === State.Success && (
<>
<p className="text-primary font-semibold">All done!</p>
<p>You will be redirected to your dashboard shortly.</p>
<HighlightedLink to="/">Redirect now.</HighlightedLink>
</>
)}
{state === State.Success && (
<>
<p className="text-primary font-semibold">All done!</p>
<p>You will be redirected to your dashboard shortly.</p>
<HighlightedLink to="/">Redirect now.</HighlightedLink>
</>
)}
{state === State.Failure && <p className="text-error">Something went wrong, please try again later.</p>}
{state === State.Failure && <p className="text-error">Something went wrong, please try again later.</p>}
</div>
</div>
</div>
</>
);
};

View File

@ -5,6 +5,7 @@ import AuthLayout from "../../layouts/AuthLayout";
import { ResetPasswordForm } from "../../components/forms/ResetPasswordForm";
import HighlightedLink from "../../components/HighlightedLink";
import { Metadata } from "../../components/Metadata";
const State = {
Pure: "PURE",
@ -19,35 +20,40 @@ const RecoverPage = ({ location }) => {
const [state, setState] = useState(State.Pure);
return (
<div className="bg-white px-8 py-10 md:py-32 lg:px-16 xl:px-28 min-h-screen">
<div className="mb-4 md:mb-16">
<img src="/images/logo-black-text.svg" alt="Skynet" />
</div>
{state !== State.Success && (
<ResetPasswordForm
token={token}
onSuccess={() => {
setState(State.Success);
navigate("/");
}}
onFailure={() => setState(State.Failure)}
/>
)}
<>
<Metadata>
<title>Recover Your Account</title>
</Metadata>
<div className="bg-white px-8 py-10 md:py-32 lg:px-16 xl:px-28 min-h-screen">
<div className="mb-4 md:mb-16">
<img src="/images/logo-black-text.svg" alt="Skynet" />
</div>
{state !== State.Success && (
<ResetPasswordForm
token={token}
onSuccess={() => {
setState(State.Success);
navigate("/");
}}
onFailure={() => setState(State.Failure)}
/>
)}
{state === State.Success && (
<p className="text-primary text-center font-semibold">
All done! You will be redirected to your dashboard shortly.
{state === State.Success && (
<p className="text-primary text-center font-semibold">
All done! You will be redirected to your dashboard shortly.
</p>
)}
{state === State.Failure && (
<p className="text-error text-center">Something went wrong, please try again later.</p>
)}
<p className="text-sm text-center mt-8">
Suddenly remembered your old password? <HighlightedLink to="/auth/login">Sign in</HighlightedLink>
</p>
)}
{state === State.Failure && (
<p className="text-error text-center">Something went wrong, please try again later.</p>
)}
<p className="text-sm text-center mt-8">
Suddenly remembered your old password? <HighlightedLink to="/auth/login">Sign in</HighlightedLink>
</p>
</div>
</div>
</>
);
};

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB