diff --git a/Props-Modules/Props.Shop/Adafruit/Api/ProductListingManager.cs b/Props-Modules/Props.Shop/Adafruit/Api/ProductListingManager.cs index 96794d4..6305a2e 100644 --- a/Props-Modules/Props.Shop/Adafruit/Api/ProductListingManager.cs +++ b/Props-Modules/Props.Shop/Adafruit/Api/ProductListingManager.cs @@ -15,7 +15,7 @@ namespace Props.Shop.Adafruit.Api private Dictionary> listings = new Dictionary>(); 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) diff --git a/Props-Modules/Props.Shop/Framework/Filters.cs b/Props-Modules/Props.Shop/Framework/Filters.cs index 8ef4000..72c354d 100644 --- a/Props-Modules/Props.Shop/Framework/Filters.cs +++ b/Props-Modules/Props.Shop/Framework/Filters.cs @@ -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; + } } } \ No newline at end of file diff --git a/Props-Modules/test/Props.Shop/Adafruit.Tests/Api/ProductListingManagerTest.cs b/Props-Modules/test/Props.Shop/Adafruit.Tests/Api/ProductListingManagerTest.cs index 6db36f2..0417170 100644 --- a/Props-Modules/test/Props.Shop/Adafruit.Tests/Api/ProductListingManagerTest.cs +++ b/Props-Modules/test/Props.Shop/Adafruit.Tests/Api/ProductListingManagerTest.cs @@ -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 results = new List(); await foreach (ProductListing item in mockProductListingManager.Search("arduino", 0.5f)) { diff --git a/Props/.eslintrc.js b/Props/.eslintrc.js index 05ceeb1..62f37aa 100644 --- a/Props/.eslintrc.js +++ b/Props/.eslintrc.js @@ -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" + }] } }; diff --git a/Props/Areas/Identity/Pages/Account/Login.cshtml b/Props/Areas/Identity/Pages/Account/Login.cshtml index 99d4780..4660fbe 100644 --- a/Props/Areas/Identity/Pages/Account/Login.cshtml +++ b/Props/Areas/Identity/Pages/Account/Login.cshtml @@ -6,8 +6,9 @@ }
-
-

@ViewData["Title"]

+
+ Props logo +

@ViewData["Title"]

diff --git a/Props/Areas/Identity/Pages/Account/Manage/_ManageNav.cshtml b/Props/Areas/Identity/Pages/Account/Manage/_ManageNav.cshtml index b5a56a7..2fa6b46 100644 --- a/Props/Areas/Identity/Pages/Account/Manage/_ManageNav.cshtml +++ b/Props/Areas/Identity/Pages/Account/Manage/_ManageNav.cshtml @@ -13,3 +13,5 @@ + +@* TODO: Finish styling this page. *@ \ No newline at end of file diff --git a/Props/Areas/Identity/Pages/Account/Register.cshtml b/Props/Areas/Identity/Pages/Account/Register.cshtml index 3df0a65..cad97e2 100644 --- a/Props/Areas/Identity/Pages/Account/Register.cshtml +++ b/Props/Areas/Identity/Pages/Account/Register.cshtml @@ -5,8 +5,9 @@ }
-
-

@ViewData["Title"]

+
+ Props logo +

@ViewData["Title"]

