Basic search outline config UI implemented.
This commit is contained in:
parent
8a1e5aca15
commit
c6b8ca523b
@ -123,8 +123,13 @@ namespace Props.Shop.Adafruit
|
||||
if (workspaceDir != null)
|
||||
{
|
||||
logger.LogDebug("Saving data in \"{0}\"...", workspaceDir);
|
||||
string configurationPath = Path.Combine(workspaceDir, Configuration.FILE_NAME);
|
||||
File.Delete(configurationPath);
|
||||
await File.WriteAllTextAsync(Path.Combine(workspaceDir, Configuration.FILE_NAME), JsonSerializer.Serialize(configuration));
|
||||
using (Stream fileStream = File.OpenWrite(Path.Combine(workspaceDir, ProductListingCacheData.FILE_NAME)))
|
||||
|
||||
string productListingCachePath = Path.Combine(workspaceDir, ProductListingCacheData.FILE_NAME);
|
||||
File.Delete(productListingCachePath);
|
||||
using (Stream fileStream = File.OpenWrite(productListingCachePath))
|
||||
{
|
||||
await JsonSerializer.SerializeAsync(fileStream, new ProductListingCacheData(await searchManager.ProductListingManager.ProductListings));
|
||||
}
|
||||
|
@ -5,12 +5,12 @@ namespace Props.Shop.Framework
|
||||
public class Filters
|
||||
{
|
||||
public Currency Currency { get; set; } = Currency.CAD;
|
||||
private float minRatingNormalized;
|
||||
private float minRatingNormalized = 0.8f;
|
||||
public int MinRating
|
||||
{
|
||||
get
|
||||
{
|
||||
return (int)(minRatingNormalized * 100);
|
||||
return (int)(minRatingNormalized * 100f);
|
||||
}
|
||||
set
|
||||
{
|
||||
@ -38,7 +38,7 @@ namespace Props.Shop.Framework
|
||||
public bool KeepUnknownPurchaseCount { get; set; } = true;
|
||||
public int MinReviews { get; set; }
|
||||
public bool KeepUnknownReviewCount { get; set; } = true;
|
||||
public bool EnableMaxShippingFee { get; set; }
|
||||
public bool EnableMaxShipping { get; set; }
|
||||
private int maxShippingFee;
|
||||
|
||||
public int MaxShippingFee
|
||||
@ -49,7 +49,7 @@ namespace Props.Shop.Framework
|
||||
}
|
||||
set
|
||||
{
|
||||
if (EnableMaxShippingFee) maxShippingFee = value;
|
||||
if (EnableMaxShipping) maxShippingFee = value;
|
||||
}
|
||||
}
|
||||
public bool KeepUnknownShipping { get; set; } = true;
|
||||
@ -72,7 +72,7 @@ namespace Props.Shop.Framework
|
||||
KeepUnknownPurchaseCount == other.KeepUnknownPurchaseCount &&
|
||||
MinReviews == other.MinReviews &&
|
||||
KeepUnknownReviewCount == other.KeepUnknownReviewCount &&
|
||||
EnableMaxShippingFee == other.EnableMaxShippingFee &&
|
||||
EnableMaxShipping == other.EnableMaxShipping &&
|
||||
MaxShippingFee == other.MaxShippingFee &&
|
||||
KeepUnknownShipping == other.KeepUnknownShipping;
|
||||
|
||||
@ -96,7 +96,7 @@ namespace Props.Shop.Framework
|
||||
|
||||
public bool Validate(ProductListing listing)
|
||||
{
|
||||
if (listing.Shipping == null && !KeepUnknownShipping || (EnableMaxShippingFee && listing.Shipping > MaxShippingFee)) return false;
|
||||
if (listing.Shipping == null && !KeepUnknownShipping || (EnableMaxShipping && listing.Shipping > MaxShippingFee)) return false;
|
||||
float shippingDifference = listing.Shipping != null ? listing.Shipping.Value : 0;
|
||||
if (!(listing.LowerPrice + shippingDifference >= LowerPrice && (!EnableUpperPrice || listing.UpperPrice + shippingDifference <= UpperPrice))) return false;
|
||||
if ((listing.Rating == null && !KeepUnrated) && MinRating > (listing.Rating == null ? 0 : listing.Rating)) return false;
|
||||
|
@ -1,24 +1,38 @@
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using System.Net;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Props.Models.Search;
|
||||
using Props.Services.Modules;
|
||||
using Props.Shop.Framework;
|
||||
|
||||
namespace Props.Controllers
|
||||
{
|
||||
public class SearchController : ApiControllerBase
|
||||
{
|
||||
private SearchOutline defaultOutline = new SearchOutline();
|
||||
IShopManager shopManager;
|
||||
ISearchManager searchManager;
|
||||
|
||||
public SearchController(IShopManager shopManager)
|
||||
public SearchController(ISearchManager searchManager)
|
||||
{
|
||||
this.shopManager = shopManager;
|
||||
this.searchManager = searchManager;
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
[Route("Available")]
|
||||
public IActionResult GetAvailableShops()
|
||||
[Route("AvailableShops")]
|
||||
public async Task<IActionResult> GetAvailableShops()
|
||||
{
|
||||
return Ok(shopManager.GetAllShopNames());
|
||||
return Ok(await searchManager.ShopManager.GetAllShopNames());
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
[Route("SearchShops/{search}/")]
|
||||
public async Task<IActionResult> GetSearch(string searchQuery, [FromQuery] SearchOutline searchOutline)
|
||||
{
|
||||
if (searchQuery == null) return BadRequest();
|
||||
|
||||
return Ok(await searchManager.Search(searchQuery, searchOutline));
|
||||
}
|
||||
}
|
||||
}
|
@ -1,37 +1,211 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Props.Data;
|
||||
using Props.Models.Search;
|
||||
using Props.Services.Modules;
|
||||
using Props.Models.User;
|
||||
using Props.Shop.Framework;
|
||||
|
||||
namespace Props.Controllers
|
||||
{
|
||||
public class SearchOutlineController : ApiControllerBase
|
||||
{
|
||||
private SearchOutline defaultOutline = new SearchOutline();
|
||||
|
||||
|
||||
public SearchOutlineController()
|
||||
private ApplicationDbContext dbContext;
|
||||
private UserManager<ApplicationUser> userManager;
|
||||
public SearchOutlineController(UserManager<ApplicationUser> userManager, ApplicationDbContext dbContext)
|
||||
{
|
||||
this.userManager = userManager;
|
||||
this.dbContext = dbContext;
|
||||
}
|
||||
|
||||
[HttpDelete]
|
||||
[Authorize]
|
||||
[Route("{name:required}")]
|
||||
public async Task<IActionResult> DeleteSearchOutline(string name)
|
||||
{
|
||||
if (string.IsNullOrEmpty(name))
|
||||
{
|
||||
return BadRequest();
|
||||
}
|
||||
ApplicationUser user = await userManager.GetUserAsync(User);
|
||||
SearchOutlinePreferences searchOutlinePrefs = user.searchOutlinePreferences;
|
||||
searchOutlinePrefs.SearchOutlines.Remove(searchOutlinePrefs.SearchOutlines.Single((outline) => name.Equals(outline.Name)));
|
||||
await userManager.UpdateAsync(user);
|
||||
return NoContent();
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
[Authorize]
|
||||
[Route("{name:required}")]
|
||||
public async Task<IActionResult> PostSearchOutline(string name)
|
||||
{
|
||||
if (string.IsNullOrEmpty(name)) return BadRequest();
|
||||
ApplicationUser user = await userManager.GetUserAsync(User);
|
||||
SearchOutline searchOutline = user.searchOutlinePreferences.SearchOutlines.SingleOrDefault((outline) => name.Equals(outline.Name));
|
||||
if (searchOutline != null) return BadRequest();
|
||||
searchOutline = new SearchOutline();
|
||||
searchOutline.Name = name;
|
||||
user.searchOutlinePreferences.SearchOutlines.Add(searchOutline);
|
||||
await userManager.UpdateAsync(user);
|
||||
return NoContent();
|
||||
}
|
||||
|
||||
[HttpPut]
|
||||
[Authorize]
|
||||
[Route("{name:required}/Filters")]
|
||||
public async Task<IActionResult> PutFilters(string name, Filters filters)
|
||||
{
|
||||
if (string.IsNullOrEmpty(name)) return BadRequest();
|
||||
ApplicationUser user = await userManager.GetUserAsync(User);
|
||||
SearchOutline searchOutline = await GetSearchOutlineByName(name);
|
||||
if (searchOutline == null) return BadRequest();
|
||||
searchOutline.Filters = filters;
|
||||
await userManager.UpdateAsync(user);
|
||||
return NoContent();
|
||||
}
|
||||
|
||||
[HttpPut]
|
||||
[Authorize]
|
||||
[Route("{outlineName:required}/DisabledShops")]
|
||||
public async Task<IActionResult> PutShopSelection(string outlineName, ISet<string> disabledShops)
|
||||
{
|
||||
if (string.IsNullOrEmpty(outlineName)) return BadRequest();
|
||||
if (disabledShops == null) return BadRequest();
|
||||
ApplicationUser user = await userManager.GetUserAsync(User);
|
||||
SearchOutline searchOutline = await GetSearchOutlineByName(outlineName);
|
||||
if (searchOutline == null) return BadRequest();
|
||||
|
||||
searchOutline.DisabledShops.Clear();
|
||||
searchOutline.DisabledShops.UnionWith(disabledShops);
|
||||
await userManager.UpdateAsync(user);
|
||||
return NoContent();
|
||||
}
|
||||
|
||||
[HttpPut]
|
||||
[Authorize]
|
||||
[Route("{oldName:required}/Name/{newName:required}")]
|
||||
public async Task<IActionResult> PutName(string oldName, string newName)
|
||||
{
|
||||
if (oldName == newName) return BadRequest();
|
||||
ApplicationUser user = await userManager.GetUserAsync(User);
|
||||
SearchOutline outline = await GetSearchOutlineByName(oldName);
|
||||
if (outline == null) return BadRequest();
|
||||
if (user.searchOutlinePreferences.SearchOutlines.Any((outline) => outline.Name.Equals(newName))) return BadRequest();
|
||||
outline.Name = newName;
|
||||
if (user.searchOutlinePreferences.NameOfLastUsed == oldName)
|
||||
{
|
||||
user.searchOutlinePreferences.NameOfLastUsed = newName;
|
||||
}
|
||||
await userManager.UpdateAsync(user);
|
||||
return NoContent();
|
||||
}
|
||||
|
||||
[HttpPut]
|
||||
[Authorize]
|
||||
[Route("{name:required}/LastUsed")]
|
||||
public async Task<IActionResult> PutLastUsed(string name)
|
||||
{
|
||||
SearchOutline outline = await GetSearchOutlineByName(name);
|
||||
if (outline == null) return BadRequest();
|
||||
ApplicationUser user = await userManager.GetUserAsync(User);
|
||||
user.searchOutlinePreferences.NameOfLastUsed = name;
|
||||
await userManager.UpdateAsync(user);
|
||||
return NoContent();
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
[Route("Filters")]
|
||||
public IActionResult GetFilters()
|
||||
[Authorize]
|
||||
[Route("{name:required}/Filters")]
|
||||
public async Task<IActionResult> GetFilters(string name)
|
||||
{
|
||||
return Ok(defaultOutline.Filters);
|
||||
Filters filters = (await GetSearchOutlineByName(name))?.Filters;
|
||||
if (filters == null) return BadRequest();
|
||||
return Ok(filters);
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
[Route("DisabledShops")]
|
||||
public IActionResult GetDisabledShops()
|
||||
[Authorize]
|
||||
[Route("{name:required}/DisabledShops")]
|
||||
public async Task<IActionResult> GetDisabledShops(string name)
|
||||
{
|
||||
return Ok(defaultOutline.Enabled);
|
||||
SearchOutline searchOutline = await GetSearchOutlineByName(name);
|
||||
if (searchOutline == null)
|
||||
{
|
||||
return BadRequest();
|
||||
}
|
||||
return Ok(searchOutline.DisabledShops);
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
[Route("SearchOutlineName")]
|
||||
public IActionResult GetSearchOutlineName()
|
||||
[Authorize]
|
||||
[Route("Names")]
|
||||
public async Task<IActionResult> GetSearchOutlineNames()
|
||||
{
|
||||
return Ok(defaultOutline.Name);
|
||||
ApplicationUser user = await userManager.GetUserAsync(User);
|
||||
return Ok(user.searchOutlinePreferences.SearchOutlines.Select((outline, Index) => outline.Name));
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
[Authorize]
|
||||
[Route("LastUsed")]
|
||||
public async Task<IActionResult> GetLastSearchOutlineName()
|
||||
{
|
||||
SearchOutline searchOutline = await GetLastUsedSearchOutline();
|
||||
|
||||
return Ok(searchOutline?.Name);
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
[Route("DefaultDisabledShops")]
|
||||
public IActionResult GetDefaultDisabledShops()
|
||||
{
|
||||
return Ok(new SearchOutline.ShopSelector());
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
[Route("DefaultFilters")]
|
||||
public IActionResult GetDefaultFilter()
|
||||
{
|
||||
return Ok(new Filters());
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
[Route("DefaultName")]
|
||||
public async Task<IActionResult> GetDefaultName()
|
||||
{
|
||||
string nameTemplate = "Search Outline {0}";
|
||||
if (User.Identity.IsAuthenticated)
|
||||
{
|
||||
ApplicationUser user = await userManager.GetUserAsync(User);
|
||||
int number = user.searchOutlinePreferences.SearchOutlines.Count;
|
||||
string name = null;
|
||||
do
|
||||
{
|
||||
name = string.Format(nameTemplate, number);
|
||||
number += 1;
|
||||
} while (user.searchOutlinePreferences.SearchOutlines.Any((outline) => name.Equals(outline.Name)));
|
||||
return Ok(name);
|
||||
}
|
||||
return Ok("Search Outline");
|
||||
}
|
||||
|
||||
private async Task<SearchOutline> GetLastUsedSearchOutline()
|
||||
{
|
||||
if (!User.Identity.IsAuthenticated) return null;
|
||||
ApplicationUser user = await userManager.GetUserAsync(User);
|
||||
return user.searchOutlinePreferences.SearchOutlines.SingleOrDefault((outline) => outline.Name.Equals(user.searchOutlinePreferences.NameOfLastUsed));
|
||||
}
|
||||
|
||||
private async Task<SearchOutline> GetSearchOutlineByName(string name)
|
||||
{
|
||||
if (name == null) throw new ArgumentNullException("name");
|
||||
if (!User.Identity.IsAuthenticated) return null;
|
||||
ApplicationUser user = await userManager.GetUserAsync(User);
|
||||
return user.searchOutlinePreferences.SearchOutlines.SingleOrDefault(outline => outline.Name.Equals(name));
|
||||
}
|
||||
}
|
||||
}
|
14
Props/Controllers/UserController.cs
Normal file
14
Props/Controllers/UserController.cs
Normal file
@ -0,0 +1,14 @@
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace Props.Controllers
|
||||
{
|
||||
public class UserController : ApiControllerBase
|
||||
{
|
||||
[HttpGet]
|
||||
[Route("LoggedIn")]
|
||||
public IActionResult GetLoggedIn()
|
||||
{
|
||||
return Ok(User.Identity.IsAuthenticated);
|
||||
}
|
||||
}
|
||||
}
|
@ -17,7 +17,7 @@ namespace Props.Data
|
||||
{
|
||||
public class ApplicationDbContext : IdentityDbContext<ApplicationUser>
|
||||
{
|
||||
public DbSet<QueryWordInfo> Keywords { get; set; }
|
||||
public DbSet<QueryWordInfo> QueryWords { get; set; }
|
||||
|
||||
public DbSet<ProductListingInfo> ProductListingInfos { get; set; }
|
||||
|
||||
@ -43,14 +43,14 @@ namespace Props.Data
|
||||
);
|
||||
|
||||
modelBuilder.Entity<SearchOutline>()
|
||||
.Property(e => e.Enabled)
|
||||
.Property(e => e.DisabledShops)
|
||||
.HasConversion(
|
||||
v => JsonSerializer.Serialize(v, null),
|
||||
v => JsonSerializer.Deserialize<SearchOutline.ShopsDisabled>(v, null),
|
||||
new ValueComparer<SearchOutline.ShopsDisabled>(
|
||||
v => JsonSerializer.Deserialize<SearchOutline.ShopSelector>(v, null),
|
||||
new ValueComparer<SearchOutline.ShopSelector>(
|
||||
(a, b) => a.Equals(b),
|
||||
c => c.GetHashCode(),
|
||||
c => c.Copy()
|
||||
c => new SearchOutline.ShopSelector(c)
|
||||
)
|
||||
);
|
||||
|
||||
|
@ -9,7 +9,7 @@ using Props.Data;
|
||||
namespace Props.Data.Migrations
|
||||
{
|
||||
[DbContext(typeof(ApplicationDbContext))]
|
||||
[Migration("20210809194646_InitialCreate")]
|
||||
[Migration("20210817042955_InitialCreate")]
|
||||
partial class InitialCreate
|
||||
{
|
||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||
@ -165,7 +165,6 @@ namespace Props.Data.Migrations
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("ProfileName")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("Id");
|
||||
@ -214,7 +213,7 @@ namespace Props.Data.Migrations
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("Keywords");
|
||||
b.ToTable("QueryWords");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Props.Models.Search.SearchOutline", b =>
|
||||
@ -223,11 +222,7 @@ namespace Props.Data.Migrations
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("ApplicationUserId")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Enabled")
|
||||
b.Property<string>("DisabledShops")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
@ -238,13 +233,11 @@ namespace Props.Data.Migrations
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<int?>("SearchOutlinePreferencesId")
|
||||
b.Property<int>("SearchOutlinePreferencesId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("ApplicationUserId");
|
||||
|
||||
b.HasIndex("SearchOutlinePreferencesId");
|
||||
|
||||
b.ToTable("SearchOutline");
|
||||
@ -320,16 +313,14 @@ namespace Props.Data.Migrations
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int?>("ActiveSearchOutlineId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("ApplicationUserId")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("Id");
|
||||
b.Property<string>("NameOfLastUsed")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasIndex("ActiveSearchOutlineId");
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("ApplicationUserId")
|
||||
.IsUnique();
|
||||
@ -440,33 +431,23 @@ namespace Props.Data.Migrations
|
||||
|
||||
modelBuilder.Entity("Props.Models.Search.SearchOutline", b =>
|
||||
{
|
||||
b.HasOne("Props.Models.User.ApplicationUser", "ApplicationUser")
|
||||
.WithMany()
|
||||
.HasForeignKey("ApplicationUserId")
|
||||
b.HasOne("Props.Models.User.SearchOutlinePreferences", "SearchOutlinePreferences")
|
||||
.WithMany("SearchOutlines")
|
||||
.HasForeignKey("SearchOutlinePreferencesId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("Props.Models.User.SearchOutlinePreferences", null)
|
||||
.WithMany("SearchOutlines")
|
||||
.HasForeignKey("SearchOutlinePreferencesId");
|
||||
|
||||
b.Navigation("ApplicationUser");
|
||||
b.Navigation("SearchOutlinePreferences");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Props.Models.User.SearchOutlinePreferences", b =>
|
||||
{
|
||||
b.HasOne("Props.Models.Search.SearchOutline", "ActiveSearchOutline")
|
||||
.WithMany()
|
||||
.HasForeignKey("ActiveSearchOutlineId");
|
||||
|
||||
b.HasOne("Props.Models.User.ApplicationUser", "ApplicationUser")
|
||||
.WithOne("searchOutlinePreferences")
|
||||
.HasForeignKey("Props.Models.User.SearchOutlinePreferences", "ApplicationUserId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("ActiveSearchOutline");
|
||||
|
||||
b.Navigation("ApplicationUser");
|
||||
});
|
||||
|
@ -46,20 +46,6 @@ namespace Props.Data.Migrations
|
||||
table.PrimaryKey("PK_AspNetUsers", x => x.Id);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "Keywords",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<int>(type: "INTEGER", nullable: false)
|
||||
.Annotation("Sqlite:Autoincrement", true),
|
||||
Word = table.Column<string>(type: "TEXT", nullable: false),
|
||||
Hits = table.Column<uint>(type: "INTEGER", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_Keywords", x => x.Id);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "ProductListingInfos",
|
||||
columns: table => new
|
||||
@ -76,6 +62,20 @@ namespace Props.Data.Migrations
|
||||
table.PrimaryKey("PK_ProductListingInfos", x => x.Id);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "QueryWords",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<int>(type: "INTEGER", nullable: false)
|
||||
.Annotation("Sqlite:Autoincrement", true),
|
||||
Word = table.Column<string>(type: "TEXT", nullable: false),
|
||||
Hits = table.Column<uint>(type: "INTEGER", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_QueryWords", x => x.Id);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "AspNetRoleClaims",
|
||||
columns: table => new
|
||||
@ -211,7 +211,7 @@ namespace Props.Data.Migrations
|
||||
.Annotation("Sqlite:Autoincrement", true),
|
||||
ApplicationUserId = table.Column<string>(type: "TEXT", nullable: false),
|
||||
Order = table.Column<string>(type: "TEXT", nullable: false),
|
||||
ProfileName = table.Column<string>(type: "TEXT", nullable: false)
|
||||
ProfileName = table.Column<string>(type: "TEXT", nullable: true)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
@ -224,6 +224,26 @@ namespace Props.Data.Migrations
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "SearchOutlinePreferences",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<int>(type: "INTEGER", nullable: false)
|
||||
.Annotation("Sqlite:Autoincrement", true),
|
||||
ApplicationUserId = table.Column<string>(type: "TEXT", nullable: false),
|
||||
NameOfLastUsed = table.Column<string>(type: "TEXT", nullable: true)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_SearchOutlinePreferences", x => x.Id);
|
||||
table.ForeignKey(
|
||||
name: "FK_SearchOutlinePreferences_AspNetUsers_ApplicationUserId",
|
||||
column: x => x.ApplicationUserId,
|
||||
principalTable: "AspNetUsers",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "QueryWordInfoQueryWordInfo",
|
||||
columns: table => new
|
||||
@ -235,15 +255,15 @@ namespace Props.Data.Migrations
|
||||
{
|
||||
table.PrimaryKey("PK_QueryWordInfoQueryWordInfo", x => new { x.FollowingId, x.PrecedingId });
|
||||
table.ForeignKey(
|
||||
name: "FK_QueryWordInfoQueryWordInfo_Keywords_FollowingId",
|
||||
name: "FK_QueryWordInfoQueryWordInfo_QueryWords_FollowingId",
|
||||
column: x => x.FollowingId,
|
||||
principalTable: "Keywords",
|
||||
principalTable: "QueryWords",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
table.ForeignKey(
|
||||
name: "FK_QueryWordInfoQueryWordInfo_Keywords_PrecedingId",
|
||||
name: "FK_QueryWordInfoQueryWordInfo_QueryWords_PrecedingId",
|
||||
column: x => x.PrecedingId,
|
||||
principalTable: "Keywords",
|
||||
principalTable: "QueryWords",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
});
|
||||
@ -254,49 +274,22 @@ namespace Props.Data.Migrations
|
||||
{
|
||||
Id = table.Column<int>(type: "INTEGER", nullable: false)
|
||||
.Annotation("Sqlite:Autoincrement", true),
|
||||
ApplicationUserId = table.Column<string>(type: "TEXT", nullable: false),
|
||||
SearchOutlinePreferencesId = table.Column<int>(type: "INTEGER", nullable: false),
|
||||
Name = table.Column<string>(type: "TEXT", nullable: false),
|
||||
Filters = table.Column<string>(type: "TEXT", nullable: true),
|
||||
Enabled = table.Column<string>(type: "TEXT", nullable: false),
|
||||
SearchOutlinePreferencesId = table.Column<int>(type: "INTEGER", nullable: true)
|
||||
DisabledShops = table.Column<string>(type: "TEXT", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_SearchOutline", x => x.Id);
|
||||
table.ForeignKey(
|
||||
name: "FK_SearchOutline_AspNetUsers_ApplicationUserId",
|
||||
column: x => x.ApplicationUserId,
|
||||
principalTable: "AspNetUsers",
|
||||
name: "FK_SearchOutline_SearchOutlinePreferences_SearchOutlinePreferencesId",
|
||||
column: x => x.SearchOutlinePreferencesId,
|
||||
principalTable: "SearchOutlinePreferences",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "SearchOutlinePreferences",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<int>(type: "INTEGER", nullable: false)
|
||||
.Annotation("Sqlite:Autoincrement", true),
|
||||
ApplicationUserId = table.Column<string>(type: "TEXT", nullable: false),
|
||||
ActiveSearchOutlineId = table.Column<int>(type: "INTEGER", nullable: true)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_SearchOutlinePreferences", x => x.Id);
|
||||
table.ForeignKey(
|
||||
name: "FK_SearchOutlinePreferences_AspNetUsers_ApplicationUserId",
|
||||
column: x => x.ApplicationUserId,
|
||||
principalTable: "AspNetUsers",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
table.ForeignKey(
|
||||
name: "FK_SearchOutlinePreferences_SearchOutline_ActiveSearchOutlineId",
|
||||
column: x => x.ActiveSearchOutlineId,
|
||||
principalTable: "SearchOutline",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Restrict);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_ApplicationPreferences_ApplicationUserId",
|
||||
table: "ApplicationPreferences",
|
||||
@ -351,50 +344,20 @@ namespace Props.Data.Migrations
|
||||
column: "ApplicationUserId",
|
||||
unique: true);
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_SearchOutline_ApplicationUserId",
|
||||
table: "SearchOutline",
|
||||
column: "ApplicationUserId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_SearchOutline_SearchOutlinePreferencesId",
|
||||
table: "SearchOutline",
|
||||
column: "SearchOutlinePreferencesId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_SearchOutlinePreferences_ActiveSearchOutlineId",
|
||||
table: "SearchOutlinePreferences",
|
||||
column: "ActiveSearchOutlineId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_SearchOutlinePreferences_ApplicationUserId",
|
||||
table: "SearchOutlinePreferences",
|
||||
column: "ApplicationUserId",
|
||||
unique: true);
|
||||
|
||||
migrationBuilder.AddForeignKey(
|
||||
name: "FK_SearchOutline_SearchOutlinePreferences_SearchOutlinePreferencesId",
|
||||
table: "SearchOutline",
|
||||
column: "SearchOutlinePreferencesId",
|
||||
principalTable: "SearchOutlinePreferences",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Restrict);
|
||||
}
|
||||
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropForeignKey(
|
||||
name: "FK_SearchOutline_AspNetUsers_ApplicationUserId",
|
||||
table: "SearchOutline");
|
||||
|
||||
migrationBuilder.DropForeignKey(
|
||||
name: "FK_SearchOutlinePreferences_AspNetUsers_ApplicationUserId",
|
||||
table: "SearchOutlinePreferences");
|
||||
|
||||
migrationBuilder.DropForeignKey(
|
||||
name: "FK_SearchOutline_SearchOutlinePreferences_SearchOutlinePreferencesId",
|
||||
table: "SearchOutline");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "ApplicationPreferences");
|
||||
|
||||
@ -422,20 +385,20 @@ namespace Props.Data.Migrations
|
||||
migrationBuilder.DropTable(
|
||||
name: "ResultsPreferences");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "SearchOutline");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "AspNetRoles");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "Keywords");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "AspNetUsers");
|
||||
name: "QueryWords");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "SearchOutlinePreferences");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "SearchOutline");
|
||||
name: "AspNetUsers");
|
||||
}
|
||||
}
|
||||
}
|
@ -163,7 +163,6 @@ namespace Props.Data.Migrations
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("ProfileName")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("Id");
|
||||
@ -212,7 +211,7 @@ namespace Props.Data.Migrations
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("Keywords");
|
||||
b.ToTable("QueryWords");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Props.Models.Search.SearchOutline", b =>
|
||||
@ -221,11 +220,7 @@ namespace Props.Data.Migrations
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("ApplicationUserId")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Enabled")
|
||||
b.Property<string>("DisabledShops")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
@ -236,13 +231,11 @@ namespace Props.Data.Migrations
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<int?>("SearchOutlinePreferencesId")
|
||||
b.Property<int>("SearchOutlinePreferencesId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("ApplicationUserId");
|
||||
|
||||
b.HasIndex("SearchOutlinePreferencesId");
|
||||
|
||||
b.ToTable("SearchOutline");
|
||||
@ -318,16 +311,14 @@ namespace Props.Data.Migrations
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int?>("ActiveSearchOutlineId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("ApplicationUserId")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("Id");
|
||||
b.Property<string>("NameOfLastUsed")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasIndex("ActiveSearchOutlineId");
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("ApplicationUserId")
|
||||
.IsUnique();
|
||||
@ -438,33 +429,23 @@ namespace Props.Data.Migrations
|
||||
|
||||
modelBuilder.Entity("Props.Models.Search.SearchOutline", b =>
|
||||
{
|
||||
b.HasOne("Props.Models.User.ApplicationUser", "ApplicationUser")
|
||||
.WithMany()
|
||||
.HasForeignKey("ApplicationUserId")
|
||||
b.HasOne("Props.Models.User.SearchOutlinePreferences", "SearchOutlinePreferences")
|
||||
.WithMany("SearchOutlines")
|
||||
.HasForeignKey("SearchOutlinePreferencesId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("Props.Models.User.SearchOutlinePreferences", null)
|
||||
.WithMany("SearchOutlines")
|
||||
.HasForeignKey("SearchOutlinePreferencesId");
|
||||
|
||||
b.Navigation("ApplicationUser");
|
||||
b.Navigation("SearchOutlinePreferences");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Props.Models.User.SearchOutlinePreferences", b =>
|
||||
{
|
||||
b.HasOne("Props.Models.Search.SearchOutline", "ActiveSearchOutline")
|
||||
.WithMany()
|
||||
.HasForeignKey("ActiveSearchOutlineId");
|
||||
|
||||
b.HasOne("Props.Models.User.ApplicationUser", "ApplicationUser")
|
||||
.WithOne("searchOutlinePreferences")
|
||||
.HasForeignKey("Props.Models.User.SearchOutlinePreferences", "ApplicationUserId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("ActiveSearchOutline");
|
||||
|
||||
b.Navigation("ApplicationUser");
|
||||
});
|
||||
|
||||
|
@ -11,53 +11,45 @@ namespace Props.Models.Search
|
||||
{
|
||||
public int Id { get; set; }
|
||||
|
||||
[Required]
|
||||
public string ApplicationUserId { get; set; }
|
||||
public int SearchOutlinePreferencesId { get; set; }
|
||||
|
||||
[Required]
|
||||
public virtual ApplicationUser ApplicationUser { get; set; }
|
||||
public virtual SearchOutlinePreferences SearchOutlinePreferences { get; set; }
|
||||
|
||||
[Required]
|
||||
public string Name { get; set; } = "Default";
|
||||
|
||||
public Filters Filters { get; set; } = new Filters();
|
||||
public string Name { get; set; }
|
||||
|
||||
public Filters Filters { get; set; }
|
||||
[Required]
|
||||
public ShopsDisabled Enabled { get; set; } = new ShopsDisabled();
|
||||
public ShopSelector DisabledShops { get; set; }
|
||||
|
||||
public sealed class ShopsDisabled : HashSet<string>
|
||||
public sealed class ShopSelector : HashSet<string>
|
||||
{
|
||||
public int TotalShops { get; set; }
|
||||
public bool this[string name]
|
||||
{
|
||||
get
|
||||
{
|
||||
return !this.Contains(name);
|
||||
return this.Contains(name);
|
||||
}
|
||||
set
|
||||
{
|
||||
if (value == false && TotalShops - Count <= 1) return;
|
||||
if (value)
|
||||
{
|
||||
this.Remove(name);
|
||||
this.Add(name);
|
||||
}
|
||||
else
|
||||
{
|
||||
this.Add(name);
|
||||
this.Remove(name);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public ShopsDisabled Copy()
|
||||
public ShopSelector()
|
||||
{
|
||||
ShopsDisabled copy = new ShopsDisabled();
|
||||
copy.Union(this);
|
||||
return copy;
|
||||
}
|
||||
|
||||
public bool IsShopToggleable(string shop)
|
||||
public ShopSelector(IEnumerable<string> disabledShops) : base(disabledShops)
|
||||
{
|
||||
return (!Contains(shop) && TotalShops - Count > 1) || Contains(shop);
|
||||
}
|
||||
}
|
||||
|
||||
@ -70,27 +62,27 @@ namespace Props.Models.Search
|
||||
SearchOutline other = (SearchOutline)obj;
|
||||
return
|
||||
Id == other.Id &&
|
||||
Name.Equals(other.Name) &&
|
||||
Filters.Equals(other.Filters) &&
|
||||
Enabled.Equals(other.Enabled);
|
||||
DisabledShops.Equals(other.DisabledShops);
|
||||
}
|
||||
|
||||
public override int GetHashCode()
|
||||
{
|
||||
return HashCode.Combine(Id, Name);
|
||||
return HashCode.Combine(Id, Name, Filters, DisabledShops);
|
||||
}
|
||||
|
||||
public SearchOutline()
|
||||
{
|
||||
this.Name = "Default";
|
||||
this.Filters = new Filters();
|
||||
this.Enabled = new ShopsDisabled();
|
||||
this.DisabledShops = new ShopSelector();
|
||||
}
|
||||
|
||||
public SearchOutline(string name, Filters filters, ShopsDisabled disabled)
|
||||
public SearchOutline(string name, Filters filters, ShopSelector disabled)
|
||||
{
|
||||
this.Name = name;
|
||||
this.Filters = filters;
|
||||
this.Enabled = disabled;
|
||||
this.DisabledShops = disabled;
|
||||
}
|
||||
}
|
||||
}
|
@ -21,7 +21,6 @@ namespace Props.Models
|
||||
[Required]
|
||||
public IList<Category> Order { get; set; }
|
||||
|
||||
[Required]
|
||||
public string ProfileName { get; set; }
|
||||
|
||||
public ResultsPreferences()
|
||||
|
@ -16,22 +16,19 @@ namespace Props.Models.User
|
||||
public virtual ApplicationUser ApplicationUser { get; set; }
|
||||
|
||||
[Required]
|
||||
public virtual ISet<SearchOutline> SearchOutlines { get; set; }
|
||||
public virtual IList<SearchOutline> SearchOutlines { get; set; }
|
||||
|
||||
[Required]
|
||||
public virtual SearchOutline ActiveSearchOutline { get; set; }
|
||||
public string NameOfLastUsed { get; set; }
|
||||
|
||||
public SearchOutlinePreferences()
|
||||
{
|
||||
SearchOutlines = new HashSet<SearchOutline>();
|
||||
ActiveSearchOutline = new SearchOutline();
|
||||
SearchOutlines.Add(ActiveSearchOutline);
|
||||
SearchOutlines = new List<SearchOutline>();
|
||||
}
|
||||
|
||||
public SearchOutlinePreferences(ISet<SearchOutline> searchOutlines, SearchOutline activeSearchOutline)
|
||||
public SearchOutlinePreferences(List<SearchOutline> searchOutlines, string nameOfLastUsed)
|
||||
{
|
||||
this.SearchOutlines = searchOutlines;
|
||||
this.ActiveSearchOutline = activeSearchOutline;
|
||||
this.NameOfLastUsed = nameOfLastUsed;
|
||||
}
|
||||
}
|
||||
}
|
9
Props/Options/MetricsOptions.cs
Normal file
9
Props/Options/MetricsOptions.cs
Normal file
@ -0,0 +1,9 @@
|
||||
namespace Props.Options
|
||||
{
|
||||
public class MetricsOptions
|
||||
{
|
||||
public const string Metrics = "Metrics";
|
||||
public int MaxQueryWords { get; set; }
|
||||
public int MaxProductListings { get; set; }
|
||||
}
|
||||
}
|
@ -8,255 +8,298 @@
|
||||
ViewData["Specific"] = "Search";
|
||||
}
|
||||
|
||||
<form method="GET">
|
||||
<div class="mt-4 mb-3 less-concise mx-auto">
|
||||
<div class="flex-grow-1 d-flex flex-column" x-data="search">
|
||||
<div class="mt-4 mb-3 less-concise">
|
||||
<div class="input-group">
|
||||
<input type="text" class="form-control border-primary" placeholder="What are you looking for?"
|
||||
aria-label="Search" aria-describedby="search-btn" id="search-bar" value="@Model.SearchQuery" name="q">
|
||||
aria-label="Search" aria-describedby="search-btn" id="search-bar" value="@Model.SearchQuery"
|
||||
x-model="query">
|
||||
<button class="btn btn-outline-secondary" type="button" id="configuration-toggle" data-bs-toggle="collapse"
|
||||
data-bs-target="#configuration"><i class="bi bi-sliders"></i></button>
|
||||
<button class="btn btn-primary" type="submit" id="search-btn">Search</button>
|
||||
<button class="btn btn-primary" id="search-btn" x-on:click="submitSearch" x-on:keyup="">Search</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="collapse tear" id="configuration">
|
||||
<div class="container my-3">
|
||||
<div class="d-flex">
|
||||
<h1 class="my-2 display-2 me-auto">Configuration</h1>
|
||||
<button class="btn align-self-start" type="button" id="configuration-close" data-bs-toggle="collapse"
|
||||
data-bs-target="#configuration"><i class="bi bi-x-lg"></i></button>
|
||||
</div>
|
||||
<div class="row justify-content-md-center">
|
||||
<section class="col-lg px-4">
|
||||
<h3>Price</h3>
|
||||
<div class="mb-3">
|
||||
<label for="max-price" class="form-label">Maximum Price</label>
|
||||
<div class="input-group">
|
||||
<div class="input-group-text">
|
||||
<input class="form-check-input mt-0" type="checkbox" id="max-price-enabled"
|
||||
checked="@Model.ActiveSearchOutline.Filters.EnableUpperPrice"
|
||||
name="ActiveSearchOutline.Filters.EnableUpperPrice">
|
||||
</div>
|
||||
<span class="input-group-text">$</span>
|
||||
<input type="number" class="form-control" min="0" id="max-price"
|
||||
value="@Model.ActiveSearchOutline.Filters.UpperPrice"
|
||||
name="ActiveSearchOutline.Filters.UpperPrice">
|
||||
<span class="input-group-text">.00</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="min-price" class="form-label">Minimum Price</label>
|
||||
<div class="input-group">
|
||||
<span class="input-group-text">$</span>
|
||||
<input type="number" class="form-control" min="0" id="min-price"
|
||||
value="@Model.ActiveSearchOutline.Filters.LowerPrice"
|
||||
name="ActiveSearchOutline.Filters.LowerPrice">
|
||||
<span class="input-group-text">.00</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="max-shipping" class="form-label">Maximum Shipping Fee</label>
|
||||
<div class="input-group">
|
||||
<div class="input-group-text">
|
||||
<input class="form-check-input mt-0" type="checkbox" id="max-shipping-enabled"
|
||||
checked="@Model.ActiveSearchOutline.Filters.EnableMaxShippingFee"
|
||||
name="ActiveSearchOutline.Filters.EnableMaxShippingFee">
|
||||
</div>
|
||||
<span class="input-group-text">$</span>
|
||||
<input type="number" class="form-control" min="0" id="max-shipping"
|
||||
value="@Model.ActiveSearchOutline.Filters.MaxShippingFee"
|
||||
name="ActiveSearchOutline.Filters.MaxShippingFee">
|
||||
<span class="input-group-text">.00</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" type="checkbox" id="keep-unknown-shipping"
|
||||
checked="@Model.ActiveSearchOutline.Filters.KeepUnknownShipping"
|
||||
name="ActiveSearchOutline.Filters.KeepUnknownShipping">
|
||||
<label class="form-check-label" for="keep-unknown-shipping">Keep Unknown Shipping</label>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<section class="col-lg px-4">
|
||||
<h3>Metrics</h3>
|
||||
<div class="mb-3">
|
||||
<label for="min-purchases" class="form-label">Minimum Purchases</label>
|
||||
<div class="input-group">
|
||||
<input type="number" class="form-control" min="0" id="min-purchases"
|
||||
value="@Model.ActiveSearchOutline.Filters.MinPurchases"
|
||||
name="ActiveSearchOutline.Filters.MinPurchases">
|
||||
<span class="input-group-text">Purchases</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" type="checkbox" id="keep-unknown-purchases"
|
||||
checked="@Model.ActiveSearchOutline.Filters.KeepUnknownPurchaseCount"
|
||||
name="ActiveSearchOutline.Filters.KeepUnknownPurchaseCount">
|
||||
<label class="form-check-label" for="keep-unknown-purchases">Keep Unknown Purchases</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="min-reviews" class="form-label">Minimum Reviews</label>
|
||||
<div class="input-group">
|
||||
<input type="number" class="form-control" min="0" id="min-reviews"
|
||||
value="@Model.ActiveSearchOutline.Filters.MinReviews"
|
||||
name="ActiveSearchOutline.Filters.MinReviews">
|
||||
<span class="input-group-text">Reviews</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" type="checkbox" id="keep-unknown-reviews"
|
||||
checked="@Model.ActiveSearchOutline.Filters.KeepUnknownReviewCount"
|
||||
name="ActiveSearchOutline.Filters.KeepUnknownReviewCount">
|
||||
<label class="form-check-label" for="keep-unknown-reviews">Keep Unknown Number of
|
||||
Reviews</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mb-1">
|
||||
<label for="min-rating" class="form-label">Minimum Rating</label>
|
||||
<input type="range" class="form-range" id="min-rating" min="0" max="100" step="1"
|
||||
value="@Model.ActiveSearchOutline.Filters.MinRating"
|
||||
name="ActiveSearchOutline.Filters.MinRating">
|
||||
<div id="min-rating-display" class="form-text"></div>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" type="checkbox" id="keep-unrated"
|
||||
checked="@Model.ActiveSearchOutline.Filters.KeepUnrated"
|
||||
name="ActiveSearchOutline.Filters.KeepUnrated">
|
||||
<label class="form-check-label" for="keep-unrated">Keep Unrated Items</label>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<section class="col-lg px-4">
|
||||
<h3>Shops Enabled</h3>
|
||||
<div class="mb-3 px-3" id="shop-checkboxes">
|
||||
@foreach (string shopName in Model.SearchManager.ShopManager.GetAllShopNames())
|
||||
{
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" type="checkbox" id="@($"{shopName}-enabled")"
|
||||
checked="@Model.ActiveSearchOutline.Enabled[shopName]">
|
||||
<label class="form-check-label" for="@($"{shopName}-enabled")">@shopName</label>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
<div class="d-flex">
|
||||
<h1 class="my-3 display-2 mx-auto">
|
||||
<i class="bi bi-sliders"></i>
|
||||
Configuration
|
||||
</h1>
|
||||
<button class="btn align-self-start m-3" type="button" id="configuration-close" data-bs-toggle="collapse"
|
||||
data-bs-target="#configuration"><i class="bi bi-x-lg"></i></button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<div id="content-pages" class="multipage mt-3 invisible">
|
||||
<ul class="nav nav-pills selectors">
|
||||
<li class="nav-item" role="presentation">
|
||||
<button type="button" data-bs-toggle="pill" data-bs-target="#quick-picks-slide"><i
|
||||
class="bi bi-stopwatch"></i></button>
|
||||
</li>
|
||||
<li class="nav-item" role="presentation">
|
||||
<button type="button" data-bs-toggle="pill" data-bs-target="#results-slide"><i
|
||||
class="bi bi-view-list"></i></button>
|
||||
</li>
|
||||
<li class="nav-item" role="presentation">
|
||||
<button type="button" data-bs-toggle="pill" data-bs-target="#info-slide"><i
|
||||
class="bi bi-info-lg"></i></button>
|
||||
</li>
|
||||
</ul>
|
||||
<div class="multipage-slides tab-content">
|
||||
<div class="multipage-slide tab-pane fade" id="quick-picks-slide">
|
||||
<div class="multipage-title">
|
||||
<h1 class="display-2"><i class="bi bi-stopwatch"></i> Quick Picks</h1>
|
||||
@if (Model.SearchResults != null)
|
||||
{
|
||||
<p>@ContentManager.Json.quickPicks.searched</p>
|
||||
}
|
||||
else
|
||||
{
|
||||
<p>@ContentManager.Json.quickPicks.prompt</p>
|
||||
}
|
||||
<hr class="less-concise">
|
||||
</div>
|
||||
<div class="multipage-content">
|
||||
@if (Model.SearchResults != null)
|
||||
{
|
||||
@if (Model.BestRatingPriceRatio != null)
|
||||
{
|
||||
<p>We found this product to have the best rating to price ratio.</p>
|
||||
}
|
||||
|
||||
@if (Model.TopRated != null)
|
||||
{
|
||||
<p>This listing was the one that had the highest rating.</p>
|
||||
}
|
||||
|
||||
@if (Model.MostPurchases != null)
|
||||
{
|
||||
<p>This listing has the most purchases.</p>
|
||||
}
|
||||
|
||||
@if (Model.MostReviews != null)
|
||||
{
|
||||
<p>This listing had the most reviews.</p>
|
||||
}
|
||||
|
||||
@if (Model.BestPrice != null)
|
||||
{
|
||||
<p>Here's the listing with the lowest price.</p>
|
||||
<div>
|
||||
@Model.BestPrice.Name
|
||||
<div class="container">
|
||||
<div class="row my-3">
|
||||
<div class="col-lg-3 px-2">
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
<h3>Search Outlines</h3>
|
||||
</div>
|
||||
<div class="col-auto" x-show="loggedIn">
|
||||
<button class="btn" x-show="!creatingSearchOutline"
|
||||
x-on:click="createSearchOutlineWithGeneratedName"
|
||||
x-bind:disabled="creatingSearchOutline">
|
||||
<i class="bi bi-plus-lg"></i>
|
||||
</button>
|
||||
<div x-show="creatingSearchOutline" class="spinner-border me-2" role="status">
|
||||
<span class="visually-hidden">Loading...</span>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
@* TODO: Add display for top results. *@
|
||||
}
|
||||
else
|
||||
{
|
||||
<div class="text-center less-concise text-muted flex-grow-1 justify-content-center d-flex flex-column">
|
||||
<h2>@ContentManager.Json.notSearched</h2>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
<div class="multipage-slide tab-pane fade" id="results-slide" x-data>
|
||||
<div class="multipage-title">
|
||||
<h2><i class="bi bi-view-list"></i> Results</h2>
|
||||
@if (Model.SearchResults != null)
|
||||
{
|
||||
<p>@ContentManager.Json.results.searched</p>
|
||||
}
|
||||
else
|
||||
{
|
||||
<p>@ContentManager.Json.results.prompt</p>
|
||||
}
|
||||
<hr class="less-concise">
|
||||
</div>
|
||||
<div class="multipage-content">
|
||||
@if (Model.SearchResults != null)
|
||||
{
|
||||
@* TODO: Display results with UI for sorting and changing views. *@
|
||||
}
|
||||
else
|
||||
{
|
||||
<div class="text-center less-concise text-muted flex-grow-1 justify-content-center d-flex flex-column">
|
||||
<h2>@ContentManager.Json.notSearched</h2>
|
||||
<div class="row px-3">
|
||||
<div style="max-height: 28em;" data-simplebar>
|
||||
<template x-for="(current, i) in searchOutlines">
|
||||
<div class="clean-radio d-flex">
|
||||
<input type="radio" x-bind:id="`${i}-selector`" name="search-outline"
|
||||
x-bind:value="i" x-model="selectedSearchOutline"
|
||||
x-on:click="loadSearchOutline(current)" x-bind:disabled="!searchOutline.ready">
|
||||
<label class="flex-grow-1" x-bind:for="`${i}-selector`">
|
||||
<span class="me-auto" x-text="current"></span>
|
||||
</label>
|
||||
<button class="btn m-1" x-show="loggedIn" x-on:click="deleteSearchOutline(current)"
|
||||
x-bind:disabled="deletingSearchOutline || (searchOutlines.length < 2)">
|
||||
<i class="bi bi-trash"></i>
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
<div class="text-muted text-center my-3" x-show="!loggedIn">
|
||||
<h3><i class="bi bi-box-arrow-in-right"></i></h3>
|
||||
<p>This is where all your search outlines will show up. <a asp-area="Identity"
|
||||
asp-page="/Account/Login">Sign in</a> to create search outlines!</p>
|
||||
</div>
|
||||
<template x-if="(searchOutlines.length < 2) && (loggedIn)">
|
||||
<div class="text-muted text-center my-3 p-3">
|
||||
<p>Add more search outlines by clicking the <i class="bi bi-plus-lg"></i> above!</p>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-lg px-4">
|
||||
<div class="row">
|
||||
<input class="title-input less-concise mx-4"
|
||||
x-bind:class="searchOutline.ready ? '' : 'invisible'" type="text"
|
||||
x-model="searchOutlines[selectedSearchOutline]" x-on:change="SearchOutlineNameChange"
|
||||
x-bind:disabled="(!loggedIn) || (changingName)">
|
||||
</div>
|
||||
<div class="row justify-content-md-center" x-bind:class="searchOutline.ready ? '' : 'invisible'">
|
||||
<section class="col-md px-3">
|
||||
<h3>Price</h3>
|
||||
<div class="mb-3">
|
||||
<label for="max-price" class="form-label">Maximum Price</label>
|
||||
<div class="input-group">
|
||||
<div class="input-group-text">
|
||||
<input class="form-check-input mt-0" type="checkbox" id="max-price-enabled"
|
||||
x-model="searchOutline.filters.enableUpperPrice"
|
||||
x-on:change="searchOutlineChanged">
|
||||
</div>
|
||||
<span class="input-group-text">$</span>
|
||||
<input type="number" class="form-control" min="0" id="max-price"
|
||||
x-model="searchOutline.filters.upperPrice"
|
||||
x-bind:disabled="!searchOutline.filters.enableUpperPrice"
|
||||
x-on:change="searchOutlineChanged" x-on:input="validateNumericalInputs">
|
||||
<span class="input-group-text">.00</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="min-price" class="form-label">Minimum Price</label>
|
||||
<div class="input-group">
|
||||
<span class="input-group-text">$</span>
|
||||
<input type="number" class="form-control" min="0" id="min-price"
|
||||
x-model="searchOutline.filters.lowerPrice" x-on:change="searchOutlineChanged"
|
||||
x-on:input="validateNumericalInputs">
|
||||
<span class="input-group-text">.00</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="max-shipping" class="form-label">Maximum Shipping Fee</label>
|
||||
<div class="input-group">
|
||||
<div class="input-group-text">
|
||||
<input class="form-check-input mt-0" type="checkbox" id="max-shipping-enabled"
|
||||
x-model="searchOutline.filters.enableMaxShipping"
|
||||
x-on:change="searchOutlineChanged">
|
||||
</div>
|
||||
<span class="input-group-text">$</span>
|
||||
<input type="number" class="form-control" min="0" id="max-shipping"
|
||||
x-model="searchOutline.filters.maxShippingFee"
|
||||
x-bind:disabled="!searchOutline.filters.enableMaxShipping"
|
||||
x-on:change="searchOutlineChanged" x-on:input="validateNumericalInputs">
|
||||
<span class="input-group-text">.00</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" type="checkbox" id="keep-unknown-shipping"
|
||||
x-model="searchOutline.filters.keepUnknownShipping"
|
||||
x-on:change="searchOutlineChanged">
|
||||
<label class="form-check-label" for="keep-unknown-shipping">Keep Unknown
|
||||
Shipping</label>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<section class="col-md px-3">
|
||||
<h3>Metrics</h3>
|
||||
<div class="mb-3">
|
||||
<label for="min-purchases" class="form-label">Minimum Purchases</label>
|
||||
<div class="input-group">
|
||||
<input type="number" class="form-control" min="0" id="min-purchases"
|
||||
x-model="searchOutline.filters.minPurchases" x-on:change="searchOutlineChanged"
|
||||
x-on:input="validateNumericalInputs">
|
||||
<span class="input-group-text">Purchases</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" type="checkbox" id="keep-unknown-purchases"
|
||||
x-model="searchOutline.filters.keepUnknownPurchaseCount"
|
||||
x-on:change="searchOutlineChanged">
|
||||
<label class="form-check-label" for="keep-unknown-purchases">Keep Unknown
|
||||
Purchases</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="min-reviews" class="form-label">Minimum Reviews</label>
|
||||
<div class="input-group">
|
||||
<input type="number" class="form-control" min="0" id="min-reviews"
|
||||
x-model="searchOutline.filters.minReviews" x-on:change="searchOutlineChanged"
|
||||
x-on:input="validateNumericalInputs">
|
||||
<span class="input-group-text">Reviews</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" type="checkbox" id="keep-unknown-reviews"
|
||||
x-model="searchOutline.filters.keepUnknownReviewCount"
|
||||
x-on:change="searchOutlineChanged">
|
||||
<label class="form-check-label" for="keep-unknown-reviews">Keep Unknown Number
|
||||
of
|
||||
Reviews</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mb-1">
|
||||
<label for="min-rating" class="form-label">Minimum Rating</label>
|
||||
<input type="range" class="form-range" id="min-rating" min="0" max="100" step="1"
|
||||
x-model="searchOutline.filters.minRating" x-on:change="searchOutlineChanged">
|
||||
<div id="min-rating-display" class="form-text"
|
||||
x-text="searchOutline.filters.minRating + '%'">
|
||||
</div>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" type="checkbox" id="keep-unrated"
|
||||
x-model="searchOutline.filters.keepUnrated" x-on:change="searchOutlineChanged">
|
||||
<label class="form-check-label" for="keep-unrated">Keep Unrated Items</label>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<section class="col-md px-3">
|
||||
<h3>Shops Enabled</h3>
|
||||
<template x-for="shopName in Object.keys(searchOutline.shopToggles)">
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" type="checkbox" value=""
|
||||
x-bind:id="`${encodeURIComponent(shopName)}-enabled`"
|
||||
x-model="searchOutline.shopToggles[shopName]"
|
||||
x-on:change="searchOutlineChanged">
|
||||
<label class="form-check-label"
|
||||
x-bind:for="`${encodeURIComponent(shopName)}-enabled`" x-text="shopName">
|
||||
</label>
|
||||
</div>
|
||||
</template>
|
||||
</section>
|
||||
</div>
|
||||
<div x-show="!searchOutline.ready">
|
||||
<div class="spinner-border center-overlay" role="status">
|
||||
<span class="visually-hidden">Loading...</span>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
<div class="multipage-slide tab-pane fade" id="info-slide">
|
||||
<div class="multipage-content">
|
||||
<div class="less-concise text-muted flex-grow-1 justify-content-center d-flex flex-column">
|
||||
<h1 class="display-3"><i class="bi bi-info-circle"></i> Get Started!</h1>
|
||||
<ol>
|
||||
@foreach (string instruction in ContentManager.Json.instructions)
|
||||
{
|
||||
<li>@instruction</li>
|
||||
}
|
||||
</ol>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="content-pages" class="multipage mt-3 invisible">
|
||||
<ul class="nav nav-pills selectors">
|
||||
<li class="nav-item" role="presentation">
|
||||
<button type="button" data-bs-toggle="pill" data-bs-target="#quick-picks-slide"><i
|
||||
class="bi bi-stopwatch"></i></button>
|
||||
</li>
|
||||
<li class="nav-item" role="presentation">
|
||||
<button type="button" data-bs-toggle="pill" data-bs-target="#results-slide"><i
|
||||
class="bi bi-view-list"></i></button>
|
||||
</li>
|
||||
<li class="nav-item" role="presentation">
|
||||
<button type="button" data-bs-toggle="pill" data-bs-target="#info-slide"><i
|
||||
class="bi bi-info-lg"></i></button>
|
||||
</li>
|
||||
</ul>
|
||||
<div class="multipage-slides tab-content">
|
||||
<div class="multipage-slide tab-pane fade" id="quick-picks-slide">
|
||||
<div class="multipage-title">
|
||||
<h1 class="display-2"><i class="bi bi-stopwatch"></i> Quick Picks</h1>
|
||||
<template x-if="hasResults()">
|
||||
<p>@ContentManager.Json.quickPicks.searched</p>
|
||||
</template>
|
||||
<template x-if="!hasResults()">
|
||||
<p>@ContentManager.Json.quickPicks.prompt</p>
|
||||
</template>
|
||||
<hr class="less-concise">
|
||||
</div>
|
||||
<div class="multipage-content">
|
||||
<template x-if="hasResults()">
|
||||
<template x-if="results.bestPrice">
|
||||
<p>Here's the listing with the lowest price.</p>
|
||||
<div>
|
||||
@* TODO: Implement best price display here *@
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@* TODO: Add display for top results. *@
|
||||
</template>
|
||||
<template x-if="!hasResults()">
|
||||
<div
|
||||
class="text-center less-concise text-muted flex-grow-1 justify-content-center d-flex flex-column">
|
||||
<h2>@ContentManager.Json.notSearched</h2>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
<div class="multipage-slide tab-pane fade" id="results-slide" x-data>
|
||||
<div class="multipage-title">
|
||||
<h2><i class="bi bi-view-list"></i> Results</h2>
|
||||
<template x-if="hasResults()">
|
||||
<p>@ContentManager.Json.results.searched</p>
|
||||
</template>
|
||||
<template x-if="!hasResults()">
|
||||
<p>@ContentManager.Json.results.prompt</p>
|
||||
</template>
|
||||
<hr class="less-concise">
|
||||
</div>
|
||||
<div class="multipage-content">
|
||||
<template x-if="hasResults()">
|
||||
@* TODO: Display results with UI for sorting and changing views. *@
|
||||
</template>
|
||||
<template x-if="!hasResults()">
|
||||
<div
|
||||
class="text-center less-concise text-muted flex-grow-1 justify-content-center d-flex flex-column">
|
||||
<h2>@ContentManager.Json.notSearched</h2>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
<div class="multipage-slide tab-pane fade" id="info-slide">
|
||||
<div class="multipage-content">
|
||||
<div class="less-concise text-muted flex-grow-1 justify-content-center d-flex flex-column">
|
||||
<h1 class="display-3"><i class="bi bi-info-circle"></i> Get Started!</h1>
|
||||
<ol>
|
||||
@foreach (string instruction in ContentManager.Json.instructions)
|
||||
{
|
||||
<li>@instruction</li>
|
||||
}
|
||||
</ol>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -6,6 +6,7 @@ using System.Threading.Tasks;
|
||||
using Castle.Core.Internal;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Mvc.ModelBinding;
|
||||
using Microsoft.AspNetCore.Mvc.RazorPages;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Props.Data;
|
||||
@ -21,36 +22,5 @@ namespace Props.Pages
|
||||
{
|
||||
[BindProperty(Name = "q", SupportsGet = true)]
|
||||
public string SearchQuery { get; set; }
|
||||
public IEnumerable<ProductListing> SearchResults { get; private set; }
|
||||
public ProductListing BestRatingPriceRatio { get; private set; }
|
||||
public ProductListing TopRated { get; private set; }
|
||||
public ProductListing MostPurchases { get; private set; }
|
||||
public ProductListing MostReviews { get; private set; }
|
||||
public ProductListing BestPrice { get; private set; }
|
||||
|
||||
public ISearchManager SearchManager { get; private set; }
|
||||
private UserManager<ApplicationUser> userManager;
|
||||
private IMetricsManager analytics;
|
||||
public SearchOutline ActiveSearchOutline { get; private set; }
|
||||
|
||||
public SearchModel(ISearchManager searchManager, UserManager<ApplicationUser> userManager, IMetricsManager analyticsManager)
|
||||
{
|
||||
this.SearchManager = searchManager;
|
||||
this.userManager = userManager;
|
||||
this.analytics = analyticsManager;
|
||||
}
|
||||
|
||||
public async Task OnGetAsync()
|
||||
{
|
||||
ActiveSearchOutline = User.Identity.IsAuthenticated ? (await userManager.GetUserAsync(User)).searchOutlinePreferences.ActiveSearchOutline : new SearchOutline();
|
||||
if (string.IsNullOrWhiteSpace(SearchQuery)) return;
|
||||
Console.WriteLine(SearchQuery);
|
||||
this.SearchResults = await SearchManager.Search(SearchQuery, ActiveSearchOutline);
|
||||
BestRatingPriceRatio = (from result in SearchResults orderby result.GetRatingToPriceRatio() descending select result).FirstOrDefault((listing) => listing.GetRatingToPriceRatio() >= 0.5f);
|
||||
TopRated = (from result in SearchResults orderby result.Rating descending select result).FirstOrDefault();
|
||||
MostPurchases = (from result in SearchResults orderby result.PurchaseCount descending select result).FirstOrDefault();
|
||||
MostReviews = (from result in SearchResults orderby result.ReviewCount descending select result).FirstOrDefault();
|
||||
BestPrice = (from result in SearchResults orderby result.UpperPrice descending select result).FirstOrDefault();
|
||||
}
|
||||
}
|
||||
}
|
@ -13,6 +13,6 @@ namespace Props.Services.Modules
|
||||
|
||||
public void RegisterSearchQuery(string query);
|
||||
|
||||
public void RegisterListing(ProductListing productListing, string shopName);
|
||||
public void RegisterProductListing(ProductListing productListing, string shopName);
|
||||
}
|
||||
}
|
@ -1,21 +1,29 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Props.Data;
|
||||
using Props.Models.Search;
|
||||
using Props.Options;
|
||||
using Props.Shop.Framework;
|
||||
|
||||
namespace Props.Services.Modules
|
||||
{
|
||||
public class LiveMetricsManager : IMetricsManager
|
||||
{
|
||||
private MetricsOptions metricsOptions;
|
||||
private ILogger<LiveMetricsManager> logger;
|
||||
ApplicationDbContext dbContext;
|
||||
public LiveMetricsManager(ApplicationDbContext dbContext, ILogger<LiveMetricsManager> logger)
|
||||
private ApplicationDbContext dbContext;
|
||||
private IQueryable<ProductListingInfo> leastPopularProductListings;
|
||||
private IQueryable<QueryWordInfo> leastPopularQueryWords;
|
||||
public LiveMetricsManager(ApplicationDbContext dbContext, ILogger<LiveMetricsManager> logger, IConfiguration configuration)
|
||||
{
|
||||
this.metricsOptions = configuration.GetSection(MetricsOptions.Metrics).Get<MetricsOptions>();
|
||||
this.logger = logger;
|
||||
this.dbContext = dbContext;
|
||||
leastPopularProductListings = from listing in dbContext.ProductListingInfos orderby listing.Hits ascending select listing;
|
||||
leastPopularQueryWords = from word in dbContext.QueryWords orderby word.Hits ascending select word;
|
||||
}
|
||||
public IEnumerable<ProductListingInfo> RetrieveTopListings(int max)
|
||||
{
|
||||
@ -27,8 +35,8 @@ namespace Props.Services.Modules
|
||||
|
||||
public IEnumerable<string> RetrieveCommonKeywords(int max)
|
||||
{
|
||||
if (dbContext.Keywords == null) return null;
|
||||
return (from k in dbContext.Keywords
|
||||
if (dbContext.QueryWords == null) return null;
|
||||
return (from k in dbContext.QueryWords
|
||||
orderby k.Hits descending
|
||||
select k.Word).Take(max);
|
||||
}
|
||||
@ -40,11 +48,11 @@ namespace Props.Services.Modules
|
||||
QueryWordInfo[] wordInfos = new QueryWordInfo[tokens.Length];
|
||||
for (int wordIndex = 0; wordIndex < tokens.Length; wordIndex++)
|
||||
{
|
||||
QueryWordInfo queryWordInfo = dbContext.Keywords.Where((k) => k.Word.ToLower().Equals(tokens[wordIndex])).SingleOrDefault() ?? new QueryWordInfo();
|
||||
QueryWordInfo queryWordInfo = dbContext.QueryWords.Where((k) => k.Word.ToLower().Equals(tokens[wordIndex])).SingleOrDefault() ?? new QueryWordInfo();
|
||||
if (queryWordInfo.Hits == 0)
|
||||
{
|
||||
queryWordInfo.Word = tokens[wordIndex];
|
||||
dbContext.Keywords.Add(queryWordInfo);
|
||||
dbContext.QueryWords.Add(queryWordInfo);
|
||||
}
|
||||
queryWordInfo.Hits += 1;
|
||||
wordInfos[wordIndex] = queryWordInfo;
|
||||
@ -60,11 +68,12 @@ namespace Props.Services.Modules
|
||||
wordInfos[wordIndex].Following.Add(wordInfos[afterIndex]);
|
||||
}
|
||||
}
|
||||
dbContext.SaveChanges();
|
||||
|
||||
CullQueryWords();
|
||||
dbContext.SaveChanges();
|
||||
}
|
||||
|
||||
public void RegisterListing(ProductListing productListing, string shopName)
|
||||
public void RegisterProductListing(ProductListing productListing, string shopName)
|
||||
{
|
||||
ProductListingInfo productListingInfo =
|
||||
(from info in dbContext.ProductListingInfos
|
||||
@ -78,7 +87,27 @@ namespace Props.Services.Modules
|
||||
productListingInfo.ProductListing = productListing;
|
||||
productListingInfo.ProductListingIdentifier = productListing.Identifier;
|
||||
productListingInfo.Hits += 1;
|
||||
|
||||
CullProductListings();
|
||||
dbContext.SaveChanges();
|
||||
}
|
||||
|
||||
private void CullProductListings()
|
||||
{
|
||||
int surplus = dbContext.ProductListingInfos.Count() - metricsOptions.MaxProductListings;
|
||||
if (surplus > 0)
|
||||
{
|
||||
dbContext.RemoveRange(leastPopularProductListings.Take(surplus));
|
||||
}
|
||||
}
|
||||
|
||||
private void CullQueryWords()
|
||||
{
|
||||
int surplus = dbContext.QueryWords.Count() - metricsOptions.MaxQueryWords;
|
||||
if (surplus > 0)
|
||||
{
|
||||
dbContext.RemoveRange(leastPopularQueryWords.Take(surplus));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -34,7 +34,7 @@ namespace Props.Services.Modules
|
||||
|
||||
foreach (string shopName in await ShopManager.GetAllShopNames())
|
||||
{
|
||||
if (searchOutline.Enabled[shopName])
|
||||
if (searchOutline.DisabledShops[shopName])
|
||||
{
|
||||
logger.LogDebug("Checking \"{0}\".", shopName);
|
||||
int amount = 0;
|
||||
@ -43,7 +43,7 @@ namespace Props.Services.Modules
|
||||
if (searchOutline.Filters.Validate(product))
|
||||
{
|
||||
amount += 1;
|
||||
metricsManager.RegisterListing(product, shopName);
|
||||
metricsManager.RegisterProductListing(product, shopName);
|
||||
results.Add(product);
|
||||
}
|
||||
if (amount >= searchOptions.MaxResults) break;
|
||||
|
@ -1,13 +1,5 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Net.Http.Json;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Builder;
|
||||
using Microsoft.AspNetCore.Hosting;
|
||||
using Microsoft.AspNetCore.HttpsPolicy;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.AspNetCore.Identity.UI;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
|
@ -21,5 +21,9 @@
|
||||
"Textual": {
|
||||
"Dir": "./textual"
|
||||
},
|
||||
"Metrics": {
|
||||
"MaxQueryWords": 4096,
|
||||
"MaxProductListings": 1024
|
||||
},
|
||||
"AllowedHosts": "*"
|
||||
}
|
@ -1,3 +1,8 @@
|
||||
import { apiHttp } from "../services/http";
|
||||
import Alpine from "alpinejs";
|
||||
import clone from "just-clone";
|
||||
|
||||
const uploadDelay = 500;
|
||||
const startingSlide = "#quick-picks-slide";
|
||||
|
||||
function initInteractiveElements() {
|
||||
@ -11,14 +16,251 @@ function initInteractiveElements() {
|
||||
});
|
||||
}
|
||||
|
||||
function initConfigVisuals() {
|
||||
const minRatingDisplay = document.querySelector("#configuration #min-rating-display");
|
||||
const minRatingSlider = document.querySelector("#configuration #min-rating");
|
||||
const updateDisplay = function () {
|
||||
minRatingDisplay.innerHTML = `Minimum rating: ${minRatingSlider.value}%`;
|
||||
};
|
||||
minRatingSlider.addEventListener("input", updateDisplay);
|
||||
updateDisplay();
|
||||
function initConfigData() {
|
||||
document.addEventListener("alpine:init", () => {
|
||||
Alpine.data("search", () => ({
|
||||
loggedIn: false,
|
||||
query: "",
|
||||
searchOutline: {
|
||||
ready: false,
|
||||
filters: {
|
||||
"currency": 0,
|
||||
"minRating": 80,
|
||||
"keepUnrated": true,
|
||||
"enableUpperPrice": false,
|
||||
"upperPrice": 0,
|
||||
"lowerPrice": 0,
|
||||
"minPurchases": 0,
|
||||
"keepUnknownPurchaseCount": true,
|
||||
"minReviews": 0,
|
||||
"keepUnknownReviewCount": true,
|
||||
"enableMaxShipping": false,
|
||||
"maxShippingFee": 0,
|
||||
"keepUnknownShipping": true
|
||||
},
|
||||
shopToggles: {},
|
||||
},
|
||||
deletingSearchOutline: false,
|
||||
creatingSearchOutline: false,
|
||||
updatingLastUsed: false,
|
||||
changingName: false,
|
||||
updatingFilters: false,
|
||||
updatingDisabledShops: false,
|
||||
searchOutlines: [],
|
||||
selectedSearchOutline: 0,
|
||||
serverSearchOutlineName: null,
|
||||
resultsQuery: null,
|
||||
results: {
|
||||
bestPrice: null,
|
||||
},
|
||||
searchOutlineChangeTimeout: null,
|
||||
hasResults() {
|
||||
return this.resultsQuery !== null;
|
||||
},
|
||||
submitSearch() {
|
||||
// TODO: implement search Web API call.
|
||||
this.resultsQuery = this.query;
|
||||
console.log("Search requested.");
|
||||
},
|
||||
SearchOutlineNameChange() {
|
||||
if (this.validateSearchOutlineName()) {
|
||||
this.changeSearchOutlineName(this.serverSearchOutlineName, this.searchOutlines[this.selectedSearchOutline]);
|
||||
}
|
||||
},
|
||||
validateNumericalInputs() {
|
||||
if (!this.searchOutline.filters.lowerPrice) this.searchOutline.filters.lowerPrice = 0;
|
||||
if (!this.searchOutline.filters.upperPrice) this.searchOutline.filters.upperPrice = 0;
|
||||
if (!this.searchOutline.filters.maxShippingFee) this.searchOutline.filters.maxShippingFee = 0;
|
||||
if (!this.searchOutline.filters.minPurchases) this.searchOutline.filters.minPurchases = 0;
|
||||
if (!this.searchOutline.filters.minRating) this.searchOutline.filters.minRating = 0;
|
||||
if (!this.searchOutline.filters.minReviews) this.searchOutline.filters.minReviews = 0;
|
||||
},
|
||||
validateSearchOutlineName() {
|
||||
let clonedSearchOutlines = this.searchOutlines.slice();
|
||||
clonedSearchOutlines.splice(this.selectedSearchOutline, 1);
|
||||
if (this.searchOutlines[this.selectedSearchOutline].length < 1 || clonedSearchOutlines.includes(this.searchOutlines[this.selectedSearchOutline])) {
|
||||
this.searchOutlines[this.selectedSearchOutline] = this.serverSearchOutlineName;
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
},
|
||||
searchOutlineChanged() {
|
||||
if (!this.loggedIn) return;
|
||||
if (this.searchOutlineChangeTimeout != null) {
|
||||
clearTimeout(this.searchOutlineChangeTimeout);
|
||||
}
|
||||
this.searchOutlineChangeTimeout = setTimeout(() => {
|
||||
this.uploadAll();
|
||||
}, uploadDelay);
|
||||
},
|
||||
uploadAll() {
|
||||
let name = this.searchOutlines[this.selectedSearchOutline];
|
||||
this.uploadFilters(name, clone(this.searchOutline.filters));
|
||||
this.uploadDisabledShops(name, clone(this.searchOutline.shopToggles));
|
||||
},
|
||||
async uploadFilters(name, filters) {
|
||||
if (!this.loggedIn) return;
|
||||
this.updatingFilters = true;
|
||||
let uploadFilterResponse = await apiHttp.put(`SearchOutline/${name}/Filters`, filters);
|
||||
this.updatingFilters = false;
|
||||
if (uploadFilterResponse.status != 204) {
|
||||
throw `Error while attempting to upload filters. Response code ${uploadFilterResponse.status} (expected 204).`;
|
||||
}
|
||||
},
|
||||
async uploadDisabledShops(name, disabledShops) {
|
||||
if (!this.loggedIn) return;
|
||||
this.updatingDisabledShops = true;
|
||||
let disabledShopSet = [];
|
||||
Object.keys(disabledShops).forEach(key => {
|
||||
if (!this.searchOutline.shopToggles[key]) {
|
||||
disabledShopSet.push(key);
|
||||
}
|
||||
});
|
||||
let uploadDisabledShopsResponse = await apiHttp.put(`SearchOutline/${name}/DisabledShops`, disabledShopSet);
|
||||
this.updatingDisabledShops = false;
|
||||
if (uploadDisabledShopsResponse.status != 204) {
|
||||
throw `Error while attempting to upload disabled shops. Response code ${uploadDisabledShopsResponse.status} (expected 204).`;
|
||||
}
|
||||
},
|
||||
async createSearchOutline(name) {
|
||||
if (!this.loggedIn) return;
|
||||
this.creatingSearchOutline = true;
|
||||
let createRequest = await apiHttp.post("SearchOutline/" + name);
|
||||
this.creatingSearchOutline = false;
|
||||
if (createRequest.status != 204) {
|
||||
throw `Could not create profile. Response code ${createRequest.status} (expected 204).`;
|
||||
}
|
||||
this.searchOutlines.push(name);
|
||||
},
|
||||
async updateLastUsed(name) {
|
||||
if (!this.loggedIn) return;
|
||||
this.updatingLastUsed = true;
|
||||
let lastUsedRequest = await apiHttp.put("SearchOutline/" + name + "/LastUsed");
|
||||
this.updatingLastUsed = false;
|
||||
if (lastUsedRequest.status != 204) {
|
||||
throw `Could not update last used search outline. Received status code ${lastUsedRequest.status} (expected 204).`;
|
||||
}
|
||||
},
|
||||
async changeSearchOutlineName(old, current) {
|
||||
if (!this.loggedIn) return;
|
||||
this.changingName = true;
|
||||
let nameChangeRequest = await apiHttp.put(`SearchOutline/${old}/Name/${current}`);
|
||||
this.changingName = false;
|
||||
if (nameChangeRequest.status != 204) {
|
||||
throw `Could not update name on server side. Received ${nameChangeRequest.status} (expected 204).`;
|
||||
}
|
||||
this.serverSearchOutlineName = current;
|
||||
},
|
||||
async loadSearchOutline(name) {
|
||||
this.searchOutline.ready = false;
|
||||
if (!this.loggedIn) {
|
||||
let defaultNameRequest = await apiHttp.get("SearchOutline/DefaultName");
|
||||
if (defaultNameRequest.status != 200) {
|
||||
console.error(`Could not load default search outline name. Got response code ${defaultNameRequest.status} (Expected 200).`);
|
||||
return;
|
||||
}
|
||||
this.searchOutlines.push(defaultNameRequest.data);
|
||||
let disabledShopsResponse = await apiHttp.get("SearchOutline/DefaultDisabledShops");
|
||||
let availableShops = (await apiHttp.get("Search/AvailableShops")).data;
|
||||
if (disabledShopsResponse.status == 200) {
|
||||
availableShops.forEach(shopName => {
|
||||
this.searchOutline.shopToggles[shopName] = !disabledShopsResponse.data.includes(shopName);
|
||||
});
|
||||
} else {
|
||||
console.error(`Could not fetch default disabled shops for "${name}". Status code: ${disabledShopsResponse.status} (Expected 200)`);
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
if (this.searchOutlineChangeTimeout != null) {
|
||||
clearTimeout(this.searchOutlineChangeTimeout);
|
||||
this.uploadAll();
|
||||
}
|
||||
let filterResponse = await apiHttp.get("SearchOutline/" + name + "/Filters");
|
||||
if (filterResponse.status == 200) {
|
||||
this.searchOutline.filters = filterResponse.data;
|
||||
} else {
|
||||
console.error(`Could not fetch filter for "${name}". Status code: ${filterResponse.status} (Expected 200)`);
|
||||
return;
|
||||
}
|
||||
|
||||
let disabledShopsResponse = await apiHttp.get("SearchOutline/" + name + "/DisabledShops");
|
||||
let availableShops = (await apiHttp.get("Search/AvailableShops")).data;
|
||||
if (disabledShopsResponse.status == 200) {
|
||||
availableShops.forEach(shopName => {
|
||||
this.searchOutline.shopToggles[shopName] = !disabledShopsResponse.data.includes(shopName);
|
||||
});
|
||||
} else {
|
||||
console.error(`Could not fetch disabled shops for "${name}". Status code: ${disabledShopsResponse.status} (Expected 200)`);
|
||||
return;
|
||||
}
|
||||
await this.updateLastUsed(name);
|
||||
}
|
||||
this.serverSearchOutlineName = name;
|
||||
this.searchOutline.ready = true;
|
||||
},
|
||||
createSearchOutlineWithGeneratedName() {
|
||||
apiHttp.get("SearchOutline/DefaultName/").then((response) => {
|
||||
if (response.status != 200) {
|
||||
throw `Could not get a default name. Response code ${response.status} (expected 200).`;
|
||||
}
|
||||
this.createSearchOutline(response.data);
|
||||
});
|
||||
},
|
||||
deleteSearchOutline(name) {
|
||||
this.deletingSearchOutline = true;
|
||||
let beforeDelete = this.searchOutlines[this.selectedSearchOutline];
|
||||
if (this.selectedSearchOutline == this.searchOutlines.length - 1 && this.searchOutlines.indexOf(name) <= this.selectedSearchOutline) {
|
||||
this.selectedSearchOutline -= 1;
|
||||
}
|
||||
apiHttp.delete(`SearchOutline/${name}`).then((results) => {
|
||||
this.deletingSearchOutline = false;
|
||||
if (results.status != 204) {
|
||||
throw `Unable to delete ${name}. Received status ${results.status} (expected 204)`;
|
||||
}
|
||||
this.searchOutlines.splice(this.searchOutlines.indexOf(name), 1);
|
||||
if (beforeDelete !== this.searchOutlines[this.selectedSearchOutline]) {
|
||||
this.loadSearchOutline(this.searchOutlines[this.selectedSearchOutline]).then(() => {
|
||||
this.deletingSearchOutline = false;
|
||||
});
|
||||
} else {
|
||||
this.deletingSearchOutline = false;
|
||||
}
|
||||
});
|
||||
},
|
||||
async init() {
|
||||
// TODO: Test logged in outline sequence and logged out outline sequence.
|
||||
this.loggedIn = (await apiHttp.get("User/LoggedIn")).data;
|
||||
if (this.loggedIn) {
|
||||
this.searchOutlines = (await apiHttp.get("SearchOutline/Names")).data;
|
||||
if (this.searchOutlines.length == 0) {
|
||||
let name = (await apiHttp.get("SearchOutline/DefaultName")).data;
|
||||
try {
|
||||
await this.createSearchOutline(name);
|
||||
await this.updateLastUsed(name);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
let lastUsedRequest = await apiHttp.get("SearchOutline/LastUsed");
|
||||
if (lastUsedRequest.status == 200) {
|
||||
this.selectedSearchOutline = this.searchOutlines.indexOf(lastUsedRequest.data);
|
||||
} else {
|
||||
console.warn(`Could not load name of last used search outline. Got response code ${lastUsedRequest.status} (Expected 200). Using "${this.searchOutlines[0]}".`);
|
||||
let putlastUsedRequest = await apiHttp.put("SearchOutline/" + this.searchOutlines[this.selectedSearchOutline] + "/LastUsed");
|
||||
if (putlastUsedRequest.status != 204) {
|
||||
console.error(`Could not update last used search outline. Received status code ${putlastUsedRequest.status} (Expected 204).`);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
this.loadSearchOutline(this.searchOutlines[this.selectedSearchOutline]);
|
||||
}
|
||||
}));
|
||||
});
|
||||
|
||||
Alpine.start();
|
||||
}
|
||||
|
||||
function initSlides() {
|
||||
@ -46,8 +288,8 @@ function initSlides() {
|
||||
|
||||
async function main() {
|
||||
initInteractiveElements();
|
||||
initConfigVisuals();
|
||||
initSlides();
|
||||
initConfigData();
|
||||
}
|
||||
|
||||
main();
|
||||
|
@ -37,17 +37,6 @@ main {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
.tear {
|
||||
border-top: 1px;
|
||||
border-top-style: solid;
|
||||
border-bottom: 1px;
|
||||
border-bottom-style: solid;
|
||||
@include themer.themed {
|
||||
$tear: themer.color-of("background");
|
||||
border-color: adjust-color($color: $tear, $lightness: -12%, $alpha: 1.0);
|
||||
background-color: adjust-color($color: $tear, $lightness: -5%, $alpha: 1.0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
footer {
|
||||
@ -82,16 +71,24 @@ footer {
|
||||
}
|
||||
}
|
||||
|
||||
.center-overlay {
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
right: 50%;
|
||||
bottom: 50%;
|
||||
top: 50%;
|
||||
}
|
||||
|
||||
.concise {
|
||||
@extend .container;
|
||||
max-width: 630px;
|
||||
width: inherit;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.less-concise {
|
||||
@extend .container;
|
||||
max-width: 720px;
|
||||
width: inherit;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
hr {
|
||||
@ -111,6 +108,115 @@ hr {
|
||||
}
|
||||
}
|
||||
|
||||
.tear {
|
||||
@include themer.themed {
|
||||
$tear: themer.color-of("background");
|
||||
background-color: adjust-color($color: $tear, $lightness: -8%, $alpha: 1.0);
|
||||
box-shadow: 0px 0px 5px 0px adjust-color($color: $tear, $lightness: -25%, $alpha: 1.0) inset;
|
||||
}
|
||||
}
|
||||
|
||||
input[type="text"].title-input {
|
||||
@include themer.themed {
|
||||
color: themer.color-of("text");
|
||||
}
|
||||
font-size: 2.5em;
|
||||
margin-bottom: 1.5rem;
|
||||
border-style: none;
|
||||
|
||||
border-bottom: 1px solid transparent;
|
||||
|
||||
background-color: transparent;
|
||||
text-align: center;
|
||||
transition: border-bottom-color 0.5s;
|
||||
|
||||
&:hover:not(:disabled) {
|
||||
@include themer.themed {
|
||||
border-bottom-color: adjust-color($color: themer.color-of("text"), $lightness: 50%);
|
||||
}
|
||||
}
|
||||
|
||||
&:focus:not(:disabled) {
|
||||
outline: none;
|
||||
@include themer.themed {
|
||||
border-bottom-color: adjust-color($color: themer.color-of("text"));
|
||||
}
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
border-bottom-color: transparent;
|
||||
@include themer.themed {
|
||||
color: themer.color-of("text");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.clean-radio {
|
||||
input[type="radio"] {
|
||||
display: none;
|
||||
& + label {
|
||||
transition: border-color 0.5s;
|
||||
padding: 0.5rem;
|
||||
border-left: 3px solid;
|
||||
@include themer.themed {
|
||||
border-color: themer.color-of("text");
|
||||
}
|
||||
cursor: pointer;
|
||||
}
|
||||
&:hover + label {
|
||||
@include themer.themed {
|
||||
border-color: adjust-color($color: themer.color-of("special"), $lightness: 30%);
|
||||
}
|
||||
}
|
||||
&:checked + label {
|
||||
@include themer.themed {
|
||||
border-color: adjust-color($color: themer.color-of("special"), $lightness: 0%);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
button.btn {
|
||||
transition: color 0.5s;
|
||||
color: transparent;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
button.btn {
|
||||
@include themer.themed {
|
||||
color: themer.color-of("text");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.border-right-themed {
|
||||
border-right: 1px solid;
|
||||
@include themer.themed {
|
||||
border-right-color: themer.color-of("text");
|
||||
}
|
||||
}
|
||||
|
||||
.border-left-themed {
|
||||
border-left: 1px solid;
|
||||
@include themer.themed {
|
||||
border-left-color: themer.color-of("text");
|
||||
}
|
||||
}
|
||||
|
||||
.border-top-themed {
|
||||
border-top: 1px solid;
|
||||
@include themer.themed {
|
||||
border-top-color: themer.color-of("text");
|
||||
}
|
||||
}
|
||||
|
||||
.border-bottom-themed {
|
||||
border-bottom: 1px solid;
|
||||
@include themer.themed {
|
||||
border-bottom-color: themer.color-of("text");
|
||||
}
|
||||
}
|
||||
|
||||
html {
|
||||
min-height: 100%;
|
||||
display: flex;
|
||||
|
10
Props/package-lock.json
generated
10
Props/package-lock.json
generated
@ -2773,6 +2773,11 @@
|
||||
"minimist": "^1.2.5"
|
||||
}
|
||||
},
|
||||
"just-clone": {
|
||||
"version": "3.2.1",
|
||||
"resolved": "https://registry.npmjs.org/just-clone/-/just-clone-3.2.1.tgz",
|
||||
"integrity": "sha512-PFotEVrrzAnwuWTUOFquDShWrHnUnhxNrVs1VFqkNfnoH3Sn5XUlDOePYn2Vv5cN8xV2y69jf8qEoQHm7eoLnw=="
|
||||
},
|
||||
"kind-of": {
|
||||
"version": "6.0.3",
|
||||
"resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz",
|
||||
@ -3012,6 +3017,11 @@
|
||||
"object-keys": "^1.1.1"
|
||||
}
|
||||
},
|
||||
"on-change": {
|
||||
"version": "3.0.2",
|
||||
"resolved": "https://registry.npmjs.org/on-change/-/on-change-3.0.2.tgz",
|
||||
"integrity": "sha512-rdt5YfIfo86aFNwvQqzzHMpaPPyVQ/XjcGK01d46chZh47G8Xzvoao79SgFb03GZfxRGREzNQVJuo31drqyIlA=="
|
||||
},
|
||||
"once": {
|
||||
"version": "1.4.0",
|
||||
"resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
|
||||
|
@ -34,6 +34,8 @@
|
||||
"axios": "^0.21.1",
|
||||
"bootstrap": "^5.0.2",
|
||||
"bootstrap-icons": "^1.5.0",
|
||||
"just-clone": "^3.2.1",
|
||||
"on-change": "^3.0.2",
|
||||
"simplebar": "^5.3.5"
|
||||
}
|
||||
}
|
||||
|
@ -5,7 +5,7 @@
|
||||
"That's it! Hit search and we'll look far and wide for what you need!"
|
||||
],
|
||||
"quickPicks": {
|
||||
"searched": "To save you some time, these are some of the better options we found!",
|
||||
"searched": "The cream of the crop!",
|
||||
"prompt": "This is where we'll show you top listings so you can get back to working on your project!"
|
||||
},
|
||||
"results": {
|
||||
|
Loading…
Reference in New Issue
Block a user