Update 0.2.0
This commit is contained in:
21
LICENSE
Normal file
21
LICENSE
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2026 Sir Blob
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
||||||
136
README.md
136
README.md
@@ -4,21 +4,18 @@ Filaprint is a modern, premium web application designed to help 3D printing enth
|
|||||||
|
|
||||||

|

|
||||||
|
|
||||||
## 🛠️ Technology Stack
|
## Technology Stack
|
||||||
|
|
||||||
- **Framework:** SvelteKit (Svelte 5)
|
- **Framework:** SvelteKit
|
||||||
- **Language:** TypeScript
|
- **Language:** TypeScript
|
||||||
- **Styling:** Tailwind CSS v4 (Cerberus Theme)
|
- **Styling:** Tailwind CSS
|
||||||
- **State Management:** Svelte 5 Runes
|
|
||||||
- **Build Tool:** Vite
|
|
||||||
- **3D Rendering:** Three.js (STL & OBJ loaders)
|
- **3D Rendering:** Three.js (STL & OBJ loaders)
|
||||||
- **Data Visualization:** Chart.js
|
- **Data Visualization:** Chart.js
|
||||||
- **Icons:** Iconify (@iconify/svelte)
|
- **Icons:** Iconify (@iconify/svelte)
|
||||||
- **Database:** MongoDB with Mongoose
|
- **Database:** MongoDB
|
||||||
- **Authentication:** JWT with bcrypt password hashing
|
|
||||||
- **Container:** Docker with Docker Compose
|
- **Container:** Docker with Docker Compose
|
||||||
|
|
||||||
## ✨ Features
|
## Features
|
||||||
|
|
||||||
### 1. Dashboard
|
### 1. Dashboard
|
||||||
|
|
||||||
@@ -91,58 +88,7 @@ Filaprint is a modern, premium web application designed to help 3D printing enth
|
|||||||
- **Admin Panel:** Manage users (Admin role only).
|
- **Admin Panel:** Manage users (Admin role only).
|
||||||
- **Role-Based Access:** Admin and User roles with appropriate permissions.
|
- **Role-Based Access:** Admin and User roles with appropriate permissions.
|
||||||
|
|
||||||
## 🗂️ Data Models (Mongoose Schemas)
|
## Getting Started
|
||||||
|
|
||||||
### User Schema
|
|
||||||
|
|
||||||
- `_id`: ObjectId
|
|
||||||
- `username`: String (Required, Unique)
|
|
||||||
- `password`: String (Hashed with bcrypt)
|
|
||||||
- `role`: String (Enum: User, Admin)
|
|
||||||
- `location`: String
|
|
||||||
- `electricity_rate`: Number (Default: 0.12 $/kWh)
|
|
||||||
- `currency`: String (Default: USD)
|
|
||||||
- `createdAt`: Date
|
|
||||||
|
|
||||||
### Spool Schema
|
|
||||||
|
|
||||||
- `_id`: ObjectId
|
|
||||||
- `user_id`: ObjectId (Ref: User)
|
|
||||||
- `brand`: String (Required)
|
|
||||||
- `material`: String (Required, Enum: PLA, PETG, ABS, ASA, TPU, Other)
|
|
||||||
- `color_hex`: String (Default: #ffffff)
|
|
||||||
- `weight_initial_g`: Number (Required)
|
|
||||||
- `weight_remaining_g`: Number (Required)
|
|
||||||
- `price`: Number
|
|
||||||
- `purchased_at`: Date
|
|
||||||
- `is_active`: Boolean (Default: true)
|
|
||||||
|
|
||||||
### Printer Schema
|
|
||||||
|
|
||||||
- `_id`: ObjectId
|
|
||||||
- `user_id`: ObjectId (Ref: User)
|
|
||||||
- `name`: String (Required)
|
|
||||||
- `model`: String
|
|
||||||
- `nozzle_diameter_mm`: Number (Default: 0.4)
|
|
||||||
- `power_consumption_watts`: Number (Default: 0)
|
|
||||||
|
|
||||||
### PrintJob Schema
|
|
||||||
|
|
||||||
- `_id`: ObjectId
|
|
||||||
- `user_id`: ObjectId (Ref: User)
|
|
||||||
- `printer_id`: ObjectId (Ref: Printer)
|
|
||||||
- `spool_id`: ObjectId (Ref: Spool)
|
|
||||||
- `name`: String
|
|
||||||
- `duration_minutes`: Number
|
|
||||||
- `filament_used_g`: Number
|
|
||||||
- `calculated_cost_filament`: Number (Total cost including electricity)
|
|
||||||
- `calculated_cost_energy`: Number (Electricity cost only)
|
|
||||||
- `status`: String (Enum: Success, Fail, Cancelled, In Progress)
|
|
||||||
- `started_at`: Date (For In Progress jobs)
|
|
||||||
- `stl_file`: String (Path to uploaded 3D model)
|
|
||||||
- `date`: Date (Default: Date.now)
|
|
||||||
|
|
||||||
## 🚀 Getting Started
|
|
||||||
|
|
||||||
### Prerequisites
|
### Prerequisites
|
||||||
|
|
||||||
@@ -150,30 +96,6 @@ Filaprint is a modern, premium web application designed to help 3D printing enth
|
|||||||
- MongoDB instance (local or Atlas)
|
- MongoDB instance (local or Atlas)
|
||||||
- Docker (optional, for containerized deployment)
|
- Docker (optional, for containerized deployment)
|
||||||
|
|
||||||
### Local Development
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Clone the repository
|
|
||||||
git clone https://github.com/yourusername/filaprint.git
|
|
||||||
cd filaprint
|
|
||||||
|
|
||||||
# Install dependencies
|
|
||||||
bun install
|
|
||||||
|
|
||||||
# Set up environment variables
|
|
||||||
cp .env.example .env
|
|
||||||
# Edit .env with your MongoDB URI and JWT secret
|
|
||||||
|
|
||||||
# Run development server
|
|
||||||
bun run dev
|
|
||||||
|
|
||||||
# Build for production
|
|
||||||
bun run build
|
|
||||||
|
|
||||||
# Start production server
|
|
||||||
bun run start
|
|
||||||
```
|
|
||||||
|
|
||||||
### Docker Deployment
|
### Docker Deployment
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
@@ -208,40 +130,7 @@ MONGO_USER=admin
|
|||||||
MONGO_PASSWORD=changeme
|
MONGO_PASSWORD=changeme
|
||||||
```
|
```
|
||||||
|
|
||||||
## 📁 Project Structure
|
## Completed Features
|
||||||
|
|
||||||
```
|
|
||||||
filaprint/
|
|
||||||
├── src/
|
|
||||||
│ ├── lib/
|
|
||||||
│ │ ├── components/
|
|
||||||
│ │ │ ├── ui/ # Base UI components (Button, Card, Input, Modal)
|
|
||||||
│ │ │ ├── prints/ # Print-specific components (LogPrintModal, EditPrintModal)
|
|
||||||
│ │ │ ├── STLViewer.svelte # 3D model viewer (STL & OBJ)
|
|
||||||
│ │ │ └── Navbar.svelte
|
|
||||||
│ │ ├── models/ # Mongoose schemas
|
|
||||||
│ │ └── server/ # Server utilities (db connection, auth)
|
|
||||||
│ ├── routes/
|
|
||||||
│ │ ├── admin/users/ # Admin user management
|
|
||||||
│ │ ├── analytics/ # Analytics dashboard
|
|
||||||
│ │ ├── api/upload-stl/ # 3D model upload endpoint
|
|
||||||
│ │ ├── library/ # 3D model library
|
|
||||||
│ │ ├── login/ # Authentication
|
|
||||||
│ │ ├── printers/ # Printer management
|
|
||||||
│ │ ├── prints/ # Print job logging
|
|
||||||
│ │ ├── register/ # User registration
|
|
||||||
│ │ ├── settings/ # User settings
|
|
||||||
│ │ └── spools/ # Filament inventory
|
|
||||||
│ └── app.css # Global styles (Cerberus theme)
|
|
||||||
├── static/
|
|
||||||
│ └── uploads/models/ # Uploaded 3D model files
|
|
||||||
├── server/ # Production server
|
|
||||||
├── Dockerfile # Container build instructions
|
|
||||||
├── docker-compose.yml # Container orchestration
|
|
||||||
└── package.json
|
|
||||||
```
|
|
||||||
|
|
||||||
## ✅ Completed Features
|
|
||||||
|
|
||||||
- [x] User authentication (Login/Register)
|
- [x] User authentication (Login/Register)
|
||||||
- [x] Dashboard with live stats and active print tracking
|
- [x] Dashboard with live stats and active print tracking
|
||||||
@@ -263,18 +152,13 @@ filaprint/
|
|||||||
- [x] Responsive design
|
- [x] Responsive design
|
||||||
- [x] Docker containerization
|
- [x] Docker containerization
|
||||||
|
|
||||||
## 🔮 Future Enhancements
|
## Future Features
|
||||||
|
|
||||||
- [ ] QR/Barcode scanning for quick spool lookup
|
- [ ] QR/Barcode scanning for quick spool lookup
|
||||||
- [ ] Photo uploads for print jobs
|
|
||||||
- [ ] Export data (CSV/PDF reports)
|
|
||||||
- [ ] Multi-language support
|
- [ ] Multi-language support
|
||||||
- [ ] Dark/Light theme toggle
|
- [ ] Some notifications
|
||||||
- [ ] Email notifications
|
|
||||||
- [ ] Print job templates
|
|
||||||
- [ ] 3D printer integration (OctoPrint, Klipper)
|
|
||||||
- [ ] Thumbnail generation for 3D models
|
- [ ] Thumbnail generation for 3D models
|
||||||
|
|
||||||
## 📄 License
|
## License
|
||||||
|
|
||||||
MIT License - See LICENSE file for details.
|
MIT License - See LICENSE file for details.
|
||||||
|
|||||||
62
bun.lock
62
bun.lock
@@ -5,10 +5,12 @@
|
|||||||
"": {
|
"": {
|
||||||
"name": "filaprint",
|
"name": "filaprint",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@iconify/svelte": "^5.1.0",
|
"@iconify/svelte": "^5.2.1",
|
||||||
"@sveltejs/adapter-node": "^5.4.0",
|
"@sveltejs/adapter-node": "^5.5.1",
|
||||||
"@threlte/core": "^8.3.1",
|
"@threlte/core": "^8.3.1",
|
||||||
"@threlte/extras": "^9.7.1",
|
"@threlte/extras": "^9.7.1",
|
||||||
|
"@types/cors": "^2.8.19",
|
||||||
|
"@types/express": "^5.0.6",
|
||||||
"@types/three": "^0.182.0",
|
"@types/three": "^0.182.0",
|
||||||
"bcryptjs": "^3.0.3",
|
"bcryptjs": "^3.0.3",
|
||||||
"chart.js": "^4.5.1",
|
"chart.js": "^4.5.1",
|
||||||
@@ -17,24 +19,24 @@
|
|||||||
"dotenv": "^17.2.3",
|
"dotenv": "^17.2.3",
|
||||||
"express": "^5.2.1",
|
"express": "^5.2.1",
|
||||||
"jsonwebtoken": "^9.0.3",
|
"jsonwebtoken": "^9.0.3",
|
||||||
"mongoose": "^9.0.2",
|
"mongoose": "^9.1.4",
|
||||||
"three": "^0.182.0",
|
"three": "^0.182.0",
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@sveltejs/adapter-auto": "^7.0.0",
|
"@sveltejs/adapter-auto": "^7.0.0",
|
||||||
"@sveltejs/kit": "^2.49.1",
|
"@sveltejs/kit": "^2.50.0",
|
||||||
"@sveltejs/vite-plugin-svelte": "^6.2.1",
|
"@sveltejs/vite-plugin-svelte": "^6.2.4",
|
||||||
"@tailwindcss/forms": "^0.5.10",
|
"@tailwindcss/forms": "^0.5.11",
|
||||||
"@tailwindcss/typography": "^0.5.19",
|
"@tailwindcss/typography": "^0.5.19",
|
||||||
"@tailwindcss/vite": "^4.1.17",
|
"@tailwindcss/vite": "^4.1.18",
|
||||||
"@types/bcryptjs": "^3.0.0",
|
"@types/bcryptjs": "^3.0.0",
|
||||||
"@types/cookie": "^1.0.0",
|
"@types/cookie": "^1.0.0",
|
||||||
"@types/jsonwebtoken": "^9.0.10",
|
"@types/jsonwebtoken": "^9.0.10",
|
||||||
"svelte": "^5.45.6",
|
"svelte": "^5.46.4",
|
||||||
"svelte-check": "^4.3.4",
|
"svelte-check": "^4.3.5",
|
||||||
"tailwindcss": "^4.1.17",
|
"tailwindcss": "^4.1.18",
|
||||||
"typescript": "^5.9.3",
|
"typescript": "^5.9.3",
|
||||||
"vite": "^7.2.6",
|
"vite": "^7.3.1",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -93,7 +95,7 @@
|
|||||||
|
|
||||||
"@esbuild/win32-x64": ["@esbuild/win32-x64@0.27.2", "", { "os": "win32", "cpu": "x64" }, "sha512-sRdU18mcKf7F+YgheI/zGf5alZatMUTKj/jNS6l744f9u3WFu4v7twcUI9vu4mknF4Y9aDlblIie0IM+5xxaqQ=="],
|
"@esbuild/win32-x64": ["@esbuild/win32-x64@0.27.2", "", { "os": "win32", "cpu": "x64" }, "sha512-sRdU18mcKf7F+YgheI/zGf5alZatMUTKj/jNS6l744f9u3WFu4v7twcUI9vu4mknF4Y9aDlblIie0IM+5xxaqQ=="],
|
||||||
|
|
||||||
"@iconify/svelte": ["@iconify/svelte@5.1.0", "", { "dependencies": { "@iconify/types": "^2.0.0" }, "peerDependencies": { "svelte": ">4.0.0" } }, "sha512-I14nSqo0pNXO5OKsT61ZO3XIPF4yRHA2ErgPsaZ1sPJdKXn80o7o8jOe1xpWphbb9FihdX6by9zlKKBss61mFw=="],
|
"@iconify/svelte": ["@iconify/svelte@5.2.1", "", { "dependencies": { "@iconify/types": "^2.0.0" }, "peerDependencies": { "svelte": ">5.0.0" } }, "sha512-zHmsIPmnIhGd5gc95bNN5FL+GifwMZv7M2rlZEpa7IXYGFJm/XGHdWf6PWQa6OBoC+R69WyiPO9NAj5wjfjbow=="],
|
||||||
|
|
||||||
"@iconify/types": ["@iconify/types@2.0.0", "", {}, "sha512-+wluvCrRhXrhyOmRDJ3q8mux9JkKy5SJ/v8ol2tu4FVjyYvtEzkc/3pK15ET6RKg4b4w4BmTk1+gsCUhf21Ykg=="],
|
"@iconify/types": ["@iconify/types@2.0.0", "", {}, "sha512-+wluvCrRhXrhyOmRDJ3q8mux9JkKy5SJ/v8ol2tu4FVjyYvtEzkc/3pK15ET6RKg4b4w4BmTk1+gsCUhf21Ykg=="],
|
||||||
|
|
||||||
@@ -171,11 +173,11 @@
|
|||||||
|
|
||||||
"@sveltejs/adapter-auto": ["@sveltejs/adapter-auto@7.0.0", "", { "peerDependencies": { "@sveltejs/kit": "^2.0.0" } }, "sha512-ImDWaErTOCkRS4Gt+5gZuymKFBobnhChXUZ9lhUZLahUgvA4OOvRzi3sahzYgbxGj5nkA6OV0GAW378+dl/gyw=="],
|
"@sveltejs/adapter-auto": ["@sveltejs/adapter-auto@7.0.0", "", { "peerDependencies": { "@sveltejs/kit": "^2.0.0" } }, "sha512-ImDWaErTOCkRS4Gt+5gZuymKFBobnhChXUZ9lhUZLahUgvA4OOvRzi3sahzYgbxGj5nkA6OV0GAW378+dl/gyw=="],
|
||||||
|
|
||||||
"@sveltejs/adapter-node": ["@sveltejs/adapter-node@5.4.0", "", { "dependencies": { "@rollup/plugin-commonjs": "^28.0.1", "@rollup/plugin-json": "^6.1.0", "@rollup/plugin-node-resolve": "^16.0.0", "rollup": "^4.9.5" }, "peerDependencies": { "@sveltejs/kit": "^2.4.0" } }, "sha512-NMsrwGVPEn+J73zH83Uhss/hYYZN6zT3u31R3IHAn3MiKC3h8fjmIAhLfTSOeNHr5wPYfjjMg8E+1gyFgyrEcQ=="],
|
"@sveltejs/adapter-node": ["@sveltejs/adapter-node@5.5.1", "", { "dependencies": { "@rollup/plugin-commonjs": "^28.0.1", "@rollup/plugin-json": "^6.1.0", "@rollup/plugin-node-resolve": "^16.0.0", "rollup": "^4.9.5" }, "peerDependencies": { "@sveltejs/kit": "^2.4.0" } }, "sha512-VpZdPNRPQuZRtgfAMETPWWKpZx9JwXmUUsgz/+eSpw/Oh7+2O1uZHlsQTuyfxydJHPrRzjfu/ItcJjY4oscCiQ=="],
|
||||||
|
|
||||||
"@sveltejs/kit": ["@sveltejs/kit@2.49.2", "", { "dependencies": { "@standard-schema/spec": "^1.0.0", "@sveltejs/acorn-typescript": "^1.0.5", "@types/cookie": "^0.6.0", "acorn": "^8.14.1", "cookie": "^0.6.0", "devalue": "^5.3.2", "esm-env": "^1.2.2", "kleur": "^4.1.5", "magic-string": "^0.30.5", "mrmime": "^2.0.0", "sade": "^1.8.1", "set-cookie-parser": "^2.6.0", "sirv": "^3.0.0" }, "peerDependencies": { "@opentelemetry/api": "^1.0.0", "@sveltejs/vite-plugin-svelte": "^3.0.0 || ^4.0.0-next.1 || ^5.0.0 || ^6.0.0-next.0", "svelte": "^4.0.0 || ^5.0.0-next.0", "vite": "^5.0.3 || ^6.0.0 || ^7.0.0-beta.0" }, "optionalPeers": ["@opentelemetry/api"], "bin": { "svelte-kit": "svelte-kit.js" } }, "sha512-Vp3zX/qlwerQmHMP6x0Ry1oY7eKKRcOWGc2P59srOp4zcqyn+etJyQpELgOi4+ZSUgteX8Y387NuwruLgGXLUQ=="],
|
"@sveltejs/kit": ["@sveltejs/kit@2.50.0", "", { "dependencies": { "@standard-schema/spec": "^1.0.0", "@sveltejs/acorn-typescript": "^1.0.5", "@types/cookie": "^0.6.0", "acorn": "^8.14.1", "cookie": "^0.6.0", "devalue": "^5.6.2", "esm-env": "^1.2.2", "kleur": "^4.1.5", "magic-string": "^0.30.5", "mrmime": "^2.0.0", "sade": "^1.8.1", "set-cookie-parser": "^2.6.0", "sirv": "^3.0.0" }, "peerDependencies": { "@opentelemetry/api": "^1.0.0", "@sveltejs/vite-plugin-svelte": "^3.0.0 || ^4.0.0-next.1 || ^5.0.0 || ^6.0.0-next.0", "svelte": "^4.0.0 || ^5.0.0-next.0", "typescript": "^5.3.3", "vite": "^5.0.3 || ^6.0.0 || ^7.0.0-beta.0" }, "optionalPeers": ["@opentelemetry/api", "typescript"], "bin": { "svelte-kit": "svelte-kit.js" } }, "sha512-Hj8sR8O27p2zshFEIJzsvfhLzxga/hWw6tRLnBjMYw70m1aS9BSYCqAUtzDBjRREtX1EvLMYgaC0mYE3Hz4KWA=="],
|
||||||
|
|
||||||
"@sveltejs/vite-plugin-svelte": ["@sveltejs/vite-plugin-svelte@6.2.1", "", { "dependencies": { "@sveltejs/vite-plugin-svelte-inspector": "^5.0.0", "debug": "^4.4.1", "deepmerge": "^4.3.1", "magic-string": "^0.30.17", "vitefu": "^1.1.1" }, "peerDependencies": { "svelte": "^5.0.0", "vite": "^6.3.0 || ^7.0.0" } }, "sha512-YZs/OSKOQAQCnJvM/P+F1URotNnYNeU3P2s4oIpzm1uFaqUEqRxUB0g5ejMjEb5Gjb9/PiBI5Ktrq4rUUF8UVQ=="],
|
"@sveltejs/vite-plugin-svelte": ["@sveltejs/vite-plugin-svelte@6.2.4", "", { "dependencies": { "@sveltejs/vite-plugin-svelte-inspector": "^5.0.0", "deepmerge": "^4.3.1", "magic-string": "^0.30.21", "obug": "^2.1.0", "vitefu": "^1.1.1" }, "peerDependencies": { "svelte": "^5.0.0", "vite": "^6.3.0 || ^7.0.0" } }, "sha512-ou/d51QSdTyN26D7h6dSpusAKaZkAiGM55/AKYi+9AGZw7q85hElbjK3kEyzXHhLSnRISHOYzVge6x0jRZ7DXA=="],
|
||||||
|
|
||||||
"@sveltejs/vite-plugin-svelte-inspector": ["@sveltejs/vite-plugin-svelte-inspector@5.0.1", "", { "dependencies": { "debug": "^4.4.1" }, "peerDependencies": { "@sveltejs/vite-plugin-svelte": "^6.0.0-next.0", "svelte": "^5.0.0", "vite": "^6.3.0 || ^7.0.0" } }, "sha512-ubWshlMk4bc8mkwWbg6vNvCeT7lGQojE3ijDh3QTR6Zr/R+GXxsGbyH4PExEPpiFmqPhYiVSVmHBjUcVc1JIrA=="],
|
"@sveltejs/vite-plugin-svelte-inspector": ["@sveltejs/vite-plugin-svelte-inspector@5.0.1", "", { "dependencies": { "debug": "^4.4.1" }, "peerDependencies": { "@sveltejs/vite-plugin-svelte": "^6.0.0-next.0", "svelte": "^5.0.0", "vite": "^6.3.0 || ^7.0.0" } }, "sha512-ubWshlMk4bc8mkwWbg6vNvCeT7lGQojE3ijDh3QTR6Zr/R+GXxsGbyH4PExEPpiFmqPhYiVSVmHBjUcVc1JIrA=="],
|
||||||
|
|
||||||
@@ -223,18 +225,38 @@
|
|||||||
|
|
||||||
"@types/bcryptjs": ["@types/bcryptjs@3.0.0", "", { "dependencies": { "bcryptjs": "*" } }, "sha512-WRZOuCuaz8UcZZE4R5HXTco2goQSI2XxjGY3hbM/xDvwmqFWd4ivooImsMx65OKM6CtNKbnZ5YL+YwAwK7c1dg=="],
|
"@types/bcryptjs": ["@types/bcryptjs@3.0.0", "", { "dependencies": { "bcryptjs": "*" } }, "sha512-WRZOuCuaz8UcZZE4R5HXTco2goQSI2XxjGY3hbM/xDvwmqFWd4ivooImsMx65OKM6CtNKbnZ5YL+YwAwK7c1dg=="],
|
||||||
|
|
||||||
|
"@types/body-parser": ["@types/body-parser@1.19.6", "", { "dependencies": { "@types/connect": "*", "@types/node": "*" } }, "sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g=="],
|
||||||
|
|
||||||
|
"@types/connect": ["@types/connect@3.4.38", "", { "dependencies": { "@types/node": "*" } }, "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug=="],
|
||||||
|
|
||||||
"@types/cookie": ["@types/cookie@1.0.0", "", { "dependencies": { "cookie": "*" } }, "sha512-mGFXbkDQJ6kAXByHS7QAggRXgols0mAdP4MuXgloGY1tXokvzaFFM4SMqWvf7AH0oafI7zlFJwoGWzmhDqTZ9w=="],
|
"@types/cookie": ["@types/cookie@1.0.0", "", { "dependencies": { "cookie": "*" } }, "sha512-mGFXbkDQJ6kAXByHS7QAggRXgols0mAdP4MuXgloGY1tXokvzaFFM4SMqWvf7AH0oafI7zlFJwoGWzmhDqTZ9w=="],
|
||||||
|
|
||||||
|
"@types/cors": ["@types/cors@2.8.19", "", { "dependencies": { "@types/node": "*" } }, "sha512-mFNylyeyqN93lfe/9CSxOGREz8cpzAhH+E93xJ4xWQf62V8sQ/24reV2nyzUWM6H6Xji+GGHpkbLe7pVoUEskg=="],
|
||||||
|
|
||||||
"@types/estree": ["@types/estree@1.0.8", "", {}, "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w=="],
|
"@types/estree": ["@types/estree@1.0.8", "", {}, "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w=="],
|
||||||
|
|
||||||
|
"@types/express": ["@types/express@5.0.6", "", { "dependencies": { "@types/body-parser": "*", "@types/express-serve-static-core": "^5.0.0", "@types/serve-static": "^2" } }, "sha512-sKYVuV7Sv9fbPIt/442koC7+IIwK5olP1KWeD88e/idgoJqDm3JV/YUiPwkoKK92ylff2MGxSz1CSjsXelx0YA=="],
|
||||||
|
|
||||||
|
"@types/express-serve-static-core": ["@types/express-serve-static-core@5.1.1", "", { "dependencies": { "@types/node": "*", "@types/qs": "*", "@types/range-parser": "*", "@types/send": "*" } }, "sha512-v4zIMr/cX7/d2BpAEX3KNKL/JrT1s43s96lLvvdTmza1oEvDudCqK9aF/djc/SWgy8Yh0h30TZx5VpzqFCxk5A=="],
|
||||||
|
|
||||||
|
"@types/http-errors": ["@types/http-errors@2.0.5", "", {}, "sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg=="],
|
||||||
|
|
||||||
"@types/jsonwebtoken": ["@types/jsonwebtoken@9.0.10", "", { "dependencies": { "@types/ms": "*", "@types/node": "*" } }, "sha512-asx5hIG9Qmf/1oStypjanR7iKTv0gXQ1Ov/jfrX6kS/EO0OFni8orbmGCn0672NHR3kXHwpAwR+B368ZGN/2rA=="],
|
"@types/jsonwebtoken": ["@types/jsonwebtoken@9.0.10", "", { "dependencies": { "@types/ms": "*", "@types/node": "*" } }, "sha512-asx5hIG9Qmf/1oStypjanR7iKTv0gXQ1Ov/jfrX6kS/EO0OFni8orbmGCn0672NHR3kXHwpAwR+B368ZGN/2rA=="],
|
||||||
|
|
||||||
"@types/ms": ["@types/ms@2.1.0", "", {}, "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA=="],
|
"@types/ms": ["@types/ms@2.1.0", "", {}, "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA=="],
|
||||||
|
|
||||||
"@types/node": ["@types/node@25.0.3", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-W609buLVRVmeW693xKfzHeIV6nJGGz98uCPfeXI1ELMLXVeKYZ9m15fAMSaUPBHYLGFsVRcMmSCksQOrZV9BYA=="],
|
"@types/node": ["@types/node@25.0.3", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-W609buLVRVmeW693xKfzHeIV6nJGGz98uCPfeXI1ELMLXVeKYZ9m15fAMSaUPBHYLGFsVRcMmSCksQOrZV9BYA=="],
|
||||||
|
|
||||||
|
"@types/qs": ["@types/qs@6.14.0", "", {}, "sha512-eOunJqu0K1923aExK6y8p6fsihYEn/BYuQ4g0CxAAgFc4b/ZLN4CrsRZ55srTdqoiLzU2B2evC+apEIxprEzkQ=="],
|
||||||
|
|
||||||
|
"@types/range-parser": ["@types/range-parser@1.2.7", "", {}, "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ=="],
|
||||||
|
|
||||||
"@types/resolve": ["@types/resolve@1.20.2", "", {}, "sha512-60BCwRFOZCQhDncwQdxxeOEEkbc5dIMccYLwbxsS4TUNeVECQ/pBJ0j09mrHOl/JJvpRPGwO9SvE4nR2Nb/a4Q=="],
|
"@types/resolve": ["@types/resolve@1.20.2", "", {}, "sha512-60BCwRFOZCQhDncwQdxxeOEEkbc5dIMccYLwbxsS4TUNeVECQ/pBJ0j09mrHOl/JJvpRPGwO9SvE4nR2Nb/a4Q=="],
|
||||||
|
|
||||||
|
"@types/send": ["@types/send@1.2.1", "", { "dependencies": { "@types/node": "*" } }, "sha512-arsCikDvlU99zl1g69TcAB3mzZPpxgw0UQnaHeC1Nwb015xp8bknZv5rIfri9xTOcMuaVgvabfIRA7PSZVuZIQ=="],
|
||||||
|
|
||||||
|
"@types/serve-static": ["@types/serve-static@2.2.0", "", { "dependencies": { "@types/http-errors": "*", "@types/node": "*" } }, "sha512-8mam4H1NHLtu7nmtalF7eyBH14QyOASmcxHhSfEoRyr0nP/YdoesEtU+uSRvMe96TW/HPTtkoKqQLl53N7UXMQ=="],
|
||||||
|
|
||||||
"@types/stats.js": ["@types/stats.js@0.17.4", "", {}, "sha512-jIBvWWShCvlBqBNIZt0KAshWpvSjhkwkEu4ZUcASoAvhmrgAUI2t1dXrjSL4xXVLB4FznPrIsX3nKXFl/Dt4vA=="],
|
"@types/stats.js": ["@types/stats.js@0.17.4", "", {}, "sha512-jIBvWWShCvlBqBNIZt0KAshWpvSjhkwkEu4ZUcASoAvhmrgAUI2t1dXrjSL4xXVLB4FznPrIsX3nKXFl/Dt4vA=="],
|
||||||
|
|
||||||
"@types/three": ["@types/three@0.182.0", "", { "dependencies": { "@dimforge/rapier3d-compat": "~0.12.0", "@tweenjs/tween.js": "~23.1.3", "@types/stats.js": "*", "@types/webxr": ">=0.5.17", "@webgpu/types": "*", "fflate": "~0.8.2", "meshoptimizer": "~0.22.0" } }, "sha512-WByN9V3Sbwbe2OkWuSGyoqQO8Du6yhYaXtXLoA5FkKTUJorZ+yOHBZ35zUUPQXlAKABZmbYp5oAqpA4RBjtJ/Q=="],
|
"@types/three": ["@types/three@0.182.0", "", { "dependencies": { "@dimforge/rapier3d-compat": "~0.12.0", "@tweenjs/tween.js": "~23.1.3", "@types/stats.js": "*", "@types/webxr": ">=0.5.17", "@webgpu/types": "*", "fflate": "~0.8.2", "meshoptimizer": "~0.22.0" } }, "sha512-WByN9V3Sbwbe2OkWuSGyoqQO8Du6yhYaXtXLoA5FkKTUJorZ+yOHBZ35zUUPQXlAKABZmbYp5oAqpA4RBjtJ/Q=="],
|
||||||
@@ -301,7 +323,7 @@
|
|||||||
|
|
||||||
"detect-libc": ["detect-libc@2.1.2", "", {}, "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="],
|
"detect-libc": ["detect-libc@2.1.2", "", {}, "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="],
|
||||||
|
|
||||||
"devalue": ["devalue@5.6.1", "", {}, "sha512-jDwizj+IlEZBunHcOuuFVBnIMPAEHvTsJj0BcIp94xYguLRVBcXO853px/MyIJvbVzWdsGvrRweIUWJw8hBP7A=="],
|
"devalue": ["devalue@5.6.2", "", {}, "sha512-nPRkjWzzDQlsejL1WVifk5rvcFi/y1onBRxjaFMjZeR9mFpqu2gmAZ9xUB9/IEanEP/vBtGeGganC/GO1fmufg=="],
|
||||||
|
|
||||||
"diet-sprite": ["diet-sprite@0.0.1", "", {}, "sha512-zSHI2WDAn1wJqJYxcmjWfJv3Iw8oL9reQIbEyx2x2/EZ4/qmUTIo8/5qOCurnAcq61EwtJJaZ0XTy2NRYqpB5A=="],
|
"diet-sprite": ["diet-sprite@0.0.1", "", {}, "sha512-zSHI2WDAn1wJqJYxcmjWfJv3Iw8oL9reQIbEyx2x2/EZ4/qmUTIo8/5qOCurnAcq61EwtJJaZ0XTy2NRYqpB5A=="],
|
||||||
|
|
||||||
@@ -459,7 +481,7 @@
|
|||||||
|
|
||||||
"mongodb-connection-string-url": ["mongodb-connection-string-url@7.0.0", "", { "dependencies": { "@types/whatwg-url": "^13.0.0", "whatwg-url": "^14.1.0" } }, "sha512-irhhjRVLE20hbkRl4zpAYLnDMM+zIZnp0IDB9akAFFUZp/3XdOfwwddc7y6cNvF2WCEtfTYRwYbIfYa2kVY0og=="],
|
"mongodb-connection-string-url": ["mongodb-connection-string-url@7.0.0", "", { "dependencies": { "@types/whatwg-url": "^13.0.0", "whatwg-url": "^14.1.0" } }, "sha512-irhhjRVLE20hbkRl4zpAYLnDMM+zIZnp0IDB9akAFFUZp/3XdOfwwddc7y6cNvF2WCEtfTYRwYbIfYa2kVY0og=="],
|
||||||
|
|
||||||
"mongoose": ["mongoose@9.0.2", "", { "dependencies": { "kareem": "3.0.0", "mongodb": "~7.0", "mpath": "0.9.0", "mquery": "6.0.0", "ms": "2.1.3", "sift": "17.1.3" } }, "sha512-+GCaqwE+X//yN9eo2M2L/n+mVti9J6vH5iQKbhD+2AArZd5iaZqK/DkmkE4S6/iYYMyVQPTXsRk7jyVOYEtJzA=="],
|
"mongoose": ["mongoose@9.1.4", "", { "dependencies": { "kareem": "3.0.0", "mongodb": "~7.0", "mpath": "0.9.0", "mquery": "6.0.0", "ms": "2.1.3", "sift": "17.1.3" } }, "sha512-V8JIyoWKWW+R2COlOsh6gaYw9TvczSiP/cN3Yuk1pv7ws5VNFAy5GPrK8jfz9tVYovmqdWOJRurMjL4ilYn9wA=="],
|
||||||
|
|
||||||
"mpath": ["mpath@0.9.0", "", {}, "sha512-ikJRQTk8hw5DEoFVxHG1Gn9T/xcjtdnOKIU1JTmGjZZlg9LST2mBLmcX3/ICIbgJydT2GOc15RnNy5mHmzfSew=="],
|
"mpath": ["mpath@0.9.0", "", {}, "sha512-ikJRQTk8hw5DEoFVxHG1Gn9T/xcjtdnOKIU1JTmGjZZlg9LST2mBLmcX3/ICIbgJydT2GOc15RnNy5mHmzfSew=="],
|
||||||
|
|
||||||
@@ -479,6 +501,8 @@
|
|||||||
|
|
||||||
"object-inspect": ["object-inspect@1.13.4", "", {}, "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew=="],
|
"object-inspect": ["object-inspect@1.13.4", "", {}, "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew=="],
|
||||||
|
|
||||||
|
"obug": ["obug@2.1.1", "", {}, "sha512-uTqF9MuPraAQ+IsnPf366RG4cP9RtUi7MLO1N3KEc+wb0a6yKpeL0lmk2IB1jY5KHPAlTc6T/JRdC/YqxHNwkQ=="],
|
||||||
|
|
||||||
"on-finished": ["on-finished@2.4.1", "", { "dependencies": { "ee-first": "1.1.1" } }, "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg=="],
|
"on-finished": ["on-finished@2.4.1", "", { "dependencies": { "ee-first": "1.1.1" } }, "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg=="],
|
||||||
|
|
||||||
"once": ["once@1.4.0", "", { "dependencies": { "wrappy": "1" } }, "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w=="],
|
"once": ["once@1.4.0", "", { "dependencies": { "wrappy": "1" } }, "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w=="],
|
||||||
@@ -553,7 +577,7 @@
|
|||||||
|
|
||||||
"supports-preserve-symlinks-flag": ["supports-preserve-symlinks-flag@1.0.0", "", {}, "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w=="],
|
"supports-preserve-symlinks-flag": ["supports-preserve-symlinks-flag@1.0.0", "", {}, "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w=="],
|
||||||
|
|
||||||
"svelte": ["svelte@5.46.1", "", { "dependencies": { "@jridgewell/remapping": "^2.3.4", "@jridgewell/sourcemap-codec": "^1.5.0", "@sveltejs/acorn-typescript": "^1.0.5", "@types/estree": "^1.0.5", "acorn": "^8.12.1", "aria-query": "^5.3.1", "axobject-query": "^4.1.0", "clsx": "^2.1.1", "devalue": "^5.5.0", "esm-env": "^1.2.1", "esrap": "^2.2.1", "is-reference": "^3.0.3", "locate-character": "^3.0.0", "magic-string": "^0.30.11", "zimmerframe": "^1.1.2" } }, "sha512-ynjfCHD3nP2el70kN5Pmg37sSi0EjOm9FgHYQdC4giWG/hzO3AatzXXJJgP305uIhGQxSufJLuYWtkY8uK/8RA=="],
|
"svelte": ["svelte@5.46.4", "", { "dependencies": { "@jridgewell/remapping": "^2.3.4", "@jridgewell/sourcemap-codec": "^1.5.0", "@sveltejs/acorn-typescript": "^1.0.5", "@types/estree": "^1.0.5", "acorn": "^8.12.1", "aria-query": "^5.3.1", "axobject-query": "^4.1.0", "clsx": "^2.1.1", "devalue": "^5.6.2", "esm-env": "^1.2.1", "esrap": "^2.2.1", "is-reference": "^3.0.3", "locate-character": "^3.0.0", "magic-string": "^0.30.11", "zimmerframe": "^1.1.2" } }, "sha512-VJwdXrmv9L8L7ZasJeWcCjoIuMRVbhuxbss0fpVnR8yorMmjNDwcjIH08vS6wmSzzzgAG5CADQ1JuXPS2nwt9w=="],
|
||||||
|
|
||||||
"svelte-check": ["svelte-check@4.3.5", "", { "dependencies": { "@jridgewell/trace-mapping": "^0.3.25", "chokidar": "^4.0.1", "fdir": "^6.2.0", "picocolors": "^1.0.0", "sade": "^1.7.4" }, "peerDependencies": { "svelte": "^4.0.0 || ^5.0.0-next.0", "typescript": ">=5.0.0" }, "bin": { "svelte-check": "bin/svelte-check" } }, "sha512-e4VWZETyXaKGhpkxOXP+B/d0Fp/zKViZoJmneZWe/05Y2aqSKj3YN2nLfYPJBQ87WEiY4BQCQ9hWGu9mPT1a1Q=="],
|
"svelte-check": ["svelte-check@4.3.5", "", { "dependencies": { "@jridgewell/trace-mapping": "^0.3.25", "chokidar": "^4.0.1", "fdir": "^6.2.0", "picocolors": "^1.0.0", "sade": "^1.7.4" }, "peerDependencies": { "svelte": "^4.0.0 || ^5.0.0-next.0", "typescript": ">=5.0.0" }, "bin": { "svelte-check": "bin/svelte-check" } }, "sha512-e4VWZETyXaKGhpkxOXP+B/d0Fp/zKViZoJmneZWe/05Y2aqSKj3YN2nLfYPJBQ87WEiY4BQCQ9hWGu9mPT1a1Q=="],
|
||||||
|
|
||||||
@@ -599,7 +623,7 @@
|
|||||||
|
|
||||||
"vary": ["vary@1.1.2", "", {}, "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg=="],
|
"vary": ["vary@1.1.2", "", {}, "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg=="],
|
||||||
|
|
||||||
"vite": ["vite@7.3.0", "", { "dependencies": { "esbuild": "^0.27.0", "fdir": "^6.5.0", "picomatch": "^4.0.3", "postcss": "^8.5.6", "rollup": "^4.43.0", "tinyglobby": "^0.2.15" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "jiti": ">=1.21.0", "less": "^4.0.0", "lightningcss": "^1.21.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-dZwN5L1VlUBewiP6H9s2+B3e3Jg96D0vzN+Ry73sOefebhYr9f94wwkMNN/9ouoU8pV1BqA1d1zGk8928cx0rg=="],
|
"vite": ["vite@7.3.1", "", { "dependencies": { "esbuild": "^0.27.0", "fdir": "^6.5.0", "picomatch": "^4.0.3", "postcss": "^8.5.6", "rollup": "^4.43.0", "tinyglobby": "^0.2.15" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "jiti": ">=1.21.0", "less": "^4.0.0", "lightningcss": "^1.21.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA=="],
|
||||||
|
|
||||||
"vitefu": ["vitefu@1.1.1", "", { "peerDependencies": { "vite": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0-beta.0" }, "optionalPeers": ["vite"] }, "sha512-B/Fegf3i8zh0yFbpzZ21amWzHmuNlLlmJT6n7bu5e+pCHUKQIfXSYokrqOBGEMMe9UG2sostKQF9mml/vYaWJQ=="],
|
"vitefu": ["vitefu@1.1.1", "", { "peerDependencies": { "vite": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0-beta.0" }, "optionalPeers": ["vite"] }, "sha512-B/Fegf3i8zh0yFbpzZ21amWzHmuNlLlmJT6n7bu5e+pCHUKQIfXSYokrqOBGEMMe9UG2sostKQF9mml/vYaWJQ=="],
|
||||||
|
|
||||||
|
|||||||
28
package.json
28
package.json
@@ -1,34 +1,36 @@
|
|||||||
{
|
{
|
||||||
"name": "filaprint",
|
"name": "filaprint",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "0.0.1",
|
"version": "0.2.0",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite dev --host",
|
"dev": "vite dev --host",
|
||||||
"build": "vite build",
|
"build": "vite build",
|
||||||
"start": "node server/server.js"
|
"start": "bun server/server.ts"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@sveltejs/adapter-auto": "^7.0.0",
|
"@sveltejs/adapter-auto": "^7.0.0",
|
||||||
"@sveltejs/kit": "^2.49.1",
|
"@sveltejs/kit": "^2.50.0",
|
||||||
"@sveltejs/vite-plugin-svelte": "^6.2.1",
|
"@sveltejs/vite-plugin-svelte": "^6.2.4",
|
||||||
"@tailwindcss/forms": "^0.5.10",
|
"@tailwindcss/forms": "^0.5.11",
|
||||||
"@tailwindcss/typography": "^0.5.19",
|
"@tailwindcss/typography": "^0.5.19",
|
||||||
"@tailwindcss/vite": "^4.1.17",
|
"@tailwindcss/vite": "^4.1.18",
|
||||||
"@types/bcryptjs": "^3.0.0",
|
"@types/bcryptjs": "^3.0.0",
|
||||||
"@types/cookie": "^1.0.0",
|
"@types/cookie": "^1.0.0",
|
||||||
"@types/jsonwebtoken": "^9.0.10",
|
"@types/jsonwebtoken": "^9.0.10",
|
||||||
"svelte": "^5.45.6",
|
"svelte": "^5.46.4",
|
||||||
"svelte-check": "^4.3.4",
|
"svelte-check": "^4.3.5",
|
||||||
"tailwindcss": "^4.1.17",
|
"tailwindcss": "^4.1.18",
|
||||||
"typescript": "^5.9.3",
|
"typescript": "^5.9.3",
|
||||||
"vite": "^7.2.6"
|
"vite": "^7.3.1"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@iconify/svelte": "^5.1.0",
|
"@iconify/svelte": "^5.2.1",
|
||||||
"@sveltejs/adapter-node": "^5.4.0",
|
"@sveltejs/adapter-node": "^5.5.1",
|
||||||
"@threlte/core": "^8.3.1",
|
"@threlte/core": "^8.3.1",
|
||||||
"@threlte/extras": "^9.7.1",
|
"@threlte/extras": "^9.7.1",
|
||||||
|
"@types/cors": "^2.8.19",
|
||||||
|
"@types/express": "^5.0.6",
|
||||||
"@types/three": "^0.182.0",
|
"@types/three": "^0.182.0",
|
||||||
"bcryptjs": "^3.0.3",
|
"bcryptjs": "^3.0.3",
|
||||||
"chart.js": "^4.5.1",
|
"chart.js": "^4.5.1",
|
||||||
@@ -37,7 +39,7 @@
|
|||||||
"dotenv": "^17.2.3",
|
"dotenv": "^17.2.3",
|
||||||
"express": "^5.2.1",
|
"express": "^5.2.1",
|
||||||
"jsonwebtoken": "^9.0.3",
|
"jsonwebtoken": "^9.0.3",
|
||||||
"mongoose": "^9.0.2",
|
"mongoose": "^9.1.4",
|
||||||
"three": "^0.182.0"
|
"three": "^0.182.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -3,9 +3,9 @@ import dotenv from 'dotenv';
|
|||||||
|
|
||||||
dotenv.config();
|
dotenv.config();
|
||||||
|
|
||||||
const MONGODB_URI = process.env.MONGODB_URI || 'mongodb://localhost:27017/filaprint';
|
const MONGODB_URI: string = process.env.MONGODB_URI || 'mongodb://localhost:27017/filaprint';
|
||||||
|
|
||||||
export const connectDB = async () => {
|
export const connectDB = async (): Promise<void> => {
|
||||||
try {
|
try {
|
||||||
await mongoose.connect(MONGODB_URI);
|
await mongoose.connect(MONGODB_URI);
|
||||||
console.log('MongoDB connected successfully');
|
console.log('MongoDB connected successfully');
|
||||||
@@ -1,14 +1,15 @@
|
|||||||
|
// @ts-ignore
|
||||||
import { handler } from '../build/handler.js';
|
import { handler } from '../build/handler.js';
|
||||||
import dotenv from 'dotenv';
|
import dotenv from 'dotenv';
|
||||||
import http from 'http';
|
import http from 'http';
|
||||||
import express from 'express';
|
import express from 'express';
|
||||||
import cors from 'cors';
|
import cors from 'cors';
|
||||||
import { connectDB } from './db.js';
|
import { connectDB } from './db';
|
||||||
|
|
||||||
dotenv.config();
|
dotenv.config();
|
||||||
|
|
||||||
const app = express();
|
const app = express();
|
||||||
const server = http.Server(app);
|
const server = http.createServer(app);
|
||||||
|
|
||||||
app.use(cors());
|
app.use(cors());
|
||||||
|
|
||||||
@@ -123,6 +123,23 @@
|
|||||||
|
|
||||||
// Track if user wants to remove the model
|
// Track if user wants to remove the model
|
||||||
let removeModel = $state(false);
|
let removeModel = $state(false);
|
||||||
|
|
||||||
|
// Elapsed time state for In Progress prints
|
||||||
|
let elapsedHours = $state(0);
|
||||||
|
let elapsedMins = $state(0);
|
||||||
|
|
||||||
|
$effect(() => {
|
||||||
|
if (print && print.status === "In Progress" && print.started_at) {
|
||||||
|
const start = new Date(print.started_at).getTime();
|
||||||
|
const now = Date.now();
|
||||||
|
const diffMins = Math.max(0, Math.floor((now - start) / 60000));
|
||||||
|
elapsedHours = Math.floor(diffMins / 60);
|
||||||
|
elapsedMins = diffMins % 60;
|
||||||
|
} else {
|
||||||
|
elapsedHours = 0;
|
||||||
|
elapsedMins = 0;
|
||||||
|
}
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<Modal title="Edit Print Log" {open} onclose={handleClose}>
|
<Modal title="Edit Print Log" {open} onclose={handleClose}>
|
||||||
@@ -495,7 +512,7 @@
|
|||||||
name="elapsed_hours"
|
name="elapsed_hours"
|
||||||
placeholder="0"
|
placeholder="0"
|
||||||
min="0"
|
min="0"
|
||||||
value="0"
|
bind:value={elapsedHours}
|
||||||
class="w-full rounded-lg bg-slate-800/50 border border-slate-700 px-4 py-2.5 text-sm text-slate-100 focus:border-blue-500 focus:outline-none pr-8"
|
class="w-full rounded-lg bg-slate-800/50 border border-slate-700 px-4 py-2.5 text-sm text-slate-100 focus:border-blue-500 focus:outline-none pr-8"
|
||||||
/>
|
/>
|
||||||
<span
|
<span
|
||||||
@@ -510,7 +527,7 @@
|
|||||||
placeholder="0"
|
placeholder="0"
|
||||||
min="0"
|
min="0"
|
||||||
max="59"
|
max="59"
|
||||||
value="0"
|
bind:value={elapsedMins}
|
||||||
class="w-full rounded-lg bg-slate-800/50 border border-slate-700 px-4 py-2.5 text-sm text-slate-100 focus:border-blue-500 focus:outline-none pr-10"
|
class="w-full rounded-lg bg-slate-800/50 border border-slate-700 px-4 py-2.5 text-sm text-slate-100 focus:border-blue-500 focus:outline-none pr-10"
|
||||||
/>
|
/>
|
||||||
<span
|
<span
|
||||||
|
|||||||
@@ -10,9 +10,10 @@
|
|||||||
printers: any[];
|
printers: any[];
|
||||||
spools: any[];
|
spools: any[];
|
||||||
onclose: () => void;
|
onclose: () => void;
|
||||||
|
action?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
let { open, printers, spools, onclose }: Props = $props();
|
let { open, printers, spools, onclose, action = "?/log" }: Props = $props();
|
||||||
let isSubmitting = $state(false);
|
let isSubmitting = $state(false);
|
||||||
let selectedStatus = $state("Success");
|
let selectedStatus = $state("Success");
|
||||||
let stlFile = $state<File | null>(null);
|
let stlFile = $state<File | null>(null);
|
||||||
@@ -85,7 +86,7 @@
|
|||||||
<Modal title="Log a Print" {open} onclose={handleClose}>
|
<Modal title="Log a Print" {open} onclose={handleClose}>
|
||||||
<form
|
<form
|
||||||
method="POST"
|
method="POST"
|
||||||
action="?/log"
|
{action}
|
||||||
use:enhance={async ({ formData }) => {
|
use:enhance={async ({ formData }) => {
|
||||||
isSubmitting = true;
|
isSubmitting = true;
|
||||||
|
|
||||||
@@ -296,8 +297,11 @@
|
|||||||
name="printer_id"
|
name="printer_id"
|
||||||
class="w-full rounded-lg bg-slate-800/50 border border-slate-700 px-4 py-2.5 text-sm text-slate-100 focus:border-blue-500 focus:outline-none"
|
class="w-full rounded-lg bg-slate-800/50 border border-slate-700 px-4 py-2.5 text-sm text-slate-100 focus:border-blue-500 focus:outline-none"
|
||||||
>
|
>
|
||||||
|
<option value="" disabled selected>Select a printer</option>
|
||||||
{#each printers as p}
|
{#each printers as p}
|
||||||
<option value={p._id}>{p.name}</option>
|
<option value={p._id}>{p.name}</option>
|
||||||
|
{:else}
|
||||||
|
<option value="" disabled>No printers found</option>
|
||||||
{/each}
|
{/each}
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
@@ -311,11 +315,14 @@
|
|||||||
name="spool_id"
|
name="spool_id"
|
||||||
class="w-full rounded-lg bg-slate-800/50 border border-slate-700 px-4 py-2.5 text-sm text-slate-100 focus:border-blue-500 focus:outline-none"
|
class="w-full rounded-lg bg-slate-800/50 border border-slate-700 px-4 py-2.5 text-sm text-slate-100 focus:border-blue-500 focus:outline-none"
|
||||||
>
|
>
|
||||||
|
<option value="" disabled selected>Select a spool</option>
|
||||||
{#each spools as s}
|
{#each spools as s}
|
||||||
<option value={s._id}
|
<option value={s._id}
|
||||||
>{s.brand}
|
>{s.brand}
|
||||||
{s.material} ({s.weight_remaining_g}g left)</option
|
{s.material} ({s.weight_remaining_g}g left)</option
|
||||||
>
|
>
|
||||||
|
{:else}
|
||||||
|
<option value="" disabled>No spools found</option>
|
||||||
{/each}
|
{/each}
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,54 +1,54 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { fade, scale } from "svelte/transition";
|
import { fade, scale } from "svelte/transition";
|
||||||
import type { Snippet } from "svelte";
|
import type { Snippet } from "svelte";
|
||||||
import Icon from "@iconify/svelte";
|
import Icon from "@iconify/svelte";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
open: boolean;
|
open: boolean;
|
||||||
title: string;
|
title: string;
|
||||||
children: Snippet;
|
children: Snippet;
|
||||||
onclose: () => void;
|
onclose: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
let { open, title, children, onclose }: Props = $props();
|
let { open, title, children, onclose }: Props = $props();
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#if open}
|
{#if open}
|
||||||
<div
|
<div
|
||||||
class="fixed inset-0 z-50 flex items-center justify-center p-4"
|
class="fixed inset-0 z-50 flex items-center justify-center p-4"
|
||||||
transition:fade={{ duration: 150 }}
|
transition:fade={{ duration: 150 }}
|
||||||
>
|
>
|
||||||
<!-- Backdrop -->
|
<!-- Backdrop -->
|
||||||
<!-- svelte-ignore a11y_click_events_have_key_events -->
|
<!-- svelte-ignore a11y_click_events_have_key_events -->
|
||||||
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
||||||
<div class="fixed inset-0 bg-black/70" onclick={onclose}></div>
|
<div class="fixed inset-0 bg-black/70" onclick={onclose}></div>
|
||||||
|
|
||||||
<!-- Dialog Panel -->
|
<!-- Dialog Panel -->
|
||||||
<div
|
<div
|
||||||
class="relative z-10 w-full max-w-lg rounded-xl border border-[#3f3f46] shadow-2xl"
|
class="relative z-10 w-full max-w-lg rounded-xl border border-[#3f3f46] shadow-2xl flex flex-col max-h-[85vh] overflow-hidden"
|
||||||
style="background-color: #18181b;"
|
style="background-color: #18181b;"
|
||||||
transition:scale={{ duration: 150, start: 0.95 }}
|
transition:scale={{ duration: 150, start: 0.95 }}
|
||||||
>
|
>
|
||||||
<!-- Header -->
|
<!-- Header -->
|
||||||
<div
|
<div
|
||||||
class="flex items-center justify-between border-b border-[#3f3f46] px-6 py-4"
|
class="flex items-center justify-between border-b border-[#3f3f46] px-6 py-4 shrink-0"
|
||||||
style="background-color: #27272a;"
|
style="background-color: #27272a;"
|
||||||
>
|
>
|
||||||
<h3 class="text-lg font-semibold text-white">{title}</h3>
|
<h3 class="text-lg font-semibold text-white">{title}</h3>
|
||||||
<button
|
<button
|
||||||
onclick={onclose}
|
onclick={onclose}
|
||||||
class="rounded p-1 text-[#a1a1aa] transition-colors hover:bg-[#3f3f46] hover:text-white"
|
class="rounded p-1 text-[#a1a1aa] transition-colors hover:bg-[#3f3f46] hover:text-white"
|
||||||
aria-label="Close"
|
aria-label="Close"
|
||||||
type="button"
|
type="button"
|
||||||
>
|
>
|
||||||
<Icon icon="mdi:close" class="h-5 w-5" />
|
<Icon icon="mdi:close" class="h-5 w-5" />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Content -->
|
<!-- Content -->
|
||||||
<div class="p-6">
|
<div class="p-6 overflow-y-auto">
|
||||||
{@render children()}
|
{@render children()}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|||||||
@@ -13,12 +13,13 @@ export const load: PageServerLoad = async ({ locals }) => {
|
|||||||
const userId = locals.user.id;
|
const userId = locals.user.id;
|
||||||
|
|
||||||
// Run in parallel - all filtered by user
|
// Run in parallel - all filtered by user
|
||||||
const [spoolCount, spools, printerCount, recentPrints, activePrinter, activePrintJob, completedPrints] = await Promise.all([
|
const [spoolCount, spools, printerCount, recentPrints, activePrinter, printers, activePrintJob, completedPrints] = await Promise.all([
|
||||||
Spool.countDocuments({ user_id: userId, is_active: true }),
|
Spool.countDocuments({ user_id: userId, is_active: true }),
|
||||||
Spool.find({ user_id: userId, is_active: true }).lean(),
|
Spool.find({ user_id: userId, is_active: true }).lean(),
|
||||||
Printer.countDocuments({ user_id: userId }),
|
Printer.countDocuments({ user_id: userId }),
|
||||||
PrintJob.find({ user_id: userId }).sort({ date: -1 }).limit(5).populate('printer_id', 'name').populate('spool_id', 'brand color_hex').lean(),
|
PrintJob.find({ user_id: userId }).sort({ date: -1, _id: -1 }).limit(5).populate('printer_id', 'name').populate('spool_id', 'brand color_hex').lean(),
|
||||||
Printer.findOne({ user_id: userId }).lean(),
|
Printer.findOne({ user_id: userId }).lean(),
|
||||||
|
Printer.find({ user_id: userId }).lean(), // Fetch all printers for dropdowns
|
||||||
PrintJob.findOne({ user_id: userId, status: 'In Progress' }).populate('printer_id', 'name').populate('spool_id', 'brand color_hex material').lean(),
|
PrintJob.findOne({ user_id: userId, status: 'In Progress' }).populate('printer_id', 'name').populate('spool_id', 'brand color_hex material').lean(),
|
||||||
PrintJob.find({ user_id: userId, status: { $ne: 'In Progress' } }).select('calculated_cost_filament').lean()
|
PrintJob.find({ user_id: userId, status: { $ne: 'In Progress' } }).select('calculated_cost_filament').lean()
|
||||||
]);
|
]);
|
||||||
@@ -60,6 +61,8 @@ export const load: PageServerLoad = async ({ locals }) => {
|
|||||||
},
|
},
|
||||||
recentPrints: JSON.parse(JSON.stringify(recentPrints)),
|
recentPrints: JSON.parse(JSON.stringify(recentPrints)),
|
||||||
activePrinter: activePrinter ? JSON.parse(JSON.stringify(activePrinter)) : null,
|
activePrinter: activePrinter ? JSON.parse(JSON.stringify(activePrinter)) : null,
|
||||||
activePrintJob: activePrintJob ? JSON.parse(JSON.stringify(activePrintJob)) : null
|
activePrintJob: activePrintJob ? JSON.parse(JSON.stringify(activePrintJob)) : null,
|
||||||
|
spools: JSON.parse(JSON.stringify(spools)),
|
||||||
|
printers: JSON.parse(JSON.stringify(printers))
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,426 +1,517 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import Card from "$lib/components/ui/Card.svelte";
|
import Card from "$lib/components/ui/Card.svelte";
|
||||||
import Button from "$lib/components/ui/Button.svelte";
|
import Button from "$lib/components/ui/Button.svelte";
|
||||||
import Icon from "@iconify/svelte";
|
import Icon from "@iconify/svelte";
|
||||||
import { onMount, onDestroy } from "svelte";
|
import { onMount, onDestroy } from "svelte";
|
||||||
import { browser } from "$app/environment";
|
import { browser } from "$app/environment";
|
||||||
|
|
||||||
let { data } = $props();
|
import LogPrintModal from "$lib/components/prints/LogPrintModal.svelte";
|
||||||
// svelte-ignore non_reactive_update
|
|
||||||
let stats = $derived(data.stats);
|
|
||||||
// svelte-ignore non_reactive_update
|
|
||||||
let recentPrints = $derived(data.recentPrints || []);
|
|
||||||
// svelte-ignore non_reactive_update
|
|
||||||
let activePrinter = $derived(data.activePrinter);
|
|
||||||
// svelte-ignore non_reactive_update
|
|
||||||
let activePrintJob = $derived(data.activePrintJob);
|
|
||||||
|
|
||||||
// Timer state
|
let { data } = $props();
|
||||||
let currentTime = $state(Date.now());
|
// svelte-ignore non_reactive_update
|
||||||
let timerInterval: ReturnType<typeof setInterval>;
|
let stats = $derived(data.stats);
|
||||||
let notificationSent = $state(false);
|
// svelte-ignore non_reactive_update
|
||||||
|
let recentPrints = $derived(data.recentPrints || []);
|
||||||
|
// svelte-ignore non_reactive_update
|
||||||
|
let activePrinter = $derived(data.activePrinter);
|
||||||
|
// svelte-ignore non_reactive_update
|
||||||
|
let activePrintJob = $derived(data.activePrintJob);
|
||||||
|
// svelte-ignore non_reactive_update
|
||||||
|
let spools = $derived(data.spools || []);
|
||||||
|
// svelte-ignore non_reactive_update
|
||||||
|
let printers = $derived(data.printers || []);
|
||||||
|
|
||||||
onMount(() => {
|
let showQuickLogModal = $state(false);
|
||||||
// Request notification permission
|
|
||||||
if (
|
|
||||||
browser &&
|
|
||||||
"Notification" in window &&
|
|
||||||
Notification.permission === "default"
|
|
||||||
) {
|
|
||||||
Notification.requestPermission();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update timer every 10 seconds
|
// Timer state
|
||||||
timerInterval = setInterval(() => {
|
let currentTime = $state(Date.now());
|
||||||
currentTime = Date.now();
|
let timerInterval: ReturnType<typeof setInterval>;
|
||||||
|
let notificationSent = $state(false);
|
||||||
|
|
||||||
// Check if print is complete
|
// ... (rest of onMount/onDestroy/notifications - keeping as is)
|
||||||
if (activePrintJob && !notificationSent) {
|
onMount(() => {
|
||||||
const progress = getProgress(
|
// Request notification permission
|
||||||
activePrintJob.started_at,
|
if (
|
||||||
activePrintJob.duration_minutes
|
browser &&
|
||||||
);
|
"Notification" in window &&
|
||||||
if (progress >= 100) {
|
Notification.permission === "default"
|
||||||
sendNotification();
|
) {
|
||||||
notificationSent = true;
|
Notification.requestPermission();
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}, 10000);
|
|
||||||
});
|
|
||||||
|
|
||||||
onDestroy(() => {
|
// Update timer every 10 seconds
|
||||||
if (timerInterval) clearInterval(timerInterval);
|
timerInterval = setInterval(() => {
|
||||||
});
|
currentTime = Date.now();
|
||||||
|
|
||||||
function sendNotification() {
|
// Check if print is complete
|
||||||
if (
|
if (activePrintJob && !notificationSent) {
|
||||||
browser &&
|
const progress = getProgress(
|
||||||
"Notification" in window &&
|
activePrintJob.started_at,
|
||||||
Notification.permission === "granted"
|
activePrintJob.duration_minutes,
|
||||||
) {
|
);
|
||||||
new Notification("🎉 Print Complete!", {
|
if (progress >= 100) {
|
||||||
body: `Your print "${activePrintJob?.name}" has finished!`,
|
sendNotification();
|
||||||
icon: "/favicon.png",
|
notificationSent = true;
|
||||||
});
|
}
|
||||||
}
|
}
|
||||||
}
|
}, 10000);
|
||||||
|
});
|
||||||
|
|
||||||
// Calculate time remaining for active print
|
onDestroy(() => {
|
||||||
function getTimeRemaining(
|
if (timerInterval) clearInterval(timerInterval);
|
||||||
startedAt: string,
|
});
|
||||||
durationMinutes: number
|
|
||||||
): string {
|
|
||||||
if (!startedAt) return "--:--";
|
|
||||||
const start = new Date(startedAt).getTime();
|
|
||||||
const elapsed = Math.floor((currentTime - start) / 60000); // minutes
|
|
||||||
const remaining = Math.max(0, durationMinutes - elapsed);
|
|
||||||
if (remaining === 0) return "Complete!";
|
|
||||||
const hours = Math.floor(remaining / 60);
|
|
||||||
const mins = remaining % 60;
|
|
||||||
return hours > 0 ? `${hours}h ${mins}m` : `${mins}m`;
|
|
||||||
}
|
|
||||||
|
|
||||||
function getProgress(startedAt: string, durationMinutes: number): number {
|
function sendNotification() {
|
||||||
if (!startedAt || !durationMinutes) return 0;
|
if (
|
||||||
const start = new Date(startedAt).getTime();
|
browser &&
|
||||||
const elapsed = (currentTime - start) / 60000;
|
"Notification" in window &&
|
||||||
return Math.min(100, Math.max(0, (elapsed / durationMinutes) * 100));
|
Notification.permission === "granted"
|
||||||
}
|
) {
|
||||||
|
new Notification("🎉 Print Complete!", {
|
||||||
|
body: `Your print "${activePrintJob?.name}" has finished!`,
|
||||||
|
icon: "/favicon.png",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Calculate time remaining for active print
|
||||||
|
function getTimeRemaining(
|
||||||
|
startedAt: string,
|
||||||
|
durationMinutes: number,
|
||||||
|
): string {
|
||||||
|
if (!startedAt) return "--:--";
|
||||||
|
const start = new Date(startedAt).getTime();
|
||||||
|
const elapsed = Math.floor((currentTime - start) / 60000); // minutes
|
||||||
|
const remaining = Math.max(0, durationMinutes - elapsed);
|
||||||
|
if (remaining === 0) return "Complete!";
|
||||||
|
const hours = Math.floor(remaining / 60);
|
||||||
|
const mins = remaining % 60;
|
||||||
|
return hours > 0 ? `${hours}h ${mins}m` : `${mins}m`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getProgress(startedAt: string, durationMinutes: number): number {
|
||||||
|
if (!startedAt || !durationMinutes) return 0;
|
||||||
|
const start = new Date(startedAt).getTime();
|
||||||
|
const elapsed = (currentTime - start) / 60000;
|
||||||
|
return Math.min(100, Math.max(0, (elapsed / durationMinutes) * 100));
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="space-y-8 fade-in">
|
<div class="space-y-8 fade-in">
|
||||||
<!-- Header -->
|
<!-- Header -->
|
||||||
<div class="flex flex-col md:flex-row md:items-center justify-between gap-4">
|
<div
|
||||||
<div>
|
class="flex flex-col md:flex-row md:items-center justify-between gap-4"
|
||||||
<h1 class="text-3xl font-bold tracking-tight text-white/90">Dashboard</h1>
|
>
|
||||||
<p class="text-slate-400 mt-1">Welcome back to your printing hub.</p>
|
<div>
|
||||||
</div>
|
<h1 class="text-3xl font-bold tracking-tight text-white/90">
|
||||||
<div class="flex gap-3">
|
Dashboard
|
||||||
<Button variant="secondary" size="sm">Quick Log</Button>
|
</h1>
|
||||||
<a href="/prints">
|
<p class="text-slate-400 mt-1">
|
||||||
<Button variant="primary" size="sm" class="shadow-blue-500/20">
|
Welcome back to your printing hub.
|
||||||
<span class="mr-2 text-lg leading-none">+</span> New Print
|
</p>
|
||||||
</Button>
|
</div>
|
||||||
</a>
|
<div class="flex gap-3">
|
||||||
</div>
|
<Button
|
||||||
</div>
|
variant="secondary"
|
||||||
|
size="sm"
|
||||||
|
onclick={() => (showQuickLogModal = true)}>Quick Log</Button
|
||||||
|
>
|
||||||
|
<a href="/prints">
|
||||||
|
<Button variant="primary" size="sm" class="shadow-blue-500/20">
|
||||||
|
<span class="mr-2 text-lg leading-none">+</span> New Print
|
||||||
|
</Button>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Stats Grid -->
|
<!-- Stats Grid -->
|
||||||
<div class="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-5 gap-4">
|
<div class="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-5 gap-4">
|
||||||
<Card class="relative overflow-hidden group">
|
<Card class="relative overflow-hidden group">
|
||||||
<div
|
<div
|
||||||
class="absolute inset-0 bg-linear-to-br from-blue-500/10 to-transparent opacity-0 group-hover:opacity-100 transition-opacity duration-500"
|
class="absolute inset-0 bg-linear-to-br from-blue-500/10 to-transparent opacity-0 group-hover:opacity-100 transition-opacity duration-500"
|
||||||
></div>
|
></div>
|
||||||
<div class="relative z-10">
|
<div class="relative z-10">
|
||||||
<p class="text-xs font-medium text-slate-400 uppercase tracking-wider">
|
<p
|
||||||
Active Spools
|
class="text-xs font-medium text-slate-400 uppercase tracking-wider"
|
||||||
</p>
|
>
|
||||||
<div class="flex items-baseline mt-2">
|
Active Spools
|
||||||
<span class="text-3xl font-bold text-white">{stats.spoolCount}</span>
|
</p>
|
||||||
</div>
|
<div class="flex items-baseline mt-2">
|
||||||
</div>
|
<span class="text-3xl font-bold text-white"
|
||||||
</Card>
|
>{stats.spoolCount}</span
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
|
||||||
<Card class="relative overflow-hidden group">
|
<Card class="relative overflow-hidden group">
|
||||||
<div
|
<div
|
||||||
class="absolute inset-0 bg-linear-to-br from-violet-500/10 to-transparent opacity-0 group-hover:opacity-100 transition-opacity duration-500"
|
class="absolute inset-0 bg-linear-to-br from-violet-500/10 to-transparent opacity-0 group-hover:opacity-100 transition-opacity duration-500"
|
||||||
></div>
|
></div>
|
||||||
<div class="relative z-10">
|
<div class="relative z-10">
|
||||||
<p class="text-xs font-medium text-slate-400 uppercase tracking-wider">
|
<p
|
||||||
Filament On Hand
|
class="text-xs font-medium text-slate-400 uppercase tracking-wider"
|
||||||
</p>
|
>
|
||||||
<div class="flex items-baseline mt-2">
|
Filament On Hand
|
||||||
{#if stats.totalWeightG >= 1000}
|
</p>
|
||||||
<span class="text-3xl font-bold text-white"
|
<div class="flex items-baseline mt-2">
|
||||||
>{stats.totalWeightKg}<span
|
{#if stats.totalWeightG >= 1000}
|
||||||
class="text-sm font-normal text-slate-500 ml-1">kg</span
|
<span class="text-3xl font-bold text-white"
|
||||||
></span
|
>{stats.totalWeightKg}<span
|
||||||
>
|
class="text-sm font-normal text-slate-500 ml-1"
|
||||||
{:else}
|
>kg</span
|
||||||
<span class="text-3xl font-bold text-white"
|
></span
|
||||||
>{stats.totalWeightG}<span
|
>
|
||||||
class="text-sm font-normal text-slate-500 ml-1">g</span
|
{:else}
|
||||||
></span
|
<span class="text-3xl font-bold text-white"
|
||||||
>
|
>{stats.totalWeightG}<span
|
||||||
{/if}
|
class="text-sm font-normal text-slate-500 ml-1"
|
||||||
</div>
|
>g</span
|
||||||
</div>
|
></span
|
||||||
</Card>
|
>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
|
||||||
<Card class="relative overflow-hidden group">
|
<Card class="relative overflow-hidden group">
|
||||||
<div
|
<div
|
||||||
class="absolute inset-0 bg-linear-to-br from-emerald-500/10 to-transparent opacity-0 group-hover:opacity-100 transition-opacity duration-500"
|
class="absolute inset-0 bg-linear-to-br from-emerald-500/10 to-transparent opacity-0 group-hover:opacity-100 transition-opacity duration-500"
|
||||||
></div>
|
></div>
|
||||||
<div class="relative z-10">
|
<div class="relative z-10">
|
||||||
<p class="text-xs font-medium text-slate-400 uppercase tracking-wider">
|
<p
|
||||||
Printers
|
class="text-xs font-medium text-slate-400 uppercase tracking-wider"
|
||||||
</p>
|
>
|
||||||
<div class="flex items-baseline mt-2">
|
Printers
|
||||||
<span class="text-3xl font-bold text-white">{stats.printerCount}</span
|
</p>
|
||||||
>
|
<div class="flex items-baseline mt-2">
|
||||||
</div>
|
<span class="text-3xl font-bold text-white"
|
||||||
</div>
|
>{stats.printerCount}</span
|
||||||
</Card>
|
>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
|
||||||
<Card class="relative overflow-hidden group">
|
<Card class="relative overflow-hidden group">
|
||||||
<div
|
<div
|
||||||
class="absolute inset-0 bg-linear-to-br from-amber-500/10 to-transparent opacity-0 group-hover:opacity-100 transition-opacity duration-500"
|
class="absolute inset-0 bg-linear-to-br from-amber-500/10 to-transparent opacity-0 group-hover:opacity-100 transition-opacity duration-500"
|
||||||
></div>
|
></div>
|
||||||
<div class="relative z-10">
|
<div class="relative z-10">
|
||||||
<p class="text-xs font-medium text-slate-400 uppercase tracking-wider">
|
<p
|
||||||
Est. Value
|
class="text-xs font-medium text-slate-400 uppercase tracking-wider"
|
||||||
</p>
|
>
|
||||||
<div class="flex items-baseline mt-2">
|
Est. Value
|
||||||
<span class="text-3xl font-bold text-white"
|
</p>
|
||||||
>${stats.estimatedValue}</span
|
<div class="flex items-baseline mt-2">
|
||||||
>
|
<span class="text-3xl font-bold text-white"
|
||||||
</div>
|
>${stats.estimatedValue}</span
|
||||||
</div>
|
>
|
||||||
</Card>
|
</div>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
|
||||||
<Card class="relative overflow-hidden group">
|
<Card class="relative overflow-hidden group">
|
||||||
<div
|
<div
|
||||||
class="absolute inset-0 bg-linear-to-br from-rose-500/10 to-transparent opacity-0 group-hover:opacity-100 transition-opacity duration-500"
|
class="absolute inset-0 bg-linear-to-br from-rose-500/10 to-transparent opacity-0 group-hover:opacity-100 transition-opacity duration-500"
|
||||||
></div>
|
></div>
|
||||||
<div class="relative z-10">
|
<div class="relative z-10">
|
||||||
<p class="text-xs font-medium text-slate-400 uppercase tracking-wider">
|
<p
|
||||||
Total Spent
|
class="text-xs font-medium text-slate-400 uppercase tracking-wider"
|
||||||
</p>
|
>
|
||||||
<div class="flex items-baseline mt-2">
|
Total Spent
|
||||||
<span class="text-3xl font-bold text-white">${stats.totalSpent}</span>
|
</p>
|
||||||
</div>
|
<div class="flex items-baseline mt-2">
|
||||||
</div>
|
<span class="text-3xl font-bold text-white"
|
||||||
</Card>
|
>${stats.totalSpent}</span
|
||||||
</div>
|
>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Main Content Grid -->
|
<!-- Main Content Grid -->
|
||||||
<div class="grid grid-cols-1 lg:grid-cols-3 gap-6">
|
<div class="grid grid-cols-1 lg:grid-cols-3 gap-6">
|
||||||
<!-- Recent Activity -->
|
<!-- Recent Activity -->
|
||||||
<div class="lg:col-span-2 space-y-6">
|
<div class="lg:col-span-2 space-y-6">
|
||||||
<div class="flex items-center justify-between">
|
<div class="flex items-center justify-between">
|
||||||
<h2 class="text-xl font-semibold text-white/90">Recent Activity</h2>
|
<h2 class="text-xl font-semibold text-white/90">
|
||||||
<a
|
Recent Activity
|
||||||
href="/prints"
|
</h2>
|
||||||
class="text-xs text-blue-400 hover:text-blue-300 transition-colors"
|
<a
|
||||||
>View All</a
|
href="/prints"
|
||||||
>
|
class="text-xs text-blue-400 hover:text-blue-300 transition-colors"
|
||||||
</div>
|
>View All</a
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
|
||||||
{#if recentPrints.length === 0}
|
{#if recentPrints.length === 0}
|
||||||
<Card
|
<Card
|
||||||
class="min-h-[300px] flex items-center justify-center border-dashed border-slate-700 bg-transparent/50"
|
class="min-h-[300px] flex items-center justify-center border-dashed border-slate-700 bg-transparent/50"
|
||||||
>
|
>
|
||||||
<div class="text-center">
|
<div class="text-center">
|
||||||
<div
|
<div
|
||||||
class="w-12 h-12 rounded-full bg-slate-800 flex items-center justify-center mx-auto mb-3 text-slate-600"
|
class="w-12 h-12 rounded-full bg-slate-800 flex items-center justify-center mx-auto mb-3 text-slate-600"
|
||||||
>
|
>
|
||||||
<Icon icon="mdi:printer-3d-nozzle" class="w-6 h-6" />
|
<Icon
|
||||||
</div>
|
icon="mdi:printer-3d-nozzle"
|
||||||
<p class="text-slate-500">No recent prints found</p>
|
class="w-6 h-6"
|
||||||
<a href="/prints">
|
/>
|
||||||
<Button
|
</div>
|
||||||
variant="ghost"
|
<p class="text-slate-500">No recent prints found</p>
|
||||||
size="sm"
|
<a href="/prints">
|
||||||
class="mt-2 text-blue-400 hover:text-blue-300"
|
<Button
|
||||||
>Log a Print</Button
|
variant="ghost"
|
||||||
>
|
size="sm"
|
||||||
</a>
|
class="mt-2 text-blue-400 hover:text-blue-300"
|
||||||
</div>
|
>Log a Print</Button
|
||||||
</Card>
|
>
|
||||||
{:else}
|
</a>
|
||||||
<div class="space-y-3">
|
</div>
|
||||||
{#each recentPrints as print}
|
</Card>
|
||||||
<Card
|
{:else}
|
||||||
class="flex items-center justify-between p-4! hover:bg-slate-800/80 transition-colors group"
|
<div class="space-y-3">
|
||||||
>
|
{#each recentPrints as print}
|
||||||
<div class="flex items-center gap-4">
|
<Card
|
||||||
<div
|
class="flex items-center justify-between p-4! hover:bg-slate-800/80 transition-colors group"
|
||||||
class="w-10 h-10 rounded-lg bg-slate-800 flex items-center justify-center group-hover:scale-105 transition-transform
|
>
|
||||||
|
<div class="flex items-center gap-4">
|
||||||
|
<div
|
||||||
|
class="w-10 h-10 rounded-lg bg-slate-800 flex items-center justify-center group-hover:scale-105 transition-transform
|
||||||
{print.status === 'Fail'
|
{print.status === 'Fail'
|
||||||
? 'text-red-400 bg-red-500/10'
|
? 'text-red-400 bg-red-500/10'
|
||||||
: print.status === 'In Progress'
|
: print.status === 'In Progress'
|
||||||
? 'text-blue-400 bg-blue-500/10'
|
? 'text-blue-400 bg-blue-500/10'
|
||||||
: 'text-blue-400 bg-blue-500/10'}"
|
: 'text-blue-400 bg-blue-500/10'}"
|
||||||
>
|
>
|
||||||
{#if print.status === "Success"}
|
{#if print.status === "Success"}
|
||||||
<Icon
|
<Icon
|
||||||
icon="mdi:check-circle"
|
icon="mdi:check-circle"
|
||||||
class="w-5 h-5 text-green-400"
|
class="w-5 h-5 text-green-400"
|
||||||
/>
|
/>
|
||||||
{:else if print.status === "Fail"}
|
{:else if print.status === "Fail"}
|
||||||
<Icon icon="mdi:close-circle" class="w-5 h-5" />
|
<Icon
|
||||||
{:else if print.status === "In Progress"}
|
icon="mdi:close-circle"
|
||||||
<Icon icon="mdi:loading" class="w-5 h-5 animate-spin" />
|
class="w-5 h-5"
|
||||||
{:else}
|
/>
|
||||||
<Icon icon="mdi:printer-3d" class="w-5 h-5" />
|
{:else if print.status === "In Progress"}
|
||||||
{/if}
|
<Icon
|
||||||
</div>
|
icon="mdi:loading"
|
||||||
<div>
|
class="w-5 h-5 animate-spin"
|
||||||
<h4 class="text-sm font-semibold text-white">{print.name}</h4>
|
/>
|
||||||
<p class="text-xs text-slate-400 mt-0.5">
|
{:else}
|
||||||
{print.printer_id?.name || "Unknown Printer"} • {print.filament_used_g}g
|
<Icon
|
||||||
</p>
|
icon="mdi:printer-3d"
|
||||||
</div>
|
class="w-5 h-5"
|
||||||
</div>
|
/>
|
||||||
<div class="text-right">
|
{/if}
|
||||||
<span
|
</div>
|
||||||
class="px-2 py-0.5 rounded text-[10px] font-medium
|
<div>
|
||||||
|
<h4
|
||||||
|
class="text-sm font-semibold text-white"
|
||||||
|
>
|
||||||
|
{print.name}
|
||||||
|
</h4>
|
||||||
|
<p class="text-xs text-slate-400 mt-0.5">
|
||||||
|
{print.printer_id?.name ||
|
||||||
|
"Unknown Printer"} • {print.filament_used_g}g
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="text-right">
|
||||||
|
<span
|
||||||
|
class="px-2 py-0.5 rounded text-[10px] font-medium
|
||||||
{print.status === 'Success'
|
{print.status === 'Success'
|
||||||
? 'bg-green-500/10 text-green-400'
|
? 'bg-green-500/10 text-green-400'
|
||||||
: print.status === 'Fail'
|
: print.status === 'Fail'
|
||||||
? 'bg-red-500/10 text-red-400'
|
? 'bg-red-500/10 text-red-400'
|
||||||
: 'bg-slate-700 text-slate-400'}"
|
: 'bg-slate-700 text-slate-400'}"
|
||||||
>
|
>
|
||||||
{print.status}
|
{print.status}
|
||||||
</span>
|
</span>
|
||||||
<p class="text-xs text-slate-500 mt-1">
|
<p class="text-xs text-slate-500 mt-1">
|
||||||
{new Date(print.date).toLocaleDateString()}
|
{new Date(print.date).toLocaleDateString(
|
||||||
</p>
|
"en-US",
|
||||||
</div>
|
{
|
||||||
</Card>
|
timeZone: "UTC",
|
||||||
{/each}
|
},
|
||||||
</div>
|
)}
|
||||||
{/if}
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
</Card>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Quick Actions / Status -->
|
<!-- Quick Actions / Status -->
|
||||||
<div class="space-y-6">
|
<div class="space-y-6">
|
||||||
<h2 class="text-xl font-semibold text-white/90">Printer Status</h2>
|
<h2 class="text-xl font-semibold text-white/90">Printer Status</h2>
|
||||||
<Card>
|
<Card>
|
||||||
{#if activePrintJob}
|
{#if activePrintJob}
|
||||||
<!-- Active Print Job -->
|
<!-- Active Print Job -->
|
||||||
<div class="flex items-center justify-between mb-4">
|
<div class="flex items-center justify-between mb-4">
|
||||||
<span class="font-medium text-white"
|
<span class="font-medium text-white"
|
||||||
>{activePrintJob.printer_id?.name || "Unknown Printer"}</span
|
>{activePrintJob.printer_id?.name ||
|
||||||
>
|
"Unknown Printer"}</span
|
||||||
<span
|
>
|
||||||
class="px-2 py-0.5 rounded-full text-[10px] uppercase font-bold tracking-wide bg-blue-500/10 text-blue-400 border border-blue-500/20 animate-pulse"
|
<span
|
||||||
>Printing</span
|
class="px-2 py-0.5 rounded-full text-[10px] uppercase font-bold tracking-wide bg-blue-500/10 text-blue-400 border border-blue-500/20 animate-pulse"
|
||||||
>
|
>Printing</span
|
||||||
</div>
|
>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="space-y-4">
|
<div class="space-y-4">
|
||||||
<!-- Job Name -->
|
<!-- Job Name -->
|
||||||
<div>
|
<div>
|
||||||
<p class="text-xs text-slate-400 mb-1">Currently Printing</p>
|
<p class="text-xs text-slate-400 mb-1">
|
||||||
<p class="text-sm font-semibold text-white">
|
Currently Printing
|
||||||
{activePrintJob.name}
|
</p>
|
||||||
</p>
|
<p class="text-sm font-semibold text-white">
|
||||||
</div>
|
{activePrintJob.name}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Progress Bar -->
|
<!-- Progress Bar -->
|
||||||
<div class="space-y-1.5">
|
<div class="space-y-1.5">
|
||||||
<div
|
<div
|
||||||
class="flex justify-between text-xs text-slate-400 font-medium"
|
class="flex justify-between text-xs text-slate-400 font-medium"
|
||||||
>
|
>
|
||||||
<span>Progress</span>
|
<span>Progress</span>
|
||||||
<span class="text-slate-200"
|
<span class="text-slate-200"
|
||||||
>{Math.round(
|
>{Math.round(
|
||||||
getProgress(
|
getProgress(
|
||||||
activePrintJob.started_at,
|
activePrintJob.started_at,
|
||||||
activePrintJob.duration_minutes
|
activePrintJob.duration_minutes,
|
||||||
)
|
),
|
||||||
)}%</span
|
)}%</span
|
||||||
>
|
>
|
||||||
</div>
|
</div>
|
||||||
<div class="h-2 bg-surface-800 rounded-full overflow-hidden">
|
<div
|
||||||
<div
|
class="h-2 bg-surface-800 rounded-full overflow-hidden"
|
||||||
class="h-full bg-blue-500 transition-all duration-1000"
|
>
|
||||||
style="width: {getProgress(
|
<div
|
||||||
activePrintJob.started_at,
|
class="h-full bg-blue-500 transition-all duration-1000"
|
||||||
activePrintJob.duration_minutes
|
style="width: {getProgress(
|
||||||
)}%"
|
activePrintJob.started_at,
|
||||||
></div>
|
activePrintJob.duration_minutes,
|
||||||
</div>
|
)}%"
|
||||||
</div>
|
></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Time & Material Info -->
|
<!-- Time & Material Info -->
|
||||||
<div class="grid grid-cols-2 gap-4 text-sm">
|
<div class="grid grid-cols-2 gap-4 text-sm">
|
||||||
<div>
|
<div>
|
||||||
<p class="text-xs text-slate-400">Time Remaining</p>
|
<p class="text-xs text-slate-400">
|
||||||
<p class="font-medium text-white">
|
Time Remaining
|
||||||
{getTimeRemaining(
|
</p>
|
||||||
activePrintJob.started_at,
|
<p class="font-medium text-white">
|
||||||
activePrintJob.duration_minutes
|
{getTimeRemaining(
|
||||||
)}
|
activePrintJob.started_at,
|
||||||
</p>
|
activePrintJob.duration_minutes,
|
||||||
</div>
|
)}
|
||||||
<div>
|
</p>
|
||||||
<p class="text-xs text-slate-400">Filament</p>
|
</div>
|
||||||
<p class="font-medium text-white">
|
<div>
|
||||||
{activePrintJob.filament_used_g}g
|
<p class="text-xs text-slate-400">Filament</p>
|
||||||
</p>
|
<p class="font-medium text-white">
|
||||||
</div>
|
{activePrintJob.filament_used_g}g
|
||||||
</div>
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Spool Info -->
|
<!-- Spool Info -->
|
||||||
{#if activePrintJob.spool_id}
|
{#if activePrintJob.spool_id}
|
||||||
<div class="flex items-center gap-2 text-sm">
|
<div class="flex items-center gap-2 text-sm">
|
||||||
<div
|
<div
|
||||||
class="w-3 h-3 rounded-full"
|
class="w-3 h-3 rounded-full"
|
||||||
style="background-color: {activePrintJob.spool_id.color_hex}"
|
style="background-color: {activePrintJob
|
||||||
></div>
|
.spool_id.color_hex}"
|
||||||
<span class="text-slate-300"
|
></div>
|
||||||
>{activePrintJob.spool_id.brand}
|
<span class="text-slate-300"
|
||||||
{activePrintJob.spool_id.material || ""}</span
|
>{activePrintJob.spool_id.brand}
|
||||||
>
|
{activePrintJob.spool_id.material ||
|
||||||
</div>
|
""}</span
|
||||||
{/if}
|
>
|
||||||
</div>
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="mt-6 pt-4 border-t border-surface-700/50">
|
<div class="mt-6 pt-4 border-t border-surface-700/50">
|
||||||
<a href="/prints" class="block">
|
<a href="/prints" class="block">
|
||||||
<Button variant="secondary" size="sm" class="w-full text-xs"
|
<Button
|
||||||
>View All Prints</Button
|
variant="secondary"
|
||||||
>
|
size="sm"
|
||||||
</a>
|
class="w-full text-xs">View All Prints</Button
|
||||||
</div>
|
>
|
||||||
{:else if activePrinter}
|
</a>
|
||||||
<!-- Idle State -->
|
</div>
|
||||||
<div class="flex items-center justify-between mb-4">
|
{:else if activePrinter}
|
||||||
<span class="font-medium text-white">{activePrinter.name}</span>
|
<!-- Idle State -->
|
||||||
<span
|
<div class="flex items-center justify-between mb-4">
|
||||||
class="px-2 py-0.5 rounded-full text-[10px] uppercase font-bold tracking-wide bg-emerald-500/10 text-emerald-400 border border-emerald-500/20"
|
<span class="font-medium text-white"
|
||||||
>Idle</span
|
>{activePrinter.name}</span
|
||||||
>
|
>
|
||||||
</div>
|
<span
|
||||||
<div class="text-center py-4">
|
class="px-2 py-0.5 rounded-full text-[10px] uppercase font-bold tracking-wide bg-emerald-500/10 text-emerald-400 border border-emerald-500/20"
|
||||||
<p class="text-sm text-slate-400 mb-4">No active print job</p>
|
>Idle</span
|
||||||
<a href="/prints">
|
>
|
||||||
<Button variant="primary" size="sm">Start a Print</Button>
|
</div>
|
||||||
</a>
|
<div class="text-center py-4">
|
||||||
</div>
|
<p class="text-sm text-slate-400 mb-4">
|
||||||
<div class="mt-4 pt-4 border-t border-surface-700/50">
|
No active print job
|
||||||
<a href="/printers" class="block">
|
</p>
|
||||||
<Button variant="secondary" size="sm" class="w-full text-xs"
|
<a href="/prints">
|
||||||
>Manage Printer</Button
|
<Button variant="primary" size="sm"
|
||||||
>
|
>Start a Print</Button
|
||||||
</a>
|
>
|
||||||
</div>
|
</a>
|
||||||
{:else}
|
</div>
|
||||||
<div class="text-center py-6">
|
<div class="mt-4 pt-4 border-t border-surface-700/50">
|
||||||
<p class="text-sm text-text-muted mb-4">No printers configured</p>
|
<a href="/printers" class="block">
|
||||||
<a href="/printers">
|
<Button
|
||||||
<Button variant="primary" size="sm">Add Printer</Button>
|
variant="secondary"
|
||||||
</a>
|
size="sm"
|
||||||
</div>
|
class="w-full text-xs">Manage Printer</Button
|
||||||
{/if}
|
>
|
||||||
</Card>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
{:else}
|
||||||
|
<div class="text-center py-6">
|
||||||
|
<p class="text-sm text-text-muted mb-4">
|
||||||
|
No printers configured
|
||||||
|
</p>
|
||||||
|
<a href="/printers">
|
||||||
|
<Button variant="primary" size="sm"
|
||||||
|
>Add Printer</Button
|
||||||
|
>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<LogPrintModal
|
||||||
|
open={showQuickLogModal}
|
||||||
|
{printers}
|
||||||
|
{spools}
|
||||||
|
onclose={() => (showQuickLogModal = false)}
|
||||||
|
action="/prints?/log"
|
||||||
|
/>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
/* Simple entry animation */
|
/* Simple entry animation */
|
||||||
.fade-in {
|
.fade-in {
|
||||||
animation: fadeIn 0.6s cubic-bezier(0.16, 1, 0.3, 1) forwards;
|
animation: fadeIn 0.6s cubic-bezier(0.16, 1, 0.3, 1) forwards;
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
transform: translateY(10px);
|
transform: translateY(10px);
|
||||||
}
|
}
|
||||||
@keyframes fadeIn {
|
@keyframes fadeIn {
|
||||||
to {
|
to {
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
transform: translateY(0);
|
transform: translateY(0);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ export const load: PageServerLoad = async ({ locals }) => {
|
|||||||
const prints = await PrintJob.find({ user_id: locals.user.id })
|
const prints = await PrintJob.find({ user_id: locals.user.id })
|
||||||
.populate('spool_id', 'brand material color_hex')
|
.populate('spool_id', 'brand material color_hex')
|
||||||
.populate('printer_id', 'name model')
|
.populate('printer_id', 'name model')
|
||||||
.sort({ date: -1 })
|
.sort({ date: -1, _id: -1 })
|
||||||
.lean();
|
.lean();
|
||||||
|
|
||||||
// Fetch active spools and printers for the "Log Print" form - filtered by user
|
// Fetch active spools and printers for the "Log Print" form - filtered by user
|
||||||
|
|||||||
Reference in New Issue
Block a user