1+ const rawBase =
2+ "https://raw.githubusercontent.com/ForjSkript/ForgeExtensions/refs/heads/main"
3+
4+ const listURL = `${ rawBase } /extensions/list.json`
5+
6+ let listData = [ ]
7+ let filteredData = [ ]
8+
9+
10+ async function loadExtensions ( ) {
11+ const list = await cachedFetch ( listURL )
12+ listData = sortExtensions ( list )
13+ filteredData = listData
14+
15+ setupSearch ( )
16+ renderExtensions ( )
17+ }
18+
19+
20+ /**
21+ * Cache fetch (5 min TTL)
22+ */
23+ async function cachedFetch ( url ) {
24+ const key = "cache:" + url
25+ const cached = localStorage . getItem ( key )
26+
27+ if ( cached ) {
28+ const { time, data } = JSON . parse ( cached )
29+ if ( Date . now ( ) - time < 5 * 60 * 1000 ) return data
30+ }
31+
32+ const res = await fetch ( url )
33+ const data = await res . json ( )
34+
35+ localStorage . setItem (
36+ key ,
37+ JSON . stringify ( { time : Date . now ( ) , data } )
38+ )
39+
40+ return data
41+ }
42+
43+
44+ /**
45+ * Sort extensions
46+ */
47+ function sortExtensions ( list ) {
48+ return [ ...list ] . sort ( ( a , b ) => {
49+ if ( a . id === "@tryforge/forgescript" ) return - 1
50+ if ( b . id === "@tryforge/forgescript" ) return 1
51+ return a . name . localeCompare ( b . name )
52+ } )
53+ }
54+
55+
56+ /**
57+ * Setup search input
58+ */
59+ function setupSearch ( ) {
60+ const input = document . getElementById ( "search" )
61+
62+ input . addEventListener ( "input" , ( ) => {
63+ const q = input . value . toLowerCase ( )
64+
65+ filteredData = listData . filter ( ext =>
66+ ext . id . toLowerCase ( ) . includes ( q ) ||
67+ ext . name . toLowerCase ( ) . includes ( q )
68+ )
69+
70+ renderExtensions ( )
71+ } )
72+ }
73+
74+
75+ /**
76+ * Detect extension type from file path
77+ */
78+ function getType ( file ) {
79+ if ( file . includes ( "extensions/official/" ) ) return "official"
80+ if ( file . includes ( "extensions/community/" ) ) return "community"
81+ return "unlisted"
82+ }
83+
84+
85+ /**
86+ * Get color styles based on type
87+ */
88+ function getTypeStyles ( type ) {
89+ switch ( type ) {
90+ case "official" :
91+ return "border-blue-500/30 bg-blue-500/5"
92+ case "community" :
93+ return "border-green-500/30 bg-green-500/5"
94+ default :
95+ return "border-yellow-500/30 bg-yellow-500/5"
96+ }
97+ }
98+
99+
100+ /**
101+ * Render extensions
102+ */
103+ function renderExtensions ( ) {
104+ const container = document . getElementById ( "extensions" )
105+ container . innerHTML = ""
106+
107+ filteredData . forEach ( ( ext , i ) => {
108+ const type = getType ( ext . file )
109+ const styles = getTypeStyles ( type )
110+
111+ const el = document . createElement ( "div" )
112+
113+ el . className = `
114+ opacity-0 translate-y-4
115+ rounded-xl border p-4 transition-all duration-500
116+ ${ styles }
117+ `
118+
119+ el . innerHTML = `
120+ <div class="flex items-center justify-between mb-2">
121+ <h2 class="text-lg font-semibold">${ ext . name } </h2>
122+ <span class="text-xs text-slate-500">${ ext . id } </span>
123+ </div>
124+
125+ <p class="text-sm text-slate-400 mb-3">
126+ ${ ext . description || "No description provided." }
127+ </p>
128+
129+ <div class="flex items-center justify-between">
130+ <span class="text-xs capitalize text-slate-400">${ type } </span>
131+
132+ <a
133+ href="${ rawBase } /${ ext . file } "
134+ target="_blank"
135+ class="text-sm text-blue-400 hover:underline"
136+ >
137+ View
138+ </a>
139+ </div>
140+ `
141+
142+ container . appendChild ( el )
143+
144+ // ✨ fade-up animation (staggered)
145+ setTimeout ( ( ) => {
146+ el . classList . remove ( "opacity-0" , "translate-y-4" )
147+ } , i * 60 )
148+ } )
149+ }
150+
151+
152+ loadExtensions ( )
0 commit comments