Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fixes #200: collaborative projects #207

Merged
merged 1 commit into from
Dec 29, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,13 @@ QuestLog is an AI-powered productivity platform that turns task/project manageme
- Personalized task insights and recommendations
- Analyzes task completion patterns, task completion rates, recent accomplishments, XP progression, and performance trends

#### 👥 Collaborative Projects
- Real-time project sharing and collaboration
- Invite system via shareable codes
- Synchronized progress across team members
- Team activity tracking
- Project-specific collaboration settings

#### 📈 Progress System
- Experience points (XP) and leveling
- Achievement badges
Expand Down Expand Up @@ -184,6 +191,10 @@ classDef database fill:#4479a1,stroke:#333,stroke-width:2px
- `GET /api/leaderboard`: Retrieve leaderboard data
- `POST /api/auth/google`: Handle Google OAuth authentication
- `GET /api/auth/<integrations>` : integrations OAuth import
- `POST /api/projects/:id/share`: Generate project share code
- `POST /api/projects/:id/join`: Join project via share code
- `GET /api/projects/:id/collaborators`: Get project collaborators
- `DELETE /api/projects/:id/collaborators/:userId`: Remove collaborator

## 💾 Data Persistence
- All data synced with MongoDB
Expand Down
Binary file modified client/public/demo/demo.mp4
Binary file not shown.
74 changes: 73 additions & 1 deletion client/src/App.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import './styles/globals.css';
import React, { useState, useEffect, useMemo } from 'react';
import { Users } from 'lucide-react';
import { CSSTransition, SwitchTransition } from 'react-transition-group';
import { Analytics } from '@vercel/analytics/react';
import { BrowserRouter, Routes, Route, Navigate } from 'react-router-dom';
Expand Down Expand Up @@ -31,6 +32,7 @@ import ThemeManager from './services/theme/ThemeManager';
import StreakManager from './services/streak/StreakManager';
import ViewManager from './services/view/ViewManager';
import BadgeManager from './services/badge/BadgeManager';
import CollaborationManager from './services/collaboration/CollaborationManager';

