zenhabit / index.html
Sebbe33's picture
Update index.html
ff9acbe verified
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>ZenHabit | Minimal Habit Tracker</title>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
<style>
:root {
--primary: #6366f1;
--primary-light: #818cf8;
--text: #1e293b;
--text-light: #64748b;
--bg: #f8fafc;
--card: #ffffff;
--border: #e2e8f0;
--shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
--success: #10b981;
--warning: #f59e0b;
--danger: #ef4444;
}
.dark-mode {
--primary: #818cf8;
--primary-light: #a5b4fc;
--text: #e2e8f0;
--text-light: #94a3b8;
--bg: #0f172a;
--card: #1e293b;
--border: #334155;
--shadow: 0 1px 3px rgba(0, 0, 0, 0.3);
}
* {
margin: 0;
padding: 0;
box-sizing: border-box;
transition: background-color 0.3s, color 0.3s;
}
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
background-color: var(--bg);
color: var(--text);
line-height: 1.6;
}
.container {
max-width: 800px;
margin: 0 auto;
padding: 2rem;
}
header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 2rem;
}
h1 {
font-size: 1.8rem;
font-weight: 700;
color: var(--primary);
display: flex;
align-items: center;
gap: 0.5rem;
}
.header-controls {
display: flex;
gap: 0.5rem;
align-items: center;
}
.icon-btn {
background: none;
border: none;
color: var(--text-light);
font-size: 1.2rem;
cursor: pointer;
transition: all 0.3s;
padding: 0.25rem;
}
.icon-btn:hover {
color: var(--primary);
transform: rotate(15deg);
}
.stats {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 1rem;
margin-bottom: 2rem;
}
.stat-card {
background-color: var(--card);
border-radius: 0.5rem;
padding: 1rem;
box-shadow: var(--shadow);
text-align: center;
}
.stat-card h3 {
font-size: 0.9rem;
color: var(--text-light);
margin-bottom: 0.5rem;
}
.stat-card p {
font-size: 1.5rem;
font-weight: 700;
color: var(--primary);
}
.habits {
margin-bottom: 2rem;
}
.habits-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 1rem;
}
.habits-header h2 {
font-size: 1.3rem;
}
.add-habit {
background-color: var(--primary);
color: white;
border: none;
border-radius: 0.3rem;
padding: 0.5rem 1rem;
font-size: 0.9rem;
cursor: pointer;
display: flex;
align-items: center;
gap: 0.5rem;
transition: background-color 0.3s;
}
.add-habit:hover {
background-color: var(--primary-light);
}
.habit-list {
display: flex;
flex-direction: column;
gap: 0.5rem;
}
.habit-item {
background-color: var(--card);
border-radius: 0.5rem;
padding: 1rem;
box-shadow: var(--shadow);
display: flex;
align-items: center;
gap: 1rem;
position: relative;
overflow: hidden;
}
.habit-item::before {
content: '';
position: absolute;
top: 0;
left: 0;
height: 100%;
width: 0.3rem;
background-color: var(--primary);
}
.habit-check {
width: 1.5rem;
height: 1.5rem;
border: 2px solid var(--border);
border-radius: 0.3rem;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.3s;
}
.habit-check.checked {
background-color: var(--primary);
border-color: var(--primary);
color: white;
}
.habit-info {
flex: 1;
}
.habit-name {
font-weight: 600;
margin-bottom: 0.2rem;
}
.habit-streak {
font-size: 0.8rem;
color: var(--text-light);
display: flex;
align-items: center;
gap: 0.3rem;
}
.habit-streak i {
color: var(--warning);
}
.habit-progress {
width: 100px;
height: 0.3rem;
background-color: var(--border);
border-radius: 1rem;
overflow: hidden;
}
.progress-bar {
height: 100%;
background-color: var(--primary);
border-radius: 1rem;
transition: width 0.5s ease;
}
.calendar {
background-color: var(--card);
border-radius: 0.5rem;
padding: 1rem;
box-shadow: var(--shadow);
}
.calendar-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 1rem;
}
.calendar-nav {
display: flex;
gap: 1rem;
}
.calendar-nav button {
background: none;
border: none;
color: var(--text-light);
cursor: pointer;
font-size: 1rem;
}
.calendar-nav button:hover {
color: var(--primary);
}
.calendar-grid {
display: grid;
grid-template-columns: repeat(7, 1fr);
gap: 0.5rem;
}
.calendar-day-header {
text-align: center;
font-size: 0.8rem;
color: var(--text-light);
padding: 0.5rem 0;
}
.calendar-day {
aspect-ratio: 1;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
border-radius: 0.3rem;
cursor: pointer;
position: relative;
}
.calendar-day:hover {
background-color: var(--border);
}
.calendar-day.today {
background-color: var(--primary);
color: white;
}
.day-number {
font-size: 0.9rem;
font-weight: 500;
}
.day-habits {
position: absolute;
bottom: 0.2rem;
display: flex;
gap: 0.2rem;
}
.day-habit-dot {
width: 0.3rem;
height: 0.3rem;
border-radius: 50%;
background-color: var(--success);
}
.modal {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.5);
display: flex;
align-items: center;
justify-content: center;
z-index: 1000;
opacity: 0;
pointer-events: none;
transition: opacity 0.3s;
}
.modal.active {
opacity: 1;
pointer-events: all;
}
.modal-content {
background-color: var(--card);
border-radius: 0.5rem;
padding: 1.5rem;
width: 90%;
max-width: 400px;
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.2);
transform: translateY(-20px);
transition: transform 0.3s;
}
.modal.active .modal-content {
transform: translateY(0);
}
.modal-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 1rem;
}
.modal-header h3 {
font-size: 1.2rem;
}
.close-modal {
background: none;
border: none;
font-size: 1.2rem;
color: var(--text-light);
cursor: pointer;
}
.form-group {
margin-bottom: 1rem;
}
.form-group label {
display: block;
margin-bottom: 0.5rem;
font-size: 0.9rem;
color: var(--text-light);
}
.form-group input,
.form-group select {
width: 100%;
padding: 0.5rem;
border: 1px solid var(--border);
border-radius: 0.3rem;
background-color: var(--bg);
color: var(--text);
}
.modal-actions {
display: flex;
justify-content: flex-end;
gap: 0.5rem;
margin-top: 1rem;
}
.btn {
padding: 0.5rem 1rem;
border-radius: 0.3rem;
cursor: pointer;
font-size: 0.9rem;
border: none;
transition: background-color 0.3s;
}
.btn-primary {
background-color: var(--primary);
color: white;
}
.btn-primary:hover {
background-color: var(--primary-light);
}
.btn-secondary {
background-color: var(--border);
color: var(--text);
}
.btn-secondary:hover {
background-color: #d1d5db;
}
@media (max-width: 600px) {
.stats {
grid-template-columns: 1fr;
}
.container {
padding: 1rem;
}
}
@keyframes pulse {
0% { transform: scale(1); }
50% { transform: scale(1.1); }
100% { transform: scale(1); }
}
.habit-check.checked {
animation: pulse 0.3s ease;
}
</style>
</head>
<body>
<div class="container">
<header>
<h1><i class="fas fa-leaf"></i> ZenHabit</h1>
<div class="header-controls">
<button class="icon-btn" id="exportBtn" title="Export Data">
<i class="fas fa-file-export"></i>
</button>
<button class="icon-btn" id="importBtn" title="Import Data">
<i class="fas fa-file-import"></i>
</button>
<button class="icon-btn" id="themeToggle" title="Toggle Theme">
<i class="fas fa-moon"></i>
</button>
</div>
</header>
<div class="stats">
<div class="stat-card">
<h3>Current Streak</h3>
<p id="currentStreak">0</p>
</div>
<div class="stat-card">
<h3>Habits Tracked</h3>
<p id="habitsTracked">0</p>
</div>
<div class="stat-card">
<h3>Completion Rate</h3>
<p id="completionRate">0%</p>
</div>
</div>
<div class="habits">
<div class="habits-header">
<h2>Today's Habits</h2>
<button class="add-habit" id="addHabitBtn">
<i class="fas fa-plus"></i> Add Habit
</button>
</div>
<div class="habit-list" id="habitList">
<!-- Habit items will be added here by JavaScript -->
</div>
</div>
<div class="calendar">
<div class="calendar-header">
<h3 id="currentMonth"></h3>
<div class="calendar-nav">
<button id="prevMonth"><i class="fas fa-chevron-left"></i></button>
<button id="nextMonth"><i class="fas fa-chevron-right"></i></button>
</div>
</div>
<div class="calendar-grid" id="calendarGrid">
<!-- Calendar days will be added here by JavaScript -->
</div>
</div>
</div>
<!-- Add Habit Modal -->
<div class="modal" id="addHabitModal">
<div class="modal-content">
<div class="modal-header">
<h3>Add New Habit</h3>
<button class="close-modal" id="closeModal">&times;</button>
</div>
<form id="habitForm">
<div class="form-group">
<label for="habitName">Habit Name</label>
<input type="text" id="habitName" placeholder="e.g. Drink water" required>
</div>
<div class="form-group">
<label for="habitFrequency">Frequency</label>
<select id="habitFrequency" required>
<option value="daily">Daily</option>
<option value="weekly">Weekly</option>
<option value="monthly">Monthly</option>
</select>
</div>
<div class="modal-actions">
<button type="button" class="btn btn-secondary" id="cancelHabit">Cancel</button>
<button type="submit" class="btn btn-primary">Add Habit</button>
</div>
</form>
</div>
</div>
<input type="file" id="fileInput" hidden accept=".json">
<script>
let habits = [];
let currentDate = new Date();
let currentYear = currentDate.getFullYear();
let currentMonthIndex = currentDate.getMonth();
// DOM Elements
const themeToggle = document.getElementById('themeToggle');
const addHabitBtn = document.getElementById('addHabitBtn');
const addHabitModal = document.getElementById('addHabitModal');
const closeModal = document.getElementById('closeModal');
const cancelHabit = document.getElementById('cancelHabit');
const habitForm = document.getElementById('habitForm');
const habitList = document.getElementById('habitList');
const currentStreak = document.getElementById('currentStreak');
const habitsTracked = document.getElementById('habitsTracked');
const completionRate = document.getElementById('completionRate');
const currentMonth = document.getElementById('currentMonth');
const calendarGrid = document.getElementById('calendarGrid');
const prevMonthBtn = document.getElementById('prevMonth');
const nextMonthBtn = document.getElementById('nextMonth');
// Initialize
document.addEventListener('DOMContentLoaded', () => {
loadData();
renderHabits();
renderCalendar();
updateStats();
if (localStorage.getItem('theme') === 'dark') {
document.body.classList.add('dark-mode');
themeToggle.innerHTML = '<i class="fas fa-sun"></i>';
}
});
// Data handling
function loadData() {
const savedData = localStorage.getItem('habits');
if (savedData) {
try {
habits = JSON.parse(savedData);
} catch (e) {
console.error('Error loading data:', e);
}
}
}
function saveData() {
localStorage.setItem('habits', JSON.stringify(habits));
}
// Import/Export
document.getElementById('exportBtn').addEventListener('click', exportData);
document.getElementById('importBtn').addEventListener('click', () => document.getElementById('fileInput').click());
document.getElementById('fileInput').addEventListener('change', importData);
function exportData() {
const data = JSON.stringify(habits, null, 2);
const blob = new Blob([data], { type: 'application/json' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `zenhabit-${new Date().toISOString().split('T')[0]}.json`;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
}
function importData(e) {
const file = e.target.files[0];
if (!file) return;
const reader = new FileReader();
reader.onload = (event) => {
try {
const imported = JSON.parse(event.target.result);
habits = imported;
saveData();
renderHabits();
updateStats();
renderCalendar();
} catch (error) {
alert('Invalid file format. Please import a valid JSON file.');
}
};
reader.readAsText(file);
}
// Habit management
function getNextId() {
return habits.length > 0 ? Math.max(...habits.map(h => h.id)) + 1 : 1;
}
habitForm.addEventListener('submit', (e) => {
e.preventDefault();
const name = document.getElementById('habitName').value.trim();
const frequency = document.getElementById('habitFrequency').value;
if (!name) return;
habits.push({
id: getNextId(),
name,
streak: 0,
frequency,
progress: 0,
checked: false
});
saveData();
renderHabits();
updateStats();
habitForm.reset();
addHabitModal.classList.remove('active');
});
function renderHabits() {
habitList.innerHTML = habits.map(habit => `
<div class="habit-item">
<div class="habit-check ${habit.checked ? 'checked' : ''}" data-id="${habit.id}">
${habit.checked ? '<i class="fas fa-check"></i>' : ''}
</div>
<div class="habit-info">
<div class="habit-name">${habit.name}</div>
<div class="habit-streak">
<i class="fas fa-fire"></i> ${habit.streak} day streak
</div>
</div>
<div class="habit-progress">
<div class="progress-bar" style="width: ${habit.progress}%"></div>
</div>
</div>
`).join('');
document.querySelectorAll('.habit-check').forEach(checkbox => {
checkbox.addEventListener('click', function() {
const habit = habits.find(h => h.id === parseInt(this.dataset.id));
habit.checked = !habit.checked;
habit.streak = habit.checked ? habit.streak + 1 : Math.max(habit.streak - 1, 0);
habit.progress = Math.min(Math.max(habit.progress + (habit.checked ? 20 : -20), 0), 100);
saveData();
updateStats();
});
});
}
function updateStats() {
const total = habits.length;
const completed = habits.filter(h => h.checked).length;
const rate = total > 0 ? Math.round((completed / total) * 100) : 0;
const maxStreak = habits.reduce((max, h) => Math.max(max, h.streak), 0);
currentStreak.textContent = maxStreak;
habitsTracked.textContent = total;
completionRate.textContent = `${rate}%`;
}
// Calendar
function renderCalendar() {
calendarGrid.innerHTML = '';
const monthName = new Date(currentYear, currentMonthIndex).toLocaleString('default', { month: 'long', year: 'numeric' });
currentMonth.textContent = monthName;
// Day headers
['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'].forEach(day => {
const header = document.createElement('div');
header.className = 'calendar-day-header';
header.textContent = day;
calendarGrid.appendChild(header);
});
// Days
const firstDay = new Date(currentYear, currentMonthIndex, 1).getDay();
const daysInMonth = new Date(currentYear, currentMonthIndex + 1, 0).getDate();
for (let i = 0; i < firstDay; i++) {
calendarGrid.appendChild(createEmptyDay());
}
for (let day = 1; day <= daysInMonth; day++) {
calendarGrid.appendChild(createCalendarDay(day));
}
}
function createEmptyDay() {
const day = document.createElement('div');
day.className = 'calendar-day';
return day;
}
function createCalendarDay(dayNumber) {
const day = document.createElement('div');
day.className = 'calendar-day';
if (new Date(currentYear, currentMonthIndex, dayNumber).toDateString() === new Date().toDateString()) {
day.classList.add('today');
}
const number = document.createElement('div');
number.className = 'day-number';
number.textContent = dayNumber;
day.appendChild(number);
// Add habit dots based on actual data (example implementation)
const completedHabits = Math.random() > 0.5 ? Math.floor(Math.random() * 3) + 1 : 0;
if (completedHabits > 0) {
const dots = document.createElement('div');
dots.className = 'day-habits';
for (let i = 0; i < completedHabits; i++) {
const dot = document.createElement('div');
dot.className = 'day-habit-dot';
dots.appendChild(dot);
}
day.appendChild(dots);
}
return day;
}
// Month navigation
prevMonthBtn.addEventListener('click', () => {
currentMonthIndex--;
if (currentMonthIndex < 0) {
currentMonthIndex = 11;
currentYear--;
}
renderCalendar();
});
nextMonthBtn.addEventListener('click', () => {
currentMonthIndex++;
if (currentMonthIndex > 11) {
currentMonthIndex = 0;
currentYear++;
}
renderCalendar();
});
// Theme toggle
themeToggle.addEventListener('click', () => {
document.body.classList.toggle('dark-mode');
if (document.body.classList.contains('dark-mode')) {
localStorage.setItem('theme', 'dark');
themeToggle.innerHTML = '<i class="fas fa-sun"></i>';
} else {
localStorage.setItem('theme', 'light');
themeToggle.innerHTML = '<i class="fas fa-moon"></i>';
}
});
// Modal controls
addHabitBtn.addEventListener('click', () => addHabitModal.classList.add('active'));
closeModal.addEventListener('click', () => addHabitModal.classList.remove('active'));
cancelHabit.addEventListener('click', () => addHabitModal.classList.remove('active'));
window.addEventListener('click', (e) => e.target === addHabitModal && addHabitModal.classList.remove('active'));
</script>
</body>
</html>