import { createLazyMemo } from '@solid-primitives/memo' import { openDB } from 'idb' import { Accessor, JSX, createContext, createMemo, createSignal, onMount, useContext } from 'solid-js' import { apiClient } from '../graphql/client/core' import { Topic } from '../graphql/schema/core.gen' import { useRouter } from '../stores/router' import { byTopicStatDesc } from '../utils/sortby' type TopicsContextType = { topicEntities: Accessor<{ [topicSlug: string]: Topic }> sortedTopics: Accessor randomTopics: Accessor topTopics: Accessor setTopicsSort: (sortBy: string) => void addTopics: (topics: Topic[]) => void loadTopics: () => Promise } const TopicsContext = createContext() export function useTopics() { return useContext(TopicsContext) } const DB_NAME = 'discourseAppDB' const DB_VERSION = 1 const STORE_NAME = 'topics' const setupIndexedDB = async () => { return await openDB(DB_NAME, DB_VERSION, { upgrade(db) { if (!db.objectStoreNames.contains(STORE_NAME)) { db.createObjectStore(STORE_NAME, { keyPath: 'id' }) } }, }) } const getTopicsFromIndexedDB = async (db) => { const tx = db.transaction(STORE_NAME, 'readonly') const store = tx.objectStore(STORE_NAME) const topics = await store.getAll() return { topics, timestamp: tx.done } } const saveTopicsToIndexedDB = async (db, topics) => { const tx = db.transaction(STORE_NAME, 'readwrite') const store = tx.objectStore(STORE_NAME) for (const topic of topics) { await store.put(topic) } await tx.done } export const TopicsProvider = (props: { children: JSX.Element }) => { const [topicEntities, setTopicEntities] = createSignal<{ [topicSlug: string]: Topic }>({}) const [sortAllBy, setSortAllBy] = createSignal<'shouts' | 'followers' | 'authors' | 'title'>('shouts') const [randomTopics, setRandomTopics] = createSignal([]) const sortedTopics = createLazyMemo(() => { const topics = Object.values(topicEntities()) const { changeSearchParams } = useRouter() switch (sortAllBy()) { case 'followers': { topics.sort(byTopicStatDesc('followers')) break } case 'shouts': { topics.sort(byTopicStatDesc('shouts')) break } case 'authors': { topics.sort(byTopicStatDesc('authors')) break } case 'title': { topics.sort((a, b) => a.title.localeCompare(b.title)) break } default: { topics.sort(byTopicStatDesc('shouts')) changeSearchParams({ by: 'shouts' }) } } return topics }) const topTopics = createMemo(() => { const topics = Object.values(topicEntities()) topics.sort(byTopicStatDesc('shouts')) return topics }) const addTopics = (...args: Topic[][]) => { const allTopics = args.flatMap((topics) => (topics || []).filter(Boolean)) const newTopicEntities = allTopics.reduce( (acc, topic) => { acc[topic.slug] = topic return acc }, {} as Record, ) setTopicEntities((prevTopicEntities) => { return { ...prevTopicEntities, ...newTopicEntities, } }) } const [db, setDb] = createSignal() const loadTopics = async () => { const ttt = await apiClient.getAllTopics() await saveTopicsToIndexedDB(db(), ttt) return ttt } onMount(async () => { const db = await setupIndexedDB() setDb(db) let { topics, timestamp } = await getTopicsFromIndexedDB(db) if (topics.length < 100 || Date.now() - timestamp > 3600000) { const newTopics = await loadTopics() await saveTopicsToIndexedDB(db, newTopics) topics = newTopics } addTopics(topics) setRandomTopics(topics) }) const value: TopicsContextType = { setTopicsSort: setSortAllBy, topicEntities, sortedTopics, randomTopics, topTopics, addTopics, loadTopics, } return {props.children} }