fix(slides): remove duplicate cacheElements/bindEvents functions causing null error
The duplicate functions at lines 455-486 were redefining cacheElements and bindEvents with wrong element IDs (kebab-case vs camelCase in HTML). This caused 'Cannot read properties of null' error on slides app init.
This commit is contained in:
parent
08469ecbf6
commit
e3b5929b99
39 changed files with 3376 additions and 1697 deletions
33
PROMPT.md
33
PROMPT.md
|
|
@ -569,25 +569,30 @@ pub fn list_files(path: &str) -> Result<Vec<FileItem>, String> {
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Final Steps Before Commit
|
## COMPILATION POLICY - CRITICAL
|
||||||
|
|
||||||
```bash
|
**NEVER compile during development. NEVER run `cargo build` or `cargo check`. Use static analysis only.**
|
||||||
# Check for warnings
|
|
||||||
cargo check 2>&1 | grep warning
|
|
||||||
|
|
||||||
# Audit dependencies (must be 0 warnings)
|
### Workflow
|
||||||
cargo audit
|
|
||||||
|
|
||||||
# Build both modes
|
1. Make all code changes
|
||||||
cargo build
|
2. Use `diagnostics` tool for static analysis (NOT compilation)
|
||||||
cargo build --features desktop
|
3. Fix any errors found by diagnostics
|
||||||
|
4. **At the end**, inform user what needs restart
|
||||||
|
|
||||||
# Verify no dead code with _ prefixes
|
### After All Changes Complete
|
||||||
grep -r "let _" src/ --include="*.rs"
|
|
||||||
|
|
||||||
# Verify no CDN references
|
| Change Type | User Action Required |
|
||||||
grep -r "unpkg.com\|cdnjs\|jsdelivr" ui/
|
|-------------|----------------------|
|
||||||
```
|
| Rust code (`.rs` files) | "Recompile and restart **botui**" |
|
||||||
|
| HTML templates (`.html` in ui/) | "Browser refresh only" |
|
||||||
|
| CSS/JS files | "Browser refresh only" |
|
||||||
|
| Askama templates (`.html` in src/) | "Recompile and restart **botui**" |
|
||||||
|
| Cargo.toml changes | "Recompile and restart **botui**" |
|
||||||
|
|
||||||
|
**Format:** At the end of your response, always state:
|
||||||
|
- ✅ **No restart needed** - browser refresh only
|
||||||
|
- 🔄 **Restart botui** - recompile required
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -56,6 +56,11 @@ const SUITE_DIRS: &[&str] = &[
|
||||||
"goals",
|
"goals",
|
||||||
"player",
|
"player",
|
||||||
"canvas",
|
"canvas",
|
||||||
|
"people",
|
||||||
|
"crm",
|
||||||
|
"billing",
|
||||||
|
"products",
|
||||||
|
"tickets",
|
||||||
];
|
];
|
||||||
|
|
||||||
pub async fn index() -> impl IntoResponse {
|
pub async fn index() -> impl IntoResponse {
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,5 @@
|
||||||
|
<link rel="stylesheet" href="admin/admin.css" />
|
||||||
|
|
||||||
<div class="admin-layout">
|
<div class="admin-layout">
|
||||||
<!-- Sidebar Navigation -->
|
<!-- Sidebar Navigation -->
|
||||||
<aside class="admin-sidebar">
|
<aside class="admin-sidebar">
|
||||||
|
|
@ -716,5 +718,5 @@
|
||||||
</div>
|
</div>
|
||||||
</dialog>
|
</dialog>
|
||||||
|
|
||||||
<link rel="stylesheet" href="admin.css" />
|
<link rel="stylesheet" href="admin/admin.css" />
|
||||||
<script src="admin.js"></script>
|
<script src="admin/admin.js"></script>
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,5 @@
|
||||||
|
<link rel="stylesheet" href="analytics/analytics.css" />
|
||||||
|
|
||||||
<div class="analytics-container" id="analytics-app">
|
<div class="analytics-container" id="analytics-app">
|
||||||
<header class="analytics-header">
|
<header class="analytics-header">
|
||||||
<div class="header-title">
|
<div class="header-title">
|
||||||
|
|
@ -534,6 +536,6 @@
|
||||||
</aside>
|
</aside>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<link rel="stylesheet" href="analytics.css" />
|
<link rel="stylesheet" href="analytics/analytics.css" />
|
||||||
<script src="analytics.js"></script>
|
<script src="analytics/analytics.js"></script>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
<!-- Attendant Console - General Bots -->
|
<!-- Attendant Console - General Bots -->
|
||||||
<link rel="stylesheet" href="attendant.css" />
|
<link rel="stylesheet" href="attendant/attendant.css" />
|
||||||
|
|
||||||
<!-- CRM Disabled State -->
|
<!-- CRM Disabled State -->
|
||||||
<!-- CRM Disabled State - Hidden by default, CRM is now enabled by default -->
|
<!-- CRM Disabled State - Hidden by default, CRM is now enabled by default -->
|
||||||
|
|
@ -572,4 +572,4 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Toast Container -->
|
<!-- Toast Container -->
|
||||||
<script src="attendant.js"></script>
|
<script src="attendant/attendant.js"></script>
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
<!-- Billing - Invoices, Payments & Quotes -->
|
<!-- Billing - Invoices, Payments & Quotes -->
|
||||||
<!-- Dynamics nomenclature: Quote → Invoice → Payment -->
|
<!-- Dynamics nomenclature: Quote → Invoice → Payment -->
|
||||||
|
|
||||||
<link rel="stylesheet" href="/suite/billing/billing.css">
|
<link rel="stylesheet" href="billing/billing.css" />
|
||||||
|
|
||||||
<div class="billing-container">
|
<div class="billing-container">
|
||||||
<!-- Header -->
|
<!-- Header -->
|
||||||
|
|
@ -9,26 +9,62 @@
|
||||||
<div class="billing-header-left">
|
<div class="billing-header-left">
|
||||||
<h1 data-i18n="billing-title">Billing</h1>
|
<h1 data-i18n="billing-title">Billing</h1>
|
||||||
<nav class="billing-tabs">
|
<nav class="billing-tabs">
|
||||||
<button class="billing-tab active" data-view="invoices" data-i18n="billing-invoices">Invoices</button>
|
<button
|
||||||
<button class="billing-tab" data-view="payments" data-i18n="billing-payments">Payments</button>
|
class="billing-tab active"
|
||||||
<button class="billing-tab" data-view="quotes" data-i18n="billing-quotes">Quotes</button>
|
data-view="invoices"
|
||||||
|
data-i18n="billing-invoices"
|
||||||
|
>
|
||||||
|
Invoices
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
class="billing-tab"
|
||||||
|
data-view="payments"
|
||||||
|
data-i18n="billing-payments"
|
||||||
|
>
|
||||||
|
Payments
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
class="billing-tab"
|
||||||
|
data-view="quotes"
|
||||||
|
data-i18n="billing-quotes"
|
||||||
|
>
|
||||||
|
Quotes
|
||||||
|
</button>
|
||||||
</nav>
|
</nav>
|
||||||
</div>
|
</div>
|
||||||
<div class="billing-header-right">
|
<div class="billing-header-right">
|
||||||
<div class="billing-search">
|
<div class="billing-search">
|
||||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
<svg
|
||||||
<circle cx="11" cy="11" r="8"/><path d="m21 21-4.35-4.35"/>
|
width="16"
|
||||||
|
height="16"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="2"
|
||||||
|
>
|
||||||
|
<circle cx="11" cy="11" r="8" />
|
||||||
|
<path d="m21 21-4.35-4.35" />
|
||||||
</svg>
|
</svg>
|
||||||
<input type="text"
|
<input
|
||||||
placeholder="Search invoices, quotes..."
|
type="text"
|
||||||
data-i18n-placeholder="billing-search-placeholder"
|
placeholder="Search invoices, quotes..."
|
||||||
hx-get="/api/billing/search"
|
data-i18n-placeholder="billing-search-placeholder"
|
||||||
hx-trigger="keyup changed delay:300ms"
|
hx-get="/api/billing/search"
|
||||||
hx-target="#billing-search-results">
|
hx-trigger="keyup changed delay:300ms"
|
||||||
|
hx-target="#billing-search-results"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<button class="btn-primary" id="billing-new-invoice">
|
<button class="btn-primary" id="billing-new-invoice">
|
||||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
<svg
|
||||||
<line x1="12" y1="5" x2="12" y2="19"/><line x1="5" y1="12" x2="19" y2="12"/>
|
width="16"
|
||||||
|
height="16"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="2"
|
||||||
|
>
|
||||||
|
<line x1="12" y1="5" x2="12" y2="19" />
|
||||||
|
<line x1="5" y1="12" x2="19" y2="12" />
|
||||||
</svg>
|
</svg>
|
||||||
<span data-i18n="billing-new-invoice">New Invoice</span>
|
<span data-i18n="billing-new-invoice">New Invoice</span>
|
||||||
</button>
|
</button>
|
||||||
|
|
@ -42,47 +78,111 @@
|
||||||
<div class="billing-summary">
|
<div class="billing-summary">
|
||||||
<div class="summary-card">
|
<div class="summary-card">
|
||||||
<div class="summary-icon pending">
|
<div class="summary-icon pending">
|
||||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
<svg
|
||||||
<circle cx="12" cy="12" r="10"/><polyline points="12 6 12 12 16 14"/>
|
width="20"
|
||||||
|
height="20"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="2"
|
||||||
|
>
|
||||||
|
<circle cx="12" cy="12" r="10" />
|
||||||
|
<polyline points="12 6 12 12 16 14" />
|
||||||
</svg>
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
<div class="summary-info">
|
<div class="summary-info">
|
||||||
<span class="summary-label" data-i18n="billing-pending">Pending</span>
|
<span class="summary-label" data-i18n="billing-pending"
|
||||||
<span class="summary-value" hx-get="/api/billing/stats/pending" hx-trigger="load">$0</span>
|
>Pending</span
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
class="summary-value"
|
||||||
|
hx-get="/api/billing/stats/pending"
|
||||||
|
hx-trigger="load"
|
||||||
|
>$0</span
|
||||||
|
>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="summary-card">
|
<div class="summary-card">
|
||||||
<div class="summary-icon overdue">
|
<div class="summary-icon overdue">
|
||||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
<svg
|
||||||
<path d="M10.29 3.86L1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z"/>
|
width="20"
|
||||||
<line x1="12" y1="9" x2="12" y2="13"/><line x1="12" y1="17" x2="12.01" y2="17"/>
|
height="20"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="2"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M10.29 3.86L1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z"
|
||||||
|
/>
|
||||||
|
<line x1="12" y1="9" x2="12" y2="13" />
|
||||||
|
<line x1="12" y1="17" x2="12.01" y2="17" />
|
||||||
</svg>
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
<div class="summary-info">
|
<div class="summary-info">
|
||||||
<span class="summary-label" data-i18n="billing-overdue">Overdue</span>
|
<span class="summary-label" data-i18n="billing-overdue"
|
||||||
<span class="summary-value overdue" hx-get="/api/billing/stats/overdue" hx-trigger="load">$0</span>
|
>Overdue</span
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
class="summary-value overdue"
|
||||||
|
hx-get="/api/billing/stats/overdue"
|
||||||
|
hx-trigger="load"
|
||||||
|
>$0</span
|
||||||
|
>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="summary-card">
|
<div class="summary-card">
|
||||||
<div class="summary-icon paid">
|
<div class="summary-icon paid">
|
||||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
<svg
|
||||||
<path d="M22 11.08V12a10 10 0 1 1-5.93-9.14"/><polyline points="22 4 12 14.01 9 11.01"/>
|
width="20"
|
||||||
|
height="20"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="2"
|
||||||
|
>
|
||||||
|
<path d="M22 11.08V12a10 10 0 1 1-5.93-9.14" />
|
||||||
|
<polyline points="22 4 12 14.01 9 11.01" />
|
||||||
</svg>
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
<div class="summary-info">
|
<div class="summary-info">
|
||||||
<span class="summary-label" data-i18n="billing-paid-month">Paid This Month</span>
|
<span class="summary-label" data-i18n="billing-paid-month"
|
||||||
<span class="summary-value paid" hx-get="/api/billing/stats/paid-month" hx-trigger="load">$0</span>
|
>Paid This Month</span
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
class="summary-value paid"
|
||||||
|
hx-get="/api/billing/stats/paid-month"
|
||||||
|
hx-trigger="load"
|
||||||
|
>$0</span
|
||||||
|
>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="summary-card">
|
<div class="summary-card">
|
||||||
<div class="summary-icon total">
|
<div class="summary-icon total">
|
||||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
<svg
|
||||||
<line x1="12" y1="1" x2="12" y2="23"/><path d="M17 5H9.5a3.5 3.5 0 0 0 0 7h5a3.5 3.5 0 0 1 0 7H6"/>
|
width="20"
|
||||||
|
height="20"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="2"
|
||||||
|
>
|
||||||
|
<line x1="12" y1="1" x2="12" y2="23" />
|
||||||
|
<path
|
||||||
|
d="M17 5H9.5a3.5 3.5 0 0 0 0 7h5a3.5 3.5 0 0 1 0 7H6"
|
||||||
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
<div class="summary-info">
|
<div class="summary-info">
|
||||||
<span class="summary-label" data-i18n="billing-revenue-month">Revenue This Month</span>
|
<span class="summary-label" data-i18n="billing-revenue-month"
|
||||||
<span class="summary-value" hx-get="/api/billing/stats/revenue-month" hx-trigger="load">$0</span>
|
>Revenue This Month</span
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
class="summary-value"
|
||||||
|
hx-get="/api/billing/stats/revenue-month"
|
||||||
|
hx-trigger="load"
|
||||||
|
>$0</span
|
||||||
|
>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -91,23 +191,64 @@
|
||||||
<div id="billing-invoices-view" class="billing-view active">
|
<div id="billing-invoices-view" class="billing-view active">
|
||||||
<div class="billing-list-header">
|
<div class="billing-list-header">
|
||||||
<div class="list-filters">
|
<div class="list-filters">
|
||||||
<select hx-get="/api/billing/invoices" hx-trigger="change" hx-target="#invoices-table-body" hx-include="this" name="status">
|
<select
|
||||||
<option value="all" data-i18n="billing-filter-all">All Invoices</option>
|
hx-get="/api/billing/invoices"
|
||||||
<option value="draft" data-i18n="billing-filter-draft">Draft</option>
|
hx-trigger="change"
|
||||||
<option value="sent" data-i18n="billing-filter-sent">Sent</option>
|
hx-target="#invoices-table-body"
|
||||||
<option value="paid" data-i18n="billing-filter-paid">Paid</option>
|
hx-include="this"
|
||||||
<option value="overdue" data-i18n="billing-filter-overdue">Overdue</option>
|
name="status"
|
||||||
<option value="cancelled" data-i18n="billing-filter-cancelled">Cancelled</option>
|
>
|
||||||
|
<option value="all" data-i18n="billing-filter-all">
|
||||||
|
All Invoices
|
||||||
|
</option>
|
||||||
|
<option value="draft" data-i18n="billing-filter-draft">
|
||||||
|
Draft
|
||||||
|
</option>
|
||||||
|
<option value="sent" data-i18n="billing-filter-sent">
|
||||||
|
Sent
|
||||||
|
</option>
|
||||||
|
<option value="paid" data-i18n="billing-filter-paid">
|
||||||
|
Paid
|
||||||
|
</option>
|
||||||
|
<option value="overdue" data-i18n="billing-filter-overdue">
|
||||||
|
Overdue
|
||||||
|
</option>
|
||||||
|
<option
|
||||||
|
value="cancelled"
|
||||||
|
data-i18n="billing-filter-cancelled"
|
||||||
|
>
|
||||||
|
Cancelled
|
||||||
|
</option>
|
||||||
</select>
|
</select>
|
||||||
<select hx-get="/api/billing/invoices" hx-trigger="change" hx-target="#invoices-table-body" hx-include="this" name="period">
|
<select
|
||||||
<option value="all" data-i18n="billing-period-all">All Time</option>
|
hx-get="/api/billing/invoices"
|
||||||
<option value="month" data-i18n="billing-period-month">This Month</option>
|
hx-trigger="change"
|
||||||
<option value="quarter" data-i18n="billing-period-quarter">This Quarter</option>
|
hx-target="#invoices-table-body"
|
||||||
<option value="year" data-i18n="billing-period-year">This Year</option>
|
hx-include="this"
|
||||||
|
name="period"
|
||||||
|
>
|
||||||
|
<option value="all" data-i18n="billing-period-all">
|
||||||
|
All Time
|
||||||
|
</option>
|
||||||
|
<option value="month" data-i18n="billing-period-month">
|
||||||
|
This Month
|
||||||
|
</option>
|
||||||
|
<option value="quarter" data-i18n="billing-period-quarter">
|
||||||
|
This Quarter
|
||||||
|
</option>
|
||||||
|
<option value="year" data-i18n="billing-period-year">
|
||||||
|
This Year
|
||||||
|
</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<div class="list-actions">
|
<div class="list-actions">
|
||||||
<button class="action-btn" hx-get="/api/billing/invoices/export" data-i18n="billing-export">Export</button>
|
<button
|
||||||
|
class="action-btn"
|
||||||
|
hx-get="/api/billing/invoices/export"
|
||||||
|
data-i18n="billing-export"
|
||||||
|
>
|
||||||
|
Export
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<table class="billing-table">
|
<table class="billing-table">
|
||||||
|
|
@ -122,7 +263,11 @@
|
||||||
<th data-i18n="billing-col-actions">Actions</th>
|
<th data-i18n="billing-col-actions">Actions</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody id="invoices-table-body" hx-get="/api/billing/invoices" hx-trigger="load">
|
<tbody
|
||||||
|
id="invoices-table-body"
|
||||||
|
hx-get="/api/billing/invoices"
|
||||||
|
hx-trigger="load"
|
||||||
|
>
|
||||||
<!-- Invoices loaded via HTMX -->
|
<!-- Invoices loaded via HTMX -->
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
|
@ -132,18 +277,49 @@
|
||||||
<div id="billing-payments-view" class="billing-view">
|
<div id="billing-payments-view" class="billing-view">
|
||||||
<div class="billing-list-header">
|
<div class="billing-list-header">
|
||||||
<div class="list-filters">
|
<div class="list-filters">
|
||||||
<select hx-get="/api/billing/payments" hx-trigger="change" hx-target="#payments-table-body" hx-include="this" name="method">
|
<select
|
||||||
<option value="all" data-i18n="billing-method-all">All Methods</option>
|
hx-get="/api/billing/payments"
|
||||||
<option value="bank" data-i18n="billing-method-bank">Bank Transfer</option>
|
hx-trigger="change"
|
||||||
<option value="card" data-i18n="billing-method-card">Credit Card</option>
|
hx-target="#payments-table-body"
|
||||||
<option value="pix" data-i18n="billing-method-pix">PIX</option>
|
hx-include="this"
|
||||||
<option value="boleto" data-i18n="billing-method-boleto">Boleto</option>
|
name="method"
|
||||||
<option value="cash" data-i18n="billing-method-cash">Cash</option>
|
>
|
||||||
|
<option value="all" data-i18n="billing-method-all">
|
||||||
|
All Methods
|
||||||
|
</option>
|
||||||
|
<option value="bank" data-i18n="billing-method-bank">
|
||||||
|
Bank Transfer
|
||||||
|
</option>
|
||||||
|
<option value="card" data-i18n="billing-method-card">
|
||||||
|
Credit Card
|
||||||
|
</option>
|
||||||
|
<option value="pix" data-i18n="billing-method-pix">
|
||||||
|
PIX
|
||||||
|
</option>
|
||||||
|
<option value="boleto" data-i18n="billing-method-boleto">
|
||||||
|
Boleto
|
||||||
|
</option>
|
||||||
|
<option value="cash" data-i18n="billing-method-cash">
|
||||||
|
Cash
|
||||||
|
</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<button class="btn-primary" hx-get="/suite/billing/partials/payment-form.html" hx-target="#billing-modal-content" hx-on::after-request="openBillingModal()">
|
<button
|
||||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
class="btn-primary"
|
||||||
<line x1="12" y1="5" x2="12" y2="19"/><line x1="5" y1="12" x2="19" y2="12"/>
|
hx-get="/suite/billing/partials/payment-form.html"
|
||||||
|
hx-target="#billing-modal-content"
|
||||||
|
hx-on::after-request="openBillingModal()"
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
width="16"
|
||||||
|
height="16"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="2"
|
||||||
|
>
|
||||||
|
<line x1="12" y1="5" x2="12" y2="19" />
|
||||||
|
<line x1="5" y1="12" x2="19" y2="12" />
|
||||||
</svg>
|
</svg>
|
||||||
<span data-i18n="billing-record-payment">Record Payment</span>
|
<span data-i18n="billing-record-payment">Record Payment</span>
|
||||||
</button>
|
</button>
|
||||||
|
|
@ -160,8 +336,11 @@
|
||||||
<th data-i18n="billing-col-actions">Actions</th>
|
<th data-i18n="billing-col-actions">Actions</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody id="payments-table-body" hx-get="/api/billing/payments" hx-trigger="load">
|
<tbody
|
||||||
</tbody>
|
id="payments-table-body"
|
||||||
|
hx-get="/api/billing/payments"
|
||||||
|
hx-trigger="load"
|
||||||
|
></tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -169,18 +348,49 @@
|
||||||
<div id="billing-quotes-view" class="billing-view">
|
<div id="billing-quotes-view" class="billing-view">
|
||||||
<div class="billing-list-header">
|
<div class="billing-list-header">
|
||||||
<div class="list-filters">
|
<div class="list-filters">
|
||||||
<select hx-get="/api/billing/quotes" hx-trigger="change" hx-target="#quotes-table-body" hx-include="this" name="status">
|
<select
|
||||||
<option value="all" data-i18n="billing-quote-all">All Quotes</option>
|
hx-get="/api/billing/quotes"
|
||||||
<option value="draft" data-i18n="billing-quote-draft">Draft</option>
|
hx-trigger="change"
|
||||||
<option value="sent" data-i18n="billing-quote-sent">Sent</option>
|
hx-target="#quotes-table-body"
|
||||||
<option value="accepted" data-i18n="billing-quote-accepted">Accepted</option>
|
hx-include="this"
|
||||||
<option value="rejected" data-i18n="billing-quote-rejected">Rejected</option>
|
name="status"
|
||||||
<option value="expired" data-i18n="billing-quote-expired">Expired</option>
|
>
|
||||||
|
<option value="all" data-i18n="billing-quote-all">
|
||||||
|
All Quotes
|
||||||
|
</option>
|
||||||
|
<option value="draft" data-i18n="billing-quote-draft">
|
||||||
|
Draft
|
||||||
|
</option>
|
||||||
|
<option value="sent" data-i18n="billing-quote-sent">
|
||||||
|
Sent
|
||||||
|
</option>
|
||||||
|
<option value="accepted" data-i18n="billing-quote-accepted">
|
||||||
|
Accepted
|
||||||
|
</option>
|
||||||
|
<option value="rejected" data-i18n="billing-quote-rejected">
|
||||||
|
Rejected
|
||||||
|
</option>
|
||||||
|
<option value="expired" data-i18n="billing-quote-expired">
|
||||||
|
Expired
|
||||||
|
</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<button class="btn-primary" hx-get="/suite/billing/partials/quote-form.html" hx-target="#billing-modal-content" hx-on::after-request="openBillingModal()">
|
<button
|
||||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
class="btn-primary"
|
||||||
<line x1="12" y1="5" x2="12" y2="19"/><line x1="5" y1="12" x2="19" y2="12"/>
|
hx-get="/suite/billing/partials/quote-form.html"
|
||||||
|
hx-target="#billing-modal-content"
|
||||||
|
hx-on::after-request="openBillingModal()"
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
width="16"
|
||||||
|
height="16"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="2"
|
||||||
|
>
|
||||||
|
<line x1="12" y1="5" x2="12" y2="19" />
|
||||||
|
<line x1="5" y1="12" x2="19" y2="12" />
|
||||||
</svg>
|
</svg>
|
||||||
<span data-i18n="billing-new-quote">New Quote</span>
|
<span data-i18n="billing-new-quote">New Quote</span>
|
||||||
</button>
|
</button>
|
||||||
|
|
@ -198,8 +408,11 @@
|
||||||
<th data-i18n="billing-col-actions">Actions</th>
|
<th data-i18n="billing-col-actions">Actions</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody id="quotes-table-body" hx-get="/api/billing/quotes" hx-trigger="load">
|
<tbody
|
||||||
</tbody>
|
id="quotes-table-body"
|
||||||
|
hx-get="/api/billing/quotes"
|
||||||
|
hx-trigger="load"
|
||||||
|
></tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -213,44 +426,56 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
(function() {
|
(function () {
|
||||||
// Tab switching
|
// Tab switching
|
||||||
document.querySelectorAll('.billing-tab').forEach(tab => {
|
document.querySelectorAll(".billing-tab").forEach((tab) => {
|
||||||
tab.addEventListener('click', function() {
|
tab.addEventListener("click", function () {
|
||||||
document.querySelectorAll('.billing-tab').forEach(t => t.classList.remove('active'));
|
document
|
||||||
document.querySelectorAll('.billing-view').forEach(v => v.classList.remove('active'));
|
.querySelectorAll(".billing-tab")
|
||||||
this.classList.add('active');
|
.forEach((t) => t.classList.remove("active"));
|
||||||
const view = this.dataset.view;
|
document
|
||||||
document.getElementById(`billing-${view}-view`).classList.add('active');
|
.querySelectorAll(".billing-view")
|
||||||
|
.forEach((v) => v.classList.remove("active"));
|
||||||
|
this.classList.add("active");
|
||||||
|
const view = this.dataset.view;
|
||||||
|
document
|
||||||
|
.getElementById(`billing-${view}-view`)
|
||||||
|
.classList.add("active");
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
|
||||||
|
|
||||||
// New Invoice button
|
// New Invoice button
|
||||||
document.getElementById('billing-new-invoice').addEventListener('click', function() {
|
document
|
||||||
htmx.ajax('GET', '/suite/billing/partials/invoice-form.html', '#billing-modal-content').then(() => {
|
.getElementById("billing-new-invoice")
|
||||||
openBillingModal();
|
.addEventListener("click", function () {
|
||||||
|
htmx.ajax(
|
||||||
|
"GET",
|
||||||
|
"/suite/billing/partials/invoice-form.html",
|
||||||
|
"#billing-modal-content",
|
||||||
|
).then(() => {
|
||||||
|
openBillingModal();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Modal functions
|
||||||
|
window.openBillingModal = function () {
|
||||||
|
document.getElementById("billing-modal").classList.add("open");
|
||||||
|
};
|
||||||
|
|
||||||
|
window.closeBillingModal = function () {
|
||||||
|
document.getElementById("billing-modal").classList.remove("open");
|
||||||
|
};
|
||||||
|
|
||||||
|
// Keyboard shortcut: Escape to close modal
|
||||||
|
document.addEventListener("keydown", function (e) {
|
||||||
|
if (e.key === "Escape") {
|
||||||
|
closeBillingModal();
|
||||||
|
}
|
||||||
});
|
});
|
||||||
});
|
|
||||||
|
|
||||||
// Modal functions
|
// Initialize i18n if available
|
||||||
window.openBillingModal = function() {
|
if (window.i18n && window.i18n.translatePage) {
|
||||||
document.getElementById('billing-modal').classList.add('open');
|
window.i18n.translatePage();
|
||||||
};
|
|
||||||
|
|
||||||
window.closeBillingModal = function() {
|
|
||||||
document.getElementById('billing-modal').classList.remove('open');
|
|
||||||
};
|
|
||||||
|
|
||||||
// Keyboard shortcut: Escape to close modal
|
|
||||||
document.addEventListener('keydown', function(e) {
|
|
||||||
if (e.key === 'Escape') {
|
|
||||||
closeBillingModal();
|
|
||||||
}
|
}
|
||||||
});
|
})();
|
||||||
|
|
||||||
// Initialize i18n if available
|
|
||||||
if (window.i18n && window.i18n.translatePage) {
|
|
||||||
window.i18n.translatePage();
|
|
||||||
}
|
|
||||||
})();
|
|
||||||
</script>
|
</script>
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,5 @@
|
||||||
|
<link rel="stylesheet" href="calendar/calendar.css" />
|
||||||
|
|
||||||
<!-- Calendar - Event Management -->
|
<!-- Calendar - Event Management -->
|
||||||
<div class="calendar-container" id="calendar-app">
|
<div class="calendar-container" id="calendar-app">
|
||||||
<!-- Sidebar -->
|
<!-- Sidebar -->
|
||||||
|
|
|
||||||
File diff suppressed because it is too large
Load diff
|
|
@ -1,7 +1,7 @@
|
||||||
<!-- CRM - Customer Relationship Management -->
|
<!-- CRM - Customer Relationship Management -->
|
||||||
<!-- Dynamics nomenclature: Lead → Opportunity → Account/Contact -->
|
<!-- Dynamics nomenclature: Lead → Opportunity → Account/Contact -->
|
||||||
|
|
||||||
<link rel="stylesheet" href="/suite/crm/crm.css">
|
<link rel="stylesheet" href="crm/crm.css">
|
||||||
|
|
||||||
<div class="crm-container">
|
<div class="crm-container">
|
||||||
<!-- Header -->
|
<!-- Header -->
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,5 @@
|
||||||
|
<link rel="stylesheet" href="dashboards/dashboards.css" />
|
||||||
|
|
||||||
<div class="dashboards-container" id="dashboards-app">
|
<div class="dashboards-container" id="dashboards-app">
|
||||||
<header class="dashboards-header">
|
<header class="dashboards-header">
|
||||||
<div class="header-title">
|
<div class="header-title">
|
||||||
|
|
|
||||||
|
|
@ -1747,6 +1747,11 @@
|
||||||
const toolboxItems = document.querySelectorAll('.toolbox-item');
|
const toolboxItems = document.querySelectorAll('.toolbox-item');
|
||||||
const canvas = document.getElementById('canvas-inner');
|
const canvas = document.getElementById('canvas-inner');
|
||||||
|
|
||||||
|
if (!canvas) {
|
||||||
|
console.warn('initDragAndDrop: canvas-inner not found');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
toolboxItems.forEach(item => {
|
toolboxItems.forEach(item => {
|
||||||
item.addEventListener('dragstart', (e) => {
|
item.addEventListener('dragstart', (e) => {
|
||||||
e.dataTransfer.setData('nodeType', item.dataset.nodeType);
|
e.dataTransfer.setData('nodeType', item.dataset.nodeType);
|
||||||
|
|
@ -1779,6 +1784,11 @@
|
||||||
const canvas = document.getElementById('canvas');
|
const canvas = document.getElementById('canvas');
|
||||||
const container = document.getElementById('canvas-container');
|
const container = document.getElementById('canvas-container');
|
||||||
|
|
||||||
|
if (!canvas || !container) {
|
||||||
|
console.warn('initCanvasInteraction: canvas or canvas-container not found');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// Pan with middle mouse or space+drag
|
// Pan with middle mouse or space+drag
|
||||||
let isPanning = false;
|
let isPanning = false;
|
||||||
let panStart = { x: 0, y: 0 };
|
let panStart = { x: 0, y: 0 };
|
||||||
|
|
@ -2295,6 +2305,11 @@
|
||||||
const canvas = document.getElementById('canvas');
|
const canvas = document.getElementById('canvas');
|
||||||
const contextMenu = document.getElementById('context-menu');
|
const contextMenu = document.getElementById('context-menu');
|
||||||
|
|
||||||
|
if (!canvas || !contextMenu) {
|
||||||
|
console.warn('initContextMenu: canvas or context-menu not found');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
canvas.addEventListener('contextmenu', (e) => {
|
canvas.addEventListener('contextmenu', (e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
const nodeEl = e.target.closest('.node');
|
const nodeEl = e.target.closest('.node');
|
||||||
|
|
@ -2312,7 +2327,10 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
function hideContextMenu() {
|
function hideContextMenu() {
|
||||||
document.getElementById('context-menu').classList.remove('visible');
|
const menu = document.getElementById('context-menu');
|
||||||
|
if (menu) {
|
||||||
|
menu.classList.remove('visible');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Context Menu Actions
|
// Context Menu Actions
|
||||||
|
|
@ -2353,14 +2371,20 @@
|
||||||
|
|
||||||
// Modal Management
|
// Modal Management
|
||||||
function showModal(id) {
|
function showModal(id) {
|
||||||
document.getElementById(id).classList.add('visible');
|
const modal = document.getElementById(id);
|
||||||
if (id === 'open-modal') {
|
if (modal) {
|
||||||
htmx.trigger('#file-list-content', 'load');
|
modal.classList.add('visible');
|
||||||
|
if (id === 'open-modal') {
|
||||||
|
htmx.trigger('#file-list-content', 'load');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function hideModal(id) {
|
function hideModal(id) {
|
||||||
document.getElementById(id).classList.remove('visible');
|
const modal = document.getElementById(id);
|
||||||
|
if (modal) {
|
||||||
|
modal.classList.remove('visible');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Save Design
|
// Save Design
|
||||||
|
|
@ -2579,7 +2603,10 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
function hideMagicPanel() {
|
function hideMagicPanel() {
|
||||||
document.getElementById('magic-panel').classList.remove('visible');
|
const panel = document.getElementById('magic-panel');
|
||||||
|
if (panel) {
|
||||||
|
panel.classList.remove('visible');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function analyzeMagicSuggestions() {
|
async function analyzeMagicSuggestions() {
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,5 @@
|
||||||
|
<link rel="stylesheet" href="docs/docs.css" />
|
||||||
|
|
||||||
<div class="docs-app" id="docs-app">
|
<div class="docs-app" id="docs-app">
|
||||||
<div class="docs-toolbar">
|
<div class="docs-toolbar">
|
||||||
<div class="toolbar-left">
|
<div class="toolbar-left">
|
||||||
|
|
|
||||||
|
|
@ -10,35 +10,37 @@
|
||||||
/>
|
/>
|
||||||
<meta name="theme-color" content="#d4f505" />
|
<meta name="theme-color" content="#d4f505" />
|
||||||
|
|
||||||
<!-- Styles -->
|
<!-- Critical CSS - minimal styles for shell/layout (loaded sync) -->
|
||||||
<link rel="stylesheet" href="css/app.css" />
|
<link rel="stylesheet" href="css/app.css" />
|
||||||
<link rel="stylesheet" href="css/apps-extended.css" />
|
|
||||||
<link rel="stylesheet" href="css/components.css" />
|
|
||||||
<link rel="stylesheet" href="css/base.css" />
|
<link rel="stylesheet" href="css/base.css" />
|
||||||
<link rel="stylesheet" href="css/partials.css" />
|
|
||||||
<link rel="stylesheet" href="css/theme-sentient.css" />
|
<link rel="stylesheet" href="css/theme-sentient.css" />
|
||||||
|
|
||||||
<!-- App-specific CSS -->
|
<!-- Non-critical CSS - loaded async for faster initial paint -->
|
||||||
<link rel="stylesheet" href="chat/chat.css" />
|
<link
|
||||||
<link rel="stylesheet" href="calendar/calendar.css" />
|
rel="stylesheet"
|
||||||
<link rel="stylesheet" href="drive/drive.css" />
|
href="css/components.css"
|
||||||
<link rel="stylesheet" href="mail/mail.css" />
|
media="print"
|
||||||
<link rel="stylesheet" href="meet/meet.css" />
|
onload="this.media='all'"
|
||||||
<link rel="stylesheet" href="paper/paper.css" />
|
/>
|
||||||
<link rel="stylesheet" href="sheet/sheet.css" />
|
<link
|
||||||
<link rel="stylesheet" href="slides/slides.css" />
|
rel="stylesheet"
|
||||||
<link rel="stylesheet" href="research/research.css" />
|
href="css/partials.css"
|
||||||
<link rel="stylesheet" href="tasks/tasks.css?v=20251230" />
|
media="print"
|
||||||
<link rel="stylesheet" href="tasks/taskmd.css?v=20251230" />
|
onload="this.media='all'"
|
||||||
<link rel="stylesheet" href="analytics/analytics.css" />
|
/>
|
||||||
<link rel="stylesheet" href="dashboards/dashboards.css" />
|
<link
|
||||||
<link rel="stylesheet" href="monitoring/monitoring.css" />
|
rel="stylesheet"
|
||||||
<link rel="stylesheet" href="crm/crm.css" />
|
href="css/apps-extended.css"
|
||||||
<link rel="stylesheet" href="billing/billing.css" />
|
media="print"
|
||||||
<link rel="stylesheet" href="products/products.css" />
|
onload="this.media='all'"
|
||||||
<link rel="stylesheet" href="tickets/tickets.css" />
|
/>
|
||||||
<link rel="stylesheet" href="docs/docs.css" />
|
<noscript>
|
||||||
<link rel="stylesheet" href="social/social.css" />
|
<link rel="stylesheet" href="css/components.css" />
|
||||||
|
<link rel="stylesheet" href="css/partials.css" />
|
||||||
|
<link rel="stylesheet" href="css/apps-extended.css" />
|
||||||
|
</noscript>
|
||||||
|
|
||||||
|
<!-- App-specific CSS loaded lazily by each view via CSSLoader -->
|
||||||
|
|
||||||
<!-- Local Libraries (no external CDN dependencies) -->
|
<!-- Local Libraries (no external CDN dependencies) -->
|
||||||
<script src="js/vendor/htmx.min.js"></script>
|
<script src="js/vendor/htmx.min.js"></script>
|
||||||
|
|
@ -53,6 +55,9 @@
|
||||||
<!-- i18n -->
|
<!-- i18n -->
|
||||||
<script src="js/i18n.js"></script>
|
<script src="js/i18n.js"></script>
|
||||||
|
|
||||||
|
<!-- CSS Lazy Loader - enables per-screen CSS loading -->
|
||||||
|
<script src="js/css-loader.js"></script>
|
||||||
|
|
||||||
<!-- Enable HTMX to process inline scripts in swapped content -->
|
<!-- Enable HTMX to process inline scripts in swapped content -->
|
||||||
<script>
|
<script>
|
||||||
htmx.config.allowEval = true;
|
htmx.config.allowEval = true;
|
||||||
|
|
@ -3286,7 +3291,6 @@
|
||||||
e,
|
e,
|
||||||
);
|
);
|
||||||
currentLoadedSection = null;
|
currentLoadedSection = null;
|
||||||
} finally {
|
|
||||||
isLoadingSection = false;
|
isLoadingSection = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -3316,13 +3320,39 @@
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Also listen for response errors
|
||||||
|
document.body.addEventListener(
|
||||||
|
"htmx:responseError",
|
||||||
|
(event) => {
|
||||||
|
if (
|
||||||
|
event.detail.target &&
|
||||||
|
event.detail.target.id === "main-content"
|
||||||
|
) {
|
||||||
|
isLoadingSection = false;
|
||||||
|
currentLoadedSection = null;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
// Load initial content based on hash or default to chat
|
// Load initial content based on hash or default to chat
|
||||||
window.addEventListener("hashchange", handleHashChange);
|
window.addEventListener("hashchange", handleHashChange);
|
||||||
|
|
||||||
// Initial load
|
// Initial load - wait for HTMX to be ready
|
||||||
setTimeout(() => {
|
function initialLoad() {
|
||||||
handleHashChange();
|
if (typeof htmx !== "undefined" && htmx.ajax) {
|
||||||
}, 100);
|
handleHashChange();
|
||||||
|
} else {
|
||||||
|
setTimeout(initialLoad, 50);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (document.readyState === "complete") {
|
||||||
|
setTimeout(initialLoad, 50);
|
||||||
|
} else {
|
||||||
|
window.addEventListener("load", () => {
|
||||||
|
setTimeout(initialLoad, 50);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// ==========================================================================
|
// ==========================================================================
|
||||||
// GBAlerts - Global Notification System
|
// GBAlerts - Global Notification System
|
||||||
|
|
|
||||||
212
ui/suite/js/css-loader.js
Normal file
212
ui/suite/js/css-loader.js
Normal file
|
|
@ -0,0 +1,212 @@
|
||||||
|
/**
|
||||||
|
* CSS Lazy Loader - Efficient on-demand stylesheet loading
|
||||||
|
* Prevents duplicate loads and handles caching automatically
|
||||||
|
*/
|
||||||
|
const CSSLoader = (function () {
|
||||||
|
const loadedStyles = new Set();
|
||||||
|
const loadingPromises = new Map();
|
||||||
|
|
||||||
|
function getAbsoluteUrl(href) {
|
||||||
|
if (href.startsWith("http://") || href.startsWith("https://")) {
|
||||||
|
return href;
|
||||||
|
}
|
||||||
|
const base = window.location.pathname.includes("/suite/")
|
||||||
|
? "/suite/"
|
||||||
|
: "/";
|
||||||
|
if (href.startsWith("/")) {
|
||||||
|
return href;
|
||||||
|
}
|
||||||
|
return base + href;
|
||||||
|
}
|
||||||
|
|
||||||
|
function load(href, options = {}) {
|
||||||
|
const absoluteUrl = getAbsoluteUrl(href);
|
||||||
|
|
||||||
|
if (loadedStyles.has(absoluteUrl)) {
|
||||||
|
return Promise.resolve();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (loadingPromises.has(absoluteUrl)) {
|
||||||
|
return loadingPromises.get(absoluteUrl);
|
||||||
|
}
|
||||||
|
|
||||||
|
const promise = new Promise((resolve, reject) => {
|
||||||
|
const existingLink = document.querySelector(
|
||||||
|
`link[href="${href}"], link[href="${absoluteUrl}"]`
|
||||||
|
);
|
||||||
|
if (existingLink) {
|
||||||
|
loadedStyles.add(absoluteUrl);
|
||||||
|
resolve();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const link = document.createElement("link");
|
||||||
|
link.rel = "stylesheet";
|
||||||
|
link.href = href;
|
||||||
|
|
||||||
|
if (options.media) {
|
||||||
|
link.media = options.media;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (options.crossOrigin) {
|
||||||
|
link.crossOrigin = options.crossOrigin;
|
||||||
|
}
|
||||||
|
|
||||||
|
link.onload = function () {
|
||||||
|
loadedStyles.add(absoluteUrl);
|
||||||
|
loadingPromises.delete(absoluteUrl);
|
||||||
|
resolve();
|
||||||
|
};
|
||||||
|
|
||||||
|
link.onerror = function () {
|
||||||
|
loadingPromises.delete(absoluteUrl);
|
||||||
|
reject(new Error(`Failed to load CSS: ${href}`));
|
||||||
|
};
|
||||||
|
|
||||||
|
const insertPoint =
|
||||||
|
options.insertAfter ||
|
||||||
|
document.querySelector('link[rel="stylesheet"]:last-of-type') ||
|
||||||
|
document.head.lastChild;
|
||||||
|
|
||||||
|
if (insertPoint && insertPoint.parentNode) {
|
||||||
|
insertPoint.parentNode.insertBefore(
|
||||||
|
link,
|
||||||
|
insertPoint.nextSibling
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
document.head.appendChild(link);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
loadingPromises.set(absoluteUrl, promise);
|
||||||
|
return promise;
|
||||||
|
}
|
||||||
|
|
||||||
|
function loadMultiple(hrefs, options = {}) {
|
||||||
|
return Promise.all(hrefs.map((href) => load(href, options)));
|
||||||
|
}
|
||||||
|
|
||||||
|
function preload(href) {
|
||||||
|
const absoluteUrl = getAbsoluteUrl(href);
|
||||||
|
|
||||||
|
if (loadedStyles.has(absoluteUrl)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const existingPreload = document.querySelector(
|
||||||
|
`link[rel="preload"][href="${href}"]`
|
||||||
|
);
|
||||||
|
if (existingPreload) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const link = document.createElement("link");
|
||||||
|
link.rel = "preload";
|
||||||
|
link.as = "style";
|
||||||
|
link.href = href;
|
||||||
|
document.head.appendChild(link);
|
||||||
|
}
|
||||||
|
|
||||||
|
function isLoaded(href) {
|
||||||
|
const absoluteUrl = getAbsoluteUrl(href);
|
||||||
|
return loadedStyles.has(absoluteUrl);
|
||||||
|
}
|
||||||
|
|
||||||
|
function unload(href) {
|
||||||
|
const absoluteUrl = getAbsoluteUrl(href);
|
||||||
|
const link = document.querySelector(
|
||||||
|
`link[href="${href}"], link[href="${absoluteUrl}"]`
|
||||||
|
);
|
||||||
|
if (link) {
|
||||||
|
link.remove();
|
||||||
|
loadedStyles.delete(absoluteUrl);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function loadForApp(appName) {
|
||||||
|
const appCssMap = {
|
||||||
|
admin: ["admin/admin.css"],
|
||||||
|
analytics: ["analytics/analytics.css"],
|
||||||
|
attendant: ["attendant/attendant.css"],
|
||||||
|
auth: ["auth/auth.css"],
|
||||||
|
billing: ["billing/billing.css"],
|
||||||
|
calendar: ["calendar/calendar.css"],
|
||||||
|
chat: ["chat/chat.css"],
|
||||||
|
crm: ["crm/crm.css"],
|
||||||
|
dashboards: ["dashboards/dashboards.css"],
|
||||||
|
docs: ["docs/docs.css"],
|
||||||
|
drive: ["drive/drive.css"],
|
||||||
|
learn: ["learn/learn.css"],
|
||||||
|
mail: ["mail/mail.css"],
|
||||||
|
meet: ["meet/meet.css"],
|
||||||
|
monitoring: ["monitoring/monitoring.css"],
|
||||||
|
paper: ["paper/paper.css"],
|
||||||
|
people: ["people/people.css"],
|
||||||
|
products: ["products/products.css"],
|
||||||
|
research: ["research/research.css"],
|
||||||
|
settings: ["settings/settings.css"],
|
||||||
|
sheet: ["sheet/sheet.css"],
|
||||||
|
slides: ["slides/slides.css"],
|
||||||
|
social: ["social/social.css"],
|
||||||
|
sources: ["sources/sources.css"],
|
||||||
|
tasks: ["tasks/tasks.css"],
|
||||||
|
tickets: ["tickets/tickets.css"],
|
||||||
|
tools: ["tools/tools.css"],
|
||||||
|
};
|
||||||
|
|
||||||
|
const cssFiles = appCssMap[appName];
|
||||||
|
if (cssFiles && cssFiles.length > 0) {
|
||||||
|
return loadMultiple(cssFiles);
|
||||||
|
}
|
||||||
|
return Promise.resolve();
|
||||||
|
}
|
||||||
|
|
||||||
|
function init() {
|
||||||
|
document.querySelectorAll('link[rel="stylesheet"]').forEach((link) => {
|
||||||
|
if (link.href) {
|
||||||
|
loadedStyles.add(link.href);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
document.body.addEventListener("htmx:beforeSwap", function (event) {
|
||||||
|
const content = event.detail.serverResponse;
|
||||||
|
if (content && typeof content === "string") {
|
||||||
|
const cssMatches = content.match(
|
||||||
|
/<link[^>]+rel=["']stylesheet["'][^>]*>/gi
|
||||||
|
);
|
||||||
|
if (cssMatches) {
|
||||||
|
cssMatches.forEach((match) => {
|
||||||
|
const hrefMatch = match.match(/href=["']([^"']+)["']/i);
|
||||||
|
if (hrefMatch && hrefMatch[1]) {
|
||||||
|
load(hrefMatch[1]).catch((err) => {
|
||||||
|
console.warn(
|
||||||
|
"CSS preload failed:",
|
||||||
|
err.message
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (document.readyState === "loading") {
|
||||||
|
document.addEventListener("DOMContentLoaded", init);
|
||||||
|
} else {
|
||||||
|
init();
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
load: load,
|
||||||
|
loadMultiple: loadMultiple,
|
||||||
|
preload: preload,
|
||||||
|
isLoaded: isLoaded,
|
||||||
|
unload: unload,
|
||||||
|
loadForApp: loadForApp,
|
||||||
|
};
|
||||||
|
})();
|
||||||
|
|
||||||
|
if (typeof window !== "undefined") {
|
||||||
|
window.CSSLoader = CSSLoader;
|
||||||
|
}
|
||||||
|
|
@ -1,10 +1,19 @@
|
||||||
|
<link rel="stylesheet" href="learn/learn.css" />
|
||||||
|
|
||||||
<div class="learn-container">
|
<div class="learn-container">
|
||||||
<!-- Sidebar -->
|
<!-- Sidebar -->
|
||||||
<aside class="learn-sidebar">
|
<aside class="learn-sidebar">
|
||||||
<div class="sidebar-header">
|
<div class="sidebar-header">
|
||||||
<h2 data-i18n="learn-title">📚 Learn</h2>
|
<h2 data-i18n="learn-title">📚 Learn</h2>
|
||||||
<button class="btn-icon-sm" onclick="toggleLearnSidebar()">
|
<button class="btn-icon-sm" onclick="toggleLearnSidebar()">
|
||||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
<svg
|
||||||
|
width="20"
|
||||||
|
height="20"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="2"
|
||||||
|
>
|
||||||
<polyline points="15 18 9 12 15 6"></polyline>
|
<polyline points="15 18 9 12 15 6"></polyline>
|
||||||
</svg>
|
</svg>
|
||||||
</button>
|
</button>
|
||||||
|
|
@ -19,19 +28,27 @@
|
||||||
<div class="stats-grid">
|
<div class="stats-grid">
|
||||||
<div class="stat-item">
|
<div class="stat-item">
|
||||||
<span class="stat-value" id="statCoursesCompleted">0</span>
|
<span class="stat-value" id="statCoursesCompleted">0</span>
|
||||||
<span class="stat-label" data-i18n="learn-completed">Concluídos</span>
|
<span class="stat-label" data-i18n="learn-completed"
|
||||||
|
>Concluídos</span
|
||||||
|
>
|
||||||
</div>
|
</div>
|
||||||
<div class="stat-item">
|
<div class="stat-item">
|
||||||
<span class="stat-value" id="statCoursesInProgress">0</span>
|
<span class="stat-value" id="statCoursesInProgress">0</span>
|
||||||
<span class="stat-label" data-i18n="learn-in-progress">Em Andamento</span>
|
<span class="stat-label" data-i18n="learn-in-progress"
|
||||||
|
>Em Andamento</span
|
||||||
|
>
|
||||||
</div>
|
</div>
|
||||||
<div class="stat-item">
|
<div class="stat-item">
|
||||||
<span class="stat-value" id="statCertificates">0</span>
|
<span class="stat-value" id="statCertificates">0</span>
|
||||||
<span class="stat-label" data-i18n="learn-certificates">Certificados</span>
|
<span class="stat-label" data-i18n="learn-certificates"
|
||||||
|
>Certificados</span
|
||||||
|
>
|
||||||
</div>
|
</div>
|
||||||
<div class="stat-item">
|
<div class="stat-item">
|
||||||
<span class="stat-value" id="statTimeSpent">0h</span>
|
<span class="stat-value" id="statTimeSpent">0h</span>
|
||||||
<span class="stat-label" data-i18n="learn-time-spent">Tempo Total</span>
|
<span class="stat-label" data-i18n="learn-time-spent"
|
||||||
|
>Tempo Total</span
|
||||||
|
>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -78,15 +95,21 @@
|
||||||
<h3 data-i18n="learn-difficulty">Dificuldade</h3>
|
<h3 data-i18n="learn-difficulty">Dificuldade</h3>
|
||||||
<div class="difficulty-filter">
|
<div class="difficulty-filter">
|
||||||
<label class="checkbox-label">
|
<label class="checkbox-label">
|
||||||
<input type="checkbox" checked data-difficulty="beginner">
|
<input type="checkbox" checked data-difficulty="beginner" />
|
||||||
<span class="difficulty-badge beginner">Iniciante</span>
|
<span class="difficulty-badge beginner">Iniciante</span>
|
||||||
</label>
|
</label>
|
||||||
<label class="checkbox-label">
|
<label class="checkbox-label">
|
||||||
<input type="checkbox" checked data-difficulty="intermediate">
|
<input
|
||||||
<span class="difficulty-badge intermediate">Intermediário</span>
|
type="checkbox"
|
||||||
|
checked
|
||||||
|
data-difficulty="intermediate"
|
||||||
|
/>
|
||||||
|
<span class="difficulty-badge intermediate"
|
||||||
|
>Intermediário</span
|
||||||
|
>
|
||||||
</label>
|
</label>
|
||||||
<label class="checkbox-label">
|
<label class="checkbox-label">
|
||||||
<input type="checkbox" checked data-difficulty="advanced">
|
<input type="checkbox" checked data-difficulty="advanced" />
|
||||||
<span class="difficulty-badge advanced">Avançado</span>
|
<span class="difficulty-badge advanced">Avançado</span>
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -103,7 +126,9 @@
|
||||||
<div class="certificates-preview" id="certificatesPreview">
|
<div class="certificates-preview" id="certificatesPreview">
|
||||||
<div class="empty-state-small">
|
<div class="empty-state-small">
|
||||||
<span>🏆</span>
|
<span>🏆</span>
|
||||||
<p data-i18n="learn-no-certificates">Nenhum certificado ainda</p>
|
<p data-i18n="learn-no-certificates">
|
||||||
|
Nenhum certificado ainda
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -115,34 +140,84 @@
|
||||||
<div class="learn-header">
|
<div class="learn-header">
|
||||||
<div class="header-left">
|
<div class="header-left">
|
||||||
<div class="tabs">
|
<div class="tabs">
|
||||||
<button class="tab active" data-tab="catalog" onclick="switchTab('catalog')">
|
<button
|
||||||
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
class="tab active"
|
||||||
|
data-tab="catalog"
|
||||||
|
onclick="switchTab('catalog')"
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
width="18"
|
||||||
|
height="18"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="2"
|
||||||
|
>
|
||||||
<path d="M4 19.5A2.5 2.5 0 0 1 6.5 17H20"></path>
|
<path d="M4 19.5A2.5 2.5 0 0 1 6.5 17H20"></path>
|
||||||
<path d="M6.5 2H20v20H6.5A2.5 2.5 0 0 1 4 19.5v-15A2.5 2.5 0 0 1 6.5 2z"></path>
|
<path
|
||||||
|
d="M6.5 2H20v20H6.5A2.5 2.5 0 0 1 4 19.5v-15A2.5 2.5 0 0 1 6.5 2z"
|
||||||
|
></path>
|
||||||
</svg>
|
</svg>
|
||||||
<span data-i18n="learn-catalog">Catálogo</span>
|
<span data-i18n="learn-catalog">Catálogo</span>
|
||||||
</button>
|
</button>
|
||||||
<button class="tab" data-tab="my-courses" onclick="switchTab('my-courses')">
|
<button
|
||||||
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
class="tab"
|
||||||
|
data-tab="my-courses"
|
||||||
|
onclick="switchTab('my-courses')"
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
width="18"
|
||||||
|
height="18"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="2"
|
||||||
|
>
|
||||||
<path d="M22 10v6M2 10l10-5 10 5-10 5z"></path>
|
<path d="M22 10v6M2 10l10-5 10 5-10 5z"></path>
|
||||||
<path d="M6 12v5c3 3 9 3 12 0v-5"></path>
|
<path d="M6 12v5c3 3 9 3 12 0v-5"></path>
|
||||||
</svg>
|
</svg>
|
||||||
<span data-i18n="learn-my-courses">Meus Cursos</span>
|
<span data-i18n="learn-my-courses">Meus Cursos</span>
|
||||||
<span class="badge" id="myCoursesCount">0</span>
|
<span class="badge" id="myCoursesCount">0</span>
|
||||||
</button>
|
</button>
|
||||||
<button class="tab" data-tab="mandatory" onclick="switchTab('mandatory')">
|
<button
|
||||||
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
class="tab"
|
||||||
<path d="M10.29 3.86L1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z"></path>
|
data-tab="mandatory"
|
||||||
|
onclick="switchTab('mandatory')"
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
width="18"
|
||||||
|
height="18"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="2"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M10.29 3.86L1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z"
|
||||||
|
></path>
|
||||||
<line x1="12" y1="9" x2="12" y2="13"></line>
|
<line x1="12" y1="9" x2="12" y2="13"></line>
|
||||||
<line x1="12" y1="17" x2="12.01" y2="17"></line>
|
<line x1="12" y1="17" x2="12.01" y2="17"></line>
|
||||||
</svg>
|
</svg>
|
||||||
<span data-i18n="learn-pending">Pendentes</span>
|
<span data-i18n="learn-pending">Pendentes</span>
|
||||||
<span class="badge warning" id="mandatoryCount">0</span>
|
<span class="badge warning" id="mandatoryCount">0</span>
|
||||||
</button>
|
</button>
|
||||||
<button class="tab" data-tab="certificates" onclick="switchTab('certificates')">
|
<button
|
||||||
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
class="tab"
|
||||||
|
data-tab="certificates"
|
||||||
|
onclick="switchTab('certificates')"
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
width="18"
|
||||||
|
height="18"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="2"
|
||||||
|
>
|
||||||
<circle cx="12" cy="8" r="7"></circle>
|
<circle cx="12" cy="8" r="7"></circle>
|
||||||
<polyline points="8.21 13.89 7 23 12 20 17 23 15.79 13.88"></polyline>
|
<polyline
|
||||||
|
points="8.21 13.89 7 23 12 20 17 23 15.79 13.88"
|
||||||
|
></polyline>
|
||||||
</svg>
|
</svg>
|
||||||
<span data-i18n="learn-certificates">Certificados</span>
|
<span data-i18n="learn-certificates">Certificados</span>
|
||||||
</button>
|
</button>
|
||||||
|
|
@ -150,27 +225,57 @@
|
||||||
</div>
|
</div>
|
||||||
<div class="header-right">
|
<div class="header-right">
|
||||||
<div class="search-box">
|
<div class="search-box">
|
||||||
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
<svg
|
||||||
|
width="18"
|
||||||
|
height="18"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="2"
|
||||||
|
>
|
||||||
<circle cx="11" cy="11" r="8"></circle>
|
<circle cx="11" cy="11" r="8"></circle>
|
||||||
<line x1="21" y1="21" x2="16.65" y2="16.65"></line>
|
<line x1="21" y1="21" x2="16.65" y2="16.65"></line>
|
||||||
</svg>
|
</svg>
|
||||||
<input type="text" id="searchCourses" placeholder="Buscar cursos..." data-i18n-placeholder="learn-search-placeholder">
|
<input
|
||||||
|
type="text"
|
||||||
|
id="searchCourses"
|
||||||
|
placeholder="Buscar cursos..."
|
||||||
|
data-i18n-placeholder="learn-search-placeholder"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<select id="sortCourses" class="sort-select">
|
<select id="sortCourses" class="sort-select">
|
||||||
<option value="recent" data-i18n="learn-sort-recent">Mais Recentes</option>
|
<option value="recent" data-i18n="learn-sort-recent">
|
||||||
<option value="popular" data-i18n="learn-sort-popular">Mais Populares</option>
|
Mais Recentes
|
||||||
<option value="duration-asc" data-i18n="learn-sort-duration-asc">Menor Duração</option>
|
</option>
|
||||||
<option value="duration-desc" data-i18n="learn-sort-duration-desc">Maior Duração</option>
|
<option value="popular" data-i18n="learn-sort-popular">
|
||||||
|
Mais Populares
|
||||||
|
</option>
|
||||||
|
<option
|
||||||
|
value="duration-asc"
|
||||||
|
data-i18n="learn-sort-duration-asc"
|
||||||
|
>
|
||||||
|
Menor Duração
|
||||||
|
</option>
|
||||||
|
<option
|
||||||
|
value="duration-desc"
|
||||||
|
data-i18n="learn-sort-duration-desc"
|
||||||
|
>
|
||||||
|
Maior Duração
|
||||||
|
</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Mandatory Training Alert -->
|
<!-- Mandatory Training Alert -->
|
||||||
<div class="mandatory-alert" id="mandatoryAlert" style="display: none;">
|
<div class="mandatory-alert" id="mandatoryAlert" style="display: none">
|
||||||
<div class="alert-icon">⚠️</div>
|
<div class="alert-icon">⚠️</div>
|
||||||
<div class="alert-content">
|
<div class="alert-content">
|
||||||
<strong data-i18n="learn-mandatory-alert-title">Treinamentos Obrigatórios Pendentes</strong>
|
<strong data-i18n="learn-mandatory-alert-title"
|
||||||
<p id="mandatoryAlertText">Você possui treinamentos obrigatórios com prazo próximo.</p>
|
>Treinamentos Obrigatórios Pendentes</strong
|
||||||
|
>
|
||||||
|
<p id="mandatoryAlertText">
|
||||||
|
Você possui treinamentos obrigatórios com prazo próximo.
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<button class="btn-primary-sm" onclick="switchTab('mandatory')">
|
<button class="btn-primary-sm" onclick="switchTab('mandatory')">
|
||||||
<span data-i18n="learn-view-pending">Ver Pendentes</span>
|
<span data-i18n="learn-view-pending">Ver Pendentes</span>
|
||||||
|
|
@ -184,7 +289,9 @@
|
||||||
<div class="section-header">
|
<div class="section-header">
|
||||||
<h3>
|
<h3>
|
||||||
<span class="section-icon">✨</span>
|
<span class="section-icon">✨</span>
|
||||||
<span data-i18n="learn-recommended">Recomendados para Você</span>
|
<span data-i18n="learn-recommended"
|
||||||
|
>Recomendados para Você</span
|
||||||
|
>
|
||||||
</h3>
|
</h3>
|
||||||
</div>
|
</div>
|
||||||
<div class="courses-carousel" id="recommendedCourses">
|
<div class="courses-carousel" id="recommendedCourses">
|
||||||
|
|
@ -197,14 +304,18 @@
|
||||||
<div class="section-header">
|
<div class="section-header">
|
||||||
<h3>
|
<h3>
|
||||||
<span class="section-icon">📚</span>
|
<span class="section-icon">📚</span>
|
||||||
<span data-i18n="learn-all-courses">Todos os Cursos</span>
|
<span data-i18n="learn-all-courses"
|
||||||
|
>Todos os Cursos</span
|
||||||
|
>
|
||||||
</h3>
|
</h3>
|
||||||
<span class="courses-count" id="coursesCountLabel">0 cursos</span>
|
<span class="courses-count" id="coursesCountLabel"
|
||||||
|
>0 cursos</span
|
||||||
|
>
|
||||||
</div>
|
</div>
|
||||||
<div class="courses-grid" id="coursesGrid">
|
<div class="courses-grid" id="coursesGrid">
|
||||||
<!-- Courses loaded dynamically -->
|
<!-- Courses loaded dynamically -->
|
||||||
</div>
|
</div>
|
||||||
<div class="load-more" id="loadMore" style="display: none;">
|
<div class="load-more" id="loadMore" style="display: none">
|
||||||
<button class="btn-secondary" onclick="loadMoreCourses()">
|
<button class="btn-secondary" onclick="loadMoreCourses()">
|
||||||
<span data-i18n="learn-load-more">Carregar Mais</span>
|
<span data-i18n="learn-load-more">Carregar Mais</span>
|
||||||
</button>
|
</button>
|
||||||
|
|
@ -219,7 +330,9 @@
|
||||||
<div class="section-header">
|
<div class="section-header">
|
||||||
<h3>
|
<h3>
|
||||||
<span class="section-icon">▶️</span>
|
<span class="section-icon">▶️</span>
|
||||||
<span data-i18n="learn-continue">Continuar Aprendendo</span>
|
<span data-i18n="learn-continue"
|
||||||
|
>Continuar Aprendendo</span
|
||||||
|
>
|
||||||
</h3>
|
</h3>
|
||||||
</div>
|
</div>
|
||||||
<div class="courses-list" id="continueLearning">
|
<div class="courses-list" id="continueLearning">
|
||||||
|
|
@ -232,7 +345,9 @@
|
||||||
<div class="section-header">
|
<div class="section-header">
|
||||||
<h3>
|
<h3>
|
||||||
<span class="section-icon">✅</span>
|
<span class="section-icon">✅</span>
|
||||||
<span data-i18n="learn-completed-courses">Cursos Concluídos</span>
|
<span data-i18n="learn-completed-courses"
|
||||||
|
>Cursos Concluídos</span
|
||||||
|
>
|
||||||
</h3>
|
</h3>
|
||||||
</div>
|
</div>
|
||||||
<div class="courses-list" id="completedCourses">
|
<div class="courses-list" id="completedCourses">
|
||||||
|
|
@ -247,7 +362,9 @@
|
||||||
<div class="section-header">
|
<div class="section-header">
|
||||||
<h3>
|
<h3>
|
||||||
<span class="section-icon">⚠️</span>
|
<span class="section-icon">⚠️</span>
|
||||||
<span data-i18n="learn-mandatory-training">Treinamentos Obrigatórios</span>
|
<span data-i18n="learn-mandatory-training"
|
||||||
|
>Treinamentos Obrigatórios</span
|
||||||
|
>
|
||||||
</h3>
|
</h3>
|
||||||
</div>
|
</div>
|
||||||
<div class="mandatory-list" id="mandatoryList">
|
<div class="mandatory-list" id="mandatoryList">
|
||||||
|
|
@ -262,7 +379,9 @@
|
||||||
<div class="section-header">
|
<div class="section-header">
|
||||||
<h3>
|
<h3>
|
||||||
<span class="section-icon">🏆</span>
|
<span class="section-icon">🏆</span>
|
||||||
<span data-i18n="learn-my-certificates">Meus Certificados</span>
|
<span data-i18n="learn-my-certificates"
|
||||||
|
>Meus Certificados</span
|
||||||
|
>
|
||||||
</h3>
|
</h3>
|
||||||
</div>
|
</div>
|
||||||
<div class="certificates-grid" id="certificatesGrid">
|
<div class="certificates-grid" id="certificatesGrid">
|
||||||
|
|
@ -278,7 +397,14 @@
|
||||||
<div class="modal-header">
|
<div class="modal-header">
|
||||||
<h3 id="modalCourseTitle">Título do Curso</h3>
|
<h3 id="modalCourseTitle">Título do Curso</h3>
|
||||||
<button class="btn-icon-sm" onclick="closeCourseModal()">
|
<button class="btn-icon-sm" onclick="closeCourseModal()">
|
||||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
<svg
|
||||||
|
width="20"
|
||||||
|
height="20"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="2"
|
||||||
|
>
|
||||||
<line x1="18" y1="6" x2="6" y2="18"></line>
|
<line x1="18" y1="6" x2="6" y2="18"></line>
|
||||||
<line x1="6" y1="6" x2="18" y2="18"></line>
|
<line x1="6" y1="6" x2="18" y2="18"></line>
|
||||||
</svg>
|
</svg>
|
||||||
|
|
@ -288,22 +414,49 @@
|
||||||
<div class="course-detail">
|
<div class="course-detail">
|
||||||
<div class="course-detail-header">
|
<div class="course-detail-header">
|
||||||
<div class="course-thumbnail" id="modalThumbnail">
|
<div class="course-thumbnail" id="modalThumbnail">
|
||||||
<img src="" alt="Course thumbnail">
|
<img src="" alt="Course thumbnail" />
|
||||||
</div>
|
</div>
|
||||||
<div class="course-info">
|
<div class="course-info">
|
||||||
<div class="course-meta">
|
<div class="course-meta">
|
||||||
<span class="difficulty-badge" id="modalDifficulty">Iniciante</span>
|
<span
|
||||||
|
class="difficulty-badge"
|
||||||
|
id="modalDifficulty"
|
||||||
|
>Iniciante</span
|
||||||
|
>
|
||||||
<span class="duration" id="modalDuration">
|
<span class="duration" id="modalDuration">
|
||||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
<svg
|
||||||
|
width="14"
|
||||||
|
height="14"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="2"
|
||||||
|
>
|
||||||
<circle cx="12" cy="12" r="10"></circle>
|
<circle cx="12" cy="12" r="10"></circle>
|
||||||
<polyline points="12 6 12 12 16 14"></polyline>
|
<polyline
|
||||||
|
points="12 6 12 12 16 14"
|
||||||
|
></polyline>
|
||||||
</svg>
|
</svg>
|
||||||
<span>30 min</span>
|
<span>30 min</span>
|
||||||
</span>
|
</span>
|
||||||
<span class="lessons-count" id="modalLessonsCount">
|
<span
|
||||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
class="lessons-count"
|
||||||
<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"></path>
|
id="modalLessonsCount"
|
||||||
<polyline points="14 2 14 8 20 8"></polyline>
|
>
|
||||||
|
<svg
|
||||||
|
width="14"
|
||||||
|
height="14"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="2"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"
|
||||||
|
></path>
|
||||||
|
<polyline
|
||||||
|
points="14 2 14 8 20 8"
|
||||||
|
></polyline>
|
||||||
</svg>
|
</svg>
|
||||||
<span>5 aulas</span>
|
<span>5 aulas</span>
|
||||||
</span>
|
</span>
|
||||||
|
|
@ -311,11 +464,23 @@
|
||||||
<p class="course-description" id="modalDescription">
|
<p class="course-description" id="modalDescription">
|
||||||
Descrição do curso...
|
Descrição do curso...
|
||||||
</p>
|
</p>
|
||||||
<div class="course-progress" id="modalProgress" style="display: none;">
|
<div
|
||||||
|
class="course-progress"
|
||||||
|
id="modalProgress"
|
||||||
|
style="display: none"
|
||||||
|
>
|
||||||
<div class="progress-bar">
|
<div class="progress-bar">
|
||||||
<div class="progress-fill" id="modalProgressFill" style="width: 0%"></div>
|
<div
|
||||||
|
class="progress-fill"
|
||||||
|
id="modalProgressFill"
|
||||||
|
style="width: 0%"
|
||||||
|
></div>
|
||||||
</div>
|
</div>
|
||||||
<span class="progress-text" id="modalProgressText">0% completo</span>
|
<span
|
||||||
|
class="progress-text"
|
||||||
|
id="modalProgressText"
|
||||||
|
>0% completo</span
|
||||||
|
>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -329,16 +494,28 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Quiz Section -->
|
<!-- Quiz Section -->
|
||||||
<div class="quiz-section" id="modalQuizSection" style="display: none;">
|
<div
|
||||||
|
class="quiz-section"
|
||||||
|
id="modalQuizSection"
|
||||||
|
style="display: none"
|
||||||
|
>
|
||||||
<h4 data-i18n="learn-quiz">Avaliação</h4>
|
<h4 data-i18n="learn-quiz">Avaliação</h4>
|
||||||
<div class="quiz-info">
|
<div class="quiz-info">
|
||||||
<div class="quiz-meta">
|
<div class="quiz-meta">
|
||||||
<span id="modalQuizQuestions">10 questões</span>
|
<span id="modalQuizQuestions">10 questões</span>
|
||||||
<span id="modalQuizTime">15 min</span>
|
<span id="modalQuizTime">15 min</span>
|
||||||
<span id="modalQuizPassing">70% para aprovação</span>
|
<span id="modalQuizPassing"
|
||||||
|
>70% para aprovação</span
|
||||||
|
>
|
||||||
</div>
|
</div>
|
||||||
<button class="btn-primary" id="startQuizBtn" onclick="startQuiz()">
|
<button
|
||||||
<span data-i18n="learn-start-quiz">Iniciar Avaliação</span>
|
class="btn-primary"
|
||||||
|
id="startQuizBtn"
|
||||||
|
onclick="startQuiz()"
|
||||||
|
>
|
||||||
|
<span data-i18n="learn-start-quiz"
|
||||||
|
>Iniciar Avaliação</span
|
||||||
|
>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -348,8 +525,19 @@
|
||||||
<button class="btn-secondary" onclick="closeCourseModal()">
|
<button class="btn-secondary" onclick="closeCourseModal()">
|
||||||
<span data-i18n="learn-close">Fechar</span>
|
<span data-i18n="learn-close">Fechar</span>
|
||||||
</button>
|
</button>
|
||||||
<button class="btn-primary" id="startCourseBtn" onclick="startCourse()">
|
<button
|
||||||
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
class="btn-primary"
|
||||||
|
id="startCourseBtn"
|
||||||
|
onclick="startCourse()"
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
width="18"
|
||||||
|
height="18"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="2"
|
||||||
|
>
|
||||||
<polygon points="5 3 19 12 5 21 5 3"></polygon>
|
<polygon points="5 3 19 12 5 21 5 3"></polygon>
|
||||||
</svg>
|
</svg>
|
||||||
<span data-i18n="learn-start-course">Iniciar Curso</span>
|
<span data-i18n="learn-start-course">Iniciar Curso</span>
|
||||||
|
|
@ -365,7 +553,14 @@
|
||||||
<div class="quiz-header-info">
|
<div class="quiz-header-info">
|
||||||
<h3 id="quizTitle">Avaliação</h3>
|
<h3 id="quizTitle">Avaliação</h3>
|
||||||
<div class="quiz-timer" id="quizTimer">
|
<div class="quiz-timer" id="quizTimer">
|
||||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
<svg
|
||||||
|
width="16"
|
||||||
|
height="16"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="2"
|
||||||
|
>
|
||||||
<circle cx="12" cy="12" r="10"></circle>
|
<circle cx="12" cy="12" r="10"></circle>
|
||||||
<polyline points="12 6 12 12 16 14"></polyline>
|
<polyline points="12 6 12 12 16 14"></polyline>
|
||||||
</svg>
|
</svg>
|
||||||
|
|
@ -373,7 +568,14 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<button class="btn-icon-sm" onclick="confirmExitQuiz()">
|
<button class="btn-icon-sm" onclick="confirmExitQuiz()">
|
||||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
<svg
|
||||||
|
width="20"
|
||||||
|
height="20"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="2"
|
||||||
|
>
|
||||||
<line x1="18" y1="6" x2="6" y2="18"></line>
|
<line x1="18" y1="6" x2="6" y2="18"></line>
|
||||||
<line x1="6" y1="6" x2="18" y2="18"></line>
|
<line x1="6" y1="6" x2="18" y2="18"></line>
|
||||||
</svg>
|
</svg>
|
||||||
|
|
@ -382,9 +584,15 @@
|
||||||
<div class="modal-body">
|
<div class="modal-body">
|
||||||
<div class="quiz-progress">
|
<div class="quiz-progress">
|
||||||
<div class="quiz-progress-bar">
|
<div class="quiz-progress-bar">
|
||||||
<div class="quiz-progress-fill" id="quizProgressFill" style="width: 0%"></div>
|
<div
|
||||||
|
class="quiz-progress-fill"
|
||||||
|
id="quizProgressFill"
|
||||||
|
style="width: 0%"
|
||||||
|
></div>
|
||||||
</div>
|
</div>
|
||||||
<span class="quiz-progress-text" id="quizProgressText">Questão 1 de 10</span>
|
<span class="quiz-progress-text" id="quizProgressText"
|
||||||
|
>Questão 1 de 10</span
|
||||||
|
>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="quiz-question" id="quizQuestion">
|
<div class="quiz-question" id="quizQuestion">
|
||||||
|
|
@ -396,19 +604,47 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-footer">
|
<div class="modal-footer">
|
||||||
<button class="btn-secondary" id="prevQuestionBtn" onclick="prevQuestion()" disabled>
|
<button
|
||||||
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
class="btn-secondary"
|
||||||
|
id="prevQuestionBtn"
|
||||||
|
onclick="prevQuestion()"
|
||||||
|
disabled
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
width="18"
|
||||||
|
height="18"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="2"
|
||||||
|
>
|
||||||
<polyline points="15 18 9 12 15 6"></polyline>
|
<polyline points="15 18 9 12 15 6"></polyline>
|
||||||
</svg>
|
</svg>
|
||||||
<span data-i18n="learn-previous">Anterior</span>
|
<span data-i18n="learn-previous">Anterior</span>
|
||||||
</button>
|
</button>
|
||||||
<button class="btn-primary" id="nextQuestionBtn" onclick="nextQuestion()">
|
<button
|
||||||
|
class="btn-primary"
|
||||||
|
id="nextQuestionBtn"
|
||||||
|
onclick="nextQuestion()"
|
||||||
|
>
|
||||||
<span data-i18n="learn-next">Próxima</span>
|
<span data-i18n="learn-next">Próxima</span>
|
||||||
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
<svg
|
||||||
|
width="18"
|
||||||
|
height="18"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="2"
|
||||||
|
>
|
||||||
<polyline points="9 18 15 12 9 6"></polyline>
|
<polyline points="9 18 15 12 9 6"></polyline>
|
||||||
</svg>
|
</svg>
|
||||||
</button>
|
</button>
|
||||||
<button class="btn-primary" id="submitQuizBtn" onclick="submitQuiz()" style="display: none;">
|
<button
|
||||||
|
class="btn-primary"
|
||||||
|
id="submitQuizBtn"
|
||||||
|
onclick="submitQuiz()"
|
||||||
|
style="display: none"
|
||||||
|
>
|
||||||
<span data-i18n="learn-submit">Enviar Respostas</span>
|
<span data-i18n="learn-submit">Enviar Respostas</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -421,7 +657,14 @@
|
||||||
<div class="modal-header">
|
<div class="modal-header">
|
||||||
<h3 data-i18n="learn-quiz-result">Resultado da Avaliação</h3>
|
<h3 data-i18n="learn-quiz-result">Resultado da Avaliação</h3>
|
||||||
<button class="btn-icon-sm" onclick="closeQuizResult()">
|
<button class="btn-icon-sm" onclick="closeQuizResult()">
|
||||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
<svg
|
||||||
|
width="20"
|
||||||
|
height="20"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="2"
|
||||||
|
>
|
||||||
<line x1="18" y1="6" x2="6" y2="18"></line>
|
<line x1="18" y1="6" x2="6" y2="18"></line>
|
||||||
<line x1="6" y1="6" x2="18" y2="18"></line>
|
<line x1="6" y1="6" x2="18" y2="18"></line>
|
||||||
</svg>
|
</svg>
|
||||||
|
|
@ -431,42 +674,84 @@
|
||||||
<div class="quiz-result" id="quizResult">
|
<div class="quiz-result" id="quizResult">
|
||||||
<div class="result-icon" id="resultIcon">🎉</div>
|
<div class="result-icon" id="resultIcon">🎉</div>
|
||||||
<h2 class="result-title" id="resultTitle">Parabéns!</h2>
|
<h2 class="result-title" id="resultTitle">Parabéns!</h2>
|
||||||
<p class="result-message" id="resultMessage">Você passou na avaliação!</p>
|
<p class="result-message" id="resultMessage">
|
||||||
|
Você passou na avaliação!
|
||||||
|
</p>
|
||||||
<div class="result-score">
|
<div class="result-score">
|
||||||
<div class="score-circle" id="scoreCircle">
|
<div class="score-circle" id="scoreCircle">
|
||||||
<span class="score-value" id="scoreValue">85%</span>
|
<span class="score-value" id="scoreValue">85%</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="score-details">
|
<div class="score-details">
|
||||||
<div class="score-detail">
|
<div class="score-detail">
|
||||||
<span class="detail-label" data-i18n="learn-correct-answers">Acertos</span>
|
<span
|
||||||
<span class="detail-value" id="correctAnswers">8/10</span>
|
class="detail-label"
|
||||||
|
data-i18n="learn-correct-answers"
|
||||||
|
>Acertos</span
|
||||||
|
>
|
||||||
|
<span class="detail-value" id="correctAnswers"
|
||||||
|
>8/10</span
|
||||||
|
>
|
||||||
</div>
|
</div>
|
||||||
<div class="score-detail">
|
<div class="score-detail">
|
||||||
<span class="detail-label" data-i18n="learn-time-taken">Tempo</span>
|
<span
|
||||||
<span class="detail-value" id="timeTaken">12:30</span>
|
class="detail-label"
|
||||||
|
data-i18n="learn-time-taken"
|
||||||
|
>Tempo</span
|
||||||
|
>
|
||||||
|
<span class="detail-value" id="timeTaken"
|
||||||
|
>12:30</span
|
||||||
|
>
|
||||||
</div>
|
</div>
|
||||||
<div class="score-detail">
|
<div class="score-detail">
|
||||||
<span class="detail-label" data-i18n="learn-attempt">Tentativa</span>
|
<span
|
||||||
<span class="detail-value" id="attemptNumber">1</span>
|
class="detail-label"
|
||||||
|
data-i18n="learn-attempt"
|
||||||
|
>Tentativa</span
|
||||||
|
>
|
||||||
|
<span class="detail-value" id="attemptNumber"
|
||||||
|
>1</span
|
||||||
|
>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="result-certificate" id="resultCertificate" style="display: none;">
|
<div
|
||||||
<p data-i18n="learn-certificate-earned">🏆 Certificado conquistado!</p>
|
class="result-certificate"
|
||||||
<button class="btn-secondary" onclick="downloadCertificate()">
|
id="resultCertificate"
|
||||||
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
style="display: none"
|
||||||
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"></path>
|
>
|
||||||
|
<p data-i18n="learn-certificate-earned">
|
||||||
|
🏆 Certificado conquistado!
|
||||||
|
</p>
|
||||||
|
<button
|
||||||
|
class="btn-secondary"
|
||||||
|
onclick="downloadCertificate()"
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
width="18"
|
||||||
|
height="18"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="2"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"
|
||||||
|
></path>
|
||||||
<polyline points="7 10 12 15 17 10"></polyline>
|
<polyline points="7 10 12 15 17 10"></polyline>
|
||||||
<line x1="12" y1="15" x2="12" y2="3"></line>
|
<line x1="12" y1="15" x2="12" y2="3"></line>
|
||||||
</svg>
|
</svg>
|
||||||
<span data-i18n="learn-download-certificate">Baixar Certificado</span>
|
<span data-i18n="learn-download-certificate"
|
||||||
|
>Baixar Certificado</span
|
||||||
|
>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-footer">
|
<div class="modal-footer">
|
||||||
<button class="btn-secondary" onclick="reviewAnswers()">
|
<button class="btn-secondary" onclick="reviewAnswers()">
|
||||||
<span data-i18n="learn-review-answers">Revisar Respostas</span>
|
<span data-i18n="learn-review-answers"
|
||||||
|
>Revisar Respostas</span
|
||||||
|
>
|
||||||
</button>
|
</button>
|
||||||
<button class="btn-primary" onclick="closeQuizResult()">
|
<button class="btn-primary" onclick="closeQuizResult()">
|
||||||
<span data-i18n="learn-continue">Continuar</span>
|
<span data-i18n="learn-continue">Continuar</span>
|
||||||
|
|
@ -480,21 +765,50 @@
|
||||||
<div class="modal-content modal-fullscreen">
|
<div class="modal-content modal-fullscreen">
|
||||||
<div class="modal-header">
|
<div class="modal-header">
|
||||||
<div class="lesson-nav">
|
<div class="lesson-nav">
|
||||||
<button class="btn-icon-sm" onclick="prevLesson()" id="prevLessonBtn">
|
<button
|
||||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
class="btn-icon-sm"
|
||||||
|
onclick="prevLesson()"
|
||||||
|
id="prevLessonBtn"
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
width="20"
|
||||||
|
height="20"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="2"
|
||||||
|
>
|
||||||
<polyline points="15 18 9 12 15 6"></polyline>
|
<polyline points="15 18 9 12 15 6"></polyline>
|
||||||
</svg>
|
</svg>
|
||||||
</button>
|
</button>
|
||||||
<span id="lessonNavTitle">Aula 1 de 5</span>
|
<span id="lessonNavTitle">Aula 1 de 5</span>
|
||||||
<button class="btn-icon-sm" onclick="nextLesson()" id="nextLessonBtn">
|
<button
|
||||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
class="btn-icon-sm"
|
||||||
|
onclick="nextLesson()"
|
||||||
|
id="nextLessonBtn"
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
width="20"
|
||||||
|
height="20"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="2"
|
||||||
|
>
|
||||||
<polyline points="9 18 15 12 9 6"></polyline>
|
<polyline points="9 18 15 12 9 6"></polyline>
|
||||||
</svg>
|
</svg>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<h3 id="lessonTitle">Título da Aula</h3>
|
<h3 id="lessonTitle">Título da Aula</h3>
|
||||||
<button class="btn-icon-sm" onclick="closeLessonModal()">
|
<button class="btn-icon-sm" onclick="closeLessonModal()">
|
||||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
<svg
|
||||||
|
width="20"
|
||||||
|
height="20"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="2"
|
||||||
|
>
|
||||||
<line x1="18" y1="6" x2="6" y2="18"></line>
|
<line x1="18" y1="6" x2="6" y2="18"></line>
|
||||||
<line x1="6" y1="6" x2="18" y2="18"></line>
|
<line x1="6" y1="6" x2="18" y2="18"></line>
|
||||||
</svg>
|
</svg>
|
||||||
|
|
@ -514,14 +828,31 @@
|
||||||
<div class="modal-footer">
|
<div class="modal-footer">
|
||||||
<div class="lesson-progress">
|
<div class="lesson-progress">
|
||||||
<div class="progress-bar">
|
<div class="progress-bar">
|
||||||
<div class="progress-fill" id="lessonProgressFill" style="width: 0%"></div>
|
<div
|
||||||
|
class="progress-fill"
|
||||||
|
id="lessonProgressFill"
|
||||||
|
style="width: 0%"
|
||||||
|
></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<button class="btn-primary" id="completeLessonBtn" onclick="completeLesson()">
|
<button
|
||||||
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
class="btn-primary"
|
||||||
|
id="completeLessonBtn"
|
||||||
|
onclick="completeLesson()"
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
width="18"
|
||||||
|
height="18"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="2"
|
||||||
|
>
|
||||||
<polyline points="20 6 9 17 4 12"></polyline>
|
<polyline points="20 6 9 17 4 12"></polyline>
|
||||||
</svg>
|
</svg>
|
||||||
<span data-i18n="learn-mark-complete">Marcar como Concluída</span>
|
<span data-i18n="learn-mark-complete"
|
||||||
|
>Marcar como Concluída</span
|
||||||
|
>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -533,7 +864,14 @@
|
||||||
<div class="modal-header">
|
<div class="modal-header">
|
||||||
<h3 data-i18n="learn-certificate">Certificado</h3>
|
<h3 data-i18n="learn-certificate">Certificado</h3>
|
||||||
<button class="btn-icon-sm" onclick="closeCertificateModal()">
|
<button class="btn-icon-sm" onclick="closeCertificateModal()">
|
||||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
<svg
|
||||||
|
width="20"
|
||||||
|
height="20"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="2"
|
||||||
|
>
|
||||||
<line x1="18" y1="6" x2="6" y2="18"></line>
|
<line x1="18" y1="6" x2="6" y2="18"></line>
|
||||||
<line x1="6" y1="6" x2="18" y2="18"></line>
|
<line x1="6" y1="6" x2="18" y2="18"></line>
|
||||||
</svg>
|
</svg>
|
||||||
|
|
@ -552,12 +890,27 @@
|
||||||
<h3 id="certUserName">Nome do Usuário</h3>
|
<h3 id="certUserName">Nome do Usuário</h3>
|
||||||
<p>concluiu com sucesso o curso</p>
|
<p>concluiu com sucesso o curso</p>
|
||||||
<h4 id="certCourseName">Nome do Curso</h4>
|
<h4 id="certCourseName">Nome do Curso</h4>
|
||||||
<p class="cert-score">com aproveitamento de <strong id="certScore">85%</strong></p>
|
<p class="cert-score">
|
||||||
|
com aproveitamento de
|
||||||
|
<strong id="certScore">85%</strong>
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="certificate-footer">
|
<div class="certificate-footer">
|
||||||
<div class="cert-date">
|
<div class="cert-date">
|
||||||
<span data-i18n="learn-issued-on">Emitido em</span>
|
<span data-i18n="learn-issued-on"
|
||||||
|
>Emitido em</span
|
||||||
|
>
|
||||||
<strong id="certDate">01/01/2025</strong>
|
<strong id="certDate">01/01/2025</strong>
|
||||||
</div>
|
</div>
|
||||||
<div class="cert-code">
|
<div class="cert-code">
|
||||||
<span data-i18n="learn-verification-code">Código de Verificação
|
<span data-i18n="learn-verification-code"
|
||||||
|
>Código de Verificação
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,5 @@
|
||||||
|
<link rel="stylesheet" href="mail/mail.css" />
|
||||||
|
|
||||||
<div class="mail-layout">
|
<div class="mail-layout">
|
||||||
<!-- Sidebar -->
|
<!-- Sidebar -->
|
||||||
<div class="panel mail-sidebar">
|
<div class="panel mail-sidebar">
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,5 @@
|
||||||
|
<link rel="stylesheet" href="meet/meet.css" />
|
||||||
|
|
||||||
<!-- Meet - Video Conferencing -->
|
<!-- Meet - Video Conferencing -->
|
||||||
<div class="meet-container" id="meet-app">
|
<div class="meet-container" id="meet-app">
|
||||||
<!-- Header -->
|
<!-- Header -->
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
<!-- Alerts partial -->
|
<!-- Alerts partial -->
|
||||||
<link rel="stylesheet" href="/static/suite/monitoring/alerts.css">
|
<link rel="stylesheet" href="monitoring/alerts.css">
|
||||||
<div class="alerts-container">
|
<div class="alerts-container">
|
||||||
<!-- Alerts Header -->
|
<!-- Alerts Header -->
|
||||||
<div class="alerts-header">
|
<div class="alerts-header">
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
<!-- Health partial -->
|
<!-- Health partial -->
|
||||||
<link rel="stylesheet" href="/static/suite/monitoring/health.css">
|
<link rel="stylesheet" href="monitoring/health.css">
|
||||||
<div class="health-container">
|
<div class="health-container">
|
||||||
<!-- Health Overview -->
|
<!-- Health Overview -->
|
||||||
<div class="health-overview"
|
<div class="health-overview"
|
||||||
|
|
|
||||||
|
|
@ -491,5 +491,5 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<link rel="stylesheet" href="/static/suite/monitoring/monitoring.css" />
|
<link rel="stylesheet" href="monitoring/monitoring.css" />
|
||||||
<script src="/static/suite/monitoring/monitoring.js"></script>
|
<script src="/static/suite/monitoring/monitoring.js"></script>
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
<!-- Logs partial -->
|
<!-- Logs partial -->
|
||||||
<link rel="stylesheet" href="/static/suite/monitoring/logs.css" />
|
<link rel="stylesheet" href="monitoring/logs.css" />
|
||||||
<div class="logs-container">
|
<div class="logs-container">
|
||||||
<!-- Logs Header -->
|
<!-- Logs Header -->
|
||||||
<div class="logs-header">
|
<div class="logs-header">
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
<!-- Metrics partial -->
|
<!-- Metrics partial -->
|
||||||
<link rel="stylesheet" href="/static/suite/monitoring/metrics.css">
|
<link rel="stylesheet" href="monitoring/metrics.css">
|
||||||
<div class="metrics-container">
|
<div class="metrics-container">
|
||||||
<!-- Metrics Header -->
|
<!-- Metrics Header -->
|
||||||
<div class="metrics-header">
|
<div class="metrics-header">
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,5 @@
|
||||||
|
<link rel="stylesheet" href="monitoring/monitoring.css" />
|
||||||
|
|
||||||
<div class="monitoring-container" id="monitoring-app">
|
<div class="monitoring-container" id="monitoring-app">
|
||||||
<header class="monitoring-header">
|
<header class="monitoring-header">
|
||||||
<h2>
|
<h2>
|
||||||
|
|
@ -1374,7 +1376,6 @@
|
||||||
hx-swap="innerHTML"
|
hx-swap="innerHTML"
|
||||||
></div>
|
></div>
|
||||||
|
|
||||||
|
<link rel="stylesheet" href="monitoring/monitoring.css" />
|
||||||
<link rel="stylesheet" href="monitoring.css" />
|
<script src="monitoring/monitoring.js"></script>
|
||||||
<script src="monitoring.js"></script>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
<!-- Resources partial -->
|
<!-- Resources partial -->
|
||||||
<link rel="stylesheet" href="/static/suite/monitoring/resources.css">
|
<link rel="stylesheet" href="monitoring/resources.css">
|
||||||
<div class="resources-container">
|
<div class="resources-container">
|
||||||
<!-- Resource Overview Cards -->
|
<!-- Resource Overview Cards -->
|
||||||
<div class="resource-cards">
|
<div class="resource-cards">
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,14 @@
|
||||||
|
<link rel="stylesheet" href="monitoring/services.css" />
|
||||||
|
|
||||||
<div class="services-container">
|
<div class="services-container">
|
||||||
<!-- Services Header -->
|
<!-- Services Header -->
|
||||||
<div class="services-header">
|
<div class="services-header">
|
||||||
<div class="header-stats"
|
<div
|
||||||
hx-get="/api/services/summary"
|
class="header-stats"
|
||||||
hx-trigger="load, every 10s"
|
hx-get="/api/services/summary"
|
||||||
hx-swap="innerHTML">
|
hx-trigger="load, every 10s"
|
||||||
|
hx-swap="innerHTML"
|
||||||
|
>
|
||||||
<div class="stat-item running">
|
<div class="stat-item running">
|
||||||
<span class="stat-number">--</span>
|
<span class="stat-number">--</span>
|
||||||
<span class="stat-label">Running</span>
|
<span class="stat-label">Running</span>
|
||||||
|
|
@ -24,14 +28,23 @@
|
||||||
</div>
|
</div>
|
||||||
<div class="header-actions">
|
<div class="header-actions">
|
||||||
<div class="search-box">
|
<div class="search-box">
|
||||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
<svg
|
||||||
|
width="16"
|
||||||
|
height="16"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="2"
|
||||||
|
>
|
||||||
<circle cx="11" cy="11" r="8"></circle>
|
<circle cx="11" cy="11" r="8"></circle>
|
||||||
<line x1="21" y1="21" x2="16.65" y2="16.65"></line>
|
<line x1="21" y1="21" x2="16.65" y2="16.65"></line>
|
||||||
</svg>
|
</svg>
|
||||||
<input type="text"
|
<input
|
||||||
id="service-search"
|
type="text"
|
||||||
placeholder="Search services..."
|
id="service-search"
|
||||||
onkeyup="filterServices(this.value)">
|
placeholder="Search services..."
|
||||||
|
onkeyup="filterServices(this.value)"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<select id="status-filter" onchange="filterByStatus(this.value)">
|
<select id="status-filter" onchange="filterByStatus(this.value)">
|
||||||
<option value="all">All Status</option>
|
<option value="all">All Status</option>
|
||||||
|
|
@ -39,8 +52,19 @@
|
||||||
<option value="warning">Warning</option>
|
<option value="warning">Warning</option>
|
||||||
<option value="stopped">Stopped</option>
|
<option value="stopped">Stopped</option>
|
||||||
</select>
|
</select>
|
||||||
<button class="action-btn" onclick="restartAllServices()" title="Restart All">
|
<button
|
||||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
class="action-btn"
|
||||||
|
onclick="restartAllServices()"
|
||||||
|
title="Restart All"
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
width="16"
|
||||||
|
height="16"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="2"
|
||||||
|
>
|
||||||
<polyline points="23 4 23 10 17 10"></polyline>
|
<polyline points="23 4 23 10 17 10"></polyline>
|
||||||
<path d="M20.49 15a9 9 0 1 1-2.12-9.36L23 10"></path>
|
<path d="M20.49 15a9 9 0 1 1-2.12-9.36L23 10"></path>
|
||||||
</svg>
|
</svg>
|
||||||
|
|
@ -50,10 +74,13 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Services Grid -->
|
<!-- Services Grid -->
|
||||||
<div class="services-grid" id="services-grid"
|
<div
|
||||||
hx-get="/api/services/status"
|
class="services-grid"
|
||||||
hx-trigger="load, every 10s"
|
id="services-grid"
|
||||||
hx-swap="innerHTML">
|
hx-get="/api/services/status"
|
||||||
|
hx-trigger="load, every 10s"
|
||||||
|
hx-swap="innerHTML"
|
||||||
|
>
|
||||||
<!-- Loading placeholder -->
|
<!-- Loading placeholder -->
|
||||||
<div class="service-card skeleton">
|
<div class="service-card skeleton">
|
||||||
<div class="skeleton-line"></div>
|
<div class="skeleton-line"></div>
|
||||||
|
|
@ -78,7 +105,14 @@
|
||||||
<div class="panel-header">
|
<div class="panel-header">
|
||||||
<h3 id="detail-service-name">Service Details</h3>
|
<h3 id="detail-service-name">Service Details</h3>
|
||||||
<button class="close-btn" onclick="closeServiceDetail()">
|
<button class="close-btn" onclick="closeServiceDetail()">
|
||||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
<svg
|
||||||
|
width="20"
|
||||||
|
height="20"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="2"
|
||||||
|
>
|
||||||
<line x1="18" y1="6" x2="6" y2="18"></line>
|
<line x1="18" y1="6" x2="6" y2="18"></line>
|
||||||
<line x1="6" y1="6" x2="18" y2="18"></line>
|
<line x1="6" y1="6" x2="18" y2="18"></line>
|
||||||
</svg>
|
</svg>
|
||||||
|
|
@ -95,9 +129,30 @@
|
||||||
<div class="service-card" data-status="running" data-service="service-name">
|
<div class="service-card" data-status="running" data-service="service-name">
|
||||||
<div class="card-header">
|
<div class="card-header">
|
||||||
<div class="service-icon">
|
<div class="service-icon">
|
||||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
<svg
|
||||||
<rect x="2" y="2" width="20" height="8" rx="2" ry="2"></rect>
|
width="24"
|
||||||
<rect x="2" y="14" width="20" height="8" rx="2" ry="2"></rect>
|
height="24"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="2"
|
||||||
|
>
|
||||||
|
<rect
|
||||||
|
x="2"
|
||||||
|
y="2"
|
||||||
|
width="20"
|
||||||
|
height="8"
|
||||||
|
rx="2"
|
||||||
|
ry="2"
|
||||||
|
></rect>
|
||||||
|
<rect
|
||||||
|
x="2"
|
||||||
|
y="14"
|
||||||
|
width="20"
|
||||||
|
height="8"
|
||||||
|
rx="2"
|
||||||
|
ry="2"
|
||||||
|
></rect>
|
||||||
<line x1="6" y1="6" x2="6.01" y2="6"></line>
|
<line x1="6" y1="6" x2="6.01" y2="6"></line>
|
||||||
<line x1="6" y1="18" x2="6.01" y2="18"></line>
|
<line x1="6" y1="18" x2="6.01" y2="18"></line>
|
||||||
</svg>
|
</svg>
|
||||||
|
|
@ -112,21 +167,42 @@
|
||||||
<p class="service-description">Service description goes here</p>
|
<p class="service-description">Service description goes here</p>
|
||||||
<div class="service-meta">
|
<div class="service-meta">
|
||||||
<span class="meta-item">
|
<span class="meta-item">
|
||||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
<svg
|
||||||
|
width="14"
|
||||||
|
height="14"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="2"
|
||||||
|
>
|
||||||
<circle cx="12" cy="12" r="10"></circle>
|
<circle cx="12" cy="12" r="10"></circle>
|
||||||
<polyline points="12 6 12 12 16 14"></polyline>
|
<polyline points="12 6 12 12 16 14"></polyline>
|
||||||
</svg>
|
</svg>
|
||||||
Uptime: 24d 5h
|
Uptime: 24d 5h
|
||||||
</span>
|
</span>
|
||||||
<span class="meta-item">
|
<span class="meta-item">
|
||||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
<svg
|
||||||
|
width="14"
|
||||||
|
height="14"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="2"
|
||||||
|
>
|
||||||
<rect x="4" y="4" width="16" height="16" rx="2"></rect>
|
<rect x="4" y="4" width="16" height="16" rx="2"></rect>
|
||||||
<rect x="9" y="9" width="6" height="6"></rect>
|
<rect x="9" y="9" width="6" height="6"></rect>
|
||||||
</svg>
|
</svg>
|
||||||
CPU: 12%
|
CPU: 12%
|
||||||
</span>
|
</span>
|
||||||
<span class="meta-item">
|
<span class="meta-item">
|
||||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
<svg
|
||||||
|
width="14"
|
||||||
|
height="14"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="2"
|
||||||
|
>
|
||||||
<rect x="2" y="6" width="20" height="12" rx="2"></rect>
|
<rect x="2" y="6" width="20" height="12" rx="2"></rect>
|
||||||
</svg>
|
</svg>
|
||||||
Mem: 256MB
|
Mem: 256MB
|
||||||
|
|
@ -134,28 +210,74 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="card-actions">
|
<div class="card-actions">
|
||||||
<button class="card-btn" onclick="viewServiceDetails('service-id')" title="Details">
|
<button
|
||||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
class="card-btn"
|
||||||
|
onclick="viewServiceDetails('service-id')"
|
||||||
|
title="Details"
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
width="16"
|
||||||
|
height="16"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="2"
|
||||||
|
>
|
||||||
<circle cx="12" cy="12" r="10"></circle>
|
<circle cx="12" cy="12" r="10"></circle>
|
||||||
<line x1="12" y1="16" x2="12" y2="12"></line>
|
<line x1="12" y1="16" x2="12" y2="12"></line>
|
||||||
<line x1="12" y1="8" x2="12.01" y2="8"></line>
|
<line x1="12" y1="8" x2="12.01" y2="8"></line>
|
||||||
</svg>
|
</svg>
|
||||||
</button>
|
</button>
|
||||||
<button class="card-btn" onclick="restartService('service-id')" title="Restart">
|
<button
|
||||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
class="card-btn"
|
||||||
|
onclick="restartService('service-id')"
|
||||||
|
title="Restart"
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
width="16"
|
||||||
|
height="16"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="2"
|
||||||
|
>
|
||||||
<polyline points="23 4 23 10 17 10"></polyline>
|
<polyline points="23 4 23 10 17 10"></polyline>
|
||||||
<path d="M20.49 15a9 9 0 1 1-2.12-9.36L23 10"></path>
|
<path d="M20.49 15a9 9 0 1 1-2.12-9.36L23 10"></path>
|
||||||
</svg>
|
</svg>
|
||||||
</button>
|
</button>
|
||||||
<button class="card-btn" onclick="stopService('service-id')" title="Stop">
|
<button
|
||||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
class="card-btn"
|
||||||
|
onclick="stopService('service-id')"
|
||||||
|
title="Stop"
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
width="16"
|
||||||
|
height="16"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="2"
|
||||||
|
>
|
||||||
<rect x="6" y="4" width="4" height="16"></rect>
|
<rect x="6" y="4" width="4" height="16"></rect>
|
||||||
<rect x="14" y="4" width="4" height="16"></rect>
|
<rect x="14" y="4" width="4" height="16"></rect>
|
||||||
</svg>
|
</svg>
|
||||||
</button>
|
</button>
|
||||||
<button class="card-btn" onclick="viewServiceLogs('service-id')" title="Logs">
|
<button
|
||||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
class="card-btn"
|
||||||
<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"></path>
|
onclick="viewServiceLogs('service-id')"
|
||||||
|
title="Logs"
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
width="16"
|
||||||
|
height="16"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="2"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"
|
||||||
|
></path>
|
||||||
<polyline points="14 2 14 8 20 8"></polyline>
|
<polyline points="14 2 14 8 20 8"></polyline>
|
||||||
</svg>
|
</svg>
|
||||||
</button>
|
</button>
|
||||||
|
|
@ -167,73 +289,76 @@
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
function filterServices(query) {
|
function filterServices(query) {
|
||||||
const cards = document.querySelectorAll('.service-card:not(.skeleton)');
|
const cards = document.querySelectorAll(".service-card:not(.skeleton)");
|
||||||
const lowerQuery = query.toLowerCase();
|
const lowerQuery = query.toLowerCase();
|
||||||
|
|
||||||
cards.forEach(card => {
|
cards.forEach((card) => {
|
||||||
const name = card.dataset.service?.toLowerCase() || '';
|
const name = card.dataset.service?.toLowerCase() || "";
|
||||||
const text = card.textContent.toLowerCase();
|
const text = card.textContent.toLowerCase();
|
||||||
const matches = name.includes(lowerQuery) || text.includes(lowerQuery);
|
const matches =
|
||||||
card.classList.toggle('hidden', !matches);
|
name.includes(lowerQuery) || text.includes(lowerQuery);
|
||||||
|
card.classList.toggle("hidden", !matches);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function filterByStatus(status) {
|
function filterByStatus(status) {
|
||||||
const cards = document.querySelectorAll('.service-card:not(.skeleton)');
|
const cards = document.querySelectorAll(".service-card:not(.skeleton)");
|
||||||
|
|
||||||
cards.forEach(card => {
|
cards.forEach((card) => {
|
||||||
if (status === 'all') {
|
if (status === "all") {
|
||||||
card.classList.remove('hidden');
|
card.classList.remove("hidden");
|
||||||
} else {
|
} else {
|
||||||
const cardStatus = card.dataset.status;
|
const cardStatus = card.dataset.status;
|
||||||
card.classList.toggle('hidden', cardStatus !== status);
|
card.classList.toggle("hidden", cardStatus !== status);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function viewServiceDetails(serviceId) {
|
function viewServiceDetails(serviceId) {
|
||||||
const panel = document.getElementById('service-detail-panel');
|
const panel = document.getElementById("service-detail-panel");
|
||||||
const content = document.getElementById('service-detail-content');
|
const content = document.getElementById("service-detail-content");
|
||||||
|
|
||||||
// Load service details via HTMX
|
// Load service details via HTMX
|
||||||
htmx.ajax('GET', `/api/services/${serviceId}/details`, {
|
htmx.ajax("GET", `/api/services/${serviceId}/details`, {
|
||||||
target: content,
|
target: content,
|
||||||
swap: 'innerHTML'
|
swap: "innerHTML",
|
||||||
});
|
});
|
||||||
|
|
||||||
document.getElementById('detail-service-name').textContent = serviceId;
|
document.getElementById("detail-service-name").textContent = serviceId;
|
||||||
panel.classList.add('open');
|
panel.classList.add("open");
|
||||||
}
|
}
|
||||||
|
|
||||||
function closeServiceDetail() {
|
function closeServiceDetail() {
|
||||||
document.getElementById('service-detail-panel').classList.remove('open');
|
document
|
||||||
|
.getElementById("service-detail-panel")
|
||||||
|
.classList.remove("open");
|
||||||
}
|
}
|
||||||
|
|
||||||
function restartService(serviceId) {
|
function restartService(serviceId) {
|
||||||
if (confirm(`Are you sure you want to restart ${serviceId}?`)) {
|
if (confirm(`Are you sure you want to restart ${serviceId}?`)) {
|
||||||
htmx.ajax('POST', `/api/services/${serviceId}/restart`, {
|
htmx.ajax("POST", `/api/services/${serviceId}/restart`, {
|
||||||
swap: 'none'
|
swap: "none",
|
||||||
}).then(() => {
|
}).then(() => {
|
||||||
htmx.trigger('#services-grid', 'refresh');
|
htmx.trigger("#services-grid", "refresh");
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function stopService(serviceId) {
|
function stopService(serviceId) {
|
||||||
if (confirm(`Are you sure you want to stop ${serviceId}?`)) {
|
if (confirm(`Are you sure you want to stop ${serviceId}?`)) {
|
||||||
htmx.ajax('POST', `/api/services/${serviceId}/stop`, {
|
htmx.ajax("POST", `/api/services/${serviceId}/stop`, {
|
||||||
swap: 'none'
|
swap: "none",
|
||||||
}).then(() => {
|
}).then(() => {
|
||||||
htmx.trigger('#services-grid', 'refresh');
|
htmx.trigger("#services-grid", "refresh");
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function startService(serviceId) {
|
function startService(serviceId) {
|
||||||
htmx.ajax('POST', `/api/services/${serviceId}/start`, {
|
htmx.ajax("POST", `/api/services/${serviceId}/start`, {
|
||||||
swap: 'none'
|
swap: "none",
|
||||||
}).then(() => {
|
}).then(() => {
|
||||||
htmx.trigger('#services-grid', 'refresh');
|
htmx.trigger("#services-grid", "refresh");
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -243,38 +368,44 @@
|
||||||
if (logsLink) {
|
if (logsLink) {
|
||||||
logsLink.click();
|
logsLink.click();
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
const serviceFilter = document.getElementById('service-filter');
|
const serviceFilter = document.getElementById("service-filter");
|
||||||
if (serviceFilter) {
|
if (serviceFilter) {
|
||||||
serviceFilter.value = serviceId;
|
serviceFilter.value = serviceId;
|
||||||
serviceFilter.dispatchEvent(new Event('change'));
|
serviceFilter.dispatchEvent(new Event("change"));
|
||||||
}
|
}
|
||||||
}, 300);
|
}, 300);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function restartAllServices() {
|
function restartAllServices() {
|
||||||
if (confirm('Are you sure you want to restart all services? This may cause temporary downtime.')) {
|
if (
|
||||||
htmx.ajax('POST', '/api/services/restart-all', {
|
confirm(
|
||||||
swap: 'none'
|
"Are you sure you want to restart all services? This may cause temporary downtime.",
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
htmx.ajax("POST", "/api/services/restart-all", {
|
||||||
|
swap: "none",
|
||||||
}).then(() => {
|
}).then(() => {
|
||||||
htmx.trigger('#services-grid', 'refresh');
|
htmx.trigger("#services-grid", "refresh");
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Close panel on escape key
|
// Close panel on escape key
|
||||||
document.addEventListener('keydown', function(e) {
|
document.addEventListener("keydown", function (e) {
|
||||||
if (e.key === 'Escape') {
|
if (e.key === "Escape") {
|
||||||
closeServiceDetail();
|
closeServiceDetail();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Close panel when clicking outside
|
// Close panel when clicking outside
|
||||||
document.addEventListener('click', function(e) {
|
document.addEventListener("click", function (e) {
|
||||||
const panel = document.getElementById('service-detail-panel');
|
const panel = document.getElementById("service-detail-panel");
|
||||||
if (panel.classList.contains('open') &&
|
if (
|
||||||
|
panel.classList.contains("open") &&
|
||||||
!panel.contains(e.target) &&
|
!panel.contains(e.target) &&
|
||||||
!e.target.closest('.card-btn')) {
|
!e.target.closest(".card-btn")
|
||||||
|
) {
|
||||||
closeServiceDetail();
|
closeServiceDetail();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,5 @@
|
||||||
|
<link rel="stylesheet" href="paper/paper.css" />
|
||||||
|
|
||||||
<!-- Paper - AI Writing & Notes -->
|
<!-- Paper - AI Writing & Notes -->
|
||||||
<div class="paper-container" id="paper-app">
|
<div class="paper-container" id="paper-app">
|
||||||
<!-- Sidebar - Notes List -->
|
<!-- Sidebar - Notes List -->
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,14 @@
|
||||||
</div>
|
</div>
|
||||||
<div class="header-actions">
|
<div class="header-actions">
|
||||||
<div class="search-box">
|
<div class="search-box">
|
||||||
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
<svg
|
||||||
|
width="18"
|
||||||
|
height="18"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="2"
|
||||||
|
>
|
||||||
<circle cx="11" cy="11" r="8"></circle>
|
<circle cx="11" cy="11" r="8"></circle>
|
||||||
<line x1="21" y1="21" x2="16.65" y2="16.65"></line>
|
<line x1="21" y1="21" x2="16.65" y2="16.65"></line>
|
||||||
</svg>
|
</svg>
|
||||||
|
|
@ -23,11 +30,18 @@
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<button class="btn btn-primary" onclick="openAddContact()">
|
<button class="btn btn-primary" onclick="openAddContact()">
|
||||||
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
<svg
|
||||||
<path d="M16 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"/>
|
width="18"
|
||||||
<circle cx="8.5" cy="7" r="4"/>
|
height="18"
|
||||||
<line x1="20" y1="8" x2="20" y2="14"/>
|
viewBox="0 0 24 24"
|
||||||
<line x1="23" y1="11" x2="17" y2="11"/>
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="2"
|
||||||
|
>
|
||||||
|
<path d="M16 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2" />
|
||||||
|
<circle cx="8.5" cy="7" r="4" />
|
||||||
|
<line x1="20" y1="8" x2="20" y2="14" />
|
||||||
|
<line x1="23" y1="11" x2="17" y2="11" />
|
||||||
</svg>
|
</svg>
|
||||||
<span data-i18n="people-add">Add Contact</span>
|
<span data-i18n="people-add">Add Contact</span>
|
||||||
</button>
|
</button>
|
||||||
|
|
@ -36,33 +50,83 @@
|
||||||
|
|
||||||
<!-- Tab Navigation -->
|
<!-- Tab Navigation -->
|
||||||
<nav class="tab-nav" role="tablist">
|
<nav class="tab-nav" role="tablist">
|
||||||
<button class="tab-btn active" role="tab" aria-selected="true" onclick="showTab('contacts', this)">
|
<button
|
||||||
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
class="tab-btn active"
|
||||||
<path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"/>
|
role="tab"
|
||||||
<circle cx="9" cy="7" r="4"/>
|
aria-selected="true"
|
||||||
|
onclick="showTab('contacts', this)"
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
width="18"
|
||||||
|
height="18"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="2"
|
||||||
|
>
|
||||||
|
<path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2" />
|
||||||
|
<circle cx="9" cy="7" r="4" />
|
||||||
</svg>
|
</svg>
|
||||||
<span data-i18n="people-tab-contacts">Contacts</span>
|
<span data-i18n="people-tab-contacts">Contacts</span>
|
||||||
</button>
|
</button>
|
||||||
<button class="tab-btn" role="tab" aria-selected="false" onclick="showTab('groups', this)">
|
<button
|
||||||
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
class="tab-btn"
|
||||||
<path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"/>
|
role="tab"
|
||||||
<circle cx="9" cy="7" r="4"/>
|
aria-selected="false"
|
||||||
<path d="M23 21v-2a4 4 0 0 0-3-3.87"/>
|
onclick="showTab('groups', this)"
|
||||||
<path d="M16 3.13a4 4 0 0 1 0 7.75"/>
|
>
|
||||||
|
<svg
|
||||||
|
width="18"
|
||||||
|
height="18"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="2"
|
||||||
|
>
|
||||||
|
<path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2" />
|
||||||
|
<circle cx="9" cy="7" r="4" />
|
||||||
|
<path d="M23 21v-2a4 4 0 0 0-3-3.87" />
|
||||||
|
<path d="M16 3.13a4 4 0 0 1 0 7.75" />
|
||||||
</svg>
|
</svg>
|
||||||
<span data-i18n="people-tab-groups">Groups</span>
|
<span data-i18n="people-tab-groups">Groups</span>
|
||||||
</button>
|
</button>
|
||||||
<button class="tab-btn" role="tab" aria-selected="false" onclick="showTab('directory', this)">
|
<button
|
||||||
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
class="tab-btn"
|
||||||
<path d="M4 19.5A2.5 2.5 0 0 1 6.5 17H20"/>
|
role="tab"
|
||||||
<path d="M6.5 2H20v20H6.5A2.5 2.5 0 0 1 4 19.5v-15A2.5 2.5 0 0 1 6.5 2z"/>
|
aria-selected="false"
|
||||||
|
onclick="showTab('directory', this)"
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
width="18"
|
||||||
|
height="18"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="2"
|
||||||
|
>
|
||||||
|
<path d="M4 19.5A2.5 2.5 0 0 1 6.5 17H20" />
|
||||||
|
<path
|
||||||
|
d="M6.5 2H20v20H6.5A2.5 2.5 0 0 1 4 19.5v-15A2.5 2.5 0 0 1 6.5 2z"
|
||||||
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
<span data-i18n="people-tab-directory">Directory</span>
|
<span data-i18n="people-tab-directory">Directory</span>
|
||||||
</button>
|
</button>
|
||||||
<button class="tab-btn" role="tab" aria-selected="false" onclick="showTab('recent', this)">
|
<button
|
||||||
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
class="tab-btn"
|
||||||
<circle cx="12" cy="12" r="10"/>
|
role="tab"
|
||||||
<polyline points="12 6 12 12 16 14"/>
|
aria-selected="false"
|
||||||
|
onclick="showTab('recent', this)"
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
width="18"
|
||||||
|
height="18"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="2"
|
||||||
|
>
|
||||||
|
<circle cx="12" cy="12" r="10" />
|
||||||
|
<polyline points="12 6 12 12 16 14" />
|
||||||
</svg>
|
</svg>
|
||||||
<span data-i18n="people-tab-recent">Recent</span>
|
<span data-i18n="people-tab-recent">Recent</span>
|
||||||
</button>
|
</button>
|
||||||
|
|
@ -74,33 +138,90 @@
|
||||||
<div id="contacts-tab" class="tab-content active">
|
<div id="contacts-tab" class="tab-content active">
|
||||||
<!-- Alphabet Filter -->
|
<!-- Alphabet Filter -->
|
||||||
<div class="alphabet-filter">
|
<div class="alphabet-filter">
|
||||||
<button class="alpha-btn active" onclick="filterByLetter('all', this)">All</button>
|
<button
|
||||||
<button class="alpha-btn" onclick="filterByLetter('A', this)">A</button>
|
class="alpha-btn active"
|
||||||
<button class="alpha-btn" onclick="filterByLetter('B', this)">B</button>
|
onclick="filterByLetter('all', this)"
|
||||||
<button class="alpha-btn" onclick="filterByLetter('C', this)">C</button>
|
>
|
||||||
<button class="alpha-btn" onclick="filterByLetter('D', this)">D</button>
|
All
|
||||||
<button class="alpha-btn" onclick="filterByLetter('E', this)">E</button>
|
</button>
|
||||||
<button class="alpha-btn" onclick="filterByLetter('F', this)">F</button>
|
<button class="alpha-btn" onclick="filterByLetter('A', this)">
|
||||||
<button class="alpha-btn" onclick="filterByLetter('G', this)">G</button>
|
A
|
||||||
<button class="alpha-btn" onclick="filterByLetter('H', this)">H</button>
|
</button>
|
||||||
<button class="alpha-btn" onclick="filterByLetter('I', this)">I</button>
|
<button class="alpha-btn" onclick="filterByLetter('B', this)">
|
||||||
<button class="alpha-btn" onclick="filterByLetter('J', this)">J</button>
|
B
|
||||||
<button class="alpha-btn" onclick="filterByLetter('K', this)">K</button>
|
</button>
|
||||||
<button class="alpha-btn" onclick="filterByLetter('L', this)">L</button>
|
<button class="alpha-btn" onclick="filterByLetter('C', this)">
|
||||||
<button class="alpha-btn" onclick="filterByLetter('M', this)">M</button>
|
C
|
||||||
<button class="alpha-btn" onclick="filterByLetter('N', this)">N</button>
|
</button>
|
||||||
<button class="alpha-btn" onclick="filterByLetter('O', this)">O</button>
|
<button class="alpha-btn" onclick="filterByLetter('D', this)">
|
||||||
<button class="alpha-btn" onclick="filterByLetter('P', this)">P</button>
|
D
|
||||||
<button class="alpha-btn" onclick="filterByLetter('Q', this)">Q</button>
|
</button>
|
||||||
<button class="alpha-btn" onclick="filterByLetter('R', this)">R</button>
|
<button class="alpha-btn" onclick="filterByLetter('E', this)">
|
||||||
<button class="alpha-btn" onclick="filterByLetter('S', this)">S</button>
|
E
|
||||||
<button class="alpha-btn" onclick="filterByLetter('T', this)">T</button>
|
</button>
|
||||||
<button class="alpha-btn" onclick="filterByLetter('U', this)">U</button>
|
<button class="alpha-btn" onclick="filterByLetter('F', this)">
|
||||||
<button class="alpha-btn" onclick="filterByLetter('V', this)">V</button>
|
F
|
||||||
<button class="alpha-btn" onclick="filterByLetter('W', this)">W</button>
|
</button>
|
||||||
<button class="alpha-btn" onclick="filterByLetter('X', this)">X</button>
|
<button class="alpha-btn" onclick="filterByLetter('G', this)">
|
||||||
<button class="alpha-btn" onclick="filterByLetter('Y', this)">Y</button>
|
G
|
||||||
<button class="alpha-btn" onclick="filterByLetter('Z', this)">Z</button>
|
</button>
|
||||||
|
<button class="alpha-btn" onclick="filterByLetter('H', this)">
|
||||||
|
H
|
||||||
|
</button>
|
||||||
|
<button class="alpha-btn" onclick="filterByLetter('I', this)">
|
||||||
|
I
|
||||||
|
</button>
|
||||||
|
<button class="alpha-btn" onclick="filterByLetter('J', this)">
|
||||||
|
J
|
||||||
|
</button>
|
||||||
|
<button class="alpha-btn" onclick="filterByLetter('K', this)">
|
||||||
|
K
|
||||||
|
</button>
|
||||||
|
<button class="alpha-btn" onclick="filterByLetter('L', this)">
|
||||||
|
L
|
||||||
|
</button>
|
||||||
|
<button class="alpha-btn" onclick="filterByLetter('M', this)">
|
||||||
|
M
|
||||||
|
</button>
|
||||||
|
<button class="alpha-btn" onclick="filterByLetter('N', this)">
|
||||||
|
N
|
||||||
|
</button>
|
||||||
|
<button class="alpha-btn" onclick="filterByLetter('O', this)">
|
||||||
|
O
|
||||||
|
</button>
|
||||||
|
<button class="alpha-btn" onclick="filterByLetter('P', this)">
|
||||||
|
P
|
||||||
|
</button>
|
||||||
|
<button class="alpha-btn" onclick="filterByLetter('Q', this)">
|
||||||
|
Q
|
||||||
|
</button>
|
||||||
|
<button class="alpha-btn" onclick="filterByLetter('R', this)">
|
||||||
|
R
|
||||||
|
</button>
|
||||||
|
<button class="alpha-btn" onclick="filterByLetter('S', this)">
|
||||||
|
S
|
||||||
|
</button>
|
||||||
|
<button class="alpha-btn" onclick="filterByLetter('T', this)">
|
||||||
|
T
|
||||||
|
</button>
|
||||||
|
<button class="alpha-btn" onclick="filterByLetter('U', this)">
|
||||||
|
U
|
||||||
|
</button>
|
||||||
|
<button class="alpha-btn" onclick="filterByLetter('V', this)">
|
||||||
|
V
|
||||||
|
</button>
|
||||||
|
<button class="alpha-btn" onclick="filterByLetter('W', this)">
|
||||||
|
W
|
||||||
|
</button>
|
||||||
|
<button class="alpha-btn" onclick="filterByLetter('X', this)">
|
||||||
|
X
|
||||||
|
</button>
|
||||||
|
<button class="alpha-btn" onclick="filterByLetter('Y', this)">
|
||||||
|
Y
|
||||||
|
</button>
|
||||||
|
<button class="alpha-btn" onclick="filterByLetter('Z', this)">
|
||||||
|
Z
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Contacts List -->
|
<!-- Contacts List -->
|
||||||
|
|
@ -139,22 +260,53 @@
|
||||||
<div class="contact-panel" id="contact-panel">
|
<div class="contact-panel" id="contact-panel">
|
||||||
<div class="panel-header">
|
<div class="panel-header">
|
||||||
<button class="close-btn" onclick="closeContactPanel()">
|
<button class="close-btn" onclick="closeContactPanel()">
|
||||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
<svg
|
||||||
<line x1="18" y1="6" x2="6" y2="18"/>
|
width="20"
|
||||||
<line x1="6" y1="6" x2="18" y2="18"/>
|
height="20"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="2"
|
||||||
|
>
|
||||||
|
<line x1="18" y1="6" x2="6" y2="18" />
|
||||||
|
<line x1="6" y1="6" x2="18" y2="18" />
|
||||||
</svg>
|
</svg>
|
||||||
</button>
|
</button>
|
||||||
<div class="panel-actions">
|
<div class="panel-actions">
|
||||||
<button class="icon-btn" title="Edit" onclick="editContact()">
|
<button class="icon-btn" title="Edit" onclick="editContact()">
|
||||||
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
<svg
|
||||||
<path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"/>
|
width="18"
|
||||||
<path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z"/>
|
height="18"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="2"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z"
|
||||||
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
</button>
|
</button>
|
||||||
<button class="icon-btn" title="Delete" onclick="deleteContact()">
|
<button
|
||||||
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
class="icon-btn"
|
||||||
<polyline points="3 6 5 6 21 6"/>
|
title="Delete"
|
||||||
<path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"/>
|
onclick="deleteContact()"
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
width="18"
|
||||||
|
height="18"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="2"
|
||||||
|
>
|
||||||
|
<polyline points="3 6 5 6 21 6" />
|
||||||
|
<path
|
||||||
|
d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"
|
||||||
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -171,9 +323,16 @@
|
||||||
<div class="modal-header">
|
<div class="modal-header">
|
||||||
<h2 id="modal-title" data-i18n="people-add-contact">Add Contact</h2>
|
<h2 id="modal-title" data-i18n="people-add-contact">Add Contact</h2>
|
||||||
<button class="close-btn" onclick="closeModal()">
|
<button class="close-btn" onclick="closeModal()">
|
||||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
<svg
|
||||||
<line x1="18" y1="6" x2="6" y2="18"/>
|
width="20"
|
||||||
<line x1="6" y1="6" x2="18" y2="18"/>
|
height="20"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="2"
|
||||||
|
>
|
||||||
|
<line x1="18" y1="6" x2="6" y2="18" />
|
||||||
|
<line x1="6" y1="6" x2="18" y2="18" />
|
||||||
</svg>
|
</svg>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -209,70 +368,80 @@
|
||||||
<textarea name="notes" rows="3"></textarea>
|
<textarea name="notes" rows="3"></textarea>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-actions">
|
<div class="form-actions">
|
||||||
<button type="button" class="btn btn-secondary" onclick="closeModal()" data-i18n="cancel">Cancel</button>
|
<button
|
||||||
<button type="submit" class="btn btn-primary" data-i18n="save">Save</button>
|
type="button"
|
||||||
|
class="btn btn-secondary"
|
||||||
|
onclick="closeModal()"
|
||||||
|
data-i18n="cancel"
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</button>
|
||||||
|
<button type="submit" class="btn btn-primary" data-i18n="save">
|
||||||
|
Save
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
</dialog>
|
</dialog>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
let currentContact = null;
|
(function () {
|
||||||
let contacts = [];
|
let currentContact = null;
|
||||||
|
let contacts = [];
|
||||||
|
|
||||||
document.addEventListener('DOMContentLoaded', () => {
|
document.addEventListener("DOMContentLoaded", () => {
|
||||||
loadContacts();
|
loadContacts();
|
||||||
});
|
});
|
||||||
|
|
||||||
async function loadContacts() {
|
async function loadContacts() {
|
||||||
try {
|
try {
|
||||||
const response = await fetch('/api/contacts');
|
const response = await fetch("/api/contacts");
|
||||||
if (response.ok) {
|
if (response.ok) {
|
||||||
contacts = await response.json();
|
contacts = await response.json();
|
||||||
renderContacts(contacts);
|
renderContacts(contacts);
|
||||||
} else {
|
} else {
|
||||||
renderEmptyState();
|
renderEmptyState();
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Failed to load contacts:", error);
|
||||||
|
renderEmptyState();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} catch (error) {
|
|
||||||
console.error('Failed to load contacts:', error);
|
|
||||||
renderEmptyState();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function renderContacts(contactsList) {
|
function renderContacts(contactsList) {
|
||||||
const container = document.getElementById('contacts-list');
|
const container = document.getElementById("contacts-list");
|
||||||
if (!contactsList || contactsList.length === 0) {
|
if (!contactsList || contactsList.length === 0) {
|
||||||
renderEmptyState();
|
renderEmptyState();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const grouped = groupByLetter(contactsList);
|
const grouped = groupByLetter(contactsList);
|
||||||
let html = '';
|
let html = "";
|
||||||
|
|
||||||
for (const [letter, group] of Object.entries(grouped)) {
|
for (const [letter, group] of Object.entries(grouped)) {
|
||||||
html += `<div class="contact-group" data-letter="${letter}">
|
html += `<div class="contact-group" data-letter="${letter}">
|
||||||
<div class="group-header">${letter}</div>
|
<div class="group-header">${letter}</div>
|
||||||
<div class="group-contacts">`;
|
<div class="group-contacts">`;
|
||||||
|
|
||||||
for (const contact of group) {
|
for (const contact of group) {
|
||||||
html += renderContactCard(contact);
|
html += renderContactCard(contact);
|
||||||
|
}
|
||||||
|
|
||||||
|
html += "</div></div>";
|
||||||
|
}
|
||||||
|
|
||||||
|
container.innerHTML = html;
|
||||||
}
|
}
|
||||||
|
|
||||||
html += '</div></div>';
|
function renderContactCard(contact) {
|
||||||
}
|
const initials = getInitials(contact.firstName, contact.lastName);
|
||||||
|
const name = `${contact.firstName} ${contact.lastName}`;
|
||||||
|
|
||||||
container.innerHTML = html;
|
return `<div class="contact-card" onclick="showContact('${contact.id}')">
|
||||||
}
|
|
||||||
|
|
||||||
function renderContactCard(contact) {
|
|
||||||
const initials = getInitials(contact.firstName, contact.lastName);
|
|
||||||
const name = `${contact.firstName} ${contact.lastName}`;
|
|
||||||
|
|
||||||
return `<div class="contact-card" onclick="showContact('${contact.id}')">
|
|
||||||
<div class="contact-avatar" style="background: ${getAvatarColor(name)}">${initials}</div>
|
<div class="contact-avatar" style="background: ${getAvatarColor(name)}">${initials}</div>
|
||||||
<div class="contact-info">
|
<div class="contact-info">
|
||||||
<div class="contact-name">${name}</div>
|
<div class="contact-name">${name}</div>
|
||||||
<div class="contact-detail">${contact.email || contact.phone || ''}</div>
|
<div class="contact-detail">${contact.email || contact.phone || ""}</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="contact-actions">
|
<div class="contact-actions">
|
||||||
<button class="icon-btn small" onclick="event.stopPropagation(); startChat('${contact.id}')" title="Chat">
|
<button class="icon-btn small" onclick="event.stopPropagation(); startChat('${contact.id}')" title="Chat">
|
||||||
|
|
@ -288,10 +457,10 @@ function renderContactCard(contact) {
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>`;
|
</div>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderEmptyState() {
|
function renderEmptyState() {
|
||||||
document.getElementById('contacts-list').innerHTML = `
|
document.getElementById("contacts-list").innerHTML = `
|
||||||
<div class="empty-state">
|
<div class="empty-state">
|
||||||
<svg width="64" height="64" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5">
|
<svg width="64" height="64" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5">
|
||||||
<path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"/>
|
<path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"/>
|
||||||
|
|
@ -310,91 +479,124 @@ function renderEmptyState() {
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
}
|
|
||||||
|
|
||||||
function groupByLetter(contactsList) {
|
|
||||||
const grouped = {};
|
|
||||||
for (const contact of contactsList) {
|
|
||||||
const letter = (contact.lastName || contact.firstName || '#').charAt(0).toUpperCase();
|
|
||||||
if (!grouped[letter]) grouped[letter] = [];
|
|
||||||
grouped[letter].push(contact);
|
|
||||||
}
|
|
||||||
return Object.fromEntries(Object.entries(grouped).sort());
|
|
||||||
}
|
|
||||||
|
|
||||||
function getInitials(firstName, lastName) {
|
|
||||||
return ((firstName?.charAt(0) || '') + (lastName?.charAt(0) || '')).toUpperCase() || '?';
|
|
||||||
}
|
|
||||||
|
|
||||||
function getAvatarColor(name) {
|
|
||||||
const colors = ['#6366f1', '#8b5cf6', '#ec4899', '#ef4444', '#f97316', '#eab308', '#22c55e', '#14b8a6', '#06b6d4', '#3b82f6'];
|
|
||||||
let hash = 0;
|
|
||||||
for (let i = 0; i < name.length; i++) {
|
|
||||||
hash = name.charCodeAt(i) + ((hash << 5) - hash);
|
|
||||||
}
|
|
||||||
return colors[Math.abs(hash) % colors.length];
|
|
||||||
}
|
|
||||||
|
|
||||||
function showTab(tabId, btn) {
|
|
||||||
document.querySelectorAll('.tab-content').forEach(tab => tab.classList.remove('active'));
|
|
||||||
document.querySelectorAll('.tab-btn').forEach(b => {
|
|
||||||
b.classList.remove('active');
|
|
||||||
b.setAttribute('aria-selected', 'false');
|
|
||||||
});
|
|
||||||
|
|
||||||
document.getElementById(tabId + '-tab').classList.add('active');
|
|
||||||
btn.classList.add('active');
|
|
||||||
btn.setAttribute('aria-selected', 'true');
|
|
||||||
}
|
|
||||||
|
|
||||||
function filterByLetter(letter, btn) {
|
|
||||||
document.querySelectorAll('.alpha-btn').forEach(b => b.classList.remove('active'));
|
|
||||||
btn.classList.add('active');
|
|
||||||
|
|
||||||
document.querySelectorAll('.contact-group').forEach(group => {
|
|
||||||
if (letter === 'all' || group.dataset.letter === letter) {
|
|
||||||
group.style.display = '';
|
|
||||||
} else {
|
|
||||||
group.style.display = 'none';
|
|
||||||
}
|
}
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function showContact(id) {
|
function groupByLetter(contactsList) {
|
||||||
currentContact = contacts.find(c => c.id === id);
|
const grouped = {};
|
||||||
if (!currentContact) return;
|
for (const contact of contactsList) {
|
||||||
|
const letter = (contact.lastName || contact.firstName || "#")
|
||||||
|
.charAt(0)
|
||||||
|
.toUpperCase();
|
||||||
|
if (!grouped[letter]) grouped[letter] = [];
|
||||||
|
grouped[letter].push(contact);
|
||||||
|
}
|
||||||
|
return Object.fromEntries(Object.entries(grouped).sort());
|
||||||
|
}
|
||||||
|
|
||||||
const panel = document.getElementById('contact-panel');
|
function getInitials(firstName, lastName) {
|
||||||
const detail = document.getElementById('contact-detail');
|
return (
|
||||||
|
(
|
||||||
|
(firstName?.charAt(0) || "") + (lastName?.charAt(0) || "")
|
||||||
|
).toUpperCase() || "?"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
detail.innerHTML = `
|
function getAvatarColor(name) {
|
||||||
|
const colors = [
|
||||||
|
"#6366f1",
|
||||||
|
"#8b5cf6",
|
||||||
|
"#ec4899",
|
||||||
|
"#ef4444",
|
||||||
|
"#f97316",
|
||||||
|
"#eab308",
|
||||||
|
"#22c55e",
|
||||||
|
"#14b8a6",
|
||||||
|
"#06b6d4",
|
||||||
|
"#3b82f6",
|
||||||
|
];
|
||||||
|
let hash = 0;
|
||||||
|
for (let i = 0; i < name.length; i++) {
|
||||||
|
hash = name.charCodeAt(i) + ((hash << 5) - hash);
|
||||||
|
}
|
||||||
|
return colors[Math.abs(hash) % colors.length];
|
||||||
|
}
|
||||||
|
|
||||||
|
function showTab(tabId, btn) {
|
||||||
|
document
|
||||||
|
.querySelectorAll(".tab-content")
|
||||||
|
.forEach((tab) => tab.classList.remove("active"));
|
||||||
|
document.querySelectorAll(".tab-btn").forEach((b) => {
|
||||||
|
b.classList.remove("active");
|
||||||
|
b.setAttribute("aria-selected", "false");
|
||||||
|
});
|
||||||
|
|
||||||
|
document.getElementById(tabId + "-tab").classList.add("active");
|
||||||
|
btn.classList.add("active");
|
||||||
|
btn.setAttribute("aria-selected", "true");
|
||||||
|
}
|
||||||
|
|
||||||
|
function filterByLetter(letter, btn) {
|
||||||
|
document
|
||||||
|
.querySelectorAll(".alpha-btn")
|
||||||
|
.forEach((b) => b.classList.remove("active"));
|
||||||
|
btn.classList.add("active");
|
||||||
|
|
||||||
|
document.querySelectorAll(".contact-group").forEach((group) => {
|
||||||
|
if (letter === "all" || group.dataset.letter === letter) {
|
||||||
|
group.style.display = "";
|
||||||
|
} else {
|
||||||
|
group.style.display = "none";
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function showContact(id) {
|
||||||
|
currentContact = contacts.find((c) => c.id === id);
|
||||||
|
if (!currentContact) return;
|
||||||
|
|
||||||
|
const panel = document.getElementById("contact-panel");
|
||||||
|
const detail = document.getElementById("contact-detail");
|
||||||
|
|
||||||
|
detail.innerHTML = `
|
||||||
<div class="contact-header">
|
<div class="contact-header">
|
||||||
<div class="contact-avatar large" style="background: ${getAvatarColor(currentContact.firstName + ' ' + currentContact.lastName)}">
|
<div class="contact-avatar large" style="background: ${getAvatarColor(currentContact.firstName + " " + currentContact.lastName)}">
|
||||||
${getInitials(currentContact.firstName, currentContact.lastName)}
|
${getInitials(currentContact.firstName, currentContact.lastName)}
|
||||||
</div>
|
</div>
|
||||||
<h2>${currentContact.firstName} ${currentContact.lastName}</h2>
|
<h2>${currentContact.firstName} ${currentContact.lastName}</h2>
|
||||||
${currentContact.title ? `<p class="contact-title">${currentContact.title}</p>` : ''}
|
${currentContact.title ? `<p class="contact-title">${currentContact.title}</p>` : ""}
|
||||||
${currentContact.company ? `<p class="contact-company">${currentContact.company}</p>` : ''}
|
${currentContact.company ? `<p class="contact-company">${currentContact.company}</p>` : ""}
|
||||||
</div>
|
</div>
|
||||||
<div class="contact-fields">
|
<div class="contact-fields">
|
||||||
${currentContact.email ? `
|
${
|
||||||
|
currentContact.email
|
||||||
|
? `
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<label>Email</label>
|
<label>Email</label>
|
||||||
<a href="mailto:${currentContact.email}">${currentContact.email}</a>
|
<a href="mailto:${currentContact.email}">${currentContact.email}</a>
|
||||||
</div>
|
</div>
|
||||||
` : ''}
|
`
|
||||||
${currentContact.phone ? `
|
: ""
|
||||||
|
}
|
||||||
|
${
|
||||||
|
currentContact.phone
|
||||||
|
? `
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<label>Phone</label>
|
<label>Phone</label>
|
||||||
<a href="tel:${currentContact.phone}">${currentContact.phone}</a>
|
<a href="tel:${currentContact.phone}">${currentContact.phone}</a>
|
||||||
</div>
|
</div>
|
||||||
` : ''}
|
`
|
||||||
${currentContact.notes ? `
|
: ""
|
||||||
|
}
|
||||||
|
${
|
||||||
|
currentContact.notes
|
||||||
|
? `
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<label>Notes</label>
|
<label>Notes</label>
|
||||||
<p>${currentContact.notes}</p>
|
<p>${currentContact.notes}</p>
|
||||||
</div>
|
</div>
|
||||||
` : ''}
|
`
|
||||||
|
: ""
|
||||||
|
}
|
||||||
</div>
|
</div>
|
||||||
<div class="contact-quick-actions">
|
<div class="contact-quick-actions">
|
||||||
<button class="action-btn" onclick="startChat('${currentContact.id}')">
|
<button class="action-btn" onclick="startChat('${currentContact.id}')">
|
||||||
|
|
@ -422,106 +624,132 @@ function showContact(id) {
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
panel.classList.add('open');
|
panel.classList.add("open");
|
||||||
}
|
|
||||||
|
|
||||||
function closeContactPanel() {
|
|
||||||
document.getElementById('contact-panel').classList.remove('open');
|
|
||||||
currentContact = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
function openAddContact() {
|
|
||||||
currentContact = null;
|
|
||||||
document.getElementById('modal-title').textContent = 'Add Contact';
|
|
||||||
document.getElementById('contact-form').reset();
|
|
||||||
document.getElementById('contact-modal').showModal();
|
|
||||||
}
|
|
||||||
|
|
||||||
function editContact() {
|
|
||||||
if (!currentContact) return;
|
|
||||||
document.getElementById('modal-title').textContent = 'Edit Contact';
|
|
||||||
const form = document.getElementById('contact-form');
|
|
||||||
form.firstName.value = currentContact.firstName || '';
|
|
||||||
form.lastName.value = currentContact.lastName || '';
|
|
||||||
form.email.value = currentContact.email || '';
|
|
||||||
form.phone.value = currentContact.phone || '';
|
|
||||||
form.company.value = currentContact.company || '';
|
|
||||||
form.title.value = currentContact.title || '';
|
|
||||||
form.notes.value = currentContact.notes || '';
|
|
||||||
document.getElementById('contact-modal').showModal();
|
|
||||||
}
|
|
||||||
|
|
||||||
function closeModal() {
|
|
||||||
document.getElementById('contact-modal').close();
|
|
||||||
}
|
|
||||||
|
|
||||||
async function saveContact(event) {
|
|
||||||
event.preventDefault();
|
|
||||||
const form = event.target;
|
|
||||||
const data = {
|
|
||||||
firstName: form.firstName.value,
|
|
||||||
lastName: form.lastName.value,
|
|
||||||
email: form.email.value,
|
|
||||||
phone: form.phone.value,
|
|
||||||
company: form.company.value,
|
|
||||||
title: form.title.value,
|
|
||||||
notes: form.notes.value
|
|
||||||
};
|
|
||||||
|
|
||||||
try {
|
|
||||||
const url = currentContact ? `/api/contacts/${currentContact.id}` : '/api/contacts';
|
|
||||||
const method = currentContact ? 'PUT' : 'POST';
|
|
||||||
|
|
||||||
const response = await fetch(url, {
|
|
||||||
method,
|
|
||||||
headers: { 'Content-Type': 'application/json' },
|
|
||||||
body: JSON.stringify(data)
|
|
||||||
});
|
|
||||||
|
|
||||||
if (response.ok) {
|
|
||||||
closeModal();
|
|
||||||
loadContacts();
|
|
||||||
if (currentContact) closeContactPanel();
|
|
||||||
}
|
}
|
||||||
} catch (error) {
|
|
||||||
console.error('Failed to save contact:', error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function deleteContact() {
|
function closeContactPanel() {
|
||||||
if (!currentContact || !confirm('Delete this contact?')) return;
|
document.getElementById("contact-panel").classList.remove("open");
|
||||||
|
currentContact = null;
|
||||||
try {
|
|
||||||
const response = await fetch(`/api/contacts/${currentContact.id}`, { method: 'DELETE' });
|
|
||||||
if (response.ok) {
|
|
||||||
closeContactPanel();
|
|
||||||
loadContacts();
|
|
||||||
}
|
}
|
||||||
} catch (error) {
|
|
||||||
console.error('Failed to delete contact:', error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function startChat(contactId) {
|
function openAddContact() {
|
||||||
window.location.href = `/#chat?contact=${contactId}`;
|
currentContact = null;
|
||||||
}
|
document.getElementById("modal-title").textContent = "Add Contact";
|
||||||
|
document.getElementById("contact-form").reset();
|
||||||
|
document.getElementById("contact-modal").showModal();
|
||||||
|
}
|
||||||
|
|
||||||
function sendEmail(email) {
|
function editContact() {
|
||||||
if (email) window.location.href = `mailto:${email}`;
|
if (!currentContact) return;
|
||||||
}
|
document.getElementById("modal-title").textContent = "Edit Contact";
|
||||||
|
const form = document.getElementById("contact-form");
|
||||||
|
form.firstName.value = currentContact.firstName || "";
|
||||||
|
form.lastName.value = currentContact.lastName || "";
|
||||||
|
form.email.value = currentContact.email || "";
|
||||||
|
form.phone.value = currentContact.phone || "";
|
||||||
|
form.company.value = currentContact.company || "";
|
||||||
|
form.title.value = currentContact.title || "";
|
||||||
|
form.notes.value = currentContact.notes || "";
|
||||||
|
document.getElementById("contact-modal").showModal();
|
||||||
|
}
|
||||||
|
|
||||||
function scheduleMeeting(contactId) {
|
function closeModal() {
|
||||||
window.location.href = `/#calendar?new=meeting&contact=${contactId}`;
|
document.getElementById("contact-modal").close();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Search functionality
|
async function saveContact(event) {
|
||||||
document.getElementById('people-search')?.addEventListener('input', (e) => {
|
event.preventDefault();
|
||||||
const query = e.target.value.toLowerCase();
|
const form = event.target;
|
||||||
const filtered = contacts.filter(c =>
|
const data = {
|
||||||
(c.firstName + ' ' + c.lastName).toLowerCase().includes(query) ||
|
firstName: form.firstName.value,
|
||||||
(c.email || '').toLowerCase().includes(query) ||
|
lastName: form.lastName.value,
|
||||||
(c.company || '').toLowerCase().includes(query)
|
email: form.email.value,
|
||||||
);
|
phone: form.phone.value,
|
||||||
renderContacts(filtered);
|
company: form.company.value,
|
||||||
});
|
title: form.title.value,
|
||||||
|
notes: form.notes.value,
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
const url = currentContact
|
||||||
|
? `/api/contacts/${currentContact.id}`
|
||||||
|
: "/api/contacts";
|
||||||
|
const method = currentContact ? "PUT" : "POST";
|
||||||
|
|
||||||
|
const response = await fetch(url, {
|
||||||
|
method,
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify(data),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.ok) {
|
||||||
|
closeModal();
|
||||||
|
loadContacts();
|
||||||
|
if (currentContact) closeContactPanel();
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Failed to save contact:", error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function deleteContact() {
|
||||||
|
if (!currentContact || !confirm("Delete this contact?")) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch(
|
||||||
|
`/api/contacts/${currentContact.id}`,
|
||||||
|
{
|
||||||
|
method: "DELETE",
|
||||||
|
},
|
||||||
|
);
|
||||||
|
if (response.ok) {
|
||||||
|
closeContactPanel();
|
||||||
|
loadContacts();
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Failed to delete contact:", error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function startChat(contactId) {
|
||||||
|
window.location.href = `/#chat?contact=${contactId}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function sendEmail(email) {
|
||||||
|
if (email) window.location.href = `mailto:${email}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function scheduleMeeting(contactId) {
|
||||||
|
window.location.href = `/#calendar?new=meeting&contact=${contactId}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Search functionality
|
||||||
|
document
|
||||||
|
.getElementById("people-search")
|
||||||
|
?.addEventListener("input", (e) => {
|
||||||
|
const query = e.target.value.toLowerCase();
|
||||||
|
const filtered = contacts.filter(
|
||||||
|
(c) =>
|
||||||
|
(c.firstName + " " + c.lastName)
|
||||||
|
.toLowerCase()
|
||||||
|
.includes(query) ||
|
||||||
|
(c.email || "").toLowerCase().includes(query) ||
|
||||||
|
(c.company || "").toLowerCase().includes(query),
|
||||||
|
);
|
||||||
|
renderContacts(filtered);
|
||||||
|
});
|
||||||
|
|
||||||
|
window.showTab = showTab;
|
||||||
|
window.filterByLetter = filterByLetter;
|
||||||
|
window.showContact = showContact;
|
||||||
|
window.closeContactPanel = closeContactPanel;
|
||||||
|
window.openAddContact = openAddContact;
|
||||||
|
window.editContact = editContact;
|
||||||
|
window.closeModal = closeModal;
|
||||||
|
window.saveContact = saveContact;
|
||||||
|
window.deleteContact = deleteContact;
|
||||||
|
window.startChat = startChat;
|
||||||
|
window.sendEmail = sendEmail;
|
||||||
|
window.scheduleMeeting = scheduleMeeting;
|
||||||
|
})();
|
||||||
</script>
|
</script>
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
<!-- Products - Product & Service Catalog -->
|
<!-- Products - Product & Service Catalog -->
|
||||||
<!-- Dynamics nomenclature: Product, Service, PriceList -->
|
<!-- Dynamics nomenclature: Product, Service, PriceList -->
|
||||||
|
|
||||||
<link rel="stylesheet" href="/suite/products/products.css">
|
<link rel="stylesheet" href="products/products.css">
|
||||||
|
|
||||||
<div class="products-container">
|
<div class="products-container">
|
||||||
<!-- Header -->
|
<!-- Header -->
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,5 @@
|
||||||
|
<link rel="stylesheet" href="research/research.css" />
|
||||||
|
|
||||||
<!-- Research - AI-Powered Search & Discovery -->
|
<!-- Research - AI-Powered Search & Discovery -->
|
||||||
<div class="research-container" id="research-app">
|
<div class="research-container" id="research-app">
|
||||||
<!-- Sidebar - Sources & Collections -->
|
<!-- Sidebar - Sources & Collections -->
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,5 @@
|
||||||
|
<link rel="stylesheet" href="settings/settings.css" />
|
||||||
|
|
||||||
<div class="settings-layout">
|
<div class="settings-layout">
|
||||||
<!-- Sidebar Navigation -->
|
<!-- Sidebar Navigation -->
|
||||||
<aside class="settings-sidebar">
|
<aside class="settings-sidebar">
|
||||||
|
|
|
||||||
|
|
@ -452,40 +452,6 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function cacheElements() {
|
|
||||||
elements.container = document.querySelector(".slides-container");
|
|
||||||
elements.sidebar = document.getElementById("slides-sidebar");
|
|
||||||
elements.thumbnails = document.getElementById("slide-thumbnails");
|
|
||||||
elements.canvas = document.getElementById("slide-canvas");
|
|
||||||
elements.canvasContainer = document.getElementById("canvas-container");
|
|
||||||
elements.selectionHandles = document.getElementById("selection-handles");
|
|
||||||
elements.propertiesPanel = document.getElementById("properties-panel");
|
|
||||||
elements.presentationName = document.getElementById("presentation-name");
|
|
||||||
elements.collaborators = document.getElementById("collaborators");
|
|
||||||
elements.contextMenu = document.getElementById("context-menu");
|
|
||||||
elements.slideContextMenu = document.getElementById("slide-context-menu");
|
|
||||||
elements.cursorIndicators = document.getElementById("cursor-indicators");
|
|
||||||
}
|
|
||||||
|
|
||||||
function bindEvents() {
|
|
||||||
elements.canvas.addEventListener("mousedown", handleCanvasMouseDown);
|
|
||||||
elements.canvas.addEventListener("dblclick", handleCanvasDoubleClick);
|
|
||||||
document.addEventListener("mousemove", handleMouseMove);
|
|
||||||
document.addEventListener("mouseup", handleMouseUp);
|
|
||||||
document.addEventListener("keydown", handleKeyDown);
|
|
||||||
document.addEventListener("click", hideContextMenus);
|
|
||||||
elements.canvas.addEventListener("contextmenu", handleContextMenu);
|
|
||||||
|
|
||||||
const handles = elements.selectionHandles.querySelectorAll(
|
|
||||||
".handle, .rotate-handle",
|
|
||||||
);
|
|
||||||
handles.forEach((handle) => {
|
|
||||||
handle.addEventListener("mousedown", handleResizeStart);
|
|
||||||
});
|
|
||||||
|
|
||||||
window.addEventListener("beforeunload", handleBeforeUnload);
|
|
||||||
}
|
|
||||||
|
|
||||||
function createNewPresentation() {
|
function createNewPresentation() {
|
||||||
const titleSlide = createSlide("title");
|
const titleSlide = createSlide("title");
|
||||||
state.slides = [titleSlide];
|
state.slides = [titleSlide];
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,5 @@
|
||||||
|
<link rel="stylesheet" href="social/social.css" />
|
||||||
|
|
||||||
<div class="social-app">
|
<div class="social-app">
|
||||||
<div class="social-header">
|
<div class="social-header">
|
||||||
<div class="social-tabs">
|
<div class="social-tabs">
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,5 @@
|
||||||
|
<link rel="stylesheet" href="sources/sources.css" />
|
||||||
|
|
||||||
<div class="sources-container">
|
<div class="sources-container">
|
||||||
<!-- Header -->
|
<!-- Header -->
|
||||||
<header class="sources-header">
|
<header class="sources-header">
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,5 @@
|
||||||
|
<link rel="stylesheet" href="tasks/autotask.css" />
|
||||||
|
|
||||||
<div class="autotask-container" data-theme="sentient">
|
<div class="autotask-container" data-theme="sentient">
|
||||||
<!-- Top Navigation Bar -->
|
<!-- Top Navigation Bar -->
|
||||||
<div class="autotask-topbar">
|
<div class="autotask-topbar">
|
||||||
|
|
@ -476,6 +478,6 @@ Examples:
|
||||||
<!-- Toast Container -->
|
<!-- Toast Container -->
|
||||||
<div class="toast-container" id="toast-container"></div>
|
<div class="toast-container" id="toast-container"></div>
|
||||||
|
|
||||||
<link rel="stylesheet" href="progress-panel.css" />
|
<link rel="stylesheet" href="tasks/progress-panel.css" />
|
||||||
<script src="progress-panel.js"></script>
|
<script src="tasks/progress-panel.js"></script>
|
||||||
<script src="autotask.js"></script>
|
<script src="tasks/autotask.js"></script>
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,5 @@
|
||||||
|
<link rel="stylesheet" href="tasks/tasks.css" />
|
||||||
|
|
||||||
<!-- =============================================================================
|
<!-- =============================================================================
|
||||||
TASKS APP - Autonomous Task Management
|
TASKS APP - Autonomous Task Management
|
||||||
Respects Theme Manager - No hardcoded theme
|
Respects Theme Manager - No hardcoded theme
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
<!-- Tickets - AI-Assisted Support Cases -->
|
<!-- Tickets - AI-Assisted Support Cases -->
|
||||||
<!-- Dynamics nomenclature: Case, Resolution, Activity -->
|
<!-- Dynamics nomenclature: Case, Resolution, Activity -->
|
||||||
|
|
||||||
<link rel="stylesheet" href="/suite/tickets/tickets.css" />
|
<link rel="stylesheet" href="tickets/tickets.css" />
|
||||||
|
|
||||||
<div class="tickets-container">
|
<div class="tickets-container">
|
||||||
<!-- Header -->
|
<!-- Header -->
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,5 @@
|
||||||
|
<link rel="stylesheet" href="tools/tools.css" />
|
||||||
|
|
||||||
<div class="compliance-container" id="compliance-app">
|
<div class="compliance-container" id="compliance-app">
|
||||||
<style>
|
<style>
|
||||||
.compliance-container {
|
.compliance-container {
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,5 @@
|
||||||
|
<link rel="stylesheet" href="tools/tools.css" />
|
||||||
|
|
||||||
<div class="security-container" id="security-app">
|
<div class="security-container" id="security-app">
|
||||||
<style>
|
<style>
|
||||||
.security-container {
|
.security-container {
|
||||||
|
|
|
||||||
|
|
@ -127,7 +127,9 @@
|
||||||
>
|
>
|
||||||
<!-- Empty state -->
|
<!-- Empty state -->
|
||||||
<div class="empty-page">
|
<div class="empty-page">
|
||||||
<p>Press <kbd>/</kbd> for commands or start typing...</p>
|
<p>
|
||||||
|
Press <kbd>/</kbd> for commands or start typing...
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -196,340 +198,346 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
.workspace-app {
|
.workspace-app {
|
||||||
display: flex;
|
display: flex;
|
||||||
height: 100%;
|
|
||||||
background: var(--bg-primary);
|
|
||||||
color: var(--text-primary);
|
|
||||||
}
|
|
||||||
|
|
||||||
.workspace-sidebar {
|
|
||||||
width: 260px;
|
|
||||||
min-width: 260px;
|
|
||||||
background: var(--bg-secondary);
|
|
||||||
border-right: 1px solid var(--border-color);
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sidebar-header {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: space-between;
|
|
||||||
padding: 1rem;
|
|
||||||
border-bottom: 1px solid var(--border-color);
|
|
||||||
}
|
|
||||||
|
|
||||||
.sidebar-header h2 {
|
|
||||||
font-size: 1rem;
|
|
||||||
font-weight: 600;
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sidebar-search {
|
|
||||||
padding: 0.75rem 1rem;
|
|
||||||
position: relative;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sidebar-search input {
|
|
||||||
width: 100%;
|
|
||||||
padding: 0.5rem 0.75rem;
|
|
||||||
border: 1px solid var(--border-color);
|
|
||||||
border-radius: 6px;
|
|
||||||
background: var(--bg-primary);
|
|
||||||
color: var(--text-primary);
|
|
||||||
font-size: 0.875rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sidebar-nav {
|
|
||||||
flex: 1;
|
|
||||||
overflow-y: auto;
|
|
||||||
padding: 0.5rem 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.nav-section {
|
|
||||||
margin-bottom: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.nav-section h3 {
|
|
||||||
font-size: 0.75rem;
|
|
||||||
font-weight: 600;
|
|
||||||
color: var(--text-muted);
|
|
||||||
text-transform: uppercase;
|
|
||||||
padding: 0.5rem 1rem;
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sidebar-footer {
|
|
||||||
padding: 0.5rem;
|
|
||||||
border-top: 1px solid var(--border-color);
|
|
||||||
}
|
|
||||||
|
|
||||||
.sidebar-btn {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 0.5rem;
|
|
||||||
width: 100%;
|
|
||||||
padding: 0.5rem 0.75rem;
|
|
||||||
border: none;
|
|
||||||
background: transparent;
|
|
||||||
color: var(--text-secondary);
|
|
||||||
font-size: 0.875rem;
|
|
||||||
cursor: pointer;
|
|
||||||
border-radius: 4px;
|
|
||||||
text-align: left;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sidebar-btn:hover {
|
|
||||||
background: var(--bg-hover);
|
|
||||||
color: var(--text-primary);
|
|
||||||
}
|
|
||||||
|
|
||||||
.workspace-main {
|
|
||||||
flex: 1;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
.page-content {
|
|
||||||
flex: 1;
|
|
||||||
overflow-y: auto;
|
|
||||||
max-width: 900px;
|
|
||||||
margin: 0 auto;
|
|
||||||
padding: 2rem;
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.page-header {
|
|
||||||
margin-bottom: 2rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.page-icon {
|
|
||||||
font-size: 3rem;
|
|
||||||
margin-bottom: 0.5rem;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
.page-title {
|
|
||||||
font-size: 2.5rem;
|
|
||||||
font-weight: 700;
|
|
||||||
border: none;
|
|
||||||
background: transparent;
|
|
||||||
color: var(--text-primary);
|
|
||||||
width: 100%;
|
|
||||||
outline: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.page-title::placeholder {
|
|
||||||
color: var(--text-muted);
|
|
||||||
}
|
|
||||||
|
|
||||||
.page-meta {
|
|
||||||
font-size: 0.875rem;
|
|
||||||
color: var(--text-muted);
|
|
||||||
margin-top: 0.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.blocks-container {
|
|
||||||
min-height: 200px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.empty-page {
|
|
||||||
color: var(--text-muted);
|
|
||||||
padding: 1rem 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.empty-page kbd {
|
|
||||||
background: var(--bg-secondary);
|
|
||||||
padding: 0.125rem 0.375rem;
|
|
||||||
border-radius: 4px;
|
|
||||||
font-family: monospace;
|
|
||||||
border: 1px solid var(--border-color);
|
|
||||||
}
|
|
||||||
|
|
||||||
.slash-menu {
|
|
||||||
position: absolute;
|
|
||||||
background: var(--bg-primary);
|
|
||||||
border: 1px solid var(--border-color);
|
|
||||||
border-radius: 8px;
|
|
||||||
box-shadow: var(--shadow-lg);
|
|
||||||
min-width: 280px;
|
|
||||||
max-height: 400px;
|
|
||||||
overflow-y: auto;
|
|
||||||
z-index: 100;
|
|
||||||
}
|
|
||||||
|
|
||||||
.slash-menu.hidden {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.slash-menu-header {
|
|
||||||
padding: 0.75rem 1rem;
|
|
||||||
font-size: 0.75rem;
|
|
||||||
font-weight: 600;
|
|
||||||
color: var(--text-muted);
|
|
||||||
text-transform: uppercase;
|
|
||||||
border-bottom: 1px solid var(--border-color);
|
|
||||||
}
|
|
||||||
|
|
||||||
.members-panel {
|
|
||||||
width: 280px;
|
|
||||||
background: var(--bg-secondary);
|
|
||||||
border-left: 1px solid var(--border-color);
|
|
||||||
transition: width 0.2s;
|
|
||||||
}
|
|
||||||
|
|
||||||
.members-panel.collapsed {
|
|
||||||
width: 48px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.members-panel.collapsed .panel-content {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.panel-toggle {
|
|
||||||
width: 100%;
|
|
||||||
padding: 0.75rem;
|
|
||||||
border: none;
|
|
||||||
background: transparent;
|
|
||||||
cursor: pointer;
|
|
||||||
font-size: 1.25rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.panel-content {
|
|
||||||
padding: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.panel-content h3 {
|
|
||||||
font-size: 0.875rem;
|
|
||||||
font-weight: 600;
|
|
||||||
margin: 0 0 1rem 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.empty-state {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
height: 100%;
|
|
||||||
text-align: center;
|
|
||||||
color: var(--text-muted);
|
|
||||||
}
|
|
||||||
|
|
||||||
.empty-state.hidden {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.empty-state-icon {
|
|
||||||
font-size: 4rem;
|
|
||||||
margin-bottom: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-icon {
|
|
||||||
width: 28px;
|
|
||||||
height: 28px;
|
|
||||||
border: none;
|
|
||||||
background: transparent;
|
|
||||||
color: var(--text-secondary);
|
|
||||||
cursor: pointer;
|
|
||||||
border-radius: 4px;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-icon:hover {
|
|
||||||
background: var(--bg-hover);
|
|
||||||
color: var(--text-primary);
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-primary {
|
|
||||||
display: inline-flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 0.5rem;
|
|
||||||
padding: 0.75rem 1.5rem;
|
|
||||||
background: var(--accent-color);
|
|
||||||
color: white;
|
|
||||||
border: none;
|
|
||||||
border-radius: 6px;
|
|
||||||
font-size: 0.875rem;
|
|
||||||
font-weight: 500;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-primary:hover {
|
|
||||||
background: var(--accent-hover);
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-secondary {
|
|
||||||
padding: 0.5rem 1rem;
|
|
||||||
background: var(--bg-primary);
|
|
||||||
color: var(--text-primary);
|
|
||||||
border: 1px solid var(--border-color);
|
|
||||||
border-radius: 6px;
|
|
||||||
font-size: 0.875rem;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-sm {
|
|
||||||
padding: 0.375rem 0.75rem;
|
|
||||||
font-size: 0.75rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.loading-placeholder {
|
|
||||||
color: var(--text-muted);
|
|
||||||
font-size: 0.875rem;
|
|
||||||
padding: 0.5rem 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.modal-container:empty {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.search-results:empty {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 768px) {
|
|
||||||
.workspace-sidebar {
|
|
||||||
position: absolute;
|
|
||||||
left: -260px;
|
|
||||||
height: 100%;
|
height: 100%;
|
||||||
z-index: 50;
|
background: var(--bg-primary);
|
||||||
transition: left 0.2s;
|
color: var(--text-primary);
|
||||||
}
|
}
|
||||||
|
|
||||||
.workspace-sidebar.open {
|
.workspace-sidebar {
|
||||||
left: 0;
|
width: 260px;
|
||||||
|
min-width: 260px;
|
||||||
|
background: var(--bg-secondary);
|
||||||
|
border-right: 1px solid var(--border-color);
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 1rem;
|
||||||
|
border-bottom: 1px solid var(--border-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-header h2 {
|
||||||
|
font-size: 1rem;
|
||||||
|
font-weight: 600;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-search {
|
||||||
|
padding: 0.75rem 1rem;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-search input {
|
||||||
|
width: 100%;
|
||||||
|
padding: 0.5rem 0.75rem;
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
border-radius: 6px;
|
||||||
|
background: var(--bg-primary);
|
||||||
|
color: var(--text-primary);
|
||||||
|
font-size: 0.875rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-nav {
|
||||||
|
flex: 1;
|
||||||
|
overflow-y: auto;
|
||||||
|
padding: 0.5rem 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-section {
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-section h3 {
|
||||||
|
font-size: 0.75rem;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--text-muted);
|
||||||
|
text-transform: uppercase;
|
||||||
|
padding: 0.5rem 1rem;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-footer {
|
||||||
|
padding: 0.5rem;
|
||||||
|
border-top: 1px solid var(--border-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-btn {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
width: 100%;
|
||||||
|
padding: 0.5rem 0.75rem;
|
||||||
|
border: none;
|
||||||
|
background: transparent;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
font-size: 0.875rem;
|
||||||
|
cursor: pointer;
|
||||||
|
border-radius: 4px;
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-btn:hover {
|
||||||
|
background: var(--bg-hover);
|
||||||
|
color: var(--text-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.workspace-main {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-content {
|
||||||
|
flex: 1;
|
||||||
|
overflow-y: auto;
|
||||||
|
max-width: 900px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 2rem;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-header {
|
||||||
|
margin-bottom: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-icon {
|
||||||
|
font-size: 3rem;
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-title {
|
||||||
|
font-size: 2.5rem;
|
||||||
|
font-weight: 700;
|
||||||
|
border: none;
|
||||||
|
background: transparent;
|
||||||
|
color: var(--text-primary);
|
||||||
|
width: 100%;
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-title::placeholder {
|
||||||
|
color: var(--text-muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-meta {
|
||||||
|
font-size: 0.875rem;
|
||||||
|
color: var(--text-muted);
|
||||||
|
margin-top: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.blocks-container {
|
||||||
|
min-height: 200px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-page {
|
||||||
|
color: var(--text-muted);
|
||||||
|
padding: 1rem 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-page kbd {
|
||||||
|
background: var(--bg-secondary);
|
||||||
|
padding: 0.125rem 0.375rem;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-family: monospace;
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.slash-menu {
|
||||||
|
position: absolute;
|
||||||
|
background: var(--bg-primary);
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
border-radius: 8px;
|
||||||
|
box-shadow: var(--shadow-lg);
|
||||||
|
min-width: 280px;
|
||||||
|
max-height: 400px;
|
||||||
|
overflow-y: auto;
|
||||||
|
z-index: 100;
|
||||||
|
}
|
||||||
|
|
||||||
|
.slash-menu.hidden {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.slash-menu-header {
|
||||||
|
padding: 0.75rem 1rem;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--text-muted);
|
||||||
|
text-transform: uppercase;
|
||||||
|
border-bottom: 1px solid var(--border-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
.members-panel {
|
.members-panel {
|
||||||
|
width: 280px;
|
||||||
|
background: var(--bg-secondary);
|
||||||
|
border-left: 1px solid var(--border-color);
|
||||||
|
transition: width 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.members-panel.collapsed {
|
||||||
|
width: 48px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.members-panel.collapsed .panel-content {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
.panel-toggle {
|
||||||
|
width: 100%;
|
||||||
|
padding: 0.75rem;
|
||||||
|
border: none;
|
||||||
|
background: transparent;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 1.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.panel-content {
|
||||||
|
padding: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.panel-content h3 {
|
||||||
|
font-size: 0.875rem;
|
||||||
|
font-weight: 600;
|
||||||
|
margin: 0 0 1rem 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-state {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
height: 100%;
|
||||||
|
text-align: center;
|
||||||
|
color: var(--text-muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-state.hidden {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-state-icon {
|
||||||
|
font-size: 4rem;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-icon {
|
||||||
|
width: 28px;
|
||||||
|
height: 28px;
|
||||||
|
border: none;
|
||||||
|
background: transparent;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
cursor: pointer;
|
||||||
|
border-radius: 4px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-icon:hover {
|
||||||
|
background: var(--bg-hover);
|
||||||
|
color: var(--text-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-primary {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
padding: 0.75rem 1.5rem;
|
||||||
|
background: var(--accent-color);
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
border-radius: 6px;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
font-weight: 500;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-primary:hover {
|
||||||
|
background: var(--accent-hover);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-secondary {
|
||||||
|
padding: 0.5rem 1rem;
|
||||||
|
background: var(--bg-primary);
|
||||||
|
color: var(--text-primary);
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
border-radius: 6px;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-sm {
|
||||||
|
padding: 0.375rem 0.75rem;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loading-placeholder {
|
||||||
|
color: var(--text-muted);
|
||||||
|
font-size: 0.875rem;
|
||||||
|
padding: 0.5rem 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-container:empty {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-results:empty {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.workspace-sidebar {
|
||||||
|
position: absolute;
|
||||||
|
left: -260px;
|
||||||
|
height: 100%;
|
||||||
|
z-index: 50;
|
||||||
|
transition: left 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.workspace-sidebar.open {
|
||||||
|
left: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.members-panel {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
function toggleMembersPanel() {
|
function toggleMembersPanel() {
|
||||||
const panel = document.getElementById('members-panel');
|
const panel = document.getElementById("members-panel");
|
||||||
panel.classList.toggle('collapsed');
|
if (panel) {
|
||||||
}
|
panel.classList.toggle("collapsed");
|
||||||
|
}
|
||||||
document.addEventListener('DOMContentLoaded', function() {
|
|
||||||
const blocksContainer = document.getElementById('blocks-container');
|
|
||||||
if (blocksContainer) {
|
|
||||||
blocksContainer.addEventListener('keydown', function(e) {
|
|
||||||
if (e.key === '/') {
|
|
||||||
const slashMenu = document.getElementById('slash-menu');
|
|
||||||
slashMenu.classList.remove('hidden');
|
|
||||||
}
|
|
||||||
if (e.key === 'Escape') {
|
|
||||||
const slashMenu = document.getElementById('slash-menu');
|
|
||||||
slashMenu.classList.add('hidden');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
});
|
|
||||||
|
document.addEventListener("DOMContentLoaded", function () {
|
||||||
|
const blocksContainer = document.getElementById("blocks-container");
|
||||||
|
if (blocksContainer) {
|
||||||
|
blocksContainer.addEventListener("keydown", function (e) {
|
||||||
|
if (e.key === "/") {
|
||||||
|
const slashMenu = document.getElementById("slash-menu");
|
||||||
|
if (slashMenu) {
|
||||||
|
slashMenu.classList.remove("hidden");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (e.key === "Escape") {
|
||||||
|
const slashMenu = document.getElementById("slash-menu");
|
||||||
|
if (slashMenu) {
|
||||||
|
slashMenu.classList.add("hidden");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue