first functionable

This commit is contained in:
Julian Freeman
2025-12-01 07:02:21 -04:00
commit 075ceef486
48 changed files with 8488 additions and 0 deletions

67
src/App.vue Normal file
View File

@@ -0,0 +1,67 @@
<script setup lang="ts">
import { useRequestStore } from './stores/requestStore';
import RequestPanel from './components/RequestPanel.vue';
import ResponsePanel from './components/ResponsePanel.vue';
import MethodBadge from './components/MethodBadge.vue';
import { History, Layers, Zap } from 'lucide-vue-next';
const store = useRequestStore();
</script>
<template>
<div class="flex h-screen w-full bg-slate-950 text-slate-200 font-sans overflow-hidden">
<!-- Sidebar -->
<aside class="w-64 flex-shrink-0 border-r border-slate-800 flex flex-col bg-slate-950">
<!-- Header -->
<div class="h-14 flex items-center px-4 border-b border-slate-800 gap-2">
<div class="bg-indigo-500/20 p-1.5 rounded-lg">
<Zap class="w-5 h-5 text-indigo-400" />
</div>
<span class="font-bold text-slate-100 tracking-tight">LiteRequest</span>
</div>
<!-- Nav Tabs -->
<div class="flex p-2 gap-1">
<button class="flex-1 flex items-center justify-center gap-2 py-1.5 text-xs font-medium bg-slate-900 text-slate-200 rounded border border-slate-800 shadow-sm">
<History class="w-3.5 h-3.5" /> History
</button>
<button class="flex-1 flex items-center justify-center gap-2 py-1.5 text-xs font-medium text-slate-500 hover:bg-slate-900 hover:text-slate-300 rounded transition-colors">
<Layers class="w-3.5 h-3.5" /> Collections
</button>
</div>
<!-- History List -->
<div class="flex-1 overflow-y-auto">
<div v-if="store.history.length === 0" class="p-8 text-center text-slate-600 text-xs">
No history yet. Make a request!
</div>
<div v-else class="flex flex-col">
<button
v-for="item in store.history"
:key="item.timestamp"
@click="store.loadRequest(item)"
class="text-left px-3 py-3 border-b border-slate-800/50 hover:bg-slate-900 transition-colors group flex flex-col gap-1.5"
>
<div class="flex items-center gap-2 overflow-hidden w-full">
<MethodBadge :method="item.method" />
<span class="text-xs text-slate-400 truncate font-mono opacity-75">{{ new Date(item.timestamp).toLocaleTimeString() }}</span>
</div>
<div class="text-sm text-slate-300 truncate px-1 font-medium" :title="item.url">
{{ item.url || 'No URL' }}
</div>
</button>
</div>
</div>
</aside>
<!-- Main Workspace -->
<main class="flex-1 flex flex-col min-w-0 bg-slate-900">
<div class="flex-1 min-h-0">
<RequestPanel />
</div>
<div class="h-1/2 border-t border-slate-800 min-h-0">
<ResponsePanel />
</div>
</main>
</div>
</template>

1
src/assets/vue.svg Normal file
View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="37.07" height="36" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 198"><path fill="#41B883" d="M204.8 0H256L128 220.8L0 0h97.92L128 51.2L157.44 0h47.36Z"></path><path fill="#41B883" d="m0 0l128 220.8L256 0h-51.2L128 132.48L50.56 0H0Z"></path><path fill="#35495E" d="M50.56 0L128 133.12L204.8 0h-47.36L128 51.2L97.92 0H50.56Z"></path></svg>

After

Width:  |  Height:  |  Size: 496 B

View File

