Skip to content

Commit 9779cf9

Browse files
no message
0 parents  commit 9779cf9

30 files changed

+12186
-0
lines changed

.gitignore

+6
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
node_modules
2+
/.cache
3+
/build
4+
/public/build
5+
/app/styles
6+
*.log

.prettierignore

+10
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
2+
/.vscode
3+
/.github
4+
/node_modules
5+
/.cache
6+
/prisma/migrations
7+
/prisma/schema.prisma
8+
/build
9+
/public/build
10+
/app/styles

.vscode/launch.json

+17
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
{
2+
"configurations": [
3+
{
4+
"name": "Run npm run dev",
5+
"command": "npm run dev",
6+
"request": "launch",
7+
"type": "node-terminal",
8+
"cwd": "${workspaceFolder}",
9+
"console": "internalConsole",
10+
// "serverReadyAction": {
11+
// "pattern": "started at http://localhost:([0-9]+)",
12+
// "uriFormat": "http://localhost:%s",
13+
// "action": "openExternally"
14+
// }
15+
}
16+
]
17+
}

.vscode/tasks.json

+35
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
{
2+
"version": "2.0.0",
3+
"tasks": [
4+
{
5+
"label": "Run dev",
6+
"type": "shell",
7+
"command": "npm run dev",
8+
"presentation": {
9+
"reveal": "always",
10+
"panel": "new",
11+
"group": "develop",
12+
},
13+
"runOptions": { "runOn": "folderOpen" }
14+
},
15+
{
16+
"label": "Run css",
17+
"type": "shell",
18+
"command": "npm run watch:css",
19+
"presentation": {
20+
"reveal": "always",
21+
"panel": "new",
22+
"group": "develop",
23+
},
24+
"runOptions": { "runOn": "folderOpen" }
25+
},
26+
{
27+
"label": "Start Dev",
28+
"dependsOn": [
29+
"Run dev",
30+
"Run css",
31+
],
32+
"problemMatcher": []
33+
}
34+
]
35+
}

README.md

Whitespace-only changes.

app/components/Dropdown/Dropdown.tsx

+73
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
import clsx from 'clsx';
2+
import { Fragment } from 'react';
3+
import { Menu, Transition } from '@headlessui/react';
4+
import { ChevronDownIcon } from '@heroicons/react/outline';
5+
6+
interface DropdownProps<T> {
7+
items: T[];
8+
itemKey: string;
9+
buttonClass?: string;
10+
buttonChild: JSX.Element | string;
11+
dropdownClass?: string;
12+
defaultItem?: (active: boolean) => JSX.Element;
13+
generateItem: (active: boolean, item: T) => JSX.Element;
14+
defaultBottom?: boolean;
15+
isRight?: boolean;
16+
isTop?: boolean;
17+
}
18+
19+
const Dropdown = <T,>({
20+
itemKey,
21+
items,
22+
defaultItem,
23+
defaultBottom,
24+
generateItem,
25+
buttonClass,
26+
buttonChild,
27+
isRight,
28+
isTop,
29+
dropdownClass,
30+
}: DropdownProps<T>) => (
31+
<Menu as="div" className="relative inline-block text-left">
32+
<div>
33+
<Menu.Button
34+
className={clsx(
35+
'inline-flex justify-center items-center w-full rounded-md border border-gray-300 shadow-sm px-4 py-2 bg-white text-sm font-medium text-gray-700 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-offset-gray-100 focus:ring-brand-500',
36+
buttonClass,
37+
)}
38+
>
39+
{buttonChild}
40+
<ChevronDownIcon className="-mr-1 ml-2 h-5 w-5" aria-hidden="true" />
41+
</Menu.Button>
42+
</div>
43+
44+
<Transition
45+
as={Fragment}
46+
enter="transition ease-out duration-100"
47+
enterFrom="transform opacity-0 scale-95"
48+
enterTo="transform opacity-100 scale-100"
49+
leave="transition ease-in duration-75"
50+
leaveFrom="transform opacity-100 scale-100"
51+
leaveTo="transform opacity-0 scale-95"
52+
>
53+
<Menu.Items
54+
className={clsx(
55+
isRight ? 'origin-top-right right-0' : 'origin-top-left left-0',
56+
isTop ? 'bottom-[100%]' : '',
57+
'absolute z-10 mt-2 w-56 rounded-md shadow-lg bg-white ring-1 ring-black ring-opacity-5 focus:outline-none',
58+
dropdownClass,
59+
)}
60+
>
61+
<div className="py-1">
62+
{defaultItem && !defaultBottom && <Menu.Item>{({ active }) => defaultItem(active)}</Menu.Item>}
63+
{items.map((item, index) => (
64+
<Menu.Item key={`${itemKey}-${index}`}>{({ active }) => generateItem(active, item)}</Menu.Item>
65+
))}
66+
{defaultItem && !!defaultBottom && <Menu.Item>{({ active }) => defaultItem(active)}</Menu.Item>}
67+
</div>
68+
</Menu.Items>
69+
</Transition>
70+
</Menu>
71+
);
72+
73+
export default Dropdown;