diff --git a/Props/Areas/Identity/Pages/Account/RegisterConfirmation.cshtml b/Props/Areas/Identity/Pages/Account/RegisterConfirmation.cshtml index 6c20a6d..dcc5edd 100644 --- a/Props/Areas/Identity/Pages/Account/RegisterConfirmation.cshtml +++ b/Props/Areas/Identity/Pages/Account/RegisterConfirmation.cshtml @@ -20,4 +20,4 @@

} } -@* TODO: https://aka.ms/aspaccountconf *@ \ No newline at end of file +@* TODO: Do something about this. *@ \ No newline at end of file diff --git a/Props/Controllers/SearchController.cs b/Props/Controllers/SearchController.cs new file mode 100644 index 0000000..16a01e9 --- /dev/null +++ b/Props/Controllers/SearchController.cs @@ -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); + } + } +} \ No newline at end of file diff --git a/Props/Data/ApplicationDbContext.cs b/Props/Data/ApplicationDbContext.cs index 523db02..fb7b6af 100644 --- a/Props/Data/ApplicationDbContext.cs +++ b/Props/Data/ApplicationDbContext.cs @@ -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 { + DbSet SearchOutlines { get; set; } + DbSet TrackedListings { get; set; } public ApplicationDbContext(DbContextOptions options) : base(options) { @@ -37,11 +40,11 @@ namespace Props.Data ); modelBuilder.Entity() - .Property(e => e.ShopStates) + .Property(e => e.Disabled) .HasConversion( v => JsonSerializer.Serialize(v, null), - v => JsonSerializer.Deserialize(v, null), - new ValueComparer( + v => JsonSerializer.Deserialize(v, null), + new ValueComparer( (a, b) => a.Equals(b), c => c.GetHashCode(), c => c.Copy() diff --git a/Props/Data/Migrations/20210721064503_InitialCreate.Designer.cs b/Props/Data/Migrations/20210722180024_InitialCreate.Designer.cs similarity index 84% rename from Props/Data/Migrations/20210721064503_InitialCreate.Designer.cs rename to Props/Data/Migrations/20210722180024_InitialCreate.Designer.cs index 1672d28..96c292f 100644 --- a/Props/Data/Migrations/20210721064503_InitialCreate.Designer.cs +++ b/Props/Data/Migrations/20210722180024_InitialCreate.Designer.cs @@ -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("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ApplicationUserId") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Order") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("ProfileName") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("ApplicationUserId") + .IsUnique(); + + b.ToTable("ResultsPreferences"); + }); + + modelBuilder.Entity("Props.Models.Search.ProductListingInfo", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Hits") + .HasColumnType("INTEGER"); + + b.Property("LastUpdated") + .HasColumnType("TEXT"); + + b.Property("ProductName") + .HasColumnType("TEXT"); + + b.Property("ProductUrl") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("TrackedListings"); + }); + + modelBuilder.Entity("Props.Models.Search.SearchOutline", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ApplicationUserId") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Disabled") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Filters") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("ApplicationUserId"); + + b.ToTable("SearchOutlines"); + }); + + modelBuilder.Entity("Props.Models.User.ApplicationUser", b => { b.Property("Id") .HasColumnType("TEXT"); @@ -214,56 +287,6 @@ namespace Props.Data.Migrations b.ToTable("AspNetUsers"); }); - modelBuilder.Entity("Props.Models.ResultsPreferences", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("ApplicationUserId") - .IsRequired() - .HasColumnType("TEXT"); - - b.Property("Order") - .IsRequired() - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("ApplicationUserId") - .IsUnique(); - - b.ToTable("ResultsPreferences"); - }); - - modelBuilder.Entity("Props.Models.SearchOutline", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("ApplicationUserId") - .IsRequired() - .HasColumnType("TEXT"); - - b.Property("Filters") - .HasColumnType("TEXT"); - - b.Property("MaxResults") - .HasColumnType("INTEGER"); - - b.Property("ShopStates") - .IsRequired() - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("ApplicationUserId") - .IsUnique(); - - b.ToTable("SearchOutline"); - }); - modelBuilder.Entity("Props.Shared.Models.User.ApplicationPreferences", b => { b.Property("Id") @@ -299,7 +322,7 @@ namespace Props.Data.Migrations modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", 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", 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", 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 } diff --git a/Props/Data/Migrations/20210721064503_InitialCreate.cs b/Props/Data/Migrations/20210722180024_InitialCreate.cs similarity index 90% rename from Props/Data/Migrations/20210721064503_InitialCreate.cs rename to Props/Data/Migrations/20210722180024_InitialCreate.cs index da1d645..2f7e628 100644 --- a/Props/Data/Migrations/20210721064503_InitialCreate.cs +++ b/Props/Data/Migrations/20210722180024_InitialCreate.cs @@ -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(type: "INTEGER", nullable: false) + .Annotation("Sqlite:Autoincrement", true), + Hits = table.Column(type: "INTEGER", nullable: false), + LastUpdated = table.Column(type: "TEXT", nullable: false), + ProductUrl = table.Column(type: "TEXT", nullable: true), + ProductName = table.Column(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(type: "INTEGER", nullable: false) .Annotation("Sqlite:Autoincrement", true), ApplicationUserId = table.Column(type: "TEXT", nullable: false), - Order = table.Column(type: "TEXT", nullable: false) + Order = table.Column(type: "TEXT", nullable: false), + ProfileName = table.Column(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(type: "INTEGER", nullable: false) .Annotation("Sqlite:Autoincrement", true), ApplicationUserId = table.Column(type: "TEXT", nullable: false), Filters = table.Column(type: "TEXT", nullable: true), - MaxResults = table.Column(type: "INTEGER", nullable: false), - ShopStates = table.Column(type: "TEXT", nullable: false) + Disabled = table.Column(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"); diff --git a/Props/Data/Migrations/ApplicationDbContextModelSnapshot.cs b/Props/Data/Migrations/ApplicationDbContextModelSnapshot.cs index 5039d0f..dd9e44d 100644 --- a/Props/Data/Migrations/ApplicationDbContextModelSnapshot.cs +++ b/Props/Data/Migrations/ApplicationDbContextModelSnapshot.cs @@ -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("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ApplicationUserId") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Order") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("ProfileName") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("ApplicationUserId") + .IsUnique(); + + b.ToTable("ResultsPreferences"); + }); + + modelBuilder.Entity("Props.Models.Search.ProductListingInfo", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Hits") + .HasColumnType("INTEGER"); + + b.Property("LastUpdated") + .HasColumnType("TEXT"); + + b.Property("ProductName") + .HasColumnType("TEXT"); + + b.Property("ProductUrl") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("TrackedListings"); + }); + + modelBuilder.Entity("Props.Models.Search.SearchOutline", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ApplicationUserId") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Disabled") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Filters") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("ApplicationUserId"); + + b.ToTable("SearchOutlines"); + }); + + modelBuilder.Entity("Props.Models.User.ApplicationUser", b => { b.Property("Id") .HasColumnType("TEXT"); @@ -212,56 +285,6 @@ namespace Props.Data.Migrations b.ToTable("AspNetUsers"); }); - modelBuilder.Entity("Props.Models.ResultsPreferences", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("ApplicationUserId") - .IsRequired() - .HasColumnType("TEXT"); - - b.Property("Order") - .IsRequired() - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("ApplicationUserId") - .IsUnique(); - - b.ToTable("ResultsPreferences"); - }); - - modelBuilder.Entity("Props.Models.SearchOutline", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("ApplicationUserId") - .IsRequired() - .HasColumnType("TEXT"); - - b.Property("Filters") - .HasColumnType("TEXT"); - - b.Property("MaxResults") - .HasColumnType("INTEGER"); - - b.Property("ShopStates") - .IsRequired() - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("ApplicationUserId") - .IsUnique(); - - b.ToTable("SearchOutline"); - }); - modelBuilder.Entity("Props.Shared.Models.User.ApplicationPreferences", b => { b.Property("Id") @@ -297,7 +320,7 @@ namespace Props.Data.Migrations modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", 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", 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", 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 } diff --git a/Props/Models/Search/ProductListingInfo.cs b/Props/Models/Search/ProductListingInfo.cs new file mode 100644 index 0000000..0553580 --- /dev/null +++ b/Props/Models/Search/ProductListingInfo.cs @@ -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; } + } +} \ No newline at end of file diff --git a/Props/Models/User/SearchOutline.cs b/Props/Models/Search/SearchOutline.cs similarity index 76% rename from Props/Models/User/SearchOutline.cs rename to Props/Models/Search/SearchOutline.cs index 212538c..c7e401d 100644 --- a/Props/Models/User/SearchOutline.cs +++ b/Props/Models/Search/SearchOutline.cs @@ -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 + public sealed class ShopsDisabled : HashSet { 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() diff --git a/Props/Models/User/ApplicationPreferences.cs b/Props/Models/User/ApplicationPreferences.cs index 970ffe9..74c3ffb 100644 --- a/Props/Models/User/ApplicationPreferences.cs +++ b/Props/Models/User/ApplicationPreferences.cs @@ -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; } diff --git a/Props/Models/User/ApplicationUser.cs b/Props/Models/User/ApplicationUser.cs index af9c4d9..d035e8c 100644 --- a/Props/Models/User/ApplicationUser.cs +++ b/Props/Models/User/ApplicationUser.cs @@ -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 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; } diff --git a/Props/Models/User/ResultsPreferences.cs b/Props/Models/User/ResultsPreferences.cs index 22f3a2b..3289f41 100644 --- a/Props/Models/User/ResultsPreferences.cs +++ b/Props/Models/User/ResultsPreferences.cs @@ -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 Order { get; set; } + [Required] + public string ProfileName { get; set; } + public ResultsPreferences() { Order = new List(Enum.GetValues().Length); diff --git a/Props/Options/ModulesOptions.cs b/Props/Options/ModulesOptions.cs new file mode 100644 index 0000000..5e626ca --- /dev/null +++ b/Props/Options/ModulesOptions.cs @@ -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; } + } +} \ No newline at end of file diff --git a/Props/Pages/Index.cshtml b/Props/Pages/Index.cshtml index 276623d..a96e1ce 100644 --- a/Props/Pages/Index.cshtml +++ b/Props/Pages/Index.cshtml @@ -8,7 +8,7 @@
- Props logo + Props logo

