From 2910331305b63c0d8bdb290a1c80f15e2511a511 Mon Sep 17 00:00:00 2001 From: Benjamin Leonard Date: Tue, 16 Apr 2024 15:23:05 +0100 Subject: [PATCH 01/33] Rough? Gross? Cool? --- app/components/AsciidocBlocks/Document.tsx | 8 +- app/components/AsciidocBlocks/Section.tsx | 37 +- app/components/AsciidocBlocks/index.ts | 6 +- app/components/Dropdown.tsx | 28 +- app/components/EmptyMessage.tsx | 52 + app/components/rfd/index.css | 49 - app/components/spinner.css | 92 ++ app/components/tome/EditorTheme.ts | 17 + app/components/tome/Sidebar.tsx | 72 ++ app/components/tome/TomeForm.tsx | 215 ++++ app/components/tome/TypingIndicator.tsx | 0 app/hooks/use-debounce.ts | 27 + app/routes/$slug.tsx | 18 +- app/routes/rfd.$slug.tsx | 4 +- app/routes/tome_.$id.delete.tsx | 30 + app/routes/tome_.$id.tsx | 121 ++ app/routes/tome_.$id_.edit.tsx | 76 ++ app/routes/tome_._index.tsx | 45 + app/routes/tome_.new.tsx | 37 + app/routes/tome_.tsx | 48 + app/styles/index.css | 15 + package-lock.json | 1306 ++++++++++++++++++-- package.json | 8 + tome/.gitignore | 1 + tome/api/index.ts | 85 ++ tome/api/main.ts | 81 ++ tome/bun.lockb | Bin 0 -> 8290 bytes tome/db/drop.sh | 10 + tome/db/init.sh | 9 + tome/db/seed.sql | 16 + tome/index.ts | 1 + tome/package.json | 18 + tome/tsconfig.json | 29 + 33 files changed, 2357 insertions(+), 204 deletions(-) create mode 100644 app/components/EmptyMessage.tsx create mode 100644 app/components/spinner.css create mode 100644 app/components/tome/EditorTheme.ts create mode 100644 app/components/tome/Sidebar.tsx create mode 100644 app/components/tome/TomeForm.tsx create mode 100644 app/components/tome/TypingIndicator.tsx create mode 100644 app/hooks/use-debounce.ts create mode 100644 app/routes/tome_.$id.delete.tsx create mode 100644 app/routes/tome_.$id.tsx create mode 100644 app/routes/tome_.$id_.edit.tsx create mode 100644 app/routes/tome_._index.tsx create mode 100644 app/routes/tome_.new.tsx create mode 100644 app/routes/tome_.tsx create mode 100644 tome/.gitignore create mode 100644 tome/api/index.ts create mode 100644 tome/api/main.ts create mode 100755 tome/bun.lockb create mode 100755 tome/db/drop.sh create mode 100755 tome/db/init.sh create mode 100644 tome/db/seed.sql create mode 100644 tome/index.ts create mode 100644 tome/package.json create mode 100644 tome/tsconfig.json diff --git a/app/components/AsciidocBlocks/Document.tsx b/app/components/AsciidocBlocks/Document.tsx index 8838960..8c456bb 100644 --- a/app/components/AsciidocBlocks/Document.tsx +++ b/app/components/AsciidocBlocks/Document.tsx @@ -25,7 +25,7 @@ import { export const ui = tunnel() -const CustomDocument = ({ document }: { document: AdocTypes.Document }) => { +export const CustomDocument = ({ document }: { document: AdocTypes.Document }) => { const [titleEl, setTitleEl] = useState(null) const bodyRef = useRef(null) const [activeItem, setActiveItem] = useState('') @@ -174,4 +174,8 @@ const CustomDocument = ({ document }: { document: AdocTypes.Document }) => { ) } -export default CustomDocument +export const MinimalDocument = ({ document }: { document: AdocTypes.Document }) => ( +
+ +
+) diff --git a/app/components/AsciidocBlocks/Section.tsx b/app/components/AsciidocBlocks/Section.tsx index 47f0339..fae9873 100644 --- a/app/components/AsciidocBlocks/Section.tsx +++ b/app/components/AsciidocBlocks/Section.tsx @@ -25,6 +25,9 @@ const Section = ({ node }: { node: SectionType }) => { let sectNum = node.getSectionNumeral() sectNum = sectNum === '.' ? '' : sectNum + const hasSectLinks = docAttrs['sectlinks'] === true + const hasSectNums = docAttrs['sectnums'] === true + const sectNumLevels = docAttrs['sectnumlevels'] ? parseInt(docAttrs['sectnumlevels']) : 3 if (node.getCaption()) { @@ -52,30 +55,40 @@ const Section = ({ node }: { node: SectionType }) => { <> {/* eslint-disable-next-line jsx-a11y/anchor-is-valid, jsx-a11y/anchor-has-content */} - {parse(stripAnchors(title))} - - + {hasSectLinks ? ( + + {parse(stripAnchors(title))} + + + ) : ( + parse(stripAnchors(title)) + )} ) if (level === 0) { + const h1Props = { + className: cn('sect0', getRole(node)), + ...(hasSectNums && { 'data-sectnum': sectNum }), // Conditionally add data-sectnum + } return ( <> -

- {title} -

+

{title}

) } else { + const elementProps = { + ...(hasSectNums && { 'data-sectnum': sectNum }), // Conditionally add data-sectnum + } + return (
- {createElement(`h${level + 1}`, { 'data-sectnum': sectNum }, title)} + {createElement(`h${level + 1}`, elementProps, title)}
diff --git a/app/components/AsciidocBlocks/index.ts b/app/components/AsciidocBlocks/index.ts index 22d326d..bd3d0c5 100644 --- a/app/components/AsciidocBlocks/index.ts +++ b/app/components/AsciidocBlocks/index.ts @@ -9,12 +9,12 @@ import { AsciiDocBlocks } from '@oxide/design-system/components/dist' import { getText, type AdocTypes, type Options } from '@oxide/react-asciidoc' -import CustomDocument, { ui } from './Document' +import { CustomDocument, MinimalDocument, ui } from './Document' import Image from './Image' import Listing from './Listing' import Section from './Section' -export const opts: Options = { +export let opts: Options = { overrides: { admonition: AsciiDocBlocks.Admonition, table: AsciiDocBlocks.Table, @@ -69,4 +69,4 @@ const convertInlineQuoted = (node: AdocTypes.Inline) => { } } -export { ui, convertInlineQuoted } +export { ui, convertInlineQuoted, MinimalDocument } diff --git a/app/components/Dropdown.tsx b/app/components/Dropdown.tsx index 75108d6..0cdce75 100644 --- a/app/components/Dropdown.tsx +++ b/app/components/Dropdown.tsx @@ -19,18 +19,18 @@ export const dropdownInnerStyles = `focus:outline-0 focus:bg-hover px-3 py-2 pr- export const DropdownItem = ({ children, - classNames, + className, onSelect, }: { children: ReactNode | string - classNames?: string + className?: string onSelect?: () => void }) => ( ( {children} @@ -59,13 +59,13 @@ export const DropdownSubTrigger = ({ export const DropdownLink = ({ children, - classNames, + className, internal = false, to, disabled = false, }: { children: React.ReactNode - classNames?: string + className?: string internal?: boolean to: string disabled?: boolean @@ -76,7 +76,7 @@ export const DropdownLink = ({ className={cn( 'block ', dropdownOuterStyles, - classNames, + className, disabled && 'pointer-events-none', )} > @@ -88,18 +88,18 @@ export const DropdownLink = ({ export const DropdownMenu = ({ children, - classNames, + className, align = 'end', }: { children: React.ReactNode - classNames?: string + className?: string align?: 'end' | 'start' | 'center' | undefined }) => ( *:last-child]:border-b-0', - classNames, + className, )} align={align} > @@ -110,16 +110,16 @@ export const DropdownMenu = ({ export const DropdownSubMenu = ({ children, - classNames, + className, }: { children: JSX.Element[] - classNames?: string + className?: string }) => ( *:last-child]:border-b-0', - classNames, + className, )} > {children} diff --git a/app/components/EmptyMessage.tsx b/app/components/EmptyMessage.tsx new file mode 100644 index 0000000..da3a809 --- /dev/null +++ b/app/components/EmptyMessage.tsx @@ -0,0 +1,52 @@ +/* + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, you can obtain one at https://mozilla.org/MPL/2.0/. + * + * Copyright Oxide Computer Company + */ +import { Button, buttonStyle } from '@oxide/design-system' +import cn from 'classnames' +import type { ReactElement } from 'react' +import { Link } from 'react-router-dom' + +const buttonStyleProps = { variant: 'ghost', size: 'sm', color: 'secondary' } as const + +type Props = { + icon?: ReactElement + title: string + body?: string +} & ( // only require buttonTo or onClick if buttonText is present + | { buttonText: string; buttonTo: string } + | { buttonText: string; onClick: () => void } + | { buttonText?: never } +) + +export function EmptyMessage(props: Props) { + let button: ReactElement | null = null + if (props.buttonText && 'buttonTo' in props) { + button = ( + + {props.buttonText} + + ) + } else if (props.buttonText && 'onClick' in props) { + button = ( + + ) + } + return ( +
+ {props.icon && ( +
+ {props.icon} +
+ )} +

{props.title}

+ {props.body &&

{props.body}

} + {button} +
+ ) +} diff --git a/app/components/rfd/index.css b/app/components/rfd/index.css index 907797b..8e2eece 100644 --- a/app/components/rfd/index.css +++ b/app/components/rfd/index.css @@ -23,52 +23,3 @@ .dialog[data-leave] { transition-duration: 50ms; } - -.spinner { - --radius: 4; - --PI: 3.14159265358979; - --circumference: calc(var(--PI) * var(--radius) * 2px); - animation: rotate 5s linear infinite; -} - -.spinner .path { - stroke-dasharray: var(--circumference); - transform-origin: center; - animation: dash 4s ease-in-out infinite; - stroke: var(--content-accent); -} - -@media (prefers-reduced-motion) { - .spinner { - animation: rotate 6s linear infinite; - } - - .spinner .path { - animation: none; - stroke-dasharray: 20; - stroke-dashoffset: 100; - } - - .spinner-lg .path { - stroke-dasharray: 50; - } -} - -.spinner .bg { - stroke: var(--content-default); -} - -@keyframes rotate { - 100% { - transform: rotate(360deg); - } -} - -@keyframes dash { - from { - stroke-dashoffset: var(--circumference); - } - to { - stroke-dashoffset: calc(var(--circumference) * -1); - } -} diff --git a/app/components/spinner.css b/app/components/spinner.css new file mode 100644 index 0000000..6ff3815 --- /dev/null +++ b/app/components/spinner.css @@ -0,0 +1,92 @@ +.spinner { + --radius: 4; + --PI: 3.14159265358979; + --circumference: calc(var(--PI) * var(--radius) * 2px); + animation: rotate 5s linear infinite; +} + +.spinner .path { + stroke-dasharray: var(--circumference); + transform-origin: center; + animation: dash 4s ease-in-out infinite; + stroke: var(--content-accent); +} + +@media (prefers-reduced-motion) { + .spinner { + animation: rotate 6s linear infinite; + } + + .spinner .path { + animation: none; + stroke-dasharray: 20; + stroke-dashoffset: 100; + } + + .spinner-lg .path { + stroke-dasharray: 50; + } +} + +.spinner .bg { + stroke: var(--content-default); +} + +@keyframes rotate { + 100% { + transform: rotate(360deg); + } +} + +@keyframes dash { + from { + stroke-dashoffset: var(--circumference); + } + to { + stroke-dashoffset: calc(var(--circumference) * -1); + } +} + +.tome .spinner .bg { + stroke: var(--base-neutral-900); +} + +.tome .spinner .path { + stroke: var(--content-accent); +} + +.typing-indicator { + display: flex; + align-items: center; + justify-content: space-around; + width: 12px; + height: 12px; +} + +.typing-indicator span { + display: block; + width: 3px; + height: 3px; + background-color: var(--content-accent); + border-radius: 50%; + animation: bounce 1.4s infinite both; +} + +.typing-indicator span:nth-child(1) { + animation-delay: -0.32s; +} + +.typing-indicator span:nth-child(2) { + animation-delay: -0.16s; +} + +@keyframes bounce { + 0%, + 80%, + 100% { + transform: translateY(0); + } + 40% { + transform: translateY(-4px); + } +} diff --git a/app/components/tome/EditorTheme.ts b/app/components/tome/EditorTheme.ts new file mode 100644 index 0000000..a0f3687 --- /dev/null +++ b/app/components/tome/EditorTheme.ts @@ -0,0 +1,17 @@ +import { createTheme } from '@uiw/codemirror-themes' + +export const editorTheme = createTheme({ + theme: 'dark', + settings: { + background: '#080f11', + foreground: '#c8cacb', + caret: '#E7E7E8', + selection: '#5B5F61', + selectionMatch: '#5B5F61', + gutterBackground: '#141B1D', + gutterForeground: '#7e8385', + gutterBorder: '#1C2225', + lineHighlight: '#1c2225', + }, + styles: [], +}) diff --git a/app/components/tome/Sidebar.tsx b/app/components/tome/Sidebar.tsx new file mode 100644 index 0000000..1a6bd9d --- /dev/null +++ b/app/components/tome/Sidebar.tsx @@ -0,0 +1,72 @@ +import { Button } from '@oxide/design-system' +import { NavLink, useFetcher, useLoaderData, useMatches } from '@remix-run/react' +import cn from 'classnames' + +import Icon from '~/components/Icon' +import { type TomeItem } from '~/routes/tome_' + +const navLinkStyles = ({ isActive }: { isActive: boolean }) => { + const activeStyle = isActive + ? 'bg-accent-secondary hover:!bg-accent-secondary-hover text-accent' + : null + return `block text-sans-md text-secondary hover:bg-hover px-2 py-1 rounded flex items-center group justify-between ${activeStyle}` +} + +const Divider = ({ className }: { className?: string }) => ( +
+) + +export const Sidebar = () => { + const fetcher = useFetcher() + const matches = useMatches() + + const tomes = matches[1].data + // const defaultClass = hideOnDesktop ? 'hidden' : 'hidden 800:flex' + // const navOpenClass = hideOnDesktop ? 'flex' : '' + + return ( + + ) +} diff --git a/app/components/tome/TomeForm.tsx b/app/components/tome/TomeForm.tsx new file mode 100644 index 0000000..3bc7a90 --- /dev/null +++ b/app/components/tome/TomeForm.tsx @@ -0,0 +1,215 @@ +/* + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, you can obtain one at https://mozilla.org/MPL/2.0/. + * + * Copyright Oxide Computer Company + */ +import { EditorView } from '@codemirror/view' +import Asciidoc, { asciidoctor } from '@oxide/react-asciidoc' +import * as Dropdown from '@radix-ui/react-dropdown-menu' +import { useActionData, useFetcher, useLoaderData } from '@remix-run/react' +import CodeMirror, { type ReactCodeMirrorRef } from '@uiw/react-codemirror' +import dayjs from 'dayjs' +import { useEffect, useMemo, useRef, useState } from 'react' + +import { MinimalDocument, opts } from '~/components/AsciidocBlocks' +import { DropdownItem, DropdownLink, DropdownMenu } from '~/components/Dropdown' +import Icon from '~/components/Icon' +import { editorTheme } from '~/components/tome/EditorTheme' +import { useDebounce } from '~/hooks/use-debounce' + +import Spinner from '../Spinner' + +const ad = asciidoctor() + +opts.customDocument = MinimalDocument + +type EditorStatus = 'idle' | 'unsaved' | 'saving' | 'saved' + +export const TomeForm = ({ + initialTitle = '', + initialBody = '', + updated, + onSave, +}: { + initialTitle?: string + initialBody?: string + updated: string + onSave: (title: string, body: string) => void +}) => { + const fetcher = useFetcher() + const [status, setStatus] = useState('idle') + const actionData = useActionData() + const [body, setBody] = useState(initialBody) + const [title, setTitle] = useState(initialTitle) + const inputRef = useRef(null) + + const debouncedBody = useDebounce(body, 750) + const debouncedTitle = useDebounce(title, 750) + + useEffect(() => { + const hasChanges = body !== initialBody || title !== initialTitle + const isSaving = fetcher.state === 'submitting' + const isSaved = fetcher.state === 'idle' && status === 'saving' + + if (!hasChanges && (isSaving || isSaved)) { + if (isSaving) { + setStatus('saving') + } else if (isSaved) { + setStatus('saved') + } + } + + if (debouncedBody === body && debouncedTitle === title && status === 'unsaved') { + onSave(title, body) + setStatus('saving') + } + }, [ + body, + title, + initialBody, + initialTitle, + debouncedBody, + debouncedTitle, + fetcher.state, + status, + onSave, + ]) + + const doc = useMemo(() => { + return ad.load(body, { + standalone: true, + sourcemap: true, + attributes: { + sectnums: false, + }, + }) + }, [body]) + + return ( + +
+
+ + +
+ + {title ? title : 'Title...'} + + { + setStatus('unsaved') + setTitle(el.target.value) + }} + name="title" + placeholder="Title..." + required + className="absolute left-1 w-full bg-transparent p-0 text-sans-xl text-default placeholder:text-quaternary focus:outline-none" + /> +
+ + +
+ + +
+
+
{ + if ((el.target as HTMLElement).id === 'code_mirror_wrapper') { + inputRef.current && inputRef.current.editor && inputRef.current.editor.focus() + } + }} + > + + { + setStatus('unsaved') + setBody(val) + }} + theme={editorTheme} + className="!normal-case !tracking-normal text-mono-md" + readOnly={false} + basicSetup + autoFocus + extensions={[EditorView.lineWrapping]} + /> +
+
+ +
+ {actionData?.error &&
{actionData.error}
} +
+
+ ) +} + +const TypingIndicator = () => ( +
+ + + +
+) + +const SavingIndicator = ({ + status, + updated, +}: { + status: EditorStatus + updated: string +}) => { + return ( +
+ {dayjs(updated).format('MMM D YYYY, h:mm A')} + {status === 'unsaved' ? ( + + ) : status === 'saved' ? ( + + ) : status === 'saving' ? ( + + ) : ( + + )} +
+ ) +} + +const MoreDropdown = () => { + const tome = useLoaderData() + const fetcher = useFetcher() // Initialize the fetcher + + const handleDelete = () => { + if (window.confirm('Are you sure you want to delete this tome?')) { + fetcher.submit( + { id: tome.id }, + { + method: 'post', + action: `/tome/${tome.id}/delete`, + encType: 'application/x-www-form-urlencoded', + }, + ) + } + } + + return ( + + + + + + + View + + Delete + + + + ) +} diff --git a/app/components/tome/TypingIndicator.tsx b/app/components/tome/TypingIndicator.tsx new file mode 100644 index 0000000..e69de29 diff --git a/app/hooks/use-debounce.ts b/app/hooks/use-debounce.ts new file mode 100644 index 0000000..decc646 --- /dev/null +++ b/app/hooks/use-debounce.ts @@ -0,0 +1,27 @@ +import { useEffect, useState } from 'react' + +/** + * Custom hook for debouncing a value. + * @template T - The type of the value to be debounced. + * @param {T} value - The value to be debounced. + * @param {number} [delay] - The delay in milliseconds for debouncing. Defaults to 500 milliseconds. + * @returns {T} The debounced value. + * @see [Documentation](https://usehooks-ts.com/react-hook/use-debounce) + * @example + * const debouncedSearchTerm = useDebounce(searchTerm, 300); + */ +export function useDebounce(value: T, delay?: number): T { + const [debouncedValue, setDebouncedValue] = useState(value) + + useEffect(() => { + const timer = setTimeout(() => { + setDebouncedValue(value) + }, delay ?? 500) + + return () => { + clearTimeout(timer) + } + }, [value, delay]) + + return debouncedValue +} diff --git a/app/routes/$slug.tsx b/app/routes/$slug.tsx index ded6bf2..ebe6a0d 100644 --- a/app/routes/$slug.tsx +++ b/app/routes/$slug.tsx @@ -6,14 +6,14 @@ * Copyright Oxide Computer Company */ -import { redirect, type LoaderArgs } from '@remix-run/node' +// import { redirect, type LoaderArgs } from '@remix-run/node' -import { parseRfdNum } from '~/utils/parseRfdNum' +// import { parseRfdNum } from '~/utils/parseRfdNum' -export async function loader({ params: { slug } }: LoaderArgs) { - if (parseRfdNum(slug)) { - return redirect(`/rfd/${slug}`) - } else { - throw new Response('Not Found', { status: 404 }) - } -} +// export async function loader({ params: { slug } }: LoaderArgs) { +// if (parseRfdNum(slug)) { +// return redirect(`/rfd/${slug}`) +// } else { +// throw new Response('Not Found', { status: 404 }) +// } +// } diff --git a/app/routes/rfd.$slug.tsx b/app/routes/rfd.$slug.tsx index b59ab82..fd18d79 100644 --- a/app/routes/rfd.$slug.tsx +++ b/app/routes/rfd.$slug.tsx @@ -67,7 +67,7 @@ ad.ConverterFactory.register(new InlineConverter(), ['html5']) export const links = () => [{ rel: 'stylesheet', href: styles }] -const resp404 = () => new Response('Not Found', { status: 404 }) +export const resp404 = () => new Response('Not Found', { status: 404 }) /** * Fetch RFD, accounting for the possibility of the RFD being public. @@ -293,7 +293,7 @@ export default function Rfd() { ) } -const PropertyRow = ({ +export const PropertyRow = ({ label, children, className, diff --git a/app/routes/tome_.$id.delete.tsx b/app/routes/tome_.$id.delete.tsx new file mode 100644 index 0000000..a35f0d8 --- /dev/null +++ b/app/routes/tome_.$id.delete.tsx @@ -0,0 +1,30 @@ +/* + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, you can obtain one at https://mozilla.org/MPL/2.0/. + * + * Copyright Oxide Computer Company + */ +import { json, redirect, type ActionArgs } from '@remix-run/node' + +import { isAuthenticated } from '~/services/authn.server' + +export async function action({ request, params }: ActionArgs) { + const user = await isAuthenticated(request) + + if (!user) throw new Response('User not found', { status: 401 }) + + const response = await fetch(`http://localhost:8080/tome/${params.id}`, { + method: 'DELETE', + headers: { + 'x-api-key': process.env.TOME_API_KEY || '', + }, + }) + + if (response.ok) { + return redirect(`/tome`) + } else { + const result = await response.json() + return json({ error: result.error }, { status: response.status }) + } +} diff --git a/app/routes/tome_.$id.tsx b/app/routes/tome_.$id.tsx new file mode 100644 index 0000000..ff2c6bc --- /dev/null +++ b/app/routes/tome_.$id.tsx @@ -0,0 +1,121 @@ +/* + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, you can obtain one at https://mozilla.org/MPL/2.0/. + * + * Copyright Oxide Computer Company + */ +import { Badge } from '@oxide/design-system' +import Asciidoc, { asciidoctor } from '@oxide/react-asciidoc' +import * as Dropdown from '@radix-ui/react-dropdown-menu' +import { type LoaderArgs } from '@remix-run/node' +import { useFetcher, useLoaderData } from '@remix-run/react' +import dayjs from 'dayjs' +import { useMemo } from 'react' +import { ClientOnly } from 'remix-utils' + +import { opts } from '~/components/AsciidocBlocks' +import Container from '~/components/Container' +import { DropdownItem, DropdownLink, DropdownMenu } from '~/components/Dropdown' +import Icon from '~/components/Icon' + +import { PropertyRow } from './rfd.$slug' + +const ad = asciidoctor() + +export async function loader({ params: { id } }: LoaderArgs) { + const response = await fetch(`http://localhost:8080/tome/${id}`, { + headers: { + 'x-api-key': 'abcdef', + }, + }) + if (!response.ok) { + throw new Response('Not Found', { status: 404 }) + } + const data = await response.json() + return data +} + +export default function Tome() { + const tome = useLoaderData() + + const doc = useMemo(() => { + return ad.load(tome.body, { + standalone: true, + sourcemap: true, + attributes: { + sectnums: true, + }, + }) + }, [tome]) + + return ( +
+ +
+

+ {tome.title} +

+ +
+ +
+
+
+ +
+ + Public + + + }> + {() => <>{dayjs(tome.created).format('MMM D YYYY, h:mm A')}} + + + + }> + {() => <>{dayjs(tome.updated).format('MMM D YYYY, h:mm A')}} + + +
+ + +
+ ) +} + +const MoreDropdown = () => { + const tome = useLoaderData() + const fetcher = useFetcher() // Initialize the fetcher + + const handleDelete = () => { + if (window.confirm('Are you sure you want to delete this tome?')) { + fetcher.submit( + { id: tome.id }, + { + method: 'post', + action: `/tome/${tome.id}/delete`, + encType: 'application/x-www-form-urlencoded', + }, + ) + } + } + + return ( + + + + + + + Edit + + Delete + + + + ) +} diff --git a/app/routes/tome_.$id_.edit.tsx b/app/routes/tome_.$id_.edit.tsx new file mode 100644 index 0000000..df12050 --- /dev/null +++ b/app/routes/tome_.$id_.edit.tsx @@ -0,0 +1,76 @@ +/* + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, you can obtain one at https://mozilla.org/MPL/2.0/. + * + * Copyright Oxide Computer Company + */ +import { json, type ActionArgs, type LoaderArgs } from '@remix-run/node' +import { useFetcher, useLoaderData } from '@remix-run/react' + +import { Sidebar } from '~/components/tome/Sidebar' +import { TomeForm } from '~/components/tome/TomeForm' +import { isAuthenticated } from '~/services/authn.server' + +export async function loader({ params: { id } }: LoaderArgs) { + const response = await fetch(`http://localhost:8080/tome/${id}`, { + headers: { + 'x-api-key': process.env.TOME_API_KEY || '', + }, + }) + if (!response.ok) { + throw new Response('Not Found', { status: 404 }) + } + const data = await response.json() + return data +} + +export async function action({ request, params }: ActionArgs) { + const formData = await request.formData() + const title = formData.get('title') + const body = formData.get('body') + + const user = await isAuthenticated(request) + + if (!user) throw new Response('User not found', { status: 401 }) + + const response = await fetch(`http://localhost:8080/tome/${params.id}`, { + method: 'PUT', + headers: { + 'x-api-key': process.env.TOME_API_KEY || '', + 'Content-Type': 'application/json; charset=utf-8', + }, + body: JSON.stringify({ title, body }), + }) + + if (response.ok) { + return json({ status: response.status }) + } else { + const result = await response.json() + return json({ error: result.error }, { status: response.status }) + } +} + +export default function TomeEdit() { + const data = useLoaderData() + const fetcher = useFetcher() + + const handleSave = (title: string, body: string) => { + fetcher.submit({ title, body }, { method: 'post' }) + } + + return ( + <> + +
+ +
+ + ) +} diff --git a/app/routes/tome_._index.tsx b/app/routes/tome_._index.tsx new file mode 100644 index 0000000..a4d829c --- /dev/null +++ b/app/routes/tome_._index.tsx @@ -0,0 +1,45 @@ +/* + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, you can obtain one at https://mozilla.org/MPL/2.0/. + * + * Copyright Oxide Computer Company + */ + +import { EmptyMessage } from '@oxide/design-system' +import { redirect, type LoaderArgs } from '@remix-run/node' +import { Link, useLoaderData } from '@remix-run/react' + +import Container from '~/components/Container' +import { isAuthenticated } from '~/services/authn.server' + +export const loader = async ({ request }: LoaderArgs) => { + const user = await isAuthenticated(request) + + if (!user) throw new Response('Not authorized', { status: 401 }) + + const response = await fetch(`http://localhost:8080/user/${user.id}`, { + headers: { + 'x-api-key': process.env.TOME_API_KEY || '', + }, + }) + if (!response.ok) { + throw new Error(`Error fetching: ${response.statusText}`) + } + const data = await response.json() + + if (data.length > 0) { + return redirect(`/tome/${data[0].id}/edit`) + } else { + return redirect('/tome/new') + } +} + +export type Tome = { + id: string + title: string + user: string + body: string + created: string + updated: string +} diff --git a/app/routes/tome_.new.tsx b/app/routes/tome_.new.tsx new file mode 100644 index 0000000..cb87685 --- /dev/null +++ b/app/routes/tome_.new.tsx @@ -0,0 +1,37 @@ +/* + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, you can obtain one at https://mozilla.org/MPL/2.0/. + * + * Copyright Oxide Computer Company + */ +import { json, redirect, type ActionArgs, type LoaderArgs } from '@remix-run/node' + +import { isAuthenticated } from '~/services/authn.server' + +export async function action({ request }: ActionArgs) { + const user = await isAuthenticated(request) + + if (!user) throw new Response('User not found', { status: 401 }) + + const response = await fetch('http://localhost:8080/tome', { + method: 'POST', + headers: { + 'x-api-key': process.env.TOME_API_KEY || '', + 'Content-Type': 'application/json; charset=utf-8', + }, + body: JSON.stringify({ title: 'Untitled', user: user.id, body: '' }), + }) + + const result = await response.json() + + if (response.ok) { + return redirect(`/tome/${result.id}/edit`) + } else { + return json({ error: result.error }, { status: response.status }) + } +} + +export async function loader(args: LoaderArgs) { + return action(args) +} diff --git a/app/routes/tome_.tsx b/app/routes/tome_.tsx new file mode 100644 index 0000000..c8b9c98 --- /dev/null +++ b/app/routes/tome_.tsx @@ -0,0 +1,48 @@ +/* + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, you can obtain one at https://mozilla.org/MPL/2.0/. + * + * Copyright Oxide Computer Company + */ + +import { type LoaderArgs } from '@remix-run/node' +import { Outlet } from '@remix-run/react' +import { useEffect } from 'react' + +import { isAuthenticated } from '~/services/authn.server' + +export const loader = async ({ request }: LoaderArgs) => { + const user = await isAuthenticated(request) + + if (!user) throw new Response('Not authorized', { status: 401 }) + + const response = await fetch(`http://localhost:8080/user/${user.id}`, { + headers: { + 'x-api-key': process.env.TOME_API_KEY || '', + }, + }) + if (!response.ok) { + throw new Error(`Error fetching: ${response.statusText}`) + } + const data = await response.json() + return data +} + +export type TomeItem = { + id: string + title: string + user: string + body: string + created: string + updated: string +} + +export default function Tome() { + useEffect(() => { + document.body.classList.add('tome') + document.body.classList.add('purple-theme') + }, []) + + return +} diff --git a/app/styles/index.css b/app/styles/index.css index cd13520..90f1d44 100644 --- a/app/styles/index.css +++ b/app/styles/index.css @@ -20,6 +20,8 @@ @import './lib/github-markdown.css'; @import './lib/loading-bar.css'; +@import '../components/spinner.css'; + @tailwind base; @tailwind components; @tailwind utilities; @@ -62,6 +64,19 @@ body { @apply bg-default; } +body.tome { + @apply m-0; +} + +.cm-line { + @apply pl-4; +} + +#code_mirror_wrapper .cm-line, +#code_mirror_wrapper .cm-gutters { + @apply !text-[15px] !normal-case !tracking-normal text-mono-md; +} + @layer base { body { @apply text-sans-sm text-default; diff --git a/package-lock.json b/package-lock.json index db3e729..e2a377c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -6,6 +6,7 @@ "": { "dependencies": { "@ariakit/react": "^0.3.5", + "@codemirror/language": "^6.10.1", "@floating-ui/react": "^0.17.0", "@meilisearch/instant-meilisearch": "^0.8.2", "@oxide/design-system": "^1.4.0", @@ -18,7 +19,11 @@ "@sentry/remix": "^7.15.0", "@tanstack/react-query": "^4.3.9", "@types/marked": "^4.0.8", + "@types/sqlite3": "^3.1.11", + "@uiw/codemirror-theme-console": "^4.21.25", + "@uiw/react-codemirror": "^4.21.25", "classnames": "^2.3.1", + "codemirror-asciidoc": "^2.0.1", "dayjs": "^1.11.5", "fuzzysort": "^2.0.1", "highlight.js": "^11.6.0", @@ -39,7 +44,10 @@ "remix-auth-oauth2": "^1.11.1", "remix-utils": "^3.3.0", "simple-text-diff": "^1.7.0", + "sqlite": "^5.1.1", + "sqlite3": "^5.1.7", "tunnel-rat": "^0.1.2", + "usehooks-ts": "^3.1.0", "zod": "^3.22.3" }, "devDependencies": { @@ -2265,6 +2273,93 @@ "version": "6.0.2", "license": "MIT" }, + "node_modules/@codemirror/autocomplete": { + "version": "6.16.0", + "resolved": "https://registry.npmjs.org/@codemirror/autocomplete/-/autocomplete-6.16.0.tgz", + "integrity": "sha512-P/LeCTtZHRTCU4xQsa89vSKWecYv1ZqwzOd5topheGRf+qtacFgBeIMQi3eL8Kt/BUNvxUWkx+5qP2jlGoARrg==", + "dependencies": { + "@codemirror/language": "^6.0.0", + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.17.0", + "@lezer/common": "^1.0.0" + }, + "peerDependencies": { + "@codemirror/language": "^6.0.0", + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.0.0", + "@lezer/common": "^1.0.0" + } + }, + "node_modules/@codemirror/commands": { + "version": "6.3.3", + "resolved": "https://registry.npmjs.org/@codemirror/commands/-/commands-6.3.3.tgz", + "integrity": "sha512-dO4hcF0fGT9tu1Pj1D2PvGvxjeGkbC6RGcZw6Qs74TH+Ed1gw98jmUgd2axWvIZEqTeTuFrg1lEB1KV6cK9h1A==", + "dependencies": { + "@codemirror/language": "^6.0.0", + "@codemirror/state": "^6.4.0", + "@codemirror/view": "^6.0.0", + "@lezer/common": "^1.1.0" + } + }, + "node_modules/@codemirror/language": { + "version": "6.10.1", + "resolved": "https://registry.npmjs.org/@codemirror/language/-/language-6.10.1.tgz", + "integrity": "sha512-5GrXzrhq6k+gL5fjkAwt90nYDmjlzTIJV8THnxNFtNKWotMIlzzN+CpqxqwXOECnUdOndmSeWntVrVcv5axWRQ==", + "dependencies": { + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.23.0", + "@lezer/common": "^1.1.0", + "@lezer/highlight": "^1.0.0", + "@lezer/lr": "^1.0.0", + "style-mod": "^4.0.0" + } + }, + "node_modules/@codemirror/lint": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/@codemirror/lint/-/lint-6.5.0.tgz", + "integrity": "sha512-+5YyicIaaAZKU8K43IQi8TBy6mF6giGeWAH7N96Z5LC30Wm5JMjqxOYIE9mxwMG1NbhT2mA3l9hA4uuKUM3E5g==", + "dependencies": { + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.0.0", + "crelt": "^1.0.5" + } + }, + "node_modules/@codemirror/search": { + "version": "6.5.6", + "resolved": "https://registry.npmjs.org/@codemirror/search/-/search-6.5.6.tgz", + "integrity": "sha512-rpMgcsh7o0GuCDUXKPvww+muLA1pDJaFrpq/CCHtpQJYz8xopu4D1hPcKRoDD0YlF8gZaqTNIRa4VRBWyhyy7Q==", + "dependencies": { + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.0.0", + "crelt": "^1.0.5" + } + }, + "node_modules/@codemirror/state": { + "version": "6.4.1", + "resolved": "https://registry.npmjs.org/@codemirror/state/-/state-6.4.1.tgz", + "integrity": "sha512-QkEyUiLhsJoZkbumGZlswmAhA7CBU02Wrz7zvH4SrcifbsqwlXShVXg65f3v/ts57W3dqyamEriMhij1Z3Zz4A==" + }, + "node_modules/@codemirror/theme-one-dark": { + "version": "6.1.2", + "resolved": "https://registry.npmjs.org/@codemirror/theme-one-dark/-/theme-one-dark-6.1.2.tgz", + "integrity": "sha512-F+sH0X16j/qFLMAfbciKTxVOwkdAS336b7AXTKOZhy8BR3eH/RelsnLgLFINrpST63mmN2OuwUt0W2ndUgYwUA==", + "dependencies": { + "@codemirror/language": "^6.0.0", + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.0.0", + "@lezer/highlight": "^1.0.0" + } + }, + "node_modules/@codemirror/view": { + "version": "6.26.3", + "resolved": "https://registry.npmjs.org/@codemirror/view/-/view-6.26.3.tgz", + "integrity": "sha512-gmqxkPALZjkgSxIeeweY/wGQXBfwTUaLs8h7OKtSwfbj9Ct3L11lD+u1sS7XHppxFQoMDiMDp07P9f3I2jWOHw==", + "dependencies": { + "@codemirror/state": "^6.4.0", + "style-mod": "^4.1.0", + "w3c-keyname": "^2.2.4" + } + }, "node_modules/@cspotcode/source-map-support": { "version": "0.8.1", "dev": true, @@ -2912,7 +3007,7 @@ }, "node_modules/@gar/promisify": { "version": "1.1.3", - "dev": true, + "devOptional": true, "license": "MIT" }, "node_modules/@headlessui/react": { @@ -3044,6 +3139,27 @@ "version": "2.0.1", "dev": true }, + "node_modules/@lezer/common": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@lezer/common/-/common-1.2.1.tgz", + "integrity": "sha512-yemX0ZD2xS/73llMZIK6KplkjIjf2EvAHcinDi/TfJ9hS25G0388+ClHt6/3but0oOxinTcQHJLDXh6w1crzFQ==" + }, + "node_modules/@lezer/highlight": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@lezer/highlight/-/highlight-1.2.0.tgz", + "integrity": "sha512-WrS5Mw51sGrpqjlh3d4/fOwpEV2Hd3YOkp9DBt4k8XZQcoTHZFB7sx030A6OcahF4J1nDQAa3jXlTVVYH50IFA==", + "dependencies": { + "@lezer/common": "^1.0.0" + } + }, + "node_modules/@lezer/lr": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@lezer/lr/-/lr-1.4.0.tgz", + "integrity": "sha512-Wst46p51km8gH0ZUmeNrtpRYmdlRHUpN1DQd3GFAyKANi8WVz8c2jHYTf1CVScFaCjQw1iO3ZZdqGDxQPRErTg==", + "dependencies": { + "@lezer/common": "^1.0.0" + } + }, "node_modules/@meilisearch/instant-meilisearch": { "version": "0.8.2", "license": "MIT", @@ -3093,7 +3209,7 @@ }, "node_modules/@npmcli/fs": { "version": "1.1.1", - "dev": true, + "devOptional": true, "license": "ISC", "dependencies": { "@gar/promisify": "^1.0.1", @@ -3102,7 +3218,7 @@ }, "node_modules/@npmcli/move-file": { "version": "1.1.2", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "mkdirp": "^1.0.4", @@ -6070,7 +6186,7 @@ }, "node_modules/@tootallnate/once": { "version": "1.1.2", - "dev": true, + "devOptional": true, "license": "MIT", "engines": { "node": ">= 6" @@ -6691,6 +6807,14 @@ "@types/node": "*" } }, + "node_modules/@types/sqlite3": { + "version": "3.1.11", + "resolved": "https://registry.npmjs.org/@types/sqlite3/-/sqlite3-3.1.11.tgz", + "integrity": "sha512-KYF+QgxAnnAh7DWPdNDroxkDI3/MspH1NMx6m/N/6fT1G6+jvsw4/ZePt8R8cr7ta58aboeTfYFBDxTJ5yv15w==", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/tough-cookie": { "version": "4.0.2", "dev": true, @@ -6896,6 +7020,83 @@ "url": "https://opencollective.com/eslint" } }, + "node_modules/@uiw/codemirror-extensions-basic-setup": { + "version": "4.21.25", + "resolved": "https://registry.npmjs.org/@uiw/codemirror-extensions-basic-setup/-/codemirror-extensions-basic-setup-4.21.25.tgz", + "integrity": "sha512-eeUKlmEE8aSoSgelS8OR2elcPGntpRo669XinAqPCLa0eKorT2B0d3ts+AE+njAeGk744tiyAEbHb2n+6OQmJw==", + "dependencies": { + "@codemirror/autocomplete": "^6.0.0", + "@codemirror/commands": "^6.0.0", + "@codemirror/language": "^6.0.0", + "@codemirror/lint": "^6.0.0", + "@codemirror/search": "^6.0.0", + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.0.0" + }, + "funding": { + "url": "https://jaywcjlove.github.io/#/sponsor" + }, + "peerDependencies": { + "@codemirror/autocomplete": ">=6.0.0", + "@codemirror/commands": ">=6.0.0", + "@codemirror/language": ">=6.0.0", + "@codemirror/lint": ">=6.0.0", + "@codemirror/search": ">=6.0.0", + "@codemirror/state": ">=6.0.0", + "@codemirror/view": ">=6.0.0" + } + }, + "node_modules/@uiw/codemirror-theme-console": { + "version": "4.21.25", + "resolved": "https://registry.npmjs.org/@uiw/codemirror-theme-console/-/codemirror-theme-console-4.21.25.tgz", + "integrity": "sha512-f2ysTLKprFF4oGhLCStFiLVFTBwtkZA/3wANv3HmAzfAzPNgT0ZtT7ZoOW+e1yOLvhgLRVLWwR6LR9Ep2hHn4Q==", + "dependencies": { + "@uiw/codemirror-themes": "4.21.25" + } + }, + "node_modules/@uiw/codemirror-themes": { + "version": "4.21.25", + "resolved": "https://registry.npmjs.org/@uiw/codemirror-themes/-/codemirror-themes-4.21.25.tgz", + "integrity": "sha512-C3t/voELxQj0eaVhrlgzaOnSALNf8bOcRbL5xN9r2+RkdsbFOmvNl3VVhlxEB7PSGc1jUZwVO4wQsB2AP178ag==", + "dependencies": { + "@codemirror/language": "^6.0.0", + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.0.0" + }, + "funding": { + "url": "https://jaywcjlove.github.io/#/sponsor" + }, + "peerDependencies": { + "@codemirror/language": ">=6.0.0", + "@codemirror/state": ">=6.0.0", + "@codemirror/view": ">=6.0.0" + } + }, + "node_modules/@uiw/react-codemirror": { + "version": "4.21.25", + "resolved": "https://registry.npmjs.org/@uiw/react-codemirror/-/react-codemirror-4.21.25.tgz", + "integrity": "sha512-mBrCoiffQ+hbTqV1JoixFEcH7BHXkS3PjTyNH7dE8Gzf3GSBRazhtSM5HrAFIiQ5FIRGFs8Gznc4UAdhtevMmw==", + "dependencies": { + "@babel/runtime": "^7.18.6", + "@codemirror/commands": "^6.1.0", + "@codemirror/state": "^6.1.1", + "@codemirror/theme-one-dark": "^6.0.0", + "@uiw/codemirror-extensions-basic-setup": "4.21.25", + "codemirror": "^6.0.0" + }, + "funding": { + "url": "https://jaywcjlove.github.io/#/sponsor" + }, + "peerDependencies": { + "@babel/runtime": ">=7.11.0", + "@codemirror/state": ">=6.0.0", + "@codemirror/theme-one-dark": ">=6.0.0", + "@codemirror/view": ">=6.0.0", + "codemirror": ">=6.0.0", + "react": ">=16.8.0", + "react-dom": ">=16.8.0" + } + }, "node_modules/@vanilla-extract/babel-plugin-debug-ids": { "version": "1.0.3", "dev": true, @@ -7746,6 +7947,18 @@ "node": ">= 6.0.0" } }, + "node_modules/agentkeepalive": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/agentkeepalive/-/agentkeepalive-4.5.0.tgz", + "integrity": "sha512-5GG/5IbQQpC9FpkRGsSvZI5QYeSCzlJHdpBQntCsuTOxhKD8lqKhrleg2Yi7yvMIf82Ycmmqln9U8V9qwEiJew==", + "optional": true, + "dependencies": { + "humanize-ms": "^1.2.1" + }, + "engines": { + "node": ">= 8.0.0" + } + }, "node_modules/aggregate-error": { "version": "3.1.0", "license": "MIT", @@ -8142,7 +8355,6 @@ }, "node_modules/base64-js": { "version": "1.5.1", - "dev": true, "funding": [ { "type": "github", @@ -8200,16 +8412,13 @@ }, "node_modules/bindings": { "version": "1.5.0", - "dev": true, "license": "MIT", - "optional": true, "dependencies": { "file-uri-to-path": "1.0.0" } }, "node_modules/bl": { "version": "4.1.0", - "dev": true, "license": "MIT", "dependencies": { "buffer": "^5.5.0", @@ -8280,7 +8489,7 @@ }, "node_modules/brace-expansion": { "version": "1.1.11", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "balanced-match": "^1.0.0", @@ -8339,7 +8548,6 @@ }, "node_modules/buffer": { "version": "5.7.1", - "dev": true, "funding": [ { "type": "github", @@ -8400,7 +8608,7 @@ }, "node_modules/cacache": { "version": "15.3.0", - "dev": true, + "devOptional": true, "license": "ISC", "dependencies": { "@npmcli/fs": "^1.0.0", @@ -8628,7 +8836,6 @@ }, "node_modules/chownr": { "version": "2.0.0", - "dev": true, "license": "ISC", "engines": { "node": ">=10" @@ -8705,6 +8912,25 @@ "node": ">=0.10.0" } }, + "node_modules/codemirror": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/codemirror/-/codemirror-6.0.1.tgz", + "integrity": "sha512-J8j+nZ+CdWmIeFIGXEFbFPtpiYacFMDR8GlHK3IyHQJMCaVRfGx9NT+Hxivv1ckLWPvNdZqndbr/7lVhrf/Svg==", + "dependencies": { + "@codemirror/autocomplete": "^6.0.0", + "@codemirror/commands": "^6.0.0", + "@codemirror/language": "^6.0.0", + "@codemirror/lint": "^6.0.0", + "@codemirror/search": "^6.0.0", + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.0.0" + } + }, + "node_modules/codemirror-asciidoc": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/codemirror-asciidoc/-/codemirror-asciidoc-2.0.1.tgz", + "integrity": "sha512-h6Xhj+ZsWh/DTNE3xMfRv9edufchsVVwPED7wSGMeEdoYk/UtCZmwRGH0ZZQkr43aNVF3tWGLZJGT+cAeYgUIg==" + }, "node_modules/color-convert": { "version": "2.0.1", "dev": true, @@ -8798,7 +9024,7 @@ }, "node_modules/concat-map": { "version": "0.0.1", - "dev": true, + "devOptional": true, "license": "MIT" }, "node_modules/console-control-strings": { @@ -8905,6 +9131,11 @@ "optional": true, "peer": true }, + "node_modules/crelt": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/crelt/-/crelt-1.0.6.tgz", + "integrity": "sha512-VQ2MBenTq1fWZUH9DJNGti7kKv6EeAuYr3cLwxUWhIu1baTaXh4Ib5W2CqHVqib4/MqbYGJqiL3Zb8GJZr3l4g==" + }, "node_modules/cross-fetch": { "version": "3.1.5", "license": "MIT", @@ -9623,7 +9854,6 @@ }, "node_modules/decompress-response": { "version": "6.0.0", - "dev": true, "license": "MIT", "dependencies": { "mimic-response": "^3.1.0" @@ -9637,7 +9867,6 @@ }, "node_modules/decompress-response/node_modules/mimic-response": { "version": "3.1.0", - "dev": true, "license": "MIT", "engines": { "node": ">=10" @@ -9690,6 +9919,14 @@ "dev": true, "license": "MIT" }, + "node_modules/deep-extend": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", + "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==", + "engines": { + "node": ">=4.0.0" + } + }, "node_modules/deep-is": { "version": "0.1.4", "dev": true, @@ -9983,6 +10220,14 @@ "node": ">=8" } }, + "node_modules/detect-libc": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.3.tgz", + "integrity": "sha512-bwy0MGW55bG41VqxxypOsdSdGqLwXPI/focwgTYCFMbdUiBAxLg9CFzG08sz2aqzknwiX7Hkl0bQENjg8iLByw==", + "engines": { + "node": ">=8" + } + }, "node_modules/detect-newline": { "version": "3.1.0", "dev": true, @@ -10219,9 +10464,29 @@ "node": ">= 0.8" } }, + "node_modules/encoding": { + "version": "0.1.13", + "resolved": "https://registry.npmjs.org/encoding/-/encoding-0.1.13.tgz", + "integrity": "sha512-ETBauow1T35Y/WZMkio9jiM0Z5xjHHmJ4XmjZOq1l/dXz3lr2sRn87nJy20RupqSh1F2m3HHPSp8ShIPQJrJ3A==", + "optional": true, + "dependencies": { + "iconv-lite": "^0.6.2" + } + }, + "node_modules/encoding/node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "optional": true, + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/end-of-stream": { "version": "1.4.4", - "dev": true, "license": "MIT", "dependencies": { "once": "^1.4.0" @@ -10247,6 +10512,21 @@ "url": "https://github.com/fb55/entities?sponsor=1" } }, + "node_modules/env-paths": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz", + "integrity": "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==", + "optional": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/err-code": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/err-code/-/err-code-2.0.3.tgz", + "integrity": "sha512-2bmlRpNKBxT/CRmPOlyISQpNj+qSeYvcym/uT0Jx2bMOlKLtSy1ZmLuVxSEKKyor/N5yhvp/ZiG1oE3DEYMSFA==", + "optional": true + }, "node_modules/error-ex": { "version": "1.3.2", "license": "MIT", @@ -11661,6 +11941,14 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/expand-template": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz", + "integrity": "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==", + "engines": { + "node": ">=6" + } + }, "node_modules/express": { "version": "4.18.2", "license": "MIT", @@ -11858,9 +12146,7 @@ }, "node_modules/file-uri-to-path": { "version": "1.0.0", - "dev": true, - "license": "MIT", - "optional": true + "license": "MIT" }, "node_modules/fill-range": { "version": "7.0.1", @@ -12005,7 +12291,6 @@ }, "node_modules/fs-constants": { "version": "1.0.0", - "dev": true, "license": "MIT" }, "node_modules/fs-extra": { @@ -12023,7 +12308,6 @@ }, "node_modules/fs-minipass": { "version": "2.1.0", - "dev": true, "license": "ISC", "dependencies": { "minipass": "^3.0.0" @@ -12306,9 +12590,14 @@ "url": "https://github.com/fisker/git-hooks-list?sponsor=1" } }, + "node_modules/github-from-package": { + "version": "0.0.0", + "resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz", + "integrity": "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==" + }, "node_modules/glob": { "version": "7.2.3", - "dev": true, + "devOptional": true, "license": "ISC", "dependencies": { "fs.realpath": "^1.0.0", @@ -12405,7 +12694,7 @@ }, "node_modules/graceful-fs": { "version": "4.2.10", - "dev": true, + "devOptional": true, "license": "ISC" }, "node_modules/grapheme-splitter": { @@ -12735,7 +13024,7 @@ }, "node_modules/http-cache-semantics": { "version": "4.1.1", - "dev": true, + "devOptional": true, "license": "BSD-2-Clause" }, "node_modules/http-errors": { @@ -12754,7 +13043,7 @@ }, "node_modules/http-proxy-agent": { "version": "4.0.1", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "@tootallnate/once": "1", @@ -12796,6 +13085,15 @@ "node": ">=10.17.0" } }, + "node_modules/humanize-ms": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/humanize-ms/-/humanize-ms-1.2.1.tgz", + "integrity": "sha512-Fl70vYtsAFb/C06PTS9dZBo7ihau+Tu/DNCk/OyHhea07S+aeMWpFFkUaXRa8fI+ScZbEI8dfSxwY7gxZ9SAVQ==", + "optional": true, + "dependencies": { + "ms": "^2.0.0" + } + }, "node_modules/iconv-lite": { "version": "0.4.24", "license": "MIT", @@ -12819,7 +13117,6 @@ }, "node_modules/ieee754": { "version": "1.2.1", - "dev": true, "funding": [ { "type": "github", @@ -12864,7 +13161,7 @@ }, "node_modules/imurmurhash": { "version": "0.1.4", - "dev": true, + "devOptional": true, "license": "MIT", "engines": { "node": ">=0.8.19" @@ -12879,7 +13176,7 @@ }, "node_modules/infer-owner": { "version": "1.0.4", - "dev": true, + "devOptional": true, "license": "ISC" }, "node_modules/inflight": { @@ -12894,6 +13191,11 @@ "version": "2.0.4", "license": "ISC" }, + "node_modules/ini": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", + "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==" + }, "node_modules/inline-style-parser": { "version": "0.1.1", "dev": true, @@ -13275,6 +13577,12 @@ "node": ">=8" } }, + "node_modules/is-lambda": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-lambda/-/is-lambda-1.0.1.tgz", + "integrity": "sha512-z7CMFGNrENq5iFB9Bqo64Xk6Y9sg+epq1myIcdHaGnbMTYOxvzsEtdYqQUylB7LxfkvgrrjP32T6Ywciio9UIQ==", + "optional": true + }, "node_modules/is-map": { "version": "2.0.2", "dev": true, @@ -13867,7 +14175,6 @@ }, "node_modules/lodash.debounce": { "version": "4.0.8", - "dev": true, "license": "MIT" }, "node_modules/lodash.includes": { @@ -14017,6 +14324,47 @@ "optional": true, "peer": true }, + "node_modules/make-fetch-happen": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/make-fetch-happen/-/make-fetch-happen-9.1.0.tgz", + "integrity": "sha512-+zopwDy7DNknmwPQplem5lAZX/eCOzSvSNNcSKm5eVwTkOBzoktEfXsa9L23J/GIRhxRsaxzkPEhrJEpE2F4Gg==", + "optional": true, + "dependencies": { + "agentkeepalive": "^4.1.3", + "cacache": "^15.2.0", + "http-cache-semantics": "^4.1.0", + "http-proxy-agent": "^4.0.1", + "https-proxy-agent": "^5.0.0", + "is-lambda": "^1.0.1", + "lru-cache": "^6.0.0", + "minipass": "^3.1.3", + "minipass-collect": "^1.0.2", + "minipass-fetch": "^1.3.2", + "minipass-flush": "^1.0.5", + "minipass-pipeline": "^1.2.4", + "negotiator": "^0.6.2", + "promise-retry": "^2.0.1", + "socks-proxy-agent": "^6.0.0", + "ssri": "^8.0.0" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/make-fetch-happen/node_modules/socks-proxy-agent": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-6.2.1.tgz", + "integrity": "sha512-a6KW9G+6B3nWZ1yB8G7pJwL3ggLy1uTzKAgCb7ttblwqdz9fMGJUuTy3uFzEP48FAs9FLILlmzDlE2JJhVQaXQ==", + "optional": true, + "dependencies": { + "agent-base": "^6.0.2", + "debug": "^4.3.3", + "socks": "^2.6.2" + }, + "engines": { + "node": ">= 10" + } + }, "node_modules/markdown-extensions": { "version": "1.1.1", "dev": true, @@ -14923,7 +15271,7 @@ }, "node_modules/minimatch": { "version": "3.1.2", - "dev": true, + "devOptional": true, "license": "ISC", "dependencies": { "brace-expansion": "^1.1.7" @@ -14938,7 +15286,6 @@ }, "node_modules/minipass": { "version": "3.3.4", - "dev": true, "license": "ISC", "dependencies": { "yallist": "^4.0.0" @@ -14949,7 +15296,7 @@ }, "node_modules/minipass-collect": { "version": "1.0.2", - "dev": true, + "devOptional": true, "license": "ISC", "dependencies": { "minipass": "^3.0.0" @@ -14958,9 +15305,26 @@ "node": ">= 8" } }, + "node_modules/minipass-fetch": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/minipass-fetch/-/minipass-fetch-1.4.1.tgz", + "integrity": "sha512-CGH1eblLq26Y15+Azk7ey4xh0J/XfJfrCox5LDJiKqI2Q2iwOLOKrlmIaODiSQS8d18jalF6y2K2ePUm0CmShw==", + "optional": true, + "dependencies": { + "minipass": "^3.1.0", + "minipass-sized": "^1.0.3", + "minizlib": "^2.0.0" + }, + "engines": { + "node": ">=8" + }, + "optionalDependencies": { + "encoding": "^0.1.12" + } + }, "node_modules/minipass-flush": { "version": "1.0.5", - "dev": true, + "devOptional": true, "license": "ISC", "dependencies": { "minipass": "^3.0.0" @@ -14971,7 +15335,7 @@ }, "node_modules/minipass-pipeline": { "version": "1.2.4", - "dev": true, + "devOptional": true, "license": "ISC", "dependencies": { "minipass": "^3.0.0" @@ -14980,9 +15344,20 @@ "node": ">=8" } }, + "node_modules/minipass-sized": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/minipass-sized/-/minipass-sized-1.0.3.tgz", + "integrity": "sha512-MbkQQ2CTiBMlA2Dm/5cY+9SWFEN8pzzOXi6rlM5Xxq0Yqbda5ZQy9sU75a673FE9ZK0Zsbr6Y5iP6u9nktfg2g==", + "optional": true, + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/minizlib": { "version": "2.1.2", - "dev": true, "license": "MIT", "dependencies": { "minipass": "^3.0.0", @@ -14994,7 +15369,6 @@ }, "node_modules/mkdirp": { "version": "1.0.4", - "dev": true, "license": "MIT", "bin": { "mkdirp": "bin/cmd.js" @@ -15005,7 +15379,6 @@ }, "node_modules/mkdirp-classic": { "version": "0.5.3", - "dev": true, "license": "MIT" }, "node_modules/mlly": { @@ -15097,6 +15470,11 @@ "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" } }, + "node_modules/napi-build-utils": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-1.0.2.tgz", + "integrity": "sha512-ONmRUqK7zj7DWX0D9ADe03wbwOBZxNAfF20PlGfCWQcD3+/MakShIHrMqx9YwPTfxDdF1zLeL+RGZiR9kGMLdg==" + }, "node_modules/natural-compare": { "version": "1.4.0", "dev": true, @@ -15122,6 +15500,17 @@ "node": ">= 0.4.0" } }, + "node_modules/node-abi": { + "version": "3.57.0", + "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.57.0.tgz", + "integrity": "sha512-Dp+A9JWxRaKuHP35H77I4kCKesDy5HUDEmScia2FyncMTOXASMyg251F5PhFoDA5uqBrDDffiLpbqnrZmNXW+g==", + "dependencies": { + "semver": "^7.3.5" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/node-addon-api": { "version": "1.7.2", "dev": true, @@ -15146,6 +15535,45 @@ } } }, + "node_modules/node-gyp": { + "version": "8.4.1", + "resolved": "https://registry.npmjs.org/node-gyp/-/node-gyp-8.4.1.tgz", + "integrity": "sha512-olTJRgUtAb/hOXG0E93wZDs5YiJlgbXxTwQAFHyNlRsXQnYzUaF2aGgujZbw+hR8aF4ZG/rST57bWMWD16jr9w==", + "optional": true, + "dependencies": { + "env-paths": "^2.2.0", + "glob": "^7.1.4", + "graceful-fs": "^4.2.6", + "make-fetch-happen": "^9.1.0", + "nopt": "^5.0.0", + "npmlog": "^6.0.0", + "rimraf": "^3.0.2", + "semver": "^7.3.5", + "tar": "^6.1.2", + "which": "^2.0.2" + }, + "bin": { + "node-gyp": "bin/node-gyp.js" + }, + "engines": { + "node": ">= 10.12.0" + } + }, + "node_modules/node-gyp/node_modules/nopt": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-5.0.0.tgz", + "integrity": "sha512-Tbj67rffqceeLpcRXrT7vKAN8CwfPeIBgM7E6iBkmKLV7bEMwpGgYLGv0jACUsECaa/vuxP0IjEont6umdMgtQ==", + "optional": true, + "dependencies": { + "abbrev": "1" + }, + "bin": { + "nopt": "bin/nopt.js" + }, + "engines": { + "node": ">=6" + } + }, "node_modules/node-releases": { "version": "2.0.10", "license": "MIT" @@ -15520,7 +15948,7 @@ }, "node_modules/p-map": { "version": "4.0.0", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "aggregate-error": "^3.0.0" @@ -15659,7 +16087,7 @@ }, "node_modules/path-is-absolute": { "version": "1.0.1", - "dev": true, + "devOptional": true, "license": "MIT", "engines": { "node": ">=0.10.0" @@ -16374,6 +16802,40 @@ "url": "https://opencollective.com/preact" } }, + "node_modules/prebuild-install": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.2.tgz", + "integrity": "sha512-UnNke3IQb6sgarcZIDU3gbMeTp/9SSU1DAIkil7PrqG1vZlBtY5msYccSKSHDqa3hNg436IXK+SNImReuA1wEQ==", + "dependencies": { + "detect-libc": "^2.0.0", + "expand-template": "^2.0.3", + "github-from-package": "0.0.0", + "minimist": "^1.2.3", + "mkdirp-classic": "^0.5.3", + "napi-build-utils": "^1.0.1", + "node-abi": "^3.3.0", + "pump": "^3.0.0", + "rc": "^1.2.7", + "simple-get": "^4.0.0", + "tar-fs": "^2.0.0", + "tunnel-agent": "^0.6.0" + }, + "bin": { + "prebuild-install": "bin.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/prebuild-install/node_modules/pump": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", + "integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==", + "dependencies": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, "node_modules/prelude-ls": { "version": "1.2.1", "dev": true, @@ -16521,9 +16983,22 @@ }, "node_modules/promise-inflight": { "version": "1.0.1", - "dev": true, + "devOptional": true, "license": "ISC" }, + "node_modules/promise-retry": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/promise-retry/-/promise-retry-2.0.1.tgz", + "integrity": "sha512-y+WKFlBR8BGXnsNlIHFGPZmyDf3DFMoLhaflAnyZgV6rG6xu+JwesTo2Q9R6XwYmtmwAFCkAk3e35jEdoeh/3g==", + "optional": true, + "dependencies": { + "err-code": "^2.0.2", + "retry": "^0.12.0" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/prop-types": { "version": "15.8.1", "dev": true, @@ -16698,6 +17173,28 @@ "node": ">= 0.8" } }, + "node_modules/rc": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", + "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", + "dependencies": { + "deep-extend": "^0.6.0", + "ini": "~1.3.0", + "minimist": "^1.2.0", + "strip-json-comments": "~2.0.1" + }, + "bin": { + "rc": "cli.js" + } + }, + "node_modules/rc/node_modules/strip-json-comments": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", + "integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/react": { "version": "18.2.0", "license": "MIT", @@ -17192,6 +17689,15 @@ "node": ">=8" } }, + "node_modules/retry": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/retry/-/retry-0.12.0.tgz", + "integrity": "sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow==", + "optional": true, + "engines": { + "node": ">= 4" + } + }, "node_modules/reusify": { "version": "1.0.4", "dev": true, @@ -17203,7 +17709,7 @@ }, "node_modules/rimraf": { "version": "3.0.2", - "dev": true, + "devOptional": true, "license": "ISC", "dependencies": { "glob": "^7.1.3" @@ -17475,6 +17981,49 @@ "version": "3.0.7", "license": "ISC" }, + "node_modules/simple-concat": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz", + "integrity": "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/simple-get": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/simple-get/-/simple-get-4.0.1.tgz", + "integrity": "sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "dependencies": { + "decompress-response": "^6.0.0", + "once": "^1.3.1", + "simple-concat": "^1.0.0" + } + }, "node_modules/simple-text-diff": { "version": "1.7.0", "license": "MIT", @@ -17491,7 +18040,7 @@ }, "node_modules/smart-buffer": { "version": "4.2.0", - "dev": true, + "devOptional": true, "license": "MIT", "engines": { "node": ">= 6.0.0", @@ -17500,7 +18049,7 @@ }, "node_modules/socks": { "version": "2.7.1", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "ip": "^2.0.0", @@ -17528,7 +18077,7 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/ip/-/ip-2.0.1.tgz", "integrity": "sha512-lJUL9imLTNi1ZfXT+DU6rBBdbiKGBuay9B6xGSPVjUeQwaH1RIGqef8RZkUtHioLmSNpPR5M4HVKJGm1j8FWVQ==", - "dev": true + "devOptional": true }, "node_modules/sort-object-keys": { "version": "1.1.3", @@ -17620,9 +18169,45 @@ "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/sqlite": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/sqlite/-/sqlite-5.1.1.tgz", + "integrity": "sha512-oBkezXa2hnkfuJwUo44Hl9hS3er+YFtueifoajrgidvqsJRQFpc5fKoAkAor1O5ZnLoa28GBScfHXs8j0K358Q==" + }, + "node_modules/sqlite3": { + "version": "5.1.7", + "resolved": "https://registry.npmjs.org/sqlite3/-/sqlite3-5.1.7.tgz", + "integrity": "sha512-GGIyOiFaG+TUra3JIfkI/zGP8yZYLPQ0pl1bH+ODjiX57sPhrLU5sQJn1y9bDKZUFYkX1crlrPfSYt0BKKdkog==", + "hasInstallScript": true, + "dependencies": { + "bindings": "^1.5.0", + "node-addon-api": "^7.0.0", + "prebuild-install": "^7.1.1", + "tar": "^6.1.11" + }, + "optionalDependencies": { + "node-gyp": "8.x" + }, + "peerDependencies": { + "node-gyp": "8.x" + }, + "peerDependenciesMeta": { + "node-gyp": { + "optional": true + } + } + }, + "node_modules/sqlite3/node_modules/node-addon-api": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-7.1.0.tgz", + "integrity": "sha512-mNcltoe1R8o7STTegSOHdnJNN7s5EUvhoS7ShnTHDyOSd+8H+UdWODq6qSv67PjC8Zc5JRT8+oLAMCr0SIXw7g==", + "engines": { + "node": "^16 || ^18 || >= 20" + } + }, "node_modules/ssri": { "version": "8.0.1", - "dev": true, + "devOptional": true, "license": "ISC", "dependencies": { "minipass": "^3.1.1" @@ -17824,6 +18409,11 @@ "url": "https://github.com/sponsors/antfu" } }, + "node_modules/style-mod": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/style-mod/-/style-mod-4.1.2.tgz", + "integrity": "sha512-wnD1HyVqpJUI2+eKZ+eo1UwghftP6yuFheBqqe+bWCotBjC2K1YnteJILRMs3SM4V/0dLEW1SC27MWP5y+mwmw==" + }, "node_modules/style-to-js": { "version": "1.1.8", "license": "MIT", @@ -18124,7 +18714,6 @@ }, "node_modules/tar": { "version": "6.1.11", - "dev": true, "license": "ISC", "dependencies": { "chownr": "^2.0.0", @@ -18140,7 +18729,6 @@ }, "node_modules/tar-fs": { "version": "2.1.1", - "dev": true, "license": "MIT", "dependencies": { "chownr": "^1.1.1", @@ -18151,12 +18739,10 @@ }, "node_modules/tar-fs/node_modules/chownr": { "version": "1.1.4", - "dev": true, "license": "ISC" }, "node_modules/tar-fs/node_modules/pump": { "version": "3.0.0", - "dev": true, "license": "MIT", "dependencies": { "end-of-stream": "^1.1.0", @@ -18165,7 +18751,6 @@ }, "node_modules/tar-stream": { "version": "2.2.0", - "dev": true, "license": "MIT", "dependencies": { "bl": "^4.0.3", @@ -18359,6 +18944,17 @@ "typescript": ">=2.8.0 || >= 3.2.0-dev || >= 3.3.0-dev || >= 3.4.0-dev || >= 3.5.0-dev || >= 3.6.0-dev || >= 3.6.0-beta || >= 3.7.0-dev || >= 3.7.0-beta" } }, + "node_modules/tunnel-agent": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", + "integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==", + "dependencies": { + "safe-buffer": "^5.0.1" + }, + "engines": { + "node": "*" + } + }, "node_modules/tunnel-rat": { "version": "0.1.2", "license": "MIT", @@ -18503,7 +19099,7 @@ }, "node_modules/unique-filename": { "version": "1.1.1", - "dev": true, + "devOptional": true, "license": "ISC", "dependencies": { "unique-slug": "^2.0.0" @@ -18511,7 +19107,7 @@ }, "node_modules/unique-slug": { "version": "2.0.2", - "dev": true, + "devOptional": true, "license": "ISC", "dependencies": { "imurmurhash": "^0.1.4" @@ -18765,6 +19361,20 @@ "react": "^16.8.0 || ^17.0.0 || ^18.0.0" } }, + "node_modules/usehooks-ts": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/usehooks-ts/-/usehooks-ts-3.1.0.tgz", + "integrity": "sha512-bBIa7yUyPhE1BCc0GmR96VU/15l/9gP1Ch5mYdLcFBaFGQsdmXkvjV0TtOqW1yUd6VjIwDunm+flSciCQXujiw==", + "dependencies": { + "lodash.debounce": "^4.0.8" + }, + "engines": { + "node": ">=16.15.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17 || ^18" + } + }, "node_modules/util": { "version": "0.12.4", "license": "MIT", @@ -19102,6 +19712,11 @@ "node": ">=6.0" } }, + "node_modules/w3c-keyname": { + "version": "2.2.8", + "resolved": "https://registry.npmjs.org/w3c-keyname/-/w3c-keyname-2.2.8.tgz", + "integrity": "sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ==" + }, "node_modules/w3c-xmlserializer": { "version": "4.0.0", "license": "MIT", @@ -20842,6 +21457,87 @@ "@braintree/sanitize-url": { "version": "6.0.2" }, + "@codemirror/autocomplete": { + "version": "6.16.0", + "resolved": "https://registry.npmjs.org/@codemirror/autocomplete/-/autocomplete-6.16.0.tgz", + "integrity": "sha512-P/LeCTtZHRTCU4xQsa89vSKWecYv1ZqwzOd5topheGRf+qtacFgBeIMQi3eL8Kt/BUNvxUWkx+5qP2jlGoARrg==", + "requires": { + "@codemirror/language": "^6.0.0", + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.17.0", + "@lezer/common": "^1.0.0" + } + }, + "@codemirror/commands": { + "version": "6.3.3", + "resolved": "https://registry.npmjs.org/@codemirror/commands/-/commands-6.3.3.tgz", + "integrity": "sha512-dO4hcF0fGT9tu1Pj1D2PvGvxjeGkbC6RGcZw6Qs74TH+Ed1gw98jmUgd2axWvIZEqTeTuFrg1lEB1KV6cK9h1A==", + "requires": { + "@codemirror/language": "^6.0.0", + "@codemirror/state": "^6.4.0", + "@codemirror/view": "^6.0.0", + "@lezer/common": "^1.1.0" + } + }, + "@codemirror/language": { + "version": "6.10.1", + "resolved": "https://registry.npmjs.org/@codemirror/language/-/language-6.10.1.tgz", + "integrity": "sha512-5GrXzrhq6k+gL5fjkAwt90nYDmjlzTIJV8THnxNFtNKWotMIlzzN+CpqxqwXOECnUdOndmSeWntVrVcv5axWRQ==", + "requires": { + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.23.0", + "@lezer/common": "^1.1.0", + "@lezer/highlight": "^1.0.0", + "@lezer/lr": "^1.0.0", + "style-mod": "^4.0.0" + } + }, + "@codemirror/lint": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/@codemirror/lint/-/lint-6.5.0.tgz", + "integrity": "sha512-+5YyicIaaAZKU8K43IQi8TBy6mF6giGeWAH7N96Z5LC30Wm5JMjqxOYIE9mxwMG1NbhT2mA3l9hA4uuKUM3E5g==", + "requires": { + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.0.0", + "crelt": "^1.0.5" + } + }, + "@codemirror/search": { + "version": "6.5.6", + "resolved": "https://registry.npmjs.org/@codemirror/search/-/search-6.5.6.tgz", + "integrity": "sha512-rpMgcsh7o0GuCDUXKPvww+muLA1pDJaFrpq/CCHtpQJYz8xopu4D1hPcKRoDD0YlF8gZaqTNIRa4VRBWyhyy7Q==", + "requires": { + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.0.0", + "crelt": "^1.0.5" + } + }, + "@codemirror/state": { + "version": "6.4.1", + "resolved": "https://registry.npmjs.org/@codemirror/state/-/state-6.4.1.tgz", + "integrity": "sha512-QkEyUiLhsJoZkbumGZlswmAhA7CBU02Wrz7zvH4SrcifbsqwlXShVXg65f3v/ts57W3dqyamEriMhij1Z3Zz4A==" + }, + "@codemirror/theme-one-dark": { + "version": "6.1.2", + "resolved": "https://registry.npmjs.org/@codemirror/theme-one-dark/-/theme-one-dark-6.1.2.tgz", + "integrity": "sha512-F+sH0X16j/qFLMAfbciKTxVOwkdAS336b7AXTKOZhy8BR3eH/RelsnLgLFINrpST63mmN2OuwUt0W2ndUgYwUA==", + "requires": { + "@codemirror/language": "^6.0.0", + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.0.0", + "@lezer/highlight": "^1.0.0" + } + }, + "@codemirror/view": { + "version": "6.26.3", + "resolved": "https://registry.npmjs.org/@codemirror/view/-/view-6.26.3.tgz", + "integrity": "sha512-gmqxkPALZjkgSxIeeweY/wGQXBfwTUaLs8h7OKtSwfbj9Ct3L11lD+u1sS7XHppxFQoMDiMDp07P9f3I2jWOHw==", + "requires": { + "@codemirror/state": "^6.4.0", + "style-mod": "^4.1.0", + "w3c-keyname": "^2.2.4" + } + }, "@cspotcode/source-map-support": { "version": "0.8.1", "dev": true, @@ -21167,7 +21863,7 @@ }, "@gar/promisify": { "version": "1.1.3", - "dev": true + "devOptional": true }, "@headlessui/react": { "version": "1.7.17", @@ -21246,6 +21942,27 @@ "version": "2.0.1", "dev": true }, + "@lezer/common": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@lezer/common/-/common-1.2.1.tgz", + "integrity": "sha512-yemX0ZD2xS/73llMZIK6KplkjIjf2EvAHcinDi/TfJ9hS25G0388+ClHt6/3but0oOxinTcQHJLDXh6w1crzFQ==" + }, + "@lezer/highlight": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@lezer/highlight/-/highlight-1.2.0.tgz", + "integrity": "sha512-WrS5Mw51sGrpqjlh3d4/fOwpEV2Hd3YOkp9DBt4k8XZQcoTHZFB7sx030A6OcahF4J1nDQAa3jXlTVVYH50IFA==", + "requires": { + "@lezer/common": "^1.0.0" + } + }, + "@lezer/lr": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@lezer/lr/-/lr-1.4.0.tgz", + "integrity": "sha512-Wst46p51km8gH0ZUmeNrtpRYmdlRHUpN1DQd3GFAyKANi8WVz8c2jHYTf1CVScFaCjQw1iO3ZZdqGDxQPRErTg==", + "requires": { + "@lezer/common": "^1.0.0" + } + }, "@meilisearch/instant-meilisearch": { "version": "0.8.2", "requires": { @@ -21281,7 +21998,7 @@ }, "@npmcli/fs": { "version": "1.1.1", - "dev": true, + "devOptional": true, "requires": { "@gar/promisify": "^1.0.1", "semver": "^7.3.5" @@ -21289,7 +22006,7 @@ }, "@npmcli/move-file": { "version": "1.1.2", - "dev": true, + "devOptional": true, "requires": { "mkdirp": "^1.0.4", "rimraf": "^3.0.2" @@ -22962,7 +23679,7 @@ }, "@tootallnate/once": { "version": "1.1.2", - "dev": true + "devOptional": true }, "@trysound/sax": { "version": "0.2.0", @@ -23490,6 +24207,14 @@ "@types/node": "*" } }, + "@types/sqlite3": { + "version": "3.1.11", + "resolved": "https://registry.npmjs.org/@types/sqlite3/-/sqlite3-3.1.11.tgz", + "integrity": "sha512-KYF+QgxAnnAh7DWPdNDroxkDI3/MspH1NMx6m/N/6fT1G6+jvsw4/ZePt8R8cr7ta58aboeTfYFBDxTJ5yv15w==", + "requires": { + "@types/node": "*" + } + }, "@types/tough-cookie": { "version": "4.0.2", "dev": true @@ -23590,6 +24315,51 @@ } } }, + "@uiw/codemirror-extensions-basic-setup": { + "version": "4.21.25", + "resolved": "https://registry.npmjs.org/@uiw/codemirror-extensions-basic-setup/-/codemirror-extensions-basic-setup-4.21.25.tgz", + "integrity": "sha512-eeUKlmEE8aSoSgelS8OR2elcPGntpRo669XinAqPCLa0eKorT2B0d3ts+AE+njAeGk744tiyAEbHb2n+6OQmJw==", + "requires": { + "@codemirror/autocomplete": "^6.0.0", + "@codemirror/commands": "^6.0.0", + "@codemirror/language": "^6.0.0", + "@codemirror/lint": "^6.0.0", + "@codemirror/search": "^6.0.0", + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.0.0" + } + }, + "@uiw/codemirror-theme-console": { + "version": "4.21.25", + "resolved": "https://registry.npmjs.org/@uiw/codemirror-theme-console/-/codemirror-theme-console-4.21.25.tgz", + "integrity": "sha512-f2ysTLKprFF4oGhLCStFiLVFTBwtkZA/3wANv3HmAzfAzPNgT0ZtT7ZoOW+e1yOLvhgLRVLWwR6LR9Ep2hHn4Q==", + "requires": { + "@uiw/codemirror-themes": "4.21.25" + } + }, + "@uiw/codemirror-themes": { + "version": "4.21.25", + "resolved": "https://registry.npmjs.org/@uiw/codemirror-themes/-/codemirror-themes-4.21.25.tgz", + "integrity": "sha512-C3t/voELxQj0eaVhrlgzaOnSALNf8bOcRbL5xN9r2+RkdsbFOmvNl3VVhlxEB7PSGc1jUZwVO4wQsB2AP178ag==", + "requires": { + "@codemirror/language": "^6.0.0", + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.0.0" + } + }, + "@uiw/react-codemirror": { + "version": "4.21.25", + "resolved": "https://registry.npmjs.org/@uiw/react-codemirror/-/react-codemirror-4.21.25.tgz", + "integrity": "sha512-mBrCoiffQ+hbTqV1JoixFEcH7BHXkS3PjTyNH7dE8Gzf3GSBRazhtSM5HrAFIiQ5FIRGFs8Gznc4UAdhtevMmw==", + "requires": { + "@babel/runtime": "^7.18.6", + "@codemirror/commands": "^6.1.0", + "@codemirror/state": "^6.1.1", + "@codemirror/theme-one-dark": "^6.0.0", + "@uiw/codemirror-extensions-basic-setup": "4.21.25", + "codemirror": "^6.0.0" + } + }, "@vanilla-extract/babel-plugin-debug-ids": { "version": "1.0.3", "dev": true, @@ -24091,6 +24861,15 @@ "debug": "4" } }, + "agentkeepalive": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/agentkeepalive/-/agentkeepalive-4.5.0.tgz", + "integrity": "sha512-5GG/5IbQQpC9FpkRGsSvZI5QYeSCzlJHdpBQntCsuTOxhKD8lqKhrleg2Yi7yvMIf82Ycmmqln9U8V9qwEiJew==", + "optional": true, + "requires": { + "humanize-ms": "^1.2.1" + } + }, "aggregate-error": { "version": "3.1.0", "requires": { @@ -24335,8 +25114,7 @@ "version": "1.0.2" }, "base64-js": { - "version": "1.5.1", - "dev": true + "version": "1.5.1" }, "basic-auth": { "version": "2.0.1", @@ -24363,15 +25141,12 @@ }, "bindings": { "version": "1.5.0", - "dev": true, - "optional": true, "requires": { "file-uri-to-path": "1.0.0" } }, "bl": { "version": "4.1.0", - "dev": true, "requires": { "buffer": "^5.5.0", "inherits": "^2.0.4", @@ -24427,7 +25202,7 @@ }, "brace-expansion": { "version": "1.1.11", - "dev": true, + "devOptional": true, "requires": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" @@ -24463,7 +25238,6 @@ }, "buffer": { "version": "5.7.1", - "dev": true, "requires": { "base64-js": "^1.3.1", "ieee754": "^1.1.13" @@ -24493,7 +25267,7 @@ }, "cacache": { "version": "15.3.0", - "dev": true, + "devOptional": true, "requires": { "@npmcli/fs": "^1.0.0", "@npmcli/move-file": "^1.0.1", @@ -24628,8 +25402,7 @@ } }, "chownr": { - "version": "2.0.0", - "dev": true + "version": "2.0.0" }, "classnames": { "version": "2.3.2" @@ -24669,6 +25442,25 @@ "code-point-at": { "version": "1.1.0" }, + "codemirror": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/codemirror/-/codemirror-6.0.1.tgz", + "integrity": "sha512-J8j+nZ+CdWmIeFIGXEFbFPtpiYacFMDR8GlHK3IyHQJMCaVRfGx9NT+Hxivv1ckLWPvNdZqndbr/7lVhrf/Svg==", + "requires": { + "@codemirror/autocomplete": "^6.0.0", + "@codemirror/commands": "^6.0.0", + "@codemirror/language": "^6.0.0", + "@codemirror/lint": "^6.0.0", + "@codemirror/search": "^6.0.0", + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.0.0" + } + }, + "codemirror-asciidoc": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/codemirror-asciidoc/-/codemirror-asciidoc-2.0.1.tgz", + "integrity": "sha512-h6Xhj+ZsWh/DTNE3xMfRv9edufchsVVwPED7wSGMeEdoYk/UtCZmwRGH0ZZQkr43aNVF3tWGLZJGT+cAeYgUIg==" + }, "color-convert": { "version": "2.0.1", "dev": true, @@ -24731,7 +25523,7 @@ }, "concat-map": { "version": "0.0.1", - "dev": true + "devOptional": true }, "console-control-strings": { "version": "1.1.0" @@ -24794,6 +25586,11 @@ "optional": true, "peer": true }, + "crelt": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/crelt/-/crelt-1.0.6.tgz", + "integrity": "sha512-VQ2MBenTq1fWZUH9DJNGti7kKv6EeAuYr3cLwxUWhIu1baTaXh4Ib5W2CqHVqib4/MqbYGJqiL3Zb8GJZr3l4g==" + }, "cross-fetch": { "version": "3.1.5", "requires": { @@ -25248,14 +26045,12 @@ }, "decompress-response": { "version": "6.0.0", - "dev": true, "requires": { "mimic-response": "^3.1.0" }, "dependencies": { "mimic-response": { - "version": "3.1.0", - "dev": true + "version": "3.1.0" } } }, @@ -25297,6 +26092,11 @@ } } }, + "deep-extend": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", + "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==" + }, "deep-is": { "version": "0.1.4", "dev": true @@ -25462,6 +26262,11 @@ "version": "6.1.0", "dev": true }, + "detect-libc": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.3.tgz", + "integrity": "sha512-bwy0MGW55bG41VqxxypOsdSdGqLwXPI/focwgTYCFMbdUiBAxLg9CFzG08sz2aqzknwiX7Hkl0bQENjg8iLByw==" + }, "detect-newline": { "version": "3.1.0", "dev": true @@ -25622,9 +26427,28 @@ "encodeurl": { "version": "1.0.2" }, + "encoding": { + "version": "0.1.13", + "resolved": "https://registry.npmjs.org/encoding/-/encoding-0.1.13.tgz", + "integrity": "sha512-ETBauow1T35Y/WZMkio9jiM0Z5xjHHmJ4XmjZOq1l/dXz3lr2sRn87nJy20RupqSh1F2m3HHPSp8ShIPQJrJ3A==", + "optional": true, + "requires": { + "iconv-lite": "^0.6.2" + }, + "dependencies": { + "iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "optional": true, + "requires": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + } + } + } + }, "end-of-stream": { "version": "1.4.4", - "dev": true, "requires": { "once": "^1.4.0" } @@ -25641,6 +26465,18 @@ "version": "2.2.0", "dev": true }, + "env-paths": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz", + "integrity": "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==", + "optional": true + }, + "err-code": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/err-code/-/err-code-2.0.3.tgz", + "integrity": "sha512-2bmlRpNKBxT/CRmPOlyISQpNj+qSeYvcym/uT0Jx2bMOlKLtSy1ZmLuVxSEKKyor/N5yhvp/ZiG1oE3DEYMSFA==", + "optional": true + }, "error-ex": { "version": "1.3.2", "requires": { @@ -26473,6 +27309,11 @@ "version": "2.2.1", "dev": true }, + "expand-template": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz", + "integrity": "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==" + }, "express": { "version": "4.18.2", "requires": { @@ -26609,9 +27450,7 @@ } }, "file-uri-to-path": { - "version": "1.0.0", - "dev": true, - "optional": true + "version": "1.0.0" }, "fill-range": { "version": "7.0.1", @@ -26697,8 +27536,7 @@ "version": "0.5.2" }, "fs-constants": { - "version": "1.0.0", - "dev": true + "version": "1.0.0" }, "fs-extra": { "version": "10.1.0", @@ -26711,7 +27549,6 @@ }, "fs-minipass": { "version": "2.1.0", - "dev": true, "requires": { "minipass": "^3.0.0" } @@ -26899,9 +27736,14 @@ "version": "1.0.3", "dev": true }, + "github-from-package": { + "version": "0.0.0", + "resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz", + "integrity": "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==" + }, "glob": { "version": "7.2.3", - "dev": true, + "devOptional": true, "requires": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", @@ -26964,7 +27806,7 @@ }, "graceful-fs": { "version": "4.2.10", - "dev": true + "devOptional": true }, "grapheme-splitter": { "version": "1.0.4", @@ -27167,7 +28009,7 @@ }, "http-cache-semantics": { "version": "4.1.1", - "dev": true + "devOptional": true }, "http-errors": { "version": "2.0.0", @@ -27181,7 +28023,7 @@ }, "http-proxy-agent": { "version": "4.0.1", - "dev": true, + "devOptional": true, "requires": { "@tootallnate/once": "1", "agent-base": "6", @@ -27207,6 +28049,15 @@ "version": "2.1.0", "dev": true }, + "humanize-ms": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/humanize-ms/-/humanize-ms-1.2.1.tgz", + "integrity": "sha512-Fl70vYtsAFb/C06PTS9dZBo7ihau+Tu/DNCk/OyHhea07S+aeMWpFFkUaXRa8fI+ScZbEI8dfSxwY7gxZ9SAVQ==", + "optional": true, + "requires": { + "ms": "^2.0.0" + } + }, "iconv-lite": { "version": "0.4.24", "requires": { @@ -27219,8 +28070,7 @@ "requires": {} }, "ieee754": { - "version": "1.2.1", - "dev": true + "version": "1.2.1" }, "ignore": { "version": "5.2.0", @@ -27238,14 +28088,14 @@ }, "imurmurhash": { "version": "0.1.4", - "dev": true + "devOptional": true }, "indent-string": { "version": "4.0.0" }, "infer-owner": { "version": "1.0.4", - "dev": true + "devOptional": true }, "inflight": { "version": "1.0.6", @@ -27257,6 +28107,11 @@ "inherits": { "version": "2.0.4" }, + "ini": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", + "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==" + }, "inline-style-parser": { "version": "0.1.1", "dev": true @@ -27468,6 +28323,12 @@ "ip-regex": "^4.0.0" } }, + "is-lambda": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-lambda/-/is-lambda-1.0.1.tgz", + "integrity": "sha512-z7CMFGNrENq5iFB9Bqo64Xk6Y9sg+epq1myIcdHaGnbMTYOxvzsEtdYqQUylB7LxfkvgrrjP32T6Ywciio9UIQ==", + "optional": true + }, "is-map": { "version": "2.0.2", "dev": true @@ -27828,8 +28689,7 @@ "dev": true }, "lodash.debounce": { - "version": "4.0.8", - "dev": true + "version": "4.0.8" }, "lodash.includes": { "version": "4.3.0", @@ -27944,6 +28804,43 @@ "optional": true, "peer": true }, + "make-fetch-happen": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/make-fetch-happen/-/make-fetch-happen-9.1.0.tgz", + "integrity": "sha512-+zopwDy7DNknmwPQplem5lAZX/eCOzSvSNNcSKm5eVwTkOBzoktEfXsa9L23J/GIRhxRsaxzkPEhrJEpE2F4Gg==", + "optional": true, + "requires": { + "agentkeepalive": "^4.1.3", + "cacache": "^15.2.0", + "http-cache-semantics": "^4.1.0", + "http-proxy-agent": "^4.0.1", + "https-proxy-agent": "^5.0.0", + "is-lambda": "^1.0.1", + "lru-cache": "^6.0.0", + "minipass": "^3.1.3", + "minipass-collect": "^1.0.2", + "minipass-fetch": "^1.3.2", + "minipass-flush": "^1.0.5", + "minipass-pipeline": "^1.2.4", + "negotiator": "^0.6.2", + "promise-retry": "^2.0.1", + "socks-proxy-agent": "^6.0.0", + "ssri": "^8.0.0" + }, + "dependencies": { + "socks-proxy-agent": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-6.2.1.tgz", + "integrity": "sha512-a6KW9G+6B3nWZ1yB8G7pJwL3ggLy1uTzKAgCb7ttblwqdz9fMGJUuTy3uFzEP48FAs9FLILlmzDlE2JJhVQaXQ==", + "optional": true, + "requires": { + "agent-base": "^6.0.2", + "debug": "^4.3.3", + "socks": "^2.6.2" + } + } + } + }, "markdown-extensions": { "version": "1.1.1", "dev": true @@ -28451,7 +29348,7 @@ }, "minimatch": { "version": "3.1.2", - "dev": true, + "devOptional": true, "requires": { "brace-expansion": "^1.1.7" } @@ -28461,47 +29358,64 @@ }, "minipass": { "version": "3.3.4", - "dev": true, "requires": { "yallist": "^4.0.0" } }, "minipass-collect": { "version": "1.0.2", - "dev": true, + "devOptional": true, "requires": { "minipass": "^3.0.0" } }, + "minipass-fetch": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/minipass-fetch/-/minipass-fetch-1.4.1.tgz", + "integrity": "sha512-CGH1eblLq26Y15+Azk7ey4xh0J/XfJfrCox5LDJiKqI2Q2iwOLOKrlmIaODiSQS8d18jalF6y2K2ePUm0CmShw==", + "optional": true, + "requires": { + "encoding": "^0.1.12", + "minipass": "^3.1.0", + "minipass-sized": "^1.0.3", + "minizlib": "^2.0.0" + } + }, "minipass-flush": { "version": "1.0.5", - "dev": true, + "devOptional": true, "requires": { "minipass": "^3.0.0" } }, "minipass-pipeline": { "version": "1.2.4", - "dev": true, + "devOptional": true, + "requires": { + "minipass": "^3.0.0" + } + }, + "minipass-sized": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/minipass-sized/-/minipass-sized-1.0.3.tgz", + "integrity": "sha512-MbkQQ2CTiBMlA2Dm/5cY+9SWFEN8pzzOXi6rlM5Xxq0Yqbda5ZQy9sU75a673FE9ZK0Zsbr6Y5iP6u9nktfg2g==", + "optional": true, "requires": { "minipass": "^3.0.0" } }, "minizlib": { "version": "2.1.2", - "dev": true, "requires": { "minipass": "^3.0.0", "yallist": "^4.0.0" } }, "mkdirp": { - "version": "1.0.4", - "dev": true + "version": "1.0.4" }, "mkdirp-classic": { - "version": "0.5.3", - "dev": true + "version": "0.5.3" }, "mlly": { "version": "1.4.0", @@ -28559,6 +29473,11 @@ "nanoid": { "version": "3.3.6" }, + "napi-build-utils": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-1.0.2.tgz", + "integrity": "sha512-ONmRUqK7zj7DWX0D9ADe03wbwOBZxNAfF20PlGfCWQcD3+/MakShIHrMqx9YwPTfxDdF1zLeL+RGZiR9kGMLdg==" + }, "natural-compare": { "version": "1.4.0", "dev": true @@ -28574,6 +29493,14 @@ "version": "2.0.2", "dev": true }, + "node-abi": { + "version": "3.57.0", + "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.57.0.tgz", + "integrity": "sha512-Dp+A9JWxRaKuHP35H77I4kCKesDy5HUDEmScia2FyncMTOXASMyg251F5PhFoDA5uqBrDDffiLpbqnrZmNXW+g==", + "requires": { + "semver": "^7.3.5" + } + }, "node-addon-api": { "version": "1.7.2", "dev": true, @@ -28585,6 +29512,35 @@ "whatwg-url": "^5.0.0" } }, + "node-gyp": { + "version": "8.4.1", + "resolved": "https://registry.npmjs.org/node-gyp/-/node-gyp-8.4.1.tgz", + "integrity": "sha512-olTJRgUtAb/hOXG0E93wZDs5YiJlgbXxTwQAFHyNlRsXQnYzUaF2aGgujZbw+hR8aF4ZG/rST57bWMWD16jr9w==", + "optional": true, + "requires": { + "env-paths": "^2.2.0", + "glob": "^7.1.4", + "graceful-fs": "^4.2.6", + "make-fetch-happen": "^9.1.0", + "nopt": "^5.0.0", + "npmlog": "^6.0.0", + "rimraf": "^3.0.2", + "semver": "^7.3.5", + "tar": "^6.1.2", + "which": "^2.0.2" + }, + "dependencies": { + "nopt": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-5.0.0.tgz", + "integrity": "sha512-Tbj67rffqceeLpcRXrT7vKAN8CwfPeIBgM7E6iBkmKLV7bEMwpGgYLGv0jACUsECaa/vuxP0IjEont6umdMgtQ==", + "optional": true, + "requires": { + "abbrev": "1" + } + } + } + }, "node-releases": { "version": "2.0.10" }, @@ -28809,7 +29765,7 @@ }, "p-map": { "version": "4.0.0", - "dev": true, + "devOptional": true, "requires": { "aggregate-error": "^3.0.0" } @@ -28895,7 +29851,7 @@ }, "path-is-absolute": { "version": "1.0.1", - "dev": true + "devOptional": true }, "path-key": { "version": "3.1.1", @@ -29277,6 +30233,36 @@ "preact": { "version": "10.11.0" }, + "prebuild-install": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.2.tgz", + "integrity": "sha512-UnNke3IQb6sgarcZIDU3gbMeTp/9SSU1DAIkil7PrqG1vZlBtY5msYccSKSHDqa3hNg436IXK+SNImReuA1wEQ==", + "requires": { + "detect-libc": "^2.0.0", + "expand-template": "^2.0.3", + "github-from-package": "0.0.0", + "minimist": "^1.2.3", + "mkdirp-classic": "^0.5.3", + "napi-build-utils": "^1.0.1", + "node-abi": "^3.3.0", + "pump": "^3.0.0", + "rc": "^1.2.7", + "simple-get": "^4.0.0", + "tar-fs": "^2.0.0", + "tunnel-agent": "^0.6.0" + }, + "dependencies": { + "pump": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", + "integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==", + "requires": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + } + } + }, "prelude-ls": { "version": "1.2.1", "dev": true @@ -29325,7 +30311,17 @@ }, "promise-inflight": { "version": "1.0.1", - "dev": true + "devOptional": true + }, + "promise-retry": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/promise-retry/-/promise-retry-2.0.1.tgz", + "integrity": "sha512-y+WKFlBR8BGXnsNlIHFGPZmyDf3DFMoLhaflAnyZgV6rG6xu+JwesTo2Q9R6XwYmtmwAFCkAk3e35jEdoeh/3g==", + "optional": true, + "requires": { + "err-code": "^2.0.2", + "retry": "^0.12.0" + } }, "prop-types": { "version": "15.8.1", @@ -29440,6 +30436,24 @@ } } }, + "rc": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", + "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", + "requires": { + "deep-extend": "^0.6.0", + "ini": "~1.3.0", + "minimist": "^1.2.0", + "strip-json-comments": "~2.0.1" + }, + "dependencies": { + "strip-json-comments": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", + "integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==" + } + } + }, "react": { "version": "18.2.0", "requires": { @@ -29744,13 +30758,19 @@ "signal-exit": "^3.0.2" } }, + "retry": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/retry/-/retry-0.12.0.tgz", + "integrity": "sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow==", + "optional": true + }, "reusify": { "version": "1.0.4", "dev": true }, "rimraf": { "version": "3.0.2", - "dev": true, + "devOptional": true, "requires": { "glob": "^7.1.3" } @@ -29927,6 +30947,21 @@ "signal-exit": { "version": "3.0.7" }, + "simple-concat": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz", + "integrity": "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==" + }, + "simple-get": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/simple-get/-/simple-get-4.0.1.tgz", + "integrity": "sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==", + "requires": { + "decompress-response": "^6.0.0", + "once": "^1.3.1", + "simple-concat": "^1.0.0" + } + }, "simple-text-diff": { "version": "1.7.0", "requires": { @@ -29938,11 +30973,11 @@ }, "smart-buffer": { "version": "4.2.0", - "dev": true + "devOptional": true }, "socks": { "version": "2.7.1", - "dev": true, + "devOptional": true, "requires": { "ip": "^2.0.0", "smart-buffer": "^4.2.0" @@ -29952,7 +30987,7 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/ip/-/ip-2.0.1.tgz", "integrity": "sha512-lJUL9imLTNi1ZfXT+DU6rBBdbiKGBuay9B6xGSPVjUeQwaH1RIGqef8RZkUtHioLmSNpPR5M4HVKJGm1j8FWVQ==", - "dev": true + "devOptional": true } } }, @@ -30027,9 +31062,33 @@ "version": "2.0.1", "dev": true }, + "sqlite": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/sqlite/-/sqlite-5.1.1.tgz", + "integrity": "sha512-oBkezXa2hnkfuJwUo44Hl9hS3er+YFtueifoajrgidvqsJRQFpc5fKoAkAor1O5ZnLoa28GBScfHXs8j0K358Q==" + }, + "sqlite3": { + "version": "5.1.7", + "resolved": "https://registry.npmjs.org/sqlite3/-/sqlite3-5.1.7.tgz", + "integrity": "sha512-GGIyOiFaG+TUra3JIfkI/zGP8yZYLPQ0pl1bH+ODjiX57sPhrLU5sQJn1y9bDKZUFYkX1crlrPfSYt0BKKdkog==", + "requires": { + "bindings": "^1.5.0", + "node-addon-api": "^7.0.0", + "node-gyp": "8.x", + "prebuild-install": "^7.1.1", + "tar": "^6.1.11" + }, + "dependencies": { + "node-addon-api": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-7.1.0.tgz", + "integrity": "sha512-mNcltoe1R8o7STTegSOHdnJNN7s5EUvhoS7ShnTHDyOSd+8H+UdWODq6qSv67PjC8Zc5JRT8+oLAMCr0SIXw7g==" + } + } + }, "ssri": { "version": "8.0.1", - "dev": true, + "devOptional": true, "requires": { "minipass": "^3.1.1" } @@ -30158,6 +31217,11 @@ "acorn": "^8.8.0" } }, + "style-mod": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/style-mod/-/style-mod-4.1.2.tgz", + "integrity": "sha512-wnD1HyVqpJUI2+eKZ+eo1UwghftP6yuFheBqqe+bWCotBjC2K1YnteJILRMs3SM4V/0dLEW1SC27MWP5y+mwmw==" + }, "style-to-js": { "version": "1.1.8", "requires": { @@ -30349,7 +31413,6 @@ }, "tar": { "version": "6.1.11", - "dev": true, "requires": { "chownr": "^2.0.0", "fs-minipass": "^2.0.0", @@ -30361,7 +31424,6 @@ }, "tar-fs": { "version": "2.1.1", - "dev": true, "requires": { "chownr": "^1.1.1", "mkdirp-classic": "^0.5.2", @@ -30370,12 +31432,10 @@ }, "dependencies": { "chownr": { - "version": "1.1.4", - "dev": true + "version": "1.1.4" }, "pump": { "version": "3.0.0", - "dev": true, "requires": { "end-of-stream": "^1.1.0", "once": "^1.3.1" @@ -30385,7 +31445,6 @@ }, "tar-stream": { "version": "2.2.0", - "dev": true, "requires": { "bl": "^4.0.3", "end-of-stream": "^1.4.1", @@ -30511,6 +31570,14 @@ "tslib": "^1.8.1" } }, + "tunnel-agent": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", + "integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==", + "requires": { + "safe-buffer": "^5.0.1" + } + }, "tunnel-rat": { "version": "0.1.2", "requires": { @@ -30597,14 +31664,14 @@ }, "unique-filename": { "version": "1.1.1", - "dev": true, + "devOptional": true, "requires": { "unique-slug": "^2.0.0" } }, "unique-slug": { "version": "2.0.2", - "dev": true, + "devOptional": true, "requires": { "imurmurhash": "^0.1.4" } @@ -30746,6 +31813,14 @@ "version": "1.2.0", "requires": {} }, + "usehooks-ts": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/usehooks-ts/-/usehooks-ts-3.1.0.tgz", + "integrity": "sha512-bBIa7yUyPhE1BCc0GmR96VU/15l/9gP1Ch5mYdLcFBaFGQsdmXkvjV0TtOqW1yUd6VjIwDunm+flSciCQXujiw==", + "requires": { + "lodash.debounce": "^4.0.8" + } + }, "util": { "version": "0.12.4", "requires": { @@ -30927,6 +32002,11 @@ "acorn-walk": "^8.2.0" } }, + "w3c-keyname": { + "version": "2.2.8", + "resolved": "https://registry.npmjs.org/w3c-keyname/-/w3c-keyname-2.2.8.tgz", + "integrity": "sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ==" + }, "w3c-xmlserializer": { "version": "4.0.0", "requires": { diff --git a/package.json b/package.json index 35fa165..c78ec85 100644 --- a/package.json +++ b/package.json @@ -14,6 +14,7 @@ }, "dependencies": { "@ariakit/react": "^0.3.5", + "@codemirror/language": "^6.10.1", "@floating-ui/react": "^0.17.0", "@meilisearch/instant-meilisearch": "^0.8.2", "@oxide/design-system": "^1.4.0", @@ -26,7 +27,11 @@ "@sentry/remix": "^7.15.0", "@tanstack/react-query": "^4.3.9", "@types/marked": "^4.0.8", + "@types/sqlite3": "^3.1.11", + "@uiw/codemirror-theme-console": "^4.21.25", + "@uiw/react-codemirror": "^4.21.25", "classnames": "^2.3.1", + "codemirror-asciidoc": "^2.0.1", "dayjs": "^1.11.5", "fuzzysort": "^2.0.1", "highlight.js": "^11.6.0", @@ -47,7 +52,10 @@ "remix-auth-oauth2": "^1.11.1", "remix-utils": "^3.3.0", "simple-text-diff": "^1.7.0", + "sqlite": "^5.1.1", + "sqlite3": "^5.1.7", "tunnel-rat": "^0.1.2", + "usehooks-ts": "^3.1.0", "zod": "^3.22.3" }, "devDependencies": { diff --git a/tome/.gitignore b/tome/.gitignore new file mode 100644 index 0000000..a08221e --- /dev/null +++ b/tome/.gitignore @@ -0,0 +1 @@ +db/tome.db \ No newline at end of file diff --git a/tome/api/index.ts b/tome/api/index.ts new file mode 100644 index 0000000..6aaa5f1 --- /dev/null +++ b/tome/api/index.ts @@ -0,0 +1,85 @@ +import { Elysia } from 'elysia' + +import { + addTome, + deleteTome, + getTome, + listAllTomes, + listTomes, + updateTome, + type TomeBody, +} from './main' + +const API_KEY = 'abcdef' + +const ServerError = { error: 'Something went wrong' } + +new Elysia() + .onRequest(({ request, set }) => { + const apiKey = request.headers.get('x-api-key') + if (apiKey !== API_KEY) { + set.status = 401 + + return 'Not Authorized' + } + }) + .get('/user/:userId', async ({ params: { userId }, set }) => { + try { + const tomes = await listTomes(userId) + set.status = 200 + return tomes + } catch (error) { + set.status = 500 + return ServerError + } + }) + .get('/tome', async ({ set }) => { + try { + const tomes = await listAllTomes() + set.status = 200 + return tomes + } catch (error) { + set.status = 500 + return ServerError + } + }) + .get('/tome/:id', async ({ params: { id }, set }) => { + const tome = await getTome(id) + if (!tome) { + set.status = 404 + return 'Not Found' + } + return tome + }) + .post('/tome', async ({ body, set }) => { + try { + const { title, user, body: tomeBody } = body as TomeBody + const id = await addTome(title, user, tomeBody) + set.status = 201 + return { id } + } catch (error) { + set.status = 400 + return { error: (error as Error).message } + } + }) + .put('/tome/:id', async ({ params: { id }, body, set }) => { + try { + const { title, body: tomeBody } = body as TomeBody + const result = await updateTome(id, title, tomeBody) + set.status = 200 + return result + } catch (error) { + set.status = 500 + return { error: (error as Error).message } + } + }) + .delete('/tome/:id', async ({ params: { id }, set }) => { + try { + await deleteTome(id) + set.status = 204 + } catch (error) { + set.status = 500 + return ServerError + } + }) + .listen(8080) diff --git a/tome/api/main.ts b/tome/api/main.ts new file mode 100644 index 0000000..ce8a432 --- /dev/null +++ b/tome/api/main.ts @@ -0,0 +1,81 @@ +import { Database } from 'bun:sqlite' +import { nanoid } from 'nanoid' +import { z } from 'zod' + +const db = new Database('./db/tome.db') + +export interface TomeBody { + title: string + user: string + body: string +} + +const tomeCreateSchema = z.object({ + title: z.string().min(1, 'Title must not be empty'), + user: z.string().min(3, 'User must not be empty'), +}) + +export const addTome = async (title: string, user: string, body: string) => { + const validationResult = tomeCreateSchema.safeParse({ title, user }) + if (!validationResult.success) { + throw new Error(`Validation failed: ${validationResult.error.message}`) + } + + const id = nanoid(6) + const created = new Date().toISOString() + const updated = created + + const statement = await db.prepare( + 'INSERT INTO tomes (id, title, created, updated, user, body) VALUES (?, ?, ?, ?, ?, ?)', + ) + await statement.run(id, title, created, updated, user, body) + + return id +} + +export const getTome = async (id: string) => { + const query = db.query('SELECT * FROM tomes WHERE id = $id') + return query.get({ $id: id }) +} + +const tomeUpdateSchema = z.object({ + title: z.string().min(1, 'Title must not be empty'), +}) + +export const updateTome = async (id: string, title: string, body: string) => { + const validationResult = tomeUpdateSchema.safeParse({ title }) + if (!validationResult.success) { + throw new Error(`Validation failed: ${validationResult.error.message}`) + } + + const updated = new Date().toISOString() + + const statement = await db.prepare( + 'UPDATE tomes SET title = ?, updated = ?, body = ? WHERE id = ?', + ) + await statement.run(title, updated, body, id) +} + +export const deleteTome = async (id: string) => { + const statement = await db.prepare('DELETE FROM tomes WHERE id = ?') + await statement.run(id) +} + +export const listTomes = async (userId: string) => { + const query = db.query('SELECT * FROM tomes WHERE user = $userId') + const tomes = await query.all({ $userId: userId }) + + // We only want the first 20 lines so we're not sending a huge response + const trimmedTomes = tomes.map((tome) => ({ + ...(tome as TomeBody), + body: (tome as TomeBody).body.split('\n').slice(0, 20).join('\n'), + })) + + return trimmedTomes +} + +export const listAllTomes = async () => { + const query = db.query('SELECT * FROM tomes') + + return query.all() +} diff --git a/tome/bun.lockb b/tome/bun.lockb new file mode 100755 index 0000000000000000000000000000000000000000..51d5f21d65af49ca7ba88982b2c63b37efb3c7a1 GIT binary patch literal 8290 zcmeHMd011&7QewzmD+;Hszm~bE7>6;A}U&?`aMN)K?M}U1%n}pNkG9`zy+zhfJ?1b zKvC;b0oRIHv~H+aR|FT(s&zwAAMO?2IX4-&+E!xU`=jssJl}USckayZcg{UCb7t;| zn~N}3rxlJ@sD!jCjyFD5WdRposg_TSR48eFq*@yjuII;zEX*j1s^0BmeLSdKnA5B( z_x!VMYl?e1SH)YnKTh_~u@1kkEIi{3oj`021=Z}WGRau)O*&9|bqtLU6Vwq*n<`wT zR=}+j)LK$h60MBaDZ(i#hC@-GKs^*nD=7Oy=?G<4<8!%MJy8Kn?V*mi+kkHlWduDw zHVW=zbPPRZV|@OkiJ|>2lpjMsC6}UFKxqN(a3zs`h{F12To07ab+Mcl%pW%=w@>Ev=M_E5XIWN$dtJO>+~k{OohDZLE^TGiCw23QpR;Y& ze|?{BAKtNl`q{NN#6kDXomZJ%^|ao8>*2v^nw1w$oLw-3^1Rmi*x|L?51!v~VPU}3 zs;H7w>wB(ijxUd29eqZzSU7rq`yD;HHCOu{zB^<^!Lg4z6%G_7*N}dz-IF}J%%aD` z>;G4Rql_YeO9<}`?xF190{56C1Ro8(JppfSL^U-DO;H5@9rPby)Q|71Pf%qgnPNhbcnvW8uah@WlV` zi_Zc)jvwm$$P~rKK1Ba{qkeqMgA3P_DMIk};PCFCAJ(49zC(V39}0L0;BnsI*uC%k znF$~dz$4$^8NXb>`v4xXc%coZ2(iBw0O)^=pg4x;2UCRLCGf(iAN>yTjcRKO82S+W z6u|ceJnC)@7nw(m(GdKPfR`EHA2GO&Oc8>w20V^G`VOU?p=k7%;ALR2uhD*>GTDAq zfU%0iD|U%vh~t3IV2@!0OSIA2P&KK<{;iDtu|%73L&WD8Yq7M43k6H8JHUnga4d-Y zf7n@TS&J|5zZBr(PzGK$H8)kb*LA-4k_#a(>F7SE9II}~Bs(k|DmS>a*jV~(-Si5d zg;Xy8d35zl%}U3T(AaYIp`c;;x$93iNq63K+}dshiBZa{k*DpI2+n3~@ZI%$B&g=cEOJ1kY z$vG@ub{yckxm}W7T)uDjE!)abb3*5xb==&=yD}l%VdAm4trK!nZ+fr0lXE^$8vNO& z*{6@_lU9aIzuC{B|2Mzxx_+$VsdI@-S-d#MiE#ae)x$&16a;#?Dek!2)orvGx9@mf zX?lKPOm$0_hZB7Bnvu6^@6Mg}nyUjhC(Eomq`cl# zm+fgcszX)hx&OX%RLA0F?Wr^n(M;5Sn$$CX!aDz8kjQ%t%sLkmJ zrQO3Zl`&bbIQp#e1GN=?V@1kyB99T=RaZvF6^!XADX)9br~j^|IqR}cJoOZLcOY(}sHR($h z@9Z_K#HV>kaIQzw8mn@f7lDIg2Rz{kH6fiJAewY!Xgp@v`>| z?n&3(&+%HSaFJ;g6 z4L)6*y|ibavt8V`EZ;uw^iL~d`yMPT@bc|9`x1-yQ{n_Dxq)qymhL)p;!fJgDQ1Iy z+&MXF-}?-B-oi^E5{L<{jmWur4 z_$@=?nI)TdUxf9LBNvyZbw2GJ{ouy!i^Hq#TuWV7soAN$BB>iVBF(LtW$lkohpg!~ zJZof*OKRScF`3P*XQU+d*f*8ya5b-n#S7m|^}^Nbe4;*cU-8@MezfIRuNBWXUa<_k zJa>?J%<1-Zm${O;vo_gvv(=Lo)Me(F zX}h($~IM7v+k}IJ5VIy`~E8Q{(AS6%5nTv>#6*;^`C# zv^vs*=gA9KDxy^C$cB?zZJj~zWpSUvctK(Ysb4j!5#fg-h=p&gRN@D0@8Cra+$Zqc z1LGH->GAxHoVahH1$eH*GZ>zi@N9$U5IjrcxffO8evIFukU4a z)%!;Lrq4lV63Lrdal*U=B7w+Y3AT_7Yco!m7+S;<##fkACOKC#Y!M5j6h*SsBn!;6 z07hD$$0m7VXpsWOvtidJnPjv2Pfn5xC%I+@Bar}zWV1=O*o@;TkRlm%8!ohs`6|XX%Pz~4OxAXHOCgvk1hpakB<#n&5Y1r>mfOQl0!G+AQz~iNS2>u z(akttNx*1OME(aLd3I>=7I+E7qCdfOW~`zp@?QbT#RJAmAd$g1;B0?$hM)_ivcG4+ zZR1?-XiWON8CrC7OdPEhd_F}_YgOUOAiY+hit^)$8>YKAYr5-oa>n|iS2j7?j72v1 zhYvoptXSjwz-iJRy-r7KfiLtdFL`L>z~`M|@xl5`n(6Izf-~jQ+F+v%$Bh5*K-__~ zCH||WMd{-;v`(&7X!O09TOs_bgnukC6~mO|Ys2+4U#W;u=qKp)8l8_&sHLM6I=wbt zpwh%d>jY|Tln@q!kdME7sQ}l2Kp&NW#v2a3f@s}4u!cSWH{9@X{&YoxcVPZt8^HhY z3~Yflj7&;xEMFcjpFlTC*}%gr3n`*|=@crtGF+h*qF=_Vr_gb77FU{Ixz;W|A( zf|jczXnw3#!I!IJG-?%eU>YXH(%N`%7L2-DL#x6y3WIasVaOZ(z@9hie8b9X#654T z29~!U@i8b$OyT*ncO!&<)&|V5Y8zpG!-eQ{wo92Q&=`Xu`T^8%!xsr4^fjXO4-f-1zDcTc?fnFPrCd%huxHv(n01%NRnpV<-`LB!~wdyJ0H1jt7E)-PLt)Za6M zY?#A_^{s+gYFz4*-?b*|`vBWy9p4aa8?nHMK^1_)f^Q7pj8kJ(5ej+Zv!C4!XxLSl z77)L^Ofv&~VDtnt9UGhJ^)54l7(?hDdCPzVBGO42%h=z|&9x%RLQ-8#kwa GT=5_CrgsVe literal 0 HcmV?d00001 diff --git a/tome/db/drop.sh b/tome/db/drop.sh new file mode 100755 index 0000000..4e82805 --- /dev/null +++ b/tome/db/drop.sh @@ -0,0 +1,10 @@ +#!/bin/bash + +DATABASE="tome.db" + +if [ -f "$DATABASE" ]; then + rm "$DATABASE" + echo "Database $DATABASE destroyed." +else + echo "Database $DATABASE does not exist." +fi \ No newline at end of file diff --git a/tome/db/init.sh b/tome/db/init.sh new file mode 100755 index 0000000..c351e44 --- /dev/null +++ b/tome/db/init.sh @@ -0,0 +1,9 @@ + #!/bin/bash + +DATABASE="tome.db" + +if [ ! -f "$DATABASE" ]; then + sqlite3 "$DATABASE" < "seed.sql" && echo "Database $DATABASE initialized." +else + echo "Database $DATABASE already exists." +fi \ No newline at end of file diff --git a/tome/db/seed.sql b/tome/db/seed.sql new file mode 100644 index 0000000..22c900f --- /dev/null +++ b/tome/db/seed.sql @@ -0,0 +1,16 @@ +BEGIN TRANSACTION; + +CREATE TABLE IF NOT EXISTS tomes ( + id TEXT PRIMARY KEY, + title TEXT, + created TEXT, + updated TEXT, + user TEXT, + body TEXT +); + +INSERT INTO tomes (id, title, created, updated, user, body) VALUES +('abcdef', 'First Tome', '2024-04-12 12:00:00', '2024-04-12 12:00:00', 'a61688eec17b', 'In a time of enchantment when the moon played hide and seek with the stars, the mystical lands have awoken.'), +('defghi', 'Second Tome', '2024-04-13 14:15:00', '2024-04-13 14:15:00', '6e1f7633aae1', 'The ancient runes whispered tales of forgotten magic, painting images of sparkling fountains and palaces of precious stones.'); + +COMMIT; \ No newline at end of file diff --git a/tome/index.ts b/tome/index.ts new file mode 100644 index 0000000..f67b2c6 --- /dev/null +++ b/tome/index.ts @@ -0,0 +1 @@ +console.log("Hello via Bun!"); \ No newline at end of file diff --git a/tome/package.json b/tome/package.json new file mode 100644 index 0000000..f785a49 --- /dev/null +++ b/tome/package.json @@ -0,0 +1,18 @@ +{ + "name": "tome", + "module": "index.ts", + "type": "module", + "devDependencies": { + "@types/bun": "^1.0.12" + }, + "peerDependencies": { + "typescript": "^5.0.0" + }, + "dependencies": { + "bun-types": "^1.1.3", + "elysia": "^1.0.13", + "elysia-rate-limit": "^3.1.4", + "nanoid": "^5.0.7", + "zod": "^3.22.4" + } +} \ No newline at end of file diff --git a/tome/tsconfig.json b/tome/tsconfig.json new file mode 100644 index 0000000..666c94f --- /dev/null +++ b/tome/tsconfig.json @@ -0,0 +1,29 @@ +{ + "compilerOptions": { + // Enable latest features + "lib": ["ESNext", "DOM"], + "target": "ESNext", + "module": "ESNext", + "moduleDetection": "force", + "jsx": "react-jsx", + "allowJs": true, + + // Bundler mode + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "verbatimModuleSyntax": true, + "noEmit": true, + + // Best practices + "strict": true, + "skipLibCheck": true, + "noFallthroughCasesInSwitch": true, + + // Some stricter flags (disabled by default) + "noUnusedLocals": false, + "noUnusedParameters": false, + "noPropertyAccessFromIndexSignature": false, + + "types": ["bun-types"] + } +} From eecdbe0d92fe293c8247c137d005a29a8e49448e Mon Sep 17 00:00:00 2001 From: Benjamin Leonard Date: Tue, 30 Apr 2024 14:11:18 +0100 Subject: [PATCH 02/33] Pre-demo commit --- .../{Document.tsx => CustomDocument.tsx} | 6 - .../AsciidocBlocks/MinimalDocument.tsx | 15 + app/components/AsciidocBlocks/Section.tsx | 4 +- app/components/AsciidocBlocks/index.ts | 6 +- app/components/Header.tsx | 6 + app/components/Modal.tsx | 32 ++ app/components/TextInput.tsx | 118 +++++ app/components/{tome => note}/EditorTheme.ts | 0 app/components/note/NoteForm.tsx | 406 ++++++++++++++++++ app/components/note/Sidebar.tsx | 123 ++++++ .../{tome => note}/TypingIndicator.tsx | 0 app/components/spinner.css | 4 +- app/components/tome/Sidebar.tsx | 72 ---- app/components/tome/TomeForm.tsx | 215 ---------- app/root.tsx | 54 ++- app/routes/notes.$id.publish.tsx | 34 ++ ..._.$id.delete.tsx => notes_.$id.delete.tsx} | 4 +- app/routes/{tome_.$id.tsx => notes_.$id.tsx} | 29 +- app/routes/notes_.$id_.edit.tsx | 96 +++++ .../{tome_._index.tsx => notes_._index.tsx} | 11 +- app/routes/notes_.create-rfd.tsx | 85 ++++ app/routes/{tome_.new.tsx => notes_.new.tsx} | 4 +- app/routes/{tome_.tsx => notes_.tsx} | 20 +- app/routes/rfd.$slug.tsx | 12 +- app/routes/tome_.$id_.edit.tsx | 76 ---- app/services/rfd.server.ts | 5 +- app/styles/index.css | 2 +- notes/.gitignore | 1 + notes/api/index.ts | 111 +++++ notes/api/main.ts | 95 ++++ {tome => notes}/bun.lockb | Bin {tome => notes}/db/drop.sh | 2 +- {tome => notes}/db/init.sh | 2 +- notes/db/note.db | 0 notes/db/seed.sql | 17 + notes/index.ts | 3 + {tome => notes}/package.json | 4 +- {tome => notes}/tsconfig.json | 0 package-lock.json | 53 ++- package.json | 6 +- tome/.gitignore | 1 - tome/api/index.ts | 85 ---- tome/api/main.ts | 81 ---- tome/db/seed.sql | 16 - tome/index.ts | 1 - 45 files changed, 1269 insertions(+), 648 deletions(-) rename app/components/AsciidocBlocks/{Document.tsx => CustomDocument.tsx} (96%) create mode 100644 app/components/AsciidocBlocks/MinimalDocument.tsx create mode 100644 app/components/TextInput.tsx rename app/components/{tome => note}/EditorTheme.ts (100%) create mode 100644 app/components/note/NoteForm.tsx create mode 100644 app/components/note/Sidebar.tsx rename app/components/{tome => note}/TypingIndicator.tsx (100%) delete mode 100644 app/components/tome/Sidebar.tsx delete mode 100644 app/components/tome/TomeForm.tsx create mode 100644 app/routes/notes.$id.publish.tsx rename app/routes/{tome_.$id.delete.tsx => notes_.$id.delete.tsx} (88%) rename app/routes/{tome_.$id.tsx => notes_.$id.tsx} (81%) create mode 100644 app/routes/notes_.$id_.edit.tsx rename app/routes/{tome_._index.tsx => notes_._index.tsx} (73%) create mode 100644 app/routes/notes_.create-rfd.tsx rename app/routes/{tome_.new.tsx => notes_.new.tsx} (90%) rename app/routes/{tome_.tsx => notes_.tsx} (76%) delete mode 100644 app/routes/tome_.$id_.edit.tsx create mode 100644 notes/.gitignore create mode 100644 notes/api/index.ts create mode 100644 notes/api/main.ts rename {tome => notes}/bun.lockb (100%) rename {tome => notes}/db/drop.sh (88%) rename {tome => notes}/db/init.sh (89%) create mode 100644 notes/db/note.db create mode 100644 notes/db/seed.sql create mode 100644 notes/index.ts rename {tome => notes}/package.json (93%) rename {tome => notes}/tsconfig.json (100%) delete mode 100644 tome/.gitignore delete mode 100644 tome/api/index.ts delete mode 100644 tome/api/main.ts delete mode 100644 tome/db/seed.sql delete mode 100644 tome/index.ts diff --git a/app/components/AsciidocBlocks/Document.tsx b/app/components/AsciidocBlocks/CustomDocument.tsx similarity index 96% rename from app/components/AsciidocBlocks/Document.tsx rename to app/components/AsciidocBlocks/CustomDocument.tsx index 8c456bb..5d6b8a5 100644 --- a/app/components/AsciidocBlocks/Document.tsx +++ b/app/components/AsciidocBlocks/CustomDocument.tsx @@ -173,9 +173,3 @@ export const CustomDocument = ({ document }: { document: AdocTypes.Document }) = ) } - -export const MinimalDocument = ({ document }: { document: AdocTypes.Document }) => ( -
- -
-) diff --git a/app/components/AsciidocBlocks/MinimalDocument.tsx b/app/components/AsciidocBlocks/MinimalDocument.tsx new file mode 100644 index 0000000..137cf55 --- /dev/null +++ b/app/components/AsciidocBlocks/MinimalDocument.tsx @@ -0,0 +1,15 @@ +/* + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, you can obtain one at https://mozilla.org/MPL/2.0/. + * + * Copyright Oxide Computer Company + */ + +import { Content, type AdocTypes } from '@oxide/react-asciidoc' + +export const MinimalDocument = ({ document }: { document: AdocTypes.Document }) => ( +
+ +
+) diff --git a/app/components/AsciidocBlocks/Section.tsx b/app/components/AsciidocBlocks/Section.tsx index fae9873..dd50e22 100644 --- a/app/components/AsciidocBlocks/Section.tsx +++ b/app/components/AsciidocBlocks/Section.tsx @@ -25,8 +25,8 @@ const Section = ({ node }: { node: SectionType }) => { let sectNum = node.getSectionNumeral() sectNum = sectNum === '.' ? '' : sectNum - const hasSectLinks = docAttrs['sectlinks'] === true - const hasSectNums = docAttrs['sectnums'] === true + const hasSectLinks = docAttrs['sectlinks'] === true || docAttrs['sectlinks'] === 'true' + const hasSectNums = docAttrs['sectnums'] === true || docAttrs['sectnums'] === 'true' const sectNumLevels = docAttrs['sectnumlevels'] ? parseInt(docAttrs['sectnumlevels']) : 3 diff --git a/app/components/AsciidocBlocks/index.ts b/app/components/AsciidocBlocks/index.ts index bd3d0c5..d934ca4 100644 --- a/app/components/AsciidocBlocks/index.ts +++ b/app/components/AsciidocBlocks/index.ts @@ -9,7 +9,6 @@ import { AsciiDocBlocks } from '@oxide/design-system/components/dist' import { getText, type AdocTypes, type Options } from '@oxide/react-asciidoc' -import { CustomDocument, MinimalDocument, ui } from './Document' import Image from './Image' import Listing from './Listing' import Section from './Section' @@ -22,7 +21,6 @@ export let opts: Options = { listing: Listing, section: Section, }, - customDocument: CustomDocument, } export const renderWithBreaks = (text: string): string => { @@ -46,7 +44,7 @@ const QUOTE_TAGS: {[key: string]: [string, string, boolean?]} = { const chop = (str: string) => str.substring(0, str.length - 1) -const convertInlineQuoted = (node: AdocTypes.Inline) => { +export const convertInlineQuoted = (node: AdocTypes.Inline) => { const type = node.getType() const quoteTag = QUOTE_TAGS[type] const [open, close, tag] = quoteTag || ['', ''] @@ -68,5 +66,3 @@ const convertInlineQuoted = (node: AdocTypes.Inline) => { return `${open}${text}${close}` } } - -export { ui, convertInlineQuoted, MinimalDocument } diff --git a/app/components/Header.tsx b/app/components/Header.tsx index f1a726b..8b1cacd 100644 --- a/app/components/Header.tsx +++ b/app/components/Header.tsx @@ -79,6 +79,12 @@ export default function Header({ currentRfd }: { currentRfd?: RfdItem }) { setOpen(false)} /> + + + {user ? ( diff --git a/app/components/Modal.tsx b/app/components/Modal.tsx index 236e51f..7978af1 100644 --- a/app/components/Modal.tsx +++ b/app/components/Modal.tsx @@ -7,6 +7,7 @@ */ import { Dialog, DialogDismiss, type DialogStore } from '@ariakit/react' +import { Button } from '@oxide/design-system' import Icon from '~/components/Icon' @@ -14,10 +15,16 @@ const Modal = ({ dialogStore, title, children, + onSubmit, + isLoading = false, + disabled = false, }: { dialogStore: DialogStore title: string children: React.ReactElement + onSubmit?: () => void + isLoading?: boolean + disabled?: boolean }) => { return ( <> @@ -34,6 +41,31 @@ const Modal = ({
{children}
+ + {onSubmit && ( +
+
+ + +
+
+ )} ) diff --git a/app/components/TextInput.tsx b/app/components/TextInput.tsx new file mode 100644 index 0000000..6ae94cf --- /dev/null +++ b/app/components/TextInput.tsx @@ -0,0 +1,118 @@ +/* + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, you can obtain one at https://mozilla.org/MPL/2.0/. + * + * Copyright Oxide Computer Company + */ +import cn from 'classnames' +import React, { useEffect } from 'react' + +/** + * This is a little complicated. We only want to allow the `rows` prop if + * `as="textarea"`. But the derivatives of `TextField`, like `NameField`, etc., + * can't use `as` no matter what. So we have them only use `TextFieldBaseProps`, + * which doesn't know about `as`. But `TextField` itself secretly takes + * `TextFieldBaseProps & TextAreaProps`. + */ +export type TextAreaProps = + | { + as: 'textarea' + /** Only used with `as="textarea"` */ + rows?: number + } + | { + as?: never + rows?: never + } + +// would prefer to refer directly to the props of Field and pass them all +// through, but couldn't get it to work. FieldAttributes is closest but +// it makes a bunch of props required that should be optional. Instead we simply +// take the props of an input field (which are part of the Field props) and +// manually tack on validate. +export type TextInputBaseProps = React.ComponentPropsWithRef<'input'> & { + // error is used to style the wrapper, also to put aria-invalid on the input + error?: boolean + disabled?: boolean + className?: string + fieldClassName?: string +} + +export const TextInput = React.forwardRef< + HTMLInputElement, + TextInputBaseProps & TextAreaProps +>( + ( + { + type = 'text', + error, + className, + disabled, + fieldClassName, + as: asProp, + ...fieldProps + }, + ref, + ) => { + const Component = asProp || 'input' + return ( +
+ +
+ ) + }, +) + +TextInput.displayName = 'TextInput' + +type HintProps = { + // ID required as a reminder to pass aria-describedby on TextField + id: string + children: React.ReactNode + className?: string +} + +/** + * Pass id here and include that ID in aria-describedby on the TextField + */ +export const TextInputHint = ({ id, children, className }: HintProps) => ( +
_a]:underline hover:[&_>_a]:text-default', + className, + )} + > + {children} +
+) + +export const TextInputError = ({ children }: { children: string }) => { + return
{children}
+} diff --git a/app/components/tome/EditorTheme.ts b/app/components/note/EditorTheme.ts similarity index 100% rename from app/components/tome/EditorTheme.ts rename to app/components/note/EditorTheme.ts diff --git a/app/components/note/NoteForm.tsx b/app/components/note/NoteForm.tsx new file mode 100644 index 0000000..4891e47 --- /dev/null +++ b/app/components/note/NoteForm.tsx @@ -0,0 +1,406 @@ +/* + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, you can obtain one at https://mozilla.org/MPL/2.0/. + * + * Copyright Oxide Computer Company + */ +import { useDialogStore, type DialogStore } from '@ariakit/react' +import { EditorView } from '@codemirror/view' +import Asciidoc, { asciidoctor } from '@oxide/react-asciidoc' +import * as Dropdown from '@radix-ui/react-dropdown-menu' +import { useFetcher, useLoaderData } from '@remix-run/react' +import CodeMirror, { type ReactCodeMirrorRef } from '@uiw/react-codemirror' +import cn from 'classnames' +import dayjs from 'dayjs' +import { useCallback, useEffect, useMemo, useRef, useState } from 'react' + +import { opts } from '~/components/AsciidocBlocks' +import { DropdownItem, DropdownLink, DropdownMenu } from '~/components/Dropdown' +import Icon from '~/components/Icon' +import { editorTheme } from '~/components/note/EditorTheme' +import { useDebounce } from '~/hooks/use-debounce' +import { useRootLoaderData } from '~/root' + +import { MinimalDocument } from '../AsciidocBlocks/MinimalDocument' +import Modal from '../Modal' +import Spinner from '../Spinner' +import { TextInput } from '../TextInput' +import { SidebarIcon } from './Sidebar' + +const ad = asciidoctor() + +type EditorStatus = 'idle' | 'unsaved' | 'saving' | 'saved' | 'error' + +export const NoteForm = ({ + initialTitle = '', + initialBody = '', + updated, + published, + onSave, + fetcher, + sidebarOpen, + setSidebarOpen, +}: { + initialTitle?: string + initialBody?: string + updated: string + published: 1 | 0 + onSave: (title: string, body: string) => void + fetcher: any + sidebarOpen: boolean + setSidebarOpen: (bool: boolean) => void +}) => { + const [status, setStatus] = useState('idle') + const [body, setBody] = useState(initialBody) + const [title, setTitle] = useState(initialTitle) + const inputRef = useRef(null) + + const debouncedBody = useDebounce(body, 750) + const debouncedTitle = useDebounce(title, 750) + + const createRfdDialog = useDialogStore() + + useEffect(() => { + const hasChanges = body !== initialBody || title !== initialTitle + + const hasError = fetcher.data?.status === 'error' + + if (hasError && status !== 'error') { + setStatus('error') + } + + const isSaving = fetcher.state === 'submitting' + const isSaved = fetcher.state === 'idle' && status === 'saving' + + if (!hasChanges && (isSaving || isSaved)) { + if (isSaving) { + setStatus('saving') + } else if (isSaved) { + setStatus('saved') + } + } + + if (debouncedBody === body && debouncedTitle === title && status === 'unsaved') { + onSave(title, body) + setStatus('saving') + } + }, [ + body, + title, + initialBody, + initialTitle, + debouncedBody, + debouncedTitle, + fetcher, + status, + onSave, + ]) + + // Handle window resizing + const [leftPaneWidth, setLeftPaneWidth] = useState(50) // Initial width in percentage + + const handleMouseDown = useCallback( + (e: React.MouseEvent) => { + e.preventDefault() + const startX = e.clientX + const startWidth = leftPaneWidth + + const handleMouseMove = (moveEvent: MouseEvent) => { + const dx = moveEvent.clientX - startX + const newWidth = + (((startWidth / 100) * window.innerWidth + dx) * 100) / window.innerWidth + setLeftPaneWidth(Math.max(20, Math.min(80, newWidth))) + } + + const handleMouseUp = () => { + document.removeEventListener('mousemove', handleMouseMove) + document.removeEventListener('mouseup', handleMouseUp) + } + + document.addEventListener('mousemove', handleMouseMove) + document.addEventListener('mouseup', handleMouseUp) + }, + [leftPaneWidth], + ) + + const doc = useMemo(() => { + return ad.load(body, { + standalone: true, + sourcemap: true, + attributes: { + sectnums: false, + }, + }) + }, [body]) + + return ( + <> + +
+
+ +
+ + {title ? title : 'Title...'} + + { + setStatus('unsaved') + setTitle(el.target.value) + }} + name="title" + placeholder="Title..." + required + className="absolute left-1 w-full bg-transparent p-0 text-sans-xl text-default placeholder:text-quaternary focus:outline-none" + /> +
+ + + + {fetcher.data?.status === 'error' && ( +
{fetcher.data.error}
+ )} +
+ + +
+
+
{ + if ((el.target as HTMLElement).id === 'code_mirror_wrapper') { + inputRef.current && + inputRef.current.editor && + inputRef.current.editor.focus() + } + }} + > + + + { + setStatus('unsaved') + setBody(val) + }} + theme={editorTheme} + className="!normal-case !tracking-normal text-mono-md" + readOnly={false} + basicSetup + autoFocus + extensions={[EditorView.lineWrapping]} + /> +
+
+
+
+
+ +
+
+ + + + ) +} + +const TypingIndicator = () => ( +
+ + + +
+) + +const SavingIndicator = ({ + status, + updated, +}: { + status: EditorStatus + updated: string +}) => { + return ( +
+ {dayjs(updated).format('MMM D YYYY, h:mm A')} + {status === 'unsaved' ? ( + + ) : status === 'error' ? ( + + ) : status === 'saved' ? ( + + ) : status === 'saving' ? ( + + ) : ( + + )} +
+ ) +} + +const MoreDropdown = ({ dialog }: { dialog: DialogStore }) => { + const note = useLoaderData() + const fetcher = useFetcher() + + const handleDelete = () => { + if (window.confirm('Are you sure you want to delete this note?')) { + fetcher.submit( + { id: note.id }, + { + method: 'post', + action: `/notes/${note.id}/delete`, + encType: 'application/x-www-form-urlencoded', + }, + ) + } + } + + const handlePublish = async () => { + const isPublished = note.published === 1 + const confirmationMessage = isPublished + ? 'Are you sure you want to unpublish this note?' + : 'Are you sure you want to publish this note?' + + if (window.confirm(confirmationMessage)) { + fetcher.submit( + { publish: isPublished ? 0 : 1 }, + { + method: 'post', + action: `/notes/${note.id}/publish`, + encType: 'application/json', + }, + ) + } + } + + return ( + + + + + + + View + + {note.published ? 'Unpublish' : 'Publish'} + + { + dialog.setOpen(true) + }} + > + Create RFD from note + + + Delete + + + + ) +} + +const CreateRfdModal = ({ + initialTitle = '', + dialog, + body, +}: { + initialTitle?: string + dialog: DialogStore + body: string +}) => { + const [title, setTitle] = useState(initialTitle) + const newRfdNumber = useRootLoaderData().newRfdNumber + const fetcher = useFetcher() + + const handleSubmit = () => { + fetcher.submit( + { title, body }, + { + method: 'post', + action: `/notes/create-rfd`, + encType: 'application/json', + }, + ) + } + + const formDisabled = fetcher.state !== 'idle' + + return ( + + + setTitle(el.target.value)} + disabled={formDisabled} + /> + +
+          {`:state: prediscussion 
+:discussion:
+:authors:
+
+`}
+          = RFD {newRfdNumber} {title ? title : '{title}'}
+          {`
+
+`}
+          {body}
+          
+
+ {fetcher.type === 'done' && !fetcher.data.ok && fetcher.data.message && ( +
+ {fetcher.data.message} +
+ )} +
+
+ ) +} diff --git a/app/components/note/Sidebar.tsx b/app/components/note/Sidebar.tsx new file mode 100644 index 0000000..d58e721 --- /dev/null +++ b/app/components/note/Sidebar.tsx @@ -0,0 +1,123 @@ +import { buttonStyle } from '@oxide/design-system' +import { Link, NavLink, useMatches } from '@remix-run/react' +import cn from 'classnames' +import { type ReactNode } from 'react' + +import Icon from '~/components/Icon' +import { type NoteItem } from '~/routes/note_' + +const navLinkStyles = ({ isActive }: { isActive: boolean }) => { + const activeStyle = isActive + ? 'bg-accent-secondary hover:!bg-accent-secondary-hover text-accent' + : null + return `block text-sans-md text-secondary hover:bg-hover px-2 py-1 rounded flex items-center group justify-between ${activeStyle}` +} + +const Divider = ({ className }: { className?: string }) => ( +
+) + +export const SidebarIcon = () => ( + + + +) + +export const Sidebar = () => { + const matches = useMatches() + + const notes = matches[1].data.notes as NoteItem[] + + return ( + + ) +} + +const LinkSection = ({ label, children }: { label: string; children: ReactNode }) => ( +
+
{label}
+
    {children}
+
+) diff --git a/app/components/tome/TypingIndicator.tsx b/app/components/note/TypingIndicator.tsx similarity index 100% rename from app/components/tome/TypingIndicator.tsx rename to app/components/note/TypingIndicator.tsx diff --git a/app/components/spinner.css b/app/components/spinner.css index 6ff3815..17e3fcc 100644 --- a/app/components/spinner.css +++ b/app/components/spinner.css @@ -47,11 +47,11 @@ } } -.tome .spinner .bg { +.note .spinner .bg { stroke: var(--base-neutral-900); } -.tome .spinner .path { +.note .spinner .path { stroke: var(--content-accent); } diff --git a/app/components/tome/Sidebar.tsx b/app/components/tome/Sidebar.tsx deleted file mode 100644 index 1a6bd9d..0000000 --- a/app/components/tome/Sidebar.tsx +++ /dev/null @@ -1,72 +0,0 @@ -import { Button } from '@oxide/design-system' -import { NavLink, useFetcher, useLoaderData, useMatches } from '@remix-run/react' -import cn from 'classnames' - -import Icon from '~/components/Icon' -import { type TomeItem } from '~/routes/tome_' - -const navLinkStyles = ({ isActive }: { isActive: boolean }) => { - const activeStyle = isActive - ? 'bg-accent-secondary hover:!bg-accent-secondary-hover text-accent' - : null - return `block text-sans-md text-secondary hover:bg-hover px-2 py-1 rounded flex items-center group justify-between ${activeStyle}` -} - -const Divider = ({ className }: { className?: string }) => ( -
-) - -export const Sidebar = () => { - const fetcher = useFetcher() - const matches = useMatches() - - const tomes = matches[1].data - // const defaultClass = hideOnDesktop ? 'hidden' : 'hidden 800:flex' - // const navOpenClass = hideOnDesktop ? 'flex' : '' - - return ( - - ) -} diff --git a/app/components/tome/TomeForm.tsx b/app/components/tome/TomeForm.tsx deleted file mode 100644 index 3bc7a90..0000000 --- a/app/components/tome/TomeForm.tsx +++ /dev/null @@ -1,215 +0,0 @@ -/* - * This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, you can obtain one at https://mozilla.org/MPL/2.0/. - * - * Copyright Oxide Computer Company - */ -import { EditorView } from '@codemirror/view' -import Asciidoc, { asciidoctor } from '@oxide/react-asciidoc' -import * as Dropdown from '@radix-ui/react-dropdown-menu' -import { useActionData, useFetcher, useLoaderData } from '@remix-run/react' -import CodeMirror, { type ReactCodeMirrorRef } from '@uiw/react-codemirror' -import dayjs from 'dayjs' -import { useEffect, useMemo, useRef, useState } from 'react' - -import { MinimalDocument, opts } from '~/components/AsciidocBlocks' -import { DropdownItem, DropdownLink, DropdownMenu } from '~/components/Dropdown' -import Icon from '~/components/Icon' -import { editorTheme } from '~/components/tome/EditorTheme' -import { useDebounce } from '~/hooks/use-debounce' - -import Spinner from '../Spinner' - -const ad = asciidoctor() - -opts.customDocument = MinimalDocument - -type EditorStatus = 'idle' | 'unsaved' | 'saving' | 'saved' - -export const TomeForm = ({ - initialTitle = '', - initialBody = '', - updated, - onSave, -}: { - initialTitle?: string - initialBody?: string - updated: string - onSave: (title: string, body: string) => void -}) => { - const fetcher = useFetcher() - const [status, setStatus] = useState('idle') - const actionData = useActionData() - const [body, setBody] = useState(initialBody) - const [title, setTitle] = useState(initialTitle) - const inputRef = useRef(null) - - const debouncedBody = useDebounce(body, 750) - const debouncedTitle = useDebounce(title, 750) - - useEffect(() => { - const hasChanges = body !== initialBody || title !== initialTitle - const isSaving = fetcher.state === 'submitting' - const isSaved = fetcher.state === 'idle' && status === 'saving' - - if (!hasChanges && (isSaving || isSaved)) { - if (isSaving) { - setStatus('saving') - } else if (isSaved) { - setStatus('saved') - } - } - - if (debouncedBody === body && debouncedTitle === title && status === 'unsaved') { - onSave(title, body) - setStatus('saving') - } - }, [ - body, - title, - initialBody, - initialTitle, - debouncedBody, - debouncedTitle, - fetcher.state, - status, - onSave, - ]) - - const doc = useMemo(() => { - return ad.load(body, { - standalone: true, - sourcemap: true, - attributes: { - sectnums: false, - }, - }) - }, [body]) - - return ( - -
-
- - -
- - {title ? title : 'Title...'} - - { - setStatus('unsaved') - setTitle(el.target.value) - }} - name="title" - placeholder="Title..." - required - className="absolute left-1 w-full bg-transparent p-0 text-sans-xl text-default placeholder:text-quaternary focus:outline-none" - /> -
- - -
- - -
-
-
{ - if ((el.target as HTMLElement).id === 'code_mirror_wrapper') { - inputRef.current && inputRef.current.editor && inputRef.current.editor.focus() - } - }} - > - - { - setStatus('unsaved') - setBody(val) - }} - theme={editorTheme} - className="!normal-case !tracking-normal text-mono-md" - readOnly={false} - basicSetup - autoFocus - extensions={[EditorView.lineWrapping]} - /> -
-
- -
- {actionData?.error &&
{actionData.error}
} -
-
- ) -} - -const TypingIndicator = () => ( -
- - - -
-) - -const SavingIndicator = ({ - status, - updated, -}: { - status: EditorStatus - updated: string -}) => { - return ( -
- {dayjs(updated).format('MMM D YYYY, h:mm A')} - {status === 'unsaved' ? ( - - ) : status === 'saved' ? ( - - ) : status === 'saving' ? ( - - ) : ( - - )} -
- ) -} - -const MoreDropdown = () => { - const tome = useLoaderData() - const fetcher = useFetcher() // Initialize the fetcher - - const handleDelete = () => { - if (window.confirm('Are you sure you want to delete this tome?')) { - fetcher.submit( - { id: tome.id }, - { - method: 'post', - action: `/tome/${tome.id}/delete`, - encType: 'application/x-www-form-urlencoded', - }, - ) - } - } - - return ( - - - - - - - View - - Delete - - - - ) -} diff --git a/app/root.tsx b/app/root.tsx index 0200b93..4a7219d 100644 --- a/app/root.tsx +++ b/app/root.tsx @@ -22,6 +22,7 @@ import { ScrollRestoration, useCatch, useLoaderData, + useMatches, useRouteLoaderData, } from '@remix-run/react' import { withSentry } from '@sentry/remix' @@ -47,6 +48,8 @@ export const meta: V2_MetaFunction = () => { return [{ title: 'RFD / Oxide' }] } +export const shouldRevalidate = () => false + export const links: LinksFunction = () => [{ rel: 'stylesheet', href: styles }] export const loader = async ({ request }: LoaderArgs) => { @@ -98,29 +101,34 @@ export const Layout = ({ }: { children: React.ReactNode theme?: string -}) => ( - - - - - - - - - - {/* Use plausible analytics only on Vercel */} - {process.env.NODE_ENV === 'production' && ( -