refactor: remove all legacy files

+ comment/delete things that haven't yet migrated to mantine
This commit is contained in:
Sonny
2024-11-07 00:29:58 +01:00
committed by Sonny
parent 861906d29b
commit 5c37fe9c31
148 changed files with 469 additions and 4728 deletions

View File

@@ -1,64 +0,0 @@
import styled from '@emotion/styled';
import { HtmlHTMLAttributes, ReactNode, useRef } from 'react';
import DropdownContainer from '~/components/common/dropdown/dropdown_container';
import DropdownLabel from '~/components/common/dropdown/dropdown_label';
import useClickOutside from '~/hooks/use_click_outside';
import useToggle from '~/hooks/use_modal';
import useShortcut from '~/hooks/use_shortcut';
const DropdownStyle = styled.div<{ opened: boolean; svgSize?: number }>(
({ opened, theme, svgSize = 24 }) => ({
cursor: 'pointer',
userSelect: 'none',
position: 'relative',
minWidth: 'fit-content',
width: 'fit-content',
maxWidth: '250px',
backgroundColor: opened ? theme.colors.secondary : theme.colors.background,
padding: '4px',
borderRadius: theme.border.radius,
'&:hover': {
backgroundColor: theme.colors.secondary,
},
'& svg': {
height: `${svgSize}px`,
width: `${svgSize}px`,
},
})
);
export default function Dropdown({
children,
label,
className,
svgSize,
onClick,
}: HtmlHTMLAttributes<HTMLDivElement> & {
label: ReactNode | string;
className?: string;
svgSize?: number;
}) {
const dropdownRef = useRef<HTMLDivElement>(null);
const { isShowing, toggle, close } = useToggle();
useClickOutside(dropdownRef, close);
useShortcut('ESCAPE_KEY', close, { disableGlobalCheck: true });
return (
<DropdownStyle
opened={isShowing}
onClick={(event) => {
onClick?.(event);
toggle();
}}
ref={dropdownRef}
className={className}
svgSize={svgSize}
>
<DropdownLabel>{label}</DropdownLabel>
<DropdownContainer show={isShowing}>{children}</DropdownContainer>
</DropdownStyle>
);
}

View File

@@ -1,21 +0,0 @@
import styled from '@emotion/styled';
import TransitionLayout from '~/components/layouts/_transition_layout';
const DropdownContainer = styled(TransitionLayout)<{ show: boolean }>(
({ show, theme }) => ({
zIndex: 99,
position: 'absolute',
top: 'calc(100% + 0.5em)',
right: 0,
minWidth: '175px',
backgroundColor: show ? theme.colors.secondary : theme.colors.background,
border: `2px solid ${theme.colors.secondary}`,
borderRadius: theme.border.radius,
boxShadow: theme.colors.boxShadow,
display: show ? 'flex' : 'none',
flexDirection: 'column',
overflow: 'hidden',
})
);
export default DropdownContainer;

View File

@@ -1,31 +0,0 @@
import styled from '@emotion/styled';
import { Link } from '@inertiajs/react';
const DropdownItemBase = styled('div', {
shouldForwardProp: (propName) => propName !== 'danger',
})<{ danger?: boolean }>(({ theme, danger }) => ({
fontSize: '14px',
whiteSpace: 'nowrap',
color: danger ? theme.colors.lightRed : theme.colors.primary,
padding: '8px 12px',
borderRadius: theme.border.radius,
'&:hover': {
backgroundColor: theme.colors.background,
},
}));
const DropdownItemButton = styled(DropdownItemBase)({
display: 'flex',
gap: '0.75em',
alignItems: 'center',
});
const DropdownItemLink = styled(DropdownItemBase.withComponent(Link))({
width: '100%',
display: 'flex',
gap: '0.75em',
alignItems: 'center',
});
export { DropdownItemButton, DropdownItemLink };

View File

@@ -1,11 +0,0 @@
import styled from '@emotion/styled';
const DropdownLabel = styled.div(({ theme }) => ({
height: 'auto',
width: 'auto',
color: theme.colors.font,
display: 'flex',
gap: '0.35em',
}));
export default DropdownLabel;

View File

