Beta 9
More 'Home' Physics More FAQs More Text
This commit is contained in:
+130
-86
@@ -27,7 +27,7 @@ import { vaporPressureHpa, solarElevationDeg, calcTmrt, utciApprox, calcConcrete
|
||||
import {
|
||||
utciCategory, precipPenalty, windCompass8, uvSplit,
|
||||
SKIN_TYPES, sunburnMinutes, burnLabel,
|
||||
VEHICLE_TYPES,
|
||||
VEHICLE_TYPES, BUILDING_TYPES,
|
||||
cloudCategory, confidenceBand, moonGlyph,
|
||||
} from './utils.js';
|
||||
import { SkyScope, WindVane, CloudIcon, ScopeReticle, PrecipIcon } from './components.js';
|
||||
@@ -113,7 +113,7 @@ export function UTCIForecast() {
|
||||
// FLIP THE `false` BELOW TO `true` TO PREVIEW THE PRO EXPERIENCE.
|
||||
// When this is wired to real billing/auth, replace `useState(false)`
|
||||
// with a check against the logged-in user.
|
||||
const [isPro, setIsPro] = useState(true);
|
||||
const [isPro, setIsPro] = useState(false);
|
||||
|
||||
// How many days the free tier shows. Days beyond this get a 🔒.
|
||||
// Bump this number if you want to give free users more access.
|
||||
@@ -178,6 +178,16 @@ export function UTCIForecast() {
|
||||
|
||||
// Vehicle type for the cabin heat column. 'car' is the default preset.
|
||||
const [vehicleType, setVehicleType] = useState('car');
|
||||
|
||||
// Vehicle ventilation — true = windows open (high convective loss).
|
||||
const [vehicleVent, setVehicleVent] = useState(false);
|
||||
|
||||
// Indoor: buildingType drives the physics preset; indoorManaged toggles the
|
||||
// curtains+ventilation model on top. indoorMode gates column visibility:
|
||||
// 'off' = hidden, 'on' = indoorT shown (managed or not depending on indoorManaged).
|
||||
const [buildingType, setBuildingType] = useState('brick');
|
||||
const [indoorManaged, setIndoorManaged] = useState(false);
|
||||
const [indoorMode, setIndoorMode] = useState('off');
|
||||
const searchTimeout = useRef(null);
|
||||
|
||||
// Refs for the two-scroller table layout (sticky-to-viewport header +
|
||||
@@ -579,7 +589,7 @@ export function UTCIForecast() {
|
||||
// dt kept for SkyScope / backward compat — same as dtUTC.
|
||||
const dt = dtUTC;
|
||||
const elev = solarElevationDeg(location.lat, location.lon, dtUTC);
|
||||
const vehicleT = calcVehicleInteriorTemp(Ta, glob, elev, vehicleType);
|
||||
const vehicleT = calcVehicleInteriorTemp(Ta, glob, elev, vehicleType, vehicleVent);
|
||||
const eh = vaporPressureHpa(Ta, RH);
|
||||
const Tmrt = calcTmrt(Ta, dir, dif, glob, elev);
|
||||
const utci = utciApprox(Ta, Tmrt, va, eh);
|
||||
@@ -605,8 +615,8 @@ export function UTCIForecast() {
|
||||
const TaArr = hourlyRows.map(r => r.Ta);
|
||||
const globArr = hourlyRows.map(r => r.glob);
|
||||
const elevArr = hourlyRows.map(r => r.elev);
|
||||
const indoorTemps = calcIndoorTempPass(TaArr, globArr, elevArr);
|
||||
const managedTemps = calcManagedIndoorTempPass(TaArr, globArr, elevArr);
|
||||
const indoorTemps = calcIndoorTempPass(TaArr, globArr, elevArr, buildingType);
|
||||
const managedTemps = calcManagedIndoorTempPass(TaArr, globArr, elevArr, buildingType);
|
||||
hourlyRows.forEach((r, i) => { r.indoorT = indoorTemps[i]; r.managedT = managedTemps[i]; });
|
||||
}
|
||||
|
||||
@@ -957,19 +967,48 @@ export function UTCIForecast() {
|
||||
}
|
||||
setActiveProfile(key);
|
||||
if (key !== 'custom') setVisibleCols({ ...profile.cols });
|
||||
// Auto-enable indoor column for profiles that include it;
|
||||
// reset to off for profiles that don't.
|
||||
if (key !== 'custom') {
|
||||
const hasIndoor = profile.cols['indoorT'] || profile.cols['managedT'];
|
||||
setIndoorMode(hasIndoor ? 'on' : 'off');
|
||||
setIndoorManaged(false);
|
||||
}
|
||||
}}
|
||||
>${profile.icon} ${profile.label}${locked ? ' 🔒' : ''}</button>`;
|
||||
})}
|
||||
</div>
|
||||
|
||||
<!--
|
||||
COLUMN TOGGLES — Pro only. Hidden entirely for free users
|
||||
(the profile selector above provides enough control for them).
|
||||
COLUMN TOGGLES — in exact table column order.
|
||||
Buttons visible to all users when profile includes that col.
|
||||
Burn + Vehicle dropdowns always shown (free + pro).
|
||||
Pro users see all toggles; free users see profile-filtered subset.
|
||||
-->
|
||||
${(FILTER_PROFILES[activeProfile]?.cols['burn'] || FILTER_PROFILES[activeProfile]?.cols['vehicleT']) && html`
|
||||
${(isPro || FILTER_PROFILES[activeProfile]?.cols['burn'] || FILTER_PROFILES[activeProfile]?.cols['vehicleT'] || FILTER_PROFILES[activeProfile]?.cols['indoorT'] || FILTER_PROFILES[activeProfile]?.cols['managedT']) && html`
|
||||
<div class="col-toggles">
|
||||
<span class="col-toggles-label">Columns:</span>
|
||||
${FILTER_PROFILES[activeProfile]?.cols['burn'] && html`
|
||||
|
||||
${isPro && (activeProfile === 'custom' || FILTER_PROFILES[activeProfile].cols['air']) && html`<button class=${`col-toggle${visibleCols.air ? ' on' : ''}`} onClick=${() => toggleCol('air')}>Air</button>`}
|
||||
${isPro && (activeProfile === 'custom' || FILTER_PROFILES[activeProfile].cols['rh']) && html`<button class=${`col-toggle${visibleCols.rh ? ' on' : ''}`} onClick=${() => toggleCol('rh')}>RH</button>`}
|
||||
${isPro && (activeProfile === 'custom' || FILTER_PROFILES[activeProfile].cols['dew']) && html`<button class=${`col-toggle${visibleCols.dew ? ' on' : ''}`} onClick=${() => toggleCol('dew')}>Dew</button>`}
|
||||
${isPro && (activeProfile === 'custom' || FILTER_PROFILES[activeProfile].cols['soilT']) && html`<button class=${`col-toggle${visibleCols.soilT ? ' on' : ''}`} onClick=${() => toggleCol('soilT')}>Soil °C</button>`}
|
||||
${isPro && (activeProfile === 'custom' || FILTER_PROFILES[activeProfile].cols['soilT6']) && html`<button class=${`col-toggle${visibleCols.soilT6 ? ' on' : ''}`} onClick=${() => toggleCol('soilT6')}>Soil 6cm</button>`}
|
||||
${isPro && (activeProfile === 'custom' || FILTER_PROFILES[activeProfile].cols['soilM']) && html`<button class=${`col-toggle${visibleCols.soilM ? ' on' : ''}`} onClick=${() => toggleCol('soilM')}>Soil moist</button>`}
|
||||
${isPro && (activeProfile === 'custom' || FILTER_PROFILES[activeProfile].cols['concreteT']) && html`<button class=${`col-toggle${visibleCols.concreteT ? ' on' : ''}`} onClick=${() => toggleCol('concreteT')}>Concrete</button>`}
|
||||
${isPro && (activeProfile === 'custom' || FILTER_PROFILES[activeProfile].cols['wind']) && html`<button class=${`col-toggle${visibleCols.wind ? ' on' : ''}`} onClick=${() => toggleCol('wind')}>Wind</button>`}
|
||||
${isPro && (activeProfile === 'custom' || FILTER_PROFILES[activeProfile].cols['dir']) && html`<button class=${`col-toggle${visibleCols.dir ? ' on' : ''}`} onClick=${() => toggleCol('dir')}>Dir</button>`}
|
||||
${isPro && (activeProfile === 'custom' || FILTER_PROFILES[activeProfile].cols['cloud']) && html`<button class=${`col-toggle${visibleCols.cloud ? ' on' : ''}`} onClick=${() => toggleCol('cloud')}>Cloud</button>`}
|
||||
${isPro && (activeProfile === 'custom' || FILTER_PROFILES[activeProfile].cols['sun']) && html`<button class=${`col-toggle${visibleCols.sun ? ' on' : ''}`} onClick=${() => toggleCol('sun')}>Sun</button>`}
|
||||
${isPro && (activeProfile === 'custom' || FILTER_PROFILES[activeProfile].cols['direct']) && html`<button class=${`col-toggle${visibleCols.direct ? ' on' : ''}`} onClick=${() => toggleCol('direct')}>Direct</button>`}
|
||||
${isPro && (activeProfile === 'custom' || FILTER_PROFILES[activeProfile].cols['diffuse']) && html`<button class=${`col-toggle${visibleCols.diffuse ? ' on' : ''}`} onClick=${() => toggleCol('diffuse')}>Diffuse</button>`}
|
||||
${isPro && (activeProfile === 'custom' || FILTER_PROFILES[activeProfile].cols['tmrt']) && html`<button class=${`col-toggle${visibleCols.tmrt ? ' on' : ''}`} onClick=${() => toggleCol('tmrt')}>Tmrt</button>`}
|
||||
${isPro && (activeProfile === 'custom' || FILTER_PROFILES[activeProfile].cols['delta']) && html`<button class=${`col-toggle${visibleCols.delta ? ' on' : ''}`} onClick=${() => toggleCol('delta')}>Δ</button>`}
|
||||
${isPro && (activeProfile === 'custom' || FILTER_PROFILES[activeProfile].cols['utci']) && html`<button class=${`col-toggle${visibleCols.utci ? ' on' : ''}`} onClick=${() => toggleCol('utci')}>UTCI</button>`}
|
||||
${isPro && (activeProfile === 'custom' || FILTER_PROFILES[activeProfile].cols['uvA']) && html`<button class=${`col-toggle${visibleCols.uvA ? ' on' : ''}`} onClick=${() => toggleCol('uvA')}>UV-A</button>`}
|
||||
${isPro && (activeProfile === 'custom' || FILTER_PROFILES[activeProfile].cols['uvB']) && html`<button class=${`col-toggle${visibleCols.uvB ? ' on' : ''}`} onClick=${() => toggleCol('uvB')}>UV-B</button>`}
|
||||
|
||||
${(activeProfile === 'custom' || FILTER_PROFILES[activeProfile]?.cols['burn']) && html`
|
||||
<select
|
||||
class=${`col-toggle burn-select${visibleCols.burn ? ' on' : ''}`}
|
||||
value=${visibleCols.burn ? skinType : 'off'}
|
||||
@@ -985,63 +1024,72 @@ export function UTCIForecast() {
|
||||
${Object.entries(SKIN_TYPES).map(([k, v]) => html`
|
||||
<option key=${k} value=${k}>${v.name.split(' · ')[1]} skin</option>`)}
|
||||
</select>`}
|
||||
${FILTER_PROFILES[activeProfile]?.cols['vehicleT'] && html`
|
||||
<select
|
||||
class=${`col-toggle burn-select${visibleCols.vehicleT ? ' on' : ''}`}
|
||||
value=${visibleCols.vehicleT ? vehicleType : 'off'}
|
||||
onChange=${(e) => {
|
||||
if (e.target.value === 'off') {
|
||||
setVisibleCols(prev => ({ ...prev, vehicleT: false }));
|
||||
} else {
|
||||
setVehicleType(e.target.value);
|
||||
setVisibleCols(prev => ({ ...prev, vehicleT: true }));
|
||||
}
|
||||
}}>
|
||||
<option value="off">${visibleCols.vehicleT ? 'Hide Vehicle' : 'Vehicle'}</option>
|
||||
${Object.entries(VEHICLE_TYPES).map(([k, v]) => html`
|
||||
<option key=${k} value=${k}>${v.name}</option>`)}
|
||||
</select>`}
|
||||
</div>`}
|
||||
|
||||
${isPro && html`
|
||||
<div class="col-toggles">
|
||||
<span class="col-toggles-label">Columns:</span>
|
||||
${[
|
||||
// Hour and UTCI+P are always-on — no toggle button for them.
|
||||
{ key: 'air', label: 'Air' },
|
||||
{ key: 'rh', label: 'RH' },
|
||||
{ key: 'dew', label: 'Dew' },
|
||||
{ key: 'soilT', label: 'Soil °C' },
|
||||
{ key: 'soilT6', label: 'Soil 6cm' },
|
||||
{ key: 'soilM', label: 'Soil moist' },
|
||||
{ key: 'concreteT', label: 'Concrete' },
|
||||
{ key: 'indoorT', label: 'Indoors' },
|
||||
{ key: 'managedT', label: 'Managed' },
|
||||
{ key: 'wind', label: 'Wind' },
|
||||
{ key: 'dir', label: 'Dir' },
|
||||
{ key: 'cloud', label: 'Cloud' },
|
||||
{ key: 'sun', label: 'Sun' },
|
||||
{ key: 'direct', label: 'Direct' },
|
||||
{ key: 'diffuse', label: 'Diffuse' },
|
||||
{ key: 'tmrt', label: 'Tmrt' },
|
||||
{ key: 'delta', label: 'Δ' },
|
||||
{ key: 'utci', label: 'UTCI' },
|
||||
{ key: 'uvA', label: 'UV-A' },
|
||||
{ key: 'uvB', label: 'UV-B' },
|
||||
].filter(c =>
|
||||
activeProfile === 'custom' || !!FILTER_PROFILES[activeProfile].cols[c.key]
|
||||
).map(c => html`
|
||||
<button
|
||||
key=${c.key}
|
||||
class=${`col-toggle${visibleCols[c.key] ? ' on' : ''}`}
|
||||
onClick=${() => toggleCol(c.key)}
|
||||
>${c.label}</button>`)}
|
||||
${(activeProfile === 'custom' || FILTER_PROFILES[activeProfile]?.cols['vehicleT']) && html`
|
||||
<span class="col-toggle-group">
|
||||
<select
|
||||
class=${`col-toggle burn-select grouped-left${visibleCols.vehicleT ? ' on' : ''}`}
|
||||
value=${visibleCols.vehicleT ? vehicleType : 'off'}
|
||||
onChange=${(e) => {
|
||||
if (e.target.value === 'off') {
|
||||
setVisibleCols(prev => ({ ...prev, vehicleT: false }));
|
||||
setVehicleVent(false);
|
||||
} else {
|
||||
setVehicleType(e.target.value);
|
||||
setVisibleCols(prev => ({ ...prev, vehicleT: true }));
|
||||
}
|
||||
}}>
|
||||
<option value="off">${visibleCols.vehicleT ? 'Hide Vehicle' : 'Vehicle'}</option>
|
||||
${Object.entries(VEHICLE_TYPES).map(([k, v]) => html`
|
||||
<option key=${k} value=${k}>${v.name}</option>`)}
|
||||
</select>
|
||||
${visibleCols.vehicleT && html`
|
||||
<label
|
||||
class=${`col-toggle-vent grouped-right${vehicleVent ? ' on' : ''}`}
|
||||
title="Ventilation — open windows significantly reduce cabin heat build-up">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked=${vehicleVent}
|
||||
onChange=${() => setVehicleVent(v => !v)}
|
||||
style=${{ position: 'absolute', opacity: 0, width: 0, height: 0 }} />
|
||||
<span class="vent-checkbox${vehicleVent ? ' checked' : ''}"></span>
|
||||
Ventilation
|
||||
</label>`}
|
||||
</span>`}
|
||||
|
||||
${(activeProfile === 'custom' || FILTER_PROFILES[activeProfile].cols['indoorT'] || FILTER_PROFILES[activeProfile].cols['managedT']) && html`
|
||||
<span class="col-toggle-group">
|
||||
<select
|
||||
class=${`col-toggle burn-select grouped-left${indoorMode === 'on' ? ' on' : ''}`}
|
||||
value=${indoorMode === 'on' ? buildingType : 'off'}
|
||||
onChange=${(e) => {
|
||||
if (e.target.value === 'off') {
|
||||
setIndoorMode('off');
|
||||
setIndoorManaged(false);
|
||||
} else {
|
||||
setBuildingType(e.target.value);
|
||||
setIndoorMode('on');
|
||||
}
|
||||
}}>
|
||||
<option value="off">${indoorMode === 'on' ? 'Hide Indoors' : 'Indoors'}</option>
|
||||
${Object.entries(BUILDING_TYPES).map(([k, v]) => html`
|
||||
<option key=${k} value=${k}>${v.name}</option>`)}
|
||||
</select>
|
||||
${indoorMode === 'on' && html`
|
||||
<label
|
||||
class=${`col-toggle-vent grouped-right${indoorManaged ? ' on' : ''}`}
|
||||
title="Managed: curtains closed by day, windows open when cooler outside">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked=${indoorManaged}
|
||||
onChange=${() => setIndoorManaged(v => !v)}
|
||||
style=${{ position: 'absolute', opacity: 0, width: 0, height: 0 }} />
|
||||
<span class="vent-checkbox${indoorManaged ? ' checked' : ''}"></span>
|
||||
Managed
|
||||
</label>`}
|
||||
</span>`}
|
||||
${isPro && (activeProfile === 'custom' || FILTER_PROFILES[activeProfile].cols['precip']) && html`<button class=${`col-toggle${visibleCols.precip ? ' on' : ''}`} onClick=${() => toggleCol('precip')}>Precip</button>`}
|
||||
|
||||
${(activeProfile === 'custom' || FILTER_PROFILES[activeProfile].cols['precip']) && html`
|
||||
<button
|
||||
class=${`col-toggle${visibleCols.precip ? ' on' : ''}`}
|
||||
onClick=${() => toggleCol('precip')}
|
||||
>Precip</button>`}
|
||||
</div>`}
|
||||
|
||||
<!--
|
||||
@@ -1084,8 +1132,8 @@ export function UTCIForecast() {
|
||||
${visibleCols.uvB && html`<th class="col-info-th" onClick=${(e) => handleThClick('uvB', e)}>UV-B <span class="col-unit">est. idx</span></th>`}
|
||||
${visibleCols.burn && html`<th class="col-info-th" onClick=${(e) => handleThClick('burn', e)}>Burn <span class="col-unit">to MED</span></th>`}
|
||||
${visibleCols.vehicleT && html`<th class="col-info-th" onClick=${(e) => handleThClick('vehicleT', e)}>Vehicle <span class="col-unit">°C peak</span></th>`}
|
||||
${visibleCols.indoorT && html`<th class="col-info-th" onClick=${(e) => handleThClick('indoorT', e)}>Indoors <span class="col-unit">°C est.</span></th>`}
|
||||
${visibleCols.managedT && html`<th class="col-info-th" onClick=${(e) => handleThClick('managedT', e)}>Managed <span class="col-unit">°C est.</span></th>`}
|
||||
${indoorMode === 'on' && !indoorManaged && html`<th class="col-info-th" onClick=${(e) => handleThClick('indoorT', e)}>Indoors <span class="col-unit">°C est.</span></th>`}
|
||||
${indoorMode === 'on' && indoorManaged && html`<th class="col-info-th" onClick=${(e) => handleThClick('managedT', e)}>Managed <span class="col-unit">°C est.</span></th>`}
|
||||
${visibleCols.precip && html`<th class="utci-tight-head col-info-th" onClick=${(e) => handleThClick('precip', e)}>Pcpt <span class="col-unit">mm/h</span></th>`}
|
||||
${visibleCols.utciP && html`<th class="col-info-th" onClick=${(e) => handleThClick('utciP', e)}>UTCI+P <span class="col-unit">°C adj.</span></th>`}
|
||||
</tr>
|
||||
@@ -1193,11 +1241,11 @@ export function UTCIForecast() {
|
||||
<td style=${{ color: r.vehicleT != null && r.vehicleT > 45 ? '#c0392b' : r.vehicleT != null && r.vehicleT > 35 ? '#e67e22' : '#7f8c8d', fontWeight: 'bold', background: tempBg(r.vehicleT) }}>
|
||||
${r.vehicleT != null ? r.vehicleT.toFixed(1) : '—'}
|
||||
</td>`}
|
||||
${visibleCols.indoorT && html`
|
||||
${indoorMode === 'on' && !indoorManaged && html`
|
||||
<td style=${{ color: r.indoorT != null && r.indoorT > 32 ? '#c0392b' : r.indoorT != null && r.indoorT > 26 ? '#e67e22' : '#4a7a4a', fontWeight: 'bold', background: tempBg(r.indoorT) }}>
|
||||
${r.indoorT != null ? r.indoorT.toFixed(1) : '—'}
|
||||
</td>`}
|
||||
${visibleCols.managedT && html`
|
||||
${indoorMode === 'on' && indoorManaged && html`
|
||||
<td style=${{ color: r.managedT != null && r.managedT > 32 ? '#c0392b' : r.managedT != null && r.managedT > 26 ? '#e67e22' : '#4a7a4a', fontWeight: 'bold', background: tempBg(r.managedT) }}>
|
||||
${r.managedT != null ? r.managedT.toFixed(1) : '—'}
|
||||
</td>`}
|
||||
@@ -1264,32 +1312,28 @@ export function UTCIForecast() {
|
||||
<div class="utci-about">
|
||||
<h2 class="utci-about-heading">What is SunScope?</h2>
|
||||
<p class="utci-about-text">
|
||||
SunScope shows how the weather will actually <em>feel</em> on your body — not just the air
|
||||
temperature. It uses the <strong>Universal Thermal Climate Index (UTCI)</strong>, a
|
||||
peer-reviewed biometeorological standard developed by Bröde et al. (2012) that combines
|
||||
air temperature, humidity, wind speed, and solar radiation into a single <em>felt
|
||||
temperature</em>. On a calm, sunny winter day UTCI can read several degrees warmer than
|
||||
the thermometer; on a grey, blustery day it can read far colder. Forecast data is
|
||||
sourced in real time from <strong>Open-Meteo</strong>, a free and open-source weather API,
|
||||
and solar radiation is used to calculate Mean Radiant Temperature — the heat your skin
|
||||
absorbs from the sun — making SunScope one of the most complete outdoor comfort forecasts
|
||||
available for free.
|
||||
SunScope is a free hourly weather forecast built around <strong>felt temperature</strong>,
|
||||
not just air temperature. It uses the <strong>Universal Thermal Climate Index (UTCI)</strong>
|
||||
— the biometeorological standard used in heat-health warning systems worldwide — to combine
|
||||
air temperature, humidity, wind, and solar radiation into a single honest number. The
|
||||
<strong>UTCI+P</strong> column adds an original rain and snow penalty so wet, windy days
|
||||
read as cold as they feel.
|
||||
</p>
|
||||
<p class="utci-about-text">
|
||||
The <strong>UTCI+P</strong> column adds the <strong>SunScope soak-factor</strong>: an
|
||||
original precipitation penalty that accounts for the extra chill of rain and snow on
|
||||
exposed skin and wet clothing. Light drizzle reduces the felt temperature by around
|
||||
1–2 °C; heavy rain combined with wind can push it down by 7–8 °C. Snow carries an
|
||||
additional penalty on top. The result is an honest, real-world comfort score for any
|
||||
location worldwide — simply search for your town or city and compare the hourly
|
||||
forecast across the next 3 days (and up to 14 days with SunScope Extra).
|
||||
Beyond felt temperature, SunScope calculates <strong>vehicle cabin heat</strong> (choose
|
||||
your vehicle type; toggle windows open), <strong>indoor temperature</strong> (seven
|
||||
building types; managed heatwave mode), <strong>urban concrete surface temperature</strong>,
|
||||
<strong>UV index and sunburn time</strong> by skin type, and <strong>soil temperature
|
||||
and moisture</strong> for farming and motorhome use. Switch profiles to see the data
|
||||
that matters for your situation — or go Custom and build your own view.
|
||||
<a href="./about.html" class="utci-about-link">Learn more →</a>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="utci-footer">
|
||||
<em>Reading the table.</em> A large positive Δ means your body is absorbing
|
||||
far more heat than the air temperature alone suggests — typically due to direct solar radiation.
|
||||
On clear sunny days this gap can exceed 10°C even at modest air temperatures.
|
||||
On clear sunny days this gap can exceed 10 °C even at modest air temperatures.
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user