starting refactor to shadcn and redb
This commit is contained in:
2
.gitignore
vendored
2
.gitignore
vendored
@@ -8,5 +8,3 @@ node_modules
|
|||||||
!.env.example
|
!.env.example
|
||||||
vite.config.js.timestamp-*
|
vite.config.js.timestamp-*
|
||||||
vite.config.ts.timestamp-*
|
vite.config.ts.timestamp-*
|
||||||
|
|
||||||
src-tauri/target
|
|
||||||
5
.vscode/settings.json
vendored
5
.vscode/settings.json
vendored
@@ -1,3 +1,6 @@
|
|||||||
{
|
{
|
||||||
"svelte.enable-ts-plugin": true
|
"svelte.enable-ts-plugin": true,
|
||||||
|
"files.associations": {
|
||||||
|
"*.css": "tailwindcss"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
79
README.md
79
README.md
@@ -1,78 +1,7 @@
|
|||||||
# Resona - API Client
|
# Tauri + SvelteKit + TypeScript
|
||||||
|
|
||||||
Resona is a modern, cross-platform API client built with Tauri, SvelteKit, and TypeScript. It provides a powerful interface for testing and managing API requests with features like workspace management, collections, and environment variables.
|
This template should help get you started developing with Tauri, SvelteKit and TypeScript in Vite.
|
||||||
|
|
||||||
## Features
|
## Recommended IDE Setup
|
||||||
|
|
||||||
- 🚀 Cross-platform desktop application (Windows, macOS, Linux)
|
[VS Code](https://code.visualstudio.com/) + [Svelte](https://marketplace.visualstudio.com/items?itemName=svelte.svelte-vscode) + [Tauri](https://marketplace.visualstudio.com/items?itemName=tauri-apps.tauri-vscode) + [rust-analyzer](https://marketplace.visualstudio.com/items?itemName=rust-lang.rust-analyzer).
|
||||||
- 📁 Workspace-based organization
|
|
||||||
- 📑 Request collections
|
|
||||||
- 🔄 Environment variables support
|
|
||||||
- 🎨 Beautiful UI with DaisyUI components
|
|
||||||
- 🌓 Light/Dark theme support
|
|
||||||
- 💾 Local storage persistence
|
|
||||||
- ⚡ Fast and lightweight
|
|
||||||
|
|
||||||
## Tech Stack
|
|
||||||
|
|
||||||
- **Frontend**: SvelteKit + TypeScript
|
|
||||||
- **Backend**: Tauri (Rust)
|
|
||||||
- **Styling**: TailwindCSS + DaisyUI
|
|
||||||
- **Database**: SQLite (via rusqlite)
|
|
||||||
- **Build Tool**: Vite
|
|
||||||
|
|
||||||
## Prerequisites
|
|
||||||
|
|
||||||
- Node.js (v16 or later)
|
|
||||||
- Bun
|
|
||||||
- Rust (latest stable)
|
|
||||||
- Cargo
|
|
||||||
- System dependencies for Tauri (see [Tauri prerequisites](https://tauri.app/v1/guides/getting-started/prerequisites))
|
|
||||||
|
|
||||||
## Installation
|
|
||||||
|
|
||||||
1. Clone the repository and install dependencies:
|
|
||||||
```bash
|
|
||||||
git clone https://github.com/Xyroscar/resona.git
|
|
||||||
cd resona
|
|
||||||
bun install
|
|
||||||
```
|
|
||||||
|
|
||||||
2. For development server:
|
|
||||||
```bash
|
|
||||||
bun run tauri dev
|
|
||||||
```
|
|
||||||
|
|
||||||
This will launch both the SvelteKit development server and the Tauri application.
|
|
||||||
|
|
||||||
## Building
|
|
||||||
|
|
||||||
To create a production build:
|
|
||||||
```bash
|
|
||||||
bun run tauri build
|
|
||||||
```
|
|
||||||
|
|
||||||
The built application will be available in the `src-tauri/target/release` directory.
|
|
||||||
|
|
||||||
|
|
||||||
## Configuration
|
|
||||||
|
|
||||||
### Development Server
|
|
||||||
|
|
||||||
The development server runs on port 1420 by default. You can modify this in the `vite.config.ts` file.
|
|
||||||
|
|
||||||
### Tauri
|
|
||||||
|
|
||||||
Tauri configuration can be found in `src-tauri/tauri.conf.json`. This includes window settings, build configurations, and security policies.
|
|
||||||
|
|
||||||
### Styling
|
|
||||||
|
|
||||||
The application uses TailwindCSS with DaisyUI for styling. Configuration can be found in:
|
|
||||||
- `tailwind.config.js` - TailwindCSS configuration
|
|
||||||
- `src/app.css` - Global styles
|
|
||||||
|
|
||||||
## Acknowledgments
|
|
||||||
|
|
||||||
- [Tauri](https://tauri.app/) - For the desktop application framework
|
|
||||||
- [SvelteKit](https://kit.svelte.dev/) - For the frontend framework
|
|
||||||
- [DaisyUI](https://daisyui.com/) - For the UI components
|
|
||||||
|
|||||||
16
components.json
Normal file
16
components.json
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
{
|
||||||
|
"$schema": "https://shadcn-svelte.com/schema.json",
|
||||||
|
"tailwind": {
|
||||||
|
"css": "src/routes/layout.css",
|
||||||
|
"baseColor": "slate"
|
||||||
|
},
|
||||||
|
"aliases": {
|
||||||
|
"components": "$lib/components",
|
||||||
|
"utils": "$lib/utils",
|
||||||
|
"ui": "$lib/components/ui",
|
||||||
|
"hooks": "$lib/hooks",
|
||||||
|
"lib": "$lib"
|
||||||
|
},
|
||||||
|
"typescript": true,
|
||||||
|
"registry": "https://shadcn-svelte.com/registry"
|
||||||
|
}
|
||||||
40
package.json
40
package.json
@@ -13,26 +13,26 @@
|
|||||||
},
|
},
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@tauri-apps/api": "^2.0.3",
|
"@tauri-apps/api": "^2",
|
||||||
"@tauri-apps/plugin-shell": ">=2.0.0",
|
"@tauri-apps/plugin-opener": "^2"
|
||||||
"autoprefixer": "^10.4.20",
|
|
||||||
"daisyui": "^4.12.14",
|
|
||||||
"tailwind": "^4.0.0",
|
|
||||||
"tailwindcss": "^3.4.14"
|
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@sveltejs/adapter-static": "^3.0.1",
|
"@lucide/svelte": "^0.554.0",
|
||||||
"@sveltejs/kit": "^2.0.0",
|
"@sveltejs/adapter-static": "^3.0.6",
|
||||||
"@sveltejs/vite-plugin-svelte": "^4.0.0",
|
"@sveltejs/kit": "^2.9.0",
|
||||||
"svelte": "^5.1.9",
|
"@sveltejs/vite-plugin-svelte": "^5.0.0",
|
||||||
"svelte-check": "^4.0.5",
|
"@tailwindcss/forms": "^0.5.10",
|
||||||
"tslib": "^2.4.1",
|
"@tailwindcss/typography": "^0.5.19",
|
||||||
"typescript": "^5.0.0",
|
"@tailwindcss/vite": "^4.1.17",
|
||||||
"vite": "^5.0.3",
|
"@tauri-apps/cli": "^2",
|
||||||
"@tauri-apps/cli": ">=2.0.0"
|
"clsx": "^2.1.1",
|
||||||
},
|
"svelte": "^5.0.0",
|
||||||
"trustedDependencies": [
|
"svelte-check": "^4.0.0",
|
||||||
"core-js",
|
"tailwind-merge": "^3.3.1",
|
||||||
"svelte-preprocess"
|
"tailwind-variants": "^3.2.2",
|
||||||
]
|
"tailwindcss": "^4.1.17",
|
||||||
|
"tw-animate-css": "^1.4.0",
|
||||||
|
"typescript": "~5.6.2",
|
||||||
|
"vite": "^6.0.3"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +0,0 @@
|
|||||||
export default {
|
|
||||||
plugins: {
|
|
||||||
tailwindcss: {},
|
|
||||||
autoprefixer: {},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
7
src-tauri/.gitignore
vendored
Normal file
7
src-tauri/.gitignore
vendored
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
# Generated by Cargo
|
||||||
|
# will have compiled files and executables
|
||||||
|
/target/
|
||||||
|
|
||||||
|
# Generated by Tauri
|
||||||
|
# will have schema files for capabilities auto-completion
|
||||||
|
/gen/schemas
|
||||||
3152
src-tauri/Cargo.lock
generated
3152
src-tauri/Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
@@ -3,21 +3,23 @@ name = "resona"
|
|||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
description = "A Tauri App"
|
description = "A Tauri App"
|
||||||
authors = ["you"]
|
authors = ["you"]
|
||||||
license = ""
|
|
||||||
repository = ""
|
|
||||||
default-run = "resona"
|
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
rust-version = "1.60"
|
|
||||||
|
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||||
|
|
||||||
|
[lib]
|
||||||
|
# The `_lib` suffix may seem redundant but it is necessary
|
||||||
|
# to make the lib name unique and wouldn't conflict with the bin name.
|
||||||
|
# This seems to be only an issue on Windows, see https://github.com/rust-lang/cargo/issues/8519
|
||||||
|
name = "resona_lib"
|
||||||
|
crate-type = ["staticlib", "cdylib", "rlib"]
|
||||||
|
|
||||||
[build-dependencies]
|
[build-dependencies]
|
||||||
tauri-build = { version = "2.0.2", features = [] }
|
tauri-build = { version = "2", features = [] }
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
serde_json = "1.0"
|
tauri = { version = "2", features = [] }
|
||||||
serde = { version = "1.0", features = ["derive"] }
|
tauri-plugin-opener = "2"
|
||||||
tauri = { version = "2.0", features = [] }
|
serde = { version = "1", features = ["derive"] }
|
||||||
reqwest = { version = "0.12", features = ["json"] }
|
serde_json = "1"
|
||||||
tokio = { version = "1.36", features = ["full"] }
|
|
||||||
uuid = { version = "1.7", features = ["v4", "serde"] }
|
|
||||||
rusqlite = "0.32"
|
|
||||||
|
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
"description": "Capability for the main window",
|
"description": "Capability for the main window",
|
||||||
"windows": ["main"],
|
"windows": ["main"],
|
||||||
"permissions": [
|
"permissions": [
|
||||||
"core:default"
|
"core:default",
|
||||||
|
"opener:default"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
@@ -1 +1 @@
|
|||||||
{"default":{"identifier":"default","description":"Capability for the main window","local":true,"windows":["main"],"permissions":["core:default"]}}
|
{"default":{"identifier":"default","description":"Capability for the main window","local":true,"windows":["main"],"permissions":["core:default","opener:default"]}}
|
||||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -1,75 +0,0 @@
|
|||||||
use crate::models::{RequestBody, ResponseBody};
|
|
||||||
use reqwest::header::{HeaderMap, HeaderName, HeaderValue};
|
|
||||||
use std::collections::HashMap;
|
|
||||||
use std::str::FromStr;
|
|
||||||
use std::time::Instant;
|
|
||||||
|
|
||||||
pub async fn send_request(request: RequestBody) -> Result<ResponseBody, String> {
|
|
||||||
let client = reqwest::Client::new();
|
|
||||||
|
|
||||||
// Create headers
|
|
||||||
let mut headers = HeaderMap::new();
|
|
||||||
for header in request.headers.iter().filter(|h| h.enabled) {
|
|
||||||
headers.insert(
|
|
||||||
HeaderName::from_str(&header.key).map_err(|e| e.to_string())?,
|
|
||||||
HeaderValue::from_str(&header.value).map_err(|e| e.to_string())?,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
let start_time = Instant::now();
|
|
||||||
|
|
||||||
// Build and send request
|
|
||||||
let response = match request.method.as_str() {
|
|
||||||
"GET" => client.get(&request.url),
|
|
||||||
"POST" => client.post(&request.url),
|
|
||||||
"PUT" => client.put(&request.url),
|
|
||||||
"DELETE" => client.delete(&request.url),
|
|
||||||
"PATCH" => client.patch(&request.url),
|
|
||||||
_ => return Err("Unsupported HTTP method".to_string()),
|
|
||||||
}
|
|
||||||
.headers(headers);
|
|
||||||
|
|
||||||
// Add body for methods that support it
|
|
||||||
let response = if let Some(body) = request.body {
|
|
||||||
response.body(body)
|
|
||||||
} else {
|
|
||||||
response
|
|
||||||
};
|
|
||||||
|
|
||||||
// Send the request
|
|
||||||
let response = response
|
|
||||||
.send()
|
|
||||||
.await
|
|
||||||
.map_err(|e| format!("Failed to send request: {}", e))?;
|
|
||||||
|
|
||||||
// Process response
|
|
||||||
let status = response.status().as_u16();
|
|
||||||
let status_text = response.status().to_string();
|
|
||||||
|
|
||||||
// Convert response headers
|
|
||||||
let headers: HashMap<String, String> = response
|
|
||||||
.headers()
|
|
||||||
.iter()
|
|
||||||
.map(|(k, v)| {
|
|
||||||
(
|
|
||||||
k.to_string(),
|
|
||||||
v.to_str().unwrap_or("").to_string(),
|
|
||||||
)
|
|
||||||
})
|
|
||||||
.collect();
|
|
||||||
|
|
||||||
let body = response
|
|
||||||
.text()
|
|
||||||
.await
|
|
||||||
.map_err(|e| format!("Failed to read response body: {}", e))?;
|
|
||||||
|
|
||||||
let elapsed = start_time.elapsed().as_millis();
|
|
||||||
|
|
||||||
Ok(ResponseBody {
|
|
||||||
status,
|
|
||||||
status_text,
|
|
||||||
headers,
|
|
||||||
body,
|
|
||||||
time: elapsed,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
@@ -1,49 +0,0 @@
|
|||||||
use crate::storage::{Storage, Collection};
|
|
||||||
use std::sync::Mutex;
|
|
||||||
use tauri::State;
|
|
||||||
|
|
||||||
#[tauri::command]
|
|
||||||
pub async fn create_collection(
|
|
||||||
state: State<'_, Mutex<Storage>>,
|
|
||||||
workspace_id: String,
|
|
||||||
name: String,
|
|
||||||
description: Option<String>,
|
|
||||||
) -> Result<Collection, String> {
|
|
||||||
let mut storage = state.lock().map_err(|e| e.to_string())?;
|
|
||||||
storage
|
|
||||||
.create_collection(&workspace_id, &name, description.as_deref())
|
|
||||||
.map_err(|e| e.to_string())
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tauri::command]
|
|
||||||
pub async fn get_workspace_collections(
|
|
||||||
state: State<'_, Mutex<Storage>>,
|
|
||||||
workspace_id: String,
|
|
||||||
) -> Result<Vec<Collection>, String> {
|
|
||||||
let mut storage = state.lock().map_err(|e| e.to_string())?;
|
|
||||||
storage
|
|
||||||
.get_workspace_collections(&workspace_id)
|
|
||||||
.map_err(|e| e.to_string())
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tauri::command]
|
|
||||||
pub async fn update_collection(
|
|
||||||
state: State<'_, Mutex<Storage>>,
|
|
||||||
id: String,
|
|
||||||
name: String,
|
|
||||||
description: Option<String>,
|
|
||||||
) -> Result<Collection, String> {
|
|
||||||
let mut storage = state.lock().map_err(|e| e.to_string())?;
|
|
||||||
storage
|
|
||||||
.update_collection(&id, &name, description.as_deref())
|
|
||||||
.map_err(|e| e.to_string())
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tauri::command]
|
|
||||||
pub async fn delete_collection(
|
|
||||||
state: State<'_, Mutex<Storage>>,
|
|
||||||
id: String,
|
|
||||||
) -> Result<(), String> {
|
|
||||||
let mut storage = state.lock().map_err(|e| e.to_string())?;
|
|
||||||
storage.delete_collection(&id).map_err(|e| e.to_string())
|
|
||||||
}
|
|
||||||
@@ -1,19 +0,0 @@
|
|||||||
mod workspace;
|
|
||||||
mod collection;
|
|
||||||
|
|
||||||
pub use workspace::*;
|
|
||||||
pub use collection::*;
|
|
||||||
|
|
||||||
pub fn handlers() -> impl Fn(tauri::ipc::Invoke) -> bool {
|
|
||||||
tauri::generate_handler![
|
|
||||||
create_workspace,
|
|
||||||
get_workspaces,
|
|
||||||
get_workspace,
|
|
||||||
update_workspace,
|
|
||||||
delete_workspace,
|
|
||||||
create_collection,
|
|
||||||
get_workspace_collections,
|
|
||||||
update_collection,
|
|
||||||
delete_collection
|
|
||||||
]
|
|
||||||
}
|
|
||||||
@@ -1,54 +0,0 @@
|
|||||||
use crate::storage::{Storage, Workspace};
|
|
||||||
use std::sync::Mutex;
|
|
||||||
use tauri::State;
|
|
||||||
|
|
||||||
#[tauri::command]
|
|
||||||
pub async fn create_workspace(
|
|
||||||
state: State<'_, Mutex<Storage>>,
|
|
||||||
name: String,
|
|
||||||
description: Option<String>,
|
|
||||||
) -> Result<Workspace, String> {
|
|
||||||
let mut storage = state.lock().map_err(|e| e.to_string())?;
|
|
||||||
storage
|
|
||||||
.create_workspace(&name, description.as_deref())
|
|
||||||
.map_err(|e| e.to_string())
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tauri::command]
|
|
||||||
pub async fn get_workspaces(
|
|
||||||
state: State<'_, Mutex<Storage>>,
|
|
||||||
) -> Result<Vec<Workspace>, String> {
|
|
||||||
let mut storage = state.lock().map_err(|e| e.to_string())?;
|
|
||||||
storage.get_workspaces().map_err(|e| e.to_string())
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tauri::command]
|
|
||||||
pub async fn get_workspace(
|
|
||||||
state: State<'_, Mutex<Storage>>,
|
|
||||||
id: String,
|
|
||||||
) -> Result<Option<Workspace>, String> {
|
|
||||||
let mut storage = state.lock().map_err(|e| e.to_string())?;
|
|
||||||
storage.get_workspace(&id).map_err(|e| e.to_string())
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tauri::command]
|
|
||||||
pub async fn update_workspace(
|
|
||||||
state: State<'_, Mutex<Storage>>,
|
|
||||||
id: String,
|
|
||||||
name: String,
|
|
||||||
description: Option<String>,
|
|
||||||
) -> Result<Workspace, String> {
|
|
||||||
let mut storage = state.lock().map_err(|e| e.to_string())?;
|
|
||||||
storage
|
|
||||||
.update_workspace(&id, &name, description.as_deref())
|
|
||||||
.map_err(|e| e.to_string())
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tauri::command]
|
|
||||||
pub async fn delete_workspace(
|
|
||||||
state: State<'_, Mutex<Storage>>,
|
|
||||||
id: String,
|
|
||||||
) -> Result<(), String> {
|
|
||||||
let mut storage = state.lock().map_err(|e| e.to_string())?;
|
|
||||||
storage.delete_workspace(&id).map_err(|e| e.to_string())
|
|
||||||
}
|
|
||||||
@@ -7,6 +7,7 @@ fn greet(name: &str) -> String {
|
|||||||
#[cfg_attr(mobile, tauri::mobile_entry_point)]
|
#[cfg_attr(mobile, tauri::mobile_entry_point)]
|
||||||
pub fn run() {
|
pub fn run() {
|
||||||
tauri::Builder::default()
|
tauri::Builder::default()
|
||||||
|
.plugin(tauri_plugin_opener::init())
|
||||||
.invoke_handler(tauri::generate_handler![greet])
|
.invoke_handler(tauri::generate_handler![greet])
|
||||||
.run(tauri::generate_context!())
|
.run(tauri::generate_context!())
|
||||||
.expect("error while running tauri application");
|
.expect("error while running tauri application");
|
||||||
|
|||||||
@@ -1,27 +1,6 @@
|
|||||||
mod models;
|
// Prevents additional console window on Windows in release, DO NOT REMOVE!!
|
||||||
mod storage;
|
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
|
||||||
mod commands;
|
|
||||||
mod client;
|
|
||||||
|
|
||||||
use commands::handlers as h;
|
|
||||||
use storage::Storage;
|
|
||||||
use tauri::Manager;
|
|
||||||
use std::sync::Mutex;
|
|
||||||
|
|
||||||
fn main() {
|
fn main() {
|
||||||
let handlers = h();
|
resona_lib::run()
|
||||||
tauri::Builder::default()
|
}
|
||||||
.setup(|app| {
|
|
||||||
let app_dir = app.path().app_data_dir()
|
|
||||||
.expect("Failed to get app data dir");
|
|
||||||
|
|
||||||
let storage = Storage::new(app_dir)
|
|
||||||
.expect("Failed to initialize storage");
|
|
||||||
|
|
||||||
app.manage(Mutex::new(storage));
|
|
||||||
Ok(())
|
|
||||||
})
|
|
||||||
.invoke_handler(handlers)
|
|
||||||
.run(tauri::generate_context!())
|
|
||||||
.expect("error while running tauri application");
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,35 +0,0 @@
|
|||||||
use serde::{Deserialize, Serialize};
|
|
||||||
use std::collections::HashMap;
|
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
|
||||||
pub struct Variable {
|
|
||||||
pub id: String,
|
|
||||||
pub name: String,
|
|
||||||
pub value: String,
|
|
||||||
pub description: Option<String>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
|
||||||
pub struct Header {
|
|
||||||
pub id: String,
|
|
||||||
pub key: String,
|
|
||||||
pub value: String,
|
|
||||||
pub enabled: bool,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Deserialize)]
|
|
||||||
pub struct RequestBody {
|
|
||||||
pub url: String,
|
|
||||||
pub method: String,
|
|
||||||
pub headers: Vec<Header>,
|
|
||||||
pub body: Option<String>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Serialize)]
|
|
||||||
pub struct ResponseBody {
|
|
||||||
pub status: u16,
|
|
||||||
pub status_text: String,
|
|
||||||
pub headers: HashMap<String, String>,
|
|
||||||
pub body: String,
|
|
||||||
pub time: u128,
|
|
||||||
}
|
|
||||||
@@ -1,103 +0,0 @@
|
|||||||
use super::*;
|
|
||||||
use rusqlite::{params, Result};
|
|
||||||
|
|
||||||
impl Storage {
|
|
||||||
pub fn create_collection(
|
|
||||||
&mut self,
|
|
||||||
workspace_id: &str,
|
|
||||||
name: &str,
|
|
||||||
description: Option<&str>,
|
|
||||||
) -> Result<Collection> {
|
|
||||||
let now = Self::now();
|
|
||||||
let id = uuid::Uuid::new_v4().to_string();
|
|
||||||
|
|
||||||
self.conn.get_mut().unwrap().execute(
|
|
||||||
"INSERT INTO collections (id, workspace_id, name, description, created_at, updated_at)
|
|
||||||
VALUES (?1, ?2, ?3, ?4, ?5, ?6)",
|
|
||||||
params![id, workspace_id, name, description, now, now],
|
|
||||||
)?;
|
|
||||||
|
|
||||||
Ok(Collection {
|
|
||||||
id,
|
|
||||||
workspace_id: workspace_id.to_string(),
|
|
||||||
name: name.to_string(),
|
|
||||||
description: description.map(String::from),
|
|
||||||
created_at: now,
|
|
||||||
updated_at: now,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_workspace_collections(&mut self, workspace_id: &str) -> Result<Vec<Collection>> {
|
|
||||||
let mut stmt = self.conn.get_mut().unwrap().prepare(
|
|
||||||
"SELECT id, workspace_id, name, description, created_at, updated_at
|
|
||||||
FROM collections
|
|
||||||
WHERE workspace_id = ?
|
|
||||||
ORDER BY created_at DESC"
|
|
||||||
)?;
|
|
||||||
|
|
||||||
let collections = stmt.query_map([workspace_id], |row| {
|
|
||||||
Ok(Collection {
|
|
||||||
id: row.get(0)?,
|
|
||||||
workspace_id: row.get(1)?,
|
|
||||||
name: row.get(2)?,
|
|
||||||
description: row.get(3)?,
|
|
||||||
created_at: row.get(4)?,
|
|
||||||
updated_at: row.get(5)?,
|
|
||||||
})
|
|
||||||
})?;
|
|
||||||
|
|
||||||
collections.collect()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn update_collection(
|
|
||||||
&mut self,
|
|
||||||
id: &str,
|
|
||||||
name: &str,
|
|
||||||
description: Option<&str>,
|
|
||||||
) -> Result<Collection> {
|
|
||||||
let now = Self::now();
|
|
||||||
|
|
||||||
self.conn.get_mut().unwrap().execute(
|
|
||||||
"UPDATE collections
|
|
||||||
SET name = ?1, description = ?2, updated_at = ?3
|
|
||||||
WHERE id = ?4",
|
|
||||||
params![name, description, now, id],
|
|
||||||
)?;
|
|
||||||
|
|
||||||
let mut stmt = self.conn.get_mut().unwrap().prepare(
|
|
||||||
"SELECT workspace_id, created_at FROM collections WHERE id = ?"
|
|
||||||
)?;
|
|
||||||
|
|
||||||
let (workspace_id, created_at) = stmt.query_row([id], |row| {
|
|
||||||
Ok((row.get::<_, String>(0)?, row.get::<_, i64>(1)?))
|
|
||||||
})?;
|
|
||||||
|
|
||||||
Ok(Collection {
|
|
||||||
id: id.to_string(),
|
|
||||||
workspace_id,
|
|
||||||
name: name.to_string(),
|
|
||||||
description: description.map(String::from),
|
|
||||||
created_at,
|
|
||||||
updated_at: now,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn delete_collection(&mut self, id: &str) -> Result<()> {
|
|
||||||
let tx = self.conn.get_mut().unwrap().transaction()?;
|
|
||||||
|
|
||||||
// Delete all requests in this collection
|
|
||||||
tx.execute(
|
|
||||||
"DELETE FROM requests WHERE collection_id = ?",
|
|
||||||
params![id],
|
|
||||||
)?;
|
|
||||||
|
|
||||||
// Delete the collection
|
|
||||||
tx.execute(
|
|
||||||
"DELETE FROM collections WHERE id = ?",
|
|
||||||
params![id],
|
|
||||||
)?;
|
|
||||||
|
|
||||||
tx.commit()?;
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,158 +0,0 @@
|
|||||||
mod workspace;
|
|
||||||
mod collection;
|
|
||||||
|
|
||||||
pub use workspace::*;
|
|
||||||
pub use collection::*;
|
|
||||||
|
|
||||||
use rusqlite::Connection;
|
|
||||||
use serde::{Deserialize, Serialize};
|
|
||||||
use std::fs;
|
|
||||||
use std::sync::RwLock;
|
|
||||||
use std::path::PathBuf;
|
|
||||||
use std::time::{SystemTime, UNIX_EPOCH};
|
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Deserialize)]
|
|
||||||
pub struct Workspace {
|
|
||||||
pub id: String,
|
|
||||||
pub name: String,
|
|
||||||
pub description: Option<String>,
|
|
||||||
pub created_at: i64,
|
|
||||||
pub updated_at: i64,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Deserialize)]
|
|
||||||
pub struct Collection {
|
|
||||||
pub id: String,
|
|
||||||
pub workspace_id: String,
|
|
||||||
pub name: String,
|
|
||||||
pub description: Option<String>,
|
|
||||||
pub created_at: i64,
|
|
||||||
pub updated_at: i64,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Deserialize)]
|
|
||||||
pub struct Variable {
|
|
||||||
pub id: String,
|
|
||||||
pub workspace_id: String,
|
|
||||||
pub name: String,
|
|
||||||
pub value: String,
|
|
||||||
pub description: Option<String>,
|
|
||||||
pub created_at: i64,
|
|
||||||
pub updated_at: i64,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Deserialize)]
|
|
||||||
pub struct Request {
|
|
||||||
pub id: String,
|
|
||||||
pub collection_id: String,
|
|
||||||
pub name: String,
|
|
||||||
pub method: String,
|
|
||||||
pub url: String,
|
|
||||||
pub created_at: i64,
|
|
||||||
pub updated_at: i64,
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct Storage {
|
|
||||||
conn: RwLock<Connection>,
|
|
||||||
data_dir: PathBuf,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Storage {
|
|
||||||
pub fn new(app_dir: PathBuf) -> rusqlite::Result<Self> {
|
|
||||||
let db_path = app_dir.join("resona.db");
|
|
||||||
let data_dir = app_dir.join("data");
|
|
||||||
|
|
||||||
fs::create_dir_all(&data_dir).map_err(|e| rusqlite::Error::InvalidPath(data_dir.clone()))?;
|
|
||||||
|
|
||||||
let conn = Connection::open(db_path)?;
|
|
||||||
Self::init_database(&conn)?;
|
|
||||||
|
|
||||||
Ok(Storage { conn: RwLock::new(conn), data_dir })
|
|
||||||
}
|
|
||||||
|
|
||||||
fn init_database(conn: &Connection) -> rusqlite::Result<()> {
|
|
||||||
conn.execute(
|
|
||||||
"CREATE TABLE IF NOT EXISTS workspaces (
|
|
||||||
id TEXT PRIMARY KEY,
|
|
||||||
name TEXT NOT NULL,
|
|
||||||
description TEXT,
|
|
||||||
created_at INTEGER NOT NULL,
|
|
||||||
updated_at INTEGER NOT NULL
|
|
||||||
)",
|
|
||||||
[],
|
|
||||||
)?;
|
|
||||||
|
|
||||||
conn.execute(
|
|
||||||
"CREATE TABLE IF NOT EXISTS collections (
|
|
||||||
id TEXT PRIMARY KEY,
|
|
||||||
workspace_id TEXT NOT NULL,
|
|
||||||
name TEXT NOT NULL,
|
|
||||||
description TEXT,
|
|
||||||
created_at INTEGER NOT NULL,
|
|
||||||
updated_at INTEGER NOT NULL,
|
|
||||||
FOREIGN KEY(workspace_id) REFERENCES workspaces(id)
|
|
||||||
)",
|
|
||||||
[],
|
|
||||||
)?;
|
|
||||||
|
|
||||||
conn.execute(
|
|
||||||
"CREATE TABLE IF NOT EXISTS variables (
|
|
||||||
id TEXT PRIMARY KEY,
|
|
||||||
workspace_id TEXT NOT NULL,
|
|
||||||
name TEXT NOT NULL,
|
|
||||||
value TEXT NOT NULL,
|
|
||||||
description TEXT,
|
|
||||||
created_at INTEGER NOT NULL,
|
|
||||||
updated_at INTEGER NOT NULL,
|
|
||||||
FOREIGN KEY(workspace_id) REFERENCES workspaces(id)
|
|
||||||
)",
|
|
||||||
[],
|
|
||||||
)?;
|
|
||||||
|
|
||||||
conn.execute(
|
|
||||||
"CREATE TABLE IF NOT EXISTS requests (
|
|
||||||
id TEXT PRIMARY KEY,
|
|
||||||
collection_id TEXT NOT NULL,
|
|
||||||
name TEXT NOT NULL,
|
|
||||||
method TEXT NOT NULL,
|
|
||||||
url TEXT NOT NULL,
|
|
||||||
created_at INTEGER NOT NULL,
|
|
||||||
updated_at INTEGER NOT NULL,
|
|
||||||
FOREIGN KEY(collection_id) REFERENCES collections(id)
|
|
||||||
)",
|
|
||||||
[],
|
|
||||||
)?;
|
|
||||||
|
|
||||||
// Create indexes
|
|
||||||
conn.execute(
|
|
||||||
"CREATE INDEX IF NOT EXISTS idx_collections_workspace
|
|
||||||
ON collections(workspace_id)",
|
|
||||||
[],
|
|
||||||
)?;
|
|
||||||
|
|
||||||
conn.execute(
|
|
||||||
"CREATE INDEX IF NOT EXISTS idx_variables_workspace
|
|
||||||
ON variables(workspace_id)",
|
|
||||||
[],
|
|
||||||
)?;
|
|
||||||
|
|
||||||
conn.execute(
|
|
||||||
"CREATE INDEX IF NOT EXISTS idx_requests_collection
|
|
||||||
ON requests(collection_id)",
|
|
||||||
[],
|
|
||||||
)?;
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) fn now() -> i64 {
|
|
||||||
SystemTime::now()
|
|
||||||
.duration_since(UNIX_EPOCH)
|
|
||||||
.unwrap()
|
|
||||||
.as_secs() as i64
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) fn get_request_data_path(&self, request_id: &str) -> PathBuf {
|
|
||||||
self.data_dir.join(format!("request_{}.json", request_id))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,115 +0,0 @@
|
|||||||
use super::*;
|
|
||||||
use rusqlite::{params, Result, OptionalExtension};
|
|
||||||
|
|
||||||
impl Storage {
|
|
||||||
pub fn create_workspace(&mut self, name: &str, description: Option<&str>) -> Result<Workspace> {
|
|
||||||
let now = Self::now();
|
|
||||||
let id = uuid::Uuid::new_v4().to_string();
|
|
||||||
|
|
||||||
self.conn.get_mut().unwrap().execute(
|
|
||||||
"INSERT INTO workspaces (id, name, description, created_at, updated_at)
|
|
||||||
VALUES (?1, ?2, ?3, ?4, ?5)",
|
|
||||||
params![id, name, description, now, now],
|
|
||||||
)?;
|
|
||||||
|
|
||||||
Ok(Workspace {
|
|
||||||
id,
|
|
||||||
name: name.to_string(),
|
|
||||||
description: description.map(String::from),
|
|
||||||
created_at: now,
|
|
||||||
updated_at: now,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_workspaces(&mut self) -> Result<Vec<Workspace>> {
|
|
||||||
let mut stmt = self.conn.get_mut().unwrap().prepare(
|
|
||||||
"SELECT id, name, description, created_at, updated_at
|
|
||||||
FROM workspaces
|
|
||||||
ORDER BY created_at DESC"
|
|
||||||
)?;
|
|
||||||
|
|
||||||
let workspace_iter = stmt.query_map([], |row| {
|
|
||||||
Ok(Workspace {
|
|
||||||
id: row.get(0)?,
|
|
||||||
name: row.get(1)?,
|
|
||||||
description: row.get(2)?,
|
|
||||||
created_at: row.get(3)?,
|
|
||||||
updated_at: row.get(4)?,
|
|
||||||
})
|
|
||||||
})?;
|
|
||||||
|
|
||||||
workspace_iter.collect()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_workspace(&mut self, id: &str) -> Result<Option<Workspace>> {
|
|
||||||
let mut stmt = self.conn.get_mut().unwrap().prepare(
|
|
||||||
"SELECT id, name, description, created_at, updated_at
|
|
||||||
FROM workspaces
|
|
||||||
WHERE id = ?"
|
|
||||||
)?;
|
|
||||||
|
|
||||||
stmt.query_row(params![id], |row| {
|
|
||||||
Ok(Workspace {
|
|
||||||
id: row.get(0)?,
|
|
||||||
name: row.get(1)?,
|
|
||||||
description: row.get(2)?,
|
|
||||||
created_at: row.get(3)?,
|
|
||||||
updated_at: row.get(4)?,
|
|
||||||
})
|
|
||||||
})
|
|
||||||
.optional()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn update_workspace(&mut self, id: &str, name: &str, description: Option<&str>) -> Result<Workspace> {
|
|
||||||
let now = Self::now();
|
|
||||||
|
|
||||||
self.conn.get_mut().unwrap().execute(
|
|
||||||
"UPDATE workspaces
|
|
||||||
SET name = ?1, description = ?2, updated_at = ?3
|
|
||||||
WHERE id = ?4",
|
|
||||||
params![name, description, now, id],
|
|
||||||
)?;
|
|
||||||
|
|
||||||
Ok(Workspace {
|
|
||||||
id: id.to_string(),
|
|
||||||
name: name.to_string(),
|
|
||||||
description: description.map(String::from),
|
|
||||||
created_at: self.get_workspace(id)?.map(|w| w.created_at).unwrap_or(now),
|
|
||||||
updated_at: now,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn delete_workspace(&mut self, id: &str) -> Result<()> {
|
|
||||||
// Start a transaction to ensure data consistency
|
|
||||||
let tx = self.conn.get_mut().unwrap().transaction()?;
|
|
||||||
|
|
||||||
// Delete variables
|
|
||||||
tx.execute(
|
|
||||||
"DELETE FROM variables WHERE workspace_id = ?",
|
|
||||||
params![id],
|
|
||||||
)?;
|
|
||||||
|
|
||||||
// Delete requests from collections in this workspace
|
|
||||||
tx.execute(
|
|
||||||
"DELETE FROM requests
|
|
||||||
WHERE collection_id IN (
|
|
||||||
SELECT id FROM collections WHERE workspace_id = ?
|
|
||||||
)",
|
|
||||||
params![id],
|
|
||||||
)?;
|
|
||||||
|
|
||||||
// Delete collections
|
|
||||||
tx.execute(
|
|
||||||
"DELETE FROM collections WHERE workspace_id = ?",
|
|
||||||
params![id],
|
|
||||||
)?;
|
|
||||||
|
|
||||||
// Finally delete the workspace
|
|
||||||
tx.execute("DELETE FROM workspaces WHERE id = ?", params![id])?;
|
|
||||||
|
|
||||||
// Commit the transaction
|
|
||||||
tx.commit()?;
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -12,7 +12,7 @@
|
|||||||
"app": {
|
"app": {
|
||||||
"windows": [
|
"windows": [
|
||||||
{
|
{
|
||||||
"title": "Resona",
|
"title": "resona",
|
||||||
"width": 800,
|
"width": 800,
|
||||||
"height": 600
|
"height": 600
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>
|
<script lang="ts">
|
||||||
import "../app.css";
|
import './layout.css';
|
||||||
|
|
||||||
|
let { children } = $props();
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<slot />
|
{@render children()}
|
||||||
|
|||||||
@@ -1,75 +1,156 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { workspaces } from '$lib/stores/workspace';
|
import { invoke } from "@tauri-apps/api/core";
|
||||||
import { onMount } from 'svelte';
|
|
||||||
import WorkspaceCard from '$lib/components/WorkspaceCard.svelte';
|
|
||||||
import WorkspaceModal from '$lib/components/modals/WorkspaceModal.svelte';
|
|
||||||
|
|
||||||
let showCreateModal = false;
|
let name = $state("");
|
||||||
let loading = true;
|
let greetMsg = $state("");
|
||||||
let error: string | null = null;
|
|
||||||
|
|
||||||
onMount(async () => {
|
async function greet(event: Event) {
|
||||||
try {
|
event.preventDefault();
|
||||||
await workspaces.loadWorkspaces();
|
// Learn more about Tauri commands at https://tauri.app/develop/calling-rust/
|
||||||
} catch (err) {
|
greetMsg = await invoke("greet", { name });
|
||||||
console.error('Failed to load workspaces:', err);
|
}
|
||||||
error = 'Failed to load workspaces';
|
|
||||||
} finally {
|
|
||||||
loading = false;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="container mx-auto px-6 py-8 max-w-7xl">
|
<main class="container">
|
||||||
<div class="flex justify-between items-center mb-8">
|
<h1>Welcome to Tauri + Svelte</h1>
|
||||||
<div>
|
|
||||||
<h1 class="text-2xl font-bold">Workspaces</h1>
|
<div class="row">
|
||||||
<p class="text-base-content/70">Manage your API collections and environments</p>
|
<a href="https://vitejs.dev" target="_blank">
|
||||||
</div>
|
<img src="/vite.svg" class="logo vite" alt="Vite Logo" />
|
||||||
<button
|
</a>
|
||||||
class="btn btn-primary"
|
<a href="https://tauri.app" target="_blank">
|
||||||
on:click={() => showCreateModal = true}
|
<img src="/tauri.svg" class="logo tauri" alt="Tauri Logo" />
|
||||||
>
|
</a>
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 mr-2" viewBox="0 0 20 20" fill="currentColor">
|
<a href="https://kit.svelte.dev" target="_blank">
|
||||||
<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" />
|
<img src="/svelte.svg" class="logo svelte-kit" alt="SvelteKit Logo" />
|
||||||
</svg>
|
</a>
|
||||||
New Workspace
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
|
<p>Click on the Tauri, Vite, and SvelteKit logos to learn more.</p>
|
||||||
|
|
||||||
{#if loading}
|
<form class="row" onsubmit={greet}>
|
||||||
<div class="flex justify-center py-16">
|
<input id="greet-input" placeholder="Enter a name..." bind:value={name} />
|
||||||
<span class="loading loading-spinner loading-lg"></span>
|
<button type="submit">Greet</button>
|
||||||
</div>
|
</form>
|
||||||
{:else if error}
|
<p>{greetMsg}</p>
|
||||||
<div class="alert alert-error">
|
</main>
|
||||||
<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>
|
|
||||||
|
|
||||||
<WorkspaceModal
|
<style>
|
||||||
show={showCreateModal}
|
.logo.vite:hover {
|
||||||
on:close={() => showCreateModal = false}
|
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;
|
|
||||||
}
|
|
||||||
@@ -10,7 +10,6 @@ const config = {
|
|||||||
kit: {
|
kit: {
|
||||||
adapter: adapter(),
|
adapter: adapter(),
|
||||||
},
|
},
|
||||||
preprocess: vitePreprocess()
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export default config;
|
export default config;
|
||||||
|
|||||||
@@ -1,14 +0,0 @@
|
|||||||
import daisyui from "daisyui";
|
|
||||||
|
|
||||||
/** @type {import('tailwindcss').Config} */
|
|
||||||
|
|
||||||
export default {
|
|
||||||
content: ['./src/**/*.{html,js,svelte,ts}'],
|
|
||||||
theme: {
|
|
||||||
extend: {},
|
|
||||||
},
|
|
||||||
plugins: [daisyui],
|
|
||||||
daisyui: {
|
|
||||||
themes: ["light", "dark"],
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -9,8 +9,12 @@
|
|||||||
"skipLibCheck": true,
|
"skipLibCheck": true,
|
||||||
"sourceMap": true,
|
"sourceMap": true,
|
||||||
"strict": true,
|
"strict": true,
|
||||||
"moduleResolution": "bundler"
|
"moduleResolution": "bundler",
|
||||||
}
|
"paths": {
|
||||||
|
"$lib": ["./src/lib"],
|
||||||
|
"$lib/*": ["./src/lib/*"]
|
||||||
|
}
|
||||||
|
},
|
||||||
// Path aliases are handled by https://kit.svelte.dev/docs/configuration#alias
|
// Path aliases are handled by https://kit.svelte.dev/docs/configuration#alias
|
||||||
// except $lib which is handled by https://kit.svelte.dev/docs/configuration#files
|
// except $lib which is handled by https://kit.svelte.dev/docs/configuration#files
|
||||||
//
|
//
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import tailwindcss from "@tailwindcss/vite";
|
||||||
import { defineConfig } from "vite";
|
import { defineConfig } from "vite";
|
||||||
import { sveltekit } from "@sveltejs/kit/vite";
|
import { sveltekit } from "@sveltejs/kit/vite";
|
||||||
|
|
||||||
@@ -6,8 +7,7 @@ const host = process.env.TAURI_DEV_HOST;
|
|||||||
|
|
||||||
// https://vitejs.dev/config/
|
// https://vitejs.dev/config/
|
||||||
export default defineConfig(async () => ({
|
export default defineConfig(async () => ({
|
||||||
plugins: [sveltekit()],
|
plugins: [tailwindcss(), sveltekit()],
|
||||||
|
|
||||||
// Vite options tailored for Tauri development and only applied in `tauri dev` or `tauri build`
|
// Vite options tailored for Tauri development and only applied in `tauri dev` or `tauri build`
|
||||||
//
|
//
|
||||||
// 1. prevent vite from obscuring rust errors
|
// 1. prevent vite from obscuring rust errors
|
||||||
@@ -17,16 +17,8 @@ export default defineConfig(async () => ({
|
|||||||
port: 1420,
|
port: 1420,
|
||||||
strictPort: true,
|
strictPort: true,
|
||||||
host: host || false,
|
host: host || false,
|
||||||
hmr: host
|
hmr: host ? { protocol: "ws", host, port: 1421 } : undefined,
|
||||||
? {
|
watch: { // 3. tell vite to ignore watching `src-tauri`
|
||||||
protocol: "ws",
|
ignored: ["**/src-tauri/**"] }
|
||||||
host,
|
}
|
||||||
port: 1421,
|
|
||||||
}
|
|
||||||
: undefined,
|
|
||||||
watch: {
|
|
||||||
// 3. tell vite to ignore watching `src-tauri`
|
|
||||||
ignored: ["**/src-tauri/**"],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}));
|
}));
|
||||||
|
|||||||
@@ -1,15 +0,0 @@
|
|||||||
import { defineConfig } from "vite";
|
|
||||||
import { sveltekit } from "@sveltejs/kit/vite";
|
|
||||||
|
|
||||||
export default defineConfig(async () => ({
|
|
||||||
plugins: [sveltekit()],
|
|
||||||
|
|
||||||
clearScreen: false,
|
|
||||||
|
|
||||||
server:{
|
|
||||||
port: 1420,
|
|
||||||
strictPort: true
|
|
||||||
},
|
|
||||||
|
|
||||||
envPrefix: ["VITE_", "TAURI_"],
|
|
||||||
}))
|
|
||||||
Reference in New Issue
Block a user