Skip to content

Commit

Permalink
Add export all posts functionality for group admins to address issue c…
Browse files Browse the repository at this point in the history
  • Loading branch information
jayana-cpc committed Jul 29, 2024
1 parent 04ad0e0 commit d88761a
Show file tree
Hide file tree
Showing 2 changed files with 227 additions and 11 deletions.
45 changes: 34 additions & 11 deletions src/components/Groups/GroupPage/GroupPageHeader.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,13 @@ import { useActiveGroup } from '../../../hooks/groups/useActiveGroup';
import { useGroupActions } from '../../../hooks/groups/useGroupActions';
import { usePostActions } from '../../../hooks/groups/usePostActions';
import { GroupData, isUserAdminOfGroup } from '../../../models/groups/groups';
import PostExportAllModal from './PostExportAllModal';

export default function GroupPageHeader(props: { group: GroupData }) {
const { leaveGroup } = useGroupActions();
const { createNewPost } = usePostActions(props.group?.id);
const [isActionsOpen, setIsActionsOpen] = useState(false);
const [showExportModal, setShowExportModal] = useState(false);
const { showAdminView, setInStudentView } = useActiveGroup();
const firebaseUser = useFirebaseUser();
const ref = useRef<HTMLDivElement>(null);
Expand All @@ -25,6 +27,10 @@ export default function GroupPageHeader(props: { group: GroupData }) {
return () => document.removeEventListener('mousedown', handleClick);
}, []);

const handleExportAllPosts = () => {
setShowExportModal(true);
};

return (
<header className="py-6 sm:py-12 bg-gradient-to-r from-light-blue-800 to-cyan-600">
<div className="max-w-7xl px-4 sm:px-8 mx-auto md:flex md:items-center md:justify-between">
Expand Down Expand Up @@ -129,17 +135,26 @@ export default function GroupPageHeader(props: { group: GroupData }) {
)}
{firebaseUser &&
isUserAdminOfGroup(props.group, firebaseUser.uid) && (
<button
type="button"
className="block w-full text-left px-4 py-2 text-sm text-gray-700 hover:bg-gray-100 hover:text-gray-900 focus:outline-none focus:bg-gray-100 focus:text-gray-900"
onClick={() => {
setInStudentView(showAdminView);
}}
>
{showAdminView
? 'Enter Student View'
: 'Exit Student View'}
</button>
<>
<button
type="button"
className="block w-full text-left px-4 py-2 text-sm text-gray-700 hover:bg-gray-100 hover:text-gray-900 focus:outline-none focus:bg-gray-100 focus:text-gray-900"
onClick={() => {
setInStudentView(showAdminView);
}}
>
{showAdminView
? 'Enter Student View'
: 'Exit Student View'}
</button>
<button
type="button"
className="block w-full text-left px-4 py-2 text-sm text-gray-700 hover:bg-gray-100 hover:text-gray-900 focus:outline-none focus:bg-gray-100 focus:text-gray-900"
onClick={handleExportAllPosts}
>
Export All Posts
</button>
</>
)}
<button
type="button"
Expand Down Expand Up @@ -173,6 +188,14 @@ export default function GroupPageHeader(props: { group: GroupData }) {
</div>
</div>
</div>
{showExportModal && (
<PostExportAllModal
showExportModal={showExportModal}
onClose={() => setShowExportModal(false)}
group={props.group}
/>
)}
</header>
);
}

193 changes: 193 additions & 0 deletions src/components/Groups/GroupPage/PostExportAllModal.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,193 @@
import { Dialog, Transition } from '@headlessui/react';
import {
CollectionReference,
arrayUnion,
collection,
doc,
getDocs,
getFirestore,
query,
serverTimestamp,
writeBatch,
} from 'firebase/firestore';
import React, { Fragment, useState } from 'react';
import { useFirebaseUser } from '../../../context/UserDataContext/UserDataContext';
import { useUserGroups } from '../../../hooks/groups/useUserGroups';
import { useFirebaseApp } from '../../../hooks/useFirebase';
import { GroupData } from '../../../models/groups/groups';
import { PostData } from '../../../models/groups/posts';
import { GroupProblemData } from '../../../models/groups/problem';

