Persist results to sessionStorage on all pages - prevents data loss on external link clicks

This commit is contained in:
root
2026-03-31 19:13:26 -05:00
parent 88e7bc9c30
commit 2b56d0c06b
8 changed files with 130 additions and 18 deletions

View File

@@ -1,4 +1,4 @@
import { useState } from 'react'
import { useState, useEffect } from 'react'
import { Lightbulb, Loader2, Sparkles } from 'lucide-react'
import { analyzeSong, type AnalyzeResponse } from '../lib/api'
import RecommendationCard from '../components/RecommendationCard'
@@ -9,10 +9,21 @@ export default function Analyze() {
const [title, setTitle] = useState('')
const [loading, setLoading] = useState(false)
const [error, setError] = useState('')
const [result, setResult] = useState<AnalyzeResponse | null>(null)
const [result, setResult] = useState<AnalyzeResponse | null>(() => {
try {
const saved = sessionStorage.getItem('vynl_analyze_results')
return saved ? JSON.parse(saved) : null
} catch { return null }
})
const [savingIds, setSavingIds] = useState<Set<string>>(new Set())
const [dislikingIds, setDislikingIds] = useState<Set<string>>(new Set())
useEffect(() => {
if (result) {
sessionStorage.setItem('vynl_analyze_results', JSON.stringify(result))
}
}, [result])
const handleAnalyze = async () => {
if (!artist.trim() || !title.trim()) return
setLoading(true)

View File

@@ -8,9 +8,20 @@ export default function ArtistDive() {
const navigate = useNavigate()
const [artist, setArtist] = useState(searchParams.get('artist') || '')
const [loading, setLoading] = useState(false)
const [result, setResult] = useState<ArtistDeepDiveResponse | null>(null)
const [result, setResult] = useState<ArtistDeepDiveResponse | null>(() => {
try {
const saved = sessionStorage.getItem('vynl_artist_dive_results')
return saved ? JSON.parse(saved) : null
} catch { return null }
})
const [error, setError] = useState('')
useEffect(() => {
if (result) {
sessionStorage.setItem('vynl_artist_dive_results', JSON.stringify(result))
}
}, [result])
const handleDive = async (artistName?: string) => {
const name = artistName || artist
if (!name.trim()) return

View File

@@ -10,9 +10,19 @@ const SORT_OPTIONS = [
export default function BandcampDiscover() {
const [tags, setTags] = useState<string[]>([])
const [selectedTags, setSelectedTags] = useState<string[]>([])
const [selectedTags, setSelectedTags] = useState<string[]>(() => {
try {
const saved = sessionStorage.getItem('vynl_bandcamp_tags')
return saved ? JSON.parse(saved) : []
} catch { return [] }
})
const [sort, setSort] = useState('new')
const [releases, setReleases] = useState<BandcampRelease[]>([])
const [releases, setReleases] = useState<BandcampRelease[]>(() => {
try {
const saved = sessionStorage.getItem('vynl_bandcamp_results')
return saved ? JSON.parse(saved) : []
} catch { return [] }
})
const [page, setPage] = useState(1)
const [loading, setLoading] = useState(false)
const [loadingMore, setLoadingMore] = useState(false)
@@ -25,6 +35,18 @@ export default function BandcampDiscover() {
.finally(() => setTagsLoading(false))
}, [])
useEffect(() => {
if (selectedTags.length > 0) {
sessionStorage.setItem('vynl_bandcamp_tags', JSON.stringify(selectedTags))
}
}, [selectedTags])
useEffect(() => {
if (releases.length > 0) {
sessionStorage.setItem('vynl_bandcamp_results', JSON.stringify(releases))
}
}, [releases])
const fetchReleases = async (newPage: number, append: boolean = false) => {
if (selectedTags.length === 0) return
append ? setLoadingMore(true) : setLoading(true)

View File

@@ -1,4 +1,4 @@
import { useState } from 'react'
import { useState, useEffect } from 'react'
import { Users, Loader2, Music, Sparkles, Heart } from 'lucide-react'
import { checkCompatibility, type CompatibilityResponse } from '../lib/api'
@@ -69,7 +69,18 @@ export default function Compatibility() {
const [email, setEmail] = useState('')
const [loading, setLoading] = useState(false)
const [error, setError] = useState<string | null>(null)
const [result, setResult] = useState<CompatibilityResponse | null>(null)
const [result, setResult] = useState<CompatibilityResponse | null>(() => {
try {
const saved = sessionStorage.getItem('vynl_compatibility_results')
return saved ? JSON.parse(saved) : null
} catch { return null }
})
useEffect(() => {
if (result) {
sessionStorage.setItem('vynl_compatibility_results', JSON.stringify(result))
}
}, [result])
const handleCompare = async (e: React.FormEvent) => {
e.preventDefault()

View File

@@ -1,19 +1,33 @@
import { useState, useCallback } from 'react'
import { useState, useCallback, useEffect } from 'react'
import { Disc3, X, Heart, ExternalLink, Loader2, RotateCcw } from 'lucide-react'
import { fillCrate, crateSave, type CrateItem } from '../lib/api'
type CardState = 'visible' | 'saving' | 'passing'
export default function CrateDigger() {
const [crate, setCrate] = useState<CrateItem[]>([])
const [currentIndex, setCurrentIndex] = useState(0)
const [crate, setCrate] = useState<CrateItem[]>(() => {
try {
const saved = sessionStorage.getItem('vynl_crate')
return saved ? JSON.parse(saved) : []
} catch { return [] }
})
const [currentIndex, setCurrentIndex] = useState(() => {
try { return Number(sessionStorage.getItem('vynl_crate_index') || '0') } catch { return 0 }
})
const [loading, setLoading] = useState(false)
const [error, setError] = useState('')
const [cardState, setCardState] = useState<CardState>('visible')
const [savedCount, setSavedCount] = useState(0)
const [crateSize, setCrateSize] = useState(0)
const [savedCount, setSavedCount] = useState(() => {
try { return Number(sessionStorage.getItem('vynl_crate_saved') || '0') } catch { return 0 }
})
const [crateSize, setCrateSize] = useState(() => {
try { return Number(sessionStorage.getItem('vynl_crate_size') || '0') } catch { return 0 }
})
const [finished, setFinished] = useState(false)
useEffect(() => { sessionStorage.setItem('vynl_crate_index', String(currentIndex)) }, [currentIndex])
useEffect(() => { sessionStorage.setItem('vynl_crate_saved', String(savedCount)) }, [savedCount])
const loadCrate = useCallback(async () => {
setLoading(true)
setError('')
@@ -25,6 +39,10 @@ export default function CrateDigger() {
const items = await fillCrate(20)
setCrate(items)
setCrateSize(items.length)
sessionStorage.setItem('vynl_crate', JSON.stringify(items))
sessionStorage.setItem('vynl_crate_size', String(items.length))
sessionStorage.setItem('vynl_crate_index', '0')
sessionStorage.setItem('vynl_crate_saved', '0')
} catch {
setError('Failed to fill the crate. Try again.')
} finally {

View File

@@ -27,7 +27,12 @@ export default function Discover() {
const [playlists, setPlaylists] = useState<PlaylistResponse[]>([])
const [selectedPlaylist, setSelectedPlaylist] = useState<string>('')
const [query, setQuery] = useState('')
const [results, setResults] = useState<RecommendationItem[]>([])
const [results, setResults] = useState<RecommendationItem[]>(() => {
try {
const saved = sessionStorage.getItem('vynl_discover_results')
return saved ? JSON.parse(saved) : []
} catch { return [] }
})
const [remaining, setRemaining] = useState<number | null>(null)
const [discovering, setDiscovering] = useState(false)
const [loading, setLoading] = useState(true)
@@ -50,6 +55,13 @@ export default function Discover() {
if (preQuery) setQuery(preQuery)
}, [searchParams])
// Persist results to sessionStorage
useEffect(() => {
if (results.length > 0) {
sessionStorage.setItem('vynl_discover_results', JSON.stringify(results))
}
}, [results])
useEffect(() => {
const load = async () => {
try {

View File

@@ -1,4 +1,4 @@
import { useState } from 'react'
import { useState, useEffect } from 'react'
import { ListMusic, Loader2, Save, Copy, Check, ExternalLink } from 'lucide-react'
import { generatePlaylist, type GeneratedPlaylistResponse } from '../lib/api'
@@ -9,11 +9,22 @@ export default function PlaylistGenerator() {
const [count, setCount] = useState(25)
const [loading, setLoading] = useState(false)
const [error, setError] = useState('')
const [result, setResult] = useState<GeneratedPlaylistResponse | null>(null)
const [result, setResult] = useState<GeneratedPlaylistResponse | null>(() => {
try {
const saved = sessionStorage.getItem('vynl_playlist_gen_results')
return saved ? JSON.parse(saved) : null
} catch { return null }
})
const [saving, setSaving] = useState(false)
const [saved, setSaved] = useState(false)
const [copied, setCopied] = useState(false)
useEffect(() => {
if (result) {
sessionStorage.setItem('vynl_playlist_gen_results', JSON.stringify(result))
}
}, [result])
const handleGenerate = async () => {
if (!theme.trim()) return
setLoading(true)

View File

@@ -1,4 +1,4 @@
import { useState } from 'react'
import { useState, useEffect } from 'react'
import { ArrowDownCircle, ExternalLink, Link2, Play, Music } from 'lucide-react'
import type { RabbitHoleStep } from '../lib/api'
import { generateRabbitHole } from '../lib/api'
@@ -10,10 +10,26 @@ export default function RabbitHole() {
const [seedTitle, setSeedTitle] = useState('')
const [stepCount, setStepCount] = useState(8)
const [loading, setLoading] = useState(false)
const [theme, setTheme] = useState('')
const [steps, setSteps] = useState<RabbitHoleStep[]>([])
const [theme, setTheme] = useState(() => {
try {
const saved = sessionStorage.getItem('vynl_rabbithole_results')
return saved ? JSON.parse(saved).theme : ''
} catch { return '' }
})
const [steps, setSteps] = useState<RabbitHoleStep[]>(() => {
try {
const saved = sessionStorage.getItem('vynl_rabbithole_results')
return saved ? JSON.parse(saved).steps : []
} catch { return [] }
})
const [error, setError] = useState('')
useEffect(() => {
if (theme && steps.length > 0) {
sessionStorage.setItem('vynl_rabbithole_results', JSON.stringify({ theme, steps }))
}
}, [theme, steps])
const handleGenerate = async () => {
setLoading(true)
setError('')