Skip to content
Draft
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
9 changes: 9 additions & 0 deletions .dev.vars.example
Original file line number Diff line number Diff line change
Expand Up @@ -12,3 +12,12 @@ REGISTRY_API_URL=https://registry-indexer.fly.dev
# The Stellar network this instance targets. Used to label the UI.
# Options: testnet | mainnet
REGISTRY_NETWORK=testnet

# The following are for Tansu workarounds. Tansu's UI does not currently allow
# creating the governance proposals needed by Stellar Registry. We are creating
# these via CLI calls on our server, and making simple HTML forms on rgstry.xyz
# that make it easy for community members to hit the server endpoints that
# create those Tansu proposals.
TANSU_ID=CBXKUSLQPVF35FYURR5C42BPYA5UOVDXX2ELKIM2CAJMCI6HXG2BHGZA
REGISTRY_ID=CAF2CLQM5X7SRVHJB2WS5IK33D4CQFFZYCFKDYLR2SCQASFPM3YN7EW7
MANAGER_ID=CAL4PZO2QDY2ELLLY6I2BUTOOZ4ZNSMZA77L3QOZKQN2XKV3Q6LR2VGI
45 changes: 45 additions & 0 deletions app/components/governance-form.module.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
.page {
max-width: 40rem;
margin: 0 auto;
padding: 2.5rem 1.5rem 4rem;
}

.form {
display: flex;
flex-direction: column;
}

.fields {
display: flex;
flex-direction: column;
gap: 1.25rem;
}

.field {
display: flex;
flex-direction: column;
gap: 0.5rem;
}

.required {
color: var(--color-destructive);
}

.error {
font-size: 0.8125rem;
color: var(--color-destructive);
margin: 0;
}

.successSummary {
font-size: 0.9375rem;
line-height: 1.6;
color: var(--color-foreground);
margin: 0 0 0.75rem;
}

.successTracking {
font-size: 0.875rem;
color: var(--color-muted-foreground);
margin: 0;
}
108 changes: 108 additions & 0 deletions app/components/governance-form.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
import { Form, useNavigation } from "react-router"
import { Button } from "./button"
import {
Card,
CardContent,
CardDescription,
CardFooter,
CardHeader,
CardTitle,
} from "./card"
import styles from "./governance-form.module.css"
import { Input } from "./input"
import { Label } from "./label"
import { Textarea } from "./textarea"
import {
type GovernanceActionData,
type GovernanceOperation,
} from "~/lib/governance"

function GovernanceForm({
operation,
actionData,
}: {
operation: GovernanceOperation
actionData: GovernanceActionData | undefined
}) {
const navigation = useNavigation()
const isSubmitting = navigation.state === "submitting"

if (actionData?.ok) {
return (
<main className={styles.page}>
<Card>
<CardHeader>
<CardTitle>Proposal captured</CardTitle>
<CardDescription>
This is a mock submission — nothing was actually sent anywhere
yet.
</CardDescription>
</CardHeader>
<CardContent>
<p className={styles.successSummary}>{actionData.result.summary}</p>
<p className={styles.successTracking}>
Mock tracking id: <code>{actionData.result.trackingId}</code>
</p>
</CardContent>
</Card>
</main>
)
}

const errors = actionData?.ok === false ? actionData.errors : undefined

return (
<main className={styles.page}>
<Form method="post" className={styles.form}>
<Card>
<CardHeader>
<CardTitle>{operation.title}</CardTitle>
<CardDescription>{operation.description}</CardDescription>
</CardHeader>
<CardContent className={styles.fields}>
{operation.fields.map((field) => {
const errorMessage = errors?.[field.name]
const fieldId = `${operation.id}-${field.name}`
return (
<div className={styles.field} key={field.name}>
<Label htmlFor={fieldId}>
{field.label}
{field.required && (
<span className={styles.required}>*</span>
)}
</Label>
{field.type === "textarea" ? (
<Textarea
id={fieldId}
name={field.name}
placeholder={field.placeholder}
aria-invalid={Boolean(errorMessage)}
rows={4}
/>
) : (
<Input
id={fieldId}
name={field.name}
placeholder={field.placeholder}
aria-invalid={Boolean(errorMessage)}
/>
)}
{errorMessage && (
<p className={styles.error}>{errorMessage}</p>
)}
</div>
)
})}
</CardContent>
<CardFooter>
<Button type="submit" disabled={isSubmitting}>
{isSubmitting ? "Submitting…" : "Submit proposal"}
</Button>
</CardFooter>
</Card>
</Form>
</main>
)
}

export { GovernanceForm }
10 changes: 10 additions & 0 deletions app/components/label.module.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
.label {
display: flex;
align-items: center;
gap: calc(var(--spacing) * 2);
font-size: var(--text-sm);
font-weight: var(--font-weight-medium);
line-height: 1;
color: var(--color-foreground);
user-select: none;
}
10 changes: 10 additions & 0 deletions app/components/label.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { type ComponentProps } from "react"
import styles from "./label.module.css"

function Label({ className, ...props }: ComponentProps<"label">) {
return (
<label className={`${styles.label} ${className ?? ""}`.trim()} {...props} />
)
}

export { Label }
54 changes: 54 additions & 0 deletions app/components/textarea.module.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
.textarea {
display: flex;
width: 100%;
min-height: calc(var(--spacing) * 20);
padding: calc(var(--spacing) * 2) calc(var(--spacing) * 3);
transition: all var(--default-transition-duration)
var(--default-transition-timing-function);
border: 1px solid var(--color-input);
border-radius: var(--radius-md);
outline: none;
background-color: transparent;
box-shadow: var(--shadow-xs);
color: var(--color-foreground);
font-size: var(--text-sm);
line-height: var(--text-sm--line-height);
resize: vertical;

&[aria-invalid="true"] {
border-color: var(--color-destructive);
}

&::placeholder {
color: var(--color-muted-foreground);
}

&::selection {
background-color: var(--color-primary);
color: var(--color-primary-foreground);
}

&:disabled {
opacity: 0.5;
cursor: not-allowed;
}

&:focus {
&:not([aria-invalid="true"]) {
border-color: var(--color-ring);
box-shadow: 0 0 0 3px
color-mix(in oklab, var(--color-ring) 50%, transparent);
}

&[aria-invalid="true"] {
box-shadow: 0 0 0 3px
color-mix(in oklab, var(--color-destructive) 20%, transparent);
}
}
}

:global(.dark) {
.textarea {
background-color: color-mix(in oklab, var(--color-input) 30%, transparent);
}
}
13 changes: 13 additions & 0 deletions app/components/textarea.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { type ComponentProps } from "react"
import styles from "./textarea.module.css"

function Textarea({ className, ...props }: ComponentProps<"textarea">) {
return (
<textarea
className={`${styles.textarea} ${className ?? ""}`.trim()}
{...props}
/>
)
}

export { Textarea }
Loading