Compare commits

...

32 Commits

Author SHA1 Message Date
3ac59f65f2 Changed current music 2026-01-18 13:31:03 +01:00
4a25c377c4 Added an "ip" page 2025-11-28 22:44:09 +01:00
8c64c5dbe3 Added style for music 2025-11-24 21:22:44 +01:00
88abbe8c14 Added music route to main 2025-11-24 21:22:34 +01:00
c5b85bc359 Added link to music to navbar 2025-11-24 21:22:17 +01:00
8a27a74e9f Created page music 2025-11-24 21:21:56 +01:00
425bac6776 Added iframe html element 2025-11-24 21:21:36 +01:00
5c456fd5cd Formatted file 2025-11-24 21:21:19 +01:00
08b5932cf8 Added robots.txt 2025-11-13 14:31:56 +01:00
c7377e06e3 Added robots.txt (to be verified working) 2025-11-06 11:08:27 +01:00
4e53f9ed1f Added database for projects 2025-10-27 14:28:37 +01:00
d692bf1cdd Changed config file to an example to be able to use local dev file 2025-10-27 09:00:52 +01:00
ec126bbdc4 Formatted project 2025-10-27 08:45:45 +01:00
0fe13b6ce7 Added database to AppState 2025-10-27 08:45:35 +01:00
9c79881c4b Added link to the repository on the project page 2025-10-26 22:36:20 +01:00
f6b6b0a80b Fixed issues with Content-Type headers. If one is present it should be modified without adding a new one (which would be disregarded by the browser) 2025-10-26 22:25:00 +01:00
146509efdf Refactored project to use Builder pattern. 2025-10-26 22:01:13 +01:00
a908bf0e8a Added comment to explicit the usage of a custom config file 2025-10-25 14:04:12 +02:00
aeff2b1759 Reworking elements to implement the Builder pattern 2025-10-25 14:03:37 +02:00
b1cb7dff27 Followed clippy recommendations 2025-10-24 12:13:36 +02:00
d50a6a3d76 Changed "application" section to "server" 2025-10-24 12:13:20 +02:00
f9726f7b35 Added command line argument for the config file location 2025-10-24 12:13:02 +02:00
48d59b6f90 Added service file to run server on linux (systemd) 2025-10-06 22:33:48 +02:00
f01659ea50 Changed file serving to use asset directory from config 2025-10-06 22:33:32 +02:00
2500fdc79b Added configuration file 2025-10-06 22:32:59 +02:00
99ebee7f35 Modified css 2025-10-06 22:32:37 +02:00
3e2987ad0c Updated pages to use the changed html elements 2025-10-06 22:32:18 +02:00
bcd50d3a17 Changed requirements for values that implement Into<String> 2025-10-06 22:31:41 +02:00
e63edda9ca Added Projects page 2025-10-06 16:46:49 +02:00
a457efb97e Added middleware for mimetype 2025-10-06 16:46:06 +02:00
4eaf54dd8f Move common css to base 2025-10-06 16:45:45 +02:00
94b68982f6 Modified elements to take static str (will change to &str) 2025-10-06 16:45:07 +02:00
23 changed files with 1457 additions and 229 deletions

1
.gitignore vendored
View File

@@ -2,3 +2,4 @@
.idea .idea
Cargo.lock Cargo.lock
.env .env
config.toml

View File

@@ -7,4 +7,7 @@ edition = "2024"
actix-web = {version = "4.11.0"} actix-web = {version = "4.11.0"}
serde = "1.0.228" serde = "1.0.228"
serde_json = "1.0.145" serde_json = "1.0.145"
sqlx = {version = "0.8.6", features = ["postgres"]} toml = "0.9.7"
sqlx = {version = "0.8.6", features = ["postgres", "runtime-tokio"]}
futures-util = "0.3.31"
clap = { version = "4.5.50", features = ["derive"] }

17
aindustries-be.service Normal file
View File

@@ -0,0 +1,17 @@
[Unit]
Description=aindustries.be website
After=network.target
[Service]
Type=simple
Restart=always
RestartSec=1
# Change with proper user to run the service
User=web
# Change with the installation location of the executable
# It is possible to use a custom config file location
# by adding '--config /path/to/config.toml' to the command bellow
ExecStart=/usr/share/bin/aindustries-be
[Install]
WantedBy=multi-user.target

View File

