Skip to content

Commit

Permalink
Fixes #200: collaborative projects
Browse files Browse the repository at this point in the history
  • Loading branch information
hussaino03 committed Dec 29, 2024
1 parent ba3fb05 commit d1b38f7
Show file tree
Hide file tree
Showing 17 changed files with 1,126 additions and 103 deletions.
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

0 comments on commit d1b38f7

Please sign in to comment.