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.

---
## 🌟 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 |
|----------------|---------------------|
|  |  |
---
## 📁 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 (
);
}
================================================
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}${k}>`).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.
) : (
)}
);
}
================================================
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 (
);
}
================================================
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 (
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);
}