Big commit that adds and changes a bunch of stuff.
A lot of this will be changed around again anyway.
This commit is contained in:
parent
99b24d0eac
commit
4b3863e0d1
|
@ -0,0 +1,116 @@
|
|||
//! All code for drawing the [`plotters`] charts.
|
||||
|
||||
use {
|
||||
async_std::{fs::create_dir_all, path::PathBuf},
|
||||
color_eyre::Result,
|
||||
plotters::prelude::*,
|
||||
};
|
||||
|
||||
use crate::group_data::GroupDataModel;
|
||||
|
||||
const BACKGROUND_1: RGBColor = RGBColor(17, 17, 17);
|
||||
const BACKGROUND_2: RGBColor = RGBColor(0, 0, 0);
|
||||
const FOREGROUND: RGBColor = RGBColor(255, 255, 255);
|
||||
const ACCENT_1: RGBColor = RGBColor(255, 0, 255);
|
||||
|
||||
/// The chart for the user count.
|
||||
#[derive(Debug)]
|
||||
pub struct UserCountChart {
|
||||
/// The groups to use for user counts.
|
||||
pub groups: Vec<GroupDataModel>,
|
||||
}
|
||||
|
||||
impl UserCountChart {
|
||||
/// Render the chart and write it to file.
|
||||
pub async fn render(&self, parent: &PathBuf, group_name: &str) -> Result<()> {
|
||||
let parent = parent.join("charts");
|
||||
create_dir_all(&parent).await?;
|
||||
|
||||
let (mut datapoints, mut min_count, mut max_count) = (vec![], i64::MAX, 0);
|
||||
|
||||
for (index, group) in self.groups.iter().enumerate() {
|
||||
datapoints.push(((index + 1) as isize, group.subscribers));
|
||||
|
||||
if group.subscribers > max_count {
|
||||
max_count = group.subscribers;
|
||||
}
|
||||
|
||||
if group.subscribers < min_count {
|
||||
min_count = group.subscribers;
|
||||
}
|
||||
}
|
||||
|
||||
let datapoints_len = datapoints.len() as isize;
|
||||
let min_count = min_count - 10;
|
||||
let max_count = max_count + 10;
|
||||
|
||||
let path = parent.join("user-count.svg");
|
||||
let chart_root = SVGBackend::new(&path, (1280, 720)).into_drawing_area();
|
||||
chart_root.fill(&BACKGROUND_1)?;
|
||||
|
||||
let text_style =
|
||||
|font_size: i32| ("sans-serif", font_size).into_font().color(&FOREGROUND);
|
||||
|
||||
let chart_root = chart_root
|
||||
.margin(20, 20, 20, 20)
|
||||
.titled("Tildes User Count", text_style(30))?;
|
||||
|
||||
chart_root.fill(&BACKGROUND_1)?;
|
||||
|
||||
let mut chart = ChartBuilder::on(&chart_root)
|
||||
.caption(
|
||||
format!("Using the {group_name} subscriber count."),
|
||||
text_style(20),
|
||||
)
|
||||
.x_label_area_size(40)
|
||||
.y_label_area_size(40)
|
||||
.margin(10)
|
||||
.build_cartesian_2d(0..(datapoints_len + 1), min_count..max_count)?;
|
||||
|
||||
chart
|
||||
.configure_mesh()
|
||||
.x_labels(datapoints.len() + 2)
|
||||
.x_label_formatter(&|x| format!("{:0}", datapoints_len - x))
|
||||
.x_desc("N days ago")
|
||||
.y_labels(5)
|
||||
.y_label_formatter(&|y| format!("{y:0}"))
|
||||
.label_style(text_style(20))
|
||||
.axis_style(&BACKGROUND_2)
|
||||
.light_line_style(&BACKGROUND_2)
|
||||
.bold_line_style(&BACKGROUND_1)
|
||||
.draw()?;
|
||||
|
||||
chart
|
||||
.draw_series(LineSeries::new(
|
||||
datapoints.clone(),
|
||||
ACCENT_1.stroke_width(2),
|
||||
))?
|
||||
.label("User Count")
|
||||
.legend(|(x, y)| {
|
||||
PathElement::new(vec![(x, y), (x + 20, y)], ACCENT_1.stroke_width(4))
|
||||
});
|
||||
|
||||
chart.draw_series(PointSeries::of_element(
|
||||
datapoints,
|
||||
5,
|
||||
&ACCENT_1,
|
||||
&|(x, y), size, style| {
|
||||
EmptyElement::at((x, y))
|
||||
+ Circle::new((0, 0), size, style.filled())
|
||||
+ Text::new(
|
||||
{
|
||||
if (x - 1) % 2 != 0 {
|
||||
String::new()
|
||||
} else {
|
||||
format!("{:0}", y)
|
||||
}
|
||||
},
|
||||
(-10, 15),
|
||||
text_style(20),
|
||||
)
|
||||
},
|
||||
))?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
|
@ -1,6 +1,7 @@
|
|||
//! All CLI-related code.
|
||||
|
||||
use {
|
||||
async_std::path::PathBuf,
|
||||
chrono::NaiveDate,
|
||||
clap::{Parser, Subcommand},
|
||||
};
|
||||
|
@ -43,6 +44,13 @@ pub enum MainSubcommands {
|
|||
#[command(subcommand)]
|
||||
command: SnapshotSubcommands,
|
||||
},
|
||||
|
||||
/// Website management.
|
||||
Web {
|
||||
/// Website management.
|
||||
#[command(subcommand)]
|
||||
command: WebSubcommands,
|
||||
},
|
||||
}
|
||||
|
||||
/// Migrate subcommands.
|
||||
|
@ -86,3 +94,14 @@ pub enum SnapshotSubcommands {
|
|||
date: Option<NaiveDate>,
|
||||
},
|
||||
}
|
||||
|
||||
/// Website subcommands.
|
||||
#[derive(Debug, Subcommand)]
|
||||
pub enum WebSubcommands {
|
||||
/// Build the website.
|
||||
Build {
|
||||
/// The output directory for the website files.
|
||||
#[clap(short, long, default_value = "public")]
|
||||
output: PathBuf,
|
||||
},
|
||||
}
|
||||
|
|
|
@ -1,15 +1,21 @@
|
|||
//! All logic for running the CLI.
|
||||
|
||||
use {
|
||||
clap::Parser, color_eyre::Result, sea_orm_migration::MigratorTrait,
|
||||
tracing::info,
|
||||
async_std::fs::create_dir_all, clap::Parser, color_eyre::Result,
|
||||
sea_orm_migration::MigratorTrait, tracing::info,
|
||||
};
|
||||
|
||||
use crate::{
|
||||
cli::{Cli, MainSubcommands, MigrateSubcommands, SnapshotSubcommands},
|
||||
group_data::get_all_by_snapshot,
|
||||
charts::UserCountChart,
|
||||
cli::{
|
||||
Cli, MainSubcommands, MigrateSubcommands, SnapshotSubcommands,
|
||||
WebSubcommands,
|
||||
},
|
||||
group_data::GroupDataModel,
|
||||
migrations::Migrator,
|
||||
snapshots::{self, get_by_date},
|
||||
scss::generate_css,
|
||||
snapshots::SnapshotModel,
|
||||
templates::HomeTemplate,
|
||||
utilities::{create_db, today},
|
||||
};
|
||||
|
||||
|
@ -43,18 +49,20 @@ pub async fn run() -> Result<()> {
|
|||
command: snapshot_command,
|
||||
} => match snapshot_command {
|
||||
SnapshotSubcommands::Create { force } => {
|
||||
snapshots::create(&db, force).await?;
|
||||
SnapshotModel::create(&db, force).await?;
|
||||
}
|
||||
|
||||
SnapshotSubcommands::List {} => {
|
||||
for snapshot in snapshots::get_all(&db).await? {
|
||||
for snapshot in SnapshotModel::get_all(&db).await? {
|
||||
info!("Snapshot {snapshot:?}")
|
||||
}
|
||||
}
|
||||
|
||||
SnapshotSubcommands::Show { date } => {
|
||||
let date = date.unwrap_or_else(today);
|
||||
let snapshot = if let Some(snapshot) = get_by_date(&db, date).await? {
|
||||
let snapshot = if let Some(snapshot) =
|
||||
SnapshotModel::get_by_date(&db, date).await?
|
||||
{
|
||||
info!("Snapshot {snapshot:?}");
|
||||
snapshot
|
||||
} else {
|
||||
|
@ -62,8 +70,8 @@ pub async fn run() -> Result<()> {
|
|||
return Ok(());
|
||||
};
|
||||
|
||||
let groups = get_all_by_snapshot(&db, &snapshot).await?;
|
||||
for group in groups {
|
||||
for group in GroupDataModel::get_all_by_snapshot(&db, &snapshot).await?
|
||||
{
|
||||
info!(
|
||||
id = group.id,
|
||||
name = group.name,
|
||||
|
@ -72,6 +80,35 @@ pub async fn run() -> Result<()> {
|
|||
}
|
||||
}
|
||||
},
|
||||
|
||||
MainSubcommands::Web {
|
||||
command: web_command,
|
||||
} => match web_command {
|
||||
WebSubcommands::Build { output } => {
|
||||
let user_count_group =
|
||||
if let Some(snapshot) = SnapshotModel::get_most_recent(&db).await? {
|
||||
GroupDataModel::get_highest_subscribers(&db, &snapshot).await?
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
create_dir_all(&output).await?;
|
||||
HomeTemplate::new(
|
||||
user_count_group.as_ref().map(|group| group.subscribers),
|
||||
)
|
||||
.render_to_file(&output)
|
||||
.await?;
|
||||
generate_css(&output).await?;
|
||||
|
||||
if let Some(group) = user_count_group {
|
||||
let groups =
|
||||
GroupDataModel::get_n_most_recent(&db, 30, &group.name).await?;
|
||||
UserCountChart { groups }
|
||||
.render(&output, &group.name)
|
||||
.await?;
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
Ok(())
|
||||
|
|
|
@ -1,14 +1,55 @@
|
|||
//! All logic for group datas.
|
||||
|
||||
use {color_eyre::Result, sea_orm::prelude::*};
|
||||
use {
|
||||
color_eyre::Result,
|
||||
sea_orm::{prelude::*, QueryOrder, QuerySelect},
|
||||
};
|
||||
|
||||
use crate::entities::{group_data, snapshot};
|
||||
pub use crate::{
|
||||
entities::group_data::{
|
||||
ActiveModel as GroupDataActiveModel, Column as GroupDataColumn,
|
||||
Entity as GroupDataEntity, Model as GroupDataModel,
|
||||
},
|
||||
snapshots::SnapshotModel,
|
||||
};
|
||||
|
||||
/// Get all group datas from a given snapshot.
|
||||
pub async fn get_all_by_snapshot(
|
||||
db: &DatabaseConnection,
|
||||
snapshot: &snapshot::Model,
|
||||
) -> Result<Vec<group_data::Model>> {
|
||||
let groups = snapshot.find_related(group_data::Entity).all(db).await?;
|
||||
Ok(groups)
|
||||
impl GroupDataModel {
|
||||
/// Get all group datas from a given snapshot.
|
||||
pub async fn get_all_by_snapshot(
|
||||
db: &DatabaseConnection,
|
||||
snapshot: &SnapshotModel,
|
||||
) -> Result<Vec<Self>> {
|
||||
let groups = snapshot.find_related(GroupDataEntity).all(db).await?;
|
||||
Ok(groups)
|
||||
}
|
||||
|
||||
/// Get the group with the highest subscriber count from a given snapshot.
|
||||
pub async fn get_highest_subscribers(
|
||||
db: &DatabaseConnection,
|
||||
snapshot: &SnapshotModel,
|
||||
) -> Result<Option<Self>> {
|
||||
let group = snapshot
|
||||
.find_related(GroupDataEntity)
|
||||
.order_by_desc(GroupDataColumn::Subscribers)
|
||||
.one(db)
|
||||
.await?;
|
||||
|
||||
Ok(group)
|
||||
}
|
||||
|
||||
/// Get the N most recently saved group datas from a given group name.
|
||||
pub async fn get_n_most_recent(
|
||||
db: &DatabaseConnection,
|
||||
amount: u64,
|
||||
name: &str,
|
||||
) -> Result<Vec<Self>> {
|
||||
let groups = GroupDataEntity::find()
|
||||
.order_by_desc(GroupDataColumn::SnapshotId)
|
||||
.filter(GroupDataColumn::Name.eq(name))
|
||||
.limit(amount)
|
||||
.all(db)
|
||||
.await?;
|
||||
|
||||
Ok(groups)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -11,9 +11,11 @@ use {
|
|||
tracing_subscriber::filter::{EnvFilter, LevelFilter},
|
||||
};
|
||||
|
||||
pub mod charts;
|
||||
pub mod cli;
|
||||
pub mod group_data;
|
||||
pub mod migrations;
|
||||
pub mod scss;
|
||||
pub mod snapshots;
|
||||
pub mod templates;
|
||||
pub mod utilities;
|
||||
|
|
|
@ -0,0 +1,47 @@
|
|||
html {
|
||||
font-size: 62.5%;
|
||||
}
|
||||
|
||||
body {
|
||||
--small-spacing: 4px;
|
||||
--medium-spacing: 8px;
|
||||
--large-spacing: 16px;
|
||||
--background-1: #222;
|
||||
--background-2: #111;
|
||||
--foreground-1: #fff;
|
||||
--anchor-1: #f0f;
|
||||
--anchor-2: #000;
|
||||
|
||||
background-color: var(--background-1);
|
||||
color: var(--foreground-1);
|
||||
font-size: 2rem;
|
||||
}
|
||||
|
||||
a,
|
||||
a:visited {
|
||||
color: var(--anchor-1);
|
||||
text-decoration: none;
|
||||
|
||||
&:hover {
|
||||
background-color: var(--anchor-1);
|
||||
color: var(--anchor-2);
|
||||
}
|
||||
}
|
||||
|
||||
h1,
|
||||
h2,
|
||||
h3,
|
||||
p,
|
||||
ol,
|
||||
li {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.bold {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.underline {
|
||||
text-decoration: underline;
|
||||
}
|
|
@ -0,0 +1,35 @@
|
|||
@mixin responsive-container($breakpoint) {
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
width: $breakpoint;
|
||||
|
||||
@media (max-width: $breakpoint) {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.page-header,
|
||||
.page-main,
|
||||
.page-footer {
|
||||
@include responsive-container(1200px);
|
||||
|
||||
margin-bottom: var(--large-spacing);
|
||||
padding: var(--large-spacing);
|
||||
}
|
||||
|
||||
.page-header {
|
||||
border-bottom: 4px solid var(--background-2);
|
||||
}
|
||||
|
||||
.page-main {
|
||||
h2 {
|
||||
margin-bottom: var(--medium-spacing);
|
||||
}
|
||||
}
|
||||
|
||||
.page-footer {
|
||||
background-color: var(--background-2);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--large-spacing);
|
||||
}
|
|
@ -0,0 +1,40 @@
|
|||
//! All SCSS files.
|
||||
|
||||
use {
|
||||
async_std::{
|
||||
fs::{create_dir_all, write},
|
||||
path::PathBuf,
|
||||
},
|
||||
color_eyre::{eyre::Context, Result},
|
||||
};
|
||||
|
||||
const MODERN_NORMALIZE_CSS: &str =
|
||||
include_str!("../../node_modules/modern-normalize/modern-normalize.css");
|
||||
|
||||
/// Generate the CSS files and write them.
|
||||
pub async fn generate_css(parent: &PathBuf) -> Result<()> {
|
||||
let parent = parent.join("css");
|
||||
create_dir_all(&parent).await?;
|
||||
|
||||
let render = |scss: &str| -> Result<String> {
|
||||
grass::from_string(scss.to_string(), &grass::Options::default())
|
||||
.wrap_err("Failed SCSS render")
|
||||
};
|
||||
|
||||
let css_to_create = vec![
|
||||
("modern-normalize.css", MODERN_NORMALIZE_CSS, false),
|
||||
("common.css", include_str!("common.scss"), true),
|
||||
("index.css", include_str!("index.scss"), true),
|
||||
];
|
||||
|
||||
for (file, css, is_scss) in css_to_create {
|
||||
let path = parent.join(file);
|
||||
if is_scss {
|
||||
write(path, render(css)?).await?;
|
||||
} else {
|
||||
write(path, css).await?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
|
@ -8,69 +8,71 @@ use {
|
|||
};
|
||||
|
||||
use crate::{
|
||||
entities::{group_data, snapshot},
|
||||
snapshots::get_by_date,
|
||||
group_data::{GroupDataActiveModel, GroupDataEntity},
|
||||
snapshots::{SnapshotActiveModel, SnapshotModel},
|
||||
utilities::{create_http_client, download_html, today},
|
||||
};
|
||||
|
||||
/// Create a snapshot for today.
|
||||
pub async fn create(db: &DatabaseConnection, force: bool) -> Result<()> {
|
||||
let snapshot_date = today();
|
||||
match (force, get_by_date(db, snapshot_date).await?) {
|
||||
(true, Some(existing)) => {
|
||||
info!("Removing existing snapshot {:?}", existing);
|
||||
existing.delete(db).await?;
|
||||
}
|
||||
impl SnapshotModel {
|
||||
/// Create a snapshot for today.
|
||||
pub async fn create(db: &DatabaseConnection, force: bool) -> Result<()> {
|
||||
let snapshot_date = today();
|
||||
match (force, Self::get_by_date(db, snapshot_date).await?) {
|
||||
(true, Some(existing)) => {
|
||||
info!("Removing existing snapshot {:?}", existing);
|
||||
existing.delete(db).await?;
|
||||
}
|
||||
|
||||
(false, Some(existing)) => {
|
||||
info!("Snapshot for today already exists");
|
||||
info!("Use --force to override snapshot {:?}", existing);
|
||||
return Ok(());
|
||||
}
|
||||
(false, Some(existing)) => {
|
||||
info!("Snapshot for today already exists");
|
||||
info!("Use --force to override snapshot {:?}", existing);
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
(_, None) => (),
|
||||
};
|
||||
(_, None) => (),
|
||||
};
|
||||
|
||||
let transaction = db.begin().await?;
|
||||
let snapshot = snapshot::ActiveModel {
|
||||
date: Set(snapshot_date),
|
||||
..Default::default()
|
||||
}
|
||||
.insert(&transaction)
|
||||
.await?;
|
||||
|
||||
info!("Scraping data for snapshot {:?}", snapshot);
|
||||
|
||||
let http = create_http_client()?;
|
||||
let group_list = GroupList::from_html(
|
||||
&download_html(&http, "https://tildes.net/groups").await?,
|
||||
)?;
|
||||
|
||||
let mut groups_to_insert = vec![];
|
||||
|
||||
for summary in group_list.summaries {
|
||||
debug!(summary = ?summary);
|
||||
let group = Group::from_html(
|
||||
&download_html(&http, format!("https://tildes.net/{}", summary.name))
|
||||
.await?,
|
||||
)?;
|
||||
|
||||
debug!(group = ?group);
|
||||
groups_to_insert.push(group_data::ActiveModel {
|
||||
description: Set(group.description),
|
||||
name: Set(group.name),
|
||||
snapshot_id: Set(snapshot.id),
|
||||
subscribers: Set(group.subscribers.into()),
|
||||
let transaction = db.begin().await?;
|
||||
let snapshot = SnapshotActiveModel {
|
||||
date: Set(snapshot_date),
|
||||
..Default::default()
|
||||
});
|
||||
}
|
||||
|
||||
info!("Inserting {} groups", groups_to_insert.len());
|
||||
group_data::Entity::insert_many(groups_to_insert)
|
||||
.exec(&transaction)
|
||||
}
|
||||
.insert(&transaction)
|
||||
.await?;
|
||||
|
||||
transaction.commit().await?;
|
||||
info!("Scraping data for snapshot {:?}", snapshot);
|
||||
|
||||
Ok(())
|
||||
let http = create_http_client()?;
|
||||
let group_list = GroupList::from_html(
|
||||
&download_html(&http, "https://tildes.net/groups").await?,
|
||||
)?;
|
||||
|
||||
let mut groups_to_insert = vec![];
|
||||
|
||||
for summary in group_list.summaries {
|
||||
debug!(summary = ?summary);
|
||||
let group = Group::from_html(
|
||||
&download_html(&http, format!("https://tildes.net/{}", summary.name))
|
||||
.await?,
|
||||
)?;
|
||||
|
||||
debug!(group = ?group);
|
||||
groups_to_insert.push(GroupDataActiveModel {
|
||||
description: Set(group.description),
|
||||
name: Set(group.name),
|
||||
snapshot_id: Set(snapshot.id),
|
||||
subscribers: Set(group.subscribers.into()),
|
||||
..Default::default()
|
||||
});
|
||||
}
|
||||
|
||||
info!("Inserting {} groups", groups_to_insert.len());
|
||||
GroupDataEntity::insert_many(groups_to_insert)
|
||||
.exec(&transaction)
|
||||
.await?;
|
||||
|
||||
transaction.commit().await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
|
|
@ -5,29 +5,44 @@ use {
|
|||
sea_orm::{prelude::*, QueryOrder},
|
||||
};
|
||||
|
||||
use crate::entities::snapshot;
|
||||
|
||||
mod create;
|
||||
|
||||
pub use create::create;
|
||||
pub use crate::entities::snapshot::{
|
||||
ActiveModel as SnapshotActiveModel, Column as SnapshotColumn,
|
||||
Entity as SnapshotEntity, Model as SnapshotModel,
|
||||
};
|
||||
|
||||
/// Get a snapshot for a given date.
|
||||
pub async fn get_by_date(
|
||||
db: &DatabaseConnection,
|
||||
date: ChronoDate,
|
||||
) -> Result<Option<snapshot::Model>> {
|
||||
let existing = snapshot::Entity::find()
|
||||
.filter(snapshot::Column::Date.eq(date))
|
||||
.order_by_desc(snapshot::Column::Date)
|
||||
.one(db)
|
||||
.await?;
|
||||
impl SnapshotModel {
|
||||
/// Get a snapshot for a given date.
|
||||
pub async fn get_by_date(
|
||||
db: &DatabaseConnection,
|
||||
date: ChronoDate,
|
||||
) -> Result<Option<Self>> {
|
||||
let existing = SnapshotEntity::find()
|
||||
.filter(SnapshotColumn::Date.eq(date))
|
||||
.order_by_desc(SnapshotColumn::Date)
|
||||
.one(db)
|
||||
.await?;
|
||||
|
||||
Ok(existing)
|
||||
}
|
||||
|
||||
/// Get all snapshots.
|
||||
pub async fn get_all(db: &DatabaseConnection) -> Result<Vec<snapshot::Model>> {
|
||||
let snapshots = snapshot::Entity::find().all(db).await?;
|
||||
|
||||
Ok(snapshots)
|
||||
Ok(existing)
|
||||
}
|
||||
|
||||
/// Get all snapshots.
|
||||
pub async fn get_all(db: &DatabaseConnection) -> Result<Vec<Self>> {
|
||||
let snapshots = SnapshotEntity::find().all(db).await?;
|
||||
|
||||
Ok(snapshots)
|
||||
}
|
||||
|
||||
/// Get the most recent snapshot.
|
||||
pub async fn get_most_recent(
|
||||
db: &DatabaseConnection,
|
||||
) -> Result<Option<Self>> {
|
||||
let snapshot = SnapshotEntity::find()
|
||||
.order_by_desc(SnapshotColumn::Date)
|
||||
.one(db)
|
||||
.await?;
|
||||
|
||||
Ok(snapshot)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -7,6 +7,7 @@
|
|||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>{{ page_title }}</title>
|
||||
<link rel="stylesheet" href="/css/modern-normalize.css">
|
||||
<link rel="stylesheet" href="/css/common.css">
|
||||
{% block head %}{% endblock %}
|
||||
</head>
|
||||
|
||||
|
|
|
@ -0,0 +1,43 @@
|
|||
{% extends "base.html" %}
|
||||
|
||||
{% block head %}
|
||||
<link rel="stylesheet" href="/css/index.css">
|
||||
{% endblock %}
|
||||
|
||||
{% block body %}
|
||||
<header class="page-header">
|
||||
<h1>Tildes Statistics</h1>
|
||||
</header>
|
||||
|
||||
<main class="page-main">
|
||||
<h2>General</h2>
|
||||
|
||||
<p>
|
||||
There are currently an
|
||||
<abbr title="Based on the group with the highest subscriber count.">estimated</abbr>
|
||||
<span class="underline">{{ user_count }}</span>
|
||||
registered users on Tildes.
|
||||
</p>
|
||||
|
||||
<img src="/charts/user-count.svg" alt="User Count Chart">
|
||||
</main>
|
||||
|
||||
<footer class="page-footer">
|
||||
<p>
|
||||
Last generated on
|
||||
<time class="underline" datetime="{{ today }}">{{- today -}}</time>.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
© Code
|
||||
<a href="https://git.bauke.xyz/Bauke/tildes-statistics">AGPL-3.0-or-later</a>,
|
||||
charts & data
|
||||
<a href="https://creativecommons.org/licenses/by-nc/4.0/">CC BY-NC 4.0</a>.
|
||||
</p>
|
||||
|
||||
<p class="bold">
|
||||
Consider joining <a href="https://tildes.net">Tildes</a>, a non-profit
|
||||
community site driven by its users' interests.
|
||||
</p>
|
||||
</footer>
|
||||
{% endblock %}
|
|
@ -1 +1,44 @@
|
|||
//! All HTML templates.
|
||||
|
||||
use {
|
||||
askama::Template,
|
||||
async_std::{fs::write, path::PathBuf},
|
||||
chrono::NaiveDate,
|
||||
color_eyre::Result,
|
||||
};
|
||||
|
||||
use crate::utilities::today;
|
||||
|
||||
/// The template for the home page.
|
||||
#[derive(Template)]
|
||||
#[template(path = "index.html")]
|
||||
pub struct HomeTemplate {
|
||||
/// The string for the `<title>` element.
|
||||
pub page_title: String,
|
||||
|
||||
/// The date of today's snapshot.
|
||||
pub today: NaiveDate,
|
||||
|
||||
/// The user count from the group with the most subscribers.
|
||||
pub user_count: String,
|
||||
}
|
||||
|
||||
impl HomeTemplate {
|
||||
/// Create a new [`HomeTemplate`].
|
||||
pub fn new(user_count: Option<i64>) -> Self {
|
||||
Self {
|
||||
page_title: "Tildes Statistics".to_string(),
|
||||
today: today(),
|
||||
user_count: user_count
|
||||
.map(|n| n.to_string())
|
||||
.unwrap_or_else(|| "unknown".to_string()),
|
||||
}
|
||||
}
|
||||
|
||||
/// Render the template and write it to file.
|
||||
pub async fn render_to_file(&self, parent: &PathBuf) -> Result<()> {
|
||||
write(parent.join("index.html"), self.render()?).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue