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
|
||||
vite.config.js.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)
|
||||
- 📁 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
|
||||
[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).
|
||||
|
||||
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",
|
||||
"dependencies": {
|
||||
"@tauri-apps/api": "^2.0.3",
|
||||
"@tauri-apps/plugin-shell": ">=2.0.0",
|
||||
"autoprefixer": "^10.4.20",
|
||||
"daisyui": "^4.12.14",
|
||||
"tailwind": "^4.0.0",
|
||||
"tailwindcss": "^3.4.14"
|
||||
"@tauri-apps/api": "^2",
|
||||
"@tauri-apps/plugin-opener": "^2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@sveltejs/adapter-static": "^3.0.1",
|
||||
"@sveltejs/kit": "^2.0.0",
|
||||
"@sveltejs/vite-plugin-svelte": "^4.0.0",
|
||||
"svelte": "^5.1.9",
|
||||
"svelte-check": "^4.0.5",
|
||||
"tslib": "^2.4.1",
|
||||
"typescript": "^5.0.0",
|
||||
"vite": "^5.0.3",
|
||||
"@tauri-apps/cli": ">=2.0.0"
|
||||
},
|
||||
"trustedDependencies": [
|
||||
"core-js",
|
||||
"svelte-preprocess"
|
||||
]
|
||||
"@lucide/svelte": "^0.554.0",
|
||||
"@sveltejs/adapter-static": "^3.0.6",
|
||||
"@sveltejs/kit": "^2.9.0",
|
||||
"@sveltejs/vite-plugin-svelte": "^5.0.0",
|
||||
"@tailwindcss/forms": "^0.5.10",
|
||||
"@tailwindcss/typography": "^0.5.19",
|
||||
"@tailwindcss/vite": "^4.1.17",
|
||||
"@tauri-apps/cli": "^2",
|
||||
"clsx": "^2.1.1",
|
||||
"svelte": "^5.0.0",
|
||||
"svelte-check": "^4.0.0",
|
||||
"tailwind-merge": "^3.3.1",
|
||||
"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"
|
||||
description = "A Tauri App"
|
||||
authors = ["you"]
|
||||
license = ""
|
||||
repository = ""
|
||||
default-run = "resona"
|
||||
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]
|
||||
tauri-build = { version = "2.0.2", features = [] }
|
||||
tauri-build = { version = "2", features = [] }
|
||||
|
||||
[dependencies]
|
||||
serde_json = "1.0"
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
tauri = { version = "2.0", features = [] }
|
||||
reqwest = { version = "0.12", features = ["json"] }
|
||||
tokio = { version = "1.36", features = ["full"] }
|
||||
uuid = { version = "1.7", features = ["v4", "serde"] }
|
||||
rusqlite = "0.32"
|
||||
tauri = { version = "2", features = [] }
|
||||
tauri-plugin-opener = "2"
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
serde_json = "1"
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
"description": "Capability for the main window",
|
||||
"windows": ["main"],
|
||||
"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)]
|
||||
pub fn run() {
|
||||
tauri::Builder::default()
|
||||
.plugin(tauri_plugin_opener::init())
|
||||
.invoke_handler(tauri::generate_handler![greet])
|
||||
.run(tauri::generate_context!())
|
||||
.expect("error while running tauri application");
|
||||
|
||||
@@ -1,27 +1,6 @@
|
||||
mod models;
|
||||
mod storage;
|
||||
mod commands;
|
||||
mod client;
|
||||
|
||||
use commands::handlers as h;
|
||||
use storage::Storage;
|
||||
use tauri::Manager;
|
||||
use std::sync::Mutex;
|
||||
// Prevents additional console window on Windows in release, DO NOT REMOVE!!
|
||||
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
|
||||
|
||||
fn main() {
|
||||
let handlers = h();
|
||||
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");
|
||||
resona_lib::run()
|
||||
}
|
||||
@@ -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": {
|
||||
"windows": [
|
||||
{
|
||||
"title": "Resona",
|
||||
"title": "resona",
|
||||
"width": 800,
|
||||
"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>
|
||||
import "../app.css";
|
||||
<script lang="ts">
|
||||
import './layout.css';
|
||||
|
||||
let { children } = $props();
|
||||
</script>
|
||||
|
||||
<slot />
|
||||
{@render children()}
|
||||
|
||||
@@ -1,75 +1,156 @@
|
||||
<script lang="ts">
|
||||
import { workspaces } from '$lib/stores/workspace';
|
||||
import { onMount } from 'svelte';
|
||||
import WorkspaceCard from '$lib/components/WorkspaceCard.svelte';
|
||||
import WorkspaceModal from '$lib/components/modals/WorkspaceModal.svelte';
|
||||
import { invoke } from "@tauri-apps/api/core";
|
||||
|
||||
let showCreateModal = false;
|
||||
let loading = true;
|
||||
let error: string | null = null;
|
||||
let name = $state("");
|
||||
let greetMsg = $state("");
|
||||
|
||||
onMount(async () => {
|
||||
try {
|
||||
await workspaces.loadWorkspaces();
|
||||
} catch (err) {
|
||||
console.error('Failed to load workspaces:', err);
|
||||
error = 'Failed to load workspaces';
|
||||
} finally {
|
||||
loading = false;
|
||||
async function greet(event: Event) {
|
||||
event.preventDefault();
|
||||
// Learn more about Tauri commands at https://tauri.app/develop/calling-rust/
|
||||
greetMsg = await invoke("greet", { name });
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<div class="container mx-auto px-6 py-8 max-w-7xl">
|
||||
<div class="flex justify-between items-center mb-8">
|
||||
<div>
|
||||
<h1 class="text-2xl font-bold">Workspaces</h1>
|
||||
<p class="text-base-content/70">Manage your API collections and environments</p>
|
||||
</div>
|
||||
<button
|
||||
class="btn btn-primary"
|
||||
on:click={() => showCreateModal = true}
|
||||
>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 mr-2" viewBox="0 0 20 20" fill="currentColor">
|
||||
<path fill-rule="evenodd" d="M10 3a1 1 0 011 1v5h5a1 1 0 110 2h-5v5a1 1 0 11-2 0v-5H4a1 1 0 110-2h5V4a1 1 0 011-1z" clip-rule="evenodd" />
|
||||
</svg>
|
||||
New Workspace
|
||||
</button>
|
||||
</div>
|
||||
<main class="container">
|
||||
<h1>Welcome to Tauri + Svelte</h1>
|
||||
|
||||
{#if loading}
|
||||
<div class="flex justify-center py-16">
|
||||
<span class="loading loading-spinner loading-lg"></span>
|
||||
<div class="row">
|
||||
<a href="https://vitejs.dev" target="_blank">
|
||||
<img src="/vite.svg" class="logo vite" alt="Vite Logo" />
|
||||
</a>
|
||||
<a href="https://tauri.app" target="_blank">
|
||||
<img src="/tauri.svg" class="logo tauri" alt="Tauri Logo" />
|
||||
</a>
|
||||
<a href="https://kit.svelte.dev" target="_blank">
|
||||
<img src="/svelte.svg" class="logo svelte-kit" alt="SvelteKit Logo" />
|
||||
</a>
|
||||
</div>
|
||||
{:else if error}
|
||||
<div class="alert alert-error">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="stroke-current shrink-0 h-6 w-6" fill="none" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 14l2-2m0 0l2-2m-2 2l-2-2m2 2l2 2m7-2a9 9 0 11-18 0 9 9 0 0118 0z" /></svg>
|
||||
<span>{error}</span>
|
||||
</div>
|
||||
{:else if $workspaces.length === 0}
|
||||
<div class="flex flex-col items-center justify-center py-16 text-center">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-16 w-16 text-base-content/20" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M20 7l-8-4-8 4m16 0l-8 4m8-4v10l-8 4m0-10L4 7m8 4v10M4 7v10l8 4" />
|
||||
</svg>
|
||||
<h3 class="mt-4 text-lg font-medium">No workspaces yet</h3>
|
||||
<p class="mt-1 text-base-content/70">Create a workspace to get started with your API collections</p>
|
||||
<button
|
||||
class="btn btn-primary mt-4"
|
||||
on:click={() => showCreateModal = true}
|
||||
>
|
||||
Create Your First Workspace
|
||||
</button>
|
||||
</div>
|
||||
{:else}
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8">
|
||||
{#each $workspaces as workspace}
|
||||
<WorkspaceCard {workspace} />
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
<p>Click on the Tauri, Vite, and SvelteKit logos to learn more.</p>
|
||||
|
||||
<WorkspaceModal
|
||||
show={showCreateModal}
|
||||
on:close={() => showCreateModal = false}
|
||||
/>
|
||||
<form class="row" onsubmit={greet}>
|
||||
<input id="greet-input" placeholder="Enter a name..." bind:value={name} />
|
||||
<button type="submit">Greet</button>
|
||||
</form>
|
||||
<p>{greetMsg}</p>
|
||||
</main>
|
||||
|
||||
<style>
|
||||
.logo.vite:hover {
|
||||
filter: drop-shadow(0 0 2em #747bff);
|
||||
}
|
||||
|
||||
.logo.svelte-kit:hover {
|
||||
filter: drop-shadow(0 0 2em #ff3e00);
|
||||
}
|
||||
|
||||
:root {
|
||||
font-family: Inter, Avenir, Helvetica, Arial, sans-serif;
|
||||
font-size: 16px;
|
||||
line-height: 24px;
|
||||
font-weight: 400;
|
||||
|
||||
color: #0f0f0f;
|
||||
background-color: #f6f6f6;
|
||||
|
||||
font-synthesis: none;
|
||||
text-rendering: optimizeLegibility;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
-webkit-text-size-adjust: 100%;
|
||||
}
|
||||
|
||||
.container {
|
||||
margin: 0;
|
||||
padding-top: 10vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.logo {
|
||||
height: 6em;
|
||||
padding: 1.5em;
|
||||
will-change: filter;
|
||||
transition: 0.75s;
|
||||
}
|
||||
|
||||
.logo.tauri:hover {
|
||||
filter: drop-shadow(0 0 2em #24c8db);
|
||||
}
|
||||
|
||||
.row {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
a {
|
||||
font-weight: 500;
|
||||
color: #646cff;
|
||||
text-decoration: inherit;
|
||||
}
|
||||
|
||||
a:hover {
|
||||
color: #535bf2;
|
||||
}
|
||||
|
||||
h1 {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
input,
|
||||
button {
|
||||
border-radius: 8px;
|
||||
border: 1px solid transparent;
|
||||
padding: 0.6em 1.2em;
|
||||
font-size: 1em;
|
||||
font-weight: 500;
|
||||
font-family: inherit;
|
||||
color: #0f0f0f;
|
||||
background-color: #ffffff;
|
||||
transition: border-color 0.25s;
|
||||
box-shadow: 0 2px 2px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
button {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
button:hover {
|
||||
border-color: #396cd8;
|
||||
}
|
||||
button:active {
|
||||
border-color: #396cd8;
|
||||
background-color: #e8e8e8;
|
||||
}
|
||||
|
||||
input,
|
||||
button {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
#greet-input {
|
||||
margin-right: 5px;
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
:root {
|
||||
color: #f6f6f6;
|
||||
background-color: #2f2f2f;
|
||||
}
|
||||
|
||||
a:hover {
|
||||
color: #24c8db;
|
||||
}
|
||||
|
||||
input,
|
||||
button {
|
||||
color: #ffffff;
|
||||
background-color: #0f0f0f98;
|
||||
}
|
||||
button:active {
|
||||
background-color: #0f0f0f69;
|
||||
}
|
||||
}
|
||||
|
||||
</style>
|
||||
|
||||
121
src/routes/layout.css
Normal file
121
src/routes/layout.css
Normal file
@@ -0,0 +1,121 @@
|
||||
@import "tailwindcss";
|
||||
|
||||
@import "tw-animate-css";
|
||||
|
||||
@custom-variant dark (&:is(.dark *));
|
||||
|
||||
:root {
|
||||
--radius: 0.625rem;
|
||||
--background: oklch(1 0 0);
|
||||
--foreground: oklch(0.129 0.042 264.695);
|
||||
--card: oklch(1 0 0);
|
||||
--card-foreground: oklch(0.129 0.042 264.695);
|
||||
--popover: oklch(1 0 0);
|
||||
--popover-foreground: oklch(0.129 0.042 264.695);
|
||||
--primary: oklch(0.208 0.042 265.755);
|
||||
--primary-foreground: oklch(0.984 0.003 247.858);
|
||||
--secondary: oklch(0.968 0.007 247.896);
|
||||
--secondary-foreground: oklch(0.208 0.042 265.755);
|
||||
--muted: oklch(0.968 0.007 247.896);
|
||||
--muted-foreground: oklch(0.554 0.046 257.417);
|
||||
--accent: oklch(0.968 0.007 247.896);
|
||||
--accent-foreground: oklch(0.208 0.042 265.755);
|
||||
--destructive: oklch(0.577 0.245 27.325);
|
||||
--border: oklch(0.929 0.013 255.508);
|
||||
--input: oklch(0.929 0.013 255.508);
|
||||
--ring: oklch(0.704 0.04 256.788);
|
||||
--chart-1: oklch(0.646 0.222 41.116);
|
||||
--chart-2: oklch(0.6 0.118 184.704);
|
||||
--chart-3: oklch(0.398 0.07 227.392);
|
||||
--chart-4: oklch(0.828 0.189 84.429);
|
||||
--chart-5: oklch(0.769 0.188 70.08);
|
||||
--sidebar: oklch(0.984 0.003 247.858);
|
||||
--sidebar-foreground: oklch(0.129 0.042 264.695);
|
||||
--sidebar-primary: oklch(0.208 0.042 265.755);
|
||||
--sidebar-primary-foreground: oklch(0.984 0.003 247.858);
|
||||
--sidebar-accent: oklch(0.968 0.007 247.896);
|
||||
--sidebar-accent-foreground: oklch(0.208 0.042 265.755);
|
||||
--sidebar-border: oklch(0.929 0.013 255.508);
|
||||
--sidebar-ring: oklch(0.704 0.04 256.788);
|
||||
}
|
||||
|
||||
.dark {
|
||||
--background: oklch(0.129 0.042 264.695);
|
||||
--foreground: oklch(0.984 0.003 247.858);
|
||||
--card: oklch(0.208 0.042 265.755);
|
||||
--card-foreground: oklch(0.984 0.003 247.858);
|
||||
--popover: oklch(0.208 0.042 265.755);
|
||||
--popover-foreground: oklch(0.984 0.003 247.858);
|
||||
--primary: oklch(0.929 0.013 255.508);
|
||||
--primary-foreground: oklch(0.208 0.042 265.755);
|
||||
--secondary: oklch(0.279 0.041 260.031);
|
||||
--secondary-foreground: oklch(0.984 0.003 247.858);
|
||||
--muted: oklch(0.279 0.041 260.031);
|
||||
--muted-foreground: oklch(0.704 0.04 256.788);
|
||||
--accent: oklch(0.279 0.041 260.031);
|
||||
--accent-foreground: oklch(0.984 0.003 247.858);
|
||||
--destructive: oklch(0.704 0.191 22.216);
|
||||
--border: oklch(1 0 0 / 10%);
|
||||
--input: oklch(1 0 0 / 15%);
|
||||
--ring: oklch(0.551 0.027 264.364);
|
||||
--chart-1: oklch(0.488 0.243 264.376);
|
||||
--chart-2: oklch(0.696 0.17 162.48);
|
||||
--chart-3: oklch(0.769 0.188 70.08);
|
||||
--chart-4: oklch(0.627 0.265 303.9);
|
||||
--chart-5: oklch(0.645 0.246 16.439);
|
||||
--sidebar: oklch(0.208 0.042 265.755);
|
||||
--sidebar-foreground: oklch(0.984 0.003 247.858);
|
||||
--sidebar-primary: oklch(0.488 0.243 264.376);
|
||||
--sidebar-primary-foreground: oklch(0.984 0.003 247.858);
|
||||
--sidebar-accent: oklch(0.279 0.041 260.031);
|
||||
--sidebar-accent-foreground: oklch(0.984 0.003 247.858);
|
||||
--sidebar-border: oklch(1 0 0 / 10%);
|
||||
--sidebar-ring: oklch(0.551 0.027 264.364);
|
||||
}
|
||||
|
||||
@theme inline {
|
||||
--radius-sm: calc(var(--radius) - 4px);
|
||||
--radius-md: calc(var(--radius) - 2px);
|
||||
--radius-lg: var(--radius);
|
||||
--radius-xl: calc(var(--radius) + 4px);
|
||||
--color-background: var(--background);
|
||||
--color-foreground: var(--foreground);
|
||||
--color-card: var(--card);
|
||||
--color-card-foreground: var(--card-foreground);
|
||||
--color-popover: var(--popover);
|
||||
--color-popover-foreground: var(--popover-foreground);
|
||||
--color-primary: var(--primary);
|
||||
--color-primary-foreground: var(--primary-foreground);
|
||||
--color-secondary: var(--secondary);
|
||||
--color-secondary-foreground: var(--secondary-foreground);
|
||||
--color-muted: var(--muted);
|
||||
--color-muted-foreground: var(--muted-foreground);
|
||||
--color-accent: var(--accent);
|
||||
--color-accent-foreground: var(--accent-foreground);
|
||||
--color-destructive: var(--destructive);
|
||||
--color-border: var(--border);
|
||||
--color-input: var(--input);
|
||||
--color-ring: var(--ring);
|
||||
--color-chart-1: var(--chart-1);
|
||||
--color-chart-2: var(--chart-2);
|
||||
--color-chart-3: var(--chart-3);
|
||||
--color-chart-4: var(--chart-4);
|
||||
--color-chart-5: var(--chart-5);
|
||||
--color-sidebar: var(--sidebar);
|
||||
--color-sidebar-foreground: var(--sidebar-foreground);
|
||||
--color-sidebar-primary: var(--sidebar-primary);
|
||||
--color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
|
||||
--color-sidebar-accent: var(--sidebar-accent);
|
||||
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
|
||||
--color-sidebar-border: var(--sidebar-border);
|
||||
--color-sidebar-ring: var(--sidebar-ring);
|
||||
}
|
||||
|
||||
@layer base {
|
||||
* {
|
||||
@apply border-border outline-ring/50;
|
||||
}
|
||||
body {
|
||||
@apply bg-background text-foreground;
|
||||
}
|
||||
}
|
||||
@@ -1,49 +0,0 @@
|
||||
<script lang="ts">
|
||||
import { page } from '$app/stores';
|
||||
import { workspaces, activeWorkspace } from '$lib/stores/workspace';
|
||||
import { onMount } from 'svelte';
|
||||
import Sidebar from '$lib/components/Sidebar.svelte';
|
||||
import WorkspaceToolbar from '$lib/components/WorkspaceToolbar.svelte';
|
||||
|
||||
let drawerOpen = true;
|
||||
|
||||
onMount(async () => {
|
||||
const workspace = await workspaces.getWorkspace($page.params.id);
|
||||
if (workspace) {
|
||||
activeWorkspace.set(workspace);
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<div class="drawer lg:drawer-open">
|
||||
<input
|
||||
id="drawer"
|
||||
type="checkbox"
|
||||
class="drawer-toggle"
|
||||
bind:checked={drawerOpen}
|
||||
/>
|
||||
|
||||
<div class="drawer-content flex flex-col">
|
||||
{#if drawerOpen}
|
||||
<button
|
||||
class="lg:hidden fixed bottom-4 right-4 btn btn-circle btn-primary z-50"
|
||||
on:click={() => drawerOpen = false}
|
||||
>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
|
||||
</svg>
|
||||
</button>
|
||||
{/if}
|
||||
<WorkspaceToolbar />
|
||||
<slot />
|
||||
</div>
|
||||
|
||||
<div class="drawer-side">
|
||||
<label
|
||||
for="drawer"
|
||||
class="drawer-overlay"
|
||||
on:click={() => drawerOpen = false}
|
||||
></label>
|
||||
<Sidebar />
|
||||
</div>
|
||||
</div>
|
||||
@@ -1,23 +0,0 @@
|
||||
<script lang="ts">
|
||||
import { page } from '$app/stores';
|
||||
import { onMount } from 'svelte';
|
||||
import { workspaces, activeWorkspace } from '$lib/stores/workspace';
|
||||
import RequestPanel from '$lib/components/RequestPanel.svelte';
|
||||
import ResponsePanel from '$lib/components/ResponsePanel.svelte';
|
||||
|
||||
onMount(async () => {
|
||||
try {
|
||||
const workspace = await workspaces.getWorkspace($page.params.id);
|
||||
if (workspace) {
|
||||
activeWorkspace.set(workspace);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to load workspace:', error);
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<div class="p-4 space-y-4">
|
||||
<RequestPanel />
|
||||
<ResponsePanel />
|
||||
</div>
|
||||
@@ -1,14 +0,0 @@
|
||||
import { writable } from 'svelte/store';
|
||||
import type { Collection, Variable, Request as ApiRequest } from '../types';
|
||||
|
||||
export const collections = writable<Collection[]>([]);
|
||||
export const variables = writable<Variable[]>([]);
|
||||
export const activeCollection = writable<Collection | null>(null);
|
||||
export const activeRequest = writable<ApiRequest | null>(null);
|
||||
export const currentResponse = writable<{
|
||||
status: number;
|
||||
statusText: string;
|
||||
headers: Record<string, string>;
|
||||
body: string;
|
||||
time: number;
|
||||
} | null>(null);
|
||||
@@ -1,58 +0,0 @@
|
||||
export interface Variable {
|
||||
id: string;
|
||||
name: string;
|
||||
value: string;
|
||||
description?: string;
|
||||
}
|
||||
|
||||
export interface Collection {
|
||||
id: string;
|
||||
workspace_id: string;
|
||||
name: string;
|
||||
description?: string;
|
||||
created_at: number;
|
||||
updated_at: number;
|
||||
requests: Request[];
|
||||
}
|
||||
|
||||
export interface Request {
|
||||
id: string;
|
||||
name: string;
|
||||
url: string;
|
||||
method: 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH';
|
||||
headers: Header[];
|
||||
queryParams: QueryParam[];
|
||||
body?: string;
|
||||
}
|
||||
|
||||
export interface Header {
|
||||
id: string;
|
||||
key: string;
|
||||
value: string;
|
||||
enabled: boolean;
|
||||
}
|
||||
|
||||
export interface QueryParam {
|
||||
id: string;
|
||||
key: string;
|
||||
value: string;
|
||||
enabled: boolean;
|
||||
}
|
||||
|
||||
export interface RequestResponse {
|
||||
status: number;
|
||||
statusText: string;
|
||||
headers: Record<string, string>;
|
||||
body: any;
|
||||
time: number;
|
||||
}
|
||||
|
||||
export interface Workspace {
|
||||
id: string;
|
||||
name: string;
|
||||
variables?: Variable[];
|
||||
collections?: Collection[];
|
||||
description?: string;
|
||||
created_at: number;
|
||||
updated_at: number;
|
||||
}
|
||||
@@ -10,7 +10,6 @@ const config = {
|
||||
kit: {
|
||||
adapter: adapter(),
|
||||
},
|
||||
preprocess: vitePreprocess()
|
||||
};
|
||||
|
||||
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,
|
||||
"sourceMap": 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
|
||||
// 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 { sveltekit } from "@sveltejs/kit/vite";
|
||||
|
||||
@@ -6,8 +7,7 @@ const host = process.env.TAURI_DEV_HOST;
|
||||
|
||||
// https://vitejs.dev/config/
|
||||
export default defineConfig(async () => ({
|
||||
plugins: [sveltekit()],
|
||||
|
||||
plugins: [tailwindcss(), sveltekit()],
|
||||
// Vite options tailored for Tauri development and only applied in `tauri dev` or `tauri build`
|
||||
//
|
||||
// 1. prevent vite from obscuring rust errors
|
||||
@@ -17,16 +17,8 @@ export default defineConfig(async () => ({
|
||||
port: 1420,
|
||||
strictPort: true,
|
||||
host: host || false,
|
||||
hmr: host
|
||||
? {
|
||||
protocol: "ws",
|
||||
host,
|
||||
port: 1421,
|
||||
hmr: host ? { protocol: "ws", host, port: 1421 } : undefined,
|
||||
watch: { // 3. tell vite to ignore watching `src-tauri`
|
||||
ignored: ["**/src-tauri/**"] }
|
||||
}
|
||||
: 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