Compare commits
32 Commits
cba44c144d
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 3ac59f65f2 | |||
| 4a25c377c4 | |||
| 8c64c5dbe3 | |||
| 88abbe8c14 | |||
| c5b85bc359 | |||
| 8a27a74e9f | |||
| 425bac6776 | |||
| 5c456fd5cd | |||
| 08b5932cf8 | |||
| c7377e06e3 | |||
| 4e53f9ed1f | |||
| d692bf1cdd | |||
| ec126bbdc4 | |||
| 0fe13b6ce7 | |||
| 9c79881c4b | |||
| f6b6b0a80b | |||
| 146509efdf | |||
| a908bf0e8a | |||
| aeff2b1759 | |||
| b1cb7dff27 | |||
| d50a6a3d76 | |||
| f9726f7b35 | |||
| 48d59b6f90 | |||
| f01659ea50 | |||
| 2500fdc79b | |||
| 99ebee7f35 | |||
| 3e2987ad0c | |||
| bcd50d3a17 | |||
| e63edda9ca | |||
| a457efb97e | |||
| 4eaf54dd8f | |||
| 94b68982f6 |
1
.gitignore
vendored
1
.gitignore
vendored
@@ -2,3 +2,4 @@
|
|||||||
.idea
|
.idea
|
||||||
Cargo.lock
|
Cargo.lock
|
||||||
.env
|
.env
|
||||||
|
config.toml
|
||||||
|
|||||||
@@ -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
17
aindustries-be.service
Normal 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
|
||||||
@@ -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;
|
||||||
|
|||||||
@@ -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
13
assets/css/ip.css
Normal 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
58
assets/css/music.css
Normal 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
21
assets/css/project.css
Normal 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
66
assets/css/projects.css
Normal 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
11
config.toml.example
Normal 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"
|
||||||
13
migrations/20251027081447_projects.sql
Normal file
13
migrations/20251027081447_projects.sql
Normal 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
|
||||||
|
)
|
||||||
13
migrations/20251027102341_website_project.sql
Normal file
13
migrations/20251027102341_website_project.sql
Normal 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.'
|
||||||
|
)
|
||||||
@@ -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,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl h2 {
|
pub(crate) fn level(self, level: u8) -> Self {
|
||||||
pub(crate) fn new(text: String) -> Self {
|
Self {
|
||||||
Self { text }
|
level: Some(level),
|
||||||
|
..self
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(non_camel_case_types)]
|
pub(crate) fn id<T>(self, id: T) -> Self
|
||||||
pub(crate) struct link {
|
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,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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>,
|
||||||
|
href: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
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,
|
id: String,
|
||||||
classes: Vec<String>,
|
classes: Vec<String>,
|
||||||
content: Vec<Box<dyn Render>>,
|
text: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Render for div {
|
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
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(non_camel_case_types)]
|
pub(crate) fn classes<T>(self, classes: Vec<T>) -> Self
|
||||||
pub(crate) struct img {
|
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 }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) struct Image {
|
||||||
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
83
src/html/layouts.rs
Normal 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,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
215
src/html/pages.rs
Normal 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 }
|
||||||
|
}
|
||||||
|
}
|
||||||
119
src/main.rs
119
src/main.rs
@@ -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
73
src/middleware/mod.rs
Normal 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)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
34
src/pages/ip.rs
Normal 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()
|
||||||
|
}
|
||||||
@@ -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
54
src/pages/music.rs
Normal 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
126
src/pages/projects.rs
Normal 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()
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user