feat(layout): created Layout component

Also adds a Transition component for dealing with transitions easily with tailwind
pull/7/head
sct 4 years ago
parent 8db2bfe44f
commit 1f497e8913

@ -17,6 +17,7 @@
"next": "9.5.2",
"react": "16.13.1",
"react-dom": "16.13.1",
"react-transition-group": "^4.4.1",
"reflect-metadata": "^0.1.13",
"sqlite3": "^5.0.0",
"typeorm": "^0.2.25"
@ -27,6 +28,7 @@
"@types/express": "^4.17.7",
"@types/node": "^14.0.27",
"@types/react": "^16.9.46",
"@types/react-transition-group": "^4.4.0",
"@typescript-eslint/eslint-plugin": "^3.9.0",
"@typescript-eslint/parser": "^3.9.0",
"commitizen": "^4.1.2",

@ -0,0 +1,271 @@
import React, { useState } from 'react';
import Transition from '../Transition';
const Layout: React.FC = ({ children }) => {
const [isSidebarOpen, setSidebarOpen] = useState(false);
const [isDropdownOpen, setDropdownOpen] = useState(false);
return (
<div className="h-screen flex overflow-hidden bg-gray-100">
<div className="md:hidden">
<Transition show={isSidebarOpen}>
<div className="fixed inset-0 flex z-40">
<Transition
enter="transition-opacity ease-linear duration-300"
enterFrom="opacity-0"
enterTo="opacity-100"
leave="transition-opacity ease-linear duration-300"
leaveFrom="opacity-100"
leaveTo="opacity-0"
>
<div className="fixed inset-0">
<div className="absolute inset-0 bg-gray-600 opacity-75"></div>
</div>
</Transition>
<Transition
enter="transition ease-in-out duration-300 transform"
enterFrom="-translate-x-full"
enterTo="translate-x-0"
leave="transition ease-in-out duration-300 transform"
leaveFrom="translate-x-0"
leaveTo="-translate-x-full"
>
<>
<div className="relative flex-1 flex flex-col max-w-xs w-full bg-gray-800">
<div className="absolute top-0 right-0 -mr-14 p-1">
<button
className="flex items-center justify-center h-12 w-12 rounded-full focus:outline-none focus:bg-gray-600"
aria-label="Close sidebar"
onClick={() => setSidebarOpen(false)}
>
<svg
className="h-6 w-6 text-white"
stroke="currentColor"
fill="none"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2"
d="M6 18L18 6M6 6l12 12"
/>
</svg>
</button>
</div>
<div className="flex-1 h-0 pt-5 pb-4 overflow-y-auto">
<div className="flex-shrink-0 flex items-center px-4">
<span className="text-xl text-cool-gray-50">
Overseerr
</span>
</div>
<nav className="mt-5 px-2 space-y-1">
<a
href="#"
className="group flex items-center px-2 py-2 text-base leading-6 font-medium rounded-md text-white bg-gray-900 focus:outline-none focus:bg-gray-700 transition ease-in-out duration-150"
>
<svg
className="mr-4 h-6 w-6 text-gray-300 group-hover:text-gray-300 group-focus:text-gray-300 transition ease-in-out duration-150"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2"
d="M3 12l2-2m0 0l7-7 7 7M5 10v10a1 1 0 001 1h3m10-11l2 2m-2-2v10a1 1 0 01-1 1h-3m-6 0a1 1 0 001-1v-4a1 1 0 011-1h2a1 1 0 011 1v4a1 1 0 001 1m-6 0h6"
/>
</svg>
Dashboard
</a>
</nav>
</div>
</div>
<div className="flex-shrink-0 w-14">
{/* <!-- Force sidebar to shrink to fit close icon --> */}
</div>
</>
</Transition>
</div>
</Transition>
</div>
<div className="hidden md:flex md:flex-shrink-0">
<div className="flex flex-col w-64">
<div className="flex flex-col h-0 flex-1 bg-gray-800">
<div className="flex-1 flex flex-col pt-5 pb-4 overflow-y-auto">
<div className="flex items-center flex-shrink-0 px-4">
<span className="text-2xl text-cool-gray-50">Overseerr</span>
</div>
<nav className="mt-5 flex-1 px-2 bg-gray-800 space-y-1">
<a
href="#"
className="group flex items-center px-2 py-2 text-sm leading-5 font-medium text-white rounded-md bg-gray-900 focus:outline-none focus:bg-gray-700 transition ease-in-out duration-150"
>
<svg
className="mr-3 h-6 w-6 text-gray-300 group-hover:text-gray-300 group-focus:text-gray-300 transition ease-in-out duration-150"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2"
d="M3 12l2-2m0 0l7-7 7 7M5 10v10a1 1 0 001 1h3m10-11l2 2m-2-2v10a1 1 0 01-1 1h-3m-6 0a1 1 0 001-1v-4a1 1 0 011-1h2a1 1 0 011 1v4a1 1 0 001 1m-6 0h6"
/>
</svg>
Dashboard
</a>
</nav>
</div>
</div>
</div>
</div>
<div className="flex flex-col w-0 flex-1 overflow-hidden">
<div className="relative z-10 flex-shrink-0 flex h-16 bg-white shadow">
<button
className="px-4 border-r border-gray-200 text-gray-500 focus:outline-none focus:bg-gray-100 focus:text-gray-600 md:hidden"
aria-label="Open sidebar"
onClick={() => setSidebarOpen(true)}
>
<svg
className="h-6 w-6"
stroke="currentColor"
fill="none"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2"
d="M4 6h16M4 12h16M4 18h7"
/>
</svg>
</button>
<div className="flex-1 px-4 flex justify-between">
<div className="flex-1 flex">
<form className="w-full flex md:ml-0" action="#" method="GET">
<label htmlFor="search_field" className="sr-only">
Search
</label>
<div className="relative w-full text-gray-400 focus-within:text-gray-600">
<div className="absolute inset-y-0 left-0 flex items-center pointer-events-none">
<svg
className="h-5 w-5"
fill="currentColor"
viewBox="0 0 20 20"
>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M8 4a4 4 0 100 8 4 4 0 000-8zM2 8a6 6 0 1110.89 3.476l4.817 4.817a1 1 0 01-1.414 1.414l-4.816-4.816A6 6 0 012 8z"
/>
</svg>
</div>
<input
id="search_field"
className="block w-full h-full pl-8 pr-3 py-2 rounded-md text-gray-900 placeholder-gray-500 focus:outline-none focus:placeholder-gray-400 sm:text-sm"
placeholder="Search"
type="search"
/>
</div>
</form>
</div>
<div className="ml-4 flex items-center md:ml-6">
<button
className="p-1 text-gray-400 rounded-full hover:bg-gray-100 hover:text-gray-500 focus:outline-none focus:shadow-outline focus:text-gray-500"
aria-label="Notifications"
>
<svg
className="h-6 w-6"
stroke="currentColor"
fill="none"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2"
d="M15 17h5l-1.405-1.405A2.032 2.032 0 0118 14.158V11a6.002 6.002 0 00-4-5.659V5a2 2 0 10-4 0v.341C7.67 6.165 6 8.388 6 11v3.159c0 .538-.214 1.055-.595 1.436L4 17h5m6 0v1a3 3 0 11-6 0v-1m6 0H9"
/>
</svg>
</button>
<div className="ml-3 relative">
<div>
<button
className="max-w-xs flex items-center text-sm rounded-full focus:outline-none focus:shadow-outline"
id="user-menu"
aria-label="User menu"
aria-haspopup="true"
onClick={() => setDropdownOpen((state) => !state)}
>
<img
className="h-8 w-8 rounded-full"
src="https://avatars1.githubusercontent.com/u/234213?s=460&u=7f30f76bd7bbdab45bab7544ebd80aa88ea11caf&v=4"
alt=""
/>
</button>
</div>
<Transition
show={isDropdownOpen}
enter="transition ease-out duration-100"
enterFrom="transform opacity-0 scale-95"
enterTo="transform opacity-100 scale-100"
leave="transition ease-in duration-75"
leaveFrom="transform opacity-100 scale-100"
leaveTo="transform opacity-0 scale-95"
>
<div className="origin-top-right absolute right-0 mt-2 w-48 rounded-md shadow-lg">
<div
className="py-1 rounded-md bg-white shadow-xs"
role="menu"
aria-orientation="vertical"
aria-labelledby="user-menu"
>
<a
href="#"
className="block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100 transition ease-in-out duration-150"
role="menuitem"
>
Your Profile
</a>
<a
href="#"
className="block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100 transition ease-in-out duration-150"
role="menuitem"
>
Settings
</a>
<a
href="#"
className="block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100 transition ease-in-out duration-150"
role="menuitem"
>
Sign out
</a>
</div>
</div>
</Transition>
</div>
</div>
</div>
</div>
<main
className="flex-1 relative z-0 overflow-y-auto focus:outline-none"
tabIndex={0}
>
<div className="pt-2 pb-6 md:py-6">
<div className="max-w-7xl mx-auto px-4 sm:px-6 md:px-8">
{children}
</div>
</div>
</main>
</div>
</div>
);
};
export default Layout;

