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,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();
}