@@ -1,31 +0,0 @@
import styled from '@emotion/styled';
const Button = styled.button<{ danger?: boolean }>(({ theme, danger }) => {
const btnColor = !danger ? theme.colors.primary : theme.colors.lightRed;
const btnDarkColor = !danger ? theme.colors.darkBlue : theme.colors.lightRed;
return {
cursor: 'pointer',
width: '100%',
textTransform: 'uppercase',
fontSize: '14px',
color: theme.colors.white,
background: btnColor,
padding: '0.75em',
border: `1px solid ${btnColor}`,
borderRadius: theme.border.radius,
transition: theme.transition.delay,
'&:disabled': {
cursor: 'not-allowed',
opacity: '0.5',
},
'&:not(:disabled):hover': {
boxShadow: `${btnDarkColor} 0 0 3px 1px`,
background: btnDarkColor,
color: theme.colors.white,
},
};
});
export default Button;

View File

@@ -1,10 +0,0 @@
import styled from '@emotion/styled';
const Form = styled.form({
width: '100%',
display: 'flex',
gap: '1em',
flexDirection: 'column',
});
export default Form;

View File

@@ -1,25 +0,0 @@
import styled from '@emotion/styled';
const FormField = styled('div', {
shouldForwardProp: (propName) => propName !== 'required',
})<{ required?: boolean }>(({ required, theme }) => ({
display: 'flex',
gap: '0.25em',
flexDirection: 'column',
'& label': {
position: 'relative',
userSelect: 'none',
width: 'fit-content',
},
'& label::after': {
position: 'absolute',
top: 0,
right: '-0.75em',
color: theme.colors.lightRed,
content: (required ? '"*"' : '""') as any,
},
}));
export default FormField;

View File

@@ -1,9 +0,0 @@
import styled from '@emotion/styled';
// TODO: create a global style variable (fontSize)
const FormFieldError = styled.p(({ theme }) => ({
fontSize: '12px',
color: theme.colors.lightRed,
}));
export default FormFieldError;

View File

@@ -1,27 +0,0 @@
import styled from '@emotion/styled';
const Input = styled.input(({ theme }) => ({
width: '100%',
color: theme.colors.font,
backgroundColor: theme.colors.secondary,
padding: '0.75em',
border: `1px solid ${theme.colors.lightGrey}`,
borderBottom: `2px solid ${theme.colors.lightGrey}`,
borderRadius: theme.border.radius,
transition: theme.transition.delay,
'&:focus': {
borderBottom: `2px solid ${theme.colors.primary}`,
},
'&:disabled': {
opacity: 0.85,
},
'&::placeholder': {
fontStyle: 'italic',
color: theme.colors.grey,
},
}));
export default Input;

View File

@@ -1,55 +0,0 @@
import { ChangeEvent, Fragment, InputHTMLAttributes, useState } from 'react';
import Toggle from 'react-toggle';
import FormField from '~/components/common/form/_form_field';
import FormFieldError from '~/components/common/form/_form_field_error';
interface InputProps
extends Omit<InputHTMLAttributes<HTMLInputElement>, 'onChange'> {
label: string;
name: string;
checked: boolean;
errors?: string[];
onChange?: (name: string, checked: boolean) => void;
}
export default function Checkbox({
name,
label,
checked = false,
errors = [],
onChange,
required = false,
...props
}: InputProps): JSX.Element {
const [checkboxChecked, setCheckboxChecked] = useState<boolean>(checked);
if (typeof window === 'undefined') return <Fragment />;
function _onChange({ target }: ChangeEvent<HTMLInputElement>) {
setCheckboxChecked(target.checked);
if (onChange) {
onChange(target.name, target.checked);
}
}
return (
<FormField
css={{ alignItems: 'center', gap: '1em', flexDirection: 'row' }}
required={required}
>
<label htmlFor={name} title={label}>
{label}
</label>
<Toggle
{...props}
onChange={_onChange}
checked={checkboxChecked}
placeholder={props.placeholder ?? 'Type something...'}
name={name}
id={name}
/>
{errors.length > 0 &&
errors.map((error) => <FormFieldError>{error}</FormFieldError>)}
</FormField>
);
}

View File

