Skip to content

Commit

Permalink
Merge pull request #43 from picture-pro/42-show-qr-code-on-upload
Browse files Browse the repository at this point in the history
feat: prototyped qr code page and integration
  • Loading branch information
johnbchron authored Feb 26, 2024
2 parents 69aa827 + 50d3920 commit 69df817
Show file tree
Hide file tree
Showing 16 changed files with 144 additions and 40 deletions.
10 changes: 10 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ leptos_axum = { version = "0.6", features = ["experimental-islands"] }
leptos_meta = { version = "0.6", features = ["nightly"] }
leptos_router = { version = "0.6", features = ["nightly"] }
log = "0.4"
qrcode = "0.13.0"
server_fn = { version = "0.6", features = ["multipart"] }
serde = { version = "1", features = ["derive"] }
simple_logger = "4.2.0"
Expand Down
1 change: 1 addition & 0 deletions crates/bl/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ base64.workspace = true
bytes.workspace = true
color-eyre.workspace = true
image.workspace = true
qrcode.workspace = true
thiserror.workspace = true
tracing.workspace = true
surrealdb.workspace = true
Expand Down
1 change: 1 addition & 0 deletions crates/bl/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
pub mod fetch;
pub mod model_ext;
pub mod qr_code;
pub mod upload;
17 changes: 5 additions & 12 deletions crates/bl/src/model_ext.rs
Original file line number Diff line number Diff line change
Expand Up @@ -31,21 +31,14 @@ pub trait ModelExt: core_types::CoreModel {
}
}

fn update(
&self,
fn patch(
id: <Self as core_types::CoreModel>::Id,
client: &SurrealRootClient,
patch: surrealdb::opt::PatchOp,
) -> impl std::future::Future<Output = Result<Self>> + Send {
let model = self.clone();

async move {
let result: Vec<Self> = client
.update(<<Self as CoreModel>::Id as CoreId>::TABLE)
.content(model)
.await?;
result
.into_iter()
.next()
.ok_or_eyre("Failed to update model")
let result: Option<Self> = client.update(id).patch(patch).await?;
result.ok_or_eyre("Failed to patch model")
}
}
}
Expand Down
21 changes: 21 additions & 0 deletions crates/bl/src/qr_code.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
use color_eyre::eyre::{Result, WrapErr};
use qrcode::QrCode;
use tracing::instrument;

/// Generate a QR code from the given data. Returns base64 encoded PNG data.
#[instrument]
pub fn generate_qr_code(data: &str) -> Result<String> {
let code =
QrCode::new(data.as_bytes()).wrap_err("Failed to create QR code")?;
let image = code.render::<image::Luma<u8>>().build();
let mut png_bytes = Vec::new();
let encoder = image::codecs::jpeg::JpegEncoder::new(&mut png_bytes);
image
.write_with_encoder(encoder)
.wrap_err("Failed to write QR code to png")?;

use base64::prelude::*;
let data = BASE64_STANDARD.encode(&png_bytes);

Ok(data)
}
18 changes: 8 additions & 10 deletions crates/bl/src/upload.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ use core_types::{
PublicArtifact,
};
use serde::{Deserialize, Serialize};
use surrealdb::opt::PatchOp;
use tracing::instrument;

use crate::model_ext::ModelExt;
Expand Down Expand Up @@ -126,16 +127,13 @@ pub async fn upload_single_photo(
))
})?;

let photo = Photo {
group: group.id,
..photo
};

photo.update(&client).await.map_err(|e| {
PhotoUploadError::DBError(format!(
"Failed to update photo with group in surreal: {e}"
))
})?;
Photo::patch(photo.id, &client, PatchOp::replace("group", group.id))
.await
.map_err(|e| {
PhotoUploadError::DBError(format!(
"Failed to update photo with group in surreal: {e}"
))
})?;

