Basic cours support #44

Merged
Anri merged 67 commits from cours into main 2024-04-01 18:11:49 +02:00
16 changed files with 173 additions and 138 deletions
Showing only changes of commit 349b822361 - Show all commits

View file

@ -18,6 +18,7 @@ WORKDIR /app
COPY --from=builder /usr/local/cargo/bin/ewp /app/ewp COPY --from=builder /usr/local/cargo/bin/ewp /app/ewp
COPY --from=builder /usr/src/ewp/LICENSE /app/LICENSE COPY --from=builder /usr/src/ewp/LICENSE /app/LICENSE
COPY --from=builder /usr/src/ewp/README.md /app/README.md
COPY --from=builder /usr/src/ewp/static /app/static COPY --from=builder /usr/src/ewp/static /app/static
COPY --from=builder /usr/src/ewp/templates /app/templates COPY --from=builder /usr/src/ewp/templates /app/templates

View file

@ -6,4 +6,4 @@ Easy WebPage generator
[![status-badge](https://ci.mylloon.fr/api/badges/Anri/mylloon.fr/status.svg)](https://ci.mylloon.fr/Anri/mylloon.fr) [![status-badge](https://ci.mylloon.fr/api/badges/Anri/mylloon.fr/status.svg)](https://ci.mylloon.fr/Anri/mylloon.fr)
- See [issues](https://git.mylloon.fr/Anri/mylloon.fr/issues) - See [issues](https://git.mylloon.fr/Anri/mylloon.fr/issues)
- See [documentation](./Documentation.md) - See [documentation](https://git.mylloon.fr/Anri/mylloon.fr/src/branch/main/Documentation.md)

View file

@ -69,13 +69,20 @@ impl FileConfig {
} }
} }
// Paths where files are stored
#[derive(Clone, Debug)]
pub struct Locations {
pub static_dir: String,
pub data_dir: String,
}
/// Configuration used internally in the app /// Configuration used internally in the app
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
pub struct Config { pub struct Config {
/// Information given in the config file /// Information given in the config file
pub fc: FileConfig, pub fc: FileConfig,
/// Location where the static files are stored /// Location where the static files are stored
pub static_location: String, pub locations: Locations,
/// Informations about templates /// Informations about templates
pub tmpl: Template, pub tmpl: Template,
} }
@ -110,7 +117,10 @@ pub fn get_config(file_path: &str) -> Config {
Config { Config {
fc: internal_config.to_owned(), fc: internal_config.to_owned(),
static_location: format!("{}/{}", files_root, static_dir), locations: Locations {
static_dir: format!("{}/{}", files_root, static_dir),
data_dir: String::from("data"),
},
tmpl: Template { tmpl: Template {
directory: format!("{}/{}", files_root, templates_dir), directory: format!("{}/{}", files_root, templates_dir),
app_name: internal_config.app_name.unwrap(), app_name: internal_config.app_name.unwrap(),

View file

@ -60,7 +60,7 @@ async fn main() -> Result<()> {
.service(portfolio::page) .service(portfolio::page)
.service(setup::page) .service(setup::page)
.service(web3::page) .service(web3::page)
.service(Files::new("/", config.static_location.to_owned())) .service(Files::new("/", config.locations.static_dir.to_owned()))
.default_service(web::to(not_found::page)) .default_service(web::to(not_found::page))
}) })
.bind(addr)? .bind(addr)?

View file

@ -6,10 +6,7 @@ use ramhorns::Content;
use serde::{Deserialize, Deserializer}; use serde::{Deserialize, Deserializer};
use std::fs; use std::fs;
/// Regular markdown files, no metadata /// Metadata for blog posts
#[derive(Content, Debug, Default, Deserialize)]
pub struct FileNoMetadata {}
#[derive(Content, Debug, Default, Deserialize)] #[derive(Content, Debug, Default, Deserialize)]
pub struct FileMetadataBlog { pub struct FileMetadataBlog {
pub title: Option<String>, pub title: Option<String>,
@ -20,39 +17,7 @@ pub struct FileMetadataBlog {
pub toc: Option<bool>, pub toc: Option<bool>,
} }
#[derive(Content, Debug, Default, Deserialize)] /// A tag, related to post blog
pub struct FileMetadataContact {
pub title: String,
pub custom: Option<bool>,
pub user: Option<String>,
pub link: Option<String>,
pub newtab: Option<bool>,
pub description: Option<String>,
}
#[derive(Content, Debug, Default, Deserialize)]
pub struct FileMetadataPortfolio {
pub title: Option<String>,
pub link: Option<String>,
pub description: Option<String>,
pub language: Option<String>,
}
pub enum TypeFileMetadata {
Blog,
Contact,
Portfolio,
Cours,
}
#[derive(Content, Debug, Default, Deserialize)]
pub struct FileMetadata {
pub blog: Option<FileMetadataBlog>,
pub contact: Option<FileMetadataContact>,
pub portfolio: Option<FileMetadataPortfolio>,
pub cours: Option<FileNoMetadata>,
}
#[derive(Content, Debug, Clone)] #[derive(Content, Debug, Clone)]
pub struct Tag { pub struct Tag {
pub name: String, pub name: String,
@ -73,6 +38,55 @@ impl<'de> Deserialize<'de> for Tag {
} }
} }
/// Metadata for contact entry
#[derive(Content, Debug, Default, Deserialize)]
pub struct FileMetadataContact {
pub title: String,
pub custom: Option<bool>,
pub user: Option<String>,
pub link: Option<String>,
pub newtab: Option<bool>,
pub description: Option<String>,
}
/// Metadata for index page
#[derive(Content, Debug, Default, Deserialize)]
pub struct FileMetadataIndex {
pub name: Option<String>,
pub pronouns: Option<String>,
pub avatar: Option<String>,
pub avatar_caption: Option<String>,
}
/// Metadata for portfolio cards
#[derive(Content, Debug, Default, Deserialize)]
pub struct FileMetadataPortfolio {
pub title: Option<String>,
pub link: Option<String>,
pub description: Option<String>,
pub language: Option<String>,
}
/// List of available metadata types
pub enum TypeFileMetadata {
Blog,
Contact,
Generic,
Index,
Portfolio,
}
/// Structure who holds all the metadata the file have
/// Usually all fields are None except one
#[derive(Content, Debug, Default, Deserialize)]
pub struct FileMetadata {
pub blog: Option<FileMetadataBlog>,
pub contact: Option<FileMetadataContact>,
pub index: Option<FileMetadataIndex>,
pub portfolio: Option<FileMetadataPortfolio>,
}
/// Global metadata
#[derive(Content, Debug)] #[derive(Content, Debug)]
pub struct Metadata { pub struct Metadata {
pub info: FileMetadata, pub info: FileMetadata,
@ -81,6 +95,7 @@ pub struct Metadata {
pub syntax_highlight: bool, pub syntax_highlight: bool,
} }
/// File description
#[derive(Content, Debug)] #[derive(Content, Debug)]
pub struct File { pub struct File {
pub metadata: Metadata, pub metadata: Metadata,
@ -220,6 +235,7 @@ pub fn get_metadata<'a>(root: &'a AstNode<'a>, mtype: TypeFileMetadata) -> FileM
match root match root
.children() .children()
.find_map(|node| match &node.data.borrow().value { .find_map(|node| match &node.data.borrow().value {
// Extract metadata from frontmatter
NodeValue::FrontMatter(text) => Some(match mtype { NodeValue::FrontMatter(text) => Some(match mtype {
TypeFileMetadata::Blog => FileMetadata { TypeFileMetadata::Blog => FileMetadata {
blog: Some(deserialize_metadata(text)), blog: Some(deserialize_metadata(text)),
@ -238,18 +254,22 @@ pub fn get_metadata<'a>(root: &'a AstNode<'a>, mtype: TypeFileMetadata) -> FileM
..FileMetadata::default() ..FileMetadata::default()
} }
} }
TypeFileMetadata::Portfolio => FileMetadata { TypeFileMetadata::Generic => FileMetadata {
portfolio: Some(deserialize_metadata(text)),
..FileMetadata::default() ..FileMetadata::default()
}, },
TypeFileMetadata::Cours => FileMetadata { TypeFileMetadata::Index => FileMetadata {
cours: Some(deserialize_metadata(text)), index: Some(deserialize_metadata(text)),
..FileMetadata::default()
},
TypeFileMetadata::Portfolio => FileMetadata {
portfolio: Some(deserialize_metadata(text)),
..FileMetadata::default() ..FileMetadata::default()
}, },
}), }),
_ => None, _ => None,
}) { }) {
Some(data) => data, Some(data) => data,
// No metadata
None => match mtype { None => match mtype {
TypeFileMetadata::Blog => FileMetadata { TypeFileMetadata::Blog => FileMetadata {
blog: Some(FileMetadataBlog::default()), blog: Some(FileMetadataBlog::default()),
@ -259,12 +279,15 @@ pub fn get_metadata<'a>(root: &'a AstNode<'a>, mtype: TypeFileMetadata) -> FileM
contact: Some(FileMetadataContact::default()), contact: Some(FileMetadataContact::default()),
..FileMetadata::default() ..FileMetadata::default()
}, },
TypeFileMetadata::Portfolio => FileMetadata { TypeFileMetadata::Generic => FileMetadata {
portfolio: Some(FileMetadataPortfolio::default()),
..FileMetadata::default() ..FileMetadata::default()
}, },
TypeFileMetadata::Cours => FileMetadata { TypeFileMetadata::Index => FileMetadata {
cours: Some(FileNoMetadata::default()), index: Some(FileMetadataIndex::default()),
..FileMetadata::default()
},
TypeFileMetadata::Portfolio => FileMetadata {
portfolio: Some(FileMetadataPortfolio::default()),
..FileMetadata::default() ..FileMetadata::default()
}, },
}, },

View file

@ -1,6 +1,7 @@
use actix_web::{get, HttpResponse, Responder}; use actix_web::{get, HttpResponse, Responder};
use serde::Serialize; use serde::Serialize;
/// Response
#[derive(Serialize)] #[derive(Serialize)]
struct Info { struct Info {
unix_epoch: u32, unix_epoch: u32,

View file

@ -42,7 +42,7 @@ struct BlogIndexTemplate {
#[once(time = 60)] #[once(time = 60)]
fn build_index(config: Config) -> String { fn build_index(config: Config) -> String {
let mut posts = get_posts("data/blog"); let mut posts = get_posts(format!("{}/blog", config.locations.data_dir));
// Sort from newest to oldest // Sort from newest to oldest
posts.sort_by_cached_key(|p| (p.date.year, p.date.month, p.date.day)); posts.sort_by_cached_key(|p| (p.date.year, p.date.month, p.date.day));
@ -81,8 +81,8 @@ struct Post {
impl Post { impl Post {
// Fetch the file content // Fetch the file content
fn fetch_content(&mut self) { fn fetch_content(&mut self, data_dir: &str) {
let blog_dir = "data/blog"; let blog_dir = format!("{}/blog", data_dir);
let ext = ".md"; let ext = ".md";
if let Some(file) = read_file( if let Some(file) = read_file(
@ -102,8 +102,8 @@ impl Hash for Post {
} }
} }
fn get_posts(location: &str) -> Vec<Post> { fn get_posts(location: String) -> Vec<Post> {
let entries = match std::fs::read_dir(location) { let entries = match std::fs::read_dir(&location) {
Ok(res) => res Ok(res) => res
.flatten() .flatten()
.filter(|f| match f.path().extension() { .filter(|f| match f.path().extension() {
@ -190,7 +190,12 @@ async fn page(path: web::Path<(String,)>, config: web::Data<Config>) -> impl Res
fn build_post(file: String, config: Config) -> String { fn build_post(file: String, config: Config) -> String {
let mut post = None; let mut post = None;
let (infos, toc) = get_post(&mut post, file, config.fc.name.unwrap_or_default()); let (infos, toc) = get_post(
&mut post,
file,
config.fc.name.unwrap_or_default(),
config.locations.data_dir,
);
config.tmpl.render( config.tmpl.render(
"blog/post.html", "blog/post.html",
@ -206,8 +211,13 @@ fn build_post(file: String, config: Config) -> String {
) )
} }
fn get_post(post: &mut Option<File>, filename: String, name: String) -> (Infos, String) { fn get_post(
let blog_dir = "data/blog"; post: &mut Option<File>,
filename: String,
name: String,
data_dir: String,
) -> (Infos, String) {
let blog_dir = format!("{}/blog", data_dir);
let ext = ".md"; let ext = ".md";
*post = read_file( *post = read_file(
@ -268,7 +278,7 @@ async fn rss(config: web::Data<Config>) -> impl Responder {
#[once(time = 10800)] // 3h #[once(time = 10800)] // 3h
fn build_rss(config: Config) -> String { fn build_rss(config: Config) -> String {
let mut posts = get_posts("data/blog"); let mut posts = get_posts(format!("{}/blog", config.locations.data_dir));
// Sort from newest to oldest // Sort from newest to oldest
posts.sort_by_cached_key(|p| (p.date.year, p.date.month, p.date.day)); posts.sort_by_cached_key(|p| (p.date.year, p.date.month, p.date.day));
@ -315,7 +325,7 @@ fn build_rss(config: Config) -> String {
.iter_mut() .iter_mut()
.map(|p| { .map(|p| {
// Get post data // Get post data
p.fetch_content(); p.fetch_content(&config.locations.data_dir);
// Build item // Build item
Item { Item {

View file

@ -30,6 +30,7 @@ async fn page(config: web::Data<Config>) -> impl Responder {
Html(build_page(config.get_ref().to_owned())) Html(build_page(config.get_ref().to_owned()))
} }
/// Contact node
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
struct ContactLink { struct ContactLink {
service: String, service: String,
@ -38,13 +39,12 @@ struct ContactLink {
} }
#[once(time = 60)] #[once(time = 60)]
fn find_links() -> Vec<ContactLink> { fn find_links(directory: String) -> Vec<ContactLink> {
// TOML file location // TOML filename
let contacts_dir = "data/contacts";
let toml_file = "links.toml"; let toml_file = "links.toml";
// Read the TOML file and parse it // Read the TOML file and parse it
let toml_str = read_to_string(format!("{contacts_dir}/{toml_file}")).unwrap_or_default(); let toml_str = read_to_string(format!("{directory}/{toml_file}")).unwrap_or_default();
let mut redirections = vec![]; let mut redirections = vec![];
match toml::de::from_str::<toml::Value>(&toml_str) { match toml::de::from_str::<toml::Value>(&toml_str) {
@ -74,9 +74,9 @@ fn find_links() -> Vec<ContactLink> {
#[routes] #[routes]
#[get("/{service}")] #[get("/{service}")]
#[get("/{service}/{scope}")] #[get("/{service}/{scope}")]
async fn service_redirection(req: HttpRequest) -> impl Responder { async fn service_redirection(config: web::Data<Config>, req: HttpRequest) -> impl Responder {
let info = req.match_info(); let info = req.match_info();
let link = find_links() let link = find_links(format!("{}/contacts", config.locations.data_dir))
.iter() .iter()
// Find requested service // Find requested service
.filter(|&x| x.service == *info.query("service")) .filter(|&x| x.service == *info.query("service"))
@ -123,7 +123,7 @@ fn remove_paragraphs(list: &mut [File]) {
#[once(time = 60)] #[once(time = 60)]
fn build_page(config: Config) -> String { fn build_page(config: Config) -> String {
let contacts_dir = "data/contacts"; let contacts_dir = format!("{}/contacts", config.locations.data_dir);
let ext = ".md"; let ext = ".md";
let socials_dir = "socials"; let socials_dir = "socials";

View file

@ -116,20 +116,20 @@ async fn build_page(config: Config) -> String {
error: false, error: false,
projects: Some( projects: Some(
data.iter() data.iter()
.filter(|&p| !p.pulls_merged.is_empty())
.cloned() .cloned()
.filter(|p| !p.pulls_merged.is_empty())
.collect(), .collect(),
), ),
waiting: Some( waiting: Some(
data.iter() data.iter()
.filter(|&p| !p.pulls_open.is_empty())
.cloned() .cloned()
.filter(|p| !p.pulls_open.is_empty())
.collect(), .collect(),
), ),
closed: Some( closed: Some(
data.iter() data.iter()
.filter(|&p| !p.pulls_closed.is_empty())
.cloned() .cloned()
.filter(|p| !p.pulls_closed.is_empty())
.collect(), .collect(),
), ),
} }

View file

@ -91,7 +91,10 @@ fn get_content(
return None; return None;
} }
read_file(&format!("{cours_dir}/{filename}"), TypeFileMetadata::Cours) read_file(
&format!("{cours_dir}/{filename}"),
TypeFileMetadata::Generic,
)
} }
// #[once(time = 60)] // #[once(time = 60)]

View file

@ -4,7 +4,10 @@ use ramhorns::Content;
use crate::{ use crate::{
config::Config, config::Config,
misc::utils::{make_kw, Html}, misc::{
markdown::{read_file, File, TypeFileMetadata},
utils::{make_kw, Html},
},
template::{Infos, NavBar}, template::{Infos, NavBar},
}; };
@ -16,11 +19,37 @@ async fn page(config: web::Data<Config>) -> impl Responder {
#[derive(Content, Debug)] #[derive(Content, Debug)]
struct IndexTemplate { struct IndexTemplate {
navbar: NavBar, navbar: NavBar,
fullname: String, name: String,
pronouns: Option<String>,
content: Option<File>,
avatar: String,
avatar_caption: String,
} }
#[once(time = 60)] #[once(time = 60)]
fn build_page(config: Config) -> String { fn build_page(config: Config) -> String {
let mut file = read_file(
&format!("{}/index.md", config.locations.data_dir),
TypeFileMetadata::Index,
);
// Default values
let mut name = config.fc.fullname.to_owned().unwrap_or_default();
let mut pronouns = None;
let mut avatar = "/icons/apple-touch-icon.png".to_owned();
let mut avatar_caption = "EWP avatar".to_owned();
if let Some(f) = &file {
if let Some(m) = &f.metadata.info.index {
name = m.name.to_owned().unwrap_or(name);
avatar = m.avatar.to_owned().unwrap_or(avatar);
pronouns = m.pronouns.to_owned();
avatar_caption = m.avatar_caption.to_owned().unwrap_or(avatar_caption);
}
} else {
file = read_file("README.md", TypeFileMetadata::Generic);
}
config.tmpl.render( config.tmpl.render(
"index.html", "index.html",
IndexTemplate { IndexTemplate {
@ -28,16 +57,16 @@ fn build_page(config: Config) -> String {
index: true, index: true,
..NavBar::default() ..NavBar::default()
}, },
fullname: config content: file,
.fc name,
.fullname pronouns,
.to_owned() avatar,
.unwrap_or("Fullname".to_owned()), avatar_caption,
}, },
Infos { Infos {
page_title: config.fc.fullname, page_title: config.fc.fullname,
page_desc: Some("Page principale".into()), page_desc: Some("Page principale".into()),
page_kw: make_kw(&["index", "étudiant"]), page_kw: make_kw(&["index", "étudiant", "accueil"]),
}, },
) )
} }

View file

@ -29,7 +29,7 @@ struct PortfolioTemplate<'a> {
#[once(time = 60)] #[once(time = 60)]
fn build_page(config: Config) -> String { fn build_page(config: Config) -> String {
let projects_dir = "data/projects"; let projects_dir = format!("{}/projects", config.locations.data_dir);
let ext = ".md"; let ext = ".md";
// Get apps // Get apps
@ -39,7 +39,7 @@ fn build_page(config: Config) -> String {
.collect::<Vec<File>>(); .collect::<Vec<File>>();
let appdata = if apps.is_empty() { let appdata = if apps.is_empty() {
(None, Some(projects_dir)) (None, Some(projects_dir.as_str()))
} else { } else {
(Some(apps), None) (Some(apps), None)
}; };

View file

@ -24,6 +24,7 @@ pub struct Infos {
pub page_kw: Option<String>, pub page_kw: Option<String>,
} }
/// Information on what page the user is currently
#[derive(Content, Debug, Default)] #[derive(Content, Debug, Default)]
pub struct NavBar { pub struct NavBar {
pub index: bool, pub index: bool,

BIN
static/badges/friends/jas.webp (Stored with Git LFS) Normal file

Binary file not shown.

View file

@ -64,8 +64,8 @@ h1 {
opacity: 1; opacity: 1;
} }
#friends a { #friends a:not(h1 > a) {
padding-right: 10px; padding-right: 5px;
} }
#friends h1 { #friends h1 {

View file

@ -10,70 +10,24 @@
{{#data}} {{#data}}
<div> <div>
<span id="name">{{fullname}}</span> <span id="name">{{name}}</span>
<span id="pronouns">(il/lui, he/him)</span> {{#pronouns}}<span id="pronouns">{{pronouns}}</span>{{/pronouns}}
<img <img
id="avatar" id="avatar"
src="/icons/apple-touch-icon.png" src="{{avatar}} "
alt="Avatar" alt="Avatar"
title="Mon avatar, dessiné un jour super rapidement sur Gimp." title="{{avatar_caption}} "
loading="lazy" loading="lazy"
/> />
</div> </div>
<p id="subname"></p> <p id="subname"></p>
<article> {{#content}} {{&content}} {{/content}} {{^content}}
<h1>Qui suis-je ?</h1> <p>
<p>Je m'appelle <b>Anri</b>, mon pseudo est <b>Mylloon</b>.</p> <b>Welcome to EWP</b>, create a <code>index.md</code> file inside your
<p> <code>data/</code> directory to get started.
J'aime beaucoup l'informatique depuis très petit, ce site est écrit de </p>
A à Z par moi-même (modulo la quantité astronomique de librairie {{/content}} {{/data}}
utilisé) en Rust. J'adore le monde de l'open source, l'immense
majorité de mes projets sont sous licence
<a
href="https://en.wikipedia.org/wiki/GNU_Affero_General_Public_License"
target="_blank"
rel="noreferrer"
>AGPLv3</a
>.
</p>
<p>
En ce moment, je suis en master d'informatique à Paris Cité
(anciennement Paris 7), c'est marrant on fait de l'OCaml 🤓☝️.
</p>
</article>
<article id="friends">
<h1>Personnes incroyables</h1>
<a
href="https://github.com/2-1-1-2"
title="GitHub de 21_12"
target="_blank"
rel="noreferrer"
><img src="/badges/friends/21_12.webp" alt="21_12" loading="lazy"
/></a>
<a
href="https://twitter.com/azazouille_"
title="Twitter de Azazouille"
target="_blank"
rel="noreferrer"
><img
src="/badges/friends/azazouille.webp"
alt="Azazouille"
loading="lazy"
/></a>
<a
href="https://102jjwy.carrd.co/"
title="Carrd de 102jjwy"
target="_blank"
rel="noreferrer"
><img src="/badges/friends/102jjwy.webp" alt="102jjwy" loading="lazy"
/></a>
</article>
{{/data}}
</main> </main>
<script src="/js/index.js"></script> <script src="/js/index.js"></script>
</body> </body>