@@ -0,0 +1,91 @@
<script setup lang="ts">
import { Trash2, Plus } from 'lucide-vue-next';
import type { KeyValue } from '../stores/requestStore';
const props = defineProps<{
modelValue: KeyValue[];
title?: string;
}>();
const emit = defineEmits(['update:modelValue']);
const addRow = () => {
const newData = [...props.modelValue, {
id: crypto.randomUUID(),
key: '',
value: '',
enabled: true
}];
emit('update:modelValue', newData);
};
const removeRow = (index: number) => {
const newData = [...props.modelValue];
newData.splice(index, 1);
emit('update:modelValue', newData);
};
</script>
<template>
<div class="flex flex-col h-full">
<div class="flex-1 overflow-y-auto p-4">
<table class="w-full text-left border-collapse">
<thead>
<tr class="text-xs text-slate-500 border-b border-slate-800">
<th class="pb-2 pl-2 font-medium w-8"></th>
<th class="pb-2 font-medium w-1/3">Key</th>
<th class="pb-2 font-medium">Value</th>
<th class="pb-2 w-8"></th>
</tr>
</thead>
<tbody>
<tr
v-for="(item, index) in modelValue"
:key="item.id"
class="group"
>
<td class="py-1 pl-2 align-middle">
<input
type="checkbox"
v-model="item.enabled"
class="rounded bg-slate-800 border-slate-700 text-indigo-500 focus:ring-indigo-500/50 focus:ring-offset-0"
>
</td>
<td class="py-1 pr-2 align-middle">
<input
type="text"
v-model="item.key"
placeholder="Key"
class="w-full bg-transparent border-b border-transparent hover:border-slate-700 focus:border-indigo-500 focus:outline-none py-1 px-1 text-sm text-slate-300 transition-colors"
>
</td>
<td class="py-1 pr-2 align-middle">
<input
type="text"
v-model="item.value"
placeholder="Value"
class="w-full bg-transparent border-b border-transparent hover:border-slate-700 focus:border-indigo-500 focus:outline-none py-1 px-1 text-sm text-slate-300 transition-colors"
>
</td>
<td class="py-1 align-middle text-right">
<button
@click="removeRow(index)"
class="p-1 text-slate-600 hover:text-rose-400 opacity-0 group-hover:opacity-100 transition-all"
title="Remove"
>
<Trash2 class="w-4 h-4" />
</button>
</td>
</tr>
</tbody>
</table>
<button
@click="addRow"
class="mt-2 flex items-center text-xs text-slate-500 hover:text-indigo-400 transition-colors px-2 py-1"
>
<Plus class="w-3 h-3 mr-1" /> Add Item
</button>
</div>
</div>
</template>

View File

@@ -0,0 +1,27 @@
<script setup lang="ts">
import { computed } from 'vue';
const props = defineProps<{
method: string;
}>();
const colors = computed(() => {
switch (props.method.toUpperCase()) {
case 'GET': return 'bg-sky-500/20 text-sky-400 border-sky-500/30';
case 'POST': return 'bg-emerald-500/20 text-emerald-400 border-emerald-500/30';
case 'PUT': return 'bg-amber-500/20 text-amber-400 border-amber-500/30';
case 'DELETE': return 'bg-rose-500/20 text-rose-400 border-rose-500/30';
case 'PATCH': return 'bg-violet-500/20 text-violet-400 border-violet-500/30';
default: return 'bg-slate-500/20 text-slate-400 border-slate-500/30';
}
});
</script>
<template>
<span
class="px-2 py-0.5 text-xs font-bold rounded border backdrop-blur-sm uppercase tracking-wider select-none"
:class="colors"
>
{{ method }}
</span>
</template>

View File

