diff --git a/apps/zbugs/src/components/emoji-data-source.ts b/apps/zbugs/src/components/emoji-data-source.ts new file mode 100644 index 000000000..6e589521c --- /dev/null +++ b/apps/zbugs/src/components/emoji-data-source.ts @@ -0,0 +1,3 @@ +import emojiDataSource from 'emoji-picker-element-data/en/emojibase/data.json?url'; + +export {emojiDataSource}; diff --git a/apps/zbugs/src/components/emoji-picker.tsx b/apps/zbugs/src/components/emoji-picker.tsx index 264ae7a45..dcecc77e4 100644 --- a/apps/zbugs/src/components/emoji-picker.tsx +++ b/apps/zbugs/src/components/emoji-picker.tsx @@ -1,5 +1,4 @@ import 'emoji-picker-element'; -import dataSource from 'emoji-picker-element-data/en/emojibase/data.json?url'; import Database from 'emoji-picker-element/database.js'; import type Picker from 'emoji-picker-element/picker.js'; import type { @@ -7,9 +6,10 @@ import type { NativeEmoji, SkinToneChangeEvent, } from 'emoji-picker-element/shared.js'; -import {createElement, memo, useEffect, useRef, type RefCallback} from 'react'; +import {createElement, memo, useRef, type RefCallback} from 'react'; import {setUserPref, useUserPref} from '../hooks/use-user-pref.js'; import {useZero} from '../hooks/use-zero.js'; +import {emojiDataSource} from './emoji-data-source.js'; export const SKIN_TONE_PREF = 'emojiSkinTone'; @@ -28,7 +28,7 @@ export const EmojiPicker = memo(({onEmojiChange}: Props) => { if (skinTonePref !== undefined) { const v = parseInt(skinTonePref, 10); if (!isNaN(v)) { - const db = new Database({dataSource}); + const db = new Database({dataSource: emojiDataSource}); db.setPreferredSkinTone(v).catch(err => { console.error('Failed to set preferred skin tone:', err); }); @@ -101,20 +101,6 @@ export const EmojiPicker = memo(({onEmojiChange}: Props) => { return createElement('emoji-picker', { 'class': 'dark', ref, - 'data-source': dataSource, + 'data-source': emojiDataSource, }); }); - -// eslint-disable-next-line react-refresh/only-export-components -export function useEmojiDataSourcePreload() { - useEffect(() => { - // Do this on a timer to not compete with other work. - const timer = setTimeout( - () => { - new Database({dataSource}); - }, - 1000 + Math.random() * 1000, - ); - return () => clearTimeout(timer); - }, []); -} diff --git a/apps/zbugs/src/components/filter.tsx b/apps/zbugs/src/components/filter.tsx index 07af62ac5..18c95dc0c 100644 --- a/apps/zbugs/src/components/filter.tsx +++ b/apps/zbugs/src/components/filter.tsx @@ -5,7 +5,7 @@ import labelIcon from '../assets/icons/label.svg'; import {useZero} from '../hooks/use-zero.js'; import {Button} from './button.js'; import {Combobox} from './combobox.js'; -import UserPicker from './user-picker.js'; +import {UserPicker} from './user-picker.js'; export type Selection = | {creator: string} diff --git a/apps/zbugs/src/components/issue-link.tsx b/apps/zbugs/src/components/issue-link.tsx index b41285b25..7867cccda 100644 --- a/apps/zbugs/src/components/issue-link.tsx +++ b/apps/zbugs/src/components/issue-link.tsx @@ -1,7 +1,7 @@ import {links, type ListContext} from '../routes.js'; import {Link, type Props as LinkProps} from './link.js'; -export default function IssueLink({ +export function IssueLink({ issue, title, children, diff --git a/apps/zbugs/src/components/label-picker.tsx b/apps/zbugs/src/components/label-picker.tsx index da5fa1527..ef1b583af 100644 --- a/apps/zbugs/src/components/label-picker.tsx +++ b/apps/zbugs/src/components/label-picker.tsx @@ -12,7 +12,7 @@ const focusInput = (input: HTMLInputElement | null) => { } }; -export default function LabelPicker({ +export function LabelPicker({ selected, onDisassociateLabel, onAssociateLabel, diff --git a/apps/zbugs/src/components/login-provider.tsx b/apps/zbugs/src/components/login-provider.tsx new file mode 100644 index 000000000..865fab8fd --- /dev/null +++ b/apps/zbugs/src/components/login-provider.tsx @@ -0,0 +1,25 @@ +import {useCallback, useSyncExternalStore} from 'react'; +import {loginContext} from '../hooks/use-login.js'; +import {clearJwt} from '../jwt.js'; +import {authRef} from '../zero-setup.js'; + +export function LoginProvider({children}: {children: React.ReactNode}) { + const loginState = useSyncExternalStore( + authRef.onChange, + useCallback(() => authRef.value, []), + ); + + return ( + { + clearJwt(); + authRef.value = undefined; + }, + loginState, + }} + > + {children} + + ); +} diff --git a/apps/zbugs/src/components/markdown.tsx b/apps/zbugs/src/components/markdown.tsx index 44b88e953..5099ff437 100644 --- a/apps/zbugs/src/components/markdown.tsx +++ b/apps/zbugs/src/components/markdown.tsx @@ -121,7 +121,7 @@ const rehypeImageToVideo: Plugin = () => { }; }; -const Markdown = memo(({children}: {children: string}) => { +export const Markdown = memo(({children}: {children: string}) => { return ( { ); }); - -export default Markdown; diff --git a/apps/zbugs/src/components/nav.tsx b/apps/zbugs/src/components/nav.tsx index 95066ec26..2415cdf9b 100644 --- a/apps/zbugs/src/components/nav.tsx +++ b/apps/zbugs/src/components/nav.tsx @@ -8,7 +8,7 @@ import logoURL from '../assets/images/logo.svg'; import markURL from '../assets/images/mark.svg'; import {useLogin} from '../hooks/use-login.js'; import {useZero} from '../hooks/use-zero.js'; -import IssueComposer from '../pages/issue/issue-composer.js'; +import {IssueComposer} from '../pages/issue/issue-composer.js'; import { links, routes, diff --git a/apps/zbugs/src/components/relative-time.tsx b/apps/zbugs/src/components/relative-time.tsx index b73e782ac..b128add8f 100644 --- a/apps/zbugs/src/components/relative-time.tsx +++ b/apps/zbugs/src/components/relative-time.tsx @@ -12,7 +12,7 @@ interface Props { }; } -function RelativeTime({timestamp, absolute = false, format}: Props) { +export function RelativeTime({timestamp, absolute = false, format}: Props) { const now = useNow(); const fullTimestamp = fullTimestampFormat.format(timestamp); return ( @@ -22,8 +22,6 @@ function RelativeTime({timestamp, absolute = false, format}: Props) { ); } -export default RelativeTime; - const fullTimestampFormat = Intl.DateTimeFormat('en-US', { year: 'numeric', month: 'short', diff --git a/apps/zbugs/src/components/user-picker.tsx b/apps/zbugs/src/components/user-picker.tsx index 500b5ff28..05c5dbaa6 100644 --- a/apps/zbugs/src/components/user-picker.tsx +++ b/apps/zbugs/src/components/user-picker.tsx @@ -18,7 +18,7 @@ type Props = { type User = Row; -export default function UserPicker({ +export function UserPicker({ onSelect, selected, disabled, diff --git a/apps/zbugs/src/hooks/use-emoji-data-source-preload.ts b/apps/zbugs/src/hooks/use-emoji-data-source-preload.ts new file mode 100644 index 000000000..667f35822 --- /dev/null +++ b/apps/zbugs/src/hooks/use-emoji-data-source-preload.ts @@ -0,0 +1,16 @@ +import Database from 'emoji-picker-element/database.js'; +import {useEffect} from 'react'; +import {emojiDataSource} from '../components/emoji-data-source.js'; + +export function useEmojiDataSourcePreload() { + useEffect(() => { + // Do this on a timer to not compete with other work. + const timer = setTimeout( + () => { + new Database({dataSource: emojiDataSource}); + }, + 1000 + Math.random() * 1000, + ); + return () => clearTimeout(timer); + }, []); +} diff --git a/apps/zbugs/src/hooks/use-is-scrolling.ts b/apps/zbugs/src/hooks/use-is-scrolling.ts index 839ce658d..04d7d480e 100644 --- a/apps/zbugs/src/hooks/use-is-scrolling.ts +++ b/apps/zbugs/src/hooks/use-is-scrolling.ts @@ -1,6 +1,6 @@ -import {useState, useEffect} from 'react'; +import {useEffect, useState} from 'react'; -const useIsScrolling = () => { +export function useIsScrolling() { const [isScrolling, setIsScrolling] = useState(false); useEffect(() => { @@ -29,6 +29,4 @@ const useIsScrolling = () => { }, []); return isScrolling; -}; - -export default useIsScrolling; +} diff --git a/apps/zbugs/src/hooks/use-login.tsx b/apps/zbugs/src/hooks/use-login.tsx index 995820316..291bbd92b 100644 --- a/apps/zbugs/src/hooks/use-login.tsx +++ b/apps/zbugs/src/hooks/use-login.tsx @@ -1,45 +1,17 @@ -import { - createContext, - useCallback, - useContext, - useSyncExternalStore, -} from 'react'; -import {clearJwt} from '../jwt.js'; -import {type LoginState, authRef} from '../zero-setup.js'; +import {createContext, useContext} from 'react'; +import {type LoginState} from '../zero-setup.js'; export type LoginContext = { logout: () => void; loginState: LoginState | undefined; }; -const loginContext = createContext(undefined); +export const loginContext = createContext(undefined); -// eslint-disable-next-line react-refresh/only-export-components -export function useLogin() { +export function useLogin(): LoginContext { const state = useContext(loginContext); if (state === undefined) { throw new Error('useLogin must be used within a LoginProvider'); } return state; } - -export function LoginProvider({children}: {children: React.ReactNode}) { - const loginState = useSyncExternalStore( - authRef.onChange, - useCallback(() => authRef.value, []), - ); - - return ( - { - clearJwt(); - authRef.value = undefined; - }, - loginState, - }} - > - {children} - - ); -} diff --git a/apps/zbugs/src/main.tsx b/apps/zbugs/src/main.tsx index 075268385..e3a0cf8bc 100644 --- a/apps/zbugs/src/main.tsx +++ b/apps/zbugs/src/main.tsx @@ -4,9 +4,9 @@ import {StrictMode} from 'react'; import {createRoot} from 'react-dom/client'; import 'react-toastify/dist/ReactToastify.css'; import {must} from 'shared/src/must.js'; -import {LoginProvider} from './hooks/use-login.js'; +import {LoginProvider} from './components/login-provider.js'; import './index.css'; -import Root from './root.js'; +import {Root} from './root.js'; createRoot(must(document.getElementById('root'))).render( diff --git a/apps/zbugs/src/pages/issue/comment-composer.tsx b/apps/zbugs/src/pages/issue/comment-composer.tsx index 54ca3e96b..939fb42eb 100644 --- a/apps/zbugs/src/pages/issue/comment-composer.tsx +++ b/apps/zbugs/src/pages/issue/comment-composer.tsx @@ -5,7 +5,7 @@ import {useLogin} from '../../hooks/use-login.js'; import {useZero} from '../../hooks/use-zero.js'; import {isCtrlEnter} from './is-ctrl-enter.js'; -export default function CommentComposer({ +export function CommentComposer({ id, body, issueID, diff --git a/apps/zbugs/src/pages/issue/comment.tsx b/apps/zbugs/src/pages/issue/comment.tsx index 229cbdd88..36f7c27f4 100644 --- a/apps/zbugs/src/pages/issue/comment.tsx +++ b/apps/zbugs/src/pages/issue/comment.tsx @@ -8,13 +8,13 @@ import {CanEdit} from '../../components/can-edit.js'; import {Confirm} from '../../components/confirm.js'; import {EmojiPanel} from '../../components/emoji-panel.js'; import {Link} from '../../components/link.js'; -import Markdown from '../../components/markdown.js'; -import RelativeTime from '../../components/relative-time.js'; +import {Markdown} from '../../components/markdown.js'; +import {RelativeTime} from '../../components/relative-time.js'; import {type Emoji} from '../../emoji-utils.js'; import {useHash} from '../../hooks/use-hash.js'; import {useLogin} from '../../hooks/use-login.js'; import {useZero} from '../../hooks/use-zero.js'; -import CommentComposer from './comment-composer.js'; +import {CommentComposer} from './comment-composer.js'; import style from './comment.module.css'; type Props = { @@ -33,98 +33,99 @@ type Props = { highlight?: boolean | undefined; }; -const Comment = memo(({id, issueID, comment, height, highlight}: Props) => { - const z = useZero(); - const [editing, setEditing] = useState(false); - const login = useLogin(); - const [deleteConfirmationShown, setDeleteConfirmationShown] = useState(false); +export const Comment = memo( + ({id, issueID, comment, height, highlight}: Props) => { + const z = useZero(); + const [editing, setEditing] = useState(false); + const login = useLogin(); + const [deleteConfirmationShown, setDeleteConfirmationShown] = + useState(false); - const hash = useHash(); - const permalink = comment && makePermalink(comment); - const isPermalinked = highlight || hash === permalink; + const hash = useHash(); + const permalink = comment && makePermalink(comment); + const isPermalinked = highlight || hash === permalink; - const edit = () => setEditing(true); - const remove = () => z.mutate.comment.delete({id}); + const edit = () => setEditing(true); + const remove = () => z.mutate.comment.delete({id}); - if (!comment) { - return
; - } - return ( -
-

- {comment.creator?.name{' '} - {comment.creator?.login} -

- - - - - - {editing ? ( - setEditing(false)} - /> - ) : ( - <> -
- {comment.body} -
-
; + } + return ( +
+

+ {comment.creator?.name{' '} + {comment.creator?.login} +

+ + + + + + {editing ? ( + setEditing(false)} /> - - )} - {editing ? null : ( - -
- - -
-
- )} - { - if (b) { - remove(); - } - setDeleteConfirmationShown(false); - }} - /> -
- ); -}); - -export {Comment as default}; + ) : ( + <> +
+ {comment.body} +
+ + + )} + {editing ? null : ( + +
+ + +
+
+ )} + { + if (b) { + remove(); + } + setDeleteConfirmationShown(false); + }} + /> + + ); + }, +); diff --git a/apps/zbugs/src/pages/issue/issue-composer.tsx b/apps/zbugs/src/pages/issue/issue-composer.tsx index eeaed5694..70e741031 100644 --- a/apps/zbugs/src/pages/issue/issue-composer.tsx +++ b/apps/zbugs/src/pages/issue/issue-composer.tsx @@ -17,7 +17,7 @@ const focusInput = (input: HTMLInputElement | null) => { } }; -export default function IssueComposer({isOpen, onDismiss}: Props) { +export function IssueComposer({isOpen, onDismiss}: Props) { const [title, setTitle] = useState(''); const [description, setDescription] = useState(''); const z = useZero(); diff --git a/apps/zbugs/src/pages/issue/issue-page.tsx b/apps/zbugs/src/pages/issue/issue-page.tsx index cc993eb60..9d2fc97bb 100644 --- a/apps/zbugs/src/pages/issue/issue-page.tsx +++ b/apps/zbugs/src/pages/issue/issue-page.tsx @@ -30,16 +30,16 @@ import {CanEdit} from '../../components/can-edit.js'; import {Combobox} from '../../components/combobox.js'; import {Confirm} from '../../components/confirm.js'; import {EmojiPanel} from '../../components/emoji-panel.js'; -import {useEmojiDataSourcePreload} from '../../components/emoji-picker.js'; -import LabelPicker from '../../components/label-picker.js'; +import {LabelPicker} from '../../components/label-picker.js'; import {Link} from '../../components/link.js'; -import Markdown from '../../components/markdown.js'; -import RelativeTime from '../../components/relative-time.js'; -import UserPicker from '../../components/user-picker.js'; +import {Markdown} from '../../components/markdown.js'; +import {RelativeTime} from '../../components/relative-time.js'; +import {UserPicker} from '../../components/user-picker.js'; import {type Emoji} from '../../emoji-utils.js'; import {useCanEdit} from '../../hooks/use-can-edit.js'; import {useDocumentHasFocus} from '../../hooks/use-document-has-focus.js'; -import useIsScrolling from '../../hooks/use-is-scrolling.js'; +import {useEmojiDataSourcePreload} from '../../hooks/use-emoji-data-source-preload.js'; +import {useIsScrolling} from '../../hooks/use-is-scrolling.js'; import {useKeypress} from '../../hooks/use-keypress.js'; import {useLogin} from '../../hooks/use-login.js'; import {useZero} from '../../hooks/use-zero.js'; @@ -47,8 +47,8 @@ import {LRUCache} from '../../lru-cache.js'; import {recordPageLoad} from '../../page-load-stats.js'; import {links, type ListContext, type ZbugsHistoryState} from '../../routes.js'; import {preload} from '../../zero-setup.js'; -import CommentComposer from './comment-composer.js'; -import Comment from './comment.js'; +import {CommentComposer} from './comment-composer.js'; +import {Comment} from './comment.js'; import {isCtrlEnter} from './is-ctrl-enter.js'; const emojiToastShowDuration = 3_000; diff --git a/apps/zbugs/src/pages/list/list-page.tsx b/apps/zbugs/src/pages/list/list-page.tsx index aa91aeb7c..6446a2b9d 100644 --- a/apps/zbugs/src/pages/list/list-page.tsx +++ b/apps/zbugs/src/pages/list/list-page.tsx @@ -16,18 +16,18 @@ import {useSearch} from 'wouter'; import {navigate} from 'wouter/use-browser-location'; import {Button} from '../../components/button.js'; import {Filter, type Selection} from '../../components/filter.js'; -import IssueLink from '../../components/issue-link.js'; +import {IssueLink} from '../../components/issue-link.js'; import {Link} from '../../components/link.js'; -import RelativeTime from '../../components/relative-time.js'; +import {RelativeTime} from '../../components/relative-time.js'; import {useClickOutside} from '../../hooks/use-click-outside.js'; import {useElementSize} from '../../hooks/use-element-size.js'; import {useKeypress} from '../../hooks/use-keypress.js'; import {useLogin} from '../../hooks/use-login.js'; import {useZero} from '../../hooks/use-zero.js'; +import {recordPageLoad} from '../../page-load-stats.js'; import {mark} from '../../perf-log.js'; import type {ListContext} from '../../routes.js'; import {preload} from '../../zero-setup.js'; -import {recordPageLoad} from '../../page-load-stats.js'; let firstRowRendered = false; const itemSize = 56; diff --git a/apps/zbugs/src/root.tsx b/apps/zbugs/src/root.tsx index f05215e28..2b4ff2ef8 100644 --- a/apps/zbugs/src/root.tsx +++ b/apps/zbugs/src/root.tsx @@ -8,7 +8,7 @@ import {ListPage} from './pages/list/list-page.js'; import {routes} from './routes.js'; import {zeroRef} from './zero-setup.js'; -export default function Root() { +export function Root() { const z = useSyncExternalStore( zeroRef.onChange, useCallback(() => zeroRef.value, []),