Compare commits

..

No commits in common. "7109b3435d61e8ec1bc8f69fdf7e9d53276200cc" and "35d9135e07ac6b9faec00067e5041e22dec26015" have entirely different histories.

26 changed files with 1603 additions and 2101 deletions

3276
Cargo.lock generated

File diff suppressed because it is too large Load diff

52
flake.lock generated
View file

@ -1,12 +1,30 @@
{
"nodes": {
"flake-utils": {
"inputs": {
"systems": "systems"
},
"locked": {
"lastModified": 1681202837,
"narHash": "sha256-H+Rh19JDwRtpVPAWp64F+rlEtxUWBAQW28eAi3SRSzg=",
"owner": "numtide",
"repo": "flake-utils",
"rev": "cfacdce06f30d2b68473a46042957675eebb3401",
"type": "github"
},
"original": {
"owner": "numtide",
"repo": "flake-utils",
"type": "github"
}
},
"nixpkgs": {
"locked": {
"lastModified": 1742889210,
"narHash": "sha256-hw63HnwnqU3ZQfsMclLhMvOezpM7RSB0dMAtD5/sOiw=",
"lastModified": 1693377291,
"narHash": "sha256-vYGY9bnqEeIncNarDZYhm6KdLKgXMS+HA2mTRaWEc80=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "698214a32beb4f4c8e3942372c694f40848b360d",
"rev": "e7f38be3775bab9659575f192ece011c033655f0",
"type": "github"
},
"original": {
@ -18,11 +36,11 @@
},
"nixpkgs_2": {
"locked": {
"lastModified": 1736320768,
"narHash": "sha256-nIYdTAiKIGnFNugbomgBJR+Xv5F1ZQU+HfaBqJKroC0=",
"lastModified": 1681358109,
"narHash": "sha256-eKyxW4OohHQx9Urxi7TQlFBTDWII+F+x2hklDOQPB50=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "4bc9c909d9ac828a039f288cf872d16d38185db8",
"rev": "96ba1c52e54e74c3197f4d43026b3f3d92e83ff9",
"type": "github"
},
"original": {
@ -40,14 +58,15 @@
},
"rust-overlay": {
"inputs": {
"flake-utils": "flake-utils",
"nixpkgs": "nixpkgs_2"
},
"locked": {
"lastModified": 1743042789,
"narHash": "sha256-yPlxN0r3pQjUIwyX/qeWSTdpHjWy/AfmM0PK1bYkO18=",
"lastModified": 1693447852,
"narHash": "sha256-K9npbs4S6+r51vpiElJi+0vwbAeftCAcOGbot/PCBnQ=",
"owner": "oxalica",
"repo": "rust-overlay",
"rev": "b4d2dee9d16e7725b71969f28862ded3a94a7934",
"rev": "40e851593ef4f9f8cd0b69c8cae7b722b9953a23",
"type": "github"
},
"original": {
@ -55,6 +74,21 @@
"repo": "rust-overlay",
"type": "github"
}
},
"systems": {
"locked": {
"lastModified": 1681028828,
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
"owner": "nix-systems",
"repo": "default",
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
"type": "github"
},
"original": {
"owner": "nix-systems",
"repo": "default",
"type": "github"
}
}
},
"root": "root",

View file

@ -6,24 +6,24 @@ edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
accept-language = "3.1"
chrono = { version = "0.4", features = ["serde"] }
clap = "4.5"
comrak = "0.37"
derive_more = { version = "2.0", features = ["full"] }
dotenv = "0.15"
gettext = "0.4"
gh-emoji = "1.0"
lazy_static = "1.5.0"
poise = "0.5" # TODO: update to 0.6
r2d2 = "0.8"
r2d2_sqlite = "0.27"
regex = "1.11"
reqwest = "0.12"
rocket = { version = "0.5", features = ["secrets", "json"] }
rocket_dyn_templates = { version = "0.2", features = ["tera"] }
rusqlite = { version = "0.34", features = ["chrono"] }
serde = "1.0"
serde_json = "1.0"
serde_yaml = "0.9.34" # deprecated
tokio = { version = "1.44", features = ["macros", "rt-multi-thread"] }
accept-language = "2.0.0"
chrono = { version = "0.4.28", features = ["serde"] }
clap = "4.4.2"
comrak = "0.18.0"
derive_more = "0.99.17"
dotenv = "0.15.0"
gettext = "0.4.0"
gh-emoji = "1.0.7"
poise = "0.5.5"
r2d2 = "0.8.10"
r2d2_sqlite = "0.22.0"
regex = "1.9.4"
reqwest = "0.11.20"
rocket = { version = "=0.5.0-rc.3", features = ["secrets", "json"] }
rocket_contrib = { version = "0.4.11", features = ["templates"] }
rocket_dyn_templates = { version = "0.1.0-rc.3", features = ["tera"] }
rusqlite = { version = "0.29.0", features = ["chrono"] }
serde = "1.0.188"
serde_json = "1.0.105"
serde_yaml = "0.9.25"
tokio = { version = "1.32.0", features = ["macros", "rt-multi-thread"] }

View file

@ -11,7 +11,6 @@ use std::{
use gettext::Catalog;
#[derive(From, Debug)]
#[allow(dead_code)]
pub enum LoadCatalogsError {
Io(std::io::Error),
Parse(gettext::Error),

View file

@ -4,12 +4,11 @@ extern crate rocket;
use clap::{Parser, Subcommand};
use poise::serenity_prelude::Http;
use rocket::{
fs::{relative, FileServer}, Ignite, Rocket
fs::{relative, FileServer},
Ignite, Rocket,
};
use rocket_dyn_templates::{tera, Template};
use tokio::time::sleep;
use std::{collections::HashMap, env, time::Duration};
use lazy_static::lazy_static;
use std::{collections::HashMap, env};
mod models;
use models::{Database, Settings};
@ -62,15 +61,6 @@ async fn main() {
}
},
None => {
tokio::task::spawn(async {
const INTERVAL: Duration = Duration::from_millis(60 * 60 * 1000); // every hour
loop {
sleep(INTERVAL).await;
if let Err(err) = DATABASE.refresh_users(&HTTP).await {
error!("{:?}", err);
}
}
});
rocket().await.expect("Failed to launch rocket");
}
}
@ -85,12 +75,9 @@ fn http() -> Http {
Http::new(&token)
}
lazy_static! {
static ref DATABASE: Database = load_database();
static ref HTTP: Http = http();
}
async fn rocket() -> Result<Rocket<Ignite>, rocket::Error> {
let http = http();
let config = rocket::Config::figment().merge(("port", 1313)).merge((
"secret_key",
env::var("SECRET")
@ -98,8 +85,9 @@ async fn rocket() -> Result<Rocket<Ignite>, rocket::Error> {
));
rocket::custom(config)
.manage(Settings::new(&HTTP).await.unwrap())
// .manage(http)
.manage(Settings::new(&http).await.unwrap())
.manage(http)
.manage(load_database())
.mount(
"/",
routes![

View file

@ -2,7 +2,7 @@ use std::collections::HashSet;
use std::{collections::HashMap, fs::File, io::Read, path::Path};
use derive_more::From;
use poise::serenity_prelude::{Http, SerenityError, StatusCode};
use poise::serenity_prelude::{Http, SerenityError};
use r2d2::{Pool, PooledConnection};
use r2d2_sqlite::rusqlite::{self, params};
use r2d2_sqlite::SqliteConnectionManager;
@ -20,15 +20,13 @@ pub struct Database {
const DATABASE_FILENAME: &str = "database.db";
#[derive(From, Debug)]
#[allow(dead_code)]
pub enum DatabaseError {
Rusqlite(rusqlite::Error),
Pool(r2d2::Error),
}
#[derive(From, Debug)]
#[allow(dead_code)]
pub enum GenericError {
pub enum LoadLegacyError {
Database(DatabaseError),
Serenity(SerenityError),
}
@ -84,7 +82,7 @@ impl Database {
.is_ok())
}
pub async fn load_legacy(&self, http: &Http) -> std::result::Result<(), GenericError> {
pub async fn load_legacy(&self, http: &Http) -> std::result::Result<(), LoadLegacyError> {
let latest_challenge = get_challenge_number();
// HashMap of archived users that are no longer sharing a server with 字ちゃん
// Their historical usernames and discriminators will be used
@ -126,7 +124,12 @@ impl Database {
},
);
}
Ok(user) => self.insert_user(&user)?,
Ok(user) => {
conn.execute(
"INSERT INTO User(id, name, discriminator, avatar, deleted) VALUES (?1, ?2, ?3, ?4, ?5)",
params![user.id, user.name, user.discriminator, user.avatar, user.deleted]
).map_err(DatabaseError::Rusqlite)?;
}
Err(error) if error.to_string().eq("Unknown User") => {
// This will also be called in the case of an invalid user ID
println!("Failed to fetch user {id}, adding to archive");
@ -140,7 +143,7 @@ impl Database {
},
);
}
Err(error) => return Err(GenericError::Serenity(error)),
Err(error) => return Err(LoadLegacyError::Serenity(error)),
},
};
}
@ -262,53 +265,11 @@ impl Database {
.collect()
}
fn get_all_users(&self) -> Result<Vec<User>> {
let conn = self.conn()?;
let mut stmt = conn.prepare("SELECT id, name, discriminator, avatar, deleted FROM User")?;
let result: Vec<Result<User>> = stmt.query_map([], |row| Ok(User {
id: row.get(0)?,
name: row.get(1)?,
discriminator: row.get(2)?,
avatar: row.get(3)?,
deleted: row.get(4)?,
}))?.map(|result| match result {
Ok(user) => Ok(user),
Err(err) => Err(DatabaseError::Rusqlite(err)),
}).collect();
result.into_iter().collect()
}
pub fn insert_user(&self, user: &User) -> Result<()> {
self.conn()?.execute(
"INSERT OR REPLACE INTO User(id, name, discriminator, avatar, deleted) VALUES (?1, ?2, ?3, ?4, ?5)",
params![user.id, user.name, user.discriminator, user.avatar, user.deleted]
)?;
Ok(())
}
#[allow(dead_code)]
pub async fn refresh_users(&self, http: &Http) -> std::result::Result<(), GenericError> {
pub fn refresh_users(&self) -> rusqlite::Result<()> {
// Periodically refresh all changable user data (name, discriminator, avatar)
// Ideally this should run periodically.
let all_users = self.get_all_users()?;
for (i, user) in all_users.iter().enumerate() {
print!("User {i}/{}, ", all_users.len());
let updated = match User::fetch(http, user.id).await {
Ok(user) => user,
Err(SerenityError::Http(error)) if error.status_code().eq(&Some(StatusCode::NOT_FOUND)) => {
println!("not found");
continue;
},
Err(error) => panic!("{error}"),
};
if user.eq(&updated) {
println!("no change");
continue;
}
println!("updated");
self.insert_user(&updated)?;
}
Ok(())
todo!()
}
#[allow(dead_code, unused_variables)]

View file

@ -1,5 +1,5 @@
mod user;
pub use user::{SessionUser, User};
pub use user::{SessionUser, User, Username};
mod challenge;
pub use challenge::Challenge;

View file

@ -26,7 +26,6 @@ impl Settings {
}
#[derive(From, Debug)]
#[allow(dead_code)]
pub enum GetSettingsError {
Io(std::io::Error),
Deserialize(serde_yaml::Error),

View file

@ -21,7 +21,6 @@ where
}
#[derive(From, Debug)]
#[allow(dead_code)]
pub enum LegacySubmissionParseError {
BadAuthorId(std::num::ParseIntError),
}
@ -46,7 +45,7 @@ impl LegacySubmission {
.next()?
// Get last number
.split('-')
.next_back()?
.last()?
.parse()
.ok()
// Check if discriminator or timestamp, then convert

View file

@ -19,7 +19,7 @@ pub trait Username {
fn username(&self) -> String;
}
#[derive(Default, Deserialize, Debug, PartialEq, Eq)]
#[derive(Default, Deserialize, Debug)]
pub struct User {
#[serde(deserialize_with = "deserialize_id")]
pub id: u64,
@ -92,16 +92,11 @@ impl User {
}
),
// Archived user or user with no avatar, calculate default avatar
// https://www.reddit.com/r/discordapp/comments/au6v4e/comment/eh61dm6/
// https://docs.rs/serenity/0.11.5/serenity/model/user/struct.User.html#method.default_avatar_url
None => format!(
"https://cdn.discordapp.com/embed/avatars/{}.png",
// https://docs.rs/serenity/0.12.4/src/serenity/model/user.rs.html#805
if self.discriminator == 0 {
// New avatar system
((self.id >> 22) % 6) as u16
} else {
// Old avatar system
self.discriminator % 5
}
self.discriminator % 5
),
}
}
@ -147,12 +142,12 @@ impl SessionUser {
}
pub fn purge(cookies: &CookieJar<'_>) {
cookies.remove_private(Cookie::from(TOKEN_COOKIE));
cookies.remove_private(Cookie::from(USER_ID_COOKIE));
cookies.remove_private(Cookie::from(USER_NAME_COOKIE));
cookies.remove_private(Cookie::from(USER_DISCRIMINATOR_COOKIE));
cookies.remove_private(Cookie::from(USER_AVATAR_COOKIE));
cookies.remove(Cookie::from(TOKEN_EXPIRE_COOKIE));
cookies.remove_private(Cookie::named(TOKEN_COOKIE));
cookies.remove_private(Cookie::named(USER_ID_COOKIE));
cookies.remove_private(Cookie::named(USER_NAME_COOKIE));
cookies.remove_private(Cookie::named(USER_DISCRIMINATOR_COOKIE));
cookies.remove_private(Cookie::named(USER_AVATAR_COOKIE));
cookies.remove(Cookie::named(TOKEN_EXPIRE_COOKIE));
}
fn from_cookies(cookies: &CookieJar<'_>) -> Option<Self> {
@ -174,14 +169,14 @@ impl SessionUser {
.get(TOKEN_EXPIRE_COOKIE)
.map(|expire| expire.value().parse::<i64>())
.and_then(Result::ok)
.is_none_or(|timestamp| Utc::now().timestamp() >= timestamp)
.map_or(true, |timestamp| Utc::now().timestamp() >= timestamp)
{
cookies.remove_private(Cookie::from(TOKEN_COOKIE));
cookies.remove_private(Cookie::from(USER_ID_COOKIE));
cookies.remove_private(Cookie::from(USER_NAME_COOKIE));
cookies.remove_private(Cookie::from(USER_DISCRIMINATOR_COOKIE));
cookies.remove_private(Cookie::from(USER_AVATAR_COOKIE));
cookies.remove(Cookie::from(TOKEN_EXPIRE_COOKIE));
cookies.remove_private(Cookie::named(TOKEN_COOKIE));
cookies.remove_private(Cookie::named(USER_ID_COOKIE));
cookies.remove_private(Cookie::named(USER_NAME_COOKIE));
cookies.remove_private(Cookie::named(USER_DISCRIMINATOR_COOKIE));
cookies.remove_private(Cookie::named(USER_AVATAR_COOKIE));
cookies.remove(Cookie::named(TOKEN_EXPIRE_COOKIE));
return Ok(None);
}
Ok(Some(user))
@ -189,7 +184,6 @@ impl SessionUser {
}
#[derive(From, Debug)]
#[allow(dead_code)]
pub enum GetUserError {
Reqwest(reqwest::Error),
Deserialize(serde_json::Error),

View file

@ -6,8 +6,8 @@ use rocket_dyn_templates::{context, Template};
use crate::{
cookies::LANG_COOKIE,
i18n::DEFAULT as DEFAULT_LANG,
models::{Challenge, SessionUser, Settings},
utils::AcceptLanguage, DATABASE,
models::{Challenge, Database, SessionUser, Settings},
utils::AcceptLanguage,
};
#[get("/<challenge>")]
@ -15,9 +15,10 @@ pub async fn get_challenge(
challenge: u32,
cookies: &CookieJar<'_>,
settings: &State<Settings>,
database: &State<Database>,
accept_language: AcceptLanguage,
) -> Template {
let (submissions, users) = DATABASE.get_challenge_user_data(challenge).unwrap();
let (submissions, users) = database.get_challenge_user_data(challenge).unwrap();
Template::render(
"challenge",
context! {

View file

@ -1,7 +1,7 @@
use poise::serenity_prelude::Http;
use rocket::response::stream::{Event, EventStream};
use rocket::{http::CookieJar, State};
use crate::HTTP;
use crate::{cookies::user::USER_ID_COOKIE, models::Settings};
// TODO: Incrementally send guilds
@ -9,6 +9,7 @@ use crate::{cookies::user::USER_ID_COOKIE, models::Settings};
pub async fn get_guilds<'a>(
cookies: &'a CookieJar<'_>,
settings: &'a State<Settings>,
http: &'a State<Http>,
) -> EventStream![Event + 'a] {
// EventStream![] is shorthand for EventStream[Event],
// but we need to pass in a lifetime parameter.
@ -27,7 +28,7 @@ pub async fn get_guilds<'a>(
if guild.hidden {
continue;
}
yield Event::data(format!("{},{}", guild.id, HTTP.get_member(guild.id, user_id).await.is_ok()));
yield Event::data(format!("{},{}", guild.id, http.get_member(guild.id, user_id).await.is_ok()));
}
}
}

View file

@ -9,8 +9,8 @@ use rocket_dyn_templates::{context, Template};
use crate::{
cookies::LANG_COOKIE,
i18n::DEFAULT as DEFAULT_LANG,
models::{DatabaseError, SessionUser, Settings},
utils::AcceptLanguage, DATABASE,
models::{Database, DatabaseError, SessionUser, Settings},
utils::AcceptLanguage,
};
#[get("/users/<user>")]
@ -18,9 +18,10 @@ pub async fn get_user(
user: String,
cookies: &CookieJar<'_>,
settings: &State<Settings>,
database: &State<Database>,
accept_language: AcceptLanguage,
) -> Result<Template, Status> {
let profile_user = match DATABASE.get_user_by_name(&user) {
let profile_user = match database.get_user_by_name(&user) {
Ok(profile_user) => profile_user,
Err(DatabaseError::Rusqlite(rusqlite::Error::QueryReturnedNoRows)) => {
return Err(Status::NotFound)
@ -34,7 +35,7 @@ pub async fn get_user(
"user",
context! {
profile_user,
submissions: DATABASE.get_submissions_by_user_name(&user).unwrap(),
submissions: database.get_submissions_by_user_name(&user).unwrap(),
settings: settings.deref(),
lang: cookies
.get(LANG_COOKIE)

View file

@ -29,7 +29,7 @@ pub async fn post_login(login: Form<Login<'_>>, cookies: &CookieJar<'_>) -> Redi
.timestamp()
.to_string(),
));
cookies.remove(Cookie::from(WELCOMED_COOKIE));
cookies.remove(Cookie::named(WELCOMED_COOKIE));
}
Redirect::to("/")
}

View file

@ -1,19 +1,20 @@
use std::ops::Deref;
use rocket::http::CookieJar;
use poise::serenity_prelude::Http;
use rocket::{http::CookieJar, State};
use crate::{models::SessionUser, HTTP};
use crate::models::SessionUser;
#[get("/testing")]
pub async fn testing(cookies: &CookieJar<'_>) -> String {
pub async fn testing(cookies: &CookieJar<'_>, http: &State<Http>) -> String {
// Get logged in user's join date in 字ちゃん server
format!(
"{:?}",
HTTP.get_guild(814700630958276649)
http.get_guild(814700630958276649)
.await
.expect("Failed to get testing guild")
.member(
HTTP.deref(),
http.deref(),
SessionUser::get(cookies)
.await
.expect("Failed to get logged in user data")

View file

@ -11,10 +11,6 @@ pub fn furigana_to_html(text: &str) -> String {
// so { needs to be escaped as \{
// Curly brace literals \{ need to have their backslash escaped as \\{
// TODO: Modify so <span lang="ja"> only wraps continuous sections of furigana
if text.trim().is_empty() {
// Don't return empty <span lang="ja"> if text is empty
return String::new();
}
let re = Regex::new(r"\[([^\]]*)\]\{([^\\}]*)}").unwrap();
format!(
"<span lang=\"ja\">{}</span>",

View file

@ -353,7 +353,6 @@ const KYUJITAI: &[(char, char)] = &[
pub trait Kyujitai {
fn to_kyujitai(&self) -> String;
#[allow(dead_code)]
fn to_shinjitai(&self) -> String;
}

View file

@ -1,5 +1,5 @@
@use "sass:color"
$bg: #fefdfa
$bg0: #fefefa
$fg: #1a1110
$bg: #FFFDF3
$bg0: #{color.adjust($bg, $lightness: -5%, $hue: -7deg, $saturation: -50%)}
$fg: #011627

View file

@ -1,38 +1,6 @@
@use "_theme" as *
$wave: 10px
$wave_scale: 2
#challenge
box-shadow: 0px 0px 10px 5px rgba(0, 0, 0, 0.125)
z-index: 1
background: #3f313a
padding: 1em
iframe
background: black
width: 100%
aspect-ratio: 16/9
margin: 0
#challenge-description, footer
padding: 1em
#challenge-content
padding: 1em
padding-top: calc(1em - $wave)
padding-bottom: calc(1em - $wave)
font-size: 1.25em
$shadow: #1a1110
$shadow-size: 1px
text-shadow: calc(-1 * $shadow-size) calc(-1 * $shadow-size) 0 $shadow, calc(-1 * $shadow-size) 0px 0 $shadow, calc(-1 * $shadow-size) $shadow-size 0 $shadow, 0px $shadow-size 0 $shadow, $shadow-size $shadow-size 0 $shadow, $shadow-size 0px 0 $shadow, $shadow-size calc(-1 * $shadow-size) 0 $shadow, 0px calc(-1 * $shadow-size) 0 $shadow;
background: #23191e
p
margin: 0
a:hover
text-decoration: none
border-bottom: 3px solid
a
&.noun
color: #a6e3a1
@ -50,22 +18,4 @@ a
color: #f9e2af
&.phrase
color: #fab387
.wave
--size: #{$wave}
--R: calc(var(--size)*1.28)
mask: radial-gradient(var(--R) at 50% calc(1.8*var(--size)),#000 99%,#0000 101%) calc(50% - 2*var(--size)) 0/calc(4*var(--size)) 100%, radial-gradient(var(--R) at 50% calc(-.8*var(--size)),#0000 99%,#000 101%) 50% var(--size)/calc(4*var(--size)) 100% repeat-x
background: #23191e
$height: calc($wave * 2)
height: $height
flex-shrink: 0
&.wave-bottom
transform: scaleY(-1) scaleX($wave_scale)
&.wave-top
transform: scaleX($wave_scale) translateX($wave * $wave_scale)
margin-top: -$height
pointer-events: none
color: #fab387

View file

@ -12,13 +12,10 @@ body
height: 100vh
box-sizing: border-box
padding: 2em
background: #2b2028
color: var(--bg)
background: $bg0
color: var(--fg)
font-size: 1.25em
b, strong
color: white
input[type=number]
background: var(--bg)
color: var(--fg)
@ -30,7 +27,6 @@ input[type=number]
appearance: textfield
a
color: #9fe2bf
text-decoration: none
&:hover
@ -42,30 +38,5 @@ a
h1
text-align: center
:first-child
margin-top: 0
margin-bottom: 0
.avatar
border-radius: 100%
display: inline
height: 2em
vertical-align: middle
background: #5e595d
button
border: none
color: inherit
font: inherit
background: #3f313a
border: 3px solid #1a1110
border-radius: 0.25em
box-shadow: 0px 2px #1a1110
cursor: pointer
&:active
box-shadow: none
transform: translateY(2px)
background: transparent
color: #3f313a
&:first-child
margin-top: 0

View file

@ -5,10 +5,8 @@ nav
--fg: #{$bg}
background: var(--bg)
color: var(--fg)
box-shadow: 0 0 8px 2px rgba(0, 0, 0, 0.5)
box-shadow: 0 0 8px 2px rgba(0, 0, 0, 0.125)
display: flex
align-items: center
z-index: 2
.dropdown
position: relative
@ -53,8 +51,5 @@ nav
cursor: pointer
.icon
height: 1em
vertical-align: middle
nav .icon
height: 1.5em
height: 1.5em
vertical-align: middle

View file

@ -10,6 +10,7 @@
background: var(--bg)
box-shadow: 0 0 16px rgba(0, 0, 0, 0.125)
border: none
border-radius: 4px
#content
width: 100%
@ -18,7 +19,7 @@
grid-template-areas: "nav nav" "challenge submissions"
grid-template-columns: 30em auto
grid-template-rows: min-content auto
border: 4px solid $fg
overflow: hidden
#nav
grid-area: nav
@ -30,10 +31,10 @@
grid-area: submissions
#challenge, #submissions
overflow-x: hidden;
overflow-y: scroll
overflow: scroll
display: flex
flex-direction: column
gap: 1em
footer
margin-top: auto

View file

@ -1,8 +1,6 @@
@use "_theme" as *
#submissions
background: #3f313a
grid-area: submissions
background: rgba(0, 0, 0, 0.125)
padding: 1em
& > div
@ -15,9 +13,7 @@
gap: $gap
& > figure
background: #5c4f57
box-shadow: 0 0 8px 2px rgba(0, 0, 0, 0.125)
border-radius: 0.5em
background: rgba(0, 0, 0, 0.125)
padding: $gap
width: 100%
box-sizing: border-box
@ -27,7 +23,6 @@
text-align: center
& > img
border-radius: 0.5em
width: 100%
cursor: pointer

View file

@ -16,7 +16,7 @@
<div id="content">
<nav id="nav">{% block nav %}{% include "nav" %}{% endblock %}</nav>
<div id="challenge">
{% block content %}{% endblock %}
<div>{% block content %}{% endblock %}</div>
<footer>
{% block content_copyright %}
Copyright {% include "copyright-years" %} Tegaki Tuesday. All rights reserved. 字ちゃん mascot art by <a href="https://twitter.com/bellumela" target="_blank">@bellumela</a>.
@ -55,11 +55,7 @@
{% if profile_user %}
<a href="/{{ submission.challenge }}">#{{ submission.challenge }}</a>
{% else %}
{% if not author.deleted %}
<a href="/users/{{ author.name }}" target="_blank">
{% endif %}
<img src="{{ author.avatar }}" alt="{{ author.username }}'s avatar" class="avatar"> {{ author.username }}
{% if author.deleted %} (deleted account){% else %}</a>{% endif %}
{% if not author.deleted %}<a href="/users/{{ author.name }}" target="_blank">{% endif %}{{ author.username }}{% if author.deleted %} (deleted account){% else %}</a>{% endif %}
{% endif %}
</figcaption>
</figure>

View file

@ -1,11 +1,7 @@
{% extends "base" %}
{% block content %}
{% if content.youtube %}
<iframe width="560" height="315" src="https://www.youtube.com/embed/{{ content.youtube }}" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerpolicy="strict-origin-when-cross-origin" allowfullscreen></iframe>
{% endif %}
<div class="wave wave-top"></div>
<div id="challenge-content">
<h1>Welcome to Tegaki Tuesday #{{ challenge }}!</h1>
{% if content.japanese %}
<div lang="ja">
<script>
@ -19,19 +15,7 @@
kyujitai = !kyujitai;
}
</script>
<a href="https://en.wikipedia.org/wiki/Ky%C5%ABjitai" target="_blank">
<svg class="icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" class="size-6">
<path fill-rule="evenodd" d="M2.25 12c0-5.385 4.365-9.75 9.75-9.75s9.75 4.365 9.75 9.75-4.365 9.75-9.75 9.75S2.25 17.385 2.25 12Zm11.378-3.917c-.89-.777-2.366-.777-3.255 0a.75.75 0 0 1-.988-1.129c1.454-1.272 3.776-1.272 5.23 0 1.513 1.324 1.513 3.518 0 4.842a3.75 3.75 0 0 1-.837.552c-.676.328-1.028.774-1.028 1.152v.75a.75.75 0 0 1-1.5 0v-.75c0-1.279 1.06-2.107 1.875-2.502.182-.088.351-.199.503-.331.83-.727.83-1.857 0-2.584ZM12 18a.75.75 0 1 0 0-1.5.75.75 0 0 0 0 1.5Z" clip-rule="evenodd" />
</svg>
</a>
<button onclick="kyujitaiToggle(); this.innerHTML = kyujitai ? '新字体' : '旧字体'">旧字体</button>
<button>
<svg class="icon" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="size-6">
<path stroke-linecap="round" stroke-linejoin="round" d="M19.5 12c0-1.232-.046-2.453-.138-3.662a4.006 4.006 0 0 0-3.7-3.7 48.678 48.678 0 0 0-7.324 0 4.006 4.006 0 0 0-3.7 3.7c-.017.22-.032.441-.046.662M19.5 12l3-3m-3 3-3-3m-12 3c0 1.232.046 2.453.138 3.662a4.006 4.006 0 0 0 3.7 3.7 48.656 48.656 0 0 0 7.324 0 4.006 4.006 0 0 0 3.7-3.7c.017-.22.032-.441.046-.662M4.5 12l3 3m-3-3-3 3" />
</svg>
</button>
<br>
<br>
{% for line in content.japanese %}
{% for subline in line %}
<p>
@ -51,9 +35,6 @@
{% endfor %}
</div>
{% endif %}
</div>
<div class="wave wave-bottom"></div>
<div id="challenge-description">
{{ content.text | safe }}
{% if content.translation %}
<p>
@ -71,5 +52,4 @@
{%- if content.suggester -%}
<p><em>This challenge was suggested by <strong>{{ content.suggester }}</strong> using the <code>-h suggest</code> command.</code></em></p>
{%- endif -%}
</div>
{% endblock %}

View file

@ -2,8 +2,7 @@
{% block content %}
<div style="text-align: center">
<br>
<img src="{{ profile_user.avatar }}" alt="{{ profile_user.username }}" class="avatar" style="font-size: 6em">
<img src="{{ profile_user.avatar }}" alt="{{ profile_user.username }}" style="border-radius: 100%">
<h1><a href="https://discord.com/users/{{ profile_user.id }}">{{ profile_user.username }}</a>'s submissions</h1>
{{ submissions | length }} images
</div>