@mytec: added iteration1 full task, removed prev
This commit is contained in:
@@ -1,251 +0,0 @@
|
|||||||
# RFCP Fixes - Iteration 1
|
|
||||||
|
|
||||||
## Issues to Fix:
|
|
||||||
|
|
||||||
### 1. Heatmap Colors - More Obvious Gradient
|
|
||||||
|
|
||||||
**Current problem:** Green→Yellow→Red gradient not obvious enough
|
|
||||||
|
|
||||||
**Fix:** Update `src/components/map/Heatmap.tsx`
|
|
||||||
|
|
||||||
Change the gradient to use more distinct colors with better visibility:
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
// In Heatmap.tsx, update the gradient prop:
|
|
||||||
|
|
||||||
<HeatmapLayer
|
|
||||||
points={heatmapPoints}
|
|
||||||
longitudeExtractor={(p: any) => p[1]}
|
|
||||||
latitudeExtractor={(p: any) => p[0]}
|
|
||||||
intensityExtractor={(p: any) => p[2]}
|
|
||||||
gradient={{
|
|
||||||
0.0: '#0000ff', // Blue (very weak, -120 dBm)
|
|
||||||
0.2: '#00ffff', // Cyan (weak, -110 dBm)
|
|
||||||
0.4: '#00ff00', // Green (fair, -100 dBm)
|
|
||||||
0.6: '#ffff00', // Yellow (good, -85 dBm)
|
|
||||||
0.8: '#ff7f00', // Orange (strong, -70 dBm)
|
|
||||||
1.0: '#ff0000', // Red (excellent, > -70 dBm)
|
|
||||||
}}
|
|
||||||
radius={25}
|
|
||||||
blur={15}
|
|
||||||
max={1.0}
|
|
||||||
/>
|
|
||||||
```
|
|
||||||
|
|
||||||
**Reasoning:**
|
|
||||||
- Blue/Cyan for weak signals (more intuitive - "cold" = weak)
|
|
||||||
- Green for acceptable
|
|
||||||
- Yellow/Orange for good
|
|
||||||
- Red for excellent (hot = strong)
|
|
||||||
|
|
||||||
This is actually inverted from typical "green=good", but in RF planning, RED = STRONG SIGNAL = GOOD!
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### 2. Coverage Radius - Increase to 100km
|
|
||||||
|
|
||||||
**Current problem:** Max radius only 20km, not enough for tactical planning
|
|
||||||
|
|
||||||
**Fix:** Update `src/components/panels/SiteForm.tsx` (or wherever Coverage Settings are)
|
|
||||||
|
|
||||||
Find the radius slider and change:
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
// Old:
|
|
||||||
<Slider
|
|
||||||
label="Radius (km)"
|
|
||||||
min={1}
|
|
||||||
max={20} // ← Change this
|
|
||||||
step={1}
|
|
||||||
value={coverageSettings.radius}
|
|
||||||
onChange={(value) => updateCoverageSettings({ radius: value })}
|
|
||||||
/>
|
|
||||||
|
|
||||||
// New:
|
|
||||||
<Slider
|
|
||||||
label="Radius (km)"
|
|
||||||
min={1}
|
|
||||||
max={100} // ← Increased to 100km
|
|
||||||
step={5} // ← Larger step for easier control
|
|
||||||
value={coverageSettings.radius}
|
|
||||||
onChange={(value) => updateCoverageSettings({ radius: value })}
|
|
||||||
help="Calculation area around each site"
|
|
||||||
/>
|
|
||||||
```
|
|
||||||
|
|
||||||
**Also update default value in store:**
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
// In src/store/coverage.ts or wherever coverage settings are initialized:
|
|
||||||
const initialSettings = {
|
|
||||||
radius: 10, // Default 10km (reasonable starting point)
|
|
||||||
resolution: 200, // 200m resolution
|
|
||||||
rsrpThreshold: -120
|
|
||||||
};
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### 3. Save & Calculate Button
|
|
||||||
|
|
||||||
**Current problem:** Two separate buttons, extra click needed
|
|
||||||
|
|
||||||
**Fix:** Update `src/components/panels/SiteForm.tsx`
|
|
||||||
|
|
||||||
Replace the button section:
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
// Old:
|
|
||||||
<div className="flex gap-2">
|
|
||||||
<Button onClick={handleSave}>Save</Button>
|
|
||||||
<Button onClick={handleDelete} variant="danger">Delete</Button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
// New:
|
|
||||||
<div className="flex gap-2">
|
|
||||||
<Button
|
|
||||||
onClick={handleSaveAndCalculate}
|
|
||||||
variant="primary"
|
|
||||||
className="flex-1"
|
|
||||||
>
|
|
||||||
💾 Save & Calculate Coverage
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
onClick={handleSave}
|
|
||||||
variant="secondary"
|
|
||||||
className="flex-1"
|
|
||||||
>
|
|
||||||
💾 Save Only
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
onClick={handleDelete}
|
|
||||||
variant="danger"
|
|
||||||
>
|
|
||||||
🗑️ Delete
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
// Add the handler:
|
|
||||||
const handleSaveAndCalculate = async () => {
|
|
||||||
// Save the site first
|
|
||||||
await handleSave();
|
|
||||||
|
|
||||||
// Then trigger coverage calculation
|
|
||||||
const sites = useSitesStore.getState().sites;
|
|
||||||
const coverage = useCoverageStore.getState();
|
|
||||||
|
|
||||||
// Calculate coverage for all sites
|
|
||||||
await coverage.calculateCoverage(sites);
|
|
||||||
|
|
||||||
// Show success toast
|
|
||||||
toast.success('Site saved and coverage calculated!');
|
|
||||||
};
|
|
||||||
```
|
|
||||||
|
|
||||||
**Alternative (simpler):** Just make the main button do both:
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
<div className="flex gap-2">
|
|
||||||
<Button
|
|
||||||
onClick={async () => {
|
|
||||||
await handleSave();
|
|
||||||
// Auto-trigger calculate after save
|
|
||||||
const sites = useSitesStore.getState().sites;
|
|
||||||
await useCoverageStore.getState().calculateCoverage(sites);
|
|
||||||
}}
|
|
||||||
variant="primary"
|
|
||||||
className="flex-1"
|
|
||||||
>
|
|
||||||
💾 Save & Calculate
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
onClick={handleDelete}
|
|
||||||
variant="danger"
|
|
||||||
>
|
|
||||||
🗑️ Delete
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### 4. Legend Colors Update
|
|
||||||
|
|
||||||
**Fix:** Update `src/components/map/Legend.tsx` to match new gradient:
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
const signalRanges = [
|
|
||||||
{ label: 'Excellent', range: '> -70 dBm', color: '#ff0000' }, // Red
|
|
||||||
{ label: 'Good', range: '-70 to -85 dBm', color: '#ff7f00' }, // Orange
|
|
||||||
{ label: 'Fair', range: '-85 to -100 dBm', color: '#ffff00' }, // Yellow
|
|
||||||
{ label: 'Poor', range: '-100 to -110 dBm', color: '#00ff00' }, // Green
|
|
||||||
{ label: 'Weak', range: '-110 to -120 dBm', color: '#00ffff' }, // Cyan
|
|
||||||
{ label: 'Very Weak', range: '< -120 dBm', color: '#0000ff' }, // Blue
|
|
||||||
];
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### 5. Resolution Adjustment for Large Radius
|
|
||||||
|
|
||||||
**Problem:** 200m resolution + 100km radius = MANY points = slow calculation
|
|
||||||
|
|
||||||
**Fix:** Auto-adjust resolution based on radius:
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
// In src/store/coverage.ts or coverage calculator:
|
|
||||||
|
|
||||||
const getOptimalResolution = (radius: number): number => {
|
|
||||||
if (radius <= 10) return 100; // 100m for small areas
|
|
||||||
if (radius <= 30) return 200; // 200m for medium areas
|
|
||||||
if (radius <= 60) return 300; // 300m for large areas
|
|
||||||
return 500; // 500m for very large areas (100km)
|
|
||||||
};
|
|
||||||
|
|
||||||
// Use in calculation:
|
|
||||||
const resolution = settings.resolution || getOptimalResolution(settings.radius);
|
|
||||||
```
|
|
||||||
|
|
||||||
Or add a notice in UI:
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
<p className="text-sm text-gray-500">
|
|
||||||
💡 Larger radius = longer calculation time.
|
|
||||||
Consider increasing resolution (200m → 500m) for faster results.
|
|
||||||
</p>
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Summary of Changes:
|
|
||||||
|
|
||||||
1. ✅ **Heatmap gradient:** Blue → Cyan → Green → Yellow → Orange → Red
|
|
||||||
2. ✅ **Max radius:** 20km → 100km (step: 5km)
|
|
||||||
3. ✅ **Button:** "Save & Calculate" as primary action
|
|
||||||
4. ✅ **Legend:** Updated colors to match new gradient
|
|
||||||
5. ✅ **Performance:** Auto-adjust resolution or show warning
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Implementation Order:
|
|
||||||
|
|
||||||
1. **Start with colors** (easiest, biggest visual impact)
|
|
||||||
2. **Then radius** (simple slider change)
|
|
||||||
3. **Then button** (requires store integration)
|
|
||||||
4. **Test everything**
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Files to Edit:
|
|
||||||
|
|
||||||
```
|
|
||||||
frontend/src/components/map/Heatmap.tsx - gradient colors
|
|
||||||
frontend/src/components/map/Legend.tsx - legend colors
|
|
||||||
frontend/src/components/panels/SiteForm.tsx - radius slider + button
|
|
||||||
frontend/src/store/coverage.ts - default settings
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
Would you like me to create the exact code patches for Claude Code to apply?
|
|
||||||
Or should I create complete replacement files?
|
|
||||||
847
RFCP-Iteration1-Full-Task.md
Normal file
847
RFCP-Iteration1-Full-Task.md
Normal file
@@ -0,0 +1,847 @@
|
|||||||
|
# RFCP - Iteration 1: Fixes & Features
|
||||||
|
## Comprehensive Task for Claude Code
|
||||||
|
|
||||||
|
Read RFCP-Fixes-Iteration1.md and RFCP-TechSpec-v3.0.md for context.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## CRITICAL FIXES (Priority: HIGH)
|
||||||
|
|
||||||
|
### 1. Heatmap Color Gradient - More Obvious
|
||||||
|
**Current:** Green → Yellow → Red (not very distinct)
|
||||||
|
**New:** Blue → Cyan → Green → Yellow → Orange → Red
|
||||||
|
|
||||||
|
**File:** `frontend/src/components/map/Heatmap.tsx`
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Update gradient in HeatmapLayer component:
|
||||||
|
gradient={{
|
||||||
|
0.0: '#0d47a1', // Dark Blue (very weak, -120 dBm)
|
||||||
|
0.2: '#00bcd4', // Cyan (weak, -110 dBm)
|
||||||
|
0.4: '#4caf50', // Green (fair, -100 dBm)
|
||||||
|
0.6: '#ffeb3b', // Yellow (good, -85 dBm)
|
||||||
|
0.8: '#ff9800', // Orange (strong, -70 dBm)
|
||||||
|
1.0: '#f44336', // Red (excellent, > -70 dBm)
|
||||||
|
}}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Reasoning:**
|
||||||
|
- Cold colors (blue/cyan) = weak signal
|
||||||
|
- Warm colors (yellow/orange/red) = strong signal
|
||||||
|
- More intuitive for RF planning
|
||||||
|
- Better contrast and visibility
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 2. Coverage Radius - Increase to 100km
|
||||||
|
**Current:** Max 20km
|
||||||
|
**New:** Max 100km with larger step
|
||||||
|
|
||||||
|
**File:** `frontend/src/components/panels/SiteForm.tsx` or wherever Coverage Settings panel is
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Find the radius slider and update:
|
||||||
|
<Slider
|
||||||
|
label="Radius"
|
||||||
|
min={1}
|
||||||
|
max={100} // ← Changed from 20
|
||||||
|
step={5} // ← Changed from 1 (easier control)
|
||||||
|
value={coverageSettings.radius}
|
||||||
|
onChange={(value) => updateCoverageSettings({ radius: value })}
|
||||||
|
suffix="km"
|
||||||
|
help="Calculation area around each site"
|
||||||
|
/>
|
||||||
|
```
|
||||||
|
|
||||||
|
**Also update default value:**
|
||||||
|
|
||||||
|
**File:** `frontend/src/store/coverage.ts` (or wherever initial settings are)
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const defaultSettings: CoverageSettings = {
|
||||||
|
radius: 10, // Changed from 20 to 10 (better default)
|
||||||
|
resolution: 200, // 200m
|
||||||
|
rsrpThreshold: -120
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 3. "Save & Calculate" Button - Primary Action
|
||||||
|
**Current:** Separate "Save" and "Calculate" buttons
|
||||||
|
**New:** "Save & Calculate" as primary action + "Save Only" as secondary
|
||||||
|
|
||||||
|
**File:** `frontend/src/components/panels/SiteForm.tsx`
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Replace button section:
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<Button
|
||||||
|
onClick={handleSaveAndCalculate}
|
||||||
|
variant="primary"
|
||||||
|
className="flex-1"
|
||||||
|
>
|
||||||
|
💾 Save & Calculate
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
onClick={handleSave}
|
||||||
|
variant="secondary"
|
||||||
|
>
|
||||||
|
💾 Save Only
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
onClick={handleDelete}
|
||||||
|
variant="danger"
|
||||||
|
>
|
||||||
|
🗑️
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
// Add handler function:
|
||||||
|
const handleSaveAndCalculate = async () => {
|
||||||
|
try {
|
||||||
|
// Save the site first
|
||||||
|
await handleSave();
|
||||||
|
|
||||||
|
// Get all sites from store
|
||||||
|
const sites = useSitesStore.getState().sites;
|
||||||
|
|
||||||
|
if (sites.length === 0) {
|
||||||
|
toast.info('Add at least one site to calculate coverage');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Trigger coverage calculation
|
||||||
|
const coverageStore = useCoverageStore.getState();
|
||||||
|
await coverageStore.calculateCoverage(sites);
|
||||||
|
|
||||||
|
// Success feedback
|
||||||
|
toast.success(`Coverage calculated for ${sites.length} site(s)!`);
|
||||||
|
} catch (error) {
|
||||||
|
toast.error(`Failed: ${error.message}`);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 4. Legend Colors - Match New Gradient
|
||||||
|
**File:** `frontend/src/components/map/Legend.tsx`
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const signalRanges = [
|
||||||
|
{
|
||||||
|
label: 'Excellent',
|
||||||
|
range: '> -70 dBm',
|
||||||
|
color: '#f44336', // Red
|
||||||
|
description: 'Very strong signal'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Good',
|
||||||
|
range: '-70 to -85 dBm',
|
||||||
|
color: '#ff9800', // Orange
|
||||||
|
description: 'Strong signal'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Fair',
|
||||||
|
range: '-85 to -100 dBm',
|
||||||
|
color: '#ffeb3b', // Yellow
|
||||||
|
description: 'Acceptable signal'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Poor',
|
||||||
|
range: '-100 to -110 dBm',
|
||||||
|
color: '#4caf50', // Green
|
||||||
|
description: 'Weak signal'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Weak',
|
||||||
|
range: '-110 to -120 dBm',
|
||||||
|
color: '#00bcd4', // Cyan
|
||||||
|
description: 'Very weak signal'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'No Service',
|
||||||
|
range: '< -120 dBm',
|
||||||
|
color: '#0d47a1', // Dark Blue
|
||||||
|
description: 'No coverage'
|
||||||
|
},
|
||||||
|
];
|
||||||
|
```
|
||||||
|
|
||||||
|
**Also update constants:**
|
||||||
|
|
||||||
|
**File:** `frontend/src/constants/rsrp-thresholds.ts`
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
export const SIGNAL_COLORS = {
|
||||||
|
excellent: '#f44336', // Red
|
||||||
|
good: '#ff9800', // Orange
|
||||||
|
fair: '#ffeb3b', // Yellow
|
||||||
|
poor: '#4caf50', // Green
|
||||||
|
weak: '#00bcd4', // Cyan
|
||||||
|
'no-service': '#0d47a1' // Dark Blue
|
||||||
|
} as const;
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## UI IMPROVEMENTS (Priority: HIGH)
|
||||||
|
|
||||||
|
### 5. Dark Theme Support 🌙
|
||||||
|
**Add complete dark mode with toggle**
|
||||||
|
|
||||||
|
**Files to create/update:**
|
||||||
|
- `frontend/src/store/settings.ts` - theme state
|
||||||
|
- `frontend/src/components/ui/ThemeToggle.tsx` - toggle button
|
||||||
|
- `frontend/tailwind.config.js` - dark mode config
|
||||||
|
- `frontend/src/index.css` - dark theme styles
|
||||||
|
|
||||||
|
**Implementation:**
|
||||||
|
|
||||||
|
**A) Tailwind Config:**
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// tailwind.config.js
|
||||||
|
export default {
|
||||||
|
darkMode: 'class', // Enable class-based dark mode
|
||||||
|
content: [
|
||||||
|
"./index.html",
|
||||||
|
"./src/**/*.{js,ts,jsx,tsx}",
|
||||||
|
],
|
||||||
|
theme: {
|
||||||
|
extend: {
|
||||||
|
colors: {
|
||||||
|
// Custom colors for dark mode
|
||||||
|
dark: {
|
||||||
|
bg: '#1a1a1a',
|
||||||
|
surface: '#2d2d2d',
|
||||||
|
border: '#404040',
|
||||||
|
text: '#e0e0e0',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
plugins: [],
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**B) Settings Store:**
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// src/store/settings.ts (create if doesn't exist)
|
||||||
|
import { create } from 'zustand';
|
||||||
|
import { persist } from 'zustand/middleware';
|
||||||
|
|
||||||
|
interface SettingsState {
|
||||||
|
theme: 'light' | 'dark' | 'system';
|
||||||
|
setTheme: (theme: 'light' | 'dark' | 'system') => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useSettingsStore = create<SettingsState>()(
|
||||||
|
persist(
|
||||||
|
(set) => ({
|
||||||
|
theme: 'system',
|
||||||
|
setTheme: (theme) => {
|
||||||
|
set({ theme });
|
||||||
|
applyTheme(theme);
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
{
|
||||||
|
name: 'rfcp-settings',
|
||||||
|
}
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
function applyTheme(theme: 'light' | 'dark' | 'system') {
|
||||||
|
const root = window.document.documentElement;
|
||||||
|
|
||||||
|
if (theme === 'system') {
|
||||||
|
const systemTheme = window.matchMedia('(prefers-color-scheme: dark)').matches
|
||||||
|
? 'dark'
|
||||||
|
: 'light';
|
||||||
|
root.classList.toggle('dark', systemTheme === 'dark');
|
||||||
|
} else {
|
||||||
|
root.classList.toggle('dark', theme === 'dark');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apply theme on page load
|
||||||
|
if (typeof window !== 'undefined') {
|
||||||
|
const stored = localStorage.getItem('rfcp-settings');
|
||||||
|
if (stored) {
|
||||||
|
const { theme } = JSON.parse(stored);
|
||||||
|
applyTheme(theme);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**C) Theme Toggle Component:**
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// src/components/ui/ThemeToggle.tsx
|
||||||
|
import { useSettingsStore } from '@/store/settings';
|
||||||
|
|
||||||
|
export function ThemeToggle() {
|
||||||
|
const { theme, setTheme } = useSettingsStore();
|
||||||
|
|
||||||
|
const icons = {
|
||||||
|
light: '☀️',
|
||||||
|
dark: '🌙',
|
||||||
|
system: '💻'
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex items-center gap-1 bg-gray-100 dark:bg-dark-surface rounded-lg p-1">
|
||||||
|
{(['light', 'dark', 'system'] as const).map((t) => (
|
||||||
|
<button
|
||||||
|
key={t}
|
||||||
|
onClick={() => setTheme(t)}
|
||||||
|
className={`
|
||||||
|
px-3 py-1.5 rounded text-sm transition-all
|
||||||
|
${theme === t
|
||||||
|
? 'bg-white dark:bg-dark-bg shadow-sm'
|
||||||
|
: 'hover:bg-gray-200 dark:hover:bg-dark-border'
|
||||||
|
}
|
||||||
|
`}
|
||||||
|
title={t.charAt(0).toUpperCase() + t.slice(1)}
|
||||||
|
>
|
||||||
|
{icons[t]}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**D) Update App.tsx:**
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Add ThemeToggle to header
|
||||||
|
import { ThemeToggle } from '@/components/ui/ThemeToggle';
|
||||||
|
|
||||||
|
function App() {
|
||||||
|
return (
|
||||||
|
<div className="h-screen flex flex-col bg-white dark:bg-dark-bg">
|
||||||
|
<header className="bg-slate-800 dark:bg-slate-900 text-white px-4 py-3 flex items-center justify-between">
|
||||||
|
<h1 className="text-xl font-bold">RFCP - RF Coverage Planner</h1>
|
||||||
|
<ThemeToggle />
|
||||||
|
</header>
|
||||||
|
{/* rest of app */}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**E) Dark Mode Styles for Components:**
|
||||||
|
|
||||||
|
Update all major components with dark mode classes:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Example pattern:
|
||||||
|
<div className="bg-white dark:bg-dark-surface text-gray-900 dark:text-dark-text">
|
||||||
|
{/* content */}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
// Borders:
|
||||||
|
className="border border-gray-200 dark:border-dark-border"
|
||||||
|
|
||||||
|
// Inputs:
|
||||||
|
className="bg-white dark:bg-dark-bg border-gray-300 dark:border-dark-border"
|
||||||
|
|
||||||
|
// Buttons:
|
||||||
|
className="bg-blue-500 hover:bg-blue-600 dark:bg-blue-600 dark:hover:bg-blue-700"
|
||||||
|
```
|
||||||
|
|
||||||
|
**Apply dark mode to:**
|
||||||
|
- Map panels (SiteForm, SiteList, Coverage Settings)
|
||||||
|
- Legend component
|
||||||
|
- Toast notifications
|
||||||
|
- Modal overlays
|
||||||
|
- Input fields and sliders
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 6. Sites List - Show More Info
|
||||||
|
**Current:** Just site name
|
||||||
|
**New:** Show key parameters inline
|
||||||
|
|
||||||
|
**File:** `frontend/src/components/panels/SiteList.tsx`
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Update site item rendering:
|
||||||
|
<div className="site-item p-3 border-b hover:bg-gray-50 dark:hover:bg-dark-surface">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
{/* Color indicator */}
|
||||||
|
<div
|
||||||
|
className="w-4 h-4 rounded-full border-2 border-white shadow"
|
||||||
|
style={{ backgroundColor: site.color }}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* Site info */}
|
||||||
|
<div>
|
||||||
|
<div className="font-medium text-gray-900 dark:text-dark-text">
|
||||||
|
{site.name}
|
||||||
|
</div>
|
||||||
|
<div className="text-sm text-gray-500 dark:text-gray-400">
|
||||||
|
📻 {site.frequency} MHz • ⚡ {site.power} dBm •
|
||||||
|
📡 {site.antennaType === 'omni' ? 'Omni' : `Sector ${site.azimuth}°`}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Actions */}
|
||||||
|
<div className="flex gap-1">
|
||||||
|
<Button onClick={() => onEdit(site.id)} size="sm" variant="ghost">
|
||||||
|
Edit
|
||||||
|
</Button>
|
||||||
|
<Button onClick={() => onDelete(site.id)} size="sm" variant="ghost">
|
||||||
|
×
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 7. Keyboard Shortcuts
|
||||||
|
**Add useful shortcuts for power users**
|
||||||
|
|
||||||
|
**File:** `frontend/src/App.tsx` or create `src/hooks/useKeyboardShortcuts.ts`
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Create hook:
|
||||||
|
// src/hooks/useKeyboardShortcuts.ts
|
||||||
|
import { useEffect } from 'react';
|
||||||
|
import { useSitesStore } from '@/store/sites';
|
||||||
|
import { useCoverageStore } from '@/store/coverage';
|
||||||
|
import { toast } from '@/components/ui/Toast';
|
||||||
|
|
||||||
|
export function useKeyboardShortcuts() {
|
||||||
|
useEffect(() => {
|
||||||
|
const handleKeyPress = (e: KeyboardEvent) => {
|
||||||
|
// Ignore if typing in input
|
||||||
|
if (e.target instanceof HTMLInputElement ||
|
||||||
|
e.target instanceof HTMLTextAreaElement) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const isMac = navigator.platform.toLowerCase().includes('mac');
|
||||||
|
const modKey = isMac ? e.metaKey : e.ctrlKey;
|
||||||
|
|
||||||
|
if (modKey) {
|
||||||
|
switch(e.key.toLowerCase()) {
|
||||||
|
case 's': // Ctrl/Cmd+S: Save current site
|
||||||
|
e.preventDefault();
|
||||||
|
const selectedSite = useSitesStore.getState().selectedSite;
|
||||||
|
if (selectedSite) {
|
||||||
|
useSitesStore.getState().updateSite(selectedSite.id, selectedSite);
|
||||||
|
toast.success('Site saved');
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'enter': // Ctrl/Cmd+Enter: Calculate coverage
|
||||||
|
e.preventDefault();
|
||||||
|
const sites = useSitesStore.getState().sites;
|
||||||
|
if (sites.length > 0) {
|
||||||
|
useCoverageStore.getState().calculateCoverage(sites);
|
||||||
|
} else {
|
||||||
|
toast.info('Add sites first');
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'n': // Ctrl/Cmd+N: New site (enter placement mode)
|
||||||
|
e.preventDefault();
|
||||||
|
useSitesStore.getState().setPlacementMode(true);
|
||||||
|
toast.info('Click on map to place new site');
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Non-modifier shortcuts
|
||||||
|
switch(e.key) {
|
||||||
|
case 'Escape': // Escape: Cancel/close
|
||||||
|
useSitesStore.getState().setSelectedSite(null);
|
||||||
|
useSitesStore.getState().setPlacementMode(false);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'h': // H: Toggle heatmap
|
||||||
|
useCoverageStore.getState().toggleHeatmap();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
window.addEventListener('keydown', handleKeyPress);
|
||||||
|
return () => window.removeEventListener('keydown', handleKeyPress);
|
||||||
|
}, []);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use in App.tsx:
|
||||||
|
function App() {
|
||||||
|
useKeyboardShortcuts();
|
||||||
|
// ...
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Add keyboard shortcuts help:**
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Add to header or settings:
|
||||||
|
<button
|
||||||
|
onClick={() => setShowShortcuts(true)}
|
||||||
|
className="text-sm text-gray-500 hover:text-gray-700"
|
||||||
|
title="Keyboard shortcuts"
|
||||||
|
>
|
||||||
|
⌨️
|
||||||
|
</button>
|
||||||
|
|
||||||
|
{showShortcuts && (
|
||||||
|
<div className="modal">
|
||||||
|
<h3>Keyboard Shortcuts</h3>
|
||||||
|
<ul>
|
||||||
|
<li><kbd>Ctrl/Cmd</kbd> + <kbd>S</kbd> - Save site</li>
|
||||||
|
<li><kbd>Ctrl/Cmd</kbd> + <kbd>Enter</kbd> - Calculate coverage</li>
|
||||||
|
<li><kbd>Ctrl/Cmd</kbd> + <kbd>N</kbd> - New site</li>
|
||||||
|
<li><kbd>Esc</kbd> - Cancel/Close</li>
|
||||||
|
<li><kbd>H</kbd> - Toggle heatmap</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 8. Map Controls - Fit to Sites & Reset View
|
||||||
|
**Add helpful map navigation buttons**
|
||||||
|
|
||||||
|
**File:** `frontend/src/components/map/Map.tsx`
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Add control buttons:
|
||||||
|
<div className="absolute top-4 right-4 z-[1000] flex flex-col gap-2">
|
||||||
|
{/* Fit to sites */}
|
||||||
|
<button
|
||||||
|
onClick={handleFitToSites}
|
||||||
|
className="
|
||||||
|
bg-white dark:bg-dark-surface shadow-lg rounded px-3 py-2
|
||||||
|
hover:bg-gray-50 dark:hover:bg-dark-border
|
||||||
|
transition-colors
|
||||||
|
"
|
||||||
|
title="Fit view to all sites"
|
||||||
|
disabled={sites.length === 0}
|
||||||
|
>
|
||||||
|
🎯 Fit
|
||||||
|
</button>
|
||||||
|
|
||||||
|
{/* Reset to Ukraine */}
|
||||||
|
<button
|
||||||
|
onClick={handleResetView}
|
||||||
|
className="
|
||||||
|
bg-white dark:bg-dark-surface shadow-lg rounded px-3 py-2
|
||||||
|
hover:bg-gray-50 dark:hover:bg-dark-border
|
||||||
|
transition-colors
|
||||||
|
"
|
||||||
|
title="Reset to Ukraine view"
|
||||||
|
>
|
||||||
|
🏠 Reset
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
// Handlers:
|
||||||
|
const handleFitToSites = () => {
|
||||||
|
if (sites.length === 0) return;
|
||||||
|
|
||||||
|
const bounds = sites.map(site => [site.lat, site.lon] as [number, number]);
|
||||||
|
mapRef.current?.fitBounds(bounds, { padding: [50, 50] });
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleResetView = () => {
|
||||||
|
mapRef.current?.setView([48.4, 35.0], 6); // Ukraine center
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 9. Better Error Handling
|
||||||
|
**Add comprehensive error handling with helpful messages**
|
||||||
|
|
||||||
|
**File:** `frontend/src/store/coverage.ts`
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
calculateCoverage: async (sites: Site[]) => {
|
||||||
|
if (sites.length === 0) {
|
||||||
|
toast.error('No sites to calculate');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
set({ isCalculating: true, error: null });
|
||||||
|
|
||||||
|
try {
|
||||||
|
const settings = get().settings;
|
||||||
|
|
||||||
|
// Validation
|
||||||
|
if (settings.radius > 100) {
|
||||||
|
throw new Error('Radius too large (max 100km)');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (settings.resolution < 50) {
|
||||||
|
throw new Error('Resolution too fine (min 50m)');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Calculate
|
||||||
|
const result = await calculator.calculateCoverage(sites, bounds, settings);
|
||||||
|
|
||||||
|
if (result.points.length === 0) {
|
||||||
|
toast.warning('No coverage points found. Try increasing radius or lowering threshold.');
|
||||||
|
} else {
|
||||||
|
toast.success(`Calculated ${result.points.length.toLocaleString()} points in ${(result.calculationTime / 1000).toFixed(1)}s`);
|
||||||
|
}
|
||||||
|
|
||||||
|
set({
|
||||||
|
coveragePoints: result.points,
|
||||||
|
isCalculating: false
|
||||||
|
});
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Coverage calculation error:', error);
|
||||||
|
|
||||||
|
// User-friendly error messages
|
||||||
|
let message = 'Calculation failed';
|
||||||
|
|
||||||
|
if (error.message.includes('timeout')) {
|
||||||
|
message = 'Calculation timeout. Try reducing radius or increasing resolution.';
|
||||||
|
} else if (error.message.includes('worker')) {
|
||||||
|
message = 'Web Worker error. Please refresh the page.';
|
||||||
|
} else if (error.message.includes('memory')) {
|
||||||
|
message = 'Out of memory. Try smaller radius or coarser resolution.';
|
||||||
|
}
|
||||||
|
|
||||||
|
toast.error(message);
|
||||||
|
set({
|
||||||
|
error: error.message,
|
||||||
|
isCalculating: false
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 10. Mobile Responsiveness Improvements
|
||||||
|
**Ensure panels don't block map on mobile**
|
||||||
|
|
||||||
|
**Files:** `SiteForm.tsx`, `SiteList.tsx`, `Legend.tsx`
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Pattern for mobile-friendly panels:
|
||||||
|
<div className="
|
||||||
|
fixed bottom-0 left-0 right-0 // Mobile: bottom sheet
|
||||||
|
md:static md:w-96 // Desktop: side panel
|
||||||
|
bg-white dark:bg-dark-surface
|
||||||
|
shadow-lg rounded-t-lg md:rounded-lg
|
||||||
|
z-[1000] md:z-auto
|
||||||
|
max-h-[70vh] md:max-h-none // Mobile: don't cover whole screen
|
||||||
|
overflow-y-auto
|
||||||
|
transform transition-transform
|
||||||
|
${isOpen ? 'translate-y-0' : 'translate-y-full md:translate-y-0'}
|
||||||
|
">
|
||||||
|
{/* Panel content */}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
// Add drag handle for mobile:
|
||||||
|
<div className="md:hidden flex justify-center p-2">
|
||||||
|
<div className="w-12 h-1 bg-gray-300 rounded-full" />
|
||||||
|
</div>
|
||||||
|
```
|
||||||
|
|
||||||
|
**Touch-friendly buttons:**
|
||||||
|
```typescript
|
||||||
|
// Minimum 44x44px touch targets
|
||||||
|
<button className="
|
||||||
|
min-w-[44px] min-h-[44px]
|
||||||
|
px-4 py-2
|
||||||
|
text-base md:text-sm
|
||||||
|
">
|
||||||
|
{/* button content */}
|
||||||
|
</button>
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## OPTIONAL ENHANCEMENTS (if time permits)
|
||||||
|
|
||||||
|
### 11. Calculation Stats Display
|
||||||
|
**Show useful statistics after calculation**
|
||||||
|
|
||||||
|
**File:** `frontend/src/components/map/Legend.tsx`
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
{coveragePoints.length > 0 && (
|
||||||
|
<div className="mt-4 p-3 bg-blue-50 dark:bg-blue-900/20 rounded-lg text-sm">
|
||||||
|
<h4 className="font-semibold mb-2">📊 Coverage Statistics</h4>
|
||||||
|
<div className="space-y-1 text-gray-700 dark:text-gray-300">
|
||||||
|
<div>Points: {coveragePoints.length.toLocaleString()}</div>
|
||||||
|
<div>Calculation: {(calculationTime / 1000).toFixed(2)}s</div>
|
||||||
|
<div>Coverage area: ~{calculateArea()} km²</div>
|
||||||
|
<div>Sites: {sites.length}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 12. Templates Verification
|
||||||
|
**Ensure quick templates work correctly**
|
||||||
|
|
||||||
|
**File:** `frontend/src/components/panels/SiteForm.tsx`
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const QUICK_TEMPLATES = {
|
||||||
|
limesdr: {
|
||||||
|
name: 'LimeSDR Mini',
|
||||||
|
power: 20,
|
||||||
|
gain: 2,
|
||||||
|
frequency: 1800,
|
||||||
|
height: 10,
|
||||||
|
antennaType: 'omni' as const
|
||||||
|
},
|
||||||
|
'low-bbu': {
|
||||||
|
name: 'Low Power BBU',
|
||||||
|
power: 40,
|
||||||
|
gain: 8,
|
||||||
|
frequency: 1800,
|
||||||
|
height: 20,
|
||||||
|
antennaType: 'omni' as const
|
||||||
|
},
|
||||||
|
'high-bbu': {
|
||||||
|
name: 'High Power BBU',
|
||||||
|
power: 43,
|
||||||
|
gain: 15,
|
||||||
|
frequency: 1800,
|
||||||
|
height: 30,
|
||||||
|
antennaType: 'sector' as const,
|
||||||
|
azimuth: 0,
|
||||||
|
beamwidth: 65
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const applyTemplate = (templateId: keyof typeof QUICK_TEMPLATES) => {
|
||||||
|
const template = QUICK_TEMPLATES[templateId];
|
||||||
|
|
||||||
|
// Apply ALL fields
|
||||||
|
setFormData({
|
||||||
|
...formData,
|
||||||
|
power: template.power,
|
||||||
|
gain: template.gain,
|
||||||
|
frequency: template.frequency,
|
||||||
|
height: template.height,
|
||||||
|
antennaType: template.antennaType,
|
||||||
|
azimuth: template.azimuth || 0,
|
||||||
|
beamwidth: template.beamwidth || 65
|
||||||
|
});
|
||||||
|
|
||||||
|
toast.success(`Applied: ${template.name}`);
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## TESTING CHECKLIST
|
||||||
|
|
||||||
|
After implementation, verify:
|
||||||
|
|
||||||
|
### Visual Tests:
|
||||||
|
- [ ] Dark theme looks good on all components
|
||||||
|
- [ ] New heatmap gradient is clearly visible (blue=weak, red=strong)
|
||||||
|
- [ ] Legend matches heatmap colors
|
||||||
|
- [ ] Sites list shows frequency, power, antenna type
|
||||||
|
- [ ] Mobile panels don't block map completely
|
||||||
|
|
||||||
|
### Functional Tests:
|
||||||
|
- [ ] "Save & Calculate" button works (saves + calculates)
|
||||||
|
- [ ] "Save Only" button still works
|
||||||
|
- [ ] Coverage radius slider goes to 100km
|
||||||
|
- [ ] Large radius (50-100km) calculates without errors
|
||||||
|
- [ ] Dark/Light/System theme toggle works
|
||||||
|
- [ ] Theme persists after page refresh
|
||||||
|
|
||||||
|
### Keyboard Shortcuts:
|
||||||
|
- [ ] Ctrl/Cmd+S saves current site
|
||||||
|
- [ ] Ctrl/Cmd+Enter calculates coverage
|
||||||
|
- [ ] Ctrl/Cmd+N enters placement mode
|
||||||
|
- [ ] Esc cancels/closes
|
||||||
|
- [ ] H toggles heatmap visibility
|
||||||
|
|
||||||
|
### Mobile Tests (if possible):
|
||||||
|
- [ ] Panels slide up from bottom on mobile
|
||||||
|
- [ ] Touch targets are large enough (44x44px)
|
||||||
|
- [ ] Sliders work with touch
|
||||||
|
- [ ] Map can be panned/zoomed on mobile
|
||||||
|
|
||||||
|
### Error Handling:
|
||||||
|
- [ ] Helpful error messages for calculation failures
|
||||||
|
- [ ] Toast notifications for all actions
|
||||||
|
- [ ] No console errors
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## IMPLEMENTATION NOTES
|
||||||
|
|
||||||
|
1. **Start with visual changes** (colors, legend) - quick wins
|
||||||
|
2. **Then functional changes** (button, radius, dark theme)
|
||||||
|
3. **Then UX improvements** (shortcuts, mobile, errors)
|
||||||
|
4. **Test thoroughly** - especially dark theme across all components
|
||||||
|
|
||||||
|
**Performance Note:**
|
||||||
|
Current calculation is instant because it's only FSPL without terrain data.
|
||||||
|
When Phase 4 (30m terrain) is implemented, large radius will take longer.
|
||||||
|
Performance warnings can be added then.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## FILES TO CREATE/MODIFY
|
||||||
|
|
||||||
|
### New Files:
|
||||||
|
- `frontend/src/store/settings.ts` - theme management
|
||||||
|
- `frontend/src/components/ui/ThemeToggle.tsx` - theme switcher
|
||||||
|
- `frontend/src/hooks/useKeyboardShortcuts.ts` - keyboard shortcuts
|
||||||
|
|
||||||
|
### Modified Files:
|
||||||
|
- `frontend/tailwind.config.js` - dark mode config
|
||||||
|
- `frontend/src/index.css` - dark theme CSS variables
|
||||||
|
- `frontend/src/App.tsx` - add ThemeToggle, use shortcuts hook
|
||||||
|
- `frontend/src/components/map/Heatmap.tsx` - new gradient
|
||||||
|
- `frontend/src/components/map/Legend.tsx` - new colors + stats
|
||||||
|
- `frontend/src/components/map/Map.tsx` - Fit/Reset buttons
|
||||||
|
- `frontend/src/components/panels/SiteForm.tsx` - Save&Calculate, radius
|
||||||
|
- `frontend/src/components/panels/SiteList.tsx` - more info display
|
||||||
|
- `frontend/src/store/coverage.ts` - better error handling, defaults
|
||||||
|
- `frontend/src/constants/rsrp-thresholds.ts` - new color constants
|
||||||
|
- All UI components - add dark mode classes
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## SUCCESS CRITERIA
|
||||||
|
|
||||||
|
✅ New heatmap gradient is clearly visible
|
||||||
|
✅ Dark theme works flawlessly
|
||||||
|
✅ Coverage radius goes to 100km
|
||||||
|
✅ "Save & Calculate" is primary action
|
||||||
|
✅ Keyboard shortcuts work
|
||||||
|
✅ Mobile responsive
|
||||||
|
✅ Better error messages
|
||||||
|
✅ No TypeScript errors
|
||||||
|
✅ All existing features still work
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Estimated Time:** 20-30 minutes for full implementation
|
||||||
|
**Complexity:** Medium (mostly UI updates + new theme system)
|
||||||
|
**Risk:** Low (additive changes, not breaking existing functionality)
|
||||||
|
|
||||||
|
Good luck! 🚀
|
||||||
Reference in New Issue
Block a user