Update.
This commit is contained in:
@@ -4,6 +4,8 @@ class AdminBilling extends HTMLElement {
|
||||
this.pricingConfig = [];
|
||||
this.stats = null;
|
||||
this.boundHandleClick = this.handleClick.bind(this);
|
||||
this.loading = true;
|
||||
this.error = null;
|
||||
}
|
||||
|
||||
async connectedCallback() {
|
||||
@@ -18,6 +20,8 @@ class AdminBilling extends HTMLElement {
|
||||
}
|
||||
|
||||
async loadData() {
|
||||
this.loading = true;
|
||||
this.error = null;
|
||||
try {
|
||||
const [pricing, stats] = await Promise.all([
|
||||
this.fetchPricing(),
|
||||
@@ -26,8 +30,11 @@ class AdminBilling extends HTMLElement {
|
||||
|
||||
this.pricingConfig = pricing;
|
||||
this.stats = stats;
|
||||
this.loading = false;
|
||||
} catch (error) {
|
||||
console.error('Failed to load admin billing data:', error);
|
||||
this.error = error.message || 'Failed to load admin billing data';
|
||||
this.loading = false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -53,6 +60,16 @@ class AdminBilling extends HTMLElement {
|
||||
}
|
||||
|
||||
render() {
|
||||
if (this.loading) {
|
||||
this.innerHTML = '<div class="admin-billing"><div class="loading">Loading admin billing data...</div></div>';
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.error) {
|
||||
this.innerHTML = `<div class="admin-billing"><div class="error-message">Error: ${this.error}</div></div>`;
|
||||
return;
|
||||
}
|
||||
|
||||
this.innerHTML = `
|
||||
<div class="admin-billing">
|
||||
<h2>Billing Administration</h2>
|
||||
|
||||
@@ -0,0 +1,78 @@
|
||||
export default class BaseComponent extends HTMLElement {
|
||||
constructor() {
|
||||
super();
|
||||
this.attachShadow({ mode: 'open' });
|
||||
this._state = {};
|
||||
this._unsubscribers = [];
|
||||
}
|
||||
|
||||
connectedCallback() {
|
||||
this.render();
|
||||
this._setupListeners();
|
||||
this._subscribe();
|
||||
}
|
||||
|
||||
disconnectedCallback() {
|
||||
this._unsubscribers.forEach(unsubscribe => unsubscribe?.());
|
||||
this._cleanup();
|
||||
}
|
||||
|
||||
attributeChangedCallback(name, oldValue, newValue) {
|
||||
if (oldValue !== newValue) {
|
||||
this.render();
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
if (!this.shadowRoot) return;
|
||||
|
||||
const style = this._getStyles();
|
||||
const template = this._getTemplate();
|
||||
|
||||
this.shadowRoot.innerHTML = `${style}${template}`;
|
||||
}
|
||||
|
||||
_getStyles() {
|
||||
return '<style>:host { display: block; }</style>';
|
||||
}
|
||||
|
||||
_getTemplate() {
|
||||
return '';
|
||||
}
|
||||
|
||||
_setupListeners() {
|
||||
}
|
||||
|
||||
_subscribe() {
|
||||
}
|
||||
|
||||
_cleanup() {
|
||||
}
|
||||
|
||||
emit(eventName, detail = {}) {
|
||||
this.dispatchEvent(
|
||||
new CustomEvent(eventName, {
|
||||
detail,
|
||||
bubbles: true,
|
||||
composed: true
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
setState(updates) {
|
||||
this._state = { ...this._state, ...updates };
|
||||
this.render();
|
||||
}
|
||||
|
||||
getState() {
|
||||
return { ...this._state };
|
||||
}
|
||||
|
||||
querySelector(selector) {
|
||||
return this.shadowRoot?.querySelector(selector);
|
||||
}
|
||||
|
||||
querySelectorAll(selector) {
|
||||
return this.shadowRoot?.querySelectorAll(selector) || [];
|
||||
}
|
||||
}
|
||||
@@ -6,13 +6,18 @@ class BillingDashboard extends HTMLElement {
|
||||
this.pricing = null;
|
||||
this.invoices = [];
|
||||
this.boundHandleClick = this.handleClick.bind(this);
|
||||
this.loading = true;
|
||||
this.error = null;
|
||||
this.stripe = null;
|
||||
}
|
||||
|
||||
async connectedCallback() {
|
||||
this.addEventListener('click', this.boundHandleClick);
|
||||
this.render();
|
||||
await this.loadData();
|
||||
this.render();
|
||||
this.attachEventListeners();
|
||||
await this.initStripe();
|
||||
}
|
||||
|
||||
disconnectedCallback() {
|
||||
@@ -20,6 +25,8 @@ class BillingDashboard extends HTMLElement {
|
||||
}
|
||||
|
||||
async loadData() {
|
||||
this.loading = true;
|
||||
this.error = null;
|
||||
try {
|
||||
const [usage, subscription, pricing, invoices] = await Promise.all([
|
||||
this.fetchCurrentUsage(),
|
||||
@@ -32,8 +39,21 @@ class BillingDashboard extends HTMLElement {
|
||||
this.subscription = subscription;
|
||||
this.pricing = pricing;
|
||||
this.invoices = invoices;
|
||||
this.loading = false;
|
||||
} catch (error) {
|
||||
console.error('Failed to load billing data:', error);
|
||||
this.error = error.message || 'Failed to load billing data';
|
||||
this.loading = false;
|
||||
}
|
||||
}
|
||||
|
||||
async initStripe() {
|
||||
if (window.Stripe) {
|
||||
const response = await fetch('/api/billing/stripe-key');
|
||||
if (response.ok) {
|
||||
const data = await response.json();
|
||||
this.stripe = window.Stripe(data.publishable_key);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -109,6 +129,16 @@ class BillingDashboard extends HTMLElement {
|
||||
}
|
||||
|
||||
render() {
|
||||
if (this.loading) {
|
||||
this.innerHTML = '<div class="billing-dashboard"><div class="loading">Loading billing data...</div></div>';
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.error) {
|
||||
this.innerHTML = `<div class="billing-dashboard"><div class="error-message">Error: ${this.error}</div></div>`;
|
||||
return;
|
||||
}
|
||||
|
||||
const estimatedCost = this.calculateEstimatedCost();
|
||||
const storageUsed = this.currentUsage?.storage_gb || 0;
|
||||
const freeStorage = parseFloat(this.pricing?.free_tier_storage_gb?.value || 15);
|
||||
@@ -253,7 +283,74 @@ class BillingDashboard extends HTMLElement {
|
||||
}
|
||||
|
||||
async showPaymentMethodModal() {
|
||||
alert('Payment method modal will be implemented with Stripe Elements');
|
||||
if (!this.stripe) {
|
||||
alert('Payment processing not available');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/billing/payment-methods/setup-intent', {
|
||||
method: 'POST',
|
||||
headers: {'Authorization': `Bearer ${localStorage.getItem('token')}`}
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const error = await response.json();
|
||||
alert(`Failed to initialize payment: ${error.detail}`);
|
||||
return;
|
||||
}
|
||||
|
||||
const { client_secret } = await response.json();
|
||||
|
||||
const modal = document.createElement('div');
|
||||
modal.className = 'modal';
|
||||
modal.innerHTML = `
|
||||
<div class="modal-content">
|
||||
<h2>Add Payment Method</h2>
|
||||
<div id="payment-element"></div>
|
||||
<div class="modal-actions">
|
||||
<button class="btn-primary" id="submitPayment">Add Card</button>
|
||||
<button class="btn-secondary" id="cancelPayment">Cancel</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
document.body.appendChild(modal);
|
||||
|
||||
const elements = this.stripe.elements({ clientSecret: client_secret });
|
||||
const paymentElement = elements.create('payment');
|
||||
paymentElement.mount('#payment-element');
|
||||
|
||||
modal.querySelector('#submitPayment').addEventListener('click', async () => {
|
||||
const submitButton = modal.querySelector('#submitPayment');
|
||||
submitButton.disabled = true;
|
||||
submitButton.textContent = 'Processing...';
|
||||
|
||||
const { error } = await this.stripe.confirmSetup({
|
||||
elements,
|
||||
confirmParams: {
|
||||
return_url: window.location.href,
|
||||
},
|
||||
redirect: 'if_required'
|
||||
});
|
||||
|
||||
if (error) {
|
||||
alert(`Payment failed: ${error.message}`);
|
||||
submitButton.disabled = false;
|
||||
submitButton.textContent = 'Add Card';
|
||||
} else {
|
||||
alert('Payment method added successfully');
|
||||
modal.remove();
|
||||
await this.loadData();
|
||||
this.render();
|
||||
}
|
||||
});
|
||||
|
||||
modal.querySelector('#cancelPayment').addEventListener('click', () => {
|
||||
modal.remove();
|
||||
});
|
||||
} catch (error) {
|
||||
alert(`Error: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
async showInvoiceDetail(invoiceId) {
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
import { api } from '../api.js';
|
||||
import app from '../app.js';
|
||||
|
||||
const api = app.getAPI();
|
||||
const logger = app.getLogger();
|
||||
|
||||
class CodeEditorView extends HTMLElement {
|
||||
constructor() {
|
||||
@@ -6,146 +9,205 @@ class CodeEditorView extends HTMLElement {
|
||||
this.editor = null;
|
||||
this.file = null;
|
||||
this.previousView = null;
|
||||
this.boundHandleClick = this.handleClick.bind(this);
|
||||
this.boundHandleEscape = this.handleEscape.bind(this);
|
||||
this.isRendered = false;
|
||||
}
|
||||
|
||||
connectedCallback() {
|
||||
this.addEventListener('click', this.boundHandleClick);
|
||||
document.addEventListener('keydown', this.boundHandleEscape);
|
||||
logger.debug('CodeEditorView connected');
|
||||
}
|
||||
|
||||
disconnectedCallback() {
|
||||
this.removeEventListener('click', this.boundHandleClick);
|
||||
document.removeEventListener('keydown', this.boundHandleEscape);
|
||||
if (this.editor) {
|
||||
this.editor.toTextArea();
|
||||
this.editor = null;
|
||||
}
|
||||
}
|
||||
|
||||
handleEscape(e) {
|
||||
if (e.key === 'Escape') {
|
||||
this.goBack();
|
||||
}
|
||||
logger.debug('CodeEditorView disconnected');
|
||||
this.destroyEditor();
|
||||
}
|
||||
|
||||
async setFile(file, previousView = 'files') {
|
||||
if (this.isRendered) {
|
||||
logger.warn('Editor already rendered, skipping');
|
||||
return;
|
||||
}
|
||||
|
||||
this.file = file;
|
||||
this.previousView = previousView;
|
||||
await this.loadAndRender();
|
||||
}
|
||||
this.isRendered = true;
|
||||
|
||||
async loadAndRender() {
|
||||
try {
|
||||
const blob = await api.downloadFile(this.file.id);
|
||||
logger.debug('Loading file', { fileName: file.name });
|
||||
const blob = await api.downloadFile(file.id);
|
||||
const content = await blob.text();
|
||||
this.render(content);
|
||||
this.initializeEditor(content);
|
||||
|
||||
this.createUI(content);
|
||||
this.createEditor(content);
|
||||
} catch (error) {
|
||||
console.error('Failed to load file:', error);
|
||||
logger.error('Failed to load file', error);
|
||||
document.dispatchEvent(new CustomEvent('show-toast', {
|
||||
detail: { message: 'Failed to load file: ' + error.message, type: 'error' }
|
||||
}));
|
||||
this.render('');
|
||||
window.history.back();
|
||||
}
|
||||
}
|
||||
|
||||
getMimeType(filename) {
|
||||
const extension = filename.split('.').pop().toLowerCase();
|
||||
const mimeMap = {
|
||||
'js': 'text/javascript',
|
||||
'json': 'application/json',
|
||||
'py': 'text/x-python',
|
||||
'md': 'text/x-markdown',
|
||||
'html': 'text/html',
|
||||
'xml': 'application/xml',
|
||||
'css': 'text/css',
|
||||
'txt': 'text/plain',
|
||||
'log': 'text/plain',
|
||||
'sh': 'text/x-sh',
|
||||
'yaml': 'text/x-yaml',
|
||||
'yml': 'text/x-yaml'
|
||||
};
|
||||
return mimeMap[extension] || 'text/plain';
|
||||
}
|
||||
|
||||
render(content) {
|
||||
createUI(content) {
|
||||
this.innerHTML = `
|
||||
<div class="code-editor-view">
|
||||
<div class="code-editor-header">
|
||||
<div class="header-left">
|
||||
<button class="button" id="back-btn">Back</button>
|
||||
<h2 class="editor-filename">${this.file.name}</h2>
|
||||
<div class="code-editor-overlay">
|
||||
<div class="code-editor-container">
|
||||
<div class="code-editor-header">
|
||||
<div class="header-left">
|
||||
<button class="button" id="back-btn">Back</button>
|
||||
<h2 class="editor-filename">${this.escapeHtml(this.file.name)}</h2>
|
||||
</div>
|
||||
<div class="header-right">
|
||||
<button class="button button-primary" id="save-btn">Save & Close</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="header-right">
|
||||
<button class="button button-primary" id="save-btn">Save</button>
|
||||
<div class="code-editor-body">
|
||||
<textarea id="editor-textarea"></textarea>
|
||||
</div>
|
||||
</div>
|
||||
<div class="code-editor-body">
|
||||
<textarea id="code-editor-textarea">${content}</textarea>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
const backBtn = this.querySelector('#back-btn');
|
||||
const saveBtn = this.querySelector('#save-btn');
|
||||
|
||||
backBtn.addEventListener('click', () => this.close());
|
||||
saveBtn.addEventListener('click', () => this.save());
|
||||
|
||||
document.addEventListener('keydown', this.handleKeydown.bind(this));
|
||||
}
|
||||
|
||||
initializeEditor(content) {
|
||||
const textarea = this.querySelector('#code-editor-textarea');
|
||||
if (!textarea) return;
|
||||
createEditor(content) {
|
||||
const textarea = this.querySelector('#editor-textarea');
|
||||
if (!textarea) {
|
||||
logger.error('Textarea not found');
|
||||
return;
|
||||
}
|
||||
|
||||
textarea.value = content;
|
||||
|
||||
const mode = this.getMode(this.file.name);
|
||||
logger.debug('Creating CodeMirror editor', { mode, fileSize: content.length });
|
||||
|
||||
this.editor = CodeMirror.fromTextArea(textarea, {
|
||||
value: content,
|
||||
mode: this.getMimeType(this.file.name),
|
||||
mode: mode,
|
||||
lineNumbers: true,
|
||||
theme: 'default',
|
||||
lineWrapping: true,
|
||||
indentUnit: 4,
|
||||
indentWithTabs: false,
|
||||
theme: 'default',
|
||||
readOnly: false,
|
||||
autofocus: true,
|
||||
extraKeys: {
|
||||
'Ctrl-S': () => this.save(),
|
||||
'Cmd-S': () => this.save()
|
||||
'Ctrl-S': () => { this.save(); return false; },
|
||||
'Cmd-S': () => { this.save(); return false; },
|
||||
'Esc': () => { this.close(); return false; }
|
||||
}
|
||||
});
|
||||
|
||||
this.editor.setSize('100%', '100%');
|
||||
|
||||
setTimeout(() => {
|
||||
if (this.editor) {
|
||||
this.editor.refresh();
|
||||
this.editor.focus();
|
||||
logger.debug('Editor ready and focused');
|
||||
}
|
||||
}, 100);
|
||||
}
|
||||
|
||||
handleClick(e) {
|
||||
if (e.target.id === 'back-btn') {
|
||||
this.goBack();
|
||||
} else if (e.target.id === 'save-btn') {
|
||||
this.save();
|
||||
getMode(filename) {
|
||||
const ext = filename.split('.').pop().toLowerCase();
|
||||
const modes = {
|
||||
'js': 'javascript',
|
||||
'json': { name: 'javascript', json: true },
|
||||
'py': 'python',
|
||||
'md': 'markdown',
|
||||
'html': 'htmlmixed',
|
||||
'xml': 'xml',
|
||||
'css': 'css',
|
||||
'txt': 'text/plain',
|
||||
'log': 'text/plain',
|
||||
'sh': 'shell',
|
||||
'yaml': 'yaml',
|
||||
'yml': 'yaml'
|
||||
};
|
||||
return modes[ext] || 'text/plain';
|
||||
}
|
||||
|
||||
escapeHtml(text) {
|
||||
const div = document.createElement('div');
|
||||
div.textContent = text;
|
||||
return div.innerHTML;
|
||||
}
|
||||
|
||||
handleKeydown(e) {
|
||||
if (e.key === 'Escape' && !this.editor.getOption('readOnly')) {
|
||||
this.close();
|
||||
}
|
||||
}
|
||||
|
||||
async save() {
|
||||
if (!this.editor) return;
|
||||
if (!this.editor) {
|
||||
logger.warn('No editor instance');
|
||||
return;
|
||||
}
|
||||
|
||||
const saveBtn = this.querySelector('#save-btn');
|
||||
if (!saveBtn) return;
|
||||
|
||||
try {
|
||||
saveBtn.disabled = true;
|
||||
saveBtn.textContent = 'Saving...';
|
||||
|
||||
const content = this.editor.getValue();
|
||||
logger.debug('Saving file', { fileName: this.file.name, size: content.length });
|
||||
|
||||
await api.updateFile(this.file.id, content);
|
||||
|
||||
logger.info('File saved successfully', { fileName: this.file.name });
|
||||
|
||||
document.dispatchEvent(new CustomEvent('show-toast', {
|
||||
detail: { message: 'File saved successfully!', type: 'success' }
|
||||
}));
|
||||
|
||||
setTimeout(() => {
|
||||
this.close();
|
||||
}, 500);
|
||||
|
||||
} catch (error) {
|
||||
logger.error('Failed to save file', error);
|
||||
document.dispatchEvent(new CustomEvent('show-toast', {
|
||||
detail: { message: 'Failed to save file: ' + error.message, type: 'error' }
|
||||
detail: { message: 'Failed to save: ' + error.message, type: 'error' }
|
||||
}));
|
||||
|
||||
if (saveBtn) {
|
||||
saveBtn.disabled = false;
|
||||
saveBtn.textContent = 'Save & Close';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
goBack() {
|
||||
close() {
|
||||
logger.debug('Closing editor');
|
||||
window.history.back();
|
||||
}
|
||||
|
||||
hide() {
|
||||
document.removeEventListener('keydown', this.boundHandleEscape);
|
||||
logger.debug('Hiding editor');
|
||||
this.destroyEditor();
|
||||
this.remove();
|
||||
}
|
||||
|
||||
destroyEditor() {
|
||||
if (this.editor) {
|
||||
this.editor.toTextArea();
|
||||
logger.debug('Destroying CodeMirror instance');
|
||||
try {
|
||||
this.editor.toTextArea();
|
||||
} catch (e) {
|
||||
logger.warn('Error destroying editor', e);
|
||||
}
|
||||
this.editor = null;
|
||||
}
|
||||
this.remove();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,101 @@
|
||||
import BaseComponent from './base-component.js';
|
||||
|
||||
export default class ErrorBoundary extends BaseComponent {
|
||||
constructor() {
|
||||
super();
|
||||
this.error = null;
|
||||
this.errorInfo = null;
|
||||
}
|
||||
|
||||
_getStyles() {
|
||||
return `<style>
|
||||
:host {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.error-container {
|
||||
padding: 2rem;
|
||||
background: #ffebee;
|
||||
border: 1px solid #f44336;
|
||||
border-radius: 8px;
|
||||
color: #c62828;
|
||||
}
|
||||
|
||||
h2 {
|
||||
margin: 0 0 1rem 0;
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
|
||||
p {
|
||||
margin: 0 0 0.5rem 0;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.error-details {
|
||||
background: rgba(0, 0, 0, 0.1);
|
||||
padding: 1rem;
|
||||
border-radius: 4px;
|
||||
font-family: monospace;
|
||||
font-size: 0.85rem;
|
||||
overflow-x: auto;
|
||||
max-height: 200px;
|
||||
overflow-y: auto;
|
||||
margin-top: 1rem;
|
||||
}
|
||||
|
||||
.error-details pre {
|
||||
margin: 0;
|
||||
white-space: pre-wrap;
|
||||
word-wrap: break-word;
|
||||
}
|
||||
|
||||
button {
|
||||
margin-top: 1rem;
|
||||
padding: 0.75rem 1.5rem;
|
||||
background: #f44336;
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
button:hover {
|
||||
background: #d32f2f;
|
||||
}
|
||||
</style>`;
|
||||
}
|
||||
|
||||
_getTemplate() {
|
||||
if (!this.error) {
|
||||
return '<slot></slot>';
|
||||
}
|
||||
|
||||
return `
|
||||
<div class="error-container" role="alert">
|
||||
<h2>Something went wrong</h2>
|
||||
<p>${this.error.message || 'An unexpected error occurred'}</p>
|
||||
<p>Please refresh the page or contact support if the problem persists.</p>
|
||||
<div class="error-details">
|
||||
<pre>${this.error.stack || 'No stack trace available'}</pre>
|
||||
</div>
|
||||
<button type="button" onclick="location.reload()">Reload Page</button>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
catch(error, errorInfo) {
|
||||
this.error = error;
|
||||
this.errorInfo = errorInfo;
|
||||
this.render();
|
||||
console.error('Error caught by boundary:', error, errorInfo);
|
||||
}
|
||||
|
||||
reset() {
|
||||
this.error = null;
|
||||
this.errorInfo = null;
|
||||
this.render();
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define('error-boundary', ErrorBoundary);
|
||||
@@ -41,14 +41,6 @@ class FilePreview extends HTMLElement {
|
||||
this.style.display = 'block';
|
||||
document.addEventListener('keydown', this.handleEscape);
|
||||
this.renderPreview();
|
||||
|
||||
if (pushState) {
|
||||
window.history.pushState(
|
||||
{ view: 'file-preview', file: file },
|
||||
'',
|
||||
`#preview/${file.id}`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
close() {
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
import { api } from '../api.js';
|
||||
import app from '../app.js';
|
||||
|
||||
const api = app.getAPI();
|
||||
const logger = app.getLogger();
|
||||
|
||||
export class LoginView extends HTMLElement {
|
||||
constructor() {
|
||||
@@ -74,9 +77,12 @@ export class LoginView extends HTMLElement {
|
||||
const errorDiv = this.querySelector('#login-error');
|
||||
|
||||
try {
|
||||
logger.info('Login attempt started', { username });
|
||||
await api.login(username, password);
|
||||
logger.info('Login successful, dispatching auth-success event');
|
||||
this.dispatchEvent(new CustomEvent('auth-success'));
|
||||
} catch (error) {
|
||||
logger.error('Login failed', { username, error: error.message });
|
||||
errorDiv.textContent = error.message;
|
||||
}
|
||||
}
|
||||
@@ -91,9 +97,12 @@ export class LoginView extends HTMLElement {
|
||||
const errorDiv = this.querySelector('#register-error');
|
||||
|
||||
try {
|
||||
logger.info('Registration attempt started', { username, email });
|
||||
await api.register(username, email, password);
|
||||
logger.info('Registration successful, dispatching auth-success event');
|
||||
this.dispatchEvent(new CustomEvent('auth-success'));
|
||||
} catch (error) {
|
||||
logger.error('Registration failed', { username, email, error: error.message });
|
||||
errorDiv.textContent = error.message;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { api } from '../api.js';
|
||||
import app from '../app.js';
|
||||
import './login-view.js';
|
||||
import './file-list.js';
|
||||
import './file-upload-view.js';
|
||||
@@ -16,22 +16,51 @@ import './admin-billing.js';
|
||||
import './code-editor-view.js';
|
||||
import { shortcuts } from '../shortcuts.js';
|
||||
|
||||
const api = app.getAPI();
|
||||
const logger = app.getLogger();
|
||||
const appState = app.getState();
|
||||
|
||||
export class RBoxApp extends HTMLElement {
|
||||
constructor() {
|
||||
super();
|
||||
this.currentView = 'files';
|
||||
this.user = null;
|
||||
this.navigationStack = [];
|
||||
this.boundHandlePopState = this.handlePopState.bind(this);
|
||||
this.popstateAttached = false;
|
||||
}
|
||||
|
||||
async connectedCallback() {
|
||||
await this.init();
|
||||
this.addEventListener('show-toast', this.handleShowToast);
|
||||
window.addEventListener('popstate', this.handlePopState.bind(this));
|
||||
try {
|
||||
await this.init();
|
||||
this.addEventListener('show-toast', this.handleShowToast);
|
||||
|
||||
if (!this.popstateAttached) {
|
||||
window.addEventListener('popstate', this.boundHandlePopState);
|
||||
this.popstateAttached = true;
|
||||
logger.debug('Popstate listener attached');
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error('Failed to initialize RBoxApp', error);
|
||||
this.innerHTML = `
|
||||
<div style="padding: 2rem; text-align: center; font-family: sans-serif;">
|
||||
<h1 style="color: #d32f2f;">Failed to Load Application</h1>
|
||||
<p>${error.message}</p>
|
||||
<button onclick="location.reload()" style="padding: 0.75rem 1.5rem; background: #2196F3; color: white; border: none; border-radius: 4px; cursor: pointer; font-size: 1rem;">
|
||||
Reload Page
|
||||
</button>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
disconnectedCallback() {
|
||||
this.removeEventListener('show-toast', this.handleShowToast);
|
||||
if (this.popstateAttached) {
|
||||
window.removeEventListener('popstate', this.boundHandlePopState);
|
||||
this.popstateAttached = false;
|
||||
logger.debug('Popstate listener removed');
|
||||
}
|
||||
}
|
||||
|
||||
handleShowToast = (event) => {
|
||||
@@ -46,15 +75,21 @@ export class RBoxApp extends HTMLElement {
|
||||
}
|
||||
|
||||
async init() {
|
||||
if (!api.getToken()) {
|
||||
this.showLogin();
|
||||
} else {
|
||||
try {
|
||||
this.user = await api.getCurrentUser();
|
||||
this.render();
|
||||
} catch (error) {
|
||||
try {
|
||||
if (!api.getToken()) {
|
||||
logger.info('No token found, showing login');
|
||||
this.showLogin();
|
||||
} else {
|
||||
logger.info('Initializing application with stored token');
|
||||
this.user = await api.getCurrentUser();
|
||||
appState.setState({ user: this.user });
|
||||
logger.info('User loaded successfully', { username: this.user.username });
|
||||
this.render();
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error('Failed to initialize application', error);
|
||||
api.setToken(null);
|
||||
this.showLogin();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -383,47 +418,63 @@ export class RBoxApp extends HTMLElement {
|
||||
}
|
||||
|
||||
handlePopState(e) {
|
||||
logger.debug('Popstate event', { state: e.state, url: window.location.href });
|
||||
|
||||
this.closeAllOverlays();
|
||||
|
||||
if (e.state && e.state.view) {
|
||||
if (e.state.view === 'code-editor' && e.state.file) {
|
||||
const view = e.state.view;
|
||||
|
||||
if (view === 'code-editor' && e.state.file) {
|
||||
logger.debug('Restoring code editor view');
|
||||
this.showCodeEditor(e.state.file, false);
|
||||
} else if (e.state.view === 'file-preview' && e.state.file) {
|
||||
} else if (view === 'file-preview' && e.state.file) {
|
||||
logger.debug('Restoring file preview view');
|
||||
this.showFilePreview(e.state.file, false);
|
||||
} else if (e.state.view === 'upload') {
|
||||
} else if (view === 'upload') {
|
||||
logger.debug('Restoring upload view');
|
||||
const folderId = e.state.folderId !== undefined ? e.state.folderId : null;
|
||||
this.showUpload(folderId, false);
|
||||
} else {
|
||||
this.switchView(e.state.view, false);
|
||||
logger.debug('Switching to view', { view });
|
||||
this.switchView(view, false);
|
||||
}
|
||||
} else {
|
||||
logger.debug('No state, defaulting to files view');
|
||||
this.switchView('files', false);
|
||||
}
|
||||
}
|
||||
|
||||
closeAllOverlays() {
|
||||
logger.debug('Closing all overlays');
|
||||
|
||||
const existingEditor = this.querySelector('code-editor-view');
|
||||
if (existingEditor) {
|
||||
logger.debug('Hiding code editor');
|
||||
existingEditor.hide();
|
||||
}
|
||||
|
||||
const existingPreview = this.querySelector('file-preview');
|
||||
if (existingPreview) {
|
||||
logger.debug('Hiding file preview');
|
||||
existingPreview.hide();
|
||||
}
|
||||
|
||||
const existingUpload = this.querySelector('file-upload-view');
|
||||
if (existingUpload) {
|
||||
logger.debug('Hiding file upload');
|
||||
existingUpload.hide();
|
||||
}
|
||||
|
||||
const shareModal = this.querySelector('share-modal');
|
||||
if (shareModal && shareModal.style.display !== 'none') {
|
||||
logger.debug('Hiding share modal');
|
||||
shareModal.style.display = 'none';
|
||||
}
|
||||
}
|
||||
|
||||
showCodeEditor(file, pushState = true) {
|
||||
logger.debug('Showing code editor', { file: file.name, pushState });
|
||||
this.closeAllOverlays();
|
||||
|
||||
const mainElement = this.querySelector('.app-main');
|
||||
@@ -432,15 +483,29 @@ export class RBoxApp extends HTMLElement {
|
||||
editorView.setFile(file, this.currentView);
|
||||
|
||||
if (pushState) {
|
||||
window.history.pushState(
|
||||
{ view: 'code-editor', file: file },
|
||||
'',
|
||||
`#editor/${file.id}`
|
||||
);
|
||||
const currentState = window.history.state || {};
|
||||
const currentView = currentState.view || this.currentView;
|
||||
|
||||
if (currentView !== 'code-editor') {
|
||||
window.history.pushState(
|
||||
{ view: 'code-editor', file: file, previousView: currentView },
|
||||
'',
|
||||
`#editor/${file.id}`
|
||||
);
|
||||
logger.debug('Pushed code editor state', { previousView: currentView });
|
||||
} else {
|
||||
logger.debug('Already in code editor view, replacing state');
|
||||
window.history.replaceState(
|
||||
{ view: 'code-editor', file: file, previousView: currentView },
|
||||
'',
|
||||
`#editor/${file.id}`
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
showFilePreview(file, pushState = true) {
|
||||
logger.debug('Showing file preview', { file: file.name, pushState });
|
||||
this.closeAllOverlays();
|
||||
|
||||
const mainElement = this.querySelector('.app-main');
|
||||
@@ -449,15 +514,29 @@ export class RBoxApp extends HTMLElement {
|
||||
preview.show(file, false);
|
||||
|
||||
if (pushState) {
|
||||
window.history.pushState(
|
||||
{ view: 'file-preview', file: file },
|
||||
'',
|
||||
`#preview/${file.id}`
|
||||
);
|
||||
const currentState = window.history.state || {};
|
||||
const currentView = currentState.view || this.currentView;
|
||||
|
||||
if (currentView !== 'file-preview') {
|
||||
window.history.pushState(
|
||||
{ view: 'file-preview', file: file, previousView: currentView },
|
||||
'',
|
||||
`#preview/${file.id}`
|
||||
);
|
||||
logger.debug('Pushed file preview state', { previousView: currentView });
|
||||
} else {
|
||||
logger.debug('Already in file preview view, replacing state');
|
||||
window.history.replaceState(
|
||||
{ view: 'file-preview', file: file, previousView: currentView },
|
||||
'',
|
||||
`#preview/${file.id}`
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
showUpload(folderId = null, pushState = true) {
|
||||
logger.debug('Showing upload view', { folderId, pushState });
|
||||
this.closeAllOverlays();
|
||||
|
||||
const mainElement = this.querySelector('.app-main');
|
||||
@@ -466,11 +545,24 @@ export class RBoxApp extends HTMLElement {
|
||||
uploadView.setFolder(folderId);
|
||||
|
||||
if (pushState) {
|
||||
window.history.pushState(
|
||||
{ view: 'upload', folderId: folderId },
|
||||
'',
|
||||
'#upload'
|
||||
);
|
||||
const currentState = window.history.state || {};
|
||||
const currentView = currentState.view || this.currentView;
|
||||
|
||||
if (currentView !== 'upload') {
|
||||
window.history.pushState(
|
||||
{ view: 'upload', folderId: folderId, previousView: currentView },
|
||||
'',
|
||||
'#upload'
|
||||
);
|
||||
logger.debug('Pushed upload state', { previousView: currentView });
|
||||
} else {
|
||||
logger.debug('Already in upload view, replacing state');
|
||||
window.history.replaceState(
|
||||
{ view: 'upload', folderId: folderId, previousView: currentView },
|
||||
'',
|
||||
'#upload'
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user