@@ -11,6 +11,7 @@
--clr-primary-a30: #c98bcc; --clr-primary-a30: #c98bcc;
--clr-primary-a40: #d29ed4; --clr-primary-a40: #d29ed4;
--clr-primary-a50: #dcb1dd; --clr-primary-a50: #dcb1dd;
--clr-primary-a60: #e1d7e1;
/** Theme surface colors */ /** Theme surface colors */
--clr-surface-a0: #121212; --clr-surface-a0: #121212;
@@ -59,6 +60,54 @@
object-fit: cover; object-fit: cover;
} }
.header {
display: flex;
flex-direction: row;
height: 40px;
/* width: 100%; */
background-color: var(--clr-primary-a0);
border-radius: 8px;
filter: drop-shadow(0 0 8px rgba(0, 0, 0, 0.2));
margin: 8px;
}
.name {
color: var(--clr-light-a0);
font-size: 18px;
margin: auto 0 auto 8px;
font-family: Lexend, serif;
}
.nav-buttons {
display: flex;
flex-direction: row;
justify-content: flex-end;
align-items: center;
width: 100%;
height: 100%;
margin: 0 8px 0 auto;
}
.nav-button {
padding: 4px 8px 4px 8px;
border-radius: 8px;
filter: drop-shadow(0 0 8px rgba(0, 0, 0, 0.2));
background-color: transparent;
color: white;
border: none;
font-size: 18px;
font-family: Lexend, serif;
}
.nav-button:hover {
text-decoration-line: underline;
text-decoration-style: wavy;
background-color: var(--clr-primary-a50);
color: purple;
text-decoration-color: purple;
}
body { body {
background-color: #32004f; background-color: #32004f;
margin: 0; margin: 0;

View File

@@ -1,21 +1,3 @@
.header {
display: flex;
flex-direction: row;
height: 40px;
/* width: 100%; */
background-color: var(--clr-primary-a0);
border-radius: 8px;
filter: drop-shadow(0 0 8px rgba(0, 0, 0, 0.2));
margin: 8px;
}
.name {
color: var(--clr-light-a0);
font-size: 18px;
margin: auto 0 auto 8px;
font-family: Lexend, serif;
}
.intro { .intro {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
@@ -23,51 +5,16 @@
max-width: 75vw; max-width: 75vw;
} }
.nav-buttons {
display: flex;
flex-direction: row;
justify-content: flex-end;
align-items: center;
width: 100%;
height: 100%;
margin: 0 8px 0 auto;
}
.nav-button {
padding: 4px 8px 4px 8px;
border-radius: 8px;
filter: drop-shadow(0 0 8px rgba(0, 0, 0, 0.2));
background-color: transparent;
color: white;
border: none;
font-size: 18px;
font-family: Lexend, serif;
}
.nav-button:hover {
text-decoration-line: underline;
text-decoration-style: wavy;
background-color: var(--clr-primary-a50);
color: purple;
text-decoration-color: purple;
}
.note {
color: gray;
font-size: 18px;
text-align: center;
}
h1 { h1 {
font-size: 68px; font-size: 68px;
font-family: 'Indie Flower', cursive; font-family: 'Indie Flower', cursive;
color: var(--clr-primary-a50); color: var(--clr-primary-a60);
} }
h2 { h2 {
font-size: 42px; font-size: 42px;
font-family: 'Indie Flower', cursive; font-family: 'Indie Flower', cursive;
color: var(--clr-primary-a50); color: var(--clr-primary-a60);
} }
@media screen and (width<700px) { @media screen and (width<700px) {

13
assets/css/ip.css Normal file
View File

@@ -0,0 +1,13 @@
.ip {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
}
.ip h1 {
font-family: 'Indie Flower', cursive;
font-size: 60px;
text-align: center;
color: var(--clr-primary-a60);
}

58
assets/css/music.css Normal file
View File

@@ -0,0 +1,58 @@
.featured {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 16px;
}
.featured h1 {
font-size: 48px;
text-align: center;
font-family: 'Indie Flower', cursive;
color: var(--clr-primary-a60);
max-width: 75vw;
}
.featured iframe {
width: 55vw;
height: 100%;
aspect-ratio: 16 / 9;
border: none;
filter: drop-shadow(0px 0px 12px rgba(0, 0, 0, 0.75));
}
.playlist {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 16px;
}
.playlist h1 {
font-size: 40px;
text-align: center;
font-family: 'Indie Flower', cursive;
color: var(--clr-primary-a60);
max-width: 75vw;
}
.playlist iframe {
width: 55vw;
height: 100%;
aspect-ratio: 16 / 9;
border: none;
filter: drop-shadow(0px 0px 12px rgba(0, 0, 0, 0.75));
}
@media (width <= 1250px) {
.featured iframe {
width: 90vw;
}
.playlist iframe {
width: 90vw;
}
}

21
assets/css/project.css Normal file
View File

@@ -0,0 +1,21 @@
h1 {
font-family: 'Indie Flower', cursive;
font-size: 75px;
text-align: center;
color: var(--clr-primary-a60);
}
p {
font-family: Lexend, serif;
color: var(--clr-primary-a60);
width: fit-content;
margin: 8px auto 8px auto;
font-size: 24px;
}
@media (width <= 750px) {
p {
max-width: 90vw;
font-size: 20px;
}
}

66
assets/css/projects.css Normal file
View File

@@ -0,0 +1,66 @@
.project {
display: flex;
flex-direction: column;
background-image: radial-gradient(circle at top left, var(--clr-primary-a10), var(--clr-primary-a50));
border-radius: 12px;
max-width: 40vw;
height: 250px;
margin: 8px auto 8px auto;
padding: 8px;
}
.project-info {
display: flex;
flex-direction: row;
justify-content: space-between;
align-content: center;
margin: 0 4px 4px 4px;
}
.project > h1 {
flex-grow: 1;
color: white;
font-family: 'Indie Flower', cursive;
width: fit-content;
font-size: 40px;
margin: 8px;
}
.project-desc {
color: white;
font-family: Lexend, serif;
width: fit-content;
margin: 0;
}
.project-view {
align-self: flex-end;
width: fit-content;
font-family: Lexend, serif;
text-align: right;
}
h1 {
font-family: 'Indie Flower', cursive;
font-size: 60px;
text-align: center;
color: var(--clr-primary-a60);
}
h2 {
font-family: 'Indie Flower', cursive;
font-size: 40px;
text-align: center;
color: var(--clr-primary-a60);
}
@media (width <= 750px) {
.project {
max-width: 80vw;
height: 150px;
}
h2 {
font-size: 24px;
}
}

11
config.toml.example Normal file
View File

@@ -0,0 +1,11 @@
[server]
# Change with actual location of assets
assets="./assets"
# Set bind address
bind="127.0.0.1:8080"
[database]
address="127.0.0.1:5432"
user="web"
password="apassword"
database_name="website"

View File

@@ -0,0 +1,13 @@
CREATE TABLE IF NOT EXISTS Projects (
"id" TEXT NOT NULL,
"title" TEXT NOT NULL,
"description" TEXT NOT NULL
);
CREATE TABLE IF NOT EXISTS Project_Detail (
"id" TEXT PRIMARY KEY NOT NULL,
"title" TEXT NOT NULL,
"git_url" TEXT NOT NULL,
"git_title" TEXT NOT NULL,
"description" TEXT NOT NULL
)

View File

@@ -0,0 +1,13 @@
INSERT INTO Projects ("id", "title", "description") VALUES (
'website',
'website',
'This project is the website you currently are on.'
);
INSERT INTO Project_Detail ("id", "title", "git_url", "git_title", "description") VALUES (
'website',
'website',
'https://git.aindustries.be/AINDUSTRIES/aindustries.be',
'aindustries-be',
'This project, the website you are on, is made in Rust such that all the pages are generated by code. <br> That is that each html element is represented by a struct which implements the Render trait (as in render the element to html). <br> As it is right now the system is not that impressive but I believe it can do amazing things in the future.'
)

View File

@@ -1,151 +1,504 @@
use crate::html::Render; use crate::html::Render;
#[allow(non_camel_case_types)] pub(crate) struct Heading {
pub(crate) struct h1 { level: u8,
id: String,
text: String, text: String,
classes: Vec<String>,
} }
impl Render for h1 { impl Render for Heading {
fn render(&self) -> String { fn render(&self) -> String {
format!("<h1>{}</h1>", self.text) let classes = self.classes.join(" ");
format!(
"<h{} id=\"{}\" class=\"{}\">{}</h{}>",
self.level, self.id, classes, self.text, self.level
)
} }
} }
impl h1 { impl Heading {
pub(crate) fn new(text: String) -> Self { pub(crate) fn builder() -> HeadingBuilder {
Self { text } HeadingBuilder::new()
} }
} }
#[allow(non_camel_case_types)] pub(crate) struct HeadingBuilder {
pub(crate) struct h2 { level: Option<u8>,
text: String, id: Option<String>,
text: Option<String>,
classes: Option<Vec<String>>,
} }
impl Render for h2 { impl HeadingBuilder {
fn render(&self) -> String { pub(crate) fn new() -> Self {
format!("<h2>{}</h2>", self.text) Self {
level: None,
id: None,
text: None,
classes: None,
}
}
pub(crate) fn level(self, level: u8) -> Self {
Self {
level: Some(level),
..self
}
}
pub(crate) fn id<T>(self, id: T) -> Self
where
T: Into<String>,
{
Self {
id: Some(id.into()),
..self
}
}
pub(crate) fn text<T>(self, text: T) -> Self
where
T: Into<String>,
{
Self {
text: Some(text.into()),
..self
}
}
pub(crate) fn classes<T>(self, classes: Vec<T>) -> Self
where
T: Into<String>,
{
Self {
classes: Some(classes.into_iter().map(|x| x.into()).collect()),
..self
}
}
pub(crate) fn build(self) -> Heading {
let level = self.level.unwrap_or(1);
let id = self.id.unwrap_or(String::new());
let text = self.text.unwrap_or(String::new());
let classes = self.classes.unwrap_or(Vec::new());
Heading {
level,
id,
text,
classes,
}
} }
} }
impl h2 { pub(crate) struct Link {
pub(crate) fn new(text: String) -> Self {
Self { text }
}
}
#[allow(non_camel_case_types)]
pub(crate) struct link {
rel: String, rel: String,
href: String, href: String,
} }
impl Render for link { impl Render for Link {
fn render(&self) -> String { fn render(&self) -> String {
format!("<link rel=\"{}\" href=\"{}\">", self.rel, self.href) format!("<link rel=\"{}\" href=\"{}\">", self.rel, self.href)
} }
} }
impl link { impl Link {
pub(crate) fn new(rel: String, href: String) -> Self { pub(crate) fn builder() -> LinkBuilder {
Self { rel, href } LinkBuilder::new()
} }
} }
#[allow(non_camel_case_types)] pub(crate) struct LinkBuilder {
pub(crate) struct div { rel: Option<String>,
id: String, href: Option<String>,
classes: Vec<String>,
content: Vec<Box<dyn Render>>,
} }
impl Render for div { impl LinkBuilder {
pub(crate) fn new() -> Self {
Self {
rel: None,
href: None,
}
}
pub(crate) fn rel<T>(self, rel: T) -> Self
where
T: Into<String>,
{
Self {
rel: Some(rel.into()),
..self
}
}
pub(crate) fn href<T>(self, href: T) -> Self
where
T: Into<String>,
{
Self {
href: Some(href.into()),
..self
}
}
pub(crate) fn build(self) -> Link {
let rel = self.rel.unwrap_or(String::new());
let href = self.href.unwrap_or(String::new());
Link { rel, href }
}
}
pub(crate) struct Paragraph {
id: String,
classes: Vec<String>,
text: String,
}
impl Render for Paragraph {
fn render(&self) -> String { fn render(&self) -> String {
let classes = self.classes.join(" "); let classes = self.classes.join(" ");
format!( format!(
"<div id=\"{}\" class=\"{}\">{}</div>", "<p id=\"{}\" class=\"{}\">{}</p>",
self.id, self.id, classes, self.text
classes,
self.content.render()
) )
} }
} }
impl div { impl Paragraph {
pub(crate) fn new(id: String, classes: Vec<String>) -> Self { pub(crate) fn builder() -> ParagraphBuilder {
ParagraphBuilder::new()
}
}
pub(crate) struct ParagraphBuilder {
id: Option<String>,
classes: Option<Vec<String>>,
text: Option<String>,
}
impl ParagraphBuilder {
pub(crate) fn new() -> Self {
Self { Self {
id, id: None,
classes, classes: None,
content: vec![], text: None,
} }
} }
pub(crate) fn append_element(&mut self, element: impl Render + 'static) { pub(crate) fn id<T>(self, id: T) -> Self
self.content.push(Box::new(element)); where
} T: Into<String>,
} {
#[allow(non_camel_case_types)]
pub(crate) struct p {
id: String,
classes: Vec<String>,
text: String
}
impl Render for p {
fn render(&self) -> String {
let classes = self.classes.join(" ");
format!("<p id=\"{}\" class=\"{}\">{}</p>", self.id, classes, self.text)
}
}
impl p {
pub(crate) fn new(id: String, classes: Vec<String>, text: String) -> Self {
Self { Self {
id, id: Some(id.into()),
classes, ..self
text
} }
} }
pub(crate) fn classes<T>(self, classes: Vec<T>) -> Self
where
T: Into<String>,
{
Self {
classes: Some(classes.into_iter().map(|x| x.into()).collect()),
..self
}
}
pub(crate) fn text<T>(self, text: T) -> Self
where
T: Into<String>,
{
Self {
text: Some(text.into()),
..self
}
}
pub(crate) fn build(self) -> Paragraph {
let id = self.id.unwrap_or(String::new());
let classes = self.classes.unwrap_or(Vec::new());
let text = self.text.unwrap_or(String::new());
Paragraph { id, classes, text }
}
} }
#[allow(non_camel_case_types)] pub(crate) struct Image {
pub(crate) struct img {
id: String, id: String,
classes: Vec<String>, classes: Vec<String>,
src: String, src: String,
} }
impl Render for img { impl Render for Image {
fn render(&self) -> String { fn render(&self) -> String {
let classes = self.classes.join(" "); let classes = self.classes.join(" ");
format!("<img id=\"{}\" class=\"{}\" src=\"{}\">", self.id, classes, self.src) format!(
"<img id=\"{}\" class=\"{}\" src=\"{}\">",
self.id, classes, self.src
)
} }
} }
impl img { impl Image {
pub(crate) fn new(id: String, classes: Vec<String>, src: String) -> Self { pub(crate) fn builder() -> ImageBuilder {
Self { id, classes, src } ImageBuilder::new()
} }
} }
#[allow(non_camel_case_types)] pub(crate) struct ImageBuilder {
pub(crate) struct a { id: Option<String>,
classes: Option<Vec<String>>,
src: Option<String>,
}
impl ImageBuilder {
pub(crate) fn new() -> Self {
Self {
id: None,
classes: None,
src: None,
}
}
pub(crate) fn id<T>(self, id: T) -> Self
where
T: Into<String>,
{
Self {
id: Some(id.into()),
..self
}
}
pub(crate) fn classes<T>(self, classes: Vec<T>) -> Self
where
T: Into<String>,
{
Self {
classes: Some(classes.into_iter().map(|x| x.into()).collect()),
..self
}
}
pub(crate) fn src<T>(self, src: T) -> Self
where
T: Into<String>,
{
Self {
src: Some(src.into()),
..self
}
}
pub(crate) fn build(self) -> Image {
let id = self.id.unwrap_or(String::new());
let classes = self.classes.unwrap_or(Vec::new());
let src = self.src.unwrap_or(String::new());
Image { id, classes, src }
}
}
pub(crate) struct Anchor {
id: String, id: String,
classes: Vec<String>, classes: Vec<String>,
href: String, href: String,
text: String, text: String,
} }
impl Render for a { impl Render for Anchor {
fn render(&self) -> String { fn render(&self) -> String {
let classes = self.classes.join(" "); let classes = self.classes.join(" ");
format!("<a id=\"{}\" class=\"{}\" href=\"{}\">{}</a>", self.id, classes, self.href, self.text) format!(
"<a id=\"{}\" class=\"{}\" href=\"{}\">{}</a>",
self.id, classes, self.href, self.text
)
} }
} }
impl a { impl Anchor {
pub(crate) fn new(id: String, classes: Vec<String>, href: String, text: String) -> Self { pub(crate) fn builder() -> AnchorBuilder {
Self { id, classes, href, text } AnchorBuilder::new()
}
}
pub(crate) struct AnchorBuilder {
id: Option<String>,
classes: Option<Vec<String>>,
href: Option<String>,
text: Option<String>,
}
impl AnchorBuilder {
pub(crate) fn new() -> Self {
Self {
id: None,
classes: None,
href: None,
text: None,
}
}
pub(crate) fn id<T>(self, id: T) -> Self
where
T: Into<String>,
{
Self {
id: Some(id.into()),
..self
}
}
pub(crate) fn classes<T>(self, classes: Vec<T>) -> Self
where
T: Into<String>,
{
Self {
classes: Some(classes.into_iter().map(|x| x.into()).collect()),
..self
}
}
pub(crate) fn href<T>(self, href: T) -> Self
where
T: Into<String>,
{
Self {
href: Some(href.into()),
..self
}
}
pub(crate) fn text<T>(self, text: T) -> Self
where
T: Into<String>,
{
Self {
text: Some(text.into()),
..self
}
}
pub(crate) fn build(self) -> Anchor {
let id = self.id.unwrap_or(String::new());
let classes = self.classes.unwrap_or(Vec::new());
let href = self.href.unwrap_or(String::new());
let text = self.text.unwrap_or(String::new());
Anchor {
id,
classes,
href,
text,
}
}
}
pub(crate) struct Iframe {
id: String,
classes: Vec<String>,
src: String,
title: String,
width: usize,
height: usize,
}
impl Render for Iframe {
fn render(&self) -> String {
let classes = self.classes.join(" ");
format!(
"<iframe id=\"{}\" classes=\"{}\" title=\"{}\" width=\"{}\" height=\"{}\" src=\"{}\"></iframe>",
self.id, classes, self.title, self.width, self.height, self.src
)
}
}
pub(crate) struct IframeBuilder {
id: Option<String>,
classes: Option<Vec<String>>,
src: Option<String>,
title: Option<String>,
width: Option<usize>,
height: Option<usize>,
}
impl IframeBuilder {
pub(crate) fn new() -> Self {
Self {
id: None,
classes: None,
src: None,
title: None,
width: None,
height: None,
}
}
pub(crate) fn id<T>(self, id: T) -> Self
where
T: Into<String>,
{
Self {
id: Some(id.into()),
..self
}
}
pub(crate) fn classes<T>(self, classes: Vec<T>) -> Self
where
T: Into<String>,
{
Self {
classes: Some(classes.into_iter().map(|x| x.into()).collect()),
..self
}
}
pub(crate) fn src<T>(self, src: T) -> Self
where
T: Into<String>,
{
Self {
src: Some(src.into()),
..self
}
}
pub(crate) fn title<T>(self, title: T) -> Self
where
T: Into<String>,
{
Self {
title: Some(title.into()),
..self
}
}
pub(crate) fn width(self, width: usize) -> Self {
Self {
width: Some(width),
..self
}
}
pub(crate) fn height(self, height: usize) -> Self {
Self {
height: Some(height),
..self
}
}
pub(crate) fn build(self) -> Iframe {
let id = self.id.unwrap_or(String::new());
let classes = self.classes.unwrap_or(Vec::new());
let title = self.title.unwrap_or(String::new());
let width = self.width.unwrap_or(0);
let height = self.height.unwrap_or(0);
let src = self.src.unwrap_or(String::new());
Iframe {
id,
title,
classes,
width,
height,
src,
}
} }
} }

