64 lines
2.3 KiB
TypeScript
64 lines
2.3 KiB
TypeScript
import type { Entry } from '../types/entry'
|
|
|
|
const KIND_ORDER: Record<Entry['kind'], number> = { review: 0, note: 1 }
|
|
|
|
function toEntries(reviews: Review[], notes: Note[]): Entry[] {
|
|
return [
|
|
...reviews.map(review => ({ kind: 'review' as const, key: `review-${review.id}`, date: review.date, text: review.text, review })),
|
|
...notes.map(note => ({ kind: 'note' as const, key: `note-${note.id}`, date: note.date, text: note.text, note }))
|
|
]
|
|
}
|
|
|
|
function searchableText(entry: Entry): string {
|
|
const date = `${formatDate(entry.date)} ${entry.date}`
|
|
const text = entry.kind === 'review'
|
|
? `review ${entry.review.medium ?? ''} ${date} ${entry.review.title} ${entry.review.author ?? ''} ${entry.text}`
|
|
: `note ${date} ${entry.text}`
|
|
return text.toLowerCase()
|
|
}
|
|
|
|
function newestFirst(a: Entry, b: Entry): number {
|
|
return b.date.localeCompare(a.date) || KIND_ORDER[a.kind] - KIND_ORDER[b.kind] || b.key.localeCompare(a.key)
|
|
}
|
|
|
|
function entryIcon(entry: Entry): string {
|
|
if (entry.kind === 'note') return NOTE_ICON
|
|
return entry.review.medium ? MEDIUM_ICONS[entry.review.medium] : REVIEW_ICON
|
|
}
|
|
|
|
function groupByYear(entries: Entry[]) {
|
|
const years = new Map<string, Entry[]>()
|
|
for (const entry of entries) {
|
|
const year = entry.date.slice(0, 4)
|
|
years.set(year, [...(years.get(year) ?? []), entry])
|
|
}
|
|
return [...years].map(([year, entries]) => ({
|
|
year,
|
|
items: entries.map(entry => ({ value: entry.key, icon: entryIcon(entry), entry }))
|
|
}))
|
|
}
|
|
|
|
export function useDiary(search: Ref<string>) {
|
|
const { data: reviews, error: reviewsError } = useFetch<Review[]>('/api/diary/reviews', { key: 'diary-reviews', default: () => [] })
|
|
const { data: notes, error: notesError } = useFetch<Note[]>('/api/diary/notes', { key: 'diary-notes', default: () => [] })
|
|
|
|
const years = computed(() => {
|
|
const terms = searchTerms(search.value)
|
|
const matches = toEntries(reviews.value, notes.value)
|
|
.filter(entry => entry.text)
|
|
.filter((entry) => {
|
|
const text = searchableText(entry)
|
|
return terms.every(term => text.includes(term))
|
|
})
|
|
.toSorted(newestFirst)
|
|
return groupByYear(matches)
|
|
})
|
|
|
|
const errors = computed(() => [
|
|
{ what: 'reviews', error: reviewsError.value },
|
|
{ what: 'notes', error: notesError.value }
|
|
].filter(({ error }) => error))
|
|
|
|
return { years, errors }
|
|
}
|