Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions GEMINI.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@

## General Instructions for the code

- Use React components to create components that can be reused.
- When you generate new TypeScript code, follow the existing coding style.
- Ensure all new functions and classes have JSDoc comments.
- Prefer functional programming paradigms where appropriate.
Expand Down
7 changes: 2 additions & 5 deletions astro.config.mjs
Original file line number Diff line number Diff line change
@@ -1,15 +1,12 @@
// @ts-check
import { defineConfig } from 'astro/config';

import react from '@astrojs/react';

import tailwindcss from '@tailwindcss/vite';

// https://astro.build/config
export default defineConfig({
integrations: [react()],

vite: {
plugins: [tailwindcss()]
}
plugins: [tailwindcss()],
},
});
64 changes: 32 additions & 32 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

78 changes: 78 additions & 0 deletions src/components/Base64Encoder.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
import React from 'react';

export default function Base64Encoder() {
const [input, setInput] = React.useState('');
const [output, setOutput] = React.useState('');

const encode = () => {
try {
setOutput(btoa(input));
} catch (e) {
setOutput('Error: Invalid input for base64 encoding.');
}
};

const decode = () => {
try {
setOutput(atob(input));
} catch (e) {
setOutput('Error: Invalid base64 string.');
}
};

const handleFileChange = (e) => {
const file = e.target.files[0];
if (file) {
const reader = new FileReader();
reader.onload = (e) => {
const result = e.target.result;
setInput(result);
setOutput(result.split(',')[1]);
};
reader.readAsDataURL(file);
}
};

return (
<div>
<div className="flex space-x-2 mb-4">
<button
className="bg-[#24e4a7] text-[#202833] font-bold py-2 px-4 rounded-md hover:bg-opacity-80 transition-colors"
onClick={encode}
>
Encode
</button>
<button
className="bg-[#24e4a7] text-[#202833] font-bold py-2 px-4 rounded-md hover:bg-opacity-80 transition-colors"
onClick={decode}
>
Decode
</button>
<input
type="file"
className="block w-full text-sm text-slate-500
file:mr-4 file:py-2 file:px-4
file:rounded-full file:border-0
file:text-sm file:font-semibold
file:bg-violet-50 file:text-violet-700
hover:file:bg-violet-100"
onChange={handleFileChange}
/>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<textarea
value={input}
onChange={(e) => setInput(e.target.value)}
className="w-full h-64 bg-[#2a3442] border border-[#344154] rounded-md px-3 py-2 text-white"
placeholder="Input"
></textarea>
<textarea
readOnly
value={output}
className="w-full h-64 bg-[#2a3442] border border-[#344154] rounded-md px-3 py-2 text-white"
placeholder="Output"
></textarea>
</div>
</div>
);
}
27 changes: 27 additions & 0 deletions src/components/GUIDGenerator.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import React from 'react';

export default function GUIDGenerator() {
const [guid, setGuid] = React.useState('');

const generateGuid = () => {
setGuid(crypto.randomUUID());
};

return (
<div>
<button
className="bg-[#24e4a7] text-[#202833] font-bold py-2 px-4 rounded-md hover:bg-opacity-80 transition-colors"
onClick={generateGuid}
>
Generate GUID
</button>
<input
type="text"
readOnly
value={guid}
className="w-full bg-[#2a3442] border border-[#344154] rounded-md px-3 py-2 mt-4 text-white"
placeholder="Click the button to generate a GUID"
/>
</div>
);
}
66 changes: 66 additions & 0 deletions src/components/Ripple.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import React, { useState, useEffect } from 'react';

const useDebouncedRippleCleanUp = (rippleCount, duration, cleanUpFunction) => {
useEffect(() => {
let bounce = null;
if (rippleCount > 0) {
clearTimeout(bounce);

bounce = setTimeout(() => {
cleanUpFunction();
clearTimeout(bounce);
}, duration * 4);
}

return () => clearTimeout(bounce);
}, [rippleCount, duration, cleanUpFunction]);
};


const Ripple = ({ duration = 850, color = 'bg-white' }) => {
const [rippleArray, setRippleArray] = useState([]);

useDebouncedRippleCleanUp(rippleArray.length, duration, () => {
setRippleArray([]);
});

const addRipple = event => {
const rippleContainer = event.currentTarget.getBoundingClientRect();
const size =
rippleContainer.width > rippleContainer.height
? rippleContainer.width
: rippleContainer.height;
const x = event.pageX - rippleContainer.x - size / 2;
const y = event.pageY - rippleContainer.y - size / 2;
const newRipple = {
x,
y,
size
};

setRippleArray([...rippleArray, newRipple]);
};

return (
<div className="absolute top-0 right-0 bottom-0 left-0" onMouseDown={addRipple}>
{rippleArray.length > 0 &&
rippleArray.map((ripple, index) => {
return (
<span
key={'span' + index}
className={`absolute rounded-full animate-ripple ${color}`}
style={{
top: ripple.y,
left: ripple.x,
width: ripple.size,
height: ripple.size,
animationDuration: `${duration}ms`
}}
/>
);
})}
</div>
);
};

export default Ripple;
44 changes: 44 additions & 0 deletions src/components/Sidebar.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import React from 'react';
import ToolCard from './ToolCard';

const tools = [
{ name: 'GUID Generator', component: 'GUIDGenerator' },
{ name: 'Base64 Encoder', component: 'Base64Encoder' },
];

export default function Sidebar() {
const [searchTerm, setSearchTerm] = React.useState('');
const [activeTool, setActiveTool] = React.useState('');

const filteredTools = tools.filter(tool =>
tool.name.toLowerCase().includes(searchTerm.toLowerCase())
);

const selectTool = (tool) => {
setActiveTool(tool.name);
const event = new CustomEvent('select-tool', { detail: tool });
document.dispatchEvent(event);
};

return (
<div>
<input
type="text"
placeholder="Search"
className="w-full bg-[#2a3442] border border-[#344154] rounded-md px-3 py-2 mb-4 text-white focus:outline-none focus:ring-2 focus:ring-[#24e4a7]"
onChange={e => setSearchTerm(e.target.value)}
value={searchTerm}
/>
<div className="grid grid-cols-2 gap-2">
{filteredTools.map(tool => (
<ToolCard
key={tool.name}
name={tool.name}
onClick={() => selectTool(tool)}
isActive={activeTool === tool.name}
/>
))}
</div>
</div>
);
}
16 changes: 16 additions & 0 deletions src/components/ToolCard.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import React from 'react';
import Ripple from './Ripple';

export default function ToolCard({ name, onClick, isActive }) {
const activeClass = isActive ? 'border-[#24e4a7] shadow-[0_0_10px_#24e4a7]' : 'border-[#344154]';

return (
<div
className={`relative overflow-hidden bg-[#2a3442] rounded-md p-4 text-center cursor-pointer transition-all duration-200 ease-out hover:-translate-y-0.5 hover:shadow-lg hover:shadow-[#24e4a7]/50 ${activeClass}`}
onClick={onClick}
>
<p className="text-sm font-medium text-white">{name}</p>
<Ripple color="bg-[#24e4a7]" />
</div>
);
}
Loading