Skip to content

Commit

Permalink
add remote storage checkpoint
Browse files Browse the repository at this point in the history
  • Loading branch information
birdup000 committed Dec 17, 2024
1 parent 3ef407f commit 98433b9
Show file tree
Hide file tree
Showing 8 changed files with 950 additions and 104 deletions.
82 changes: 74 additions & 8 deletions app/components/TaskPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import LoginForm from './LoginForm';
import AIAssistant from './AIAssistant';
import ShortcutsDialog from './ShortcutsDialog';
import { Task, TaskList } from '../types/task';
import { StorageConfig } from '../types/storage';
import { Comment } from './CommentSection';
import { colors } from '../../tailwind.config';
import TaskStats from './TaskStats';
Expand Down Expand Up @@ -61,7 +62,32 @@ const TaskPanel: React.FC = () => {
const [showCompletedRecurring, setShowCompletedRecurring] = React.useState(true);
const [showIntegrations, setShowIntegrations] = React.useState(false);
const [isEditorOpen, setIsEditorOpen] = React.useState(false);
const { tasks, addTask, updateTask, deleteTask, reorderTasks, importTasks, lists, addList, updateList, deleteList } = useTasks();
const [storageConfig, setStorageConfig] = React.useState<StorageConfig>(() => {
const email = localStorage.getItem('email');
const userId = email || 'anonymous';
return {
remoteEnabled: localStorage.getItem('remoteStorageEnabled') === 'true',
apiKey: localStorage.getItem('remoteStorageApiKey') || undefined,
userId
};
});

const {
tasks,
addTask,
updateTask,
deleteTask,
reorderTasks,
importTasks,
lists,
addList,
updateList,
deleteList,
sync,
isLoading
} = useTasks(storageConfig);

const [isSyncing, setIsSyncing] = React.useState(false);
const {
searchTerm,
setSearchTerm,
Expand Down Expand Up @@ -436,6 +462,32 @@ const TaskPanel: React.FC = () => {
>
🔌
</button>
{storageConfig.remoteEnabled && (
<div className="relative group">
<button
onClick={async () => {
try {
setIsSyncing(true);
const userId = storageConfig.userId || 'anonymous';
setStorageConfig(prev => ({ ...prev, userId }));
await sync();
} catch (error) {
console.error('Sync failed:', error);
} finally {
setIsSyncing(false);
}
}}
className="p-2 rounded-lg bg-[#2A2A2A] hover:bg-[#333333] transition-colors"
title="Sync Tasks"
disabled={isSyncing || isLoading}
>
{isSyncing ? '⏳' : '🔄'}
</button>
<span className="absolute hidden group-hover:block bottom-full left-1/2 transform -translate-x-1/2 mb-2 px-2 py-1 text-xs bg-gray-900 text-white rounded whitespace-nowrap">
{isSyncing ? 'Syncing...' : 'Sync Tasks'}
</span>
</div>
)}
<button
onClick={() => setShowLogin(true)}
className="p-2 rounded-lg bg-[#2A2A2A] hover:bg-[#333333] transition-colors"
Expand Down Expand Up @@ -525,21 +577,28 @@ const TaskPanel: React.FC = () => {
reader.onload = (event) => {
const csv = event.target?.result as string;
const lines = csv.split('\n');
const userId = storageConfig.userId || 'anonymous';
const importedTasks = lines.map(line => {
const [title, description, status, priority, dueDate, tags, assignees] = line.split(',');
return {
const task: Task = {
id: crypto.randomUUID(),
title,
description,
status: status as Task['status'],
priority: priority as Task['priority'],
title: title || 'Untitled Task',
description: description || '',
status: (status as Task['status']) || 'todo',
priority: (priority as Task['priority']) || 'medium',
dueDate: dueDate ? new Date(dueDate) : undefined,
tags: tags ? tags.split(', ').filter(t => t) : undefined,
assignees: assignees ? assignees.split(', ').filter(a => a) : undefined,
createdAt: new Date(),
updatedAt: new Date(),
listId: currentList,
owner: userId,
collaborators: [],
activityLog: [],
comments: [],
version: 1
};
return task;
});
importTasks(importedTasks, currentList);
e.target.value = ''; // Reset file input
Expand Down Expand Up @@ -697,7 +756,8 @@ const TaskPanel: React.FC = () => {
addList({ id: newListId, name: 'New List' });

// Create a default task for the new list
addTask({
const userId = storageConfig.userId || 'anonymous';
const newTask: Task = {
id: crypto.randomUUID(),
title: 'New Task',
description: 'This is a default task for the new list.',
Expand All @@ -706,7 +766,13 @@ const TaskPanel: React.FC = () => {
listId: newListId,
createdAt: new Date(),
updatedAt: new Date(),
});
owner: userId,
collaborators: [],
activityLog: [],
comments: [],
version: 1
};
addTask(newTask);

setCurrentList(newListId);
setShowListActions(false);
Expand Down
105 changes: 72 additions & 33 deletions app/hooks/useTasks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,44 +4,80 @@ import { useState, useEffect } from 'react';
import { Task, TaskList } from '../types/task';
import { ActivityLog } from '../types/collaboration';
import { findChanges, mergeTaskChanges } from '../utils/collaboration';
import { loadTasksFromLocalStorage, saveTasksToLocalStorage, loadListsFromLocalStorage, saveListsToLocalStorage } from '../utils/storage';
import { createStorageManager } from '../utils/storage';
import { StorageConfig } from '../types/storage';

const TASKS_STORAGE_KEY = 'tasks';
const LISTS_STORAGE_KEY = 'lists';

export const useTasks = () => {
export const useTasks = (storageConfig: StorageConfig) => {
const [tasks, setTasks] = useState<Task[]>([]);
const [lists, setLists] = useState<TaskList[]>([]);

// Load tasks and lists from localStorage only on client-side after initial render
const [isLoading, setIsLoading] = useState(true);
const [error, setError] = useState<Error | null>(null);
const storage = createStorageManager(storageConfig);

// Load tasks and lists from storage
useEffect(() => {
const storedTasks = loadTasksFromLocalStorage() || [];
const storedLists = loadListsFromLocalStorage() || [{ id: 'default', name: 'General Task List' }];

if (storedTasks.length === 0 && storedLists.length > 0) {
const defaultListId = storedLists[0].id;
const defaultTask: Task = {
id: 'default-task',
title: 'Example Task',
listId: defaultListId,
description: 'This is an example task.',
createdAt: new Date(),
updatedAt: new Date(),
completedAt: undefined,
priority: 'medium',
status: 'todo',
};
setTasks([defaultTask]);
} else {
setTasks(storedTasks);
}
setLists(storedLists);
}, []);
const loadData = async () => {
try {
setIsLoading(true);
const [storedTasks, storedLists] = await Promise.all([
storage.getTasks(),
storage.getLists()
]);

const defaultList = { id: 'default', name: 'General Task List' };
const initialLists = storedLists.length > 0 ? storedLists : [defaultList];
setLists(initialLists);

if (storedTasks.length === 0 && initialLists.length > 0) {
const defaultTask: Task = {
id: 'default-task',
title: 'Example Task',
listId: initialLists[0].id,
description: 'This is an example task.',
createdAt: new Date(),
updatedAt: new Date(),
completedAt: undefined,
priority: 'medium',
status: 'todo',
owner: storageConfig.userId || 'anonymous',
collaborators: [],
activityLog: [],
comments: [],
version: 1
};
setTasks([defaultTask]);
await storage.saveTasks([defaultTask]);
} else {
setTasks(storedTasks);
}
} catch (err) {
setError(err instanceof Error ? err : new Error('Failed to load data'));
} finally {
setIsLoading(false);
}
};

loadData();
}, [storage]);

// Sync with storage when tasks or lists change
useEffect(() => {
saveTasksToLocalStorage(tasks);
saveListsToLocalStorage(lists);
}, [tasks, lists]);
const syncData = async () => {
try {
await Promise.all([
storage.saveTasks(tasks),
storage.saveLists(lists)
]);
} catch (err) {
console.error('Error syncing data:', err);
}
};

if (!isLoading) {
syncData();
}
}, [tasks, lists, storage, isLoading]);

const addTask = (task: Task) => {
if (!task.listId) {
Expand All @@ -61,7 +97,7 @@ export const useTasks = () => {
const newActivityLogs: ActivityLog[] = changes.map(change => ({
id: Date.now().toString(),
taskId: updatedTask.id,
userId: currentUser,
userId: storageConfig.userId || 'anonymous',
action: 'updated',
timestamp: new Date(),
details: {
Expand All @@ -78,7 +114,7 @@ export const useTasks = () => {
activityLog: [...(oldTask.activityLog || []), ...newActivityLogs],
lastViewed: {
...(oldTask.lastViewed || {}),
[currentUser]: new Date()
[storageConfig.userId || 'anonymous']: new Date()
}
};
setTasks(prev => prev.map(t => t.id === updatedTask.id ? updatedTask : t));
Expand Down Expand Up @@ -125,5 +161,8 @@ export const useTasks = () => {
addList,
updateList,
deleteList,
sync,
isLoading,
error
};
};
13 changes: 13 additions & 0 deletions app/types/remote-storage.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
declare module '@frigade/remote-storage' {
export interface RemoteStorageOptions {
apiKey: string;
userId: string;
}

export class RemoteStorage {
constructor(options: RemoteStorageOptions);
get(key: string): Promise<string | null>;
set(key: string, value: string): Promise<void>;
delete(key: string): Promise<void>;
}
}
27 changes: 27 additions & 0 deletions app/types/storage.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { Task, TaskList } from './task';

export interface StorageConfig {
remoteEnabled: boolean;
apiKey?: string;
userId?: string;
}

export interface StorageProvider {
getTasks(): Promise<Task[]>;
saveTasks(tasks: Task[]): Promise<void>;
getLists(): Promise<TaskList[]>;
saveLists(lists: TaskList[]): Promise<void>;
sync(): Promise<SyncResult>;
}

export interface SyncResult {
tasks: Task[];
lists: TaskList[];
lastSynced: Date;
}

export interface RemoteStorageOptions {
apiKey: string;
userId: string;
namespace?: string;
}
Loading

0 comments on commit 98433b9

Please sign in to comment.