Skip to content

Commit

Permalink
Merge pull request #305 from NIAEFEUP/251-create-collaborative-sessio…
Browse files Browse the repository at this point in the history
…n-frontend-components

251 create collaborative session frontend components
  • Loading branch information
jose-carlos-sousa authored Sep 16, 2024
2 parents 6279f09 + 9092376 commit 952e090
Show file tree
Hide file tree
Showing 9 changed files with 420 additions and 14 deletions.
9 changes: 9 additions & 0 deletions src/@types/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -94,3 +94,12 @@ export type ImportedCourses = {
[key: string]: string
}

export type CollabSession = {
id: number
name: string
lastEdited: string
lifeSpan: number
currentUser: string
link : string
participants: Array<string>
}
2 changes: 1 addition & 1 deletion src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ const App = () => {
<Layout location={page.location} title={page.location} liquid={page.liquid}>
<div>
<page.element />
<Toaster />
<Toaster/>
</div>
</Layout>
}
Expand Down
7 changes: 5 additions & 2 deletions src/components/planner/sidebar/SessionController.tsx
Original file line number Diff line number Diff line change
@@ -1,16 +1,19 @@
import CoursePicker from './sessionController/CoursePicker'
import Export from './sessionController/Export'
import Refresh from './sessionController/Refresh'