@@ -1,79 +0,0 @@
import { useTheme } from '@emotion/react';
import { InputHTMLAttributes, ReactNode, useEffect, useState } from 'react';
import Select, {
FormatOptionLabelMeta,
GroupBase,
OptionsOrGroups,
} from 'react-select';
import FormField from '~/components/common/form/_form_field';
type Option = { label: string | number; value: string | number };
interface SelectorProps
extends Omit<InputHTMLAttributes<HTMLInputElement>, 'onChange'> {
label: string;
name: string;
errors?: string[];
options: OptionsOrGroups<Option, GroupBase<Option>>;
value: number | string;
onChangeCallback?: (value: number | string) => void;
formatOptionLabel?: (
data: Option,
formatOptionLabelMeta: FormatOptionLabelMeta<Option>
) => ReactNode;
}
export default function Selector({
name,
label,
value,
errors = [],
options,
onChangeCallback,
formatOptionLabel,
required = false,
...props
}: SelectorProps): JSX.Element {
const theme = useTheme();
const [selectorValue, setSelectorValue] = useState<Option>();
useEffect(() => {
if (options.length === 0) return;
const option = options.find((o: any) => o.value === value);
if (option) {
setSelectorValue(option as Option);
}
}, [options, value]);
const handleChange = (selectedOption: Option) => {
setSelectorValue(selectedOption);
if (onChangeCallback) {
onChangeCallback(selectedOption.value);
}
};
return (
<FormField required={required}>
{label && (
<label htmlFor={name} title={`${name} field`}>
{label}
</label>
)}
<Select
value={selectorValue}
onChange={(newValue) => handleChange(newValue as Option)}
options={options}
isDisabled={props.disabled}
menuPlacement="auto"
formatOptionLabel={
formatOptionLabel
? (val, formatOptionLabelMeta) =>
formatOptionLabel(val, formatOptionLabelMeta)
: undefined
}
css={{ color: theme.colors.black }}
/>
</FormField>
);
}

View File

@@ -1,49 +0,0 @@
import { ChangeEvent, InputHTMLAttributes, useState } from 'react';
import FormField from '~/components/common/form/_form_field';
import FormFieldError from '~/components/common/form/_form_field_error';
import Input from '~/components/common/form/_input';
interface InputProps
extends Omit<InputHTMLAttributes<HTMLInputElement>, 'onChange'> {
label: string;
name: string;
value?: string;
errors?: string[];
onChange?: (name: string, value: string) => void;
}
export default function TextBox({
name,
label,
value = '',
errors = [],
onChange,
required = false,
...props
}: InputProps): JSX.Element {
const [inputValue, setInputValue] = useState<string>(value);
function _onChange({ target }: ChangeEvent<HTMLInputElement>) {
setInputValue(target.value);
if (onChange) {
onChange(target.name, target.value);
}
}
return (
<FormField required={required}>
<label htmlFor={name} title={label}>
{label}
</label>
<Input
{...props}
name={name}
onChange={_onChange}
value={inputValue}
placeholder={props.placeholder ?? 'Type something...'}
/>
{errors.length > 0 &&
errors.map((error) => <FormFieldError>{error}</FormFieldError>)}
</FormField>
);
}

View File

@@ -1,22 +0,0 @@
import styled from '@emotion/styled';
const IconButton = styled.button(({ theme }) => ({
cursor: 'pointer',
height: '2rem',
width: '2rem',
fontSize: '1rem',
color: theme.colors.font,
backgroundColor: theme.colors.grey,
borderRadius: '50%',
border: 0,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
'&:disabled': {
cursor: 'not-allowed',
opacity: 0.15,
},
}));
export default IconButton;

View File

@@ -1,8 +0,0 @@
import styled from '@emotion/styled';
const Legend = styled.span(({ theme }) => ({
fontSize: '13px',
color: theme.colors.grey,
}));
export default Legend;

View File

@@ -1,11 +0,0 @@
import styled from '@emotion/styled';
const ModalBody = styled.div({
width: '100%',
display: 'flex',
flex: 1,
alignItems: 'center',
flexDirection: 'column',
});
export default ModalBody;

View File

