feat: revamp stats, style, and add nav
This commit is contained in:
parent
be39ec07ca
commit
7378ed1445
|
@ -33,8 +33,8 @@ body {
|
|||
margin: 0;
|
||||
|
||||
font-family: sans-serif;
|
||||
background-color: var(--background);
|
||||
color: var(--text);
|
||||
/* background-color: var(--background); */
|
||||
/* color: var(--text); */
|
||||
}
|
||||
|
||||
.js body {
|
||||
|
|
|
@ -0,0 +1,13 @@
|
|||
(function () {
|
||||
function setUpLanguage() {
|
||||
let language = document.getElementById('language');
|
||||
for (let elem of language.querySelectorAll('[data-value]')) {
|
||||
elem.addEventListener('click', () => {
|
||||
document.cookie = `lang=${encodeURIComponent(elem.dataset.value)};path=/;max-age=31536000;samesite=lax`;
|
||||
window.location.reload();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
setUpLanguage();
|
||||
})();
|
File diff suppressed because one or more lines are too long
|
@ -28,11 +28,15 @@
|
|||
margin-top: 1em;
|
||||
}
|
||||
|
||||
#container > .settings {
|
||||
#container > .settings > .controls {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
#container > .settings > .controls > .search {
|
||||
margin-right: 1em;
|
||||
}
|
||||
|
||||
#listings > .no-listings {
|
||||
margin-top: 1em;
|
||||
}
|
||||
|
@ -45,11 +49,12 @@
|
|||
margin: 0 -1em;
|
||||
padding: 1em;
|
||||
|
||||
background-color: var(--row-background);
|
||||
/* background-color: var(--row-background); */
|
||||
}
|
||||
|
||||
#listings > .listing:nth-child(2n) {
|
||||
background-color: var(--row-background-alternate);
|
||||
background-color: var(--muted-border-color);
|
||||
/* background-color: var(--row-background-alternate); */
|
||||
}
|
||||
|
||||
#listings > .listing .description {
|
||||
|
|
|
@ -64,15 +64,14 @@
|
|||
let language = document.getElementById('language');
|
||||
if (state.lang === null) {
|
||||
state.lang = language.dataset.accept;
|
||||
let cookie = document.cookie
|
||||
.split(';')
|
||||
.find(row => row.trim().startsWith('lang='));
|
||||
if (cookie !== undefined) {
|
||||
state.lang = decodeURIComponent(cookie.split('=')[1]);
|
||||
}
|
||||
}
|
||||
|
||||
language.value = state.lang;
|
||||
let cookie = document.cookie
|
||||
.split(';')
|
||||
.find(row => row.trim().startsWith('lang='));
|
||||
if (cookie !== undefined) {
|
||||
state.lang = decodeURIComponent(cookie.split('=')[1]);
|
||||
}
|
||||
}
|
||||
|
||||
function setUpList() {
|
||||
|
@ -156,19 +155,9 @@
|
|||
});
|
||||
}
|
||||
|
||||
function setUpLanguage() {
|
||||
let language = document.getElementById('language');
|
||||
language.addEventListener('change', () => {
|
||||
state.lang = language.value;
|
||||
document.cookie = `lang=${encodeURIComponent(language.value)};path=/;max-age=31536000;samesite=lax`;
|
||||
window.location.reload();
|
||||
});
|
||||
}
|
||||
|
||||
addJsClass();
|
||||
saveLoadState();
|
||||
reflectState();
|
||||
setUpLanguage();
|
||||
state.list = setUpList();
|
||||
setUpDataCentreFilter();
|
||||
setUpCategoryFilter();
|
||||
|
|
File diff suppressed because one or more lines are too long
|
@ -1,5 +1,5 @@
|
|||
body {
|
||||
margin: 1em;
|
||||
margin-top: 1em;
|
||||
}
|
||||
|
||||
.total {
|
||||
|
@ -10,10 +10,15 @@ body {
|
|||
}
|
||||
|
||||
.chart {
|
||||
height: 50vh;
|
||||
max-height: 50vh;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.chart canvas {
|
||||
.chart svg {
|
||||
max-width: 100%;
|
||||
max-height: 100%;
|
||||
}
|
||||
|
@ -35,11 +40,12 @@ body {
|
|||
}
|
||||
|
||||
.chart-containers .container table tr {
|
||||
background-color: var(--row-background);
|
||||
/* background-color: var(--row-background); */
|
||||
}
|
||||
|
||||
.chart-containers .container table tr:nth-child(2n) {
|
||||
background-color: var(--row-background-alternate);
|
||||
/* background-color: var(--row-background-alternate); */
|
||||
background-color: var(--muted-border-color);
|
||||
}
|
||||
|
||||
.chart-containers .container table tr td {
|
||||
|
|
|
@ -85,42 +85,252 @@
|
|||
return newData;
|
||||
}
|
||||
|
||||
function makeChart(tableId, chartId, chartType, combine = false) {
|
||||
function extractData(tableId, extractor = null) {
|
||||
if (extractor === null) {
|
||||
extractor = (cols) => {
|
||||
return {
|
||||
label: cols[0].innerHTML,
|
||||
value: Number(cols[1].innerHTML),
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
let table = document.getElementById(tableId);
|
||||
let data = [];
|
||||
for (let row of table.querySelectorAll('tbody > tr')) {
|
||||
let cols = row.querySelectorAll('td');
|
||||
data.push({
|
||||
x: cols[0].innerText,
|
||||
y: Number(cols[1].innerText),
|
||||
});
|
||||
data.push(extractor(cols));
|
||||
}
|
||||
|
||||
if (combine) {
|
||||
data = combineTopN(data, 15);
|
||||
}
|
||||
|
||||
new Chart(
|
||||
document.getElementById(chartId),
|
||||
{
|
||||
type: chartType,
|
||||
data: {
|
||||
datasets: [{
|
||||
data: data.map(entry => entry.y),
|
||||
backgroundColor: colours,
|
||||
}],
|
||||
labels: data.map(entry => entry.x),
|
||||
},
|
||||
options: {
|
||||
borderWidth: chartType === 'doughnut' ? 0 : 2,
|
||||
...options,
|
||||
},
|
||||
},
|
||||
);
|
||||
return data;
|
||||
}
|
||||
|
||||
makeChart('duties', 'dutiesChart', 'pie', true);
|
||||
makeChart('hosts', 'hostsChart', 'doughnut');
|
||||
makeChart('hours', 'hoursChart', 'bar');
|
||||
makeChart('days', 'daysChart', 'bar');
|
||||
function wrap(text) {
|
||||
text.each(function () {
|
||||
var text = d3.select(this),
|
||||
words = text.text().split(/\s+/).reverse(),
|
||||
word,
|
||||
line = [],
|
||||
lineNumber = 0,
|
||||
lineHeight = 1.1, // ems
|
||||
x = text.attr("x"),
|
||||
y = text.attr("y"),
|
||||
dy = 0, //parseFloat(text.attr("dy")),
|
||||
tspan = text.text(null)
|
||||
.append("tspan")
|
||||
.attr("x", x)
|
||||
.attr("y", y)
|
||||
.attr("dy", dy + "em");
|
||||
while (word = words.pop()) {
|
||||
line.push(word);
|
||||
tspan.text(line.join(" "));
|
||||
if (tspan.node().getComputedTextLength() > text.attr('width') - 6) {
|
||||
line.pop();
|
||||
tspan.text(line.join(" "));
|
||||
line = [word];
|
||||
tspan = text.append("tspan")
|
||||
.attr("x", x)
|
||||
.attr("y", y)
|
||||
.attr("dy", ++lineNumber * lineHeight + dy + "em")
|
||||
.text(word);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function makeTreeMap(data, graphId, opts = {}) {
|
||||
let drawLabels = opts.drawLabels === undefined ? true : opts.drawLabels;
|
||||
let grouped = opts.grouped === undefined ? false : opts.grouped;
|
||||
|
||||
let elem = document.getElementById(graphId);
|
||||
const [width, height] = [elem.offsetWidth, elem.offsetHeight];
|
||||
|
||||
let svg = d3.select(`#${graphId}`)
|
||||
.append('svg')
|
||||
.attr('viewBox', `0 0 ${width} ${height}`);
|
||||
|
||||
if (grouped) {
|
||||
d3.treemap()
|
||||
.size([width, height])
|
||||
.paddingInner(2)
|
||||
.paddingTop(4)
|
||||
.paddingRight(4)
|
||||
.paddingLeft(4)
|
||||
.paddingBottom(4)
|
||||
(data);
|
||||
} else {
|
||||
d3.treemap()
|
||||
.size([width, height])
|
||||
.paddingInner(2)
|
||||
(data);
|
||||
}
|
||||
|
||||
let a = 0;
|
||||
let group = svg.selectAll('g')
|
||||
.data(data.leaves())
|
||||
.enter()
|
||||
.append('g');
|
||||
|
||||
let title = (d) => `${d.data.label} (${(d.data.value / d.parent.value * 100).toFixed(2)}% - ${d.data.value.toLocaleString()})`;
|
||||
|
||||
group
|
||||
.append('title')
|
||||
.text(title);
|
||||
|
||||
group
|
||||
.append('rect')
|
||||
.attr('x', d => d.x0)
|
||||
.attr('y', d => d.y0)
|
||||
.attr('width', d => d.x1 - d.x0)
|
||||
.attr('height', d => d.y1 - d.y0)
|
||||
.style('fill', d => {
|
||||
let colour = colours[a];
|
||||
a += 1;
|
||||
a %= colours.length;
|
||||
return colour;
|
||||
});
|
||||
|
||||
if (drawLabels) {
|
||||
svg.selectAll('text')
|
||||
.data(data.leaves().filter(d => d.data.value / d.parent.value >= .05 ))
|
||||
.enter()
|
||||
.append('text')
|
||||
.attr('x', d => d.x0 + 5)
|
||||
.attr('y', d => d.y0 + 20)
|
||||
.attr('width', d => d.x1 - d.x0)
|
||||
.text(title)
|
||||
.attr('font-size', '1em')
|
||||
.attr('fill', 'white')
|
||||
.call(wrap);
|
||||
}
|
||||
|
||||
if (grouped) {
|
||||
svg.selectAll('borders')
|
||||
.data(data.descendants().filter(d => d.depth === 1))
|
||||
.enter()
|
||||
.append('rect')
|
||||
.attr('x', d => d.x0)
|
||||
.attr('y', d => d.y0)
|
||||
.attr('width', d => d.x1 - d.x0)
|
||||
.attr('height', d => d.y1 - d.y0)
|
||||
.attr('fill', 'none')
|
||||
.attr('stroke', '#374956');
|
||||
|
||||
svg.selectAll('titles')
|
||||
.data(data.descendants().filter(d => d.depth === 1))
|
||||
.enter()
|
||||
.append('text')
|
||||
.attr('x', d => d.x1 - 2)
|
||||
.attr('y', d => d.y1 - 2)
|
||||
.attr('text-anchor', 'end')
|
||||
.style('transform', d => {
|
||||
if ((d.x1 - d.x0) < (d.y1 - d.y0)) {
|
||||
return 'rotate(90deg)';
|
||||
}
|
||||
|
||||
return null;
|
||||
})
|
||||
.style('transform-box', 'fill-box')
|
||||
.style('transform-origin', '95%')
|
||||
.text(d => d.data[0])
|
||||
.attr("font-size", d => {
|
||||
if (d === data) {
|
||||
return "1em";
|
||||
}
|
||||
let width = d.x1 - d.x0, height = d.y1 - d.y0;
|
||||
return Math.max(Math.min(width/5, height/2, Math.sqrt((width*width + height*height))/10), 9)
|
||||
})
|
||||
.attr('fill', 'white');
|
||||
}
|
||||
}
|
||||
|
||||
function makeBarPlot(data, graphId) {
|
||||
let elem = document.getElementById(graphId);
|
||||
const [marginLeft, marginRight, marginTop, marginBottom] = [100, 0, 16, 50];
|
||||
const [width, height] = [elem.offsetWidth - marginLeft - marginRight, elem.offsetHeight - marginTop - marginBottom];
|
||||
|
||||
let svg = d3.select(`#${graphId}`)
|
||||
.append('svg')
|
||||
.attr('viewBox', `0 0 ${width + marginLeft + marginRight} ${height + marginTop + marginBottom}`)
|
||||
.append('g')
|
||||
.attr('transform', `translate(${marginLeft}, ${marginTop})`);
|
||||
|
||||
let x = d3.scaleBand()
|
||||
.range([0, width])
|
||||
.domain(data.map(d => d.label))
|
||||
.padding(0.2);
|
||||
svg.append('g')
|
||||
.attr('transform', `translate(0, ${height})`)
|
||||
.call(d3.axisBottom(x))
|
||||
.attr('font-size', '1em')
|
||||
.selectAll('text')
|
||||
.style('text-anchor', 'middle')
|
||||
.attr('font-size', '1em');
|
||||
|
||||
let y = d3.scaleLinear()
|
||||
.domain([0, data.map(d => d.value).reduce((max, a) => Math.max(max, a))])
|
||||
.range([height, 0]);
|
||||
svg.append('g')
|
||||
.call(d3.axisLeft(y))
|
||||
.attr('font-size', '1em')
|
||||
.selectAll('text')
|
||||
.attr('font-size', '1em');
|
||||
|
||||
let sum = data.map(d => d.value).reduce((total, a) => total + a);
|
||||
let colourIdx = 0;
|
||||
let group = svg.selectAll('mybar')
|
||||
.data(data)
|
||||
.enter()
|
||||
.append('g');
|
||||
group.append('title')
|
||||
.text(d => `${d.value} (${(d.value / sum * 100).toFixed(2)}%)`);
|
||||
group.append('rect')
|
||||
.attr('x', d => x(d.label))
|
||||
.attr('y', d => y(d.value))
|
||||
.attr('width', x.bandwidth())
|
||||
.attr('height', d => height - y(d.value))
|
||||
.attr('fill', d => {
|
||||
let colour = colours[colourIdx];
|
||||
colourIdx += 1;
|
||||
colourIdx %= colours.length;
|
||||
return colour;
|
||||
});
|
||||
}
|
||||
|
||||
makeTreeMap(
|
||||
d3.hierarchy({
|
||||
children: extractData('duties'),
|
||||
}).sum(d => d.value),
|
||||
'dutiesChart',
|
||||
);
|
||||
makeTreeMap(
|
||||
d3.hierarchy(
|
||||
d3.group(
|
||||
extractData(
|
||||
'hosts',
|
||||
(cols) => {
|
||||
return {
|
||||
label: cols[1].innerHTML,
|
||||
world: cols[0].innerHTML,
|
||||
value: Number(cols[2].innerHTML),
|
||||
};
|
||||
},
|
||||
),
|
||||
d => d.world,
|
||||
)
|
||||
).sum(d => d.value),
|
||||
'hostsChart',
|
||||
{
|
||||
drawLabels: false,
|
||||
grouped: true,
|
||||
},
|
||||
);
|
||||
makeBarPlot(
|
||||
extractData('hours'),
|
||||
'hoursChart',
|
||||
);
|
||||
makeBarPlot(
|
||||
extractData('days'),
|
||||
'daysChart',
|
||||
);
|
||||
})();
|
||||
|
|
|
@ -41,6 +41,15 @@ impl Language {
|
|||
}
|
||||
}
|
||||
|
||||
pub fn name(&self) -> &'static str {
|
||||
match self {
|
||||
Self::English => "english",
|
||||
Self::Japanese => "日本語",
|
||||
Self::German => "deutsch",
|
||||
Self::French => "français",
|
||||
}
|
||||
}
|
||||
|
||||
pub fn from_codes(val: Option<&str>) -> Self {
|
||||
let val = match val {
|
||||
Some(v) => v,
|
||||
|
|
|
@ -14,27 +14,27 @@ pub struct CachedStatistics {
|
|||
#[derive(Debug, Clone, Deserialize)]
|
||||
pub struct Aliases {
|
||||
#[serde(deserialize_with = "alias_de")]
|
||||
pub aliases: HashMap<u32, Vec<Alias>>,
|
||||
pub aliases: HashMap<u32, Alias>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize)]
|
||||
pub struct Statistics {
|
||||
pub count: Vec<Count>,
|
||||
#[serde(default)]
|
||||
pub aliases: HashMap<u32, Vec<Alias>>,
|
||||
pub aliases: HashMap<u32, Alias>,
|
||||
pub duties: Vec<DutyInfo>,
|
||||
pub hosts: Vec<HostInfo>,
|
||||
pub hours: Vec<HourInfo>,
|
||||
pub days: Vec<DayInfo>,
|
||||
}
|
||||
|
||||
fn alias_de<'de, D>(de: D) -> std::result::Result<HashMap<u32, Vec<Alias>>, D::Error>
|
||||
where D: Deserializer<'de>
|
||||
fn alias_de<'de, D>(de: D) -> std::result::Result<HashMap<u32, Alias>, D::Error>
|
||||
where D: Deserializer<'de>
|
||||
{
|
||||
let aliases: Vec<AliasInfo> = Deserialize::deserialize(de)?;
|
||||
let map = aliases
|
||||
.into_iter()
|
||||
.map(|info| (info.content_id_lower, info.aliases))
|
||||
.map(|info| (info.content_id, info.alias))
|
||||
.collect();
|
||||
Ok(map)
|
||||
}
|
||||
|
@ -49,25 +49,17 @@ impl Statistics {
|
|||
}
|
||||
|
||||
pub fn player_name(&self, cid: &u32) -> Cow<str> {
|
||||
let aliases = match self.aliases.get(cid) {
|
||||
let alias = match self.aliases.get(cid) {
|
||||
Some(a) => a,
|
||||
None => return "<unknown>".into(),
|
||||
};
|
||||
|
||||
if aliases.is_empty() {
|
||||
return "<unknown>".into();
|
||||
}
|
||||
|
||||
let world = match crate::ffxiv::WORLDS.get(&aliases[0].home_world) {
|
||||
let world = match crate::ffxiv::WORLDS.get(&alias.home_world) {
|
||||
Some(world) => world.name(),
|
||||
None => "<unknown>",
|
||||
};
|
||||
|
||||
format!("{} @ {}", aliases[0].name.text(), world).into()
|
||||
}
|
||||
|
||||
pub fn num_host_listings(&self) -> usize {
|
||||
self.hosts.iter().map(|info| info.count).sum()
|
||||
format!("{} @ {}", alias.name.text(), world).into()
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -79,8 +71,8 @@ pub struct Count {
|
|||
#[derive(Debug, Clone, Deserialize)]
|
||||
pub struct AliasInfo {
|
||||
#[serde(rename = "_id")]
|
||||
pub content_id_lower: u32,
|
||||
pub aliases: Vec<Alias>,
|
||||
pub content_id: u32,
|
||||
pub alias: Alias,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize)]
|
||||
|
@ -114,7 +106,28 @@ impl DutyInfo {
|
|||
#[derive(Debug, Clone, Deserialize)]
|
||||
pub struct HostInfo {
|
||||
#[serde(rename = "_id")]
|
||||
pub content_id_lower: u32,
|
||||
pub created_world: u32,
|
||||
pub count: usize,
|
||||
pub content_ids: Vec<HostInfoInfo>,
|
||||
}
|
||||
|
||||
impl HostInfo {
|
||||
pub fn num_other(&self) -> usize {
|
||||
let top15: usize = self.content_ids.iter().map(|info| info.count).sum();
|
||||
self.count - top15
|
||||
}
|
||||
|
||||
pub fn world_name(&self) -> &'static str {
|
||||
match crate::ffxiv::WORLDS.get(&self.created_world) {
|
||||
Some(world) => world.name(),
|
||||
None => "<unknown>",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize)]
|
||||
pub struct HostInfoInfo {
|
||||
pub content_id: u32,
|
||||
pub count: usize,
|
||||
}
|
||||
|
||||
|
|
|
@ -149,6 +149,9 @@ fn assets() -> BoxedFilter<(impl Reply, )> {
|
|||
.or(listings_js())
|
||||
.or(stats_css())
|
||||
.or(stats_js())
|
||||
.or(d3())
|
||||
.or(pico())
|
||||
.or(common_js())
|
||||
)
|
||||
.boxed()
|
||||
}
|
||||
|
@ -202,6 +205,27 @@ fn stats_js() -> BoxedFilter<(impl Reply, )> {
|
|||
.boxed()
|
||||
}
|
||||
|
||||
fn d3() -> BoxedFilter<(impl Reply, )> {
|
||||
warp::path("d3.js")
|
||||
.and(warp::path::end())
|
||||
.and(warp::fs::file("./assets/d3.v7.min.js"))
|
||||
.boxed()
|
||||
}
|
||||
|
||||
fn pico() -> BoxedFilter<(impl Reply, )> {
|
||||
warp::path("pico.css")
|
||||
.and(warp::path::end())
|
||||
.and(warp::fs::file("./assets/pico.min.css"))
|
||||
.boxed()
|
||||
}
|
||||
|
||||
fn common_js() -> BoxedFilter<(impl Reply, )> {
|
||||
warp::path("common.js")
|
||||
.and(warp::path::end())
|
||||
.and(warp::fs::file("./assets/common.js"))
|
||||
.boxed()
|
||||
}
|
||||
|
||||
fn index() -> BoxedFilter<(impl Reply, )> {
|
||||
let route = warp::path::end()
|
||||
.map(|| warp::redirect(Uri::from_static("/listings")));
|
||||
|
|
|
@ -37,20 +37,42 @@ lazy_static::lazy_static! {
|
|||
"hosts": [
|
||||
{
|
||||
"$group": {
|
||||
"_id": "$listing.content_id_lower",
|
||||
"count": {
|
||||
"$sum": 1
|
||||
"_id": {
|
||||
"world": "$listing.created_world",
|
||||
"content_id": "$listing.content_id_lower",
|
||||
},
|
||||
"count": { "$sum": 1 },
|
||||
}
|
||||
},
|
||||
{
|
||||
"$sort": {
|
||||
"count": -1
|
||||
"count": -1,
|
||||
}
|
||||
},
|
||||
{
|
||||
"$limit": 15
|
||||
}
|
||||
"$group": {
|
||||
"_id": "$_id.world",
|
||||
"count": {
|
||||
"$sum": "$count",
|
||||
},
|
||||
"content_ids": {
|
||||
"$push": {
|
||||
"content_id": "$_id.content_id",
|
||||
"count": "$count",
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"$addFields": {
|
||||
"content_ids": {
|
||||
"$slice": ["$content_ids", 0, 15],
|
||||
},
|
||||
}
|
||||
},
|
||||
{
|
||||
"$sort": { "count": -1 }
|
||||
},
|
||||
],
|
||||
"hours": [
|
||||
{
|
||||
|
@ -94,11 +116,16 @@ lazy_static::lazy_static! {
|
|||
doc! {
|
||||
"$facet": {
|
||||
"aliases": [
|
||||
{
|
||||
"$sort": {
|
||||
"created_at": -1,
|
||||
}
|
||||
},
|
||||
{
|
||||
"$group": {
|
||||
"_id": "$listing.content_id_lower",
|
||||
"aliases": {
|
||||
"$addToSet": {
|
||||
"alias": {
|
||||
"$first": {
|
||||
"name": "$listing.name",
|
||||
"home_world": "$listing.home_world",
|
||||
},
|
||||
|
@ -141,7 +168,7 @@ async fn get_stats_internal(state: &State, docs: impl IntoIterator<Item = Docume
|
|||
let doc = doc.ok_or_else(|| anyhow::anyhow!("missing document"))?;
|
||||
let mut stats: Statistics = mongodb::bson::from_document(doc)?;
|
||||
|
||||
let ids: Vec<u32> = stats.hosts.iter().map(|host| host.content_id_lower).collect();
|
||||
let ids: Vec<u32> = stats.hosts.iter().flat_map(|host| host.content_ids.iter().map(|entry| entry.content_id)).collect();
|
||||
let mut aliases_query: Vec<Document> = ALIASES_QUERY.iter().cloned().collect();
|
||||
aliases_query.insert(0, doc! {
|
||||
"$match": {
|
||||
|
|
|
@ -1,11 +1,40 @@
|
|||
<!doctype html>
|
||||
<html lang="{{ lang.code() }}" class="no-js">
|
||||
<head>
|
||||
<meta charset="utf-8"/>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1"/>
|
||||
<title>{% block title %}{% endblock %}</title>
|
||||
<link rel="stylesheet" href="/assets/minireset.css"/>
|
||||
{%- block head %}{% endblock -%}
|
||||
</head>
|
||||
<body>{% block body %}{% endblock %}</body>
|
||||
<html lang="{{ lang.code() }}" class="no-js" data-theme="dark">
|
||||
<head>
|
||||
<meta charset="utf-8"/>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1"/>
|
||||
<title>{% block title %}{% endblock %}</title>
|
||||
<link rel="stylesheet" href="/assets/minireset.css"/>
|
||||
<link rel="stylesheet" href="/assets/pico.css"/>
|
||||
<script defer src="/assets/common.js"></script>
|
||||
{%- block head %}{% endblock -%}
|
||||
</head>
|
||||
<body>
|
||||
<nav class="container">
|
||||
<ul>
|
||||
<li><strong><a href="/" class="contrast">xivpf</a></strong></li>
|
||||
</ul>
|
||||
<ul>
|
||||
<li role="list" dir="rtl">
|
||||
<a href="javascript:void(0)" aria-haspopup="listbox">stats</a>
|
||||
<ul role="listbox">
|
||||
<li><a href="/stats">all time</a></li>
|
||||
<li><a href="/stats/7days">7 days</a></li>
|
||||
</ul>
|
||||
</li>
|
||||
<li role="list" dir="rtl">
|
||||
<a href="javascript:void(0)" aria-haspopup="listbox">{{ lang.name() }}</a>
|
||||
<ul role="listbox" aria-haspopup="listbox" id="language" data-accept="{{ lang.code() }}">
|
||||
<li><a href="javascript:void(0)" data-value="en">english</a></li>
|
||||
<li><a href="javascript:void(0)" data-value="ja">日本語</a></li>
|
||||
<li><a href="javascript:void(0)" data-value="de">deutsch</a></li>
|
||||
<li><a href="javascript:void(0)" data-value="fr">français</a></li>
|
||||
</ul>
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
<div class="container">
|
||||
{% block body %}{% endblock %}
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
{% extends "_frame.html" %}
|
||||
|
||||
{% block title -%}
|
||||
Remote Party Finder
|
||||
xivpf - listings
|
||||
{%- endblock %}
|
||||
|
||||
{% block head %}
|
||||
|
@ -14,37 +14,39 @@ Remote Party Finder
|
|||
{% block body %}
|
||||
<div id="container">
|
||||
<div class="requires-js settings">
|
||||
<div class="left">
|
||||
<input type="search" class="search" placeholder="Search"/>
|
||||
<div class="controls">
|
||||
<input type="search" class="search" placeholder="search"/>
|
||||
<select id="data-centre-filter">
|
||||
<option value="All">All</option>
|
||||
<optgroup label="North America">
|
||||
<option value="Aether">Aether</option>
|
||||
<option value="Crystal">Crystal</option>
|
||||
<option value="Primal">Primal</option>
|
||||
<option value="All">all</option>
|
||||
<optgroup label="north america">
|
||||
<option value="Aether">aether</option>
|
||||
<option value="Crystal">crystal</option>
|
||||
<option value="Primal">primal</option>
|
||||
</optgroup>
|
||||
<optgroup label="Europe">
|
||||
<option value="Chaos">Chaos</option>
|
||||
<option value="Light">Light</option>
|
||||
<optgroup label="europe">
|
||||
<option value="Chaos">chaos</option>
|
||||
<option value="Light">light</option>
|
||||
</optgroup>
|
||||
<optgroup label="Japan">
|
||||
<option value="Elemental">Elemental</option>
|
||||
<option value="Gaia">Gaia</option>
|
||||
<option value="Mana">Mana</option>
|
||||
<optgroup label="japan">
|
||||
<option value="Elemental">elemental</option>
|
||||
<option value="Gaia">gaia</option>
|
||||
<option value="Mana">mana</option>
|
||||
</optgroup>
|
||||
<optgroup label="Oceania">
|
||||
<option value="Materia">Materia</option>
|
||||
<optgroup label="oceania">
|
||||
<option value="Materia">materia</option>
|
||||
</optgroup>
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<details class="filter-controls">
|
||||
<summary>Advanced</summary>
|
||||
<summary>advanced</summary>
|
||||
<div>
|
||||
<div class="control">
|
||||
<label>
|
||||
Categories
|
||||
<select multiple id="category-filter">
|
||||
{%- for category in PartyFinderCategory::ALL %}
|
||||
<option value="{{ category.as_str() }}">{{ category.name().text(lang) }}</option>
|
||||
<option value="{{ category.as_str() }}">{{ category.name().text(lang) }}</option>
|
||||
{%- endfor %}
|
||||
</select>
|
||||
</label>
|
||||
|
@ -52,14 +54,6 @@ Remote Party Finder
|
|||
</div>
|
||||
</details>
|
||||
</div>
|
||||
<div class="right">
|
||||
<select id="language" data-accept="{{ lang.code() }}">
|
||||
<option value="en">English</option>
|
||||
<option value="ja">日本語</option>
|
||||
<option value="de">Deutsch</option>
|
||||
<option value="fr">Français</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div id="listings" class="list">
|
||||
{%- if containers.is_empty() %}
|
||||
|
@ -68,10 +62,10 @@ Remote Party Finder
|
|||
{%- for container in containers %}
|
||||
{%- let listing = container.listing.borrow() %}
|
||||
<div
|
||||
class="listing"
|
||||
data-id="{{ listing.id }}"
|
||||
data-centre="{{ listing.data_centre_name().unwrap_or_default() }}"
|
||||
data-pf-category="{{ listing.html_pf_category() }}">
|
||||
class="listing"
|
||||
data-id="{{ listing.id }}"
|
||||
data-centre="{{ listing.data_centre_name().unwrap_or_default() }}"
|
||||
data-pf-category="{{ listing.html_pf_category() }}">
|
||||
<div class="left">
|
||||
{%- let duty_class %}
|
||||
{%- if listing.is_cross_world() %}
|
||||
|
|
|
@ -1,13 +1,13 @@
|
|||
{% extends "_frame.html" %}
|
||||
|
||||
{% block title -%}
|
||||
Remote Party Finder
|
||||
xivpf - stats
|
||||
{%- endblock %}
|
||||
|
||||
{% block head %}
|
||||
<link rel="stylesheet" href="/assets/common.css"/>
|
||||
<link rel="stylesheet" href="/assets/stats.css"/>
|
||||
<script src="https://cdn.jsdelivr.net/npm/chart.js@3.5.1/dist/chart.min.js"></script>
|
||||
<script defer src="/assets/d3.js"></script>
|
||||
<script defer src="/assets/stats.js"></script>
|
||||
{% endblock %}
|
||||
|
||||
|
@ -19,8 +19,7 @@ Remote Party Finder
|
|||
<div class="chart-containers">
|
||||
<div class="container">
|
||||
<h1>Top categories</h1>
|
||||
<div class="chart">
|
||||
<canvas id="dutiesChart"></canvas>
|
||||
<div id="dutiesChart" class="chart">
|
||||
</div>
|
||||
<details>
|
||||
<summary>Details</summary>
|
||||
|
@ -45,29 +44,33 @@ Remote Party Finder
|
|||
|
||||
<div class="container">
|
||||
<h1>Top hosts</h1>
|
||||
<div class="chart">
|
||||
<canvas id="hostsChart"></canvas>
|
||||
<div id="hostsChart" class="chart">
|
||||
</div>
|
||||
<details>
|
||||
<summary>Details</summary>
|
||||
<table id="hosts">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Name</th>
|
||||
<th>Count</th>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>World (created)</th>
|
||||
<th>Name</th>
|
||||
<th>Count</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{%- for info in stats.hosts %}
|
||||
<tr>
|
||||
<td>{{ stats.player_name(info.content_id_lower) }}</td>
|
||||
<td>{{ info.count }}</td>
|
||||
</tr>
|
||||
{%- endfor %}
|
||||
<tr>
|
||||
<td>Other</td>
|
||||
<td>{{ stats.num_listings() - stats.num_host_listings() }}</td>
|
||||
</tr>
|
||||
{%- for info in stats.hosts %}
|
||||
{%- for entry in info.content_ids %}
|
||||
<tr>
|
||||
<td>{{ info.world_name() }}</td>
|
||||
<td>{{ stats.player_name(entry.content_id) }}</td>
|
||||
<td>{{ entry.count }}</td>
|
||||
</tr>
|
||||
{%- endfor %}
|
||||
<tr>
|
||||
<td>{{ info.world_name() }}</td>
|
||||
<td>Other</td>
|
||||
<td>{{ info.num_other() }}
|
||||
</tr>
|
||||
{%- endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</details>
|
||||
|
@ -75,8 +78,7 @@ Remote Party Finder
|
|||
|
||||
<div class="container">
|
||||
<h1>Top hours (UTC)</h1>
|
||||
<div class="chart">
|
||||
<canvas id="hoursChart"></canvas>
|
||||
<div id="hoursChart" class="chart">
|
||||
</div>
|
||||
<details>
|
||||
<summary>Details</summary>
|
||||
|
@ -101,8 +103,7 @@ Remote Party Finder
|
|||
|
||||
<div class="container">
|
||||
<h1>Top days (UTC)</h1>
|
||||
<div class="chart">
|
||||
<canvas id="daysChart"></canvas>
|
||||
<div id="daysChart" class="chart">
|
||||
</div>
|
||||
<details>
|
||||
<summary>Details</summary>
|
||||
|
|
Loading…
Reference in New Issue