Skip to content

Instantly share code, notes, and snippets.

@lxchurbakov
Last active June 3, 2023 04:25
Show Gist options
  • Save lxchurbakov/b47087342fe16cc5d2b1ea82ef241178 to your computer and use it in GitHub Desktop.
Save lxchurbakov/b47087342fe16cc5d2b1ea82ef241178 to your computer and use it in GitHub Desktop.
minimal-ui
import React from 'react';
import styled, { css } from 'styled-components';
type PropsOf<T> = T extends React.FC<infer P> ? P : never;
export type BaseProps = {
p?: string;
pt?: string;
pl?: string;
pr?: string;
pb?: string;
m?: string;
mt?: string;
mr?: string;
mb?: string;
ml?: string;
w?: string;
h?: string;
mw?: string;
mh?: string;
};
export const Base = styled.div<BaseProps>`
padding: ${props => props.p};
padding-top: ${props => props.pt};
padding-right: ${props => props.pr};
padding-left: ${props => props.pl};
padding-bottom: ${props => props.pb};
margin: ${props => props.m};
margin-top: ${props => props.mt};
margin-right: ${props => props.mr};
margin-left: ${props => props.ml};
margin-bottom: ${props => props.mb};
width: ${props => props.w};
height: ${props => props.h};
max-width: ${props => props.mw};
max-height: ${props => props.mh};
box-sizing: border-box;
`;
export const Card = styled(Base)<{ color?: string }>`
background: ${props => props.color || '#f5f5f5'};
border-radius: 4px;
`;
export const Flex = styled(Base)<{
direction?: 'row' | 'column';
align?: 'center' | 'flex-start' | 'flex-end';
justify?: 'center' | 'flex-start' | 'flex-end' | 'space-around' | 'space-between';
wrap?: boolean;
gap?: number;
}>`
display: flex;
flex-direction: ${props => props.dir || 'row'};
align-items: ${props => props.align || 'center'};
justify-content: ${props => props.justify || 'center'};
flex-wrap: ${props => props.wrap ? 'wrap' : 'nowrap'};
gap: ${props => props.gap || 0}px;
`;
export const Text = styled(Base)<{ size: number, weight: number, color: string }>`
font-family: Inter;
font-size: ${props => props.size}px;
font-weight: ${props => props.weight};
color: ${props => props.color};
line-height: 1.6;
text-align: ${props => props.align};
`;
export const Image = styled.img<BaseProps>`
padding: ${props => props.p};
padding-top: ${props => props.pt};
padding-right: ${props => props.pr};
padding-left: ${props => props.pl};
padding-bottom: ${props => props.pb};
margin: ${props => props.m};
margin-top: ${props => props.mt};
margin-right: ${props => props.mr};
margin-left: ${props => props.ml};
margin-bottom: ${props => props.mb};
width: ${props => props.w};
height: ${props => props.h};
max-width: ${props => props.mw};
max-height: ${props => props.mh};
object-fit: cover;
box-sizing: border-box;
`;
export const Clickable = styled(Card)<{ color?: string, border?: string, radius?: string }>`
background: ${props => props.color || 'none'};
cursor: pointer;
user-select: none;
display: inline-block;
border: ${props => props.border || 'none'};
border-radius: ${props => props.radius || '0px'};
&:active {
transform: translateY(1px);
}
`;
export const Container = ({ children, ...props }: { children: any } & PropsOf<typeof Flex>) => {
return (
<Flex {...props}>
<Base p="100px 20px" w="100%" mw="1200px">
{children}
</Base>
</Flex>
)
};
export const Disabled = styled(Base)<{ disabled: boolean }>`
${props => props.disabled && css`
opacity: .5;
pointer-events: none;
`};
`;
export const Table = styled(Base)`
border-radius: 8px;
overflow: hidden;
`;
export const Row = styled(Flex).attrs({
align: 'center',
justify: 'flex-start',
w: '100%'
})`
&:nth-child(2n+1) {
background: #E4EAF5;
}
`;
export const Item = styled(Flex)`
padding: 8px 16px;
`;
import React from 'react';
import styled, { css } from 'styled-components';
import { PropsOf } from '/src/libs/utils';
import { ArrowDownShort } from '@styled-icons/bootstrap/ArrowDownShort';
import { CloseOutline } from '@styled-icons/evaicons-outline/CloseOutline';
import { BaseProps, Base, Flex, Card, Text } from './atoms';
import { colors } from '/src/libs/theme';
type StyledProps = { className?: string, style?: React.CSSProperties };
type CursorOption = 'pointer' | 'text';
const InputWrap = styled(Flex)<{ cursor: CursorOption }>`
background: ${colors.blueLight};
padding: 12px 16px;
cursor: ${props => props.cursor};
border-radius: 8px;
`;
const InputCore = styled.input<{ hasPrefix: boolean, cursor: CursorOption }>`
background: none;
border: none;
font-family: Inter;
font-size: 16px;
font-weight: 400;
width: 100%;
box-sizing: border-box;
cursor: ${props => props.cursor};
&:focus {
outline: none;
}
`;
export const StringInput = ({ value, onChange, placeholder, prefix, suffix, cursor, onFocus, onBlur, ...props }: {
value: string; onChange: (value: string) => void; placeholder?: string;
prefix?: React.ReactNode; suffix?: React.ReactNode; cursor?: CursorOption;
onFocus?: () => void; onBlur?: () => void;
} & StyledProps & BaseProps) => {
const inputRef = React.useRef<HTMLInputElement>(null);
const handler = React.useCallback((e: React.ChangeEvent<HTMLInputElement>) => onChange(e.target.value || ''), [onChange]);
const focus = React.useCallback(() => inputRef.current?.focus(), []);
return (
<InputWrap onClick={focus} cursor={cursor || 'text'} align="flex-start" {...props}>
{prefix && <Base mr="12px">{prefix}</Base>}
<InputCore
ref={inputRef}
hasPrefix={!!prefix}
placeholder={placeholder}
value={value}
onChange={handler}
cursor={cursor || 'text'}
onFocus={onFocus}
onBlur={onBlur}
/>
{suffix && <Base ml="12px">{suffix}</Base>}
</InputWrap>
);
};
const DropdownBase= styled(Base)`
position: relative;
`;
const DropdownCard = styled(Card)`
position: absolute;
top: calc(100% + 4px);
left: 0;
width: 100%;
max-height: 300px;
overflow-y: scroll;
`;
const DropdownOption = styled(Base)`
padding: 12px;
cursor: pointer;
&:hover {
background: ${colors.blueLight};
}
`;
export const OptionsInput = <T extends string>({ value, onChange, options, placeholder, prefix, ...props }: {
value: T | null, options: { value: T, label: string }[], onChange: (value: T | null) => void, placeholder?: string,
} & Pick<PropsOf<typeof StringInput>, 'prefix'> & StyledProps & BaseProps) => {
const label = React.useMemo(() => options.find(($) => $.value === value)?.label || '', [options, value]);
const [isOpen, setIsOpen] = React.useState(false);
const open = React.useCallback(() => setIsOpen(true), [setIsOpen]);
const close = React.useCallback(() => setTimeout(() => setIsOpen(false), 100), [setIsOpen]);
return (
<DropdownBase {...props}>
<StringInput
placeholder={placeholder}
prefix={prefix}
value={label}
onChange={() => {}}
cursor="pointer"
suffix={value === null ? (
<ArrowDownShort style={{ transform: `rotate(${isOpen ? 180 : 0}deg)`, marginRight: '-4px' }} width={24} color="#333" />
) : (
<CloseOutline width={24} color="#333" onClick={() => onChange(null)} />
)}
onFocus={open}
onBlur={close}
/>
{isOpen && (
<DropdownCard>
{options.map((option) => (
<DropdownOption key={option.value} onClick={() => onChange(option.value)}>
<Text size={16} weight={400} color="#333">{option.label}</Text>
</DropdownOption>
))}
</DropdownCard>
)}
</DropdownBase>
);
};
const Checkbox = styled.div`
position: relative;
background: #232426;
border: 2px solid #4C4D4F;
width: 20px;
height: 20px;
border-radius: 4px;
${props => props.value && css`
background: ${colors.blue};
border-color: ${colors.blue};
&::after {
display: block;
position: absolute;
top: 2px;
content: " ";
width:16px;
height: 8px;
border-top: 3px solid white;
border-right: 3px solid white;
transform: rotate(135deg);
}
`};
`;
export const BooleanInput = ({ value, onChange, ...props }: { value: boolean, onChange: ($: boolean) => void } & BaseProps) => {
return (
<Clickable color="none" onClick={() => onChange(!value)} {...props}>
<Checkbox value={value} />
</Clickable>
);
};
import _ from 'lodash';
import React from 'react';
export const useEventListener = (eventName, handler, element = window) => {
React.useEffect(() => {
element.addEventListener(eventName, handler);
return () => element.removeEventListener(eventName, handler);
}, [eventName, handler, element]);
};
export const getWindowWidth = () => {
return window.innerWidth;
};
export const useWindowWidth = () => {
const [size, setSize] = React.useState(getWindowWidth());
const listener = React.useMemo(() => _.debounce(() => setSize(getWindowWidth()), 100), [setSize]);
useEventListener('resize', listener, window);
return size;
};
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment