mirror of
https://github.com/Sonny93/my-links.git
synced 2025-12-09 07:03:25 +00:00
feat: create tab and selector components
This commit is contained in:
79
inertia/components/common/form/selector.tsx
Normal file
79
inertia/components/common/form/selector.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
@@ -6,7 +6,6 @@ const ModalBody = styled.div({
|
||||
flex: 1,
|
||||
alignItems: 'center',
|
||||
flexDirection: 'column',
|
||||
overflow: 'auto',
|
||||
});
|
||||
|
||||
export default ModalBody;
|
||||
|
||||
@@ -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',
|
||||
|
||||
24
inertia/components/common/tabs/tab_item.tsx
Normal file
24
inertia/components/common/tabs/tab_item.tsx
Normal 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;
|
||||
10
inertia/components/common/tabs/tab_list.tsx
Normal file
10
inertia/components/common/tabs/tab_list.tsx
Normal 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;
|
||||
12
inertia/components/common/tabs/tab_panel.tsx
Normal file
12
inertia/components/common/tabs/tab_panel.tsx
Normal 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;
|
||||
42
inertia/components/common/tabs/tabs.tsx
Normal file
42
inertia/components/common/tabs/tabs.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
@@ -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"
|
||||
|
||||
@@ -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>
|
||||
)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -67,9 +67,10 @@ function GlobalStyles() {
|
||||
},
|
||||
|
||||
hr: {
|
||||
color: localTheme.colors.secondary,
|
||||
width: '100%',
|
||||
marginBlock: '1em',
|
||||
border: 0,
|
||||
borderTop: `1px solid ${localTheme.colors.background}`,
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user