Skip to content

Conversation

@Bekiboo
Copy link
Collaborator

@Bekiboo Bekiboo commented Jan 28, 2026

Description of change

Disclaimer Modal Improvements

Added interactive disclaimer modals across platforms with:

  • Pulse animation when user clicks outside instead of closing
  • Tooltip/hint message guiding users to accept
  • localStorage persistence - disclaimer only appears once per user
  • Hidden close button to ensure acceptance

Changes:

  • group-charter-manager: Dialog component + disclaimer modal with Tooltip wrapper
  • blabsy: Layout disclaimer with inline hint message
  • eVoting: Dialog component + layout disclaimer with Tooltip wrapper
  • pictique: Modal component + layout disclaimer

Issue Number

Closes #628

Type of change

  • Update (a change which updates existing functionality)
  • Fix (a change which fixes an issue)

How the change has been tested

Tested with group-charter-manager, eVoting and pictique, but couldn't test with Blabsy

Change checklist

  • I have ensured that the CI Checks pass locally
  • I have removed any unnecessary logic
  • My code is well documented
  • I have signed my commits
  • My code follows the pattern of the application
  • I have self reviewed my code

Summary by CodeRabbit

  • New Features

    • Disclaimer modals now require explicit acceptance before accessing protected areas; acceptance is persisted across sessions.
    • Clicking outside shows a pulsing animation and a one-time hint/tooltip guiding acceptance rather than silently closing or logging out.
    • Modal actions and close controls were standardized for a more consistent experience across apps.
  • Bug Fixes

    • Improved handling in restricted environments to avoid accidental prompt loss and ensure the disclaimer state initializes before content loads.

✏️ Tip: You can customize this high-level summary in your review settings.

@Bekiboo Bekiboo self-assigned this Jan 28, 2026
@coderabbitai
Copy link
Contributor

coderabbitai bot commented Jan 28, 2026

📝 Walkthrough

Walkthrough

This PR centralizes disclaimer handling across platforms: reads/stores acceptance in localStorage, prevents logout on outside-click by triggering a pulse animation and hint, renders disclaimers until accepted, and adds dialog/modal API opts for controlling outside-click and close-button behavior.

Changes

Cohort / File(s) Summary
Disclaimer state & flows
platforms/blabsy/src/components/layout/common-layout.tsx, platforms/eVoting/src/app/(app)/layout.tsx, platforms/group-charter-manager/src/components/disclaimer-modal.tsx, platforms/pictique/src/routes/(protected)/+layout.svelte
Initialize disclaimer state from localStorage; persist acceptance on “I Understand”; keep modal mounted until accepted; replace sign-out-on-outside-click with handler that triggers pulsing animation and a one-time hint; delay rendering protected children until acceptance.
Dialog component prop change
platforms/eVoting/src/components/ui/dialog.tsx, platforms/group-charter-manager/src/components/ui/dialog.tsx
Extend DialogContent props with optional hideCloseButton?: boolean and conditionally omit the close button when true.
Modal outside-click hook
platforms/pictique/src/lib/ui/Modal/Modal.svelte
Add onClickOutside?: (modal: HTMLDialogElement) => void prop and delegate outside-click behavior to it when provided; otherwise retain default close behavior.
UI: animations, tooltip/hint, styling
platforms/*/...disclaimer*.tsx, platforms/*/.../layout.tsx, platforms/group-charter-manager/src/components/disclaimer-modal.tsx
Add pulsing scale animation (inline keyframes/class), TooltipProvider/TooltipTrigger/TooltipContent hint flow, full-width accept button styling, and showHint/isPulsing UI states.

Sequence Diagram

sequenceDiagram
    participant User
    participant App
    participant localStorage
    participant Modal
    rect rgba(220,240,255,0.5)
    User->>App: Navigate to protected route / Mount
    App->>localStorage: safeGetItem(DISCLAIMER_KEY)
    localStorage-->>App: value or null
    end
    alt accepted
        App->>User: Render protected content
    else not accepted
        App->>Modal: Render disclaimer modal (open)
        User->>Modal: Interact
        alt Click "I Understand"
            Modal->>localStorage: safeSetItem(DISCLAIMER_KEY, "true")
            localStorage-->>App: persisted
            App->>User: Render protected content
        else Click outside
            Modal->>App: invoke onClickOutside / handler
            App->>App: set isPulsing = true, showHint = true
            App->>User: Play pulse animation and show hint tooltip
        end
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

