feat: create tab and selector components

This commit is contained in:
Sonny
2024-06-02 01:22:53 +02:00
committed by Sonny
parent e9ccefd938
commit 8e1e3bea17
14 changed files with 335 additions and 43 deletions

View File

@@ -0,0 +1,79 @@
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

@@ -6,7 +6,6 @@ const ModalBody = styled.div({
flex: 1,
alignItems: 'center',
flexDirection: 'column',
overflow: 'auto',
});
export default ModalBody;

View File

@@ -3,7 +3,7 @@ import TransitionLayout from '~/components/layouts/_transition_layout';
const ModalContainer = styled(TransitionLayout)(({ theme }) => ({
minWidth: '500px',
background: theme.colors.secondary,
background: theme.colors.background,
padding: '1em',
borderRadius: theme.border.radius,
marginTop: '6em',

View File

@@ -0,0 +1,24 @@
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

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

View File

@@ -0,0 +1,12 @@
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

@@ -0,0 +1,42 @@
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,6 +1,7 @@
import { FormEvent } from 'react';
import { useTranslation } from 'react-i18next';
import Checkbox from '~/components/common/form/checkbox';
import Selector from '~/components/common/form/selector';
import TextBox from '~/components/common/form/textbox';
import BackToDashboard from '~/components/common/navigation/back_to_dashboard';
import FormLayout from '~/components/layouts/form_layout';
@@ -91,17 +92,18 @@ export default function FormLink({
errors={errors?.description}
disabled={disableInputs}
/>
<select
onChange={({ target }) => setData('collectionId', target.value)}
defaultValue={data.collectionId}
disabled={disableInputs}
>
{collections?.map((collection) => (
<option key={collection.id} value={collection.id}>
{collection.name}
</option>
))}
</select>
<Selector
label={t('collection.collections')}
name="collection"
placeholder={t('collection.collections')}
value={data.collectionId}
onChangeCallback={(value) => setData('collectionId', value)}
options={collections.map(({ id, name }) => ({
label: name,
value: id,
}))}
required
/>
<Checkbox
label={t('favorite')}
name="favorite"

View File

@@ -1,32 +1,53 @@
import dayjs from 'dayjs';
import { ChangeEvent } from 'react';
import { useTranslation } from 'react-i18next';
import Selector from '~/components/common/form/selector';
import { LS_LANG_KEY } from '~/constants';
import { languages } from '~/i18n';
export default function LangSelector() {
type Country = 'fr' | 'en';
export default function LangSelector({
onSelected,
}: {
onSelected?: (country: Country) => void;
}) {
const { t, i18n } = useTranslation('common');
const onToggleLanguageClick = ({
target,
}: ChangeEvent<HTMLSelectElement>) => {
dayjs.locale(target.value);
i18n.changeLanguage(target.value);
localStorage.setItem(LS_LANG_KEY, target.value);
const onToggleLanguageClick = (newLocale: string) => {
dayjs.locale(newLocale);
i18n.changeLanguage(newLocale);
localStorage.setItem(LS_LANG_KEY, newLocale);
};
return (
<select
onChange={onToggleLanguageClick}
name="lang-selector"
id="lang-selector"
defaultValue={i18n.language}
>
{languages.map((lang) => (
<option value={lang} key={lang}>
{t(`language.${lang}`)}
</option>
))}
</select>
<Selector
name="lng-select"
label={t('select-your-lang')}
value={i18n.language}
onChangeCallback={(value) => {
onToggleLanguageClick(value.toString());
if (onSelected) {
setTimeout(() => onSelected(value.toString() as Country), 150);
}
}}
options={languages.map((lang: Country) => ({
label: t(`language.${lang}`),
value: lang,
}))}
formatOptionLabel={(country) => (
<div
className="country-option"
style={{ display: 'flex', gap: '.5em', alignItems: 'center' }}
>
<img
src={`/icons/${country.value}.svg`}
alt="country-image"
height={24}
width={24}
/>
<span>{country.label}</span>
</div>
)}
/>
);
}

View File

@@ -67,9 +67,10 @@ function GlobalStyles() {
},
hr: {
color: localTheme.colors.secondary,
width: '100%',
marginBlock: '1em',
border: 0,
borderTop: `1px solid ${localTheme.colors.background}`,
},
});