app/components/Dropdown/index.tsx

+2
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
export { default } from './Dropdown';
2+
export * from './Dropdown';

app/components/Input/Input.tsx

+86
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
import { ExclamationCircleIcon } from '@heroicons/react/solid';
2+
import clsx from 'clsx';
3+
import React, { HTMLProps } from 'react';
4+
5+
interface InputProps extends HTMLProps<HTMLInputElement> {
6+
label?: string;
7+
name: string;
8+
error?: string;
9+
addOn?: string;
10+
leadingIcon?: CustomSVG;
11+
trailingIcon?: CustomSVG;
12+
}
13+
14+
const Input: React.FC<InputProps> = ({
15+
children,
16+
error,
17+
leadingIcon: LIcon,
18+
trailingIcon: TIcon,
19+
addOn,
20+
label,
21+
type = 'text',
22+
name,
23+
id,
24+
placeholder,
25+
className,
26+
...props
27+
}) => {
28+
return (
29+
<div className={clsx('relative font-medium text-grey-4', className)}>
30+
<div>
31+
{label && (
32+
<label htmlFor={name} className="mb-1 block text-sm font-medium text-gray-700">
33+
{label}
34+
</label>
35+
)}
36+
<div className="relative rounded-md shadow-sm">
37+
{!!LIcon && (
38+
<div className="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
39+
<LIcon className="h-5 w-5 text-gray-400" aria-hidden="true" />
40+
</div>
41+
)}
42+
<div className="flex">
43+
{!!addOn && (
44+
<span className="inline-flex items-center px-3 rounded-l-md border border-r-0 border-gray-300 bg-gray-50 text-gray-500 sm:text-sm">
45+
{addOn}
46+
</span>
47+
)}
48+
<input
49+
placeholder={placeholder || label}
50+
name={name}
51+
id={id || name}
52+
aria-invalid={!!error}
53+
aria-describedby={`${name}-error`}
54+
type={type}
55+
{...props}
56+
className={clsx(
57+
'focus:ring-brand-500 focus:border-brand-500 block w-full sm:text-sm border-gray-300 placeholder-gray-400',
58+
!!addOn ? 'rounded-none rounded-r-md' : 'rounded-md',
59+
!!LIcon && 'pl-10',
60+
(!!TIcon || !!error) && 'pr-10',
61+
)}
62+
/>
63+
</div>
64+
65+
{!!TIcon && !error && (
66+
<div className="absolute inset-y-0 right-0 pr-3 flex items-center pointer-events-none">
67+
<TIcon className="h-5 w-5 text-gray-400" aria-hidden="true" />
68+
</div>
69+
)}
70+
{!!error && (
71+
<div className="absolute inset-y-0 right-0 pr-3 flex items-center pointer-events-none">
72+
<ExclamationCircleIcon className="h-5 w-5 text-red-500" aria-hidden="true" />
73+
</div>
74+
)}
75+
</div>
76+
</div>
77+
{error && (
78+
<span id={`${name}-error`} className="absolute left-0 -bottom-5 text-xs text-red-600">
79+
{error}
80+
</span>
81+
)}
82+
</div>
83+
);
84+
};
85+
86+
export default Input;

app/components/Input/index.tsx

