add some tools
This commit is contained in:
parent
42cdf41f47
commit
6db68cf014
47
app/tools/CatgirlGenerator/page.tsx
Normal file
47
app/tools/CatgirlGenerator/page.tsx
Normal file
@ -0,0 +1,47 @@
|
||||
"use client";
|
||||
|
||||
import NicknameGenerator from '@/components/tools/CatgirlGenerator/NicknameGenerator'
|
||||
import TextReversal from '@/components/tools/CatgirlGenerator/TextReversal'
|
||||
import CustomTemplate from '@/components/tools/CatgirlGenerator/CustomTemplate'
|
||||
import DarkModeToggle from '@/components/tools/CatgirlGenerator/DarkModeToggle'
|
||||
import Footer from "@/components/tools/CatgirlGenerator/Footer";
|
||||
export default function Home() {
|
||||
return (
|
||||
<div className="min-h-screen bg-gray-100 dark:bg-gray-900 transition-colors duration-300">
|
||||
<div className="container mx-auto px-4 py-8">
|
||||
<header className="mb-8 flex justify-between items-center">
|
||||
<h1 className="text-4xl font-bold text-gray-800 dark:text-white">扩展昵称生成器</h1>
|
||||
<DarkModeToggle />
|
||||
</header>
|
||||
<main>
|
||||
<section className="mb-12">
|
||||
<h2 className="text-2xl font-semibold mb-4 text-gray-800 dark:text-white">原理说明</h2>
|
||||
<p className="text-gray-600 dark:text-gray-300 mb-4">
|
||||
效果:当别人@你时,会在@的文字后面加上一另段文字。下图结尾的「喵~」不是发送者输入的。
|
||||
</p>
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-4 mb-4">
|
||||
{['step1.jpg', 'step2.jpg', 'example.jpg'].map((img, index) => (
|
||||
<img
|
||||
key={index}
|
||||
src={`/img/CatgirlGenerator/${img}`}
|
||||
alt={`Example ${index + 1}`}
|
||||
className="w-full h-auto rounded-lg shadow-md transition-transform duration-300 hover:scale-105"
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
<p className="text-gray-600 dark:text-gray-300">
|
||||
原理:用unicode控制字符 <code className="bg-gray-200 dark:bg-gray-700 px-1 rounded">RLI</code> 和
|
||||
<code className="bg-gray-200 dark:bg-gray-700 px-1 rounded">LRI</code> 包裹倒序的后缀部分,
|
||||
在别人@后,@后面的文本会跑到被包裹部分的前面去。
|
||||
</p>
|
||||
</section>
|
||||
<NicknameGenerator />
|
||||
<TextReversal />
|
||||
<CustomTemplate />
|
||||
</main>
|
||||
</div>
|
||||
<Footer />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
55
app/tools/Compressor/page.tsx
Normal file
55
app/tools/Compressor/page.tsx
Normal file
@ -0,0 +1,55 @@
|
||||
'use client'
|
||||
|
||||
import { useState } from 'react'
|
||||
import { ThemeProvider } from 'next-themes'
|
||||
import FileUpload from '@/components/tools/compressor/FileUpload'
|
||||
import CompressionOptions from '@/components/tools/compressor/CompressionOptions'
|
||||
import CompressionResult from '@/components/tools/compressor/CompressionResult'
|
||||
import ThemeToggle from '@/components/tools/compressor/ThemeToggle'
|
||||
import Instructions from '@/components/tools/compressor/Instructions'
|
||||
import useImageCompression from '@/hooks/useImageCompression'
|
||||
|
||||
export default function ImageCompressor() {
|
||||
const [file, setFile] = useState<File | null>(null)
|
||||
const { compressImage, compressedImage, isCompressing, progress, error } = useImageCompression()
|
||||
|
||||
const handleCompress = async (format: string) => {
|
||||
if (file) {
|
||||
await compressImage(file, format)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<ThemeProvider attribute="class">
|
||||
<div className="min-h-screen bg-gray-100 dark:bg-gray-900 py-12 px-4 sm:px-6 lg:px-8 transition-colors duration-200">
|
||||
<div className="max-w-3xl mx-auto">
|
||||
<div className="flex justify-between items-center mb-8">
|
||||
<h1 className="text-3xl font-bold text-gray-900 dark:text-white">图片压缩工具</h1>
|
||||
<ThemeToggle />
|
||||
</div>
|
||||
<div className="bg-white dark:bg-gray-800 shadow-md rounded-lg p-6 transition-colors duration-200">
|
||||
<Instructions />
|
||||
<FileUpload onFileSelect={setFile} selectedFile={file} />
|
||||
{file && (
|
||||
<CompressionOptions
|
||||
onCompress={handleCompress}
|
||||
isCompressing={isCompressing}
|
||||
progress={progress}
|
||||
/>
|
||||
)}
|
||||
{compressedImage && (
|
||||
<CompressionResult
|
||||
originalImage={file!}
|
||||
compressedImage={compressedImage}
|
||||
/>
|
||||
)}
|
||||
{error && (
|
||||
<p className="mt-4 text-red-600 dark:text-red-400 text-center">{error}</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</ThemeProvider>
|
||||
)
|
||||
}
|
||||
|
119
app/tools/hash-random/page.tsx
Normal file
119
app/tools/hash-random/page.tsx
Normal file
@ -0,0 +1,119 @@
|
||||
"use client";
|
||||
|
||||
import { useState, useEffect } from "react";
|
||||
import RandomSeed from "@/components/tools/hash-random/RandomSeed";
|
||||
import ParticipantList from "@/components/tools/hash-random/ParticipantList";
|
||||
import ResultDisplay from "@/components/tools/hash-random/ResultDisplay";
|
||||
import HashTable from "@/components/tools/hash-random/HashTable";
|
||||
import CoreAlgorithm from "@/components/tools/hash-random/CoreAlgorithm";
|
||||
import { useHashCalculation } from "@/hooks/useHashCalculation";
|
||||
import DarkModeToggle from "@/components/tools/hash-random/DarkModeToggle";
|
||||
import SaveLoadSelection from "@/components/tools/hash-random/SaveLoadSelection";
|
||||
import Footer from "@/components/tools/hash-random/Footer";
|
||||
import { AnimatePresence, motion } from "framer-motion";
|
||||
|
||||
export default function Home() {
|
||||
const [salt, setSalt] = useState("");
|
||||
const [count, setCount] = useState(1);
|
||||
const [participants, setParticipants] = useState<string[]>([]);
|
||||
const { sortedResults, saltHash, isCalculating } = useHashCalculation(
|
||||
salt,
|
||||
participants,
|
||||
count
|
||||
);
|
||||
const [darkMode, setDarkMode] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
const isDarkMode = localStorage.getItem("darkMode") === "true";
|
||||
setDarkMode(isDarkMode);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
document.documentElement.classList.toggle("dark", darkMode);
|
||||
localStorage.setItem("darkMode", darkMode.toString());
|
||||
}, [darkMode]);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`min-h-screen transition-colors duration-300 ${
|
||||
darkMode
|
||||
? "dark bg-gray-900"
|
||||
: "bg-gradient-to-br from-blue-50 to-indigo-100"
|
||||
}`}
|
||||
>
|
||||
<div className="container mx-auto px-4 py-12">
|
||||
<div className="flex justify-between items-center mb-8">
|
||||
<h1 className="text-5xl font-extrabold text-indigo-800 dark:text-indigo-200">
|
||||
哈希随机抽取
|
||||
</h1>
|
||||
<DarkModeToggle darkMode={darkMode} setDarkMode={setDarkMode} />
|
||||
</div>
|
||||
<div className="flex justify-center space-x-4 mb-12">
|
||||
<span className="bg-indigo-500 text-white px-4 py-2 rounded-full text-sm font-semibold shadow-md">
|
||||
事前不可知
|
||||
</span>
|
||||
<span className="bg-green-500 text-white px-4 py-2 rounded-full text-sm font-semibold shadow-md">
|
||||
事后可复现
|
||||
</span>
|
||||
<span className="bg-purple-500 text-white px-4 py-2 rounded-full text-sm font-semibold shadow-md">
|
||||
高度随机化
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-8">
|
||||
<div className="space-y-8">
|
||||
<RandomSeed
|
||||
salt={salt}
|
||||
setSalt={setSalt}
|
||||
count={count}
|
||||
setCount={setCount}
|
||||
/>
|
||||
<ParticipantList setParticipants={setParticipants} />
|
||||
<SaveLoadSelection
|
||||
salt={salt}
|
||||
setSalt={setSalt}
|
||||
count={count}
|
||||
setCount={setCount}
|
||||
participants={participants}
|
||||
setParticipants={setParticipants}
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-8">
|
||||
<AnimatePresence mode="wait">
|
||||
{isCalculating ? (
|
||||
<motion.div
|
||||
key="loading"
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
exit={{ opacity: 0 }}
|
||||
className="flex justify-center items-center h-64"
|
||||
>
|
||||
<div className="loader ease-linear rounded-full border-8 border-t-8 border-gray-200 h-24 w-24"></div>
|
||||
</motion.div>
|
||||
) : (
|
||||
<motion.div
|
||||
key="results"
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
exit={{ opacity: 0 }}
|
||||
>
|
||||
<ResultDisplay
|
||||
results={sortedResults.slice(0, count)}
|
||||
saltHash={saltHash}
|
||||
/>
|
||||
<HashTable
|
||||
results={sortedResults.slice(0, 1000)}
|
||||
saltHash={saltHash}
|
||||
/>
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<CoreAlgorithm />
|
||||
</div>
|
||||
<Footer />
|
||||
</div>
|
||||
);
|
||||
}
|
@ -33,7 +33,7 @@ export default function Component() {
|
||||
useEffect(() => {
|
||||
async function loadData() {
|
||||
try {
|
||||
const response = await fetch('https://aps.icu/api/load-yaml-data');
|
||||
const response = await fetch('/api/load-yaml-data');
|
||||
const yamlData = await response.json();
|
||||
setData(yamlData);
|
||||
} catch (error) {
|
||||
|
69
components/tools/CatgirlGenerator/CustomTemplate.tsx
Normal file
69
components/tools/CatgirlGenerator/CustomTemplate.tsx
Normal file
@ -0,0 +1,69 @@
|
||||
'use client'
|
||||
|
||||
import { useState } from 'react'
|
||||
import { motion } from 'framer-motion'
|
||||
import Toast from './Toast'
|
||||
|
||||
export default function CustomTemplate() {
|
||||
const [input, setInput] = useState('')
|
||||
const [output, setOutput] = useState('')
|
||||
const [showToast, setShowToast] = useState(false)
|
||||
|
||||
const generateTemplate = () => {
|
||||
setOutput(input.replace(/#r/g, '\u2067').replace(/#l/g, '\u2066'))
|
||||
}
|
||||
|
||||
const copyToClipboard = () => {
|
||||
navigator.clipboard.writeText(output)
|
||||
setShowToast(true)
|
||||
setTimeout(() => setShowToast(false), 3000)
|
||||
}
|
||||
|
||||
return (
|
||||
<motion.section
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.5, delay: 0.4 }}
|
||||
className="mb-12 bg-white dark:bg-gray-800 p-6 rounded-lg shadow-md"
|
||||
>
|
||||
<h2 className="text-2xl font-semibold mb-4 text-gray-800 dark:text-white">自定义模板</h2>
|
||||
<p className="text-gray-600 dark:text-gray-300 mb-4">
|
||||
使用 <code className="bg-gray-200 dark:bg-gray-700 px-1 rounded">#r</code> 表示 RLI 字符,
|
||||
使用 <code className="bg-gray-200 dark:bg-gray-700 px-1 rounded">#l</code> 表示 LRI 字符。
|
||||
</p>
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<label htmlFor="template" className="block text-sm font-medium text-gray-700 dark:text-gray-300">输入模板字符串</label>
|
||||
<input
|
||||
type="text"
|
||||
id="template"
|
||||
value={input}
|
||||
onChange={(e) => setInput(e.target.value)}
|
||||
onKeyUp={generateTemplate}
|
||||
className="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-300 focus:ring focus:ring-indigo-200 focus:ring-opacity-50 dark:bg-gray-700 dark:border-gray-600 dark:text-white"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label htmlFor="result" className="block text-sm font-medium text-gray-700 dark:text-gray-300">输出</label>
|
||||
<div className="mt-1 flex rounded-md shadow-sm">
|
||||
<input
|
||||
type="text"
|
||||
id="result"
|
||||
value={output}
|
||||
readOnly
|
||||
className="flex-1 rounded-none rounded-l-md border-gray-300 focus:border-indigo-300 focus:ring focus:ring-indigo-200 focus:ring-opacity-50 dark:bg-gray-700 dark:border-gray-600 dark:text-white"
|
||||
/>
|
||||
<button
|
||||
onClick={copyToClipboard}
|
||||
className="inline-flex items-center px-3 rounded-r-md border border-l-0 border-gray-300 bg-gray-50 text-gray-500 text-sm dark:bg-gray-600 dark:border-gray-500 dark:text-gray-300"
|
||||
>
|
||||
复制
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<Toast message="复制成功!" isVisible={showToast} />
|
||||
</motion.section>
|
||||
)
|
||||
}
|
||||
|
36
components/tools/CatgirlGenerator/DarkModeToggle.tsx
Normal file
36
components/tools/CatgirlGenerator/DarkModeToggle.tsx
Normal file
@ -0,0 +1,36 @@
|
||||
'use client'
|
||||
|
||||
import { useState, useEffect } from 'react'
|
||||
import { motion } from 'framer-motion'
|
||||
import { Moon, Sun } from 'lucide-react'
|
||||
|
||||
export default function DarkModeToggle() {
|
||||
const [darkMode, setDarkMode] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
if (typeof window !== 'undefined') {
|
||||
const isDarkMode = localStorage.getItem('darkMode') === 'true'
|
||||
setDarkMode(isDarkMode)
|
||||
document.documentElement.classList.toggle('dark', isDarkMode)
|
||||
}
|
||||
}, [])
|
||||
|
||||
const toggleDarkMode = () => {
|
||||
const newDarkMode = !darkMode
|
||||
setDarkMode(newDarkMode)
|
||||
localStorage.setItem('darkMode', newDarkMode.toString())
|
||||
document.documentElement.classList.toggle('dark', newDarkMode)
|
||||
}
|
||||
|
||||
return (
|
||||
<motion.button
|
||||
onClick={toggleDarkMode}
|
||||
className="p-2 rounded-full bg-gray-200 dark:bg-gray-700 text-gray-800 dark:text-gray-200"
|
||||
whileHover={{ scale: 1.1 }}
|
||||
whileTap={{ scale: 0.9 }}
|
||||
>
|
||||
{darkMode ? <Sun size={24} /> : <Moon size={24} />}
|
||||
</motion.button>
|
||||
)
|
||||
}
|
||||
|
22
components/tools/CatgirlGenerator/Footer.tsx
Normal file
22
components/tools/CatgirlGenerator/Footer.tsx
Normal file
@ -0,0 +1,22 @@
|
||||
export default function Footer() {
|
||||
return (
|
||||
<footer className="bg-white dark:bg-gray-800 shadow-md mt-12 py-6 transition-colors duration-200">
|
||||
<div className="container mx-auto px-4 text-center">
|
||||
<p className="text-gray-600 dark:text-gray-300">
|
||||
© {new Date().getFullYear()} APS NAV.
|
||||
</p>
|
||||
<p className="text-gray-600 dark:text-gray-300 mt-2">
|
||||
创意来源:{" "}
|
||||
<a
|
||||
href="https://github.com/TaylorAndTony/CatgirlGenerator"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-indigo-600 dark:text-indigo-400 hover:underline"
|
||||
>
|
||||
TaylorAndTony
|
||||
</a>
|
||||
</p>
|
||||
</div>
|
||||
</footer>
|
||||
);
|
||||
}
|
102
components/tools/CatgirlGenerator/NicknameGenerator.tsx
Normal file
102
components/tools/CatgirlGenerator/NicknameGenerator.tsx
Normal file
@ -0,0 +1,102 @@
|
||||
'use client'
|
||||
|
||||
import { useState } from 'react'
|
||||
import { motion } from 'framer-motion'
|
||||
import Toast from './Toast'
|
||||
|
||||
export default function NicknameGenerator() {
|
||||
const [prefix, setPrefix] = useState('爱蜜莉雅')
|
||||
const [suffix, setSuffix] = useState('碳~')
|
||||
const [result, setResult] = useState('')
|
||||
const [result2, setResult2] = useState('')
|
||||
const [showToast, setShowToast] = useState(false)
|
||||
|
||||
const generateNickname = () => {
|
||||
const wrapped = '\u2067' + suffix.split('').reverse().join('') + '\u2066'
|
||||
setResult(prefix + wrapped)
|
||||
setResult2(prefix + '\u2067' + suffix + '\u2066')
|
||||
}
|
||||
|
||||
const copyToClipboard = (text: string) => {
|
||||
navigator.clipboard.writeText(text)
|
||||
setShowToast(true)
|
||||
setTimeout(() => setShowToast(false), 3000)
|
||||
}
|
||||
|
||||
return (
|
||||
<motion.section
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.5 }}
|
||||
className="mb-12 bg-white dark:bg-gray-800 p-6 rounded-lg shadow-md"
|
||||
>
|
||||
<h2 className="text-2xl font-semibold mb-4 text-gray-800 dark:text-white">基本生成</h2>
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<label htmlFor="prefix" className="block text-sm font-medium text-gray-700 dark:text-gray-300">前半部分</label>
|
||||
<input
|
||||
type="text"
|
||||
id="prefix"
|
||||
value={prefix}
|
||||
onChange={(e) => setPrefix(e.target.value)}
|
||||
className="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-300 focus:ring focus:ring-indigo-200 focus:ring-opacity-50 dark:bg-gray-700 dark:border-gray-600 dark:text-white"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label htmlFor="suffix" className="block text-sm font-medium text-gray-700 dark:text-gray-300">后半部分</label>
|
||||
<input
|
||||
type="text"
|
||||
id="suffix"
|
||||
value={suffix}
|
||||
onChange={(e) => setSuffix(e.target.value)}
|
||||
className="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-300 focus:ring focus:ring-indigo-200 focus:ring-opacity-50 dark:bg-gray-700 dark:border-gray-600 dark:text-white"
|
||||
/>
|
||||
</div>
|
||||
<button
|
||||
onClick={generateNickname}
|
||||
className="px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-opacity-50 transition-colors duration-300"
|
||||
>
|
||||
生成
|
||||
</button>
|
||||
<div>
|
||||
<label htmlFor="result" className="block text-sm font-medium text-gray-700 dark:text-gray-300">结果1</label>
|
||||
<div className="mt-1 flex rounded-md shadow-sm">
|
||||
<input
|
||||
type="text"
|
||||
id="result"
|
||||
value={result}
|
||||
readOnly
|
||||
className="flex-1 rounded-none rounded-l-md border-gray-300 focus:border-indigo-300 focus:ring focus:ring-indigo-200 focus:ring-opacity-50 dark:bg-gray-700 dark:border-gray-600 dark:text-white"
|
||||
/>
|
||||
<button
|
||||
onClick={() => copyToClipboard(result)}
|
||||
className="inline-flex items-center px-3 rounded-r-md border border-l-0 border-gray-300 bg-gray-50 text-gray-500 text-sm dark:bg-gray-600 dark:border-gray-500 dark:text-gray-300"
|
||||
>
|
||||
复制
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<label htmlFor="result2" className="block text-sm font-medium text-gray-700 dark:text-gray-300">结果2(修复版)</label>
|
||||
<div className="mt-1 flex rounded-md shadow-sm">
|
||||
<input
|
||||
type="text"
|
||||
id="result2"
|
||||
value={result2}
|
||||
readOnly
|
||||
className="flex-1 rounded-none rounded-l-md border-gray-300 focus:border-indigo-300 focus:ring focus:ring-indigo-200 focus:ring-opacity-50 dark:bg-gray-700 dark:border-gray-600 dark:text-white"
|
||||
/>
|
||||
<button
|
||||
onClick={() => copyToClipboard(result2)}
|
||||
className="inline-flex items-center px-3 rounded-r-md border border-l-0 border-gray-300 bg-gray-50 text-gray-500 text-sm dark:bg-gray-600 dark:border-gray-500 dark:text-gray-300"
|
||||
>
|
||||
复制
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<Toast message="复制成功!" isVisible={showToast} />
|
||||
</motion.section>
|
||||
)
|
||||
}
|
||||
|
65
components/tools/CatgirlGenerator/TextReversal.tsx
Normal file
65
components/tools/CatgirlGenerator/TextReversal.tsx
Normal file
@ -0,0 +1,65 @@
|
||||
'use client'
|
||||
|
||||
import { useState } from 'react'
|
||||
import { motion } from 'framer-motion'
|
||||
import Toast from './Toast'
|
||||
|
||||
export default function TextReversal() {
|
||||
const [input, setInput] = useState('')
|
||||
const [output, setOutput] = useState('')
|
||||
const [showToast, setShowToast] = useState(false)
|
||||
|
||||
const reverseText = () => {
|
||||
setOutput(input.split('').reverse().join(''))
|
||||
}
|
||||
|
||||
const copyToClipboard = () => {
|
||||
navigator.clipboard.writeText(output)
|
||||
setShowToast(true)
|
||||
setTimeout(() => setShowToast(false), 3000)
|
||||
}
|
||||
|
||||
return (
|
||||
<motion.section
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.5, delay: 0.2 }}
|
||||
className="mb-12 bg-white dark:bg-gray-800 p-6 rounded-lg shadow-md"
|
||||
>
|
||||
<h2 className="text-2xl font-semibold mb-4 text-gray-800 dark:text-white">文本反转</h2>
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<label htmlFor="input" className="block text-sm font-medium text-gray-700 dark:text-gray-300">输入</label>
|
||||
<input
|
||||
type="text"
|
||||
id="input"
|
||||
value={input}
|
||||
onChange={(e) => setInput(e.target.value)}
|
||||
onKeyUp={reverseText}
|
||||
className="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-300 focus:ring focus:ring-indigo-200 focus:ring-opacity-50 dark:bg-gray-700 dark:border-gray-600 dark:text-white"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label htmlFor="output" className="block text-sm font-medium text-gray-700 dark:text-gray-300">输出</label>
|
||||
<div className="mt-1 flex rounded-md shadow-sm">
|
||||
<input
|
||||
type="text"
|
||||
id="output"
|
||||
value={output}
|
||||
readOnly
|
||||
className="flex-1 rounded-none rounded-l-md border-gray-300 focus:border-indigo-300 focus:ring focus:ring-indigo-200 focus:ring-opacity-50 dark:bg-gray-700 dark:border-gray-600 dark:text-white"
|
||||
/>
|
||||
<button
|
||||
onClick={copyToClipboard}
|
||||
className="inline-flex items-center px-3 rounded-r-md border border-l-0 border-gray-300 bg-gray-50 text-gray-500 text-sm dark:bg-gray-600 dark:border-gray-500 dark:text-gray-300"
|
||||
>
|
||||
复制
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<Toast message="复制成功!" isVisible={showToast} />
|
||||
</motion.section>
|
||||
)
|
||||
}
|
||||
|
26
components/tools/CatgirlGenerator/Toast.tsx
Normal file
26
components/tools/CatgirlGenerator/Toast.tsx
Normal file
@ -0,0 +1,26 @@
|
||||
import { motion, AnimatePresence } from 'framer-motion'
|
||||
import { CheckCircle } from 'lucide-react'
|
||||
|
||||
interface ToastProps {
|
||||
message: string
|
||||
isVisible: boolean
|
||||
}
|
||||
|
||||
export default function Toast({ message, isVisible }: ToastProps) {
|
||||
return (
|
||||
<AnimatePresence>
|
||||
{isVisible && (
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 50 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
exit={{ opacity: 0, y: 50 }}
|
||||
className="fixed bottom-4 right-4 bg-green-500 text-white px-4 py-2 rounded-md shadow-lg flex items-center space-x-2"
|
||||
>
|
||||
<CheckCircle size={20} />
|
||||
<span>{message}</span>
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
)
|
||||
}
|
||||
|
51
components/tools/compressor/CompressionOptions.tsx
Normal file
51
components/tools/compressor/CompressionOptions.tsx
Normal file
@ -0,0 +1,51 @@
|
||||
import { useState } from 'react'
|
||||
import { motion } from 'framer-motion'
|
||||
|
||||
interface CompressionOptionsProps {
|
||||
onCompress: (format: string) => void
|
||||
isCompressing: boolean
|
||||
progress: number
|
||||
}
|
||||
|
||||
export default function CompressionOptions({ onCompress, isCompressing, progress }: CompressionOptionsProps) {
|
||||
const [format, setFormat] = useState('webp')
|
||||
|
||||
return (
|
||||
<div className="mt-6">
|
||||
<div className="flex flex-wrap -mx-2">
|
||||
<div className="w-full px-2 mb-4">
|
||||
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
|
||||
输出格式
|
||||
</label>
|
||||
<select
|
||||
value={format}
|
||||
onChange={(e) => setFormat(e.target.value)}
|
||||
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md shadow-sm focus:outline-none focus:ring-blue-500 focus:border-blue-500 dark:bg-gray-700 dark:text-white"
|
||||
>
|
||||
<option value="webp">WebP</option>
|
||||
<option value="jpeg">JPEG</option>
|
||||
<option value="png">PNG</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<motion.button
|
||||
onClick={() => onCompress(format)}
|
||||
disabled={isCompressing}
|
||||
className="w-full bg-blue-500 text-white py-2 px-4 rounded-md hover:bg-blue-600 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 disabled:opacity-50 disabled:cursor-not-allowed dark:bg-blue-600 dark:hover:bg-blue-700"
|
||||
whileHover={{ scale: 1.05 }}
|
||||
whileTap={{ scale: 0.95 }}
|
||||
>
|
||||
{isCompressing ? '压缩中...' : '开始压缩'}
|
||||
</motion.button>
|
||||
{isCompressing && (
|
||||
<div className="mt-4">
|
||||
<div className="w-full bg-gray-200 rounded-full h-2.5 dark:bg-gray-700">
|
||||
<div className="bg-blue-600 h-2.5 rounded-full" style={{ width: `${progress}%` }}></div>
|
||||
</div>
|
||||
<p className="text-center mt-2 text-sm text-gray-600 dark:text-gray-400">{progress}%</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
69
components/tools/compressor/CompressionResult.tsx
Normal file
69
components/tools/compressor/CompressionResult.tsx
Normal file
@ -0,0 +1,69 @@
|
||||
import { useState } from 'react'
|
||||
import Image from 'next/image'
|
||||
|
||||
interface CompressionResultProps {
|
||||
originalImage: File
|
||||
compressedImage: File
|
||||
}
|
||||
|
||||
export default function CompressionResult({ originalImage, compressedImage }: CompressionResultProps) {
|
||||
const [showOriginal, setShowOriginal] = useState(false)
|
||||
|
||||
const originalSizeKB = (originalImage.size / 1024).toFixed(2)
|
||||
const compressedSizeKB = (compressedImage.size / 1024).toFixed(2)
|
||||
const compressionRatio = ((1 - compressedImage.size / originalImage.size) * 100).toFixed(2)
|
||||
|
||||
return (
|
||||
<div className="mt-6">
|
||||
<h2 className="text-lg font-semibold mb-2 text-gray-900 dark:text-white">压缩结果</h2>
|
||||
<div className="bg-gray-100 dark:bg-gray-700 rounded-lg p-4">
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<p className="text-sm font-medium text-gray-500 dark:text-gray-400">原始大小</p>
|
||||
<p className="text-lg font-semibold text-gray-900 dark:text-white">{originalSizeKB} KB</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-sm font-medium text-gray-500 dark:text-gray-400">压缩后大小</p>
|
||||
<p className="text-lg font-semibold text-gray-900 dark:text-white">{compressedSizeKB} KB</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-sm font-medium text-gray-500 dark:text-gray-400">压缩比例</p>
|
||||
<p className="text-lg font-semibold text-gray-900 dark:text-white">{compressionRatio}%</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-sm font-medium text-gray-500 dark:text-gray-400">输出格式</p>
|
||||
<p className="text-lg font-semibold text-gray-900 dark:text-white">{compressedImage.type.split('/')[1].toUpperCase()}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-4">
|
||||
<a
|
||||
href={URL.createObjectURL(compressedImage)}
|
||||
download={`compressed_${compressedImage.name}`}
|
||||
className="inline-block bg-green-500 text-white py-2 px-4 rounded-md hover:bg-green-600 focus:outline-none focus:ring-2 focus:ring-green-500 focus:ring-offset-2 dark:bg-green-600 dark:hover:bg-green-700"
|
||||
>
|
||||
下载压缩后的图片
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-6">
|
||||
<h3 className="text-lg font-semibold mb-2 text-gray-900 dark:text-white">图片对比</h3>
|
||||
<div className="relative">
|
||||
<Image
|
||||
src={URL.createObjectURL(showOriginal ? originalImage : compressedImage)}
|
||||
alt="Compressed Image"
|
||||
width={600}
|
||||
height={400}
|
||||
className="w-full h-auto rounded-lg"
|
||||
/>
|
||||
<button
|
||||
className="absolute top-2 right-2 bg-white dark:bg-gray-800 text-gray-900 dark:text-white px-3 py-1 rounded-md text-sm font-medium"
|
||||
onClick={() => setShowOriginal(!showOriginal)}
|
||||
>
|
||||
{showOriginal ? '显示压缩后' : '显示原图'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
88
components/tools/compressor/FileUpload.tsx
Normal file
88
components/tools/compressor/FileUpload.tsx
Normal file
@ -0,0 +1,88 @@
|
||||
import { useCallback, useState } from "react";
|
||||
import { useDropzone } from "react-dropzone";
|
||||
import { motion, HTMLMotionProps } from "framer-motion";
|
||||
|
||||
interface FileUploadProps {
|
||||
onFileSelect: (file: File) => void;
|
||||
selectedFile: File | null;
|
||||
}
|
||||
|
||||
export default function FileUpload({
|
||||
onFileSelect,
|
||||
selectedFile,
|
||||
}: FileUploadProps) {
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
const onDrop = useCallback(
|
||||
(acceptedFiles: File[]) => {
|
||||
if (acceptedFiles.length > 0) {
|
||||
const file = acceptedFiles[0];
|
||||
if (file.type.startsWith("image/")) {
|
||||
onFileSelect(file);
|
||||
setError(null);
|
||||
} else {
|
||||
setError("请上传有效的图片文件。");
|
||||
}
|
||||
}
|
||||
},
|
||||
[onFileSelect]
|
||||
);
|
||||
|
||||
const { getRootProps, getInputProps, isDragActive } = useDropzone({
|
||||
onDrop,
|
||||
accept: {
|
||||
"image/*": [".jpeg", ".jpg", ".png", ".gif", ".webp"],
|
||||
},
|
||||
multiple: false,
|
||||
});
|
||||
|
||||
return (
|
||||
<>
|
||||
<motion.div
|
||||
{...getRootProps() as Omit<HTMLMotionProps<"div">, "onDragStart">}
|
||||
className={`border-2 border-dashed rounded-lg p-8 text-center cursor-pointer transition-colors ${
|
||||
isDragActive
|
||||
? "border-blue-500 bg-blue-50 dark:bg-blue-900"
|
||||
: "border-gray-300 dark:border-gray-600 hover:border-gray-400 dark:hover:border-gray-500"
|
||||
}`}
|
||||
whileHover={{ scale: 1.02 }}
|
||||
whileTap={{ scale: 0.98 }}
|
||||
>
|
||||
<input {...getInputProps()} />
|
||||
{isDragActive ? (
|
||||
<p className="text-blue-500 dark:text-blue-400">拖放图片到这里 ...</p>
|
||||
) : (
|
||||
<p className="text-gray-600 dark:text-gray-300">
|
||||
拖放图片到这里,或点击选择图片
|
||||
</p>
|
||||
)}
|
||||
</motion.div>
|
||||
{error && <p className="text-red-500">{error}</p>}
|
||||
{selectedFile && (
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.5 }}
|
||||
className="mt-4 p-4 bg-green-100 dark:bg-green-800 rounded-lg"
|
||||
>
|
||||
<p className="text-green-700 dark:text-green-200">
|
||||
<span className="font-semibold">已成功上传:</span>{" "}
|
||||
{selectedFile.name}
|
||||
</p>
|
||||
<p className="text-sm text-green-600 dark:text-green-300 mt-1">
|
||||
文件大小: {(selectedFile.size / 1024 / 1024).toFixed(2)} MB
|
||||
</p>
|
||||
{selectedFile.type.startsWith("image/") && (
|
||||
<div className="mt-2">
|
||||
<img
|
||||
src={URL.createObjectURL(selectedFile)}
|
||||
alt={selectedFile.name}
|
||||
className="max-w-full h-auto rounded-lg"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</motion.div>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
15
components/tools/compressor/Instructions.tsx
Normal file
15
components/tools/compressor/Instructions.tsx
Normal file
@ -0,0 +1,15 @@
|
||||
export default function Instructions() {
|
||||
return (
|
||||
<div className="mb-6 p-4 bg-blue-50 dark:bg-blue-900 rounded-lg">
|
||||
<h2 className="text-lg font-semibold mb-2 text-blue-800 dark:text-blue-200">使用说明</h2>
|
||||
<ol className="list-decimal list-inside text-sm text-blue-700 dark:text-blue-200">
|
||||
<li>拖放或点击上传按钮选择要压缩的图片</li>
|
||||
<li>选择输出格式(WebP、JPEG 或 PNG)</li>
|
||||
<li>点击"开始压缩"按钮</li>
|
||||
<li>等待压缩完成,查看结果并下载压缩后的图片</li>
|
||||
</ol>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
|
28
components/tools/compressor/ThemeToggle.tsx
Normal file
28
components/tools/compressor/ThemeToggle.tsx
Normal file
@ -0,0 +1,28 @@
|
||||
'use client'
|
||||
|
||||
import { useTheme } from 'next-themes'
|
||||
import { useState, useEffect } from 'react'
|
||||
import { Sun, Moon } from 'lucide-react'
|
||||
|
||||
export default function ThemeToggle() {
|
||||
const [mounted, setMounted] = useState(false)
|
||||
const { theme, setTheme } = useTheme()
|
||||
|
||||
useEffect(() => {
|
||||
setMounted(true)
|
||||
}, [])
|
||||
|
||||
if (!mounted) {
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<button
|
||||
onClick={() => setTheme(theme === 'dark' ? 'light' : 'dark')}
|
||||
className="p-2 rounded-md hover:bg-gray-200 dark:hover:bg-gray-700 transition-colors duration-200"
|
||||
>
|
||||
{theme === 'dark' ? <Sun className="w-5 h-5" /> : <Moon className="w-5 h-5" />}
|
||||
</button>
|
||||
)
|
||||
}
|
||||
|
57
components/tools/hash-random/CoreAlgorithm.tsx
Normal file
57
components/tools/hash-random/CoreAlgorithm.tsx
Normal file
@ -0,0 +1,57 @@
|
||||
import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter'
|
||||
import { tomorrow } from 'react-syntax-highlighter/dist/esm/styles/prism'
|
||||
|
||||
export default function CoreAlgorithm() {
|
||||
const distanceCode = `
|
||||
function calcMd5Distance(s1, s2) {
|
||||
let distance = 0;
|
||||
for (let i = 0; i < Math.min(s1.length, s2.length); i++) {
|
||||
if (s1[i] === s2[i]) {
|
||||
continue;
|
||||
}
|
||||
distance += Math.abs(s1.charCodeAt(i) - s2.charCodeAt(i));
|
||||
}
|
||||
return distance;
|
||||
}
|
||||
`
|
||||
|
||||
const sortCode = `
|
||||
function makeSortedNameHashDistanceTuple(targetHash, names) {
|
||||
let nameHashDistanceTuple = names.map((name) => {
|
||||
let nameMd5 = makeMd5(name);
|
||||
let distance = calcMd5Distance(targetHash, nameMd5);
|
||||
return [name, nameMd5, distance];
|
||||
});
|
||||
// sort by distance
|
||||
nameHashDistanceTuple.sort((a, b) => {
|
||||
return a[2] - b[2];
|
||||
});
|
||||
return nameHashDistanceTuple;
|
||||
}
|
||||
`
|
||||
|
||||
return (
|
||||
<div className="mt-12 bg-white dark:bg-gray-800 rounded-lg shadow-md p-6 transition-colors duration-200">
|
||||
<h2 className="text-2xl font-bold mb-4 text-indigo-800 dark:text-indigo-200">核心算法</h2>
|
||||
<p className="mb-4 text-gray-600 dark:text-gray-300">md5 哈希计算依赖 js-md5 库。</p>
|
||||
<div className="mb-6">
|
||||
<SyntaxHighlighter language="html" style={tomorrow} className="rounded-md">
|
||||
{'<script src="https://cdn.jsdelivr.net/npm/js-md5@0.7.3/src/md5.min.js"></script>'}
|
||||
</SyntaxHighlighter>
|
||||
</div>
|
||||
<div className="mb-6">
|
||||
<h3 className="text-lg font-semibold mb-2 text-indigo-700 dark:text-indigo-300">哈希距离计算算法</h3>
|
||||
<SyntaxHighlighter language="javascript" style={tomorrow} className="rounded-md">
|
||||
{distanceCode}
|
||||
</SyntaxHighlighter>
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="text-lg font-semibold mb-2 text-indigo-700 dark:text-indigo-300">哈希距离排序算法</h3>
|
||||
<SyntaxHighlighter language="javascript" style={tomorrow} className="rounded-md">
|
||||
{sortCode}
|
||||
</SyntaxHighlighter>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
19
components/tools/hash-random/DarkModeToggle.tsx
Normal file
19
components/tools/hash-random/DarkModeToggle.tsx
Normal file
@ -0,0 +1,19 @@
|
||||
import { Moon, Sun } from 'lucide-react'
|
||||
|
||||
interface DarkModeToggleProps {
|
||||
darkMode: boolean
|
||||
setDarkMode: (darkMode: boolean) => void
|
||||
}
|
||||
|
||||
export default function DarkModeToggle({ darkMode, setDarkMode }: DarkModeToggleProps) {
|
||||
return (
|
||||
<button
|
||||
onClick={() => setDarkMode(!darkMode)}
|
||||
className="p-2 rounded-full bg-gray-200 dark:bg-gray-700 text-gray-800 dark:text-gray-200 transition-colors duration-200"
|
||||
aria-label={darkMode ? "Switch to light mode" : "Switch to dark mode"}
|
||||
>
|
||||
{darkMode ? <Sun size={24} /> : <Moon size={24} />}
|
||||
</button>
|
||||
)
|
||||
}
|
||||
|
22
components/tools/hash-random/Footer.tsx
Normal file
22
components/tools/hash-random/Footer.tsx
Normal file
@ -0,0 +1,22 @@
|
||||
export default function Footer() {
|
||||
return (
|
||||
<footer className="bg-white dark:bg-gray-800 shadow-md mt-12 py-6 transition-colors duration-200">
|
||||
<div className="container mx-auto px-4 text-center">
|
||||
<p className="text-gray-600 dark:text-gray-300">
|
||||
© {new Date().getFullYear()} APS NAV.
|
||||
</p>
|
||||
<p className="text-gray-600 dark:text-gray-300 mt-2">
|
||||
创意来源:{" "}
|
||||
<a
|
||||
href="https://github.com/TaylorAndTony/hash-random"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-indigo-600 dark:text-indigo-400 hover:underline"
|
||||
>
|
||||
TaylorAndTony
|
||||
</a>
|
||||
</p>
|
||||
</div>
|
||||
</footer>
|
||||
);
|
||||
}
|
37
components/tools/hash-random/HashTable.tsx
Normal file
37
components/tools/hash-random/HashTable.tsx
Normal file
@ -0,0 +1,37 @@
|
||||
interface HashTableProps {
|
||||
results: [string, string, number][]
|
||||
saltHash: string
|
||||
}
|
||||
|
||||
export default function HashTable({ results }: HashTableProps) {
|
||||
return (
|
||||
<div className="bg-white dark:bg-gray-800 rounded-lg shadow-md p-6 transition-colors duration-200">
|
||||
<h2 className="text-2xl font-bold mb-4 text-indigo-800 dark:text-indigo-200">哈希表</h2>
|
||||
<p className="mb-4 text-gray-600 dark:text-gray-300">为保持页面性能,仅显示前面最多 1000 位人员的哈希校验</p>
|
||||
<div className="overflow-x-auto">
|
||||
<table className="w-full border-collapse">
|
||||
<thead>
|
||||
<tr className="bg-indigo-100 dark:bg-indigo-900">
|
||||
<th className="border border-indigo-200 dark:border-indigo-700 px-4 py-2 text-left text-indigo-800 dark:text-indigo-200">序号</th>
|
||||
<th className="border border-indigo-200 dark:border-indigo-700 px-4 py-2 text-left text-indigo-800 dark:text-indigo-200">参与人</th>
|
||||
<th className="border border-indigo-200 dark:border-indigo-700 px-4 py-2 text-left text-indigo-800 dark:text-indigo-200">哈希值</th>
|
||||
<th className="border border-indigo-200 dark:border-indigo-700 px-4 py-2 text-left text-indigo-800 dark:text-indigo-200">与种子的距离</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{results.map(([name, hash, distance], index) => (
|
||||
<tr key={index} className={index % 2 === 0 ? 'bg-gray-50 dark:bg-gray-800' : 'bg-white dark:bg-gray-700'}>
|
||||
<td className="border border-indigo-200 dark:border-indigo-700 px-4 py-2 text-gray-800 dark:text-gray-200">{index + 1}</td>
|
||||
<td className="border border-indigo-200 dark:border-indigo-700 px-4 py-2 text-gray-800 dark:text-gray-200">{name}</td>
|
||||
<td className="border border-indigo-200 dark:border-indigo-700 px-4 py-2 font-mono text-sm text-gray-800 dark:text-gray-200">{hash}</td>
|
||||
<td className="border border-indigo-200 dark:border-indigo-700 px-4 py-2 text-gray-800 dark:text-gray-200">{distance}</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
|
40
components/tools/hash-random/ParticipantList.tsx
Normal file
40
components/tools/hash-random/ParticipantList.tsx
Normal file
@ -0,0 +1,40 @@
|
||||
import { useState } from 'react'
|
||||
|
||||
interface ParticipantListProps {
|
||||
setParticipants: (participants: string[]) => void
|
||||
}
|
||||
|
||||
export default function ParticipantList({ setParticipants }: ParticipantListProps) {
|
||||
const [participantText, setParticipantText] = useState('')
|
||||
|
||||
const handleRoll = () => {
|
||||
const participantArray = participantText.split(/[\n\s]+/).filter(Boolean)
|
||||
setParticipants(participantArray)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="bg-white dark:bg-gray-800 rounded-lg shadow-md p-6 transition-colors duration-200">
|
||||
<h2 className="text-2xl font-bold mb-4 text-indigo-800 dark:text-indigo-200">参与人员</h2>
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<label htmlFor="participants" className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
|
||||
按行或空格分开所有参与人名:
|
||||
</label>
|
||||
<textarea
|
||||
id="participants"
|
||||
value={participantText}
|
||||
onChange={(e) => setParticipantText(e.target.value)}
|
||||
className="w-full h-32 border-gray-300 rounded-md shadow-sm focus:ring-indigo-500 focus:border-indigo-500 dark:bg-gray-700 dark:border-gray-600 dark:text-white"
|
||||
/>
|
||||
</div>
|
||||
<button
|
||||
onClick={handleRoll}
|
||||
className="w-full bg-indigo-600 text-white px-4 py-2 rounded-md hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 transition-colors"
|
||||
>
|
||||
抽取
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
68
components/tools/hash-random/RandomSeed.tsx
Normal file
68
components/tools/hash-random/RandomSeed.tsx
Normal file
@ -0,0 +1,68 @@
|
||||
import { ChangeEvent, useState } from 'react'
|
||||
|
||||
interface RandomSeedProps {
|
||||
salt: string
|
||||
setSalt: (salt: string) => void
|
||||
count: number
|
||||
setCount: (count: number) => void
|
||||
}
|
||||
|
||||
export default function RandomSeed({ salt, setSalt, count, setCount }: RandomSeedProps) {
|
||||
const [errors, setErrors] = useState({ salt: '', count: '' })
|
||||
|
||||
const validateSalt = (value: string) => {
|
||||
if (value.trim() === '') {
|
||||
setErrors(prev => ({ ...prev, salt: '请输入随机种子' }))
|
||||
} else {
|
||||
setErrors(prev => ({ ...prev, salt: '' }))
|
||||
}
|
||||
setSalt(value)
|
||||
}
|
||||
|
||||
const validateCount = (value: number) => {
|
||||
if (value < 1) {
|
||||
setErrors(prev => ({ ...prev, count: '抽取数量必须大于0' }))
|
||||
} else {
|
||||
setErrors(prev => ({ ...prev, count: '' }))
|
||||
}
|
||||
setCount(value)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="bg-white dark:bg-gray-800 rounded-lg shadow-md p-6 transition-colors duration-200">
|
||||
<h2 className="text-2xl font-bold mb-4 text-indigo-800 dark:text-indigo-200">随机种子</h2>
|
||||
<p className="mb-4 text-gray-600 dark:text-gray-300">
|
||||
选择一个随机的文本作为种子值。这个种子值将被用作计算哈希值的输入。为了获得最好的排序效果,建议选择事先无法确定的内容作为种子,如明天的热搜第一条,或某一个群聊内最新的一条消息。
|
||||
</p>
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<label htmlFor="salt" className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
|
||||
填写事先无法预测的文本:
|
||||
</label>
|
||||
<input
|
||||
id="salt"
|
||||
type="text"
|
||||
value={salt}
|
||||
onChange={(e: ChangeEvent<HTMLInputElement>) => validateSalt(e.target.value)}
|
||||
className="w-full border-gray-300 rounded-md shadow-sm focus:ring-indigo-500 focus:border-indigo-500 dark:bg-gray-700 dark:border-gray-600 dark:text-white"
|
||||
/>
|
||||
{errors.salt && <p className="mt-1 text-sm text-red-600 dark:text-red-400">{errors.salt}</p>}
|
||||
</div>
|
||||
<div>
|
||||
<label htmlFor="count" className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
|
||||
抽取数量:
|
||||
</label>
|
||||
<input
|
||||
id="count"
|
||||
type="number"
|
||||
value={count}
|
||||
onChange={(e: ChangeEvent<HTMLInputElement>) => validateCount(Number(e.target.value))}
|
||||
className="w-24 border-gray-300 rounded-md shadow-sm focus:ring-indigo-500 focus:border-indigo-500 dark:bg-gray-700 dark:border-gray-600 dark:text-white"
|
||||
/>
|
||||
{errors.count && <p className="mt-1 text-sm text-red-600 dark:text-red-400">{errors.count}</p>}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
70
components/tools/hash-random/ResultDisplay.tsx
Normal file
70
components/tools/hash-random/ResultDisplay.tsx
Normal file
@ -0,0 +1,70 @@
|
||||
import { useState } from "react";
|
||||
|
||||
interface ResultDisplayProps {
|
||||
results: [string, string, number][];
|
||||
saltHash: string;
|
||||
}
|
||||
|
||||
export default function ResultDisplay({
|
||||
results,
|
||||
saltHash,
|
||||
}: ResultDisplayProps) {
|
||||
const [copied, setCopied] = useState(false);
|
||||
|
||||
const handleCopy = () => {
|
||||
const resultText = results.map(([name]) => name).join("\n");
|
||||
navigator.clipboard.writeText(resultText);
|
||||
setCopied(true);
|
||||
setTimeout(() => setCopied(false), 2000);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="bg-white dark:bg-gray-800 rounded-lg shadow-md p-6 transition-colors duration-200">
|
||||
<h2 className="text-2xl font-bold mb-4 text-indigo-800 dark:text-indigo-200">
|
||||
随机结果
|
||||
</h2>
|
||||
<p className="mb-4 text-gray-600 dark:text-gray-300">
|
||||
此网页将逐一比较每个参与者的哈希值与种子的哈希值。通过计算它们之间的相似度,可以找出与种子值最接近的参与者,并将他们按照相似度的程度进行排序。这种排序方法可以确保在每次排序过程中都能产生一致而可靠的结果,满足了「事后可复现」。
|
||||
</p>
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<label
|
||||
htmlFor="results"
|
||||
className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1"
|
||||
>
|
||||
抽取结果:
|
||||
</label>
|
||||
<div className="flex">
|
||||
<textarea
|
||||
id="results"
|
||||
value={results.map(([name]) => name).join("\n")}
|
||||
readOnly
|
||||
className="flex-grow h-32 border-gray-300 rounded-l-md shadow-sm bg-gray-50 dark:bg-gray-700 dark:border-gray-600 dark:text-white"
|
||||
/>
|
||||
<button
|
||||
onClick={handleCopy}
|
||||
className="bg-green-500 text-white px-4 py-2 rounded-r-md hover:bg-green-600 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-green-500 transition-colors"
|
||||
>
|
||||
{copied ? "已复制" : "复制"}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<label
|
||||
htmlFor="saltHash"
|
||||
className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1"
|
||||
>
|
||||
种子哈希:
|
||||
</label>
|
||||
<input
|
||||
id="saltHash"
|
||||
type="text"
|
||||
value={saltHash}
|
||||
readOnly
|
||||
className="w-full border-gray-300 rounded-md shadow-sm bg-gray-50 dark:bg-gray-700 dark:border-gray-600 dark:text-white"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
98
components/tools/hash-random/SaveLoadSelection.tsx
Normal file
98
components/tools/hash-random/SaveLoadSelection.tsx
Normal file
@ -0,0 +1,98 @@
|
||||
import { useState } from 'react'
|
||||
|
||||
interface SaveLoadSelectionProps {
|
||||
salt: string
|
||||
setSalt: (salt: string) => void
|
||||
count: number
|
||||
setCount: (count: number) => void
|
||||
participants: string[]
|
||||
setParticipants: (participants: string[]) => void
|
||||
}
|
||||
|
||||
// 定义 SelectionData 类型
|
||||
interface SelectionData {
|
||||
salt: string
|
||||
count: number
|
||||
participants: string[]
|
||||
}
|
||||
|
||||
export default function SaveLoadSelection({
|
||||
salt,
|
||||
setSalt,
|
||||
count,
|
||||
setCount,
|
||||
participants,
|
||||
setParticipants,
|
||||
}: SaveLoadSelectionProps) {
|
||||
// 使用 SelectionData 类型更新 savedSelections 的类型
|
||||
const [savedSelections, setSavedSelections] = useState<{ name: string; data: SelectionData }[]>([])
|
||||
const [selectionName, setSelectionName] = useState('')
|
||||
|
||||
const saveSelection = () => {
|
||||
if (selectionName) {
|
||||
const newSelection = {
|
||||
name: selectionName,
|
||||
data: { salt, count, participants },
|
||||
}
|
||||
setSavedSelections([...savedSelections, newSelection])
|
||||
setSelectionName('')
|
||||
}
|
||||
}
|
||||
|
||||
// 使用 SelectionData 类型更新 loadSelection 的参数
|
||||
const loadSelection = (selection: { name: string; data: SelectionData }) => {
|
||||
setSalt(selection.data.salt)
|
||||
setCount(selection.data.count)
|
||||
setParticipants(selection.data.participants)
|
||||
}
|
||||
|
||||
const deleteSelection = (index: number) => {
|
||||
const newSelections = [...savedSelections];
|
||||
newSelections.splice(index, 1);
|
||||
setSavedSelections(newSelections);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="bg-white dark:bg-gray-800 rounded-lg shadow-md p-6 transition-colors duration-200">
|
||||
<h2 className="text-2xl font-bold mb-4 text-indigo-800 dark:text-indigo-200">保存/加载选择</h2>
|
||||
<div className="space-y-4">
|
||||
<div className="flex space-x-2">
|
||||
<input
|
||||
type="text"
|
||||
value={selectionName}
|
||||
onChange={(e) => setSelectionName(e.target.value)}
|
||||
placeholder="输入保存名称"
|
||||
className="flex-grow border-gray-300 rounded-md shadow-sm focus:ring-indigo-500 focus:border-indigo-500 dark:bg-gray-700 dark:border-gray-600 dark:text-white"
|
||||
/>
|
||||
<button
|
||||
onClick={saveSelection}
|
||||
className="bg-indigo-600 text-white px-4 py-2 rounded-md hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 transition-colors"
|
||||
>
|
||||
保存
|
||||
</button>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
{savedSelections.map((selection, index) => (
|
||||
<div key={index} className="flex justify-between items-center">
|
||||
<span className="text-gray-700 dark:text-gray-300">{selection.name}</span>
|
||||
<div>
|
||||
<button
|
||||
onClick={() => loadSelection(selection)}
|
||||
className="bg-green-500 text-white px-3 py-1 rounded-md hover:bg-green-600 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-green-500 transition-colors mr-2"
|
||||
>
|
||||
加载
|
||||
</button>
|
||||
<button
|
||||
onClick={() => deleteSelection(index)}
|
||||
className="bg-red-500 text-white px-3 py-1 rounded-md hover:bg-red-600 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-red-500 transition-colors"
|
||||
>
|
||||
删除
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
12
data/本站工具.yml
Normal file
12
data/本站工具.yml
Normal file
@ -0,0 +1,12 @@
|
||||
- name: 图片压缩
|
||||
url: /tools/Compressor
|
||||
description: 简洁的图片压缩工具
|
||||
subcategory: 其它
|
||||
- name: 哈希随机抽取
|
||||
url: /tools/hash-random
|
||||
description: 逐一比较每个参与者的哈希值与种子的哈希值,通过计算它们之间的相似度,找出与种子值最接近的参与者
|
||||
subcategory: 其它
|
||||
- name: 扩展昵称生成器
|
||||
url: /tools/CatgirlGenerator
|
||||
description: 当别人@你时,会在@的文字后面加上一另段文字
|
||||
subcategory: 其它
|
53
hooks/useHashCalculation.ts
Normal file
53
hooks/useHashCalculation.ts
Normal file
@ -0,0 +1,53 @@
|
||||
import { useState, useEffect } from 'react'
|
||||
import md5 from 'md5'
|
||||
|
||||
function calcMd5Distance(s1: string, s2: string) {
|
||||
let distance = 0
|
||||
for (let i = 0; i < Math.min(s1.length, s2.length); i++) {
|
||||
if (s1[i] === s2[i]) {
|
||||
continue
|
||||
}
|
||||
distance += Math.abs(s1.charCodeAt(i) - s2.charCodeAt(i))
|
||||
}
|
||||
return distance
|
||||
}
|
||||
|
||||
function makeSortedNameHashDistanceTuple(targetHash: string, names: string[]) {
|
||||
let nameHashDistanceTuple = names.map((name) => {
|
||||
let nameMd5 = md5(name)
|
||||
let distance = calcMd5Distance(targetHash, nameMd5)
|
||||
return [name, nameMd5, distance] as [string, string, number]
|
||||
})
|
||||
// sort by distance
|
||||
nameHashDistanceTuple.sort((a, b) => {
|
||||
return a[2] - b[2]
|
||||
})
|
||||
return nameHashDistanceTuple
|
||||
}
|
||||
|
||||
export function useHashCalculation(salt: string, participants: string[], count: number) {
|
||||
const [sortedResults, setSortedResults] = useState<[string, string, number][]>([])
|
||||
const [saltHash, setSaltHash] = useState('')
|
||||
const [isCalculating, setIsCalculating] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
if (salt && participants.length > 0) {
|
||||
setIsCalculating(true)
|
||||
const timer = setTimeout(() => {
|
||||
const newSaltHash = md5(salt)
|
||||
setSaltHash(newSaltHash)
|
||||
const newSortedResults = makeSortedNameHashDistanceTuple(newSaltHash, participants)
|
||||
setSortedResults(newSortedResults)
|
||||
setIsCalculating(false)
|
||||
}, 1000) // Simulate a delay for the calculation
|
||||
return () => clearTimeout(timer)
|
||||
} else {
|
||||
setSortedResults([])
|
||||
setSaltHash('')
|
||||
setIsCalculating(false)
|
||||
}
|
||||
}, [salt, participants])
|
||||
|
||||
return { sortedResults, saltHash, isCalculating }
|
||||
}
|
||||
|
38
hooks/useImageCompression.ts
Normal file
38
hooks/useImageCompression.ts
Normal file
@ -0,0 +1,38 @@
|
||||
import { useState } from 'react'
|
||||
import imageCompression from 'browser-image-compression'
|
||||
|
||||
export default function useImageCompression() {
|
||||
const [compressedImage, setCompressedImage] = useState<File | null>(null)
|
||||
const [isCompressing, setIsCompressing] = useState(false)
|
||||
const [progress, setProgress] = useState(0)
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
|
||||
const compressImage = async (file: File, format: string) => {
|
||||
setIsCompressing(true)
|
||||
setProgress(0)
|
||||
setError(null)
|
||||
|
||||
try {
|
||||
const options = {
|
||||
maxSizeMB: 1,
|
||||
maxWidthOrHeight: 1920,
|
||||
useWebWorker: true,
|
||||
fileType: `image/${format}`,
|
||||
onProgress: (progress: number) => {
|
||||
setProgress(Math.round(progress))
|
||||
},
|
||||
}
|
||||
|
||||
const compressedFile = await imageCompression(file, options)
|
||||
setCompressedImage(compressedFile)
|
||||
} catch (err) {
|
||||
setError('压缩过程中出错,请重试。')
|
||||
console.error(err)
|
||||
} finally {
|
||||
setIsCompressing(false)
|
||||
}
|
||||
}
|
||||
|
||||
return { compressImage, compressedImage, isCompressing, progress, error }
|
||||
}
|
||||
|
440
package-lock.json
generated
440
package-lock.json
generated
@ -11,23 +11,31 @@
|
||||
"@radix-ui/react-label": "^2.1.0",
|
||||
"@radix-ui/react-scroll-area": "^1.2.1",
|
||||
"@radix-ui/react-slot": "^1.1.0",
|
||||
"browser-image-compression": "^2.0.2",
|
||||
"class-variance-authority": "^0.7.0",
|
||||
"clsx": "^2.1.1",
|
||||
"framer-motion": "^11.15.0",
|
||||
"js-yaml": "^4.1.0",
|
||||
"lucide-react": "^0.460.0",
|
||||
"md5": "^2.3.0",
|
||||
"next": "14.2.16",
|
||||
"next-auth": "^4.24.10",
|
||||
"next-themes": "^0.4.4",
|
||||
"react": "^18",
|
||||
"react-dom": "^18",
|
||||
"react-dropzone": "^14.3.5",
|
||||
"react-syntax-highlighter": "^15.6.1",
|
||||
"react-toastify": "^10.0.6",
|
||||
"tailwind-merge": "^2.5.4",
|
||||
"tailwindcss-animate": "^1.0.7"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/js-yaml": "^4.0.9",
|
||||
"@types/md5": "^2.3.5",
|
||||
"@types/node": "^20",
|
||||
"@types/react": "^18",
|
||||
"@types/react-dom": "^18",
|
||||
"@types/react-syntax-highlighter": "^15.5.13",
|
||||
"eslint": "^8",
|
||||
"eslint-config-next": "14.2.16",
|
||||
"postcss": "^8",
|
||||
@ -712,6 +720,15 @@
|
||||
"tslib": "^2.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/hast": {
|
||||
"version": "2.3.10",
|
||||
"resolved": "https://registry.npmmirror.com/@types/hast/-/hast-2.3.10.tgz",
|
||||
"integrity": "sha512-McWspRw8xx8J9HurkVBfYj0xKoE25tOFlHGdx4MJ5xORQrMGZNqJhVQWaIbm6Oyla5kYOXtDiopzKRJzEOkwJw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/unist": "^2"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/js-yaml": {
|
||||
"version": "4.0.9",
|
||||
"resolved": "https://registry.npmmirror.com/@types/js-yaml/-/js-yaml-4.0.9.tgz",
|
||||
@ -726,6 +743,13 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/md5": {
|
||||
"version": "2.3.5",
|
||||
"resolved": "https://registry.npmmirror.com/@types/md5/-/md5-2.3.5.tgz",
|
||||
"integrity": "sha512-/i42wjYNgE6wf0j2bcTX6kuowmdL/6PE4IVitMpm2eYKBUuYCprdcWVK+xEF0gcV6ufMCRhtxmReGfc6hIK7Jw==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/node": {
|
||||
"version": "20.17.6",
|
||||
"resolved": "https://registry.npmmirror.com/@types/node/-/node-20.17.6.tgz",
|
||||
@ -764,6 +788,22 @@
|
||||
"@types/react": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/react-syntax-highlighter": {
|
||||
"version": "15.5.13",
|
||||
"resolved": "https://registry.npmmirror.com/@types/react-syntax-highlighter/-/react-syntax-highlighter-15.5.13.tgz",
|
||||
"integrity": "sha512-uLGJ87j6Sz8UaBAooU0T6lWJ0dBmjZgN1PZTrj05TNql2/XpC6+4HhMT5syIdFUUt+FASfCeLLv4kBygNU+8qA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/react": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/unist": {
|
||||
"version": "2.0.11",
|
||||
"resolved": "https://registry.npmmirror.com/@types/unist/-/unist-2.0.11.tgz",
|
||||
"integrity": "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@typescript-eslint/eslint-plugin": {
|
||||
"version": "7.18.0",
|
||||
"resolved": "https://registry.npmmirror.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-7.18.0.tgz",
|
||||
@ -1270,6 +1310,15 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/attr-accept": {
|
||||
"version": "2.2.5",
|
||||
"resolved": "https://registry.npmmirror.com/attr-accept/-/attr-accept-2.2.5.tgz",
|
||||
"integrity": "sha512-0bDNnY/u6pPwHDMoF0FieU354oBi0a8rD9FcsLwzcGWbc8KS8KPIi7y+s13OlVY+gMWc/9xEMUgNE6Qm8ZllYQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=4"
|
||||
}
|
||||
},
|
||||
"node_modules/available-typed-arrays": {
|
||||
"version": "1.0.7",
|
||||
"resolved": "https://registry.npmmirror.com/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz",
|
||||
@ -1347,6 +1396,15 @@
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/browser-image-compression": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmmirror.com/browser-image-compression/-/browser-image-compression-2.0.2.tgz",
|
||||
"integrity": "sha512-pBLlQyUf6yB8SmmngrcOw3EoS4RpQ1BcylI3T9Yqn7+4nrQTXJD4sJDe5ODnJdrvNMaio5OicFo75rDyJD2Ucw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"uzip": "0.20201231.0"
|
||||
}
|
||||
},
|
||||
"node_modules/busboy": {
|
||||
"version": "1.6.0",
|
||||
"resolved": "https://registry.npmmirror.com/busboy/-/busboy-1.6.0.tgz",
|
||||
@ -1434,6 +1492,45 @@
|
||||
"url": "https://github.com/chalk/chalk?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/character-entities": {
|
||||
"version": "1.2.4",
|
||||
"resolved": "https://registry.npmmirror.com/character-entities/-/character-entities-1.2.4.tgz",
|
||||
"integrity": "sha512-iBMyeEHxfVnIakwOuDXpVkc54HijNgCyQB2w0VfGQThle6NXn50zU6V/u+LDhxHcDUPojn6Kpga3PTAD8W1bQw==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/wooorm"
|
||||
}
|
||||
},
|
||||
"node_modules/character-entities-legacy": {
|
||||
"version": "1.1.4",
|
||||
"resolved": "https://registry.npmmirror.com/character-entities-legacy/-/character-entities-legacy-1.1.4.tgz",
|
||||
"integrity": "sha512-3Xnr+7ZFS1uxeiUDvV02wQ+QDbc55o97tIV5zHScSPJpcLm/r0DFPcoY3tYRp+VZukxuMeKgXYmsXQHO05zQeA==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/wooorm"
|
||||
}
|
||||
},
|
||||
"node_modules/character-reference-invalid": {
|
||||
"version": "1.1.4",
|
||||
"resolved": "https://registry.npmmirror.com/character-reference-invalid/-/character-reference-invalid-1.1.4.tgz",
|
||||
"integrity": "sha512-mKKUkUbhPpQlCOfIuZkvSEgktjPFIsZKRRbC6KWVEMvlzblj3i3asQv5ODsrwt0N3pHAEvjP8KTQPHkp0+6jOg==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/wooorm"
|
||||
}
|
||||
},
|
||||
"node_modules/charenc": {
|
||||
"version": "0.0.2",
|
||||
"resolved": "https://registry.npmmirror.com/charenc/-/charenc-0.0.2.tgz",
|
||||
"integrity": "sha512-yrLQ/yVUFXkzg7EDQsPieE/53+0RlaWTs+wBrvW36cyilJ2SaDWfl4Yj7MtLTXleV9uEKefbAGUPv2/iWSooRA==",
|
||||
"license": "BSD-3-Clause",
|
||||
"engines": {
|
||||
"node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/chokidar": {
|
||||
"version": "3.6.0",
|
||||
"resolved": "https://registry.npmmirror.com/chokidar/-/chokidar-3.6.0.tgz",
|
||||
@ -1524,6 +1621,16 @@
|
||||
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/comma-separated-tokens": {
|
||||
"version": "1.0.8",
|
||||
"resolved": "https://registry.npmmirror.com/comma-separated-tokens/-/comma-separated-tokens-1.0.8.tgz",
|
||||
"integrity": "sha512-GHuDRO12Sypu2cV70d1dkA2EUmXHgntrzbpvOB+Qy+49ypNfGgFQIC2fhhXbnyrJRynDCAARsT7Ou0M6hirpfw==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/wooorm"
|
||||
}
|
||||
},
|
||||
"node_modules/commander": {
|
||||
"version": "4.1.1",
|
||||
"resolved": "https://registry.npmmirror.com/commander/-/commander-4.1.1.tgz",
|
||||
@ -1563,6 +1670,15 @@
|
||||
"node": ">= 8"
|
||||
}
|
||||
},
|
||||
"node_modules/crypt": {
|
||||
"version": "0.0.2",
|
||||
"resolved": "https://registry.npmmirror.com/crypt/-/crypt-0.0.2.tgz",
|
||||
"integrity": "sha512-mCxBlsHFYh9C+HVpiEacem8FEBnMXgU9gy4zmNC+SXAZNB/1idgp/aulFJ4FgCi7GPEVbfyng092GqL2k2rmow==",
|
||||
"license": "BSD-3-Clause",
|
||||
"engines": {
|
||||
"node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/cssesc": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmmirror.com/cssesc/-/cssesc-3.0.0.tgz",
|
||||
@ -2455,6 +2571,19 @@
|
||||
"reusify": "^1.0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/fault": {
|
||||
"version": "1.0.4",
|
||||
"resolved": "https://registry.npmmirror.com/fault/-/fault-1.0.4.tgz",
|
||||
"integrity": "sha512-CJ0HCB5tL5fYTEA7ToAq5+kTwd++Borf1/bifxd9iT70QcXr4MRrO3Llf8Ifs70q+SJcGHFtnIE/Nw6giCtECA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"format": "^0.2.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/wooorm"
|
||||
}
|
||||
},
|
||||
"node_modules/file-entry-cache": {
|
||||
"version": "6.0.1",
|
||||
"resolved": "https://registry.npmmirror.com/file-entry-cache/-/file-entry-cache-6.0.1.tgz",
|
||||
@ -2468,6 +2597,18 @@
|
||||
"node": "^10.12.0 || >=12.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/file-selector": {
|
||||
"version": "2.1.2",
|
||||
"resolved": "https://registry.npmmirror.com/file-selector/-/file-selector-2.1.2.tgz",
|
||||
"integrity": "sha512-QgXo+mXTe8ljeqUFaX3QVHc5osSItJ/Km+xpocx0aSqWGMSCf6qYs/VnzZgS864Pjn5iceMRFigeAV7AfTlaig==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"tslib": "^2.7.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 12"
|
||||
}
|
||||
},
|
||||
"node_modules/fill-range": {
|
||||
"version": "7.1.1",
|
||||
"resolved": "https://registry.npmmirror.com/fill-range/-/fill-range-7.1.1.tgz",
|
||||
@ -2545,6 +2686,41 @@
|
||||
"url": "https://github.com/sponsors/isaacs"
|
||||
}
|
||||
},
|
||||
"node_modules/format": {
|
||||
"version": "0.2.2",
|
||||
"resolved": "https://registry.npmmirror.com/format/-/format-0.2.2.tgz",
|
||||
"integrity": "sha512-wzsgA6WOq+09wrU1tsJ09udeR/YZRaeArL9e1wPbFg3GG2yDnC2ldKpxs4xunpFF9DgqCqOIra3bc1HWrJ37Ww==",
|
||||
"engines": {
|
||||
"node": ">=0.4.x"
|
||||
}
|
||||
},
|
||||
"node_modules/framer-motion": {
|
||||
"version": "11.15.0",
|
||||
"resolved": "https://registry.npmmirror.com/framer-motion/-/framer-motion-11.15.0.tgz",
|
||||
"integrity": "sha512-MLk8IvZntxOMg7lDBLw2qgTHHv664bYoYmnFTmE0Gm/FW67aOJk0WM3ctMcG+Xhcv+vh5uyyXwxvxhSeJzSe+w==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"motion-dom": "^11.14.3",
|
||||
"motion-utils": "^11.14.3",
|
||||
"tslib": "^2.4.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@emotion/is-prop-valid": "*",
|
||||
"react": "^18.0.0 || ^19.0.0",
|
||||
"react-dom": "^18.0.0 || ^19.0.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@emotion/is-prop-valid": {
|
||||
"optional": true
|
||||
},
|
||||
"react": {
|
||||
"optional": true
|
||||
},
|
||||
"react-dom": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/fs.realpath": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmmirror.com/fs.realpath/-/fs.realpath-1.0.0.tgz",
|
||||
@ -2880,6 +3056,48 @@
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/hast-util-parse-selector": {
|
||||
"version": "2.2.5",
|
||||
"resolved": "https://registry.npmmirror.com/hast-util-parse-selector/-/hast-util-parse-selector-2.2.5.tgz",
|
||||
"integrity": "sha512-7j6mrk/qqkSehsM92wQjdIgWM2/BW61u/53G6xmC8i1OmEdKLHbk419QKQUjz6LglWsfqoiHmyMRkP1BGjecNQ==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/unified"
|
||||
}
|
||||
},
|
||||
"node_modules/hastscript": {
|
||||
"version": "6.0.0",
|
||||
"resolved": "https://registry.npmmirror.com/hastscript/-/hastscript-6.0.0.tgz",
|
||||
"integrity": "sha512-nDM6bvd7lIqDUiYEiu5Sl/+6ReP0BMk/2f4U/Rooccxkj0P5nm+acM5PrGJ/t5I8qPGiqZSE6hVAwZEdZIvP4w==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/hast": "^2.0.0",
|
||||
"comma-separated-tokens": "^1.0.0",
|
||||
"hast-util-parse-selector": "^2.0.0",
|
||||
"property-information": "^5.0.0",
|
||||
"space-separated-tokens": "^1.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/unified"
|
||||
}
|
||||
},
|
||||
"node_modules/highlight.js": {
|
||||
"version": "10.7.3",
|
||||
"resolved": "https://registry.npmmirror.com/highlight.js/-/highlight.js-10.7.3.tgz",
|
||||
"integrity": "sha512-tzcUFauisWKNHaRkN4Wjl/ZA07gENAjFl3J/c480dprkGTg5EQstgaNFqBfUqCq54kZRIEcreTsAgF/m2quD7A==",
|
||||
"license": "BSD-3-Clause",
|
||||
"engines": {
|
||||
"node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/highlightjs-vue": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmmirror.com/highlightjs-vue/-/highlightjs-vue-1.0.0.tgz",
|
||||
"integrity": "sha512-PDEfEF102G23vHmPhLyPboFCD+BkMGu+GuJe2d9/eH4FsCwvgBpnc9n0pGE+ffKdph38s6foEZiEjdgHdzp+IA==",
|
||||
"license": "CC0-1.0"
|
||||
},
|
||||
"node_modules/ignore": {
|
||||
"version": "5.3.2",
|
||||
"resolved": "https://registry.npmmirror.com/ignore/-/ignore-5.3.2.tgz",
|
||||
@ -2951,6 +3169,30 @@
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/is-alphabetical": {
|
||||
"version": "1.0.4",
|
||||
"resolved": "https://registry.npmmirror.com/is-alphabetical/-/is-alphabetical-1.0.4.tgz",
|
||||
"integrity": "sha512-DwzsA04LQ10FHTZuL0/grVDk4rFoVH1pjAToYwBrHSxcrBIGQuXrQMtD5U1b0U2XVgKZCTLLP8u2Qxqhy3l2Vg==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/wooorm"
|
||||
}
|
||||
},
|
||||
"node_modules/is-alphanumerical": {
|
||||
"version": "1.0.4",
|
||||
"resolved": "https://registry.npmmirror.com/is-alphanumerical/-/is-alphanumerical-1.0.4.tgz",
|
||||
"integrity": "sha512-UzoZUr+XfVz3t3v4KyGEniVL9BDRoQtY7tOyrRybkVNjDFWyo1yhXNGrrBTQxp3ib9BLAWs7k2YKBQsFRkZG9A==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"is-alphabetical": "^1.0.0",
|
||||
"is-decimal": "^1.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/wooorm"
|
||||
}
|
||||
},
|
||||
"node_modules/is-array-buffer": {
|
||||
"version": "3.0.4",
|
||||
"resolved": "https://registry.npmmirror.com/is-array-buffer/-/is-array-buffer-3.0.4.tgz",
|
||||
@ -3026,6 +3268,12 @@
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/is-buffer": {
|
||||
"version": "1.1.6",
|
||||
"resolved": "https://registry.npmmirror.com/is-buffer/-/is-buffer-1.1.6.tgz",
|
||||
"integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/is-bun-module": {
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmmirror.com/is-bun-module/-/is-bun-module-1.2.1.tgz",
|
||||
@ -3096,6 +3344,16 @@
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/is-decimal": {
|
||||
"version": "1.0.4",
|
||||
"resolved": "https://registry.npmmirror.com/is-decimal/-/is-decimal-1.0.4.tgz",
|
||||
"integrity": "sha512-RGdriMmQQvZ2aqaQq3awNA6dCGtKpiDFcOzrTWrDAT2MiWrKQVPmxLGHl7Y2nNu6led0kEyoX0enY0qXYsv9zw==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/wooorm"
|
||||
}
|
||||
},
|
||||
"node_modules/is-extglob": {
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmmirror.com/is-extglob/-/is-extglob-2.1.1.tgz",
|
||||
@ -3155,6 +3413,16 @@
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/is-hexadecimal": {
|
||||
"version": "1.0.4",
|
||||
"resolved": "https://registry.npmmirror.com/is-hexadecimal/-/is-hexadecimal-1.0.4.tgz",
|
||||
"integrity": "sha512-gyPJuv83bHMpocVYoqof5VDiZveEoGoFL8m3BXNb2VW8Xs+rz9kqO8LOQ5DH6EsuvilT1ApazU0pyl+ytbPtlw==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/wooorm"
|
||||
}
|
||||
},
|
||||
"node_modules/is-map": {
|
||||
"version": "2.0.3",
|
||||
"resolved": "https://registry.npmmirror.com/is-map/-/is-map-2.0.3.tgz",
|
||||
@ -3581,6 +3849,20 @@
|
||||
"loose-envify": "cli.js"
|
||||
}
|
||||
},
|
||||
"node_modules/lowlight": {
|
||||
"version": "1.20.0",
|
||||
"resolved": "https://registry.npmmirror.com/lowlight/-/lowlight-1.20.0.tgz",
|
||||
"integrity": "sha512-8Ktj+prEb1RoCPkEOrPMYUN/nCggB7qAWe3a7OpMjWQkh3l2RD5wKRQ+o8Q8YuI9RG/xs95waaI/E6ym/7NsTw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"fault": "^1.0.0",
|
||||
"highlight.js": "~10.7.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/wooorm"
|
||||
}
|
||||
},
|
||||
"node_modules/lru-cache": {
|
||||
"version": "10.4.3",
|
||||
"resolved": "https://registry.npmmirror.com/lru-cache/-/lru-cache-10.4.3.tgz",
|
||||
@ -3596,6 +3878,17 @@
|
||||
"react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0-rc"
|
||||
}
|
||||
},
|
||||
"node_modules/md5": {
|
||||
"version": "2.3.0",
|
||||
"resolved": "https://registry.npmmirror.com/md5/-/md5-2.3.0.tgz",
|
||||
"integrity": "sha512-T1GITYmFaKuO91vxyoQMFETst+O71VUPEU3ze5GNzDm0OWdP8v1ziTaAEPUr/3kLsY3Sftgz242A1SetQiDL7g==",
|
||||
"license": "BSD-3-Clause",
|
||||
"dependencies": {
|
||||
"charenc": "0.0.2",
|
||||
"crypt": "0.0.2",
|
||||
"is-buffer": "~1.1.6"
|
||||
}
|
||||
},
|
||||
"node_modules/merge2": {
|
||||
"version": "1.4.1",
|
||||
"resolved": "https://registry.npmmirror.com/merge2/-/merge2-1.4.1.tgz",
|
||||
@ -3650,6 +3943,18 @@
|
||||
"node": ">=16 || 14 >=14.17"
|
||||
}
|
||||
},
|
||||
"node_modules/motion-dom": {
|
||||
"version": "11.14.3",
|
||||
"resolved": "https://registry.npmmirror.com/motion-dom/-/motion-dom-11.14.3.tgz",
|
||||
"integrity": "sha512-lW+D2wBy5vxLJi6aCP0xyxTxlTfiu+b+zcpVbGVFUxotwThqhdpPRSmX8xztAgtZMPMeU0WGVn/k1w4I+TbPqA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/motion-utils": {
|
||||
"version": "11.14.3",
|
||||
"resolved": "https://registry.npmmirror.com/motion-utils/-/motion-utils-11.14.3.tgz",
|
||||
"integrity": "sha512-Xg+8xnqIJTpr0L/cidfTTBFkvRw26ZtGGuIhA94J9PQ2p4mEa06Xx7QVYZH0BP+EpMSaDlu+q0I0mmvwADPsaQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/ms": {
|
||||
"version": "2.1.3",
|
||||
"resolved": "https://registry.npmmirror.com/ms/-/ms-2.1.3.tgz",
|
||||
@ -3775,6 +4080,16 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/next-themes": {
|
||||
"version": "0.4.4",
|
||||
"resolved": "https://registry.npmmirror.com/next-themes/-/next-themes-0.4.4.tgz",
|
||||
"integrity": "sha512-LDQ2qIOJF0VnuVrrMSMLrWGjRMkq+0mpgl6e0juCLqdJ+oo8Q84JRWT6Wh11VDQKkMMe+dVzDKLWs5n87T+PkQ==",
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"react": "^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc",
|
||||
"react-dom": "^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc"
|
||||
}
|
||||
},
|
||||
"node_modules/next/node_modules/postcss": {
|
||||
"version": "8.4.31",
|
||||
"resolved": "https://registry.npmmirror.com/postcss/-/postcss-8.4.31.tgz",
|
||||
@ -4063,6 +4378,24 @@
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/parse-entities": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmmirror.com/parse-entities/-/parse-entities-2.0.0.tgz",
|
||||
"integrity": "sha512-kkywGpCcRYhqQIchaWqZ875wzpS/bMKhz5HnN3p7wveJTkTtyAB/AlnS0f8DFSqYW1T82t6yEAkEcB+A1I3MbQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"character-entities": "^1.0.0",
|
||||
"character-entities-legacy": "^1.0.0",
|
||||
"character-reference-invalid": "^1.0.0",
|
||||
"is-alphanumerical": "^1.0.0",
|
||||
"is-decimal": "^1.0.0",
|
||||
"is-hexadecimal": "^1.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/wooorm"
|
||||
}
|
||||
},
|
||||
"node_modules/path-exists": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmmirror.com/path-exists/-/path-exists-4.0.0.tgz",
|
||||
@ -4363,11 +4696,19 @@
|
||||
"integrity": "sha512-WuxUnVtlWL1OfZFQFuqvnvs6MiAGk9UNsBostyBOB0Is9wb5uRESevA6rnl/rkksXaGX3GzZhPup5d6Vp1nFew==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/prismjs": {
|
||||
"version": "1.29.0",
|
||||
"resolved": "https://registry.npmmirror.com/prismjs/-/prismjs-1.29.0.tgz",
|
||||
"integrity": "sha512-Kx/1w86q/epKcmte75LNrEoT+lX8pBpavuAbvJWRXar7Hz8jrtF+e3vY751p0R8H9HdArwaCTNDDzHg/ScJK1Q==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/prop-types": {
|
||||
"version": "15.8.1",
|
||||
"resolved": "https://registry.npmmirror.com/prop-types/-/prop-types-15.8.1.tgz",
|
||||
"integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"loose-envify": "^1.4.0",
|
||||
@ -4375,6 +4716,19 @@
|
||||
"react-is": "^16.13.1"
|
||||
}
|
||||
},
|
||||
"node_modules/property-information": {
|
||||
"version": "5.6.0",
|
||||
"resolved": "https://registry.npmmirror.com/property-information/-/property-information-5.6.0.tgz",
|
||||
"integrity": "sha512-YUHSPk+A30YPv+0Qf8i9Mbfe/C0hdPXk1s1jPVToV8pk8BQtpw10ct89Eo7OWkutrwqvT0eicAxlOg3dOAu8JA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"xtend": "^4.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/wooorm"
|
||||
}
|
||||
},
|
||||
"node_modules/punycode": {
|
||||
"version": "2.3.1",
|
||||
"resolved": "https://registry.npmmirror.com/punycode/-/punycode-2.3.1.tgz",
|
||||
@ -4430,13 +4784,46 @@
|
||||
"react": "^18.3.1"
|
||||
}
|
||||
},
|
||||
"node_modules/react-dropzone": {
|
||||
"version": "14.3.5",
|
||||
"resolved": "https://registry.npmmirror.com/react-dropzone/-/react-dropzone-14.3.5.tgz",
|
||||
"integrity": "sha512-9nDUaEEpqZLOz5v5SUcFA0CjM4vq8YbqO0WRls+EYT7+DvxUdzDPKNCPLqGfj3YL9MsniCLCD4RFA6M95V6KMQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"attr-accept": "^2.2.4",
|
||||
"file-selector": "^2.1.0",
|
||||
"prop-types": "^15.8.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 10.13"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": ">= 16.8 || 18.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/react-is": {
|
||||
"version": "16.13.1",
|
||||
"resolved": "https://registry.npmmirror.com/react-is/-/react-is-16.13.1.tgz",
|
||||
"integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/react-syntax-highlighter": {
|
||||
"version": "15.6.1",
|
||||
"resolved": "https://registry.npmmirror.com/react-syntax-highlighter/-/react-syntax-highlighter-15.6.1.tgz",
|
||||
"integrity": "sha512-OqJ2/vL7lEeV5zTJyG7kmARppUjiB9h9udl4qHQjjgEos66z00Ia0OckwYfRxCSFrW8RJIBnsBwQsHZbVPspqg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.3.1",
|
||||
"highlight.js": "^10.4.1",
|
||||
"highlightjs-vue": "^1.0.0",
|
||||
"lowlight": "^1.17.0",
|
||||
"prismjs": "^1.27.0",
|
||||
"refractor": "^3.6.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": ">= 0.14.0"
|
||||
}
|
||||
},
|
||||
"node_modules/react-toastify": {
|
||||
"version": "10.0.6",
|
||||
"resolved": "https://registry.npmmirror.com/react-toastify/-/react-toastify-10.0.6.tgz",
|
||||
@ -4493,6 +4880,30 @@
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/refractor": {
|
||||
"version": "3.6.0",
|
||||
"resolved": "https://registry.npmmirror.com/refractor/-/refractor-3.6.0.tgz",
|
||||
"integrity": "sha512-MY9W41IOWxxk31o+YvFCNyNzdkc9M20NoZK5vq6jkv4I/uh2zkWcfudj0Q1fovjUQJrNewS9NMzeTtqPf+n5EA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"hastscript": "^6.0.0",
|
||||
"parse-entities": "^2.0.0",
|
||||
"prismjs": "~1.27.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/wooorm"
|
||||
}
|
||||
},
|
||||
"node_modules/refractor/node_modules/prismjs": {
|
||||
"version": "1.27.0",
|
||||
"resolved": "https://registry.npmmirror.com/prismjs/-/prismjs-1.27.0.tgz",
|
||||
"integrity": "sha512-t13BGPUlFDR7wRB5kQDG4jjl7XeuH6jbJGt11JHPL96qwsEHNX2+68tFXqc1/k+/jALsbSWJKUOT/hcYAZ5LkA==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/regenerator-runtime": {
|
||||
"version": "0.14.1",
|
||||
"resolved": "https://registry.npmmirror.com/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz",
|
||||
@ -4791,6 +5202,16 @@
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/space-separated-tokens": {
|
||||
"version": "1.1.5",
|
||||
"resolved": "https://registry.npmmirror.com/space-separated-tokens/-/space-separated-tokens-1.1.5.tgz",
|
||||
"integrity": "sha512-q/JSVd1Lptzhf5bkYm4ob4iWPjx0KiRe3sRFBNrVqbJkFaBm5vbbowy1mymoPNLRa52+oadOhJ+K49wsSeSjTA==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/wooorm"
|
||||
}
|
||||
},
|
||||
"node_modules/streamsearch": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmmirror.com/streamsearch/-/streamsearch-1.1.0.tgz",
|
||||
@ -5396,6 +5817,12 @@
|
||||
"uuid": "dist/bin/uuid"
|
||||
}
|
||||
},
|
||||
"node_modules/uzip": {
|
||||
"version": "0.20201231.0",
|
||||
"resolved": "https://registry.npmmirror.com/uzip/-/uzip-0.20201231.0.tgz",
|
||||
"integrity": "sha512-OZeJfZP+R0z9D6TmBgLq2LHzSSptGMGDGigGiEe0pr8UBe/7fdflgHlHBNDASTXB5jnFuxHpNaJywSg8YFeGng==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/which": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmmirror.com/which/-/which-2.0.2.tgz",
|
||||
@ -5605,6 +6032,15 @@
|
||||
"dev": true,
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/xtend": {
|
||||
"version": "4.0.2",
|
||||
"resolved": "https://registry.npmmirror.com/xtend/-/xtend-4.0.2.tgz",
|
||||
"integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/yallist": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmmirror.com/yallist/-/yallist-4.0.0.tgz",
|
||||
|
@ -12,23 +12,31 @@
|
||||
"@radix-ui/react-label": "^2.1.0",
|
||||
"@radix-ui/react-scroll-area": "^1.2.1",
|
||||
"@radix-ui/react-slot": "^1.1.0",
|
||||
"browser-image-compression": "^2.0.2",
|
||||
"class-variance-authority": "^0.7.0",
|
||||
"clsx": "^2.1.1",
|
||||
"framer-motion": "^11.15.0",
|
||||
"js-yaml": "^4.1.0",
|
||||
"lucide-react": "^0.460.0",
|
||||
"md5": "^2.3.0",
|
||||
"next": "14.2.16",
|
||||
"next-auth": "^4.24.10",
|
||||
"next-themes": "^0.4.4",
|
||||
"react": "^18",
|
||||
"react-dom": "^18",
|
||||
"react-dropzone": "^14.3.5",
|
||||
"react-syntax-highlighter": "^15.6.1",
|
||||
"react-toastify": "^10.0.6",
|
||||
"tailwind-merge": "^2.5.4",
|
||||
"tailwindcss-animate": "^1.0.7"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/js-yaml": "^4.0.9",
|
||||
"@types/md5": "^2.3.5",
|
||||
"@types/node": "^20",
|
||||
"@types/react": "^18",
|
||||
"@types/react-dom": "^18",
|
||||
"@types/react-syntax-highlighter": "^15.5.13",
|
||||
"eslint": "^8",
|
||||
"eslint-config-next": "14.2.16",
|
||||
"postcss": "^8",
|
||||
|
BIN
public/img/CatgirlGenerator/example.jpg
Normal file
BIN
public/img/CatgirlGenerator/example.jpg
Normal file
Binary file not shown.
After Width: | Height: | Size: 64 KiB |
BIN
public/img/CatgirlGenerator/step1.jpg
Normal file
BIN
public/img/CatgirlGenerator/step1.jpg
Normal file
Binary file not shown.
After Width: | Height: | Size: 54 KiB |
BIN
public/img/CatgirlGenerator/step2.jpg
Normal file
BIN
public/img/CatgirlGenerator/step2.jpg
Normal file
Binary file not shown.
After Width: | Height: | Size: 45 KiB |
@ -9,6 +9,14 @@ const config: Config = {
|
||||
],
|
||||
theme: {
|
||||
extend: {
|
||||
backgroundColor: {
|
||||
'dark-primary': '#1a202c',
|
||||
'dark-secondary': '#2d3748',
|
||||
},
|
||||
textColor: {
|
||||
'dark-primary': '#e2e8f0',
|
||||
'dark-secondary': '#a0aec0',
|
||||
},
|
||||
colors: {
|
||||
background: 'hsl(var(--background))',
|
||||
foreground: 'hsl(var(--foreground))',
|
||||
|
Loading…
Reference in New Issue
Block a user