mirror of
https://codeberg.org/demostf/frontend.git
synced 2026-06-03 18:24:12 +02:00
demo filtering
This commit is contained in:
parent
cb56c80555
commit
57d034159b
14 changed files with 402 additions and 35 deletions
49
Cargo.lock
generated
49
Cargo.lock
generated
|
|
@ -202,6 +202,7 @@ checksum = "3b32c5ea3aabaf4deb5f5ced2d688ec0844c881c9e6c696a8b769a05fc691e62"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"async-trait",
|
"async-trait",
|
||||||
"axum-core",
|
"axum-core",
|
||||||
|
"axum-macros",
|
||||||
"bitflags",
|
"bitflags",
|
||||||
"bytes",
|
"bytes",
|
||||||
"futures-util",
|
"futures-util",
|
||||||
|
|
@ -244,6 +245,18 @@ dependencies = [
|
||||||
"tower-service",
|
"tower-service",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "axum-macros"
|
||||||
|
version = "0.3.7"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "2bb524613be645939e280b7279f7b017f98cf7f5ef084ec374df373530e73277"
|
||||||
|
dependencies = [
|
||||||
|
"heck",
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn 2.0.14",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "backtrace"
|
name = "backtrace"
|
||||||
version = "0.3.67"
|
version = "0.3.67"
|
||||||
|
|
@ -1079,6 +1092,8 @@ dependencies = [
|
||||||
"quick-xml",
|
"quick-xml",
|
||||||
"rand 0.8.5",
|
"rand 0.8.5",
|
||||||
"reqwest",
|
"reqwest",
|
||||||
|
"sea-query",
|
||||||
|
"sea-query-binder",
|
||||||
"serde",
|
"serde",
|
||||||
"sqlx",
|
"sqlx",
|
||||||
"steam-openid",
|
"steam-openid",
|
||||||
|
|
@ -2990,6 +3005,40 @@ dependencies = [
|
||||||
"untrusted",
|
"untrusted",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "sea-query"
|
||||||
|
version = "0.28.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "5dd34be05fdde9ec79231414bdd44ba1aa9c57349190076699e90721cb5eb59b"
|
||||||
|
dependencies = [
|
||||||
|
"sea-query-derive",
|
||||||
|
"time 0.3.20",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "sea-query-binder"
|
||||||
|
version = "0.3.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "03548c63aec07afd4fd190923e0160d2f2fc92def27470b54154cf232da6203b"
|
||||||
|
dependencies = [
|
||||||
|
"sea-query",
|
||||||
|
"sqlx",
|
||||||
|
"time 0.3.20",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "sea-query-derive"
|
||||||
|
version = "0.3.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "63f62030c60f3a691f5fe251713b4e220b306e50a71e1d6f9cce1f24bb781978"
|
||||||
|
dependencies = [
|
||||||
|
"heck",
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn 1.0.109",
|
||||||
|
"thiserror",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "seahash"
|
name = "seahash"
|
||||||
version = "4.1.0"
|
version = "4.1.0"
|
||||||
|
|
|
||||||
|
|
@ -9,12 +9,14 @@ tracing-subscriber = { version = "0.3.16", features = ["env-filter"] }
|
||||||
serde = { version = "1.0.159", features = ["derive"] }
|
serde = { version = "1.0.159", features = ["derive"] }
|
||||||
toml = "0.7.3"
|
toml = "0.7.3"
|
||||||
sqlx = { version = "0.6.3", features = ["postgres", "time", "runtime-tokio-rustls"] }
|
sqlx = { version = "0.6.3", features = ["postgres", "time", "runtime-tokio-rustls"] }
|
||||||
|
sea-query = { version = "0.28.4", features = ["backend-postgres", "with-time", "derive"] }
|
||||||
|
sea-query-binder = { version = "0.3.0", features = ["with-time", "sqlx-postgres"] }
|
||||||
thiserror = "1.0.40"
|
thiserror = "1.0.40"
|
||||||
tokio = { version = "1.27.0", features = ["full"] }
|
tokio = { version = "1.27.0", features = ["full"] }
|
||||||
config = { version = "0.13.3", features = ["toml"] }
|
config = { version = "0.13.3", features = ["toml"] }
|
||||||
time = "0.3.20"
|
time = "0.3.20"
|
||||||
maud = { version = "0.24.0", git = "https://github.com/lambda-fairy/maud", rev = "7233cda35eed7bba91c9c55564d65498067c3822", features = ["axum"] }
|
maud = { version = "0.24.0", git = "https://github.com/lambda-fairy/maud", rev = "7233cda35eed7bba91c9c55564d65498067c3822", features = ["axum"] }
|
||||||
axum = { version = "0.6.12", features = ["headers"] }
|
axum = { version = "0.6.12", features = ["headers", "macros"] }
|
||||||
hyper = "0.14.25"
|
hyper = "0.14.25"
|
||||||
hyperlocal = "0.8.0"
|
hyperlocal = "0.8.0"
|
||||||
tower-http = { version = "0.4.0", features = ["trace"] }
|
tower-http = { version = "0.4.0", features = ["trace"] }
|
||||||
|
|
|
||||||
2
build/derive/Cargo.lock
generated
2
build/derive/Cargo.lock
generated
|
|
@ -537,6 +537,7 @@ version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"base64 0.21.0",
|
"base64 0.21.0",
|
||||||
|
"fnv",
|
||||||
"jsx-dom-expressions",
|
"jsx-dom-expressions",
|
||||||
"lightningcss",
|
"lightningcss",
|
||||||
"rand 0.8.5",
|
"rand 0.8.5",
|
||||||
|
|
@ -560,7 +561,6 @@ name = "demostf-build-derive"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"demostf-build-bundlers",
|
"demostf-build-bundlers",
|
||||||
"fnv",
|
|
||||||
"merge",
|
"merge",
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,7 @@
|
||||||
|
import {FilterSet} from "./filterbar";
|
||||||
|
|
||||||
export type SteamId = string;
|
export type SteamId = string;
|
||||||
|
|
||||||
export interface SteamUser {
|
export interface SteamUser {
|
||||||
id: number;
|
id: number;
|
||||||
steamid: SteamId;
|
steamid: SteamId;
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,22 @@ ready(() => {
|
||||||
const maps = filterBar.dataset.maps.split(",");
|
const maps = filterBar.dataset.maps.split(",");
|
||||||
const apiBase = filterBar.dataset.apiBase;
|
const apiBase = filterBar.dataset.apiBase;
|
||||||
const api = new Api(apiBase);
|
const api = new Api(apiBase);
|
||||||
|
const demoListBody = document.querySelector('.demolist tbody');
|
||||||
|
|
||||||
render(() => <FilterBar maps={maps} api={api} />, filterBar)
|
render(() => <FilterBar maps={maps} api={api} onChange={onFilter.bind(null, api, demoListBody)}/>, filterBar);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const onFilter = async (api, demoListBody, filter) => {
|
||||||
|
if (!(filter.mode || filter.map || filter.players.length)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let queryParams = new URLSearchParams({
|
||||||
|
players: filter.players.map(player => player.id).join(','),
|
||||||
|
mode: filter.mode.toLowerCase(),
|
||||||
|
map: filter.map,
|
||||||
|
});
|
||||||
|
console.log(queryParams);
|
||||||
|
const response = await fetch("/fragments/demo-list?" + queryParams);
|
||||||
|
document.querySelector('.demolist tbody').innerHTML = await response.text();
|
||||||
|
}
|
||||||
|
|
@ -1,19 +1,47 @@
|
||||||
import {Select, createOptions, createAsyncOptions} from "@thisbeyond/solid-select";
|
import {Select, createOptions, createAsyncOptions} from "@thisbeyond/solid-select";
|
||||||
import {Api} from "./api";
|
import {Api, SteamUser} from "./api";
|
||||||
|
import {createEffect, createSignal} from "solid-js";
|
||||||
|
|
||||||
export interface FilterBarProps {
|
export interface FilterBarProps {
|
||||||
maps: string[],
|
maps: string[],
|
||||||
api: Api,
|
api: Api,
|
||||||
|
onChange: (FilterSet) => void,
|
||||||
}
|
}
|
||||||
|
|
||||||
export const FilterBar = ({maps, api}: FilterBarProps) => {
|
export const FilterBar = ({maps, api, onChange}: FilterBarProps) => {
|
||||||
const modes = createOptions(["4v4", "6v6", "Highlander"]);
|
const modes = createOptions(["4v4", "6v6", "Highlander"]);
|
||||||
const mapOptions = createOptions(maps);
|
const mapOptions = createOptions(maps, {
|
||||||
|
createable: true
|
||||||
|
});
|
||||||
const playerOptions = createAsyncOptions(search => api.searchPlayer(search));
|
const playerOptions = createAsyncOptions(search => api.searchPlayer(search));
|
||||||
const playerFormat = player => player.name;
|
const playerFormat = player => player.name;
|
||||||
|
|
||||||
|
|
||||||
|
const [filterSet, setFilterSet] = createSignal({
|
||||||
|
mode: "",
|
||||||
|
map: "",
|
||||||
|
players: []
|
||||||
|
});
|
||||||
|
createEffect(() => onChange(filterSet()));
|
||||||
|
|
||||||
return <div class="filter-bar">
|
return <div class="filter-bar">
|
||||||
<Select class="mode" placeholder="All Types" {...modes} />
|
<Select class="mode" onChange={mode => setFilterSet({
|
||||||
<Select class="maps" placeholder="All Maps" {...mapOptions} />
|
...filterSet(),
|
||||||
<Select class="players" multiple placeholder="All Players" format={playerFormat} {...playerOptions} />
|
mode
|
||||||
|
})} placeholder="All Types" {...modes} />
|
||||||
|
<Select class="maps" onChange={map => setFilterSet({
|
||||||
|
...filterSet(),
|
||||||
|
map
|
||||||
|
})} placeholder="All Maps" {...mapOptions} />
|
||||||
|
<Select class="players" onChange={players => setFilterSet({
|
||||||
|
...filterSet(),
|
||||||
|
players
|
||||||
|
})} multiple placeholder="All Players" format={playerFormat} {...playerOptions} />
|
||||||
</div>;
|
</div>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface FilterSet {
|
||||||
|
mode: string,
|
||||||
|
map: string,
|
||||||
|
players: SteamUser[],
|
||||||
|
}
|
||||||
147
src/data/demo.rs
147
src/data/demo.rs
|
|
@ -1,10 +1,20 @@
|
||||||
use crate::data::chat::Chat;
|
use crate::data::chat::Chat;
|
||||||
use crate::data::player::Player;
|
use crate::data::player::Player;
|
||||||
|
use crate::data::schema::{ArrayAgg, CleanMapName, Demos, Players};
|
||||||
use crate::data::steam_id::SteamId;
|
use crate::data::steam_id::SteamId;
|
||||||
use crate::Result;
|
use crate::Result;
|
||||||
use maud::Render;
|
use maud::Render;
|
||||||
|
use sea_query::extension::postgres::PgExpr;
|
||||||
|
use sea_query::{
|
||||||
|
Alias, Expr, Func, Order, PostgresQueryBuilder, Query, SelectStatement, SimpleExpr,
|
||||||
|
};
|
||||||
|
use sea_query_binder::SqlxBinder;
|
||||||
|
use serde::{Deserialize, Deserializer};
|
||||||
use sqlx::{query_as, Executor, FromRow, Postgres};
|
use sqlx::{query_as, Executor, FromRow, Postgres};
|
||||||
|
use std::borrow::Cow;
|
||||||
use std::fmt::Write;
|
use std::fmt::Write;
|
||||||
|
use std::ops::Range;
|
||||||
|
use std::str::FromStr;
|
||||||
use time::format_description::well_known::Iso8601;
|
use time::format_description::well_known::Iso8601;
|
||||||
use time::{OffsetDateTime, PrimitiveDateTime, UtcOffset};
|
use time::{OffsetDateTime, PrimitiveDateTime, UtcOffset};
|
||||||
use tracing::instrument;
|
use tracing::instrument;
|
||||||
|
|
@ -156,19 +166,9 @@ impl ListDemo {
|
||||||
#[instrument(skip(connection))]
|
#[instrument(skip(connection))]
|
||||||
pub async fn list(
|
pub async fn list(
|
||||||
connection: impl Executor<'_, Database = Postgres>,
|
connection: impl Executor<'_, Database = Postgres>,
|
||||||
before: Option<u32>,
|
filter: Filter,
|
||||||
) -> Result<Vec<Self>> {
|
) -> Result<Vec<Self>> {
|
||||||
if let Some(before) = before {
|
if filter.is_empty() {
|
||||||
Ok(query_as!(
|
|
||||||
ListDemo,
|
|
||||||
r#"SELECT
|
|
||||||
id, name, map, red, blu, duration, created_at, server, "playerCount" as player_count
|
|
||||||
FROM demos WHERE deleted_at IS NULL and id < $1 ORDER BY id DESC LIMIT 50"#,
|
|
||||||
before as i32
|
|
||||||
)
|
|
||||||
.fetch_all(connection)
|
|
||||||
.await?)
|
|
||||||
} else {
|
|
||||||
Ok(query_as!(
|
Ok(query_as!(
|
||||||
ListDemo,
|
ListDemo,
|
||||||
r#"SELECT
|
r#"SELECT
|
||||||
|
|
@ -177,6 +177,31 @@ impl ListDemo {
|
||||||
)
|
)
|
||||||
.fetch_all(connection)
|
.fetch_all(connection)
|
||||||
.await?)
|
.await?)
|
||||||
|
} else {
|
||||||
|
let mut query = Query::select();
|
||||||
|
query
|
||||||
|
.column((Demos::Table, Demos::Id))
|
||||||
|
.column((Demos::Table, Demos::Name))
|
||||||
|
.columns([
|
||||||
|
Demos::Map,
|
||||||
|
Demos::Red,
|
||||||
|
Demos::Blu,
|
||||||
|
Demos::Duration,
|
||||||
|
Demos::Server,
|
||||||
|
Demos::CreatedAt,
|
||||||
|
])
|
||||||
|
.expr_as(Expr::col(Demos::PlayerCount), Alias::new("player_count"))
|
||||||
|
.from(Demos::Table)
|
||||||
|
.and_where(Expr::col(Demos::DeletedAt).is_null())
|
||||||
|
.order_by(Demos::Id, Order::Desc)
|
||||||
|
.limit(50);
|
||||||
|
filter.apply(&mut query);
|
||||||
|
|
||||||
|
let (sql, values) = query.build_sqlx(PostgresQueryBuilder);
|
||||||
|
|
||||||
|
Ok(sqlx::query_as_with::<_, ListDemo, _>(&sql, values)
|
||||||
|
.fetch_all(connection)
|
||||||
|
.await?)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -314,3 +339,101 @@ impl Render for RelativeDate {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Default, Deserialize, Eq, PartialEq)]
|
||||||
|
pub enum GameMode {
|
||||||
|
#[serde(rename = "4v4")]
|
||||||
|
Fours,
|
||||||
|
#[serde(rename = "6v6")]
|
||||||
|
Sixes,
|
||||||
|
#[serde(rename = "prolander")]
|
||||||
|
Prolander,
|
||||||
|
#[serde(rename = "highlander")]
|
||||||
|
HighLander,
|
||||||
|
#[default]
|
||||||
|
#[serde(other)]
|
||||||
|
Any,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl GameMode {
|
||||||
|
fn player_count(&self) -> Option<Range<i32>> {
|
||||||
|
match self {
|
||||||
|
GameMode::Fours => Some(7..9),
|
||||||
|
GameMode::Sixes => Some(11..13),
|
||||||
|
GameMode::Prolander => Some(14..15),
|
||||||
|
GameMode::HighLander => Some(17..19),
|
||||||
|
GameMode::Any => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Default, Debug, Deserialize)]
|
||||||
|
pub struct Filter {
|
||||||
|
#[serde(default)]
|
||||||
|
mode: GameMode,
|
||||||
|
#[serde(default)]
|
||||||
|
map: String,
|
||||||
|
#[serde(default)]
|
||||||
|
#[serde(deserialize_with = "deserialize_array")]
|
||||||
|
players: Vec<i32>,
|
||||||
|
#[serde(default)]
|
||||||
|
before: Option<i32>,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn deserialize_array<'de, D, T>(deserializer: D) -> Result<Vec<T>, D::Error>
|
||||||
|
where
|
||||||
|
D: Deserializer<'de>,
|
||||||
|
T: Deserialize<'de> + FromStr,
|
||||||
|
{
|
||||||
|
let s = <Cow<str>>::deserialize(deserializer)?;
|
||||||
|
Ok(s.split(",").map(T::from_str).flatten().collect())
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Filter {
|
||||||
|
fn is_empty(&self) -> bool {
|
||||||
|
self.mode != GameMode::default()
|
||||||
|
&& self.map.is_empty()
|
||||||
|
&& self.before.is_none()
|
||||||
|
&& self.players.is_empty()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn apply(&self, query: &mut SelectStatement) {
|
||||||
|
if let Some(count) = self.mode.player_count() {
|
||||||
|
query.and_where(Expr::col(Demos::PlayerCount).between(count.start, count.end));
|
||||||
|
}
|
||||||
|
if !self.map.is_empty() {
|
||||||
|
let val = Expr::value(&self.map);
|
||||||
|
query.and_where(
|
||||||
|
Expr::col(Demos::Map).eq(val.clone()).or(SimpleExpr::from(
|
||||||
|
Func::cust(CleanMapName).arg(Expr::col(Demos::Map)),
|
||||||
|
)
|
||||||
|
.eq(val)),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if let Some(before) = &self.before {
|
||||||
|
query.and_where(Expr::col(Demos::Id).lt(*before));
|
||||||
|
}
|
||||||
|
if !self.players.is_empty() && self.players.len() < 19 {
|
||||||
|
let mut player_iter = self.players.iter();
|
||||||
|
let mut players = format!("array[{}", player_iter.next().unwrap());
|
||||||
|
for player in player_iter {
|
||||||
|
write!(&mut players, ",{}", player).unwrap();
|
||||||
|
}
|
||||||
|
players.push_str("]");
|
||||||
|
// query.and_where(Expr::cust(&players).contained(sub_array));
|
||||||
|
|
||||||
|
query
|
||||||
|
.inner_join(
|
||||||
|
Players::Table,
|
||||||
|
Expr::col((Demos::Table, Demos::Id)).equals((Players::Table, Players::DemoId)),
|
||||||
|
)
|
||||||
|
.and_where(Expr::col(Players::UserId).is_in(self.players.clone()));
|
||||||
|
query.group_by_col((Demos::Table, Players::Id));
|
||||||
|
query.and_having(
|
||||||
|
Expr::cust(&players).contained(
|
||||||
|
Func::cust(ArrayAgg).arg(Expr::col((Players::Table, Players::UserId))),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,5 +2,6 @@ pub mod chat;
|
||||||
pub mod demo;
|
pub mod demo;
|
||||||
pub mod maps;
|
pub mod maps;
|
||||||
pub mod player;
|
pub mod player;
|
||||||
|
mod schema;
|
||||||
pub mod steam_id;
|
pub mod steam_id;
|
||||||
pub mod user;
|
pub mod user;
|
||||||
|
|
|
||||||
94
src/data/schema.rs
Normal file
94
src/data/schema.rs
Normal file
|
|
@ -0,0 +1,94 @@
|
||||||
|
use sea_query::Iden;
|
||||||
|
use std::fmt::Write;
|
||||||
|
|
||||||
|
#[allow(dead_code)]
|
||||||
|
#[derive(Iden)]
|
||||||
|
pub enum Demos {
|
||||||
|
Table,
|
||||||
|
#[iden = "id"]
|
||||||
|
Id,
|
||||||
|
#[iden = "name"]
|
||||||
|
Name,
|
||||||
|
#[iden = "url"]
|
||||||
|
Url,
|
||||||
|
#[iden = "map"]
|
||||||
|
Map,
|
||||||
|
#[iden = "red"]
|
||||||
|
Red,
|
||||||
|
#[iden = "blu"]
|
||||||
|
Blu,
|
||||||
|
#[iden = "uploader"]
|
||||||
|
Uploader,
|
||||||
|
#[iden = "duration"]
|
||||||
|
Duration,
|
||||||
|
#[iden = "created_at"]
|
||||||
|
CreatedAt,
|
||||||
|
#[iden = "updated_at"]
|
||||||
|
UpdatedAt,
|
||||||
|
#[iden = "backend"]
|
||||||
|
Backend,
|
||||||
|
#[iden = "path"]
|
||||||
|
Path,
|
||||||
|
#[iden = "scoreBlue"]
|
||||||
|
ScoreBlue,
|
||||||
|
#[iden = "scoreRed"]
|
||||||
|
ScoreRed,
|
||||||
|
#[iden = "version"]
|
||||||
|
Version,
|
||||||
|
#[iden = "server"]
|
||||||
|
Server,
|
||||||
|
#[iden = "nick"]
|
||||||
|
Nick,
|
||||||
|
#[iden = "deleted_at"]
|
||||||
|
DeletedAt,
|
||||||
|
#[iden = "playerCount"]
|
||||||
|
PlayerCount,
|
||||||
|
#[iden = "hash"]
|
||||||
|
Hash,
|
||||||
|
#[iden = "blue_team_id"]
|
||||||
|
BlueTeamId,
|
||||||
|
#[iden = "red_team_id"]
|
||||||
|
RedTeamId,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct CleanMapName;
|
||||||
|
|
||||||
|
impl Iden for CleanMapName {
|
||||||
|
fn unquoted(&self, s: &mut dyn Write) {
|
||||||
|
write!(s, "clean_map_name").unwrap()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Iden)]
|
||||||
|
#[iden = "ARRAY"]
|
||||||
|
pub struct ArrayFunc;
|
||||||
|
|
||||||
|
#[derive(Iden)]
|
||||||
|
#[iden = "array_agg"]
|
||||||
|
pub struct ArrayAgg;
|
||||||
|
|
||||||
|
#[allow(dead_code)]
|
||||||
|
#[derive(Iden)]
|
||||||
|
pub enum Players {
|
||||||
|
Table,
|
||||||
|
#[iden = "id"]
|
||||||
|
Id,
|
||||||
|
#[iden = "demo_id"]
|
||||||
|
DemoId,
|
||||||
|
#[iden = "demo_user_id"]
|
||||||
|
DemoUserId,
|
||||||
|
#[iden = "user_id"]
|
||||||
|
UserId,
|
||||||
|
#[iden = "name"]
|
||||||
|
Name,
|
||||||
|
#[iden = "team"]
|
||||||
|
Team,
|
||||||
|
#[iden = "class"]
|
||||||
|
Class,
|
||||||
|
#[iden = "kills"]
|
||||||
|
Kills,
|
||||||
|
#[iden = "assists"]
|
||||||
|
Assists,
|
||||||
|
#[iden = "deaths"]
|
||||||
|
Deaths,
|
||||||
|
}
|
||||||
|
|
@ -4,8 +4,10 @@ use sqlx::database::HasValueRef;
|
||||||
use sqlx::error::BoxDynError;
|
use sqlx::error::BoxDynError;
|
||||||
use sqlx::{Database, Decode, Type};
|
use sqlx::{Database, Decode, Type};
|
||||||
use std::borrow::Cow;
|
use std::borrow::Cow;
|
||||||
|
use std::convert::Infallible;
|
||||||
use std::fmt::{Debug, Formatter};
|
use std::fmt::{Debug, Formatter};
|
||||||
use std::fmt::{Display, Write};
|
use std::fmt::{Display, Write};
|
||||||
|
use std::str::FromStr;
|
||||||
use steamid_ng::SteamID;
|
use steamid_ng::SteamID;
|
||||||
|
|
||||||
#[derive(Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
|
#[derive(Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
|
||||||
|
|
@ -90,15 +92,7 @@ where
|
||||||
{
|
{
|
||||||
fn decode(value: <DB as HasValueRef<'r>>::ValueRef) -> Result<Self, BoxDynError> {
|
fn decode(value: <DB as HasValueRef<'r>>::ValueRef) -> Result<Self, BoxDynError> {
|
||||||
let str = <&str as Decode<DB>>::decode(value)?;
|
let str = <&str as Decode<DB>>::decode(value)?;
|
||||||
if let Ok(id) = str.parse() {
|
Ok(str.parse().unwrap())
|
||||||
Ok(Self::Id(id))
|
|
||||||
} else if str == "serveme.tf" {
|
|
||||||
Ok(Self::Raw("serveme.tf".into()))
|
|
||||||
} else if str == "essentialstf" {
|
|
||||||
Ok(Self::Raw("essentialstf".into()))
|
|
||||||
} else {
|
|
||||||
Ok(Self::Raw(str.to_string().into()))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -144,3 +138,19 @@ impl SteamId {
|
||||||
ProfileLink(self)
|
ProfileLink(self)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl FromStr for SteamId {
|
||||||
|
type Err = Infallible;
|
||||||
|
|
||||||
|
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||||
|
if let Ok(id) = s.parse() {
|
||||||
|
Ok(Self::Id(id))
|
||||||
|
} else if s == "serveme.tf" {
|
||||||
|
Ok(Self::Raw("serveme.tf".into()))
|
||||||
|
} else if s == "essentialstf" {
|
||||||
|
Ok(Self::Raw("essentialstf".into()))
|
||||||
|
} else {
|
||||||
|
Ok(Self::Raw(s.to_string().into()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -28,7 +28,7 @@ impl IntoResponse for Error {
|
||||||
fn into_response(self) -> Response {
|
fn into_response(self) -> Response {
|
||||||
match self {
|
match self {
|
||||||
Error::NotFound => (StatusCode::NOT_FOUND, "not found").into_response(),
|
Error::NotFound => (StatusCode::NOT_FOUND, "not found").into_response(),
|
||||||
_ => todo!(),
|
e => format!("{:#}", e).into_response(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
24
src/fragments/demo_list.rs
Normal file
24
src/fragments/demo_list.rs
Normal file
|
|
@ -0,0 +1,24 @@
|
||||||
|
use crate::data::demo::ListDemo;
|
||||||
|
use maud::{html, Markup, Render};
|
||||||
|
|
||||||
|
pub struct DemoList {
|
||||||
|
pub demos: Vec<ListDemo>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Render for DemoList {
|
||||||
|
fn render(&self) -> Markup {
|
||||||
|
html! {
|
||||||
|
@for demo in &self.demos {
|
||||||
|
tr {
|
||||||
|
td .title {
|
||||||
|
a href = (demo.url()) { (demo.server) " - " (demo.red) " vs " (demo.blu) }
|
||||||
|
}
|
||||||
|
td .format { (demo.format()) }
|
||||||
|
td .map { (demo.map) }
|
||||||
|
td .duration { (demo.duration()) }
|
||||||
|
td .date title = (demo.date()) { (demo.relative_date()) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
1
src/fragments/mod.rs
Normal file
1
src/fragments/mod.rs
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
pub mod demo_list;
|
||||||
26
src/main.rs
26
src/main.rs
|
|
@ -2,16 +2,18 @@ mod asset;
|
||||||
mod config;
|
mod config;
|
||||||
mod data;
|
mod data;
|
||||||
mod error;
|
mod error;
|
||||||
|
mod fragments;
|
||||||
mod pages;
|
mod pages;
|
||||||
mod session;
|
mod session;
|
||||||
|
|
||||||
use crate::asset::serve_asset;
|
use crate::asset::serve_asset;
|
||||||
pub use crate::config::Config;
|
pub use crate::config::Config;
|
||||||
use crate::config::Listen;
|
use crate::config::Listen;
|
||||||
use crate::data::demo::{Demo, ListDemo};
|
use crate::data::demo::{Demo, Filter, ListDemo};
|
||||||
use crate::data::maps::map_list;
|
use crate::data::maps::map_list;
|
||||||
use crate::data::steam_id::SteamId;
|
use crate::data::steam_id::SteamId;
|
||||||
use crate::data::user::User;
|
use crate::data::user::User;
|
||||||
|
use crate::fragments::demo_list::DemoList;
|
||||||
use crate::pages::about::AboutPage;
|
use crate::pages::about::AboutPage;
|
||||||
use crate::pages::demo::DemoPage;
|
use crate::pages::demo::DemoPage;
|
||||||
use crate::pages::index::{DemoListScript, Index};
|
use crate::pages::index::{DemoListScript, Index};
|
||||||
|
|
@ -19,7 +21,7 @@ use crate::pages::upload::{UploadPage, UploadScript};
|
||||||
use crate::pages::{render, GlobalStyle};
|
use crate::pages::{render, GlobalStyle};
|
||||||
use crate::session::{SessionData, COOKIE_NAME};
|
use crate::session::{SessionData, COOKIE_NAME};
|
||||||
use async_session::{MemoryStore, Session, SessionStore};
|
use async_session::{MemoryStore, Session, SessionStore};
|
||||||
use axum::extract::{MatchedPath, Path, RawQuery};
|
use axum::extract::{MatchedPath, Path, Query, RawQuery};
|
||||||
use axum::headers::Cookie;
|
use axum::headers::Cookie;
|
||||||
use axum::http::header::{LOCATION, SET_COOKIE};
|
use axum::http::header::{LOCATION, SET_COOKIE};
|
||||||
use axum::http::{HeaderValue, Request, StatusCode};
|
use axum::http::{HeaderValue, Request, StatusCode};
|
||||||
|
|
@ -28,7 +30,7 @@ use axum::{extract::State, routing::get, Router, Server, TypedHeader};
|
||||||
use demostf_build::Asset;
|
use demostf_build::Asset;
|
||||||
pub use error::Error;
|
pub use error::Error;
|
||||||
use hyperlocal::UnixServerExt;
|
use hyperlocal::UnixServerExt;
|
||||||
use maud::Markup;
|
use maud::{Markup, Render};
|
||||||
use sqlx::PgPool;
|
use sqlx::PgPool;
|
||||||
use std::env::{args, var};
|
use std::env::{args, var};
|
||||||
use std::fs::{remove_file, set_permissions, Permissions};
|
use std::fs::{remove_file, set_permissions, Permissions};
|
||||||
|
|
@ -88,6 +90,7 @@ async fn main() -> Result<()> {
|
||||||
.route(DemoListScript::route(), get(serve_asset::<DemoListScript>))
|
.route(DemoListScript::route(), get(serve_asset::<DemoListScript>))
|
||||||
.route(LogoPng::route(), get(serve_asset::<LogoPng>))
|
.route(LogoPng::route(), get(serve_asset::<LogoPng>))
|
||||||
.route(LogoSvg::route(), get(serve_asset::<LogoSvg>))
|
.route(LogoSvg::route(), get(serve_asset::<LogoSvg>))
|
||||||
|
.route("/fragments/demo-list", get(demo_list))
|
||||||
.route("/about", get(about))
|
.route("/about", get(about))
|
||||||
.route("/login/callback", get(login_callback))
|
.route("/login/callback", get(login_callback))
|
||||||
.route("/login", get(login))
|
.route("/login", get(login))
|
||||||
|
|
@ -134,8 +137,14 @@ async fn main() -> Result<()> {
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn index(State(app): State<Arc<App>>, session: SessionData) -> Result<Markup> {
|
#[axum::debug_handler]
|
||||||
let demos = ListDemo::list(&app.connection, None).await?;
|
async fn index(
|
||||||
|
State(app): State<Arc<App>>,
|
||||||
|
session: SessionData,
|
||||||
|
filter: Option<Query<Filter>>,
|
||||||
|
) -> Result<Markup> {
|
||||||
|
let filter = filter.map(|filter| filter.0).unwrap_or_default();
|
||||||
|
let demos = ListDemo::list(&app.connection, filter).await?;
|
||||||
let maps = map_list(&app.connection).await?.collect();
|
let maps = map_list(&app.connection).await?.collect();
|
||||||
Ok(render(
|
Ok(render(
|
||||||
Index {
|
Index {
|
||||||
|
|
@ -258,6 +267,13 @@ async fn upload(State(app): State<Arc<App>>, session: SessionData) -> impl IntoR
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[axum::debug_handler]
|
||||||
|
async fn demo_list(State(app): State<Arc<App>>, filter: Option<Query<Filter>>) -> Result<Markup> {
|
||||||
|
let filter = filter.map(|filter| filter.0).unwrap_or_default();
|
||||||
|
let demos = ListDemo::list(&app.connection, filter).await?;
|
||||||
|
Ok(DemoList { demos }.render())
|
||||||
|
}
|
||||||
|
|
||||||
async fn handler_404() -> impl IntoResponse {
|
async fn handler_404() -> impl IntoResponse {
|
||||||
Error::NotFound
|
Error::NotFound
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue