Update.
This commit is contained in:
@@ -998,3 +998,47 @@ body.dark-mode {
|
||||
padding-bottom: calc(var(--spacing-unit) * 2);
|
||||
margin-bottom: calc(var(--spacing-unit) * 2);
|
||||
}
|
||||
|
||||
/* Footer Styles */
|
||||
.app-footer {
|
||||
background-color: var(--accent-color);
|
||||
border-top: 1px solid var(--border-color);
|
||||
padding: calc(var(--spacing-unit) * 2) calc(var(--spacing-unit) * 3);
|
||||
text-align: center;
|
||||
font-size: 0.875rem;
|
||||
color: var(--text-color-light);
|
||||
flex-shrink: 0; /* Prevent footer from shrinking */
|
||||
}
|
||||
|
||||
.footer-nav {
|
||||
margin-bottom: var(--spacing-unit);
|
||||
}
|
||||
|
||||
.footer-links {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
justify-content: center;
|
||||
gap: calc(var(--spacing-unit) * 2);
|
||||
}
|
||||
|
||||
.footer-links li {
|
||||
display: inline;
|
||||
}
|
||||
|
||||
.footer-links a {
|
||||
color: var(--primary-color);
|
||||
text-decoration: none;
|
||||
transition: color 0.2s ease;
|
||||
}
|
||||
|
||||
.footer-links a:hover {
|
||||
color: var(--secondary-color);
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.footer-text {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,146 @@
|
||||
// static/js/components/cookie-consent.js
|
||||
import app from '../app.js';
|
||||
|
||||
const COOKIE_CONSENT_KEY = 'rbox_cookie_consent';
|
||||
|
||||
export class CookieConsent extends HTMLElement {
|
||||
constructor() {
|
||||
super();
|
||||
this.attachShadow({ mode: 'open' });
|
||||
this.hasConsented = this.checkConsent();
|
||||
}
|
||||
|
||||
connectedCallback() {
|
||||
if (!this.hasConsented) {
|
||||
this.render();
|
||||
}
|
||||
}
|
||||
|
||||
checkConsent() {
|
||||
const consent = localStorage.getItem(COOKIE_CONSENT_KEY);
|
||||
if (consent) {
|
||||
// In a real application, you'd parse this and apply preferences
|
||||
// For now, just checking if it exists
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
setConsent(status) {
|
||||
// In a real application, 'status' would be a detailed object
|
||||
// For now, a simple string 'accepted' or 'declined'
|
||||
localStorage.setItem(COOKIE_CONSENT_KEY, status);
|
||||
this.hasConsented = true;
|
||||
this.remove(); // Remove the banner after consent
|
||||
app.getLogger().info(`Cookie consent: ${status}`);
|
||||
// Trigger an event for other parts of the app to react to consent change
|
||||
document.dispatchEvent(new CustomEvent('cookie-consent-changed', { detail: { status } }));
|
||||
}
|
||||
|
||||
render() {
|
||||
this.shadowRoot.innerHTML = `
|
||||
<style>
|
||||
:host {
|
||||
display: block;
|
||||
position: fixed;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
background-color: var(--accent-color, #333);
|
||||
color: var(--text-color-light, #eee);
|
||||
padding: 15px 20px;
|
||||
box-shadow: 0 -2px 10px rgba(0, 0, 0, 0.2);
|
||||
z-index: 10000;
|
||||
font-family: var(--font-family, sans-serif);
|
||||
font-size: 0.9rem;
|
||||
line-height: 1.4;
|
||||
}
|
||||
.consent-container {
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 15px;
|
||||
}
|
||||
.consent-message {
|
||||
flex: 1;
|
||||
min-width: 250px;
|
||||
color: var(--text-color, #333);
|
||||
}
|
||||
.consent-message a {
|
||||
color: var(--primary-color, #007bff);
|
||||
text-decoration: underline;
|
||||
}
|
||||
.consent-buttons {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
.consent-button {
|
||||
background-color: var(--primary-color, #007bff);
|
||||
color: var(--accent-color, #fff);
|
||||
border: none;
|
||||
padding: 8px 15px;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
font-size: 0.9rem;
|
||||
transition: background-color 0.2s ease;
|
||||
}
|
||||
.consent-button:hover {
|
||||
background-color: var(--secondary-color, #0056b3);
|
||||
}
|
||||
.consent-button.decline {
|
||||
background-color: #6c757d;
|
||||
}
|
||||
.consent-button.decline:hover {
|
||||
background-color: #5a6268;
|
||||
}
|
||||
.consent-button.customize {
|
||||
background-color: transparent;
|
||||
border: 1px solid var(--primary-color, #007bff);
|
||||
color: var(--primary-color, #007bff);
|
||||
}
|
||||
.consent-button.customize:hover {
|
||||
background-color: var(--primary-color, #007bff);
|
||||
color: var(--accent-color, #fff);
|
||||
}
|
||||
@media (max-width: 768px) {
|
||||
.consent-container {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
}
|
||||
.consent-buttons {
|
||||
width: 100%;
|
||||
justify-content: stretch;
|
||||
}
|
||||
.consent-button {
|
||||
flex: 1;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
<div class="consent-container">
|
||||
<p class="consent-message">
|
||||
We use cookies to ensure you get the best experience on our website. For more details, please read our
|
||||
<a href="/static/legal/cookie_policy.md" target="_blank" rel="noopener noreferrer">Cookie Policy</a>.
|
||||
</p>
|
||||
<div class="consent-buttons">
|
||||
<button class="consent-button accept">Accept All</button>
|
||||
<button class="consent-button decline">Decline All</button>
|
||||
<button class="consent-button customize">Customize</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
this.shadowRoot.querySelector('.consent-button.accept').addEventListener('click', () => this.setConsent('accepted'));
|
||||
this.shadowRoot.querySelector('.consent-button.decline').addEventListener('click', () => this.setConsent('declined'));
|
||||
this.shadowRoot.querySelector('.consent-button.customize').addEventListener('click', () => {
|
||||
// For now, customize acts like accept. In a real app, this would open a modal.
|
||||
app.getLogger().info('Customize cookie consent clicked. (Placeholder: acting as accept)');
|
||||
this.setConsent('accepted');
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define('cookie-consent', CookieConsent);
|
||||
@@ -83,12 +83,12 @@ export class LoginView extends HTMLElement {
|
||||
errorDiv.textContent = '';
|
||||
|
||||
try {
|
||||
logger.info('Login attempt started', { username });
|
||||
logger.info('Login attempt started', { action: 'USER_LOGIN_ATTEMPT', username });
|
||||
await api.login(username, password);
|
||||
logger.info('Login successful, dispatching auth-success event');
|
||||
logger.info('Login successful', { action: 'USER_LOGIN_SUCCESS', username });
|
||||
this.dispatchEvent(new CustomEvent('auth-success'));
|
||||
} catch (error) {
|
||||
logger.error('Login failed', { username, error: error.message });
|
||||
logger.error('Login failed', { action: 'USER_LOGIN_FAILURE', username, error: error.message });
|
||||
errorDiv.textContent = error.message;
|
||||
errorDiv.style.display = 'block';
|
||||
}
|
||||
@@ -107,12 +107,12 @@ export class LoginView extends HTMLElement {
|
||||
errorDiv.textContent = '';
|
||||
|
||||
try {
|
||||
logger.info('Registration attempt started', { username, email });
|
||||
logger.info('Registration attempt started', { action: 'USER_REGISTER_ATTEMPT', username, email });
|
||||
await api.register(username, email, password);
|
||||
logger.info('Registration successful, dispatching auth-success event');
|
||||
logger.info('Registration successful', { action: 'USER_REGISTER_SUCCESS', username, email });
|
||||
this.dispatchEvent(new CustomEvent('auth-success'));
|
||||
} catch (error) {
|
||||
logger.error('Registration failed', { username, email, error: error.message });
|
||||
logger.error('Registration failed', { action: 'USER_REGISTER_FAILURE', username, email, error: error.message });
|
||||
errorDiv.textContent = error.message;
|
||||
errorDiv.style.display = 'block';
|
||||
}
|
||||
|
||||
@@ -14,6 +14,8 @@ import './shared-items.js';
|
||||
import './billing-dashboard.js';
|
||||
import './admin-billing.js';
|
||||
import './code-editor-view.js';
|
||||
import './cookie-consent.js';
|
||||
import './user-settings.js'; // Import the new user settings component
|
||||
import { shortcuts } from '../shortcuts.js';
|
||||
|
||||
const api = app.getAPI();
|
||||
@@ -126,6 +128,7 @@ export class RBoxApp extends HTMLElement {
|
||||
<li><a href="#" class="nav-link" data-view="shared">Shared Items</a></li>
|
||||
<li><a href="#" class="nav-link" data-view="deleted">Deleted Files</a></li>
|
||||
<li><a href="#" class="nav-link" data-view="billing">Billing</a></li>
|
||||
<li><a href="#" class="nav-link" data-view="user-settings">User Settings</a></li>
|
||||
${this.user && this.user.is_superuser ? `<li><a href="#" class="nav-link" data-view="admin">Admin Dashboard</a></li>` : ''}
|
||||
${this.user && this.user.is_superuser ? `<li><a href="#" class="nav-link" data-view="admin-billing">Admin Billing</a></li>` : ''}
|
||||
</ul>
|
||||
@@ -145,7 +148,24 @@ export class RBoxApp extends HTMLElement {
|
||||
</div>
|
||||
|
||||
<share-modal></share-modal>
|
||||
|
||||
<footer class="app-footer">
|
||||
<nav class="footer-nav">
|
||||
<ul class="footer-links">
|
||||
<li><a href="/static/legal/privacy_policy.md" target="_blank" rel="noopener noreferrer">Privacy Policy</a></li>
|
||||
<li><a href="/static/legal/data_processing_agreement.md" target="_blank" rel="noopener noreferrer">Data Processing Agreement</a></li>
|
||||
<li><a href="/static/legal/terms_of_service.md" target="_blank" rel="noopener noreferrer">Terms of Service</a></li>
|
||||
<li><a href="/static/legal/cookie_policy.md" target="_blank" rel="noopener noreferrer">Cookie Policy</a></li>
|
||||
<li><a href="/static/legal/security_policy.md" target="_blank" rel="noopener noreferrer">Security Policy</a></li>
|
||||
<li><a href="/static/legal/compliance_statement.md" target="_blank" rel="noopener noreferrer">Compliance Statement</a></li>
|
||||
<li><a href="/static/legal/data_portability_deletion_policy.md" target="_blank" rel="noopener noreferrer">Data Portability & Deletion</a></li>
|
||||
<li><a href="/static/legal/contact_complaint_mechanism.md" target="_blank" rel="noopener noreferrer">Contact & Complaints</a></li>
|
||||
</ul>
|
||||
</nav>
|
||||
<p class="footer-text">© ${new Date().getFullYear()} RBox Cloud Storage. All rights reserved.</p>
|
||||
</footer>
|
||||
</div>
|
||||
<cookie-consent></cookie-consent>
|
||||
`;
|
||||
|
||||
this.initializeNavigation();
|
||||
@@ -358,6 +378,7 @@ export class RBoxApp extends HTMLElement {
|
||||
|
||||
attachListeners() {
|
||||
this.querySelector('#logout-btn')?.addEventListener('click', () => {
|
||||
logger.info('User logout initiated', { action: 'USER_LOGOUT' });
|
||||
api.logout();
|
||||
});
|
||||
|
||||
@@ -639,6 +660,10 @@ export class RBoxApp extends HTMLElement {
|
||||
mainContent.innerHTML = '<billing-dashboard></billing-dashboard>';
|
||||
this.attachListeners();
|
||||
break;
|
||||
case 'user-settings':
|
||||
mainContent.innerHTML = '<user-settings></user-settings>';
|
||||
this.attachListeners();
|
||||
break;
|
||||
case 'admin-billing':
|
||||
mainContent.innerHTML = '<admin-billing></admin-billing>';
|
||||
this.attachListeners();
|
||||
|
||||
@@ -0,0 +1,117 @@
|
||||
// static/js/components/user-settings.js
|
||||
import app from '../app.js';
|
||||
|
||||
const api = app.getAPI();
|
||||
const logger = app.getLogger();
|
||||
|
||||
export class UserSettings extends HTMLElement {
|
||||
constructor() {
|
||||
super();
|
||||
this.boundHandleClick = this.handleClick.bind(this);
|
||||
}
|
||||
|
||||
connectedCallback() {
|
||||
this.render();
|
||||
this.addEventListener('click', this.boundHandleClick);
|
||||
}
|
||||
|
||||
disconnectedCallback() {
|
||||
this.removeEventListener('click', this.boundHandleClick);
|
||||
}
|
||||
|
||||
render() {
|
||||
this.innerHTML = `
|
||||
<div class="user-settings-container">
|
||||
<h2>User Settings</h2>
|
||||
|
||||
<div class="settings-section">
|
||||
<h3>Data Management</h3>
|
||||
<p>You can export a copy of your personal data or delete your account.</p>
|
||||
<button id="exportDataBtn" class="button button-primary">Export My Data</button>
|
||||
<button id="deleteAccountBtn" class="button button-danger">Delete My Account</button>
|
||||
</div>
|
||||
|
||||
<!-- Add more settings sections here later -->
|
||||
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
async handleClick(event) {
|
||||
if (event.target.id === 'exportDataBtn') {
|
||||
await this.exportUserData();
|
||||
} else if (event.target.id === 'deleteAccountBtn') {
|
||||
await this.deleteAccount();
|
||||
}
|
||||
}
|
||||
|
||||
async exportUserData() {
|
||||
try {
|
||||
logger.info('Initiating data export...', { action: 'USER_DATA_EXPORT_ATTEMPT' });
|
||||
const response = await fetch('/api/users/me/export', {
|
||||
headers: {
|
||||
'Authorization': `Bearer ${api.getToken()}`,
|
||||
'Accept': 'application/json'
|
||||
}
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error! status: ${response.status}`);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
const filename = `rbox_user_data_${new Date().toISOString().slice(0,10)}.json`;
|
||||
const blob = new Blob([JSON.stringify(data, null, 2)], { type: 'application/json' });
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = filename;
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
document.body.removeChild(a);
|
||||
URL.revokeObjectURL(url);
|
||||
logger.info('User data exported successfully.', { action: 'USER_DATA_EXPORT_SUCCESS' });
|
||||
document.dispatchEvent(new CustomEvent('show-toast', {
|
||||
detail: { message: 'Your data has been exported successfully!', type: 'success' }
|
||||
}));
|
||||
} catch (error) {
|
||||
logger.error('Failed to export user data:', { action: 'USER_DATA_EXPORT_FAILURE', error: error.message });
|
||||
document.dispatchEvent(new CustomEvent('show-toast', {
|
||||
detail: { message: `Failed to export data: ${error.message}`, type: 'error' }
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
async deleteAccount() {
|
||||
if (!confirm('Are you absolutely sure you want to delete your account? This action cannot be undone and all your data will be permanently lost.')) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
logger.warn('Initiating account deletion...', { action: 'USER_ACCOUNT_DELETE_ATTEMPT' });
|
||||
const response = await fetch('/api/users/me', {
|
||||
method: 'DELETE',
|
||||
headers: {
|
||||
'Authorization': `Bearer ${api.getToken()}`
|
||||
}
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error! status: ${response.status}`);
|
||||
}
|
||||
|
||||
logger.info('Account deleted successfully. Logging out...', { action: 'USER_ACCOUNT_DELETE_SUCCESS' });
|
||||
api.logout(); // Clear token and redirect to login
|
||||
document.dispatchEvent(new CustomEvent('show-toast', {
|
||||
detail: { message: 'Your account has been successfully deleted.', type: 'success' }
|
||||
}));
|
||||
} catch (error) {
|
||||
logger.error('Failed to delete account:', { action: 'USER_ACCOUNT_DELETE_FAILURE', error: error.message });
|
||||
document.dispatchEvent(new CustomEvent('show-toast', {
|
||||
detail: { message: `Failed to delete account: ${error.message}`, type: 'error' }
|
||||
}));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define('user-settings', UserSettings);
|
||||
Reference in New Issue
Block a user