Added primitive search mechanism in backend.

Began implementing search mechanism for frontend.
This commit is contained in:
2021-08-05 01:22:19 -05:00
parent f71758ca69
commit c94ea4a624
56 changed files with 1623 additions and 490 deletions

View File

@@ -1,25 +1,32 @@
@page
@using Props.Services.Content
@model SearchModel
@inject IContentManager<SearchModel> ContentManager
@{
ViewData["Title"] = "Search";
ViewData["Specific"] = "Search";
}
<div>
<div class="my-4 less-concise mx-auto">
<div class="mt-4 mb-3">
<div class="less-concise mx-auto">
<div class="input-group">
<input type="text" class="form-control bg-transparent border-primary" placeholder="What are you looking for?" aria-label="Search" aria-describedby="search-btn" id="search-bar">
<button class="btn btn-outline-secondary" type="button" id="configuration-toggle" data-bs-toggle="collapse" data-bs-target="#configuration"><i class="bi bi-sliders"></i></button>
<button class="btn btn-outline-primary" type="button" id="search-btn">Search</button>
<input type="text" class="form-control border-primary" placeholder="What are you looking for?"
aria-label="Search" aria-describedby="search-btn" id="search-bar" value="@Model.SearchQuery">
<button class="btn btn-outline-secondary" type="button" id="configuration-toggle" data-bs-toggle="collapse"
data-bs-target="#configuration"><i class="bi bi-sliders"></i></button>
<button class="btn btn-primary" type="button" id="search-btn">Search</button>
</div>
</div>
</div>
<div class="collapse tear" id="configuration">
<div class="collapse tear" id="configuration" x-data="configuration">
<div class="p-3">
<div class="container invisible">
<div class="container">
<div class="d-flex">
<h1 class="my-2 display-2 me-auto">Configuration</h1>
<button class="btn align-self-start" type="button" id="configuration-close" data-bs-toggle="collapse" data-bs-target="#configuration"><i class="bi bi-x-lg"></i></button>
<button class="btn align-self-start" type="button" id="configuration-close" data-bs-toggle="collapse"
data-bs-target="#configuration"><i class="bi bi-x-lg"></i></button>
</div>
<div class="row justify-content-md-center">
<section class="col-lg px-4">
@@ -28,10 +35,11 @@
<label for="max-price" class="form-label">Maximum Price</label>
<div class="input-group">
<div class="input-group-text">
<input class="form-check-input mt-0" type="checkbox" id="max-price-enabled">
<input class="form-check-input mt-0" type="checkbox" id="max-price-enabled"
x-model="maxPriceEnabled">
</div>
<span class="input-group-text">$</span>
<input type="number" class="form-control" min="0" id="max-price">
<input type="number" class="form-control" min="0" id="max-price" x-model="maxPrice">
<span class="input-group-text">.00</span>
</div>
</div>
@@ -39,7 +47,7 @@
<label for="min-price" class="form-label">Minimum Price</label>
<div class="input-group">
<span class="input-group-text">$</span>
<input type="number" class="form-control" min="0" id="min-price">
<input type="number" class="form-control" min="0" id="min-price" x-model="minPrice">
<span class="input-group-text">.00</span>
</div>
</div>
@@ -50,13 +58,14 @@
<input class="form-check-input mt-0" type="checkbox" id="max-shipping-enabled">
</div>
<span class="input-group-text">$</span>
<input type="number" class="form-control" min="0" id="max-shipping">
<input type="number" class="form-control" min="0" id="max-shipping" x-model="maxShipping">
<span class="input-group-text">.00</span>
</div>
</div>
<div class="mb-3">
<div class="form-check">
<input class="form-check-input" type="checkbox" id="keep-unknown-shipping">
<input class="form-check-input" type="checkbox" id="keep-unknown-shipping"
x-model="keepUnknownShipping">
<label class="form-check-label" for="keep-unknown-shipping">Keep Unknown Shipping</label>
</div>
</div>
@@ -66,37 +75,41 @@
<div class="mb-3">
<label for="min-purchases" class="form-label">Minimum Purchases</label>
<div class="input-group">
<input type="number" class="form-control" min="0" id="min-purchases">
<input type="number" class="form-control" min="0" id="min-purchases" x-model="minPurchases">
<span class="input-group-text">Purchases</span>
</div>
</div>
<div class="mb-3">
<div class="form-check">
<input class="form-check-input" type="checkbox" id="keep-unknown-purchases">
<input class="form-check-input" type="checkbox" id="keep-unknown-purchases"
x-model="keepUnknownPurchases">
<label class="form-check-label" for="keep-unknown-purchases">Keep Unknown Purchases</label>
</div>
</div>
<div class="mb-3">
<label for="min-reviews" class="form-label">Minimum Reviews</label>
<div class="input-group">
<input type="number" class="form-control" min="0" id="min-reviews">
<input type="number" class="form-control" min="0" id="min-reviews" x-model="minReviews">
<span class="input-group-text">Reviews</span>
</div>
</div>
<div class="mb-3">
<div class="form-check">
<input class="form-check-input" type="checkbox" id="keep-unknown-reviews">
<label class="form-check-label" for="keep-unknown-reviews">Keep Unknown Number of Reviews</label>
<input class="form-check-input" type="checkbox" id="keep-unknown-reviews"
x-model="keepUnknownReviews">
<label class="form-check-label" for="keep-unknown-reviews">Keep Unknown Number of
Reviews</label>
</div>
</div>
<div class="mb-1">
<label for="min-rating" class="form-label">Minimum Rating</label>
<input type="range" class="form-range" id="min-rating" min="0" max="100" step="1">
<div id="min-rating-display" class="form-text"></div>
<input type="range" class="form-range" id="min-rating" min="0" max="100" step="1"
x-model="minRating">
<div id="min-rating-display" class="form-text">Minimum rating: <b x-text="minRating"></b>%</div>
</div>
<div class="mb-3">
<div class="form-check">
<input class="form-check-input" type="checkbox" id="keep-unrated">
<input class="form-check-input" type="checkbox" id="keep-unrated" x-model="keepUnrated">
<label class="form-check-label" for="keep-unrated">Keep Unrated Items</label>
</div>
</div>
@@ -104,6 +117,14 @@
<section class="col-lg px-4">
<h3>Shops Enabled</h3>
<div class="mb-3 px-3" id="shop-checkboxes">
<template x-for="shop in Object.keys(shops)">
<div class="form-check">
<input class="form-check-input" type="checkbox" :id="`${shop}-enabled`"
x-model="shops[shop]">
<label class="form-check-label" :for="`${shop}-enabled`"><span
x-text="shop"></span></label>
</div>
</template>
</div>
</section>
</div>
@@ -111,4 +132,111 @@
</div>
</div>
@* TODO: Add results display and default results display *@
<div id="content-pages" class="multipage mt-3 invisible">
<ul class="nav nav-pills selectors">
<li class="nav-item" role="presentation">
<button type="button" data-bs-toggle="pill" data-bs-target="#quick-picks-slide"><i
class="bi bi-stopwatch"></i></button>
</li>
<li class="nav-item" role="presentation">
<button type="button" data-bs-toggle="pill" data-bs-target="#results-slide"><i
class="bi bi-view-list"></i></button>
</li>
<li class="nav-item" role="presentation">
<button type="button" data-bs-toggle="pill" data-bs-target="#info-slide"><i
class="bi bi-info-lg"></i></button>
</li>
</ul>
<div class="multipage-slides tab-content">
<div class="multipage-slide tab-pane fade" id="quick-picks-slide">
<div class="multipage-title">
<h1 class="display-2"><i class="bi bi-stopwatch"></i> Quick Picks</h1>
@if (Model.SearchResults != null)
{
<p>@ContentManager.Json.quickPicks.searched</p>
}
else
{
<p>@ContentManager.Json.quickPicks.prompt</p>
}
<hr class="less-concise">
</div>
<div class="multipage-content">
@if (Model.SearchResults != null)
{
@if (Model.BestRatingPriceRatio != null)
{
<p>We found this product to have the best rating to price ratio.</p>
}
@if (Model.TopRated != null)
{
<p>This listing was the one that had the highest rating.</p>
}
@if (Model.MostPurchases != null)
{
<p>This listing has the most purchases.</p>
}
@if (Model.MostReviews != null)
{
<p>This listing had the most reviews.</p>
}
@if (Model.BestPrice != null)
{
<p>Looking for the lowest price? Well here it is.</p>
}
@* TODO: Add display for top results. *@
}
else
{
<div class="text-center less-concise text-muted flex-grow-1 justify-content-center d-flex flex-column">
<h2>@ContentManager.Json.notSearched</h2>
</div>
}
</div>
</div>
<div class="multipage-slide tab-pane fade" id="results-slide" x-data>
<div class="multipage-title">
<h2><i class="bi bi-view-list"></i> Results</h2>
@if (Model.SearchResults != null)
{
<p>@ContentManager.Json.results.searched</p>
}
else
{
<p>@ContentManager.Json.results.prompt</p>
}
<hr class="less-concise">
</div>
<div class="multipage-content">
@if (Model.SearchResults != null)
{
@* TODO: Display results with UI for sorting and changing views. *@
}
else
{
<div class="text-center less-concise text-muted flex-grow-1 justify-content-center d-flex flex-column">
<h2>@ContentManager.Json.notSearched</h2>
</div>
}
</div>
</div>
<div class="multipage-slide tab-pane fade" id="info-slide">
<div class="multipage-content">
<div class="less-concise text-muted flex-grow-1 justify-content-center d-flex flex-column">
<h1 class="display-3"><i class="bi bi-info-circle"></i> Get Started!</h1>
<ol>
@foreach (string instruction in ContentManager.Json.instructions)
{
<li>@instruction</li>
}
</ol>
</div>
</div>
</div>
</div>
</div>

View File

@@ -1,9 +1,54 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Castle.Core.Internal;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
using Microsoft.EntityFrameworkCore;
using Props.Data;
using Props.Extensions;
using Props.Models.Search;
using Props.Models.User;
using Props.Services.Modules;
using Props.Shop.Framework;
namespace Props.Pages
{
public class SearchModel : PageModel
{
// TODO: Complete the search model.
[BindProperty(Name = "q", SupportsGet = true)]
public string SearchQuery { get; set; }
public IEnumerable<ProductListing> SearchResults { get; private set; }
public ProductListing BestRatingPriceRatio { get; private set; }
public ProductListing TopRated { get; private set; }
public ProductListing MostPurchases { get; private set; }
public ProductListing MostReviews { get; private set; }
public ProductListing BestPrice { get; private set; }
private ISearchManager searchManager;
private UserManager<ApplicationUser> userManager;
private IMetricsManager analytics;
public SearchModel(ISearchManager searchManager, UserManager<ApplicationUser> userManager, IMetricsManager analyticsManager)
{
this.searchManager = searchManager;
this.userManager = userManager;
this.analytics = analyticsManager;
}
public async Task OnGet()
{
if (string.IsNullOrWhiteSpace(SearchQuery)) return;
SearchOutline activeSearchOutline = User.Identity.IsAuthenticated ? (await userManager.GetUserAsync(User)).searchOutlinePreferences.ActiveSearchOutline : new SearchOutline();
this.SearchResults = searchManager.Search(SearchQuery, activeSearchOutline);
BestRatingPriceRatio = (from result in SearchResults orderby result.GetRatingToPriceRatio() descending select result).FirstOrDefault((listing) => listing.GetRatingToPriceRatio() >= 0.5f);
TopRated = (from result in SearchResults orderby result.Rating descending select result).FirstOrDefault();
MostPurchases = (from result in SearchResults orderby result.PurchaseCount descending select result).FirstOrDefault();
MostReviews = (from result in SearchResults orderby result.ReviewCount descending select result).FirstOrDefault();
BestPrice = (from result in SearchResults orderby result.UpperPrice descending select result).FirstOrDefault();
}
}
}