@ -0,0 +1,124 @@
import React from 'react';
import { CSSTransition as ReactCSSTransition } from 'react-transition-group';
import { useRef, useEffect, useContext } from 'react';
interface CSSTransitionProps {
show?: boolean;
enter?: string;
enterFrom?: string;
enterTo?: string;
leave?: string;
leaveFrom?: string;
leaveTo?: string;
appear?: boolean;
}
const TransitionContext = React.createContext<{
parent: { show?: boolean; isInitialRender?: boolean; appear?: boolean };
}>({
parent: {},
});
function useIsInitialRender() {
const isInitialRender = useRef(true);
useEffect(() => {
isInitialRender.current = false;
}, []);
return isInitialRender.current;
}
const CSSTransition: React.FC<CSSTransitionProps> = ({
show,
enter = '',
enterFrom = '',
enterTo = '',
leave = '',
leaveFrom = '',
leaveTo = '',
appear,
children,
}) => {
const enterClasses = enter.split(' ').filter((s) => s.length);
const enterFromClasses = enterFrom.split(' ').filter((s) => s.length);
const enterToClasses = enterTo.split(' ').filter((s) => s.length);
const leaveClasses = leave.split(' ').filter((s) => s.length);
const leaveFromClasses = leaveFrom.split(' ').filter((s) => s.length);
const leaveToClasses = leaveTo.split(' ').filter((s) => s.length);
const addClasses = (node: HTMLElement, classes: string[]) => {
classes.length && node.classList.add(...classes);
};
const removeClasses = (node: HTMLElement, classes: string[]) => {
classes.length && node.classList.remove(...classes);
};
return (
<ReactCSSTransition
appear={appear}
unmountOnExit
in={show}
addEndListener={(node, done) => {
node.addEventListener('transitionend', done, false);
}}
onEnter={(node: HTMLElement) => {
addClasses(node, [...enterClasses, ...enterFromClasses]);
}}
onEntering={(node: HTMLElement) => {
removeClasses(node, enterFromClasses);
addClasses(node, enterToClasses);
}}
onEntered={(node: HTMLElement) => {
removeClasses(node, [...enterToClasses, ...enterClasses]);
}}
onExit={(node) => {
addClasses(node, [...leaveClasses, ...leaveFromClasses]);
}}
onExiting={(node) => {
removeClasses(node, leaveFromClasses);
addClasses(node, leaveToClasses);
}}
onExited={(node) => {
removeClasses(node, [...leaveToClasses, ...leaveClasses]);
}}
>
{children}
</ReactCSSTransition>
);
};
const Transition: React.FC<CSSTransitionProps> = ({
show,
appear,
...rest
}) => {
const { parent } = useContext(TransitionContext);
const isInitialRender = useIsInitialRender();
const isChild = show === undefined;
if (isChild) {
return (
<CSSTransition
appear={parent.appear || !parent.isInitialRender}
show={parent.show}
{...rest}
/>
);
}
return (
<TransitionContext.Provider
value={{
parent: {
show,
isInitialRender,
appear,
},
}}
>
<CSSTransition appear={appear} show={show} {...rest} />
</TransitionContext.Provider>
);
};
export default Transition;

