8484
8585 .empty-state { text-align : center; color : var (--muted ); margin-top : 3rem ; }
8686
87+ .search-bar {
88+ display : flex;
89+ justify-content : center;
90+ margin : 1.2rem 0 1rem ;
91+ }
92+
93+ .search-bar input {
94+ width : min (24rem , 72vw );
95+ }
96+
97+ .filters {
98+ display : flex;
99+ justify-content : center;
100+ flex-wrap : wrap;
101+ gap : 0.4rem ;
102+ margin-bottom : 1.6rem ;
103+ }
104+
105+ .filters button {
106+ font-family : inherit;
107+ font-size : 0.82rem ;
108+ padding : 0.22rem 0.75rem ;
109+ border-radius : 999px ;
110+ border : 1px solid rgba (47 , 130 , 205 , 0.22 );
111+ background : transparent;
112+ color : var (--accent );
113+ cursor : pointer;
114+ transition : all 0.15s ease;
115+ }
116+
117+ .filters button : hover {
118+ background : rgba (47 , 130 , 205 , 0.08 );
119+ border-color : rgba (47 , 130 , 205 , 0.35 );
120+ }
121+
122+ .filters button .active {
123+ background : rgba (47 , 130 , 205 , 0.14 );
124+ border-color : rgba (47 , 130 , 205 , 0.4 );
125+ color : var (--accent );
126+ font-weight : 550 ;
127+ }
128+
129+ .no-results {
130+ width : 100% ;
131+ text-align : center;
132+ color : var (--muted );
133+ margin-top : 2rem ;
134+ font-size : 0.88rem ;
135+ display : none;
136+ }
137+
87138 @media (prefers-color-scheme : dark) {
88139 .thumbnail { box-shadow : 0 4px 18px -6px rgba (0 , 0 , 0 , 0.4 ); border-color : rgba (100 , 160 , 200 , 0.15 ); }
89140 .video-card : hover .thumbnail { box-shadow : 0 12px 34px -14px rgba (0 , 0 , 0 , 0.55 ); }
99150{% block content %}
100151< h1 class ="hero "> {{ name }}'s videos</ h1 >
101152{% if channel.videos %}
153+ < div class ="search-bar ">
154+ < input type ="text " id ="video-search " placeholder ="Filter videos… " autocomplete ="off ">
155+ </ div >
156+ < div class ="filters ">
157+ < button class ="active " data-filter ="all "> All</ button >
158+ < button data-filter ="newest "> Newest</ button >
159+ < button data-filter ="oldest "> Oldest</ button >
160+ < button data-filter ="popular "> Popular</ button >
161+ < button data-filter ="undownloaded "> Not downloaded</ button >
162+ </ div >
102163< div id ="content ">
103164 {% for video in channel.videos %}
104165 {% set downloaded = video.downloaded() %}
105- < a href ="{{ url_for('routes.video', name=name, kind='videos', id=video.id) }} " class ="video-card ">
166+ < a href ="{{ url_for('routes.video', name=name, kind='videos', id=video.id) }} " class ="video-card "
167+ data-views ="{{ video.views.current() }} " data-uploaded ="{{ video.uploaded.timestamp() }} ">
106168 < div class ="thumbnail ">
107169 < img src ="{{ url_for('routes.archive_thumbnail', name=name, id=video.thumbnail.current().id) }} "
108170 alt ="Thumbnail for {{ video.title.current() }} " loading ="lazy " />
@@ -120,13 +182,69 @@ <h1 class="hero">{{ name }}'s videos</h1>
120182 </ a >
121183 {% endfor %}
122184</ div >
185+ < p class ="no-results " id ="no-results "> No videos match your search.</ p >
123186{% else %}
124187< p class ="empty-state "> No videos found in this archive yet.</ p >
125188{% endif %}
126189{% endblock %}
127190
128191{% block scripts %}
129192< script >
193+ const searchInput = document . getElementById ( "video-search" ) ;
194+ const filterBtns = document . querySelectorAll ( ".filters button" ) ;
195+ const grid = document . getElementById ( "content" ) ;
196+ const noResults = document . getElementById ( "no-results" ) ;
197+ let activeFilter = "all" ;
198+
199+ function getCards ( ) { return Array . from ( grid . querySelectorAll ( ".video-card" ) ) ; }
200+
201+ function applyFilters ( ) {
202+ const query = ( searchInput ? searchInput . value . toLowerCase ( ) . trim ( ) : "" ) ;
203+ const cards = getCards ( ) ;
204+
205+ // -- apply sort / filter --
206+ if ( activeFilter === "popular" ) {
207+ cards . sort ( function ( a , b ) {
208+ return ( parseInt ( b . getAttribute ( "data-views" ) ) || 0 ) - ( parseInt ( a . getAttribute ( "data-views" ) ) || 0 ) ;
209+ } ) ;
210+ } else if ( activeFilter === "newest" ) {
211+ cards . sort ( function ( a , b ) {
212+ return ( parseInt ( b . getAttribute ( "data-uploaded" ) ) || 0 ) - ( parseInt ( a . getAttribute ( "data-uploaded" ) ) || 0 ) ;
213+ } ) ;
214+ } else if ( activeFilter === "oldest" ) {
215+ cards . sort ( function ( a , b ) {
216+ return ( parseInt ( a . getAttribute ( "data-uploaded" ) ) || 0 ) - ( parseInt ( b . getAttribute ( "data-uploaded" ) ) || 0 ) ;
217+ } ) ;
218+ }
219+
220+ // -- reorder in DOM --
221+ cards . forEach ( function ( c ) { grid . appendChild ( c ) ; } ) ;
222+
223+ // -- show / hide --
224+ let visible = 0 ;
225+ cards . forEach ( function ( card ) {
226+ var title = ( card . querySelector ( ".title" ) . textContent || "" ) . toLowerCase ( ) ;
227+ var matchSearch = ! query || title . indexOf ( query ) !== - 1 ;
228+ var matchDownload = activeFilter !== "undownloaded" || card . querySelector ( ".video-flag" ) ;
229+ var show = matchSearch && matchDownload ;
230+ card . style . display = show ? "" : "none" ;
231+ if ( show ) visible ++ ;
232+ } ) ;
233+
234+ if ( noResults ) noResults . style . display = visible === 0 ? "" : "none" ;
235+ }
236+
237+ if ( searchInput ) searchInput . addEventListener ( "input" , applyFilters ) ;
238+
239+ filterBtns . forEach ( function ( btn ) {
240+ btn . addEventListener ( "click" , function ( ) {
241+ filterBtns . forEach ( function ( b ) { b . classList . remove ( "active" ) ; } ) ;
242+ btn . classList . add ( "active" ) ;
243+ activeFilter = btn . getAttribute ( "data-filter" ) ;
244+ applyFilters ( ) ;
245+ } ) ;
246+ } ) ;
247+
130248 function setCookie ( name , value , days ) {
131249 var expires = "" ;
132250 if ( days ) {
0 commit comments