Added primitive search mechanism in backend.
Began implementing search mechanism for frontend.
This commit is contained in:
parent
f71758ca69
commit
c94ea4a624
@ -1,7 +1,9 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Net.Http;
|
||||
using System.Text.Json;
|
||||
using System.Threading.Tasks;
|
||||
using Props.Shop.Adafruit.Api;
|
||||
using Props.Shop.Framework;
|
||||
|
||||
@ -9,7 +11,7 @@ namespace Props.Shop.Adafruit
|
||||
{
|
||||
public class AdafruitShop : IShop
|
||||
{
|
||||
private ProductListingManager productListingManager;
|
||||
private SearchManager searchManager;
|
||||
private Configuration configuration;
|
||||
private HttpClient http;
|
||||
private bool disposedValue;
|
||||
@ -27,24 +29,24 @@ namespace Props.Shop.Adafruit
|
||||
false,
|
||||
true
|
||||
);
|
||||
|
||||
public byte[] GetDataForPersistence()
|
||||
{
|
||||
return JsonSerializer.SerializeToUtf8Bytes(configuration);
|
||||
}
|
||||
|
||||
public IEnumerable<IOption> Initialize(byte[] data)
|
||||
public void Initialize(string workspaceDir)
|
||||
{
|
||||
http = new HttpClient();
|
||||
http.BaseAddress = new Uri("http://www.adafruit.com/api");
|
||||
configuration = JsonSerializer.Deserialize<Configuration>(data);
|
||||
this.productListingManager = new ProductListingManager();
|
||||
return null;
|
||||
http.BaseAddress = new Uri("http://www.adafruit.com/api/");
|
||||
configuration = new Configuration(); // TODO Implement config persistence.
|
||||
}
|
||||
|
||||
public IAsyncEnumerable<ProductListing> Search(string query, Filters filters)
|
||||
public async Task InitializeAsync(string workspaceDir)
|
||||
{
|
||||
return productListingManager.Search(query, configuration.Similarity, http);
|
||||
ProductListingManager productListingManager = new ProductListingManager(http);
|
||||
this.searchManager = new SearchManager(productListingManager, configuration.Similarity);
|
||||
await productListingManager.DownloadListings();
|
||||
productListingManager.StartUpdateTimer();
|
||||
}
|
||||
|
||||
public IEnumerable<ProductListing> Search(string query, Filters filters)
|
||||
{
|
||||
return searchManager.Search(query);
|
||||
}
|
||||
|
||||
protected virtual void Dispose(bool disposing)
|
||||
@ -54,6 +56,7 @@ namespace Props.Shop.Adafruit
|
||||
if (disposing)
|
||||
{
|
||||
http.Dispose();
|
||||
searchManager.Dispose();
|
||||
}
|
||||
disposedValue = true;
|
||||
}
|
||||
|
@ -0,0 +1,16 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
using Props.Shop.Framework;
|
||||
|
||||
namespace Props.Shop.Adafruit.Api
|
||||
{
|
||||
public interface IProductListingManager : IDisposable
|
||||
{
|
||||
public event EventHandler DataUpdateEvent;
|
||||
public IDictionary<string, IList<ProductListing>> ActiveListings { get; }
|
||||
public Task DownloadListings();
|
||||
public void StartUpdateTimer(int delay = 1000 * 60 * 5, int period = 1000 * 60 * 5);
|
||||
public void StopUpdateTimer();
|
||||
}
|
||||
}
|
@ -1,44 +1,49 @@
|
||||
using System;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Net.Http;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using Props.Shop.Framework;
|
||||
|
||||
namespace Props.Shop.Adafruit.Api
|
||||
{
|
||||
public class ListingsParser
|
||||
public class ProductListingsParser
|
||||
{
|
||||
public IEnumerable<ProductListing> ProductListings { get; private set; }
|
||||
public ListingsParser(string json)
|
||||
public void BuildProductListings(Stream stream)
|
||||
{
|
||||
dynamic data = JArray.Parse(json);
|
||||
List<ProductListing> parsed = new List<ProductListing>();
|
||||
foreach (dynamic item in data)
|
||||
using (StreamReader streamReader = new StreamReader(stream))
|
||||
{
|
||||
if (item.products_discontinued == 0)
|
||||
dynamic data = JArray.Load(new JsonTextReader(streamReader));
|
||||
List<ProductListing> parsed = new List<ProductListing>();
|
||||
foreach (dynamic item in data)
|
||||
{
|
||||
ProductListing res = new ProductListing();
|
||||
res.Name = item.product_name;
|
||||
res.LowerPrice = item.product_price;
|
||||
res.UpperPrice = res.LowerPrice;
|
||||
foreach (dynamic discount in item.discount_pricing)
|
||||
if (item.products_discontinued == 0)
|
||||
{
|
||||
if (discount.discounted_price < res.LowerPrice)
|
||||
ProductListing res = new ProductListing();
|
||||
res.Name = item.product_name;
|
||||
res.LowerPrice = item.product_price;
|
||||
res.UpperPrice = res.LowerPrice;
|
||||
foreach (dynamic discount in item.discount_pricing)
|
||||
{
|
||||
res.LowerPrice = discount.discounted_price;
|
||||
}
|
||||
if (discount.discounted_price > res.UpperPrice)
|
||||
{
|
||||
res.UpperPrice = discount.discounted_price;
|
||||
if (discount.discounted_price < res.LowerPrice)
|
||||
{
|
||||
res.LowerPrice = discount.discounted_price;
|
||||
}
|
||||
if (discount.discounted_price > res.UpperPrice)
|
||||
{
|
||||
res.UpperPrice = discount.discounted_price;
|
||||
}
|
||||
}
|
||||
res.URL = item.product_url;
|
||||
res.InStock = item.product_stock > 0;
|
||||
parsed.Add(res);
|
||||
}
|
||||
res.URL = item.product_url;
|
||||
res.InStock = item.product_stock > 0;
|
||||
parsed.Add(res);
|
||||
}
|
||||
ProductListings = parsed;
|
||||
}
|
||||
ProductListings = parsed;
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,77 @@
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
using System.Net.Http;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Props.Shop.Framework;
|
||||
|
||||
namespace Props.Shop.Adafruit.Api
|
||||
{
|
||||
public class ProductListingManager : IProductListingManager
|
||||
{
|
||||
public event EventHandler DataUpdateEvent;
|
||||
private bool disposedValue;
|
||||
private volatile Dictionary<string, IList<ProductListing>> activeListings;
|
||||
public IDictionary<string, IList<ProductListing>> ActiveListings => activeListings;
|
||||
private ProductListingsParser parser = new ProductListingsParser();
|
||||
private HttpClient httpClient;
|
||||
private Timer updateTimer;
|
||||
|
||||
public ProductListingManager(HttpClient httpClient)
|
||||
{
|
||||
this.httpClient = httpClient;
|
||||
}
|
||||
|
||||
public async Task DownloadListings() {
|
||||
if (disposedValue) throw new ObjectDisposedException("ProductListingManager");
|
||||
HttpResponseMessage responseMessage = await httpClient.GetAsync("products");
|
||||
parser.BuildProductListings(responseMessage.Content.ReadAsStream());
|
||||
Dictionary<string, IList<ProductListing>> listings = new Dictionary<string, IList<ProductListing>>();
|
||||
foreach (ProductListing product in parser.ProductListings)
|
||||
{
|
||||
IList<ProductListing> sameProducts = listings.GetValueOrDefault(product.Name);
|
||||
if (sameProducts == null) {
|
||||
sameProducts = new List<ProductListing>();
|
||||
listings.Add(product.Name, sameProducts);
|
||||
}
|
||||
|
||||
sameProducts.Add(product);
|
||||
}
|
||||
activeListings = listings;
|
||||
DataUpdateEvent?.Invoke(this, null);
|
||||
}
|
||||
|
||||
public void StartUpdateTimer(int delay = 1000 * 60 * 5, int period = 1000 * 60 * 5) {
|
||||
if (disposedValue) throw new ObjectDisposedException("ProductListingManager");
|
||||
if (updateTimer != null) throw new InvalidOperationException("Update timer already started.");
|
||||
updateTimer = new Timer(async (state) => await DownloadListings(), null, delay, period);
|
||||
}
|
||||
|
||||
public void StopUpdateTimer() {
|
||||
if (disposedValue) throw new ObjectDisposedException("ProductListingManager");
|
||||
if (updateTimer != null) throw new InvalidOperationException("Update timer not started.");
|
||||
updateTimer.Dispose();
|
||||
updateTimer = null;
|
||||
}
|
||||
|
||||
protected virtual void Dispose(bool disposing)
|
||||
{
|
||||
if (!disposedValue)
|
||||
{
|
||||
if (disposing)
|
||||
{
|
||||
updateTimer?.Dispose();
|
||||
updateTimer = null;
|
||||
}
|
||||
disposedValue = true;
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
Dispose(disposing: true);
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
}
|
||||
}
|
@ -1,58 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Net.Http;
|
||||
using System.Threading.Tasks;
|
||||
using FuzzySharp;
|
||||
using FuzzySharp.Extractor;
|
||||
using Props.Shop.Framework;
|
||||
|
||||
namespace Props.Shop.Adafruit.Api
|
||||
{
|
||||
public class ProductListingManager
|
||||
{
|
||||
private double minutesPerRequest;
|
||||
private Dictionary<string, List<ProductListing>> listings = new Dictionary<string, List<ProductListing>>();
|
||||
private bool requested = false;
|
||||
public DateTime TimeOfLastRequest { get; private set; }
|
||||
public bool RequestReady => !requested || DateTime.Now - TimeOfLastRequest > TimeSpan.FromMinutes(minutesPerRequest);
|
||||
|
||||
public ProductListingManager(int requestsPerMinute = 5)
|
||||
{
|
||||
this.minutesPerRequest = 1 / requestsPerMinute;
|
||||
}
|
||||
|
||||
public async Task RefreshListings(HttpClient http)
|
||||
{
|
||||
requested = true;
|
||||
TimeOfLastRequest = DateTime.Now;
|
||||
HttpResponseMessage response = await http.GetAsync("/products");
|
||||
SetListingsData(await response.Content.ReadAsStringAsync());
|
||||
}
|
||||
|
||||
public void SetListingsData(string data)
|
||||
{
|
||||
ListingsParser listingsParser = new ListingsParser(data);
|
||||
foreach (ProductListing listing in listingsParser.ProductListings)
|
||||
{
|
||||
List<ProductListing> similar = listings.GetValueOrDefault(listing.Name, new List<ProductListing>());
|
||||
similar.Add(listing);
|
||||
listings[listing.Name] = similar;
|
||||
}
|
||||
}
|
||||
|
||||
public async IAsyncEnumerable<ProductListing> Search(string query, float similarity, HttpClient httpClient = null)
|
||||
{
|
||||
if (RequestReady && httpClient != null) await RefreshListings(httpClient);
|
||||
IEnumerable<ExtractedResult<string>> resultNames = Process.ExtractAll(query, listings.Keys, cutoff: (int)similarity * 100);
|
||||
foreach (ExtractedResult<string> resultName in resultNames)
|
||||
{
|
||||
foreach (ProductListing product in listings[resultName.Value])
|
||||
{
|
||||
yield return product;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
72
Props-Modules/Props.Shop/Adafruit/Api/SearchManager.cs
Normal file
72
Props-Modules/Props.Shop/Adafruit/Api/SearchManager.cs
Normal file
@ -0,0 +1,72 @@
|
||||
using System;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using FuzzySharp;
|
||||
using FuzzySharp.Extractor;
|
||||
using Props.Shop.Framework;
|
||||
|
||||
namespace Props.Shop.Adafruit.Api
|
||||
{
|
||||
public class SearchManager : IDisposable
|
||||
{
|
||||
public float Similarity { get; set; }
|
||||
private readonly object searchLock = new object();
|
||||
private IDictionary<string, IList<ProductListing>> searched;
|
||||
private IProductListingManager listingManager;
|
||||
private bool disposedValue;
|
||||
|
||||
public SearchManager(IProductListingManager productListingManager, float similarity = 0.8f)
|
||||
{
|
||||
this.listingManager = productListingManager ?? throw new ArgumentNullException("productListingManager");
|
||||
this.Similarity = similarity;
|
||||
listingManager.DataUpdateEvent += OnDataUpdate;
|
||||
}
|
||||
|
||||
private void OnDataUpdate(object sender, EventArgs eventArgs)
|
||||
{
|
||||
BuildSearchIndex();
|
||||
}
|
||||
|
||||
private void BuildSearchIndex()
|
||||
{
|
||||
lock (searchLock)
|
||||
{
|
||||
searched = new Dictionary<string, IList<ProductListing>>(listingManager.ActiveListings);
|
||||
}
|
||||
}
|
||||
|
||||
public IEnumerable<ProductListing> Search(string query)
|
||||
{
|
||||
lock (searchLock)
|
||||
{
|
||||
foreach (ExtractedResult<string> listingNames in Process.ExtractAll(query, searched.Keys, cutoff: (int)(Similarity * 100)))
|
||||
{
|
||||
foreach (ProductListing same in searched[listingNames.Value])
|
||||
{
|
||||
yield return same;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected virtual void Dispose(bool disposing)
|
||||
{
|
||||
if (!disposedValue)
|
||||
{
|
||||
if (disposing)
|
||||
{
|
||||
listingManager.Dispose();
|
||||
}
|
||||
|
||||
disposedValue = true;
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
Dispose(disposing: true);
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
}
|
||||
}
|
@ -3,5 +3,10 @@ namespace Props.Shop.Adafruit
|
||||
public class Configuration
|
||||
{
|
||||
public float Similarity { get; set; }
|
||||
|
||||
public Configuration()
|
||||
{
|
||||
Similarity = 0.8f;
|
||||
}
|
||||
}
|
||||
}
|
@ -1,35 +0,0 @@
|
||||
using System;
|
||||
using Props.Shop.Framework;
|
||||
|
||||
namespace Props.Shop.Adafruit.Options
|
||||
{
|
||||
public class SimilarityOption : IOption
|
||||
{
|
||||
private Configuration configuration;
|
||||
public string Name => "Query Similarity";
|
||||
|
||||
public string Description => "The minimum level of similarity for a listing to be returned.";
|
||||
|
||||
public bool Required => true;
|
||||
|
||||
public Type Type => typeof(float);
|
||||
|
||||
internal SimilarityOption(Configuration configuration)
|
||||
{
|
||||
this.configuration = configuration;
|
||||
}
|
||||
|
||||
public string GetValue()
|
||||
{
|
||||
return configuration.Similarity.ToString();
|
||||
}
|
||||
|
||||
public bool SetValue(string value)
|
||||
{
|
||||
float parsed;
|
||||
bool success = float.TryParse(value, out parsed);
|
||||
configuration.Similarity = parsed;
|
||||
return success;
|
||||
}
|
||||
}
|
||||
}
|
@ -7,6 +7,7 @@
|
||||
<ItemGroup>
|
||||
<PackageReference Include="FuzzySharp" Version="2.0.2" />
|
||||
<PackageReference Include="Newtonsoft.Json" Version="13.0.1" />
|
||||
<PackageReference Include="System.Linq.Async" Version="5.0.0" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
@ -3,6 +3,7 @@ using System.Collections.Generic;
|
||||
using System.Net.Http;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
using System.Threading.Tasks;
|
||||
using Props.Shop.Framework;
|
||||
|
||||
namespace Props.Shop.Ebay
|
||||
@ -29,13 +30,16 @@ namespace Props.Shop.Ebay
|
||||
|
||||
private HttpClient httpClient;
|
||||
|
||||
public IEnumerable<IOption> Initialize(byte[] data)
|
||||
public void Initialize(string workspaceDir)
|
||||
{
|
||||
httpClient = new HttpClient();
|
||||
configuration = JsonSerializer.Deserialize<Configuration>(data);
|
||||
return new List<IOption>() {
|
||||
new SandboxOption(configuration),
|
||||
};
|
||||
configuration = new Configuration(); // TODO: Implement config persistence.
|
||||
}
|
||||
|
||||
|
||||
public Task InitializeAsync(string workspaceDir)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
protected virtual void Dispose(bool disposing)
|
||||
@ -61,7 +65,7 @@ namespace Props.Shop.Ebay
|
||||
return JsonSerializer.SerializeToUtf8Bytes(configuration);
|
||||
}
|
||||
|
||||
public IAsyncEnumerable<ProductListing> Search(string query, Filters filters)
|
||||
public IEnumerable<ProductListing> Search(string query, Filters filters)
|
||||
{
|
||||
// TODO: Implement the search system.
|
||||
throw new NotImplementedException();
|
||||
|
@ -1,35 +0,0 @@
|
||||
using System;
|
||||
using Props.Shop.Framework;
|
||||
|
||||
namespace Props.Shop.Ebay
|
||||
{
|
||||
public class SandboxOption : IOption
|
||||
{
|
||||
private Configuration configuration;
|
||||
public string Name => "Ebay Sandbox";
|
||||
|
||||
public string Description => "For development purposes, Ebay Sandbox allows use of Ebay APIs (with exceptions) in a sandbox environment before applying for production use.";
|
||||
|
||||
public bool Required => true;
|
||||
|
||||
public Type Type => typeof(bool);
|
||||
|
||||
internal SandboxOption(Configuration configuration)
|
||||
{
|
||||
this.configuration = configuration;
|
||||
}
|
||||
|
||||
public string GetValue()
|
||||
{
|
||||
return configuration.Sandbox.ToString();
|
||||
}
|
||||
|
||||
public bool SetValue(string value)
|
||||
{
|
||||
bool sandbox = false;
|
||||
bool res = bool.TryParse(value, out sandbox);
|
||||
configuration.Sandbox = sandbox;
|
||||
return res;
|
||||
}
|
||||
}
|
||||
}
|
@ -12,10 +12,10 @@ namespace Props.Shop.Framework
|
||||
string ShopDescription { get; }
|
||||
string ShopModuleAuthor { get; }
|
||||
|
||||
public IAsyncEnumerable<ProductListing> Search(string query, Filters filters);
|
||||
public IEnumerable<ProductListing> Search(string query, Filters filters);
|
||||
|
||||
IEnumerable<IOption> Initialize(byte[] data);
|
||||
void Initialize(string workspaceDir);
|
||||
Task InitializeAsync(string workspaceDir);
|
||||
public SupportedFeatures SupportedFeatures { get; }
|
||||
public byte[] GetDataForPersistence();
|
||||
}
|
||||
}
|
@ -1,6 +1,6 @@
|
||||
namespace Props.Shop.Framework
|
||||
{
|
||||
public struct ProductListing
|
||||
public class ProductListing
|
||||
{
|
||||
public float LowerPrice { get; set; }
|
||||
public float UpperPrice { get; set; }
|
||||
|
@ -0,0 +1,23 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
using Props.Shop.Framework;
|
||||
using Xunit;
|
||||
|
||||
namespace Props.Shop.Adafruit.Tests
|
||||
{
|
||||
public class AdafruitShopTest
|
||||
{
|
||||
[Fact]
|
||||
public async Task TestSearch() {
|
||||
AdafruitShop mockAdafruitShop = new AdafruitShop();
|
||||
mockAdafruitShop.Initialize(null);
|
||||
await mockAdafruitShop.InitializeAsync(null);
|
||||
int count = 0;
|
||||
foreach (ProductListing listing in mockAdafruitShop.Search("raspberry pi", new Filters()))
|
||||
{
|
||||
count += 1;
|
||||
}
|
||||
Assert.True(count > 0);
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,87 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Props.Shop.Adafruit.Api;
|
||||
using Props.Shop.Framework;
|
||||
|
||||
namespace Props.Shop.Adafruit.Tests.Api
|
||||
{
|
||||
public class FakeProductListingManager : IProductListingManager
|
||||
{
|
||||
private ProductListingsParser parser;
|
||||
private Timer updateTimer;
|
||||
private bool disposedValue;
|
||||
|
||||
private volatile Dictionary<string, IList<ProductListing>> activeListings;
|
||||
|
||||
public IDictionary<string, IList<ProductListing>> ActiveListings => activeListings;
|
||||
|
||||
public event EventHandler DataUpdateEvent;
|
||||
|
||||
public FakeProductListingManager()
|
||||
{
|
||||
parser = new ProductListingsParser();
|
||||
}
|
||||
|
||||
public Task DownloadListings()
|
||||
{
|
||||
if (disposedValue) throw new ObjectDisposedException("FakeProductListingManager");
|
||||
using (Stream stream = File.OpenRead("./Assets/products.json"))
|
||||
{
|
||||
parser.BuildProductListings(stream);
|
||||
}
|
||||
|
||||
Dictionary<string, IList<ProductListing>> results = new Dictionary<string, IList<ProductListing>>();
|
||||
foreach (ProductListing product in parser.ProductListings)
|
||||
{
|
||||
IList<ProductListing> sameProducts = results.GetValueOrDefault(product.Name);
|
||||
if (sameProducts == null) {
|
||||
sameProducts = new List<ProductListing>();
|
||||
results.Add(product.Name, sameProducts);
|
||||
}
|
||||
|
||||
sameProducts.Add(product);
|
||||
}
|
||||
activeListings = results;
|
||||
DataUpdateEvent?.Invoke(this, null);
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public void StartUpdateTimer(int delay = 300000, int period = 300000)
|
||||
{
|
||||
if (disposedValue) throw new ObjectDisposedException("FakeProductListingManager");
|
||||
if (updateTimer != null) throw new InvalidOperationException("Update timer already started.");
|
||||
updateTimer = new Timer((state) => DownloadListings(), null, delay, period);
|
||||
}
|
||||
|
||||
public void StopUpdateTimer()
|
||||
{
|
||||
if (disposedValue) throw new ObjectDisposedException("FakeProductListingManager");
|
||||
if (updateTimer == null) throw new InvalidOperationException("Update timer not started.");
|
||||
updateTimer.Dispose();
|
||||
updateTimer = null;
|
||||
}
|
||||
|
||||
protected virtual void Dispose(bool disposing)
|
||||
{
|
||||
if (!disposedValue)
|
||||
{
|
||||
if (disposing)
|
||||
{
|
||||
updateTimer?.Dispose();
|
||||
updateTimer = null;
|
||||
}
|
||||
|
||||
disposedValue = true;
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
Dispose(disposing: true);
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
}
|
||||
}
|
@ -10,7 +10,11 @@ namespace Props.Shop.Adafruit.Tests
|
||||
[Fact]
|
||||
public void TestParsing()
|
||||
{
|
||||
ListingsParser mockParser = new ListingsParser(File.ReadAllText("./Assets/products.json"));
|
||||
ProductListingsParser mockParser = new ProductListingsParser();
|
||||
using (Stream stream = File.OpenRead("./Assets/products.json"))
|
||||
{
|
||||
mockParser.BuildProductListings(stream);
|
||||
}
|
||||
Assert.NotEmpty(mockParser.ProductListings);
|
||||
}
|
||||
}
|
||||
|
@ -1,25 +0,0 @@
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Threading.Tasks;
|
||||
using Props.Shop.Adafruit.Api;
|
||||
using Props.Shop.Framework;
|
||||
using Xunit;
|
||||
|
||||
namespace Props.Shop.Adafruit.Tests.Api
|
||||
{
|
||||
public class ProductListingManagerTest
|
||||
{
|
||||
[Fact]
|
||||
public async Task TestSearch()
|
||||
{
|
||||
ProductListingManager mockProductListingManager = new ProductListingManager();
|
||||
mockProductListingManager.SetListingsData(File.ReadAllText("./Assets/products.json"));
|
||||
List<ProductListing> results = new List<ProductListing>();
|
||||
await foreach (ProductListing item in mockProductListingManager.Search("arduino", 0.5f))
|
||||
{
|
||||
results.Add(item);
|
||||
}
|
||||
Assert.NotEmpty(results);
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,20 @@
|
||||
using System.Threading.Tasks;
|
||||
using Props.Shop.Adafruit.Api;
|
||||
using Xunit;
|
||||
|
||||
namespace Props.Shop.Adafruit.Tests.Api
|
||||
{
|
||||
public class SearchManagerTest
|
||||
{
|
||||
[Fact]
|
||||
public async Task SearchTest()
|
||||
{
|
||||
FakeProductListingManager stubProductListingManager = new FakeProductListingManager();
|
||||
SearchManager searchManager = new SearchManager(stubProductListingManager);
|
||||
await stubProductListingManager.DownloadListings();
|
||||
searchManager.Similarity = 0.8f;
|
||||
Assert.NotEmpty(searchManager.Search("Raspberry Pi"));
|
||||
searchManager.Dispose();
|
||||
}
|
||||
}
|
||||
}
|
1
Props/.vscode/settings.json
vendored
1
Props/.vscode/settings.json
vendored
@ -5,4 +5,5 @@
|
||||
],
|
||||
"todo-tree.regex.regex": "((//|#|<!--|;|@\\*|/\\*|^)\\s*($TAGS)|^\\s*- \\[ \\])",
|
||||
"todo-tree.regex.subTagRegex": "(\\*@)",
|
||||
"editor.formatOnSave": true,
|
||||
}
|
@ -6,7 +6,8 @@
|
||||
}
|
||||
|
||||
<div class="jumbotron text-center">
|
||||
<img alt="Props logo" src="~/images/logo-simplified.svg" class="img-fluid" style="max-height: 150px;" asp-append-version="true" />
|
||||
<img alt="Props logo" src="~/images/logo-simplified.svg" class="img-fluid" style="max-height: 150px;"
|
||||
asp-append-version="true" />
|
||||
<h1 class="mt-3 mb-4 display-1">@ViewData["Title"]</h1>
|
||||
<p>Welcome back!</p>
|
||||
</div>
|
||||
@ -40,13 +41,16 @@
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-lg">
|
||||
<a class="link-secondary" id="forgot-password" asp-page="./ForgotPassword">Forgot your password?</a>
|
||||
<a class="link-secondary" id="forgot-password" asp-page="./ForgotPassword">Forgot your
|
||||
password?</a>
|
||||
</div>
|
||||
<div class="col-lg">
|
||||
<a class="link-secondary" asp-page="./Register" asp-route-returnUrl="@Model.ReturnUrl">Register as a new user</a>
|
||||
<a class="link-secondary" asp-page="./Register" asp-route-returnUrl="@Model.ReturnUrl">Register
|
||||
as a new user</a>
|
||||
</div>
|
||||
<div class="col-lg">
|
||||
<a class="link-secondary" id="resend-confirmation" asp-page="./ResendEmailConfirmation">Resend email confirmation</a>
|
||||
<a class="link-secondary" id="resend-confirmation" asp-page="./ResendEmailConfirmation">Resend
|
||||
email confirmation</a>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
@ -56,12 +60,14 @@
|
||||
<div class="col-md-6 md-offset-2">
|
||||
<h4>Use another service to log in.</h4>
|
||||
<hr />
|
||||
<form id="external-account" asp-page="./ExternalLogin" asp-route-returnUrl="@Model.ReturnUrl" method="post" class="form-horizontal">
|
||||
<form id="external-account" asp-page="./ExternalLogin" asp-route-returnUrl="@Model.ReturnUrl" method="post"
|
||||
class="form-horizontal">
|
||||
<div>
|
||||
<p>
|
||||
@foreach (var provider in Model.ExternalLogins)
|
||||
{
|
||||
<button type="submit" class="btn btn-primary" name="provider" value="@provider.Name" title="Log in using your @provider.DisplayName account">@provider.DisplayName</button>
|
||||
<button type="submit" class="btn btn-primary" name="provider" value="@provider.Name"
|
||||
title="Log in using your @provider.DisplayName account">@provider.DisplayName</button>
|
||||
}
|
||||
</p>
|
||||
</div>
|
||||
|
@ -3,15 +3,22 @@
|
||||
var hasExternalLogins = (await SignInManager.GetExternalAuthenticationSchemesAsync()).Any();
|
||||
}
|
||||
<ul class="nav nav-pills flex-column">
|
||||
<li class="nav-item"><a class="nav-link @ManageNavPages.IndexNavClass(ViewContext)" id="profile" asp-page="./Index">Profile</a></li>
|
||||
<li class="nav-item"><a class="nav-link @ManageNavPages.EmailNavClass(ViewContext)" id="email" asp-page="./Email">Email</a></li>
|
||||
<li class="nav-item"><a class="nav-link @ManageNavPages.ChangePasswordNavClass(ViewContext)" id="change-password" asp-page="./ChangePassword">Password</a></li>
|
||||
<li class="nav-item"><a class="nav-link @ManageNavPages.IndexNavClass(ViewContext)" id="profile"
|
||||
asp-page="./Index">Profile</a></li>
|
||||
<li class="nav-item"><a class="nav-link @ManageNavPages.EmailNavClass(ViewContext)" id="email"
|
||||
asp-page="./Email">Email</a></li>
|
||||
<li class="nav-item"><a class="nav-link @ManageNavPages.ChangePasswordNavClass(ViewContext)" id="change-password"
|
||||
asp-page="./ChangePassword">Password</a></li>
|
||||
@if (hasExternalLogins)
|
||||
{
|
||||
<li id="external-logins" class="nav-item"><a id="external-login" class="nav-link @ManageNavPages.ExternalLoginsNavClass(ViewContext)" asp-page="./ExternalLogins">External logins</a></li>
|
||||
<li id="external-logins" class="nav-item"><a id="external-login"
|
||||
class="nav-link @ManageNavPages.ExternalLoginsNavClass(ViewContext)" asp-page="./ExternalLogins">External
|
||||
logins</a></li>
|
||||
}
|
||||
<li class="nav-item"><a class="nav-link @ManageNavPages.TwoFactorAuthenticationNavClass(ViewContext)" id="two-factor" asp-page="./TwoFactorAuthentication">Two-factor authentication</a></li>
|
||||
<li class="nav-item"><a class="nav-link @ManageNavPages.PersonalDataNavClass(ViewContext)" id="personal-data" asp-page="./PersonalData">Personal data</a></li>
|
||||
<li class="nav-item"><a class="nav-link @ManageNavPages.TwoFactorAuthenticationNavClass(ViewContext)"
|
||||
id="two-factor" asp-page="./TwoFactorAuthentication">Two-factor authentication</a></li>
|
||||
<li class="nav-item"><a class="nav-link @ManageNavPages.PersonalDataNavClass(ViewContext)" id="personal-data"
|
||||
asp-page="./PersonalData">Personal data</a></li>
|
||||
</ul>
|
||||
|
||||
@* TODO: Finish styling this page. *@
|
||||
@* TODO: Finish styling account page. *@
|
@ -9,8 +9,10 @@
|
||||
if (@Model.DisplayConfirmAccountLink)
|
||||
{
|
||||
<p>
|
||||
This app does not currently have a real email sender registered, see <a href="https://aka.ms/aspaccountconf">these docs</a> for how to configure a real email sender.
|
||||
Normally this would be emailed: <a id="confirm-link" href="@Model.EmailConfirmationUrl">Click here to confirm your account</a>
|
||||
This app does not currently have a real email sender registered, see <a href="https://aka.ms/aspaccountconf">these
|
||||
docs</a> for how to configure a real email sender.
|
||||
Normally this would be emailed: <a id="confirm-link" href="@Model.EmailConfirmationUrl">Click here to confirm your
|
||||
account</a>
|
||||
</p>
|
||||
}
|
||||
else
|
||||
@ -20,4 +22,4 @@
|
||||
</p>
|
||||
}
|
||||
}
|
||||
@* TODO: Do something about this. *@
|
||||
@* TODO: implement account confirmation. *@
|
16
Props/Content/search.json
Normal file
16
Props/Content/search.json
Normal file
@ -0,0 +1,16 @@
|
||||
{
|
||||
"instructions": [
|
||||
"Type in the electronic component your looking for in the search bar above.",
|
||||
"Optionally, hit the configure button to modify your search criteria. With an account, you can even create and save multiple search profiles known as \"search outlines\".",
|
||||
"That's it! Hit search and we'll look far and wide for what you need!"
|
||||
],
|
||||
"quickPicks": {
|
||||
"searched": "To save you some time, these are some of the better options we found!",
|
||||
"prompt": "This is where we'll show you top listings so you can get back to working on your project!"
|
||||
},
|
||||
"results": {
|
||||
"searched": "We searched far and wide. Here's what we found!",
|
||||
"prompt": "This is were we'll display all the results we went through to show you the top. This is a good place to check if none of our quick picks cater to your needs."
|
||||
},
|
||||
"notSearched": "Nothing to show yet!"
|
||||
}
|
@ -15,24 +15,10 @@ namespace Props.Controllers
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
[Route("Shops/Available")]
|
||||
[Route("Available")]
|
||||
public IActionResult GetAvailableShops()
|
||||
{
|
||||
return Ok(shopManager.AvailableShops());
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
[Route("Default/Filters")]
|
||||
public IActionResult GetDefaultFilters()
|
||||
{
|
||||
return Ok(defaultOutline.Filters);
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
[Route("Default/DisabledShops")]
|
||||
public IActionResult GetDefaultDisabledShops()
|
||||
{
|
||||
return Ok(defaultOutline.Disabled);
|
||||
return Ok(shopManager.GetAllShopNames());
|
||||
}
|
||||
}
|
||||
}
|
37
Props/Controllers/SearchOutlineController.cs
Normal file
37
Props/Controllers/SearchOutlineController.cs
Normal file
@ -0,0 +1,37 @@
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Props.Models.Search;
|
||||
using Props.Services.Modules;
|
||||
|
||||
namespace Props.Controllers
|
||||
{
|
||||
public class SearchOutlineController : ApiControllerBase
|
||||
{
|
||||
private SearchOutline defaultOutline = new SearchOutline();
|
||||
|
||||
|
||||
public SearchOutlineController()
|
||||
{
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
[Route("Filters")]
|
||||
public IActionResult GetFilters()
|
||||
{
|
||||
return Ok(defaultOutline.Filters);
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
[Route("DisabledShops")]
|
||||
public IActionResult GetDisabledShops()
|
||||
{
|
||||
return Ok(defaultOutline.Enabled);
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
[Route("SearchOutlineName")]
|
||||
public IActionResult GetSearchOutlineName()
|
||||
{
|
||||
return Ok(defaultOutline.Name);
|
||||
}
|
||||
}
|
||||
}
|
@ -3,10 +3,11 @@ using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.AspNetCore.Identity.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.ChangeTracking;
|
||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||
using Microsoft.Extensions.Localization;
|
||||
using Props.Models;
|
||||
using Props.Models.Search;
|
||||
using Props.Models.User;
|
||||
@ -16,8 +17,10 @@ namespace Props.Data
|
||||
{
|
||||
public class ApplicationDbContext : IdentityDbContext<ApplicationUser>
|
||||
{
|
||||
DbSet<SearchOutline> SearchOutlines { get; set; }
|
||||
DbSet<ProductListingInfo> TrackedListings { get; set; }
|
||||
public DbSet<QueryWordInfo> Keywords { get; set; }
|
||||
|
||||
public DbSet<ProductListingInfo> ProductListingInfos { get; set; }
|
||||
|
||||
public ApplicationDbContext(DbContextOptions<ApplicationDbContext> options)
|
||||
: base(options)
|
||||
{
|
||||
@ -40,7 +43,7 @@ namespace Props.Data
|
||||
);
|
||||
|
||||
modelBuilder.Entity<SearchOutline>()
|
||||
.Property(e => e.Disabled)
|
||||
.Property(e => e.Enabled)
|
||||
.HasConversion(
|
||||
v => JsonSerializer.Serialize(v, null),
|
||||
v => JsonSerializer.Deserialize<SearchOutline.ShopsDisabled>(v, null),
|
||||
@ -50,6 +53,7 @@ namespace Props.Data
|
||||
c => c.Copy()
|
||||
)
|
||||
);
|
||||
|
||||
modelBuilder.Entity<SearchOutline>()
|
||||
.Property(e => e.Filters)
|
||||
.HasConversion(
|
||||
|
@ -9,7 +9,7 @@ using Props.Data;
|
||||
namespace Props.Data.Migrations
|
||||
{
|
||||
[DbContext(typeof(ApplicationDbContext))]
|
||||
[Migration("20210724073427_InitialCreate")]
|
||||
[Migration("20210805055109_InitialCreate")]
|
||||
partial class InitialCreate
|
||||
{
|
||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||
@ -188,18 +188,36 @@ namespace Props.Data.Migrations
|
||||
b.Property<DateTime>("LastUpdated")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("OriginName")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("ProductName")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("ProductUrl")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("ShopName")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("TrackedListings");
|
||||
b.ToTable("ProductListingInfos");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Props.Models.Search.QueryWordInfo", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<uint>("Hits")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("Word")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("Keywords");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Props.Models.Search.SearchOutline", b =>
|
||||
@ -212,18 +230,27 @@ namespace Props.Data.Migrations
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Disabled")
|
||||
b.Property<string>("Enabled")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Filters")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<int?>("SearchOutlinePreferencesId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("ApplicationUserId");
|
||||
|
||||
b.ToTable("SearchOutlines");
|
||||
b.HasIndex("SearchOutlinePreferencesId");
|
||||
|
||||
b.ToTable("SearchOutline");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Props.Models.User.ApplicationUser", b =>
|
||||
@ -290,6 +317,29 @@ namespace Props.Data.Migrations
|
||||
b.ToTable("AspNetUsers");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Props.Models.User.SearchOutlinePreferences", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int?>("ActiveSearchOutlineId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("ApplicationUserId")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("ActiveSearchOutlineId");
|
||||
|
||||
b.HasIndex("ApplicationUserId")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("SearchOutlinePreferences");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Props.Shared.Models.User.ApplicationPreferences", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
@ -314,6 +364,21 @@ namespace Props.Data.Migrations
|
||||
b.ToTable("ApplicationPreferences");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("QueryWordInfoQueryWordInfo", b =>
|
||||
{
|
||||
b.Property<int>("FollowingId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("PrecedingId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.HasKey("FollowingId", "PrecedingId");
|
||||
|
||||
b.HasIndex("PrecedingId");
|
||||
|
||||
b.ToTable("QueryWordInfoQueryWordInfo");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim<string>", b =>
|
||||
{
|
||||
b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null)
|
||||
@ -379,11 +444,32 @@ namespace Props.Data.Migrations
|
||||
modelBuilder.Entity("Props.Models.Search.SearchOutline", b =>
|
||||
{
|
||||
b.HasOne("Props.Models.User.ApplicationUser", "ApplicationUser")
|
||||
.WithMany("SearchOutlines")
|
||||
.WithMany()
|
||||
.HasForeignKey("ApplicationUserId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("Props.Models.User.SearchOutlinePreferences", null)
|
||||
.WithMany("SearchOutlines")
|
||||
.HasForeignKey("SearchOutlinePreferencesId");
|
||||
|
||||
b.Navigation("ApplicationUser");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Props.Models.User.SearchOutlinePreferences", b =>
|
||||
{
|
||||
b.HasOne("Props.Models.Search.SearchOutline", "ActiveSearchOutline")
|
||||
.WithMany()
|
||||
.HasForeignKey("ActiveSearchOutlineId");
|
||||
|
||||
b.HasOne("Props.Models.User.ApplicationUser", "ApplicationUser")
|
||||
.WithOne("searchOutlinePreferences")
|
||||
.HasForeignKey("Props.Models.User.SearchOutlinePreferences", "ApplicationUserId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("ActiveSearchOutline");
|
||||
|
||||
b.Navigation("ApplicationUser");
|
||||
});
|
||||
|
||||
@ -398,6 +484,21 @@ namespace Props.Data.Migrations
|
||||
b.Navigation("ApplicationUser");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("QueryWordInfoQueryWordInfo", b =>
|
||||
{
|
||||
b.HasOne("Props.Models.Search.QueryWordInfo", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("FollowingId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("Props.Models.Search.QueryWordInfo", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("PrecedingId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Props.Models.User.ApplicationUser", b =>
|
||||
{
|
||||
b.Navigation("ApplicationPreferences")
|
||||
@ -406,6 +507,12 @@ namespace Props.Data.Migrations
|
||||
b.Navigation("ResultsPreferences")
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("searchOutlinePreferences")
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Props.Models.User.SearchOutlinePreferences", b =>
|
||||
{
|
||||
b.Navigation("SearchOutlines");
|
||||
});
|
||||
#pragma warning restore 612, 618
|
@ -47,12 +47,26 @@ namespace Props.Data.Migrations
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "TrackedListings",
|
||||
name: "Keywords",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<int>(type: "INTEGER", nullable: false)
|
||||
.Annotation("Sqlite:Autoincrement", true),
|
||||
OriginName = table.Column<string>(type: "TEXT", nullable: true),
|
||||
Word = table.Column<string>(type: "TEXT", nullable: false),
|
||||
Hits = table.Column<uint>(type: "INTEGER", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_Keywords", x => x.Id);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "ProductListingInfos",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<int>(type: "INTEGER", nullable: false)
|
||||
.Annotation("Sqlite:Autoincrement", true),
|
||||
ShopName = table.Column<string>(type: "TEXT", nullable: true),
|
||||
Hits = table.Column<uint>(type: "INTEGER", nullable: false),
|
||||
LastUpdated = table.Column<DateTime>(type: "TEXT", nullable: false),
|
||||
ProductUrl = table.Column<string>(type: "TEXT", nullable: true),
|
||||
@ -60,7 +74,7 @@ namespace Props.Data.Migrations
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_TrackedListings", x => x.Id);
|
||||
table.PrimaryKey("PK_ProductListingInfos", x => x.Id);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
@ -212,26 +226,78 @@ namespace Props.Data.Migrations
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "SearchOutlines",
|
||||
name: "QueryWordInfoQueryWordInfo",
|
||||
columns: table => new
|
||||
{
|
||||
FollowingId = table.Column<int>(type: "INTEGER", nullable: false),
|
||||
PrecedingId = table.Column<int>(type: "INTEGER", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_QueryWordInfoQueryWordInfo", x => new { x.FollowingId, x.PrecedingId });
|
||||
table.ForeignKey(
|
||||
name: "FK_QueryWordInfoQueryWordInfo_Keywords_FollowingId",
|
||||
column: x => x.FollowingId,
|
||||
principalTable: "Keywords",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
table.ForeignKey(
|
||||
name: "FK_QueryWordInfoQueryWordInfo_Keywords_PrecedingId",
|
||||
column: x => x.PrecedingId,
|
||||
principalTable: "Keywords",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "SearchOutline",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<int>(type: "INTEGER", nullable: false)
|
||||
.Annotation("Sqlite:Autoincrement", true),
|
||||
ApplicationUserId = table.Column<string>(type: "TEXT", nullable: false),
|
||||
Name = table.Column<string>(type: "TEXT", nullable: false),
|
||||
Filters = table.Column<string>(type: "TEXT", nullable: true),
|
||||
Disabled = table.Column<string>(type: "TEXT", nullable: false)
|
||||
Enabled = table.Column<string>(type: "TEXT", nullable: false),
|
||||
SearchOutlinePreferencesId = table.Column<int>(type: "INTEGER", nullable: true)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_SearchOutlines", x => x.Id);
|
||||
table.PrimaryKey("PK_SearchOutline", x => x.Id);
|
||||
table.ForeignKey(
|
||||
name: "FK_SearchOutlines_AspNetUsers_ApplicationUserId",
|
||||
name: "FK_SearchOutline_AspNetUsers_ApplicationUserId",
|
||||
column: x => x.ApplicationUserId,
|
||||
principalTable: "AspNetUsers",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "SearchOutlinePreferences",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<int>(type: "INTEGER", nullable: false)
|
||||
.Annotation("Sqlite:Autoincrement", true),
|
||||
ApplicationUserId = table.Column<string>(type: "TEXT", nullable: false),
|
||||
ActiveSearchOutlineId = table.Column<int>(type: "INTEGER", nullable: true)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_SearchOutlinePreferences", x => x.Id);
|
||||
table.ForeignKey(
|
||||
name: "FK_SearchOutlinePreferences_AspNetUsers_ApplicationUserId",
|
||||
column: x => x.ApplicationUserId,
|
||||
principalTable: "AspNetUsers",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
table.ForeignKey(
|
||||
name: "FK_SearchOutlinePreferences_SearchOutline_ActiveSearchOutlineId",
|
||||
column: x => x.ActiveSearchOutlineId,
|
||||
principalTable: "SearchOutline",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Restrict);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_ApplicationPreferences_ApplicationUserId",
|
||||
table: "ApplicationPreferences",
|
||||
@ -275,6 +341,11 @@ namespace Props.Data.Migrations
|
||||
column: "NormalizedUserName",
|
||||
unique: true);
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_QueryWordInfoQueryWordInfo_PrecedingId",
|
||||
table: "QueryWordInfoQueryWordInfo",
|
||||
column: "PrecedingId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_ResultsPreferences_ApplicationUserId",
|
||||
table: "ResultsPreferences",
|
||||
@ -282,13 +353,49 @@ namespace Props.Data.Migrations
|
||||
unique: true);
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_SearchOutlines_ApplicationUserId",
|
||||
table: "SearchOutlines",
|
||||
name: "IX_SearchOutline_ApplicationUserId",
|
||||
table: "SearchOutline",
|
||||
column: "ApplicationUserId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_SearchOutline_SearchOutlinePreferencesId",
|
||||
table: "SearchOutline",
|
||||
column: "SearchOutlinePreferencesId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_SearchOutlinePreferences_ActiveSearchOutlineId",
|
||||
table: "SearchOutlinePreferences",
|
||||
column: "ActiveSearchOutlineId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_SearchOutlinePreferences_ApplicationUserId",
|
||||
table: "SearchOutlinePreferences",
|
||||
column: "ApplicationUserId",
|
||||
unique: true);
|
||||
|
||||
migrationBuilder.AddForeignKey(
|
||||
name: "FK_SearchOutline_SearchOutlinePreferences_SearchOutlinePreferencesId",
|
||||
table: "SearchOutline",
|
||||
column: "SearchOutlinePreferencesId",
|
||||
principalTable: "SearchOutlinePreferences",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Restrict);
|
||||
}
|
||||
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropForeignKey(
|
||||
name: "FK_SearchOutline_AspNetUsers_ApplicationUserId",
|
||||
table: "SearchOutline");
|
||||
|
||||
migrationBuilder.DropForeignKey(
|
||||
name: "FK_SearchOutlinePreferences_AspNetUsers_ApplicationUserId",
|
||||
table: "SearchOutlinePreferences");
|
||||
|
||||
migrationBuilder.DropForeignKey(
|
||||
name: "FK_SearchOutline_SearchOutlinePreferences_SearchOutlinePreferencesId",
|
||||
table: "SearchOutline");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "ApplicationPreferences");
|
||||
|
||||
@ -307,20 +414,29 @@ namespace Props.Data.Migrations
|
||||
migrationBuilder.DropTable(
|
||||
name: "AspNetUserTokens");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "ProductListingInfos");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "QueryWordInfoQueryWordInfo");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "ResultsPreferences");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "SearchOutlines");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "TrackedListings");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "AspNetRoles");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "Keywords");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "AspNetUsers");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "SearchOutlinePreferences");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "SearchOutline");
|
||||
}
|
||||
}
|
||||
}
|
@ -186,18 +186,36 @@ namespace Props.Data.Migrations
|
||||
b.Property<DateTime>("LastUpdated")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("OriginName")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("ProductName")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("ProductUrl")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("ShopName")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("TrackedListings");
|
||||
b.ToTable("ProductListingInfos");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Props.Models.Search.QueryWordInfo", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<uint>("Hits")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("Word")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("Keywords");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Props.Models.Search.SearchOutline", b =>
|
||||
@ -210,18 +228,27 @@ namespace Props.Data.Migrations
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Disabled")
|
||||
b.Property<string>("Enabled")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Filters")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<int?>("SearchOutlinePreferencesId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("ApplicationUserId");
|
||||
|
||||
b.ToTable("SearchOutlines");
|
||||
b.HasIndex("SearchOutlinePreferencesId");
|
||||
|
||||
b.ToTable("SearchOutline");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Props.Models.User.ApplicationUser", b =>
|
||||
@ -288,6 +315,29 @@ namespace Props.Data.Migrations
|
||||
b.ToTable("AspNetUsers");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Props.Models.User.SearchOutlinePreferences", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int?>("ActiveSearchOutlineId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("ApplicationUserId")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("ActiveSearchOutlineId");
|
||||
|
||||
b.HasIndex("ApplicationUserId")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("SearchOutlinePreferences");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Props.Shared.Models.User.ApplicationPreferences", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
@ -312,6 +362,21 @@ namespace Props.Data.Migrations
|
||||
b.ToTable("ApplicationPreferences");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("QueryWordInfoQueryWordInfo", b =>
|
||||
{
|
||||
b.Property<int>("FollowingId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("PrecedingId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.HasKey("FollowingId", "PrecedingId");
|
||||
|
||||
b.HasIndex("PrecedingId");
|
||||
|
||||
b.ToTable("QueryWordInfoQueryWordInfo");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim<string>", b =>
|
||||
{
|
||||
b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null)
|
||||
@ -377,11 +442,32 @@ namespace Props.Data.Migrations
|
||||
modelBuilder.Entity("Props.Models.Search.SearchOutline", b =>
|
||||
{
|
||||
b.HasOne("Props.Models.User.ApplicationUser", "ApplicationUser")
|
||||
.WithMany("SearchOutlines")
|
||||
.WithMany()
|
||||
.HasForeignKey("ApplicationUserId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("Props.Models.User.SearchOutlinePreferences", null)
|
||||
.WithMany("SearchOutlines")
|
||||
.HasForeignKey("SearchOutlinePreferencesId");
|
||||
|
||||
b.Navigation("ApplicationUser");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Props.Models.User.SearchOutlinePreferences", b =>
|
||||
{
|
||||
b.HasOne("Props.Models.Search.SearchOutline", "ActiveSearchOutline")
|
||||
.WithMany()
|
||||
.HasForeignKey("ActiveSearchOutlineId");
|
||||
|
||||
b.HasOne("Props.Models.User.ApplicationUser", "ApplicationUser")
|
||||
.WithOne("searchOutlinePreferences")
|
||||
.HasForeignKey("Props.Models.User.SearchOutlinePreferences", "ApplicationUserId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("ActiveSearchOutline");
|
||||
|
||||
b.Navigation("ApplicationUser");
|
||||
});
|
||||
|
||||
@ -396,6 +482,21 @@ namespace Props.Data.Migrations
|
||||
b.Navigation("ApplicationUser");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("QueryWordInfoQueryWordInfo", b =>
|
||||
{
|
||||
b.HasOne("Props.Models.Search.QueryWordInfo", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("FollowingId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("Props.Models.Search.QueryWordInfo", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("PrecedingId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Props.Models.User.ApplicationUser", b =>
|
||||
{
|
||||
b.Navigation("ApplicationPreferences")
|
||||
@ -404,6 +505,12 @@ namespace Props.Data.Migrations
|
||||
b.Navigation("ResultsPreferences")
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("searchOutlinePreferences")
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Props.Models.User.SearchOutlinePreferences", b =>
|
||||
{
|
||||
b.Navigation("SearchOutlines");
|
||||
});
|
||||
#pragma warning restore 612, 618
|
||||
|
14
Props/Extensions/ProductListingExtensions.cs
Normal file
14
Props/Extensions/ProductListingExtensions.cs
Normal file
@ -0,0 +1,14 @@
|
||||
using Props.Shop.Framework;
|
||||
|
||||
namespace Props.Extensions
|
||||
{
|
||||
public static class ProductListingExtensions
|
||||
{
|
||||
public static float? GetRatingToPriceRatio(this ProductListing productListing)
|
||||
{
|
||||
int reviewFactor = productListing.ReviewCount.HasValue ? productListing.ReviewCount.Value : 1;
|
||||
int purchaseFactor = productListing.PurchaseCount.HasValue ? productListing.PurchaseCount.Value : 1;
|
||||
return (productListing.Rating * (reviewFactor > purchaseFactor ? reviewFactor : purchaseFactor)) / (productListing.LowerPrice * productListing.UpperPrice);
|
||||
}
|
||||
}
|
||||
}
|
@ -7,7 +7,7 @@ namespace Props.Models.Search
|
||||
{
|
||||
public int Id { get; set; }
|
||||
|
||||
public string OriginName { get; set; }
|
||||
public string ShopName { get; set; }
|
||||
|
||||
public uint Hits { get; set; }
|
||||
|
||||
|
34
Props/Models/Search/QueryWordInfo.cs
Normal file
34
Props/Models/Search/QueryWordInfo.cs
Normal file
@ -0,0 +1,34 @@
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.ComponentModel.DataAnnotations.Schema;
|
||||
|
||||
namespace Props.Models.Search
|
||||
{
|
||||
public class QueryWordInfo
|
||||
{
|
||||
public int Id { get; set; }
|
||||
|
||||
[Required]
|
||||
public string Word { get; set; }
|
||||
public uint Hits { get; set; }
|
||||
|
||||
[Required]
|
||||
public virtual ISet<QueryWordInfo> Preceding { get; set; }
|
||||
|
||||
[Required]
|
||||
public virtual ISet<QueryWordInfo> Following { get; set; }
|
||||
|
||||
public QueryWordInfo()
|
||||
{
|
||||
this.Preceding = new HashSet<QueryWordInfo>();
|
||||
this.Following = new HashSet<QueryWordInfo>();
|
||||
}
|
||||
|
||||
public QueryWordInfo(ISet<QueryWordInfo> preceding, ISet<QueryWordInfo> following)
|
||||
{
|
||||
this.Preceding = preceding;
|
||||
this.Following = following;
|
||||
}
|
||||
}
|
||||
}
|
@ -17,10 +17,13 @@ namespace Props.Models.Search
|
||||
[Required]
|
||||
public virtual ApplicationUser ApplicationUser { get; set; }
|
||||
|
||||
[Required]
|
||||
public string Name { get; set; } = "Default";
|
||||
|
||||
public Filters Filters { get; set; } = new Filters();
|
||||
|
||||
[Required]
|
||||
public ShopsDisabled Disabled { get; set; } = new ShopsDisabled();
|
||||
public ShopsDisabled Enabled { get; set; } = new ShopsDisabled();
|
||||
|
||||
public sealed class ShopsDisabled : HashSet<string>
|
||||
{
|
||||
@ -68,12 +71,26 @@ namespace Props.Models.Search
|
||||
return
|
||||
Id == other.Id &&
|
||||
Filters.Equals(other.Filters) &&
|
||||
Disabled.Equals(other.Disabled);
|
||||
Enabled.Equals(other.Enabled);
|
||||
}
|
||||
|
||||
public override int GetHashCode()
|
||||
{
|
||||
return Id;
|
||||
return HashCode.Combine(Id, Name);
|
||||
}
|
||||
|
||||
public SearchOutline()
|
||||
{
|
||||
this.Name = "Default";
|
||||
this.Filters = new Filters();
|
||||
this.Enabled = new ShopsDisabled();
|
||||
}
|
||||
|
||||
public SearchOutline(string name, Filters filters, ShopsDisabled disabled)
|
||||
{
|
||||
this.Name = name;
|
||||
this.Filters = filters;
|
||||
this.Enabled = disabled;
|
||||
}
|
||||
}
|
||||
}
|
@ -4,6 +4,7 @@ using System.ComponentModel.DataAnnotations;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Props.Models.Search;
|
||||
using Props.Shared.Models.User;
|
||||
|
||||
@ -11,7 +12,8 @@ namespace Props.Models.User
|
||||
{
|
||||
public class ApplicationUser : IdentityUser
|
||||
{
|
||||
public virtual ISet<SearchOutline> SearchOutlines { get; set; }
|
||||
[Required]
|
||||
public virtual SearchOutlinePreferences searchOutlinePreferences { get; set; }
|
||||
|
||||
[Required]
|
||||
public virtual ResultsPreferences ResultsPreferences { get; private set; }
|
||||
@ -19,15 +21,16 @@ namespace Props.Models.User
|
||||
[Required]
|
||||
public virtual ApplicationPreferences ApplicationPreferences { get; private set; }
|
||||
|
||||
// TODO: Write project system.
|
||||
public ApplicationUser()
|
||||
{
|
||||
searchOutlinePreferences = new SearchOutlinePreferences();
|
||||
ResultsPreferences = new ResultsPreferences();
|
||||
ApplicationPreferences = new ApplicationPreferences();
|
||||
}
|
||||
|
||||
public ApplicationUser(ResultsPreferences resultsPreferences, ApplicationPreferences applicationPreferences)
|
||||
public ApplicationUser(SearchOutlinePreferences searchOutlinePreferences, ResultsPreferences resultsPreferences, ApplicationPreferences applicationPreferences)
|
||||
{
|
||||
this.searchOutlinePreferences = searchOutlinePreferences;
|
||||
this.ResultsPreferences = resultsPreferences;
|
||||
this.ApplicationPreferences = applicationPreferences;
|
||||
}
|
||||
|
37
Props/Models/User/SearchOutlinePreferences.cs
Normal file
37
Props/Models/User/SearchOutlinePreferences.cs
Normal file
@ -0,0 +1,37 @@
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using Props.Models.Search;
|
||||
using Props.Models.User;
|
||||
|
||||
namespace Props.Models.User
|
||||
{
|
||||
public class SearchOutlinePreferences
|
||||
{
|
||||
public int Id { get; set; }
|
||||
|
||||
[Required]
|
||||
public string ApplicationUserId { get; set; }
|
||||
|
||||
[Required]
|
||||
public virtual ApplicationUser ApplicationUser { get; set; }
|
||||
|
||||
[Required]
|
||||
public virtual ISet<SearchOutline> SearchOutlines { get; set; }
|
||||
|
||||
[Required]
|
||||
public virtual SearchOutline ActiveSearchOutline { get; set; }
|
||||
|
||||
public SearchOutlinePreferences()
|
||||
{
|
||||
SearchOutlines = new HashSet<SearchOutline>();
|
||||
ActiveSearchOutline = new SearchOutline();
|
||||
SearchOutlines.Add(ActiveSearchOutline);
|
||||
}
|
||||
|
||||
public SearchOutlinePreferences(ISet<SearchOutline> searchOutlines, SearchOutline activeSearchOutline)
|
||||
{
|
||||
this.SearchOutlines = searchOutlines;
|
||||
this.ActiveSearchOutline = activeSearchOutline;
|
||||
}
|
||||
}
|
||||
}
|
@ -5,7 +5,6 @@ namespace Props.Options
|
||||
public const string Modules = "Modules";
|
||||
public string ShopsDir { get; set; }
|
||||
public bool RecursiveLoad { get; set; }
|
||||
public int MaxResults { get; set; }
|
||||
public string ShopRegex { get; set; }
|
||||
}
|
||||
}
|
9
Props/Options/SearchOptions.cs
Normal file
9
Props/Options/SearchOptions.cs
Normal file
@ -0,0 +1,9 @@
|
||||
namespace Props.Options
|
||||
{
|
||||
public class SearchOptions
|
||||
{
|
||||
public const string Search = "Search";
|
||||
public int MaxResults { get; set; }
|
||||
|
||||
}
|
||||
}
|
@ -1,25 +1,32 @@
|
||||
@page
|
||||
@using Props.Services.Content
|
||||
@model SearchModel
|
||||
@inject IContentManager<SearchModel> ContentManager
|
||||
|
||||
@{
|
||||
ViewData["Title"] = "Search";
|
||||
ViewData["Specific"] = "Search";
|
||||
}
|
||||
|
||||
<div>
|
||||
<div class="my-4 less-concise mx-auto">
|
||||
<div class="mt-4 mb-3">
|
||||
<div class="less-concise mx-auto">
|
||||
<div class="input-group">
|
||||
<input type="text" class="form-control bg-transparent border-primary" placeholder="What are you looking for?" aria-label="Search" aria-describedby="search-btn" id="search-bar">
|
||||
<button class="btn btn-outline-secondary" type="button" id="configuration-toggle" data-bs-toggle="collapse" data-bs-target="#configuration"><i class="bi bi-sliders"></i></button>
|
||||
<button class="btn btn-outline-primary" type="button" id="search-btn">Search</button>
|
||||
<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">
|
||||
<button class="btn btn-outline-secondary" type="button" id="configuration-toggle" data-bs-toggle="collapse"
|
||||
data-bs-target="#configuration"><i class="bi bi-sliders"></i></button>
|
||||
<button class="btn btn-primary" type="button" id="search-btn">Search</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="collapse tear" id="configuration">
|
||||
<div class="collapse tear" id="configuration" x-data="configuration">
|
||||
<div class="p-3">
|
||||
<div class="container invisible">
|
||||
<div class="container">
|
||||
<div class="d-flex">
|
||||
<h1 class="my-2 display-2 me-auto">Configuration</h1>
|
||||
<button class="btn align-self-start" type="button" id="configuration-close" data-bs-toggle="collapse" data-bs-target="#configuration"><i class="bi bi-x-lg"></i></button>
|
||||
<button class="btn align-self-start" type="button" id="configuration-close" data-bs-toggle="collapse"
|
||||
data-bs-target="#configuration"><i class="bi bi-x-lg"></i></button>
|
||||
</div>
|
||||
<div class="row justify-content-md-center">
|
||||
<section class="col-lg px-4">
|
||||
@ -28,10 +35,11 @@
|
||||
<label for="max-price" class="form-label">Maximum Price</label>
|
||||
<div class="input-group">
|
||||
<div class="input-group-text">
|
||||
<input class="form-check-input mt-0" type="checkbox" id="max-price-enabled">
|
||||
<input class="form-check-input mt-0" type="checkbox" id="max-price-enabled"
|
||||
x-model="maxPriceEnabled">
|
||||
</div>
|
||||
<span class="input-group-text">$</span>
|
||||
<input type="number" class="form-control" min="0" id="max-price">
|
||||
<input type="number" class="form-control" min="0" id="max-price" x-model="maxPrice">
|
||||
<span class="input-group-text">.00</span>
|
||||
</div>
|
||||
</div>
|
||||
@ -39,7 +47,7 @@
|
||||
<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">
|
||||
<input type="number" class="form-control" min="0" id="min-price" x-model="minPrice">
|
||||
<span class="input-group-text">.00</span>
|
||||
</div>
|
||||
</div>
|
||||
@ -50,13 +58,14 @@
|
||||
<input class="form-check-input mt-0" type="checkbox" id="max-shipping-enabled">
|
||||
</div>
|
||||
<span class="input-group-text">$</span>
|
||||
<input type="number" class="form-control" min="0" id="max-shipping">
|
||||
<input type="number" class="form-control" min="0" id="max-shipping" x-model="maxShipping">
|
||||
<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">
|
||||
<input class="form-check-input" type="checkbox" id="keep-unknown-shipping"
|
||||
x-model="keepUnknownShipping">
|
||||
<label class="form-check-label" for="keep-unknown-shipping">Keep Unknown Shipping</label>
|
||||
</div>
|
||||
</div>
|
||||
@ -66,37 +75,41 @@
|
||||
<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">
|
||||
<input type="number" class="form-control" min="0" id="min-purchases" x-model="minPurchases">
|
||||
<span class="input-group-text">Purchases</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" type="checkbox" id="keep-unknown-purchases">
|
||||
<input class="form-check-input" type="checkbox" id="keep-unknown-purchases"
|
||||
x-model="keepUnknownPurchases">
|
||||
<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">
|
||||
<input type="number" class="form-control" min="0" id="min-reviews" x-model="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">
|
||||
<label class="form-check-label" for="keep-unknown-reviews">Keep Unknown Number of Reviews</label>
|
||||
<input class="form-check-input" type="checkbox" id="keep-unknown-reviews"
|
||||
x-model="keepUnknownReviews">
|
||||
<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">
|
||||
<div id="min-rating-display" class="form-text"></div>
|
||||
<input type="range" class="form-range" id="min-rating" min="0" max="100" step="1"
|
||||
x-model="minRating">
|
||||
<div id="min-rating-display" class="form-text">Minimum rating: <b x-text="minRating"></b>%</div>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" type="checkbox" id="keep-unrated">
|
||||
<input class="form-check-input" type="checkbox" id="keep-unrated" x-model="keepUnrated">
|
||||
<label class="form-check-label" for="keep-unrated">Keep Unrated Items</label>
|
||||
</div>
|
||||
</div>
|
||||
@ -104,6 +117,14 @@
|
||||
<section class="col-lg px-4">
|
||||
<h3>Shops Enabled</h3>
|
||||
<div class="mb-3 px-3" id="shop-checkboxes">
|
||||
<template x-for="shop in Object.keys(shops)">
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" type="checkbox" :id="`${shop}-enabled`"
|
||||
x-model="shops[shop]">
|
||||
<label class="form-check-label" :for="`${shop}-enabled`"><span
|
||||
x-text="shop"></span></label>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
@ -111,4 +132,111 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@* TODO: Add results display and default results display *@
|
||||
<div id="content-pages" class="multipage mt-3 invisible">
|
||||
<ul class="nav nav-pills selectors">
|
||||
<li class="nav-item" role="presentation">
|
||||
<button type="button" data-bs-toggle="pill" data-bs-target="#quick-picks-slide"><i
|
||||
class="bi bi-stopwatch"></i></button>
|
||||
</li>
|
||||
<li class="nav-item" role="presentation">
|
||||
<button type="button" data-bs-toggle="pill" data-bs-target="#results-slide"><i
|
||||
class="bi bi-view-list"></i></button>
|
||||
</li>
|
||||
<li class="nav-item" role="presentation">
|
||||
<button type="button" data-bs-toggle="pill" data-bs-target="#info-slide"><i
|
||||
class="bi bi-info-lg"></i></button>
|
||||
</li>
|
||||
</ul>
|
||||
<div class="multipage-slides tab-content">
|
||||
<div class="multipage-slide tab-pane fade" id="quick-picks-slide">
|
||||
<div class="multipage-title">
|
||||
<h1 class="display-2"><i class="bi bi-stopwatch"></i> Quick Picks</h1>
|
||||
@if (Model.SearchResults != null)
|
||||
{
|
||||
<p>@ContentManager.Json.quickPicks.searched</p>
|
||||
}
|
||||
else
|
||||
{
|
||||
<p>@ContentManager.Json.quickPicks.prompt</p>
|
||||
}
|
||||
<hr class="less-concise">
|
||||
</div>
|
||||
<div class="multipage-content">
|
||||
@if (Model.SearchResults != null)
|
||||
{
|
||||
@if (Model.BestRatingPriceRatio != null)
|
||||
{
|
||||
<p>We found this product to have the best rating to price ratio.</p>
|
||||
}
|
||||
|
||||
@if (Model.TopRated != null)
|
||||
{
|
||||
<p>This listing was the one that had the highest rating.</p>
|
||||
}
|
||||
|
||||
@if (Model.MostPurchases != null)
|
||||
{
|
||||
<p>This listing has the most purchases.</p>
|
||||
}
|
||||
|
||||
@if (Model.MostReviews != null)
|
||||
{
|
||||
<p>This listing had the most reviews.</p>
|
||||
}
|
||||
|
||||
@if (Model.BestPrice != null)
|
||||
{
|
||||
<p>Looking for the lowest price? Well here it is.</p>
|
||||
}
|
||||
|
||||
@* TODO: Add display for top results. *@
|
||||
}
|
||||
else
|
||||
{
|
||||
<div class="text-center less-concise text-muted flex-grow-1 justify-content-center d-flex flex-column">
|
||||
<h2>@ContentManager.Json.notSearched</h2>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
<div class="multipage-slide tab-pane fade" id="results-slide" x-data>
|
||||
<div class="multipage-title">
|
||||
<h2><i class="bi bi-view-list"></i> Results</h2>
|
||||
@if (Model.SearchResults != null)
|
||||
{
|
||||
<p>@ContentManager.Json.results.searched</p>
|
||||
}
|
||||
else
|
||||
{
|
||||
<p>@ContentManager.Json.results.prompt</p>
|
||||
}
|
||||
<hr class="less-concise">
|
||||
</div>
|
||||
<div class="multipage-content">
|
||||
@if (Model.SearchResults != null)
|
||||
{
|
||||
@* TODO: Display results with UI for sorting and changing views. *@
|
||||
}
|
||||
else
|
||||
{
|
||||
<div class="text-center less-concise text-muted flex-grow-1 justify-content-center d-flex flex-column">
|
||||
<h2>@ContentManager.Json.notSearched</h2>
|
||||
</div>
|
||||
}
|
||||
</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>
|
@ -1,9 +1,54 @@
|
||||
using System;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Castle.Core.Internal;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Mvc.RazorPages;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Props.Data;
|
||||
using Props.Extensions;
|
||||
using Props.Models.Search;
|
||||
using Props.Models.User;
|
||||
using Props.Services.Modules;
|
||||
using Props.Shop.Framework;
|
||||
|
||||
namespace Props.Pages
|
||||
{
|
||||
public class SearchModel : PageModel
|
||||
{
|
||||
// TODO: Complete the search model.
|
||||
[BindProperty(Name = "q", SupportsGet = true)]
|
||||
public string SearchQuery { get; set; }
|
||||
public IEnumerable<ProductListing> SearchResults { get; private set; }
|
||||
public ProductListing BestRatingPriceRatio { get; private set; }
|
||||
public ProductListing TopRated { get; private set; }
|
||||
public ProductListing MostPurchases { get; private set; }
|
||||
public ProductListing MostReviews { get; private set; }
|
||||
public ProductListing BestPrice { get; private set; }
|
||||
|
||||
private ISearchManager searchManager;
|
||||
private UserManager<ApplicationUser> userManager;
|
||||
private IMetricsManager analytics;
|
||||
|
||||
public SearchModel(ISearchManager searchManager, UserManager<ApplicationUser> userManager, IMetricsManager analyticsManager)
|
||||
{
|
||||
this.searchManager = searchManager;
|
||||
this.userManager = userManager;
|
||||
this.analytics = analyticsManager;
|
||||
}
|
||||
|
||||
public async Task OnGet()
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(SearchQuery)) return;
|
||||
SearchOutline activeSearchOutline = User.Identity.IsAuthenticated ? (await userManager.GetUserAsync(User)).searchOutlinePreferences.ActiveSearchOutline : new SearchOutline();
|
||||
this.SearchResults = 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();
|
||||
}
|
||||
}
|
||||
}
|
@ -11,6 +11,10 @@
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>@ViewData["Title"] - Props</title>
|
||||
<script src="~/js/site.js" asp-append-version="true"></script>
|
||||
@if (!string.IsNullOrEmpty((ViewData["Specific"] as string)))
|
||||
{
|
||||
<script defer src="@($"~/js/specific/{(ViewData["Specific"])}.js")" asp-append-version="true"></script>
|
||||
}
|
||||
</head>
|
||||
|
||||
<body class="theme-light">
|
||||
@ -34,10 +38,12 @@
|
||||
@if (SignInManager.IsSignedIn(User))
|
||||
{
|
||||
<li class="nav-item">
|
||||
<nav-link class="nav-link" asp-area="Identity" asp-page="/Account/Manage/Index" title="Manage">Hello @User.Identity.Name!</nav-link>
|
||||
<nav-link class="nav-link" asp-area="Identity" asp-page="/Account/Manage/Index"
|
||||
title="Manage">Hello @User.Identity.Name!</nav-link>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<form class="form-inline" asp-area="Identity" asp-page="/Account/Logout" asp-route-returnUrl="@Url.Page("/", new { area = "" })" method="post">
|
||||
<form class="form-inline" asp-area="Identity" asp-page="/Account/Logout"
|
||||
asp-route-returnUrl="@Url.Page("/", new { area = "" })" method="post">
|
||||
<button type="submit" class="nav-link btn btn-link">Logout</button>
|
||||
</form>
|
||||
</li>
|
||||
@ -45,7 +51,8 @@
|
||||
else
|
||||
{
|
||||
<li class="nav-item">
|
||||
<nav-link class="nav-link" asp-area="Identity" asp-page="/Account/Register">Register</nav-link>
|
||||
<nav-link class="nav-link" asp-area="Identity" asp-page="/Account/Register">Register
|
||||
</nav-link>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<nav-link class="nav-link" asp-area="Identity" asp-page="/Account/Login">Login</nav-link>
|
||||
@ -62,11 +69,6 @@
|
||||
<footer id="footer">
|
||||
© 2021 - Props - <a asp-area="" asp-page="/Privacy">Privacy</a>
|
||||
</footer>
|
||||
|
||||
@if (!string.IsNullOrEmpty((ViewData["Specific"] as string)))
|
||||
{
|
||||
<script src="@($"~/js/specific/{(ViewData["Specific"])}.js")" asp-append-version="true"></script>
|
||||
}
|
||||
@await RenderSectionAsync("Scripts", required: false)
|
||||
</body>
|
||||
|
||||
|
@ -35,6 +35,9 @@
|
||||
<Content Include=".\content\**\*">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</Content>
|
||||
<Content Include=".\shops\**\*">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</Content>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
18
Props/Services/Modules/IMetricsManager.cs
Normal file
18
Props/Services/Modules/IMetricsManager.cs
Normal file
@ -0,0 +1,18 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
using Props.Models.Search;
|
||||
using Props.Shop.Framework;
|
||||
|
||||
namespace Props.Services.Modules
|
||||
{
|
||||
public interface IMetricsManager
|
||||
{
|
||||
public IEnumerable<ProductListingInfo> RetrieveTopListings(int max = 10);
|
||||
|
||||
public IEnumerable<string> RetrieveCommonKeywords(int max = 50);
|
||||
|
||||
public void RegisterSearchQuery(string query);
|
||||
|
||||
public void RegisterListing(ProductListing productListing, string shopName);
|
||||
}
|
||||
}
|
12
Props/Services/Modules/ISearchManager.cs
Normal file
12
Props/Services/Modules/ISearchManager.cs
Normal file
@ -0,0 +1,12 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
using Props.Models.Search;
|
||||
using Props.Shop.Framework;
|
||||
|
||||
namespace Props.Services.Modules
|
||||
{
|
||||
public interface ISearchManager
|
||||
{
|
||||
public IEnumerable<ProductListing> Search(string query, SearchOutline searchOutline);
|
||||
}
|
||||
}
|
@ -1,3 +1,4 @@
|
||||
using System;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
@ -6,9 +7,10 @@ using Props.Shop.Framework;
|
||||
|
||||
namespace Props.Services.Modules
|
||||
{
|
||||
public interface IShopManager
|
||||
public interface IShopManager : IDisposable
|
||||
{
|
||||
public IEnumerable<string> AvailableShops();
|
||||
public Task<IList<ProductListing>> Search(string query, SearchOutline searchOutline);
|
||||
public IEnumerable<string> GetAllShopNames();
|
||||
public IShop GetShop(string name);
|
||||
public IEnumerable<IShop> GetAllShops();
|
||||
}
|
||||
}
|
85
Props/Services/Modules/LiveMetricsManager.cs
Normal file
85
Props/Services/Modules/LiveMetricsManager.cs
Normal file
@ -0,0 +1,85 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Props.Data;
|
||||
using Props.Models.Search;
|
||||
using Props.Shop.Framework;
|
||||
|
||||
namespace Props.Services.Modules
|
||||
{
|
||||
public class LiveMetricsManager : IMetricsManager
|
||||
{
|
||||
private ILogger<LiveMetricsManager> logger;
|
||||
ApplicationDbContext dbContext;
|
||||
public LiveMetricsManager(ApplicationDbContext dbContext, ILogger<LiveMetricsManager> logger)
|
||||
{
|
||||
this.logger = logger;
|
||||
this.dbContext = dbContext;
|
||||
}
|
||||
public IEnumerable<ProductListingInfo> RetrieveTopListings(int max)
|
||||
{
|
||||
if (dbContext.ProductListingInfos == null) return null;
|
||||
return (from l in dbContext.ProductListingInfos
|
||||
orderby l.Hits descending
|
||||
select l).Take(max);
|
||||
}
|
||||
|
||||
public IEnumerable<string> RetrieveCommonKeywords(int max)
|
||||
{
|
||||
if (dbContext.Keywords == null) return null;
|
||||
return (from k in dbContext.Keywords
|
||||
orderby k.Hits descending
|
||||
select k.Word).Take(max);
|
||||
}
|
||||
|
||||
public void RegisterSearchQuery(string query)
|
||||
{
|
||||
query = query.ToLower();
|
||||
string[] tokens = query.Split(' ');
|
||||
QueryWordInfo[] wordInfos = new QueryWordInfo[tokens.Length];
|
||||
for (int wordIndex = 0; wordIndex < tokens.Length; wordIndex++)
|
||||
{
|
||||
QueryWordInfo queryWordInfo = dbContext.Keywords.Where((k) => k.Word.ToLower().Equals(tokens[wordIndex])).SingleOrDefault() ?? new QueryWordInfo();
|
||||
if (queryWordInfo.Hits == 0)
|
||||
{
|
||||
queryWordInfo.Word = tokens[wordIndex];
|
||||
dbContext.Keywords.Add(queryWordInfo);
|
||||
}
|
||||
queryWordInfo.Hits += 1;
|
||||
wordInfos[wordIndex] = queryWordInfo;
|
||||
}
|
||||
for (int wordIndex = 0; wordIndex < tokens.Length; wordIndex++)
|
||||
{
|
||||
for (int beforeIndex = 0; beforeIndex < wordIndex; beforeIndex++)
|
||||
{
|
||||
wordInfos[wordIndex].Preceding.Add(wordInfos[beforeIndex]);
|
||||
}
|
||||
for (int afterIndex = wordIndex; afterIndex < tokens.Length; afterIndex++)
|
||||
{
|
||||
wordInfos[wordIndex].Following.Add(wordInfos[afterIndex]);
|
||||
}
|
||||
}
|
||||
dbContext.SaveChanges();
|
||||
|
||||
}
|
||||
|
||||
public void RegisterListing(ProductListing productListing, string shopName)
|
||||
{
|
||||
ProductListingInfo productListingInfo =
|
||||
(from info in dbContext.ProductListingInfos
|
||||
where info.ProductUrl.Equals(productListing.URL)
|
||||
select info).SingleOrDefault() ?? new ProductListingInfo();
|
||||
if (productListingInfo.Hits == 0)
|
||||
{
|
||||
dbContext.Add(productListingInfo);
|
||||
}
|
||||
productListingInfo.ShopName = shopName;
|
||||
productListingInfo.ProductName = productListing.Name;
|
||||
productListingInfo.ProductUrl = productListing.URL;
|
||||
productListingInfo.LastUpdated = DateTime.UtcNow;
|
||||
productListingInfo.Hits += 1;
|
||||
dbContext.SaveChanges();
|
||||
}
|
||||
}
|
||||
}
|
58
Props/Services/Modules/LiveSearchManager.cs
Normal file
58
Props/Services/Modules/LiveSearchManager.cs
Normal file
@ -0,0 +1,58 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
using Castle.Core.Logging;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Props.Models.Search;
|
||||
using Props.Options;
|
||||
using Props.Shop.Framework;
|
||||
|
||||
namespace Props.Services.Modules
|
||||
{
|
||||
public class LiveSearchManager : ISearchManager
|
||||
{
|
||||
private ILogger<LiveSearchManager> logger;
|
||||
private SearchOptions searchOptions;
|
||||
private IShopManager shopManager;
|
||||
private IMetricsManager metricsManager;
|
||||
|
||||
public LiveSearchManager(IMetricsManager metricsManager, IShopManager shopManager, IConfiguration configuration, ILogger<LiveSearchManager> logger)
|
||||
{
|
||||
this.logger = logger;
|
||||
this.metricsManager = metricsManager;
|
||||
this.shopManager = shopManager;
|
||||
this.searchOptions = configuration.GetSection(SearchOptions.Search).Get<SearchOptions>();
|
||||
}
|
||||
public IEnumerable<ProductListing> Search(string query, SearchOutline searchOutline)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(query)) throw new ArgumentException($"Query \"{query}\" is null or whitepsace.");
|
||||
if (searchOutline == null) throw new ArgumentNullException("searchOutline");
|
||||
List<ProductListing> results = new List<ProductListing>();
|
||||
metricsManager.RegisterSearchQuery(query);
|
||||
logger.LogDebug("Searching for \"{0}\".", query);
|
||||
|
||||
foreach (string shopName in shopManager.GetAllShopNames())
|
||||
{
|
||||
if (searchOutline.Enabled[shopName])
|
||||
{
|
||||
logger.LogDebug("Checking \"{0}\".", shopName);
|
||||
int amount = 0;
|
||||
foreach (ProductListing product in shopManager.GetShop(shopName).Search(query, searchOutline.Filters))
|
||||
{
|
||||
if (searchOutline.Filters.Validate(product))
|
||||
{
|
||||
amount += 1;
|
||||
metricsManager.RegisterListing(product, shopName);
|
||||
results.Add(product);
|
||||
}
|
||||
if (amount >= searchOptions.MaxResults) break;
|
||||
}
|
||||
logger.LogDebug("Found {0} listings that satisfy the search filters from {1}.", amount, shopName);
|
||||
}
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
}
|
||||
}
|
@ -1,24 +1,30 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using System.Text;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.CodeAnalysis.CSharp;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Props.Data;
|
||||
using Props.Models.Search;
|
||||
using Props.Options;
|
||||
using Props.Shop.Framework;
|
||||
|
||||
namespace Props.Services.Modules
|
||||
{
|
||||
public class LocalShopManager : IShopManager
|
||||
public class ModularShopManager : IShopManager
|
||||
{
|
||||
private ILogger<LocalShopManager> logger;
|
||||
private ILogger<ModularShopManager> logger;
|
||||
private Dictionary<string, IShop> shops;
|
||||
private ModulesOptions options;
|
||||
private IConfiguration configuration;
|
||||
public LocalShopManager(IConfiguration configuration, ILogger<LocalShopManager> logger)
|
||||
private bool disposedValue;
|
||||
|
||||
public ModularShopManager(IConfiguration configuration, ILogger<ModularShopManager> logger)
|
||||
{
|
||||
this.configuration = configuration;
|
||||
this.logger = logger;
|
||||
@ -34,35 +40,25 @@ namespace Props.Services.Modules
|
||||
}
|
||||
}
|
||||
|
||||
public IEnumerable<string> AvailableShops()
|
||||
public IEnumerable<string> GetAllShopNames()
|
||||
{
|
||||
return shops.Keys;
|
||||
}
|
||||
|
||||
public async Task<IList<ProductListing>> Search(string query, SearchOutline searchOutline)
|
||||
|
||||
public IShop GetShop(string name)
|
||||
{
|
||||
List<ProductListing> results = new List<ProductListing>();
|
||||
foreach (string shopName in shops.Keys)
|
||||
{
|
||||
if (!searchOutline.Disabled[shopName])
|
||||
{
|
||||
int amount = 0;
|
||||
await foreach (ProductListing product in shops[shopName].Search(query, searchOutline.Filters))
|
||||
{
|
||||
if (searchOutline.Filters.Validate(product))
|
||||
{
|
||||
amount += 1;
|
||||
results.Add(product);
|
||||
}
|
||||
if (amount >= options.MaxResults) break;
|
||||
}
|
||||
}
|
||||
}
|
||||
return results;
|
||||
return shops[name];
|
||||
}
|
||||
|
||||
public IEnumerable<IShop> GetAllShops()
|
||||
{
|
||||
return shops.Values;
|
||||
}
|
||||
|
||||
private IEnumerable<IShop> LoadShops(string shopsDir, string shopRegex, bool recursiveLoad)
|
||||
{
|
||||
Stack<Task> asyncInitTasks = new Stack<Task>();
|
||||
Stack<string> directories = new Stack<string>();
|
||||
directories.Push(shopsDir);
|
||||
string currentDirectory = null;
|
||||
@ -90,7 +86,11 @@ namespace Props.Services.Modules
|
||||
IShop shop = Activator.CreateInstance(type) as IShop;
|
||||
if (shop != null)
|
||||
{
|
||||
// TODO: load persisted shop data.
|
||||
shop.Initialize(null);
|
||||
asyncInitTasks.Push(shop.InitializeAsync(null));
|
||||
success += 1;
|
||||
logger.LogDebug("Loaded \"{0}\".", shop.ShopName);
|
||||
yield return shop;
|
||||
}
|
||||
}
|
||||
@ -102,6 +102,32 @@ namespace Props.Services.Modules
|
||||
}
|
||||
}
|
||||
}
|
||||
logger.LogDebug("Waiting for all shops to finish asynchronous initialization.");
|
||||
Task.WaitAll(asyncInitTasks.ToArray());
|
||||
logger.LogDebug("All shops finished asynchronous initialization.");
|
||||
}
|
||||
|
||||
protected virtual void Dispose(bool disposing)
|
||||
{
|
||||
if (!disposedValue)
|
||||
{
|
||||
if (disposing)
|
||||
{
|
||||
foreach (string shopName in shops.Keys)
|
||||
{
|
||||
// TODO: Get shop data to persist.
|
||||
shops[shopName].Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
disposedValue = true;
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
Dispose(disposing: true);
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
}
|
||||
}
|
@ -55,7 +55,9 @@ namespace Props
|
||||
services.AddRazorPages();
|
||||
|
||||
services.AddSingleton(typeof(IContentManager<>), typeof(CachedContentManager<>));
|
||||
services.AddSingleton<IShopManager, LocalShopManager>();
|
||||
services.AddSingleton<IShopManager, ModularShopManager>();
|
||||
services.AddScoped<IMetricsManager, LiveMetricsManager>();
|
||||
services.AddScoped<ISearchManager, LiveSearchManager>();
|
||||
}
|
||||
|
||||
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
|
||||
|
@ -2,16 +2,9 @@
|
||||
"DetailedErrors": true,
|
||||
"Logging": {
|
||||
"LogLevel": {
|
||||
"Default": "Information",
|
||||
"Default": "Debug",
|
||||
"Microsoft": "Warning",
|
||||
"Microsoft.Hosting.Lifetime": "Information"
|
||||
}
|
||||
},
|
||||
"webOptimizer": {
|
||||
"enableCaching": false,
|
||||
"enableMemoryCache": false,
|
||||
"enableDiskCache": false,
|
||||
"enableTagHelperBundling": false,
|
||||
"allowEmptyBundle": false
|
||||
}
|
||||
}
|
@ -12,9 +12,11 @@
|
||||
"Modules": {
|
||||
"ShopsDir": "./shops",
|
||||
"RecursiveLoad": "false",
|
||||
"MaxResults": "100",
|
||||
"ShopRegex": "Props\\.Shop\\.."
|
||||
},
|
||||
"Search": {
|
||||
"MaxResults": 100
|
||||
},
|
||||
"Content": {
|
||||
"Dir": "./content"
|
||||
},
|
||||
|
@ -1,117 +1,76 @@
|
||||
import Alpine from "alpinejs";
|
||||
import { apiHttp } from "~/assets/js/services/http.js";
|
||||
|
||||
// All input fields.
|
||||
let inputs = {
|
||||
maxPriceEnabled: document.getElementById("max-price-enabled"),
|
||||
maxShippingEnabled: document.getElementById("max-shipping-enabled"),
|
||||
minRating: document.getElementById("min-rating"),
|
||||
maxPrice: document.getElementById("max-price"),
|
||||
maxShipping: document.getElementById("max-shipping"),
|
||||
keepUnknownPurchases: document.getElementById("keep-unknown-purchases"),
|
||||
keepUnknownReviews: document.getElementById("keep-unknown-reviews"),
|
||||
keepUnknownShipping: document.getElementById("keep-unknown-shipping"),
|
||||
keepUnrated: document.getElementById("keep-unrated"),
|
||||
minPrice: document.getElementById("min-price"),
|
||||
minPurchases: document.getElementById("min-purchases"),
|
||||
minReviews: document.getElementById("min-reviews"),
|
||||
shopToggles: {}
|
||||
};
|
||||
const startingSlide = "#quick-picks-slide";
|
||||
|
||||
async function main() {
|
||||
setupInteractiveBehavior();
|
||||
await setupInitialValues((await apiHttp.get("/Search/Default/Filters")).data);
|
||||
await setupShopToggles((await apiHttp.get("/Search/Shops/Available")).data);
|
||||
|
||||
document.querySelector("#configuration .invisible").classList.remove("invisible"); // Load completed, show the UI.
|
||||
}
|
||||
|
||||
function setupInteractiveBehavior() {
|
||||
let configurationElem = document.getElementById("configuration");
|
||||
function initInteractiveElements() {
|
||||
let configurationToggle = document.getElementById("configuration-toggle");
|
||||
let configurationElem = document.getElementById("configuration");
|
||||
configurationElem.addEventListener("show.bs.collapse", function () {
|
||||
configurationToggle.classList.add("active");
|
||||
});
|
||||
configurationElem.addEventListener("hidden.bs.collapse", function () {
|
||||
configurationToggle.classList.remove("active");
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
inputs.maxPriceEnabled.addEventListener("change", function () {
|
||||
inputs.maxPrice.disabled = !this.checked;
|
||||
});
|
||||
|
||||
inputs.maxShippingEnabled.addEventListener("change", function () {
|
||||
inputs.maxShipping.disabled = !this.checked;
|
||||
});
|
||||
|
||||
inputs.minRating.addEventListener("input", function () {
|
||||
document.getElementById("min-rating-display").innerHTML = `Minimum rating: ${this.value}%`;
|
||||
async function initConfigurationData() {
|
||||
const givenConfig = (await apiHttp.get("/SearchOutline/Filters")).data;
|
||||
const disabledShops = (await apiHttp.get("/SearchOutline/DisabledShops")).data;
|
||||
const availableShops = (await apiHttp.get("/Search/Available")).data;
|
||||
document.addEventListener("alpine:init", () => {
|
||||
Alpine.data("configuration", () => {
|
||||
const configuration = {
|
||||
maxPriceEnabled: givenConfig.enableUpperPrice,
|
||||
maxPrice: givenConfig.upperPrice,
|
||||
minPrice: givenConfig.lowerPrice,
|
||||
maxShippingEnabled: givenConfig.enableMaxShippingFee,
|
||||
maxShipping: givenConfig.maxShippingFee,
|
||||
keepUnknownShipping: givenConfig.keepUnknownShipping,
|
||||
minRating: givenConfig.minRating * 100,
|
||||
keepUnrated: givenConfig.keepUnrated,
|
||||
minReviews: givenConfig.minReviews,
|
||||
keepUnknownReviews: givenConfig.keepUnknownReviewCount,
|
||||
keepUnknownPurchases: givenConfig.keepUnknownPurchaseCount,
|
||||
minPurchases: givenConfig.minPurchases,
|
||||
shops: {},
|
||||
};
|
||||
availableShops.forEach(shop => {
|
||||
configuration.shops[shop] = !disabledShops.includes(shop);
|
||||
});
|
||||
return configuration;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async function setupInitialValues(filters) {
|
||||
inputs.maxShippingEnabled.checked = filters.enableMaxShippingFee;
|
||||
inputs.maxShippingEnabled.dispatchEvent(new Event("change"));
|
||||
function initSlides() {
|
||||
document.querySelectorAll("#content-pages > .selectors > .nav-item > button").forEach(tabElem => {
|
||||
tabElem.addEventListener("click", () => {
|
||||
const destUrl = new URL(tabElem.getAttribute("data-bs-target"), window.location.href);
|
||||
if (location.href === destUrl.href) return;
|
||||
history.pushState({}, document.title, destUrl);
|
||||
});
|
||||
});
|
||||
const goTo = () => {
|
||||
const match = location.href.match("(#[\\w-]+)");
|
||||
const idAnchor = match && match[1] ? match[1] : startingSlide;
|
||||
document.querySelector("#content-pages > .selectors > .nav-item > .active")?.classList.remove("active");
|
||||
document.querySelector("#content-pages > .multipage-slides > .active.show")?.classList.remove("active", "show");
|
||||
document.querySelector(`#content-pages > .selectors > .nav-item > [data-bs-target="${idAnchor}"]`).classList.add("active");
|
||||
document.querySelector(`#content-pages > .multipage-slides > ${idAnchor}`).classList.add("active", "show");
|
||||
};
|
||||
window.addEventListener("popstate", goTo);
|
||||
goTo();
|
||||
|
||||
inputs.maxPriceEnabled.checked = filters.enableUpperPrice;
|
||||
inputs.maxPriceEnabled.dispatchEvent(new Event("change"));
|
||||
|
||||
inputs.keepUnknownPurchases.checked = filters.keepUnknownPurchaseCount;
|
||||
inputs.keepUnknownPurchases.dispatchEvent(new Event("change"));
|
||||
|
||||
inputs.keepUnknownReviews.checked = filters.keepUnknownReviewCount;
|
||||
inputs.keepUnknownReviews.dispatchEvent(new Event("change"));
|
||||
|
||||
inputs.keepUnknownShipping.checked = filters.keepUnknownShipping;
|
||||
inputs.keepUnknownShipping.dispatchEvent(new Event("change"));
|
||||
|
||||
inputs.keepUnrated.checked = filters.keepUnrated;
|
||||
inputs.keepUnrated.dispatchEvent(new Event("change"));
|
||||
|
||||
inputs.minPrice.value = filters.lowerPrice;
|
||||
inputs.minPrice.dispatchEvent(new Event("change"));
|
||||
|
||||
inputs.maxShipping.value = filters.maxShippingFee;
|
||||
inputs.maxShipping.dispatchEvent(new Event("change"));
|
||||
|
||||
inputs.minPurchases.value = filters.minPurchases;
|
||||
inputs.minPurchases.dispatchEvent(new Event("change"));
|
||||
|
||||
inputs.minRating.value = filters.minRating * 100;
|
||||
inputs.minRating.dispatchEvent(new Event("input"));
|
||||
|
||||
inputs.minReviews.value = filters.minReviews;
|
||||
inputs.minReviews.dispatchEvent(new Event("change"));
|
||||
|
||||
inputs.maxPrice.value = filters.upperPrice;
|
||||
inputs.maxPrice.dispatchEvent(new Event("change"));
|
||||
require("bootstrap/js/dist/tab.js");
|
||||
document.querySelector("#content-pages").classList.remove("invisible");
|
||||
}
|
||||
|
||||
async function setupShopToggles(availableShops) {
|
||||
let disabledShops = (await apiHttp.get("/Search/Default/DisabledShops")).data;
|
||||
let shopsElem = document.getElementById("shop-checkboxes");
|
||||
availableShops.forEach(shopName => {
|
||||
let id = `${shopName}-enabled`;
|
||||
let shopLabelElem = document.createElement("label");
|
||||
shopLabelElem.classList.add("form-check-label");
|
||||
shopLabelElem.htmlFor = id;
|
||||
shopLabelElem.innerHTML = `Enable ${shopName}`;
|
||||
|
||||
let shopCheckboxElem = document.createElement("input");
|
||||
shopCheckboxElem.classList.add("form-check-input");
|
||||
shopCheckboxElem.type = "checkbox";
|
||||
shopCheckboxElem.id = id;
|
||||
shopCheckboxElem.checked = !disabledShops.includes(shopName);
|
||||
inputs.shopToggles[shopName] = shopCheckboxElem;
|
||||
|
||||
let shopToggleElem = document.createElement("div");
|
||||
shopToggleElem.classList.add("form-check");
|
||||
shopToggleElem.appendChild(shopCheckboxElem);
|
||||
shopToggleElem.appendChild(shopLabelElem);
|
||||
shopsElem.appendChild(shopToggleElem);
|
||||
});
|
||||
async function main() {
|
||||
initInteractiveElements();
|
||||
await initConfigurationData();
|
||||
initSlides();
|
||||
Alpine.start();
|
||||
}
|
||||
|
||||
main();
|
||||
|
||||
// TODO: Implement search.
|
||||
|
@ -17,10 +17,11 @@ header > nav {
|
||||
&.active {
|
||||
@include themer.themed {
|
||||
color: themer.color-of("navbar-active");
|
||||
border-color: themer.color-of("navbar-active");
|
||||
border-bottom-color: themer.color-of("navbar-active");
|
||||
}
|
||||
padding-bottom: 2px;
|
||||
border-bottom-style: solid;
|
||||
border-bottom-width: 1px;
|
||||
border-bottom-width: 2px;
|
||||
}
|
||||
}
|
||||
|
||||
@ -82,26 +83,32 @@ footer {
|
||||
}
|
||||
|
||||
.concise {
|
||||
@extend .container;
|
||||
max-width: 630px;
|
||||
width: inherit;
|
||||
}
|
||||
|
||||
.less-concise {
|
||||
@extend .container;
|
||||
max-width: 720px;
|
||||
width: inherit;
|
||||
}
|
||||
|
||||
hr.concise {
|
||||
@extend .my-3;
|
||||
@include themer.themed {
|
||||
color: themer.color-of("special");
|
||||
hr {
|
||||
&.concise {
|
||||
@extend .my-2;
|
||||
width: 15%;
|
||||
max-width: 160px;
|
||||
min-width: 32px;
|
||||
height: 2px;
|
||||
}
|
||||
&.less-concise {
|
||||
@extend .my-2;
|
||||
width: 30%;
|
||||
max-width: 270px;
|
||||
min-width: 32px;
|
||||
height: 2px;
|
||||
}
|
||||
width: 15%;
|
||||
max-width: 160px;
|
||||
min-width: 32px;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
height: 2px;
|
||||
}
|
||||
|
||||
html {
|
||||
@ -118,4 +125,62 @@ body {
|
||||
background-color: themer.color-of("background");
|
||||
color: themer.color-of("text");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.text-muted {
|
||||
@include themer.themed {
|
||||
color: themer.color-of("muted") !important;
|
||||
}
|
||||
}
|
||||
|
||||
.multipage {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex-grow: 1;
|
||||
|
||||
.multipage-slides, .multipage-slides > .multipage-slide.active {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.multipage-slide {
|
||||
.multipage-content {
|
||||
@extend .container;
|
||||
flex-grow: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.multipage-title {
|
||||
@extend .less-concise;
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
|
||||
.nav-pills.selectors {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
margin-bottom: 1rem;
|
||||
button[type="button"] {
|
||||
margin-right: 5px;
|
||||
margin-left: 5px;
|
||||
opacity: 0.4;
|
||||
border-style: none;
|
||||
font-size: 1.5rem;
|
||||
min-width: 30px;
|
||||
width: auto;
|
||||
height: auto;
|
||||
background-color: transparent;
|
||||
background-clip: border-box;
|
||||
@include themer.themed {
|
||||
border-bottom: 2px solid themer.color-of("text");
|
||||
}
|
||||
border-bottom-style: none;
|
||||
|
||||
&.active {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
59
Props/package-lock.json
generated
59
Props/package-lock.json
generated
@ -1349,6 +1349,19 @@
|
||||
"integrity": "sha512-8h7k1YgQKxKXWckzFCMfsIwn0Y61UK6tlD6y2lOb3hTOIMlK3t9/QwHOhc81TwU+RMf0As5fj7NPjroERCnejQ==",
|
||||
"dev": true
|
||||
},
|
||||
"@vue/reactivity": {
|
||||
"version": "3.1.5",
|
||||
"resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.1.5.tgz",
|
||||
"integrity": "sha512-1tdfLmNjWG6t/CsPldh+foumYFo3cpyCHgBYQ34ylaMsJ+SNHQ1kApMIa8jN+i593zQuaw3AdWH0nJTARzCFhg==",
|
||||
"requires": {
|
||||
"@vue/shared": "3.1.5"
|
||||
}
|
||||
},
|
||||
"@vue/shared": {
|
||||
"version": "3.1.5",
|
||||
"resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.1.5.tgz",
|
||||
"integrity": "sha512-oJ4F3TnvpXaQwZJNF3ZK+kLPHKarDmJjJ6jyzVNDKH9md1dptjC7lWR//jrGuLdek/U6iltWxqAnYOu8gCiOvA=="
|
||||
},
|
||||
"@webassemblyjs/ast": {
|
||||
"version": "1.11.1",
|
||||
"resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.11.1.tgz",
|
||||
@ -1558,6 +1571,14 @@
|
||||
"integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==",
|
||||
"dev": true
|
||||
},
|
||||
"alpinejs": {
|
||||
"version": "3.2.2",
|
||||
"resolved": "https://registry.npmjs.org/alpinejs/-/alpinejs-3.2.2.tgz",
|
||||
"integrity": "sha512-XkQKikB4tJTLIQuRUeF86CZnvmAKhjGzw5lmMri+7MTQzz77DTetuOqldBWjEgdJ/DOExXuiM57rQwWfBPdMPA==",
|
||||
"requires": {
|
||||
"@vue/reactivity": "^3.0.2"
|
||||
}
|
||||
},
|
||||
"ansi-colors": {
|
||||
"version": "4.1.1",
|
||||
"resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.1.tgz",
|
||||
@ -2411,14 +2432,6 @@
|
||||
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.14.1.tgz",
|
||||
"integrity": "sha512-HWqDgT7ZEkqRzBvc2s64vSZ/hfOceEol3ac/7tKwzuvEyWx3/4UegXh5oBOIotkGsObyk3xznnSRVADBgWSQVg=="
|
||||
},
|
||||
"framesync": {
|
||||
"version": "5.3.0",
|
||||
"resolved": "https://registry.npmjs.org/framesync/-/framesync-5.3.0.tgz",
|
||||
"integrity": "sha512-oc5m68HDO/tuK2blj7ZcdEBRx3p1PjrgHazL8GYEpvULhrtGIFbQArN6cQS2QhW8mitffaB+VYzMjDqBxxQeoA==",
|
||||
"requires": {
|
||||
"tslib": "^2.1.0"
|
||||
}
|
||||
},
|
||||
"fs.realpath": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
|
||||
@ -2554,11 +2567,6 @@
|
||||
"integrity": "sha512-chXa79rL/UC2KlX17jo3vRGz0azaWEx5tGqZg5pO3NUyEJVB17dMruQlzCCOfUvElghKcm5194+BCRvi2Rv/Gw==",
|
||||
"dev": true
|
||||
},
|
||||
"hey-listen": {
|
||||
"version": "1.0.8",
|
||||
"resolved": "https://registry.npmjs.org/hey-listen/-/hey-listen-1.0.8.tgz",
|
||||
"integrity": "sha512-COpmrF2NOg4TBWUJ5UVyaCU2A88wEMkUPK4hNqyCkqHbxT92BbvfjoSozkAIIm6XhicGlJHhFdullInrdhwU8Q=="
|
||||
},
|
||||
"human-signals": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz",
|
||||
@ -3125,17 +3133,6 @@
|
||||
"find-up": "^4.0.0"
|
||||
}
|
||||
},
|
||||
"popmotion": {
|
||||
"version": "9.4.0",
|
||||
"resolved": "https://registry.npmjs.org/popmotion/-/popmotion-9.4.0.tgz",
|
||||
"integrity": "sha512-FGnHjc8iDMrAwuZhka8eNx0yzcaufDqyZzW9vjJebRuC6BryR5ICyBmUH+wCgUuuaFSSU4r6oT2WtnbnDGcr3g==",
|
||||
"requires": {
|
||||
"framesync": "5.3.0",
|
||||
"hey-listen": "^1.0.8",
|
||||
"style-value-types": "4.1.4",
|
||||
"tslib": "^2.1.0"
|
||||
}
|
||||
},
|
||||
"postcss": {
|
||||
"version": "8.3.6",
|
||||
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.3.6.tgz",
|
||||
@ -3588,15 +3585,6 @@
|
||||
"integrity": "sha512-1k9ZosJCRFaRbY6hH49JFlRB0fVSbmnyq1iTPjNxUmGVjBNEmwrrHPenhlp+Lgo51BojHSf6pl2FcqYaN3PfVg==",
|
||||
"dev": true
|
||||
},
|
||||
"style-value-types": {
|
||||
"version": "4.1.4",
|
||||
"resolved": "https://registry.npmjs.org/style-value-types/-/style-value-types-4.1.4.tgz",
|
||||
"integrity": "sha512-LCJL6tB+vPSUoxgUBt9juXIlNJHtBMy8jkXzUJSBzeHWdBu6lhzHqCvLVkXFGsFIlNa2ln1sQHya/gzaFmB2Lg==",
|
||||
"requires": {
|
||||
"hey-listen": "^1.0.8",
|
||||
"tslib": "^2.1.0"
|
||||
}
|
||||
},
|
||||
"supports-color": {
|
||||
"version": "8.1.1",
|
||||
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz",
|
||||
@ -3700,11 +3688,6 @@
|
||||
"is-number": "^7.0.0"
|
||||
}
|
||||
},
|
||||
"tslib": {
|
||||
"version": "2.3.0",
|
||||
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.0.tgz",
|
||||
"integrity": "sha512-N82ooyxVNm6h1riLCoyS9e3fuJ3AMG2zIZs2Gd1ATcSFjSA23Q0fzjjZeh0jbJvWVDZ0cJT8yaNNaaXHzueNjg=="
|
||||
},
|
||||
"type-check": {
|
||||
"version": "0.4.0",
|
||||
"resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz",
|
||||
|
@ -30,10 +30,10 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.14.8",
|
||||
"alpinejs": "^3.2.2",
|
||||
"axios": "^0.21.1",
|
||||
"bootstrap": "^5.0.2",
|
||||
"bootstrap-icons": "^1.5.0",
|
||||
"popmotion": "^9.4.0",
|
||||
"simplebar": "^5.3.5"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
84
Props/shops/Props.Shop.Adafruit.deps.json
Normal file
84
Props/shops/Props.Shop.Adafruit.deps.json
Normal file
@ -0,0 +1,84 @@
|
||||
{
|
||||
"runtimeTarget": {
|
||||
"name": ".NETCoreApp,Version=v5.0",
|
||||
"signature": ""
|
||||
},
|
||||
"compilationOptions": {},
|
||||
"targets": {
|
||||
".NETCoreApp,Version=v5.0": {
|
||||
"Props.Shop.Adafruit/1.0.0": {
|
||||
"dependencies": {
|
||||
"FuzzySharp": "2.0.2",
|
||||
"Newtonsoft.Json": "13.0.1",
|
||||
"Props.Shop.Framework": "1.0.0",
|
||||
"System.Linq.Async": "5.0.0"
|
||||
},
|
||||
"runtime": {
|
||||
"Props.Shop.Adafruit.dll": {}
|
||||
}
|
||||
},
|
||||
"FuzzySharp/2.0.2": {
|
||||
"runtime": {
|
||||
"lib/netcoreapp2.1/FuzzySharp.dll": {
|
||||
"assemblyVersion": "1.0.4.0",
|
||||
"fileVersion": "1.0.4.0"
|
||||
}
|
||||
}
|
||||
},
|
||||
"Newtonsoft.Json/13.0.1": {
|
||||
"runtime": {
|
||||
"lib/netstandard2.0/Newtonsoft.Json.dll": {
|
||||
"assemblyVersion": "13.0.0.0",
|
||||
"fileVersion": "13.0.1.25517"
|
||||
}
|
||||
}
|
||||
},
|
||||
"System.Linq.Async/5.0.0": {
|
||||
"runtime": {
|
||||
"lib/netcoreapp3.1/System.Linq.Async.dll": {
|
||||
"assemblyVersion": "5.0.0.0",
|
||||
"fileVersion": "5.0.0.1"
|
||||
}
|
||||
}
|
||||
},
|
||||
"Props.Shop.Framework/1.0.0": {
|
||||
"runtime": {
|
||||
"Props.Shop.Framework.dll": {}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"libraries": {
|
||||
"Props.Shop.Adafruit/1.0.0": {
|
||||
"type": "project",
|
||||
"serviceable": false,
|
||||
"sha512": ""
|
||||
},
|
||||
"FuzzySharp/2.0.2": {
|
||||
"type": "package",
|
||||
"serviceable": true,
|
||||
"sha512": "sha512-sBKqWxw3g//peYxDZ8JipRlyPbIyBtgzqBVA5GqwHVeqtIrw75maGXAllztf+1aJhchD+drcQIgf2mFho8ZV8A==",
|
||||
"path": "fuzzysharp/2.0.2",
|
||||
"hashPath": "fuzzysharp.2.0.2.nupkg.sha512"
|
||||
},
|
||||
"Newtonsoft.Json/13.0.1": {
|
||||
"type": "package",
|
||||
"serviceable": true,
|
||||
"sha512": "sha512-ppPFpBcvxdsfUonNcvITKqLl3bqxWbDCZIzDWHzjpdAHRFfZe0Dw9HmA0+za13IdyrgJwpkDTDA9fHaxOrt20A==",
|
||||
"path": "newtonsoft.json/13.0.1",
|
||||
"hashPath": "newtonsoft.json.13.0.1.nupkg.sha512"
|
||||
},
|
||||
"System.Linq.Async/5.0.0": {
|
||||
"type": "package",
|
||||
"serviceable": true,
|
||||
"sha512": "sha512-cPtIuuH8TIjVHSi2ewwReWGW1PfChPE0LxPIDlfwVcLuTM9GANFTXiMB7k3aC4sk3f0cQU25LNKzx+jZMxijqw==",
|
||||
"path": "system.linq.async/5.0.0",
|
||||
"hashPath": "system.linq.async.5.0.0.nupkg.sha512"
|
||||
},
|
||||
"Props.Shop.Framework/1.0.0": {
|
||||
"type": "project",
|
||||
"serviceable": false,
|
||||
"sha512": ""
|
||||
}
|
||||
}
|
||||
}
|
Binary file not shown.
Loading…
Reference in New Issue
Block a user