diff --git a/css/components/block.css b/css/components/block.css
new file mode 100644
index 0000000..b920897
--- /dev/null
+++ b/css/components/block.css
@@ -0,0 +1,157 @@
+.blocked-users-manager {
+ display: block;
+}
+
+.block-header {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ margin-bottom: 1rem;
+}
+
+.block-header h3 {
+ margin: 0;
+ font-size: 1rem;
+ color: var(--color-text);
+}
+
+.block-count {
+ font-size: 0.875rem;
+ color: var(--color-text-secondary);
+}
+
+.block-add-form {
+ display: flex;
+ gap: 0.5rem;
+ margin-bottom: 1rem;
+}
+
+.block-input {
+ flex: 1;
+ padding: 0.625rem 0.875rem;
+ background: var(--color-surface);
+ border: 1px solid var(--color-border);
+ border-radius: var(--radius-md);
+ color: var(--color-text);
+ font-size: 0.875rem;
+}
+
+.block-input:focus {
+ outline: none;
+ border-color: var(--color-primary);
+ box-shadow: 0 0 0 3px rgba(233, 69, 96, 0.2);
+}
+
+.block-input::placeholder {
+ color: var(--color-text-muted);
+}
+
+.block-add-btn {
+ flex-shrink: 0;
+}
+
+.blocked-list {
+ display: flex;
+ flex-direction: column;
+ gap: 0.5rem;
+ max-height: 300px;
+ overflow-y: auto;
+}
+
+.blocked-item {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ padding: 0.75rem;
+ background: var(--color-surface);
+ border-radius: var(--radius-md);
+}
+
+.blocked-item .blocked-username {
+ font-size: 0.875rem;
+ color: var(--color-text);
+ font-weight: 500;
+}
+
+.blocked-empty {
+ text-align: center;
+ padding: 2rem;
+ color: var(--color-text-muted);
+}
+
+.blocked-empty p {
+ margin: 0;
+}
+
+.block-actions {
+ margin-top: 1rem;
+ padding-top: 1rem;
+ border-top: 1px solid var(--color-border);
+}
+
+.rant-detail-blocked {
+ padding: 1rem;
+}
+
+.rant-detail-blocked .detail-header {
+ display: flex;
+ align-items: center;
+ gap: 1rem;
+ margin-bottom: 2rem;
+}
+
+.blocked-message {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ justify-content: center;
+ padding: 3rem;
+ text-align: center;
+ color: var(--color-text-secondary);
+}
+
+.blocked-message svg {
+ color: var(--color-error);
+ margin-bottom: 1rem;
+ opacity: 0.7;
+}
+
+.blocked-message p {
+ margin: 0.25rem 0;
+}
+
+.blocked-message .blocked-username {
+ font-weight: 600;
+ color: var(--color-text);
+ margin-bottom: 1rem;
+}
+
+.blocked-message .unblock-btn {
+ margin-top: 1rem;
+}
+
+.profile-blocked-banner {
+ display: flex;
+ align-items: center;
+ gap: 0.5rem;
+ padding: 0.75rem 1rem;
+ background: rgba(248, 113, 113, 0.1);
+ border: 1px solid var(--color-error);
+ border-radius: var(--radius-md);
+ color: var(--color-error);
+ margin: 0 1rem 1rem;
+ font-size: 0.875rem;
+}
+
+.profile-blocked-banner svg {
+ flex-shrink: 0;
+}
+
+.block-user-btn {
+ margin-top: 0.5rem;
+}
+
+.btn-small {
+ padding: 0.375rem 0.75rem;
+ font-size: 0.75rem;
+}
diff --git a/index.html b/index.html
index 67d269e..ce3b65d 100644
--- a/index.html
+++ b/index.html
@@ -38,6 +38,7 @@
+
diff --git a/js/app.js b/js/app.js
index 8c758d3..5ecc457 100644
--- a/js/app.js
+++ b/js/app.js
@@ -10,6 +10,7 @@ import { StorageService } from './services/storage.js';
import { AuthService } from './services/auth.js';
import { Router } from './services/router.js';
import { ThemeService } from './services/theme.js';
+import { BlockService } from './services/block.js';
import { markdownRenderer } from './utils/markdown.js';
import { BaseComponent } from './components/base-component.js';
@@ -33,6 +34,7 @@ import { CommentForm } from './components/comment-form.js';
import { UserProfile } from './components/user-profile.js';
import { NotificationList, NotificationItem } from './components/notification-list.js';
import { PostForm, PostModal } from './components/post-form.js';
+import { BlockedUsersManager } from './components/blocked-users-manager.js';
import { HomePage } from './pages/home-page.js';
import { RantPage } from './pages/rant-page.js';
@@ -52,6 +54,7 @@ class Application {
this.auth = new AuthService(this.api, this.storage);
this.router = new Router();
this.theme = new ThemeService(this.storage);
+ this.block = new BlockService(this.storage);
this.toast = null;
this.header = null;
this.nav = null;
@@ -90,6 +93,7 @@ class Application {
async init() {
await this.auth.init();
this.theme.init();
+ this.block.init();
await markdownRenderer.init();
this.setupToast();
diff --git a/js/components/blocked-users-manager.js b/js/components/blocked-users-manager.js
new file mode 100644
index 0000000..50eae55
--- /dev/null
+++ b/js/components/blocked-users-manager.js
@@ -0,0 +1,111 @@
+import { BaseComponent } from './base-component.js';
+
+class BlockedUsersManager extends BaseComponent {
+ init() {
+ this.blockChangeHandler = () => this.render();
+ this.render();
+ this.bindEvents();
+ }
+
+ render() {
+ const blockedUsers = this.getBlock()?.getBlockedUsers() || [];
+
+ this.addClass('blocked-users-manager');
+
+ this.setHtml(`
+
+
+
+ ${blockedUsers.length > 0 ? blockedUsers.map(username => `
+
+ @${username}
+
+
+ `).join('') : `
+
+ `}
+
+ ${blockedUsers.length > 0 ? `
+
+
+
+ ` : ''}
+ `);
+ }
+
+ bindEvents() {
+ this.on(this, 'submit', this.handleSubmit);
+ this.on(this, 'click', this.handleClick);
+ window.addEventListener('rantii:block-change', this.blockChangeHandler);
+ }
+
+ onDisconnected() {
+ window.removeEventListener('rantii:block-change', this.blockChangeHandler);
+ }
+
+ handleSubmit(e) {
+ e.preventDefault();
+ const input = this.$('.block-input');
+ if (!input) return;
+
+ const username = input.value.trim().replace(/^@/, '');
+ if (!username) return;
+
+ const block = this.getBlock();
+ if (block?.isBlocked(username)) {
+ window.app?.toast?.error(`@${username} is already blocked`);
+ return;
+ }
+
+ if (block?.block(username)) {
+ window.app?.toast?.success(`Blocked @${username}`);
+ input.value = '';
+ }
+ }
+
+ handleClick(e) {
+ const unblockBtn = e.target.closest('.unblock-btn');
+ const clearAllBtn = e.target.closest('.clear-all-btn');
+
+ if (unblockBtn) {
+ e.preventDefault();
+ e.stopPropagation();
+ const username = unblockBtn.dataset.username;
+ if (username && this.getBlock()?.unblock(username)) {
+ window.app?.toast?.success(`Unblocked @${username}`);
+ }
+ return;
+ }
+
+ if (clearAllBtn) {
+ e.preventDefault();
+ e.stopPropagation();
+ if (confirm('Unblock all users?')) {
+ this.getBlock()?.clear();
+ window.app?.toast?.success('All users unblocked');
+ }
+ }
+ }
+
+ getBlock() {
+ return window.app?.block;
+ }
+}
+
+customElements.define('blocked-users-manager', BlockedUsersManager);
+
+export { BlockedUsersManager };
diff --git a/js/components/notification-list.js b/js/components/notification-list.js
index d850a97..8f094ec 100644
--- a/js/components/notification-list.js
+++ b/js/components/notification-list.js
@@ -13,10 +13,21 @@ class NotificationList extends BaseComponent {
this.notifications = [];
this.isLoading = false;
this.unreadCount = 0;
+ this.blockChangeHandler = () => this.render();
this.render();
this.bindEvents();
}
+ getBlock() {
+ return window.app?.block;
+ }
+
+ getFilteredNotifications() {
+ const block = this.getBlock();
+ if (!block) return this.notifications;
+ return block.filterNotifications(this.notifications);
+ }
+
async load() {
if (!this.isLoggedIn()) {
this.render();
@@ -62,7 +73,9 @@ class NotificationList extends BaseComponent {
return;
}
- if (this.notifications.length === 0) {
+ const filteredNotifications = this.getFilteredNotifications();
+
+ if (filteredNotifications.length === 0) {
this.setHtml(`