feat: add basic search and filtering

This commit is contained in:
Anna 2021-10-04 15:14:40 -04:00
parent 7225f40e68
commit c0e12531a2
5 changed files with 240 additions and 139 deletions

View File

@ -35,51 +35,54 @@ body {
color: var(--text); color: var(--text);
} }
body > .listing { #listings > .listing {
display: flex; display: grid;
flex-flow: row nowrap; grid-template-columns: 3fr auto 2fr;
justify-content: space-between; gap: 1em;
padding: 1em; padding: 1em;
margin-bottom: 1em;
background-color: var(--row-background); background-color: var(--row-background);
} }
body > .listing .left { #listings > .listing:nth-child(2n) {
flex: 2 0 0;
}
body > .listing .middle {
flex: 1 0 0 ;
}
body > .listing:nth-child(2n) {
background-color: var(--row-background-alternate); background-color: var(--row-background-alternate);
} }
body > .listing .description { #listings > .listing .description {
white-space: pre-wrap; white-space: pre-wrap;
word-break: break-word; word-break: break-word;
} }
body > .listing .stat { #listings > .listing .description .desc-green {
color: var(--green-text);
}
#listings > .listing .description .desc-blue {
color: var(--light-blue-text);
}
#listings > .listing .description .desc-yellow {
color: var(--gold-text);
}
#listings > .listing .stat {
color: var(--meta-text); color: var(--meta-text);
} }
body > .listing .duty { #listings > .listing .duty {
font-size: 1.2em; font-size: 1.2em;
} }
body > .listing .duty.cross { #listings > .listing .duty.cross {
color: var(--cross-duty-text); color: var(--cross-duty-text);
} }
body > .listing .duty.local { #listings > .listing .duty.local {
color: var(--local-duty-text); color: var(--local-duty-text);
} }
body > .listing .meta { #listings > .listing .meta {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
@ -89,13 +92,13 @@ body > .listing .meta {
text-align: right; text-align: right;
} }
body > .listing .meta > .item { #listings > .listing .meta > .item {
display: flex; display: flex;
flex-flow: row nowrap; flex-flow: row nowrap;
align-self: flex-end; align-self: flex-end;
} }
body > .listing .meta > .item .icon { #listings > .listing .meta > .item .icon {
height: 1em; height: 1em;
width: 1em; width: 1em;
fill: var(--text); fill: var(--text);
@ -104,7 +107,7 @@ body > .listing .meta > .item .icon {
justify-self: center; justify-self: center;
} }
body > .listing .party { #listings > .listing .party {
margin-top: .5em; margin-top: .5em;
display: grid; display: grid;
@ -112,19 +115,19 @@ body > .listing .party {
gap: .5em; gap: .5em;
} }
body > .listing .party > .total { #listings > .listing .party > .total {
align-self: center; align-self: center;
justify-self: center; justify-self: center;
} }
body > .listing .party > .slot { #listings > .listing .party > .slot {
width: 2em; width: 2em;
height: 2em; height: 2em;
border: 1px solid currentColor; border: 1px solid currentColor;
margin-right: .5em; margin-right: .5em;
} }
body > .listing .party > .slot:not(.filled).dps.tank { #listings > .listing .party > .slot:not(.filled).dps.tank {
background: linear-gradient( background: linear-gradient(
to bottom, to bottom,
var(--tank-blue) 0%, var(--tank-blue) 0%,
@ -133,7 +136,7 @@ body > .listing .party > .slot:not(.filled).dps.tank {
); );
} }
body > .listing .party > .slot:not(.filled).dps.healer { #listings > .listing .party > .slot:not(.filled).dps.healer {
background: linear-gradient( background: linear-gradient(
to bottom, to bottom,
var(--healer-green) 0%, var(--healer-green) 0%,
@ -142,7 +145,7 @@ body > .listing .party > .slot:not(.filled).dps.healer {
); );
} }
body > .listing .party > .slot:not(.filled).tank.healer { #listings > .listing .party > .slot:not(.filled).tank.healer {
background: linear-gradient( background: linear-gradient(
to bottom, to bottom,
var(--tank-blue) 0%, var(--tank-blue) 0%,
@ -151,7 +154,7 @@ body > .listing .party > .slot:not(.filled).tank.healer {
); );
} }
body > .listing .party > .slot:not(.filled).tank.healer.dps { #listings > .listing .party > .slot:not(.filled).tank.healer.dps {
background: linear-gradient( background: linear-gradient(
to bottom, to bottom,
var(--tank-blue) 0%, var(--tank-blue) 0%,
@ -162,70 +165,65 @@ body > .listing .party > .slot:not(.filled).tank.healer.dps {
); );
} }
body > .listing .party > .slot:not(.filled).dps { #listings > .listing .party > .slot:not(.filled).dps {
background-color: var(--dps-red); background-color: var(--dps-red);
} }
body > .listing .party > .slot.filled { #listings > .listing .party > .slot.filled {
background-color: var(--slot-background); background-color: var(--slot-background);
border-color: var(--icon-gold); border-color: var(--icon-gold);
} }
body > .listing .party > .slot.empty { #listings > .listing .party > .slot.empty {
background-color: var(--slot-empty); background-color: var(--slot-empty);
} }
body > .listing .party > .slot.dps { #listings > .listing .party > .slot.dps {
background-color: var(--dps-red); background-color: var(--dps-red);
} }
body > .listing .party > .slot.healer { #listings > .listing .party > .slot.healer {
background-color: var(--healer-green); background-color: var(--healer-green);
} }
body > .listing .party > .slot.tank { #listings > .listing .party > .slot.tank {
background-color: var(--tank-blue); background-color: var(--tank-blue);
} }
body > .listing .party > .slot > svg { #listings > .listing .party > .slot > svg {
width: 100%; width: 100%;
height: 100%; height: 100%;
fill: var(--icon-gold); fill: var(--icon-gold);
} }
body > .listing .party > .slot.filled:not(.dps):not(.healer):not(.tank) > svg { #listings > .listing .party > .slot.filled:not(.dps):not(.healer):not(.tank) > svg {
fill: #C6C6C6; fill: #C6C6C6;
} }
/* Really, this could be 26em */ @media (max-width: 50em) {
@media (max-width: 30em) { #listings > .listing {
body > .listing { grid-template-columns: repeat(auto-fit, 100%);
flex-flow: column nowrap;
} }
body > .listing > :not(:first-child) { #listings > .listing > :not(:first-child) {
margin-top: .5em; margin: 0;
margin-left: 0;
} }
body > .listing .meta { #listings > .listing .meta {
flex-grow: 1;
margin-left: 0;
text-align: unset; text-align: unset;
} }
body > .listing .meta > .item { #listings > .listing .meta > .item {
align-self: unset; align-self: unset;
} }
body > .listing .meta > .item .icon { #listings > .listing .meta > .item .icon {
order: 1; order: 1;
margin-left: 0; margin-left: 0;
margin-right: .5em; margin-right: .5em;
} }
body > .listing .meta > .item > .text { #listings > .listing .meta > .item .text {
order: 2; order: 2;
} }
} }