Ok(group.clone())
}
12 changes: 11 additions & 1 deletion crates/core_types/src/ssr.rs
Original file line number Diff line number Diff line change
Expand Up @@ -37,11 +37,20 @@ impl From<UlidOrThing> for ulid::Ulid {
/// This trait is mainly meant for bounds and to be extended, hence it has no
/// methods.
pub trait CoreModel:
Clone + Debug + Serialize + for<'a> Deserialize<'a> + Sized + Send + 'static
Clone
+ Debug
+ Serialize
+ for<'a> Deserialize<'a>
+ Sized
+ Send
+ Sync
+ 'static
{
/// The id type for this model.
type Id: CoreId<Model = Self> + IntoResource<Option<Self>> + Send;

/// Get the ID for this model.
fn id(&self) -> Self::Id;
/// Get the metadata for this model.
fn meta(&self) -> &crate::ObjectMeta;
}
Expand Down Expand Up @@ -103,6 +112,7 @@ macro_rules! impl_table {

impl CoreModel for $model_type {
type Id = $id_type;
fn id(&self) -> $id_type { self.id }
fn meta(&self) -> &crate::ObjectMeta { &self.meta }
}
};
Expand Down
2 changes: 1 addition & 1 deletion crates/site-app/src/components/gallery.rs
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ fn PhotoGroupList(groups: Vec<core_types::PhotoGroup>) -> impl IntoView {
}

view! {
<div class="flex-1 flex flex-col gap-4 items-stretch lg:max-w-lg">
<div class="flex-1 flex flex-col gap-4 items-stretch lg:max-w-xl">
{ groups.into_iter().map(|group| {
view! {
<PhotoGroup group=group />
Expand Down
17 changes: 6 additions & 11 deletions crates/site-app/src/components/photo_group.rs
Original file line number Diff line number Diff line change
Expand Up @@ -36,11 +36,8 @@ pub fn PhotoGroup(
.into_view(),
};

let share_url = format!(
"{}/photo/{}",
std::env::var("APP_BASE_URL").expect("APP_BASE_URL not set"),
group.id.0
);
let purchase_url = format!("/photo/{}", group.id.0);
let qr_code_url = format!("/qr/{}", group.id.0);

let photos_element = view! {
{ group.photos.clone().into_iter().map(|photo_id| {
Expand Down Expand Up @@ -97,12 +94,10 @@ pub fn PhotoGroup(
</div>
{ match read_only {
false => view! {
<a
class="d-btn d-btn-primary d-btn-sm text-lg font-semibold tracking-tight"
href={ share_url }
>
"Share"
</a>
<div class="flex flex-col gap-4">
<a class="d-btn d-btn-sm" href={ purchase_url }>"Purchase Page"</a>
<a class="d-btn d-btn-primary d-btn-sm" href={ qr_code_url }>"QR Code"</a>
</div>
}.into_view(),
true => ().into_view()
} }
Expand Down
12 changes: 8 additions & 4 deletions crates/site-app/src/components/photo_upload.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,12 @@ pub fn PhotoUpload() -> impl IntoView {
});
let pending = upload_action.pending();
let value = upload_action.value();
let successful = move || matches!(value(), Some(Ok(_)));

create_effect(move |_| {
if successful() {
crate::components::navigation::reload();
if let Some(Ok(id)) = value() {
// redirect to the qr code page
let url = format!("/qr/{}", id.0);
crate::components::navigation::navigate_to(&url);
}
});

Expand All @@ -36,7 +37,10 @@ pub fn PhotoUpload() -> impl IntoView {
</label>
</div>
<div class="d-form-control">
<input type="file" class="d-file-input d-file-input-bordered w-full" name="photo" />
<input
type="file" class="d-file-input d-file-input-bordered w-full"
name="photo" accept="image/*" required=true
/>
</div>
<div class="mt-6"></div>
<div class="d-form-control">
Expand Down
1 change: 1 addition & 0 deletions crates/site-app/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ pub fn App() -> impl IntoView {
<Route path="/signup" view=pages::auth::signup_page::SignupPage/>
<Route path="/photo/:id" view=pages::purchase::PurchasePage/>
<Route path="/photo" view=pages::purchase::error::PurchasePageNoId/>
<Route path="/qr/:id" view=pages::qr_code::QrCodePage/>
</Routes>
<Footer/>
</div>
Expand Down
1 change: 1 addition & 0 deletions crates/site-app/src/pages/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ pub mod auth;
pub mod dashboard;
pub mod home_page;
pub mod purchase;
pub mod qr_code;

use leptos::*;

Expand Down
66 changes: 66 additions & 0 deletions crates/site-app/src/pages/qr_code.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
use leptos::*;
use leptos_router::use_params_map;

use crate::pages::SmallPageWrapper;

#[component]
pub fn QrCodePage() -> impl IntoView {
let params = use_params_map();
let id = params().get("id").cloned();

// routing makes sure that the ID param exists
let id = id.unwrap();
let url = format!(
"{}/photo/{id}",
std::env::var("APP_BASE_URL").expect("APP_BASE_URL not set"),
);

view! {
<SmallPageWrapper>
<div class="d-card-body gap-4">
<p class="text-2xl font-semibold tracking-tight">"QR Code"</p>
<QrCode data=url class="rounded-box border shadow" />
<div class="flex flex-row items-center gap-4">
<a href="/" class="d-btn d-btn-primary d-btn-sm">"Back to Dashboard"</a>
<div class="flex-1" />
<a href={format!("/photo/{}", id)} class="d-btn d-btn-sm">"View Photo"</a>
</div>
</div>
</SmallPageWrapper>
}
}

#[component]
pub fn QrCode(
data: String,
#[prop(default = "")] class: &'static str,
) -> impl IntoView {
let qr_code = create_resource(move || data.clone(), generate_qr_code);

view! {
<Suspense fallback=|| view!{}>
{ qr_code.map(|r| {
match r {
Ok(qr_code) => view! {
<img src={format!("data:image/png;base64,{}", qr_code)} class=class />
}.into_view(),
Err(e) => view! {
<div>
<p>{e.to_string()}</p>
<p>"Failed to generate QR code"</p>
</div>
}.into_view(),
}
})}
</Suspense>
}
}

#[server]
pub async fn generate_qr_code(data: String) -> Result<String, ServerFnError> {
bl::qr_code::generate_qr_code(&data).map_err(|e| {
let error = e.to_string();
logging::error!("Failed to generate QR code: {}", error);
ServerFnError::new(error)
})
}
2 changes: 2 additions & 0 deletions crates/site-app/style/fonts.css
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,6 @@
font-family: 'inter';
src: url('/fonts/inter.ttf');
font-display: swap;
font-weight: 100 900;
font-style: normal;
}
2 changes: 1 addition & 1 deletion justfile
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,4 @@ surreal:
wipe-surreal:
rm -rf /tmp/surreal_data
apply-surreal:
cd migrations && surrealdb-migrations apply --username $SURREALDB_ROOT_USER --password $SURREALDB_ROOT_PASS --ns main --db main
cd migrations && surrealdb-migrations apply --username $SURREAL_USER --password $SURREAL_PASS --ns main --db main

0 comments on commit 69df817

Please sign in to comment.