Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions src/presentation/apps/sidebar/App.css
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,12 @@
align-items: center;
}

.sender-actions {
display: flex;
align-items: center;
gap: 6px;
}

#senders {
padding: 10px;
overflow: hidden auto;
Expand Down
214 changes: 197 additions & 17 deletions src/presentation/apps/sidebar/components/actionButton.css
Original file line number Diff line number Diff line change
@@ -1,32 +1,212 @@
.action-button {
position: relative;
border: none;
border-radius: 20px;
border-radius: 12px;
cursor: pointer;
font-size: 14px;
height: 32px;
margin: 4px;
padding: 0 16px;
box-shadow: 0 3px 5px rgb(0 0 0 / 20%);
font-size: 13px;
font-weight: 600;
height: 38px;
margin: 2px;
padding: 0 12px;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
overflow: hidden;
min-width: 90px;
display: flex;
align-items: center;
justify-content: center;
box-shadow:
0 2px 8px rgb(0 0 0 / 15%),
0 1px 3px rgb(0 0 0 / 10%);
}

#unsubscribe-button {
background-color: #233b86;
color: white;
.action-button:hover:not(.disabled) {
transform: translateY(-1px);
box-shadow:
0 4px 12px rgb(0 0 0 / 20%),
0 2px 6px rgb(0 0 0 / 15%);
}

#unsubscribe-button:hover {
background-color: #1a4c9b;
.action-button:active:not(.disabled) {
transform: translateY(0);
transition: transform 0.1s ease;
}

#delete-button {
background-color: #bb1826;
color: white;
.action-button.disabled {
cursor: not-allowed;
opacity: 0.6;
transform: none !important;
}

#delete-button:hover {
background-color: #ca2633;
/* Button States */
.action-button#unsubscribe-button {
background: #000 !important;
color: white !important;
border: 1px solid #000 !important;
}

.action-button#unsubscribe-button:hover:not(.disabled) {
background: #1a1a1a !important;
border: 1px solid #1a1a1a !important;
}

.action-button#unsubscribe-button.success {
background: #000 !important;
border: 1px solid #000 !important;
}

.action-button#unsubscribe-button.error {
background: #333 !important;
border: 1px solid #333 !important;
}

.action-button#delete-button {
background: linear-gradient(135deg, #ff416c 0%, #ff4b2b 100%) !important;
color: white !important;
}

.action-button#delete-button:hover:not(.disabled) {
background: linear-gradient(135deg, #e73c5e 0%, #e7431f 100%) !important;
}

.action-button#delete-button.success {
background: linear-gradient(135deg, #56ab2f 0%, #a8e6cf 100%) !important;
}

.action-button#delete-button.error {
background: linear-gradient(135deg, #8b0000 0%, #a52a2a 100%) !important;
}

/* Loading State */
.action-button.loading {
pointer-events: none;
animation: pulse 2s infinite;
}

.action-button.loading #unsubscribe-button,
.action-button.loading #delete-button {
background: linear-gradient(135deg, #6c757d 0%, #495057 100%);
}

/* Button Content */
.button-content {
display: flex;
align-items: center;
justify-content: center;
position: relative;
z-index: 2;
}

.action-button .i {
margin-right: 5px;
margin-right: 6px;
font-size: 12px;
transition: all 0.3s ease;
}

/* Icon Animations */
.spinner {
animation: spin 1s linear infinite;
}

@keyframes spin {
from {
transform: rotate(0deg);
}

to {
transform: rotate(360deg);
}
}

.success-icon {
animation: success-pop 0.5s cubic-bezier(0.68, -0.55, 0.265, 1.55);
}

@keyframes success-pop {
0% {
transform: scale(0) rotate(0deg);
}

50% {
transform: scale(1.2) rotate(180deg);
}

100% {
transform: scale(1) rotate(360deg);
}
}

.error-icon {
animation: error-shake 0.5s ease-in-out;
}

@keyframes error-shake {
0%,
100% {
transform: translateX(0);
}

25% {
transform: translateX(-3px);
}

75% {
transform: translateX(3px);
}
}

/* Ripple Effect */
.button-ripple {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: radial-gradient(
circle,
rgb(255 255 255 / 30%) 0%,
transparent 70%
);
transform: scale(0);
opacity: 0;
pointer-events: none;
transition: all 0.6s ease;
}

