mirror of
https://github.com/danielvici/tool-website.git
synced 2026-01-16 18:31:26 +00:00
Initial commit: Tool Website with Password Generator, Image Converter and Bookmarks
This commit is contained in:
33
.gitignore
vendored
Normal file
33
.gitignore
vendored
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
# dependencies
|
||||||
|
/node_modules
|
||||||
|
/.pnp
|
||||||
|
.pnp.js
|
||||||
|
|
||||||
|
# testing
|
||||||
|
/coverage
|
||||||
|
|
||||||
|
# next.js
|
||||||
|
/.next/
|
||||||
|
/out/
|
||||||
|
|
||||||
|
# production
|
||||||
|
/build
|
||||||
|
|
||||||
|
# misc
|
||||||
|
.DS_Store
|
||||||
|
*.pem
|
||||||
|
|
||||||
|
# debug
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
|
||||||
|
# local env files
|
||||||
|
.env*.local
|
||||||
|
|
||||||
|
# vercel
|
||||||
|
.vercel
|
||||||
|
|
||||||
|
# typescript
|
||||||
|
*.tsbuildinfo
|
||||||
|
next-env.d.ts
|
||||||
49
README.md
Normal file
49
README.md
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
# Tool Website
|
||||||
|
|
||||||
|
A collection of useful web-based tools built with Next.js, TypeScript, and Tailwind CSS.
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
- Password Generator
|
||||||
|
- Image Format Converter
|
||||||
|
- Website Bookmarks
|
||||||
|
- More tools coming soon!
|
||||||
|
|
||||||
|
## Tech Stack
|
||||||
|
|
||||||
|
- Next.js 14
|
||||||
|
- TypeScript
|
||||||
|
- Tailwind CSS
|
||||||
|
- React
|
||||||
|
- Sharp (for image processing)
|
||||||
|
|
||||||
|
## Getting Started
|
||||||
|
|
||||||
|
1. Clone the repository:
|
||||||
|
```bash
|
||||||
|
git clone https://github.com/yourusername/tool-website.git
|
||||||
|
```
|
||||||
|
|
||||||
|
2. Install dependencies:
|
||||||
|
```bash
|
||||||
|
cd tool-website
|
||||||
|
npm install
|
||||||
|
```
|
||||||
|
|
||||||
|
3. Run the development server:
|
||||||
|
```bash
|
||||||
|
npm run dev
|
||||||
|
```
|
||||||
|
|
||||||
|
4. Open [http://localhost:3000](http://localhost:3000) in your browser.
|
||||||
|
|
||||||
|
## Development
|
||||||
|
|
||||||
|
- `npm run dev` - Start development server
|
||||||
|
- `npm run build` - Build for production
|
||||||
|
- `npm start` - Start production server
|
||||||
|
- `npm run lint` - Run ESLint
|
||||||
|
|
||||||
|
## Privacy
|
||||||
|
|
||||||
|
All tools operate entirely in the browser. No data is sent to external servers (except for explicitly shared URLs in bookmarks). Your data remains on your device and is stored only in your browser's local storage.
|
||||||
34
app/about/page.tsx
Normal file
34
app/about/page.tsx
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
export default function About() {
|
||||||
|
return (
|
||||||
|
<main className="min-h-screen p-8">
|
||||||
|
<div className="max-w-2xl mx-auto">
|
||||||
|
<h1 className="text-4xl font-bold mb-8 text-white">About</h1>
|
||||||
|
|
||||||
|
<div className="bg-zinc-800 rounded-lg shadow-md p-6">
|
||||||
|
<div className="prose prose-invert max-w-none">
|
||||||
|
<p className="text-gray-300 mb-4">
|
||||||
|
Welcome to our Tool Website! This platform provides a collection of useful web-based tools
|
||||||
|
designed to help you with various tasks. All tools run directly in your browser, ensuring
|
||||||
|
your data stays private and secure.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<h2 className="text-2xl font-bold text-white mt-6 mb-4">Features</h2>
|
||||||
|
<ul className="list-disc pl-6 text-gray-300 space-y-2">
|
||||||
|
<li>Password Generator - Create secure passwords with customizable options</li>
|
||||||
|
<li>Image Converter - Convert images between different formats</li>
|
||||||
|
<li>Website Bookmarks - Save and organize your favorite websites</li>
|
||||||
|
<li>More tools coming soon!</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<h2 className="text-2xl font-bold text-white mt-6 mb-4">Privacy</h2>
|
||||||
|
<p className="text-gray-300">
|
||||||
|
All tools operate entirely in your browser. No data is sent to external servers
|
||||||
|
(except for explicitly shared URLs in bookmarks). Your data remains on your device
|
||||||
|
and is stored only in your browser's local storage.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
)
|
||||||
|
}
|
||||||
64
app/api/convert/route.ts
Normal file
64
app/api/convert/route.ts
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
import { NextRequest, NextResponse } from 'next/server'
|
||||||
|
import sharp from 'sharp'
|
||||||
|
|
||||||
|
export async function POST(req: NextRequest) {
|
||||||
|
try {
|
||||||
|
const formData = await req.formData()
|
||||||
|
const file = formData.get('file') as File
|
||||||
|
const format = formData.get('format') as string
|
||||||
|
|
||||||
|
if (!file || !format) {
|
||||||
|
return NextResponse.json(
|
||||||
|
{ error: 'File and format are required' },
|
||||||
|
{ status: 400 }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const buffer = Buffer.from(await file.arrayBuffer())
|
||||||
|
let convertedBuffer: Buffer
|
||||||
|
|
||||||
|
try {
|
||||||
|
const sharpInstance = sharp(buffer)
|
||||||
|
|
||||||
|
switch (format) {
|
||||||
|
case 'png':
|
||||||
|
convertedBuffer = await sharpInstance.png().toBuffer()
|
||||||
|
break
|
||||||
|
case 'jpg':
|
||||||
|
case 'jpeg':
|
||||||
|
convertedBuffer = await sharpInstance.jpeg().toBuffer()
|
||||||
|
break
|
||||||
|
case 'webp':
|
||||||
|
convertedBuffer = await sharpInstance.webp().toBuffer()
|
||||||
|
break
|
||||||
|
case 'gif':
|
||||||
|
convertedBuffer = await sharpInstance.gif().toBuffer()
|
||||||
|
break
|
||||||
|
case 'bmp':
|
||||||
|
convertedBuffer = await sharpInstance.bmp().toBuffer()
|
||||||
|
break
|
||||||
|
default:
|
||||||
|
throw new Error('Unsupported format')
|
||||||
|
}
|
||||||
|
|
||||||
|
return new NextResponse(convertedBuffer, {
|
||||||
|
headers: {
|
||||||
|
'Content-Type': `image/${format}`,
|
||||||
|
'Content-Disposition': `attachment; filename="converted.${format}"`,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Conversion error:', error)
|
||||||
|
return NextResponse.json(
|
||||||
|
{ error: 'Error converting image' },
|
||||||
|
{ status: 500 }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Server error:', error)
|
||||||
|
return NextResponse.json(
|
||||||
|
{ error: 'Server error' },
|
||||||
|
{ status: 500 }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
134
app/bildconverter/page.tsx
Normal file
134
app/bildconverter/page.tsx
Normal file
@@ -0,0 +1,134 @@
|
|||||||
|
'use client'
|
||||||
|
import { useState, useCallback } from 'react'
|
||||||
|
import { useDropzone } from 'react-dropzone'
|
||||||
|
|
||||||
|
export default function ImageConverter() {
|
||||||
|
const [selectedFile, setSelectedFile] = useState<File | null>(null)
|
||||||
|
const [convertTo, setConvertTo] = useState('png')
|
||||||
|
const [isConverting, setIsConverting] = useState(false)
|
||||||
|
const [error, setError] = useState<string | null>(null)
|
||||||
|
|
||||||
|
const onDrop = useCallback((acceptedFiles: File[]) => {
|
||||||
|
setError(null)
|
||||||
|
if (acceptedFiles[0]) {
|
||||||
|
setSelectedFile(acceptedFiles[0])
|
||||||
|
}
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
const { getRootProps, getInputProps, isDragActive } = useDropzone({
|
||||||
|
onDrop,
|
||||||
|
accept: {
|
||||||
|
'image/*': ['.jpeg', '.jpg', '.png', '.gif', '.webp', '.bmp']
|
||||||
|
},
|
||||||
|
multiple: false
|
||||||
|
})
|
||||||
|
|
||||||
|
const handleConvert = async () => {
|
||||||
|
if (!selectedFile) return
|
||||||
|
setError(null)
|
||||||
|
setIsConverting(true)
|
||||||
|
|
||||||
|
try {
|
||||||
|
const formData = new FormData()
|
||||||
|
formData.append('file', selectedFile)
|
||||||
|
formData.append('format', convertTo)
|
||||||
|
|
||||||
|
const response = await fetch('/api/convert', {
|
||||||
|
method: 'POST',
|
||||||
|
body: formData,
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
const errorData = await response.json()
|
||||||
|
throw new Error(errorData.error || 'Konvertierung fehlgeschlagen')
|
||||||
|
}
|
||||||
|
|
||||||
|
// Blob aus der Antwort erstellen
|
||||||
|
const blob = await response.blob()
|
||||||
|
|
||||||
|
// Download-Link erstellen und klicken
|
||||||
|
const url = window.URL.createObjectURL(blob)
|
||||||
|
const a = document.createElement('a')
|
||||||
|
a.href = url
|
||||||
|
a.download = `picture_converted_to.${convertTo}`
|
||||||
|
document.body.appendChild(a)
|
||||||
|
a.click()
|
||||||
|
window.URL.revokeObjectURL(url)
|
||||||
|
document.body.removeChild(a)
|
||||||
|
} catch (err) {
|
||||||
|
setError(err instanceof Error ? err.message : 'Ein Fehler ist aufgetreten')
|
||||||
|
} finally {
|
||||||
|
setIsConverting(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<main className="min-h-screen p-8">
|
||||||
|
<div className="max-w-2xl mx-auto">
|
||||||
|
<h1 className="text-4xl font-bold mb-8 text-white">Bild Converter</h1>
|
||||||
|
|
||||||
|
<div className="bg-zinc-800 rounded-lg shadow-md p-6">
|
||||||
|
<div className="space-y-6">
|
||||||
|
{/* Drag & Drop Zone */}
|
||||||
|
<div
|
||||||
|
{...getRootProps()}
|
||||||
|
className={`border-2 border-dashed rounded-lg p-8 text-center cursor-pointer transition-colors
|
||||||
|
${isDragActive ? 'border-blue-500 bg-blue-500/10' : 'border-gray-600 hover:border-blue-500'}
|
||||||
|
`}
|
||||||
|
>
|
||||||
|
<input {...getInputProps()} />
|
||||||
|
<p className="text-gray-300">
|
||||||
|
{isDragActive
|
||||||
|
? 'Lass das Bild hier fallen...'
|
||||||
|
: 'Ziehe ein Bild hierher oder klicke zum Auswählen'}
|
||||||
|
</p>
|
||||||
|
{selectedFile && (
|
||||||
|
<p className="mt-2 text-blue-400">
|
||||||
|
Ausgewählt: {selectedFile.name}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Format Auswahl */}
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-300 mb-2">
|
||||||
|
Zielformat
|
||||||
|
</label>
|
||||||
|
<select
|
||||||
|
value={convertTo}
|
||||||
|
onChange={(e) => setConvertTo(e.target.value)}
|
||||||
|
className="w-full bg-zinc-700 text-white rounded-md border border-gray-600 p-2"
|
||||||
|
>
|
||||||
|
<option value="png">PNG</option>
|
||||||
|
<option value="jpg">JPG</option>
|
||||||
|
<option value="webp">WebP</option>
|
||||||
|
<option value="gif">GIF</option>
|
||||||
|
<option value="bmp">BMP</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Error Message */}
|
||||||
|
{error && (
|
||||||
|
<div className="text-red-500 text-sm p-3 bg-red-500/10 rounded-md border border-red-500/20">
|
||||||
|
{error}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Konvertieren Button */}
|
||||||
|
<button
|
||||||
|
onClick={handleConvert}
|
||||||
|
disabled={!selectedFile || isConverting}
|
||||||
|
className={`w-full py-2 px-4 rounded-md text-white transition-colors
|
||||||
|
${!selectedFile || isConverting
|
||||||
|
? 'bg-blue-500/50 cursor-not-allowed'
|
||||||
|
: 'bg-blue-500 hover:bg-blue-600'}
|
||||||
|
`}
|
||||||
|
>
|
||||||
|
{isConverting ? 'Konvertiere...' : 'Konvertieren'}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
)
|
||||||
|
}
|
||||||
98
app/components/AddWebsite.tsx
Normal file
98
app/components/AddWebsite.tsx
Normal file
@@ -0,0 +1,98 @@
|
|||||||
|
'use client'
|
||||||
|
import { useState } from 'react'
|
||||||
|
import { PinnedWebsite } from '../types/types'
|
||||||
|
|
||||||
|
export default function AddWebsite() {
|
||||||
|
const [url, setUrl] = useState('')
|
||||||
|
const [title, setTitle] = useState('')
|
||||||
|
const [isAdding, setIsAdding] = useState(false)
|
||||||
|
|
||||||
|
const handleSubmit = (e: React.FormEvent) => {
|
||||||
|
e.preventDefault()
|
||||||
|
|
||||||
|
// Validiere URL
|
||||||
|
try {
|
||||||
|
new URL(url)
|
||||||
|
} catch {
|
||||||
|
alert('Please enter a valid URL')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const newWebsite: PinnedWebsite = {
|
||||||
|
id: Date.now().toString(),
|
||||||
|
title: title || url,
|
||||||
|
url: url.startsWith('http') ? url : `https://${url}`,
|
||||||
|
addedAt: Date.now()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Lade bestehende Websites
|
||||||
|
const existingWebsites = JSON.parse(localStorage.getItem('pinnedWebsites') || '[]')
|
||||||
|
|
||||||
|
// Füge neue Website hinzu
|
||||||
|
localStorage.setItem('pinnedWebsites', JSON.stringify([...existingWebsites, newWebsite]))
|
||||||
|
|
||||||
|
// Setze Formular zurück
|
||||||
|
setUrl('')
|
||||||
|
setTitle('')
|
||||||
|
setIsAdding(false)
|
||||||
|
|
||||||
|
// Lade die Seite neu um die neue Website anzuzeigen
|
||||||
|
window.location.reload()
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="mb-6">
|
||||||
|
{!isAdding ? (
|
||||||
|
<button
|
||||||
|
onClick={() => setIsAdding(true)}
|
||||||
|
className="w-full p-4 border border-dashed border-gray-700 rounded-lg text-gray-400 hover:border-blue-500 hover:text-blue-500 transition-colors"
|
||||||
|
>
|
||||||
|
+ Add New Website
|
||||||
|
</button>
|
||||||
|
) : (
|
||||||
|
<form onSubmit={handleSubmit} className="bg-zinc-800 p-4 rounded-lg space-y-4">
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-300 mb-1">
|
||||||
|
URL
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={url}
|
||||||
|
onChange={(e) => setUrl(e.target.value)}
|
||||||
|
placeholder="https://example.com"
|
||||||
|
className="w-full bg-zinc-700 text-white rounded-md border border-gray-600 p-2"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-300 mb-1">
|
||||||
|
Title (optional)
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={title}
|
||||||
|
onChange={(e) => setTitle(e.target.value)}
|
||||||
|
placeholder="My Website"
|
||||||
|
className="w-full bg-zinc-700 text-white rounded-md border border-gray-600 p-2"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="flex space-x-3">
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
className="flex-1 bg-blue-500 text-white py-2 px-4 rounded-md hover:bg-blue-600 transition-colors"
|
||||||
|
>
|
||||||
|
Add
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => setIsAdding(false)}
|
||||||
|
className="flex-1 bg-zinc-700 text-white py-2 px-4 rounded-md hover:bg-zinc-600 transition-colors"
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
23
app/components/Navbar.tsx
Normal file
23
app/components/Navbar.tsx
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
'use client'
|
||||||
|
import React from 'react'
|
||||||
|
import Link from 'next/link'
|
||||||
|
|
||||||
|
export default function Navbar() {
|
||||||
|
return (
|
||||||
|
<nav className="bg-blue-900 p-4 shadow-lg">
|
||||||
|
<div className="container mx-auto">
|
||||||
|
<div className="flex justify-between items-center">
|
||||||
|
<div className="flex-1" /> {/* Spacer */}
|
||||||
|
<Link href="/" className="text-white font-bold text-xl hover:text-blue-100">
|
||||||
|
START
|
||||||
|
</Link>
|
||||||
|
<div className="flex-1 flex justify-end">
|
||||||
|
<Link href="/about" className="text-white hover:text-blue-100">
|
||||||
|
About
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
)
|
||||||
|
}
|
||||||
14
app/globals.css
Normal file
14
app/globals.css
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
@tailwind base;
|
||||||
|
@tailwind components;
|
||||||
|
@tailwind utilities;
|
||||||
|
|
||||||
|
html, body {
|
||||||
|
@apply bg-zinc-900;
|
||||||
|
height: 100%;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
main {
|
||||||
|
height: calc(100vh - 64px); /* 64px ist die Höhe der Navbar */
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
25
app/layout.tsx
Normal file
25
app/layout.tsx
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
import './globals.css'
|
||||||
|
import { Inter } from 'next/font/google'
|
||||||
|
import Navbar from './components/Navbar'
|
||||||
|
|
||||||
|
const inter = Inter({ subsets: ['latin'] })
|
||||||
|
|
||||||
|
export const metadata = {
|
||||||
|
title: 'Tool Website',
|
||||||
|
description: 'Eine Sammlung nützlicher Tools',
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function RootLayout({
|
||||||
|
children,
|
||||||
|
}: {
|
||||||
|
children: React.ReactNode
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<html lang="de">
|
||||||
|
<body className={inter.className}>
|
||||||
|
<Navbar />
|
||||||
|
{children}
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
)
|
||||||
|
}
|
||||||
100
app/page.tsx
Normal file
100
app/page.tsx
Normal file
@@ -0,0 +1,100 @@
|
|||||||
|
'use client'
|
||||||
|
import { useEffect, useState } from 'react'
|
||||||
|
import Link from 'next/link'
|
||||||
|
import { PinnedWebsite } from './types/types'
|
||||||
|
import AddWebsite from './components/AddWebsite'
|
||||||
|
|
||||||
|
export default function Home() {
|
||||||
|
const [pinnedWebsites, setPinnedWebsites] = useState<PinnedWebsite[]>([])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const saved = localStorage.getItem('pinnedWebsites')
|
||||||
|
if (saved) {
|
||||||
|
setPinnedWebsites(JSON.parse(saved))
|
||||||
|
}
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
const handleDelete = (id: string) => {
|
||||||
|
const newWebsites = pinnedWebsites.filter(site => site.id !== id)
|
||||||
|
localStorage.setItem('pinnedWebsites', JSON.stringify(newWebsites))
|
||||||
|
setPinnedWebsites(newWebsites)
|
||||||
|
}
|
||||||
|
|
||||||
|
const tools = [
|
||||||
|
{
|
||||||
|
name: 'Password Generator',
|
||||||
|
file: '/passwortgenerator',
|
||||||
|
description: 'Create secure passwords with custom options',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Image Converter',
|
||||||
|
file: '/bildconverter',
|
||||||
|
description: 'Convert images between different formats',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'More Coming Soon',
|
||||||
|
file: '/about',
|
||||||
|
description: 'Stay tuned for new tools!',
|
||||||
|
isComingSoon: true
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
return (
|
||||||
|
<main className="min-h-screen p-8 text-white">
|
||||||
|
<div className="max-w-7xl mx-auto space-y-12">
|
||||||
|
{/* Tools Section */}
|
||||||
|
<section>
|
||||||
|
<h2 className="text-2xl font-bold mb-6">Tools</h2>
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||||
|
{tools.map((tool, index) => (
|
||||||
|
<Link
|
||||||
|
key={index}
|
||||||
|
href={tool.file}
|
||||||
|
className={`p-6 rounded-lg border border-gray-700 bg-zinc-800 transition-colors duration-200 cursor-pointer
|
||||||
|
${tool.isComingSoon
|
||||||
|
? 'opacity-50 hover:opacity-75'
|
||||||
|
: 'hover:border-blue-500'}`}
|
||||||
|
>
|
||||||
|
<h2 className="text-xl font-semibold mb-2">{tool.name}</h2>
|
||||||
|
<p className="text-gray-400">{tool.description}</p>
|
||||||
|
</Link>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
{/* Pinned Websites Section */}
|
||||||
|
<section>
|
||||||
|
<h2 className="text-2xl font-bold mb-6">Bookmarked Websites</h2>
|
||||||
|
<AddWebsite />
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||||
|
{pinnedWebsites.map((website) => (
|
||||||
|
<div
|
||||||
|
key={website.id}
|
||||||
|
className="group relative p-6 rounded-lg border border-gray-700 bg-zinc-800 hover:border-blue-500 transition-colors"
|
||||||
|
>
|
||||||
|
<a
|
||||||
|
href={website.url}
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
className="block"
|
||||||
|
>
|
||||||
|
<h3 className="text-xl font-semibold mb-2">{website.title}</h3>
|
||||||
|
<p className="text-gray-400 truncate">{website.url}</p>
|
||||||
|
</a>
|
||||||
|
<button
|
||||||
|
onClick={(e) => {
|
||||||
|
e.preventDefault()
|
||||||
|
handleDelete(website.id)
|
||||||
|
}}
|
||||||
|
className="absolute top-2 right-2 opacity-0 group-hover:opacity-100 text-gray-400 hover:text-red-500 transition-opacity"
|
||||||
|
>
|
||||||
|
✕
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
)
|
||||||
|
}
|
||||||
103
app/passwortgenerator/page.tsx
Normal file
103
app/passwortgenerator/page.tsx
Normal file
@@ -0,0 +1,103 @@
|
|||||||
|
'use client'
|
||||||
|
import { useState } from 'react'
|
||||||
|
|
||||||
|
export default function PasswordGenerator() {
|
||||||
|
const [password, setPassword] = useState('')
|
||||||
|
const [length, setLength] = useState(12)
|
||||||
|
const [includeNumbers, setIncludeNumbers] = useState(true)
|
||||||
|
const [includeSymbols, setIncludeSymbols] = useState(true)
|
||||||
|
|
||||||
|
const generatePassword = () => {
|
||||||
|
const numbers = '0123456789'
|
||||||
|
const symbols = '!@#$%^&*()_+-=[]{}|;:,.<>?'
|
||||||
|
const letters = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ'
|
||||||
|
|
||||||
|
let chars = letters
|
||||||
|
if (includeNumbers) chars += numbers
|
||||||
|
if (includeSymbols) chars += symbols
|
||||||
|
|
||||||
|
let newPassword = ''
|
||||||
|
for (let i = 0; i < length; i++) {
|
||||||
|
newPassword += chars[Math.floor(Math.random() * chars.length)]
|
||||||
|
}
|
||||||
|
|
||||||
|
setPassword(newPassword)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<main className="min-h-screen p-8">
|
||||||
|
<div className="max-w-2xl mx-auto">
|
||||||
|
<h1 className="text-4xl font-bold mb-8">Passwort Generator</h1>
|
||||||
|
|
||||||
|
<div className="bg-white rounded-lg shadow-md p-6">
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-700">
|
||||||
|
Passwortlänge: {length}
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="range"
|
||||||
|
min="8"
|
||||||
|
max="32"
|
||||||
|
value={length}
|
||||||
|
onChange={(e) => setLength(Number(e.target.value))}
|
||||||
|
className="w-full mt-2"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex space-x-4">
|
||||||
|
<label className="flex items-center">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={includeNumbers}
|
||||||
|
onChange={(e) => setIncludeNumbers(e.target.checked)}
|
||||||
|
className="rounded border-gray-300 text-blue-600 mr-2"
|
||||||
|
/>
|
||||||
|
Zahlen
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<label className="flex items-center">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={includeSymbols}
|
||||||
|
onChange={(e) => setIncludeSymbols(e.target.checked)}
|
||||||
|
className="rounded border-gray-300 text-blue-600 mr-2"
|
||||||
|
/>
|
||||||
|
Sonderzeichen
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button
|
||||||
|
onClick={generatePassword}
|
||||||
|
className="w-full bg-blue-600 text-white py-2 px-4 rounded-md hover:bg-blue-700 transition-colors"
|
||||||
|
>
|
||||||
|
Passwort generieren
|
||||||
|
</button>
|
||||||
|
|
||||||
|
{password && (
|
||||||
|
<div className="mt-4">
|
||||||
|
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||||
|
Generiertes Passwort:
|
||||||
|
</label>
|
||||||
|
<div className="flex">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
readOnly
|
||||||
|
value={password}
|
||||||
|
className="flex-1 border rounded-l-md p-2"
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
onClick={() => navigator.clipboard.writeText(password)}
|
||||||
|
className="bg-gray-100 px-4 rounded-r-md border-t border-r border-b hover:bg-gray-200"
|
||||||
|
>
|
||||||
|
Kopieren
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
)
|
||||||
|
}
|
||||||
7
app/types/types.ts
Normal file
7
app/types/types.ts
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
export type PinnedWebsite = {
|
||||||
|
id: string;
|
||||||
|
title: string;
|
||||||
|
url: string;
|
||||||
|
icon?: string;
|
||||||
|
addedAt: number;
|
||||||
|
}
|
||||||
3257
package-lock.json
generated
Normal file
3257
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
28
package.json
Normal file
28
package.json
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
{
|
||||||
|
"name": "tool-website",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"private": true,
|
||||||
|
"scripts": {
|
||||||
|
"dev": "next dev",
|
||||||
|
"build": "next build",
|
||||||
|
"start": "next start",
|
||||||
|
"lint": "next lint"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@tailwindcss/postcss": "^4.1.7",
|
||||||
|
"next": "^14.0.0",
|
||||||
|
"react": "^18.2.0",
|
||||||
|
"react-dom": "^18.2.0",
|
||||||
|
"react-dropzone": "^14.3.8",
|
||||||
|
"sharp": "^0.34.1"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/node": "^20.0.0",
|
||||||
|
"@types/react": "^18.2.0",
|
||||||
|
"@types/react-dom": "^18.2.0",
|
||||||
|
"autoprefixer": "^10.4.0",
|
||||||
|
"postcss": "^8.5.3",
|
||||||
|
"tailwindcss": "^3.4.17",
|
||||||
|
"typescript": "^5.0.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
6
postcss.config.js
Normal file
6
postcss.config.js
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
module.exports = {
|
||||||
|
plugins: {
|
||||||
|
tailwindcss: {},
|
||||||
|
autoprefixer: {},
|
||||||
|
},
|
||||||
|
}
|
||||||
6
postcss.config.mjs
Normal file
6
postcss.config.mjs
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
const config = {
|
||||||
|
plugins: {
|
||||||
|
"@tailwindcss/postcss": {},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
export default config;
|
||||||
28
start.js
Normal file
28
start.js
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
const { exec } = require('child_process');
|
||||||
|
const os = require('os');
|
||||||
|
|
||||||
|
// Starte den Entwicklungsserver
|
||||||
|
exec('npm run dev', (error, stdout, stderr) => {
|
||||||
|
if (error) {
|
||||||
|
console.error(`Fehler beim Starten des Servers: ${error}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Warte kurz und öffne dann den Browser
|
||||||
|
setTimeout(() => {
|
||||||
|
const platform = os.platform();
|
||||||
|
const url = 'http://localhost:3000';
|
||||||
|
|
||||||
|
switch (platform) {
|
||||||
|
case 'win32':
|
||||||
|
exec(`start ${url}`);
|
||||||
|
break;
|
||||||
|
case 'darwin':
|
||||||
|
exec(`open ${url}`);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
exec(`xdg-open ${url}`);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}, 3000); // Warte 3 Sekunden, bis der Server gestartet ist
|
||||||
12
tailwind.config.js
Normal file
12
tailwind.config.js
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
/** @type {import('tailwindcss').Config} */
|
||||||
|
module.exports = {
|
||||||
|
content: [
|
||||||
|
'./pages/**/*.{js,ts,jsx,tsx,mdx}',
|
||||||
|
'./components/**/*.{js,ts,jsx,tsx,mdx}',
|
||||||
|
'./app/**/*.{js,ts,jsx,tsx,mdx}',
|
||||||
|
],
|
||||||
|
theme: {
|
||||||
|
extend: {},
|
||||||
|
},
|
||||||
|
plugins: [],
|
||||||
|
}
|
||||||
28
tsconfig.json
Normal file
28
tsconfig.json
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "es5",
|
||||||
|
"lib": ["dom", "dom.iterable", "esnext"],
|
||||||
|
"allowJs": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"strict": true,
|
||||||
|
"forceConsistentCasingInFileNames": true,
|
||||||
|
"noEmit": true,
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"module": "esnext",
|
||||||
|
"moduleResolution": "node",
|
||||||
|
"resolveJsonModule": true,
|
||||||
|
"isolatedModules": true,
|
||||||
|
"jsx": "preserve",
|
||||||
|
"incremental": true,
|
||||||
|
"plugins": [
|
||||||
|
{
|
||||||
|
"name": "next"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"paths": {
|
||||||
|
"@/*": ["./*"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
|
||||||
|
"exclude": ["node_modules"]
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user