+2
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
export { default } from './Input';
2+
export * from './Input';
+96
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
import { ArrowNarrowLeftIcon, ArrowNarrowRightIcon } from '@heroicons/react/outline';
2+
import clsx from 'clsx';
3+
import React from 'react';
4+
import { Link } from 'remix';
5+
6+
interface PaginationProps {
7+
total: number;
8+
itemsPerPage: number;
9+
currentPage: number;
10+
getLink: (page: number) => string;
11+
}
12+
const Pagination: React.FC<PaginationProps> = ({ total, itemsPerPage, currentPage, getLink }) => {
13+
const pageCount = Math.ceil(total / itemsPerPage);
14+
15+
const getButtons = () => {
16+
let buttons: JSX.Element[] = [];
17+
18+
for (let i = 1; i <= pageCount; i++) {
19+
const pageButton = (
20+
<Link
21+
key={`pagination-${i}`}
22+
to={getLink(i)}
23+
className={clsx(
24+
'border-transparent border-t-2 pt-4 px-4 inline-flex items-center text-sm font-medium',
25+
i === currentPage ? 'border-brand-500 text-brand-600' : 'text-gray-500 hover:text-gray-700 hover:border-gray-300',
26+
)}
27+
aria-current="page"
28+
>
29+
{i}
30+
</Link>
31+
);
32+
33+
const divider = (
34+
<span
35+
key={`pagination-divider-${i}`}
36+
className="border-transparent text-gray-500 border-t-2 pt-4 px-4 inline-flex items-center text-sm font-medium"
37+
>
38+
...
39+
</span>
40+
);
41+
if (pageCount > 8) {
42+
//Always have a divider after page 1
43+
if (i === 1) {
44+
buttons.push(pageButton);
45+
buttons.push(divider);
46+
//Always have a divider before the last page
47+
} else if (i === pageCount) {
48+
buttons.push(divider);
49+
buttons.push(pageButton);
50+
} else if (
51+
i === currentPage ||
52+
i === currentPage - 1 ||
53+
i === currentPage + 1 ||
54+
(currentPage < 3 && i < 5) ||
55+
(currentPage >= pageCount - 2 && i > pageCount - 4)
56+
) {
57+
buttons.push(pageButton);
58+
}
59+
} else {
60+
buttons.push(pageButton);
61+
}
62+
}
63+
64+
return buttons;
65+
};
66+
67+
return pageCount > 1 ? (
68+
<nav className="border-t border-gray-200 my-4 px-4 flex items-center justify-between">
69+
<div className="-mt-px w-0 flex-1 flex">
70+
{currentPage !== 1 && (
71+
<Link
72+
to={getLink(currentPage - 1)}
73+
className="border-t-2 border-transparent pt-4 pr-1 inline-flex items-center text-sm font-medium text-gray-500 hover:text-gray-700 hover:border-gray-300"
74+
>
75+
<ArrowNarrowLeftIcon className="mr-3 h-5 w-5 text-gray-400" aria-hidden="true" />
76+
Previous
77+
</Link>
78+
)}
79+
</div>
80+
<div className="">{getButtons()}</div>
81+
<div className="-mt-px w-0 flex-1 flex justify-end">
82+
{currentPage !== pageCount && (
83+
<Link
84+
to={getLink(currentPage + 1)}
85+
className="border-t-2 border-transparent pt-4 pl-1 inline-flex items-center text-sm font-medium text-gray-500 hover:text-gray-700 hover:border-gray-300"
86+
>
87+
Next
88+
<ArrowNarrowRightIcon className="ml-3 h-5 w-5 text-gray-400" aria-hidden="true" />
89+
</Link>
90+
)}
91+
</div>
92+
</nav>
93+
) : null;
94+
};
95+
96+
export default Pagination;

app/components/Pagination/index.tsx

+2
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
export { default } from './Pagination';
2+
export * from './Pagination';
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
import { CalendarIcon } from '@heroicons/react/outline';
2+
import clsx from 'clsx';
3+
import React from 'react';
4+
import { Link } from 'remix';
5+
import { getNewTableUrl, ItemTableParams } from '~/utils/itemTable';
6+
import Dropdown from '../Dropdown';
7+
8+
interface AgeDropdownProps {
9+
tableParams: ItemTableParams;
10+
ages: Array<{ value: number; label: string }>;
11+
baseUrl: string;
12+
}
13+
14+
const AgeDropdown: React.FC<AgeDropdownProps> = ({ tableParams, ages, baseUrl }) => {
15+
const getLink = (active: boolean, label: string, value?: string) => (
16+
<Link
17+
to={getNewTableUrl(baseUrl, tableParams, 'age', value)}
18+
className={clsx(active || value === tableParams.age ? 'bg-gray-100 text-gray-900' : 'text-gray-700', 'block px-4 py-2 text-sm')}
19+
>
20+
{label}
21+
</Link>
22+
);
23+
24+
return (
25+
<Dropdown
26+
items={ages}
27+
itemKey="ages"
28+
buttonChild={
29+
<>
30+
<CalendarIcon className="w-4 mr-2" />
31+
{ages.find(age => age.value === tableParams.age)?.label || 'All time'}
32+
</>
33+
}
34+
defaultBottom
35+
isRight
36+
defaultItem={() => getLink(!tableParams.age, 'All time')}
37+
generateItem={(active, { label, value }) => getLink(active || value === tableParams.age, label, value.toString())}
38+
/>
39+
);
40+
};
41+
export default AgeDropdown;

0 commit comments

Comments
 (0)