Possibly related PRs

Suggested reviewers

  • coodos
  • sosweetham

Poem

🐰 I nibble code and tap the key,

No logout leaps when clicks roam free.
A gentle pulse, a helpful hint—
Click to stay, don’t lose your stint. ✨

🚥 Pre-merge checks | ✅ 3 | ❌ 2
❌ Failed checks (2 warnings)
Check name Status Explanation Resolution
Title check ⚠️ Warning The title refers to adding 'allowClickOutside prop' but the PR actually implements pulse animations, tooltips, and localStorage persistence for disclaimer modals without introducing an 'allowClickOutside' prop. Update title to reflect the main change: something like 'Add interactive disclaimer modals with pulse animation and localStorage persistence' to accurately represent the comprehensive changes made.
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (3 passed)
Check name Status Explanation
Description check ✅ Passed The description adequately covers the main changes across platforms, lists the modifications, and addresses the bug fix. While the section headings don't perfectly match the template, the content is complete and informative.
Linked Issues check ✅ Passed The PR successfully addresses issue #628 by preventing unintended logout when users click outside disclaimer modals through pulse animations and mandatory acceptance flows.
Out of Scope Changes check ✅ Passed All changes are focused on implementing interactive disclaimer modals with localStorage persistence and pulse animations. The scope aligns with issue #628 requirements to prevent unintended logout.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@Bekiboo Bekiboo marked this pull request as ready for review January 28, 2026 08:33
@Bekiboo Bekiboo requested a review from coodos as a code owner January 28, 2026 08:33
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 7

🤖 Fix all issues with AI agents
In `@platforms/blabsy/src/components/layout/common-layout.tsx`:
- Around line 59-96: The mailto anchor currently uses the className
'outline-none' which removes keyboard focus indicators; update the anchor in
common-layout.tsx (the <a href='mailto:...'> element) to add focus-visible
utility classes consistent with other patterns (e.g., use focus-visible:ring-2,
focus-visible:ring-yellow-400, focus-visible:ring-offset-2 and keep outline-none
only for non-focus visuals) so keyboard users see a clear focus ring; ensure the
class list mirrors the project's .main-tab/.custom-underline focus-visible
approach and preserves existing styling while restoring accessible focus
outlines.
- Line 5: The handleOutsideClick handler currently sets a timeout that calls
setIsPulsing(false) which may fire after the component unmounts; add a ref
(e.g., pulseTimeoutRef) to store the timeout ID when you call setTimeout, clear
that timeout in the CommonLayout component's cleanup (useEffect return) and also
clear it before setting a new timeout inside handleOutsideClick so you never
leave an untracked timer running; update any checks around
isPulsing/setIsPulsing to avoid calling state after unmount.
- Around line 14-27: The component initializes disclaimerAccepted to true which
lets the app render before localStorage is read and also calls
localStorage.getItem/setItem without guards; change ProtectedLayout to track a
separate "storageChecked" flag (e.g., initialize disclaimerAccepted to false or
undefined and add storageChecked state) and only render children after
storageChecked is true (or render a lightweight placeholder), and wrap all
localStorage access in try-catch blocks around the calls referenced by
DISCLAIMER_KEY, the useEffect that calls localStorage.getItem, and wherever
localStorage.setItem is used so failures in private mode/quota errors are
handled (fall back to non-persistent acceptance and still call
setDisclaimerAccepted/set storageChecked inside the try/catch).

In `@platforms/eVoting/src/app/`(app)/layout.tsx:
- Around line 44-49: LocalStorage access in the useEffect that reads
DISCLAIMER_KEY can throw in restricted/private browsing; wrap the access in a
safe guard (try/catch or a helper like safeStorageGet/safeStorageSet) around
localStorage.getItem and localStorage.setItem so runtime errors are swallowed
and a sensible fallback (false) is used, and update both the useEffect that
reads DISCLAIMER_KEY (where setDisclaimerAccepted is called) and the code path
that writes the key (the places that call localStorage.setItem for
DISCLAIMER_KEY) to use this safe helper; reference the useEffect,
DISCLAIMER_KEY, and setDisclaimerAccepted symbols when making the change.
- Around line 98-105: The pulse animation isn't applied because there's no
.animate-pulse-scale class and the inline style only sets animationName; update
the component rendering logic (the className and style that reference isPulsing,
animate-pulse-scale and animationName) to use a real animation
declaration—either add a CSS class .animate-pulse-scale that sets
animation-duration, animation-timing-function and animation-iteration-count for
the `@keyframes` pulse-scale, or replace the conditional class with a Tailwind
arbitrary animation (e.g. use isPulsing &&
"animate-[pulse-scale_1.2s_linear_infinite]") and remove the inline
style.animationName so the animation runs.

