1+ document . addEventListener ( 'DOMContentLoaded' , ( ) => {
2+ const searchInput = document . getElementById ( 'search' ) ;
3+ const snippetsContainer = document . getElementById ( 'snippets-container' ) ;
4+ const sidebar = document . getElementById ( 'sidebar' ) ;
5+ const themeSelect = document . getElementById ( 'theme-select' ) ;
6+ let dbData = null ;
7+ let currentSnippets = [ ] ;
8+ let loadedCount = 0 ;
9+ const batchSize = 10 ;
10+
11+ // Theme handling
12+ function setPrismTheme ( isDark ) {
13+ const prismLink = document . getElementById ( 'prism-theme' ) ;
14+ if ( isDark ) {
15+ prismLink . href = 'https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/themes/prism-okaidia.min.css' ;
16+ } else {
17+ prismLink . href = 'https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/themes/prism.min.css' ;
18+ }
19+ }
20+
21+ function applyTheme ( theme ) {
22+ const html = document . documentElement ;
23+ localStorage . setItem ( 'theme' , theme ) ;
24+ let isDark = false ;
25+ if ( theme === 'dark' ) {
26+ html . classList . add ( 'dark' ) ;
27+ isDark = true ;
28+ } else if ( theme === 'light' ) {
29+ html . classList . remove ( 'dark' ) ;
30+ isDark = false ;
31+ } else { // auto
32+ const prefersDark = window . matchMedia ( '(prefers-color-scheme: dark)' ) . matches ;
33+ if ( prefersDark ) {
34+ html . classList . add ( 'dark' ) ;
35+ isDark = true ;
36+ } else {
37+ html . classList . remove ( 'dark' ) ;
38+ isDark = false ;
39+ }
40+ }
41+ themeSelect . value = theme ;
42+ setPrismTheme ( isDark ) ;
43+ }
44+
45+ // Load saved theme or default to auto
46+ const savedTheme = localStorage . getItem ( 'theme' ) || 'auto' ;
47+ applyTheme ( savedTheme ) ;
48+
49+ // Theme selector change
50+ themeSelect . addEventListener ( 'change' , ( e ) => {
51+ applyTheme ( e . target . value ) ;
52+ } ) ;
53+
54+ // Listen for system theme changes when in auto mode
55+ window . matchMedia ( '(prefers-color-scheme: dark)' ) . addEventListener ( 'change' , ( e ) => {
56+ if ( localStorage . getItem ( 'theme' ) === 'auto' ) {
57+ if ( e . matches ) {
58+ document . documentElement . classList . add ( 'dark' ) ;
59+ setPrismTheme ( true ) ;
60+ } else {
61+ document . documentElement . classList . remove ( 'dark' ) ;
62+ setPrismTheme ( false ) ;
63+ }
64+ }
65+ } ) ;
66+
67+ // Sidebar toggle
68+ window . toggleSidebar = function ( ) {
69+ sidebar . classList . toggle ( 'sidebar-hidden' ) ;
70+ } ;
71+
72+ // Clear search function
73+ window . clearSearch = function ( ) {
74+ searchInput . value = '' ;
75+ searchInput . dispatchEvent ( new Event ( 'input' ) ) ;
76+ } ;
77+
78+ // Load db.json
79+ fetch ( 'db.json' )
80+ . then ( response => response . json ( ) )
81+ . then ( data => {
82+ dbData = data ;
83+ const activeSnippets = data . snippets . filter ( s => ! s . isDeleted ) ;
84+ currentSnippets = activeSnippets ;
85+ loadedCount = 0 ;
86+ const initialBatch = currentSnippets . slice ( 0 , batchSize ) ;
87+ renderSnippets ( initialBatch , true ) ;
88+ loadedCount += initialBatch . length ;
89+ renderTree ( data . folders , activeSnippets ) ;
90+ } )
91+ . catch ( error => console . error ( 'Error loading db.json:' , error ) ) ;
92+
93+ // Search functionality
94+ const clearSearchButton = document . getElementById ( 'clear-search' ) ;
95+ searchInput . addEventListener ( 'input' , ( e ) => {
96+ const query = e . target . value . toLowerCase ( ) ;
97+ const activeSnippets = dbData . snippets . filter ( s => ! s . isDeleted ) ;
98+ const filteredSnippets = activeSnippets . filter ( snippet =>
99+ snippet . name . toLowerCase ( ) . includes ( query ) ||
100+ snippet . description ?. toLowerCase ( ) . includes ( query ) ||
101+ snippet . content . some ( content => content . value . toLowerCase ( ) . includes ( query ) )
102+ ) ;
103+ currentSnippets = filteredSnippets ;
104+ loadedCount = 0 ;
105+ const initialBatch = currentSnippets . slice ( 0 , batchSize ) ;
106+ renderSnippets ( initialBatch , true ) ;
107+ loadedCount += initialBatch . length ;
108+
109+ // Show/hide clear button
110+ clearSearchButton . style . display = e . target . value ? 'block' : 'none' ;
111+ } ) ;
112+
113+ // Sidebar toggle
114+ sidebar . addEventListener ( 'click' , ( e ) => {
115+ if ( e . target . classList . contains ( 'folder' ) ) {
116+ e . target . classList . toggle ( 'open' ) ;
117+ } else if ( e . target . classList . contains ( 'snippet-item' ) ) {
118+ const snippetId = e . target . dataset . snippetId ;
119+ const element = document . getElementById ( `snippet-${ snippetId } ` ) ;
120+ if ( element ) {
121+ element . scrollIntoView ( { behavior : 'smooth' } ) ;
122+ }
123+ }
124+ } ) ;
125+
126+ // Infinite scroll
127+ snippetsContainer . addEventListener ( 'scroll' , ( ) => {
128+ if ( snippetsContainer . scrollTop + snippetsContainer . clientHeight >= snippetsContainer . scrollHeight - 100 ) {
129+ if ( loadedCount < currentSnippets . length ) {
130+ const nextBatch = currentSnippets . slice ( loadedCount , loadedCount + batchSize ) ;
131+ renderSnippets ( nextBatch , false ) ;
132+ loadedCount += nextBatch . length ;
133+ }
134+ }
135+ } ) ;
136+
137+ function renderSnippets ( snippets , clear = false ) {
138+ if ( clear ) {
139+ snippetsContainer . innerHTML = '' ;
140+ }
141+ snippets . forEach ( snippet => {
142+ const snippetDiv = document . createElement ( 'div' ) ;
143+ snippetDiv . className = 'bg-white rounded-lg shadow-md mb-4 p-4 dark:bg-gray-700 dark:shadow-lg transition-colors' ;
144+ snippetDiv . id = `snippet-${ snippet . id } ` ;
145+
146+ const title = document . createElement ( 'h2' ) ;
147+ title . className = 'text-xl font-semibold text-gray-800 mb-2 dark:text-white' ;
148+ title . textContent = snippet . name ;
149+ snippetDiv . appendChild ( title ) ;
150+
151+ if ( snippet . tagsIds . length > 0 ) {
152+ const tagsDiv = document . createElement ( 'div' ) ;
153+ tagsDiv . className = 'mb-2' ;
154+ snippet . tagsIds . forEach ( tagId => {
155+ const tag = dbData . tags . find ( t => t . id === tagId ) ;
156+ if ( tag ) {
157+ const tagSpan = document . createElement ( 'span' ) ;
158+ tagSpan . className = 'inline-block bg-gray-200 text-gray-800 px-2 py-1 rounded text-sm mr-2 dark:bg-gray-600 dark:text-gray-200' ;
159+ tagSpan . textContent = tag . name ;
160+ tagsDiv . appendChild ( tagSpan ) ;
161+ }
162+ } ) ;
163+ snippetDiv . appendChild ( tagsDiv ) ;
164+ }
165+
166+ if ( snippet . description ) {
167+ const desc = document . createElement ( 'p' ) ;
168+ desc . className = 'text-gray-600 mb-2 dark:text-gray-300' ;
169+ desc . textContent = snippet . description ;
170+ snippetDiv . appendChild ( desc ) ;
171+ }
172+
173+ snippet . content . forEach ( content => {
174+ if ( content . value . trim ( ) ) {
175+ const pre = document . createElement ( 'pre' ) ;
176+ pre . className = 'bg-gray-100 rounded p-4 overflow-x-auto mb-4 dark:bg-gray-900 relative' ;
177+ const code = document . createElement ( 'code' ) ;
178+ code . className = `language-${ content . language } ` ;
179+ code . textContent = content . value ;
180+ pre . appendChild ( code ) ;
181+
182+ // Add copy button
183+ const copyButton = document . createElement ( 'button' ) ;
184+ copyButton . className = 'copy-to-clipboard-button' ;
185+ copyButton . innerHTML = '📋' ;
186+ copyButton . title = 'Copy to clipboard' ;
187+ copyButton . onclick = async ( ) => {
188+ try {
189+ await navigator . clipboard . writeText ( content . value ) ;
190+ copyButton . innerHTML = '✅' ;
191+ setTimeout ( ( ) => copyButton . innerHTML = '📋' , 2000 ) ;
192+ } catch ( err ) {
193+ console . error ( 'Failed to copy: ' , err ) ;
194+ copyButton . innerHTML = '❌' ;
195+ setTimeout ( ( ) => copyButton . innerHTML = '📋' , 2000 ) ;
196+ }
197+ } ;
198+ pre . appendChild ( copyButton ) ;
199+
200+ snippetDiv . appendChild ( pre ) ;
201+ Prism . highlightElement ( code ) ;
202+ }
203+ } ) ;
204+
205+ snippetsContainer . appendChild ( snippetDiv ) ;
206+ } ) ;
207+ }
208+
209+ function renderTree ( folders , snippets ) {
210+ const folderMap = { } ;
211+ folders . forEach ( folder => {
212+ folderMap [ folder . id ] = { ...folder , children : [ ] , snippets : [ ] } ;
213+ } ) ;
214+
215+ // Group snippets by folder
216+ snippets . forEach ( snippet => {
217+ if ( folderMap [ snippet . folderId ] ) {
218+ folderMap [ snippet . folderId ] . snippets . push ( snippet ) ;
219+ }
220+ } ) ;
221+
222+ // Build tree
223+ const rootFolders = [ ] ;
224+ folders . forEach ( folder => {
225+ if ( ! folder . parentId || ! folderMap [ folder . parentId ] ) {
226+ rootFolders . push ( folderMap [ folder . id ] ) ;
227+ } else {
228+ folderMap [ folder . parentId ] . children . push ( folderMap [ folder . id ] ) ;
229+ }
230+ } ) ;
231+
232+ function buildTree ( folders ) {
233+ const ul = document . createElement ( 'ul' ) ;
234+ ul . className = 'tree' ;
235+ folders . forEach ( folder => {
236+ const li = document . createElement ( 'li' ) ;
237+ const folderDiv = document . createElement ( 'div' ) ;
238+ folderDiv . className = 'folder' ;
239+ folderDiv . textContent = folder . name ;
240+ li . appendChild ( folderDiv ) ;
241+
242+ if ( folder . snippets . length > 0 || folder . children . length > 0 ) {
243+ const subUl = document . createElement ( 'ul' ) ;
244+ folder . children . forEach ( child => {
245+ subUl . appendChild ( buildTree ( [ child ] ) . firstChild ) ;
246+ } ) ;
247+ folder . snippets . forEach ( snippet => {
248+ const snippetLi = document . createElement ( 'li' ) ;
249+ snippetLi . className = 'snippet-item' ;
250+ snippetLi . textContent = snippet . name ;
251+ snippetLi . dataset . snippetId = snippet . id ;
252+ subUl . appendChild ( snippetLi ) ;
253+ } ) ;
254+ li . appendChild ( subUl ) ;
255+ }
256+ ul . appendChild ( li ) ;
257+ } ) ;
258+ return ul ;
259+ }
260+
261+ const tree = buildTree ( rootFolders ) ;
262+ document . getElementById ( 'sidebar-content' ) . appendChild ( tree ) ;
263+ }
264+ } ) ;
0 commit comments