wip
This commit is contained in:
parent
0e0754fa10
commit
b51840d09a
771
Cargo.lock
generated
771
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
@ -20,6 +20,11 @@ tokio = { version = "1.34.0", features = ["macros"] }
|
|||||||
tracing = { version = "0.1.40", features = ["attributes", "std"] }
|
tracing = { version = "0.1.40", features = ["attributes", "std"] }
|
||||||
yansi = "0.5.1"
|
yansi = "0.5.1"
|
||||||
tera = "1.19.1"
|
tera = "1.19.1"
|
||||||
nanobyte_opentelemetry = { path = "../nanobyte_opentelemetry" }
|
|
||||||
tempfile = "3.8.1"
|
tempfile = "3.8.1"
|
||||||
urlencoding = "2.1.3"
|
urlencoding = "2.1.3"
|
||||||
|
nanobyte_opentelemetry = { version = "0.2.2", registry = "gitea", features = ["rocket-reqwest"]}
|
||||||
|
reqwest = { version = "0.11", features = ["json"] }
|
||||||
|
phf = { version = "0.11.2", features = ["macros"] }
|
||||||
|
|
||||||
|
nanobyte_tera = { git = "https://glpat-Us_EdFTzQLv4shViQXi_:glpat-Us_EdFTzQLv4shViQXi_@gitlab.nanobyte.cz/tools/nanobyte_tera.git", branch = "master" }
|
||||||
|
ovlach_tera = { git = "https://glpat-_yPuXbEzECyk3FaHudCN:glpat-_yPuXbEzECyk3FaHudCN@gitlab.nanobyte.cz/ondrej/ovlach_tera.git", branch = "master" }
|
||||||
|
@ -1,2 +1,8 @@
|
|||||||
[debug]
|
[debug]
|
||||||
|
static_route = "http://localhost:8001"
|
||||||
|
cv_backend_path = "http://localhost:8002"
|
||||||
|
|
||||||
|
[default]
|
||||||
|
static_route = "http://localhost:8001"
|
||||||
|
cv_backend_path = "http://localhost:8002"
|
||||||
port = 8003
|
port = 8003
|
21
resources/cs-CZ/ovlach_frontend
Normal file
21
resources/cs-CZ/ovlach_frontend
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
devops-enginner-web-developer = DevOPS engineer/Backend web developer(TODO do API)
|
||||||
|
download-cv = Stáhnout životopis
|
||||||
|
hire-me = Kontakt
|
||||||
|
about-me = O mě
|
||||||
|
professional-skills = Jazyky a technologie
|
||||||
|
work-experience = Zaměstnání
|
||||||
|
contact = Kontakt
|
||||||
|
bio = Základní udaje
|
||||||
|
education = Vzdělání
|
||||||
|
title-about = O mě
|
||||||
|
title-skills = Technologie
|
||||||
|
title-experience = Zaměstnání
|
||||||
|
title-education = Vzdělání
|
||||||
|
title-contact = Kontakt
|
||||||
|
age = Věk
|
||||||
|
email = E-mail
|
||||||
|
phone = Telefon
|
||||||
|
czech = Česky
|
||||||
|
english = Anglicky
|
||||||
|
skills-languages = Jazyky
|
||||||
|
skills-technology = Technologie/Frameworky
|
21
resources/en-US/ovlach_frontend
Normal file
21
resources/en-US/ovlach_frontend
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
devops-enginner-web-developer = DevOPS engineer/Backend web developer
|
||||||
|
download-cv = Download CV
|
||||||
|
hire-me = Hire me
|
||||||
|
about-me = About me
|
||||||
|
professional-skills = Professional Skills
|
||||||
|
education = Education
|
||||||
|
work-experience = Work experience
|
||||||
|
contact = Contact
|
||||||
|
bio = Bio
|
||||||
|
title-about = About
|
||||||
|
title-skills = Skills
|
||||||
|
title-experience = Experience
|
||||||
|
title-education = Education
|
||||||
|
title-contact = Contact
|
||||||
|
age = Age
|
||||||
|
email = Email
|
||||||
|
phone = Phone
|
||||||
|
czech = Czech
|
||||||
|
english = English
|
||||||
|
skills-languages = Languages
|
||||||
|
skills-technology = Technologies/Frameworks
|
@ -32,7 +32,6 @@ impl Fairing for Chromium {
|
|||||||
|
|
||||||
async fn on_ignite(&self, rocket: Rocket<Build>) -> fairing::Result {
|
async fn on_ignite(&self, rocket: Rocket<Build>) -> fairing::Result {
|
||||||
let new_rocket = rocket.manage(ChromiumCoordinator::new().await);
|
let new_rocket = rocket.manage(ChromiumCoordinator::new().await);
|
||||||
let coordinator = new_rocket.state::<ChromiumCoordinator>().unwrap();
|
|
||||||
Ok(new_rocket)
|
Ok(new_rocket)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
14
src/lib.rs
14
src/lib.rs
@ -1,15 +1,27 @@
|
|||||||
use chromium::rocket::Chromium;
|
use chromium::rocket::Chromium;
|
||||||
use nanobyte_opentelemetry::rocket::TracingFairing;
|
use nanobyte_opentelemetry::rocket::TracingFairing;
|
||||||
use rocket::{Rocket, Build, routes};
|
use rocket::{Rocket, Build, routes, fairing::AdHoc};
|
||||||
|
use serde::Deserialize;
|
||||||
pub mod routes;
|
pub mod routes;
|
||||||
mod chromium;
|
mod chromium;
|
||||||
mod tools;
|
mod tools;
|
||||||
|
mod services;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
#[serde(crate = "rocket::serde")]
|
||||||
|
pub struct CVBackendConfig {
|
||||||
|
cv_backend_path: String,
|
||||||
|
}
|
||||||
|
|
||||||
pub fn rocket_builder() -> Rocket<Build> {
|
pub fn rocket_builder() -> Rocket<Build> {
|
||||||
rocket::build()
|
rocket::build()
|
||||||
.attach(TracingFairing::ignite())
|
.attach(TracingFairing::ignite())
|
||||||
.attach(Chromium::ignite())
|
.attach(Chromium::ignite())
|
||||||
|
.attach(
|
||||||
|
AdHoc::config::<CVBackendConfig>()
|
||||||
|
)
|
||||||
.mount("/", routes![
|
.mount("/", routes![
|
||||||
routes::pdf::render_pdf_cv,
|
routes::pdf::render_pdf_cv,
|
||||||
routes::pdf::render_html_cv
|
routes::pdf::render_html_cv
|
||||||
|
@ -1,12 +1,15 @@
|
|||||||
use headless_chrome::Browser;
|
use headless_chrome::Browser;
|
||||||
use headless_chrome::types::PrintToPdfOptions;
|
use headless_chrome::types::PrintToPdfOptions;
|
||||||
use nanobyte_opentelemetry::rocket::TracingSpan;
|
use nanobyte_opentelemetry::rocket::{TracingSpan, RequestId, OtelReqwestClient};
|
||||||
use rocket::{get, response::stream::ByteStream, fairing::Fairing, fs::NamedFile, futures::TryFutureExt};
|
use ovlach_data::cv::cv::CV;
|
||||||
|
use reqwest::Client;
|
||||||
|
use rocket::fs::NamedFile;
|
||||||
|
use ::rocket::{State, http::Status};
|
||||||
|
use ::rocket::get;
|
||||||
use tempfile::NamedTempFile;
|
use tempfile::NamedTempFile;
|
||||||
use tera::Context;
|
use tera::Context;
|
||||||
use tracing::{info_span, error, debug};
|
use tracing::{info_span, error, debug};
|
||||||
use crate::{chromium::rocket::BrowserHolder, tools::{tera::NanoTera, pdf::PdfStream}};
|
use crate::{chromium::rocket::BrowserHolder, tools::{tera::NanoTera, pdf::PdfStream, rocket::RequestLanguage}, services::cv::fetch_cv_data_from_backend, CVBackendConfig};
|
||||||
use urlencoding::encode;
|
|
||||||
|
|
||||||
// TODO: request-id
|
// TODO: request-id
|
||||||
fn generate_pdf(browser: Browser, file: &NamedTempFile) -> Vec<u8> {
|
fn generate_pdf(browser: Browser, file: &NamedTempFile) -> Vec<u8> {
|
||||||
@ -38,29 +41,59 @@ fn generate_pdf(browser: Browser, file: &NamedTempFile) -> Vec<u8> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[tracing::instrument]
|
#[tracing::instrument]
|
||||||
fn render_template(template_name: &str, file: &NamedTempFile, tera: NanoTera) {
|
fn render_template(template_name: &str, file: &NamedTempFile, tera: NanoTera, cv: CV, language: String) {
|
||||||
// TODO: handle errors
|
// TODO: handle errors
|
||||||
tera.0.render_to("two_column.html.tera", &Context::new(), file);
|
let mut tera_context = Context::new();
|
||||||
|
tera_context.insert("cv", &cv);
|
||||||
|
tera_context.insert("lang", language.as_str());
|
||||||
|
|
||||||
|
match tera.0.render_to("two_column.html.tera", &tera_context, file) {
|
||||||
|
Ok(_) => {
|
||||||
|
debug!("Rendered template to {}", file.path().to_str().unwrap());
|
||||||
|
},
|
||||||
|
Err(e) => {
|
||||||
|
error!("Error rendering template {}: {}", &template_name, e);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[get("/cv/<username>/output.pdf")]
|
#[get("/cv/<username>/<language>/output.pdf")]
|
||||||
pub async fn render_pdf_cv(username: &str, browser: BrowserHolder, tera: NanoTera, tracing: TracingSpan) -> PdfStream {
|
pub async fn render_pdf_cv(username: &str, tera: NanoTera, tracing: TracingSpan, request_client: OtelReqwestClient,
|
||||||
|
cv_config: &State<CVBackendConfig>, language: RequestLanguage, browser: BrowserHolder) -> Result<PdfStream, Status> {
|
||||||
let entered_span = tracing.0.enter();
|
let entered_span = tracing.0.enter();
|
||||||
let file = tempfile::Builder::new().suffix(".html").tempfile().unwrap();
|
match fetch_cv_data_from_backend(cv_config.cv_backend_path.clone(), request_client.0).await {
|
||||||
render_template("two_column", &file, tera);
|
Ok(cv_data) => {
|
||||||
let span = info_span!("render_pdf", username = username);
|
let file = tempfile::Builder::new().suffix(".html").tempfile().unwrap();
|
||||||
let pdf = span.in_scope(||{
|
render_template("two_column", &file, tera, cv_data, language.language);
|
||||||
generate_pdf(browser.browser, &file)
|
let span = info_span!("render_pdf", username = username);
|
||||||
});
|
let pdf = span.in_scope(||{
|
||||||
drop(entered_span);
|
generate_pdf(browser.browser, &file)
|
||||||
PdfStream::new(pdf)
|
});
|
||||||
|
drop(entered_span);
|
||||||
|
Ok(PdfStream::new(pdf))
|
||||||
|
},
|
||||||
|
Err(e) => {
|
||||||
|
error!("Error fetching cv data: {:?}", e);
|
||||||
|
Err(Status::InternalServerError)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Route only for debuging
|
/// Route only for debuging
|
||||||
#[get("/cv/<username>/output.html")]
|
#[get("/cv/<username>/<language>/output.html")]
|
||||||
pub async fn render_html_cv(username: &str, tera: NanoTera, tracing: TracingSpan) -> NamedFile {
|
pub async fn render_html_cv(username: &str, tera: NanoTera, tracing: TracingSpan, request_client: OtelReqwestClient,
|
||||||
let entered_span = tracing.0.enter();
|
cv_config: &State<CVBackendConfig>, language: RequestLanguage) -> Result<NamedFile, Status> {
|
||||||
let file = tempfile::Builder::new().suffix(".html").tempfile().unwrap();
|
let _ = tracing.0.enter();
|
||||||
render_template("two_column", &file, tera);
|
match fetch_cv_data_from_backend(cv_config.cv_backend_path.clone(), request_client.0).await {
|
||||||
NamedFile::open(file.path()).await.unwrap()
|
Ok(cv_data) => {
|
||||||
|
let file = tempfile::Builder::new().suffix(".html").tempfile().unwrap();
|
||||||
|
render_template("two_column", &file, tera, cv_data, language.language);
|
||||||
|
Ok(NamedFile::open(file.path()).await.unwrap())
|
||||||
|
},
|
||||||
|
Err(e) => {
|
||||||
|
error!("Error fetching cv data: {:?}", e);
|
||||||
|
Err(Status::InternalServerError)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
23
src/services/cv.rs
Normal file
23
src/services/cv.rs
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
use ovlach_data::cv::cv::CV;
|
||||||
|
use reqwest::Client;
|
||||||
|
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub enum FetchError {
|
||||||
|
ReqwestError(reqwest::Error)
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<reqwest::Error> for FetchError {
|
||||||
|
fn from(e: reqwest::Error) -> Self {
|
||||||
|
FetchError::ReqwestError(e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn fetch_cv_data_from_backend(backend_host: String, client: Client) -> Result<CV, FetchError> {
|
||||||
|
let resp = client
|
||||||
|
.get(format!("{}/{}", backend_host, "/api/cv/ovlach")).send()
|
||||||
|
.await?
|
||||||
|
.json::<CV>()
|
||||||
|
.await?;
|
||||||
|
Ok(resp)
|
||||||
|
}
|
1
src/services/mod.rs
Normal file
1
src/services/mod.rs
Normal file
@ -0,0 +1 @@
|
|||||||
|
pub mod cv;
|
@ -1,2 +1,3 @@
|
|||||||
pub mod tera;
|
pub mod tera;
|
||||||
pub mod pdf;
|
pub mod pdf;
|
||||||
|
pub(crate) mod rocket;
|
26
src/tools/rocket.rs
Normal file
26
src/tools/rocket.rs
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
use phf::phf_map;
|
||||||
|
use rocket::request::FromParam;
|
||||||
|
|
||||||
|
pub struct RequestLanguage {
|
||||||
|
pub language: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
static LANG_TO_CODES: phf::Map<&'static str, &'static str> = phf_map! {
|
||||||
|
"cs" => "cs-CZ",
|
||||||
|
"en" => "en-US",
|
||||||
|
};
|
||||||
|
|
||||||
|
impl<'r> FromParam<'r> for RequestLanguage {
|
||||||
|
type Error = &'r str;
|
||||||
|
|
||||||
|
fn from_param(param: &'r str) -> Result<Self, Self::Error> {
|
||||||
|
match LANG_TO_CODES.get(param) {
|
||||||
|
Some(val) => Ok(RequestLanguage {
|
||||||
|
language: val.to_string(),
|
||||||
|
}),
|
||||||
|
None => Ok(RequestLanguage {
|
||||||
|
language: LANG_TO_CODES["en"].to_string(),
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -1,5 +1,9 @@
|
|||||||
|
use std::{collections::HashMap};
|
||||||
|
|
||||||
|
use nanobyte_tera::{l18n::translate_filter, date::{calculate_age, get_year}, string::insert_space_every, gravatar::gravatar_link};
|
||||||
|
use ovlach_tera::entity::lang_entity;
|
||||||
use rocket::{request::{FromRequest, Outcome}, Request};
|
use rocket::{request::{FromRequest, Outcome}, Request};
|
||||||
use tera::Tera;
|
use tera::{Tera, Value, Error};
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub struct NanoTera(pub Tera);
|
pub struct NanoTera(pub Tera);
|
||||||
@ -11,6 +15,33 @@ impl<'r> FromRequest<'r> for NanoTera {
|
|||||||
type Error = ();
|
type Error = ();
|
||||||
|
|
||||||
async fn from_request(request: &'r Request<'_>) -> Outcome<Self, ()> {
|
async fn from_request(request: &'r Request<'_>) -> Outcome<Self, ()> {
|
||||||
rocket::outcome::Outcome::Success(NanoTera(Tera::new("templates/*").unwrap()))
|
let mut tera = Tera::new("templates/*").unwrap();
|
||||||
|
tera.register_filter("translate", translate_filter);
|
||||||
|
tera.register_filter("calculate_age", calculate_age);
|
||||||
|
tera.register_filter("insert_space_every", insert_space_every);
|
||||||
|
tera.register_filter("gravatar_link", gravatar_link);
|
||||||
|
// filters specific to API
|
||||||
|
tera.register_filter("lang_entity", lang_entity);
|
||||||
|
tera.register_filter("format_date", get_year); // deprecated
|
||||||
|
tera.register_filter("get_year", get_year);
|
||||||
|
tera.register_filter("strip_proto", strip_proto);
|
||||||
|
rocket::outcome::Outcome::Success(NanoTera(tera))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/// Strip protocol from URL (value)
|
||||||
|
pub fn strip_proto(
|
||||||
|
tera_value: &Value,
|
||||||
|
_: &HashMap<String, Value>
|
||||||
|
) -> Result<Value, Error> {
|
||||||
|
let value = tera_value.as_str().unwrap();
|
||||||
|
if value.starts_with("http://") {
|
||||||
|
Ok(Value::String(value.strip_prefix("http://").unwrap().to_string()))
|
||||||
|
} else if value.starts_with("https://") {
|
||||||
|
Ok(Value::String(value.strip_prefix("https://").unwrap().to_string()))
|
||||||
|
} else {
|
||||||
|
Ok(Value::String(value.to_string()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@ -113,6 +113,90 @@
|
|||||||
order: 0;
|
order: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.flex-container-skills {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
flex-wrap: nowrap;
|
||||||
|
justify-content: normal;
|
||||||
|
align-items: normal;
|
||||||
|
align-content: normal;
|
||||||
|
width: 99%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-header .flex-container-skills {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header {
|
||||||
|
padding-top: 1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.flex-items-skills:nth-child(1) {
|
||||||
|
display: block;
|
||||||
|
flex-grow: 0;
|
||||||
|
flex-shrink: 1;
|
||||||
|
flex-basis: auto;
|
||||||
|
align-self: auto;
|
||||||
|
width: 50%;
|
||||||
|
order: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.flex-items-skills:nth-child(2) {
|
||||||
|
display: block;
|
||||||
|
flex-grow: 0;
|
||||||
|
flex-shrink: 1;
|
||||||
|
flex-basis: auto;
|
||||||
|
text-align: left;
|
||||||
|
align-self: auto;
|
||||||
|
width: 50%;
|
||||||
|
order: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tech-name {
|
||||||
|
font-weight: 800;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tech-level {
|
||||||
|
}
|
||||||
|
|
||||||
|
.job p {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.job:nth-child(n+3) {
|
||||||
|
padding-top: 10px;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.contact-bar {
|
||||||
|
font-size: 11pt;
|
||||||
|
padding-left: 1em;
|
||||||
|
color: black;
|
||||||
|
}
|
||||||
|
|
||||||
|
.contact-bar i::after{
|
||||||
|
content: "\a";
|
||||||
|
white-space: pre;
|
||||||
|
}
|
||||||
|
|
||||||
|
.photo {
|
||||||
|
padding-bottom: 1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.contact-bar:first-child {
|
||||||
|
padding-top: 2em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.email{
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon {
|
||||||
|
height: 1em;
|
||||||
|
vertical-align:middle;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
@ -120,78 +204,134 @@
|
|||||||
<div class="flex-items">
|
<div class="flex-items">
|
||||||
<div class="header">
|
<div class="header">
|
||||||
<h1>Ondřej Vlach</h1>
|
<h1>Ondřej Vlach</h1>
|
||||||
<div class="about-me-small">Software engineer</div>
|
<div class="about-me-small">{{ "devops-enginner-web-developer" | translate(lang=lang) }}</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="part">
|
<div class="part">
|
||||||
<div class="section-header">
|
<div class="section-header">
|
||||||
<h2>Experience</h2>
|
<div class="flex-container-skills">
|
||||||
</div>
|
<div class="flex-items-skills">
|
||||||
<div class="job part-content">
|
<h2>{{ "skills-languages" | translate(lang=lang) }}</h2>
|
||||||
<div class="flex-container-experience">
|
|
||||||
<div class="flex-items-experience">
|
|
||||||
<div class="title">Title</div>
|
|
||||||
<div class="company">Company</div>
|
|
||||||
<div class="languages">PHP HTML Aholamora Snake</div>
|
|
||||||
<p>
|
|
||||||
baklklbkalkblaklkb lakbl aklkbl akblka lkblakbla
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="flex-items-experience">
|
<div class="flex-items-skills">
|
||||||
<div class="location">Location</div>
|
<h2>{{ "skills-technology" | translate(lang=lang) }}</h2>
|
||||||
<div class="dates">Dates</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="flex-container-skills">
|
||||||
|
<div class="flex-items-skills">
|
||||||
|
{% for skill in cv.skills %}
|
||||||
|
{% if skill.techtype == "LANGUAGE" %}
|
||||||
|
<div>
|
||||||
|
<span class="tech-name">{{ skill.name }}</span><span class="tech-level"> ({{ skill.skill | lower}})</span>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
<div class="flex-items-skills">
|
||||||
|
{% for skill in cv.skills %}
|
||||||
|
{% if skill.techtype == "TECHNOLOGY" %}
|
||||||
|
<div>
|
||||||
|
<span class="tech-name">{{ skill.name }}</span><span class="tech-level"> ({{ skill.skill | lower}})</span>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="part">
|
<div class="part">
|
||||||
<div class="section-header">
|
<div class="section-header">
|
||||||
<h2>Experience</h2>
|
<h2>{{ "work-experience" | translate(lang=lang) }}</h2>
|
||||||
</div>
|
</div>
|
||||||
|
{% for job in cv.jobs %}
|
||||||
<div class="job part-content">
|
<div class="job part-content">
|
||||||
<div class="flex-container-experience">
|
<div class="flex-container-experience">
|
||||||
<div class="flex-items-experience">
|
<div class="flex-items-experience">
|
||||||
<div class="title">Title</div>
|
<div class="title">{{ job.title }}</div>
|
||||||
<div class="company">Company</div>
|
<div class="languages">{{ job.languages }}</div>
|
||||||
<div class="languages">PHP HTML Aholamora Snake</div>
|
|
||||||
<p>
|
<p>
|
||||||
baklklbkalkblaklkb lakbl aklkbl akblka lkblakbla
|
{{ job.description | lang_entity(lang=lang) }}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex-items-experience">
|
<div class="flex-items-experience">
|
||||||
<div class="location">Location</div>
|
<div class="company">@{{ job.company }}</div>
|
||||||
<div class="dates">Dates</div>
|
<div class="dates">
|
||||||
|
{% if job.from | format_date(type="job") != job.from | format_date(type="job") %}
|
||||||
|
<div class="text-muted text-small mb-3">{{ job.from | format_date(type="job") }} - {{ job.to | format_date(type="job") }}</div>
|
||||||
|
{% else %}
|
||||||
|
<div class="text-muted text-small mb-3">{{ job.from | format_date(type="job") }}</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
{% endfor %}
|
||||||
</div>
|
</div>
|
||||||
<div class="part">
|
<div class="part">
|
||||||
<div class="section-header">
|
<div class="section-header">
|
||||||
<h2>Experience</h2>
|
<h2>{{ "education" | translate(lang=lang) }}</h2>
|
||||||
</div>
|
</div>
|
||||||
<div class="job part-content">
|
<div class="job part-content">
|
||||||
|
{% for education in cv.education %}
|
||||||
<div class="flex-container-experience">
|
<div class="flex-container-experience">
|
||||||
<div class="flex-items-experience">
|
<div class="flex-items-experience">
|
||||||
<div class="title">Title</div>
|
{% if education.degree %}
|
||||||
<div class="company">Company</div>
|
<div class="title">{{ education.degree | lang_entity(lang=lang) }}</div>
|
||||||
<div class="languages">PHP HTML Aholamora Snake</div>
|
{% else %}
|
||||||
<p>
|
<div class="title">{{ education.school }}</div>
|
||||||
baklklbkalkblaklkb lakbl aklkbl akblka lkblakbla
|
{% endif %}
|
||||||
</p>
|
{% if education.description %}
|
||||||
|
<p>
|
||||||
|
|
||||||
|
{{ education.description | lang_entity(lang=lang) }}
|
||||||
|
</p>
|
||||||
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex-items-experience">
|
<div class="flex-items-experience">
|
||||||
<div class="location">Location</div>
|
{% if education.degree %}
|
||||||
<div class="dates">Dates</div>
|
<div class="company">@{{ education.school }}</div>
|
||||||
|
{% endif %}
|
||||||
|
<div class="dates">{{ education.from | format_date(type="job") }} - {{ education.to | format_date(type="job") }}</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
{% endfor %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex-items">
|
<div class="flex-items">
|
||||||
Photo2342342314213 423142342314213423
|
|
||||||
|
|
||||||
<div class="contact-bar">
|
<div class="contact-bar">
|
||||||
<span class="phone">L</span>
|
<div class="photo">
|
||||||
<span class="email">L</span>
|
<img class="img-thumbnail shadow-2-strong" src="{{ cv.person.email | gravatar_link }}" width="160" height="160"/>
|
||||||
|
</div>
|
||||||
|
{% if cv.person.social.facebook %}
|
||||||
|
<i class="fa-facebook"> {{ cv.person.social.facebook | strip_proto }}</i>
|
||||||
|
{% endif %}
|
||||||
|
{% if cv.person.social.github %}
|
||||||
|
<i class="fa-github">
|
||||||
|
<img src="data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIgdmlld0JveD0iMCAwIDI0IDI0Ij48cGF0aCBkPSJNMTIgMGMtNi42MjYgMC0xMiA1LjM3My0xMiAxMiAwIDUuMzAyIDMuNDM4IDkuOCA4LjIwNyAxMS4zODcuNTk5LjExMS43OTMtLjI2MS43OTMtLjU3N3YtMi4yMzRjLTMuMzM4LjcyNi00LjAzMy0xLjQxNi00LjAzMy0xLjQxNi0uNTQ2LTEuMzg3LTEuMzMzLTEuNzU2LTEuMzMzLTEuNzU2LTEuMDg5LS43NDUuMDgzLS43MjkuMDgzLS43MjkgMS4yMDUuMDg0IDEuODM5IDEuMjM3IDEuODM5IDEuMjM3IDEuMDcgMS44MzQgMi44MDcgMS4zMDQgMy40OTIuOTk3LjEwNy0uNzc1LjQxOC0xLjMwNS43NjItMS42MDQtMi42NjUtLjMwNS01LjQ2Ny0xLjMzNC01LjQ2Ny01LjkzMSAwLTEuMzExLjQ2OS0yLjM4MSAxLjIzNi0zLjIyMS0uMTI0LS4zMDMtLjUzNS0xLjUyNC4xMTctMy4xNzYgMCAwIDEuMDA4LS4zMjIgMy4zMDEgMS4yMy45NTctLjI2NiAxLjk4My0uMzk5IDMuMDAzLS40MDQgMS4wMi4wMDUgMi4wNDcuMTM4IDMuMDA2LjQwNCAyLjI5MS0xLjU1MiAzLjI5Ny0xLjIzIDMuMjk3LTEuMjMuNjUzIDEuNjUzLjI0MiAyLjg3NC4xMTggMy4xNzYuNzcuODQgMS4yMzUgMS45MTEgMS4yMzUgMy4yMjEgMCA0LjYwOS0yLjgwNyA1LjYyNC01LjQ3OSA1LjkyMS40My4zNzIuODIzIDEuMTAyLjgyMyAyLjIyMnYzLjI5M2MwIC4zMTkuMTkyLjY5NC44MDEuNTc2IDQuNzY1LTEuNTg5IDguMTk5LTYuMDg2IDguMTk5LTExLjM4NiAwLTYuNjI3LTUuMzczLTEyLTEyLTEyeiIvPjwvc3ZnPg==" class="icon">
|
||||||
|
{{ cv.person.social.github | strip_proto }}
|
||||||
|
</i>
|
||||||
|
{% endif %}
|
||||||
|
{% if cv.person.social.linkedin %}
|
||||||
|
<i class="fa-linkedin"> {{ cv.person.social.linikedin | strip_proto }}</i>
|
||||||
|
{% endif %}
|
||||||
|
{% if cv.person.social.instagram %}
|
||||||
|
<i class="fa-instagram"> {{ cv.person.social.instagram | strip_proto }}</i>
|
||||||
|
{% endif %}
|
||||||
|
{% if cv.person.social.mastodon %}
|
||||||
|
<i class="fa-mastodon">
|
||||||
|
<img alt="Mastodon" src="data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXRmLTgiPz48IS0tIFVwbG9hZGVkIHRvOiBTVkcgUmVwbywgd3d3LnN2Z3JlcG8uY29tLCBHZW5lcmF0b3I6IFNWRyBSZXBvIE1peGVyIFRvb2xzIC0tPgo8c3ZnIGZpbGw9IiMwMDAwMDAiIHdpZHRoPSI4MDBweCIgaGVpZ2h0PSI4MDBweCIgdmlld0JveD0iMCAwIDI0IDI0IiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHhtbDpzcGFjZT0icHJlc2VydmUiPjxwYXRoIGQ9Ik0yMS4zMjcgOC41NjZjMC00LjMzOS0yLjg0My01LjYxLTIuODQzLTUuNjEtMS40MzMtLjY1OC0zLjg5NC0uOTM1LTYuNDUxLS45NTZoLS4wNjNjLTIuNTU3LjAyMS01LjAxNi4yOTgtNi40NS45NTYgMCAwLTIuODQzIDEuMjcyLTIuODQzIDUuNjEgMCAuOTkzLS4wMTkgMi4xODEuMDEyIDMuNDQxLjEwMyA0LjI0My43NzggOC40MjUgNC43MDEgOS40NjMgMS44MDkuNDc5IDMuMzYyLjU3OSA0LjYxMi41MSAyLjI2OC0uMTI2IDMuNTQxLS44MDkgMy41NDEtLjgwOWwtLjA3NS0xLjY0NnMtMS42MjEuNTExLTMuNDQxLjQ0OWMtMS44MDQtLjA2Mi0zLjcwNy0uMTk0LTMuOTk5LTIuNDA5YTQuNTIzIDQuNTIzIDAgMCAxLS4wNC0uNjIxczEuNzcuNDMzIDQuMDE0LjUzNmMxLjM3Mi4wNjMgMi42NTgtLjA4IDMuOTY1LS4yMzYgMi41MDYtLjI5OSA0LjY4OC0xLjg0MyA0Ljk2Mi0zLjI1NC40MzQtMi4yMjMuMzk4LTUuNDI0LjM5OC01LjQyNHptLTMuMzUzIDUuNTloLTIuMDgxVjkuMDU3YzAtMS4wNzUtLjQ1Mi0xLjYyLTEuMzU3LTEuNjItMSAwLTEuNTAxLjY0Ny0xLjUwMSAxLjkyN3YyLjc5MWgtMi4wNjlWOS4zNjRjMC0xLjI4LS41MDEtMS45MjctMS41MDItMS45MjctLjkwNSAwLTEuMzU3LjU0Ni0xLjM1NyAxLjYydjUuMDk5SDYuMDI2VjguOTAzYzAtMS4wNzQuMjczLTEuOTI3LjgyMy0yLjU1OC41NjYtLjYzMSAxLjMwNy0uOTU1IDIuMjI4LS45NTUgMS4wNjUgMCAxLjg3Mi40MDkgMi40MDUgMS4yMjhsLjUxOC44NjkuNTE5LS44NjljLjUzMy0uODE5IDEuMzQtMS4yMjggMi40MDUtMS4yMjguOTIgMCAxLjY2Mi4zMjQgMi4yMjguOTU1LjU0OS42MzEuODIyIDEuNDg0LjgyMiAyLjU1OHY1LjI1M3oiLz48L3N2Zz4=" class="icon" />
|
||||||
|
{{ cv.person.social.mastodon | strip_proto }}</i>
|
||||||
|
{% endif %}
|
||||||
|
<i class="email fa-envelope">
|
||||||
|
|
||||||
|
<img alt="E-mail" src="data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIgdmlld0JveD0iMCAwIDI0IDI0Ij48cGF0aCBkPSJNMCAzdjE4aDI0di0xOGgtMjR6bTYuNjIzIDcuOTI5bC00LjYyMyA1LjcxMnYtOS40NThsNC42MjMgMy43NDZ6bS00LjE0MS01LjkyOWgxOS4wMzVsLTkuNTE3IDcuNzEzLTkuNTE4LTcuNzEzem01LjY5NCA3LjE4OGwzLjgyNCAzLjA5OSAzLjgzLTMuMTA0IDUuNjEyIDYuODE3aC0xOC43NzlsNS41MTMtNi44MTJ6bTkuMjA4LTEuMjY0bDQuNjE2LTMuNzQxdjkuMzQ4bC00LjYxNi01LjYwN3oiLz48L3N2Zz4=" class="icon">
|
||||||
|
{{cv.person.email}}
|
||||||
|
</i>
|
||||||
|
<i class="phone fa-phone">
|
||||||
|
<img alt="Phone" src="data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIgdmlld0JveD0iMCAwIDI0IDI0Ij48cGF0aCBkPSJNNi4xNzYgMS4zMjJsMi44NDQtMS4zMjIgNC4wNDEgNy44OS0yLjcyNCAxLjM0MWMtLjUzOCAxLjI1OSAyLjE1OSA2LjI4OSAzLjI5NyA2LjM3Mi4wOS0uMDU4IDIuNjcxLTEuMzI4IDIuNjcxLTEuMzI4bDQuMTEgNy45MzJzLTIuNzY0IDEuMzU0LTIuODU0IDEuMzk2Yy03Ljg2MiAzLjU5MS0xOS4xMDMtMTguMjU4LTExLjM4NS0yMi4yODF6bTEuOTI5IDEuMjc0bC0xLjAyMy41MDRjLTUuMjk0IDIuNzYyIDQuMTc3IDIxLjE4NSA5LjY0OCAxOC42ODZsLjk3MS0uNDc0LTIuMjcxLTQuMzgzLTEuMDI2LjVjLTMuMTYzIDEuNTQ3LTguMjYyLTguMjE5LTUuMDU1LTkuOTM4bDEuMDA3LS40OTctMi4yNTEtNC4zOTh6Ii8+PC9zdmc+" class="icon">
|
||||||
|
+{{cv.person.phone | insert_space_every(times=3)}}
|
||||||
|
</i>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
Loading…
Reference in New Issue
Block a user