feat: implement demo for pdf generation using typst
This commit is contained in:
1 parent
9f88c4d6a7
commit
97ca4a64e9
3 files changed
+79
-5
No files matched your search
Generated
+1
-2
@@ -2938,7 +2938,7 @@ dependencies = [
|
|||||||
"aws-config",
|
"aws-config",
|
||||||
"aws-sdk-dynamodb",
|
"aws-sdk-dynamodb",
|
||||||
"axum",
|
"axum",
|
||||||
"chrono",
|
"base64 0.22.1",
|
||||||
"console_error_panic_hook",
|
"console_error_panic_hook",
|
||||||
"leptos",
|
"leptos",
|
||||||
"leptos_axum",
|
"leptos_axum",
|
||||||
@@ -2946,7 +2946,6 @@ dependencies = [
|
|||||||
"leptos_oidc",
|
"leptos_oidc",
|
||||||
"leptos_router",
|
"leptos_router",
|
||||||
"macros",
|
"macros",
|
||||||
"reactive_stores",
|
|
||||||
"serde",
|
"serde",
|
||||||
"serde_dynamo",
|
"serde_dynamo",
|
||||||
"tokio",
|
"tokio",
|
||||||
|
|||||||
+1
-2
@@ -22,11 +22,10 @@ serde_dynamo = { version = "4.2.14", features = ["aws-sdk-dynamodb+1"], optional
|
|||||||
uuid = { version = "1.17.0", features = ["serde", "v4", "js"] }
|
uuid = { version = "1.17.0", features = ["serde", "v4", "js"] }
|
||||||
leptos_oidc = "0.9.0"
|
leptos_oidc = "0.9.0"
|
||||||
url = "2.5.4"
|
url = "2.5.4"
|
||||||
|
base64 = "0.22.1"
|
||||||
|
|
||||||
macros = { path = "./macros" }
|
macros = { path = "./macros" }
|
||||||
traits = { path = "./traits" }
|
traits = { path = "./traits" }
|
||||||
chrono = { version = "0.4.42", features = ["serde"] }
|
|
||||||
reactive_stores = "0.2.5"
|
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
hydrate = [
|
hydrate = [
|
||||||
|
|||||||
+77
-1
@@ -1,9 +1,12 @@
|
|||||||
// Server dependencies
|
// Server dependencies
|
||||||
#[cfg(feature = "ssr")]
|
#[cfg(feature = "ssr")]
|
||||||
use aws_sdk_dynamodb::{Client, error::ProvideErrorMetadata, types::AttributeValue};
|
use aws_sdk_dynamodb::{Client, error::ProvideErrorMetadata, types::AttributeValue};
|
||||||
|
|
||||||
#[cfg(feature = "ssr")]
|
#[cfg(feature = "ssr")]
|
||||||
use serde_dynamo::{from_item, to_item};
|
use serde_dynamo::{from_item, to_item};
|
||||||
|
#[cfg(feature = "ssr")]
|
||||||
|
use std::process::{Command, Stdio};
|
||||||
|
#[cfg(feature = "ssr")]
|
||||||
|
use std::io::Write;
|
||||||
|
|
||||||
use crate::common::{ExpandableInfo, UserClaims};
|
use crate::common::{ExpandableInfo, UserClaims};
|
||||||
use crate::pages::UnauthenticatedPage;
|
use crate::pages::UnauthenticatedPage;
|
||||||
@@ -16,6 +19,11 @@ use leptos::leptos_dom::logging::console_log;
|
|||||||
use leptos::prelude::*;
|
use leptos::prelude::*;
|
||||||
use leptos_oidc::{Algorithm, AuthLoaded, AuthSignal, Authenticated};
|
use leptos_oidc::{Algorithm, AuthLoaded, AuthSignal, Authenticated};
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
|
use base64::Engine;
|
||||||
|
use leptos::task::spawn_local;
|
||||||
|
use leptos::web_sys::HtmlAnchorElement;
|
||||||
|
use leptos_router::hooks::use_navigate;
|
||||||
|
use leptos::wasm_bindgen::JsCast;
|
||||||
use traits::{AsReactive, ReactiveCapture};
|
use traits::{AsReactive, ReactiveCapture};
|
||||||
|
|
||||||
/// # Get Student Info
|
/// # Get Student Info
|
||||||
@@ -100,6 +108,48 @@ pub async fn log_expandable(info: ExpandableInfo) -> Result<(), ServerFnError> {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[server(CreateExamplePdf, endpoint = "/example-pdf")]
|
||||||
|
pub async fn create_example_pdf() -> Result<Vec<u8>, ServerFnError> {
|
||||||
|
// Create typst template. This will be replaced with getting the template from S3 in the future.
|
||||||
|
let template = r#"
|
||||||
|
= Testing Typst
|
||||||
|
This form will create a new student application. The PDF that's generated (like this one)
|
||||||
|
is a template that will be used later. We want to preprocess these templates to insert the
|
||||||
|
correct values from the API later on.
|
||||||
|
|
||||||
|
= Second Header
|
||||||
|
This area is going to do some math because I think that's fun. We'll typeset a simple sum:
|
||||||
|
$ sum_(i=0)^(10)
|
||||||
|
(n_i) $
|
||||||
|
|
||||||
|
= Third Test
|
||||||
|
This is just going to create a bulleted list inside a numbered list:
|
||||||
|
+ Numbers
|
||||||
|
+ Number again
|
||||||
|
- Not a number.
|
||||||
|
"#;
|
||||||
|
|
||||||
|
// Create and run the typst command.
|
||||||
|
let mut command = Command::new("typst");
|
||||||
|
command
|
||||||
|
.arg("compile")
|
||||||
|
.arg("-") // Use stdin as input
|
||||||
|
.arg("-") // Write output to stdout
|
||||||
|
.stdin(Stdio::piped())
|
||||||
|
.stdout(Stdio::piped());
|
||||||
|
|
||||||
|
let mut process = command.spawn()?;
|
||||||
|
|
||||||
|
process.stdin
|
||||||
|
.take().unwrap()
|
||||||
|
.write(template.as_bytes())?;
|
||||||
|
|
||||||
|
let output = process.wait_with_output()?;
|
||||||
|
let pdf_bytes = output.stdout;
|
||||||
|
|
||||||
|
Ok(pdf_bytes)
|
||||||
|
}
|
||||||
|
|
||||||
/// The main home page component. Contains a simple contact form.
|
/// The main home page component. Contains a simple contact form.
|
||||||
#[component]
|
#[component]
|
||||||
pub fn HomePage() -> impl IntoView {
|
pub fn HomePage() -> impl IntoView {
|
||||||
@@ -129,6 +179,9 @@ pub fn HomePage() -> impl IntoView {
|
|||||||
);
|
);
|
||||||
let submit_action = ServerAction::<CreateSampleSubmission>::new();
|
let submit_action = ServerAction::<CreateSampleSubmission>::new();
|
||||||
let log_action = ServerAction::<LogExpandableInfo>::new();
|
let log_action = ServerAction::<LogExpandableInfo>::new();
|
||||||
|
let pdf_action = ServerAction::<CreateExamplePdf>::new();
|
||||||
|
|
||||||
|
let navigate = use_navigate();
|
||||||
|
|
||||||
view! {
|
view! {
|
||||||
<AuthLoaded fallback=Loading>
|
<AuthLoaded fallback=Loading>
|
||||||
@@ -322,6 +375,29 @@ pub fn HomePage() -> impl IntoView {
|
|||||||
disabled=elements_disabled
|
disabled=elements_disabled
|
||||||
>"Submit"</ActionButton>
|
>"Submit"</ActionButton>
|
||||||
</Row>
|
</Row>
|
||||||
|
<Row>
|
||||||
|
<ActionButton
|
||||||
|
on:click=move |_| {
|
||||||
|
console_log("Attempting to get PDF from server endpoint");
|
||||||
|
spawn_local(async move {
|
||||||
|
let result = create_example_pdf().await;
|
||||||
|
if let Ok(bytes) = result {
|
||||||
|
let base64 = base64::engine::general_purpose::STANDARD.encode(bytes);
|
||||||
|
let data_url = format!("data:application/pdf;base64,{}", base64);
|
||||||
|
|
||||||
|
console_log("Found document, opening in new tab...");
|
||||||
|
|
||||||
|
let link = document().create_element("a")
|
||||||
|
.unwrap().dyn_into::<HtmlAnchorElement>()
|
||||||
|
.unwrap();
|
||||||
|
link.set_href(&*data_url);
|
||||||
|
link.set_target("_blank");
|
||||||
|
link.click();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
>"Get PDF"</ActionButton>
|
||||||
|
</Row>
|
||||||
<Row>
|
<Row>
|
||||||
<p>
|
<p>
|
||||||
{result_msg}
|
{result_msg}
|
||||||
|
|||||||
Reference in new issue
Block a user