Skip to content
Open
Show file tree
Hide file tree
Changes from 2 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
18 changes: 17 additions & 1 deletion App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ const App: React.FC = () => {
const startPos = { x: grapeRect.left + grapeRect.width / 2, y: grapeRect.top + grapeRect.height / 2 };
const endPos = { x: mouthRect.left + mouthRect.width / 2, y: mouthRect.top + mouthRect.height / 2 };
setFlyingGrapes(prev => [...prev, { id: Date.now(), startPos, endPos }]);
vibrarTelefono([200, 200]);
}

// 2. Scoring Logic
Expand All @@ -78,13 +79,26 @@ const App: React.FC = () => {

}, [timeDiff, grapeStatus, lastCorrectChime]);

const vibrarTelefono = (pattern) => {
console.log("Vibrando", pattern);
if ("vibrate" in navigator) {
// Intentar vibrar 500ms
const exito = navigator.vibrate(pattern);
if (!exito) {
console.log("La vibración fue bloqueada o no es compatible.");
}
} else {
console.log("Tu navegador no soporta la API de vibración.");
}
};

const startTest = () => {
audioService.init();
setIsTestMode(true);
const now = Date.now();
const target = TARGET_DATE.getTime();
setOffsetTime(target - 40000 - now);
vibrarTelefono([200, 200]);
};

const getPhaseText = () => {
Expand Down Expand Up @@ -124,13 +138,15 @@ const App: React.FC = () => {
text: '¡Aún no ansioso!, espera a que empiecen las campanadas.',
duration: EARLY_CLICK_MODAL_AUTO_CLOSE_MS
});
vibrarTelefono([200, 200]);
}, []);

const showInstructionsMessage = useCallback(() => {
setOverlayMessage({
text: 'Vamos a empezar bien el año!!! 🥳 Espera a que empiecen las campanadas y toca cada uva cuando suene la campana.',
duration: INSTRUCTIONS_MODAL_AUTO_CLOSE_MS
});
vibrarTelefono([200, 200]);
}, []);

const handleOverlayClose = useCallback(() => {
Expand Down Expand Up @@ -210,7 +226,7 @@ const App: React.FC = () => {
/>
</div >

{/* Grapes Grid & Message Overlay Container */}
{/* Grapes Grid */}
<div className="relative">
<div className={`transition-opacity duration-1000 ${phase === AppPhase.CHIMES || phase === AppPhase.CELEBRATION || phase === AppPhase.GAP ? 'opacity-100' : 'opacity-20'}`}>
<GrapeGrid
Expand Down
16 changes: 15 additions & 1 deletion hooks/useAudioController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,20 +53,34 @@ export const useAudioController = ({ phase, timeDiff }: UseAudioControllerProps)
if (phase === AppPhase.QUARTERS) {
const progress = Math.abs(T_QUARTERS_START) - timeDiff;
const quarterIdx = Math.floor(progress / 3500);

// vibrarTelefono([200, 200]);
if (quarterIdx >= 0 && quarterIdx < 4 && quarterIdx !== prevQuarterIndexRef.current) {
audioService.playQuarter();
prevQuarterIndexRef.current = quarterIdx;
}
}

function vibrarTelefono(pattern) {
console.log("Vibrando", pattern);
if ("vibrate" in navigator) {
// Intentar vibrar 500ms
const exito = navigator.vibrate(pattern);
if (!exito) {
console.log("La vibración fue bloqueada o no es compatible.");
}
} else {
console.log("Tu navegador no soporta la API de vibración.");
}
}

// 3. Chimes Logic
if (phase === AppPhase.CHIMES) {
const msSinceMidnight = Math.abs(timeDiff);
const currentChimeIndex = Math.floor(msSinceMidnight / CHIME_INTERVAL);

if (currentChimeIndex !== prevChimeIndexRef.current && currentChimeIndex < 12) {
audioService.playChime();
// vibrarTelefono([200, 200]);
prevChimeIndexRef.current = currentChimeIndex;
}
}
Expand Down
17 changes: 15 additions & 2 deletions hooks/useCampanadasTimer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,9 +25,22 @@ export const useCampanadasTimer = ({ offsetTime }: UseCampanadasTimerProps) => {
const [chimeCount, setChimeCount] = useState(0);
const [timeDiff, setTimeDiff] = useState(0);

const vibrarTelefono = (pattern) => {
console.log("Vibrando", pattern);
if ("vibrate" in navigator) {
// Intentar vibrar 500ms
const exito = navigator.vibrate(pattern);
if (!exito) {
console.log("La vibración fue bloqueada o no es compatible.");
}
} else {
console.log("Tu navegador no soporta la API de vibración.");
}
};
Comment on lines +28 to +39

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion | 🟠 Major

Extract duplicated function to a shared utility module and add TypeScript types.

This function is duplicated across at least three files (App.tsx, useCampanadasTimer.ts, and useAudioController.ts per the AI summary). Consider extracting it to a shared utility module (e.g., utils/vibration.ts) with proper TypeScript typing:

type VibrationPattern = number | number[];

export const vibrarTelefono = (pattern: VibrationPattern): void => {
  console.log("Vibrando", pattern);
  if ("vibrate" in navigator) {
    const exito = navigator.vibrate(pattern);
    if (!exito) {
      console.log("La vibración fue bloqueada o no es compatible.");
    }
  } else {
    console.log("Tu navegador no soporta la API de vibración.");
  }
};

Then import and use it across all three files.

🤖 Prompt for AI Agents
In @hooks/useCampanadasTimer.ts around lines 28 - 39, Extract the duplicated
vibrarTelefono implementation into a single shared utility module (e.g., export
function vibrarTelefono from a new vibration util) and add TypeScript types:
declare type VibrationPattern = number | number[] and export const
vibrarTelefono = (pattern: VibrationPattern): void => { ... }; then replace the
local implementations in App.tsx, useCampanadasTimer.ts and
useAudioController.ts to import and call the shared vibrarTelefono, keeping the
existing console/log behavior and runtime checks for "vibrate" in navigator.


Comment on lines +28 to +40

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

# First, let's locate and examine the file
find . -type f -name "useCampanadasTimer.ts" | head -5

Repository: emaza/campanadas

Length of output: 89


🏁 Script executed:

# Let's also search for VibratePattern type in the codebase
rg "VibratePattern" --type ts

Repository: emaza/campanadas

Length of output: 42


🏁 Script executed:

# Check how vibrarTelefono is used/called in the file
rg "vibrarTelefono" --type ts -B 2 -A 2

Repository: emaza/campanadas

Length of output: 2646


🌐 Web query:

navigator.vibrate() API requirements user-gesture HTTPS browser support MDN

💡 Result:

Summary — navigator.vibrate() (MDN)

  • Secure context: available only in secure contexts (HTTPS). [1]
  • User gesture: requires a sticky user activation (user interaction) for the call to actually vibrate. [1]
  • Browser support (high level): supported in Chromium-based browsers, Firefox and many Android browsers; not supported (or very limited) in Safari / iOS Safari. Some engines disable vibration in cross-origin iframes. Implementation history: Chromium (Chrome/Android) began requiring a user gesture around Chrome 60 and disallowed cross-origin iframe vibration around Chrome 55; Opera and Samsung browsers have similar restrictions; Firefox behavior differs slightly across versions (pattern truncation, iframe rules). See browser-compat tables for exact version details. [1][2]

Sources

  • Navigator.vibrate() — MDN Web Docs. [1]
  • Vibration API — MDN Web Docs (browser-compat/details and implementation notes). [2]

🏁 Script executed:

# Get the full context of useCampanadasTimer.ts
wc -l ./hooks/useCampanadasTimer.ts

Repository: emaza/campanadas

Length of output: 92


🏁 Script executed:

# Read the file to see the complete implementation
cat -n ./hooks/useCampanadasTimer.ts | head -50

Repository: emaza/campanadas

Length of output: 1810


🏁 Script executed:

# Check if there are any lint configs or console rules
find . -type f -name ".eslintrc*" -o -name "eslint.config.*" -o -name "lint.config.*" | head -5

Repository: emaza/campanadas

Length of output: 42


🏁 Script executed:

# Check if console is being used elsewhere and how it's handled
rg "console\.(log|warn|error)" --type ts --stats | head -20

Repository: emaza/campanadas

Length of output: 1013


Extract to a shared utility, add proper types and browser/environment guards, and remove console logs.

This function is duplicated identically in three files (App.tsx, useCampanadasTimer.ts, useAudioController.ts) and should be consolidated into a shared hook or utility.

Additionally, the pattern parameter is untyped, and the direct navigator reference will crash in test or non-browser environments without proper guards. Console logs should be removed from production hooks.

Suggested improvements

Create a shared hook (e.g., hooks/useVibration.ts):

+export const useVibration = () => {
+  const vibrate = (pattern: number | number[] = [200, 200]): boolean => {
+    if (typeof navigator === 'undefined' || typeof navigator.vibrate !== 'function') {
+      return false;
+    }
+    return navigator.vibrate(pattern);
+  };
+
+  return { vibrate };
+};

Then import and use in all three files:

-  const vibrarTelefono = (pattern) => {
-    console.log("Vibrando", pattern);
-    if ("vibrate" in navigator) {
-      const exito = navigator.vibrate(pattern);
-      if (!exito) {
-        console.log("La vibración fue bloqueada o no es compatible.");
-      }
-    } else {
-      console.log("Tu navegador no soporta la API de vibración.");
-    }
-  };

+  const { vibrate } = useVibration();

Note: navigator.vibrate() requires HTTPS and an active user gesture (which your current usage on user interactions already satisfies), and has limited support on Safari/iOS.

Committable suggestion skipped: line range outside the PR's diff.

useEffect(() => {
let animationFrameId: number;

vibrarTelefono([300, 300]);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🌐 Web query:

Web Vibration API user gesture requirement browser security policy

💡 Result:

Short answer

  • The Vibration API (navigator.vibrate()) is gated by user activation: browsers require a user gesture (sticky activation — the page must have been interacted with) for vibration to run; otherwise the call is blocked or returns false. [1][2][3][4]

Key points / security rationale

  • This is a deliberate policy to prevent abusive/background vibration (and to treat first‑ and third‑party contexts separately). Browsers may also choose to further restrict/disable vibration (e.g., in cross‑origin iframes). [3][4][1]

Browser behaviour notes

  • Chromium-family: shipped a user‑gesture requirement (calls return false when no user activation); there is source/flag text and related issues. Cross‑origin iframe uses were also limited. [2][5]
  • Firefox/Opera/Samsung have implemented similar gating and iframe restrictions; Safari historically lacks support. Check current browser versions for exact behavior. [1][3]

References

  1. MDN — Navigator.vibrate() (security: sticky user activation required). [MDN]. [link].
  2. Chromium source & feature discussion: "Requiring user gesture for the Vibration API" (flag/issue). [chromium]. [link].
  3. HTML Standard — user activation / activation‑gated APIs (sticky activation semantics). [WHATWG HTML]. [link].
  4. W3C/public-device-apis log and Vibration API discussion describing the spec change to require user activation. [W3C]. [link].

If you want, I can fetch exact browser-version behavior (which versions enforce the rule, iframe specifics, and example detection code).


🏁 Script executed:

# Find and read the useCampanadasTimer.ts file to understand context
fd -t f "useCampanadasTimer.ts" --exec cat -n {}

Repository: emaza/campanadas

Length of output: 3688


Move vibration calls outside of useEffect initialization to user-initiated events.

The Vibration API requires user activation (a sticky gesture like click or tap) to function in modern browsers (Chrome, Firefox, Opera, Samsung). Calling vibrarTelefono([300, 300]) on line 43 during useEffect initialization runs before any user interaction and will be silently blocked—the navigator.vibrate() call will return false in most browsers.

The current defensive logging catches this silently, but the vibration won't work. Move vibration triggers to event handlers or other user-initiated actions instead.

🤖 Prompt for AI Agents
In @hooks/useCampanadasTimer.ts at line 43, The call to vibrarTelefono([300,
300]) inside the useEffect initialization of useCampanadasTimer will be blocked
by browsers because vibration must be triggered by a user gesture; remove that
call from the useEffect and instead invoke vibrarTelefono from a user-initiated
handler (e.g., a click/tap or explicit "play"/"ring" action) tied to the
component that uses useCampanadasTimer; update any callers or event handlers to
call vibrarTelefono (or expose a startVibration function from the hook) and keep
the defensive logging in place if the API still returns false.

const updateTime = () => {
const now = Date.now() + offsetTime;
const visualDate = new Date(now);
Expand Down Expand Up @@ -68,7 +81,7 @@ export const useCampanadasTimer = ({ offsetTime }: UseCampanadasTimerProps) => {
const currentChimeIndex = Math.floor(msSinceMidnight / CHIME_INTERVAL);
setChimeCount(currentChimeIndex + 1);
} else if (currentPhase !== AppPhase.CELEBRATION) {
if (currentPhase === AppPhase.COUNTDOWN) {
if (currentPhase === AppPhase.COUNTDOWN) {
setChimeCount(0);
}
}
Expand Down