feat: added redux
This commit is contained in:
115
furumi-node-player/client/package-lock.json
generated
115
furumi-node-player/client/package-lock.json
generated
@@ -8,9 +8,11 @@
|
|||||||
"name": "client",
|
"name": "client",
|
||||||
"version": "0.0.0",
|
"version": "0.0.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@reduxjs/toolkit": "^2.11.2",
|
||||||
"axios": "^1.7.9",
|
"axios": "^1.7.9",
|
||||||
"react": "^19.2.4",
|
"react": "^19.2.4",
|
||||||
"react-dom": "^19.2.4"
|
"react-dom": "^19.2.4",
|
||||||
|
"react-redux": "^9.2.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@eslint/js": "^9.39.4",
|
"@eslint/js": "^9.39.4",
|
||||||
@@ -587,6 +589,32 @@
|
|||||||
"url": "https://github.com/sponsors/Boshen"
|
"url": "https://github.com/sponsors/Boshen"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@reduxjs/toolkit": {
|
||||||
|
"version": "2.11.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@reduxjs/toolkit/-/toolkit-2.11.2.tgz",
|
||||||
|
"integrity": "sha512-Kd6kAHTA6/nUpp8mySPqj3en3dm0tdMIgbttnQ1xFMVpufoj+ADi8pXLBsd4xzTRHQa7t/Jv8W5UnCuW4kuWMQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@standard-schema/spec": "^1.0.0",
|
||||||
|
"@standard-schema/utils": "^0.3.0",
|
||||||
|
"immer": "^11.0.0",
|
||||||
|
"redux": "^5.0.1",
|
||||||
|
"redux-thunk": "^3.1.0",
|
||||||
|
"reselect": "^5.1.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": "^16.9.0 || ^17.0.0 || ^18 || ^19",
|
||||||
|
"react-redux": "^7.2.1 || ^8.1.3 || ^9.0.0"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"react": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"react-redux": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@rolldown/binding-android-arm64": {
|
"node_modules/@rolldown/binding-android-arm64": {
|
||||||
"version": "1.0.0-rc.10",
|
"version": "1.0.0-rc.10",
|
||||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.0-rc.10.tgz",
|
"resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.0-rc.10.tgz",
|
||||||
@@ -849,6 +877,18 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/@standard-schema/spec": {
|
||||||
|
"version": "1.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz",
|
||||||
|
"integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/@standard-schema/utils": {
|
||||||
|
"version": "0.3.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@standard-schema/utils/-/utils-0.3.0.tgz",
|
||||||
|
"integrity": "sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/@tybys/wasm-util": {
|
"node_modules/@tybys/wasm-util": {
|
||||||
"version": "0.10.1",
|
"version": "0.10.1",
|
||||||
"resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.1.tgz",
|
"resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.1.tgz",
|
||||||
@@ -888,7 +928,7 @@
|
|||||||
"version": "19.2.14",
|
"version": "19.2.14",
|
||||||
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.14.tgz",
|
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.14.tgz",
|
||||||
"integrity": "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==",
|
"integrity": "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==",
|
||||||
"dev": true,
|
"devOptional": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"csstype": "^3.2.2"
|
"csstype": "^3.2.2"
|
||||||
@@ -904,6 +944,12 @@
|
|||||||
"@types/react": "^19.2.0"
|
"@types/react": "^19.2.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@types/use-sync-external-store": {
|
||||||
|
"version": "0.0.6",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/use-sync-external-store/-/use-sync-external-store-0.0.6.tgz",
|
||||||
|
"integrity": "sha512-zFDAD+tlpf2r4asuHEj0XH6pY6i0g5NeAHPn+15wk3BV6JA69eERFXC1gyGThDkVa1zCyKr5jox1+2LbV/AMLg==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/@typescript-eslint/eslint-plugin": {
|
"node_modules/@typescript-eslint/eslint-plugin": {
|
||||||
"version": "8.57.1",
|
"version": "8.57.1",
|
||||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.57.1.tgz",
|
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.57.1.tgz",
|
||||||
@@ -1496,7 +1542,7 @@
|
|||||||
"version": "3.2.3",
|
"version": "3.2.3",
|
||||||
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz",
|
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz",
|
||||||
"integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==",
|
"integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==",
|
||||||
"dev": true,
|
"devOptional": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/debug": {
|
"node_modules/debug": {
|
||||||
@@ -2127,6 +2173,16 @@
|
|||||||
"node": ">= 4"
|
"node": ">= 4"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/immer": {
|
||||||
|
"version": "11.1.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/immer/-/immer-11.1.4.tgz",
|
||||||
|
"integrity": "sha512-XREFCPo6ksxVzP4E0ekD5aMdf8WMwmdNaz6vuvxgI40UaEiu6q3p8X52aU6GdyvLY3XXX/8R7JOTXStz/nBbRw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"funding": {
|
||||||
|
"type": "opencollective",
|
||||||
|
"url": "https://opencollective.com/immer"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/import-fresh": {
|
"node_modules/import-fresh": {
|
||||||
"version": "3.3.1",
|
"version": "3.3.1",
|
||||||
"resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz",
|
"resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz",
|
||||||
@@ -2831,6 +2887,50 @@
|
|||||||
"react": "^19.2.4"
|
"react": "^19.2.4"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/react-redux": {
|
||||||
|
"version": "9.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/react-redux/-/react-redux-9.2.0.tgz",
|
||||||
|
"integrity": "sha512-ROY9fvHhwOD9ySfrF0wmvu//bKCQ6AeZZq1nJNtbDC+kk5DuSuNX/n6YWYF/SYy7bSba4D4FSz8DJeKY/S/r+g==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@types/use-sync-external-store": "^0.0.6",
|
||||||
|
"use-sync-external-store": "^1.4.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@types/react": "^18.2.25 || ^19",
|
||||||
|
"react": "^18.0 || ^19",
|
||||||
|
"redux": "^5.0.0"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@types/react": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"redux": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/redux": {
|
||||||
|
"version": "5.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/redux/-/redux-5.0.1.tgz",
|
||||||
|
"integrity": "sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/redux-thunk": {
|
||||||
|
"version": "3.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/redux-thunk/-/redux-thunk-3.1.0.tgz",
|
||||||
|
"integrity": "sha512-NW2r5T6ksUKXCabzhL9z+h206HQw/NJkcLm1GPImRQ8IzfXwRGqjVhKJGauHirT0DAuyy6hjdnMZaRoAcy0Klw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"peerDependencies": {
|
||||||
|
"redux": "^5.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/reselect": {
|
||||||
|
"version": "5.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/reselect/-/reselect-5.1.1.tgz",
|
||||||
|
"integrity": "sha512-K/BG6eIky/SBpzfHZv/dd+9JBFiS4SWV7FIujVyJRux6e45+73RaUHXLmIR1f7WOMaQ0U1km6qwklRQxpJJY0w==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/resolve-from": {
|
"node_modules/resolve-from": {
|
||||||
"version": "4.0.0",
|
"version": "4.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz",
|
||||||
@@ -3094,6 +3194,15 @@
|
|||||||
"punycode": "^2.1.0"
|
"punycode": "^2.1.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/use-sync-external-store": {
|
||||||
|
"version": "1.6.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.6.0.tgz",
|
||||||
|
"integrity": "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==",
|
||||||
|
"license": "MIT",
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/vite": {
|
"node_modules/vite": {
|
||||||
"version": "8.0.1",
|
"version": "8.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/vite/-/vite-8.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/vite/-/vite-8.0.1.tgz",
|
||||||
|
|||||||
@@ -10,9 +10,11 @@
|
|||||||
"preview": "vite preview"
|
"preview": "vite preview"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@reduxjs/toolkit": "^2.11.2",
|
||||||
"axios": "^1.7.9",
|
"axios": "^1.7.9",
|
||||||
"react": "^19.2.4",
|
"react": "^19.2.4",
|
||||||
"react-dom": "^19.2.4"
|
"react-dom": "^19.2.4",
|
||||||
|
"react-redux": "^9.2.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@eslint/js": "^9.39.4",
|
"@eslint/js": "^9.39.4",
|
||||||
|
|||||||
@@ -1,10 +1,14 @@
|
|||||||
import { StrictMode } from 'react'
|
import { StrictMode } from 'react'
|
||||||
import { createRoot } from 'react-dom/client'
|
import { createRoot } from 'react-dom/client'
|
||||||
|
import { Provider } from 'react-redux'
|
||||||
|
import { store } from './store'
|
||||||
import './index.css'
|
import './index.css'
|
||||||
import App from './App.tsx'
|
import App from './App.tsx'
|
||||||
|
|
||||||
createRoot(document.getElementById('root')!).render(
|
createRoot(document.getElementById('root')!).render(
|
||||||
<StrictMode>
|
<StrictMode>
|
||||||
<App />
|
<Provider store={store}>
|
||||||
|
<App />
|
||||||
|
</Provider>
|
||||||
</StrictMode>,
|
</StrictMode>,
|
||||||
)
|
)
|
||||||
|
|||||||
21
furumi-node-player/client/src/store/index.ts
Normal file
21
furumi-node-player/client/src/store/index.ts
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
import { configureStore } from '@reduxjs/toolkit'
|
||||||
|
import { useDispatch, useSelector, type TypedUseSelectorHook } from 'react-redux'
|
||||||
|
import artistsReducer from './slices/artistsSlice'
|
||||||
|
import albumsReducer from './slices/albumsSlice'
|
||||||
|
import albumTracksReducer from './slices/albumTracksSlice'
|
||||||
|
import trackDetailReducer from './slices/trackDetailSlice'
|
||||||
|
|
||||||
|
export const store = configureStore({
|
||||||
|
reducer: {
|
||||||
|
artists: artistsReducer,
|
||||||
|
albums: albumsReducer,
|
||||||
|
albumTracks: albumTracksReducer,
|
||||||
|
trackDetail: trackDetailReducer,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
export type RootState = ReturnType<typeof store.getState>
|
||||||
|
export type AppDispatch = typeof store.dispatch
|
||||||
|
|
||||||
|
export const useAppDispatch = useDispatch.withTypes<AppDispatch>()
|
||||||
|
export const useAppSelector: TypedUseSelectorHook<RootState> = useSelector
|
||||||
@@ -0,0 +1,54 @@
|
|||||||
|
import { createSlice, createAsyncThunk } from '@reduxjs/toolkit'
|
||||||
|
import type { Track } from '../../types'
|
||||||
|
import { getAlbumTracks } from '../../furumiApi'
|
||||||
|
|
||||||
|
export const fetchAlbumTracks = createAsyncThunk(
|
||||||
|
'albumTracks/fetch',
|
||||||
|
async (albumSlug: string, { rejectWithValue }) => {
|
||||||
|
const data = await getAlbumTracks(albumSlug)
|
||||||
|
if (data === null) return rejectWithValue('Failed to fetch album tracks')
|
||||||
|
return { albumSlug, tracks: data }
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
interface AlbumTracksState {
|
||||||
|
byAlbum: Record<string, Track[]>
|
||||||
|
loading: boolean
|
||||||
|
error: string | null
|
||||||
|
}
|
||||||
|
|
||||||
|
const initialState: AlbumTracksState = {
|
||||||
|
byAlbum: {},
|
||||||
|
loading: false,
|
||||||
|
error: null,
|
||||||
|
}
|
||||||
|
|
||||||
|
const albumTracksSlice = createSlice({
|
||||||
|
name: 'albumTracks',
|
||||||
|
initialState,
|
||||||
|
reducers: {
|
||||||
|
clearAlbumTracks(state) {
|
||||||
|
state.byAlbum = {}
|
||||||
|
state.error = null
|
||||||
|
},
|
||||||
|
},
|
||||||
|
extraReducers: (builder) => {
|
||||||
|
builder
|
||||||
|
.addCase(fetchAlbumTracks.pending, (state) => {
|
||||||
|
state.loading = true
|
||||||
|
state.error = null
|
||||||
|
})
|
||||||
|
.addCase(fetchAlbumTracks.fulfilled, (state, action) => {
|
||||||
|
state.loading = false
|
||||||
|
state.byAlbum[action.payload.albumSlug] = action.payload.tracks
|
||||||
|
state.error = null
|
||||||
|
})
|
||||||
|
.addCase(fetchAlbumTracks.rejected, (state, action) => {
|
||||||
|
state.loading = false
|
||||||
|
state.error = action.payload as string ?? 'Unknown error'
|
||||||
|
})
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
export const { clearAlbumTracks } = albumTracksSlice.actions
|
||||||
|
export default albumTracksSlice.reducer
|
||||||
54
furumi-node-player/client/src/store/slices/albumsSlice.ts
Normal file
54
furumi-node-player/client/src/store/slices/albumsSlice.ts
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
import { createSlice, createAsyncThunk } from '@reduxjs/toolkit'
|
||||||
|
import type { Album } from '../../types'
|
||||||
|
import { getArtistAlbums } from '../../furumiApi'
|
||||||
|
|
||||||
|
export const fetchArtistAlbums = createAsyncThunk(
|
||||||
|
'albums/fetchByArtist',
|
||||||
|
async (artistSlug: string, { rejectWithValue }) => {
|
||||||
|
const data = await getArtistAlbums(artistSlug)
|
||||||
|
if (data === null) return rejectWithValue('Failed to fetch albums')
|
||||||
|
return { artistSlug, albums: data }
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
interface AlbumsState {
|
||||||
|
byArtist: Record<string, Album[]>
|
||||||
|
loading: boolean
|
||||||
|
error: string | null
|
||||||
|
}
|
||||||
|
|
||||||
|
const initialState: AlbumsState = {
|
||||||
|
byArtist: {},
|
||||||
|
loading: false,
|
||||||
|
error: null,
|
||||||
|
}
|
||||||
|
|
||||||
|
const albumsSlice = createSlice({
|
||||||
|
name: 'albums',
|
||||||
|
initialState,
|
||||||
|
reducers: {
|
||||||
|
clearAlbums(state) {
|
||||||
|
state.byArtist = {}
|
||||||
|
state.error = null
|
||||||
|
},
|
||||||
|
},
|
||||||
|
extraReducers: (builder) => {
|
||||||
|
builder
|
||||||
|
.addCase(fetchArtistAlbums.pending, (state) => {
|
||||||
|
state.loading = true
|
||||||
|
state.error = null
|
||||||
|
})
|
||||||
|
.addCase(fetchArtistAlbums.fulfilled, (state, action) => {
|
||||||
|
state.loading = false
|
||||||
|
state.byArtist[action.payload.artistSlug] = action.payload.albums
|
||||||
|
state.error = null
|
||||||
|
})
|
||||||
|
.addCase(fetchArtistAlbums.rejected, (state, action) => {
|
||||||
|
state.loading = false
|
||||||
|
state.error = action.payload as string ?? 'Unknown error'
|
||||||
|
})
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
export const { clearAlbums } = albumsSlice.actions
|
||||||
|
export default albumsSlice.reducer
|
||||||
54
furumi-node-player/client/src/store/slices/artistsSlice.ts
Normal file
54
furumi-node-player/client/src/store/slices/artistsSlice.ts
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
import { createSlice, createAsyncThunk } from '@reduxjs/toolkit'
|
||||||
|
import type { Artist } from '../../types'
|
||||||
|
import { getArtists } from '../../furumiApi'
|
||||||
|
|
||||||
|
export const fetchArtists = createAsyncThunk(
|
||||||
|
'artists/fetch',
|
||||||
|
async (_, { rejectWithValue }) => {
|
||||||
|
const data = await getArtists()
|
||||||
|
if (data === null) return rejectWithValue('Failed to fetch artists')
|
||||||
|
return data
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
interface ArtistsState {
|
||||||
|
items: Artist[]
|
||||||
|
loading: boolean
|
||||||
|
error: string | null
|
||||||
|
}
|
||||||
|
|
||||||
|
const initialState: ArtistsState = {
|
||||||
|
items: [],
|
||||||
|
loading: false,
|
||||||
|
error: null,
|
||||||
|
}
|
||||||
|
|
||||||
|
const artistsSlice = createSlice({
|
||||||
|
name: 'artists',
|
||||||
|
initialState,
|
||||||
|
reducers: {
|
||||||
|
clearArtists(state) {
|
||||||
|
state.items = []
|
||||||
|
state.error = null
|
||||||
|
},
|
||||||
|
},
|
||||||
|
extraReducers: (builder) => {
|
||||||
|
builder
|
||||||
|
.addCase(fetchArtists.pending, (state) => {
|
||||||
|
state.loading = true
|
||||||
|
state.error = null
|
||||||
|
})
|
||||||
|
.addCase(fetchArtists.fulfilled, (state, action) => {
|
||||||
|
state.loading = false
|
||||||
|
state.items = action.payload
|
||||||
|
state.error = null
|
||||||
|
})
|
||||||
|
.addCase(fetchArtists.rejected, (state, action) => {
|
||||||
|
state.loading = false
|
||||||
|
state.error = action.payload as string ?? 'Unknown error'
|
||||||
|
})
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
export const { clearArtists } = artistsSlice.actions
|
||||||
|
export default artistsSlice.reducer
|
||||||
@@ -0,0 +1,57 @@
|
|||||||
|
import { createSlice, createAsyncThunk } from '@reduxjs/toolkit'
|
||||||
|
import type { TrackDetail } from '../../types'
|
||||||
|
import { getTrackInfo } from '../../furumiApi'
|
||||||
|
|
||||||
|
export const fetchTrackDetail = createAsyncThunk(
|
||||||
|
'trackDetail/fetch',
|
||||||
|
async (trackSlug: string, { rejectWithValue }) => {
|
||||||
|
const data = await getTrackInfo(trackSlug)
|
||||||
|
if (data === null) return rejectWithValue('Failed to fetch track detail')
|
||||||
|
return { trackSlug, detail: data }
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
interface TrackDetailState {
|
||||||
|
bySlug: Record<string, TrackDetail>
|
||||||
|
loading: boolean
|
||||||
|
error: string | null
|
||||||
|
}
|
||||||
|
|
||||||
|
const initialState: TrackDetailState = {
|
||||||
|
bySlug: {},
|
||||||
|
loading: false,
|
||||||
|
error: null,
|
||||||
|
}
|
||||||
|
|
||||||
|
const trackDetailSlice = createSlice({
|
||||||
|
name: 'trackDetail',
|
||||||
|
initialState,
|
||||||
|
reducers: {
|
||||||
|
clearTrackDetail(state) {
|
||||||
|
state.bySlug = {}
|
||||||
|
state.error = null
|
||||||
|
},
|
||||||
|
removeTrackDetail(state, action: { payload: string }) {
|
||||||
|
delete state.bySlug[action.payload]
|
||||||
|
},
|
||||||
|
},
|
||||||
|
extraReducers: (builder) => {
|
||||||
|
builder
|
||||||
|
.addCase(fetchTrackDetail.pending, (state) => {
|
||||||
|
state.loading = true
|
||||||
|
state.error = null
|
||||||
|
})
|
||||||
|
.addCase(fetchTrackDetail.fulfilled, (state, action) => {
|
||||||
|
state.loading = false
|
||||||
|
state.bySlug[action.payload.trackSlug] = action.payload.detail
|
||||||
|
state.error = null
|
||||||
|
})
|
||||||
|
.addCase(fetchTrackDetail.rejected, (state, action) => {
|
||||||
|
state.loading = false
|
||||||
|
state.error = action.payload as string ?? 'Unknown error'
|
||||||
|
})
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
export const { clearTrackDetail, removeTrackDetail } = trackDetailSlice.actions
|
||||||
|
export default trackDetailSlice.reducer
|
||||||
Reference in New Issue
Block a user