|
1 | | -'use client'; |
| 1 | +import ScriptGenerator from "../components/ScriptGenerator/ScriptGenerator"; |
2 | 2 |
|
3 | | -import { useEffect, useState } from "react"; |
4 | | - |
5 | | -// Define types |
6 | | -type OS = "windows" | "macos" | "linux"; |
7 | | -type WindowsPkg = "choco" | "winget" | "scoop"; |
8 | | -type MacPkg = "homebrew"; |
9 | | -type LinuxPkg = "apt" | "dnf" | "pacman"; |
10 | | -type PkgManager = WindowsPkg | MacPkg | LinuxPkg; |
11 | | - |
12 | | -interface Tool { |
13 | | - name: string; |
14 | | - iconsrc: string; |
15 | | - install: Partial<Record<PkgManager, string>>; |
16 | | -} |
17 | | - |
18 | | -interface ToolCategory { |
19 | | - category: string; |
20 | | - tools: Tool[]; |
21 | | -} |
22 | | - |
23 | | -const osOptions: OS[] = ["windows", "macos", "linux"]; |
24 | | -const pkgManagers: Record<OS, PkgManager[]> = { |
25 | | - windows: ["choco", "winget", "scoop"], |
26 | | - macos: ["homebrew"], |
27 | | - linux: ["apt", "dnf", "pacman"], |
28 | | -}; |
29 | | - |
30 | | -export default function ScriptGenerator() { |
31 | | - const [toolsData, setToolsData] = useState<ToolCategory[]>([]); |
32 | | - const [selectedOS, setSelectedOS] = useState<OS>("windows"); |
33 | | - const [selectedPkg, setSelectedPkg] = useState<PkgManager>("choco"); |
34 | | - const [selectedTools, setSelectedTools] = useState<string[]>([]); |
35 | | - const [loading, setLoading] = useState(true); |
36 | | - const [searchQuery, setSearchQuery] = useState<string>(""); |
37 | | - |
38 | | - // Theme |
39 | | - const [theme, setTheme] = useState<"light" | "dark">("dark"); |
40 | | - useEffect(() => { |
41 | | - document.documentElement.setAttribute("data-theme", theme); |
42 | | - }, [theme]); |
43 | | - |
44 | | - // Fetch tools |
45 | | - useEffect(() => { |
46 | | - const fetchTools = async () => { |
47 | | - try { |
48 | | - const res = await fetch("./tools.json"); |
49 | | - if (!res.ok) throw new Error("Failed to fetch JSON"); |
50 | | - const data: ToolCategory[] = await res.json(); |
51 | | - setToolsData(data); |
52 | | - } catch { |
53 | | - alert("Failed to load tool data."); |
54 | | - } finally { |
55 | | - setLoading(false); |
56 | | - } |
57 | | - }; |
58 | | - fetchTools(); |
59 | | - }, []); |
60 | | - |
61 | | - const handleToolSelect = (toolName: string) => { |
62 | | - setSelectedTools((prev) => |
63 | | - prev.includes(toolName) ? prev.filter((t) => t !== toolName) : [...prev, toolName] |
64 | | - ); |
65 | | - }; |
66 | | - |
67 | | - const generateScript = () => |
68 | | - toolsData |
69 | | - .flatMap((category) => |
70 | | - category.tools |
71 | | - .filter((tool) => selectedTools.includes(tool.name) && tool.install[selectedPkg]) |
72 | | - .map((tool) => tool.install[selectedPkg]!) |
73 | | - ) |
74 | | - .join("\n"); |
75 | | - |
76 | | - const handleCopy = async () => { |
77 | | - try { |
78 | | - await navigator.clipboard.writeText(generateScript()); |
79 | | - alert("Script copied!"); |
80 | | - } catch { |
81 | | - alert("Failed to copy"); |
82 | | - } |
83 | | - }; |
84 | | - |
85 | | - const handleDownload = () => { |
86 | | - const blob = new Blob([generateScript()], { type: "text/plain" }); |
87 | | - const link = document.createElement("a"); |
88 | | - link.href = URL.createObjectURL(blob); |
89 | | - link.download = "install_script.sh"; |
90 | | - link.click(); |
91 | | - }; |
92 | | - |
93 | | - if (loading) |
94 | | - return ( |
95 | | - <div className="text-[var(--foreground)] font-sans text-center mt-20"> |
96 | | - <div>Loading tools data...</div> |
97 | | - <div className="spinner">⏳</div> |
98 | | - </div> |
99 | | - ); |
100 | | - |
101 | | - return ( |
102 | | - <div |
103 | | - className="min-h-screen font-sans bg-[var(--background)] text-[var(--foreground)] transition-colors duration-300" |
104 | | - data-theme={theme} |
105 | | - > |
106 | | - <div className="max-w-screen mx-auto px-4 py-10"> |
107 | | - {/* Header with Title, Search, and Action buttons */} |
108 | | - <div className="flex flex-col lg:flex-row lg:items-center lg:justify-between gap-6 mb-12"> |
109 | | - {/* Left side - Title */} |
110 | | - <h1 className="text-4xl sm:text-6xl font-extrabold tracking-tight text-left"> |
111 | | - <span className="text-transparent bg-clip-text bg-gradient-to-r from-indigo-500 via-purple-600 to-pink-500"> |
112 | | - DevSetup |
113 | | - </span> |
114 | | - </h1> |
115 | | - |
116 | | - {/* Center/Bottom - Search Box */} |
117 | | - <div className="relative w-full max-w-2xl lg:max-w-3xl hidden sm:block"> |
118 | | - <input |
119 | | - type="text" |
120 | | - placeholder="Search for development tools..." |
121 | | - value={searchQuery} |
122 | | - onChange={(e) => setSearchQuery(e.target.value)} |
123 | | - className="w-full px-8 py-4 pl-16 bg-gradient-to-r from-[var(--card-bg)] to-gray-800/50 text-[var(--foreground)] border-2 border-gray-600/50 rounded-2xl focus:outline-none focus:ring-4 focus:ring-indigo-500/30 focus:border-indigo-400 transition-all duration-500 text-lg placeholder-gray-400 shadow-2xl hover:shadow-indigo-500/20 backdrop-blur-sm" |
124 | | - /> |
125 | | - <div className="absolute left-5 top-1/2 transform -translate-y-1/2"> |
126 | | - <svg |
127 | | - className="w-6 h-6 text-indigo-400" |
128 | | - fill="none" |
129 | | - stroke="currentColor" |
130 | | - viewBox="0 0 24 24" |
131 | | - > |
132 | | - <path |
133 | | - strokeLinecap="round" |
134 | | - strokeLinejoin="round" |
135 | | - strokeWidth={2.5} |
136 | | - d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" |
137 | | - /> |
138 | | - </svg> |
139 | | - </div> |
140 | | - <div className="absolute inset-0 rounded-2xl bg-gradient-to-r from-indigo-500/10 to-purple-500/10 pointer-events-none"></div> |
141 | | - </div> |
142 | | - |
143 | | - {/* Right side - Action buttons */} |
144 | | - <div className="flex gap-4 lg:flex-shrink-0"> |
145 | | - {/* Add New + button */} |
146 | | - <a |
147 | | - href="https://forms.gle/cWfDnzvYo5dBuy7C7" |
148 | | - target="_blank" |
149 | | - rel="noopener noreferrer" |
150 | | - className="px-8 py-4 text-lg bg-gradient-to-r from-indigo-500 via-purple-600 to-pink-500 hover:from-indigo-600 hover:via-purple-700 hover:to-pink-600 text-white font-bold rounded-2xl shadow-2xl hover:shadow-indigo-500/30 transition-all duration-300 transform hover:scale-105 hover:-translate-y-1" |
151 | | - > |
152 | | - Add New + |
153 | | - </a> |
154 | | - |
155 | | - {/* Theme Toggle button */} |
156 | | - <button |
157 | | - onClick={() => setTheme(theme === "light" ? "dark" : "light")} |
158 | | - className="px-8 py-4 text-lg bg-gradient-to-r from-gray-600 to-gray-800 hover:from-gray-700 hover:to-gray-900 text-white font-bold rounded-2xl shadow-2xl hover:shadow-gray-500/30 transition-all duration-300 transform hover:scale-105 hover:-translate-y-1" |
159 | | - > |
160 | | - {theme === "light" ? "🌞 Light" : "🌙 Dark"} |
161 | | - </button> |
162 | | - </div> |
163 | | - </div> |
164 | | - |
165 | | - {/* Mobile Search Box */} |
166 | | - <div className="mb-10 sm:hidden"> |
167 | | - <div className="relative"> |
168 | | - <input |
169 | | - type="text" |
170 | | - placeholder="🔍 Search for development tools..." |
171 | | - value={searchQuery} |
172 | | - onChange={(e) => setSearchQuery(e.target.value)} |
173 | | - className="w-full px-8 py-5 pl-16 bg-gradient-to-r from-[var(--card-bg)] to-gray-800/50 text-[var(--foreground)] border-2 border-gray-600/50 rounded-2xl focus:outline-none focus:ring-4 focus:ring-indigo-500/30 focus:border-indigo-400 transition-all duration-500 text-lg placeholder-gray-400 shadow-2xl hover:shadow-indigo-500/20 backdrop-blur-sm" |
174 | | - /> |
175 | | - <div className="absolute left-5 top-1/2 transform -translate-y-1/2"> |
176 | | - <svg |
177 | | - className="w-6 h-6 text-indigo-400" |
178 | | - fill="none" |
179 | | - stroke="currentColor" |
180 | | - viewBox="0 0 24 24" |
181 | | - > |
182 | | - <path |
183 | | - strokeLinecap="round" |
184 | | - strokeLinejoin="round" |
185 | | - strokeWidth={2.5} |
186 | | - d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" |
187 | | - /> |
188 | | - </svg> |
189 | | - </div> |
190 | | - <div className="absolute inset-0 rounded-2xl bg-gradient-to-r from-indigo-500/10 to-purple-500/10 pointer-events-none"></div> |
191 | | - </div> |
192 | | - </div> |
193 | | - |
194 | | - {/* OS Selector */} |
195 | | - <div className="flex flex-wrap gap-3 justify-center sm:justify-start mb-6"> |
196 | | - {osOptions.map((os) => ( |
197 | | - <button |
198 | | - key={os} |
199 | | - onClick={() => { |
200 | | - setSelectedOS(os); |
201 | | - setSelectedPkg(pkgManagers[os][0]); |
202 | | - setSelectedTools([]); |
203 | | - }} |
204 | | - className={`px-6 py-3 rounded-full text-sm font-semibold transition duration-200 ${ |
205 | | - selectedOS === os |
206 | | - ? "bg-gradient-to-r from-indigo-500 to-purple-600 text-white shadow-lg" |
207 | | - : "bg-[var(--button-bg)] text-[var(--button-text)] hover:bg-gray-700" |
208 | | - }`} |
209 | | - > |
210 | | - {os.toUpperCase()} |
211 | | - </button> |
212 | | - ))} |
213 | | - </div> |
214 | | - |
215 | | - {/* Package Manager Selector */} |
216 | | - <div className="flex flex-wrap gap-3 justify-center sm:justify-start mb-10"> |
217 | | - {pkgManagers[selectedOS].map((pkg) => ( |
218 | | - <button |
219 | | - key={pkg} |
220 | | - onClick={() => { |
221 | | - setSelectedPkg(pkg); |
222 | | - setSelectedTools([]); |
223 | | - }} |
224 | | - className={`px-6 py-3 rounded-full text-sm font-semibold transition duration-200 ${ |
225 | | - selectedPkg === pkg |
226 | | - ? "bg-gradient-to-r from-green-500 to-green-600 text-white shadow-lg" |
227 | | - : "bg-[var(--button-bg)] text-[var(--button-text)] hover:bg-gray-700" |
228 | | - }`} |
229 | | - > |
230 | | - {pkg} |
231 | | - </button> |
232 | | - ))} |
233 | | - </div> |
234 | | - |
235 | | - {/* Tool Categories */} |
236 | | - {toolsData.map((group, i) => { |
237 | | - // Filter tools based on search query |
238 | | - const filteredTools = group.tools.filter(tool => |
239 | | - tool.name.toLowerCase().includes(searchQuery.toLowerCase()) |
240 | | - ); |
241 | | - |
242 | | - // Only render category if it has matching tools |
243 | | - if (filteredTools.length === 0) return null; |
244 | | - |
245 | | - return ( |
246 | | - <div key={i} className="mb-10"> |
247 | | - <h2 className="text-2xl font-semibold mb-4 border-b border-gray-700 pb-1"> |
248 | | - {group.category} |
249 | | - </h2> |
250 | | - <div className="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 xl:grid-cols-5 gap-6"> |
251 | | - {filteredTools.map((tool, index) => { |
252 | | - const isAvailable = !!tool.install[selectedPkg]; |
253 | | - return ( |
254 | | - <label |
255 | | - key={index} |
256 | | - className={`relative flex flex-col items-center justify-center rounded-2xl p-4 transition cursor-pointer border border-transparent bg-[var(--card-bg)] ${ |
257 | | - isAvailable? "hover:shadow-xl hover:border-indigo-500" : "opacity-50 cursor-not-allowed" |
258 | | - }`} |
259 | | - > |
260 | | - <input |
261 | | - type="checkbox" |
262 | | - disabled={!isAvailable} |
263 | | - checked={selectedTools.includes(tool.name)} |
264 | | - onChange={() => handleToolSelect(tool.name)} |
265 | | - className="absolute top-2 right-2 w-5 h-5 accent-indigo-500" |
266 | | - /> |
267 | | - <span className="text-sm text-center">{tool.name}</span> |
268 | | - </label> |
269 | | - ); |
270 | | - })} |
271 | | - </div> |
272 | | - </div> |
273 | | - ); |
274 | | - })} |
275 | | - |
276 | | - {/* Script Output */} |
277 | | - <div className="mt-12"> |
278 | | - <h3 className="text-lg font-semibold mb-2">Generated Script:</h3> |
279 | | - <textarea |
280 | | - readOnly |
281 | | - value={generateScript()} |
282 | | - rows={6} |
283 | | - className="w-full bg-[var(--card-bg)] text-[var(--foreground)] border border-gray-600 rounded-lg p-4 font-mono resize-none transition-colors duration-300" |
284 | | - /> |
285 | | - </div> |
286 | | - |
287 | | - {/* Actions */} |
288 | | - <div className="mt-6 flex flex-wrap gap-4"> |
289 | | - <button |
290 | | - onClick={handleCopy} |
291 | | - className="px-6 py-3 bg-blue-600 hover:bg-blue-700 text-white font-semibold rounded-full shadow-lg transition duration-300" |
292 | | - > |
293 | | - Copy to Clipboard |
294 | | - </button> |
295 | | - <button |
296 | | - onClick={handleDownload} |
297 | | - className="px-6 py-3 bg-indigo-600 hover:bg-indigo-700 text-white font-semibold rounded-full shadow-lg transition duration-300" |
298 | | - > |
299 | | - Download Script |
300 | | - </button> |
301 | | - </div> |
302 | | - </div> |
303 | | - </div> |
304 | | - ); |
| 3 | +export default function Home() { |
| 4 | + return <ScriptGenerator />; |
305 | 5 | } |
0 commit comments