Skip to content

Commit

Permalink
Added Twitter share feature (#72)
Browse files Browse the repository at this point in the history
* added twitter share button (incomplete) + created image upload server

Signed-off-by: danbugs <[email protected]>

* fix && fmt

Signed-off-by: danbugs <[email protected]>

* adding recaptcha

Signed-off-by: danbugs <[email protected]>

* enabling cors for app.js

Signed-off-by: danbugs <[email protected]>

* added missing packages for app.js

Signed-off-by: danbugs <[email protected]>

* bug fix

Signed-off-by: danbugs <[email protected]>

* typo

Signed-off-by: danbugs <[email protected]>

* decreased attack surface

Signed-off-by: danbugs <[email protected]>

---------

Signed-off-by: danbugs <[email protected]>
  • Loading branch information
danbugs authored Jan 4, 2024
1 parent fcacd0a commit b6d9b6f
Show file tree
Hide file tree
Showing 11 changed files with 494 additions and 10 deletions.
18 changes: 18 additions & 0 deletions Cargo.lock

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

2 changes: 2 additions & 0 deletions frontend/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,12 @@ country-emoji = "0.2"
js-sys = "0.3"
wasm-bindgen = "0.2"
yew-router = "0.18"
yew-recaptcha-v3 = { git = "https://github.com/danbugs/yew-recaptcha/", rev = "196f089158ea192b4e7db5b6c32d977cc5681d3c" }

[dependencies.web-sys]
version = "0.3"
features = [
"HtmlInputElement",
"HtmlButtonElement",
"HtmlCanvasElement",
]
1 change: 1 addition & 0 deletions frontend/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
crossorigin="anonymous" referrerpolicy="no-referrer" />
<link href='http://fonts.googleapis.com/css?family=Ubuntu&subset=cyrillic,latin' rel='stylesheet' type='text/css' />
<link rel="icon" type="image/png" href="/assets/favicon.png" />
<script src="https://cdnjs.cloudflare.com/ajax/libs/html2canvas/1.3.2/html2canvas.min.js"></script>
<style type="text/css">
body {
font-family: 'Ubuntu', sans-serif;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,16 @@
use yew::{function_component, html, Html, Properties};
use gloo_net::http::Request;
use js_sys::encode_uri_component;
use web_sys::{window, HtmlElement};
use yew::{function_component, html, use_effect_with, use_state, Callback, Html, Properties};

use crate::models::{Set, Tournament};
use crate::models::{CaptchaRequest, ImageData, Set, Tournament, UploadResponse};

use crate::components::loading_spinner::LoadingSpinner;
use crate::utils::calculate_spr_or_uf;
use wasm_bindgen::prelude::*;
use yew_recaptcha_v3::recaptcha::use_recaptcha;

const RECAPTCHA_SITE_KEY: &str = std::env!("RECAPTCHA_SITE_KEY");

#[derive(Properties, PartialEq)]
pub struct Props {
Expand All @@ -12,15 +19,152 @@ pub struct Props {
pub selected_tournament_sets: Option<Vec<Set>>,
}

#[wasm_bindgen]
extern "C" {
#[wasm_bindgen(js_namespace = window)]
pub fn html2canvas(element: &HtmlElement) -> js_sys::Promise;
}

#[function_component(PlayerProfileTournamentList)]
pub fn player_profile_tournament_list(props: &Props) -> Html {
let is_screenshotting = use_state(|| None::<i32>);

let last_token = use_state(|| None);
let on_execute = use_state(|| None);

// Recaptcha will be called only when on_execute changes.
let on_execute_clone = on_execute.clone();
use_recaptcha(RECAPTCHA_SITE_KEY.to_string(), on_execute_clone);

{
let is_screenshotting = is_screenshotting.clone();
let last_token = last_token.clone();
use_effect_with(last_token, move |lt| {
if (*is_screenshotting).is_some() {
let window = window().unwrap();
let document = window.document().unwrap();
let screenshotting_id = (*is_screenshotting).unwrap();
let element = document
.get_element_by_id(&format!("result-section-{}", screenshotting_id))
.unwrap();
let html_element = element.dyn_into::<HtmlElement>().unwrap();

let promise = html2canvas(&html_element);

let is_screenshotting = is_screenshotting.clone();
let lt = lt.clone();
wasm_bindgen_futures::spawn_local(async move {
let captcha_res =
Request::post(&format!("{}/check-captcha", env!("SERVER_ADDRESS_2")))
.json(&CaptchaRequest {
token: (*lt).clone().unwrap(),
})
.unwrap()
.send()
.await;

match captcha_res {
Ok(response) if response.ok() => {
let result = wasm_bindgen_futures::JsFuture::from(promise).await;
match result {
Ok(canvas) => {
let canvas: web_sys::HtmlCanvasElement =
canvas.dyn_into().unwrap_throw();
let data_url = canvas.to_data_url().unwrap_throw();

let upload_res = Request::post(&format!(
"{}/upload",
env!("SERVER_ADDRESS_2")
))
.header("Content-Type", "application/json")
.json(&ImageData { image: data_url })
.unwrap()
.send()
.await;

if let Ok(upload_response) = upload_res {
if upload_response.ok() {
let json_result: Result<UploadResponse, _> =
upload_response.json().await;
match json_result {
Ok(UploadResponse::Success(success)) => {
// Construct Twitter Web Intent URL
let twitter_message = "Heads up, the URL below will be rendered as an image once you send out the tweet - feel free to delete this message and add your own comment about your run while leaving the URL at the bottom.\n";
let image_url = format!(
"https://smithe.pictures/image/{}",
success.filename
);
let tweet_intent_url = format!("https://twitter.com/intent/tweet?text={}%0A{}", encode_uri_component(twitter_message), encode_uri_component(&image_url));

// Open the Twitter Intent URL in a new tab/window
let window = web_sys::window().unwrap();
let _ = window.open_with_url(&tweet_intent_url);
}
Ok(UploadResponse::Error(error)) => {
web_sys::console::log_1(
&format!("Error: {}", error.message).into(),
);
}
Err(e) => {
web_sys::console::log_1(
&format!(
"Failed to parse JSON response: {:?}",
e
)
.into(),
);
}
}
} else {
web_sys::console::log_1(
&"Image upload failed with non-success status"
.into(),
);
}
} else {
web_sys::console::log_1(&"Failed to send image".into());
}
}
Err(e) => {
web_sys::console::log_1(&format!("Error: {:?}", e).into())
}
}
}
_ => {
// Handle captcha verification failure
web_sys::console::log_1(&"Captcha verification failed".into());
}
}

is_screenshotting.set(None);
});
}
});
}

if props.display {
html! {
html! {
<div>
<div class="accordion" id="accordion">
{
props.selected_player_tournaments.as_ref().unwrap().iter().map(|t| {
let onclick = {
let last_token = last_token.clone();
let on_execute = on_execute.clone();
let is_screenshotting = is_screenshotting.clone();
let tid = t.tournament_id;
Callback::from(move |_| {
let last_token = last_token.clone();
// setting the on_execute callback will force recaptcha to be recalculated.
on_execute.set(Some(Callback::from(move |token| {
last_token.set(Some(token));
})));

is_screenshotting.set(Some(tid));
()
})
};
html! {
<div class="accordion-item">
<h2 class="accordion-header" id={format!("heading-{}", t.tournament_id)}>
Expand Down Expand Up @@ -108,12 +252,54 @@ pub fn player_profile_tournament_list(props: &Props) -> Html {
).collect::<Html>()
}
<div class="row justify-content-end p-2">
<div class="col-auto">
<a href={format!("{}", t.link)}
target="_blank" rel="noopener noreferrer" class="btn btn-primary btn-sm">
<i class="bi bi-trophy" aria-hidden="true"></i> {" View on StartGG"}
</a>
</div>
{
if (*is_screenshotting).is_none() {
html! {
<>
<div class="col-auto">
<a href={format!("{}", t.link)}
target="_blank" rel="noopener noreferrer" class="btn btn-primary btn-sm">
<i class="bi bi-trophy" aria-hidden="true"></i> {" View on StartGG"}
</a>
</div>
<div class="col-auto">
<a {onclick} rel="noopener noreferrer" class="btn btn-secondary btn-sm">
<i class="bi bi-twitter" aria-hidden="true"></i> {" Share on Twitter"}
</a>
</div>
</>
}
} else {
html! {
<div class="screenshot-container" id={format!("result-section-{}", t.tournament_id)}>
<div class="tournament-info">
<h3 class="tournament-title">{(&t.event_name).to_string()}</h3>
<p class="tournament-details">
{format!("Seed: {}, Placement: {}/{}", &t.seed, &t.placement, &t.num_entrants)}
</p>
</div>
<div class="match-results">
{
for props.selected_tournament_sets.as_ref().unwrap().iter().filter(|s| s.tournament_id == t.tournament_id).map(|s| {
html! {
<div class="match-result">
<span class={if s.requester_score > s.opponent_score { "win" } else if s.requester_score < s.opponent_score { "loss" } else { "tie" }}>
{format!("{} - {} vs {} (Seed: {})", s.requester_score, s.opponent_score, s.opponent_tag_with_prefix, s.opponent_seed)}
</span>
</div>
}
})
}
</div>
<div class="twitter-footer">
<span class="screenshot-message">
{format!("See my full results at smithe.net/player/{}", t.requester_id)}
</span>
</div>
</div>
}
}
}
</div>
</ul>
</div>
Expand Down
1 change: 0 additions & 1 deletion frontend/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,6 @@ fn app() -> Html {
}

fn switch(routes: Route) -> Html {
web_sys::console::log_1(&format!("route: {:#?}", routes).into());
html! {
<div>
<header>
Expand Down
28 changes: 28 additions & 0 deletions frontend/src/models.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,33 @@
use serde::{Deserialize, Serialize};

#[derive(Serialize, Deserialize)]
pub struct ImageData {
pub image: String,
}

#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct UploadSuccessResponse {
pub message: String,
pub filename: String,
}

#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct UploadErrorResponse {
pub message: String,
}

#[derive(Serialize, Deserialize, Debug, Clone)]
#[serde(untagged)]
pub enum UploadResponse {
Success(UploadSuccessResponse),
Error(UploadErrorResponse),
}

#[derive(Serialize, Deserialize)]
pub struct CaptchaRequest {
pub token: String,
}

#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
pub struct Player {
pub player_id: i32,
Expand Down
59 changes: 58 additions & 1 deletion frontend/styles.css
Original file line number Diff line number Diff line change
Expand Up @@ -18,4 +18,61 @@ ul.list-group.list-group-hover button:hover{
color: black !important;
background-color: rgb(198, 38, 62, 0.6) !important;
}


/* screenshot stuff */
.screenshot-container {
width: 800px; /* Fixed width */
height: 418px; /* Fixed height */
border-radius: 25px; /* Rounded edges, considering Twitter's card style */
overflow: hidden;
background: white;
box-shadow: 0 0 15px rgba(0,0,0,0.1);
padding: 15px; /* Further reduced padding to fit the title */
font-family: 'Arial', sans-serif;
display: flex;
flex-direction: column;
justify-content: space-between; /* Distribute space evenly */
}

.tournament-info {
width: 100%;
text-align: center;
}

.tournament-title {
font-size: 22px; /* Slightly reduced font size to prevent cutting off */
margin-bottom: 2px; /* Minimized bottom margin */
}

.tournament-details {
font-size: 16px; /* Adjusted for space */
}

.match-results {
width: 770px; /* Width to fit within the padding */
text-align: center;
font-size: 15px; /* Slightly reduced font size to fit more content */
}

.match-result {
background: #f2f2f2;
padding: 6px 10px; /* Slightly reduced padding */
margin: 3px 0; /* Reduced margin to fit more items */
border-radius: 10px; /* Adjusted border radius */
}

.win {
color: green;
}

.loss {
color: red;
}

.tie {
color: #FFCC00;
}

.twitter-footer {
font-size: 15px; /* Slightly reduced font size */
}
3 changes: 3 additions & 0 deletions image-upload-backend/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
node_modules/
package-lock.json
uploads/
Loading

0 comments on commit b6d9b6f

Please sign in to comment.