In `@platforms/group-charter-manager/src/components/disclaimer-modal.tsx`:
- Around line 28-32: Wrap all direct localStorage accesses in try/catch and
guard with a feature check before use: in the useEffect that reads
DISCLAIMER_KEY and calls setDisclaimerAccepted, catch exceptions from
localStorage.getItem and default to false; similarly, guard the code paths that
write DISCLAIMER_KEY (the accept/close handlers around the modal, referenced at
lines handling save of DISCLAIMER_KEY) so they try/catch localStorage.setItem
and degrade gracefully (update state without persisting if storage is
unavailable). Reference the existing useEffect, DISCLAIMER_KEY,
setDisclaimerAccepted, and the modal accept/close handlers to locate where to
add these guards.

In `@platforms/pictique/src/lib/ui/Modal/Modal.svelte`:
- Around line 23-42: The handler currently calls onclose even when the modal is
kept open (branch where !isInDialog && !allowClickOutside); remove the onclose
invocation from that branch so onclose is only called when modal.close() is
invoked, and if you need a signal for the blocked outside click add and call a
separate optional callback (e.g., onOutsideClick?) instead; update the code
paths around isInDialog, allowClickOutside, modal.close(), modal.animate() and
onclose to ensure onclose is never invoked when the animate/pulse branch runs.
🧹 Nitpick comments (3)
platforms/group-charter-manager/src/components/disclaimer-modal.tsx (1)

34-39: Clear the pulse timeout on unmount to avoid stale state updates.

The timeout can fire after unmount and trigger a setState warning.

♻️ Suggested cleanup
-import { useState, useEffect } from "react";
+import { useState, useEffect, useRef } from "react";
@@
     const [showHint, setShowHint] = useState(false);
     const [isPulsing, setIsPulsing] = useState(false);
+    const pulseTimeoutRef = useRef<number | null>(null);
@@
     const handleInteractOutside = (e: Event) => {
         e.preventDefault();
         setIsPulsing(true);
         setShowHint(true);
-        setTimeout(() => setIsPulsing(false), 400);
+        if (pulseTimeoutRef.current) window.clearTimeout(pulseTimeoutRef.current);
+        pulseTimeoutRef.current = window.setTimeout(() => setIsPulsing(false), 400);
     };
+
+    useEffect(() => {
+        return () => {
+            if (pulseTimeoutRef.current) window.clearTimeout(pulseTimeoutRef.current);
+        };
+    }, []);
platforms/eVoting/src/app/(app)/layout.tsx (1)

51-56: Clear the pulse timeout on unmount to avoid stale state updates.

The timeout can fire after unmount and trigger a setState warning.

♻️ Suggested cleanup
-import { useEffect, useState } from "react";
+import { useEffect, useState, useRef } from "react";
@@
     const [disclaimerAccepted, setDisclaimerAccepted] = useState(false);
     const [isPulsing, setIsPulsing] = useState(false);
     const [showHint, setShowHint] = useState(false);
+    const pulseTimeoutRef = useRef<number | null>(null);
@@
     const handleInteractOutside = (e: Event) => {
         e.preventDefault();
         setIsPulsing(true);
         setShowHint(true);
-        setTimeout(() => setIsPulsing(false), 400);
+        if (pulseTimeoutRef.current) window.clearTimeout(pulseTimeoutRef.current);
+        pulseTimeoutRef.current = window.setTimeout(() => setIsPulsing(false), 400);
     };
+
+    useEffect(() => {
+        return () => {
+            if (pulseTimeoutRef.current) window.clearTimeout(pulseTimeoutRef.current);
+        };
+    }, []);
platforms/blabsy/src/components/layout/common-layout.tsx (1)

47-58: Consider moving inline keyframes to a stylesheet

Injecting a <style> tag on render can duplicate styles and complicate CSP. Prefer a CSS module/global stylesheet for pulse-scale.