83
src/html/layouts.rs Normal file
View File

@@ -0,0 +1,83 @@
use crate::html::Render;
pub(crate) struct Division {
id: String,
classes: Vec<String>,
elements: Vec<Box<dyn Render>>,
}
impl Render for Division {
fn render(&self) -> String {
let classes = self.classes.join(" ");
format!(
"<div id=\"{}\" class=\"{}\">{}</div>",
self.id,
classes,
self.elements.render()
)
}
}
impl Division {
pub(crate) fn builder() -> DivisionBuilder {
DivisionBuilder::new()
}
pub(crate) fn append_element(&mut self, element: impl Render + 'static) {
self.elements.push(Box::new(element));
}
}
pub(crate) struct DivisionBuilder {
id: Option<String>,
classes: Option<Vec<String>>,
elements: Option<Vec<Box<dyn Render>>>,
}
impl DivisionBuilder {
pub(crate) fn new() -> Self {
Self {
id: None,
classes: None,
elements: None,
}
}
pub(crate) fn id<T>(self, id: T) -> Self
where
T: Into<String>,
{
Self {
id: Some(id.into()),
..self
}
}
pub(crate) fn classes<T>(self, classes: Vec<T>) -> Self
where
T: Into<String>,
{
Self {
classes: Some(classes.into_iter().map(|x| x.into()).collect()),
..self
}
}
pub(crate) fn elements(self, elements: Vec<Box<dyn Render>>) -> Self {
Self {
elements: Some(elements),
..self
}
}
pub(crate) fn build(self) -> Division {
let id = self.id.unwrap_or(String::new());
let classes = self.classes.unwrap_or(Vec::new());
let elements = self.elements.unwrap_or(Vec::new());
Division {
id,
classes,
elements,
}
}
}