View File

@@ -11,6 +11,10 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>@ViewData["Title"] - Props</title>
<script src="~/js/site.js" asp-append-version="true"></script>
@if (!string.IsNullOrEmpty((ViewData["Specific"] as string)))
{
<script defer src="@($"~/js/specific/{(ViewData["Specific"])}.js")" asp-append-version="true"></script>
}
</head>
<body class="theme-light">
@@ -34,10 +38,12 @@
@if (SignInManager.IsSignedIn(User))
{
<li class="nav-item">
<nav-link class="nav-link" asp-area="Identity" asp-page="/Account/Manage/Index" title="Manage">Hello @User.Identity.Name!</nav-link>
<nav-link class="nav-link" asp-area="Identity" asp-page="/Account/Manage/Index"
title="Manage">Hello @User.Identity.Name!</nav-link>
</li>
<li class="nav-item">
<form class="form-inline" asp-area="Identity" asp-page="/Account/Logout" asp-route-returnUrl="@Url.Page("/", new { area = "" })" method="post">
<form class="form-inline" asp-area="Identity" asp-page="/Account/Logout"
asp-route-returnUrl="@Url.Page("/", new { area = "" })" method="post">
<button type="submit" class="nav-link btn btn-link">Logout</button>
</form>
</li>
@@ -45,7 +51,8 @@
else
{
<li class="nav-item">
<nav-link class="nav-link" asp-area="Identity" asp-page="/Account/Register">Register</nav-link>
<nav-link class="nav-link" asp-area="Identity" asp-page="/Account/Register">Register
</nav-link>
</li>
<li class="nav-item">
<nav-link class="nav-link" asp-area="Identity" asp-page="/Account/Login">Login</nav-link>
@@ -62,11 +69,6 @@
<footer id="footer">
&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>