1+ "use client"
2+
3+ import { useState , useEffect } from "react"
4+ import Fuse from "fuse.js"
5+ import { Search , Copy , Check } from "lucide-react"
6+ import { Input } from "@/components/ui/input"
7+ import { Button } from "@/components/ui/button"
8+ import {
9+ Accordion ,
10+ AccordionContent ,
11+ AccordionItem ,
12+ AccordionTrigger ,
13+ } from "@/components/ui/accordion"
14+ import { ScrollArea } from "@/components/ui/scroll-area"
15+ import { Skeleton } from "@/components/ui/skeleton"
16+ import { Alert , AlertDescription , AlertTitle } from "@/components/ui/alert"
17+ import { motion , AnimatePresence } from "framer-motion"
18+
19+ const CHEAT_SHEET_URL = "https://raw.githubusercontent.com/amine250/cheatsheet/refs/heads/main/cheat-sheet-data.json"
20+
21+ type CheatSheetItem = {
22+ title : string
23+ content : string
24+ }
25+
26+ type CheatSheetCategory = {
27+ category : string
28+ items : CheatSheetItem [ ]
29+ }
30+
31+ const fuseOptions = {
32+ keys : [ "category" , "items.title" , "items.content" ] ,
33+ threshold : 0.4 ,
34+ }
35+
36+ export default function ITCheatSheet ( ) {
37+ const [ cheatSheetData , setCheatSheetData ] = useState < CheatSheetCategory [ ] > ( [ ] )
38+ const [ searchQuery , setSearchQuery ] = useState ( "" )
39+ const [ searchResults , setSearchResults ] = useState < CheatSheetCategory [ ] > ( [ ] )
40+ const [ isLoading , setIsLoading ] = useState ( true )
41+ const [ error , setError ] = useState < string | null > ( null )
42+ const [ copiedStates , setCopiedStates ] = useState < { [ key : string ] : boolean } > ( { } )
43+
44+ useEffect ( ( ) => {
45+ const fetchCheatSheetData = async ( ) => {
46+ try {
47+ const response = await fetch ( CHEAT_SHEET_URL )
48+ if ( ! response . ok ) {
49+ throw new Error ( "Failed to fetch cheat sheet data" )
50+ }
51+ const data = await response . json ( )
52+ setCheatSheetData ( data )
53+ setSearchResults ( data )
54+ } catch ( err ) {
55+ setError ( "Error loading cheat sheet data. Please try again later." )
56+ } finally {
57+ setIsLoading ( false )
58+ }
59+ }
60+
61+ fetchCheatSheetData ( )
62+ } , [ ] )
63+
64+ useEffect ( ( ) => {
65+ if ( cheatSheetData . length > 0 ) {
66+ const fuse = new Fuse ( cheatSheetData , fuseOptions )
67+
68+ if ( searchQuery === "" ) {
69+ setSearchResults ( cheatSheetData )
70+ } else {
71+ const results = fuse . search ( searchQuery ) . map ( ( result ) => result . item )
72+ setSearchResults ( results )
73+ }
74+ }
75+ } , [ searchQuery , cheatSheetData ] )
76+
77+ const handleSearch = ( e : React . ChangeEvent < HTMLInputElement > ) => {
78+ setSearchQuery ( e . target . value )
79+ }
80+
81+ const handleCopy = ( content : string , itemId : string ) => {
82+ navigator . clipboard . writeText ( content )
83+ setCopiedStates ( { ...copiedStates , [ itemId ] : true } )
84+ setTimeout ( ( ) => {
85+ setCopiedStates ( { ...copiedStates , [ itemId ] : false } )
86+ } , 2000 )
87+ }
88+
89+ if ( isLoading ) {
90+ return (
91+ < div className = "container mx-auto p-4 max-w-6xl" >
92+ < h1 className = "text-4xl font-bold mb-6 text-primary text-center" > Amine's Cheat Sheet</ h1 >
93+ < Skeleton className = "w-full h-10 mb-6" />
94+ < div className = "grid grid-cols-1 md:grid-cols-2 gap-4" >
95+ { [ 1 , 2 , 3 , 4 ] . map ( ( i ) => (
96+ < Skeleton key = { i } className = "w-full h-40" />
97+ ) ) }
98+ </ div >
99+ </ div >
100+ )
101+ }
102+
103+ if ( error ) {
104+ return (
105+ < div className = "container mx-auto p-4 max-w-6xl" >
106+ < Alert variant = "destructive" >
107+ < AlertTitle > Error</ AlertTitle >
108+ < AlertDescription > { error } </ AlertDescription >
109+ </ Alert >
110+ </ div >
111+ )
112+ }
113+
114+ const leftColumnCategories = searchResults . filter ( ( _ , index ) => index % 2 === 0 )
115+ const rightColumnCategories = searchResults . filter ( ( _ , index ) => index % 2 !== 0 )
116+
117+ return (
118+ < div className = "container mx-auto p-4 max-w-6xl" >
119+ < motion . h1
120+ initial = { { opacity : 0 , y : - 20 } }
121+ animate = { { opacity : 1 , y : 0 } }
122+ transition = { { duration : 0.5 } }
123+ className = "text-4xl font-bold mb-6 text-primary text-center"
124+ >
125+ Amine's Cheat Sheet
126+ </ motion . h1 >
127+ < motion . div
128+ initial = { { opacity : 0 , y : 20 } }
129+ animate = { { opacity : 1 , y : 0 } }
130+ transition = { { duration : 0.5 , delay : 0.2 } }
131+ className = "relative mb-6"
132+ >
133+ < Search className = "absolute left-2 top-2.5 h-4 w-4 text-muted-foreground" />
134+ < Input
135+ type = "search"
136+ placeholder = "Search commands, scripts, etc..."
137+ value = { searchQuery }
138+ onChange = { handleSearch }
139+ className = "pl-8 bg-background border-primary/20 focus:border-primary transition-all duration-300"
140+ />
141+ </ motion . div >
142+ < div className = "grid grid-cols-1 md:grid-cols-2 gap-4" >
143+ < ScrollArea className = "h-[calc(100vh-200px)] pr-4" >
144+ < AnimatePresence >
145+ { leftColumnCategories . map ( ( category , index ) => (
146+ < motion . div
147+ key = { category . category }
148+ initial = { { opacity : 0 , y : 20 } }
149+ animate = { { opacity : 1 , y : 0 } }
150+ exit = { { opacity : 0 , y : - 20 } }
151+ transition = { { duration : 0.3 , delay : index * 0.1 } }
152+ >
153+ < Accordion type = "single" collapsible className = "mb-4" >
154+ < AccordionItem value = { `item-${ index } ` } className = "border border-primary/20 rounded-lg" >
155+ < AccordionTrigger className = "hover:bg-primary/5 px-4 py-2 rounded-t-lg transition-all duration-300" >
156+ { category . category }
157+ </ AccordionTrigger >
158+ < AccordionContent className = "px-4 py-2" >
159+ { category . items . map ( ( item , itemIndex ) => (
160+ < Accordion key = { itemIndex } type = "single" collapsible className = "mb-2" >
161+ < AccordionItem value = { `subitem-${ itemIndex } ` } className = "border border-primary/10 rounded-md" >
162+ < AccordionTrigger className = "hover:bg-primary/5 px-3 py-1 rounded-t-md text-sm transition-all duration-300" >
163+ { item . title }
164+ </ AccordionTrigger >
165+ < AccordionContent className = "px-3 py-2" >
166+ < pre className = "bg-muted p-2 rounded-md overflow-x-auto text-sm" >
167+ < code > { item . content } </ code >
168+ </ pre >
169+ < Button
170+ variant = "outline"
171+ size = "sm"
172+ className = "mt-2 transition-all duration-300 hover:bg-primary hover:text-primary-foreground"
173+ onClick = { ( ) => handleCopy ( item . content , `${ category . category } -${ item . title } ` ) }
174+ >
175+ { copiedStates [ `${ category . category } -${ item . title } ` ] ? (
176+ < >
177+ < Check className = "w-4 h-4 mr-2" />
178+ Copied!
179+ </ >
180+ ) : (
181+ < >
182+ < Copy className = "w-4 h-4 mr-2" />
183+ Copy to Clipboard
184+ </ >
185+ ) }
186+ </ Button >
187+ </ AccordionContent >
188+ </ AccordionItem >
189+ </ Accordion >
190+ ) ) }
191+ </ AccordionContent >
192+ </ AccordionItem >
193+ </ Accordion >
194+ </ motion . div >
195+ ) ) }
196+ </ AnimatePresence >
197+ </ ScrollArea >
198+ < ScrollArea className = "h-[calc(100vh-200px)] pr-4" >
199+ < AnimatePresence >
200+ { rightColumnCategories . map ( ( category , index ) => (
201+ < motion . div
202+ key = { category . category }
203+ initial = { { opacity : 0 , y : 20 } }
204+ animate = { { opacity : 1 , y : 0 } }
205+ exit = { { opacity : 0 , y : - 20 } }
206+ transition = { { duration : 0.3 , delay : index * 0.1 } }
207+ >
208+ < Accordion type = "single" collapsible className = "mb-4" >
209+ < AccordionItem value = { `item-${ index } ` } className = "border border-primary/20 rounded-lg" >
210+ < AccordionTrigger className = "hover:bg-primary/5 px-4 py-2 rounded-t-lg transition-all duration-300" >
211+ { category . category }
212+ </ AccordionTrigger >
213+ < AccordionContent className = "px-4 py-2" >
214+ { category . items . map ( ( item , itemIndex ) => (
215+ < Accordion key = { itemIndex } type = "single" collapsible className = "mb-2" >
216+ < AccordionItem value = { `subitem-${ itemIndex } ` } className = "border border-primary/10 rounded-md" >
217+ < AccordionTrigger className = "hover:bg-primary/5 px-3 py-1 rounded-t-md text-sm transition-all duration-300" >
218+ { item . title }
219+ </ AccordionTrigger >
220+ < AccordionContent className = "px-3 py-2" >
221+ < pre className = "bg-muted p-2 rounded-md overflow-x-auto text-sm" >
222+ < code > { item . content } </ code >
223+ </ pre >
224+ < Button
225+ variant = "outline"
226+ size = "sm"
227+ className = "mt-2 transition-all duration-300 hover:bg-primary hover:text-primary-foreground"
228+ onClick = { ( ) => handleCopy ( item . content , `${ category . category } -${ item . title } ` ) }
229+ >
230+ { copiedStates [ `${ category . category } -${ item . title } ` ] ? (
231+ < >
232+ < Check className = "w-4 h-4 mr-2" />
233+ Copied!
234+ </ >
235+ ) : (
236+ < >
237+ < Copy className = "w-4 h-4 mr-2" />
238+ Copy to Clipboard
239+ </ >
240+ ) }
241+ </ Button >
242+ </ AccordionContent >
243+ </ AccordionItem >
244+ </ Accordion >
245+ ) ) }
246+ </ AccordionContent >
247+ </ AccordionItem >
248+ </ Accordion >
249+ </ motion . div >
250+ ) ) }
251+ </ AnimatePresence >
252+ </ ScrollArea >
253+ </ div >
254+ </ div >
255+ )
256+ }
0 commit comments