Initial commit

This commit is contained in:
Luka Radenovic 2021-09-27 12:17:33 +02:00
commit fa30c04815
117 changed files with 33513 additions and 0 deletions

View file

@ -0,0 +1,134 @@
import React, { Fragment } from 'react';
import { Disclosure, Menu, Transition } from '@headlessui/react';
import { MenuIcon, XIcon } from '@heroicons/react/outline';
import { Link, RouteComponentProps } from '@reach/router';
import { useAuth } from 'src/services/auth';
const navigation = [
{ name: 'Dashboard', to: '/dashboard' },
{ name: 'Apps', to: '/apps' },
{ name: 'Users', to: '/users' },
];
function classNames(...classes: any[]) {
return classes.filter(Boolean).join(' ');
}
type HeaderProps = RouteComponentProps;
const Header: React.FC<HeaderProps> = () => {
const { logOut } = useAuth();
return (
<Disclosure as="nav" className="bg-white shadow">
{({ open }) => (
<>
<div className="max-w-7xl mx-auto px-2 sm:px-6 lg:px-8">
<div className="relative flex justify-between h-16">
<div className="absolute inset-y-0 left-0 flex items-center sm:hidden">
{/* Mobile menu button */}
<Disclosure.Button className="inline-flex items-center justify-center p-2 rounded-md text-gray-400 hover:text-gray-500 hover:bg-gray-100 focus:outline-none focus:ring-2 focus:ring-inset focus:ring-primary-500">
<span className="sr-only">Open main menu</span>
{open ? (
<XIcon className="block h-6 w-6" aria-hidden="true" />
) : (
<MenuIcon className="block h-6 w-6" aria-hidden="true" />
)}
</Disclosure.Button>
</div>
<div className="flex-1 flex items-center justify-center sm:items-stretch sm:justify-start">
<div className="flex-shrink-0 flex items-center">
<img className="block lg:hidden" src="/assets/logo-small.svg" alt="Stackspin" />
<img className="hidden lg:block" src="/assets/logo.svg" alt="Stackspin" />
</div>
<div className="hidden sm:ml-6 sm:flex sm:space-x-8">
{/* Current: "border-primary-500 text-gray-900", Default: "border-transparent text-gray-500 hover:border-gray-300 hover:text-gray-700" */}
{navigation.map((item) => (
<Link
key={item.name}
to={item.to}
getProps={({ isCurrent }) => {
// the object returned here is passed to the
// anchor element's props
return {
className: isCurrent
? 'border-primary-400 text-gray-900 inline-flex items-center px-1 pt-1 border-b-2 text-sm font-medium'
: 'border-transparent text-gray-500 hover:border-gray-300 hover:text-gray-700 inline-flex items-center px-1 pt-1 border-b-2 text-sm font-medium',
'aria-current': isCurrent ? 'page' : undefined,
};
}}
>
{item.name}
</Link>
))}
</div>
</div>
<div className="absolute inset-y-0 right-0 flex items-center pr-2 sm:static sm:inset-auto sm:ml-6 sm:pr-0">
{/* Profile dropdown */}
<Menu as="div" className="ml-3 relative">
<div>
<Menu.Button className="bg-white rounded-full flex text-sm focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary">
<span className="sr-only">Open user menu</span>
<img
className="h-8 w-8 rounded-full"
src="https://images.unsplash.com/photo-1472099645785-5658abf4ff4e?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=facearea&facepad=2&w=256&h=256&q=80"
alt=""
/>
</Menu.Button>
</div>
<Transition
as={Fragment}
enter="transition ease-out duration-200"
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"
>
<Menu.Items className="origin-top-right absolute right-0 mt-2 w-48 rounded-md shadow-lg py-1 bg-white ring-1 ring-black ring-opacity-5 focus:outline-none">
<Menu.Item>
{({ active }) => (
<a
onClick={() => logOut()}
className={classNames(active ? 'bg-gray-100' : '', 'block px-4 py-2 text-sm text-gray-700')}
>
Sign out
</a>
)}
</Menu.Item>
</Menu.Items>
</Transition>
</Menu>
</div>
</div>
</div>
<Disclosure.Panel className="sm:hidden">
<div className="pt-2 pb-4 space-y-1">
{navigation.map((item) => (
<Link
key={item.name}
to={item.to}
getProps={({ isCurrent }) => {
// the object returned here is passed to the
// anchor element's props
return {
className: isCurrent
? 'bg-primary-50 border-primary-400 text-primary-700 block pl-3 pr-4 py-2 border-l-4 text-base font-medium'
: 'border-transparent text-gray-500 hover:bg-gray-50 hover:border-gray-300 hover:text-gray-700 block pl-3 pr-4 py-2 border-l-4 text-base font-medium',
'aria-current': isCurrent ? 'page' : undefined,
};
}}
>
{item.name}
</Link>
))}
</div>
</Disclosure.Panel>
</>
)}
</Disclosure>
);
};
export default Header;

