feat: BreakPilot PWA - Full codebase (clean push without large binaries)
Some checks failed
Tests / Go Tests (push) Has been cancelled
Tests / Python Tests (push) Has been cancelled
Tests / Integration Tests (push) Has been cancelled
Tests / Go Lint (push) Has been cancelled
Tests / Python Lint (push) Has been cancelled
Tests / Security Scan (push) Has been cancelled
Tests / All Checks Passed (push) Has been cancelled
Security Scanning / Secret Scanning (push) Has been cancelled
Security Scanning / Dependency Vulnerability Scan (push) Has been cancelled
Security Scanning / Go Security Scan (push) Has been cancelled
Security Scanning / Python Security Scan (push) Has been cancelled
Security Scanning / Node.js Security Scan (push) Has been cancelled
Security Scanning / Docker Image Security (push) Has been cancelled
Security Scanning / Security Summary (push) Has been cancelled
CI/CD Pipeline / Go Tests (push) Has been cancelled
CI/CD Pipeline / Python Tests (push) Has been cancelled
CI/CD Pipeline / Website Tests (push) Has been cancelled
CI/CD Pipeline / Linting (push) Has been cancelled
CI/CD Pipeline / Security Scan (push) Has been cancelled
CI/CD Pipeline / Docker Build & Push (push) Has been cancelled
CI/CD Pipeline / Integration Tests (push) Has been cancelled
CI/CD Pipeline / Deploy to Staging (push) Has been cancelled
CI/CD Pipeline / Deploy to Production (push) Has been cancelled
CI/CD Pipeline / CI Summary (push) Has been cancelled
ci/woodpecker/manual/build-ci-image Pipeline was successful
ci/woodpecker/manual/main Pipeline failed
Some checks failed
Tests / Go Tests (push) Has been cancelled
Tests / Python Tests (push) Has been cancelled
Tests / Integration Tests (push) Has been cancelled
Tests / Go Lint (push) Has been cancelled
Tests / Python Lint (push) Has been cancelled
Tests / Security Scan (push) Has been cancelled
Tests / All Checks Passed (push) Has been cancelled
Security Scanning / Secret Scanning (push) Has been cancelled
Security Scanning / Dependency Vulnerability Scan (push) Has been cancelled
Security Scanning / Go Security Scan (push) Has been cancelled
Security Scanning / Python Security Scan (push) Has been cancelled
Security Scanning / Node.js Security Scan (push) Has been cancelled
Security Scanning / Docker Image Security (push) Has been cancelled
Security Scanning / Security Summary (push) Has been cancelled
CI/CD Pipeline / Go Tests (push) Has been cancelled
CI/CD Pipeline / Python Tests (push) Has been cancelled
CI/CD Pipeline / Website Tests (push) Has been cancelled
CI/CD Pipeline / Linting (push) Has been cancelled
CI/CD Pipeline / Security Scan (push) Has been cancelled
CI/CD Pipeline / Docker Build & Push (push) Has been cancelled
CI/CD Pipeline / Integration Tests (push) Has been cancelled
CI/CD Pipeline / Deploy to Staging (push) Has been cancelled
CI/CD Pipeline / Deploy to Production (push) Has been cancelled
CI/CD Pipeline / CI Summary (push) Has been cancelled
ci/woodpecker/manual/build-ci-image Pipeline was successful
ci/woodpecker/manual/main Pipeline failed
All services: admin-v2, studio-v2, website, ai-compliance-sdk, consent-service, klausur-service, voice-service, and infrastructure. Large PDFs and compiled binaries excluded via .gitignore.
This commit is contained in:
815
backend/frontend/static/css/customer.css
Normal file
815
backend/frontend/static/css/customer.css
Normal file
@@ -0,0 +1,815 @@
|
||||
/**
|
||||
* BreakPilot Customer Portal - Slim CSS
|
||||
*
|
||||
* Light/Dark Theme with Sky Blue/Fuchsia accent colors
|
||||
* Matching Website Design (Inter font)
|
||||
*/
|
||||
|
||||
/* CSS Variables - Light Mode (default) */
|
||||
:root {
|
||||
/* Primary Colors */
|
||||
--bp-primary: #0ea5e9;
|
||||
--bp-primary-hover: #0284c7;
|
||||
--bp-primary-soft: rgba(14, 165, 233, 0.1);
|
||||
|
||||
/* Accent Colors */
|
||||
--bp-accent: #d946ef;
|
||||
--bp-accent-soft: rgba(217, 70, 239, 0.15);
|
||||
|
||||
/* Background */
|
||||
--bp-bg: #f8fafc;
|
||||
--bp-bg-elevated: #ffffff;
|
||||
--bp-bg-card: #ffffff;
|
||||
|
||||
/* Text */
|
||||
--bp-text: #0f172a;
|
||||
--bp-text-muted: #64748b;
|
||||
--bp-text-light: #94a3b8;
|
||||
|
||||
/* Borders */
|
||||
--bp-border: #e2e8f0;
|
||||
--bp-border-light: #f1f5f9;
|
||||
|
||||
/* Feedback */
|
||||
--bp-success: #10b981;
|
||||
--bp-danger: #ef4444;
|
||||
--bp-warning: #f59e0b;
|
||||
|
||||
/* Shadows */
|
||||
--bp-shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.05);
|
||||
--bp-shadow-md: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
|
||||
--bp-shadow-lg: 0 10px 15px -3px rgba(0, 0, 0, 0.1);
|
||||
|
||||
/* Spacing */
|
||||
--bp-radius: 8px;
|
||||
--bp-radius-lg: 12px;
|
||||
|
||||
/* Font */
|
||||
--bp-font: 'Inter', system-ui, -apple-system, sans-serif;
|
||||
}
|
||||
|
||||
/* Dark Mode */
|
||||
[data-theme="dark"] {
|
||||
--bp-primary: #38bdf8;
|
||||
--bp-primary-hover: #0ea5e9;
|
||||
--bp-primary-soft: rgba(56, 189, 248, 0.1);
|
||||
|
||||
--bp-accent: #e879f9;
|
||||
--bp-accent-soft: rgba(232, 121, 249, 0.15);
|
||||
|
||||
--bp-bg: #0f172a;
|
||||
--bp-bg-elevated: #1e293b;
|
||||
--bp-bg-card: #1e293b;
|
||||
|
||||
--bp-text: #f1f5f9;
|
||||
--bp-text-muted: #94a3b8;
|
||||
--bp-text-light: #64748b;
|
||||
|
||||
--bp-border: #334155;
|
||||
--bp-border-light: #1e293b;
|
||||
|
||||
--bp-shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.3);
|
||||
--bp-shadow-md: 0 4px 6px -1px rgba(0, 0, 0, 0.4);
|
||||
--bp-shadow-lg: 0 10px 15px -3px rgba(0, 0, 0, 0.5);
|
||||
}
|
||||
|
||||
/* Reset */
|
||||
*, *::before, *::after {
|
||||
box-sizing: border-box;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
/* Base */
|
||||
html, body {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: var(--bp-font);
|
||||
background: var(--bp-bg);
|
||||
color: var(--bp-text);
|
||||
line-height: 1.6;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
}
|
||||
|
||||
/* App Root */
|
||||
.app-root {
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
/* Header */
|
||||
.header {
|
||||
background: var(--bp-bg-elevated);
|
||||
border-bottom: 1px solid var(--bp-border);
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 100;
|
||||
}
|
||||
|
||||
.header-content {
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
padding: 16px 24px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.brand {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
text-decoration: none;
|
||||
color: var(--bp-text);
|
||||
}
|
||||
|
||||
.brand-logo {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
background: linear-gradient(135deg, var(--bp-primary), var(--bp-accent));
|
||||
border-radius: var(--bp-radius);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-weight: 700;
|
||||
color: white;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.brand-text {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.brand-name {
|
||||
font-weight: 600;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.brand-sub {
|
||||
font-size: 12px;
|
||||
color: var(--bp-text-muted);
|
||||
}
|
||||
|
||||
.header-nav {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
/* Buttons */
|
||||
.btn {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 8px;
|
||||
padding: 8px 16px;
|
||||
border-radius: var(--bp-radius);
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
border: none;
|
||||
transition: all 0.15s ease;
|
||||
font-family: inherit;
|
||||
}
|
||||
|
||||
.btn:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
background: var(--bp-primary);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-primary:hover:not(:disabled) {
|
||||
background: var(--bp-primary-hover);
|
||||
}
|
||||
|
||||
.btn-ghost {
|
||||
background: transparent;
|
||||
color: var(--bp-text-muted);
|
||||
}
|
||||
|
||||
.btn-ghost:hover:not(:disabled) {
|
||||
background: var(--bp-primary-soft);
|
||||
color: var(--bp-primary);
|
||||
}
|
||||
|
||||
.btn-danger {
|
||||
background: var(--bp-danger);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-danger:hover:not(:disabled) {
|
||||
background: #dc2626;
|
||||
}
|
||||
|
||||
.btn-lg {
|
||||
padding: 12px 24px;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.btn-full {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
/* User Menu */
|
||||
.user-menu {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.user-menu-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 6px 12px;
|
||||
background: var(--bp-bg-elevated);
|
||||
border: 1px solid var(--bp-border);
|
||||
border-radius: var(--bp-radius);
|
||||
cursor: pointer;
|
||||
font-family: inherit;
|
||||
color: var(--bp-text);
|
||||
}
|
||||
|
||||
.user-menu-btn:hover {
|
||||
border-color: var(--bp-primary);
|
||||
}
|
||||
|
||||
.user-avatar {
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
background: linear-gradient(135deg, var(--bp-primary), var(--bp-accent));
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.user-name {
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.user-dropdown {
|
||||
position: absolute;
|
||||
top: calc(100% + 8px);
|
||||
right: 0;
|
||||
min-width: 200px;
|
||||
background: var(--bp-bg-elevated);
|
||||
border: 1px solid var(--bp-border);
|
||||
border-radius: var(--bp-radius-lg);
|
||||
box-shadow: var(--bp-shadow-lg);
|
||||
display: none;
|
||||
z-index: 200;
|
||||
}
|
||||
|
||||
.user-menu.open .user-dropdown {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.user-dropdown-header {
|
||||
padding: 12px 16px;
|
||||
border-bottom: 1px solid var(--bp-border);
|
||||
}
|
||||
|
||||
.user-dropdown-name {
|
||||
display: block;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.user-dropdown-email {
|
||||
display: block;
|
||||
font-size: 12px;
|
||||
color: var(--bp-text-muted);
|
||||
}
|
||||
|
||||
.user-dropdown hr {
|
||||
border: none;
|
||||
border-top: 1px solid var(--bp-border);
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.user-dropdown-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
width: 100%;
|
||||
padding: 10px 16px;
|
||||
background: none;
|
||||
border: none;
|
||||
font-size: 14px;
|
||||
color: var(--bp-text);
|
||||
cursor: pointer;
|
||||
text-align: left;
|
||||
font-family: inherit;
|
||||
}
|
||||
|
||||
.user-dropdown-item:hover {
|
||||
background: var(--bp-primary-soft);
|
||||
}
|
||||
|
||||
.user-dropdown-item.danger {
|
||||
color: var(--bp-danger);
|
||||
}
|
||||
|
||||
.user-dropdown-item.danger:hover {
|
||||
background: rgba(239, 68, 68, 0.1);
|
||||
}
|
||||
|
||||
/* Main */
|
||||
.main {
|
||||
flex: 1;
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
padding: 32px 24px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
/* Welcome Section */
|
||||
.welcome-section {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-height: 400px;
|
||||
}
|
||||
|
||||
.welcome-card {
|
||||
text-align: center;
|
||||
max-width: 400px;
|
||||
}
|
||||
|
||||
.welcome-card h1 {
|
||||
font-size: 28px;
|
||||
font-weight: 700;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.welcome-card p {
|
||||
color: var(--bp-text-muted);
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
/* Dashboard */
|
||||
.dashboard-header {
|
||||
margin-bottom: 32px;
|
||||
}
|
||||
|
||||
.dashboard-header h1 {
|
||||
font-size: 28px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.dashboard-header p {
|
||||
color: var(--bp-text-muted);
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.dashboard-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
|
||||
gap: 24px;
|
||||
}
|
||||
|
||||
.dashboard-card {
|
||||
background: var(--bp-bg-card);
|
||||
border: 1px solid var(--bp-border);
|
||||
border-radius: var(--bp-radius-lg);
|
||||
padding: 24px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.dashboard-card:hover {
|
||||
border-color: var(--bp-primary);
|
||||
box-shadow: var(--bp-shadow-md);
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
.card-icon {
|
||||
font-size: 32px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.dashboard-card h2 {
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.dashboard-card p {
|
||||
font-size: 14px;
|
||||
color: var(--bp-text-muted);
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.card-badge {
|
||||
display: inline-block;
|
||||
padding: 4px 12px;
|
||||
background: var(--bp-primary-soft);
|
||||
color: var(--bp-primary);
|
||||
border-radius: 20px;
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
/* Footer */
|
||||
.footer {
|
||||
background: var(--bp-bg-elevated);
|
||||
border-top: 1px solid var(--bp-border);
|
||||
padding: 24px;
|
||||
}
|
||||
|
||||
.footer-content {
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
font-size: 14px;
|
||||
color: var(--bp-text-muted);
|
||||
}
|
||||
|
||||
.footer-nav {
|
||||
display: flex;
|
||||
gap: 24px;
|
||||
}
|
||||
|
||||
.footer-nav a {
|
||||
color: var(--bp-text-muted);
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.footer-nav a:hover {
|
||||
color: var(--bp-primary);
|
||||
}
|
||||
|
||||
/* Modal */
|
||||
.modal {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: none;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 1000;
|
||||
}
|
||||
|
||||
.modal.active {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.modal-backdrop {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
}
|
||||
|
||||
.modal-content {
|
||||
position: relative;
|
||||
background: var(--bp-bg-elevated);
|
||||
border-radius: var(--bp-radius-lg);
|
||||
width: 100%;
|
||||
max-width: 480px;
|
||||
max-height: 90vh;
|
||||
overflow-y: auto;
|
||||
box-shadow: var(--bp-shadow-lg);
|
||||
margin: 20px;
|
||||
}
|
||||
|
||||
.modal-lg {
|
||||
max-width: 720px;
|
||||
}
|
||||
|
||||
.modal-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 20px 24px;
|
||||
border-bottom: 1px solid var(--bp-border);
|
||||
}
|
||||
|
||||
.modal-header h2 {
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.modal-close {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: none;
|
||||
border: none;
|
||||
font-size: 24px;
|
||||
color: var(--bp-text-muted);
|
||||
cursor: pointer;
|
||||
border-radius: var(--bp-radius);
|
||||
}
|
||||
|
||||
.modal-close:hover {
|
||||
background: var(--bp-primary-soft);
|
||||
color: var(--bp-primary);
|
||||
}
|
||||
|
||||
.modal-body {
|
||||
padding: 24px;
|
||||
}
|
||||
|
||||
/* Auth Tabs */
|
||||
.auth-tabs {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.auth-tab {
|
||||
flex: 1;
|
||||
padding: 12px;
|
||||
background: none;
|
||||
border: none;
|
||||
border-bottom: 2px solid transparent;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
color: var(--bp-text-muted);
|
||||
cursor: pointer;
|
||||
font-family: inherit;
|
||||
}
|
||||
|
||||
.auth-tab:hover {
|
||||
color: var(--bp-text);
|
||||
}
|
||||
|
||||
.auth-tab.active {
|
||||
color: var(--bp-primary);
|
||||
border-bottom-color: var(--bp-primary);
|
||||
}
|
||||
|
||||
/* Forms */
|
||||
.auth-form {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.form-group {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.form-group label {
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.form-group input {
|
||||
padding: 10px 14px;
|
||||
border: 1px solid var(--bp-border);
|
||||
border-radius: var(--bp-radius);
|
||||
font-size: 14px;
|
||||
font-family: inherit;
|
||||
background: var(--bp-bg);
|
||||
color: var(--bp-text);
|
||||
}
|
||||
|
||||
.form-group input:focus {
|
||||
outline: none;
|
||||
border-color: var(--bp-primary);
|
||||
box-shadow: 0 0 0 3px var(--bp-primary-soft);
|
||||
}
|
||||
|
||||
.form-group input:disabled {
|
||||
background: var(--bp-border-light);
|
||||
color: var(--bp-text-muted);
|
||||
}
|
||||
|
||||
.form-hint {
|
||||
text-align: center;
|
||||
font-size: 14px;
|
||||
color: var(--bp-text-muted);
|
||||
}
|
||||
|
||||
.form-hint a {
|
||||
color: var(--bp-primary);
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.form-hint a:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.form-message {
|
||||
padding: 12px;
|
||||
border-radius: var(--bp-radius);
|
||||
font-size: 14px;
|
||||
display: none;
|
||||
}
|
||||
|
||||
.form-message.success {
|
||||
display: block;
|
||||
background: rgba(16, 185, 129, 0.1);
|
||||
color: var(--bp-success);
|
||||
}
|
||||
|
||||
.form-message.error {
|
||||
display: block;
|
||||
background: rgba(239, 68, 68, 0.1);
|
||||
color: var(--bp-danger);
|
||||
}
|
||||
|
||||
/* Consents List */
|
||||
.consents-loading {
|
||||
text-align: center;
|
||||
padding: 40px;
|
||||
color: var(--bp-text-muted);
|
||||
}
|
||||
|
||||
.consents-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.consent-item {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
padding: 16px;
|
||||
background: var(--bp-bg);
|
||||
border: 1px solid var(--bp-border);
|
||||
border-radius: var(--bp-radius);
|
||||
}
|
||||
|
||||
.consent-info h3 {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.consent-info p {
|
||||
font-size: 13px;
|
||||
color: var(--bp-text-muted);
|
||||
}
|
||||
|
||||
.consent-date {
|
||||
font-size: 12px;
|
||||
color: var(--bp-text-light);
|
||||
}
|
||||
|
||||
.consent-actions {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.consents-empty {
|
||||
text-align: center;
|
||||
padding: 40px;
|
||||
color: var(--bp-text-muted);
|
||||
}
|
||||
|
||||
/* Export */
|
||||
.export-info {
|
||||
background: var(--bp-primary-soft);
|
||||
padding: 16px;
|
||||
border-radius: var(--bp-radius);
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.export-info p {
|
||||
font-size: 14px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.export-info p:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.export-actions {
|
||||
margin-top: 16px;
|
||||
}
|
||||
|
||||
.export-status {
|
||||
margin-top: 24px;
|
||||
padding-top: 24px;
|
||||
border-top: 1px solid var(--bp-border);
|
||||
}
|
||||
|
||||
.export-status h3 {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
/* Legal */
|
||||
.legal-loading {
|
||||
text-align: center;
|
||||
padding: 40px;
|
||||
color: var(--bp-text-muted);
|
||||
}
|
||||
|
||||
.legal-content {
|
||||
font-size: 14px;
|
||||
line-height: 1.7;
|
||||
}
|
||||
|
||||
.legal-content h1, .legal-content h2, .legal-content h3 {
|
||||
margin-top: 24px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.legal-content p {
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.legal-content ul, .legal-content ol {
|
||||
margin-bottom: 12px;
|
||||
padding-left: 24px;
|
||||
}
|
||||
|
||||
/* Settings */
|
||||
.settings-section {
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.settings-section h3 {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.settings-section form {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.settings-section hr {
|
||||
border: none;
|
||||
border-top: 1px solid var(--bp-border);
|
||||
margin: 24px 0;
|
||||
}
|
||||
|
||||
.danger-zone {
|
||||
padding: 16px;
|
||||
background: rgba(239, 68, 68, 0.05);
|
||||
border: 1px solid rgba(239, 68, 68, 0.2);
|
||||
border-radius: var(--bp-radius);
|
||||
}
|
||||
|
||||
.danger-zone h3 {
|
||||
color: var(--bp-danger);
|
||||
}
|
||||
|
||||
.danger-zone p {
|
||||
font-size: 14px;
|
||||
color: var(--bp-text-muted);
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
/* Utilities */
|
||||
.hidden {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
/* Responsive */
|
||||
@media (max-width: 768px) {
|
||||
.header-content {
|
||||
padding: 12px 16px;
|
||||
}
|
||||
|
||||
.brand-text {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.user-name {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.main {
|
||||
padding: 24px 16px;
|
||||
}
|
||||
|
||||
.dashboard-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.footer-content {
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
347
backend/frontend/static/css/modules/admin/content.css
Normal file
347
backend/frontend/static/css/modules/admin/content.css
Normal file
@@ -0,0 +1,347 @@
|
||||
/* ==========================================
|
||||
ADMIN PANEL - Content Area
|
||||
Main content, buttons, cards, status bar
|
||||
========================================== */
|
||||
|
||||
.content {
|
||||
padding: 14px 16px 16px 16px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 14px;
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
/* Buttons */
|
||||
.btn {
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
padding: 8px 16px;
|
||||
border-radius: 8px;
|
||||
border: 1px solid var(--bp-border-subtle);
|
||||
background: var(--bp-surface-elevated);
|
||||
color: var(--bp-text);
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.btn:hover:not(:disabled) {
|
||||
border-color: var(--bp-primary);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
border-color: var(--bp-accent);
|
||||
background: var(--bp-btn-primary-bg);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-primary:hover:not(:disabled) {
|
||||
background: var(--bp-btn-primary-hover);
|
||||
box-shadow: 0 4px 12px rgba(34,197,94,0.3);
|
||||
}
|
||||
|
||||
.btn-ghost {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.btn-ghost:hover:not(:disabled) {
|
||||
background: var(--bp-surface-elevated);
|
||||
}
|
||||
|
||||
.btn-sm {
|
||||
padding: 6px 12px;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.btn:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
/* Cards Grid */
|
||||
.cards-grid {
|
||||
flex: 1;
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
grid-auto-rows: 1fr;
|
||||
gap: 10px;
|
||||
min-height: 0;
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.card {
|
||||
border-radius: 14px;
|
||||
border: 1px solid var(--bp-border-subtle);
|
||||
background: var(--bp-card-bg);
|
||||
padding: 10px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.card-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
|
||||
.card-title {
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.card-actions {
|
||||
display: flex;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
/* Toggle Pills */
|
||||
.panel-tools-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 8px;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
|
||||
.card-toggle-bar {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 6px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.toggle-pill {
|
||||
font-size: 11px;
|
||||
padding: 4px 8px;
|
||||
border-radius: 999px;
|
||||
border: 1px solid var(--bp-border-subtle);
|
||||
background: var(--bp-surface-elevated);
|
||||
color: var(--bp-text-muted);
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.toggle-pill.active {
|
||||
border-color: var(--bp-accent);
|
||||
color: var(--bp-accent);
|
||||
background: var(--bp-accent-soft);
|
||||
}
|
||||
|
||||
/* Status Bar */
|
||||
.status-bar {
|
||||
position: fixed;
|
||||
right: 18px;
|
||||
bottom: 18px;
|
||||
padding: 8px 12px;
|
||||
border-radius: 999px;
|
||||
background: var(--bp-surface-elevated);
|
||||
border: 1px solid var(--bp-border-subtle);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
font-size: 12px;
|
||||
min-width: 230px;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.status-dot {
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
border-radius: 999px;
|
||||
background: var(--bp-text-muted);
|
||||
}
|
||||
|
||||
.status-dot.busy {
|
||||
background: var(--bp-accent);
|
||||
}
|
||||
|
||||
.status-dot.error {
|
||||
background: var(--bp-danger);
|
||||
}
|
||||
|
||||
.status-text-main {
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.status-text-sub {
|
||||
font-size: 11px;
|
||||
color: var(--bp-text-muted);
|
||||
}
|
||||
|
||||
/* Footer */
|
||||
.footer {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 12px;
|
||||
font-size: 11px;
|
||||
color: var(--bp-text-muted);
|
||||
border-top: 1px solid var(--bp-border-subtle);
|
||||
background: var(--bp-surface-elevated);
|
||||
transition: background 0.3s ease;
|
||||
}
|
||||
|
||||
.footer a {
|
||||
color: var(--bp-text-muted);
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.footer a:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
/* Pager */
|
||||
.pager {
|
||||
grid-column: 1 / -1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 8px;
|
||||
padding: 2px 0;
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
.pager button {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
border-radius: 999px;
|
||||
border: 1px solid var(--bp-border-subtle);
|
||||
background: var(--bp-surface-elevated);
|
||||
color: var(--bp-text);
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.pager button:hover {
|
||||
border-color: var(--bp-primary);
|
||||
color: var(--bp-primary);
|
||||
}
|
||||
|
||||
/* Unit Items */
|
||||
.unit-item {
|
||||
padding: 8px 10px;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
font-size: 12px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 8px;
|
||||
transition: all 0.15s ease;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.unit-item:hover {
|
||||
background: var(--bp-surface-elevated);
|
||||
}
|
||||
|
||||
.unit-item.active {
|
||||
background: var(--bp-surface-elevated);
|
||||
color: var(--bp-accent);
|
||||
border: 1px solid var(--bp-accent);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.unit-item-meta {
|
||||
font-size: 10px;
|
||||
color: var(--bp-text-muted);
|
||||
margin-top: 2px;
|
||||
}
|
||||
|
||||
.btn-unit-add {
|
||||
align-self: flex-end;
|
||||
}
|
||||
|
||||
/* Language Selector */
|
||||
.language-selector {
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
.lang-select {
|
||||
background: var(--bp-surface-elevated);
|
||||
border: 1px solid var(--bp-border-subtle);
|
||||
border-radius: 6px;
|
||||
color: var(--bp-text);
|
||||
padding: 6px 10px;
|
||||
font-size: 12px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.lang-select:hover {
|
||||
border-color: var(--bp-primary);
|
||||
}
|
||||
|
||||
.lang-select:focus {
|
||||
outline: none;
|
||||
border-color: var(--bp-primary);
|
||||
}
|
||||
|
||||
/* Light Mode */
|
||||
[data-theme="light"] .btn-primary:hover:not(:disabled) {
|
||||
box-shadow: 0 4px 12px rgba(14, 165, 233, 0.3);
|
||||
}
|
||||
|
||||
[data-theme="light"] .btn-ghost {
|
||||
border-color: var(--bp-primary);
|
||||
color: var(--bp-primary);
|
||||
}
|
||||
|
||||
[data-theme="light"] .btn-ghost:hover:not(:disabled) {
|
||||
background: var(--bp-primary-soft);
|
||||
}
|
||||
|
||||
[data-theme="light"] .toggle-pill.active {
|
||||
border-color: var(--bp-primary);
|
||||
color: var(--bp-primary);
|
||||
background: var(--bp-primary-soft);
|
||||
}
|
||||
|
||||
[data-theme="light"] .status-bar {
|
||||
background: var(--bp-surface);
|
||||
border: 2px solid var(--bp-primary);
|
||||
box-shadow: 0 4px 16px rgba(14, 165, 233, 0.15);
|
||||
}
|
||||
|
||||
[data-theme="light"] .pager button {
|
||||
border: 2px solid var(--bp-primary);
|
||||
background: var(--bp-surface);
|
||||
color: var(--bp-primary);
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
[data-theme="light"] .pager button:hover {
|
||||
background: var(--bp-primary);
|
||||
color: white;
|
||||
}
|
||||
|
||||
[data-theme="light"] .unit-item:hover {
|
||||
background: var(--bp-primary-soft);
|
||||
}
|
||||
|
||||
[data-theme="light"] .unit-item.active {
|
||||
background: var(--bp-primary-soft);
|
||||
color: var(--bp-primary);
|
||||
border: 2px solid var(--bp-primary);
|
||||
}
|
||||
|
||||
[data-theme="light"] .lang-select {
|
||||
background: var(--bp-surface);
|
||||
border: 1px solid var(--bp-primary);
|
||||
color: var(--bp-text);
|
||||
}
|
||||
|
||||
[data-theme="light"] .lang-select option {
|
||||
background: var(--bp-surface);
|
||||
color: var(--bp-text);
|
||||
}
|
||||
|
||||
/* RTL Support */
|
||||
[dir="rtl"] .card-actions {
|
||||
flex-direction: row-reverse;
|
||||
}
|
||||
183
backend/frontend/static/css/modules/admin/dsms.css
Normal file
183
backend/frontend/static/css/modules/admin/dsms.css
Normal file
@@ -0,0 +1,183 @@
|
||||
/* ==========================================
|
||||
ADMIN PANEL - DSMS Styles
|
||||
Decentralized Storage Management System
|
||||
========================================== */
|
||||
|
||||
.dsms-subtab {
|
||||
padding: 6px 12px;
|
||||
border: none;
|
||||
background: transparent;
|
||||
color: var(--bp-text-muted);
|
||||
cursor: pointer;
|
||||
border-radius: 6px;
|
||||
font-size: 12px;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.dsms-subtab:hover {
|
||||
background: var(--bp-border-subtle);
|
||||
color: var(--bp-text);
|
||||
}
|
||||
|
||||
.dsms-subtab.active {
|
||||
background: var(--bp-primary);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.dsms-content {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.dsms-content.active {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.dsms-status-card {
|
||||
background: var(--bp-surface-elevated);
|
||||
border: 1px solid var(--bp-border);
|
||||
border-radius: 8px;
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.dsms-status-card h4 {
|
||||
margin: 0 0 8px 0;
|
||||
font-size: 12px;
|
||||
color: var(--bp-text-muted);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.05em;
|
||||
}
|
||||
|
||||
.dsms-status-card .value {
|
||||
font-size: 24px;
|
||||
font-weight: 600;
|
||||
color: var(--bp-text);
|
||||
}
|
||||
|
||||
.dsms-status-card .value.online {
|
||||
color: var(--bp-accent);
|
||||
}
|
||||
|
||||
.dsms-status-card .value.offline {
|
||||
color: var(--bp-danger);
|
||||
}
|
||||
|
||||
.dsms-verify-success {
|
||||
background: var(--bp-accent-soft);
|
||||
border: 1px solid var(--bp-accent);
|
||||
border-radius: 8px;
|
||||
padding: 16px;
|
||||
color: var(--bp-accent);
|
||||
}
|
||||
|
||||
.dsms-verify-error {
|
||||
background: rgba(239, 68, 68, 0.1);
|
||||
border: 1px solid var(--bp-danger);
|
||||
border-radius: 8px;
|
||||
padding: 16px;
|
||||
color: var(--bp-danger);
|
||||
}
|
||||
|
||||
/* DSMS WebUI Styles */
|
||||
.dsms-webui-nav {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
padding: 10px 12px;
|
||||
border: none;
|
||||
background: transparent;
|
||||
color: var(--bp-text-muted);
|
||||
font-size: 14px;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
text-align: left;
|
||||
width: 100%;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.dsms-webui-nav:hover {
|
||||
background: var(--bp-surface-elevated);
|
||||
color: var(--bp-text);
|
||||
}
|
||||
|
||||
.dsms-webui-nav.active {
|
||||
background: var(--bp-primary-soft);
|
||||
color: var(--bp-primary);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.dsms-webui-section {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.dsms-webui-section.active {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.dsms-webui-stat-card {
|
||||
background: var(--bp-surface-elevated);
|
||||
border: 1px solid var(--bp-border);
|
||||
border-radius: 8px;
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.dsms-webui-stat-label {
|
||||
font-size: 12px;
|
||||
color: var(--bp-text-muted);
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.dsms-webui-stat-value {
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
color: var(--bp-text);
|
||||
}
|
||||
|
||||
.dsms-webui-stat-sub {
|
||||
font-size: 11px;
|
||||
color: var(--bp-text-muted);
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
.dsms-webui-upload-zone {
|
||||
border: 2px dashed var(--bp-border);
|
||||
border-radius: 12px;
|
||||
padding: 48px 24px;
|
||||
background: var(--bp-input-bg);
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.dsms-webui-upload-zone.dragover {
|
||||
border-color: var(--bp-primary);
|
||||
background: var(--bp-primary-soft);
|
||||
}
|
||||
|
||||
.dsms-webui-file-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 12px 16px;
|
||||
background: var(--bp-surface-elevated);
|
||||
border: 1px solid var(--bp-border);
|
||||
border-radius: 8px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.dsms-webui-file-item .cid {
|
||||
font-family: monospace;
|
||||
font-size: 12px;
|
||||
color: var(--bp-text-muted);
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
/* Light Mode */
|
||||
[data-theme="light"] .dsms-status-card {
|
||||
background: var(--bp-bg);
|
||||
}
|
||||
|
||||
[data-theme="light"] .dsms-webui-stat-card {
|
||||
background: var(--bp-bg);
|
||||
}
|
||||
|
||||
[data-theme="light"] .dsms-webui-file-item {
|
||||
background: var(--bp-bg);
|
||||
}
|
||||
323
backend/frontend/static/css/modules/admin/learning.css
Normal file
323
backend/frontend/static/css/modules/admin/learning.css
Normal file
@@ -0,0 +1,323 @@
|
||||
/* ==========================================
|
||||
ADMIN PANEL - Learning Module Styles
|
||||
MC Questions, Cloze/Lückentext
|
||||
========================================== */
|
||||
|
||||
/* MC Preview Styles */
|
||||
.mc-preview {
|
||||
margin-top: 8px;
|
||||
max-height: 300px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.mc-question {
|
||||
background: var(--bp-surface-elevated);
|
||||
border: 1px solid var(--bp-border-subtle);
|
||||
border-radius: 8px;
|
||||
padding: 10px;
|
||||
margin-bottom: 8px;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.mc-question-text {
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
margin-bottom: 8px;
|
||||
color: var(--bp-text);
|
||||
}
|
||||
|
||||
.mc-options {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.mc-option {
|
||||
font-size: 11px;
|
||||
padding: 6px 10px;
|
||||
border-radius: 6px;
|
||||
background: var(--bp-surface-elevated);
|
||||
border: 1px solid var(--bp-border-subtle);
|
||||
cursor: pointer;
|
||||
transition: all 0.15s ease;
|
||||
}
|
||||
|
||||
.mc-option:hover {
|
||||
background: var(--bp-accent-soft);
|
||||
border-color: var(--bp-accent);
|
||||
}
|
||||
|
||||
.mc-option.selected {
|
||||
background: var(--bp-accent-soft);
|
||||
border-color: var(--bp-accent);
|
||||
}
|
||||
|
||||
.mc-option.correct {
|
||||
background: rgba(90, 191, 96, 0.2);
|
||||
border-color: var(--bp-accent);
|
||||
}
|
||||
|
||||
.mc-option.incorrect {
|
||||
background: rgba(108, 27, 27, 0.2);
|
||||
border-color: rgba(239,68,68,0.6);
|
||||
}
|
||||
|
||||
.mc-option-label {
|
||||
font-weight: 600;
|
||||
margin-right: 6px;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.mc-feedback {
|
||||
margin-top: 8px;
|
||||
font-size: 11px;
|
||||
padding: 6px 8px;
|
||||
border-radius: 6px;
|
||||
background: rgba(34,197,94,0.1);
|
||||
border: 1px solid rgba(34,197,94,0.3);
|
||||
color: var(--bp-accent);
|
||||
}
|
||||
|
||||
.mc-stats {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
margin-top: 8px;
|
||||
padding: 8px;
|
||||
background: var(--bp-surface-elevated);
|
||||
border-radius: 6px;
|
||||
font-size: 11px;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.mc-stats-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
/* MC Modal */
|
||||
.mc-modal {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
background: rgba(0,0,0,0.85);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 50;
|
||||
}
|
||||
|
||||
.mc-modal.hidden {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.mc-modal-content {
|
||||
background: var(--bp-bg);
|
||||
border: 1px solid var(--bp-border-subtle);
|
||||
border-radius: 16px;
|
||||
padding: 20px;
|
||||
max-width: 600px;
|
||||
width: 90%;
|
||||
max-height: 80vh;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.mc-modal-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.mc-modal-title {
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.mc-modal-close {
|
||||
font-size: 11px;
|
||||
padding: 4px 10px;
|
||||
border-radius: 999px;
|
||||
border: 1px solid var(--bp-border-subtle);
|
||||
background: var(--bp-surface-elevated);
|
||||
color: var(--bp-text);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
/* Cloze / Lückentext Styles */
|
||||
.cloze-preview {
|
||||
margin-top: 8px;
|
||||
max-height: 250px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.cloze-item {
|
||||
background: var(--bp-surface-elevated);
|
||||
border: 1px solid var(--bp-border-subtle);
|
||||
border-radius: 8px;
|
||||
padding: 10px;
|
||||
margin-bottom: 8px;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.cloze-sentence {
|
||||
font-size: 13px;
|
||||
line-height: 1.8;
|
||||
color: var(--bp-text);
|
||||
}
|
||||
|
||||
.cloze-gap {
|
||||
display: inline-block;
|
||||
min-width: 60px;
|
||||
border-bottom: 2px solid var(--bp-accent);
|
||||
margin: 0 2px;
|
||||
padding: 2px 4px;
|
||||
text-align: center;
|
||||
background: var(--bp-accent-soft);
|
||||
border-radius: 4px 4px 0 0;
|
||||
}
|
||||
|
||||
.cloze-gap-input {
|
||||
width: 80px;
|
||||
padding: 4px 8px;
|
||||
font-size: 12px;
|
||||
border: 1px solid var(--bp-border-subtle);
|
||||
border-radius: 4px;
|
||||
background: var(--bp-surface-elevated);
|
||||
color: var(--bp-text);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.cloze-gap-input:focus {
|
||||
outline: none;
|
||||
border-color: var(--bp-primary);
|
||||
}
|
||||
|
||||
.cloze-gap-input.correct {
|
||||
border-color: var(--bp-accent);
|
||||
background: rgba(90, 191, 96, 0.2);
|
||||
}
|
||||
|
||||
.cloze-gap-input.incorrect {
|
||||
border-color: var(--bp-danger);
|
||||
background: rgba(108, 27, 27, 0.2);
|
||||
}
|
||||
|
||||
.cloze-translation {
|
||||
margin-top: 8px;
|
||||
padding: 8px;
|
||||
background: var(--bp-accent-soft);
|
||||
border: 1px solid var(--bp-accent);
|
||||
border-radius: 6px;
|
||||
font-size: 11px;
|
||||
color: var(--bp-text-muted);
|
||||
}
|
||||
|
||||
.cloze-translation-label {
|
||||
font-size: 10px;
|
||||
color: var(--bp-text-muted);
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.cloze-hint {
|
||||
font-size: 10px;
|
||||
color: var(--bp-text-muted);
|
||||
font-style: italic;
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
.cloze-stats {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
padding: 8px;
|
||||
background: var(--bp-surface-elevated);
|
||||
border-radius: 6px;
|
||||
font-size: 11px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.cloze-feedback {
|
||||
margin-top: 6px;
|
||||
font-size: 11px;
|
||||
padding: 4px 8px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.cloze-feedback.correct {
|
||||
background: rgba(34,197,94,0.15);
|
||||
color: var(--bp-accent);
|
||||
}
|
||||
|
||||
.cloze-feedback.incorrect {
|
||||
background: rgba(239,68,68,0.15);
|
||||
color: #ef4444;
|
||||
}
|
||||
|
||||
/* Light Mode */
|
||||
[data-theme="light"] .mc-question {
|
||||
background: var(--bp-surface);
|
||||
border: 1px solid var(--bp-primary);
|
||||
box-shadow: 0 2px 8px rgba(14, 165, 233, 0.08);
|
||||
}
|
||||
|
||||
[data-theme="light"] .mc-option {
|
||||
background: var(--bp-bg);
|
||||
border: 1px solid var(--bp-border);
|
||||
}
|
||||
|
||||
[data-theme="light"] .mc-option:hover {
|
||||
background: var(--bp-primary-soft);
|
||||
border-color: var(--bp-primary);
|
||||
}
|
||||
|
||||
[data-theme="light"] .mc-option.selected {
|
||||
background: var(--bp-primary-soft);
|
||||
border-color: var(--bp-primary);
|
||||
}
|
||||
|
||||
[data-theme="light"] .mc-stats {
|
||||
background: var(--bp-bg);
|
||||
border: 1px solid var(--bp-border);
|
||||
}
|
||||
|
||||
[data-theme="light"] .mc-modal-content {
|
||||
background: var(--bp-surface);
|
||||
border: 2px solid var(--bp-primary);
|
||||
box-shadow: 0 8px 32px rgba(14, 165, 233, 0.2);
|
||||
}
|
||||
|
||||
[data-theme="light"] .mc-modal-close {
|
||||
border: 1px solid var(--bp-primary);
|
||||
background: var(--bp-surface);
|
||||
color: var(--bp-primary);
|
||||
}
|
||||
|
||||
[data-theme="light"] .mc-modal-close:hover {
|
||||
background: var(--bp-primary);
|
||||
color: white;
|
||||
}
|
||||
|
||||
[data-theme="light"] .cloze-item {
|
||||
background: var(--bp-surface);
|
||||
border: 1px solid var(--bp-primary);
|
||||
box-shadow: 0 2px 8px rgba(14, 165, 233, 0.08);
|
||||
}
|
||||
|
||||
[data-theme="light"] .cloze-gap {
|
||||
border-bottom-color: var(--bp-primary);
|
||||
background: var(--bp-primary-soft);
|
||||
}
|
||||
|
||||
[data-theme="light"] .cloze-gap-input {
|
||||
background: var(--bp-surface);
|
||||
border: 1px solid var(--bp-border);
|
||||
}
|
||||
|
||||
[data-theme="light"] .cloze-translation {
|
||||
background: var(--bp-primary-soft);
|
||||
border: 1px solid var(--bp-primary);
|
||||
}
|
||||
|
||||
[data-theme="light"] .cloze-stats {
|
||||
background: var(--bp-bg);
|
||||
border: 1px solid var(--bp-border);
|
||||
}
|
||||
132
backend/frontend/static/css/modules/admin/modal.css
Normal file
132
backend/frontend/static/css/modules/admin/modal.css
Normal file
@@ -0,0 +1,132 @@
|
||||
/* ==========================================
|
||||
ADMIN PANEL - Modal & Tabs
|
||||
Basic modal structure and navigation
|
||||
========================================== */
|
||||
|
||||
.admin-modal {
|
||||
display: none;
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: rgba(0, 0, 0, 0.7);
|
||||
z-index: 10000;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
backdrop-filter: blur(4px);
|
||||
}
|
||||
|
||||
.admin-modal.active {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.admin-modal-content {
|
||||
background: var(--bp-surface);
|
||||
border-radius: 16px;
|
||||
width: 95%;
|
||||
max-width: 1000px;
|
||||
max-height: 90vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
box-shadow: 0 20px 60px rgba(0,0,0,0.4);
|
||||
border: 1px solid var(--bp-border);
|
||||
}
|
||||
|
||||
.admin-modal-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 20px 24px;
|
||||
border-bottom: 1px solid var(--bp-border);
|
||||
}
|
||||
|
||||
.admin-modal-header h2 {
|
||||
margin: 0;
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.admin-tabs {
|
||||
display: flex;
|
||||
gap: 4px;
|
||||
padding: 12px 24px;
|
||||
border-bottom: 1px solid var(--bp-border);
|
||||
background: var(--bp-surface-elevated);
|
||||
}
|
||||
|
||||
.admin-tab {
|
||||
padding: 8px 16px;
|
||||
border: none;
|
||||
background: transparent;
|
||||
color: var(--bp-text-muted);
|
||||
cursor: pointer;
|
||||
border-radius: 8px;
|
||||
font-size: 13px;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.admin-tab:hover {
|
||||
background: var(--bp-border-subtle);
|
||||
color: var(--bp-text);
|
||||
}
|
||||
|
||||
.admin-tab.active {
|
||||
background: var(--bp-primary);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.admin-body {
|
||||
padding: 24px;
|
||||
overflow-y: auto;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.admin-content {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.admin-content.active {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.admin-toolbar {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 20px;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.admin-toolbar-left {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.admin-search {
|
||||
padding: 8px 12px;
|
||||
border: 1px solid var(--bp-border);
|
||||
border-radius: 8px;
|
||||
background: var(--bp-surface-elevated);
|
||||
color: var(--bp-text);
|
||||
width: 250px;
|
||||
}
|
||||
|
||||
/* Light Mode */
|
||||
[data-theme="light"] .admin-modal-content {
|
||||
background: var(--bp-surface);
|
||||
border-color: var(--bp-border);
|
||||
}
|
||||
|
||||
[data-theme="light"] .admin-tabs {
|
||||
background: var(--bp-bg);
|
||||
}
|
||||
|
||||
[data-theme="light"] .admin-tab.active {
|
||||
background: var(--bp-primary);
|
||||
color: white;
|
||||
}
|
||||
202
backend/frontend/static/css/modules/admin/preview.css
Normal file
202
backend/frontend/static/css/modules/admin/preview.css
Normal file
@@ -0,0 +1,202 @@
|
||||
/* ==========================================
|
||||
ADMIN PANEL - Preview & Compare
|
||||
Image preview, document comparison
|
||||
========================================== */
|
||||
|
||||
.compare-header {
|
||||
background: var(--bp-surface-elevated);
|
||||
padding: 6px 10px;
|
||||
font-size: 12px;
|
||||
border-bottom: 1px solid var(--bp-border-subtle);
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.compare-header span {
|
||||
color: var(--bp-text-muted);
|
||||
}
|
||||
|
||||
.compare-body {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 6px;
|
||||
}
|
||||
|
||||
.compare-body-inner {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.preview-img {
|
||||
max-width: 100%;
|
||||
max-height: 100%;
|
||||
object-fit: contain;
|
||||
box-shadow: 0 18px 40px rgba(0,0,0,0.5);
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
.clean-frame {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border: none;
|
||||
border-radius: 10px;
|
||||
background: white;
|
||||
}
|
||||
|
||||
/* Preview Navigation */
|
||||
.preview-nav {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
pointer-events: none;
|
||||
padding: 0 4px;
|
||||
}
|
||||
|
||||
.preview-nav button {
|
||||
pointer-events: auto;
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
border-radius: 999px;
|
||||
border: 1px solid var(--bp-border-subtle);
|
||||
background: var(--bp-surface-elevated);
|
||||
color: var(--bp-text);
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.preview-nav button:hover:not(:disabled) {
|
||||
border-color: var(--bp-primary);
|
||||
color: var(--bp-primary);
|
||||
}
|
||||
|
||||
.preview-nav button:disabled {
|
||||
opacity: 0.35;
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.preview-nav span {
|
||||
position: absolute;
|
||||
bottom: 6px;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
font-size: 11px;
|
||||
padding: 2px 8px;
|
||||
border-radius: 999px;
|
||||
background: var(--bp-surface-elevated);
|
||||
border: 1px solid var(--bp-border-subtle);
|
||||
}
|
||||
|
||||
/* Preview Thumbnails */
|
||||
.preview-thumbnails {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
padding: 8px 4px;
|
||||
overflow-y: auto;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.preview-thumb {
|
||||
min-width: 90px;
|
||||
width: 90px;
|
||||
height: 70px;
|
||||
border-radius: 8px;
|
||||
border: 2px solid rgba(148,163,184,0.25);
|
||||
background: rgba(15,23,42,0.5);
|
||||
cursor: pointer;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
position: relative;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.preview-thumb:hover {
|
||||
border-color: var(--bp-accent);
|
||||
}
|
||||
|
||||
.preview-thumb.active {
|
||||
border-color: var(--bp-accent);
|
||||
border-width: 3px;
|
||||
}
|
||||
|
||||
.preview-thumb img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.preview-thumb-label {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
font-size: 9px;
|
||||
padding: 2px;
|
||||
background: rgba(0,0,0,0.8);
|
||||
color: white;
|
||||
text-align: center;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
/* Light Mode */
|
||||
[data-theme="light"] .compare-header {
|
||||
background: var(--bp-primary-soft);
|
||||
border-bottom: 1px solid var(--bp-primary);
|
||||
}
|
||||
|
||||
[data-theme="light"] .compare-header span {
|
||||
color: var(--bp-primary);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
[data-theme="light"] .preview-img {
|
||||
box-shadow: 0 8px 24px rgba(14, 165, 233, 0.15);
|
||||
}
|
||||
|
||||
[data-theme="light"] .preview-nav button {
|
||||
border: 2px solid var(--bp-primary);
|
||||
background: var(--bp-surface);
|
||||
color: var(--bp-primary);
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
[data-theme="light"] .preview-nav button:hover:not(:disabled) {
|
||||
background: var(--bp-primary);
|
||||
color: white;
|
||||
}
|
||||
|
||||
[data-theme="light"] .preview-nav span {
|
||||
background: var(--bp-surface);
|
||||
border: 1px solid var(--bp-primary);
|
||||
color: var(--bp-primary);
|
||||
}
|
||||
|
||||
[data-theme="light"] .preview-thumb {
|
||||
background: var(--bp-bg);
|
||||
border-color: var(--bp-border);
|
||||
}
|
||||
|
||||
[data-theme="light"] .preview-thumb:hover,
|
||||
[data-theme="light"] .preview-thumb.active {
|
||||
border-color: var(--bp-primary);
|
||||
}
|
||||
267
backend/frontend/static/css/modules/admin/sidebar.css
Normal file
267
backend/frontend/static/css/modules/admin/sidebar.css
Normal file
@@ -0,0 +1,267 @@
|
||||
/* ==========================================
|
||||
ADMIN PANEL - Sidebar & Navigation
|
||||
Main layout sidebar and menu items
|
||||
========================================== */
|
||||
|
||||
.main-layout {
|
||||
display: grid;
|
||||
grid-template-columns: 240px minmax(0, 1fr);
|
||||
height: 100%;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.sidebar {
|
||||
border-right: 1px solid var(--bp-border-subtle);
|
||||
background: var(--bp-gradient-sidebar);
|
||||
padding: 14px 10px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 18px;
|
||||
min-width: 0;
|
||||
height: 100%;
|
||||
max-height: 100vh;
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
transition: background 0.3s ease, border-color 0.3s ease;
|
||||
}
|
||||
|
||||
.sidebar-section-title {
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.08em;
|
||||
color: var(--bp-text);
|
||||
padding: 8px 6px 6px 6px;
|
||||
margin-top: 12px;
|
||||
}
|
||||
|
||||
.sidebar-menu {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.sidebar-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 8px 10px;
|
||||
border-radius: 9px;
|
||||
cursor: pointer;
|
||||
font-size: 13px;
|
||||
color: var(--bp-text-muted);
|
||||
}
|
||||
|
||||
.sidebar-item.active {
|
||||
background: var(--bp-surface-elevated);
|
||||
color: var(--bp-accent);
|
||||
border: 1px solid var(--bp-accent-soft);
|
||||
}
|
||||
|
||||
.sidebar-item-label {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 7px;
|
||||
}
|
||||
|
||||
.sidebar-item-badge {
|
||||
font-size: 10px;
|
||||
border-radius: 999px;
|
||||
padding: 2px 7px;
|
||||
border: 1px solid var(--bp-border-subtle);
|
||||
}
|
||||
|
||||
/* Sub-Navigation */
|
||||
.sidebar-sub-menu {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
padding: 4px 0 8px 20px;
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
.sidebar-sub-item {
|
||||
font-size: 12px;
|
||||
color: var(--bp-text-muted);
|
||||
padding: 6px 10px;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
transition: all 0.15s ease;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.sidebar-sub-item::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
left: -12px;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
width: 4px;
|
||||
height: 4px;
|
||||
border-radius: 50%;
|
||||
background: var(--bp-border);
|
||||
}
|
||||
|
||||
.sidebar-sub-item:hover {
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
color: var(--bp-text);
|
||||
}
|
||||
|
||||
.sidebar-sub-item.active {
|
||||
background: rgba(var(--bp-primary), 0.15);
|
||||
color: var(--bp-primary);
|
||||
}
|
||||
|
||||
.sidebar-sub-item.active::before {
|
||||
background: var(--bp-primary);
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
}
|
||||
|
||||
.sidebar-footer {
|
||||
margin-top: auto;
|
||||
font-size: 11px;
|
||||
color: var(--bp-text-muted);
|
||||
padding: 0 6px;
|
||||
}
|
||||
|
||||
/* Template Items */
|
||||
.template-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 8px 10px;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
font-size: 13px;
|
||||
transition: all 0.15s ease;
|
||||
}
|
||||
|
||||
.template-item:hover {
|
||||
background: rgba(255, 255, 255, 0.08);
|
||||
}
|
||||
|
||||
/* Letter Items */
|
||||
.letter-item {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 10px 8px;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
transition: all 0.15s ease;
|
||||
border-bottom: 1px solid var(--bp-border);
|
||||
}
|
||||
|
||||
.letter-item:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.letter-item:hover {
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
}
|
||||
|
||||
.letter-info {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
}
|
||||
|
||||
.letter-title {
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.letter-date {
|
||||
font-size: 11px;
|
||||
color: var(--bp-text-muted);
|
||||
}
|
||||
|
||||
.letter-status {
|
||||
font-size: 10px;
|
||||
padding: 2px 6px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.letter-status.sent {
|
||||
background: rgba(16, 185, 129, 0.2);
|
||||
color: #10b981;
|
||||
}
|
||||
|
||||
.letter-status.draft {
|
||||
background: rgba(245, 158, 11, 0.2);
|
||||
color: #f59e0b;
|
||||
}
|
||||
|
||||
/* Meeting Items */
|
||||
.meeting-item {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 8px;
|
||||
border-radius: 6px;
|
||||
background: rgba(255, 255, 255, 0.03);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.meeting-item:hover {
|
||||
background: rgba(255, 255, 255, 0.08);
|
||||
}
|
||||
|
||||
.meeting-info {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
}
|
||||
|
||||
.meeting-title {
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.meeting-time {
|
||||
font-size: 11px;
|
||||
color: var(--bp-text-muted);
|
||||
}
|
||||
|
||||
/* Light Mode */
|
||||
[data-theme="light"] .sidebar-item.active {
|
||||
background: var(--bp-primary-soft);
|
||||
color: var(--bp-primary);
|
||||
border: 1px solid var(--bp-primary);
|
||||
}
|
||||
|
||||
[data-theme="light"] .sidebar-sub-item:hover {
|
||||
background: rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
||||
[data-theme="light"] .sidebar-sub-item.active {
|
||||
background: rgba(14, 165, 233, 0.1);
|
||||
color: #0ea5e9;
|
||||
}
|
||||
|
||||
[data-theme="light"] .sidebar-sub-item.active::before {
|
||||
background: #0ea5e9;
|
||||
}
|
||||
|
||||
[data-theme="light"] .template-item:hover {
|
||||
background: rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
||||
[data-theme="light"] .meeting-item {
|
||||
background: rgba(0, 0, 0, 0.02);
|
||||
}
|
||||
|
||||
[data-theme="light"] .meeting-item:hover {
|
||||
background: rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
||||
/* RTL Support */
|
||||
[dir="rtl"] .sidebar {
|
||||
border-right: none;
|
||||
border-left: 1px solid rgba(148,163,184,0.2);
|
||||
}
|
||||
|
||||
[dir="rtl"] .main-layout {
|
||||
direction: rtl;
|
||||
}
|
||||
115
backend/frontend/static/css/modules/admin/tables.css
Normal file
115
backend/frontend/static/css/modules/admin/tables.css
Normal file
@@ -0,0 +1,115 @@
|
||||
/* ==========================================
|
||||
ADMIN PANEL - Tables & Badges
|
||||
Table styles and status indicators
|
||||
========================================== */
|
||||
|
||||
.admin-table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.admin-table th,
|
||||
.admin-table td {
|
||||
padding: 12px;
|
||||
text-align: left;
|
||||
border-bottom: 1px solid var(--bp-border);
|
||||
}
|
||||
|
||||
.admin-table th {
|
||||
background: var(--bp-surface-elevated);
|
||||
font-weight: 600;
|
||||
color: var(--bp-text);
|
||||
}
|
||||
|
||||
.admin-table tr:hover {
|
||||
background: var(--bp-surface-elevated);
|
||||
}
|
||||
|
||||
.admin-table td {
|
||||
color: var(--bp-text-muted);
|
||||
}
|
||||
|
||||
/* Status Badges */
|
||||
.admin-badge {
|
||||
display: inline-block;
|
||||
padding: 4px 8px;
|
||||
border-radius: 4px;
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.admin-badge-published {
|
||||
background: rgba(74, 222, 128, 0.2);
|
||||
color: #4ADE80;
|
||||
}
|
||||
|
||||
.admin-badge-draft {
|
||||
background: rgba(251, 191, 36, 0.2);
|
||||
color: #FBBF24;
|
||||
}
|
||||
|
||||
.admin-badge-archived {
|
||||
background: rgba(156, 163, 175, 0.2);
|
||||
color: #9CA3AF;
|
||||
}
|
||||
|
||||
.admin-badge-rejected {
|
||||
background: rgba(239, 68, 68, 0.2);
|
||||
color: #EF4444;
|
||||
}
|
||||
|
||||
.admin-badge-review {
|
||||
background: rgba(147, 51, 234, 0.2);
|
||||
color: #A855F7;
|
||||
}
|
||||
|
||||
.admin-badge-approved {
|
||||
background: rgba(34, 197, 94, 0.2);
|
||||
color: #22C55E;
|
||||
}
|
||||
|
||||
.admin-badge-submitted {
|
||||
background: rgba(59, 130, 246, 0.2);
|
||||
color: #3B82F6;
|
||||
}
|
||||
|
||||
.admin-badge-mandatory {
|
||||
background: rgba(239, 68, 68, 0.2);
|
||||
color: #EF4444;
|
||||
}
|
||||
|
||||
.admin-badge-optional {
|
||||
background: rgba(156, 163, 175, 0.2);
|
||||
color: #9CA3AF;
|
||||
}
|
||||
|
||||
.admin-badge-active {
|
||||
background: rgba(34, 197, 94, 0.2);
|
||||
color: #22C55E;
|
||||
}
|
||||
|
||||
.admin-badge-inactive {
|
||||
background: rgba(156, 163, 175, 0.2);
|
||||
color: #9CA3AF;
|
||||
}
|
||||
|
||||
.admin-badge-pending {
|
||||
background: rgba(251, 191, 36, 0.2);
|
||||
color: #FBBF24;
|
||||
}
|
||||
|
||||
.admin-badge-completed {
|
||||
background: rgba(74, 222, 128, 0.2);
|
||||
color: #4ADE80;
|
||||
}
|
||||
|
||||
/* Light Mode */
|
||||
[data-theme="light"] .admin-table th {
|
||||
background: var(--bp-bg);
|
||||
}
|
||||
|
||||
[data-theme="light"] .admin-table tr:hover {
|
||||
background: var(--bp-primary-soft);
|
||||
}
|
||||
163
backend/frontend/static/css/modules/base/layout.css
Normal file
163
backend/frontend/static/css/modules/base/layout.css
Normal file
@@ -0,0 +1,163 @@
|
||||
/* ==========================================
|
||||
Base Layout Styles
|
||||
Reset, Scrollbar, App Structure, Topbar
|
||||
========================================== */
|
||||
|
||||
* { box-sizing: border-box; }
|
||||
|
||||
/* Custom Scrollbar */
|
||||
::-webkit-scrollbar {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-track {
|
||||
background: var(--bp-scrollbar-track);
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb {
|
||||
background: var(--bp-scrollbar-thumb);
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb:hover {
|
||||
background: var(--bp-scrollbar-thumb);
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
html, body {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
font-family: system-ui, -apple-system, BlinkMacSystemFont, "SF Pro Text", sans-serif;
|
||||
background: var(--bp-gradient-bg);
|
||||
color: var(--bp-text);
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
transition: background 0.3s ease, color 0.3s ease;
|
||||
}
|
||||
|
||||
.app-root {
|
||||
display: grid;
|
||||
grid-template-rows: 56px 1fr 32px;
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
/* ==========================================
|
||||
Topbar Styles
|
||||
========================================== */
|
||||
.topbar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 0 20px;
|
||||
border-bottom: 1px solid var(--bp-border-subtle);
|
||||
backdrop-filter: blur(18px);
|
||||
background: var(--bp-gradient-topbar);
|
||||
transition: background 0.3s ease, border-color 0.3s ease;
|
||||
}
|
||||
|
||||
.brand {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.brand-logo {
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
border-radius: 999px;
|
||||
border: 2px solid var(--bp-accent);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-weight: 700;
|
||||
font-size: 14px;
|
||||
color: var(--bp-accent);
|
||||
}
|
||||
|
||||
.brand-text-main {
|
||||
font-weight: 600;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.brand-text-sub {
|
||||
font-size: 12px;
|
||||
color: var(--bp-text-muted);
|
||||
}
|
||||
|
||||
/* ==========================================
|
||||
Top Navigation
|
||||
========================================== */
|
||||
.top-nav {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 18px;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.top-nav-item {
|
||||
cursor: pointer;
|
||||
padding: 4px 10px;
|
||||
border-radius: 999px;
|
||||
color: var(--bp-text-muted);
|
||||
border: 1px solid transparent;
|
||||
}
|
||||
|
||||
.top-nav-item.active {
|
||||
color: var(--bp-primary);
|
||||
border-color: var(--bp-accent-soft);
|
||||
background: var(--bp-surface-elevated);
|
||||
}
|
||||
|
||||
[data-theme="light"] .top-nav-item.active {
|
||||
color: var(--bp-primary);
|
||||
background: var(--bp-primary-soft);
|
||||
border-color: var(--bp-primary);
|
||||
}
|
||||
|
||||
.top-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.pill {
|
||||
font-size: 11px;
|
||||
padding: 4px 10px;
|
||||
border-radius: 999px;
|
||||
border: 1px solid var(--bp-border-subtle);
|
||||
color: var(--bp-text-muted);
|
||||
}
|
||||
|
||||
/* ==========================================
|
||||
Theme Toggle Button
|
||||
========================================== */
|
||||
.theme-toggle {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
padding: 4px 10px;
|
||||
border-radius: 999px;
|
||||
border: 1px solid var(--bp-border-subtle);
|
||||
background: var(--bp-surface-elevated);
|
||||
color: var(--bp-text-muted);
|
||||
cursor: pointer;
|
||||
font-size: 11px;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.theme-toggle:hover {
|
||||
border-color: var(--bp-primary);
|
||||
color: var(--bp-primary);
|
||||
}
|
||||
|
||||
.theme-toggle-icon {
|
||||
font-size: 14px;
|
||||
}
|
||||
69
backend/frontend/static/css/modules/base/variables.css
Normal file
69
backend/frontend/static/css/modules/base/variables.css
Normal file
@@ -0,0 +1,69 @@
|
||||
/* ==========================================
|
||||
CSS Custom Properties (Variables)
|
||||
Dark Mode (Default) + Light Mode
|
||||
========================================== */
|
||||
|
||||
:root {
|
||||
--bp-primary: #0f766e;
|
||||
--bp-primary-soft: #ccfbf1;
|
||||
--bp-bg: #020617;
|
||||
--bp-surface: #020617;
|
||||
--bp-surface-elevated: rgba(15,23,42,0.9);
|
||||
--bp-border: #1f2937;
|
||||
--bp-border-subtle: rgba(148,163,184,0.25);
|
||||
--bp-accent: #22c55e;
|
||||
--bp-accent-soft: rgba(34,197,94,0.2);
|
||||
--bp-text: #e5e7eb;
|
||||
--bp-text-muted: #9ca3af;
|
||||
--bp-danger: #ef4444;
|
||||
--bp-gradient-bg: radial-gradient(circle at top left, #1f2937 0, #020617 50%, #000 100%);
|
||||
--bp-gradient-surface: radial-gradient(circle at top left, rgba(15,23,42,0.9) 0, #020617 50%, #000 100%);
|
||||
--bp-gradient-sidebar: radial-gradient(circle at top, #020617 0, #020617 40%, #000 100%);
|
||||
--bp-gradient-topbar: linear-gradient(to right, rgba(15,23,42,0.9), rgba(15,23,42,0.6));
|
||||
--bp-btn-primary-bg: linear-gradient(to right, var(--bp-primary), #15803d);
|
||||
--bp-btn-primary-hover: linear-gradient(to right, #0f766e, #166534);
|
||||
--bp-card-bg: var(--bp-gradient-surface);
|
||||
--bp-input-bg: rgba(255,255,255,0.05);
|
||||
--bp-scrollbar-track: rgba(15,23,42,0.5);
|
||||
--bp-scrollbar-thumb: rgba(148,163,184,0.5);
|
||||
}
|
||||
|
||||
/* ==========================================
|
||||
LIGHT MODE - Website Design
|
||||
Farben aus BreakPilot Website:
|
||||
- Primary: Sky Blue #0ea5e9 (Modern, Vertrauen)
|
||||
- Accent: Fuchsia #d946ef (Kreativ, Energie)
|
||||
- Text: Slate #0f172a (Klarheit)
|
||||
- Background: White/Slate-50 (Clean, Professional)
|
||||
========================================== */
|
||||
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap');
|
||||
|
||||
[data-theme="light"] {
|
||||
--bp-primary: #0ea5e9;
|
||||
--bp-primary-soft: rgba(14, 165, 233, 0.1);
|
||||
--bp-bg: #f8fafc;
|
||||
--bp-surface: #FFFFFF;
|
||||
--bp-surface-elevated: #FFFFFF;
|
||||
--bp-border: #e2e8f0;
|
||||
--bp-border-subtle: rgba(14, 165, 233, 0.2);
|
||||
--bp-accent: #d946ef;
|
||||
--bp-accent-soft: rgba(217, 70, 239, 0.15);
|
||||
--bp-text: #0f172a;
|
||||
--bp-text-muted: #64748b;
|
||||
--bp-danger: #ef4444;
|
||||
--bp-gradient-bg: linear-gradient(145deg, #f8fafc 0%, #f1f5f9 100%);
|
||||
--bp-gradient-surface: linear-gradient(145deg, #FFFFFF 0%, #f8fafc 100%);
|
||||
--bp-gradient-sidebar: linear-gradient(180deg, #FFFFFF 0%, #f1f5f9 100%);
|
||||
--bp-gradient-topbar: linear-gradient(to right, #FFFFFF, #f8fafc);
|
||||
--bp-btn-primary-bg: linear-gradient(135deg, #0ea5e9 0%, #d946ef 100%);
|
||||
--bp-btn-primary-hover: linear-gradient(135deg, #0284c7 0%, #c026d3 100%);
|
||||
--bp-card-bg: linear-gradient(145deg, #FFFFFF 0%, #f8fafc 100%);
|
||||
--bp-input-bg: #FFFFFF;
|
||||
--bp-scrollbar-track: rgba(0,0,0,0.05);
|
||||
--bp-scrollbar-thumb: rgba(14, 165, 233, 0.3);
|
||||
--bp-gold: #eab308;
|
||||
}
|
||||
|
||||
[data-theme="light"] body {
|
||||
font-family: 'Inter', system-ui, -apple-system, sans-serif;
|
||||
}
|
||||
261
backend/frontend/static/css/modules/components/auth-modal.css
Normal file
261
backend/frontend/static/css/modules/components/auth-modal.css
Normal file
@@ -0,0 +1,261 @@
|
||||
/* ==========================================
|
||||
AUTH MODAL STYLES
|
||||
Login, Register, Password Reset
|
||||
========================================== */
|
||||
|
||||
.auth-modal {
|
||||
display: none;
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: rgba(0, 0, 0, 0.7);
|
||||
z-index: 10000;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
backdrop-filter: blur(4px);
|
||||
}
|
||||
|
||||
.auth-modal.active {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.auth-modal-content {
|
||||
background: var(--bp-surface);
|
||||
border-radius: 16px;
|
||||
width: 90%;
|
||||
max-width: 420px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
box-shadow: 0 20px 60px rgba(0,0,0,0.4);
|
||||
border: 1px solid var(--bp-border);
|
||||
}
|
||||
|
||||
.auth-modal-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 20px 24px;
|
||||
border-bottom: 1px solid var(--bp-border);
|
||||
}
|
||||
|
||||
.auth-modal-header h2 {
|
||||
margin: 0;
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.auth-modal-close {
|
||||
background: none;
|
||||
border: none;
|
||||
font-size: 28px;
|
||||
cursor: pointer;
|
||||
color: var(--bp-text-muted);
|
||||
padding: 0;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.auth-modal-close:hover {
|
||||
color: var(--bp-text);
|
||||
}
|
||||
|
||||
.auth-body {
|
||||
padding: 24px;
|
||||
}
|
||||
|
||||
.auth-form {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.auth-form-group {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.auth-form-group label {
|
||||
font-size: 13px;
|
||||
color: var(--bp-text);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.auth-form-group input {
|
||||
padding: 12px 14px;
|
||||
border: 1px solid var(--bp-border);
|
||||
border-radius: 8px;
|
||||
background: var(--bp-surface-elevated);
|
||||
color: var(--bp-text);
|
||||
font-size: 14px;
|
||||
transition: border-color 0.2s ease;
|
||||
}
|
||||
|
||||
.auth-form-group input:focus {
|
||||
outline: none;
|
||||
border-color: var(--bp-primary);
|
||||
}
|
||||
|
||||
.auth-form-group input::placeholder {
|
||||
color: var(--bp-text-muted);
|
||||
}
|
||||
|
||||
.auth-submit-btn {
|
||||
padding: 12px 20px;
|
||||
background: var(--bp-btn-primary-bg);
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
color: white;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.auth-submit-btn:hover {
|
||||
background: var(--bp-btn-primary-hover);
|
||||
}
|
||||
|
||||
.auth-submit-btn:disabled {
|
||||
opacity: 0.6;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.auth-divider {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
margin: 8px 0;
|
||||
color: var(--bp-text-muted);
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.auth-divider::before,
|
||||
.auth-divider::after {
|
||||
content: '';
|
||||
flex: 1;
|
||||
height: 1px;
|
||||
background: var(--bp-border);
|
||||
}
|
||||
|
||||
.auth-social-btns {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.auth-social-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 10px;
|
||||
padding: 12px;
|
||||
border: 1px solid var(--bp-border);
|
||||
border-radius: 8px;
|
||||
background: var(--bp-surface-elevated);
|
||||
color: var(--bp-text);
|
||||
font-size: 14px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.auth-social-btn:hover {
|
||||
border-color: var(--bp-primary);
|
||||
background: var(--bp-surface);
|
||||
}
|
||||
|
||||
.auth-social-btn img {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
}
|
||||
|
||||
.auth-footer {
|
||||
padding: 16px 24px;
|
||||
border-top: 1px solid var(--bp-border);
|
||||
text-align: center;
|
||||
font-size: 13px;
|
||||
color: var(--bp-text-muted);
|
||||
}
|
||||
|
||||
.auth-footer a {
|
||||
color: var(--bp-primary);
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.auth-footer a:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.auth-error {
|
||||
padding: 12px;
|
||||
background: rgba(239, 68, 68, 0.1);
|
||||
border: 1px solid rgba(239, 68, 68, 0.3);
|
||||
border-radius: 8px;
|
||||
color: var(--bp-danger);
|
||||
font-size: 13px;
|
||||
display: none;
|
||||
}
|
||||
|
||||
.auth-error.active {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.auth-success {
|
||||
padding: 12px;
|
||||
background: rgba(34, 197, 94, 0.1);
|
||||
border: 1px solid rgba(34, 197, 94, 0.3);
|
||||
border-radius: 8px;
|
||||
color: var(--bp-accent);
|
||||
font-size: 13px;
|
||||
display: none;
|
||||
}
|
||||
|
||||
.auth-success.active {
|
||||
display: block;
|
||||
}
|
||||
|
||||
/* Password strength indicator */
|
||||
.password-strength {
|
||||
display: flex;
|
||||
gap: 4px;
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.password-strength-bar {
|
||||
flex: 1;
|
||||
height: 4px;
|
||||
background: var(--bp-border);
|
||||
border-radius: 2px;
|
||||
transition: background 0.2s ease;
|
||||
}
|
||||
|
||||
.password-strength-bar.weak { background: #ef4444; }
|
||||
.password-strength-bar.fair { background: #f59e0b; }
|
||||
.password-strength-bar.good { background: #22c55e; }
|
||||
.password-strength-bar.strong { background: #0f766e; }
|
||||
|
||||
.password-strength-text {
|
||||
font-size: 11px;
|
||||
color: var(--bp-text-muted);
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
/* Light Mode Overrides */
|
||||
[data-theme="light"] .auth-modal-content {
|
||||
background: var(--bp-surface);
|
||||
border-color: var(--bp-border);
|
||||
}
|
||||
|
||||
[data-theme="light"] .auth-form-group input {
|
||||
background: var(--bp-bg);
|
||||
border-color: var(--bp-border);
|
||||
}
|
||||
|
||||
[data-theme="light"] .auth-social-btn {
|
||||
background: var(--bp-bg);
|
||||
}
|
||||
155
backend/frontend/static/css/modules/components/communication.css
Normal file
155
backend/frontend/static/css/modules/components/communication.css
Normal file
@@ -0,0 +1,155 @@
|
||||
/* ==========================================
|
||||
Communication Panel Styles (Matrix + Jitsi)
|
||||
Chat, Room List, Messages
|
||||
========================================== */
|
||||
|
||||
/* Room List Styles */
|
||||
.room-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
padding: 10px 12px;
|
||||
border-radius: 8px;
|
||||
cursor: pointer;
|
||||
transition: background 0.2s;
|
||||
}
|
||||
|
||||
.room-item:hover {
|
||||
background: var(--bp-surface-elevated);
|
||||
}
|
||||
|
||||
.room-item.active {
|
||||
background: var(--bp-primary-soft);
|
||||
border-left: 3px solid var(--bp-primary);
|
||||
}
|
||||
|
||||
.room-icon {
|
||||
font-size: 20px;
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: var(--bp-surface-elevated);
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.room-info {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.room-name {
|
||||
font-weight: 500;
|
||||
font-size: 13px;
|
||||
color: var(--bp-text);
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.room-preview {
|
||||
font-size: 11px;
|
||||
color: var(--bp-text-muted);
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.room-badge {
|
||||
background: var(--bp-primary);
|
||||
color: white;
|
||||
font-size: 10px;
|
||||
font-weight: 600;
|
||||
padding: 2px 6px;
|
||||
border-radius: 10px;
|
||||
min-width: 18px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
/* Chat Message Styles */
|
||||
.chat-system-msg {
|
||||
text-align: center;
|
||||
padding: 8px 16px;
|
||||
background: var(--bp-surface-elevated);
|
||||
border-radius: 8px;
|
||||
font-size: 12px;
|
||||
color: var(--bp-text-muted);
|
||||
}
|
||||
|
||||
.chat-msg {
|
||||
padding: 12px 16px;
|
||||
border-radius: 12px;
|
||||
max-width: 85%;
|
||||
}
|
||||
|
||||
.chat-msg-self {
|
||||
background: var(--bp-primary-soft);
|
||||
border: 1px solid var(--bp-border-subtle);
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
.chat-msg-other {
|
||||
background: var(--bp-surface-elevated);
|
||||
border: 1px solid var(--bp-border);
|
||||
}
|
||||
|
||||
.chat-msg-notification {
|
||||
background: rgba(239, 68, 68, 0.1);
|
||||
border: 1px solid rgba(239, 68, 68, 0.3);
|
||||
}
|
||||
|
||||
.chat-msg-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
|
||||
.chat-msg-sender {
|
||||
font-weight: 600;
|
||||
font-size: 12px;
|
||||
color: var(--bp-primary);
|
||||
}
|
||||
|
||||
.chat-msg-time {
|
||||
font-size: 10px;
|
||||
color: var(--bp-text-muted);
|
||||
}
|
||||
|
||||
.chat-msg-content {
|
||||
font-size: 13px;
|
||||
line-height: 1.5;
|
||||
color: var(--bp-text);
|
||||
}
|
||||
|
||||
.chat-msg-actions {
|
||||
display: flex;
|
||||
gap: 6px;
|
||||
margin-top: 10px;
|
||||
padding-top: 10px;
|
||||
border-top: 1px solid var(--bp-border);
|
||||
}
|
||||
|
||||
/* Button danger variant (global) */
|
||||
.btn-danger {
|
||||
background: #ef4444 !important;
|
||||
color: white !important;
|
||||
}
|
||||
|
||||
.btn-danger:hover {
|
||||
background: #dc2626 !important;
|
||||
}
|
||||
|
||||
/* Light Mode Overrides */
|
||||
[data-theme="light"] .room-item.active {
|
||||
background: var(--bp-primary-soft);
|
||||
}
|
||||
|
||||
[data-theme="light"] .chat-msg-self {
|
||||
background: var(--bp-primary-soft);
|
||||
}
|
||||
|
||||
[data-theme="light"] .chat-msg-other {
|
||||
background: var(--bp-bg);
|
||||
}
|
||||
167
backend/frontend/static/css/modules/components/editor.css
Normal file
167
backend/frontend/static/css/modules/components/editor.css
Normal file
@@ -0,0 +1,167 @@
|
||||
/* ==========================================
|
||||
RICH TEXT EDITOR STYLES
|
||||
WYSIWYG Editor Components
|
||||
========================================== */
|
||||
|
||||
.editor-container {
|
||||
border: 1px solid var(--bp-border);
|
||||
border-radius: 8px;
|
||||
background: var(--bp-surface-elevated);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.editor-toolbar {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 4px;
|
||||
padding: 8px 12px;
|
||||
border-bottom: 1px solid var(--bp-border);
|
||||
background: var(--bp-surface);
|
||||
}
|
||||
|
||||
.editor-toolbar-group {
|
||||
display: flex;
|
||||
gap: 2px;
|
||||
padding-right: 8px;
|
||||
margin-right: 8px;
|
||||
border-right: 1px solid var(--bp-border);
|
||||
}
|
||||
|
||||
.editor-toolbar-group:last-child {
|
||||
border-right: none;
|
||||
margin-right: 0;
|
||||
padding-right: 0;
|
||||
}
|
||||
|
||||
.editor-btn {
|
||||
padding: 6px 10px;
|
||||
border: none;
|
||||
background: transparent;
|
||||
color: var(--bp-text);
|
||||
cursor: pointer;
|
||||
border-radius: 4px;
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
transition: all 0.2s ease;
|
||||
min-width: 32px;
|
||||
}
|
||||
|
||||
.editor-btn:hover {
|
||||
background: var(--bp-border-subtle);
|
||||
}
|
||||
|
||||
.editor-btn.active {
|
||||
background: var(--bp-primary);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.editor-btn-upload {
|
||||
background: var(--bp-primary);
|
||||
color: white;
|
||||
padding: 6px 12px;
|
||||
}
|
||||
|
||||
.editor-btn-upload:hover {
|
||||
filter: brightness(1.1);
|
||||
}
|
||||
|
||||
.editor-content {
|
||||
min-height: 300px;
|
||||
max-height: 500px;
|
||||
overflow-y: auto;
|
||||
padding: 16px;
|
||||
color: var(--bp-text);
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.editor-content:focus {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.editor-content[contenteditable="true"] {
|
||||
cursor: text;
|
||||
}
|
||||
|
||||
.editor-content h1 {
|
||||
font-size: 24px;
|
||||
font-weight: 700;
|
||||
margin: 0 0 16px 0;
|
||||
}
|
||||
|
||||
.editor-content h2 {
|
||||
font-size: 20px;
|
||||
font-weight: 600;
|
||||
margin: 24px 0 12px 0;
|
||||
}
|
||||
|
||||
.editor-content h3 {
|
||||
font-size: 17px;
|
||||
font-weight: 600;
|
||||
margin: 20px 0 10px 0;
|
||||
}
|
||||
|
||||
.editor-content p {
|
||||
margin: 0 0 12px 0;
|
||||
}
|
||||
|
||||
.editor-content ul, .editor-content ol {
|
||||
margin: 0 0 12px 0;
|
||||
padding-left: 24px;
|
||||
}
|
||||
|
||||
.editor-content li {
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.editor-content strong {
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.editor-content em {
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.editor-content a {
|
||||
color: var(--bp-primary);
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.editor-content blockquote {
|
||||
border-left: 3px solid var(--bp-primary);
|
||||
margin: 16px 0;
|
||||
padding: 8px 16px;
|
||||
background: var(--bp-surface);
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.editor-content hr {
|
||||
border: none;
|
||||
border-top: 1px solid var(--bp-border);
|
||||
margin: 20px 0;
|
||||
}
|
||||
|
||||
.word-upload-input {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.editor-status {
|
||||
padding: 8px 12px;
|
||||
font-size: 12px;
|
||||
color: var(--bp-text-muted);
|
||||
border-top: 1px solid var(--bp-border);
|
||||
background: var(--bp-surface);
|
||||
}
|
||||
|
||||
/* Light Mode Overrides */
|
||||
[data-theme="light"] .editor-container {
|
||||
border-color: var(--bp-border);
|
||||
}
|
||||
|
||||
[data-theme="light"] .editor-toolbar {
|
||||
background: var(--bp-bg);
|
||||
border-color: var(--bp-border);
|
||||
}
|
||||
|
||||
[data-theme="light"] .editor-content {
|
||||
background: var(--bp-surface);
|
||||
}
|
||||
205
backend/frontend/static/css/modules/components/legal-modal.css
Normal file
205
backend/frontend/static/css/modules/components/legal-modal.css
Normal file
@@ -0,0 +1,205 @@
|
||||
/* ==========================================
|
||||
LEGAL MODAL STYLES
|
||||
Privacy, Terms, Cookie Settings
|
||||
========================================== */
|
||||
|
||||
.legal-modal {
|
||||
display: none;
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: rgba(0, 0, 0, 0.7);
|
||||
z-index: 10000;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
backdrop-filter: blur(4px);
|
||||
}
|
||||
|
||||
.legal-modal.active {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.legal-modal-content {
|
||||
background: var(--bp-surface);
|
||||
border-radius: 16px;
|
||||
width: 90%;
|
||||
max-width: 700px;
|
||||
max-height: 80vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
box-shadow: 0 20px 60px rgba(0,0,0,0.4);
|
||||
border: 1px solid var(--bp-border);
|
||||
}
|
||||
|
||||
.legal-modal-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 20px 24px;
|
||||
border-bottom: 1px solid var(--bp-border);
|
||||
}
|
||||
|
||||
.legal-modal-header h2 {
|
||||
margin: 0;
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.legal-modal-close {
|
||||
background: none;
|
||||
border: none;
|
||||
font-size: 28px;
|
||||
cursor: pointer;
|
||||
color: var(--bp-text-muted);
|
||||
padding: 0;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.legal-modal-close:hover {
|
||||
color: var(--bp-text);
|
||||
}
|
||||
|
||||
.legal-tabs {
|
||||
display: flex;
|
||||
gap: 4px;
|
||||
padding: 12px 24px;
|
||||
border-bottom: 1px solid var(--bp-border);
|
||||
background: var(--bp-surface-elevated);
|
||||
}
|
||||
|
||||
.legal-tab {
|
||||
padding: 8px 16px;
|
||||
border: none;
|
||||
background: transparent;
|
||||
color: var(--bp-text-muted);
|
||||
cursor: pointer;
|
||||
border-radius: 8px;
|
||||
font-size: 13px;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.legal-tab:hover {
|
||||
background: var(--bp-border-subtle);
|
||||
color: var(--bp-text);
|
||||
}
|
||||
|
||||
.legal-tab.active {
|
||||
background: var(--bp-primary);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.legal-body {
|
||||
padding: 24px;
|
||||
overflow-y: auto;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.legal-content {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.legal-content.active {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.legal-content h3 {
|
||||
margin-top: 0;
|
||||
color: var(--bp-text);
|
||||
}
|
||||
|
||||
.legal-content p {
|
||||
line-height: 1.6;
|
||||
color: var(--bp-text-muted);
|
||||
}
|
||||
|
||||
.legal-content ul {
|
||||
color: var(--bp-text-muted);
|
||||
line-height: 1.8;
|
||||
}
|
||||
|
||||
/* Cookie Categories */
|
||||
.cookie-categories {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
margin-top: 16px;
|
||||
}
|
||||
|
||||
.cookie-category {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 12px;
|
||||
padding: 12px;
|
||||
background: var(--bp-surface-elevated);
|
||||
border-radius: 8px;
|
||||
border: 1px solid var(--bp-border);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.cookie-category input {
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
.cookie-category span {
|
||||
flex: 1;
|
||||
font-size: 13px;
|
||||
color: var(--bp-text);
|
||||
}
|
||||
|
||||
/* GDPR Actions */
|
||||
.gdpr-actions {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
margin-top: 16px;
|
||||
}
|
||||
|
||||
.gdpr-action {
|
||||
padding: 16px;
|
||||
background: var(--bp-surface-elevated);
|
||||
border-radius: 12px;
|
||||
border: 1px solid var(--bp-border);
|
||||
}
|
||||
|
||||
.gdpr-action h4 {
|
||||
margin: 0 0 8px 0;
|
||||
font-size: 14px;
|
||||
color: var(--bp-text);
|
||||
}
|
||||
|
||||
.gdpr-action p {
|
||||
margin: 0 0 12px 0;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.btn-danger {
|
||||
background: var(--bp-danger) !important;
|
||||
border-color: var(--bp-danger) !important;
|
||||
}
|
||||
|
||||
.btn-danger:hover {
|
||||
filter: brightness(1.1);
|
||||
}
|
||||
|
||||
/* Light Mode Overrides */
|
||||
[data-theme="light"] .legal-modal-content {
|
||||
background: var(--bp-surface);
|
||||
border-color: var(--bp-border);
|
||||
}
|
||||
|
||||
[data-theme="light"] .legal-tabs {
|
||||
background: var(--bp-bg);
|
||||
}
|
||||
|
||||
[data-theme="light"] .legal-tab.active {
|
||||
background: var(--bp-primary);
|
||||
color: white;
|
||||
}
|
||||
|
||||
[data-theme="light"] .cookie-category,
|
||||
[data-theme="light"] .gdpr-action {
|
||||
background: var(--bp-bg);
|
||||
border-color: var(--bp-border);
|
||||
}
|
||||
315
backend/frontend/static/css/modules/components/notifications.css
Normal file
315
backend/frontend/static/css/modules/components/notifications.css
Normal file
@@ -0,0 +1,315 @@
|
||||
/* ==========================================
|
||||
NOTIFICATION STYLES
|
||||
Bell, Panel, Items, Preferences
|
||||
========================================== */
|
||||
|
||||
.notification-bell {
|
||||
position: relative;
|
||||
display: none;
|
||||
}
|
||||
|
||||
.notification-bell.active {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.notification-bell-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
background: var(--bp-surface-elevated);
|
||||
border: 1px solid var(--bp-border);
|
||||
border-radius: 8px;
|
||||
color: var(--bp-text-muted);
|
||||
cursor: pointer;
|
||||
font-size: 18px;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.notification-bell-btn:hover {
|
||||
border-color: var(--bp-primary);
|
||||
color: var(--bp-primary);
|
||||
}
|
||||
|
||||
.notification-badge {
|
||||
position: absolute;
|
||||
top: 2px;
|
||||
right: 2px;
|
||||
min-width: 18px;
|
||||
height: 18px;
|
||||
background: var(--bp-danger);
|
||||
color: white;
|
||||
border-radius: 999px;
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 0 5px;
|
||||
}
|
||||
|
||||
.notification-badge.hidden {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* Notification Panel */
|
||||
.notification-panel {
|
||||
display: none;
|
||||
position: absolute;
|
||||
top: 100%;
|
||||
right: 0;
|
||||
margin-top: 8px;
|
||||
width: 360px;
|
||||
max-height: 480px;
|
||||
background: var(--bp-surface);
|
||||
border: 1px solid var(--bp-border);
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 10px 40px rgba(0,0,0,0.3);
|
||||
z-index: 1000;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.notification-panel.active {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.notification-panel-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 16px;
|
||||
border-bottom: 1px solid var(--bp-border);
|
||||
}
|
||||
|
||||
.notification-panel-title {
|
||||
font-weight: 600;
|
||||
font-size: 16px;
|
||||
color: var(--bp-text);
|
||||
}
|
||||
|
||||
.notification-panel-actions {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.notification-panel-action {
|
||||
padding: 4px 8px;
|
||||
background: none;
|
||||
border: 1px solid var(--bp-border);
|
||||
border-radius: 6px;
|
||||
color: var(--bp-text-muted);
|
||||
cursor: pointer;
|
||||
font-size: 11px;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.notification-panel-action:hover {
|
||||
border-color: var(--bp-primary);
|
||||
color: var(--bp-primary);
|
||||
}
|
||||
|
||||
/* Notification List */
|
||||
.notification-list {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
max-height: 380px;
|
||||
}
|
||||
|
||||
.notification-item {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
padding: 14px 16px;
|
||||
border-bottom: 1px solid var(--bp-border);
|
||||
cursor: pointer;
|
||||
transition: background 0.2s ease;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.notification-item:hover {
|
||||
background: var(--bp-surface-elevated);
|
||||
}
|
||||
|
||||
.notification-item.unread {
|
||||
background: rgba(15, 118, 110, 0.08);
|
||||
}
|
||||
|
||||
.notification-item.unread::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
width: 3px;
|
||||
background: var(--bp-primary);
|
||||
}
|
||||
|
||||
.notification-icon {
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
border-radius: 50%;
|
||||
background: var(--bp-primary-soft);
|
||||
color: var(--bp-primary);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 16px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.notification-content {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.notification-title {
|
||||
font-weight: 500;
|
||||
font-size: 13px;
|
||||
color: var(--bp-text);
|
||||
margin-bottom: 4px;
|
||||
line-height: 1.3;
|
||||
}
|
||||
|
||||
.notification-body {
|
||||
font-size: 12px;
|
||||
color: var(--bp-text-muted);
|
||||
line-height: 1.4;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 2;
|
||||
-webkit-box-orient: vertical;
|
||||
}
|
||||
|
||||
.notification-time {
|
||||
font-size: 11px;
|
||||
color: var(--bp-text-muted);
|
||||
margin-top: 6px;
|
||||
}
|
||||
|
||||
.notification-empty {
|
||||
padding: 40px 20px;
|
||||
text-align: center;
|
||||
color: var(--bp-text-muted);
|
||||
}
|
||||
|
||||
.notification-empty-icon {
|
||||
font-size: 36px;
|
||||
margin-bottom: 12px;
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.notification-footer {
|
||||
padding: 12px 16px;
|
||||
border-top: 1px solid var(--bp-border);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.notification-footer-btn {
|
||||
background: none;
|
||||
border: none;
|
||||
color: var(--bp-primary);
|
||||
cursor: pointer;
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.notification-footer-btn:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
/* Notification Preferences Modal */
|
||||
.notification-prefs-modal {
|
||||
display: none;
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: rgba(0, 0, 0, 0.7);
|
||||
z-index: 10001;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
backdrop-filter: blur(4px);
|
||||
}
|
||||
|
||||
.notification-prefs-modal.active {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.notification-prefs-content {
|
||||
background: var(--bp-surface);
|
||||
border-radius: 16px;
|
||||
width: 90%;
|
||||
max-width: 420px;
|
||||
box-shadow: 0 20px 60px rgba(0,0,0,0.4);
|
||||
border: 1px solid var(--bp-border);
|
||||
}
|
||||
|
||||
.notification-pref-item {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 14px 0;
|
||||
border-bottom: 1px solid var(--bp-border);
|
||||
}
|
||||
|
||||
.notification-pref-item:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.notification-pref-label {
|
||||
font-size: 14px;
|
||||
color: var(--bp-text);
|
||||
}
|
||||
|
||||
.notification-pref-desc {
|
||||
font-size: 12px;
|
||||
color: var(--bp-text-muted);
|
||||
margin-top: 2px;
|
||||
}
|
||||
|
||||
/* Toggle Switch */
|
||||
.toggle-switch {
|
||||
position: relative;
|
||||
width: 48px;
|
||||
height: 26px;
|
||||
background: var(--bp-border);
|
||||
border-radius: 999px;
|
||||
cursor: pointer;
|
||||
transition: background 0.3s ease;
|
||||
}
|
||||
|
||||
.toggle-switch.active {
|
||||
background: var(--bp-primary);
|
||||
}
|
||||
|
||||
.toggle-switch-handle {
|
||||
position: absolute;
|
||||
top: 3px;
|
||||
left: 3px;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
background: white;
|
||||
border-radius: 50%;
|
||||
transition: transform 0.3s ease;
|
||||
}
|
||||
|
||||
.toggle-switch.active .toggle-switch-handle {
|
||||
transform: translateX(22px);
|
||||
}
|
||||
|
||||
/* Light Mode Overrides */
|
||||
[data-theme="light"] .notification-panel {
|
||||
background: var(--bp-surface);
|
||||
border-color: var(--bp-border);
|
||||
}
|
||||
|
||||
[data-theme="light"] .notification-item.unread {
|
||||
background: rgba(14, 165, 233, 0.05);
|
||||
}
|
||||
|
||||
[data-theme="light"] .notification-item.unread::before {
|
||||
background: var(--bp-primary);
|
||||
}
|
||||
124
backend/frontend/static/css/modules/components/suspension.css
Normal file
124
backend/frontend/static/css/modules/components/suspension.css
Normal file
@@ -0,0 +1,124 @@
|
||||
/* ==========================================
|
||||
SUSPENSION OVERLAY STYLES
|
||||
Account suspension/blocking display
|
||||
========================================== */
|
||||
|
||||
.suspension-overlay {
|
||||
display: none;
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: rgba(0, 0, 0, 0.9);
|
||||
z-index: 20000;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
backdrop-filter: blur(8px);
|
||||
}
|
||||
|
||||
.suspension-overlay.active {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.suspension-content {
|
||||
background: var(--bp-surface);
|
||||
border-radius: 20px;
|
||||
width: 90%;
|
||||
max-width: 500px;
|
||||
padding: 40px;
|
||||
text-align: center;
|
||||
box-shadow: 0 20px 60px rgba(0,0,0,0.5);
|
||||
border: 1px solid var(--bp-danger);
|
||||
}
|
||||
|
||||
.suspension-icon {
|
||||
font-size: 64px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.suspension-title {
|
||||
font-size: 24px;
|
||||
font-weight: 700;
|
||||
color: var(--bp-danger);
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.suspension-message {
|
||||
font-size: 15px;
|
||||
color: var(--bp-text-muted);
|
||||
line-height: 1.6;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.suspension-reason {
|
||||
background: rgba(239, 68, 68, 0.1);
|
||||
border: 1px solid rgba(239, 68, 68, 0.3);
|
||||
border-radius: 12px;
|
||||
padding: 16px;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.suspension-reason-label {
|
||||
font-size: 12px;
|
||||
color: var(--bp-text-muted);
|
||||
margin-bottom: 8px;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
.suspension-reason-text {
|
||||
font-size: 14px;
|
||||
color: var(--bp-text);
|
||||
}
|
||||
|
||||
.suspension-actions {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.suspension-btn {
|
||||
padding: 14px 24px;
|
||||
border-radius: 10px;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
border: none;
|
||||
}
|
||||
|
||||
.suspension-btn-primary {
|
||||
background: var(--bp-btn-primary-bg);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.suspension-btn-primary:hover {
|
||||
background: var(--bp-btn-primary-hover);
|
||||
}
|
||||
|
||||
.suspension-btn-secondary {
|
||||
background: transparent;
|
||||
border: 1px solid var(--bp-border);
|
||||
color: var(--bp-text);
|
||||
}
|
||||
|
||||
.suspension-btn-secondary:hover {
|
||||
border-color: var(--bp-primary);
|
||||
color: var(--bp-primary);
|
||||
}
|
||||
|
||||
.suspension-countdown {
|
||||
font-size: 12px;
|
||||
color: var(--bp-text-muted);
|
||||
margin-top: 16px;
|
||||
}
|
||||
|
||||
/* Light Mode Overrides */
|
||||
[data-theme="light"] .suspension-content {
|
||||
background: var(--bp-surface);
|
||||
}
|
||||
|
||||
[data-theme="light"] .suspension-reason {
|
||||
background: rgba(239, 68, 68, 0.05);
|
||||
}
|
||||
52
backend/frontend/static/css/studio.css
Normal file
52
backend/frontend/static/css/studio.css
Normal file
@@ -0,0 +1,52 @@
|
||||
/* ==========================================
|
||||
BreakPilot Studio - Main CSS Entry Point
|
||||
Modular CSS Architecture
|
||||
========================================== */
|
||||
|
||||
/* Base Styles */
|
||||
@import url('./modules/base/variables.css');
|
||||
@import url('./modules/base/layout.css');
|
||||
|
||||
/* UI Components */
|
||||
@import url('./modules/components/legal-modal.css');
|
||||
@import url('./modules/components/auth-modal.css');
|
||||
@import url('./modules/components/notifications.css');
|
||||
@import url('./modules/components/suspension.css');
|
||||
@import url('./modules/components/editor.css');
|
||||
@import url('./modules/components/communication.css');
|
||||
|
||||
/* Admin Panel */
|
||||
@import url('./modules/admin/modal.css');
|
||||
@import url('./modules/admin/tables.css');
|
||||
@import url('./modules/admin/sidebar.css');
|
||||
@import url('./modules/admin/content.css');
|
||||
@import url('./modules/admin/dsms.css');
|
||||
@import url('./modules/admin/preview.css');
|
||||
@import url('./modules/admin/learning.css');
|
||||
|
||||
/* ==========================================
|
||||
Module Architecture (Total: 3,710 → 15 modules)
|
||||
|
||||
base/
|
||||
├── variables.css (~65 lines) - CSS Custom Properties
|
||||
└── layout.css (~145 lines) - App structure, topbar
|
||||
|
||||
components/
|
||||
├── legal-modal.css (~170 lines) - Privacy, terms, cookies
|
||||
├── auth-modal.css (~225 lines) - Login, register
|
||||
├── notifications.css (~225 lines) - Bell, panel, items
|
||||
├── suspension.css (~115 lines) - Account blocking
|
||||
├── editor.css (~140 lines) - Rich text editor
|
||||
└── communication.css (~120 lines) - Chat, Matrix, Jitsi
|
||||
|
||||
admin/
|
||||
├── modal.css (~100 lines) - Admin modal structure
|
||||
├── tables.css (~100 lines) - Tables and badges
|
||||
├── sidebar.css (~240 lines) - Navigation sidebar
|
||||
├── content.css (~260 lines) - Content area, buttons
|
||||
├── dsms.css (~160 lines) - Storage management
|
||||
├── preview.css (~175 lines) - Preview/compare
|
||||
└── learning.css (~285 lines) - MC, cloze quizzes
|
||||
|
||||
Original file backed up as: studio_original.css
|
||||
========================================== */
|
||||
3711
backend/frontend/static/css/studio_original.css
Normal file
3711
backend/frontend/static/css/studio_original.css
Normal file
File diff suppressed because it is too large
Load Diff
632
backend/frontend/static/js/customer.js
Normal file
632
backend/frontend/static/js/customer.js
Normal file
@@ -0,0 +1,632 @@
|
||||
/**
|
||||
* BreakPilot Customer Portal - Slim JavaScript
|
||||
*
|
||||
* Features:
|
||||
* - Login/Register
|
||||
* - My Consents View
|
||||
* - Data Export Request
|
||||
* - Legal Documents
|
||||
* - Theme Toggle
|
||||
*/
|
||||
|
||||
// API Base URLs
|
||||
const CONSENT_SERVICE_URL = 'http://localhost:8081';
|
||||
const BACKEND_URL = '';
|
||||
|
||||
// State
|
||||
let currentUser = null;
|
||||
let authToken = localStorage.getItem('bp_token');
|
||||
|
||||
// Initialize on DOM ready
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
initTheme();
|
||||
initEventListeners();
|
||||
checkAuth();
|
||||
});
|
||||
|
||||
// ============================================================
|
||||
// Theme
|
||||
// ============================================================
|
||||
|
||||
function initTheme() {
|
||||
const saved = localStorage.getItem('bp_theme') || 'light';
|
||||
document.body.setAttribute('data-theme', saved);
|
||||
updateThemeIcon(saved);
|
||||
}
|
||||
|
||||
function toggleTheme() {
|
||||
const current = document.body.getAttribute('data-theme');
|
||||
const next = current === 'dark' ? 'light' : 'dark';
|
||||
document.body.setAttribute('data-theme', next);
|
||||
localStorage.setItem('bp_theme', next);
|
||||
updateThemeIcon(next);
|
||||
}
|
||||
|
||||
function updateThemeIcon(theme) {
|
||||
const icon = document.getElementById('theme-icon');
|
||||
if (icon) icon.textContent = theme === 'dark' ? '☀️' : '🌙';
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// Event Listeners
|
||||
// ============================================================
|
||||
|
||||
function initEventListeners() {
|
||||
// Theme toggle
|
||||
const themeBtn = document.getElementById('btn-theme');
|
||||
if (themeBtn) themeBtn.addEventListener('click', toggleTheme);
|
||||
|
||||
// Login button
|
||||
const loginBtn = document.getElementById('btn-login');
|
||||
if (loginBtn) loginBtn.addEventListener('click', showLoginModal);
|
||||
|
||||
// Legal button
|
||||
const legalBtn = document.getElementById('btn-legal');
|
||||
if (legalBtn) legalBtn.addEventListener('click', () => showLegalDocument('privacy'));
|
||||
|
||||
// User menu toggle
|
||||
const userMenuBtn = document.getElementById('user-menu-btn');
|
||||
if (userMenuBtn) {
|
||||
userMenuBtn.addEventListener('click', () => {
|
||||
document.getElementById('user-menu').classList.toggle('open');
|
||||
});
|
||||
}
|
||||
|
||||
// Close user menu on outside click
|
||||
document.addEventListener('click', (e) => {
|
||||
const userMenu = document.getElementById('user-menu');
|
||||
if (userMenu && !userMenu.contains(e.target)) {
|
||||
userMenu.classList.remove('open');
|
||||
}
|
||||
});
|
||||
|
||||
// Login form
|
||||
const loginForm = document.getElementById('login-form');
|
||||
if (loginForm) loginForm.addEventListener('submit', handleLogin);
|
||||
|
||||
// Register form
|
||||
const registerForm = document.getElementById('register-form');
|
||||
if (registerForm) registerForm.addEventListener('submit', handleRegister);
|
||||
|
||||
// Forgot password form
|
||||
const forgotForm = document.getElementById('forgot-form');
|
||||
if (forgotForm) forgotForm.addEventListener('submit', handleForgotPassword);
|
||||
|
||||
// Profile form
|
||||
const profileForm = document.getElementById('profile-form');
|
||||
if (profileForm) profileForm.addEventListener('submit', handleProfileUpdate);
|
||||
|
||||
// Password form
|
||||
const passwordForm = document.getElementById('password-form');
|
||||
if (passwordForm) passwordForm.addEventListener('submit', handlePasswordChange);
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// Authentication
|
||||
// ============================================================
|
||||
|
||||
async function checkAuth() {
|
||||
if (!authToken) {
|
||||
showLoggedOutState();
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const res = await fetch(`${CONSENT_SERVICE_URL}/api/v1/auth/me`, {
|
||||
headers: { 'Authorization': `Bearer ${authToken}` }
|
||||
});
|
||||
|
||||
if (res.ok) {
|
||||
currentUser = await res.json();
|
||||
showLoggedInState();
|
||||
} else {
|
||||
localStorage.removeItem('bp_token');
|
||||
authToken = null;
|
||||
showLoggedOutState();
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Auth check failed:', err);
|
||||
showLoggedOutState();
|
||||
}
|
||||
}
|
||||
|
||||
function showLoggedInState() {
|
||||
document.getElementById('btn-login')?.classList.add('hidden');
|
||||
document.getElementById('user-menu')?.classList.remove('hidden');
|
||||
document.getElementById('welcome-section')?.classList.add('hidden');
|
||||
document.getElementById('dashboard')?.classList.remove('hidden');
|
||||
|
||||
if (currentUser) {
|
||||
const initials = getInitials(currentUser.name || currentUser.email);
|
||||
document.querySelectorAll('.user-avatar').forEach(el => el.textContent = initials);
|
||||
document.getElementById('user-name').textContent = currentUser.name || 'Benutzer';
|
||||
document.getElementById('dropdown-name').textContent = currentUser.name || 'Benutzer';
|
||||
document.getElementById('dropdown-email').textContent = currentUser.email;
|
||||
}
|
||||
|
||||
loadConsentCount();
|
||||
}
|
||||
|
||||
function showLoggedOutState() {
|
||||
document.getElementById('btn-login')?.classList.remove('hidden');
|
||||
document.getElementById('user-menu')?.classList.add('hidden');
|
||||
document.getElementById('welcome-section')?.classList.remove('hidden');
|
||||
document.getElementById('dashboard')?.classList.add('hidden');
|
||||
}
|
||||
|
||||
function getInitials(name) {
|
||||
if (!name) return 'BP';
|
||||
const parts = name.split(' ').filter(p => p.length > 0);
|
||||
if (parts.length >= 2) {
|
||||
return (parts[0][0] + parts[parts.length - 1][0]).toUpperCase();
|
||||
}
|
||||
return name.substring(0, 2).toUpperCase();
|
||||
}
|
||||
|
||||
async function handleLogin(e) {
|
||||
e.preventDefault();
|
||||
const email = document.getElementById('login-email').value;
|
||||
const password = document.getElementById('login-password').value;
|
||||
const messageEl = document.getElementById('login-message');
|
||||
|
||||
try {
|
||||
const res = await fetch(`${CONSENT_SERVICE_URL}/api/v1/auth/login`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ email, password })
|
||||
});
|
||||
|
||||
const data = await res.json();
|
||||
|
||||
if (res.ok) {
|
||||
authToken = data.token;
|
||||
localStorage.setItem('bp_token', authToken);
|
||||
currentUser = data.user;
|
||||
showMessage(messageEl, 'Erfolgreich angemeldet!', 'success');
|
||||
setTimeout(() => {
|
||||
closeAllModals();
|
||||
showLoggedInState();
|
||||
}, 500);
|
||||
} else {
|
||||
showMessage(messageEl, data.error || 'Anmeldung fehlgeschlagen', 'error');
|
||||
}
|
||||
} catch (err) {
|
||||
showMessage(messageEl, 'Verbindungsfehler', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
async function handleRegister(e) {
|
||||
e.preventDefault();
|
||||
const name = document.getElementById('register-name').value;
|
||||
const email = document.getElementById('register-email').value;
|
||||
const password = document.getElementById('register-password').value;
|
||||
const passwordConfirm = document.getElementById('register-password-confirm').value;
|
||||
const messageEl = document.getElementById('register-message');
|
||||
|
||||
if (password !== passwordConfirm) {
|
||||
showMessage(messageEl, 'Passwörter stimmen nicht überein', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const res = await fetch(`${CONSENT_SERVICE_URL}/api/v1/auth/register`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ name, email, password })
|
||||
});
|
||||
|
||||
const data = await res.json();
|
||||
|
||||
if (res.ok) {
|
||||
showMessage(messageEl, 'Registrierung erfolgreich! Sie können sich jetzt anmelden.', 'success');
|
||||
setTimeout(() => switchAuthTab('login'), 1500);
|
||||
} else {
|
||||
showMessage(messageEl, data.error || 'Registrierung fehlgeschlagen', 'error');
|
||||
}
|
||||
} catch (err) {
|
||||
showMessage(messageEl, 'Verbindungsfehler', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
async function handleForgotPassword(e) {
|
||||
e.preventDefault();
|
||||
const email = document.getElementById('forgot-email').value;
|
||||
const messageEl = document.getElementById('forgot-message');
|
||||
|
||||
try {
|
||||
const res = await fetch(`${CONSENT_SERVICE_URL}/api/v1/auth/request-password-reset`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ email })
|
||||
});
|
||||
|
||||
showMessage(messageEl, 'Wenn ein Konto mit dieser E-Mail existiert, erhalten Sie einen Link zum Zurücksetzen.', 'success');
|
||||
} catch (err) {
|
||||
showMessage(messageEl, 'Verbindungsfehler', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
function logout() {
|
||||
localStorage.removeItem('bp_token');
|
||||
authToken = null;
|
||||
currentUser = null;
|
||||
showLoggedOutState();
|
||||
document.getElementById('user-menu')?.classList.remove('open');
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// Modals
|
||||
// ============================================================
|
||||
|
||||
function showLoginModal() {
|
||||
document.getElementById('login-modal')?.classList.add('active');
|
||||
switchAuthTab('login');
|
||||
}
|
||||
|
||||
function showMyConsents() {
|
||||
document.getElementById('consents-modal')?.classList.add('active');
|
||||
document.getElementById('user-menu')?.classList.remove('open');
|
||||
loadMyConsents();
|
||||
}
|
||||
|
||||
function showDataExport() {
|
||||
document.getElementById('export-modal')?.classList.add('active');
|
||||
document.getElementById('user-menu')?.classList.remove('open');
|
||||
loadExportRequests();
|
||||
}
|
||||
|
||||
function showAccountSettings() {
|
||||
document.getElementById('settings-modal')?.classList.add('active');
|
||||
document.getElementById('user-menu')?.classList.remove('open');
|
||||
|
||||
if (currentUser) {
|
||||
document.getElementById('profile-name').value = currentUser.name || '';
|
||||
document.getElementById('profile-email').value = currentUser.email || '';
|
||||
}
|
||||
}
|
||||
|
||||
function showLegalDocument(type) {
|
||||
const modal = document.getElementById('legal-modal');
|
||||
const titleEl = document.getElementById('legal-title');
|
||||
const loadingEl = document.getElementById('legal-loading');
|
||||
const contentEl = document.getElementById('legal-content');
|
||||
|
||||
const titles = {
|
||||
privacy: 'Datenschutzerklärung',
|
||||
terms: 'Allgemeine Geschäftsbedingungen',
|
||||
imprint: 'Impressum',
|
||||
cookies: 'Cookie-Richtlinie'
|
||||
};
|
||||
|
||||
titleEl.textContent = titles[type] || 'Dokument';
|
||||
loadingEl.style.display = 'block';
|
||||
contentEl.innerHTML = '';
|
||||
modal?.classList.add('active');
|
||||
|
||||
loadLegalDocument(type).then(html => {
|
||||
loadingEl.style.display = 'none';
|
||||
contentEl.innerHTML = html;
|
||||
}).catch(() => {
|
||||
loadingEl.style.display = 'none';
|
||||
contentEl.innerHTML = '<p>Dokument konnte nicht geladen werden.</p>';
|
||||
});
|
||||
}
|
||||
|
||||
function showForgotPassword() {
|
||||
document.getElementById('login-form')?.classList.add('hidden');
|
||||
document.getElementById('register-form')?.classList.add('hidden');
|
||||
document.getElementById('forgot-form')?.classList.remove('hidden');
|
||||
document.querySelectorAll('.auth-tab').forEach(t => t.classList.remove('active'));
|
||||
}
|
||||
|
||||
function closeAllModals() {
|
||||
document.querySelectorAll('.modal.active').forEach(m => m.classList.remove('active'));
|
||||
}
|
||||
|
||||
function switchAuthTab(tab) {
|
||||
document.querySelectorAll('.auth-tab').forEach(t => {
|
||||
t.classList.toggle('active', t.dataset.tab === tab);
|
||||
});
|
||||
document.getElementById('login-form')?.classList.toggle('hidden', tab !== 'login');
|
||||
document.getElementById('register-form')?.classList.toggle('hidden', tab !== 'register');
|
||||
document.getElementById('forgot-form')?.classList.add('hidden');
|
||||
|
||||
// Clear messages
|
||||
document.querySelectorAll('.form-message').forEach(m => {
|
||||
m.className = 'form-message';
|
||||
m.textContent = '';
|
||||
});
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// Consents
|
||||
// ============================================================
|
||||
|
||||
async function loadConsentCount() {
|
||||
if (!authToken) return;
|
||||
|
||||
try {
|
||||
const res = await fetch(`${CONSENT_SERVICE_URL}/api/v1/consents/my`, {
|
||||
headers: { 'Authorization': `Bearer ${authToken}` }
|
||||
});
|
||||
|
||||
if (res.ok) {
|
||||
const data = await res.json();
|
||||
const count = data.consents?.length || 0;
|
||||
const badge = document.getElementById('consent-count');
|
||||
if (badge) badge.textContent = `${count} aktiv`;
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Failed to load consent count:', err);
|
||||
}
|
||||
}
|
||||
|
||||
async function loadMyConsents() {
|
||||
const loadingEl = document.getElementById('consents-loading');
|
||||
const listEl = document.getElementById('consents-list');
|
||||
const emptyEl = document.getElementById('consents-empty');
|
||||
|
||||
loadingEl.style.display = 'block';
|
||||
listEl.innerHTML = '';
|
||||
emptyEl?.classList.add('hidden');
|
||||
|
||||
try {
|
||||
const res = await fetch(`${CONSENT_SERVICE_URL}/api/v1/consents/my`, {
|
||||
headers: { 'Authorization': `Bearer ${authToken}` }
|
||||
});
|
||||
|
||||
if (res.ok) {
|
||||
const data = await res.json();
|
||||
const consents = data.consents || [];
|
||||
|
||||
loadingEl.style.display = 'none';
|
||||
|
||||
if (consents.length === 0) {
|
||||
emptyEl?.classList.remove('hidden');
|
||||
return;
|
||||
}
|
||||
|
||||
listEl.innerHTML = consents.map(c => `
|
||||
<div class="consent-item">
|
||||
<div class="consent-info">
|
||||
<h3>${escapeHtml(c.document_name || c.document_type)}</h3>
|
||||
<p>Version ${c.version || '1.0'}</p>
|
||||
</div>
|
||||
<div class="consent-date">
|
||||
Zugestimmt am ${formatDate(c.created_at)}
|
||||
</div>
|
||||
</div>
|
||||
`).join('');
|
||||
} else {
|
||||
loadingEl.style.display = 'none';
|
||||
listEl.innerHTML = '<p>Fehler beim Laden der Zustimmungen.</p>';
|
||||
}
|
||||
} catch (err) {
|
||||
loadingEl.style.display = 'none';
|
||||
listEl.innerHTML = '<p>Verbindungsfehler.</p>';
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// Data Export (GDPR)
|
||||
// ============================================================
|
||||
|
||||
async function requestDataExport() {
|
||||
const messageEl = document.getElementById('export-message');
|
||||
const btn = document.getElementById('btn-request-export');
|
||||
|
||||
btn.disabled = true;
|
||||
|
||||
try {
|
||||
const res = await fetch(`${BACKEND_URL}/api/gdpr/request-export`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Authorization': `Bearer ${authToken}`,
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
});
|
||||
|
||||
if (res.ok) {
|
||||
showMessage(messageEl, 'Ihre Anfrage wurde erfolgreich eingereicht. Sie erhalten eine E-Mail, sobald der Export bereit ist.', 'success');
|
||||
loadExportRequests();
|
||||
} else {
|
||||
const data = await res.json();
|
||||
showMessage(messageEl, data.error || 'Anfrage fehlgeschlagen', 'error');
|
||||
}
|
||||
} catch (err) {
|
||||
showMessage(messageEl, 'Verbindungsfehler', 'error');
|
||||
} finally {
|
||||
btn.disabled = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function loadExportRequests() {
|
||||
const statusEl = document.getElementById('export-status');
|
||||
const listEl = document.getElementById('export-requests-list');
|
||||
|
||||
if (!statusEl || !listEl) return;
|
||||
|
||||
try {
|
||||
const res = await fetch(`${BACKEND_URL}/api/gdpr/my-requests`, {
|
||||
headers: { 'Authorization': `Bearer ${authToken}` }
|
||||
});
|
||||
|
||||
if (res.ok) {
|
||||
const data = await res.json();
|
||||
const requests = data.requests || [];
|
||||
|
||||
if (requests.length > 0) {
|
||||
statusEl.classList.remove('hidden');
|
||||
listEl.innerHTML = requests.map(r => `
|
||||
<div class="consent-item">
|
||||
<div class="consent-info">
|
||||
<h3>${r.type === 'export' ? 'Datenexport' : 'Datenlöschung'}</h3>
|
||||
<p>Status: ${r.status}</p>
|
||||
</div>
|
||||
<div class="consent-date">
|
||||
${formatDate(r.created_at)}
|
||||
</div>
|
||||
</div>
|
||||
`).join('');
|
||||
} else {
|
||||
statusEl.classList.add('hidden');
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Failed to load export requests:', err);
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// Legal Documents
|
||||
// ============================================================
|
||||
|
||||
async function loadLegalDocument(type) {
|
||||
try {
|
||||
const res = await fetch(`${CONSENT_SERVICE_URL}/api/v1/documents/type/${type}/published`);
|
||||
|
||||
if (res.ok) {
|
||||
const data = await res.json();
|
||||
return data.content || '<p>Dokument ist leer.</p>';
|
||||
}
|
||||
|
||||
return '<p>Dokument nicht gefunden.</p>';
|
||||
} catch (err) {
|
||||
return '<p>Fehler beim Laden.</p>';
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// Profile & Settings
|
||||
// ============================================================
|
||||
|
||||
async function handleProfileUpdate(e) {
|
||||
e.preventDefault();
|
||||
const name = document.getElementById('profile-name').value;
|
||||
const messageEl = document.getElementById('profile-message');
|
||||
|
||||
try {
|
||||
const res = await fetch(`${CONSENT_SERVICE_URL}/api/v1/auth/update-profile`, {
|
||||
method: 'PUT',
|
||||
headers: {
|
||||
'Authorization': `Bearer ${authToken}`,
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({ name })
|
||||
});
|
||||
|
||||
if (res.ok) {
|
||||
currentUser.name = name;
|
||||
showMessage(messageEl, 'Profil aktualisiert', 'success');
|
||||
showLoggedInState();
|
||||
} else {
|
||||
showMessage(messageEl, 'Aktualisierung fehlgeschlagen', 'error');
|
||||
}
|
||||
} catch (err) {
|
||||
showMessage(messageEl, 'Verbindungsfehler', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
async function handlePasswordChange(e) {
|
||||
e.preventDefault();
|
||||
const currentPassword = document.getElementById('current-password').value;
|
||||
const newPassword = document.getElementById('new-password').value;
|
||||
const confirmPassword = document.getElementById('new-password-confirm').value;
|
||||
const messageEl = document.getElementById('password-message');
|
||||
|
||||
if (newPassword !== confirmPassword) {
|
||||
showMessage(messageEl, 'Passwörter stimmen nicht überein', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const res = await fetch(`${CONSENT_SERVICE_URL}/api/v1/auth/change-password`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Authorization': `Bearer ${authToken}`,
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({
|
||||
current_password: currentPassword,
|
||||
new_password: newPassword
|
||||
})
|
||||
});
|
||||
|
||||
if (res.ok) {
|
||||
showMessage(messageEl, 'Passwort geändert', 'success');
|
||||
document.getElementById('password-form').reset();
|
||||
} else {
|
||||
const data = await res.json();
|
||||
showMessage(messageEl, data.error || 'Änderung fehlgeschlagen', 'error');
|
||||
}
|
||||
} catch (err) {
|
||||
showMessage(messageEl, 'Verbindungsfehler', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
function confirmDeleteAccount() {
|
||||
if (confirm('Sind Sie sicher, dass Sie Ihr Konto löschen möchten? Diese Aktion kann nicht rückgängig gemacht werden.')) {
|
||||
if (confirm('Letzte Warnung: Alle Ihre Daten werden unwiderruflich gelöscht. Fortfahren?')) {
|
||||
deleteAccount();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function deleteAccount() {
|
||||
try {
|
||||
const res = await fetch(`${CONSENT_SERVICE_URL}/api/v1/auth/delete-account`, {
|
||||
method: 'DELETE',
|
||||
headers: { 'Authorization': `Bearer ${authToken}` }
|
||||
});
|
||||
|
||||
if (res.ok) {
|
||||
alert('Ihr Konto wurde gelöscht.');
|
||||
logout();
|
||||
closeAllModals();
|
||||
} else {
|
||||
alert('Kontoloöschung fehlgeschlagen.');
|
||||
}
|
||||
} catch (err) {
|
||||
alert('Verbindungsfehler');
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// Utilities
|
||||
// ============================================================
|
||||
|
||||
function showMessage(el, text, type) {
|
||||
if (!el) return;
|
||||
el.textContent = text;
|
||||
el.className = `form-message ${type}`;
|
||||
}
|
||||
|
||||
function escapeHtml(str) {
|
||||
if (!str) return '';
|
||||
const div = document.createElement('div');
|
||||
div.textContent = str;
|
||||
return div.innerHTML;
|
||||
}
|
||||
|
||||
function formatDate(dateStr) {
|
||||
if (!dateStr) return '-';
|
||||
const date = new Date(dateStr);
|
||||
return date.toLocaleDateString('de-DE', {
|
||||
day: '2-digit',
|
||||
month: '2-digit',
|
||||
year: 'numeric'
|
||||
});
|
||||
}
|
||||
|
||||
// Export for global access
|
||||
window.showLoginModal = showLoginModal;
|
||||
window.showMyConsents = showMyConsents;
|
||||
window.showDataExport = showDataExport;
|
||||
window.showAccountSettings = showAccountSettings;
|
||||
window.showLegalDocument = showLegalDocument;
|
||||
window.showForgotPassword = showForgotPassword;
|
||||
window.closeAllModals = closeAllModals;
|
||||
window.switchAuthTab = switchAuthTab;
|
||||
window.logout = logout;
|
||||
window.requestDataExport = requestDataExport;
|
||||
window.confirmDeleteAccount = confirmDeleteAccount;
|
||||
154
backend/frontend/static/js/modules/README.md
Normal file
154
backend/frontend/static/js/modules/README.md
Normal file
@@ -0,0 +1,154 @@
|
||||
# Studio JavaScript Modules
|
||||
|
||||
Das monolithische studio.js (9.787 Zeilen) wurde in modulare ES6-Module aufgeteilt.
|
||||
|
||||
## Modul-Struktur
|
||||
|
||||
```
|
||||
backend/frontend/static/js/
|
||||
├── studio.js # Original (noch nicht aktualisiert)
|
||||
└── modules/
|
||||
├── theme.js # Dark/Light Mode (105 Zeilen)
|
||||
├── translations.js # Übersetzungen DE/EN (971 Zeilen)
|
||||
├── i18n.js # Internationalisierung (250 Zeilen)
|
||||
├── lightbox.js # Bildvorschau (234 Zeilen)
|
||||
├── api-helpers.js # API-Utilities (360 Zeilen)
|
||||
├── file-manager.js # Dateiverwaltung (614 Zeilen)
|
||||
├── learning-units-module.js # Lerneinheiten (517 Zeilen)
|
||||
├── mc-module.js # Multiple Choice (474 Zeilen)
|
||||
├── cloze-module.js # Lückentext (430 Zeilen)
|
||||
├── mindmap-module.js # Mindmap (223 Zeilen)
|
||||
└── qa-leitner-module.js # Q&A / Leitner (444 Zeilen)
|
||||
```
|
||||
|
||||
## Module-Übersicht
|
||||
|
||||
### theme.js
|
||||
- Dark/Light Mode Toggle
|
||||
- Speichert Präferenz in localStorage
|
||||
- Exports: `getCurrentTheme()`, `setTheme()`, `initThemeToggle()`
|
||||
|
||||
### translations.js
|
||||
- Übersetzungswörterbuch für DE/EN
|
||||
- Export: `translations` Objekt
|
||||
|
||||
### i18n.js
|
||||
- Internationalisierungsfunktionen
|
||||
- Exports: `t()`, `applyLanguage()`, `updateUITexts()`
|
||||
|
||||
### lightbox.js
|
||||
- Bildvorschau-Modal
|
||||
- Exports: `openLightbox()`, `closeLightbox()`
|
||||
|
||||
### api-helpers.js
|
||||
- API-Fetch mit Fehlerbehandlung
|
||||
- Status-Anzeige
|
||||
- Exports: `apiFetch()`, `setStatus()`
|
||||
|
||||
### file-manager.js
|
||||
- Arbeitsblatt-Upload und -Verwaltung
|
||||
- Eingang-Dateien laden
|
||||
- Exports: `loadEingangFiles()`, `renderEingangList()`, usw.
|
||||
|
||||
### learning-units-module.js
|
||||
- Lerneinheiten CRUD
|
||||
- Arbeitsblatt-Zuordnung
|
||||
- Exports: `loadLearningUnits()`, `addUnitFromForm()`, usw.
|
||||
|
||||
### mc-module.js
|
||||
- Multiple Choice Generierung
|
||||
- Quiz-Vorschau und Bewertung
|
||||
- Exports: `generateMcQuestions()`, `renderMcPreview()`, usw.
|
||||
|
||||
### cloze-module.js
|
||||
- Lückentext-Generierung
|
||||
- Interaktive Ausfüllung
|
||||
- Exports: `generateClozeTexts()`, `renderClozePreview()`, usw.
|
||||
|
||||
### mindmap-module.js
|
||||
- Mindmap-Generierung
|
||||
- SVG-Rendering
|
||||
- Exports: `generateMindmap()`, `renderMindmapPreview()`, usw.
|
||||
|
||||
### qa-leitner-module.js
|
||||
- Frage-Antwort-Generierung
|
||||
- Leitner-System Integration
|
||||
- Exports: `generateQaQuestions()`, `renderQaPreview()`, usw.
|
||||
|
||||
## Verwendung
|
||||
|
||||
```javascript
|
||||
// Als ES6 Modul importieren
|
||||
import { getCurrentTheme, setTheme, initThemeToggle } from './modules/theme.js';
|
||||
import { t, applyLanguage } from './modules/i18n.js';
|
||||
import { openLightbox, closeLightbox } from './modules/lightbox.js';
|
||||
// ...
|
||||
|
||||
// Theme initialisieren
|
||||
initThemeToggle();
|
||||
|
||||
// Übersetzung abrufen
|
||||
const label = t('btn_create');
|
||||
```
|
||||
|
||||
## TODO
|
||||
|
||||
Die Haupt-studio.js sollte aktualisiert werden, um diese Module zu importieren:
|
||||
|
||||
```javascript
|
||||
// In studio.js
|
||||
import * as Theme from './modules/theme.js';
|
||||
import * as I18n from './modules/i18n.js';
|
||||
import * as FileManager from './modules/file-manager.js';
|
||||
// ...
|
||||
```
|
||||
|
||||
## Statistiken
|
||||
|
||||
| Komponente | Zeilen |
|
||||
|------------|--------|
|
||||
| theme.js | 105 |
|
||||
| translations.js | 971 |
|
||||
| i18n.js | 250 |
|
||||
| lightbox.js | 234 |
|
||||
| api-helpers.js | 360 |
|
||||
| file-manager.js | 614 |
|
||||
| learning-units-module.js | 517 |
|
||||
| mc-module.js | 474 |
|
||||
| cloze-module.js | 430 |
|
||||
| mindmap-module.js | 223 |
|
||||
| qa-leitner-module.js | 444 |
|
||||
| **Gesamt Module** | **4.622** |
|
||||
| studio.js (Original) | 9.787 |
|
||||
|
||||
## Remaining to Extract (~5,165 lines)
|
||||
|
||||
The following sections remain in studio.js and should be extracted:
|
||||
|
||||
| Section | Lines | Target Module |
|
||||
|---------|-------|---------------|
|
||||
| GDPR Functions | ~150 | gdpr-module.js |
|
||||
| Legal Modal | ~200 | legal-module.js |
|
||||
| Authentication | ~450 | auth-module.js |
|
||||
| Notifications | ~400 | notifications-module.js |
|
||||
| Word Upload | ~140 | upload-module.js |
|
||||
| Admin Documents | ~940 | admin/documents.js |
|
||||
| Cookie Categories Admin | ~130 | admin/cookies.js |
|
||||
| Admin Stats | ~170 | admin/stats.js |
|
||||
| User Data Export | ~55 | admin/export.js |
|
||||
| DSR Management | ~450 | admin/dsr.js |
|
||||
| DSMS Functions | ~520 | dsms-module.js |
|
||||
| Email Templates | ~400 | admin/email-templates.js |
|
||||
| Communication Panel | ~2,140 | communication-module.js |
|
||||
|
||||
## Refactoring-Historie
|
||||
|
||||
**03.02.2026**: Refactoring status documented
|
||||
- Existing modules cover ~47% of original studio.js (4,622 of 9,787 lines)
|
||||
- Remaining ~5,165 lines identified for future extraction
|
||||
- Build tooling (Webpack/Vite) recommended for ES6 module bundling
|
||||
|
||||
**19.01.2026**: Module aus studio.js extrahiert:
|
||||
- Alle funktionalen Bereiche in separate ES6-Module aufgeteilt
|
||||
- Module verwenden Export/Import-Syntax
|
||||
- Original studio.js noch nicht aktualisiert (backward compatibility)
|
||||
360
backend/frontend/static/js/modules/api-helpers.js
Normal file
360
backend/frontend/static/js/modules/api-helpers.js
Normal file
@@ -0,0 +1,360 @@
|
||||
/**
|
||||
* BreakPilot Studio - API Helpers Module
|
||||
*
|
||||
* Gemeinsame Funktionen für API-Aufrufe und Status-Verwaltung:
|
||||
* - fetchJSON: Wrapper für fetch mit Error-Handling
|
||||
* - postJSON: POST-Requests mit JSON-Body
|
||||
* - setStatus: Status-Leiste aktualisieren
|
||||
* - showNotification: Toast-Benachrichtigungen
|
||||
*
|
||||
* Refactored: 2026-01-19
|
||||
*/
|
||||
|
||||
import { t } from './i18n.js';
|
||||
|
||||
// Status-Bar Element-Referenzen (werden bei init gesetzt)
|
||||
let statusBar = null;
|
||||
let statusDot = null;
|
||||
let statusMain = null;
|
||||
let statusSub = null;
|
||||
|
||||
/**
|
||||
* Initialisiert die Status-Bar Referenzen
|
||||
* Sollte beim DOMContentLoaded aufgerufen werden
|
||||
*/
|
||||
export function initStatusBar() {
|
||||
statusBar = document.getElementById('status-bar');
|
||||
statusDot = document.getElementById('status-dot');
|
||||
statusMain = document.getElementById('status-main');
|
||||
statusSub = document.getElementById('status-sub');
|
||||
}
|
||||
|
||||
/**
|
||||
* Setzt den Status in der Status-Leiste
|
||||
* @param {string} type - 'ready'|'working'|'success'|'error'
|
||||
* @param {string} main - Haupttext
|
||||
* @param {string} [sub] - Optionaler Untertext
|
||||
*/
|
||||
export function setStatus(type, main, sub = '') {
|
||||
if (!statusBar || !statusDot || !statusMain) {
|
||||
console.log(`[Status ${type}]: ${main}`, sub);
|
||||
return;
|
||||
}
|
||||
|
||||
// Alle Status-Klassen entfernen
|
||||
statusBar.classList.remove('status-ready', 'status-working', 'status-success', 'status-error');
|
||||
statusDot.classList.remove('dot-ready', 'dot-working', 'dot-success', 'dot-error');
|
||||
|
||||
// Neue Status-Klasse setzen
|
||||
statusBar.classList.add(`status-${type}`);
|
||||
statusDot.classList.add(`dot-${type}`);
|
||||
|
||||
// Texte setzen
|
||||
statusMain.textContent = main;
|
||||
if (statusSub) {
|
||||
statusSub.textContent = sub;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Setzt den Status auf "Bereit"
|
||||
*/
|
||||
export function setStatusReady() {
|
||||
setStatus('ready', t('status_ready') || 'Bereit', '');
|
||||
}
|
||||
|
||||
/**
|
||||
* Setzt den Status auf "Arbeitet..."
|
||||
* @param {string} message - Was gerade gemacht wird
|
||||
*/
|
||||
export function setStatusWorking(message) {
|
||||
setStatus('working', message, '');
|
||||
}
|
||||
|
||||
/**
|
||||
* Setzt den Status auf "Erfolg"
|
||||
* @param {string} message - Erfolgsmeldung
|
||||
* @param {string} [details] - Optionale Details
|
||||
*/
|
||||
export function setStatusSuccess(message, details = '') {
|
||||
setStatus('success', message, details);
|
||||
}
|
||||
|
||||
/**
|
||||
* Setzt den Status auf "Fehler"
|
||||
* @param {string} message - Fehlermeldung
|
||||
* @param {string} [details] - Optionale Details
|
||||
*/
|
||||
export function setStatusError(message, details = '') {
|
||||
setStatus('error', message, details);
|
||||
}
|
||||
|
||||
/**
|
||||
* Führt einen GET-Request aus und parst JSON
|
||||
* @param {string} url - Die URL
|
||||
* @param {Object} [options] - Zusätzliche fetch-Optionen
|
||||
* @returns {Promise<any>} - Das geparste JSON
|
||||
* @throws {Error} - Bei Netzwerk- oder Parse-Fehlern
|
||||
*/
|
||||
export async function fetchJSON(url, options = {}) {
|
||||
const response = await fetch(url, {
|
||||
...options,
|
||||
headers: {
|
||||
'Accept': 'application/json',
|
||||
...options.headers,
|
||||
},
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text().catch(() => 'Unknown error');
|
||||
throw new Error(`HTTP ${response.status}: ${errorText}`);
|
||||
}
|
||||
|
||||
return response.json();
|
||||
}
|
||||
|
||||
/**
|
||||
* Führt einen POST-Request mit JSON-Body aus
|
||||
* @param {string} url - Die URL
|
||||
* @param {Object} data - Die zu sendenden Daten
|
||||
* @param {Object} [options] - Zusätzliche fetch-Optionen
|
||||
* @returns {Promise<any>} - Das geparste JSON
|
||||
* @throws {Error} - Bei Netzwerk- oder Parse-Fehlern
|
||||
*/
|
||||
export async function postJSON(url, data = {}, options = {}) {
|
||||
const response = await fetch(url, {
|
||||
method: 'POST',
|
||||
...options,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Accept': 'application/json',
|
||||
...options.headers,
|
||||
},
|
||||
body: JSON.stringify(data),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text().catch(() => 'Unknown error');
|
||||
throw new Error(`HTTP ${response.status}: ${errorText}`);
|
||||
}
|
||||
|
||||
return response.json();
|
||||
}
|
||||
|
||||
/**
|
||||
* Führt einen POST-Request ohne Body aus (für Trigger-Endpoints)
|
||||
* @param {string} url - Die URL
|
||||
* @param {Object} [options] - Zusätzliche fetch-Optionen
|
||||
* @returns {Promise<any>} - Das geparste JSON
|
||||
*/
|
||||
export async function postTrigger(url, options = {}) {
|
||||
const response = await fetch(url, {
|
||||
method: 'POST',
|
||||
...options,
|
||||
headers: {
|
||||
'Accept': 'application/json',
|
||||
...options.headers,
|
||||
},
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text().catch(() => 'Unknown error');
|
||||
throw new Error(`HTTP ${response.status}: ${errorText}`);
|
||||
}
|
||||
|
||||
return response.json();
|
||||
}
|
||||
|
||||
/**
|
||||
* Führt einen DELETE-Request aus
|
||||
* @param {string} url - Die URL
|
||||
* @param {Object} [options] - Zusätzliche fetch-Optionen
|
||||
* @returns {Promise<any>} - Das geparste JSON
|
||||
*/
|
||||
export async function deleteRequest(url, options = {}) {
|
||||
const response = await fetch(url, {
|
||||
method: 'DELETE',
|
||||
...options,
|
||||
headers: {
|
||||
'Accept': 'application/json',
|
||||
...options.headers,
|
||||
},
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text().catch(() => 'Unknown error');
|
||||
throw new Error(`HTTP ${response.status}: ${errorText}`);
|
||||
}
|
||||
|
||||
return response.json();
|
||||
}
|
||||
|
||||
/**
|
||||
* Lädt eine Datei hoch
|
||||
* @param {string} url - Die Upload-URL
|
||||
* @param {File|FormData} file - Die Datei oder FormData
|
||||
* @param {function} [onProgress] - Progress-Callback (0-100)
|
||||
* @returns {Promise<any>} - Das geparste JSON
|
||||
*/
|
||||
export async function uploadFile(url, file, onProgress = null) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const xhr = new XMLHttpRequest();
|
||||
|
||||
xhr.open('POST', url);
|
||||
|
||||
if (onProgress && xhr.upload) {
|
||||
xhr.upload.addEventListener('progress', (e) => {
|
||||
if (e.lengthComputable) {
|
||||
const percent = Math.round((e.loaded / e.total) * 100);
|
||||
onProgress(percent);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
xhr.onload = () => {
|
||||
if (xhr.status >= 200 && xhr.status < 300) {
|
||||
try {
|
||||
resolve(JSON.parse(xhr.responseText));
|
||||
} catch (e) {
|
||||
resolve({ status: 'OK', message: xhr.responseText });
|
||||
}
|
||||
} else {
|
||||
reject(new Error(`Upload failed: ${xhr.status} ${xhr.statusText}`));
|
||||
}
|
||||
};
|
||||
|
||||
xhr.onerror = () => reject(new Error('Network error during upload'));
|
||||
|
||||
// FormData erstellen falls nötig
|
||||
let formData;
|
||||
if (file instanceof FormData) {
|
||||
formData = file;
|
||||
} else {
|
||||
formData = new FormData();
|
||||
formData.append('file', file);
|
||||
}
|
||||
|
||||
xhr.send(formData);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Zeigt eine kurze Benachrichtigung (Toast)
|
||||
* @param {string} message - Die Nachricht
|
||||
* @param {string} [type='info'] - 'info'|'success'|'error'|'warning'
|
||||
* @param {number} [duration=3000] - Anzeigedauer in ms
|
||||
*/
|
||||
export function showNotification(message, type = 'info', duration = 3000) {
|
||||
// Prüfe ob Toast-Container existiert, sonst erstellen
|
||||
let container = document.getElementById('toast-container');
|
||||
if (!container) {
|
||||
container = document.createElement('div');
|
||||
container.id = 'toast-container';
|
||||
container.style.cssText = 'position:fixed;top:16px;right:16px;z-index:10000;display:flex;flex-direction:column;gap:8px;';
|
||||
document.body.appendChild(container);
|
||||
}
|
||||
|
||||
// Toast erstellen
|
||||
const toast = document.createElement('div');
|
||||
toast.className = `toast toast-${type}`;
|
||||
toast.style.cssText = `
|
||||
padding: 12px 16px;
|
||||
border-radius: 8px;
|
||||
background: var(--bp-card-bg, #1e293b);
|
||||
color: var(--bp-text, #e2e8f0);
|
||||
box-shadow: 0 4px 12px rgba(0,0,0,0.3);
|
||||
font-size: 13px;
|
||||
animation: slideIn 0.3s ease;
|
||||
border-left: 4px solid ${type === 'success' ? '#22c55e' : type === 'error' ? '#ef4444' : type === 'warning' ? '#f59e0b' : '#3b82f6'};
|
||||
`;
|
||||
toast.textContent = message;
|
||||
|
||||
container.appendChild(toast);
|
||||
|
||||
// Nach duration entfernen
|
||||
setTimeout(() => {
|
||||
toast.style.animation = 'slideOut 0.3s ease';
|
||||
setTimeout(() => toast.remove(), 300);
|
||||
}, duration);
|
||||
}
|
||||
|
||||
/**
|
||||
* Wrapper für API-Aufrufe mit Status-Anzeige und Error-Handling
|
||||
* @param {function} apiCall - Die async API-Funktion
|
||||
* @param {Object} options - Optionen
|
||||
* @param {string} options.workingMessage - Nachricht während des Ladens
|
||||
* @param {string} options.successMessage - Nachricht bei Erfolg
|
||||
* @param {string} options.errorMessage - Nachricht bei Fehler
|
||||
* @returns {Promise<any>} - Das Ergebnis oder null bei Fehler
|
||||
*/
|
||||
export async function withStatus(apiCall, options = {}) {
|
||||
const {
|
||||
workingMessage = 'Wird geladen...',
|
||||
successMessage = 'Erfolgreich',
|
||||
errorMessage = 'Fehler',
|
||||
} = options;
|
||||
|
||||
setStatusWorking(workingMessage);
|
||||
|
||||
try {
|
||||
const result = await apiCall();
|
||||
setStatusSuccess(successMessage);
|
||||
return result;
|
||||
} catch (error) {
|
||||
console.error(errorMessage, error);
|
||||
setStatusError(errorMessage, String(error.message || error));
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Debounce-Funktion für häufige Events
|
||||
* @param {function} func - Die zu debouncende Funktion
|
||||
* @param {number} wait - Wartezeit in ms
|
||||
* @returns {function} - Die gedebouncte Funktion
|
||||
*/
|
||||
export function debounce(func, wait) {
|
||||
let timeout;
|
||||
return function executedFunction(...args) {
|
||||
const later = () => {
|
||||
clearTimeout(timeout);
|
||||
func(...args);
|
||||
};
|
||||
clearTimeout(timeout);
|
||||
timeout = setTimeout(later, wait);
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Throttle-Funktion für Rate-Limiting
|
||||
* @param {function} func - Die zu throttlende Funktion
|
||||
* @param {number} limit - Minimaler Abstand in ms
|
||||
* @returns {function} - Die gethrottlete Funktion
|
||||
*/
|
||||
export function throttle(func, limit) {
|
||||
let inThrottle;
|
||||
return function(...args) {
|
||||
if (!inThrottle) {
|
||||
func.apply(this, args);
|
||||
inThrottle = true;
|
||||
setTimeout(() => inThrottle = false, limit);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// CSS für Toast-Animationen (einmal injizieren)
|
||||
if (typeof document !== 'undefined' && !document.getElementById('toast-styles')) {
|
||||
const style = document.createElement('style');
|
||||
style.id = 'toast-styles';
|
||||
style.textContent = `
|
||||
@keyframes slideIn {
|
||||
from { transform: translateX(100%); opacity: 0; }
|
||||
to { transform: translateX(0); opacity: 1; }
|
||||
}
|
||||
@keyframes slideOut {
|
||||
from { transform: translateX(0); opacity: 1; }
|
||||
to { transform: translateX(100%); opacity: 0; }
|
||||
}
|
||||
`;
|
||||
document.head.appendChild(style);
|
||||
}
|
||||
430
backend/frontend/static/js/modules/cloze-module.js
Normal file
430
backend/frontend/static/js/modules/cloze-module.js
Normal file
@@ -0,0 +1,430 @@
|
||||
/**
|
||||
* BreakPilot Studio - Cloze (Lückentext) Module
|
||||
*
|
||||
* Lückentext-Funktionalität mit Übersetzung:
|
||||
* - Generierung von Lückentexten aus Arbeitsblättern
|
||||
* - Mehrsprachige Übersetzungen (TR, AR, RU, UK, PL, EN)
|
||||
* - Interaktives Übungsmodul mit Auswertung
|
||||
* - Druckfunktion (mit/ohne Lösungen)
|
||||
*
|
||||
* Refactored: 2026-01-19
|
||||
*/
|
||||
|
||||
import { t } from './i18n.js';
|
||||
import { setStatus } from './api-helpers.js';
|
||||
|
||||
// State
|
||||
let currentClozeData = null;
|
||||
let clozeAnswers = {};
|
||||
|
||||
// DOM References
|
||||
let clozePreview = null;
|
||||
let clozeBadge = null;
|
||||
let clozeLanguageSelect = null;
|
||||
let clozeModal = null;
|
||||
let clozeModalBody = null;
|
||||
let clozeModalClose = null;
|
||||
let btnClozeGenerate = null;
|
||||
let btnClozeShow = null;
|
||||
let btnClozePrint = null;
|
||||
|
||||
// Callbacks
|
||||
let getEingangFilesCallback = null;
|
||||
let getCurrentIndexCallback = null;
|
||||
|
||||
/**
|
||||
* Initialisiert das Cloze-Modul
|
||||
* @param {Object} options - Konfiguration
|
||||
*/
|
||||
export function initClozeModule(options = {}) {
|
||||
getEingangFilesCallback = options.getEingangFiles || (() => []);
|
||||
getCurrentIndexCallback = options.getCurrentIndex || (() => 0);
|
||||
|
||||
// DOM References
|
||||
clozePreview = document.getElementById('cloze-preview') || options.previewEl;
|
||||
clozeBadge = document.getElementById('cloze-badge') || options.badgeEl;
|
||||
clozeLanguageSelect = document.getElementById('cloze-language') || options.languageSelectEl;
|
||||
clozeModal = document.getElementById('cloze-modal') || options.modalEl;
|
||||
clozeModalBody = document.getElementById('cloze-modal-body') || options.modalBodyEl;
|
||||
clozeModalClose = document.getElementById('cloze-modal-close') || options.modalCloseEl;
|
||||
btnClozeGenerate = document.getElementById('btn-cloze-generate') || options.generateBtn;
|
||||
btnClozeShow = document.getElementById('btn-cloze-show') || options.showBtn;
|
||||
btnClozePrint = document.getElementById('btn-cloze-print') || options.printBtn;
|
||||
|
||||
// Event-Listener
|
||||
if (btnClozeGenerate) {
|
||||
btnClozeGenerate.addEventListener('click', generateClozeTexts);
|
||||
}
|
||||
|
||||
if (btnClozeShow) {
|
||||
btnClozeShow.addEventListener('click', openClozeModal);
|
||||
}
|
||||
|
||||
if (btnClozePrint) {
|
||||
btnClozePrint.addEventListener('click', openClozePrintDialog);
|
||||
}
|
||||
|
||||
if (clozeModalClose) {
|
||||
clozeModalClose.addEventListener('click', closeClozeModal);
|
||||
}
|
||||
|
||||
if (clozeModal) {
|
||||
clozeModal.addEventListener('click', (ev) => {
|
||||
if (ev.target === clozeModal) {
|
||||
closeClozeModal();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Event für Datei-Wechsel
|
||||
window.addEventListener('fileSelected', () => {
|
||||
loadClozePreviewForCurrent();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Generiert Lückentexte für alle Arbeitsblätter
|
||||
*/
|
||||
export async function generateClozeTexts() {
|
||||
const targetLang = clozeLanguageSelect ? clozeLanguageSelect.value : 'tr';
|
||||
|
||||
try {
|
||||
setStatus(t('cloze_generating') || 'Generiere Lückentexte …', t('ai_working') || 'Bitte warten, KI arbeitet.', 'busy');
|
||||
if (clozeBadge) clozeBadge.textContent = t('generating') || 'Generiert...';
|
||||
|
||||
const resp = await fetch('/api/generate-cloze?target_language=' + targetLang, { method: 'POST' });
|
||||
if (!resp.ok) {
|
||||
throw new Error('HTTP ' + resp.status);
|
||||
}
|
||||
|
||||
const result = await resp.json();
|
||||
if (result.status === 'OK' && result.generated.length > 0) {
|
||||
setStatus(t('cloze_generated') || 'Lückentexte generiert', result.generated.length + ' ' + (t('files_created') || 'Dateien erstellt'));
|
||||
if (clozeBadge) clozeBadge.textContent = t('ready') || 'Fertig';
|
||||
if (btnClozeShow) btnClozeShow.style.display = 'inline-block';
|
||||
if (btnClozePrint) btnClozePrint.style.display = 'inline-block';
|
||||
|
||||
// Lade Vorschau für aktuelle Datei
|
||||
await loadClozePreviewForCurrent();
|
||||
} else if (result.errors && result.errors.length > 0) {
|
||||
setStatus(t('cloze_error') || 'Fehler bei Lückentext-Generierung', result.errors[0].error, 'error');
|
||||
if (clozeBadge) clozeBadge.textContent = t('error') || 'Fehler';
|
||||
} else {
|
||||
setStatus(t('no_cloze_generated') || 'Keine Lückentexte generiert', t('analysis_missing') || 'Möglicherweise fehlen Analyse-Daten.', 'error');
|
||||
if (clozeBadge) clozeBadge.textContent = t('ready') || 'Bereit';
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Lückentext-Generierung fehlgeschlagen:', e);
|
||||
setStatus(t('cloze_error') || 'Fehler bei Lückentext-Generierung', String(e), 'error');
|
||||
if (clozeBadge) clozeBadge.textContent = t('error') || 'Fehler';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Lädt Cloze-Vorschau für die aktuelle Datei
|
||||
*/
|
||||
export async function loadClozePreviewForCurrent() {
|
||||
const eingangFiles = getEingangFilesCallback();
|
||||
const currentIndex = getCurrentIndexCallback();
|
||||
|
||||
if (!eingangFiles.length) {
|
||||
if (clozePreview) clozePreview.innerHTML = '<div style="font-size:11px;color:var(--bp-text-muted);">' + (t('no_worksheets') || 'Keine Arbeitsblätter vorhanden.') + '</div>';
|
||||
return;
|
||||
}
|
||||
|
||||
const currentFile = eingangFiles[currentIndex];
|
||||
if (!currentFile) return;
|
||||
|
||||
try {
|
||||
const resp = await fetch('/api/cloze-data/' + encodeURIComponent(currentFile));
|
||||
const result = await resp.json();
|
||||
|
||||
if (result.status === 'OK' && result.data) {
|
||||
currentClozeData = result.data;
|
||||
renderClozePreview(result.data);
|
||||
if (btnClozeShow) btnClozeShow.style.display = 'inline-block';
|
||||
if (btnClozePrint) btnClozePrint.style.display = 'inline-block';
|
||||
} else {
|
||||
if (clozePreview) clozePreview.innerHTML = '<div style="font-size:11px;color:var(--bp-text-muted);">' + (t('no_cloze_for_worksheet') || 'Noch keine Lückentexte für dieses Arbeitsblatt generiert.') + '</div>';
|
||||
currentClozeData = null;
|
||||
if (btnClozeShow) btnClozeShow.style.display = 'none';
|
||||
if (btnClozePrint) btnClozePrint.style.display = 'none';
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Fehler beim Laden der Lückentext-Daten:', e);
|
||||
if (clozePreview) clozePreview.innerHTML = '';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Rendert die Cloze-Vorschau
|
||||
* @param {Object} clozeData - Cloze-Daten
|
||||
*/
|
||||
function renderClozePreview(clozeData) {
|
||||
if (!clozePreview) return;
|
||||
if (!clozeData || !clozeData.cloze_items || clozeData.cloze_items.length === 0) {
|
||||
clozePreview.innerHTML = '<div style="font-size:11px;color:var(--bp-text-muted);">' + (t('no_cloze_texts') || 'Keine Lückentexte vorhanden.') + '</div>';
|
||||
return;
|
||||
}
|
||||
|
||||
const items = clozeData.cloze_items;
|
||||
const metadata = clozeData.metadata || {};
|
||||
|
||||
let html = '';
|
||||
|
||||
// Statistiken
|
||||
html += '<div class="cloze-stats">';
|
||||
if (metadata.subject) {
|
||||
html += '<div><strong>' + (t('subject') || 'Fach') + ':</strong> ' + escapeHtml(metadata.subject) + '</div>';
|
||||
}
|
||||
if (metadata.grade_level) {
|
||||
html += '<div><strong>' + (t('grade') || 'Stufe') + ':</strong> ' + escapeHtml(metadata.grade_level) + '</div>';
|
||||
}
|
||||
html += '<div><strong>' + (t('sentences') || 'Sätze') + ':</strong> ' + items.length + '</div>';
|
||||
if (metadata.total_gaps) {
|
||||
html += '<div><strong>' + (t('gaps') || 'Lücken') + ':</strong> ' + metadata.total_gaps + '</div>';
|
||||
}
|
||||
html += '</div>';
|
||||
|
||||
// Zeige erste 2 Sätze als Vorschau
|
||||
const previewItems = items.slice(0, 2);
|
||||
previewItems.forEach((item, idx) => {
|
||||
html += '<div class="cloze-item">';
|
||||
html += '<div class="cloze-sentence">' + (idx + 1) + '. ' + item.sentence_with_gaps.replace(/___/g, '<span class="cloze-gap">___</span>') + '</div>';
|
||||
|
||||
// Übersetzung anzeigen
|
||||
if (item.translation && item.translation.full_sentence) {
|
||||
html += '<div class="cloze-translation">';
|
||||
html += '<div class="cloze-translation-label">' + (item.translation.language_name || t('translation') || 'Übersetzung') + ':</div>';
|
||||
html += escapeHtml(item.translation.full_sentence);
|
||||
html += '</div>';
|
||||
}
|
||||
html += '</div>';
|
||||
});
|
||||
|
||||
if (items.length > 2) {
|
||||
html += '<div style="font-size:11px;color:var(--bp-text-muted);text-align:center;margin-top:8px;">+ ' + (items.length - 2) + ' ' + (t('more_sentences') || 'weitere Sätze') + '</div>';
|
||||
}
|
||||
|
||||
clozePreview.innerHTML = html;
|
||||
}
|
||||
|
||||
/**
|
||||
* Öffnet das Cloze-Modal
|
||||
*/
|
||||
export function openClozeModal() {
|
||||
if (!currentClozeData || !currentClozeData.cloze_items) {
|
||||
alert(t('no_cloze_texts') || 'Keine Lückentexte vorhanden. Bitte zuerst generieren.');
|
||||
return;
|
||||
}
|
||||
|
||||
clozeAnswers = {}; // Reset Antworten
|
||||
renderClozeModal(currentClozeData);
|
||||
if (clozeModal) clozeModal.classList.remove('hidden');
|
||||
}
|
||||
|
||||
/**
|
||||
* Schließt das Cloze-Modal
|
||||
*/
|
||||
export function closeClozeModal() {
|
||||
if (clozeModal) clozeModal.classList.add('hidden');
|
||||
}
|
||||
|
||||
/**
|
||||
* Rendert den Modal-Inhalt
|
||||
* @param {Object} clozeData - Cloze-Daten
|
||||
*/
|
||||
function renderClozeModal(clozeData) {
|
||||
if (!clozeModalBody) return;
|
||||
|
||||
const items = clozeData.cloze_items;
|
||||
const metadata = clozeData.metadata || {};
|
||||
|
||||
let html = '';
|
||||
|
||||
// Header
|
||||
html += '<div class="cloze-stats" style="margin-bottom:16px;">';
|
||||
if (metadata.source_title) {
|
||||
html += '<div><strong>' + (t('worksheet') || 'Arbeitsblatt') + ':</strong> ' + escapeHtml(metadata.source_title) + '</div>';
|
||||
}
|
||||
if (metadata.total_gaps) {
|
||||
html += '<div><strong>' + (t('total_gaps') || 'Lücken gesamt') + ':</strong> ' + metadata.total_gaps + '</div>';
|
||||
}
|
||||
html += '</div>';
|
||||
|
||||
html += '<div style="font-size:12px;color:var(--bp-text-muted);margin-bottom:12px;">' + (t('cloze_instruction') || 'Fülle die Lücken aus und klicke auf "Prüfen".') + '</div>';
|
||||
|
||||
// Alle Sätze mit Eingabefeldern
|
||||
items.forEach((item, idx) => {
|
||||
html += '<div class="cloze-item" data-cid="' + item.id + '">';
|
||||
|
||||
// Satz mit Eingabefeldern statt ___
|
||||
let sentenceHtml = item.sentence_with_gaps;
|
||||
const gaps = item.gaps || [];
|
||||
|
||||
// Ersetze ___ durch Eingabefelder
|
||||
let gapIndex = 0;
|
||||
sentenceHtml = sentenceHtml.replace(/___/g, () => {
|
||||
const gap = gaps[gapIndex] || { id: 'g' + gapIndex, word: '' };
|
||||
const inputId = item.id + '_' + gap.id;
|
||||
gapIndex++;
|
||||
return '<input type="text" class="cloze-gap-input" data-cid="' + item.id + '" data-gid="' + gap.id + '" data-answer="' + escapeHtml(gap.word) + '" id="input_' + inputId + '" autocomplete="off">';
|
||||
});
|
||||
|
||||
html += '<div class="cloze-sentence">' + (idx + 1) + '. ' + sentenceHtml + '</div>';
|
||||
|
||||
// Übersetzung als Hilfe
|
||||
if (item.translation && item.translation.sentence_with_gaps) {
|
||||
html += '<div class="cloze-translation">';
|
||||
html += '<div class="cloze-translation-label">' + (item.translation.language_name || t('translation') || 'Übersetzung') + ' (' + (t('with_gaps') || 'mit Lücken') + '):</div>';
|
||||
html += escapeHtml(item.translation.sentence_with_gaps);
|
||||
html += '</div>';
|
||||
}
|
||||
|
||||
html += '</div>';
|
||||
});
|
||||
|
||||
// Buttons
|
||||
html += '<div style="margin-top:16px;text-align:center;display:flex;gap:8px;justify-content:center;">';
|
||||
html += '<button class="btn btn-primary" id="btn-cloze-check">' + (t('check') || 'Prüfen') + '</button>';
|
||||
html += '<button class="btn btn-ghost" id="btn-cloze-show-answers">' + (t('show_answers') || 'Lösungen zeigen') + '</button>';
|
||||
html += '</div>';
|
||||
|
||||
clozeModalBody.innerHTML = html;
|
||||
|
||||
// Event-Listener für Prüfen-Button
|
||||
const btnCheck = document.getElementById('btn-cloze-check');
|
||||
if (btnCheck) {
|
||||
btnCheck.addEventListener('click', checkClozeAnswers);
|
||||
}
|
||||
|
||||
// Event-Listener für Lösungen zeigen
|
||||
const btnShowAnswers = document.getElementById('btn-cloze-show-answers');
|
||||
if (btnShowAnswers) {
|
||||
btnShowAnswers.addEventListener('click', showClozeAnswers);
|
||||
}
|
||||
|
||||
// Enter-Taste zum Prüfen
|
||||
clozeModalBody.querySelectorAll('.cloze-gap-input').forEach(input => {
|
||||
input.addEventListener('keypress', (e) => {
|
||||
if (e.key === 'Enter') {
|
||||
checkClozeAnswers();
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Überprüft die Cloze-Antworten
|
||||
*/
|
||||
function checkClozeAnswers() {
|
||||
if (!clozeModalBody) return;
|
||||
|
||||
let correct = 0;
|
||||
let total = 0;
|
||||
|
||||
clozeModalBody.querySelectorAll('.cloze-gap-input').forEach(input => {
|
||||
const userAnswer = input.value.trim().toLowerCase();
|
||||
const correctAnswer = input.getAttribute('data-answer').toLowerCase();
|
||||
total++;
|
||||
|
||||
// Entferne vorherige Klassen
|
||||
input.classList.remove('correct', 'incorrect');
|
||||
|
||||
if (userAnswer === correctAnswer) {
|
||||
input.classList.add('correct');
|
||||
correct++;
|
||||
} else if (userAnswer !== '') {
|
||||
input.classList.add('incorrect');
|
||||
}
|
||||
});
|
||||
|
||||
// Zeige Ergebnis
|
||||
let existingResult = clozeModalBody.querySelector('.cloze-result');
|
||||
if (existingResult) existingResult.remove();
|
||||
|
||||
const percentage = Math.round(correct / total * 100);
|
||||
const resultHtml = '<div class="cloze-result" style="margin-top:16px;padding:12px;background:rgba(15,23,42,0.6);border-radius:8px;text-align:center;">' +
|
||||
'<div style="font-size:18px;font-weight:600;">' + correct + ' ' + (t('of') || 'von') + ' ' + total + ' ' + (t('correct_answers') || 'richtig') + '</div>' +
|
||||
'<div style="font-size:12px;color:var(--bp-text-muted);margin-top:4px;">' + percentage + '% ' + (t('percent_correct') || 'korrekt') + '</div>' +
|
||||
'</div>';
|
||||
|
||||
const resultDiv = document.createElement('div');
|
||||
resultDiv.innerHTML = resultHtml;
|
||||
clozeModalBody.appendChild(resultDiv.firstChild);
|
||||
}
|
||||
|
||||
/**
|
||||
* Zeigt alle Cloze-Lösungen
|
||||
*/
|
||||
function showClozeAnswers() {
|
||||
if (!clozeModalBody) return;
|
||||
|
||||
clozeModalBody.querySelectorAll('.cloze-gap-input').forEach(input => {
|
||||
const correctAnswer = input.getAttribute('data-answer');
|
||||
input.value = correctAnswer;
|
||||
input.classList.remove('incorrect');
|
||||
input.classList.add('correct');
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Öffnet den Druck-Dialog
|
||||
*/
|
||||
export function openClozePrintDialog() {
|
||||
if (!currentClozeData) {
|
||||
alert(t('no_cloze_texts') || 'Keine Lückentexte vorhanden.');
|
||||
return;
|
||||
}
|
||||
|
||||
const eingangFiles = getEingangFilesCallback();
|
||||
const currentIndex = getCurrentIndexCallback();
|
||||
const currentFile = eingangFiles[currentIndex];
|
||||
|
||||
const confirmMsg = (t('cloze_print_with_answers') || 'Mit Lösungen drucken?') +
|
||||
'\n\nOK = ' + (t('with_filled_gaps') || 'Mit ausgefüllten Lücken') +
|
||||
'\n' + (t('cancel') || 'Abbrechen') + ' = ' + (t('exercise_with_wordbank') || 'Übungsblatt mit Wortbank');
|
||||
|
||||
const choice = confirm(confirmMsg);
|
||||
const url = '/api/print-cloze/' + encodeURIComponent(currentFile) + '?show_answers=' + choice;
|
||||
window.open(url, '_blank');
|
||||
}
|
||||
|
||||
// === Getter und Setter ===
|
||||
|
||||
/**
|
||||
* Gibt die aktuellen Cloze-Daten zurück
|
||||
* @returns {Object|null}
|
||||
*/
|
||||
export function getClozeData() {
|
||||
return currentClozeData;
|
||||
}
|
||||
|
||||
/**
|
||||
* Setzt die Cloze-Daten
|
||||
* @param {Object} data
|
||||
*/
|
||||
export function setClozeData(data) {
|
||||
currentClozeData = data;
|
||||
if (data) {
|
||||
renderClozePreview(data);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gibt die aktuelle Zielsprache zurück
|
||||
* @returns {string}
|
||||
*/
|
||||
export function getTargetLanguage() {
|
||||
return clozeLanguageSelect ? clozeLanguageSelect.value : 'tr';
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper: HTML-Escape
|
||||
*/
|
||||
function escapeHtml(text) {
|
||||
if (!text) return '';
|
||||
const div = document.createElement('div');
|
||||
div.textContent = text;
|
||||
return div.innerHTML;
|
||||
}
|
||||
614
backend/frontend/static/js/modules/file-manager.js
Normal file
614
backend/frontend/static/js/modules/file-manager.js
Normal file
@@ -0,0 +1,614 @@
|
||||
/**
|
||||
* BreakPilot Studio - File Manager Module
|
||||
*
|
||||
* Datei-Verwaltung für den Arbeitsblatt-Editor:
|
||||
* - Laden und Rendern der Dateiliste
|
||||
* - Upload von Dateien
|
||||
* - Löschen von Dateien
|
||||
* - Vorschau-Funktionen
|
||||
* - Navigation zwischen Dateien
|
||||
*
|
||||
* Refactored: 2026-01-19
|
||||
*/
|
||||
|
||||
import { t } from './i18n.js';
|
||||
import { setStatus, setStatusWorking, setStatusError, setStatusSuccess, fetchJSON } from './api-helpers.js';
|
||||
import { openLightbox } from './lightbox.js';
|
||||
|
||||
// State
|
||||
let allEingangFiles = [];
|
||||
let eingangFiles = [];
|
||||
let currentIndex = 0;
|
||||
let currentSelectedFile = null;
|
||||
let worksheetPairs = {};
|
||||
let allWorksheetPairs = {};
|
||||
let showOnlyUnitFiles = false;
|
||||
let currentUnitId = null;
|
||||
|
||||
// DOM References (werden bei init gesetzt)
|
||||
let eingangListEl = null;
|
||||
let eingangCountEl = null;
|
||||
let previewContainer = null;
|
||||
let fileInput = null;
|
||||
let btnUploadInline = null;
|
||||
|
||||
/**
|
||||
* Initialisiert den File Manager
|
||||
* @param {Object} options - Konfiguration
|
||||
*/
|
||||
export function initFileManager(options = {}) {
|
||||
eingangListEl = document.getElementById('eingang-list') || options.listEl;
|
||||
eingangCountEl = document.getElementById('eingang-count') || options.countEl;
|
||||
previewContainer = document.getElementById('preview-container') || options.previewEl;
|
||||
fileInput = document.getElementById('file-input') || options.fileInput;
|
||||
btnUploadInline = document.getElementById('btn-upload-inline') || options.uploadBtn;
|
||||
|
||||
// Upload-Button Event
|
||||
if (btnUploadInline) {
|
||||
btnUploadInline.addEventListener('click', handleUpload);
|
||||
}
|
||||
|
||||
// Initial load
|
||||
loadEingangFiles();
|
||||
loadWorksheetPairs();
|
||||
}
|
||||
|
||||
/**
|
||||
* Setzt die aktuelle Lerneinheit
|
||||
* @param {string} unitId - Die Unit-ID
|
||||
*/
|
||||
export function setCurrentUnit(unitId) {
|
||||
currentUnitId = unitId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Setzt den Filter für Lerneinheit-Dateien
|
||||
* @param {boolean} show - Nur Unit-Dateien anzeigen
|
||||
*/
|
||||
export function setShowOnlyUnitFiles(show) {
|
||||
showOnlyUnitFiles = show;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gibt die aktuelle Dateiliste zurück
|
||||
* @returns {string[]} - Liste der Dateinamen
|
||||
*/
|
||||
export function getFiles() {
|
||||
return eingangFiles.slice();
|
||||
}
|
||||
|
||||
/**
|
||||
* Gibt den aktuellen Index zurück
|
||||
* @returns {number}
|
||||
*/
|
||||
export function getCurrentIndex() {
|
||||
return currentIndex;
|
||||
}
|
||||
|
||||
/**
|
||||
* Setzt den aktuellen Index
|
||||
* @param {number} idx
|
||||
*/
|
||||
export function setCurrentIndex(idx) {
|
||||
currentIndex = idx;
|
||||
renderEingangList();
|
||||
renderPreviewForCurrent();
|
||||
}
|
||||
|
||||
/**
|
||||
* Gibt den aktuell ausgewählten Dateinamen zurück
|
||||
* @returns {string|null}
|
||||
*/
|
||||
export function getCurrentFile() {
|
||||
return eingangFiles[currentIndex] || null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Lädt die Dateien aus dem Eingang
|
||||
*/
|
||||
export async function loadEingangFiles() {
|
||||
try {
|
||||
const data = await fetchJSON('/api/eingang-dateien');
|
||||
allEingangFiles = data.eingang || [];
|
||||
eingangFiles = allEingangFiles.slice();
|
||||
currentIndex = 0;
|
||||
renderEingangList();
|
||||
} catch (e) {
|
||||
console.error('Fehler beim Laden der Dateien:', e);
|
||||
setStatusError(t('error') || 'Fehler', String(e));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Lädt die Worksheet-Pairs (Original → Bereinigt)
|
||||
*/
|
||||
export async function loadWorksheetPairs() {
|
||||
try {
|
||||
const data = await fetchJSON('/api/worksheet-pairs');
|
||||
allWorksheetPairs = {};
|
||||
(data.pairs || []).forEach((p) => {
|
||||
allWorksheetPairs[p.original] = { clean_html: p.clean_html, clean_image: p.clean_image };
|
||||
});
|
||||
worksheetPairs = { ...allWorksheetPairs };
|
||||
renderPreviewForCurrent();
|
||||
} catch (e) {
|
||||
console.error('Fehler beim Laden der Neuaufbau-Daten:', e);
|
||||
setStatusError(t('error') || 'Fehler', String(e));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Rendert die Dateiliste
|
||||
*/
|
||||
export function renderEingangList() {
|
||||
if (!eingangListEl) return;
|
||||
|
||||
eingangListEl.innerHTML = '';
|
||||
|
||||
if (!eingangFiles.length) {
|
||||
const li = document.createElement('li');
|
||||
li.className = 'file-empty';
|
||||
li.textContent = t('no_files') || 'Noch keine Dateien vorhanden.';
|
||||
eingangListEl.appendChild(li);
|
||||
if (eingangCountEl) {
|
||||
eingangCountEl.textContent = '0 ' + (t('files') || 'Dateien');
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
eingangFiles.forEach((filename, idx) => {
|
||||
const li = document.createElement('li');
|
||||
li.className = 'file-item';
|
||||
if (idx === currentIndex) {
|
||||
li.classList.add('active');
|
||||
}
|
||||
|
||||
const nameSpan = document.createElement('span');
|
||||
nameSpan.className = 'file-item-name';
|
||||
nameSpan.textContent = filename;
|
||||
|
||||
const actionsSpan = document.createElement('span');
|
||||
actionsSpan.style.display = 'flex';
|
||||
actionsSpan.style.gap = '6px';
|
||||
|
||||
// Button: Aus Lerneinheit entfernen
|
||||
const removeFromUnitBtn = document.createElement('span');
|
||||
removeFromUnitBtn.className = 'file-item-delete';
|
||||
removeFromUnitBtn.textContent = '✕';
|
||||
removeFromUnitBtn.title = t('remove_from_unit') || 'Aus Lerneinheit entfernen';
|
||||
removeFromUnitBtn.addEventListener('click', (ev) => {
|
||||
ev.stopPropagation();
|
||||
if (!currentUnitId) {
|
||||
alert(t('select_unit_first') || 'Zum Entfernen bitte zuerst eine Lerneinheit auswählen.');
|
||||
return;
|
||||
}
|
||||
const ok = confirm(t('confirm_remove_from_unit') || 'Dieses Arbeitsblatt aus der aktuellen Lerneinheit entfernen? Die Datei selbst bleibt erhalten.');
|
||||
if (!ok) return;
|
||||
removeWorksheetFromCurrentUnit(eingangFiles[idx]);
|
||||
});
|
||||
|
||||
// Button: Datei komplett löschen
|
||||
const deleteFileBtn = document.createElement('span');
|
||||
deleteFileBtn.className = 'file-item-delete';
|
||||
deleteFileBtn.textContent = '🗑️';
|
||||
deleteFileBtn.title = t('delete_file') || 'Datei komplett löschen';
|
||||
deleteFileBtn.style.color = '#ef4444';
|
||||
deleteFileBtn.addEventListener('click', async (ev) => {
|
||||
ev.stopPropagation();
|
||||
const ok = confirm(t('confirm_delete_file') || `Datei "${eingangFiles[idx]}" wirklich komplett löschen? Diese Aktion kann nicht rückgängig gemacht werden.`);
|
||||
if (!ok) return;
|
||||
await deleteFileCompletely(eingangFiles[idx]);
|
||||
});
|
||||
|
||||
actionsSpan.appendChild(removeFromUnitBtn);
|
||||
actionsSpan.appendChild(deleteFileBtn);
|
||||
|
||||
li.appendChild(nameSpan);
|
||||
li.appendChild(actionsSpan);
|
||||
|
||||
li.addEventListener('click', () => {
|
||||
currentIndex = idx;
|
||||
currentSelectedFile = filename;
|
||||
renderEingangList();
|
||||
renderPreviewForCurrent();
|
||||
// Event für andere Module
|
||||
window.dispatchEvent(new CustomEvent('fileSelected', {
|
||||
detail: { filename, index: idx }
|
||||
}));
|
||||
});
|
||||
|
||||
eingangListEl.appendChild(li);
|
||||
});
|
||||
|
||||
if (eingangCountEl) {
|
||||
eingangCountEl.textContent = eingangFiles.length + ' ' + (eingangFiles.length === 1 ? (t('file') || 'Datei') : (t('files') || 'Dateien'));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Rendert die Vorschau für die aktuelle Datei
|
||||
*/
|
||||
export function renderPreviewForCurrent() {
|
||||
if (!previewContainer) return;
|
||||
|
||||
if (!eingangFiles.length) {
|
||||
const message = showOnlyUnitFiles && currentUnitId
|
||||
? (t('no_files_in_unit') || 'Dieser Lerneinheit sind noch keine Arbeitsblätter zugeordnet.')
|
||||
: (t('no_files') || 'Keine Dateien vorhanden.');
|
||||
previewContainer.innerHTML = `<div class="preview-placeholder">${message}</div>`;
|
||||
return;
|
||||
}
|
||||
|
||||
if (currentIndex < 0) currentIndex = 0;
|
||||
if (currentIndex >= eingangFiles.length) currentIndex = eingangFiles.length - 1;
|
||||
|
||||
const filename = eingangFiles[currentIndex];
|
||||
const entry = worksheetPairs[filename] || { clean_html: null, clean_image: null };
|
||||
|
||||
renderPreview(entry, currentIndex);
|
||||
}
|
||||
|
||||
/**
|
||||
* Rendert die Vorschau (Original vs. Bereinigt)
|
||||
* @param {Object} entry - Die Worksheet-Pair-Daten
|
||||
* @param {number} index - Der Index
|
||||
*/
|
||||
function renderPreview(entry, index) {
|
||||
if (!previewContainer) return;
|
||||
|
||||
previewContainer.innerHTML = '';
|
||||
|
||||
const wrapper = document.createElement('div');
|
||||
wrapper.className = 'compare-wrapper';
|
||||
|
||||
// Original-Sektion
|
||||
const originalSection = createPreviewSection(
|
||||
t('original_scan') || 'Original-Scan',
|
||||
t('old_left') || 'Alt (links)',
|
||||
() => {
|
||||
const img = document.createElement('img');
|
||||
img.className = 'preview-img';
|
||||
const imgSrc = '/preview-file/' + encodeURIComponent(eingangFiles[index]);
|
||||
img.src = imgSrc;
|
||||
img.alt = 'Original ' + eingangFiles[index];
|
||||
img.addEventListener('dblclick', () => openLightbox(imgSrc, eingangFiles[index]));
|
||||
return img;
|
||||
}
|
||||
);
|
||||
|
||||
// Bereinigt-Sektion
|
||||
const cleanSection = createPreviewSection(
|
||||
t('rebuilt_worksheet') || 'Neu aufgebautes Arbeitsblatt',
|
||||
createPrintButton(),
|
||||
() => {
|
||||
if (entry.clean_image) {
|
||||
const imgClean = document.createElement('img');
|
||||
imgClean.className = 'preview-img';
|
||||
const cleanSrc = '/preview-clean-file/' + encodeURIComponent(entry.clean_image);
|
||||
imgClean.src = cleanSrc;
|
||||
imgClean.alt = 'Neu aufgebaut ' + eingangFiles[index];
|
||||
imgClean.addEventListener('dblclick', () => openLightbox(cleanSrc, eingangFiles[index] + ' (neu)'));
|
||||
return imgClean;
|
||||
} else if (entry.clean_html) {
|
||||
const frame = document.createElement('iframe');
|
||||
frame.className = 'clean-frame';
|
||||
frame.src = '/api/clean-html/' + encodeURIComponent(entry.clean_html);
|
||||
frame.title = t('rebuilt_worksheet') || 'Neu aufgebautes Arbeitsblatt';
|
||||
frame.addEventListener('dblclick', () => {
|
||||
window.open('/api/clean-html/' + encodeURIComponent(entry.clean_html), '_blank');
|
||||
});
|
||||
return frame;
|
||||
} else {
|
||||
const placeholder = document.createElement('div');
|
||||
placeholder.className = 'preview-placeholder';
|
||||
placeholder.textContent = t('no_rebuild_data') || 'Noch keine Neuaufbau-Daten vorhanden.';
|
||||
return placeholder;
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
// Thumbnails in der Mitte
|
||||
const thumbsColumn = document.createElement('div');
|
||||
thumbsColumn.className = 'preview-thumbnails';
|
||||
thumbsColumn.id = 'preview-thumbnails-middle';
|
||||
renderThumbnailsInColumn(thumbsColumn);
|
||||
|
||||
wrapper.appendChild(originalSection);
|
||||
wrapper.appendChild(thumbsColumn);
|
||||
wrapper.appendChild(cleanSection);
|
||||
|
||||
// Navigation
|
||||
const navDiv = createNavigationButtons();
|
||||
wrapper.appendChild(navDiv);
|
||||
|
||||
previewContainer.appendChild(wrapper);
|
||||
}
|
||||
|
||||
/**
|
||||
* Erstellt eine Vorschau-Sektion
|
||||
*/
|
||||
function createPreviewSection(title, rightContent, contentFactory) {
|
||||
const section = document.createElement('div');
|
||||
section.className = 'compare-section';
|
||||
|
||||
const header = document.createElement('div');
|
||||
header.className = 'compare-header';
|
||||
|
||||
const titleSpan = document.createElement('span');
|
||||
titleSpan.textContent = title;
|
||||
|
||||
const rightSpan = document.createElement('span');
|
||||
rightSpan.style.display = 'flex';
|
||||
rightSpan.style.alignItems = 'center';
|
||||
rightSpan.style.gap = '8px';
|
||||
|
||||
if (typeof rightContent === 'string') {
|
||||
rightSpan.textContent = rightContent;
|
||||
} else if (rightContent instanceof Node) {
|
||||
rightSpan.appendChild(rightContent);
|
||||
}
|
||||
|
||||
header.appendChild(titleSpan);
|
||||
header.appendChild(rightSpan);
|
||||
|
||||
const body = document.createElement('div');
|
||||
body.className = 'compare-body';
|
||||
|
||||
const inner = document.createElement('div');
|
||||
inner.className = 'compare-body-inner';
|
||||
|
||||
const content = contentFactory();
|
||||
inner.appendChild(content);
|
||||
body.appendChild(inner);
|
||||
|
||||
section.appendChild(header);
|
||||
section.appendChild(body);
|
||||
|
||||
return section;
|
||||
}
|
||||
|
||||
/**
|
||||
* Erstellt den Druck-Button
|
||||
*/
|
||||
function createPrintButton() {
|
||||
const container = document.createElement('span');
|
||||
container.style.display = 'flex';
|
||||
container.style.alignItems = 'center';
|
||||
container.style.gap = '8px';
|
||||
|
||||
const btn = document.createElement('button');
|
||||
btn.type = 'button';
|
||||
btn.className = 'btn btn-sm btn-ghost no-print';
|
||||
btn.style.padding = '4px 10px';
|
||||
btn.style.fontSize = '11px';
|
||||
btn.textContent = '🖨️ ' + (t('print') || 'Drucken');
|
||||
btn.addEventListener('click', () => {
|
||||
const currentFile = eingangFiles[currentIndex];
|
||||
if (!currentFile) {
|
||||
alert(t('worksheet_no_data') || 'Keine Arbeitsblatt-Daten vorhanden.');
|
||||
return;
|
||||
}
|
||||
window.open('/api/print-worksheet/' + encodeURIComponent(currentFile), '_blank');
|
||||
});
|
||||
|
||||
const label = document.createElement('span');
|
||||
label.textContent = t('new_right') || 'Neu (rechts)';
|
||||
|
||||
container.appendChild(btn);
|
||||
container.appendChild(label);
|
||||
|
||||
return container;
|
||||
}
|
||||
|
||||
/**
|
||||
* Rendert Thumbnails in einer Spalte
|
||||
*/
|
||||
function renderThumbnailsInColumn(container) {
|
||||
container.innerHTML = '';
|
||||
|
||||
if (eingangFiles.length <= 1) return;
|
||||
|
||||
const maxThumbs = 5;
|
||||
let thumbCount = 0;
|
||||
|
||||
for (let i = 0; i < eingangFiles.length && thumbCount < maxThumbs; i++) {
|
||||
if (i === currentIndex) continue;
|
||||
|
||||
const filename = eingangFiles[i];
|
||||
const thumb = document.createElement('div');
|
||||
thumb.className = 'preview-thumb';
|
||||
|
||||
const img = document.createElement('img');
|
||||
img.src = '/preview-file/' + encodeURIComponent(filename);
|
||||
img.alt = filename;
|
||||
|
||||
const label = document.createElement('div');
|
||||
label.className = 'preview-thumb-label';
|
||||
label.textContent = `${i + 1}`;
|
||||
|
||||
thumb.appendChild(img);
|
||||
thumb.appendChild(label);
|
||||
|
||||
thumb.addEventListener('click', () => {
|
||||
currentIndex = i;
|
||||
renderEingangList();
|
||||
renderPreviewForCurrent();
|
||||
});
|
||||
|
||||
container.appendChild(thumb);
|
||||
thumbCount++;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Erstellt die Navigations-Buttons
|
||||
*/
|
||||
function createNavigationButtons() {
|
||||
const navDiv = document.createElement('div');
|
||||
navDiv.className = 'preview-nav';
|
||||
|
||||
const prevBtn = document.createElement('button');
|
||||
prevBtn.type = 'button';
|
||||
prevBtn.textContent = '‹';
|
||||
prevBtn.disabled = currentIndex === 0;
|
||||
prevBtn.addEventListener('click', () => {
|
||||
if (currentIndex > 0) {
|
||||
currentIndex--;
|
||||
renderEingangList();
|
||||
renderPreviewForCurrent();
|
||||
}
|
||||
});
|
||||
|
||||
const nextBtn = document.createElement('button');
|
||||
nextBtn.type = 'button';
|
||||
nextBtn.textContent = '›';
|
||||
nextBtn.disabled = currentIndex >= eingangFiles.length - 1;
|
||||
nextBtn.addEventListener('click', () => {
|
||||
if (currentIndex < eingangFiles.length - 1) {
|
||||
currentIndex++;
|
||||
renderEingangList();
|
||||
renderPreviewForCurrent();
|
||||
}
|
||||
});
|
||||
|
||||
const positionSpan = document.createElement('span');
|
||||
positionSpan.textContent = `${currentIndex + 1} ${t('of') || 'von'} ${eingangFiles.length}`;
|
||||
|
||||
navDiv.appendChild(prevBtn);
|
||||
navDiv.appendChild(positionSpan);
|
||||
navDiv.appendChild(nextBtn);
|
||||
|
||||
return navDiv;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle File Upload
|
||||
*/
|
||||
async function handleUpload(ev) {
|
||||
ev.preventDefault();
|
||||
ev.stopPropagation();
|
||||
|
||||
if (!fileInput) return;
|
||||
|
||||
const files = fileInput.files;
|
||||
if (!files || !files.length) {
|
||||
alert(t('select_files_first') || 'Bitte erst Dateien auswählen.');
|
||||
return;
|
||||
}
|
||||
|
||||
const formData = new FormData();
|
||||
for (const file of files) {
|
||||
formData.append('files', file);
|
||||
}
|
||||
|
||||
try {
|
||||
setStatusWorking(t('uploading') || 'Upload läuft …');
|
||||
|
||||
const resp = await fetch('/api/upload-multi', {
|
||||
method: 'POST',
|
||||
body: formData,
|
||||
});
|
||||
|
||||
if (!resp.ok) {
|
||||
console.error('Upload-Fehler: HTTP', resp.status);
|
||||
setStatusError(t('upload_error') || 'Fehler beim Upload', 'HTTP ' + resp.status);
|
||||
return;
|
||||
}
|
||||
|
||||
setStatusSuccess(t('upload_complete') || 'Upload abgeschlossen');
|
||||
fileInput.value = '';
|
||||
|
||||
// Liste neu laden
|
||||
await loadEingangFiles();
|
||||
await loadWorksheetPairs();
|
||||
} catch (e) {
|
||||
console.error('Netzwerkfehler beim Upload', e);
|
||||
setStatusError(t('network_error') || 'Netzwerkfehler', String(e));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Entfernt ein Arbeitsblatt aus der aktuellen Lerneinheit
|
||||
* @param {string} filename - Der Dateiname
|
||||
*/
|
||||
async function removeWorksheetFromCurrentUnit(filename) {
|
||||
if (!currentUnitId) return;
|
||||
|
||||
try {
|
||||
setStatusWorking(t('removing_from_unit') || 'Entferne aus Lerneinheit...');
|
||||
|
||||
const resp = await fetch(`/api/units/${currentUnitId}/worksheets/${encodeURIComponent(filename)}`, {
|
||||
method: 'DELETE'
|
||||
});
|
||||
|
||||
if (!resp.ok) {
|
||||
throw new Error('HTTP ' + resp.status);
|
||||
}
|
||||
|
||||
setStatusSuccess(t('removed_from_unit') || 'Aus Lerneinheit entfernt');
|
||||
await loadEingangFiles();
|
||||
} catch (e) {
|
||||
console.error('Fehler beim Entfernen:', e);
|
||||
setStatusError(t('error') || 'Fehler', String(e));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Löscht eine Datei komplett
|
||||
* @param {string} filename - Der Dateiname
|
||||
*/
|
||||
async function deleteFileCompletely(filename) {
|
||||
try {
|
||||
setStatusWorking(t('deleting_file') || 'Lösche Datei...');
|
||||
|
||||
const resp = await fetch('/api/delete-file/' + encodeURIComponent(filename), {
|
||||
method: 'DELETE'
|
||||
});
|
||||
|
||||
if (!resp.ok) {
|
||||
throw new Error('HTTP ' + resp.status);
|
||||
}
|
||||
|
||||
setStatusSuccess(t('file_deleted') || 'Datei gelöscht');
|
||||
await loadEingangFiles();
|
||||
await loadWorksheetPairs();
|
||||
} catch (e) {
|
||||
console.error('Fehler beim Löschen:', e);
|
||||
setStatusError(t('error') || 'Fehler', String(e));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Navigiert zur nächsten Datei
|
||||
*/
|
||||
export function nextFile() {
|
||||
if (currentIndex < eingangFiles.length - 1) {
|
||||
currentIndex++;
|
||||
renderEingangList();
|
||||
renderPreviewForCurrent();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Navigiert zur vorherigen Datei
|
||||
*/
|
||||
export function prevFile() {
|
||||
if (currentIndex > 0) {
|
||||
currentIndex--;
|
||||
renderEingangList();
|
||||
renderPreviewForCurrent();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Aktualisiert die Worksheet-Pairs für einen bestimmten Dateinamen
|
||||
* @param {string} filename
|
||||
* @param {Object} pair
|
||||
*/
|
||||
export function updateWorksheetPair(filename, pair) {
|
||||
worksheetPairs[filename] = pair;
|
||||
allWorksheetPairs[filename] = pair;
|
||||
if (eingangFiles[currentIndex] === filename) {
|
||||
renderPreviewForCurrent();
|
||||
}
|
||||
}
|
||||
250
backend/frontend/static/js/modules/i18n.js
Normal file
250
backend/frontend/static/js/modules/i18n.js
Normal file
@@ -0,0 +1,250 @@
|
||||
/**
|
||||
* BreakPilot Studio - i18n Module
|
||||
*
|
||||
* Internationalisierungs-Funktionen:
|
||||
* - t(key): Übersetzungsfunktion
|
||||
* - setLanguage(lang): Sprache wechseln
|
||||
* - applyLanguage(): UI-Texte aktualisieren
|
||||
* - getCurrentLang(): Aktuelle Sprache abrufen
|
||||
*
|
||||
* Refactored: 2026-01-19
|
||||
*/
|
||||
|
||||
import { translations, rtlLanguages, defaultLanguage, availableLanguages } from './translations.js';
|
||||
|
||||
// Aktuelle Sprache (aus localStorage oder Standard)
|
||||
let currentLang = localStorage.getItem('bp_language') || defaultLanguage;
|
||||
|
||||
/**
|
||||
* Übersetzungsfunktion
|
||||
* @param {string} key - Übersetzungsschlüssel
|
||||
* @returns {string} - Übersetzter Text oder Fallback
|
||||
*/
|
||||
export function t(key) {
|
||||
const lang = translations[currentLang] || translations[defaultLanguage];
|
||||
return lang[key] || translations[defaultLanguage][key] || key;
|
||||
}
|
||||
|
||||
/**
|
||||
* Aktuelle Sprache abrufen
|
||||
* @returns {string} - Sprachcode (de, en, tr, etc.)
|
||||
*/
|
||||
export function getCurrentLang() {
|
||||
return currentLang;
|
||||
}
|
||||
|
||||
/**
|
||||
* Prüft ob aktuelle Sprache RTL ist
|
||||
* @returns {boolean}
|
||||
*/
|
||||
export function isRTL() {
|
||||
return rtlLanguages.includes(currentLang);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sprache wechseln
|
||||
* @param {string} lang - Neuer Sprachcode
|
||||
*/
|
||||
export function setLanguage(lang) {
|
||||
if (translations[lang]) {
|
||||
currentLang = lang;
|
||||
localStorage.setItem('bp_language', lang);
|
||||
applyLanguage();
|
||||
return true;
|
||||
}
|
||||
console.warn(`Language '${lang}' not available`);
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Wendet die aktuelle Sprache auf alle UI-Elemente an
|
||||
*/
|
||||
export function applyLanguage() {
|
||||
// RTL-Unterstützung
|
||||
if (isRTL()) {
|
||||
document.body.classList.add('rtl');
|
||||
document.documentElement.setAttribute('dir', 'rtl');
|
||||
} else {
|
||||
document.body.classList.remove('rtl');
|
||||
document.documentElement.setAttribute('dir', 'ltr');
|
||||
}
|
||||
|
||||
// Alle Elemente mit data-i18n-Attribut aktualisieren
|
||||
document.querySelectorAll('[data-i18n]').forEach(el => {
|
||||
const key = el.getAttribute('data-i18n');
|
||||
const translated = t(key);
|
||||
|
||||
// Verschiedene Element-Typen behandeln
|
||||
if (el.tagName === 'INPUT' || el.tagName === 'TEXTAREA') {
|
||||
el.placeholder = translated;
|
||||
} else {
|
||||
el.textContent = translated;
|
||||
}
|
||||
});
|
||||
|
||||
// Elemente mit data-i18n-title für Tooltips
|
||||
document.querySelectorAll('[data-i18n-title]').forEach(el => {
|
||||
const key = el.getAttribute('data-i18n-title');
|
||||
el.title = t(key);
|
||||
});
|
||||
|
||||
// Elemente mit data-i18n-value für value-Attribute
|
||||
document.querySelectorAll('[data-i18n-value]').forEach(el => {
|
||||
const key = el.getAttribute('data-i18n-value');
|
||||
el.value = t(key);
|
||||
});
|
||||
|
||||
// Custom Event für andere Module
|
||||
window.dispatchEvent(new CustomEvent('languageChanged', {
|
||||
detail: { language: currentLang }
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
* Aktualisiert spezifische UI-Texte (Legacy-Kompatibilität)
|
||||
* Diese Funktion wird von älterem Code verwendet der direkt UI-IDs referenziert
|
||||
*/
|
||||
export function updateUITexts() {
|
||||
// Sidebar
|
||||
const sidebarAreas = document.querySelector('.sidebar h4');
|
||||
if (sidebarAreas) sidebarAreas.textContent = t('sidebar_areas');
|
||||
|
||||
// Breadcrumb / Brand
|
||||
const brandSub = document.querySelector('.brand-sub');
|
||||
if (brandSub) brandSub.textContent = t('brand_sub');
|
||||
|
||||
// Tab Labels
|
||||
const navCompare = document.getElementById('nav-compare');
|
||||
if (navCompare) navCompare.textContent = t('nav_compare');
|
||||
|
||||
const navTiles = document.getElementById('nav-tiles');
|
||||
if (navTiles) navTiles.textContent = t('nav_tiles');
|
||||
|
||||
// Buttons
|
||||
const uploadBtn = document.getElementById('uploadBtn');
|
||||
if (uploadBtn) {
|
||||
const textSpan = uploadBtn.querySelector('.btn-text');
|
||||
if (textSpan) textSpan.textContent = t('btn_upload');
|
||||
}
|
||||
|
||||
const deleteBtn = document.getElementById('deleteBtn');
|
||||
if (deleteBtn) {
|
||||
const textSpan = deleteBtn.querySelector('.btn-text');
|
||||
if (textSpan) textSpan.textContent = t('btn_delete');
|
||||
}
|
||||
|
||||
// Card Headers
|
||||
document.querySelectorAll('.card-header').forEach(header => {
|
||||
const icon = header.querySelector('i');
|
||||
const iconHTML = icon ? icon.outerHTML : '';
|
||||
|
||||
// Original / Cleaned Sections
|
||||
if (header.closest('.scan-section')?.classList.contains('original-scan')) {
|
||||
header.innerHTML = iconHTML + ' ' + t('original_scan');
|
||||
} else if (header.closest('.scan-section')?.classList.contains('cleaned-scan')) {
|
||||
header.innerHTML = iconHTML + ' ' + t('cleaned_version');
|
||||
}
|
||||
});
|
||||
|
||||
// Tiles - MC
|
||||
const mcTile = document.querySelector('.mc-tile');
|
||||
if (mcTile) {
|
||||
const title = mcTile.querySelector('.tile-content h3');
|
||||
if (title) title.textContent = t('mc_title');
|
||||
const desc = mcTile.querySelector('.tile-content p');
|
||||
if (desc) desc.textContent = t('mc_desc');
|
||||
}
|
||||
|
||||
// Tiles - Cloze
|
||||
const clozeTile = document.querySelector('.cloze-tile');
|
||||
if (clozeTile) {
|
||||
const title = clozeTile.querySelector('.tile-content h3');
|
||||
if (title) title.textContent = t('cloze_title');
|
||||
const desc = clozeTile.querySelector('.tile-content p');
|
||||
if (desc) desc.textContent = t('cloze_desc');
|
||||
}
|
||||
|
||||
// Tiles - Q&A
|
||||
const qaTile = document.querySelector('.qa-tile');
|
||||
if (qaTile) {
|
||||
const title = qaTile.querySelector('.tile-content h3');
|
||||
if (title) title.textContent = t('qa_title');
|
||||
const desc = qaTile.querySelector('.tile-content p');
|
||||
if (desc) desc.textContent = t('qa_desc');
|
||||
}
|
||||
|
||||
// Tiles - Mindmap
|
||||
const mindmapTile = document.querySelector('.mindmap-tile');
|
||||
if (mindmapTile) {
|
||||
const title = mindmapTile.querySelector('.tile-content h3');
|
||||
if (title) title.textContent = t('mindmap_title');
|
||||
const desc = mindmapTile.querySelector('.tile-content p');
|
||||
if (desc) desc.textContent = t('mindmap_desc');
|
||||
}
|
||||
|
||||
// Footer
|
||||
const imprintLink = document.querySelector('footer a[href*="imprint"]');
|
||||
if (imprintLink) imprintLink.textContent = t('imprint');
|
||||
|
||||
const privacyLink = document.querySelector('footer a[href*="privacy"]');
|
||||
if (privacyLink) privacyLink.textContent = t('privacy');
|
||||
|
||||
const contactLink = document.querySelector('footer a[href*="contact"]');
|
||||
if (contactLink) contactLink.textContent = t('contact');
|
||||
|
||||
// Process Button
|
||||
const fullProcessBtn = document.getElementById('fullProcessBtn');
|
||||
if (fullProcessBtn) {
|
||||
const textSpan = fullProcessBtn.querySelector('.btn-text');
|
||||
if (textSpan) textSpan.textContent = t('btn_full_process');
|
||||
}
|
||||
|
||||
// Status Bar
|
||||
const statusText = document.getElementById('statusText');
|
||||
if (statusText && statusText.textContent === 'Bereit' || statusText?.textContent === 'Ready') {
|
||||
statusText.textContent = t('status_ready');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialisiert das Sprachwahl-UI
|
||||
* @param {string} containerId - ID des Containers für Sprachauswahl
|
||||
*/
|
||||
export function initLanguageSelector(containerId = 'language-selector') {
|
||||
const container = document.getElementById(containerId);
|
||||
if (!container) return;
|
||||
|
||||
// Dropdown erstellen
|
||||
container.innerHTML = `
|
||||
<select id="language-select" class="language-select">
|
||||
${Object.entries(availableLanguages).map(([code, name]) =>
|
||||
`<option value="${code}" ${code === currentLang ? 'selected' : ''}>${name}</option>`
|
||||
).join('')}
|
||||
</select>
|
||||
`;
|
||||
|
||||
// Event Handler
|
||||
const select = document.getElementById('language-select');
|
||||
if (select) {
|
||||
select.addEventListener('change', (e) => {
|
||||
setLanguage(e.target.value);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Formatiert einen Status-Text mit Platzhaltern
|
||||
* @param {string} key - Übersetzungsschlüssel
|
||||
* @param {Object} vars - Variablen zum Einsetzen
|
||||
* @returns {string} - Formatierter Text
|
||||
*/
|
||||
export function tFormat(key, vars = {}) {
|
||||
let text = t(key);
|
||||
Object.entries(vars).forEach(([k, v]) => {
|
||||
text = text.replace(new RegExp(`\\{${k}\\}`, 'g'), v);
|
||||
});
|
||||
return text;
|
||||
}
|
||||
|
||||
// Re-export für Convenience
|
||||
export { translations, rtlLanguages, defaultLanguage, availableLanguages };
|
||||
517
backend/frontend/static/js/modules/learning-units-module.js
Normal file
517
backend/frontend/static/js/modules/learning-units-module.js
Normal file
@@ -0,0 +1,517 @@
|
||||
/**
|
||||
* BreakPilot Studio - Learning Units Module
|
||||
*
|
||||
* Lerneinheiten-Verwaltung:
|
||||
* - Laden, Erstellen, Löschen von Lerneinheiten
|
||||
* - Zuordnung von Arbeitsblättern zu Lerneinheiten
|
||||
* - Filter für Lerneinheiten-spezifische Ansicht
|
||||
*
|
||||
* Refactored: 2026-01-19
|
||||
*/
|
||||
|
||||
import { t } from './i18n.js';
|
||||
import { setStatus } from './api-helpers.js';
|
||||
|
||||
// State
|
||||
let units = [];
|
||||
let currentUnitId = null;
|
||||
let showOnlyUnitFiles = false;
|
||||
|
||||
// DOM References
|
||||
let unitListEl = null;
|
||||
let unitHeading1 = null;
|
||||
let unitHeading2 = null;
|
||||
let btnAddUnit = null;
|
||||
let btnToggleFilter = null;
|
||||
let btnAttachCurrentToLu = null;
|
||||
|
||||
// Form Inputs
|
||||
let unitStudentInput = null;
|
||||
let unitSubjectInput = null;
|
||||
let unitGradeInput = null;
|
||||
let unitTitleInput = null;
|
||||
|
||||
// Callbacks für File-Manager Integration
|
||||
let getEingangFilesCallback = null;
|
||||
let getAllEingangFilesCallback = null;
|
||||
let getAllWorksheetPairsCallback = null;
|
||||
let setFilteredDataCallback = null;
|
||||
let renderListCallback = null;
|
||||
let renderPreviewCallback = null;
|
||||
let getCurrentWorksheetBasenameCallback = null;
|
||||
|
||||
/**
|
||||
* Initialisiert das Learning Units Modul
|
||||
* @param {Object} options - Konfiguration
|
||||
*/
|
||||
export function initLearningUnitsModule(options = {}) {
|
||||
// DOM-Referenzen
|
||||
unitListEl = document.getElementById('unit-list') || options.unitListEl;
|
||||
unitHeading1 = document.getElementById('unit-heading-1') || options.unitHeading1;
|
||||
unitHeading2 = document.getElementById('unit-heading-2') || options.unitHeading2;
|
||||
btnAddUnit = document.getElementById('btn-add-unit') || options.btnAddUnit;
|
||||
btnToggleFilter = document.getElementById('btn-toggle-filter') || options.btnToggleFilter;
|
||||
btnAttachCurrentToLu = document.getElementById('btn-attach-current-to-lu') || options.btnAttachCurrentToLu;
|
||||
|
||||
// Form Inputs
|
||||
unitStudentInput = document.getElementById('unit-student') || options.unitStudentInput;
|
||||
unitSubjectInput = document.getElementById('unit-subject') || options.unitSubjectInput;
|
||||
unitGradeInput = document.getElementById('unit-grade') || options.unitGradeInput;
|
||||
unitTitleInput = document.getElementById('unit-title') || options.unitTitleInput;
|
||||
|
||||
// Callbacks
|
||||
getEingangFilesCallback = options.getEingangFiles || (() => []);
|
||||
getAllEingangFilesCallback = options.getAllEingangFiles || (() => []);
|
||||
getAllWorksheetPairsCallback = options.getAllWorksheetPairs || (() => ({}));
|
||||
setFilteredDataCallback = options.setFilteredData || (() => {});
|
||||
renderListCallback = options.renderList || (() => {});
|
||||
renderPreviewCallback = options.renderPreview || (() => {});
|
||||
getCurrentWorksheetBasenameCallback = options.getCurrentWorksheetBasename || (() => null);
|
||||
|
||||
// Event-Listener
|
||||
if (btnAddUnit) {
|
||||
btnAddUnit.addEventListener('click', (ev) => {
|
||||
ev.preventDefault();
|
||||
addUnitFromForm();
|
||||
});
|
||||
}
|
||||
|
||||
if (btnAttachCurrentToLu) {
|
||||
btnAttachCurrentToLu.addEventListener('click', (ev) => {
|
||||
ev.preventDefault();
|
||||
attachCurrentWorksheetToUnit();
|
||||
});
|
||||
}
|
||||
|
||||
if (btnToggleFilter) {
|
||||
btnToggleFilter.addEventListener('click', () => {
|
||||
showOnlyUnitFiles = !showOnlyUnitFiles;
|
||||
if (showOnlyUnitFiles) {
|
||||
btnToggleFilter.textContent = t('only_unit') || 'Nur Lerneinheit';
|
||||
btnToggleFilter.classList.add('btn-primary');
|
||||
} else {
|
||||
btnToggleFilter.textContent = t('all_files') || 'Alle Dateien';
|
||||
btnToggleFilter.classList.remove('btn-primary');
|
||||
}
|
||||
applyUnitFilter();
|
||||
});
|
||||
}
|
||||
|
||||
// Initial laden
|
||||
loadLearningUnits();
|
||||
}
|
||||
|
||||
/**
|
||||
* Aktualisiert die Überschrift mit dem Namen der aktuellen Lerneinheit
|
||||
* @param {Object|null} unit - Lerneinheit oder null
|
||||
*/
|
||||
function updateUnitHeading(unit = null) {
|
||||
if (!unit && currentUnitId && units && units.length) {
|
||||
unit = units.find((u) => u.id === currentUnitId) || null;
|
||||
}
|
||||
|
||||
let text = t('no_unit_selected') || 'Keine Lerneinheit ausgewählt';
|
||||
if (unit) {
|
||||
const name = unit.label || unit.title || t('learning_unit') || 'Lerneinheit';
|
||||
text = (t('learning_unit') || 'Lerneinheit') + ': ' + name;
|
||||
}
|
||||
|
||||
if (unitHeading1) unitHeading1.textContent = text;
|
||||
if (unitHeading2) unitHeading2.textContent = text;
|
||||
}
|
||||
|
||||
/**
|
||||
* Wendet den Lerneinheiten-Filter an
|
||||
* Zeigt nur Dateien der aktuellen Lerneinheit oder alle Dateien
|
||||
*/
|
||||
export function applyUnitFilter() {
|
||||
let unit = null;
|
||||
if (currentUnitId && units && units.length) {
|
||||
unit = units.find((u) => u.id === currentUnitId) || null;
|
||||
}
|
||||
|
||||
const allEingangFiles = getAllEingangFilesCallback();
|
||||
const allWorksheetPairs = getAllWorksheetPairsCallback();
|
||||
|
||||
// Wenn Filter deaktiviert ODER keine Lerneinheit ausgewählt -> alle Dateien anzeigen
|
||||
if (!showOnlyUnitFiles || !unit || !Array.isArray(unit.worksheet_files) || unit.worksheet_files.length === 0) {
|
||||
setFilteredDataCallback(allEingangFiles.slice(), { ...allWorksheetPairs }, 0);
|
||||
renderListCallback();
|
||||
renderPreviewCallback();
|
||||
updateUnitHeading(unit);
|
||||
return;
|
||||
}
|
||||
|
||||
// Filter aktiv: nur Dateien der aktuellen Lerneinheit anzeigen
|
||||
const allowed = new Set(unit.worksheet_files || []);
|
||||
const filteredFiles = allEingangFiles.filter((f) => allowed.has(f));
|
||||
|
||||
const filteredPairs = {};
|
||||
Object.keys(allWorksheetPairs).forEach((key) => {
|
||||
if (allowed.has(key)) {
|
||||
filteredPairs[key] = allWorksheetPairs[key];
|
||||
}
|
||||
});
|
||||
|
||||
setFilteredDataCallback(filteredFiles, filteredPairs, 0);
|
||||
renderListCallback();
|
||||
renderPreviewCallback();
|
||||
updateUnitHeading(unit);
|
||||
}
|
||||
|
||||
/**
|
||||
* Lädt alle Lerneinheiten vom Server
|
||||
*/
|
||||
export async function loadLearningUnits() {
|
||||
try {
|
||||
const resp = await fetch('/api/learning-units/');
|
||||
if (!resp.ok) {
|
||||
console.error('Fehler beim Laden der Lerneinheiten', resp.status);
|
||||
return;
|
||||
}
|
||||
units = await resp.json();
|
||||
if (units.length && !currentUnitId) {
|
||||
currentUnitId = units[0].id;
|
||||
}
|
||||
renderUnits();
|
||||
applyUnitFilter();
|
||||
} catch (e) {
|
||||
console.error('Netzwerkfehler beim Laden der Lerneinheiten', e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Rendert die Lerneinheiten-Liste
|
||||
*/
|
||||
export function renderUnits() {
|
||||
if (!unitListEl) return;
|
||||
|
||||
unitListEl.innerHTML = '';
|
||||
|
||||
if (!units.length) {
|
||||
const li = document.createElement('li');
|
||||
li.className = 'unit-item';
|
||||
li.textContent = t('no_units_yet') || 'Noch keine Lerneinheiten angelegt.';
|
||||
unitListEl.appendChild(li);
|
||||
updateUnitHeading(null);
|
||||
return;
|
||||
}
|
||||
|
||||
units.forEach((u) => {
|
||||
const li = document.createElement('li');
|
||||
li.className = 'unit-item';
|
||||
if (u.id === currentUnitId) {
|
||||
li.classList.add('active');
|
||||
}
|
||||
|
||||
const contentDiv = document.createElement('div');
|
||||
contentDiv.style.flex = '1';
|
||||
contentDiv.style.minWidth = '0';
|
||||
|
||||
const titleEl = document.createElement('div');
|
||||
titleEl.textContent = u.label || u.title || t('learning_unit') || 'Lerneinheit';
|
||||
|
||||
const metaEl = document.createElement('div');
|
||||
metaEl.className = 'unit-item-meta';
|
||||
|
||||
const metaParts = [];
|
||||
if (u.meta) {
|
||||
metaParts.push(u.meta);
|
||||
}
|
||||
if (Array.isArray(u.worksheet_files)) {
|
||||
metaParts.push((t('worksheets') || 'Blätter') + ': ' + u.worksheet_files.length);
|
||||
}
|
||||
|
||||
metaEl.textContent = metaParts.join(' · ');
|
||||
|
||||
contentDiv.appendChild(titleEl);
|
||||
contentDiv.appendChild(metaEl);
|
||||
|
||||
// Delete-Button
|
||||
const deleteBtn = document.createElement('span');
|
||||
deleteBtn.textContent = '🗑️';
|
||||
deleteBtn.style.cursor = 'pointer';
|
||||
deleteBtn.style.fontSize = '12px';
|
||||
deleteBtn.style.color = '#ef4444';
|
||||
deleteBtn.title = t('delete_unit') || 'Lerneinheit löschen';
|
||||
deleteBtn.addEventListener('click', async (ev) => {
|
||||
ev.stopPropagation();
|
||||
const confirmMsg = t('confirm_delete_unit') || 'Lerneinheit "{name}" wirklich löschen? Diese Aktion kann nicht rückgängig gemacht werden.';
|
||||
const ok = confirm(confirmMsg.replace('{name}', u.label || u.title || ''));
|
||||
if (!ok) return;
|
||||
await deleteLearningUnit(u.id);
|
||||
});
|
||||
|
||||
li.appendChild(contentDiv);
|
||||
li.appendChild(deleteBtn);
|
||||
|
||||
li.addEventListener('click', () => {
|
||||
currentUnitId = u.id;
|
||||
renderUnits();
|
||||
applyUnitFilter();
|
||||
|
||||
// Event für andere Module
|
||||
window.dispatchEvent(new CustomEvent('unitSelected', { detail: { unitId: u.id, unit: u } }));
|
||||
});
|
||||
|
||||
unitListEl.appendChild(li);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Erstellt eine neue Lerneinheit aus dem Formular
|
||||
*/
|
||||
export async function addUnitFromForm() {
|
||||
const student = (unitStudentInput && unitStudentInput.value || '').trim();
|
||||
const subject = (unitSubjectInput && unitSubjectInput.value || '').trim();
|
||||
const grade = (unitGradeInput && unitGradeInput.value || '').trim();
|
||||
const title = (unitTitleInput && unitTitleInput.value || '').trim();
|
||||
|
||||
if (!student && !subject && !title) {
|
||||
alert(t('unit_form_empty') || 'Bitte mindestens einen Wert (Schüler/in, Fach oder Thema) eintragen.');
|
||||
return;
|
||||
}
|
||||
|
||||
const payload = {
|
||||
student,
|
||||
subject,
|
||||
title,
|
||||
grade,
|
||||
};
|
||||
|
||||
try {
|
||||
const resp = await fetch('/api/learning-units/', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(payload),
|
||||
});
|
||||
if (!resp.ok) {
|
||||
console.error('Fehler beim Anlegen der Lerneinheit', resp.status);
|
||||
alert(t('unit_create_error') || 'Lerneinheit konnte nicht angelegt werden.');
|
||||
return;
|
||||
}
|
||||
|
||||
const created = await resp.json();
|
||||
units.push(created);
|
||||
currentUnitId = created.id;
|
||||
|
||||
// Formular leeren
|
||||
if (unitStudentInput) unitStudentInput.value = '';
|
||||
if (unitSubjectInput) unitSubjectInput.value = '';
|
||||
if (unitTitleInput) unitTitleInput.value = '';
|
||||
if (unitGradeInput) unitGradeInput.value = '';
|
||||
|
||||
renderUnits();
|
||||
applyUnitFilter();
|
||||
|
||||
// Event für andere Module
|
||||
window.dispatchEvent(new CustomEvent('unitCreated', { detail: { unit: created } }));
|
||||
} catch (e) {
|
||||
console.error('Netzwerkfehler beim Anlegen der Lerneinheit', e);
|
||||
alert(t('network_error') || 'Netzwerkfehler beim Anlegen der Lerneinheit.');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Ordnet das aktuelle Arbeitsblatt der aktuellen Lerneinheit zu
|
||||
*/
|
||||
export async function attachCurrentWorksheetToUnit() {
|
||||
if (!currentUnitId) {
|
||||
alert(t('select_unit_first') || 'Bitte zuerst eine Lerneinheit auswählen oder anlegen.');
|
||||
return;
|
||||
}
|
||||
|
||||
const basename = getCurrentWorksheetBasenameCallback();
|
||||
if (!basename) {
|
||||
alert(t('select_worksheet_first') || 'Bitte zuerst ein Arbeitsblatt im linken Bereich auswählen.');
|
||||
return;
|
||||
}
|
||||
|
||||
const payload = { worksheet_files: [basename] };
|
||||
|
||||
try {
|
||||
const resp = await fetch(`/api/learning-units/${currentUnitId}/attach-worksheets`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(payload),
|
||||
});
|
||||
if (!resp.ok) {
|
||||
console.error('Fehler beim Zuordnen des Arbeitsblatts', resp.status);
|
||||
alert(t('attach_error') || 'Arbeitsblatt konnte nicht zugeordnet werden.');
|
||||
return;
|
||||
}
|
||||
|
||||
const updated = await resp.json();
|
||||
const idx = units.findIndex((u) => u.id === updated.id);
|
||||
if (idx !== -1) {
|
||||
units[idx] = updated;
|
||||
}
|
||||
renderUnits();
|
||||
applyUnitFilter();
|
||||
|
||||
// Event für andere Module
|
||||
window.dispatchEvent(new CustomEvent('worksheetAttached', { detail: { unitId: currentUnitId, filename: basename } }));
|
||||
} catch (e) {
|
||||
console.error('Netzwerkfehler beim Zuordnen des Arbeitsblatts', e);
|
||||
alert(t('network_error') || 'Netzwerkfehler beim Zuordnen des Arbeitsblatts.');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Entfernt ein Arbeitsblatt aus der aktuellen Lerneinheit
|
||||
* @param {string} filename - Dateiname des Arbeitsblatts
|
||||
*/
|
||||
export async function removeWorksheetFromCurrentUnit(filename) {
|
||||
if (!currentUnitId) {
|
||||
alert(t('select_unit_first') || 'Bitte zuerst eine Lerneinheit auswählen.');
|
||||
return;
|
||||
}
|
||||
if (!filename) {
|
||||
alert(t('error_no_filename') || 'Fehler: kein Dateiname übergeben.');
|
||||
return;
|
||||
}
|
||||
|
||||
const payload = { worksheet_file: filename };
|
||||
|
||||
try {
|
||||
const resp = await fetch(`/api/learning-units/${currentUnitId}/remove-worksheet`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(payload),
|
||||
});
|
||||
if (!resp.ok) {
|
||||
console.error('Fehler beim Entfernen des Arbeitsblatts', resp.status);
|
||||
alert(t('remove_worksheet_error') || 'Arbeitsblatt konnte nicht aus der Lerneinheit entfernt werden.');
|
||||
return;
|
||||
}
|
||||
|
||||
const updated = await resp.json();
|
||||
const idx = units.findIndex((u) => u.id === updated.id);
|
||||
if (idx !== -1) {
|
||||
units[idx] = updated;
|
||||
}
|
||||
renderUnits();
|
||||
applyUnitFilter();
|
||||
|
||||
// Event für andere Module
|
||||
window.dispatchEvent(new CustomEvent('worksheetRemoved', { detail: { unitId: currentUnitId, filename } }));
|
||||
} catch (e) {
|
||||
console.error('Netzwerkfehler beim Entfernen des Arbeitsblatts', e);
|
||||
alert(t('network_error') || 'Netzwerkfehler beim Entfernen des Arbeitsblatts.');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Löscht eine Lerneinheit
|
||||
* @param {string} unitId - ID der Lerneinheit
|
||||
*/
|
||||
export async function deleteLearningUnit(unitId) {
|
||||
if (!unitId) {
|
||||
alert(t('error_no_unit_id') || 'Fehler: keine Lerneinheit-ID übergeben.');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
setStatus(t('deleting_unit') || 'Lösche Lerneinheit …', '', 'busy');
|
||||
|
||||
const resp = await fetch(`/api/learning-units/${unitId}`, {
|
||||
method: 'DELETE',
|
||||
});
|
||||
|
||||
if (!resp.ok) {
|
||||
console.error('Fehler beim Löschen der Lerneinheit', resp.status);
|
||||
setStatus(t('delete_error') || 'Fehler beim Löschen', '', 'error');
|
||||
alert(t('unit_delete_error') || 'Lerneinheit konnte nicht gelöscht werden.');
|
||||
return;
|
||||
}
|
||||
|
||||
const result = await resp.json();
|
||||
if (result.status === 'deleted') {
|
||||
setStatus(t('unit_deleted') || 'Lerneinheit gelöscht', '');
|
||||
|
||||
// Lerneinheit aus der lokalen Liste entfernen
|
||||
units = units.filter((u) => u.id !== unitId);
|
||||
|
||||
// Wenn die gelöschte Einheit ausgewählt war, Auswahl zurücksetzen
|
||||
if (currentUnitId === unitId) {
|
||||
currentUnitId = units.length > 0 ? units[0].id : null;
|
||||
}
|
||||
|
||||
renderUnits();
|
||||
applyUnitFilter();
|
||||
|
||||
// Event für andere Module
|
||||
window.dispatchEvent(new CustomEvent('unitDeleted', { detail: { unitId } }));
|
||||
} else {
|
||||
setStatus(t('error') || 'Fehler', t('unknown_error') || 'Unbekannter Fehler', 'error');
|
||||
alert(t('unit_delete_error') || 'Fehler beim Löschen der Lerneinheit.');
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Netzwerkfehler beim Löschen der Lerneinheit', e);
|
||||
setStatus(t('network_error') || 'Netzwerkfehler', String(e), 'error');
|
||||
alert(t('network_error') || 'Netzwerkfehler beim Löschen der Lerneinheit.');
|
||||
}
|
||||
}
|
||||
|
||||
// === Getter und Setter ===
|
||||
|
||||
/**
|
||||
* Gibt alle Lerneinheiten zurück
|
||||
* @returns {Array}
|
||||
*/
|
||||
export function getUnits() {
|
||||
return units;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gibt die aktuelle Lerneinheit-ID zurück
|
||||
* @returns {string|null}
|
||||
*/
|
||||
export function getCurrentUnitId() {
|
||||
return currentUnitId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Setzt die aktuelle Lerneinheit-ID
|
||||
* @param {string|null} unitId
|
||||
*/
|
||||
export function setCurrentUnitId(unitId) {
|
||||
currentUnitId = unitId;
|
||||
renderUnits();
|
||||
applyUnitFilter();
|
||||
}
|
||||
|
||||
/**
|
||||
* Gibt zurück, ob der Filter aktiv ist
|
||||
* @returns {boolean}
|
||||
*/
|
||||
export function getShowOnlyUnitFiles() {
|
||||
return showOnlyUnitFiles;
|
||||
}
|
||||
|
||||
/**
|
||||
* Setzt den Filter-Status
|
||||
* @param {boolean} value
|
||||
*/
|
||||
export function setShowOnlyUnitFiles(value) {
|
||||
showOnlyUnitFiles = value;
|
||||
if (btnToggleFilter) {
|
||||
if (showOnlyUnitFiles) {
|
||||
btnToggleFilter.textContent = t('only_unit') || 'Nur Lerneinheit';
|
||||
btnToggleFilter.classList.add('btn-primary');
|
||||
} else {
|
||||
btnToggleFilter.textContent = t('all_files') || 'Alle Dateien';
|
||||
btnToggleFilter.classList.remove('btn-primary');
|
||||
}
|
||||
}
|
||||
applyUnitFilter();
|
||||
}
|
||||
|
||||
/**
|
||||
* Gibt die aktuelle Lerneinheit zurück
|
||||
* @returns {Object|null}
|
||||
*/
|
||||
export function getCurrentUnit() {
|
||||
if (!currentUnitId || !units.length) return null;
|
||||
return units.find((u) => u.id === currentUnitId) || null;
|
||||
}
|
||||
234
backend/frontend/static/js/modules/lightbox.js
Normal file
234
backend/frontend/static/js/modules/lightbox.js
Normal file
@@ -0,0 +1,234 @@
|
||||
/**
|
||||
* BreakPilot Studio - Lightbox Module
|
||||
*
|
||||
* Vollbild-Bildvorschau und Modal-Funktionen:
|
||||
* - Lightbox für Arbeitsblatt-Vorschauen
|
||||
* - Keyboard-Navigation (Escape zum Schließen)
|
||||
* - Click-outside zum Schließen
|
||||
*
|
||||
* Refactored: 2026-01-19
|
||||
*/
|
||||
|
||||
// DOM-Referenzen
|
||||
let lightboxEl = null;
|
||||
let lightboxImg = null;
|
||||
let lightboxCaption = null;
|
||||
let lightboxClose = null;
|
||||
|
||||
// Callback für Close-Event
|
||||
let onCloseCallback = null;
|
||||
|
||||
/**
|
||||
* Initialisiert die Lightbox
|
||||
* Sucht nach Standard-IDs oder erstellt das DOM
|
||||
*/
|
||||
export function initLightbox() {
|
||||
lightboxEl = document.getElementById('lightbox');
|
||||
lightboxImg = document.getElementById('lightbox-img');
|
||||
lightboxCaption = document.getElementById('lightbox-caption');
|
||||
lightboxClose = document.getElementById('lightbox-close');
|
||||
|
||||
// Falls keine Lightbox im DOM, erstelle sie
|
||||
if (!lightboxEl) {
|
||||
createLightboxDOM();
|
||||
}
|
||||
|
||||
// Event-Listener
|
||||
if (lightboxClose) {
|
||||
lightboxClose.addEventListener('click', closeLightbox);
|
||||
}
|
||||
|
||||
if (lightboxEl) {
|
||||
lightboxEl.addEventListener('click', (ev) => {
|
||||
// Schließen bei Klick auf Hintergrund
|
||||
if (ev.target === lightboxEl) {
|
||||
closeLightbox();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Escape-Taste
|
||||
document.addEventListener('keydown', (ev) => {
|
||||
if (ev.key === 'Escape' && isLightboxOpen()) {
|
||||
closeLightbox();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Erstellt das Lightbox-DOM dynamisch
|
||||
*/
|
||||
function createLightboxDOM() {
|
||||
lightboxEl = document.createElement('div');
|
||||
lightboxEl.id = 'lightbox';
|
||||
lightboxEl.className = 'lightbox hidden';
|
||||
lightboxEl.innerHTML = `
|
||||
<div class="lightbox-content">
|
||||
<button class="lightbox-close" id="lightbox-close">Schließen ✕</button>
|
||||
<img id="lightbox-img" class="lightbox-img" src="" alt="Vorschau">
|
||||
<div id="lightbox-caption" class="lightbox-caption"></div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
document.body.appendChild(lightboxEl);
|
||||
|
||||
// Referenzen aktualisieren
|
||||
lightboxImg = document.getElementById('lightbox-img');
|
||||
lightboxCaption = document.getElementById('lightbox-caption');
|
||||
lightboxClose = document.getElementById('lightbox-close');
|
||||
|
||||
// CSS injizieren falls nicht vorhanden
|
||||
if (!document.getElementById('lightbox-styles')) {
|
||||
const style = document.createElement('style');
|
||||
style.id = 'lightbox-styles';
|
||||
style.textContent = `
|
||||
.lightbox {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: rgba(0, 0, 0, 0.9);
|
||||
z-index: 10000;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
opacity: 1;
|
||||
transition: opacity 0.3s ease;
|
||||
}
|
||||
.lightbox.hidden {
|
||||
display: none;
|
||||
opacity: 0;
|
||||
}
|
||||
.lightbox-content {
|
||||
position: relative;
|
||||
max-width: 90%;
|
||||
max-height: 90%;
|
||||
}
|
||||
.lightbox-img {
|
||||
max-width: 100%;
|
||||
max-height: 85vh;
|
||||
object-fit: contain;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 4px 24px rgba(0, 0, 0, 0.5);
|
||||
}
|
||||
.lightbox-close {
|
||||
position: absolute;
|
||||
top: -40px;
|
||||
right: 0;
|
||||
background: transparent;
|
||||
border: none;
|
||||
color: white;
|
||||
font-size: 14px;
|
||||
cursor: pointer;
|
||||
padding: 8px 16px;
|
||||
border-radius: 4px;
|
||||
transition: background 0.2s;
|
||||
}
|
||||
.lightbox-close:hover {
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
.lightbox-caption {
|
||||
color: white;
|
||||
text-align: center;
|
||||
margin-top: 12px;
|
||||
font-size: 14px;
|
||||
}
|
||||
`;
|
||||
document.head.appendChild(style);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Öffnet die Lightbox mit einem Bild
|
||||
* @param {string} src - Bild-URL
|
||||
* @param {string} [caption] - Optionale Bildunterschrift
|
||||
*/
|
||||
export function openLightbox(src, caption = '') {
|
||||
if (!src) {
|
||||
console.warn('openLightbox: No image source provided');
|
||||
return;
|
||||
}
|
||||
|
||||
// Lazy-Init falls noch nicht initialisiert
|
||||
if (!lightboxEl) {
|
||||
initLightbox();
|
||||
}
|
||||
|
||||
if (lightboxImg) {
|
||||
lightboxImg.src = src;
|
||||
lightboxImg.alt = caption || 'Vorschau';
|
||||
}
|
||||
|
||||
if (lightboxCaption) {
|
||||
lightboxCaption.textContent = caption;
|
||||
}
|
||||
|
||||
if (lightboxEl) {
|
||||
lightboxEl.classList.remove('hidden');
|
||||
// Body-Scroll verhindern
|
||||
document.body.style.overflow = 'hidden';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Schließt die Lightbox
|
||||
*/
|
||||
export function closeLightbox() {
|
||||
if (lightboxEl) {
|
||||
lightboxEl.classList.add('hidden');
|
||||
// Body-Scroll wiederherstellen
|
||||
document.body.style.overflow = '';
|
||||
}
|
||||
|
||||
if (lightboxImg) {
|
||||
lightboxImg.src = '';
|
||||
}
|
||||
|
||||
// Callback ausführen
|
||||
if (onCloseCallback) {
|
||||
onCloseCallback();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Prüft ob die Lightbox geöffnet ist
|
||||
* @returns {boolean}
|
||||
*/
|
||||
export function isLightboxOpen() {
|
||||
return lightboxEl && !lightboxEl.classList.contains('hidden');
|
||||
}
|
||||
|
||||
/**
|
||||
* Setzt einen Callback für das Close-Event
|
||||
* @param {function} callback
|
||||
*/
|
||||
export function onClose(callback) {
|
||||
onCloseCallback = callback;
|
||||
}
|
||||
|
||||
/**
|
||||
* Wechselt das Bild in der offenen Lightbox
|
||||
* @param {string} src - Neue Bild-URL
|
||||
* @param {string} [caption] - Neue Bildunterschrift
|
||||
*/
|
||||
export function changeLightboxImage(src, caption = '') {
|
||||
if (lightboxImg) {
|
||||
lightboxImg.src = src;
|
||||
lightboxImg.alt = caption || 'Vorschau';
|
||||
}
|
||||
|
||||
if (lightboxCaption) {
|
||||
lightboxCaption.textContent = caption;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Setzt den Text des Close-Buttons
|
||||
* @param {string} text - Der neue Text
|
||||
*/
|
||||
export function setCloseButtonText(text) {
|
||||
if (lightboxClose) {
|
||||
lightboxClose.textContent = text;
|
||||
}
|
||||
}
|
||||
474
backend/frontend/static/js/modules/mc-module.js
Normal file
474
backend/frontend/static/js/modules/mc-module.js
Normal file
@@ -0,0 +1,474 @@
|
||||
/**
|
||||
* BreakPilot Studio - Multiple Choice Module
|
||||
*
|
||||
* Multiple Choice Quiz-Funktionalität:
|
||||
* - Generierung von MC-Fragen aus Arbeitsblättern
|
||||
* - Interaktives Quiz mit Auswertung
|
||||
* - Druckfunktion (mit/ohne Lösungen)
|
||||
*
|
||||
* Refactored: 2026-01-19
|
||||
*/
|
||||
|
||||
import { t } from './i18n.js';
|
||||
import { setStatus } from './api-helpers.js';
|
||||
|
||||
// State
|
||||
let currentMcData = null;
|
||||
let mcAnswers = {};
|
||||
|
||||
// DOM References
|
||||
let mcPreview = null;
|
||||
let mcBadge = null;
|
||||
let mcModal = null;
|
||||
let mcModalBody = null;
|
||||
let mcModalClose = null;
|
||||
let btnMcGenerate = null;
|
||||
let btnMcShow = null;
|
||||
let btnMcPrint = null;
|
||||
|
||||
// Callback für aktuelle Datei
|
||||
let getCurrentFileCallback = null;
|
||||
let getEingangFilesCallback = null;
|
||||
let getCurrentIndexCallback = null;
|
||||
|
||||
/**
|
||||
* Initialisiert das Multiple Choice Modul
|
||||
* @param {Object} options - Konfiguration
|
||||
*/
|
||||
export function initMcModule(options = {}) {
|
||||
getCurrentFileCallback = options.getCurrentFile || (() => null);
|
||||
getEingangFilesCallback = options.getEingangFiles || (() => []);
|
||||
getCurrentIndexCallback = options.getCurrentIndex || (() => 0);
|
||||
|
||||
// DOM References
|
||||
mcPreview = document.getElementById('mc-preview') || options.previewEl;
|
||||
mcBadge = document.getElementById('mc-badge') || options.badgeEl;
|
||||
mcModal = document.getElementById('mc-modal') || options.modalEl;
|
||||
mcModalBody = document.getElementById('mc-modal-body') || options.modalBodyEl;
|
||||
mcModalClose = document.getElementById('mc-modal-close') || options.modalCloseEl;
|
||||
btnMcGenerate = document.getElementById('btn-mc-generate') || options.generateBtn;
|
||||
btnMcShow = document.getElementById('btn-mc-show') || options.showBtn;
|
||||
btnMcPrint = document.getElementById('btn-mc-print') || options.printBtn;
|
||||
|
||||
// Event-Listener
|
||||
if (btnMcGenerate) {
|
||||
btnMcGenerate.addEventListener('click', generateMcQuestions);
|
||||
}
|
||||
|
||||
if (btnMcShow) {
|
||||
btnMcShow.addEventListener('click', openMcModal);
|
||||
}
|
||||
|
||||
if (btnMcPrint) {
|
||||
btnMcPrint.addEventListener('click', openMcPrintDialog);
|
||||
}
|
||||
|
||||
if (mcModalClose) {
|
||||
mcModalClose.addEventListener('click', closeMcModal);
|
||||
}
|
||||
|
||||
if (mcModal) {
|
||||
mcModal.addEventListener('click', (ev) => {
|
||||
if (ev.target === mcModal) {
|
||||
closeMcModal();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Event für Datei-Wechsel
|
||||
window.addEventListener('fileSelected', () => {
|
||||
loadMcPreviewForCurrent();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Generiert MC-Fragen für alle Arbeitsblätter
|
||||
*/
|
||||
export async function generateMcQuestions() {
|
||||
try {
|
||||
setStatus(t('mc_generating') || 'Generiere MC-Fragen …', t('ai_working') || 'Bitte warten, KI arbeitet.', 'busy');
|
||||
if (mcBadge) mcBadge.textContent = t('generating') || 'Generiert...';
|
||||
|
||||
const resp = await fetch('/api/generate-mc', { method: 'POST' });
|
||||
if (!resp.ok) {
|
||||
throw new Error('HTTP ' + resp.status);
|
||||
}
|
||||
|
||||
const result = await resp.json();
|
||||
if (result.status === 'OK' && result.generated.length > 0) {
|
||||
setStatus(t('mc_generated') || 'MC-Fragen generiert', result.generated.length + ' ' + (t('files_created') || 'Dateien erstellt'));
|
||||
if (mcBadge) mcBadge.textContent = t('ready') || 'Fertig';
|
||||
if (btnMcShow) btnMcShow.style.display = 'inline-block';
|
||||
if (btnMcPrint) btnMcPrint.style.display = 'inline-block';
|
||||
|
||||
// Lade die erste MC-Datei für Vorschau
|
||||
await loadMcPreviewForCurrent();
|
||||
} else if (result.errors && result.errors.length > 0) {
|
||||
setStatus(t('mc_error') || 'Fehler bei MC-Generierung', result.errors[0].error, 'error');
|
||||
if (mcBadge) mcBadge.textContent = t('error') || 'Fehler';
|
||||
} else {
|
||||
setStatus(t('no_mc_generated') || 'Keine MC-Fragen generiert', t('analysis_missing') || 'Möglicherweise fehlen Analyse-Daten.', 'error');
|
||||
if (mcBadge) mcBadge.textContent = t('ready') || 'Bereit';
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('MC-Generierung fehlgeschlagen:', e);
|
||||
setStatus(t('mc_error') || 'Fehler bei MC-Generierung', String(e), 'error');
|
||||
if (mcBadge) mcBadge.textContent = t('error') || 'Fehler';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Lädt MC-Vorschau für die aktuelle Datei
|
||||
*/
|
||||
export async function loadMcPreviewForCurrent() {
|
||||
const eingangFiles = getEingangFilesCallback();
|
||||
const currentIndex = getCurrentIndexCallback();
|
||||
|
||||
if (!eingangFiles.length) {
|
||||
if (mcPreview) mcPreview.innerHTML = '<div style="font-size:11px;color:var(--bp-text-muted);">' + (t('no_worksheets') || 'Keine Arbeitsblätter vorhanden.') + '</div>';
|
||||
return;
|
||||
}
|
||||
|
||||
const currentFile = eingangFiles[currentIndex];
|
||||
if (!currentFile) return;
|
||||
|
||||
try {
|
||||
const resp = await fetch('/api/mc-data/' + encodeURIComponent(currentFile));
|
||||
const result = await resp.json();
|
||||
|
||||
if (result.status === 'OK' && result.data) {
|
||||
currentMcData = result.data;
|
||||
renderMcPreview(result.data);
|
||||
if (btnMcShow) btnMcShow.style.display = 'inline-block';
|
||||
if (btnMcPrint) btnMcPrint.style.display = 'inline-block';
|
||||
} else {
|
||||
if (mcPreview) mcPreview.innerHTML = '<div style="font-size:11px;color:var(--bp-text-muted);">' + (t('no_mc_for_worksheet') || 'Noch keine MC-Fragen für dieses Arbeitsblatt generiert.') + '</div>';
|
||||
currentMcData = null;
|
||||
if (btnMcPrint) btnMcPrint.style.display = 'none';
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Fehler beim Laden der MC-Daten:', e);
|
||||
if (mcPreview) mcPreview.innerHTML = '';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Rendert die MC-Vorschau
|
||||
* @param {Object} mcData - MC-Daten
|
||||
*/
|
||||
function renderMcPreview(mcData) {
|
||||
if (!mcPreview) return;
|
||||
if (!mcData || !mcData.questions || mcData.questions.length === 0) {
|
||||
mcPreview.innerHTML = '<div style="font-size:11px;color:var(--bp-text-muted);">' + (t('no_questions') || 'Keine Fragen vorhanden.') + '</div>';
|
||||
return;
|
||||
}
|
||||
|
||||
const questions = mcData.questions;
|
||||
const metadata = mcData.metadata || {};
|
||||
|
||||
let html = '';
|
||||
|
||||
// Zeige Metadaten
|
||||
if (metadata.grade_level || metadata.subject) {
|
||||
html += '<div class="mc-stats">';
|
||||
if (metadata.subject) {
|
||||
html += '<div class="mc-stats-item"><strong>' + (t('subject') || 'Fach') + ':</strong> ' + escapeHtml(metadata.subject) + '</div>';
|
||||
}
|
||||
if (metadata.grade_level) {
|
||||
html += '<div class="mc-stats-item"><strong>' + (t('grade') || 'Stufe') + ':</strong> ' + escapeHtml(metadata.grade_level) + '</div>';
|
||||
}
|
||||
html += '<div class="mc-stats-item"><strong>' + (t('questions') || 'Fragen') + ':</strong> ' + questions.length + '</div>';
|
||||
html += '</div>';
|
||||
}
|
||||
|
||||
// Zeige erste 2 Fragen als Vorschau
|
||||
const previewQuestions = questions.slice(0, 2);
|
||||
previewQuestions.forEach((q, idx) => {
|
||||
html += '<div class="mc-question">';
|
||||
html += '<div class="mc-question-text">' + (idx + 1) + '. ' + escapeHtml(q.question) + '</div>';
|
||||
html += '<div class="mc-options">';
|
||||
q.options.forEach(opt => {
|
||||
html += '<div class="mc-option" data-qid="' + q.id + '" data-opt="' + opt.id + '">';
|
||||
html += '<span class="mc-option-label">' + opt.id + ')</span> ' + escapeHtml(opt.text);
|
||||
html += '</div>';
|
||||
});
|
||||
html += '</div>';
|
||||
html += '</div>';
|
||||
});
|
||||
|
||||
if (questions.length > 2) {
|
||||
html += '<div style="font-size:11px;color:var(--bp-text-muted);text-align:center;margin-top:8px;">+ ' + (questions.length - 2) + ' ' + (t('more_questions') || 'weitere Fragen') + '</div>';
|
||||
}
|
||||
|
||||
mcPreview.innerHTML = html;
|
||||
|
||||
// Event-Listener für Antwort-Auswahl
|
||||
mcPreview.querySelectorAll('.mc-option').forEach(optEl => {
|
||||
optEl.addEventListener('click', () => handleMcOptionClick(optEl));
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Behandelt Klick auf eine MC-Option
|
||||
* @param {Element} optEl - Angeklicktes Option-Element
|
||||
*/
|
||||
function handleMcOptionClick(optEl) {
|
||||
const qid = optEl.getAttribute('data-qid');
|
||||
const optId = optEl.getAttribute('data-opt');
|
||||
|
||||
if (!currentMcData) return;
|
||||
|
||||
// Finde die Frage
|
||||
const question = currentMcData.questions.find(q => q.id === qid);
|
||||
if (!question) return;
|
||||
|
||||
// Markiere alle Optionen dieser Frage
|
||||
const questionEl = optEl.closest('.mc-question');
|
||||
const allOptions = questionEl.querySelectorAll('.mc-option');
|
||||
|
||||
allOptions.forEach(opt => {
|
||||
opt.classList.remove('selected', 'correct', 'incorrect');
|
||||
const thisOptId = opt.getAttribute('data-opt');
|
||||
if (thisOptId === question.correct_answer) {
|
||||
opt.classList.add('correct');
|
||||
} else if (thisOptId === optId) {
|
||||
opt.classList.add('incorrect');
|
||||
}
|
||||
});
|
||||
|
||||
// Speichere Antwort
|
||||
mcAnswers[qid] = optId;
|
||||
|
||||
// Zeige Feedback
|
||||
const isCorrect = optId === question.correct_answer;
|
||||
let feedbackEl = questionEl.querySelector('.mc-feedback');
|
||||
if (!feedbackEl) {
|
||||
feedbackEl = document.createElement('div');
|
||||
feedbackEl.className = 'mc-feedback';
|
||||
questionEl.appendChild(feedbackEl);
|
||||
}
|
||||
|
||||
if (isCorrect) {
|
||||
feedbackEl.style.background = 'rgba(34,197,94,0.1)';
|
||||
feedbackEl.style.borderColor = 'rgba(34,197,94,0.3)';
|
||||
feedbackEl.style.color = 'var(--bp-accent)';
|
||||
feedbackEl.textContent = (t('correct') || 'Richtig!') + ' ' + (question.explanation || '');
|
||||
} else {
|
||||
feedbackEl.style.background = 'rgba(239,68,68,0.1)';
|
||||
feedbackEl.style.borderColor = 'rgba(239,68,68,0.3)';
|
||||
feedbackEl.style.color = '#ef4444';
|
||||
feedbackEl.textContent = (t('incorrect') || 'Leider falsch.') + ' ' + (question.explanation || '');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Öffnet das MC-Quiz-Modal
|
||||
*/
|
||||
export function openMcModal() {
|
||||
if (!currentMcData || !currentMcData.questions) {
|
||||
alert(t('no_mc_questions') || 'Keine MC-Fragen vorhanden. Bitte zuerst generieren.');
|
||||
return;
|
||||
}
|
||||
|
||||
mcAnswers = {}; // Reset Antworten
|
||||
renderMcModal(currentMcData);
|
||||
if (mcModal) mcModal.classList.remove('hidden');
|
||||
}
|
||||
|
||||
/**
|
||||
* Schließt das MC-Modal
|
||||
*/
|
||||
export function closeMcModal() {
|
||||
if (mcModal) mcModal.classList.add('hidden');
|
||||
}
|
||||
|
||||
/**
|
||||
* Rendert den Modal-Inhalt
|
||||
* @param {Object} mcData - MC-Daten
|
||||
*/
|
||||
function renderMcModal(mcData) {
|
||||
if (!mcModalBody) return;
|
||||
|
||||
const questions = mcData.questions;
|
||||
const metadata = mcData.metadata || {};
|
||||
|
||||
let html = '';
|
||||
|
||||
// Header mit Metadaten
|
||||
html += '<div class="mc-stats" style="margin-bottom:16px;">';
|
||||
if (metadata.source_title) {
|
||||
html += '<div class="mc-stats-item"><strong>' + (t('worksheet') || 'Arbeitsblatt') + ':</strong> ' + escapeHtml(metadata.source_title) + '</div>';
|
||||
}
|
||||
if (metadata.subject) {
|
||||
html += '<div class="mc-stats-item"><strong>' + (t('subject') || 'Fach') + ':</strong> ' + escapeHtml(metadata.subject) + '</div>';
|
||||
}
|
||||
if (metadata.grade_level) {
|
||||
html += '<div class="mc-stats-item"><strong>' + (t('grade') || 'Stufe') + ':</strong> ' + escapeHtml(metadata.grade_level) + '</div>';
|
||||
}
|
||||
html += '</div>';
|
||||
|
||||
// Alle Fragen
|
||||
questions.forEach((q, idx) => {
|
||||
html += '<div class="mc-question" data-qid="' + q.id + '">';
|
||||
html += '<div class="mc-question-text">' + (idx + 1) + '. ' + escapeHtml(q.question) + '</div>';
|
||||
html += '<div class="mc-options">';
|
||||
q.options.forEach(opt => {
|
||||
html += '<div class="mc-option" data-qid="' + q.id + '" data-opt="' + opt.id + '">';
|
||||
html += '<span class="mc-option-label">' + opt.id + ')</span> ' + escapeHtml(opt.text);
|
||||
html += '</div>';
|
||||
});
|
||||
html += '</div>';
|
||||
html += '</div>';
|
||||
});
|
||||
|
||||
// Auswertungs-Button
|
||||
html += '<div style="margin-top:16px;text-align:center;">';
|
||||
html += '<button class="btn btn-primary" id="btn-mc-evaluate">' + (t('evaluate') || 'Auswerten') + '</button>';
|
||||
html += '</div>';
|
||||
|
||||
mcModalBody.innerHTML = html;
|
||||
|
||||
// Event-Listener
|
||||
mcModalBody.querySelectorAll('.mc-option').forEach(optEl => {
|
||||
optEl.addEventListener('click', () => {
|
||||
const qid = optEl.getAttribute('data-qid');
|
||||
const optId = optEl.getAttribute('data-opt');
|
||||
|
||||
// Deselektiere andere Optionen der gleichen Frage
|
||||
const questionEl = optEl.closest('.mc-question');
|
||||
questionEl.querySelectorAll('.mc-option').forEach(o => o.classList.remove('selected'));
|
||||
optEl.classList.add('selected');
|
||||
|
||||
mcAnswers[qid] = optId;
|
||||
});
|
||||
});
|
||||
|
||||
const btnEvaluate = document.getElementById('btn-mc-evaluate');
|
||||
if (btnEvaluate) {
|
||||
btnEvaluate.addEventListener('click', evaluateMcQuiz);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Wertet das Quiz aus
|
||||
*/
|
||||
function evaluateMcQuiz() {
|
||||
if (!currentMcData || !mcModalBody) return;
|
||||
|
||||
let correct = 0;
|
||||
let total = currentMcData.questions.length;
|
||||
|
||||
currentMcData.questions.forEach(q => {
|
||||
const questionEl = mcModalBody.querySelector('.mc-question[data-qid="' + q.id + '"]');
|
||||
if (!questionEl) return;
|
||||
|
||||
const userAnswer = mcAnswers[q.id];
|
||||
const allOptions = questionEl.querySelectorAll('.mc-option');
|
||||
|
||||
allOptions.forEach(opt => {
|
||||
opt.classList.remove('correct', 'incorrect');
|
||||
const optId = opt.getAttribute('data-opt');
|
||||
if (optId === q.correct_answer) {
|
||||
opt.classList.add('correct');
|
||||
} else if (optId === userAnswer && userAnswer !== q.correct_answer) {
|
||||
opt.classList.add('incorrect');
|
||||
}
|
||||
});
|
||||
|
||||
// Zeige Erklärung
|
||||
let feedbackEl = questionEl.querySelector('.mc-feedback');
|
||||
if (!feedbackEl) {
|
||||
feedbackEl = document.createElement('div');
|
||||
feedbackEl.className = 'mc-feedback';
|
||||
questionEl.appendChild(feedbackEl);
|
||||
}
|
||||
|
||||
if (userAnswer === q.correct_answer) {
|
||||
correct++;
|
||||
feedbackEl.style.background = 'rgba(34,197,94,0.1)';
|
||||
feedbackEl.style.borderColor = 'rgba(34,197,94,0.3)';
|
||||
feedbackEl.style.color = 'var(--bp-accent)';
|
||||
feedbackEl.textContent = (t('correct') || 'Richtig!') + ' ' + (q.explanation || '');
|
||||
} else if (userAnswer) {
|
||||
feedbackEl.style.background = 'rgba(239,68,68,0.1)';
|
||||
feedbackEl.style.borderColor = 'rgba(239,68,68,0.3)';
|
||||
feedbackEl.style.color = '#ef4444';
|
||||
feedbackEl.textContent = (t('incorrect') || 'Falsch.') + ' ' + (q.explanation || '');
|
||||
} else {
|
||||
feedbackEl.style.background = 'rgba(148,163,184,0.1)';
|
||||
feedbackEl.style.borderColor = 'rgba(148,163,184,0.3)';
|
||||
feedbackEl.style.color = 'var(--bp-text-muted)';
|
||||
feedbackEl.textContent = (t('not_answered') || 'Nicht beantwortet.') + ' ' + (t('correct_was') || 'Richtig wäre:') + ' ' + q.correct_answer.toUpperCase();
|
||||
}
|
||||
});
|
||||
|
||||
// Zeige Gesamtergebnis
|
||||
const percentage = Math.round(correct / total * 100);
|
||||
const resultHtml = '<div style="margin-top:16px;padding:12px;background:rgba(15,23,42,0.6);border-radius:8px;text-align:center;">' +
|
||||
'<div style="font-size:18px;font-weight:600;">' + correct + ' ' + (t('of') || 'von') + ' ' + total + ' ' + (t('correct_answers') || 'richtig') + '</div>' +
|
||||
'<div style="font-size:12px;color:var(--bp-text-muted);margin-top:4px;">' + percentage + '% ' + (t('percent_correct') || 'korrekt') + '</div>' +
|
||||
'</div>';
|
||||
|
||||
const existingResult = mcModalBody.querySelector('.mc-result');
|
||||
if (existingResult) {
|
||||
existingResult.remove();
|
||||
}
|
||||
|
||||
const resultDiv = document.createElement('div');
|
||||
resultDiv.className = 'mc-result';
|
||||
resultDiv.innerHTML = resultHtml;
|
||||
mcModalBody.appendChild(resultDiv);
|
||||
}
|
||||
|
||||
/**
|
||||
* Öffnet den Druck-Dialog
|
||||
*/
|
||||
export function openMcPrintDialog() {
|
||||
if (!currentMcData) {
|
||||
alert(t('no_mc_questions') || 'Keine MC-Fragen vorhanden.');
|
||||
return;
|
||||
}
|
||||
|
||||
const eingangFiles = getEingangFilesCallback();
|
||||
const currentIndex = getCurrentIndexCallback();
|
||||
const currentFile = eingangFiles[currentIndex];
|
||||
|
||||
const confirmMsg = (t('mc_print_with_answers') || 'Mit Lösungen drucken?') +
|
||||
'\n\nOK = ' + (t('solution_sheet') || 'Lösungsblatt mit markierten Antworten') +
|
||||
'\n' + (t('cancel') || 'Abbrechen') + ' = ' + (t('exercise_sheet') || 'Übungsblatt ohne Lösungen');
|
||||
|
||||
const choice = confirm(confirmMsg);
|
||||
const url = '/api/print-mc/' + encodeURIComponent(currentFile) + '?show_answers=' + choice;
|
||||
window.open(url, '_blank');
|
||||
}
|
||||
|
||||
// === Getter und Setter ===
|
||||
|
||||
/**
|
||||
* Gibt die aktuellen MC-Daten zurück
|
||||
* @returns {Object|null}
|
||||
*/
|
||||
export function getMcData() {
|
||||
return currentMcData;
|
||||
}
|
||||
|
||||
/**
|
||||
* Setzt die MC-Daten
|
||||
* @param {Object} data
|
||||
*/
|
||||
export function setMcData(data) {
|
||||
currentMcData = data;
|
||||
if (data) {
|
||||
renderMcPreview(data);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper: HTML-Escape
|
||||
*/
|
||||
function escapeHtml(text) {
|
||||
if (!text) return '';
|
||||
const div = document.createElement('div');
|
||||
div.textContent = text;
|
||||
return div.innerHTML;
|
||||
}
|
||||
223
backend/frontend/static/js/modules/mindmap-module.js
Normal file
223
backend/frontend/static/js/modules/mindmap-module.js
Normal file
@@ -0,0 +1,223 @@
|
||||
/**
|
||||
* BreakPilot Studio - Mindmap Module
|
||||
*
|
||||
* Mindmap/Lernplakat-Generierung aus Arbeitsblättern:
|
||||
* - Generieren von Mindmaps aus analysierten Arbeitsblättern
|
||||
* - Vorschau und Anzeige
|
||||
* - Druck-Funktion (A4/A3)
|
||||
*
|
||||
* Refactored: 2026-01-19
|
||||
*/
|
||||
|
||||
import { t } from './i18n.js';
|
||||
import { setStatus, setStatusWorking, setStatusError, setStatusSuccess, fetchJSON } from './api-helpers.js';
|
||||
|
||||
// State
|
||||
let currentMindmapData = null;
|
||||
|
||||
// DOM References
|
||||
let mindmapPreview = null;
|
||||
let mindmapBadge = null;
|
||||
let btnMindmapGenerate = null;
|
||||
let btnMindmapShow = null;
|
||||
let btnMindmapPrint = null;
|
||||
|
||||
// Callback für aktuelle Datei
|
||||
let getCurrentFileCallback = null;
|
||||
|
||||
/**
|
||||
* Initialisiert das Mindmap-Modul
|
||||
* @param {Object} options - Konfiguration
|
||||
* @param {Function} options.getCurrentFile - Callback um die aktuelle Datei zu bekommen
|
||||
*/
|
||||
export function initMindmapModule(options = {}) {
|
||||
getCurrentFileCallback = options.getCurrentFile || (() => null);
|
||||
|
||||
mindmapPreview = document.getElementById('mindmap-preview') || options.previewEl;
|
||||
mindmapBadge = document.getElementById('mindmap-badge') || options.badgeEl;
|
||||
btnMindmapGenerate = document.getElementById('btn-mindmap-generate') || options.generateBtn;
|
||||
btnMindmapShow = document.getElementById('btn-mindmap-show') || options.showBtn;
|
||||
btnMindmapPrint = document.getElementById('btn-mindmap-print') || options.printBtn;
|
||||
|
||||
// Event-Listener
|
||||
if (btnMindmapGenerate) {
|
||||
btnMindmapGenerate.addEventListener('click', generateMindmap);
|
||||
}
|
||||
|
||||
if (btnMindmapShow) {
|
||||
btnMindmapShow.addEventListener('click', openMindmapView);
|
||||
}
|
||||
|
||||
if (btnMindmapPrint) {
|
||||
btnMindmapPrint.addEventListener('click', openMindmapPrint);
|
||||
}
|
||||
|
||||
// Event für Datei-Wechsel
|
||||
window.addEventListener('fileSelected', (ev) => {
|
||||
loadMindmapData();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Generiert eine Mindmap für die aktuelle Datei
|
||||
*/
|
||||
export async function generateMindmap() {
|
||||
const currentFile = getCurrentFileCallback();
|
||||
if (!currentFile) {
|
||||
alert(t('select_file_first') || 'Bitte zuerst eine Datei auswählen.');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
setStatusWorking(t('mindmap_generating') || 'Generiere Mindmap...');
|
||||
if (mindmapBadge) {
|
||||
mindmapBadge.textContent = t('generating') || 'Generiert...';
|
||||
mindmapBadge.className = 'card-badge';
|
||||
}
|
||||
|
||||
const resp = await fetch('/api/generate-mindmap/' + encodeURIComponent(currentFile), {
|
||||
method: 'POST'
|
||||
});
|
||||
|
||||
if (!resp.ok) {
|
||||
throw new Error('HTTP ' + resp.status);
|
||||
}
|
||||
|
||||
const data = await resp.json();
|
||||
|
||||
if (data.status === 'OK') {
|
||||
if (mindmapBadge) {
|
||||
mindmapBadge.textContent = t('ready') || 'Fertig';
|
||||
mindmapBadge.className = 'card-badge badge-success';
|
||||
}
|
||||
setStatusSuccess(t('mindmap_generated') || 'Mindmap erstellt!');
|
||||
|
||||
// Lade Mindmap-Daten
|
||||
await loadMindmapData();
|
||||
} else if (data.status === 'NOT_FOUND') {
|
||||
if (mindmapBadge) {
|
||||
mindmapBadge.textContent = t('no_analysis') || 'Keine Analyse';
|
||||
mindmapBadge.className = 'card-badge badge-error';
|
||||
}
|
||||
setStatusError(t('analyze_first') || 'Bitte zuerst analysieren (Neuaufbau starten)');
|
||||
} else {
|
||||
throw new Error(data.message || 'Fehler bei der Mindmap-Generierung');
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Mindmap error:', err);
|
||||
if (mindmapBadge) {
|
||||
mindmapBadge.textContent = t('error') || 'Fehler';
|
||||
mindmapBadge.className = 'card-badge badge-error';
|
||||
}
|
||||
setStatusError(t('error') || 'Fehler', err.message);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Lädt die Mindmap-Daten für die aktuelle Datei
|
||||
*/
|
||||
export async function loadMindmapData() {
|
||||
const currentFile = getCurrentFileCallback();
|
||||
|
||||
if (!currentFile) {
|
||||
if (mindmapPreview) mindmapPreview.innerHTML = '';
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const resp = await fetch('/api/mindmap-data/' + encodeURIComponent(currentFile));
|
||||
const data = await resp.json();
|
||||
|
||||
if (data.status === 'OK' && data.data) {
|
||||
currentMindmapData = data.data;
|
||||
renderMindmapPreview();
|
||||
if (btnMindmapShow) btnMindmapShow.style.display = 'inline-block';
|
||||
if (btnMindmapPrint) btnMindmapPrint.style.display = 'inline-block';
|
||||
if (mindmapBadge) {
|
||||
mindmapBadge.textContent = t('ready') || 'Fertig';
|
||||
mindmapBadge.className = 'card-badge badge-success';
|
||||
}
|
||||
} else {
|
||||
currentMindmapData = null;
|
||||
if (mindmapPreview) mindmapPreview.innerHTML = '';
|
||||
if (btnMindmapShow) btnMindmapShow.style.display = 'none';
|
||||
if (btnMindmapPrint) btnMindmapPrint.style.display = 'none';
|
||||
if (mindmapBadge) {
|
||||
mindmapBadge.textContent = t('ready') || 'Bereit';
|
||||
mindmapBadge.className = 'card-badge';
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Error loading mindmap:', err);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Rendert die Mindmap-Vorschau
|
||||
*/
|
||||
function renderMindmapPreview() {
|
||||
if (!mindmapPreview) return;
|
||||
|
||||
if (!currentMindmapData) {
|
||||
mindmapPreview.innerHTML = '';
|
||||
return;
|
||||
}
|
||||
|
||||
const topic = currentMindmapData.topic || 'Thema';
|
||||
const categories = currentMindmapData.categories || [];
|
||||
const categoryCount = categories.length;
|
||||
const termCount = categories.reduce((sum, cat) => sum + (cat.terms ? cat.terms.length : 0), 0);
|
||||
|
||||
mindmapPreview.innerHTML = `
|
||||
<div style="margin-top:10px;padding:12px;background:linear-gradient(135deg,#f0f9ff,#e0f2fe);border-radius:10px;text-align:center;">
|
||||
<div style="font-size:18px;font-weight:bold;color:#0369a1;margin-bottom:8px;">${escapeHtml(topic)}</div>
|
||||
<div style="font-size:12px;color:#64748b;">
|
||||
${categoryCount} ${t('categories') || 'Kategorien'} | ${termCount} ${t('terms') || 'Begriffe'}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Öffnet die Mindmap-Ansicht (A4)
|
||||
*/
|
||||
export function openMindmapView() {
|
||||
const currentFile = getCurrentFileCallback();
|
||||
if (!currentFile) return;
|
||||
window.open('/api/mindmap-html/' + encodeURIComponent(currentFile) + '?format=a4', '_blank');
|
||||
}
|
||||
|
||||
/**
|
||||
* Öffnet die Mindmap-Druckansicht (A3)
|
||||
*/
|
||||
export function openMindmapPrint() {
|
||||
const currentFile = getCurrentFileCallback();
|
||||
if (!currentFile) return;
|
||||
window.open('/api/mindmap-html/' + encodeURIComponent(currentFile) + '?format=a3', '_blank');
|
||||
}
|
||||
|
||||
/**
|
||||
* Gibt die aktuellen Mindmap-Daten zurück
|
||||
* @returns {Object|null}
|
||||
*/
|
||||
export function getMindmapData() {
|
||||
return currentMindmapData;
|
||||
}
|
||||
|
||||
/**
|
||||
* Setzt die Mindmap-Daten
|
||||
* @param {Object} data
|
||||
*/
|
||||
export function setMindmapData(data) {
|
||||
currentMindmapData = data;
|
||||
renderMindmapPreview();
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper: HTML-Escape
|
||||
*/
|
||||
function escapeHtml(text) {
|
||||
const div = document.createElement('div');
|
||||
div.textContent = text;
|
||||
return div.innerHTML;
|
||||
}
|
||||
444
backend/frontend/static/js/modules/qa-leitner-module.js
Normal file
444
backend/frontend/static/js/modules/qa-leitner-module.js
Normal file
@@ -0,0 +1,444 @@
|
||||
/**
|
||||
* BreakPilot Studio - Q&A Leitner Module
|
||||
*
|
||||
* Frage-Antwort Lernkarten mit Leitner-System:
|
||||
* - Generieren von Q&A aus analysierten Arbeitsblättern
|
||||
* - Leitner-Box-System (Neu, Gelernt, Gefestigt)
|
||||
* - Lern-Session mit Selbstbewertung
|
||||
* - Fortschrittsspeicherung
|
||||
*
|
||||
* Refactored: 2026-01-19
|
||||
*/
|
||||
|
||||
import { t } from './i18n.js';
|
||||
import { setStatus, setStatusWorking, setStatusError, setStatusSuccess, fetchJSON } from './api-helpers.js';
|
||||
|
||||
// State
|
||||
let currentQaData = null;
|
||||
let currentQaIndex = 0;
|
||||
let qaSessionStats = { correct: 0, incorrect: 0, total: 0 };
|
||||
|
||||
// DOM References
|
||||
let qaPreview = null;
|
||||
let qaBadge = null;
|
||||
let qaModal = null;
|
||||
let qaModalBody = null;
|
||||
let qaModalClose = null;
|
||||
let btnQaGenerate = null;
|
||||
let btnQaLearn = null;
|
||||
let btnQaPrint = null;
|
||||
|
||||
// Callback für aktuelle Datei
|
||||
let getCurrentFileCallback = null;
|
||||
let getFilesCallback = null;
|
||||
let getCurrentIndexCallback = null;
|
||||
|
||||
/**
|
||||
* Initialisiert das Q&A Leitner-Modul
|
||||
* @param {Object} options - Konfiguration
|
||||
*/
|
||||
export function initQaModule(options = {}) {
|
||||
getCurrentFileCallback = options.getCurrentFile || (() => null);
|
||||
getFilesCallback = options.getFiles || (() => []);
|
||||
getCurrentIndexCallback = options.getCurrentIndex || (() => 0);
|
||||
|
||||
qaPreview = document.getElementById('qa-preview') || options.previewEl;
|
||||
qaBadge = document.getElementById('qa-badge') || options.badgeEl;
|
||||
qaModal = document.getElementById('qa-modal') || options.modalEl;
|
||||
qaModalBody = document.getElementById('qa-modal-body') || options.modalBodyEl;
|
||||
qaModalClose = document.getElementById('qa-modal-close') || options.closeBtn;
|
||||
btnQaGenerate = document.getElementById('btn-qa-generate') || options.generateBtn;
|
||||
btnQaLearn = document.getElementById('btn-qa-learn') || options.learnBtn;
|
||||
btnQaPrint = document.getElementById('btn-qa-print') || options.printBtn;
|
||||
|
||||
// Event-Listener
|
||||
if (btnQaGenerate) {
|
||||
btnQaGenerate.addEventListener('click', generateQaQuestions);
|
||||
}
|
||||
|
||||
if (btnQaLearn) {
|
||||
btnQaLearn.addEventListener('click', openQaModal);
|
||||
}
|
||||
|
||||
if (btnQaPrint) {
|
||||
btnQaPrint.addEventListener('click', openQaPrintDialog);
|
||||
}
|
||||
|
||||
if (qaModalClose) {
|
||||
qaModalClose.addEventListener('click', closeQaModal);
|
||||
}
|
||||
|
||||
if (qaModal) {
|
||||
qaModal.addEventListener('click', (ev) => {
|
||||
if (ev.target === qaModal) {
|
||||
closeQaModal();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Event für Datei-Wechsel
|
||||
window.addEventListener('fileSelected', () => {
|
||||
loadQaPreviewForCurrent();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Generiert Q&A für alle Dateien
|
||||
*/
|
||||
export async function generateQaQuestions() {
|
||||
try {
|
||||
setStatusWorking(t('status_generating_qa') || 'Generiere Q&A ...');
|
||||
if (qaBadge) qaBadge.textContent = t('mc_generating') || 'Generiert...';
|
||||
|
||||
const resp = await fetch('/api/generate-qa', { method: 'POST' });
|
||||
if (!resp.ok) {
|
||||
throw new Error('HTTP ' + resp.status);
|
||||
}
|
||||
|
||||
const result = await resp.json();
|
||||
if (result.status === 'OK' && result.generated.length > 0) {
|
||||
setStatusSuccess(
|
||||
t('status_qa_generated') || 'Q&A generiert',
|
||||
result.generated.length + ' ' + (t('status_files_created') || 'Dateien erstellt')
|
||||
);
|
||||
if (qaBadge) qaBadge.textContent = t('mc_done') || 'Fertig';
|
||||
if (btnQaLearn) btnQaLearn.style.display = 'inline-block';
|
||||
if (btnQaPrint) btnQaPrint.style.display = 'inline-block';
|
||||
|
||||
await loadQaPreviewForCurrent();
|
||||
} else if (result.errors && result.errors.length > 0) {
|
||||
setStatusError(t('error') || 'Fehler', result.errors[0].error);
|
||||
if (qaBadge) qaBadge.textContent = t('mc_error') || 'Fehler';
|
||||
} else {
|
||||
setStatusError(t('error') || 'Fehler', 'Keine Q&A generiert.');
|
||||
if (qaBadge) qaBadge.textContent = t('mc_ready') || 'Bereit';
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Q&A-Generierung fehlgeschlagen:', e);
|
||||
setStatusError(t('error') || 'Fehler', String(e));
|
||||
if (qaBadge) qaBadge.textContent = t('mc_error') || 'Fehler';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Lädt die Q&A-Vorschau für die aktuelle Datei
|
||||
*/
|
||||
export async function loadQaPreviewForCurrent() {
|
||||
const files = getFilesCallback();
|
||||
if (!files.length) {
|
||||
if (qaPreview) {
|
||||
qaPreview.innerHTML = `<div style="font-size:11px;color:var(--bp-text-muted);">${t('qa_no_questions') || 'Noch keine Q&A vorhanden.'}</div>`;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const currentFile = getCurrentFileCallback();
|
||||
if (!currentFile) return;
|
||||
|
||||
try {
|
||||
const resp = await fetch('/api/qa-data/' + encodeURIComponent(currentFile));
|
||||
const result = await resp.json();
|
||||
|
||||
if (result.status === 'OK' && result.data) {
|
||||
currentQaData = result.data;
|
||||
renderQaPreview(result.data);
|
||||
if (btnQaLearn) btnQaLearn.style.display = 'inline-block';
|
||||
if (btnQaPrint) btnQaPrint.style.display = 'inline-block';
|
||||
} else {
|
||||
if (qaPreview) {
|
||||
qaPreview.innerHTML = `<div style="font-size:11px;color:var(--bp-text-muted);">${t('qa_no_questions') || 'Noch keine Q&A für dieses Arbeitsblatt generiert.'}</div>`;
|
||||
}
|
||||
currentQaData = null;
|
||||
if (btnQaLearn) btnQaLearn.style.display = 'none';
|
||||
if (btnQaPrint) btnQaPrint.style.display = 'none';
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Fehler beim Laden der Q&A-Daten:', e);
|
||||
if (qaPreview) qaPreview.innerHTML = '';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Rendert die Q&A-Vorschau
|
||||
*/
|
||||
function renderQaPreview(qaData) {
|
||||
if (!qaPreview) return;
|
||||
if (!qaData || !qaData.qa_items || qaData.qa_items.length === 0) {
|
||||
qaPreview.innerHTML = `<div style="font-size:11px;color:var(--bp-text-muted);">${t('qa_no_questions') || 'Keine Fragen vorhanden.'}</div>`;
|
||||
return;
|
||||
}
|
||||
|
||||
const items = qaData.qa_items;
|
||||
|
||||
// Zähle Fragen nach Box
|
||||
let box0 = 0, box1 = 0, box2 = 0;
|
||||
items.forEach(item => {
|
||||
const box = item.leitner ? item.leitner.box : 0;
|
||||
if (box === 0) box0++;
|
||||
else if (box === 1) box1++;
|
||||
else box2++;
|
||||
});
|
||||
|
||||
let html = `
|
||||
<div class="mc-stats" style="margin-bottom:8px;">
|
||||
<div style="display:flex;gap:12px;font-size:11px;">
|
||||
<div style="color:#ef4444;">${t('qa_box_new') || 'Neu'}: ${box0}</div>
|
||||
<div style="color:#f59e0b;">${t('qa_box_learning') || 'Lernt'}: ${box1}</div>
|
||||
<div style="color:#22c55e;">${t('qa_box_mastered') || 'Gefestigt'}: ${box2}</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
// Zeige erste 2 Fragen als Vorschau
|
||||
const previewItems = items.slice(0, 2);
|
||||
previewItems.forEach((item, idx) => {
|
||||
html += `
|
||||
<div class="mc-question" style="padding:8px;margin-bottom:6px;background:rgba(255,255,255,0.03);border-radius:6px;">
|
||||
<div style="font-size:12px;font-weight:500;margin-bottom:4px;">${idx + 1}. ${escapeHtml(item.question)}</div>
|
||||
<div style="font-size:11px;color:var(--bp-text-muted);">→ ${escapeHtml(item.answer.substring(0, 60))}${item.answer.length > 60 ? '...' : ''}</div>
|
||||
</div>
|
||||
`;
|
||||
});
|
||||
|
||||
if (items.length > 2) {
|
||||
html += `<div style="font-size:11px;color:var(--bp-text-muted);text-align:center;margin-top:4px;">+ ${items.length - 2} ${t('questions') || 'weitere Fragen'}</div>`;
|
||||
}
|
||||
|
||||
qaPreview.innerHTML = html;
|
||||
}
|
||||
|
||||
/**
|
||||
* Öffnet das Lern-Modal
|
||||
*/
|
||||
export function openQaModal() {
|
||||
if (!currentQaData || !currentQaData.qa_items) {
|
||||
alert(t('qa_no_questions') || 'Keine Q&A vorhanden. Bitte zuerst generieren.');
|
||||
return;
|
||||
}
|
||||
|
||||
currentQaIndex = 0;
|
||||
qaSessionStats = { correct: 0, incorrect: 0, total: 0 };
|
||||
renderQaLearningCard();
|
||||
if (qaModal) qaModal.classList.remove('hidden');
|
||||
}
|
||||
|
||||
/**
|
||||
* Schließt das Lern-Modal
|
||||
*/
|
||||
export function closeQaModal() {
|
||||
if (qaModal) qaModal.classList.add('hidden');
|
||||
}
|
||||
|
||||
/**
|
||||
* Rendert die aktuelle Lernkarte
|
||||
*/
|
||||
function renderQaLearningCard() {
|
||||
const items = currentQaData.qa_items;
|
||||
|
||||
if (currentQaIndex >= items.length) {
|
||||
renderQaSessionSummary();
|
||||
return;
|
||||
}
|
||||
|
||||
const item = items[currentQaIndex];
|
||||
const leitner = item.leitner || { box: 0 };
|
||||
const boxNames = [t('qa_box_new') || 'Neu', t('qa_box_learning') || 'Gelernt', t('qa_box_mastered') || 'Gefestigt'];
|
||||
const boxColors = ['#ef4444', '#f59e0b', '#22c55e'];
|
||||
|
||||
let html = `
|
||||
<!-- Fortschrittsanzeige -->
|
||||
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:16px;">
|
||||
<div style="font-size:12px;color:var(--bp-text-muted);">${t('question') || 'Frage'} ${currentQaIndex + 1} / ${items.length}</div>
|
||||
<div style="display:flex;align-items:center;gap:6px;">
|
||||
<span style="font-size:10px;color:${boxColors[leitner.box]};background:${boxColors[leitner.box]}22;padding:2px 8px;border-radius:10px;">${boxNames[leitner.box]}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Frage -->
|
||||
<div style="background:rgba(255,255,255,0.05);padding:20px;border-radius:12px;margin-bottom:16px;">
|
||||
<div style="font-size:11px;color:var(--bp-text-muted);margin-bottom:8px;">${t('question') || 'Frage'}:</div>
|
||||
<div style="font-size:16px;font-weight:500;line-height:1.5;">${escapeHtml(item.question)}</div>
|
||||
</div>
|
||||
|
||||
<!-- Eingabefeld -->
|
||||
<div id="qa-input-container" style="margin-bottom:16px;">
|
||||
<div style="font-size:11px;color:var(--bp-text-muted);margin-bottom:8px;">${t('qa_your_answer') || 'Deine Antwort'}:</div>
|
||||
<textarea id="qa-user-answer" style="width:100%;min-height:80px;padding:12px;border:1px solid rgba(255,255,255,0.2);border-radius:8px;background:rgba(255,255,255,0.05);color:var(--bp-text);font-size:14px;resize:vertical;font-family:inherit;" placeholder="${t('qa_type_answer') || 'Schreibe deine Antwort hier...'}"></textarea>
|
||||
</div>
|
||||
|
||||
<!-- Prüfen-Button -->
|
||||
<div id="qa-check-btn-container" style="text-align:center;margin-bottom:16px;">
|
||||
<button class="btn btn-primary" id="btn-qa-check-answer" style="padding:12px 32px;">${t('qa_check_answer') || 'Antwort prüfen'}</button>
|
||||
</div>
|
||||
|
||||
<!-- Vergleichs-Container (versteckt) -->
|
||||
<div id="qa-comparison-container" style="display:none;">
|
||||
<div id="qa-user-answer-display" style="background:rgba(59,130,246,0.1);padding:16px;border-radius:12px;margin-bottom:12px;border-left:3px solid #3b82f6;">
|
||||
<div style="font-size:11px;color:#3b82f6;margin-bottom:8px;">${t('qa_your_answer') || 'Deine Antwort'}:</div>
|
||||
<div id="qa-user-answer-text" style="font-size:14px;line-height:1.5;"></div>
|
||||
</div>
|
||||
|
||||
<div style="background:rgba(34,197,94,0.1);padding:16px;border-radius:12px;margin-bottom:16px;border-left:3px solid #22c55e;">
|
||||
<div style="font-size:11px;color:#22c55e;margin-bottom:8px;">${t('qa_correct_answer') || 'Richtige Antwort'}:</div>
|
||||
<div style="font-size:14px;line-height:1.5;">${escapeHtml(item.answer)}</div>
|
||||
${item.key_terms && item.key_terms.length > 0 ? `
|
||||
<div style="margin-top:12px;font-size:11px;color:var(--bp-text-muted);">
|
||||
${t('qa_key_terms') || 'Schlüsselbegriffe'}: <span style="color:#22c55e;">${item.key_terms.join(', ')}</span>
|
||||
</div>
|
||||
` : ''}
|
||||
</div>
|
||||
|
||||
<!-- Selbstbewertung -->
|
||||
<div style="text-align:center;margin-bottom:8px;">
|
||||
<div style="font-size:12px;color:var(--bp-text-muted);margin-bottom:12px;">${t('qa_self_evaluate') || 'War deine Antwort richtig?'}</div>
|
||||
<div style="display:flex;gap:12px;justify-content:center;">
|
||||
<button class="btn" id="btn-qa-incorrect" style="background:#ef4444;padding:12px 24px;">${t('qa_incorrect') || 'Falsch'}</button>
|
||||
<button class="btn" id="btn-qa-correct" style="background:#22c55e;padding:12px 24px;">${t('qa_correct') || 'Richtig'}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Session-Statistik -->
|
||||
<div style="margin-top:16px;padding-top:12px;border-top:1px solid rgba(255,255,255,0.1);display:flex;justify-content:center;gap:20px;font-size:11px;">
|
||||
<div style="color:#22c55e;">${t('qa_session_correct') || 'Richtig'}: ${qaSessionStats.correct}</div>
|
||||
<div style="color:#ef4444;">${t('qa_session_incorrect') || 'Falsch'}: ${qaSessionStats.incorrect}</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
if (qaModalBody) {
|
||||
qaModalBody.innerHTML = html;
|
||||
|
||||
// Event Listener
|
||||
document.getElementById('btn-qa-check-answer')?.addEventListener('click', () => {
|
||||
const userAnswer = document.getElementById('qa-user-answer')?.value.trim() || '';
|
||||
document.getElementById('qa-user-answer-text').textContent = userAnswer || (t('qa_no_answer') || '(keine Antwort eingegeben)');
|
||||
document.getElementById('qa-input-container').style.display = 'none';
|
||||
document.getElementById('qa-check-btn-container').style.display = 'none';
|
||||
document.getElementById('qa-comparison-container').style.display = 'block';
|
||||
});
|
||||
|
||||
// Enter zum Prüfen
|
||||
document.getElementById('qa-user-answer')?.addEventListener('keydown', (e) => {
|
||||
if (e.key === 'Enter' && !e.shiftKey) {
|
||||
e.preventDefault();
|
||||
document.getElementById('btn-qa-check-answer')?.click();
|
||||
}
|
||||
});
|
||||
|
||||
// Fokus
|
||||
setTimeout(() => {
|
||||
document.getElementById('qa-user-answer')?.focus();
|
||||
}, 100);
|
||||
|
||||
document.getElementById('btn-qa-correct')?.addEventListener('click', () => handleQaAnswer(true));
|
||||
document.getElementById('btn-qa-incorrect')?.addEventListener('click', () => handleQaAnswer(false));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Verarbeitet die Antwort
|
||||
*/
|
||||
async function handleQaAnswer(correct) {
|
||||
const item = currentQaData.qa_items[currentQaIndex];
|
||||
|
||||
qaSessionStats.total++;
|
||||
if (correct) qaSessionStats.correct++;
|
||||
else qaSessionStats.incorrect++;
|
||||
|
||||
// Speichere Fortschritt
|
||||
try {
|
||||
const currentFile = getCurrentFileCallback();
|
||||
if (currentFile) {
|
||||
await fetch(`/api/qa-progress?filename=${encodeURIComponent(currentFile)}&item_id=${encodeURIComponent(item.id)}&correct=${correct}`, {
|
||||
method: 'POST'
|
||||
});
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Fehler beim Speichern des Fortschritts:', e);
|
||||
}
|
||||
|
||||
currentQaIndex++;
|
||||
renderQaLearningCard();
|
||||
}
|
||||
|
||||
/**
|
||||
* Rendert die Session-Zusammenfassung
|
||||
*/
|
||||
function renderQaSessionSummary() {
|
||||
const percent = qaSessionStats.total > 0 ? Math.round(qaSessionStats.correct / qaSessionStats.total * 100) : 0;
|
||||
const emoji = percent >= 80 ? '🎉' : percent >= 50 ? '👍' : '💪';
|
||||
|
||||
let html = `
|
||||
<div style="text-align:center;padding:20px;">
|
||||
<div style="font-size:48px;margin-bottom:16px;">${emoji}</div>
|
||||
<div style="font-size:24px;font-weight:600;margin-bottom:8px;">${t('qa_session_complete') || 'Lernrunde abgeschlossen!'}</div>
|
||||
<div style="font-size:18px;margin-bottom:24px;">
|
||||
${qaSessionStats.correct} / ${qaSessionStats.total} ${t('qa_result_correct') || 'richtig'} (${percent}%)
|
||||
</div>
|
||||
|
||||
<div style="display:flex;justify-content:center;gap:24px;margin-bottom:24px;">
|
||||
<div style="text-align:center;">
|
||||
<div style="font-size:32px;color:#22c55e;">${qaSessionStats.correct}</div>
|
||||
<div style="font-size:11px;color:var(--bp-text-muted);">${t('qa_correct') || 'Richtig'}</div>
|
||||
</div>
|
||||
<div style="text-align:center;">
|
||||
<div style="font-size:32px;color:#ef4444;">${qaSessionStats.incorrect}</div>
|
||||
<div style="font-size:11px;color:var(--bp-text-muted);">${t('qa_incorrect') || 'Falsch'}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="display:flex;gap:12px;justify-content:center;">
|
||||
<button class="btn btn-primary" id="btn-qa-restart">${t('qa_restart') || 'Nochmal lernen'}</button>
|
||||
<button class="btn btn-ghost" id="btn-qa-close-summary">${t('close') || 'Schließen'}</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
if (qaModalBody) {
|
||||
qaModalBody.innerHTML = html;
|
||||
|
||||
document.getElementById('btn-qa-restart')?.addEventListener('click', () => {
|
||||
currentQaIndex = 0;
|
||||
qaSessionStats = { correct: 0, incorrect: 0, total: 0 };
|
||||
renderQaLearningCard();
|
||||
});
|
||||
|
||||
document.getElementById('btn-qa-close-summary')?.addEventListener('click', closeQaModal);
|
||||
|
||||
// Aktualisiere Preview
|
||||
loadQaPreviewForCurrent();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Öffnet den Druck-Dialog
|
||||
*/
|
||||
function openQaPrintDialog() {
|
||||
if (!currentQaData) {
|
||||
alert(t('qa_no_questions') || 'Keine Q&A vorhanden.');
|
||||
return;
|
||||
}
|
||||
|
||||
const currentFile = getCurrentFileCallback();
|
||||
if (!currentFile) return;
|
||||
|
||||
const choice = confirm((t('qa_print_with_answers') || 'Mit Lösungen drucken?') + '\n\nOK = Mit Lösungen\nAbbrechen = Nur Fragen');
|
||||
const url = '/api/print-qa/' + encodeURIComponent(currentFile) + '?show_answers=' + choice;
|
||||
window.open(url, '_blank');
|
||||
}
|
||||
|
||||
/**
|
||||
* Gibt die aktuellen Q&A-Daten zurück
|
||||
*/
|
||||
export function getQaData() {
|
||||
return currentQaData;
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper: HTML-Escape
|
||||
*/
|
||||
function escapeHtml(text) {
|
||||
const div = document.createElement('div');
|
||||
div.textContent = text || '';
|
||||
return div.innerHTML;
|
||||
}
|
||||
105
backend/frontend/static/js/modules/theme.js
Normal file
105
backend/frontend/static/js/modules/theme.js
Normal file
@@ -0,0 +1,105 @@
|
||||
/**
|
||||
* BreakPilot Studio - Theme Module
|
||||
*
|
||||
* Dark/Light Mode Toggle-Funktionalität:
|
||||
* - Speichert Präferenz in localStorage
|
||||
* - Unterstützt data-theme Attribut auf <html>
|
||||
*
|
||||
* Refactored: 2026-01-19
|
||||
*/
|
||||
|
||||
// Initialisiere Theme sofort beim Laden (IIFE)
|
||||
(function initializeTheme() {
|
||||
const savedTheme = localStorage.getItem('bp-theme') || 'dark';
|
||||
document.documentElement.setAttribute('data-theme', savedTheme);
|
||||
console.log('Initial theme set to:', savedTheme);
|
||||
})();
|
||||
|
||||
/**
|
||||
* Holt das aktuelle Theme
|
||||
* @returns {string} - 'dark' oder 'light'
|
||||
*/
|
||||
export function getCurrentTheme() {
|
||||
return document.documentElement.getAttribute('data-theme') || 'dark';
|
||||
}
|
||||
|
||||
/**
|
||||
* Setzt das Theme
|
||||
* @param {string} theme - 'dark' oder 'light'
|
||||
*/
|
||||
export function setTheme(theme) {
|
||||
if (theme !== 'dark' && theme !== 'light') {
|
||||
console.warn(`Invalid theme: ${theme}. Use 'dark' or 'light'.`);
|
||||
return;
|
||||
}
|
||||
document.documentElement.setAttribute('data-theme', theme);
|
||||
localStorage.setItem('bp-theme', theme);
|
||||
|
||||
// Custom Event für andere Module
|
||||
window.dispatchEvent(new CustomEvent('themeChanged', {
|
||||
detail: { theme }
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
* Wechselt zwischen Dark und Light Mode
|
||||
* @returns {string} - Das neue Theme
|
||||
*/
|
||||
export function toggleTheme() {
|
||||
const current = getCurrentTheme();
|
||||
const newTheme = current === 'dark' ? 'light' : 'dark';
|
||||
setTheme(newTheme);
|
||||
return newTheme;
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialisiert den Theme-Toggle-Button
|
||||
* Sucht nach Elements mit IDs: theme-toggle, theme-icon, theme-label
|
||||
*/
|
||||
export function initThemeToggle() {
|
||||
const toggle = document.getElementById('theme-toggle');
|
||||
const icon = document.getElementById('theme-icon');
|
||||
const label = document.getElementById('theme-label');
|
||||
|
||||
if (!toggle || !icon || !label) {
|
||||
console.warn('Theme toggle elements not found (theme-toggle, theme-icon, theme-label)');
|
||||
return;
|
||||
}
|
||||
|
||||
function updateToggleUI(theme) {
|
||||
if (theme === 'light') {
|
||||
icon.textContent = '☀️';
|
||||
label.textContent = 'Light';
|
||||
} else {
|
||||
icon.textContent = '🌙';
|
||||
label.textContent = 'Dark';
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize UI based on current theme
|
||||
updateToggleUI(getCurrentTheme());
|
||||
|
||||
// Click-Handler
|
||||
toggle.addEventListener('click', function() {
|
||||
console.log('Theme toggle clicked');
|
||||
const newTheme = toggleTheme();
|
||||
console.log('Switched to:', newTheme);
|
||||
updateToggleUI(newTheme);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Prüft ob Dark Mode aktiv ist
|
||||
* @returns {boolean}
|
||||
*/
|
||||
export function isDarkMode() {
|
||||
return getCurrentTheme() === 'dark';
|
||||
}
|
||||
|
||||
/**
|
||||
* Prüft ob Light Mode aktiv ist
|
||||
* @returns {boolean}
|
||||
*/
|
||||
export function isLightMode() {
|
||||
return getCurrentTheme() === 'light';
|
||||
}
|
||||
971
backend/frontend/static/js/modules/translations.js
Normal file
971
backend/frontend/static/js/modules/translations.js
Normal file
@@ -0,0 +1,971 @@
|
||||
/**
|
||||
* BreakPilot Studio - Translations Module
|
||||
*
|
||||
* Enthält alle UI-Übersetzungen für 7 Sprachen:
|
||||
* - de: Deutsch (Standard)
|
||||
* - en: English
|
||||
* - tr: Türkisch
|
||||
* - ar: Arabisch (RTL)
|
||||
* - ru: Russisch
|
||||
* - uk: Ukrainisch
|
||||
* - pl: Polnisch
|
||||
*
|
||||
* Refactored: 2026-01-19
|
||||
*/
|
||||
|
||||
export const translations = {
|
||||
de: {
|
||||
// Navigation & Header
|
||||
brand_sub: "Studio",
|
||||
nav_compare: "Arbeitsblätter",
|
||||
nav_tiles: "Lern-Kacheln",
|
||||
login: "Login / Anmeldung",
|
||||
mvp_local: "MVP · Lokal auf deinem Mac",
|
||||
|
||||
// Sidebar
|
||||
sidebar_areas: "Bereiche",
|
||||
sidebar_studio: "Arbeitsblatt Studio",
|
||||
sidebar_active: "aktiv",
|
||||
sidebar_parents: "Eltern-Kanal",
|
||||
sidebar_soon: "demnächst",
|
||||
sidebar_correction: "Korrektur / Noten",
|
||||
sidebar_units: "Lerneinheiten (lokal)",
|
||||
input_student: "Schüler/in",
|
||||
input_subject: "Fach",
|
||||
input_grade: "Klasse (z.B. 7a)",
|
||||
input_unit_title: "Lerneinheit / Thema",
|
||||
btn_create: "Anlegen",
|
||||
btn_add_current: "Aktuelles Arbeitsblatt hinzufügen",
|
||||
btn_filter_unit: "Nur Lerneinheit",
|
||||
btn_filter_all: "Alle Dateien",
|
||||
|
||||
// Screen 1 - Compare
|
||||
uploaded_worksheets: "Hochgeladene Arbeitsblätter",
|
||||
files: "Dateien",
|
||||
btn_upload: "Hochladen",
|
||||
btn_delete: "Löschen",
|
||||
original_scan: "Original-Scan",
|
||||
cleaned_version: "Bereinigt (Handschrift entfernt)",
|
||||
no_cleaned: "Noch keine bereinigte Version vorhanden.",
|
||||
process_hint: "Klicke auf 'Verarbeiten', um das Arbeitsblatt zu analysieren und zu bereinigen.",
|
||||
worksheet_print: "Drucken",
|
||||
worksheet_no_data: "Keine Arbeitsblatt-Daten vorhanden.",
|
||||
btn_full_process: "Verarbeiten (Analyse + Bereinigung + HTML)",
|
||||
btn_original_generate: "Nur Original-HTML generieren",
|
||||
|
||||
// Screen 2 - Tiles
|
||||
learning_unit: "Lerneinheit",
|
||||
no_unit_selected: "Keine Lerneinheit ausgewählt",
|
||||
|
||||
// MC Tile
|
||||
mc_title: "Multiple Choice Test",
|
||||
mc_ready: "Bereit",
|
||||
mc_generating: "Generiert...",
|
||||
mc_done: "Fertig",
|
||||
mc_error: "Fehler",
|
||||
mc_desc: "Erzeugt passende MC-Aufgaben zur ursprünglichen Schwierigkeit (z. B. Klasse 7), ohne das Niveau zu verändern.",
|
||||
mc_generate: "MC generieren",
|
||||
mc_show: "Fragen anzeigen",
|
||||
mc_quiz_title: "Multiple Choice Quiz",
|
||||
mc_evaluate: "Auswerten",
|
||||
mc_correct: "Richtig!",
|
||||
mc_incorrect: "Leider falsch.",
|
||||
mc_not_answered: "Nicht beantwortet. Richtig wäre:",
|
||||
mc_result: "von",
|
||||
mc_result_correct: "richtig",
|
||||
mc_percent: "korrekt",
|
||||
mc_no_questions: "Noch keine MC-Fragen für dieses Arbeitsblatt generiert.",
|
||||
mc_print: "Drucken",
|
||||
mc_print_with_answers: "Mit Lösungen drucken?",
|
||||
|
||||
// Cloze Tile
|
||||
cloze_title: "Lückentext",
|
||||
cloze_desc: "Erzeugt Lückentexte mit mehreren sinnvollen Lücken pro Satz. Inkl. Übersetzung für Eltern.",
|
||||
cloze_translation: "Übersetzung:",
|
||||
cloze_generate: "Lückentext generieren",
|
||||
cloze_start: "Übung starten",
|
||||
cloze_exercise_title: "Lückentext-Übung",
|
||||
cloze_instruction: "Fülle die Lücken aus und klicke auf 'Prüfen'.",
|
||||
cloze_check: "Prüfen",
|
||||
cloze_show_answers: "Lösungen zeigen",
|
||||
cloze_no_texts: "Noch keine Lückentexte für dieses Arbeitsblatt generiert.",
|
||||
cloze_sentences: "Sätze",
|
||||
cloze_gaps: "Lücken",
|
||||
cloze_gaps_total: "Lücken gesamt",
|
||||
cloze_with_gaps: "(mit Lücken)",
|
||||
cloze_print: "Drucken",
|
||||
cloze_print_with_answers: "Mit Lösungen drucken?",
|
||||
|
||||
// QA Tile
|
||||
qa_title: "Frage-Antwort-Blatt",
|
||||
qa_desc: "Frage-Antwort-Paare mit Leitner-Box System. Wiederholung nach Schwierigkeitsgrad.",
|
||||
qa_generate: "Q&A generieren",
|
||||
qa_learn: "Lernen starten",
|
||||
qa_print: "Drucken",
|
||||
qa_no_questions: "Noch keine Q&A für dieses Arbeitsblatt generiert.",
|
||||
qa_box_new: "Neu",
|
||||
qa_box_learning: "Gelernt",
|
||||
qa_box_mastered: "Gefestigt",
|
||||
qa_show_answer: "Antwort zeigen",
|
||||
qa_your_answer: "Deine Antwort",
|
||||
qa_type_answer: "Schreibe deine Antwort hier...",
|
||||
qa_check_answer: "Antwort prüfen",
|
||||
qa_correct_answer: "Richtige Antwort",
|
||||
qa_self_evaluate: "War deine Antwort richtig?",
|
||||
qa_no_answer: "(keine Antwort eingegeben)",
|
||||
qa_correct: "Richtig",
|
||||
qa_incorrect: "Falsch",
|
||||
qa_key_terms: "Schlüsselbegriffe",
|
||||
qa_session_correct: "Richtig",
|
||||
qa_session_incorrect: "Falsch",
|
||||
qa_session_complete: "Lernrunde abgeschlossen!",
|
||||
qa_result_correct: "richtig",
|
||||
qa_restart: "Nochmal lernen",
|
||||
qa_print_with_answers: "Mit Lösungen drucken?",
|
||||
question: "Frage",
|
||||
answer: "Antwort",
|
||||
status_generating_qa: "Generiere Q&A …",
|
||||
status_qa_generated: "Q&A generiert",
|
||||
|
||||
// Common
|
||||
close: "Schließen",
|
||||
subject: "Fach",
|
||||
grade: "Stufe",
|
||||
questions: "Fragen",
|
||||
worksheet: "Arbeitsblatt",
|
||||
loading: "Lädt...",
|
||||
error: "Fehler",
|
||||
success: "Erfolgreich",
|
||||
|
||||
// Footer
|
||||
imprint: "Impressum",
|
||||
privacy: "Datenschutz",
|
||||
contact: "Kontakt",
|
||||
|
||||
// Status messages
|
||||
status_ready: "Bereit",
|
||||
status_processing: "Verarbeitet...",
|
||||
status_generating_mc: "Generiere MC-Fragen …",
|
||||
status_generating_cloze: "Generiere Lückentexte …",
|
||||
status_please_wait: "Bitte warten, KI arbeitet.",
|
||||
status_mc_generated: "MC-Fragen generiert",
|
||||
status_cloze_generated: "Lückentexte generiert",
|
||||
status_files_created: "Dateien erstellt",
|
||||
|
||||
// Mindmap Tile
|
||||
mindmap_title: "Mindmap Lernposter",
|
||||
mindmap_desc: "Erstellt eine kindgerechte Mindmap mit dem Hauptthema in der Mitte und allen Fachbegriffen in farbigen Kategorien.",
|
||||
mindmap_generate: "Mindmap erstellen",
|
||||
mindmap_show: "Ansehen",
|
||||
mindmap_print_a3: "A3 Drucken",
|
||||
generating_mindmap: "Erstelle Mindmap...",
|
||||
mindmap_generated: "Mindmap erstellt!",
|
||||
no_analysis: "Keine Analyse",
|
||||
analyze_first: "Bitte zuerst analysieren (Verarbeiten starten)",
|
||||
categories: "Kategorien",
|
||||
terms: "Begriffe",
|
||||
},
|
||||
|
||||
tr: {
|
||||
brand_sub: "Stüdyo",
|
||||
nav_compare: "Çalışma Sayfaları",
|
||||
nav_tiles: "Öğrenme Kartları",
|
||||
login: "Giriş / Kayıt",
|
||||
mvp_local: "MVP · Mac'inizde Yerel",
|
||||
sidebar_areas: "Alanlar",
|
||||
sidebar_studio: "Çalışma Sayfası Stüdyosu",
|
||||
sidebar_active: "aktif",
|
||||
sidebar_parents: "Ebeveyn Kanalı",
|
||||
sidebar_soon: "yakında",
|
||||
sidebar_correction: "Düzeltme / Notlar",
|
||||
sidebar_units: "Öğrenme Birimleri (yerel)",
|
||||
input_student: "Öğrenci",
|
||||
input_subject: "Ders",
|
||||
input_grade: "Sınıf (örn. 7a)",
|
||||
input_unit_title: "Öğrenme Birimi / Konu",
|
||||
btn_create: "Oluştur",
|
||||
btn_add_current: "Mevcut çalışma sayfasını ekle",
|
||||
btn_filter_unit: "Sadece Birim",
|
||||
btn_filter_all: "Tüm Dosyalar",
|
||||
uploaded_worksheets: "Yüklenen Çalışma Sayfaları",
|
||||
files: "Dosya",
|
||||
btn_upload: "Yükle",
|
||||
btn_delete: "Sil",
|
||||
original_scan: "Orijinal Tarama",
|
||||
cleaned_version: "Temizlenmiş (El yazısı kaldırıldı)",
|
||||
no_cleaned: "Henüz temizlenmiş sürüm yok.",
|
||||
process_hint: "Çalışma sayfasını analiz etmek ve temizlemek için 'İşle'ye tıklayın.",
|
||||
worksheet_print: "Yazdır",
|
||||
worksheet_no_data: "Çalışma sayfası verisi yok.",
|
||||
btn_full_process: "İşle (Analiz + Temizleme + HTML)",
|
||||
btn_original_generate: "Sadece Orijinal HTML Oluştur",
|
||||
learning_unit: "Öğrenme Birimi",
|
||||
no_unit_selected: "Öğrenme birimi seçilmedi",
|
||||
mc_title: "Çoktan Seçmeli Test",
|
||||
mc_ready: "Hazır",
|
||||
mc_generating: "Oluşturuluyor...",
|
||||
mc_done: "Tamamlandı",
|
||||
mc_error: "Hata",
|
||||
mc_desc: "Orijinal zorluğa uygun (örn. 7. sınıf) çoktan seçmeli sorular oluşturur.",
|
||||
mc_generate: "ÇS Oluştur",
|
||||
mc_show: "Soruları Göster",
|
||||
mc_quiz_title: "Çoktan Seçmeli Quiz",
|
||||
mc_evaluate: "Değerlendir",
|
||||
mc_correct: "Doğru!",
|
||||
mc_incorrect: "Maalesef yanlış.",
|
||||
mc_not_answered: "Cevaplanmadı. Doğru cevap:",
|
||||
mc_result: "/",
|
||||
mc_result_correct: "doğru",
|
||||
mc_percent: "doğru",
|
||||
mc_no_questions: "Bu çalışma sayfası için henüz ÇS sorusu oluşturulmadı.",
|
||||
mc_print: "Yazdır",
|
||||
mc_print_with_answers: "Cevaplarla yazdır?",
|
||||
cloze_title: "Boşluk Doldurma",
|
||||
cloze_desc: "Her cümlede birden fazla anlamlı boşluk içeren metinler oluşturur. Ebeveynler için çeviri dahil.",
|
||||
cloze_translation: "Çeviri:",
|
||||
cloze_generate: "Boşluk Metni Oluştur",
|
||||
cloze_start: "Alıştırmayı Başlat",
|
||||
cloze_exercise_title: "Boşluk Doldurma Alıştırması",
|
||||
cloze_instruction: "Boşlukları doldurun ve 'Kontrol Et'e tıklayın.",
|
||||
cloze_check: "Kontrol Et",
|
||||
cloze_show_answers: "Cevapları Göster",
|
||||
cloze_no_texts: "Bu çalışma sayfası için henüz boşluk metni oluşturulmadı.",
|
||||
cloze_sentences: "Cümle",
|
||||
cloze_gaps: "Boşluk",
|
||||
cloze_gaps_total: "Toplam boşluk",
|
||||
cloze_with_gaps: "(boşluklu)",
|
||||
cloze_print: "Yazdır",
|
||||
cloze_print_with_answers: "Cevaplarla yazdır?",
|
||||
qa_title: "Soru-Cevap Sayfası",
|
||||
qa_desc: "Leitner kutu sistemiyle soru-cevap çiftleri. Zorluk derecesine göre tekrar.",
|
||||
qa_generate: "S&C Oluştur",
|
||||
qa_learn: "Öğrenmeye Başla",
|
||||
qa_print: "Yazdır",
|
||||
qa_no_questions: "Bu çalışma sayfası için henüz S&C oluşturulmadı.",
|
||||
qa_box_new: "Yeni",
|
||||
qa_box_learning: "Öğreniliyor",
|
||||
qa_box_mastered: "Pekiştirildi",
|
||||
qa_show_answer: "Cevabı Göster",
|
||||
qa_your_answer: "Senin Cevabın",
|
||||
qa_type_answer: "Cevabını buraya yaz...",
|
||||
qa_check_answer: "Cevabı Kontrol Et",
|
||||
qa_correct_answer: "Doğru Cevap",
|
||||
qa_self_evaluate: "Cevabın doğru muydu?",
|
||||
qa_no_answer: "(cevap girilmedi)",
|
||||
qa_correct: "Doğru",
|
||||
qa_incorrect: "Yanlış",
|
||||
qa_key_terms: "Anahtar Kavramlar",
|
||||
qa_session_correct: "Doğru",
|
||||
qa_session_incorrect: "Yanlış",
|
||||
qa_session_complete: "Öğrenme turu tamamlandı!",
|
||||
qa_result_correct: "doğru",
|
||||
qa_restart: "Tekrar Öğren",
|
||||
qa_print_with_answers: "Cevaplarla yazdır?",
|
||||
question: "Soru",
|
||||
answer: "Cevap",
|
||||
status_generating_qa: "S&C oluşturuluyor…",
|
||||
status_qa_generated: "S&C oluşturuldu",
|
||||
close: "Kapat",
|
||||
subject: "Ders",
|
||||
grade: "Seviye",
|
||||
questions: "Soru",
|
||||
worksheet: "Çalışma Sayfası",
|
||||
loading: "Yükleniyor...",
|
||||
error: "Hata",
|
||||
success: "Başarılı",
|
||||
imprint: "Künye",
|
||||
privacy: "Gizlilik",
|
||||
contact: "İletişim",
|
||||
status_ready: "Hazır",
|
||||
status_processing: "İşleniyor...",
|
||||
status_generating_mc: "ÇS soruları oluşturuluyor…",
|
||||
status_generating_cloze: "Boşluk metinleri oluşturuluyor…",
|
||||
status_please_wait: "Lütfen bekleyin, yapay zeka çalışıyor.",
|
||||
status_mc_generated: "ÇS soruları oluşturuldu",
|
||||
status_cloze_generated: "Boşluk metinleri oluşturuldu",
|
||||
status_files_created: "dosya oluşturuldu",
|
||||
mindmap_title: "Zihin Haritası Poster",
|
||||
mindmap_desc: "Ana konuyu ortada ve tüm terimleri renkli kategorilerde gösteren çocuk dostu bir zihin haritası oluşturur.",
|
||||
mindmap_generate: "Zihin Haritası Oluştur",
|
||||
mindmap_show: "Görüntüle",
|
||||
mindmap_print_a3: "A3 Yazdır",
|
||||
generating_mindmap: "Zihin haritası oluşturuluyor...",
|
||||
mindmap_generated: "Zihin haritası oluşturuldu!",
|
||||
no_analysis: "Analiz yok",
|
||||
analyze_first: "Lütfen önce analiz edin (İşle'ye tıklayın)",
|
||||
categories: "Kategoriler",
|
||||
terms: "Terimler",
|
||||
},
|
||||
|
||||
ar: {
|
||||
brand_sub: "ستوديو",
|
||||
nav_compare: "أوراق العمل",
|
||||
nav_tiles: "بطاقات التعلم",
|
||||
login: "تسجيل الدخول / التسجيل",
|
||||
mvp_local: "MVP · محلي على جهازك",
|
||||
sidebar_areas: "الأقسام",
|
||||
sidebar_studio: "استوديو أوراق العمل",
|
||||
sidebar_active: "نشط",
|
||||
sidebar_parents: "قناة الوالدين",
|
||||
sidebar_soon: "قريباً",
|
||||
sidebar_correction: "التصحيح / الدرجات",
|
||||
sidebar_units: "وحدات التعلم (محلية)",
|
||||
input_student: "الطالب/ة",
|
||||
input_subject: "المادة",
|
||||
input_grade: "الصف (مثل 7أ)",
|
||||
input_unit_title: "وحدة التعلم / الموضوع",
|
||||
btn_create: "إنشاء",
|
||||
btn_add_current: "إضافة ورقة العمل الحالية",
|
||||
btn_filter_unit: "الوحدة فقط",
|
||||
btn_filter_all: "جميع الملفات",
|
||||
uploaded_worksheets: "أوراق العمل المحملة",
|
||||
files: "ملفات",
|
||||
btn_upload: "تحميل",
|
||||
btn_delete: "حذف",
|
||||
original_scan: "المسح الأصلي",
|
||||
cleaned_version: "منظف (تم إزالة الكتابة اليدوية)",
|
||||
no_cleaned: "لا توجد نسخة منظفة بعد.",
|
||||
process_hint: "انقر على 'معالجة' لتحليل وتنظيف ورقة العمل.",
|
||||
worksheet_print: "طباعة",
|
||||
worksheet_no_data: "لا توجد بيانات ورقة العمل.",
|
||||
btn_full_process: "معالجة (تحليل + تنظيف + HTML)",
|
||||
btn_original_generate: "إنشاء HTML الأصلي فقط",
|
||||
learning_unit: "وحدة التعلم",
|
||||
no_unit_selected: "لم يتم اختيار وحدة تعلم",
|
||||
mc_title: "اختبار متعدد الخيارات",
|
||||
mc_ready: "جاهز",
|
||||
mc_generating: "جاري الإنشاء...",
|
||||
mc_done: "تم",
|
||||
mc_error: "خطأ",
|
||||
mc_desc: "ينشئ أسئلة اختيار من متعدد تناسب مستوى الصعوبة الأصلي (مثل الصف 7).",
|
||||
mc_generate: "إنشاء أسئلة",
|
||||
mc_show: "عرض الأسئلة",
|
||||
mc_quiz_title: "اختبار متعدد الخيارات",
|
||||
mc_evaluate: "تقييم",
|
||||
mc_correct: "صحيح!",
|
||||
mc_incorrect: "للأسف خطأ.",
|
||||
mc_not_answered: "لم تتم الإجابة. الإجابة الصحيحة:",
|
||||
mc_result: "من",
|
||||
mc_result_correct: "صحيح",
|
||||
mc_percent: "صحيح",
|
||||
mc_no_questions: "لم يتم إنشاء أسئلة بعد لورقة العمل هذه.",
|
||||
mc_print: "طباعة",
|
||||
mc_print_with_answers: "طباعة مع الإجابات؟",
|
||||
cloze_title: "ملء الفراغات",
|
||||
cloze_desc: "ينشئ نصوصاً بفراغات متعددة في كل جملة. يشمل الترجمة للوالدين.",
|
||||
cloze_translation: "الترجمة:",
|
||||
cloze_generate: "إنشاء نص الفراغات",
|
||||
cloze_start: "بدء التمرين",
|
||||
cloze_exercise_title: "تمرين ملء الفراغات",
|
||||
cloze_instruction: "املأ الفراغات وانقر على 'تحقق'.",
|
||||
cloze_check: "تحقق",
|
||||
cloze_show_answers: "عرض الإجابات",
|
||||
cloze_no_texts: "لم يتم إنشاء نصوص فراغات بعد لورقة العمل هذه.",
|
||||
cloze_sentences: "جمل",
|
||||
cloze_gaps: "فراغات",
|
||||
cloze_gaps_total: "إجمالي الفراغات",
|
||||
cloze_with_gaps: "(مع فراغات)",
|
||||
cloze_print: "طباعة",
|
||||
cloze_print_with_answers: "طباعة مع الإجابات؟",
|
||||
qa_title: "ورقة الأسئلة والأجوبة",
|
||||
qa_desc: "أزواج أسئلة وأجوبة مع نظام صندوق لايتنر. التكرار حسب الصعوبة.",
|
||||
qa_generate: "إنشاء س&ج",
|
||||
qa_learn: "بدء التعلم",
|
||||
qa_print: "طباعة",
|
||||
qa_no_questions: "لم يتم إنشاء س&ج بعد لورقة العمل هذه.",
|
||||
qa_box_new: "جديد",
|
||||
qa_box_learning: "قيد التعلم",
|
||||
qa_box_mastered: "متقن",
|
||||
qa_show_answer: "عرض الإجابة",
|
||||
qa_your_answer: "إجابتك",
|
||||
qa_type_answer: "اكتب إجابتك هنا...",
|
||||
qa_check_answer: "تحقق من الإجابة",
|
||||
qa_correct_answer: "الإجابة الصحيحة",
|
||||
qa_self_evaluate: "هل كانت إجابتك صحيحة؟",
|
||||
qa_no_answer: "(لم يتم إدخال إجابة)",
|
||||
qa_correct: "صحيح",
|
||||
qa_incorrect: "خطأ",
|
||||
qa_key_terms: "المصطلحات الرئيسية",
|
||||
qa_session_correct: "صحيح",
|
||||
qa_session_incorrect: "خطأ",
|
||||
qa_session_complete: "اكتملت جولة التعلم!",
|
||||
qa_result_correct: "صحيح",
|
||||
qa_restart: "تعلم مرة أخرى",
|
||||
qa_print_with_answers: "طباعة مع الإجابات؟",
|
||||
question: "سؤال",
|
||||
answer: "إجابة",
|
||||
status_generating_qa: "جاري إنشاء س&ج…",
|
||||
status_qa_generated: "تم إنشاء س&ج",
|
||||
close: "إغلاق",
|
||||
subject: "المادة",
|
||||
grade: "المستوى",
|
||||
questions: "أسئلة",
|
||||
worksheet: "ورقة العمل",
|
||||
loading: "جاري التحميل...",
|
||||
error: "خطأ",
|
||||
success: "نجاح",
|
||||
imprint: "البصمة",
|
||||
privacy: "الخصوصية",
|
||||
contact: "اتصل بنا",
|
||||
status_ready: "جاهز",
|
||||
status_processing: "جاري المعالجة...",
|
||||
status_generating_mc: "جاري إنشاء الأسئلة…",
|
||||
status_generating_cloze: "جاري إنشاء نصوص الفراغات…",
|
||||
status_please_wait: "يرجى الانتظار، الذكاء الاصطناعي يعمل.",
|
||||
status_mc_generated: "تم إنشاء الأسئلة",
|
||||
status_cloze_generated: "تم إنشاء نصوص الفراغات",
|
||||
status_files_created: "ملفات تم إنشاؤها",
|
||||
mindmap_title: "ملصق خريطة ذهنية",
|
||||
mindmap_desc: "ينشئ خريطة ذهنية مناسبة للأطفال مع الموضوع الرئيسي في المنتصف وجميع المصطلحات في فئات ملونة.",
|
||||
mindmap_generate: "إنشاء خريطة ذهنية",
|
||||
mindmap_show: "عرض",
|
||||
mindmap_print_a3: "طباعة A3",
|
||||
generating_mindmap: "جاري إنشاء الخريطة الذهنية...",
|
||||
mindmap_generated: "تم إنشاء الخريطة الذهنية!",
|
||||
no_analysis: "لا يوجد تحليل",
|
||||
analyze_first: "يرجى التحليل أولاً (انقر على معالجة)",
|
||||
categories: "الفئات",
|
||||
terms: "المصطلحات",
|
||||
},
|
||||
|
||||
ru: {
|
||||
brand_sub: "Студия",
|
||||
nav_compare: "Рабочие листы",
|
||||
nav_tiles: "Учебные карточки",
|
||||
login: "Вход / Регистрация",
|
||||
mvp_local: "MVP · Локально на вашем Mac",
|
||||
sidebar_areas: "Разделы",
|
||||
sidebar_studio: "Студия рабочих листов",
|
||||
sidebar_active: "активно",
|
||||
sidebar_parents: "Канал для родителей",
|
||||
sidebar_soon: "скоро",
|
||||
sidebar_correction: "Проверка / Оценки",
|
||||
sidebar_units: "Учебные блоки (локально)",
|
||||
input_student: "Ученик",
|
||||
input_subject: "Предмет",
|
||||
input_grade: "Класс (напр. 7а)",
|
||||
input_unit_title: "Учебный блок / Тема",
|
||||
btn_create: "Создать",
|
||||
btn_add_current: "Добавить текущий лист",
|
||||
btn_filter_unit: "Только блок",
|
||||
btn_filter_all: "Все файлы",
|
||||
uploaded_worksheets: "Загруженные рабочие листы",
|
||||
files: "файлов",
|
||||
btn_upload: "Загрузить",
|
||||
btn_delete: "Удалить",
|
||||
original_scan: "Оригинальный скан",
|
||||
cleaned_version: "Очищено (рукопись удалена)",
|
||||
no_cleaned: "Очищенная версия пока недоступна.",
|
||||
process_hint: "Нажмите 'Обработать' для анализа и очистки листа.",
|
||||
worksheet_print: "Печать",
|
||||
worksheet_no_data: "Нет данных рабочего листа.",
|
||||
btn_full_process: "Обработать (Анализ + Очистка + HTML)",
|
||||
btn_original_generate: "Только оригинальный HTML",
|
||||
learning_unit: "Учебный блок",
|
||||
no_unit_selected: "Блок не выбран",
|
||||
mc_title: "Тест с выбором ответа",
|
||||
mc_ready: "Готово",
|
||||
mc_generating: "Создается...",
|
||||
mc_done: "Готово",
|
||||
mc_error: "Ошибка",
|
||||
mc_desc: "Создает вопросы с выбором ответа соответствующей сложности (напр. 7 класс).",
|
||||
mc_generate: "Создать тест",
|
||||
mc_show: "Показать вопросы",
|
||||
mc_quiz_title: "Тест с выбором ответа",
|
||||
mc_evaluate: "Оценить",
|
||||
mc_correct: "Правильно!",
|
||||
mc_incorrect: "К сожалению, неверно.",
|
||||
mc_not_answered: "Нет ответа. Правильный ответ:",
|
||||
mc_result: "из",
|
||||
mc_result_correct: "правильно",
|
||||
mc_percent: "верно",
|
||||
mc_no_questions: "Вопросы для этого листа еще не созданы.",
|
||||
mc_print: "Печать",
|
||||
mc_print_with_answers: "Печатать с ответами?",
|
||||
cloze_title: "Текст с пропусками",
|
||||
cloze_desc: "Создает тексты с несколькими пропусками в каждом предложении. Включая перевод для родителей.",
|
||||
cloze_translation: "Перевод:",
|
||||
cloze_generate: "Создать текст",
|
||||
cloze_start: "Начать упражнение",
|
||||
cloze_exercise_title: "Упражнение с пропусками",
|
||||
cloze_instruction: "Заполните пропуски и нажмите 'Проверить'.",
|
||||
cloze_check: "Проверить",
|
||||
cloze_show_answers: "Показать ответы",
|
||||
cloze_no_texts: "Тексты для этого листа еще не созданы.",
|
||||
cloze_sentences: "предложений",
|
||||
cloze_gaps: "пропусков",
|
||||
cloze_gaps_total: "Всего пропусков",
|
||||
cloze_with_gaps: "(с пропусками)",
|
||||
cloze_print: "Печать",
|
||||
cloze_print_with_answers: "Печатать с ответами?",
|
||||
qa_title: "Лист вопросов и ответов",
|
||||
qa_desc: "Пары вопрос-ответ с системой Лейтнера. Повторение по уровню сложности.",
|
||||
qa_generate: "Создать В&О",
|
||||
qa_learn: "Начать обучение",
|
||||
qa_print: "Печать",
|
||||
qa_no_questions: "В&О для этого листа еще не созданы.",
|
||||
qa_box_new: "Новый",
|
||||
qa_box_learning: "Изучается",
|
||||
qa_box_mastered: "Освоено",
|
||||
qa_show_answer: "Показать ответ",
|
||||
qa_your_answer: "Твой ответ",
|
||||
qa_type_answer: "Напиши свой ответ здесь...",
|
||||
qa_check_answer: "Проверить ответ",
|
||||
qa_correct_answer: "Правильный ответ",
|
||||
qa_self_evaluate: "Твой ответ был правильным?",
|
||||
qa_no_answer: "(ответ не введён)",
|
||||
qa_correct: "Правильно",
|
||||
qa_incorrect: "Неправильно",
|
||||
qa_key_terms: "Ключевые термины",
|
||||
qa_session_correct: "Правильно",
|
||||
qa_session_incorrect: "Неправильно",
|
||||
qa_session_complete: "Раунд обучения завершен!",
|
||||
qa_result_correct: "правильно",
|
||||
qa_restart: "Учить снова",
|
||||
qa_print_with_answers: "Печатать с ответами?",
|
||||
question: "Вопрос",
|
||||
answer: "Ответ",
|
||||
status_generating_qa: "Создание В&О…",
|
||||
status_qa_generated: "В&О созданы",
|
||||
close: "Закрыть",
|
||||
subject: "Предмет",
|
||||
grade: "Уровень",
|
||||
questions: "вопросов",
|
||||
worksheet: "Рабочий лист",
|
||||
loading: "Загрузка...",
|
||||
error: "Ошибка",
|
||||
success: "Успешно",
|
||||
imprint: "Импрессум",
|
||||
privacy: "Конфиденциальность",
|
||||
contact: "Контакт",
|
||||
status_ready: "Готово",
|
||||
status_processing: "Обработка...",
|
||||
status_generating_mc: "Создание вопросов…",
|
||||
status_generating_cloze: "Создание текстов…",
|
||||
status_please_wait: "Пожалуйста, подождите, ИИ работает.",
|
||||
status_mc_generated: "Вопросы созданы",
|
||||
status_cloze_generated: "Тексты созданы",
|
||||
status_files_created: "файлов создано",
|
||||
mindmap_title: "Плакат Майнд-карта",
|
||||
mindmap_desc: "Создает детскую ментальную карту с главной темой в центре и всеми терминами в цветных категориях.",
|
||||
mindmap_generate: "Создать карту",
|
||||
mindmap_show: "Просмотр",
|
||||
mindmap_print_a3: "Печать A3",
|
||||
generating_mindmap: "Создание карты...",
|
||||
mindmap_generated: "Карта создана!",
|
||||
no_analysis: "Нет анализа",
|
||||
analyze_first: "Сначала выполните анализ (нажмите Обработать)",
|
||||
categories: "Категории",
|
||||
terms: "Термины",
|
||||
},
|
||||
|
||||
uk: {
|
||||
brand_sub: "Студія",
|
||||
nav_compare: "Робочі аркуші",
|
||||
nav_tiles: "Навчальні картки",
|
||||
login: "Вхід / Реєстрація",
|
||||
mvp_local: "MVP · Локально на вашому Mac",
|
||||
sidebar_areas: "Розділи",
|
||||
sidebar_studio: "Студія робочих аркушів",
|
||||
sidebar_active: "активно",
|
||||
sidebar_parents: "Канал для батьків",
|
||||
sidebar_soon: "незабаром",
|
||||
sidebar_correction: "Перевірка / Оцінки",
|
||||
sidebar_units: "Навчальні блоки (локально)",
|
||||
input_student: "Учень",
|
||||
input_subject: "Предмет",
|
||||
input_grade: "Клас (напр. 7а)",
|
||||
input_unit_title: "Навчальний блок / Тема",
|
||||
btn_create: "Створити",
|
||||
btn_add_current: "Додати поточний аркуш",
|
||||
btn_filter_unit: "Лише блок",
|
||||
btn_filter_all: "Усі файли",
|
||||
uploaded_worksheets: "Завантажені робочі аркуші",
|
||||
files: "файлів",
|
||||
btn_upload: "Завантажити",
|
||||
btn_delete: "Видалити",
|
||||
original_scan: "Оригінальний скан",
|
||||
cleaned_version: "Очищено (рукопис видалено)",
|
||||
no_cleaned: "Очищена версія ще недоступна.",
|
||||
process_hint: "Натисніть 'Обробити' для аналізу та очищення аркуша.",
|
||||
worksheet_print: "Друк",
|
||||
worksheet_no_data: "Немає даних робочого аркуша.",
|
||||
btn_full_process: "Обробити (Аналіз + Очищення + HTML)",
|
||||
btn_original_generate: "Лише оригінальний HTML",
|
||||
learning_unit: "Навчальний блок",
|
||||
no_unit_selected: "Блок не вибрано",
|
||||
mc_title: "Тест з вибором відповіді",
|
||||
mc_ready: "Готово",
|
||||
mc_generating: "Створюється...",
|
||||
mc_done: "Готово",
|
||||
mc_error: "Помилка",
|
||||
mc_desc: "Створює питання з вибором відповіді відповідної складності (напр. 7 клас).",
|
||||
mc_generate: "Створити тест",
|
||||
mc_show: "Показати питання",
|
||||
mc_quiz_title: "Тест з вибором відповіді",
|
||||
mc_evaluate: "Оцінити",
|
||||
mc_correct: "Правильно!",
|
||||
mc_incorrect: "На жаль, неправильно.",
|
||||
mc_not_answered: "Немає відповіді. Правильна відповідь:",
|
||||
mc_result: "з",
|
||||
mc_result_correct: "правильно",
|
||||
mc_percent: "вірно",
|
||||
mc_no_questions: "Питання для цього аркуша ще не створені.",
|
||||
mc_print: "Друк",
|
||||
mc_print_with_answers: "Друкувати з відповідями?",
|
||||
cloze_title: "Текст з пропусками",
|
||||
cloze_desc: "Створює тексти з кількома пропусками в кожному реченні. Включаючи переклад для батьків.",
|
||||
cloze_translation: "Переклад:",
|
||||
cloze_generate: "Створити текст",
|
||||
cloze_start: "Почати вправу",
|
||||
cloze_exercise_title: "Вправа з пропусками",
|
||||
cloze_instruction: "Заповніть пропуски та натисніть 'Перевірити'.",
|
||||
cloze_check: "Перевірити",
|
||||
cloze_show_answers: "Показати відповіді",
|
||||
cloze_no_texts: "Тексти для цього аркуша ще не створені.",
|
||||
cloze_sentences: "речень",
|
||||
cloze_gaps: "пропусків",
|
||||
cloze_gaps_total: "Всього пропусків",
|
||||
cloze_with_gaps: "(з пропусками)",
|
||||
cloze_print: "Друк",
|
||||
cloze_print_with_answers: "Друкувати з відповідями?",
|
||||
qa_title: "Аркуш питань і відповідей",
|
||||
qa_desc: "Пари питання-відповідь з системою Лейтнера. Повторення за рівнем складності.",
|
||||
qa_generate: "Створити П&В",
|
||||
qa_learn: "Почати навчання",
|
||||
qa_print: "Друк",
|
||||
qa_no_questions: "П&В для цього аркуша ще не створені.",
|
||||
qa_box_new: "Новий",
|
||||
qa_box_learning: "Вивчається",
|
||||
qa_box_mastered: "Засвоєно",
|
||||
qa_show_answer: "Показати відповідь",
|
||||
qa_your_answer: "Твоя відповідь",
|
||||
qa_type_answer: "Напиши свою відповідь тут...",
|
||||
qa_check_answer: "Перевірити відповідь",
|
||||
qa_correct_answer: "Правильна відповідь",
|
||||
qa_self_evaluate: "Твоя відповідь була правильною?",
|
||||
qa_no_answer: "(відповідь не введена)",
|
||||
qa_correct: "Правильно",
|
||||
qa_incorrect: "Неправильно",
|
||||
qa_key_terms: "Ключові терміни",
|
||||
qa_session_correct: "Правильно",
|
||||
qa_session_incorrect: "Неправильно",
|
||||
qa_session_complete: "Раунд навчання завершено!",
|
||||
qa_result_correct: "правильно",
|
||||
qa_restart: "Вчити знову",
|
||||
qa_print_with_answers: "Друкувати з відповідями?",
|
||||
question: "Питання",
|
||||
answer: "Відповідь",
|
||||
status_generating_qa: "Створення П&В…",
|
||||
status_qa_generated: "П&В створені",
|
||||
close: "Закрити",
|
||||
subject: "Предмет",
|
||||
grade: "Рівень",
|
||||
questions: "питань",
|
||||
worksheet: "Робочий аркуш",
|
||||
loading: "Завантаження...",
|
||||
error: "Помилка",
|
||||
success: "Успішно",
|
||||
imprint: "Імпресум",
|
||||
privacy: "Конфіденційність",
|
||||
contact: "Контакт",
|
||||
status_ready: "Готово",
|
||||
status_processing: "Обробка...",
|
||||
status_generating_mc: "Створення питань…",
|
||||
status_generating_cloze: "Створення текстів…",
|
||||
status_please_wait: "Будь ласка, зачекайте, ШІ працює.",
|
||||
status_mc_generated: "Питання створені",
|
||||
status_cloze_generated: "Тексти створені",
|
||||
status_files_created: "файлів створено",
|
||||
mindmap_title: "Плакат Інтелект-карта",
|
||||
mindmap_desc: "Створює дитячу інтелект-карту з головною темою в центрі та всіма термінами в кольорових категоріях.",
|
||||
mindmap_generate: "Створити карту",
|
||||
mindmap_show: "Переглянути",
|
||||
mindmap_print_a3: "Друк A3",
|
||||
generating_mindmap: "Створення карти...",
|
||||
mindmap_generated: "Карту створено!",
|
||||
no_analysis: "Немає аналізу",
|
||||
analyze_first: "Спочатку виконайте аналіз (натисніть Обробити)",
|
||||
categories: "Категорії",
|
||||
terms: "Терміни",
|
||||
},
|
||||
|
||||
pl: {
|
||||
brand_sub: "Studio",
|
||||
nav_compare: "Karty pracy",
|
||||
nav_tiles: "Karty nauki",
|
||||
login: "Logowanie / Rejestracja",
|
||||
mvp_local: "MVP · Lokalnie na Twoim Mac",
|
||||
sidebar_areas: "Sekcje",
|
||||
sidebar_studio: "Studio kart pracy",
|
||||
sidebar_active: "aktywne",
|
||||
sidebar_parents: "Kanał dla rodziców",
|
||||
sidebar_soon: "wkrótce",
|
||||
sidebar_correction: "Korekta / Oceny",
|
||||
sidebar_units: "Jednostki nauki (lokalnie)",
|
||||
input_student: "Uczeń",
|
||||
input_subject: "Przedmiot",
|
||||
input_grade: "Klasa (np. 7a)",
|
||||
input_unit_title: "Jednostka nauki / Temat",
|
||||
btn_create: "Utwórz",
|
||||
btn_add_current: "Dodaj bieżącą kartę",
|
||||
btn_filter_unit: "Tylko jednostka",
|
||||
btn_filter_all: "Wszystkie pliki",
|
||||
uploaded_worksheets: "Przesłane karty pracy",
|
||||
files: "plików",
|
||||
btn_upload: "Prześlij",
|
||||
btn_delete: "Usuń",
|
||||
original_scan: "Oryginalny skan",
|
||||
cleaned_version: "Oczyszczone (pismo ręczne usunięte)",
|
||||
no_cleaned: "Oczyszczona wersja jeszcze niedostępna.",
|
||||
process_hint: "Kliknij 'Przetwórz', aby przeanalizować i oczyścić kartę.",
|
||||
worksheet_print: "Drukuj",
|
||||
worksheet_no_data: "Brak danych arkusza.",
|
||||
btn_full_process: "Przetwórz (Analiza + Czyszczenie + HTML)",
|
||||
btn_original_generate: "Tylko oryginalny HTML",
|
||||
learning_unit: "Jednostka nauki",
|
||||
no_unit_selected: "Nie wybrano jednostki",
|
||||
mc_title: "Test wielokrotnego wyboru",
|
||||
mc_ready: "Gotowe",
|
||||
mc_generating: "Tworzenie...",
|
||||
mc_done: "Gotowe",
|
||||
mc_error: "Błąd",
|
||||
mc_desc: "Tworzy pytania wielokrotnego wyboru o odpowiednim poziomie trudności (np. klasa 7).",
|
||||
mc_generate: "Utwórz test",
|
||||
mc_show: "Pokaż pytania",
|
||||
mc_quiz_title: "Test wielokrotnego wyboru",
|
||||
mc_evaluate: "Oceń",
|
||||
mc_correct: "Dobrze!",
|
||||
mc_incorrect: "Niestety źle.",
|
||||
mc_not_answered: "Brak odpowiedzi. Poprawna odpowiedź:",
|
||||
mc_result: "z",
|
||||
mc_result_correct: "poprawnie",
|
||||
mc_percent: "poprawnie",
|
||||
mc_no_questions: "Pytania dla tej karty jeszcze nie zostały utworzone.",
|
||||
mc_print: "Drukuj",
|
||||
mc_print_with_answers: "Drukować z odpowiedziami?",
|
||||
cloze_title: "Tekst z lukami",
|
||||
cloze_desc: "Tworzy teksty z wieloma lukami w każdym zdaniu. W tym tłumaczenie dla rodziców.",
|
||||
cloze_translation: "Tłumaczenie:",
|
||||
cloze_generate: "Utwórz tekst",
|
||||
cloze_start: "Rozpocznij ćwiczenie",
|
||||
cloze_exercise_title: "Ćwiczenie z lukami",
|
||||
cloze_instruction: "Wypełnij luki i kliknij 'Sprawdź'.",
|
||||
cloze_check: "Sprawdź",
|
||||
cloze_show_answers: "Pokaż odpowiedzi",
|
||||
cloze_no_texts: "Teksty dla tej karty jeszcze nie zostały utworzone.",
|
||||
cloze_sentences: "zdań",
|
||||
cloze_gaps: "luk",
|
||||
cloze_gaps_total: "Łącznie luk",
|
||||
cloze_with_gaps: "(z lukami)",
|
||||
cloze_print: "Drukuj",
|
||||
cloze_print_with_answers: "Drukować z odpowiedziami?",
|
||||
qa_title: "Arkusz pytań i odpowiedzi",
|
||||
qa_desc: "Pary pytanie-odpowiedź z systemem Leitnera. Powtórki według poziomu trudności.",
|
||||
qa_generate: "Utwórz P&O",
|
||||
qa_learn: "Rozpocznij naukę",
|
||||
qa_print: "Drukuj",
|
||||
qa_no_questions: "P&O dla tej karty jeszcze nie zostały utworzone.",
|
||||
qa_box_new: "Nowy",
|
||||
qa_box_learning: "W nauce",
|
||||
qa_box_mastered: "Opanowane",
|
||||
qa_show_answer: "Pokaż odpowiedź",
|
||||
qa_your_answer: "Twoja odpowiedź",
|
||||
qa_type_answer: "Napisz swoją odpowiedź tutaj...",
|
||||
qa_check_answer: "Sprawdź odpowiedź",
|
||||
qa_correct_answer: "Prawidłowa odpowiedź",
|
||||
qa_self_evaluate: "Czy twoja odpowiedź była poprawna?",
|
||||
qa_no_answer: "(nie wprowadzono odpowiedzi)",
|
||||
qa_correct: "Dobrze",
|
||||
qa_incorrect: "Źle",
|
||||
qa_key_terms: "Kluczowe pojęcia",
|
||||
qa_session_correct: "Dobrze",
|
||||
qa_session_incorrect: "Źle",
|
||||
qa_session_complete: "Runda nauki zakończona!",
|
||||
qa_result_correct: "poprawnie",
|
||||
qa_restart: "Ucz się ponownie",
|
||||
qa_print_with_answers: "Drukować z odpowiedziami?",
|
||||
question: "Pytanie",
|
||||
answer: "Odpowiedź",
|
||||
status_generating_qa: "Tworzenie P&O…",
|
||||
status_qa_generated: "P&O utworzone",
|
||||
close: "Zamknij",
|
||||
subject: "Przedmiot",
|
||||
grade: "Poziom",
|
||||
questions: "pytań",
|
||||
worksheet: "Karta pracy",
|
||||
loading: "Ładowanie...",
|
||||
error: "Błąd",
|
||||
success: "Sukces",
|
||||
imprint: "Impressum",
|
||||
privacy: "Prywatność",
|
||||
contact: "Kontakt",
|
||||
status_ready: "Gotowe",
|
||||
status_processing: "Przetwarzanie...",
|
||||
status_generating_mc: "Tworzenie pytań…",
|
||||
status_generating_cloze: "Tworzenie tekstów…",
|
||||
status_please_wait: "Proszę czekać, AI pracuje.",
|
||||
status_mc_generated: "Pytania utworzone",
|
||||
status_cloze_generated: "Teksty utworzone",
|
||||
status_files_created: "plików utworzono",
|
||||
mindmap_title: "Plakat Mapa myśli",
|
||||
mindmap_desc: "Tworzy przyjazną dla dzieci mapę myśli z głównym tematem w centrum i wszystkimi terminami w kolorowych kategoriach.",
|
||||
mindmap_generate: "Utwórz mapę",
|
||||
mindmap_show: "Podgląd",
|
||||
mindmap_print_a3: "Drukuj A3",
|
||||
generating_mindmap: "Tworzenie mapy...",
|
||||
mindmap_generated: "Mapa utworzona!",
|
||||
no_analysis: "Brak analizy",
|
||||
analyze_first: "Najpierw wykonaj analizę (kliknij Przetwórz)",
|
||||
categories: "Kategorie",
|
||||
terms: "Terminy",
|
||||
},
|
||||
|
||||
en: {
|
||||
brand_sub: "Studio",
|
||||
nav_compare: "Worksheets",
|
||||
nav_tiles: "Learning Tiles",
|
||||
login: "Login / Sign Up",
|
||||
mvp_local: "MVP · Local on your Mac",
|
||||
sidebar_areas: "Areas",
|
||||
sidebar_studio: "Worksheet Studio",
|
||||
sidebar_active: "active",
|
||||
sidebar_parents: "Parents Channel",
|
||||
sidebar_soon: "coming soon",
|
||||
sidebar_correction: "Correction / Grades",
|
||||
sidebar_units: "Learning Units (local)",
|
||||
input_student: "Student",
|
||||
input_subject: "Subject",
|
||||
input_grade: "Grade (e.g. 7a)",
|
||||
input_unit_title: "Learning Unit / Topic",
|
||||
btn_create: "Create",
|
||||
btn_add_current: "Add current worksheet",
|
||||
btn_filter_unit: "Unit only",
|
||||
btn_filter_all: "All files",
|
||||
uploaded_worksheets: "Uploaded Worksheets",
|
||||
files: "files",
|
||||
btn_upload: "Upload",
|
||||
btn_delete: "Delete",
|
||||
original_scan: "Original Scan",
|
||||
cleaned_version: "Cleaned (handwriting removed)",
|
||||
no_cleaned: "No cleaned version available yet.",
|
||||
process_hint: "Click 'Process' to analyze and clean the worksheet.",
|
||||
worksheet_print: "Print",
|
||||
worksheet_no_data: "No worksheet data available.",
|
||||
btn_full_process: "Process (Analysis + Cleaning + HTML)",
|
||||
btn_original_generate: "Generate Original HTML Only",
|
||||
learning_unit: "Learning Unit",
|
||||
no_unit_selected: "No unit selected",
|
||||
mc_title: "Multiple Choice Test",
|
||||
mc_ready: "Ready",
|
||||
mc_generating: "Generating...",
|
||||
mc_done: "Done",
|
||||
mc_error: "Error",
|
||||
mc_desc: "Creates multiple choice questions matching the original difficulty level (e.g. Grade 7).",
|
||||
mc_generate: "Generate MC",
|
||||
mc_show: "Show Questions",
|
||||
mc_quiz_title: "Multiple Choice Quiz",
|
||||
mc_evaluate: "Evaluate",
|
||||
mc_correct: "Correct!",
|
||||
mc_incorrect: "Unfortunately wrong.",
|
||||
mc_not_answered: "Not answered. Correct answer:",
|
||||
mc_result: "of",
|
||||
mc_result_correct: "correct",
|
||||
mc_percent: "correct",
|
||||
mc_no_questions: "No MC questions generated yet for this worksheet.",
|
||||
mc_print: "Print",
|
||||
mc_print_with_answers: "Print with answers?",
|
||||
cloze_title: "Fill in the Blanks",
|
||||
cloze_desc: "Creates texts with multiple meaningful gaps per sentence. Including translation for parents.",
|
||||
cloze_translation: "Translation:",
|
||||
cloze_generate: "Generate Cloze Text",
|
||||
cloze_start: "Start Exercise",
|
||||
cloze_exercise_title: "Fill in the Blanks Exercise",
|
||||
cloze_instruction: "Fill in the blanks and click 'Check'.",
|
||||
cloze_check: "Check",
|
||||
cloze_show_answers: "Show Answers",
|
||||
cloze_no_texts: "No cloze texts generated yet for this worksheet.",
|
||||
cloze_sentences: "sentences",
|
||||
cloze_gaps: "gaps",
|
||||
cloze_gaps_total: "Total gaps",
|
||||
cloze_with_gaps: "(with gaps)",
|
||||
cloze_print: "Print",
|
||||
cloze_print_with_answers: "Print with answers?",
|
||||
qa_title: "Question & Answer Sheet",
|
||||
qa_desc: "Q&A pairs with Leitner box system. Spaced repetition by difficulty level.",
|
||||
qa_generate: "Generate Q&A",
|
||||
qa_learn: "Start Learning",
|
||||
qa_print: "Print",
|
||||
qa_no_questions: "No Q&A generated yet for this worksheet.",
|
||||
qa_box_new: "New",
|
||||
qa_box_learning: "Learning",
|
||||
qa_box_mastered: "Mastered",
|
||||
qa_show_answer: "Show Answer",
|
||||
qa_your_answer: "Your Answer",
|
||||
qa_type_answer: "Write your answer here...",
|
||||
qa_check_answer: "Check Answer",
|
||||
qa_correct_answer: "Correct Answer",
|
||||
qa_self_evaluate: "Was your answer correct?",
|
||||
qa_no_answer: "(no answer entered)",
|
||||
qa_correct: "Correct",
|
||||
qa_incorrect: "Incorrect",
|
||||
qa_key_terms: "Key Terms",
|
||||
qa_session_correct: "Correct",
|
||||
qa_session_incorrect: "Incorrect",
|
||||
qa_session_complete: "Learning session complete!",
|
||||
qa_result_correct: "correct",
|
||||
qa_restart: "Learn Again",
|
||||
qa_print_with_answers: "Print with answers?",
|
||||
question: "Question",
|
||||
answer: "Answer",
|
||||
status_generating_qa: "Generating Q&A…",
|
||||
status_qa_generated: "Q&A generated",
|
||||
close: "Close",
|
||||
subject: "Subject",
|
||||
grade: "Level",
|
||||
questions: "questions",
|
||||
worksheet: "Worksheet",
|
||||
loading: "Loading...",
|
||||
error: "Error",
|
||||
success: "Success",
|
||||
imprint: "Imprint",
|
||||
privacy: "Privacy",
|
||||
contact: "Contact",
|
||||
status_ready: "Ready",
|
||||
status_processing: "Processing...",
|
||||
status_generating_mc: "Generating MC questions…",
|
||||
status_generating_cloze: "Generating cloze texts…",
|
||||
status_please_wait: "Please wait, AI is working.",
|
||||
status_mc_generated: "MC questions generated",
|
||||
status_cloze_generated: "Cloze texts generated",
|
||||
status_files_created: "files created",
|
||||
mindmap_title: "Mindmap Learning Poster",
|
||||
mindmap_desc: "Creates a child-friendly mindmap with the main topic in the center and all terms in colorful categories.",
|
||||
mindmap_generate: "Create Mindmap",
|
||||
mindmap_show: "View",
|
||||
mindmap_print_a3: "Print A3",
|
||||
generating_mindmap: "Creating mindmap...",
|
||||
mindmap_generated: "Mindmap created!",
|
||||
no_analysis: "No analysis",
|
||||
analyze_first: "Please analyze first (click Process)",
|
||||
categories: "Categories",
|
||||
terms: "Terms",
|
||||
}
|
||||
};
|
||||
|
||||
// RTL-Sprachen (Right-to-Left)
|
||||
export const rtlLanguages = ['ar'];
|
||||
|
||||
// Standard-Sprache
|
||||
export const defaultLanguage = 'de';
|
||||
|
||||
// Verfügbare Sprachen mit Labels
|
||||
export const availableLanguages = {
|
||||
de: 'Deutsch',
|
||||
en: 'English',
|
||||
tr: 'Türkçe',
|
||||
ar: 'العربية',
|
||||
ru: 'Русский',
|
||||
uk: 'Українська',
|
||||
pl: 'Polski'
|
||||
};
|
||||
9788
backend/frontend/static/js/studio.js
Normal file
9788
backend/frontend/static/js/studio.js
Normal file
File diff suppressed because it is too large
Load Diff
124
backend/frontend/static/manifest.json
Normal file
124
backend/frontend/static/manifest.json
Normal file
@@ -0,0 +1,124 @@
|
||||
{
|
||||
"name": "BreakPilot - Digitaler Lehrerassistent",
|
||||
"short_name": "BreakPilot",
|
||||
"description": "DSGVO-konforme KI-Unterstuetzung fuer Lehrkraefte",
|
||||
"start_url": "/app",
|
||||
"display": "standalone",
|
||||
"background_color": "#0a0a0a",
|
||||
"theme_color": "#6366f1",
|
||||
"orientation": "any",
|
||||
"scope": "/",
|
||||
"lang": "de",
|
||||
"categories": ["education", "productivity"],
|
||||
"icons": [
|
||||
{
|
||||
"src": "/static/icons/icon-72.png",
|
||||
"sizes": "72x72",
|
||||
"type": "image/png",
|
||||
"purpose": "any maskable"
|
||||
},
|
||||
{
|
||||
"src": "/static/icons/icon-96.png",
|
||||
"sizes": "96x96",
|
||||
"type": "image/png",
|
||||
"purpose": "any maskable"
|
||||
},
|
||||
{
|
||||
"src": "/static/icons/icon-128.png",
|
||||
"sizes": "128x128",
|
||||
"type": "image/png",
|
||||
"purpose": "any maskable"
|
||||
},
|
||||
{
|
||||
"src": "/static/icons/icon-144.png",
|
||||
"sizes": "144x144",
|
||||
"type": "image/png",
|
||||
"purpose": "any maskable"
|
||||
},
|
||||
{
|
||||
"src": "/static/icons/icon-152.png",
|
||||
"sizes": "152x152",
|
||||
"type": "image/png",
|
||||
"purpose": "any maskable"
|
||||
},
|
||||
{
|
||||
"src": "/static/icons/icon-192.png",
|
||||
"sizes": "192x192",
|
||||
"type": "image/png",
|
||||
"purpose": "any maskable"
|
||||
},
|
||||
{
|
||||
"src": "/static/icons/icon-384.png",
|
||||
"sizes": "384x384",
|
||||
"type": "image/png",
|
||||
"purpose": "any maskable"
|
||||
},
|
||||
{
|
||||
"src": "/static/icons/icon-512.png",
|
||||
"sizes": "512x512",
|
||||
"type": "image/png",
|
||||
"purpose": "any maskable"
|
||||
}
|
||||
],
|
||||
"screenshots": [
|
||||
{
|
||||
"src": "/static/screenshots/dashboard.png",
|
||||
"sizes": "1280x720",
|
||||
"type": "image/png",
|
||||
"form_factor": "wide",
|
||||
"label": "Dashboard mit Moduluebersicht"
|
||||
},
|
||||
{
|
||||
"src": "/static/screenshots/klausur.png",
|
||||
"sizes": "1280x720",
|
||||
"type": "image/png",
|
||||
"form_factor": "wide",
|
||||
"label": "KI-gestuetzte Klausurkorrektur"
|
||||
}
|
||||
],
|
||||
"shortcuts": [
|
||||
{
|
||||
"name": "Neue Korrektur",
|
||||
"short_name": "Korrektur",
|
||||
"description": "Starte eine neue Klausurkorrektur",
|
||||
"url": "/app?module=klausur-korrektur",
|
||||
"icons": [{ "src": "/static/icons/shortcut-correction.png", "sizes": "96x96" }]
|
||||
},
|
||||
{
|
||||
"name": "Unterricht starten",
|
||||
"short_name": "Unterricht",
|
||||
"description": "Starte den Unterrichtsbegleiter",
|
||||
"url": "/app?module=companion",
|
||||
"icons": [{ "src": "/static/icons/shortcut-lesson.png", "sizes": "96x96" }]
|
||||
}
|
||||
],
|
||||
"share_target": {
|
||||
"action": "/app/share",
|
||||
"method": "POST",
|
||||
"enctype": "multipart/form-data",
|
||||
"params": {
|
||||
"title": "title",
|
||||
"text": "text",
|
||||
"url": "url",
|
||||
"files": [
|
||||
{
|
||||
"name": "files",
|
||||
"accept": ["image/*", "application/pdf"]
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"file_handlers": [
|
||||
{
|
||||
"action": "/app/open",
|
||||
"accept": {
|
||||
"application/pdf": [".pdf"],
|
||||
"image/*": [".png", ".jpg", ".jpeg"]
|
||||
}
|
||||
}
|
||||
],
|
||||
"handle_links": "preferred",
|
||||
"launch_handler": {
|
||||
"client_mode": "navigate-existing"
|
||||
}
|
||||
}
|
||||
343
backend/frontend/static/service-worker.js
Normal file
343
backend/frontend/static/service-worker.js
Normal file
@@ -0,0 +1,343 @@
|
||||
/**
|
||||
* BreakPilot PWA Service Worker
|
||||
*
|
||||
* Funktionen:
|
||||
* 1. Cacht statische Assets fuer Offline-Nutzung
|
||||
* 2. Cacht LLM-Modell fuer lokale Header-Extraktion
|
||||
* 3. Background Sync fuer Korrektur-Uploads
|
||||
*/
|
||||
|
||||
const APP_CACHE = 'breakpilot-app-v1';
|
||||
const LLM_CACHE = 'breakpilot-llm-v1';
|
||||
const DATA_CACHE = 'breakpilot-data-v1';
|
||||
|
||||
// Statische Assets zum Cachen beim Install
|
||||
const STATIC_ASSETS = [
|
||||
'/',
|
||||
'/app',
|
||||
'/static/css/main.css',
|
||||
'/static/js/main.js'
|
||||
];
|
||||
|
||||
// LLM/OCR Assets (groessere Dateien, separater Cache)
|
||||
const LLM_ASSETS = [
|
||||
// Tesseract.js (OCR)
|
||||
'https://cdn.jsdelivr.net/npm/tesseract.js@5/dist/tesseract.min.js',
|
||||
'https://cdn.jsdelivr.net/npm/tesseract.js-core@5.0.0/tesseract-core.wasm.js',
|
||||
|
||||
// Tesseract Deutsch Sprachpaket
|
||||
'https://tessdata.projectnaptha.com/4.0.0/deu.traineddata.gz',
|
||||
|
||||
// Transformers.js (fuer zukuenftige Vision-Modelle)
|
||||
'https://cdn.jsdelivr.net/npm/@xenova/transformers@2.17.1/dist/transformers.min.js'
|
||||
];
|
||||
|
||||
// ============================================================
|
||||
// INSTALL EVENT
|
||||
// ============================================================
|
||||
|
||||
self.addEventListener('install', (event) => {
|
||||
console.log('[ServiceWorker] Installing...');
|
||||
|
||||
event.waitUntil(
|
||||
Promise.all([
|
||||
// App Cache
|
||||
caches.open(APP_CACHE).then((cache) => {
|
||||
console.log('[ServiceWorker] Caching app shell');
|
||||
return cache.addAll(STATIC_ASSETS);
|
||||
}),
|
||||
|
||||
// LLM Cache (optional, bei Fehler ignorieren)
|
||||
caches.open(LLM_CACHE).then((cache) => {
|
||||
console.log('[ServiceWorker] Pre-caching LLM assets');
|
||||
return Promise.allSettled(
|
||||
LLM_ASSETS.map(url =>
|
||||
cache.add(url).catch(err => {
|
||||
console.warn('[ServiceWorker] Failed to cache:', url, err);
|
||||
})
|
||||
)
|
||||
);
|
||||
})
|
||||
]).then(() => {
|
||||
console.log('[ServiceWorker] Install complete');
|
||||
return self.skipWaiting();
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
// ============================================================
|
||||
// ACTIVATE EVENT
|
||||
// ============================================================
|
||||
|
||||
self.addEventListener('activate', (event) => {
|
||||
console.log('[ServiceWorker] Activating...');
|
||||
|
||||
event.waitUntil(
|
||||
// Alte Caches aufräumen
|
||||
caches.keys().then((cacheNames) => {
|
||||
return Promise.all(
|
||||
cacheNames
|
||||
.filter(name => {
|
||||
// Nur alte Versionen loeschen
|
||||
return name.startsWith('breakpilot-') &&
|
||||
![APP_CACHE, LLM_CACHE, DATA_CACHE].includes(name);
|
||||
})
|
||||
.map(name => {
|
||||
console.log('[ServiceWorker] Deleting old cache:', name);
|
||||
return caches.delete(name);
|
||||
})
|
||||
);
|
||||
}).then(() => {
|
||||
console.log('[ServiceWorker] Claiming clients');
|
||||
return self.clients.claim();
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
// ============================================================
|
||||
// FETCH EVENT
|
||||
// ============================================================
|
||||
|
||||
self.addEventListener('fetch', (event) => {
|
||||
const url = new URL(event.request.url);
|
||||
|
||||
// API-Requests: Network-First (immer frische Daten)
|
||||
if (url.pathname.startsWith('/api/')) {
|
||||
event.respondWith(networkFirstStrategy(event.request));
|
||||
return;
|
||||
}
|
||||
|
||||
// LLM/OCR Assets: Cache-First (grosse Dateien)
|
||||
if (isLLMAsset(url)) {
|
||||
event.respondWith(cacheFirstStrategy(event.request, LLM_CACHE));
|
||||
return;
|
||||
}
|
||||
|
||||
// Statische Assets: Cache-First mit Network-Fallback
|
||||
if (isStaticAsset(url)) {
|
||||
event.respondWith(cacheFirstStrategy(event.request, APP_CACHE));
|
||||
return;
|
||||
}
|
||||
|
||||
// Alles andere: Network-First
|
||||
event.respondWith(networkFirstStrategy(event.request));
|
||||
});
|
||||
|
||||
// ============================================================
|
||||
// CACHING STRATEGIES
|
||||
// ============================================================
|
||||
|
||||
/**
|
||||
* Cache-First Strategy: Versucht Cache, dann Network.
|
||||
* Gut fuer statische Assets und LLM-Modelle.
|
||||
*/
|
||||
async function cacheFirstStrategy(request, cacheName) {
|
||||
const cache = await caches.open(cacheName);
|
||||
const cachedResponse = await cache.match(request);
|
||||
|
||||
if (cachedResponse) {
|
||||
// Im Hintergrund aktualisieren (Stale-While-Revalidate)
|
||||
fetchAndCache(request, cache);
|
||||
return cachedResponse;
|
||||
}
|
||||
|
||||
// Nicht im Cache: Von Network holen und cachen
|
||||
try {
|
||||
const networkResponse = await fetch(request);
|
||||
if (networkResponse.ok) {
|
||||
cache.put(request, networkResponse.clone());
|
||||
}
|
||||
return networkResponse;
|
||||
} catch (error) {
|
||||
console.error('[ServiceWorker] Fetch failed:', error);
|
||||
return new Response('Offline - Bitte Internetverbindung pruefen', {
|
||||
status: 503,
|
||||
statusText: 'Service Unavailable'
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Network-First Strategy: Versucht Network, dann Cache.
|
||||
* Gut fuer API-Calls und dynamische Inhalte.
|
||||
*/
|
||||
async function networkFirstStrategy(request) {
|
||||
const cache = await caches.open(DATA_CACHE);
|
||||
|
||||
try {
|
||||
const networkResponse = await fetch(request);
|
||||
|
||||
// Erfolgreiche GET-Requests cachen
|
||||
if (networkResponse.ok && request.method === 'GET') {
|
||||
cache.put(request, networkResponse.clone());
|
||||
}
|
||||
|
||||
return networkResponse;
|
||||
|
||||
} catch (error) {
|
||||
// Network failed: Versuche Cache
|
||||
const cachedResponse = await cache.match(request);
|
||||
if (cachedResponse) {
|
||||
return cachedResponse;
|
||||
}
|
||||
|
||||
// Kein Cache: Offline-Fehler
|
||||
return new Response(JSON.stringify({
|
||||
error: 'offline',
|
||||
message: 'Keine Internetverbindung'
|
||||
}), {
|
||||
status: 503,
|
||||
headers: { 'Content-Type': 'application/json' }
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Holt Resource von Network und cached sie (im Hintergrund).
|
||||
*/
|
||||
async function fetchAndCache(request, cache) {
|
||||
try {
|
||||
const response = await fetch(request);
|
||||
if (response.ok) {
|
||||
cache.put(request, response.clone());
|
||||
}
|
||||
} catch (error) {
|
||||
// Ignorieren - Cache ist noch gueltig
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// HELPERS
|
||||
// ============================================================
|
||||
|
||||
function isStaticAsset(url) {
|
||||
return url.pathname.startsWith('/static/') ||
|
||||
url.pathname.endsWith('.css') ||
|
||||
url.pathname.endsWith('.js') ||
|
||||
url.pathname.endsWith('.png') ||
|
||||
url.pathname.endsWith('.jpg') ||
|
||||
url.pathname.endsWith('.svg') ||
|
||||
url.pathname.endsWith('.woff2');
|
||||
}
|
||||
|
||||
function isLLMAsset(url) {
|
||||
const llmDomains = [
|
||||
'cdn.jsdelivr.net',
|
||||
'huggingface.co',
|
||||
'tessdata.projectnaptha.com'
|
||||
];
|
||||
|
||||
const llmExtensions = [
|
||||
'.onnx',
|
||||
'.wasm',
|
||||
'.traineddata',
|
||||
'.traineddata.gz',
|
||||
'.bin'
|
||||
];
|
||||
|
||||
return llmDomains.some(d => url.hostname.includes(d)) ||
|
||||
llmExtensions.some(e => url.pathname.endsWith(e));
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// BACKGROUND SYNC (fuer Offline-Uploads)
|
||||
// ============================================================
|
||||
|
||||
self.addEventListener('sync', (event) => {
|
||||
console.log('[ServiceWorker] Sync event:', event.tag);
|
||||
|
||||
if (event.tag === 'upload-exams') {
|
||||
event.waitUntil(uploadPendingExams());
|
||||
}
|
||||
});
|
||||
|
||||
async function uploadPendingExams() {
|
||||
// Hole ausstehende Uploads aus IndexedDB
|
||||
const db = await openDB('breakpilot-pending', 1);
|
||||
const pendingUploads = await db.getAll('uploads');
|
||||
|
||||
for (const upload of pendingUploads) {
|
||||
try {
|
||||
const response = await fetch(upload.url, {
|
||||
method: 'POST',
|
||||
body: upload.formData
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
await db.delete('uploads', upload.id);
|
||||
|
||||
// Benachrichtigung an Client
|
||||
self.clients.matchAll().then(clients => {
|
||||
clients.forEach(client => {
|
||||
client.postMessage({
|
||||
type: 'upload-complete',
|
||||
id: upload.id
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('[ServiceWorker] Upload failed:', error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Simple IndexedDB wrapper
|
||||
function openDB(name, version) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const request = indexedDB.open(name, version);
|
||||
request.onerror = () => reject(request.error);
|
||||
request.onsuccess = () => resolve(request.result);
|
||||
request.onupgradeneeded = (event) => {
|
||||
const db = event.target.result;
|
||||
if (!db.objectStoreNames.contains('uploads')) {
|
||||
db.createObjectStore('uploads', { keyPath: 'id', autoIncrement: true });
|
||||
}
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// PUSH NOTIFICATIONS (fuer Korrektur-Fortschritt)
|
||||
// ============================================================
|
||||
|
||||
self.addEventListener('push', (event) => {
|
||||
const data = event.data?.json() || {};
|
||||
|
||||
const options = {
|
||||
body: data.body || 'Neue Benachrichtigung',
|
||||
icon: '/static/icons/icon-192.png',
|
||||
badge: '/static/icons/badge-72.png',
|
||||
tag: data.tag || 'default',
|
||||
data: data.url || '/',
|
||||
actions: data.actions || []
|
||||
};
|
||||
|
||||
event.waitUntil(
|
||||
self.registration.showNotification(
|
||||
data.title || 'BreakPilot',
|
||||
options
|
||||
)
|
||||
);
|
||||
});
|
||||
|
||||
self.addEventListener('notificationclick', (event) => {
|
||||
event.notification.close();
|
||||
|
||||
event.waitUntil(
|
||||
clients.matchAll({ type: 'window' }).then(clientList => {
|
||||
// Existierendes Fenster fokussieren
|
||||
for (const client of clientList) {
|
||||
if (client.url.includes('/app') && 'focus' in client) {
|
||||
return client.focus();
|
||||
}
|
||||
}
|
||||
// Neues Fenster oeffnen
|
||||
if (clients.openWindow) {
|
||||
return clients.openWindow(event.notification.data || '/app');
|
||||
}
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
console.log('[ServiceWorker] Loaded');
|
||||
Reference in New Issue
Block a user