@@ -1,24 +0,0 @@
import styled from '@emotion/styled';
import TransitionLayout from '~/components/layouts/_transition_layout';
const ModalContainer = styled(TransitionLayout)(({ theme }) => ({
minWidth: '500px',
background: theme.colors.background,
padding: '1em',
borderRadius: theme.border.radius,
marginTop: '6em',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
flexDirection: 'column',
boxShadow: theme.colors.boxShadow,
[`@media (max-width: ${theme.media.mobile})`]: {
maxHeight: 'calc(100% - 2em)',
width: 'calc(100% - 2em)',
minWidth: 'unset',
marginTop: '1em',
},
}));
export default ModalContainer;

View File

@@ -1,20 +0,0 @@
import styled from '@emotion/styled';
const ModalHeader = styled.h3({
width: '100%',
marginBottom: '0.75em',
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between',
});
const ModalCloseBtn = styled.button(({ theme }) => ({
cursor: 'pointer',
color: theme.colors.primary,
backgroundColor: 'transparent',
border: 0,
padding: 0,
margin: 0,
}));
export { ModalHeader, ModalCloseBtn };

View File

@@ -1,19 +0,0 @@
import styled from '@emotion/styled';
import { rgba } from '~/lib/color';
const ModalWrapper = styled.div(({ theme }) => ({
zIndex: 9999,
position: 'absolute',
top: 0,
left: 0,
height: '100%',
width: '100%',
background: rgba(theme.colors.black, 0.35),
backdropFilter: 'blur(0.1em)',
display: 'flex',
alignItems: 'center',
flexDirection: 'column',
transition: theme.transition.delay,
}));
export default ModalWrapper;

View File

@@ -1,67 +0,0 @@
import { Fragment, ReactNode, useEffect, useRef } from 'react';
import { createPortal } from 'react-dom';
import { IoClose } from 'react-icons/io5';
import ModalBody from '~/components/common/modal/_modal_body';
import ModalContainer from '~/components/common/modal/_modal_container';
import {
ModalCloseBtn,
ModalHeader,
} from '~/components/common/modal/_modal_header';
import ModalWrapper from '~/components/common/modal/_modal_wrapper';
import TextEllipsis from '~/components/common/text_ellipsis';
import useClickOutside from '~/hooks/use_click_outside';
import useShortcut from '~/hooks/use_shortcut';
import { useGlobalHotkeysStore } from '~/store/global_hotkeys_store';
interface ModalProps {
title?: string;
children: ReactNode;
opened: boolean;
hideCloseBtn?: boolean;
className?: string;
close: () => void;
}
export default function Modal({
title,
children,
opened = true,
hideCloseBtn = false,
className,
close,
}: ModalProps) {
const modalRef = useRef<HTMLDivElement>(null);
const { setGlobalHotkeysEnabled } = useGlobalHotkeysStore();
useClickOutside(modalRef, close);
useShortcut('ESCAPE_KEY', close, { disableGlobalCheck: true });
useEffect(() => setGlobalHotkeysEnabled(!opened), [opened]);
if (typeof window === 'undefined') {
return <Fragment />;
}
return (
opened &&
createPortal(
<ModalWrapper>
<ModalContainer className={className} ref={modalRef}>
{(!hideCloseBtn || title) && (
<ModalHeader>
{title && <TextEllipsis>{title}</TextEllipsis>}
{!hideCloseBtn && (
<ModalCloseBtn onClick={close}>
<IoClose size={20} />
</ModalCloseBtn>
)}
</ModalHeader>
)}
<ModalBody>{children}</ModalBody>
</ModalContainer>
</ModalWrapper>,
document.body
)
);
}

View File

@@ -1,15 +0,0 @@
import styled from '@emotion/styled';
import { rgba } from '~/lib/color';
const RoundedImage = styled.img(({ theme }) => {
const transparentBlack = rgba(theme.colors.black, 0.1);
return {
borderRadius: '50%',
'&:hover': {
boxShadow: `0 1px 3px 0 ${transparentBlack}, 0 1px 2px -1px ${transparentBlack}`,
},
};
});
export default RoundedImage;

View File

@@ -1,227 +0,0 @@
import styled from '@emotion/styled';
import {
ColumnDef,
flexRender,
getCoreRowModel,
getPaginationRowModel,
getSortedRowModel,
PaginationState,
SortingState,
useReactTable,
} from '@tanstack/react-table';
import { useState } from 'react';
import IconButton from '~/components/common/icon_button';
import {
MdKeyboardArrowLeft,
MdKeyboardArrowRight,
MdKeyboardDoubleArrowLeft,
MdKeyboardDoubleArrowRight,
} from 'react-icons/md';
import Input from '~/components/common/form/_input';
const TablePageFooter = styled.div({
display: 'flex',
gap: '1em',
alignItems: 'center',
});
const Box = styled(TablePageFooter)({
gap: '0.35em',
});
const Resizer = styled.div<{ isResizing: boolean }>(
({ theme, isResizing }) => ({
cursor: 'col-resize',
userSelect: 'none',
touchAction: 'none',
position: 'absolute',
right: 0,
top: 0,
height: '100%',
width: '5px',
opacity: !isResizing ? 0 : 1,
background: !isResizing ? theme.colors.white : theme.colors.primary,
'&:hover': {
opacity: 0.5,
},
})
);
type TableProps<T> = {
columns: ColumnDef<T>[];
data: T[];
defaultSorting?: SortingState;
};
export default function Table<T>({
columns,
data,
defaultSorting = [],
}: TableProps<T>) {
const [pagination, setPagination] = useState<PaginationState>({
pageIndex: 0,
pageSize: 10,
});
const [sorting, setSorting] = useState<SortingState>(defaultSorting);
const table = useReactTable({
data,
columns,
enableColumnResizing: true,
columnResizeMode: 'onChange',
state: {
pagination,
sorting,
},
onPaginationChange: setPagination,
getCoreRowModel: getCoreRowModel(),
getPaginationRowModel: getPaginationRowModel(),
getSortedRowModel: getSortedRowModel(),
onSortingChange: setSorting,
debugTable: true,
});
return (
<div css={{ fontSize: '0.9rem', paddingBlock: '1em' }}>
<div
css={{
maxWidth: '100%',
marginBottom: '1em',
display: 'block',
overflowX: 'auto',
overflowY: 'hidden',
}}
>
<table>
<thead>
{table.getHeaderGroups().map((headerGroup) => (
<tr key={headerGroup.id}>
{headerGroup.headers.map((header) => (
<th
key={header.id}
style={{
width: header.getSize(),
}}
css={{
position: 'relative',
userSelect: 'none',
// resizer
'&:hover > div > div': {
opacity: 0.5,
},
}}
colSpan={header.colSpan}
>
{header.isPlaceholder ? null : (
<div
css={{
cursor: header.column.getCanSort()
? 'pointer'
: 'default',
}}
onClick={header.column.getToggleSortingHandler()}
title={
header.column.getCanSort()
? header.column.getNextSortingOrder() === 'asc'
? 'Sort ascending'
: header.column.getNextSortingOrder() === 'desc'
? 'Sort descending'
: 'Clear sort'
: undefined
}
>
{flexRender(
header.column.columnDef.header,
header.getContext()
)}
{{
asc: ' 🔼',
desc: ' 🔽',
}[header.column.getIsSorted() as string] ?? null}
{header.column.getCanResize() && (
<Resizer
onMouseDown={header.getResizeHandler()}
onTouchStart={header.getResizeHandler()}
isResizing={header.column.getIsResizing()}
/>
)}
</div>
)}
</th>
))}
</tr>
))}
</thead>
<tbody>
{table
.getRowModel()
.rows.slice(0, 10)
.map((row) => (
<tr key={row.id}>
{row.getVisibleCells().map((cell) => (
<td key={cell.id} style={{ width: cell.column.getSize() }}>
{flexRender(
cell.column.columnDef.cell,
cell.getContext()
)}
</td>
))}
</tr>
))}
</tbody>
</table>
</div>
{table.getPageCount() > 1 && (
<TablePageFooter>
<Box>
<IconButton
onClick={() => table.setPageIndex(0)}
disabled={!table.getCanPreviousPage()}
>
<MdKeyboardDoubleArrowLeft />
</IconButton>
<IconButton
onClick={() => table.previousPage()}
disabled={!table.getCanPreviousPage()}
>
<MdKeyboardArrowLeft />
</IconButton>
<IconButton
onClick={() => table.nextPage()}
disabled={!table.getCanNextPage()}
>
<MdKeyboardArrowRight />
</IconButton>
<IconButton
onClick={() => table.setPageIndex(table.getPageCount() - 1)}
disabled={!table.getCanNextPage()}
>
<MdKeyboardDoubleArrowRight />
</IconButton>
</Box>
<Box>
<span>Page</span>
<strong>
{table.getState().pagination.pageIndex + 1} of{' '}
{table.getPageCount()}
</strong>
</Box>
<Box>
Go to page
<Input
type="number"
min="1"
max={table.getPageCount()}
defaultValue={table.getState().pagination.pageIndex + 1}
onChange={(e) => {
const page = e.target.value ? Number(e.target.value) - 1 : 0;
table.setPageIndex(page);
}}
/>
</Box>
</TablePageFooter>
)}
</div>
);
}

