add template and implement rendering
parent
b2d6678405
commit
150940cd8c
@ -0,0 +1,117 @@
|
||||
// Front-end page rendering
|
||||
use crate::blog;
|
||||
use crate::router::Router;
|
||||
use crate::utils::*;
|
||||
use chrono::NaiveDateTime;
|
||||
use handlebars::Handlebars;
|
||||
use include_dir::{include_dir, Dir};
|
||||
use js_sys::{Date, Uint8Array};
|
||||
use serde::Serialize;
|
||||
use std::vec::Vec;
|
||||
use web_sys::*;
|
||||
|
||||
// TODO: allow static configuration of which theme to use
|
||||
const THEME_DIR: Dir = include_dir!("theme/default");
|
||||
|
||||
pub fn build_routes(router: &mut Router) {
|
||||
router.add_route("/static/", &serve_static);
|
||||
}
|
||||
|
||||
async fn serve_static(_req: Request, url: Url) -> MyResult<Response> {
|
||||
let path = url.pathname();
|
||||
|
||||
if let Some(file) = THEME_DIR.get_file(&path[1..path.len()]) {
|
||||
let u8arr: Uint8Array = file.contents().into();
|
||||
Response::new_with_opt_buffer_source_and_init(
|
||||
Some(&u8arr),
|
||||
ResponseInit::new()
|
||||
.status(200)
|
||||
.headers(headers!{
|
||||
"Content-Type" => mime_guess::from_path(path).first().unwrap().essence_str()
|
||||
}.as_ref())
|
||||
).internal_err()
|
||||
} else {
|
||||
Err(Error::NotFound("This file does not exist".into()))
|
||||
}
|
||||
}
|
||||
|
||||
// Context objects used when rendering pages
|
||||
#[derive(Serialize)]
|
||||
struct BlogRootContext {
|
||||
theme_config: &'static serde_json::Value,
|
||||
title: &'static str,
|
||||
description: &'static str,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
struct HomePagePost {
|
||||
title: String,
|
||||
url: String,
|
||||
timestamp: u64,
|
||||
summary: String
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
struct HomePageContext {
|
||||
blog: &'static BlogRootContext,
|
||||
posts: Vec<HomePagePost>
|
||||
}
|
||||
|
||||
lazy_static! {
|
||||
static ref THEME_CONFIG: serde_json::Value = serde_json::from_str(
|
||||
include_str!("../theme_config.json")).unwrap();
|
||||
|
||||
static ref ROOT_CONTEXT: BlogRootContext = {
|
||||
BlogRootContext {
|
||||
theme_config: &THEME_CONFIG,
|
||||
title: &crate::CONFIG.title,
|
||||
description: &crate::CONFIG.description
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
handlebars_helper!(cur_year: |dummy: u64| Date::new_0().get_full_year());
|
||||
// TODO: actually implement this helper
|
||||
handlebars_helper!(format_date: |date: u64, format: str| {
|
||||
NaiveDateTime::from_timestamp(date as i64, 0).format(format).to_string()
|
||||
});
|
||||
|
||||
fn build_handlebars() -> Handlebars<'static> {
|
||||
let mut hbs = Handlebars::new();
|
||||
|
||||
// Helpers
|
||||
hbs.register_helper("cur_year", Box::new(cur_year));
|
||||
hbs.register_helper("format_date", Box::new(format_date));
|
||||
|
||||
// Templates
|
||||
for file in THEME_DIR.files() {
|
||||
let path = file.path().to_str().unwrap();
|
||||
if path.ends_with(".hbs") {
|
||||
// Register all .hbs templates
|
||||
hbs.register_template_string(
|
||||
path, file.contents_utf8().unwrap()).unwrap();
|
||||
}
|
||||
}
|
||||
return hbs;
|
||||
}
|
||||
|
||||
pub async fn render_homepage() -> MyResult<String> {
|
||||
let hbs = build_handlebars();
|
||||
let mut context = HomePageContext {
|
||||
blog: &ROOT_CONTEXT,
|
||||
posts: vec![]
|
||||
};
|
||||
let posts_list = blog::PostsList::load().await;
|
||||
for uuid in posts_list.0.iter() {
|
||||
let post = blog::Post::find_by_uuid(uuid).await?;
|
||||
let post_cache = blog::PostContentCache::find_or_render(&post).await;
|
||||
context.posts.push(HomePagePost {
|
||||
title: post.title,
|
||||
url: post.url,
|
||||
timestamp: post.timestamp,
|
||||
summary: post_cache.content // TODO: make actual summaries
|
||||
});
|
||||
}
|
||||
hbs.render("home.hbs", &context)
|
||||
.map_err(|e| Error::BadRequest(format!("{:#?}", e)))
|
||||
}
|
@ -0,0 +1,29 @@
|
||||
<html lang="{{ blog.lang }}">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>{{ blog.title }}</title>
|
||||
<link rel="stylesheet" href="/static/style.css?ver=20200410"/>
|
||||
</head>
|
||||
<body>
|
||||
<div class="page-wrapper">
|
||||
{{> sidebar.hbs }}
|
||||
<div class="post-list">
|
||||
{{ #each posts }}
|
||||
<article class="post with-divider with-divider-wide-center with-divider-thin">
|
||||
<h1><a href="{{ this.url }}">{{ this.title }}</a></h1>
|
||||
<span class="date">{{ format_date this.timestamp "%e %b, %Y" }}</span>
|
||||
<section>
|
||||
{{{ this.summary }}}
|
||||
</section>
|
||||
<a href="{{ this.url }}"><span class="read-more"></span></a>
|
||||
</article>
|
||||
{{ /each }}
|
||||
<div class="pagination">
|
||||
<a href="#"><span class="page-older"></span></a>
|
||||
<a href="#"><span class="page-newer"></span></a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<script src="/static/script.js?ver=20200410"></script>
|
||||
</body>
|
||||
</html>
|
@ -0,0 +1,20 @@
|
||||
<div class="sidebar">
|
||||
<section class="avatar">
|
||||
<img src="{{ blog.theme_config.avatar_url }}" />
|
||||
</section>
|
||||
<section class="introduction with-divider with-divider-right">
|
||||
{{ blog.description }}
|
||||
</section>
|
||||
<section class="links with-divider with-divider-right">
|
||||
<ul>
|
||||
{{ #each blog.theme_config.nav_links }}
|
||||
<li><a href="{{ this.url }}">{{ this.name }}</a></li>
|
||||
{{ /each }}
|
||||
</ul>
|
||||
</section>
|
||||
<section class="copyright">
|
||||
Copyright © {{ cur_year 0 }} {{ blog.title }}<br />
|
||||
Powered by <a href="https://github.com/PeterCxy/paprika" target="_blank">paprika</a><br />
|
||||
Hosted on <a href="https://workers.cloudflare.com/" target="_blank">Cloudflare Workers</a>
|
||||
</section>
|
||||
</div>
|
@ -0,0 +1,9 @@
|
||||
// Avatar animation on hover
|
||||
// Unfortunately for best result, this can only be done with JS
|
||||
let avatar = document.querySelector(".sidebar .avatar img");
|
||||
avatar.onmouseover = (ev) => {
|
||||
ev.target.className = "animate";
|
||||
};
|
||||
avatar.onanimationend = (ev) => {
|
||||
ev.target.className = "";
|
||||
};
|
@ -0,0 +1,304 @@
|
||||
@import url('https://fonts.googleapis.com/css2?family=Fira+Sans:wght@300&display=swap');
|
||||
|
||||
/* Get rid of default padding */
|
||||
html, body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
font-family: 'Fira Sans', sans-serif;
|
||||
font-size: 18px;
|
||||
text-shadow: 0 0 3px #dddddd;
|
||||
color: rgb(100, 100, 100);
|
||||
}
|
||||
|
||||
html {
|
||||
background-image: url("/static/whitenoise-100x100.png");
|
||||
background-repeat: repeat;
|
||||
background-attachment: fixed;
|
||||
}
|
||||
|
||||
.hidden {
|
||||
display: none;
|
||||
}
|
||||
|
||||
a, a:link, a:visited, a:hover, a:active {
|
||||
text-decoration: none;
|
||||
color: inherit;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
body {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.page-wrapper {
|
||||
display: inline-block;
|
||||
position: absolute;
|
||||
left: 25vw;
|
||||
padding-top: 10px;
|
||||
width: 60vw;
|
||||
max-width: 1000px;
|
||||
min-width: 600px;
|
||||
padding-bottom: 15px;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.with-divider::after {
|
||||
content: "";
|
||||
display: block;
|
||||
margin-top: 20px;
|
||||
width: 40px;
|
||||
height: 3px;
|
||||
background-color: #E91E63;
|
||||
}
|
||||
|
||||
.with-divider.with-divider-wide-center::after {
|
||||
margin-left: calc(50% - 90px);
|
||||
width: 180px;
|
||||
}
|
||||
|
||||
.with-divider.with-divider-thin::after {
|
||||
margin-top: 18px;
|
||||
height: 2px;
|
||||
}
|
||||
|
||||
.with-divider.with-divider-right::after {
|
||||
margin-left: calc(100% - 40px);
|
||||
}
|
||||
|
||||
/* Sidebar */
|
||||
.sidebar {
|
||||
float: left;
|
||||
position: sticky;
|
||||
top: 20px;
|
||||
text-align: right;
|
||||
width: 22%;
|
||||
}
|
||||
|
||||
.sidebar section {
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.sidebar .avatar {
|
||||
display: inline-block;
|
||||
overflow: hidden;
|
||||
width: 100px;
|
||||
height: 100px;
|
||||
line-height: 100px;
|
||||
text-align: center;
|
||||
background-color: #ffffff;
|
||||
border-radius: 50%;
|
||||
box-shadow: 0px 0px 6px 0px #cccccc;
|
||||
}
|
||||
|
||||
.sidebar .avatar img {
|
||||
display: inline-block;
|
||||
margin-top: 5%;
|
||||
width: 90%;
|
||||
height: 90%;
|
||||
border-radius: 50%;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.sidebar .avatar img.animate {
|
||||
animation: spin 1s ease-in-out;
|
||||
}
|
||||
/*
|
||||
.sidebar .avatar img:hover {
|
||||
animation: spin 0.5s ease-in-out;
|
||||
}*/
|
||||
|
||||
@keyframes spin {
|
||||
20% {
|
||||
transform: rotate(30deg);
|
||||
}
|
||||
|
||||
40% {
|
||||
transform: rotate(-30deg);
|
||||
}
|
||||
|
||||
60% {
|
||||
transform: rotate(30deg);
|
||||
}
|
||||
|
||||
80% {
|
||||
transform: rotate(-30deg);
|
||||
}
|
||||
|
||||
100% {
|
||||
transform: rotate(0);
|
||||
}
|
||||
}
|
||||
|
||||
.sidebar .introduction {
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.sidebar .links ul {
|
||||
list-style-type: none;
|
||||
padding-bottom: 10px;
|
||||
}
|
||||
|
||||
.sidebar .links li {
|
||||
padding-top: 10px;
|
||||
}
|
||||
|
||||
.sidebar .links a {
|
||||
transition: color 0.2s ease-in-out;
|
||||
}
|
||||
|
||||
.sidebar .links a:hover {
|
||||
color: #F06292;
|
||||
}
|
||||
|
||||
.sidebar .copyright {
|
||||
font-size: 0.6em;
|
||||
color: rgb(200, 200, 200);
|
||||
}
|
||||
|
||||
.copyright a,
|
||||
.copyright a:link,
|
||||
.copyright a:visited,
|
||||
.copyright a:hover,
|
||||
.copyright a:active {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
/* Home page post list */
|
||||
.post-list, .content {
|
||||
margin-top: 15px;
|
||||
margin-left: 25%;
|
||||
width: 75%;
|
||||
}
|
||||
|
||||
.post-list .post {
|
||||
padding: 10px 5px 5px 5px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.post-list .post h1 {
|
||||
font-size: 1.2em;
|
||||
margin: 0 0 0.5em 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.post-list .post h1 a {
|
||||
color: inherit;
|
||||
transition: color 0.2s ease-in-out;
|
||||
}
|
||||
|
||||
.post-list .post section {
|
||||
margin-top: 0.5em;
|
||||
}
|
||||
|
||||
.post-list .post h1 a:hover {
|
||||
color: #E91E63;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.post-list .post .read-more::after {
|
||||
content: "......";
|
||||
color: #F06292;
|
||||
}
|
||||
|
||||
.post-list .pagination {
|
||||
font-size: 0.8em;
|
||||
}
|
||||
|
||||
.post-list .pagination .page-older:after {
|
||||
content: "Older >";
|
||||
color: #E91E63;
|
||||
float: right;
|
||||
}
|
||||
|
||||
.post-list .pagination .page-newer:after {
|
||||
content: "< Newer";
|
||||
color: #E91E63;
|
||||
float: left;
|
||||
}
|
||||
|
||||
.post a,
|
||||
.post a:link,
|
||||
.post a:visited,
|
||||
.post a:hover,
|
||||
.post a:active {
|
||||
color: #E91E63;
|
||||
}
|
||||
|
||||
.post a:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
/* Posts */
|
||||
.date {
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.post img,
|
||||
.content img {
|
||||
max-width: 100%;
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
.toc-wrapper {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
float: right;
|
||||
position: fixed;
|
||||
right: 0;
|
||||
top: 0;
|
||||
width: 10%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.toc {
|
||||
max-width: 200px;
|
||||
font-size: 0.6em;
|
||||
text-align: left;
|
||||
background-color: #ffffff;
|
||||
border-radius: 5px;
|
||||
box-shadow: 0 1px 3px rgba(0,0,0,0.12), 0 1px 2px rgba(0,0,0,0.24);
|
||||
padding-left: 15px;
|
||||
}
|
||||
|
||||
.toc li {
|
||||
margin-bottom: 2px;
|
||||
}
|
||||
|
||||
.toc li::before {
|
||||
content: "-";
|
||||
position: absolute;
|
||||
left: 5px; /* Relative to parent */
|
||||
}
|
||||
|
||||
.toc ul {
|
||||
margin-top: 5px;
|
||||
margin-bottom: 5px;
|
||||
padding-left: 0px;
|
||||
margin-right: 10px;
|
||||
margin-left: 0;
|
||||
list-style-type: none;
|
||||
}
|
||||
|
||||
.toc ul li.current {
|
||||
color: #000000;
|
||||
}
|
||||
|
||||
.toc ul li.current::before {
|
||||
content: "+";
|
||||
}
|
||||
|
||||
.toc ul ul {
|
||||
margin-left: 5px;
|
||||
}
|
||||
|
||||
.toc ul ul ul {
|
||||
margin-left: 10px;
|
||||
}
|
||||
|
||||
.toc ul ul ul ul {
|
||||
margin-left: 15px;
|
||||
}
|
||||
|
||||
.toc ul ul ul ul ul {
|
||||
margin-left: 20px;
|
||||
}
|
Binary file not shown.
After Width: | Height: | Size: 5.0 KiB |
Loading…
Reference in New Issue