Props

diff --git a/Props/Pages/Search.cshtml b/Props/Pages/Search.cshtml index d2806f1..019df12 100644 --- a/Props/Pages/Search.cshtml +++ b/Props/Pages/Search.cshtml @@ -4,13 +4,107 @@ ViewData["Specific"] = "Search"; } -
- +
+
- +
- +
+ +
+ +
+ +@* TODO: Add results display and default results display *@ \ No newline at end of file diff --git a/Props/Pages/Search.cshtml.cs b/Props/Pages/Search.cshtml.cs index 74e9cd0..7f55d95 100644 --- a/Props/Pages/Search.cshtml.cs +++ b/Props/Pages/Search.cshtml.cs @@ -4,6 +4,6 @@ namespace Props.Pages { public class SearchModel : PageModel { - + // TODO: Complete the search model. } } \ No newline at end of file diff --git a/Props/Pages/Shared/_Layout.cshtml b/Props/Pages/Shared/_Layout.cshtml index 287215d..8ac047e 100644 --- a/Props/Pages/Shared/_Layout.cshtml +++ b/Props/Pages/Shared/_Layout.cshtml @@ -11,10 +11,6 @@ @ViewData["Title"] - Props - @if (!string.IsNullOrEmpty((ViewData["Specific"] as string))) - { - - } @@ -67,6 +63,10 @@ © 2021 - Props - Privacy + @if (!string.IsNullOrEmpty((ViewData["Specific"] as string))) + { + + } @await RenderSectionAsync("Scripts", required: false) diff --git a/Props/Services/Content/ContentManager.cs b/Props/Services/Content/CachedContentManager.cs similarity index 64% rename from Props/Services/Content/ContentManager.cs rename to Props/Services/Content/CachedContentManager.cs index 0e4262b..59902fb 100644 --- a/Props/Services/Content/ContentManager.cs +++ b/Props/Services/Content/CachedContentManager.cs @@ -3,13 +3,13 @@ using Newtonsoft.Json.Linq; namespace Props.Services.Content { - public class ContentManager : IContentManager + public class CachedContentManager : IContentManager { private dynamic data; private readonly string directory; private readonly string fileName; - dynamic IContentManager.Json + dynamic IContentManager.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"; } } } \ No newline at end of file diff --git a/Props/Services/Modules/IShopManager.cs b/Props/Services/Modules/IShopManager.cs new file mode 100644 index 0000000..72d962d --- /dev/null +++ b/Props/Services/Modules/IShopManager.cs @@ -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 AvailableShops(); + public Task> Search(string query, SearchOutline searchOutline); + } +} \ No newline at end of file diff --git a/Props/Services/Modules/LocalShopManager.cs b/Props/Services/Modules/LocalShopManager.cs new file mode 100644 index 0000000..c5f3248 --- /dev/null +++ b/Props/Services/Modules/LocalShopManager.cs @@ -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 logger; + private Dictionary shops; + private ModulesOptions options; + private IConfiguration configuration; + public LocalShopManager(IConfiguration configuration, ILogger logger) + { + this.configuration = configuration; + this.logger = logger; + options = configuration.GetSection(ModulesOptions.Modules).Get(); + + shops = new Dictionary(); + 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 AvailableShops() + { + return shops.Keys; + } + + public async Task> Search(string query, SearchOutline searchOutline) + { + List results = new List(); + 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 LoadShops(string shopsDir, string shopRegex, bool recursiveLoad) + { + Stack directories = new Stack(); + 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); + } + } + } + } + } + } +} \ No newline at end of file diff --git a/Props/Services/Modules/ShopAssemblyLoadContext.cs b/Props/Services/Modules/ShopAssemblyLoadContext.cs index 02f1165..4d3c16f 100644 --- a/Props/Services/Modules/ShopAssemblyLoadContext.cs +++ b/Props/Services/Modules/ShopAssemblyLoadContext.cs @@ -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; } diff --git a/Props/Startup.cs b/Props/Startup.cs index bce372d..fc35776 100644 --- a/Props/Startup.cs +++ b/Props/Startup.cs @@ -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(); services.AddRazorPages(); - services.AddSingleton(typeof(IContentManager<>), typeof(ContentManager<>)); + services.AddSingleton(typeof(IContentManager<>), typeof(CachedContentManager<>)); + services.AddSingleton(); } // 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(); }); } diff --git a/Props/appsettings.json b/Props/appsettings.json index 6b9dcee..8f4900b 100644 --- a/Props/appsettings.json +++ b/Props/appsettings.json @@ -9,8 +9,11 @@ "Microsoft.Hosting.Lifetime": "Information" } }, - "Shops": { - "Path": "./shops" + "Modules": { + "ShopsDir": "./shops", + "RecursiveLoad": "false", + "MaxResults": "100", + "ShopRegex": "Props\\.Shop\\.." }, "AllowedHosts": "*" } \ No newline at end of file diff --git a/Props/assets/images/logo-simplified.svg b/Props/assets/images/logo-simplified.svg new file mode 100644 index 0000000..7f91365 --- /dev/null +++ b/Props/assets/images/logo-simplified.svg @@ -0,0 +1,232 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Props/assets/js/main.js b/Props/assets/js/main.js index 593c4e2..f490be1 100644 --- a/Props/assets/js/main.js +++ b/Props/assets/js/main.js @@ -1,2 +1,2 @@ -import "../../node_modules/bootstrap/js/dist/collapse"; +import "~/node_modules/bootstrap/js/dist/collapse"; import "simplebar"; diff --git a/Props/assets/js/specific/search.js b/Props/assets/js/specific/search.js index 375eb06..a6cac76 100644 --- a/Props/assets/js/specific/search.js +++ b/Props/assets/js/specific/search.js @@ -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. diff --git a/Props/assets/scss/_variables.scss b/Props/assets/scss/_variables.scss index 655447f..8649f20 100644 --- a/Props/assets/scss/_variables.scss +++ b/Props/assets/scss/_variables.scss @@ -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, + ), ); diff --git a/Props/assets/scss/main.scss b/Props/assets/scss/main.scss index 1ecba19..af519f9 100644 --- a/Props/assets/scss/main.scss +++ b/Props/assets/scss/main.scss @@ -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 { @@ -112,4 +134,7 @@ body { background-color: themer.color-of("background"); color: themer.color-of("text"); } -} \ No newline at end of file +} + +@import "~/node_modules/bootstrap-icons/font/bootstrap-icons.css"; +@import "~/node_modules/simplebar/dist/simplebar.min.css"; diff --git a/Props/babel.config.js b/Props/babel.config.js new file mode 100644 index 0000000..d489795 --- /dev/null +++ b/Props/babel.config.js @@ -0,0 +1,11 @@ +module.exports = function (api) { + api.cache(true); + + const presets = []; + const plugins = []; + + return { + presets, + plugins + }; +}; diff --git a/Props/package-lock.json b/Props/package-lock.json index dc59c53..1806247 100644 --- a/Props/package-lock.json +++ b/Props/package-lock.json @@ -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", diff --git a/Props/package.json b/Props/package.json index 9c0aea6..69050c2 100644 --- a/Props/package.json +++ b/Props/package.json @@ -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", diff --git a/Props/shops/FuzzySharp.dll b/Props/shops/FuzzySharp.dll new file mode 100644 index 0000000..ade710f Binary files /dev/null and b/Props/shops/FuzzySharp.dll differ diff --git a/Props/shops/Newtonsoft.Json.dll b/Props/shops/Newtonsoft.Json.dll new file mode 100644 index 0000000..1ffeabe Binary files /dev/null and b/Props/shops/Newtonsoft.Json.dll differ diff --git a/Props/shops/Props.Shop.Adafruit.dll b/Props/shops/Props.Shop.Adafruit.dll new file mode 100644 index 0000000..ac442bd Binary files /dev/null and b/Props/shops/Props.Shop.Adafruit.dll differ diff --git a/Props/webpack.config.js b/Props/webpack.config.js index caaecee..75059af 100644 --- a/Props/webpack.config.js +++ b/Props/webpack.config.js @@ -31,7 +31,8 @@ let config = { use: { loader: "babel-loader", options: { - presets: ["@babel/preset-env"] + presets: ["@babel/preset-env"], + plugins: ["@babel/plugin-transform-runtime"] } } },