View File

@@ -1,24 +0,0 @@
import styled from '@emotion/styled';
import { rgba } from '~/lib/color';
const TabItem = styled.li<{ active?: boolean; danger?: boolean }>(
({ theme, active, danger }) => {
const activeColor = !danger ? theme.colors.primary : theme.colors.lightRed;
return {
userSelect: 'none',
cursor: 'pointer',
backgroundColor: active
? rgba(activeColor, 0.15)
: theme.colors.secondary,
padding: '10px 20px',
border: `1px solid ${active ? rgba(activeColor, 0.1) : theme.colors.secondary}`,
borderBottom: `1px solid ${active ? rgba(activeColor, 0.25) : theme.colors.secondary}`,
display: 'flex',
gap: '0.35em',
alignItems: 'center',
transition: '.075s',
};
}
);
export default TabItem;

View File

@@ -1,10 +0,0 @@
import styled from '@emotion/styled';
const TabList = styled.ul({
padding: 0,
margin: 0,
display: 'flex',
listStyle: 'none',
});
export default TabList;

View File

@@ -1,12 +0,0 @@
import styled from '@emotion/styled';
import { rgba } from '~/lib/color';
const TabPanel = styled.div(({ theme }) => ({
zIndex: 1,
position: 'relative',
border: `1px solid ${rgba(theme.colors.primary, 0.25)}`,
padding: '20px',
marginTop: '-1px',
}));
export default TabPanel;

View File

@@ -1,42 +0,0 @@
import { ReactNode, useState } from 'react';
import { IconType } from 'react-icons/lib';
import TabItem from '~/components/common/tabs/tab_item';
import TabList from '~/components/common/tabs/tab_list';
import TabPanel from '~/components/common/tabs/tab_panel';
import TransitionLayout from '~/components/layouts/_transition_layout';
export interface Tab {
title: string;
content: ReactNode;
icon?: IconType;
danger?: boolean;
}
export default function Tabs({ tabs }: { tabs: Tab[] }) {
const [activeTabIndex, setActiveTabIndex] = useState<number>(0);
const handleTabClick = (index: number) => {
setActiveTabIndex(index);
};
return (
<div css={{ width: '100%' }}>
<TabList>
{tabs.map(({ title, icon: Icon, danger }, index) => (
<TabItem
key={index}
active={index === activeTabIndex}
onClick={() => handleTabClick(index)}
danger={danger ?? false}
>
{!!Icon && <Icon size={20} />}
{title}
</TabItem>
))}
</TabList>
<TabPanel key={tabs[activeTabIndex].title}>
<TransitionLayout>{tabs[activeTabIndex].content}</TransitionLayout>
</TabPanel>
</div>
);
}

View File

@@ -1,20 +0,0 @@
import styled from '@emotion/styled';
const TextEllipsis = styled.p<{ lines?: number }>(({ lines = 1 }) => {
if (lines > 1) {
return {
overflow: 'hidden',
display: '-webkit-box',
WebkitLineClamp: lines,
WebkitBoxOrient: 'vertical',
};
}
return {
textOverflow: 'ellipsis',
whiteSpace: 'nowrap',
overflow: 'hidden',
};
});
export default TextEllipsis;

View File

@@ -1,12 +0,0 @@
import styled from '@emotion/styled';
const UnstyledList = styled.ul({
'&, & li': {
listStyleType: 'none',
margin: 0,
padding: 0,
border: 0,
},
});
export default UnstyledList;