Repository: sadat-rakib/tab-whisperer Files analyzed: 21 Estimated tokens: 8.3k Directory structure: └── sadat-rakib-tab-whisperer/ ├── README.md ├── index.html ├── package.json ├── postcss.config.js ├── tailwind.config.js ├── tsconfig.json ├── vite.config.ts └── src/ ├── App.tsx ├── index.css ├── main.tsx ├── components/ │ ├── AnalyticsDashboard.tsx │ ├── CompletionPrompt.tsx │ ├── ExportData.tsx │ ├── HistoryList.tsx │ ├── IntentModal.tsx │ ├── LoadingScreen.tsx │ ├── SmartSuggestions.tsx │ ├── StatsDashboard.tsx │ ├── WelcomeScreen.tsx │ └── XPConfetti.tsx └── utils/ └── downloadJSON.ts ================================================ FILE: README.md ================================================ # 🚀 Tab Whisperer — A Smarter Way to Browse Tab Whisperer is a productivity-focused web application built to **track and manage browser tab usage** effectively. It empowers users to set intentions, time limits, and reasons for opening tabs — helping reduce digital distraction and improve focus. Designed with a clean UI, analytics dashboard, and goal-setting system. ![Tab Whisperer Preview](./assets/demo-preview.png) --- ## 🌟 Features - 🧠 **Tab Intentions** — Add what tab you want to open and *why*. - ⏱️ **Estimated Time Tracking** — Set a time goal for how long you'll use a tab. - 📊 **Analytics Dashboard** — View usage summaries, completed sessions, and time statistics. - 💡 **Minimal UI** — Clean and intuitive interface built with TailwindCSS and React. - 🔮 **Upcoming (Planned)** — Chrome Extension to monitor real tab activity in real-time. --- ## 🔧 Tech Stack | Tech | Purpose | |--------------|--------------------------------| | **React** | UI library | | **TypeScript** | Static typing | | **Vite** | Fast dev server + bundler | | **Tailwind CSS** | Styling framework | | **Chart.js** | Graphs and visualizations | | **Heroicons** | UI icons | --- ## 📸 Screenshots | Main Interface | Analytics Dashboard | |----------------|---------------------| | ![Main](./assets/main-ui.png) | ![Dashboard](./assets/dashboard.png) | --- ## 📁 Project Structure ``` tab-whisperer/ ├── public/ ├── src/ │ ├── components/ │ ├── pages/ │ ├── utils/ │ ├── App.tsx │ └── main.tsx ├── tailwind.config.js ├── vite.config.ts ├── package.json └── README.md ``` --- ## 🛠️ Installation Clone the repo and install dependencies: ```bash git clone https://github.com/Sadat-Rakib/tab-whisperer.git cd tab-whisperer npm install npm run dev ``` --- ## 📦 Upcoming Update: Chrome Extension 🚀 > **Planned Feature** > A browser extension to track actual tabs, send activity to the dashboard, and monitor usage automatically. Will use Chrome Extension APIs + background scripts + local storage. --- ## 🧠 Inspiration Digital overconsumption is real. Tab Whisperer aims to tackle digital multitasking by **introducing accountability and purpose** behind every tab opened. --- ## 🤝 Contributing Contributions are welcome! Open an issue or submit a PR. All ideas to enhance productivity are appreciated. --- ## 🪪 License MIT © [Sadat Rakib](https://github.com/Sadat-Rakib) --- ## 📫 Contact - **GitHub**: [@Sadat-Rakib](https://github.com/Sadat-Rakib) - **LinkedIn**: [Sadat Rakib](https://linkedin.com/in/sadat-rakib) - **Portfolio**: *Coming Soon* --- > ⭐ *If you like this project, consider starring the repo to support future development!* ================================================ FILE: index.html ================================================ Tab Whisperer
================================================ FILE: package.json ================================================ { "homepage": "https://Sadat-Rakib.github.io/tab-whisperer", "name": "tab-whisperer", "version": "1.0.0", "private": true, "type": "module", "scripts": { "dev": "vite", "build": "vite build", "preview": "vite preview", "predeploy": "npm run build", "deploy": "gh-pages -d dist" }, "dependencies": { "@vitejs/plugin-react": "^4.4.1", "canvas-confetti": "^1.9.3", "chart.js": "^4.4.9", "classnames": "^2.3.2", "lottie-react": "^2.4.0", "lucide-react": "^0.263.0", "react": "^18.2.0", "react-chartjs-2": "^5.3.0", "react-confetti": "^6.1.0", "react-dom": "^18.2.0", "recharts": "^2.7.2", "uuid": "^11.1.0" }, "devDependencies": { "@types/react": "^18.0.38", "@types/react-dom": "^18.0.11", "autoprefixer": "^10.4.14", "gh-pages": "^6.3.0", "postcss": "^8.4.21", "tailwindcss": "^3.3.2", "typescript": "^5.1.6", "vite": "^4.4.9" } } ================================================ FILE: postcss.config.js ================================================ export default { plugins: { tailwindcss: {}, autoprefixer: {}, }, } ================================================ FILE: tailwind.config.js ================================================ /** @type {import('tailwindcss').Config} */ module.exports = { content: [ "./index.html", "./src/**/*.{js,ts,jsx,tsx}", ], theme: { extend: { animation: { 'fade-in': 'fade-in 0.6s ease-out forwards', 'fade-in-slow': 'fade-in-slow 1.2s ease-out forwards', 'pulse-slow': 'pulse-slow 4s infinite', 'backgroundShift': 'backgroundShift 15s ease infinite', 'moveStars': 'moveStars 60s linear infinite', }, keyframes: { 'fade-in': { from: { opacity: '0', transform: 'scale(0.95)' }, to: { opacity: '1', transform: 'scale(1)' }, }, 'fade-in-slow': { from: { opacity: '0', transform: 'translateY(10px)' }, to: { opacity: '1', transform: 'translateY(0)' }, }, 'pulse-slow': { '0%, 100%': { opacity: '0.3', transform: 'scale(1)' }, '50%': { opacity: '0.6', transform: 'scale(1.05)' }, }, backgroundShift: { '0%': { backgroundPosition: '0% 50%' }, '50%': { backgroundPosition: '100% 50%' }, '100%': { backgroundPosition: '0% 50%' }, }, moveStars: { '0%': { backgroundPosition: '0 0' }, '100%': { backgroundPosition: '-1000px 1000px' }, }, }, colors: { // Optional: custom color names 'tw-purple': '#8b5cf6', 'tw-indigo': '#6366f1', 'tw-dark': '#0f172a', }, }, }, plugins: [], } ================================================ FILE: tsconfig.json ================================================ { "compilerOptions": { "target": "ESNext", "useDefineForClassFields": true, "lib": [ "DOM", "DOM.Iterable", "ESNext" ], "allowJs": false, "skipLibCheck": true, "esModuleInterop": false, "allowSyntheticDefaultImports": true, "strict": true, "forceConsistentCasingInFileNames": true, "module": "ESNext", "moduleResolution": "Node", "resolveJsonModule": true, "isolatedModules": true, "noEmit": true, "jsx": "react-jsx" }, "include": [ "src" ] } ================================================ FILE: vite.config.ts ================================================ import { defineConfig } from "vite"; import react from "@vitejs/plugin-react"; export default defineConfig({ plugins: [react()], }); ================================================ FILE: src/App.tsx ================================================ import React, { useEffect, useState } from "react"; import IntentModal from "./components/IntentModal"; import HistoryList from "./components/HistoryList"; import CompletionPrompt from "./components/CompletionPrompt"; import StatsDashboard from "./components/StatsDashboard"; import WelcomeScreen from "./components/WelcomeScreen"; import LoadingScreen from "./components/LoadingScreen"; import SmartSuggestions from "./components/SmartSuggestions"; import XPConfetti from "./components/XPConfetti"; import AnalyticsDashboard from "./components/AnalyticsDashboard"; import ExportData from "./components/ExportData"; import { Sparkles, Activity, Star, Target } from "lucide-react"; const XP_KEY = "tabXP"; const LOG_KEY = "tabLogs"; const THEME_KEY = "theme"; function addXP(amount: number) { const current = parseInt(localStorage.getItem(XP_KEY) || "0"); localStorage.setItem(XP_KEY, (current + amount).toString()); } function App() { const [showModal, setShowModal] = useState(false); const [showPrompt, setShowPrompt] = useState(false); const [activeLogId, setActiveLogId] = useState(null); const [showWelcome, setShowWelcome] = useState(true); const [logs, setLogs] = useState([]); const [loading, setLoading] = useState(true); const [showConfetti, setShowConfetti] = useState(false); const [isDarkMode, setIsDarkMode] = useState(localStorage.getItem(THEME_KEY) === "dark"); useEffect(() => { const timer = setTimeout(() => setLoading(false), 1800); return () => clearTimeout(timer); }, []); useEffect(() => { const storedLogs = JSON.parse(localStorage.getItem(LOG_KEY) || "[]"); setLogs(storedLogs); const latest = storedLogs[storedLogs.length - 1]; if (latest && latest.justOpened) { latest.justOpened = false; localStorage.setItem(LOG_KEY, JSON.stringify(storedLogs)); return; } if (latest && !latest.completed && !latest.promptShown) { setActiveLogId(latest.id); storedLogs[storedLogs.length - 1].promptShown = true; localStorage.setItem(LOG_KEY, JSON.stringify(storedLogs)); setTimeout(() => { setShowPrompt(true); }, (latest.estMinutes || 1) * 60 * 1000); } }, []); const handleModalClose = () => { setShowModal(false); const updatedLogs = JSON.parse(localStorage.getItem(LOG_KEY) || "[]"); setLogs(updatedLogs); }; const toggleTheme = () => { setIsDarkMode((prev) => { const newTheme = !prev ? "dark" : "light"; localStorage.setItem(THEME_KEY, newTheme); return !prev; }); }; if (loading) return ; if (showWelcome) { return (
setShowWelcome(false)} />
); } return (
{showConfetti && }

Tab Whisperer

Your Focus, Tracked. Your XP, Earned.

XP: {localStorage.getItem(XP_KEY) || "0"}
{showModal && } {showPrompt && activeLogId && ( setShowPrompt(false)} /> )}
{logs.length === 0 ? (
No sessions yet? Start mastering your browsing flow 💡
) : ( )}

Total Sessions

{logs.length}

Completed

{logs.filter((l) => l.completed).length}

Completion Rate

{logs.length === 0 ? "0%" : `${Math.round((logs.filter((l) => l.completed).length / logs.length) * 100)}%`}

); } export default App; ================================================ FILE: src/index.css ================================================ @tailwind base; @tailwind components; @tailwind utilities; /* ✅ Scrollbar Styling */ ::-webkit-scrollbar { width: 8px; } ::-webkit-scrollbar-track { background: #111; } ::-webkit-scrollbar-thumb { background-color: #6366f1; border-radius: 6px; } /* ✅ Fade-in Animation */ @keyframes fade-in { from { opacity: 0; transform: scale(0.95); } to { opacity: 1; transform: scale(1); } } .animate-fade-in { animation: fade-in 0.6s ease-out forwards; } @keyframes fade-in-slow { from { opacity: 0; transform: translateY(10px); } to { opacity: 1; transform: translateY(0); } } .animate-fade-in-slow { animation: fade-in-slow 1.2s ease-out forwards; } /* ✅ Neon Glow Text */ .glow-text { text-shadow: 0 0 8px #a855f7, 0 0 12px #a855f7, 0 0 16px #a855f7; } /* ✅ Glow Buttons */ .glow-button { box-shadow: 0 0 6px #8b5cf6, 0 0 14px #8b5cf6; transition: all 0.3s ease-in-out; } .glow-button:hover { transform: scale(1.05); box-shadow: 0 0 10px #c084fc, 0 0 20px #c084fc; } /* ✅ Glow Card (used in stats boxes) */ .glow-card { border: 1px solid rgba(255, 255, 255, 0.05); backdrop-filter: blur(4px); } /* ✅ Futuristic Background Animation */ @keyframes backgroundShift { 0% { background-position: 0% 50%; } 50% { background-position: 100% 50%; } 100% { background-position: 0% 50%; } } .bg-animated { background: linear-gradient(270deg, #3b0764, #4c1d95, #0f172a); background-size: 600% 600%; animation: backgroundShift 15s ease infinite; } /* ✅ Slow Pulse Effect */ @keyframes pulse-slow { 0%, 100% { opacity: 0.3; transform: scale(1); } 50% { opacity: 0.6; transform: scale(1.05); } } .animate-pulse-slow { animation: pulse-slow 4s infinite; } /* ✅ Starfield Background Effect */ .bg-particles::before { content: ""; position: absolute; inset: 0; z-index: 0; background-image: radial-gradient(white 1px, transparent 1px); background-size: 20px 20px; animation: moveStars 60s linear infinite; opacity: 0.03; } @keyframes moveStars { 0% { background-position: 0 0; } 100% { background-position: -1000px 1000px; } } /* ✅ Dot Pattern */ .bg-dot-pattern { background-image: radial-gradient(#6366f1 0.5px, transparent 0.5px); background-size: 18px 18px; } ================================================ FILE: src/main.tsx ================================================ import React from 'react'; import ReactDOM from 'react-dom/client'; import App from './App.tsx'; import './index.css'; ReactDOM.createRoot(document.getElementById('root')!).render( ); ================================================ FILE: src/components/AnalyticsDashboard.tsx ================================================ import React, { useEffect, useState } from "react"; import { Bar } from "react-chartjs-2"; import { Chart, BarElement, CategoryScale, LinearScale, Tooltip, Legend, } from "chart.js"; Chart.register(BarElement, CategoryScale, LinearScale, Tooltip, Legend); const LOG_KEY = "tabLogs"; export default function AnalyticsDashboard() { const [weeklyData, setWeeklyData] = useState([]); useEffect(() => { const logs = JSON.parse(localStorage.getItem(LOG_KEY) || "[]"); const data = Array(7).fill(0); // Sun to Sat const today = new Date(); logs.forEach((log: any) => { const date = new Date(log.createdAt); const dayDiff = Math.floor((today.getTime() - date.getTime()) / (1000 * 60 * 60 * 24)); if (dayDiff >= 0 && dayDiff < 7) { const dayIndex = (today.getDay() - dayDiff + 7) % 7; // map to Sun-Sat (0-6) data[dayIndex]++; } }); setWeeklyData(data); }, []); const labels = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"]; const chartData = { labels, datasets: [ { label: "Focus Sessions", data: weeklyData, backgroundColor: weeklyData.map((count) => count > 0 ? "#8b5cf6" : "#3f3f46" ), borderRadius: 6, }, ], }; const options = { responsive: true, plugins: { legend: { display: true, labels: { color: "#d4d4d8", }, }, }, scales: { x: { ticks: { color: "#d4d4d8", }, grid: { color: "#27272a", }, }, y: { ticks: { color: "#d4d4d8", }, grid: { color: "#27272a", }, }, }, }; return (

📊 Weekly Activity

); } ================================================ FILE: src/components/CompletionPrompt.tsx ================================================ type Props = { logId: number; onClose: () => void; }; export default function CompletionPrompt({ logId, onClose }: Props) { const handleResponse = (completed: boolean) => { const logs = JSON.parse(localStorage.getItem("tabLogs") || "[]"); const index = logs.findIndex((log: any) => log.id === logId); if (index !== -1) { logs[index].completed = completed; localStorage.setItem("tabLogs", JSON.stringify(logs)); if (completed) { const XP_KEY = "tabXP"; const currentXP = parseInt(localStorage.getItem(XP_KEY) || "0"); localStorage.setItem(XP_KEY, (currentXP + 10).toString()); } } onClose(); }; return (

Did you complete your task?

); } ================================================ FILE: src/components/ExportData.tsx ================================================ import React from "react"; const LOG_KEY = "tabLogs"; export default function ExportData() { const handleExport = (format: "json" | "csv" | "xml") => { const logs = JSON.parse(localStorage.getItem(LOG_KEY) || "[]"); let data: string; let type: string; let extension: string; if (format === "json") { data = JSON.stringify(logs, null, 2); type = "application/json"; extension = "json"; } else if (format === "csv") { const keys = Object.keys(logs[0] || {}); const rows = logs.map((log: any) => keys.map(k => `"${log[k] || ""}"`).join(",")); data = `${keys.join(",")}\n${rows.join("\n")}`; type = "text/csv"; extension = "csv"; } else { data = `\n${logs.map(log => `\n${Object.entries(log).map(([k, v]) => ` <${k}>${v}`).join("\n")}\n`).join("\n") }\n`; type = "application/xml"; extension = "xml"; } const blob = new Blob([data], { type }); const url = URL.createObjectURL(blob); const a = document.createElement("a"); a.href = url; a.download = `tab-logs.${extension}`; a.click(); }; return (
{["json", "csv", "xml"].map(format => ( ))}
); } ================================================ FILE: src/components/HistoryList.tsx ================================================ export default function HistoryList() { const logs = JSON.parse(localStorage.getItem("tabLogs") || "[]").reverse(); return (

History

{logs.length === 0 ? (

No tab sessions logged yet.

) : (
    {logs.map((log: any) => (
  • {new Date(log.timestamp).toLocaleString()}
    {log.description}
    {log.url}
    {log.completed ? ( ✔ Done ) : ( ✘ Skipped )}
  • ))}
)}
); } ================================================ FILE: src/components/IntentModal.tsx ================================================ import React, { useState, useEffect } from "react"; import { v4 as uuidv4 } from "uuid"; const LOG_KEY = "tabLogs"; type Props = { onClose: () => void; }; export default function IntentModal({ onClose }: Props) { const [tabName, setTabName] = useState(""); const [reason, setReason] = useState(""); const [minutes, setMinutes] = useState(5); const handleStart = () => { if (!tabName) return; const newLog = { id: uuidv4(), tabName, reason, estMinutes: minutes, createdAt: new Date().toISOString(), completed: false, justOpened: true, }; const logs = JSON.parse(localStorage.getItem(LOG_KEY) || "[]"); logs.push(newLog); localStorage.setItem(LOG_KEY, JSON.stringify(logs)); window.open("https://" + tabName, "_blank"); // Optional: Open the tab directly onClose(); }; return (

New Intent

{/* Tab Name Input */}
setTabName(e.target.value)} />
{/* Reason Input */}
setReason(e.target.value)} />
{/* Time Input */}
setMinutes(Number(e.target.value))} />
{/* Buttons */}
); } ================================================ FILE: src/components/LoadingScreen.tsx ================================================ import React from "react"; import { Loader2 } from "lucide-react"; export default function LoadingScreen() { return (

Initializing Tab Whisperer...

Loading your focused universe 🌌

); } ================================================ FILE: src/components/SmartSuggestions.tsx ================================================ import React, { useEffect, useState } from "react"; const LOG_KEY = "tabLogs"; export default function SmartSuggestions() { const [suggestions, setSuggestions] = useState([]); useEffect(() => { const logs = JSON.parse(localStorage.getItem(LOG_KEY) || "[]"); const titles = logs.map((log: any) => log.title?.trim()).filter(Boolean); const frequency: Record = {}; titles.forEach(title => { frequency[title] = (frequency[title] || 0) + 1; }); const sorted = Object.entries(frequency) .sort((a, b) => b[1] - a[1]) .slice(0, 5) .map(entry => entry[0]); setSuggestions(sorted); }, []); if (suggestions.length === 0) return null; return (

Smart Suggestions

Based on your most frequent focus sessions:

    {suggestions.map((s, i) => (
  • {s}
  • ))}
); } ================================================ FILE: src/components/StatsDashboard.tsx ================================================ export default function StatsDashboard() { const logs = JSON.parse(localStorage.getItem("tabLogs") || "[]"); const totalSessions = logs.length; const completedSessions = logs.filter((log: any) => log.completed).length; const failedSessions = totalSessions - completedSessions; const completionRate = totalSessions ? Math.round((completedSessions / totalSessions) * 100) : 0; return (

📊 Your Stats

Total Sessions
{totalSessions}
Completed
{completedSessions}
Completion Rate
{completionRate}%
); } ================================================ FILE: src/components/WelcomeScreen.tsx ================================================ type Props = { onStart: () => void; }; export default function WelcomeScreen({ onStart }: Props) { return (

Welcome to Tab Whisperer

Enhance your browsing mindfulness. Log your tab intent and gain XP for staying focused.

); } ================================================ FILE: src/components/XPConfetti.tsx ================================================ import React, { useEffect, useRef } from "react"; import confetti from "canvas-confetti"; export default function XPConfetti() { const ref = useRef(null); useEffect(() => { if (!ref.current) return; const myConfetti = confetti.create(ref.current, { resize: true }); myConfetti({ particleCount: 100, spread: 160, origin: { y: 0.6 }, colors: ["#facc15", "#a855f7", "#34d399"], }); setTimeout(() => ref.current?.remove(), 1500); }, []); return ( ); } ================================================ FILE: src/utils/downloadJSON.ts ================================================ export function downloadJSON(data: object, filename = "tab-logs.json") { const blob = new Blob([JSON.stringify(data, null, 2)], { type: "application/json", }); const url = URL.createObjectURL(blob); const a = document.createElement("a"); a.href = url; a.download = filename; a.click(); URL.revokeObjectURL(url); }