View File

@@ -1,63 +1,31 @@
pub(crate) mod elements; pub(crate) mod elements;
pub(crate) mod layouts;
pub(crate) mod pages;
macro_rules! boxed_vec {
($($element:ident),+) => {
vec![
$(
Box::new($element) as Box<dyn Render>,
)*
]
};
}
pub(crate) use boxed_vec;
pub(crate) trait Render { pub(crate) trait Render {
fn render(&self) -> String; fn render(&self) -> String;
} }
pub(crate) struct Page {
title: String,
head: Vec<Box<dyn Render>>,
body: Vec<Box<dyn Render>>,
}
impl Render for Vec<Box<dyn Render>> { impl Render for Vec<Box<dyn Render>> {
fn render(&self) -> String { fn render(&self) -> String {
let mut result = String::new(); let mut result = String::new();
for element in self { for element in self {
let render = element.render(); let render = element.render();
result.push_str(&render); result.push_str(&render);
result.push('\n');
} }
result result
} }
} }
impl Render for Page {
fn render(&self) -> String {
format!(
"\
<!DOCTYPE html>
<html lang=\"en\">
<head>
<meta charset=\"UTF-8\">
<title>{}</title>
<meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\" />
{}
</head>
<body>
{}
</body>
</html>",
self.title,
self.head.render(),
self.body.render()
)
}
}
impl Page {
pub(crate) fn new(title: String) -> Self {
Page {
title,
head: vec![],
body: vec![],
}
}
pub(crate) fn append_element_to_body(&mut self, element: impl Render + 'static) {
self.body.push(Box::new(element));
}
pub(crate) fn append_element_to_head(&mut self, element: impl Render + 'static) {
self.head.push(Box::new(element));
}
}

