stuff happened
This commit is contained in:
@ -1,3 +1,4 @@
|
|||||||
pub mod authors;
|
pub mod authors;
|
||||||
pub mod comments;
|
pub mod comments;
|
||||||
pub mod posts;
|
pub mod posts;
|
||||||
|
pub mod projects;
|
||||||
|
15
backend/public/src/datasources/projects.rs
Normal file
15
backend/public/src/datasources/projects.rs
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
use sqlx::{FromRow, Pool, Postgres, Row};
|
||||||
|
|
||||||
|
use crate::routes::projects::Project;
|
||||||
|
|
||||||
|
pub struct ProjectsDatasource;
|
||||||
|
impl ProjectsDatasource {
|
||||||
|
pub async fn get_all(pool: &Pool<Postgres>) -> Result<Vec<Project>, sqlx::Error> {
|
||||||
|
sqlx::query_as!(
|
||||||
|
Project,
|
||||||
|
"SELECT project_id, title, repo, summary, tech, wip, created_at FROM projects p WHERE deleted_at IS NULL ORDER BY p.created_at DESC"
|
||||||
|
)
|
||||||
|
.fetch_all(pool)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
}
|
@ -131,6 +131,10 @@ async fn main() {
|
|||||||
"/authors",
|
"/authors",
|
||||||
routes::authors::AuthorsRoute::routes(&app_state),
|
routes::authors::AuthorsRoute::routes(&app_state),
|
||||||
)
|
)
|
||||||
|
.nest(
|
||||||
|
"/projects",
|
||||||
|
routes::projects::ProjectsRoute::routes(&app_state),
|
||||||
|
)
|
||||||
.layer(CorsLayer::permissive())
|
.layer(CorsLayer::permissive())
|
||||||
.layer(
|
.layer(
|
||||||
TraceLayer::new_for_http()
|
TraceLayer::new_for_http()
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
pub mod authors;
|
pub mod authors;
|
||||||
pub mod comments;
|
pub mod comments;
|
||||||
pub mod posts;
|
pub mod posts;
|
||||||
|
pub mod projects;
|
||||||
pub mod root;
|
pub mod root;
|
||||||
|
@ -1,7 +1,11 @@
|
|||||||
use crate::{
|
use crate::{
|
||||||
datasources::posts::PostsDatasource,
|
datasources::posts::PostsDatasource,
|
||||||
state::AppState,
|
state::AppState,
|
||||||
utils::{datetime::*, rss},
|
utils::{
|
||||||
|
datetime::*,
|
||||||
|
rss,
|
||||||
|
sitemap::{self, SitemapEntry},
|
||||||
|
},
|
||||||
};
|
};
|
||||||
use axum::http::{HeaderMap, HeaderValue};
|
use axum::http::{HeaderMap, HeaderValue};
|
||||||
use axum::{
|
use axum::{
|
||||||
@ -60,6 +64,7 @@ impl PostsRoute {
|
|||||||
.route("/hot", get(PostsRoute::get_hot_posts))
|
.route("/hot", get(PostsRoute::get_hot_posts))
|
||||||
.route("/featured", get(PostsRoute::get_featured_posts))
|
.route("/featured", get(PostsRoute::get_featured_posts))
|
||||||
.route("/rss", get(PostsRoute::get_rss_posts))
|
.route("/rss", get(PostsRoute::get_rss_posts))
|
||||||
|
.route("/sitemap", get(PostsRoute::get_sitemap))
|
||||||
.with_state(app_state.clone())
|
.with_state(app_state.clone())
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -330,7 +335,8 @@ impl PostsRoute {
|
|||||||
|
|
||||||
match PostsDatasource::get_all(&state.database).await {
|
match PostsDatasource::get_all(&state.database).await {
|
||||||
Ok(posts) => {
|
Ok(posts) => {
|
||||||
let web_url = std::env::var("BASE_URI_WEB").expect("No environment variable found");
|
let web_url =
|
||||||
|
std::env::var("BASE_URI_WEB").expect("Environment BASE_URI_WEB variable found");
|
||||||
let mapped_posts: HashMap<String, Post> = posts
|
let mapped_posts: HashMap<String, Post> = posts
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|post| (post.post_id.to_string(), post))
|
.map(|post| (post.post_id.to_string(), post))
|
||||||
@ -343,9 +349,42 @@ impl PostsRoute {
|
|||||||
);
|
);
|
||||||
let mut headers = HeaderMap::new();
|
let mut headers = HeaderMap::new();
|
||||||
headers.insert(
|
headers.insert(
|
||||||
header::CONTENT_DISPOSITION,
|
header::CONTENT_TYPE,
|
||||||
HeaderValue::from_str(r#"attachment; filename="posts.xml""#).unwrap(),
|
HeaderValue::from_static("application/xml"),
|
||||||
);
|
);
|
||||||
|
(headers, xml)
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
let mut headers = HeaderMap::new();
|
||||||
|
headers.insert("Content-Type", HeaderValue::from_static("text/plain"));
|
||||||
|
(headers, e.to_string())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn get_sitemap(State(app_state): State<AppState>) -> impl IntoResponse {
|
||||||
|
let state = app_state.lock().await;
|
||||||
|
// let cached: Option<Vec<Post>> = None; // TODO: maybe implement cache, later??
|
||||||
|
|
||||||
|
match PostsDatasource::get_all(&state.database).await {
|
||||||
|
Ok(posts) => {
|
||||||
|
let web_url =
|
||||||
|
std::env::var("BASE_URI_WEB").expect("Environment BASE_URI_WEB variable found");
|
||||||
|
let mut entries: HashMap<String, SitemapEntry> = posts
|
||||||
|
.into_iter()
|
||||||
|
.map(|p| {
|
||||||
|
(
|
||||||
|
p.post_id.to_string(),
|
||||||
|
SitemapEntry {
|
||||||
|
location: format!("{}/posts/{}", web_url, p.post_id.to_string()),
|
||||||
|
lastmod: p.created_at.unwrap_or_else(|| chrono::Utc::now()),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
sitemap::get_static_pages(&mut entries, &web_url);
|
||||||
|
let xml: String = sitemap::generate_sitemap(&entries);
|
||||||
|
let mut headers = HeaderMap::new();
|
||||||
headers.insert(
|
headers.insert(
|
||||||
header::CONTENT_TYPE,
|
header::CONTENT_TYPE,
|
||||||
HeaderValue::from_static("application/xml"),
|
HeaderValue::from_static("application/xml"),
|
||||||
|
69
backend/public/src/routes/projects.rs
Normal file
69
backend/public/src/routes/projects.rs
Normal file
@ -0,0 +1,69 @@
|
|||||||
|
use crate::{datasources::projects::ProjectsDatasource, state::AppState, utils::datetime::*};
|
||||||
|
use axum::http::{HeaderMap, HeaderValue};
|
||||||
|
use axum::{extract::State, http::StatusCode, response::IntoResponse, routing::get, Json, Router};
|
||||||
|
use fred::types::Expiration;
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
#[derive(sqlx::FromRow, Deserialize, Serialize, Debug, Clone)]
|
||||||
|
pub struct Project {
|
||||||
|
pub project_id: i32,
|
||||||
|
pub title: String,
|
||||||
|
pub repo: Option<String>,
|
||||||
|
pub summary: String,
|
||||||
|
pub tech: String,
|
||||||
|
pub wip: Option<bool>,
|
||||||
|
#[serde(serialize_with = "serialize_datetime")]
|
||||||
|
#[serde(deserialize_with = "deserialize_datetime")]
|
||||||
|
pub created_at: Option<chrono::DateTime<chrono::Utc>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct ProjectsRoute;
|
||||||
|
impl ProjectsRoute {
|
||||||
|
pub fn routes(app_state: &AppState) -> Router {
|
||||||
|
Router::new()
|
||||||
|
.route("/", get(ProjectsRoute::get_all))
|
||||||
|
.with_state(app_state.clone())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn get_all(State(app_state): State<AppState>) -> impl IntoResponse {
|
||||||
|
let mut state = app_state.lock().await;
|
||||||
|
let cached: Option<Vec<Project>> = state
|
||||||
|
.cache
|
||||||
|
.get(String::from("projects:all"))
|
||||||
|
.await
|
||||||
|
.unwrap_or(None);
|
||||||
|
|
||||||
|
if let Some(projects) = cached {
|
||||||
|
tracing::info!("grabbing all projects from cache");
|
||||||
|
return Ok(Json(projects));
|
||||||
|
};
|
||||||
|
|
||||||
|
match ProjectsDatasource::get_all(&state.database).await {
|
||||||
|
Ok(projects) => {
|
||||||
|
tracing::info!("grabbing all projects from database");
|
||||||
|
if let p = &projects {
|
||||||
|
let projects = p.clone();
|
||||||
|
let state = app_state.clone();
|
||||||
|
|
||||||
|
tracing::info!("storing database data in cache");
|
||||||
|
tokio::spawn(async move {
|
||||||
|
let mut s = state.lock().await;
|
||||||
|
let _ = s
|
||||||
|
.cache
|
||||||
|
.set(
|
||||||
|
String::from("projects:all"),
|
||||||
|
&projects,
|
||||||
|
Some(Expiration::EX(10)),
|
||||||
|
None,
|
||||||
|
false,
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(Json(projects))
|
||||||
|
}
|
||||||
|
Err(e) => Err((StatusCode::INTERNAL_SERVER_ERROR, e.to_string())),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -1,10 +1,15 @@
|
|||||||
use axum::{
|
use axum::{
|
||||||
|
extract::State,
|
||||||
http::StatusCode,
|
http::StatusCode,
|
||||||
response::{Html, IntoResponse},
|
response::{Html, IntoResponse},
|
||||||
routing::get,
|
routing::get,
|
||||||
Router,
|
Router,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
use crate::{datasources::posts::PostsDatasource, state::AppState};
|
||||||
|
|
||||||
|
use super::posts::Post;
|
||||||
|
|
||||||
pub struct RootRoute;
|
pub struct RootRoute;
|
||||||
impl RootRoute {
|
impl RootRoute {
|
||||||
pub fn routes() -> Router {
|
pub fn routes() -> Router {
|
||||||
|
@ -27,13 +27,7 @@ impl Cache {
|
|||||||
where
|
where
|
||||||
T: for<'de> serde::Deserialize<'de>,
|
T: for<'de> serde::Deserialize<'de>,
|
||||||
{
|
{
|
||||||
if !self.inmem.is_connected() {
|
self.is_connected()?;
|
||||||
return Err(Box::new(std::io::Error::new(
|
|
||||||
std::io::ErrorKind::Other,
|
|
||||||
"Not connected to cache".to_string(),
|
|
||||||
)));
|
|
||||||
}
|
|
||||||
|
|
||||||
let value: Option<String> = self.inmem.get(&key).await?;
|
let value: Option<String> = self.inmem.get(&key).await?;
|
||||||
|
|
||||||
match value {
|
match value {
|
||||||
@ -56,23 +50,34 @@ impl Cache {
|
|||||||
where
|
where
|
||||||
T: for<'de> serde::Deserialize<'de> + serde::Serialize,
|
T: for<'de> serde::Deserialize<'de> + serde::Serialize,
|
||||||
{
|
{
|
||||||
if !self.inmem.is_connected() {
|
self.is_connected()?;
|
||||||
|
let json_string = match serde_json::to_string::<T>(contents) {
|
||||||
|
Ok(s) => s,
|
||||||
|
Err(_) => {
|
||||||
return Err(Box::new(std::io::Error::new(
|
return Err(Box::new(std::io::Error::new(
|
||||||
std::io::ErrorKind::Other,
|
std::io::ErrorKind::Other,
|
||||||
"Not connected to cache".to_string(),
|
"Unable to deserialize contents passed to cache".to_string(),
|
||||||
)));
|
)))
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
|
||||||
let json_string = serde_json::to_string(contents)?;
|
Ok(self
|
||||||
self.inmem
|
.inmem
|
||||||
.set(key, json_string, expiration, set_opts, get)
|
.set(key, json_string, expiration, set_opts, get)
|
||||||
.await?;
|
.await?)
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn del(&mut self, key: String) -> Result<(), Box<dyn std::error::Error>> {
|
pub async fn del(&mut self, key: String) -> Result<(), Box<dyn std::error::Error>> {
|
||||||
self.inmem.del(key).await?;
|
Ok(self.inmem.del(key).await?)
|
||||||
Ok(())
|
}
|
||||||
|
|
||||||
|
fn is_connected(&mut self) -> Result<(), Box<dyn std::error::Error>> {
|
||||||
|
match self.inmem.is_connected() {
|
||||||
|
true => Ok(()),
|
||||||
|
false => Err(Box::new(std::io::Error::new(
|
||||||
|
std::io::ErrorKind::Other,
|
||||||
|
"Not connected to cache".to_string(),
|
||||||
|
))),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,2 +1,3 @@
|
|||||||
pub mod datetime;
|
pub mod datetime;
|
||||||
pub mod rss;
|
pub mod rss;
|
||||||
|
pub mod sitemap;
|
||||||
|
@ -13,7 +13,8 @@ pub struct RssEntry {
|
|||||||
|
|
||||||
impl From<posts::Post> for RssEntry {
|
impl From<posts::Post> for RssEntry {
|
||||||
fn from(post: posts::Post) -> Self {
|
fn from(post: posts::Post) -> Self {
|
||||||
let web_url = std::env::var("BASE_URI_WEB").expect("Environment variable not found");
|
let web_url =
|
||||||
|
std::env::var("BASE_URI_WEB").expect("Environment variable BASE_URI_WEB not found");
|
||||||
let post_url = format!("{}{}{}", web_url, "/posts/", post.post_id.to_string());
|
let post_url = format!("{}{}{}", web_url, "/posts/", post.post_id.to_string());
|
||||||
let author_full_name = format!("{} {}", post.first_name.unwrap(), post.last_name.unwrap());
|
let author_full_name = format!("{} {}", post.first_name.unwrap(), post.last_name.unwrap());
|
||||||
|
|
||||||
@ -58,10 +59,7 @@ pub fn generate_rss(
|
|||||||
link: &str,
|
link: &str,
|
||||||
posts: &HashMap<String, posts::Post>,
|
posts: &HashMap<String, posts::Post>,
|
||||||
) -> String {
|
) -> String {
|
||||||
println!("{:?}", posts);
|
|
||||||
let values = posts.clone().into_values();
|
let values = posts.clone().into_values();
|
||||||
println!("{:?}", values);
|
|
||||||
|
|
||||||
let rss_entries = values
|
let rss_entries = values
|
||||||
.map(|p| p.into())
|
.map(|p| p.into())
|
||||||
.map(|r: RssEntry| r.to_item())
|
.map(|r: RssEntry| r.to_item())
|
||||||
@ -69,8 +67,9 @@ pub fn generate_rss(
|
|||||||
|
|
||||||
let safe_title = escape_str_pcdata(title);
|
let safe_title = escape_str_pcdata(title);
|
||||||
let safe_description = escape_str_pcdata(description);
|
let safe_description = escape_str_pcdata(description);
|
||||||
println!("{:?}", rss_entries);
|
|
||||||
|
|
||||||
|
// TODO: change the atom link in this string - it's not correct
|
||||||
|
// change it when we know the URL
|
||||||
format!(
|
format!(
|
||||||
r#"<?xml version="1.0" encoding="UTF-8"?>
|
r#"<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
|
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
|
||||||
|
62
backend/public/src/utils/sitemap.rs
Normal file
62
backend/public/src/utils/sitemap.rs
Normal file
@ -0,0 +1,62 @@
|
|||||||
|
use std::collections::HashMap;
|
||||||
|
|
||||||
|
pub struct SitemapEntry {
|
||||||
|
pub location: String,
|
||||||
|
pub lastmod: chrono::DateTime<chrono::Utc>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SitemapEntry {
|
||||||
|
fn to_item(&self) -> String {
|
||||||
|
format!(
|
||||||
|
r#"
|
||||||
|
<url>
|
||||||
|
<loc>{}</loc>
|
||||||
|
<lastmod>{}</lastmod>
|
||||||
|
</url>
|
||||||
|
"#,
|
||||||
|
self.location,
|
||||||
|
self.lastmod.to_rfc3339(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn generate_sitemap(entries: &HashMap<String, SitemapEntry>) -> String {
|
||||||
|
let urls = entries
|
||||||
|
.values()
|
||||||
|
.into_iter()
|
||||||
|
.map(|entry| entry.to_item())
|
||||||
|
.collect::<String>();
|
||||||
|
format!(
|
||||||
|
r#"
|
||||||
|
<!-- Generated by Kyouma 1.0.0-SE -->
|
||||||
|
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
|
||||||
|
{}
|
||||||
|
</urlset>
|
||||||
|
"#,
|
||||||
|
urls
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_static_pages(entries: &mut HashMap<String, SitemapEntry>, web_url: &String) {
|
||||||
|
entries.insert(
|
||||||
|
(entries.len() + 1).to_string(),
|
||||||
|
SitemapEntry {
|
||||||
|
location: web_url.clone(),
|
||||||
|
lastmod: chrono::Utc::now(),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
entries.insert(
|
||||||
|
(entries.len() + 1).to_string(),
|
||||||
|
SitemapEntry {
|
||||||
|
location: format!("{}/posts", web_url),
|
||||||
|
lastmod: chrono::Utc::now(),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
entries.insert(
|
||||||
|
(entries.len() + 1).to_string(),
|
||||||
|
SitemapEntry {
|
||||||
|
location: format!("{}/projects", web_url),
|
||||||
|
lastmod: chrono::Utc::now(),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
1428
backend/task/Cargo.lock
generated
1428
backend/task/Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
@ -7,6 +7,7 @@ edition = "2021"
|
|||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
tokio = { version = "1.19.2", features = ["full"] }
|
tokio = { version = "1.19.2", features = ["full"] }
|
||||||
|
reqwest = { version = "0.12.20", features = ["json", "rustls-tls"] }
|
||||||
job_scheduler = "1.2.1"
|
job_scheduler = "1.2.1"
|
||||||
sqlx = { version = "0.8.2", features = [
|
sqlx = { version = "0.8.2", features = [
|
||||||
"postgres",
|
"postgres",
|
||||||
@ -20,6 +21,7 @@ futures = "0.3.30"
|
|||||||
markdown = "1.0.0-alpha.20"
|
markdown = "1.0.0-alpha.20"
|
||||||
serde = { version = "*", features = ["derive"] }
|
serde = { version = "*", features = ["derive"] }
|
||||||
serde_yml = "*"
|
serde_yml = "*"
|
||||||
aws-sdk-s3 = "1.77.0"
|
aws-sdk-s3 = "1.94.0"
|
||||||
|
aws-config = "1.8"
|
||||||
tracing = "0.1"
|
tracing = "0.1"
|
||||||
tracing-subscriber = { version = "0.3.18", features = ["env-filter"] }
|
tracing-subscriber = { version = "0.3.18", features = ["env-filter"] }
|
||||||
|
@ -4,4 +4,12 @@ also known as `task`
|
|||||||
|
|
||||||
## What is this?
|
## What is this?
|
||||||
|
|
||||||
I don't know yet - hopefully this will be filled out soon.
|
This is a task runner/scheduler programs that will fire off various tasks. These tasks can be anything from an blog post import task to a RSS generator task. Additionally, there is task logs inside the database so that you can keep track of tasks when something goes wrong.
|
||||||
|
|
||||||
|
## Things you should know
|
||||||
|
|
||||||
|
`task` uses a `.env` file at the root of the project. The file takes standard environment variables (like enviroment variables you would put into a `.bashrc` or ad-hoc into your shell).
|
||||||
|
|
||||||
|
For `task` to work properly, please make sure to first create the `.env` file, then fill out the following environment variables:
|
||||||
|
|
||||||
|
- `DATABASE_URL` - needed for communicating to Postgres
|
||||||
|
@ -3,7 +3,7 @@ use sqlx::{postgres::PgPoolOptions, Pool, Postgres};
|
|||||||
use std::env;
|
use std::env;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
use tasks::import_posts;
|
use tasks::*;
|
||||||
|
|
||||||
//mod config;
|
//mod config;
|
||||||
mod tasks;
|
mod tasks;
|
||||||
@ -87,14 +87,24 @@ impl<'a> TaskManager<'a> {
|
|||||||
for job in &results {
|
for job in &results {
|
||||||
tracing::info!("Registering job: {}", job.task_name);
|
tracing::info!("Registering job: {}", job.task_name);
|
||||||
|
|
||||||
let pool = Arc::new(self.pool.clone());
|
|
||||||
let schedule = job
|
let schedule = job
|
||||||
.schedule
|
.schedule
|
||||||
.parse()
|
.parse()
|
||||||
.map_err(|e| format!("Failed to parse schedule '{}': {}", job.schedule, e))?;
|
.map_err(|e| format!("Failed to parse schedule '{}': {}", job.schedule, e))?;
|
||||||
|
|
||||||
let task = match job.task_id {
|
let task: Box<dyn Fn() + Send + Sync> = match job.task_id {
|
||||||
1 => Box::new(move || import_posts::register(&pool)),
|
1 => {
|
||||||
|
let pool = Arc::new(self.pool.clone());
|
||||||
|
Box::new(move || import_posts::register(&pool))
|
||||||
|
}
|
||||||
|
2 => {
|
||||||
|
let pool = Arc::new(self.pool.clone());
|
||||||
|
Box::new(move || upload_rss::register(&pool))
|
||||||
|
}
|
||||||
|
3 => {
|
||||||
|
let pool = Arc::new(self.pool.clone());
|
||||||
|
Box::new(move || upload_sitemap::register(&pool))
|
||||||
|
}
|
||||||
id => return Err(format!("Unknown task_id: {}", id).into()),
|
id => return Err(format!("Unknown task_id: {}", id).into()),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -64,7 +64,6 @@ async fn import_posts(
|
|||||||
|
|
||||||
// Process file contents
|
// Process file contents
|
||||||
let file_md_contents = process_read_file(&file_path)?;
|
let file_md_contents = process_read_file(&file_path)?;
|
||||||
// println!("{:?}", file_md_contents);
|
|
||||||
// Extract metadata
|
// Extract metadata
|
||||||
let document = crate::utils::front_matter::YamlFrontMatter::parse::<MarkdownMetadata>(
|
let document = crate::utils::front_matter::YamlFrontMatter::parse::<MarkdownMetadata>(
|
||||||
&file_md_contents,
|
&file_md_contents,
|
||||||
@ -74,10 +73,8 @@ async fn import_posts(
|
|||||||
markdown::to_html_with_options(&document.content, &markdown::Options::default());
|
markdown::to_html_with_options(&document.content, &markdown::Options::default());
|
||||||
println!("{:?}", content);
|
println!("{:?}", content);
|
||||||
|
|
||||||
// println!("{:?}", document);
|
|
||||||
let title = document.metadata.title;
|
let title = document.metadata.title;
|
||||||
let content_final = content.unwrap();
|
let content_final = content.unwrap();
|
||||||
// println!("{:?}", title);
|
|
||||||
|
|
||||||
// Insert into database
|
// Insert into database
|
||||||
let results = sqlx::query_as::<_, InsertPosts>(
|
let results = sqlx::query_as::<_, InsertPosts>(
|
||||||
|
@ -1 +1,3 @@
|
|||||||
pub mod import_posts;
|
pub mod import_posts;
|
||||||
|
pub mod upload_rss;
|
||||||
|
pub mod upload_sitemap;
|
||||||
|
40
backend/task/src/tasks/upload_rss.rs
Normal file
40
backend/task/src/tasks/upload_rss.rs
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
use sqlx::{Pool, Postgres};
|
||||||
|
|
||||||
|
use crate::utils::{
|
||||||
|
request::{Request, Response},
|
||||||
|
task_log,
|
||||||
|
{upload::S3ClientConfig, *},
|
||||||
|
};
|
||||||
|
|
||||||
|
pub fn register(pool: &sqlx::Pool<sqlx::Postgres>) {
|
||||||
|
let p = pool.clone();
|
||||||
|
tokio::spawn(async move {
|
||||||
|
let _ = upload_rss(&p).await;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn upload_rss(pool: &sqlx::Pool<sqlx::Postgres>) -> Result<(), Box<dyn std::error::Error>> {
|
||||||
|
// start task logging
|
||||||
|
task_log::start(2, pool).await?;
|
||||||
|
|
||||||
|
// get request and request the things
|
||||||
|
let request = Request::new();
|
||||||
|
let rss_url = format!("{}/posts/rss", request.base_url);
|
||||||
|
let rss_result = request.request_url::<String>(&rss_url).await.unwrap();
|
||||||
|
|
||||||
|
// upload the sucker to obj storage
|
||||||
|
if let Response::Xml(rss) = rss_result {
|
||||||
|
let client_config = S3ClientConfig::from_env().unwrap();
|
||||||
|
let s3_client = upload::create_s3_client(&client_config).await.unwrap();
|
||||||
|
let _ = upload::upload(
|
||||||
|
&s3_client,
|
||||||
|
client_config.bucket.as_str(),
|
||||||
|
"feed.xml",
|
||||||
|
rss.as_str(),
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
println!("Finished uploading RSS feed");
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
40
backend/task/src/tasks/upload_sitemap.rs
Normal file
40
backend/task/src/tasks/upload_sitemap.rs
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
use crate::utils::{
|
||||||
|
request::{Request, Response},
|
||||||
|
task_log,
|
||||||
|
{upload::S3ClientConfig, *},
|
||||||
|
};
|
||||||
|
|
||||||
|
pub fn register(pool: &sqlx::Pool<sqlx::Postgres>) {
|
||||||
|
let p = pool.clone();
|
||||||
|
tokio::spawn(async move {
|
||||||
|
let _ = upload_sitemap(&p).await;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn upload_sitemap(
|
||||||
|
pool: &sqlx::Pool<sqlx::Postgres>,
|
||||||
|
) -> Result<(), Box<dyn std::error::Error>> {
|
||||||
|
// TODO:: get sitemap and upload it to bucket??
|
||||||
|
task_log::start(3, pool).await?;
|
||||||
|
|
||||||
|
// get request and request the things
|
||||||
|
let request = Request::new();
|
||||||
|
let sitemap_url = format!("{}/posts/sitemap", request.base_url);
|
||||||
|
let sitemap_result = request.request_url::<String>(&sitemap_url).await;
|
||||||
|
|
||||||
|
// upload the sucker to obj storage
|
||||||
|
if let Response::Xml(sitemap) = sitemap_result {
|
||||||
|
let client_config = S3ClientConfig::from_env().unwrap();
|
||||||
|
let s3_client = upload::create_s3_client(&client_config).await.unwrap();
|
||||||
|
let _ = upload::upload(
|
||||||
|
&s3_client,
|
||||||
|
client_config.bucket.as_str(),
|
||||||
|
"sitemap.xml",
|
||||||
|
sitemap.as_str(),
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
println!("Finished uploading sitemap!");
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
@ -21,10 +21,7 @@ impl YamlFrontMatter {
|
|||||||
markdown: &str,
|
markdown: &str,
|
||||||
) -> Result<Document, Box<dyn std::error::Error>> {
|
) -> Result<Document, Box<dyn std::error::Error>> {
|
||||||
let yaml = YamlFrontMatter::extract(markdown)?;
|
let yaml = YamlFrontMatter::extract(markdown)?;
|
||||||
println!("File front matter metadata (raw): {:?}", yaml.0);
|
|
||||||
// println!("File content: {:?}", yaml.1);
|
|
||||||
let clean_yaml = YamlFrontMatter::unescape_str(&yaml.0);
|
let clean_yaml = YamlFrontMatter::unescape_str(&yaml.0);
|
||||||
println!("File front matter metadata (clean): {:?}", clean_yaml);
|
|
||||||
let metadata = match YamlFrontMatter::from_yaml_str(clean_yaml.as_str()) {
|
let metadata = match YamlFrontMatter::from_yaml_str(clean_yaml.as_str()) {
|
||||||
Ok(m) => m,
|
Ok(m) => m,
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
|
@ -1,2 +1,4 @@
|
|||||||
pub mod front_matter;
|
pub mod front_matter;
|
||||||
|
pub mod request;
|
||||||
pub mod task_log;
|
pub mod task_log;
|
||||||
|
pub mod upload;
|
||||||
|
85
backend/task/src/utils/request.rs
Normal file
85
backend/task/src/utils/request.rs
Normal file
@ -0,0 +1,85 @@
|
|||||||
|
use reqwest::StatusCode;
|
||||||
|
use std::env;
|
||||||
|
use std::time::Duration;
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct Request<'a> {
|
||||||
|
pub client: reqwest::Client,
|
||||||
|
pub base_url: Box<str>,
|
||||||
|
pub full_url: Option<&'a str>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub enum Response<T> {
|
||||||
|
Json(T),
|
||||||
|
Xml(String),
|
||||||
|
Text(String),
|
||||||
|
Bytes(Vec<u8>),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> Request<'a> {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Request {
|
||||||
|
client: reqwest::ClientBuilder::new()
|
||||||
|
.use_rustls_tls()
|
||||||
|
.timeout(Duration::from_secs(30))
|
||||||
|
.build()
|
||||||
|
.unwrap(),
|
||||||
|
base_url: env::var("BASE_URI_API")
|
||||||
|
.expect("Environment variable BASE_URI_API is not found")
|
||||||
|
.into_boxed_str(),
|
||||||
|
full_url: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn request_url<T>(
|
||||||
|
&self,
|
||||||
|
url: &String,
|
||||||
|
) -> Result<Response<T>, Box<dyn std::error::Error>>
|
||||||
|
where
|
||||||
|
T: for<'de> serde::Deserialize<'de>,
|
||||||
|
{
|
||||||
|
println!("{}", url);
|
||||||
|
let api_result = match self.client.get(url).send().await {
|
||||||
|
Ok(r) => r,
|
||||||
|
Err(e) => return Err(Box::new(e)),
|
||||||
|
};
|
||||||
|
|
||||||
|
match api_result.status() {
|
||||||
|
StatusCode::OK => {
|
||||||
|
// TODO: handle errors here
|
||||||
|
let content_type = api_result
|
||||||
|
.headers()
|
||||||
|
.get("content-type")
|
||||||
|
.and_then(|v| v.to_str().ok())
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
if content_type.contains("application/json") {
|
||||||
|
match api_result.json::<T>().await {
|
||||||
|
Ok(j) => Ok(Response::Json(j)),
|
||||||
|
Err(e) => return Err(Box::new(e)),
|
||||||
|
}
|
||||||
|
} else if content_type.contains("application/xml") {
|
||||||
|
match api_result.text().await {
|
||||||
|
Ok(x) => Ok(Response::Xml(x)),
|
||||||
|
Err(e) => return Err(Box::new(e)),
|
||||||
|
}
|
||||||
|
} else if content_type.starts_with("text/") {
|
||||||
|
match api_result.text().await {
|
||||||
|
Ok(t) => Ok(Response::Text(t)),
|
||||||
|
Err(e) => return Err(Box::new(e)),
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
match api_result.bytes().await {
|
||||||
|
Ok(b) => Ok(Response::Bytes(b.to_vec())),
|
||||||
|
Err(e) => Err(Box::new(e)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
status => Err(Box::new(std::io::Error::new(
|
||||||
|
std::io::ErrorKind::Other,
|
||||||
|
format!("Unexpected status code: {}", status),
|
||||||
|
))),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
73
backend/task/src/utils/upload.rs
Normal file
73
backend/task/src/utils/upload.rs
Normal file
@ -0,0 +1,73 @@
|
|||||||
|
use aws_config::{BehaviorVersion, Region};
|
||||||
|
use aws_sdk_s3::{config::Credentials, Client, Config};
|
||||||
|
use std::env;
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct S3ClientConfig {
|
||||||
|
pub access_key: String,
|
||||||
|
secret_key: String,
|
||||||
|
endpoint: String,
|
||||||
|
pub bucket: String,
|
||||||
|
region: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl S3ClientConfig {
|
||||||
|
pub fn from_env() -> Result<Self, Box<dyn std::error::Error>> {
|
||||||
|
Ok(S3ClientConfig {
|
||||||
|
access_key: env::var("LINODE_ACCESS_KEY")
|
||||||
|
.map_err(|_| "LINODE_ACCESS_KEY environment variable not set")?,
|
||||||
|
secret_key: env::var("LINODE_SECRET_KEY")
|
||||||
|
.map_err(|_| "LINODE_SECRET_KEY environment variable not set")?,
|
||||||
|
endpoint: env::var("LINODE_ENDPOINT")
|
||||||
|
.unwrap_or_else(|_| "us-ord-1.linodeobjects.com".to_string()),
|
||||||
|
bucket: env::var("LINODE_BUCKET")
|
||||||
|
.map_err(|_| "LINODE_BUCKET environment variable not set")?,
|
||||||
|
region: env::var("LINODE_REGION").unwrap_or_else(|_| "us-ord".to_string()),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn create_s3_client(
|
||||||
|
config: &S3ClientConfig,
|
||||||
|
) -> Result<Client, Box<dyn std::error::Error>> {
|
||||||
|
let credentials = Credentials::new(
|
||||||
|
&config.access_key,
|
||||||
|
&config.secret_key,
|
||||||
|
None,
|
||||||
|
None,
|
||||||
|
"linode-object-storage",
|
||||||
|
);
|
||||||
|
|
||||||
|
let s3_config = Config::builder()
|
||||||
|
.behavior_version(BehaviorVersion::latest())
|
||||||
|
.region(Region::new(config.region.clone()))
|
||||||
|
.endpoint_url(format!("https://{}", config.endpoint))
|
||||||
|
.credentials_provider(credentials)
|
||||||
|
.force_path_style(false)
|
||||||
|
.build();
|
||||||
|
|
||||||
|
Ok(Client::from_conf(s3_config))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn upload(
|
||||||
|
client: &Client,
|
||||||
|
bucket: &str,
|
||||||
|
key: &str,
|
||||||
|
content: &str,
|
||||||
|
) -> Result<(), Box<dyn std::error::Error>> {
|
||||||
|
println!("Uploading to Linode Object Storage...");
|
||||||
|
println!("Bucket: {}", bucket);
|
||||||
|
|
||||||
|
let put_object_req = client
|
||||||
|
.put_object()
|
||||||
|
.bucket(bucket)
|
||||||
|
.key(key)
|
||||||
|
.body(content.as_bytes().to_vec().into())
|
||||||
|
.acl(aws_sdk_s3::types::ObjectCannedAcl::PublicRead)
|
||||||
|
.content_type("application/rss+xml")
|
||||||
|
.send()
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
println!("Upload successful! ETag: {:?}", put_object_req.e_tag());
|
||||||
|
Ok(())
|
||||||
|
}
|
16
config.kdl
16
config.kdl
@ -1,16 +0,0 @@
|
|||||||
layout {
|
|
||||||
pane {
|
|
||||||
pane
|
|
||||||
pane split_direction="horizontal" {
|
|
||||||
pane
|
|
||||||
pane
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
keybinds {
|
|
||||||
unbind "Ctrl s"
|
|
||||||
}
|
|
||||||
|
|
||||||
theme "catppuccin-mocha"
|
|
||||||
|
|
@ -61,19 +61,22 @@
|
|||||||
wget
|
wget
|
||||||
nixpkgs-fmt
|
nixpkgs-fmt
|
||||||
openssl
|
openssl
|
||||||
|
openssl.dev
|
||||||
patchelf
|
patchelf
|
||||||
deno
|
deno
|
||||||
sqlx-cli
|
sqlx-cli
|
||||||
cargo-watch
|
cargo-watch
|
||||||
cargo-chef
|
cargo-chef
|
||||||
valkey
|
valkey
|
||||||
|
pkg-config-unwrapped
|
||||||
];
|
];
|
||||||
|
|
||||||
# Environment variables
|
# Environment variables
|
||||||
env = {
|
env = {
|
||||||
RUST_BACKTRACE = "1";
|
RUST_BACKTRACE = "1";
|
||||||
RUST_SRC_PATH = "${pkgs.rustToolchain}/lib/rustlib/src/rust/library";
|
RUST_SRC_PATH = "${pkgs.rustToolchain}/lib/rustlib/src/rust/library";
|
||||||
ZELLIJ_CONFIG_FILE = "config.kdl";
|
PKG_CONFIG_PATH = "${pkgs.openssl.dev}/lib/pkgconfig";
|
||||||
|
# ZELLIJ_CONFIG_FILE = "config.kdl";
|
||||||
# PATH = "$PATH:$HOME/.local/share/nvim/mason/bin/deno";
|
# PATH = "$PATH:$HOME/.local/share/nvim/mason/bin/deno";
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
@ -3,7 +3,7 @@ import { Post } from "../types/index.ts";
|
|||||||
export const PostBody = function PostBody({ post }: PostBodyOpts) {
|
export const PostBody = function PostBody({ post }: PostBodyOpts) {
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
class="p-6 bg-[#313244] shadow-md text-[#f5e0dc]"
|
class="p-6 bg-[#313244] shadow-md text-[#f5e0dc] post-content"
|
||||||
dangerouslySetInnerHTML={{ __html: post.body }}
|
dangerouslySetInnerHTML={{ __html: post.body }}
|
||||||
></div>
|
></div>
|
||||||
);
|
);
|
||||||
|
@ -1,8 +1,13 @@
|
|||||||
|
import { Head } from "$fresh/runtime.ts";
|
||||||
import { Post } from "../types/index.ts";
|
import { Post } from "../types/index.ts";
|
||||||
import { convertUtc } from "../lib/convertUtc.ts";
|
import { convertUtc } from "../lib/convertUtc.ts";
|
||||||
|
|
||||||
export const PostHeader = function PostHeader({ post }: PostHeaderOpts) {
|
export const PostHeader = function PostHeader({ post }: PostHeaderOpts) {
|
||||||
return (
|
return (
|
||||||
|
<>
|
||||||
|
<Head>
|
||||||
|
<title>Wyatt J. Miller | {post.title}</title>
|
||||||
|
</Head>
|
||||||
<div class="p-6 bg-[#313244] shadow-md">
|
<div class="p-6 bg-[#313244] shadow-md">
|
||||||
<div class="min-w-screen flex flex-col items-center justify-between bg-[#45475a] rounded-lg shadow-md">
|
<div class="min-w-screen flex flex-col items-center justify-between bg-[#45475a] rounded-lg shadow-md">
|
||||||
<div class="sm:mt-14 sm:mb-14 mt-8 mb-8 flex flex-col items-center gap-y-5 gap-x-10 md:flex-row">
|
<div class="sm:mt-14 sm:mb-14 mt-8 mb-8 flex flex-col items-center gap-y-5 gap-x-10 md:flex-row">
|
||||||
@ -18,6 +23,7 @@ export const PostHeader = function PostHeader({ post }: PostHeaderOpts) {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
14
frontend/components/ShareLinkButton.tsx
Normal file
14
frontend/components/ShareLinkButton.tsx
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
export const ShareLinkButton = function ShareLinkButton({props}) {
|
||||||
|
const [text. setText] = useState("Share");
|
||||||
|
|
||||||
|
const onClickHandler = () => {
|
||||||
|
navigator.clipboard.writeText(location.href);
|
||||||
|
setText("Copied to clipboard!");
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<button onClick={onClickHandler}>
|
||||||
|
{text}
|
||||||
|
</button>
|
||||||
|
)
|
||||||
|
}
|
@ -11,15 +11,10 @@
|
|||||||
},
|
},
|
||||||
"lint": {
|
"lint": {
|
||||||
"rules": {
|
"rules": {
|
||||||
"tags": [
|
"tags": ["fresh", "recommended"]
|
||||||
"fresh",
|
|
||||||
"recommended"
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"exclude": [
|
"exclude": ["**/_fresh/*"],
|
||||||
"**/_fresh/*"
|
|
||||||
],
|
|
||||||
"imports": {
|
"imports": {
|
||||||
"$fresh/": "https://deno.land/x/fresh@1.6.8/",
|
"$fresh/": "https://deno.land/x/fresh@1.6.8/",
|
||||||
"$std/": "https://deno.land/std@0.216.0/",
|
"$std/": "https://deno.land/std@0.216.0/",
|
||||||
@ -33,7 +28,8 @@
|
|||||||
"preact/jsx-runtime": "npm:preact@10.22.1/jsx-runtime",
|
"preact/jsx-runtime": "npm:preact@10.22.1/jsx-runtime",
|
||||||
"tailwindcss": "npm:tailwindcss@3.4.1",
|
"tailwindcss": "npm:tailwindcss@3.4.1",
|
||||||
"tailwindcss/": "npm:/tailwindcss@3.4.1/",
|
"tailwindcss/": "npm:/tailwindcss@3.4.1/",
|
||||||
"tailwindcss/plugin": "npm:/tailwindcss@3.4.1/plugin.js"
|
"tailwindcss/plugin": "npm:/tailwindcss@3.4.1/plugin.js",
|
||||||
|
"tailwind-highlightjs": "npm:tailwind-highlightjs"
|
||||||
},
|
},
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"jsx": "react-jsx",
|
"jsx": "react-jsx",
|
||||||
|
@ -27,7 +27,6 @@ export const handler: Handlers<PageData> = {
|
|||||||
|
|
||||||
export default function PostIdentifier({ data }: PageProps<PageData>) {
|
export default function PostIdentifier({ data }: PageProps<PageData>) {
|
||||||
const { postData } = data;
|
const { postData } = data;
|
||||||
console.log(postData);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
|
@ -1,6 +1,32 @@
|
|||||||
|
import { FreshContext, Handlers, PageProps } from "$fresh/server.ts";
|
||||||
import { ProjectCard } from "../../islands/ProjectCard.tsx";
|
import { ProjectCard } from "../../islands/ProjectCard.tsx";
|
||||||
|
|
||||||
export default function Projects() {
|
interface ProjectData {
|
||||||
|
project_id: number;
|
||||||
|
title: string;
|
||||||
|
repo?: string;
|
||||||
|
summary: string;
|
||||||
|
tech: string;
|
||||||
|
wip?: boolean;
|
||||||
|
created_at: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const handler: Handlers<ProjectData> = {
|
||||||
|
async GET(_req: Request, ctx: FreshContext) {
|
||||||
|
const projectResult = await fetch(
|
||||||
|
`${Deno.env.get("BASE_URI_API")}/projects`,
|
||||||
|
);
|
||||||
|
|
||||||
|
const projectData = await projectResult.json();
|
||||||
|
return ctx.render({
|
||||||
|
projectData,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function Projects({ data }: PageProps<ProjectData>) {
|
||||||
|
const { projectData: projects } = data;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div class="space-y-12 px-10 py-8 sm:min-h-screen bg-[#313244]">
|
<div class="space-y-12 px-10 py-8 sm:min-h-screen bg-[#313244]">
|
||||||
<section
|
<section
|
||||||
@ -11,55 +37,17 @@ export default function Projects() {
|
|||||||
Projects
|
Projects
|
||||||
</h1>
|
</h1>
|
||||||
<div class="grid grid-cols-1 sm:grid-cols-2">
|
<div class="grid grid-cols-1 sm:grid-cols-2">
|
||||||
|
{projects.map((project: any) => {
|
||||||
|
return (
|
||||||
<ProjectCard
|
<ProjectCard
|
||||||
wip
|
title={project.title}
|
||||||
title="Website v2"
|
repo={project.repo ?? undefined}
|
||||||
summary="This website was built by yours truly!"
|
summary={project.summary}
|
||||||
// repo="https://scm.wyattjmiller.com/wymiller/my-website-v2"
|
tech={project.tech}
|
||||||
tech="Typescript, Deno, Fresh, Tailwind, Rust, PostgreSQL, Docker"
|
wip={project.wip ?? true}
|
||||||
/>
|
|
||||||
<ProjectCard
|
|
||||||
title="BallBot"
|
|
||||||
repo="https://scm.wyattjmiller.com/wymiller/ballbot"
|
|
||||||
summary="A Discord bot that tells me NFL games, teams, and more!"
|
|
||||||
tech="Rust, Discord SDK, Docker"
|
|
||||||
/>
|
|
||||||
<ProjectCard
|
|
||||||
title="Nix configurations"
|
|
||||||
repo="https://scm.wyattjmiller.com/wymiller/nix-config-v2"
|
|
||||||
summary="My 'master' declarative system configuration for multiple computers"
|
|
||||||
tech="Nix"
|
|
||||||
/>
|
|
||||||
<ProjectCard
|
|
||||||
wip
|
|
||||||
title="omega"
|
|
||||||
summary="Music bot for Discord that plays music from different music sources"
|
|
||||||
tech="Rust, Discord SDK, SurrealDB, yt-dlp"
|
|
||||||
/>
|
|
||||||
<ProjectCard
|
|
||||||
title="gt"
|
|
||||||
repo="https://scm.wyattjmiller.com/wymiller/gt"
|
|
||||||
summary="Command line application to interact with Gitea"
|
|
||||||
tech="Rust"
|
|
||||||
/>
|
|
||||||
<ProjectCard
|
|
||||||
title="The Boyos Bot"
|
|
||||||
repo="https://github.com/NoahFlowa/BoyosBot"
|
|
||||||
summary="All-in-one Discord bot, built with my friend, NoahFlowa"
|
|
||||||
tech="Javascript, Node, Discord SDK, Docker"
|
|
||||||
/>
|
|
||||||
<ProjectCard
|
|
||||||
title="drillsergeant"
|
|
||||||
repo="https://scm.wyattjmiller.com/wymiller/drillsergeant"
|
|
||||||
summary="Git commit counter, to scratch an itch I had"
|
|
||||||
tech="C#, .NET"
|
|
||||||
/>
|
|
||||||
<ProjectCard
|
|
||||||
title="bleak"
|
|
||||||
repo="https://scm.wyattjmiller.com/wymiller/bleak"
|
|
||||||
summary="Turns your Raspberry Pi into a lighting controller"
|
|
||||||
tech="Rust"
|
|
||||||
/>
|
/>
|
||||||
|
);
|
||||||
|
})}
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
</div>
|
</div>
|
||||||
|
1
frontend/static/robots.txt
Normal file
1
frontend/static/robots.txt
Normal file
@ -0,0 +1 @@
|
|||||||
|
Sitemap: https://wyattjmiller.us-ord-1.linodeobjects.com/feed.xml
|
@ -1,3 +1,31 @@
|
|||||||
@tailwind base;
|
@tailwind base;
|
||||||
@tailwind components;
|
@tailwind components;
|
||||||
@tailwind utilities;
|
@tailwind utilities;
|
||||||
|
|
||||||
|
.post-content h1 {
|
||||||
|
@apply text-3xl font-bold text-[#f5e0dc] mb-4 mt-6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.post-content h2 {
|
||||||
|
@apply text-2xl font-semibold text-[#f5e0dc] mb-3 mt-5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.post-content h3 {
|
||||||
|
@apply text-xl font-medium text-[#f5e0dc] mb-2 mt-4;
|
||||||
|
}
|
||||||
|
|
||||||
|
.post-content h4 {
|
||||||
|
@apply text-lg font-medium text-[#f5e0dc] mb-2 mt-3;
|
||||||
|
}
|
||||||
|
|
||||||
|
.post-content p {
|
||||||
|
@apply mb-3 text-[#f5e0dc];
|
||||||
|
}
|
||||||
|
|
||||||
|
.post-content pre {
|
||||||
|
@apply overflow-x-scroll bg-[#454656] p-2 mb-4 rounded-lg;
|
||||||
|
}
|
||||||
|
|
||||||
|
.post-content code {
|
||||||
|
@apply text-[#DCC9C6];
|
||||||
|
}
|
||||||
|
@ -1,5 +1,7 @@
|
|||||||
import { type Config } from "tailwindcss";
|
import { type Config } from "tailwindcss";
|
||||||
|
import twHLJS from "tailwind-highlightjs";
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
content: ["{routes,islands,components}/**/*.{ts,tsx}"],
|
content: ["{routes,islands,components}/**/*.{ts,tsx}"],
|
||||||
|
// plugins: [twHLJS],
|
||||||
} satisfies Config;
|
} satisfies Config;
|
||||||
|
Reference in New Issue
Block a user