export default function PostExportAllModal(props: {
showExportModal: boolean;
onClose: () => void;
group: GroupData;
}) {
const firebaseApp = useFirebaseApp();
const firebaseUser = useFirebaseUser();
const groups = useUserGroups();
const [groupsUsedMap, setGroupsUsedMap] = useState(new Map<string, MapData>());

async function handleGroupExportChange(g: GroupData) {
if (groupsUsedMap.has(g.id)) {
setGroupsUsedMap(
new Map(
groupsUsedMap.set(g.id, new MapData(!groupsUsedMap.get(g.id)?.used, g))
)
);
} else {
setGroupsUsedMap(new Map(groupsUsedMap.set(g.id, new MapData(true, g))));
}
}

async function exportAllPosts() {
const firestore = getFirestore(firebaseApp);
const postsQuery = query(
collection(firestore, 'groups', props.group.id, 'posts') as CollectionReference<PostData>
);
const postsSnapshot = await getDocs(postsQuery);
const posts = postsSnapshot.docs.map(doc => ({ id: doc.id, ...doc.data() }));

for (const [groupId, mapData] of groupsUsedMap.entries()) {
if (mapData.used) {
const batch = writeBatch(firestore);
for (const post of posts) {
const newPostRef = doc(collection(firestore, 'groups', groupId, 'posts'));
batch.set(newPostRef, { ...post, timestamp: serverTimestamp() });

const problemsQuery = query(
collection(firestore, 'groups', props.group.id, 'posts', post.id, 'problems')
);
const problemsSnapshot = await getDocs(problemsQuery);
const problems = problemsSnapshot.docs.map(doc => ({ id: doc.id, ...doc.data() }));

for (const problem of problems) {
const newProblemRef = doc(
collection(firestore, 'groups', groupId, 'posts', newPostRef.id, 'problems')
);
batch.set(newProblemRef, { ...problem });
}
}
await batch.commit();
}
}
props.onClose();
}

return (
<Transition
show={props.showExportModal}
enter="transition ease-out duration-100"
enterFrom="transform opacity-0 scale-95"
enterTo="transform opacity-100 scale-100"
leave="transition ease-in duration-75"
leaveFrom="transform opacity-100 scale-100"
leaveTo="transform opacity-0 scale-95"
className="origin-top-right absolute z-10 right-0 mt-2 w-56 rounded-md shadow-lg bg-white ring-1 ring-black ring-opacity-5 focus:outline-none"
role="menu"
aria-orientation="vertical"
aria-labelledby="options-menu-0"
>
<Dialog
as="div"
className="fixed z-10 inset-0 overflow-y-auto"
onClose={() => props.onClose()}
>
<div className="flex items-end justify-center min-h-screen pt-4 px-4 pb-20 text-center sm:block sm:p-0">
<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"
>
<Dialog.Overlay className="fixed inset-0 bg-gray-500 bg-opacity-75 dark:bg-gray-900 dark:bg-opacity-75 transition-opacity" />
</Transition.Child>

<span
className="hidden sm:inline-block sm:align-middle sm:h-screen"
aria-hidden="true"
>
&#8203;
</span>
<Transition.Child
as={Fragment}
enter="ease-out duration-300"
enterFrom="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
enterTo="opacity-100 translate-y-0 sm:scale-100"
leave="ease-in duration-200"
leaveFrom="opacity-100 translate-y-0 sm:scale-100"
leaveTo="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
>
<div className="inline-block align-bottom bg-white rounded-lg text-left overflow-hidden shadow-xl transform transition-all sm:my-8 sm:align-middle sm:max-w-lg sm:w-full">
<div className="bg-white px-4 pt-5 pb-4 sm:p-6 sm:pb-4">
<div className="sm:flex sm:items-start">
<div className="mt-3 text-center sm:mt-0 sm:ml-4 sm:text-left">
<Dialog.Title
as="h3"
className="text-lg leading-6 font-medium text-gray-900"
>
Export All Posts
</Dialog.Title>
<div className="mt-2 text-gray-500">
Please select the groups you would like to export all posts to.
<div className="block">
{groups.isSuccess &&
(groups.data && groups.data.length > 0 ? (
groups.data.map(group =>
group &&
group.ownerIds.includes(firebaseUser!.uid) ? (
<div key={group.id}>
<label className="inline-flex items-center">
<input
type="checkbox"
className="form-checkbox"
onChange={() => handleGroupExportChange(group)}
/>
<span className="ml-2 text-gray-500">
{group.name}
</span>
</label>
</div>
) : null
)
) : (
<div>
<p>You aren't an admin in any groups yet!</p>
</div>
))}
</div>
</div>
</div>
</div>
</div>
<div className="bg-gray-50 px-4 py-3 sm:px-6 sm:flex sm:flex-row-reverse">
<button
type="button"
className="w-full inline-flex justify-center rounded-md border border-transparent shadow-sm px-4 py-2 bg-green-600 text-base font-medium text-white hover:bg-gray-400 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-red-500 sm:ml-3 sm:w-auto sm:text-sm"
onClick={() => exportAllPosts()}
>
Export
</button>
<button
type="button"
className="mt-3 w-full inline-flex justify-center rounded-md border border-gray-300 shadow-sm px-4 py-2 bg-white text-base font-medium text-gray-700 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 sm:mt-0 sm:ml-3 sm:w-auto sm:text-sm"
onClick={() => props.onClose()}
>
Cancel
</button>
</div>
</div>
</Transition.Child>
</div>
</Dialog>
</Transition>
);
}

class MapData {
constructor(public used: boolean, public group: GroupData) {}
}

0 comments on commit d88761a

Please sign in to comment.