list warrens + explore nested folders
This commit is contained in:
@@ -1,5 +1,6 @@
|
|||||||
.gitignore
|
.gitignore
|
||||||
|
|
||||||
|
frontend/.env
|
||||||
frontend/dist
|
frontend/dist
|
||||||
frontend/.nuxt
|
frontend/.nuxt
|
||||||
frontend/.output
|
frontend/.output
|
||||||
|
|||||||
@@ -16,8 +16,10 @@ RUN npm run generate
|
|||||||
FROM rust:bookworm AS backend-builder
|
FROM rust:bookworm AS backend-builder
|
||||||
WORKDIR /usr/src/warren
|
WORKDIR /usr/src/warren
|
||||||
|
|
||||||
|
COPY backend/Cargo.toml backend/Cargo.lock ./
|
||||||
|
RUN mkdir src && echo "fn main() {}" > src/main.rs && cargo build --release
|
||||||
COPY backend/ .
|
COPY backend/ .
|
||||||
RUN cargo build --release
|
RUN touch src/main.rs && cargo build --release
|
||||||
|
|
||||||
|
|
||||||
FROM debian:bookworm
|
FROM debian:bookworm
|
||||||
|
|||||||
2
backend/.gitignore
vendored
2
backend/.gitignore
vendored
@@ -1 +1,3 @@
|
|||||||
target
|
target
|
||||||
|
serve
|
||||||
|
.env
|
||||||
|
|||||||
1339
backend/Cargo.lock
generated
1339
backend/Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
@@ -8,5 +8,10 @@ axum = "0.8.4"
|
|||||||
dotenv = "0.15.0"
|
dotenv = "0.15.0"
|
||||||
env_logger = "0.11.8"
|
env_logger = "0.11.8"
|
||||||
log = "0.4.27"
|
log = "0.4.27"
|
||||||
|
serde = { version = "1.0.219", features = ["derive"] }
|
||||||
|
serde_json = "1.0.140"
|
||||||
|
sqlx = { version = "0.8.6", features = ["postgres", "runtime-tokio", "uuid"] }
|
||||||
|
thiserror = "2.0.12"
|
||||||
tokio = { version = "1.46.1", features = ["full"] }
|
tokio = { version = "1.46.1", features = ["full"] }
|
||||||
tower-http = { version = "0.6.6", features = ["fs"] }
|
tower-http = { version = "0.6.6", features = ["cors", "fs"] }
|
||||||
|
uuid = { version = "1.17.0", features = ["serde"] }
|
||||||
|
|||||||
@@ -0,0 +1,7 @@
|
|||||||
|
CREATE TABLE warrens (
|
||||||
|
id UUID PRIMARY KEY DEFAULT GEN_RANDOM_UUID(),
|
||||||
|
path VARCHAR NOT NULL,
|
||||||
|
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX idx_warrens_path ON warrens(path);
|
||||||
1
backend/migrations/20250712024344_add_warren_name.sql
Normal file
1
backend/migrations/20250712024344_add_warren_name.sql
Normal file
@@ -0,0 +1 @@
|
|||||||
|
ALTER TABLE warrens ADD COLUMN name VARCHAR NOT NULL;
|
||||||
10
backend/src/api/mod.rs
Normal file
10
backend/src/api/mod.rs
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
use crate::server::Router;
|
||||||
|
|
||||||
|
mod state;
|
||||||
|
mod warrens;
|
||||||
|
|
||||||
|
pub use state::AppState;
|
||||||
|
|
||||||
|
pub(super) fn router() -> Router {
|
||||||
|
Router::new().nest("/warrens", warrens::router())
|
||||||
|
}
|
||||||
21
backend/src/api/state.rs
Normal file
21
backend/src/api/state.rs
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
use sqlx::{Pool, Postgres};
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct AppState {
|
||||||
|
pool: Pool<Postgres>,
|
||||||
|
serve_dir: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AppState {
|
||||||
|
pub fn new(pool: Pool<Postgres>, serve_dir: String) -> Self {
|
||||||
|
Self { pool, serve_dir }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn pool(&self) -> &Pool<Postgres> {
|
||||||
|
&self.pool
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn serve_dir(&self) -> &str {
|
||||||
|
&self.serve_dir
|
||||||
|
}
|
||||||
|
}
|
||||||
27
backend/src/api/warrens/get_warren_path.rs
Normal file
27
backend/src/api/warrens/get_warren_path.rs
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
use axum::Json;
|
||||||
|
use axum::extract::{Path, State};
|
||||||
|
use serde::Deserialize;
|
||||||
|
use uuid::Uuid;
|
||||||
|
|
||||||
|
use crate::Result;
|
||||||
|
use crate::api::AppState;
|
||||||
|
use crate::warrens::Warren;
|
||||||
|
|
||||||
|
use crate::fs::DirectoryEntry;
|
||||||
|
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
pub(super) struct WarrenRequestPath {
|
||||||
|
warren_id: Uuid,
|
||||||
|
rest: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) async fn route(
|
||||||
|
State(state): State<AppState>,
|
||||||
|
Path(WarrenRequestPath { warren_id, rest }): Path<WarrenRequestPath>,
|
||||||
|
) -> Result<Json<Vec<DirectoryEntry>>> {
|
||||||
|
let warren = Warren::get(state.pool(), &warren_id).await?;
|
||||||
|
|
||||||
|
let entries = warren.read_path(state.serve_dir(), rest).await?;
|
||||||
|
|
||||||
|
Ok(Json(entries))
|
||||||
|
}
|
||||||
9
backend/src/api/warrens/list_warrens.rs
Normal file
9
backend/src/api/warrens/list_warrens.rs
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
use axum::{Json, extract::State};
|
||||||
|
|
||||||
|
use crate::{Result, api::AppState, warrens::Warren};
|
||||||
|
|
||||||
|
pub(super) async fn route(State(state): State<AppState>) -> Result<Json<Vec<Warren>>> {
|
||||||
|
let warrens = Warren::list(state.pool()).await?;
|
||||||
|
|
||||||
|
Ok(Json(warrens))
|
||||||
|
}
|
||||||
13
backend/src/api/warrens/mod.rs
Normal file
13
backend/src/api/warrens/mod.rs
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
mod get_warren_path;
|
||||||
|
mod list_warrens;
|
||||||
|
|
||||||
|
use axum::routing::get;
|
||||||
|
|
||||||
|
use crate::server::Router;
|
||||||
|
|
||||||
|
pub(super) fn router() -> Router {
|
||||||
|
Router::new()
|
||||||
|
.route("/", get(list_warrens::route))
|
||||||
|
.route("/{warren_id}", get(get_warren_path::route))
|
||||||
|
.route("/{warren_id}/{*rest}", get(get_warren_path::route))
|
||||||
|
}
|
||||||
28
backend/src/db/mod.rs
Normal file
28
backend/src/db/mod.rs
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
use std::env;
|
||||||
|
|
||||||
|
use sqlx::{Connection, PgConnection, Pool, Postgres, postgres::PgPoolOptions};
|
||||||
|
|
||||||
|
use crate::error::AppError;
|
||||||
|
|
||||||
|
pub async fn get_postgres_pool() -> Result<Pool<Postgres>, AppError> {
|
||||||
|
let host = env::var("POSTGRES_HOST")?;
|
||||||
|
let port = env::var("POSTGRES_PORT")?;
|
||||||
|
let user = env::var("POSTGRES_USER")?;
|
||||||
|
let password = env::var("POSTGRES_PASSWORD")?;
|
||||||
|
let database = env::var("POSTGRES_DATABASE")?;
|
||||||
|
|
||||||
|
let base_url = format!("postgres://{user}:{password}@{host}:{port}");
|
||||||
|
|
||||||
|
let mut connection = PgConnection::connect(&base_url).await?;
|
||||||
|
let _ = sqlx::query(&format!("CREATE DATABASE {database}"))
|
||||||
|
.execute(&mut connection)
|
||||||
|
.await;
|
||||||
|
connection.close().await?;
|
||||||
|
|
||||||
|
let url = format!("{base_url}/{database}");
|
||||||
|
|
||||||
|
let pool = PgPoolOptions::new().connect(&url).await?;
|
||||||
|
sqlx::migrate!("./migrations").run(&pool).await?;
|
||||||
|
|
||||||
|
Ok(pool)
|
||||||
|
}
|
||||||
44
backend/src/error.rs
Normal file
44
backend/src/error.rs
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
use axum::{
|
||||||
|
body::Body,
|
||||||
|
http::{StatusCode, header::InvalidHeaderValue},
|
||||||
|
response::{IntoResponse, Response},
|
||||||
|
};
|
||||||
|
use thiserror::Error;
|
||||||
|
|
||||||
|
#[derive(Debug, Error)]
|
||||||
|
pub enum AppError {
|
||||||
|
#[error("IO: {0}")]
|
||||||
|
Io(#[from] std::io::Error),
|
||||||
|
#[error("Could not get file name")]
|
||||||
|
FileName,
|
||||||
|
#[error("SQLx: {0}")]
|
||||||
|
Sqlx(#[from] sqlx::Error),
|
||||||
|
#[error("Failed to migrate the database")]
|
||||||
|
DatabaseMigration(#[from] sqlx::migrate::MigrateError),
|
||||||
|
#[error("Var: {0}")]
|
||||||
|
Var(#[from] std::env::VarError),
|
||||||
|
#[error("InvalidHeaderValue: {0}")]
|
||||||
|
InvalidHeaderValue(#[from] InvalidHeaderValue),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl IntoResponse for AppError {
|
||||||
|
fn into_response(self) -> Response {
|
||||||
|
let status = match self {
|
||||||
|
Self::Io(error) => match error.kind() {
|
||||||
|
std::io::ErrorKind::NotFound => StatusCode::NOT_FOUND,
|
||||||
|
_ => StatusCode::INTERNAL_SERVER_ERROR,
|
||||||
|
},
|
||||||
|
Self::FileName => StatusCode::INTERNAL_SERVER_ERROR,
|
||||||
|
// TODO: Improve
|
||||||
|
Self::Sqlx(_) => StatusCode::INTERNAL_SERVER_ERROR,
|
||||||
|
Self::DatabaseMigration(_) => StatusCode::INTERNAL_SERVER_ERROR,
|
||||||
|
Self::Var(_) => StatusCode::INTERNAL_SERVER_ERROR,
|
||||||
|
Self::InvalidHeaderValue(_) => StatusCode::INTERNAL_SERVER_ERROR,
|
||||||
|
};
|
||||||
|
|
||||||
|
Response::builder()
|
||||||
|
.status(status)
|
||||||
|
.body(Body::empty())
|
||||||
|
.unwrap()
|
||||||
|
}
|
||||||
|
}
|
||||||
38
backend/src/fs/dir.rs
Normal file
38
backend/src/fs/dir.rs
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
use std::path::Path;
|
||||||
|
|
||||||
|
use tokio::fs;
|
||||||
|
|
||||||
|
use crate::{Result, error::AppError};
|
||||||
|
|
||||||
|
use super::{DirectoryEntry, FileType};
|
||||||
|
|
||||||
|
pub async fn get_dir_entries<P>(path: P) -> Result<Vec<DirectoryEntry>>
|
||||||
|
where
|
||||||
|
P: AsRef<Path>,
|
||||||
|
{
|
||||||
|
let mut dir = fs::read_dir(path).await?;
|
||||||
|
|
||||||
|
let mut files = Vec::new();
|
||||||
|
|
||||||
|
while let Ok(Some(entry)) = dir.next_entry().await {
|
||||||
|
let name = entry
|
||||||
|
.file_name()
|
||||||
|
.into_string()
|
||||||
|
.map_err(|_| AppError::FileName)?;
|
||||||
|
let file_type = {
|
||||||
|
let file_type = entry.file_type().await?;
|
||||||
|
|
||||||
|
if file_type.is_dir() {
|
||||||
|
FileType::Directory
|
||||||
|
} else if file_type.is_file() {
|
||||||
|
FileType::File
|
||||||
|
} else {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
files.push(DirectoryEntry::new(name, file_type));
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(files)
|
||||||
|
}
|
||||||
24
backend/src/fs/mod.rs
Normal file
24
backend/src/fs/mod.rs
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
mod dir;
|
||||||
|
pub use dir::*;
|
||||||
|
|
||||||
|
use serde::Serialize;
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, Serialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub enum FileType {
|
||||||
|
File,
|
||||||
|
Directory,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct DirectoryEntry {
|
||||||
|
name: String,
|
||||||
|
file_type: FileType,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl DirectoryEntry {
|
||||||
|
pub fn new(name: String, file_type: FileType) -> Self {
|
||||||
|
Self { name, file_type }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,7 +1,17 @@
|
|||||||
|
use db::get_postgres_pool;
|
||||||
|
use error::AppError;
|
||||||
|
|
||||||
|
pub mod api;
|
||||||
|
pub mod db;
|
||||||
|
pub mod error;
|
||||||
|
pub mod fs;
|
||||||
mod server;
|
mod server;
|
||||||
|
pub mod warrens;
|
||||||
|
|
||||||
|
pub type Result<T> = std::result::Result<T, AppError>;
|
||||||
|
|
||||||
#[tokio::main]
|
#[tokio::main]
|
||||||
async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
async fn main() -> std::result::Result<(), AppError> {
|
||||||
dotenv::dotenv().ok();
|
dotenv::dotenv().ok();
|
||||||
|
|
||||||
env_logger::builder()
|
env_logger::builder()
|
||||||
@@ -11,7 +21,9 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|||||||
.parse_default_env()
|
.parse_default_env()
|
||||||
.init();
|
.init();
|
||||||
|
|
||||||
server::start().await?;
|
let pool = get_postgres_pool().await?;
|
||||||
|
|
||||||
|
server::start(pool).await?;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,11 +1,32 @@
|
|||||||
use std::{env, error::Error};
|
use std::env;
|
||||||
|
|
||||||
use axum::Router;
|
use axum::http::HeaderValue;
|
||||||
|
use sqlx::{Pool, Postgres};
|
||||||
use tokio::net::TcpListener;
|
use tokio::net::TcpListener;
|
||||||
use tower_http::services::ServeDir;
|
use tower_http::{
|
||||||
|
cors::{self, CorsLayer},
|
||||||
|
services::ServeDir,
|
||||||
|
};
|
||||||
|
|
||||||
pub(super) async fn start() -> Result<(), Box<dyn Error>> {
|
use crate::{
|
||||||
let mut app = Router::new();
|
Result,
|
||||||
|
api::{self, AppState},
|
||||||
|
};
|
||||||
|
|
||||||
|
pub type Router = axum::Router<AppState>;
|
||||||
|
|
||||||
|
pub(super) async fn start(pool: Pool<Postgres>) -> Result<()> {
|
||||||
|
let cors_origin = env::var("CORS_ALLOW_ORIGIN").unwrap_or("*".to_string());
|
||||||
|
let serve_dir = std::env::var("SERVE_DIRECTORY")?;
|
||||||
|
|
||||||
|
let mut app = Router::new()
|
||||||
|
.nest("/api", api::router())
|
||||||
|
.layer(
|
||||||
|
CorsLayer::new().allow_origin(cors::AllowOrigin::exact(HeaderValue::from_str(
|
||||||
|
&cors_origin,
|
||||||
|
)?)),
|
||||||
|
)
|
||||||
|
.with_state(AppState::new(pool, serve_dir));
|
||||||
|
|
||||||
if !cfg!(debug_assertions) {
|
if !cfg!(debug_assertions) {
|
||||||
let frontend_path = env::var("STATIC_FRONTEND_DIR").unwrap_or("./frontend".to_string());
|
let frontend_path = env::var("STATIC_FRONTEND_DIR").unwrap_or("./frontend".to_string());
|
||||||
|
|||||||
49
backend/src/warrens/db.rs
Normal file
49
backend/src/warrens/db.rs
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
use sqlx::{Acquire, PgExecutor, Postgres};
|
||||||
|
use uuid::Uuid;
|
||||||
|
|
||||||
|
use crate::Result;
|
||||||
|
|
||||||
|
use super::Warren;
|
||||||
|
|
||||||
|
impl Warren {
|
||||||
|
pub async fn get<'c, E>(e: E, id: &Uuid) -> Result<Self>
|
||||||
|
where
|
||||||
|
E: PgExecutor<'c> + Acquire<'c, Database = Postgres>,
|
||||||
|
{
|
||||||
|
let warren: Self = sqlx::query_as(
|
||||||
|
"
|
||||||
|
SELECT
|
||||||
|
*
|
||||||
|
FROM
|
||||||
|
warrens
|
||||||
|
WHERE
|
||||||
|
id = $1
|
||||||
|
",
|
||||||
|
)
|
||||||
|
.bind(id)
|
||||||
|
.fetch_one(e)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
Ok(warren)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn list<'c, E>(e: E) -> Result<Vec<Self>>
|
||||||
|
where
|
||||||
|
E: PgExecutor<'c> + Acquire<'c, Database = Postgres>,
|
||||||
|
{
|
||||||
|
let warren: Vec<Self> = sqlx::query_as(
|
||||||
|
"
|
||||||
|
SELECT
|
||||||
|
*
|
||||||
|
FROM
|
||||||
|
warrens
|
||||||
|
LIMIT
|
||||||
|
50
|
||||||
|
",
|
||||||
|
)
|
||||||
|
.fetch_all(e)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
Ok(warren)
|
||||||
|
}
|
||||||
|
}
|
||||||
53
backend/src/warrens/mod.rs
Normal file
53
backend/src/warrens/mod.rs
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
pub mod db;
|
||||||
|
|
||||||
|
use std::path::PathBuf;
|
||||||
|
|
||||||
|
use serde::Serialize;
|
||||||
|
use sqlx::prelude::FromRow;
|
||||||
|
use uuid::Uuid;
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
Result,
|
||||||
|
fs::{DirectoryEntry, get_dir_entries},
|
||||||
|
};
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, FromRow)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct Warren {
|
||||||
|
id: Uuid,
|
||||||
|
name: String,
|
||||||
|
#[serde(skip)]
|
||||||
|
path: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Warren {
|
||||||
|
pub fn id(&self) -> &Uuid {
|
||||||
|
&self.id
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn name(&self) -> &str {
|
||||||
|
&self.name
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn path(&self) -> &str {
|
||||||
|
&self.path
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Warren {
|
||||||
|
pub async fn read_path(
|
||||||
|
&self,
|
||||||
|
serve_path: &str,
|
||||||
|
path: Option<String>,
|
||||||
|
) -> Result<Vec<DirectoryEntry>> {
|
||||||
|
let mut final_path = PathBuf::from(serve_path);
|
||||||
|
|
||||||
|
final_path.push(self.path.strip_prefix("/").unwrap_or(&self.path));
|
||||||
|
|
||||||
|
if let Some(ref rest) = path {
|
||||||
|
final_path.push(rest);
|
||||||
|
}
|
||||||
|
|
||||||
|
get_dir_entries(final_path).await
|
||||||
|
}
|
||||||
|
}
|
||||||
25
compose.yaml
25
compose.yaml
@@ -1,14 +1,39 @@
|
|||||||
services:
|
services:
|
||||||
warren:
|
warren:
|
||||||
|
depends_on:
|
||||||
|
- 'postgres'
|
||||||
image: 'warren:latest'
|
image: 'warren:latest'
|
||||||
container_name: 'warren'
|
container_name: 'warren'
|
||||||
build: '.'
|
build: '.'
|
||||||
|
ports:
|
||||||
|
- '8081:8080'
|
||||||
|
restart: 'unless-stopped'
|
||||||
|
networks:
|
||||||
|
- 'warren-net'
|
||||||
|
environment:
|
||||||
|
- 'BIND_ADDRESS=0.0.0.0:8080'
|
||||||
|
- 'POSTGRES_HOST=warren-postgres'
|
||||||
|
- 'POSTGRES_PORT=5432'
|
||||||
|
- 'POSTGRES_USER=postgres'
|
||||||
|
- 'POSTGRES_PASSWORD=pg'
|
||||||
|
- 'POSTGRES_DATABASE=warren'
|
||||||
|
- 'SERVE_DIRECTORY=/serve'
|
||||||
|
- 'CORS_ALLOW_ORIGIN=http://localhost:3000'
|
||||||
|
volumes:
|
||||||
|
- './backend/serve:/serve:rw'
|
||||||
postgres:
|
postgres:
|
||||||
image: 'postgres:17'
|
image: 'postgres:17'
|
||||||
container_name: 'warren-db'
|
container_name: 'warren-db'
|
||||||
|
hostname: 'warren-postgres'
|
||||||
|
networks:
|
||||||
|
- 'warren-net'
|
||||||
volumes:
|
volumes:
|
||||||
- './postgres-data:/var/lib/postgresql/data'
|
- './postgres-data:/var/lib/postgresql/data'
|
||||||
environment:
|
environment:
|
||||||
- 'POSTGRES_PASSWORD=pg'
|
- 'POSTGRES_PASSWORD=pg'
|
||||||
ports:
|
ports:
|
||||||
- '5432:5432/tcp'
|
- '5432:5432/tcp'
|
||||||
|
networks:
|
||||||
|
warren-net:
|
||||||
|
name: 'warren-net'
|
||||||
|
external: false
|
||||||
|
|||||||
3
frontend/.env.dev
Normal file
3
frontend/.env.dev
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
# this file is ignored when the app is built since we're using SSG
|
||||||
|
|
||||||
|
NUXT_PUBLIC_API_BASE="http://127.0.0.1:8080/api"
|
||||||
1
frontend/.gitignore
vendored
1
frontend/.gitignore
vendored
@@ -22,3 +22,4 @@ logs
|
|||||||
.env
|
.env
|
||||||
.env.*
|
.env.*
|
||||||
!.env.example
|
!.env.example
|
||||||
|
!.env.dev
|
||||||
|
|||||||
@@ -1,3 +1,11 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { getWarrens } from './lib/api/warrens';
|
||||||
|
|
||||||
|
const store = useWarrenStore();
|
||||||
|
|
||||||
|
store.warrens = await getWarrens();
|
||||||
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<NuxtRouteAnnouncer />
|
<NuxtRouteAnnouncer />
|
||||||
<NuxtLoadingIndicator />
|
<NuxtLoadingIndicator />
|
||||||
|
|||||||
@@ -9,6 +9,7 @@
|
|||||||
"@nuxt/icon": "1.15.0",
|
"@nuxt/icon": "1.15.0",
|
||||||
"@nuxt/image": "1.10.0",
|
"@nuxt/image": "1.10.0",
|
||||||
"@nuxt/test-utils": "3.19.2",
|
"@nuxt/test-utils": "3.19.2",
|
||||||
|
"@pinia/nuxt": "^0.11.1",
|
||||||
"@tailwindcss/vite": "^4.1.11",
|
"@tailwindcss/vite": "^4.1.11",
|
||||||
"@vueuse/core": "^13.5.0",
|
"@vueuse/core": "^13.5.0",
|
||||||
"class-variance-authority": "^0.7.1",
|
"class-variance-authority": "^0.7.1",
|
||||||
@@ -16,6 +17,7 @@
|
|||||||
"eslint": "^9.0.0",
|
"eslint": "^9.0.0",
|
||||||
"lucide-vue-next": "^0.525.0",
|
"lucide-vue-next": "^0.525.0",
|
||||||
"nuxt": "^3.17.6",
|
"nuxt": "^3.17.6",
|
||||||
|
"pinia": "^3.0.3",
|
||||||
"reka-ui": "^2.3.2",
|
"reka-ui": "^2.3.2",
|
||||||
"shadcn-nuxt": "2.2.0",
|
"shadcn-nuxt": "2.2.0",
|
||||||
"tailwind-merge": "^3.3.1",
|
"tailwind-merge": "^3.3.1",
|
||||||
@@ -369,6 +371,8 @@
|
|||||||
|
|
||||||
"@parcel/watcher-win32-x64": ["@parcel/watcher-win32-x64@2.5.1", "", { "os": "win32", "cpu": "x64" }, "sha512-9lHBdJITeNR++EvSQVUcaZoWupyHfXe1jZvGZ06O/5MflPcuPLtEphScIBL+AiCWBO46tDSHzWyD0uDmmZqsgA=="],
|
"@parcel/watcher-win32-x64": ["@parcel/watcher-win32-x64@2.5.1", "", { "os": "win32", "cpu": "x64" }, "sha512-9lHBdJITeNR++EvSQVUcaZoWupyHfXe1jZvGZ06O/5MflPcuPLtEphScIBL+AiCWBO46tDSHzWyD0uDmmZqsgA=="],
|
||||||
|
|
||||||
|
"@pinia/nuxt": ["@pinia/nuxt@0.11.1", "", { "dependencies": { "@nuxt/kit": "^3.9.0" }, "peerDependencies": { "pinia": "^3.0.3" } }, "sha512-tCD8ioWhhIHKwm8Y9VvyhBAV/kK4W5uGBIYbI5iM4N1t7duOqK6ECBUavrMxMolELayqqMLb9+evegrh3S7s2A=="],
|
||||||
|
|
||||||
"@pkgjs/parseargs": ["@pkgjs/parseargs@0.11.0", "", {}, "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg=="],
|
"@pkgjs/parseargs": ["@pkgjs/parseargs@0.11.0", "", {}, "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg=="],
|
||||||
|
|
||||||
"@polka/url": ["@polka/url@1.0.0-next.29", "", {}, "sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww=="],
|
"@polka/url": ["@polka/url@1.0.0-next.29", "", {}, "sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww=="],
|
||||||
@@ -547,7 +551,7 @@
|
|||||||
|
|
||||||
"@vue/compiler-ssr": ["@vue/compiler-ssr@3.5.17", "", { "dependencies": { "@vue/compiler-dom": "3.5.17", "@vue/shared": "3.5.17" } }, "sha512-hkDbA0Q20ZzGgpj5uZjb9rBzQtIHLS78mMilwrlpWk2Ep37DYntUz0PonQ6kr113vfOEdM+zTBuJDaceNIW0tQ=="],
|
"@vue/compiler-ssr": ["@vue/compiler-ssr@3.5.17", "", { "dependencies": { "@vue/compiler-dom": "3.5.17", "@vue/shared": "3.5.17" } }, "sha512-hkDbA0Q20ZzGgpj5uZjb9rBzQtIHLS78mMilwrlpWk2Ep37DYntUz0PonQ6kr113vfOEdM+zTBuJDaceNIW0tQ=="],
|
||||||
|
|
||||||
"@vue/devtools-api": ["@vue/devtools-api@6.6.4", "", {}, "sha512-sGhTPMuXqZ1rVOk32RylztWkfXTRhuS7vgAKv0zjqk8gbsHkJ7xfFf+jbySxt7tWObEJwyKaHMikV/WGDiQm8g=="],
|
"@vue/devtools-api": ["@vue/devtools-api@7.7.7", "", { "dependencies": { "@vue/devtools-kit": "^7.7.7" } }, "sha512-lwOnNBH2e7x1fIIbVT7yF5D+YWhqELm55/4ZKf45R9T8r9dE2AIOy8HKjfqzGsoTHFbWbr337O4E0A0QADnjBg=="],
|
||||||
|
|
||||||
"@vue/devtools-core": ["@vue/devtools-core@7.7.7", "", { "dependencies": { "@vue/devtools-kit": "^7.7.7", "@vue/devtools-shared": "^7.7.7", "mitt": "^3.0.1", "nanoid": "^5.1.0", "pathe": "^2.0.3", "vite-hot-client": "^2.0.4" }, "peerDependencies": { "vue": "^3.0.0" } }, "sha512-9z9TLbfC+AjAi1PQyWX+OErjIaJmdFlbDHcD+cAMYKY6Bh5VlsAtCeGyRMrXwIlMEQPukvnWt3gZBLwTAIMKzQ=="],
|
"@vue/devtools-core": ["@vue/devtools-core@7.7.7", "", { "dependencies": { "@vue/devtools-kit": "^7.7.7", "@vue/devtools-shared": "^7.7.7", "mitt": "^3.0.1", "nanoid": "^5.1.0", "pathe": "^2.0.3", "vite-hot-client": "^2.0.4" }, "peerDependencies": { "vue": "^3.0.0" } }, "sha512-9z9TLbfC+AjAi1PQyWX+OErjIaJmdFlbDHcD+cAMYKY6Bh5VlsAtCeGyRMrXwIlMEQPukvnWt3gZBLwTAIMKzQ=="],
|
||||||
|
|
||||||
@@ -1445,6 +1449,8 @@
|
|||||||
|
|
||||||
"picomatch": ["picomatch@4.0.2", "", {}, "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg=="],
|
"picomatch": ["picomatch@4.0.2", "", {}, "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg=="],
|
||||||
|
|
||||||
|
"pinia": ["pinia@3.0.3", "", { "dependencies": { "@vue/devtools-api": "^7.7.2" }, "peerDependencies": { "typescript": ">=4.4.4", "vue": "^2.7.0 || ^3.5.11" }, "optionalPeers": ["typescript"] }, "sha512-ttXO/InUULUXkMHpTdp9Fj4hLpD/2AoJdmAbAeW2yu1iy1k+pkFekQXw5VpC0/5p51IOR/jDaDRfRWRnMMsGOA=="],
|
||||||
|
|
||||||
"pkg-types": ["pkg-types@1.3.1", "", { "dependencies": { "confbox": "^0.1.8", "mlly": "^1.7.4", "pathe": "^2.0.1" } }, "sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ=="],
|
"pkg-types": ["pkg-types@1.3.1", "", { "dependencies": { "confbox": "^0.1.8", "mlly": "^1.7.4", "pathe": "^2.0.1" } }, "sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ=="],
|
||||||
|
|
||||||
"pluralize": ["pluralize@8.0.0", "", {}, "sha512-Nc3IT5yHzflTfbjgqWcCPpo7DaKy4FnpB0l/zCAW0Tc7jxAiuqSxHasntB3D7887LSrA93kDJ9IXovxJYxyLCA=="],
|
"pluralize": ["pluralize@8.0.0", "", {}, "sha512-Nc3IT5yHzflTfbjgqWcCPpo7DaKy4FnpB0l/zCAW0Tc7jxAiuqSxHasntB3D7887LSrA93kDJ9IXovxJYxyLCA=="],
|
||||||
@@ -2199,6 +2205,8 @@
|
|||||||
|
|
||||||
"vite-plugin-vue-tracer/pathe": ["pathe@2.0.3", "", {}, "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w=="],
|
"vite-plugin-vue-tracer/pathe": ["pathe@2.0.3", "", {}, "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w=="],
|
||||||
|
|
||||||
|
"vue-router/@vue/devtools-api": ["@vue/devtools-api@6.6.4", "", {}, "sha512-sGhTPMuXqZ1rVOk32RylztWkfXTRhuS7vgAKv0zjqk8gbsHkJ7xfFf+jbySxt7tWObEJwyKaHMikV/WGDiQm8g=="],
|
||||||
|
|
||||||
"winston/is-stream": ["is-stream@2.0.1", "", {}, "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg=="],
|
"winston/is-stream": ["is-stream@2.0.1", "", {}, "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg=="],
|
||||||
|
|
||||||
"winston/readable-stream": ["readable-stream@3.6.2", "", { "dependencies": { "inherits": "^2.0.3", "string_decoder": "^1.1.1", "util-deprecate": "^1.0.1" } }, "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA=="],
|
"winston/readable-stream": ["readable-stream@3.6.2", "", { "dependencies": { "inherits": "^2.0.3", "string_decoder": "^1.1.1", "util-deprecate": "^1.0.1" } }, "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA=="],
|
||||||
|
|||||||
@@ -14,18 +14,7 @@ import {
|
|||||||
|
|
||||||
const route = useRoute();
|
const route = useRoute();
|
||||||
|
|
||||||
const warrens = [
|
const store = useWarrenStore();
|
||||||
{
|
|
||||||
title: 'Thyr',
|
|
||||||
url: '/warrens/Thyr',
|
|
||||||
icon: h(Icon, { name: 'lucide:folder-root' }),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: 'Serc',
|
|
||||||
url: '/warrens/Serc',
|
|
||||||
icon: h(Icon, { name: 'lucide:folder-root' }),
|
|
||||||
},
|
|
||||||
];
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@@ -57,22 +46,24 @@ const warrens = [
|
|||||||
<CollapsibleContent>
|
<CollapsibleContent>
|
||||||
<SidebarMenuSub>
|
<SidebarMenuSub>
|
||||||
<SidebarMenuSubItem
|
<SidebarMenuSubItem
|
||||||
v-for="warren in warrens"
|
v-for="(warren, uuid) in store.warrens"
|
||||||
:key="warren.title"
|
:key="uuid"
|
||||||
>
|
>
|
||||||
<SidebarMenuSubButton
|
<SidebarMenuSubButton
|
||||||
as-child
|
as-child
|
||||||
:tooltip="warren.title"
|
:tooltip="warren.name"
|
||||||
:is-active="
|
:is-active="
|
||||||
warren.url === route.path
|
route.path.startsWith(
|
||||||
|
`/warrens/${uuid}`
|
||||||
|
)
|
||||||
"
|
"
|
||||||
class="transition-all"
|
class="transition-all"
|
||||||
>
|
>
|
||||||
<NuxtLink :to="warren.url">
|
<NuxtLink :to="`/warrens/${uuid}`">
|
||||||
<component
|
<Icon
|
||||||
:is="warren.icon"
|
name="lucide:folder-root"
|
||||||
></component>
|
/>
|
||||||
<span>{{ warren.title }}</span>
|
<span>{{ warren.name }}</span>
|
||||||
</NuxtLink>
|
</NuxtLink>
|
||||||
</SidebarMenuSubButton>
|
</SidebarMenuSubButton>
|
||||||
</SidebarMenuSubItem>
|
</SidebarMenuSubItem>
|
||||||
|
|||||||
@@ -1,24 +1,30 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import type { DirectoryEntryType } from '~/types';
|
import type { FileType } from '~/types';
|
||||||
|
|
||||||
const route = useRoute();
|
const route = useRoute();
|
||||||
|
|
||||||
const { name, entryType } = defineProps<{
|
const { name, entryType, disabled } = defineProps<{
|
||||||
name: string;
|
name: string;
|
||||||
entryType: DirectoryEntryType;
|
entryType: FileType;
|
||||||
|
disabled: boolean,
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const iconName = entryType === 'file' ? 'lucide:file' : 'lucide:folder';
|
const iconName = entryType === 'file' ? 'lucide:file' : 'lucide:folder';
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<Button class="w-36 h-12" variant="outline" size="lg">
|
<NuxtLink
|
||||||
<NuxtLink
|
:to="joinPaths(route.path, name)"
|
||||||
class="flex flex-row items-center gap-1.5"
|
:class="['select-none', { 'pointer-events-none': disabled }]"
|
||||||
:to="joinPaths(route.path, name)"
|
>
|
||||||
|
<Button
|
||||||
|
class="w-44 h-12"
|
||||||
|
variant="outline"
|
||||||
|
size="lg"
|
||||||
|
:disabled="disabled"
|
||||||
>
|
>
|
||||||
<Icon :name="iconName" />
|
<Icon :name="iconName" />
|
||||||
<span>{{ name }}</span>
|
<span class="truncate">{{ name }}</span>
|
||||||
</NuxtLink>
|
</Button>
|
||||||
</Button>
|
</NuxtLink>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -1,34 +1,22 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ScrollArea } from '@/components/ui/scroll-area';
|
import { ScrollArea } from '@/components/ui/scroll-area';
|
||||||
import type { DirectoryEntryType } from '~/types';
|
import type { DirectoryEntry } from '~/types';
|
||||||
const items: Array<{ name: string; entryType: DirectoryEntryType }> = [
|
const { entries } = defineProps<{
|
||||||
/* {
|
entries: DirectoryEntry[];
|
||||||
name: 'File A',
|
}>();
|
||||||
entryType: 'file',
|
|
||||||
},
|
const { isLoading } = useLoadingIndicator();
|
||||||
{
|
|
||||||
name: 'File B',
|
|
||||||
entryType: 'file',
|
|
||||||
}, */
|
|
||||||
{
|
|
||||||
name: 'Directory A',
|
|
||||||
entryType: 'directory',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'Directory B',
|
|
||||||
entryType: 'directory',
|
|
||||||
},
|
|
||||||
];
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<ScrollArea class="w-full h-full">
|
<ScrollArea class="w-full h-full">
|
||||||
<div class="flex flex-row gap-2">
|
<div class="flex flex-row gap-2">
|
||||||
<DirectoryEntry
|
<DirectoryEntry
|
||||||
v-for="item in items"
|
v-for="entry in entries"
|
||||||
:key="item.name"
|
:key="entry.name"
|
||||||
:name="item.name"
|
:name="entry.name"
|
||||||
:entry-type="item.entryType"
|
:entry-type="entry.fileType"
|
||||||
|
:disabled="isLoading"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</ScrollArea>
|
</ScrollArea>
|
||||||
|
|||||||
29
frontend/lib/api/warrens.ts
Normal file
29
frontend/lib/api/warrens.ts
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
import type { DirectoryEntry } from '~/types';
|
||||||
|
import type { Warren } from '~/types/warrens';
|
||||||
|
|
||||||
|
export async function getWarrens(): Promise<Record<string, Warren>> {
|
||||||
|
const arr: Warren[] = await $fetch(getApiUrl('warrens'), {
|
||||||
|
method: 'GET',
|
||||||
|
});
|
||||||
|
|
||||||
|
const warrens: Record<string, Warren> = {};
|
||||||
|
|
||||||
|
for (const warren of arr) {
|
||||||
|
warrens[warren.id] = warren;
|
||||||
|
}
|
||||||
|
|
||||||
|
return warrens;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getWarrenDirectory(
|
||||||
|
path: string
|
||||||
|
): Promise<DirectoryEntry[]> {
|
||||||
|
const entries: DirectoryEntry[] = await $fetch(
|
||||||
|
getApiUrl(`warrens/${path}`),
|
||||||
|
{
|
||||||
|
method: 'GET',
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
return entries;
|
||||||
|
}
|
||||||
@@ -13,6 +13,7 @@ export default defineNuxtConfig({
|
|||||||
'@nuxt/test-utils',
|
'@nuxt/test-utils',
|
||||||
'shadcn-nuxt',
|
'shadcn-nuxt',
|
||||||
'@nuxtjs/color-mode',
|
'@nuxtjs/color-mode',
|
||||||
|
'@pinia/nuxt',
|
||||||
],
|
],
|
||||||
|
|
||||||
css: ['~/assets/css/tailwind.css'],
|
css: ['~/assets/css/tailwind.css'],
|
||||||
@@ -53,4 +54,10 @@ export default defineNuxtConfig({
|
|||||||
},
|
},
|
||||||
|
|
||||||
ssr: false,
|
ssr: false,
|
||||||
|
|
||||||
|
runtimeConfig: {
|
||||||
|
public: {
|
||||||
|
apiBase: '/api',
|
||||||
|
},
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "nuxt build",
|
"build": "nuxt build",
|
||||||
"dev": "nuxt dev",
|
"dev": "nuxt dev --dotenv .env.dev",
|
||||||
"generate": "nuxt generate",
|
"generate": "nuxt generate",
|
||||||
"preview": "nuxt preview",
|
"preview": "nuxt preview",
|
||||||
"postinstall": "nuxt prepare",
|
"postinstall": "nuxt prepare",
|
||||||
@@ -16,6 +16,7 @@
|
|||||||
"@nuxt/icon": "1.15.0",
|
"@nuxt/icon": "1.15.0",
|
||||||
"@nuxt/image": "1.10.0",
|
"@nuxt/image": "1.10.0",
|
||||||
"@nuxt/test-utils": "3.19.2",
|
"@nuxt/test-utils": "3.19.2",
|
||||||
|
"@pinia/nuxt": "^0.11.1",
|
||||||
"@tailwindcss/vite": "^4.1.11",
|
"@tailwindcss/vite": "^4.1.11",
|
||||||
"@vueuse/core": "^13.5.0",
|
"@vueuse/core": "^13.5.0",
|
||||||
"class-variance-authority": "^0.7.1",
|
"class-variance-authority": "^0.7.1",
|
||||||
@@ -23,6 +24,7 @@
|
|||||||
"eslint": "^9.0.0",
|
"eslint": "^9.0.0",
|
||||||
"lucide-vue-next": "^0.525.0",
|
"lucide-vue-next": "^0.525.0",
|
||||||
"nuxt": "^3.17.6",
|
"nuxt": "^3.17.6",
|
||||||
|
"pinia": "^3.0.3",
|
||||||
"reka-ui": "^2.3.2",
|
"reka-ui": "^2.3.2",
|
||||||
"shadcn-nuxt": "2.2.0",
|
"shadcn-nuxt": "2.2.0",
|
||||||
"tailwind-merge": "^3.3.1",
|
"tailwind-merge": "^3.3.1",
|
||||||
|
|||||||
@@ -1,3 +1,9 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { getWarrenDirectory } from '~/lib/api/warrens';
|
||||||
|
const route = useRoute();
|
||||||
|
const entries = await getWarrenDirectory(route.path.split('/warrens/')[1]);
|
||||||
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<DirectoryList />
|
<DirectoryList :entries="entries" />
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -1,28 +1,22 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ScrollArea } from '@/components/ui/scroll-area';
|
import { ScrollArea } from '@/components/ui/scroll-area';
|
||||||
|
|
||||||
const warrens = ['Thyr', 'Serc'];
|
const store = useWarrenStore();
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<ScrollArea class="w-full h-full">
|
<ScrollArea class="w-full h-full">
|
||||||
<div class="flex flex-row gap-2">
|
<div class="flex flex-row gap-2">
|
||||||
<Button
|
<NuxtLink
|
||||||
v-for="(warren, i) in warrens"
|
v-for="(warren, uuid) in store.warrens"
|
||||||
:key="i"
|
:key="uuid"
|
||||||
class="w-36 h-12"
|
:to="`/warrens/${uuid}`"
|
||||||
variant="outline"
|
|
||||||
size="lg"
|
|
||||||
as-child
|
|
||||||
>
|
>
|
||||||
<NuxtLink
|
<Button class="w-44 h-12" variant="outline" size="lg">
|
||||||
class="flex flex-row items-center gap-1.5"
|
|
||||||
:to="`/warrens/${warren}`"
|
|
||||||
>
|
|
||||||
<Icon name="lucide:folder-root" />
|
<Icon name="lucide:folder-root" />
|
||||||
{{ warren }}
|
<span clas="truncate">{{ warren.name }}</span>
|
||||||
</NuxtLink>
|
</Button>
|
||||||
</Button>
|
</NuxtLink>
|
||||||
</div>
|
</div>
|
||||||
</ScrollArea>
|
</ScrollArea>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
8
frontend/stores/index.ts
Normal file
8
frontend/stores/index.ts
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
import { defineStore } from 'pinia';
|
||||||
|
import type { Warren } from '~/types/warrens';
|
||||||
|
|
||||||
|
export const useWarrenStore = defineStore('warrens', {
|
||||||
|
state: () => ({
|
||||||
|
warrens: {} as Record<string, Warren>,
|
||||||
|
}),
|
||||||
|
});
|
||||||
@@ -1,6 +1,11 @@
|
|||||||
export type DirectoryEntryType = 'file' | 'directory';
|
export type FileType = 'file' | 'directory';
|
||||||
|
|
||||||
export type BreadcrumbData = {
|
export type BreadcrumbData = {
|
||||||
name: string;
|
name: string;
|
||||||
href: string;
|
href: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type DirectoryEntry = {
|
||||||
|
name: string;
|
||||||
|
fileType: FileType;
|
||||||
|
};
|
||||||
|
|||||||
4
frontend/types/warrens.ts
Normal file
4
frontend/types/warrens.ts
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
export type Warren = {
|
||||||
|
id: string,
|
||||||
|
name: string,
|
||||||
|
};
|
||||||
5
frontend/utils/api.ts
Normal file
5
frontend/utils/api.ts
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
export function getApiUrl(path: string): string {
|
||||||
|
const API_BASE_URL = useRuntimeConfig().public.apiBase;
|
||||||
|
console.log(API_BASE_URL);
|
||||||
|
return `${API_BASE_URL}/${path}`;
|
||||||
|
}
|
||||||
@@ -1,6 +1,8 @@
|
|||||||
import type { BreadcrumbData } from '~/types';
|
import type { BreadcrumbData } from '~/types';
|
||||||
|
|
||||||
export function getBreadcrumbs(path: string): BreadcrumbData[] {
|
export function getBreadcrumbs(path: string): BreadcrumbData[] {
|
||||||
|
const { warrens } = useWarrenStore();
|
||||||
|
|
||||||
const crumbs = path
|
const crumbs = path
|
||||||
.split('/')
|
.split('/')
|
||||||
.filter((v) => v.length > 0)
|
.filter((v) => v.length > 0)
|
||||||
@@ -21,6 +23,14 @@ export function getBreadcrumbs(path: string): BreadcrumbData[] {
|
|||||||
.join('/');
|
.join('/');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
crumbs.length >= 3 &&
|
||||||
|
crumbs[1].href === '/warrens' &&
|
||||||
|
crumbs[2].name in warrens
|
||||||
|
) {
|
||||||
|
crumbs[2].name = warrens[crumbs[2].name].name;
|
||||||
|
}
|
||||||
|
|
||||||
return crumbs;
|
return crumbs;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user