cleaning, prolly broke a lot but whatever
This commit is contained in:
@@ -1,108 +0,0 @@
|
||||
import moment from "moment";
|
||||
import Image from "next/image";
|
||||
import { useState } from "react";
|
||||
import toast from "react-hot-toast";
|
||||
import { FaCheck, FaPlay } from "react-icons/fa";
|
||||
import { useAuth } from "../contexts/authContext";
|
||||
import { useKeyboardContext } from "../contexts/keyboardContext";
|
||||
import { useTeamDashboardContext } from "../contexts/teamDashboardContext";
|
||||
import Announcement, { AnnouncementState } from "../models/announcement";
|
||||
import { User } from "../models/user";
|
||||
import { AnnouncementService } from "../services/announcementService";
|
||||
import SkeletonLoader from "./Loading/skeletonLoader";
|
||||
|
||||
interface IAnnouncementCardProps {
|
||||
announcement: Announcement;
|
||||
}
|
||||
|
||||
const announcementService = new AnnouncementService();
|
||||
|
||||
export default function AnnouncementCard(props: IAnnouncementCardProps) {
|
||||
const { currUser } = useAuth();
|
||||
const { teamUsersMap, user } = useTeamDashboardContext();
|
||||
const { handleAddAudioToQueue } = useKeyboardContext();
|
||||
const [loading, setLoading] = useState<boolean>(false);
|
||||
|
||||
function playAnnouncement() {
|
||||
handleAddAudioToQueue([props.announcement.audioDataUrl], true);
|
||||
}
|
||||
|
||||
async function resolveAnnouncement() {
|
||||
setLoading(true);
|
||||
|
||||
try {
|
||||
await announcementService.updateAnnouncementState(
|
||||
props.announcement.id,
|
||||
AnnouncementState.resolved
|
||||
);
|
||||
} catch (error) {
|
||||
toast.error("unable to resolve announcement");
|
||||
}
|
||||
|
||||
setLoading(false);
|
||||
}
|
||||
|
||||
async function deleteAnnouncement() {
|
||||
try {
|
||||
await announcementService.updateAnnouncementState(
|
||||
props.announcement.id,
|
||||
AnnouncementState.deleted
|
||||
);
|
||||
} catch (error) {
|
||||
toast.error("unable to resolve announcement");
|
||||
}
|
||||
}
|
||||
|
||||
var announcementUser: User = null;
|
||||
if (props.announcement.createdByUserId in teamUsersMap) {
|
||||
announcementUser = teamUsersMap[props.announcement.createdByUserId];
|
||||
} else if (currUser.uid == props.announcement.createdByUserId) {
|
||||
announcementUser = user;
|
||||
}
|
||||
|
||||
const relativeCreatedDate = moment(
|
||||
props.announcement.createdDate.toDate()
|
||||
).fromNow();
|
||||
|
||||
if (loading) {
|
||||
return <SkeletonLoader />;
|
||||
}
|
||||
|
||||
return (
|
||||
<span className="flex flex-row p-3 bg-gray-300 bg-opacity-25 rounded-lg items-center shrink-0">
|
||||
<span className="relative mr-2 grid items-center justify-items-center">
|
||||
<span className="bg-gray-200 bg-opacity-20 rounded-full shadow-md absolute w-full h-full"></span>
|
||||
|
||||
<Image
|
||||
src={"/avatars/svg/Artboards_Diversity_Avatars_by_Netguru-22.svg"}
|
||||
alt="profile"
|
||||
width={50}
|
||||
height={50}
|
||||
/>
|
||||
</span>
|
||||
|
||||
<span className="flex flex-col items-baseline mr-5">
|
||||
<span className="text-md font-bold text-white">
|
||||
{announcementUser?.nickName}
|
||||
</span>
|
||||
<span className="text-xs text-gray-200">{relativeCreatedDate}</span>
|
||||
</span>
|
||||
|
||||
{props.announcement.state == AnnouncementState.active && (
|
||||
<button
|
||||
onClick={resolveAnnouncement}
|
||||
className="bg-gray-300 bg-opacity-25 p-2 ml-2 rounded hover:bg-opacity-40"
|
||||
>
|
||||
<FaCheck className="text-lg text-white" />
|
||||
</button>
|
||||
)}
|
||||
|
||||
<button
|
||||
onClick={playAnnouncement}
|
||||
className="bg-gray-300 bg-opacity-25 p-2 ml-2 rounded hover:bg-opacity-40"
|
||||
>
|
||||
<FaPlay className="text-lg text-white" />
|
||||
</button>
|
||||
</span>
|
||||
);
|
||||
}
|
||||
@@ -1,185 +0,0 @@
|
||||
import { FaAngleDown, FaCheck, FaMicrophoneAlt, FaPlay } from "react-icons/fa";
|
||||
import { UserStatus } from "../../models/user";
|
||||
|
||||
import Image from "next/image";
|
||||
import { useEffect, useState } from "react";
|
||||
import {
|
||||
collection,
|
||||
getFirestore,
|
||||
onSnapshot,
|
||||
query,
|
||||
where,
|
||||
} from "firebase/firestore";
|
||||
import { Collections } from "../../services/collections";
|
||||
import { useTeamDashboardContext } from "../../contexts/teamDashboardContext";
|
||||
import { useAuth } from "../../contexts/authContext";
|
||||
import Announcement, { AnnouncementState } from "../../models/announcement";
|
||||
import { getTime } from "../../helpers/dateTime";
|
||||
import AnnouncementCard from "../AnnouncementCard";
|
||||
import { Dropdown, Menu, Radio, Tooltip } from "antd";
|
||||
import { useKeyboardContext } from "../../contexts/keyboardContext";
|
||||
|
||||
// for query
|
||||
const yesterday = new Date();
|
||||
yesterday.setDate(yesterday.getDate() - 1);
|
||||
|
||||
const db = getFirestore();
|
||||
|
||||
export default function Announcements() {
|
||||
const { currUser } = useAuth();
|
||||
const { team } = useTeamDashboardContext();
|
||||
const { isRecordingAnnouncement } = useKeyboardContext();
|
||||
|
||||
const [annMap, setAnnMap] = useState<Map<string, Announcement>>(
|
||||
new Map<string, Announcement>()
|
||||
);
|
||||
|
||||
// SECTION: LISTENER for Announcements
|
||||
useEffect(() => {
|
||||
/**
|
||||
* QUERY:
|
||||
* all announcements for the team in the past 24 hours for now
|
||||
* order createdDate desc
|
||||
*
|
||||
*/
|
||||
const q = query(
|
||||
collection(db, Collections.announcements),
|
||||
where("teamId", "==", team.id),
|
||||
where("createdDate", ">", yesterday)
|
||||
);
|
||||
|
||||
// return unsubscribe
|
||||
return onSnapshot(q, (snapshot) => {
|
||||
snapshot.docChanges().forEach((change) => {
|
||||
let updatedOrNewAnn = change.doc.data() as Announcement;
|
||||
updatedOrNewAnn.id = change.doc.id;
|
||||
|
||||
if (change.type === "added" || change.type === "modified") {
|
||||
// update rooms map
|
||||
setAnnMap((prevMap) => {
|
||||
return new Map(prevMap.set(updatedOrNewAnn.id, updatedOrNewAnn));
|
||||
});
|
||||
}
|
||||
if (change.type === "removed") {
|
||||
// going to more so be resolved or deleted
|
||||
console.log("Removed annoncement: ", updatedOrNewAnn);
|
||||
}
|
||||
});
|
||||
});
|
||||
}, []);
|
||||
|
||||
const allAnn = Array.from(annMap.values());
|
||||
// sort the announcements
|
||||
allAnn.sort(function (a, b) {
|
||||
return getTime(b.createdDate.toDate()) - getTime(a.createdDate.toDate());
|
||||
});
|
||||
|
||||
const activeAnn = allAnn.filter(
|
||||
(ann) => ann.state == AnnouncementState.active
|
||||
);
|
||||
const resolvedAnn = allAnn.filter(
|
||||
(ann) => ann.state == AnnouncementState.resolved
|
||||
);
|
||||
|
||||
const [selectedTabPane, setSelectedTabPane] = useState<string>(
|
||||
AnnouncementState.active
|
||||
);
|
||||
|
||||
function getTabContent() {
|
||||
switch (selectedTabPane) {
|
||||
case AnnouncementState.active:
|
||||
return (
|
||||
<>
|
||||
{activeAnn.map((ann) => {
|
||||
return <AnnouncementCard key={ann.id} announcement={ann} />;
|
||||
})}
|
||||
</>
|
||||
);
|
||||
case AnnouncementState.resolved:
|
||||
return (
|
||||
<>
|
||||
{resolvedAnn.map((ann) => {
|
||||
return <AnnouncementCard key={ann.id} announcement={ann} />;
|
||||
})}
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const TimePeriodFilterMenu = (
|
||||
<Menu>
|
||||
<Menu.Item key={1} disabled>
|
||||
Past Week (Coming Soon)
|
||||
</Menu.Item>
|
||||
<Menu.Item key={2} disabled>
|
||||
This Month (Coming Soon)
|
||||
</Menu.Item>
|
||||
</Menu>
|
||||
);
|
||||
|
||||
return (
|
||||
<section className="p-5 flex flex-col bg-gray-100 bg-opacity-25 rounded-lg shadow-md">
|
||||
<span className="flex flex-row justify-start items-center pb-5">
|
||||
<span className="flex flex-col mr-auto">
|
||||
<span className="text-white mr-auto">
|
||||
ANNOUNCEMENTS
|
||||
<Tooltip title={"Press and hold A to send an announcement."}>
|
||||
<button
|
||||
className={`right-1 rounded-lg py-1 px-2 ml-1
|
||||
shadow-md text-center text-white text-sm font-bold ${
|
||||
isRecordingAnnouncement
|
||||
? "bg-orange-500 text-white"
|
||||
: ""
|
||||
}`}
|
||||
>
|
||||
A
|
||||
</button>
|
||||
</Tooltip>
|
||||
</span>
|
||||
<span className="text-gray-300 text-xs">
|
||||
updates, pep talks, blockers, reminders
|
||||
</span>
|
||||
</span>
|
||||
|
||||
{/* tab pane */}
|
||||
|
||||
<Radio.Group
|
||||
value={selectedTabPane}
|
||||
onChange={(e) => setSelectedTabPane(e.target.value)}
|
||||
>
|
||||
<Radio.Button value={AnnouncementState.active}>
|
||||
{AnnouncementState.active}{" "}
|
||||
<span className="text-xs text-orange-500">
|
||||
{activeAnn.length > 0 ? activeAnn.length : ""}
|
||||
</span>
|
||||
</Radio.Button>
|
||||
<Radio.Button value={AnnouncementState.resolved}>
|
||||
{AnnouncementState.resolved}{" "}
|
||||
<span className="text-xs text-orange-500">
|
||||
{activeAnn.length > 0 ? activeAnn.length : ""}
|
||||
</span>
|
||||
</Radio.Button>
|
||||
</Radio.Group>
|
||||
|
||||
<Dropdown overlay={TimePeriodFilterMenu}>
|
||||
<span className="ml-2 text-sm text-gray-300 flex flex-row items-center uppercase hover:cursor-pointer">
|
||||
24 HRS <FaAngleDown />
|
||||
</span>
|
||||
</Dropdown>
|
||||
|
||||
<Tooltip title={"Press and hold A to record an announcement"}>
|
||||
<button className="bg-gray-300 bg-opacity-25 p-2 ml-2 rounded hover:bg-opacity-40">
|
||||
<FaMicrophoneAlt className="text-lg text-white" />
|
||||
</button>
|
||||
</Tooltip>
|
||||
{/* <button className="bg-gray-300 bg-opacity-25 p-2 ml-2 rounded hover:bg-opacity-40">
|
||||
<FaPlay className="text-lg text-white" />
|
||||
</button> */}
|
||||
</span>
|
||||
|
||||
<div className="flex flex-row overflow-auto whitespace-nowrap space-x-5 items-center pb-5">
|
||||
{getTabContent()}
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
@@ -1,351 +0,0 @@
|
||||
import { useAuth } from "../../contexts/authContext";
|
||||
import Image from "next/image";
|
||||
import {
|
||||
FaMicrophoneAlt,
|
||||
FaHeadphonesAlt,
|
||||
FaTh,
|
||||
FaBell,
|
||||
FaSearch,
|
||||
FaBuilding,
|
||||
FaCalendarDay,
|
||||
FaClock,
|
||||
FaPeopleCarry,
|
||||
FaAngleDown,
|
||||
FaCheck,
|
||||
FaDatabase,
|
||||
FaMicrophoneAltSlash,
|
||||
FaVolumeMute,
|
||||
FaLayerGroup,
|
||||
FaUser,
|
||||
} from "react-icons/fa";
|
||||
import { useTeamDashboardContext } from "../../contexts/teamDashboardContext";
|
||||
import router from "next/router";
|
||||
import Moment from "react-moment";
|
||||
import { User } from "../../models/user";
|
||||
import { generateGreetings } from "../../helpers/dateTime";
|
||||
import { Dropdown, Menu, Tooltip } from "antd";
|
||||
import { useRouter } from "next/router";
|
||||
import { TeamMemberRole, TeamMemberStatus } from "../../models/teamMember";
|
||||
import { toast } from "react-hot-toast";
|
||||
import SubMenu from "antd/lib/menu/SubMenu";
|
||||
import { useEffect, useState } from "react";
|
||||
import TeamService from "../../services/teamService";
|
||||
import { Team } from "../../models/team";
|
||||
import Loading from "../Loading";
|
||||
import { useKeyboardContext } from "../../contexts/keyboardContext";
|
||||
import UserStatusBubble from "../UserStatusBubble";
|
||||
|
||||
const teamService = new TeamService();
|
||||
|
||||
const today = new Date();
|
||||
|
||||
export default function Header() {
|
||||
const { currUser, logOut } = useAuth();
|
||||
const { team, user, userTeamMember } = useTeamDashboardContext();
|
||||
const router = useRouter();
|
||||
const { teamid } = router.query;
|
||||
const {
|
||||
hasRecPermit,
|
||||
|
||||
audioInputDeviceId,
|
||||
audioOutputDeviceId,
|
||||
selectAudioOutput,
|
||||
selectAudioInput,
|
||||
inputDevices,
|
||||
outputDevices,
|
||||
|
||||
isMuted,
|
||||
isSilenceMode,
|
||||
muteOrUnmute,
|
||||
silenceOrLivenMode,
|
||||
} = useKeyboardContext();
|
||||
|
||||
const [usersTeams, setUserTeams] = useState<Team[]>(null);
|
||||
|
||||
// get team data for the team dropdown
|
||||
useEffect(() => {
|
||||
(async function () {
|
||||
try {
|
||||
// get all the teams that this user is part of
|
||||
const newTeams: Team[] =
|
||||
await teamService.getActiveOrInvitedTeamsbyUser(
|
||||
currUser.uid,
|
||||
currUser.email
|
||||
);
|
||||
|
||||
setUserTeams(newTeams);
|
||||
} catch (error) {
|
||||
console.log("problem getting teams of this user");
|
||||
toast.error("problem on load");
|
||||
}
|
||||
})();
|
||||
}, []);
|
||||
|
||||
async function handleSignOut() {
|
||||
console.log("clicked log out button");
|
||||
|
||||
try {
|
||||
await logOut();
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
toast.error("Unable to log out!");
|
||||
}
|
||||
}
|
||||
|
||||
async function handleAdminRoute() {
|
||||
if (userTeamMember.role == TeamMemberRole.admin) {
|
||||
router.push("/teams/" + teamid + "/admin");
|
||||
return;
|
||||
}
|
||||
|
||||
toast.error("You are not a team admin!");
|
||||
}
|
||||
|
||||
const searchBar = (
|
||||
<div className="pt-2 flex flex-row relative items-center">
|
||||
<button type="submit" className="absolute left-0 top-0 mt-5 ml-5">
|
||||
<FaSearch className="text-white" />
|
||||
</button>
|
||||
|
||||
<input
|
||||
className=" bg-white bg-opacity-40 h-10 px-5 pl-10 pr-16 rounded-lg text-white text-sm focus:outline-none"
|
||||
type="search"
|
||||
name="search"
|
||||
placeholder="Search"
|
||||
/>
|
||||
|
||||
<button
|
||||
className="absolute right-1 rounded-lg py-1 px-2 ml-1
|
||||
shadow-md text-center text-white text-sm font-bold"
|
||||
>
|
||||
CTRL + K
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
|
||||
const TeamsMenu = (
|
||||
<Menu key={2} title="teams">
|
||||
<Menu.Item
|
||||
onClick={() => window.open("/teams", "_self")}
|
||||
key={"team hub"}
|
||||
icon={<FaLayerGroup />}
|
||||
>
|
||||
<button>Team Hub</button>
|
||||
</Menu.Item>
|
||||
<Menu.Item onClick={handleAdminRoute} key={"admin"} icon={<FaDatabase />}>
|
||||
<button>Admin</button>
|
||||
</Menu.Item>
|
||||
<Menu.Divider />
|
||||
{/* all current teams that this person is a part of */}
|
||||
{usersTeams &&
|
||||
usersTeams.map((uteam, i) => {
|
||||
return (
|
||||
<Menu.Item
|
||||
key={i + 5}
|
||||
icon={team.id == uteam.id ? <FaCheck /> : <></>}
|
||||
onClick={() => {
|
||||
console.log("going to " + uteam.id);
|
||||
window.location.href = "/teams/" + uteam.id;
|
||||
}}
|
||||
>
|
||||
<button>{uteam.name}</button>
|
||||
</Menu.Item>
|
||||
);
|
||||
})}
|
||||
</Menu>
|
||||
);
|
||||
const UserMenu = (
|
||||
<Menu title="user menu">
|
||||
<Menu.Item
|
||||
key={2}
|
||||
onClick={() => router.push("/teams/profile")}
|
||||
icon={<FaUser />}
|
||||
>
|
||||
<button>Profile</button>
|
||||
</Menu.Item>
|
||||
|
||||
<Menu.Item
|
||||
onClick={() => window.open("/teams", "_self")}
|
||||
key={"team hub"}
|
||||
icon={<FaLayerGroup />}
|
||||
>
|
||||
<button>Team Hub</button>
|
||||
</Menu.Item>
|
||||
|
||||
<Menu.Divider />
|
||||
|
||||
<Menu.Item danger key={3} onClick={handleSignOut}>
|
||||
<button>Sign Out</button>
|
||||
</Menu.Item>
|
||||
</Menu>
|
||||
);
|
||||
|
||||
const handleSilenceOrLivenMode = (event) => {
|
||||
event.preventDefault();
|
||||
|
||||
silenceOrLivenMode();
|
||||
};
|
||||
|
||||
const handleMuteUnmute = (event) => {
|
||||
event.preventDefault();
|
||||
|
||||
muteOrUnmute();
|
||||
};
|
||||
|
||||
const audioInputDropMenu = (
|
||||
<Menu title="audio input">
|
||||
<Menu.Item danger key={"muting input"} onClick={handleMuteUnmute}>
|
||||
<button>{isMuted ? "Unmute" : "Mute"}</button>
|
||||
</Menu.Item>
|
||||
|
||||
<Menu.Divider />
|
||||
|
||||
{/* device */}
|
||||
{inputDevices.map((device, i) => {
|
||||
return (
|
||||
<Menu.Item
|
||||
key={device.deviceId}
|
||||
icon={device.deviceId == audioInputDeviceId ? <FaCheck /> : <></>}
|
||||
onClick={() => selectAudioInput(device.deviceId)}
|
||||
>
|
||||
<button>{device.label}</button>
|
||||
</Menu.Item>
|
||||
);
|
||||
})}
|
||||
</Menu>
|
||||
);
|
||||
|
||||
const audioOutputDropMenu = (
|
||||
<Menu title="audio ouput">
|
||||
<Menu.Item
|
||||
danger
|
||||
key={"silence output"}
|
||||
onClick={handleSilenceOrLivenMode}
|
||||
>
|
||||
<button>{isSilenceMode ? "Unsilence" : "Silence"}</button>
|
||||
</Menu.Item>
|
||||
|
||||
<Menu.Divider />
|
||||
|
||||
{/* devices */}
|
||||
{outputDevices.map((device, i) => {
|
||||
return (
|
||||
<Menu.Item
|
||||
key={device.deviceId}
|
||||
icon={device.deviceId == audioOutputDeviceId ? <FaCheck /> : <></>}
|
||||
onClick={() => selectAudioOutput(device.deviceId)}
|
||||
>
|
||||
<button>{device.label}</button>
|
||||
</Menu.Item>
|
||||
);
|
||||
})}
|
||||
</Menu>
|
||||
);
|
||||
|
||||
function renderAudioInput() {
|
||||
if (!hasRecPermit) {
|
||||
return (
|
||||
<Tooltip title="enable mic permissions">
|
||||
<span>
|
||||
<FaMicrophoneAltSlash className="text-xl text-orange-400 ease-in-out duration-300 hover:scale-110 hover:cursor-pointer" />
|
||||
</span>
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
|
||||
if (isMuted) {
|
||||
return (
|
||||
<span onClick={handleMuteUnmute}>
|
||||
<FaMicrophoneAltSlash className="text-xl text-orange-400 ease-in-out duration-300 hover:scale-110 hover:cursor-pointer" />
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Dropdown overlay={audioInputDropMenu}>
|
||||
<span onClick={handleMuteUnmute}>
|
||||
<FaMicrophoneAlt className="text-xl text-gray-200 hover:text-white ease-in-out duration-300 hover:scale-110 hover:cursor-pointer" />
|
||||
</span>
|
||||
</Dropdown>
|
||||
);
|
||||
}
|
||||
|
||||
function renderAudioOutput() {
|
||||
if (isSilenceMode) {
|
||||
return (
|
||||
<span onClick={handleSilenceOrLivenMode}>
|
||||
<FaVolumeMute className="text-xl text-orange-400 ease-in-out duration-300 hover:scale-110 hover:cursor-pointer" />
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Dropdown overlay={audioOutputDropMenu}>
|
||||
<span onClick={handleSilenceOrLivenMode}>
|
||||
<FaHeadphonesAlt className="text-xl text-gray-200 hover:text-white ease-in-out duration-300 hover:scale-110 hover:cursor-pointer" />
|
||||
</span>
|
||||
</Dropdown>
|
||||
);
|
||||
}
|
||||
|
||||
const periodOfDay = generateGreetings();
|
||||
|
||||
return (
|
||||
<section className="flex-1 flex flex-row items-center justify-between py-5">
|
||||
{/* welcome message */}
|
||||
<span className="flex flex-col items-baseline">
|
||||
<span className="font-bold text-xl text-white capitalize ">
|
||||
👋Hey {user.firstName}, {periodOfDay}!
|
||||
</span>
|
||||
|
||||
{/* Date and time */}
|
||||
<span className="flex flex-row space-x-2">
|
||||
<span className="text-sky-700 bg-sky-200 p-1 rounded-md text-xs font-bold mt-2 flex flex-row items-center space-x-1">
|
||||
<FaCalendarDay />
|
||||
<Moment date={today} format="ddd, MMM DD" />
|
||||
</span>
|
||||
|
||||
<span className="text-emerald-700 bg-emerald-200 p-1 rounded-md text-xs font-bold mt-2 flex flex-row items-center space-x-1">
|
||||
<FaClock />
|
||||
<Moment format="h:mm a" />
|
||||
</span>
|
||||
</span>
|
||||
</span>
|
||||
|
||||
{/* header control section */}
|
||||
<span className="flex-row items-center space-x-5 hidden md:flex ml-auto">
|
||||
{/* search bar */}
|
||||
|
||||
<FaBell className=" shrink-0 text-lg text-gray-200 hover:text-white ease-in-out duration-300 hover:scale-110 hover:cursor-pointer" />
|
||||
|
||||
{renderAudioOutput()}
|
||||
|
||||
{renderAudioInput()}
|
||||
|
||||
{/* teams menu */}
|
||||
|
||||
<Dropdown overlay={TeamsMenu} trigger={["click"]}>
|
||||
<button
|
||||
className="text-gray-200 flex flex-row items-center"
|
||||
onClick={(e) => e.preventDefault()}
|
||||
>
|
||||
{team.name} <FaAngleDown />
|
||||
</button>
|
||||
</Dropdown>
|
||||
</span>
|
||||
|
||||
{/* avatar */}
|
||||
<Dropdown
|
||||
overlay={UserMenu}
|
||||
trigger={["click"]}
|
||||
className="ml-2 shrink-0"
|
||||
>
|
||||
<span className="relative flex hover:cursor-pointer">
|
||||
<span className="bg-gray-200 bg-opacity-30 rounded-full shadow-md absolute w-full h-full"></span>
|
||||
<UserStatusBubble status={user.userStatus} />
|
||||
<img src={user.avatarUrl} alt="asdf" className="rounded-full w-12" />
|
||||
</span>
|
||||
</Dropdown>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
@@ -1,260 +0,0 @@
|
||||
import { BsThreeDots } from "react-icons/bs";
|
||||
import { FaAngleDown, FaCode, FaFileImage, FaPlus } from "react-icons/fa";
|
||||
|
||||
import Image from "next/image";
|
||||
import { Dropdown, Menu, Radio, Tooltip } from "antd";
|
||||
import {
|
||||
ShowModalType,
|
||||
useKeyboardContext,
|
||||
} from "../../contexts/keyboardContext";
|
||||
import CreateOrUpdateLink from "../Modals/CreateOrUpdateLink";
|
||||
import { useEffect, useState } from "react";
|
||||
import LinkCard from "../LinkCard";
|
||||
import { Collections } from "../../services/collections";
|
||||
import {
|
||||
collection,
|
||||
getFirestore,
|
||||
onSnapshot,
|
||||
orderBy,
|
||||
query,
|
||||
where,
|
||||
} from "firebase/firestore";
|
||||
import { useTeamDashboardContext } from "../../contexts/teamDashboardContext";
|
||||
import Link, { LinkState } from "../../models/link";
|
||||
import { useAuth } from "../../contexts/authContext";
|
||||
|
||||
const lastWeek = new Date();
|
||||
lastWeek.setDate(lastWeek.getDate() - 7);
|
||||
|
||||
const db = getFirestore();
|
||||
|
||||
export default function Links() {
|
||||
const { currUser } = useAuth();
|
||||
const { handleModalType } = useKeyboardContext();
|
||||
|
||||
const { team } = useTeamDashboardContext();
|
||||
|
||||
const [linksMap, setLinksMap] = useState<Map<string, Link>>(
|
||||
new Map<string, Link>()
|
||||
);
|
||||
|
||||
// SECTION: LISTENER for LINKS
|
||||
useEffect(() => {
|
||||
/**
|
||||
* QUERY:
|
||||
* all links for the team in the past 7 days
|
||||
* order createdDate desc
|
||||
*
|
||||
*/
|
||||
const q = query(
|
||||
collection(db, Collections.links),
|
||||
where("teamId", "==", team.id),
|
||||
where("createdDate", ">", lastWeek),
|
||||
orderBy("createdDate", "desc")
|
||||
);
|
||||
|
||||
// return unsubscribe
|
||||
return onSnapshot(q, (snapshot) => {
|
||||
snapshot.docChanges().forEach((change) => {
|
||||
let updatedOrNewLink = change.doc.data() as Link;
|
||||
updatedOrNewLink.id = change.doc.id;
|
||||
|
||||
if (change.type === "added" || change.type === "modified") {
|
||||
setLinksMap((prevMap) => {
|
||||
return new Map(prevMap.set(updatedOrNewLink.id, updatedOrNewLink));
|
||||
});
|
||||
}
|
||||
if (change.type === "removed") {
|
||||
console.log("Removed link: ", updatedOrNewLink);
|
||||
}
|
||||
});
|
||||
});
|
||||
}, []);
|
||||
|
||||
const TimePeriodFilterMenu = (
|
||||
<Menu>
|
||||
<Menu.Item key={1} disabled>
|
||||
Past 24 Hours (Coming Soon)
|
||||
</Menu.Item>
|
||||
<Menu.Item key={2} disabled>
|
||||
This Month (Coming Soon)
|
||||
</Menu.Item>
|
||||
</Menu>
|
||||
);
|
||||
|
||||
const [selectedTabPane, setSelectedTabPane] = useState<string>(
|
||||
LinkTypeFilter.me
|
||||
);
|
||||
|
||||
const allLinks = Array.from(linksMap.values());
|
||||
|
||||
const meLinks = allLinks.filter((link) => {
|
||||
// if archived or deleted, don't show
|
||||
if (link.state != LinkState.active) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// if I am the sender, then also show, but only if it's me sending to a specific person
|
||||
if (link.createdByUserId == currUser.uid && link.recipients?.length > 0) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// if I am a receiver
|
||||
if (link.recipients?.includes(currUser.uid)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
});
|
||||
const teamLinks = allLinks.filter((link) => {
|
||||
// not archived and does not have a recipients list
|
||||
if (link.state == LinkState.active && !link.recipients) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
});
|
||||
const archivedLinks = allLinks.filter(
|
||||
(link) => link.state == LinkState.archived
|
||||
);
|
||||
|
||||
function getFilteredContent() {
|
||||
switch (selectedTabPane) {
|
||||
case LinkTypeFilter.me:
|
||||
return meLinks;
|
||||
case LinkTypeFilter.team:
|
||||
return teamLinks;
|
||||
case LinkTypeFilter.archived:
|
||||
return archivedLinks;
|
||||
default:
|
||||
return allLinks;
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<section className="p-5 flex flex-col bg-gray-100 bg-opacity-25 rounded-lg shadow-md">
|
||||
{/* header row */}
|
||||
<Tooltip title={"archive unneeded links, keep yourself focused on today"}>
|
||||
{/* modal */}
|
||||
<CreateOrUpdateLink />
|
||||
|
||||
<span className="flex flex-row justify-start pb-5 items-center">
|
||||
<span className="flex flex-col mr-20">
|
||||
<span className="text-white mr-auto">
|
||||
DRAWER
|
||||
<button
|
||||
onClick={() => handleModalType(ShowModalType.createLink)}
|
||||
className="right-1 rounded-lg py-1 px-2 ml-1
|
||||
shadow-md text-center text-white text-sm font-bold"
|
||||
>
|
||||
CTRL + V
|
||||
</button>
|
||||
</span>
|
||||
|
||||
<span className="text-gray-300 text-xs">
|
||||
links: jira tickets, drive files/folders, powerpoints...
|
||||
</span>
|
||||
|
||||
{meLinks.length > 6 || teamLinks.length > 6 ? (
|
||||
<span className="text-gray-300 text-xs">
|
||||
Make sure to{" "}
|
||||
<span className="text-orange-500">
|
||||
{"clear out your drawer and your team's"}
|
||||
</span>{" "}
|
||||
so that you are focused on this week.
|
||||
</span>
|
||||
) : (
|
||||
<></>
|
||||
)}
|
||||
</span>
|
||||
|
||||
<div className="ml-auto">
|
||||
<Radio.Group
|
||||
value={selectedTabPane}
|
||||
onChange={(e) => setSelectedTabPane(e.target.value)}
|
||||
>
|
||||
<Radio.Button value={LinkTypeFilter.me}>
|
||||
{LinkTypeFilter.me}{" "}
|
||||
<span className="text-xs text-orange-500">
|
||||
{meLinks.length > 0 ? meLinks.length : ""}
|
||||
</span>
|
||||
</Radio.Button>
|
||||
<Radio.Button value={LinkTypeFilter.team}>
|
||||
{LinkTypeFilter.team}{" "}
|
||||
<span className="text-xs text-orange-500">
|
||||
{teamLinks.length}
|
||||
</span>
|
||||
</Radio.Button>
|
||||
<Tooltip title={"coming soon"}>
|
||||
<Radio.Button disabled value={LinkTypeFilter.favorites}>
|
||||
{LinkTypeFilter.favorites}{" "}
|
||||
{/* <span className="text-xs text-orange-500">
|
||||
{liveRooms.length > 0 ? fa.length : ""}
|
||||
</span> */}
|
||||
</Radio.Button>
|
||||
</Tooltip>
|
||||
<Radio.Button value={LinkTypeFilter.archived}>
|
||||
{LinkTypeFilter.archived}{" "}
|
||||
<span className="text-xs text-orange-500">
|
||||
{archivedLinks.length > 0 ? archivedLinks.length : ""}
|
||||
</span>
|
||||
</Radio.Button>
|
||||
</Radio.Group>
|
||||
</div>
|
||||
|
||||
<Dropdown overlay={TimePeriodFilterMenu}>
|
||||
<span className="ml-2 text-sm text-gray-300 flex flex-row items-center uppercase hover:cursor-pointer">
|
||||
Week <FaAngleDown />
|
||||
</span>
|
||||
</Dropdown>
|
||||
|
||||
<Tooltip
|
||||
title={
|
||||
"screenshots: paste screenshot into the paste.pics website and bring back the link here and just paste it"
|
||||
}
|
||||
>
|
||||
<button
|
||||
onClick={() => window.open("https://paste.pics/", "_blank")}
|
||||
className="bg-gray-300 bg-opacity-25 p-2 rounded hover:bg-opacity-40 ml-2"
|
||||
>
|
||||
<FaFileImage className="text-lg text-white" />
|
||||
</button>
|
||||
</Tooltip>
|
||||
|
||||
<Tooltip
|
||||
title={
|
||||
"code blocks: copy the link and paste it in here when you are done"
|
||||
}
|
||||
>
|
||||
<button
|
||||
onClick={() => window.open("https://www.codepile.net/", "_blank")}
|
||||
className="bg-gray-300 bg-opacity-25 p-2 rounded hover:bg-opacity-40 ml-2"
|
||||
>
|
||||
<FaCode className="text-lg text-white" />
|
||||
</button>
|
||||
</Tooltip>
|
||||
|
||||
<button
|
||||
onClick={() => handleModalType(ShowModalType.createLink)}
|
||||
className="bg-gray-300 bg-opacity-25 p-2 ml-2 rounded hover:bg-opacity-40"
|
||||
>
|
||||
<FaPlus className="text-lg text-white" />
|
||||
</button>
|
||||
</span>
|
||||
</Tooltip>
|
||||
|
||||
{/* table of links */}
|
||||
<div className="flex flex-row space-x-2 overflow-x-auto py-2">
|
||||
{getFilteredContent().map((link) => (
|
||||
<LinkCard key={link.id} link={link} />
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
enum LinkTypeFilter {
|
||||
team = "team",
|
||||
me = "me",
|
||||
favorites = "favorites",
|
||||
archived = "archived",
|
||||
}
|
||||
@@ -1,191 +0,0 @@
|
||||
import {
|
||||
ClientConfig,
|
||||
IAgoraRTC,
|
||||
IAgoraRTCClient,
|
||||
IMicrophoneAudioTrack,
|
||||
} from "agora-rtc-sdk-ng";
|
||||
import { Tooltip } from "antd";
|
||||
import {
|
||||
collection,
|
||||
getFirestore,
|
||||
onSnapshot,
|
||||
query,
|
||||
where,
|
||||
} from "firebase/firestore";
|
||||
import { useEffect, useState } from "react";
|
||||
import toast from "react-hot-toast";
|
||||
import { useAuth } from "../../contexts/authContext";
|
||||
import { useTeamDashboardContext } from "../../contexts/teamDashboardContext";
|
||||
import OfficeRoom from "../../models/officeRoom";
|
||||
import { appId } from "../../services/agoraService";
|
||||
import { Collections } from "../../services/collections";
|
||||
import OfficeRoomService from "../../services/officeRoomService";
|
||||
import OfficeCard from "../OfficeCard";
|
||||
|
||||
const db = getFirestore();
|
||||
|
||||
const officeRoomService = new OfficeRoomService();
|
||||
|
||||
const config: ClientConfig = {
|
||||
mode: "rtc",
|
||||
codec: "vp8",
|
||||
};
|
||||
|
||||
export default function Office() {
|
||||
const { currUser } = useAuth();
|
||||
const { team } = useTeamDashboardContext();
|
||||
const [agoraRtc, setAgoraRtc] = useState<IAgoraRTC>(null);
|
||||
const [agoraRtcClient, setAgoraRtcClient] = useState<IAgoraRTCClient>(null);
|
||||
const [localAudioTrack, setLocalAudioTrack] =
|
||||
useState<IMicrophoneAudioTrack>(null);
|
||||
|
||||
const [officeRoomsMap, setOfficeRoomsMap] = useState<Map<string, OfficeRoom>>(
|
||||
new Map<string, OfficeRoom>()
|
||||
);
|
||||
|
||||
// get all offices for this team : realtime
|
||||
useEffect(() => {
|
||||
/**
|
||||
* QUERY:
|
||||
* all offices for this team
|
||||
*
|
||||
*/
|
||||
const q = query(
|
||||
collection(db, Collections.officeRooms),
|
||||
where("teamId", "==", team.id)
|
||||
);
|
||||
|
||||
// return unsubscribe
|
||||
return onSnapshot(q, (snapshot) => {
|
||||
if (snapshot.empty) {
|
||||
// seed data and create the initial rooms
|
||||
console.log("seeding office rooms for this team");
|
||||
officeRoomService.createInitialOfficeRooms(currUser.uid, team.id);
|
||||
return;
|
||||
}
|
||||
|
||||
snapshot.docChanges().forEach((change) => {
|
||||
let updatedOfficeRoom = change.doc.data() as OfficeRoom;
|
||||
updatedOfficeRoom.id = change.doc.id;
|
||||
|
||||
if (change.type === "added" || change.type === "modified") {
|
||||
// update office rooms map on change of the room
|
||||
setOfficeRoomsMap((prevMap) => {
|
||||
return new Map(
|
||||
prevMap.set(updatedOfficeRoom.id, updatedOfficeRoom)
|
||||
);
|
||||
});
|
||||
}
|
||||
if (change.type === "removed") {
|
||||
console.log("Removed office room: ", updatedOfficeRoom);
|
||||
}
|
||||
});
|
||||
});
|
||||
}, []);
|
||||
|
||||
// set up agora stuff
|
||||
useEffect(() => {
|
||||
(async function () {
|
||||
// dynamic import as the server side import doesn't work
|
||||
const AgoraRTC = (await import("agora-rtc-sdk-ng")).default;
|
||||
const agoraClient = AgoraRTC.createClient(config);
|
||||
|
||||
setAgoraRtcClient(agoraClient);
|
||||
setAgoraRtc(AgoraRTC);
|
||||
})();
|
||||
}, []);
|
||||
|
||||
async function handleJoinChannel(channelName: string, agoraToken: string) {
|
||||
const localTrack: IMicrophoneAudioTrack =
|
||||
await agoraRtc.createMicrophoneAudioTrack();
|
||||
|
||||
setLocalAudioTrack(localTrack);
|
||||
|
||||
let init = async (chanName: string) => {
|
||||
agoraRtcClient.on("user-published", async (user, mediaType) => {
|
||||
await agoraRtcClient.subscribe(user, mediaType);
|
||||
console.log("subscribe success");
|
||||
|
||||
if (mediaType === "audio") {
|
||||
user.audioTrack?.play();
|
||||
}
|
||||
});
|
||||
|
||||
agoraRtcClient.on("user-unpublished", async (user, type) => {
|
||||
console.log("unpublished", user, type);
|
||||
if (type === "audio") {
|
||||
user.audioTrack?.stop();
|
||||
}
|
||||
|
||||
await agoraRtcClient.unsubscribe(user);
|
||||
});
|
||||
|
||||
agoraRtcClient.on("user-left", (user) => {
|
||||
console.log("user left", user);
|
||||
});
|
||||
|
||||
await agoraRtcClient.join(appId, chanName, agoraToken, null);
|
||||
if (localTrack) await agoraRtcClient.publish(localTrack);
|
||||
};
|
||||
|
||||
if (localTrack) {
|
||||
console.log("init ready");
|
||||
init(channelName);
|
||||
} else {
|
||||
toast.error("Not ready for joining call");
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
async function handleLeaveChannel() {
|
||||
// destroy local track
|
||||
localAudioTrack?.close();
|
||||
|
||||
// leave all channels
|
||||
await agoraRtcClient.leave();
|
||||
}
|
||||
|
||||
const allOfficeRooms = Array.from(officeRoomsMap.values());
|
||||
|
||||
allOfficeRooms.sort((a, b) => {
|
||||
if (a.name < b.name) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
if (a.name > b.name) {
|
||||
return 1;
|
||||
}
|
||||
return 0;
|
||||
});
|
||||
|
||||
// if there are none, then show user button to create initial office rooms and then create them
|
||||
|
||||
return (
|
||||
<section className="p-5 flex flex-col bg-gray-100 bg-opacity-25 rounded-lg shadow-md w-96 shrink-0 flex-1 overflow-auto">
|
||||
<Tooltip
|
||||
title={
|
||||
"Tell teammates to chat in the 'kitchen' or other places in the office to resolve matters on the spot."
|
||||
}
|
||||
>
|
||||
<span className="flex flex-row justify-start items-center pb-5">
|
||||
<span className="flex flex-col">
|
||||
<span className="text-white uppercase">Office</span>
|
||||
</span>
|
||||
</span>
|
||||
</Tooltip>
|
||||
|
||||
{/* all office rooms */}
|
||||
<span className="flex flex-col overflow-auto pr-2 space-y-2">
|
||||
{allOfficeRooms.map((officeRoom) => (
|
||||
<OfficeCard
|
||||
key={officeRoom.id}
|
||||
officeRoom={officeRoom}
|
||||
handleJoinChannel={handleJoinChannel}
|
||||
handleLeaveChannel={handleLeaveChannel}
|
||||
/>
|
||||
))}
|
||||
{/* <OfficeCard /> */}
|
||||
</span>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
@@ -1,451 +0,0 @@
|
||||
import React, { useEffect, useState } from "react";
|
||||
|
||||
import { FaAngleDown, FaBell, FaClock, FaPlus, FaLink } from "react-icons/fa";
|
||||
import { IoTimer } from "react-icons/io5";
|
||||
import { BsThreeDots } from "react-icons/bs";
|
||||
import Image from "next/image";
|
||||
import CreateOrUpdateRoom from "../Modals/CreateOrUpdateRoom";
|
||||
import {
|
||||
ShowModalType,
|
||||
useKeyboardContext,
|
||||
} from "../../contexts/keyboardContext";
|
||||
import {
|
||||
collection,
|
||||
getFirestore,
|
||||
onSnapshot,
|
||||
orderBy,
|
||||
query,
|
||||
where,
|
||||
} from "firebase/firestore";
|
||||
import { Collections } from "../../services/collections";
|
||||
import { useTeamDashboardContext } from "../../contexts/teamDashboardContext";
|
||||
import Room, { RoomStatus, RoomType } from "../../models/room";
|
||||
import { useAuth } from "../../contexts/authContext";
|
||||
import { Divider, Dropdown, Menu, Radio, Tooltip } from "antd";
|
||||
import RoomCard from "../RoomCard";
|
||||
import toast from "react-hot-toast";
|
||||
import RoomTypeTag from "../RoomTypeTag";
|
||||
import moment from "moment";
|
||||
import { getTime } from "../../helpers/dateTime";
|
||||
|
||||
enum RoomTypeFilter {
|
||||
team = "team",
|
||||
me = "me",
|
||||
archived = "archived",
|
||||
}
|
||||
|
||||
enum RoomTimeFilter {
|
||||
today = "today",
|
||||
week = "week",
|
||||
month = "month",
|
||||
}
|
||||
|
||||
const db = getFirestore();
|
||||
|
||||
const today = new Date();
|
||||
const lastWeek = new Date();
|
||||
lastWeek.setDate(lastWeek.getDate() - 7);
|
||||
const nextWeek = new Date();
|
||||
nextWeek.setDate(nextWeek.getDate() + 7);
|
||||
|
||||
export default function DashboardRoom() {
|
||||
const { currUser } = useAuth();
|
||||
const { handleModalType, showModalType } = useKeyboardContext();
|
||||
const { team } = useTeamDashboardContext();
|
||||
|
||||
const [roomsMap, setRoomsMap] = useState<Map<string, Room>>(
|
||||
new Map<string, Room>()
|
||||
);
|
||||
|
||||
// get recurring rooms data realtime: ALL of them
|
||||
useEffect(() => {
|
||||
/**
|
||||
* QUERY:
|
||||
* all recurring rooms, as the created date can be a year ago
|
||||
*
|
||||
*/
|
||||
const q = query(
|
||||
collection(db, Collections.rooms),
|
||||
where("teamId", "==", team.id),
|
||||
where("type", "==", RoomType.recurring)
|
||||
);
|
||||
|
||||
// return unsubscribe
|
||||
return onSnapshot(q, (snapshot) => {
|
||||
snapshot.docChanges().forEach(handleDocChange);
|
||||
});
|
||||
}, []);
|
||||
|
||||
// get scheduled rooms data realtime: the ones with the date range
|
||||
useEffect(() => {
|
||||
/**
|
||||
* QUERY:
|
||||
* all scheduled rooms in the past 7 days or the next 7 days
|
||||
*
|
||||
*/
|
||||
const q = query(
|
||||
collection(db, Collections.rooms),
|
||||
where("teamId", "==", team.id),
|
||||
where("type", "==", RoomType.scheduled),
|
||||
where("scheduledDateTime", ">", lastWeek),
|
||||
where("scheduledDateTime", "<", nextWeek),
|
||||
orderBy("scheduledDateTime", "asc")
|
||||
);
|
||||
|
||||
// return unsubscribe
|
||||
return onSnapshot(q, (snapshot) => {
|
||||
snapshot.docChanges().forEach(handleDocChange);
|
||||
});
|
||||
}, []);
|
||||
|
||||
// get now rooms data realtime
|
||||
useEffect(() => {
|
||||
/**
|
||||
* QUERY:
|
||||
* all live/now rooms going on right now
|
||||
*
|
||||
*/
|
||||
const q = query(
|
||||
collection(db, Collections.rooms),
|
||||
where("teamId", "==", team.id),
|
||||
where("type", "==", RoomType.now),
|
||||
where("createdDate", ">", lastWeek),
|
||||
orderBy("createdDate", "asc")
|
||||
);
|
||||
|
||||
// return unsubscribe
|
||||
return onSnapshot(q, (snapshot) => {
|
||||
snapshot.docChanges().forEach(handleDocChange);
|
||||
});
|
||||
}, []);
|
||||
|
||||
function handleDocChange(change) {
|
||||
let updatedRoom = change.doc.data() as Room;
|
||||
updatedRoom.id = change.doc.id;
|
||||
|
||||
if (change.type === "added" || change.type === "modified") {
|
||||
// update rooms map
|
||||
setRoomsMap((prevMap) => {
|
||||
return new Map(prevMap.set(updatedRoom.id, updatedRoom));
|
||||
});
|
||||
}
|
||||
if (change.type === "removed") {
|
||||
console.log("Removed room: ", updatedRoom);
|
||||
}
|
||||
}
|
||||
|
||||
// on CTRL + Q, new tab to google meet
|
||||
// on CTRL + V, show modal to create meeting with the link
|
||||
|
||||
const [selectedTabPane, setSelectedTabPane] = useState<string>(
|
||||
RoomTypeFilter.me
|
||||
);
|
||||
|
||||
// sort this mega array
|
||||
const allRooms = Array.from(roomsMap.values());
|
||||
|
||||
const teamRooms = allRooms.filter(
|
||||
(room) => room.status != RoomStatus.archived
|
||||
);
|
||||
const meRooms = allRooms.filter(
|
||||
(room) =>
|
||||
room.members.includes(currUser.uid) && room.status != RoomStatus.archived
|
||||
);
|
||||
|
||||
const archivedRooms = allRooms.filter(
|
||||
(room) => room.status == RoomStatus.archived
|
||||
);
|
||||
|
||||
// data for different meRooms
|
||||
const recurring = meRooms.filter(
|
||||
(room) => room.type == RoomType.recurring && room.status == RoomStatus.empty
|
||||
);
|
||||
const now = meRooms.filter(
|
||||
(room) =>
|
||||
(room.type == RoomType.now && room.status != RoomStatus.archived) ||
|
||||
room.status == RoomStatus.live
|
||||
);
|
||||
const scheduled = meRooms.filter(
|
||||
(room) => room.type == RoomType.scheduled && room.status == RoomStatus.empty
|
||||
);
|
||||
|
||||
// sorting array of scheduled rooms
|
||||
scheduled.sort(function (a, b) {
|
||||
// Turn your strings into dates, and then subtract them
|
||||
// to get a value that is either negative, positive, or zero.
|
||||
if (!b.scheduledDateTime) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
return (
|
||||
getTime(a.scheduledDateTime.toDate()) -
|
||||
getTime(b.scheduledDateTime.toDate())
|
||||
);
|
||||
});
|
||||
|
||||
var relativeTimeNextMeeting = "";
|
||||
|
||||
// go through scheduled and find the first one coming up
|
||||
if (scheduled && scheduled.length > 0) {
|
||||
const firstOneInFuture = scheduled.find(
|
||||
(room) => moment(room.scheduledDateTime.toDate()).diff(today) > 0
|
||||
);
|
||||
|
||||
if (firstOneInFuture) {
|
||||
const meetingDatetime: Date = firstOneInFuture.scheduledDateTime.toDate();
|
||||
relativeTimeNextMeeting = moment(meetingDatetime).fromNow();
|
||||
}
|
||||
}
|
||||
|
||||
function getRoomContent() {
|
||||
// return data based on the selected filters
|
||||
switch (selectedTabPane) {
|
||||
case RoomTypeFilter.team:
|
||||
// render sections for different types
|
||||
|
||||
const recurringTeam = teamRooms.filter(
|
||||
(room) =>
|
||||
room.type == RoomType.recurring && room.status == RoomStatus.empty
|
||||
);
|
||||
|
||||
const nowTeam = teamRooms.filter(
|
||||
(room) =>
|
||||
(room.type == RoomType.now && room.status != RoomStatus.archived) ||
|
||||
room.status == RoomStatus.live
|
||||
);
|
||||
|
||||
const scheduledTeam = teamRooms.filter(
|
||||
(room) =>
|
||||
room.type == RoomType.scheduled && room.status == RoomStatus.empty
|
||||
);
|
||||
|
||||
// sorting array of scheduled rooms
|
||||
scheduledTeam.sort(function (a, b) {
|
||||
// Turn your strings into dates, and then subtract them
|
||||
// to get a value that is either negative, positive, or zero.
|
||||
if (!b.scheduledDateTime) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
return (
|
||||
getTime(a.scheduledDateTime.toDate()) -
|
||||
getTime(b.scheduledDateTime.toDate())
|
||||
);
|
||||
});
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="flex flex-row flex-wrap">
|
||||
{nowTeam.map((room) => (
|
||||
<RoomCard
|
||||
key={room.id}
|
||||
room={room}
|
||||
updateRoomHandler={handleUpdateRoom}
|
||||
/>
|
||||
))}
|
||||
|
||||
{scheduledTeam.map((room) => (
|
||||
<RoomCard
|
||||
key={room.id}
|
||||
room={room}
|
||||
updateRoomHandler={handleUpdateRoom}
|
||||
/>
|
||||
))}
|
||||
|
||||
{recurringTeam.map((room) => (
|
||||
<RoomCard
|
||||
key={room.id}
|
||||
room={room}
|
||||
updateRoomHandler={handleUpdateRoom}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
|
||||
case RoomTypeFilter.me:
|
||||
return (
|
||||
<>
|
||||
<div className="flex flex-row flex-wrap">
|
||||
{now.map((room) => (
|
||||
<RoomCard
|
||||
key={room.id}
|
||||
room={room}
|
||||
updateRoomHandler={handleUpdateRoom}
|
||||
/>
|
||||
))}
|
||||
|
||||
{scheduled.map((room) => (
|
||||
<RoomCard
|
||||
key={room.id}
|
||||
room={room}
|
||||
updateRoomHandler={handleUpdateRoom}
|
||||
/>
|
||||
))}
|
||||
|
||||
{recurring.map((room) => (
|
||||
<RoomCard
|
||||
key={room.id}
|
||||
room={room}
|
||||
updateRoomHandler={handleUpdateRoom}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
case RoomTypeFilter.archived:
|
||||
return (
|
||||
<span className="flex flex-col space-y-5 px-10">
|
||||
{archivedRooms.map((room) => {
|
||||
// if the room is
|
||||
return (
|
||||
<RoomCard
|
||||
key={room.id}
|
||||
room={room}
|
||||
updateRoomHandler={handleUpdateRoom}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</span>
|
||||
);
|
||||
default:
|
||||
return allRooms.map((room) => {
|
||||
// if the room is
|
||||
return (
|
||||
<RoomCard
|
||||
key={room.id}
|
||||
room={room}
|
||||
updateRoomHandler={handleUpdateRoom}
|
||||
/>
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const TimePeriodFilterMenu = (
|
||||
<Menu>
|
||||
<Menu.Item key={1} disabled>
|
||||
Past 24 Hours (Coming Soon)
|
||||
</Menu.Item>
|
||||
<Menu.Item key={2} disabled>
|
||||
This Month (Coming Soon)
|
||||
</Menu.Item>
|
||||
</Menu>
|
||||
);
|
||||
|
||||
const [selectedUpdateRoom, setSelectedUpdateRoom] = useState<Room>(null);
|
||||
|
||||
async function handleUpdateRoom(roomId: string) {
|
||||
console.log("going to update room");
|
||||
|
||||
// get the room details and pass it into the modal
|
||||
if (!roomsMap.has(roomId)) {
|
||||
toast.error("not a valid room to edit");
|
||||
return;
|
||||
}
|
||||
|
||||
// pass this to the modal for use
|
||||
setSelectedUpdateRoom(roomsMap.get(roomId));
|
||||
|
||||
// call the keyboard context to show the modal
|
||||
handleModalType(ShowModalType.createRoom);
|
||||
}
|
||||
|
||||
async function handleCloseModal() {
|
||||
console.log("closing modal");
|
||||
|
||||
// to make sure that the next time the props changes, the modal has the updated room object
|
||||
setSelectedUpdateRoom(null);
|
||||
|
||||
// close the modal
|
||||
handleModalType(ShowModalType.na);
|
||||
}
|
||||
|
||||
return (
|
||||
<section className="p-5 flex flex-col bg-gray-100 bg-opacity-25 rounded-lg shadow-md flex-1 overflow-auto">
|
||||
{/* modal for creating room */}
|
||||
<CreateOrUpdateRoom
|
||||
show={showModalType == ShowModalType.createRoom}
|
||||
updateRoom={selectedUpdateRoom}
|
||||
handleClose={handleCloseModal}
|
||||
/>
|
||||
|
||||
{/* header */}
|
||||
<Tooltip title={" archive rooms to keep your team focused"}>
|
||||
<span className="flex flex-row justify-end space-x-3 pb-5 items-center">
|
||||
<span className="flex flex-col mr-auto">
|
||||
<span className="text-white ">
|
||||
ROOMS
|
||||
<Tooltip
|
||||
title={"Press q to create an instant room if you have GSuite."}
|
||||
>
|
||||
<button
|
||||
className="right-1 rounded-lg py-1 px-2 ml-1
|
||||
shadow-md text-center text-white text-sm font-bold"
|
||||
>
|
||||
Q
|
||||
</button>
|
||||
</Tooltip>
|
||||
</span>
|
||||
{relativeTimeNextMeeting ? (
|
||||
<span className="text-gray-300 text-xs">
|
||||
{"You have your next scheduled one "}{" "}
|
||||
<span className="text-orange-500">
|
||||
{relativeTimeNextMeeting}
|
||||
{"."}
|
||||
</span>
|
||||
</span>
|
||||
) : (
|
||||
<></>
|
||||
)}
|
||||
</span>
|
||||
|
||||
<Radio.Group
|
||||
value={selectedTabPane}
|
||||
onChange={(e) => setSelectedTabPane(e.target.value)}
|
||||
>
|
||||
<Radio.Button value={RoomTypeFilter.me}>
|
||||
{RoomTypeFilter.me}{" "}
|
||||
<span className="text-xs text-orange-500">
|
||||
{meRooms.length > 0 ? meRooms.length : ""}
|
||||
</span>
|
||||
</Radio.Button>
|
||||
<Radio.Button value={RoomTypeFilter.team}>
|
||||
{RoomTypeFilter.team}{" "}
|
||||
<span className="text-xs text-orange-500">
|
||||
{teamRooms.length}
|
||||
</span>
|
||||
</Radio.Button>
|
||||
<Radio.Button value={RoomTypeFilter.archived}>
|
||||
{RoomTypeFilter.archived}{" "}
|
||||
<span className="text-xs text-orange-500">
|
||||
{archivedRooms.length > 0 ? archivedRooms.length : ""}
|
||||
</span>
|
||||
</Radio.Button>
|
||||
</Radio.Group>
|
||||
|
||||
<Dropdown overlay={TimePeriodFilterMenu}>
|
||||
<span className="text-sm text-gray-300 flex flex-row items-center uppercase hover:cursor-pointer">
|
||||
Week <FaAngleDown />
|
||||
</span>
|
||||
</Dropdown>
|
||||
|
||||
<button
|
||||
onClick={() => {
|
||||
setSelectedUpdateRoom(null);
|
||||
handleModalType(ShowModalType.createRoom);
|
||||
}}
|
||||
className="bg-gray-300 bg-opacity-25 p-2 rounded hover:bg-opacity-40"
|
||||
>
|
||||
<FaPlus className="text-lg text-white" />
|
||||
</button>
|
||||
|
||||
{/* <BsThreeDots className="text-xl text-white" /> */}
|
||||
</span>
|
||||
</Tooltip>
|
||||
|
||||
{/* all rooms */}
|
||||
<span className="overflow-auto">{getRoomContent()}</span>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
@@ -1,245 +0,0 @@
|
||||
import { useRouter } from "next/router";
|
||||
import { FaPlus, FaBackward, FaArrowCircleDown } from "react-icons/fa";
|
||||
import { BsThreeDots } from "react-icons/bs";
|
||||
import { useTeamDashboardContext } from "../../contexts/teamDashboardContext";
|
||||
import { useEffect, useState } from "react";
|
||||
import { toast } from "react-hot-toast";
|
||||
import { TeamMemberRole, TeamMemberStatus } from "../../models/teamMember";
|
||||
import { getFirestore } from "firebase/firestore";
|
||||
import { Tooltip } from "antd";
|
||||
import {
|
||||
ShowModalType,
|
||||
useKeyboardContext,
|
||||
} from "../../contexts/keyboardContext";
|
||||
import UserStatusBubble, { UserPulse } from "../UserStatusBubble";
|
||||
import { useAuth } from "../../contexts/authContext";
|
||||
import PowerPlayer from "../Modals/PowerPlayer";
|
||||
|
||||
const maxNumberOfKeyboardMappings: number = 9;
|
||||
|
||||
export default function TeamVoiceLine() {
|
||||
const { currUser } = useAuth();
|
||||
const {
|
||||
addTeamShortcutBinding,
|
||||
selectTeamMember,
|
||||
selectedTeammate,
|
||||
isRecording,
|
||||
handleModalType,
|
||||
} = useKeyboardContext();
|
||||
const router = useRouter();
|
||||
const { teamid } = router.query;
|
||||
const { userTeamMember, teamUsers, messagesByTeamMate } =
|
||||
useTeamDashboardContext();
|
||||
|
||||
// todo use a global is loading
|
||||
const [loading, setLoading] = useState<Boolean>(false);
|
||||
|
||||
// set up shortcuts for each teammate
|
||||
useEffect(() => {
|
||||
// create shortcuts
|
||||
if (!teamUsers) {
|
||||
console.log("no team members to map to shortcuts");
|
||||
return;
|
||||
}
|
||||
|
||||
teamUsers.forEach((tmUser, i) => {
|
||||
// make sure we don't map a user if they are past the max allowed
|
||||
if (i < maxNumberOfKeyboardMappings) {
|
||||
let shortcut: number = i + 49; // 49 is what number 1 is on the keyboard
|
||||
addTeamShortcutBinding(shortcut, tmUser.id);
|
||||
}
|
||||
});
|
||||
}, [teamUsers]);
|
||||
|
||||
async function handleAdminRoute() {
|
||||
if (userTeamMember.role == TeamMemberRole.admin) {
|
||||
router.push("/teams/" + teamid + "/admin");
|
||||
return;
|
||||
}
|
||||
|
||||
toast.error("You are not a team admin!");
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
// todo move all variable stuff like shortcut mappings here
|
||||
}, [teamUsers]);
|
||||
|
||||
function renderTeamMemberList() {
|
||||
// show loading skeleton if not yet got friend info
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="w-full h-24 flex animate-pulse flex-row items-center justify-center space-x-5">
|
||||
<div className="w-12 bg-gray-300 h-12 rounded-full "></div>
|
||||
<div className="flex flex-col space-y-3">
|
||||
<div className="w-60 bg-gray-300 h-6 rounded-md "></div>
|
||||
<div className="w-40 bg-gray-300 h-6 rounded-md "></div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// if not teammates, stale state message to tell admin to add people
|
||||
if (!teamUsers) {
|
||||
return <span className="text-gray-300">Please add team members.</span>;
|
||||
}
|
||||
|
||||
return teamUsers.map((tmember, i) => {
|
||||
var isMessageIncoming: boolean = false;
|
||||
if (
|
||||
messagesByTeamMate.has(tmember.id) &&
|
||||
messagesByTeamMate.get(tmember.id)[0]?.receiverUserId == currUser.uid
|
||||
) {
|
||||
isMessageIncoming = true;
|
||||
}
|
||||
|
||||
var customSelectClasses: string = "";
|
||||
if (tmember.id == selectedTeammate) {
|
||||
customSelectClasses = "bg-white scale-100 z-20 ";
|
||||
} else if (isMessageIncoming) {
|
||||
customSelectClasses = "bg-orange-500 bg-opacity-20";
|
||||
}
|
||||
|
||||
return (
|
||||
<span
|
||||
onClick={() => selectTeamMember(tmember.id)}
|
||||
key={i}
|
||||
className={`rounded flex flex-row items-center py-2 px-2 justify-items-start
|
||||
ease-in-out duration-300 hover:cursor-pointer ${customSelectClasses} hover:bg-gray-200 hover:bg-opacity-20`}
|
||||
>
|
||||
<span
|
||||
className={`relative flex mr-2 transition-all duration-500 ${
|
||||
tmember.id == selectedTeammate ? "scale-110" : ""
|
||||
}`}
|
||||
>
|
||||
<span className="bg-gray-200 bg-opacity-30 rounded-full shadow-md absolute w-full h-full"></span>
|
||||
|
||||
<UserStatusBubble status={tmember.userStatus} />
|
||||
|
||||
<img
|
||||
src={tmember.avatarUrl}
|
||||
alt="asdf"
|
||||
className="rounded-full w-12"
|
||||
/>
|
||||
</span>
|
||||
|
||||
<span className="flex flex-col">
|
||||
{tmember.id == selectedTeammate ? (
|
||||
<>
|
||||
<span className="flex flex-row items-center space-x-2">
|
||||
<span className="text-md text-black font-bold">
|
||||
{tmember.nickName}
|
||||
</span>
|
||||
</span>
|
||||
|
||||
<span className={"text-xs span-sans text-gray-500"}>
|
||||
{tmember.teamRole}{" "}
|
||||
</span>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<span className="flex flex-row items-center space-x-2">
|
||||
<span className="text-sm text-white font-bold">
|
||||
{tmember.nickName}
|
||||
</span>
|
||||
|
||||
<span className="text-white shadow-xl font-bold px-3 py-1 rounded">
|
||||
{/* keyboard shortcuts start from 1 */}
|
||||
{i + 1}
|
||||
</span>
|
||||
</span>
|
||||
|
||||
<span className={"text-xs span-sans text-gray-300"}>
|
||||
{tmember.teamRole}
|
||||
</span>
|
||||
</>
|
||||
)}
|
||||
</span>
|
||||
|
||||
{tmember.id == selectedTeammate ? (
|
||||
<>
|
||||
<Tooltip title="press and hold R to send audio message">
|
||||
{isRecording ? (
|
||||
<button className="ml-auto shadow-lg w-10 h-10 border-orange-400 bg-orange-400 bg-opacity-80 p-2 rounded hover:bg-opacity-100">
|
||||
<span className="text-sm text-white font-bold">R</span>
|
||||
</button>
|
||||
) : (
|
||||
<button className="ml-auto shadow-lg w-10 h-10 border-orange-400 bg-opacity-80 p-2 rounded hover:bg-opacity-100">
|
||||
<span className="text-sm text-orange-500 font-bold">R</span>
|
||||
</button>
|
||||
)}
|
||||
</Tooltip>
|
||||
|
||||
<Tooltip title="press space to play latest convo chunk">
|
||||
<button className="ml-2 h-10 shadow-lg border-green-400 bg-opacity-80 p-2 rounded hover:bg-opacity-100">
|
||||
<span className="text-sm text-green-500 font-bold">
|
||||
SPACE
|
||||
</span>
|
||||
</button>
|
||||
</Tooltip>
|
||||
|
||||
<BsThreeDots className="text-black ml-2 hover:cursor-pointer" />
|
||||
</>
|
||||
) : (
|
||||
<span className="flex flex-row items-center ml-auto ">
|
||||
{isMessageIncoming ? (
|
||||
<FaArrowCircleDown className="text-orange-500" />
|
||||
) : (
|
||||
""
|
||||
)}
|
||||
|
||||
<UserPulse status={tmember.userStatus} />
|
||||
</span>
|
||||
)}
|
||||
</span>
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
const [showPowerPlayer, setShowPowerPlayer] = useState<boolean>(false);
|
||||
|
||||
function handleCloseModal() {
|
||||
handleModalType(ShowModalType.na);
|
||||
setShowPowerPlayer(false);
|
||||
}
|
||||
|
||||
function handleShowModal() {
|
||||
handleModalType(ShowModalType.powerPlayer); // disables controls
|
||||
|
||||
setShowPowerPlayer(true);
|
||||
}
|
||||
|
||||
return (
|
||||
<section className="p-5 flex flex-col bg-gray-100 bg-opacity-25 rounded-lg shadow-md w-96 shrink-0 max-h-[35rem]">
|
||||
<span className="flex flex-row justify-start items-center pb-5">
|
||||
<span className="flex flex-col">
|
||||
<span className="text-white">TEAM</span>
|
||||
</span>
|
||||
|
||||
<Tooltip title="power playback mode">
|
||||
<button
|
||||
onClick={handleShowModal}
|
||||
className="bg-gray-300 bg-opacity-25 p-2 ml-auto rounded hover:bg-opacity-40"
|
||||
>
|
||||
<FaBackward className="text-lg text-white" />
|
||||
</button>
|
||||
</Tooltip>
|
||||
|
||||
<Tooltip title="add members">
|
||||
<button
|
||||
onClick={handleAdminRoute}
|
||||
className="bg-gray-300 bg-opacity-25 p-2 ml-2 rounded hover:bg-opacity-40"
|
||||
>
|
||||
<FaPlus className="text-lg text-white" />
|
||||
</button>
|
||||
</Tooltip>
|
||||
</span>
|
||||
|
||||
<PowerPlayer show={showPowerPlayer} handleCloseModal={handleCloseModal} />
|
||||
|
||||
{/* list of team members */}
|
||||
<div className="flex flex-col overflow-y-auto space-y-2">
|
||||
{renderTeamMemberList()}
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
+2
-54
@@ -1,7 +1,6 @@
|
||||
import { Select, Tag } from "antd";
|
||||
import Avatar from "antd/lib/avatar/avatar";
|
||||
import { useEffect, useRef } from "react";
|
||||
import { FaPaperPlane, FaRegTimesCircle } from "react-icons/fa";
|
||||
import SimpleUserDetailsRow from "../UserDetails/SimpleUserDetailsRow";
|
||||
|
||||
const options = [
|
||||
{ value: "gold" },
|
||||
@@ -45,58 +44,7 @@ export default function CreateConversation() {
|
||||
{/* list of selected people who are nirvana users */}
|
||||
|
||||
<span className="flex flex-col">
|
||||
<span className="flex flex-row items-center py-1 border-t">
|
||||
<Avatar shape="square" size={"small"}>
|
||||
A
|
||||
</Avatar>
|
||||
|
||||
<span className="flex flex-col ml-2">
|
||||
<span className="text-slate-400 text-xs">
|
||||
{"[email protected]"}
|
||||
</span>
|
||||
<span className="text-orange-500 text-xs">
|
||||
Not a valid Nirvana user.
|
||||
</span>
|
||||
</span>
|
||||
|
||||
<button className="p-2 rounded-full hover:cursor-pointer text-sky-500 ml-auto">
|
||||
<FaPaperPlane className="ml-auto text-lg" />
|
||||
</button>
|
||||
</span>
|
||||
<span className="flex flex-row items-center py-1 border-t">
|
||||
<Avatar
|
||||
src={"https://joeschmoe.io/api/v1/random"}
|
||||
shape="square"
|
||||
size={"small"}
|
||||
/>
|
||||
|
||||
<span className="flex flex-col ml-2">
|
||||
<span className="text-teal-500 font-bold">{"Joe Smoe"}</span>
|
||||
<span className="text-slate-400 text-xs">
|
||||
{"[email protected]"}
|
||||
</span>
|
||||
</span>
|
||||
|
||||
<button className="p-2 rounded-full hover:cursor-pointer text-orange-500 ml-auto">
|
||||
<FaRegTimesCircle className="ml-auto text-lg" />
|
||||
</button>
|
||||
</span>
|
||||
<span className="flex flex-row items-center py-1 border-t">
|
||||
<Avatar
|
||||
src={"https://joeschmoe.io/api/v1/2"}
|
||||
shape="square"
|
||||
size={"small"}
|
||||
/>
|
||||
|
||||
<span className="flex flex-col ml-2">
|
||||
<span className="text-teal-500 font-bold">{"Elon Musk"}</span>
|
||||
<span className="text-slate-400 text-xs">{"[email protected]"}</span>
|
||||
</span>
|
||||
|
||||
<button className="p-2 rounded-full hover:cursor-pointer text-orange-500 ml-auto">
|
||||
<FaRegTimesCircle className="ml-auto text-lg" />
|
||||
</button>
|
||||
</span>
|
||||
<SimpleUserDetailsRow />
|
||||
</span>
|
||||
|
||||
<span className="flex flex-col mt-10">
|
||||
@@ -4,7 +4,7 @@ import { useRouter } from "next/router";
|
||||
import { useState } from "react";
|
||||
import { FaAngleRight, FaGripHorizontal } from "react-icons/fa";
|
||||
import { useAuth } from "../../contexts/authContext";
|
||||
import MainLogo from "../MainLogo";
|
||||
import MainLogo from "../Logo/MainLogo";
|
||||
|
||||
enum LandingPageNavigation {
|
||||
product = "/",
|
||||
|
||||
@@ -1,195 +0,0 @@
|
||||
import {
|
||||
FaAngleDoubleRight,
|
||||
FaArchive,
|
||||
FaExternalLinkAlt,
|
||||
FaFilePdf,
|
||||
FaTrash,
|
||||
} from "react-icons/fa";
|
||||
import Link, { LinkState, LinkType } from "../models/link";
|
||||
import Image from "next/image";
|
||||
import { BsThreeDots } from "react-icons/bs";
|
||||
import LinkIcon from "./LinkIcon";
|
||||
import { useTeamDashboardContext } from "../contexts/teamDashboardContext";
|
||||
import { useAuth } from "../contexts/authContext";
|
||||
import { User } from "../models/user";
|
||||
import { Avatar, Dropdown, Menu, Tooltip } from "antd";
|
||||
import { useState } from "react";
|
||||
import SkeletonLoader from "./Loading/skeletonLoader";
|
||||
import moment from "moment";
|
||||
import { LinkService } from "../services/linkService";
|
||||
|
||||
interface ILinkCardProps {
|
||||
link: Link;
|
||||
}
|
||||
|
||||
const linkService = new LinkService();
|
||||
|
||||
export default function LinkCard(props: ILinkCardProps) {
|
||||
const { currUser } = useAuth();
|
||||
const { teamUsersMap, user } = useTeamDashboardContext();
|
||||
const [loading, setLoading] = useState<boolean>(false);
|
||||
|
||||
var sender: User;
|
||||
|
||||
if (props.link.createdByUserId == currUser.uid) {
|
||||
sender = user;
|
||||
} else {
|
||||
sender = teamUsersMap[props.link.createdByUserId];
|
||||
}
|
||||
|
||||
var receivers: User[] = props.link.recipients?.reduce((results, userId) => {
|
||||
if (userId in teamUsersMap) {
|
||||
results.push(teamUsersMap[userId]);
|
||||
}
|
||||
|
||||
// if I am the receiver still add me to the receivers
|
||||
if (userId == currUser.uid) {
|
||||
results.push(user);
|
||||
}
|
||||
|
||||
return results;
|
||||
}, [] as User[]);
|
||||
|
||||
async function handleDeleteLink() {
|
||||
setLoading(true);
|
||||
if (
|
||||
confirm(
|
||||
"Are you sure you want to delete link? It will disappear for all recipients."
|
||||
)
|
||||
) {
|
||||
console.log("deleting link");
|
||||
|
||||
await linkService.updateLinkState(props.link.id, LinkState.deleted);
|
||||
|
||||
try {
|
||||
} catch (error) {}
|
||||
}
|
||||
|
||||
setLoading(false);
|
||||
}
|
||||
|
||||
async function handleArchivingLink() {
|
||||
setLoading(true);
|
||||
|
||||
if (
|
||||
confirm(
|
||||
"Are you sure you want to archive link? It will be in the archive tab."
|
||||
)
|
||||
) {
|
||||
await linkService.updateLinkState(props.link.id, LinkState.archived);
|
||||
try {
|
||||
} catch (error) {}
|
||||
}
|
||||
|
||||
setLoading(false);
|
||||
}
|
||||
|
||||
if (loading) {
|
||||
return <SkeletonLoader />;
|
||||
}
|
||||
|
||||
const LinkOptionsMenu = (
|
||||
<Menu>
|
||||
<Menu.Item
|
||||
key={2}
|
||||
danger
|
||||
onClick={handleArchivingLink}
|
||||
icon={<FaArchive />}
|
||||
>
|
||||
<button>Archive Link</button>
|
||||
</Menu.Item>
|
||||
<Menu.Item
|
||||
key={3}
|
||||
danger
|
||||
onClick={handleArchivingLink}
|
||||
icon={<FaTrash />}
|
||||
>
|
||||
<button>Delete Link</button>
|
||||
</Menu.Item>
|
||||
</Menu>
|
||||
);
|
||||
|
||||
const relativeDateTime: string = moment(
|
||||
props.link.createdDate.toDate()
|
||||
).fromNow();
|
||||
|
||||
var receiversNames = "team";
|
||||
if (receivers) {
|
||||
receiversNames = receivers.map((receiver) => receiver.firstName).join(", ");
|
||||
}
|
||||
|
||||
return (
|
||||
<span className="flex flex-col rounded-lg w-72 max-h-60 overflow-clip shrink-0">
|
||||
{/* attmnt header */}
|
||||
<Tooltip title={props.link.link}>
|
||||
<span
|
||||
onClick={() => window.open(props.link.link, "_blank")}
|
||||
className="flex flex-row bg-gray-300 bg-opacity-25 py-5 px-3 items-center justify-start hover:cursor-pointer h-full"
|
||||
>
|
||||
<LinkIcon
|
||||
className="text-4xl mr-2 shrink-0"
|
||||
linkType={props.link.type}
|
||||
/>
|
||||
|
||||
<span className="flex flex-col items-baseline mr-10 space-y-1">
|
||||
<span className="text-md font-bold text-white text-ellipsis overflow-hidden">
|
||||
{props.link.name}
|
||||
</span>
|
||||
|
||||
<span
|
||||
className={`text-gray-200 text-xs mb-auto text-ellipsis whitespace-pre-line max-h-10 overflow-hidden`}
|
||||
>
|
||||
{props.link.description}
|
||||
</span>
|
||||
</span>
|
||||
|
||||
{/* attachment actions */}
|
||||
<button className="bg-gray-300 bg-opacity-25 p-2 ml-auto rounded hover:bg-opacity-40">
|
||||
<FaExternalLinkAlt className="text-sm text-white" />
|
||||
</button>
|
||||
</span>
|
||||
</Tooltip>
|
||||
|
||||
{/* attmnt footer */}
|
||||
<Tooltip
|
||||
title={
|
||||
sender.nickName + " -> " + receiversNames + ": " + relativeDateTime
|
||||
}
|
||||
>
|
||||
<span className="flex flex-row items-center bg-gray-400 bg-opacity-30 p-3">
|
||||
<Avatar src={sender.avatarUrl} className="shadow-xl" />
|
||||
|
||||
<FaAngleDoubleRight className="text-orange-500 text-2xl mx-2" />
|
||||
|
||||
<Avatar.Group>
|
||||
{receivers ? (
|
||||
receivers.map((receiverUser, i) => {
|
||||
return (
|
||||
<Avatar
|
||||
key={receiverUser.id}
|
||||
src={receiverUser.avatarUrl}
|
||||
className="shadow-xl"
|
||||
/>
|
||||
);
|
||||
})
|
||||
) : (
|
||||
<span className="text-xs text-white bg-emerald-400 p-1 rounded-md font-bold flex flex-row space-x-2 items-center">
|
||||
<span>team</span>
|
||||
</span>
|
||||
)}
|
||||
</Avatar.Group>
|
||||
|
||||
<span className="ml-auto">
|
||||
<Dropdown
|
||||
className="ml-auto"
|
||||
overlay={LinkOptionsMenu}
|
||||
trigger={["click"]}
|
||||
>
|
||||
<BsThreeDots className="text-white ml-2 hover:cursor-pointer" />
|
||||
</Dropdown>
|
||||
</span>
|
||||
</span>
|
||||
</Tooltip>
|
||||
</span>
|
||||
);
|
||||
}
|
||||
+2
-2
@@ -11,8 +11,8 @@ import {
|
||||
FaMicrophoneAlt,
|
||||
} from "react-icons/fa";
|
||||
import { UserStatus } from "../../models/user";
|
||||
import MainLogo from "../MainLogo";
|
||||
import UserStatusBubble from "../UserStatusBubble";
|
||||
import MainLogo from "../Logo/MainLogo";
|
||||
import UserStatusBubble from "../UserDetails/UserStatusBubble";
|
||||
|
||||
export default function Header() {
|
||||
const router = useRouter();
|
||||
@@ -1,179 +0,0 @@
|
||||
import { Avatar, Tooltip } from "antd";
|
||||
import toast from "react-hot-toast";
|
||||
import { FaPhoneSlash, FaPlus, FaWalking } from "react-icons/fa";
|
||||
import { HiSpeakerphone } from "react-icons/hi";
|
||||
import { useAuth } from "../contexts/authContext";
|
||||
import { useTeamDashboardContext } from "../contexts/teamDashboardContext";
|
||||
import OfficeRoom, { OfficeRoomState } from "../models/officeRoom";
|
||||
import { User } from "../models/user";
|
||||
import OfficeRoomService from "../services/officeRoomService";
|
||||
import { VscDebugDisconnect } from "react-icons/vsc";
|
||||
import AgoraService from "../services/agoraService";
|
||||
|
||||
interface IOfficeCard {
|
||||
officeRoom: OfficeRoom;
|
||||
handleJoinChannel: Function;
|
||||
handleLeaveChannel: Function;
|
||||
}
|
||||
|
||||
const officeRoomService = new OfficeRoomService();
|
||||
const agoraService = new AgoraService();
|
||||
|
||||
export default function OfficeCard(props: IOfficeCard) {
|
||||
const { currUser } = useAuth();
|
||||
const { teamUsersMap, user } = useTeamDashboardContext();
|
||||
|
||||
var allMembersInRoom = props.officeRoom.members?.reduce((results, userId) => {
|
||||
if (userId in teamUsersMap) {
|
||||
results.push(teamUsersMap[userId]);
|
||||
}
|
||||
|
||||
if (userId == currUser.uid) {
|
||||
results.push(user);
|
||||
}
|
||||
|
||||
return results;
|
||||
}, [] as User[]);
|
||||
|
||||
async function handleJoinOfficeRoom() {
|
||||
toast.loading("Connecting");
|
||||
|
||||
// make sure user is not in another room
|
||||
// get agora token from cloud function
|
||||
// join channel for agora
|
||||
|
||||
if (props.officeRoom.members.includes(currUser.uid)) {
|
||||
toast.error("You are already in this office room!");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// agora token from CF
|
||||
const agoraToken = await agoraService.getAgoraToken(props.officeRoom.id);
|
||||
|
||||
// // function to do it all
|
||||
// await officeRoomService.joinOfficeRoom(props.officeRoom, currUser.uid);
|
||||
|
||||
// handle joining agora channel
|
||||
await props.handleJoinChannel(props.officeRoom.id, agoraToken);
|
||||
|
||||
// update firestore to add ourselves in the office room
|
||||
const newMembers = [...props.officeRoom.members, currUser.uid];
|
||||
await officeRoomService.updateMembersInOfficeRoom(
|
||||
props.officeRoom.id,
|
||||
newMembers
|
||||
);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
toast.error("problem joining office room");
|
||||
}
|
||||
|
||||
toast.dismiss();
|
||||
toast.success("joined office room");
|
||||
}
|
||||
|
||||
async function handleLeaveOfficeRoom() {
|
||||
toast.loading("leaving");
|
||||
|
||||
if (!props.officeRoom.members.includes(currUser.uid)) {
|
||||
toast.error("You are not in this office room!");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// leave agora channel
|
||||
await props.handleLeaveChannel();
|
||||
|
||||
// leave from firestore database
|
||||
const newMembersInRoom = props.officeRoom.members.filter(
|
||||
(memberId) => memberId != currUser.uid
|
||||
);
|
||||
|
||||
await officeRoomService.updateMembersInOfficeRoom(
|
||||
props.officeRoom.id,
|
||||
newMembersInRoom
|
||||
);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
toast.error("problem leaving office room");
|
||||
}
|
||||
toast.dismiss();
|
||||
toast.success("left office room");
|
||||
}
|
||||
|
||||
// check if user is in the office room
|
||||
var isUserInRoom: boolean = false;
|
||||
if (props.officeRoom.members?.includes(currUser.uid)) {
|
||||
isUserInRoom = true;
|
||||
}
|
||||
|
||||
return (
|
||||
<span
|
||||
className={`group flex flex-row justify-start items-center transition-all duration-500 hover:bg-gray-200 hover:bg-opacity-25
|
||||
py-3 px-3 min-h-[3rem] rounded-lg ${
|
||||
isUserInRoom ? "bg-orange-500 bg-opacity-25" : ""
|
||||
}`}
|
||||
>
|
||||
{renderOfficePulse(props.officeRoom.state)}
|
||||
|
||||
{/* office location name */}
|
||||
<span className="text-gray-200 text-md font-semibold ml-2">
|
||||
{props.officeRoom.name}
|
||||
</span>
|
||||
|
||||
{/* all members in room */}
|
||||
<span className="ml-auto items-center flex">
|
||||
<Avatar.Group>
|
||||
{allMembersInRoom.map((member) => {
|
||||
return (
|
||||
<Tooltip key={member.id} title={member.nickName}>
|
||||
<Avatar
|
||||
src={member.avatarUrl}
|
||||
style={{ backgroundColor: "teal", verticalAlign: "middle" }}
|
||||
className="shadow-xl hover:z-20 hover:cursor-pointer"
|
||||
>
|
||||
{member.nickName[0]}
|
||||
</Avatar>
|
||||
</Tooltip>
|
||||
);
|
||||
})}
|
||||
</Avatar.Group>
|
||||
</span>
|
||||
|
||||
{isUserInRoom ? (
|
||||
<button
|
||||
onClick={handleLeaveOfficeRoom}
|
||||
className="ml-2 bg-orange-500 bg-opacity-40 p-2 rounded"
|
||||
>
|
||||
<VscDebugDisconnect className="text-orange-500 text-xl" />
|
||||
</button>
|
||||
) : (
|
||||
<Tooltip title={"Join"}>
|
||||
<button
|
||||
onClick={handleJoinOfficeRoom}
|
||||
className="ml-2 bg-gray-300 bg-opacity-25 p-2 rounded hover:bg-opacity-40 group-hover:flex hidden"
|
||||
>
|
||||
<FaWalking className="text-lg text-white" />
|
||||
</button>
|
||||
</Tooltip>
|
||||
)}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
function renderOfficePulse(officeRoomState: OfficeRoomState) {
|
||||
switch (officeRoomState) {
|
||||
case OfficeRoomState.active:
|
||||
return (
|
||||
<span className="h-4 w-4 rounded-full bg-green-500 animate-pulse shadow-lg"></span>
|
||||
);
|
||||
case OfficeRoomState.idle:
|
||||
return (
|
||||
<span className="h-4 w-4 rounded-full bg-gray-400 animate-pulse shadow-lg"></span>
|
||||
);
|
||||
default:
|
||||
return (
|
||||
<span className="h-4 w-4 rounded-full bg-gray-400 animate-pulse shadow-lg"></span>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,473 +0,0 @@
|
||||
import { BsThreeDots } from "react-icons/bs";
|
||||
import {
|
||||
FaArchive,
|
||||
FaBell,
|
||||
FaCalendarDay,
|
||||
FaClock,
|
||||
FaLink,
|
||||
FaPlus,
|
||||
} from "react-icons/fa";
|
||||
import { IoTimer } from "react-icons/io5";
|
||||
import Room, { RoomStatus, RoomType } from "../models/room";
|
||||
import RoomTypeTag from "./RoomTypeTag";
|
||||
import Image from "next/image";
|
||||
import { useAuth } from "../contexts/authContext";
|
||||
import { Avatar, Divider, Dropdown, Menu, Popover, Tooltip } from "antd";
|
||||
import { UserOutlined, AntDesignOutlined } from "@ant-design/icons";
|
||||
import { useTeamDashboardContext } from "../contexts/teamDashboardContext";
|
||||
import { User, UserStatus } from "../models/user";
|
||||
import RoomService from "../services/roomService";
|
||||
import toast from "react-hot-toast";
|
||||
import { useState } from "react";
|
||||
import SkeletonLoader from "./Loading/skeletonLoader";
|
||||
import { useKeyboardContext } from "../contexts/keyboardContext";
|
||||
import UserService from "../services/userService";
|
||||
import Moment from "react-moment";
|
||||
import moment from "moment";
|
||||
|
||||
interface IRoomCardProps {
|
||||
room: Room;
|
||||
updateRoomHandler: Function;
|
||||
}
|
||||
|
||||
const roomService = new RoomService();
|
||||
const userService = new UserService();
|
||||
|
||||
const today = new Date();
|
||||
|
||||
export default function RoomCard(props: IRoomCardProps) {
|
||||
const { currUser } = useAuth();
|
||||
const { teamUsersMap, user } = useTeamDashboardContext();
|
||||
const [loading, setLoading] = useState<boolean>(false);
|
||||
|
||||
var isUserInRoom: boolean;
|
||||
if (props.room.membersInRoom?.includes(currUser.uid)) {
|
||||
isUserInRoom = true;
|
||||
}
|
||||
|
||||
// first array of all members invited
|
||||
var allMembersInvited = props.room.members?.reduce((results, userId) => {
|
||||
if (userId in teamUsersMap) {
|
||||
results.push(teamUsersMap[userId]);
|
||||
}
|
||||
|
||||
if (userId == currUser.uid) {
|
||||
results.push(user);
|
||||
}
|
||||
|
||||
return results;
|
||||
}, [] as User[]);
|
||||
|
||||
// second array of all members in room
|
||||
var allMembersInRoom = props.room.membersInRoom?.reduce((results, userId) => {
|
||||
if (userId in teamUsersMap) {
|
||||
results.push(teamUsersMap[userId]);
|
||||
}
|
||||
|
||||
if (userId == currUser.uid) {
|
||||
results.push(user);
|
||||
}
|
||||
|
||||
return results;
|
||||
}, [] as User[]);
|
||||
|
||||
// third array all members invited and not in the array of users in room
|
||||
const allMembersInvitedButNotInRoom = allMembersInvited.filter(
|
||||
(invitedMember) => {
|
||||
if (allMembersInRoom.some((user) => user.id === invitedMember.id)) {
|
||||
/* vendors contains the element we're looking for */
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
);
|
||||
|
||||
// show members who are in the invite list but not already in the room
|
||||
const membersInvited = () => {
|
||||
if (allMembersInvited?.length == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const listOfNames = allMembersInvited
|
||||
.map((user) => user.nickName)
|
||||
.join(", ");
|
||||
|
||||
return (
|
||||
<Tooltip title={listOfNames}>
|
||||
<span className="inline-flex flex-row-reverse items-center shrink-0 mr-1">
|
||||
<Avatar.Group>
|
||||
{allMembersInvitedButNotInRoom.map((user, i) => {
|
||||
return (
|
||||
<Avatar
|
||||
key={user.id}
|
||||
src={user.avatarUrl}
|
||||
className="shadow-xl"
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</Avatar.Group>
|
||||
</span>
|
||||
</Tooltip>
|
||||
);
|
||||
};
|
||||
|
||||
const membersInRoom = () => {
|
||||
if (allMembersInRoom?.length == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const listOfNames = allMembersInRoom
|
||||
.map((user) => user.nickName)
|
||||
.join(", ");
|
||||
|
||||
return (
|
||||
<Tooltip title={listOfNames}>
|
||||
<span className="inline-flex flex-row-reverse items-center shrink-0 mr-auto">
|
||||
<Avatar.Group>
|
||||
{allMembersInRoom.map((user, i) => {
|
||||
return (
|
||||
<Avatar
|
||||
key={user.id}
|
||||
src={user.avatarUrl}
|
||||
className="shadow-xl"
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</Avatar.Group>
|
||||
</span>
|
||||
</Tooltip>
|
||||
);
|
||||
};
|
||||
|
||||
async function handleLeavingRoom() {
|
||||
setLoading(true);
|
||||
|
||||
try {
|
||||
// filter current membersInRoom and return new array
|
||||
const newMembersInRoom = props.room.membersInRoom.filter(
|
||||
(memberId) => memberId != currUser.uid
|
||||
);
|
||||
|
||||
// update the room with room id to have a new array of userIds
|
||||
await roomService.updateMembersInRoom(props.room.id, newMembersInRoom);
|
||||
|
||||
// change curr user status to free now
|
||||
await userService.updateUserStatus(currUser.uid, UserStatus.online);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
|
||||
toast.error("unable to leave room...something went wrong");
|
||||
}
|
||||
|
||||
setLoading(false);
|
||||
}
|
||||
|
||||
async function handleJoiningRoom() {
|
||||
setLoading(true);
|
||||
|
||||
try {
|
||||
// update members in room
|
||||
const newMembersInRoom = [...props.room.membersInRoom, currUser.uid];
|
||||
|
||||
// update the room with room id to have a new array of userIds
|
||||
await roomService.updateMembersInRoom(props.room.id, newMembersInRoom);
|
||||
|
||||
// change curr user status to free now
|
||||
await userService.updateUserStatus(currUser.uid, UserStatus.busy);
|
||||
|
||||
// then window.open to room's link
|
||||
window.open(props.room.link, "_blank");
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
|
||||
toast.error("unable to leave room...something went wrong");
|
||||
}
|
||||
|
||||
setLoading(false);
|
||||
}
|
||||
|
||||
async function handleArchivingRoom() {
|
||||
if (
|
||||
confirm(
|
||||
"Are you sure you want to archive room? It will be added to the archived tab."
|
||||
)
|
||||
) {
|
||||
console.log("archiving room");
|
||||
|
||||
try {
|
||||
const newRoom: Room = { ...props.room };
|
||||
|
||||
newRoom.status = RoomStatus.archived;
|
||||
newRoom.membersInRoom = [];
|
||||
await roomService.updateRoom(newRoom);
|
||||
} catch (error) {}
|
||||
}
|
||||
}
|
||||
|
||||
if (loading) {
|
||||
return <SkeletonLoader />;
|
||||
}
|
||||
|
||||
async function handleUpdateRoom() {
|
||||
props.updateRoomHandler(props.room.id);
|
||||
}
|
||||
|
||||
const RoomOptionsMenu = (
|
||||
<Menu>
|
||||
<Menu.Item key={1}>
|
||||
<button onClick={handleUpdateRoom}>Edit Room Details</button>
|
||||
</Menu.Item>
|
||||
<Menu.Item key={2} danger onClick={handleArchivingRoom}>
|
||||
Archive Room
|
||||
</Menu.Item>
|
||||
</Menu>
|
||||
);
|
||||
|
||||
const AgendaContent = (
|
||||
<div>
|
||||
<span className="text-sm whitespace-pre-line">
|
||||
{props.room.description}
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
|
||||
// handle specifics for a scheduled room
|
||||
var dateTimeScheduled = null;
|
||||
var isPastRoom = false; // signal user to archive this room if it's past
|
||||
if (props.room.type == RoomType.scheduled) {
|
||||
dateTimeScheduled = props.room.scheduledDateTime.toDate();
|
||||
|
||||
// check if the moment date scheduled is future or past
|
||||
|
||||
if (moment(today).diff(moment(dateTimeScheduled)) > 0) {
|
||||
isPastRoom = true;
|
||||
}
|
||||
}
|
||||
|
||||
function renderTopRightCardInfo() {
|
||||
if (props.room.type == RoomType.scheduled && props.room.scheduledDateTime) {
|
||||
return (
|
||||
<>
|
||||
<span className="text-sky-700 bg-sky-200 p-1 rounded-md text-xs font-bold flex flex-row items-center space-x-1">
|
||||
<FaCalendarDay />
|
||||
<Moment date={dateTimeScheduled} format="ddd Do" />
|
||||
</span>
|
||||
|
||||
<span className="text-emerald-700 bg-emerald-200 p-1 rounded-md text-xs font-bold mt-2 flex flex-row items-center space-x-1">
|
||||
<FaClock />
|
||||
<Moment date={dateTimeScheduled} format="h:mm a" />
|
||||
</span>
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (props.room.status == RoomStatus.archived) {
|
||||
return (
|
||||
<>
|
||||
<span
|
||||
className={`flex flex-row bg-gray-300 bg-opacity-25 px-5 items-center justify-start rounded-lg`}
|
||||
>
|
||||
{membersInvited()}
|
||||
|
||||
{/* header */}
|
||||
<span className="flex flex-row justify-between items-start space-x-1 p-5 h-full">
|
||||
{/* meeting details */}
|
||||
<span className="flex flex-col items-baseline justify-start max-w-xs h-full">
|
||||
<span
|
||||
className={`${
|
||||
isUserInRoom ? " text-gray-500" : "text-white"
|
||||
} font-semibold`}
|
||||
>
|
||||
{props.room.name}
|
||||
</span>
|
||||
<Popover content={AgendaContent} title="Agenda">
|
||||
<span
|
||||
className={`${
|
||||
isUserInRoom ? "text-gray-400" : "text-gray-200"
|
||||
} text-xs mb-auto text-ellipsis whitespace-pre-line max-h-[3rem] overflow-hidden`}
|
||||
>
|
||||
{props.room.description}
|
||||
</span>
|
||||
</Popover>
|
||||
|
||||
{/* all invited members who are not in the room already */}
|
||||
</span>
|
||||
</span>
|
||||
|
||||
<RoomTypeTag
|
||||
roomStatus={props.room.status}
|
||||
roomType={props.room.type}
|
||||
/>
|
||||
|
||||
{/* room status and link(s) */}
|
||||
<span className="ml-auto flex flex-col items-end justify-between h-full w-fit">
|
||||
{renderTopRightCardInfo()}
|
||||
|
||||
<span
|
||||
className={`${
|
||||
isUserInRoom ? "text-gray-400" : "text-gray-200"
|
||||
} text-xs text-right mb-auto`}
|
||||
>
|
||||
{props.room.approximateDateTime}
|
||||
</span>
|
||||
</span>
|
||||
|
||||
{/* room attachments */}
|
||||
<span className="flex flex-row space-x-2">
|
||||
{props.room.attachments && props.room.attachments.length > 0 ? (
|
||||
<button
|
||||
onClick={() => window.open(props.room.attachments[0], "_blank")}
|
||||
className={`${
|
||||
isUserInRoom
|
||||
? " bg-gray-400 bg-opacity-25 text-gray-400"
|
||||
: "bg-gray-300 bg-opacity-25 text-white"
|
||||
} bg-gray-400 bg-opacity-25 p-2 ml-auto rounded hover:bg-opacity-40`}
|
||||
>
|
||||
<FaLink className="text-sm" />
|
||||
</button>
|
||||
) : (
|
||||
<></>
|
||||
)}
|
||||
</span>
|
||||
|
||||
{/* footer */}
|
||||
<span className="flex flex-row justify-end items-center p-3 rounded-lg">
|
||||
<button
|
||||
onClick={handleJoiningRoom}
|
||||
className="text-sm font-semibold py-1 px-4 rounded bg-gray-200 text-green-500"
|
||||
>
|
||||
Join
|
||||
</button>
|
||||
</span>
|
||||
</span>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<span
|
||||
className={`flex flex-col ${
|
||||
isUserInRoom ? " bg-white bg-opacity-80" : "bg-gray-300 bg-opacity-25"
|
||||
} rounded-lg justify-between md:w-96 w-72 m-2 shrink-0 h-[14rem] shadow-md overflow-ellipsis`}
|
||||
>
|
||||
{/* header */}
|
||||
<span className="flex flex-row justify-between items-start space-x-1 p-5 h-full">
|
||||
{/* meeting details */}
|
||||
<span className="flex flex-col items-baseline justify-start max-w-[12rem] h-full">
|
||||
<span
|
||||
className={`${
|
||||
isUserInRoom ? " text-gray-500" : "text-white"
|
||||
} font-semibold`}
|
||||
>
|
||||
{props.room.name}
|
||||
</span>
|
||||
<Popover content={AgendaContent} title="Agenda">
|
||||
<span
|
||||
className={`${
|
||||
isUserInRoom ? "text-gray-400" : "text-gray-200"
|
||||
} text-xs mb-auto text-ellipsis whitespace-pre-line max-h-[3rem] truncate`}
|
||||
>
|
||||
{props.room.description}
|
||||
</span>
|
||||
</Popover>
|
||||
|
||||
{/* badges and tags */}
|
||||
{/* <span className="flex flex-row flex-wrap space-x-2">
|
||||
<span className="text-xs my-3 text-white bg-cyan-400 p-1 rounded-md font-bold flex flex-row space-x-2 items-center">
|
||||
<span>scrum</span>
|
||||
</span>
|
||||
</span> */}
|
||||
|
||||
{/* all invited members who are not in the room already */}
|
||||
{membersInvited()}
|
||||
</span>
|
||||
|
||||
{/* room status and link(s) */}
|
||||
<span className="flex flex-col items-end justify-between h-full w-fit">
|
||||
<RoomTypeTag
|
||||
roomStatus={props.room.status}
|
||||
roomType={props.room.type}
|
||||
/>
|
||||
|
||||
{renderTopRightCardInfo()}
|
||||
|
||||
<span
|
||||
className={`${
|
||||
isUserInRoom ? "text-gray-400" : "text-gray-200"
|
||||
} text-xs text-right mb-auto`}
|
||||
>
|
||||
{props.room.approximateDateTime}
|
||||
</span>
|
||||
|
||||
{/* room attachments */}
|
||||
<span className="flex flex-row space-x-2">
|
||||
{props.room.attachments && props.room.attachments.length > 0 ? (
|
||||
<button
|
||||
onClick={() => window.open(props.room.attachments[0], "_blank")}
|
||||
className={`${
|
||||
isUserInRoom
|
||||
? " bg-gray-400 bg-opacity-25 text-gray-400"
|
||||
: "bg-gray-300 bg-opacity-25 text-white"
|
||||
} bg-gray-400 bg-opacity-25 p-2 ml-auto rounded hover:bg-opacity-40`}
|
||||
>
|
||||
<FaLink className="text-sm" />
|
||||
</button>
|
||||
) : (
|
||||
<button
|
||||
onClick={handleUpdateRoom}
|
||||
className={`${
|
||||
isUserInRoom
|
||||
? " bg-gray-400 bg-opacity-25 text-gray-400"
|
||||
: "bg-gray-300 bg-opacity-25 text-white"
|
||||
} bg-gray-400 bg-opacity-25 p-2 ml-auto rounded hover:bg-opacity-40`}
|
||||
>
|
||||
<FaPlus className="text-sm" />
|
||||
</button>
|
||||
)}
|
||||
</span>
|
||||
</span>
|
||||
</span>
|
||||
|
||||
{/* footer */}
|
||||
<span className="flex flex-row justify-end items-center bg-gray-400 bg-opacity-30 p-3 rounded-lg">
|
||||
{membersInRoom()}
|
||||
|
||||
{/* tell them to archive if it's past */}
|
||||
{isPastRoom ? (
|
||||
<Tooltip title={"please archive this meeting, it is over"}>
|
||||
<button
|
||||
onClick={handleArchivingRoom}
|
||||
className="flex flex-row items-center space-x-2 text-sm mx-2 text-orange-500 font-semibold py-1 px-4 bg-gray-200 rounded"
|
||||
>
|
||||
<FaArchive /> <span>Archive</span>
|
||||
</button>
|
||||
</Tooltip>
|
||||
) : (
|
||||
<></>
|
||||
)}
|
||||
|
||||
{isUserInRoom ? (
|
||||
<button
|
||||
onClick={handleLeavingRoom}
|
||||
className="text-sm text-orange-500 font-semibold py-1 px-4 bg-gray-200 rounded"
|
||||
>
|
||||
👋 Leave
|
||||
</button>
|
||||
) : (
|
||||
<button
|
||||
onClick={handleJoiningRoom}
|
||||
className="text-sm font-semibold py-1 px-4 rounded bg-gray-200 text-green-500"
|
||||
>
|
||||
Join
|
||||
</button>
|
||||
)}
|
||||
|
||||
<Dropdown overlay={RoomOptionsMenu} trigger={["click"]}>
|
||||
<BsThreeDots className="text-white ml-2 hover:cursor-pointer" />
|
||||
</Dropdown>
|
||||
</span>
|
||||
</span>
|
||||
);
|
||||
}
|
||||
@@ -1,82 +0,0 @@
|
||||
import { FaClock } from "react-icons/fa";
|
||||
import { IoTimer } from "react-icons/io5";
|
||||
import { RoomStatus, RoomType } from "../models/room";
|
||||
|
||||
interface RoomTypeTagProps {
|
||||
roomType: RoomType;
|
||||
roomStatus: RoomStatus;
|
||||
}
|
||||
|
||||
export default function RoomTypeTag(props: RoomTypeTagProps) {
|
||||
const universalClassNames =
|
||||
"text-xs font-bold p-1 rounded-md flex flex-row space-x-2 items-center shadow-lg";
|
||||
|
||||
// show live even if it's some other type because people are in it
|
||||
if (props.roomStatus == RoomStatus.live) {
|
||||
return (
|
||||
<span className={"bg-red-500 text-white " + universalClassNames}>
|
||||
<FaClock />
|
||||
<span>live</span>
|
||||
</span>
|
||||
);
|
||||
} else if (props.roomStatus == RoomStatus.archived) {
|
||||
if (props.roomType == RoomType.now) {
|
||||
return (
|
||||
// <span className={"text-gray-700 bg-gray-200 " + universalClassNames}>
|
||||
// <span>archived</span>
|
||||
// </span>
|
||||
|
||||
<span className={"text-gray-700 bg-gray-200 " + universalClassNames}>
|
||||
<FaClock />
|
||||
<span>now room</span>
|
||||
</span>
|
||||
);
|
||||
} else if (props.roomType == RoomType.scheduled) {
|
||||
return (
|
||||
<span className={"text-blue-700 bg-blue-200 " + universalClassNames}>
|
||||
<FaClock />
|
||||
<span>scheduled room</span>
|
||||
</span>
|
||||
);
|
||||
} else if (props.roomType == RoomType.recurring) {
|
||||
return (
|
||||
<span
|
||||
className={"text-yellow-700 bg-yellow-200 " + universalClassNames}
|
||||
>
|
||||
<IoTimer />
|
||||
<span>recurring room</span>
|
||||
</span>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// if it's just empty and not empty, then go ahead and show the right type
|
||||
switch (props.roomType) {
|
||||
case RoomType.now:
|
||||
return (
|
||||
<span className={"bg-red-500 text-white " + universalClassNames}>
|
||||
<FaClock />
|
||||
<span>live</span>
|
||||
</span>
|
||||
);
|
||||
|
||||
case RoomType.scheduled:
|
||||
return (
|
||||
<></>
|
||||
// <span className={"text-blue-700 bg-blue-200 " + universalClassNames}>
|
||||
// <FaClock />
|
||||
// <span>scheduled</span>
|
||||
// </span>
|
||||
);
|
||||
|
||||
case RoomType.recurring:
|
||||
return (
|
||||
<span
|
||||
className={"text-yellow-700 bg-yellow-200 " + universalClassNames}
|
||||
>
|
||||
<IoTimer />
|
||||
<span>recurring</span>
|
||||
</span>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,38 +0,0 @@
|
||||
import Room, { RoomType } from "../models/room";
|
||||
import RoomCard from "./RoomCard";
|
||||
|
||||
interface IRoomsData {
|
||||
rooms: Room[];
|
||||
}
|
||||
|
||||
// different rows for the types
|
||||
export function RoomsRowsByType(props: IRoomsData) {
|
||||
// got the top level content/rooms
|
||||
// now let's drill down and filter
|
||||
const recurringRooms = props.rooms.filter(
|
||||
(room) => room.type == RoomType.recurring
|
||||
);
|
||||
|
||||
const scheduledRooms = props.rooms.filter(
|
||||
(room) => room.type == RoomType.scheduled
|
||||
);
|
||||
|
||||
const nowRooms = props.rooms.filter(
|
||||
(room) => room.type == RoomType.scheduled
|
||||
);
|
||||
|
||||
return;
|
||||
// return props.rooms.map((room) => {
|
||||
// // if the room is
|
||||
// return (
|
||||
// <RoomCard
|
||||
// key={room.id}
|
||||
// room={room}
|
||||
// updateRoomHandler={handleUpdateRoom}
|
||||
// />
|
||||
// );
|
||||
// })
|
||||
}
|
||||
|
||||
// simple table for the archived rooms
|
||||
export function ArchivedRoomsTable(props: IRoomsData) {}
|
||||
@@ -0,0 +1,61 @@
|
||||
import { Avatar } from "antd";
|
||||
import { FaPaperPlane, FaRegTimesCircle } from "react-icons/fa";
|
||||
import User from "@nirvana/common/models/user";
|
||||
|
||||
export default function SelectedUserRow(props: { user: User }) {
|
||||
console.log(props);
|
||||
return (
|
||||
<>
|
||||
<span className="flex flex-row items-center py-1 border-t">
|
||||
<Avatar shape="square" size={"small"}>
|
||||
A
|
||||
</Avatar>
|
||||
|
||||
<span className="flex flex-col ml-2">
|
||||
<span className="text-slate-400 text-xs">
|
||||
{"[email protected]"}
|
||||
</span>
|
||||
<span className="text-orange-500 text-xs">
|
||||
Not a valid Nirvana user.
|
||||
</span>
|
||||
</span>
|
||||
|
||||
<button className="p-2 rounded-full hover:cursor-pointer text-sky-500 ml-auto">
|
||||
<FaPaperPlane className="ml-auto text-lg" />
|
||||
</button>
|
||||
</span>
|
||||
<span className="flex flex-row items-center py-1 border-t">
|
||||
<Avatar
|
||||
src={"https://joeschmoe.io/api/v1/random"}
|
||||
shape="square"
|
||||
size={"small"}
|
||||
/>
|
||||
|
||||
<span className="flex flex-col ml-2">
|
||||
<span className="text-teal-500 font-bold">{"Joe Smoe"}</span>
|
||||
<span className="text-slate-400 text-xs">{"[email protected]"}</span>
|
||||
</span>
|
||||
|
||||
<button className="p-2 rounded-full hover:cursor-pointer text-orange-500 ml-auto">
|
||||
<FaRegTimesCircle className="ml-auto text-lg" />
|
||||
</button>
|
||||
</span>
|
||||
<span className="flex flex-row items-center py-1 border-t">
|
||||
<Avatar
|
||||
src={"https://joeschmoe.io/api/v1/2"}
|
||||
shape="square"
|
||||
size={"small"}
|
||||
/>
|
||||
|
||||
<span className="flex flex-col ml-2">
|
||||
<span className="text-teal-500 font-bold">{"Elon Musk"}</span>
|
||||
<span className="text-slate-400 text-xs">{"[email protected]"}</span>
|
||||
</span>
|
||||
|
||||
<button className="p-2 rounded-full hover:cursor-pointer text-orange-500 ml-auto">
|
||||
<FaRegTimesCircle className="ml-auto text-lg" />
|
||||
</button>
|
||||
</span>
|
||||
</>
|
||||
);
|
||||
}
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
import { IoPulseOutline, IoRemoveOutline } from "react-icons/io5";
|
||||
import { UserStatus } from "../models/user";
|
||||
import { UserStatus } from "../../models/user";
|
||||
|
||||
interface UserStatusPropsInterface {
|
||||
status: UserStatus;
|
||||
Reference in New Issue
Block a user