starting refactor to shadcn and redb

This commit is contained in:
xyroscar
2025-11-23 19:03:09 -08:00
parent 72e2573f1e
commit 9bb1d91f56
55 changed files with 4163 additions and 4328 deletions

View File

@@ -1 +0,0 @@
<slot />

View File

@@ -1,19 +0,0 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
:root {
--sidebar-width: 300px;
}
body {
@apply bg-base-100 text-base-content;
}
.drawer-side {
width: var(--sidebar-width) !important;
}
.drawer.drawer-open > .drawer-content {
padding-left: var(--sidebar-width);
}

View File

@@ -1,311 +0,0 @@
<script lang="ts">
import { invoke } from '@tauri-apps/api/core';
import { activeRequest, variables, currentResponse } from '../../stores';
import type { Header, QueryParam } from '../../types';
let url = '';
let method = 'GET';
let headers: Header[] = [];
let queryParams: QueryParam[] = [];
let body = '';
let loading = false;
let activeTab = 'headers';
const methods = ['GET', 'POST', 'PUT', 'DELETE', 'PATCH'];
// Subscribe to activeRequest changes
$: if ($activeRequest) {
url = $activeRequest.url;
method = $activeRequest.method;
headers = $activeRequest.headers;
queryParams = $activeRequest.queryParams || [];
body = $activeRequest.body || '';
}
function addHeader() {
headers = [...headers, {
id: crypto.randomUUID(),
key: '',
value: '',
enabled: true
}];
}
function addQueryParam() {
queryParams = [...queryParams, {
id: crypto.randomUUID(),
key: '',
value: '',
enabled: true
}];
}
function removeHeader(id: string) {
headers = headers.filter(h => h.id !== id);
}
function removeQueryParam(id: string) {
queryParams = queryParams.filter(p => p.id !== id);
}
function interpolateVariables(value: string): string {
return value.replace(/\{\{(.+?)\}\}/g, (_, key) => {
const variable = $variables.find(v => v.name === key.trim());
return variable ? variable.value : `{{${key}}}`;
});
}
function buildUrl(baseUrl: string, params: QueryParam[]): string {
try {
if (!baseUrl) return '';
const url = new URL(baseUrl);
params
.filter(p => p.enabled && p.key)
.forEach(p => {
try {
url.searchParams.append(
interpolateVariables(p.key),
interpolateVariables(p.value || '') // Handle undefined value
);
} catch (error) {
console.error('Error appending parameter:', error);
}
});
return url.toString();
} catch (error) {
console.error('Error building URL:', error);
return baseUrl; // Return original URL if there's an error
}
}
async function sendRequest() {
loading = true;
try {
const interpolatedHeaders = headers.map(h => ({
...h,
key: interpolateVariables(h.key),
value: interpolateVariables(h.value)
}));
const request = {
url: buildUrl(interpolateVariables(url), queryParams),
method,
headers: interpolatedHeaders,
body: body ? interpolateVariables(body) : undefined
};
const response = await invoke<{
status: number;
statusText: string;
headers: Record<string, string>;
body: string;
time: number;
}>('send_api_request', { request });
currentResponse.set(response);
} catch (error) {
console.error('Error:', error);
} finally {
loading = false;
}
}
// Add these functions to handle tab clicks explicitly
function setActiveTab(tab: 'headers' | 'body' | 'query') {
activeTab = tab;
}
// Separate function to handle query param updates
function updateQueryParam(param: QueryParam, field: 'key' | 'value' | 'enabled', value: string | boolean) {
queryParams = queryParams.map(p =>
p.id === param.id
? { ...p, [field]: value }
: p
);
}
</script>
<div class="card bg-base-100 shadow-lg border border-base-300">
<div class="card-body p-4">
<div class="flex space-x-2">
<div class="join flex-1">
<select
class="select select-bordered join-item w-28 font-medium"
bind:value={method}
>
{#each methods as m}
<option value={m}>{m}</option>
{/each}
</select>
<input
type="text"
placeholder="Enter request URL"
class="input input-bordered join-item flex-1 min-w-[400px]"
bind:value={url}
/>
<button
class="btn join-item btn-primary"
on:click={sendRequest}
disabled={loading || !url}
>
{#if loading}
<span class="loading loading-spinner loading-sm"></span>
{:else}
Send
{/if}
</button>
</div>
</div>
<div class="tabs tabs-boxed bg-base-200 mt-4">
<a
href="#headers"
class="tab"
class:tab-active={activeTab === 'headers'}
on:click|preventDefault={() => setActiveTab('headers')}
>
Headers
</a>
<a
href="#body"
class="tab"
class:tab-active={activeTab === 'body'}
on:click|preventDefault={() => setActiveTab('body')}
>
Body
</a>
<a
href="#query"
class="tab"
class:tab-active={activeTab === 'query'}
on:click|preventDefault={() => setActiveTab('query')}
>
Query
</a>
</div>
<!-- Headers Section -->
<div class="mt-4" class:hidden={activeTab !== 'headers'}>
<div class="flex justify-between items-center mb-2">
<h3 class="text-sm font-medium opacity-70">Request Headers</h3>
<button
type="button"
class="btn btn-sm btn-ghost"
on:click={() => addHeader()}
>
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4 mr-1" viewBox="0 0 20 20" fill="currentColor">
<path fill-rule="evenodd" d="M10 3a1 1 0 011 1v5h5a1 1 0 110 2h-5v5a1 1 0 11-2 0v-5H4a1 1 0 110-2h5V4a1 1 0 011-1z" clip-rule="evenodd" />
</svg>
Add Header
</button>
</div>
<div class="space-y-2">
{#each headers as header (header.id)}
<div class="join w-full">
<input
type="checkbox"
class="checkbox join-item ml-2"
bind:checked={header.enabled}
/>
<input
type="text"
placeholder="Header name"
class="input input-bordered input-sm join-item w-1/3"
bind:value={header.key}
/>
<input
type="text"
placeholder="Value"
class="input input-bordered input-sm join-item flex-1"
bind:value={header.value}
/>
<button
type="button"
class="btn btn-sm join-item btn-ghost text-error"
on:click={() => removeHeader(header.id)}
>
×
</button>
</div>
{/each}
</div>
</div>
<!-- Body Section -->
<div class="mt-4" class:hidden={activeTab !== 'body'}>
<h3 class="text-sm font-medium opacity-70 mb-2">Request Body</h3>
<textarea
class="textarea textarea-bordered w-full h-48 font-mono text-sm"
placeholder="Enter JSON body"
bind:value={body}
></textarea>
</div>
<!-- Query Section -->
<div class="mt-4" class:hidden={activeTab !== 'query'}>
<div class="flex justify-between items-center mb-2">
<h3 class="text-sm font-medium opacity-70">Query Parameters</h3>
<button
type="button"
class="btn btn-sm btn-ghost"
on:click={addQueryParam}
>
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4 mr-1" viewBox="0 0 20 20" fill="currentColor">
<path fill-rule="evenodd" d="M10 3a1 1 0 011 1v5h5a1 1 0 110 2h-5v5a1 1 0 11-2 0v-5H4a1 1 0 110-2h5V4a1 1 0 011-1z" clip-rule="evenodd" />
</svg>
Add Parameter
</button>
</div>
<div class="space-y-2">
{#each queryParams as param (param.id)}
<div class="join w-full">
<input
type="checkbox"
class="checkbox join-item ml-2"
checked={param.enabled}
on:change={(e) => updateQueryParam(param, 'enabled', e.currentTarget.checked)}
/>
<input
type="text"
placeholder="Parameter name"
class="input input-bordered input-sm join-item w-1/3"
value={param.key}
on:input={(e) => updateQueryParam(param, 'key', e.currentTarget.value)}
/>
<input
type="text"
placeholder="Value"
class="input input-bordered input-sm join-item flex-1"
value={param.value}
on:input={(e) => updateQueryParam(param, 'value', e.currentTarget.value)}
/>
<button
type="button"
class="btn btn-sm join-item btn-ghost text-error"
on:click={() => removeQueryParam(param.id)}
>
×
</button>
</div>
{/each}
</div>
{#if queryParams.length > 0 && url}
<div class="mt-4">
<h4 class="text-sm font-medium opacity-70 mb-2">Preview URL</h4>
<div class="bg-base-200 p-3 rounded-lg">
<code class="text-xs break-all">
{buildUrl(url, queryParams)}
</code>
</div>
</div>
{/if}
</div>
</div>
</div>

View File

@@ -1,85 +0,0 @@
<script lang="ts">
import { currentResponse } from '../../stores';
let activeTab = 'body';
function formatJson(json: string): string {
try {
return JSON.stringify(JSON.parse(json), null, 2);
} catch {
return json;
}
}
function getStatusColor(status: number): string {
if (status >= 200 && status < 300) return 'text-success';
if (status >= 400) return 'text-error';
return 'text-warning';
}
</script>
{#if $currentResponse}
<div class="card bg-base-100 shadow-lg border border-base-300">
<div class="card-body p-4">
<!-- Status and Time -->
<div class="flex justify-between items-center">
<div class="flex items-center space-x-2">
<span class={getStatusColor($currentResponse.status)}>
{$currentResponse.status}
</span>
<span class="text-base-content/70">
{$currentResponse.statusText}
</span>
</div>
<span class="text-base-content/70">
{$currentResponse.time}ms
</span>
</div>
<!-- Tabs -->
<div class="tabs tabs-boxed bg-base-200 mt-4">
<button
class="tab"
class:tab-active={activeTab === 'body'}
on:click={() => activeTab = 'body'}
>
Response Body
</button>
<button
class="tab"
class:tab-active={activeTab === 'headers'}
on:click={() => activeTab = 'headers'}
>
Response Headers
</button>
</div>
<!-- Content -->
{#if activeTab === 'body'}
<div class="bg-base-200 rounded-lg mt-4">
<pre class="overflow-x-auto p-4 text-sm">
<code>{formatJson($currentResponse.body)}</code>
</pre>
</div>
{:else}
<div class="overflow-x-auto mt-4">
<table class="table table-sm">
<thead>
<tr>
<th>Header</th>
<th>Value</th>
</tr>
</thead>
<tbody>
{#each Object.entries($currentResponse.headers) as [key, value]}
<tr>
<td class="font-mono text-sm">{key}</td>
<td class="font-mono text-sm">{value}</td>
</tr>
{/each}
</tbody>
</table>
</div>
{/if}
</div>
</div>
{/if}

View File

@@ -1,87 +0,0 @@
<script lang="ts">
import { collections, variables, activeCollection, activeRequest } from '../../stores';
import type { Collection, Request } from '../../types';
import CollectionModal from './modals/CollectionModal.svelte';
let showCollectionModal = false;
function selectRequest(collection: Collection, request: Request) {
activeCollection.set(collection);
activeRequest.set(request);
}
function handleCollectionSave(event: CustomEvent<Collection>) {
collections.update(cols => [...cols, event.detail]);
showCollectionModal = false;
}
</script>
<div class="bg-base-200 min-h-screen h-full p-4 border-r border-base-300">
<!-- Collections Section -->
<div class="flex flex-col">
<div class="flex justify-between items-center mb-4">
<h2 class="font-medium flex items-center">
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4 mr-2" viewBox="0 0 20 20" fill="currentColor">
<path d="M7 3a1 1 0 000 2h6a1 1 0 100-2H7zM4 7a1 1 0 011-1h10a1 1 0 110 2H5a1 1 0 01-1-1zM2 11a2 2 0 012-2h12a2 2 0 012 2v4a2 2 0 01-2 2H4a2 2 0 01-2-2v-4z" />
</svg>
Collections
</h2>
<button class="btn btn-sm btn-primary" on:click={() => showCollectionModal = true}>
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4 mr-1" viewBox="0 0 20 20" fill="currentColor">
<path fill-rule="evenodd" d="M10 3a1 1 0 011 1v5h5a1 1 0 110 2h-5v5a1 1 0 11-2 0v-5H4a1 1 0 110-2h5V4a1 1 0 011-1z" clip-rule="evenodd" />
</svg>
New
</button>
</div>
{#if $collections.length === 0}
<div class="bg-base-100 rounded-lg p-4 text-center">
<p class="text-sm text-base-content/70">No collections yet</p>
<p class="text-xs text-base-content/50 mt-1">Create a new collection to get started</p>
</div>
{/if}
{#each $collections as collection}
<div class="collapse collapse-arrow bg-base-100 shadow-sm mb-2">
<input type="checkbox" />
<div class="collapse-title font-medium">
{collection.name}
</div>
<div class="collapse-content">
<div class="flex flex-col gap-1">
{#each collection.requests as request}
<button
class="flex items-center w-full px-3 py-2 hover:bg-base-300 rounded-lg text-left transition-colors"
class:bg-primary-content={$activeRequest?.id === request.id}
on:click={() => selectRequest(collection, request)}
>
<span class="text-xs font-medium px-2 py-0.5 rounded mr-2"
class:bg-success={request.method === 'GET'}
class:bg-info={request.method === 'POST'}
class:bg-warning={request.method === 'PUT'}
class:bg-error={request.method === 'DELETE'}
>
{request.method}
</span>
<span class="text-sm truncate">{request.name}</span>
</button>
{/each}
<button class="btn btn-sm btn-ghost mt-2 w-full">
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4 mr-2" viewBox="0 0 20 20" fill="currentColor">
<path fill-rule="evenodd" d="M10 3a1 1 0 011 1v5h5a1 1 0 110 2h-5v5a1 1 0 11-2 0v-5H4a1 1 0 110-2h5V4a1 1 0 011-1z" clip-rule="evenodd" />
</svg>
Add Request
</button>
</div>
</div>
</div>
{/each}
</div>
</div>
<CollectionModal
show={showCollectionModal}
on:save={handleCollectionSave}
on:close={() => showCollectionModal = false}
/>

View File

@@ -1,99 +0,0 @@
<script lang="ts">
import { goto } from '$app/navigation';
import type { Workspace } from '../../types';
import { formatDate } from '../utils/date';
import { collections } from '$lib/stores/collection';
import { workspaces } from '$lib/stores/workspace';
import WorkspaceModal from './modals/WorkspaceModal.svelte';
export let workspace: Workspace;
let showEditModal = false;
function openWorkspace() {
goto(`/workspace/${workspace.id}`);
}
function handleKeyDown(event: KeyboardEvent) {
if (event.key === 'Enter' || event.key === ' ') {
event.preventDefault();
openWorkspace();
}
}
async function handleDelete(event: MouseEvent) {
event.stopPropagation();
if (confirm('Are you sure you want to delete this workspace?')) {
try {
await workspaces.deleteWorkspace(workspace.id);
} catch (error) {
console.error('Failed to delete workspace:', error);
}
}
}
function handleEdit(event: MouseEvent) {
event.stopPropagation();
showEditModal = true;
}
// Compute collections and variables count
$: workspaceCollections = $collections.filter(c => c.workspace_id === workspace.id);
$: workspaceVariables = workspace.variables || [];
</script>
<div class="w-full">
<button
class="card bg-base-100 shadow-lg hover:shadow-xl transition-shadow w-full text-left"
on:click={openWorkspace}
on:keydown={handleKeyDown}
>
<div class="card-body p-6">
<div class="flex justify-between items-start">
<h2 class="card-title text-xl">{workspace.name}</h2>
<div class="dropdown dropdown-end">
<!-- svelte-ignore a11y_click_events_have_key_events -->
<!-- svelte-ignore a11y_no_static_element_interactions -->
<div
class="btn btn-ghost btn-sm btn-square"
on:click|stopPropagation
>
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" viewBox="0 0 20 20" fill="currentColor">
<path d="M10 6a2 2 0 110-4 2 2 0 010 4zM10 12a2 2 0 110-4 2 2 0 010 4zM10 18a2 2 0 110-4 2 2 0 010 4z" />
</svg>
</div>
<ul class="dropdown-content menu p-2 shadow bg-base-100 rounded-box w-52">
<li><a href="#top" on:click|preventDefault={handleEdit}>Edit</a></li>
<li><a href="#top" class="text-error" on:click|preventDefault={handleDelete}>Delete</a></li>
</ul>
</div>
</div>
{#if workspace.description}
<p class="text-base-content/70 text-sm mt-2">{workspace.description}</p>
{/if}
<div class="flex gap-6 mt-6">
<div class="stat bg-base-200 rounded-lg p-4">
<div class="stat-title text-xs">Collections</div>
<div class="stat-value text-lg">{workspaceCollections.length}</div>
</div>
<div class="stat bg-base-200 rounded-lg p-4">
<div class="stat-title text-xs">Variables</div>
<div class="stat-value text-lg">{workspaceVariables.length}</div>
</div>
</div>
<div class="card-actions justify-end mt-4">
<div class="text-xs text-base-content/50">
Updated {formatDate(workspace.updated_at)}
</div>
</div>
</div>
</button>
</div>
<WorkspaceModal
show={showEditModal}
{workspace}
on:close={() => showEditModal = false}
/>

View File

@@ -1,120 +0,0 @@
<script lang="ts">
import { workspaces, activeWorkspace } from '../stores/workspace';
import { onMount } from 'svelte';
let showCreateModal = false;
let newWorkspaceName = '';
let newWorkspaceDescription = '';
let showDropdown = false;
onMount(() => {
workspaces.loadWorkspaces();
});
async function handleCreateWorkspace() {
try {
const workspace = await workspaces.createWorkspace(
newWorkspaceName,
newWorkspaceDescription || undefined
);
activeWorkspace.set(workspace);
showCreateModal = false;
newWorkspaceName = '';
newWorkspaceDescription = '';
} catch (error) {
console.error('Failed to create workspace:', error);
}
}
</script>
<div class="dropdown">
<button type="button" class="btn m-1" aria-haspopup="true" aria-expanded={showDropdown}>
{#if $activeWorkspace}
{$activeWorkspace.name}
{:else}
Select Workspace
{/if}
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4 ml-2" viewBox="0 0 20 20" fill="currentColor">
<path fill-rule="evenodd" d="M5.293 7.293a1 1 0 011.414 0L10 10.586l3.293-3.293a1 1 0 111.414 1.414l-4 4a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414z" clip-rule="evenodd" />
</svg>
</button>
<div
class="dropdown-content menu p-2 shadow bg-base-100 rounded-box w-52"
role="menu"
>
{#each $workspaces as workspace}
<li>
<button
class="btn btn-ghost justify-start"
class:btn-active={$activeWorkspace?.id === workspace.id}
on:click={() => activeWorkspace.set(workspace)}
>
{workspace.name}
</button>
</li>
{/each}
<li>
<button
class="btn btn-ghost justify-start text-primary"
on:click={() => showCreateModal = true}
>
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4 mr-2" viewBox="0 0 20 20" fill="currentColor">
<path fill-rule="evenodd" d="M10 3a1 1 0 011 1v5h5a1 1 0 110 2h-5v5a1 1 0 11-2 0v-5H4a1 1 0 110-2h5V4a1 1 0 011-1z" clip-rule="evenodd" />
</svg>
New Workspace
</button>
</li>
</div>
</div>
{#if showCreateModal}
<div class="modal modal-open">
<div class="modal-box">
<h3 class="font-bold text-lg">Create New Workspace</h3>
<form on:submit|preventDefault={handleCreateWorkspace} class="mt-4">
<div class="form-control">
<label class="label" for="name">
<span class="label-text">Workspace Name</span>
</label>
<input
type="text"
id="name"
class="input input-bordered"
bind:value={newWorkspaceName}
placeholder="My Workspace"
required
/>
</div>
<div class="form-control mt-4">
<label class="label" for="description">
<span class="label-text">Description</span>
</label>
<textarea
id="description"
class="textarea textarea-bordered"
bind:value={newWorkspaceDescription}
placeholder="Optional description"
/>
</div>
<div class="modal-action">
<button
type="button"
class="btn"
on:click={() => showCreateModal = false}
>
Cancel
</button>
<button
type="submit"
class="btn btn-primary"
disabled={!newWorkspaceName}
>
Create
</button>
</div>
</form>
</div>
</div>
{/if}

View File

@@ -1,51 +0,0 @@
<script lang="ts">
import { goto } from '$app/navigation';
import { activeWorkspace } from '../stores/workspace';
import VariablesModal from './modals/VariablesModal.svelte';
let showVariablesModal = false;
function goToWorkspaces() {
goto('/');
}
</script>
<div class="navbar bg-base-100 border-b border-base-300">
<div class="flex-none lg:hidden">
<label for="drawer" class="btn btn-square btn-ghost">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" class="inline-block w-6 h-6 stroke-current">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 12h16M4 18h16"></path>
</svg>
</label>
</div>
<div class="flex-1 flex items-center gap-2">
<button
class="btn btn-ghost btn-sm"
on:click={goToWorkspaces}
>
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" viewBox="0 0 20 20" fill="currentColor">
<path fill-rule="evenodd" d="M9.707 16.707a1 1 0 01-1.414 0l-6-6a1 1 0 010-1.414l6-6a1 1 0 011.414 1.414L5.414 9H17a1 1 0 110 2H5.414l4.293 4.293a1 1 0 010 1.414z" clip-rule="evenodd" />
</svg>
All Workspaces
</button>
<span class="text-xl font-bold">{$activeWorkspace?.name}</span>
</div>
<div class="flex-none">
<button
class="btn btn-ghost btn-sm"
on:click={() => showVariablesModal = true}
>
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 mr-2" viewBox="0 0 20 20" fill="currentColor">
<path fill-rule="evenodd" d="M4.649 3.084A1 1 0 015.163 4.4 13.95 13.95 0 004 10c0 1.993.416 3.886 1.164 5.6a1 1 0 01-1.832.8A15.95 15.95 0 012 10c0-2.274.475-4.44 1.332-6.4a1 1 0 011.317-.516zM12.96 7a3 3 0 00-2.342 1.126l-.328.41-.111-.279A2 2 0 008.323 7H8a1 1 0 000 2h.323l.532 1.33-1.035 1.295a1 1 0 01-.781.375H7a1 1 0 100 2h.039a3 3 0 002.342-1.126l.328-.41.111.279A2 2 0 0011.677 14H12a1 1 0 100-2h-.323l-.532-1.33 1.035-1.295A1 1 0 0112.961 9H13a1 1 0 100-2h-.039z" clip-rule="evenodd" />
</svg>
Variables
</button>
</div>
</div>
<VariablesModal
show={showVariablesModal}
on:close={() => showVariablesModal = false}
/>

View File

@@ -1,97 +0,0 @@
<script lang="ts">
import { createEventDispatcher } from 'svelte';
import type { Collection } from '../../../types';
import { collections } from '../../stores/collection';
import { activeWorkspace } from '../../stores/workspace';
export let show = false;
export let collection: Collection | null = null;
let name = '';
let description = '';
$: if (collection) {
name = collection.name;
description = collection.description || '';
}
const dispatch = createEventDispatcher<{
close: void;
}>();
async function handleSubmit() {
try {
if (!$activeWorkspace) return;
if (collection) {
await collections.updateCollection(
collection.id,
name,
description || undefined
);
} else {
await collections.createCollection(
$activeWorkspace.id,
name,
description || undefined
);
}
handleClose();
} catch (error) {
console.error('Failed to save collection:', error);
}
}
function handleClose() {
name = '';
description = '';
dispatch('close');
}
</script>
{#if show}
<div class="modal modal-open">
<div class="modal-box">
<h3 class="font-bold text-lg">
{collection ? 'Edit' : 'Create New'} Collection
</h3>
<form on:submit|preventDefault={handleSubmit} class="mt-4">
<div class="form-control">
<label class="label" for="name">
<span class="label-text">Collection Name</span>
</label>
<input
type="text"
id="name"
class="input input-bordered"
bind:value={name}
placeholder="My Collection"
required
/>
</div>
<div class="form-control mt-4">
<label class="label" for="description">
<span class="label-text">Description</span>
</label>
<textarea
id="description"
class="textarea textarea-bordered"
bind:value={description}
placeholder="Optional description"
></textarea>
</div>
<div class="modal-action">
<button type="button" class="btn" on:click={handleClose}>
Cancel
</button>
<button type="submit" class="btn btn-primary" disabled={!name}>
{collection ? 'Save' : 'Create'}
</button>
</div>
</form>
</div>
</div>
{/if}

View File

@@ -1,77 +0,0 @@
<script lang="ts">
import { createEventDispatcher } from 'svelte';
import type { Variable } from '../../../types';
import { activeWorkspace } from '../../stores/workspace';
export let show = false;
let variables: Variable[] = [];
$: if ($activeWorkspace) {
variables = $activeWorkspace.variables || [];
}
const dispatch = createEventDispatcher<{
close: void;
}>();
function addVariable() {
variables = [...variables, {
id: crypto.randomUUID(),
name: '',
value: '',
description: ''
}];
}
function removeVariable(id: string) {
variables = variables.filter(v => v.id !== id);
}
async function handleSave() {
// TODO: Implement variable saving
dispatch('close');
}
</script>
{#if show}
<div class="modal modal-open">
<div class="modal-box max-w-3xl">
<h3 class="font-bold text-lg mb-4">Workspace Variables</h3>
<div class="space-y-4">
{#each variables as variable (variable.id)}
<div class="flex gap-2">
<input
type="text"
class="input input-bordered w-1/3"
placeholder="Variable name"
bind:value={variable.name}
/>
<input
type="text"
class="input input-bordered flex-1"
placeholder="Value"
bind:value={variable.value}
/>
<button
class="btn btn-ghost btn-sm text-error"
on:click={() => removeVariable(variable.id)}
>
×
</button>
</div>
{/each}
<button class="btn btn-ghost btn-sm w-full" on:click={addVariable}>
Add Variable
</button>
</div>
<div class="modal-action">
<button class="btn" on:click={() => dispatch('close')}>Cancel</button>
<button class="btn btn-primary" on:click={handleSave}>Save Changes</button>
</div>
</div>
</div>
{/if}

View File

@@ -1,142 +0,0 @@
<script lang="ts">
import { createEventDispatcher } from 'svelte';
import { workspaces } from '../../stores/workspace';
import type { Workspace } from '../../../types';
export let show = false;
export let workspace: Workspace | null = null;
let name = '';
let description = '';
let isSubmitting = false;
let error = '';
let initialized = false;
$: if (show && !initialized) {
if (workspace) {
name = workspace.name;
description = workspace.description || '';
} else {
name = '';
description = '';
}
initialized = true;
error = '';
}
$: if (!show) {
initialized = false;
}
const dispatch = createEventDispatcher<{
close: void;
}>();
async function handleSubmit(event: SubmitEvent) {
event.preventDefault();
if (isSubmitting) return;
error = '';
try {
isSubmitting = true;
if (workspace) {
await workspaces.updateWorkspace(
workspace.id,
name,
description || undefined
);
} else {
await workspaces.createWorkspace(
name,
description || undefined
);
}
handleClose();
} catch (e) {
error = e instanceof Error ? e.message : 'Failed to save workspace';
console.error('Failed to save workspace:', e);
} finally {
isSubmitting = false;
}
}
function handleClose() {
name = '';
description = '';
error = '';
initialized = false;
dispatch('close');
}
</script>
<div class="modal {show ? 'modal-open' : ''}" role="dialog">
<div class="modal-box">
<h3 class="font-bold text-lg">
{workspace ? 'Edit' : 'Create New'} Workspace
</h3>
<form on:submit={handleSubmit} class="mt-4">
{#if error}
<div class="alert alert-error mb-4">
<svg xmlns="http://www.w3.org/2000/svg" class="stroke-current shrink-0 h-6 w-6" fill="none" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 14l2-2m0 0l2-2m-2 2l-2-2m2 2l2 2m7-2a9 9 0 11-18 0 9 9 0 0118 0z" /></svg>
<span>{error}</span>
</div>
{/if}
<div class="form-control">
<label class="label" for="name">
<span class="label-text">Workspace Name</span>
</label>
<input
type="text"
id="name"
class="input input-bordered"
bind:value={name}
placeholder="My Workspace"
required
/>
</div>
<div class="form-control mt-4">
<label class="label" for="description">
<span class="label-text">Description</span>
</label>
<textarea
id="description"
class="textarea textarea-bordered"
bind:value={description}
placeholder="Optional description"
></textarea>
</div>
<div class="modal-action">
<button
type="button"
class="btn"
on:click={handleClose}
disabled={isSubmitting}
>
Cancel
</button>
<button
type="submit"
class="btn btn-primary"
disabled={!name || isSubmitting}
>
{#if isSubmitting}
<span class="loading loading-spinner loading-sm"></span>
{/if}
{workspace ? 'Save' : 'Create'}
</button>
</div>
</form>
</div>
<button
type="button"
class="modal-backdrop"
on:click={handleClose}
aria-label="Close modal"
></button>
</div>

View File

@@ -1,72 +0,0 @@
import { writable, derived } from 'svelte/store';
import { invoke } from '@tauri-apps/api/core';
import type { Collection } from '../../types';
import { activeWorkspace } from './workspace';
function createCollectionStore() {
const { subscribe, set, update } = writable<Collection[]>([]);
return {
subscribe,
loadCollections: async (workspaceId: string) => {
try {
const collections = await invoke<Collection[]>('get_workspace_collections', {
workspaceId
});
set(collections);
} catch (error) {
console.error('Error loading collections:', error);
set([]);
}
},
createCollection: async (workspaceId: string, name: string, description?: string) => {
try {
const collection = await invoke<Collection>('create_collection', {
workspaceId,
name,
description,
});
update(collections => [...collections, collection]);
return collection;
} catch (error) {
console.error('Error creating collection:', error);
throw error;
}
},
updateCollection: async (id: string, name: string, description?: string) => {
try {
const collection = await invoke<Collection>('update_collection', {
id,
name,
description,
});
update(collections =>
collections.map(c => c.id === id ? collection : c)
);
return collection;
} catch (error) {
console.error('Error updating collection:', error);
throw error;
}
},
deleteCollection: async (id: string) => {
try {
await invoke('delete_collection', { id });
update(collections => collections.filter(c => c.id !== id));
} catch (error) {
console.error('Error deleting collection:', error);
throw error;
}
},
};
}
export const collections = createCollectionStore();
export const activeCollection = writable<Collection | null>(null);
// Auto-load collections when workspace changes
activeWorkspace.subscribe(workspace => {
if (workspace) {
collections.loadCollections(workspace.id);
}
});

View File

@@ -1,76 +0,0 @@
import { writable, derived } from 'svelte/store';
import { invoke } from '@tauri-apps/api/core';
import type { Workspace } from '../../types';
function createWorkspaceStore() {
const { subscribe, set, update } = writable<Workspace[]>([]);
return {
subscribe,
loadWorkspaces: async () => {
try {
const workspaces = await invoke<Workspace[]>('get_workspaces');
set(workspaces);
} catch (error) {
console.error('Error loading workspaces:', error);
throw error;
}
},
getWorkspace: async (id: string) => {
try {
const workspace = await invoke<Workspace>('get_workspace', { id });
return workspace;
} catch (error) {
console.error('Error getting workspace:', error);
throw error;
}
},
createWorkspace: async (name: string, description?: string) => {
try {
const workspace = await invoke<Workspace>('create_workspace', {
name,
description,
});
update(workspaces => [...workspaces, workspace]);
return workspace;
} catch (error) {
console.error('Error creating workspace:', error);
throw new Error(error instanceof Error ? error.message : 'Failed to create workspace');
}
},
updateWorkspace: async (id: string, name: string, description?: string) => {
try {
const workspace = await invoke<Workspace>('update_workspace', {
id,
name,
description,
});
update(workspaces =>
workspaces.map(w => w.id === id ? workspace : w)
);
return workspace;
} catch (error) {
console.error('Error updating workspace:', error);
throw new Error(error instanceof Error ? error.message : 'Failed to update workspace');
}
},
deleteWorkspace: async (id: string) => {
try {
await invoke('delete_workspace', { id });
update(workspaces => workspaces.filter(w => w.id !== id));
} catch (error) {
console.error('Error deleting workspace:', error);
throw new Error(error instanceof Error ? error.message : 'Failed to delete workspace');
}
},
};
}
export const workspaces = createWorkspaceStore();
export const activeWorkspace = writable<Workspace | null>(null);
// Derived store for the current workspace's variables
export const currentVariables = derived(
activeWorkspace,
$workspace => $workspace?.variables || []
);

13
src/lib/utils.ts Normal file
View File

@@ -0,0 +1,13 @@
import { clsx, type ClassValue } from "clsx";
import { twMerge } from "tailwind-merge";
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs));
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export type WithoutChild<T> = T extends { child?: any } ? Omit<T, "child"> : T;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export type WithoutChildren<T> = T extends { children?: any } ? Omit<T, "children"> : T;
export type WithoutChildrenOrChild<T> = WithoutChildren<WithoutChild<T>>;
export type WithElementRef<T, U extends HTMLElement = HTMLElement> = T & { ref?: U | null };

View File

@@ -1,18 +0,0 @@
export function formatDate(timestamp: number): string {
const date = new Date(timestamp * 1000);
const now = new Date();
const diff = now.getTime() - date.getTime();
// Less than 24 hours
if (diff < 24 * 60 * 60 * 1000) {
return date.toLocaleTimeString();
}
// Less than a week
if (diff < 7 * 24 * 60 * 60 * 1000) {
return date.toLocaleDateString(undefined, { weekday: 'long' });
}
// Otherwise
return date.toLocaleDateString();
}

View File

@@ -1,11 +0,0 @@
<script>
import { page } from '$app/stores';
</script>
<div class="min-h-screen flex items-center justify-center">
<div class="text-center">
<h1 class="text-9xl font-bold text-base-content/20">{$page.status}</h1>
<p class="text-xl mt-4">{$page.error?.message || 'Page not found'}</p>
<a href="/" class="btn btn-primary mt-8">Go Home</a>
</div>
</div>

View File

@@ -1,5 +1,7 @@
<script>
import "../app.css";
<script lang="ts">
import './layout.css';
let { children } = $props();
</script>
<slot />
{@render children()}

View File

@@ -1,75 +1,156 @@
<script lang="ts">
import { workspaces } from '$lib/stores/workspace';
import { onMount } from 'svelte';
import WorkspaceCard from '$lib/components/WorkspaceCard.svelte';
import WorkspaceModal from '$lib/components/modals/WorkspaceModal.svelte';
import { invoke } from "@tauri-apps/api/core";
let showCreateModal = false;
let loading = true;
let error: string | null = null;
let name = $state("");
let greetMsg = $state("");
onMount(async () => {
try {
await workspaces.loadWorkspaces();
} catch (err) {
console.error('Failed to load workspaces:', err);
error = 'Failed to load workspaces';
} finally {
loading = false;
}
});
async function greet(event: Event) {
event.preventDefault();
// Learn more about Tauri commands at https://tauri.app/develop/calling-rust/
greetMsg = await invoke("greet", { name });
}
</script>
<div class="container mx-auto px-6 py-8 max-w-7xl">
<div class="flex justify-between items-center mb-8">
<div>
<h1 class="text-2xl font-bold">Workspaces</h1>
<p class="text-base-content/70">Manage your API collections and environments</p>
</div>
<button
class="btn btn-primary"
on:click={() => showCreateModal = true}
>
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 mr-2" viewBox="0 0 20 20" fill="currentColor">
<path fill-rule="evenodd" d="M10 3a1 1 0 011 1v5h5a1 1 0 110 2h-5v5a1 1 0 11-2 0v-5H4a1 1 0 110-2h5V4a1 1 0 011-1z" clip-rule="evenodd" />
</svg>
New Workspace
</button>
<main class="container">
<h1>Welcome to Tauri + Svelte</h1>
<div class="row">
<a href="https://vitejs.dev" target="_blank">
<img src="/vite.svg" class="logo vite" alt="Vite Logo" />
</a>
<a href="https://tauri.app" target="_blank">
<img src="/tauri.svg" class="logo tauri" alt="Tauri Logo" />
</a>
<a href="https://kit.svelte.dev" target="_blank">
<img src="/svelte.svg" class="logo svelte-kit" alt="SvelteKit Logo" />
</a>
</div>
<p>Click on the Tauri, Vite, and SvelteKit logos to learn more.</p>
{#if loading}
<div class="flex justify-center py-16">
<span class="loading loading-spinner loading-lg"></span>
</div>
{:else if error}
<div class="alert alert-error">
<svg xmlns="http://www.w3.org/2000/svg" class="stroke-current shrink-0 h-6 w-6" fill="none" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 14l2-2m0 0l2-2m-2 2l-2-2m2 2l2 2m7-2a9 9 0 11-18 0 9 9 0 0118 0z" /></svg>
<span>{error}</span>
</div>
{:else if $workspaces.length === 0}
<div class="flex flex-col items-center justify-center py-16 text-center">
<svg xmlns="http://www.w3.org/2000/svg" class="h-16 w-16 text-base-content/20" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M20 7l-8-4-8 4m16 0l-8 4m8-4v10l-8 4m0-10L4 7m8 4v10M4 7v10l8 4" />
</svg>
<h3 class="mt-4 text-lg font-medium">No workspaces yet</h3>
<p class="mt-1 text-base-content/70">Create a workspace to get started with your API collections</p>
<button
class="btn btn-primary mt-4"
on:click={() => showCreateModal = true}
>
Create Your First Workspace
</button>
</div>
{:else}
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8">
{#each $workspaces as workspace}
<WorkspaceCard {workspace} />
{/each}
</div>
{/if}
</div>
<form class="row" onsubmit={greet}>
<input id="greet-input" placeholder="Enter a name..." bind:value={name} />
<button type="submit">Greet</button>
</form>
<p>{greetMsg}</p>
</main>
<WorkspaceModal
show={showCreateModal}
on:close={() => showCreateModal = false}
/>
<style>
.logo.vite:hover {
filter: drop-shadow(0 0 2em #747bff);
}
.logo.svelte-kit:hover {
filter: drop-shadow(0 0 2em #ff3e00);
}
:root {
font-family: Inter, Avenir, Helvetica, Arial, sans-serif;
font-size: 16px;
line-height: 24px;
font-weight: 400;
color: #0f0f0f;
background-color: #f6f6f6;
font-synthesis: none;
text-rendering: optimizeLegibility;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
-webkit-text-size-adjust: 100%;
}
.container {
margin: 0;
padding-top: 10vh;
display: flex;
flex-direction: column;
justify-content: center;
text-align: center;
}
.logo {
height: 6em;
padding: 1.5em;
will-change: filter;
transition: 0.75s;
}
.logo.tauri:hover {
filter: drop-shadow(0 0 2em #24c8db);
}
.row {
display: flex;
justify-content: center;
}
a {
font-weight: 500;
color: #646cff;
text-decoration: inherit;
}
a:hover {
color: #535bf2;
}
h1 {
text-align: center;
}
input,
button {
border-radius: 8px;
border: 1px solid transparent;
padding: 0.6em 1.2em;
font-size: 1em;
font-weight: 500;
font-family: inherit;
color: #0f0f0f;
background-color: #ffffff;
transition: border-color 0.25s;
box-shadow: 0 2px 2px rgba(0, 0, 0, 0.2);
}
button {
cursor: pointer;
}
button:hover {
border-color: #396cd8;
}
button:active {
border-color: #396cd8;
background-color: #e8e8e8;
}
input,
button {
outline: none;
}
#greet-input {
margin-right: 5px;
}
@media (prefers-color-scheme: dark) {
:root {
color: #f6f6f6;
background-color: #2f2f2f;
}
a:hover {
color: #24c8db;
}
input,
button {
color: #ffffff;
background-color: #0f0f0f98;
}
button:active {
background-color: #0f0f0f69;
}
}
</style>

121
src/routes/layout.css Normal file
View File

@@ -0,0 +1,121 @@
@import "tailwindcss";
@import "tw-animate-css";
@custom-variant dark (&:is(.dark *));
:root {
--radius: 0.625rem;
--background: oklch(1 0 0);
--foreground: oklch(0.129 0.042 264.695);
--card: oklch(1 0 0);
--card-foreground: oklch(0.129 0.042 264.695);
--popover: oklch(1 0 0);
--popover-foreground: oklch(0.129 0.042 264.695);
--primary: oklch(0.208 0.042 265.755);
--primary-foreground: oklch(0.984 0.003 247.858);
--secondary: oklch(0.968 0.007 247.896);
--secondary-foreground: oklch(0.208 0.042 265.755);
--muted: oklch(0.968 0.007 247.896);
--muted-foreground: oklch(0.554 0.046 257.417);
--accent: oklch(0.968 0.007 247.896);
--accent-foreground: oklch(0.208 0.042 265.755);
--destructive: oklch(0.577 0.245 27.325);
--border: oklch(0.929 0.013 255.508);
--input: oklch(0.929 0.013 255.508);
--ring: oklch(0.704 0.04 256.788);
--chart-1: oklch(0.646 0.222 41.116);
--chart-2: oklch(0.6 0.118 184.704);
--chart-3: oklch(0.398 0.07 227.392);
--chart-4: oklch(0.828 0.189 84.429);
--chart-5: oklch(0.769 0.188 70.08);
--sidebar: oklch(0.984 0.003 247.858);
--sidebar-foreground: oklch(0.129 0.042 264.695);
--sidebar-primary: oklch(0.208 0.042 265.755);
--sidebar-primary-foreground: oklch(0.984 0.003 247.858);
--sidebar-accent: oklch(0.968 0.007 247.896);
--sidebar-accent-foreground: oklch(0.208 0.042 265.755);
--sidebar-border: oklch(0.929 0.013 255.508);
--sidebar-ring: oklch(0.704 0.04 256.788);
}
.dark {
--background: oklch(0.129 0.042 264.695);
--foreground: oklch(0.984 0.003 247.858);
--card: oklch(0.208 0.042 265.755);
--card-foreground: oklch(0.984 0.003 247.858);
--popover: oklch(0.208 0.042 265.755);
--popover-foreground: oklch(0.984 0.003 247.858);
--primary: oklch(0.929 0.013 255.508);
--primary-foreground: oklch(0.208 0.042 265.755);
--secondary: oklch(0.279 0.041 260.031);
--secondary-foreground: oklch(0.984 0.003 247.858);
--muted: oklch(0.279 0.041 260.031);
--muted-foreground: oklch(0.704 0.04 256.788);
--accent: oklch(0.279 0.041 260.031);
--accent-foreground: oklch(0.984 0.003 247.858);
--destructive: oklch(0.704 0.191 22.216);
--border: oklch(1 0 0 / 10%);
--input: oklch(1 0 0 / 15%);
--ring: oklch(0.551 0.027 264.364);
--chart-1: oklch(0.488 0.243 264.376);
--chart-2: oklch(0.696 0.17 162.48);
--chart-3: oklch(0.769 0.188 70.08);
--chart-4: oklch(0.627 0.265 303.9);
--chart-5: oklch(0.645 0.246 16.439);
--sidebar: oklch(0.208 0.042 265.755);
--sidebar-foreground: oklch(0.984 0.003 247.858);
--sidebar-primary: oklch(0.488 0.243 264.376);
--sidebar-primary-foreground: oklch(0.984 0.003 247.858);
--sidebar-accent: oklch(0.279 0.041 260.031);
--sidebar-accent-foreground: oklch(0.984 0.003 247.858);
--sidebar-border: oklch(1 0 0 / 10%);
--sidebar-ring: oklch(0.551 0.027 264.364);
}
@theme inline {
--radius-sm: calc(var(--radius) - 4px);
--radius-md: calc(var(--radius) - 2px);
--radius-lg: var(--radius);
--radius-xl: calc(var(--radius) + 4px);
--color-background: var(--background);
--color-foreground: var(--foreground);
--color-card: var(--card);
--color-card-foreground: var(--card-foreground);
--color-popover: var(--popover);
--color-popover-foreground: var(--popover-foreground);
--color-primary: var(--primary);
--color-primary-foreground: var(--primary-foreground);
--color-secondary: var(--secondary);
--color-secondary-foreground: var(--secondary-foreground);
--color-muted: var(--muted);
--color-muted-foreground: var(--muted-foreground);
--color-accent: var(--accent);
--color-accent-foreground: var(--accent-foreground);
--color-destructive: var(--destructive);
--color-border: var(--border);
--color-input: var(--input);
--color-ring: var(--ring);
--color-chart-1: var(--chart-1);
--color-chart-2: var(--chart-2);
--color-chart-3: var(--chart-3);
--color-chart-4: var(--chart-4);
--color-chart-5: var(--chart-5);
--color-sidebar: var(--sidebar);
--color-sidebar-foreground: var(--sidebar-foreground);
--color-sidebar-primary: var(--sidebar-primary);
--color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
--color-sidebar-accent: var(--sidebar-accent);
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
--color-sidebar-border: var(--sidebar-border);
--color-sidebar-ring: var(--sidebar-ring);
}
@layer base {
* {
@apply border-border outline-ring/50;
}
body {
@apply bg-background text-foreground;
}
}

View File

@@ -1,49 +0,0 @@
<script lang="ts">
import { page } from '$app/stores';
import { workspaces, activeWorkspace } from '$lib/stores/workspace';
import { onMount } from 'svelte';
import Sidebar from '$lib/components/Sidebar.svelte';
import WorkspaceToolbar from '$lib/components/WorkspaceToolbar.svelte';
let drawerOpen = true;
onMount(async () => {
const workspace = await workspaces.getWorkspace($page.params.id);
if (workspace) {
activeWorkspace.set(workspace);
}
});
</script>
<div class="drawer lg:drawer-open">
<input
id="drawer"
type="checkbox"
class="drawer-toggle"
bind:checked={drawerOpen}
/>
<div class="drawer-content flex flex-col">
{#if drawerOpen}
<button
class="lg:hidden fixed bottom-4 right-4 btn btn-circle btn-primary z-50"
on:click={() => drawerOpen = false}
>
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
{/if}
<WorkspaceToolbar />
<slot />
</div>
<div class="drawer-side">
<label
for="drawer"
class="drawer-overlay"
on:click={() => drawerOpen = false}
></label>
<Sidebar />
</div>
</div>

View File

@@ -1,23 +0,0 @@
<script lang="ts">
import { page } from '$app/stores';
import { onMount } from 'svelte';
import { workspaces, activeWorkspace } from '$lib/stores/workspace';
import RequestPanel from '$lib/components/RequestPanel.svelte';
import ResponsePanel from '$lib/components/ResponsePanel.svelte';
onMount(async () => {
try {
const workspace = await workspaces.getWorkspace($page.params.id);
if (workspace) {
activeWorkspace.set(workspace);
}
} catch (error) {
console.error('Failed to load workspace:', error);
}
});
</script>
<div class="p-4 space-y-4">
<RequestPanel />
<ResponsePanel />
</div>

View File

@@ -1,14 +0,0 @@
import { writable } from 'svelte/store';
import type { Collection, Variable, Request as ApiRequest } from '../types';
export const collections = writable<Collection[]>([]);
export const variables = writable<Variable[]>([]);
export const activeCollection = writable<Collection | null>(null);
export const activeRequest = writable<ApiRequest | null>(null);
export const currentResponse = writable<{
status: number;
statusText: string;
headers: Record<string, string>;
body: string;
time: number;
} | null>(null);

View File

@@ -1,58 +0,0 @@
export interface Variable {
id: string;
name: string;
value: string;
description?: string;
}
export interface Collection {
id: string;
workspace_id: string;
name: string;
description?: string;
created_at: number;
updated_at: number;
requests: Request[];
}
export interface Request {
id: string;
name: string;
url: string;
method: 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH';
headers: Header[];
queryParams: QueryParam[];
body?: string;
}
export interface Header {
id: string;
key: string;
value: string;
enabled: boolean;
}
export interface QueryParam {
id: string;
key: string;
value: string;
enabled: boolean;
}
export interface RequestResponse {
status: number;
statusText: string;
headers: Record<string, string>;
body: any;
time: number;
}
export interface Workspace {
id: string;
name: string;
variables?: Variable[];
collections?: Collection[];
description?: string;
created_at: number;
updated_at: number;
}