41
assets/listings.js Normal file
View File

@ -0,0 +1,41 @@
(function () {
let select = document.getElementById('data-centre-filter');
let data_centres = [];
for (let elem of document.querySelectorAll('#listings > .listing')) {
let centre = elem.dataset['centre'];
if (data_centres.indexOf(centre) === -1) {
data_centres.push(centre);
let opt = document.createElement('option');
opt.innerText = centre;
select.appendChild(opt);
}
}
let options = {
valueNames: [
'duty',
'creator',
'description',
{data: ['centre']},
],
};
let list = new List('container', options);
select.addEventListener('change', () => {
let centre = select.value;
if (centre === 'All') {
list.filter();
return;
}
console.log(`looking for ${centre}`);
list.filter(item => {
console.log(item.values().centre === centre)
return item.values().centre === centre;
// console.log(item.elm.dataset['centre']);
// return item.elm.dataset['centre'] === centre;
});
});
})();

View File

@ -109,6 +109,45 @@ impl PartyFinderListing {
.map(|world| Cow::from(world.name())) .map(|world| Cow::from(world.name()))
.unwrap_or_else(|| Cow::from(self.home_world.to_string())) .unwrap_or_else(|| Cow::from(self.home_world.to_string()))
} }
pub fn prepend_flags(&self) -> (&'static str, String) {
let mut colour_class = "";
let mut flags = Vec::new();
if self.objective.contains(ObjectiveFlags::PRACTICE) {
flags.push("[Practice]");
colour_class = "desc-green";
}
if self.objective.contains(ObjectiveFlags::DUTY_COMPLETION) {
flags.push("[Duty Completion]");
colour_class = "desc-blue";
}
if self.objective.contains(ObjectiveFlags::LOOT) {
flags.push("[Loot]");
colour_class = "desc-yellow";
}
if self.conditions.contains(ConditionFlags::DUTY_COMPLETE) {
flags.push("[Duty Complete]");
}
if self.conditions.contains(ConditionFlags::DUTY_INCOMPLETE) {
flags.push("[Duty Incomplete]");
}
if self.search_area.contains(SearchAreaFlags::ONE_PLAYER_PER_JOB) {
flags.push("[One Player per Job]");
}
(colour_class, flags.join(""))
}
pub fn data_centre_name(&self) -> Option<&'static str> {
crate::ffxiv::WORLDS.get(&u32::from(self.created_world))
.map(|w| w.data_center().name())
}
} }
#[derive(Debug, Deserialize, Serialize, PartialEq)] #[derive(Debug, Deserialize, Serialize, PartialEq)]

View File

@ -79,6 +79,7 @@ fn assets() -> BoxedFilter<(impl Reply, )> {
icons() icons()
.or(minireset()) .or(minireset())
.or(listings_css()) .or(listings_css())
.or(listings_js())
) )
.boxed() .boxed()
} }
@ -104,6 +105,13 @@ fn listings_css() -> BoxedFilter<(impl Reply, )> {
.boxed() .boxed()
} }
fn listings_js() -> BoxedFilter<(impl Reply, )> {
warp::path("listings.js")
.and(warp::path::end())
.and(warp::fs::file("./assets/listings.js"))
.boxed()
}
fn index() -> BoxedFilter<(impl Reply, )> { fn index() -> BoxedFilter<(impl Reply, )> {
let route = warp::path::end() let route = warp::path::end()
.map(|| warp::redirect(Uri::from_static("/listings"))); .map(|| warp::redirect(Uri::from_static("/listings")));

View File

@ -6,99 +6,114 @@ Remote Party Finder
{% block head %} {% block head %}
<link rel="stylesheet" href="/assets/listings.css"/> <link rel="stylesheet" href="/assets/listings.css"/>
<script defer src="https://cdnjs.cloudflare.com/ajax/libs/list.js/2.3.1/list.min.js"></script>
<script defer src="/assets/listings.js"></script>
{% endblock %} {% endblock %}
{% block body %} {% block body %}
{% for container in containers %} <div id="container">
{% let listing = container.listing.borrow() %} <input type="search" class="search" placeholder="Search"/>
<div class="listing" data-id="{{ listing.id }}" data-updated-minutes="{{ container.since_updated().num_minutes() }}"> <select id="data-centre-filter">
<div class="left"> <option>All</option>
{% let duty_class %} </select>
{% if listing.is_cross_world() %} <div id="listings" class="list">
{% let duty_class = " cross" %} {% for container in containers %}
{% else %} {% let listing = container.listing.borrow() %}
{% let duty_class = " local" %} <div
{% endif %} class="listing"
<div class="duty{{ duty_class }}">{{ listing.duty_name() }}</div> data-id="{{ listing.id }}"
<div class="description"> data-centre="{{ listing.data_centre_name().unwrap_or_default() }}">
{%- let desc = listing.description.full_text() %} <div class="left">
{%- if desc.is_empty() -%} {% let duty_class %}
<em>None</em> {% if listing.is_cross_world() %}
{%- else -%} {% let duty_class = " cross" %}
{{ desc }} {% else %}
{%- endif -%} {% let duty_class = " local" %}
</div> {% endif %}
<div class="party"> <div class="duty{{ duty_class }}">{{ listing.duty_name() }}</div>
{% for slot in listing.slots() %} <div class="description">
{% let filled %} {%- let desc = listing.description.full_text() %}
{% let title %} {%- if desc.trim().is_empty() -%}
{% let role_class %} <em>None</em>
{% match slot %} {%- else -%}
{% when Ok with (slot) %} {%- let (colour_class, prepend_flags) = listing.prepend_flags() -%}
{% let filled = " filled" %} <span class="{{ colour_class }}">{{ prepend_flags }} </span>
{% match slot.role() %} {{- desc.trim() }}
{% when Some with (role) %} {%- endif -%}
{% let role_class = " {}"|format(role.as_str().to_lowercase()) %}
{% when None %}
{% let role_class = "".to_string() %}
{% endmatch %}
{% let title = slot.code().to_string() %}
{% when Err with (tuple) %}
{% let filled = "" %}
{% let title = tuple.1.clone() %}
{% let role_class = " {}"|format(tuple.0) %}
{% endmatch %}
<div class="slot{{ filled }}{{ role_class }}" title="{{ title }}">
{% if !filled.is_empty() %}
<svg viewBox="0 0 32 32">
<use href="/assets/icons.svg#{{ title }}"></use>
</svg>
{% endif %}
</div> </div>
{% endfor %} <div class="party">
<div class="total">{{ listing.slots_filled() }}/{{ listing.slots_available }}</div> {% for slot in listing.slots() %}
</div> {% let filled %}
</div> {% let title %}
<div class="middle"> {% let role_class %}
<div class="stat"> {% match slot %}
<div class="name">Min IL</div> {% when Ok with (slot) %}
<div class="value">{{ listing.min_item_level }}</div> {% let filled = " filled" %}
</div> {% match slot.role() %}
</div> {% when Some with (role) %}
<div class="right meta"> {% let role_class = " {}"|format(role.as_str().to_lowercase()) %}
<div class="item creator"> {% when None %}
<span class="text">{{ listing.name.full_text() }} @ {{ listing.home_world_string() }}</span> {% let role_class = "".to_string() %}
<span title="Creator"> {% endmatch %}
<svg class="icon" viewBox="0 0 32 32"> {% let title = slot.code().to_string() %}
<use href="/assets/icons.svg#user"></use> {% when Err with (tuple) %}
</svg> {% let filled = "" %}
</span> {% let title = tuple.1.clone() %}
</div> {% let role_class = " {}"|format(tuple.0) %}
<div class="item world"> {% endmatch %}
<span class="text">{{ listing.created_world_string() }}</span> <div class="slot{{ filled }}{{ role_class }}" title="{{ title }}">
<span title="Created on"> {% if !filled.is_empty() %}
<svg class="icon" viewBox="0 0 32 32"> <svg viewBox="0 0 32 32">
<use href="/assets/icons.svg#sphere"></use> <use href="/assets/icons.svg#{{ title }}"></use>
</svg> </svg>
</span> {% endif %}
</div> </div>
<div class="item expires"> {% endfor %}
<span class="text">{{ container.human_time_left() }}</span> <div class="total">{{ listing.slots_filled() }}/{{ listing.slots_available }}</div>
<span title="Expires"> </div>
<svg class="icon" viewBox="0 0 32 32"> </div>
<use href="/assets/icons.svg#stopwatch"></use> <div class="middle">
</svg> <div class="stat">
</span> <div class="name">Min IL</div>
</div> <div class="value">{{ listing.min_item_level }}</div>
<div class="item updated"> </div>
<span class="text">{{ container.human_since_updated() }}</span> </div>
<span title="Updated"> <div class="right meta">
<svg class="icon" viewBox="0 0 32 32"> <div class="item creator">
<use href="/assets/icons.svg#clock"></use> <span class="text">{{ listing.name.full_text() }} @ {{ listing.home_world_string() }}</span>
</svg> <span title="Creator">
</span> <svg class="icon" viewBox="0 0 32 32">
<use href="/assets/icons.svg#user"></use>
</svg>
</span>
</div>
<div class="item world">
<span class="text">{{ listing.created_world_string() }}</span>
<span title="Created on">
<svg class="icon" viewBox="0 0 32 32">
<use href="/assets/icons.svg#sphere"></use>
</svg>
</span>
</div>
<div class="item expires">
<span class="text">{{ container.human_time_left() }}</span>
<span title="Expires">
<svg class="icon" viewBox="0 0 32 32">
<use href="/assets/icons.svg#stopwatch"></use>
</svg>
</span>
</div>
<div class="item updated">
<span class="text">{{ container.human_since_updated() }}</span>
<span title="Updated">
<svg class="icon" viewBox="0 0 32 32">
<use href="/assets/icons.svg#clock"></use>
</svg>
</span>
</div>
</div>
</div> </div>
{% endfor %}
</div> </div>
</div> </div>
{% endfor %}
{% endblock %} {% endblock %}