View file

@ -0,0 +1 @@
export { default as Header } from './Header';

View file

@ -0,0 +1,17 @@
import React from 'react';
import { RouteComponentProps } from '@reach/router';
import { Header } from '../Header';
type DashboardProps = RouteComponentProps;
const Layout: React.FC<DashboardProps> = ({ children }) => {
return (
<>
<Header />
{children}
</>
);
};
export default Layout;

View file

@ -0,0 +1 @@
export { default as Layout } from './Layout';

View file

@ -0,0 +1,89 @@
import React, { Fragment, useRef } from 'react';
import { Dialog, Transition } from '@headlessui/react';
import { ExclamationIcon } from '@heroicons/react/outline';
type ConfirmationModalProps = {
open: boolean;
onClose: () => void;
title: string;
body: string;
};
export const ConfirmationModal = ({ open, onClose, title, body }: ConfirmationModalProps) => {
const cancelButtonRef = useRef(null);
return (
<Transition.Root show={open} as={Fragment}>
<Dialog
as="div"
auto-reopen="true"
className="fixed z-10 inset-0 overflow-y-auto"
initialFocus={cancelButtonRef}
onClose={onClose}
>
<div className="flex items-end justify-center min-h-screen pt-4 px-4 pb-20 text-center sm:block sm:p-0">
<Transition.Child
as={Fragment}
enter="ease-out duration-300"
enterFrom="opacity-0"
enterTo="opacity-100"
leave="ease-in duration-200"
leaveFrom="opacity-100"
leaveTo="opacity-0"
>
<Dialog.Overlay className="fixed inset-0 bg-gray-500 bg-opacity-75 transition-opacity" />
</Transition.Child>
{/* This element is to trick the browser into centering the modal contents. */}
<span className="hidden sm:inline-block sm:align-middle sm:h-screen" aria-hidden="true">
&#8203;
</span>
<Transition.Child
as={Fragment}
enter="ease-out duration-300"
enterFrom="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
enterTo="opacity-100 translate-y-0 sm:scale-100"
leave="ease-in duration-200"
leaveFrom="opacity-100 translate-y-0 sm:scale-100"
leaveTo="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
>
<div className="inline-block align-bottom bg-white rounded-lg text-left overflow-hidden shadow-xl transform transition-all sm:my-8 sm:align-middle sm:max-w-lg sm:w-full">
<div className="bg-white px-4 pt-5 pb-4 sm:p-6 sm:pb-4">
<div className="sm:flex sm:items-start">
<div className="mx-auto flex-shrink-0 flex items-center justify-center h-12 w-12 rounded-full bg-red-100 sm:mx-0 sm:h-10 sm:w-10">
<ExclamationIcon className="h-6 w-6 text-red-600" aria-hidden="true" />
</div>
<div className="mt-3 text-center sm:mt-0 sm:ml-4 sm:text-left">
<Dialog.Title as="h3" className="text-lg leading-6 font-medium text-gray-900">
{title}
</Dialog.Title>
<div className="mt-2">
<p className="text-sm text-gray-500">{body}</p>
</div>
</div>
</div>
</div>
<div className="bg-gray-50 px-4 py-3 sm:px-6 sm:flex sm:flex-row-reverse">
<button
type="button"
className="w-full inline-flex justify-center rounded-md border border-transparent shadow-sm px-4 py-2 bg-red-600 text-base font-medium text-white hover:bg-red-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-red-500 sm:ml-3 sm:w-auto sm:text-sm"
onClick={onClose}
>
Delete
</button>
<button
type="button"
className="mt-3 w-full inline-flex justify-center rounded-md border border-gray-300 shadow-sm px-4 py-2 bg-white text-base font-medium text-gray-700 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary sm:mt-0 sm:ml-3 sm:w-auto sm:text-sm"
onClick={onClose}
ref={cancelButtonRef}
>
Cancel
</button>
</div>
</div>
</Transition.Child>
</div>
</Dialog>
</Transition.Root>
);
};

