starting refactor to shadcn and redb
This commit is contained in:
@@ -1 +0,0 @@
|
||||
<slot />
|
||||
19
src/app.css
19
src/app.css
@@ -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);
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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}
|
||||
@@ -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}
|
||||
/>
|
||||
@@ -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}
|
||||
/>
|
||||
@@ -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}
|
||||
@@ -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}
|
||||
/>
|
||||
@@ -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}
|
||||
@@ -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}
|
||||
@@ -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>
|
||||
@@ -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);
|
||||
}
|
||||
});
|
||||
@@ -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
13
src/lib/utils.ts
Normal 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 };
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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>
|
||||
@@ -1,5 +1,7 @@
|
||||
<script>
|
||||
import "../app.css";
|
||||
<script lang="ts">
|
||||
import './layout.css';
|
||||
|
||||
let { children } = $props();
|
||||
</script>
|
||||
|
||||
<slot />
|
||||
{@render children()}
|
||||
|
||||
@@ -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
121
src/routes/layout.css
Normal 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;
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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);
|
||||
@@ -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;
|
||||
}
|
||||
Reference in New Issue
Block a user