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
74 changes: 74 additions & 0 deletions css/generic-snackbar.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
/* Basic Styling for GenericSnackbar */
.generic-snackbar {
background-color: #323232; /* Standard snackbar background color */
color: #ffffff; /* Standard text color */
padding: 14px 24px;
border-radius: 4px;
box-shadow: 0 3px 5px -1px rgba(0,0,0,0.2),
0 6px 10px 0 rgba(0,0,0,0.14),
0 1px 18px 0 rgba(0,0,0,0.12);
display: flex;
justify-content: space-between;
align-items: center;
margin-top: 8px; /* Spacing between stacked snackbars */
overflow: hidden; /* Ensures content doesn't spill during animation */
min-height: 48px; /* Standard height */
box-sizing: border-box;
}

.generic-snackbar .snackbar-text {
flex-grow: 1;
font-size: 0.875rem;
line-height: 1.25rem;
}

.generic-snackbar .snackbar-buttons {
display: flex;
align-items: center;
margin-left: 16px; /* Spacing between text and buttons */
}

.generic-snackbar .snackbar-buttons button {
/* Using Material Design Lite button styling as a base, assuming MDL is available */
/* text-transform: uppercase; */ /* Often used, but can be optional */
font-weight: 500;
/* color: #bb86fc; */ /* Example accent color for buttons, adjust as needed */
padding: 0 8px; /* Horizontal padding for buttons */
min-width: auto; /* Override MDL's min-width if necessary for compact buttons */
height: 36px; /* Standard height for buttons in snackbars */
line-height: 36px; /* Vertically center text */
}

.generic-snackbar .snackbar-buttons button:not(:last-child) {
margin-right: 8px; /* Spacing between buttons */
}

/* Kind-specific styling examples (add more as needed) */
.snackbar-kind-error {
background-color: #d32f2f; /* Red for errors */
color: #ffffff;
}

.snackbar-kind-error .snackbar-buttons button {
color: #ffffff; /* White buttons on red background */
}

.snackbar-kind-success {
background-color: #4caf50; /* Green for success */
color: #ffffff;
}

.snackbar-kind-success .snackbar-buttons button {
color: #ffffff; /* White buttons on green background */
}

/* RTL support adjustments */
.generic-snackbar[dir="rtl"] .snackbar-buttons {
margin-left: 0;
margin-right: 16px; /* Spacing between text and buttons for RTL */
}

.generic-snackbar[dir="rtl"] .snackbar-buttons button:not(:last-child) {
margin-right: 0;
margin-left: 8px; /* Spacing between buttons for RTL */
}
128 changes: 128 additions & 0 deletions js/GenericSnackbar.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
import * as React from "react";
import { animated, useSpring, config } from "@react-spring/web";

const { useState, useEffect, useMemo } = React;

export interface SnackbarButtonProps {
text: string | React.ReactNode;
onClick: () => void;
className?: string;
disabled?: boolean; // Added disabled prop
}

export interface GenericSnackbarProps {
id: string;
message: string | React.ReactNode;
buttons?: SnackbarButtonProps[];
isVisible: boolean;
onDismiss: (id: string) => void; // Called for explicit dismiss actions like timeout
onAnimatedOut?: (id: string) => void; // Called after exit animation completes
kind?: string;
autoHideDuration?: number | null; // Duration in ms, null to disable auto-hide
}

export function GenericSnackbar({
id,
message,
buttons,
isVisible,
onDismiss,
onAnimatedOut,
kind,
autoHideDuration = 6000, // Default to 6 seconds
}: GenericSnackbarProps): React.ReactElement | null {
const [isShown, setIsShown] = useState(false); // Internal state to help drive animation

// Animation for the snackbar
const animationProps = useSpring({
from: { opacity: 0, transform: "translateY(100%)" },
to: {
opacity: isVisible ? 1 : 0, // Drive directly by isVisible for opacity
transform: isVisible ? "translateY(0%)" : "translateY(100%)", // And transform
},
config: config.gentle,
onRest: (result) => {
// onRest can be called for both entry and exit animations.
// We are interested when the snackbar has animated out.
if (!isVisible && result.finished) { // Check if animation finished and snackbar is not visible
onAnimatedOut?.(id);
}
},
});

useEffect(() => {
// This effect manages the isShown state, primarily for initial mount.
// The animation itself is now more directly driven by the isVisible prop.
if (isVisible) {
setIsShown(true);
} else {
// If isVisible is false from the start, or becomes false
setIsShown(false);
}
}, [isVisible]);

useEffect(() => {
let timerId: NodeJS.Timeout | null = null;
if (isVisible && autoHideDuration) {
timerId = setTimeout(() => {
onDismiss(id);
}, autoHideDuration);
}
return () => {
if (timerId) {
clearTimeout(timerId);
}
};
}, [id, isVisible, autoHideDuration, onDismiss]);

// Memoize buttons to prevent re-renders if props haven't changed
const memoizedButtons = useMemo(() => {
return buttons?.map((buttonProps, index) => (
<button
key={index}
onClick={() => {
if (buttonProps.disabled) return;
buttonProps.onClick();
}}
className={`mdl-button mdl-js-button mdl-button--colored ${buttonProps.className || ""} ${buttonProps.disabled ? "mdl-button--disabled" : ""}`}
disabled={buttonProps.disabled}
>
{buttonProps.text}
</button>
));
}, [buttons]);

// Determine CSS classes
const snackbarClasses = ["generic-snackbar"];
if (kind) {
snackbarClasses.push(`snackbar-kind-${kind}`);
}
// Add basic styling for visibility and layout
// More specific styling should be handled by CSS files.
const direction = localStorage.languageOption === "hebrew" ? "rtl" : "ltr";

// Do not render if it's not supposed to be visible and not currently shown (e.g. already dismissed and animated out)
// This is a slight adjustment to ensure that if isVisible is false from the start, nothing is rendered.
// The main purpose of this component is to animate in/out based on isVisible.
// If isVisible is false and isShown is also false (meaning it's fully dismissed or was never meant to show), return null.
if (!isVisible && !isShown) {
return null;
}


return (
<animated.div
style={animationProps}
className={snackbarClasses.join(" ")}
dir={direction}
role="alert"
aria-live="assertive"
aria-atomic="true"
>
<div className="snackbar-text">{message}</div>
{memoizedButtons && memoizedButtons.length > 0 && (
<div className="snackbar-buttons">{memoizedButtons}</div>
)}
</animated.div>
);
}
113 changes: 113 additions & 0 deletions js/GoogleSignInSnackbar.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
import * as React from "react";
import { useSnackbar, SnackbarConfig } from "./SnackbarProvider";
import { LocalStorageInt } from "./localStorage";

