Dashboard v2 lighthouse scores improvements (#1972)
* Metadata improvements * Accessibility improvements * Improve performance on mobile
This commit is contained in:
parent
d077a27fa9
commit
7bd54359e6
|
@ -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: [
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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}>
|
||||
|
|
|
@ -101,7 +101,6 @@ export default function CurrentUsage() {
|
|||
>
|
||||
UPGRADE
|
||||
</Link>{" "}
|
||||
{/* TODO: proper URL */}
|
||||
<span>{usage.filesLimit}</span>
|
||||
</span>
|
||||
</div>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
};
|
|
@ -0,0 +1 @@
|
|||
export * from "./Metadata";
|
|
@ -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>
|
||||
|
|
|
@ -94,6 +94,7 @@ export const NavBar = () => {
|
|||
partiallyActive
|
||||
/>
|
||||
<DropdownMenuLink
|
||||
as="button"
|
||||
onClick={onLogout}
|
||||
activeClassName="text-primary"
|
||||
className="cursor-pointer"
|
||||
|
|
|
@ -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)}
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
)}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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>
|
||||
)}
|
||||
|
|
|
@ -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,6 +18,10 @@ const LoginPage = ({ location }) => {
|
|||
}, [user, redirectTo]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<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" />
|
||||
|
@ -27,6 +32,7 @@ const LoginPage = ({ location }) => {
|
|||
}}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
@ -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,6 +16,10 @@ const ResetPasswordPage = () => {
|
|||
const [state, setState] = useState(State.Pure);
|
||||
|
||||
return (
|
||||
<>
|
||||
<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" />
|
||||
|
@ -40,6 +45,7 @@ const ResetPasswordPage = () => {
|
|||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
@ -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" />
|
||||
|
|
|
@ -1,16 +1,21 @@
|
|||
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 (
|
||||
<>
|
||||
<Metadata>
|
||||
<title>My Files</title>
|
||||
</Metadata>
|
||||
<Panel title="Files">
|
||||
<Tabs defaultTab={defaultTab || "uploads"}>
|
||||
<Tab id="uploads" title="Uploads" />
|
||||
|
@ -23,6 +28,7 @@ const FilesPage = () => {
|
|||
</TabPanel>
|
||||
</Tabs>
|
||||
</Panel>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
@ -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,6 +25,13 @@ const IndexPage = () => {
|
|||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Metadata>
|
||||
<title>Dashboard</title>
|
||||
</Metadata>
|
||||
{verifyingSubscription ? (
|
||||
<FullScreenLoadingIndicator />
|
||||
) : (
|
||||
<PlansProvider>
|
||||
<div className="w-full">
|
||||
<Slider
|
||||
|
@ -69,6 +77,8 @@ const IndexPage = () => {
|
|||
</div>
|
||||
)}
|
||||
</PlansProvider>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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,6 +53,10 @@ const EmailConfirmationPage = ({ location }) => {
|
|||
}, [token]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<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" />
|
||||
|
@ -70,6 +75,7 @@ const EmailConfirmationPage = ({ location }) => {
|
|||
{state === State.Failure && <p className="text-error">Something went wrong, please try again later.</p>}
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
@ -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,6 +20,10 @@ const RecoverPage = ({ location }) => {
|
|||
const [state, setState] = useState(State.Pure);
|
||||
|
||||
return (
|
||||
<>
|
||||
<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" />
|
||||
|
@ -48,6 +53,7 @@ const RecoverPage = ({ location }) => {
|
|||
Suddenly remembered your old password? <HighlightedLink to="/auth/login">Sign in</HighlightedLink>
|
||||
</p>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
Binary file not shown.
After Width: | Height: | Size: 2.1 KiB |
Reference in New Issue