feat: unify research in profile-bound decks with TweetDeck-style UI
This commit is contained in:
@@ -1,17 +1,30 @@
|
||||
import { useEffect, useState } from 'react'
|
||||
import { type Deck, deckSchema, EMPTY_DECK } from './model'
|
||||
import { useEffect, useRef, useState } from 'react'
|
||||
import {
|
||||
type Deck,
|
||||
deckSchema,
|
||||
EMPTY_DECK,
|
||||
EMPTY_WORKSPACE,
|
||||
newId,
|
||||
type Workspace,
|
||||
workspaceSchema,
|
||||
} from './model'
|
||||
|
||||
export const DECK_STORAGE_KEY = 'twitter-lite-research-deck'
|
||||
|
||||
export function useDeck() {
|
||||
const [deck, setDeck] = useState<Deck>(EMPTY_DECK)
|
||||
const [workspace, setWorkspace] = useState<Workspace>(EMPTY_WORKSPACE)
|
||||
const current = useRef(workspace)
|
||||
const [ready, setReady] = useState(false)
|
||||
const [storageError, setStorageError] = useState<string>()
|
||||
|
||||
useEffect(() => {
|
||||
try {
|
||||
const saved = localStorage.getItem(DECK_STORAGE_KEY)
|
||||
if (saved !== null) setDeck(deckSchema.parse(JSON.parse(saved)))
|
||||
if (saved !== null) {
|
||||
const restored = workspaceSchema.parse(JSON.parse(saved))
|
||||
current.current = restored
|
||||
setWorkspace(restored)
|
||||
}
|
||||
} catch {
|
||||
setStorageError(
|
||||
'保存したデッキを読み込めませんでした。編集内容を保存すると、このブラウザのデッキを置き換えます。',
|
||||
@@ -20,9 +33,10 @@ export function useDeck() {
|
||||
setReady(true)
|
||||
}, [])
|
||||
|
||||
function save(next: Deck) {
|
||||
const validated = deckSchema.parse(next)
|
||||
setDeck(validated)
|
||||
function persist(next: Workspace) {
|
||||
const validated = workspaceSchema.parse(next)
|
||||
current.current = validated
|
||||
setWorkspace(validated)
|
||||
try {
|
||||
localStorage.setItem(DECK_STORAGE_KEY, JSON.stringify(validated))
|
||||
setStorageError(undefined)
|
||||
@@ -35,5 +49,56 @@ export function useDeck() {
|
||||
}
|
||||
}
|
||||
|
||||
return { deck, ready, storageError, save }
|
||||
function save(next: Deck) {
|
||||
const validated = deckSchema.parse(next)
|
||||
const previous = current.current
|
||||
const exists = previous.decks.some((deck) => deck.id === validated.id)
|
||||
return persist({
|
||||
...previous,
|
||||
activeDeckId: validated.id,
|
||||
decks: exists
|
||||
? previous.decks.map((deck) =>
|
||||
deck.id === validated.id ? validated : deck,
|
||||
)
|
||||
: [...previous.decks, validated],
|
||||
})
|
||||
}
|
||||
|
||||
function create(title: string) {
|
||||
return save({ id: newId(), title, columns: [] })
|
||||
}
|
||||
function select(id: string) {
|
||||
if (!current.current.decks.some((deck) => deck.id === id))
|
||||
throw new Error('デッキが見つかりません。')
|
||||
return persist({ ...current.current, activeDeckId: id })
|
||||
}
|
||||
function remove(id: string) {
|
||||
const previous = current.current
|
||||
if (!previous.decks.some((deck) => deck.id === id))
|
||||
throw new Error('デッキが見つかりません。')
|
||||
const decks = previous.decks.filter((deck) => deck.id !== id)
|
||||
const first = decks[0]
|
||||
if (!first) throw new Error('最後のデッキは削除できません。')
|
||||
return persist({
|
||||
...previous,
|
||||
decks,
|
||||
activeDeckId:
|
||||
previous.activeDeckId === id ? first.id : previous.activeDeckId,
|
||||
})
|
||||
}
|
||||
|
||||
const deck =
|
||||
workspace.decks.find((deck) => deck.id === workspace.activeDeckId) ??
|
||||
EMPTY_DECK
|
||||
return {
|
||||
deck,
|
||||
workspace,
|
||||
ready,
|
||||
storageError,
|
||||
save,
|
||||
create,
|
||||
select,
|
||||
remove,
|
||||
getWorkspace: () => current.current,
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user