improve loading states:

* app loader
 * specific loaders with spinner
This commit is contained in:
m5r 2021-10-18 00:06:45 +02:00
parent 29101b1daf
commit 931384b468
18 changed files with 867 additions and 46 deletions

View File

@ -0,0 +1,15 @@
.ring {
display: inline-block;
width: 50px;
height: 50px;
border: 3px solid rgba(0, 0, 0, 0.15);
border-radius: 50%;
border-top-color: currentColor;
animation: spin 1s ease-in-out infinite;
}
@keyframes spin {
to {
transform: rotate(360deg);
}
}

View File

@ -0,0 +1,11 @@
import clsx from "clsx";
import styles from "./spinner.module.css";
export default function Spinner() {
return (
<div className="h-full flex">
<div className={clsx(styles.ring, "m-auto text-primary-400")} />
</div>
);
}

View File

@ -29,7 +29,7 @@ function NavLink({ path, label, icon }: NavLinkProps) {
return ( return (
<div className="flex flex-col items-center justify-around h-full"> <div className="flex flex-col items-center justify-around h-full">
<Link href={path}> <Link href={path} prefetch={false}>
<a <a
className={clsx("flex flex-col items-center", { className={clsx("flex flex-col items-center", {
"text-primary-500": isActiveRoute, "text-primary-500": isActiveRoute,

View File

@ -1,5 +1,5 @@
import type { ErrorInfo, FunctionComponent } from "react"; import type { ErrorInfo, FunctionComponent } from "react";
import { Component } from "react"; import { Component, Suspense } from "react";
import { import {
Head, Head,
withRouter, withRouter,
@ -14,6 +14,7 @@ import type { WithRouterProps } from "next/dist/client/with-router";
import appLogger from "../../../../integrations/logger"; import appLogger from "../../../../integrations/logger";
import Footer from "./footer"; import Footer from "./footer";
import Loader from "./loader";
type Props = { type Props = {
title: string; title: string;
@ -23,7 +24,7 @@ type Props = {
const logger = appLogger.child({ module: "Layout" }); const logger = appLogger.child({ module: "Layout" });
const Layout: FunctionComponent<Props> = ({ children, title, pageTitle = title, hideFooter = false }) => { const AppLayout: FunctionComponent<Props> = ({ children, title, pageTitle = title, hideFooter = false }) => {
return ( return (
<> <>
{pageTitle ? ( {pageTitle ? (
@ -32,16 +33,18 @@ const Layout: FunctionComponent<Props> = ({ children, title, pageTitle = title,
</Head> </Head>
) : null} ) : null}
<div className="h-full w-full overflow-hidden fixed bg-gray-100"> <Suspense fallback={<Loader />}>
<div className="flex flex-col w-full h-full"> <div className="h-full w-full overflow-hidden fixed bg-gray-100">
<div className="flex flex-col flex-1 w-full overflow-y-auto"> <div className="flex flex-col w-full h-full">
<main className="flex flex-col flex-1 my-0 h-full"> <div className="flex flex-col flex-1 w-full overflow-y-auto">
<ErrorBoundary>{children}</ErrorBoundary> <main className="flex flex-col flex-1 my-0 h-full">
</main> <ErrorBoundary>{children}</ErrorBoundary>
</main>
</div>
{!hideFooter ? <Footer /> : null}
</div> </div>
{!hideFooter ? <Footer /> : null}
</div> </div>
</div> </Suspense>
</> </>
); );
}; };
@ -109,4 +112,4 @@ const ErrorBoundary = withRouter(
}, },
); );
export default Layout; export default AppLayout;

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,8 @@
#gradientCanvas {
width: 100%;
height: 100%;
--gradient-color-1: #c3e4ff;
--gradient-color-2: #6ec3f4;
--gradient-color-3: #eae2ff;
--gradient-color-4: #b9beff;
}

View File

@ -0,0 +1,26 @@
import { useEffect } from "react";
import Logo from "../../components/logo";
import { Gradient } from "./loader-gradient.js";
import styles from "./loader.module.css";
export default function Loader() {
useEffect(() => {
const gradient = new Gradient();
// @ts-ignore
gradient.initGradient(`#${styles.gradientCanvas}`);
}, []);
return (
<div className="min-h-screen min-w-screen relative">
<div className="relative z-10 min-h-screen flex flex-col justify-center py-12 px-4 sm:px-6 lg:px-8">
<div className="flex flex-col sm:mx-auto sm:w-full sm:max-w-sm">
<Logo className="mx-auto h-12 w-12" />
<span className="mt-2 text-center text-lg leading-9 text-gray-900">Loading up Shellphone...</span>
</div>
</div>
<canvas id={styles.gradientCanvas} className="absolute top-0 z-0" />
</div>
);
}

View File

@ -3,13 +3,14 @@ import type { BlitzPage } from "blitz";
import { Routes, dynamic } from "blitz"; import { Routes, dynamic } from "blitz";
import { atom, useAtom } from "jotai"; import { atom, useAtom } from "jotai";
import Layout from "app/core/layouts/layout"; import AppLayout from "app/core/layouts/layout";
import ConversationsList from "../components/conversations-list"; import ConversationsList from "../components/conversations-list";
import NewMessageButton from "../components/new-message-button"; import NewMessageButton from "../components/new-message-button";
import MissingTwilioCredentials from "app/core/components/missing-twilio-credentials"; import MissingTwilioCredentials from "app/core/components/missing-twilio-credentials";
import useNotifications from "app/core/hooks/use-notifications"; import useNotifications from "app/core/hooks/use-notifications";
import useCurrentUser from "app/core/hooks/use-current-user"; import useCurrentUser from "app/core/hooks/use-current-user";
import PageTitle from "../../core/components/page-title"; import PageTitle from "../../core/components/page-title";
import Spinner from "../../core/components/spinner";
const Messages: BlitzPage = () => { const Messages: BlitzPage = () => {
const { hasFilledTwilioCredentials, hasPhoneNumber } = useCurrentUser(); const { hasFilledTwilioCredentials, hasPhoneNumber } = useCurrentUser();
@ -26,7 +27,7 @@ const Messages: BlitzPage = () => {
return ( return (
<> <>
<MissingTwilioCredentials /> <MissingTwilioCredentials />
<PageTitle className="filter blur-sm absolute top-0" title="Messages" /> <PageTitle className="filter blur-sm select-none absolute top-0" title="Messages" />
</> </>
); );
} }
@ -35,7 +36,8 @@ const Messages: BlitzPage = () => {
<> <>
<PageTitle title="Messages" /> <PageTitle title="Messages" />
<section className="flex flex-grow flex-col"> <section className="flex flex-grow flex-col">
<Suspense fallback="Loading..."> <Suspense fallback={<Spinner />}>
{/* TODO: skeleton conversations list */}
<ConversationsList /> <ConversationsList />
</Suspense> </Suspense>
</section> </section>
@ -52,7 +54,7 @@ const NewMessageBottomSheet = dynamic(() => import("../components/new-message-bo
export const bottomSheetOpenAtom = atom(false); export const bottomSheetOpenAtom = atom(false);
Messages.getLayout = (page) => <Layout title="Messages">{page}</Layout>; Messages.getLayout = (page) => <AppLayout title="Messages">{page}</AppLayout>;
Messages.authenticate = { redirectTo: Routes.SignIn().pathname }; Messages.authenticate = { redirectTo: Routes.SignIn().pathname };

View File

@ -3,7 +3,7 @@ import type { BlitzPage } from "blitz";
import { Routes, useRouter } from "blitz"; import { Routes, useRouter } from "blitz";
import { IoChevronBack, IoInformationCircle, IoCall } from "react-icons/io5"; import { IoChevronBack, IoInformationCircle, IoCall } from "react-icons/io5";
import Layout from "../../../core/layouts/layout"; import AppLayout from "../../../core/layouts/layout";
import Conversation from "../../components/conversation"; import Conversation from "../../components/conversation";
import useConversation from "../../hooks/use-conversation"; import useConversation from "../../hooks/use-conversation";
@ -14,7 +14,7 @@ const ConversationPage: BlitzPage = () => {
const conversation = useConversation(recipient)[0]; const conversation = useConversation(recipient)[0];
return ( return (
<Layout title={pageTitle} hideFooter> <AppLayout title={pageTitle} hideFooter>
<header className="absolute top-0 w-screen h-12 backdrop-filter backdrop-blur-sm bg-white bg-opacity-75 border-b grid grid-cols-3 items-center"> <header className="absolute top-0 w-screen h-12 backdrop-filter backdrop-blur-sm bg-white bg-opacity-75 border-b grid grid-cols-3 items-center">
<span className="col-start-1 col-span-1 pl-2 cursor-pointer" onClick={router.back}> <span className="col-start-1 col-span-1 pl-2 cursor-pointer" onClick={router.back}>
<IoChevronBack className="h-8 w-8" /> <IoChevronBack className="h-8 w-8" />
@ -28,7 +28,7 @@ const ConversationPage: BlitzPage = () => {
<Suspense fallback={<div className="pt-12">Loading messages with {recipient}</div>}> <Suspense fallback={<div className="pt-12">Loading messages with {recipient}</div>}>
<Conversation /> <Conversation />
</Suspense> </Suspense>
</Layout> </AppLayout>
); );
}; };

View File

@ -21,7 +21,7 @@ import "app/core/styles/index.css";
const { publicRuntimeConfig } = getConfig(); const { publicRuntimeConfig } = getConfig();
export default function App({ Component, pageProps }: AppProps) { export default function App({ Component, pageProps }: AppProps) {
const session = useSession(); const session = useSession({ suspense: false });
usePanelbear(publicRuntimeConfig.panelBear.siteId); usePanelbear(publicRuntimeConfig.panelBear.siteId);
useEffect(() => { useEffect(() => {
if (session.userId) { if (session.userId) {
@ -42,7 +42,8 @@ export default function App({ Component, pageProps }: AppProps) {
FallbackComponent={RootErrorFallback} FallbackComponent={RootErrorFallback}
onReset={useQueryErrorResetBoundary().reset} onReset={useQueryErrorResetBoundary().reset}
> >
<Suspense fallback="Silence, ca pousse">{getLayout(<Component {...pageProps} />)}</Suspense> {/* TODO: better default fallback */}
<Suspense fallback={null}>{getLayout(<Component {...pageProps} />)}</Suspense>
</ErrorBoundary> </ErrorBoundary>
); );
} }

View File

@ -1,11 +1,11 @@
import { useRouter } from "blitz"; import { useRouter } from "blitz";
import Layout from "../core/layouts/layout"; import AppLayout from "../core/layouts/layout";
export default function Offline() { export default function Offline() {
const router = useRouter(); const router = useRouter();
return ( return (
<Layout title="App went offline"> <AppLayout title="App went offline">
<h2 className="mt-6 text-center text-3xl leading-9 font-extrabold text-gray-900"> <h2 className="mt-6 text-center text-3xl leading-9 font-extrabold text-gray-900">
Oops, looks like you went offline. Oops, looks like you went offline.
</h2> </h2>
@ -17,6 +17,6 @@ export default function Offline() {
</span> </span>
</button> </button>
</p> </p>
</Layout> </AppLayout>
); );
} }

View File

@ -2,11 +2,12 @@ import { Suspense } from "react";
import type { BlitzPage } from "blitz"; import type { BlitzPage } from "blitz";
import { Routes } from "blitz"; import { Routes } from "blitz";
import Layout from "app/core/layouts/layout"; import AppLayout from "app/core/layouts/layout";
import PhoneCallsList from "../components/phone-calls-list"; import PhoneCallsList from "../components/phone-calls-list";
import MissingTwilioCredentials from "app/core/components/missing-twilio-credentials"; import MissingTwilioCredentials from "app/core/components/missing-twilio-credentials";
import useCurrentUser from "app/core/hooks/use-current-user"; import useCurrentUser from "app/core/hooks/use-current-user";
import PageTitle from "../../core/components/page-title"; import PageTitle from "../../core/components/page-title";
import Spinner from "../../core/components/spinner";
const PhoneCalls: BlitzPage = () => { const PhoneCalls: BlitzPage = () => {
const { hasFilledTwilioCredentials, hasPhoneNumber } = useCurrentUser(); const { hasFilledTwilioCredentials, hasPhoneNumber } = useCurrentUser();
@ -15,7 +16,7 @@ const PhoneCalls: BlitzPage = () => {
return ( return (
<> <>
<MissingTwilioCredentials /> <MissingTwilioCredentials />
<PageTitle className="filter blur-sm absolute top-0" title="Calls" /> <PageTitle className="filter blur-sm select-none absolute top-0" title="Calls" />
</> </>
); );
} }
@ -24,7 +25,8 @@ const PhoneCalls: BlitzPage = () => {
<> <>
<PageTitle className="pl-12" title="Calls" /> <PageTitle className="pl-12" title="Calls" />
<section className="flex flex-grow flex-col"> <section className="flex flex-grow flex-col">
<Suspense fallback="Loading..."> <Suspense fallback={<Spinner />}>
{/* TODO: skeleton phone calls list */}
<PhoneCallsList /> <PhoneCallsList />
</Suspense> </Suspense>
</section> </section>
@ -32,7 +34,7 @@ const PhoneCalls: BlitzPage = () => {
); );
}; };
PhoneCalls.getLayout = (page) => <Layout title="Calls">{page}</Layout>; PhoneCalls.getLayout = (page) => <AppLayout title="Calls">{page}</AppLayout>;
PhoneCalls.authenticate = { redirectTo: Routes.SignIn() }; PhoneCalls.authenticate = { redirectTo: Routes.SignIn() };

View File

@ -7,7 +7,7 @@ import { Transition } from "@headlessui/react";
import { IoBackspace, IoCall } from "react-icons/io5"; import { IoBackspace, IoCall } from "react-icons/io5";
import { Direction } from "db"; import { Direction } from "db";
import Layout from "app/core/layouts/layout"; import AppLayout from "app/core/layouts/layout";
import Keypad from "../components/keypad"; import Keypad from "../components/keypad";
import usePhoneCalls from "../hooks/use-phone-calls"; import usePhoneCalls from "../hooks/use-phone-calls";
import useKeyPress from "../hooks/use-key-press"; import useKeyPress from "../hooks/use-key-press";
@ -161,7 +161,7 @@ const pressBackspaceAtom = atom(null, (get, set) => {
set(phoneNumberAtom, (prevState) => prevState.slice(0, -1)); set(phoneNumberAtom, (prevState) => prevState.slice(0, -1));
}); });
KeypadPage.getLayout = (page) => <Layout title="Keypad">{page}</Layout>; KeypadPage.getLayout = (page) => <AppLayout title="Keypad">{page}</AppLayout>;
KeypadPage.authenticate = { redirectTo: Routes.SignIn() }; KeypadPage.authenticate = { redirectTo: Routes.SignIn() };

View File

@ -10,10 +10,11 @@ export default function BillingHistory() {
skip, skip,
pagesNumber, pagesNumber,
currentPage, currentPage,
goToPreviousPage, lastPage,
hasPreviousPage, hasPreviousPage,
goToNextPage,
hasNextPage, hasNextPage,
goToPreviousPage,
goToNextPage,
setPage, setPage,
} = usePaymentsHistory(); } = usePaymentsHistory();
@ -104,8 +105,8 @@ export default function BillingHistory() {
Previous Previous
</button> </button>
<p className="text-sm text-gray-700 self-center"> <p className="text-sm text-gray-700 self-center">
Page <span className="font-medium">1</span> of{" "} Page <span className="font-medium">{currentPage}</span> of{" "}
<span className="font-medium">4</span> <span className="font-medium">{lastPage}</span>
</p> </p>
<button <button
onClick={goToNextPage} onClick={goToNextPage}

View File

@ -9,7 +9,7 @@ type Props = {
const PaddleLink: FunctionComponent<Props> = ({ onClick, text }) => ( const PaddleLink: FunctionComponent<Props> = ({ onClick, text }) => (
<button className="flex space-x-2 items-center text-left" onClick={onClick}> <button className="flex space-x-2 items-center text-left" onClick={onClick}>
<HiExternalLink className="w-6 h-6 flex-shrink-0" /> <HiExternalLink className="w-6 h-6 flex-shrink-0" />
<span className="transition-colors duration-150 border-b border-transparent hover:border-primary-500"> <span className="font-medium transition-colors duration-150 border-b border-transparent hover:border-primary-500">
{text} {text}
</span> </span>
</button> </button>

View File

@ -1,4 +1,5 @@
import type { FunctionComponent } from "react"; import type { FunctionComponent } from "react";
import { Suspense } from "react";
import { Link, Routes, useMutation, useRouter } from "blitz"; import { Link, Routes, useMutation, useRouter } from "blitz";
import clsx from "clsx"; import clsx from "clsx";
import { import {
@ -10,9 +11,10 @@ import {
IoPersonCircleOutline, IoPersonCircleOutline,
} from "react-icons/io5"; } from "react-icons/io5";
import Layout from "app/core/layouts/layout"; import AppLayout from "app/core/layouts/layout";
import logout from "app/auth/mutations/logout";
import Divider from "./divider"; import Divider from "./divider";
import Spinner from "../../core/components/spinner";
import logout from "app/auth/mutations/logout";
const subNavigation = [ const subNavigation = [
{ name: "Account", href: Routes.Account(), icon: IoPersonCircleOutline }, { name: "Account", href: Routes.Account(), icon: IoPersonCircleOutline },
@ -26,7 +28,7 @@ const SettingsLayout: FunctionComponent = ({ children }) => {
const [logoutMutation] = useMutation(logout); const [logoutMutation] = useMutation(logout);
return ( return (
<Layout title="Settings"> <AppLayout title="Settings">
<header className="bg-gray-100 px-2 sm:px-6 lg:px-8"> <header className="bg-gray-100 px-2 sm:px-6 lg:px-8">
<header className="flex"> <header className="flex">
<span className="flex items-center cursor-pointer" onClick={router.back}> <span className="flex items-center cursor-pointer" onClick={router.back}>
@ -43,7 +45,7 @@ const SettingsLayout: FunctionComponent = ({ children }) => {
const isCurrentPage = item.href.pathname === router.pathname; const isCurrentPage = item.href.pathname === router.pathname;
return ( return (
<Link key={item.name} href={item.href}> <Link key={item.name} href={item.href} prefetch>
<a <a
className={clsx( className={clsx(
isCurrentPage isCurrentPage
@ -79,10 +81,12 @@ const SettingsLayout: FunctionComponent = ({ children }) => {
</nav> </nav>
</aside> </aside>
<div className="flex-grow overflow-y-auto space-y-6 px-2 sm:px-6 lg:col-span-9">{children}</div> <div className="flex-grow overflow-y-auto space-y-6 px-2 sm:px-6 lg:col-span-9">
<Suspense fallback={<Spinner />}>{children}</Suspense>
</div>
</div> </div>
</main> </main>
</Layout> </AppLayout>
); );
}; };

View File

@ -14,6 +14,7 @@ export default function usePaymentsHistory() {
.fill(-1) .fill(-1)
.map((_, i) => i + 1); .map((_, i) => i + 1);
const currentPage = Math.floor((skip / count) * totalPages) + 1; const currentPage = Math.floor((skip / count) * totalPages) + 1;
const lastPage = pagesNumber[pagesNumber.length - 1];
const hasPreviousPage = skip > 0; const hasPreviousPage = skip > 0;
const hasNextPage = hasMore && !!nextPage; const hasNextPage = hasMore && !!nextPage;
const goToPreviousPage = () => hasPreviousPage && setSkip(skip - itemsPerPage); const goToPreviousPage = () => hasPreviousPage && setSkip(skip - itemsPerPage);
@ -26,6 +27,7 @@ export default function usePaymentsHistory() {
skip, skip,
pagesNumber, pagesNumber,
currentPage, currentPage,
lastPage,
hasPreviousPage, hasPreviousPage,
hasNextPage, hasNextPage,
goToPreviousPage, goToPreviousPage,

View File

@ -1,4 +1,3 @@
import { Suspense } from "react";
import type { BlitzPage } from "blitz"; import type { BlitzPage } from "blitz";
import { Routes, dynamic } from "blitz"; import { Routes, dynamic } from "blitz";
@ -8,10 +7,8 @@ import PhoneNumberForm from "../../components/phone/phone-number-form";
const PhoneSettings: BlitzPage = () => { const PhoneSettings: BlitzPage = () => {
return ( return (
<div className="flex flex-col space-y-6"> <div className="flex flex-col space-y-6">
<Suspense fallback="Loading..."> <TwilioApiForm />
<TwilioApiForm /> <PhoneNumberForm />
<PhoneNumberForm />
</Suspense>
</div> </div>
); );
}; };