perf: cache aggressively and trim client bundle
Build and Push Docker Images / Build Backend (FastAPI) (push) Successful in 1m1s
Build and Push Docker Images / Build Frontend (Next.js) (push) Successful in 53s
Build and Push Docker Images / Build Pipeline (Meltano + dbt + Airflow) (push) Successful in 2m4s
Build and Push Docker Images / Trigger Portainer Update (push) Successful in 1s
Build and Push Docker Images / Build Backend (FastAPI) (push) Successful in 1m1s
Build and Push Docker Images / Build Frontend (Next.js) (push) Successful in 53s
Build and Push Docker Images / Build Pipeline (Meltano + dbt + Airflow) (push) Successful in 2m4s
Build and Push Docker Images / Trigger Portainer Update (push) Successful in 1s
Frontend - Dynamic-import Chart.js components on detail/compare views so Chart.js no longer ships in initial JS. - Drop force-dynamic on home, compare, rankings so internal data fetches reuse Next.js's per-call revalidate cache. - Switch /school/[slug] to ISR with a 7-day revalidate window (school data updates annually). - Preconnect to analytics + postcodes.io; remove redundant defer on the Umami Script tag (afterInteractive already covers it). - Bump images.minimumCacheTTL to 1 year. - Extract HowItWorks and Editorial sections as server components passed to HomeView via slot props so their JSX stays out of the client bundle. Backend - Add GZipMiddleware (min 512 bytes). - Add CacheAndETagMiddleware: per-path Cache-Control with long s-maxage + stale-while-revalidate, ETag generation, and 304 on If-None-Match. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -7,8 +7,13 @@
|
||||
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
import { useRouter, usePathname, useSearchParams } from 'next/navigation';
|
||||
import dynamic from 'next/dynamic';
|
||||
import { useComparison } from '@/hooks/useComparison';
|
||||
import { ComparisonChart } from './ComparisonChart';
|
||||
|
||||
const ComparisonChart = dynamic(
|
||||
() => import('./ComparisonChart').then((m) => m.ComparisonChart),
|
||||
{ ssr: false },
|
||||
);
|
||||
import { SchoolSearchModal } from './SchoolSearchModal';
|
||||
import { EmptyState } from './EmptyState';
|
||||
import { LoadingSkeleton } from './LoadingSkeleton';
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
// Server component: pure markup, no client state.
|
||||
|
||||
import styles from './HomeView.module.css';
|
||||
|
||||
interface EditorialSectionProps {
|
||||
totalSchools: number | null;
|
||||
localAuthorityCount: number;
|
||||
}
|
||||
|
||||
export function EditorialSection({ totalSchools, localAuthorityCount }: EditorialSectionProps) {
|
||||
return (
|
||||
<section className={styles.editorial}>
|
||||
<div className={styles.editorialGrid}>
|
||||
<div className={styles.editorialText}>
|
||||
<div className={styles.editorialKicker}>About school data</div>
|
||||
<h2 className={styles.editorialHeading}>Making UK school performance data actually readable</h2>
|
||||
<p>
|
||||
School performance data in England is rich but fragmented. The Department for Education publishes
|
||||
Key Stage 2 SATs, GCSE attainment, Ofsted outcomes, progress scores, admissions figures and
|
||||
demographics — each in its own table, each with its own jargon.
|
||||
</p>
|
||||
<p>
|
||||
SchoolCompare brings it all into one place. Every school page shows performance against the national
|
||||
average, explains what the numbers mean, and lets you shortlist schools side by side. Built for
|
||||
parents, governors, journalists, and anyone who wants to understand a school without reading a
|
||||
40-page inspection report.
|
||||
</p>
|
||||
</div>
|
||||
<div className={styles.factbox}>
|
||||
<h3 className={styles.factboxHeading}>Coverage at a glance</h3>
|
||||
<div className={styles.factRow}>
|
||||
<span className={styles.factKey}>Schools covered</span>
|
||||
<span className={styles.factVal}>{totalSchools ? `${totalSchools.toLocaleString()}` : '24,000+'}</span>
|
||||
</div>
|
||||
<div className={styles.factRow}>
|
||||
<span className={styles.factKey}>Local authorities</span>
|
||||
<span className={styles.factVal}>{localAuthorityCount > 0 ? localAuthorityCount : 152}</span>
|
||||
</div>
|
||||
<div className={styles.factRow}>
|
||||
<span className={styles.factKey}>Phases</span>
|
||||
<span className={styles.factVal}>Primary & Secondary</span>
|
||||
</div>
|
||||
<div className={styles.factRow}>
|
||||
<span className={styles.factKey}>Latest results year</span>
|
||||
<span className={styles.factVal}>2024/25</span>
|
||||
</div>
|
||||
<div className={styles.factRow}>
|
||||
<span className={styles.factKey}>Historical data</span>
|
||||
<span className={styles.factVal}>2016–2025</span>
|
||||
</div>
|
||||
<div className={styles.factRow}>
|
||||
<span className={styles.factKey}>Metrics per school</span>
|
||||
<span className={styles.factVal}>40+</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
@@ -23,6 +23,11 @@ interface HomeViewProps {
|
||||
initialSchools: SchoolsResponse;
|
||||
filters: Filters;
|
||||
totalSchools?: number | null;
|
||||
// Slot props for static markup the server pre-renders so it stays out of
|
||||
// the client bundle. Server passes null when the landing sections shouldn't
|
||||
// show (e.g. an active search).
|
||||
howItWorks?: React.ReactNode;
|
||||
editorial?: React.ReactNode;
|
||||
}
|
||||
|
||||
function daysUntil(month: number, day: number): number {
|
||||
@@ -58,7 +63,7 @@ const ADMISSIONS_CHIPS: CountdownChipData[] = [
|
||||
{ type: 'offer', track: 'Secondary · Offer Day', milestone: 'Secondary National Offer Day', month: 3, day: 1 },
|
||||
];
|
||||
|
||||
export function HomeView({ initialSchools, filters, totalSchools }: HomeViewProps) {
|
||||
export function HomeView({ initialSchools, filters, totalSchools, howItWorks, editorial }: HomeViewProps) {
|
||||
const searchParams = useSearchParams();
|
||||
const router = useRouter();
|
||||
const pathname = usePathname();
|
||||
@@ -370,170 +375,9 @@ export function HomeView({ initialSchools, filters, totalSchools }: HomeViewProp
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* How it works — only on landing page */}
|
||||
{!isSearchActive && (
|
||||
<section className={styles.howItWorks}>
|
||||
<div className={styles.hiwHeader}>
|
||||
<h2 className={styles.hiwHeading}>What you'll see on every school</h2>
|
||||
<span className={styles.hiwSub}>Primary or secondary — the page adapts to the phase</span>
|
||||
</div>
|
||||
<div className={styles.hiwGrid}>
|
||||
|
||||
{/* Card 1 — Performance */}
|
||||
<div className={styles.hiwCard}>
|
||||
<div className={styles.hiwVisual}>
|
||||
{/* Primary: mini cascade */}
|
||||
<div className={styles.hiwPhaseBlock}>
|
||||
<div className={styles.hiwPhaseLabel}>Primary · Year 6 · <strong>Key Stage 2 SATs</strong></div>
|
||||
<div className={styles.miniCascade}>
|
||||
{[
|
||||
{ subj: 'Reading', exp: 96, exc: 73, nat: 75 },
|
||||
{ subj: 'Writing', exp: 81, exc: 15, nat: 72 },
|
||||
{ subj: 'Maths', exp: 85, exc: 47, nat: 74 },
|
||||
].map(({ subj, exp, exc, nat }) => (
|
||||
<div key={subj} className={styles.miniCascadeCol}>
|
||||
<div className={styles.miniSubj}>{subj}</div>
|
||||
<div className={styles.miniRowHead}><span>Expected</span><strong>{exp}%</strong></div>
|
||||
<div className={styles.miniTrack}>
|
||||
<div className={styles.miniNatPill} style={{ left: `${nat}%` }}>{nat}%</div>
|
||||
<div className={styles.miniBarExp} style={{ width: `${exp}%` }} />
|
||||
</div>
|
||||
<div className={styles.miniRowHead}><span>Exceeding</span><strong>{exc}%</strong></div>
|
||||
<div className={styles.miniTrack}>
|
||||
<div className={styles.miniBarExc} style={{ width: `${exc}%` }} />
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
{/* Secondary: Attainment 8 */}
|
||||
<div className={styles.hiwPhaseBlock}>
|
||||
<div className={styles.hiwPhaseLabel}>Secondary · Year 11 · <strong>GCSE Attainment 8</strong></div>
|
||||
<div className={styles.att8Row}>
|
||||
<div className={styles.att8BarWrap}>
|
||||
<div className={styles.att8BarHead}><span>This school</span><span>National avg 50.2</span></div>
|
||||
<div className={styles.att8Track}>
|
||||
<div className={styles.att8Fill} style={{ width: '62%' }} />
|
||||
<div className={styles.att8NatLine} style={{ left: '50%' }} />
|
||||
</div>
|
||||
</div>
|
||||
<div className={styles.att8Score}>
|
||||
<div className={styles.att8Value}>62.4</div>
|
||||
<div className={styles.att8Delta}>+12.2 vs national</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className={styles.hiwCardBody}>
|
||||
<div className={styles.hiwStep}>Performance</div>
|
||||
<div className={styles.hiwTitle}>Results against the national average</div>
|
||||
<p className={styles.hiwDesc}>For primary schools, each subject's Expected and Exceeding percentages side by side. For secondary schools, GCSE Attainment 8 with the national benchmark overlaid.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Card 2 — Ofsted */}
|
||||
<div className={styles.hiwCard}>
|
||||
<div className={styles.hiwVisual}>
|
||||
<div className={styles.ofstedPreview}>
|
||||
<div className={styles.ofstedHead}>
|
||||
<span className={styles.ofstedBullet} />
|
||||
<span className={styles.ofstedTitle}>Latest Ofsted inspection</span>
|
||||
</div>
|
||||
<span className={styles.ofstedBadge}>OUTSTANDING</span>
|
||||
<div className={styles.ofstedVerdict}>Rated <em>Outstanding</em> at last inspection.</div>
|
||||
<div className={styles.ofstedMeta}>Full inspection · March 2024</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className={styles.hiwCardBody}>
|
||||
<div className={styles.hiwStep}>Judgement</div>
|
||||
<div className={styles.hiwTitle}>Ofsted at a glance</div>
|
||||
<p className={styles.hiwDesc}>Current grade, inspection date, and a plain-English headline — without opening a 40-page report.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Card 3 — Compare */}
|
||||
<div className={styles.hiwCard}>
|
||||
<div className={styles.hiwVisual}>
|
||||
<div className={styles.comparePreview}>
|
||||
<div className={styles.compareHead}>
|
||||
<div className={`${styles.compareHeadCell} ${styles.compareHeadLabel}`}>Metric</div>
|
||||
<div className={styles.compareHeadCell}>Our Lady<br />Queen of Heaven</div>
|
||||
<div className={styles.compareHeadCell}>St Mary's<br />Catholic Primary</div>
|
||||
</div>
|
||||
{[
|
||||
{ label: 'Reading, Writing & Maths', a: '70%', b: '64%', aHi: true },
|
||||
{ label: 'Ofsted', a: 'Outstanding', b: 'Good', aHi: true },
|
||||
{ label: 'Reading progress', a: '+2.1', b: '+0.4', aHi: true },
|
||||
].map(({ label, a, b, aHi }) => (
|
||||
<div key={label} className={styles.compareRow}>
|
||||
<span className={styles.compareRowLabel}>{label}</span>
|
||||
<span className={`${styles.compareRowVal} ${aHi ? styles.compareRowValHi : ''}`}>{a}</span>
|
||||
<span className={styles.compareRowVal}>{b}</span>
|
||||
</div>
|
||||
))}
|
||||
<div className={styles.compareFoot}>+ pin up to 5 schools</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className={styles.hiwCardBody}>
|
||||
<div className={styles.hiwStep}>Compare</div>
|
||||
<div className={styles.hiwTitle}>Side-by-side shortlists</div>
|
||||
<p className={styles.hiwDesc}>Pin up to five schools and every metric aligns in the same columns — works for primary and secondary alike.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</section>
|
||||
)}
|
||||
|
||||
{/* Editorial — only on landing page */}
|
||||
{!isSearchActive && (
|
||||
<section className={styles.editorial}>
|
||||
<div className={styles.editorialGrid}>
|
||||
<div className={styles.editorialText}>
|
||||
<div className={styles.editorialKicker}>About school data</div>
|
||||
<h2 className={styles.editorialHeading}>Making UK school performance data actually readable</h2>
|
||||
<p>
|
||||
School performance data in England is rich but fragmented. The Department for Education publishes
|
||||
Key Stage 2 SATs, GCSE attainment, Ofsted outcomes, progress scores, admissions figures and
|
||||
demographics — each in its own table, each with its own jargon.
|
||||
</p>
|
||||
<p>
|
||||
SchoolCompare brings it all into one place. Every school page shows performance against the national
|
||||
average, explains what the numbers mean, and lets you shortlist schools side by side. Built for
|
||||
parents, governors, journalists, and anyone who wants to understand a school without reading a
|
||||
40-page inspection report.
|
||||
</p>
|
||||
</div>
|
||||
<div className={styles.factbox}>
|
||||
<h3 className={styles.factboxHeading}>Coverage at a glance</h3>
|
||||
<div className={styles.factRow}>
|
||||
<span className={styles.factKey}>Schools covered</span>
|
||||
<span className={styles.factVal}>{totalSchools ? `${totalSchools.toLocaleString()}` : '24,000+'}</span>
|
||||
</div>
|
||||
<div className={styles.factRow}>
|
||||
<span className={styles.factKey}>Local authorities</span>
|
||||
<span className={styles.factVal}>{filters.local_authorities.length > 0 ? filters.local_authorities.length : 152}</span>
|
||||
</div>
|
||||
<div className={styles.factRow}>
|
||||
<span className={styles.factKey}>Phases</span>
|
||||
<span className={styles.factVal}>Primary & Secondary</span>
|
||||
</div>
|
||||
<div className={styles.factRow}>
|
||||
<span className={styles.factKey}>Latest results year</span>
|
||||
<span className={styles.factVal}>2024/25</span>
|
||||
</div>
|
||||
<div className={styles.factRow}>
|
||||
<span className={styles.factKey}>Historical data</span>
|
||||
<span className={styles.factVal}>2016–2025</span>
|
||||
</div>
|
||||
<div className={styles.factRow}>
|
||||
<span className={styles.factKey}>Metrics per school</span>
|
||||
<span className={styles.factVal}>40+</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
)}
|
||||
{/* How it works + Editorial — server-rendered slots, only on landing */}
|
||||
{!isSearchActive && howItWorks}
|
||||
{!isSearchActive && editorial}
|
||||
|
||||
{/* Results Section */}
|
||||
<section className={`${styles.results} ${resultsView === 'map' && isLocationSearch ? styles.mapViewResults : ''}`}>
|
||||
|
||||
@@ -0,0 +1,120 @@
|
||||
// Server component: pure markup, no client state.
|
||||
// Rendered into HomeView via a slot prop so its JSX doesn't bloat the
|
||||
// HomeView client bundle.
|
||||
|
||||
import styles from './HomeView.module.css';
|
||||
|
||||
export function HowItWorksSection() {
|
||||
const miniCascade = [
|
||||
{ subj: 'Reading', exp: 96, exc: 73, nat: 75 },
|
||||
{ subj: 'Writing', exp: 81, exc: 15, nat: 72 },
|
||||
{ subj: 'Maths', exp: 85, exc: 47, nat: 74 },
|
||||
];
|
||||
const compareRows = [
|
||||
{ label: 'Reading, Writing & Maths', a: '70%', b: '64%', aHi: true },
|
||||
{ label: 'Ofsted', a: 'Outstanding', b: 'Good', aHi: true },
|
||||
{ label: 'Reading progress', a: '+2.1', b: '+0.4', aHi: true },
|
||||
];
|
||||
|
||||
return (
|
||||
<section className={styles.howItWorks}>
|
||||
<div className={styles.hiwHeader}>
|
||||
<h2 className={styles.hiwHeading}>What you'll see on every school</h2>
|
||||
<span className={styles.hiwSub}>Primary or secondary — the page adapts to the phase</span>
|
||||
</div>
|
||||
<div className={styles.hiwGrid}>
|
||||
{/* Card 1 — Performance */}
|
||||
<div className={styles.hiwCard}>
|
||||
<div className={styles.hiwVisual}>
|
||||
<div className={styles.hiwPhaseBlock}>
|
||||
<div className={styles.hiwPhaseLabel}>Primary · Year 6 · <strong>Key Stage 2 SATs</strong></div>
|
||||
<div className={styles.miniCascade}>
|
||||
{miniCascade.map(({ subj, exp, exc, nat }) => (
|
||||
<div key={subj} className={styles.miniCascadeCol}>
|
||||
<div className={styles.miniSubj}>{subj}</div>
|
||||
<div className={styles.miniRowHead}><span>Expected</span><strong>{exp}%</strong></div>
|
||||
<div className={styles.miniTrack}>
|
||||
<div className={styles.miniNatPill} style={{ left: `${nat}%` }}>{nat}%</div>
|
||||
<div className={styles.miniBarExp} style={{ width: `${exp}%` }} />
|
||||
</div>
|
||||
<div className={styles.miniRowHead}><span>Exceeding</span><strong>{exc}%</strong></div>
|
||||
<div className={styles.miniTrack}>
|
||||
<div className={styles.miniBarExc} style={{ width: `${exc}%` }} />
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<div className={styles.hiwPhaseBlock}>
|
||||
<div className={styles.hiwPhaseLabel}>Secondary · Year 11 · <strong>GCSE Attainment 8</strong></div>
|
||||
<div className={styles.att8Row}>
|
||||
<div className={styles.att8BarWrap}>
|
||||
<div className={styles.att8BarHead}><span>This school</span><span>National avg 50.2</span></div>
|
||||
<div className={styles.att8Track}>
|
||||
<div className={styles.att8Fill} style={{ width: '62%' }} />
|
||||
<div className={styles.att8NatLine} style={{ left: '50%' }} />
|
||||
</div>
|
||||
</div>
|
||||
<div className={styles.att8Score}>
|
||||
<div className={styles.att8Value}>62.4</div>
|
||||
<div className={styles.att8Delta}>+12.2 vs national</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className={styles.hiwCardBody}>
|
||||
<div className={styles.hiwStep}>Performance</div>
|
||||
<div className={styles.hiwTitle}>Results against the national average</div>
|
||||
<p className={styles.hiwDesc}>For primary schools, each subject's Expected and Exceeding percentages side by side. For secondary schools, GCSE Attainment 8 with the national benchmark overlaid.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Card 2 — Ofsted */}
|
||||
<div className={styles.hiwCard}>
|
||||
<div className={styles.hiwVisual}>
|
||||
<div className={styles.ofstedPreview}>
|
||||
<div className={styles.ofstedHead}>
|
||||
<span className={styles.ofstedBullet} />
|
||||
<span className={styles.ofstedTitle}>Latest Ofsted inspection</span>
|
||||
</div>
|
||||
<span className={styles.ofstedBadge}>OUTSTANDING</span>
|
||||
<div className={styles.ofstedVerdict}>Rated <em>Outstanding</em> at last inspection.</div>
|
||||
<div className={styles.ofstedMeta}>Full inspection · March 2024</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className={styles.hiwCardBody}>
|
||||
<div className={styles.hiwStep}>Judgement</div>
|
||||
<div className={styles.hiwTitle}>Ofsted at a glance</div>
|
||||
<p className={styles.hiwDesc}>Current grade, inspection date, and a plain-English headline — without opening a 40-page report.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Card 3 — Compare */}
|
||||
<div className={styles.hiwCard}>
|
||||
<div className={styles.hiwVisual}>
|
||||
<div className={styles.comparePreview}>
|
||||
<div className={styles.compareHead}>
|
||||
<div className={`${styles.compareHeadCell} ${styles.compareHeadLabel}`}>Metric</div>
|
||||
<div className={styles.compareHeadCell}>Our Lady<br />Queen of Heaven</div>
|
||||
<div className={styles.compareHeadCell}>St Mary's<br />Catholic Primary</div>
|
||||
</div>
|
||||
{compareRows.map(({ label, a, b, aHi }) => (
|
||||
<div key={label} className={styles.compareRow}>
|
||||
<span className={styles.compareRowLabel}>{label}</span>
|
||||
<span className={`${styles.compareRowVal} ${aHi ? styles.compareRowValHi : ''}`}>{a}</span>
|
||||
<span className={styles.compareRowVal}>{b}</span>
|
||||
</div>
|
||||
))}
|
||||
<div className={styles.compareFoot}>+ pin up to 5 schools</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className={styles.hiwCardBody}>
|
||||
<div className={styles.hiwStep}>Compare</div>
|
||||
<div className={styles.hiwTitle}>Side-by-side shortlists</div>
|
||||
<p className={styles.hiwDesc}>Pin up to five schools and every metric aligns in the same columns — works for primary and secondary alike.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
@@ -7,8 +7,8 @@
|
||||
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import dynamic from 'next/dynamic';
|
||||
import { useComparison } from '@/hooks/useComparison';
|
||||
import { PerformanceChart } from './PerformanceChart';
|
||||
import { SchoolMap } from './SchoolMap';
|
||||
import { MetricTooltip } from './MetricTooltip';
|
||||
import type {
|
||||
@@ -22,7 +22,12 @@ import {
|
||||
buildOfstedHeroChip,
|
||||
} from '@/lib/utils';
|
||||
import { DeltaChip } from './DeltaChip';
|
||||
import SatsChart from './SatsChart';
|
||||
|
||||
const PerformanceChart = dynamic(
|
||||
() => import('./PerformanceChart').then((m) => m.PerformanceChart),
|
||||
{ ssr: false },
|
||||
);
|
||||
const SatsChart = dynamic(() => import('./SatsChart'), { ssr: false });
|
||||
import { track, getNavigationSource } from '@/lib/analytics';
|
||||
import styles from './SchoolDetailView.module.css';
|
||||
|
||||
|
||||
@@ -8,10 +8,15 @@
|
||||
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import dynamic from 'next/dynamic';
|
||||
import { useComparison } from '@/hooks/useComparison';
|
||||
import { PerformanceChart } from './PerformanceChart';
|
||||
import { MetricTooltip } from './MetricTooltip';
|
||||
import { SchoolMap } from './SchoolMap';
|
||||
|
||||
const PerformanceChart = dynamic(
|
||||
() => import('./PerformanceChart').then((m) => m.PerformanceChart),
|
||||
{ ssr: false },
|
||||
);
|
||||
import type {
|
||||
School, SchoolResult, AbsenceData,
|
||||
OfstedInspection, OfstedParentView, SchoolCensus,
|
||||
|
||||
Reference in New Issue
Block a user