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)
|
if (workspaceDir != null)
|
||||||
{
|
{
|
||||||
logger.LogDebug("Saving data in \"{0}\"...", workspaceDir);
|
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));
|
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));
|
await JsonSerializer.SerializeAsync(fileStream, new ProductListingCacheData(await searchManager.ProductListingManager.ProductListings));
|
||||||
}
|
}
|
||||||
|
@ -5,12 +5,12 @@ namespace Props.Shop.Framework
|
|||||||
public class Filters
|
public class Filters
|
||||||
{
|
{
|
||||||
public Currency Currency { get; set; } = Currency.CAD;
|
public Currency Currency { get; set; } = Currency.CAD;
|
||||||
private float minRatingNormalized;
|
private float minRatingNormalized = 0.8f;
|
||||||
public int MinRating
|
public int MinRating
|
||||||
{
|
{
|
||||||
get
|
get
|
||||||
{
|
{
|
||||||
return (int)(minRatingNormalized * 100);
|
return (int)(minRatingNormalized * 100f);
|
||||||
}
|
}
|
||||||
set
|
set
|
||||||
{
|
{
|
||||||
@ -38,7 +38,7 @@ namespace Props.Shop.Framework
|
|||||||
public bool KeepUnknownPurchaseCount { get; set; } = true;
|
public bool KeepUnknownPurchaseCount { get; set; } = true;
|
||||||
public int MinReviews { get; set; }
|
public int MinReviews { get; set; }
|
||||||
public bool KeepUnknownReviewCount { get; set; } = true;
|
public bool KeepUnknownReviewCount { get; set; } = true;
|
||||||
public bool EnableMaxShippingFee { get; set; }
|
public bool EnableMaxShipping { get; set; }
|
||||||
private int maxShippingFee;
|
private int maxShippingFee;
|
||||||
|
|
||||||
public int MaxShippingFee
|
public int MaxShippingFee
|
||||||
@ -49,7 +49,7 @@ namespace Props.Shop.Framework
|
|||||||
}
|
}
|
||||||
set
|
set
|
||||||
{
|
{
|
||||||
if (EnableMaxShippingFee) maxShippingFee = value;
|
if (EnableMaxShipping) maxShippingFee = value;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
public bool KeepUnknownShipping { get; set; } = true;
|
public bool KeepUnknownShipping { get; set; } = true;
|
||||||
@ -72,7 +72,7 @@ namespace Props.Shop.Framework
|
|||||||
KeepUnknownPurchaseCount == other.KeepUnknownPurchaseCount &&
|
KeepUnknownPurchaseCount == other.KeepUnknownPurchaseCount &&
|
||||||
MinReviews == other.MinReviews &&
|
MinReviews == other.MinReviews &&
|
||||||
KeepUnknownReviewCount == other.KeepUnknownReviewCount &&
|
KeepUnknownReviewCount == other.KeepUnknownReviewCount &&
|
||||||
EnableMaxShippingFee == other.EnableMaxShippingFee &&
|
EnableMaxShipping == other.EnableMaxShipping &&
|
||||||
MaxShippingFee == other.MaxShippingFee &&
|
MaxShippingFee == other.MaxShippingFee &&
|
||||||
KeepUnknownShipping == other.KeepUnknownShipping;
|
KeepUnknownShipping == other.KeepUnknownShipping;
|
||||||
|
|
||||||
@ -96,7 +96,7 @@ namespace Props.Shop.Framework
|
|||||||
|
|
||||||
public bool Validate(ProductListing listing)
|
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;
|
float shippingDifference = listing.Shipping != null ? listing.Shipping.Value : 0;
|
||||||
if (!(listing.LowerPrice + shippingDifference >= LowerPrice && (!EnableUpperPrice || listing.UpperPrice + shippingDifference <= UpperPrice))) return false;
|
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;
|
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 Microsoft.AspNetCore.Mvc;
|
||||||
using Props.Models.Search;
|
using Props.Models.Search;
|
||||||
using Props.Services.Modules;
|
using Props.Services.Modules;
|
||||||
|
using Props.Shop.Framework;
|
||||||
|
|
||||||
namespace Props.Controllers
|
namespace Props.Controllers
|
||||||
{
|
{
|
||||||
public class SearchController : ApiControllerBase
|
public class SearchController : ApiControllerBase
|
||||||
{
|
{
|
||||||
private SearchOutline defaultOutline = new SearchOutline();
|
private SearchOutline defaultOutline = new SearchOutline();
|
||||||
IShopManager shopManager;
|
ISearchManager searchManager;
|
||||||
|
|
||||||
public SearchController(IShopManager shopManager)
|
public SearchController(ISearchManager searchManager)
|
||||||
{
|
{
|
||||||
this.shopManager = shopManager;
|
this.searchManager = searchManager;
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpGet]
|
[HttpGet]
|
||||||
[Route("Available")]
|
[Route("AvailableShops")]
|
||||||
public IActionResult GetAvailableShops()
|
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 Microsoft.AspNetCore.Mvc;
|
||||||
|
using Props.Data;
|
||||||
using Props.Models.Search;
|
using Props.Models.Search;
|
||||||
using Props.Services.Modules;
|
using Props.Models.User;
|
||||||
|
using Props.Shop.Framework;
|
||||||
|
|
||||||
namespace Props.Controllers
|
namespace Props.Controllers
|
||||||
{
|
{
|
||||||
public class SearchOutlineController : ApiControllerBase
|
public class SearchOutlineController : ApiControllerBase
|
||||||
{
|
{
|
||||||
private SearchOutline defaultOutline = new SearchOutline();
|
private ApplicationDbContext dbContext;
|
||||||
|
private UserManager<ApplicationUser> userManager;
|
||||||
|
public SearchOutlineController(UserManager<ApplicationUser> userManager, ApplicationDbContext dbContext)
|
||||||
public SearchOutlineController()
|
|
||||||
{
|
{
|
||||||
|
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]
|
[HttpGet]
|
||||||
[Route("Filters")]
|
[Authorize]
|
||||||
public IActionResult GetFilters()
|
[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]
|
[HttpGet]
|
||||||
[Route("DisabledShops")]
|
[Authorize]
|
||||||
public IActionResult GetDisabledShops()
|
[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]
|
[HttpGet]
|
||||||
[Route("SearchOutlineName")]
|
[Authorize]
|
||||||
public IActionResult GetSearchOutlineName()
|
[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 class ApplicationDbContext : IdentityDbContext<ApplicationUser>
|
||||||
{
|
{
|
||||||
public DbSet<QueryWordInfo> Keywords { get; set; }
|
public DbSet<QueryWordInfo> QueryWords { get; set; }
|
||||||
|
|
||||||
public DbSet<ProductListingInfo> ProductListingInfos { get; set; }
|
public DbSet<ProductListingInfo> ProductListingInfos { get; set; }
|
||||||
|
|
||||||
@ -43,14 +43,14 @@ namespace Props.Data
|
|||||||
);
|
);
|
||||||
|
|
||||||
modelBuilder.Entity<SearchOutline>()
|
modelBuilder.Entity<SearchOutline>()
|
||||||
.Property(e => e.Enabled)
|
.Property(e => e.DisabledShops)
|
||||||
.HasConversion(
|
.HasConversion(
|
||||||
v => JsonSerializer.Serialize(v, null),
|
v => JsonSerializer.Serialize(v, null),
|
||||||
v => JsonSerializer.Deserialize<SearchOutline.ShopsDisabled>(v, null),
|
v => JsonSerializer.Deserialize<SearchOutline.ShopSelector>(v, null),
|
||||||
new ValueComparer<SearchOutline.ShopsDisabled>(
|
new ValueComparer<SearchOutline.ShopSelector>(
|
||||||
(a, b) => a.Equals(b),
|
(a, b) => a.Equals(b),
|
||||||
c => c.GetHashCode(),
|
c => c.GetHashCode(),
|
||||||
c => c.Copy()
|
c => new SearchOutline.ShopSelector(c)
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -9,7 +9,7 @@ using Props.Data;
|
|||||||
namespace Props.Data.Migrations
|
namespace Props.Data.Migrations
|
||||||
{
|
{
|
||||||
[DbContext(typeof(ApplicationDbContext))]
|
[DbContext(typeof(ApplicationDbContext))]
|
||||||
[Migration("20210809194646_InitialCreate")]
|
[Migration("20210817042955_InitialCreate")]
|
||||||
partial class InitialCreate
|
partial class InitialCreate
|
||||||
{
|
{
|
||||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||||
@ -165,7 +165,6 @@ namespace Props.Data.Migrations
|
|||||||
.HasColumnType("TEXT");
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
b.Property<string>("ProfileName")
|
b.Property<string>("ProfileName")
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("TEXT");
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
b.HasKey("Id");
|
b.HasKey("Id");
|
||||||
@ -214,7 +213,7 @@ namespace Props.Data.Migrations
|
|||||||
|
|
||||||
b.HasKey("Id");
|
b.HasKey("Id");
|
||||||
|
|
||||||
b.ToTable("Keywords");
|
b.ToTable("QueryWords");
|
||||||
});
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("Props.Models.Search.SearchOutline", b =>
|
modelBuilder.Entity("Props.Models.Search.SearchOutline", b =>
|
||||||
@ -223,11 +222,7 @@ namespace Props.Data.Migrations
|
|||||||
.ValueGeneratedOnAdd()
|
.ValueGeneratedOnAdd()
|
||||||
.HasColumnType("INTEGER");
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
b.Property<string>("ApplicationUserId")
|
b.Property<string>("DisabledShops")
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("TEXT");
|
|
||||||
|
|
||||||
b.Property<string>("Enabled")
|
|
||||||
.IsRequired()
|
.IsRequired()
|
||||||
.HasColumnType("TEXT");
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
@ -238,13 +233,11 @@ namespace Props.Data.Migrations
|
|||||||
.IsRequired()
|
.IsRequired()
|
||||||
.HasColumnType("TEXT");
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
b.Property<int?>("SearchOutlinePreferencesId")
|
b.Property<int>("SearchOutlinePreferencesId")
|
||||||
.HasColumnType("INTEGER");
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
b.HasKey("Id");
|
b.HasKey("Id");
|
||||||
|
|
||||||
b.HasIndex("ApplicationUserId");
|
|
||||||
|
|
||||||
b.HasIndex("SearchOutlinePreferencesId");
|
b.HasIndex("SearchOutlinePreferencesId");
|
||||||
|
|
||||||
b.ToTable("SearchOutline");
|
b.ToTable("SearchOutline");
|
||||||
@ -320,16 +313,14 @@ namespace Props.Data.Migrations
|
|||||||
.ValueGeneratedOnAdd()
|
.ValueGeneratedOnAdd()
|
||||||
.HasColumnType("INTEGER");
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
b.Property<int?>("ActiveSearchOutlineId")
|
|
||||||
.HasColumnType("INTEGER");
|
|
||||||
|
|
||||||
b.Property<string>("ApplicationUserId")
|
b.Property<string>("ApplicationUserId")
|
||||||
.IsRequired()
|
.IsRequired()
|
||||||
.HasColumnType("TEXT");
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
b.HasKey("Id");
|
b.Property<string>("NameOfLastUsed")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
b.HasIndex("ActiveSearchOutlineId");
|
b.HasKey("Id");
|
||||||
|
|
||||||
b.HasIndex("ApplicationUserId")
|
b.HasIndex("ApplicationUserId")
|
||||||
.IsUnique();
|
.IsUnique();
|
||||||
@ -440,33 +431,23 @@ namespace Props.Data.Migrations
|
|||||||
|
|
||||||
modelBuilder.Entity("Props.Models.Search.SearchOutline", b =>
|
modelBuilder.Entity("Props.Models.Search.SearchOutline", b =>
|
||||||
{
|
{
|
||||||
b.HasOne("Props.Models.User.ApplicationUser", "ApplicationUser")
|
b.HasOne("Props.Models.User.SearchOutlinePreferences", "SearchOutlinePreferences")
|
||||||
.WithMany()
|
.WithMany("SearchOutlines")
|
||||||
.HasForeignKey("ApplicationUserId")
|
.HasForeignKey("SearchOutlinePreferencesId")
|
||||||
.OnDelete(DeleteBehavior.Cascade)
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
.IsRequired();
|
.IsRequired();
|
||||||
|
|
||||||
b.HasOne("Props.Models.User.SearchOutlinePreferences", null)
|
b.Navigation("SearchOutlinePreferences");
|
||||||
.WithMany("SearchOutlines")
|
|
||||||
.HasForeignKey("SearchOutlinePreferencesId");
|
|
||||||
|
|
||||||
b.Navigation("ApplicationUser");
|
|
||||||
});
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("Props.Models.User.SearchOutlinePreferences", b =>
|
modelBuilder.Entity("Props.Models.User.SearchOutlinePreferences", b =>
|
||||||
{
|
{
|
||||||
b.HasOne("Props.Models.Search.SearchOutline", "ActiveSearchOutline")
|
|
||||||
.WithMany()
|
|
||||||
.HasForeignKey("ActiveSearchOutlineId");
|
|
||||||
|
|
||||||
b.HasOne("Props.Models.User.ApplicationUser", "ApplicationUser")
|
b.HasOne("Props.Models.User.ApplicationUser", "ApplicationUser")
|
||||||
.WithOne("searchOutlinePreferences")
|
.WithOne("searchOutlinePreferences")
|
||||||
.HasForeignKey("Props.Models.User.SearchOutlinePreferences", "ApplicationUserId")
|
.HasForeignKey("Props.Models.User.SearchOutlinePreferences", "ApplicationUserId")
|
||||||
.OnDelete(DeleteBehavior.Cascade)
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
.IsRequired();
|
.IsRequired();
|
||||||
|
|
||||||
b.Navigation("ActiveSearchOutline");
|
|
||||||
|
|
||||||
b.Navigation("ApplicationUser");
|
b.Navigation("ApplicationUser");
|
||||||
});
|
});
|
||||||
|
|
@ -46,20 +46,6 @@ namespace Props.Data.Migrations
|
|||||||
table.PrimaryKey("PK_AspNetUsers", x => x.Id);
|
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(
|
migrationBuilder.CreateTable(
|
||||||
name: "ProductListingInfos",
|
name: "ProductListingInfos",
|
||||||
columns: table => new
|
columns: table => new
|
||||||
@ -76,6 +62,20 @@ namespace Props.Data.Migrations
|
|||||||
table.PrimaryKey("PK_ProductListingInfos", x => x.Id);
|
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(
|
migrationBuilder.CreateTable(
|
||||||
name: "AspNetRoleClaims",
|
name: "AspNetRoleClaims",
|
||||||
columns: table => new
|
columns: table => new
|
||||||
@ -211,7 +211,7 @@ namespace Props.Data.Migrations
|
|||||||
.Annotation("Sqlite:Autoincrement", true),
|
.Annotation("Sqlite:Autoincrement", true),
|
||||||
ApplicationUserId = table.Column<string>(type: "TEXT", nullable: false),
|
ApplicationUserId = table.Column<string>(type: "TEXT", nullable: false),
|
||||||
Order = 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 =>
|
constraints: table =>
|
||||||
{
|
{
|
||||||
@ -224,6 +224,26 @@ namespace Props.Data.Migrations
|
|||||||
onDelete: ReferentialAction.Cascade);
|
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(
|
migrationBuilder.CreateTable(
|
||||||
name: "QueryWordInfoQueryWordInfo",
|
name: "QueryWordInfoQueryWordInfo",
|
||||||
columns: table => new
|
columns: table => new
|
||||||
@ -235,15 +255,15 @@ namespace Props.Data.Migrations
|
|||||||
{
|
{
|
||||||
table.PrimaryKey("PK_QueryWordInfoQueryWordInfo", x => new { x.FollowingId, x.PrecedingId });
|
table.PrimaryKey("PK_QueryWordInfoQueryWordInfo", x => new { x.FollowingId, x.PrecedingId });
|
||||||
table.ForeignKey(
|
table.ForeignKey(
|
||||||
name: "FK_QueryWordInfoQueryWordInfo_Keywords_FollowingId",
|
name: "FK_QueryWordInfoQueryWordInfo_QueryWords_FollowingId",
|
||||||
column: x => x.FollowingId,
|
column: x => x.FollowingId,
|
||||||
principalTable: "Keywords",
|
principalTable: "QueryWords",
|
||||||
principalColumn: "Id",
|
principalColumn: "Id",
|
||||||
onDelete: ReferentialAction.Cascade);
|
onDelete: ReferentialAction.Cascade);
|
||||||
table.ForeignKey(
|
table.ForeignKey(
|
||||||
name: "FK_QueryWordInfoQueryWordInfo_Keywords_PrecedingId",
|
name: "FK_QueryWordInfoQueryWordInfo_QueryWords_PrecedingId",
|
||||||
column: x => x.PrecedingId,
|
column: x => x.PrecedingId,
|
||||||
principalTable: "Keywords",
|
principalTable: "QueryWords",
|
||||||
principalColumn: "Id",
|
principalColumn: "Id",
|
||||||
onDelete: ReferentialAction.Cascade);
|
onDelete: ReferentialAction.Cascade);
|
||||||
});
|
});
|
||||||
@ -254,49 +274,22 @@ namespace Props.Data.Migrations
|
|||||||
{
|
{
|
||||||
Id = table.Column<int>(type: "INTEGER", nullable: false)
|
Id = table.Column<int>(type: "INTEGER", nullable: false)
|
||||||
.Annotation("Sqlite:Autoincrement", true),
|
.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),
|
Name = table.Column<string>(type: "TEXT", nullable: false),
|
||||||
Filters = table.Column<string>(type: "TEXT", nullable: true),
|
Filters = table.Column<string>(type: "TEXT", nullable: true),
|
||||||
Enabled = table.Column<string>(type: "TEXT", nullable: false),
|
DisabledShops = table.Column<string>(type: "TEXT", nullable: false)
|
||||||
SearchOutlinePreferencesId = table.Column<int>(type: "INTEGER", nullable: true)
|
|
||||||
},
|
},
|
||||||
constraints: table =>
|
constraints: table =>
|
||||||
{
|
{
|
||||||
table.PrimaryKey("PK_SearchOutline", x => x.Id);
|
table.PrimaryKey("PK_SearchOutline", x => x.Id);
|
||||||
table.ForeignKey(
|
table.ForeignKey(
|
||||||
name: "FK_SearchOutline_AspNetUsers_ApplicationUserId",
|
name: "FK_SearchOutline_SearchOutlinePreferences_SearchOutlinePreferencesId",
|
||||||
column: x => x.ApplicationUserId,
|
column: x => x.SearchOutlinePreferencesId,
|
||||||
principalTable: "AspNetUsers",
|
principalTable: "SearchOutlinePreferences",
|
||||||
principalColumn: "Id",
|
principalColumn: "Id",
|
||||||
onDelete: ReferentialAction.Cascade);
|
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(
|
migrationBuilder.CreateIndex(
|
||||||
name: "IX_ApplicationPreferences_ApplicationUserId",
|
name: "IX_ApplicationPreferences_ApplicationUserId",
|
||||||
table: "ApplicationPreferences",
|
table: "ApplicationPreferences",
|
||||||
@ -351,50 +344,20 @@ namespace Props.Data.Migrations
|
|||||||
column: "ApplicationUserId",
|
column: "ApplicationUserId",
|
||||||
unique: true);
|
unique: true);
|
||||||
|
|
||||||
migrationBuilder.CreateIndex(
|
|
||||||
name: "IX_SearchOutline_ApplicationUserId",
|
|
||||||
table: "SearchOutline",
|
|
||||||
column: "ApplicationUserId");
|
|
||||||
|
|
||||||
migrationBuilder.CreateIndex(
|
migrationBuilder.CreateIndex(
|
||||||
name: "IX_SearchOutline_SearchOutlinePreferencesId",
|
name: "IX_SearchOutline_SearchOutlinePreferencesId",
|
||||||
table: "SearchOutline",
|
table: "SearchOutline",
|
||||||
column: "SearchOutlinePreferencesId");
|
column: "SearchOutlinePreferencesId");
|
||||||
|
|
||||||
migrationBuilder.CreateIndex(
|
|
||||||
name: "IX_SearchOutlinePreferences_ActiveSearchOutlineId",
|
|
||||||
table: "SearchOutlinePreferences",
|
|
||||||
column: "ActiveSearchOutlineId");
|
|
||||||
|
|
||||||
migrationBuilder.CreateIndex(
|
migrationBuilder.CreateIndex(
|
||||||
name: "IX_SearchOutlinePreferences_ApplicationUserId",
|
name: "IX_SearchOutlinePreferences_ApplicationUserId",
|
||||||
table: "SearchOutlinePreferences",
|
table: "SearchOutlinePreferences",
|
||||||
column: "ApplicationUserId",
|
column: "ApplicationUserId",
|
||||||
unique: true);
|
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)
|
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(
|
migrationBuilder.DropTable(
|
||||||
name: "ApplicationPreferences");
|
name: "ApplicationPreferences");
|
||||||
|
|
||||||
@ -422,20 +385,20 @@ namespace Props.Data.Migrations
|
|||||||
migrationBuilder.DropTable(
|
migrationBuilder.DropTable(
|
||||||
name: "ResultsPreferences");
|
name: "ResultsPreferences");
|
||||||
|
|
||||||
|
migrationBuilder.DropTable(
|
||||||
|
name: "SearchOutline");
|
||||||
|
|
||||||
migrationBuilder.DropTable(
|
migrationBuilder.DropTable(
|
||||||
name: "AspNetRoles");
|
name: "AspNetRoles");
|
||||||
|
|
||||||
migrationBuilder.DropTable(
|
migrationBuilder.DropTable(
|
||||||
name: "Keywords");
|
name: "QueryWords");
|
||||||
|
|
||||||
migrationBuilder.DropTable(
|
|
||||||
name: "AspNetUsers");
|
|
||||||
|
|
||||||
migrationBuilder.DropTable(
|
migrationBuilder.DropTable(
|
||||||
name: "SearchOutlinePreferences");
|
name: "SearchOutlinePreferences");
|
||||||
|
|
||||||
migrationBuilder.DropTable(
|
migrationBuilder.DropTable(
|
||||||
name: "SearchOutline");
|
name: "AspNetUsers");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -163,7 +163,6 @@ namespace Props.Data.Migrations
|
|||||||
.HasColumnType("TEXT");
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
b.Property<string>("ProfileName")
|
b.Property<string>("ProfileName")
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("TEXT");
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
b.HasKey("Id");
|
b.HasKey("Id");
|
||||||
@ -212,7 +211,7 @@ namespace Props.Data.Migrations
|
|||||||
|
|
||||||
b.HasKey("Id");
|
b.HasKey("Id");
|
||||||
|
|
||||||
b.ToTable("Keywords");
|
b.ToTable("QueryWords");
|
||||||
});
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("Props.Models.Search.SearchOutline", b =>
|
modelBuilder.Entity("Props.Models.Search.SearchOutline", b =>
|
||||||
@ -221,11 +220,7 @@ namespace Props.Data.Migrations
|
|||||||
.ValueGeneratedOnAdd()
|
.ValueGeneratedOnAdd()
|
||||||
.HasColumnType("INTEGER");
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
b.Property<string>("ApplicationUserId")
|
b.Property<string>("DisabledShops")
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("TEXT");
|
|
||||||
|
|
||||||
b.Property<string>("Enabled")
|
|
||||||
.IsRequired()
|
.IsRequired()
|
||||||
.HasColumnType("TEXT");
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
@ -236,13 +231,11 @@ namespace Props.Data.Migrations
|
|||||||
.IsRequired()
|
.IsRequired()
|
||||||
.HasColumnType("TEXT");
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
b.Property<int?>("SearchOutlinePreferencesId")
|
b.Property<int>("SearchOutlinePreferencesId")
|
||||||
.HasColumnType("INTEGER");
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
b.HasKey("Id");
|
b.HasKey("Id");
|
||||||
|
|
||||||
b.HasIndex("ApplicationUserId");
|
|
||||||
|
|
||||||
b.HasIndex("SearchOutlinePreferencesId");
|
b.HasIndex("SearchOutlinePreferencesId");
|
||||||
|
|
||||||
b.ToTable("SearchOutline");
|
b.ToTable("SearchOutline");
|
||||||
@ -318,16 +311,14 @@ namespace Props.Data.Migrations
|
|||||||
.ValueGeneratedOnAdd()
|
.ValueGeneratedOnAdd()
|
||||||
.HasColumnType("INTEGER");
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
b.Property<int?>("ActiveSearchOutlineId")
|
|
||||||
.HasColumnType("INTEGER");
|
|
||||||
|
|
||||||
b.Property<string>("ApplicationUserId")
|
b.Property<string>("ApplicationUserId")
|
||||||
.IsRequired()
|
.IsRequired()
|
||||||
.HasColumnType("TEXT");
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
b.HasKey("Id");
|
b.Property<string>("NameOfLastUsed")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
b.HasIndex("ActiveSearchOutlineId");
|
b.HasKey("Id");
|
||||||
|
|
||||||
b.HasIndex("ApplicationUserId")
|
b.HasIndex("ApplicationUserId")
|
||||||
.IsUnique();
|
.IsUnique();
|
||||||
@ -438,33 +429,23 @@ namespace Props.Data.Migrations
|
|||||||
|
|
||||||
modelBuilder.Entity("Props.Models.Search.SearchOutline", b =>
|
modelBuilder.Entity("Props.Models.Search.SearchOutline", b =>
|
||||||
{
|
{
|
||||||
b.HasOne("Props.Models.User.ApplicationUser", "ApplicationUser")
|
b.HasOne("Props.Models.User.SearchOutlinePreferences", "SearchOutlinePreferences")
|
||||||
.WithMany()
|
.WithMany("SearchOutlines")
|
||||||
.HasForeignKey("ApplicationUserId")
|
.HasForeignKey("SearchOutlinePreferencesId")
|
||||||
.OnDelete(DeleteBehavior.Cascade)
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
.IsRequired();
|
.IsRequired();
|
||||||
|
|
||||||
b.HasOne("Props.Models.User.SearchOutlinePreferences", null)
|
b.Navigation("SearchOutlinePreferences");
|
||||||
.WithMany("SearchOutlines")
|
|
||||||
.HasForeignKey("SearchOutlinePreferencesId");
|
|
||||||
|
|
||||||
b.Navigation("ApplicationUser");
|
|
||||||
});
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("Props.Models.User.SearchOutlinePreferences", b =>
|
modelBuilder.Entity("Props.Models.User.SearchOutlinePreferences", b =>
|
||||||
{
|
{
|
||||||
b.HasOne("Props.Models.Search.SearchOutline", "ActiveSearchOutline")
|
|
||||||
.WithMany()
|
|
||||||
.HasForeignKey("ActiveSearchOutlineId");
|
|
||||||
|
|
||||||
b.HasOne("Props.Models.User.ApplicationUser", "ApplicationUser")
|
b.HasOne("Props.Models.User.ApplicationUser", "ApplicationUser")
|
||||||
.WithOne("searchOutlinePreferences")
|
.WithOne("searchOutlinePreferences")
|
||||||
.HasForeignKey("Props.Models.User.SearchOutlinePreferences", "ApplicationUserId")
|
.HasForeignKey("Props.Models.User.SearchOutlinePreferences", "ApplicationUserId")
|
||||||
.OnDelete(DeleteBehavior.Cascade)
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
.IsRequired();
|
.IsRequired();
|
||||||
|
|
||||||
b.Navigation("ActiveSearchOutline");
|
|
||||||
|
|
||||||
b.Navigation("ApplicationUser");
|
b.Navigation("ApplicationUser");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -11,53 +11,45 @@ namespace Props.Models.Search
|
|||||||
{
|
{
|
||||||
public int Id { get; set; }
|
public int Id { get; set; }
|
||||||
|
|
||||||
[Required]
|
public int SearchOutlinePreferencesId { get; set; }
|
||||||
public string ApplicationUserId { get; set; }
|
|
||||||
|
|
||||||
[Required]
|
[Required]
|
||||||
public virtual ApplicationUser ApplicationUser { get; set; }
|
public virtual SearchOutlinePreferences SearchOutlinePreferences { get; set; }
|
||||||
|
|
||||||
[Required]
|
[Required]
|
||||||
public string Name { get; set; } = "Default";
|
public string Name { get; set; }
|
||||||
|
|
||||||
public Filters Filters { get; set; } = new Filters();
|
|
||||||
|
|
||||||
|
public Filters Filters { get; set; }
|
||||||
[Required]
|
[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]
|
public bool this[string name]
|
||||||
{
|
{
|
||||||
get
|
get
|
||||||
{
|
{
|
||||||
return !this.Contains(name);
|
return this.Contains(name);
|
||||||
}
|
}
|
||||||
set
|
set
|
||||||
{
|
{
|
||||||
if (value == false && TotalShops - Count <= 1) return;
|
|
||||||
if (value)
|
if (value)
|
||||||
{
|
{
|
||||||
this.Remove(name);
|
this.Add(name);
|
||||||
}
|
}
|
||||||
else
|
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;
|
SearchOutline other = (SearchOutline)obj;
|
||||||
return
|
return
|
||||||
Id == other.Id &&
|
Id == other.Id &&
|
||||||
|
Name.Equals(other.Name) &&
|
||||||
Filters.Equals(other.Filters) &&
|
Filters.Equals(other.Filters) &&
|
||||||
Enabled.Equals(other.Enabled);
|
DisabledShops.Equals(other.DisabledShops);
|
||||||
}
|
}
|
||||||
|
|
||||||
public override int GetHashCode()
|
public override int GetHashCode()
|
||||||
{
|
{
|
||||||
return HashCode.Combine(Id, Name);
|
return HashCode.Combine(Id, Name, Filters, DisabledShops);
|
||||||
}
|
}
|
||||||
|
|
||||||
public SearchOutline()
|
public SearchOutline()
|
||||||
{
|
{
|
||||||
this.Name = "Default";
|
|
||||||
this.Filters = new Filters();
|
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.Name = name;
|
||||||
this.Filters = filters;
|
this.Filters = filters;
|
||||||
this.Enabled = disabled;
|
this.DisabledShops = disabled;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -21,7 +21,6 @@ namespace Props.Models
|
|||||||
[Required]
|
[Required]
|
||||||
public IList<Category> Order { get; set; }
|
public IList<Category> Order { get; set; }
|
||||||
|
|
||||||
[Required]
|
|
||||||
public string ProfileName { get; set; }
|
public string ProfileName { get; set; }
|
||||||
|
|
||||||
public ResultsPreferences()
|
public ResultsPreferences()
|
||||||
|
@ -16,22 +16,19 @@ namespace Props.Models.User
|
|||||||
public virtual ApplicationUser ApplicationUser { get; set; }
|
public virtual ApplicationUser ApplicationUser { get; set; }
|
||||||
|
|
||||||
[Required]
|
[Required]
|
||||||
public virtual ISet<SearchOutline> SearchOutlines { get; set; }
|
public virtual IList<SearchOutline> SearchOutlines { get; set; }
|
||||||
|
|
||||||
[Required]
|
public string NameOfLastUsed { get; set; }
|
||||||
public virtual SearchOutline ActiveSearchOutline { get; set; }
|
|
||||||
|
|
||||||
public SearchOutlinePreferences()
|
public SearchOutlinePreferences()
|
||||||
{
|
{
|
||||||
SearchOutlines = new HashSet<SearchOutline>();
|
SearchOutlines = new List<SearchOutline>();
|
||||||
ActiveSearchOutline = new SearchOutline();
|
|
||||||
SearchOutlines.Add(ActiveSearchOutline);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public SearchOutlinePreferences(ISet<SearchOutline> searchOutlines, SearchOutline activeSearchOutline)
|
public SearchOutlinePreferences(List<SearchOutline> searchOutlines, string nameOfLastUsed)
|
||||||
{
|
{
|
||||||
this.SearchOutlines = searchOutlines;
|
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,253 +8,296 @@
|
|||||||
ViewData["Specific"] = "Search";
|
ViewData["Specific"] = "Search";
|
||||||
}
|
}
|
||||||
|
|
||||||
<form method="GET">
|
<div class="flex-grow-1 d-flex flex-column" x-data="search">
|
||||||
<div class="mt-4 mb-3 less-concise mx-auto">
|
<div class="mt-4 mb-3 less-concise">
|
||||||
<div class="input-group">
|
<div class="input-group">
|
||||||
<input type="text" class="form-control border-primary" placeholder="What are you looking for?"
|
<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"
|
<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>
|
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>
|
</div>
|
||||||
|
|
||||||
<div class="collapse tear" id="configuration">
|
<div class="collapse tear" id="configuration">
|
||||||
<div class="container my-3">
|
<div class="d-flex">
|
||||||
<div class="d-flex">
|
<h1 class="my-3 display-2 mx-auto">
|
||||||
<h1 class="my-2 display-2 me-auto">Configuration</h1>
|
<i class="bi bi-sliders"></i>
|
||||||
<button class="btn align-self-start" type="button" id="configuration-close" data-bs-toggle="collapse"
|
Configuration
|
||||||
data-bs-target="#configuration"><i class="bi bi-x-lg"></i></button>
|
</h1>
|
||||||
</div>
|
<button class="btn align-self-start m-3" type="button" id="configuration-close" data-bs-toggle="collapse"
|
||||||
<div class="row justify-content-md-center">
|
data-bs-target="#configuration"><i class="bi bi-x-lg"></i></button>
|
||||||
<section class="col-lg px-4">
|
</div>
|
||||||
<h3>Price</h3>
|
<div class="container">
|
||||||
<div class="mb-3">
|
<div class="row my-3">
|
||||||
<label for="max-price" class="form-label">Maximum Price</label>
|
<div class="col-lg-3 px-2">
|
||||||
<div class="input-group">
|
<div class="row">
|
||||||
<div class="input-group-text">
|
<div class="col">
|
||||||
<input class="form-check-input mt-0" type="checkbox" id="max-price-enabled"
|
<h3>Search Outlines</h3>
|
||||||
checked="@Model.ActiveSearchOutline.Filters.EnableUpperPrice"
|
</div>
|
||||||
name="ActiveSearchOutline.Filters.EnableUpperPrice">
|
<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>
|
||||||
<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>
|
</div>
|
||||||
<div class="mb-3">
|
<div class="row px-3">
|
||||||
<label for="min-price" class="form-label">Minimum Price</label>
|
<div style="max-height: 28em;" data-simplebar>
|
||||||
<div class="input-group">
|
<template x-for="(current, i) in searchOutlines">
|
||||||
<span class="input-group-text">$</span>
|
<div class="clean-radio d-flex">
|
||||||
<input type="number" class="form-control" min="0" id="min-price"
|
<input type="radio" x-bind:id="`${i}-selector`" name="search-outline"
|
||||||
value="@Model.ActiveSearchOutline.Filters.LowerPrice"
|
x-bind:value="i" x-model="selectedSearchOutline"
|
||||||
name="ActiveSearchOutline.Filters.LowerPrice">
|
x-on:click="loadSearchOutline(current)" x-bind:disabled="!searchOutline.ready">
|
||||||
<span class="input-group-text">.00</span>
|
<label class="flex-grow-1" x-bind:for="`${i}-selector`">
|
||||||
</div>
|
<span class="me-auto" x-text="current"></span>
|
||||||
</div>
|
</label>
|
||||||
<div class="mb-3">
|
<button class="btn m-1" x-show="loggedIn" x-on:click="deleteSearchOutline(current)"
|
||||||
<label for="max-shipping" class="form-label">Maximum Shipping Fee</label>
|
x-bind:disabled="deletingSearchOutline || (searchOutlines.length < 2)">
|
||||||
<div class="input-group">
|
<i class="bi bi-trash"></i>
|
||||||
<div class="input-group-text">
|
</button>
|
||||||
<input class="form-check-input mt-0" type="checkbox" id="max-shipping-enabled"
|
</div>
|
||||||
checked="@Model.ActiveSearchOutline.Filters.EnableMaxShippingFee"
|
</template>
|
||||||
name="ActiveSearchOutline.Filters.EnableMaxShippingFee">
|
<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>
|
</div>
|
||||||
<span class="input-group-text">$</span>
|
<template x-if="(searchOutlines.length < 2) && (loggedIn)">
|
||||||
<input type="number" class="form-control" min="0" id="max-shipping"
|
<div class="text-muted text-center my-3 p-3">
|
||||||
value="@Model.ActiveSearchOutline.Filters.MaxShippingFee"
|
<p>Add more search outlines by clicking the <i class="bi bi-plus-lg"></i> above!</p>
|
||||||
name="ActiveSearchOutline.Filters.MaxShippingFee">
|
</div>
|
||||||
<span class="input-group-text">.00</span>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="mb-3">
|
</div>
|
||||||
<div class="form-check">
|
<div class="col-lg px-4">
|
||||||
<input class="form-check-input" type="checkbox" id="keep-unknown-shipping"
|
<div class="row">
|
||||||
checked="@Model.ActiveSearchOutline.Filters.KeepUnknownShipping"
|
<input class="title-input less-concise mx-4"
|
||||||
name="ActiveSearchOutline.Filters.KeepUnknownShipping">
|
x-bind:class="searchOutline.ready ? '' : 'invisible'" type="text"
|
||||||
<label class="form-check-label" for="keep-unknown-shipping">Keep Unknown Shipping</label>
|
x-model="searchOutlines[selectedSearchOutline]" x-on:change="SearchOutlineNameChange"
|
||||||
</div>
|
x-bind:disabled="(!loggedIn) || (changingName)">
|
||||||
</div>
|
</div>
|
||||||
</section>
|
<div class="row justify-content-md-center" x-bind:class="searchOutline.ready ? '' : 'invisible'">
|
||||||
<section class="col-lg px-4">
|
<section class="col-md px-3">
|
||||||
<h3>Metrics</h3>
|
<h3>Price</h3>
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label for="min-purchases" class="form-label">Minimum Purchases</label>
|
<label for="max-price" class="form-label">Maximum Price</label>
|
||||||
<div class="input-group">
|
<div class="input-group">
|
||||||
<input type="number" class="form-control" min="0" id="min-purchases"
|
<div class="input-group-text">
|
||||||
value="@Model.ActiveSearchOutline.Filters.MinPurchases"
|
<input class="form-check-input mt-0" type="checkbox" id="max-price-enabled"
|
||||||
name="ActiveSearchOutline.Filters.MinPurchases">
|
x-model="searchOutline.filters.enableUpperPrice"
|
||||||
<span class="input-group-text">Purchases</span>
|
x-on:change="searchOutlineChanged">
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<span class="input-group-text">$</span>
|
||||||
<div class="mb-3">
|
<input type="number" class="form-control" min="0" id="max-price"
|
||||||
<div class="form-check">
|
x-model="searchOutline.filters.upperPrice"
|
||||||
<input class="form-check-input" type="checkbox" id="keep-unknown-purchases"
|
x-bind:disabled="!searchOutline.filters.enableUpperPrice"
|
||||||
checked="@Model.ActiveSearchOutline.Filters.KeepUnknownPurchaseCount"
|
x-on:change="searchOutlineChanged" x-on:input="validateNumericalInputs">
|
||||||
name="ActiveSearchOutline.Filters.KeepUnknownPurchaseCount">
|
<span class="input-group-text">.00</span>
|
||||||
<label class="form-check-label" for="keep-unknown-purchases">Keep Unknown Purchases</label>
|
</div>
|
||||||
</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>
|
||||||
}
|
<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>
|
||||||
</section>
|
<div x-show="!searchOutline.ready">
|
||||||
|
<div class="spinner-border center-overlay" role="status">
|
||||||
|
<span class="visually-hidden">Loading...</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
|
||||||
|
|
||||||
<div id="content-pages" class="multipage mt-3 invisible">
|
<div id="content-pages" class="multipage mt-3 invisible">
|
||||||
<ul class="nav nav-pills selectors">
|
<ul class="nav nav-pills selectors">
|
||||||
<li class="nav-item" role="presentation">
|
<li class="nav-item" role="presentation">
|
||||||
<button type="button" data-bs-toggle="pill" data-bs-target="#quick-picks-slide"><i
|
<button type="button" data-bs-toggle="pill" data-bs-target="#quick-picks-slide"><i
|
||||||
class="bi bi-stopwatch"></i></button>
|
class="bi bi-stopwatch"></i></button>
|
||||||
</li>
|
</li>
|
||||||
<li class="nav-item" role="presentation">
|
<li class="nav-item" role="presentation">
|
||||||
<button type="button" data-bs-toggle="pill" data-bs-target="#results-slide"><i
|
<button type="button" data-bs-toggle="pill" data-bs-target="#results-slide"><i
|
||||||
class="bi bi-view-list"></i></button>
|
class="bi bi-view-list"></i></button>
|
||||||
</li>
|
</li>
|
||||||
<li class="nav-item" role="presentation">
|
<li class="nav-item" role="presentation">
|
||||||
<button type="button" data-bs-toggle="pill" data-bs-target="#info-slide"><i
|
<button type="button" data-bs-toggle="pill" data-bs-target="#info-slide"><i
|
||||||
class="bi bi-info-lg"></i></button>
|
class="bi bi-info-lg"></i></button>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
<div class="multipage-slides tab-content">
|
<div class="multipage-slides tab-content">
|
||||||
<div class="multipage-slide tab-pane fade" id="quick-picks-slide">
|
<div class="multipage-slide tab-pane fade" id="quick-picks-slide">
|
||||||
<div class="multipage-title">
|
<div class="multipage-title">
|
||||||
<h1 class="display-2"><i class="bi bi-stopwatch"></i> Quick Picks</h1>
|
<h1 class="display-2"><i class="bi bi-stopwatch"></i> Quick Picks</h1>
|
||||||
@if (Model.SearchResults != null)
|
<template x-if="hasResults()">
|
||||||
{
|
<p>@ContentManager.Json.quickPicks.searched</p>
|
||||||
<p>@ContentManager.Json.quickPicks.searched</p>
|
</template>
|
||||||
}
|
<template x-if="!hasResults()">
|
||||||
else
|
<p>@ContentManager.Json.quickPicks.prompt</p>
|
||||||
{
|
</template>
|
||||||
<p>@ContentManager.Json.quickPicks.prompt</p>
|
<hr class="less-concise">
|
||||||
}
|
</div>
|
||||||
<hr class="less-concise">
|
<div class="multipage-content">
|
||||||
</div>
|
<template x-if="hasResults()">
|
||||||
<div class="multipage-content">
|
<template x-if="results.bestPrice">
|
||||||
@if (Model.SearchResults != null)
|
<p>Here's the listing with the lowest price.</p>
|
||||||
{
|
<div>
|
||||||
@if (Model.BestRatingPriceRatio != null)
|
@* TODO: Implement best price display here *@
|
||||||
{
|
</div>
|
||||||
<p>We found this product to have the best rating to price ratio.</p>
|
</template>
|
||||||
}
|
|
||||||
|
|
||||||
@if (Model.TopRated != null)
|
@* TODO: Add display for top results. *@
|
||||||
{
|
</template>
|
||||||
<p>This listing was the one that had the highest rating.</p>
|
<template x-if="!hasResults()">
|
||||||
}
|
<div
|
||||||
|
class="text-center less-concise text-muted flex-grow-1 justify-content-center d-flex flex-column">
|
||||||
@if (Model.MostPurchases != null)
|
<h2>@ContentManager.Json.notSearched</h2>
|
||||||
{
|
|
||||||
<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>
|
</div>
|
||||||
}
|
</template>
|
||||||
|
</div>
|
||||||
@* TODO: Add display for top results. *@
|
</div>
|
||||||
}
|
<div class="multipage-slide tab-pane fade" id="results-slide" x-data>
|
||||||
else
|
<div class="multipage-title">
|
||||||
{
|
<h2><i class="bi bi-view-list"></i> Results</h2>
|
||||||
<div class="text-center less-concise text-muted flex-grow-1 justify-content-center d-flex flex-column">
|
<template x-if="hasResults()">
|
||||||
<h2>@ContentManager.Json.notSearched</h2>
|
<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 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>
|
|
||||||
}
|
|
||||||
</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 Castle.Core.Internal;
|
||||||
using Microsoft.AspNetCore.Identity;
|
using Microsoft.AspNetCore.Identity;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
using Microsoft.AspNetCore.Mvc.ModelBinding;
|
||||||
using Microsoft.AspNetCore.Mvc.RazorPages;
|
using Microsoft.AspNetCore.Mvc.RazorPages;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using Props.Data;
|
using Props.Data;
|
||||||
@ -21,36 +22,5 @@ namespace Props.Pages
|
|||||||
{
|
{
|
||||||
[BindProperty(Name = "q", SupportsGet = true)]
|
[BindProperty(Name = "q", SupportsGet = true)]
|
||||||
public string SearchQuery { get; set; }
|
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 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;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
|
using Microsoft.Extensions.Configuration;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
using Props.Data;
|
using Props.Data;
|
||||||
using Props.Models.Search;
|
using Props.Models.Search;
|
||||||
|
using Props.Options;
|
||||||
using Props.Shop.Framework;
|
using Props.Shop.Framework;
|
||||||
|
|
||||||
namespace Props.Services.Modules
|
namespace Props.Services.Modules
|
||||||
{
|
{
|
||||||
public class LiveMetricsManager : IMetricsManager
|
public class LiveMetricsManager : IMetricsManager
|
||||||
{
|
{
|
||||||
|
private MetricsOptions metricsOptions;
|
||||||
private ILogger<LiveMetricsManager> logger;
|
private ILogger<LiveMetricsManager> logger;
|
||||||
ApplicationDbContext dbContext;
|
private ApplicationDbContext dbContext;
|
||||||
public LiveMetricsManager(ApplicationDbContext dbContext, ILogger<LiveMetricsManager> logger)
|
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.logger = logger;
|
||||||
this.dbContext = dbContext;
|
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)
|
public IEnumerable<ProductListingInfo> RetrieveTopListings(int max)
|
||||||
{
|
{
|
||||||
@ -27,8 +35,8 @@ namespace Props.Services.Modules
|
|||||||
|
|
||||||
public IEnumerable<string> RetrieveCommonKeywords(int max)
|
public IEnumerable<string> RetrieveCommonKeywords(int max)
|
||||||
{
|
{
|
||||||
if (dbContext.Keywords == null) return null;
|
if (dbContext.QueryWords == null) return null;
|
||||||
return (from k in dbContext.Keywords
|
return (from k in dbContext.QueryWords
|
||||||
orderby k.Hits descending
|
orderby k.Hits descending
|
||||||
select k.Word).Take(max);
|
select k.Word).Take(max);
|
||||||
}
|
}
|
||||||
@ -40,11 +48,11 @@ namespace Props.Services.Modules
|
|||||||
QueryWordInfo[] wordInfos = new QueryWordInfo[tokens.Length];
|
QueryWordInfo[] wordInfos = new QueryWordInfo[tokens.Length];
|
||||||
for (int wordIndex = 0; wordIndex < tokens.Length; wordIndex++)
|
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)
|
if (queryWordInfo.Hits == 0)
|
||||||
{
|
{
|
||||||
queryWordInfo.Word = tokens[wordIndex];
|
queryWordInfo.Word = tokens[wordIndex];
|
||||||
dbContext.Keywords.Add(queryWordInfo);
|
dbContext.QueryWords.Add(queryWordInfo);
|
||||||
}
|
}
|
||||||
queryWordInfo.Hits += 1;
|
queryWordInfo.Hits += 1;
|
||||||
wordInfos[wordIndex] = queryWordInfo;
|
wordInfos[wordIndex] = queryWordInfo;
|
||||||
@ -60,11 +68,12 @@ namespace Props.Services.Modules
|
|||||||
wordInfos[wordIndex].Following.Add(wordInfos[afterIndex]);
|
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 =
|
ProductListingInfo productListingInfo =
|
||||||
(from info in dbContext.ProductListingInfos
|
(from info in dbContext.ProductListingInfos
|
||||||
@ -78,7 +87,27 @@ namespace Props.Services.Modules
|
|||||||
productListingInfo.ProductListing = productListing;
|
productListingInfo.ProductListing = productListing;
|
||||||
productListingInfo.ProductListingIdentifier = productListing.Identifier;
|
productListingInfo.ProductListingIdentifier = productListing.Identifier;
|
||||||
productListingInfo.Hits += 1;
|
productListingInfo.Hits += 1;
|
||||||
|
|
||||||
|
CullProductListings();
|
||||||
dbContext.SaveChanges();
|
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())
|
foreach (string shopName in await ShopManager.GetAllShopNames())
|
||||||
{
|
{
|
||||||
if (searchOutline.Enabled[shopName])
|
if (searchOutline.DisabledShops[shopName])
|
||||||
{
|
{
|
||||||
logger.LogDebug("Checking \"{0}\".", shopName);
|
logger.LogDebug("Checking \"{0}\".", shopName);
|
||||||
int amount = 0;
|
int amount = 0;
|
||||||
@ -43,7 +43,7 @@ namespace Props.Services.Modules
|
|||||||
if (searchOutline.Filters.Validate(product))
|
if (searchOutline.Filters.Validate(product))
|
||||||
{
|
{
|
||||||
amount += 1;
|
amount += 1;
|
||||||
metricsManager.RegisterListing(product, shopName);
|
metricsManager.RegisterProductListing(product, shopName);
|
||||||
results.Add(product);
|
results.Add(product);
|
||||||
}
|
}
|
||||||
if (amount >= searchOptions.MaxResults) break;
|
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.Builder;
|
||||||
using Microsoft.AspNetCore.Hosting;
|
using Microsoft.AspNetCore.Hosting;
|
||||||
using Microsoft.AspNetCore.HttpsPolicy;
|
|
||||||
using Microsoft.AspNetCore.Identity;
|
|
||||||
using Microsoft.AspNetCore.Identity.UI;
|
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using Microsoft.Extensions.Configuration;
|
using Microsoft.Extensions.Configuration;
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
@ -21,5 +21,9 @@
|
|||||||
"Textual": {
|
"Textual": {
|
||||||
"Dir": "./textual"
|
"Dir": "./textual"
|
||||||
},
|
},
|
||||||
|
"Metrics": {
|
||||||
|
"MaxQueryWords": 4096,
|
||||||
|
"MaxProductListings": 1024
|
||||||
|
},
|
||||||
"AllowedHosts": "*"
|
"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";
|
const startingSlide = "#quick-picks-slide";
|
||||||
|
|
||||||
function initInteractiveElements() {
|
function initInteractiveElements() {
|
||||||
@ -11,14 +16,251 @@ function initInteractiveElements() {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function initConfigVisuals() {
|
function initConfigData() {
|
||||||
const minRatingDisplay = document.querySelector("#configuration #min-rating-display");
|
document.addEventListener("alpine:init", () => {
|
||||||
const minRatingSlider = document.querySelector("#configuration #min-rating");
|
Alpine.data("search", () => ({
|
||||||
const updateDisplay = function () {
|
loggedIn: false,
|
||||||
minRatingDisplay.innerHTML = `Minimum rating: ${minRatingSlider.value}%`;
|
query: "",
|
||||||
};
|
searchOutline: {
|
||||||
minRatingSlider.addEventListener("input", updateDisplay);
|
ready: false,
|
||||||
updateDisplay();
|
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() {
|
function initSlides() {
|
||||||
@ -46,8 +288,8 @@ function initSlides() {
|
|||||||
|
|
||||||
async function main() {
|
async function main() {
|
||||||
initInteractiveElements();
|
initInteractiveElements();
|
||||||
initConfigVisuals();
|
|
||||||
initSlides();
|
initSlides();
|
||||||
|
initConfigData();
|
||||||
}
|
}
|
||||||
|
|
||||||
main();
|
main();
|
||||||
|
@ -37,17 +37,6 @@ main {
|
|||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
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 {
|
footer {
|
||||||
@ -82,16 +71,24 @@ footer {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.center-overlay {
|
||||||
|
position: absolute;
|
||||||
|
left: 50%;
|
||||||
|
right: 50%;
|
||||||
|
bottom: 50%;
|
||||||
|
top: 50%;
|
||||||
|
}
|
||||||
|
|
||||||
.concise {
|
.concise {
|
||||||
@extend .container;
|
@extend .container;
|
||||||
max-width: 630px;
|
max-width: 630px;
|
||||||
width: inherit;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.less-concise {
|
.less-concise {
|
||||||
@extend .container;
|
@extend .container;
|
||||||
max-width: 720px;
|
max-width: 720px;
|
||||||
width: inherit;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
hr {
|
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 {
|
html {
|
||||||
min-height: 100%;
|
min-height: 100%;
|
||||||
display: flex;
|
display: flex;
|
||||||
|
10
Props/package-lock.json
generated
10
Props/package-lock.json
generated
@ -2773,6 +2773,11 @@
|
|||||||
"minimist": "^1.2.5"
|
"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": {
|
"kind-of": {
|
||||||
"version": "6.0.3",
|
"version": "6.0.3",
|
||||||
"resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz",
|
||||||
@ -3012,6 +3017,11 @@
|
|||||||
"object-keys": "^1.1.1"
|
"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": {
|
"once": {
|
||||||
"version": "1.4.0",
|
"version": "1.4.0",
|
||||||
"resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
|
"resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
|
||||||
|
@ -34,6 +34,8 @@
|
|||||||
"axios": "^0.21.1",
|
"axios": "^0.21.1",
|
||||||
"bootstrap": "^5.0.2",
|
"bootstrap": "^5.0.2",
|
||||||
"bootstrap-icons": "^1.5.0",
|
"bootstrap-icons": "^1.5.0",
|
||||||
|
"just-clone": "^3.2.1",
|
||||||
|
"on-change": "^3.0.2",
|
||||||
"simplebar": "^5.3.5"
|
"simplebar": "^5.3.5"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -5,7 +5,7 @@
|
|||||||
"That's it! Hit search and we'll look far and wide for what you need!"
|
"That's it! Hit search and we'll look far and wide for what you need!"
|
||||||
],
|
],
|
||||||
"quickPicks": {
|
"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!"
|
"prompt": "This is where we'll show you top listings so you can get back to working on your project!"
|
||||||
},
|
},
|
||||||
"results": {
|
"results": {
|
||||||
|
Loading…
Reference in New Issue
Block a user