// Assume gtag is available globally, or provide it via context/props
declare global {
interface Window {
gtag: (...args: any[]) => void;
// Define other global properties if needed, e.g., for googleSignIn function
triggerGoogleSignIn?: () => void; // Example, replace with actual
}
}

const { useEffect, useCallback } = React;

const GOOGLE_SIGN_IN_SHOW_COUNTER_LS_KEY = "googleSignInShowCounter"; // From original logic
const GOOGLE_SIGN_IN_SNACKBAR_SHOWN_COUNT_LS_KEY = "googleSignInSnackbarShownCount";
const GOOGLE_SIGN_IN_SNACKBAR_DISMISSED_LS_KEY = "googleSignInSnackbarShownDismissed";

const MAX_SHOW_COUNT = 9999999; // Effectively infinite as per original
const SNACKBAR_ID = "google-sign-in-snackbar";

// Helper functions mirroring 'Kind' class logic for this specific snackbar
const getShownCount = () => new LocalStorageInt(GOOGLE_SIGN_IN_SNACKBAR_SHOWN_COUNT_LS_KEY).get() || 0;
const incrementShownCount = () => {
new LocalStorageInt(GOOGLE_SIGN_IN_SNACKBAR_SHOWN_COUNT_LS_KEY).set(getShownCount() + 1);
};
const getDismissed = () => localStorage.getItem(GOOGLE_SIGN_IN_SNACKBAR_DISMISSED_LS_KEY) === "true";
const setDismissed = () => localStorage.setItem(GOOGLE_SIGN_IN_SNACKBAR_DISMISSED_LS_KEY, "true");

const customShowLogic = () => {
// This counter is different from the general "shownCount" for the snackbar instance itself.
// This seems to be a global counter for how many times the app has considered showing this.
const showCountConsidered = new LocalStorageInt(GOOGLE_SIGN_IN_SHOW_COUNTER_LS_KEY).getAndIncrement();
let modulo = 50;
if (showCountConsidered < 5) modulo = 1;
else if (showCountConsidered < 20) modulo = 2;
else if (showCountConsidered < 50) modulo = 4;
else if (showCountConsidered < 100) modulo = 10;
return showCountConsidered % modulo === 0;
};

// Placeholder for actual Google Sign-In trigger function
// This should be replaced with the actual implementation used in the project.
const triggerGoogleSignIn = () => {
console.log("Attempting to trigger Google Sign-In");
if (window.triggerGoogleSignIn) {
window.triggerGoogleSignIn();
} else {
alert("Google Sign-In function not implemented yet.");
}
};


export function GoogleSignInSnackbarTrigger() {
const { showSnackbar, hideSnackbar } = useSnackbar();

const handleSignInClick = useCallback(() => {
if (window.gtag) {
window.gtag("event", "snackbar.googleSignIn.signInClicked");
}
triggerGoogleSignIn(); // Call the actual sign-in function
hideSnackbar(SNACKBAR_ID); // Dismiss after clicking
}, [hideSnackbar]);

const handleDismissClick = useCallback(() => {
if (window.gtag) {
window.gtag("event", "snackbar.googleSignIn.dismissed");
}
setDismissed();
hideSnackbar(SNACKBAR_ID);
}, [hideSnackbar]);

useEffect(() => {
// Logic from SnackbarManager constructor and GOOGLE_SIGN_IN Kind
// No shouldResetShowCount for this kind in original code.

const isDismissed = getDismissed();
const currentInstanceShowCount = getShownCount();

if (!isDismissed && customShowLogic() && currentInstanceShowCount < MAX_SHOW_COUNT) {
// Delay incrementing the shown count for this specific snackbar instance
setTimeout(() => incrementShownCount(), 5 * 1000);

const useHebrew = localStorage.languageOption === "hebrew";
// Message and buttons need to be defined. The original code doesn't specify
// the message for this snackbar directly in the `snackbars.googleSignIn.show(...)` call
// as it was a "startup" snackbar. We need to define a default message/buttons.
// Assuming a generic message for now. This should be reviewed for actual content.
const message = useHebrew ? "התחבר עם גוגל לחוויה טובה יותר" : "Sign in with Google for a better experience";

const snackbarConfig: SnackbarConfig = {
id: SNACKBAR_ID,
message: message,
buttons: [
{
text: useHebrew ? "התחבר" : "Sign In",
onClick: handleSignInClick,
},
{
text: useHebrew ? "סגור" : "Dismiss",
onClick: handleDismissClick,
},
],
kind: "google-sign-in",
autoHideDuration: 15000, // Example: 15 seconds
};
showSnackbar(snackbarConfig);
}
}, [showSnackbar, handleSignInClick, handleDismissClick]);

return null; // This component is a trigger
}
Loading
Loading