mirror of
https://github.com/iib0011/omni-tools.git
synced 2025-12-29 16:16:02 +00:00
Merge branch 'main' into tool/hidden-character-detector
This commit is contained in:
@@ -12,6 +12,7 @@ import { darkTheme, lightTheme } from '../config/muiConfig';
|
||||
import ScrollToTopButton from './ScrollToTopButton';
|
||||
import { I18nextProvider } from 'react-i18next';
|
||||
import i18n from '../i18n';
|
||||
import { UserTypeFilterProvider } from 'providers/UserTypeFilterProvider';
|
||||
|
||||
export type Mode = 'dark' | 'light' | 'system';
|
||||
|
||||
@@ -57,18 +58,20 @@ function App() {
|
||||
}}
|
||||
>
|
||||
<CustomSnackBarProvider>
|
||||
<BrowserRouter>
|
||||
<Navbar
|
||||
mode={mode}
|
||||
onChangeMode={() => {
|
||||
setMode((prev) => nextMode(prev));
|
||||
localStorage.setItem('theme', nextMode(mode));
|
||||
}}
|
||||
/>
|
||||
<Suspense fallback={<Loading />}>
|
||||
<AppRoutes />
|
||||
</Suspense>
|
||||
</BrowserRouter>
|
||||
<UserTypeFilterProvider>
|
||||
<BrowserRouter>
|
||||
<Navbar
|
||||
mode={mode}
|
||||
onChangeMode={() => {
|
||||
setMode((prev) => nextMode(prev));
|
||||
localStorage.setItem('theme', nextMode(mode));
|
||||
}}
|
||||
/>
|
||||
<Suspense fallback={<Loading />}>
|
||||
<AppRoutes />
|
||||
</Suspense>
|
||||
</BrowserRouter>
|
||||
</UserTypeFilterProvider>
|
||||
</CustomSnackBarProvider>
|
||||
</SnackbarProvider>
|
||||
<ScrollToTopButton />
|
||||
|
||||
@@ -25,6 +25,7 @@ import {
|
||||
toggleBookmarked
|
||||
} from '@utils/bookmark';
|
||||
import IconButton from '@mui/material/IconButton';
|
||||
import { useUserTypeFilter } from '../providers/UserTypeFilterProvider';
|
||||
|
||||
const GroupHeader = styled('div')(({ theme }) => ({
|
||||
position: 'sticky',
|
||||
@@ -50,6 +51,7 @@ export default function Hero() {
|
||||
const { t } = useTranslation(validNamespaces);
|
||||
const [inputValue, setInputValue] = useState<string>('');
|
||||
const theme = useTheme();
|
||||
const { selectedUserTypes } = useUserTypeFilter();
|
||||
const [filteredTools, setFilteredTools] = useState<DefinedTool[]>(tools);
|
||||
const [bookmarkedToolPaths, setBookmarkedToolPaths] = useState<string[]>(
|
||||
getBookmarkedToolPaths()
|
||||
@@ -96,12 +98,13 @@ export default function Hero() {
|
||||
];
|
||||
|
||||
const handleInputChange = (
|
||||
event: React.ChangeEvent<{}>,
|
||||
_event: React.ChangeEvent<{}>,
|
||||
newInputValue: string
|
||||
) => {
|
||||
setInputValue(newInputValue);
|
||||
setFilteredTools(filterTools(tools, newInputValue, t));
|
||||
setFilteredTools(filterTools(tools, newInputValue, selectedUserTypes, t));
|
||||
};
|
||||
|
||||
const toolsMap = new Map<string, ToolInfo>();
|
||||
for (const tool of filteredTools) {
|
||||
toolsMap.set(tool.path, {
|
||||
|
||||
@@ -103,7 +103,7 @@ export default function ToolHeader({
|
||||
items={[
|
||||
{ title: 'All tools', link: '/' },
|
||||
{
|
||||
title: getToolsByCategory(t).find(
|
||||
title: getToolsByCategory([], t).find(
|
||||
(category) => category.type === type
|
||||
)!.rawTitle,
|
||||
link: '/categories/' + type
|
||||
|
||||
@@ -43,7 +43,7 @@ export default function ToolLayout({
|
||||
const toolDescription: string = t(i18n.description);
|
||||
|
||||
const otherCategoryTools =
|
||||
getToolsByCategory(t)
|
||||
getToolsByCategory([], t)
|
||||
.find((category) => category.type === type)
|
||||
?.tools.filter((tool) => t(tool.name) !== toolTitle)
|
||||
.map((tool) => ({
|
||||
@@ -77,8 +77,9 @@ export default function ToolLayout({
|
||||
<AllTools
|
||||
title={t('translation:toolLayout.allToolsTitle', '', {
|
||||
type: capitalizeFirstLetter(
|
||||
getToolsByCategory(t).find((category) => category.type === type)!
|
||||
.title
|
||||
getToolsByCategory([], t).find(
|
||||
(category) => category.type === type
|
||||
)!.title
|
||||
)
|
||||
})}
|
||||
toolCards={otherCategoryTools}
|
||||
|
||||
47
src/components/UserTypeFilter.tsx
Normal file
47
src/components/UserTypeFilter.tsx
Normal file
@@ -0,0 +1,47 @@
|
||||
import React from 'react';
|
||||
import { Box, Chip } from '@mui/material';
|
||||
import { UserType } from '@tools/defineTool';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
interface UserTypeFilterProps {
|
||||
selectedUserTypes: UserType[];
|
||||
userTypes?: UserType[];
|
||||
onUserTypesChange: (userTypes: UserType[]) => void;
|
||||
}
|
||||
|
||||
export default function UserTypeFilter({
|
||||
selectedUserTypes,
|
||||
onUserTypesChange,
|
||||
userTypes = ['generalUsers', 'developers']
|
||||
}: UserTypeFilterProps) {
|
||||
const { t } = useTranslation('translation');
|
||||
if (userTypes.length <= 1) return null;
|
||||
return (
|
||||
<Box
|
||||
sx={{
|
||||
display: 'flex',
|
||||
flexWrap: 'wrap',
|
||||
gap: 1,
|
||||
minWidth: 200,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center'
|
||||
}}
|
||||
>
|
||||
{userTypes.map((userType) => (
|
||||
<Chip
|
||||
key={userType}
|
||||
label={t(`userTypes.${userType}`)}
|
||||
color={selectedUserTypes.includes(userType) ? 'primary' : 'default'}
|
||||
onClick={() => {
|
||||
const isSelected = selectedUserTypes.includes(userType);
|
||||
const newUserTypes = isSelected
|
||||
? selectedUserTypes.filter((ut) => ut !== userType)
|
||||
: [...selectedUserTypes, userType];
|
||||
onUserTypesChange(newUserTypes);
|
||||
}}
|
||||
sx={{ cursor: 'pointer' }}
|
||||
/>
|
||||
))}
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
@@ -5,7 +5,10 @@ import InputHeader from '../InputHeader';
|
||||
import InputFooter from './InputFooter';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import Editor from '@monaco-editor/react';
|
||||
import { globalInputHeight } from '../../config/uiConfig';
|
||||
import {
|
||||
globalInputHeight,
|
||||
codeInputHeightOffset
|
||||
} from '../../config/uiConfig';
|
||||
|
||||
export default function ToolCodeInput({
|
||||
value,
|
||||
@@ -53,14 +56,62 @@ export default function ToolCodeInput({
|
||||
return (
|
||||
<Box>
|
||||
<InputHeader title={title || t('toolTextInput.input')} />
|
||||
<Box height={globalInputHeight}>
|
||||
<Editor
|
||||
height={'87%'}
|
||||
language={language}
|
||||
theme={theme.palette.mode === 'dark' ? 'vs-dark' : 'light'}
|
||||
value={value}
|
||||
onChange={(value) => onChange(value ?? '')}
|
||||
/>
|
||||
<Box
|
||||
height={`${globalInputHeight + codeInputHeightOffset}px`} // The +codeInputHeightOffset compensates for internal padding/border differences between Monaco Editor and MUI TextField
|
||||
sx={{
|
||||
display: 'flex',
|
||||
flexDirection: 'column'
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
sx={(theme) => ({
|
||||
height: '100%',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
backgroundColor: 'background.paper',
|
||||
'.monaco-editor': {
|
||||
height: '100% !important',
|
||||
outline: 'none !important',
|
||||
'.overflow-guard': {
|
||||
height: '100% !important',
|
||||
border:
|
||||
theme.palette.mode === 'light'
|
||||
? '1px solid rgba(0, 0, 0, 0.23)'
|
||||
: '1px solid rgba(255, 255, 255, 0.23)',
|
||||
borderRadius: 1,
|
||||
transition: theme.transitions.create(
|
||||
['border-color', 'background-color'],
|
||||
{
|
||||
duration: theme.transitions.duration.shorter
|
||||
}
|
||||
)
|
||||
},
|
||||
'&:hover .overflow-guard': {
|
||||
borderColor: theme.palette.text.primary
|
||||
}
|
||||
},
|
||||
'.decorationsOverviewRuler': {
|
||||
display: 'none !important'
|
||||
}
|
||||
})}
|
||||
>
|
||||
<Editor
|
||||
height="100%"
|
||||
language={language}
|
||||
theme={theme.palette.mode === 'dark' ? 'vs-dark' : 'light'}
|
||||
value={value}
|
||||
onChange={(value) => onChange(value ?? '')}
|
||||
options={{
|
||||
scrollbar: {
|
||||
vertical: 'visible',
|
||||
horizontal: 'visible',
|
||||
verticalScrollbarSize: 10,
|
||||
horizontalScrollbarSize: 10,
|
||||
alwaysConsumeMouseWheel: false
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
<InputFooter handleCopy={handleCopy} handleImport={handleImportClick} />
|
||||
<input
|
||||
type="file"
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
export const globalInputHeight = 300;
|
||||
export const codeInputHeightOffset = 7; // Offset to visually match Monaco and MUI TextField heights
|
||||
export const globalDescriptionFontSize = 12;
|
||||
export const categoriesColors: string[] = [
|
||||
'#8FBC5D',
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import i18n, { Namespace, ParseKeys } from 'i18next';
|
||||
import { initReactI18next } from 'react-i18next';
|
||||
import Backend from 'i18next-http-backend';
|
||||
import LanguageDetector from 'i18next-browser-languagedetector';
|
||||
|
||||
export const validNamespaces = [
|
||||
'string',
|
||||
@@ -24,15 +25,20 @@ export type FullI18nKey = {
|
||||
|
||||
i18n
|
||||
.use(Backend)
|
||||
.use(LanguageDetector)
|
||||
.use(initReactI18next)
|
||||
.init({
|
||||
lng: localStorage.getItem('lang') || 'en',
|
||||
supportedLngs: ['en', 'de', 'es', 'fr', 'pt', 'ja', 'hi', 'nl', 'ru', 'zh'],
|
||||
fallbackLng: 'en',
|
||||
interpolation: {
|
||||
escapeValue: false // react already safes from xss => https://www.i18next.com/translation-function/interpolation#unescape
|
||||
},
|
||||
backend: {
|
||||
loadPath: '/locales/{{lng}}/{{ns}}.json'
|
||||
},
|
||||
detection: {
|
||||
lookupLocalStorage: 'lang',
|
||||
caches: ['localStorage'] // cache the detected lang back to localStorage
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@ import { categoriesColors } from 'config/uiConfig';
|
||||
import { Icon } from '@iconify/react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { getI18nNamespaceFromToolCategory } from '@utils/string';
|
||||
import { validNamespaces } from '../../i18n';
|
||||
import { useUserTypeFilter } from '../../providers/UserTypeFilterProvider';
|
||||
|
||||
type ArrayElement<ArrayType extends readonly unknown[]> =
|
||||
ArrayType extends readonly (infer ElementType)[] ? ElementType : never;
|
||||
@@ -84,10 +84,11 @@ const SingleCategory = function ({
|
||||
</Stack>
|
||||
<Typography sx={{ mt: 2 }}>{categoryDescription}</Typography>
|
||||
</Box>
|
||||
<Grid mt={1} container spacing={2}>
|
||||
<Grid container spacing={2} mt={2}>
|
||||
<Grid item xs={12} md={6}>
|
||||
<Button
|
||||
fullWidth
|
||||
sx={{ height: '100%' }}
|
||||
onClick={() => navigate('/categories/' + category.type)}
|
||||
variant={'contained'}
|
||||
>
|
||||
@@ -96,7 +97,7 @@ const SingleCategory = function ({
|
||||
</Grid>
|
||||
<Grid item xs={12} md={6}>
|
||||
<Button
|
||||
sx={{ backgroundColor: 'background.default' }}
|
||||
sx={{ backgroundColor: 'background.default', height: '100%' }}
|
||||
fullWidth
|
||||
onClick={() => navigate(category.example.path)}
|
||||
variant={'outlined'}
|
||||
@@ -111,11 +112,15 @@ const SingleCategory = function ({
|
||||
</Grid>
|
||||
);
|
||||
};
|
||||
|
||||
export default function Categories() {
|
||||
const { selectedUserTypes } = useUserTypeFilter();
|
||||
const { t } = useTranslation();
|
||||
const categories = getToolsByCategory(selectedUserTypes, t);
|
||||
|
||||
return (
|
||||
<Grid width={'80%'} container mt={2} spacing={2}>
|
||||
{getToolsByCategory(t).map((category, index) => (
|
||||
<Grid width={'80%'} container spacing={2}>
|
||||
{categories.map((category, index) => (
|
||||
<SingleCategory key={category.type} category={category} index={index} />
|
||||
))}
|
||||
</Grid>
|
||||
|
||||
@@ -2,9 +2,12 @@ import { Box, useTheme } from '@mui/material';
|
||||
import Hero from 'components/Hero';
|
||||
import Categories from './Categories';
|
||||
import { Helmet } from 'react-helmet';
|
||||
import { useUserTypeFilter } from 'providers/UserTypeFilterProvider';
|
||||
import UserTypeFilter from '@components/UserTypeFilter';
|
||||
|
||||
export default function Home() {
|
||||
const theme = useTheme();
|
||||
const { selectedUserTypes, setSelectedUserTypes } = useUserTypeFilter();
|
||||
return (
|
||||
<Box
|
||||
padding={{
|
||||
@@ -28,6 +31,12 @@ export default function Home() {
|
||||
>
|
||||
<Helmet title={'OmniTools'} />
|
||||
<Hero />
|
||||
<Box my={3}>
|
||||
<UserTypeFilter
|
||||
selectedUserTypes={selectedUserTypes}
|
||||
onUserTypesChange={setSelectedUserTypes}
|
||||
/>
|
||||
</Box>
|
||||
<Categories />
|
||||
</Box>
|
||||
);
|
||||
|
||||
@@ -22,28 +22,38 @@ import IconButton from '@mui/material/IconButton';
|
||||
import ArrowBackIcon from '@mui/icons-material/ArrowBack';
|
||||
import SearchIcon from '@mui/icons-material/Search';
|
||||
import { Helmet } from 'react-helmet';
|
||||
import UserTypeFilter from '@components/UserTypeFilter';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { I18nNamespaces, validNamespaces } from '../../i18n';
|
||||
import { useUserTypeFilter } from '../../providers/UserTypeFilterProvider';
|
||||
|
||||
const StyledLink = styled(Link)(({ theme }) => ({
|
||||
'&:hover': {
|
||||
color: theme.palette.mode === 'dark' ? 'white' : theme.palette.primary.light
|
||||
}
|
||||
}));
|
||||
|
||||
export default function ToolsByCategory() {
|
||||
const navigate = useNavigate();
|
||||
const theme = useTheme();
|
||||
const mainContentRef = React.useRef<HTMLDivElement>(null);
|
||||
const { categoryName } = useParams();
|
||||
const [searchTerm, setSearchTerm] = React.useState<string>('');
|
||||
const { selectedUserTypes, setSelectedUserTypes } = useUserTypeFilter();
|
||||
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 toolsByCategory = getToolsByCategory(selectedUserTypes, t).find(
|
||||
({ type }) => type === categoryName
|
||||
);
|
||||
const categoryDefinedTools = toolsByCategory?.tools ?? [];
|
||||
|
||||
const categoryTools = filterTools(toolsByCategory, searchTerm, t);
|
||||
const categoryTools = filterTools(
|
||||
categoryDefinedTools,
|
||||
searchTerm,
|
||||
selectedUserTypes,
|
||||
t
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (mainContentRef.current) {
|
||||
@@ -90,7 +100,20 @@ export default function ToolsByCategory() {
|
||||
onChange={(event) => setSearchTerm(event.target.value)}
|
||||
/>
|
||||
</Stack>
|
||||
<Grid container spacing={2} mt={2}>
|
||||
<Box
|
||||
width={'100%'}
|
||||
display={'flex'}
|
||||
alignItems={'center'}
|
||||
justifyContent={'center'}
|
||||
my={2}
|
||||
>
|
||||
<UserTypeFilter
|
||||
userTypes={toolsByCategory?.userTypes ?? undefined}
|
||||
selectedUserTypes={selectedUserTypes}
|
||||
onUserTypesChange={setSelectedUserTypes}
|
||||
/>
|
||||
</Box>
|
||||
<Grid container spacing={2}>
|
||||
{categoryTools.map((tool, index) => (
|
||||
<Grid item xs={12} md={6} lg={4} key={tool.path}>
|
||||
<Stack
|
||||
|
||||
@@ -20,6 +20,7 @@ export const tool = defineTool('audio', {
|
||||
i18n: {
|
||||
name: 'audio:changeSpeed.title',
|
||||
description: 'audio:changeSpeed.description',
|
||||
shortDescription: 'audio:changeSpeed.shortDescription'
|
||||
shortDescription: 'audio:changeSpeed.shortDescription',
|
||||
userTypes: ['generalUsers', 'developers']
|
||||
}
|
||||
});
|
||||
|
||||
@@ -21,6 +21,7 @@ export const tool = defineTool('audio', {
|
||||
i18n: {
|
||||
name: 'audio:extractAudio.title',
|
||||
description: 'audio:extractAudio.description',
|
||||
shortDescription: 'audio:extractAudio.shortDescription'
|
||||
shortDescription: 'audio:extractAudio.shortDescription',
|
||||
userTypes: ['generalUsers', 'developers']
|
||||
}
|
||||
});
|
||||
|
||||
@@ -6,7 +6,8 @@ export const tool = defineTool('audio', {
|
||||
name: 'audio:mergeAudio.title',
|
||||
description: 'audio:mergeAudio.description',
|
||||
shortDescription: 'audio:mergeAudio.shortDescription',
|
||||
longDescription: 'audio:mergeAudio.longDescription'
|
||||
longDescription: 'audio:mergeAudio.longDescription',
|
||||
userTypes: ['generalUsers', 'developers']
|
||||
},
|
||||
|
||||
path: 'merge-audio',
|
||||
@@ -24,6 +25,5 @@ export const tool = defineTool('audio', {
|
||||
'audio editing',
|
||||
'multiple files'
|
||||
],
|
||||
|
||||
component: lazy(() => import('./index'))
|
||||
});
|
||||
|
||||
@@ -6,7 +6,8 @@ export const tool = defineTool('audio', {
|
||||
name: 'audio:trim.title',
|
||||
description: 'audio:trim.description',
|
||||
shortDescription: 'audio:trim.shortDescription',
|
||||
longDescription: 'audio:trim.longDescription'
|
||||
longDescription: 'audio:trim.longDescription',
|
||||
userTypes: ['generalUsers', 'developers']
|
||||
},
|
||||
|
||||
path: 'trim',
|
||||
@@ -24,6 +25,5 @@ export const tool = defineTool('audio', {
|
||||
'audio editing',
|
||||
'time'
|
||||
],
|
||||
|
||||
component: lazy(() => import('./index'))
|
||||
});
|
||||
|
||||
@@ -12,6 +12,5 @@ export const tool = defineTool('csv', {
|
||||
path: 'csv-to-yaml',
|
||||
icon: 'nonicons:yaml-16',
|
||||
keywords: ['csv', 'to', 'yaml'],
|
||||
|
||||
component: lazy(() => import('./index'))
|
||||
});
|
||||
|
||||
@@ -12,6 +12,5 @@ export const tool = defineTool('csv', {
|
||||
icon: 'hugeicons:column-insert',
|
||||
|
||||
keywords: ['insert', 'csv', 'columns', 'append', 'prepend'],
|
||||
|
||||
component: lazy(() => import('./index'))
|
||||
});
|
||||
|
||||
@@ -13,6 +13,5 @@ export const tool = defineTool('csv', {
|
||||
icon: 'carbon:transpose',
|
||||
|
||||
keywords: ['transpose', 'csv'],
|
||||
|
||||
component: lazy(() => import('./index'))
|
||||
});
|
||||
|
||||
@@ -5,12 +5,13 @@ export const tool = defineTool('image-generic', {
|
||||
i18n: {
|
||||
name: 'image:compress.title',
|
||||
description: 'image:compress.description',
|
||||
shortDescription: 'image:compress.shortDescription'
|
||||
shortDescription: 'image:compress.shortDescription',
|
||||
userTypes: ['generalUsers']
|
||||
},
|
||||
|
||||
path: 'compress',
|
||||
component: lazy(() => import('./index')),
|
||||
icon: 'material-symbols-light:compress-rounded',
|
||||
|
||||
keywords: ['image', 'compress', 'reduce', 'quality']
|
||||
keywords: ['image', 'compress', 'reduce', 'quality'],
|
||||
component: lazy(() => import('./index'))
|
||||
});
|
||||
|
||||
@@ -5,6 +5,7 @@ import ToolContent from '@components/ToolContent';
|
||||
import { ToolComponentProps } from '@tools/defineTool';
|
||||
import ToolImageInput from '@components/input/ToolImageInput';
|
||||
import { removeBackground } from '@imgly/background-removal';
|
||||
import * as heic2any from 'heic2any';
|
||||
|
||||
const initialValues = {};
|
||||
|
||||
@@ -23,8 +24,33 @@ export default function RemoveBackgroundFromImage({
|
||||
setIsProcessing(true);
|
||||
|
||||
try {
|
||||
// Convert the input file to a Blob URL
|
||||
const inputUrl = URL.createObjectURL(input);
|
||||
let fileToProcess = input;
|
||||
// Check if the file is HEIC (by MIME type or extension)
|
||||
if (
|
||||
input.type === 'image/heic' ||
|
||||
input.name?.toLowerCase().endsWith('.heic')
|
||||
) {
|
||||
// Convert HEIC to PNG using heic2any
|
||||
const convertedBlob = await heic2any.default({
|
||||
blob: input,
|
||||
toType: 'image/png'
|
||||
});
|
||||
// heic2any returns a Blob or an array of Blobs
|
||||
let pngBlob;
|
||||
if (Array.isArray(convertedBlob)) {
|
||||
pngBlob = convertedBlob[0];
|
||||
} else {
|
||||
pngBlob = convertedBlob;
|
||||
}
|
||||
fileToProcess = new File(
|
||||
[pngBlob],
|
||||
input.name.replace(/\.[^/.]+$/, '') + '.png',
|
||||
{ type: 'image/png' }
|
||||
);
|
||||
}
|
||||
|
||||
// Convert the file to a Blob URL
|
||||
const inputUrl = URL.createObjectURL(fileToProcess);
|
||||
|
||||
// Process the image with the background removal library
|
||||
const blob = await removeBackground(inputUrl, {
|
||||
@@ -36,7 +62,7 @@ export default function RemoveBackgroundFromImage({
|
||||
// Create a new file from the blob
|
||||
const newFile = new File(
|
||||
[blob],
|
||||
input.name.replace(/\.[^/.]+$/, '') + '-no-bg.png',
|
||||
fileToProcess.name.replace(/\.[^/.]+$/, '') + '-no-bg.png',
|
||||
{
|
||||
type: 'image/png'
|
||||
}
|
||||
|
||||
@@ -5,7 +5,8 @@ export const tool = defineTool('image-generic', {
|
||||
i18n: {
|
||||
name: 'image:resize.title',
|
||||
description: 'image:resize.description',
|
||||
shortDescription: 'image:resize.shortDescription'
|
||||
shortDescription: 'image:resize.shortDescription',
|
||||
userTypes: ['generalUsers']
|
||||
},
|
||||
|
||||
path: 'resize',
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import React, { useState } from 'react';
|
||||
import ToolContent from '@components/ToolContent';
|
||||
import ToolTextInput from '@components/input/ToolTextInput';
|
||||
import ToolCodeInput from '@components/input/ToolCodeInput';
|
||||
import ToolTextResult from '@components/result/ToolTextResult';
|
||||
import { escapeJson } from './service';
|
||||
import { CardExampleType } from '@components/examples/ToolExamples';
|
||||
@@ -88,7 +88,12 @@ export default function EscapeJsonTool({
|
||||
<ToolContent
|
||||
title={title}
|
||||
inputComponent={
|
||||
<ToolTextInput title="Input JSON" value={input} onChange={setInput} />
|
||||
<ToolCodeInput
|
||||
title="Input JSON"
|
||||
value={input}
|
||||
onChange={setInput}
|
||||
language="json"
|
||||
/>
|
||||
}
|
||||
resultComponent={
|
||||
<ToolTextResult
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useState } from 'react';
|
||||
import ToolContent from '@components/ToolContent';
|
||||
import ToolTextInput from '@components/input/ToolTextInput';
|
||||
import ToolCodeInput from '@components/input/ToolCodeInput';
|
||||
import ToolTextResult from '@components/result/ToolTextResult';
|
||||
import { convertJsonToXml } from './service';
|
||||
import { CardExampleType } from '@components/examples/ToolExamples';
|
||||
@@ -84,7 +84,12 @@ export default function JsonToXml({ title }: ToolComponentProps) {
|
||||
compute={compute}
|
||||
exampleCards={exampleCards}
|
||||
inputComponent={
|
||||
<ToolTextInput title="Input Json" value={input} onChange={setInput} />
|
||||
<ToolCodeInput
|
||||
title="Input Json"
|
||||
value={input}
|
||||
onChange={setInput}
|
||||
language="json"
|
||||
/>
|
||||
}
|
||||
resultComponent={
|
||||
<ToolTextResult title="Output XML" value={result} extension={'xml'} />
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import React, { useState } from 'react';
|
||||
import ToolContent from '@components/ToolContent';
|
||||
import ToolTextInput from '@components/input/ToolTextInput';
|
||||
import ToolCodeInput from '@components/input/ToolCodeInput';
|
||||
import ToolTextResult from '@components/result/ToolTextResult';
|
||||
import { minifyJson } from './service';
|
||||
import { CardExampleType } from '@components/examples/ToolExamples';
|
||||
@@ -60,10 +60,11 @@ export default function MinifyJson({ title }: ToolComponentProps) {
|
||||
<ToolContent
|
||||
title={title}
|
||||
inputComponent={
|
||||
<ToolTextInput
|
||||
<ToolCodeInput
|
||||
title={t('minify.inputTitle')}
|
||||
value={input}
|
||||
onChange={setInput}
|
||||
language="json"
|
||||
/>
|
||||
}
|
||||
resultComponent={
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Box } from '@mui/material';
|
||||
import React, { useRef, useState } from 'react';
|
||||
import ToolTextInput from '@components/input/ToolTextInput';
|
||||
import ToolCodeInput from '@components/input/ToolCodeInput';
|
||||
import ToolTextResult from '@components/result/ToolTextResult';
|
||||
import { beautifyJson } from './service';
|
||||
import ToolInfo from '@components/ToolInfo';
|
||||
@@ -130,10 +130,11 @@ export default function PrettifyJson({ title }: ToolComponentProps) {
|
||||
title={title}
|
||||
input={input}
|
||||
inputComponent={
|
||||
<ToolTextInput
|
||||
<ToolCodeInput
|
||||
title={t('prettify.inputTitle')}
|
||||
value={input}
|
||||
onChange={setInput}
|
||||
language="json"
|
||||
/>
|
||||
}
|
||||
resultComponent={
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Box } from '@mui/material';
|
||||
import React, { useState } from 'react';
|
||||
import ToolContent from '@components/ToolContent';
|
||||
import ToolTextInput from '@components/input/ToolTextInput';
|
||||
import ToolCodeInput from '@components/input/ToolCodeInput';
|
||||
import ToolTextResult from '@components/result/ToolTextResult';
|
||||
import { stringifyJson } from './service';
|
||||
import { ToolComponentProps } from '@tools/defineTool';
|
||||
@@ -103,10 +103,11 @@ export default function StringifyJson({ title }: ToolComponentProps) {
|
||||
compute={compute}
|
||||
exampleCards={exampleCards}
|
||||
inputComponent={
|
||||
<ToolTextInput
|
||||
<ToolCodeInput
|
||||
title="JavaScript Object/Array"
|
||||
value={input}
|
||||
onChange={setInput}
|
||||
language="json"
|
||||
/>
|
||||
}
|
||||
resultComponent={
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import React, { useState } from 'react';
|
||||
import ToolContent from '@components/ToolContent';
|
||||
import ToolTextInput from '@components/input/ToolTextInput';
|
||||
import ToolCodeInput from '@components/input/ToolCodeInput';
|
||||
import ToolTextResult from '@components/result/ToolTextResult';
|
||||
import { convertTsvToJson } from './service';
|
||||
import { CardExampleType } from '@components/examples/ToolExamples';
|
||||
@@ -216,7 +216,12 @@ export default function TsvToJson({
|
||||
exampleCards={exampleCards}
|
||||
getGroups={getGroups}
|
||||
inputComponent={
|
||||
<ToolTextInput title="Input TSV" value={input} onChange={setInput} />
|
||||
<ToolCodeInput
|
||||
title="Input TSV"
|
||||
value={input}
|
||||
onChange={setInput}
|
||||
language="tsv"
|
||||
/>
|
||||
}
|
||||
resultComponent={
|
||||
<ToolTextResult title="Output JSON" value={result} extension={'json'} />
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import React, { useState } from 'react';
|
||||
import ToolTextInput from '@components/input/ToolTextInput';
|
||||
import ToolCodeInput from '@components/input/ToolCodeInput';
|
||||
import ToolTextResult from '@components/result/ToolTextResult';
|
||||
import { CardExampleType } from '@components/examples/ToolExamples';
|
||||
import { validateJson } from './service';
|
||||
@@ -65,10 +65,11 @@ export default function ValidateJson({ title }: ToolComponentProps) {
|
||||
<ToolContent
|
||||
title={title}
|
||||
inputComponent={
|
||||
<ToolTextInput
|
||||
<ToolCodeInput
|
||||
title={t('validateJson.inputTitle')}
|
||||
value={input}
|
||||
onChange={setInput}
|
||||
language="json"
|
||||
/>
|
||||
}
|
||||
resultComponent={
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
import { defineTool } from '@tools/defineTool';
|
||||
import { lazy } from 'react';
|
||||
|
||||
export const tool = defineTool('json', {
|
||||
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'
|
||||
}
|
||||
});
|
||||
import { defineTool } from '@tools/defineTool';
|
||||
import { lazy } from 'react';
|
||||
|
||||
export const tool = defineTool('json', {
|
||||
path: 'validateJson',
|
||||
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 @@ export const tool = defineTool('list', {
|
||||
i18n: {
|
||||
name: 'list:duplicate.title',
|
||||
description: 'list:duplicate.description',
|
||||
shortDescription: 'list:duplicate.shortDescription'
|
||||
shortDescription: 'list:duplicate.shortDescription',
|
||||
userTypes: ['generalUsers', 'developers']
|
||||
}
|
||||
});
|
||||
|
||||
@@ -11,6 +11,7 @@ export const tool = defineTool('list', {
|
||||
i18n: {
|
||||
name: 'list:findMostPopular.title',
|
||||
description: 'list:findMostPopular.description',
|
||||
shortDescription: 'list:findMostPopular.shortDescription'
|
||||
shortDescription: 'list:findMostPopular.shortDescription',
|
||||
userTypes: ['generalUsers', 'developers']
|
||||
}
|
||||
});
|
||||
|
||||
@@ -10,6 +10,7 @@ export const tool = defineTool('list', {
|
||||
i18n: {
|
||||
name: 'list:findUnique.title',
|
||||
description: 'list:findUnique.description',
|
||||
shortDescription: 'list:findUnique.shortDescription'
|
||||
shortDescription: 'list:findUnique.shortDescription',
|
||||
userTypes: ['generalUsers', 'developers']
|
||||
}
|
||||
});
|
||||
|
||||
@@ -10,6 +10,7 @@ export const tool = defineTool('list', {
|
||||
i18n: {
|
||||
name: 'list:group.title',
|
||||
description: 'list:group.description',
|
||||
shortDescription: 'list:group.shortDescription'
|
||||
shortDescription: 'list:group.shortDescription',
|
||||
userTypes: ['generalUsers', 'developers']
|
||||
}
|
||||
});
|
||||
|
||||
@@ -5,12 +5,12 @@ import { lazy } from 'react';
|
||||
export const tool = defineTool('list', {
|
||||
path: 'reverse',
|
||||
icon: 'proicons:reverse',
|
||||
|
||||
keywords: ['reverse'],
|
||||
component: lazy(() => import('./index')),
|
||||
i18n: {
|
||||
name: 'list:reverse.title',
|
||||
description: 'list:reverse.description',
|
||||
shortDescription: 'list:reverse.shortDescription'
|
||||
}
|
||||
shortDescription: 'list:reverse.shortDescription',
|
||||
userTypes: ['generalUsers']
|
||||
},
|
||||
component: lazy(() => import('./index'))
|
||||
});
|
||||
|
||||
@@ -11,6 +11,7 @@ export const tool = defineTool('list', {
|
||||
i18n: {
|
||||
name: 'list:rotate.title',
|
||||
description: 'list:rotate.description',
|
||||
shortDescription: 'list:rotate.shortDescription'
|
||||
shortDescription: 'list:rotate.shortDescription',
|
||||
userTypes: ['generalUsers']
|
||||
}
|
||||
});
|
||||
|
||||
@@ -11,6 +11,7 @@ export const tool = defineTool('list', {
|
||||
i18n: {
|
||||
name: 'list:shuffle.title',
|
||||
description: 'list:shuffle.description',
|
||||
shortDescription: 'list:shuffle.shortDescription'
|
||||
shortDescription: 'list:shuffle.shortDescription',
|
||||
userTypes: ['generalUsers']
|
||||
}
|
||||
});
|
||||
|
||||
@@ -11,6 +11,7 @@ export const tool = defineTool('list', {
|
||||
i18n: {
|
||||
name: 'list:sort.title',
|
||||
description: 'list:sort.description',
|
||||
shortDescription: 'list:sort.shortDescription'
|
||||
shortDescription: 'list:sort.shortDescription',
|
||||
userTypes: ['generalUsers', 'developers']
|
||||
}
|
||||
});
|
||||
|
||||
@@ -10,6 +10,7 @@ export const tool = defineTool('list', {
|
||||
i18n: {
|
||||
name: 'list:truncate.title',
|
||||
description: 'list:truncate.description',
|
||||
shortDescription: 'list:truncate.shortDescription'
|
||||
shortDescription: 'list:truncate.shortDescription',
|
||||
userTypes: ['generalUsers']
|
||||
}
|
||||
});
|
||||
|
||||
@@ -11,6 +11,7 @@ export const tool = defineTool('list', {
|
||||
i18n: {
|
||||
name: 'list:unwrap.title',
|
||||
description: 'list:unwrap.description',
|
||||
shortDescription: 'list:unwrap.shortDescription'
|
||||
shortDescription: 'list:unwrap.shortDescription',
|
||||
userTypes: ['generalUsers', 'developers']
|
||||
}
|
||||
});
|
||||
|
||||
@@ -11,6 +11,7 @@ export const tool = defineTool('list', {
|
||||
i18n: {
|
||||
name: 'list:wrap.title',
|
||||
description: 'list:wrap.description',
|
||||
shortDescription: 'list:wrap.shortDescription'
|
||||
shortDescription: 'list:wrap.shortDescription',
|
||||
userTypes: ['generalUsers', 'developers']
|
||||
}
|
||||
});
|
||||
|
||||
@@ -10,6 +10,7 @@ export const tool = defineTool('number', {
|
||||
i18n: {
|
||||
name: 'number:arithmeticSequence.title',
|
||||
description: 'number:arithmeticSequence.description',
|
||||
shortDescription: 'number:arithmeticSequence.shortDescription'
|
||||
shortDescription: 'number:arithmeticSequence.shortDescription',
|
||||
userTypes: ['generalUsers']
|
||||
}
|
||||
});
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import { tool as numberRandomPortGenerator } from './random-port-generator/meta';
|
||||
import { tool as numberRandomNumberGenerator } from './random-number-generator/meta';
|
||||
import { tool as numberSum } from './sum/meta';
|
||||
import { tool as numberGenerate } from './generate/meta';
|
||||
import { tool as numberArithmeticSequence } from './arithmetic-sequence/meta';
|
||||
@@ -6,5 +8,7 @@ export const numberTools = [
|
||||
numberSum,
|
||||
numberGenerate,
|
||||
numberArithmeticSequence,
|
||||
numberRandomPortGenerator,
|
||||
numberRandomNumberGenerator,
|
||||
...genericCalcTools
|
||||
];
|
||||
|
||||
200
src/pages/tools/number/random-number-generator/index.tsx
Normal file
200
src/pages/tools/number/random-number-generator/index.tsx
Normal file
@@ -0,0 +1,200 @@
|
||||
import { Alert, Box } from '@mui/material';
|
||||
import { useState } from 'react';
|
||||
import ToolContent from '@components/ToolContent';
|
||||
import { ToolComponentProps } from '@tools/defineTool';
|
||||
import ToolTextResult from '@components/result/ToolTextResult';
|
||||
import { GetGroupsType } from '@components/options/ToolOptions';
|
||||
import { formatNumbers, generateRandomNumbers, validateInput } from './service';
|
||||
import { InitialValuesType, RandomNumberResult } from './types';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import TextFieldWithDesc from '@components/options/TextFieldWithDesc';
|
||||
import CheckboxWithDesc from '@components/options/CheckboxWithDesc';
|
||||
|
||||
const initialValues: InitialValuesType = {
|
||||
minValue: 1,
|
||||
maxValue: 100,
|
||||
count: 10,
|
||||
allowDecimals: false,
|
||||
allowDuplicates: true,
|
||||
sortResults: false,
|
||||
separator: ', '
|
||||
};
|
||||
|
||||
export default function RandomNumberGenerator({
|
||||
title,
|
||||
longDescription
|
||||
}: ToolComponentProps) {
|
||||
const { t } = useTranslation('number');
|
||||
const [result, setResult] = useState<RandomNumberResult | null>(null);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [formattedResult, setFormattedResult] = useState<string>('');
|
||||
|
||||
const compute = (values: InitialValuesType) => {
|
||||
try {
|
||||
setError(null);
|
||||
setResult(null);
|
||||
setFormattedResult('');
|
||||
|
||||
// Validate input
|
||||
const validationError = validateInput(values);
|
||||
if (validationError) {
|
||||
setError(validationError);
|
||||
return;
|
||||
}
|
||||
|
||||
// Generate random numbers
|
||||
const randomResult = generateRandomNumbers(values);
|
||||
setResult(randomResult);
|
||||
|
||||
// Format for display
|
||||
const formatted = formatNumbers(
|
||||
randomResult.numbers,
|
||||
values.separator,
|
||||
values.allowDecimals
|
||||
);
|
||||
setFormattedResult(formatted);
|
||||
} catch (err) {
|
||||
console.error('Random number generation failed:', err);
|
||||
setError(t('randomNumberGenerator.error.generationFailed'));
|
||||
}
|
||||
};
|
||||
|
||||
const getGroups: GetGroupsType<InitialValuesType> | null = ({
|
||||
values,
|
||||
updateField
|
||||
}) => [
|
||||
{
|
||||
title: t('randomNumberGenerator.options.range.title'),
|
||||
component: (
|
||||
<Box>
|
||||
<TextFieldWithDesc
|
||||
value={values.minValue.toString()}
|
||||
onOwnChange={(value) =>
|
||||
updateField('minValue', parseInt(value) || 1)
|
||||
}
|
||||
description={t(
|
||||
'randomNumberGenerator.options.range.minDescription'
|
||||
)}
|
||||
inputProps={{
|
||||
type: 'number',
|
||||
'data-testid': 'min-value-input'
|
||||
}}
|
||||
/>
|
||||
<TextFieldWithDesc
|
||||
value={values.maxValue.toString()}
|
||||
onOwnChange={(value) =>
|
||||
updateField('maxValue', parseInt(value) || 100)
|
||||
}
|
||||
description={t(
|
||||
'randomNumberGenerator.options.range.maxDescription'
|
||||
)}
|
||||
inputProps={{
|
||||
type: 'number',
|
||||
'data-testid': 'max-value-input'
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
)
|
||||
},
|
||||
{
|
||||
title: t('randomNumberGenerator.options.generation.title'),
|
||||
component: (
|
||||
<Box>
|
||||
<TextFieldWithDesc
|
||||
value={values.count.toString()}
|
||||
onOwnChange={(value) => updateField('count', parseInt(value) || 10)}
|
||||
description={t(
|
||||
'randomNumberGenerator.options.generation.countDescription'
|
||||
)}
|
||||
inputProps={{
|
||||
type: 'number',
|
||||
min: 1,
|
||||
max: 10000,
|
||||
'data-testid': 'count-input'
|
||||
}}
|
||||
/>
|
||||
|
||||
<CheckboxWithDesc
|
||||
title={t(
|
||||
'randomNumberGenerator.options.generation.allowDecimals.title'
|
||||
)}
|
||||
checked={values.allowDecimals}
|
||||
onChange={(value) => updateField('allowDecimals', value)}
|
||||
description={t(
|
||||
'randomNumberGenerator.options.generation.allowDecimals.description'
|
||||
)}
|
||||
/>
|
||||
|
||||
<CheckboxWithDesc
|
||||
title={t(
|
||||
'randomNumberGenerator.options.generation.allowDuplicates.title'
|
||||
)}
|
||||
checked={values.allowDuplicates}
|
||||
onChange={(value) => updateField('allowDuplicates', value)}
|
||||
description={t(
|
||||
'randomNumberGenerator.options.generation.allowDuplicates.description'
|
||||
)}
|
||||
/>
|
||||
|
||||
<CheckboxWithDesc
|
||||
title={t(
|
||||
'randomNumberGenerator.options.generation.sortResults.title'
|
||||
)}
|
||||
checked={values.sortResults}
|
||||
onChange={(value) => updateField('sortResults', value)}
|
||||
description={t(
|
||||
'randomNumberGenerator.options.generation.sortResults.description'
|
||||
)}
|
||||
/>
|
||||
</Box>
|
||||
)
|
||||
},
|
||||
{
|
||||
title: t('randomNumberGenerator.options.output.title'),
|
||||
component: (
|
||||
<Box>
|
||||
<TextFieldWithDesc
|
||||
value={values.separator}
|
||||
onOwnChange={(value) => updateField('separator', value)}
|
||||
description={t(
|
||||
'randomNumberGenerator.options.output.separatorDescription'
|
||||
)}
|
||||
inputProps={{
|
||||
'data-testid': 'separator-input'
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
];
|
||||
|
||||
return (
|
||||
<ToolContent
|
||||
title={title}
|
||||
initialValues={initialValues}
|
||||
compute={compute}
|
||||
getGroups={getGroups}
|
||||
resultComponent={
|
||||
<Box>
|
||||
{error && (
|
||||
<Alert severity="error" sx={{ mb: 2 }}>
|
||||
{error}
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
{result && (
|
||||
<ToolTextResult
|
||||
title={t('randomNumberGenerator.result.title')}
|
||||
value={formattedResult}
|
||||
/>
|
||||
)}
|
||||
</Box>
|
||||
}
|
||||
toolInfo={{
|
||||
title: t('randomNumberGenerator.info.title'),
|
||||
description:
|
||||
longDescription || t('randomNumberGenerator.info.description')
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
26
src/pages/tools/number/random-number-generator/meta.ts
Normal file
26
src/pages/tools/number/random-number-generator/meta.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
import { defineTool } from '@tools/defineTool';
|
||||
import { lazy } from 'react';
|
||||
|
||||
export const tool = defineTool('number', {
|
||||
i18n: {
|
||||
name: 'number:randomNumberGenerator.title',
|
||||
description: 'number:randomNumberGenerator.description',
|
||||
shortDescription: 'number:randomNumberGenerator.shortDescription',
|
||||
longDescription: 'number:randomNumberGenerator.longDescription',
|
||||
userTypes: ['generalUsers']
|
||||
},
|
||||
path: 'random-number-generator',
|
||||
icon: 'mdi:dice-multiple',
|
||||
keywords: [
|
||||
'random',
|
||||
'number',
|
||||
'generator',
|
||||
'range',
|
||||
'min',
|
||||
'max',
|
||||
'integer',
|
||||
'decimal',
|
||||
'float'
|
||||
],
|
||||
component: lazy(() => import('./index'))
|
||||
});
|
||||
@@ -0,0 +1,248 @@
|
||||
import { expect, describe, it } from 'vitest';
|
||||
import { generateRandomNumbers, validateInput, formatNumbers } from './service';
|
||||
import { InitialValuesType } from './types';
|
||||
|
||||
describe('Random Number Generator Service', () => {
|
||||
describe('generateRandomNumbers', () => {
|
||||
it('should generate random numbers within the specified range', () => {
|
||||
const options: InitialValuesType = {
|
||||
minValue: 1,
|
||||
maxValue: 10,
|
||||
count: 5,
|
||||
allowDecimals: false,
|
||||
allowDuplicates: true,
|
||||
sortResults: false,
|
||||
separator: ', '
|
||||
};
|
||||
|
||||
const result = generateRandomNumbers(options);
|
||||
|
||||
expect(result.numbers).toHaveLength(5);
|
||||
expect(result.min).toBe(1);
|
||||
expect(result.max).toBe(10);
|
||||
expect(result.count).toBe(5);
|
||||
|
||||
// Check that all numbers are within range
|
||||
result.numbers.forEach((num) => {
|
||||
expect(num).toBeGreaterThanOrEqual(1);
|
||||
expect(num).toBeLessThanOrEqual(10);
|
||||
expect(Number.isInteger(num)).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
it('should generate decimal numbers when allowDecimals is true', () => {
|
||||
const options: InitialValuesType = {
|
||||
minValue: 0,
|
||||
maxValue: 1,
|
||||
count: 3,
|
||||
allowDecimals: true,
|
||||
allowDuplicates: true,
|
||||
sortResults: false,
|
||||
separator: ', '
|
||||
};
|
||||
|
||||
const result = generateRandomNumbers(options);
|
||||
|
||||
expect(result.numbers).toHaveLength(3);
|
||||
|
||||
// Check that numbers are within range and can be decimals
|
||||
result.numbers.forEach((num) => {
|
||||
expect(num).toBeGreaterThanOrEqual(0);
|
||||
expect(num).toBeLessThanOrEqual(1);
|
||||
});
|
||||
});
|
||||
|
||||
it('should generate unique numbers when allowDuplicates is false', () => {
|
||||
const options: InitialValuesType = {
|
||||
minValue: 1,
|
||||
maxValue: 5,
|
||||
count: 3,
|
||||
allowDecimals: false,
|
||||
allowDuplicates: false,
|
||||
sortResults: false,
|
||||
separator: ', '
|
||||
};
|
||||
|
||||
const result = generateRandomNumbers(options);
|
||||
|
||||
expect(result.numbers).toHaveLength(3);
|
||||
|
||||
// Check for uniqueness
|
||||
const uniqueNumbers = new Set(result.numbers);
|
||||
expect(uniqueNumbers.size).toBe(3);
|
||||
});
|
||||
|
||||
it('should sort results when sortResults is true', () => {
|
||||
const options: InitialValuesType = {
|
||||
minValue: 1,
|
||||
maxValue: 10,
|
||||
count: 5,
|
||||
allowDecimals: false,
|
||||
allowDuplicates: true,
|
||||
sortResults: true,
|
||||
separator: ', '
|
||||
};
|
||||
|
||||
const result = generateRandomNumbers(options);
|
||||
|
||||
expect(result.numbers).toHaveLength(5);
|
||||
expect(result.isSorted).toBe(true);
|
||||
|
||||
// Check that numbers are sorted
|
||||
for (let i = 1; i < result.numbers.length; i++) {
|
||||
expect(result.numbers[i]).toBeGreaterThanOrEqual(result.numbers[i - 1]);
|
||||
}
|
||||
});
|
||||
|
||||
it('should throw error when minValue >= maxValue', () => {
|
||||
const options: InitialValuesType = {
|
||||
minValue: 10,
|
||||
maxValue: 5,
|
||||
count: 5,
|
||||
allowDecimals: false,
|
||||
allowDuplicates: true,
|
||||
sortResults: false,
|
||||
separator: ', '
|
||||
};
|
||||
|
||||
expect(() => generateRandomNumbers(options)).toThrow(
|
||||
'Minimum value must be less than maximum value'
|
||||
);
|
||||
});
|
||||
|
||||
it('should throw error when count <= 0', () => {
|
||||
const options: InitialValuesType = {
|
||||
minValue: 1,
|
||||
maxValue: 10,
|
||||
count: 0,
|
||||
allowDecimals: false,
|
||||
allowDuplicates: true,
|
||||
sortResults: false,
|
||||
separator: ', '
|
||||
};
|
||||
|
||||
expect(() => generateRandomNumbers(options)).toThrow(
|
||||
'Count must be greater than 0'
|
||||
);
|
||||
});
|
||||
|
||||
it('should throw error when unique count exceeds available range', () => {
|
||||
const options: InitialValuesType = {
|
||||
minValue: 1,
|
||||
maxValue: 5,
|
||||
count: 10,
|
||||
allowDecimals: false,
|
||||
allowDuplicates: false,
|
||||
sortResults: false,
|
||||
separator: ', '
|
||||
};
|
||||
|
||||
expect(() => generateRandomNumbers(options)).toThrow(
|
||||
'Cannot generate unique numbers: count exceeds available range'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('validateInput', () => {
|
||||
it('should return null for valid input', () => {
|
||||
const options: InitialValuesType = {
|
||||
minValue: 1,
|
||||
maxValue: 10,
|
||||
count: 5,
|
||||
allowDecimals: false,
|
||||
allowDuplicates: true,
|
||||
sortResults: false,
|
||||
separator: ', '
|
||||
};
|
||||
|
||||
const result = validateInput(options);
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
|
||||
it('should return error when minValue >= maxValue', () => {
|
||||
const options: InitialValuesType = {
|
||||
minValue: 10,
|
||||
maxValue: 5,
|
||||
count: 5,
|
||||
allowDecimals: false,
|
||||
allowDuplicates: true,
|
||||
sortResults: false,
|
||||
separator: ', '
|
||||
};
|
||||
|
||||
const result = validateInput(options);
|
||||
expect(result).toBe('Minimum value must be less than maximum value');
|
||||
});
|
||||
|
||||
it('should return error when count <= 0', () => {
|
||||
const options: InitialValuesType = {
|
||||
minValue: 1,
|
||||
maxValue: 10,
|
||||
count: 0,
|
||||
allowDecimals: false,
|
||||
allowDuplicates: true,
|
||||
sortResults: false,
|
||||
separator: ', '
|
||||
};
|
||||
|
||||
const result = validateInput(options);
|
||||
expect(result).toBe('Count must be greater than 0');
|
||||
});
|
||||
|
||||
it('should return error when count > 10000', () => {
|
||||
const options: InitialValuesType = {
|
||||
minValue: 1,
|
||||
maxValue: 10,
|
||||
count: 10001,
|
||||
allowDecimals: false,
|
||||
allowDuplicates: true,
|
||||
sortResults: false,
|
||||
separator: ', '
|
||||
};
|
||||
|
||||
const result = validateInput(options);
|
||||
expect(result).toBe('Count cannot exceed 10,000');
|
||||
});
|
||||
|
||||
it('should return error when range > 1000000', () => {
|
||||
const options: InitialValuesType = {
|
||||
minValue: 1,
|
||||
maxValue: 1000002,
|
||||
count: 5,
|
||||
allowDecimals: false,
|
||||
allowDuplicates: true,
|
||||
sortResults: false,
|
||||
separator: ', '
|
||||
};
|
||||
|
||||
const result = validateInput(options);
|
||||
expect(result).toBe('Range cannot exceed 1,000,000');
|
||||
});
|
||||
});
|
||||
|
||||
describe('formatNumbers', () => {
|
||||
it('should format integers correctly', () => {
|
||||
const numbers = [1, 2, 3, 4, 5];
|
||||
const result = formatNumbers(numbers, ', ', false);
|
||||
expect(result).toBe('1, 2, 3, 4, 5');
|
||||
});
|
||||
|
||||
it('should format decimals correctly', () => {
|
||||
const numbers = [1.5, 2.7, 3.2];
|
||||
const result = formatNumbers(numbers, ' | ', true);
|
||||
expect(result).toBe('1.50 | 2.70 | 3.20');
|
||||
});
|
||||
|
||||
it('should handle custom separators', () => {
|
||||
const numbers = [1, 2, 3];
|
||||
const result = formatNumbers(numbers, ' -> ', false);
|
||||
expect(result).toBe('1 -> 2 -> 3');
|
||||
});
|
||||
|
||||
it('should handle empty array', () => {
|
||||
const numbers: number[] = [];
|
||||
const result = formatNumbers(numbers, ', ', false);
|
||||
expect(result).toBe('');
|
||||
});
|
||||
});
|
||||
});
|
||||
157
src/pages/tools/number/random-number-generator/service.ts
Normal file
157
src/pages/tools/number/random-number-generator/service.ts
Normal file
@@ -0,0 +1,157 @@
|
||||
import { InitialValuesType, RandomNumberResult } from './types';
|
||||
|
||||
/**
|
||||
* Generate random numbers within a specified range
|
||||
*/
|
||||
export function generateRandomNumbers(
|
||||
options: InitialValuesType
|
||||
): RandomNumberResult {
|
||||
const {
|
||||
minValue,
|
||||
maxValue,
|
||||
count,
|
||||
allowDecimals,
|
||||
allowDuplicates,
|
||||
sortResults
|
||||
} = options;
|
||||
|
||||
if (minValue >= maxValue) {
|
||||
throw new Error('Minimum value must be less than maximum value');
|
||||
}
|
||||
|
||||
if (count <= 0) {
|
||||
throw new Error('Count must be greater than 0');
|
||||
}
|
||||
|
||||
if (!allowDuplicates && count > maxValue - minValue + 1) {
|
||||
throw new Error(
|
||||
'Cannot generate unique numbers: count exceeds available range'
|
||||
);
|
||||
}
|
||||
|
||||
const numbers: number[] = [];
|
||||
|
||||
if (allowDuplicates) {
|
||||
// Generate random numbers with possible duplicates
|
||||
for (let i = 0; i < count; i++) {
|
||||
const randomNumber = generateRandomNumber(
|
||||
minValue,
|
||||
maxValue,
|
||||
allowDecimals
|
||||
);
|
||||
numbers.push(randomNumber);
|
||||
}
|
||||
} else {
|
||||
// Generate unique random numbers
|
||||
const availableNumbers = new Set<number>();
|
||||
|
||||
// Create a pool of available numbers
|
||||
for (let i = minValue; i <= maxValue; i++) {
|
||||
if (allowDecimals) {
|
||||
// For decimals, we need to generate more granular values
|
||||
for (let j = 0; j < 100; j++) {
|
||||
availableNumbers.add(i + j / 100);
|
||||
}
|
||||
} else {
|
||||
availableNumbers.add(i);
|
||||
}
|
||||
}
|
||||
|
||||
const availableArray = Array.from(availableNumbers);
|
||||
|
||||
// Shuffle the available numbers
|
||||
for (let i = availableArray.length - 1; i > 0; i--) {
|
||||
const j = Math.floor(Math.random() * (i + 1));
|
||||
[availableArray[i], availableArray[j]] = [
|
||||
availableArray[j],
|
||||
availableArray[i]
|
||||
];
|
||||
}
|
||||
|
||||
// Take the first 'count' numbers
|
||||
for (let i = 0; i < Math.min(count, availableArray.length); i++) {
|
||||
numbers.push(availableArray[i]);
|
||||
}
|
||||
}
|
||||
|
||||
// Sort if requested
|
||||
if (sortResults) {
|
||||
numbers.sort((a, b) => a - b);
|
||||
}
|
||||
|
||||
return {
|
||||
numbers,
|
||||
min: minValue,
|
||||
max: maxValue,
|
||||
count,
|
||||
hasDuplicates: !allowDuplicates && hasDuplicatesInArray(numbers),
|
||||
isSorted: sortResults
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a single random number within the specified range
|
||||
*/
|
||||
function generateRandomNumber(
|
||||
min: number,
|
||||
max: number,
|
||||
allowDecimals: boolean
|
||||
): number {
|
||||
if (allowDecimals) {
|
||||
return Math.random() * (max - min) + min;
|
||||
} else {
|
||||
return Math.floor(Math.random() * (max - min + 1)) + min;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if an array has duplicate values
|
||||
*/
|
||||
function hasDuplicatesInArray(arr: number[]): boolean {
|
||||
const seen = new Set<number>();
|
||||
for (const num of arr) {
|
||||
if (seen.has(num)) {
|
||||
return true;
|
||||
}
|
||||
seen.add(num);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Format numbers for display
|
||||
*/
|
||||
export function formatNumbers(
|
||||
numbers: number[],
|
||||
separator: string,
|
||||
allowDecimals: boolean
|
||||
): string {
|
||||
return numbers
|
||||
.map((num) => (allowDecimals ? num.toFixed(2) : Math.round(num).toString()))
|
||||
.join(separator);
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate input parameters
|
||||
*/
|
||||
export function validateInput(options: InitialValuesType): string | null {
|
||||
const { minValue, maxValue, count } = options;
|
||||
|
||||
if (minValue >= maxValue) {
|
||||
return 'Minimum value must be less than maximum value';
|
||||
}
|
||||
|
||||
if (count <= 0) {
|
||||
return 'Count must be greater than 0';
|
||||
}
|
||||
|
||||
if (count > 10000) {
|
||||
return 'Count cannot exceed 10,000';
|
||||
}
|
||||
|
||||
if (maxValue - minValue > 1000000) {
|
||||
return 'Range cannot exceed 1,000,000';
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
18
src/pages/tools/number/random-number-generator/types.ts
Normal file
18
src/pages/tools/number/random-number-generator/types.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
export type InitialValuesType = {
|
||||
minValue: number;
|
||||
maxValue: number;
|
||||
count: number;
|
||||
allowDecimals: boolean;
|
||||
allowDuplicates: boolean;
|
||||
sortResults: boolean;
|
||||
separator: string;
|
||||
};
|
||||
|
||||
export type RandomNumberResult = {
|
||||
numbers: number[];
|
||||
min: number;
|
||||
max: number;
|
||||
count: number;
|
||||
hasDuplicates: boolean;
|
||||
isSorted: boolean;
|
||||
};
|
||||
233
src/pages/tools/number/random-port-generator/index.tsx
Normal file
233
src/pages/tools/number/random-port-generator/index.tsx
Normal file
@@ -0,0 +1,233 @@
|
||||
import { Alert, Box } from '@mui/material';
|
||||
import { useState } from 'react';
|
||||
import ToolContent from '@components/ToolContent';
|
||||
import { ToolComponentProps } from '@tools/defineTool';
|
||||
import ToolTextResult from '@components/result/ToolTextResult';
|
||||
import { GetGroupsType } from '@components/options/ToolOptions';
|
||||
import {
|
||||
formatPorts,
|
||||
generateRandomPorts,
|
||||
getPortRangeInfo,
|
||||
validateInput
|
||||
} from './service';
|
||||
import { InitialValuesType, RandomPortResult } from './types';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import TextFieldWithDesc from '@components/options/TextFieldWithDesc';
|
||||
import CheckboxWithDesc from '@components/options/CheckboxWithDesc';
|
||||
import SimpleRadio from '@components/options/SimpleRadio';
|
||||
|
||||
const initialValues: InitialValuesType = {
|
||||
portRange: 'registered',
|
||||
minPort: 1024,
|
||||
maxPort: 49151,
|
||||
count: 5,
|
||||
allowDuplicates: false,
|
||||
sortResults: false,
|
||||
separator: ', '
|
||||
};
|
||||
|
||||
export default function RandomPortGenerator({
|
||||
title,
|
||||
longDescription
|
||||
}: ToolComponentProps) {
|
||||
const { t } = useTranslation('number');
|
||||
const [result, setResult] = useState<RandomPortResult | null>(null);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [formattedResult, setFormattedResult] = useState<string>('');
|
||||
|
||||
const compute = (values: InitialValuesType) => {
|
||||
try {
|
||||
setError(null);
|
||||
setResult(null);
|
||||
setFormattedResult('');
|
||||
|
||||
// Validate input
|
||||
const validationError = validateInput(values);
|
||||
if (validationError) {
|
||||
setError(validationError);
|
||||
return;
|
||||
}
|
||||
|
||||
// Generate random ports
|
||||
const randomResult = generateRandomPorts(values);
|
||||
setResult(randomResult);
|
||||
|
||||
// Format for display
|
||||
const formatted = formatPorts(randomResult.ports, values.separator);
|
||||
setFormattedResult(formatted);
|
||||
} catch (err) {
|
||||
console.error('Random port generation failed:', err);
|
||||
setError(t('randomPortGenerator.error.generationFailed'));
|
||||
}
|
||||
};
|
||||
const portOptions = [
|
||||
{
|
||||
value: 'well-known',
|
||||
label: t('randomPortGenerator.options.range.wellKnown')
|
||||
},
|
||||
{
|
||||
value: 'registered',
|
||||
label: t('randomPortGenerator.options.range.registered')
|
||||
},
|
||||
{
|
||||
value: 'dynamic',
|
||||
label: t('randomPortGenerator.options.range.dynamic')
|
||||
},
|
||||
{
|
||||
value: 'custom',
|
||||
label: t('randomPortGenerator.options.range.custom')
|
||||
}
|
||||
] as const;
|
||||
const getGroups: GetGroupsType<InitialValuesType> | null = ({
|
||||
values,
|
||||
updateField
|
||||
}) => [
|
||||
{
|
||||
title: t('randomPortGenerator.options.range.title'),
|
||||
component: (
|
||||
<Box>
|
||||
{portOptions.map((option) => (
|
||||
<SimpleRadio
|
||||
key={option.value}
|
||||
title={option.label}
|
||||
checked={values.portRange === option.value}
|
||||
onClick={() => updateField('portRange', option.value)}
|
||||
/>
|
||||
))}
|
||||
|
||||
{values.portRange === 'custom' && (
|
||||
<Box sx={{ mt: 2 }}>
|
||||
<TextFieldWithDesc
|
||||
value={values.minPort.toString()}
|
||||
onOwnChange={(value) =>
|
||||
updateField('minPort', parseInt(value) || 1024)
|
||||
}
|
||||
description={t(
|
||||
'randomPortGenerator.options.range.minPortDescription'
|
||||
)}
|
||||
inputProps={{
|
||||
type: 'number',
|
||||
min: 1,
|
||||
max: 65535,
|
||||
'data-testid': 'min-port-input'
|
||||
}}
|
||||
/>
|
||||
<TextFieldWithDesc
|
||||
value={values.maxPort.toString()}
|
||||
onOwnChange={(value) =>
|
||||
updateField('maxPort', parseInt(value) || 49151)
|
||||
}
|
||||
description={t(
|
||||
'randomPortGenerator.options.range.maxPortDescription'
|
||||
)}
|
||||
inputProps={{
|
||||
type: 'number',
|
||||
min: 1,
|
||||
max: 65535,
|
||||
'data-testid': 'max-port-input'
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
)}
|
||||
|
||||
<Box
|
||||
sx={{ mt: 2, p: 2, bgcolor: 'background.paper', borderRadius: 1 }}
|
||||
>
|
||||
<strong>{getPortRangeInfo(values.portRange).name}</strong>
|
||||
<br />
|
||||
{getPortRangeInfo(values.portRange).description}
|
||||
</Box>
|
||||
</Box>
|
||||
)
|
||||
},
|
||||
{
|
||||
title: t('randomPortGenerator.options.generation.title'),
|
||||
component: (
|
||||
<Box>
|
||||
<TextFieldWithDesc
|
||||
value={values.count.toString()}
|
||||
onOwnChange={(value) => updateField('count', parseInt(value) || 5)}
|
||||
description={t(
|
||||
'randomPortGenerator.options.generation.countDescription'
|
||||
)}
|
||||
inputProps={{
|
||||
type: 'number',
|
||||
min: 1,
|
||||
max: 1000,
|
||||
'data-testid': 'count-input'
|
||||
}}
|
||||
/>
|
||||
|
||||
<CheckboxWithDesc
|
||||
title={t(
|
||||
'randomPortGenerator.options.generation.allowDuplicates.title'
|
||||
)}
|
||||
checked={values.allowDuplicates}
|
||||
onChange={(value) => updateField('allowDuplicates', value)}
|
||||
description={t(
|
||||
'randomPortGenerator.options.generation.allowDuplicates.description'
|
||||
)}
|
||||
/>
|
||||
|
||||
<CheckboxWithDesc
|
||||
title={t(
|
||||
'randomPortGenerator.options.generation.sortResults.title'
|
||||
)}
|
||||
checked={values.sortResults}
|
||||
onChange={(value) => updateField('sortResults', value)}
|
||||
description={t(
|
||||
'randomPortGenerator.options.generation.sortResults.description'
|
||||
)}
|
||||
/>
|
||||
</Box>
|
||||
)
|
||||
},
|
||||
{
|
||||
title: t('randomPortGenerator.options.output.title'),
|
||||
component: (
|
||||
<Box>
|
||||
<TextFieldWithDesc
|
||||
value={values.separator}
|
||||
onOwnChange={(value) => updateField('separator', value)}
|
||||
description={t(
|
||||
'randomPortGenerator.options.output.separatorDescription'
|
||||
)}
|
||||
inputProps={{
|
||||
'data-testid': 'separator-input'
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
];
|
||||
|
||||
return (
|
||||
<ToolContent
|
||||
title={title}
|
||||
initialValues={initialValues}
|
||||
compute={compute}
|
||||
getGroups={getGroups}
|
||||
resultComponent={
|
||||
<Box>
|
||||
{error && (
|
||||
<Alert severity="error" sx={{ mb: 2 }}>
|
||||
{error}
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
{result && (
|
||||
<ToolTextResult
|
||||
title={t('randomPortGenerator.result.title')}
|
||||
value={formattedResult}
|
||||
/>
|
||||
)}
|
||||
</Box>
|
||||
}
|
||||
toolInfo={{
|
||||
title: t('randomPortGenerator.info.title'),
|
||||
description:
|
||||
longDescription || t('randomPortGenerator.info.description')
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
26
src/pages/tools/number/random-port-generator/meta.ts
Normal file
26
src/pages/tools/number/random-port-generator/meta.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
import { defineTool } from '@tools/defineTool';
|
||||
import { lazy } from 'react';
|
||||
|
||||
export const tool = defineTool('number', {
|
||||
i18n: {
|
||||
name: 'number:randomPortGenerator.title',
|
||||
description: 'number:randomPortGenerator.description',
|
||||
shortDescription: 'number:randomPortGenerator.shortDescription',
|
||||
longDescription: 'number:randomPortGenerator.longDescription',
|
||||
userTypes: ['developers']
|
||||
},
|
||||
path: 'random-port-generator',
|
||||
icon: 'mdi:network',
|
||||
keywords: [
|
||||
'random',
|
||||
'port',
|
||||
'generator',
|
||||
'network',
|
||||
'tcp',
|
||||
'udp',
|
||||
'server',
|
||||
'client',
|
||||
'development'
|
||||
],
|
||||
component: lazy(() => import('./index'))
|
||||
});
|
||||
@@ -0,0 +1,315 @@
|
||||
import { expect, describe, it } from 'vitest';
|
||||
import {
|
||||
generateRandomPorts,
|
||||
validateInput,
|
||||
formatPorts,
|
||||
getPortRangeInfo,
|
||||
isCommonPort,
|
||||
getPortService,
|
||||
PORT_RANGES
|
||||
} from './service';
|
||||
import { InitialValuesType } from './types';
|
||||
|
||||
describe('Random Port Generator Service', () => {
|
||||
describe('generateRandomPorts', () => {
|
||||
it('should generate random ports within the well-known range', () => {
|
||||
const options: InitialValuesType = {
|
||||
portRange: 'well-known',
|
||||
minPort: 1,
|
||||
maxPort: 1023,
|
||||
count: 5,
|
||||
allowDuplicates: true,
|
||||
sortResults: false,
|
||||
separator: ', '
|
||||
};
|
||||
|
||||
const result = generateRandomPorts(options);
|
||||
|
||||
expect(result.ports).toHaveLength(5);
|
||||
expect(result.range.min).toBe(1);
|
||||
expect(result.range.max).toBe(1023);
|
||||
expect(result.count).toBe(5);
|
||||
|
||||
// Check that all ports are within range
|
||||
result.ports.forEach((port) => {
|
||||
expect(port).toBeGreaterThanOrEqual(1);
|
||||
expect(port).toBeLessThanOrEqual(1023);
|
||||
expect(Number.isInteger(port)).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
it('should generate random ports within the registered range', () => {
|
||||
const options: InitialValuesType = {
|
||||
portRange: 'registered',
|
||||
minPort: 1024,
|
||||
maxPort: 49151,
|
||||
count: 3,
|
||||
allowDuplicates: false,
|
||||
sortResults: false,
|
||||
separator: ', '
|
||||
};
|
||||
|
||||
const result = generateRandomPorts(options);
|
||||
|
||||
expect(result.ports).toHaveLength(3);
|
||||
expect(result.range.min).toBe(1024);
|
||||
expect(result.range.max).toBe(49151);
|
||||
|
||||
// Check for uniqueness
|
||||
const uniquePorts = new Set(result.ports);
|
||||
expect(uniquePorts.size).toBe(3);
|
||||
});
|
||||
|
||||
it('should generate random ports within custom range', () => {
|
||||
const options: InitialValuesType = {
|
||||
portRange: 'custom',
|
||||
minPort: 8000,
|
||||
maxPort: 8100,
|
||||
count: 4,
|
||||
allowDuplicates: true,
|
||||
sortResults: true,
|
||||
separator: ', '
|
||||
};
|
||||
|
||||
const result = generateRandomPorts(options);
|
||||
|
||||
expect(result.ports).toHaveLength(4);
|
||||
expect(result.range.min).toBe(8000);
|
||||
expect(result.range.max).toBe(8100);
|
||||
expect(result.isSorted).toBe(true);
|
||||
|
||||
// Check that numbers are sorted
|
||||
for (let i = 1; i < result.ports.length; i++) {
|
||||
expect(result.ports[i]).toBeGreaterThanOrEqual(result.ports[i - 1]);
|
||||
}
|
||||
});
|
||||
|
||||
it('should throw error when minPort >= maxPort', () => {
|
||||
const options: InitialValuesType = {
|
||||
portRange: 'custom',
|
||||
minPort: 1000,
|
||||
maxPort: 500,
|
||||
count: 5,
|
||||
allowDuplicates: true,
|
||||
sortResults: false,
|
||||
separator: ', '
|
||||
};
|
||||
|
||||
expect(() => generateRandomPorts(options)).toThrow(
|
||||
'Minimum port must be less than maximum port'
|
||||
);
|
||||
});
|
||||
|
||||
it('should throw error when count <= 0', () => {
|
||||
const options: InitialValuesType = {
|
||||
portRange: 'registered',
|
||||
minPort: 1024,
|
||||
maxPort: 49151,
|
||||
count: 0,
|
||||
allowDuplicates: true,
|
||||
sortResults: false,
|
||||
separator: ', '
|
||||
};
|
||||
|
||||
expect(() => generateRandomPorts(options)).toThrow(
|
||||
'Count must be greater than 0'
|
||||
);
|
||||
});
|
||||
|
||||
it('should throw error when ports are outside valid range', () => {
|
||||
const options: InitialValuesType = {
|
||||
portRange: 'custom',
|
||||
minPort: 0,
|
||||
maxPort: 70000,
|
||||
count: 5,
|
||||
allowDuplicates: true,
|
||||
sortResults: false,
|
||||
separator: ', '
|
||||
};
|
||||
|
||||
expect(() => generateRandomPorts(options)).toThrow(
|
||||
'Ports must be between 1 and 65535'
|
||||
);
|
||||
});
|
||||
|
||||
it('should throw error when unique count exceeds available range', () => {
|
||||
const options: InitialValuesType = {
|
||||
portRange: 'custom',
|
||||
minPort: 1,
|
||||
maxPort: 5,
|
||||
count: 10,
|
||||
allowDuplicates: false,
|
||||
sortResults: false,
|
||||
separator: ', '
|
||||
};
|
||||
|
||||
expect(() => generateRandomPorts(options)).toThrow(
|
||||
'Cannot generate unique ports: count exceeds available range'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('validateInput', () => {
|
||||
it('should return null for valid input', () => {
|
||||
const options: InitialValuesType = {
|
||||
portRange: 'registered',
|
||||
minPort: 1024,
|
||||
maxPort: 49151,
|
||||
count: 5,
|
||||
allowDuplicates: true,
|
||||
sortResults: false,
|
||||
separator: ', '
|
||||
};
|
||||
|
||||
const result = validateInput(options);
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
|
||||
it('should return error when count <= 0', () => {
|
||||
const options: InitialValuesType = {
|
||||
portRange: 'registered',
|
||||
minPort: 1024,
|
||||
maxPort: 49151,
|
||||
count: 0,
|
||||
allowDuplicates: true,
|
||||
sortResults: false,
|
||||
separator: ', '
|
||||
};
|
||||
|
||||
const result = validateInput(options);
|
||||
expect(result).toBe('Count must be greater than 0');
|
||||
});
|
||||
|
||||
it('should return error when count > 1000', () => {
|
||||
const options: InitialValuesType = {
|
||||
portRange: 'registered',
|
||||
minPort: 1024,
|
||||
maxPort: 49151,
|
||||
count: 1001,
|
||||
allowDuplicates: true,
|
||||
sortResults: false,
|
||||
separator: ', '
|
||||
};
|
||||
|
||||
const result = validateInput(options);
|
||||
expect(result).toBe('Count cannot exceed 1,000');
|
||||
});
|
||||
|
||||
it('should return error when custom range has invalid ports', () => {
|
||||
const options: InitialValuesType = {
|
||||
portRange: 'custom',
|
||||
minPort: 0,
|
||||
maxPort: 70000,
|
||||
count: 5,
|
||||
allowDuplicates: true,
|
||||
sortResults: false,
|
||||
separator: ', '
|
||||
};
|
||||
|
||||
const result = validateInput(options);
|
||||
expect(result).toBe('Ports must be between 1 and 65535');
|
||||
});
|
||||
|
||||
it('should return error when custom range has minPort >= maxPort', () => {
|
||||
const options: InitialValuesType = {
|
||||
portRange: 'custom',
|
||||
minPort: 1000,
|
||||
maxPort: 500,
|
||||
count: 5,
|
||||
allowDuplicates: true,
|
||||
sortResults: false,
|
||||
separator: ', '
|
||||
};
|
||||
|
||||
const result = validateInput(options);
|
||||
expect(result).toBe('Minimum port must be less than maximum port');
|
||||
});
|
||||
});
|
||||
|
||||
describe('formatPorts', () => {
|
||||
it('should format ports correctly', () => {
|
||||
const ports = [80, 443, 8080, 3000];
|
||||
const result = formatPorts(ports, ', ');
|
||||
expect(result).toBe('80, 443, 8080, 3000');
|
||||
});
|
||||
|
||||
it('should handle custom separators', () => {
|
||||
const ports = [80, 443, 8080];
|
||||
const result = formatPorts(ports, ' -> ');
|
||||
expect(result).toBe('80 -> 443 -> 8080');
|
||||
});
|
||||
|
||||
it('should handle empty array', () => {
|
||||
const ports: number[] = [];
|
||||
const result = formatPorts(ports, ', ');
|
||||
expect(result).toBe('');
|
||||
});
|
||||
});
|
||||
|
||||
describe('getPortRangeInfo', () => {
|
||||
it('should return correct port range info for well-known', () => {
|
||||
const result = getPortRangeInfo('well-known');
|
||||
expect(result.name).toBe('Well-Known Ports');
|
||||
expect(result.min).toBe(1);
|
||||
expect(result.max).toBe(1023);
|
||||
});
|
||||
|
||||
it('should return correct port range info for registered', () => {
|
||||
const result = getPortRangeInfo('registered');
|
||||
expect(result.name).toBe('Registered Ports');
|
||||
expect(result.min).toBe(1024);
|
||||
expect(result.max).toBe(49151);
|
||||
});
|
||||
|
||||
it('should return correct port range info for dynamic', () => {
|
||||
const result = getPortRangeInfo('dynamic');
|
||||
expect(result.name).toBe('Dynamic Ports');
|
||||
expect(result.min).toBe(49152);
|
||||
expect(result.max).toBe(65535);
|
||||
});
|
||||
|
||||
it('should return custom range for unknown range', () => {
|
||||
const result = getPortRangeInfo('unknown');
|
||||
expect(result.name).toBe('Custom Range');
|
||||
});
|
||||
});
|
||||
|
||||
describe('isCommonPort', () => {
|
||||
it('should identify common ports correctly', () => {
|
||||
expect(isCommonPort(80)).toBe(true);
|
||||
expect(isCommonPort(443)).toBe(true);
|
||||
expect(isCommonPort(22)).toBe(true);
|
||||
expect(isCommonPort(3306)).toBe(true);
|
||||
});
|
||||
|
||||
it('should return false for uncommon ports', () => {
|
||||
expect(isCommonPort(12345)).toBe(false);
|
||||
expect(isCommonPort(54321)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getPortService', () => {
|
||||
it('should return correct service names for common ports', () => {
|
||||
expect(getPortService(80)).toBe('HTTP');
|
||||
expect(getPortService(443)).toBe('HTTPS');
|
||||
expect(getPortService(22)).toBe('SSH');
|
||||
expect(getPortService(3306)).toBe('MySQL');
|
||||
});
|
||||
|
||||
it('should return "Unknown" for uncommon ports', () => {
|
||||
expect(getPortService(12345)).toBe('Unknown');
|
||||
expect(getPortService(54321)).toBe('Unknown');
|
||||
});
|
||||
});
|
||||
|
||||
describe('PORT_RANGES', () => {
|
||||
it('should have correct port range definitions', () => {
|
||||
expect(PORT_RANGES['well-known'].min).toBe(1);
|
||||
expect(PORT_RANGES['well-known'].max).toBe(1023);
|
||||
expect(PORT_RANGES['registered'].min).toBe(1024);
|
||||
expect(PORT_RANGES['registered'].max).toBe(49151);
|
||||
expect(PORT_RANGES['dynamic'].min).toBe(49152);
|
||||
expect(PORT_RANGES['dynamic'].max).toBe(65535);
|
||||
});
|
||||
});
|
||||
});
|
||||
214
src/pages/tools/number/random-port-generator/service.ts
Normal file
214
src/pages/tools/number/random-port-generator/service.ts
Normal file
@@ -0,0 +1,214 @@
|
||||
import { InitialValuesType, RandomPortResult, PortRange } from './types';
|
||||
|
||||
// Standard port ranges according to IANA
|
||||
export const PORT_RANGES: Record<string, PortRange> = {
|
||||
'well-known': {
|
||||
name: 'Well-Known Ports',
|
||||
min: 1,
|
||||
max: 1023,
|
||||
description:
|
||||
'System ports (1-1023) - Reserved for common services like HTTP, HTTPS, SSH, etc.'
|
||||
},
|
||||
registered: {
|
||||
name: 'Registered Ports',
|
||||
min: 1024,
|
||||
max: 49151,
|
||||
description:
|
||||
'User ports (1024-49151) - Available for applications and services'
|
||||
},
|
||||
dynamic: {
|
||||
name: 'Dynamic Ports',
|
||||
min: 49152,
|
||||
max: 65535,
|
||||
description:
|
||||
'Private ports (49152-65535) - Available for temporary or private use'
|
||||
},
|
||||
custom: {
|
||||
name: 'Custom Range',
|
||||
min: 1,
|
||||
max: 65535,
|
||||
description: 'Custom port range - Specify your own min and max values'
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Generate random network ports within a specified range
|
||||
*/
|
||||
export function generateRandomPorts(
|
||||
options: InitialValuesType
|
||||
): RandomPortResult {
|
||||
const { portRange, minPort, maxPort, count, allowDuplicates, sortResults } =
|
||||
options;
|
||||
|
||||
// Get the appropriate port range
|
||||
const range = PORT_RANGES[portRange];
|
||||
const actualMin = portRange === 'custom' ? minPort : range.min;
|
||||
const actualMax = portRange === 'custom' ? maxPort : range.max;
|
||||
|
||||
if (actualMin >= actualMax) {
|
||||
throw new Error('Minimum port must be less than maximum port');
|
||||
}
|
||||
|
||||
if (count <= 0) {
|
||||
throw new Error('Count must be greater than 0');
|
||||
}
|
||||
|
||||
if (actualMin < 1 || actualMax > 65535) {
|
||||
throw new Error('Ports must be between 1 and 65535');
|
||||
}
|
||||
|
||||
if (!allowDuplicates && count > actualMax - actualMin + 1) {
|
||||
throw new Error(
|
||||
'Cannot generate unique ports: count exceeds available range'
|
||||
);
|
||||
}
|
||||
|
||||
const ports: number[] = [];
|
||||
|
||||
if (allowDuplicates) {
|
||||
// Generate random ports with possible duplicates
|
||||
for (let i = 0; i < count; i++) {
|
||||
const randomPort = generateRandomPort(actualMin, actualMax);
|
||||
ports.push(randomPort);
|
||||
}
|
||||
} else {
|
||||
// Generate unique random ports
|
||||
const availablePorts = new Set<number>();
|
||||
|
||||
// Create a pool of available ports
|
||||
for (let i = actualMin; i <= actualMax; i++) {
|
||||
availablePorts.add(i);
|
||||
}
|
||||
|
||||
const availableArray = Array.from(availablePorts);
|
||||
|
||||
// Shuffle the available ports
|
||||
for (let i = availableArray.length - 1; i > 0; i--) {
|
||||
const j = Math.floor(Math.random() * (i + 1));
|
||||
[availableArray[i], availableArray[j]] = [
|
||||
availableArray[j],
|
||||
availableArray[i]
|
||||
];
|
||||
}
|
||||
|
||||
// Take the first 'count' ports
|
||||
for (let i = 0; i < Math.min(count, availableArray.length); i++) {
|
||||
ports.push(availableArray[i]);
|
||||
}
|
||||
}
|
||||
|
||||
// Sort if requested
|
||||
if (sortResults) {
|
||||
ports.sort((a, b) => a - b);
|
||||
}
|
||||
|
||||
return {
|
||||
ports,
|
||||
range: {
|
||||
...range,
|
||||
min: actualMin,
|
||||
max: actualMax
|
||||
},
|
||||
count,
|
||||
hasDuplicates: !allowDuplicates && hasDuplicatesInArray(ports),
|
||||
isSorted: sortResults
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a single random port within the specified range
|
||||
*/
|
||||
function generateRandomPort(min: number, max: number): number {
|
||||
return Math.floor(Math.random() * (max - min + 1)) + min;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if an array has duplicate values
|
||||
*/
|
||||
function hasDuplicatesInArray(arr: number[]): boolean {
|
||||
const seen = new Set<number>();
|
||||
for (const num of arr) {
|
||||
if (seen.has(num)) {
|
||||
return true;
|
||||
}
|
||||
seen.add(num);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Format ports for display
|
||||
*/
|
||||
export function formatPorts(ports: number[], separator: string): string {
|
||||
return ports.map((port) => port.toString()).join(separator);
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate input parameters
|
||||
*/
|
||||
export function validateInput(options: InitialValuesType): string | null {
|
||||
const { portRange, minPort, maxPort, count } = options;
|
||||
|
||||
if (count <= 0) {
|
||||
return 'Count must be greater than 0';
|
||||
}
|
||||
|
||||
if (count > 1000) {
|
||||
return 'Count cannot exceed 1,000';
|
||||
}
|
||||
|
||||
if (portRange === 'custom') {
|
||||
if (minPort >= maxPort) {
|
||||
return 'Minimum port must be less than maximum port';
|
||||
}
|
||||
|
||||
if (minPort < 1 || maxPort > 65535) {
|
||||
return 'Ports must be between 1 and 65535';
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get port range information
|
||||
*/
|
||||
export function getPortRangeInfo(portRange: string): PortRange {
|
||||
return PORT_RANGES[portRange] || PORT_RANGES['custom'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a port is commonly used
|
||||
*/
|
||||
export function isCommonPort(port: number): boolean {
|
||||
const commonPorts = [
|
||||
20, 21, 22, 23, 25, 53, 80, 110, 143, 443, 993, 995, 3306, 5432, 6379, 8080
|
||||
];
|
||||
return commonPorts.includes(port);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get port service information
|
||||
*/
|
||||
export function getPortService(port: number): string {
|
||||
const portServices: Record<number, string> = {
|
||||
20: 'FTP Data',
|
||||
21: 'FTP Control',
|
||||
22: 'SSH',
|
||||
23: 'Telnet',
|
||||
25: 'SMTP',
|
||||
53: 'DNS',
|
||||
80: 'HTTP',
|
||||
110: 'POP3',
|
||||
143: 'IMAP',
|
||||
443: 'HTTPS',
|
||||
993: 'IMAPS',
|
||||
995: 'POP3S',
|
||||
3306: 'MySQL',
|
||||
5432: 'PostgreSQL',
|
||||
6379: 'Redis',
|
||||
8080: 'HTTP Alternative'
|
||||
};
|
||||
|
||||
return portServices[port] || 'Unknown';
|
||||
}
|
||||
24
src/pages/tools/number/random-port-generator/types.ts
Normal file
24
src/pages/tools/number/random-port-generator/types.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
export type InitialValuesType = {
|
||||
portRange: 'well-known' | 'registered' | 'dynamic' | 'custom';
|
||||
minPort: number;
|
||||
maxPort: number;
|
||||
count: number;
|
||||
allowDuplicates: boolean;
|
||||
sortResults: boolean;
|
||||
separator: string;
|
||||
};
|
||||
|
||||
export type PortRange = {
|
||||
name: string;
|
||||
min: number;
|
||||
max: number;
|
||||
description: string;
|
||||
};
|
||||
|
||||
export type RandomPortResult = {
|
||||
ports: number[];
|
||||
range: PortRange;
|
||||
count: number;
|
||||
hasDuplicates: boolean;
|
||||
isSorted: boolean;
|
||||
};
|
||||
@@ -11,6 +11,7 @@ export const tool = defineTool('number', {
|
||||
i18n: {
|
||||
name: 'number:sum.title',
|
||||
description: 'number:sum.description',
|
||||
shortDescription: 'number:sum.shortDescription'
|
||||
shortDescription: 'number:sum.shortDescription',
|
||||
userTypes: ['generalUsers']
|
||||
}
|
||||
});
|
||||
|
||||
@@ -19,11 +19,11 @@ export const tool = defineTool('pdf', {
|
||||
'browser',
|
||||
'webassembly'
|
||||
],
|
||||
|
||||
component: lazy(() => import('./index')),
|
||||
i18n: {
|
||||
name: 'pdf:compressPdf.title',
|
||||
description: 'pdf:compressPdf.description',
|
||||
shortDescription: 'pdf:compressPdf.shortDescription'
|
||||
shortDescription: 'pdf:compressPdf.shortDescription',
|
||||
userTypes: ['generalUsers']
|
||||
}
|
||||
});
|
||||
|
||||
@@ -5,7 +5,8 @@ export const tool = defineTool('pdf', {
|
||||
i18n: {
|
||||
name: 'pdf:editor.title',
|
||||
description: 'pdf:editor.description',
|
||||
shortDescription: 'pdf:editor.shortDescription'
|
||||
shortDescription: 'pdf:editor.shortDescription',
|
||||
userTypes: ['generalUsers']
|
||||
},
|
||||
|
||||
path: 'editor',
|
||||
|
||||
@@ -9,6 +9,7 @@ export const meta = defineTool('pdf', {
|
||||
i18n: {
|
||||
name: 'pdf:mergePdf.title',
|
||||
description: 'pdf:mergePdf.description',
|
||||
shortDescription: 'pdf:mergePdf.shortDescription'
|
||||
shortDescription: 'pdf:mergePdf.shortDescription',
|
||||
userTypes: ['generalUsers']
|
||||
}
|
||||
});
|
||||
|
||||
@@ -9,6 +9,7 @@ export const meta = defineTool('pdf', {
|
||||
i18n: {
|
||||
name: 'pdf:pdfToEpub.title',
|
||||
description: 'pdf:pdfToEpub.description',
|
||||
shortDescription: 'pdf:pdfToEpub.shortDescription'
|
||||
shortDescription: 'pdf:pdfToEpub.shortDescription',
|
||||
userTypes: ['generalUsers']
|
||||
}
|
||||
});
|
||||
|
||||
@@ -6,13 +6,13 @@ export const tool = defineTool('pdf', {
|
||||
name: 'pdf:pdfToPng.title',
|
||||
description: 'pdf:pdfToPng.description',
|
||||
shortDescription: 'pdf:pdfToPng.shortDescription',
|
||||
longDescription: 'pdf:pdfToPng.longDescription'
|
||||
longDescription: 'pdf:pdfToPng.longDescription',
|
||||
userTypes: ['generalUsers']
|
||||
},
|
||||
|
||||
path: 'pdf-to-png',
|
||||
icon: 'mdi:image-multiple', // Iconify icon ID
|
||||
|
||||
keywords: ['pdf', 'png', 'convert', 'image', 'extract', 'pages'],
|
||||
|
||||
component: lazy(() => import('./index'))
|
||||
});
|
||||
|
||||
@@ -18,11 +18,11 @@ export const tool = defineTool('pdf', {
|
||||
'browser',
|
||||
'encryption'
|
||||
],
|
||||
|
||||
component: lazy(() => import('./index')),
|
||||
i18n: {
|
||||
name: 'pdf:protectPdf.title',
|
||||
description: 'pdf:protectPdf.description',
|
||||
shortDescription: 'pdf:protectPdf.shortDescription'
|
||||
shortDescription: 'pdf:protectPdf.shortDescription',
|
||||
userTypes: ['generalUsers']
|
||||
}
|
||||
});
|
||||
|
||||
@@ -6,13 +6,13 @@ export const tool = defineTool('pdf', {
|
||||
name: 'pdf:rotatePdf.title',
|
||||
description: 'pdf:rotatePdf.description',
|
||||
shortDescription: 'pdf:rotatePdf.shortDescription',
|
||||
longDescription: 'pdf:rotatePdf.longDescription'
|
||||
longDescription: 'pdf:rotatePdf.longDescription',
|
||||
userTypes: ['generalUsers']
|
||||
},
|
||||
|
||||
path: 'rotate-pdf',
|
||||
icon: 'carbon:rotate',
|
||||
|
||||
keywords: ['pdf', 'rotate', 'rotation', 'document', 'pages', 'orientation'],
|
||||
|
||||
component: lazy(() => import('./index'))
|
||||
});
|
||||
|
||||
@@ -9,6 +9,7 @@ export const meta = defineTool('pdf', {
|
||||
i18n: {
|
||||
name: 'pdf:splitPdf.title',
|
||||
description: 'pdf:splitPdf.description',
|
||||
shortDescription: 'pdf:splitPdf.shortDescription'
|
||||
shortDescription: 'pdf:splitPdf.shortDescription',
|
||||
userTypes: ['generalUsers']
|
||||
}
|
||||
});
|
||||
|
||||
@@ -10,6 +10,7 @@ export const tool = defineTool('string', {
|
||||
i18n: {
|
||||
name: 'string:base64.title',
|
||||
description: 'string:base64.description',
|
||||
shortDescription: 'string:base64.shortDescription'
|
||||
shortDescription: 'string:base64.shortDescription',
|
||||
userTypes: ['generalUsers', 'developers']
|
||||
}
|
||||
});
|
||||
|
||||
@@ -11,6 +11,7 @@ export const tool = defineTool('string', {
|
||||
i18n: {
|
||||
name: 'string:censor.title',
|
||||
description: 'string:censor.description',
|
||||
shortDescription: 'string:censor.shortDescription'
|
||||
shortDescription: 'string:censor.shortDescription',
|
||||
userTypes: ['generalUsers']
|
||||
}
|
||||
});
|
||||
|
||||
@@ -11,6 +11,7 @@ export const tool = defineTool('string', {
|
||||
i18n: {
|
||||
name: 'string:createPalindrome.title',
|
||||
description: 'string:createPalindrome.description',
|
||||
shortDescription: 'string:createPalindrome.shortDescription'
|
||||
shortDescription: 'string:createPalindrome.shortDescription',
|
||||
userTypes: ['generalUsers']
|
||||
}
|
||||
});
|
||||
|
||||
@@ -11,6 +11,7 @@ export const tool = defineTool('string', {
|
||||
i18n: {
|
||||
name: 'string:extractSubstring.title',
|
||||
description: 'string:extractSubstring.description',
|
||||
shortDescription: 'string:extractSubstring.shortDescription'
|
||||
shortDescription: 'string:extractSubstring.shortDescription',
|
||||
userTypes: ['generalUsers', 'developers']
|
||||
}
|
||||
});
|
||||
|
||||
@@ -3,13 +3,15 @@ import { lazy } from 'react';
|
||||
|
||||
export const tool = defineTool('string', {
|
||||
path: 'join',
|
||||
|
||||
icon: 'material-symbols-light:join',
|
||||
|
||||
keywords: ['join'],
|
||||
keywords: ['text', 'join'],
|
||||
component: lazy(() => import('./index')),
|
||||
i18n: {
|
||||
name: 'string:join.title',
|
||||
description: 'string:join.description',
|
||||
shortDescription: 'string:join.shortDescription'
|
||||
shortDescription: 'string:join.shortDescription',
|
||||
userTypes: ['generalUsers']
|
||||
}
|
||||
});
|
||||
|
||||
@@ -11,6 +11,7 @@ export const tool = defineTool('string', {
|
||||
i18n: {
|
||||
name: 'string:palindrome.title',
|
||||
description: 'string:palindrome.description',
|
||||
shortDescription: 'string:palindrome.shortDescription'
|
||||
shortDescription: 'string:palindrome.shortDescription',
|
||||
userTypes: ['generalUsers']
|
||||
}
|
||||
});
|
||||
|
||||
@@ -11,6 +11,7 @@ export const tool = defineTool('string', {
|
||||
i18n: {
|
||||
name: 'string:quote.title',
|
||||
description: 'string:quote.description',
|
||||
shortDescription: 'string:quote.shortDescription'
|
||||
shortDescription: 'string:quote.shortDescription',
|
||||
userTypes: ['generalUsers', 'developers']
|
||||
}
|
||||
});
|
||||
|
||||
@@ -11,6 +11,7 @@ export const tool = defineTool('string', {
|
||||
i18n: {
|
||||
name: 'string:randomizeCase.title',
|
||||
description: 'string:randomizeCase.description',
|
||||
shortDescription: 'string:randomizeCase.shortDescription'
|
||||
shortDescription: 'string:randomizeCase.shortDescription',
|
||||
userTypes: ['generalUsers']
|
||||
}
|
||||
});
|
||||
|
||||
@@ -10,6 +10,7 @@ export const tool = defineTool('string', {
|
||||
i18n: {
|
||||
name: 'string:removeDuplicateLines.title',
|
||||
description: 'string:removeDuplicateLines.description',
|
||||
shortDescription: 'string:removeDuplicateLines.shortDescription'
|
||||
shortDescription: 'string:removeDuplicateLines.shortDescription',
|
||||
userTypes: ['generalUsers', 'developers']
|
||||
}
|
||||
});
|
||||
|
||||
@@ -11,6 +11,7 @@ export const tool = defineTool('string', {
|
||||
i18n: {
|
||||
name: 'string:repeat.title',
|
||||
description: 'string:repeat.description',
|
||||
shortDescription: 'string:repeat.shortDescription'
|
||||
shortDescription: 'string:repeat.shortDescription',
|
||||
userTypes: ['generalUsers', 'developers']
|
||||
}
|
||||
});
|
||||
|
||||
@@ -10,6 +10,7 @@ export const tool = defineTool('string', {
|
||||
i18n: {
|
||||
name: 'string:reverse.title',
|
||||
description: 'string:reverse.description',
|
||||
shortDescription: 'string:reverse.shortDescription'
|
||||
shortDescription: 'string:reverse.shortDescription',
|
||||
userTypes: ['generalUsers', 'developers']
|
||||
}
|
||||
});
|
||||
|
||||
@@ -6,7 +6,8 @@ export const tool = defineTool('string', {
|
||||
i18n: {
|
||||
name: 'string:rot13.title',
|
||||
description: 'string:rot13.description',
|
||||
shortDescription: 'string:rot13.shortDescription'
|
||||
shortDescription: 'string:rot13.shortDescription',
|
||||
userTypes: ['generalUsers', 'developers']
|
||||
},
|
||||
|
||||
path: 'rot13',
|
||||
|
||||
@@ -6,7 +6,8 @@ export const tool = defineTool('string', {
|
||||
i18n: {
|
||||
name: 'string:rotate.title',
|
||||
description: 'string:rotate.description',
|
||||
shortDescription: 'string:rotate.shortDescription'
|
||||
shortDescription: 'string:rotate.shortDescription',
|
||||
userTypes: ['generalUsers', 'developers']
|
||||
},
|
||||
|
||||
path: 'rotate',
|
||||
|
||||
@@ -3,6 +3,7 @@ import { lazy } from 'react';
|
||||
|
||||
export const tool = defineTool('string', {
|
||||
path: 'split',
|
||||
|
||||
icon: 'material-symbols-light:call-split',
|
||||
|
||||
keywords: ['split'],
|
||||
@@ -10,6 +11,7 @@ export const tool = defineTool('string', {
|
||||
i18n: {
|
||||
name: 'string:split.title',
|
||||
description: 'string:split.description',
|
||||
shortDescription: 'string:split.shortDescription'
|
||||
shortDescription: 'string:split.shortDescription',
|
||||
userTypes: ['generalUsers', 'developers']
|
||||
}
|
||||
});
|
||||
|
||||
@@ -11,6 +11,7 @@ export const tool = defineTool('string', {
|
||||
i18n: {
|
||||
name: 'string:statistic.title',
|
||||
description: 'string:statistic.description',
|
||||
shortDescription: 'string:statistic.shortDescription'
|
||||
shortDescription: 'string:statistic.shortDescription',
|
||||
userTypes: ['generalUsers', 'developers']
|
||||
}
|
||||
});
|
||||
|
||||
@@ -5,7 +5,8 @@ export const tool = defineTool('string', {
|
||||
i18n: {
|
||||
name: 'string:textReplacer.title',
|
||||
description: 'string:textReplacer.description',
|
||||
shortDescription: 'string:textReplacer.shortDescription'
|
||||
shortDescription: 'string:textReplacer.shortDescription',
|
||||
userTypes: ['generalUsers', 'developers']
|
||||
},
|
||||
|
||||
path: 'replacer',
|
||||
|
||||
@@ -10,6 +10,7 @@ export const tool = defineTool('string', {
|
||||
i18n: {
|
||||
name: 'string:toMorse.title',
|
||||
description: 'string:toMorse.description',
|
||||
shortDescription: 'string:toMorse.shortDescription'
|
||||
shortDescription: 'string:toMorse.shortDescription',
|
||||
userTypes: ['generalUsers', 'developers']
|
||||
}
|
||||
});
|
||||
|
||||
@@ -10,6 +10,7 @@ export const tool = defineTool('string', {
|
||||
i18n: {
|
||||
name: 'string:truncate.title',
|
||||
description: 'string:truncate.description',
|
||||
shortDescription: 'string:truncate.shortDescription'
|
||||
shortDescription: 'string:truncate.shortDescription',
|
||||
userTypes: ['generalUsers', 'developers']
|
||||
}
|
||||
});
|
||||
|
||||
@@ -10,6 +10,7 @@ export const tool = defineTool('string', {
|
||||
i18n: {
|
||||
name: 'string:uppercase.title',
|
||||
description: 'string:uppercase.description',
|
||||
shortDescription: 'string:uppercase.shortDescription'
|
||||
shortDescription: 'string:uppercase.shortDescription',
|
||||
userTypes: ['generalUsers', 'developers']
|
||||
}
|
||||
});
|
||||
|
||||
@@ -5,7 +5,16 @@ export const tool = defineTool('string', {
|
||||
path: 'url-decode-string',
|
||||
icon: 'codicon:symbol-string',
|
||||
|
||||
keywords: ['uppercase'],
|
||||
keywords: [
|
||||
'url',
|
||||
'decode',
|
||||
'string',
|
||||
'url decode',
|
||||
'unescape',
|
||||
'encoding',
|
||||
'percent',
|
||||
'decode url'
|
||||
],
|
||||
component: lazy(() => import('./index')),
|
||||
i18n: {
|
||||
name: 'string:urlDecode.toolInfo.title',
|
||||
|
||||
@@ -5,7 +5,7 @@ export const tool = defineTool('string', {
|
||||
path: 'url-encode-string',
|
||||
icon: 'ic:baseline-percentage',
|
||||
|
||||
keywords: ['uppercase'],
|
||||
keywords: ['url', 'encode', 'string', 'url encode', 'encoding', 'percent'],
|
||||
component: lazy(() => import('./index')),
|
||||
i18n: {
|
||||
name: 'string:urlEncode.toolInfo.title',
|
||||
|
||||
@@ -10,6 +10,7 @@ export const tool = defineTool('time', {
|
||||
i18n: {
|
||||
name: 'time:checkLeapYears.title',
|
||||
description: 'time:checkLeapYears.description',
|
||||
shortDescription: 'time:checkLeapYears.shortDescription'
|
||||
shortDescription: 'time:checkLeapYears.shortDescription',
|
||||
userTypes: ['generalUsers']
|
||||
}
|
||||
});
|
||||
|
||||
@@ -10,6 +10,7 @@ export const tool = defineTool('time', {
|
||||
i18n: {
|
||||
name: 'time:convertDaysToHours.title',
|
||||
description: 'time:convertDaysToHours.description',
|
||||
shortDescription: 'time:convertDaysToHours.shortDescription'
|
||||
shortDescription: 'time:convertDaysToHours.shortDescription',
|
||||
userTypes: ['generalUsers']
|
||||
}
|
||||
});
|
||||
|
||||
@@ -10,6 +10,7 @@ export const tool = defineTool('time', {
|
||||
i18n: {
|
||||
name: 'time:convertHoursToDays.title',
|
||||
description: 'time:convertHoursToDays.description',
|
||||
shortDescription: 'time:convertHoursToDays.shortDescription'
|
||||
shortDescription: 'time:convertHoursToDays.shortDescription',
|
||||
userTypes: ['generalUsers']
|
||||
}
|
||||
});
|
||||
|
||||
@@ -10,6 +10,7 @@ export const tool = defineTool('time', {
|
||||
i18n: {
|
||||
name: 'time:convertSecondsToTime.title',
|
||||
description: 'time:convertSecondsToTime.description',
|
||||
shortDescription: 'time:convertSecondsToTime.shortDescription'
|
||||
shortDescription: 'time:convertSecondsToTime.shortDescription',
|
||||
userTypes: ['generalUsers']
|
||||
}
|
||||
});
|
||||
|
||||
@@ -5,11 +5,12 @@ export const tool = defineTool('time', {
|
||||
path: 'convert-time-to-seconds',
|
||||
icon: 'material-symbols:schedule',
|
||||
|
||||
keywords: ['time', 'seconds', 'convert', 'format'],
|
||||
keywords: ['time', 'seconds', 'convert', 'format', 'HH:MM:SS'],
|
||||
component: lazy(() => import('./index')),
|
||||
i18n: {
|
||||
name: 'time:convertTimeToSeconds.title',
|
||||
description: 'time:convertTimeToSeconds.description',
|
||||
shortDescription: 'time:convertTimeToSeconds.shortDescription'
|
||||
shortDescription: 'time:convertTimeToSeconds.shortDescription',
|
||||
userTypes: ['generalUsers', 'developers']
|
||||
}
|
||||
});
|
||||
|
||||
@@ -5,8 +5,7 @@ export const tool = defineTool('time', {
|
||||
i18n: {
|
||||
name: 'time:convertUnixToDate.title',
|
||||
description: 'time:convertUnixToDate.description',
|
||||
shortDescription: 'time:convertUnixToDate.shortDescription',
|
||||
longDescription: 'time:convertUnixToDate.longDescription'
|
||||
shortDescription: 'time:convertUnixToDate.shortDescription'
|
||||
},
|
||||
path: 'convert-unix-to-date',
|
||||
icon: 'material-symbols:schedule',
|
||||
|
||||
@@ -4,12 +4,21 @@ import { lazy } from 'react';
|
||||
export const tool = defineTool('time', {
|
||||
path: 'crontab-guru',
|
||||
icon: 'material-symbols:schedule',
|
||||
|
||||
keywords: ['cron', 'schedule', 'automation', 'expression'],
|
||||
keywords: [
|
||||
'crontab',
|
||||
'cron',
|
||||
'schedule',
|
||||
'guru',
|
||||
'time',
|
||||
'expression',
|
||||
'parser',
|
||||
'explain'
|
||||
],
|
||||
component: lazy(() => import('./index')),
|
||||
i18n: {
|
||||
name: 'time:crontabGuru.title',
|
||||
description: 'time:crontabGuru.description',
|
||||
shortDescription: 'time:crontabGuru.shortDescription'
|
||||
shortDescription: 'time:crontabGuru.shortDescription',
|
||||
userTypes: ['developers']
|
||||
}
|
||||
});
|
||||
|
||||
@@ -10,6 +10,7 @@ export const tool = defineTool('time', {
|
||||
i18n: {
|
||||
name: 'time:timeBetweenDates.title',
|
||||
description: 'time:timeBetweenDates.description',
|
||||
shortDescription: 'time:timeBetweenDates.shortDescription'
|
||||
shortDescription: 'time:timeBetweenDates.shortDescription',
|
||||
userTypes: ['generalUsers', 'developers']
|
||||
}
|
||||
});
|
||||
|
||||
@@ -10,6 +10,7 @@ export const tool = defineTool('time', {
|
||||
i18n: {
|
||||
name: 'time:truncateClockTime.title',
|
||||
description: 'time:truncateClockTime.description',
|
||||
shortDescription: 'time:truncateClockTime.shortDescription'
|
||||
shortDescription: 'time:truncateClockTime.shortDescription',
|
||||
userTypes: ['generalUsers', 'developers']
|
||||
}
|
||||
});
|
||||
|
||||
@@ -10,6 +10,7 @@ export const tool = defineTool('video', {
|
||||
i18n: {
|
||||
name: 'video:changeSpeed.title',
|
||||
description: 'video:changeSpeed.description',
|
||||
shortDescription: 'video:changeSpeed.shortDescription'
|
||||
shortDescription: 'video:changeSpeed.shortDescription',
|
||||
userTypes: ['generalUsers']
|
||||
}
|
||||
});
|
||||
|
||||
@@ -8,15 +8,20 @@ export const tool = defineTool('video', {
|
||||
keywords: [
|
||||
'compress',
|
||||
'video',
|
||||
'resize',
|
||||
'scale',
|
||||
'resolution',
|
||||
'reduce size'
|
||||
'reduce',
|
||||
'size',
|
||||
'optimize',
|
||||
'mp4',
|
||||
'mov',
|
||||
'avi',
|
||||
'video editing',
|
||||
'shrink'
|
||||
],
|
||||
component: lazy(() => import('./index')),
|
||||
i18n: {
|
||||
name: 'video:compress.title',
|
||||
description: 'video:compress.description',
|
||||
shortDescription: 'video:compress.shortDescription'
|
||||
shortDescription: 'video:compress.shortDescription',
|
||||
userTypes: ['generalUsers']
|
||||
}
|
||||
});
|
||||
|
||||
@@ -4,12 +4,22 @@ import { lazy } from 'react';
|
||||
export const tool = defineTool('video', {
|
||||
path: 'crop-video',
|
||||
icon: 'material-symbols:crop',
|
||||
|
||||
keywords: ['video', 'crop', 'trim', 'edit', 'resize'],
|
||||
component: lazy(() => import('./index')),
|
||||
keywords: [
|
||||
'crop',
|
||||
'video',
|
||||
'trim',
|
||||
'aspect ratio',
|
||||
'mp4',
|
||||
'mov',
|
||||
'avi',
|
||||
'video editing',
|
||||
'resize'
|
||||
],
|
||||
i18n: {
|
||||
name: 'video:cropVideo.title',
|
||||
description: 'video:cropVideo.description',
|
||||
shortDescription: 'video:cropVideo.shortDescription'
|
||||
}
|
||||
shortDescription: 'video:cropVideo.shortDescription',
|
||||
userTypes: ['generalUsers']
|
||||
},
|
||||
component: lazy(() => import('./index'))
|
||||
});
|
||||
|
||||
@@ -10,6 +10,7 @@ export const tool = defineTool('video', {
|
||||
i18n: {
|
||||
name: 'video:flip.title',
|
||||
description: 'video:flip.description',
|
||||
shortDescription: 'video:flip.shortDescription'
|
||||
shortDescription: 'video:flip.shortDescription',
|
||||
userTypes: ['generalUsers']
|
||||
}
|
||||
});
|
||||
|
||||
@@ -10,6 +10,7 @@ export const tool = defineTool('video', {
|
||||
i18n: {
|
||||
name: 'video:loop.title',
|
||||
description: 'video:loop.description',
|
||||
shortDescription: 'video:loop.shortDescription'
|
||||
shortDescription: 'video:loop.shortDescription',
|
||||
userTypes: ['generalUsers']
|
||||
}
|
||||
});
|
||||
|
||||
@@ -10,6 +10,7 @@ export const tool = defineTool('video', {
|
||||
i18n: {
|
||||
name: 'video:rotate.title',
|
||||
description: 'video:rotate.description',
|
||||
shortDescription: 'video:rotate.shortDescription'
|
||||
shortDescription: 'video:rotate.shortDescription',
|
||||
userTypes: ['generalUsers']
|
||||
}
|
||||
});
|
||||
|
||||
@@ -9,6 +9,7 @@ export const tool = defineTool('video', {
|
||||
i18n: {
|
||||
name: 'video:trim.title',
|
||||
description: 'video:trim.description',
|
||||
shortDescription: 'video:trim.shortDescription'
|
||||
shortDescription: 'video:trim.shortDescription',
|
||||
userTypes: ['generalUsers']
|
||||
}
|
||||
});
|
||||
|
||||
@@ -9,6 +9,7 @@ export const tool = defineTool('video', {
|
||||
i18n: {
|
||||
name: 'video:videoToGif.title',
|
||||
description: 'video:videoToGif.description',
|
||||
shortDescription: 'video:videoToGif.shortDescription'
|
||||
shortDescription: 'video:videoToGif.shortDescription',
|
||||
userTypes: ['generalUsers']
|
||||
}
|
||||
});
|
||||
|
||||
77
src/providers/UserTypeFilterProvider.tsx
Normal file
77
src/providers/UserTypeFilterProvider.tsx
Normal file
@@ -0,0 +1,77 @@
|
||||
import React, {
|
||||
createContext,
|
||||
useContext,
|
||||
useState,
|
||||
useEffect,
|
||||
ReactNode,
|
||||
useMemo
|
||||
} from 'react';
|
||||
import { UserType } from '@tools/defineTool';
|
||||
|
||||
interface UserTypeFilterContextType {
|
||||
selectedUserTypes: UserType[];
|
||||
setSelectedUserTypes: (userTypes: UserType[]) => void;
|
||||
}
|
||||
|
||||
const UserTypeFilterContext = createContext<UserTypeFilterContextType | null>(
|
||||
null
|
||||
);
|
||||
|
||||
interface UserTypeFilterProviderProps {
|
||||
children: ReactNode;
|
||||
}
|
||||
|
||||
export function UserTypeFilterProvider({
|
||||
children
|
||||
}: UserTypeFilterProviderProps) {
|
||||
const [selectedUserTypes, setSelectedUserTypes] = useState<UserType[]>(() => {
|
||||
try {
|
||||
const saved = localStorage.getItem('selectedUserTypes');
|
||||
return saved ? JSON.parse(saved) : [];
|
||||
} catch (error) {
|
||||
console.error(
|
||||
'Error loading selectedUserTypes from localStorage:',
|
||||
error
|
||||
);
|
||||
return [];
|
||||
}
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
try {
|
||||
localStorage.setItem(
|
||||
'selectedUserTypes',
|
||||
JSON.stringify(selectedUserTypes)
|
||||
);
|
||||
} catch (error) {
|
||||
console.error('Error saving selectedUserTypes to localStorage:', error);
|
||||
}
|
||||
}, [selectedUserTypes]);
|
||||
|
||||
const contextValue = useMemo(
|
||||
() => ({
|
||||
selectedUserTypes,
|
||||
setSelectedUserTypes
|
||||
}),
|
||||
[selectedUserTypes]
|
||||
);
|
||||
|
||||
return (
|
||||
<UserTypeFilterContext.Provider value={contextValue}>
|
||||
{children}
|
||||
</UserTypeFilterContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
export function useUserTypeFilter(): UserTypeFilterContextType {
|
||||
const context = useContext(UserTypeFilterContext);
|
||||
|
||||
if (!context) {
|
||||
throw new Error(
|
||||
'useUserTypeFilter must be used within a UserTypeFilterProvider. ' +
|
||||
'Make sure your component is wrapped with <UserTypeFilterProvider>.'
|
||||
);
|
||||
}
|
||||
|
||||
return context;
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user