@@ -0,0 +1,133 @@
<script setup lang="ts">
import { ref } from 'vue';
import { useRequestStore } from '../stores/requestStore';
import KeyValueEditor from './KeyValueEditor.vue';
import { invoke } from '@tauri-apps/api/core';
import { Codemirror } from 'vue-codemirror';
import { json } from '@codemirror/lang-json';
import { oneDark } from '@codemirror/theme-one-dark';
import { Play, Loader2 } from 'lucide-vue-next';
const store = useRequestStore();
const activeTab = ref('params');
const isLoading = ref(false);
const extensions = [json(), oneDark];
const methods = ['GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'HEAD', 'OPTIONS'];
const executeRequest = async () => {
if (!store.activeRequest.url) return;
isLoading.value = true;
try {
// Prepare headers and params map
const headers: Record<string, string> = {};
store.activeRequest.headers.filter(h => h.enabled && h.key).forEach(h => headers[h.key] = h.value);
const params: Record<string, string> = {};
store.activeRequest.params.filter(p => p.enabled && p.key).forEach(p => params[p.key] = p.value);
// Execute
const response: any = await invoke('execute_request', {
method: store.activeRequest.method,
url: store.activeRequest.url,
headers,
body: store.activeRequest.body || null,
queryParams: params,
});
// Update Store
store.activeRequest.response = {
status: response.status,
headers: response.headers,
body: response.body,
time: response.time_elapsed,
size: new Blob([response.body]).size
};
// Add to history if successful
store.addToHistory(store.activeRequest);
} catch (error) {
console.error(error);
// Simple alert for now, or could use a toast
alert('Request Failed: ' + error);
} finally {
isLoading.value = false;
}
};
</script>
<template>
<div class="flex flex-col h-full bg-slate-900">
<!-- Top Bar -->
<div class="p-4 border-b border-slate-800 flex gap-2 items-center">
<div class="flex-1 flex items-center bg-slate-950 rounded-lg border border-slate-800 focus-within:border-indigo-500/50 focus-within:ring-1 focus-within:ring-indigo-500/50 transition-all overflow-hidden">
<select
v-model="store.activeRequest.method"
class="bg-transparent text-xs font-bold px-3 py-2 text-slate-300 border-r border-slate-800 focus:outline-none hover:bg-slate-900 cursor-pointer uppercase appearance-none"
>
<option v-for="m in methods" :key="m" :value="m">{{ m }}</option>
</select>
<input
type="text"
v-model="store.activeRequest.url"
placeholder="https://api.example.com/v1/users"
class="flex-1 bg-transparent border-none focus:ring-0 text-sm text-slate-200 px-3 py-2 placeholder-slate-600"
@keydown.enter="executeRequest"
>
</div>
<button
@click="executeRequest"
:disabled="isLoading"
class="bg-indigo-600 hover:bg-indigo-500 disabled:opacity-50 disabled:cursor-not-allowed text-white px-4 py-2 rounded-lg font-medium text-sm flex items-center gap-2 transition-all shadow-lg shadow-indigo-900/20"
>
<Loader2 v-if="isLoading" class="w-4 h-4 animate-spin" />
<Play v-else class="w-4 h-4 fill-current" />
Send
</button>
</div>
<!-- Configuration Tabs -->
<div class="flex border-b border-slate-800">
<button
v-for="tab in ['params', 'headers', 'body']"
:key="tab"
@click="activeTab = tab"
class="px-4 py-2 text-xs font-medium uppercase tracking-wide border-b-2 transition-colors"
:class="activeTab === tab ? 'border-indigo-500 text-indigo-400 bg-indigo-500/5' : 'border-transparent text-slate-500 hover:text-slate-300 hover:bg-slate-800/50'"
>
{{ tab }}
</button>
</div>
<!-- Content Area -->
<div class="flex-1 overflow-hidden relative">
<KeepAlive>
<KeyValueEditor
v-if="activeTab === 'params'"
v-model="store.activeRequest.params"
/>
</KeepAlive>
<KeepAlive>
<KeyValueEditor
v-if="activeTab === 'headers'"
v-model="store.activeRequest.headers"
/>
</KeepAlive>
<div v-if="activeTab === 'body'" class="h-full w-full overflow-hidden">
<Codemirror
v-model="store.activeRequest.body"
placeholder="Request Body (JSON)"
:style="{ height: '100%' }"
:autofocus="true"
:indent-with-tab="true"
:tab-size="2"
:extensions="extensions"
/>
</div>
</div>
</div>
</template>

View File

@@ -0,0 +1,60 @@
<script setup lang="ts">
import { computed } from 'vue';
import { useRequestStore } from '../stores/requestStore';
import { Codemirror } from 'vue-codemirror';
import { json } from '@codemirror/lang-json';
import { oneDark } from '@codemirror/theme-one-dark';
import { EditorView } from '@codemirror/view';
const store = useRequestStore();
const extensions = [json(), oneDark, EditorView.editable.of(false)];
const formattedBody = computed(() => {
if (!store.activeRequest.response) return '';
try {
// Auto pretty print
return JSON.stringify(JSON.parse(store.activeRequest.response.body), null, 2);
} catch (e) {
return store.activeRequest.response.body;
}
});
const statusColor = computed(() => {
const s = store.activeRequest.response?.status || 0;
if (s >= 200 && s < 300) return 'text-emerald-400';
if (s >= 400) return 'text-rose-400';
return 'text-amber-400';
});
</script>
<template>
<div class="flex flex-col h-full border-t border-slate-800 bg-slate-900">
<div v-if="store.activeRequest.response" class="flex flex-col h-full">
<!-- Meta Bar -->
<div class="px-4 py-2 border-b border-slate-800 flex gap-4 text-xs items-center bg-slate-950/50">
<div class="font-mono">
Status: <span :class="['font-bold', statusColor]">{{ store.activeRequest.response.status }}</span>
</div>
<div class="text-slate-500">
Time: <span class="text-slate-300">{{ store.activeRequest.response.time }}ms</span>
</div>
<div class="text-slate-500">
Size: <span class="text-slate-300">{{ (store.activeRequest.response.size / 1024).toFixed(2) }} KB</span>
</div>
</div>
<!-- Output -->
<div class="flex-1 overflow-hidden">
<Codemirror
:model-value="formattedBody"
:style="{ height: '100%' }"
:extensions="extensions"
/>
</div>
</div>
<div v-else class="flex-1 flex items-center justify-center text-slate-600 text-sm">
No response yet
</div>
</div>
</template>

8
src/main.ts Normal file
View File

@@ -0,0 +1,8 @@
import { createApp } from "vue";
import { createPinia } from "pinia";
import "./style.css";
import App from "./App.vue";
const app = createApp(App);
app.use(createPinia());
app.mount("#app");

View File

@@ -0,0 +1,99 @@
import { defineStore } from 'pinia';
import { ref, watch } from 'vue';
export interface KeyValue {
id: string;
key: string;
value: string;
enabled: boolean;
}
export interface RequestData {
id: string;
method: string;
url: string;
params: KeyValue[];
headers: KeyValue[];
body: string;
response?: {
status: number;
headers: Record<string, string>;
body: string;
time: number;
size: number;
};
timestamp: number;
}
export const useRequestStore = defineStore('request', () => {
// State
const history = ref<RequestData[]>([]);
const activeRequest = ref<RequestData>({
id: crypto.randomUUID(),
method: 'GET',
url: '',
params: [
{ id: crypto.randomUUID(), key: '', value: '', enabled: true }
],
headers: [
{ id: crypto.randomUUID(), key: '', value: '', enabled: true }
],
body: '',
timestamp: Date.now(),
});
// Load history from local storage
const savedHistory = localStorage.getItem('request_history');
if (savedHistory) {
try {
history.value = JSON.parse(savedHistory);
} catch (e) {
console.error('Failed to parse history', e);
}
}
// Actions
const addToHistory = (req: RequestData) => {
// Add to beginning, limit to 50
const newEntry = { ...req, timestamp: Date.now() };
// Don't store the empty trailing rows in history to save space?
// Actually keep them for consistency or filter them. Let's filter empty keys.
newEntry.params = newEntry.params.filter(p => p.key.trim() !== '');
newEntry.headers = newEntry.headers.filter(h => h.key.trim() !== '');
history.value.unshift(newEntry);
if (history.value.length > 50) {
history.value.pop();
}
};
const clearHistory = () => {
history.value = [];
};
const loadRequest = (req: RequestData) => {
// Deep copy
const loaded = JSON.parse(JSON.stringify(req));
loaded.id = crypto.randomUUID();
loaded.response = undefined;
// Ensure at least one empty row for editing
if (loaded.params.length === 0) loaded.params.push({ id: crypto.randomUUID(), key: '', value: '', enabled: true });
if (loaded.headers.length === 0) loaded.headers.push({ id: crypto.randomUUID(), key: '', value: '', enabled: true });
activeRequest.value = loaded;
};
// Watch and Save
watch(history, (newVal) => {
localStorage.setItem('request_history', JSON.stringify(newVal));
}, { deep: true });
return {
history,
activeRequest,
addToHistory,
clearHistory,
loadRequest
};
});

11
src/style.css Normal file
View File

@@ -0,0 +1,11 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
body {
@apply bg-slate-950 text-slate-200 h-screen overflow-hidden;
}
#app {
@apply h-full w-full;
}

7
src/vite-env.d.ts vendored Normal file
View File

@@ -0,0 +1,7 @@
/// <reference types="vite/client" />
declare module "*.vue" {
import type { DefineComponent } from "vue";
const component: DefineComponent<{}, {}, any>;
export default component;
}