TypeScript 97.5%
SQL 1.4%
Python 0.8%
1"use client";2import * as React from "react";3import { useRouter } from "next/navigation";4import { Plus } from "lucide-react";5import { COUNTRIES } from "@fetcha/core/client";6import { createDashboardSession, type DashboardSession } from "@/actions/sessions";7import { Button } from "@/components/ui/button";8import { Dialog, DialogClose, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, DialogTrigger } from "@/components/ui/dialog";9import { Field, FieldError, Hint, Label } from "@/components/ui/label";10import { Input } from "@/components/ui/input";11import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";12import { Alert } from "@/components/ui/alert";13import { CodeBlock } from "@/components/ui/code-block";14import { CopyButton } from "@/components/ui/copy-button";15import { Badge } from "@/components/ui/badge";1617const ANY = "__any__";1819function curlFor(sessionId: string, apiBase: string, country?: string): string {20 const body: Record<string, unknown> = { url: "https://example.com", session: sessionId };21 if (country) body.country = country;22 return `curl -X POST ${apiBase}/v1/fetch \\23 -H "Authorization: Bearer fch_live_YOUR_KEY" \\24 -H "Content-Type: application/json" \\25 -d '${JSON.stringify(body)}'`;26}2728export function CreateSessionDialog({ apiBase, defaultCountry }: { apiBase: string; defaultCountry?: string | null }) {29 const router = useRouter();30 const [open, setOpen] = React.useState(false);31 const [pending, startTransition] = React.useTransition();32 const [error, setError] = React.useState<string | null>(null);33 const [created, setCreated] = React.useState<DashboardSession | null>(null);3435 const [country, setCountry] = React.useState<string>(defaultCountry && defaultCountry in COUNTRIES ? defaultCountry : ANY);36 const [network, setNetwork] = React.useState<string>("auto");37 const [region, setRegion] = React.useState("");38 const [city, setCity] = React.useState("");39 const [ttl, setTtl] = React.useState(600);40 const [label, setLabel] = React.useState("");4142 function reset() {43 setError(null);44 setCreated(null);45 setRegion("");46 setCity("");47 setTtl(600);48 setLabel("");49 setNetwork("auto");50 }5152 function submit(e: React.FormEvent) {53 e.preventDefault();54 setError(null);55 startTransition(async () => {56 const res = await createDashboardSession({57 country: country === ANY ? undefined : country,58 region: region || undefined,59 city: city || undefined,60 network: network as "auto" | "residential",61 ttl,62 label: label || undefined,63 });64 if (!res.ok) {65 setError(res.error);66 return;67 }68 setCreated(res.data ?? null);69 router.refresh();70 });71 }7273 const ttlLabel = ttl >= 60 ? `${Math.floor(ttl / 60)} min${ttl % 60 ? ` ${ttl % 60} s` : ""}` : `${ttl} s`;7475 return (76 <Dialog77 open={open}78 onOpenChange={(o) => {79 setOpen(o);80 if (!o) reset();81 }}82 >83 <DialogTrigger asChild>84 <Button variant="primary" size="sm">85 <Plus /> Create session86 </Button>87 </DialogTrigger>88 <DialogContent size="lg">89 {created ? (90 <>91 <DialogHeader>92 <DialogTitle>Session created</DialogTitle>93 <DialogDescription>94 Pass this id as <code className="font-mono">session</code> in each request. It keeps the same exit IP and cookies until it expires or you close it.95 </DialogDescription>96 </DialogHeader>97 <div className="flex items-center gap-2 rounded-md border border-border bg-bg-subtle px-3 py-2">98 <code className="min-w-0 flex-1 truncate font-mono text-[13px]">{created.id}</code>99 <Badge variant="success" dot>100 {created.status}101 </Badge>102 <CopyButton value={created.id} label="Copy" />103 </div>104 <dl className="grid grid-cols-3 gap-3 text-[12.5px]">105 <div>106 <dt className="text-fg-subtle">Network</dt>107 <dd className="font-medium">{created.network}</dd>108 </div>109 <div>110 <dt className="text-fg-subtle">Country</dt>111 <dd className="font-mono">{created.country ?? "any"}</dd>112 </div>113 <div>114 <dt className="text-fg-subtle">Expires</dt>115 <dd className="font-mono">{new Date(created.expires_at).toLocaleTimeString("en-US", { hour: "2-digit", minute: "2-digit" })}</dd>116 </div>117 </dl>118 <CodeBlock code={curlFor(created.id, apiBase, created.country ?? undefined)} lang="bash" title="Use it in a request" />119 <DialogFooter>120 <Button variant="outline" onClick={() => reset()}>121 Create another122 </Button>123 <DialogClose asChild>124 <Button>Done</Button>125 </DialogClose>126 </DialogFooter>127 </>128 ) : (129 <form onSubmit={submit} className="contents">130 <DialogHeader>131 <DialogTitle>Create a sticky session</DialogTitle>132 <DialogDescription>A session pins one exit IP and carries cookies across requests, for logins, carts and paginated crawls. Sessions live 1 to 30 minutes.</DialogDescription>133 </DialogHeader>134 {error ? <Alert variant="danger">{error}</Alert> : null}135 <div className="grid gap-4 sm:grid-cols-2">136 <Field>137 <Label htmlFor="s-country">Country</Label>138 <Select value={country} onValueChange={setCountry}>139 <SelectTrigger id="s-country">140 <SelectValue />141 </SelectTrigger>142 <SelectContent>143 <SelectItem value={ANY}>Any country</SelectItem>144 {Object.entries(COUNTRIES).map(([code, name]) => (145 <SelectItem key={code} value={code}>146 <span className="font-mono text-[12px] text-fg-subtle">{code}</span> {name}147 </SelectItem>148 ))}149 </SelectContent>150 </Select>151 </Field>152 <Field>153 <Label htmlFor="s-network">Network</Label>154 <Select value={network} onValueChange={setNetwork}>155 <SelectTrigger id="s-network">156 <SelectValue />157 </SelectTrigger>158 <SelectContent>159 <SelectItem value="auto">Auto (recommended)</SelectItem>160 <SelectItem value="residential">Residential</SelectItem>161 <SelectItem value="datacenter" disabled>162 Datacenter — coming soon163 </SelectItem>164 <SelectItem value="isp" disabled>165 ISP — coming soon166 </SelectItem>167 <SelectItem value="mobile" disabled>168 Mobile — coming soon169 </SelectItem>170 </SelectContent>171 </Select>172 <Hint>Auto resolves to residential today.</Hint>173 </Field>174 <Field>175 <Label htmlFor="s-region">Region (optional)</Label>176 <Input id="s-region" value={region} onChange={(e) => setRegion(e.target.value)} placeholder="quebec" maxLength={64} autoComplete="off" />177 </Field>178 <Field>179 <Label htmlFor="s-city">City (optional)</Label>180 <Input id="s-city" value={city} onChange={(e) => setCity(e.target.value)} placeholder="montreal" maxLength={128} autoComplete="off" />181 </Field>182 <Field className="sm:col-span-2">183 <div className="flex items-center justify-between">184 <Label htmlFor="s-ttl">Time to live</Label>185 <span className="font-mono tabular text-[12.5px] text-fg-muted">{ttlLabel}</span>186 </div>187 <div className="flex items-center gap-3">188 <input id="s-ttl" type="range" min={60} max={1800} step={30} value={ttl} onChange={(e) => setTtl(Number(e.target.value))} className="h-1.5 flex-1 cursor-pointer appearance-none rounded-full bg-bg-muted accent-[var(--accent)]" aria-valuetext={ttlLabel} />189 <Input type="number" min={60} max={1800} step={1} value={ttl} onChange={(e) => setTtl(Math.min(1800, Math.max(60, Number(e.target.value) || 60)))} className="w-24 font-mono" aria-label="TTL in seconds" />190 </div>191 <Hint>Seconds, 60–1800. The clock does not reset on use.</Hint>192 </Field>193 <Field className="sm:col-span-2">194 <Label htmlFor="s-label">Label (optional)</Label>195 <Input id="s-label" value={label} onChange={(e) => setLabel(e.target.value)} placeholder="checkout-flow-ca" maxLength={128} autoComplete="off" />196 <FieldError />197 </Field>198 </div>199 <DialogFooter>200 <DialogClose asChild>201 <Button type="button" variant="ghost">202 Cancel203 </Button>204 </DialogClose>205 <Button type="submit" variant="primary" loading={pending}>206 Create session207 </Button>208 </DialogFooter>209 </form>210 )}211 </DialogContent>212 </Dialog>213 );214}215