Footer and Setting Fixes
This commit is contained in:
@@ -1,6 +1,7 @@
|
|||||||
# TypstDrive
|
# TypstDrive
|
||||||
|
|
||||||
[](https://github.com/your-username/typstdrive)
|
[](https://github.com/your-username/typstdrive)
|
||||||
|
[](https://typst.app/)
|
||||||
[](https://www.rust-lang.org/)
|
[](https://www.rust-lang.org/)
|
||||||
[](https://kit.svelte.dev/)
|
[](https://kit.svelte.dev/)
|
||||||
[](https://tailwindcss.com/)
|
[](https://tailwindcss.com/)
|
||||||
@@ -8,7 +9,7 @@
|
|||||||
[](https://www.sqlite.org/)
|
[](https://www.sqlite.org/)
|
||||||
[](https://www.docker.com/)
|
[](https://www.docker.com/)
|
||||||
|
|
||||||
TypstDrive is a real-time collaborative web editor for Typst. With built-in dark mode, multiple themes, and a clean Google Docs-like interface, it makes creating and sharing documents effortless.
|
TypstDrive is a collaborative web editor for Typst. With built-in dark mode, multiple themes, and a clean Google Docs-like interface, it makes creating and sharing documents effortless.
|
||||||
|
|
||||||
## Features
|
## Features
|
||||||
|
|
||||||
@@ -89,3 +90,13 @@ If you'd like to contribute or run TypstDrive without Docker:
|
|||||||
2. Build and run: `cargo run`
|
2. Build and run: `cargo run`
|
||||||
|
|
||||||
Note: The frontend expects the backend to be running on port 3000. During local development via Vite, API calls are proxied automatically.
|
Note: The frontend expects the backend to be running on port 3000. During local development via Vite, API calls are proxied automatically.
|
||||||
|
|
||||||
|
## Screenshots
|
||||||
|
|
||||||
|
<p align="center">
|
||||||
|
<img src="preview/editor.png" alt="Editor view" width="100%">
|
||||||
|
</p>
|
||||||
|
<p align="center">
|
||||||
|
<img src="preview/dashboard.png" alt="Dashboard view" width="49%">
|
||||||
|
<img src="preview/register.png" alt="Authentication view" width="49%">
|
||||||
|
</p>
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 67 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 241 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 118 KiB |
+33
-1
@@ -7,7 +7,7 @@ use axum_extra::extract::cookie::{Cookie, SameSite, SignedCookieJar};
|
|||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
models::{User, RegisterRequest, LoginRequest, ChangePasswordRequest, UpdateProfileRequest},
|
models::{User, RegisterRequest, LoginRequest, ChangePasswordRequest, UpdateProfileRequest, StorageStats},
|
||||||
AppState,
|
AppState,
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -188,3 +188,35 @@ pub async fn change_password(
|
|||||||
|
|
||||||
Ok(StatusCode::OK)
|
Ok(StatusCode::OK)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub async fn storage_stats(
|
||||||
|
State(state): State<AppState>,
|
||||||
|
jar: SignedCookieJar,
|
||||||
|
) -> Result<Json<StorageStats>, (StatusCode, String)> {
|
||||||
|
let user_id = jar.get("session_user_id").map(|c| c.value().to_string())
|
||||||
|
.ok_or((StatusCode::UNAUTHORIZED, "Not logged in".to_string()))?;
|
||||||
|
|
||||||
|
let docs_size: (i64,) = sqlx::query_as(
|
||||||
|
"SELECT COALESCE(SUM(LENGTH(content)), 0) FROM documents WHERE owner_id = ?"
|
||||||
|
)
|
||||||
|
.bind(&user_id)
|
||||||
|
.fetch_one(&state.db)
|
||||||
|
.await
|
||||||
|
.unwrap_or((0,));
|
||||||
|
|
||||||
|
let files_size: (i64,) = sqlx::query_as(
|
||||||
|
"SELECT COALESCE(SUM(LENGTH(data)), 0) FROM files WHERE owner_id = ?"
|
||||||
|
)
|
||||||
|
.bind(&user_id)
|
||||||
|
.fetch_one(&state.db)
|
||||||
|
.await
|
||||||
|
.unwrap_or((0,));
|
||||||
|
|
||||||
|
let stats = StorageStats {
|
||||||
|
documents_size_bytes: docs_size.0,
|
||||||
|
files_size_bytes: files_size.0,
|
||||||
|
total_size_bytes: docs_size.0 + files_size.0,
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(Json(stats))
|
||||||
|
}
|
||||||
|
|||||||
@@ -70,6 +70,7 @@ async fn main() {
|
|||||||
.route("/auth/login", post(auth::login))
|
.route("/auth/login", post(auth::login))
|
||||||
.route("/auth/logout", post(auth::logout))
|
.route("/auth/logout", post(auth::logout))
|
||||||
.route("/auth/me", get(auth::me).put(auth::update_profile))
|
.route("/auth/me", get(auth::me).put(auth::update_profile))
|
||||||
|
.route("/auth/storage", get(auth::storage_stats))
|
||||||
.route("/auth/change-password", put(auth::change_password))
|
.route("/auth/change-password", put(auth::change_password))
|
||||||
.route("/folders", get(folders::list_folders).post(folders::create_folder))
|
.route("/folders", get(folders::list_folders).post(folders::create_folder))
|
||||||
.route("/folders/{id}", delete(folders::delete_folder).patch(folders::update_folder))
|
.route("/folders/{id}", delete(folders::delete_folder).patch(folders::update_folder))
|
||||||
|
|||||||
@@ -83,3 +83,10 @@ pub struct UpdateDocumentRequest {
|
|||||||
pub title: Option<String>,
|
pub title: Option<String>,
|
||||||
pub folder_id: Option<String>,
|
pub folder_id: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
|
pub struct StorageStats {
|
||||||
|
pub documents_size_bytes: i64,
|
||||||
|
pub files_size_bytes: i64,
|
||||||
|
pub total_size_bytes: i64,
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import Icon from '@iconify/svelte';
|
import Icon from '@iconify/svelte';
|
||||||
|
let { sticky = true }: { sticky?: boolean } = $props();
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<footer class="mt-auto py-6 text-center text-sm text-gray-500 dark:text-gray-400 border-t border-gray-200 dark:border-white/10 bg-[var(--theme-bg)] sticky bottom-0 w-full z-10 flex-shrink-0 transition-colors duration-200">
|
<footer class="mt-auto py-6 text-center text-sm text-gray-500 dark:text-gray-400 border-t border-gray-200 dark:border-white/10 bg-[var(--theme-bg)] {sticky ? 'sticky bottom-0' : ''} w-full z-10 flex-shrink-0 transition-colors duration-200">
|
||||||
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 flex flex-col justify-center items-center gap-4">
|
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 flex flex-col justify-center items-center gap-4">
|
||||||
<a
|
<a
|
||||||
href="https://github.com/SirBlobby/TypstDrive"
|
href="https://github.com/SirBlobby/TypstDrive"
|
||||||
|
|||||||
@@ -476,5 +476,5 @@
|
|||||||
{/if}
|
{/if}
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
<Footer />
|
<Footer sticky={false} />
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -21,11 +21,30 @@
|
|||||||
let usernameError = $state('');
|
let usernameError = $state('');
|
||||||
let usernameSuccess = $state(false);
|
let usernameSuccess = $state(false);
|
||||||
|
|
||||||
onMount(() => {
|
let storageStats = $state<{documents_size_bytes: number, files_size_bytes: number, total_size_bytes: number} | null>(null);
|
||||||
|
|
||||||
|
function formatBytes(bytes: number) {
|
||||||
|
if (bytes === 0) return '0 Bytes';
|
||||||
|
const k = 1024;
|
||||||
|
const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB'];
|
||||||
|
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
||||||
|
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
|
||||||
|
}
|
||||||
|
|
||||||
|
onMount(async () => {
|
||||||
if (!$userStore) {
|
if (!$userStore) {
|
||||||
goto('/login');
|
goto('/login');
|
||||||
} else {
|
} else {
|
||||||
username = $userStore.username;
|
username = $userStore.username;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const res = await fetch('/api/auth/storage');
|
||||||
|
if (res.ok) {
|
||||||
|
storageStats = await res.json();
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error("Failed to fetch storage stats", err);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -266,7 +285,9 @@
|
|||||||
<div class="bg-gray-50 dark:bg-black/20 rounded-lg p-5 border border-gray-200 dark:border-white/10">
|
<div class="bg-gray-50 dark:bg-black/20 rounded-lg p-5 border border-gray-200 dark:border-white/10">
|
||||||
<div class="mb-2 flex justify-between items-end">
|
<div class="mb-2 flex justify-between items-end">
|
||||||
<span class="text-sm font-medium text-gray-700 dark:text-gray-300">Total Space Used</span>
|
<span class="text-sm font-medium text-gray-700 dark:text-gray-300">Total Space Used</span>
|
||||||
<span class="text-sm font-bold text-gray-900 dark:text-white">45 MB</span>
|
<span class="text-sm font-bold text-gray-900 dark:text-white">
|
||||||
|
{storageStats ? formatBytes(storageStats.total_size_bytes) : 'Loading...'}
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="grid grid-cols-2 gap-4 text-sm mt-6">
|
<div class="grid grid-cols-2 gap-4 text-sm mt-6">
|
||||||
@@ -274,14 +295,18 @@
|
|||||||
<div class="w-3 h-3 rounded-full bg-blue-500"></div>
|
<div class="w-3 h-3 rounded-full bg-blue-500"></div>
|
||||||
<div>
|
<div>
|
||||||
<p class="text-gray-500 dark:text-gray-400">Documents</p>
|
<p class="text-gray-500 dark:text-gray-400">Documents</p>
|
||||||
<p class="font-semibold text-gray-900 dark:text-white">12 MB</p>
|
<p class="font-semibold text-gray-900 dark:text-white">
|
||||||
|
{storageStats ? formatBytes(storageStats.documents_size_bytes) : '...'}
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="bg-white dark:bg-black/40 p-3 rounded-lg border border-gray-200 dark:border-white/10 flex items-center gap-3">
|
<div class="bg-white dark:bg-black/40 p-3 rounded-lg border border-gray-200 dark:border-white/10 flex items-center gap-3">
|
||||||
<div class="w-3 h-3 rounded-full bg-purple-500"></div>
|
<div class="w-3 h-3 rounded-full bg-purple-500"></div>
|
||||||
<div>
|
<div>
|
||||||
<p class="text-gray-500 dark:text-gray-400">Images & Assets</p>
|
<p class="text-gray-500 dark:text-gray-400">Images & Assets</p>
|
||||||
<p class="font-semibold text-gray-900 dark:text-white">33 MB</p>
|
<p class="font-semibold text-gray-900 dark:text-white">
|
||||||
|
{storageStats ? formatBytes(storageStats.files_size_bytes) : '...'}
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user