View file

@ -0,0 +1 @@
export { ConfirmationModal } from './ConfirmationModal';

View file

@ -0,0 +1 @@
export { ConfirmationModal } from './ConfirmationModal';

View file

@ -0,0 +1,178 @@
import { ArrowSmDownIcon, ArrowSmUpIcon } from '@heroicons/react/solid';
import React, { useEffect } from 'react';
import { useTable, useRowSelect, Column, IdType, useSortBy } from 'react-table';
export interface ReactTableProps<T extends Record<string, unknown>> {
columns: Column<T>[];
data: T[];
onRowClick?(row: T): void;
pagination?: boolean;
getSelectedRowIds?(rows: Record<IdType<T>, boolean>): void;
selectable?: boolean;
}
const IndeterminateCheckbox = React.forwardRef(({ indeterminate, ...rest }: any, ref) => {
const defaultRef = React.useRef(null);
const resolvedRef: any = ref || defaultRef;
React.useEffect(() => {
resolvedRef.current.indeterminate = indeterminate;
}, [resolvedRef, indeterminate]);
return (
<>
<input
type="checkbox"
ref={resolvedRef}
{...rest}
className="focus:ring-primary-800 h-4 w-4 text-primary-700 border-gray-300 rounded"
/>
</>
);
});
export const Table = <T extends Record<string, unknown>>({
columns,
data,
pagination = false,
onRowClick,
getSelectedRowIds,
selectable = false,
}: ReactTableProps<T>) => {
const {
getTableProps,
getTableBodyProps,
headerGroups,
rows,
prepareRow,
pageCount,
state: { selectedRowIds },
} = useTable(
{
columns,
data,
},
useSortBy,
useRowSelect,
selectable
? (hooks) => {
hooks.visibleColumns.push((columns2) => [
{
id: 'selection',
Header: ({ getToggleAllRowsSelectedProps }: { getToggleAllRowsSelectedProps: any }) => (
<div>
<IndeterminateCheckbox {...getToggleAllRowsSelectedProps()} />
</div>
),
Cell: ({ row }: { row: any }) => (
<div>
<IndeterminateCheckbox {...row.getToggleRowSelectedProps()} />
</div>
),
width: 16,
},
...columns2,
]);
}
: () => {},
);
useEffect(() => {
if (selectedRowIds && getSelectedRowIds) {
getSelectedRowIds(selectedRowIds);
}
}, [selectedRowIds, getSelectedRowIds]);
return (
<>
<table className="min-w-full divide-y divide-gray-200 table-auto" {...getTableProps()}>
<thead className="bg-gray-50">
{headerGroups.map((headerGroup: any, index: any) => (
<tr {...headerGroup.getHeaderGroupProps()} key={index!}>
{headerGroup.headers.map((column: any) => (
<th
key={column}
{...column.getHeaderProps([
{
style: {
width: column.width ? column.width : 'auto !important',
},
},
column.getSortByToggleProps(),
])}
scope="col"
className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"
>
<div className="flex items-center">
<span>{column.render('Header')}</span>
{(column as any).isSorted ? (
(column as any).isSortedDesc ? (
<ArrowSmDownIcon className="w-4 h-4 text-gray-400 ml-1" />
) : (
<ArrowSmUpIcon className="w-4 h-4 text-gray-400 ml-1" />
)
) : (
''
)}
</div>
</th>
))}
</tr>
))}
</thead>
<tbody {...getTableBodyProps()}>
{rows.map((row: any, rowIndex) => {
prepareRow(row);
return (
<tr
key={row}
{...row.getRowProps()}
className={rowIndex % 2 === 0 ? 'bg-white group' : 'bg-gray-50 group'}
onClick={onRowClick ? () => onRowClick(row.original as T) : () => {}}
>
{row.cells.map((cell: any) => {
return (
<td
key={cell}
{...cell.getCellProps()}
className="px-6 py-4 whitespace-nowrap text-sm text-gray-500"
>
{cell.render('Cell')}
</td>
);
})}
</tr>
);
})}
</tbody>
</table>
{pagination && pageCount > 1 && (
<div className="bg-white px-4 py-3 flex items-center justify-between border-t border-gray-100 sm:px-6">
<div className="flex-1 flex justify-between sm:hidden">
<a
href="#"
className="relative inline-flex items-center px-4 py-2 border border-gray-300 text-sm font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50"
>
Previous
</a>
<a
href="#"
className="ml-3 relative inline-flex items-center px-4 py-2 border border-gray-300 text-sm font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50"
>
Next
</a>
</div>
<div className="hidden sm:flex-1 sm:flex sm:items-center sm:justify-between">
<div>
<p className="text-sm text-gray-700">
Showing <span className="font-medium">1</span> to <span className="font-medium">3</span> of{' '}
<span className="font-medium">3</span> results
</p>
</div>
</div>
</div>
)}
</>
);
};

