mirror of
https://github.com/iib0011/omni-tools.git
synced 2025-12-29 16:16:02 +00:00
Merge branch 'main' of https://github.com/iib0011/omni-tools into fork/AshAnand34/merge-video-tool
This commit is contained in:
33
src/@types/i18n.d.ts
vendored
Normal file
33
src/@types/i18n.d.ts
vendored
Normal file
@@ -0,0 +1,33 @@
|
||||
import 'i18next';
|
||||
|
||||
import translation from '../../public/locales/en/translation.json';
|
||||
import string from '../../public/locales/en/string.json';
|
||||
import number from '../../public/locales/en/number.json';
|
||||
import video from '../../public/locales/en/video.json';
|
||||
import list from '../../public/locales/en/list.json';
|
||||
import json from '../../public/locales/en/json.json';
|
||||
import time from '../../public/locales/en/time.json';
|
||||
import csv from '../../public/locales/en/csv.json';
|
||||
import pdf from '../../public/locales/en/pdf.json';
|
||||
import audio from '../../public/locales/en/audio.json';
|
||||
import xml from '../../public/locales/en/xml.json';
|
||||
import image from '../../public/locales/en/image.json';
|
||||
|
||||
declare module 'i18next' {
|
||||
interface CustomTypeOptions {
|
||||
resources: {
|
||||
translation: typeof translation;
|
||||
string: typeof string;
|
||||
number: typeof number;
|
||||
video: typeof video;
|
||||
list: typeof list;
|
||||
json: typeof json;
|
||||
time: typeof time;
|
||||
csv: typeof csv;
|
||||
pdf: typeof pdf;
|
||||
audio: typeof audio;
|
||||
xml: typeof xml;
|
||||
image: typeof image;
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -10,6 +10,8 @@ import { tools } from '../tools';
|
||||
import './index.css';
|
||||
import { darkTheme, lightTheme } from '../config/muiConfig';
|
||||
import ScrollToTopButton from './ScrollToTopButton';
|
||||
import { I18nextProvider } from 'react-i18next';
|
||||
import i18n from '../i18n';
|
||||
|
||||
export type Mode = 'dark' | 'light' | 'system';
|
||||
|
||||
@@ -44,32 +46,34 @@ function App() {
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<ThemeProvider theme={theme}>
|
||||
<CssBaseline />
|
||||
<SnackbarProvider
|
||||
maxSnack={5}
|
||||
anchorOrigin={{
|
||||
vertical: 'bottom',
|
||||
horizontal: 'right'
|
||||
}}
|
||||
>
|
||||
<CustomSnackBarProvider>
|
||||
<BrowserRouter>
|
||||
<Navbar
|
||||
mode={mode}
|
||||
onChangeMode={() => {
|
||||
setMode((prev) => nextMode(prev));
|
||||
localStorage.setItem('theme', nextMode(mode));
|
||||
}}
|
||||
/>
|
||||
<Suspense fallback={<Loading />}>
|
||||
<AppRoutes />
|
||||
</Suspense>
|
||||
</BrowserRouter>
|
||||
</CustomSnackBarProvider>
|
||||
</SnackbarProvider>
|
||||
<ScrollToTopButton />
|
||||
</ThemeProvider>
|
||||
<I18nextProvider i18n={i18n}>
|
||||
<ThemeProvider theme={theme}>
|
||||
<CssBaseline />
|
||||
<SnackbarProvider
|
||||
maxSnack={5}
|
||||
anchorOrigin={{
|
||||
vertical: 'bottom',
|
||||
horizontal: 'right'
|
||||
}}
|
||||
>
|
||||
<CustomSnackBarProvider>
|
||||
<BrowserRouter>
|
||||
<Navbar
|
||||
mode={mode}
|
||||
onChangeMode={() => {
|
||||
setMode((prev) => nextMode(prev));
|
||||
localStorage.setItem('theme', nextMode(mode));
|
||||
}}
|
||||
/>
|
||||
<Suspense fallback={<Loading />}>
|
||||
<AppRoutes />
|
||||
</Suspense>
|
||||
</BrowserRouter>
|
||||
</CustomSnackBarProvider>
|
||||
</SnackbarProvider>
|
||||
<ScrollToTopButton />
|
||||
</ThemeProvider>
|
||||
</I18nextProvider>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -15,9 +15,16 @@ import { useState } from 'react';
|
||||
import { DefinedTool } from '@tools/defineTool';
|
||||
import { filterTools, tools } from '@tools/index';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import _ from 'lodash';
|
||||
import { Icon } from '@iconify/react';
|
||||
import { getToolCategoryTitle } from '@utils/string';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { FullI18nKey, validNamespaces } from '../i18n';
|
||||
import {
|
||||
getBookmarkedToolPaths,
|
||||
isBookmarked,
|
||||
toggleBookmarked
|
||||
} from '@utils/bookmark';
|
||||
import IconButton from '@mui/material/IconButton';
|
||||
|
||||
const GroupHeader = styled('div')(({ theme }) => ({
|
||||
position: 'sticky',
|
||||
@@ -33,44 +40,98 @@ const GroupHeader = styled('div')(({ theme }) => ({
|
||||
const GroupItems = styled('ul')({
|
||||
padding: 0
|
||||
});
|
||||
const exampleTools: { label: string; url: string }[] = [
|
||||
{
|
||||
label: 'Create a transparent image',
|
||||
url: '/image-generic/create-transparent'
|
||||
},
|
||||
{ label: 'Prettify JSON', url: '/json/prettify' },
|
||||
{ label: 'Change GIF speed', url: '/gif/change-speed' },
|
||||
{ label: 'Sort a list', url: '/list/sort' },
|
||||
{ label: 'Compress PNG', url: '/png/compress-png' },
|
||||
{ label: 'Split a text', url: '/string/split' },
|
||||
{ label: 'Split PDF', url: '/pdf/split-pdf' },
|
||||
{ label: 'Trim video', url: '/video/trim' },
|
||||
{ label: 'Calculate number sum', url: '/number/sum' }
|
||||
];
|
||||
|
||||
type ToolInfo = {
|
||||
label: FullI18nKey;
|
||||
url: string;
|
||||
};
|
||||
|
||||
export default function Hero() {
|
||||
const { t } = useTranslation(validNamespaces);
|
||||
const [inputValue, setInputValue] = useState<string>('');
|
||||
const theme = useTheme();
|
||||
const [filteredTools, setFilteredTools] = useState<DefinedTool[]>(tools);
|
||||
const [bookmarkedToolPaths, setBookmarkedToolPaths] = useState<string[]>(
|
||||
getBookmarkedToolPaths()
|
||||
);
|
||||
const navigate = useNavigate();
|
||||
|
||||
const exampleTools: ToolInfo[] = [
|
||||
{
|
||||
label: 'translation:hero.examples.createTransparentImage',
|
||||
url: '/image-generic/create-transparent'
|
||||
},
|
||||
{
|
||||
label: 'translation:hero.examples.prettifyJson',
|
||||
url: '/json/prettify'
|
||||
},
|
||||
{
|
||||
label: 'translation:hero.examples.changeGifSpeed',
|
||||
url: '/gif/change-speed'
|
||||
},
|
||||
{
|
||||
label: 'translation:hero.examples.sortList',
|
||||
url: '/list/sort'
|
||||
},
|
||||
{
|
||||
label: 'translation:hero.examples.compressPng',
|
||||
url: '/png/compress-png'
|
||||
},
|
||||
{
|
||||
label: 'translation:hero.examples.splitText',
|
||||
url: '/string/split'
|
||||
},
|
||||
{
|
||||
label: 'translation:hero.examples.splitPdf',
|
||||
url: '/pdf/split-pdf'
|
||||
},
|
||||
{
|
||||
label: 'translation:hero.examples.trimVideo',
|
||||
url: '/video/trim'
|
||||
},
|
||||
{
|
||||
label: 'translation:hero.examples.calculateNumberSum',
|
||||
url: '/number/sum'
|
||||
}
|
||||
];
|
||||
|
||||
const handleInputChange = (
|
||||
event: React.ChangeEvent<{}>,
|
||||
newInputValue: string
|
||||
) => {
|
||||
setInputValue(newInputValue);
|
||||
setFilteredTools(filterTools(tools, newInputValue));
|
||||
setFilteredTools(filterTools(tools, newInputValue, t));
|
||||
};
|
||||
const toolsMap = new Map<string, ToolInfo>();
|
||||
for (const tool of filteredTools) {
|
||||
toolsMap.set(tool.path, {
|
||||
label: tool.name,
|
||||
url: '/' + tool.path
|
||||
});
|
||||
}
|
||||
|
||||
const displayedTools =
|
||||
bookmarkedToolPaths.length > 0
|
||||
? bookmarkedToolPaths.flatMap((path) => {
|
||||
const tool = toolsMap.get(path);
|
||||
if (tool === undefined) {
|
||||
return [];
|
||||
}
|
||||
return [tool];
|
||||
})
|
||||
: exampleTools;
|
||||
|
||||
return (
|
||||
<Box width={{ xs: '90%', md: '80%', lg: '60%' }}>
|
||||
<Stack mb={1} direction={'row'} spacing={1} justifyContent={'center'}>
|
||||
<Typography sx={{ textAlign: 'center' }} fontSize={{ xs: 25, md: 30 }}>
|
||||
Get Things Done Quickly with{' '}
|
||||
{t('translation:hero.title')}{' '}
|
||||
<Typography
|
||||
fontSize={{ xs: 25, md: 30 }}
|
||||
display={'inline'}
|
||||
color={'primary'}
|
||||
>
|
||||
OmniTools
|
||||
{t('translation:hero.brand')}
|
||||
</Typography>
|
||||
</Typography>
|
||||
</Stack>
|
||||
@@ -79,9 +140,7 @@ export default function Hero() {
|
||||
fontSize={{ xs: 15, md: 20 }}
|
||||
mb={2}
|
||||
>
|
||||
Boost your productivity with OmniTools, the ultimate toolkit for getting
|
||||
things done quickly! Access thousands of user-friendly utilities for
|
||||
editing images, text, lists, and data, all directly from your browser.
|
||||
{t('translation:hero.description')}
|
||||
</Typography>
|
||||
|
||||
<Autocomplete
|
||||
@@ -92,18 +151,18 @@ export default function Hero() {
|
||||
renderGroup={(params) => {
|
||||
return (
|
||||
<li key={params.key}>
|
||||
<GroupHeader>{getToolCategoryTitle(params.group)}</GroupHeader>
|
||||
<GroupHeader>{getToolCategoryTitle(params.group, t)}</GroupHeader>
|
||||
<GroupItems>{params.children}</GroupItems>
|
||||
</li>
|
||||
);
|
||||
}}
|
||||
inputValue={inputValue}
|
||||
getOptionLabel={(option) => option.name}
|
||||
getOptionLabel={(option) => t(option.name)}
|
||||
renderInput={(params) => (
|
||||
<TextField
|
||||
{...params}
|
||||
fullWidth
|
||||
placeholder={'Search all tools'}
|
||||
placeholder={t('translation:hero.searchPlaceholder')}
|
||||
InputProps={{
|
||||
...params.InputProps,
|
||||
endAdornment: <SearchIcon />,
|
||||
@@ -121,12 +180,42 @@ export default function Hero() {
|
||||
{...props}
|
||||
onClick={() => navigate('/' + option.path)}
|
||||
>
|
||||
<Stack direction={'row'} spacing={2} alignItems={'center'}>
|
||||
<Icon fontSize={20} icon={option.icon} />
|
||||
<Box>
|
||||
<Typography fontWeight={'bold'}>{option.name}</Typography>
|
||||
<Typography fontSize={12}>{option.shortDescription}</Typography>
|
||||
</Box>
|
||||
<Stack
|
||||
direction={'row'}
|
||||
alignItems={'center'}
|
||||
justifyContent={'space-between'}
|
||||
width={'100%'}
|
||||
>
|
||||
<Stack direction={'row'} spacing={2} alignItems={'center'}>
|
||||
<Icon fontSize={20} icon={option.icon} />
|
||||
<Box>
|
||||
<Typography fontWeight={'bold'}>{t(option.name)}</Typography>
|
||||
<Typography fontSize={12}>
|
||||
{t(option.shortDescription)}
|
||||
</Typography>
|
||||
</Box>
|
||||
</Stack>
|
||||
<IconButton
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
toggleBookmarked(option.path);
|
||||
setBookmarkedToolPaths(getBookmarkedToolPaths());
|
||||
}}
|
||||
>
|
||||
<Icon
|
||||
fontSize={20}
|
||||
color={
|
||||
isBookmarked(option.path)
|
||||
? theme.palette.primary.main
|
||||
: theme.palette.grey[500]
|
||||
}
|
||||
icon={
|
||||
isBookmarked(option.path)
|
||||
? 'mdi:bookmark'
|
||||
: 'mdi:bookmark-plus-outline'
|
||||
}
|
||||
/>
|
||||
</IconButton>
|
||||
</Stack>
|
||||
</Box>
|
||||
)}
|
||||
@@ -137,7 +226,7 @@ export default function Hero() {
|
||||
}}
|
||||
/>
|
||||
<Grid container spacing={2} mt={2}>
|
||||
{exampleTools.map((tool) => (
|
||||
{displayedTools.map((tool) => (
|
||||
<Grid
|
||||
onClick={() =>
|
||||
navigate(tool.url.startsWith('/') ? tool.url : `/${tool.url}`)
|
||||
@@ -162,10 +251,30 @@ export default function Hero() {
|
||||
cursor: 'pointer',
|
||||
'&:hover': {
|
||||
backgroundColor: 'background.hover'
|
||||
}
|
||||
},
|
||||
height: '100%'
|
||||
}}
|
||||
>
|
||||
<Typography>{tool.label}</Typography>
|
||||
<Stack direction={'row'} spacing={1} alignItems={'center'}>
|
||||
<Typography textAlign={'center'}>{t(tool.label)}</Typography>
|
||||
{bookmarkedToolPaths.length > 0 && (
|
||||
<IconButton
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
const path = tool.url.substring(1);
|
||||
toggleBookmarked(path);
|
||||
setBookmarkedToolPaths(getBookmarkedToolPaths());
|
||||
}}
|
||||
size={'small'}
|
||||
>
|
||||
<Icon
|
||||
icon={'mdi:close'}
|
||||
color={theme.palette.grey[500]}
|
||||
fontSize={15}
|
||||
/>
|
||||
</IconButton>
|
||||
)}
|
||||
</Stack>
|
||||
</Box>
|
||||
</Grid>
|
||||
))}
|
||||
|
||||
@@ -13,22 +13,39 @@ import {
|
||||
ListItem,
|
||||
ListItemButton,
|
||||
ListItemText,
|
||||
Stack
|
||||
Stack,
|
||||
Select,
|
||||
MenuItem,
|
||||
FormControl
|
||||
} from '@mui/material';
|
||||
import useMediaQuery from '@mui/material/useMediaQuery';
|
||||
import { useTheme } from '@mui/material/styles';
|
||||
import { Icon } from '@iconify/react';
|
||||
import { Mode } from 'components/App';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
interface NavbarProps {
|
||||
mode: Mode;
|
||||
onChangeMode: () => void;
|
||||
}
|
||||
const languages = [
|
||||
{ code: 'en', label: 'English' },
|
||||
{ code: 'de', label: 'Deutsch' },
|
||||
{ code: 'es', label: 'Español' },
|
||||
{ code: 'fr', label: 'Français' },
|
||||
{ code: 'pt', label: 'Português' },
|
||||
{ code: 'ja', label: '日本語' },
|
||||
{ code: 'hi', label: 'हिंदी' },
|
||||
{ code: 'nl', label: 'Nederlands' },
|
||||
{ code: 'ru', label: 'Русский' },
|
||||
{ code: 'zh', label: '中文' }
|
||||
];
|
||||
|
||||
const Navbar: React.FC<NavbarProps> = ({
|
||||
mode,
|
||||
onChangeMode: onChangeMode
|
||||
}) => {
|
||||
const { t, i18n } = useTranslation();
|
||||
const navigate = useNavigate();
|
||||
const theme = useTheme();
|
||||
const isMobile = useMediaQuery(theme.breakpoints.down('md'));
|
||||
@@ -36,12 +53,51 @@ const Navbar: React.FC<NavbarProps> = ({
|
||||
const toggleDrawer = (open: boolean) => () => {
|
||||
setDrawerOpen(open);
|
||||
};
|
||||
|
||||
const handleLanguageChange = (event: any) => {
|
||||
const newLanguage = event.target.value;
|
||||
i18n.changeLanguage(newLanguage);
|
||||
localStorage.setItem('lang', newLanguage);
|
||||
};
|
||||
|
||||
const navItems: { label: string; path: string }[] = [
|
||||
// { label: 'Features', path: '/features' }
|
||||
// { label: 'About Us', path: '/about-us' }
|
||||
];
|
||||
|
||||
const languageSelector = (
|
||||
<FormControl size="small" sx={{ minWidth: 120 }}>
|
||||
<Select
|
||||
value={i18n.language}
|
||||
onChange={handleLanguageChange}
|
||||
displayEmpty
|
||||
sx={{
|
||||
color: 'inherit',
|
||||
'& .MuiSelect-icon': {
|
||||
color: 'inherit'
|
||||
},
|
||||
'& .MuiOutlinedInput-notchedOutline': {
|
||||
borderColor: 'transparent'
|
||||
},
|
||||
'&:hover .MuiOutlinedInput-notchedOutline': {
|
||||
borderColor: 'transparent'
|
||||
},
|
||||
'&.Mui-focused .MuiOutlinedInput-notchedOutline': {
|
||||
borderColor: 'transparent'
|
||||
}
|
||||
}}
|
||||
>
|
||||
{languages.map((lang) => (
|
||||
<MenuItem key={lang.code} value={lang.code}>
|
||||
{lang.label}
|
||||
</MenuItem>
|
||||
))}
|
||||
</Select>
|
||||
</FormControl>
|
||||
);
|
||||
|
||||
const buttons: ReactNode[] = [
|
||||
languageSelector,
|
||||
<Icon
|
||||
key={mode}
|
||||
onClick={onChangeMode}
|
||||
@@ -83,7 +139,7 @@ const Navbar: React.FC<NavbarProps> = ({
|
||||
/>
|
||||
}
|
||||
>
|
||||
Buy me a coffee
|
||||
{t('navbar.buyMeACoffee')}
|
||||
</Button>
|
||||
];
|
||||
const drawerList = (
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Box, Button, styled, useTheme } from '@mui/material';
|
||||
import { Box, Button, Stack, styled, useTheme } from '@mui/material';
|
||||
import Typography from '@mui/material/Typography';
|
||||
import ToolBreadcrumb from './ToolBreadcrumb';
|
||||
import { capitalizeFirstLetter } from '../utils/string';
|
||||
@@ -7,6 +7,11 @@ import { Icon, IconifyIcon } from '@iconify/react';
|
||||
import { categoriesColors } from '../config/uiConfig';
|
||||
import { getToolsByCategory } from '@tools/index';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { isBookmarked, toggleBookmarked } from '@utils/bookmark';
|
||||
import IconButton from '@mui/material/IconButton';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import useMediaQuery from '@mui/material/useMediaQuery';
|
||||
import { validNamespaces } from '../i18n';
|
||||
|
||||
const StyledButton = styled(Button)(({ theme }) => ({
|
||||
backgroundColor: 'white',
|
||||
@@ -21,10 +26,14 @@ interface ToolHeaderProps {
|
||||
description: string;
|
||||
icon?: IconifyIcon | string;
|
||||
type: string;
|
||||
path: string;
|
||||
}
|
||||
|
||||
function ToolLinks() {
|
||||
const { t } = useTranslation();
|
||||
const [examplesVisible, setExamplesVisible] = useState(false);
|
||||
const theme = useTheme();
|
||||
const isMd = useMediaQuery(theme.breakpoints.down('md'));
|
||||
|
||||
useEffect(() => {
|
||||
const timeout = setTimeout(() => {
|
||||
@@ -45,16 +54,18 @@ function ToolLinks() {
|
||||
}
|
||||
return (
|
||||
<Grid container spacing={2} mt={1}>
|
||||
<Grid item md={12} lg={6}>
|
||||
<StyledButton
|
||||
sx={{ backgroundColor: 'background.paper' }}
|
||||
fullWidth
|
||||
variant="outlined"
|
||||
onClick={() => scrollToElement('tool')}
|
||||
>
|
||||
Use This Tool
|
||||
</StyledButton>
|
||||
</Grid>
|
||||
{isMd && (
|
||||
<Grid item md={12} lg={6}>
|
||||
<StyledButton
|
||||
sx={{ backgroundColor: 'background.paper' }}
|
||||
fullWidth
|
||||
variant="outlined"
|
||||
onClick={() => scrollToElement('tool')}
|
||||
>
|
||||
Use This Tool
|
||||
</StyledButton>
|
||||
</Grid>
|
||||
)}
|
||||
{examplesVisible && (
|
||||
<Grid item md={12} lg={6}>
|
||||
<StyledButton
|
||||
@@ -63,7 +74,7 @@ function ToolLinks() {
|
||||
sx={{ backgroundColor: 'background.paper' }}
|
||||
onClick={() => scrollToElement('examples')}
|
||||
>
|
||||
See Examples
|
||||
{t('toolHeader.seeExamples')}
|
||||
</StyledButton>
|
||||
</Grid>
|
||||
)}
|
||||
@@ -80,15 +91,19 @@ export default function ToolHeader({
|
||||
icon,
|
||||
title,
|
||||
description,
|
||||
type
|
||||
type,
|
||||
path
|
||||
}: ToolHeaderProps) {
|
||||
const theme = useTheme();
|
||||
const { t } = useTranslation();
|
||||
const [bookmarked, setBookmarked] = useState<boolean>(isBookmarked(path));
|
||||
return (
|
||||
<Box my={4}>
|
||||
<ToolBreadcrumb
|
||||
items={[
|
||||
{ title: 'All tools', link: '/' },
|
||||
{
|
||||
title: getToolsByCategory().find(
|
||||
title: getToolsByCategory(t).find(
|
||||
(category) => category.type === type
|
||||
)!.rawTitle,
|
||||
link: '/categories/' + type
|
||||
@@ -98,9 +113,27 @@ export default function ToolHeader({
|
||||
/>
|
||||
<Grid mt={1} container spacing={2}>
|
||||
<Grid item xs={12} md={8}>
|
||||
<Typography mb={2} fontSize={30} color={'primary'}>
|
||||
{title}
|
||||
</Typography>
|
||||
<Stack direction={'row'} spacing={2} alignItems={'center'}>
|
||||
<Typography mb={2} fontSize={30} color={'primary'}>
|
||||
{title}
|
||||
</Typography>
|
||||
<IconButton
|
||||
onClick={(e) => {
|
||||
toggleBookmarked(path);
|
||||
setBookmarked(!bookmarked);
|
||||
}}
|
||||
>
|
||||
<Icon
|
||||
fontSize={30}
|
||||
color={
|
||||
bookmarked
|
||||
? theme.palette.primary.main
|
||||
: theme.palette.grey[500]
|
||||
}
|
||||
icon={bookmarked ? 'mdi:bookmark' : 'mdi:bookmark-plus-outline'}
|
||||
/>
|
||||
</IconButton>
|
||||
</Stack>
|
||||
<Typography fontSize={20}>{description}</Typography>
|
||||
<ToolLinks />
|
||||
</Grid>
|
||||
|
||||
@@ -5,26 +5,47 @@ import ToolHeader from './ToolHeader';
|
||||
import Separator from './Separator';
|
||||
import AllTools from './allTools/AllTools';
|
||||
import { getToolsByCategory } from '@tools/index';
|
||||
import { capitalizeFirstLetter } from '../utils/string';
|
||||
import {
|
||||
capitalizeFirstLetter,
|
||||
getI18nNamespaceFromToolCategory
|
||||
} from '../utils/string';
|
||||
import { IconifyIcon } from '@iconify/react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { ToolCategory } from '@tools/defineTool';
|
||||
import { FullI18nKey } from '../i18n';
|
||||
|
||||
export default function ToolLayout({
|
||||
children,
|
||||
title,
|
||||
description,
|
||||
icon,
|
||||
type
|
||||
i18n,
|
||||
type,
|
||||
fullPath
|
||||
}: {
|
||||
title: string;
|
||||
description: string;
|
||||
icon?: IconifyIcon | string;
|
||||
type: string;
|
||||
type: ToolCategory;
|
||||
fullPath: string;
|
||||
children: ReactNode;
|
||||
i18n?: {
|
||||
name: FullI18nKey;
|
||||
description: FullI18nKey;
|
||||
shortDescription: FullI18nKey;
|
||||
};
|
||||
}) {
|
||||
const { t } = useTranslation([
|
||||
'translation',
|
||||
getI18nNamespaceFromToolCategory(type)
|
||||
]);
|
||||
|
||||
// Use i18n keys if available, otherwise fall back to provided strings
|
||||
//@ts-ignore
|
||||
const toolTitle: string = t(i18n.name);
|
||||
//@ts-ignore
|
||||
const toolDescription: string = t(i18n.description);
|
||||
|
||||
const otherCategoryTools =
|
||||
getToolsByCategory()
|
||||
getToolsByCategory(t)
|
||||
.find((category) => category.type === type)
|
||||
?.tools.filter((tool) => tool.name !== title)
|
||||
?.tools.filter((tool) => t(tool.name) !== toolTitle)
|
||||
.map((tool) => ({
|
||||
title: tool.name,
|
||||
description: tool.shortDescription,
|
||||
@@ -41,22 +62,25 @@ export default function ToolLayout({
|
||||
sx={{ backgroundColor: 'background.default' }}
|
||||
>
|
||||
<Helmet>
|
||||
<title>{`${title} - OmniTools`}</title>
|
||||
<title>{`${toolTitle} - OmniTools`}</title>
|
||||
</Helmet>
|
||||
<Box width={'85%'}>
|
||||
<ToolHeader
|
||||
title={title}
|
||||
description={description}
|
||||
title={toolTitle}
|
||||
description={toolDescription}
|
||||
icon={icon}
|
||||
type={type}
|
||||
path={fullPath}
|
||||
/>
|
||||
{children}
|
||||
<Separator backgroundColor="#5581b5" margin="50px" />
|
||||
<AllTools
|
||||
title={`All ${capitalizeFirstLetter(
|
||||
getToolsByCategory().find((category) => category.type === type)!
|
||||
.rawTitle
|
||||
)} tools`}
|
||||
title={t('translation:toolLayout.allToolsTitle', '', {
|
||||
type: capitalizeFirstLetter(
|
||||
getToolsByCategory(t).find((category) => category.type === type)!
|
||||
.title
|
||||
)
|
||||
})}
|
||||
toolCards={otherCategoryTools}
|
||||
/>
|
||||
</Box>
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
import { Box, Grid, Stack, Typography } from '@mui/material';
|
||||
import ToolCard from './ToolCard';
|
||||
import { IconifyIcon } from '@iconify/react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { FullI18nKey } from '../../i18n';
|
||||
|
||||
export interface ToolCardProps {
|
||||
title: string;
|
||||
description: string;
|
||||
title: FullI18nKey;
|
||||
description: FullI18nKey;
|
||||
link: string;
|
||||
icon: IconifyIcon | string;
|
||||
}
|
||||
@@ -15,6 +17,7 @@ interface AllToolsProps {
|
||||
}
|
||||
|
||||
export default function AllTools({ title, toolCards }: AllToolsProps) {
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<Box mt={4} mb={10}>
|
||||
<Typography mb={2} fontSize={30} color={'primary'}>
|
||||
@@ -25,8 +28,10 @@ export default function AllTools({ title, toolCards }: AllToolsProps) {
|
||||
{toolCards.map((card, index) => (
|
||||
<Grid item xs={12} md={6} lg={4} key={index}>
|
||||
<ToolCard
|
||||
title={card.title}
|
||||
description={card.description}
|
||||
//@ts-ignore
|
||||
title={t(card.title)}
|
||||
//@ts-ignore
|
||||
description={t(card.description)}
|
||||
link={card.link}
|
||||
icon={card.icon}
|
||||
/>
|
||||
|
||||
@@ -3,6 +3,7 @@ import ExampleCard, { ExampleCardProps } from './ExampleCard';
|
||||
import React from 'react';
|
||||
import { GetGroupsType } from '@components/options/ToolOptions';
|
||||
import { useFormikContext } from 'formik';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
export type CardExampleType<T> = Omit<
|
||||
ExampleCardProps<T>,
|
||||
@@ -24,6 +25,7 @@ export default function ToolExamples<T>({
|
||||
getGroups,
|
||||
setInput
|
||||
}: ExampleProps<T>) {
|
||||
const { t } = useTranslation();
|
||||
const { setValues } = useFormikContext<T>();
|
||||
|
||||
function changeInputResult(newInput: string | undefined, newOptions: T) {
|
||||
@@ -39,10 +41,10 @@ export default function ToolExamples<T>({
|
||||
<Box id={'examples'} mt={4}>
|
||||
<Box mt={4} display="flex" gap={1} alignItems="center">
|
||||
<Typography mb={2} fontSize={30} color={'primary'}>
|
||||
{`${title} Examples`}
|
||||
{t('toolExamples.title', { title })}
|
||||
</Typography>
|
||||
<Typography mb={2} fontSize={30} color={'secondary'}>
|
||||
{subtitle ?? 'Click to try!'}
|
||||
{subtitle ?? t('toolExamples.subtitle')}
|
||||
</Typography>
|
||||
</Box>
|
||||
|
||||
|
||||
@@ -12,6 +12,7 @@ import { globalInputHeight } from '../../config/uiConfig';
|
||||
import { CustomSnackBarContext } from '../../contexts/CustomSnackBarContext';
|
||||
import greyPattern from '@assets/grey-pattern.png';
|
||||
import { isArray } from 'lodash';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
interface BaseFileInputComponentProps extends BaseFileInputProps {
|
||||
children: (props: { preview: string | undefined }) => ReactNode;
|
||||
@@ -26,6 +27,7 @@ export default function BaseFileInput({
|
||||
children,
|
||||
type
|
||||
}: BaseFileInputComponentProps) {
|
||||
const { t } = useTranslation();
|
||||
const [preview, setPreview] = useState<string | null>(null);
|
||||
const [isDragging, setIsDragging] = useState<boolean>(false);
|
||||
const theme = useTheme();
|
||||
@@ -60,9 +62,9 @@ export default function BaseFileInput({
|
||||
|
||||
navigator.clipboard
|
||||
.write([clipboardItem])
|
||||
.then(() => showSnackBar('File copied', 'success'))
|
||||
.then(() => showSnackBar(t('baseFileInput.fileCopied'), 'success'))
|
||||
.catch((err) => {
|
||||
showSnackBar('Failed to copy: ' + err, 'error');
|
||||
showSnackBar(t('baseFileInput.copyFailed', { error: err }), 'error');
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -190,7 +192,7 @@ export default function BaseFileInput({
|
||||
variant="h6"
|
||||
align="center"
|
||||
>
|
||||
Drop your {type} here
|
||||
{t('baseFileInput.dropFileHere', { type })}
|
||||
</Typography>
|
||||
) : (
|
||||
<Typography
|
||||
@@ -200,9 +202,7 @@ export default function BaseFileInput({
|
||||
: theme.palette.grey['600']
|
||||
}
|
||||
>
|
||||
Click here to select a {type} from your device, press Ctrl+V to
|
||||
use a {type} from your clipboard, or drag and drop a file from
|
||||
desktop
|
||||
{t('baseFileInput.selectFileDescription', { type })}
|
||||
</Typography>
|
||||
)}
|
||||
</Box>
|
||||
|
||||
@@ -3,6 +3,7 @@ import Button from '@mui/material/Button';
|
||||
import PublishIcon from '@mui/icons-material/Publish';
|
||||
import ContentPasteIcon from '@mui/icons-material/ContentPaste';
|
||||
import ClearIcon from '@mui/icons-material/Clear';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
export default function InputFooter({
|
||||
handleImport,
|
||||
@@ -13,19 +14,21 @@ export default function InputFooter({
|
||||
handleCopy?: () => void;
|
||||
handleClear?: () => void;
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<Stack mt={1} direction={'row'} spacing={2}>
|
||||
<Button onClick={handleImport} startIcon={<PublishIcon />}>
|
||||
Import from file
|
||||
{t('inputFooter.importFromFile')}
|
||||
</Button>
|
||||
{handleCopy && (
|
||||
<Button onClick={handleCopy} startIcon={<ContentPasteIcon />}>
|
||||
Copy to clipboard
|
||||
{t('inputFooter.copyToClipboard')}
|
||||
</Button>
|
||||
)}
|
||||
{handleClear && (
|
||||
<Button onClick={handleClear} startIcon={<ClearIcon />}>
|
||||
Clear
|
||||
{t('inputFooter.clear')}
|
||||
</Button>
|
||||
)}
|
||||
</Stack>
|
||||
|
||||
@@ -2,6 +2,7 @@ import React, { useState, useEffect } from 'react';
|
||||
import { Grid, Select, MenuItem } from '@mui/material';
|
||||
import TextFieldWithDesc from '@components/options/TextFieldWithDesc';
|
||||
import Qty from 'js-quantities';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
//
|
||||
|
||||
const siPrefixes: { [key: string]: number } = {
|
||||
@@ -23,6 +24,7 @@ export default function NumericInputWithUnit(props: {
|
||||
onOwnChange?: (value: { value: number; unit: string }) => void;
|
||||
defaultPrefix?: string;
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
const [inputValue, setInputValue] = useState(props.value.value);
|
||||
const [prefix, setPrefix] = useState(props.defaultPrefix || 'Default prefix');
|
||||
|
||||
@@ -158,7 +160,7 @@ export default function NumericInputWithUnit(props: {
|
||||
<Select
|
||||
fullWidth
|
||||
disabled={disableChangingUnit}
|
||||
placeholder={'Unit'}
|
||||
placeholder={t('numericInputWithUnit.unit')}
|
||||
sx={{ width: { xs: '75%', sm: '80%', md: '90%' } }}
|
||||
value={unit}
|
||||
onChange={(event) => {
|
||||
|
||||
@@ -6,6 +6,7 @@ import InputFooter from './InputFooter';
|
||||
import { CustomSnackBarContext } from '../../contexts/CustomSnackBarContext';
|
||||
import { isArray } from 'lodash';
|
||||
import MusicNoteIcon from '@mui/icons-material/MusicNote';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
interface MultiAudioInputComponentProps {
|
||||
accept: string[];
|
||||
@@ -27,7 +28,10 @@ export default function ToolMultipleAudioInput({
|
||||
title,
|
||||
type
|
||||
}: MultiAudioInputComponentProps) {
|
||||
const { t } = useTranslation();
|
||||
const theme = useTheme();
|
||||
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||
const { showSnackBar } = useContext(CustomSnackBarContext);
|
||||
|
||||
const handleFileChange = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const files = event.target.files;
|
||||
@@ -93,7 +97,12 @@ export default function ToolMultipleAudioInput({
|
||||
return (
|
||||
<Box>
|
||||
<InputHeader
|
||||
title={title || 'Input ' + type.charAt(0).toUpperCase() + type.slice(1)}
|
||||
title={
|
||||
title ||
|
||||
t('toolMultipleAudioInput.inputTitle', {
|
||||
type: type.charAt(0).toUpperCase() + type.slice(1)
|
||||
})
|
||||
}
|
||||
/>
|
||||
<Box
|
||||
sx={{
|
||||
@@ -152,7 +161,7 @@ export default function ToolMultipleAudioInput({
|
||||
))
|
||||
) : (
|
||||
<Typography variant="body2" color="text.secondary">
|
||||
No files selected
|
||||
{t('toolMultipleAudioInput.noFilesSelected')}
|
||||
</Typography>
|
||||
)}
|
||||
</Box>
|
||||
|
||||
@@ -6,6 +6,7 @@ import InputFooter from './InputFooter';
|
||||
import { CustomSnackBarContext } from '../../contexts/CustomSnackBarContext';
|
||||
import { isArray } from 'lodash';
|
||||
import PictureAsPdfIcon from '@mui/icons-material/PictureAsPdf';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
interface MultiPdfInputComponentProps {
|
||||
accept: string[];
|
||||
@@ -27,6 +28,7 @@ export default function ToolMultiFileInput({
|
||||
title,
|
||||
type
|
||||
}: MultiPdfInputComponentProps) {
|
||||
const { t } = useTranslation();
|
||||
const theme = useTheme();
|
||||
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||
const { showSnackBar } = useContext(CustomSnackBarContext);
|
||||
@@ -96,7 +98,12 @@ export default function ToolMultiFileInput({
|
||||
return (
|
||||
<Box>
|
||||
<InputHeader
|
||||
title={title || 'Input ' + type.charAt(0).toUpperCase() + type.slice(1)}
|
||||
title={
|
||||
title ||
|
||||
t('toolMultiplePdfInput.inputTitle', {
|
||||
type: type.charAt(0).toUpperCase() + type.slice(1)
|
||||
})
|
||||
}
|
||||
/>
|
||||
<Box
|
||||
sx={{
|
||||
@@ -156,7 +163,7 @@ export default function ToolMultiFileInput({
|
||||
))
|
||||
) : (
|
||||
<Typography variant="body2" color="text.secondary">
|
||||
No files selected
|
||||
{t('toolMultiplePdfInput.noFilesSelected')}
|
||||
</Typography>
|
||||
)}
|
||||
</Box>
|
||||
|
||||
@@ -3,6 +3,7 @@ import React, { useContext, useRef } from 'react';
|
||||
import { CustomSnackBarContext } from '../../contexts/CustomSnackBarContext';
|
||||
import InputHeader from '../InputHeader';
|
||||
import InputFooter from './InputFooter';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
export default function ToolTextInput({
|
||||
value,
|
||||
@@ -15,15 +16,16 @@ export default function ToolTextInput({
|
||||
onChange: (value: string) => void;
|
||||
placeholder?: string;
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
const { showSnackBar } = useContext(CustomSnackBarContext);
|
||||
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
const handleCopy = () => {
|
||||
navigator.clipboard
|
||||
.writeText(value)
|
||||
.then(() => showSnackBar('Text copied', 'success'))
|
||||
.then(() => showSnackBar(t('toolTextInput.copied'), 'success'))
|
||||
.catch((err) => {
|
||||
showSnackBar('Failed to copy: ' + err, 'error');
|
||||
showSnackBar(t('toolTextInput.copyFailed', { error: err }), 'error');
|
||||
});
|
||||
};
|
||||
const handleFileChange = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
@@ -45,14 +47,14 @@ export default function ToolTextInput({
|
||||
};
|
||||
return (
|
||||
<Box>
|
||||
<InputHeader title={title} />
|
||||
<InputHeader title={title || t('toolTextInput.input')} />
|
||||
<TextField
|
||||
value={value}
|
||||
onChange={(event) => onChange(event.target.value)}
|
||||
fullWidth
|
||||
multiline
|
||||
rows={10}
|
||||
placeholder={placeholder}
|
||||
placeholder={placeholder || t('toolTextInput.placeholder')}
|
||||
sx={{
|
||||
'&.MuiTextField-root': {
|
||||
backgroundColor: 'background.paper'
|
||||
|
||||
@@ -4,6 +4,7 @@ import Typography from '@mui/material/Typography';
|
||||
import React, { ReactNode } from 'react';
|
||||
import { FormikProps, FormikValues, useFormikContext } from 'formik';
|
||||
import ToolOptionGroups, { ToolOptionGroup } from './ToolOptionGroups';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
export type UpdateField<T> = <Y extends keyof T>(field: Y, value: T[Y]) => void;
|
||||
type NonEmptyArray<T> = [T, ...T[]];
|
||||
@@ -20,6 +21,7 @@ export default function ToolOptions<T extends FormikValues>({
|
||||
getGroups: GetGroupsType<T> | null;
|
||||
vertical?: boolean;
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
const theme = useTheme();
|
||||
const formikContext = useFormikContext<T>();
|
||||
|
||||
@@ -45,7 +47,7 @@ export default function ToolOptions<T extends FormikValues>({
|
||||
>
|
||||
<Stack direction={'row'} spacing={1} alignItems={'center'}>
|
||||
<SettingsIcon />
|
||||
<Typography fontSize={22}>Tool options</Typography>
|
||||
<Typography fontSize={22}>{t('toolOptions.title')}</Typography>
|
||||
</Stack>
|
||||
<Box mt={2}>
|
||||
<Stack direction={'row'} spacing={2}>
|
||||
|
||||
@@ -3,13 +3,14 @@ import Button from '@mui/material/Button';
|
||||
import DownloadIcon from '@mui/icons-material/Download';
|
||||
import ContentPasteIcon from '@mui/icons-material/ContentPaste';
|
||||
import React from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
export default function ResultFooter({
|
||||
handleDownload,
|
||||
handleCopy,
|
||||
disabled,
|
||||
hideCopy,
|
||||
downloadLabel = 'Download'
|
||||
downloadLabel
|
||||
}: {
|
||||
handleDownload: () => void;
|
||||
handleCopy?: () => void;
|
||||
@@ -17,6 +18,7 @@ export default function ResultFooter({
|
||||
hideCopy?: boolean;
|
||||
downloadLabel?: string;
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<Stack mt={1} direction={'row'} spacing={2}>
|
||||
<Button
|
||||
@@ -24,7 +26,7 @@ export default function ResultFooter({
|
||||
onClick={handleDownload}
|
||||
startIcon={<DownloadIcon />}
|
||||
>
|
||||
{downloadLabel}
|
||||
{downloadLabel || t('resultFooter.download')}
|
||||
</Button>
|
||||
{!hideCopy && (
|
||||
<Button
|
||||
@@ -32,7 +34,7 @@ export default function ResultFooter({
|
||||
onClick={handleCopy}
|
||||
startIcon={<ContentPasteIcon />}
|
||||
>
|
||||
Copy to clipboard
|
||||
{t('resultFooter.copy')}
|
||||
</Button>
|
||||
)}
|
||||
</Stack>
|
||||
|
||||
@@ -5,6 +5,7 @@ import greyPattern from '@assets/grey-pattern.png';
|
||||
import { globalInputHeight } from '../../config/uiConfig';
|
||||
import ResultFooter from './ResultFooter';
|
||||
import { CustomSnackBarContext } from '../../contexts/CustomSnackBarContext';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
export default function ToolFileResult({
|
||||
title = 'Result',
|
||||
@@ -19,6 +20,7 @@ export default function ToolFileResult({
|
||||
loading?: boolean;
|
||||
loadingText?: string;
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
const [preview, setPreview] = React.useState<string | null>(null);
|
||||
const { showSnackBar } = useContext(CustomSnackBarContext);
|
||||
const theme = useTheme();
|
||||
@@ -41,9 +43,9 @@ export default function ToolFileResult({
|
||||
|
||||
navigator.clipboard
|
||||
.write([clipboardItem])
|
||||
.then(() => showSnackBar('File copied', 'success'))
|
||||
.then(() => showSnackBar(t('toolFileResult.copied'), 'success'))
|
||||
.catch((err) => {
|
||||
showSnackBar('Failed to copy: ' + err, 'error');
|
||||
showSnackBar(t('toolFileResult.copyFailed', { error: err }), 'error');
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -91,7 +93,7 @@ export default function ToolFileResult({
|
||||
|
||||
return (
|
||||
<Box>
|
||||
<InputHeader title={title} />
|
||||
<InputHeader title={title || t('toolFileResult.result')} />
|
||||
<Box
|
||||
sx={{
|
||||
width: '100%',
|
||||
@@ -114,7 +116,7 @@ export default function ToolFileResult({
|
||||
>
|
||||
<CircularProgress />
|
||||
<Typography variant="body2" sx={{ mt: 2 }}>
|
||||
{loadingText}... This may take a moment.
|
||||
{loadingText || t('toolFileResult.loading')}
|
||||
</Typography>
|
||||
</Box>
|
||||
) : (
|
||||
|
||||
@@ -9,8 +9,11 @@ import InputHeader from '../InputHeader';
|
||||
import greyPattern from '@assets/grey-pattern.png';
|
||||
import { globalInputHeight } from '../../config/uiConfig';
|
||||
import ResultFooter from './ResultFooter';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import React, { useContext } from 'react';
|
||||
import { CustomSnackBarContext } from '../../contexts/CustomSnackBarContext';
|
||||
|
||||
export default function ToolFileResult({
|
||||
export default function ToolMultiFileResult({
|
||||
title = 'Result',
|
||||
value,
|
||||
zipFile,
|
||||
@@ -23,7 +26,9 @@ export default function ToolFileResult({
|
||||
loading?: boolean;
|
||||
loadingText?: string;
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
const theme = useTheme();
|
||||
const { showSnackBar } = useContext(CustomSnackBarContext);
|
||||
|
||||
const getFileType = (
|
||||
file: File
|
||||
@@ -46,9 +51,25 @@ export default function ToolFileResult({
|
||||
URL.revokeObjectURL(url);
|
||||
};
|
||||
|
||||
const handleCopy = () => {
|
||||
if (zipFile) {
|
||||
const blob = new Blob([zipFile], { type: zipFile.type });
|
||||
const clipboardItem = new ClipboardItem({ [zipFile.type]: blob });
|
||||
navigator.clipboard
|
||||
.write([clipboardItem])
|
||||
.then(() => showSnackBar(t('toolMultiFileResult.copied'), 'success'))
|
||||
.catch((err) => {
|
||||
showSnackBar(
|
||||
t('toolMultiFileResult.copyFailed', { error: err }),
|
||||
'error'
|
||||
);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Box>
|
||||
<InputHeader title={title} />
|
||||
<InputHeader title={title || t('toolMultiFileResult.result')} />
|
||||
<Box
|
||||
sx={{
|
||||
width: '100%',
|
||||
@@ -77,7 +98,7 @@ export default function ToolFileResult({
|
||||
>
|
||||
<CircularProgress />
|
||||
<Typography variant="body2" sx={{ mt: 2 }}>
|
||||
{loadingText}... This may take a moment.
|
||||
{loadingText || t('toolMultiFileResult.loading')}
|
||||
</Typography>
|
||||
</Box>
|
||||
) : (
|
||||
|
||||
@@ -6,6 +6,7 @@ import ResultFooter from './ResultFooter';
|
||||
import { replaceSpecialCharacters } from '@utils/string';
|
||||
import mime from 'mime';
|
||||
import { globalInputHeight } from '../../config/uiConfig';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
export default function ToolTextResult({
|
||||
title = 'Result',
|
||||
@@ -20,13 +21,14 @@ export default function ToolTextResult({
|
||||
keepSpecialCharacters?: boolean;
|
||||
loading?: boolean;
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
const { showSnackBar } = useContext(CustomSnackBarContext);
|
||||
const handleCopy = () => {
|
||||
navigator.clipboard
|
||||
.writeText(value)
|
||||
.then(() => showSnackBar('Text copied', 'success'))
|
||||
.then(() => showSnackBar(t('toolTextResult.copied'), 'success'))
|
||||
.catch((err) => {
|
||||
showSnackBar('Failed to copy: ' + err, 'error');
|
||||
showSnackBar(t('toolTextResult.copyFailed', { error: err }), 'error');
|
||||
});
|
||||
};
|
||||
const handleDownload = () => {
|
||||
@@ -48,7 +50,7 @@ export default function ToolTextResult({
|
||||
};
|
||||
return (
|
||||
<Box>
|
||||
<InputHeader title={title} />
|
||||
<InputHeader title={title || t('toolTextResult.result')} />
|
||||
{loading ? (
|
||||
<Box
|
||||
sx={{
|
||||
@@ -61,7 +63,7 @@ export default function ToolTextResult({
|
||||
>
|
||||
<CircularProgress />
|
||||
<Typography variant="body2" sx={{ mt: 2 }}>
|
||||
Loading... This may take a moment.
|
||||
{t('toolTextResult.loading')}
|
||||
</Typography>
|
||||
</Box>
|
||||
) : (
|
||||
|
||||
39
src/i18n/index.ts
Normal file
39
src/i18n/index.ts
Normal file
@@ -0,0 +1,39 @@
|
||||
import i18n, { Namespace, ParseKeys } from 'i18next';
|
||||
import { initReactI18next } from 'react-i18next';
|
||||
import Backend from 'i18next-http-backend';
|
||||
|
||||
export const validNamespaces = [
|
||||
'string',
|
||||
'number',
|
||||
'video',
|
||||
'list',
|
||||
'json',
|
||||
'time',
|
||||
'csv',
|
||||
'pdf',
|
||||
'audio',
|
||||
'xml',
|
||||
'translation',
|
||||
'image'
|
||||
] as const satisfies readonly Namespace[];
|
||||
|
||||
export type I18nNamespaces = (typeof validNamespaces)[number];
|
||||
export type FullI18nKey = {
|
||||
[K in I18nNamespaces]: `${K}:${ParseKeys<K>}`;
|
||||
}[I18nNamespaces];
|
||||
|
||||
i18n
|
||||
.use(Backend)
|
||||
.use(initReactI18next)
|
||||
.init({
|
||||
lng: localStorage.getItem('lang') || 'en',
|
||||
fallbackLng: 'en',
|
||||
interpolation: {
|
||||
escapeValue: false // react already safes from xss => https://www.i18next.com/translation-function/interpolation#unescape
|
||||
},
|
||||
backend: {
|
||||
loadPath: '/locales/{{lng}}/{{ns}}.json'
|
||||
}
|
||||
});
|
||||
|
||||
export default i18n;
|
||||
@@ -7,6 +7,9 @@ import Button from '@mui/material/Button';
|
||||
import { useState } from 'react';
|
||||
import { categoriesColors } from 'config/uiConfig';
|
||||
import { Icon } from '@iconify/react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { getI18nNamespaceFromToolCategory } from '@utils/string';
|
||||
import { validNamespaces } from '../../i18n';
|
||||
|
||||
type ArrayElement<ArrayType extends readonly unknown[]> =
|
||||
ArrayType extends readonly (infer ElementType)[] ? ElementType : never;
|
||||
@@ -18,10 +21,26 @@ const SingleCategory = function ({
|
||||
category: ArrayElement<ReturnType<typeof getToolsByCategory>>;
|
||||
index: number;
|
||||
}) {
|
||||
const { t } = useTranslation(getI18nNamespaceFromToolCategory(category.type));
|
||||
const navigate = useNavigate();
|
||||
const theme = useTheme();
|
||||
const [hovered, setHovered] = useState<boolean>(false);
|
||||
const toggleHover = () => setHovered((prevState) => !prevState);
|
||||
|
||||
// Get translated category title and description
|
||||
const categoryTitle = t(`categories.${category.type}.title`, category.title);
|
||||
const categoryDescription = t(
|
||||
`categories.${category.type}.description`,
|
||||
category.description
|
||||
);
|
||||
const seeAllText = t('translation:categories.seeAll', 'See all {{title}}', {
|
||||
title: categoryTitle
|
||||
});
|
||||
const tryText = t('translation:categories.try', 'Try {{title}}', {
|
||||
//@ts-ignore
|
||||
title: t(category.example.title)
|
||||
});
|
||||
|
||||
return (
|
||||
<Grid
|
||||
item
|
||||
@@ -60,10 +79,10 @@ const SingleCategory = function ({
|
||||
}}
|
||||
to={'/categories/' + category.type}
|
||||
>
|
||||
{category.title}
|
||||
{categoryTitle}
|
||||
</Link>
|
||||
</Stack>
|
||||
<Typography sx={{ mt: 2 }}>{category.description}</Typography>
|
||||
<Typography sx={{ mt: 2 }}>{categoryDescription}</Typography>
|
||||
</Box>
|
||||
<Grid mt={1} container spacing={2}>
|
||||
<Grid item xs={12} md={6}>
|
||||
@@ -71,7 +90,9 @@ const SingleCategory = function ({
|
||||
fullWidth
|
||||
onClick={() => navigate('/categories/' + category.type)}
|
||||
variant={'contained'}
|
||||
>{`See all ${category.title}`}</Button>
|
||||
>
|
||||
{seeAllText}
|
||||
</Button>
|
||||
</Grid>
|
||||
<Grid item xs={12} md={6}>
|
||||
<Button
|
||||
@@ -79,7 +100,9 @@ const SingleCategory = function ({
|
||||
fullWidth
|
||||
onClick={() => navigate(category.example.path)}
|
||||
variant={'outlined'}
|
||||
>{`Try ${category.example.title}`}</Button>
|
||||
>
|
||||
{tryText}
|
||||
</Button>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Stack>
|
||||
@@ -89,9 +112,10 @@ const SingleCategory = function ({
|
||||
);
|
||||
};
|
||||
export default function Categories() {
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<Grid width={'80%'} container mt={2} spacing={2}>
|
||||
{getToolsByCategory().map((category, index) => (
|
||||
{getToolsByCategory(t).map((category, index) => (
|
||||
<SingleCategory key={category.type} category={category} index={index} />
|
||||
))}
|
||||
</Grid>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { Box, useTheme } from '@mui/material';
|
||||
import Hero from 'components/Hero';
|
||||
import Categories from './Categories';
|
||||
import { Helmet } from 'react-helmet';
|
||||
|
||||
export default function Home() {
|
||||
const theme = useTheme();
|
||||
@@ -25,6 +26,7 @@ export default function Home() {
|
||||
justifyContent={'center'}
|
||||
width={'100%'}
|
||||
>
|
||||
<Helmet title={'OmniTools'} />
|
||||
<Hero />
|
||||
<Categories />
|
||||
</Box>
|
||||
|
||||
@@ -2,8 +2,8 @@ import {
|
||||
Box,
|
||||
Divider,
|
||||
Stack,
|
||||
TextField,
|
||||
styled,
|
||||
TextField,
|
||||
useTheme
|
||||
} from '@mui/material';
|
||||
import Grid from '@mui/material/Grid';
|
||||
@@ -11,16 +11,19 @@ import Typography from '@mui/material/Typography';
|
||||
import { Link, useNavigate, useParams } from 'react-router-dom';
|
||||
import { filterTools, getToolsByCategory } from '../../tools';
|
||||
import Hero from 'components/Hero';
|
||||
import { capitalizeFirstLetter, getToolCategoryTitle } from '@utils/string';
|
||||
import {
|
||||
getI18nNamespaceFromToolCategory,
|
||||
getToolCategoryTitle
|
||||
} from '@utils/string';
|
||||
import { Icon } from '@iconify/react';
|
||||
import { categoriesColors } from 'config/uiConfig';
|
||||
import React, { useEffect } from 'react';
|
||||
import IconButton from '@mui/material/IconButton';
|
||||
import { ArrowBack } from '@mui/icons-material';
|
||||
import BackButton from '@components/BackButton';
|
||||
import ArrowBackIcon from '@mui/icons-material/ArrowBack';
|
||||
import SearchIcon from '@mui/icons-material/Search';
|
||||
import { Helmet } from 'react-helmet';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { I18nNamespaces, validNamespaces } from '../../i18n';
|
||||
|
||||
const StyledLink = styled(Link)(({ theme }) => ({
|
||||
'&:hover': {
|
||||
@@ -33,7 +36,14 @@ export default function ToolsByCategory() {
|
||||
const mainContentRef = React.useRef<HTMLDivElement>(null);
|
||||
const { categoryName } = useParams();
|
||||
const [searchTerm, setSearchTerm] = React.useState<string>('');
|
||||
const rawTitle = getToolCategoryTitle(categoryName as string);
|
||||
const { t } = useTranslation(validNamespaces);
|
||||
const rawTitle = getToolCategoryTitle(categoryName as string, t);
|
||||
// First get tools by category without filtering
|
||||
const toolsByCategory =
|
||||
getToolsByCategory(t).find(({ type }) => type === categoryName)?.tools ??
|
||||
[];
|
||||
|
||||
const categoryTools = filterTools(toolsByCategory, searchTerm, t);
|
||||
|
||||
useEffect(() => {
|
||||
if (mainContentRef.current) {
|
||||
@@ -44,7 +54,7 @@ export default function ToolsByCategory() {
|
||||
return (
|
||||
<Box sx={{ backgroundColor: 'background.default' }}>
|
||||
<Helmet>
|
||||
<title>{`${rawTitle} Tools`}</title>
|
||||
<title>{rawTitle}</title>
|
||||
</Helmet>
|
||||
<Box
|
||||
padding={{ xs: 1, md: 3, lg: 5 }}
|
||||
@@ -63,10 +73,9 @@ export default function ToolsByCategory() {
|
||||
<IconButton onClick={() => navigate('/')}>
|
||||
<ArrowBackIcon color={'primary'} />
|
||||
</IconButton>
|
||||
<Typography
|
||||
fontSize={22}
|
||||
color={theme.palette.primary.main}
|
||||
>{`All ${rawTitle} Tools`}</Typography>
|
||||
<Typography fontSize={22} color={theme.palette.primary.main}>
|
||||
{t('translation:toolLayout.allToolsTitle', { type: rawTitle })}
|
||||
</Typography>
|
||||
</Stack>
|
||||
<TextField
|
||||
placeholder={'Search'}
|
||||
@@ -82,11 +91,7 @@ export default function ToolsByCategory() {
|
||||
/>
|
||||
</Stack>
|
||||
<Grid container spacing={2} mt={2}>
|
||||
{filterTools(
|
||||
getToolsByCategory().find(({ type }) => type === categoryName)
|
||||
?.tools ?? [],
|
||||
searchTerm
|
||||
).map((tool, index) => (
|
||||
{categoryTools.map((tool, index) => (
|
||||
<Grid item xs={12} md={6} lg={4} key={tool.path}>
|
||||
<Stack
|
||||
sx={{
|
||||
@@ -120,10 +125,12 @@ export default function ToolsByCategory() {
|
||||
}}
|
||||
to={'/' + tool.path}
|
||||
>
|
||||
{tool.name}
|
||||
{/*@ts-ignore*/}
|
||||
{t(tool.name)}
|
||||
</StyledLink>
|
||||
<Typography sx={{ mt: 2 }}>
|
||||
{tool.shortDescription}
|
||||
{/*@ts-ignore*/}
|
||||
{t(tool.shortDescription)}
|
||||
</Typography>
|
||||
</Box>
|
||||
</Stack>
|
||||
|
||||
@@ -9,6 +9,7 @@ import ToolFileResult from '@components/result/ToolFileResult';
|
||||
import TextFieldWithDesc from '@components/options/TextFieldWithDesc';
|
||||
import RadioWithTextField from '@components/options/RadioWithTextField';
|
||||
import { changeAudioSpeed } from './service';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
const initialValues: InitialValuesType = {
|
||||
newSpeed: 2,
|
||||
@@ -25,6 +26,7 @@ export default function ChangeSpeed({
|
||||
title,
|
||||
longDescription
|
||||
}: ToolComponentProps) {
|
||||
const { t } = useTranslation('audio');
|
||||
const [input, setInput] = useState<File | null>(null);
|
||||
const [result, setResult] = useState<File | null>(null);
|
||||
const [loading, setLoading] = useState(false);
|
||||
@@ -49,20 +51,20 @@ export default function ChangeSpeed({
|
||||
updateField
|
||||
}) => [
|
||||
{
|
||||
title: 'New Audio Speed',
|
||||
title: t('changeSpeed.newAudioSpeed'),
|
||||
component: (
|
||||
<Box>
|
||||
<TextFieldWithDesc
|
||||
value={values.newSpeed.toString()}
|
||||
onOwnChange={(val) => updateField('newSpeed', Number(val))}
|
||||
description="Default multiplier: 2 means 2x faster"
|
||||
description={t('changeSpeed.speedDescription')}
|
||||
type="number"
|
||||
/>
|
||||
</Box>
|
||||
)
|
||||
},
|
||||
{
|
||||
title: 'Output Format',
|
||||
title: t('changeSpeed.outputFormat'),
|
||||
component: (
|
||||
<Box mt={2}>
|
||||
<RadioGroup
|
||||
@@ -96,15 +98,19 @@ export default function ChangeSpeed({
|
||||
<ToolAudioInput
|
||||
value={input}
|
||||
onChange={setInput}
|
||||
title={'Input Audio'}
|
||||
title={t('changeSpeed.inputTitle')}
|
||||
/>
|
||||
}
|
||||
resultComponent={
|
||||
loading ? (
|
||||
<ToolFileResult title="Setting Speed" value={null} loading={true} />
|
||||
<ToolFileResult
|
||||
title={t('changeSpeed.settingSpeed')}
|
||||
value={null}
|
||||
loading={true}
|
||||
/>
|
||||
) : (
|
||||
<ToolFileResult
|
||||
title="Edited Audio"
|
||||
title={t('changeSpeed.resultTitle')}
|
||||
value={result}
|
||||
extension={result ? result.name.split('.').pop() : undefined}
|
||||
/>
|
||||
@@ -114,7 +120,10 @@ export default function ChangeSpeed({
|
||||
getGroups={getGroups}
|
||||
setInput={setInput}
|
||||
compute={compute}
|
||||
toolInfo={{ title: `What is ${title}?`, description: longDescription }}
|
||||
toolInfo={{
|
||||
title: t('changeSpeed.toolInfo.title', { title }),
|
||||
description: longDescription
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -2,12 +2,24 @@ import { defineTool } from '@tools/defineTool';
|
||||
import { lazy } from 'react';
|
||||
|
||||
export const tool = defineTool('audio', {
|
||||
name: 'Change speed',
|
||||
path: 'change-speed',
|
||||
icon: 'material-symbols-light:speed-outline',
|
||||
description:
|
||||
'This online utility lets you change the speed of an audio. You can speed it up or slow it down.',
|
||||
shortDescription: 'Quickly change audio speed',
|
||||
keywords: ['change', 'speed'],
|
||||
component: lazy(() => import('./index'))
|
||||
icon: 'material-symbols:speed',
|
||||
|
||||
keywords: [
|
||||
'audio',
|
||||
'speed',
|
||||
'tempo',
|
||||
'playback',
|
||||
'accelerate',
|
||||
'slow down',
|
||||
'pitch',
|
||||
'media'
|
||||
],
|
||||
|
||||
component: lazy(() => import('./index')),
|
||||
i18n: {
|
||||
name: 'audio:changeSpeed.title',
|
||||
description: 'audio:changeSpeed.description',
|
||||
shortDescription: 'audio:changeSpeed.shortDescription'
|
||||
}
|
||||
});
|
||||
|
||||
@@ -8,6 +8,7 @@ import ToolVideoInput from '@components/input/ToolVideoInput';
|
||||
import { GetGroupsType } from '@components/options/ToolOptions';
|
||||
import ToolFileResult from '@components/result/ToolFileResult';
|
||||
import SelectWithDesc from '@components/options/SelectWithDesc';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
const initialValues: InitialValuesType = {
|
||||
outputFormat: 'aac'
|
||||
@@ -17,6 +18,7 @@ export default function ExtractAudio({
|
||||
title,
|
||||
longDescription
|
||||
}: ToolComponentProps) {
|
||||
const { t } = useTranslation('audio');
|
||||
const [file, setFile] = useState<File | null>(null);
|
||||
const [audioFile, setAudioFile] = useState<File | null>(null);
|
||||
const [loading, setLoading] = useState(false);
|
||||
@@ -27,7 +29,7 @@ export default function ExtractAudio({
|
||||
}) => {
|
||||
return [
|
||||
{
|
||||
title: 'Output Format',
|
||||
title: t('extractAudio.outputFormat'),
|
||||
component: (
|
||||
<Box>
|
||||
<SelectWithDesc
|
||||
@@ -40,9 +42,7 @@ export default function ExtractAudio({
|
||||
{ label: 'MP3', value: 'mp3' },
|
||||
{ label: 'WAV', value: 'wav' }
|
||||
]}
|
||||
description={
|
||||
'Select the format for the audio to be extracted as.'
|
||||
}
|
||||
description={t('extractAudio.outputFormatDescription')}
|
||||
/>
|
||||
</Box>
|
||||
)
|
||||
@@ -68,23 +68,33 @@ export default function ExtractAudio({
|
||||
title={title}
|
||||
input={file}
|
||||
inputComponent={
|
||||
<ToolVideoInput value={file} onChange={setFile} title={'Input Video'} />
|
||||
<ToolVideoInput
|
||||
value={file}
|
||||
onChange={setFile}
|
||||
title={t('extractAudio.inputTitle')}
|
||||
/>
|
||||
}
|
||||
resultComponent={
|
||||
loading ? (
|
||||
<ToolFileResult
|
||||
title={'Extracting Audio'}
|
||||
title={t('extractAudio.extractingAudio')}
|
||||
value={null}
|
||||
loading={true}
|
||||
/>
|
||||
) : (
|
||||
<ToolFileResult title={'Extracted Audio'} value={audioFile} />
|
||||
<ToolFileResult
|
||||
title={t('extractAudio.resultTitle')}
|
||||
value={audioFile}
|
||||
/>
|
||||
)
|
||||
}
|
||||
initialValues={initialValues}
|
||||
getGroups={getGroups}
|
||||
compute={compute}
|
||||
toolInfo={{ title: `What is ${title}?`, description: longDescription }}
|
||||
toolInfo={{
|
||||
title: t('extractAudio.toolInfo.title', { title }),
|
||||
description: longDescription
|
||||
}}
|
||||
setInput={setFile}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -2,13 +2,9 @@ import { defineTool } from '@tools/defineTool';
|
||||
import { lazy } from 'react';
|
||||
|
||||
export const tool = defineTool('audio', {
|
||||
name: 'Extract audio',
|
||||
path: 'extract-audio',
|
||||
icon: 'mdi:music-note',
|
||||
description:
|
||||
'Extract the audio track from a video file and save it as a separate audio file in your chosen format (AAC, MP3, WAV).',
|
||||
shortDescription:
|
||||
'Extract audio from video files (MP4, MOV, etc.) to AAC, MP3, or WAV.',
|
||||
|
||||
keywords: [
|
||||
'extract',
|
||||
'audio',
|
||||
@@ -20,7 +16,11 @@ export const tool = defineTool('audio', {
|
||||
'media',
|
||||
'convert'
|
||||
],
|
||||
longDescription:
|
||||
'This tool allows you to extract the audio track from a video file (such as MP4, MOV, AVI, etc.) and save it as a standalone audio file in your preferred format (AAC, MP3, or WAV). Useful for podcasts, music, or any scenario where you need just the audio from a video.',
|
||||
component: lazy(() => import('./index'))
|
||||
|
||||
component: lazy(() => import('./index')),
|
||||
i18n: {
|
||||
name: 'audio:extractAudio.title',
|
||||
description: 'audio:extractAudio.description',
|
||||
shortDescription: 'audio:extractAudio.shortDescription'
|
||||
}
|
||||
});
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { Box, FormControlLabel, Radio, RadioGroup } from '@mui/material';
|
||||
import React, { useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import ToolContent from '@components/ToolContent';
|
||||
import { ToolComponentProps } from '@tools/defineTool';
|
||||
import { GetGroupsType } from '@components/options/ToolOptions';
|
||||
@@ -24,6 +25,7 @@ export default function MergeAudio({
|
||||
title,
|
||||
longDescription
|
||||
}: ToolComponentProps) {
|
||||
const { t } = useTranslation('audio');
|
||||
const [input, setInput] = useState<MultiAudioInput[]>([]);
|
||||
const [result, setResult] = useState<File | null>(null);
|
||||
const [loading, setLoading] = useState(false);
|
||||
@@ -51,7 +53,7 @@ export default function MergeAudio({
|
||||
updateField
|
||||
}) => [
|
||||
{
|
||||
title: 'Output Format',
|
||||
title: t('mergeAudio.outputFormat'),
|
||||
component: (
|
||||
<Box mt={2}>
|
||||
<RadioGroup
|
||||
@@ -87,16 +89,20 @@ export default function MergeAudio({
|
||||
value={input}
|
||||
onChange={setInput}
|
||||
accept={['audio/*', '.mp3', '.wav', '.aac']}
|
||||
title={'Input Audio Files'}
|
||||
title={t('mergeAudio.inputTitle')}
|
||||
type="audio"
|
||||
/>
|
||||
}
|
||||
resultComponent={
|
||||
loading ? (
|
||||
<ToolFileResult title="Merging Audio" value={null} loading={true} />
|
||||
<ToolFileResult
|
||||
title={t('mergeAudio.mergingAudio')}
|
||||
value={null}
|
||||
loading={true}
|
||||
/>
|
||||
) : (
|
||||
<ToolFileResult
|
||||
title="Merged Audio"
|
||||
title={t('mergeAudio.resultTitle')}
|
||||
value={result}
|
||||
extension={result ? result.name.split('.').pop() : undefined}
|
||||
/>
|
||||
@@ -106,7 +112,10 @@ export default function MergeAudio({
|
||||
getGroups={getGroups}
|
||||
setInput={setInput}
|
||||
compute={compute}
|
||||
toolInfo={{ title: `What is ${title}?`, description: longDescription }}
|
||||
toolInfo={{
|
||||
title: t('mergeAudio.toolInfo.title', { title }),
|
||||
description: longDescription
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -2,12 +2,16 @@ import { defineTool } from '@tools/defineTool';
|
||||
import { lazy } from 'react';
|
||||
|
||||
export const tool = defineTool('audio', {
|
||||
name: 'Merge Audio',
|
||||
i18n: {
|
||||
name: 'audio:mergeAudio.title',
|
||||
description: 'audio:mergeAudio.description',
|
||||
shortDescription: 'audio:mergeAudio.shortDescription',
|
||||
longDescription: 'audio:mergeAudio.longDescription'
|
||||
},
|
||||
|
||||
path: 'merge-audio',
|
||||
icon: 'fluent:merge-20-regular',
|
||||
description:
|
||||
'Combine multiple audio files into a single audio file by concatenating them in sequence.',
|
||||
shortDescription: 'Merge multiple audio files into one (MP3, AAC, WAV).',
|
||||
|
||||
keywords: [
|
||||
'merge',
|
||||
'audio',
|
||||
@@ -20,7 +24,6 @@ export const tool = defineTool('audio', {
|
||||
'audio editing',
|
||||
'multiple files'
|
||||
],
|
||||
longDescription:
|
||||
'This tool allows you to merge multiple audio files into a single file by concatenating them in the order you upload them. Perfect for combining podcast segments, music tracks, or any audio files that need to be joined together. Supports various audio formats including MP3, AAC, and WAV.',
|
||||
|
||||
component: lazy(() => import('./index'))
|
||||
});
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { Box, FormControlLabel, Radio, RadioGroup } from '@mui/material';
|
||||
import React, { useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import ToolContent from '@components/ToolContent';
|
||||
import { ToolComponentProps } from '@tools/defineTool';
|
||||
import { GetGroupsType } from '@components/options/ToolOptions';
|
||||
@@ -22,6 +23,7 @@ const formatOptions = [
|
||||
];
|
||||
|
||||
export default function Trim({ title, longDescription }: ToolComponentProps) {
|
||||
const { t } = useTranslation('audio');
|
||||
const [input, setInput] = useState<File | null>(null);
|
||||
const [result, setResult] = useState<File | null>(null);
|
||||
const [loading, setLoading] = useState(false);
|
||||
@@ -48,28 +50,28 @@ export default function Trim({ title, longDescription }: ToolComponentProps) {
|
||||
updateField
|
||||
}) => [
|
||||
{
|
||||
title: 'Time Settings',
|
||||
title: t('trim.timeSettings'),
|
||||
component: (
|
||||
<Box>
|
||||
<TextFieldWithDesc
|
||||
value={values.startTime}
|
||||
onOwnChange={(val) => updateField('startTime', val)}
|
||||
description="Start time in format HH:MM:SS (e.g., 00:00:30)"
|
||||
label="Start Time"
|
||||
description={t('trim.startTimeDescription')}
|
||||
label={t('trim.startTime')}
|
||||
/>
|
||||
<Box mt={2}>
|
||||
<TextFieldWithDesc
|
||||
value={values.endTime}
|
||||
onOwnChange={(val) => updateField('endTime', val)}
|
||||
description="End time in format HH:MM:SS (e.g., 00:01:30)"
|
||||
label="End Time"
|
||||
description={t('trim.endTimeDescription')}
|
||||
label={t('trim.endTime')}
|
||||
/>
|
||||
</Box>
|
||||
</Box>
|
||||
)
|
||||
},
|
||||
{
|
||||
title: 'Output Format',
|
||||
title: t('trim.outputFormat'),
|
||||
component: (
|
||||
<Box mt={2}>
|
||||
<RadioGroup
|
||||
@@ -104,15 +106,19 @@ export default function Trim({ title, longDescription }: ToolComponentProps) {
|
||||
<ToolAudioInput
|
||||
value={input}
|
||||
onChange={setInput}
|
||||
title={'Input Audio'}
|
||||
title={t('trim.inputTitle')}
|
||||
/>
|
||||
}
|
||||
resultComponent={
|
||||
loading ? (
|
||||
<ToolFileResult title="Trimming Audio" value={null} loading={true} />
|
||||
<ToolFileResult
|
||||
title={t('trim.trimmingAudio')}
|
||||
value={null}
|
||||
loading={true}
|
||||
/>
|
||||
) : (
|
||||
<ToolFileResult
|
||||
title="Trimmed Audio"
|
||||
title={t('trim.resultTitle')}
|
||||
value={result}
|
||||
extension={result ? result.name.split('.').pop() : undefined}
|
||||
/>
|
||||
@@ -122,7 +128,10 @@ export default function Trim({ title, longDescription }: ToolComponentProps) {
|
||||
getGroups={getGroups}
|
||||
setInput={setInput}
|
||||
compute={compute}
|
||||
toolInfo={{ title: `What is ${title}?`, description: longDescription }}
|
||||
toolInfo={{
|
||||
title: t('trim.toolInfo.title', { title }),
|
||||
description: longDescription
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -2,13 +2,16 @@ import { defineTool } from '@tools/defineTool';
|
||||
import { lazy } from 'react';
|
||||
|
||||
export const tool = defineTool('audio', {
|
||||
name: 'Trim Audio',
|
||||
i18n: {
|
||||
name: 'audio:trim.title',
|
||||
description: 'audio:trim.description',
|
||||
shortDescription: 'audio:trim.shortDescription',
|
||||
longDescription: 'audio:trim.longDescription'
|
||||
},
|
||||
|
||||
path: 'trim',
|
||||
icon: 'mdi:scissors-cutting',
|
||||
description:
|
||||
'Cut and trim audio files to extract specific segments by specifying start and end times.',
|
||||
shortDescription:
|
||||
'Trim audio files to extract specific time segments (MP3, AAC, WAV).',
|
||||
|
||||
keywords: [
|
||||
'trim',
|
||||
'audio',
|
||||
@@ -21,7 +24,6 @@ export const tool = defineTool('audio', {
|
||||
'audio editing',
|
||||
'time'
|
||||
],
|
||||
longDescription:
|
||||
'This tool allows you to trim audio files by specifying start and end times. You can extract specific segments from longer audio files, remove unwanted parts, or create shorter clips. Supports various audio formats including MP3, AAC, and WAV. Perfect for podcast editing, music production, or any audio editing needs.',
|
||||
|
||||
component: lazy(() => import('./index'))
|
||||
});
|
||||
|
||||
@@ -2,14 +2,14 @@ import { defineTool } from '@tools/defineTool';
|
||||
import { lazy } from 'react';
|
||||
|
||||
export const tool = defineTool('csv', {
|
||||
name: 'Change CSV separator',
|
||||
path: 'change-csv-separator',
|
||||
icon: 'material-symbols:split-scene-rounded',
|
||||
description:
|
||||
'Just upload your CSV file in the form below and it will automatically get a new column delimiter character. In the tool options, you can specify which delimiter and quote characters are used in the source CSV file and customize the desired delimiter and quote characters for the output CSV. You can also filter the input CSV before the conversion process and skip blank lines and comment lines.',
|
||||
shortDescription: 'Quickly change the CSV column delimiter to a new symbol.',
|
||||
keywords: ['change', 'csv', 'sepa rator'],
|
||||
longDescription:
|
||||
'This tool changes the field separator in CSV (Comma Separated Values) files. This is useful because different programs may use different default separators. While a comma is the most common separator in CSV files, some programs require files to be tab-separated (TSV), semicolon-separated (SSV), pipe-separated (PSV), or have another separation symbol. The default comma may not be so convenient as a delimiter in CSV files because commas are frequently present within fields. In such cases, it can be difficult and confusing to distinguish between commas as delimiters and commas as punctuation symbols. By replacing the comma with another delimiter, you can convert the file into a more easily readable and parsable format. In the options section of this tool, you can configure both the input and output CSV file formats. For the input CSV, you can specify its current delimiter (by default, it is a comma) and also indicate the quotation mark character used to wrap fields. For the output CSV, you can set a new delimiter, choose a new quotation mark character, and optionally enclose all the fields in quotes. Additionally, you have the option to remove empty lines from the input CSV and eliminate comment lines that start with a specified character (usually a hash "#" or double slashes "//"). Csv-abulous!',
|
||||
component: lazy(() => import('./index'))
|
||||
icon: 'material-symbols:code',
|
||||
|
||||
keywords: ['csv', 'separator', 'delimiter', 'change'],
|
||||
component: lazy(() => import('./index')),
|
||||
i18n: {
|
||||
name: 'csv:changeCsvSeparator.title',
|
||||
description: 'csv:changeCsvSeparator.description',
|
||||
shortDescription: 'csv:changeCsvSeparator.shortDescription'
|
||||
}
|
||||
});
|
||||
|
||||
@@ -2,14 +2,14 @@ import { defineTool } from '@tools/defineTool';
|
||||
import { lazy } from 'react';
|
||||
|
||||
export const tool = defineTool('csv', {
|
||||
name: 'Convert CSV Rows to Columns',
|
||||
i18n: {
|
||||
name: 'csv:csvRowsToColumns.title',
|
||||
description: 'csv:csvRowsToColumns.description',
|
||||
shortDescription: 'csv:csvRowsToColumns.shortDescription',
|
||||
longDescription: 'csv:csvRowsToColumns.longDescription'
|
||||
},
|
||||
path: 'csv-rows-to-columns',
|
||||
icon: 'fluent:text-arrow-down-right-column-24-filled',
|
||||
description:
|
||||
'This tool converts rows of a CSV (Comma Separated Values) file into columns. It extracts the horizontal lines from the input CSV one by one, rotates them 90 degrees, and outputs them as vertical columns one after another, separated by commas.',
|
||||
longDescription:
|
||||
'This tool converts rows of a CSV (Comma Separated Values) file into columns. For example, if the input CSV data has 6 rows, then the output will have 6 columns and the elements of the rows will be arranged from the top to bottom. In a well-formed CSV, the number of values in each row is the same. However, in cases when rows are missing fields, the program can fix them and you can choose from the available options: fill missing data with empty elements or replace missing data with custom elements, such as "missing", "?", or "x". During the conversion process, the tool also cleans the CSV file from unnecessary information, such as empty lines (these are lines without visible information) and comments. To help the tool correctly identify comments, in the options, you can specify the symbol at the beginning of a line that starts a comment. This symbol is typically a hash "#" or double slash "//". Csv-abulous!.',
|
||||
shortDescription: 'Convert CSV rows to columns.',
|
||||
keywords: ['csv', 'rows', 'columns', 'transpose'],
|
||||
component: lazy(() => import('./index'))
|
||||
});
|
||||
|
||||
@@ -6,10 +6,9 @@ import { convertCsvToJson } from './service';
|
||||
import { CardExampleType } from '@components/examples/ToolExamples';
|
||||
import { ToolComponentProps } from '@tools/defineTool';
|
||||
import { Box } from '@mui/material';
|
||||
import RadioWithTextField from '@components/options/RadioWithTextField';
|
||||
import SimpleRadio from '@components/options/SimpleRadio';
|
||||
import CheckboxWithDesc from '@components/options/CheckboxWithDesc';
|
||||
import TextFieldWithDesc from '@components/options/TextFieldWithDesc';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
type InitialValuesType = {
|
||||
delimiter: string;
|
||||
@@ -114,6 +113,7 @@ id,name,active
|
||||
];
|
||||
|
||||
export default function CsvToJson({ title }: ToolComponentProps) {
|
||||
const { t } = useTranslation('csv');
|
||||
const [input, setInput] = useState<string>('');
|
||||
const [result, setResult] = useState<string>('');
|
||||
|
||||
@@ -131,8 +131,10 @@ export default function CsvToJson({ title }: ToolComponentProps) {
|
||||
setResult(jsonResult);
|
||||
} catch (error) {
|
||||
setResult(
|
||||
`Error: ${
|
||||
error instanceof Error ? error.message : 'Invalid CSV format'
|
||||
`${t('csvToJson.error')}: ${
|
||||
error instanceof Error
|
||||
? error.message
|
||||
: t('csvToJson.invalidCsvFormat')
|
||||
}`
|
||||
);
|
||||
}
|
||||
@@ -148,28 +150,36 @@ export default function CsvToJson({ title }: ToolComponentProps) {
|
||||
compute={compute}
|
||||
exampleCards={exampleCards}
|
||||
inputComponent={
|
||||
<ToolTextInput title="Input CSV" value={input} onChange={setInput} />
|
||||
<ToolTextInput
|
||||
title={t('csvToJson.inputTitle')}
|
||||
value={input}
|
||||
onChange={setInput}
|
||||
/>
|
||||
}
|
||||
resultComponent={
|
||||
<ToolTextResult title="Output JSON" value={result} extension={'json'} />
|
||||
<ToolTextResult
|
||||
title={t('csvToJson.resultTitle')}
|
||||
value={result}
|
||||
extension={'json'}
|
||||
/>
|
||||
}
|
||||
getGroups={({ values, updateField }) => [
|
||||
{
|
||||
title: 'Input CSV Format',
|
||||
title: t('csvToJson.inputCsvFormat'),
|
||||
component: (
|
||||
<Box>
|
||||
<TextFieldWithDesc
|
||||
description="Column Separator"
|
||||
description={t('csvToJson.columnSeparator')}
|
||||
value={values.delimiter}
|
||||
onOwnChange={(val) => updateField('delimiter', val)}
|
||||
/>
|
||||
<TextFieldWithDesc
|
||||
description="Field Quote"
|
||||
description={t('csvToJson.fieldQuote')}
|
||||
onOwnChange={(val) => updateField('quote', val)}
|
||||
value={values.quote}
|
||||
/>
|
||||
<TextFieldWithDesc
|
||||
description="Comment Symbol"
|
||||
description={t('csvToJson.commentSymbol')}
|
||||
value={values.comment}
|
||||
onOwnChange={(val) => updateField('comment', val)}
|
||||
/>
|
||||
@@ -177,35 +187,34 @@ export default function CsvToJson({ title }: ToolComponentProps) {
|
||||
)
|
||||
},
|
||||
{
|
||||
title: 'Conversion Options',
|
||||
title: t('csvToJson.conversionOptions'),
|
||||
component: (
|
||||
<Box>
|
||||
<CheckboxWithDesc
|
||||
checked={values.useHeaders}
|
||||
onChange={(value) => updateField('useHeaders', value)}
|
||||
title="Use Headers"
|
||||
description="First row is treated as column headers"
|
||||
title={t('csvToJson.useHeaders')}
|
||||
description={t('csvToJson.useHeadersDescription')}
|
||||
/>
|
||||
<CheckboxWithDesc
|
||||
checked={values.skipEmptyLines}
|
||||
onChange={(value) => updateField('skipEmptyLines', value)}
|
||||
title="Skip Empty Lines"
|
||||
description="Don't process empty lines in the CSV"
|
||||
title={t('csvToJson.skipEmptyLines')}
|
||||
description={t('csvToJson.skipEmptyLinesDescription')}
|
||||
/>
|
||||
<CheckboxWithDesc
|
||||
checked={values.dynamicTypes}
|
||||
onChange={(value) => updateField('dynamicTypes', value)}
|
||||
title="Dynamic Types"
|
||||
description="Convert numbers and booleans to their proper types"
|
||||
title={t('csvToJson.dynamicTypes')}
|
||||
description={t('csvToJson.dynamicTypesDescription')}
|
||||
/>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
]}
|
||||
toolInfo={{
|
||||
title: 'What Is a CSV to JSON Converter?',
|
||||
description:
|
||||
'This tool transforms Comma Separated Values (CSV) files to JavaScript Object Notation (JSON) data structures. It supports various CSV formats with customizable delimiters, quote characters, and comment symbols. The converter can treat the first row as headers, skip empty lines, and automatically detect data types like numbers and booleans. The resulting JSON can be used for data migration, backups, or as input for other applications.'
|
||||
title: t('csvToJson.toolInfo.title'),
|
||||
description: t('csvToJson.toolInfo.description')
|
||||
}}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -2,12 +2,15 @@ import { defineTool } from '@tools/defineTool';
|
||||
import { lazy } from 'react';
|
||||
|
||||
export const tool = defineTool('csv', {
|
||||
name: 'Convert CSV to JSON',
|
||||
i18n: {
|
||||
name: 'csv:csvToJson.title',
|
||||
description: 'csv:csvToJson.description',
|
||||
shortDescription: 'csv:csvToJson.shortDescription'
|
||||
},
|
||||
|
||||
path: 'csv-to-json',
|
||||
icon: 'lets-icons:json-light',
|
||||
description:
|
||||
'Convert CSV files to JSON format with customizable options for delimiters, quotes, and output formatting. Support for headers, comments, and dynamic type conversion.',
|
||||
shortDescription: 'Convert CSV data to JSON format.',
|
||||
|
||||
keywords: ['csv', 'json', 'convert', 'transform', 'parse'],
|
||||
component: lazy(() => import('./index'))
|
||||
});
|
||||
|
||||
@@ -2,14 +2,15 @@ import { defineTool } from '@tools/defineTool';
|
||||
import { lazy } from 'react';
|
||||
|
||||
export const tool = defineTool('csv', {
|
||||
name: 'Convert CSV to TSV',
|
||||
i18n: {
|
||||
name: 'csv:csvToTsv.title',
|
||||
description: 'csv:csvToTsv.description',
|
||||
shortDescription: 'csv:csvToTsv.shortDescription',
|
||||
longDescription: 'csv:csvToTsv.longDescription'
|
||||
},
|
||||
|
||||
path: 'csv-to-tsv',
|
||||
icon: 'codicon:keyboard-tab',
|
||||
description:
|
||||
'Upload your CSV file in the form below and it will automatically get converted to a TSV file. In the tool options, you can customize the input CSV format – specify the field delimiter, quotation character, and comment symbol, as well as skip empty CSV lines, and choose whether to preserve CSV column headers.',
|
||||
shortDescription: 'Convert CSV data to TSV format.',
|
||||
longDescription:
|
||||
'This tool transforms Comma Separated Values (CSV) data to Tab Separated Values (TSV) data. Both CSV and TSV are popular file formats for storing tabular data but they use different delimiters to separate values – CSV uses commas (","), while TSV uses tabs ("\t"). If we compare CSV files to TSV files, then CSV files are much harder to parse than TSV files because the values themselves may contain commas, so it is not always obvious where one field starts and ends without complicated parsing rules. TSV, on the other hand, uses just a tab symbol, which does not usually appear in data, so separating fields in TSV is as simple as splitting the input by the tab character. To convert CSV to TSV, simply input the CSV data in the input of this tool. In rare cases when a CSV file has a delimiter other than a comma, you can specify the current delimiter in the options of the tool. You can also specify the current quote character and the comment start character. Additionally, empty CSV lines can be skipped by activating the "Ignore Lines with No Data" option. If this option is off, then empty lines in the CSV are converted to empty TSV lines. The "Preserve Headers" option allows you to choose whether to process column headers of a CSV file. If the option is selected, then the resulting TSV file will include the first row of the input CSV file, which contains the column names. Alternatively, if the headers option is not selected, the first row will be skipped during the data conversion process. For the reverse conversion from TSV to CSV, you can use our Convert TSV to CSV tool. Csv-abulous!',
|
||||
keywords: ['csv', 'tsv', 'convert', 'transform', 'parse'],
|
||||
component: lazy(() => import('./index'))
|
||||
});
|
||||
|
||||
@@ -2,11 +2,15 @@ import { defineTool } from '@tools/defineTool';
|
||||
import { lazy } from 'react';
|
||||
|
||||
export const tool = defineTool('csv', {
|
||||
name: 'Convert CSV to XML',
|
||||
i18n: {
|
||||
name: 'csv:csvToXml.title',
|
||||
description: 'csv:csvToXml.description',
|
||||
shortDescription: 'csv:csvToXml.shortDescription'
|
||||
},
|
||||
|
||||
path: 'csv-to-xml',
|
||||
icon: 'mdi-light:xml',
|
||||
description: 'Convert CSV files to XML format with customizable options.',
|
||||
shortDescription: 'Convert CSV data to XML format.',
|
||||
|
||||
keywords: ['csv', 'xml', 'convert', 'transform', 'parse'],
|
||||
component: lazy(() => import('./index'))
|
||||
});
|
||||
|
||||
@@ -2,14 +2,16 @@ import { defineTool } from '@tools/defineTool';
|
||||
import { lazy } from 'react';
|
||||
|
||||
export const tool = defineTool('csv', {
|
||||
name: 'Convert CSV to YAML',
|
||||
i18n: {
|
||||
name: 'csv:csvToYaml.title',
|
||||
description: 'csv:csvToYaml.description',
|
||||
shortDescription: 'csv:csvToYaml.shortDescription',
|
||||
longDescription: 'csv:csvToYaml.longDescription'
|
||||
},
|
||||
|
||||
path: 'csv-to-yaml',
|
||||
icon: 'nonicons:yaml-16',
|
||||
description:
|
||||
'Just upload your CSV file in the form below and it will automatically get converted to a YAML file. In the tool options, you can specify the field delimiter character, field quote character, and comment character to adapt the tool to custom CSV formats. Additionally, you can select the output YAML format: one that preserves CSV headers or one that excludes CSV headers.',
|
||||
shortDescription: 'Quickly convert a CSV file to a YAML file.',
|
||||
keywords: ['csv', 'to', 'yaml'],
|
||||
longDescription:
|
||||
'This tool transforms CSV (Comma Separated Values) data into the YAML (Yet Another Markup Language) data. CSV is a simple, tabular format that is used to represent matrix-like data types consisting of rows and columns. YAML, on the other hand, is a more advanced format (actually a superset of JSON), which creates more human-readable data for serialization, and it supports lists, dictionaries, and nested objects. This program supports various input CSV formats – the input data can be comma-separated (default), semicolon-separated, pipe-separated, or use another completely different delimiter. You can specify the exact delimiter your data uses in the options. Similarly, in the options, you can specify the quote character that is used to wrap CSV fields (by default a double-quote symbol). You can also skip lines that start with comments by specifying the comment symbols in the options. This allows you to keep your data clean by skipping unnecessary lines. There are two ways to convert CSV to YAML. The first method converts each CSV row into a YAML list. The second method extracts headers from the first CSV row and creates YAML objects with keys based on these headers. You can also customize the output YAML format by specifying the number of spaces for indenting YAML structures. If you need to perform the reverse conversion, that is, transform YAML into CSV, you can use our Convert YAML to CSV tool. Csv-abulous!',
|
||||
|
||||
component: lazy(() => import('./index'))
|
||||
});
|
||||
|
||||
@@ -10,6 +10,7 @@ import { findIncompleteCsvRecords } from './service';
|
||||
import { InitialValuesType } from './types';
|
||||
import TextFieldWithDesc from '@components/options/TextFieldWithDesc';
|
||||
import CheckboxWithDesc from '@components/options/CheckboxWithDesc';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
const initialValues: InitialValuesType = {
|
||||
csvSeparator: ',',
|
||||
@@ -103,6 +104,7 @@ export default function FindIncompleteCsvRecords({
|
||||
title,
|
||||
longDescription
|
||||
}: ToolComponentProps) {
|
||||
const { t } = useTranslation('csv');
|
||||
const [input, setInput] = useState<string>('');
|
||||
const [result, setResult] = useState<string>('');
|
||||
|
||||
@@ -115,55 +117,57 @@ export default function FindIncompleteCsvRecords({
|
||||
updateField
|
||||
}) => [
|
||||
{
|
||||
title: 'Csv input Options',
|
||||
title: t('findIncompleteCsvRecords.csvInputOptions'),
|
||||
component: (
|
||||
<Box>
|
||||
<TextFieldWithDesc
|
||||
value={values.csvSeparator}
|
||||
onOwnChange={(val) => updateField('csvSeparator', val)}
|
||||
description={
|
||||
'Enter the character used to delimit columns in the CSV input file.'
|
||||
}
|
||||
description={t('findIncompleteCsvRecords.csvSeparatorDescription')}
|
||||
/>
|
||||
<TextFieldWithDesc
|
||||
value={values.quoteCharacter}
|
||||
onOwnChange={(val) => updateField('quoteCharacter', val)}
|
||||
description={
|
||||
'Enter the quote character used to quote the CSV input fields.'
|
||||
}
|
||||
description={t(
|
||||
'findIncompleteCsvRecords.quoteCharacterDescription'
|
||||
)}
|
||||
/>
|
||||
<TextFieldWithDesc
|
||||
value={values.commentCharacter}
|
||||
onOwnChange={(val) => updateField('commentCharacter', val)}
|
||||
description={
|
||||
'Enter the character indicating the start of a comment line. Lines starting with this symbol will be skipped.'
|
||||
}
|
||||
description={t(
|
||||
'findIncompleteCsvRecords.commentCharacterDescription'
|
||||
)}
|
||||
/>
|
||||
</Box>
|
||||
)
|
||||
},
|
||||
{
|
||||
title: 'Checking Options',
|
||||
title: t('findIncompleteCsvRecords.checkingOptions'),
|
||||
component: (
|
||||
<Box>
|
||||
<CheckboxWithDesc
|
||||
checked={values.emptyLines}
|
||||
onChange={(value) => updateField('emptyLines', value)}
|
||||
title="Delete Lines with No Data"
|
||||
description="Remove empty lines from CSV input file."
|
||||
title={t('findIncompleteCsvRecords.deleteLinesWithNoData')}
|
||||
description={t(
|
||||
'findIncompleteCsvRecords.deleteLinesWithNoDataDescription'
|
||||
)}
|
||||
/>
|
||||
|
||||
<CheckboxWithDesc
|
||||
checked={values.emptyValues}
|
||||
onChange={(value) => updateField('emptyValues', value)}
|
||||
title="Find Empty Values"
|
||||
description="Display a message about CSV fields that are empty (These are not missing fields but fields that contain nothing)."
|
||||
title={t('findIncompleteCsvRecords.findEmptyValues')}
|
||||
description={t(
|
||||
'findIncompleteCsvRecords.findEmptyValuesDescription'
|
||||
)}
|
||||
/>
|
||||
|
||||
<CheckboxWithDesc
|
||||
checked={values.messageLimit}
|
||||
onChange={(value) => updateField('messageLimit', value)}
|
||||
title="Limit number of messages"
|
||||
title={t('findIncompleteCsvRecords.limitNumberOfMessages')}
|
||||
/>
|
||||
|
||||
{values.messageLimit && (
|
||||
@@ -172,7 +176,9 @@ export default function FindIncompleteCsvRecords({
|
||||
onOwnChange={(val) => updateField('messageNumber', Number(val))}
|
||||
type="number"
|
||||
inputProps={{ min: 1 }}
|
||||
description={'Set the limit of number of messages in the output.'}
|
||||
description={t(
|
||||
'findIncompleteCsvRecords.messageLimitDescription'
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
</Box>
|
||||
@@ -184,15 +190,27 @@ export default function FindIncompleteCsvRecords({
|
||||
title={title}
|
||||
input={input}
|
||||
inputComponent={
|
||||
<ToolTextInput title={'Input CSV'} value={input} onChange={setInput} />
|
||||
<ToolTextInput
|
||||
title={t('findIncompleteCsvRecords.inputTitle')}
|
||||
value={input}
|
||||
onChange={setInput}
|
||||
/>
|
||||
}
|
||||
resultComponent={
|
||||
<ToolTextResult
|
||||
title={t('findIncompleteCsvRecords.resultTitle')}
|
||||
value={result}
|
||||
/>
|
||||
}
|
||||
resultComponent={<ToolTextResult title={'CSV Status'} value={result} />}
|
||||
initialValues={initialValues}
|
||||
exampleCards={exampleCards}
|
||||
getGroups={getGroups}
|
||||
setInput={setInput}
|
||||
compute={compute}
|
||||
toolInfo={{ title: `What is a ${title}?`, description: longDescription }}
|
||||
toolInfo={{
|
||||
title: t('findIncompleteCsvRecords.toolInfo.title', { title }),
|
||||
description: longDescription
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -2,15 +2,15 @@ import { defineTool } from '@tools/defineTool';
|
||||
import { lazy } from 'react';
|
||||
|
||||
export const tool = defineTool('csv', {
|
||||
name: 'Find incomplete CSV records',
|
||||
path: 'find-incomplete-csv-records',
|
||||
icon: 'tdesign:search-error',
|
||||
description:
|
||||
'Just upload your CSV file in the form below and this tool will automatically check if none of the rows or columns are missing values. In the tool options, you can adjust the input file format (specify the delimiter, quote character, and comment character). Additionally, you can enable checking for empty values, skip empty lines, and set a limit on the number of error messages in the output.',
|
||||
shortDescription:
|
||||
'Quickly find rows and columns in CSV that are missing values.',
|
||||
|
||||
keywords: ['find', 'incomplete', 'csv', 'records'],
|
||||
longDescription:
|
||||
'This tool checks the completeness of CSV (Comma Separated Values) files and identifies incomplete records within the data. It finds rows and columns where one or more values are missing and displays their positions in the output so that you can quickly find and fix your CSV file. A valid CSV file has the same number of values (fields) in all rows and the same number of values (fields) in all columns. If the CSV you load in this tool is complete, the program will notify you with a green badge. If at least one value is missing in any row or column, the program will show a red badge and indicate the exact location of the missing value. If the CSV file has a field with no characters in it, then such a field is called an empty field. It is not a missing field, just empty as it contains nothing. You can activate the "Find Empty Values" checkbox in the options to identify all such fields in the CSV. If the file contains empty lines, you can ignore them with the "Skip Empty Lines" option or check them for completeness along with other lines. You can also configure the delimiter, quote, and comment characters in the options. This allows you to adapt to other file formats besides CSV, such as TSV (Tab Separated Values), SSV (Semicolon Separated Values), or PSV (Pipe Separated Values). If the file has too many incomplete or empty records, you can set a limit on the output messages to display, for example, 5, 10, or 20 messages. If you want to quickly fill in the missing data with default values, you can use our Fill Incomplete CSV Records tool. Csv-abulous!',
|
||||
component: lazy(() => import('./index'))
|
||||
|
||||
component: lazy(() => import('./index')),
|
||||
i18n: {
|
||||
name: 'csv:findIncompleteCsvRecords.title',
|
||||
description: 'csv:findIncompleteCsvRecords.description',
|
||||
shortDescription: 'csv:findIncompleteCsvRecords.shortDescription'
|
||||
}
|
||||
});
|
||||
|
||||
@@ -1,16 +1,17 @@
|
||||
import { Box } from '@mui/material';
|
||||
import React, { useState } from 'react';
|
||||
import ToolContent from '@components/ToolContent';
|
||||
import { ToolComponentProps } from '@tools/defineTool';
|
||||
import ToolTextInput from '@components/input/ToolTextInput';
|
||||
import ToolTextResult from '@components/result/ToolTextResult';
|
||||
import { GetGroupsType } from '@components/options/ToolOptions';
|
||||
import { CardExampleType } from '@components/examples/ToolExamples';
|
||||
import { main } from './service';
|
||||
import { getCsvHeaders } from '@utils/csv';
|
||||
import { InitialValuesType } from './types';
|
||||
import { GetGroupsType } from '@components/options/ToolOptions';
|
||||
import TextFieldWithDesc from '@components/options/TextFieldWithDesc';
|
||||
import SelectWithDesc from '@components/options/SelectWithDesc';
|
||||
import { CardExampleType } from '@components/examples/ToolExamples';
|
||||
import { ToolComponentProps } from '@tools/defineTool';
|
||||
import { getCsvHeaders } from '@utils/csv';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
const initialValues: InitialValuesType = {
|
||||
csvToInsert: '',
|
||||
@@ -27,15 +28,19 @@ const initialValues: InitialValuesType = {
|
||||
|
||||
const exampleCards: CardExampleType<InitialValuesType>[] = [
|
||||
{
|
||||
title: 'Add One Column to a CSV File',
|
||||
title: 'Insert a single column by position',
|
||||
description:
|
||||
'In this example, we insert a column with the title "city" into a CSV file that already contains two other columns with titles "name" and "age". The new column consists of three values: "city", "dallas", and "houston", corresponding to the height of the input CSV data. The value "city" is the header value (appearing on the first row) and values "dallas" and "houston" are data values (appearing on rows two and three). We specify the position of the new column by an ordinal number and set it to 1 in the options. This value indicates that the new "city" column should be placed after the first column.',
|
||||
sampleText: `name,age
|
||||
john,25
|
||||
emma,22`,
|
||||
sampleResult: `name,city,age
|
||||
john,dallas,25
|
||||
emma,houston,22`,
|
||||
'In this example, we insert a single column "city" at position 1 in the CSV data. The input CSV has data about cars, including the "Brand" and "Model" of the car. We now add a "city" column at position 1. To do this, we enter the city data in the comma-separated format in the "New Column" option, and to quickly add the new column at position 1, then we specify the position number.',
|
||||
sampleText: `Brand,Model
|
||||
Toyota,Camry
|
||||
Ford,Mustang
|
||||
Honda,Accord
|
||||
Chevrolet,Malibu`,
|
||||
sampleResult: `city,Brand,Model
|
||||
dallas,Toyota,Camry
|
||||
houston,Ford,Mustang
|
||||
dallas,Honda,Accord
|
||||
houston,Chevrolet,Malibu`,
|
||||
sampleOptions: {
|
||||
csvToInsert: `city
|
||||
dallas
|
||||
@@ -118,6 +123,7 @@ export default function InsertCsvColumns({
|
||||
title,
|
||||
longDescription
|
||||
}: ToolComponentProps) {
|
||||
const { t } = useTranslation('csv');
|
||||
const [input, setInput] = useState<string>('');
|
||||
const [result, setResult] = useState<string>('');
|
||||
|
||||
@@ -139,7 +145,7 @@ export default function InsertCsvColumns({
|
||||
updateField
|
||||
}) => [
|
||||
{
|
||||
title: 'CSV to insert',
|
||||
title: t('insertCsvColumns.csvToInsert'),
|
||||
component: (
|
||||
<Box>
|
||||
<TextFieldWithDesc
|
||||
@@ -147,44 +153,42 @@ export default function InsertCsvColumns({
|
||||
rows={3}
|
||||
value={values.csvToInsert}
|
||||
onOwnChange={(val) => updateField('csvToInsert', val)}
|
||||
title="CSV separator"
|
||||
description={`Enter one or more columns you want to insert into the CSV.
|
||||
the character used to delimit columns has to be the same with the one in the CSV input file.
|
||||
Ps: Blank lines will be ignored`}
|
||||
title={t('insertCsvColumns.csvSeparator')}
|
||||
description={t('insertCsvColumns.csvToInsertDescription')}
|
||||
/>
|
||||
</Box>
|
||||
)
|
||||
},
|
||||
{
|
||||
title: 'CSV Options',
|
||||
title: t('insertCsvColumns.csvOptions'),
|
||||
component: (
|
||||
<Box>
|
||||
<TextFieldWithDesc
|
||||
value={values.separator}
|
||||
onOwnChange={(val) => updateField('separator', val)}
|
||||
description={
|
||||
'Enter the character used to delimit columns in the CSV input file.'
|
||||
}
|
||||
description={t('insertCsvColumns.separatorDescription')}
|
||||
/>
|
||||
<TextFieldWithDesc
|
||||
value={values.quoteChar}
|
||||
onOwnChange={(val) => updateField('quoteChar', val)}
|
||||
description={
|
||||
'Enter the quote character used to quote the CSV input fields.'
|
||||
}
|
||||
description={t('insertCsvColumns.quoteCharDescription')}
|
||||
/>
|
||||
<TextFieldWithDesc
|
||||
value={values.commentCharacter}
|
||||
onOwnChange={(val) => updateField('commentCharacter', val)}
|
||||
description={
|
||||
'Enter the character indicating the start of a comment line. Lines starting with this symbol will be skipped.'
|
||||
}
|
||||
description={t('insertCsvColumns.commentCharacterDescription')}
|
||||
/>
|
||||
<SelectWithDesc
|
||||
selected={values.customFill}
|
||||
options={[
|
||||
{ label: 'Fill With Empty Values', value: false },
|
||||
{ label: 'Fill With Customs Values', value: true }
|
||||
{
|
||||
label: t('insertCsvColumns.fillWithEmptyValues'),
|
||||
value: false
|
||||
},
|
||||
{
|
||||
label: t('insertCsvColumns.fillWithCustomValues'),
|
||||
value: true
|
||||
}
|
||||
]}
|
||||
onChange={(value) => {
|
||||
updateField('customFill', value);
|
||||
@@ -192,48 +196,59 @@ export default function InsertCsvColumns({
|
||||
updateField('customFillValue', ''); // Reset custom fill value
|
||||
}
|
||||
}}
|
||||
description={
|
||||
'If the input CSV file is incomplete (missing values), then add empty fields or custom symbols to records to make a well-formed CSV?'
|
||||
}
|
||||
description={t('insertCsvColumns.customFillDescription')}
|
||||
/>
|
||||
{values.customFill && (
|
||||
<TextFieldWithDesc
|
||||
value={values.customFillValue}
|
||||
onOwnChange={(val) => updateField('customFillValue', val)}
|
||||
description={
|
||||
'Use this custom value to fill in missing fields. (Works only with "Custom Values" mode above.)'
|
||||
}
|
||||
description={t('insertCsvColumns.customFillValueDescription')}
|
||||
/>
|
||||
)}
|
||||
</Box>
|
||||
)
|
||||
},
|
||||
{
|
||||
title: 'Position Options',
|
||||
title: t('insertCsvColumns.positionOptions'),
|
||||
component: (
|
||||
<Box>
|
||||
<SelectWithDesc
|
||||
selected={values.insertingPosition}
|
||||
options={[
|
||||
{ label: 'Prepend columns', value: 'prepend' },
|
||||
{ label: 'Append columns', value: 'append' },
|
||||
{ label: 'Custom position', value: 'custom' }
|
||||
{
|
||||
label: t('insertCsvColumns.prependColumns'),
|
||||
value: 'prepend'
|
||||
},
|
||||
{
|
||||
label: t('insertCsvColumns.appendColumns'),
|
||||
value: 'append'
|
||||
},
|
||||
{
|
||||
label: t('insertCsvColumns.customPosition'),
|
||||
value: 'custom'
|
||||
}
|
||||
]}
|
||||
onChange={(value) => updateField('insertingPosition', value)}
|
||||
description={'Specify where to insert the columns in the CSV file.'}
|
||||
description={t('insertCsvColumns.insertingPositionDescription')}
|
||||
/>
|
||||
|
||||
{values.insertingPosition === 'custom' && (
|
||||
<SelectWithDesc
|
||||
selected={values.customPostionOptions}
|
||||
options={[
|
||||
{ label: 'Header name', value: 'headerName' },
|
||||
{ label: 'Position ', value: 'rowNumber' }
|
||||
{
|
||||
label: t('insertCsvColumns.headerName'),
|
||||
value: 'headerName'
|
||||
},
|
||||
{
|
||||
label: t('insertCsvColumns.position'),
|
||||
value: 'rowNumber'
|
||||
}
|
||||
]}
|
||||
onChange={(value) => updateField('customPostionOptions', value)}
|
||||
description={
|
||||
'Select the method to insert the columns in the CSV file.'
|
||||
}
|
||||
description={t(
|
||||
'insertCsvColumns.customPositionOptionsDescription'
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
|
||||
@@ -243,42 +258,53 @@ export default function InsertCsvColumns({
|
||||
selected={values.headerName}
|
||||
options={headerOptions}
|
||||
onChange={(value) => updateField('headerName', value)}
|
||||
description={
|
||||
'Header of the column you want to insert columns after.'
|
||||
}
|
||||
description={t('insertCsvColumns.headerNameDescription')}
|
||||
/>
|
||||
)}
|
||||
|
||||
{values.insertingPosition === 'custom' &&
|
||||
values.customPostionOptions === 'rowNumber' && (
|
||||
<TextFieldWithDesc
|
||||
value={values.rowNumber}
|
||||
onOwnChange={(val) => updateField('rowNumber', Number(val))}
|
||||
inputProps={{ min: 1, max: headers.length }}
|
||||
type="number"
|
||||
description={
|
||||
'Number of the column you want to insert columns after.'
|
||||
value={values.rowNumber.toString()}
|
||||
onOwnChange={(val) =>
|
||||
updateField('rowNumber', parseInt(val) || 0)
|
||||
}
|
||||
description={t('insertCsvColumns.rowNumberDescription')}
|
||||
type="number"
|
||||
/>
|
||||
)}
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
];
|
||||
|
||||
return (
|
||||
<ToolContent
|
||||
title={title}
|
||||
input={input}
|
||||
inputComponent={
|
||||
<ToolTextInput value={input} title="Input CSV" onChange={setInput} />
|
||||
<ToolTextInput
|
||||
title={t('insertCsvColumns.inputTitle')}
|
||||
value={input}
|
||||
onChange={setInput}
|
||||
/>
|
||||
}
|
||||
resultComponent={
|
||||
<ToolTextResult
|
||||
title={t('insertCsvColumns.resultTitle')}
|
||||
value={result}
|
||||
extension={'csv'}
|
||||
/>
|
||||
}
|
||||
resultComponent={<ToolTextResult title="Output CSV" value={result} />}
|
||||
initialValues={initialValues}
|
||||
exampleCards={exampleCards}
|
||||
getGroups={getGroups}
|
||||
setInput={setInput}
|
||||
compute={compute}
|
||||
toolInfo={{ title: `What is a ${title}?`, description: longDescription }}
|
||||
input={input}
|
||||
setInput={setInput}
|
||||
toolInfo={{
|
||||
title: t('insertCsvColumns.toolInfo.title'),
|
||||
description: t('insertCsvColumns.toolInfo.description')
|
||||
}}
|
||||
exampleCards={exampleCards}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -2,14 +2,16 @@ import { defineTool } from '@tools/defineTool';
|
||||
import { lazy } from 'react';
|
||||
|
||||
export const tool = defineTool('csv', {
|
||||
name: 'Insert CSV columns',
|
||||
i18n: {
|
||||
name: 'csv:insertCsvColumns.title',
|
||||
description: 'csv:insertCsvColumns.description',
|
||||
shortDescription: 'csv:insertCsvColumns.shortDescription'
|
||||
},
|
||||
|
||||
path: 'insert-csv-columns',
|
||||
icon: 'hugeicons:column-insert',
|
||||
description:
|
||||
'Just upload your CSV file in the form below, paste the new column in the options, and it will automatically get inserted in your CSV. In the tool options, you can also specify more than one column to insert, set the insertion position, and optionally skip the empty and comment lines.',
|
||||
shortDescription:
|
||||
'Quickly insert one or more new columns anywhere in a CSV file.',
|
||||
|
||||
keywords: ['insert', 'csv', 'columns', 'append', 'prepend'],
|
||||
longDescription: '',
|
||||
|
||||
component: lazy(() => import('./index'))
|
||||
});
|
||||
|
||||
@@ -2,14 +2,15 @@ import { defineTool } from '@tools/defineTool';
|
||||
import { lazy } from 'react';
|
||||
|
||||
export const tool = defineTool('csv', {
|
||||
name: 'Swap CSV Columns',
|
||||
i18n: {
|
||||
name: 'csv:swapCsvColumns.title',
|
||||
description: 'csv:swapCsvColumns.description',
|
||||
shortDescription: 'csv:swapCsvColumns.shortDescription',
|
||||
longDescription: 'csv:swapCsvColumns.longDescription'
|
||||
},
|
||||
|
||||
path: 'swap-csv-columns',
|
||||
icon: 'eva:swap-outline',
|
||||
description:
|
||||
'Just upload your CSV file in the form below, specify the columns to swap, and the tool will automatically change the positions of the specified columns in the output file. In the tool options, you can specify the column positions or names that you want to swap, as well as fix incomplete data and optionally remove empty records and records that have been commented out.',
|
||||
shortDescription: 'Reorder CSV columns.',
|
||||
longDescription:
|
||||
'This tool reorganizes CSV data by swapping the positions of its columns. Swapping columns can enhance the readability of a CSV file by placing frequently used data together or in the front for easier data comparison and editing. For example, you can swap the first column with the last or swap the second column with the third. To swap columns based on their positions, select the "Set Column Position" mode and enter the numbers of the "from" and "to" columns to be swapped in the first and second blocks of options. For example, if you have a CSV file with four columns "1, 2, 3, 4" and swap columns with positions "2" and "4", the output CSV will have columns in the order: "1, 4, 3, 2".As an alternative to positions, you can swap columns by specifying their headers (column names on the first row of data). If you enable this mode in the options, then you can enter the column names like "location" and "city", and the program will swap these two columns. If any of the specified columns have incomplete data (some fields are missing), you can choose to skip such data or fill the missing fields with empty values or custom values (specified in the options). Additionally, you can specify the symbol used for comments in the CSV data, such as "#" or "//". If you do not need the commented lines in the output, you can remove them by using the "Delete Comments" checkbox. You can also activate the checkbox "Delete Empty Lines" to get rid of empty lines that contain no visible information. Csv-abulous!',
|
||||
keywords: ['csv', 'swap', 'columns'],
|
||||
component: lazy(() => import('./index'))
|
||||
});
|
||||
|
||||
@@ -2,14 +2,17 @@ import { defineTool } from '@tools/defineTool';
|
||||
import { lazy } from 'react';
|
||||
|
||||
export const tool = defineTool('csv', {
|
||||
name: 'Transpose CSV',
|
||||
i18n: {
|
||||
name: 'csv:transposeCsv.title',
|
||||
description: 'csv:transposeCsv.description',
|
||||
shortDescription: 'csv:transposeCsv.shortDescription',
|
||||
longDescription: 'csv:transposeCsv.longDescription'
|
||||
},
|
||||
|
||||
path: 'transpose-csv',
|
||||
icon: 'carbon:transpose',
|
||||
description:
|
||||
'Just upload your CSV file in the form below, and this tool will automatically transpose your CSV. In the tool options, you can specify the character that starts the comment lines in the CSV to remove them. Additionally, if the CSV is incomplete (missing values), you can replace missing values with the empty character or a custom character.',
|
||||
shortDescription: 'Quickly transpose a CSV file.',
|
||||
|
||||
keywords: ['transpose', 'csv'],
|
||||
longDescription:
|
||||
'This tool transposes Comma Separated Values (CSV). It treats the CSV as a matrix of data and flips all elements across the main diagonal. The output contains the same CSV data as the input, but now all the rows have become columns, and all the columns have become rows. After transposition, the CSV file will have opposite dimensions. For example, if the input file has 4 columns and 3 rows, the output file will have 3 columns and 4 rows. During conversion, the program also cleans the data from unnecessary lines and corrects incomplete data. Specifically, the tool automatically deletes all empty records and comments that begin with a specific character, which you can set in the option. Additionally, in cases where the CSV data is corrupted or lost, the utility completes the file with empty fields or custom fields that can be specified in the options. Csv-abulous!',
|
||||
|
||||
component: lazy(() => import('./index'))
|
||||
});
|
||||
|
||||
@@ -2,12 +2,15 @@ import { defineTool } from '@tools/defineTool';
|
||||
import { lazy } from 'react';
|
||||
|
||||
export const tool = defineTool('image-generic', {
|
||||
name: 'Change colors in image',
|
||||
i18n: {
|
||||
name: 'image:changeColors.title',
|
||||
description: 'image:changeColors.description',
|
||||
shortDescription: 'image:changeColors.shortDescription'
|
||||
},
|
||||
|
||||
path: 'change-colors',
|
||||
icon: 'cil:color-fill',
|
||||
description:
|
||||
"World's simplest online Image color changer. Just import your image (JPG, PNG, SVG) in the editor on the left, select which colors to change, and you'll instantly get a new image with the new colors on the right. Free, quick, and very powerful. Import an image – replace its colors.",
|
||||
shortDescription: 'Quickly swap colors in a image',
|
||||
|
||||
keywords: ['change', 'colors', 'in', 'png', 'image', 'jpg'],
|
||||
component: lazy(() => import('./index'))
|
||||
});
|
||||
|
||||
@@ -2,12 +2,15 @@ import { defineTool } from '@tools/defineTool';
|
||||
import { lazy } from 'react';
|
||||
|
||||
export const tool = defineTool('image-generic', {
|
||||
name: 'Change image Opacity',
|
||||
i18n: {
|
||||
name: 'image:changeOpacity.title',
|
||||
description: 'image:changeOpacity.description',
|
||||
shortDescription: 'image:changeOpacity.shortDescription'
|
||||
},
|
||||
|
||||
path: 'change-opacity',
|
||||
icon: 'material-symbols:opacity',
|
||||
description:
|
||||
'Easily adjust the transparency of your images. Simply upload your image, use the slider to set the desired opacity level between 0 (fully transparent) and 1 (fully opaque), and download the modified image.',
|
||||
shortDescription: 'Adjust transparency of images',
|
||||
|
||||
keywords: ['opacity', 'transparency', 'png', 'alpha', 'jpg', 'jpeg', 'image'],
|
||||
component: lazy(() => import('./index'))
|
||||
});
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import React, { useContext, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { InitialValuesType } from './types';
|
||||
import { compressImage } from './service';
|
||||
import ToolContent from '@components/ToolContent';
|
||||
@@ -17,6 +18,7 @@ const initialValues: InitialValuesType = {
|
||||
};
|
||||
|
||||
export default function CompressImage({ title }: ToolComponentProps) {
|
||||
const { t } = useTranslation('image');
|
||||
const [input, setInput] = useState<File | null>(null);
|
||||
const [result, setResult] = useState<File | null>(null);
|
||||
const [isProcessing, setIsProcessing] = useState<boolean>(false);
|
||||
@@ -37,7 +39,7 @@ export default function CompressImage({ title }: ToolComponentProps) {
|
||||
setResult(compressed);
|
||||
setCompressedSize(compressed.size);
|
||||
} else {
|
||||
showSnackBar('Failed to compress image. Please try again.', 'error');
|
||||
showSnackBar(t('compress.failedToCompress'), 'error');
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Error in compression:', err);
|
||||
@@ -55,12 +57,12 @@ export default function CompressImage({ title }: ToolComponentProps) {
|
||||
value={input}
|
||||
onChange={setInput}
|
||||
accept={['image/*']}
|
||||
title={'Input image'}
|
||||
title={t('compress.inputTitle')}
|
||||
/>
|
||||
}
|
||||
resultComponent={
|
||||
<ToolFileResult
|
||||
title={'Compressed image'}
|
||||
title={t('compress.resultTitle')}
|
||||
value={result}
|
||||
loading={isProcessing}
|
||||
/>
|
||||
@@ -68,14 +70,14 @@ export default function CompressImage({ title }: ToolComponentProps) {
|
||||
initialValues={initialValues}
|
||||
getGroups={({ values, updateField }) => [
|
||||
{
|
||||
title: 'Compression options',
|
||||
title: t('compress.compressionOptions'),
|
||||
component: (
|
||||
<Box>
|
||||
<TextFieldWithDesc
|
||||
name="maxFileSizeInMB"
|
||||
type="number"
|
||||
inputProps={{ min: 0.1, step: 0.1 }}
|
||||
description="Maximum file size in megabytes"
|
||||
description={t('compress.maxFileSizeDescription')}
|
||||
onOwnChange={(value) =>
|
||||
updateNumberField(value, 'maxFileSizeInMB', updateField)
|
||||
}
|
||||
@@ -85,7 +87,7 @@ export default function CompressImage({ title }: ToolComponentProps) {
|
||||
name="quality"
|
||||
type="number"
|
||||
inputProps={{ min: 10, max: 100, step: 1 }}
|
||||
description="Image quality percentage (lower means smaller file size)"
|
||||
description={t('compress.qualityDescription')}
|
||||
onOwnChange={(value) =>
|
||||
updateNumberField(value, 'quality', updateField)
|
||||
}
|
||||
@@ -95,18 +97,20 @@ export default function CompressImage({ title }: ToolComponentProps) {
|
||||
)
|
||||
},
|
||||
{
|
||||
title: 'File sizes',
|
||||
title: t('compress.fileSizes'),
|
||||
component: (
|
||||
<Box>
|
||||
<Box>
|
||||
{originalSize !== null && (
|
||||
<Typography>
|
||||
Original Size: {(originalSize / 1024).toFixed(2)} KB
|
||||
{t('compress.originalSize')}:{' '}
|
||||
{(originalSize / 1024).toFixed(2)} KB
|
||||
</Typography>
|
||||
)}
|
||||
{compressedSize !== null && (
|
||||
<Typography>
|
||||
Compressed Size: {(compressedSize / 1024).toFixed(2)} KB
|
||||
{t('compress.compressedSize')}:{' '}
|
||||
{(compressedSize / 1024).toFixed(2)} KB
|
||||
</Typography>
|
||||
)}
|
||||
</Box>
|
||||
|
||||
@@ -2,13 +2,15 @@ import { defineTool } from '@tools/defineTool';
|
||||
import { lazy } from 'react';
|
||||
|
||||
export const tool = defineTool('image-generic', {
|
||||
name: 'Compress Image',
|
||||
i18n: {
|
||||
name: 'image:compress.title',
|
||||
description: 'image:compress.description',
|
||||
shortDescription: 'image:compress.shortDescription'
|
||||
},
|
||||
|
||||
path: 'compress',
|
||||
component: lazy(() => import('./index')),
|
||||
icon: 'material-symbols-light:compress-rounded',
|
||||
description:
|
||||
'Compress images to reduce file size while maintaining reasonable quality.',
|
||||
shortDescription:
|
||||
'Compress images to reduce file size while maintaining reasonable quality.',
|
||||
|
||||
keywords: ['image', 'compress', 'reduce', 'quality']
|
||||
});
|
||||
|
||||
@@ -2,12 +2,15 @@ import { defineTool } from '@tools/defineTool';
|
||||
import { lazy } from 'react';
|
||||
|
||||
export const tool = defineTool('image-generic', {
|
||||
name: 'Convert Images to JPG',
|
||||
i18n: {
|
||||
name: 'image:convertToJpg.title',
|
||||
description: 'image:convertToJpg.description',
|
||||
shortDescription: 'image:convertToJpg.shortDescription'
|
||||
},
|
||||
|
||||
path: 'convert-to-jpg',
|
||||
icon: 'ph:file-jpg-thin',
|
||||
description:
|
||||
'Convert various image formats (PNG, GIF, TIF, PSD, SVG, WEBP, HEIC, RAW) to JPG with customizable quality and background color settings.',
|
||||
shortDescription: 'Convert images to JPG with quality control',
|
||||
|
||||
keywords: [
|
||||
'convert',
|
||||
'jpg',
|
||||
|
||||
@@ -2,12 +2,15 @@ import { defineTool } from '@tools/defineTool';
|
||||
import { lazy } from 'react';
|
||||
|
||||
export const tool = defineTool('image-generic', {
|
||||
name: 'Create transparent PNG',
|
||||
i18n: {
|
||||
name: 'image:createTransparent.title',
|
||||
description: 'image:createTransparent.description',
|
||||
shortDescription: 'image:createTransparent.shortDescription'
|
||||
},
|
||||
|
||||
path: 'create-transparent',
|
||||
icon: 'mdi:circle-transparent',
|
||||
shortDescription: 'Quickly make an image transparent',
|
||||
description:
|
||||
"World's simplest online Portable Network Graphics transparency maker. Just import your image in the editor on the left and you will instantly get a transparent PNG on the right. Free, quick, and very powerful. Import an image – get a transparent PNG.",
|
||||
|
||||
keywords: ['create', 'transparent'],
|
||||
component: lazy(() => import('./index'))
|
||||
});
|
||||
|
||||
@@ -2,11 +2,15 @@ import { defineTool } from '@tools/defineTool';
|
||||
import { lazy } from 'react';
|
||||
|
||||
export const tool = defineTool('image-generic', {
|
||||
name: 'Crop',
|
||||
i18n: {
|
||||
name: 'image:crop.title',
|
||||
description: 'image:crop.description',
|
||||
shortDescription: 'image:crop.shortDescription'
|
||||
},
|
||||
|
||||
path: 'crop',
|
||||
icon: 'mdi:crop', // Iconify icon as a string
|
||||
description: 'A tool to crop images with precision and ease.',
|
||||
shortDescription: 'Crop images quickly.',
|
||||
|
||||
keywords: ['crop', 'image', 'edit', 'resize', 'trim'],
|
||||
component: lazy(() => import('./index'))
|
||||
});
|
||||
|
||||
@@ -2,12 +2,15 @@ import { defineTool } from '@tools/defineTool';
|
||||
import { lazy } from 'react';
|
||||
|
||||
export const tool = defineTool('image-generic', {
|
||||
name: 'Image Editor',
|
||||
i18n: {
|
||||
name: 'image:editor.title',
|
||||
description: 'image:editor.description',
|
||||
shortDescription: 'image:editor.shortDescription'
|
||||
},
|
||||
|
||||
path: 'editor',
|
||||
icon: 'mdi:image-edit',
|
||||
description:
|
||||
'Advanced image editor with tools for cropping, rotating, annotating, adjusting colors, and adding watermarks. Edit your images with professional-grade tools directly in your browser.',
|
||||
shortDescription: 'Edit images with advanced tools and features',
|
||||
|
||||
keywords: [
|
||||
'image',
|
||||
'editor',
|
||||
|
||||
@@ -2,12 +2,15 @@ import { defineTool } from '@tools/defineTool';
|
||||
import { lazy } from 'react';
|
||||
|
||||
export const tool = defineTool('image-generic', {
|
||||
name: 'Image to Text (OCR)',
|
||||
i18n: {
|
||||
name: 'image:imageToText.title',
|
||||
description: 'image:imageToText.description',
|
||||
shortDescription: 'image:imageToText.shortDescription'
|
||||
},
|
||||
|
||||
path: 'image-to-text',
|
||||
icon: 'mdi:text-recognition', // Iconify icon as a string
|
||||
description:
|
||||
'Extract text from images (JPG, PNG) using optical character recognition (OCR).',
|
||||
shortDescription: 'Extract text from images using OCR.',
|
||||
|
||||
keywords: [
|
||||
'ocr',
|
||||
'optical character recognition',
|
||||
|
||||
@@ -2,12 +2,14 @@ import { defineTool } from '@tools/defineTool';
|
||||
import { lazy } from 'react';
|
||||
|
||||
export const tool = defineTool('image-generic', {
|
||||
name: 'QR Code Generator',
|
||||
i18n: {
|
||||
name: 'image:qrCode.title',
|
||||
description: 'image:qrCode.description',
|
||||
shortDescription: 'image:qrCode.shortDescription'
|
||||
},
|
||||
|
||||
path: 'qr-code',
|
||||
icon: 'mdi:qrcode', // Iconify icon as a string
|
||||
description:
|
||||
'Generate QR codes for different data types: URL, Text, Email, Phone, SMS, WiFi, vCard, and more.',
|
||||
shortDescription: 'Create customized QR codes for various data formats.',
|
||||
keywords: [
|
||||
'qr code',
|
||||
'qrcode',
|
||||
|
||||
@@ -2,12 +2,15 @@ import { defineTool } from '@tools/defineTool';
|
||||
import { lazy } from 'react';
|
||||
|
||||
export const tool = defineTool('image-generic', {
|
||||
name: 'Remove Background from Image',
|
||||
i18n: {
|
||||
name: 'image:removeBackground.title',
|
||||
description: 'image:removeBackground.description',
|
||||
shortDescription: 'image:removeBackground.shortDescription'
|
||||
},
|
||||
|
||||
path: 'remove-background',
|
||||
icon: 'mdi:image-remove',
|
||||
description:
|
||||
"World's simplest online tool to remove backgrounds from images. Just upload your image and our AI-powered tool will automatically remove the background, giving you a transparent PNG. Perfect for product photos, profile pictures, and design assets.",
|
||||
shortDescription: 'Automatically remove backgrounds from images',
|
||||
|
||||
keywords: [
|
||||
'remove',
|
||||
'background',
|
||||
|
||||
@@ -11,6 +11,7 @@ import SimpleRadio from '@components/options/SimpleRadio';
|
||||
import CheckboxWithDesc from '@components/options/CheckboxWithDesc';
|
||||
import { processImage } from './service';
|
||||
import { InitialValuesType } from './types';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
const initialValues: InitialValuesType = {
|
||||
resizeMethod: 'pixels' as 'pixels' | 'percentage',
|
||||
@@ -45,6 +46,7 @@ const validationSchema = Yup.object({
|
||||
});
|
||||
|
||||
export default function ResizeImage({ title }: ToolComponentProps) {
|
||||
const { t } = useTranslation('image');
|
||||
const [input, setInput] = useState<File | null>(null);
|
||||
const [result, setResult] = useState<File | null>(null);
|
||||
|
||||
@@ -58,22 +60,20 @@ export default function ResizeImage({ title }: ToolComponentProps) {
|
||||
updateField
|
||||
}) => [
|
||||
{
|
||||
title: 'Resize Method',
|
||||
title: t('resize.resizeMethod'),
|
||||
component: (
|
||||
<Box>
|
||||
<SimpleRadio
|
||||
onClick={() => updateField('resizeMethod', 'pixels')}
|
||||
checked={values.resizeMethod === 'pixels'}
|
||||
description={'Resize by specifying dimensions in pixels.'}
|
||||
title={'Resize by Pixels'}
|
||||
description={t('resize.resizeByPixelsDescription')}
|
||||
title={t('resize.resizeByPixels')}
|
||||
/>
|
||||
<SimpleRadio
|
||||
onClick={() => updateField('resizeMethod', 'percentage')}
|
||||
checked={values.resizeMethod === 'percentage'}
|
||||
description={
|
||||
'Resize by specifying a percentage of the original size.'
|
||||
}
|
||||
title={'Resize by Percentage'}
|
||||
description={t('resize.resizeByPercentageDescription')}
|
||||
title={t('resize.resizeByPercentage')}
|
||||
/>
|
||||
</Box>
|
||||
)
|
||||
@@ -81,7 +81,7 @@ export default function ResizeImage({ title }: ToolComponentProps) {
|
||||
...(values.resizeMethod === 'pixels'
|
||||
? [
|
||||
{
|
||||
title: 'Dimension Type',
|
||||
title: t('resize.dimensionType'),
|
||||
component: (
|
||||
<Box>
|
||||
<CheckboxWithDesc
|
||||
@@ -89,35 +89,29 @@ export default function ResizeImage({ title }: ToolComponentProps) {
|
||||
onChange={(value) =>
|
||||
updateField('maintainAspectRatio', value)
|
||||
}
|
||||
description={
|
||||
'Maintain the original aspect ratio of the image.'
|
||||
}
|
||||
title={'Maintain Aspect Ratio'}
|
||||
description={t('resize.maintainAspectRatioDescription')}
|
||||
title={t('resize.maintainAspectRatio')}
|
||||
/>
|
||||
{values.maintainAspectRatio && (
|
||||
<Box>
|
||||
<SimpleRadio
|
||||
onClick={() => updateField('dimensionType', 'width')}
|
||||
checked={values.dimensionType === 'width'}
|
||||
description={
|
||||
'Specify the width in pixels and calculate height based on aspect ratio.'
|
||||
}
|
||||
title={'Set Width'}
|
||||
description={t('resize.setWidthDescription')}
|
||||
title={t('resize.setWidth')}
|
||||
/>
|
||||
<SimpleRadio
|
||||
onClick={() => updateField('dimensionType', 'height')}
|
||||
checked={values.dimensionType === 'height'}
|
||||
description={
|
||||
'Specify the height in pixels and calculate width based on aspect ratio.'
|
||||
}
|
||||
title={'Set Height'}
|
||||
description={t('resize.setHeightDescription')}
|
||||
title={t('resize.setHeight')}
|
||||
/>
|
||||
</Box>
|
||||
)}
|
||||
<TextFieldWithDesc
|
||||
value={values.width}
|
||||
onOwnChange={(val) => updateField('width', val)}
|
||||
description={'Width (in pixels)'}
|
||||
description={t('resize.widthDescription')}
|
||||
disabled={
|
||||
values.maintainAspectRatio &&
|
||||
values.dimensionType === 'height'
|
||||
@@ -131,7 +125,7 @@ export default function ResizeImage({ title }: ToolComponentProps) {
|
||||
<TextFieldWithDesc
|
||||
value={values.height}
|
||||
onOwnChange={(val) => updateField('height', val)}
|
||||
description={'Height (in pixels)'}
|
||||
description={t('resize.heightDescription')}
|
||||
disabled={
|
||||
values.maintainAspectRatio &&
|
||||
values.dimensionType === 'width'
|
||||
@@ -148,15 +142,13 @@ export default function ResizeImage({ title }: ToolComponentProps) {
|
||||
]
|
||||
: [
|
||||
{
|
||||
title: 'Percentage',
|
||||
title: t('resize.percentage'),
|
||||
component: (
|
||||
<Box>
|
||||
<TextFieldWithDesc
|
||||
value={values.percentage}
|
||||
onOwnChange={(val) => updateField('percentage', val)}
|
||||
description={
|
||||
'Percentage of original size (e.g., 50 for half size, 200 for double size)'
|
||||
}
|
||||
description={t('resize.percentageDescription')}
|
||||
inputProps={{
|
||||
'data-testid': 'percentage-input',
|
||||
type: 'number',
|
||||
@@ -183,20 +175,19 @@ export default function ResizeImage({ title }: ToolComponentProps) {
|
||||
value={input}
|
||||
onChange={setInput}
|
||||
accept={['image/jpeg', 'image/png', 'image/svg+xml', 'image/gif']}
|
||||
title={'Input Image'}
|
||||
title={t('resize.inputTitle')}
|
||||
/>
|
||||
}
|
||||
resultComponent={
|
||||
<ToolFileResult
|
||||
title={'Resized Image'}
|
||||
title={t('resize.resultTitle')}
|
||||
value={result}
|
||||
extension={input?.name.split('.').pop() || 'png'}
|
||||
/>
|
||||
}
|
||||
toolInfo={{
|
||||
title: 'Resize Image',
|
||||
description:
|
||||
'This tool allows you to resize JPG, PNG, SVG, or GIF images. You can resize by specifying dimensions in pixels or by percentage, with options to maintain the original aspect ratio.'
|
||||
title: t('resize.toolInfo.title'),
|
||||
description: t('resize.toolInfo.description')
|
||||
}}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -2,12 +2,15 @@ import { defineTool } from '@tools/defineTool';
|
||||
import { lazy } from 'react';
|
||||
|
||||
export const tool = defineTool('image-generic', {
|
||||
name: 'Resize Image',
|
||||
i18n: {
|
||||
name: 'image:resize.title',
|
||||
description: 'image:resize.description',
|
||||
shortDescription: 'image:resize.shortDescription'
|
||||
},
|
||||
|
||||
path: 'resize',
|
||||
icon: 'mdi:resize', // Iconify icon as a string
|
||||
description:
|
||||
'Resize JPG, PNG, SVG or GIF images by pixels or percentage while maintaining aspect ratio or not.',
|
||||
shortDescription: 'Resize images easily.',
|
||||
|
||||
keywords: [
|
||||
'resize',
|
||||
'image',
|
||||
|
||||
@@ -2,11 +2,15 @@ import { defineTool } from '@tools/defineTool';
|
||||
import { lazy } from 'react';
|
||||
|
||||
export const tool = defineTool('image-generic', {
|
||||
name: 'Rotate Image',
|
||||
i18n: {
|
||||
name: 'image:rotate.title',
|
||||
description: 'image:rotate.description',
|
||||
shortDescription: 'image:rotate.shortDescription'
|
||||
},
|
||||
|
||||
path: 'rotate',
|
||||
icon: 'mdi:rotate-clockwise',
|
||||
description: 'Rotate an image by a specified angle.',
|
||||
shortDescription: 'Rotate an image easily.',
|
||||
|
||||
keywords: ['rotate', 'image', 'angle', 'jpg', 'png'],
|
||||
component: lazy(() => import('./index'))
|
||||
});
|
||||
|
||||
@@ -3,12 +3,15 @@ import { lazy } from 'react';
|
||||
// import image from '@assets/text.png';
|
||||
|
||||
export const tool = defineTool('png', {
|
||||
name: 'Compress png',
|
||||
i18n: {
|
||||
name: 'image:compressPng.title',
|
||||
description: 'image:compressPng.description',
|
||||
shortDescription: 'image:compressPng.shortDescription'
|
||||
},
|
||||
|
||||
path: 'compress-png',
|
||||
icon: 'material-symbols-light:compress',
|
||||
description:
|
||||
'This is a program that compresses PNG pictures. As soon as you paste your PNG picture in the input area, the program will compress it and show the result in the output area. In the options, you can adjust the compression level, as well as find the old and new picture file sizes.',
|
||||
shortDescription: 'Quickly compress a PNG',
|
||||
|
||||
keywords: ['compress', 'png'],
|
||||
component: lazy(() => import('./index'))
|
||||
});
|
||||
|
||||
@@ -2,12 +2,15 @@ import { defineTool } from '@tools/defineTool';
|
||||
import { lazy } from 'react';
|
||||
|
||||
export const tool = defineTool('png', {
|
||||
name: 'Convert JPG to PNG',
|
||||
i18n: {
|
||||
name: 'image:convertJgpToPng.title',
|
||||
description: 'image:convertJgpToPng.description',
|
||||
shortDescription: 'image:convertJgpToPng.shortDescription'
|
||||
},
|
||||
|
||||
path: 'convert-jgp-to-png',
|
||||
icon: 'ph:file-jpg-thin',
|
||||
description:
|
||||
'Quickly convert your JPG images to PNG. Just import your PNG image in the editor on the left',
|
||||
shortDescription: 'Quickly convert your JPG images to PNG',
|
||||
|
||||
keywords: ['convert', 'jgp', 'png'],
|
||||
component: lazy(() => import('./index'))
|
||||
});
|
||||
|
||||
@@ -2,13 +2,14 @@ import { defineTool } from '@tools/defineTool';
|
||||
import { lazy } from 'react';
|
||||
|
||||
export const tool = defineTool('json', {
|
||||
name: 'Escape JSON',
|
||||
path: 'escape-json',
|
||||
icon: 'lets-icons:json-light',
|
||||
description:
|
||||
'Free online JSON escaper. Just load your JSON in the input field and it will automatically get escaped. In the tool options, you can optionally enable wrapping the escaped JSON in double quotes to get an escaped JSON string.',
|
||||
shortDescription: 'Quickly escape special JSON characters.',
|
||||
longDescription: `This tool converts special characters in JSON files and data structures into their escaped versions. Such special characters are, for example, double quotes, newline characters, backslashes, tabs, and many others. If these characters aren't escaped and appear in a raw JSON string without escaping, they can lead to errors in data parsing. The program turns them into safe versions by adding a backslash (\\) before the character, changing its interpretation. Additionally, you can enable the "Wrap Output in Quotes" checkbox in the options, which adds double quotes around the resulting escaped JSON data. This is useful when the escaped JSON data needs to be used as a string in other data structures or the JavaScript programming language. Json-abulous!`,
|
||||
keywords: ['escape', 'json'],
|
||||
component: lazy(() => import('./index'))
|
||||
icon: 'material-symbols:code',
|
||||
|
||||
keywords: ['json', 'escape', 'characters', 'format'],
|
||||
component: lazy(() => import('./index')),
|
||||
i18n: {
|
||||
name: 'json:escapeJson.title',
|
||||
description: 'json:escapeJson.description',
|
||||
shortDescription: 'json:escapeJson.shortDescription'
|
||||
}
|
||||
});
|
||||
|
||||
@@ -2,11 +2,14 @@ import { defineTool } from '@tools/defineTool';
|
||||
import { lazy } from 'react';
|
||||
|
||||
export const tool = defineTool('json', {
|
||||
name: 'Convert JSON to XML',
|
||||
path: 'json-to-xml',
|
||||
icon: 'mdi-light:xml',
|
||||
description: 'Convert JSON files to XML format with customizable options.',
|
||||
shortDescription: 'Convert JSON data to XML format',
|
||||
keywords: ['json', 'xml', 'convert', 'transform', 'parse'],
|
||||
component: lazy(() => import('./index'))
|
||||
icon: 'material-symbols:code',
|
||||
|
||||
keywords: ['json', 'xml', 'convert', 'transform'],
|
||||
component: lazy(() => import('./index')),
|
||||
i18n: {
|
||||
name: 'json:jsonToXml.title',
|
||||
description: 'json:jsonToXml.description',
|
||||
shortDescription: 'json:jsonToXml.shortDescription'
|
||||
}
|
||||
});
|
||||
|
||||
@@ -5,6 +5,7 @@ import ToolTextResult from '@components/result/ToolTextResult';
|
||||
import { minifyJson } from './service';
|
||||
import { CardExampleType } from '@components/examples/ToolExamples';
|
||||
import { ToolComponentProps } from '@tools/defineTool';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
type InitialValuesType = Record<string, never>;
|
||||
|
||||
@@ -47,6 +48,7 @@ const exampleCards: CardExampleType<InitialValuesType>[] = [
|
||||
];
|
||||
|
||||
export default function MinifyJson({ title }: ToolComponentProps) {
|
||||
const { t } = useTranslation('json');
|
||||
const [input, setInput] = useState<string>('');
|
||||
const [result, setResult] = useState<string>('');
|
||||
|
||||
@@ -58,11 +60,15 @@ export default function MinifyJson({ title }: ToolComponentProps) {
|
||||
<ToolContent
|
||||
title={title}
|
||||
inputComponent={
|
||||
<ToolTextInput title="Input JSON" value={input} onChange={setInput} />
|
||||
<ToolTextInput
|
||||
title={t('minify.inputTitle')}
|
||||
value={input}
|
||||
onChange={setInput}
|
||||
/>
|
||||
}
|
||||
resultComponent={
|
||||
<ToolTextResult
|
||||
title="Minified JSON"
|
||||
title={t('minify.resultTitle')}
|
||||
value={result}
|
||||
extension={'json'}
|
||||
/>
|
||||
@@ -70,9 +76,8 @@ export default function MinifyJson({ title }: ToolComponentProps) {
|
||||
initialValues={initialValues}
|
||||
getGroups={null}
|
||||
toolInfo={{
|
||||
title: 'What Is JSON Minification?',
|
||||
description:
|
||||
"JSON minification is the process of removing all unnecessary whitespace characters from JSON data while maintaining its validity. This includes removing spaces, newlines, and indentation that aren't required for the JSON to be parsed correctly. Minification reduces the size of JSON data, making it more efficient for storage and transmission while keeping the exact same data structure and values."
|
||||
title: t('minify.toolInfo.title'),
|
||||
description: t('minify.toolInfo.description')
|
||||
}}
|
||||
exampleCards={exampleCards}
|
||||
input={input}
|
||||
|
||||
@@ -2,12 +2,14 @@ import { defineTool } from '@tools/defineTool';
|
||||
import { lazy } from 'react';
|
||||
|
||||
export const tool = defineTool('json', {
|
||||
name: 'Minify JSON',
|
||||
path: 'minify',
|
||||
icon: 'lets-icons:json-light',
|
||||
description:
|
||||
'Minify your JSON by removing all unnecessary whitespace and formatting. This tool compresses JSON data to its smallest possible size while maintaining valid JSON structure.',
|
||||
shortDescription: 'Quickly compress JSON file.',
|
||||
keywords: ['minify', 'compress', 'minimize', 'json', 'compact'],
|
||||
component: lazy(() => import('./index'))
|
||||
icon: 'material-symbols:code',
|
||||
|
||||
keywords: ['json', 'minify', 'compress', 'whitespace'],
|
||||
component: lazy(() => import('./index')),
|
||||
i18n: {
|
||||
name: 'json:minify.title',
|
||||
description: 'json:minify.description',
|
||||
shortDescription: 'json:minify.shortDescription'
|
||||
}
|
||||
});
|
||||
|
||||
@@ -14,6 +14,7 @@ import RadioWithTextField from '@components/options/RadioWithTextField';
|
||||
import SimpleRadio from '@components/options/SimpleRadio';
|
||||
import { isNumber, updateNumberField } from '../../../../utils/string';
|
||||
import ToolContent from '@components/ToolContent';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
type InitialValuesType = {
|
||||
indentationType: 'tab' | 'space';
|
||||
@@ -115,6 +116,7 @@ const exampleCards: CardExampleType<InitialValuesType>[] = [
|
||||
];
|
||||
|
||||
export default function PrettifyJson({ title }: ToolComponentProps) {
|
||||
const { t } = useTranslation('json');
|
||||
const [input, setInput] = useState<string>('');
|
||||
const [result, setResult] = useState<string>('');
|
||||
|
||||
@@ -128,11 +130,15 @@ export default function PrettifyJson({ title }: ToolComponentProps) {
|
||||
title={title}
|
||||
input={input}
|
||||
inputComponent={
|
||||
<ToolTextInput title={'Input JSON'} value={input} onChange={setInput} />
|
||||
<ToolTextInput
|
||||
title={t('prettify.inputTitle')}
|
||||
value={input}
|
||||
onChange={setInput}
|
||||
/>
|
||||
}
|
||||
resultComponent={
|
||||
<ToolTextResult
|
||||
title={'Pretty JSON'}
|
||||
title={t('prettify.resultTitle')}
|
||||
value={result}
|
||||
extension={'json'}
|
||||
/>
|
||||
@@ -140,14 +146,14 @@ export default function PrettifyJson({ title }: ToolComponentProps) {
|
||||
initialValues={initialValues}
|
||||
getGroups={({ values, updateField }) => [
|
||||
{
|
||||
title: 'Indentation',
|
||||
title: t('prettify.indentation'),
|
||||
component: (
|
||||
<Box>
|
||||
<RadioWithTextField
|
||||
checked={values.indentationType === 'space'}
|
||||
title={'Use Spaces'}
|
||||
title={t('prettify.useSpaces')}
|
||||
fieldName={'indentationType'}
|
||||
description={'Indent output with spaces'}
|
||||
description={t('prettify.useSpacesDescription')}
|
||||
value={values.spacesCount.toString()}
|
||||
onRadioClick={() => updateField('indentationType', 'space')}
|
||||
onTextChange={(val) =>
|
||||
@@ -157,8 +163,8 @@ export default function PrettifyJson({ title }: ToolComponentProps) {
|
||||
<SimpleRadio
|
||||
onClick={() => updateField('indentationType', 'tab')}
|
||||
checked={values.indentationType === 'tab'}
|
||||
description={'Indent output with tabs.'}
|
||||
title={'Use Tabs'}
|
||||
description={t('prettify.useTabsDescription')}
|
||||
title={t('prettify.useTabs')}
|
||||
/>
|
||||
</Box>
|
||||
)
|
||||
@@ -168,9 +174,8 @@ export default function PrettifyJson({ title }: ToolComponentProps) {
|
||||
setInput={setInput}
|
||||
exampleCards={exampleCards}
|
||||
toolInfo={{
|
||||
title: 'What Is a JSON Prettifier?',
|
||||
description:
|
||||
'This tool adds consistent formatting to the data in JavaScript Object Notation (JSON) format. This transformation makes the JSON code more readable, making it easier to understand and edit. The program parses the JSON data structure into tokens and then reformats them by adding indentation and line breaks. If the data is hierarchial, then it adds indentation at the beginning of lines to visually show the depth of the JSON and adds newlines to break long single-line JSON arrays into multiple shorter, more readable ones. Additionally, this utility can remove unnecessary spaces and tabs from your JSON code (especially leading and trailing whitespaces), making it more compact. You can choose the line indentation method in the options: indent with spaces or indent with tabs. When using spaces, you can also specify how many spaces to use for each indentation level (usually 2 or 4 spaces). '
|
||||
title: t('prettify.toolInfo.title'),
|
||||
description: t('prettify.toolInfo.description')
|
||||
}}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -2,12 +2,14 @@ import { defineTool } from '@tools/defineTool';
|
||||
import { lazy } from 'react';
|
||||
|
||||
export const tool = defineTool('json', {
|
||||
name: 'Prettify JSON',
|
||||
path: 'prettify',
|
||||
icon: 'lets-icons:json-light',
|
||||
description:
|
||||
"Just load your JSON in the input field and it will automatically get prettified. In the tool options, you can choose whether to use spaces or tabs for indentation and if you're using spaces, you can specify the number of spaces to add per indentation level.",
|
||||
shortDescription: 'Quickly beautify a JSON data structure.',
|
||||
keywords: ['prettify'],
|
||||
component: lazy(() => import('./index'))
|
||||
icon: 'material-symbols:code',
|
||||
|
||||
keywords: ['json', 'prettify', 'format', 'beautify'],
|
||||
component: lazy(() => import('./index')),
|
||||
i18n: {
|
||||
name: 'json:prettify.title',
|
||||
description: 'json:prettify.description',
|
||||
shortDescription: 'json:prettify.shortDescription'
|
||||
}
|
||||
});
|
||||
|
||||
@@ -2,20 +2,14 @@ import { defineTool } from '@tools/defineTool';
|
||||
import { lazy } from 'react';
|
||||
|
||||
export const tool = defineTool('json', {
|
||||
name: 'Stringify JSON',
|
||||
path: 'stringify',
|
||||
icon: 'ant-design:field-string-outlined',
|
||||
description:
|
||||
'Convert JavaScript objects and arrays into their JSON string representation. Options include custom indentation and HTML character escaping for web-safe JSON strings.',
|
||||
shortDescription: 'Convert JavaScript objects to JSON strings',
|
||||
keywords: [
|
||||
'stringify',
|
||||
'serialize',
|
||||
'convert',
|
||||
'object',
|
||||
'array',
|
||||
'json',
|
||||
'string'
|
||||
],
|
||||
component: lazy(() => import('./index'))
|
||||
icon: 'material-symbols:code',
|
||||
|
||||
keywords: ['json', 'stringify', 'serialize', 'convert'],
|
||||
component: lazy(() => import('./index')),
|
||||
i18n: {
|
||||
name: 'json:stringify.title',
|
||||
description: 'json:stringify.description',
|
||||
shortDescription: 'json:stringify.shortDescription'
|
||||
}
|
||||
});
|
||||
|
||||
@@ -2,14 +2,14 @@ import { defineTool } from '@tools/defineTool';
|
||||
import { lazy } from 'react';
|
||||
|
||||
export const tool = defineTool('json', {
|
||||
name: 'Convert TSV to JSON',
|
||||
path: 'tsv-to-json',
|
||||
icon: 'material-symbols:tsv-rounded',
|
||||
description:
|
||||
'Convert TSV files to JSON format with customizable options for delimiters, quotes, and output formatting. Support for headers, comments, and dynamic type conversion.',
|
||||
shortDescription: 'Convert TSV data to JSON format.',
|
||||
longDescription:
|
||||
'This tool allows you to convert TSV (Tab-Separated Values) files into JSON format. You can customize the conversion process by specifying delimiters, quote characters, and whether to use headers. It also supports dynamic type conversion for values, handling comments, and skipping empty lines. The output can be formatted with indentation or minified as needed.',
|
||||
keywords: ['tsv', 'json', 'convert', 'transform', 'parse'],
|
||||
component: lazy(() => import('./index'))
|
||||
icon: 'material-symbols:code',
|
||||
|
||||
keywords: ['tsv', 'json', 'convert', 'tabular'],
|
||||
component: lazy(() => import('./index')),
|
||||
i18n: {
|
||||
name: 'json:tsvToJson.title',
|
||||
description: 'json:tsvToJson.description',
|
||||
shortDescription: 'json:tsvToJson.shortDescription'
|
||||
}
|
||||
});
|
||||
|
||||
@@ -5,6 +5,7 @@ import { CardExampleType } from '@components/examples/ToolExamples';
|
||||
import { validateJson } from './service';
|
||||
import { ToolComponentProps } from '@tools/defineTool';
|
||||
import ToolContent from '@components/ToolContent';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
const exampleCards: CardExampleType<{}>[] = [
|
||||
{
|
||||
@@ -46,6 +47,7 @@ const exampleCards: CardExampleType<{}>[] = [
|
||||
];
|
||||
|
||||
export default function ValidateJson({ title }: ToolComponentProps) {
|
||||
const { t } = useTranslation('json');
|
||||
const [input, setInput] = useState<string>('');
|
||||
const [result, setResult] = useState<string>('');
|
||||
|
||||
@@ -53,9 +55,9 @@ export default function ValidateJson({ title }: ToolComponentProps) {
|
||||
const { valid, error } = validateJson(input);
|
||||
|
||||
if (valid) {
|
||||
setResult('✅ Valid JSON');
|
||||
setResult(t('validateJson.validJson'));
|
||||
} else {
|
||||
setResult(`❌ ${error}`);
|
||||
setResult(t('validateJson.invalidJson', { error }));
|
||||
}
|
||||
};
|
||||
|
||||
@@ -63,25 +65,20 @@ export default function ValidateJson({ title }: ToolComponentProps) {
|
||||
<ToolContent
|
||||
title={title}
|
||||
inputComponent={
|
||||
<ToolTextInput title="Input JSON" value={input} onChange={setInput} />
|
||||
<ToolTextInput
|
||||
title={t('validateJson.inputTitle')}
|
||||
value={input}
|
||||
onChange={setInput}
|
||||
/>
|
||||
}
|
||||
resultComponent={
|
||||
<ToolTextResult title="Validation Result" value={result} />
|
||||
<ToolTextResult title={t('validateJson.resultTitle')} value={result} />
|
||||
}
|
||||
initialValues={{}}
|
||||
getGroups={null}
|
||||
toolInfo={{
|
||||
title: 'What is JSON Validation?',
|
||||
description: `
|
||||
JSON (JavaScript Object Notation) is a lightweight data-interchange format.
|
||||
JSON validation ensures that the structure of the data conforms to the JSON standard.
|
||||
A valid JSON object must have:
|
||||
- Property names enclosed in double quotes.
|
||||
- Properly balanced curly braces {}.
|
||||
- No trailing commas after the last key-value pair.
|
||||
- Proper nesting of objects and arrays.
|
||||
This tool checks the input JSON and provides feedback to help identify and fix common errors.
|
||||
`
|
||||
title: t('validateJson.toolInfo.title'),
|
||||
description: t('validateJson.toolInfo.description')
|
||||
}}
|
||||
exampleCards={exampleCards}
|
||||
input={input}
|
||||
|
||||
@@ -2,12 +2,14 @@ import { defineTool } from '@tools/defineTool';
|
||||
import { lazy } from 'react';
|
||||
|
||||
export const tool = defineTool('json', {
|
||||
name: 'Validate JSON',
|
||||
path: 'validateJson',
|
||||
icon: 'lets-icons:json-light',
|
||||
description:
|
||||
'Validate JSON data and identify formatting issues such as missing quotes, trailing commas, and incorrect brackets.',
|
||||
shortDescription: 'Quickly validate a JSON data structure.',
|
||||
keywords: ['validate', 'json', 'syntax'],
|
||||
component: lazy(() => import('./index'))
|
||||
path: 'validate-json',
|
||||
icon: 'material-symbols:check-circle',
|
||||
|
||||
keywords: ['json', 'validate', 'check', 'syntax', 'errors'],
|
||||
component: lazy(() => import('./index')),
|
||||
i18n: {
|
||||
name: 'json:validateJson.title',
|
||||
description: 'json:validateJson.description',
|
||||
shortDescription: 'json:validateJson.shortDescription'
|
||||
}
|
||||
});
|
||||
|
||||
@@ -11,6 +11,7 @@ import TextFieldWithDesc from '@components/options/TextFieldWithDesc';
|
||||
import SimpleRadio from '@components/options/SimpleRadio';
|
||||
import CheckboxWithDesc from '@components/options/CheckboxWithDesc';
|
||||
import * as Yup from 'yup';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
interface InitialValuesType {
|
||||
splitOperatorType: SplitOperatorType;
|
||||
@@ -101,6 +102,7 @@ const exampleCards: CardExampleType<InitialValuesType>[] = [
|
||||
];
|
||||
|
||||
export default function Duplicate({ title }: ToolComponentProps) {
|
||||
const { t } = useTranslation('list');
|
||||
const [input, setInput] = useState<string>('');
|
||||
const [result, setResult] = useState<string>('');
|
||||
|
||||
@@ -121,9 +123,9 @@ export default function Duplicate({ title }: ToolComponentProps) {
|
||||
);
|
||||
} catch (error) {
|
||||
if (error instanceof Error) {
|
||||
setResult(`Error: ${error.message}`);
|
||||
setResult(`${t('duplicate.error')}: ${error.message}`);
|
||||
} else {
|
||||
setResult('An unknown error occurred');
|
||||
setResult(t('duplicate.unknownError'));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -134,55 +136,53 @@ export default function Duplicate({ title }: ToolComponentProps) {
|
||||
updateField
|
||||
}) => [
|
||||
{
|
||||
title: 'Split Options',
|
||||
title: t('duplicate.splitOptions'),
|
||||
component: (
|
||||
<Box>
|
||||
<SimpleRadio
|
||||
onClick={() => updateField('splitOperatorType', 'symbol')}
|
||||
checked={values.splitOperatorType === 'symbol'}
|
||||
title={'Split by Symbol'}
|
||||
title={t('duplicate.splitBySymbol')}
|
||||
/>
|
||||
<SimpleRadio
|
||||
onClick={() => updateField('splitOperatorType', 'regex')}
|
||||
checked={values.splitOperatorType === 'regex'}
|
||||
title={'Split by Regular Expression'}
|
||||
title={t('duplicate.splitByRegex')}
|
||||
/>
|
||||
<TextFieldWithDesc
|
||||
value={values.splitSeparator}
|
||||
onOwnChange={(val) => updateField('splitSeparator', val)}
|
||||
description={'Separator to split the list'}
|
||||
description={t('duplicate.splitSeparatorDescription')}
|
||||
/>
|
||||
<TextFieldWithDesc
|
||||
value={values.joinSeparator}
|
||||
onOwnChange={(val) => updateField('joinSeparator', val)}
|
||||
description={'Separator to join the duplicated list'}
|
||||
description={t('duplicate.joinSeparatorDescription')}
|
||||
/>
|
||||
</Box>
|
||||
)
|
||||
},
|
||||
{
|
||||
title: 'Duplication Options',
|
||||
title: t('duplicate.duplicationOptions'),
|
||||
component: (
|
||||
<Box>
|
||||
<TextFieldWithDesc
|
||||
value={values.copy}
|
||||
onOwnChange={(val) => updateField('copy', val)}
|
||||
description={'Number of copies (can be fractional)'}
|
||||
description={t('duplicate.copyDescription')}
|
||||
type="number"
|
||||
/>
|
||||
<CheckboxWithDesc
|
||||
title={'Concatenate'}
|
||||
title={t('duplicate.concatenate')}
|
||||
checked={values.concatenate}
|
||||
onChange={(checked) => updateField('concatenate', checked)}
|
||||
description={
|
||||
'Concatenate copies (if unchecked, items will be interweaved)'
|
||||
}
|
||||
description={t('duplicate.concatenateDescription')}
|
||||
/>
|
||||
<CheckboxWithDesc
|
||||
title={'Reverse'}
|
||||
title={t('duplicate.reverse')}
|
||||
checked={values.reverse}
|
||||
onChange={(checked) => updateField('reverse', checked)}
|
||||
description={'Reverse the duplicated items'}
|
||||
description={t('duplicate.reverseDescription')}
|
||||
/>
|
||||
</Box>
|
||||
)
|
||||
@@ -193,18 +193,21 @@ export default function Duplicate({ title }: ToolComponentProps) {
|
||||
<ToolContent
|
||||
title={title}
|
||||
inputComponent={
|
||||
<ToolTextInput title="Input List" value={input} onChange={setInput} />
|
||||
<ToolTextInput
|
||||
title={t('duplicate.inputTitle')}
|
||||
value={input}
|
||||
onChange={setInput}
|
||||
/>
|
||||
}
|
||||
resultComponent={
|
||||
<ToolTextResult title="Duplicated List" value={result} />
|
||||
<ToolTextResult title={t('duplicate.resultTitle')} value={result} />
|
||||
}
|
||||
initialValues={initialValues}
|
||||
getGroups={getGroups}
|
||||
validationSchema={validationSchema}
|
||||
toolInfo={{
|
||||
title: 'List Duplication',
|
||||
description:
|
||||
"This tool allows you to duplicate items in a list. You can specify the number of copies (including fractional values), control whether items are concatenated or interweaved, and even reverse the duplicated items. It's useful for creating repeated patterns, generating test data, or expanding lists with predictable content."
|
||||
title: t('duplicate.toolInfo.title'),
|
||||
description: t('duplicate.toolInfo.description')
|
||||
}}
|
||||
exampleCards={exampleCards}
|
||||
input={input}
|
||||
|
||||
@@ -3,12 +3,14 @@ import { lazy } from 'react';
|
||||
// import image from '@assets/text.png';
|
||||
|
||||
export const tool = defineTool('list', {
|
||||
name: 'Duplicate',
|
||||
path: 'duplicate',
|
||||
icon: 'mdi:content-duplicate',
|
||||
description:
|
||||
'A tool to duplicate each item in a list a specified number of times. Perfect for creating repeated patterns, test data, or expanding datasets.',
|
||||
shortDescription: 'Repeat items in a list multiple times.',
|
||||
icon: 'material-symbols-light:content-copy',
|
||||
|
||||
keywords: ['duplicate'],
|
||||
component: lazy(() => import('./index'))
|
||||
component: lazy(() => import('./index')),
|
||||
i18n: {
|
||||
name: 'list:duplicate.title',
|
||||
description: 'list:duplicate.description',
|
||||
shortDescription: 'list:duplicate.shortDescription'
|
||||
}
|
||||
});
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { Box } from '@mui/material';
|
||||
import React, { useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import ToolTextInput from '@components/input/ToolTextInput';
|
||||
import ToolTextResult from '@components/result/ToolTextResult';
|
||||
import {
|
||||
@@ -14,6 +15,7 @@ import CheckboxWithDesc from '@components/options/CheckboxWithDesc';
|
||||
import SelectWithDesc from '@components/options/SelectWithDesc';
|
||||
import ToolContent from '@components/ToolContent';
|
||||
import { ToolComponentProps } from '@tools/defineTool';
|
||||
import { ParseKeys } from 'i18next';
|
||||
|
||||
const initialValues = {
|
||||
splitSeparatorType: 'symbol' as SplitOperatorType,
|
||||
@@ -25,23 +27,24 @@ const initialValues = {
|
||||
trimItems: false
|
||||
};
|
||||
const splitOperators: {
|
||||
title: string;
|
||||
description: string;
|
||||
title: ParseKeys<'list'>;
|
||||
description: ParseKeys<'list'>;
|
||||
type: SplitOperatorType;
|
||||
}[] = [
|
||||
{
|
||||
title: 'Use a Symbol for Splitting',
|
||||
description: 'Delimit input list items with a character.',
|
||||
title: 'findMostPopular.splitOperators.symbol.title',
|
||||
description: 'findMostPopular.splitOperators.symbol.description',
|
||||
type: 'symbol'
|
||||
},
|
||||
{
|
||||
title: 'Use a Regex for Splitting',
|
||||
title: 'findMostPopular.splitOperators.regex.title',
|
||||
type: 'regex',
|
||||
description: 'Delimit input list items with a regular expression.'
|
||||
description: 'findMostPopular.splitOperators.regex.description'
|
||||
}
|
||||
];
|
||||
|
||||
export default function FindMostPopular({ title }: ToolComponentProps) {
|
||||
const { t } = useTranslation('list');
|
||||
const [input, setInput] = useState<string>('');
|
||||
const [result, setResult] = useState<string>('');
|
||||
const compute = (optionsValues: typeof initialValues, input: any) => {
|
||||
@@ -74,28 +77,35 @@ export default function FindMostPopular({ title }: ToolComponentProps) {
|
||||
title={title}
|
||||
input={input}
|
||||
inputComponent={
|
||||
<ToolTextInput title={'Input list'} value={input} onChange={setInput} />
|
||||
<ToolTextInput
|
||||
title={t('findMostPopular.inputTitle')}
|
||||
value={input}
|
||||
onChange={setInput}
|
||||
/>
|
||||
}
|
||||
resultComponent={
|
||||
<ToolTextResult title={'Most popular items'} value={result} />
|
||||
<ToolTextResult
|
||||
title={t('findMostPopular.resultTitle')}
|
||||
value={result}
|
||||
/>
|
||||
}
|
||||
initialValues={initialValues}
|
||||
getGroups={({ values, updateField }) => [
|
||||
{
|
||||
title: 'How to Extract List Items?',
|
||||
title: t('findMostPopular.extractListItems'),
|
||||
component: (
|
||||
<Box>
|
||||
{splitOperators.map(({ title, description, type }) => (
|
||||
<SimpleRadio
|
||||
key={type}
|
||||
onClick={() => updateField('splitSeparatorType', type)}
|
||||
title={title}
|
||||
description={description}
|
||||
title={t(title)}
|
||||
description={t(description)}
|
||||
checked={values.splitSeparatorType === type}
|
||||
/>
|
||||
))}
|
||||
<TextFieldWithDesc
|
||||
description={'Set a delimiting symbol or regular expression.'}
|
||||
description={t('findMostPopular.splitSeparatorDescription')}
|
||||
value={values.splitSeparator}
|
||||
onOwnChange={(val) => updateField('splitSeparator', val)}
|
||||
/>
|
||||
@@ -103,26 +113,24 @@ export default function FindMostPopular({ title }: ToolComponentProps) {
|
||||
)
|
||||
},
|
||||
{
|
||||
title: 'Item comparison',
|
||||
title: t('findMostPopular.itemComparison'),
|
||||
component: (
|
||||
<Box>
|
||||
<CheckboxWithDesc
|
||||
title={'Remove empty items'}
|
||||
description={'Ignore empty items from comparison.'}
|
||||
title={t('findMostPopular.removeEmptyItems')}
|
||||
description={t('findMostPopular.removeEmptyItemsDescription')}
|
||||
checked={values.deleteEmptyItems}
|
||||
onChange={(value) => updateField('deleteEmptyItems', value)}
|
||||
/>
|
||||
<CheckboxWithDesc
|
||||
title={'Trim top list items'}
|
||||
description={
|
||||
'Remove leading and trailing spaces before comparing items'
|
||||
}
|
||||
title={t('findMostPopular.trimItems')}
|
||||
description={t('findMostPopular.trimItemsDescription')}
|
||||
checked={values.trimItems}
|
||||
onChange={(value) => updateField('trimItems', value)}
|
||||
/>
|
||||
<CheckboxWithDesc
|
||||
title={'Ignore Item Case'}
|
||||
description={'Compare all list items in lowercase.'}
|
||||
title={t('findMostPopular.ignoreItemCase')}
|
||||
description={t('findMostPopular.ignoreItemCaseDescription')}
|
||||
checked={values.ignoreItemCase}
|
||||
onChange={(value) => updateField('ignoreItemCase', value)}
|
||||
/>
|
||||
@@ -130,27 +138,42 @@ export default function FindMostPopular({ title }: ToolComponentProps) {
|
||||
)
|
||||
},
|
||||
{
|
||||
title: 'Top item output format',
|
||||
title: t('findMostPopular.outputFormat'),
|
||||
component: (
|
||||
<Box>
|
||||
<SelectWithDesc
|
||||
selected={values.displayFormat}
|
||||
options={[
|
||||
{ label: 'Show item percentage', value: 'percentage' },
|
||||
{ label: 'Show item count', value: 'count' },
|
||||
{ label: 'Show item total', value: 'total' }
|
||||
{
|
||||
label: t('findMostPopular.displayOptions.percentage'),
|
||||
value: 'percentage'
|
||||
},
|
||||
{
|
||||
label: t('findMostPopular.displayOptions.count'),
|
||||
value: 'count'
|
||||
},
|
||||
{
|
||||
label: t('findMostPopular.displayOptions.total'),
|
||||
value: 'total'
|
||||
}
|
||||
]}
|
||||
onChange={(value) => updateField('displayFormat', value)}
|
||||
description={'How to display the most popular list items?'}
|
||||
description={t('findMostPopular.displayFormatDescription')}
|
||||
/>
|
||||
<SelectWithDesc
|
||||
selected={values.sortingMethod}
|
||||
options={[
|
||||
{ label: 'Sort Alphabetically', value: 'alphabetic' },
|
||||
{ label: 'Sort by count', value: 'count' }
|
||||
{
|
||||
label: t('findMostPopular.sortOptions.alphabetic'),
|
||||
value: 'alphabetic'
|
||||
},
|
||||
{
|
||||
label: t('findMostPopular.sortOptions.count'),
|
||||
value: 'count'
|
||||
}
|
||||
]}
|
||||
onChange={(value) => updateField('sortingMethod', value)}
|
||||
description={'Select a sorting method.'}
|
||||
description={t('findMostPopular.sortingMethodDescription')}
|
||||
/>
|
||||
</Box>
|
||||
)
|
||||
|
||||
@@ -3,12 +3,14 @@ import { lazy } from 'react';
|
||||
// import image from '@assets/text.png';
|
||||
|
||||
export const tool = defineTool('list', {
|
||||
name: 'Find most popular',
|
||||
path: 'find-most-popular',
|
||||
icon: 'material-symbols-light:query-stats',
|
||||
description:
|
||||
'A tool to identify and count the most frequently occurring items in a list. Useful for data analysis, finding trends, or identifying common elements.',
|
||||
shortDescription: 'Find most common items in a list.',
|
||||
icon: 'material-symbols-light:trending-up',
|
||||
|
||||
keywords: ['find', 'most', 'popular'],
|
||||
component: lazy(() => import('./index'))
|
||||
component: lazy(() => import('./index')),
|
||||
i18n: {
|
||||
name: 'list:findMostPopular.title',
|
||||
description: 'list:findMostPopular.description',
|
||||
shortDescription: 'list:findMostPopular.shortDescription'
|
||||
}
|
||||
});
|
||||
|
||||
@@ -7,6 +7,7 @@ import { findUniqueCompute, SplitOperatorType } from './service';
|
||||
import SimpleRadio from '@components/options/SimpleRadio';
|
||||
import TextFieldWithDesc from '@components/options/TextFieldWithDesc';
|
||||
import CheckboxWithDesc from '@components/options/CheckboxWithDesc';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
const initialValues = {
|
||||
splitOperatorType: 'symbol' as SplitOperatorType,
|
||||
@@ -35,6 +36,7 @@ const splitOperators: {
|
||||
];
|
||||
|
||||
export default function FindUnique() {
|
||||
const { t } = useTranslation('list');
|
||||
const [input, setInput] = useState<string>('');
|
||||
const [result, setResult] = useState<string>('');
|
||||
const compute = (optionsValues: typeof initialValues, input: any) => {
|
||||
@@ -64,18 +66,24 @@ export default function FindUnique() {
|
||||
|
||||
return (
|
||||
<ToolContent
|
||||
title="Find Unique"
|
||||
title={t('findUnique.title')}
|
||||
initialValues={initialValues}
|
||||
compute={compute}
|
||||
input={input}
|
||||
setInput={setInput}
|
||||
inputComponent={
|
||||
<ToolTextInput title={'Input list'} value={input} onChange={setInput} />
|
||||
<ToolTextInput
|
||||
title={t('findUnique.inputTitle')}
|
||||
value={input}
|
||||
onChange={setInput}
|
||||
/>
|
||||
}
|
||||
resultComponent={
|
||||
<ToolTextResult title={t('findUnique.resultTitle')} value={result} />
|
||||
}
|
||||
resultComponent={<ToolTextResult title={'Unique items'} value={result} />}
|
||||
getGroups={({ values, updateField }) => [
|
||||
{
|
||||
title: 'Input List Delimiter',
|
||||
title: t('findUnique.inputListDelimiter'),
|
||||
component: (
|
||||
<Box>
|
||||
{splitOperators.map(({ title, description, type }) => (
|
||||
@@ -88,7 +96,7 @@ export default function FindUnique() {
|
||||
/>
|
||||
))}
|
||||
<TextFieldWithDesc
|
||||
description={'Set a delimiting symbol or regular expression.'}
|
||||
description={t('findUnique.delimiterDescription')}
|
||||
value={values.splitSeparator}
|
||||
onOwnChange={(val) => updateField('splitSeparator', val)}
|
||||
/>
|
||||
@@ -96,7 +104,7 @@ export default function FindUnique() {
|
||||
)
|
||||
},
|
||||
{
|
||||
title: 'Output List Delimiter',
|
||||
title: t('findUnique.outputListDelimiter'),
|
||||
component: (
|
||||
<Box>
|
||||
<TextFieldWithDesc
|
||||
@@ -104,18 +112,14 @@ export default function FindUnique() {
|
||||
onOwnChange={(value) => updateField('joinSeparator', value)}
|
||||
/>
|
||||
<CheckboxWithDesc
|
||||
title={'Trim top list items'}
|
||||
description={
|
||||
'Remove leading and trailing spaces before comparing items'
|
||||
}
|
||||
title={t('findUnique.trimItems')}
|
||||
description={t('findUnique.trimItemsDescription')}
|
||||
checked={values.trimItems}
|
||||
onChange={(value) => updateField('trimItems', value)}
|
||||
/>
|
||||
<CheckboxWithDesc
|
||||
title={'Skip empty items'}
|
||||
description={
|
||||
"Don't include the empty list items in the output."
|
||||
}
|
||||
title={t('findUnique.skipEmptyItems')}
|
||||
description={t('findUnique.skipEmptyItemsDescription')}
|
||||
checked={values.deleteEmptyItems}
|
||||
onChange={(value) => updateField('deleteEmptyItems', value)}
|
||||
/>
|
||||
@@ -123,22 +127,20 @@ export default function FindUnique() {
|
||||
)
|
||||
},
|
||||
{
|
||||
title: 'Unique Item Options',
|
||||
title: t('findUnique.uniqueItemOptions'),
|
||||
component: (
|
||||
<Box>
|
||||
<CheckboxWithDesc
|
||||
title={'Find Absolutely Unique Items'}
|
||||
description={
|
||||
'Display only those items of the list that exist in a single copy.'
|
||||
}
|
||||
title={t('findUnique.findAbsolutelyUniqueItems')}
|
||||
description={t(
|
||||
'findUnique.findAbsolutelyUniqueItemsDescription'
|
||||
)}
|
||||
checked={values.absolutelyUnique}
|
||||
onChange={(value) => updateField('absolutelyUnique', value)}
|
||||
/>
|
||||
<CheckboxWithDesc
|
||||
title={'Case Sensitive Items'}
|
||||
description={
|
||||
'Output items with different case as unique elements in the list.'
|
||||
}
|
||||
title={t('findUnique.caseSensitiveItems')}
|
||||
description={t('findUnique.caseSensitiveItemsDescription')}
|
||||
checked={values.caseSensitive}
|
||||
onChange={(value) => updateField('caseSensitive', value)}
|
||||
/>
|
||||
|
||||
@@ -2,11 +2,14 @@ import { defineTool } from '@tools/defineTool';
|
||||
import { lazy } from 'react';
|
||||
|
||||
export const tool = defineTool('list', {
|
||||
name: 'Find unique',
|
||||
path: 'find-unique',
|
||||
icon: 'mynaui:one',
|
||||
description: "World's simplest browser-based utility for finding unique items in a list. Just input your list with any separator, and it will automatically identify and extract unique items. Perfect for removing duplicates, finding distinct values, or analyzing data uniqueness. You can customize the input/output separators and choose whether to preserve the original order.",
|
||||
shortDescription: 'Find unique items in a list',
|
||||
icon: 'material-symbols-light:search',
|
||||
|
||||
keywords: ['find', 'unique'],
|
||||
component: lazy(() => import('./index'))
|
||||
component: lazy(() => import('./index')),
|
||||
i18n: {
|
||||
name: 'list:findUnique.title',
|
||||
description: 'list:findUnique.description',
|
||||
shortDescription: 'list:findUnique.shortDescription'
|
||||
}
|
||||
});
|
||||
|
||||
@@ -9,6 +9,7 @@ import CheckboxWithDesc from '@components/options/CheckboxWithDesc';
|
||||
import { formatNumber } from '../../../../utils/number';
|
||||
import ToolContent from '@components/ToolContent';
|
||||
import { ToolComponentProps } from '@tools/defineTool';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
const initialValues = {
|
||||
splitOperatorType: 'symbol' as SplitOperatorType,
|
||||
@@ -40,6 +41,7 @@ const splitOperators: {
|
||||
];
|
||||
|
||||
export default function FindUnique({ title }: ToolComponentProps) {
|
||||
const { t } = useTranslation('list');
|
||||
const [input, setInput] = useState<string>('');
|
||||
const [result, setResult] = useState<string>('');
|
||||
const compute = (optionsValues: typeof initialValues, input: any) => {
|
||||
@@ -78,28 +80,32 @@ export default function FindUnique({ title }: ToolComponentProps) {
|
||||
title={title}
|
||||
input={input}
|
||||
inputComponent={
|
||||
<ToolTextInput title={'Input list'} value={input} onChange={setInput} />
|
||||
<ToolTextInput
|
||||
title={t('group.inputTitle')}
|
||||
value={input}
|
||||
onChange={setInput}
|
||||
/>
|
||||
}
|
||||
resultComponent={
|
||||
<ToolTextResult title={'Grouped items'} value={result} />
|
||||
<ToolTextResult title={t('group.resultTitle')} value={result} />
|
||||
}
|
||||
initialValues={initialValues}
|
||||
getGroups={({ values, updateField }) => [
|
||||
{
|
||||
title: 'Input Item Separator',
|
||||
title: t('group.inputItemSeparator'),
|
||||
component: (
|
||||
<Box>
|
||||
{splitOperators.map(({ title, description, type }) => (
|
||||
<SimpleRadio
|
||||
key={type}
|
||||
onClick={() => updateField('splitOperatorType', type)}
|
||||
title={title}
|
||||
description={description}
|
||||
title={t(`group.splitOperators.${type}.title`)}
|
||||
description={t(`group.splitOperators.${type}.description`)}
|
||||
checked={values.splitOperatorType === type}
|
||||
/>
|
||||
))}
|
||||
<TextFieldWithDesc
|
||||
description={'Set a delimiting symbol or regular expression.'}
|
||||
description={t('group.splitSeparatorDescription')}
|
||||
value={values.splitSeparator}
|
||||
onOwnChange={(val) => updateField('splitSeparator', val)}
|
||||
/>
|
||||
@@ -107,12 +113,12 @@ export default function FindUnique({ title }: ToolComponentProps) {
|
||||
)
|
||||
},
|
||||
{
|
||||
title: 'Group Size and Separators',
|
||||
title: t('group.groupSizeAndSeparators'),
|
||||
component: (
|
||||
<Box>
|
||||
<TextFieldWithDesc
|
||||
value={values.groupNumber}
|
||||
description={'Number of items in a group'}
|
||||
description={t('group.groupNumberDescription')}
|
||||
type={'number'}
|
||||
onOwnChange={(value) =>
|
||||
updateField('groupNumber', formatNumber(value, 1))
|
||||
@@ -120,52 +126,46 @@ export default function FindUnique({ title }: ToolComponentProps) {
|
||||
/>
|
||||
<TextFieldWithDesc
|
||||
value={values.itemSeparator}
|
||||
description={'Item separator character'}
|
||||
description={t('group.itemSeparatorDescription')}
|
||||
onOwnChange={(value) => updateField('itemSeparator', value)}
|
||||
/>
|
||||
<TextFieldWithDesc
|
||||
value={values.groupSeparator}
|
||||
description={'Group separator character'}
|
||||
description={t('group.groupSeparatorDescription')}
|
||||
onOwnChange={(value) => updateField('groupSeparator', value)}
|
||||
/>
|
||||
<TextFieldWithDesc
|
||||
value={values.leftWrap}
|
||||
description={"Group's left wrap symbol."}
|
||||
description={t('group.leftWrapDescription')}
|
||||
onOwnChange={(value) => updateField('leftWrap', value)}
|
||||
/>
|
||||
<TextFieldWithDesc
|
||||
value={values.rightWrap}
|
||||
description={"Group's right wrap symbol."}
|
||||
description={t('group.rightWrapDescription')}
|
||||
onOwnChange={(value) => updateField('rightWrap', value)}
|
||||
/>
|
||||
</Box>
|
||||
)
|
||||
},
|
||||
{
|
||||
title: 'Empty Items and Padding',
|
||||
title: t('group.emptyItemsAndPadding'),
|
||||
component: (
|
||||
<Box>
|
||||
<CheckboxWithDesc
|
||||
title={'Delete Empty Items'}
|
||||
description={
|
||||
"Ignore empty items and don't include them in the groups."
|
||||
}
|
||||
title={t('group.deleteEmptyItems')}
|
||||
description={t('group.deleteEmptyItemsDescription')}
|
||||
checked={values.deleteEmptyItems}
|
||||
onChange={(value) => updateField('deleteEmptyItems', value)}
|
||||
/>
|
||||
<CheckboxWithDesc
|
||||
title={'Pad Non-full Groups'}
|
||||
description={
|
||||
'Fill non-full groups with a custom item (enter below).'
|
||||
}
|
||||
title={t('group.padNonFullGroups')}
|
||||
description={t('group.padNonFullGroupsDescription')}
|
||||
checked={values.padNonFullGroup}
|
||||
onChange={(value) => updateField('padNonFullGroup', value)}
|
||||
/>
|
||||
<TextFieldWithDesc
|
||||
value={values.paddingChar}
|
||||
description={
|
||||
'Use this character or item to pad non-full groups.'
|
||||
}
|
||||
description={t('group.paddingCharDescription')}
|
||||
onOwnChange={(value) => updateField('paddingChar', value)}
|
||||
/>
|
||||
</Box>
|
||||
|
||||
@@ -2,11 +2,14 @@ import { defineTool } from '@tools/defineTool';
|
||||
import { lazy } from 'react';
|
||||
|
||||
export const tool = defineTool('list', {
|
||||
name: 'Group',
|
||||
path: 'group',
|
||||
icon: 'pajamas:group',
|
||||
description: "World's simplest browser-based utility for grouping list items. Input your list and specify grouping criteria to organize items into logical groups. Perfect for categorizing data, organizing information, or creating structured lists. Supports custom separators and various grouping options.",
|
||||
shortDescription: 'Group list items by common properties',
|
||||
|
||||
keywords: ['group'],
|
||||
component: lazy(() => import('./index'))
|
||||
component: lazy(() => import('./index')),
|
||||
i18n: {
|
||||
name: 'list:group.title',
|
||||
description: 'list:group.description',
|
||||
shortDescription: 'list:group.shortDescription'
|
||||
}
|
||||
});
|
||||
|
||||
@@ -9,6 +9,7 @@ import TextFieldWithDesc from '@components/options/TextFieldWithDesc';
|
||||
import { CardExampleType } from '@components/examples/ToolExamples';
|
||||
import { ToolComponentProps } from '@tools/defineTool';
|
||||
import ToolContent from '@components/ToolContent';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
const initialValues = {
|
||||
splitOperatorType: 'symbol' as SplitOperatorType,
|
||||
@@ -111,6 +112,7 @@ argument`,
|
||||
];
|
||||
|
||||
export default function Reverse({ title }: ToolComponentProps) {
|
||||
const { t } = useTranslation('list');
|
||||
const [input, setInput] = useState<string>('');
|
||||
const [result, setResult] = useState<string>('');
|
||||
|
||||
@@ -119,15 +121,15 @@ export default function Reverse({ title }: ToolComponentProps) {
|
||||
updateField
|
||||
}) => [
|
||||
{
|
||||
title: 'Splitter Mode',
|
||||
title: t('reverse.splitterMode'),
|
||||
component: (
|
||||
<Box>
|
||||
{splitOperators.map(({ title, description, type }) => (
|
||||
<SimpleRadio
|
||||
key={type}
|
||||
onClick={() => updateField('splitOperatorType', type)}
|
||||
title={title}
|
||||
description={description}
|
||||
title={t(`reverse.splitOperators.${type}.title`)}
|
||||
description={t(`reverse.splitOperators.${type}.description`)}
|
||||
checked={values.splitOperatorType === type}
|
||||
/>
|
||||
))}
|
||||
@@ -135,11 +137,11 @@ export default function Reverse({ title }: ToolComponentProps) {
|
||||
)
|
||||
},
|
||||
{
|
||||
title: 'Item Separator',
|
||||
title: t('reverse.itemSeparator'),
|
||||
component: (
|
||||
<Box>
|
||||
<TextFieldWithDesc
|
||||
description={'Set a delimiting symbol or regular expression.'}
|
||||
description={t('reverse.itemSeparatorDescription')}
|
||||
value={values.splitSeparator}
|
||||
onOwnChange={(val) => updateField('splitSeparator', val)}
|
||||
/>
|
||||
@@ -147,11 +149,11 @@ export default function Reverse({ title }: ToolComponentProps) {
|
||||
)
|
||||
},
|
||||
{
|
||||
title: 'Output List Options',
|
||||
title: t('reverse.outputListOptions'),
|
||||
component: (
|
||||
<Box>
|
||||
<TextFieldWithDesc
|
||||
description={'Output list item separator.'}
|
||||
description={t('reverse.outputSeparatorDescription')}
|
||||
value={values.joinSeparator}
|
||||
onOwnChange={(val) => updateField('joinSeparator', val)}
|
||||
/>
|
||||
@@ -176,15 +178,18 @@ export default function Reverse({ title }: ToolComponentProps) {
|
||||
input={input}
|
||||
setInput={setInput}
|
||||
inputComponent={
|
||||
<ToolTextInput title={'Input list'} value={input} onChange={setInput} />
|
||||
<ToolTextInput
|
||||
title={t('reverse.inputTitle')}
|
||||
value={input}
|
||||
onChange={setInput}
|
||||
/>
|
||||
}
|
||||
resultComponent={
|
||||
<ToolTextResult title={'Reversed list'} value={result} />
|
||||
<ToolTextResult title={t('reverse.resultTitle')} value={result} />
|
||||
}
|
||||
toolInfo={{
|
||||
title: 'What Is a List Reverser?',
|
||||
description:
|
||||
'With this utility, you can reverse the order of items in a list. The utility first splits the input list into individual items and then iterates through them from the last item to the first item, printing each item to the output during the iteration. The input list may contain anything that can be represented as textual data, which includes digits, numbers, strings, words, sentences, etc. The input item separator can also be a regular expression. For example, the regex /[;,]/ will allow you to use items that are either comma- or semicolon-separated. The input and output list items delimiters can be customized in the options. By default, both input and output lists are comma-separated. Listabulous!'
|
||||
title: t('reverse.toolInfo.title'),
|
||||
description: t('reverse.toolInfo.description')
|
||||
}}
|
||||
exampleCards={exampleCards}
|
||||
/>
|
||||
|
||||
@@ -3,11 +3,14 @@ import { lazy } from 'react';
|
||||
// import image from '@assets/text.png';
|
||||
|
||||
export const tool = defineTool('list', {
|
||||
name: 'Reverse',
|
||||
path: 'reverse',
|
||||
icon: 'proicons:reverse',
|
||||
description: 'This is a super simple browser-based application prints all list items in reverse. The input items can be separated by any symbol and you can also change the separator of the reversed list items.',
|
||||
shortDescription: 'Quickly reverse a list',
|
||||
|
||||
keywords: ['reverse'],
|
||||
component: lazy(() => import('./index'))
|
||||
component: lazy(() => import('./index')),
|
||||
i18n: {
|
||||
name: 'list:reverse.title',
|
||||
description: 'list:reverse.description',
|
||||
shortDescription: 'list:reverse.shortDescription'
|
||||
}
|
||||
});
|
||||
|
||||
@@ -3,12 +3,14 @@ import { lazy } from 'react';
|
||||
// import image from '@assets/text.png';
|
||||
|
||||
export const tool = defineTool('list', {
|
||||
name: 'Rotate',
|
||||
path: 'rotate',
|
||||
icon: 'material-symbols-light:rotate-right',
|
||||
description:
|
||||
'A tool to rotate items in a list by a specified number of positions. Shift elements left or right while maintaining their relative order.',
|
||||
shortDescription: 'Shift list items by position.',
|
||||
|
||||
keywords: ['rotate'],
|
||||
component: lazy(() => import('./index'))
|
||||
component: lazy(() => import('./index')),
|
||||
i18n: {
|
||||
name: 'list:rotate.title',
|
||||
description: 'list:rotate.description',
|
||||
shortDescription: 'list:rotate.shortDescription'
|
||||
}
|
||||
});
|
||||
|
||||
@@ -7,6 +7,7 @@ import { shuffleList, SplitOperatorType } from './service';
|
||||
import SimpleRadio from '@components/options/SimpleRadio';
|
||||
import TextFieldWithDesc from '@components/options/TextFieldWithDesc';
|
||||
import { isNumber } from '@utils/string';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
const initialValues = {
|
||||
splitOperatorType: 'symbol' as SplitOperatorType,
|
||||
@@ -32,6 +33,7 @@ const splitOperators: {
|
||||
];
|
||||
|
||||
export default function Shuffle() {
|
||||
const { t } = useTranslation('list');
|
||||
const [input, setInput] = useState<string>('');
|
||||
const [result, setResult] = useState<string>('');
|
||||
const compute = (optionsValues: typeof initialValues, input: any) => {
|
||||
@@ -51,20 +53,24 @@ export default function Shuffle() {
|
||||
|
||||
return (
|
||||
<ToolContent
|
||||
title="Shuffle"
|
||||
title={t('shuffle.title')}
|
||||
initialValues={initialValues}
|
||||
compute={compute}
|
||||
input={input}
|
||||
setInput={setInput}
|
||||
inputComponent={
|
||||
<ToolTextInput title={'Input list'} value={input} onChange={setInput} />
|
||||
<ToolTextInput
|
||||
title={t('shuffle.inputTitle')}
|
||||
value={input}
|
||||
onChange={setInput}
|
||||
/>
|
||||
}
|
||||
resultComponent={
|
||||
<ToolTextResult title={'Shuffled list'} value={result} />
|
||||
<ToolTextResult title={t('shuffle.resultTitle')} value={result} />
|
||||
}
|
||||
getGroups={({ values, updateField }) => [
|
||||
{
|
||||
title: 'Input list separator',
|
||||
title: t('shuffle.inputListSeparator'),
|
||||
component: (
|
||||
<Box>
|
||||
{splitOperators.map(({ title, description, type }) => (
|
||||
@@ -77,7 +83,7 @@ export default function Shuffle() {
|
||||
/>
|
||||
))}
|
||||
<TextFieldWithDesc
|
||||
description={'Set a delimiting symbol or regular expression.'}
|
||||
description={t('shuffle.delimiterDescription')}
|
||||
value={values.splitSeparator}
|
||||
onOwnChange={(val) => updateField('splitSeparator', val)}
|
||||
/>
|
||||
@@ -85,11 +91,11 @@ export default function Shuffle() {
|
||||
)
|
||||
},
|
||||
{
|
||||
title: 'Shuffled List Length',
|
||||
title: t('shuffle.shuffledListLength'),
|
||||
component: (
|
||||
<Box>
|
||||
<TextFieldWithDesc
|
||||
description={'Output this many random items'}
|
||||
description={t('shuffle.outputLengthDescription')}
|
||||
value={values.length}
|
||||
onOwnChange={(val) => updateField('length', val)}
|
||||
/>
|
||||
@@ -97,13 +103,13 @@ export default function Shuffle() {
|
||||
)
|
||||
},
|
||||
{
|
||||
title: 'Shuffled List Separator',
|
||||
title: t('shuffle.shuffledListSeparator'),
|
||||
component: (
|
||||
<Box>
|
||||
<TextFieldWithDesc
|
||||
value={values.joinSeparator}
|
||||
onOwnChange={(value) => updateField('joinSeparator', value)}
|
||||
description={'Use this separator in the randomized list.'}
|
||||
description={t('shuffle.joinSeparatorDescription')}
|
||||
/>
|
||||
</Box>
|
||||
)
|
||||
|
||||
@@ -3,12 +3,14 @@ import { lazy } from 'react';
|
||||
// import image from '@assets/text.png';
|
||||
|
||||
export const tool = defineTool('list', {
|
||||
name: 'Shuffle',
|
||||
path: 'shuffle',
|
||||
icon: 'material-symbols-light:shuffle',
|
||||
description:
|
||||
'A tool to randomly reorder items in a list. Perfect for randomizing data, creating random selections, or generating random sequences.',
|
||||
shortDescription: 'Randomly reorder list items.',
|
||||
|
||||
keywords: ['shuffle'],
|
||||
component: lazy(() => import('./index'))
|
||||
component: lazy(() => import('./index')),
|
||||
i18n: {
|
||||
name: 'list:shuffle.title',
|
||||
description: 'list:shuffle.description',
|
||||
shortDescription: 'list:shuffle.shortDescription'
|
||||
}
|
||||
});
|
||||
|
||||
@@ -9,6 +9,7 @@ import CheckboxWithDesc from '@components/options/CheckboxWithDesc';
|
||||
import SelectWithDesc from '@components/options/SelectWithDesc';
|
||||
import ToolContent from '@components/ToolContent';
|
||||
import { ToolComponentProps } from '@tools/defineTool';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
const initialValues = {
|
||||
splitSeparatorType: 'symbol' as SplitOperatorType,
|
||||
@@ -36,7 +37,8 @@ const splitOperators: {
|
||||
}
|
||||
];
|
||||
|
||||
export default function SplitText({ title }: ToolComponentProps) {
|
||||
export default function SortList({ title }: ToolComponentProps) {
|
||||
const { t } = useTranslation('list');
|
||||
const [input, setInput] = useState<string>('');
|
||||
const [result, setResult] = useState<string>('');
|
||||
const compute = (optionsValues: typeof initialValues, input: any) => {
|
||||
@@ -69,26 +71,32 @@ export default function SplitText({ title }: ToolComponentProps) {
|
||||
title={title}
|
||||
input={input}
|
||||
inputComponent={
|
||||
<ToolTextInput title={'Input list'} value={input} onChange={setInput} />
|
||||
<ToolTextInput
|
||||
title={t('sort.inputTitle')}
|
||||
value={input}
|
||||
onChange={setInput}
|
||||
/>
|
||||
}
|
||||
resultComponent={
|
||||
<ToolTextResult title={t('sort.resultTitle')} value={result} />
|
||||
}
|
||||
resultComponent={<ToolTextResult title={'Sorted list'} value={result} />}
|
||||
initialValues={initialValues}
|
||||
getGroups={({ values, updateField }) => [
|
||||
{
|
||||
title: 'Input item separator',
|
||||
title: t('sort.inputItemSeparator'),
|
||||
component: (
|
||||
<Box>
|
||||
{splitOperators.map(({ title, description, type }) => (
|
||||
<SimpleRadio
|
||||
key={type}
|
||||
onClick={() => updateField('splitSeparatorType', type)}
|
||||
title={title}
|
||||
description={description}
|
||||
title={t(`sort.splitOperators.${type}.title`)}
|
||||
description={t(`sort.splitOperators.${type}.description`)}
|
||||
checked={values.splitSeparatorType === type}
|
||||
/>
|
||||
))}
|
||||
<TextFieldWithDesc
|
||||
description={'Set a delimiting symbol or regular expression.'}
|
||||
description={t('sort.splitSeparatorDescription')}
|
||||
value={values.splitSeparator}
|
||||
onOwnChange={(val) => updateField('splitSeparator', val)}
|
||||
/>
|
||||
@@ -96,35 +104,45 @@ export default function SplitText({ title }: ToolComponentProps) {
|
||||
)
|
||||
},
|
||||
{
|
||||
title: 'Sort method',
|
||||
title: t('sort.sortMethod'),
|
||||
component: (
|
||||
<Box>
|
||||
<SelectWithDesc
|
||||
selected={values.sortingMethod}
|
||||
options={[
|
||||
{ label: 'Sort Alphabetically', value: 'alphabetic' },
|
||||
{ label: 'Sort Numerically', value: 'numeric' },
|
||||
{ label: 'Sort by Length', value: 'length' }
|
||||
{
|
||||
label: t('sort.sortOptions.alphabetic'),
|
||||
value: 'alphabetic'
|
||||
},
|
||||
{
|
||||
label: t('sort.sortOptions.numeric'),
|
||||
value: 'numeric'
|
||||
},
|
||||
{ label: t('sort.sortOptions.length'), value: 'length' }
|
||||
]}
|
||||
onChange={(value) => updateField('sortingMethod', value)}
|
||||
description={'Select a sorting method.'}
|
||||
description={t('sort.sortMethodDescription')}
|
||||
/>
|
||||
<SelectWithDesc
|
||||
selected={values.increasing}
|
||||
options={[
|
||||
{ label: 'Increasing order', value: true },
|
||||
{ label: 'Decreasing order', value: false }
|
||||
{
|
||||
label: t('sort.orderOptions.increasing'),
|
||||
value: true
|
||||
},
|
||||
{
|
||||
label: t('sort.orderOptions.decreasing'),
|
||||
value: false
|
||||
}
|
||||
]}
|
||||
onChange={(value) => {
|
||||
updateField('increasing', value);
|
||||
}}
|
||||
description={'Select a sorting order.'}
|
||||
description={t('sort.orderDescription')}
|
||||
/>
|
||||
<CheckboxWithDesc
|
||||
title={'Case Sensitive Sort'}
|
||||
description={
|
||||
'Sort uppercase and lowercase items separately. Capital letters precede lowercase letters in an ascending list. (Works only in alphabetical sorting mode.)'
|
||||
}
|
||||
title={t('sort.caseSensitive')}
|
||||
description={t('sort.caseSensitiveDescription')}
|
||||
checked={values.caseSensitive}
|
||||
onChange={(val) => updateField('caseSensitive', val)}
|
||||
/>
|
||||
@@ -132,19 +150,17 @@ export default function SplitText({ title }: ToolComponentProps) {
|
||||
)
|
||||
},
|
||||
{
|
||||
title: 'Sorted item properties',
|
||||
title: t('sort.sortedItemProperties'),
|
||||
component: (
|
||||
<Box>
|
||||
<TextFieldWithDesc
|
||||
description={
|
||||
'Use this symbol as a joiner between items in a sorted list.'
|
||||
}
|
||||
description={t('sort.joinSeparatorDescription')}
|
||||
value={values.joinSeparator}
|
||||
onOwnChange={(val) => updateField('joinSeparator', val)}
|
||||
/>
|
||||
<CheckboxWithDesc
|
||||
title={'Remove duplicates'}
|
||||
description={'Delete duplicate list items.'}
|
||||
title={t('sort.removeDuplicates')}
|
||||
description={t('sort.removeDuplicatesDescription')}
|
||||
checked={values.removeDuplicated}
|
||||
onChange={(val) => updateField('removeDuplicated', val)}
|
||||
/>
|
||||
|
||||
@@ -3,12 +3,14 @@ import { lazy } from 'react';
|
||||
// import image from '@assets/text.png';
|
||||
|
||||
export const tool = defineTool('list', {
|
||||
name: 'Sort',
|
||||
path: 'sort',
|
||||
icon: 'basil:sort-outline',
|
||||
description:
|
||||
'This is a super simple browser-based application that sorts items in a list and arranges them in increasing or decreasing order. You can sort the items alphabetically, numerically, or by their length. You can also remove duplicate and empty items, as well as trim individual items that have whitespace around them. You can use any separator character to separate the input list items or alternatively use a regular expression to separate them. Additionally, you can create a new delimiter for the sorted output list.',
|
||||
shortDescription: 'Quickly sort a list',
|
||||
icon: 'material-symbols-light:sort',
|
||||
|
||||
keywords: ['sort'],
|
||||
component: lazy(() => import('./index'))
|
||||
component: lazy(() => import('./index')),
|
||||
i18n: {
|
||||
name: 'list:sort.title',
|
||||
description: 'list:sort.description',
|
||||
shortDescription: 'list:sort.shortDescription'
|
||||
}
|
||||
});
|
||||
|
||||
@@ -2,12 +2,14 @@ import { defineTool } from '@tools/defineTool';
|
||||
import { lazy } from 'react';
|
||||
|
||||
export const tool = defineTool('list', {
|
||||
name: 'Truncate',
|
||||
path: 'truncate',
|
||||
icon: 'mdi:format-horizontal-align-right',
|
||||
description:
|
||||
"World's simplest browser-based utility for truncating lists. Quickly limit the number of items in your list by specifying a maximum length. Perfect for sampling data, creating previews, or managing large lists. Supports custom separators and various truncation options.",
|
||||
shortDescription: 'Limit the number of items in a list',
|
||||
icon: 'material-symbols-light:content-cut',
|
||||
|
||||
keywords: ['truncate'],
|
||||
component: lazy(() => import('./index'))
|
||||
component: lazy(() => import('./index')),
|
||||
i18n: {
|
||||
name: 'list:truncate.title',
|
||||
description: 'list:truncate.description',
|
||||
shortDescription: 'list:truncate.shortDescription'
|
||||
}
|
||||
});
|
||||
|
||||
@@ -3,12 +3,14 @@ import { lazy } from 'react';
|
||||
// import image from '@assets/text.png';
|
||||
|
||||
export const tool = defineTool('list', {
|
||||
name: 'Unwrap',
|
||||
path: 'unwrap',
|
||||
icon: 'mdi:unwrap',
|
||||
description:
|
||||
'A tool to remove characters from the beginning and end of each item in a list. Perfect for cleaning up formatted data or removing unwanted wrappers.',
|
||||
shortDescription: 'Remove characters around list items.',
|
||||
icon: 'material-symbols-light:unfold-more',
|
||||
|
||||
keywords: ['unwrap'],
|
||||
component: lazy(() => import('./index'))
|
||||
component: lazy(() => import('./index')),
|
||||
i18n: {
|
||||
name: 'list:unwrap.title',
|
||||
description: 'list:unwrap.description',
|
||||
shortDescription: 'list:unwrap.shortDescription'
|
||||
}
|
||||
});
|
||||
|
||||
@@ -11,6 +11,7 @@ import TextFieldWithDesc from '@components/options/TextFieldWithDesc';
|
||||
import SimpleRadio from '@components/options/SimpleRadio';
|
||||
import CheckboxWithDesc from '@components/options/CheckboxWithDesc';
|
||||
import * as Yup from 'yup';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
interface InitialValuesType {
|
||||
splitOperatorType: SplitOperatorType;
|
||||
@@ -85,6 +86,7 @@ const exampleCards: CardExampleType<InitialValuesType>[] = [
|
||||
];
|
||||
|
||||
export default function Wrap({ title }: ToolComponentProps) {
|
||||
const { t } = useTranslation('list');
|
||||
const [input, setInput] = useState<string>('');
|
||||
const [result, setResult] = useState<string>('');
|
||||
|
||||
@@ -117,50 +119,50 @@ export default function Wrap({ title }: ToolComponentProps) {
|
||||
updateField
|
||||
}) => [
|
||||
{
|
||||
title: 'Split Options',
|
||||
title: t('wrap.splitOptions'),
|
||||
component: (
|
||||
<Box>
|
||||
<SimpleRadio
|
||||
onClick={() => updateField('splitOperatorType', 'symbol')}
|
||||
checked={values.splitOperatorType === 'symbol'}
|
||||
title={'Split by Symbol'}
|
||||
title={t('wrap.splitBySymbol')}
|
||||
/>
|
||||
<SimpleRadio
|
||||
onClick={() => updateField('splitOperatorType', 'regex')}
|
||||
checked={values.splitOperatorType === 'regex'}
|
||||
title={'Split by Regular Expression'}
|
||||
title={t('wrap.splitByRegex')}
|
||||
/>
|
||||
<TextFieldWithDesc
|
||||
value={values.splitSeparator}
|
||||
onOwnChange={(val) => updateField('splitSeparator', val)}
|
||||
description={'Separator to split the list'}
|
||||
description={t('wrap.splitSeparatorDescription')}
|
||||
/>
|
||||
<TextFieldWithDesc
|
||||
value={values.joinSeparator}
|
||||
onOwnChange={(val) => updateField('joinSeparator', val)}
|
||||
description={'Separator to join the wrapped list'}
|
||||
description={t('wrap.joinSeparatorDescription')}
|
||||
/>
|
||||
<CheckboxWithDesc
|
||||
checked={values.deleteEmptyItems}
|
||||
onChange={(checked) => updateField('deleteEmptyItems', checked)}
|
||||
title={'Remove empty items'}
|
||||
title={t('wrap.removeEmptyItems')}
|
||||
/>
|
||||
</Box>
|
||||
)
|
||||
},
|
||||
{
|
||||
title: 'Wrap Options',
|
||||
title: t('wrap.wrapOptions'),
|
||||
component: (
|
||||
<Box>
|
||||
<TextFieldWithDesc
|
||||
value={values.left}
|
||||
onOwnChange={(val) => updateField('left', val)}
|
||||
description={'Text to add before each item'}
|
||||
description={t('wrap.leftTextDescription')}
|
||||
/>
|
||||
<TextFieldWithDesc
|
||||
value={values.right}
|
||||
onOwnChange={(val) => updateField('right', val)}
|
||||
description={'Text to add after each item'}
|
||||
description={t('wrap.rightTextDescription')}
|
||||
/>
|
||||
</Box>
|
||||
)
|
||||
@@ -171,16 +173,21 @@ export default function Wrap({ title }: ToolComponentProps) {
|
||||
<ToolContent
|
||||
title={title}
|
||||
inputComponent={
|
||||
<ToolTextInput title="Input List" value={input} onChange={setInput} />
|
||||
<ToolTextInput
|
||||
title={t('wrap.inputTitle')}
|
||||
value={input}
|
||||
onChange={setInput}
|
||||
/>
|
||||
}
|
||||
resultComponent={
|
||||
<ToolTextResult title={t('wrap.resultTitle')} value={result} />
|
||||
}
|
||||
resultComponent={<ToolTextResult title="Wrapped List" value={result} />}
|
||||
initialValues={initialValues}
|
||||
getGroups={getGroups}
|
||||
validationSchema={validationSchema}
|
||||
toolInfo={{
|
||||
title: 'List Wrapping',
|
||||
description:
|
||||
"This tool allows you to add text before and after each item in a list. You can specify different text for the left and right sides, and control how the list is processed. It's useful for adding quotes, brackets, or other formatting to list items, preparing data for different formats, or creating structured text."
|
||||
title: t('wrap.toolInfo.title'),
|
||||
description: t('wrap.toolInfo.description')
|
||||
}}
|
||||
exampleCards={exampleCards}
|
||||
input={input}
|
||||
|
||||
@@ -3,12 +3,14 @@ import { lazy } from 'react';
|
||||
// import image from '@assets/text.png';
|
||||
|
||||
export const tool = defineTool('list', {
|
||||
name: 'Wrap',
|
||||
path: 'wrap',
|
||||
icon: 'mdi:wrap',
|
||||
description:
|
||||
'A tool to wrap each item in a list with custom prefix and suffix characters. Useful for formatting lists for code, markup languages, or presentation.',
|
||||
shortDescription: 'Add characters around list items.',
|
||||
icon: 'material-symbols-light:wrap-text',
|
||||
|
||||
keywords: ['wrap'],
|
||||
component: lazy(() => import('./index'))
|
||||
component: lazy(() => import('./index')),
|
||||
i18n: {
|
||||
name: 'list:wrap.title',
|
||||
description: 'list:wrap.description',
|
||||
shortDescription: 'list:wrap.shortDescription'
|
||||
}
|
||||
});
|
||||
|
||||
@@ -7,6 +7,7 @@ import { generateArithmeticSequence } from './service';
|
||||
import * as Yup from 'yup';
|
||||
import { CardExampleType } from '@components/examples/ToolExamples';
|
||||
import { ToolComponentProps } from '@tools/defineTool';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
type InitialValuesType = {
|
||||
firstTerm: string;
|
||||
@@ -70,6 +71,7 @@ const exampleCards: CardExampleType<InitialValuesType>[] = [
|
||||
];
|
||||
|
||||
export default function ArithmeticSequence({ title }: ToolComponentProps) {
|
||||
const { t } = useTranslation('number');
|
||||
const [result, setResult] = useState<string>('');
|
||||
|
||||
return (
|
||||
@@ -77,35 +79,39 @@ export default function ArithmeticSequence({ title }: ToolComponentProps) {
|
||||
title={title}
|
||||
inputComponent={null}
|
||||
resultComponent={
|
||||
<ToolTextResult title="Generated Sequence" value={result} />
|
||||
<ToolTextResult
|
||||
title={t('arithmeticSequence.resultTitle')}
|
||||
value={result}
|
||||
/>
|
||||
}
|
||||
initialValues={initialValues}
|
||||
validationSchema={validationSchema}
|
||||
exampleCards={exampleCards}
|
||||
toolInfo={{
|
||||
title: 'What is an Arithmetic Sequence?',
|
||||
description:
|
||||
'An arithmetic sequence is a sequence of numbers where the difference between each consecutive term is constant. This constant difference is called the common difference. Given the first term (a₁) and the common difference (d), each term can be found by adding the common difference to the previous term.'
|
||||
title: t('arithmeticSequence.toolInfo.title'),
|
||||
description: t('arithmeticSequence.toolInfo.description')
|
||||
}}
|
||||
getGroups={({ values, updateField }) => [
|
||||
{
|
||||
title: 'Sequence Parameters',
|
||||
title: t('arithmeticSequence.sequenceParameters'),
|
||||
component: (
|
||||
<Box>
|
||||
<TextFieldWithDesc
|
||||
description="First term of the sequence (a₁)"
|
||||
description={t('arithmeticSequence.firstTermDescription')}
|
||||
value={values.firstTerm}
|
||||
onOwnChange={(val) => updateField('firstTerm', val)}
|
||||
type="number"
|
||||
/>
|
||||
<TextFieldWithDesc
|
||||
description="Common difference between terms (d)"
|
||||
description={t(
|
||||
'arithmeticSequence.commonDifferenceDescription'
|
||||
)}
|
||||
value={values.commonDifference}
|
||||
onOwnChange={(val) => updateField('commonDifference', val)}
|
||||
type="number"
|
||||
/>
|
||||
<TextFieldWithDesc
|
||||
description="Number of terms to generate (n)"
|
||||
description={t('arithmeticSequence.numberOfTermsDescription')}
|
||||
value={values.numberOfTerms}
|
||||
onOwnChange={(val) => updateField('numberOfTerms', val)}
|
||||
type="number"
|
||||
@@ -114,10 +120,10 @@ export default function ArithmeticSequence({ title }: ToolComponentProps) {
|
||||
)
|
||||
},
|
||||
{
|
||||
title: 'Output Format',
|
||||
title: t('arithmeticSequence.outputFormat'),
|
||||
component: (
|
||||
<TextFieldWithDesc
|
||||
description="Separator between terms"
|
||||
description={t('arithmeticSequence.separatorDescription')}
|
||||
value={values.separator}
|
||||
onOwnChange={(val) => updateField('separator', val)}
|
||||
/>
|
||||
|
||||
@@ -2,20 +2,14 @@ import { defineTool } from '@tools/defineTool';
|
||||
import { lazy } from 'react';
|
||||
|
||||
export const tool = defineTool('number', {
|
||||
name: 'Generate Arithmetic Sequence',
|
||||
path: 'arithmetic-sequence',
|
||||
icon: 'ic:sharp-plus',
|
||||
description:
|
||||
'Generate an arithmetic sequence by specifying the first term (a₁), common difference (d), and number of terms (n). The tool creates a sequence where each number differs from the previous by a constant difference.',
|
||||
shortDescription:
|
||||
'Generate a sequence where each term differs by a constant value.',
|
||||
keywords: [
|
||||
'arithmetic',
|
||||
'sequence',
|
||||
'progression',
|
||||
'numbers',
|
||||
'series',
|
||||
'generate'
|
||||
],
|
||||
component: lazy(() => import('./index'))
|
||||
icon: 'material-symbols:functions',
|
||||
|
||||
keywords: ['arithmetic', 'sequence', 'math', 'progression'],
|
||||
component: lazy(() => import('./index')),
|
||||
i18n: {
|
||||
name: 'number:arithmeticSequence.title',
|
||||
description: 'number:arithmeticSequence.description',
|
||||
shortDescription: 'number:arithmeticSequence.shortDescription'
|
||||
}
|
||||
});
|
||||
|
||||
@@ -5,6 +5,7 @@ import { listOfIntegers } from './service';
|
||||
import TextFieldWithDesc from '@components/options/TextFieldWithDesc';
|
||||
import ToolContent from '@components/ToolContent';
|
||||
import { ToolComponentProps } from '@tools/defineTool';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
const initialValues = {
|
||||
firstValue: '1',
|
||||
@@ -14,6 +15,7 @@ const initialValues = {
|
||||
};
|
||||
|
||||
export default function GenerateNumbers({ title }: ToolComponentProps) {
|
||||
const { t } = useTranslation('number');
|
||||
const [result, setResult] = useState<string>('');
|
||||
|
||||
const compute = (optionsValues: typeof initialValues) => {
|
||||
@@ -34,23 +36,23 @@ export default function GenerateNumbers({ title }: ToolComponentProps) {
|
||||
initialValues={initialValues}
|
||||
getGroups={({ values, updateField }) => [
|
||||
{
|
||||
title: 'Arithmetic sequence option',
|
||||
title: t('generate.arithmeticSequenceOption'),
|
||||
component: (
|
||||
<Box>
|
||||
<TextFieldWithDesc
|
||||
description={'Start sequence from this number.'}
|
||||
description={t('generate.startSequenceDescription')}
|
||||
value={values.firstValue}
|
||||
onOwnChange={(val) => updateField('firstValue', val)}
|
||||
type={'number'}
|
||||
/>
|
||||
<TextFieldWithDesc
|
||||
description={'Increase each element by this amount'}
|
||||
description={t('generate.stepDescription')}
|
||||
value={values.step}
|
||||
onOwnChange={(val) => updateField('step', val)}
|
||||
type={'number'}
|
||||
/>
|
||||
<TextFieldWithDesc
|
||||
description={'Number of elements in sequence.'}
|
||||
description={t('generate.numberOfElementsDescription')}
|
||||
value={values.numberOfNumbers}
|
||||
onOwnChange={(val) => updateField('numberOfNumbers', val)}
|
||||
type={'number'}
|
||||
@@ -59,12 +61,10 @@ export default function GenerateNumbers({ title }: ToolComponentProps) {
|
||||
)
|
||||
},
|
||||
{
|
||||
title: 'Separator',
|
||||
title: t('generate.separator'),
|
||||
component: (
|
||||
<TextFieldWithDesc
|
||||
description={
|
||||
'Separate elements in the arithmetic sequence by this character.'
|
||||
}
|
||||
description={t('generate.separatorDescription')}
|
||||
value={values.separator}
|
||||
onOwnChange={(val) => updateField('separator', val)}
|
||||
/>
|
||||
@@ -73,7 +73,7 @@ export default function GenerateNumbers({ title }: ToolComponentProps) {
|
||||
]}
|
||||
compute={compute}
|
||||
resultComponent={
|
||||
<ToolTextResult title={'Generated numbers'} value={result} />
|
||||
<ToolTextResult title={t('generate.resultTitle')} value={result} />
|
||||
}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -3,12 +3,14 @@ import { lazy } from 'react';
|
||||
// import image from '@assets/text.png';
|
||||
|
||||
export const tool = defineTool('number', {
|
||||
name: 'Generate numbers',
|
||||
path: 'generate',
|
||||
shortDescription: 'Quickly calculate a list of integers in your browser',
|
||||
icon: 'lsicon:number-filled',
|
||||
description:
|
||||
'Quickly calculate a list of integers in your browser. To get your list, just specify the first integer, change value and total count in the options below, and this utility will generate that many integers',
|
||||
keywords: ['generate'],
|
||||
component: lazy(() => import('./index'))
|
||||
icon: 'material-symbols:add-circle',
|
||||
|
||||
keywords: ['generate', 'random', 'numbers'],
|
||||
component: lazy(() => import('./index')),
|
||||
i18n: {
|
||||
name: 'number:generate.title',
|
||||
description: 'number:generate.description',
|
||||
shortDescription: 'number:generate.shortDescription'
|
||||
}
|
||||
});
|
||||
|
||||
@@ -13,14 +13,14 @@ const ohmsLawCalc: GenericCalcType = {
|
||||
'power',
|
||||
'V=IR'
|
||||
],
|
||||
shortDescription:
|
||||
"Calculate voltage, current, or resistance in electrical circuits using Ohm's Law",
|
||||
name: "Ohm's Law",
|
||||
path: 'ohms-law',
|
||||
description: 'Calculates voltage, current and resistance',
|
||||
longDescription:
|
||||
"This calculator applies Ohm's Law (V = I × R) to determine any of the three electrical parameters when the other two are known. Ohm's Law is a fundamental principle in electrical engineering that describes the relationship between voltage (V), current (I), and resistance (R). This tool is essential for electronics hobbyists, electrical engineers, and students working with circuits to quickly solve for unknown values in their electrical designs.",
|
||||
formula: 'V = I * R',
|
||||
i18n: {
|
||||
name: 'number:ohmsLaw.title',
|
||||
description: 'number:ohmsLaw.description',
|
||||
shortDescription: 'number:ohmsLaw.shortDescription',
|
||||
longDescription: 'number:ohmsLaw.longDescription'
|
||||
},
|
||||
presets: [],
|
||||
variables: [
|
||||
{
|
||||
|
||||
@@ -11,14 +11,15 @@ const slackline: GenericCalcType = {
|
||||
'tension',
|
||||
'clothesline'
|
||||
],
|
||||
shortDescription:
|
||||
'Calculate the approximate tension of a slackline or clothesline. Do not rely on this for safety.',
|
||||
name: 'Slackline Tension',
|
||||
path: 'slackline-tension',
|
||||
description: 'Calculates tension in a slackline',
|
||||
longDescription: 'This calculator assumes a load in the center of the rope',
|
||||
formula: 'T = (W * sqrt((S**2) + ((L/2)**2)) )/ (2S)',
|
||||
presets: [],
|
||||
i18n: {
|
||||
name: 'number:slackline.title',
|
||||
description: 'number:slackline.description',
|
||||
shortDescription: 'number:slackline.shortDescription',
|
||||
longDescription: 'number:slackline.longDescription'
|
||||
},
|
||||
variables: [
|
||||
{
|
||||
name: 'L',
|
||||
|
||||
@@ -13,15 +13,15 @@ const areaSphere: GenericCalcType = {
|
||||
'3D',
|
||||
'shape'
|
||||
],
|
||||
shortDescription:
|
||||
'Calculate the surface area of a sphere based on its radius',
|
||||
name: 'Area of a Sphere',
|
||||
path: 'area-sphere',
|
||||
description: 'Area of a Sphere',
|
||||
longDescription:
|
||||
'This calculator determines the surface area of a sphere using the formula A = 4πr². You can either input the radius to find the surface area or enter the surface area to calculate the required radius. This tool is useful for students studying geometry, engineers working with spherical objects, and anyone needing to perform calculations involving spherical surfaces.',
|
||||
formula: 'A = 4 * pi * r**2',
|
||||
presets: [],
|
||||
i18n: {
|
||||
name: 'number:sphereArea.title',
|
||||
description: 'number:sphereArea.description',
|
||||
shortDescription: 'number:sphereArea.shortDescription',
|
||||
longDescription: 'number:sphereArea.longDescription'
|
||||
},
|
||||
variables: [
|
||||
{
|
||||
name: 'A',
|
||||
|
||||
@@ -14,12 +14,13 @@ const volumeSphere: GenericCalcType = {
|
||||
'shape',
|
||||
'capacity'
|
||||
],
|
||||
shortDescription: 'Calculate the volume of a sphere using radius or diameter',
|
||||
name: 'Volume of a Sphere',
|
||||
i18n: {
|
||||
name: 'number:sphereVolume.title',
|
||||
description: 'number:sphereVolume.description',
|
||||
shortDescription: 'number:sphereVolume.shortDescription',
|
||||
longDescription: 'number:sphereVolume.longDescription'
|
||||
},
|
||||
path: 'volume-sphere',
|
||||
description: 'Volume of a Sphere',
|
||||
longDescription:
|
||||
'This calculator computes the volume of a sphere using the formula V = (4/3)πr³. You can input either the radius or diameter to find the volume, or enter the volume to determine the required radius. The tool is valuable for students, engineers, and professionals working with spherical objects in fields such as physics, engineering, and manufacturing.',
|
||||
formula: 'v = (4/3) * pi * r**3',
|
||||
presets: [],
|
||||
variables: [
|
||||
|
||||
@@ -16,15 +16,14 @@ const voltageDropInWire: GenericCalcType = {
|
||||
'AWG',
|
||||
'gauge'
|
||||
],
|
||||
shortDescription:
|
||||
'Calculate voltage drop and power loss in electrical cables based on length, material, and current',
|
||||
name: 'Round trip voltage drop in cable',
|
||||
path: 'cable-voltage-drop',
|
||||
formula: 'x = (((p * L) / (A/10**6) ) *2) * I',
|
||||
description:
|
||||
'Calculates round trip voltage and power loss in a 2 conductor cable',
|
||||
longDescription:
|
||||
'This calculator helps determine the voltage drop and power loss in a two-conductor electrical cable. It takes into account the cable length, wire gauge (cross-sectional area), material resistivity, and current flow. The tool calculates the round-trip voltage drop, total resistance of the cable, and the power dissipated as heat. This is particularly useful for electrical engineers, electricians, and hobbyists when designing electrical systems to ensure voltage levels remain within acceptable limits at the load.',
|
||||
i18n: {
|
||||
name: 'number:voltageDropInWire.title',
|
||||
description: 'number:voltageDropInWire.description',
|
||||
shortDescription: 'number:voltageDropInWire.shortDescription',
|
||||
longDescription: 'number:voltageDropInWire.longDescription'
|
||||
},
|
||||
presets: [
|
||||
{
|
||||
title: 'Material',
|
||||
|
||||
@@ -26,6 +26,8 @@ import { CustomSnackBarContext } from 'contexts/CustomSnackBarContext';
|
||||
import Typography from '@mui/material/Typography';
|
||||
import Grid from '@mui/material/Grid';
|
||||
import useMediaQuery from '@mui/material/useMediaQuery';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { validNamespaces } from '../../../../i18n';
|
||||
|
||||
function numericSolveEquationFor(
|
||||
equation: string,
|
||||
@@ -61,6 +63,7 @@ export default async function makeTool(
|
||||
|
||||
return function GenericCalc({ title }: ToolComponentProps) {
|
||||
const { showSnackBar } = useContext(CustomSnackBarContext);
|
||||
const { t } = useTranslation(validNamespaces);
|
||||
const theme = useTheme();
|
||||
const lessThanSmall = useMediaQuery(theme.breakpoints.down('sm'));
|
||||
|
||||
@@ -236,8 +239,10 @@ export default async function makeTool(
|
||||
inputComponent={null}
|
||||
initialValues={initialValues}
|
||||
toolInfo={{
|
||||
title: calcData.name,
|
||||
description: calcData.longDescription
|
||||
title: t(calcData.i18n.name),
|
||||
description: calcData.i18n.longDescription
|
||||
? t(calcData.i18n.longDescription)
|
||||
: undefined
|
||||
}}
|
||||
verticalGroups
|
||||
// @ts-ignore
|
||||
|
||||
@@ -9,6 +9,7 @@ import CheckboxWithDesc from '@components/options/CheckboxWithDesc';
|
||||
import { CardExampleType } from '@components/examples/ToolExamples';
|
||||
import { ToolComponentProps } from '@tools/defineTool';
|
||||
import ToolContent from '@components/ToolContent';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
const initialValues = {
|
||||
extractionType: 'smart' as NumberExtractionType,
|
||||
@@ -118,6 +119,7 @@ const exampleCards: CardExampleType<InitialValuesType>[] = [
|
||||
];
|
||||
|
||||
export default function SumNumbers({ title }: ToolComponentProps) {
|
||||
const { t } = useTranslation('number');
|
||||
const [input, setInput] = useState<string>('');
|
||||
const [result, setResult] = useState<string>('');
|
||||
|
||||
@@ -126,16 +128,16 @@ export default function SumNumbers({ title }: ToolComponentProps) {
|
||||
updateField
|
||||
}) => [
|
||||
{
|
||||
title: 'Number extraction',
|
||||
title: t('sum.numberExtraction'),
|
||||
component: extractionTypes.map(
|
||||
({ title, description, type, withTextField, textValueAccessor }) =>
|
||||
withTextField ? (
|
||||
<RadioWithTextField
|
||||
key={type}
|
||||
checked={type === values.extractionType}
|
||||
title={title}
|
||||
title={t(`sum.extractionTypes.${type}.title`)}
|
||||
fieldName={'extractionType'}
|
||||
description={description}
|
||||
description={t(`sum.extractionTypes.${type}.description`)}
|
||||
value={
|
||||
textValueAccessor ? values[textValueAccessor].toString() : ''
|
||||
}
|
||||
@@ -149,18 +151,18 @@ export default function SumNumbers({ title }: ToolComponentProps) {
|
||||
key={title}
|
||||
onClick={() => updateField('extractionType', type)}
|
||||
checked={values.extractionType === type}
|
||||
description={description}
|
||||
title={title}
|
||||
description={t(`sum.extractionTypes.${type}.description`)}
|
||||
title={t(`sum.extractionTypes.${type}.title`)}
|
||||
/>
|
||||
)
|
||||
)
|
||||
},
|
||||
{
|
||||
title: 'Running Sum',
|
||||
title: t('sum.runningSum'),
|
||||
component: (
|
||||
<CheckboxWithDesc
|
||||
title={'Print Running Sum'}
|
||||
description={"Display the sum as it's calculated step by step."}
|
||||
title={t('sum.printRunningSum')}
|
||||
description={t('sum.printRunningSumDescription')}
|
||||
checked={values.printRunningSum}
|
||||
onChange={(value) => updateField('printRunningSum', value)}
|
||||
/>
|
||||
@@ -171,8 +173,16 @@ export default function SumNumbers({ title }: ToolComponentProps) {
|
||||
<ToolContent
|
||||
title={title}
|
||||
input={input}
|
||||
inputComponent={<ToolTextInput value={input} onChange={setInput} />}
|
||||
resultComponent={<ToolTextResult title={'Total'} value={result} />}
|
||||
inputComponent={
|
||||
<ToolTextInput
|
||||
title={t('sum.inputTitle')}
|
||||
value={input}
|
||||
onChange={setInput}
|
||||
/>
|
||||
}
|
||||
resultComponent={
|
||||
<ToolTextResult title={t('sum.resultTitle')} value={result} />
|
||||
}
|
||||
initialValues={initialValues}
|
||||
getGroups={getGroups}
|
||||
compute={(optionsValues, input) => {
|
||||
@@ -181,9 +191,8 @@ export default function SumNumbers({ title }: ToolComponentProps) {
|
||||
}}
|
||||
setInput={setInput}
|
||||
toolInfo={{
|
||||
title: 'What Is a Number Sum Calculator?',
|
||||
description:
|
||||
'This is an online browser-based utility for calculating the sum of a bunch of numbers. You can enter the numbers separated by a comma, space, or any other character, including the line break. You can also simply paste a fragment of textual data that contains numerical values that you want to sum up and the utility will extract them and find their sum.'
|
||||
title: t('sum.toolInfo.title'),
|
||||
description: t('sum.toolInfo.description')
|
||||
}}
|
||||
exampleCards={exampleCards}
|
||||
/>
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user