import CollaborativeSession from './sessionController/CollaborativeSession'
import DevMode from '../../ui/DevMode'
/**
* Sidebar with all the main schedule interactions
*/
const SessionController = () => {
return (
<div className="flex w-full gap-1">
<CoursePicker />
{/* <CollaborativeSession /> */}
<Refresh />
<DevMode>
<CollaborativeSession />
</DevMode>
<Export />
</div>
)
Expand Down
138 changes: 138 additions & 0 deletions src/components/planner/sidebar/sessionController/CollabModal.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
import React, { useContext, useState, useEffect, Fragment } from 'react';
import { Dialog, Transition } from '@headlessui/react';
import { XMarkIcon } from '@heroicons/react/24/solid';
import CollabPickSession from './CollabPickSession';
import CollabSession from './CollabSession';
import CollabSessionContext from '../../../../contexts/CollabSessionContext';

const PICK_SESSION = 'PICK_SESSION';
const SESSION = 'SESSION';
const generateUniqueId = () => Date.now();
const CollabModal = ({ isOpen, closeModal }) => {
const { sessions, setSessions, currentSessionId, setcurrentSessionId } = useContext(CollabSessionContext);
const [currentView, setCurrentView] = useState(PICK_SESSION); //Defines in which modal we are

useEffect(() => {
if (isOpen) {
if (currentSessionId !== null && sessions.find(s => s.id === currentSessionId)) {
setCurrentView(SESSION);
} else {
setCurrentView(PICK_SESSION);
}
}
}, [isOpen, currentSessionId, sessions]);

const currentSession = sessions.find(s => s.id === currentSessionId) || null;

const handleStartSession = (sessionId) => {
setcurrentSessionId(sessionId);
setCurrentView(SESSION);
};

const handleCreateSession = () => { //Dummy function to create a session...
const newSession = {
id: generateUniqueId(),
name: Math.random().toString(36).substr(2, 9),
lastEdited: new Date().toLocaleDateString(),
lifeSpan: 30,
currentUser: 'TheCreator',
link: `https://collab.app/session/${Date.now().toString()}`,
participants: ['TheCreator'],
};
console.log('CollabModal -> newSession', newSession);
setSessions(prevSessions => [...prevSessions, newSession]);
setcurrentSessionId(newSession.id);
setCurrentView(SESSION);
};

const handleExitSession = () => {
setcurrentSessionId(null);
setCurrentView(PICK_SESSION);
};

const handleDeleteSession = (sessionId) => {
setSessions(prevSessions => prevSessions.filter(session => session.id !== sessionId));
if (currentSession?.id === sessionId) {
handleExitSession();
}
};

const handleUpdateUser = (updatedUser) => {
if (currentSession) {
const updatedSession = {
...currentSession,
currentUser: updatedUser,
participants: currentSession.participants.map(participant =>
participant === currentSession.currentUser ? updatedUser : participant
)
};
setSessions(prevSessions =>
prevSessions.map(session =>
session.id === currentSession.id ? updatedSession : session
)
);
}
};
return (
<Transition appear show={isOpen} as={Fragment}>
<Dialog as="div" className="relative z-10" onClose={closeModal}>
<Transition.Child
as={Fragment}
enter="ease-out duration-300"
enterFrom="opacity-0"
enterTo="opacity-100"
leave="ease-in duration-200"
leaveFrom="opacity-100"
leaveTo="opacity-0"
>
<div className="fixed inset-0 bg-black bg-opacity-25" />
</Transition.Child>

<div className="fixed inset-0 overflow-y-auto">
<div className="flex min-h-full items-center justify-center p-4 text-center">
<Transition.Child
as={Fragment}
enter="ease-out duration-300"
enterFrom="opacity-0 scale-95"
enterTo="opacity-100 scale-100"
leave="ease-in duration-200"
leaveFrom="opacity-100 scale-100"
leaveTo="opacity-0 scale-95"
>
<Dialog.Panel className="w-full max-w-xl transform overflow-hidden rounded-2xl bg-white p-6 text-left align-middle shadow-xl transition-all">
<div className="flex justify-end">
<button
type="button"
className="text-gray-400 hover:text-gray-600"
onClick={closeModal}
>
<XMarkIcon className="h-6 w-6" />
</button>
</div>

{currentView === PICK_SESSION && (
<CollabPickSession
sessions={sessions}
onStartSession={handleStartSession}
onCreateSession={handleCreateSession}
onDeleteSession={handleDeleteSession}
/>
)}

{currentView === SESSION && currentSession && (
<CollabSession
session={currentSession}
onExitSession={handleExitSession}
onUpdateUser={handleUpdateUser}
/>
)}
</Dialog.Panel>
</Transition.Child>
</div>
</div>
</Dialog>
</Transition>
);
};

export default CollabModal;
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import React from 'react';
import { PlayCircleIcon, UserGroupIcon } from '@heroicons/react/20/solid';
import { Button } from '../../../ui/button';

const CollabPickSession = ({ sessions, onStartSession, onCreateSession, onDeleteSession }) => (
<div className="text-center">
<UserGroupIcon className="h-40 w-40 mx-auto text-primary" />
<h3 className="text-xl font-bold leading-6 text-primary">
Colaboração ao vivo...
</h3>

<p className="mt-6 text-sm text-gray-600">
<span className="font-bold block text-gray-800">
Podes convidar amigos para as tuas opções para colaborar contigo.
</span>
Não te preocupes, todas as tuas opções continuam a guardar localmente no teu dispositivo.
</p>

<div className="flex justify-center mt-6 space-x-4 ">
<Button
variant="icon"
className="flex items-center rounded-lg bg-primary py-6"
onClick={onCreateSession}
>
<PlayCircleIcon className="h-8 w-8 mr-2 " />
Iniciar nova sessão
</Button>
</div>

<div className="mt-6 text-center">
<h4 className="text-md font-bold ">Sessões anteriores</h4>
<p className="text-sm text-gray-600">
As sessões têm um tempo de vida, pelo que se não quiseres perder as tuas opções, terás de guardar para o teu dispositivo localmente.
</p>
<ul className="mt-4 flex flex-col sm:grid sm:grid-cols-1 sm:gap-y-4">
{sessions.map((session) => (
<li key={session.id} className="sm:grid sm:grid-cols-7 flex flex-col sm:mt-0 mt-6 items-center text-sm text-gray-800 gap-4">
<span className="col-span-2 truncate whitespace-nowrap font-bold">{session.name}</span>
<span className="col-span-2 text-gray-600 truncate whitespace-nowrap">editado {session.lastEdited}</span>
<span className="col-span-2 text-gray-600 truncate whitespace-nowrap">expira em {session.lifeSpan} dias</span>
<div className="col-span-1 flex justify-end space-x-4">
<a
href="#"
className="text-primary hover:underline"
onClick={() => onStartSession(session.id)}
>
Entrar
</a>
<button
className="text-primary hover:text-red-800"
onClick={() => onDeleteSession(session.id)}
>
&times;
</button>
</div>
</li>
))}
</ul>
</div>
</div>
);

export default CollabPickSession;
127 changes: 127 additions & 0 deletions src/components/planner/sidebar/sessionController/CollabSession.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
import React, { useState } from 'react';
import { DocumentDuplicateIcon, CheckIcon } from '@heroicons/react/24/outline';
import { StopIcon } from '@heroicons/react/24/solid';
import { Button } from '../../../ui/button';
import { useToast } from '../../../ui/use-toast';

const pastelColors = [ //Colors for the participants
'bg-orange-200 text-orange-700',
'bg-blue-200 text-blue-800',
'bg-yellow-200 text-yellow-800',
'bg-green-200 text-green-800',
'bg-purple-200 text-purple-800',
];

const CollabSession = ({ session, onExitSession, onUpdateUser }) => {
const { toast } = useToast();
const [copied, setCopied] = useState(false);
const [lastValidUser, setLastValidUser] = useState(session.currentUser);

const handleCopyLink = () => {
navigator.clipboard.writeText(session.link);
toast({
title: 'Link copiado',
description: 'Podes partilhar o link com amigos para colaborar contigo.',
});
setCopied(true);
setTimeout(() => {
setCopied(false);
}, 2000);
};

const handleUserChange = (e) => {
const newValue = e.target.value.trim();
if (newValue !== '') {
setLastValidUser(newValue);
}
};

const handleUserBlur = (e) => {
const newValue = e.target.value.trim();
if (newValue !== '') {
onUpdateUser(newValue);
} else {
onUpdateUser(lastValidUser);
}
};

return (
<div className="text-left">
<h3 className="text-xl font-bold leading-6 mb-6">Colaboração ao vivo...</h3>

<div className="mt-4">
<label className="block text-sm font-medium text-gray-700 mb-1">O teu nome</label>
<input
type="text"
defaultValue={session.currentUser}
onChange={handleUserChange}
onBlur={handleUserBlur}
className="mt-1 block w-full rounded-md border-gray-300 shadow-sm"
/>
</div>

<div className="mt-4">
<label className="block text-sm font-medium text-gray-700 mb-1">Link</label>
<div className="flex items-center mt-1">
<input
type="text"
value={session.link}
className="flex-1 block w-full rounded-md bg-red-50 border-gray-300 shadow-sm sm:text-sm"
readOnly
/>
<Button
variant="icon"
className={`ml-2 px-3 py-1 flex items-center ${copied ? 'bg-green-200 text-white' : 'bg-primary text-white'} text-sm font-medium rounded-lg min-w-[120px]`}
onClick={handleCopyLink}
>
{copied ? (
<CheckIcon className="h-5 w-5 text-green-700" />
) : (
<DocumentDuplicateIcon className="h-5 w-5" />
)}
{copied ? '' : ' Copiar link'}
</Button>
</div>
</div>
<div className="mt-6">
<h4 className="text-md font-medium text-gray-900 mb-2">Participantes</h4>
<div className="flex flex-wrap space-x-2">
{session.participants.map((user, index) => (
<div key={index} className="relative group mb-2">
<div
className={`rounded-full h-10 w-10 flex items-center justify-center ${
pastelColors[index % pastelColors.length]
}`}
>
{user[0]}
</div>
{user && (
<div className="absolute bottom-12 left-1/2 transform -translate-x-1/2 invisible opacity-0 group-hover:visible group-hover:opacity-100 transition-opacity duration-200 bg-gray-900 text-white text-xs rounded-md px-2 py-1">
{user}
</div>
)}
</div>
))}
</div>
</div>


<p className="mt-6 text-sm text-gray-600">
Interromper a sessão irá desconectar-te da sala, mas podes continuar a criar e ver mais combinações de horários localmente
</p>

<div className="mt-6 text-center">
<Button
type="button"
className="px-4 py-2 bg-white border border-primary text-primary font-medium rounded-lg hover:bg-primary hover:text-white focus:outline-none focus-visible:ring-2 focus-visible:ring-red-500"
onClick={onExitSession}
>
<StopIcon className="h-5 w-5 mr-2" />
Sair da sessão
</Button>
</div>
</div>
);
};

export default CollabSession;
Loading

0 comments on commit 952e090

Please sign in to comment.