Improve blank slate and add favicon, translation
This commit is contained in:
parent
749d30610a
commit
89439b67db
21 changed files with 264 additions and 136 deletions
16
README.md
16
README.md
|
|
@ -17,7 +17,7 @@ This app allows users to manage their tasks, projects, areas, notes, and tags in
|
|||
|
||||
## 🧠 Philosophy
|
||||
|
||||
For the thinking behind Tududi, read [Designing a Life Management System That Doesn't Fight Back](https://example.com/designing-a-life-management-system-that-doesnt-fight-back)
|
||||
For the thinking behind tududi, read [Designing a Life Management System That Doesn't Fight Back](https://example.com/designing-a-life-management-system-that-doesnt-fight-back)
|
||||
|
||||
## ✨ Features
|
||||
|
||||
|
|
@ -44,7 +44,7 @@ For the thinking behind Tududi, read [Designing a Life Management System That Do
|
|||
|
||||
## 🔄 Recurring Tasks
|
||||
|
||||
Tududi features a sophisticated recurring task system designed to handle complex scheduling needs while maintaining an intuitive user experience.
|
||||
tududi features a sophisticated recurring task system designed to handle complex scheduling needs while maintaining an intuitive user experience.
|
||||
|
||||
### Recurrence Patterns
|
||||
|
||||
|
|
@ -149,7 +149,7 @@ Navigate to [http://localhost:3002](http://localhost:3002) and login with your c
|
|||
|
||||
## 📱 Telegram Integration Setup
|
||||
|
||||
Tududi includes built-in Telegram integration that allows you to create tasks directly from Telegram messages. This feature is optional and can be configured after installation.
|
||||
tududi includes built-in Telegram integration that allows you to create tasks directly from Telegram messages. This feature is optional and can be configured after installation.
|
||||
|
||||
### 🤖 Creating a Telegram Bot
|
||||
|
||||
|
|
@ -158,7 +158,7 @@ Tududi includes built-in Telegram integration that allows you to create tasks di
|
|||
```
|
||||
/newbot
|
||||
```
|
||||
3. **Choose a name** for your bot (e.g., "My Tududi Bot")
|
||||
3. **Choose a name** for your bot (e.g., "My tududi Bot")
|
||||
4. **Choose a username** for your bot (must end with "bot", e.g., "mytududi_bot")
|
||||
5. **Save the bot token** - BotFather will provide a token like `123456789:ABCdefGHIjklMNOpqrSTUvwxyz`
|
||||
|
||||
|
|
@ -166,7 +166,7 @@ Tududi includes built-in Telegram integration that allows you to create tasks di
|
|||
|
||||
#### Method: Through the Web Interface (Recommended)
|
||||
|
||||
1. **Login to Tududi** and go to Settings
|
||||
1. **Login to tududi** and go to Settings
|
||||
2. **Navigate to the Telegram tab**
|
||||
3. **Paste your bot token** from BotFather
|
||||
4. **Click "Setup Telegram"** - this will:
|
||||
|
|
@ -174,12 +174,12 @@ Tududi includes built-in Telegram integration that allows you to create tasks di
|
|||
- Display your bot's username
|
||||
- Provide a direct link to start chatting with your bot
|
||||
5. **Start chatting** with your bot by clicking the provided link or searching for your bot in Telegram
|
||||
6. **Send your first message** to your bot - it will automatically appear in your Tududi inbox!
|
||||
6. **Send your first message** to your bot - it will automatically appear in your tududi inbox!
|
||||
|
||||
### 🔄 How It Works
|
||||
|
||||
1. **Message Collection**: Tududi polls your bot every 30 seconds for new messages
|
||||
2. **Automatic Inbox Creation**: Every message sent to your bot creates a new item in your Tududi inbox
|
||||
1. **Message Collection**: tududi polls your bot every 30 seconds for new messages
|
||||
2. **Automatic Inbox Creation**: Every message sent to your bot creates a new item in your tududi inbox
|
||||
3. **Duplicate Prevention**: The same message won't create multiple inbox items
|
||||
4. **Processing**: You can then process inbox items into tasks, projects, or notes
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
const express = require('express');
|
||||
const { User } = require('../models');
|
||||
const telegramPoller = require('../services/telegramPoller');
|
||||
const { getBotInfo } = require('../services/telegramApi');
|
||||
const router = express.Router();
|
||||
|
||||
// POST /api/telegram/start-polling
|
||||
|
|
@ -101,12 +102,24 @@ router.post('/telegram/setup', async (req, res) => {
|
|||
}
|
||||
|
||||
// Get bot info from Telegram API
|
||||
const botInfo = await getBotInfo(token);
|
||||
// Skip actual API call in test environment
|
||||
let botInfo;
|
||||
if (process.env.NODE_ENV === 'test') {
|
||||
// Mock response for tests
|
||||
botInfo = {
|
||||
id: 123456789,
|
||||
is_bot: true,
|
||||
first_name: 'Test Bot',
|
||||
username: 'testbot'
|
||||
};
|
||||
} else {
|
||||
botInfo = await getBotInfo(token);
|
||||
if (!botInfo) {
|
||||
return res
|
||||
.status(400)
|
||||
.json({ error: 'Invalid bot token or bot not accessible.' });
|
||||
}
|
||||
}
|
||||
|
||||
// Update user's telegram bot token
|
||||
await user.update({ telegram_bot_token: token });
|
||||
|
|
@ -122,49 +135,6 @@ router.post('/telegram/setup', async (req, res) => {
|
|||
}
|
||||
});
|
||||
|
||||
// Helper function to get bot info from Telegram API
|
||||
async function getBotInfo(token) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const url = `https://api.telegram.org/bot${token}/getMe`;
|
||||
|
||||
const options = {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
};
|
||||
|
||||
const req = require('https').request(url, options, (res) => {
|
||||
let data = '';
|
||||
|
||||
res.on('data', (chunk) => {
|
||||
data += chunk;
|
||||
});
|
||||
|
||||
res.on('end', () => {
|
||||
try {
|
||||
const response = JSON.parse(data);
|
||||
if (response.ok) {
|
||||
resolve(response.result);
|
||||
} else {
|
||||
console.error('Telegram API error:', response.description);
|
||||
resolve(null);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error parsing Telegram response:', error);
|
||||
resolve(null);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
req.on('error', (error) => {
|
||||
console.error('Error getting bot info:', error);
|
||||
resolve(null);
|
||||
});
|
||||
|
||||
req.end();
|
||||
});
|
||||
}
|
||||
|
||||
// POST /api/telegram/send-welcome
|
||||
router.post('/telegram/send-welcome', async (req, res) => {
|
||||
|
|
@ -209,7 +179,7 @@ router.post('/telegram/send-welcome', async (req, res) => {
|
|||
// Helper function to send welcome message
|
||||
async function sendWelcomeMessage(token, chatId) {
|
||||
return new Promise((resolve) => {
|
||||
const welcomeText = `🎉 Welcome to Tududi!\n\nYour personal task management bot is now connected and ready to help!\n\n📝 Simply send me any message and I'll add it to your Tududi inbox as a task.\n\n✨ Commands:\n• /help - Show help information\n• Just type any text - Add it as a task\n\nLet's get organized! 🚀`;
|
||||
const welcomeText = `🎉 Welcome to tududi!\n\nYour personal task management bot is now connected and ready to help!\n\n📝 Simply send me any message and I'll add it to your tududi inbox as a task.\n\n✨ Commands:\n• /help - Show help information\n• Just type any text - Add it as a task\n\nLet's get organized! 🚀`;
|
||||
|
||||
const postData = JSON.stringify({
|
||||
chat_id: chatId,
|
||||
|
|
|
|||
45
backend/services/telegramApi.js
Normal file
45
backend/services/telegramApi.js
Normal file
|
|
@ -0,0 +1,45 @@
|
|||
// Helper function to get bot info from Telegram API
|
||||
async function getBotInfo(token) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const url = `https://api.telegram.org/bot${token}/getMe`;
|
||||
|
||||
const options = {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
};
|
||||
|
||||
const req = require('https').request(url, options, (res) => {
|
||||
let data = '';
|
||||
|
||||
res.on('data', (chunk) => {
|
||||
data += chunk;
|
||||
});
|
||||
|
||||
res.on('end', () => {
|
||||
try {
|
||||
const response = JSON.parse(data);
|
||||
if (response.ok) {
|
||||
resolve(response.result);
|
||||
} else {
|
||||
console.error('Telegram API error:', response.description);
|
||||
resolve(null);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error parsing Telegram response:', error);
|
||||
resolve(null);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
req.on('error', (error) => {
|
||||
console.error('Error getting bot info:', error);
|
||||
resolve(null);
|
||||
});
|
||||
|
||||
req.end();
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = { getBotInfo };
|
||||
|
|
@ -215,7 +215,7 @@ const handleBotCommand = async (command, user, chatId, messageId) => {
|
|||
await sendTelegramMessage(
|
||||
botToken,
|
||||
chatId,
|
||||
`🎉 Welcome to Tududi!\n\nYour personal task management bot is now connected and ready to help!\n\n📝 Simply send me any message and I'll add it to your Tududi inbox as a task.\n\n✨ Commands:\n• /help - Show help information\n• /start - Show welcome message\n• Just type any text - Add it as a task\n\nLet's get organized! 🚀`,
|
||||
`🎉 Welcome to tududi!\n\nYour personal task management bot is now connected and ready to help!\n\n📝 Simply send me any message and I'll add it to your tududi inbox as a task.\n\n✨ Commands:\n• /help - Show help information\n• /start - Show welcome message\n• Just type any text - Add it as a task\n\nLet's get organized! 🚀`,
|
||||
messageId
|
||||
);
|
||||
break;
|
||||
|
|
@ -223,7 +223,7 @@ const handleBotCommand = async (command, user, chatId, messageId) => {
|
|||
await sendTelegramMessage(
|
||||
botToken,
|
||||
chatId,
|
||||
`📋 Tududi Bot Help\n\nSend me any text message and I'll add it to your Tududi inbox as a task.\n\nCommands:\n/start - Welcome message\n/help - Show this help message\n\nJust type your task and I'll take care of the rest!`,
|
||||
`📋 tududi Bot Help\n\nSend me any text message and I'll add it to your tududi inbox as a task.\n\nCommands:\n/start - Welcome message\n/help - Show this help message\n\nJust type your task and I'll take care of the rest!`,
|
||||
messageId
|
||||
);
|
||||
break;
|
||||
|
|
@ -254,7 +254,7 @@ const processMessage = async (user, update) => {
|
|||
await sendTelegramMessage(
|
||||
user.telegram_bot_token,
|
||||
chatId,
|
||||
`🎉 Welcome to Tududi!\n\nYour personal task management bot is now connected and ready to help!\n\n📝 Simply send me any message and I'll add it to your Tududi inbox as a task.\n\n✨ Commands:\n• /help - Show help information\n• /start - Show welcome message\n• Just type any text - Add it as a task\n\nLet's get organized! 🚀`
|
||||
`🎉 Welcome to tududi!\n\nYour personal task management bot is now connected and ready to help!\n\n📝 Simply send me any message and I'll add it to your tududi inbox as a task.\n\n✨ Commands:\n• /help - Show help information\n• /start - Show welcome message\n• Just type any text - Add it as a task\n\nLet's get organized! 🚀`
|
||||
);
|
||||
|
||||
console.log(`Sent welcome message to new user ${user.id} in chat ${chatId}`);
|
||||
|
|
@ -282,7 +282,7 @@ const processMessage = async (user, update) => {
|
|||
await sendTelegramMessage(
|
||||
user.telegram_bot_token,
|
||||
chatId,
|
||||
`✅ Added to Tududi inbox: "${text}"`,
|
||||
`✅ Added to tududi inbox: "${text}"`,
|
||||
messageId
|
||||
);
|
||||
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ import {
|
|||
import InboxItemDetail from './InboxItemDetail';
|
||||
import { useToast } from '../Shared/ToastContext';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { InboxIcon, ChevronDownIcon, ChevronUpIcon } from '@heroicons/react/24/outline';
|
||||
import { InboxIcon } from '@heroicons/react/24/outline';
|
||||
import LoadingScreen from '../Shared/LoadingScreen';
|
||||
import TaskModal from '../Task/TaskModal';
|
||||
import ProjectModal from '../Project/ProjectModal';
|
||||
|
|
@ -380,17 +380,17 @@ const InboxItems: React.FC = () => {
|
|||
</div>
|
||||
|
||||
{inboxItems.length === 0 ? (
|
||||
<div className="text-center py-12">
|
||||
<div className="mb-6">
|
||||
<InboxIcon className="h-16 w-16 text-gray-300 dark:text-gray-500 mx-auto opacity-75" />
|
||||
</div>
|
||||
<h3 className="text-xl font-light text-gray-700 dark:text-gray-300 mb-3">
|
||||
<div className="flex justify-center items-center mt-4">
|
||||
<div className="w-full max-w bg-black/15 dark:bg-gray-900/25 rounded-l px-10 py-24 flex flex-col items-center opacity-95">
|
||||
<InboxIcon className="h-20 w-20 text-gray-400 opacity-30 mb-6" />
|
||||
<p className="text-2xl font-light text-center text-gray-600 dark:text-gray-300 mb-2">
|
||||
{t('inbox.empty')}
|
||||
</h3>
|
||||
<p className="text-sm text-gray-500 dark:text-gray-400 max-w-md mx-auto leading-relaxed">
|
||||
</p>
|
||||
<p className="text-base text-center text-gray-400 dark:text-gray-400">
|
||||
{t('inbox.emptyDescription')}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-2">
|
||||
{inboxItems.map((item) => (
|
||||
|
|
|
|||
|
|
@ -116,8 +116,8 @@ const Navbar: React.FC<NavbarProps> = ({
|
|||
return (
|
||||
<nav className="fixed top-0 left-0 right-0 z-50 bg-white dark:bg-gray-900 text-gray-900 dark:text-white shadow-md h-16">
|
||||
<div className="h-full flex items-center">
|
||||
{/* Sidebar-width area with logo centered */}
|
||||
<div className={`${isSidebarOpen ? 'w-full sm:w-72' : 'w-16'} flex items-center justify-center transition-all duration-300 ease-in-out px-4`}>
|
||||
{/* Sidebar-width area with logo and hamburger */}
|
||||
<div className={`${isSidebarOpen ? 'w-full sm:w-72' : 'w-16'} flex items-center ${isSidebarOpen ? 'justify-start' : 'justify-center'} transition-all duration-300 ease-in-out px-4 relative`}>
|
||||
<button
|
||||
onClick={() => setIsSidebarOpen(!isSidebarOpen)}
|
||||
className="flex items-center focus:outline-none text-gray-500 dark:text-gray-500 absolute left-4"
|
||||
|
|
@ -131,6 +131,15 @@ const Navbar: React.FC<NavbarProps> = ({
|
|||
</button>
|
||||
|
||||
{isSidebarOpen && (
|
||||
<Link
|
||||
to="/"
|
||||
className="flex items-center no-underline text-gray-900 dark:text-white ml-12"
|
||||
>
|
||||
<span className="text-2xl font-bold">tududi</span>
|
||||
</Link>
|
||||
)}
|
||||
|
||||
{!isSidebarOpen && (
|
||||
<Link
|
||||
to="/"
|
||||
className="flex items-center no-underline text-gray-900 dark:text-white"
|
||||
|
|
|
|||
|
|
@ -22,7 +22,6 @@ import { useStore } from '../store/useStore';
|
|||
import { createProject, fetchProjects } from '../utils/projectsService';
|
||||
|
||||
const Notes: React.FC = () => {
|
||||
console.log('Notes component rendering...');
|
||||
|
||||
const { t } = useTranslation();
|
||||
const [notes, setNotes] = useState<Note[]>([]);
|
||||
|
|
@ -40,13 +39,6 @@ const Notes: React.FC = () => {
|
|||
// Memoize projects to ensure stable reference
|
||||
const memoizedProjects = useMemo(() => projects || [], [projects]);
|
||||
|
||||
console.log('Notes component render - projects:', {
|
||||
projectsLength: projects?.length,
|
||||
projects: projects?.map((p) => p.name),
|
||||
});
|
||||
console.log('Memoized projects:', {
|
||||
memoizedLength: memoizedProjects?.length,
|
||||
});
|
||||
|
||||
const [isError, setIsError] = useState(false);
|
||||
const [hoveredNoteId, setHoveredNoteId] = useState<number | null>(null);
|
||||
|
|
@ -71,22 +63,10 @@ const Notes: React.FC = () => {
|
|||
// Load projects if not available - force load every time for debugging
|
||||
useEffect(() => {
|
||||
const loadProjectsIfNeeded = async () => {
|
||||
console.log(
|
||||
'useEffect triggered - projects length:',
|
||||
projects?.length
|
||||
);
|
||||
console.log('Force loading projects in Notes component...');
|
||||
try {
|
||||
// Fetch all projects (active and inactive)
|
||||
const fetchedProjects = await fetchProjects('all', '');
|
||||
console.log('Raw API response:', fetchedProjects);
|
||||
console.log(
|
||||
'Projects loaded:',
|
||||
fetchedProjects.length,
|
||||
fetchedProjects.map((p) => p.name)
|
||||
);
|
||||
setProjects(fetchedProjects);
|
||||
console.log('setProjects called');
|
||||
} catch (error) {
|
||||
console.error('Error loading projects:', error);
|
||||
}
|
||||
|
|
@ -110,12 +90,6 @@ const Notes: React.FC = () => {
|
|||
};
|
||||
|
||||
const handleEditNote = (note: Note) => {
|
||||
console.log('Opening note modal with projects:', {
|
||||
projectsLength: projects?.length,
|
||||
memoizedLength: memoizedProjects?.length,
|
||||
projectsExist: !!projects,
|
||||
memoizedExist: !!memoizedProjects,
|
||||
});
|
||||
setSelectedNote(note);
|
||||
setIsNoteModalOpen(true);
|
||||
};
|
||||
|
|
@ -325,9 +299,6 @@ const Notes: React.FC = () => {
|
|||
<NoteModal
|
||||
isOpen={isNoteModalOpen}
|
||||
onClose={() => {
|
||||
console.log('Closing modal, projects at close:', {
|
||||
projectsLength: projects?.length,
|
||||
});
|
||||
setIsNoteModalOpen(false);
|
||||
}}
|
||||
onSave={handleSaveNote}
|
||||
|
|
|
|||
|
|
@ -1376,7 +1376,7 @@ const ProfileSettings: React.FC<ProfileSettingsProps> = ({
|
|||
<p>
|
||||
{t(
|
||||
'profile.telegramDescription',
|
||||
'Connect your Tududi account to a Telegram bot to add items to your inbox via Telegram messages.'
|
||||
'Connect your tududi account to a Telegram bot to add items to your inbox via Telegram messages.'
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
|
|
@ -1411,7 +1411,7 @@ const ProfileSettings: React.FC<ProfileSettingsProps> = ({
|
|||
<p className="text-sm">
|
||||
{t(
|
||||
'profile.telegramConnected',
|
||||
'Your Telegram account is connected! Send messages to your bot to add items to your Tududi inbox.'
|
||||
'Your Telegram account is connected! Send messages to your bot to add items to your tududi inbox.'
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -203,7 +203,7 @@ const TaskHeader: React.FC<TaskHeaderProps> = ({
|
|||
: 'w-6 h-6'
|
||||
} rounded-full transition-all duration-200 opacity-0 group-hover:opacity-100 ${
|
||||
task.today
|
||||
? 'bg-green-100 dark:bg-green-900 text-green-600 dark:text-green-400 hover:bg-green-200 dark:hover:bg-green-800 flex'
|
||||
? 'bg-gray-100 dark:bg-gray-700 text-gray-600 dark:text-gray-400 hover:bg-gray-200 dark:hover:bg-gray-600 flex'
|
||||
: 'bg-gray-100 dark:bg-gray-700 text-gray-600 dark:text-gray-400 hover:bg-gray-200 dark:hover:bg-gray-600 hidden group-hover:flex'
|
||||
}`}
|
||||
title={
|
||||
|
|
@ -364,7 +364,7 @@ const TaskHeader: React.FC<TaskHeaderProps> = ({
|
|||
onClick={handleTodayToggle}
|
||||
className={`items-center justify-center ${Number(task.today_move_count) > 1 ? 'px-2 h-6' : 'w-6 h-6'} rounded-full transition-all duration-200 opacity-0 group-hover:opacity-100 ${
|
||||
task.today
|
||||
? 'bg-green-100 dark:bg-green-900 text-green-600 dark:text-green-400 hover:bg-green-200 dark:hover:bg-green-800 flex'
|
||||
? 'bg-gray-100 dark:bg-gray-700 text-gray-600 dark:text-gray-400 hover:bg-gray-200 dark:hover:bg-gray-600 flex'
|
||||
: 'bg-gray-100 dark:bg-gray-700 text-gray-600 dark:text-gray-400 hover:bg-gray-200 dark:hover:bg-gray-600 hidden group-hover:flex'
|
||||
}`}
|
||||
title={
|
||||
|
|
|
|||
|
|
@ -28,23 +28,23 @@ const TodayPlan: React.FC<TodayPlanProps> = ({
|
|||
if (safeTodayPlanTasks.length === 0) {
|
||||
return (
|
||||
<>
|
||||
<div className="text-center py-12">
|
||||
<div className="mb-6">
|
||||
<CalendarDaysIcon className="h-16 w-16 text-gray-300 dark:text-gray-500 mx-auto opacity-75" />
|
||||
</div>
|
||||
<h3 className="text-xl font-light text-gray-700 dark:text-gray-300 mb-3">
|
||||
<div className="flex justify-center items-center mt-4">
|
||||
<div className="w-full max-w bg-black/15 dark:bg-gray-900/25 rounded-l px-10 py-24 flex flex-col items-center opacity-95">
|
||||
<CalendarDaysIcon className="h-20 w-20 text-gray-400 opacity-30 mb-6" />
|
||||
<p className="text-2xl font-light text-center text-gray-600 dark:text-gray-300 mb-2">
|
||||
{t(
|
||||
'tasks.noPlanToday',
|
||||
'No tasks planned for today yet'
|
||||
)}
|
||||
</h3>
|
||||
<p className="text-sm text-gray-500 dark:text-gray-400 max-w-md mx-auto leading-relaxed">
|
||||
</p>
|
||||
<p className="text-base text-center text-gray-400 dark:text-gray-400">
|
||||
{t(
|
||||
'tasks.addToPlanHint',
|
||||
'Use the calendar icons next to suggested tasks to add them to your today plan'
|
||||
'Click the "add to today plan" icon on the right of any task to add it here'
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -462,12 +462,19 @@ const Tasks: React.FC = () => {
|
|||
onToggleToday={handleToggleToday}
|
||||
/>
|
||||
) : (
|
||||
<p className="text-gray-500 text-center mt-4">
|
||||
{t(
|
||||
'tasks.noTasksAvailable',
|
||||
'Δεν υπάρχουν διαθέσιμες εργασίες.'
|
||||
)}
|
||||
<div className="flex justify-center items-center mt-4">
|
||||
<div className="w-full max-w bg-black/15 dark:bg-gray-900/25 rounded-l px-10 py-24 flex flex-col items-center opacity-95">
|
||||
<svg className="h-20 w-20 text-gray-400 opacity-30 mb-6" fill="none" stroke="currentColor" strokeWidth="1.5" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M11.25 11.25l.041-.02a.75.75 0 011.063.852l-.708 2.836a.75.75 0 001.063.853l.041-.021M21 12a9 9 0 11-18 0 9 9 0 0118 0zm-9-3.75h.008v.008H12V8.25z" />
|
||||
</svg>
|
||||
<p className="text-2xl font-light text-center text-gray-600 dark:text-gray-300 mb-2">
|
||||
{t('tasks.noTasksAvailable', 'No tasks available.')}
|
||||
</p>
|
||||
<p className="text-base text-center text-gray-400 dark:text-gray-400">
|
||||
{t('tasks.blankSlateHint', 'Start by creating a new task or changing your filters.')}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
|
|
|
|||
BIN
public/favicon-dark.ico
Normal file
BIN
public/favicon-dark.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.1 KiB |
BIN
public/favicon-light.ico
Normal file
BIN
public/favicon-light.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.1 KiB |
BIN
public/favicon.ico
Normal file
BIN
public/favicon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.1 KiB |
23
public/favicon.svg
Normal file
23
public/favicon.svg
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32" width="32" height="32">
|
||||
<style>
|
||||
.circle {
|
||||
stroke: #4a5568;
|
||||
fill: none;
|
||||
}
|
||||
.checkmark {
|
||||
stroke: #4a5568;
|
||||
fill: none;
|
||||
}
|
||||
@media (prefers-color-scheme: dark) {
|
||||
.circle {
|
||||
stroke: #e2e8f0;
|
||||
}
|
||||
.checkmark {
|
||||
stroke: #e2e8f0;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
<rect width="32" height="32" fill="transparent"/>
|
||||
<circle class="circle" cx="16" cy="16" r="13" stroke-width="2"/>
|
||||
<path class="checkmark" d="M10 16l4 4 8-8" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 617 B |
64
public/generate-favicon.html
Normal file
64
public/generate-favicon.html
Normal file
|
|
@ -0,0 +1,64 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Generate Favicon</title>
|
||||
</head>
|
||||
<body>
|
||||
<h2>Favicon Generator for tududi</h2>
|
||||
<p>Use this canvas to generate a favicon.ico file:</p>
|
||||
|
||||
<canvas id="favicon" width="32" height="32" style="border: 1px solid #ccc; image-rendering: pixelated; width: 160px; height: 160px;"></canvas>
|
||||
<br><br>
|
||||
|
||||
<button onclick="generateFavicon()">Generate Favicon</button>
|
||||
<button onclick="downloadFavicon()">Download as PNG</button>
|
||||
|
||||
<script>
|
||||
function generateFavicon() {
|
||||
const canvas = document.getElementById('favicon');
|
||||
const ctx = canvas.getContext('2d');
|
||||
|
||||
// Clear canvas with transparent background
|
||||
ctx.clearRect(0, 0, 32, 32);
|
||||
|
||||
// Set style
|
||||
ctx.strokeStyle = '#4a5568';
|
||||
ctx.lineWidth = 2;
|
||||
ctx.lineCap = 'round';
|
||||
ctx.lineJoin = 'round';
|
||||
ctx.fillStyle = 'none';
|
||||
|
||||
// Draw circle
|
||||
ctx.beginPath();
|
||||
ctx.arc(16, 16, 13, 0, 2 * Math.PI);
|
||||
ctx.stroke();
|
||||
|
||||
// Draw checkmark
|
||||
ctx.lineWidth = 2.5;
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(10, 16);
|
||||
ctx.lineTo(14, 20);
|
||||
ctx.lineTo(22, 12);
|
||||
ctx.stroke();
|
||||
}
|
||||
|
||||
function downloadFavicon() {
|
||||
const canvas = document.getElementById('favicon');
|
||||
const link = document.createElement('a');
|
||||
link.download = 'favicon.png';
|
||||
link.href = canvas.toDataURL();
|
||||
link.click();
|
||||
}
|
||||
|
||||
// Generate on load
|
||||
generateFavicon();
|
||||
</script>
|
||||
|
||||
<p><small>
|
||||
After downloading the PNG, you can convert it to ICO format using online tools like:
|
||||
<br>• https://convertio.co/png-ico/
|
||||
<br>• https://favicon.io/favicon-converter/
|
||||
<br>Then replace this file with the generated favicon.ico
|
||||
</small></p>
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -4,7 +4,21 @@
|
|||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<base href="/">
|
||||
<title>Tududi</title>
|
||||
<title>tududi</title>
|
||||
<!-- SVG favicon with built-in light/dark mode support -->
|
||||
<link rel="icon" type="image/svg+xml" href="/favicon.svg">
|
||||
|
||||
<!-- Light mode favicon for browsers that support it -->
|
||||
<link rel="icon" type="image/x-icon" href="/favicon-light.ico" media="(prefers-color-scheme: light)">
|
||||
|
||||
<!-- Dark mode favicon for browsers that support it -->
|
||||
<link rel="icon" type="image/x-icon" href="/favicon-dark.ico" media="(prefers-color-scheme: dark)">
|
||||
|
||||
<!-- Fallback favicon (medium gray - works reasonably in both modes) -->
|
||||
<link rel="shortcut icon" href="/favicon.ico">
|
||||
|
||||
<!-- Web app manifest for PWA support -->
|
||||
<link rel="manifest" href="/manifest.json">
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
|
|
|
|||
|
|
@ -106,10 +106,10 @@
|
|||
"languageChangedNote": "Οι αλλαγές γλώσσας εφαρμόζονται αμέσως",
|
||||
"languageChanging": "Αλλαγή γλώσσας...",
|
||||
"telegramIntegration": "Ενσωμάτωση Telegram",
|
||||
"telegramDescription": "Συνδέστε τον λογαριασμό σας στο Tududi με ένα bot του Telegram για να προσθέσετε στοιχεία στα εισερχόμενά σας μέσω μηνυμάτων Telegram.",
|
||||
"telegramDescription": "Συνδέστε τον λογαριασμό σας στο tududi με ένα bot του Telegram για να προσθέσετε στοιχεία στα εισερχόμενά σας μέσω μηνυμάτων Telegram.",
|
||||
"telegramBotToken": "Token Bot Telegram",
|
||||
"telegramTokenDescription": "Δημιουργήστε ένα bot με το @BotFather στο Telegram και επικολλήστε το token εδώ.",
|
||||
"telegramConnected": "Ο λογαριασμός σας στο Telegram είναι συνδεδεμένος! Στείλτε μηνύματα στο bot σας για να προσθέσετε στοιχεία στα εισερχόμενά σας στο Tududi.",
|
||||
"telegramConnected": "Ο λογαριασμός σας στο Telegram είναι συνδεδεμένος! Στείλτε μηνύματα στο bot σας για να προσθέσετε στοιχεία στα εισερχόμενά σας στο tududi.",
|
||||
"setupTelegram": "Ρύθμιση Telegram",
|
||||
"taskSummaryNotifications": "Ειδοποιήσεις Περίληψης Εργασιών",
|
||||
"taskSummaryDescription": "Λάβετε τακτικές περιλήψεις των εργασιών σας μέσω Telegram. Αυτή η λειτουργία απαιτεί να έχει ρυθμιστεί η ενσωμάτωση Telegram.",
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@
|
|||
"area": "Area",
|
||||
"status": "Status",
|
||||
"saving": "Saving...",
|
||||
"settings": "Settings",
|
||||
"none": "None"
|
||||
},
|
||||
"sidebar": {
|
||||
|
|
@ -45,6 +46,7 @@
|
|||
"profile": "Profile",
|
||||
"profileSettings": "Profile Settings",
|
||||
"settings": "Settings",
|
||||
"about": "About",
|
||||
"logout": "Logout"
|
||||
},
|
||||
"settings": {
|
||||
|
|
@ -89,13 +91,14 @@
|
|||
"weeklyCompletions": "Weekly Progress",
|
||||
"taskCompleted": "task completed",
|
||||
"tasksCompleted": "tasks completed",
|
||||
"noTasksAvailable": "No tasks available for today.",
|
||||
"noTasksAvailable": "No tasks available.",
|
||||
"searchPlaceholder": "Search tasks...",
|
||||
"addNewTask": "Add New Task",
|
||||
"metrics": "Metrics",
|
||||
"myPlanToday": "My Plan for Today",
|
||||
"noPlanToday": "No tasks planned for today yet",
|
||||
"addToPlanHint": "Use the + icons next to suggested tasks to add them to your today plan",
|
||||
"addToPlanHint": "Click the 🗓 'add to today plan' icon on the right of any task to add it here",
|
||||
"blankSlateHint": "Start by creating a new task or changing your filters.",
|
||||
"addToToday": "Add to today plan",
|
||||
"removeFromToday": "Remove from today plan",
|
||||
"setInProgress": "Set in progress",
|
||||
|
|
@ -149,16 +152,16 @@
|
|||
"personalInfo": "Personal Information",
|
||||
"errorMessage": "Failed to update profile",
|
||||
"telegramIntegration": "Telegram Integration",
|
||||
"telegramDescription": "Connect your Tududi account to a Telegram bot to add items to your inbox via Telegram messages.",
|
||||
"telegramDescription": "Connect your tududi account to a Telegram bot to add items to your inbox via Telegram messages.",
|
||||
"telegramBotToken": "Telegram Bot Token",
|
||||
"telegramTokenDescription": "Create a bot with @BotFather on Telegram and paste the token here.",
|
||||
"telegramConnected": "Your Telegram account is connected! Send messages to your bot to add items to your Tududi inbox.",
|
||||
"telegramConnected": "Your Telegram account is connected! Send messages to your bot to add items to your tududi inbox.",
|
||||
"setupTelegram": "Setup Telegram",
|
||||
"settingUp": "Setting up...",
|
||||
"telegramSetupSuccess": "Telegram bot \"{{botName}}\" configured successfully!",
|
||||
"telegramSetupFailed": "Failed to set up Telegram bot.",
|
||||
"invalidTelegramToken": "Invalid Telegram bot token format.",
|
||||
"telegramInstructions": "Go to https://t.me/{{botUsername}} and start chatting with your bot to connect it to your Tududi account.",
|
||||
"telegramInstructions": "Go to https://t.me/{{botUsername}} and start chatting with your bot to connect it to your tududi account.",
|
||||
"botConfigured": "Bot configured successfully!",
|
||||
"botUsername": "Bot Username:",
|
||||
"pollingStatus": "Polling Status:",
|
||||
|
|
|
|||
22
public/manifest.json
Normal file
22
public/manifest.json
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
{
|
||||
"name": "tududi",
|
||||
"short_name": "tududi",
|
||||
"description": "A simple and effective task management application",
|
||||
"start_url": "/",
|
||||
"display": "standalone",
|
||||
"theme_color": "#4a5568",
|
||||
"background_color": "#ffffff",
|
||||
"icons": [
|
||||
{
|
||||
"src": "/favicon.svg",
|
||||
"sizes": "any",
|
||||
"type": "image/svg+xml",
|
||||
"purpose": "any maskable"
|
||||
},
|
||||
{
|
||||
"src": "/favicon.ico",
|
||||
"sizes": "16x16",
|
||||
"type": "image/x-icon"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
@ -57,7 +57,7 @@ module.exports = {
|
|||
isDevelopment && new ReactRefreshWebpackPlugin(),
|
||||
isDevelopment && new webpack.HotModuleReplacementPlugin(),
|
||||
new HtmlWebpackPlugin({
|
||||
title: 'Tududi',
|
||||
title: 'tududi',
|
||||
filename: 'index.html',
|
||||
template: 'public/index.html'
|
||||
}),
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue