refactor: adopt shared Input/Textarea components across 15 files

Replace 42 raw <input>/<textarea> elements with <Input>/<Textarea>
from components/ui/. Consistent focus states, error handling, and
styling across all form fields.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Michael Chihlas
2026-03-08 01:03:41 -05:00
parent 94b428d168
commit 905e16de8b
15 changed files with 87 additions and 192 deletions

View File

@@ -1,7 +1,8 @@
import { useState } from 'react'
import { cn } from '@/lib/utils'
import { Modal } from '@/components/common/Modal'
import { Button } from '@/components/ui/Button'
import { Input } from '@/components/ui/Input'
import { Textarea } from '@/components/ui/Textarea'
interface CreateCategoryModalProps {
isOpen: boolean
@@ -93,7 +94,7 @@ export function CreateCategoryModal({
<label htmlFor="name" className="mb-1 block text-sm font-medium text-foreground">
Category Name <span className="text-red-400">*</span>
</label>
<input
<Input
id="name"
type="text"
value={name}
@@ -102,12 +103,6 @@ export function CreateCategoryModal({
maxLength={100}
placeholder="e.g., Network Troubleshooting"
required
className={cn(
'w-full rounded-md border border-border bg-card px-3 py-2 text-sm text-foreground',
'placeholder:text-muted-foreground',
'focus:border-primary focus:outline-hidden focus:ring-1 focus:ring-primary/20',
'disabled:opacity-50'
)}
/>
<p className="mt-1 text-xs text-muted-foreground">
{name.length}/100 characters
@@ -118,19 +113,13 @@ export function CreateCategoryModal({
<label htmlFor="description" className="mb-1 block text-sm font-medium text-foreground">
Description <span className="text-muted-foreground">(optional)</span>
</label>
<textarea
<Textarea
id="description"
value={description}
onChange={(e) => setDescription(e.target.value)}
disabled={isSaving}
rows={3}
placeholder="Brief description of this category..."
className={cn(
'w-full rounded-md border border-border bg-card px-3 py-2 text-sm text-foreground',
'placeholder:text-muted-foreground',
'focus:border-primary focus:outline-hidden focus:ring-1 focus:ring-primary/20',
'disabled:opacity-50'
)}
/>
</div>
</form>

View File

@@ -1,8 +1,9 @@
import { useState } from 'react'
import { cn } from '@/lib/utils'
import type { StepCategoryListItem } from '@/types'
import { Modal } from '@/components/common/Modal'
import { Button } from '@/components/ui/Button'
import { Input } from '@/components/ui/Input'
import { Textarea } from '@/components/ui/Textarea'
interface EditCategoryModalProps {
isOpen: boolean
@@ -105,7 +106,7 @@ export function EditCategoryModal({
<label htmlFor="edit-name" className="mb-1 block text-sm font-medium text-foreground">
Category Name <span className="text-red-400">*</span>
</label>
<input
<Input
id="edit-name"
type="text"
value={name}
@@ -114,12 +115,6 @@ export function EditCategoryModal({
maxLength={100}
placeholder="e.g., Network Troubleshooting"
required
className={cn(
'w-full rounded-md border border-border bg-card px-3 py-2 text-sm text-foreground',
'placeholder:text-muted-foreground',
'focus:border-primary focus:outline-hidden focus:ring-1 focus:ring-primary/20',
'disabled:opacity-50'
)}
/>
<p className="mt-1 text-xs text-muted-foreground">
{name.length}/100 characters
@@ -130,19 +125,13 @@ export function EditCategoryModal({
<label htmlFor="edit-description" className="mb-1 block text-sm font-medium text-foreground">
Description <span className="text-muted-foreground">(optional)</span>
</label>
<textarea
<Textarea
id="edit-description"
value={description}
onChange={(e) => setDescription(e.target.value)}
disabled={isSaving}
rows={3}
placeholder="Brief description of this category..."
className={cn(
'w-full rounded-md border border-border bg-card px-3 py-2 text-sm text-foreground',
'placeholder:text-muted-foreground',
'focus:border-primary focus:outline-hidden focus:ring-1 focus:ring-primary/20',
'disabled:opacity-50'
)}
/>
</div>
</form>

View File

@@ -1,8 +1,9 @@
import { useState } from 'react'
import { Modal } from '@/components/common/Modal'
import { GitFork } from 'lucide-react'
import { cn } from '@/lib/utils'
import { Button } from '@/components/ui/Button'
import { Input } from '@/components/ui/Input'
import { Textarea } from '@/components/ui/Textarea'
interface ForkTreeModalProps {
isOpen: boolean
@@ -83,16 +84,12 @@ export function ForkTreeModal({
<label htmlFor="tree-name" className="mb-1.5 block text-sm font-medium text-foreground">
Tree Name <span className="text-red-400">*</span>
</label>
<input
<Input
id="tree-name"
type="text"
value={name}
onChange={(e) => setName(e.target.value)}
placeholder="My Custom Tree"
className={cn(
'w-full rounded-md border border-border bg-card px-3 py-2 text-sm text-foreground',
'focus:outline-hidden focus:border-primary focus:ring-1 focus:ring-primary/20'
)}
/>
</div>
@@ -100,17 +97,13 @@ export function ForkTreeModal({
<label htmlFor="tree-description" className="mb-1.5 block text-sm font-medium text-foreground">
Description <span className="text-muted-foreground">(optional)</span>
</label>
<textarea
<Textarea
id="tree-description"
value={description}
onChange={(e) => setDescription(e.target.value)}
placeholder="Describe what this tree helps troubleshoot..."
rows={3}
className={cn(
'w-full rounded-md border border-border bg-card px-3 py-2 text-sm text-foreground',
'focus:outline-hidden focus:border-primary focus:ring-1 focus:ring-primary/20',
'resize-none'
)}
className="resize-none"
/>
</div>
</div>

View File

@@ -1,7 +1,8 @@
import { useState } from 'react'
import { X } from 'lucide-react'
import { cn } from '@/lib/utils'
import { Button } from '@/components/ui/Button'
import { Input } from '@/components/ui/Input'
import { Textarea } from '@/components/ui/Textarea'
interface SaveSessionAsTreeModalProps {
isOpen: boolean
@@ -60,7 +61,7 @@ export function SaveSessionAsTreeModal({
<label htmlFor="treeName" className="mb-1 block text-sm font-medium text-foreground">
Tree Name <span className="text-muted-foreground">(optional)</span>
</label>
<input
<Input
id="treeName"
type="text"
value={treeName}
@@ -68,12 +69,6 @@ export function SaveSessionAsTreeModal({
placeholder={defaultTreeName || "Auto-generated if left blank"}
disabled={isSaving}
maxLength={255}
className={cn(
'w-full rounded-md border border-border bg-card px-3 py-2 text-sm text-foreground',
'placeholder:text-muted-foreground',
'focus:border-primary focus:outline-hidden focus:ring-1 focus:ring-primary/20',
'disabled:opacity-50'
)}
/>
</div>
@@ -82,19 +77,13 @@ export function SaveSessionAsTreeModal({
<label htmlFor="description" className="mb-1 block text-sm font-medium text-foreground">
Description <span className="text-muted-foreground">(optional)</span>
</label>
<textarea
<Textarea
id="description"
value={description}
onChange={(e) => setDescription(e.target.value)}
placeholder="Add a description for this tree"
disabled={isSaving}
rows={3}
className={cn(
'w-full rounded-md border border-border bg-card px-3 py-2 text-sm text-foreground',
'placeholder:text-muted-foreground',
'focus:border-primary focus:outline-hidden focus:ring-1 focus:ring-primary/20',
'disabled:opacity-50'
)}
/>
</div>

View File

@@ -8,6 +8,7 @@ import { toast } from '@/lib/toast'
import { Spinner } from '@/components/common/Spinner'
import { Modal } from '@/components/common/Modal'
import { Button } from '@/components/ui/Button'
import { Input } from '@/components/ui/Input'
interface ShareSessionModalProps {
sessionId: string
@@ -246,15 +247,11 @@ export function ShareSessionModal({ sessionId, sessionLabel, isOpen, onClose }:
<label className="mb-2 block text-sm font-medium text-foreground">
Share Name <span className="text-muted-foreground">(optional)</span>
</label>
<input
<Input
type="text"
value={shareName}
onChange={(e) => setShareName(e.target.value.slice(0, 100))}
placeholder="e.g. Training link, Customer escalation"
className={cn(
'w-full rounded-md border border-border bg-card px-3 py-2 text-sm text-foreground placeholder-muted-foreground',
'focus:border-primary focus:outline-hidden focus:ring-1 focus:ring-primary/20'
)}
maxLength={100}
/>
</div>

View File

@@ -4,6 +4,7 @@ import { StarRating } from '@/components/common/StarRating'
import { cn } from '@/lib/utils'
import type { Step } from '@/types'
import { Button } from '@/components/ui/Button'
import { Textarea } from '@/components/ui/Textarea'
interface StepRatingData {
rating: number
@@ -164,7 +165,7 @@ export function StepRatingModal({
<label htmlFor={`review-${step.id}`} className="mb-1 block text-sm font-medium text-foreground">
Review <span className="text-muted-foreground">(optional)</span>
</label>
<textarea
<Textarea
id={`review-${step.id}`}
value={rating?.review || ''}
onChange={(e) => handleReviewChange(step.id, e.target.value)}
@@ -172,12 +173,6 @@ export function StepRatingModal({
maxLength={500}
rows={2}
placeholder="Share your experience with this step..."
className={cn(
'w-full rounded-md border border-border bg-card px-3 py-2 text-sm text-foreground',
'placeholder:text-muted-foreground',
'focus:border-primary focus:outline-hidden focus:ring-1 focus:ring-primary/20',
'disabled:opacity-50'
)}
/>
<p className="mt-1 text-xs text-muted-foreground text-right">
{rating?.review?.length || 0}/500

View File

@@ -4,6 +4,8 @@ import { cn } from '@/lib/utils'
import { stepCategoriesApi } from '@/api/stepCategories'
import type { StepCreate, StepCategory, StepCommand } from '@/types/step'
import { Button } from '@/components/ui/Button'
import { Input } from '@/components/ui/Input'
import { Textarea } from '@/components/ui/Textarea'
interface StepFormProps {
onSubmit: (data: StepCreate) => void
@@ -173,20 +175,14 @@ export function StepForm({ onSubmit, onCancel, initialData, submitLabel, isSubmi
<label htmlFor="title" className="mb-2 block text-sm font-medium text-foreground">
Title <span className="text-red-400">*</span>
</label>
<input
<Input
id="title"
type="text"
value={title}
onChange={(e) => setTitle(e.target.value)}
placeholder="Enter step title"
className={cn(
'w-full rounded-md border bg-card px-3 py-2 text-sm text-foreground focus:outline-hidden focus:border-primary focus:ring-1 focus:ring-primary/20',
errors.title ? 'border-red-400/50' : 'border-border'
)}
error={errors.title}
/>
{errors.title && (
<p className="mt-1 text-xs text-red-400">{errors.title}</p>
)}
</div>
{/* Instructions */}
@@ -195,20 +191,14 @@ export function StepForm({ onSubmit, onCancel, initialData, submitLabel, isSubmi
Instructions <span className="text-red-400">*</span>
<span className="ml-2 text-xs font-normal text-muted-foreground">(Markdown supported)</span>
</label>
<textarea
<Textarea
id="instructions"
value={instructions}
onChange={(e) => setInstructions(e.target.value)}
placeholder="Describe what to do in this step..."
rows={6}
className={cn(
'w-full rounded-md border bg-card px-3 py-2 text-sm text-foreground focus:outline-hidden focus:border-primary focus:ring-1 focus:ring-primary/20',
errors.instructions ? 'border-red-400/50' : 'border-border'
)}
error={errors.instructions}
/>
{errors.instructions && (
<p className="mt-1 text-xs text-red-400">{errors.instructions}</p>
)}
</div>
{/* Help Text */}
@@ -216,13 +206,12 @@ export function StepForm({ onSubmit, onCancel, initialData, submitLabel, isSubmi
<label htmlFor="helpText" className="mb-2 block text-sm font-medium text-foreground">
Help Text <span className="text-xs font-normal text-muted-foreground">(Optional)</span>
</label>
<textarea
<Textarea
id="helpText"
value={helpText}
onChange={(e) => setHelpText(e.target.value)}
placeholder="Additional context or tips..."
rows={3}
className="w-full rounded-md border border-border bg-card px-3 py-2 text-sm text-foreground focus:outline-hidden focus:border-primary focus:ring-1 focus:ring-primary/20"
/>
</div>
@@ -256,31 +245,22 @@ export function StepForm({ onSubmit, onCancel, initialData, submitLabel, isSubmi
</button>
</div>
<div className="space-y-2">
<input
<Input
type="text"
value={cmd.label}
onChange={(e) => updateCommand(index, 'label', e.target.value)}
placeholder="Command label (e.g., 'Restart service')"
className={cn(
'w-full rounded-md border bg-card px-3 py-1.5 text-sm text-foreground',
errors[`command_${index}_label`] ? 'border-red-400/50' : 'border-border'
)}
className="py-1.5"
error={errors[`command_${index}_label`]}
/>
<input
<Input
type="text"
value={cmd.command}
onChange={(e) => updateCommand(index, 'command', e.target.value)}
placeholder="Command (e.g., 'systemctl restart nginx')"
className={cn(
'w-full rounded-md border bg-card px-3 py-1.5 font-mono text-sm text-foreground',
errors[`command_${index}_command`] ? 'border-red-400/50' : 'border-border'
)}
className="py-1.5 font-mono"
error={errors[`command_${index}_command`]}
/>
{(errors[`command_${index}_label`] || errors[`command_${index}_command`]) && (
<p className="text-xs text-red-400">
{errors[`command_${index}_label`] || errors[`command_${index}_command`]}
</p>
)}
</div>
</div>
))}
@@ -312,14 +292,14 @@ export function StepForm({ onSubmit, onCancel, initialData, submitLabel, isSubmi
Tags <span className="text-xs font-normal text-muted-foreground">(Optional)</span>
</label>
<div className="flex gap-2">
<input
<Input
id="tagInput"
type="text"
value={tagInput}
onChange={(e) => setTagInput(e.target.value)}
onKeyDown={handleTagInputKeyDown}
placeholder="Type tag and press Enter"
className="flex-1 rounded-md border border-border bg-card px-3 py-2 text-sm text-foreground focus:outline-hidden focus:border-primary focus:ring-1 focus:ring-primary/20"
className="flex-1"
/>
<button
type="button"

View File

@@ -1,7 +1,7 @@
import { useState, useEffect, useCallback } from 'react'
import { Plus, Trash2, Pencil, FolderTree } from 'lucide-react'
import { Button } from '@/components/ui/Button'
import { cn } from '@/lib/utils'
import { Input } from '@/components/ui/Input'
import { toast } from '@/lib/toast'
import { Modal } from '@/components/common/Modal'
import { PageHeader } from '@/components/common/PageHeader'
@@ -79,8 +79,6 @@ export function TeamCategoriesPage() {
setForm({ name: cat.name, slug: cat.slug, description: cat.description || '' })
}
const inputCn = cn('w-full rounded-md border border-border bg-card px-3 py-2 text-sm text-foreground', 'placeholder:text-muted-foreground focus:outline-hidden focus:border-primary focus:ring-2 focus:ring-primary/20')
return (
<div className="space-y-6">
<PageHeader
@@ -141,15 +139,15 @@ export function TeamCategoriesPage() {
<div className="space-y-4">
<div>
<label className="mb-1 block text-sm font-medium text-foreground">Name</label>
<input type="text" value={form.name} onChange={(e) => { const name = e.target.value; setForm(f => ({ ...f, name, slug: generateSlug(name) })) }} placeholder="e.g. Networking" className={inputCn} />
<Input type="text" value={form.name} onChange={(e) => { const name = e.target.value; setForm(f => ({ ...f, name, slug: generateSlug(name) })) }} placeholder="e.g. Networking" />
</div>
<div>
<label className="mb-1 block text-sm font-medium text-foreground">Slug</label>
<input type="text" value={form.slug} onChange={(e) => setForm({ ...form, slug: e.target.value })} className={inputCn} />
<Input type="text" value={form.slug} onChange={(e) => setForm({ ...form, slug: e.target.value })} />
</div>
<div>
<label className="mb-1 block text-sm font-medium text-foreground">Description</label>
<input type="text" value={form.description} onChange={(e) => setForm({ ...form, description: e.target.value })} placeholder="Optional" className={inputCn} />
<Input type="text" value={form.description} onChange={(e) => setForm({ ...form, description: e.target.value })} placeholder="Optional" />
</div>
</div>
</Modal>
@@ -166,15 +164,15 @@ export function TeamCategoriesPage() {
<div className="space-y-4">
<div>
<label className="mb-1 block text-sm font-medium text-foreground">Name</label>
<input type="text" value={form.name} onChange={(e) => setForm({ ...form, name: e.target.value })} className={inputCn} />
<Input type="text" value={form.name} onChange={(e) => setForm({ ...form, name: e.target.value })} />
</div>
<div>
<label className="mb-1 block text-sm font-medium text-foreground">Slug</label>
<input type="text" value={form.slug} onChange={(e) => setForm({ ...form, slug: e.target.value })} className={inputCn} />
<Input type="text" value={form.slug} onChange={(e) => setForm({ ...form, slug: e.target.value })} />
</div>
<div>
<label className="mb-1 block text-sm font-medium text-foreground">Description</label>
<input type="text" value={form.description} onChange={(e) => setForm({ ...form, description: e.target.value })} placeholder="Optional" className={inputCn} />
<Input type="text" value={form.description} onChange={(e) => setForm({ ...form, description: e.target.value })} placeholder="Optional" />
</div>
</div>
</Modal>

View File

@@ -1,6 +1,7 @@
import { useState, useEffect, useCallback } from 'react'
import { Plus, Trash2, ToggleLeft } from 'lucide-react'
import { Button } from '@/components/ui/Button'
import { Input } from '@/components/ui/Input'
import { DataTable, PageHeader, StatusBadge, ActionMenu, EmptyState } from '@/components/admin'
import type { Column } from '@/components/admin'
import { Modal } from '@/components/common/Modal'
@@ -146,7 +147,7 @@ export function FeatureFlagsPage() {
},
]
const inputCn = cn('w-full rounded-md border border-border bg-card px-3 py-2 text-sm text-foreground', 'placeholder:text-muted-foreground focus:outline-hidden focus:border-primary focus:ring-2 focus:ring-primary/20')
const selectClass = cn('w-full rounded-md border border-border bg-card px-3 py-2 text-sm text-foreground', 'placeholder:text-muted-foreground focus:outline-hidden focus:border-primary focus:ring-2 focus:ring-primary/20')
return (
<div className="space-y-8">
@@ -197,15 +198,15 @@ export function FeatureFlagsPage() {
<div className="space-y-4">
<div>
<label className="mb-1 block text-sm font-medium text-foreground">Flag Key</label>
<input type="text" value={createForm.flag_key} onChange={(e) => setCreateForm({ ...createForm, flag_key: e.target.value })} placeholder="e.g. custom_branding" className={inputCn} />
<Input type="text" value={createForm.flag_key} onChange={(e) => setCreateForm({ ...createForm, flag_key: e.target.value })} placeholder="e.g. custom_branding" />
</div>
<div>
<label className="mb-1 block text-sm font-medium text-foreground">Display Name</label>
<input type="text" value={createForm.display_name} onChange={(e) => setCreateForm({ ...createForm, display_name: e.target.value })} placeholder="e.g. Custom Branding" className={inputCn} />
<Input type="text" value={createForm.display_name} onChange={(e) => setCreateForm({ ...createForm, display_name: e.target.value })} placeholder="e.g. Custom Branding" />
</div>
<div>
<label className="mb-1 block text-sm font-medium text-foreground">Description</label>
<input type="text" value={createForm.description ?? ''} onChange={(e) => setCreateForm({ ...createForm, description: e.target.value || null })} placeholder="Optional description" className={inputCn} />
<Input type="text" value={createForm.description ?? ''} onChange={(e) => setCreateForm({ ...createForm, description: e.target.value || null })} placeholder="Optional description" />
</div>
</div>
</Modal>
@@ -222,11 +223,11 @@ export function FeatureFlagsPage() {
<div className="space-y-4">
<div>
<label className="mb-1 block text-sm font-medium text-foreground">Account Display Code</label>
<input type="text" value={overrideForm.account_display_code} onChange={(e) => setOverrideForm({ ...overrideForm, account_display_code: e.target.value })} placeholder="e.g. ABC-1234" className={inputCn} />
<Input type="text" value={overrideForm.account_display_code} onChange={(e) => setOverrideForm({ ...overrideForm, account_display_code: e.target.value })} placeholder="e.g. ABC-1234" />
</div>
<div>
<label className="mb-1 block text-sm font-medium text-foreground">Feature Flag</label>
<select value={overrideForm.flag_id} onChange={(e) => setOverrideForm({ ...overrideForm, flag_id: e.target.value })} className={inputCn}>
<select value={overrideForm.flag_id} onChange={(e) => setOverrideForm({ ...overrideForm, flag_id: e.target.value })} className={selectClass}>
<option value="">Select a flag...</option>
{flags.map(f => <option key={f.id} value={f.id}>{f.display_name}</option>)}
</select>
@@ -237,7 +238,7 @@ export function FeatureFlagsPage() {
</div>
<div>
<label className="mb-1 block text-sm font-medium text-foreground">Note</label>
<input type="text" value={overrideForm.note ?? ''} onChange={(e) => setOverrideForm({ ...overrideForm, note: e.target.value || null })} placeholder="Reason" className={inputCn} />
<Input type="text" value={overrideForm.note ?? ''} onChange={(e) => setOverrideForm({ ...overrideForm, note: e.target.value || null })} placeholder="Reason" />
</div>
</div>
</Modal>

View File

@@ -1,12 +1,12 @@
import { useState, useEffect, useCallback } from 'react'
import { Plus, Trash2, Pencil, FolderTree } from 'lucide-react'
import { Button } from '@/components/ui/Button'
import { Input } from '@/components/ui/Input'
import { DataTable, PageHeader, ActionMenu, EmptyState } from '@/components/admin'
import type { Column } from '@/components/admin'
import { Modal } from '@/components/common/Modal'
import { adminApi } from '@/api/admin'
import { toast } from '@/lib/toast'
import { cn } from '@/lib/utils'
import type { AdminCategory, GlobalCategoryCreate } from '@/types/admin'
export function GlobalCategoriesPage() {
@@ -88,8 +88,6 @@ export function GlobalCategoriesPage() {
},
]
const inputCn = cn('w-full rounded-md border border-border bg-card px-3 py-2 text-sm text-foreground', 'placeholder:text-muted-foreground focus:outline-hidden focus:border-primary focus:ring-2 focus:ring-primary/20')
return (
<div className="space-y-6">
<PageHeader
@@ -127,15 +125,15 @@ export function GlobalCategoriesPage() {
<div className="space-y-4">
<div>
<label className="mb-1 block text-sm font-medium text-foreground">Name</label>
<input type="text" value={form.name} onChange={(e) => { const name = e.target.value; setForm(f => ({ ...f, name, slug: generateSlug(name) })) }} placeholder="e.g. Networking" className={inputCn} />
<Input type="text" value={form.name} onChange={(e) => { const name = e.target.value; setForm(f => ({ ...f, name, slug: generateSlug(name) })) }} placeholder="e.g. Networking" />
</div>
<div>
<label className="mb-1 block text-sm font-medium text-foreground">Slug</label>
<input type="text" value={form.slug} onChange={(e) => setForm({ ...form, slug: e.target.value })} placeholder="e.g. networking" className={inputCn} />
<Input type="text" value={form.slug} onChange={(e) => setForm({ ...form, slug: e.target.value })} placeholder="e.g. networking" />
</div>
<div>
<label className="mb-1 block text-sm font-medium text-foreground">Description</label>
<input type="text" value={form.description ?? ''} onChange={(e) => setForm({ ...form, description: e.target.value || null })} placeholder="Optional description" className={inputCn} />
<Input type="text" value={form.description ?? ''} onChange={(e) => setForm({ ...form, description: e.target.value || null })} placeholder="Optional description" />
</div>
</div>
</Modal>
@@ -156,15 +154,15 @@ export function GlobalCategoriesPage() {
<div className="space-y-4">
<div>
<label className="mb-1 block text-sm font-medium text-foreground">Name</label>
<input type="text" value={form.name} onChange={(e) => setForm({ ...form, name: e.target.value })} placeholder="e.g. Networking" className={inputCn} />
<Input type="text" value={form.name} onChange={(e) => setForm({ ...form, name: e.target.value })} placeholder="e.g. Networking" />
</div>
<div>
<label className="mb-1 block text-sm font-medium text-foreground">Slug</label>
<input type="text" value={form.slug} onChange={(e) => setForm({ ...form, slug: e.target.value })} placeholder="e.g. networking" className={inputCn} />
<Input type="text" value={form.slug} onChange={(e) => setForm({ ...form, slug: e.target.value })} placeholder="e.g. networking" />
</div>
<div>
<label className="mb-1 block text-sm font-medium text-foreground">Description</label>
<input type="text" value={form.description ?? ''} onChange={(e) => setForm({ ...form, description: e.target.value || null })} placeholder="Optional description" className={inputCn} />
<Input type="text" value={form.description ?? ''} onChange={(e) => setForm({ ...form, description: e.target.value || null })} placeholder="Optional description" />
</div>
</div>
</Modal>

View File

@@ -1,6 +1,7 @@
import { useState, useEffect, useCallback } from 'react'
import { Plus, Copy, Trash2, Ticket, Mail, MailCheck, RefreshCw } from 'lucide-react'
import { Button } from '@/components/ui/Button'
import { Input } from '@/components/ui/Input'
import { DataTable, PageHeader, StatusBadge, ActionMenu, EmptyState } from '@/components/admin'
import type { Column } from '@/components/admin'
import { Modal } from '@/components/common/Modal'
@@ -108,7 +109,7 @@ export function InviteCodesPage() {
}
}
const inputClass = cn(
const selectClass = cn(
'w-full rounded-md border border-border bg-card px-3 py-2 text-sm text-foreground',
'placeholder:text-muted-foreground focus:outline-hidden focus:border-primary focus:ring-2 focus:ring-primary/20'
)
@@ -254,12 +255,11 @@ export function InviteCodesPage() {
<div className="space-y-4">
<div>
<label className="mb-1 block text-sm font-medium text-foreground">Recipient Email</label>
<input
<Input
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
placeholder="Optional — will send invite email"
className={inputClass}
/>
</div>
@@ -273,7 +273,7 @@ export function InviteCodesPage() {
setAssignedPlan(plan)
if (plan === 'free') setTrialDays('')
}}
className={inputClass}
className={selectClass}
>
{PLAN_OPTIONS.map(o => (
<option key={o.value} value={o.value}>{o.label}</option>
@@ -284,14 +284,13 @@ export function InviteCodesPage() {
{assignedPlan !== 'free' && (
<div>
<label className="mb-1 block text-sm font-medium text-foreground">Trial Duration (days)</label>
<input
<Input
type="number"
value={trialDays}
onChange={(e) => setTrialDays(e.target.value)}
placeholder="e.g. 14 (1-90)"
min={1}
max={90}
className={inputClass}
/>
<p className="mt-1 text-xs text-muted-foreground">Leave empty for no trial account gets full plan immediately.</p>
</div>
@@ -299,23 +298,21 @@ export function InviteCodesPage() {
<div>
<label className="mb-1 block text-sm font-medium text-foreground">Expires in (days)</label>
<input
<Input
type="number"
value={expiresInDays}
onChange={(e) => setExpiresInDays(e.target.value)}
placeholder="Leave empty for no expiry"
className={inputClass}
/>
</div>
<div>
<label className="mb-1 block text-sm font-medium text-foreground">Note</label>
<input
<Input
type="text"
value={note}
onChange={(e) => setNote(e.target.value)}
placeholder="Optional note (e.g. who this is for)"
className={inputClass}
/>
</div>
</div>

View File

@@ -1,12 +1,12 @@
import { useState, useEffect, useCallback } from 'react'
import { Plus, Trash2, Gauge } from 'lucide-react'
import { Button } from '@/components/ui/Button'
import { Input } from '@/components/ui/Input'
import { DataTable, PageHeader, ActionMenu, EmptyState } from '@/components/admin'
import type { Column } from '@/components/admin'
import { Modal } from '@/components/common/Modal'
import { adminApi } from '@/api/admin'
import { toast } from '@/lib/toast'
import { cn } from '@/lib/utils'
import type { PlanLimitConfig, AccountOverrideResponse, AccountOverrideCreate } from '@/types/admin'
export function PlanLimitsPage() {
@@ -109,11 +109,6 @@ export function PlanLimitsPage() {
},
]
const inputCn = cn(
'w-full rounded-md border border-border bg-card px-3 py-2 text-sm text-foreground',
'placeholder:text-muted-foreground focus:outline-hidden focus:border-primary focus:ring-2 focus:ring-primary/20'
)
return (
<div className="space-y-8">
<PageHeader title="Plan Limits" description="Configure plan tier limits and account-specific overrides" />
@@ -161,15 +156,15 @@ export function PlanLimitsPage() {
<div className="space-y-4">
<div>
<label className="mb-1 block text-sm font-medium text-foreground">Max Trees (empty = unlimited)</label>
<input type="number" value={editPlan.max_trees ?? ''} onChange={(e) => setEditPlan({ ...editPlan, max_trees: e.target.value ? parseInt(e.target.value) : null })} className={inputCn} />
<Input type="number" value={editPlan.max_trees ?? ''} onChange={(e) => setEditPlan({ ...editPlan, max_trees: e.target.value ? parseInt(e.target.value) : null })} />
</div>
<div>
<label className="mb-1 block text-sm font-medium text-foreground">Max Sessions/Month (empty = unlimited)</label>
<input type="number" value={editPlan.max_sessions_per_month ?? ''} onChange={(e) => setEditPlan({ ...editPlan, max_sessions_per_month: e.target.value ? parseInt(e.target.value) : null })} className={inputCn} />
<Input type="number" value={editPlan.max_sessions_per_month ?? ''} onChange={(e) => setEditPlan({ ...editPlan, max_sessions_per_month: e.target.value ? parseInt(e.target.value) : null })} />
</div>
<div>
<label className="mb-1 block text-sm font-medium text-foreground">Max Users (empty = unlimited)</label>
<input type="number" value={editPlan.max_users ?? ''} onChange={(e) => setEditPlan({ ...editPlan, max_users: e.target.value ? parseInt(e.target.value) : null })} className={inputCn} />
<Input type="number" value={editPlan.max_users ?? ''} onChange={(e) => setEditPlan({ ...editPlan, max_users: e.target.value ? parseInt(e.target.value) : null })} />
</div>
</div>
)}
@@ -191,23 +186,23 @@ export function PlanLimitsPage() {
<div className="space-y-4">
<div>
<label className="mb-1 block text-sm font-medium text-foreground">Account Display Code</label>
<input type="text" value={overrideForm.account_display_code} onChange={(e) => setOverrideForm({ ...overrideForm, account_display_code: e.target.value })} placeholder="e.g. ABC-1234" className={inputCn} />
<Input type="text" value={overrideForm.account_display_code} onChange={(e) => setOverrideForm({ ...overrideForm, account_display_code: e.target.value })} placeholder="e.g. ABC-1234" />
</div>
<div>
<label className="mb-1 block text-sm font-medium text-foreground">Max Trees Override</label>
<input type="number" value={overrideForm.override_max_trees ?? ''} onChange={(e) => setOverrideForm({ ...overrideForm, override_max_trees: e.target.value ? parseInt(e.target.value) : null })} className={inputCn} />
<Input type="number" value={overrideForm.override_max_trees ?? ''} onChange={(e) => setOverrideForm({ ...overrideForm, override_max_trees: e.target.value ? parseInt(e.target.value) : null })} />
</div>
<div>
<label className="mb-1 block text-sm font-medium text-foreground">Max Sessions/Month Override</label>
<input type="number" value={overrideForm.override_max_sessions_per_month ?? ''} onChange={(e) => setOverrideForm({ ...overrideForm, override_max_sessions_per_month: e.target.value ? parseInt(e.target.value) : null })} className={inputCn} />
<Input type="number" value={overrideForm.override_max_sessions_per_month ?? ''} onChange={(e) => setOverrideForm({ ...overrideForm, override_max_sessions_per_month: e.target.value ? parseInt(e.target.value) : null })} />
</div>
<div>
<label className="mb-1 block text-sm font-medium text-foreground">Max Users Override</label>
<input type="number" value={overrideForm.override_max_users ?? ''} onChange={(e) => setOverrideForm({ ...overrideForm, override_max_users: e.target.value ? parseInt(e.target.value) : null })} className={inputCn} />
<Input type="number" value={overrideForm.override_max_users ?? ''} onChange={(e) => setOverrideForm({ ...overrideForm, override_max_users: e.target.value ? parseInt(e.target.value) : null })} />
</div>
<div>
<label className="mb-1 block text-sm font-medium text-foreground">Note</label>
<input type="text" value={overrideForm.note ?? ''} onChange={(e) => setOverrideForm({ ...overrideForm, note: e.target.value || null })} placeholder="Reason for override" className={inputCn} />
<Input type="text" value={overrideForm.note ?? ''} onChange={(e) => setOverrideForm({ ...overrideForm, note: e.target.value || null })} placeholder="Reason for override" />
</div>
</div>
</Modal>

View File

@@ -1,5 +1,6 @@
import { useState, useEffect } from 'react'
import { PageHeader } from '@/components/admin'
import { Textarea } from '@/components/ui/Textarea'
import { adminApi } from '@/api/admin'
import { toast } from '@/lib/toast'
import { cn } from '@/lib/utils'
@@ -93,15 +94,11 @@ export function SettingsPage() {
{maintenanceMode && (
<div>
<label className="mb-1 block text-sm font-medium text-foreground">Maintenance Message</label>
<textarea
<Textarea
value={maintenanceMessage}
onChange={(e) => setSettings({ ...settings, maintenance_message: e.target.value })}
rows={3}
placeholder="We're performing scheduled maintenance. Please check back later."
className={cn(
'w-full rounded-md border border-border bg-card px-3 py-2 text-sm text-foreground',
'placeholder:text-muted-foreground focus:outline-hidden focus:border-primary focus:ring-2 focus:ring-primary/20'
)}
/>
</div>
)}

View File

@@ -2,6 +2,7 @@ import { useState, useEffect, useCallback } from 'react'
import { useParams, useNavigate } from 'react-router-dom'
import { ArrowLeft, Shield, Crown, UserCheck, UserX, Clock, Ticket, KeyRound, Copy, Check, Archive, ArchiveRestore, Trash2 } from 'lucide-react'
import { Button } from '@/components/ui/Button'
import { Input } from '@/components/ui/Input'
import { StatusBadge } from '@/components/admin'
import { Modal } from '@/components/common/Modal'
import { Spinner } from '@/components/common/Spinner'
@@ -187,7 +188,7 @@ export function UserDetailPage() {
}
}
const inputClass = cn(
const selectClass = cn(
'w-full rounded-md border border-border bg-card px-3 py-2 text-sm text-foreground',
'placeholder:text-muted-foreground focus:outline-hidden focus:border-primary focus:ring-2 focus:ring-primary/20'
)
@@ -534,7 +535,7 @@ export function UserDetailPage() {
aria-label="Subscription plan"
value={selectedPlan}
onChange={(e) => setSelectedPlan(e.target.value)}
className={inputClass}
className={selectClass}
>
{PLAN_OPTIONS.map(p => (
<option key={p} value={p}>{p.charAt(0).toUpperCase() + p.slice(1)}</option>
@@ -646,13 +647,12 @@ export function UserDetailPage() {
>
<div>
<label className="mb-1 block text-sm font-medium text-foreground">Days to add</label>
<input
<Input
type="number"
value={trialDays}
onChange={(e) => setTrialDays(e.target.value)}
min={1}
max={90}
className={inputClass}
/>
<p className="mt-1 text-xs text-muted-foreground">1-90 days. Will convert to trialing status if not already.</p>
</div>

View File

@@ -2,6 +2,7 @@ import { useState, useEffect, useCallback } from 'react'
import { useNavigate } from 'react-router-dom'
import { UserCheck, UserX, Shield, ArrowRightLeft, ExternalLink, UserPlus, Copy, Check, Mail } from 'lucide-react'
import { Button } from '@/components/ui/Button'
import { Input } from '@/components/ui/Input'
import { DataTable, Pagination, SearchInput, PageHeader, StatusBadge, ActionMenu } from '@/components/admin'
import type { Column } from '@/components/admin'
import { Modal } from '@/components/common/Modal'
@@ -361,15 +362,11 @@ export function UsersPage() {
</p>
<div>
<label className="mb-1 block text-sm font-medium text-foreground">Account Display Code</label>
<input
<Input
type="text"
value={displayCode}
onChange={(e) => setDisplayCode(e.target.value)}
placeholder="e.g. ABC-1234"
className={cn(
'w-full rounded-md border border-border bg-card px-3 py-2 text-sm text-foreground',
'placeholder:text-muted-foreground focus:outline-hidden focus:border-primary focus:ring-2 focus:ring-primary/20'
)}
/>
</div>
</div>
@@ -393,28 +390,20 @@ export function UsersPage() {
<div className="space-y-4">
<div>
<label className="mb-1 block text-sm font-medium text-foreground">Name</label>
<input
<Input
type="text"
value={createForm.name}
onChange={(e) => setCreateForm(f => ({ ...f, name: e.target.value }))}
placeholder="Full name"
className={cn(
'w-full rounded-md border border-border bg-card px-3 py-2 text-sm text-foreground',
'placeholder:text-muted-foreground focus:outline-hidden focus:border-primary focus:ring-2 focus:ring-primary/20'
)}
/>
</div>
<div>
<label className="mb-1 block text-sm font-medium text-foreground">Email</label>
<input
<Input
type="email"
value={createForm.email}
onChange={(e) => setCreateForm(f => ({ ...f, email: e.target.value }))}
placeholder="user@example.com"
className={cn(
'w-full rounded-md border border-border bg-card px-3 py-2 text-sm text-foreground',
'placeholder:text-muted-foreground focus:outline-hidden focus:border-primary focus:ring-2 focus:ring-primary/20'
)}
/>
</div>
<div>
@@ -435,15 +424,11 @@ export function UsersPage() {
<>
<div>
<label className="mb-1 block text-sm font-medium text-foreground">Account Display Code</label>
<input
<Input
type="text"
value={createForm.account_display_code}
onChange={(e) => setCreateForm(f => ({ ...f, account_display_code: e.target.value }))}
placeholder="e.g. ABC12345"
className={cn(
'w-full rounded-md border border-border bg-card px-3 py-2 text-sm text-foreground',
'placeholder:text-muted-foreground focus:outline-hidden focus:border-primary focus:ring-2 focus:ring-primary/20'
)}
/>
</div>
<div>
@@ -532,28 +517,20 @@ export function UsersPage() {
<div className="space-y-4">
<div>
<label className="mb-1 block text-sm font-medium text-foreground">Email</label>
<input
<Input
type="email"
value={inviteForm.email}
onChange={(e) => setInviteForm(f => ({ ...f, email: e.target.value }))}
placeholder="user@example.com"
className={cn(
'w-full rounded-md border border-border bg-card px-3 py-2 text-sm text-foreground',
'placeholder:text-muted-foreground focus:outline-hidden focus:border-primary focus:ring-2 focus:ring-primary/20'
)}
/>
</div>
<div>
<label className="mb-1 block text-sm font-medium text-foreground">Account Display Code</label>
<input
<Input
type="text"
value={inviteForm.account_display_code}
onChange={(e) => setInviteForm(f => ({ ...f, account_display_code: e.target.value }))}
placeholder="e.g. ABC12345"
className={cn(
'w-full rounded-md border border-border bg-card px-3 py-2 text-sm text-foreground',
'placeholder:text-muted-foreground focus:outline-hidden focus:border-primary focus:ring-2 focus:ring-primary/20'
)}
/>
</div>
<div>