diff --git a/GanjooRazor/Areas/Admin/Pages/ReviewEdits.cshtml b/GanjooRazor/Areas/Admin/Pages/ReviewEdits.cshtml index f928da7c..4d837bcd 100644 --- a/GanjooRazor/Areas/Admin/Pages/ReviewEdits.cshtml +++ b/GanjooRazor/Areas/Admin/Pages/ReviewEdits.cshtml @@ -12,6 +12,10 @@

@ViewData["Title"]

@if (geoTag.LocationId == null) {
@@ -946,7 +955,7 @@ else
- +
@@ -958,7 +967,7 @@ else
- +
diff --git a/GanjooRazor/Areas/Admin/Pages/ReviewEdits.cshtml.cs b/GanjooRazor/Areas/Admin/Pages/ReviewEdits.cshtml.cs index 1a8a4f21..22d23976 100644 --- a/GanjooRazor/Areas/Admin/Pages/ReviewEdits.cshtml.cs +++ b/GanjooRazor/Areas/Admin/Pages/ReviewEdits.cshtml.cs @@ -68,6 +68,17 @@ namespace GanjooRazor.Areas.Admin.Pages /// public List Locations { get; set; } + /// + /// camelCase JSON of Locations (id/name/latitude/longitude only), used client-side for the + /// search-as-you-type location picker on each geo tag (see setupLocationAutocomplete in bk.js) - + /// same shape/purpose as Editor.cshtml.cs's AllLocationsJson + /// + public string AllLocationsJson => + JsonConvert.SerializeObject( + (Locations ?? new List()).Select(l => new { l.Id, l.Name, l.Latitude, l.Longitude }), + new JsonSerializerSettings { ContractResolver = new Newtonsoft.Json.Serialization.CamelCasePropertyNamesContractResolver() } + ); + /// /// groups verses into couplets - same logic as SuggestQuoted.cshtml.cs's/Editor.cshtml.cs's GetCouplets, /// duplicated here rather than shared, so this page doesn't take on a cross-file dependency on those diff --git a/GanjooRazor/Areas/User/Pages/Editor.cshtml b/GanjooRazor/Areas/User/Pages/Editor.cshtml index 7a3228e6..db506606 100644 --- a/GanjooRazor/Areas/User/Pages/Editor.cshtml +++ b/GanjooRazor/Areas/User/Pages/Editor.cshtml @@ -126,6 +126,42 @@ var allGeoLocations = @Html.Raw(Model.AllLocationsJson); $(document).ready(function () { renderPendingGeoDateTags(); + setupLocationAutocomplete( + document.getElementById('geotag-location-search'), + document.getElementById('geotag-location-select'), + document.getElementById('geotag-location-results'), + allGeoLocations, + function (loc) { + // picking an existing location (or clearing the field) means whatever was + // typed into the "new location" panel no longer applies to this suggestion + var widget = document.getElementById('new-location-widget'); + if (loc != null && widget.style.display !== 'none') { + toggleNewLocationWidget(); + } + }, + function (typedText) { + var widget = document.getElementById('new-location-widget'); + if (widget.style.display === 'none' || widget.style.display === '') { + toggleNewLocationWidget(); + } + document.getElementById('geotag-new-location-name').value = typedText; + } + ); + var directAddLocationSearch = document.getElementById('location-search'); + if (directAddLocationSearch != null) { + // admin-only direct-add row (bypasses the suggestion/moderation flow entirely) - + // no "new location" concept here, so an unmatched search just has nothing to pick + setupLocationAutocomplete( + directAddLocationSearch, + document.getElementById('location'), + document.getElementById('location-results'), + allGeoLocations, + null, + function (typedText) { + upToast('این مکان در فهرست نیست. برای افزودن مکان جدید از «مکان‌ها» در پایین این جدول استفاده کنید.', 'error'); + } + ); + } }); var geoDateTagMap = null; var geoDateTagMarker = null; @@ -268,7 +304,7 @@ var s = geoDateTagSuggestions[i]; var status = s.markForDelete ? '🗑️ درخواست حذف' : '➕ پیشنهاد جدید'; var coupletText = (s.coupletIndex != null && geoDateTagCoupletTexts[s.coupletIndex]) ? geoDateTagCoupletTexts[s.coupletIndex] : '— کل شعر —'; - var locationText = s.markForDelete ? '' : (s.locationId ? $('#geotag-location-select option[value="' + s.locationId + '"]').text() : (s.suggestedLocationName || '')); + var locationText = s.markForDelete ? '' : (s.locationId ? ((allGeoLocations.filter(function (l) { return l.id === s.locationId; })[0] || {}).name || ('مکان #' + s.locationId)) : (s.suggestedLocationName || '')); html += '' + status + '' + coupletText + '' + locationText + '' + (s.lunarYear || '') + '' + (s.lunarMonth || '') + '' + (s.lunarDay || '') + '' + '✕ حذف از فهرست'; } @@ -354,6 +390,8 @@ function resetGeoDateTagForm() { $('#geotag-couplet-select').val(''); $('#geotag-location-select').val(''); + $('#geotag-location-search').val(''); + $('#geotag-location-results').hide().empty(); $('#geotag-new-location-name').val(''); $('#geotag-new-location-lat').val(''); $('#geotag-new-location-lng').val(''); @@ -2001,14 +2039,13 @@ else
- - + +
+ + + +
+

شروع به تایپ کنید تا فهرست مکان‌های موجود بر اساس نام فیلتر شود؛ اگر مکان مورد نظر پیدا نشد، گزینهٔ تعریف آن به‌عنوان مکان جدید نمایش داده می‌شود.

📍 مکان جدید پیشنهاد بده
@@ -2203,14 +2240,11 @@ else } - +
+ + + +
diff --git a/GanjooRazor/Areas/User/Pages/Editor.cshtml.cs b/GanjooRazor/Areas/User/Pages/Editor.cshtml.cs index d97b8b79..bf71031c 100644 --- a/GanjooRazor/Areas/User/Pages/Editor.cshtml.cs +++ b/GanjooRazor/Areas/User/Pages/Editor.cshtml.cs @@ -218,6 +218,11 @@ namespace GanjooRazor.Areas.User.Pages FatalError = ""; CanEdit = Request.Cookies["CanEdit"] == "True"; + // set before any of the early "API call failed" returns below, so the view - which + // reads Model.Couplets unconditionally near the top of its script block, before the + // FatalError check further down - never sees a null and throws ArgumentNullException + // out of Couplets.ToDictionary(...) on a failed request + Couplets = Array.Empty>(); ShowAdminOps = CanEdit && Request.Query["admin"] == "1"; using (HttpClient secureClient = new HttpClient(new GanjoorReloginHandler(Request, Response))) diff --git a/GanjooRazor/wwwroot/css/user-panel.css b/GanjooRazor/wwwroot/css/user-panel.css index f820d00f..93cdbcc1 100644 --- a/GanjooRazor/wwwroot/css/user-panel.css +++ b/GanjooRazor/wwwroot/css/user-panel.css @@ -612,6 +612,51 @@ body.up-modal-open { box-shadow: 0 0 0 3px rgba(51, 0, 0, 0.12); } +/* ---------- location search-as-you-type (see setupLocationAutocomplete in bk.js) ---------- */ + +.up-autocomplete-wrap { + position: relative; +} + +.up-autocomplete-results { + position: absolute; + z-index: 50; + top: calc(100% + 2px); + right: 0; + left: 0; + max-height: 260px; + overflow-y: auto; + background: var(--up-surface); + border: 1px solid var(--up-border); + border-radius: var(--up-radius-sm); + box-shadow: var(--up-shadow-sm); +} + +.up-autocomplete-item { + padding: 8px 12px; + font-size: 0.9rem; + cursor: pointer; + border-bottom: 1px solid var(--up-border); +} + +.up-autocomplete-item:last-child { + border-bottom: none; +} + +.up-autocomplete-item:hover { + background: var(--up-bg); +} + +.up-autocomplete-item--empty, +.up-autocomplete-item--more { + color: var(--up-text-muted); + cursor: default; +} + +.up-autocomplete-item--empty { + cursor: pointer; +} + /* ---------- editable surfaces (contenteditable divs standing in for inputs) ---------- Needs a strong, unmistakable affordance since it isn't a native form control - nothing else on the page should look like this. */ diff --git a/GanjooRazor/wwwroot/js/bk.js b/GanjooRazor/wwwroot/js/bk.js index 09605c72..c254aa48 100644 --- a/GanjooRazor/wwwroot/js/bk.js +++ b/GanjooRazor/wwwroot/js/bk.js @@ -2498,4 +2498,104 @@ function load7DaysVisits(url) { } }, }); -} \ No newline at end of file +} +// Turns a plain text into a "search as you type" picker over a small +// in-memory location catalog ({id, name, latitude, longitude}[]), instead of a +// long the user types a location name into +// hiddenInput - an existing/hidden that ends up holding the selected +// location's Id as a string (cleared, i.e. '', whenever the +// text doesn't currently match a selected location) - callers +// that used to read a