Skip to content

Commit f3e99e3

Browse files
authored
Merge pull request #120 from ibrahimcesar/claude/restore-progress-bar-015rRWWYfzBcTSHQmY9Hhqqz
Restore reading progress bar with confetti
2 parents 1ff947c + 64f41a7 commit f3e99e3

11 files changed

Lines changed: 406 additions & 0 deletions

File tree

package-lock.json

Lines changed: 11 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
"@docusaurus/preset-classic": "^3.9.2",
2424
"@docusaurus/theme-mermaid": "^3.9.2",
2525
"@mdx-js/react": "^3.1.1",
26+
"canvas-confetti": "^1.9.4",
2627
"clsx": "^2.1.1",
2728
"prism-react-renderer": "^2.4.1",
2829
"react": "^18.3.1",

src/components/CodePlayground.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,8 @@ export default function CodePlayground({
114114
showInlineErrors: true,
115115
showConsole: showConsole,
116116
showConsoleButton: showConsole,
117+
showRefreshButton: true,
118+
showRunButton: true,
117119
editorHeight: height,
118120
autorun: autorun,
119121
autoReload: true,
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
import React, { useState, useEffect } from 'react';
2+
import styles from './styles.module.css';
3+
4+
/**
5+
* Toggle para Modo de Leitura (Distraction-Free)
6+
*
7+
* Botão que esconde/mostra a sidebar para uma leitura sem distrações.
8+
* O estado é persistido no localStorage.
9+
*/
10+
export default function ReadModeToggle() {
11+
const [isReadMode, setIsReadMode] = useState(false);
12+
13+
// Carregar estado do localStorage ao montar
14+
useEffect(() => {
15+
const savedMode = localStorage.getItem('readMode') === 'true';
16+
setIsReadMode(savedMode);
17+
if (savedMode) {
18+
document.documentElement.setAttribute('data-read-mode', 'true');
19+
}
20+
}, []);
21+
22+
const toggleReadMode = () => {
23+
const newMode = !isReadMode;
24+
setIsReadMode(newMode);
25+
localStorage.setItem('readMode', newMode.toString());
26+
27+
if (newMode) {
28+
document.documentElement.setAttribute('data-read-mode', 'true');
29+
} else {
30+
document.documentElement.removeAttribute('data-read-mode');
31+
}
32+
};
33+
34+
return (
35+
<button
36+
className={styles.readModeButton}
37+
onClick={toggleReadMode}
38+
title={isReadMode ? 'Sair do modo leitura' : 'Modo leitura sem distrações'}
39+
aria-label={isReadMode ? 'Sair do modo leitura' : 'Ativar modo leitura'}
40+
type="button"
41+
>
42+
<svg
43+
width="20"
44+
height="20"
45+
viewBox="0 0 20 20"
46+
fill="currentColor"
47+
aria-hidden="true"
48+
>
49+
{isReadMode ? (
50+
// Ícone de olho (modo normal)
51+
<>
52+
<path d="M10 3C5 3 1.73 7.11 1 10c.73 2.89 4 7 9 7s8.27-4.11 9-7c-.73-2.89-4-7-9-7zm0 12c-2.76 0-5-2.24-5-5s2.24-5 5-5 5 2.24 5 5-2.24 5-5 5z" />
53+
<circle cx="10" cy="10" r="3" />
54+
</>
55+
) : (
56+
// Ícone de livro/leitura (modo leitura)
57+
<>
58+
<path d="M10 2c-1.82 0-3.53.5-5 1.35C3.53 2.5 1.82 2 0 2v14c1.82 0 3.53.5 5 1.35 1.47-.85 3.18-1.35 5-1.35s3.53.5 5 1.35c1.47-.85 3.18-1.35 5-1.35V2c-1.82 0-3.53.5-5 1.35C13.53 2.5 11.82 2 10 2zm0 12.5c-1.2 0-2.27-.24-3.2-.64V4.14c.93-.4 2-.64 3.2-.64s2.27.24 3.2.64v9.72c-.93.4-2 .64-3.2.64z" />
59+
</>
60+
)}
61+
</svg>
62+
<span className={styles.buttonText}>
63+
{isReadMode ? 'Mostrar Barra' : 'Modo Leitura'}
64+
</span>
65+
</button>
66+
);
67+
}
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
.readModeButton {
2+
display: inline-flex;
3+
align-items: center;
4+
gap: 6px;
5+
padding: 6px 12px;
6+
border: 1px solid var(--ifm-color-emphasis-300);
7+
border-radius: 6px;
8+
background: transparent;
9+
color: var(--ifm-navbar-link-color);
10+
font-size: 14px;
11+
font-weight: 500;
12+
cursor: pointer;
13+
transition: all 0.2s ease;
14+
white-space: nowrap;
15+
margin-right: 8px;
16+
}
17+
18+
.readModeButton:hover {
19+
background: var(--ifm-color-emphasis-100);
20+
border-color: var(--ifm-color-emphasis-400);
21+
color: var(--ifm-navbar-link-hover-color);
22+
}
23+
24+
.readModeButton:active {
25+
transform: scale(0.95);
26+
}
27+
28+
.buttonText {
29+
display: inline;
30+
}
31+
32+
/* Responsivo: esconder texto em telas pequenas */
33+
@media (max-width: 768px) {
34+
.buttonText {
35+
display: none;
36+
}
37+
38+
.readModeButton {
39+
padding: 8px;
40+
margin-right: 4px;
41+
}
42+
}
43+
44+
/* Estilos para modo escuro */
45+
html[data-theme='dark'] .readModeButton {
46+
border-color: var(--ifm-color-emphasis-300);
47+
}
48+
49+
html[data-theme='dark'] .readModeButton:hover {
50+
background: var(--ifm-color-emphasis-200);
51+
border-color: var(--ifm-color-emphasis-500);
52+
}
Lines changed: 149 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,149 @@
1+
import React, { useState, useEffect, useCallback, useRef } from 'react';
2+
import { useLocation } from '@docusaurus/router';
3+
import confetti from 'canvas-confetti';
4+
import styles from './styles.module.css';
5+
6+
/**
7+
* Caminhos onde a barra de progresso NÃO deve ser exibida
8+
*
9+
* Adicione aqui os paths que são muito curtos ou onde a barra
10+
* de progresso não faz sentido (ex: landing pages, índices, etc)
11+
*/
12+
const EXCLUDED_PATHS = [
13+
'/pt_BR/',
14+
'/pt_BR',
15+
// Adicione mais paths aqui conforme necessário
16+
// Exemplo: '/sobre', '/contato', etc
17+
];
18+
19+
/**
20+
* Barra de Progresso de Leitura
21+
*
22+
* Mostra o progresso de leitura da página atual e dispara confetti
23+
* quando o usuário completa a leitura (chega ao final da página).
24+
*
25+
* A barra não é exibida em páginas definidas em EXCLUDED_PATHS.
26+
*/
27+
export default function ReadingProgressBar() {
28+
const location = useLocation();
29+
const [progress, setProgress] = useState(0);
30+
const hasCompletedRef = useRef(false);
31+
const confettiTimeoutRef = useRef(null);
32+
33+
// Verificar se a página atual está na lista de exclusão
34+
const isExcludedPath = EXCLUDED_PATHS.includes(location.pathname);
35+
36+
const fireConfetti = useCallback(() => {
37+
const duration = 3000;
38+
const animationEnd = Date.now() + duration;
39+
const defaults = {
40+
startVelocity: 30,
41+
spread: 360,
42+
ticks: 60,
43+
zIndex: 10000,
44+
colors: ['#f7df1e', '#FFD700', '#FFA500', '#FF6347']
45+
};
46+
47+
function randomInRange(min, max) {
48+
return Math.random() * (max - min) + min;
49+
}
50+
51+
const interval = setInterval(function() {
52+
const timeLeft = animationEnd - Date.now();
53+
54+
if (timeLeft <= 0) {
55+
return clearInterval(interval);
56+
}
57+
58+
const particleCount = 50 * (timeLeft / duration);
59+
60+
confetti({
61+
...defaults,
62+
particleCount,
63+
origin: { x: randomInRange(0.1, 0.3), y: Math.random() - 0.2 }
64+
});
65+
confetti({
66+
...defaults,
67+
particleCount,
68+
origin: { x: randomInRange(0.7, 0.9), y: Math.random() - 0.2 }
69+
});
70+
}, 250);
71+
}, []);
72+
73+
const handleScroll = useCallback(() => {
74+
// Não processar scroll em páginas excluídas
75+
if (isExcludedPath) return;
76+
77+
const windowHeight = window.innerHeight;
78+
const documentHeight = document.documentElement.scrollHeight;
79+
const scrollTop = window.scrollY || window.pageYOffset || document.documentElement.scrollTop;
80+
81+
// Calcular progresso
82+
const scrollableHeight = documentHeight - windowHeight;
83+
const scrollPercentage = scrollableHeight > 0
84+
? Math.min((scrollTop / scrollableHeight) * 100, 100)
85+
: 0;
86+
87+
setProgress(scrollPercentage);
88+
89+
// Disparar confetti quando completar a leitura (chegou a 95%+)
90+
if (scrollPercentage >= 95 && !hasCompletedRef.current) {
91+
hasCompletedRef.current = true;
92+
93+
// Adicionar pequeno delay para garantir que o usuário realmente chegou ao final
94+
if (confettiTimeoutRef.current) {
95+
clearTimeout(confettiTimeoutRef.current);
96+
}
97+
98+
confettiTimeoutRef.current = setTimeout(() => {
99+
fireConfetti();
100+
}, 300);
101+
}
102+
103+
// Reset se o usuário voltar para o topo
104+
if (scrollPercentage < 10) {
105+
hasCompletedRef.current = false;
106+
}
107+
}, [fireConfetti, isExcludedPath]);
108+
109+
useEffect(() => {
110+
// Adicionar listener de scroll
111+
window.addEventListener('scroll', handleScroll, { passive: true });
112+
113+
// Calcular progresso inicial
114+
handleScroll();
115+
116+
// Cleanup
117+
return () => {
118+
window.removeEventListener('scroll', handleScroll);
119+
if (confettiTimeoutRef.current) {
120+
clearTimeout(confettiTimeoutRef.current);
121+
}
122+
};
123+
}, [handleScroll]);
124+
125+
// Reset quando mudar de página
126+
useEffect(() => {
127+
hasCompletedRef.current = false;
128+
setProgress(0);
129+
}, [location.pathname]);
130+
131+
// Não renderizar a barra em páginas excluídas
132+
if (isExcludedPath) {
133+
return null;
134+
}
135+
136+
return (
137+
<div className={styles.progressBarContainer}>
138+
<div
139+
className={styles.progressBar}
140+
style={{ width: `${progress}%` }}
141+
role="progressbar"
142+
aria-valuenow={Math.round(progress)}
143+
aria-valuemin="0"
144+
aria-valuemax="100"
145+
aria-label={`Progresso de leitura: ${Math.round(progress)}%`}
146+
/>
147+
</div>
148+
);
149+
}
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
.progressBarContainer {
2+
position: fixed;
3+
top: 60px; /* Abaixo da navbar do Docusaurus */
4+
left: 0;
5+
width: 100%;
6+
height: 4px;
7+
background-color: rgba(0, 0, 0, 0.1);
8+
z-index: 9999;
9+
transition: opacity 0.3s ease;
10+
}
11+
12+
html[data-theme='dark'] .progressBarContainer {
13+
background-color: rgba(255, 255, 255, 0.1);
14+
}
15+
16+
.progressBar {
17+
height: 100%;
18+
background: linear-gradient(90deg, #f7df1e 0%, #ffa500 50%, #ff6347 100%);
19+
transition: width 0.2s ease-out;
20+
box-shadow: 0 0 10px rgba(247, 223, 30, 0.5);
21+
}
22+
23+
html[data-theme='dark'] .progressBar {
24+
box-shadow: 0 0 10px rgba(247, 223, 30, 0.7);
25+
}
26+
27+
/* Animação de pulso quando chegando perto do final */
28+
@keyframes pulse {
29+
0%, 100% {
30+
box-shadow: 0 0 10px rgba(247, 223, 30, 0.5);
31+
}
32+
50% {
33+
box-shadow: 0 0 20px rgba(247, 223, 30, 0.8);
34+
}
35+
}
36+
37+
.progressBar[style*="width: 9"],
38+
.progressBar[style*="width: 100"] {
39+
animation: pulse 1.5s ease-in-out infinite;
40+
}

src/css/custom.css

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,9 @@
44
* Este arquivo contém estilos customizados para o tema do Docusaurus.
55
*/
66

7+
/* Importar estilos do modo leitura */
8+
@import './read-mode.css';
9+
710
:root {
811
/* Cores primárias */
912
--ifm-color-primary: #f7df1e;

0 commit comments

Comments
 (0)