.action-button:active:not(.disabled) .button-ripple {
transform: scale(2);
opacity: 1;
transition: all 0.3s ease;
}

/* Micro-interactions */
.action-button.success .i {
color: #fff;
}

.action-button.error .i {
color: #fff;
}

.action-button:hover:not(.disabled) .i {
transform: scale(1.1);
}

@keyframes pulse {
0% {
box-shadow:
0 2px 8px rgb(0 0 0 / 15%),
0 1px 3px rgb(0 0 0 / 10%);
}

50% {
box-shadow:
0 4px 16px rgb(0 0 0 / 25%),
0 2px 6px rgb(0 0 0 / 20%);
}

100% {
box-shadow:
0 2px 8px rgb(0 0 0 / 15%),
0 1px 3px rgb(0 0 0 / 10%);
}
}
77 changes: 72 additions & 5 deletions src/presentation/apps/sidebar/components/actionButton.tsx
Original file line number Diff line number Diff line change
@@ -1,27 +1,57 @@
import "./actionButton.css";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { faBan, faTrash } from "@fortawesome/free-solid-svg-icons";
import {
faBan,
faTrash,
faSpinner,
faCheck,
faExclamationTriangle,
} from "@fortawesome/free-solid-svg-icons";
import { IconProp } from "@fortawesome/fontawesome-svg-core";
import { useModal } from "../providers/modalContext";
import { useApp } from "../../../providers/app_provider";
import { useState, useEffect } from "react";

type ButtonState = "idle" | "loading" | "success" | "error";

export const ActionButton = ({ id }: { id: string }) => {
const text: string = id == "unsubscribe-button" ? "Unsubscribe" : "Delete";
const icon: IconProp = id == "unsubscribe-button" ? faBan : faTrash;
const { selectedSenders } = useApp();
const { setModal } = useModal();
const [buttonState, setButtonState] = useState<ButtonState>("idle");

const selectedCount = Object.keys(selectedSenders).length;
const isDisabled = selectedCount === 0 || buttonState === "loading";

// Reset button state when selection changes
useEffect(() => {
if (buttonState !== "idle" && buttonState !== "loading") {
const timer = setTimeout(() => setButtonState("idle"), 2000);
return () => clearTimeout(timer);
}
}, [buttonState]);

const handleClick = () => {
if (isDisabled) return;

const selectedSenderKeys: string[] = Object.keys(selectedSenders);
if (selectedSenderKeys.length > 0) {
setButtonState("loading");

// Simulate processing time for demo
setTimeout(() => {
setButtonState("success");
}, 1500);

// open confirmation modal
setModal({
action: id === "unsubscribe-button" ? "unsubscribe" : "delete",
type: "confirm",
extras: {
emailsNum: selectedSenderKeys.reduce(
(sum, key) => sum + selectedSenders[key],
0,
0
),
sendersNum: selectedSenderKeys.length,
},
Expand All @@ -32,15 +62,52 @@ export const ActionButton = ({ id }: { id: string }) => {
}
};

const getButtonContent = () => {
switch (buttonState) {
case "loading":
return (
<>
<FontAwesomeIcon icon={faSpinner} className="i spinner" />
Processing...
</>
);
case "success":
return (
<>
<FontAwesomeIcon icon={faCheck} className="i success-icon" />
{id === "unsubscribe-button" ? "Unsubscribed!" : "Deleted!"}
</>
);
case "error":
return (
<>
<FontAwesomeIcon
icon={faExclamationTriangle}
className="i error-icon"
/>
Failed
</>
);
default:
return (
<>
<FontAwesomeIcon icon={icon} className="i" />
{selectedCount > 0 ? `${text} (${selectedCount})` : text}
</>
);
}
};

return (
<button
id={id}
className="action-button"
className={`action-button ${buttonState} ${isDisabled ? "disabled" : ""}`}
aria-label={text}
onClick={handleClick}
disabled={isDisabled}
>
<FontAwesomeIcon icon={icon} className="i" />
{text}
<span className="button-content">{getButtonContent()}</span>
<div className="button-ripple"></div>
</button>
);
};
Loading