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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 29 additions & 0 deletions app/app/create/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import { getTokenMetadata, getTokenBalance } from '@/lib/contract'
import { parseTokenAmount, formatTokenAmount } from '@/lib/stream-utils'
import { StreamPreview } from '@/components/streams/stream-preview'
import { CreateConfirmation } from '@/components/streams/create-confirmation'
import { StreamTemplates, type StreamTemplate } from '@/components/streams/stream-templates'
import type { TokenInfo } from '@/types/stream'

const CUSTOM_VALUE = '__custom__'
Expand Down Expand Up @@ -118,6 +119,7 @@ function CreateForm() {
})

const [errors, setErrors] = useState<Partial<Record<keyof FormState, string>>>({})
const [selectedTemplateId, setSelectedTemplateId] = useState<string | undefined>(undefined)

const selectedToken = isCustom && customToken
? customToken
Expand Down Expand Up @@ -255,6 +257,30 @@ function CreateForm() {
}
}

function handleTemplateSelect(template: StreamTemplate) {
setSelectedTemplateId(template.id)
const newStart = localDatetimeMin(60)
const newEnd = addDuration(newStart, template.durationSeconds)
const hasCliff = template.cliffSeconds > 0
const newCliff = hasCliff ? addDuration(newStart, template.cliffSeconds) : newStart

setForm((prev) => {
const amount = prev.amount
const cliffAmount = hasCliff && template.cliffPercent > 0 && amount
? String(Math.floor(Number(amount) * template.cliffPercent / 100))
: ''
return {
...prev,
startDate: newStart,
endDate: newEnd,
hasCliff,
cliffDate: newCliff,
cliffAmount,
}
})
setErrors({})
}

const input = showConfirmation ? buildInput() : null
const durationSeconds = (new Date(form.endDate).getTime() - new Date(form.startDate).getTime()) / 1000
const amountPerSecond = input && durationSeconds > 0
Expand All @@ -279,6 +305,9 @@ function CreateForm() {
</p>
</div>

<div className="mt-8 rounded-2xl border border-border bg-card p-5">
<StreamTemplates onSelect={handleTemplateSelect} selectedId={selectedTemplateId} />
</div>
{cloneId && (
<div className="mt-4 flex items-center gap-2.5 rounded-xl border border-primary/30 bg-primary/5 px-4 py-3">
<Copy className="size-4 shrink-0 text-primary" />
Expand Down
113 changes: 113 additions & 0 deletions components/streams/stream-templates.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
'use client'

import { Briefcase, Lock, Gift, CreditCard } from 'lucide-react'

export interface StreamTemplate {
id: string
name: string
description: string
icon: React.ComponentType<{ className?: string }>
durationSeconds: number
cliffSeconds: number
cliffPercent: number
badge: string
}

export const STREAM_TEMPLATES: StreamTemplate[] = [
{
id: 'payroll',
name: 'Payroll',
description: 'Monthly salary streaming',
icon: Briefcase,
durationSeconds: 30 * 24 * 3600,
cliffSeconds: 0,
cliffPercent: 0,
badge: '1 month',
},
{
id: 'vesting',
name: 'Token Vesting',
description: 'Standard vesting schedule',
icon: Lock,
durationSeconds: 2 * 365 * 24 * 3600,
cliffSeconds: 365 * 24 * 3600,
cliffPercent: 25,
badge: '2yr / 1yr cliff',
},
{
id: 'grant',
name: 'Grant / Sponsorship',
description: 'Milestone-based grant',
icon: Gift,
durationSeconds: 90 * 24 * 3600,
cliffSeconds: 30 * 24 * 3600,
cliffPercent: 10,
badge: '3 months',
},
{
id: 'subscription',
name: 'Subscription',
description: 'Continuous subscription payment',
icon: CreditCard,
durationSeconds: 30 * 24 * 3600,
cliffSeconds: 0,
cliffPercent: 0,
badge: '1 month',
},
]

interface StreamTemplatesProps {
onSelect: (template: StreamTemplate) => void
selectedId?: string
}

export function StreamTemplates({ onSelect, selectedId }: StreamTemplatesProps) {
return (
<div className="space-y-3">
<div className="flex items-center justify-between">
<h2 className="text-sm font-medium text-muted-foreground uppercase tracking-wide">
Quick templates
</h2>
<span className="text-xs text-muted-foreground">Select one to pre-fill the form</span>
</div>
<div className="grid grid-cols-2 gap-3 sm:grid-cols-4">
{STREAM_TEMPLATES.map((tpl) => {
const Icon = tpl.icon
const isSelected = selectedId === tpl.id
return (
<button
key={tpl.id}
type="button"
onClick={() => onSelect(tpl)}
className={
'flex flex-col items-start gap-2 rounded-xl border p-3 text-left transition-all hover:border-primary hover:shadow-sm ' +
(isSelected
? 'border-primary bg-primary/5 shadow-sm'
: 'border-border bg-card')
}
>
<div className={
'flex size-8 items-center justify-center rounded-lg ' +
(isSelected ? 'bg-primary/10 text-primary' : 'bg-secondary text-muted-foreground')
}>
<Icon className="size-4" />
</div>
<div className="min-w-0">
<p className="text-xs font-semibold leading-tight">{tpl.name}</p>
<p className="mt-0.5 text-xs text-muted-foreground leading-tight">{tpl.description}</p>
</div>
<span className={
'rounded-full px-2 py-0.5 text-xs font-medium ' +
(isSelected
? 'bg-primary/10 text-primary'
: 'bg-secondary text-muted-foreground')
}>
{tpl.badge}
</span>
</button>
)
})}
</div>
</div>
)
}
Loading