added request panel and collections sidebar
This commit is contained in:
261
src/lib/components/request-panel.svelte
Normal file
261
src/lib/components/request-panel.svelte
Normal file
@@ -0,0 +1,261 @@
|
||||
<script lang="ts">
|
||||
import { Button } from "$lib/components/ui/button/index.js";
|
||||
import { Input } from "$lib/components/ui/input/index.js";
|
||||
import * as DropdownMenu from "$lib/components/ui/dropdown-menu/index.js";
|
||||
import SendIcon from "@lucide/svelte/icons/send";
|
||||
import ChevronDownIcon from "@lucide/svelte/icons/chevron-down";
|
||||
import type { Request, HttpMethod } from "$lib/types/request";
|
||||
|
||||
type Props = {
|
||||
request: Request;
|
||||
onSend: (request: Request) => void;
|
||||
onUpdate: (request: Request) => void;
|
||||
};
|
||||
|
||||
let { request, onSend, onUpdate }: Props = $props();
|
||||
|
||||
let localRequest = $state<Request>({ ...request });
|
||||
let activeTab = $state<"params" | "headers" | "body">("params");
|
||||
|
||||
$effect(() => {
|
||||
localRequest = { ...request };
|
||||
});
|
||||
|
||||
const methods: HttpMethod[] = [
|
||||
"GET",
|
||||
"POST",
|
||||
"PUT",
|
||||
"PATCH",
|
||||
"DELETE",
|
||||
"HEAD",
|
||||
"OPTIONS",
|
||||
];
|
||||
|
||||
function getMethodColor(method: string): string {
|
||||
const colors: Record<string, string> = {
|
||||
GET: "bg-green-500/10 text-green-500 hover:bg-green-500/20",
|
||||
POST: "bg-blue-500/10 text-blue-500 hover:bg-blue-500/20",
|
||||
PUT: "bg-orange-500/10 text-orange-500 hover:bg-orange-500/20",
|
||||
PATCH: "bg-yellow-500/10 text-yellow-500 hover:bg-yellow-500/20",
|
||||
DELETE: "bg-red-500/10 text-red-500 hover:bg-red-500/20",
|
||||
HEAD: "bg-purple-500/10 text-purple-500 hover:bg-purple-500/20",
|
||||
OPTIONS: "bg-gray-500/10 text-gray-500 hover:bg-gray-500/20",
|
||||
};
|
||||
return colors[method] || "bg-gray-500/10 text-gray-500";
|
||||
}
|
||||
|
||||
function handleMethodChange(method: HttpMethod) {
|
||||
localRequest.method = method;
|
||||
onUpdate(localRequest);
|
||||
}
|
||||
|
||||
function handleUrlChange(e: Event) {
|
||||
const target = e.target as HTMLInputElement;
|
||||
localRequest.url = target.value;
|
||||
onUpdate(localRequest);
|
||||
}
|
||||
|
||||
function handleSend() {
|
||||
onSend(localRequest);
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="flex flex-col h-full">
|
||||
<div class="border-b p-4">
|
||||
<div class="flex items-center gap-2">
|
||||
<DropdownMenu.Root>
|
||||
<DropdownMenu.Trigger>
|
||||
{#snippet child({ props })}
|
||||
<Button
|
||||
variant="outline"
|
||||
class={`font-mono font-semibold min-w-[100px] ${getMethodColor(localRequest.method)}`}
|
||||
{...props}
|
||||
>
|
||||
{localRequest.method}
|
||||
<ChevronDownIcon class="ml-1 size-4" />
|
||||
</Button>
|
||||
{/snippet}
|
||||
</DropdownMenu.Trigger>
|
||||
<DropdownMenu.Content>
|
||||
{#each methods as method (method)}
|
||||
<DropdownMenu.Item onclick={() => handleMethodChange(method)}>
|
||||
<span
|
||||
class={`font-mono font-semibold ${getMethodColor(method).split(" ")[1]}`}
|
||||
>
|
||||
{method}
|
||||
</span>
|
||||
</DropdownMenu.Item>
|
||||
{/each}
|
||||
</DropdownMenu.Content>
|
||||
</DropdownMenu.Root>
|
||||
|
||||
<Input
|
||||
class="flex-1 font-mono text-sm"
|
||||
placeholder="Enter request URL"
|
||||
value={localRequest.url}
|
||||
oninput={handleUrlChange}
|
||||
/>
|
||||
|
||||
<Button onclick={handleSend}>
|
||||
<SendIcon class="size-4 mr-2" />
|
||||
Send
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex-1 flex flex-col overflow-hidden">
|
||||
<div class="border-b">
|
||||
<div class="flex">
|
||||
<button
|
||||
class={`px-4 py-2 text-sm font-medium border-b-2 transition-colors ${
|
||||
activeTab === "params"
|
||||
? "border-primary text-primary"
|
||||
: "border-transparent text-muted-foreground hover:text-foreground"
|
||||
}`}
|
||||
onclick={() => (activeTab = "params")}
|
||||
>
|
||||
Params
|
||||
</button>
|
||||
<button
|
||||
class={`px-4 py-2 text-sm font-medium border-b-2 transition-colors ${
|
||||
activeTab === "headers"
|
||||
? "border-primary text-primary"
|
||||
: "border-transparent text-muted-foreground hover:text-foreground"
|
||||
}`}
|
||||
onclick={() => (activeTab = "headers")}
|
||||
>
|
||||
Headers
|
||||
</button>
|
||||
<button
|
||||
class={`px-4 py-2 text-sm font-medium border-b-2 transition-colors ${
|
||||
activeTab === "body"
|
||||
? "border-primary text-primary"
|
||||
: "border-transparent text-muted-foreground hover:text-foreground"
|
||||
}`}
|
||||
onclick={() => (activeTab = "body")}
|
||||
>
|
||||
Body
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex-1 p-4 overflow-auto">
|
||||
{#if activeTab === "params"}
|
||||
<div class="space-y-2">
|
||||
{#each localRequest.params as param, i (i)}
|
||||
<div class="flex items-center gap-2">
|
||||
<Input
|
||||
class="flex-1"
|
||||
placeholder="Key"
|
||||
value={param.key}
|
||||
oninput={(e) => {
|
||||
const target = e.target as HTMLInputElement;
|
||||
localRequest.params[i].key = target.value;
|
||||
onUpdate(localRequest);
|
||||
}}
|
||||
/>
|
||||
<Input
|
||||
class="flex-1"
|
||||
placeholder="Value"
|
||||
value={param.value}
|
||||
oninput={(e) => {
|
||||
const target = e.target as HTMLInputElement;
|
||||
localRequest.params[i].value = target.value;
|
||||
onUpdate(localRequest);
|
||||
}}
|
||||
/>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onclick={() => {
|
||||
localRequest.params = localRequest.params.filter(
|
||||
(_, idx) => idx !== i
|
||||
);
|
||||
onUpdate(localRequest);
|
||||
}}
|
||||
>
|
||||
×
|
||||
</Button>
|
||||
</div>
|
||||
{/each}
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onclick={() => {
|
||||
localRequest.params = [
|
||||
...localRequest.params,
|
||||
{ key: "", value: "", enabled: true },
|
||||
];
|
||||
onUpdate(localRequest);
|
||||
}}
|
||||
>
|
||||
Add Parameter
|
||||
</Button>
|
||||
</div>
|
||||
{:else if activeTab === "headers"}
|
||||
<div class="space-y-2">
|
||||
{#each localRequest.headers as header, i (i)}
|
||||
<div class="flex items-center gap-2">
|
||||
<Input
|
||||
class="flex-1"
|
||||
placeholder="Key"
|
||||
value={header.key}
|
||||
oninput={(e) => {
|
||||
const target = e.target as HTMLInputElement;
|
||||
localRequest.headers[i].key = target.value;
|
||||
onUpdate(localRequest);
|
||||
}}
|
||||
/>
|
||||
<Input
|
||||
class="flex-1"
|
||||
placeholder="Value"
|
||||
value={header.value}
|
||||
oninput={(e) => {
|
||||
const target = e.target as HTMLInputElement;
|
||||
localRequest.headers[i].value = target.value;
|
||||
onUpdate(localRequest);
|
||||
}}
|
||||
/>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onclick={() => {
|
||||
localRequest.headers = localRequest.headers.filter(
|
||||
(_, idx) => idx !== i
|
||||
);
|
||||
onUpdate(localRequest);
|
||||
}}
|
||||
>
|
||||
×
|
||||
</Button>
|
||||
</div>
|
||||
{/each}
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onclick={() => {
|
||||
localRequest.headers = [
|
||||
...localRequest.headers,
|
||||
{ key: "", value: "", enabled: true },
|
||||
];
|
||||
onUpdate(localRequest);
|
||||
}}
|
||||
>
|
||||
Add Header
|
||||
</Button>
|
||||
</div>
|
||||
{:else if activeTab === "body"}
|
||||
<textarea
|
||||
class="w-full h-64 p-3 font-mono text-sm border rounded-md bg-background resize-none focus:outline-none focus:ring-2 focus:ring-ring"
|
||||
placeholder="Request body (JSON, XML, etc.)"
|
||||
value={localRequest.body}
|
||||
oninput={(e) => {
|
||||
const target = e.target as HTMLTextAreaElement;
|
||||
localRequest.body = target.value;
|
||||
onUpdate(localRequest);
|
||||
}}
|
||||
></textarea>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
247
src/lib/components/workspace-sidebar.svelte
Normal file
247
src/lib/components/workspace-sidebar.svelte
Normal file
@@ -0,0 +1,247 @@
|
||||
<script lang="ts">
|
||||
import * as Collapsible from "$lib/components/ui/collapsible/index.js";
|
||||
import * as Sidebar from "$lib/components/ui/sidebar/index.js";
|
||||
import * as DropdownMenu from "$lib/components/ui/dropdown-menu/index.js";
|
||||
import ChevronRightIcon from "@lucide/svelte/icons/chevron-right";
|
||||
import FolderIcon from "@lucide/svelte/icons/folder";
|
||||
import FolderOpenIcon from "@lucide/svelte/icons/folder-open";
|
||||
import PlusIcon from "@lucide/svelte/icons/plus";
|
||||
import MoreHorizontalIcon from "@lucide/svelte/icons/more-horizontal";
|
||||
import FileIcon from "@lucide/svelte/icons/file";
|
||||
import ArrowLeftIcon from "@lucide/svelte/icons/arrow-left";
|
||||
import type { ComponentProps } from "svelte";
|
||||
import type { Collection } from "$lib/types/collection";
|
||||
import type { Request } from "$lib/types/request";
|
||||
|
||||
type Props = ComponentProps<typeof Sidebar.Root> & {
|
||||
workspaceName: string;
|
||||
collections: Collection[];
|
||||
standaloneRequests: Request[];
|
||||
selectedRequestId: string | null;
|
||||
onRequestSelect: (request: Request) => void;
|
||||
onCreateCollection: () => void;
|
||||
onCreateRequest: (collectionId: string | null) => void;
|
||||
onEditCollection: (collection: Collection) => void;
|
||||
onDeleteCollection: (collection: Collection) => void;
|
||||
onEditRequest: (request: Request) => void;
|
||||
onDeleteRequest: (request: Request) => void;
|
||||
onBack: () => void;
|
||||
};
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
workspaceName,
|
||||
collections,
|
||||
standaloneRequests,
|
||||
selectedRequestId,
|
||||
onRequestSelect,
|
||||
onCreateCollection,
|
||||
onCreateRequest,
|
||||
onEditCollection,
|
||||
onDeleteCollection,
|
||||
onEditRequest,
|
||||
onDeleteRequest,
|
||||
onBack,
|
||||
...restProps
|
||||
}: Props = $props();
|
||||
|
||||
function getMethodColor(method: string): string {
|
||||
const colors: Record<string, string> = {
|
||||
GET: "text-green-500",
|
||||
POST: "text-blue-500",
|
||||
PUT: "text-orange-500",
|
||||
PATCH: "text-yellow-500",
|
||||
DELETE: "text-red-500",
|
||||
HEAD: "text-purple-500",
|
||||
OPTIONS: "text-gray-500",
|
||||
};
|
||||
return colors[method] || "text-gray-500";
|
||||
}
|
||||
</script>
|
||||
|
||||
<Sidebar.Root bind:ref {...restProps}>
|
||||
<Sidebar.Header>
|
||||
<Sidebar.Menu>
|
||||
<Sidebar.MenuItem>
|
||||
<Sidebar.MenuButton size="lg" onclick={onBack}>
|
||||
<div
|
||||
class="bg-sidebar-primary text-sidebar-primary-foreground flex aspect-square size-8 items-center justify-center rounded-lg"
|
||||
>
|
||||
<ArrowLeftIcon class="size-4" />
|
||||
</div>
|
||||
<div class="flex flex-col gap-0.5 leading-none">
|
||||
<span class="font-medium truncate">{workspaceName}</span>
|
||||
<span class="text-xs text-muted-foreground">Back to workspaces</span
|
||||
>
|
||||
</div>
|
||||
</Sidebar.MenuButton>
|
||||
</Sidebar.MenuItem>
|
||||
</Sidebar.Menu>
|
||||
</Sidebar.Header>
|
||||
|
||||
<Sidebar.Content class="gap-0">
|
||||
<Sidebar.Group>
|
||||
<Sidebar.GroupLabel class="flex items-center justify-between pr-2">
|
||||
<span>Collections</span>
|
||||
<button
|
||||
class="hover:bg-sidebar-accent rounded p-1"
|
||||
onclick={onCreateCollection}
|
||||
>
|
||||
<PlusIcon class="size-4" />
|
||||
</button>
|
||||
</Sidebar.GroupLabel>
|
||||
<Sidebar.GroupContent>
|
||||
<Sidebar.Menu>
|
||||
{#each collections as collection (collection.id)}
|
||||
<Collapsible.Root open class="group/collapsible">
|
||||
<Sidebar.MenuItem>
|
||||
<Collapsible.Trigger class="w-full">
|
||||
{#snippet child({ props })}
|
||||
<Sidebar.MenuButton {...props}>
|
||||
<FolderIcon
|
||||
class="size-4 group-data-[state=closed]/collapsible:block hidden"
|
||||
/>
|
||||
<FolderOpenIcon
|
||||
class="size-4 group-data-[state=open]/collapsible:block hidden"
|
||||
/>
|
||||
<span class="truncate">{collection.name}</span>
|
||||
<ChevronRightIcon
|
||||
class="ms-auto size-4 transition-transform group-data-[state=open]/collapsible:rotate-90"
|
||||
/>
|
||||
</Sidebar.MenuButton>
|
||||
{/snippet}
|
||||
</Collapsible.Trigger>
|
||||
<DropdownMenu.Root>
|
||||
<DropdownMenu.Trigger>
|
||||
{#snippet child({ props })}
|
||||
<Sidebar.MenuAction {...props}>
|
||||
<MoreHorizontalIcon class="size-4" />
|
||||
</Sidebar.MenuAction>
|
||||
{/snippet}
|
||||
</DropdownMenu.Trigger>
|
||||
<DropdownMenu.Content side="right" align="start">
|
||||
<DropdownMenu.Item
|
||||
onclick={() => onCreateRequest(collection.id)}
|
||||
>
|
||||
Add Request
|
||||
</DropdownMenu.Item>
|
||||
<DropdownMenu.Item
|
||||
onclick={() => onEditCollection(collection)}
|
||||
>
|
||||
Edit Collection
|
||||
</DropdownMenu.Item>
|
||||
<DropdownMenu.Separator />
|
||||
<DropdownMenu.Item
|
||||
class="text-destructive"
|
||||
onclick={() => onDeleteCollection(collection)}
|
||||
>
|
||||
Delete Collection
|
||||
</DropdownMenu.Item>
|
||||
</DropdownMenu.Content>
|
||||
</DropdownMenu.Root>
|
||||
</Sidebar.MenuItem>
|
||||
<Collapsible.Content>
|
||||
<Sidebar.MenuSub>
|
||||
{#each collection.requests as request (request.id)}
|
||||
<Sidebar.MenuSubItem>
|
||||
<Sidebar.MenuSubButton
|
||||
isActive={selectedRequestId === request.id}
|
||||
onclick={() => onRequestSelect(request)}
|
||||
>
|
||||
<span
|
||||
class={`text-xs font-mono font-semibold ${getMethodColor(request.method)}`}
|
||||
>
|
||||
{request.method.substring(0, 3)}
|
||||
</span>
|
||||
<span class="truncate">{request.name}</span>
|
||||
</Sidebar.MenuSubButton>
|
||||
<DropdownMenu.Root>
|
||||
<DropdownMenu.Trigger>
|
||||
{#snippet child({ props })}
|
||||
<Sidebar.MenuAction {...props}>
|
||||
<MoreHorizontalIcon class="size-4" />
|
||||
</Sidebar.MenuAction>
|
||||
{/snippet}
|
||||
</DropdownMenu.Trigger>
|
||||
<DropdownMenu.Content side="right" align="start">
|
||||
<DropdownMenu.Item
|
||||
onclick={() => onEditRequest(request)}
|
||||
>
|
||||
Edit Request
|
||||
</DropdownMenu.Item>
|
||||
<DropdownMenu.Separator />
|
||||
<DropdownMenu.Item
|
||||
class="text-destructive"
|
||||
onclick={() => onDeleteRequest(request)}
|
||||
>
|
||||
Delete Request
|
||||
</DropdownMenu.Item>
|
||||
</DropdownMenu.Content>
|
||||
</DropdownMenu.Root>
|
||||
</Sidebar.MenuSubItem>
|
||||
{/each}
|
||||
</Sidebar.MenuSub>
|
||||
</Collapsible.Content>
|
||||
</Collapsible.Root>
|
||||
{/each}
|
||||
</Sidebar.Menu>
|
||||
</Sidebar.GroupContent>
|
||||
</Sidebar.Group>
|
||||
|
||||
<Sidebar.Separator />
|
||||
|
||||
<Sidebar.Group>
|
||||
<Sidebar.GroupLabel class="flex items-center justify-between pr-2">
|
||||
<span>Requests</span>
|
||||
<button
|
||||
class="hover:bg-sidebar-accent rounded p-1"
|
||||
onclick={() => onCreateRequest(null)}
|
||||
>
|
||||
<PlusIcon class="size-4" />
|
||||
</button>
|
||||
</Sidebar.GroupLabel>
|
||||
<Sidebar.GroupContent>
|
||||
<Sidebar.Menu>
|
||||
{#each standaloneRequests as request (request.id)}
|
||||
<Sidebar.MenuItem>
|
||||
<Sidebar.MenuButton
|
||||
isActive={selectedRequestId === request.id}
|
||||
onclick={() => onRequestSelect(request)}
|
||||
>
|
||||
<span
|
||||
class={`text-xs font-mono font-semibold ${getMethodColor(request.method)}`}
|
||||
>
|
||||
{request.method.substring(0, 3)}
|
||||
</span>
|
||||
<span class="truncate">{request.name}</span>
|
||||
</Sidebar.MenuButton>
|
||||
<DropdownMenu.Root>
|
||||
<DropdownMenu.Trigger>
|
||||
{#snippet child({ props })}
|
||||
<Sidebar.MenuAction {...props}>
|
||||
<MoreHorizontalIcon class="size-4" />
|
||||
</Sidebar.MenuAction>
|
||||
{/snippet}
|
||||
</DropdownMenu.Trigger>
|
||||
<DropdownMenu.Content side="right" align="start">
|
||||
<DropdownMenu.Item onclick={() => onEditRequest(request)}>
|
||||
Edit Request
|
||||
</DropdownMenu.Item>
|
||||
<DropdownMenu.Separator />
|
||||
<DropdownMenu.Item
|
||||
class="text-destructive"
|
||||
onclick={() => onDeleteRequest(request)}
|
||||
>
|
||||
Delete Request
|
||||
</DropdownMenu.Item>
|
||||
</DropdownMenu.Content>
|
||||
</DropdownMenu.Root>
|
||||
</Sidebar.MenuItem>
|
||||
{/each}
|
||||
</Sidebar.Menu>
|
||||
</Sidebar.GroupContent>
|
||||
</Sidebar.Group>
|
||||
</Sidebar.Content>
|
||||
|
||||
<Sidebar.Rail />
|
||||
</Sidebar.Root>
|
||||
Reference in New Issue
Block a user