feat: dark mode

This commit is contained in:
Ibrahima G. Coulibaly
2025-03-31 01:27:44 +00:00
parent e53642db31
commit d676383d22
26 changed files with 297 additions and 551 deletions

View File

@@ -1,14 +1,14 @@
import { BrowserRouter, useRoutes } from 'react-router-dom';
import routesConfig from '../config/routesConfig';
import Navbar from './Navbar';
import { Suspense } from 'react';
import { Suspense, useMemo, useState } from 'react';
import Loading from './Loading';
import { ThemeProvider } from '@mui/material';
import theme from '../config/muiConfig';
import { CssBaseline, ThemeProvider } from '@mui/material';
import { CustomSnackBarProvider } from '../contexts/CustomSnackBarContext';
import { SnackbarProvider } from 'notistack';
import { tools } from '../tools';
import './index.css';
import { darkTheme, lightTheme } from '../config/muiConfig';
const AppRoutes = () => {
const updatedRoutesConfig = [...routesConfig];
@@ -19,21 +19,29 @@ const AppRoutes = () => {
};
function App() {
const [darkMode, setDarkMode] = useState<boolean>(() => {
return localStorage.getItem('theme') === 'dark';
});
const theme = useMemo(() => (darkMode ? darkTheme : lightTheme), [darkMode]);
return (
<ThemeProvider theme={theme}>
<CssBaseline />
<SnackbarProvider
maxSnack={5}
anchorOrigin={{
vertical: 'bottom',
horizontal: 'right'
}}
classes={{
containerRoot: 'bottom-0 right-0 mb-52 md:mb-68 mr-8 lg:mr-80 z-99'
}}
>
<CustomSnackBarProvider>
<BrowserRouter>
<Navbar />
<Navbar
onSwitchTheme={() => {
setDarkMode((prevState) => !prevState);
localStorage.setItem('theme', darkMode ? 'light' : 'dark');
}}
/>
<Suspense fallback={<Loading />}>
<AppRoutes />
</Suspense>

View File

@@ -1,4 +1,4 @@
import { Autocomplete, Box, Stack, TextField } from '@mui/material';
import { Autocomplete, Box, Stack, TextField, useTheme } from '@mui/material';
import Typography from '@mui/material/Typography';
import SearchIcon from '@mui/icons-material/Search';
import Grid from '@mui/material/Grid';
@@ -25,6 +25,7 @@ const exampleTools: { label: string; url: string }[] = [
];
export default function Hero() {
const [inputValue, setInputValue] = useState<string>('');
const theme = useTheme();
const [filteredTools, setFilteredTools] = useState<DefinedTool[]>(
_.shuffle(tools)
);
@@ -78,7 +79,7 @@ export default function Hero() {
endAdornment: <SearchIcon />,
sx: {
borderRadius: 4,
backgroundColor: 'white'
backgroundColor: 'background.paper'
}
}}
onChange={(event) => handleInputChange(event, event.target.value)}
@@ -125,11 +126,13 @@ export default function Hero() {
borderWidth: 1,
padding: 1,
borderRadius: 3,
borderColor: 'grey',
borderColor: theme.palette.mode === 'dark' ? '#363b41' : 'grey',
borderStyle: 'solid',
backgroundColor: 'white',
backgroundColor: 'background.paper',
cursor: 'pointer',
'&:hover': { backgroundColor: '#FAFAFD' }
'&:hover': {
backgroundColor: 'background.hover'
}
}}
>
<Typography>{tool.label}</Typography>

View File

@@ -17,8 +17,13 @@ import {
import useMediaQuery from '@mui/material/useMediaQuery';
import { useTheme } from '@mui/material/styles';
import { Icon } from '@iconify/react';
import DarkModeIcon from '@mui/icons-material/DarkMode';
const Navbar: React.FC = () => {
interface NavbarProps {
onSwitchTheme: () => void;
}
const Navbar: React.FC<NavbarProps> = ({ onSwitchTheme }) => {
const navigate = useNavigate();
const theme = useTheme();
const isMobile = useMediaQuery(theme.breakpoints.down('md'));
@@ -30,7 +35,9 @@ const Navbar: React.FC = () => {
// { label: 'Features', path: '/features' }
// { label: 'About Us', path: '/about-us' }
];
const buttons: ReactNode[] = [
<DarkModeIcon onClick={onSwitchTheme} style={{ cursor: 'pointer' }} />,
<Icon
onClick={() => window.open('https://discord.gg/SDbbn3hT4b', '_blank')}
style={{ cursor: 'pointer' }}
@@ -81,9 +88,10 @@ const Navbar: React.FC = () => {
return (
<AppBar
position="static"
style={{
backgroundColor: '#F5F5FA',
color: 'black'
sx={{
background: 'transparent',
boxShadow: 'none',
color: 'text.primary'
}}
>
<Toolbar

View File

@@ -31,7 +31,7 @@ function ToolLinks() {
<Grid container spacing={2} mt={1}>
<Grid item md={12} lg={6}>
<StyledButton
sx={{ backgroundColor: 'white' }}
sx={{ backgroundColor: 'background.paper' }}
fullWidth
variant="outlined"
onClick={() => scrollToElement('tool')}
@@ -43,6 +43,7 @@ function ToolLinks() {
<StyledButton
fullWidth
variant="outlined"
sx={{ backgroundColor: 'background.paper' }}
onClick={() => scrollToElement('examples')}
>
See Examples

View File

@@ -38,7 +38,7 @@ export default function ToolLayout({
display={'flex'}
flexDirection={'column'}
alignItems={'center'}
sx={{ backgroundColor: '#F5F5FA' }}
sx={{ backgroundColor: 'background.default' }}
>
<Helmet>
<title>{`${title} - Omni Tools`}</title>

View File

@@ -1,4 +1,12 @@
import { Box, Card, CardContent, Link, Stack, Typography } from '@mui/material';
import {
Box,
Card,
CardContent,
Link,
Stack,
Typography,
useTheme
} from '@mui/material';
import { ToolCardProps } from './AllTools';
import ChevronRightIcon from '@mui/icons-material/ChevronRight';
import { useNavigate } from 'react-router-dom';
@@ -10,6 +18,7 @@ export default function ToolCard({
link,
icon
}: ToolCardProps) {
const theme = useTheme();
const navigate = useNavigate();
return (
<Card
@@ -17,10 +26,13 @@ export default function ToolCard({
raised
sx={{
borderRadius: 2,
bgcolor: '#5581b5',
borderColor: '#5581b5',
bgcolor: 'background.darkSecondary',
borderColor: 'background.darkSecondary',
color: '#fff',
boxShadow: '6px 6px 12px #b8b9be, -6px -6px 12px #fff',
boxShadow:
theme.palette.mode === 'dark'
? null
: '6px 6px 12px #b8b9be, -6px -6px 12px #fff',
cursor: 'pointer',
height: '100%',
'&:hover': {

View File

@@ -38,14 +38,16 @@ export default function ExampleCard<T>({
changeInputResult(sampleText, sampleOptions);
}}
sx={{
bgcolor: theme.palette.background.default,
bgcolor: 'background.lightSecondary',
height: '100%',
overflow: 'hidden',
borderRadius: 2,
transition: 'background-color 0.3s ease',
cursor: 'pointer',
'&:hover': {
boxShadow: '12px 9px 11px 2px #b8b9be, -6px -6px 12px #fff'
boxShadow: `12px 9px 11px 2px ${
theme.palette.mode === 'dark' ? theme.palette.grey[900] : '#b8b9be'
}, -6px -6px 12px ${theme.palette.mode === 'dark' ? 'black' : '#fff'}`
}
}}
>

View File

@@ -94,7 +94,7 @@ export default function BaseFileInput({
border: preview ? 0 : 1,
borderRadius: 2,
boxShadow: '5',
bgcolor: 'white',
bgcolor: 'background.paper',
position: 'relative'
}}
>
@@ -106,7 +106,8 @@ export default function BaseFileInput({
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
backgroundImage: `url(${greyPattern})`,
backgroundImage:
theme.palette.mode === 'dark' ? null : `url(${greyPattern})`,
position: 'relative',
overflow: 'hidden'
}}
@@ -126,7 +127,13 @@ export default function BaseFileInput({
cursor: 'pointer'
}}
>
<Typography color={theme.palette.grey['600']}>
<Typography
color={
theme.palette.mode === 'dark'
? theme.palette.grey['300']
: theme.palette.grey['600']
}
>
Click here to select a {type} from your device, press Ctrl+V to
use a {type} from your clipboard, drag and drop a file from
desktop

View File

@@ -1,377 +0,0 @@
import { Box, useTheme } from '@mui/material';
import Typography from '@mui/material/Typography';
import React, { useContext, useEffect, useRef, useState } from 'react';
import ReactCrop, { Crop, PixelCrop } from 'react-image-crop';
import 'react-image-crop/dist/ReactCrop.css';
import InputHeader from '../InputHeader';
import InputFooter from './InputFooter';
import { CustomSnackBarContext } from '../../contexts/CustomSnackBarContext';
import greyPattern from '@assets/grey-pattern.png';
import { globalInputHeight } from '../../config/uiConfig';
import Slider from 'rc-slider';
import 'rc-slider/assets/index.css';
interface ToolFileInputProps {
value: File | null;
onChange: (file: File) => void;
accept: string[];
title?: string;
showCropOverlay?: boolean;
cropShape?: 'rectangular' | 'circular';
cropPosition?: { x: number; y: number };
cropSize?: { width: number; height: number };
onCropChange?: (
position: { x: number; y: number },
size: { width: number; height: number }
) => void;
type?: 'image' | 'video' | 'audio';
// Video specific props
showTrimControls?: boolean;
onTrimChange?: (trimStart: number, trimEnd: number) => void;
trimStart?: number;
trimEnd?: number;
}
export default function ToolFileInput({
value,
onChange,
accept,
title = 'File',
showCropOverlay = false,
cropShape = 'rectangular',
cropPosition = { x: 0, y: 0 },
cropSize = { width: 100, height: 100 },
onCropChange,
type = 'image',
showTrimControls = false,
onTrimChange,
trimStart = 0,
trimEnd = 100
}: ToolFileInputProps) {
const [preview, setPreview] = useState<string | null>(null);
const theme = useTheme();
const { showSnackBar } = useContext(CustomSnackBarContext);
const fileInputRef = useRef<HTMLInputElement>(null);
const imageRef = useRef<HTMLImageElement>(null);
const videoRef = useRef<HTMLVideoElement>(null);
const [imgWidth, setImgWidth] = useState(0);
const [imgHeight, setImgHeight] = useState(0);
const [videoDuration, setVideoDuration] = useState(0);
// Convert position and size to crop format used by ReactCrop
const [crop, setCrop] = useState<Crop>({
unit: 'px',
x: 0,
y: 0,
width: 0,
height: 0
});
const RATIO = imageRef.current ? imgWidth / imageRef.current.width : 1;
useEffect(() => {
if (imgWidth && imgHeight) {
setCrop({
unit: 'px',
x: cropPosition.x / RATIO,
y: cropPosition.y / RATIO,
width: cropSize.width / RATIO,
height: cropSize.height / RATIO
});
}
}, [cropPosition, cropSize, imgWidth, imgHeight]);
const handleCopy = () => {
if (value) {
const blob = new Blob([value], { type: value.type });
const clipboardItem = new ClipboardItem({ [value.type]: blob });
navigator.clipboard
.write([clipboardItem])
.then(() => showSnackBar('File copied', 'success'))
.catch((err) => {
showSnackBar('Failed to copy: ' + err, 'error');
});
}
};
useEffect(() => {
if (value) {
const objectUrl = URL.createObjectURL(value);
setPreview(objectUrl);
// Clean up memory when the component is unmounted or the file changes
return () => URL.revokeObjectURL(objectUrl);
} else {
setPreview(null);
setImgWidth(0);
setImgHeight(0);
}
}, [value]);
const handleFileChange = (event: React.ChangeEvent<HTMLInputElement>) => {
const file = event.target.files?.[0];
if (file) onChange(file);
};
const handleImportClick = () => {
fileInputRef.current?.click();
};
// Handle image load to set dimensions
const onImageLoad = (e: React.SyntheticEvent<HTMLImageElement>) => {
const { naturalWidth: width, naturalHeight: height } = e.currentTarget;
setImgWidth(width);
setImgHeight(height);
// Initialize crop with a centered default crop if needed
if (!crop.width && !crop.height && onCropChange) {
const initialCrop: Crop = {
unit: 'px',
x: Math.floor(width / 4),
y: Math.floor(height / 4),
width: Math.floor(width / 2),
height: Math.floor(height / 2)
};
setCrop(initialCrop);
// Notify parent component of initial crop
onCropChange(
{ x: initialCrop.x, y: initialCrop.y },
{ width: initialCrop.width, height: initialCrop.height }
);
}
};
// Handle video load to set duration
const onVideoLoad = (e: React.SyntheticEvent<HTMLVideoElement>) => {
const duration = e.currentTarget.duration;
setVideoDuration(duration);
// Initialize trim with full duration if needed
if (onTrimChange && trimStart === 0 && trimEnd === 100) {
onTrimChange(0, duration);
}
};
const handleCropChange = (newCrop: Crop) => {
setCrop(newCrop);
};
const handleCropComplete = (crop: PixelCrop) => {
if (onCropChange) {
onCropChange(
{ x: Math.round(crop.x * RATIO), y: Math.round(crop.y * RATIO) },
{
width: Math.round(crop.width * RATIO),
height: Math.round(crop.height * RATIO)
}
);
}
};
const handleTrimChange = (start: number, end: number) => {
if (onTrimChange) {
onTrimChange(start, end);
}
};
useEffect(() => {
const handlePaste = (event: ClipboardEvent) => {
const clipboardItems = event.clipboardData?.items ?? [];
const item = clipboardItems[0];
if (
item &&
(item.type.includes('image') || item.type.includes('video'))
) {
const file = item.getAsFile();
if (file) onChange(file);
}
};
window.addEventListener('paste', handlePaste);
return () => {
window.removeEventListener('paste', handlePaste);
};
}, [onChange]);
// Format seconds to MM:SS format
const formatTime = (seconds: number) => {
const minutes = Math.floor(seconds / 60);
const remainingSeconds = Math.floor(seconds % 60);
return `${minutes.toString().padStart(2, '0')}:${remainingSeconds
.toString()
.padStart(2, '0')}`;
};
return (
<Box>
<InputHeader title={title} />
<Box
sx={{
width: '100%',
height: globalInputHeight,
border: preview ? 0 : 1,
borderRadius: 2,
boxShadow: '5',
bgcolor: 'white',
position: 'relative'
}}
>
{preview ? (
<Box
width="100%"
height="100%"
sx={{
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
backgroundImage: `url(${greyPattern})`,
position: 'relative',
overflow: 'hidden'
}}
>
{type === 'image' &&
(showCropOverlay ? (
<ReactCrop
crop={crop}
onChange={handleCropChange}
onComplete={handleCropComplete}
circularCrop={cropShape === 'circular'}
style={{ maxWidth: '100%', maxHeight: globalInputHeight }}
>
<img
ref={imageRef}
src={preview}
alt="Preview"
style={{ maxWidth: '100%', maxHeight: globalInputHeight }}
onLoad={onImageLoad}
/>
</ReactCrop>
) : (
<img
ref={imageRef}
src={preview}
alt="Preview"
style={{ maxWidth: '100%', maxHeight: globalInputHeight }}
onLoad={onImageLoad}
/>
))}
{type === 'video' && (
<Box
sx={{
position: 'relative',
width: '100%',
height: '100%',
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
justifyContent: 'center'
}}
>
<video
ref={videoRef}
src={preview}
style={{
maxWidth: '100%',
maxHeight: showTrimControls ? 'calc(100% - 50px)' : '100%'
}}
onLoadedMetadata={onVideoLoad}
controls={!showTrimControls}
/>
{showTrimControls && videoDuration > 0 && (
<Box
sx={{
width: '100%',
padding: '10px 20px',
position: 'absolute',
bottom: 0,
left: 0,
backgroundColor: 'rgba(0,0,0,0.5)',
color: 'white',
display: 'flex',
flexDirection: 'column',
gap: 1
}}
>
<Box
sx={{
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center'
}}
>
<Typography variant="caption">
Start: {formatTime(trimStart || 0)}
</Typography>
<Typography variant="caption">
End: {formatTime(trimEnd || videoDuration)}
</Typography>
</Box>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 2 }}>
<div
className="range-slider-container"
style={{ margin: '20px 0', width: '100%' }}
>
<Slider
range
min={0}
max={videoDuration}
step={0.1}
value={[trimStart || 0, trimEnd || videoDuration]}
onChange={(values) => {
if (Array.isArray(values)) {
handleTrimChange(values[0], values[1]);
}
}}
allowCross={false}
pushable={0.1} // Minimum distance between handles
/>
</div>
</Box>
</Box>
)}
</Box>
)}
{type === 'audio' && (
<audio
src={preview}
controls
style={{ width: '100%', maxWidth: '500px' }}
/>
)}
</Box>
) : (
<Box
onClick={handleImportClick}
sx={{
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
justifyContent: 'center',
padding: 5,
height: '100%',
cursor: 'pointer'
}}
>
<Typography color={theme.palette.grey['600']}>
Click here to select a {type} from your device, press Ctrl+V to
use a {type} from your clipboard, drag and drop a file from
desktop
</Typography>
</Box>
)}
</Box>
<InputFooter handleCopy={handleCopy} handleImport={handleImportClick} />
<input
ref={fileInputRef}
style={{ display: 'none' }}
type="file"
accept={accept.join(',')}
onChange={handleFileChange}
/>
</Box>
);
}

View File

@@ -52,7 +52,7 @@ export default function ToolTextInput({
rows={10}
sx={{
'&.MuiTextField-root': {
backgroundColor: 'white'
backgroundColor: 'background.paper'
}
}}
inputProps={{

View File

@@ -30,7 +30,7 @@ const ColorSelector: React.FC<ColorSelectorProps & TextFieldProps> = ({
<Box mb={1}>
<Stack direction={'row'}>
<TextField
sx={{ backgroundColor: 'white' }}
sx={{ backgroundColor: 'background.paper' }}
value={color}
onChange={handleColorChange}
{...props}

View File

@@ -19,7 +19,7 @@ const TextFieldWithDesc = ({
<Box>
<TextField
placeholder={placeholder}
sx={{ backgroundColor: 'white' }}
sx={{ backgroundColor: 'background.paper' }}
value={value}
onChange={(event) => onOwnChange(event.target.value)}
{...props}

View File

@@ -36,7 +36,7 @@ export default function ToolOptions<T extends FormikValues>({
mb: 2,
borderRadius: 2,
padding: 2,
backgroundColor: theme.palette.background.default,
backgroundColor: 'background.lightSecondary',
boxShadow: '2'
}}
mt={2}

View File

@@ -1,4 +1,4 @@
import { Box, CircularProgress, Typography } from '@mui/material';
import { Box, CircularProgress, Typography, useTheme } from '@mui/material';
import React, { useContext } from 'react';
import InputHeader from '../InputHeader';
import greyPattern from '@assets/grey-pattern.png';
@@ -21,6 +21,7 @@ export default function ToolFileResult({
}) {
const [preview, setPreview] = React.useState<string | null>(null);
const { showSnackBar } = useContext(CustomSnackBarContext);
const theme = useTheme();
React.useEffect(() => {
if (value) {
@@ -87,7 +88,7 @@ export default function ToolFileResult({
border: preview ? 0 : 1,
borderRadius: 2,
boxShadow: '5',
bgcolor: 'white'
bgcolor: 'background.paper'
}}
>
{loading ? (
@@ -114,7 +115,8 @@ export default function ToolFileResult({
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
backgroundImage: `url(${greyPattern})`
backgroundImage:
theme.palette.mode === 'dark' ? null : `url(${greyPattern})`
}}
>
{fileType === 'image' && (

View File

@@ -52,7 +52,7 @@ export default function ToolTextResult({
multiline
sx={{
'&.MuiTextField-root': {
backgroundColor: 'white'
backgroundColor: 'background.paper'
}
}}
rows={10}

View File

@@ -1,13 +1,50 @@
import { createTheme } from '@mui/material';
import { createTheme, ThemeOptions } from '@mui/material';
const theme = createTheme({
const sharedThemeOptions: ThemeOptions = {
typography: {
button: {
textTransform: 'none'
}
},
palette: { background: { default: '#ebf5ff' } },
zIndex: { snackbar: 100000 }
};
export const lightTheme = createTheme({
...sharedThemeOptions,
palette: {
background: {
default: '#F5F5FA',
hover: '#FAFAFD',
lightSecondary: '#EBF5FF',
darkSecondary: '#5581b5'
}
},
components: {
MuiButton: {
styleOverrides: {
contained: { color: '#ffffff', backgroundColor: '#1976d2' }
}
}
}
});
export default theme;
export const darkTheme = createTheme({
...sharedThemeOptions,
palette: {
mode: 'dark',
background: {
default: '#1C1F20',
paper: '#181a1b',
hover: '#1a1c1d',
lightSecondary: '#1E2021',
darkSecondary: '#3C5F8A'
},
text: { primary: '#ffffff' }
},
components: {
MuiButton: {
styleOverrides: {
contained: { color: '#ffffff', backgroundColor: '#145ea8' }
}
}
}
});

View File

@@ -1,6 +1,6 @@
import { getToolsByCategory } from '@tools/index';
import Grid from '@mui/material/Grid';
import { Box, Card, CardContent, Stack } from '@mui/material';
import { Box, Card, CardContent, Stack, useTheme } from '@mui/material';
import { Link, useNavigate } from 'react-router-dom';
import Typography from '@mui/material/Typography';
import Button from '@mui/material/Button';
@@ -19,6 +19,7 @@ const SingleCategory = function ({
index: number;
}) {
const navigate = useNavigate();
const theme = useTheme();
const [hovered, setHovered] = useState<boolean>(false);
const toggleHover = () => setHovered((prevState) => !prevState);
return (
@@ -32,7 +33,7 @@ const SingleCategory = function ({
<Card
sx={{
height: '100%',
backgroundColor: hovered ? '#FAFAFD' : 'white'
backgroundColor: hovered ? 'background.hover' : 'background.paper'
}}
>
<CardContent sx={{ height: '100%' }}>
@@ -52,7 +53,11 @@ const SingleCategory = function ({
color={categoriesColors[index % categoriesColors.length]}
/>
<Link
style={{ fontSize: 20, fontWeight: 700, color: 'black' }}
style={{
fontSize: 20,
fontWeight: 700,
color: theme.palette.mode === 'dark' ? 'white' : 'black'
}}
to={'/categories/' + category.type}
>
{category.title}
@@ -70,7 +75,7 @@ const SingleCategory = function ({
</Grid>
<Grid item xs={12} md={6}>
<Button
sx={{ backgroundColor: 'white' }}
sx={{ backgroundColor: 'background.default' }}
fullWidth
onClick={() => navigate(category.example.path)}
variant={'outlined'}

View File

@@ -1,16 +1,23 @@
import { Box } from '@mui/material';
import { Box, useTheme } from '@mui/material';
import Hero from 'components/Hero';
import Categories from './Categories';
export default function Home() {
const theme = useTheme();
return (
<Box
padding={{
xs: 1,
md: 3,
lg: 5,
background: `url(/assets/background.svg)`,
backgroundColor: '#F5F5FA'
lg: 5
}}
sx={{
background: `url(/assets/${
theme.palette.mode === 'dark'
? 'background-dark.png'
: 'background.svg'
})`,
backgroundColor: 'background.default'
}}
display={'flex'}
flexDirection={'column'}

View File

@@ -26,7 +26,7 @@ export default function Home() {
}, []);
return (
<Box sx={{ backgroundColor: '#F5F5FA' }}>
<Box sx={{ backgroundColor: 'background.default' }}>
<Box
padding={{ xs: 1, md: 3, lg: 5 }}
display={'flex'}
@@ -55,12 +55,14 @@ export default function Home() {
<Grid item xs={12} md={6} lg={4} key={tool.path}>
<Stack
sx={{
backgroundColor: 'white',
boxShadow: '5px 4px 2px #E9E9ED',
backgroundColor: 'background.paper',
boxShadow: `5px 4px 2px ${
theme.palette.mode === 'dark' ? 'black' : '#E9E9ED'
}`,
cursor: 'pointer',
height: '100%',
'&:hover': {
backgroundColor: theme.palette.background.default // Change this to your desired hover color
backgroundColor: theme.palette.background.hover
}
}}
onClick={() => navigate('/' + tool.path)}
@@ -77,7 +79,12 @@ export default function Home() {
color={categoriesColors[index % categoriesColors.length]}
/>
<Box>
<Link style={{ fontSize: 20 }} to={'/' + tool.path}>
<Link
style={{
fontSize: 20
}}
to={'/' + tool.path}
>
{tool.name}
</Link>
<Typography sx={{ mt: 2 }}>

View File

@@ -1,6 +1,5 @@
import { Box, Typography } from '@mui/material';
import React, { useEffect, useRef, useState } from 'react';
import ToolFileInput from '@components/input/ToolFileInput';
import React, { useEffect, useState } from 'react';
import ToolFileResult from '@components/result/ToolFileResult';
import TextFieldWithDesc from '@components/options/TextFieldWithDesc';
import ToolContent from '@components/ToolContent';
@@ -8,7 +7,6 @@ import { ToolComponentProps } from '@tools/defineTool';
import { parsePageRanges, splitPdf } from './service';
import { CardExampleType } from '@components/examples/ToolExamples';
import { PDFDocument } from 'pdf-lib';
import { FormikProps } from 'formik';
import ToolPdfInput from '@components/input/ToolPdfInput';
type InitialValuesType = {

View File

@@ -15,7 +15,6 @@ export async function compressVideo(
input: File,
options: CompressVideoOptions
): Promise<File> {
console.log('Compressing video...', options);
if (!ffmpeg.loaded) {
await ffmpeg.load({
wasmURL:

View File

@@ -1,7 +1,6 @@
import { Box } from '@mui/material';
import React, { useState } from 'react';
import * as Yup from 'yup';
import ToolFileInput from '@components/input/ToolFileInput';
import ToolFileResult from '@components/result/ToolFileResult';
import TextFieldWithDesc from 'components/options/TextFieldWithDesc';
import Typography from '@mui/material/Typography';
@@ -9,6 +8,8 @@ import { FrameOptions, GifReader, GifWriter } from 'omggif';
import { gifBinaryToFile } from '@utils/gif';
import ToolContent from '@components/ToolContent';
import { ToolComponentProps } from '@tools/defineTool';
import ToolVideoInput from '@components/input/ToolVideoInput';
import ToolImageInput from '@components/input/ToolImageInput';
const initialValues = {
newSpeed: 200
@@ -108,7 +109,7 @@ export default function ChangeSpeed({ title }: ToolComponentProps) {
title={title}
input={input}
inputComponent={
<ToolFileInput
<ToolImageInput
value={input}
onChange={setInput}
accept={['image/gif']}