Skip to content

Commit 8dfcc4f

Browse files
committed
refactor(panel): make the 'save configuration' button float
1 parent 8f30187 commit 8dfcc4f

3 files changed

Lines changed: 102 additions & 20 deletions

File tree

rustmail_panel/src/components/configuration.rs

Lines changed: 96 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,8 @@ pub fn configuration_page() -> Html {
1818
let show_restart_modal = use_state(|| false);
1919
let save_message = use_state(|| None::<(bool, String)>);
2020

21-
let expanded_sections = use_state(|| vec![true, false, false, false, false, false, false, false]);
21+
let expanded_sections =
22+
use_state(|| vec![true, false, false, false, false, false, false, false]);
2223

2324
let permissions = use_state(|| None::<Vec<PanelPermission>>);
2425
{
@@ -120,21 +121,35 @@ pub fn configuration_page() -> Html {
120121
let show_restart_modal = show_restart_modal.clone();
121122
let save_message = save_message.clone();
122123
let i18n = i18n.clone();
124+
let config = config.clone();
123125

124126
Callback::from(move |new_config: ConfigResponse| {
125127
let show_restart_modal = show_restart_modal.clone();
126128
let save_message = save_message.clone();
127129
let i18n = i18n.clone();
130+
let config = config.clone();
128131

129132
spawn_local(async move {
130133
match Request::put("/api/bot/config").json(&new_config) {
131134
Ok(req) => match req.send().await {
132135
Ok(resp) => {
133136
if resp.ok() {
134-
save_message.set(Some((true, i18n.t("panel.configuration.save_success"))));
137+
save_message
138+
.set(Some((true, i18n.t("panel.configuration.save_success"))));
135139
show_restart_modal.set(true);
140+
if let Ok(resp) = Request::get("/api/bot/config").send().await {
141+
if resp.ok() {
142+
if let Ok(config_data) = resp.json::<ConfigResponse>().await
143+
{
144+
config.set(Some(config_data));
145+
}
146+
}
147+
}
136148
} else {
137-
let error_msg = resp.text().await.unwrap_or_else(|_| "Erreur inconnue".to_string());
149+
let error_msg = resp
150+
.text()
151+
.await
152+
.unwrap_or_else(|_| "Erreur inconnue".to_string());
138153
save_message.set(Some((false, error_msg)));
139154
}
140155
}
@@ -334,8 +349,24 @@ struct ConfigFormProps {
334349
fn config_form(props: &ConfigFormProps) -> Html {
335350
let (i18n, _set_language) = use_translation();
336351
let config = use_state(|| props.config.clone());
352+
let is_saving = use_state(|| false);
353+
354+
let has_changes = *config != props.config;
355+
let original_config = props.config.clone();
356+
357+
{
358+
let is_saving = is_saving.clone();
359+
let config_matches = *config == props.config;
360+
use_effect_with(config_matches, move |matches| {
361+
if *matches {
362+
is_saving.set(false);
363+
}
364+
|| ()
365+
});
366+
}
337367

338368
html! {
369+
<>
339370
<div class="space-y-4">
340371
<AccordionSection
341372
title={i18n.t("panel.configuration.sections.bot")}
@@ -424,24 +455,66 @@ fn config_form(props: &ConfigFormProps) -> Html {
424455
>
425456
<LogsReminderSection config={config.clone()} />
426457
</AccordionSection>
458+
</div>
427459

428-
<div class="pt-4 border-t border-slate-600">
429-
<button
430-
onclick={{
431-
let config = (*config).clone();
432-
let on_save = props.on_save.clone();
433-
move |_| on_save.emit(config.clone())
434-
}}
435-
class="w-full px-4 py-3 bg-blue-600 hover:bg-blue-700 text-white rounded-md transition font-semibold flex items-center justify-center gap-2"
460+
{if has_changes && !*is_saving {
461+
html! {
462+
<div
463+
class="fixed bottom-8 left-1/2 -translate-x-1/2 z-50 transition-all duration-300 ease-out"
464+
style="animation: slideUp 0.3s ease-out;"
436465
>
437-
<i class="bi bi-save"></i>
438-
{i18n.t("panel.configuration.save")}
439-
</button>
440-
<p class="mt-2 text-xs text-gray-500 text-center">
441-
{i18n.t("panel.configuration.save_help")}
442-
</p>
443-
</div>
444-
</div>
466+
<style>
467+
{"
468+
@keyframes slideUp {
469+
from {
470+
opacity: 0;
471+
transform: translateX(-50%) translateY(20px);
472+
}
473+
to {
474+
opacity: 1;
475+
transform: translateX(-50%) translateY(0);
476+
}
477+
}
478+
"}
479+
</style>
480+
<div class="flex items-center gap-3 px-6 py-3 bg-blue-600 hover:bg-blue-700 rounded-full shadow-2xl transition-all">
481+
<button
482+
onclick={{
483+
let config = config.clone();
484+
let original_config = original_config.clone();
485+
Callback::from(move |_| {
486+
config.set(original_config.clone());
487+
})
488+
}}
489+
class="text-white hover:text-gray-200 transition text-sm font-medium"
490+
>
491+
{i18n.t("panel.configuration.reset")}
492+
</button>
493+
<div class="w-px h-6 bg-white/30"></div>
494+
<button
495+
onclick={{
496+
let config = (*config).clone();
497+
let on_save = props.on_save.clone();
498+
let is_saving = is_saving.clone();
499+
move |_| {
500+
is_saving.set(true);
501+
on_save.emit(config.clone());
502+
}
503+
}}
504+
class="text-white font-semibold text-sm flex items-center gap-2"
505+
>
506+
{i18n.t("panel.configuration.unsaved_changes")}
507+
<span class="px-3 py-1 bg-white/20 hover:bg-white/30 rounded-full transition">
508+
{i18n.t("panel.configuration.save_button")}
509+
</span>
510+
</button>
511+
</div>
512+
</div>
513+
}
514+
} else {
515+
html! {}
516+
}}
517+
</>
445518
}
446519
}
447520

@@ -502,7 +575,10 @@ struct TextInputProps {
502575

503576
#[function_component(TextInput)]
504577
fn text_input(props: &TextInputProps) -> Html {
505-
let input_type = props.input_type.clone().unwrap_or_else(|| "text".to_string());
578+
let input_type = props
579+
.input_type
580+
.clone()
581+
.unwrap_or_else(|| "text".to_string());
506582
let placeholder = props.placeholder.clone().unwrap_or_default();
507583

508584
html! {

rustmail_panel/src/i18n/en/en.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,9 @@
121121
"save": "Save Configuration",
122122
"save_help": "A backup will be automatically created before saving",
123123
"save_success": "Configuration saved!",
124+
"reset": "Reset",
125+
"unsaved_changes": "Unsaved changes",
126+
"save_button": "Save",
124127
"loading": "Loading...",
125128
"load_error": "Unable to load configuration",
126129
"restart_modal": {

rustmail_panel/src/i18n/fr/fr.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,9 @@
121121
"save": "Sauvegarder la Configuration",
122122
"save_help": "Un backup sera automatiquement créé avant la sauvegarde",
123123
"save_success": "Configuration sauvegardée !",
124+
"reset": "Réinitialiser",
125+
"unsaved_changes": "Modifications non sauvegardées",
126+
"save_button": "Sauvegarder",
124127
"loading": "Chargement...",
125128
"load_error": "Impossible de charger la configuration",
126129
"restart_modal": {

0 commit comments

Comments
 (0)