1- import React , { useState , useEffect , useCallback } from 'react' ;
1+ import React , { useState , useEffect , useCallback , useRef } from 'react' ;
22import { getStorage } from 'firebase/storage' ;
33import { useFirebaseApp } from 'reactfire' ;
44import useGraffiticodeAuth from '../hooks/use-graffiticode-auth' ;
@@ -21,6 +21,9 @@ export function ImageGallery() {
2121 const [ dragging , setDragging ] = useState ( false ) ;
2222 const [ uploadProgress , setUploadProgress ] = useState < number | null > ( null ) ;
2323 const [ uploadError , setUploadError ] = useState < string | null > ( null ) ;
24+ const dragImageRef = useRef < string | null > ( null ) ;
25+ const dragOverImageRef = useRef < string | null > ( null ) ;
26+ const [ dragOverUrl , setDragOverUrl ] = useState < string | null > ( null ) ;
2427
2528 const loadImages = useCallback ( async ( ) => {
2629 if ( ! user ?. uid ) {
@@ -31,7 +34,21 @@ export function ImageGallery() {
3134 setLoading ( true ) ;
3235 setError ( null ) ;
3336 const storage = getStorage ( firebaseApp ) ;
34- const result = await listUserImages ( storage , user . uid ) ;
37+ let result = await listUserImages ( storage , user . uid ) ;
38+ // Apply saved order from localStorage
39+ const orderKey = `graffiticode:imageOrder:${ user . uid } ` ;
40+ const savedOrder = localStorage . getItem ( orderKey ) ;
41+ if ( savedOrder ) {
42+ try {
43+ const orderUrls : string [ ] = JSON . parse ( savedOrder ) ;
44+ const orderMap = new Map ( orderUrls . map ( ( url , idx ) => [ url , idx ] ) ) ;
45+ result = [ ...result ] . sort ( ( a , b ) => {
46+ const aIdx = orderMap . has ( a . downloadURL ) ? orderMap . get ( a . downloadURL ) ! : Infinity ;
47+ const bIdx = orderMap . has ( b . downloadURL ) ? orderMap . get ( b . downloadURL ) ! : Infinity ;
48+ return aIdx - bIdx ;
49+ } ) ;
50+ } catch { }
51+ }
3552 setImages ( result ) ;
3653 } catch ( err ) {
3754 setError ( 'Failed to load images' ) ;
@@ -173,21 +190,60 @@ export function ImageGallery() {
173190 const isSelected = selectedUrls . has ( img . downloadURL ) ;
174191 // When dragging, use all selected if this image is selected, otherwise just this image
175192 const dragUrls = isSelected && selectedUrls . size > 0 ? selectedUrls : new Set ( [ img . downloadURL ] ) ;
193+ const isDragOver = dragOverUrl === img . downloadURL && dragImageRef . current !== img . downloadURL ;
176194 return (
177195 < button
178196 key = { img . downloadURL }
179197 onClick = { ( e ) => handleClick ( img , e ) }
180198 draggable
181199 onDragStart = { ( e ) => {
200+ dragImageRef . current = img . downloadURL ;
182201 const markdown = buildMarkdown ( dragUrls ) ;
183202 e . dataTransfer . setData ( 'text/plain' , markdown ) ;
184203 e . dataTransfer . setData ( 'application/x-gc-image' , 'true' ) ;
185- e . dataTransfer . effectAllowed = 'copy' ;
204+ e . dataTransfer . setData ( 'application/x-gc-image-reorder' , img . downloadURL ) ;
205+ e . dataTransfer . effectAllowed = 'copyMove' ;
206+ } }
207+ onDragOver = { ( e ) => {
208+ if ( dragImageRef . current ) {
209+ e . preventDefault ( ) ;
210+ e . dataTransfer . dropEffect = 'move' ;
211+ if ( dragOverImageRef . current !== img . downloadURL ) {
212+ dragOverImageRef . current = img . downloadURL ;
213+ setDragOverUrl ( img . downloadURL ) ;
214+ }
215+ }
216+ } }
217+ onDrop = { ( e ) => {
218+ if ( dragImageRef . current && dragImageRef . current !== img . downloadURL ) {
219+ e . preventDefault ( ) ;
220+ e . stopPropagation ( ) ;
221+ const fromIdx = images . findIndex ( i => i . downloadURL === dragImageRef . current ) ;
222+ const toIdx = images . findIndex ( i => i . downloadURL === img . downloadURL ) ;
223+ if ( fromIdx !== - 1 && toIdx !== - 1 ) {
224+ const reordered = [ ...images ] ;
225+ const [ moved ] = reordered . splice ( fromIdx , 1 ) ;
226+ reordered . splice ( toIdx , 0 , moved ) ;
227+ setImages ( reordered ) ;
228+ const orderKey = `graffiticode:imageOrder:${ user . uid } ` ;
229+ localStorage . setItem ( orderKey , JSON . stringify ( reordered . map ( i => i . downloadURL ) ) ) ;
230+ }
231+ }
232+ dragImageRef . current = null ;
233+ dragOverImageRef . current = null ;
234+ setDragOverUrl ( null ) ;
235+ } }
236+ onDragEnd = { ( ) => {
237+ dragImageRef . current = null ;
238+ dragOverImageRef . current = null ;
239+ setDragOverUrl ( null ) ;
186240 } }
187241 className = { `group relative flex flex-col items-center p-2 rounded cursor-pointer border ${
188- isSelected
189- ? 'border-blue-500 bg-blue-50'
190- : 'border-transparent hover:border-gray-200 hover:bg-gray-50'
242+ isDragOver
243+ ? 'border-blue-400 bg-blue-50'
244+ : isSelected
245+ ? 'border-blue-500 bg-blue-50'
246+ : 'border-transparent hover:border-gray-200 hover:bg-gray-50'
191247 } `}
192248 >
193249 < span
0 commit comments