Implemented groundwork for search configuration.

This commit is contained in:
Harrison Deng 2021-07-22 16:12:27 -05:00
parent 3129e5e564
commit 2719142538
41 changed files with 1037 additions and 205 deletions

View File

@ -15,7 +15,7 @@ namespace Props.Shop.Adafruit.Api
private Dictionary<string, List<ProductListing>> listings = new Dictionary<string, List<ProductListing>>();
private bool requested = false;
public DateTime TimeOfLastRequest { get; private set; }
public bool RequestReady => DateTime.Now - TimeOfLastRequest > TimeSpan.FromMinutes(minutesPerRequest);
public bool RequestReady => !requested || DateTime.Now - TimeOfLastRequest > TimeSpan.FromMinutes(minutesPerRequest);
public ProductListingManager(int requestsPerMinute = 5)
{
@ -27,10 +27,10 @@ namespace Props.Shop.Adafruit.Api
requested = true;
TimeOfLastRequest = DateTime.Now;
HttpResponseMessage response = await http.GetAsync("/products");
SetListings(await response.Content.ReadAsStringAsync());
SetListingsData(await response.Content.ReadAsStringAsync());
}
public void SetListings(string data)
public void SetListingsData(string data)
{
ListingsParser listingsParser = new ListingsParser(data);
foreach (ProductListing listing in listingsParser.ProductListings)

View File

@ -25,7 +25,7 @@ namespace Props.Shop.Framework
public int MinPurchases { get; set; }
public bool KeepUnknownPurchaseCount { get; set; } = true;
public int MinReviews { get; set; }
public bool KeepUnknownRatingCount { get; set; } = true;
public bool KeepUnknownReviewCount { get; set; } = true;
public bool EnableMaxShippingFee { get; set; }
private int maxShippingFee;
@ -59,7 +59,7 @@ namespace Props.Shop.Framework
MinPurchases == other.MinPurchases &&
KeepUnknownPurchaseCount == other.KeepUnknownPurchaseCount &&
MinReviews == other.MinReviews &&
KeepUnknownRatingCount == other.KeepUnknownRatingCount &&
KeepUnknownReviewCount == other.KeepUnknownReviewCount &&
EnableMaxShippingFee == other.EnableMaxShippingFee &&
MaxShippingFee == other.MaxShippingFee &&
KeepUnknownShipping == other.KeepUnknownShipping;
@ -81,5 +81,16 @@ namespace Props.Shop.Framework
{
return (Filters)this.MemberwiseClone();
}
public bool Validate(ProductListing listing)
{
if (listing.Shipping == null && !KeepUnknownShipping || (EnableMaxShippingFee && listing.Shipping > MaxShippingFee)) return false;
float shippingDifference = listing.Shipping != null ? listing.Shipping.Value : 0;
if (!(listing.LowerPrice + shippingDifference >= LowerPrice && (!EnableUpperPrice || listing.UpperPrice + shippingDifference <= UpperPrice))) return false;
if ((listing.Rating == null && !KeepUnrated) && MinRating > (listing.Rating == null ? 0 : listing.Rating)) return false;
if ((listing.PurchaseCount == null && !KeepUnknownPurchaseCount) || MinPurchases > (listing.PurchaseCount == null ? 0 : listing.PurchaseCount)) return false;
if ((listing.ReviewCount == null && !KeepUnknownReviewCount) || MinReviews > (listing.ReviewCount == null ? 0 : listing.ReviewCount)) return false;
return true;
}
}
}

View File

@ -13,7 +13,7 @@ namespace Props.Shop.Adafruit.Tests.Api
public async Task TestSearch()
{
ProductListingManager mockProductListingManager = new ProductListingManager();
mockProductListingManager.SetListings(File.ReadAllText("./Assets/products.json"));
mockProductListingManager.SetListingsData(File.ReadAllText("./Assets/products.json"));
List<ProductListing> results = new List<ProductListing>();
await foreach (ProductListing item in mockProductListingManager.Search("arduino", 0.5f))
{

View File

@ -5,7 +5,7 @@ module.exports = {
"node": true,
},
"extends": "eslint:recommended",
"parser": "babel-eslint",
"parser": "@babel/eslint-parser",
"rules": {
"no-console": process.env.NODE_ENV === "production" ? "warn" : "off",
"no-debugger": process.env.NODE_ENV === "production" ? "warn" : "off",
@ -22,6 +22,10 @@ module.exports = {
"always"
],
"comma-dangle": ["error", "only-multiline"],
"space-before-function-paren": ["error", "never"]
"space-before-function-paren": ["error", {
"anonymous": "always",
"named": "never",
"asyncArrow": "always"
}]
}
};

View File

@ -6,8 +6,9 @@
}
<div class="flex-grow-1 d-flex flex-column justify-content-center">
<div class="jumbotron border-top border-bottom">
<h1 class="mx-auto mt-3 mb-4 text-center">@ViewData["Title"]</h1>
<div class="jumbotron sole d-flex flex-column align-content-center">
<img alt="Props logo" src="~/images/logo-simplified.svg" class="img-fluid" style="max-height: 180px;" asp-append-version="true" />
<h1 class="mt-3 mb-4 text-center">@ViewData["Title"]</h1>
<div class="my-3 row justify-content-md-center">
<div class="col-md-4">
<form id="account" method="post">

View File

@ -13,3 +13,5 @@
<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. *@

View File

@ -5,8 +5,9 @@
}
<div class="flex-grow-1 d-flex flex-column justify-content-center">
<div class="jumbotron border-top border-bottom">
<h1 class="mx-auto mt-3 mb-4 text-center">@ViewData["Title"]</h1>
<div class="jumbotron sole d-flex flex-column align-content-center">
<img alt="Props logo" src="~/images/logo-simplified.svg" class="img-fluid" style="max-height: 180px;" asp-append-version="true" />
<h1 class="mt-3 mb-4 text-center">@ViewData["Title"]</h1>
<div class="my-3 row justify-content-md-center">
<div class="col-md-4">
<form asp-route-returnUrl="@Model.ReturnUrl" method="post">

View File

@ -20,4 +20,4 @@
</p>
}
}
@* TODO: https://aka.ms/aspaccountconf *@
@* TODO: Do something about this. *@

View File