const AppContent = () => {
const [isDark, setIsDark] = useState(false);
Expand All @@ -47,6 +49,7 @@ const AppContent = () => {
const [currentStreak, setCurrentStreak] = useState(0);
const [loading, setLoading] = useState(true);
const [streakData, setStreakData] = useState({ current: 0, longest: 0 });
const [showAnnouncement, setShowAnnouncement] = useState(false);

const {
level,
Expand Down Expand Up @@ -87,6 +90,10 @@ const AppContent = () => {
() => new BadgeManager(setUnlockedBadges, addNotification),
[addNotification]
);
const collaborationManager = useMemo(
() => new CollaborationManager(setTasks, setError),
[]
);

useEffect(() => {
themeManager.initializeTheme();
Expand Down Expand Up @@ -145,6 +152,36 @@ const AppContent = () => {
}
}, [userId, userName, addNotification]);

// Temporary announcement banner starts
useEffect(() => {
if (userId) {
const stored = localStorage.getItem('announcements');
const allAnnouncements = stored ? JSON.parse(stored) : [];
const collaborationAnnouncementSeen = allAnnouncements.some(
(a) => a.type === 'collaboration' && a.seen === true
);

if (!collaborationAnnouncementSeen) {
setShowAnnouncement(true);
}
}
}, [userId]);

const dismissAnnouncement = () => {
const stored = localStorage.getItem('announcements');
const allAnnouncements = stored ? JSON.parse(stored) : [];

allAnnouncements.push({
type: 'collaboration',
seen: true,
timestamp: new Date().toISOString()
});

localStorage.setItem('announcements', JSON.stringify(allAnnouncements));
setShowAnnouncement(false);
};
// Temporary announcement banner ends

const handleClearDataClick = () => {
setShowClearDataModal(true);
};
Expand Down Expand Up @@ -198,6 +235,36 @@ const AppContent = () => {
</div>
)}

{/* Temporary announcement banner */}
{showAnnouncement && (
<div className="relative bg-gradient-to-r from-blue-500/10 via-purple-500/10 to-blue-500/10 dark:from-blue-400/10 dark:via-purple-400/10 dark:to-blue-400/10 border-b border-blue-200 dark:border-blue-800">
<div className="max-w-7xl mx-auto py-3 px-4">
<div className="flex items-center justify-between gap-4">
<div className="flex items-center gap-3">
<span className="flex h-8 w-8 items-center justify-center rounded-full bg-blue-500/10 dark:bg-blue-400/10">
<Users className="h-5 w-5 text-blue-600 dark:text-blue-400" />
</span>
<div>
<p className="text-sm font-medium text-gray-900 dark:text-white">
New Feature: Collaborative Projects
</p>
<p className="text-sm text-gray-600 dark:text-gray-400">
Share and collaborate on projects with your team in real-time
</p>
</div>
</div>
<button
onClick={dismissAnnouncement}
className="shrink-0 w-8 h-8 rounded-lg flex items-center justify-center bg-red-500/10 hover:bg-red-500/20 transition-colors"
aria-label="Dismiss announcement"
>
<span className="text-red-600 dark:text-red-400 text-lg">×</span>
</button>
</div>
</div>
</div>
)}

{/* Main Layout Container */}
<div className="flex flex-col min-h-screen">
<div className="flex-1 max-w-7xl mx-auto px-4 py-6 w-full">
Expand All @@ -219,7 +286,10 @@ const AppContent = () => {
}
onClearDataClick={handleClearDataClick}
/>
<Form addTask={taskManager.addTask} />
<Form
addTask={taskManager.addTask}
taskManager={taskManager}
/>
</div>
</div>

Expand All @@ -242,6 +312,8 @@ const AppContent = () => {
isCompleted={false}
addTask={taskManager.addTask}
updateTask={taskManager.updateTask}
collaborationManager={collaborationManager}
userId={userId}
/>
)}
{currentView === 'completed' && (
Expand Down
28 changes: 15 additions & 13 deletions client/src/components/Landing/Landing.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@ import {
Trophy,
Sparkles,
ChartLine,
Flag
Flag,
Users
} from 'lucide-react';
import Footer from '../Layout/Footer';

Expand Down Expand Up @@ -155,9 +156,9 @@ const Landing = ({ isDark, onToggle }) => {
{/* Secondary Content - Stats */}
<div className="mt-16 grid grid-cols-2 md:grid-cols-4 gap-4 max-w-3xl mx-auto">
{[
{ label: 'Active Users', value: '250+' },
{ label: 'Tasks Completed', value: '500+' },
{ label: 'Total XP Earned', value: '350K+' },
{ label: 'Active Users', value: '270+' },
{ label: 'Tasks Completed', value: '520+' },
{ label: 'Total XP Earned', value: '360K+' },
{ label: 'Badges Created', value: '30+' }
].map((stat, index) => (
<div
Expand Down Expand Up @@ -213,16 +214,16 @@ const Landing = ({ isDark, onToggle }) => {
<div className="grid grid-cols-1 md:grid-cols-3 gap-8">
{[
{
icon: <Brain className="w-8 h-8" />,
title: 'AI Assistant',
icon: <Trophy className="w-8 h-8" />,
title: 'Gamified Progress',
description:
'Get personalized insights and quest optimization suggestions from our Gemini-powered AI'
'Earn XP, unlock achievements, and switch seamlessly between tasks and projects. Sync your progress across devices with cloud saves'
},
{
icon: <Trophy className="w-8 h-8" />,
title: 'Gamified Progress',
icon: <Users className="w-8 h-8" />,
title: 'Collaborative Projects',
description:
'Earn XP, unlock achievements, and switch seamlessly between tasks and projects'
'Invite teammates via shareable codes and sync progress across your team in real-time'
},
{
icon: <ChartLine className="w-8 h-8" />,
Expand All @@ -243,10 +244,10 @@ const Landing = ({ isDark, onToggle }) => {
'Compete with others globally while maintaining privacy control'
},
{
icon: <Rocket className="w-8 h-8" />,
title: 'Cross-Platform',
icon: <Brain className="w-8 h-8" />,
title: 'AI Assistant',
description:
'Sync your progress across devices with cloud saves'
'Get personalized insights and quest optimization suggestions from our Gemini-powered AI'
}
].map((feature, index) => (
<div
Expand Down Expand Up @@ -293,3 +294,4 @@ const Landing = ({ isDark, onToggle }) => {
};

export default Landing;

120 changes: 91 additions & 29 deletions client/src/components/Modal Management/Layout/Form.js
Original file line number Diff line number Diff line change
@@ -1,18 +1,20 @@
import React, { useState, useEffect } from 'react';
import { createPortal } from 'react-dom';
import { ClipboardList, FolderTree } from 'lucide-react';
import { ClipboardList, FolderTree, UsersRound } from 'lucide-react';
import TaskForm from '../Tasks/TaskForm';
import ProjectForm from '../Projects/ProjectForm';

const Form = ({ addTask }) => {
const [isProjectView, setIsProjectView] = useState(false);
const Form = ({ addTask, taskManager }) => {
const [currentView, setCurrentView] = useState('task'); // 'task', 'project', or 'join'
const [joinCode, setJoinCode] = useState('');
const [error, setError] = useState('');

const handleClose = () => {
const modal = document.getElementById('newtask-form');
if (modal) {
modal.style.display = 'none';
}
setIsProjectView(false);
setCurrentView('task');
};

useEffect(() => {
Expand All @@ -24,34 +26,42 @@ const Form = ({ addTask }) => {
};
}, []);

const handleJoinProject = async (e) => {
e.preventDefault();
if (!joinCode.trim()) {
setError('Please enter a project code');
return;
}

const success = await taskManager.joinProject(joinCode.trim());
if (success) {
handleClose();
} else {
setError('Invalid project code. Please check and try again.');
}
};

const modalContent = (
<div
id="newtask-form"
className="hidden fixed inset-0 bg-black/50 backdrop-blur-sm
animate-fadeIn"
style={{
display: 'none',
zIndex: 9999
}}
className="hidden fixed inset-0 bg-black/50 backdrop-blur-sm animate-fadeIn"
style={{ display: 'none', zIndex: 9999 }}
>
<div className="flex items-center justify-center p-4 min-h-screen">
<div
className="relative w-full max-w-md bg-white dark:bg-gray-800 rounded-lg shadow-xl
<div className="relative w-full max-w-md bg-white dark:bg-gray-800 rounded-lg shadow-xl
transform scale-100 animate-modalSlide max-h-[calc(100vh-2rem)] overflow-y-auto"
onClick={(e) => e.stopPropagation()}
>
onClick={(e) => e.stopPropagation()}>
<div className="p-6 space-y-6">
{/* toggle */}
{/* Toggle Buttons */}
<div className="flex items-center justify-between">
<div className="flex bg-gray-100 dark:bg-gray-700 p-1 rounded-lg">
<button
type="button"
onClick={() => setIsProjectView(false)}
onClick={() => setCurrentView('task')}
className={`px-4 py-2 text-sm rounded-md transition-all duration-200
${
!isProjectView
? 'bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100 shadow-sm'
: 'text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-gray-200'
${currentView === 'task'
? 'bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100 shadow-sm'
: 'text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-gray-200'
}`}
>
<div className="flex items-center gap-2">
Expand All @@ -61,32 +71,84 @@ const Form = ({ addTask }) => {
</button>
<button
type="button"
onClick={() => setIsProjectView(true)}
onClick={() => setCurrentView('project')}
className={`px-4 py-2 text-sm rounded-md transition-all duration-200
${
isProjectView
? 'bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100 shadow-sm'
: 'text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-gray-200'
${currentView === 'project'
? 'bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100 shadow-sm'
: 'text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-gray-200'
}`}
>
<div className="flex items-center gap-2">
<FolderTree className="w-4 h-4" />
<span>Project</span>
</div>
</button>
<button
type="button"
onClick={() => setCurrentView('join')}
className={`px-4 py-2 text-sm rounded-md transition-all duration-200
${currentView === 'join'
? 'bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100 shadow-sm'
: 'text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-gray-200'
}`}
>
<div className="flex items-center gap-2">
<UsersRound className="w-4 h-4" />
<span>Join</span>
</div>
</button>
</div>
{/* Close button */}
<button
type="button"
onClick={handleClose}
className="w-8 h-8 rounded-lg flex items-center justify-center bg-red-500/10 hover:bg-red-500/20 transition-colors"
>
<span className="text-red-600 dark:text-red-400 text-lg">
×
</span>
<span className="text-red-600 dark:text-red-400 text-lg">×</span>
</button>
</div>

{isProjectView ? (
{currentView === 'join' ? (
<form onSubmit={handleJoinProject}>
<div className="space-y-4">
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
Received an invitation to collaborate?
</label>
<input
type="text"
value={joinCode}
onChange={(e) => {
setJoinCode(e.target.value);
setError('');
}}
placeholder="Paste project code here"
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600
rounded-lg bg-white dark:bg-gray-700 text-gray-900
dark:text-white focus:ring-2 focus:ring-blue-500"
/>
{error && (
<p className="mt-1 text-sm text-red-600 dark:text-red-400">
{error}
</p>
)}
</div>
<button
type="submit"
className="w-full p-1.5 sm:p-2 rounded-lg bg-white dark:bg-gray-800 font-bold text-base sm:text-lg
border-2 border-gray-800 text-gray-800 dark:text-gray-200
shadow-[2px_2px_#2563EB] hover:shadow-none hover:translate-x-0.5
hover:translate-y-0.5 transition-all duration-200"
>
<div className="flex items-center justify-center gap-3">
<span>👥</span>
<span>Join Project</span>
<span className="text-sm opacity-75">(Enter ↵)</span>
</div>
</button>
</div>
</form>
) : currentView === 'project' ? (
<ProjectForm addTask={addTask} />
) : (
<TaskForm addTask={addTask} />
Expand Down
Loading
Loading