Files
rfc-app/frontend/src/components/ProposeModal.jsx
T
Ben Stull 0f8b318afa Release 0.4.0: auto-set RFC owner = proposer
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-27 22:53:09 -07:00

157 lines
5.4 KiB
React
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// ProposeModal.jsx — §9.1.
//
// Title (required) and pitch (required textarea), with a slug field
// that auto-fills from the title via the same deterministic kebab-case
// the backend uses. Tags are chip-input (free-form for slice 1; the
// AI-suggested chips of §9.1 are deferred to Slice 2 when the AI surface
// is wired up).
//
// The submit button drives the §17 POST /api/rfcs/propose endpoint;
// success navigates the proposer to the pending-idea view per §9.3.
import { useEffect, useState } from 'react'
import { proposeRFC } from '../api'
function slugify(title) {
return title
.toLowerCase()
.trim()
.replace(/[^a-z0-9]+/g, '-')
.replace(/^-+|-+$/g, '')
}
export default function ProposeModal({ viewer, onClose, onSubmitted }) {
const [title, setTitle] = useState('')
const [slug, setSlug] = useState('')
const [slugEdited, setSlugEdited] = useState(false)
const [pitch, setPitch] = useState('')
const [tagInput, setTagInput] = useState('')
const [tags, setTags] = useState([])
const [submitting, setSubmitting] = useState(false)
const [error, setError] = useState(null)
useEffect(() => {
if (!slugEdited) setSlug(slugify(title))
}, [title, slugEdited])
function addTag() {
const t = tagInput.trim()
if (t && !tags.includes(t)) setTags([...tags, t])
setTagInput('')
}
async function handleSubmit(e) {
e.preventDefault()
if (!title.trim() || !slug || !pitch.trim()) return
setSubmitting(true)
setError(null)
try {
const result = await proposeRFC({
title: title.trim(),
slug,
pitch: pitch.trim(),
tags,
})
onSubmitted?.(result)
} catch (err) {
setError(err.message || 'Submission failed.')
} finally {
setSubmitting(false)
}
}
return (
<div className="modal-overlay" onClick={e => { if (e.target === e.currentTarget) onClose() }}>
<div className="modal">
<div className="modal-header">
<h2>Propose a New RFC</h2>
<button className="modal-close" onClick={onClose}>×</button>
</div>
<form onSubmit={handleSubmit}>
<div className="modal-body">
<label htmlFor="propose-title">Title</label>
<input
id="propose-title"
value={title}
onChange={e => setTitle(e.target.value)}
placeholder="e.g. Open Human Model"
autoFocus
required
/>
<p className="field-help">The word or topic this RFC will define.</p>
<label htmlFor="propose-slug">Slug</label>
<input
id="propose-slug"
value={slug}
onChange={e => { setSlug(slugify(e.target.value)); setSlugEdited(true) }}
placeholder="open-human-model"
required
/>
<p className="field-help">Auto-derived from the title; edit if needed.</p>
<label htmlFor="propose-pitch">Why is this RFC needed?</label>
<textarea
id="propose-pitch"
value={pitch}
onChange={e => setPitch(e.target.value)}
placeholder="One or two paragraphs answering 'why this RFC is needed.'"
rows={5}
required
/>
<label htmlFor="propose-tag">Tags (optional)</label>
<div style={{ display: 'flex', gap: 6, alignItems: 'center', marginBottom: 4 }}>
<input
id="propose-tag"
value={tagInput}
onChange={e => setTagInput(e.target.value)}
onKeyDown={e => {
if (e.key === 'Enter' || e.key === ',') { e.preventDefault(); addTag() }
}}
placeholder="identity, schema"
style={{ marginBottom: 0 }}
/>
<button type="button" className="btn-secondary" onClick={addTag} style={{ padding: '6px 12px' }}>
Add
</button>
</div>
{tags.length > 0 && (
<div style={{ marginTop: 4, marginBottom: 14 }}>
{tags.map(t => (
<span key={t} className="entry-tag" style={{ display: 'inline-block', marginRight: 4 }}>
{t}{' '}
<button
type="button"
onClick={() => setTags(tags.filter(x => x !== t))}
style={{ background: 'none', border: 'none', cursor: 'pointer', color: '#999' }}
>×</button>
</span>
))}
</div>
)}
{viewer && (
<p className="field-help" style={{ marginTop: 14, marginBottom: 0 }}>
Owner: <strong>{viewer.display_name || viewer.gitea_login}</strong> you'll be the first owner of this super-draft. Additional owners can claim later (§13.1).
</p>
)}
{error && <p className="field-error">{error}</p>}
</div>
<div className="modal-actions">
<button type="button" className="btn-secondary" onClick={onClose}>Cancel</button>
<button
type="submit"
className="btn-primary"
disabled={!title.trim() || !slug || !pitch.trim() || submitting}
>
{submitting ? 'Opening PR' : 'Open proposal PR'}
</button>
</div>
</form>
</div>
</div>
)
}