@ -1,11 +1,16 @@
import React from 'react';
import '../styles/globals.css';
import App from 'next/app';
import Layout from '../components/Layout';
class CoreApp extends App {
public render(): JSX.Element {
const { Component, pageProps } = this.props;
return <Component {...pageProps} />;
return (
<Layout>
<Component {...pageProps} />
</Layout>
);
}
}

@ -0,0 +1,46 @@
import React from 'react';
import { NextPage } from 'next';
import Link from 'next/link';
import { Undefinable } from '../utils/typeHelpers';
interface ErrorProps {
statusCode?: number;
}
const getErrorMessage = (statusCode?: number) => {
switch (statusCode) {
case 404:
return 'Page not found.';
default:
return 'Something went wrong.';
}
};
const Error: NextPage<ErrorProps> = ({ statusCode }) => {
return (
<div className="flex items-center justify-center relative top-0 left-0 bottom-0 right-0 h-screen flex-col">
<div className="text-4xl">{statusCode ? statusCode : 'Oops'}</div>
<p>
{getErrorMessage(statusCode)}{' '}
<Link href="/">
<a>Go home</a>
</Link>
</p>
</div>
);
};
Error.getInitialProps = async ({ res, err }): Promise<ErrorProps> => {
// Apologies for how gross ternary is but this is just temporary. Honestly,
// blame the nextjs docs
let statusCode: Undefinable<number>;
if (!!res) {
statusCode = res.statusCode;
} else {
statusCode = err ? err.statusCode : undefined;
}
return { statusCode };
};
export default Error;

