Cleanup imports and ssr cabable components
This commit is contained in:
parent
1d418e1444
commit
bc790df89e
8 changed files with 40 additions and 57 deletions
|
|
@ -1,5 +1,6 @@
|
||||||
import "./globals.css";
|
import "./globals.css";
|
||||||
import { Providers } from "./providers";
|
import { ReactNode } from "react";
|
||||||
|
import { Providers } from "@/app/providers";
|
||||||
import { Metadata } from "next";
|
import { Metadata } from "next";
|
||||||
import Script from "next/script";
|
import Script from "next/script";
|
||||||
import { Footer } from "@/components/Footer";
|
import { Footer } from "@/components/Footer";
|
||||||
|
|
@ -48,12 +49,7 @@ export const metadata: Metadata = {
|
||||||
const isDev = process.env.NEXT_PUBLIC_NODE_ENV !== 'production' || process.env.NODE_ENV == null;
|
const isDev = process.env.NEXT_PUBLIC_NODE_ENV !== 'production' || process.env.NODE_ENV == null;
|
||||||
//const isDev = false;
|
//const isDev = false;
|
||||||
|
|
||||||
export default function RootLayout({
|
export default function RootLayout({ children }: { children: ReactNode }) {
|
||||||
children,
|
|
||||||
}: {
|
|
||||||
children: React.ReactNode;
|
|
||||||
}) {
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<html lang="en" suppressHydrationWarning>
|
<html lang="en" suppressHydrationWarning>
|
||||||
<head>
|
<head>
|
||||||
|
|
|
||||||
|
|
@ -1,32 +1,17 @@
|
||||||
'use client';
|
|
||||||
|
|
||||||
import { useState } from 'react';
|
|
||||||
import { DocumentUploader } from '@/components/DocumentUploader';
|
import { DocumentUploader } from '@/components/DocumentUploader';
|
||||||
import { DocumentList } from '@/components/DocumentList';
|
import { DocumentList } from '@/components/DocumentList';
|
||||||
import { SettingsModal } from '@/components/SettingsModal';
|
import { SettingsModal } from '@/components/SettingsModal';
|
||||||
import { SettingsIcon } from '@/components/icons/Icons';
|
|
||||||
import { Button } from '@headlessui/react';
|
|
||||||
|
|
||||||
export default function Home() {
|
export default function Home() {
|
||||||
const [isSettingsOpen, setIsSettingsOpen] = useState(false);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='p-3.5 sm:p-5'>
|
<div className='p-3.5 sm:p-5'>
|
||||||
<Button
|
<SettingsModal />
|
||||||
onClick={() => setIsSettingsOpen(true)}
|
|
||||||
className="rounded-full p-2 text-foreground hover:bg-offbase transform transition-transform duration-200 ease-in-out hover:scale-[1.1] hover:text-accent absolute top-1 left-1 sm:top-3 sm:left-3"
|
|
||||||
aria-label="Settings"
|
|
||||||
tabIndex={0}
|
|
||||||
>
|
|
||||||
<SettingsIcon className="w-4 h-4 sm:w-5 sm:h-5 hover:animate-spin-slow" />
|
|
||||||
</Button>
|
|
||||||
<h1 className="text-xl font-bold text-center flex-grow">OpenReader WebUI</h1>
|
<h1 className="text-xl font-bold text-center flex-grow">OpenReader WebUI</h1>
|
||||||
<p className="text-sm mt-1 text-center text-muted mb-5">A bring your own text-to-speech api web interface for reading documents with high quality voices</p>
|
<p className="text-sm mt-1 text-center text-muted mb-5">A bring your own text-to-speech api web interface for reading documents with high quality voices</p>
|
||||||
<div className="flex flex-col items-center gap-5">
|
<div className="flex flex-col items-center gap-5">
|
||||||
<DocumentUploader className='max-w-xl' />
|
<DocumentUploader className='max-w-xl' />
|
||||||
<DocumentList />
|
<DocumentList />
|
||||||
</div>
|
</div>
|
||||||
<SettingsModal isOpen={isSettingsOpen} setIsOpen={setIsSettingsOpen} />
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,4 @@
|
||||||
'use client';
|
import { Fragment, KeyboardEvent } from 'react';
|
||||||
|
|
||||||
import { Fragment, useEffect } from 'react';
|
|
||||||
import { Dialog, DialogPanel, DialogTitle, Transition, TransitionChild } from '@headlessui/react';
|
import { Dialog, DialogPanel, DialogTitle, Transition, TransitionChild } from '@headlessui/react';
|
||||||
|
|
||||||
interface ConfirmDialogProps {
|
interface ConfirmDialogProps {
|
||||||
|
|
@ -24,23 +22,21 @@ export function ConfirmDialog({
|
||||||
cancelText = 'Cancel',
|
cancelText = 'Cancel',
|
||||||
isDangerous = false,
|
isDangerous = false,
|
||||||
}: ConfirmDialogProps) {
|
}: ConfirmDialogProps) {
|
||||||
useEffect(() => {
|
const handleKeyDown = (e: KeyboardEvent) => {
|
||||||
if (!isOpen) return;
|
if (e.key === 'Enter') {
|
||||||
|
e.preventDefault();
|
||||||
const handleKeyDown = (e: KeyboardEvent) => {
|
onConfirm();
|
||||||
if (e.key === 'Enter') {
|
}
|
||||||
e.preventDefault();
|
};
|
||||||
onConfirm();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
window.addEventListener('keydown', handleKeyDown);
|
|
||||||
return () => window.removeEventListener('keydown', handleKeyDown);
|
|
||||||
}, [isOpen, onConfirm]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Transition appear show={isOpen} as={Fragment}>
|
<Transition appear show={isOpen} as={Fragment}>
|
||||||
<Dialog as="div" className="relative z-50" onClose={onClose}>
|
<Dialog
|
||||||
|
as="div"
|
||||||
|
className="relative z-50"
|
||||||
|
onClose={onClose}
|
||||||
|
onKeyDown={handleKeyDown}
|
||||||
|
>
|
||||||
<TransitionChild
|
<TransitionChild
|
||||||
as={Fragment}
|
as={Fragment}
|
||||||
enter="ease-out duration-300"
|
enter="ease-out duration-300"
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@ import { Fragment, useState, useEffect, useCallback } from 'react';
|
||||||
import { Dialog, DialogPanel, DialogTitle, Transition, TransitionChild, Listbox, ListboxButton, ListboxOptions, ListboxOption, Button } from '@headlessui/react';
|
import { Dialog, DialogPanel, DialogTitle, Transition, TransitionChild, Listbox, ListboxButton, ListboxOptions, ListboxOption, Button } from '@headlessui/react';
|
||||||
import { useTheme } from '@/contexts/ThemeContext';
|
import { useTheme } from '@/contexts/ThemeContext';
|
||||||
import { useConfig } from '@/contexts/ConfigContext';
|
import { useConfig } from '@/contexts/ConfigContext';
|
||||||
import { ChevronUpDownIcon, CheckIcon } from './icons/Icons';
|
import { ChevronUpDownIcon, CheckIcon, SettingsIcon } from '@/components/icons/Icons';
|
||||||
import { indexedDBService } from '@/utils/indexedDB';
|
import { indexedDBService } from '@/utils/indexedDB';
|
||||||
import { useDocuments } from '@/contexts/DocumentContext';
|
import { useDocuments } from '@/contexts/DocumentContext';
|
||||||
import { setItem, getItem } from '@/utils/indexedDB';
|
import { setItem, getItem } from '@/utils/indexedDB';
|
||||||
|
|
@ -13,17 +13,14 @@ import { THEMES } from '@/contexts/ThemeContext';
|
||||||
|
|
||||||
const isDev = process.env.NEXT_PUBLIC_NODE_ENV !== 'production' || process.env.NODE_ENV == null;
|
const isDev = process.env.NEXT_PUBLIC_NODE_ENV !== 'production' || process.env.NODE_ENV == null;
|
||||||
|
|
||||||
interface SettingsModalProps {
|
|
||||||
isOpen: boolean;
|
|
||||||
setIsOpen: (isOpen: boolean) => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
const themes = THEMES.map(id => ({
|
const themes = THEMES.map(id => ({
|
||||||
id,
|
id,
|
||||||
name: id.charAt(0).toUpperCase() + id.slice(1)
|
name: id.charAt(0).toUpperCase() + id.slice(1)
|
||||||
}));
|
}));
|
||||||
|
|
||||||
export function SettingsModal({ isOpen, setIsOpen }: SettingsModalProps) {
|
export function SettingsModal() {
|
||||||
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
|
|
||||||
const { theme, setTheme } = useTheme();
|
const { theme, setTheme } = useTheme();
|
||||||
const { apiKey, baseUrl, updateConfig } = useConfig();
|
const { apiKey, baseUrl, updateConfig } = useConfig();
|
||||||
const { refreshPDFs, refreshEPUBs, clearPDFs, clearEPUBs } = useDocuments();
|
const { refreshPDFs, refreshEPUBs, clearPDFs, clearEPUBs } = useDocuments();
|
||||||
|
|
@ -95,7 +92,14 @@ export function SettingsModal({ isOpen, setIsOpen }: SettingsModalProps) {
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<Button
|
||||||
|
onClick={() => setIsOpen(true)}
|
||||||
|
className="rounded-full p-2 text-foreground hover:bg-offbase transform transition-transform duration-200 ease-in-out hover:scale-[1.1] hover:text-accent absolute top-1 left-1 sm:top-3 sm:left-3"
|
||||||
|
aria-label="Settings"
|
||||||
|
tabIndex={0}
|
||||||
|
>
|
||||||
|
<SettingsIcon className="w-4 h-4 sm:w-5 sm:h-5 hover:animate-spin-slow" />
|
||||||
|
|
||||||
<Transition appear show={isOpen} as={Fragment}>
|
<Transition appear show={isOpen} as={Fragment}>
|
||||||
<Dialog as="div" className="relative z-50" onClose={() => setIsOpen(false)}>
|
<Dialog as="div" className="relative z-50" onClose={() => setIsOpen(false)}>
|
||||||
<TransitionChild
|
<TransitionChild
|
||||||
|
|
@ -306,6 +310,6 @@ export function SettingsModal({ isOpen, setIsOpen }: SettingsModalProps) {
|
||||||
confirmText="Delete"
|
confirmText="Delete"
|
||||||
isDangerous={true}
|
isDangerous={true}
|
||||||
/>
|
/>
|
||||||
</>
|
</Button>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { createContext, useContext, useEffect, useState } from 'react';
|
import { createContext, useContext, useEffect, useState, ReactNode } from 'react';
|
||||||
import { getItem, indexedDBService, setItem } from '@/utils/indexedDB';
|
import { getItem, indexedDBService, setItem } from '@/utils/indexedDB';
|
||||||
|
|
||||||
export type ViewType = 'single' | 'dual' | 'scroll';
|
export type ViewType = 'single' | 'dual' | 'scroll';
|
||||||
|
|
@ -29,7 +29,7 @@ type ConfigValues = {
|
||||||
|
|
||||||
const ConfigContext = createContext<ConfigContextType | undefined>(undefined);
|
const ConfigContext = createContext<ConfigContextType | undefined>(undefined);
|
||||||
|
|
||||||
export function ConfigProvider({ children }: { children: React.ReactNode }) {
|
export function ConfigProvider({ children }: { children: ReactNode }) {
|
||||||
// Config state
|
// Config state
|
||||||
const [apiKey, setApiKey] = useState<string>('');
|
const [apiKey, setApiKey] = useState<string>('');
|
||||||
const [baseUrl, setBaseUrl] = useState<string>('');
|
const [baseUrl, setBaseUrl] = useState<string>('');
|
||||||
|
|
|
||||||
|
|
@ -21,6 +21,7 @@ import {
|
||||||
useEffect,
|
useEffect,
|
||||||
useCallback,
|
useCallback,
|
||||||
useMemo,
|
useMemo,
|
||||||
|
RefObject,
|
||||||
} from 'react';
|
} from 'react';
|
||||||
|
|
||||||
import { indexedDBService } from '@/utils/indexedDB';
|
import { indexedDBService } from '@/utils/indexedDB';
|
||||||
|
|
@ -51,12 +52,12 @@ interface PDFContextType {
|
||||||
|
|
||||||
// PDF functionality
|
// PDF functionality
|
||||||
onDocumentLoadSuccess: (pdf: PDFDocumentProxy) => void;
|
onDocumentLoadSuccess: (pdf: PDFDocumentProxy) => void;
|
||||||
highlightPattern: (text: string, pattern: string, containerRef: React.RefObject<HTMLDivElement>) => void;
|
highlightPattern: (text: string, pattern: string, containerRef: RefObject<HTMLDivElement>) => void;
|
||||||
clearHighlights: () => void;
|
clearHighlights: () => void;
|
||||||
handleTextClick: (
|
handleTextClick: (
|
||||||
event: MouseEvent,
|
event: MouseEvent,
|
||||||
pdfText: string,
|
pdfText: string,
|
||||||
containerRef: React.RefObject<HTMLDivElement>,
|
containerRef: RefObject<HTMLDivElement>,
|
||||||
stopAndPlayFromIndex: (index: number) => void,
|
stopAndPlayFromIndex: (index: number) => void,
|
||||||
isProcessing: boolean
|
isProcessing: boolean
|
||||||
) => void;
|
) => void;
|
||||||
|
|
|
||||||
|
|
@ -14,7 +14,7 @@
|
||||||
|
|
||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import React, {
|
import {
|
||||||
createContext,
|
createContext,
|
||||||
useContext,
|
useContext,
|
||||||
useState,
|
useState,
|
||||||
|
|
@ -22,6 +22,7 @@ import React, {
|
||||||
useEffect,
|
useEffect,
|
||||||
useRef,
|
useRef,
|
||||||
useMemo,
|
useMemo,
|
||||||
|
ReactNode,
|
||||||
} from 'react';
|
} from 'react';
|
||||||
import OpenAI from 'openai';
|
import OpenAI from 'openai';
|
||||||
import { Howl } from 'howler';
|
import { Howl } from 'howler';
|
||||||
|
|
@ -84,7 +85,7 @@ const TTSContext = createContext<TTSContextType | undefined>(undefined);
|
||||||
* Main provider component that manages the TTS state and functionality.
|
* Main provider component that manages the TTS state and functionality.
|
||||||
* Handles initialization of OpenAI client, audio context, and media session.
|
* Handles initialization of OpenAI client, audio context, and media session.
|
||||||
*/
|
*/
|
||||||
export function TTSProvider({ children }: { children: React.ReactNode }) {
|
export function TTSProvider({ children }: { children: ReactNode }) {
|
||||||
// Configuration context consumption
|
// Configuration context consumption
|
||||||
const {
|
const {
|
||||||
apiKey: openApiKey,
|
apiKey: openApiKey,
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { createContext, useContext, useEffect, useState } from 'react';
|
import { createContext, useContext, useEffect, useState, ReactNode } from 'react';
|
||||||
|
|
||||||
const THEMES = ['system', 'light', 'dark', 'aqua', 'forest', 'vibrant'] as const;
|
const THEMES = ['system', 'light', 'dark', 'aqua', 'forest', 'vibrant'] as const;
|
||||||
type Theme = (typeof THEMES)[number];
|
type Theme = (typeof THEMES)[number];
|
||||||
|
|
@ -24,7 +24,7 @@ const getEffectiveTheme = (theme: Theme): Theme => {
|
||||||
return theme;
|
return theme;
|
||||||
};
|
};
|
||||||
|
|
||||||
export function ThemeProvider({ children }: { children: React.ReactNode }) {
|
export function ThemeProvider({ children }: { children: ReactNode }) {
|
||||||
const [theme, setTheme] = useState<Theme>(() => {
|
const [theme, setTheme] = useState<Theme>(() => {
|
||||||
if (typeof window === 'undefined') return 'system';
|
if (typeof window === 'undefined') return 'system';
|
||||||
return (localStorage.getItem('theme') as Theme) || 'system';
|
return (localStorage.getItem('theme') as Theme) || 'system';
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue