diff --git a/src/components/dashboard/chat/panel.rs b/src/components/dashboard/chat/panel.rs index b938417..bd03773 100644 --- a/src/components/dashboard/chat/panel.rs +++ b/src/components/dashboard/chat/panel.rs @@ -11,7 +11,6 @@ use crate::server::og::request::GetOGsForUserRequest; use gloo_storage::Storage; use crate::theme::Theme; -use crate::theme::THEME; use bson::oid::ObjectId; use chrono::Utc; use dioxus::prelude::*; @@ -44,6 +43,7 @@ pub fn ChatPanel(conversation_id: Signal, user_token: Signal) let mut ogs = use_signal(Vec::::new); let mut thinking = use_signal(|| false); let mut loading = use_signal(|| false); + let theme = use_context::>(); let _ = use_resource(move || async move { let now = Utc::now().timestamp(); @@ -159,7 +159,7 @@ pub fn ChatPanel(conversation_id: Signal, user_token: Signal) div { class: format!( "flex flex-col h-full {}", - if *THEME.read() == Theme::Dark { "bg-gray-900 text-white" } else { "bg-white text-gray-900" } + if theme() == Theme::Dark { "bg-gray-900 text-white" } else { "bg-white text-gray-900" } ), div { @@ -168,7 +168,7 @@ pub fn ChatPanel(conversation_id: Signal, user_token: Signal) select { class: format!( "p-2 rounded-lg mb-2 md:mb-0 flex-grow w-full md:w-auto truncate {}", - if *THEME.read() == Theme::Dark { "bg-gray-700 text-white" } else { "bg-gray-100 text-black" } + if theme() == Theme::Dark { "bg-gray-700 text-white" } else { "bg-gray-100 text-black" } ), onchange: move |evt| handle_og_change(evt.value()), option { value: "", "Select a og" }, @@ -184,7 +184,7 @@ pub fn ChatPanel(conversation_id: Signal, user_token: Signal) input { class: format!( "flex-1 p-2 rounded-lg border w-full {}", - if *THEME.read() == Theme::Dark { "bg-gray-700 text-white border-gray-600" } else { "border-gray-300" } + if theme() == Theme::Dark { "bg-gray-700 text-white border-gray-600" } else { "border-gray-300" } ), r#type: "text", placeholder: "Type your query here...", diff --git a/src/components/dashboard/chat/sidebar.rs b/src/components/dashboard/chat/sidebar.rs index e518b20..ff5144c 100644 --- a/src/components/dashboard/chat/sidebar.rs +++ b/src/components/dashboard/chat/sidebar.rs @@ -4,7 +4,6 @@ use crate::server::conversation::model::Conversation; use crate::server::conversation::request::CreateConversationRequest; use crate::server::conversation::request::GetConversationsRequest; use crate::theme::Theme; -use crate::theme::THEME; use bson::oid::ObjectId; use chrono::Utc; @@ -19,6 +18,7 @@ pub fn ConversationsSidebar( ) -> Element { let token_clone = token.clone(); let og_id_clone = og_id.clone(); + let theme = use_context::>(); use_effect(move || { let token = token.clone(); @@ -34,7 +34,7 @@ pub fn ConversationsSidebar( rsx! { div { - class: format!("p-4 {}", if *THEME.read() == Theme::Dark { "border-gray-600 bg-gray-900" } else { "border-gray-200" }), + class: format!("p-4 {}", if theme() == Theme::Dark { "border-gray-600 bg-gray-900" } else { "border-gray-200" }), h3 { class: "text-lg font-semibold mb-4 text-blue-500", "Conversations" } diff --git a/src/components/dashboard/fields/input.rs b/src/components/dashboard/fields/input.rs index 9a04b0d..2cb964a 100644 --- a/src/components/dashboard/fields/input.rs +++ b/src/components/dashboard/fields/input.rs @@ -1,5 +1,4 @@ use crate::theme::Theme; -use crate::theme::THEME; use dioxus::prelude::*; #[component] @@ -10,7 +9,8 @@ pub fn InputField( validate: fn(&str) -> bool, required: bool, ) -> Element { - let dark_mode = *THEME.read() == Theme::Dark; + let theme = use_context::>(); + let dark_mode = theme() == Theme::Dark; let handle_input = move |e: Event| { let input_value = e.value().clone(); @@ -20,6 +20,7 @@ pub fn InputField( rsx! { div { + class: "flex-grow w-full", label { class: format!("block text-sm font-medium {}", if dark_mode { "text-gray-300" } else { "text-gray-700" }), "{label}" diff --git a/src/components/dashboard/fields/number.rs b/src/components/dashboard/fields/number.rs index fd720f8..e151ee7 100644 --- a/src/components/dashboard/fields/number.rs +++ b/src/components/dashboard/fields/number.rs @@ -1,10 +1,10 @@ use crate::theme::Theme; -use crate::theme::THEME; use dioxus::prelude::*; #[component] pub fn NumberField(label: &'static str, value: Signal, required: bool) -> Element { - let dark_mode = *THEME.read() == Theme::Dark; + let theme = use_context::>(); + let dark_mode = theme() == Theme::Dark; rsx! { div { label { class: format!("block text-sm font-medium {}", if dark_mode { "text-gray-300" } else { "text-gray-700" }), "{label}" } diff --git a/src/components/dashboard/fields/select.rs b/src/components/dashboard/fields/select.rs index 27a9d12..a9f03cc 100644 --- a/src/components/dashboard/fields/select.rs +++ b/src/components/dashboard/fields/select.rs @@ -1,5 +1,4 @@ use crate::theme::Theme; -use crate::theme::THEME; use dioxus::prelude::*; #[component] @@ -8,7 +7,8 @@ pub fn SelectField( options: Vec<&'static str>, selected: Signal, ) -> Element { - let dark_mode = *THEME.read() == Theme::Dark; + let theme = use_context::>(); + let dark_mode = theme() == Theme::Dark; rsx! { div { label { class: format!("block text-sm font-medium {}", if dark_mode { "text-gray-300" } else { "text-gray-700" }), "{label}" } diff --git a/src/components/dashboard/navbar.rs b/src/components/dashboard/navbar.rs index 5c866ea..7dc2794 100644 --- a/src/components/dashboard/navbar.rs +++ b/src/components/dashboard/navbar.rs @@ -1,15 +1,18 @@ use crate::components::spinner::Spinner; use crate::components::spinner::SpinnerSize; -use crate::pages::dashboard::toggle_theme; +use crate::theme::Theme; +use crate::theme::ThemeToggle; use dioxus::prelude::*; use gloo_storage::Storage; use gloo_storage::{LocalStorage, SessionStorage}; #[component] -pub fn Navbar(dark_mode: bool) -> Element { +pub fn Navbar() -> Element { let mut show_dropdown = use_signal(|| false); let mut loading = use_signal(|| false); let navigator = use_navigator(); + let theme = use_context::>(); + let dark_mode = theme() == Theme::Dark; let handle_logout = move |e: Event| { e.stop_propagation(); @@ -25,11 +28,7 @@ pub fn Navbar(dark_mode: bool) -> Element { h1 { class: "text-2xl font-semibold", "Dashboard" } div { class: "flex items-center space-x-4", - button { - onclick: |_| toggle_theme(), - class: "p-2 rounded-full text-lg", - if dark_mode { "🌙" } else { "🌞" } - } + ThemeToggle {} div { class: "relative", button { diff --git a/src/components/dashboard/ogs/create.rs b/src/components/dashboard/ogs/create.rs index f7b14ab..f93132b 100644 --- a/src/components/dashboard/ogs/create.rs +++ b/src/components/dashboard/ogs/create.rs @@ -6,7 +6,6 @@ use crate::components::toast::manager::ToastType; use crate::server::og::controller::store_og; use crate::server::og::request::StoreOGRequest; use crate::theme::Theme; -use crate::theme::THEME; use chrono::{Duration, Utc}; use dioxus::prelude::*; use gloo_storage::{LocalStorage, Storage}; @@ -37,7 +36,8 @@ struct CachedOGsData { #[component] pub fn CreateOGPanel(user_token: Signal) -> Element { - let dark_mode = *THEME.read() == Theme::Dark; + let theme = use_context::>(); + let dark_mode = theme() == Theme::Dark; let mut title = use_signal(|| "Open SASS".to_string()); let mut description = @@ -49,6 +49,17 @@ pub fn CreateOGPanel(user_token: Signal) -> Element { let twitter_card = use_signal(|| "summary_large_image".to_string()); let twitter_site = use_signal(|| "@opensassorg".to_string()); + let mut from_color = use_signal(|| String::from("purple-300")); + let mut to_color = use_signal(|| String::from("pink-300")); + let mut img_drag = use_signal(|| None); + let mut img_position = use_signal(|| (0., 0.)); + let mut author_drag = use_signal(|| None); + let mut author_position = use_signal(|| (0., 0.)); + let mut title_drag = use_signal(|| None); + let mut title_position = use_signal(|| (0., 0.)); + let mut description_drag = use_signal(|| None); + let mut description_position = use_signal(|| (0., 0.)); + let mut title_valid = use_signal(|| true); let mut description_valid = use_signal(|| true); let mut site_name_valid = use_signal(|| true); @@ -57,6 +68,8 @@ pub fn CreateOGPanel(user_token: Signal) -> Element { let mut locale_valid = use_signal(|| true); let mut twitter_card_valid = use_signal(|| true); let mut twitter_site_valid = use_signal(|| true); + let mut from_color_valid = use_signal(|| true); + let mut to_color_valid = use_signal(|| true); let mut loading = use_signal(|| false); let mut generated_metadata = use_signal(|| None::); @@ -70,7 +83,13 @@ pub fn CreateOGPanel(user_token: Signal) -> Element { document::eval(r#" const element = document.getElementById('preview-section'); if (element) { - html2canvas(element).then((canvas) => { + html2canvas(element, { + letterRendering: 1, + logging: true, + allowTaint: true, + useCORS: true, + }) + .then((canvas) => { const base64Image = canvas.toDataURL('image/png'); const imageData = base64Image.replace(/^data:image\/png;base64,/, ''); const payload = `req[image_url]=${encodeURIComponent(imageData)}`; @@ -266,12 +285,18 @@ pub fn CreateOGPanel(user_token: Signal) -> Element { document::eval(r#" const element = document.getElementById('preview-section'); if (element) { - html2canvas(element).then((canvas) => { - const link = document.createElement('a'); - link.download = 'og-preview.png'; - link.href = canvas.toDataURL('image/png'); - link.click(); - }); + html2canvas(element, { + letterRendering: 1, + logging: true, + allowTaint: true, + useCORS: true, + }).then((canvas) => { + const link = document.createElement('a'); + link.download = 'og-preview.png'; + link.href = canvas.toDataURL('image/png'); + link.click(); + } + ); } "#); } @@ -367,6 +392,32 @@ pub fn CreateOGPanel(user_token: Signal) -> Element { InputField { label: "Site Name", value: site_name, is_valid: site_name_valid, validate: validate_field, required: false }, InputField { label: "Brand Image", value: image_url, is_valid: image_url_valid, validate: validate_field, required: false }, InputField { label: "Author", value: author, is_valid: author_valid, validate: validate_field, required: false }, + div { + class: "mb-4 w-full", + label { + class: format!("text-sm font-medium {}", if dark_mode { "text-gray-300" } else { "text-gray-700" }), + "Background Color" + } + div { + class: format!( + "border rounded flex flex-col md:flex-row gap-4 mt-1 w-full p-2 {}", + if dark_mode { "bg-gray-900" } else { "" }), + InputField { + label: "From Color", + value: from_color, + is_valid: from_color_valid, + validate: validate_field, + required: true + }, + InputField { + label: "To Color", + value: to_color, + is_valid: to_color_valid, + validate: validate_field, + required: true + } + }, + }, InputField { label: "Locale", value: locale, is_valid: locale_valid, validate: validate_field, required: false }, InputField { label: "Twitter Card Type", value: twitter_card, is_valid: twitter_card_valid, validate: validate_field, required: false }, InputField { label: "Twitter Site", value: twitter_site, is_valid: twitter_site_valid, validate: validate_field, required: false }, @@ -404,23 +455,128 @@ pub fn CreateOGPanel(user_token: Signal) -> Element { div { id: "preview-section", - class: "relative bg-gradient-to-r from-purple-300 to-pink-300 p-4 rounded-lg shadow-md w-full h-full aspect-w-16 aspect-h-9", - h1 { - class: "absolute top-4 left-4 text-4xl font-bold text-gray-900", - "{title()}" - }, + class: format!( + "relative bg-gradient-to-r from-{} to-{} p-4 rounded-lg shadow-md w-full h-full aspect-w-16 aspect-h-9", + from_color(), + to_color() + ), div { - class: "absolute top-1/3 left-4 text-xl text-gray-900", - "{description()}" + class: "absolute w-full", + draggable: true, + onmousedown: move |event| { + let (x, y) = (event.coordinates().client().x, event.coordinates().client().y); + title_drag.set(Some((x, y))); + }, + onmousemove: move |event| { + if let Some((start_x, start_y)) = title_drag() { + let delta_x = event.coordinates().client().x - start_x; + let delta_y = event.coordinates().client().y - start_y; + + title_position.set((delta_x, delta_y)); + } + }, + onmouseup: move |_| { + title_drag.set(None); + }, + style: if title_position().1 != 0. || title_position().1 != 0. { + format!( + "top: {}px; left: {}px;", + title_position().1, + title_position().0 + ) + } else { + "".to_string() + }, + h1 { + class: "absolute top-4 left-4 text-4xl font-bold text-gray-900", + "{title()}" + }, }, + div { + class: "absolute w-full top-1/3 left-4 text-xl text-gray-900", + draggable: true, + onmousedown: move |event| { + let (x, y) = (event.coordinates().client().x, event.coordinates().client().y); + description_drag.set(Some((x, y))); + }, + onmousemove: move |event| { + if let Some((start_x, start_y)) = description_drag() { + let delta_x = event.coordinates().client().x - start_x; + let delta_y = event.coordinates().client().y - start_y; + + description_position.set((delta_x, delta_y)); + } + }, + onmouseup: move |_| { + description_drag.set(None); + }, + style: if description_position().1 != 0. || description_position().1 != 0. { + format!( + "top: {}px; left: {}px;", + description_position().1, + description_position().0 + ) + } else { + "".to_string() + }, + div { + class: "absolute top-1/3 left-4 text-xl text-gray-900", + "{description()}" + }, + } h3 { class: "absolute bottom-4 left-4 text-sm text-gray-900 italic", + draggable: true, + onmousedown: move |event| { + let (x, y) = (event.coordinates().client().x, event.coordinates().client().y); + author_drag.set(Some((x, y))); + }, + onmousemove: move |event| { + if let Some((start_x, start_y)) = author_drag() { + let delta_x = event.coordinates().client().x - start_x; + let delta_y = event.coordinates().client().y - start_y; + + author_position.set((delta_x, delta_y)); + } + }, + onmouseup: move |_| { + author_drag.set(None); + }, + style: if author_position().1 != 0. || author_position().1 != 0. { + format!( + "top: {}px; left: {}px;", + author_position().1, + author_position().0 + ) + } else { + "".to_string() + }, "Author: {author()} | Site: {site_name()}" }, - img { - class: "absolute top-0 right-0 w-24 h-24 shadow-lg m-4", - src: "{image_url()}", - alt: "Brand Logo" + div { + class: "absolute w-24 h-24 m-4", + draggable: true, + onmousedown: move |event| { + let (x, y) = (event.coordinates().client().x, event.coordinates().client().y); + img_drag.set(Some((x, y))); + }, + onmousemove: move |event| { + if let Some((start_x, start_y)) = img_drag() { + let delta_x = start_x - event.coordinates().client().x; + let delta_y = event.coordinates().client().y - start_y; + + img_position.set((delta_x, delta_y)); + } + }, + onmouseup: move |_| { + img_drag.set(None); + }, + style: format!( + "background-image: url('{}'); background-size: cover; background-position: center; background-repeat: no-repeat; top: {}px; right: {}px;", + image_url(), + img_position().1, + img_position().0 + ), } }, button { @@ -475,11 +631,22 @@ fn generate_meta_tags(metadata: Metadata) -> String { async fn generate_ai_title() -> Result { let mut client = NanoAI::new(); + let system_prompt = format!( + " + **System Prompt (SP):** You are an expert in content generation for web metadata and SEO optimization. + + **Prompt (P):** Generate a unique, concise, and creative image title for an OG (Open Graph) metadata tag. + The title should align with modern web standards, capture user attention, and concisely describe the associated website content. + + **Expected Format (EF):** + - A short, unique title (maximum 20 characters) that is impactful and descriptive. + + **Roleplay (RP):** Act as an experienced SEO copywriter crafting metadata titles for websites. + " + ); + match client.create_session(None).await { - Ok(_) => match client - .send_prompt("Generate a unique short image title text for an OG metadata tag") - .await - { + Ok(_) => match client.send_prompt(&system_prompt).await { Ok(response) => Ok(response), Err(err) => Err(err.to_string()), }, @@ -489,11 +656,22 @@ async fn generate_ai_title() -> Result { async fn generate_ai_description() -> Result { let mut client = NanoAI::new(); + let system_prompt = format!( + " + **System Prompt (SP):** You are an expert in content generation for web metadata and SEO optimization. + + **Prompt (P):** Generate a unique and concise description for an OG (Open Graph) metadata tag. + The description should provide a brief, engaging summary of the associated content, optimized for search engines and user engagement. + + **Expected Format (EF):** + - A short sentence (maximum 60 characters) that is compelling, informative, and aligned with modern web best practices. + + **Roleplay (RP):** Act as an experienced SEO copywriter crafting metadata descriptions for websites. + " + ); + match client.create_session(None).await { - Ok(_) => match client - .send_prompt("Generate a unique short description text for an OG metadata tag") - .await - { + Ok(_) => match client.send_prompt(&system_prompt).await { Ok(response) => Ok(response), Err(err) => Err(err.to_string()), }, diff --git a/src/components/dashboard/ogs/list.rs b/src/components/dashboard/ogs/list.rs index a41f473..70ccb92 100644 --- a/src/components/dashboard/ogs/list.rs +++ b/src/components/dashboard/ogs/list.rs @@ -5,7 +5,6 @@ use crate::server::og::controller::get_ogs_for_user; use crate::server::og::model::OG; use crate::server::og::request::GetOGsForUserRequest; use crate::theme::Theme; -use crate::theme::THEME; use chrono::Utc; use dioxus::prelude::*; use gloo_storage::{LocalStorage, Storage}; @@ -22,7 +21,8 @@ pub const CACHE_TIMEOUT: i64 = 2 * 60 * 60; #[component] pub fn OGsPanel(user_token: Signal) -> Element { - let dark_mode = *THEME.read() == Theme::Dark; + let theme = use_context::>(); + let dark_mode = theme() == Theme::Dark; let mut ogs = use_signal(Vec::new); let mut displayed_ogs = use_signal(Vec::new); let mut loading = use_signal(|| true); diff --git a/src/components/dashboard/ogs/read.rs b/src/components/dashboard/ogs/read.rs index eea6ed9..f0c3764 100644 --- a/src/components/dashboard/ogs/read.rs +++ b/src/components/dashboard/ogs/read.rs @@ -4,12 +4,12 @@ use crate::server::og::controller::get_og_for_user; use crate::server::og::model::OG; use crate::server::og::request::GetOGForUserRequest; use crate::theme::Theme; -use crate::theme::THEME; use dioxus::prelude::*; #[component] pub fn ViewOGPanel(og_id: String, user_token: Signal) -> Element { - let dark_mode = *THEME.read() == Theme::Dark; + let theme = use_context::>(); + let dark_mode = theme() == Theme::Dark; let mut selected_og = use_signal(|| None::); let mut loading = use_signal(|| true); diff --git a/src/components/dashboard/profile.rs b/src/components/dashboard/profile.rs index 93ebfc1..74a5222 100644 --- a/src/components/dashboard/profile.rs +++ b/src/components/dashboard/profile.rs @@ -6,7 +6,6 @@ use crate::components::dashboard::profile::view::ProfileDetails; use crate::server::auth::controller::about_me; use crate::server::auth::model::User; use crate::theme::Theme; -use crate::theme::THEME; use dioxus::prelude::*; use gloo_storage::SessionStorage; @@ -14,7 +13,8 @@ use gloo_storage::Storage; #[component] pub fn ProfilePagePanel() -> Element { - let dark_mode = *THEME.read() == Theme::Dark; + let theme = use_context::>(); + let dark_mode = theme() == Theme::Dark; let mut user_token = use_signal(|| "".to_string()); let mut user_data = use_signal(|| None::); let mut edit_mode = use_signal(|| false); diff --git a/src/components/dashboard/sidebar.rs b/src/components/dashboard/sidebar.rs index 6d78e28..50de1a8 100644 --- a/src/components/dashboard/sidebar.rs +++ b/src/components/dashboard/sidebar.rs @@ -1,7 +1,6 @@ use crate::components::common::logo::Logo; use crate::router::Route; use crate::theme::Theme; -use crate::theme::THEME; use dioxus::prelude::*; #[derive(PartialEq, Clone)] @@ -15,7 +14,8 @@ pub enum Tab { #[component] pub fn Sidebar(active_tab: Signal, navigate: bool) -> Element { - let dark_mode = *THEME.read() == Theme::Dark; + let theme = use_context::>(); + let dark_mode = theme() == Theme::Dark; let navigator = use_navigator(); let tab_style = |tab: Tab| -> String { @@ -53,8 +53,8 @@ pub fn Sidebar(active_tab: Signal, navigate: bool) -> Element { active_tab.set(Tab::OGs); }, i { - width: 100, - height: 100, + width: 30, + height: 30, class: "fa-solid fa-folder-open", }, span { class: "hidden md:inline", "OGs" } @@ -68,8 +68,8 @@ pub fn Sidebar(active_tab: Signal, navigate: bool) -> Element { active_tab.set(Tab::Chat); }, i { - width: 100, - height: 100, + width: 30, + height: 30, class: "fa-solid fa-message", }, span { class: "hidden md:inline", "Chat" } @@ -83,8 +83,8 @@ pub fn Sidebar(active_tab: Signal, navigate: bool) -> Element { active_tab.set(Tab::CreateOG); }, i { - width: 100, - height: 100, + width: 30, + height: 30, class: "fa-solid fa-file-lines", }, span { class: "hidden md:inline", "Generate" } @@ -92,8 +92,8 @@ pub fn Sidebar(active_tab: Signal, navigate: bool) -> Element { div { class: tab_style(Tab::ViewOG), onclick: move |_| active_tab.set(Tab::ViewOG), i { - width: 100, - height: 100, + width: 30, + height: 30, class: "fa-solid fa-address-book", }, span { class: "hidden md:inline", "View OG" } @@ -106,8 +106,8 @@ pub fn Sidebar(active_tab: Signal, navigate: bool) -> Element { active_tab.set(Tab::EditProfile); }, i { - width: 100, - height: 100, + width: 30, + height: 30, class: "fa-regular fa-pen-to-square", }, span { class: "hidden md:inline", "Profile" } diff --git a/src/components/features.rs b/src/components/features.rs index b398410..c324f98 100644 --- a/src/components/features.rs +++ b/src/components/features.rs @@ -3,7 +3,6 @@ pub(crate) mod item; use crate::components::features::grid::Grid; use crate::theme::Theme; -use crate::theme::THEME; use dioxus::prelude::*; #[derive(Props, Clone, PartialEq)] @@ -15,7 +14,7 @@ struct Feature { #[component] pub fn Features() -> Element { - let dark_mode = *THEME.read(); + let dark_mode = use_context::>(); let features = vec![ Feature { @@ -55,7 +54,7 @@ pub fn Features() -> Element { id: "features", class: format!( "relative min-h-screen flex flex-col items-center justify-center px-6 overflow-hidden transition-colors duration-300 {}", - if dark_mode == Theme::Dark { "bg-gray-900 text-white" } else { "bg-gray-50 text-gray-900" } + if dark_mode() == Theme::Dark { "bg-gray-900 text-white" } else { "bg-gray-50 text-gray-900" } ), div { @@ -74,14 +73,14 @@ pub fn Features() -> Element { h2 { class: format!( "text-3xl md:text-5xl font-extrabold tracking-tight {}", - if dark_mode == Theme::Dark { "text-white" } else { "text-gray-900" } + if dark_mode() == Theme::Dark { "text-white" } else { "text-gray-900" } ), "Why Choose OG Nano?" } p { class: format!( "text-lg md:text-xl {}", - if dark_mode == Theme::Dark { "text-gray-300" } else { "text-gray-700" } + if dark_mode() == Theme::Dark { "text-gray-300" } else { "text-gray-700" } ), "Leverage the power of AI to craft stunning OG images for your websites in record time." } diff --git a/src/components/features/item.rs b/src/components/features/item.rs index 82431a2..70b8810 100644 --- a/src/components/features/item.rs +++ b/src/components/features/item.rs @@ -1,5 +1,4 @@ use crate::theme::Theme; -use crate::theme::THEME; use dioxus::prelude::*; #[derive(Props, PartialEq, Clone)] @@ -11,13 +10,13 @@ pub struct ItemProps { #[component] pub fn FeatureItem(props: ItemProps) -> Element { - let dark_mode = *THEME.read(); + let dark_mode = use_context::>(); rsx! { div { class: format!( "flex flex-col items-center p-6 rounded-lg border transition-transform duration-300 shadow-lg {} {}", - if dark_mode == Theme::Dark { "bg-gray-800 hover:bg-gray-700 border-gray-700" } + if dark_mode() == Theme::Dark { "bg-gray-800 hover:bg-gray-700 border-gray-700" } else { "bg-white hover:bg-gray-100 border-gray-300" }, "transform hover:-translate-y-2 hover:scale-105" ), @@ -28,14 +27,14 @@ pub fn FeatureItem(props: ItemProps) -> Element { h3 { class: format!( "text-lg font-semibold {}", - if dark_mode == Theme::Dark { "text-white" } else { "text-gray-800" } + if dark_mode() == Theme::Dark { "text-white" } else { "text-gray-800" } ), "{props.title}" } p { class: format!( "text-sm text-center mt-2 {}", - if dark_mode == Theme::Dark { "text-gray-400" } else { "text-gray-600" } + if dark_mode() == Theme::Dark { "text-gray-400" } else { "text-gray-600" } ), "{props.description}" } diff --git a/src/components/hero.rs b/src/components/hero.rs index 83bac85..b1b20a3 100644 --- a/src/components/hero.rs +++ b/src/components/hero.rs @@ -1,17 +1,16 @@ use crate::router::Route; use crate::theme::Theme; -use crate::theme::THEME; use dioxus::prelude::*; #[component] pub fn Hero() -> Element { - let dark_mode = *THEME.read(); + let dark_mode = use_context::>(); rsx! { section { class: format!( "relative min-h-screen flex flex-col items-center justify-center px-6 overflow-hidden transition-colors duration-300 {}", - if dark_mode == Theme::Dark { "bg-gray-900 text-white" } else { "bg-gray-50 text-gray-900" } + if dark_mode() == Theme::Dark { "bg-gray-900 text-white" } else { "bg-gray-50 text-gray-900" } ), div { class: "absolute inset-0 bg-gradient-to-bl from-indigo-500 via-purple-500 to-transparent opacity-30 pointer-events-none" @@ -31,7 +30,7 @@ pub fn Hero() -> Element { p { class: format!( "text-lg md:text-2xl leading-relaxed max-w-3xl mx-auto animate-fade-in delay-150 {}", - if dark_mode == Theme::Dark { "text-gray-300" } else { "text-gray-400" } + if dark_mode() == Theme::Dark { "text-gray-300" } else { "text-gray-400" } ), "Unleash the power of Gemini Nano AI to craft visually stunning OG images that make your website stand out. It's fast, intuitive, and cutting-edge." }, @@ -46,7 +45,7 @@ pub fn Hero() -> Element { to: Route::Login {}, class: format!( "py-3 px-8 rounded-full shadow-lg focus:outline-none transform hover:scale-105 transition-transform duration-150 font-semibold {}", - if dark_mode == Theme::Dark { + if dark_mode() == Theme::Dark { "bg-gray-800 text-gray-100 hover:bg-gray-700" } else { "bg-gray-100 text-gray-800 hover:bg-gray-200" @@ -71,7 +70,7 @@ pub fn Hero() -> Element { div { class: format!( "absolute bottom-0 w-full h-64 bg-gradient-to-t from-gray-900 to-transparent opacity-80 pointer-events-none {}", - if dark_mode == Theme::Dark { "opacity-60" } else { "" } + if dark_mode() == Theme::Dark { "opacity-60" } else { "" } ) } } diff --git a/src/components/navbar.rs b/src/components/navbar.rs index f29d578..e6a0f27 100644 --- a/src/components/navbar.rs +++ b/src/components/navbar.rs @@ -7,12 +7,11 @@ use crate::components::navbar::links::NavLinks; use crate::router::Route; use crate::theme::Theme; use crate::theme::ThemeToggle; -use crate::theme::THEME; use dioxus::prelude::*; #[component] fn NavBar(show_items: bool) -> Element { - let dark_mode = *THEME.read(); + let dark_mode = use_context::>(); let mut is_menu_open = use_signal(|| false); let toggle_menu = move |_| { @@ -31,7 +30,7 @@ fn NavBar(show_items: bool) -> Element { class: format!( "items-center justify-between px-8 py-4 shadow-md hidden md:flex rounded-lg {}", - if dark_mode == Theme::Dark { "bg-white text-black" } else { "bg-gray-900 text-white" } + if dark_mode() == Theme::Dark { "bg-white text-black" } else { "bg-gray-900 text-white" } ), NavLinks {show_items}, AuthButtons { is_vertical: false } @@ -42,7 +41,7 @@ fn NavBar(show_items: bool) -> Element { button { class: format!("text-3xl md:hidden transform duration-300 {} {}", if is_menu_open() { "rotate-90" } else { "rotate-0" }, - if dark_mode == Theme::Dark { "text-white" } else { "text-black" }, + if dark_mode() == Theme::Dark { "text-white" } else { "text-black" }, ), onclick: toggle_menu, @@ -53,7 +52,7 @@ fn NavBar(show_items: bool) -> Element { class: format!( "fixed top-0 left-0 w-2/5 md:w-auto h-auto p-4 z-50 md:hidden transition-transform transform duration-500 ease-in-out {} {}", if is_menu_open() { "translate-x-0 opacity-100" } else { "-translate-x-full opacity-0" }, - if dark_mode == Theme::Dark { "bg-gray-900 text-white" } else { "bg-white text-black" } + if dark_mode() == Theme::Dark { "bg-gray-900 text-white" } else { "bg-white text-black" } ), NavLinks {show_items} AuthButtons { is_vertical: true } diff --git a/src/components/navbar/btns.rs b/src/components/navbar/btns.rs index a9c5809..a5d9e96 100644 --- a/src/components/navbar/btns.rs +++ b/src/components/navbar/btns.rs @@ -1,6 +1,5 @@ use crate::router::Route; use crate::theme::Theme; -use crate::theme::THEME; use dioxus::prelude::*; #[derive(Props, Clone, PartialEq)] @@ -10,7 +9,7 @@ pub struct AuthButtonsProps { #[component] pub fn AuthButtons(props: AuthButtonsProps) -> Element { - let dark_mode = *THEME.read(); + let dark_mode = use_context::>(); let button_class = if props.is_vertical { "flex flex-col gap-4" } else { @@ -24,7 +23,7 @@ pub fn AuthButtons(props: AuthButtonsProps) -> Element { class: format!( "border px-5 py-2 text-lg hover:bg-gray-100 {}", - if dark_mode == Theme::Dark { "border-gray-700" } else { "border-gray-300" } + if dark_mode() == Theme::Dark { "border-gray-700" } else { "border-gray-300" } ), "Register" } diff --git a/src/components/testimonial.rs b/src/components/testimonial.rs index 38f9e95..641af3b 100644 --- a/src/components/testimonial.rs +++ b/src/components/testimonial.rs @@ -5,7 +5,6 @@ pub(crate) mod rating; use crate::components::testimonial::author::AuthorInfo; use crate::components::testimonial::rating::StarRating; use crate::theme::Theme; -use crate::theme::THEME; use dioxus::prelude::*; #[derive(Props, Clone, PartialEq)] @@ -29,8 +28,8 @@ pub fn Testimonial() -> Element { author_image: asset!("/assets/jeff.webp"), company_logo: asset!("/assets/amazon.webp"), star_images: vec![rsx! {i { - width: 100, - height: 100, + width: 30, + height: 30, class: "fa-solid fa-star", }}; 5], }, @@ -41,8 +40,8 @@ pub fn Testimonial() -> Element { author_image: asset!("/assets/zuck.webp"), company_logo: asset!("/assets/meta.webp"), star_images: vec![rsx! {i { - width: 100, - height: 100, + width: 30, + height: 30, class: "fa-solid fa-star", }}; 5], }, @@ -53,14 +52,14 @@ pub fn Testimonial() -> Element { author_image: asset!("/assets/elon.webp"), company_logo: asset!("/assets/spacex.webp"), star_images: vec![rsx! {i { - width: 100, - height: 100, + width: 30, + height: 30, class: "fa-solid fa-star", }}; 5], }, ]; - let dark_mode = *THEME.read(); + let dark_mode = use_context::>(); let mut current_index = use_signal(|| 0); client! { @@ -87,14 +86,14 @@ pub fn Testimonial() -> Element { section { id: "testimonial", class: format!("flex flex-col items-center justify-center min-h-screen p-8 {}", - if dark_mode == Theme::Dark { "bg-gray-900 text-white" } else { "bg-white text-black" }), + if dark_mode() == Theme::Dark { "bg-gray-900 text-white" } else { "bg-white text-black" }), div { class: "flex flex-col items-center mb-8", h2 { class: "text-4xl font-bold text-center", "What People Are Saying about Nano OG" } - p { class: format!("mt-2 text-lg {}", if dark_mode == Theme::Dark { "text-gray-300" } else { "text-gray-700" }), + p { class: format!("mt-2 text-lg {}", if dark_mode() == Theme::Dark { "text-gray-300" } else { "text-gray-700" }), "Nano OG: Where AI takes your website (and your imagination) on a wild ride." } } @@ -104,11 +103,11 @@ pub fn Testimonial() -> Element { div { class: format!("transition-transform duration-500 transform {}, hover:scale-105 hover:shadow-xl", if current_index() == i { "opacity-100 scale-100" } else { "opacity-50 scale-75 blur-sm" }), div { class: format!("{} p-8 rounded-xl shadow-2xl text-center max-w-sm border", - if dark_mode == Theme::Dark { "border-gray-700 bg-gray-800" } else { "bg-white border-gray-300" }), + if dark_mode() == Theme::Dark { "border-gray-700 bg-gray-800" } else { "bg-white border-gray-300" }), StarRating { star_images: testimonial.star_images.clone() } blockquote { class: format!("text-lg font-semibold italic {}", - if dark_mode == Theme::Dark { "text-gray-400" } else { "text-gray-600" } + if dark_mode() == Theme::Dark { "text-gray-400" } else { "text-gray-600" } ), "{testimonial.quote}" } diff --git a/src/main.rs b/src/main.rs index f734c20..ec1c39f 100644 --- a/src/main.rs +++ b/src/main.rs @@ -4,6 +4,7 @@ use dioxus::prelude::*; use dioxus_logger::tracing; use nano_og::components::toast::provider::ToastProvider; use nano_og::router::Route; +use nano_og::theme::ThemeProvider; fn main() { #[cfg(feature = "web")] @@ -59,8 +60,10 @@ fn main() { fn App() -> Element { rsx! { + ThemeProvider { ToastProvider { Router:: {} + } } } } diff --git a/src/pages/dashboard.rs b/src/pages/dashboard.rs index 547a489..689ea12 100644 --- a/src/pages/dashboard.rs +++ b/src/pages/dashboard.rs @@ -7,24 +7,16 @@ use crate::components::dashboard::profile::ProfilePagePanel; use crate::components::dashboard::sidebar::Sidebar; use crate::components::dashboard::sidebar::Tab; use crate::server::auth::controller::about_me; -use crate::theme::{Theme, THEME}; +use crate::theme::Theme; use dioxus::prelude::*; use gloo_storage::SessionStorage; use gloo_storage::Storage; -pub fn toggle_theme() { - let current_theme = *THEME.read(); - let new_theme = match current_theme { - Theme::Light => Theme::Dark, - Theme::Dark => Theme::Light, - }; - *THEME.write() = new_theme; -} - #[component] pub fn Dashboard() -> Element { let active_tab = use_signal(|| Tab::OGs); - let dark_mode = *THEME.read() == Theme::Dark; + let theme = use_context::>(); + let dark_mode = theme() == Theme::Dark; let mut user_token = use_signal(|| "".to_string()); let navigator = use_navigator(); let current_tab = match active_tab() { @@ -59,7 +51,7 @@ pub fn Dashboard() -> Element { Sidebar { navigate: false, active_tab: active_tab.clone() } div { class: "flex-1 p-4 md:p-8", - Navbar { dark_mode } + Navbar { } div { class: format!("p-4 shadow rounded-lg {}", if dark_mode { "bg-gray-800" } else { "bg-white" }), {current_tab} diff --git a/src/pages/login.rs b/src/pages/login.rs index 8fa1f15..19baea6 100644 --- a/src/pages/login.rs +++ b/src/pages/login.rs @@ -6,7 +6,6 @@ use crate::router::Route; use crate::server::auth::controller::{about_me, login_user}; use crate::server::auth::response::LoginUserSchema; use crate::theme::Theme; -use crate::theme::THEME; use chrono::Duration; use dioxus::prelude::*; use gloo_storage::SessionStorage; @@ -21,7 +20,7 @@ fn extract_token(cookie_str: &str) -> Option { #[component] pub fn Login() -> Element { let navigator = use_navigator(); - let dark_mode = *THEME.read(); + let dark_mode = use_context::>(); let mut toasts_manager = use_context::>(); let mut email = use_signal(|| "".to_string()); @@ -157,11 +156,11 @@ pub fn Login() -> Element { rsx! { div { class: format!("min-h-screen flex items-center justify-center {}", - if dark_mode == Theme::Dark { "bg-blue-500 text-white" } else { "bg-blue-900 text-gray-900" } + if dark_mode() == Theme::Dark { "bg-blue-500 text-white" } else { "bg-blue-900 text-gray-900" } ), style: "background-image: linear-gradient(90deg, rgba(0,0,0,0.05) 1px, transparent 1px), linear-gradient(rgba(0,0,0,0.05) 1px, transparent 1px); background-size: 40px 40px;", form { - style: if dark_mode == Theme::Dark { "background-color: #1f2937; color: white;" } else { "background-color: white; color: black;" }, + style: if dark_mode() == Theme::Dark { "background-color: #1f2937; color: white;" } else { "background-color: white; color: black;" }, class: "w-full max-w-md flex flex-col items-center p-6 bg-white shadow-lg rounded-lg transform transition-all duration-300 hover:shadow-2xl", onsubmit: handle_login, Link { @@ -196,7 +195,7 @@ pub fn Login() -> Element { input { class: format!( "w-full p-3 border rounded-md shadow-sm transition-all {} {}", - if dark_mode == Theme::Dark { "bg-gray-700 text-white" } else { "bg-white text-gray-900" }, + if dark_mode() == Theme::Dark { "bg-gray-700 text-white" } else { "bg-white text-gray-900" }, if email_valid() { "border-gray-300" } else { "border-red-500" } ), r#type: "text", @@ -219,7 +218,7 @@ pub fn Login() -> Element { input { class: format!( "w-full p-3 border rounded-md shadow-sm transition-all {} {}", - if dark_mode == Theme::Dark { "bg-gray-700 text-white" } else { "bg-white text-gray-900" }, + if dark_mode() == Theme::Dark { "bg-gray-700 text-white" } else { "bg-white text-gray-900" }, if password_valid() { "border-gray-300" } else { "border-red-500" } ), r#type: if show_password() { "text" } else { "password" }, diff --git a/src/pages/og.rs b/src/pages/og.rs index 9388fc1..0b1946f 100644 --- a/src/pages/og.rs +++ b/src/pages/og.rs @@ -10,7 +10,6 @@ use crate::components::dashboard::sidebar::Sidebar; use crate::components::dashboard::sidebar::Tab; use crate::server::auth::controller::about_me; use crate::theme::Theme; -use crate::theme::THEME; use bson::oid::ObjectId; use dioxus::prelude::*; use gloo_storage::SessionStorage; @@ -19,7 +18,8 @@ use gloo_storage::Storage; #[component] pub fn ViewOG(id: String) -> Element { let active_tab = use_signal(|| Tab::ViewOG); - let dark_mode = *THEME.read() == Theme::Dark; + let theme = use_context::>(); + let dark_mode = theme() == Theme::Dark; let mut user_token = use_signal(|| "".to_string()); let navigator = use_navigator(); let mut current_tab = rsx! { OGsPanel { user_token } }; @@ -59,7 +59,7 @@ pub fn ViewOG(id: String) -> Element { Sidebar { navigate: true, active_tab: active_tab.clone() } div { class: "flex-1 p-4 md:p-8", - Navbar { dark_mode } + Navbar { } div { class: format!("p-4 shadow rounded-lg {}", if dark_mode { "bg-gray-800" } else { "bg-white" }), {current_tab} @@ -72,7 +72,8 @@ pub fn ViewOG(id: String) -> Element { #[component] pub fn EditOG(id: String) -> Element { let active_tab = use_signal(|| Tab::ViewOG); - let dark_mode = *THEME.read() == Theme::Dark; + let theme = use_context::>(); + let dark_mode = theme() == Theme::Dark; let mut user_token = use_signal(|| "".to_string()); let navigator = use_navigator(); let mut current_tab = rsx! { OGsPanel { user_token } }; @@ -110,7 +111,7 @@ pub fn EditOG(id: String) -> Element { Sidebar { navigate: true, active_tab: active_tab.clone() } div { class: "flex-1 p-4 md:p-8", - Navbar { dark_mode } + Navbar { } div { class: format!("p-4 shadow rounded-lg {}", if dark_mode { "bg-gray-800" } else { "bg-white" }), {current_tab} diff --git a/src/pages/signup.rs b/src/pages/signup.rs index 224c7dc..24316aa 100644 --- a/src/pages/signup.rs +++ b/src/pages/signup.rs @@ -7,7 +7,6 @@ use crate::router::Route; use crate::server::auth::controller::{about_me, register_user}; use crate::server::auth::response::RegisterUserSchema; use crate::theme::Theme; -use crate::theme::THEME; use chrono::Duration; use dioxus::prelude::*; use gloo_storage::SessionStorage; @@ -17,7 +16,7 @@ use regex::Regex; #[component] pub fn Register() -> Element { let navigator = use_navigator(); - let dark_mode = *THEME.read(); + let dark_mode = use_context::>(); let mut name = use_signal(|| "".to_string()); let mut email = use_signal(|| "".to_string()); @@ -131,11 +130,11 @@ pub fn Register() -> Element { rsx! { div { class: format!("min-h-screen flex items-center justify-center {}", - if dark_mode == Theme::Dark { "bg-blue-500 text-white" } else { "bg-blue-900 text-gray-900" } + if dark_mode() == Theme::Dark { "bg-blue-500 text-white" } else { "bg-blue-900 text-gray-900" } ), style: "background-image: linear-gradient(90deg, rgba(0,0,0,0.05) 1px, transparent 1px), linear-gradient(rgba(0,0,0,0.05) 1px, transparent 1px); background-size: 40px 40px;", form { - style: if dark_mode == Theme::Dark { "background-color: #1f2937; color: white;" } else { "background-color: white; color: black;" }, + style: if dark_mode() == Theme::Dark { "background-color: #1f2937; color: white;" } else { "background-color: white; color: black;" }, class: "w-full max-w-md flex flex-col items-center p-6 bg-white shadow-lg rounded-lg transform transition-all duration-300 hover:shadow-2xl", onsubmit: handle_register, Link { @@ -169,7 +168,7 @@ pub fn Register() -> Element { input { class: format!( "w-full p-3 border rounded-md shadow-sm transition-all {} {}", - if dark_mode == Theme::Dark { "bg-gray-700 text-white" } else { "bg-white text-gray-900" }, + if dark_mode() == Theme::Dark { "bg-gray-700 text-white" } else { "bg-white text-gray-900" }, if name_valid() { "border-gray-300" } else { "border-red-500" } ), r#type: "text", @@ -191,7 +190,7 @@ pub fn Register() -> Element { input { class: format!( "w-full p-3 border rounded-md shadow-sm transition-all {} {}", - if dark_mode == Theme::Dark { "bg-gray-700 text-white" } else { "bg-white text-gray-900" }, + if dark_mode() == Theme::Dark { "bg-gray-700 text-white" } else { "bg-white text-gray-900" }, if email_valid() { "border-gray-300" } else { "border-red-500" } ), r#type: "text", @@ -213,7 +212,7 @@ pub fn Register() -> Element { input { class: format!( "w-full p-3 border rounded-md shadow-sm transition-all {} {}", - if dark_mode == Theme::Dark { "bg-gray-700 text-white" } else { "bg-white text-gray-900" }, + if dark_mode() == Theme::Dark { "bg-gray-700 text-white" } else { "bg-white text-gray-900" }, if password_valid() { "border-gray-300" } else { "border-red-500" } ), r#type: if show_password() { "text" } else { "password" }, @@ -249,7 +248,7 @@ pub fn Register() -> Element { Spinner { aria_label: "Loading spinner".to_string(), size: SpinnerSize::Md, - dark_mode: dark_mode == Theme::Dark, + dark_mode: dark_mode() == Theme::Dark, } span { "Signing Up..." } } else { diff --git a/src/theme.rs b/src/theme.rs index 18201a7..ee4c7f7 100644 --- a/src/theme.rs +++ b/src/theme.rs @@ -1,20 +1,31 @@ use dioxus::prelude::*; -#[derive(PartialEq, Clone, Copy, Debug)] +#[derive(PartialEq, Clone, Copy, Default, Debug)] pub enum Theme { - Light, + #[default] Dark, + Light, } -pub static THEME: GlobalSignal = GlobalSignal::new(|| Theme::Dark); +#[derive(Props, PartialEq, Clone)] +pub struct ThemeProviderProps { + pub children: Element, +} #[component] -pub fn ThemeToggle() -> Element { - use_effect(|| { - *THEME.write() = Theme::Dark; - }); +pub fn ThemeProvider(props: ThemeProviderProps) -> Element { + let theme = use_signal(|| Theme::default()); + + use_context_provider(|| theme); - let mut theme = use_signal(|| Theme::Dark); + rsx! { + {props.children} + } +} + +#[component] +pub fn ThemeToggle() -> Element { + let mut theme = use_context::>(); let toggle_theme = move |_| { let new_theme = if theme() == Theme::Light { @@ -23,7 +34,6 @@ pub fn ThemeToggle() -> Element { Theme::Light }; theme.set(new_theme); - *THEME.write() = new_theme; }; rsx! {