commit 08ed4906cc842b3c4b7e14bf589b75d18154cef7 Author: jafreli Date: Sat Jul 12 00:33:53 2025 +0200 test impl diff --git a/app.py b/app.py new file mode 100644 index 0000000..cb2692e --- /dev/null +++ b/app.py @@ -0,0 +1,99 @@ +from flask import Flask, render_template, request, jsonify +from tinydb import TinyDB, Query +from datetime import datetime, timedelta + +app = Flask(__name__) +db = TinyDB('habits.json') +Habit = Query() + +# Helper function to get the current date string +def get_current_date(): + return datetime.now().strftime('%Y-%m-%d') + +# Helper function to get a list of dates for the last 30 days +def get_last_30_days(): + dates = [] + for i in range(30): + date = datetime.now() - timedelta(days=i) + dates.append(date.strftime('%Y-%m-%d')) + return dates[::-1] # Reverse to have oldest first + +@app.route('/') +def index(): + return render_template('index.html') + +@app.route('/habits', methods=['GET']) +def get_habits(): + habits_data = db.all() + # Add completion status for the last 30 days + last_30_days = get_last_30_days() + for habit in habits_data: + habit['completion_history'] = {} + for date_str in last_30_days: + habit['completion_history'][date_str] = date_str in habit.get('completed_dates', []) + return jsonify(habits_data) + +@app.route('/habits', methods=['POST']) +def add_habit(): + data = request.json + name = data.get('name') + if not name: + return jsonify({'error': 'Habit name is required'}), 400 + + habit_id = db.insert({'name': name, 'completed_dates': []}) + return jsonify({'id': habit_id, 'name': name, 'completed_dates': []}), 201 + +@app.route('/habits//complete', methods=['POST']) +def complete_habit(habit_id): + date_to_complete = request.json.get('date', get_current_date()) + + habit = db.get(doc_id=habit_id) + if not habit: + return jsonify({'error': 'Habit not found'}), 404 + + if date_to_complete not in habit['completed_dates']: + habit['completed_dates'].append(date_to_complete) + db.update({'completed_dates': habit['completed_dates']}, doc_ids=[habit_id]) + + return jsonify({'message': f'Habit {habit_id} marked as completed for {date_to_complete}', 'completed_dates': habit['completed_dates']}) + +@app.route('/habits//uncomplete', methods=['POST']) +def uncomplete_habit(habit_id): + date_to_uncomplete = request.json.get('date', get_current_date()) + + habit = db.get(doc_id=habit_id) + if not habit: + return jsonify({'error': 'Habit not found'}), 404 + + if date_to_uncomplete in habit['completed_dates']: + habit['completed_dates'].remove(date_to_uncomplete) + db.update({'completed_dates': habit['completed_dates']}, doc_ids=[habit_id]) + + return jsonify({'message': f'Habit {habit_id} marked as uncompleted for {date_to_uncomplete}', 'completed_dates': habit['completed_dates']}) + +@app.route('/habits/', methods=['PUT']) +def update_habit(habit_id): + data = request.json + name = data.get('name') + + if not name: + return jsonify({'error': 'Habit name is required'}), 400 + + habit = db.get(doc_id=habit_id) + if not habit: + return jsonify({'error': 'Habit not found'}), 404 + + db.update({'name': name}, doc_ids=[habit_id]) + return jsonify({'message': f'Habit {habit_id} updated', 'name': name}) + +@app.route('/habits/', methods=['DELETE']) +def delete_habit(habit_id): + habit = db.get(doc_id=habit_id) + if not habit: + return jsonify({'error': 'Habit not found'}), 404 + + db.remove(doc_ids=[habit_id]) + return jsonify({'message': f'Habit {habit_id} deleted'}) + +if __name__ == '__main__': + app.run(debug=True) \ No newline at end of file diff --git a/habits.json b/habits.json new file mode 100644 index 0000000..a373537 --- /dev/null +++ b/habits.json @@ -0,0 +1 @@ +{"_default": {"1": {"name": "Zocken", "completed_dates": []}, "2": {"name": "Gehen", "completed_dates": []}}} \ No newline at end of file diff --git a/static/css/style.css b/static/css/style.css new file mode 100644 index 0000000..a0d1e18 --- /dev/null +++ b/static/css/style.css @@ -0,0 +1,230 @@ +body { + font-family: Arial, sans-serif; + background-color: #1a1a1a; + color: #e0e0e0; + margin: 0; + padding: 20px; + display: flex; + justify-content: center; +} + +.container { + width: 100%; + max-width: 600px; + background-color: #2a2a2a; + border-radius: 10px; + padding: 20px; + box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2); +} + +h1 { + color: #4CAF50; + text-align: center; + margin-bottom: 20px; +} + +.add-habit { + display: flex; + margin-bottom: 20px; +} + +.add-habit input[type="text"] { + flex-grow: 1; + padding: 10px; + border: 1px solid #444; + border-radius: 5px; + background-color: #333; + color: #e0e0e0; + margin-right: 10px; +} + +.add-habit button { + padding: 10px 15px; + background-color: #4CAF50; + color: white; + border: none; + border-radius: 5px; + cursor: pointer; + font-size: 16px; +} + +.add-habit button:hover { + background-color: #45a049; +} + +.habit-item { + background-color: #333; + border-radius: 8px; + margin-bottom: 15px; + padding: 15px; + display: flex; + flex-direction: column; + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); +} + +.habit-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 10px; +} + +.habit-name { + font-size: 1.2em; + font-weight: bold; + color: #e0e0e0; +} + +.habit-actions button { + background: none; + border: none; + color: #e0e0e0; + font-size: 1.5em; + cursor: pointer; +} + +.habit-actions button:hover { + color: #666; +} + +.date-grid { + display: grid; + grid-template-columns: repeat(30, 1fr); /* Adjust based on your date range */ + gap: 2px; + padding: 5px 0; + overflow-x: auto; /* Enable horizontal scrolling if dates exceed width */ + -ms-overflow-style: none; /* IE and Edge */ + scrollbar-width: none; /* Firefox */ +} + +/* Hide scrollbar for Chrome, Safari and Opera */ +.date-grid::-webkit-scrollbar { + display: none; +} + +.date-square { + width: 15px; /* Adjust size as needed */ + height: 15px; + background-color: #555; + border-radius: 2px; + cursor: pointer; +} + +.date-square.completed { + background-color: #4CAF50; +} + +/* Modal Styles */ +.modal { + display: none; /* Hidden by default */ + position: fixed; /* Stay in place */ + z-index: 1; /* Sit on top */ + left: 0; + top: 0; + width: 100%; /* Full width */ + height: 100%; /* Full height */ + overflow: auto; /* Enable scroll if needed */ + background-color: rgba(0,0,0,0.4); /* Black w/ opacity */ + justify-content: center; + align-items: center; +} + +.modal-content { + background-color: #2a2a2a; + margin: auto; + padding: 30px; + border-radius: 10px; + width: 80%; + max-width: 500px; + box-shadow: 0 5px 15px rgba(0,0,0,0.3); + position: relative; +} + +.close-button { + color: #aaa; + float: right; + font-size: 28px; + font-weight: bold; + position: absolute; + top: 10px; + right: 20px; + cursor: pointer; +} + +.close-button:hover, +.close-button:focus { + color: white; + text-decoration: none; +} + +#modalHabitName { + text-align: center; + color: #4CAF50; + margin-bottom: 20px; +} + +.date-grid-modal { + display: grid; + grid-template-columns: repeat(7, 1fr); /* 7 days a week */ + gap: 5px; + margin-bottom: 20px; +} + +.date-cell { + text-align: center; + padding: 8px; + border-radius: 5px; + background-color: #444; + cursor: pointer; + color: #e0e0e0; +} + +.date-cell.completed { + background-color: #4CAF50; +} + +.date-cell.today { + border: 2px solid #007bff; /* Highlight today */ +} + +.modal-actions { + display: flex; + justify-content: space-around; + gap: 10px; +} + +.modal-actions button { + padding: 10px 20px; + border: none; + border-radius: 5px; + cursor: pointer; + font-size: 16px; + flex-grow: 1; +} + +#modalCompleteTodayBtn { + background-color: #4CAF50; + color: white; +} + +#modalCompleteTodayBtn:hover { + background-color: #45a049; +} + +#modalEditBtn { + background-color: #007bff; + color: white; +} + +#modalEditBtn:hover { + background-color: #0056b3; +} + +#modalDeleteBtn { + background-color: #dc3545; + color: white; +} + +#modalDeleteBtn:hover { + background-color: #c82333; +} \ No newline at end of file diff --git a/static/js/script.js b/static/js/script.js new file mode 100644 index 0000000..4051518 --- /dev/null +++ b/static/js/script.js @@ -0,0 +1,247 @@ +document.addEventListener('DOMContentLoaded', fetchHabits); + +const habitListDiv = document.getElementById('habitList'); +const dateModal = document.getElementById('dateModal'); +const modalHabitName = document.getElementById('modalHabitName'); +const modalDateGrid = document.getElementById('modalDateGrid'); +const modalCompleteTodayBtn = document.getElementById('modalCompleteTodayBtn'); + +let currentHabitId = null; + +function getCurrentDate() { + const today = new Date(); + const year = today.getFullYear(); + const month = String(today.getMonth() + 1).padStart(2, '0'); + const day = String(today.getDate()).padStart(2, '0'); + return `${year}-${month}-${day}`; +} + +function getPastDates(days) { + const dates = []; + for (let i = days - 1; i >= 0; i--) { + const d = new Date(); + d.setDate(d.getDate() - i); + const year = d.getFullYear(); + const month = String(d.getMonth() + 1).padStart(2, '0'); + const day = String(d.getDate()).padStart(2, '0'); + dates.push(`${year}-${month}-${day}`); + } + return dates; +} + +async function fetchHabits() { + try { + const response = await fetch('/habits'); + const habits = await response.json(); + renderHabits(habits); + } catch (error) { + console.error('Error fetching habits:', error); + } +} + +function renderHabits(habits) { + habitListDiv.innerHTML = ''; // Clear previous habits + const last30Days = getPastDates(30); + + habits.forEach(habit => { + const habitItem = document.createElement('div'); + habitItem.className = 'habit-item'; + habitItem.dataset.id = habit.id; // Store habit ID + + const habitHeader = document.createElement('div'); + habitHeader.className = 'habit-header'; + + const habitName = document.createElement('div'); + habitName.className = 'habit-name'; + habitName.textContent = habit.name; + habitHeader.appendChild(habitName); + + const habitActions = document.createElement('div'); + habitActions.className = 'habit-actions'; + const openModalButton = document.createElement('button'); + openModalButton.innerHTML = '✔'; // Checkmark symbol + openModalButton.onclick = () => openHabitModal(habit.id, habit.name, habit.completed_dates); + habitActions.appendChild(openModalButton); + habitHeader.appendChild(habitActions); + habitItem.appendChild(habitHeader); + + const dateGrid = document.createElement('div'); + dateGrid.className = 'date-grid'; + + last30Days.forEach(date => { + const dateSquare = document.createElement('div'); + dateSquare.className = 'date-square'; + if (habit.completed_dates && habit.completed_dates.includes(date)) { + dateSquare.classList.add('completed'); + } + dateSquare.title = date; // Show date on hover + dateGrid.appendChild(dateSquare); + }); + habitItem.appendChild(dateGrid); + + habitListDiv.appendChild(habitItem); + }); +} + +async function addHabit() { + const newHabitNameInput = document.getElementById('newHabitName'); + const name = newHabitNameInput.value.trim(); + + if (name) { + try { + const response = await fetch('/habits', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ name: name }) + }); + const newHabit = await response.json(); + if (response.ok) { + newHabitNameInput.value = ''; + fetchHabits(); // Re-fetch and re-render all habits + } else { + console.error('Failed to add habit:', newHabit.error); + } + } catch (error) { + console.error('Error adding habit:', error); + } + } +} + +async function openHabitModal(habitId, habitName, completedDates) { + currentHabitId = habitId; + modalHabitName.textContent = habitName; + modalCompleteTodayBtn.dataset.habitId = habitId; // Store habit ID for button + + modalDateGrid.innerHTML = ''; + const today = getCurrentDate(); + const startOfWeek = new Date(); + startOfWeek.setDate(startOfWeek.getDate() - (startOfWeek.getDay() + 6) % 7); // Go back to Monday + + for (let i = 0; i < 35; i++) { // Display 5 weeks (5 * 7 days) + const d = new Date(startOfWeek); + d.setDate(startOfWeek.getDate() + i); + const dateStr = `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}`; + + const dateCell = document.createElement('div'); + dateCell.className = 'date-cell'; + dateCell.textContent = d.getDate(); + if (completedDates.includes(dateStr)) { + dateCell.classList.add('completed'); + } + if (dateStr === today) { + dateCell.classList.add('today'); + } + dateCell.dataset.date = dateStr; + dateCell.onclick = (event) => toggleCompletionForDate(event, habitId, dateStr); + modalDateGrid.appendChild(dateCell); + } + + dateModal.style.display = 'flex'; // Show the modal +} + +function closeModal() { + dateModal.style.display = 'none'; + currentHabitId = null; + fetchHabits(); // Refresh habits after closing modal +} + +async function toggleCompletionForDate(event, habitId, date) { + const cell = event.target; + let response; + let data; + + if (cell.classList.contains('completed')) { + // Mark as uncompleted + response = await fetch(`/habits/${habitId}/uncomplete`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ date: date }) + }); + data = await response.json(); + if (response.ok) { + cell.classList.remove('completed'); + // Update the completion_history in the rendered habits + // (You might need to re-fetch or update the local habit data for the main view) + } + } else { + // Mark as completed + response = await fetch(`/habits/${habitId}/complete`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ date: date }) + }); + data = await response.json(); + if (response.ok) { + cell.classList.add('completed'); + // Update the completion_history + } + } +} + + +async function completeHabitForDate(event, habitId, date) { + try { + const response = await fetch(`/habits/${habitId}/complete`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ date: date }) + }); + const data = await response.json(); + if (response.ok) { + // Update the modal's date grid immediately + const dateCells = modalDateGrid.querySelectorAll('.date-cell'); + dateCells.forEach(cell => { + if (cell.dataset.date === date) { + cell.classList.add('completed'); + } + }); + // Optionally, disable the "Today erledigen" button if already completed + // modalCompleteTodayBtn.disabled = true; + } else { + console.error('Failed to complete habit:', data.error); + } + } catch (error) { + console.error('Error completing habit:', error); + } +} + +async function editHabitName() { + const newName = prompt("Neuen Namen für die Gewohnheit eingeben:", modalHabitName.textContent); + if (newName && newName.trim() !== "") { + try { + const response = await fetch(`/habits/${currentHabitId}`, { + method: 'PUT', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ name: newName.trim() }) + }); + const data = await response.json(); + if (response.ok) { + modalHabitName.textContent = newName.trim(); + fetchHabits(); // Re-fetch and re-render all habits to update the main view + } else { + console.error('Failed to update habit:', data.error); + } + } catch (error) { + console.error('Error updating habit:', error); + } + } +} + +async function deleteHabitFromModal() { + if (confirm("Bist du sicher, dass du diese Gewohnheit löschen möchtest?")) { + try { + const response = await fetch(`/habits/${currentHabitId}`, { + method: 'DELETE' + }); + const data = await response.json(); + if (response.ok) { + closeModal(); + fetchHabits(); // Re-fetch and re-render all habits + } else { + console.error('Failed to delete habit:', data.error); + } + } catch (error) { + console.error('Error deleting habit:', error); + } + } +} \ No newline at end of file diff --git a/templates/index.html b/templates/index.html new file mode 100644 index 0000000..9e82fd1 --- /dev/null +++ b/templates/index.html @@ -0,0 +1,35 @@ + + + + + + HabitKit Clone + + + +
+

Habit Tracker

+
+ + +
+
+
+
+ + + + + + \ No newline at end of file