diff --git a/app/contexts/PresentationContext.tsx b/app/contexts/PresentationContext.tsx index f28148c..db9d020 100644 --- a/app/contexts/PresentationContext.tsx +++ b/app/contexts/PresentationContext.tsx @@ -1,11 +1,14 @@ +import type { ReactNode } from 'react'; import { createContext, - useContext, - useState, useCallback, + useContext, + useEffect, useMemo, + useState, } from 'react'; -import type { ReactNode } from 'react'; + +import useLocalStorage from '~/hooks/useLocalStorage'; interface PresentationContextType { markdownText: string; @@ -16,22 +19,26 @@ interface PresentationContextType { prevSlide: () => void; goToSlide: (index: number) => void; totalSlides: number; + canNext: boolean; + canPrev: boolean; } -const PresentationContext = createContext< - PresentationContextType | undefined ->(undefined); +const PresentationContext = createContext( + undefined +); export function usePresentationContext() { const context = useContext(PresentationContext); if (!context) { throw new Error( - 'usePresentationContext must be used within a PresentationProvider', + 'usePresentationContext must be used within a PresentationProvider' ); } return context; } +const STORAGE_KEY = 'airdeck-markdown'; + interface PresentationProviderProps { children: ReactNode; initialMarkdown?: string; @@ -41,20 +48,29 @@ export function PresentationProvider({ children, initialMarkdown = '', }: PresentationProviderProps) { - const [markdownText, setMarkdownText] = useState(initialMarkdown); - const [currentSlide, setCurrentSlide] = useState(0); + const [markdownText, setMarkdownText] = useLocalStorage( + STORAGE_KEY, + initialMarkdown + ); + + const [currentSlide, setCurrentSlide] = useState(0); const slides = useMemo( () => markdownText - .split(/\n---\n/) + .split(/\r?\n---\r?\n/) .map((slide) => slide.trim()) .filter((slide) => slide.length > 0), - [markdownText], + [markdownText] ); const totalSlides = slides.length; + const safeCurrentSlide = + totalSlides > 0 ? Math.min(currentSlide, totalSlides - 1) : 0; + const canNext = safeCurrentSlide < totalSlides - 1; + const canPrev = safeCurrentSlide > 0; + const nextSlide = useCallback(() => { setCurrentSlide((prev) => Math.min(prev + 1, totalSlides - 1)); }, [totalSlides]); @@ -69,22 +85,37 @@ export function PresentationProvider({ setCurrentSlide(index); } }, - [totalSlides], + [totalSlides] + ); + + const value = useMemo( + () => ({ + markdownText, + setMarkdownText, + slides, + currentSlide: safeCurrentSlide, + nextSlide, + prevSlide, + goToSlide, + totalSlides, + canNext, + canPrev, + }), + [ + markdownText, + slides, + safeCurrentSlide, + nextSlide, + prevSlide, + goToSlide, + totalSlides, + canNext, + canPrev, + ] ); return ( - + {children} ); diff --git a/app/hooks/useLocalStorage.ts b/app/hooks/useLocalStorage.ts new file mode 100644 index 0000000..d3bdac3 --- /dev/null +++ b/app/hooks/useLocalStorage.ts @@ -0,0 +1,39 @@ +import { useState, useEffect, useCallback } from 'react'; + +const useLocalStorage = ( + key: string, + defaultValue: T +): [T, (value: T | ((val: T) => T)) => void] => { + const [value, setValue] = useState(defaultValue); + + useEffect(() => { + try { + const item = window.localStorage.getItem(key); + if (item) { + setValue(JSON.parse(item)); + } + } catch (error) { + console.warn(`Error reading localStorage key "${key}":`, error); + } + }, [key]); + + const setStoredValue = useCallback( + (valueOrFn: T | ((val: T) => T)) => { + try { + const newValue = + valueOrFn instanceof Function ? valueOrFn(value) : valueOrFn; + setValue(newValue); + if (typeof window !== 'undefined') { + window.localStorage.setItem(key, JSON.stringify(newValue)); + } + } catch (error) { + console.error(error); + } + }, + [key, value] + ); + + return [value, setStoredValue]; +}; + +export default useLocalStorage;