Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 34 additions & 0 deletions app/components/Modal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
*/

import { Dialog, DialogDismiss, type DialogStore } from '@ariakit/react'
import { Button } from '@oxide/design-system'
import cn from 'classnames'

import Icon from '~/components/Icon'
Expand All @@ -23,11 +24,17 @@ const Modal = ({
title,
children,
width = 'medium',
onSubmit,
isLoading = false,
disabled = false,
}: {
dialogStore: DialogStore
title: string
children: React.ReactElement
width?: Width
onSubmit?: () => void
isLoading?: boolean
disabled?: boolean
}) => {
return (
<>
Expand All @@ -49,6 +56,33 @@ const Modal = ({
<main className="text-sans-md text-default overflow-y-auto px-4 py-6">
{children}
</main>

{onSubmit && (
<footer className="border-secondary flex items-center justify-end border-t px-3 py-3">
<div className="space-x-2">
<Button
variant="secondary"
size="sm"
onClick={() => {
dialogStore.hide()
}}
>
Cancel
</Button>
<Button
size="sm"
type="submit"
onClick={() => !disabled && onSubmit()}
loading={isLoading}
className={
disabled ? 'pointer-events-none cursor-not-allowed opacity-40' : ''
}
>
Create RFD
</Button>
</div>
</footer>
)}
</Dialog>
</>
)
Expand Down
156 changes: 122 additions & 34 deletions app/components/NewRfdButton.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,16 +6,20 @@
* Copyright Oxide Computer Company
*/

import { useDialogStore } from '@ariakit/react'
import { useDialogStore, type DialogStore } from '@ariakit/react'
import cn from 'classnames'
import { useState } from 'react'
import { useFetcher } from 'react-router'

import Icon from '~/components/Icon'
import { useRootLoaderData } from '~/root'

import Modal from './Modal'
import { TextInput } from './TextInput'

const NewRfdButton = () => {
const dialog = useDialogStore()
const newRfdNumber = useRootLoaderData().newRfdNumber
const { user } = useRootLoaderData()

return (
<>
Expand All @@ -26,40 +30,124 @@ const NewRfdButton = () => {
<Icon name="add-roundel" size={16} />
</button>

<Modal dialogStore={dialog} title="Create new RFD">
<>
<p>
There is a prototype script in the rfd{' '}
<a
href="https://github.com/oxidecomputer/rfd"
className="text-accent-tertiary hover:text-accent-secondary"
>
repository
</a>
,{' '}
<code className="align-[1px]; text-mono-code bg-raise border-secondary mr-px ml-px rounded border px-[4px] py-px">
scripts/new.sh
</code>
, that will create a new RFD when used like the code below.
</p>

<p className="mt-2">
{newRfdNumber
? 'The snippet below automatically updates to ensure the new RFD number is correct.'
: 'Replace the number below with the next free number'}
</p>
<pre className="text-mono-code border-secondary 800:px-7 800:py-6 mt-4 overflow-x-auto rounded border px-5 py-4">
<code className="text-mono-code text-[0.825rem]!">
<span className="text-quaternary mr-2 inline-block select-none">$</span>
scripts/new.sh{' '}
{newRfdNumber ? newRfdNumber.toString().padStart(4, '0') : '0042'} "My title
here"
</code>
</pre>
</>
</Modal>
<CreateRfdModal
data={{
title: 'Untitled',
name: user?.displayName || '',
email: user?.email || '',
}}
dialog={dialog}
/>
</>
)
}

const CreateRfdModal = ({
data,
dialog,
}: {
data: { title: string; name: string; email: string }
dialog: DialogStore
}) => {
const [title, setTitle] = useState('')
const [name, setName] = useState('')
const [email, setEmail] = useState('')

const { newRfdNumber } = useRootLoaderData()
const fetcher = useFetcher()

const body = `:state: prediscussion
:discussion:
:authors: ${name || data.name} <${email || data.email}>

= RFD ${newRfdNumber} ${title || '{title}'}

== Determinations
`

const handleSubmit = () => {
fetcher.submit(
{ title, body },
{
method: 'post',
action: `/api/new-rfd`,
encType: 'application/json',
},
)
}

const formDisabled = fetcher.state !== 'idle'
const isFormInvalid =
!title.trim() || !(name || data.name).trim() || !(email || data.email).trim()
const submitDisabled = formDisabled || isFormInvalid

return (
<Modal
dialogStore={dialog}
title="Create new RFD"
onSubmit={handleSubmit}
disabled={submitDisabled}
isLoading={fetcher.state === 'loading' || fetcher.state === 'submitting'}
>
<fetcher.Form className="space-y-4">
<div className="space-y-2">
<TextInput
name="title"
placeholder="Title"
value={title}
onChange={(el) => setTitle(el.target.value)}
disabled={formDisabled}
required
/>
<div className="flex w-full gap-2">
<TextInput
name="name"
placeholder={data.name !== '' ? data.name : 'Author name'}
value={name}
onChange={(el) => setName(el.target.value)}
disabled={formDisabled}
className="w-1/3"
/>
<TextInput
name="email"
placeholder={data.email !== '' ? data.email : 'Author email'}
value={email}
onChange={(el) => setEmail(el.target.value)}
disabled={formDisabled}
className="w-2/3"
/>
</div>
</div>

<pre
className={cn(
'relative h-[160px] overflow-hidden rounded-lg border p-4 select-none',
formDisabled
? 'text-quaternary bg-disabled border-default'
: 'bg-default border-secondary',
)}
>
{body}
<div
className="absolute bottom-0 left-0 h-[100px] w-full"
style={{
background: `linear-gradient(0, ${
formDisabled ? 'var(--surface-disabled)' : 'var(--surface-default)'
} 0%, rgba(8, 15, 17, 0) 100%)`,
}}
/>
</pre>
{fetcher.state === 'idle' &&
fetcher.data &&
!fetcher.data.ok &&
fetcher.data.message && (
<div className="text-sans-lg text-error-secondary my-2">
{fetcher.data.message}
</div>
)}
</fetcher.Form>
</Modal>
)
}

export default NewRfdButton
41 changes: 41 additions & 0 deletions app/components/TextInput.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
/*
* 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 { forwardRef } from 'react'

export type TextInputBaseProps = React.ComponentPropsWithRef<'input'> & {
disabled?: boolean
className?: string
}

export const TextInput = forwardRef<HTMLInputElement, TextInputBaseProps>(
({ type = 'text', className, disabled, ...fieldProps }, ref) => {
return (
<div
className={cn(
'border-default hover:border-hover flex rounded border',
disabled && '!border-default',
className,
)}
>
<input
ref={ref}
type={type}
className={cn(
`text-sans-md text-default bg-default placeholder:text-quaternary disabled:text-tertiary disabled:bg-disabled focus:outline-accent-secondary w-full rounded border-none px-3 py-[0.6875rem] !outline-offset-1 disabled:cursor-not-allowed`,
disabled && 'text-disabled bg-disabled',
)}
disabled={disabled}
{...fieldProps}
autoComplete="off"
data-1p-ignore
/>
</div>
)
},
)
74 changes: 0 additions & 74 deletions app/components/rfd/index.css

This file was deleted.

1 change: 0 additions & 1 deletion app/root.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -119,7 +119,6 @@ const Layout = ({ children, theme }: { children: React.ReactNode; theme?: string
<Links />
<link rel="icon" href="/favicon.svg" />
<link rel="icon" type="image/png" href="/favicon.png" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
{/* Use plausible analytics only on Vercel */}
{process.env.NODE_ENV === 'production' && (
<script defer data-domain="rfd.shared.oxide.computer" src="/js/viewscript.js" />
Expand Down
Loading
Loading