redis optimization/improvments
This commit is contained in:
@@ -0,0 +1,724 @@
|
||||
# View Architecture & Reference
|
||||
|
||||
**Version:** 1.0
|
||||
**Status:** PRODUCTION
|
||||
**Last Updated:** January 2026
|
||||
|
||||
---
|
||||
|
||||
## Overview
|
||||
|
||||
This document defines the professional view architecture for Job Info. All UI should follow these patterns for consistency, maintainability, and professional appearance.
|
||||
|
||||
---
|
||||
|
||||
## View Hierarchy
|
||||
|
||||
```
|
||||
JobInfo Application
|
||||
├── AuthView
|
||||
│ └── Sign In Form
|
||||
├── LandingView
|
||||
│ ├── Header
|
||||
│ ├── JobCardView
|
||||
│ │ ├── Search & Filters
|
||||
│ │ └── Job Cards (Grid)
|
||||
│ └── Footer
|
||||
├── ManagerInfoView
|
||||
│ ├── Job Header
|
||||
│ ├── Job Info Panel
|
||||
│ ├── FileListView
|
||||
│ │ ├── File Search
|
||||
│ │ ├── File Categories
|
||||
│ │ │ ├── Manager Info
|
||||
│ │ │ ├── Contracts / Estimates
|
||||
│ │ │ ├── Submittals
|
||||
│ │ │ ├── Plans
|
||||
│ │ │ └── Other
|
||||
│ │ └── File Actions
|
||||
│ ├── NotesSection
|
||||
│ └── Footer
|
||||
├── NotesView
|
||||
│ ├── Notes Header
|
||||
│ ├── Notes Content (Read-only)
|
||||
│ └── Back to Job Detail
|
||||
└── FilePreviewModal
|
||||
├── PDF Viewer
|
||||
├── Image Viewer
|
||||
└── Word Document Handler
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 1. AuthView (Sign In Page)
|
||||
|
||||
**Purpose:** Authenticate user via PocketBase
|
||||
|
||||
**Components:**
|
||||
- Logo/Header
|
||||
- Email input
|
||||
- Password input
|
||||
- Sign In button
|
||||
- Error messages
|
||||
- Remember Me checkbox (optional)
|
||||
|
||||
**State:**
|
||||
- `isLoading`: boolean
|
||||
- `errorMessage`: string | null
|
||||
- `email`: string
|
||||
- `password`: string
|
||||
|
||||
**Key Functions:**
|
||||
```javascript
|
||||
async function handleSignIn(email, password) {
|
||||
// 1. Show loading state
|
||||
// 2. Call pb.collection('users').authWithPassword()
|
||||
// 3. Store token in localStorage
|
||||
// 4. Redirect to index.html (LandingView)
|
||||
// 5. Handle errors gracefully
|
||||
}
|
||||
```
|
||||
|
||||
**HTML Structure:**
|
||||
```html
|
||||
<div class="auth-container">
|
||||
<div class="auth-card">
|
||||
<div class="auth-header">
|
||||
<img src="assets/logo.png" alt="Logo" />
|
||||
<h1>Job Info</h1>
|
||||
</div>
|
||||
|
||||
<form id="signinForm" class="auth-form">
|
||||
<div class="form-group">
|
||||
<label for="email">Email</label>
|
||||
<input
|
||||
id="email"
|
||||
type="email"
|
||||
placeholder="you@example.com"
|
||||
class="form-input"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="password">Password</label>
|
||||
<input
|
||||
id="password"
|
||||
type="password"
|
||||
placeholder="••••••••"
|
||||
class="form-input"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div id="errorMsg" class="error-message hidden"></div>
|
||||
|
||||
<button type="submit" class="btn btn-primary btn-lg w-full">
|
||||
Sign In
|
||||
</button>
|
||||
</form>
|
||||
|
||||
<div class="auth-footer">
|
||||
<p class="text-sm text-gray-600">
|
||||
Secure login via PocketBase
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
```
|
||||
|
||||
**CSS Classes:**
|
||||
```css
|
||||
.auth-container { /* Full screen container */ }
|
||||
.auth-card { /* Centered white card */ }
|
||||
.auth-header { /* Logo + title */ }
|
||||
.auth-form { /* Form styling */ }
|
||||
.form-group { /* Input group */ }
|
||||
.form-input { /* Input field */ }
|
||||
.error-message { /* Red error text */ }
|
||||
.btn { /* Button base */ }
|
||||
.btn-primary { /* Blue button */ }
|
||||
.btn-lg { /* Large button */ }
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 2. LandingView (Home / Jobs List)
|
||||
|
||||
**Purpose:** Display list of jobs with search and filters
|
||||
|
||||
**Components:**
|
||||
- Header (with user info & sign out)
|
||||
- Search input
|
||||
- Filter controls (status, date range)
|
||||
- Job cards in grid
|
||||
- Pagination
|
||||
- Loading skeleton
|
||||
- Empty state
|
||||
|
||||
**State:**
|
||||
```javascript
|
||||
const landingState = {
|
||||
jobs: [], // Job list
|
||||
page: 1,
|
||||
perPage: 10,
|
||||
total: 0,
|
||||
sort: '-Job_Number', // Default sort
|
||||
searchTerm: '',
|
||||
filters: {
|
||||
status: '',
|
||||
dateRange: { start: null, end: null }
|
||||
},
|
||||
isLoading: false,
|
||||
error: null,
|
||||
cache: {} // For pagination memory
|
||||
};
|
||||
```
|
||||
|
||||
**Key Functions:**
|
||||
```javascript
|
||||
async function loadJobs(page = 1, searchTerm = '', filters = {}) {
|
||||
// 1. Show loading skeleton
|
||||
// 2. Build cache key
|
||||
// 3. Check Redis cache
|
||||
// 4. If miss, fetch from /api/jobs
|
||||
// 5. Display jobs
|
||||
// 6. Handle pagination
|
||||
}
|
||||
|
||||
function handleSearch(term) {
|
||||
// 1. Debounce (300ms)
|
||||
// 2. Update state.searchTerm
|
||||
// 3. Reset to page 1
|
||||
// 4. Call loadJobs()
|
||||
}
|
||||
|
||||
function handleFilterChange(filter) {
|
||||
// 1. Update state.filters
|
||||
// 2. Reset to page 1
|
||||
// 3. Call loadJobs()
|
||||
}
|
||||
|
||||
async function handleJobClick(job) {
|
||||
// 1. Navigate to ManagerInfoView
|
||||
// 2. Store current job in state
|
||||
// 3. Fetch job files
|
||||
}
|
||||
```
|
||||
|
||||
**HTML Structure:**
|
||||
```html
|
||||
<div class="landing-container">
|
||||
<!-- Header -->
|
||||
<header class="landing-header">
|
||||
<div class="header-left">
|
||||
<img src="assets/logo.png" alt="Logo" class="logo" />
|
||||
<h1>Job Info</h1>
|
||||
</div>
|
||||
|
||||
<div class="header-right">
|
||||
<span class="user-name" id="userNameDisplay">John Doe</span>
|
||||
<button class="btn btn-secondary" onclick="handleSignOut()">
|
||||
Sign Out
|
||||
</button>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<!-- Search & Filters -->
|
||||
<div class="search-filters-bar">
|
||||
<div class="search-group">
|
||||
<input
|
||||
type="search"
|
||||
id="jobSearch"
|
||||
placeholder="Search jobs by name or number..."
|
||||
class="search-input"
|
||||
/>
|
||||
<button class="btn-search">🔍</button>
|
||||
</div>
|
||||
|
||||
<div class="filters-group">
|
||||
<select id="statusFilter" class="filter-select">
|
||||
<option value="">All Statuses</option>
|
||||
<option value="Active">Active</option>
|
||||
<option value="Completed">Completed</option>
|
||||
<option value="Pending">Pending</option>
|
||||
</select>
|
||||
|
||||
<input
|
||||
type="date"
|
||||
id="startDate"
|
||||
class="filter-input"
|
||||
/>
|
||||
<input
|
||||
type="date"
|
||||
id="endDate"
|
||||
class="filter-input"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Job Cards Grid -->
|
||||
<div class="jobs-grid" id="jobsGrid">
|
||||
<!-- JobCard components inserted here -->
|
||||
</div>
|
||||
|
||||
<!-- Pagination -->
|
||||
<div class="pagination">
|
||||
<button class="btn-pagination" onclick="previousPage()">← Previous</button>
|
||||
<span id="pageInfo">Page 1 of 10</span>
|
||||
<button class="btn-pagination" onclick="nextPage()">Next →</button>
|
||||
</div>
|
||||
|
||||
<!-- Loading Skeleton -->
|
||||
<div id="loadingSkeleton" class="skeleton-grid hidden">
|
||||
<!-- 10 skeleton cards -->
|
||||
</div>
|
||||
|
||||
<!-- Empty State -->
|
||||
<div id="emptyState" class="empty-state hidden">
|
||||
<div class="empty-state-icon">📭</div>
|
||||
<h3>No jobs found</h3>
|
||||
<p>Try adjusting your filters or search term</p>
|
||||
</div>
|
||||
</div>
|
||||
```
|
||||
|
||||
**JobCard Component:**
|
||||
```html
|
||||
<div class="job-card" onclick="handleJobClick(jobData)">
|
||||
<div class="card-header">
|
||||
<h3 class="card-title">Job# 4757</h3>
|
||||
<span class="card-badge badge-active">Active</span>
|
||||
</div>
|
||||
|
||||
<div class="card-body">
|
||||
<p class="card-subtitle">Construction Project Name</p>
|
||||
|
||||
<div class="card-details">
|
||||
<div class="detail-row">
|
||||
<span class="detail-label">Manager:</span>
|
||||
<span class="detail-value">John Smith</span>
|
||||
</div>
|
||||
<div class="detail-row">
|
||||
<span class="detail-label">Start Date:</span>
|
||||
<span class="detail-value">Jan 15, 2025</span>
|
||||
</div>
|
||||
<div class="detail-row">
|
||||
<span class="detail-label">Files:</span>
|
||||
<span class="detail-value">42</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card-footer">
|
||||
<button class="btn-card-action">View Details →</button>
|
||||
</div>
|
||||
</div>
|
||||
```
|
||||
|
||||
**CSS Tailwind Classes:**
|
||||
```css
|
||||
.landing-container /* Full page layout */
|
||||
.landing-header /* Sticky header with logo */
|
||||
.header-left /* Logo + title */
|
||||
.header-right /* User info + sign out */
|
||||
.search-filters-bar /* Search + filter controls */
|
||||
.search-group /* Search input + button */
|
||||
.filters-group /* Filter dropdowns */
|
||||
.jobs-grid /* Grid of cards (responsive) */
|
||||
.job-card /* Individual card */
|
||||
.card-header /* Card title + badge */
|
||||
.card-body /* Card content */
|
||||
.card-details /* Key-value details */
|
||||
.pagination /* Page navigation */
|
||||
.skeleton-grid /* Loading placeholders */
|
||||
.empty-state /* No results message */
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 3. ManagerInfoView (Job Detail)
|
||||
|
||||
**Purpose:** Display job details, files, and notes
|
||||
|
||||
**Components:**
|
||||
- Job header with info
|
||||
- File list (categorized)
|
||||
- Notes section
|
||||
- Back button
|
||||
|
||||
**State:**
|
||||
```javascript
|
||||
const managerInfoState = {
|
||||
job: null, // Current job object
|
||||
files: [], // Files for this job
|
||||
fileFilter: '', // Search within files
|
||||
notes: [], // Sticky notes
|
||||
isLoadingFiles: false,
|
||||
error: null
|
||||
};
|
||||
```
|
||||
|
||||
**HTML Structure:**
|
||||
```html
|
||||
<div class="manager-info-container">
|
||||
<!-- Back Button -->
|
||||
<button class="btn btn-secondary" onclick="goBackToLanding()">
|
||||
← Back to Jobs
|
||||
</button>
|
||||
|
||||
<!-- Job Header -->
|
||||
<div class="job-header">
|
||||
<div class="header-title">
|
||||
<h1>Job# <span id="jobNumber">4757</span></h1>
|
||||
<p class="job-name" id="jobName">Construction Project Name</p>
|
||||
</div>
|
||||
|
||||
<div class="header-badges">
|
||||
<span class="badge badge-active">Active</span>
|
||||
<span class="badge">42 Files</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Job Info Panel -->
|
||||
<div class="job-info-panel">
|
||||
<div class="info-group">
|
||||
<label>Manager</label>
|
||||
<p class="info-value" id="jobManager">John Smith</p>
|
||||
</div>
|
||||
|
||||
<div class="info-group">
|
||||
<label>Contact</label>
|
||||
<p class="info-value" id="jobContact">john@example.com</p>
|
||||
</div>
|
||||
|
||||
<div class="info-group">
|
||||
<label>Start Date</label>
|
||||
<p class="info-value" id="jobStartDate">Jan 15, 2025</p>
|
||||
</div>
|
||||
|
||||
<div class="info-group">
|
||||
<label>Status</label>
|
||||
<p class="info-value" id="jobStatus">Active</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- File List Section -->
|
||||
<div class="file-list-section">
|
||||
<h2>Files</h2>
|
||||
|
||||
<div class="file-search-row">
|
||||
<input
|
||||
id="fileSearchInput"
|
||||
type="search"
|
||||
placeholder="Search files..."
|
||||
class="search-input"
|
||||
/>
|
||||
<a href="#" target="_blank" rel="noopener" class="btn btn-secondary">
|
||||
Open Folder
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<!-- File Groups (categorized) -->
|
||||
<div id="fileGroups" class="file-groups">
|
||||
<!-- FileGroup components inserted here -->
|
||||
</div>
|
||||
|
||||
<!-- Loading State -->
|
||||
<div id="fileLoading" class="loading-spinner hidden">
|
||||
<div class="spinner"></div>
|
||||
<p>Loading files...</p>
|
||||
</div>
|
||||
|
||||
<!-- Empty State -->
|
||||
<div id="filesEmpty" class="empty-state hidden">
|
||||
<p>No files found</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Notes Section -->
|
||||
<div class="notes-section">
|
||||
<h2>Notes</h2>
|
||||
|
||||
<div class="notes-grid" id="notesGrid">
|
||||
<!-- StickyNote components inserted here -->
|
||||
</div>
|
||||
|
||||
<button class="btn btn-primary" onclick="expandNotes()">
|
||||
Expand Notes
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
```
|
||||
|
||||
**FileGroup Component:**
|
||||
```html
|
||||
<div class="file-group">
|
||||
<div class="group-header">
|
||||
<h3>Manager Info <span class="file-count">(5)</span></h3>
|
||||
</div>
|
||||
|
||||
<div class="file-list">
|
||||
<div class="file-item">
|
||||
<div class="file-icon">📄</div>
|
||||
<div class="file-info">
|
||||
<p class="file-name">manager_info.pdf</p>
|
||||
<p class="file-meta">1.2 MB · Jan 20, 2025</p>
|
||||
</div>
|
||||
<div class="file-actions">
|
||||
<button class="btn btn-sm btn-primary" onclick="previewFile(...)">
|
||||
Preview
|
||||
</button>
|
||||
<a href="#" target="_blank" class="btn btn-sm btn-secondary">
|
||||
Download
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- More files... -->
|
||||
</div>
|
||||
</div>
|
||||
```
|
||||
|
||||
**StickyNote Component:**
|
||||
```html
|
||||
<div class="sticky-note note-yellow">
|
||||
<div class="note-content">
|
||||
<p>Project kickoff meeting scheduled for Monday</p>
|
||||
</div>
|
||||
<div class="note-date">Jan 20, 2025</div>
|
||||
</div>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. NotesView (Expanded Notes)
|
||||
|
||||
**Purpose:** Full-screen view of job notes
|
||||
|
||||
**Components:**
|
||||
- Notes list (full content)
|
||||
- Back button
|
||||
|
||||
**HTML Structure:**
|
||||
```html
|
||||
<div class="notes-view-container">
|
||||
<button class="btn btn-secondary" onclick="goBackToJobDetail()">
|
||||
← Back to Job
|
||||
</button>
|
||||
|
||||
<h1>Job# <span id="noteJobNumber">4757</span> - Notes</h1>
|
||||
|
||||
<div class="notes-expanded-grid" id="notesExpandedGrid">
|
||||
<!-- Full StickyNote components with larger content -->
|
||||
</div>
|
||||
|
||||
<div id="notesEmpty" class="empty-state hidden">
|
||||
<p>No notes for this job</p>
|
||||
</div>
|
||||
</div>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 5. FilePreviewModal
|
||||
|
||||
**Purpose:** Preview files (PDF, images, documents)
|
||||
|
||||
**Components:**
|
||||
- Modal overlay
|
||||
- Viewer (PDF, image, or iframe for documents)
|
||||
- File info
|
||||
- Close button
|
||||
|
||||
**HTML Structure:**
|
||||
```html
|
||||
<div id="iframeViewerModal" class="modal hidden">
|
||||
<div class="modal-content">
|
||||
<!-- Header -->
|
||||
<div class="modal-header">
|
||||
<h3 id="iframeTitle">File Preview</h3>
|
||||
<button class="btn-close" onclick="closeModal()">✕</button>
|
||||
</div>
|
||||
|
||||
<!-- PDF Viewer -->
|
||||
<div id="pdfViewer" class="viewer hidden">
|
||||
<!-- Canvas for PDF rendering -->
|
||||
</div>
|
||||
|
||||
<!-- Image Viewer -->
|
||||
<div id="imageViewer" class="viewer hidden">
|
||||
<img id="imageContent" src="" alt="" />
|
||||
</div>
|
||||
|
||||
<!-- iFrame for Documents -->
|
||||
<div id="iframeViewer" class="viewer hidden">
|
||||
<iframe id="fileFrame" src=""></iframe>
|
||||
</div>
|
||||
|
||||
<!-- Loading -->
|
||||
<div id="iframeLoader" class="loader hidden">
|
||||
<div class="spinner"></div>
|
||||
<p>Loading file...</p>
|
||||
</div>
|
||||
|
||||
<!-- Footer -->
|
||||
<div class="modal-footer">
|
||||
<a id="downloadLink" href="" target="_blank" class="btn btn-primary">
|
||||
Download
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 6. Component Standards
|
||||
|
||||
### Button Styles
|
||||
|
||||
```html
|
||||
<!-- Primary (Blue) -->
|
||||
<button class="btn btn-primary">Action</button>
|
||||
|
||||
<!-- Secondary (Gray) -->
|
||||
<button class="btn btn-secondary">Cancel</button>
|
||||
|
||||
<!-- Danger (Red) -->
|
||||
<button class="btn btn-danger">Delete</button>
|
||||
|
||||
<!-- Disabled -->
|
||||
<button class="btn btn-primary" disabled>Disabled</button>
|
||||
|
||||
<!-- Sizes -->
|
||||
<button class="btn btn-sm">Small</button>
|
||||
<button class="btn">Normal</button>
|
||||
<button class="btn btn-lg">Large</button>
|
||||
```
|
||||
|
||||
### Card Styles
|
||||
|
||||
```html
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h3>Title</h3>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<p>Content</p>
|
||||
</div>
|
||||
<div class="card-footer">
|
||||
<button>Action</button>
|
||||
</div>
|
||||
</div>
|
||||
```
|
||||
|
||||
### Badge Styles
|
||||
|
||||
```html
|
||||
<span class="badge badge-active">Active</span>
|
||||
<span class="badge badge-pending">Pending</span>
|
||||
<span class="badge badge-completed">Completed</span>
|
||||
```
|
||||
|
||||
### Input Styles
|
||||
|
||||
```html
|
||||
<input type="text" class="input-field" placeholder="..." />
|
||||
<input type="email" class="input-field" placeholder="..." />
|
||||
<input type="date" class="input-field" />
|
||||
|
||||
<select class="select-field">
|
||||
<option>Option 1</option>
|
||||
</select>
|
||||
|
||||
<textarea class="textarea-field"></textarea>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 7. Responsive Design
|
||||
|
||||
### Breakpoints (Tailwind)
|
||||
|
||||
```
|
||||
Mobile: < 640px (sm: 640px)
|
||||
Tablet: 768px (md: 768px)
|
||||
Desktop: 1024px (lg: 1024px)
|
||||
Wide: 1280px (xl: 1280px)
|
||||
```
|
||||
|
||||
### Grid Layouts
|
||||
|
||||
```html
|
||||
<!-- Job Cards: 1 col mobile, 2 col tablet, 3 col desktop -->
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||
<!-- Cards -->
|
||||
</div>
|
||||
|
||||
<!-- File List: Full width on mobile, 2 col on tablet+ -->
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<!-- Files -->
|
||||
</div>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 8. Accessibility Standards
|
||||
|
||||
### WCAG 2.1 Level AA
|
||||
|
||||
- ✅ Color contrast ≥ 4.5:1 for text
|
||||
- ✅ Semantic HTML (use `<button>`, not `<div onclick>`)
|
||||
- ✅ ARIA labels for icons
|
||||
- ✅ Keyboard navigation (Tab, Enter)
|
||||
- ✅ Screen reader friendly
|
||||
|
||||
**Example:**
|
||||
```html
|
||||
<!-- ✅ CORRECT -->
|
||||
<button class="btn" aria-label="Close modal">✕</button>
|
||||
<label for="email">Email</label>
|
||||
<input id="email" type="email" />
|
||||
|
||||
<!-- ❌ WRONG -->
|
||||
<div onclick="close()" class="cursor-pointer">✕</div>
|
||||
<input type="email" placeholder="Email" />
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 9. Internationalization (i18n)
|
||||
|
||||
### Labels & Messages
|
||||
|
||||
All user-facing text should be:
|
||||
- English
|
||||
- Professional tone
|
||||
- Clear and concise
|
||||
- Consistent across views
|
||||
|
||||
**Example:**
|
||||
```javascript
|
||||
const LABELS = {
|
||||
SIGN_IN: 'Sign In',
|
||||
SEARCH_JOBS: 'Search jobs by name or number...',
|
||||
NO_FILES_FOUND: 'No files found',
|
||||
LOADING: 'Loading...',
|
||||
ERROR: 'An error occurred. Please try again.'
|
||||
};
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Summary
|
||||
|
||||
| View | Purpose | Key Components |
|
||||
|------|---------|-----------------|
|
||||
| AuthView | User login | Form, inputs, validation |
|
||||
| LandingView | Job list | Cards, search, filters, pagination |
|
||||
| ManagerInfoView | Job detail | Header, files, notes, actions |
|
||||
| NotesView | Expanded notes | Full-screen notes display |
|
||||
| FilePreviewModal | File preview | Viewer (PDF/image/document) |
|
||||
|
||||
**All UI must follow these patterns for consistency and professionalism.**
|
||||
Reference in New Issue
Block a user