Make dialogs responsive for mobile devices (#209)
* Make dialogs responsive for mobile devices * Support mouse backward/forward buttons * Use back icon to close layer 2 components on mobile devices Currently layer 2 components are: 1. sidebar 2. notebook 3. dialogs They are all fullscreen on mobile devices. * Add translations
This commit is contained in:
@@ -135,5 +135,8 @@
|
||||
"Apply to this book": "Auf dieses Buch anwenden",
|
||||
"Unable to fetch the translation. Try again later.": "Übersetzung konnte nicht abgerufen werden. Versuchen Sie es später erneut.",
|
||||
"Update Now!": "Jetzt aktualisieren!",
|
||||
"Update": "Aktualisieren"
|
||||
"Update": "Aktualisieren",
|
||||
"Check update": "Aktualisierung prüfen",
|
||||
"Already the latest version": "Bereits die neueste Version",
|
||||
"Book Details": "Buchdetails"
|
||||
}
|
||||
|
||||
@@ -135,5 +135,8 @@
|
||||
"Apply to this book": "Εφαρμογή σε αυτό το βιβλίο",
|
||||
"Unable to fetch the translation. Try again later.": "Αδυναμία λήψης της μετάφρασης. Δοκιμάστε ξανά αργότερα.",
|
||||
"Update Now!": "Ενημέρωση τώρα!",
|
||||
"Update": "Ενημέρωση"
|
||||
"Update": "Ενημέρωση",
|
||||
"Check update": "Έλεγχος ενημέρωσης",
|
||||
"Already the latest version": "Ήδη η τελευταία έκδοση",
|
||||
"Book Details": "Λεπτομέρειες βιβλίου"
|
||||
}
|
||||
|
||||
@@ -135,5 +135,8 @@
|
||||
"Apply to this book": "Aplicar a este libro",
|
||||
"Unable to fetch the translation. Try again later.": "No se puede obtener la traducción. Inténtalo de nuevo más tarde.",
|
||||
"Update Now!": "¡Actualizar ahora!",
|
||||
"Update": "Actualizar"
|
||||
"Update": "Actualizar",
|
||||
"Check update": "Comprobar actualización",
|
||||
"Already the latest version": "Ya es la última versión",
|
||||
"Book Details": "Detalles del libro"
|
||||
}
|
||||
|
||||
@@ -135,5 +135,8 @@
|
||||
"Apply to this book": "Appliquer à ce livre",
|
||||
"Unable to fetch the translation. Try again later.": "Impossible de récupérer la traduction. Réessayez plus tard.",
|
||||
"Update Now!": "Mettre à jour maintenant !",
|
||||
"Update": "Mettre à jour"
|
||||
"Update": "Mettre à jour",
|
||||
"Check update": "Vérifier la mise à jour",
|
||||
"Already the latest version": "Déjà la dernière version",
|
||||
"Book Details": "Détails du livre"
|
||||
}
|
||||
|
||||
@@ -135,5 +135,8 @@
|
||||
"Apply to this book": "इस पुस्तक पर लागू करें",
|
||||
"Unable to fetch the translation. Try again later.": "अनुवाद प्राप्त करने में असमर्थ। बाद में पुनः प्रयास करें।",
|
||||
"Update Now!": "अभी अपडेट करें!",
|
||||
"Update": "अपडेट करें"
|
||||
"Update": "अपडेट करें",
|
||||
"Check update": "अपडेट जांचें",
|
||||
"Already the latest version": "पहले से ही नवीनतम संस्करण",
|
||||
"Book Details": "पुस्तक विवरण"
|
||||
}
|
||||
|
||||
@@ -135,5 +135,8 @@
|
||||
"Apply to this book": "Terapkan ke buku ini",
|
||||
"Unable to fetch the translation. Try again later.": "Tidak dapat mengambil terjemahan. Coba lagi nanti.",
|
||||
"Update Now!": "Perbarui Sekarang!",
|
||||
"Update": "Perbarui"
|
||||
"Update": "Perbarui",
|
||||
"Check update": "Periksa pembaruan",
|
||||
"Already the latest version": "Sudah versi terbaru",
|
||||
"Book Details": "Detail Buku"
|
||||
}
|
||||
|
||||
@@ -135,5 +135,8 @@
|
||||
"Apply to this book": "Applica a questo libro",
|
||||
"Unable to fetch the translation. Try again later.": "Impossibile recuperare la traduzione. Riprova più tardi.",
|
||||
"Update Now!": "Aggiorna ora!",
|
||||
"Update": "Aggiorna"
|
||||
"Update": "Aggiorna",
|
||||
"Check update": "Controlla aggiornamento",
|
||||
"Already the latest version": "Già l'ultima versione",
|
||||
"Book Details": "Dettagli libro"
|
||||
}
|
||||
|
||||
@@ -135,5 +135,8 @@
|
||||
"Apply to this book": "この書籍に適用",
|
||||
"Unable to fetch the translation. Try again later.": "翻訳を取得できません。後で再試行してください。",
|
||||
"Update Now!": "今すぐ更新!",
|
||||
"Update": "更新"
|
||||
"Update": "更新",
|
||||
"Check update": "更新を確認",
|
||||
"Already the latest version": "すでに最新バージョン",
|
||||
"Book Details": "書籍の詳細"
|
||||
}
|
||||
|
||||
@@ -135,5 +135,8 @@
|
||||
"Apply to this book": "이 책에 적용",
|
||||
"Unable to fetch the translation. Try again later.": "번역을 가져올 수 없습니다. 나중에 다시 시도하세요.",
|
||||
"Update Now!": "지금 업데이트!",
|
||||
"Update": "업데이트"
|
||||
"Update": "업데이트",
|
||||
"Check update": "업데이트 확인",
|
||||
"Already the latest version": "이미 최신 버전",
|
||||
"Book Details": "책 세부 정보"
|
||||
}
|
||||
|
||||
@@ -135,5 +135,8 @@
|
||||
"Apply to this book": "Zastosuj do tej książki",
|
||||
"Unable to fetch the translation. Try again later.": "Nie udało się pobrać tłumaczenia. Spróbuj ponownie później.",
|
||||
"Update Now!": "Zaktualizuj teraz!",
|
||||
"Update": "Zaktualizuj"
|
||||
"Update": "Zaktualizuj",
|
||||
"Check update": "Sprawdź aktualizację",
|
||||
"Already the latest version": "Już najnowsza wersja",
|
||||
"Book Details": "Szczegóły książki"
|
||||
}
|
||||
|
||||
@@ -135,5 +135,8 @@
|
||||
"Apply to this book": "Aplicar a este livro",
|
||||
"Unable to fetch the translation. Try again later.": "Não foi possível buscar a tradução. Tente novamente mais tarde.",
|
||||
"Update Now!": "Atualizar Agora!",
|
||||
"Update": "Atualizar"
|
||||
"Update": "Atualizar",
|
||||
"Check update": "Verificar atualização",
|
||||
"Already the latest version": "Já é a versão mais recente",
|
||||
"Book Details": "Detalhes do Livro"
|
||||
}
|
||||
|
||||
@@ -135,5 +135,8 @@
|
||||
"Apply to this book": "Применить к этой книге",
|
||||
"Unable to fetch the translation. Try again later.": "Не удалось получить перевод. Попробуйте позже.",
|
||||
"Update Now!": "Обновить сейчас!",
|
||||
"Update": "Обновить"
|
||||
"Update": "Обновить",
|
||||
"Check update": "Проверить обновление",
|
||||
"Already the latest version": "Уже последняя версия",
|
||||
"Book Details": "Детали книги"
|
||||
}
|
||||
|
||||
@@ -135,5 +135,8 @@
|
||||
"Apply to this book": "Bu kitaba uygula",
|
||||
"Unable to fetch the translation. Try again later.": "Çeviri alınamıyor. Daha sonra tekrar deneyin.",
|
||||
"Update Now!": "Şimdi Güncelle!",
|
||||
"Update": "Güncelle"
|
||||
"Update": "Güncelle",
|
||||
"Check update": "Güncellemeyi kontrol et",
|
||||
"Already the latest version": "Zaten en son sürüm",
|
||||
"Book Details": "Kitap Detayları"
|
||||
}
|
||||
|
||||
@@ -135,5 +135,8 @@
|
||||
"Apply to this book": "Застосувати до цієї книги",
|
||||
"Unable to fetch the translation. Try again later.": "Не вдалося отримати переклад. Спробуйте пізніше.",
|
||||
"Update Now!": "Оновити зараз!",
|
||||
"Update": "Оновити"
|
||||
"Update": "Оновити",
|
||||
"Check update": "Перевірити оновлення",
|
||||
"Already the latest version": "Вже остання версія",
|
||||
"Book Details": "Деталі книги"
|
||||
}
|
||||
|
||||
@@ -135,5 +135,8 @@
|
||||
"Apply to this book": "Áp dụng cho sách này",
|
||||
"Unable to fetch the translation. Try again later.": "Không thể tải dịch vụ. Vui lòng thử lại sau.",
|
||||
"Update Now!": "Cập nhật ngay!",
|
||||
"Update": "Cập nhật"
|
||||
"Update": "Cập nhật",
|
||||
"Check update": "Kiểm tra cập nhật",
|
||||
"Already the latest version": "Đã là phiên bản mới nhất",
|
||||
"Book Details": "Chi tiết sách"
|
||||
}
|
||||
|
||||
@@ -135,5 +135,8 @@
|
||||
"Apply to this book": "应用于此书籍",
|
||||
"Unable to fetch the translation. Try again later.": "无法获取翻译,请稍后重试。",
|
||||
"Update Now!": "立即更新!",
|
||||
"Update": "更新"
|
||||
"Update": "更新",
|
||||
"Check update": "检查更新",
|
||||
"Already the latest version": "已经是最新版本",
|
||||
"Book Details": "书籍详情"
|
||||
}
|
||||
|
||||
@@ -135,5 +135,8 @@
|
||||
"Apply to this book": "應用於此書籍",
|
||||
"Unable to fetch the translation. Try again later.": "無法獲取翻譯,請稍後再試。",
|
||||
"Update Now!": "立即更新!",
|
||||
"Update": "更新"
|
||||
"Update": "更新",
|
||||
"Check update": "檢查更新",
|
||||
"Already the latest version": "已經是最新版本",
|
||||
"Book Details": "書籍詳情"
|
||||
}
|
||||
|
||||
@@ -48,8 +48,8 @@ const LibraryHeader: React.FC<LibraryHeaderProps> = ({
|
||||
isTrafficLightVisible ? 'pl-16' : 'pl-2',
|
||||
)}
|
||||
>
|
||||
<div className='flex items-center justify-between space-x-6'>
|
||||
<div className='exclude-title-bar-mousedown sm:w relative flex w-full items-center pl-4'>
|
||||
<div className='flex items-center justify-between space-x-12'>
|
||||
<div className='exclude-title-bar-mousedown relative flex w-full items-center pl-4'>
|
||||
<span className='absolute left-8 text-gray-500'>
|
||||
<FaSearch className='h-4 w-4' />
|
||||
</span>
|
||||
@@ -97,20 +97,22 @@ const LibraryHeader: React.FC<LibraryHeaderProps> = ({
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div className='flex h-full items-center'>
|
||||
<div className='flex h-full items-center gap-x-4'>
|
||||
<Dropdown
|
||||
className='exclude-title-bar-mousedown dropdown-bottom dropdown-end mr-2'
|
||||
className='exclude-title-bar-mousedown dropdown-bottom dropdown-end'
|
||||
buttonClassName='btn btn-ghost h-8 min-h-8 w-8 p-0'
|
||||
toggleButton={<MdOutlineMenu size={16} />}
|
||||
>
|
||||
<SettingsMenu />
|
||||
</Dropdown>
|
||||
<WindowButtons
|
||||
headerRef={headerRef}
|
||||
showMinimize={!isTrafficLightVisible && appService?.appPlatform !== 'web'}
|
||||
showMaximize={!isTrafficLightVisible && appService?.appPlatform !== 'web'}
|
||||
showClose={!isTrafficLightVisible && appService?.appPlatform !== 'web'}
|
||||
/>
|
||||
{!isTrafficLightVisible && appService?.appPlatform !== 'web' && (
|
||||
<WindowButtons
|
||||
headerRef={headerRef}
|
||||
showMinimize={true}
|
||||
showMaximize={true}
|
||||
showClose={true}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -84,8 +84,12 @@ const HeaderBar: React.FC<HeaderBarProps> = ({
|
||||
<WindowButtons
|
||||
className='window-buttons flex h-full items-center'
|
||||
headerRef={headerRef}
|
||||
showMinimize={bookKeys.length == 1 && appService?.appPlatform !== 'web'}
|
||||
showMaximize={bookKeys.length == 1 && appService?.appPlatform !== 'web'}
|
||||
showMinimize={
|
||||
bookKeys.length == 1 && !isTrafficLightVisible && appService?.appPlatform !== 'web'
|
||||
}
|
||||
showMaximize={
|
||||
bookKeys.length == 1 && !isTrafficLightVisible && appService?.appPlatform !== 'web'
|
||||
}
|
||||
onClose={() => onCloseBook(bookKey)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -305,6 +305,7 @@ const Annotator: React.FC<{ bookKey: string }> = ({ bookKey }) => {
|
||||
if (!selection || !selection.text) return;
|
||||
const { sectionHref: href } = progress;
|
||||
selection.href = href;
|
||||
setShowAnnotPopup(false);
|
||||
setHighlightOptionsVisible(false);
|
||||
setNotebookVisible(true);
|
||||
setNotebookNewAnnotation(selection);
|
||||
|
||||
@@ -3,7 +3,7 @@ import React from 'react';
|
||||
|
||||
import { FiSearch } from 'react-icons/fi';
|
||||
import { LuNotebookPen } from 'react-icons/lu';
|
||||
import { MdOutlinePushPin, MdPushPin } from 'react-icons/md';
|
||||
import { MdArrowBackIosNew, MdOutlinePushPin, MdPushPin } from 'react-icons/md';
|
||||
import { useTranslation } from '@/hooks/useTranslation';
|
||||
|
||||
const NotebookHeader: React.FC<{
|
||||
@@ -18,7 +18,7 @@ const NotebookHeader: React.FC<{
|
||||
<LuNotebookPen size={14} />
|
||||
<div className='notebook-title text-sm font-medium'>{_('Notebook')}</div>
|
||||
</div>
|
||||
<div className='z-10 flex items-center gap-x-3'>
|
||||
<div className='z-10 flex items-center gap-x-4'>
|
||||
<button
|
||||
onClick={handleTogglePin}
|
||||
className={clsx(
|
||||
@@ -30,14 +30,9 @@ const NotebookHeader: React.FC<{
|
||||
</button>
|
||||
<button
|
||||
onClick={handleClose}
|
||||
className={'bg-base-300/65 btn btn-ghost btn-circle h-6 min-h-6 w-6 sm:hidden'}
|
||||
className={'btn btn-ghost btn-circle flex h-6 min-h-6 w-6 hover:bg-transparent sm:hidden'}
|
||||
>
|
||||
<svg xmlns='http://www.w3.org/2000/svg' width='1em' height='1em' viewBox='0 0 24 24'>
|
||||
<path
|
||||
fill='currentColor'
|
||||
d='M19 6.41L17.59 5L12 10.59L6.41 5L5 6.41L10.59 12L5 17.59L6.41 19L12 13.41L17.59 19L19 17.59L13.41 12z'
|
||||
/>
|
||||
</svg>
|
||||
<MdArrowBackIosNew size={20} />
|
||||
</button>
|
||||
<button className='btn btn-ghost left-0 h-8 min-h-8 w-8 p-0'>
|
||||
<FiSearch size={18} />
|
||||
|
||||
@@ -57,7 +57,7 @@ const FontDropdown: React.FC<DropdownProps> = ({
|
||||
tabIndex={0}
|
||||
className={clsx(
|
||||
'dropdown-content bgcolor-base-200 menu rounded-box relative z-[1] overflow-y-scroll shadow',
|
||||
'!mr-5 mb-[-46px] inline max-h-80 w-[250px] overflow-y-scroll',
|
||||
'!mr-5 mb-[-46px] inline max-h-80 w-[220px] overflow-y-scroll',
|
||||
)}
|
||||
>
|
||||
{moreOptions.map((option) => (
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import clsx from 'clsx';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import React, { useState } from 'react';
|
||||
import { BookConfig } from '@/types/book';
|
||||
import { useSettingsStore } from '@/store/settingsStore';
|
||||
import { useTranslation } from '@/hooks/useTranslation';
|
||||
@@ -8,12 +8,13 @@ import { RiDashboardLine } from 'react-icons/ri';
|
||||
import { VscSymbolColor } from 'react-icons/vsc';
|
||||
import { PiDotsThreeVerticalBold } from 'react-icons/pi';
|
||||
import { IoAccessibilityOutline } from 'react-icons/io5';
|
||||
import { MdArrowBackIosNew } from 'react-icons/md';
|
||||
|
||||
import FontPanel from './FontPanel';
|
||||
import LayoutPanel from './LayoutPanel';
|
||||
import ColorPanel from './ColorPanel';
|
||||
import WindowButtons from '@/components/WindowButtons';
|
||||
import Dropdown from '@/components/Dropdown';
|
||||
import Dialog from '@/components/Dialog';
|
||||
import DialogMenu from './DialogMenu';
|
||||
import MiscPanel from './MiscPanel';
|
||||
|
||||
@@ -30,20 +31,6 @@ const SettingsDialog: React.FC<{ bookKey: string; config: BookConfig }> = ({ boo
|
||||
const [activePanel, setActivePanel] = useState<SettingsPanelType>('Font');
|
||||
const { setFontLayoutSettingsDialogOpen } = useSettingsStore();
|
||||
|
||||
const handleKeyDown = (event: KeyboardEvent) => {
|
||||
if (event.key === 'Escape') {
|
||||
setFontLayoutSettingsDialogOpen(false);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
window.addEventListener('keydown', handleKeyDown);
|
||||
return () => {
|
||||
window.removeEventListener('keydown', handleKeyDown);
|
||||
};
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
const tabConfig = [
|
||||
{
|
||||
tab: 'Font',
|
||||
@@ -67,50 +54,77 @@ const SettingsDialog: React.FC<{ bookKey: string; config: BookConfig }> = ({ boo
|
||||
},
|
||||
] as TabConfig[];
|
||||
|
||||
return (
|
||||
<dialog className='modal modal-open sm:min-w-90 h-full w-full !bg-[rgba(0,0,0,0.2)] sm:w-full'>
|
||||
<div className='modal-box settings-content flex h-full max-h-full w-full max-w-full flex-col rounded-none p-0 sm:h-[65%] sm:w-1/2 sm:min-w-[480px] sm:max-w-full sm:rounded-2xl'>
|
||||
<div className='dialog-header bg-base-100 sticky top-0 z-10 flex items-center justify-center px-4 pt-2'>
|
||||
<div className='dialog-tabs flex h-10 max-w-[75%] flex-grow items-center justify-around sm:max-w-[80%]'>
|
||||
{tabConfig.map(({ tab, icon: Icon, label }) => (
|
||||
<button
|
||||
key={tab}
|
||||
className={clsx(
|
||||
'btn btn-ghost text-base-content h-8 min-h-8',
|
||||
activePanel === tab ? 'btn-active' : '',
|
||||
)}
|
||||
onClick={() => setActivePanel(tab)}
|
||||
>
|
||||
<Icon size={20} className='mr-0' />
|
||||
{window.innerWidth >= 500 ? label : ''}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
<div className='flex h-full items-center justify-end'>
|
||||
<Dropdown
|
||||
className='dropdown-bottom dropdown-end'
|
||||
buttonClassName='btn btn-ghost h-8 min-h-8 w-8 p-0'
|
||||
toggleButton={<PiDotsThreeVerticalBold size={16} />}
|
||||
>
|
||||
<DialogMenu />
|
||||
</Dropdown>
|
||||
<WindowButtons
|
||||
className='window-buttons absolute right-4 !ml-2 flex h-full items-center'
|
||||
showMinimize={false}
|
||||
showMaximize={false}
|
||||
onClose={() => setFontLayoutSettingsDialogOpen(false)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
const handleClose = () => {
|
||||
setFontLayoutSettingsDialogOpen(false);
|
||||
};
|
||||
|
||||
<div className='text-base-content mt-2 flex-grow overflow-y-auto px-[10%]'>
|
||||
{activePanel === 'Font' && <FontPanel bookKey={bookKey} />}
|
||||
{activePanel === 'Layout' && <LayoutPanel bookKey={bookKey} />}
|
||||
{activePanel === 'Color' && <ColorPanel bookKey={bookKey} />}
|
||||
{activePanel === 'Misc' && <MiscPanel bookKey={bookKey} />}
|
||||
</div>
|
||||
</div>
|
||||
</dialog>
|
||||
return (
|
||||
<>
|
||||
<Dialog
|
||||
isOpen={true}
|
||||
onClose={handleClose}
|
||||
className='modal-open'
|
||||
boxClassName='sm:w-1/2 sm:min-w-[480px]'
|
||||
header={
|
||||
<div className='flex w-full items-center justify-between'>
|
||||
<button
|
||||
tabIndex={-1}
|
||||
onClick={handleClose}
|
||||
className={
|
||||
'btn btn-ghost btn-circle flex h-6 min-h-6 w-6 hover:bg-transparent focus:outline-none sm:hidden'
|
||||
}
|
||||
>
|
||||
<MdArrowBackIosNew size={20} />
|
||||
</button>
|
||||
<div className='dialog-tabs flex h-10 max-w-[100%] flex-grow items-center justify-around pl-4'>
|
||||
{tabConfig.map(({ tab, icon: Icon, label }) => (
|
||||
<button
|
||||
key={tab}
|
||||
className={clsx(
|
||||
'btn btn-ghost text-base-content h-8 min-h-8',
|
||||
activePanel === tab ? 'btn-active' : '',
|
||||
)}
|
||||
onClick={() => setActivePanel(tab)}
|
||||
>
|
||||
<Icon size={20} className='mr-0' />
|
||||
{window.innerWidth >= 500 ? label : ''}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
<div className='flex h-full items-center justify-end gap-x-2'>
|
||||
<Dropdown
|
||||
className='dropdown-bottom dropdown-end'
|
||||
buttonClassName='btn btn-ghost h-8 min-h-8 w-8 p-0'
|
||||
toggleButton={<PiDotsThreeVerticalBold size={16} />}
|
||||
>
|
||||
<DialogMenu />
|
||||
</Dropdown>
|
||||
<button
|
||||
onClick={handleClose}
|
||||
className={'bg-base-300/65 btn btn-ghost btn-circle hidden h-6 min-h-6 w-6 sm:flex'}
|
||||
>
|
||||
<svg
|
||||
xmlns='http://www.w3.org/2000/svg'
|
||||
width='1em'
|
||||
height='1em'
|
||||
viewBox='0 0 24 24'
|
||||
>
|
||||
<path
|
||||
fill='currentColor'
|
||||
d='M19 6.41L17.59 5L12 10.59L6.41 5L5 6.41L10.59 12L5 17.59L6.41 19L12 13.41L17.59 19L19 17.59L13.41 12z'
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
{activePanel === 'Font' && <FontPanel bookKey={bookKey} />}
|
||||
{activePanel === 'Layout' && <LayoutPanel bookKey={bookKey} />}
|
||||
{activePanel === 'Color' && <ColorPanel bookKey={bookKey} />}
|
||||
{activePanel === 'Misc' && <MiscPanel bookKey={bookKey} />}
|
||||
</Dialog>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@ import React from 'react';
|
||||
import { GiBookshelf } from 'react-icons/gi';
|
||||
import { FiSearch } from 'react-icons/fi';
|
||||
import { MdOutlineMenu, MdOutlinePushPin, MdPushPin } from 'react-icons/md';
|
||||
import { MdArrowBackIosNew } from 'react-icons/md';
|
||||
|
||||
import useTrafficLight from '@/hooks/useTrafficLight';
|
||||
import Dropdown from '@/components/Dropdown';
|
||||
@@ -24,12 +25,18 @@ const SidebarHeader: React.FC<{
|
||||
isTrafficLightVisible ? 'pl-20' : 'pl-1.5',
|
||||
)}
|
||||
>
|
||||
<div className='flex items-center'>
|
||||
<div className='flex items-center gap-x-4'>
|
||||
<button
|
||||
onClick={onClose}
|
||||
className={'btn btn-ghost btn-circle flex h-6 min-h-6 w-6 hover:bg-transparent sm:hidden'}
|
||||
>
|
||||
<MdArrowBackIosNew size={20} />
|
||||
</button>
|
||||
<button className='btn btn-ghost h-8 min-h-8 w-8 p-0' onClick={onGoToLibrary}>
|
||||
<GiBookshelf size={20} className='fill-base-content' />
|
||||
</button>
|
||||
</div>
|
||||
<div className='flex size-[70%] min-w-24 max-w-32 items-center justify-between'>
|
||||
<div className='flex min-w-24 max-w-32 items-center justify-between sm:size-[70%]'>
|
||||
<button
|
||||
onClick={onToggleSearchBar}
|
||||
className={clsx(
|
||||
@@ -50,7 +57,7 @@ const SidebarHeader: React.FC<{
|
||||
>
|
||||
<BookMenu />
|
||||
</Dropdown>
|
||||
<div className='right-0 flex h-8 w-8 items-center justify-center'>
|
||||
<div className='right-0 hidden h-8 w-8 items-center justify-center sm:flex'>
|
||||
<button
|
||||
onClick={onTogglePin}
|
||||
className={clsx(
|
||||
@@ -60,17 +67,6 @@ const SidebarHeader: React.FC<{
|
||||
>
|
||||
{isPinned ? <MdPushPin size={14} /> : <MdOutlinePushPin size={14} />}
|
||||
</button>
|
||||
<button
|
||||
onClick={onClose}
|
||||
className={'bg-base-300/65 btn btn-ghost btn-circle h-6 min-h-6 w-6 sm:hidden'}
|
||||
>
|
||||
<svg xmlns='http://www.w3.org/2000/svg' width='1em' height='1em' viewBox='0 0 24 24'>
|
||||
<path
|
||||
fill='currentColor'
|
||||
d='M19 6.41L17.59 5L12 10.59L6.41 5L5 6.41L10.59 12L5 17.59L6.41 19L12 13.41L17.59 19L19 17.59L13.41 12z'
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -46,6 +46,12 @@ export const useClickEvent = (
|
||||
} else if (deltaY < 0) {
|
||||
viewRef.current?.prev(1);
|
||||
}
|
||||
} else if (msg.data.type === 'iframe-mouseup') {
|
||||
if (msg.data.button === 3) {
|
||||
viewRef.current?.history.back();
|
||||
} else if (msg.data.button === 4) {
|
||||
viewRef.current?.history.forward();
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
@@ -115,14 +121,18 @@ export const useTouchEvent = (
|
||||
const touch = e.targetTouches[0];
|
||||
if (touch) {
|
||||
touchEnd = touch;
|
||||
};
|
||||
}
|
||||
|
||||
const windowWidth = window.innerWidth;
|
||||
if (touchEnd) {
|
||||
const deltaY = touchEnd.screenY - touchStart.screenY;
|
||||
const deltaX = touchEnd.screenX - touchStart.screenX;
|
||||
// also check for deltaX to prevent swipe page turn from triggering the toggle
|
||||
if (deltaY < -10 && Math.abs(deltaY) > Math.abs(deltaX) && Math.abs(deltaX) < windowWidth * 0.3) {
|
||||
if (
|
||||
deltaY < -10 &&
|
||||
Math.abs(deltaY) > Math.abs(deltaX) &&
|
||||
Math.abs(deltaX) < windowWidth * 0.3
|
||||
) {
|
||||
// swipe up to toggle the header bar and the footer bar
|
||||
setHoveredBookKey(hoveredBookKey ? null : bookKey);
|
||||
}
|
||||
@@ -151,4 +161,4 @@ export const useTouchEvent = (
|
||||
};
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [hoveredBookKey, viewRef]);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -30,6 +30,7 @@ export const handleMousedown = (bookKey: string, event: MouseEvent) => {
|
||||
{
|
||||
type: 'iframe-mousedown',
|
||||
bookKey,
|
||||
button: event.button,
|
||||
screenX: event.screenX,
|
||||
screenY: event.screenY,
|
||||
clientX: event.clientX,
|
||||
@@ -46,6 +47,7 @@ export const handleMouseup = (bookKey: string, event: MouseEvent) => {
|
||||
{
|
||||
type: 'iframe-mouseup',
|
||||
bookKey,
|
||||
button: event.button,
|
||||
screenX: event.screenX,
|
||||
screenY: event.screenY,
|
||||
clientX: event.clientX,
|
||||
@@ -150,7 +152,7 @@ const handleTouchEv = (bookKey: string, event: TouchEvent, type: string) => {
|
||||
},
|
||||
'*',
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
export const handleTouchStart = (bookKey: string, event: TouchEvent) => {
|
||||
handleTouchEv(bookKey, event, 'iframe-touchstart');
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import React from 'react';
|
||||
import Image from 'next/image';
|
||||
import packageJson from '../../package.json';
|
||||
import WindowButtons from './WindowButtons';
|
||||
import { useTranslation } from '@/hooks/useTranslation';
|
||||
import { hasUpdater } from '@/services/environment';
|
||||
import { checkForAppUpdates } from '@/helpers/updater';
|
||||
import Dialog from './Dialog';
|
||||
|
||||
export const setAboutDialogVisible = (visible: boolean) => {
|
||||
const dialog = document.getElementById('about_window');
|
||||
@@ -27,67 +27,67 @@ export const AboutWindow = () => {
|
||||
};
|
||||
|
||||
return (
|
||||
<dialog id='about_window' className='modal'>
|
||||
<form method='dialog' className='modal-box w-96 max-w-lg p-4'>
|
||||
<div className='dialog-header bg-base-100 sticky top-0 z-10 flex items-center justify-center p-2'>
|
||||
<WindowButtons
|
||||
className='window-buttons absolute right-0 flex h-full items-center'
|
||||
showMinimize={false}
|
||||
showMaximize={false}
|
||||
onClose={() => setAboutDialogVisible(false)}
|
||||
/>
|
||||
</div>
|
||||
<div className='flex flex-col items-center px-8'>
|
||||
<div className='mb-4'>
|
||||
<Image src='/icon.png' alt='App Logo' className='h-24 w-24' width={64} height={64} />
|
||||
<>
|
||||
<Dialog
|
||||
id='about_window'
|
||||
isOpen={false}
|
||||
title={_('About Readest')}
|
||||
onClose={() => setAboutDialogVisible(false)}
|
||||
boxClassName='sm:!w-96'
|
||||
>
|
||||
<div className='about-content flex h-full flex-col items-center justify-center'>
|
||||
<div className='flex flex-col items-center px-8'>
|
||||
<div className='mb-4'>
|
||||
<Image src='/icon.png' alt='App Logo' className='h-24 w-24' width={64} height={64} />
|
||||
</div>
|
||||
<h2 className='text-2xl font-bold'>Readest</h2>
|
||||
<p className='text-neutral-content text-sm'>
|
||||
{_('Version {{version}}', { version: packageJson.version })}
|
||||
</p>
|
||||
{hasUpdater() && !isUpdated && (
|
||||
<span className='badge badge-primary mt-2' onClick={handleCheckUpdate}>
|
||||
{_('Check update')}
|
||||
</span>
|
||||
)}
|
||||
{isUpdated && (
|
||||
<p className='text-neutral-content mt-2 text-xs'>{_('Already the latest version')}</p>
|
||||
)}
|
||||
</div>
|
||||
<h2 className='text-2xl font-bold'>Readest</h2>
|
||||
<p className='text-neutral-content text-sm'>
|
||||
{_('Version {{version}}', { version: packageJson.version })}
|
||||
</p>
|
||||
{hasUpdater() && !isUpdated && (
|
||||
<span className='badge badge-primary mt-2' onClick={handleCheckUpdate}>
|
||||
{_('Check update')}
|
||||
</span>
|
||||
)}
|
||||
{isUpdated && (
|
||||
<p className='text-neutral-content mt-2 text-xs'>{_('Already the latest version')}</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className='divider'></div>
|
||||
<div className='divider py-12 sm:py-2'></div>
|
||||
|
||||
<div className='flex flex-col items-center px-4 text-center'>
|
||||
<p className='text-neutral-content text-sm'>
|
||||
© {new Date().getFullYear()} Bilingify LLC. All rights reserved.
|
||||
</p>
|
||||
<p className='text-neutral-content mt-2 text-xs'>
|
||||
This software is licensed under the{' '}
|
||||
<a
|
||||
href='https://www.gnu.org/licenses/agpl-3.0.html'
|
||||
target='_blank'
|
||||
rel='noopener noreferrer'
|
||||
className='text-blue-500 underline'
|
||||
>
|
||||
GNU Affero General Public License v3.0
|
||||
</a>
|
||||
. You are free to use, modify, and distribute this software under the terms of the AGPL
|
||||
v3 license. Please see the license for more details.
|
||||
</p>
|
||||
<p className='text-neutral-content mt-2 text-xs'>
|
||||
Source code is available at{' '}
|
||||
<a
|
||||
href='https://github.com/readest/readest'
|
||||
target='_blank'
|
||||
rel='noopener noreferrer'
|
||||
className='text-blue-500 underline'
|
||||
>
|
||||
GitHub
|
||||
</a>
|
||||
.
|
||||
</p>
|
||||
<div className='flex flex-col items-center px-4 text-center'>
|
||||
<p className='text-neutral-content text-sm'>
|
||||
© {new Date().getFullYear()} Bilingify LLC. All rights reserved.
|
||||
</p>
|
||||
<p className='text-neutral-content mt-2 text-xs'>
|
||||
This software is licensed under the{' '}
|
||||
<a
|
||||
href='https://www.gnu.org/licenses/agpl-3.0.html'
|
||||
target='_blank'
|
||||
rel='noopener noreferrer'
|
||||
className='text-blue-500 underline'
|
||||
>
|
||||
GNU Affero General Public License v3.0
|
||||
</a>
|
||||
. You are free to use, modify, and distribute this software under the terms of the
|
||||
AGPL v3 license. Please see the license for more details.
|
||||
</p>
|
||||
<p className='text-neutral-content mt-2 text-xs'>
|
||||
Source code is available at{' '}
|
||||
<a
|
||||
href='https://github.com/readest/readest'
|
||||
target='_blank'
|
||||
rel='noopener noreferrer'
|
||||
className='text-blue-500 underline'
|
||||
>
|
||||
GitHub
|
||||
</a>
|
||||
.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</dialog>
|
||||
</Dialog>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -13,7 +13,7 @@ const Alert: React.FC<{
|
||||
<div
|
||||
role='alert'
|
||||
className={clsx(
|
||||
'alert fixed bottom-4 left-1/2 flex -translate-x-1/2 transform items-center justify-between',
|
||||
'alert fixed bottom-4 left-1/2 z-[100] flex -translate-x-1/2 transform items-center justify-between',
|
||||
'rounded-lg bg-gray-100 p-4 shadow-2xl',
|
||||
'w-full max-w-[90vw] sm:max-w-[70vw] md:max-w-[50vw] lg:max-w-[40vw] xl:max-w-[40vw]',
|
||||
)}
|
||||
|
||||
@@ -2,7 +2,6 @@ import clsx from 'clsx';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import Image from 'next/image';
|
||||
|
||||
import Alert from '@/components/Alert';
|
||||
import { Book } from '@/types/book';
|
||||
import { BookDoc } from '@/libs/document';
|
||||
import { useEnv } from '@/context/EnvContext';
|
||||
@@ -10,8 +9,9 @@ import { useSettingsStore } from '@/store/settingsStore';
|
||||
import { useTranslation } from '@/hooks/useTranslation';
|
||||
import { useLibraryStore } from '@/store/libraryStore';
|
||||
import { formatDate, formatLanguage, formatPublisher, formatSubject } from '@/utils/book';
|
||||
import WindowButtons from '@/components/WindowButtons';
|
||||
import Alert from '@/components/Alert';
|
||||
import Spinner from './Spinner';
|
||||
import Dialog from './Dialog';
|
||||
|
||||
interface BookDetailModalProps {
|
||||
book: Book;
|
||||
@@ -28,21 +28,6 @@ const BookDetailModal = ({ book, isOpen, onClose }: BookDetailModalProps) => {
|
||||
const { settings } = useSettingsStore();
|
||||
const { deleteBook } = useLibraryStore();
|
||||
|
||||
useEffect(() => {
|
||||
if (!isOpen) return;
|
||||
|
||||
const handleKeyDown = (event: KeyboardEvent) => {
|
||||
if (event.key === 'Escape') {
|
||||
onClose();
|
||||
}
|
||||
};
|
||||
|
||||
window.addEventListener('keydown', handleKeyDown);
|
||||
return () => {
|
||||
window.removeEventListener('keydown', handleKeyDown);
|
||||
};
|
||||
}, [isOpen, onClose]);
|
||||
|
||||
useEffect(() => {
|
||||
const loadingTimeout = setTimeout(() => setLoading(true), 300);
|
||||
const fetchBookDetails = async () => {
|
||||
@@ -63,7 +48,7 @@ const BookDetailModal = ({ book, isOpen, onClose }: BookDetailModalProps) => {
|
||||
|
||||
const handleDelete = () => {
|
||||
setShowDeleteAlert(true);
|
||||
}
|
||||
};
|
||||
|
||||
const confirmDelete = () => {
|
||||
deleteBook(envConfig, book);
|
||||
@@ -71,8 +56,6 @@ const BookDetailModal = ({ book, isOpen, onClose }: BookDetailModalProps) => {
|
||||
setShowDeleteAlert(false);
|
||||
};
|
||||
|
||||
if (!isOpen) return null;
|
||||
|
||||
if (!bookMeta)
|
||||
return (
|
||||
loading && (
|
||||
@@ -83,113 +66,132 @@ const BookDetailModal = ({ book, isOpen, onClose }: BookDetailModalProps) => {
|
||||
);
|
||||
|
||||
return (
|
||||
<div className='fixed inset-0 z-50 flex w-full select-text items-center justify-center'>
|
||||
<div className='min-w-[480px] max-w-md'>
|
||||
<div className='fixed inset-0 bg-gray-800 bg-opacity-70' onClick={handleClose} />
|
||||
<>
|
||||
<Dialog
|
||||
title={_('Book Details')}
|
||||
isOpen={isOpen}
|
||||
onClose={handleClose}
|
||||
boxClassName='sm:min-w-[480px]'
|
||||
contentClassName='!p-6'
|
||||
>
|
||||
<div className='z-50 flex w-full select-text items-center justify-center'>
|
||||
<div className='relative w-full rounded-lg'>
|
||||
<div className='mb-10 flex h-40 items-start'>
|
||||
<div className='book-cover relative mr-10 aspect-[28/41] h-40 items-end shadow-lg'>
|
||||
<Image
|
||||
src={book.coverImageUrl!}
|
||||
alt={book.title}
|
||||
fill={true}
|
||||
className='w-10 object-cover'
|
||||
onError={(e) => {
|
||||
(e.target as HTMLImageElement).style.display = 'none';
|
||||
(e.target as HTMLImageElement).nextElementSibling?.classList.remove(
|
||||
'invisible',
|
||||
);
|
||||
}}
|
||||
/>
|
||||
<div
|
||||
className={clsx(
|
||||
'invisible absolute inset-0 flex items-center justify-center p-1',
|
||||
'text-neutral-content rounded-none text-center font-serif text-base font-medium',
|
||||
)}
|
||||
>
|
||||
{book.title}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className='bg-base-200 relative z-50 w-full rounded-lg p-6 shadow-xl'>
|
||||
<div className='absolute right-4 top-4 flex space-x-2'>
|
||||
<WindowButtons
|
||||
className='window-buttons flex'
|
||||
showMinimize={false}
|
||||
showMaximize={false}
|
||||
onClose={handleClose}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className='mb-6 flex h-40 items-start'>
|
||||
<div className='book-cover relative mr-10 aspect-[28/41] h-40 items-end shadow-lg'>
|
||||
<Image
|
||||
src={book.coverImageUrl!}
|
||||
alt={book.title}
|
||||
fill={true}
|
||||
className='w-10 object-cover'
|
||||
onError={(e) => {
|
||||
(e.target as HTMLImageElement).style.display = 'none';
|
||||
(e.target as HTMLImageElement).nextElementSibling?.classList.remove('invisible');
|
||||
}}
|
||||
/>
|
||||
<div
|
||||
className={clsx(
|
||||
'invisible absolute inset-0 flex items-center justify-center p-1',
|
||||
'text-neutral-content rounded-none text-center font-serif text-base font-medium',
|
||||
<div className='title-author flex h-40 flex-col justify-between'>
|
||||
<div>
|
||||
<p className='text-base-content mb-2 line-clamp-2 break-all text-2xl font-bold'>
|
||||
{book.title || _('Untitled')}
|
||||
</p>
|
||||
<p className='text-neutral-content line-clamp-1'>{book.author || _('Unknown')}</p>
|
||||
</div>
|
||||
{window.innerWidth >= 400 && (
|
||||
<div className='flex flex-wrap items-center gap-x-4 gap-y-2 py-2'>
|
||||
<button
|
||||
className='btn rounded bg-red-600 text-white hover:bg-red-700'
|
||||
onClick={handleDelete}
|
||||
>
|
||||
{_('Delete')}
|
||||
</button>
|
||||
<button className='btn btn-disabled bg-primary/25 hover:bg-primary/85 rounded px-4 text-white'>
|
||||
{_('More Info')}
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
>
|
||||
{book.title}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className='title-author flex h-40 max-w-[60%] flex-col justify-between pr-4'>
|
||||
<div>
|
||||
<h2 className='text-base-content mb-2 line-clamp-2 break-all text-2xl font-bold'>
|
||||
{book.title || _('Untitled')}
|
||||
</h2>
|
||||
<p className='text-neutral-content line-clamp-1'>{book.author || _('Unknown')}</p>
|
||||
{window.innerWidth < 400 && (
|
||||
<div className='flex flex-wrap items-center gap-x-4 gap-y-2 py-2'>
|
||||
<button
|
||||
className='btn rounded bg-red-600 text-white hover:bg-red-700'
|
||||
onClick={handleDelete}
|
||||
>
|
||||
{_('Delete')}
|
||||
</button>
|
||||
<button className='btn btn-disabled bg-primary/25 hover:bg-primary/85 rounded px-4 text-white'>
|
||||
{_('More Info')}
|
||||
</button>
|
||||
</div>
|
||||
<button
|
||||
className='w-36 rounded bg-red-600 px-4 py-2 text-white hover:bg-red-700'
|
||||
onClick={handleDelete}
|
||||
>
|
||||
{_('Delete Book')}
|
||||
</button>
|
||||
<button className='btn-disabled bg-primary/25 hover:bg-primary/85 w-36 rounded px-4 py-2 text-white'>
|
||||
{_('More Info')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className='text-base-content mb-4'>
|
||||
<div className='mb-4 grid grid-cols-3 gap-4'>
|
||||
<div className='overflow-hidden'>
|
||||
<span className='font-bold'>{_('Publisher:')}</span>
|
||||
<p className='text-neutral-content line-clamp-1 text-sm'>
|
||||
{formatPublisher(bookMeta.publisher || '') || _('Unknown')}
|
||||
</p>
|
||||
</div>
|
||||
<div className='overflow-hidden'>
|
||||
<span className='font-bold'>{_('Published:')}</span>
|
||||
<p className='text-neutral-content max-w-28 text-ellipsis text-sm'>
|
||||
{formatDate(bookMeta.published) || _('Unknown')}
|
||||
</p>
|
||||
</div>
|
||||
<div className='overflow-hidden'>
|
||||
<span className='font-bold'>{_('Updated:')}</span>
|
||||
<p className='text-neutral-content text-sm'>{formatDate(book.lastUpdated) || ''}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className='text-base-content my-4'>
|
||||
<div className='mb-4 grid grid-cols-2 gap-4 sm:grid-cols-3'>
|
||||
<div className='overflow-hidden'>
|
||||
<span className='font-bold'>{_('Publisher:')}</span>
|
||||
<p className='text-neutral-content line-clamp-1 text-sm'>
|
||||
{formatPublisher(bookMeta.publisher || '') || _('Unknown')}
|
||||
</p>
|
||||
</div>
|
||||
<div className='overflow-hidden'>
|
||||
<span className='font-bold'>{_('Published:')}</span>
|
||||
<p className='text-neutral-content max-w-28 text-ellipsis text-sm'>
|
||||
{formatDate(bookMeta.published) || _('Unknown')}
|
||||
</p>
|
||||
</div>
|
||||
<div className='overflow-hidden'>
|
||||
<span className='font-bold'>{_('Updated:')}</span>
|
||||
<p className='text-neutral-content text-sm'>
|
||||
{formatDate(book.lastUpdated) || ''}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className='grid grid-cols-3 gap-4'>
|
||||
<div className='overflow-hidden'>
|
||||
<span className='font-bold'>{_('Language:')}</span>
|
||||
<p className='text-neutral-content text-sm'>
|
||||
{formatLanguage(bookMeta.language) || _('Unknown')}
|
||||
</p>
|
||||
</div>
|
||||
<div className='overflow-hidden'>
|
||||
<span className='font-bold'>{_('Identifier:')}</span>
|
||||
<p className='text-neutral-content line-clamp-1 text-sm'>
|
||||
{bookMeta.identifier || 'N/A'}
|
||||
</p>
|
||||
</div>
|
||||
<div className='overflow-hidden'>
|
||||
<span className='font-bold'>{_('Subjects:')}</span>
|
||||
<p className='text-neutral-content line-clamp-1 text-sm'>
|
||||
{formatSubject(bookMeta.subject) || _('Unknown')}
|
||||
</p>
|
||||
<div className='overflow-hidden'>
|
||||
<span className='font-bold'>{_('Language:')}</span>
|
||||
<p className='text-neutral-content text-sm'>
|
||||
{formatLanguage(bookMeta.language) || _('Unknown')}
|
||||
</p>
|
||||
</div>
|
||||
<div className='overflow-hidden'>
|
||||
<span className='font-bold'>{_('Identifier:')}</span>
|
||||
<p className='text-neutral-content line-clamp-1 text-sm'>
|
||||
{bookMeta.identifier || 'N/A'}
|
||||
</p>
|
||||
</div>
|
||||
<div className='overflow-hidden'>
|
||||
<span className='font-bold'>{_('Subjects:')}</span>
|
||||
<p className='text-neutral-content line-clamp-1 text-sm'>
|
||||
{formatSubject(bookMeta.subject) || _('Unknown')}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Dialog>
|
||||
{showDeleteAlert && (
|
||||
<Alert
|
||||
title={_('Confirm Deletion')}
|
||||
message={_('Are you sure to delete the selected books?')}
|
||||
onClickCancel={() => { setShowDeleteAlert(false); handleClose(); }}
|
||||
onClickCancel={() => {
|
||||
setShowDeleteAlert(false);
|
||||
}}
|
||||
onClickConfirm={confirmDelete}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -0,0 +1,111 @@
|
||||
import clsx from 'clsx';
|
||||
import React, { ReactNode, useEffect } from 'react';
|
||||
import { MdArrowBackIosNew } from 'react-icons/md';
|
||||
|
||||
interface DialogProps {
|
||||
id?: string;
|
||||
isOpen: boolean;
|
||||
children: ReactNode;
|
||||
header?: ReactNode;
|
||||
title?: string;
|
||||
className?: string;
|
||||
boxClassName?: string;
|
||||
contentClassName?: string;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
const Dialog: React.FC<DialogProps> = ({
|
||||
id,
|
||||
isOpen,
|
||||
children,
|
||||
header,
|
||||
title,
|
||||
className,
|
||||
boxClassName,
|
||||
contentClassName,
|
||||
onClose,
|
||||
}) => {
|
||||
const handleKeyDown = (event: KeyboardEvent) => {
|
||||
if (event.key === 'Escape') {
|
||||
onClose();
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
window.addEventListener('keydown', handleKeyDown);
|
||||
return () => {
|
||||
window.removeEventListener('keydown', handleKeyDown);
|
||||
};
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<dialog
|
||||
id={id ?? 'dialog'}
|
||||
open={isOpen}
|
||||
className={clsx(
|
||||
'modal sm:min-w-90 z-50 h-full w-full !bg-[rgba(0,0,0,0.2)] sm:w-full',
|
||||
className,
|
||||
)}
|
||||
>
|
||||
<div
|
||||
className={clsx(
|
||||
'modal-box settings-content flex h-full max-h-full w-full max-w-full flex-col rounded-none p-0 sm:rounded-2xl',
|
||||
'sm:h-[60%] sm:w-[65%] sm:max-w-[600px]',
|
||||
boxClassName,
|
||||
)}
|
||||
>
|
||||
<div className='dialog-header bg-base-100 sticky top-2 z-10 flex items-center justify-between px-4'>
|
||||
{header ? (
|
||||
header
|
||||
) : (
|
||||
<div className='flex h-11 w-full items-center justify-between'>
|
||||
<button
|
||||
tabIndex={-1}
|
||||
onClick={onClose}
|
||||
className={
|
||||
'btn btn-ghost btn-circle flex h-6 min-h-6 w-6 hover:bg-transparent focus:outline-none sm:hidden'
|
||||
}
|
||||
>
|
||||
<MdArrowBackIosNew size={20} />
|
||||
</button>
|
||||
<div className='z-15 pointer-events-none absolute inset-0 flex h-11 items-center justify-center'>
|
||||
<span className='line-clamp-1 text-center font-bold'>{title ?? ''}</span>
|
||||
</div>
|
||||
<button
|
||||
tabIndex={-1}
|
||||
onClick={onClose}
|
||||
className={
|
||||
'bg-base-300/65 btn btn-ghost btn-circle ml-auto hidden h-6 min-h-6 w-6 focus:outline-none sm:flex'
|
||||
}
|
||||
>
|
||||
<svg
|
||||
xmlns='http://www.w3.org/2000/svg'
|
||||
width='1em'
|
||||
height='1em'
|
||||
viewBox='0 0 24 24'
|
||||
>
|
||||
<path
|
||||
fill='currentColor'
|
||||
d='M19 6.41L17.59 5L12 10.59L6.41 5L5 6.41L10.59 12L5 17.59L6.41 19L12 13.41L17.59 19L19 17.59L13.41 12z'
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div
|
||||
className={clsx(
|
||||
'text-base-content my-2 flex-grow overflow-y-auto px-[10%]',
|
||||
contentClassName,
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
</dialog>
|
||||
);
|
||||
};
|
||||
|
||||
export default Dialog;
|
||||
Reference in New Issue
Block a user