@ -978,7 +978,7 @@
dependencies:
regenerator-runtime "^0.13.4"
"@babel/runtime@^7.10.2", "@babel/runtime@^7.8.4", "@babel/runtime@^7.9.6":
"@babel/runtime@^7.10.2", "@babel/runtime@^7.5.5", "@babel/runtime@^7.8.4", "@babel/runtime@^7.8.7", "@babel/runtime@^7.9.6":
version "7.11.2"
resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.11.2.tgz#f549c13c754cc40b87644b9fa9f09a6a95fe0736"
integrity sha512-TeWkU52so0mPtDcaCTxNBI/IHiz0pZgr8VEFqXFtZWpYD08ZB6FaSwVAS8MKRQAP3bYKiVjwysOJgMFY28o6Tw==
@ -1331,7 +1331,14 @@
resolved "https://registry.yarnpkg.com/@types/range-parser/-/range-parser-1.2.3.tgz#7ee330ba7caafb98090bece86a5ee44115904c2c"
integrity sha512-ewFXqrQHlFsgc09MK5jP5iR7vumV/BYayNC6PgJO2LPe8vrnNFyjQjSppfEngITi0qvfKtzFvgKymGheFM9UOA==
"@types/react@^16.9.46":
"@types/react-transition-group@^4.4.0":
version "4.4.0"
resolved "https://registry.yarnpkg.com/@types/react-transition-group/-/react-transition-group-4.4.0.tgz#882839db465df1320e4753e6e9f70ca7e9b4d46d"
integrity sha512-/QfLHGpu+2fQOqQaXh8MG9q03bFENooTb/it4jr5kKaZlDQfWvjqWZg48AwzPVMBHlRuTRAY7hRHCEOXz5kV6w==
dependencies:
"@types/react" "*"
"@types/react@*", "@types/react@^16.9.46":
version "16.9.46"
resolved "https://registry.yarnpkg.com/@types/react/-/react-16.9.46.tgz#f0326cd7adceda74148baa9bff6e918632f5069e"
integrity sha512-dbHzO3aAq1lB3jRQuNpuZ/mnu+CdD3H0WVaaBQA8LTT3S33xhVBUj232T8M3tAhSWJs/D/UqORYUlJNl/8VQZg==
@ -3311,6 +3318,14 @@ doctrine@^3.0.0:
dependencies:
esutils "^2.0.2"
dom-helpers@^5.0.1:
version "5.2.0"
resolved "https://registry.yarnpkg.com/dom-helpers/-/dom-helpers-5.2.0.tgz#57fd054c5f8f34c52a3eeffdb7e7e93cd357d95b"
integrity sha512-Ru5o9+V8CpunKnz5LGgWXkmrH/20cGKwcHwS4m73zIvs54CN9epEmT/HLqFJW3kXpakAFkEdzgy1hzlJe3E4OQ==
dependencies:
"@babel/runtime" "^7.8.7"
csstype "^3.0.2"
dom-serializer@1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/dom-serializer/-/dom-serializer-1.0.1.tgz#79695eb49af3cd8abc8d93a73da382deb1ca0795"
@ -7140,6 +7155,16 @@ react-refresh@0.8.3:
resolved "https://registry.yarnpkg.com/react-refresh/-/react-refresh-0.8.3.tgz#721d4657672d400c5e3c75d063c4a85fb2d5d68f"
integrity sha512-X8jZHc7nCMjaCqoU+V2I0cOhNW+QMBwSUkeXnTi8IPe6zaRWfn60ZzvFDZqWPfmSJfjub7dDW1SP0jaHWLu/hg==
react-transition-group@^4.4.1:
version "4.4.1"
resolved "https://registry.yarnpkg.com/react-transition-group/-/react-transition-group-4.4.1.tgz#63868f9325a38ea5ee9535d828327f85773345c9"
integrity sha512-Djqr7OQ2aPUiYurhPalTrVy9ddmFCCzwhqQmtN+J3+3DzLO209Fdr70QrN8Z3DsglWql6iY1lDWAfpFiBtuKGw==
dependencies:
"@babel/runtime" "^7.5.5"
dom-helpers "^5.0.1"
loose-envify "^1.4.0"
prop-types "^15.6.2"
react@16.13.1:
version "16.13.1"
resolved "https://registry.yarnpkg.com/react/-/react-16.13.1.tgz#2e818822f1a9743122c063d6410d85c1e3afe48e"

Loading…
Cancel
Save