215
src/html/pages.rs Normal file
View File

@@ -0,0 +1,215 @@
use crate::html::Render;
use crate::html::boxed_vec;
use crate::html::elements::{Anchor, Image, Link, Paragraph};
use crate::html::layouts::Division;
pub(crate) struct Page {
title: String,
head: Vec<Box<dyn Render>>,
body: Vec<Box<dyn Render>>,
}
impl Render for Page {
fn render(&self) -> String {
format!(
"\
<!DOCTYPE html>
<html lang=\"en\">
<head>
<meta charset=\"UTF-8\">
<title>{}</title>
<meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\" />
{}
</head>
<body>
{}
</body>
</html>",
self.title,
self.head.render(),
self.body.render()
)
}
}
impl Page {
pub(crate) fn builder() -> PageBuilder {
PageBuilder::new()
}
pub(crate) fn append_element_to_body(&mut self, element: impl Render + 'static) {
self.body.push(Box::new(element));
}
pub(crate) fn append_element_to_head(&mut self, element: impl Render + 'static) {
self.head.push(Box::new(element));
}
}
pub(crate) struct PageBuilder {
title: Option<String>,
head: Option<Vec<Box<dyn Render>>>,
body: Option<Vec<Box<dyn Render>>>,
}
impl PageBuilder {
pub(crate) fn new() -> Self {
Self {
title: None,
head: None,
body: None,
}
}
pub(crate) fn title<T>(self, title: T) -> Self
where
T: Into<String>,
{
Self {
title: Some(title.into()),
..self
}
}
pub(crate) fn head(self, head: Vec<Box<dyn Render>>) -> Self {
Self {
head: Some(head),
..self
}
}
pub(crate) fn body(self, body: Vec<Box<dyn Render>>) -> Self {
Self {
body: Some(body),
..self
}
}
pub(crate) fn build(self) -> Page {
let title = self.title.unwrap_or(String::new());
let head = self.head.unwrap_or(Vec::new());
let body = self.body.unwrap_or(Vec::new());
Page { title, head, body }
}
}
pub(crate) struct BasePage {
page: Page,
}
impl BasePage {
pub(crate) fn builder() -> BasePageBuilder {
BasePageBuilder::new()
}
pub(crate) fn append_element_to_head(&mut self, element: impl Render + 'static) {
self.page.append_element_to_head(element);
}
pub(crate) fn append_element_to_body(&mut self, element: impl Render + 'static) {
self.page.append_element_to_body(element);
}
}
impl Render for BasePage {
fn render(&self) -> String {
self.page.render()
}
}
pub(crate) struct BasePageBuilder {
title: Option<String>,
head: Option<Vec<Box<dyn Render>>>,
body: Option<Vec<Box<dyn Render>>>,
}
impl BasePageBuilder {
pub(crate) fn new() -> Self {
Self {
title: None,
head: None,
body: None,
}
}
pub(crate) fn title<T>(self, title: T) -> Self
where
T: Into<String>,
{
Self {
title: Some(title.into()),
..self
}
}
pub(crate) fn head(self, head: Vec<Box<dyn Render>>) -> Self {
Self {
head: Some(head),
..self
}
}
pub(crate) fn body(self, body: Vec<Box<dyn Render>>) -> Self {
Self {
body: Some(body),
..self
}
}
pub(crate) fn build(self) -> BasePage {
let title = self.title.unwrap_or(String::new());
let head = self.head.unwrap_or(Vec::new());
let body = self.body.unwrap_or(Vec::new());
let name = Paragraph::builder()
.id("name")
.classes(vec!["name"])
.text("AINDUSTRIES")
.build();
let home = Anchor::builder()
.id("home")
.classes(vec!["nav-button"])
.href("/")
.text("Home")
.build();
let projects = Anchor::builder()
.id("projects")
.classes(vec!["nav-button"])
.href("/projects")
.text("Projects")
.build();
let music = Anchor::builder()
.id("music")
.classes(vec!["nav-button"])
.href("/music")
.text("Music")
.build();
let buttons = Division::builder()
.classes(vec!["nav-buttons"])
.elements(boxed_vec![home, projects, music])
.build();
let header = Division::builder()
.id("header")
.classes(vec!["header"])
.elements(boxed_vec![name, buttons])
.build();
// background
let image = Image::builder()
.id("background")
.classes(vec!["background"])
.src("/static/img/background.png")
.build();
// css
let base = Link::builder()
.rel("stylesheet")
.href("/static/css/base.css")
.build();
let mut final_head = boxed_vec![base];
final_head.extend(head);
let mut final_body = boxed_vec![image, header];
final_body.extend(body);
let page = Page::builder()
.title(title)
.head(final_head)
.body(final_body)
.build();
BasePage { page }
}
}

