Compare commits

..

4 Commits

Author SHA1 Message Date
4ae00cac87 Fix drag and drop 2025-10-18 22:15:18 +02:00
b7388c2ccc update on homepage 2025-10-18 22:08:41 +02:00
c89c667304 robots.txt 2025-10-18 21:46:51 +02:00
831edbef49 SEO info 2025-10-18 21:39:08 +02:00
4 changed files with 114 additions and 16 deletions

View File

@@ -5,6 +5,41 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Habit Tracker App</title> <title>Habit Tracker App</title>
<meta name="description" content="Track your habits and visualize your progress with HabitGrid." /> <meta name="description" content="Track your habits and visualize your progress with HabitGrid." />
<meta name="keywords" content="habit tracker, productivity, goals, progress, HabitGrid, daily habits, motivation" />
<meta name="author" content="Mihajlo Ciric" />
<meta name="theme-color" content="#2563eb" />
<!-- Open Graph Meta Tags -->
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@type": "WebApplication",
"name": "HabitGrid",
"url": "https://myhabitgrid.com",
"description": "HabitGrid is a habit tracker app that helps users build and maintain daily habits using a GitHub-style contribution grid. Visualize your progress, build streaks, and stay motivated.",
"applicationCategory": "Productivity",
"operatingSystem": "All",
"creator": {
"@type": "Person",
"name": "Mihajlo Ciric"
},
"keywords": ["habit tracker", "productivity", "goals", "progress", "daily habits", "motivation", "HabitGrid"],
"offers": {
"@type": "Offer",
"price": "0",
"priceCurrency": "USD"
}
}
</script>
<meta property="og:title" content="Habit Tracker App" />
<meta property="og:description" content="Track your habits and visualize your progress with HabitGrid." />
<meta property="og:type" content="website" />
<meta property="og:url" content="https://myhabitgrid.com" />
<meta property="og:image" content="/assets/fav.png" />
<!-- Twitter Card Meta Tags -->
<meta name="twitter:card" content="summary_large_image" />
<meta name="twitter:title" content="Habit Tracker App" />
<meta name="twitter:description" content="Track your habits and visualize your progress with HabitGrid." />
<meta name="twitter:image" content="/assets/fav.png" />
<link rel="icon" type="image/png" href="/assets/fav.png" /> <link rel="icon" type="image/png" href="/assets/fav.png" />
<link rel="stylesheet" href="/src/index.css" /> <link rel="stylesheet" href="/src/index.css" />
</head> </head>

3
public/robots.txt Normal file
View File

@@ -0,0 +1,3 @@
User-agent: *
Allow: /
Sitemap: https://myhabitgrid.com/sitemap.xml

16
public/sitemap.xml Normal file
View File

@@ -0,0 +1,16 @@
<?xml version="1.0" encoding="UTF-8"?>
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
<url>
<loc>https://yourdomain.com/</loc>
</url>
<url>
<loc>https://yourdomain.com/add</loc>
</url>
<url>
<loc>https://yourdomain.com/settings</loc>
</url>
<url>
<loc>https://yourdomain.com/login-providers</loc>
</url>
<!-- For dynamic routes like /habit/:id and /edit/:id, add actual URLs if you have them -->
</urlset>

View File

