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
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]);
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