@@ -39,21 +39,156 @@ <h1 id="title">Loading...</h1>
3939 </ div >
4040
4141 < script >
42+ fetch ( 'https:/<!DOCTYPE html>
43+ < html lang = "en" >
44+ < head >
45+ < meta charset = "UTF-8" />
46+ < meta name = "viewport" content = "width=device-width, initial-scale=1.0" />
47+ < title > Smart Collection Browser</ title >
48+ < style >
49+ body {
50+ margin : 0 ;
51+ min - height : 100 vh ;
52+ display : flex ;
53+ justify - content : center ;
54+ align - items : center ;
55+ font - family : Arial , Helvetica , sans - serif ;
56+ background - color : #f5f5f5;
57+ }
58+
59+ .layout {
60+ display : flex ;
61+ gap : 24 px ;
62+ align - items : center ;
63+ }
64+
65+ .main {
66+ text - align : center ;
67+ transition : opacity 0.4 s ease , transform 0.4 s ease ;
68+ }
69+
70+ .main.fade {
71+ opacity : 0 ;
72+ transform : scale ( 0.95 ) ;
73+ }
74+
75+ h1 {
76+ margin - bottom : 20 px ;
77+ }
78+
79+ .main img {
80+ max - width : 60 vw ;
81+ max - height : 70 vh ;
82+ object - fit : contain ;
83+ border - radius : 8 px ;
84+ box - shadow : 0 4 px 12 px rgba ( 0 , 0 , 0 , 0.15 ) ;
85+ }
86+
87+ .similar {
88+ display : flex ;
89+ flex - direction : column ;
90+ gap : 12 px ;
91+ }
92+
93+ .similar img {
94+ width : 120 px ;
95+ height : 120 px ;
96+ object - fit : cover ;
97+ cursor : pointer ;
98+ border - radius : 6 px ;
99+ box - shadow : 0 2 px 6 px rgba ( 0 , 0 , 0 , 0.2 ) ;
100+ transition : transform 0.2 s ease ;
101+ }
102+
103+ .similar img:hover {
104+ transform : scale ( 1.05 ) ;
105+ }
106+ </ style >
107+ </ head >
108+ < body >
109+ < div class = "layout" >
110+ < div class = "main" id = "main" >
111+ < h1 id = "title" > Loading...</ h1 >
112+ < img id = "image" />
113+ </ div >
114+ < div class = "similar" id = "similar" > </ div >
115+ </ div >
116+
117+ < script >
118+ let collection = [];
119+ let currentIndex = 0;
120+
121+ // --- Simple ML: TF-IDF cosine similarity on titles ---
122+ function tokenize(text) {
123+ return text . toLowerCase ( ) . split ( / \W + / ) . filter ( Boolean ) ;
124+ }
125+
126+ function buildTf(tokens) {
127+ const tf = { } ;
128+ tokens . forEach ( t => tf [ t ] = ( tf [ t ] || 0 ) + 1 ) ;
129+ return tf ;
130+ }
131+
132+ function cosine(a, b) {
133+ let dot = 0 , magA = 0 , magB = 0 ;
134+ for ( const k in a ) {
135+ if ( b [ k ] ) dot += a [ k ] * b [ k ] ;
136+ magA + = a [ k ] * a [ k ] ;
137+ }
138+ for ( const k in b ) magB += b [ k ] * b [ k ] ;
139+ return dot / ( Math . sqrt ( magA ) * Math . sqrt ( magB ) || 1 ) ;
140+ }
141+
142+ function findSimilar(index, count = 4) {
143+ const baseTf = collection [ index ] . tf ;
144+ return collection
145+ . map ( ( item , i ) => ( {
146+ index : i ,
147+ score : i === index ? - 1 : cosine ( baseTf , item . tf )
148+ } ) )
149+ . sort ( ( a , b ) => b . score - a . score )
150+ . slice ( 0 , count ) ;
151+ }
152+
153+ function render(index) {
154+ const main = document . getElementById ( 'main' ) ;
155+ main . classList . add ( 'fade' ) ;
156+
157+ setTimeout ( ( ) => {
158+ currentIndex = index ;
159+ const item = collection [ index ] ;
160+ document . getElementById ( 'title' ) . textContent = item . Title ;
161+ document . getElementById ( 'image' ) . src = item [ 'Image URL' ] ;
162+
163+ const similarDiv = document . getElementById ( 'similar' ) ;
164+ similarDiv . innerHTML = '' ;
165+
166+ findSimilar ( index ) . forEach ( sim => {
167+ const img = document . createElement ( 'img' ) ;
168+ img . src = collection [ sim . index ] [ 'Image URL' ] ;
169+ img . title = collection [ sim . index ] . Title ;
170+ img . onclick = ( ) => render ( sim . index ) ;
171+ similarDiv . appendChild ( img ) ;
172+ } ) ;
173+
174+ main . classList . remove ( 'fade' ) ;
175+ } , 300 ) ;
176+ }
177+
42178 fetch('https://johnstack.github.io/JavaScript-Sandpit/ng_sandbox/collection.json')
43- . then ( response => response . json ( ) )
179+ .then(r => r . json ( ) )
44180 . then ( data => {
45- if ( ! Array . isArray ( data ) || data . length === 0 ) {
46- throw new Error ( 'collection.json is empty or not an array' ) ;
47- }
48-
49- const randomEntry = data [ Math . floor ( Math . random ( ) * data . length ) ] ;
181+ collection = data . map ( item => ( {
182+ ...item ,
183+ tf : buildTf ( tokenize ( item . Title || '' ) )
184+ } ) ) ;
50185
51- document . getElementById ( 'title' ) . textContent = randomEntry [ 'Title' ] || 'Untitled' ;
52- document . getElementById ( 'image' ) . src = randomEntry [ 'Image URL' ] || '' ;
186+ const start = Math . floor ( Math . random ( ) * collection . length ) ;
187+ render ( start ) ;
53188 } )
54- . catch ( error => {
55- document . getElementById ( 'title' ) . textContent = 'Error loading collection' ;
56- console . error ( error ) ;
189+ .catch(err => {
190+ document . getElementById ( 'title' ) . textContent = 'Failed to load collection' ;
191+ console . error ( err ) ;
57192 } );
58193 </ script >
59194</ body >
0 commit comments