@@ -2,7 +2,7 @@ import React, { useState, useEffect } from 'react';
import { DragDropContext, Droppable, Draggable } from '@hello-pangea/dnd'; import { DragDropContext, Droppable, Draggable } from '@hello-pangea/dnd';
// ...existing code... // ...existing code...
import { motion, AnimatePresence } from 'framer-motion'; import { motion, AnimatePresence } from 'framer-motion';
import { Plus, Settings, TrendingUp, Flame, Calendar, Moon, Sun } from 'lucide-react'; import { Plus, Settings, TrendingUp, Flame, Calendar, Moon, Sun, Star } from 'lucide-react';
import { Button } from '../components/ui/button'; import { Button } from '../components/ui/button';
import { useToast } from '../components/ui/use-toast'; import { useToast } from '../components/ui/use-toast';
import HabitCard from '../components/HabitCard'; import HabitCard from '../components/HabitCard';
@@ -188,67 +188,83 @@ const HomePage = () => {
let newHabits = [...habits]; let newHabits = [...habits];
// If dropping into uncategorized, always unset category // Helper to update local storage and UI instantly
const updateLocalOrder = (habitsArr) => {
localStorage.setItem('habitgrid_data', JSON.stringify(habitsArr));
setHabits(habitsArr);
};
// Collect async remote updates to fire after local update
let remoteUpdates = [];
if (destination.droppableId === 'uncategorized') { if (destination.droppableId === 'uncategorized') {
let items, removed; let items, removed;
if (source.droppableId === 'uncategorized') { if (source.droppableId === 'uncategorized') {
// Reorder within uncategorized
items = Array.from(uncategorized); items = Array.from(uncategorized);
[removed] = items.splice(source.index, 1); [removed] = items.splice(source.index, 1);
} else { } else {
// Move from category to uncategorized
items = Array.from(uncategorized); items = Array.from(uncategorized);
const sourceItems = Array.from(grouped[source.droppableId]); const sourceItems = Array.from(grouped[source.droppableId]);
[removed] = sourceItems.splice(source.index, 1); [removed] = sourceItems.splice(source.index, 1);
removed.category = ''; removed.category = '';
grouped[source.droppableId] = sourceItems; grouped[source.droppableId] = sourceItems;
} }
// Always set category to ''
removed.category = ''; removed.category = '';
items.splice(destination.index, 0, removed); items.splice(destination.index, 0, removed);
items.forEach((h, i) => updateHabit(h.id, { sortOrder: i, category: '' })); items.forEach((h, i) => {
h.sortOrder = i;
h.category = '';
remoteUpdates.push(updateHabit(h.id, { sortOrder: i, category: '' }));
});
newHabits = [ newHabits = [
...items, ...items,
...Object.values(grouped).flat() ...Object.values(grouped).flat()
]; ];
updateLocalOrder(newHabits);
} else if (source.droppableId === 'uncategorized' && grouped[destination.droppableId]) { } else if (source.droppableId === 'uncategorized' && grouped[destination.droppableId]) {
// Move from uncategorized to category
const items = Array.from(uncategorized); const items = Array.from(uncategorized);
const [removed] = items.splice(source.index, 1); const [removed] = items.splice(source.index, 1);
removed.category = destination.droppableId; removed.category = destination.droppableId;
const destItems = Array.from(grouped[destination.droppableId] || []); const destItems = Array.from(grouped[destination.droppableId] || []);
destItems.splice(destination.index, 0, removed); destItems.splice(destination.index, 0, removed);
destItems.forEach((h, i) => updateHabit(h.id, { sortOrder: i, category: h.category })); destItems.forEach((h, i) => {
h.sortOrder = i;
remoteUpdates.push(updateHabit(h.id, { sortOrder: i, category: h.category }));
});
newHabits = [ newHabits = [
...items, ...items,
...Object.values({ ...grouped, [destination.droppableId]: destItems }).flat() ...Object.values({ ...grouped, [destination.droppableId]: destItems }).flat()
]; ];
updateLocalOrder(newHabits);
} else if (grouped[source.droppableId] && grouped[destination.droppableId]) { } else if (grouped[source.droppableId] && grouped[destination.droppableId]) {
// Move within or between categories
const sourceItems = Array.from(grouped[source.droppableId]); const sourceItems = Array.from(grouped[source.droppableId]);
const [removed] = sourceItems.splice(source.index, 1); const [removed] = sourceItems.splice(source.index, 1);
if (source.droppableId === destination.droppableId) { if (source.droppableId === destination.droppableId) {
// Reorder within same category
sourceItems.splice(destination.index, 0, removed); sourceItems.splice(destination.index, 0, removed);
sourceItems.forEach((h, i) => updateHabit(h.id, { sortOrder: i, category: h.category })); sourceItems.forEach((h, i) => {
h.sortOrder = i;
remoteUpdates.push(updateHabit(h.id, { sortOrder: i, category: h.category }));
});
grouped[source.droppableId] = sourceItems; grouped[source.droppableId] = sourceItems;
} else { } else {
// Move to another category
const destItems = Array.from(grouped[destination.droppableId] || []); const destItems = Array.from(grouped[destination.droppableId] || []);
removed.category = destination.droppableId; removed.category = destination.droppableId;
destItems.splice(destination.index, 0, removed); destItems.splice(destination.index, 0, removed);
destItems.forEach((h, i) => updateHabit(h.id, { sortOrder: i, category: h.category })); destItems.forEach((h, i) => {
h.sortOrder = i;
remoteUpdates.push(updateHabit(h.id, { sortOrder: i, category: h.category }));
});
grouped[source.droppableId] = sourceItems; grouped[source.droppableId] = sourceItems;
grouped[destination.droppableId] = destItems; grouped[destination.droppableId] = destItems;
} }
// Flatten
newHabits = [ newHabits = [
...uncategorized, ...uncategorized,
...Object.values(grouped).flat() ...Object.values(grouped).flat()
]; ];
updateLocalOrder(newHabits);
} }
// Force immediate UI update after all updates // Fire remote updates async, do not block UI
loadHabits(); Promise.allSettled(remoteUpdates);
}} }}
> >
<div className="space-y-6"> <div className="space-y-6">
@@ -425,6 +441,34 @@ const HomePage = () => {
</Button> </Button>
</motion.div> </motion.div>
)} )}
<motion.div
initial={{ opacity: 0, scale: 0.8 }}
animate={{ opacity: 1, scale: 1 }}
transition={{ delay: 0.3 }}
className="fixed bottom-6 left-6"
>
<Button
onClick={() => window.open("https://github.com/nagaoo0/HabitGrid", "_blank")}
size="lg"
className="rounded-full shadow-lg hover:shadow-xl transition-shadow bg-gray-600 hover:bg-gray-700 text-white"
>
<a
href="https://github.com/nagaoo0/HabitGrid"
target="_blank"
rel="noopener noreferrer"
className="flex items-center space-x-2"
>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="white" className="w-7 h-7 text-slate-700 dark:text-slate-200">
<path d="M12 2C6.477 2 2 6.484 2 12.021c0 4.428 2.865 8.186 6.839 9.525.5.092.682-.217.682-.483 0-.237-.009-.868-.014-1.703-2.782.605-3.369-1.342-3.369-1.342-.454-1.155-1.11-1.463-1.11-1.463-.908-.62.069-.608.069-.608 1.004.07 1.532 1.032 1.532 1.032.892 1.53 2.341 1.088 2.91.832.091-.646.35-1.088.636-1.34-2.221-.253-4.555-1.113-4.555-4.951 0-1.093.39-1.988 1.029-2.688-.103-.254-.446-1.272.098-2.65 0 0 .84-.27 2.75 1.025A9.564 9.564 0 0 1 12 6.844c.85.004 1.705.115 2.504.337 1.909-1.295 2.748-1.025 2.748-1.025.546 1.378.202 2.396.1 2.65.64.7 1.028 1.595 1.028 2.688 0 3.847-2.337 4.695-4.566 4.944.359.309.678.92.678 1.855 0 1.338-.012 2.419-.012 2.747 0 .268.18.579.688.481C19.138 20.204 22 16.447 22 12.021 22 6.484 17.523 2 12 2z" />
</svg>
</a>
</Button>
</motion.div>
</div> </div>
</div> </div>
); );