@@ -37,6 +37,12 @@ const categoryRank = (cat?: string | null) => {
3737 return k < 0 ? EVENT_CATEGORIES . length : k ;
3838} ;
3939
40+ /** Site order: by category, then by the position staff set, newest first among equal positions. */
41+ const byDisplayOrder = ( a : EventItem , b : EventItem ) =>
42+ categoryRank ( a . category ) - categoryRank ( b . category ) ||
43+ ( a . sort_order ?? 0 ) - ( b . sort_order ?? 0 ) ||
44+ ( b . created_at || '' ) . localeCompare ( a . created_at || '' ) ;
45+
4046export type EventCategoryKey = typeof EVENT_CATEGORIES [ number ] [ 'key' ] ;
4147
4248export default function EventsManager ( {
@@ -49,6 +55,7 @@ export default function EventsManager({
4955 const [ loading , setLoading ] = useState ( false ) ;
5056 const [ searchQuery , setSearchQuery ] = useState ( '' ) ;
5157 const [ categoryFilter , setCategoryFilter ] = useState ( 'all' ) ;
58+ const [ reordering , setReordering ] = useState ( false ) ;
5259
5360 // Modals
5461 const [ isUploadModalOpen , setIsUploadModalOpen ] = useState ( false ) ;
@@ -357,6 +364,49 @@ export default function EventsManager({
357364 } ) ;
358365 } ;
359366
367+ // Each category's events in site order, unfiltered: positions and arrow moves ignore the search box.
368+ const orderedEvents = [ ...events ] . sort ( byDisplayOrder ) ;
369+ const categoryOrder = ( cat ?: string | null ) =>
370+ orderedEvents . filter ( ( ev ) => ( ev . category || '' ) . toLowerCase ( ) === ( cat || '' ) . toLowerCase ( ) ) ;
371+
372+ // Move an event one place up (-1) or down (1) within its category, then save the category's whole order.
373+ const moveEvent = async ( event : EventItem , dir : - 1 | 1 ) => {
374+ if ( reordering || ! event . id ) return ;
375+ const list = categoryOrder ( event . category ) ;
376+ const from = list . findIndex ( ( ev ) => ev . id === event . id ) ;
377+ const to = from + dir ;
378+ if ( from < 0 || to < 0 || to >= list . length ) return ;
379+
380+ const reordered = [ ...list ] ;
381+ [ reordered [ from ] , reordered [ to ] ] = [ reordered [ to ] , reordered [ from ] ] ;
382+ const ids = reordered . map ( ( ev ) => ev . id as string ) ;
383+ const position = new Map ( ids . map ( ( id , i ) => [ id , i + 1 ] ) ) ;
384+
385+ const previous = events ;
386+ setEvents ( ( prev ) => prev . map ( ( ev ) => ( ev . id && position . has ( ev . id ) ? { ...ev , sort_order : position . get ( ev . id ) } : ev ) ) ) ;
387+ setReordering ( true ) ;
388+ try {
389+ const token = await getAuthToken ( ) ;
390+ const headers : Record < string , string > = { 'Content-Type' : 'application/json' } ;
391+ if ( token ) headers [ 'Authorization' ] = `Bearer ${ token } ` ;
392+
393+ const res = await fetch ( '/api/admin/events/order' , {
394+ method : 'POST' ,
395+ headers,
396+ credentials : 'include' ,
397+ body : JSON . stringify ( { category : event . category , ids } ) ,
398+ } ) ;
399+ const data = await res . json ( ) . catch ( ( ) => ( { } ) ) ;
400+ if ( ! res . ok ) throw new Error ( httpErrorMessage ( res ) || data . error || 'Failed to save event order' ) ;
401+ } catch ( err : unknown ) {
402+ setEvents ( previous ) ;
403+ const msg = err instanceof Error ? err . message : 'Failed to save event order' ;
404+ showToast ( msg , 'error' ) ;
405+ } finally {
406+ setReordering ( false ) ;
407+ }
408+ } ;
409+
360410 // Filtered Events
361411 const filteredEvents = events . filter ( ( ev ) => {
362412 const matchesSearch =
@@ -369,7 +419,7 @@ export default function EventsManager({
369419 ev . category ?. toLowerCase ( ) === categoryFilter . toLowerCase ( ) ;
370420
371421 return matchesSearch && matchesCat ;
372- } ) . sort ( ( a , b ) => categoryRank ( a . category ) - categoryRank ( b . category ) ) ;
422+ } ) . sort ( byDisplayOrder ) ;
373423
374424 // Category counts for top stat cards
375425 const countFlagship = events . filter ( ( e ) => ( e . category || '' ) . toLowerCase ( ) === 'flagship events' ) . length ;
@@ -645,7 +695,12 @@ export default function EventsManager({
645695 gap : '22px' ,
646696 } }
647697 >
648- { filteredEvents . map ( ( event ) => (
698+ { filteredEvents . map ( ( event ) => {
699+ const siblings = categoryOrder ( event . category ) ;
700+ const position = siblings . findIndex ( ( ev ) => ev . id === event . id ) ;
701+ const canMoveUp = ! reordering && position > 0 ;
702+ const canMoveDown = ! reordering && position >= 0 && position < siblings . length - 1 ;
703+ return (
649704 < article
650705 key = { event . id }
651706 style = { {
@@ -755,6 +810,21 @@ export default function EventsManager({
755810 >
756811 { event . title }
757812 </ h3 >
813+ { position >= 0 && (
814+ < span
815+ title = "Position in its category on the site"
816+ style = { {
817+ flex : 'none' ,
818+ marginLeft : '10px' ,
819+ fontSize : '12px' ,
820+ fontWeight : 800 ,
821+ letterSpacing : '.08em' ,
822+ color : 'oklch(0.8 0.12 85)' ,
823+ } }
824+ >
825+ #{ position + 1 }
826+ </ span >
827+ ) }
758828 </ div >
759829
760830 { /* Description */ }
@@ -821,6 +891,33 @@ export default function EventsManager({
821891 borderTop : '1px solid rgba(236,232,223,0.1)' ,
822892 } }
823893 >
894+ { ( [
895+ { dir : - 1 , enabled : canMoveUp , label : 'Move up' , points : '18 15 12 9 6 15' } ,
896+ { dir : 1 , enabled : canMoveDown , label : 'Move down' , points : '6 9 12 15 18 9' } ,
897+ ] as const ) . map ( ( arrow ) => (
898+ < button
899+ key = { arrow . dir }
900+ type = "button"
901+ onClick = { ( ) => moveEvent ( event , arrow . dir ) }
902+ disabled = { ! arrow . enabled }
903+ aria-label = { arrow . label }
904+ title = { arrow . label }
905+ style = { {
906+ padding : '9px 12px' ,
907+ background : 'rgba(236,232,223,0.06)' ,
908+ border : '1px solid rgba(236,232,223,0.22)' ,
909+ clipPath : 'polygon(6px 0, 100% 0, 100% calc(100% - 6px), calc(100% - 6px) 100%, 0 100%, 0 6px)' ,
910+ color : '#ECE8DF' ,
911+ cursor : arrow . enabled ? 'pointer' : 'not-allowed' ,
912+ opacity : arrow . enabled ? 1 : 0.35 ,
913+ } }
914+ >
915+ < svg width = "14" height = "14" viewBox = "0 0 24 24" fill = "none" stroke = "currentColor" strokeWidth = "2.5" >
916+ < polyline points = { arrow . points } />
917+ </ svg >
918+ </ button >
919+ ) ) }
920+
824921 < button
825922 type = "button"
826923 onClick = { ( ) => openEditModal ( event ) }
@@ -871,7 +968,8 @@ export default function EventsManager({
871968 </ div >
872969 </ div >
873970 </ article >
874- ) ) }
971+ ) ;
972+ } ) }
875973 </ div >
876974 ) }
877975
0 commit comments