View file

@ -0,0 +1 @@
export { Table } from './Table';

View file

@ -0,0 +1,8 @@
import React from 'react';
import { TabPanelProps } from './types';
export const TabPanel = ({ children, isActive }: TabPanelProps) => {
if (!isActive) return null;
return <div className="pt-8 pb-4">{children}</div>;
};

View file

@ -0,0 +1,64 @@
import React, { useState } from 'react';
import { TabPanel } from './TabPanel';
import { TabsProps } from './types';
export const Tabs = ({ tabs }: TabsProps) => {
const [activeTabIndex, setActiveTabIndex] = useState<number>(0);
const handleTabPress = (index: number) => () => {
setActiveTabIndex(index);
};
function classNames(...classes: any) {
return classes.filter(Boolean).join(' ');
}
return (
<>
<div className="sm:hidden">
<label htmlFor="tabs" className="sr-only">
Select a tab
</label>
<select
id="tabs"
name="tabs"
className="block w-full focus:ring-primary-500 focus:border-primary-500 border-gray-300 rounded-md"
// defaultValue={tabs ? tabs.find((tab) => tab.current).name : undefined}
>
{tabs.map((tab) => (
<option key={tab.name}>{tab.name}</option>
))}
</select>
</div>
<div className="hidden sm:block">
<nav className="flex space-x-4" aria-label="Tabs">
{tabs.map((tab, tabIndex) => (
<a
onClick={handleTabPress(tabIndex)}
key={tab.name}
className={classNames(
activeTabIndex === tabIndex
? 'bg-gray-100 text-gray-700'
: 'text-gray-500 hover:text-gray-700 hover:bg-gray-50',
'px-3 py-2 font-medium text-sm rounded-md cursor-pointer',
)}
aria-current={activeTabIndex === tabIndex ? 'page' : undefined}
>
{tab.name}
</a>
))}
</nav>
</div>
{tabs.map(({ component, name, disabled }, index) =>
disabled ? (
<React.Fragment key={name} />
) : (
<TabPanel key={name} isActive={activeTabIndex === index}>
{component}
</TabPanel>
),
)}
</>
);
};

View file

@ -0,0 +1 @@
export { Tabs } from './Tabs';

View file

@ -0,0 +1,21 @@
type Tab = {
disabled?: boolean;
name: string;
component: JSX.Element;
};
export interface TabsProps {
tabs: Tab[];
}
export interface TabPanelProps {
isActive: boolean;
children: JSX.Element | JSX.Element[];
}
export interface TabProps {
title: string;
isActive: boolean;
onPress(): void;
disabled: boolean;
}

4
src/components/index.ts Normal file
View file

@ -0,0 +1,4 @@
export { Layout } from './Layout';
export { Header } from './Header';
export { Table } from './Table';
export { Tabs } from './Tabs';