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"] }
|
||||
yansi = "0.5.1"
|
||||
tera = "1.19.1"
|
||||
nanobyte_opentelemetry = { path = "../nanobyte_opentelemetry" }
|
||||
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]
|
||||
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
|
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 {
|
||||
let new_rocket = rocket.manage(ChromiumCoordinator::new().await);
|
||||
let coordinator = new_rocket.state::<ChromiumCoordinator>().unwrap();
|
||||
Ok(new_rocket)
|
||||
}
|
||||
}
|
||||
|
14
src/lib.rs
14
src/lib.rs
@ -1,15 +1,27 @@
|
||||
use chromium::rocket::Chromium;
|
||||
use nanobyte_opentelemetry::rocket::TracingFairing;
|
||||
use rocket::{Rocket, Build, routes};
|
||||
use rocket::{Rocket, Build, routes, fairing::AdHoc};
|
||||
use serde::Deserialize;
|
||||
pub mod routes;
|
||||
mod chromium;
|
||||
mod tools;
|
||||
mod services;
|
||||
|
||||
|
||||
|
||||
#[derive(Deserialize)]
|
||||
#[serde(crate = "rocket::serde")]
|
||||
pub struct CVBackendConfig {
|
||||
cv_backend_path: String,
|
||||
}
|
||||
|
||||
pub fn rocket_builder() -> Rocket<Build> {
|
||||
rocket::build()
|
||||
.attach(TracingFairing::ignite())
|
||||
.attach(Chromium::ignite())
|
||||
.attach(
|
||||
AdHoc::config::<CVBackendConfig>()
|
||||
)
|
||||
.mount("/", routes![
|
||||
routes::pdf::render_pdf_cv,
|
||||
routes::pdf::render_html_cv
|
||||
|
@ -1,12 +1,15 @@
|
||||
use headless_chrome::Browser;
|
||||
use headless_chrome::types::PrintToPdfOptions;
|
||||
use nanobyte_opentelemetry::rocket::TracingSpan;
|
||||
use rocket::{get, response::stream::ByteStream, fairing::Fairing, fs::NamedFile, futures::TryFutureExt};
|
||||
use nanobyte_opentelemetry::rocket::{TracingSpan, RequestId, OtelReqwestClient};
|
||||
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 tera::Context;
|
||||
use tracing::{info_span, error, debug};
|
||||
use crate::{chromium::rocket::BrowserHolder, tools::{tera::NanoTera, pdf::PdfStream}};
|
||||
use urlencoding::encode;
|
||||
use crate::{chromium::rocket::BrowserHolder, tools::{tera::NanoTera, pdf::PdfStream, rocket::RequestLanguage}, services::cv::fetch_cv_data_from_backend, CVBackendConfig};
|
||||
|
||||
// TODO: request-id
|
||||
fn generate_pdf(browser: Browser, file: &NamedTempFile) -> Vec<u8> {
|
||||
@ -38,29 +41,59 @@ fn generate_pdf(browser: Browser, file: &NamedTempFile) -> Vec<u8> {
|
||||
}
|
||||
|
||||
#[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
|
||||
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")]
|
||||
pub async fn render_pdf_cv(username: &str, browser: BrowserHolder, tera: NanoTera, tracing: TracingSpan) -> PdfStream {
|
||||
#[get("/cv/<username>/<language>/output.pdf")]
|
||||
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 file = tempfile::Builder::new().suffix(".html").tempfile().unwrap();
|
||||
render_template("two_column", &file, tera);
|
||||
let span = info_span!("render_pdf", username = username);
|
||||
let pdf = span.in_scope(||{
|
||||
generate_pdf(browser.browser, &file)
|
||||
});
|
||||
drop(entered_span);
|
||||
PdfStream::new(pdf)
|
||||
match fetch_cv_data_from_backend(cv_config.cv_backend_path.clone(), request_client.0).await {
|
||||
Ok(cv_data) => {
|
||||
let file = tempfile::Builder::new().suffix(".html").tempfile().unwrap();
|
||||
render_template("two_column", &file, tera, cv_data, language.language);
|
||||
let span = info_span!("render_pdf", username = username);
|
||||
let pdf = span.in_scope(||{
|
||||
generate_pdf(browser.browser, &file)
|
||||
});
|
||||
drop(entered_span);
|
||||
Ok(PdfStream::new(pdf))
|
||||
},
|
||||
Err(e) => {
|
||||
error!("Error fetching cv data: {:?}", e);
|
||||
Err(Status::InternalServerError)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Route only for debuging
|
||||
#[get("/cv/<username>/output.html")]
|
||||
pub async fn render_html_cv(username: &str, tera: NanoTera, tracing: TracingSpan) -> NamedFile {
|
||||
let entered_span = tracing.0.enter();
|
||||
let file = tempfile::Builder::new().suffix(".html").tempfile().unwrap();
|
||||
render_template("two_column", &file, tera);
|
||||
NamedFile::open(file.path()).await.unwrap()
|
||||
#[get("/cv/<username>/<language>/output.html")]
|
||||
pub async fn render_html_cv(username: &str, tera: NanoTera, tracing: TracingSpan, request_client: OtelReqwestClient,
|
||||
cv_config: &State<CVBackendConfig>, language: RequestLanguage) -> Result<NamedFile, Status> {
|
||||
let _ = tracing.0.enter();
|
||||
match fetch_cv_data_from_backend(cv_config.cv_backend_path.clone(), request_client.0).await {
|
||||
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 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 tera::Tera;
|
||||
use tera::{Tera, Value, Error};
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct NanoTera(pub Tera);
|
||||
@ -11,6 +15,33 @@ impl<'r> FromRequest<'r> for NanoTera {
|
||||
type Error = ();
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
.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>
|
||||
</head>
|
||||
<body>
|
||||
@ -120,78 +204,134 @@
|
||||
<div class="flex-items">
|
||||
<div class="header">
|
||||
<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 class="part">
|
||||
<div class="section-header">
|
||||
<h2>Experience</h2>
|
||||
</div>
|
||||
<div class="job part-content">
|
||||
<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 class="flex-container-skills">
|
||||
<div class="flex-items-skills">
|
||||
<h2>{{ "skills-languages" | translate(lang=lang) }}</h2>
|
||||
</div>
|
||||
<div class="flex-items-experience">
|
||||
<div class="location">Location</div>
|
||||
<div class="dates">Dates</div>
|
||||
<div class="flex-items-skills">
|
||||
<h2>{{ "skills-technology" | translate(lang=lang) }}</h2>
|
||||
</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 class="part">
|
||||
<div class="section-header">
|
||||
<h2>Experience</h2>
|
||||
<h2>{{ "work-experience" | translate(lang=lang) }}</h2>
|
||||
</div>
|
||||
{% for job in cv.jobs %}
|
||||
<div class="job part-content">
|
||||
<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>
|
||||
<div class="title">{{ job.title }}</div>
|
||||
<div class="languages">{{ job.languages }}</div>
|
||||
<p>
|
||||
baklklbkalkblaklkb lakbl aklkbl akblka lkblakbla
|
||||
{{ job.description | lang_entity(lang=lang) }}
|
||||
</p>
|
||||
</div>
|
||||
<div class="flex-items-experience">
|
||||
<div class="location">Location</div>
|
||||
<div class="dates">Dates</div>
|
||||
<div class="company">@{{ job.company }}</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>
|
||||
{% endfor %}
|
||||
</div>
|
||||
<div class="part">
|
||||
<div class="section-header">
|
||||
<h2>Experience</h2>
|
||||
<h2>{{ "education" | translate(lang=lang) }}</h2>
|
||||
</div>
|
||||
<div class="job part-content">
|
||||
{% for education in cv.education %}
|
||||
<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>
|
||||
{% if education.degree %}
|
||||
<div class="title">{{ education.degree | lang_entity(lang=lang) }}</div>
|
||||
{% else %}
|
||||
<div class="title">{{ education.school }}</div>
|
||||
{% endif %}
|
||||
{% if education.description %}
|
||||
<p>
|
||||
|
||||
{{ education.description | lang_entity(lang=lang) }}
|
||||
</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<div class="flex-items-experience">
|
||||
<div class="location">Location</div>
|
||||
<div class="dates">Dates</div>
|
||||
{% if education.degree %}
|
||||
<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>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex-items">
|
||||
Photo2342342314213 423142342314213423
|
||||
|
||||
<div class="contact-bar">
|
||||
<span class="phone">L</span>
|
||||
<span class="email">L</span>
|
||||
<div class="photo">
|
||||
<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>
|
||||
|
Loading…
Reference in New Issue
Block a user