♻️ Suggested change
-                <style>{`
-                    `@keyframes` pulse-scale {
-                        0% { transform: scale(1); }
-                        25% { transform: scale(1.01); }
-                        50% { transform: scale(0.99); }
-                        75% { transform: scale(1.005); }
-                        100% { transform: scale(1); }
-                    }
-                    .animate-pulse-scale {
-                        animation: pulse-scale 0.4s ease-in-out;
-                    }
-                `}</style>

Add the keyframes to your component stylesheet (module or global), e.g.:

`@keyframes` pulse-scale {
  0% { transform: scale(1); }
  25% { transform: scale(1.01); }
  50% { transform: scale(0.99); }
  75% { transform: scale(1.005); }
  100% { transform: scale(1); }
}
.animate-pulse-scale { animation: pulse-scale 0.4s ease-in-out; }

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In `@platforms/group-charter-manager/src/components/disclaimer-modal.tsx`:
- Around line 51-56: handleInteractOutside sets showHint true but never resets
it; add an auto-dismiss by scheduling setShowHint(false) (e.g., 2–4s) inside
handleInteractOutside, store the timer id in a ref (e.g., hintTimeoutRef) and
clear any existing timeout before creating a new one, and also clear the
timeout(s) in a useEffect cleanup to avoid leaks; keep the existing setIsPulsing
behavior but similarly clear its timeout when appropriate.
🧹 Nitpick comments (2)
platforms/pictique/src/routes/(protected)/+layout.svelte (1)

139-145: Consider adding accessibility attributes to the dynamic hint.

The hint message appears dynamically but won't be announced to screen reader users. Adding role="alert" ensures assistive technologies announce this important guidance.

♿ Suggested accessibility improvement
 		{`#if` showHint}
 			<p
-				class="mt-2 rounded-md border border-red-300 bg-red-100 px-3 py-2 text-center text-xs text-red-800"
+				class="mt-2 rounded-md border border-red-300 bg-red-100 px-3 py-2 text-center text-xs text-red-800"
+				role="alert"
 			>
 				💡 You must accept the disclaimer to continue. This will only appear once.
 			</p>
 		{/if}
platforms/group-charter-manager/src/components/disclaimer-modal.tsx (1)

63-81: Simplify the animation implementation.

The animation is currently defined via three overlapping mechanisms:

  1. Tailwind arbitrary class: animate-[pulse-scale_0.4s_ease-in-out]
  2. Inline style: animationName: isPulsing ? 'pulse-scale' : 'none'
  3. styled-jsx keyframes

This is redundant and potentially fragile—styled-jsx scopes CSS, which may conflict with Tailwind's expectation of globally-accessible keyframes. Consider using just one approach.

♻️ Suggested simplification using inline style only
 <DialogContent
     className={cn(
-        "max-w-lg mx-auto backdrop-blur-md p-6 rounded-lg",
-        isPulsing && "animate-[pulse-scale_0.4s_ease-in-out]"
+        "max-w-lg mx-auto backdrop-blur-md p-6 rounded-lg"
     )}
     style={{
-        animationName: isPulsing ? 'pulse-scale' : 'none'
+        animation: isPulsing ? 'pulse-scale 0.4s ease-in-out' : 'none'
     }}
     hideCloseButton={true}
     onInteractOutside={handleInteractOutside}
 >

@Bekiboo Bekiboo merged commit 609f4e9 into main Jan 28, 2026
4 checks passed
@Bekiboo Bekiboo deleted the fix/disclaimer-pop-up branch January 28, 2026 09:26
coodos pushed a commit that referenced this pull request Jan 30, 2026
* fix: add allowClickOutside prop to Modal for improved interaction

* fix: enhance DisclaimerModal with tooltip and animation effects

* fix: update ProtectedLayout to include disclaimer modal with enhanced styling and interaction

* fix: enhance disclaimer modal with pulsing animation and tooltip for improved user interaction

* fix: improve disclaimer handling with localStorage check and user hint

* fix: update Modal to use onClickOutside prop for improved interaction and animation

* fix: implement safe localStorage access methods for disclaimer handling across components

* fix: improve disclaimer handling with enhanced localStorage access and user interaction
@coderabbitai coderabbitai bot mentioned this pull request Jan 30, 2026
6 tasks
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug] Misusing disclaimer pop-up logs out user

3 participants