View File

@@ -1,20 +1,127 @@
mod html; mod html;
mod middleware;
mod pages; mod pages;
use actix_web::{App, HttpServer}; use actix_web::{App, HttpServer, web};
use clap::Parser;
use serde::Deserialize;
use sqlx::PgPool;
use std::fs::File;
use std::io::Read;
use std::process::exit;
use toml::from_slice;
use pages::{files::file, index}; use crate::pages::files::robots;
use middleware::MimeType;
use pages::{
files::file,
index,
ip::ip,
music::music,
projects::{project, projects},
};
#[derive(Parser)]
#[command(name = "aindustries-be", version, about = "This is the amazing webserver for aindustries.be!", long_about = None)]
struct Cli {
#[arg(
short,
long,
value_name = "CONFIG",
help = "Path to config file, defaults to /etc/aindustries-be/config.toml"
)]
config: Option<String>,
}
#[derive(Deserialize)]
struct Config {
server: ServerConfig,
database: DatabaseConfig,
}
#[derive(Deserialize)]
struct ServerConfig {
assets: String,
bind: String,
}
#[derive(Deserialize)]
struct DatabaseConfig {
address: String,
user: String,
password: String,
database_name: String,
}
struct AppState {
assets: String,
pool: PgPool,
}
#[actix_web::main] #[actix_web::main]
async fn main() { async fn main() {
let bind_address = "0.0.0.0:8080"; let cli = Cli::parse();
if let Ok(server) = let config_file_path = cli
HttpServer::new(|| App::new().service(index).service(file)).bind(bind_address) .config
.unwrap_or("/etc/aindustries-be/config.toml".to_string());
let mut config_file = match File::open(&config_file_path) {
Ok(config_file) => config_file,
Err(e) => {
eprintln!("Could not open config file ({config_file_path}): {e}");
exit(1);
}
};
let mut buf = vec![];
if let Err(e) = config_file.read_to_end(&mut buf) {
eprintln!("Could not read config file: {e}");
exit(1);
}
let config: Config = match from_slice(&buf) {
Ok(config) => config,
Err(e) => {
eprintln!("Could not parse config file {e}");
exit(1);
}
};
let bind_address = config.server.bind;
let assets = config.server.assets;
let database_address = format!(
"postgres://{}:{}@{}/{}",
config.database.user,
config.database.password,
config.database.address,
config.database.database_name
);
let pool = match PgPool::connect(&database_address).await {
Ok(pool) => pool,
Err(e) => {
eprintln!("Could not connect to database: {e}");
exit(1);
}
};
let assets = assets.replace("~", "/home/conta/Code/aindustries.be/assets");
let data = web::Data::new(AppState { assets, pool });
if let Ok(server) = HttpServer::new(move || {
App::new()
.app_data(data.clone())
.service(file)
.service(robots)
.service(
web::scope("")
.wrap(MimeType::new("text/html"))
.service(index)
.service(projects)
.service(project)
.service(music)
.service(ip),
)
})
.bind(bind_address)
{ {
match server.run().await { match server.run().await {
Ok(_) => {} Ok(_) => {}
Err(e) => { Err(e) => {
eprintln!("An error occurred: {}", e); println!("An error occurred: {e}");
} }
} }
} }

73
src/middleware/mod.rs Normal file
View File

@@ -0,0 +1,73 @@
use actix_web::Error;
use actix_web::dev::{Service, ServiceRequest, ServiceResponse, Transform, forward_ready};
use actix_web::http::header::{CONTENT_TYPE, HeaderValue};
use futures_util::future::LocalBoxFuture;
use std::future::{Ready, ready};
pub(crate) struct MimeType {
mime_type: String,
}
impl MimeType {
pub(crate) fn new<T>(mime_type: T) -> Self
where
T: Into<String>,
{
Self {
mime_type: mime_type.into(),
}
}
}
impl<S, B> Transform<S, ServiceRequest> for MimeType
where
S: Service<ServiceRequest, Response = ServiceResponse<B>, Error = Error>,
S::Future: 'static,
B: 'static,
{
type Response = ServiceResponse<B>;
type Error = Error;
type Transform = MimeTypeMiddleware<S>;
type InitError = ();
type Future = Ready<Result<Self::Transform, Self::InitError>>;
fn new_transform(&self, service: S) -> Self::Future {
let mime_type = self.mime_type.clone();
ready(Ok(MimeTypeMiddleware { service, mime_type }))
}
}
pub(crate) struct MimeTypeMiddleware<S> {
service: S,
mime_type: String,
}
impl<S, B> Service<ServiceRequest> for MimeTypeMiddleware<S>
where
S: Service<ServiceRequest, Response = ServiceResponse<B>, Error = Error>,
S::Future: 'static,
B: 'static,
{
type Response = ServiceResponse<B>;
type Error = Error;
type Future = LocalBoxFuture<'static, Result<Self::Response, Self::Error>>;
forward_ready!(service);
fn call(&self, req: ServiceRequest) -> Self::Future {
let fut = self.service.call(req);
let val = HeaderValue::from_str(self.mime_type.as_str()).expect("Invalid MimeType");
Box::pin(async move {
let mut res = fut.await?;
match res.headers_mut().get_mut(CONTENT_TYPE) {
Some(content_type) => {
*content_type = val;
}
None => {
res.headers_mut().append(CONTENT_TYPE, val);
}
}
Ok(res)
})
}
}

View File

@@ -1,15 +1,15 @@
use crate::AppState;
use actix_web::web::Path; use actix_web::web::Path;
use actix_web::{HttpResponse, Responder, get}; use actix_web::{HttpResponse, Responder, get, web};
use std::fs::File; use std::fs::File;
use std::io::Read; use std::io::Read;
use std::path::PathBuf; use std::path::PathBuf;
#[get("/static/{file_type}/{file_name}")] #[get("/static/{file_type}/{file_name}")]
async fn file(file: Path<(String, String)>) -> impl Responder { async fn file(file: Path<(String, String)>, data: web::Data<AppState>) -> impl Responder {
//TODO use assets dir let mut path = PathBuf::from(&data.assets);
let mut path = PathBuf::from("assets"); path.push(file.0.replace("/", ""));
path.push(&file.0.replace("/", "")); path.push(file.1.replace("/", ""));
path.push(&file.1.replace("/", ""));
let mut file = File::open(path).unwrap(); let mut file = File::open(path).unwrap();
let mut bytes = Vec::new(); let mut bytes = Vec::new();
if file.read_to_end(&mut bytes).is_err() { if file.read_to_end(&mut bytes).is_err() {
@@ -17,3 +17,10 @@ async fn file(file: Path<(String, String)>) -> impl Responder {
}; };
HttpResponse::Ok().body(bytes) HttpResponse::Ok().body(bytes)
} }
#[get("/robots.txt")]
async fn robots() -> impl Responder {
"User-agent: *\n\
Disallow: /\n\
Allow: /$"
}

34
src/pages/ip.rs Normal file
View File

@@ -0,0 +1,34 @@
use crate::html::elements::{Heading, Link};
use crate::html::layouts::Division;
use crate::html::pages::BasePage;
use crate::html::{Render, boxed_vec};
use actix_web::HttpRequest;
use actix_web::{Responder, get};
#[get("/ip")]
async fn ip(request: HttpRequest) -> impl Responder {
let info = request.connection_info();
let ip = info.realip_remote_addr();
let text = Heading::builder()
.text(format!(
"Your IP address is {}<br>Don't ask how I know.",
ip.unwrap_or("?")
))
.build();
let div = Division::builder()
.classes(vec!["ip"])
.elements(boxed_vec![text])
.build();
let css = Link::builder()
.rel("stylesheet")
.href("/static/css/ip.css")
.build();
BasePage::builder()
.title("Your IP address")
.head(boxed_vec![css])
.body(boxed_vec![div])
.build()
.render()
}

View File

@@ -1,45 +1,41 @@
pub(crate) mod files; pub(crate) mod files;
pub(crate) mod ip;
pub(crate) mod music;
pub(crate) mod projects;
use crate::html::elements::{a, div, h1, h2, img, link, p}; use crate::html::elements::{Heading, Link};
use crate::html::{Page, Render}; use crate::html::layouts::Division;
use actix_web::http::header::ContentType; use crate::html::pages::BasePage;
use actix_web::mime::TEXT_HTML; use crate::html::{Render, boxed_vec};
use actix_web::{HttpResponse, Responder, get}; use actix_web::{Responder, get};
#[get("/")] #[get("/")]
async fn index() -> impl Responder { async fn index() -> impl Responder {
let mut page = Page::new("AINDUSTRIES".to_string());
// header
let mut header = div::new("header".to_string(), vec!["header".to_string()]);
let name = p::new("name".to_string(), vec!["name".to_string()], "AINDUSTRIES".to_string());
let mut buttons = div::new("buttons".to_string(), vec!["nav-buttons".to_string()]);
let home = a::new("home".to_string(), vec!["nav-button".to_string()], "/".to_string(), "Home".to_string());
buttons.append_element(home);
header.append_element(name);
header.append_element(buttons);
// background
let image = img::new("background".to_string(), vec!["background".to_string()], "static/img/background.png".to_string());
// introduction // introduction
let mut intro = div::new("intro".to_string(), vec!["intro".to_string()]); let hi = Heading::builder().text("Hello and welcome!").build();
let hi = h1::new("Hello and welcome!".to_string()); let detail = Heading::builder()
let detail = h2::new("This here is a small website to show the passion I have for computers.<br>\ .level(2)
.text(
"This here is a small website to show the passion I have for computers.<br>\
I have always had a good time creating and discovering new things.<br>\ I have always had a good time creating and discovering new things.<br>\
Your may discover some of my projects here on this showcase.".to_string()); Your may discover some of my projects here on this little showcase.",
let note = p::new("note".to_string(), vec!["note".to_string()], "Website in construction.".to_string()); )
intro.append_element(hi); .build();
intro.append_element(detail); let intro = Division::builder()
intro.append_element(note); .id("intro")
.classes(vec!["intro"])
.elements(boxed_vec![hi, detail])
.build();
// css // css
let base = link::new("stylesheet".to_string(), "static/css/base.css".to_string()); let index = Link::builder()
let index = link::new("stylesheet".to_string(), "static/css/index.css".to_string()); .rel("stylesheet")
.href("static/css/index.css")
.build();
// add elements to the page in order // add elements to the page in order
page.append_element_to_head(base); let page = BasePage::builder()
page.append_element_to_head(index); .title("AINDUSTRIES")
page.append_element_to_body(image); .head(boxed_vec![index])
page.append_element_to_body(header); .body(boxed_vec![intro])
page.append_element_to_body(intro); .build();
let response = HttpResponse::Ok() page.render()
.insert_header(ContentType(TEXT_HTML))
.body(page.render());
response
} }

54
src/pages/music.rs Normal file
View File

@@ -0,0 +1,54 @@
use crate::html::layouts::DivisionBuilder;
use crate::html::pages::BasePageBuilder;
use crate::html::{Render, boxed_vec};
use crate::AppState;
use crate::html::elements::{HeadingBuilder, IframeBuilder, Link};
use actix_web::web::Data;
use actix_web::{Responder, get, web};
use sqlx::query_as;
#[get("/music")]
async fn music(state: Data<AppState>) -> impl Responder {
let featured_title = HeadingBuilder::new()
.level(1)
.text("The song I've been listening to the most recently is \"Shiawase - VIP\" by Dion Timmer.")
.build();
let featured_iframe = IframeBuilder::new()
.src("https://www.youtube.com/embed/suisIF4hwyw")
.build();
let featured = DivisionBuilder::new()
.classes(vec!["featured"])
.id("featured")
.elements(boxed_vec![featured_title, featured_iframe])
.build();
let playlist_title = HeadingBuilder::new()
.text(
"You can find below my primary playlist, \
the one containing almost all the songs I have been listening to throughout the years.",
)
.build();
let playlist_iframe = IframeBuilder::new()
.src(
"https://open.spotify.com/embed/playlist/\
3owjyc1RILDGkgCmdCal39?utm_source=generator",
)
.build();
let playlist = DivisionBuilder::new()
.id("playlist")
.classes(vec!["playlist"])
.elements(boxed_vec![playlist_title, playlist_iframe])
.build();
let css = Link::builder()
.rel("stylesheet")
.href("/static/css/music.css")
.build();
let page = BasePageBuilder::new()
.title("Music")
.head(boxed_vec![css])
.body(boxed_vec![featured, playlist])
.build();
page.render()
}

126
src/pages/projects.rs Normal file
View File

@@ -0,0 +1,126 @@
use crate::AppState;
use crate::html::elements::{Anchor, Heading, Link, Paragraph};
use crate::html::layouts::Division;
use crate::html::pages::BasePage;
use crate::html::{Render, boxed_vec};
use actix_web::web::Data;
use actix_web::{Responder, get, web};
use serde::Deserialize;
use sqlx::query_as;
#[derive(Deserialize)]
struct Project {
id: String,
title: String,
description: String,
}
#[get("/projects")]
async fn projects(app_state: Data<AppState>) -> impl Responder {
let title = Heading::builder().text("My projects").build();
let desc = Heading::builder()
.level(2)
.text(
"Here you will find all my projects which deserve to be shown<br>\
(I've done a lot of small projects but they are not worth it.)",
)
.build();
let projects = match query_as!(Project, "SELECT id, title, description FROM Projects")
.fetch_all(&app_state.pool)
.await
{
Ok(projects) => projects,
Err(_) => {
vec![]
}
};
let items: Vec<Box<dyn Render>> = projects
.into_iter()
.map(|p| {
let title = Heading::builder().text(p.title).build();
let description = Paragraph::builder()
.classes(vec!["project-desc"])
.text(p.description)
.build();
let view = Anchor::builder()
.classes(vec!["project-view"])
.href(format!("/projects/{}", p.id))
.text("Learn More")
.build();
let info = Division::builder()
.classes(vec!["project-info"])
.elements(boxed_vec![description, view])
.build();
Box::new(
Division::builder()
.classes(vec!["project"])
.elements(boxed_vec![title, info])
.build(),
) as Box<dyn Render>
})
.collect();
let css = Link::builder()
.rel("stylesheet")
.href("/static/css/projects.css")
.build();
let mut body = boxed_vec![title, desc];
body.extend(items);
let page = BasePage::builder()
.title("Projects")
.head(boxed_vec![css])
.body(body)
.build();
page.render()
}
#[derive(Deserialize)]
struct ProjectDetail {
id: String,
title: String,
git_url: String,
git_title: String,
description: String,
}
#[get("/projects/{project}")]
async fn project(project: web::Path<String>, app_state: Data<AppState>) -> impl Responder {
let project = project.into_inner();
let css = Link::builder()
.rel("stylesheet")
.href("/static/css/project.css")
.build();
let mut page = BasePage::builder()
.title(format!("Project-{}", project))
.head(boxed_vec![css])
.build();
let project = match query_as!(
ProjectDetail,
"SELECT * FROM Project_Detail WHERE id = $1",
project
)
.fetch_one(&app_state.pool)
.await
{
Ok(project) => project,
Err(error) => {
// return not found page
eprintln!("Error fetching project: {:?}", error);
return page.render();
}
};
let title = Heading::builder().text(project.title).build();
let gitea = Anchor::builder()
.href(project.git_url)
.text(project.git_title)
.build();
let desc = Paragraph::builder()
.classes(vec!["description"])
.text(format!(
"{}<br><br> Wish to see more? Check out the gitea repository: {}",
project.description,
gitea.render()
))
.build();
page.append_element_to_body(title);
page.append_element_to_body(desc);
page.render()
}