Skip to content

Commit 1647a1e

Browse files
authored
Merge pull request #24 from CurseForgeCommunity/feat-searchengine
Feat searchengine
2 parents d7ba4b5 + e5b1a73 commit 1647a1e

File tree

4 files changed

+699
-1
lines changed

4 files changed

+699
-1
lines changed
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
using CurseForge.APIClient.Models.Mods;
2+
3+
namespace CFLookup.Models
4+
{
5+
public class ProjectSearchCriteria
6+
{
7+
public string? Query { get; set; }
8+
9+
public List<ProjectSearchFilter> Filters { get; set; } = new();
10+
11+
public int Page { get; set; } = 1;
12+
13+
public int PageSize { get; set; } = 50;
14+
}
15+
16+
public class ProjectSearchFilter
17+
{
18+
public string? Field { get; set; }
19+
20+
public string? Operator { get; set; }
21+
22+
public string? Value { get; set; }
23+
}
24+
25+
public class ProjectSearchResult
26+
{
27+
public long ProjectId { get; set; }
28+
29+
public int GameId { get; set; }
30+
31+
public string Name { get; set; } = string.Empty;
32+
33+
public string Slug { get; set; } = string.Empty;
34+
35+
public string Summary { get; set; } = string.Empty;
36+
37+
public ModStatus Status { get; set; }
38+
39+
public long DownloadCount { get; set; }
40+
41+
public bool IsFeatured { get; set; }
42+
43+
public int PrimaryCategoryId { get; set; }
44+
45+
public int ClassId { get; set; }
46+
47+
public bool AllowModDistribution { get; set; }
48+
49+
public long GamePopularityRank { get; set; }
50+
51+
public bool IsAvailable { get; set; }
52+
53+
public long ThumbsUpCount { get; set; }
54+
55+
public DateTimeOffset DateCreated { get; set; }
56+
57+
public DateTimeOffset DateModified { get; set; }
58+
59+
public DateTimeOffset DateReleased { get; set; }
60+
61+
public DateTimeOffset LatestUpdate { get; set; }
62+
}
63+
64+
public class ProjectSearchPageViewModel
65+
{
66+
public ProjectSearchCriteria Criteria { get; set; } = new();
67+
68+
public List<ProjectSearchResult> Results { get; set; } = new();
69+
70+
public bool HasNextPage { get; set; }
71+
72+
public bool HasPreviousPage => Criteria.Page > 1;
73+
}
74+
}
75+
Lines changed: 257 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,257 @@
1+
@page "/project-search"
2+
@model CFLookup.Pages.ProjectSearchModel
3+
@using CFLookup.Models
4+
@{
5+
ViewData["Title"] = "Project DB Search";
6+
}
7+
8+
<h1 class="mb-4">Project database search</h1>
9+
10+
<p class="text-muted">
11+
Use this page to search the cached project data stored in PostgreSQL.
12+
</p>
13+
14+
<form method="get" class="mb-4">
15+
<div class="card bg-secondary border-0 mb-3">
16+
<div class="card-body">
17+
<div class="mb-3">
18+
<label asp-for="Criteria.Query" class="form-label">Search text</label>
19+
<input asp-for="Criteria.Query" class="form-control" placeholder="Search name, slug, or summary" />
20+
<span class="form-text text-muted">
21+
Searches project <strong>name</strong>, <strong>slug</strong>, and <strong>summary</strong>.
22+
</span>
23+
</div>
24+
25+
<hr />
26+
27+
<h2 class="h5 mb-3">Advanced filters</h2>
28+
29+
@{
30+
var filters = Model.Criteria.Filters ?? new List<ProjectSearchFilter>();
31+
if (filters.Count == 0)
32+
{
33+
filters.Add(new ProjectSearchFilter());
34+
}
35+
}
36+
37+
<div id="filter-rows">
38+
@for (var i = 0; i < filters.Count; i++)
39+
{
40+
<div class="row g-2 mb-2 filter-row">
41+
<div class="col-md-4">
42+
<select class="form-select" asp-for="Criteria.Filters[@i].Field">
43+
<option value="">-- Field --</option>
44+
<option value="GameId">Game id</option>
45+
<option value="Status">Status (enum value)</option>
46+
<option value="DownloadCount">Download count</option>
47+
<option value="ThumbsUpCount">Thumbs up</option>
48+
<option value="Rating">Rating</option>
49+
<option value="GamePopularityRank">Game popularity rank</option>
50+
<option value="ClassId">Class id</option>
51+
<option value="PrimaryCategoryId">Primary category id</option>
52+
<option value="IsFeatured">Is featured</option>
53+
<option value="IsAvailable">Is available</option>
54+
<option value="AllowModDistribution">Allow mod distribution</option>
55+
<option value="DateCreated">Date created</option>
56+
<option value="DateModified">Date modified</option>
57+
<option value="DateReleased">Date released</option>
58+
<option value="LatestUpdate">Latest update</option>
59+
<option value="Name">Name</option>
60+
<option value="Slug">Slug</option>
61+
</select>
62+
</div>
63+
<div class="col-md-3">
64+
<select class="form-select" asp-for="Criteria.Filters[@i].Operator">
65+
<option value="">-- Operator --</option>
66+
<option value="eq">Equals</option>
67+
<option value="neq">Not equal</option>
68+
<option value="gte">&gt;=</option>
69+
<option value="lte">&lt;=</option>
70+
<option value="gt">&gt;</option>
71+
<option value="lt">&lt;</option>
72+
<option value="contains">Contains</option>
73+
</select>
74+
</div>
75+
<div class="col-md-4">
76+
<input class="form-control" name="Criteria.Filters[@i].Value" value="@filters[i].Value" />
77+
</div>
78+
<div class="col-md-1 d-flex align-items-center">
79+
<button type="button" class="btn btn-outline-danger btn-sm remove-filter" title="Remove filter">&times;</button>
80+
</div>
81+
</div>
82+
}
83+
</div>
84+
85+
<div class="mt-2">
86+
<button type="button" id="add-filter-btn" class="btn btn-outline-light btn-sm">
87+
Add filter
88+
</button>
89+
</div>
90+
</div>
91+
</div>
92+
93+
<div class="d-flex justify-content-between align-items-center">
94+
<button type="submit" class="btn btn-primary">
95+
Search
96+
</button>
97+
98+
<div>
99+
@if (Model.ViewModel.HasPreviousPage)
100+
{
101+
<button type="submit"
102+
name="Criteria.Page"
103+
value="@(Model.Criteria.Page - 1)"
104+
class="btn btn-outline-light me-2">
105+
&laquo; Previous
106+
</button>
107+
}
108+
@if (Model.ViewModel.HasNextPage)
109+
{
110+
<button type="submit"
111+
name="Criteria.Page"
112+
value="@(Model.Criteria.Page + 1)"
113+
class="btn btn-outline-light">
114+
Next &raquo;
115+
</button>
116+
}
117+
</div>
118+
</div>
119+
</form>
120+
121+
@if (Model.ViewModel.Results != null && Model.ViewModel.Results.Count > 0)
122+
{
123+
<h2 class="h5 mt-4">
124+
Results
125+
<span class="text-muted">
126+
(@Model.ViewModel.Results.Count@(Model.ViewModel.HasNextPage ? "+" : ""))
127+
</span>
128+
</h2>
129+
130+
<div class="table-responsive mt-2">
131+
<table class="table table-dark table-striped table-sm align-middle">
132+
<thead>
133+
<tr>
134+
<th scope="col">Project ID</th>
135+
<th scope="col">Name</th>
136+
<th scope="col">Slug</th>
137+
<th scope="col">Game ID</th>
138+
<th scope="col">Status</th>
139+
<th scope="col">Downloads</th>
140+
<th scope="col">Featured</th>
141+
<th scope="col">Available</th>
142+
</tr>
143+
</thead>
144+
<tbody>
145+
@foreach (var row in Model.ViewModel.Results)
146+
{
147+
<tr>
148+
<td>
149+
<a asp-page="/Index"
150+
asp-route-projectId="@row.ProjectId"
151+
>
152+
@row.ProjectId
153+
</a>
154+
</td>
155+
<td>@row.Name</td>
156+
<td>@row.Slug</td>
157+
<td>@row.GameId</td>
158+
<td>@row.Status</td>
159+
<td>@row.DownloadCount</td>
160+
<td>@(row.IsFeatured ? "Yes" : "No")</td>
161+
<td>@(row.IsAvailable ? "Yes" : "No")</td>
162+
</tr>
163+
}
164+
</tbody>
165+
</table>
166+
</div>
167+
}
168+
else
169+
{
170+
<p class="mt-4 text-muted">
171+
No results yet. Enter a search query and optional filters, then run a search.
172+
</p>
173+
}
174+
175+
<template id="filter-row-template">
176+
<div class="row g-2 mb-2 filter-row">
177+
<div class="col-md-4">
178+
<select class="form-select" name="Criteria.Filters[__index__].Field">
179+
<option value="">-- Field --</option>
180+
<option value="GameId">Game id</option>
181+
<option value="Status">Status (enum value)</option>
182+
<option value="DownloadCount">Download count</option>
183+
<option value="ThumbsUpCount">Thumbs up</option>
184+
<option value="Rating">Rating</option>
185+
<option value="GamePopularityRank">Game popularity rank</option>
186+
<option value="ClassId">Class id</option>
187+
<option value="PrimaryCategoryId">Primary category id</option>
188+
<option value="IsFeatured">Is featured</option>
189+
<option value="IsAvailable">Is available</option>
190+
<option value="AllowModDistribution">Allow mod distribution</option>
191+
<option value="DateCreated">Date created</option>
192+
<option value="DateModified">Date modified</option>
193+
<option value="DateReleased">Date released</option>
194+
<option value="LatestUpdate">Latest update</option>
195+
<option value="Name">Name</option>
196+
<option value="Slug">Slug</option>
197+
</select>
198+
</div>
199+
<div class="col-md-3">
200+
<select class="form-select" name="Criteria.Filters[__index__].Operator">
201+
<option value="">-- Operator --</option>
202+
<option value="eq">Equals</option>
203+
<option value="neq">Not equal</option>
204+
<option value="gte">&gt;=</option>
205+
<option value="lte">&lt;=</option>
206+
<option value="gt">&gt;</option>
207+
<option value="lt">&lt;</option>
208+
<option value="contains">Contains</option>
209+
</select>
210+
</div>
211+
<div class="col-md-4">
212+
<input class="form-control" name="Criteria.Filters[__index__].Value" />
213+
</div>
214+
<div class="col-md-1 d-flex align-items-center">
215+
<button type="button" class="btn btn-outline-danger btn-sm remove-filter" title="Remove filter">&times;</button>
216+
</div>
217+
</div>
218+
</template>
219+
220+
@section Scripts {
221+
<script>
222+
(function () {
223+
const container = document.getElementById('filter-rows');
224+
const template = document.getElementById('filter-row-template');
225+
const addButton = document.getElementById('add-filter-btn');
226+
227+
if (!container || !template || !addButton) {
228+
return;
229+
}
230+
231+
function getNextIndex() {
232+
const rows = container.querySelectorAll('.filter-row');
233+
return rows.length;
234+
}
235+
236+
addButton.addEventListener('click', function () {
237+
const index = getNextIndex();
238+
const html = template.innerHTML.replace(/__index__/g, index);
239+
const wrapper = document.createElement('div');
240+
wrapper.innerHTML = html;
241+
const row = wrapper.firstElementChild;
242+
container.appendChild(row);
243+
});
244+
245+
container.addEventListener('click', function (e) {
246+
const target = e.target;
247+
if (target && target.classList.contains('remove-filter')) {
248+
const row = target.closest('.filter-row');
249+
if (row) {
250+
row.remove();
251+
}
252+
}
253+
});
254+
})();
255+
</script>
256+
}
257+

0 commit comments

Comments
 (0)