1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
import React, { useState, useEffect } from 'react';
import { Link, useNavigate } from 'react-router-dom';
import { useTranslation } from '../js/i18n';
import SEO from '../components/SEO';
const tools = [
{ id: 'handwrite', icon: '/assets/icon/handwrite.png', path: '/handwriting' },
{ id: 'quoteCard', icon: '/assets/icon/quotecard.png', path: '/quote-card' },
{ id: 'markdown2image', icon: '/assets/icon/markdown2image.png', path: '/markdown-to-image' },
{ id: 'subtitleGenerator', icon: '/assets/icon/subtitle2image.png', path: '/subtitle-to-image' },
{ id: 'imageCompressor', icon: '/assets/icon/image-compressor.png', path: '/image-compressor' },
{ id: 'imageWatermark', icon: '/assets/icon/image-watermark.png', path: '/image-watermark' },
{ id: 'imageBackgroundRemover', icon: '/assets/icon/image-background-remover.png', path: '/background-remover' },
{ id: 'textBehindImage', icon: '/assets/icon/text-behind-image.png', path: '/text-behind-image' },
{ id: 'latex2image', icon: '/assets/icon/latex2image.png', path: '/latex-to-image' },
{ id: 'jsonFormatter', icon: '/assets/icon/json-format.png', path: '/json-formatter' },
{ id: 'urlEncodeDecode', icon: '/assets/icon/url-endecode.png', path: '/url-encode-and-decode' },
{ id: 'imageBase64Converter', icon: '/assets/icon/image-base64.png', path: '/image-base64' },
{ id: 'textDiff', icon: '/assets/icon/diff.png', path: '/text-diff' },
{ id: 'openAITimeline', icon: '/assets/icon/openai_small.svg', path: '/openai-timeline' },
{ id: 'anthropicTimeline', icon: '/assets/icon/anthropic_small.svg', path: '/anthropic-timeline' },
{ id: 'deepSeekTimeline', icon: '/assets/icon/deepseek_small.jpg', path: '/deepseek-timeline' },
{ id: 'modelPrice', icon: '/assets/icon/model-price.svg', path: '/llm-model-price' },
{ id: 'drugsList', icon: '/assets/icon/drugs.svg', path: '/drugs-list' },
{ id: 'fisherai', icon: '/assets/icon/fisherai.png', path: 'https://chromewebstore.google.com/detail/fisherai-your-best-summar/ipfiijaobcenaibdpaacbbpbjefgekbj', external: true }
];
const Home = () => {
const { t } = useTranslation();
const [loading, setLoading] = useState('');
const navigate = useNavigate();
useEffect(() => {
const handleClearLoading = () => {
setLoading('');
};
window.addEventListener('clearLoadingState', handleClearLoading);
return () => {
window.removeEventListener('clearLoadingState', handleClearLoading);
};
}, []);
const handleNavigate = (tool) => {
if (tool.external) {
window.open(tool.path, '_blank', 'noopener,noreferrer');
return;
}
setLoading(tool.id);
window.scrollTo(0, 0);
navigate(tool.path);
};
const renderToolLink = (tool) => {
const content = (
<div className={`group flex items-center gap-4 p-6 bg-white rounded-xl shadow-lg hover:shadow-xl transform hover:-translate-y-1 transition-all duration-300 relative ${loading === tool.id ? 'pointer-events-none' : ''}`}>
{loading === tool.id && (
<div className="absolute inset-0 bg-white/80 rounded-xl flex items-center justify-center z-10">
<div className="w-8 h-8 border-4 border-indigo-500 border-t-transparent rounded-full animate-spin"></div>
</div>
)}
<img
src={tool.icon}
alt={`${t(`tools.${tool.id}.title`)} icon`}
className="w-12 h-12 object-contain group-hover:scale-110 transition-transform duration-300"
loading="lazy"
/>
<div className="flex-1">
<h3 className="text-lg font-semibold text-gray-800 mb-1 group-hover:text-indigo-600 transition-colors duration-300">
{t(`tools.${tool.id}.title`)}
</h3>
<p className="text-sm text-gray-600 overflow-hidden text-ellipsis [-webkit-line-clamp:2] [display:-webkit-box] [-webkit-box-orient:vertical]">
{t(`tools.${tool.id}.description`)}
</p>
</div>
</div>
);
return tool.external ? (
<a
onClick={() => handleNavigate(tool)}
className="block cursor-pointer"
>
{content}
</a>
) : (
<div onClick={() => handleNavigate(tool)} className="block cursor-pointer">
{content}
</div>
);
};
return (
<>
<SEO
title={t('title')}
description={t('slogan')}
/>
<main className="min-h-screen bg-gradient-to-br from-indigo-50/50 via-white to-indigo-50/50 pt-16">
{/* Hero Section */}
<div className="relative overflow-hidden">
<div className="absolute inset-0 bg-grid-pattern opacity-5"></div>
<div className="max-w-7xl mx-auto px-4 pt-20 sm:pt-32 pb-12 sm:pb-20">
<div className="text-center relative z-10">
<h1 className="text-4xl sm:text-5xl font-bold text-indigo-900/90 mb-4 sm:mb-6 animate-fade-in">
AI Toolbox
</h1>
<p className="text-lg sm:text-xl text-indigo-800/80 max-w-2xl mx-auto mb-8 sm:mb-12 animate-fade-in-delay px-4">
{t('slogan')}
</p>
<div className="w-full h-0.5 max-w-xs mx-auto bg-gradient-to-r from-transparent via-indigo-400/50 to-transparent opacity-75"></div>
</div>
</div>
</div>
{/* Tools Grid */}
<div className="max-w-7xl mx-auto px-4 py-8 sm:py-16">
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4 sm:gap-8">
{tools.map(tool => (
<React.Fragment key={tool.id}>
{renderToolLink(tool)}
</React.Fragment>
))}
</div>
</div>
{/* Footer Navigation */}
<div className="max-w-7xl mx-auto px-4 pb-12 sm:pb-20">
<div className="flex flex-wrap justify-center gap-4 sm:gap-8">
<a
href="https://github.com/fisherdaddy/ai-toolbox"
target="_blank"
rel="noopener noreferrer"
className="group flex items-center px-6 py-3 rounded-full bg-white/80 hover:bg-white shadow-sm hover:shadow-md transition-all duration-300"
>
<svg className="w-5 h-5 mr-3 text-gray-700 group-hover:text-indigo-500 transition-colors" viewBox="0 0 24 24" fill="currentColor">
<path d="M12 0C5.37 0 0 5.37 0 12c0 5.31 3.435 9.795 8.205 11.385.6.105.825-.255.825-.57 0-.285-.015-1.23-.015-2.235-3.015.555-3.795-.735-4.035-1.41-.135-.345-.72-1.41-1.23-1.695-.42-.225-1.02-.78-.015-.795.945-.015 1.62.87 1.845 1.23 1.08 1.815 2.805 1.305 3.495.99.105-.78.42-1.305.765-1.605-2.67-.3-5.46-1.335-5.46-5.925 0-1.305.465-2.385 1.23-3.225-.12-.3-.54-1.53.12-3.18 0 0 1.005-.315 3.3 1.23.96-.27 1.98-.405 3-.405s2.04.135 3 .405c2.295-1.56 3.3-1.23 3.3-1.23.66 1.65.24 2.88.12 3.18.765.84 1.23 1.905 1.23 3.225 0 4.605-2.805 5.625-5.475 5.925.435.375.81 1.095.81 2.22 0 1.605-.015 2.895-.015 3.3 0 .315.225.69.825.57A12.02 12.02 0 0024 12c0-6.63-5.37-12-12-12z"/>
</svg>
<span className="text-gray-700 group-hover:text-indigo-500 font-medium transition-colors">GitHub</span>
</a>
<Link
to="/about"
className="group flex items-center px-6 py-3 rounded-full bg-white/80 hover:bg-white shadow-sm hover:shadow-md transition-all duration-300"
>
<svg className="w-5 h-5 mr-3 text-gray-700 group-hover:text-indigo-500 transition-colors" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
<path strokeLinecap="round" strokeLinejoin="round" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
<span className="text-gray-700 group-hover:text-indigo-500 font-medium transition-colors">{t('navigation.about')}</span>
</Link>
</div>
</div>
</main>
<style jsx global>{`
.bg-grid-pattern {
background-image: radial-gradient(circle at 1px 1px, rgb(226 232 240 / 30%) 1px, transparent 0);
background-size: 24px 24px;
}
.animate-fade-in {
animation: fadeIn 0.8s ease-out;
}
.animate-fade-in-delay {
animation: fadeIn 0.8s ease-out 0.2s both;
}
@keyframes fadeIn {
from {
opacity: 0;
transform: translateY(10px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
`}</style>
</>
);
};
export default Home;