@ -0,0 +1,41 @@
using Microsoft.AspNetCore.Mvc;
using Props.Models.Search;
using Props.Services.Modules;
namespace Props.Controllers
{
[ApiController]
[Route("api/[Controller]")]
public class SearchController : ControllerBase
{
private SearchOutline defaultOutline = new SearchOutline();
IShopManager shopManager;
public SearchController(IShopManager shopManager)
{
this.shopManager = shopManager;
}
[HttpGet]
[Route("Shops/Available")]
public IActionResult GetAvailableShops()
{
return Ok(shopManager.AvailableShops());
}
[HttpGet]
[Route("Default/Outline/Filters")]
public IActionResult GetDefaultFilters()
{
return Ok(defaultOutline.Filters);
}
[HttpGet]
[Route("Default/Outline/DisabledShops")]
public IActionResult GetDefaultDisabledShops()
{
return Ok(defaultOutline.Disabled);
}
}
}

View File

@ -8,6 +8,7 @@ using Microsoft.AspNetCore.Identity.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.ChangeTracking;
using Props.Models;
using Props.Models.Search;
using Props.Models.User;
using Props.Shop.Framework;
@ -15,6 +16,8 @@ namespace Props.Data
{
public class ApplicationDbContext : IdentityDbContext<ApplicationUser>
{
DbSet<SearchOutline> SearchOutlines { get; set; }
DbSet<ProductListingInfo> TrackedListings { get; set; }
public ApplicationDbContext(DbContextOptions<ApplicationDbContext> options)
: base(options)
{
@ -37,11 +40,11 @@ namespace Props.Data
);
modelBuilder.Entity<SearchOutline>()
.Property(e => e.ShopStates)
.Property(e => e.Disabled)
.HasConversion(
v => JsonSerializer.Serialize(v, null),
v => JsonSerializer.Deserialize<SearchOutline.ShopToggler>(v, null),
new ValueComparer<SearchOutline.ShopToggler>(
v => JsonSerializer.Deserialize<SearchOutline.ShopsDisabled>(v, null),
new ValueComparer<SearchOutline.ShopsDisabled>(
(a, b) => a.Equals(b),
c => c.GetHashCode(),
c => c.Copy()

View File

@ -9,7 +9,7 @@ using Props.Data;
namespace Props.Data.Migrations
{
[DbContext(typeof(ApplicationDbContext))]
[Migration("20210721064503_InitialCreate")]
[Migration("20210722180024_InitialCreate")]
partial class InitialCreate
{
protected override void BuildTargetModel(ModelBuilder modelBuilder)
@ -150,7 +150,80 @@ namespace Props.Data.Migrations
b.ToTable("AspNetUserTokens");
});
modelBuilder.Entity("Props.Models.ApplicationUser", b =>
modelBuilder.Entity("Props.Models.ResultsPreferences", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<string>("ApplicationUserId")
.IsRequired()
.HasColumnType("TEXT");
b.Property<string>("Order")
.IsRequired()
.HasColumnType("TEXT");
b.Property<string>("ProfileName")
.IsRequired()
.HasColumnType("TEXT");
b.HasKey("Id");
b.HasIndex("ApplicationUserId")
.IsUnique();
b.ToTable("ResultsPreferences");
});
modelBuilder.Entity("Props.Models.Search.ProductListingInfo", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<uint>("Hits")
.HasColumnType("INTEGER");
b.Property<DateTime>("LastUpdated")
.HasColumnType("TEXT");
b.Property<string>("ProductName")
.HasColumnType("TEXT");
b.Property<string>("ProductUrl")
.HasColumnType("TEXT");
b.HasKey("Id");
b.ToTable("TrackedListings");
});
modelBuilder.Entity("Props.Models.Search.SearchOutline", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<string>("ApplicationUserId")
.IsRequired()
.HasColumnType("TEXT");
b.Property<string>("Disabled")
.IsRequired()
.HasColumnType("TEXT");
b.Property<string>("Filters")
.HasColumnType("TEXT");
b.HasKey("Id");
b.HasIndex("ApplicationUserId");
b.ToTable("SearchOutlines");
});
modelBuilder.Entity("Props.Models.User.ApplicationUser", b =>
{
b.Property<string>("Id")
.HasColumnType("TEXT");
@ -214,56 +287,6 @@ namespace Props.Data.Migrations
b.ToTable("AspNetUsers");
});
modelBuilder.Entity("Props.Models.ResultsPreferences", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<string>("ApplicationUserId")
.IsRequired()
.HasColumnType("TEXT");
b.Property<string>("Order")
.IsRequired()
.HasColumnType("TEXT");
b.HasKey("Id");
b.HasIndex("ApplicationUserId")
.IsUnique();
b.ToTable("ResultsPreferences");
});
modelBuilder.Entity("Props.Models.SearchOutline", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<string>("ApplicationUserId")
.IsRequired()
.HasColumnType("TEXT");
b.Property<string>("Filters")
.HasColumnType("TEXT");
b.Property<int>("MaxResults")
.HasColumnType("INTEGER");
b.Property<string>("ShopStates")
.IsRequired()
.HasColumnType("TEXT");
b.HasKey("Id");
b.HasIndex("ApplicationUserId")
.IsUnique();
b.ToTable("SearchOutline");
});
modelBuilder.Entity("Props.Shared.Models.User.ApplicationPreferences", b =>
{
b.Property<int>("Id")
@ -299,7 +322,7 @@ namespace Props.Data.Migrations
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim<string>", b =>
{
b.HasOne("Props.Models.ApplicationUser", null)
b.HasOne("Props.Models.User.ApplicationUser", null)
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
@ -308,7 +331,7 @@ namespace Props.Data.Migrations
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin<string>", b =>
{
b.HasOne("Props.Models.ApplicationUser", null)
b.HasOne("Props.Models.User.ApplicationUser", null)
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
@ -323,7 +346,7 @@ namespace Props.Data.Migrations
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("Props.Models.ApplicationUser", null)
b.HasOne("Props.Models.User.ApplicationUser", null)
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
@ -332,7 +355,7 @@ namespace Props.Data.Migrations
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken<string>", b =>
{
b.HasOne("Props.Models.ApplicationUser", null)
b.HasOne("Props.Models.User.ApplicationUser", null)
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
@ -341,32 +364,38 @@ namespace Props.Data.Migrations
modelBuilder.Entity("Props.Models.ResultsPreferences", b =>
{
b.HasOne("Props.Models.ApplicationUser", null)
b.HasOne("Props.Models.User.ApplicationUser", "ApplicationUser")
.WithOne("ResultsPreferences")
.HasForeignKey("Props.Models.ResultsPreferences", "ApplicationUserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("ApplicationUser");
});
modelBuilder.Entity("Props.Models.SearchOutline", b =>
modelBuilder.Entity("Props.Models.Search.SearchOutline", b =>
{
b.HasOne("Props.Models.ApplicationUser", null)
.WithOne("SearchOutline")
.HasForeignKey("Props.Models.SearchOutline", "ApplicationUserId")
b.HasOne("Props.Models.User.ApplicationUser", "ApplicationUser")
.WithMany("SearchOutlines")
.HasForeignKey("ApplicationUserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("ApplicationUser");
});
modelBuilder.Entity("Props.Shared.Models.User.ApplicationPreferences", b =>
{
b.HasOne("Props.Models.ApplicationUser", null)
b.HasOne("Props.Models.User.ApplicationUser", "ApplicationUser")
.WithOne("ApplicationPreferences")
.HasForeignKey("Props.Shared.Models.User.ApplicationPreferences", "ApplicationUserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("ApplicationUser");
});
modelBuilder.Entity("Props.Models.ApplicationUser", b =>
modelBuilder.Entity("Props.Models.User.ApplicationUser", b =>
{
b.Navigation("ApplicationPreferences")
.IsRequired();
@ -374,8 +403,7 @@ namespace Props.Data.Migrations
b.Navigation("ResultsPreferences")
.IsRequired();
b.Navigation("SearchOutline")
.IsRequired();
b.Navigation("SearchOutlines");
});
#pragma warning restore 612, 618
}

View File

@ -46,6 +46,22 @@ namespace Props.Data.Migrations
table.PrimaryKey("PK_AspNetUsers", x => x.Id);
});
migrationBuilder.CreateTable(
name: "TrackedListings",
columns: table => new
{
Id = table.Column<int>(type: "INTEGER", nullable: false)
.Annotation("Sqlite:Autoincrement", 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),
ProductName = table.Column<string>(type: "TEXT", nullable: true)
},
constraints: table =>
{
table.PrimaryKey("PK_TrackedListings", x => x.Id);
});
migrationBuilder.CreateTable(
name: "AspNetRoleClaims",
columns: table => new
@ -180,7 +196,8 @@ namespace Props.Data.Migrations
Id = table.Column<int>(type: "INTEGER", nullable: false)
.Annotation("Sqlite:Autoincrement", true),
ApplicationUserId = table.Column<string>(type: "TEXT", nullable: false),
Order = table.Column<string>(type: "TEXT", nullable: false)
Order = table.Column<string>(type: "TEXT", nullable: false),
ProfileName = table.Column<string>(type: "TEXT", nullable: false)
},
constraints: table =>
{
@ -194,21 +211,20 @@ namespace Props.Data.Migrations
});
migrationBuilder.CreateTable(
name: "SearchOutline",
name: "SearchOutlines",
columns: table => new
{
Id = table.Column<int>(type: "INTEGER", nullable: false)
.Annotation("Sqlite:Autoincrement", true),
ApplicationUserId = table.Column<string>(type: "TEXT", nullable: false),
Filters = table.Column<string>(type: "TEXT", nullable: true),
MaxResults = table.Column<int>(type: "INTEGER", nullable: false),
ShopStates = table.Column<string>(type: "TEXT", nullable: false)
Disabled = table.Column<string>(type: "TEXT", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_SearchOutline", x => x.Id);
table.PrimaryKey("PK_SearchOutlines", x => x.Id);
table.ForeignKey(
name: "FK_SearchOutline_AspNetUsers_ApplicationUserId",
name: "FK_SearchOutlines_AspNetUsers_ApplicationUserId",
column: x => x.ApplicationUserId,
principalTable: "AspNetUsers",
principalColumn: "Id",
@ -265,10 +281,9 @@ namespace Props.Data.Migrations
unique: true);
migrationBuilder.CreateIndex(
name: "IX_SearchOutline_ApplicationUserId",
table: "SearchOutline",
column: "ApplicationUserId",
unique: true);
name: "IX_SearchOutlines_ApplicationUserId",
table: "SearchOutlines",
column: "ApplicationUserId");
}
protected override void Down(MigrationBuilder migrationBuilder)
@ -295,7 +310,10 @@ namespace Props.Data.Migrations
name: "ResultsPreferences");
migrationBuilder.DropTable(
name: "SearchOutline");
name: "SearchOutlines");
migrationBuilder.DropTable(
name: "TrackedListings");
migrationBuilder.DropTable(
name: "AspNetRoles");

View File

@ -148,7 +148,80 @@ namespace Props.Data.Migrations
b.ToTable("AspNetUserTokens");
});
modelBuilder.Entity("Props.Models.ApplicationUser", b =>
modelBuilder.Entity("Props.Models.ResultsPreferences", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<string>("ApplicationUserId")
.IsRequired()
.HasColumnType("TEXT");
b.Property<string>("Order")
.IsRequired()
.HasColumnType("TEXT");
b.Property<string>("ProfileName")
.IsRequired()
.HasColumnType("TEXT");
b.HasKey("Id");
b.HasIndex("ApplicationUserId")
.IsUnique();
b.ToTable("ResultsPreferences");
});
modelBuilder.Entity("Props.Models.Search.ProductListingInfo", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<uint>("Hits")
.HasColumnType("INTEGER");
b.Property<DateTime>("LastUpdated")
.HasColumnType("TEXT");
b.Property<string>("ProductName")
.HasColumnType("TEXT");
b.Property<string>("ProductUrl")
.HasColumnType("TEXT");
b.HasKey("Id");
b.ToTable("TrackedListings");
});
modelBuilder.Entity("Props.Models.Search.SearchOutline", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<string>("ApplicationUserId")
.IsRequired()
.HasColumnType("TEXT");
b.Property<string>("Disabled")
.IsRequired()
.HasColumnType("TEXT");
b.Property<string>("Filters")
.HasColumnType("TEXT");
b.HasKey("Id");
b.HasIndex("ApplicationUserId");
b.ToTable("SearchOutlines");
});
modelBuilder.Entity("Props.Models.User.ApplicationUser", b =>
{
b.Property<string>("Id")
.HasColumnType("TEXT");
@ -212,56 +285,6 @@ namespace Props.Data.Migrations
b.ToTable("AspNetUsers");
});
modelBuilder.Entity("Props.Models.ResultsPreferences", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<string>("ApplicationUserId")
.IsRequired()
.HasColumnType("TEXT");
b.Property<string>("Order")
.IsRequired()
.HasColumnType("TEXT");
b.HasKey("Id");
b.HasIndex("ApplicationUserId")
.IsUnique();
b.ToTable("ResultsPreferences");
});
modelBuilder.Entity("Props.Models.SearchOutline", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<string>("ApplicationUserId")
.IsRequired()
.HasColumnType("TEXT");
b.Property<string>("Filters")
.HasColumnType("TEXT");
b.Property<int>("MaxResults")
.HasColumnType("INTEGER");
b.Property<string>("ShopStates")
.IsRequired()
.HasColumnType("TEXT");
b.HasKey("Id");
b.HasIndex("ApplicationUserId")
.IsUnique();
b.ToTable("SearchOutline");
});
modelBuilder.Entity("Props.Shared.Models.User.ApplicationPreferences", b =>
{
b.Property<int>("Id")
@ -297,7 +320,7 @@ namespace Props.Data.Migrations
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim<string>", b =>
{
b.HasOne("Props.Models.ApplicationUser", null)
b.HasOne("Props.Models.User.ApplicationUser", null)
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
@ -306,7 +329,7 @@ namespace Props.Data.Migrations
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin<string>", b =>
{
b.HasOne("Props.Models.ApplicationUser", null)
b.HasOne("Props.Models.User.ApplicationUser", null)
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
@ -321,7 +344,7 @@ namespace Props.Data.Migrations
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("Props.Models.ApplicationUser", null)
b.HasOne("Props.Models.User.ApplicationUser", null)
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
@ -330,7 +353,7 @@ namespace Props.Data.Migrations
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken<string>", b =>
{
b.HasOne("Props.Models.ApplicationUser", null)
b.HasOne("Props.Models.User.ApplicationUser", null)
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
@ -339,32 +362,38 @@ namespace Props.Data.Migrations
modelBuilder.Entity("Props.Models.ResultsPreferences", b =>
{
b.HasOne("Props.Models.ApplicationUser", null)
b.HasOne("Props.Models.User.ApplicationUser", "ApplicationUser")
.WithOne("ResultsPreferences")
.HasForeignKey("Props.Models.ResultsPreferences", "ApplicationUserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("ApplicationUser");
});
modelBuilder.Entity("Props.Models.SearchOutline", b =>
modelBuilder.Entity("Props.Models.Search.SearchOutline", b =>
{
b.HasOne("Props.Models.ApplicationUser", null)
.WithOne("SearchOutline")
.HasForeignKey("Props.Models.SearchOutline", "ApplicationUserId")
b.HasOne("Props.Models.User.ApplicationUser", "ApplicationUser")
.WithMany("SearchOutlines")
.HasForeignKey("ApplicationUserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("ApplicationUser");
});
modelBuilder.Entity("Props.Shared.Models.User.ApplicationPreferences", b =>
{
b.HasOne("Props.Models.ApplicationUser", null)
b.HasOne("Props.Models.User.ApplicationUser", "ApplicationUser")
.WithOne("ApplicationPreferences")
.HasForeignKey("Props.Shared.Models.User.ApplicationPreferences", "ApplicationUserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("ApplicationUser");
});
modelBuilder.Entity("Props.Models.ApplicationUser", b =>
modelBuilder.Entity("Props.Models.User.ApplicationUser", b =>
{
b.Navigation("ApplicationPreferences")
.IsRequired();
@ -372,8 +401,7 @@ namespace Props.Data.Migrations
b.Navigation("ResultsPreferences")
.IsRequired();
b.Navigation("SearchOutline")
.IsRequired();
b.Navigation("SearchOutlines");
});
#pragma warning restore 612, 618
}

View File

@ -0,0 +1,18 @@
using System;
using Props.Shop.Framework;
namespace Props.Models.Search
{
public class ProductListingInfo
{
public int Id { get; set; }
public uint Hits { get; set; }
public DateTime LastUpdated { get; set; }
public string ProductUrl { get; set; }
public string ProductName { get; set; }
}
}

View File

@ -2,23 +2,27 @@ using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Linq;
using Props.Models.User;
using Props.Shop.Framework;
namespace Props.Models
namespace Props.Models.Search
{
public class SearchOutline
{
public int Id { get; set; }
[Required]
public string ApplicationUserId { get; set; }
public Filters Filters { get; set; }
public int MaxResults { get; set; } = 100;
[Required]
public virtual ApplicationUser ApplicationUser { get; set; }
public Filters Filters { get; set; } = new Filters();
[Required]
public ShopToggler ShopStates { get; set; } = new ShopToggler();
public ShopsDisabled Disabled { get; set; } = new ShopsDisabled();
public sealed class ShopToggler : HashSet<string>
public sealed class ShopsDisabled : HashSet<string>
{
public int TotalShops { get; set; }
public bool this[string name]
@ -41,9 +45,9 @@ namespace Props.Models
}
}
public ShopToggler Copy()
public ShopsDisabled Copy()
{
ShopToggler copy = new ShopToggler();
ShopsDisabled copy = new ShopsDisabled();
copy.Union(this);
return copy;
}
@ -63,9 +67,8 @@ namespace Props.Models
SearchOutline other = (SearchOutline)obj;
return
Id == other.Id &&
MaxResults == other.MaxResults &&
Filters.Equals(other.Filters) &&
ShopStates.Equals(other.ShopStates);
Disabled.Equals(other.Disabled);
}
public override int GetHashCode()

View File

@ -1,4 +1,5 @@
using System.ComponentModel.DataAnnotations;
using Props.Models.User;
namespace Props.Shared.Models.User
{
@ -9,6 +10,9 @@ namespace Props.Shared.Models.User
[Required]
public string ApplicationUserId { get; set; }
[Required]
public virtual ApplicationUser ApplicationUser { get; set; }
public bool EnableSearchHistory { get; set; } = true;
public bool DarkMode { get; set; } = false;
}

View File

@ -4,30 +4,30 @@ using System.ComponentModel.DataAnnotations;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Identity;
using Props.Models.Search;
using Props.Shared.Models.User;
namespace Props.Models.User
{
public class ApplicationUser : IdentityUser
{
[Required]
public virtual SearchOutline SearchOutline { get; private set; }
public virtual ISet<SearchOutline> SearchOutlines { get; set; }
[Required]
public virtual ResultsPreferences ResultsPreferences { get; private set; }
[Required]
public virtual ApplicationPreferences ApplicationPreferences { get; private set; }
// TODO: Write project system.
public ApplicationUser()
{
SearchOutline = new SearchOutline();
ResultsPreferences = new ResultsPreferences();
ApplicationPreferences = new ApplicationPreferences();
}
public ApplicationUser(SearchOutline searchOutline, ResultsPreferences resultsPreferences, ApplicationPreferences applicationPreferences)
public ApplicationUser(ResultsPreferences resultsPreferences, ApplicationPreferences applicationPreferences)
{
this.SearchOutline = searchOutline;
this.ResultsPreferences = resultsPreferences;
this.ApplicationPreferences = applicationPreferences;
}

View File

@ -3,18 +3,27 @@ using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Linq;
using System.Text.Json;
using Props.Models.User;
namespace Props.Models
{
public class ResultsPreferences
{
public int Id { get; set; }
[Required]
public string ApplicationUserId { get; set; }
[Required]
public virtual ApplicationUser ApplicationUser { get; set; }
[Required]
public IList<Category> Order { get; set; }
[Required]
public string ProfileName { get; set; }
public ResultsPreferences()
{
Order = new List<Category>(Enum.GetValues<Category>().Length);

View File

@ -0,0 +1,11 @@
namespace Props.Options
{
public class ModulesOptions
{
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; }
}
}

View File

@ -8,7 +8,7 @@
<section class="jumbotron d-flex flex-column align-items-center">
<div>
<img alt="Props logo" src="./images/logo.svg" class="img-fluid" style="max-height: 540px;" />
<img alt="Props logo" src="~/images/logo.svg" class="img-fluid" style="max-height: 540px;" asp-append-version="true" />
</div>
<div class="text-center px-3 my-2 concise">
<h1 class="my-2">Props</h1>

View File

@ -4,13 +4,107 @@
ViewData["Specific"] = "Search";
}
<div class="container d-flex flex-column align-items-center">
<form class="my-4" style="width: 720px;">
<div class="container">
<div class="my-4 less-concise mx-auto">
<div class="input-group">
<input type="text" class="form-control" placeholder="What are you looking for?" aria-label="Search" aria-describedby="search-btn">
<input type="text" class="form-control" placeholder="What are you looking for?" aria-label="Search" aria-describedby="search-btn" id="search-bar">
<input type="checkbox" class="btn-check" id="config-check-toggle" autocomplete="off">
<label class="btn btn-outline-secondary" for="config-check-toggle"><i class="bi bi-sliders"></i></label>
<button class="btn btn-outline-primary" type="button" id="search-btn">Search</button>
</div>
</form>
</div>
</div>
<div class="tear d-none" id="configuration">
<div class="container invisible">
<h2 class="my-2">Configuration</h2>
<div class="row justify-content-md-center">
<section class="col-lg">
<h3>Price</h3>
<div class="mb-3">
<label for="max-price" class="form-label">Maximum Price</label>
<div class="input-group">
<div class="input-group-text">
<input class="form-check-input mt-0" type="checkbox" id="max-price-enabled">
</div>
<span class="input-group-text">$</span>
<input type="number" class="form-control" min="0" id="max-price">
<span class="input-group-text">.00</span>
</div>
</div>
<div class="mb-3">
<label for="min-price" class="form-label">Minimum Price</label>
<div class="input-group">
<span class="input-group-text">$</span>
<input type="number" class="form-control" min="0" id="min-price">
<span class="input-group-text">.00</span>
</div>
</div>
<div class="mb-3">
<label for="max-shipping" class="form-label">Maximum Shipping Fee</label>
<div class="input-group">
<div class="input-group-text">
<input class="form-check-input mt-0" type="checkbox" id="max-shipping-enabled">
</div>
<span class="input-group-text">$</span>
<input type="number" class="form-control" min="0" id="max-shipping">
<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">
<label class="form-check-label" for="keep-unknown-shipping">Keep Unknown Shipping</label>
</div>
</div>
</section>
<section class="col-lg">
<h3>Metrics</h3>
<div class="mb-3">
<label for="min-purchases" class="form-label">Minimum Purchases</label>
<div class="input-group">
<input type="number" class="form-control" min="0" id="min-purchases">
<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">
<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">
<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>
</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>
</div>
<div class="mb-3">
<div class="form-check">
<input class="form-check-input" type="checkbox" id="keep-unrated">
<label class="form-check-label" for="keep-unrated">Keep Minimum Rating</label>
</div>
</div>
</section>
<section class="col-md">
<h3>Shops Enabled</h3>
<div class="mb-3 px-3" id="shop-checkboxes">
</div>
</section>
</div>
</div>
</div>
@* TODO: Add results display and default results display *@

View File

@ -4,6 +4,6 @@ namespace Props.Pages
{
public class SearchModel : PageModel
{
// TODO: Complete the search model.
}
}

View File

@ -11,10 +11,6 @@
<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 src="@($"~/js/specific/{(ViewData["Specific"])}.js")" asp-append-version="true"></script>
}
</head>
<body class="theme-light">
@ -67,6 +63,10 @@
&copy; 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>

View File

@ -3,13 +3,13 @@ using Newtonsoft.Json.Linq;
namespace Props.Services.Content
{
public class ContentManager<Page> : IContentManager<Page>
public class CachedContentManager<TPage> : IContentManager<TPage>
{
private dynamic data;
private readonly string directory;
private readonly string fileName;
dynamic IContentManager<Page>.Json
dynamic IContentManager<TPage>.Json
{
get
{
@ -18,10 +18,10 @@ namespace Props.Services.Content
}
}
public ContentManager(string directory = "content")
public CachedContentManager(string directory = "content")
{
this.directory = directory;
this.fileName = typeof(Page).Name.Replace("Model", "") + ".json";
this.fileName = typeof(TPage).Name.Replace("Model", "") + ".json";
}
}
}

View File

@ -0,0 +1,14 @@
using System.Collections;
using System.Collections.Generic;
using System.Threading.Tasks;
using Props.Models.Search;
using Props.Shop.Framework;
namespace Props.Services.Modules
{
public interface IShopManager
{
public IEnumerable<string> AvailableShops();
public Task<IList<ProductListing>> Search(string query, SearchOutline searchOutline);
}
}

View File

@ -0,0 +1,107 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Reflection;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
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 LocalShopManager : IShopManager
{
private ILogger<LocalShopManager> logger;
private Dictionary<string, IShop> shops;
private ModulesOptions options;
private IConfiguration configuration;
public LocalShopManager(IConfiguration configuration, ILogger<LocalShopManager> logger)
{
this.configuration = configuration;
this.logger = logger;
options = configuration.GetSection(ModulesOptions.Modules).Get<ModulesOptions>();
shops = new Dictionary<string, IShop>();
foreach (IShop shop in LoadShops(options.ShopsDir, options.ShopRegex, options.RecursiveLoad))
{
if (!shops.TryAdd(shop.ShopName, shop))
{
logger.LogWarning("Duplicate shop {0} detected. Ignoring the latter.", shop.ShopName);
}
}
}
public IEnumerable<string> AvailableShops()
{
return shops.Keys;
}
public async Task<IList<ProductListing>> Search(string query, SearchOutline searchOutline)
{
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;
}
private IEnumerable<IShop> LoadShops(string shopsDir, string shopRegex, bool recursiveLoad)
{
Stack<string> directories = new Stack<string>();
directories.Push(shopsDir);
string currentDirectory = null;
while (directories.TryPop(out currentDirectory))
{
if (recursiveLoad)
{
foreach (string dir in Directory.EnumerateDirectories(currentDirectory))
{
directories.Push(dir);
}
}
foreach (string file in Directory.EnumerateFiles(currentDirectory))
{
if (Path.GetExtension(file).Equals(".dll") && Regex.IsMatch(file, shopRegex))
{
ShopAssemblyLoadContext context = new ShopAssemblyLoadContext(file);
Assembly assembly = context.LoadFromAssemblyName(new AssemblyName(Path.GetFileNameWithoutExtension(file)));
int success = 0;
foreach (Type type in assembly.GetTypes())
{
if (typeof(IShop).IsAssignableFrom(type))
{
IShop shop = Activator.CreateInstance(type) as IShop;
if (shop != null)
{
success += 1;
yield return shop;
}
}
}
if (success == 0)
{
logger.LogWarning("There were no shops found within the assembly at path \"{0}\".", file);
}
}
}
}
}
}
}

View File

@ -2,6 +2,7 @@ using System;
using System.Reflection;
using System.Runtime.Loader;
using Microsoft.Extensions.DependencyModel;
using Props.Shop.Framework;
namespace Props.Services.Modules
{
@ -16,6 +17,7 @@ namespace Props.Services.Modules
protected override Assembly Load(AssemblyName assemblyName)
{
if (assemblyName.FullName.Equals(typeof(IShop).Assembly.FullName)) return null;
string assemblyPath = resolver.ResolveAssemblyToPath(assemblyName);
return assemblyPath != null ? LoadFromAssemblyPath(assemblyPath) : null;
}

View File

@ -15,6 +15,7 @@ using Microsoft.Extensions.Hosting;
using Props.Data;
using Props.Models.User;
using Props.Services.Content;
using Props.Services.Modules;
namespace Props
{
@ -53,7 +54,8 @@ namespace Props
.AddEntityFrameworkStores<ApplicationDbContext>();
services.AddRazorPages();
services.AddSingleton(typeof(IContentManager<>), typeof(ContentManager<>));
services.AddSingleton(typeof(IContentManager<>), typeof(CachedContentManager<>));
services.AddSingleton<IShopManager, LocalShopManager>();
}
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
@ -75,6 +77,7 @@ namespace Props
app.UseEndpoints(endpoints =>
{
endpoints.MapControllers();
endpoints.MapRazorPages();
});
}

View File

@ -9,8 +9,11 @@
"Microsoft.Hosting.Lifetime": "Information"
}
},
"Shops": {
"Path": "./shops"
"Modules": {
"ShopsDir": "./shops",
"RecursiveLoad": "false",
"MaxResults": "100",
"ShopRegex": "Props\\.Shop\\.."
},
"AllowedHosts": "*"
}

View File

@ -0,0 +1,232 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
width="130.02815mm"
height="183.73235mm"
viewBox="0 0 130.02815 183.73235"
version="1.1"
id="svg5"
inkscape:version="1.1 (c68e22c387, 2021-05-23)"
sodipodi:docname="logo-simplified.svg"
inkscape:export-filename="C:\Users\yunya\Documents\Props\Props\client\src\assets\images\logo.png"
inkscape:export-xdpi="96"
inkscape:export-ydpi="96"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<sodipodi:namedview
id="namedview7"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:pageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
inkscape:document-units="mm"
showgrid="false"
inkscape:snap-global="true"
inkscape:zoom="0.4734482"
inkscape:cx="11.616899"
inkscape:cy="186.92647"
inkscape:window-width="1920"
inkscape:window-height="1027"
inkscape:window-x="-8"
inkscape:window-y="-8"
inkscape:window-maximized="1"
inkscape:current-layer="layer1" />
<defs
id="defs2" />
<g
inkscape:label="Layer 1"
inkscape:groupmode="layer"
id="layer1"
transform="translate(-40.717182,-65.583996)">
<rect
style="fill:#51b6bf;fill-opacity:1;stroke:none;stroke-width:0.394384"
id="rect846"
width="130.02815"
height="180.11604"
x="40.717182"
y="69.200294"
ry="7.2278728" />
<rect
style="fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:0.355205"
id="rect6139"
width="114.73653"
height="165.58026"
x="48.362995"
y="76.468185"
ry="6.6445661" />
<g
id="g20855"
transform="translate(0,-4.5861139)">
<rect
style="fill:none;fill-opacity:1;stroke:#3b3485;stroke-width:1.465;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
id="rect4822-1"
width="8.1061144"
height="8.1061144"
x="56.92775"
y="113.97274"
ry="1.3718038" />
<rect
style="fill:#8383bc;fill-opacity:1;stroke:none;stroke-width:1.86051;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
id="rect5136-7"
width="42.169209"
height="3.4690557"
x="69.150803"
y="116.29127"
ry="1.7345278" />
</g>
<g
id="g20859"
transform="translate(0,-7.0555611)">
<rect
style="fill:none;fill-opacity:1;stroke:#3b3485;stroke-width:1.465;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
id="rect4822-14"
width="8.1061144"
height="8.1061144"
x="56.92775"
y="127.76641"
ry="1.3718038" />
<rect
style="fill:#8383bc;fill-opacity:1;stroke:none;stroke-width:1.43107;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
id="rect5136-7-2"
width="21.986813"
height="3.9363635"
x="68.953468"
y="129.85129"
ry="1.9681817" />
</g>
<g
id="g20845"
transform="translate(0,-2.1166667)">
<rect
style="fill:none;fill-opacity:1;stroke:#3b3485;stroke-width:1.465;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
id="rect4822"
width="8.1061144"
height="8.1061144"
x="56.92775"
y="100.17907"
ry="1.3718038" />
<rect
style="fill:#8383bc;fill-opacity:1;stroke:none;stroke-width:1.59414;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
id="rect5136"
width="28.571259"
height="3.7589138"
x="69.028404"
y="102.35267"
ry="1.8794569" />
<rect
style="fill:#8383bc;fill-opacity:1;stroke:none;stroke-width:1.50408;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
id="rect5136-5"
width="24.788029"
height="3.8569105"
x="102.89133"
y="102.30367"
ry="1.9284552" />
</g>
<g
id="g20863"
transform="translate(0,-9.5250005)">
<rect
style="fill:none;fill-opacity:1;stroke:#3b3485;stroke-width:1.465;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
id="rect4822-1-4"
width="8.1061144"
height="8.1061144"
x="56.92775"
y="141.56007"
ry="1.3718038" />
<rect
style="fill:#8383bc;fill-opacity:1;stroke:none;stroke-width:1.57172;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
id="rect5728"
width="27.594185"
height="3.7833092"
x="69.018089"
y="143.72148"
ry="1.8916546" />
</g>
<rect
style="fill:#202042;fill-opacity:1;stroke:none;stroke-width:0.23824"
id="rect1392"
width="53.152565"
height="18.233921"
x="-132.30754"
y="-83.817917"
ry="3.4788733"
transform="scale(-1)" />
<rect
style="fill:#8383bc;fill-opacity:1;stroke:none;stroke-width:2.22336;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
id="rect6412"
width="73.657707"
height="2.8362327"
x="56.336582"
y="201.33577"
ry="1.4181163" />
<rect
style="fill:#8383bc;fill-opacity:1;stroke:none;stroke-width:2.23876;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
id="rect6414"
width="69.712616"
height="3.0383809"
x="56.344284"
y="209.24466"
ry="1.5191904" />
<rect
style="fill:#8383bc;fill-opacity:1;stroke:none;stroke-width:2.42357;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
id="rect6416"
width="86.988022"
height="2.8535743"
x="56.436687"
y="217.23825"
ry="1.4267871" />
<rect
style="fill:#8383bc;fill-opacity:1;stroke:none;stroke-width:2.38882;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
id="rect6418"
width="83.495445"
height="2.8883159"
x="56.419315"
y="225.12207"
ry="1.444158" />
<rect
style="fill:#2f3898;fill-opacity:1;stroke:none;stroke-width:3.0459;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
id="rect6295-8"
width="57.490276"
height="6.8199105"
x="76.986122"
y="87.608482"
ry="3.4099553" />
<rect
style="fill:#2f3898;fill-opacity:1;stroke:none;stroke-width:2.58155;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
id="rect6490"
width="41.297459"
height="6.8199105"
x="85.082527"
y="188.09093"
ry="3.4099553" />
<rect
style="fill:#2f3898;fill-opacity:1;stroke:none;stroke-width:1.87563;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
id="rect6490-3"
width="21.8001"
height="6.8199105"
x="94.831207"
y="151.63762"
ry="3.4099553" />
<rect
style="opacity:1;fill:#2bc8d7;fill-opacity:1;stroke:none;stroke-width:1.63579;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
id="rect6492"
width="16.931116"
height="13.580167"
x="132.27409"
y="201.23309"
ry="2.6454868" />
<rect
style="opacity:1;fill:#dcf8f6;fill-opacity:1;stroke:none;stroke-width:1.465;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
id="rect6932"
width="79.717339"
height="19.929335"
x="65.872589"
y="161.02196"
ry="5.6437054" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 7.6 KiB

View File

@ -1,2 +1,2 @@
import "../../node_modules/bootstrap/js/dist/collapse";
import "~/node_modules/bootstrap/js/dist/collapse";
import "simplebar";

View File

@ -1 +1,115 @@
console.log("abc");
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: {}
};
async function main() {
setupInteractiveBehavior();
await setupInitialValues((await apiHttp.get("/Search/Default/Outline/Filters")).data);
await setupShopToggles((await apiHttp.get("/Search/Shops/Available")).data);
document.getElementById("configuration").querySelector(".invisible").classList.remove("invisible"); // Load completed, show the UI.
}
function setupInteractiveBehavior() {
document.getElementById("config-check-toggle").addEventListener("change", function () {
let configElem = document.getElementById("configuration");
if (this.checked) {
configElem.classList.remove("d-none");
} else {
configElem.classList.add("d-none");
}
});
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 setupInitialValues(filters) {
inputs.maxShippingEnabled.checked = filters.enableMaxShippingFee;
inputs.maxShippingEnabled.dispatchEvent(new Event("change"));
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"));
}
async function setupShopToggles(availableShops) {
let disabledShops = (await apiHttp.get("/Search/Default/Outline/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);
});
}
main();
// TODO: Animate configuration toggle.

View File

@ -1,3 +1,12 @@
$themes: (
"light": ("background": #f4f4f4, "navbar": #FFF8F8, "main": #BDF2D5, "footer": #F2F2F2,"sub": #F4FCFC, "bold": #647b9b, "text": #1A1A1A, "muted": #797a7e),
"light": (
"background": #f4f4f4,
"navbar": #FFF8F8,
"main": #BDF2D5,
"footer": #F2F2F2,
"sub": #F4FCFC,
"bold": #647b9b,
"text": #1A1A1A,
"muted": #797a7e,
),
);

View File

@ -1,7 +1,6 @@
@use "themer";
@use "~/node_modules/bootstrap/scss/bootstrap";
@import "~/node_modules/bootstrap-icons/font/bootstrap-icons.css";
@import "~/node_modules/simplebar/dist/simplebar.min.css";
@use "sass:color";
header > nav {
@extend .navbar-expand-lg;
@ -33,6 +32,19 @@ main {
flex-grow: 1;
display: flex;
flex-direction: column;
.tear {
@extend .p-3;
border-top: 1px;
border-top-style: solid;
border-bottom: 1px;
border-bottom-style: solid;
@include themer.themed {
$tear: themer.color-of("background");
border-color: adjust-color($color: $tear, $lightness: -20%, $alpha: 1.0);
background-color: adjust-color($color: $tear, $lightness: -5%, $alpha: 1.0);
}
}
}
footer {
@ -65,6 +77,16 @@ footer {
background-color: themer.color-of("sub");
}
}
&.sole {
@include themer.themed {
background-color: themer.color-of("main");
border-color: themer.color-of("sub");
border-top-style: solid;
border-bottom-style: solid;
border-width: 1em;
}
}
}
.concise {
@ -113,3 +135,6 @@ body {
color: themer.color-of("text");
}
}
@import "~/node_modules/bootstrap-icons/font/bootstrap-icons.css";
@import "~/node_modules/simplebar/dist/simplebar.min.css";

11
Props/babel.config.js Normal file
View File

@ -0,0 +1,11 @@
module.exports = function (api) {
api.cache(true);
const presets = [];
const plugins = [];
return {
presets,
plugins
};
};

View File

@ -63,6 +63,23 @@
}
}
},
"@babel/eslint-parser": {
"version": "7.14.7",
"resolved": "https://registry.npmjs.org/@babel/eslint-parser/-/eslint-parser-7.14.7.tgz",
"integrity": "sha512-6WPwZqO5priAGIwV6msJcdc9TsEPzYeYdS/Xuoap+/ihkgN6dzHp2bcAAwyWZ5bLzk0vvjDmKvRwkqNaiJ8BiQ==",
"requires": {
"eslint-scope": "^5.1.1",
"eslint-visitor-keys": "^2.1.0",
"semver": "^6.3.0"
},
"dependencies": {
"semver": {
"version": "6.3.0",
"resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz",
"integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw=="
}
}
},
"@babel/generator": {
"version": "7.14.8",
"resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.14.8.tgz",
@ -950,6 +967,28 @@
"@babel/helper-plugin-utils": "^7.14.5"
}
},
"@babel/plugin-transform-runtime": {
"version": "7.14.5",
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-runtime/-/plugin-transform-runtime-7.14.5.tgz",
"integrity": "sha512-fPMBhh1AV8ZyneiCIA+wYYUH1arzlXR1UMcApjvchDhfKxhy2r2lReJv8uHEyihi4IFIGlr1Pdx7S5fkESDQsg==",
"dev": true,
"requires": {
"@babel/helper-module-imports": "^7.14.5",
"@babel/helper-plugin-utils": "^7.14.5",
"babel-plugin-polyfill-corejs2": "^0.2.2",
"babel-plugin-polyfill-corejs3": "^0.2.2",
"babel-plugin-polyfill-regenerator": "^0.2.2",
"semver": "^6.3.0"
},
"dependencies": {
"semver": {
"version": "6.3.0",
"resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz",
"integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==",
"dev": true
}
}
},
"@babel/plugin-transform-shorthand-properties": {
"version": "7.14.5",
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-shorthand-properties/-/plugin-transform-shorthand-properties-7.14.5.tgz",
@ -1121,7 +1160,6 @@
"version": "7.14.8",
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.14.8.tgz",
"integrity": "sha512-twj3L8Og5SaCRCErB4x4ajbvBIVV77CGeFglHpeg5WC5FF8TZzBWXtTJ4MqaD9QszLYTtr+IsaAL2rEUevb+eg==",
"dev": true,
"requires": {
"regenerator-runtime": "^0.13.4"
}
@ -2136,7 +2174,6 @@
"version": "5.1.1",
"resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz",
"integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==",
"dev": true,
"requires": {
"esrecurse": "^4.3.0",
"estraverse": "^4.1.1"
@ -2162,8 +2199,7 @@
"eslint-visitor-keys": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-2.1.0.tgz",
"integrity": "sha512-0rSmRBzXgDzIsD6mGdJgevzgezI534Cer5L/vyMX0kHzT/jiB43jRhd9YUlMGYLQy2zprNmoT8qasCGtY+QaKw==",
"dev": true
"integrity": "sha512-0rSmRBzXgDzIsD6mGdJgevzgezI534Cer5L/vyMX0kHzT/jiB43jRhd9YUlMGYLQy2zprNmoT8qasCGtY+QaKw=="
},
"espree": {
"version": "7.3.1",
@ -2217,7 +2253,6 @@
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz",
"integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==",
"dev": true,
"requires": {
"estraverse": "^5.2.0"
},
@ -2225,16 +2260,14 @@
"estraverse": {
"version": "5.2.0",
"resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.2.0.tgz",
"integrity": "sha512-BxbNGGNm0RyRYvUdHpIwv9IWzeM9XClbOxwoATuFdOE7ZE6wHL+HQ5T8hoPM+zHvmKzzsEqhgy0GrQ5X13afiQ==",
"dev": true
"integrity": "sha512-BxbNGGNm0RyRYvUdHpIwv9IWzeM9XClbOxwoATuFdOE7ZE6wHL+HQ5T8hoPM+zHvmKzzsEqhgy0GrQ5X13afiQ=="
}
}
},
"estraverse": {
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz",
"integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==",
"dev": true
"integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw=="
},
"esutils": {
"version": "2.0.3",
@ -3227,8 +3260,7 @@
"regenerator-runtime": {
"version": "0.13.7",
"resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.7.tgz",
"integrity": "sha512-a54FxoJDIr27pgf7IgeQGxmqUNYrcV338lf/6gH456HZ/PhX+5BcwHXG9ajESmwe6WRO0tAzRUrRmNONWgkrew==",
"dev": true
"integrity": "sha512-a54FxoJDIr27pgf7IgeQGxmqUNYrcV338lf/6gH456HZ/PhX+5BcwHXG9ajESmwe6WRO0tAzRUrRmNONWgkrew=="
},
"regenerator-transform": {
"version": "0.14.5",

View File

@ -9,6 +9,7 @@
"license": "ISC",
"devDependencies": {
"@babel/core": "^7.14.8",
"@babel/plugin-transform-runtime": "^7.14.5",
"@babel/preset-env": "^7.14.8",
"babel-eslint": "^10.1.0",
"babel-loader": "^8.2.2",
@ -23,6 +24,8 @@
"webpack-cli": "^4.7.2"
},
"dependencies": {
"@babel/eslint-parser": "^7.14.7",
"@babel/runtime": "^7.14.8",
"axios": "^0.21.1",
"bootstrap": "^5.0.2",
"bootstrap-icons": "^1.5.0",

BIN
Props/shops/FuzzySharp.dll Normal file

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -31,7 +31,8 @@ let config = {
use: {
loader: "babel-loader",
options: {
presets: ["@babel/preset-env"]
presets: ["@babel/preset-env"],
plugins: ["@babel/plugin-transform-runtime"]
}
}
},