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:
@@ -1,7 +1,8 @@
|
|||||||
import { useState } from 'react'
|
import { useState } from 'react'
|
||||||
import { cn } from '@/lib/utils'
|
|
||||||
import { Modal } from '@/components/common/Modal'
|
import { Modal } from '@/components/common/Modal'
|
||||||
import { Button } from '@/components/ui/Button'
|
import { Button } from '@/components/ui/Button'
|
||||||
|
import { Input } from '@/components/ui/Input'
|
||||||
|
import { Textarea } from '@/components/ui/Textarea'
|
||||||
|
|
||||||
interface CreateCategoryModalProps {
|
interface CreateCategoryModalProps {
|
||||||
isOpen: boolean
|
isOpen: boolean
|
||||||
@@ -93,7 +94,7 @@ export function CreateCategoryModal({
|
|||||||
<label htmlFor="name" className="mb-1 block text-sm font-medium text-foreground">
|
<label htmlFor="name" className="mb-1 block text-sm font-medium text-foreground">
|
||||||
Category Name <span className="text-red-400">*</span>
|
Category Name <span className="text-red-400">*</span>
|
||||||
</label>
|
</label>
|
||||||
<input
|
<Input
|
||||||
id="name"
|
id="name"
|
||||||
type="text"
|
type="text"
|
||||||
value={name}
|
value={name}
|
||||||
@@ -102,12 +103,6 @@ export function CreateCategoryModal({
|
|||||||
maxLength={100}
|
maxLength={100}
|
||||||
placeholder="e.g., Network Troubleshooting"
|
placeholder="e.g., Network Troubleshooting"
|
||||||
required
|
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">
|
<p className="mt-1 text-xs text-muted-foreground">
|
||||||
{name.length}/100 characters
|
{name.length}/100 characters
|
||||||
@@ -118,19 +113,13 @@ export function CreateCategoryModal({
|
|||||||
<label htmlFor="description" className="mb-1 block text-sm font-medium text-foreground">
|
<label htmlFor="description" className="mb-1 block text-sm font-medium text-foreground">
|
||||||
Description <span className="text-muted-foreground">(optional)</span>
|
Description <span className="text-muted-foreground">(optional)</span>
|
||||||
</label>
|
</label>
|
||||||
<textarea
|
<Textarea
|
||||||
id="description"
|
id="description"
|
||||||
value={description}
|
value={description}
|
||||||
onChange={(e) => setDescription(e.target.value)}
|
onChange={(e) => setDescription(e.target.value)}
|
||||||
disabled={isSaving}
|
disabled={isSaving}
|
||||||
rows={3}
|
rows={3}
|
||||||
placeholder="Brief description of this category..."
|
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>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
import { useState } from 'react'
|
import { useState } from 'react'
|
||||||
import { cn } from '@/lib/utils'
|
|
||||||
import type { StepCategoryListItem } from '@/types'
|
import type { StepCategoryListItem } from '@/types'
|
||||||
import { Modal } from '@/components/common/Modal'
|
import { Modal } from '@/components/common/Modal'
|
||||||
import { Button } from '@/components/ui/Button'
|
import { Button } from '@/components/ui/Button'
|
||||||
|
import { Input } from '@/components/ui/Input'
|
||||||
|
import { Textarea } from '@/components/ui/Textarea'
|
||||||
|
|
||||||
interface EditCategoryModalProps {
|
interface EditCategoryModalProps {
|
||||||
isOpen: boolean
|
isOpen: boolean
|
||||||
@@ -105,7 +106,7 @@ export function EditCategoryModal({
|
|||||||
<label htmlFor="edit-name" className="mb-1 block text-sm font-medium text-foreground">
|
<label htmlFor="edit-name" className="mb-1 block text-sm font-medium text-foreground">
|
||||||
Category Name <span className="text-red-400">*</span>
|
Category Name <span className="text-red-400">*</span>
|
||||||
</label>
|
</label>
|
||||||
<input
|
<Input
|
||||||
id="edit-name"
|
id="edit-name"
|
||||||
type="text"
|
type="text"
|
||||||
value={name}
|
value={name}
|
||||||
@@ -114,12 +115,6 @@ export function EditCategoryModal({
|
|||||||
maxLength={100}
|
maxLength={100}
|
||||||
placeholder="e.g., Network Troubleshooting"
|
placeholder="e.g., Network Troubleshooting"
|
||||||
required
|
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">
|
<p className="mt-1 text-xs text-muted-foreground">
|
||||||
{name.length}/100 characters
|
{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">
|
<label htmlFor="edit-description" className="mb-1 block text-sm font-medium text-foreground">
|
||||||
Description <span className="text-muted-foreground">(optional)</span>
|
Description <span className="text-muted-foreground">(optional)</span>
|
||||||
</label>
|
</label>
|
||||||
<textarea
|
<Textarea
|
||||||
id="edit-description"
|
id="edit-description"
|
||||||
value={description}
|
value={description}
|
||||||
onChange={(e) => setDescription(e.target.value)}
|
onChange={(e) => setDescription(e.target.value)}
|
||||||
disabled={isSaving}
|
disabled={isSaving}
|
||||||
rows={3}
|
rows={3}
|
||||||
placeholder="Brief description of this category..."
|
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>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
import { useState } from 'react'
|
import { useState } from 'react'
|
||||||
import { Modal } from '@/components/common/Modal'
|
import { Modal } from '@/components/common/Modal'
|
||||||
import { GitFork } from 'lucide-react'
|
import { GitFork } from 'lucide-react'
|
||||||
import { cn } from '@/lib/utils'
|
|
||||||
import { Button } from '@/components/ui/Button'
|
import { Button } from '@/components/ui/Button'
|
||||||
|
import { Input } from '@/components/ui/Input'
|
||||||
|
import { Textarea } from '@/components/ui/Textarea'
|
||||||
|
|
||||||
interface ForkTreeModalProps {
|
interface ForkTreeModalProps {
|
||||||
isOpen: boolean
|
isOpen: boolean
|
||||||
@@ -83,16 +84,12 @@ export function ForkTreeModal({
|
|||||||
<label htmlFor="tree-name" className="mb-1.5 block text-sm font-medium text-foreground">
|
<label htmlFor="tree-name" className="mb-1.5 block text-sm font-medium text-foreground">
|
||||||
Tree Name <span className="text-red-400">*</span>
|
Tree Name <span className="text-red-400">*</span>
|
||||||
</label>
|
</label>
|
||||||
<input
|
<Input
|
||||||
id="tree-name"
|
id="tree-name"
|
||||||
type="text"
|
type="text"
|
||||||
value={name}
|
value={name}
|
||||||
onChange={(e) => setName(e.target.value)}
|
onChange={(e) => setName(e.target.value)}
|
||||||
placeholder="My Custom Tree"
|
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>
|
</div>
|
||||||
|
|
||||||
@@ -100,17 +97,13 @@ export function ForkTreeModal({
|
|||||||
<label htmlFor="tree-description" className="mb-1.5 block text-sm font-medium text-foreground">
|
<label htmlFor="tree-description" className="mb-1.5 block text-sm font-medium text-foreground">
|
||||||
Description <span className="text-muted-foreground">(optional)</span>
|
Description <span className="text-muted-foreground">(optional)</span>
|
||||||
</label>
|
</label>
|
||||||
<textarea
|
<Textarea
|
||||||
id="tree-description"
|
id="tree-description"
|
||||||
value={description}
|
value={description}
|
||||||
onChange={(e) => setDescription(e.target.value)}
|
onChange={(e) => setDescription(e.target.value)}
|
||||||
placeholder="Describe what this tree helps troubleshoot..."
|
placeholder="Describe what this tree helps troubleshoot..."
|
||||||
rows={3}
|
rows={3}
|
||||||
className={cn(
|
className="resize-none"
|
||||||
'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'
|
|
||||||
)}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
import { useState } from 'react'
|
import { useState } from 'react'
|
||||||
import { X } from 'lucide-react'
|
import { X } from 'lucide-react'
|
||||||
import { cn } from '@/lib/utils'
|
|
||||||
import { Button } from '@/components/ui/Button'
|
import { Button } from '@/components/ui/Button'
|
||||||
|
import { Input } from '@/components/ui/Input'
|
||||||
|
import { Textarea } from '@/components/ui/Textarea'
|
||||||
|
|
||||||
interface SaveSessionAsTreeModalProps {
|
interface SaveSessionAsTreeModalProps {
|
||||||
isOpen: boolean
|
isOpen: boolean
|
||||||
@@ -60,7 +61,7 @@ export function SaveSessionAsTreeModal({
|
|||||||
<label htmlFor="treeName" className="mb-1 block text-sm font-medium text-foreground">
|
<label htmlFor="treeName" className="mb-1 block text-sm font-medium text-foreground">
|
||||||
Tree Name <span className="text-muted-foreground">(optional)</span>
|
Tree Name <span className="text-muted-foreground">(optional)</span>
|
||||||
</label>
|
</label>
|
||||||
<input
|
<Input
|
||||||
id="treeName"
|
id="treeName"
|
||||||
type="text"
|
type="text"
|
||||||
value={treeName}
|
value={treeName}
|
||||||
@@ -68,12 +69,6 @@ export function SaveSessionAsTreeModal({
|
|||||||
placeholder={defaultTreeName || "Auto-generated if left blank"}
|
placeholder={defaultTreeName || "Auto-generated if left blank"}
|
||||||
disabled={isSaving}
|
disabled={isSaving}
|
||||||
maxLength={255}
|
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>
|
</div>
|
||||||
|
|
||||||
@@ -82,19 +77,13 @@ export function SaveSessionAsTreeModal({
|
|||||||
<label htmlFor="description" className="mb-1 block text-sm font-medium text-foreground">
|
<label htmlFor="description" className="mb-1 block text-sm font-medium text-foreground">
|
||||||
Description <span className="text-muted-foreground">(optional)</span>
|
Description <span className="text-muted-foreground">(optional)</span>
|
||||||
</label>
|
</label>
|
||||||
<textarea
|
<Textarea
|
||||||
id="description"
|
id="description"
|
||||||
value={description}
|
value={description}
|
||||||
onChange={(e) => setDescription(e.target.value)}
|
onChange={(e) => setDescription(e.target.value)}
|
||||||
placeholder="Add a description for this tree"
|
placeholder="Add a description for this tree"
|
||||||
disabled={isSaving}
|
disabled={isSaving}
|
||||||
rows={3}
|
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>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import { toast } from '@/lib/toast'
|
|||||||
import { Spinner } from '@/components/common/Spinner'
|
import { Spinner } from '@/components/common/Spinner'
|
||||||
import { Modal } from '@/components/common/Modal'
|
import { Modal } from '@/components/common/Modal'
|
||||||
import { Button } from '@/components/ui/Button'
|
import { Button } from '@/components/ui/Button'
|
||||||
|
import { Input } from '@/components/ui/Input'
|
||||||
|
|
||||||
interface ShareSessionModalProps {
|
interface ShareSessionModalProps {
|
||||||
sessionId: string
|
sessionId: string
|
||||||
@@ -246,15 +247,11 @@ export function ShareSessionModal({ sessionId, sessionLabel, isOpen, onClose }:
|
|||||||
<label className="mb-2 block text-sm font-medium text-foreground">
|
<label className="mb-2 block text-sm font-medium text-foreground">
|
||||||
Share Name <span className="text-muted-foreground">(optional)</span>
|
Share Name <span className="text-muted-foreground">(optional)</span>
|
||||||
</label>
|
</label>
|
||||||
<input
|
<Input
|
||||||
type="text"
|
type="text"
|
||||||
value={shareName}
|
value={shareName}
|
||||||
onChange={(e) => setShareName(e.target.value.slice(0, 100))}
|
onChange={(e) => setShareName(e.target.value.slice(0, 100))}
|
||||||
placeholder="e.g. Training link, Customer escalation"
|
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}
|
maxLength={100}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { StarRating } from '@/components/common/StarRating'
|
|||||||
import { cn } from '@/lib/utils'
|
import { cn } from '@/lib/utils'
|
||||||
import type { Step } from '@/types'
|
import type { Step } from '@/types'
|
||||||
import { Button } from '@/components/ui/Button'
|
import { Button } from '@/components/ui/Button'
|
||||||
|
import { Textarea } from '@/components/ui/Textarea'
|
||||||
|
|
||||||
interface StepRatingData {
|
interface StepRatingData {
|
||||||
rating: number
|
rating: number
|
||||||
@@ -164,7 +165,7 @@ export function StepRatingModal({
|
|||||||
<label htmlFor={`review-${step.id}`} className="mb-1 block text-sm font-medium text-foreground">
|
<label htmlFor={`review-${step.id}`} className="mb-1 block text-sm font-medium text-foreground">
|
||||||
Review <span className="text-muted-foreground">(optional)</span>
|
Review <span className="text-muted-foreground">(optional)</span>
|
||||||
</label>
|
</label>
|
||||||
<textarea
|
<Textarea
|
||||||
id={`review-${step.id}`}
|
id={`review-${step.id}`}
|
||||||
value={rating?.review || ''}
|
value={rating?.review || ''}
|
||||||
onChange={(e) => handleReviewChange(step.id, e.target.value)}
|
onChange={(e) => handleReviewChange(step.id, e.target.value)}
|
||||||
@@ -172,12 +173,6 @@ export function StepRatingModal({
|
|||||||
maxLength={500}
|
maxLength={500}
|
||||||
rows={2}
|
rows={2}
|
||||||
placeholder="Share your experience with this step..."
|
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">
|
<p className="mt-1 text-xs text-muted-foreground text-right">
|
||||||
{rating?.review?.length || 0}/500
|
{rating?.review?.length || 0}/500
|
||||||
|
|||||||
@@ -4,6 +4,8 @@ import { cn } from '@/lib/utils'
|
|||||||
import { stepCategoriesApi } from '@/api/stepCategories'
|
import { stepCategoriesApi } from '@/api/stepCategories'
|
||||||
import type { StepCreate, StepCategory, StepCommand } from '@/types/step'
|
import type { StepCreate, StepCategory, StepCommand } from '@/types/step'
|
||||||
import { Button } from '@/components/ui/Button'
|
import { Button } from '@/components/ui/Button'
|
||||||
|
import { Input } from '@/components/ui/Input'
|
||||||
|
import { Textarea } from '@/components/ui/Textarea'
|
||||||
|
|
||||||
interface StepFormProps {
|
interface StepFormProps {
|
||||||
onSubmit: (data: StepCreate) => void
|
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">
|
<label htmlFor="title" className="mb-2 block text-sm font-medium text-foreground">
|
||||||
Title <span className="text-red-400">*</span>
|
Title <span className="text-red-400">*</span>
|
||||||
</label>
|
</label>
|
||||||
<input
|
<Input
|
||||||
id="title"
|
id="title"
|
||||||
type="text"
|
type="text"
|
||||||
value={title}
|
value={title}
|
||||||
onChange={(e) => setTitle(e.target.value)}
|
onChange={(e) => setTitle(e.target.value)}
|
||||||
placeholder="Enter step title"
|
placeholder="Enter step title"
|
||||||
className={cn(
|
error={errors.title}
|
||||||
'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'
|
|
||||||
)}
|
|
||||||
/>
|
/>
|
||||||
{errors.title && (
|
|
||||||
<p className="mt-1 text-xs text-red-400">{errors.title}</p>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Instructions */}
|
{/* Instructions */}
|
||||||
@@ -195,20 +191,14 @@ export function StepForm({ onSubmit, onCancel, initialData, submitLabel, isSubmi
|
|||||||
Instructions <span className="text-red-400">*</span>
|
Instructions <span className="text-red-400">*</span>
|
||||||
<span className="ml-2 text-xs font-normal text-muted-foreground">(Markdown supported)</span>
|
<span className="ml-2 text-xs font-normal text-muted-foreground">(Markdown supported)</span>
|
||||||
</label>
|
</label>
|
||||||
<textarea
|
<Textarea
|
||||||
id="instructions"
|
id="instructions"
|
||||||
value={instructions}
|
value={instructions}
|
||||||
onChange={(e) => setInstructions(e.target.value)}
|
onChange={(e) => setInstructions(e.target.value)}
|
||||||
placeholder="Describe what to do in this step..."
|
placeholder="Describe what to do in this step..."
|
||||||
rows={6}
|
rows={6}
|
||||||
className={cn(
|
error={errors.instructions}
|
||||||
'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'
|
|
||||||
)}
|
|
||||||
/>
|
/>
|
||||||
{errors.instructions && (
|
|
||||||
<p className="mt-1 text-xs text-red-400">{errors.instructions}</p>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Help Text */}
|
{/* 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">
|
<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>
|
Help Text <span className="text-xs font-normal text-muted-foreground">(Optional)</span>
|
||||||
</label>
|
</label>
|
||||||
<textarea
|
<Textarea
|
||||||
id="helpText"
|
id="helpText"
|
||||||
value={helpText}
|
value={helpText}
|
||||||
onChange={(e) => setHelpText(e.target.value)}
|
onChange={(e) => setHelpText(e.target.value)}
|
||||||
placeholder="Additional context or tips..."
|
placeholder="Additional context or tips..."
|
||||||
rows={3}
|
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>
|
</div>
|
||||||
|
|
||||||
@@ -256,31 +245,22 @@ export function StepForm({ onSubmit, onCancel, initialData, submitLabel, isSubmi
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<input
|
<Input
|
||||||
type="text"
|
type="text"
|
||||||
value={cmd.label}
|
value={cmd.label}
|
||||||
onChange={(e) => updateCommand(index, 'label', e.target.value)}
|
onChange={(e) => updateCommand(index, 'label', e.target.value)}
|
||||||
placeholder="Command label (e.g., 'Restart service')"
|
placeholder="Command label (e.g., 'Restart service')"
|
||||||
className={cn(
|
className="py-1.5"
|
||||||
'w-full rounded-md border bg-card px-3 py-1.5 text-sm text-foreground',
|
error={errors[`command_${index}_label`]}
|
||||||
errors[`command_${index}_label`] ? 'border-red-400/50' : 'border-border'
|
|
||||||
)}
|
|
||||||
/>
|
/>
|
||||||
<input
|
<Input
|
||||||
type="text"
|
type="text"
|
||||||
value={cmd.command}
|
value={cmd.command}
|
||||||
onChange={(e) => updateCommand(index, 'command', e.target.value)}
|
onChange={(e) => updateCommand(index, 'command', e.target.value)}
|
||||||
placeholder="Command (e.g., 'systemctl restart nginx')"
|
placeholder="Command (e.g., 'systemctl restart nginx')"
|
||||||
className={cn(
|
className="py-1.5 font-mono"
|
||||||
'w-full rounded-md border bg-card px-3 py-1.5 font-mono text-sm text-foreground',
|
error={errors[`command_${index}_command`]}
|
||||||
errors[`command_${index}_command`] ? 'border-red-400/50' : 'border-border'
|
|
||||||
)}
|
|
||||||
/>
|
/>
|
||||||
{(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>
|
||||||
</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>
|
Tags <span className="text-xs font-normal text-muted-foreground">(Optional)</span>
|
||||||
</label>
|
</label>
|
||||||
<div className="flex gap-2">
|
<div className="flex gap-2">
|
||||||
<input
|
<Input
|
||||||
id="tagInput"
|
id="tagInput"
|
||||||
type="text"
|
type="text"
|
||||||
value={tagInput}
|
value={tagInput}
|
||||||
onChange={(e) => setTagInput(e.target.value)}
|
onChange={(e) => setTagInput(e.target.value)}
|
||||||
onKeyDown={handleTagInputKeyDown}
|
onKeyDown={handleTagInputKeyDown}
|
||||||
placeholder="Type tag and press Enter"
|
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
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { useState, useEffect, useCallback } from 'react'
|
import { useState, useEffect, useCallback } from 'react'
|
||||||
import { Plus, Trash2, Pencil, FolderTree } from 'lucide-react'
|
import { Plus, Trash2, Pencil, FolderTree } from 'lucide-react'
|
||||||
import { Button } from '@/components/ui/Button'
|
import { Button } from '@/components/ui/Button'
|
||||||
import { cn } from '@/lib/utils'
|
import { Input } from '@/components/ui/Input'
|
||||||
import { toast } from '@/lib/toast'
|
import { toast } from '@/lib/toast'
|
||||||
import { Modal } from '@/components/common/Modal'
|
import { Modal } from '@/components/common/Modal'
|
||||||
import { PageHeader } from '@/components/common/PageHeader'
|
import { PageHeader } from '@/components/common/PageHeader'
|
||||||
@@ -79,8 +79,6 @@ export function TeamCategoriesPage() {
|
|||||||
setForm({ name: cat.name, slug: cat.slug, description: cat.description || '' })
|
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 (
|
return (
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
<PageHeader
|
<PageHeader
|
||||||
@@ -141,15 +139,15 @@ export function TeamCategoriesPage() {
|
|||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<div>
|
<div>
|
||||||
<label className="mb-1 block text-sm font-medium text-foreground">Name</label>
|
<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>
|
||||||
<div>
|
<div>
|
||||||
<label className="mb-1 block text-sm font-medium text-foreground">Slug</label>
|
<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>
|
||||||
<div>
|
<div>
|
||||||
<label className="mb-1 block text-sm font-medium text-foreground">Description</label>
|
<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>
|
||||||
</div>
|
</div>
|
||||||
</Modal>
|
</Modal>
|
||||||
@@ -166,15 +164,15 @@ export function TeamCategoriesPage() {
|
|||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<div>
|
<div>
|
||||||
<label className="mb-1 block text-sm font-medium text-foreground">Name</label>
|
<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>
|
||||||
<div>
|
<div>
|
||||||
<label className="mb-1 block text-sm font-medium text-foreground">Slug</label>
|
<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>
|
||||||
<div>
|
<div>
|
||||||
<label className="mb-1 block text-sm font-medium text-foreground">Description</label>
|
<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>
|
||||||
</div>
|
</div>
|
||||||
</Modal>
|
</Modal>
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { useState, useEffect, useCallback } from 'react'
|
import { useState, useEffect, useCallback } from 'react'
|
||||||
import { Plus, Trash2, ToggleLeft } from 'lucide-react'
|
import { Plus, Trash2, ToggleLeft } from 'lucide-react'
|
||||||
import { Button } from '@/components/ui/Button'
|
import { Button } from '@/components/ui/Button'
|
||||||
|
import { Input } from '@/components/ui/Input'
|
||||||
import { DataTable, PageHeader, StatusBadge, ActionMenu, EmptyState } from '@/components/admin'
|
import { DataTable, PageHeader, StatusBadge, ActionMenu, EmptyState } from '@/components/admin'
|
||||||
import type { Column } from '@/components/admin'
|
import type { Column } from '@/components/admin'
|
||||||
import { Modal } from '@/components/common/Modal'
|
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 (
|
return (
|
||||||
<div className="space-y-8">
|
<div className="space-y-8">
|
||||||
@@ -197,15 +198,15 @@ export function FeatureFlagsPage() {
|
|||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<div>
|
<div>
|
||||||
<label className="mb-1 block text-sm font-medium text-foreground">Flag Key</label>
|
<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>
|
||||||
<div>
|
<div>
|
||||||
<label className="mb-1 block text-sm font-medium text-foreground">Display Name</label>
|
<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>
|
||||||
<div>
|
<div>
|
||||||
<label className="mb-1 block text-sm font-medium text-foreground">Description</label>
|
<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>
|
||||||
</div>
|
</div>
|
||||||
</Modal>
|
</Modal>
|
||||||
@@ -222,11 +223,11 @@ export function FeatureFlagsPage() {
|
|||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<div>
|
<div>
|
||||||
<label className="mb-1 block text-sm font-medium text-foreground">Account Display Code</label>
|
<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>
|
||||||
<div>
|
<div>
|
||||||
<label className="mb-1 block text-sm font-medium text-foreground">Feature Flag</label>
|
<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>
|
<option value="">Select a flag...</option>
|
||||||
{flags.map(f => <option key={f.id} value={f.id}>{f.display_name}</option>)}
|
{flags.map(f => <option key={f.id} value={f.id}>{f.display_name}</option>)}
|
||||||
</select>
|
</select>
|
||||||
@@ -237,7 +238,7 @@ export function FeatureFlagsPage() {
|
|||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label className="mb-1 block text-sm font-medium text-foreground">Note</label>
|
<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>
|
||||||
</div>
|
</div>
|
||||||
</Modal>
|
</Modal>
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
import { useState, useEffect, useCallback } from 'react'
|
import { useState, useEffect, useCallback } from 'react'
|
||||||
import { Plus, Trash2, Pencil, FolderTree } from 'lucide-react'
|
import { Plus, Trash2, Pencil, FolderTree } from 'lucide-react'
|
||||||
import { Button } from '@/components/ui/Button'
|
import { Button } from '@/components/ui/Button'
|
||||||
|
import { Input } from '@/components/ui/Input'
|
||||||
import { DataTable, PageHeader, ActionMenu, EmptyState } from '@/components/admin'
|
import { DataTable, PageHeader, ActionMenu, EmptyState } from '@/components/admin'
|
||||||
import type { Column } from '@/components/admin'
|
import type { Column } from '@/components/admin'
|
||||||
import { Modal } from '@/components/common/Modal'
|
import { Modal } from '@/components/common/Modal'
|
||||||
import { adminApi } from '@/api/admin'
|
import { adminApi } from '@/api/admin'
|
||||||
import { toast } from '@/lib/toast'
|
import { toast } from '@/lib/toast'
|
||||||
import { cn } from '@/lib/utils'
|
|
||||||
import type { AdminCategory, GlobalCategoryCreate } from '@/types/admin'
|
import type { AdminCategory, GlobalCategoryCreate } from '@/types/admin'
|
||||||
|
|
||||||
export function GlobalCategoriesPage() {
|
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 (
|
return (
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
<PageHeader
|
<PageHeader
|
||||||
@@ -127,15 +125,15 @@ export function GlobalCategoriesPage() {
|
|||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<div>
|
<div>
|
||||||
<label className="mb-1 block text-sm font-medium text-foreground">Name</label>
|
<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>
|
||||||
<div>
|
<div>
|
||||||
<label className="mb-1 block text-sm font-medium text-foreground">Slug</label>
|
<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>
|
||||||
<div>
|
<div>
|
||||||
<label className="mb-1 block text-sm font-medium text-foreground">Description</label>
|
<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>
|
||||||
</div>
|
</div>
|
||||||
</Modal>
|
</Modal>
|
||||||
@@ -156,15 +154,15 @@ export function GlobalCategoriesPage() {
|
|||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<div>
|
<div>
|
||||||
<label className="mb-1 block text-sm font-medium text-foreground">Name</label>
|
<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>
|
||||||
<div>
|
<div>
|
||||||
<label className="mb-1 block text-sm font-medium text-foreground">Slug</label>
|
<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>
|
||||||
<div>
|
<div>
|
||||||
<label className="mb-1 block text-sm font-medium text-foreground">Description</label>
|
<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>
|
||||||
</div>
|
</div>
|
||||||
</Modal>
|
</Modal>
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { useState, useEffect, useCallback } from 'react'
|
import { useState, useEffect, useCallback } from 'react'
|
||||||
import { Plus, Copy, Trash2, Ticket, Mail, MailCheck, RefreshCw } from 'lucide-react'
|
import { Plus, Copy, Trash2, Ticket, Mail, MailCheck, RefreshCw } from 'lucide-react'
|
||||||
import { Button } from '@/components/ui/Button'
|
import { Button } from '@/components/ui/Button'
|
||||||
|
import { Input } from '@/components/ui/Input'
|
||||||
import { DataTable, PageHeader, StatusBadge, ActionMenu, EmptyState } from '@/components/admin'
|
import { DataTable, PageHeader, StatusBadge, ActionMenu, EmptyState } from '@/components/admin'
|
||||||
import type { Column } from '@/components/admin'
|
import type { Column } from '@/components/admin'
|
||||||
import { Modal } from '@/components/common/Modal'
|
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',
|
'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'
|
'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 className="space-y-4">
|
||||||
<div>
|
<div>
|
||||||
<label className="mb-1 block text-sm font-medium text-foreground">Recipient Email</label>
|
<label className="mb-1 block text-sm font-medium text-foreground">Recipient Email</label>
|
||||||
<input
|
<Input
|
||||||
type="email"
|
type="email"
|
||||||
value={email}
|
value={email}
|
||||||
onChange={(e) => setEmail(e.target.value)}
|
onChange={(e) => setEmail(e.target.value)}
|
||||||
placeholder="Optional — will send invite email"
|
placeholder="Optional — will send invite email"
|
||||||
className={inputClass}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -273,7 +273,7 @@ export function InviteCodesPage() {
|
|||||||
setAssignedPlan(plan)
|
setAssignedPlan(plan)
|
||||||
if (plan === 'free') setTrialDays('')
|
if (plan === 'free') setTrialDays('')
|
||||||
}}
|
}}
|
||||||
className={inputClass}
|
className={selectClass}
|
||||||
>
|
>
|
||||||
{PLAN_OPTIONS.map(o => (
|
{PLAN_OPTIONS.map(o => (
|
||||||
<option key={o.value} value={o.value}>{o.label}</option>
|
<option key={o.value} value={o.value}>{o.label}</option>
|
||||||
@@ -284,14 +284,13 @@ export function InviteCodesPage() {
|
|||||||
{assignedPlan !== 'free' && (
|
{assignedPlan !== 'free' && (
|
||||||
<div>
|
<div>
|
||||||
<label className="mb-1 block text-sm font-medium text-foreground">Trial Duration (days)</label>
|
<label className="mb-1 block text-sm font-medium text-foreground">Trial Duration (days)</label>
|
||||||
<input
|
<Input
|
||||||
type="number"
|
type="number"
|
||||||
value={trialDays}
|
value={trialDays}
|
||||||
onChange={(e) => setTrialDays(e.target.value)}
|
onChange={(e) => setTrialDays(e.target.value)}
|
||||||
placeholder="e.g. 14 (1-90)"
|
placeholder="e.g. 14 (1-90)"
|
||||||
min={1}
|
min={1}
|
||||||
max={90}
|
max={90}
|
||||||
className={inputClass}
|
|
||||||
/>
|
/>
|
||||||
<p className="mt-1 text-xs text-muted-foreground">Leave empty for no trial — account gets full plan immediately.</p>
|
<p className="mt-1 text-xs text-muted-foreground">Leave empty for no trial — account gets full plan immediately.</p>
|
||||||
</div>
|
</div>
|
||||||
@@ -299,23 +298,21 @@ export function InviteCodesPage() {
|
|||||||
|
|
||||||
<div>
|
<div>
|
||||||
<label className="mb-1 block text-sm font-medium text-foreground">Expires in (days)</label>
|
<label className="mb-1 block text-sm font-medium text-foreground">Expires in (days)</label>
|
||||||
<input
|
<Input
|
||||||
type="number"
|
type="number"
|
||||||
value={expiresInDays}
|
value={expiresInDays}
|
||||||
onChange={(e) => setExpiresInDays(e.target.value)}
|
onChange={(e) => setExpiresInDays(e.target.value)}
|
||||||
placeholder="Leave empty for no expiry"
|
placeholder="Leave empty for no expiry"
|
||||||
className={inputClass}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<label className="mb-1 block text-sm font-medium text-foreground">Note</label>
|
<label className="mb-1 block text-sm font-medium text-foreground">Note</label>
|
||||||
<input
|
<Input
|
||||||
type="text"
|
type="text"
|
||||||
value={note}
|
value={note}
|
||||||
onChange={(e) => setNote(e.target.value)}
|
onChange={(e) => setNote(e.target.value)}
|
||||||
placeholder="Optional note (e.g. who this is for)"
|
placeholder="Optional note (e.g. who this is for)"
|
||||||
className={inputClass}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
import { useState, useEffect, useCallback } from 'react'
|
import { useState, useEffect, useCallback } from 'react'
|
||||||
import { Plus, Trash2, Gauge } from 'lucide-react'
|
import { Plus, Trash2, Gauge } from 'lucide-react'
|
||||||
import { Button } from '@/components/ui/Button'
|
import { Button } from '@/components/ui/Button'
|
||||||
|
import { Input } from '@/components/ui/Input'
|
||||||
import { DataTable, PageHeader, ActionMenu, EmptyState } from '@/components/admin'
|
import { DataTable, PageHeader, ActionMenu, EmptyState } from '@/components/admin'
|
||||||
import type { Column } from '@/components/admin'
|
import type { Column } from '@/components/admin'
|
||||||
import { Modal } from '@/components/common/Modal'
|
import { Modal } from '@/components/common/Modal'
|
||||||
import { adminApi } from '@/api/admin'
|
import { adminApi } from '@/api/admin'
|
||||||
import { toast } from '@/lib/toast'
|
import { toast } from '@/lib/toast'
|
||||||
import { cn } from '@/lib/utils'
|
|
||||||
import type { PlanLimitConfig, AccountOverrideResponse, AccountOverrideCreate } from '@/types/admin'
|
import type { PlanLimitConfig, AccountOverrideResponse, AccountOverrideCreate } from '@/types/admin'
|
||||||
|
|
||||||
export function PlanLimitsPage() {
|
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 (
|
return (
|
||||||
<div className="space-y-8">
|
<div className="space-y-8">
|
||||||
<PageHeader title="Plan Limits" description="Configure plan tier limits and account-specific overrides" />
|
<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 className="space-y-4">
|
||||||
<div>
|
<div>
|
||||||
<label className="mb-1 block text-sm font-medium text-foreground">Max Trees (empty = unlimited)</label>
|
<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>
|
||||||
<div>
|
<div>
|
||||||
<label className="mb-1 block text-sm font-medium text-foreground">Max Sessions/Month (empty = unlimited)</label>
|
<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>
|
||||||
<div>
|
<div>
|
||||||
<label className="mb-1 block text-sm font-medium text-foreground">Max Users (empty = unlimited)</label>
|
<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>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@@ -191,23 +186,23 @@ export function PlanLimitsPage() {
|
|||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<div>
|
<div>
|
||||||
<label className="mb-1 block text-sm font-medium text-foreground">Account Display Code</label>
|
<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>
|
||||||
<div>
|
<div>
|
||||||
<label className="mb-1 block text-sm font-medium text-foreground">Max Trees Override</label>
|
<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>
|
||||||
<div>
|
<div>
|
||||||
<label className="mb-1 block text-sm font-medium text-foreground">Max Sessions/Month Override</label>
|
<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>
|
||||||
<div>
|
<div>
|
||||||
<label className="mb-1 block text-sm font-medium text-foreground">Max Users Override</label>
|
<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>
|
||||||
<div>
|
<div>
|
||||||
<label className="mb-1 block text-sm font-medium text-foreground">Note</label>
|
<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>
|
||||||
</div>
|
</div>
|
||||||
</Modal>
|
</Modal>
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { useState, useEffect } from 'react'
|
import { useState, useEffect } from 'react'
|
||||||
import { PageHeader } from '@/components/admin'
|
import { PageHeader } from '@/components/admin'
|
||||||
|
import { Textarea } from '@/components/ui/Textarea'
|
||||||
import { adminApi } from '@/api/admin'
|
import { adminApi } from '@/api/admin'
|
||||||
import { toast } from '@/lib/toast'
|
import { toast } from '@/lib/toast'
|
||||||
import { cn } from '@/lib/utils'
|
import { cn } from '@/lib/utils'
|
||||||
@@ -93,15 +94,11 @@ export function SettingsPage() {
|
|||||||
{maintenanceMode && (
|
{maintenanceMode && (
|
||||||
<div>
|
<div>
|
||||||
<label className="mb-1 block text-sm font-medium text-foreground">Maintenance Message</label>
|
<label className="mb-1 block text-sm font-medium text-foreground">Maintenance Message</label>
|
||||||
<textarea
|
<Textarea
|
||||||
value={maintenanceMessage}
|
value={maintenanceMessage}
|
||||||
onChange={(e) => setSettings({ ...settings, maintenance_message: e.target.value })}
|
onChange={(e) => setSettings({ ...settings, maintenance_message: e.target.value })}
|
||||||
rows={3}
|
rows={3}
|
||||||
placeholder="We're performing scheduled maintenance. Please check back later."
|
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>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { useState, useEffect, useCallback } from 'react'
|
|||||||
import { useParams, useNavigate } from 'react-router-dom'
|
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 { ArrowLeft, Shield, Crown, UserCheck, UserX, Clock, Ticket, KeyRound, Copy, Check, Archive, ArchiveRestore, Trash2 } from 'lucide-react'
|
||||||
import { Button } from '@/components/ui/Button'
|
import { Button } from '@/components/ui/Button'
|
||||||
|
import { Input } from '@/components/ui/Input'
|
||||||
import { StatusBadge } from '@/components/admin'
|
import { StatusBadge } from '@/components/admin'
|
||||||
import { Modal } from '@/components/common/Modal'
|
import { Modal } from '@/components/common/Modal'
|
||||||
import { Spinner } from '@/components/common/Spinner'
|
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',
|
'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'
|
'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"
|
aria-label="Subscription plan"
|
||||||
value={selectedPlan}
|
value={selectedPlan}
|
||||||
onChange={(e) => setSelectedPlan(e.target.value)}
|
onChange={(e) => setSelectedPlan(e.target.value)}
|
||||||
className={inputClass}
|
className={selectClass}
|
||||||
>
|
>
|
||||||
{PLAN_OPTIONS.map(p => (
|
{PLAN_OPTIONS.map(p => (
|
||||||
<option key={p} value={p}>{p.charAt(0).toUpperCase() + p.slice(1)}</option>
|
<option key={p} value={p}>{p.charAt(0).toUpperCase() + p.slice(1)}</option>
|
||||||
@@ -646,13 +647,12 @@ export function UserDetailPage() {
|
|||||||
>
|
>
|
||||||
<div>
|
<div>
|
||||||
<label className="mb-1 block text-sm font-medium text-foreground">Days to add</label>
|
<label className="mb-1 block text-sm font-medium text-foreground">Days to add</label>
|
||||||
<input
|
<Input
|
||||||
type="number"
|
type="number"
|
||||||
value={trialDays}
|
value={trialDays}
|
||||||
onChange={(e) => setTrialDays(e.target.value)}
|
onChange={(e) => setTrialDays(e.target.value)}
|
||||||
min={1}
|
min={1}
|
||||||
max={90}
|
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>
|
<p className="mt-1 text-xs text-muted-foreground">1-90 days. Will convert to trialing status if not already.</p>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { useState, useEffect, useCallback } from 'react'
|
|||||||
import { useNavigate } from 'react-router-dom'
|
import { useNavigate } from 'react-router-dom'
|
||||||
import { UserCheck, UserX, Shield, ArrowRightLeft, ExternalLink, UserPlus, Copy, Check, Mail } from 'lucide-react'
|
import { UserCheck, UserX, Shield, ArrowRightLeft, ExternalLink, UserPlus, Copy, Check, Mail } from 'lucide-react'
|
||||||
import { Button } from '@/components/ui/Button'
|
import { Button } from '@/components/ui/Button'
|
||||||
|
import { Input } from '@/components/ui/Input'
|
||||||
import { DataTable, Pagination, SearchInput, PageHeader, StatusBadge, ActionMenu } from '@/components/admin'
|
import { DataTable, Pagination, SearchInput, PageHeader, StatusBadge, ActionMenu } from '@/components/admin'
|
||||||
import type { Column } from '@/components/admin'
|
import type { Column } from '@/components/admin'
|
||||||
import { Modal } from '@/components/common/Modal'
|
import { Modal } from '@/components/common/Modal'
|
||||||
@@ -361,15 +362,11 @@ export function UsersPage() {
|
|||||||
</p>
|
</p>
|
||||||
<div>
|
<div>
|
||||||
<label className="mb-1 block text-sm font-medium text-foreground">Account Display Code</label>
|
<label className="mb-1 block text-sm font-medium text-foreground">Account Display Code</label>
|
||||||
<input
|
<Input
|
||||||
type="text"
|
type="text"
|
||||||
value={displayCode}
|
value={displayCode}
|
||||||
onChange={(e) => setDisplayCode(e.target.value)}
|
onChange={(e) => setDisplayCode(e.target.value)}
|
||||||
placeholder="e.g. ABC-1234"
|
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>
|
||||||
</div>
|
</div>
|
||||||
@@ -393,28 +390,20 @@ export function UsersPage() {
|
|||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<div>
|
<div>
|
||||||
<label className="mb-1 block text-sm font-medium text-foreground">Name</label>
|
<label className="mb-1 block text-sm font-medium text-foreground">Name</label>
|
||||||
<input
|
<Input
|
||||||
type="text"
|
type="text"
|
||||||
value={createForm.name}
|
value={createForm.name}
|
||||||
onChange={(e) => setCreateForm(f => ({ ...f, name: e.target.value }))}
|
onChange={(e) => setCreateForm(f => ({ ...f, name: e.target.value }))}
|
||||||
placeholder="Full name"
|
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>
|
||||||
<div>
|
<div>
|
||||||
<label className="mb-1 block text-sm font-medium text-foreground">Email</label>
|
<label className="mb-1 block text-sm font-medium text-foreground">Email</label>
|
||||||
<input
|
<Input
|
||||||
type="email"
|
type="email"
|
||||||
value={createForm.email}
|
value={createForm.email}
|
||||||
onChange={(e) => setCreateForm(f => ({ ...f, email: e.target.value }))}
|
onChange={(e) => setCreateForm(f => ({ ...f, email: e.target.value }))}
|
||||||
placeholder="user@example.com"
|
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>
|
||||||
<div>
|
<div>
|
||||||
@@ -435,15 +424,11 @@ export function UsersPage() {
|
|||||||
<>
|
<>
|
||||||
<div>
|
<div>
|
||||||
<label className="mb-1 block text-sm font-medium text-foreground">Account Display Code</label>
|
<label className="mb-1 block text-sm font-medium text-foreground">Account Display Code</label>
|
||||||
<input
|
<Input
|
||||||
type="text"
|
type="text"
|
||||||
value={createForm.account_display_code}
|
value={createForm.account_display_code}
|
||||||
onChange={(e) => setCreateForm(f => ({ ...f, account_display_code: e.target.value }))}
|
onChange={(e) => setCreateForm(f => ({ ...f, account_display_code: e.target.value }))}
|
||||||
placeholder="e.g. ABC12345"
|
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>
|
||||||
<div>
|
<div>
|
||||||
@@ -532,28 +517,20 @@ export function UsersPage() {
|
|||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<div>
|
<div>
|
||||||
<label className="mb-1 block text-sm font-medium text-foreground">Email</label>
|
<label className="mb-1 block text-sm font-medium text-foreground">Email</label>
|
||||||
<input
|
<Input
|
||||||
type="email"
|
type="email"
|
||||||
value={inviteForm.email}
|
value={inviteForm.email}
|
||||||
onChange={(e) => setInviteForm(f => ({ ...f, email: e.target.value }))}
|
onChange={(e) => setInviteForm(f => ({ ...f, email: e.target.value }))}
|
||||||
placeholder="user@example.com"
|
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>
|
||||||
<div>
|
<div>
|
||||||
<label className="mb-1 block text-sm font-medium text-foreground">Account Display Code</label>
|
<label className="mb-1 block text-sm font-medium text-foreground">Account Display Code</label>
|
||||||
<input
|
<Input
|
||||||
type="text"
|
type="text"
|
||||||
value={inviteForm.account_display_code}
|
value={inviteForm.account_display_code}
|
||||||
onChange={(e) => setInviteForm(f => ({ ...f, account_display_code: e.target.value }))}
|
onChange={(e) => setInviteForm(f => ({ ...f, account_display_code: e.target.value }))}
|
||||||
placeholder="e.g. ABC12345"
|
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>
|
||||||
<div>
|
<div>
|
||||||
|
|||||||
Reference in New Issue
Block a user