having one solution for different components of app

This commit is contained in:
Arjun Patel
2022-01-27 20:17:00 -08:00
parent 0d23d1dfd7
commit 0f7e5a831b
256 changed files with 7997 additions and 0 deletions
+108
View File
@@ -0,0 +1,108 @@
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>
);
}
+185
View File
@@ -0,0 +1,185 @@
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>
);
}
+351
View File
@@ -0,0 +1,351 @@
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>
);
}
+260
View File
@@ -0,0 +1,260 @@
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",
}
+191
View File
@@ -0,0 +1,191 @@
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>
);
}
+451
View File
@@ -0,0 +1,451 @@
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>
);
}
+245
View File
@@ -0,0 +1,245 @@
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>
);
}
@@ -0,0 +1,7 @@
export default function BackgroundLayout({ children }) {
return (
<div id="bg" className="text-white min-w-full min-h-screen">
{children}
</div>
);
}
@@ -0,0 +1,259 @@
import { Dropdown, Menu } from "antd";
import Link from "next/link";
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";
enum LandingPageNavigation {
product = "/",
features = "/features",
pricing = "/pricing",
philosophy = "/philosophy",
}
export default function LangingPageLayout({ children }) {
const { currUser } = useAuth();
const router = useRouter();
const [loading, setLoading] = useState(false);
const currPage = router.pathname;
console.log(currPage);
function handleLogin() {
setLoading(true);
router.push("/teams");
}
const getStartedButton = (
<button
onClick={() => window.open("/teams/login", "_blank")}
className="rounded font-semibold bg-teal-600 p-2 text-white shadow-lg flex flex-row items-center space-x-2"
>
<span>Get Started</span>
<FaAngleRight />
</button>
);
const mobileMenu = (
<Menu>
<Menu.Item key="0">
<Link href={LandingPageNavigation.product}>Product</Link>
</Menu.Item>
<Menu.Item key="1">
<Link href={LandingPageNavigation.features}>Features</Link>
</Menu.Item>
<Menu.Item key="3">
<Link href={LandingPageNavigation.pricing}>Pricing</Link>
</Menu.Item>
<Menu.Item key="4">
<Link href={LandingPageNavigation.philosophy}>Philosophy</Link>
</Menu.Item>
<Menu.Divider />
{currUser ? (
<Menu.Item key="5">
<button
onClick={() => window.open("/teams", "_self")}
className="rounded font-semibold bg-gray-200 p-2 text-teal-600 shadow-lg flex flex-row items-center space-x-2"
>
Continue
</button>
</Menu.Item>
) : (
<>
<Menu.Item key="6">
<button onClick={handleLogin} className="ml-auto">
Log In
</button>
</Menu.Item>
<Menu.Item key="7">
<button
onClick={() => window.open("/teams/login", "_blank")}
className="text-teal-600"
>
Get Started
</button>
</Menu.Item>
</>
)}
</Menu>
);
return (
<div className="landing-page-bg bg-left-bottom bg-fixed bg-cover bg-no-repeat min-h-screen">
<div className="container mx-auto px-5 lg:px-20 min-h-screen">
{/* header */}
<div className="flex flex-row py-5 px-5 items-center justify-start mx-auto">
<MainLogo className="mr-auto text-3xl" />
<span className="hidden md:flex flex-row space-x-5 items-center">
<span
onClick={() => router.push(LandingPageNavigation.product)}
className={`text-gray-500 hover:text-teal-600 hover:cursor-pointer ${
currPage == LandingPageNavigation.product
? "text-teal-600 underline underline-offset-4"
: ""
}`}
>
Product
</span>
<a
href={LandingPageNavigation.features}
className={`text-gray-500 hover:text-teal-600 hover:cursor-pointer ${
currPage == LandingPageNavigation.features
? "text-teal-600 underline underline-offset-4"
: ""
}`}
>
Features
</a>
<a
href={LandingPageNavigation.pricing}
className={`text-gray-500 hover:text-teal-600 hover:cursor-pointer ${
currPage == LandingPageNavigation.pricing
? "text-teal-600 underline underline-offset-4"
: ""
}`}
>
Pricing
</a>
<a
href={LandingPageNavigation.philosophy}
className={`text-gray-500 border-r-gray-400 border-r-2 pr-5 hover:text-teal-600 hover:cursor-pointer ${
currPage == LandingPageNavigation.philosophy
? "text-teal-600 underline underline-offset-4"
: ""
}`}
>
Philosophy
</a>
{currUser ? (
<>
<span className="text-gray-500">Welcome back!</span>
<button
onClick={() => window.open("/teams", "_self")}
className="rounded font-semibold bg-gray-200 p-2 text-teal-600 shadow-lg flex flex-row items-center space-x-2"
>
Continue
</button>
</>
) : (
<>
<button
onClick={handleLogin}
className="ml-auto text-gray-500 hidden md:block"
>
Log In
</button>
{getStartedButton}
</>
)}
</span>
<Dropdown overlay={mobileMenu} trigger={["click"]}>
<FaGripHorizontal className="md:hidden text-3xl text-teal-600" />
</Dropdown>
</div>
{children}
</div>
{/* footer */}
<div className="bg-black w-full lg:px-[10rem] px-[5rem] py-[5rem] ">
<span className="flex md:flex-row md:space-x-5 space-y-5 flex-col items-start justify-between">
<span className="flex flex-col">
<MainLogo className="text-white text-3xl" />
<span className="text-gray-200 mt-auto">&copy; nirvana</span>
<span className="text-gray-200 flex space-x-2">
<a
className="text-gray-200"
href="https://docs.google.com/document/d/1NRWN-6kDyOcADaUAQ-YWVHnz6i9ccGJ3/edit?usp=sharing&ouid=113470786690353109086&rtpof=true&sd=true"
>
terms and conditions
</a>{" "}
<span>|</span>
<a
className="text-gray-200"
href="https://docs.google.com/document/d/1S3JsGqXgkriAsBOybXpVR0JJ4hiRkaNt/edit?usp=sharing&ouid=113470786690353109086&rtpof=true&sd=true"
>
privacy policy.
</a>
</span>
</span>
<span className="flex flex-col items-start text-gray-200">
<span className="font-bold text-white text-lg">Product</span>
<Link href={"/"}>Home</Link>
<Link href="/features">Features</Link>
<Link href="/pricing">Pricing</Link>
<Link href="/philosophy">Philosophy</Link>
</span>
<span className="flex flex-col items-start text-gray-200">
<span className="font-bold text-white text-lg">Resources</span>
<a
onClick={() =>
window.open(
"https://www.vox.com/recode/2019/5/1/18511575/productivity-slack-google-microsoft-facebook",
"_blank"
)
}
>
The productivity pit: how Slack is ruining work
</a>
<a
onClick={() =>
window.open(
"https://www.nationalgeographic.com/science/article/coronavirus-zoom-fatigue-is-taxing-the-brain-here-is-why-that-happens",
"_blank"
)
}
>
Zoom fatigue is taxing the brain. Here&apos;s why that happens
</a>
<a
onClick={() =>
window.open(
"https://nulab.com/blog/collaboration/work-chat-distractions-do-work-instant-messengers-make-us-more-or-less-productive/",
"_blank"
)
}
>
Work chat distractions: Do work instant messengers <br></br>make
us more or less productive?
</a>
</span>
<span className="flex flex-col items-start text-gray-200">
<span className="font-bold text-white text-lg">Get in touch</span>
<span>Questions or feedback?</span>
<span>{"We'd love to hear from you."}</span>
<a
onClick={() =>
window.open(
"mailto:[email protected]?subject=Interested in Nirvana for Startups",
"_blank"
)
}
>
Email Us
</a>
</span>
</span>
</div>
</div>
);
}
+35
View File
@@ -0,0 +1,35 @@
import Head from "next/head"
export default function SiteLayout({ children }) {
return (
<>
<Head>
<meta charSet="UTF-8" />
<title>nirvana</title>
<link rel="icon" href="/icon.png" />
{/* Google Fonts */}
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link
rel="preconnect"
href="https://fonts.gstatic.com"
crossOrigin="true"
/>
<link
href="https://fonts.googleapis.com/css2?family=Satisfy&display=swap"
rel="stylesheet"
/>
<link
href="https://fonts.googleapis.com/css2?family=Baloo+Bhaijaan+2&display=swap"
rel="stylesheet"
/>
<link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Poppins:300,400,500,600,700" />
</Head>
<main>
{children}
</main>
</>
)
}
+195
View File
@@ -0,0 +1,195 @@
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>
);
}
+61
View File
@@ -0,0 +1,61 @@
import {
FaAtlassian,
FaFileAlt,
FaFileCode,
FaFileImage,
FaFilePdf,
FaGithubSquare,
FaGlobe,
FaGoogleDrive,
} from "react-icons/fa";
import Link, { LinkType } from "../models/link";
export default function LinkIcon(props: {
className?: string;
linkType: LinkType;
}) {
// github
// atlassian
// drive
// onedrive
// dropbox
// image
// pdf
// https://www.codepile.net/ for code snippets
const { className, linkType, ...rest } = props;
switch (linkType) {
case LinkType.default:
return <FaGlobe {...rest} className={`${className} text-slate-300 `} />;
case LinkType.atlassian:
return <FaAtlassian {...rest} className={`${className} text-sky-500 `} />;
case LinkType.codePile:
return <FaFileCode {...rest} className={`${className} text-pink-200 `} />;
case LinkType.github:
return (
<FaGithubSquare {...rest} className={`${className} text-gray-200 `} />
);
case LinkType.googleDrive:
return (
<FaGoogleDrive {...rest} className={`${className} text-emerald-500 `} />
);
case LinkType.image:
return (
<FaFileImage {...rest} className={`${className} text-purple-500 `} />
);
case LinkType.pdf:
<FaFilePdf {...rest} className={`${className} text-orange-500 `} />;
default:
return (
<FaFileAlt {...rest} className={`${className} text-orange-500 `} />
);
}
}
+17
View File
@@ -0,0 +1,17 @@
import { Divider } from "antd";
export default function Loading() {
return (
<div className="container mx-auto flex flex-col max-w-md m-10 bg-white p-10 rounded-lg shadow-md space-y-5 items-center animate-pulse">
{/* header */}
<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>
</div>
);
}
+11
View File
@@ -0,0 +1,11 @@
export default function SkeletonLoader() {
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>
);
}
+6
View File
@@ -0,0 +1,6 @@
// {/* side bar */}
// <section className="flex-none w-80 p-10 border-r-2 flex flex-row bg-fuchsia-50 items-center">
// {/* logo */}
// <Image src="/handcrafts/undraw_handcrafts_leaf.svg" alt="logo" width={30} height={30} />
// <font className="top-0 text-3xl text-teal font-satisfy">nirvana</font>
// </section>
+7
View File
@@ -0,0 +1,7 @@
export default function MainLogo({ ...props }) {
return (
<span {...props} id="main-title">
nirvana
</span>
);
}
@@ -0,0 +1,231 @@
import { Divider, Select, Switch, Tooltip } from "antd";
const { Option } = Select;
import Modal from "antd/lib/modal/Modal";
import { useEffect, useState } from "react";
import toast from "react-hot-toast";
import { useAuth } from "../../contexts/authContext";
import {
ShowModalType,
useKeyboardContext,
} from "../../contexts/keyboardContext";
import { useTeamDashboardContext } from "../../contexts/teamDashboardContext";
import Link, { LinkType } from "../../models/link";
import { SendService } from "../../services/sendService";
import LinkIcon from "../LinkIcon";
const sendService = new SendService();
export default function CreateOrUpdateLink() {
const { currUser } = useAuth();
const { pastedLink, handleModalType, showModalType } = useKeyboardContext();
const { teamUsers, team } = useTeamDashboardContext();
useEffect(() => {
if (pastedLink) {
setLink(pastedLink ?? "");
}
}, [pastedLink]);
function closeModal() {
handleModalType(ShowModalType.na);
}
function handleSelectRecipients(value) {
// passed in array of selections...userIds
setRecipientsSelected(value);
console.log(value);
}
function handleSelectRecipientMode(isChecked) {
// clear the list of recipients if team selected
if (isChecked) {
setRecipientsSelected([]);
setIsTeamLink(true);
} else {
setRecipientsSelected([]);
setIsTeamLink(false);
}
}
async function handleSubmitLink() {
if (!link || !name) {
toast.error("Please add a valid link and name");
return;
}
if (
!isTeamLink &&
(!recipientsSelected || recipientsSelected.length == 0)
) {
toast.error(
"Please select members or send it to the team with the toggle"
);
return;
}
// if it's a team link, make sure recipients is null ... but the model should handle this for us?
try {
const newLink = new Link(
name,
description,
link,
team.id,
recipientsSelected,
currUser.uid
);
handleModalType(ShowModalType.na);
// send to database
await sendService.sendLink(newLink);
resetForm();
toast.success("link sent");
} catch (error) {
toast.error("Something went wrong in sending the link.");
console.log(error);
}
}
function resetForm() {
setLink("");
setName("");
setDescription("");
setShowMoreDetails(false);
setIsTeamLink(true);
setRecipientsSelected([]);
}
const [link, setLink] = useState<string>("");
const [name, setName] = useState<string>("");
const [description, setDescription] = useState<string>("");
const [showMoreDetails, setShowMoreDetails] = useState<boolean>(false);
const [isTeamLink, setIsTeamLink] = useState<boolean>(true);
const [recipientsSelected, setRecipientsSelected] = useState<string[]>([]);
return (
<Modal
title="Link Details"
visible={showModalType == ShowModalType.createLink}
onCancel={closeModal}
onOk={handleSubmitLink}
>
<div className="flex flex-col space-y-5">
<span className="flex flex-col items-start">
<span className="text-lg">Link</span>
<span className="text-gray-300 text-xs mb-2">
Please make sure this is valid for others to access.
</span>
{/* icon and link input */}
<span className="flex flex-row items-center space-x-2 w-full">
<LinkIcon className="text-3xl" linkType={Link.getLinkType(link)} />
<input
autoFocus
className="flex-1 rounded-lg bg-gray-50 p-3"
value={link}
placeholder="https://jira.atlassian.com/team/xxx"
onChange={(e) => setLink(e.target.value)}
/>
</span>
</span>
<span className="flex flex-col items-start">
<span className="text-lg">Name</span>
<span className="text-gray-300 text-xs mb-2">
Make sure this makes sense to who you are sending it to.
</span>
<input
className="w-full rounded-lg bg-gray-50 p-3"
value={name}
placeholder="ex. User Story - Customer Creating Orders"
onChange={(e) => setName(e.target.value)}
/>
</span>
<span className="flex flex-col items-start">
<span className="text-lg">Recipients</span>
<span className="text-gray-300 text-xs mb-2">
Choose team or individual recipients
</span>
<span className="mb-2">
<Switch
defaultChecked
onChange={handleSelectRecipientMode}
checkedChildren={<span>Team</span>}
unCheckedChildren={<span>Members</span>}
/>
</span>
{/* select team members to send to */}
{!isTeamLink ? (
<Select
mode="multiple"
allowClear
style={{ width: "100%" }}
placeholder="Please select team members"
onChange={handleSelectRecipients}
value={recipientsSelected}
optionLabelProp="label"
filterOption={(input, option) =>
option.props.label.toLowerCase().indexOf(input.toLowerCase()) >=
0
}
>
{teamUsers.map((tu) => {
return (
<Option key={tu.id} label={tu.firstName}>
{tu.firstName} {tu.lastName}
</Option>
);
})}
</Select>
) : (
<></>
)}
</span>
{showMoreDetails ? (
<>
<Divider />
<button
className="text-sm text-gray-300 text-left underline decoration-gray-300"
onClick={() => setShowMoreDetails(false)}
>
Hide details...
</button>
<span className="flex flex-col items-start">
<span className="text-md">Description</span>
<span className="text-gray-300 text-xs mb-2">
Optional - Add any pointers about this file.
</span>
<input
className="w-full rounded-lg bg-gray-50 p-3"
value={description}
placeholder="ex. Please review this and add feedback asap"
onChange={(e) => setDescription(e.target.value)}
/>
</span>
</>
) : (
<button
className="text-sm text-gray-300 text-left underline decoration-gray-300"
onClick={() => setShowMoreDetails(true)}
>
Click to show more details...
</button>
)}
</div>
</Modal>
);
}
@@ -0,0 +1,532 @@
import {
DatePicker,
Divider,
Modal,
Radio,
Select,
Switch,
TimePicker,
Tooltip,
} from "antd";
import { Timestamp } from "firebase/firestore";
import moment from "moment";
import React, { useEffect, useState } from "react";
import toast from "react-hot-toast";
import { GlobalHotKeys, KeyMap } from "react-hotkeys";
import { useAuth } from "../../contexts/authContext";
import {
ShowModalType,
useKeyboardContext,
} from "../../contexts/keyboardContext";
import { useTeamDashboardContext } from "../../contexts/teamDashboardContext";
import Room, { RoomStatus, RoomType } from "../../models/room";
import { User } from "../../models/user";
import RoomService from "../../services/roomService";
const { Option } = Select;
const roomService = new RoomService();
interface IModalProps {
show: boolean;
updateRoom: Room;
handleClose: Function;
}
export default function CreateOrUpdateRoomModal(props: IModalProps) {
const { currUser } = useAuth();
const { teamUsers, team, user } = useTeamDashboardContext();
const { pastedLink, handleModalType, showModalType } = useKeyboardContext();
// handle when we want to update a room and props changes
// prefill with existing stuff
useEffect(() => {
console.log("change in props");
if (props.updateRoom) {
setRoomLink(props.updateRoom.link);
setRoomName(props.updateRoom.name);
setRoomDescription(props.updateRoom.description);
if (props.updateRoom.attachments && props.updateRoom.attachments[0]) {
setRoomAttachment(props.updateRoom.attachments[0]);
}
setMembersSelected(props.updateRoom.members);
setRoomType(props.updateRoom.type);
setRoomAppxDateTime(props.updateRoom.approximateDateTime);
if (props.updateRoom.scheduledDateTime) {
const convertedToMoment = moment(
props.updateRoom.scheduledDateTime.toDate()
);
setDateTimePicker(convertedToMoment);
}
} else {
// update state when user does ctrl + v
resetForm();
}
}, [props.updateRoom]);
// update state when user does ctrl + v
useEffect(() => {
// todo: check if link is valid google meet link? again?
console.log("updated link!!!!!");
setRoomLink(pastedLink);
}, [pastedLink]);
const handleCloseModal = () => {
resetForm();
props.handleClose();
};
const handleSubmit = async () => {
// make sure link, room, and type are selected
if (!roomName) {
toast.error("Please fill in room link and name.");
return;
}
if (!roomLink && !hasGSuite) {
toast.error("must provide room link if you don't have a GSuite");
return;
}
try {
const newRoom = new Room();
if (roomAttachment) {
newRoom.attachments = [roomAttachment];
}
if (hasGSuite) {
// create google meet link based on the room name without spaces
var slugName =
user.firstName +
"-" +
"room" +
moment(new Date()).format("dddddohmma");
slugName = slugName.replace(/\s/g, "-");
const autoLink =
"https://accounts.google.com/AccountChooser/signinchooser?continue=https://g.co/meet/" +
slugName;
newRoom.link = autoLink;
} else {
newRoom.link = roomLink;
}
if (roomType == RoomType.now) {
// make sure approximate time isn't selected
newRoom.approximateDateTime = null;
newRoom.status = RoomStatus.live;
// automatically add me to the room
newRoom.membersInRoom = [];
// navigate to the new room automatically
// window.open(roomLink, "_blank");
} else if (roomType == RoomType.scheduled) {
// make sure that either appx or time picker is selected, not both
if (!dateTimePicker) {
toast.error("Please select a date and time!");
return;
}
newRoom.approximateDateTime = null;
newRoom.scheduledDateTime = Timestamp.fromDate(dateTimePicker.toDate());
} else if (roomType == RoomType.recurring) {
// clear the field for the time picker if they ever selected that
newRoom.scheduledDateTime = null;
newRoom.approximateDateTime = roomAppxDateTime;
}
newRoom.members = [...membersSelected]; // add currUser to the list of "people"
newRoom.type = roomType;
newRoom.description = roomDescription;
newRoom.name = roomName;
newRoom.createdByUserId = currUser.uid;
newRoom.teamId = team.id;
if (props.updateRoom) {
newRoom.id = props.updateRoom.id;
newRoom.membersInRoom = props.updateRoom.membersInRoom;
toast.success("updated room");
} else {
toast.success("created room");
}
console.log(newRoom);
handleModalType(ShowModalType.na);
await roomService.createOrUpdateRoom(newRoom);
resetForm();
} catch (error) {
toast.error("problem creating/updating room");
console.log(error);
}
};
function resetForm() {
setRoomLink("");
setRoomName(
user.firstName +
"'s Room - " +
moment(new Date()).format("dddd do h:mm a")
);
setRoomDescription("");
setMembersSelected([currUser.uid] as string[]);
setRoomAttachment("");
setRoomAppxDateTime("");
setRoomType(RoomType.now);
setDateTimePicker(null);
setShowMoreDetails(false);
sethasGSuite(true);
}
const [roomLink, setRoomLink] = useState<string>(pastedLink ?? "");
const [roomName, setRoomName] = useState<string>(
user.firstName + "'s Room - " + moment(new Date()).format("dddd do h:mm a")
);
const [roomDescription, setRoomDescription] = useState<string>("");
const [roomAttachment, setRoomAttachment] = useState<string>("");
const [membersSelected, setMembersSelected] = useState<string[]>([
currUser.uid,
]);
const [roomType, setRoomType] = useState<RoomType>(RoomType.now);
const [roomAppxDateTime, setRoomAppxDateTime] = useState<string>(""); // for certain room types
const [dateTimePicker, setDateTimePicker] = useState(null);
const [showMoreDetails, setShowMoreDetails] = useState<boolean>(false);
const [hasGSuite, sethasGSuite] = useState<boolean>(true);
function handleSelectMember(value) {
// passed in array of selections...userIds
setMembersSelected(value);
console.log(value);
}
function handleDateTimePickerChange(time) {
console.log(time);
setRoomAppxDateTime("");
setDateTimePicker(time);
}
const allUsersForSelection: User[] = [...teamUsers, user];
const MemberSelection = () => {
return (
<Select
mode="multiple"
allowClear
style={{ width: "100%" }}
placeholder="Please select team members"
onChange={handleSelectMember}
value={membersSelected}
optionLabelProp="label"
filterOption={(input, option) =>
option.props.label.toLowerCase().indexOf(input.toLowerCase()) >= 0
}
>
{allUsersForSelection.map((tu) => {
return (
<Option key={tu.id} label={tu.firstName}>
{tu.firstName} {tu.lastName}
</Option>
);
})}
</Select>
);
};
async function createRoomNow() {
console.log("creating room");
// reset form for the right values to be in there
resetForm();
// submit form
// handleSubmit().then((res) => console.log(res));
const newRoom = new Room();
// create google meet link based on the room name without spaces
var slugName =
user.firstName + "-" + "room" + moment(new Date()).format("dddddohmma");
slugName = slugName.replace(/\s/g, "-");
const autoLink =
"https://accounts.google.com/AccountChooser/signinchooser?continue=https://g.co/meet/" +
slugName;
newRoom.link = autoLink;
newRoom.status = RoomStatus.empty;
newRoom.membersInRoom = [];
newRoom.approximateDateTime = null;
newRoom.members = [currUser.uid]; // add currUser to the list of "people"
newRoom.type = RoomType.now;
newRoom.name = roomName;
newRoom.createdByUserId = currUser.uid;
newRoom.teamId = team.id;
console.log(newRoom);
handleModalType(ShowModalType.na);
await roomService.createOrUpdateRoom(newRoom);
resetForm();
toast.success("created room");
}
const keyMap: KeyMap = {
CREATE_ROOM_NOW: {
name: "create room now",
sequence: "q",
action: "keyup",
},
};
const handlers = {
CREATE_ROOM_NOW: createRoomNow,
};
return (
<>
<GlobalHotKeys keyMap={keyMap} handlers={handlers} />
<Modal
title="Room Details"
centered
visible={props.show}
onOk={handleSubmit}
onCancel={handleCloseModal}
>
<div className="flex flex-col">
{hasGSuite ? (
<span className="flex flex-col items-start">
<span className="text-lg">Link</span>
<span className="text-gray-300 text-xs flex-1">
We&apos;ll create a link for you. Don&apos;t have a GSuite?{" "}
<button
onClick={() => sethasGSuite(false)}
className="text-blue-500"
>
Click here.
</button>
</span>
</span>
) : (
<span className="flex flex-col items-start">
<span className="text-lg">Link</span>
{roomLink ? (
<span className="text-gray-300 text-xs mb-2">
Please make sure this is valid so that your team can join
properly.
</span>
) : (
<span className="text-xs mb-2">
<a
href="https://meet.google.com/new"
target={"_blank"}
rel={"noreferrer"}
className=""
>
Click here to create one
</a>{" "}
and then come back and paste the link for your team.{" "}
<button
onClick={() => sethasGSuite(true)}
className="text-blue-500"
>
Have a GSuite? Click here.
</button>
</span>
)}
<input
autoFocus
className="w-full rounded-lg bg-gray-50 p-3"
value={roomLink}
placeholder="https://meet.google.com/xxx-xxxx"
onChange={(e) => setRoomLink(e.target.value)}
/>
</span>
)}
<span className="flex flex-col items-start flex-1 mt-4 ">
<span className="text-lg">Room Name</span>
<span className="text-gray-300 text-xs mb-2 flex-1">
Be specific enough, everyone down the hall will see this.
</span>
<input
placeholder="ex. Design - Ecommerce Figma"
className="w-full rounded-lg bg-gray-50 p-3"
value={roomName}
onChange={(e) => setRoomName(e.target.value)}
/>
</span>
{/* room type selection */}
<span className="flex flex-col items-start flex-1 mt-4 ">
<span className="text-lg">Type</span>
<span className="text-gray-300 text-xs mb-2 flex-1"></span>
<Radio.Group
value={roomType}
onChange={(event) => setRoomType(event.target.value)}
>
<Radio.Button value={RoomType.now}>{RoomType.now}</Radio.Button>
<Radio.Button value={RoomType.scheduled}>
{RoomType.scheduled}
</Radio.Button>
<Radio.Button value={RoomType.recurring}>
{RoomType.recurring}
</Radio.Button>
</Radio.Group>
{roomType == RoomType.recurring ? (
<Tooltip
title={`Put things like "ping me when ready" or "sometime this afternoon"`}
>
<span className="flex flex-col items-stretch flex-1 mt-4">
<span className="text-md">Approximate Slot</span>
<span className="text-gray-300 text-xs mb-2 flex-1">
Sometimes you don&#39;t have a specific time or want to
propose a rough period.
</span>
<input
placeholder="ex. 2pm-ish...after lunch...every evening"
className="w-full rounded-lg bg-gray-50 p-3"
value={roomAppxDateTime}
onChange={(e) => setRoomAppxDateTime(e.target.value)}
/>
</span>
</Tooltip>
) : (
<></>
)}
{roomType == RoomType.scheduled ? (
<span className="flex flex-col items-start flex-1 mt-4">
<span className="text-md">Specific Time</span>
{/* <TimePicker
minuteStep={15}
use12Hours
format="h:mm a"
value={timePicker}
defaultValue={moment(new Date())}
onChange={handleTimePickerChange}
/> */}
<DatePicker
minuteStep={15}
use12Hours
format="YYYY-MM-DD h:mm a"
value={dateTimePicker}
onChange={handleDateTimePickerChange}
showTime={{ defaultValue: moment("00:00:00", "HH:mm:ss") }}
/>
</span>
) : (
<></>
)}
</span>
{/* people selection */}
<span className="flex flex-col items-start flex-1 mt-4">
<span className="flex flex-row w-full items-center">
<span className="text-lg flex-1">People</span>
{/* team or personal */}
<span className="flex flex-col items-end">
<Tooltip
title={
"Private mode coming soon, but keep it collaborative for now."
}
>
<Switch
disabled={true}
defaultChecked
checkedChildren={<span>Visible</span>}
unCheckedChildren={<span>Private</span>}
/>
</Tooltip>
<span className="text-gray-300 text-xs mb-2 flex-1">
Team sees this room.
</span>
</span>
</span>
<span className="text-gray-300 text-xs mb-2 flex-1">
Optional - Add any mandatory attendees you want or just tell them
later.
</span>
{MemberSelection()}
</span>
{showMoreDetails ? (
<>
<Divider />
<button
className="text-sm text-gray-300 text-left underline decoration-gray-300"
onClick={() => setShowMoreDetails(false)}
>
Hide details...
</button>
<span className="flex flex-col items-start flex-1 mt-4">
<span className="text-md">Agenda</span>
<span className="text-gray-300 text-xs mb-2 flex-1">
Optional
</span>
<textarea
placeholder="ex. Let's discuss ways to..."
className="w-full rounded-lg bg-gray-50 p-3"
value={roomDescription}
onChange={(e) => setRoomDescription(e.target.value)}
/>
</span>
<span className="flex flex-col items-start flex-1 mt-4">
<span className="text-md">Attachment</span>
<span className="text-gray-300 text-xs mb-2">
Optional - ppt, meeting notes...
</span>
<span className="text-gray-300 text-xs mb-2">
have multiple? just post it in the team attachments
</span>
<input
placeholder="https://"
className="w-full rounded-lg bg-gray-50 p-3"
value={roomAttachment}
onChange={(e) => setRoomAttachment(e.target.value)}
/>
</span>
</>
) : (
<button
className="text-sm text-gray-300 text-left underline decoration-gray-300 mt-5"
onClick={() => setShowMoreDetails(true)}
>
Click to show more details...
</button>
)}
</div>
</Modal>
</>
);
}
+134
View File
@@ -0,0 +1,134 @@
import { Avatar, Modal, Tooltip } from "antd";
import { Message } from "../../models/message";
import { FaBackward } from "react-icons/fa";
import { useTeamDashboardContext } from "../../contexts/teamDashboardContext";
import { useAuth } from "../../contexts/authContext";
import { useKeyboardContext } from "../../contexts/keyboardContext";
import moment from "moment";
export default function PowerPlayer(props: {
show: boolean;
handleCloseModal: Function;
}) {
const { currUser } = useAuth();
const { allMessages, teamUsersMap } = useTeamDashboardContext();
const { handleAddAudioToQueue } = useKeyboardContext();
// two arrays:
// 1. all of my messages in order date desc => top
// const sentMessages = allMessages.filter(msg => msg.senderUserId == currUser.uid)
// // 2. all of incoming messages in order date desc => bottom
// const receivedMessages = allMessages.filter(msg => msg.receiverUserId == currUser.uid)
const messageRangeHalfToPlay = 2;
function handlePlayAudio(url: string) {
// todo play like 2 indexes lower to 2 indexes higher instead of
// const indexOfMessage = 5;
// var startPlay = indexOfMessage - messageRangeHalfToPlay;
// if (startPlay < 0) {
// startPlay = 0;
// }
// var endPlay = indexOfMessage + messageRangeHalfToPlay;
// if (endPlay > allMessages.length) {
// endPlay = allMessages.length;
// }
// // loop through start to end and add to queue
// var convoChunk: string[] = []
handleAddAudioToQueue([url], true);
}
function handleClose(e) {
props.handleCloseModal();
}
return (
<Modal
title="Power Playback "
visible={props.show}
onCancel={handleClose}
footer={
<button
onClick={handleClose}
className="ml-auto text-sm text-orange-500 font-semibold py-1 px-4 bg-gray-200 rounded"
>
👋 Close
</button>
}
>
<span className="flex flex-col items-start">
<span>One timeline to listen to the past 24 hours.</span>
<span className="text-sm text-gray-400">
The bottom are your messages. The top are incoming messages. Click on
the bubbles to listen to conversations during that period.
</span>
</span>
<div className="flex flex-col py-20 overflow-x-scroll">
{/* top incoming messages */}
<div className="flex flex-row">
{allMessages.map((msg, i) => {
const relativeDateTime: string = moment(
msg.createdDate.toDate()
).fromNow();
if (msg.senderUserId == currUser.uid) {
// if I am the sender
const receiverUser = teamUsersMap[msg.receiverUserId];
if (!receiverUser) {
return;
}
return (
<Tooltip
title={
"you -> " + receiverUser.nickName + ": " + relativeDateTime
}
>
<span
key={i}
onClick={() => handlePlayAudio(msg.audioDataUrl)}
className="border-r-2 border-r-sky-400 translate-y-10 min-w-max hover:cursor-pointer border-t-2 border-t-black"
>
<Avatar.Group className="translate-x-8 pl-2 translate-y-10">
<Avatar src={receiverUser.avatarUrl} />
{/* <Avatar src={currUser.photoURL} /> */}
<div className="rounded-full w-8 h-8 -translate-x-2 bg-teal-500"></div>
</Avatar.Group>
</span>
</Tooltip>
);
}
const sender = teamUsersMap[msg.senderUserId];
// incoming messages
return (
<span
key={i}
onClick={() => handlePlayAudio(msg.audioDataUrl)}
className="border-r-2 border-r-orange-400 min-w-max hover:cursor-pointer border-b-2 border-b-black"
>
<Tooltip title={sender?.firstName + ": " + relativeDateTime}>
<img
src={sender ? sender?.avatarUrl : "K"}
alt="K"
className="rounded-full w-10 h-10 translate-x-2/4 -translate-y-2/4"
/>
</Tooltip>
</span>
);
})}
</div>
{/* middle line */}
{/* <div className="border border-b-2 border-b-black flex-1 w-full"></div> */}
</div>
</Modal>
);
}
+547
View File
@@ -0,0 +1,547 @@
import { Carousel, Divider, Drawer, Modal } from "antd";
import { CarouselRef } from "antd/lib/carousel";
import { useEffect, useRef, useState } from "react";
import { GlobalHotKeys, KeyMap } from "react-hotkeys";
import { BsThreeDots } from "react-icons/bs";
import {
FaArrowLeft,
FaArrowRight,
FaClock,
FaLink,
FaPlus,
FaUserCheck,
} from "react-icons/fa";
import { FcGoogle } from "react-icons/fc";
import { useKeyboardContext } from "../../contexts/keyboardContext";
import Rooms from "../demo/Rooms";
import Image from "next/image";
import Cookies from "js-cookie";
import { CookieType } from "../../helpers/cookies";
export default function ShortcutHelpModal() {
const [visible, setVisible] = useState<boolean>(false);
const carouselRef = useRef<CarouselRef>();
const { isRecording } = useKeyboardContext();
useEffect(() => {
// if have the specific cookie, then don't show the modal, otherwise show the modal
const cookie = Cookies.get(CookieType.TEAM_SHORTCUTS_ONBOARDING);
if (!cookie) {
setVisible(true);
Cookies.set(CookieType.TEAM_SHORTCUTS_ONBOARDING, "true");
}
}, []);
function handleShowModal() {
setVisible(true);
}
function handleDismissModal() {
setVisible(false);
}
const keyMap: KeyMap = {
SHOW_HELP_MODAL: {
name: "show help modal",
sequence: "/",
action: "keyup",
},
};
const handlers = { SHOW_HELP_MODAL: handleShowModal };
return (
<>
{/* fixed help on bottom left */}
<span
onClick={handleShowModal}
className="hover:cursor-pointer z-50 backdrop-blur-md fixed bottom-10 right-10 bg-gray-200 bg-opacity-80 py-2 px-3 rounded text-gray-500 flex flex-row items-center space-x-2"
>
<span>Help</span>
<button
className={`right-1 rounded-lg py-1 px-2 ml-1
shadow-md text-center text-gray-500 text-sm font-bold`}
>
/
</button>
</span>
<GlobalHotKeys keyMap={keyMap} handlers={handlers} />
{/* <Drawer
title="Help"
placement={"right"}
closable={false}
onClose={handleDismissModal}
visible={visible}
zIndex={0}
></Drawer> */}
<Modal
title="Help"
onCancel={handleDismissModal}
visible={visible}
footer={<></>}
zIndex={5}
width={750}
>
<Carousel ref={carouselRef} dotPosition="top" className="pt-5">
<div>
<span className="flex flex-col px-10">
<span className="flex flex-row justify-between items-start mb-10">
<FaArrowLeft
onClick={() => carouselRef.current.prev()}
className="hover:cursor-pointer bg-teal-600 text-white p-1 rounded-full text-xl"
/>
<span className="flex flex-col items-center">
<span className="text-xl">Live Collaboration</span>
<span className="text-md text-gray-300 text-center">
Building the habit of{" "}
<span className="text-teal-600">
{" "}
resolving issues on the spot
</span>{" "}
<br></br>
through seamless communication.
</span>
</span>
<FaArrowRight
onClick={() => carouselRef.current.next()}
className="hover:cursor-pointer bg-teal-600 text-white p-1 rounded-full text-xl"
/>
</span>
<span className="flex flex-row justify-between items-center">
<span className="flex flex-col ml-2 items-start justify-start">
<span className="text-lg text-gray-500 text-left">
Hop into Office Rooms
</span>
<span className="text-md text-gray-300 text-left">
Work with your closest teammates in the{" "}
<span className="text-teal-600"> hallway or corner </span>
throughout the day.
<br></br> Have lunch and take breaks in the{" "}
<span className="text-teal-600">kitchen.</span>
</span>
</span>
<img
src="/screenshots/officerooms.png"
className="h-[15rem] rounded-lg"
/>
</span>
<Divider />
<span className="flex flex-row justify-between items-center">
{/* spontaneous bug room */}
<span
className="shrink-0 h-[15rem] flex flex-col bg-gray-200 bg-opacity-80
rounded-lg justify-between w-80 max-w-screen-sm m-2 overflow-clip shadow-lg"
>
{/* header */}
<span className="flex flex-1 flex-row justify-between items-baseline space-x-1 p-5">
{/* meeting details */}
<span className="flex flex-col items-baseline justify-start max-w-xs pr-10">
<span className="text-gray-500 font-semibold mr-auto">
bug fixing
</span>
<span className="text-xs text-gray-400 overflow-wrap mb-auto h-full">
were just fixing that jsx bug thats a paiiinnnn
</span>
{/* badges and tags */}
<span className="flex flex-row flex-wrap mt-auto">
<span className="text-xs m-1 text-white bg-red-400 p-1 rounded-md font-bold flex flex-row items-center">
<span>blockers</span>
</span>
<span className="text-xs m-1 text-white bg-indigo-400 p-1 rounded-md font-bold flex flex-row items-center">
<span>engineering</span>
</span>
</span>
</span>
{/* room status */}
<span className="flex flex-col items-end space-y-1 justify-between h-full">
<span
className="text-xs bg-red-500
text-white font-bold p-1 rounded-md flex flex-row space-x-2 items-center"
>
<FaClock />
<span>live</span>
</span>
{/* room attachments */}
<span className="flex flex-row space-x-2">
<button className="bg-gray-400 bg-opacity-25 p-2 ml-auto rounded hover:bg-opacity-40">
<FaLink className="text-sm text-gray-400" />
</button>
<button className="bg-gray-400 bg-opacity-25 p-2 ml-auto rounded hover:bg-opacity-40">
<FaLink className="text-sm text-gray-400" />
</button>
</span>
</span>
</span>
{/* footer */}
<span className="flex flex-row items-center bg-gray-400 bg-opacity-30 p-3">
<span className="inline-flex flex-row-reverse items-center shrink-0 mr-1">
<span className="relative flex">
<span className="bg-gray-200 rounded-full shadow-md absolute w-full h-full"></span>
<Image
className=""
src={
"/avatars/svg/Artboards_Diversity_Avatars_by_Netguru-20.svg"
}
alt="profile"
width={30}
height={30}
/>
</span>
<span className="-mr-4 relative flex">
<span className="bg-gray-200 rounded-full shadow-md absolute w-full h-full"></span>
<Image
className=""
src={
"/avatars/svg/Artboards_Diversity_Avatars_by_Netguru-22.svg"
}
alt="profile"
width={30}
height={30}
/>
</span>
</span>
<span className="text-xs text-gray-400">
Arjun and Liam
</span>
<button className="ml-auto text-sm text-orange-500 font-semibold py-1 px-4 bg-gray-200 rounded">
👋 Leave
</button>
<BsThreeDots className="text-white ml-2 hover:cursor-pointer" />{" "}
</span>
</span>
<span className="flex flex-col ml-2 items-end">
<span className="text-lg text-gray-500">
Create a Meeting Room
</span>
<span className={`text-md text-gray-300 text-right`}>
Click on the{" "}
<button className="inline bg-gray-300 bg-opacity-25 p-1 rounded hover:bg-opacity-40">
<FaPlus className="text-md text-white" />
</button>{" "}
to create a formal <FcGoogle className="inline text-lg" />{" "}
Meet.
</span>
<span className={`text-md text-teal-600 text-right`}>
Spontaneous, scheduled, or recurring rooms.
</span>
<span className="flex flex-row items-center space-x-2">
<button className="inline bg-gray-300 bg-opacity-25 p-2 rounded hover:bg-opacity-40">
<FaPlus className="text-md text-white" />
</button>
<span>or</span>
<button
className={`right-1 rounded-lg py-1 px-2 ml-1
shadow-md text-center text-gray-300 text-sm font-bold`}
>
Q
</button>
</span>
</span>
</span>
<Divider />
<span className="flex flex-row justify-between items-center">
<span className="flex flex-col ml-2 items-start">
<span className="text-lg text-gray-500">
{"Cross Collaborate"}
</span>
<span className={`text-md text-gray-300 text-left`}>
Walk into the office with clarity of what <br></br>
<span className="text-teal-600">
team conversations{" "}
</span>{" "}
are going on across the hall.
</span>
</span>
<img
src="/illustrations/undraw_team_collaboration_re_ow29.svg"
className="h-[10rem]"
/>
</span>
</span>
</div>
<div>
<span className="flex flex-col px-10">
<span className="flex flex-row justify-between items-start mb-10">
<FaArrowLeft
onClick={() => carouselRef.current.prev()}
className="hover:cursor-pointer bg-teal-600 text-white p-1 rounded-full text-xl"
/>
<span className="text-xl">Audio Messages</span>
<FaArrowRight
onClick={() => carouselRef.current.next()}
className="hover:cursor-pointer bg-teal-600 text-white p-1 rounded-full text-xl"
/>
</span>
<span className="flex flex-row justify-between items-center">
<span className="flex flex-col ml-2 items-center">
<span className="text-lg text-gray-500">
Select a Teammate
</span>
<span className="text-md text-gray-300 text-center">
Type numbers on your keyboard.
</span>
<span className="flex flex-row space-x-2">
<button
className={`right-1 rounded-lg py-1 px-2 ml-1
shadow-md text-center text-gray-300 text-sm font-bold`}
>
1
</button>
<button
className={`right-1 rounded-lg py-1 px-2 ml-1
shadow-md text-center text-gray-300 text-sm font-bold`}
>
2
</button>
<button
className={`right-1 rounded-lg py-1 px-2 ml-1
shadow-md text-center text-gray-300 text-sm font-bold`}
>
3
</button>
<span className="text-gray-300">...</span>
</span>
</span>
<img
src="/screenshots/teamvoiceline.png"
className="h-[10rem] w-[22rem]"
/>
</span>
<Divider />
<span className="flex flex-row justify-between items-center">
<img
src="/illustrations/undraw_conference_speaker_re_1rna.svg"
className="h-[10rem]"
/>
<span className="flex flex-col ml-2 items-center">
<span className="text-lg text-gray-500">
Send an Audio Message
</span>
<span className={`text-md text-gray-300 text-center`}>
Press and hold R.{" "}
<span className="text-teal-600">
Your teammate will <br></br> hear you instantly
</span>{" "}
if they are online.
</span>
<span className={`text-md text-gray-300 text-center`}>
As if they were across the table.
</span>
<button
className={`right-1 rounded-lg py-1 px-2 ml-1
shadow-md text-center text-gray-300 text-sm font-bold
${isRecording ? "bg-orange-500 text-white" : ""}`}
>
R
</button>
</span>
</span>
<Divider />
<span className="flex flex-row justify-between items-center">
<span className="flex flex-col ml-2 items-center">
<span className="text-lg text-gray-500">
Play Incoming Message
</span>
<span className={`text-md text-gray-300 text-center`}>
Select teammate and then press SPACE <br></br> to listen to
the last conversation.
</span>
{/* <span className={`text-md text-gray-300 text-center`}>
Deselect teammate by pressing <br></br> ESC.
</span> */}
<button
className={`right-1 rounded-lg py-1 px-2 ml-1
shadow-md text-center text-gray-300 text-sm font-bold`}
>
SPACE
</button>
</span>
<img
src="/illustrations/undraw_tutorial_video_vtd1.svg"
className="h-[10rem]"
/>
</span>
<Divider />
<span className="flex flex-row justify-between items-center">
<img
src="/illustrations/undraw_back_home_nl-5-c.svg"
className="h-[10rem]"
/>
<span className="flex flex-col ml-2 items-center">
<span className="text-lg text-gray-500">Clear Your View</span>
<span className={`text-md text-gray-300 text-center`}>
Deselect teammate and <br></br> stop playing audio by
pressing ESC.
</span>
<button
className={`right-1 rounded-lg py-1 px-2 ml-1
shadow-md text-center text-gray-300 text-sm font-bold`}
>
ESC
</button>
</span>
</span>
</span>
</div>
<div>
<span className="flex flex-col px-10">
<span className="flex flex-row justify-between items-start mb-10">
<FaArrowLeft
onClick={() => carouselRef.current.prev()}
className="hover:cursor-pointer bg-teal-600 text-white p-1 rounded-full text-xl shrink-0"
/>
<span className="flex flex-col items-center">
<span className="text-xl">Drawer</span>
<span className="text-md text-gray-300 text-center">
Organize your desk and{" "}
<span className="text-teal-600">
focus on today&apos;s priorities.
</span>
</span>
<img
src="/screenshots/drawer.png"
className="rounded-lg mt-5"
/>
</span>
<FaArrowRight
onClick={() => carouselRef.current.next()}
className="hover:cursor-pointer bg-teal-600 text-white p-1 rounded-full text-xl shrink-0"
/>
</span>
<span className="flex flex-row justify-between items-center">
<span className="flex flex-col ml-2 items-start">
<span className="text-lg text-gray-500">Add Your Drawer</span>
<span className={`text-md text-gray-300 text-left max-w-xs`}>
Paste{" "}
<span className={`text-teal-600`}>important links:</span>{" "}
Jira tickets, ppts, drive files/folders, github, code
snippets.
</span>
<span className="flex flex-row items-left">
{" "}
<button
className={`right-1 rounded-lg py-1 px-2 ml-1
shadow-md text-center text-gray-300 text-sm font-bold`}
>
CTRL
</button>{" "}
+{" "}
<button
className={`right-1 rounded-lg py-1 px-2 ml-1
shadow-md text-center text-gray-300 text-sm font-bold`}
>
V
</button>{" "}
</span>
</span>
<img
src="/illustrations/undraw_memory_storage_reh0.svg"
className="h-[12rem]"
/>
</span>
<Divider />
<span className="flex flex-row justify-between items-center">
<img
src="/illustrations/undraw_team_spirit_re_yl1v.svg"
className="h-[12rem]"
/>
<span className="flex flex-col ml-2 items-end max-w-xs">
<span className="text-lg text-gray-500">Team Drawer</span>
<span className={`text-md text-gray-300 text-right`}>
Make sure your team has <br></br> what they need from you.
</span>
<span className={`text-md text-teal-600 text-right`}>
Keep this clean to stay focused.
</span>
</span>
</span>
<Divider />
<span className="flex flex-row justify-between items-center">
<span className="flex flex-col ml-2 items-start">
<span className="text-lg text-gray-500">
{"You Decide What You Keep"}
</span>
<span className={`text-md text-gray-300 text-left`}>
Last month&apos;s mess is weighing you down.
</span>
<span className={`text-md text-gray-300 text-left`}>
Only see{" "}
<span className="text-teal-600">
this week&apos;s files/links
</span>{" "}
to keep <br></br> your head clear.
</span>
</span>
<img
src="/illustrations/undraw_my_files_swob.svg"
className="h-[10rem]"
/>
</span>
<Divider />
<span className={`text-md text-gray-300 text-center`}>
NOTE: this is{" "}
<span className="text-orange-500">NOT file storage.</span>
</span>
<span className={`text-md text-gray-300 text-center`}>
They are merely bookmarked links <br></br> to your source of
truth (Google Drive, Github...).
</span>
</span>
</div>
</Carousel>
</Modal>
</>
);
}
+179
View File
@@ -0,0 +1,179 @@
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>
);
}
}
+473
View File
@@ -0,0 +1,473 @@
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>
);
}
+82
View File
@@ -0,0 +1,82 @@
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>
);
}
}
+38
View File
@@ -0,0 +1,38 @@
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) {}
+47
View File
@@ -0,0 +1,47 @@
import { IoPulseOutline, IoRemoveOutline } from "react-icons/io5";
import { UserStatus } from "../models/user";
interface UserStatusPropsInterface {
status: UserStatus;
}
export default function UserStatusBubble(props: UserStatusPropsInterface) {
switch (props.status) {
case UserStatus.online:
return (
<span className="absolute top-0 right-0 w-3 h-3 bg-green-400 rounded-full"></span>
);
case UserStatus.busy:
return (
<span className="absolute top-0 right-0 w-3 h-3 bg-orange-400 rounded-full"></span>
);
case UserStatus.offline:
return (
<span className="absolute top-0 right-0 w-3 h-3 bg-gray-400 rounded-full"></span>
);
default:
return (
<span className="absolute top-0 right-0 w-3 h-3 bg-gray-400 rounded-full"></span>
);
}
}
// same as status pretty much but prolly less needed for all components
export function UserPulse(props: UserStatusPropsInterface) {
switch (props.status) {
case UserStatus.online:
return (
<IoPulseOutline className="text-green-500 text-2xl animate-pulse mx-2" />
);
case UserStatus.busy:
return (
<IoPulseOutline className="text-orange-400 text-2xl animate-pulse mx-2" />
);
case UserStatus.offline:
return <IoRemoveOutline className="text-gray-400 text-2xl mx-2" />;
default:
return (
<IoPulseOutline className="text-gray-400 text-2xl animate-pulse mx-2" />
);
}
}
+146
View File
@@ -0,0 +1,146 @@
import { FaAngleDown, FaCheck, FaMicrophoneAlt, FaPlay } from "react-icons/fa";
import { UserStatus } from "../../models/user";
import Image from "next/image";
import { Tooltip } from "antd";
import { DemoStep, IVoiceDemoProps } from "./VoiceLineConceptDemo";
import { useEffect, useState } from "react";
import toast from "react-hot-toast";
export default function Announcements(props: IVoiceDemoProps) {
const isAnnouncementsTurn =
props.demoStep == DemoStep.playAnnouncement ||
props.demoStep == DemoStep.doneDemo;
const [showAnnouncement, setShowAnnouncement] = useState<boolean>(false);
useEffect(() => {
if (props.demoStep == DemoStep.playAnnouncement) {
setTimeout(function () {
setShowAnnouncement(true);
var audio = new Audio(
"https://firebasestorage.googleapis.com/v0/b/nirvana-for-business.appspot.com/o/messages%2Fb2858200-4a15-478a-b863-bdf88b26b2e4.mp3?alt=media&token=3ffc8955-50fa-4418-a146-1c73f56409d1"
);
audio.play();
toast.success("Harold's saying his update!");
}, 2000);
setTimeout(function () {
props.handleChangeDemoStep(DemoStep.doneDemo);
toast.success("done with the demo!");
}, 11000);
}
}, [props.demoStep]);
return (
<section
className={`p-5 flex flex-col bg-gray-100 bg-opacity-25 rounded-lg shadow-md -translate-x-32 backdrop-blur-xl ${
isAnnouncementsTurn ? " z-20 " : "blur-sm"
}`}
>
<span className="flex flex-row justify-start items-center pb-5">
<span className="flex flex-col mr-20">
<span className="bg-clip-text text-transparent bg-gradient-to-r from-pink-500 to-violet-500">
ANNOUNCEMENTS
{/* <button
className="right-1 rounded-lg py-1 px-2 ml-1
shadow-md text-center text-gray-400 text-sm font-bold"
>
CTRL + A
</button> */}
</span>
<span className="text-gray-400 text-xs">
updates, pep talks, blockers, reminders
</span>
</span>
{/* tab pane */}
<span className="ml-auto flex flex-row space-x-5 uppercase mr-5">
<span className="underline underline-offset-8 decoration-gray-500 text-gray-500 hover:cursor-pointer">
Active
</span>
<span className="text-gray-400 hover:cursor-pointer">Resolved</span>
</span>
<span className="text-sm text-gray-400 flex flex-row items-center">
TODAY <FaAngleDown />
</span>
<Tooltip title={"You can create announcements in the future."}>
<button className="bg-gray-400 bg-opacity-25 p-2 ml-2 rounded hover:bg-opacity-40">
<FaMicrophoneAlt className="text-lg text-gray-500" />
</button>
</Tooltip>
</span>
<div className="flex flex-row overflow-auto whitespace-nowrap space-x-5 items-center">
{/* arjun's announcement */}
<span
className={`flex flex-row p-3 bg-gray-300 bg-opacity-25 rounded-lg items-center transition-all duration-300 ${
showAnnouncement ? "animate-pulse bg-orange-500" : "hidden "
}`}
>
<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-29.svg"}
alt="profile"
width={50}
height={50}
/>
</span>
<span className="flex flex-col items-baseline mr-5">
<span className="text-md font-semibold text-gray-600">Harold</span>
<span className="text-xs text-gray-400">2 seconds ago</span>
<span className="text-xs text-white bg-indigo-400 p-1 rounded-md font-semibold flex flex-row items-center">
<span>engineering</span>
</span>
</span>
<Tooltip
title={"Announcements should be resolved to keep the team focused."}
>
<button className="bg-gray-400 bg-opacity-25 p-2 ml-2 rounded hover:bg-opacity-40">
<FaCheck className="text-lg text-gray-500" />
</button>
</Tooltip>
</span>
{/* adriana's announcement */}
<span className="flex flex-row p-3 bg-gray-300 bg-opacity-25 rounded-lg items-center">
<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-06.svg"}
alt="profile"
width={50}
height={50}
/>
</span>
<span className="flex flex-col items-baseline">
<span className="text-md font-semibold text-gray-600">Adriana</span>
<span className="text-xs text-gray-400">30 minutes ago</span>
<span className="text-xs text-white bg-red-400 p-1 rounded-md font-semibold flex flex-row items-center">
<span>blockers</span>
</span>
</span>
<Tooltip title={"Resolve"}>
<button className="bg-gray-400 bg-opacity-25 p-2 ml-2 rounded hover:bg-opacity-40">
<FaCheck className="text-lg text-gray-500" />
</button>
</Tooltip>
</span>
</div>
</section>
);
}
@@ -0,0 +1,25 @@
import Link from "next/link";
import { FaAngleRight } from "react-icons/fa";
export default function LandingPageActionBar() {
return (
<span className="flex md:my-20 my-10 mx-auto md:flex-row flex-col items-center max-w-screen-md p-10 backdrop-blur-md bg-gray-200 bg-opacity-40 rounded-lg">
<span className="flex flex-col items-start text-3xl font-bold">
<span>Ready to Focus?</span>
<span className="text-teal-600">{"It's now or never."}</span>
</span>
<span className="flex flex-row items-center space-x-5 md:ml-auto">
<Link href="/features">Features</Link>
<button
onClick={() => window.open("/teams/login", "_blank")}
className="rounded font-semibold bg-teal-600 p-2 text-white shadow-lg flex flex-row items-center space-x-2"
>
<span>Get Started</span>
<FaAngleRight />
</button>
</span>
</span>
);
}
+260
View File
@@ -0,0 +1,260 @@
import { BsThreeDots } from "react-icons/bs";
import { FaAngleDown, FaBell, FaClock, FaLink, FaPlus } from "react-icons/fa";
import { IoTimer } from "react-icons/io5";
import Image from "next/image";
export default function Rooms() {
return (
<div className="flex flex-col">
{/* spontaneous bugs room */}
<span className="shrink-0 h-[15rem] backdrop-blur-xl flex flex-col bg-white bg-opacity-80 rounded-lg justify-between w-96 max-w-screen-sm m-2 overflow-clip">
{/* header */}
<span className="flex flex-1 flex-row justify-between items-baseline space-x-1 p-5">
{/* meeting details */}
<span className="flex flex-col items-baseline justify-start max-w-xs pr-10">
<span className="text-gray-500 font-semibold mr-auto">
bug fixing
</span>
<span className="text-xs text-gray-400 overflow-wrap mb-auto h-full">
were just fixing that jsx bug thats a paiiinnnn
</span>
{/* badges and tags */}
<span className="flex flex-row flex-wrap mt-auto">
<span className="text-xs m-1 text-white bg-red-400 p-1 rounded-md font-bold flex flex-row items-center">
<span>blockers</span>
</span>
<span className="text-xs m-1 text-white bg-indigo-400 p-1 rounded-md font-bold flex flex-row items-center">
<span>engineering</span>
</span>
</span>
</span>
{/* room status */}
<span className="flex flex-col items-end space-y-1 justify-between h-full">
<span
className="text-xs bg-red-500
text-white font-bold p-1 rounded-md flex flex-row space-x-2 items-center"
>
<FaClock />
<span>live</span>
</span>
{/* room attachments */}
<span className="flex flex-row space-x-2">
<button className="bg-gray-400 bg-opacity-25 p-2 ml-auto rounded hover:bg-opacity-40">
<FaLink className="text-sm text-gray-400" />
</button>
<button className="bg-gray-400 bg-opacity-25 p-2 ml-auto rounded hover:bg-opacity-40">
<FaLink className="text-sm text-gray-400" />
</button>
</span>
</span>
</span>
{/* footer */}
<span className="flex flex-row items-center bg-gray-400 bg-opacity-30 p-3">
<span className="inline-flex flex-row-reverse items-center shrink-0 mr-1">
<span className="relative flex">
<span className="bg-gray-200 rounded-full shadow-md absolute w-full h-full"></span>
<Image
className=""
src={
"/avatars/svg/Artboards_Diversity_Avatars_by_Netguru-20.svg"
}
alt="profile"
width={30}
height={30}
/>
</span>
<span className="-mr-4 relative flex">
<span className="bg-gray-200 rounded-full shadow-md absolute w-full h-full"></span>
<Image
className=""
src={
"/avatars/svg/Artboards_Diversity_Avatars_by_Netguru-22.svg"
}
alt="profile"
width={30}
height={30}
/>
</span>
</span>
<span className="text-xs text-gray-400">Arjun and Liam</span>
<button className="ml-auto text-sm text-orange-500 font-semibold py-1 px-4 bg-gray-200 rounded">
👋 Leave
</button>
<BsThreeDots className="text-white ml-2 hover:cursor-pointer" />{" "}
</span>
</span>
{/* live room - design meeting */}
<span className="shrink-0 h-[15rem] backdrop-blur-xl translate-x-32 -translate-y-32 flex flex-col bg-gray-300 bg-opacity-25 rounded-lg justify-between w-96 max-w-screen-sm m-2 overflow-clip">
{/* header */}
<span className="flex flex-row justify-between items-baseline space-x-1 p-5">
{/* meeting details */}
<span className="flex flex-col items-baseline max-w-xs pr-20">
<span className="text-gray-500 font-semibold mr-auto">
Shopping Cart Experience
</span>
<span className="text-xs text-gray-400 overflow-wrap">
lets finish the mockups @josh, @mark, and @arjun please step in
for feedback
</span>
{/* badges and tags */}
<span className="flex flex-row flex-wrap space-x-2">
<span className="text-xs my-3 text-white bg-purple-400 p-1 rounded-md font-bold flex flex-row space-x-2 items-center">
<span>design</span>
</span>
</span>
</span>
{/* room status */}
<span className="flex flex-col items-end justify-between h-full">
<span className="text-blue-700 bg-blue-200 p-1 rounded-md text-xs font-bold flex flex-row items-center space-x-1">
<FaClock />
<span>scheduled</span>
</span>
<span className="text-gray-400 text-xs text-right mb-auto">
afternoon
</span>
{/* room attachments */}
<span className="flex flex-row space-x-2">
<button className="bg-gray-400 bg-opacity-25 p-2 rounded hover:bg-opacity-40">
<FaLink className="text-sm text-gray-400" />
</button>
<button className="bg-gray-400 bg-opacity-25 p-2 rounded hover:bg-opacity-40">
<FaLink className="text-sm text-gray-400" />
</button>
</span>
</span>
</span>
{/* footer */}
<span className="flex flex-row items-center bg-gray-400 bg-opacity-30 p-3">
<span className="inline-flex flex-row-reverse items-center shrink-0 mr-1">
<span className="relative flex">
<span className="bg-gray-200 rounded-full shadow-md absolute w-full h-full"></span>
<Image
className=""
src={
"/avatars/svg/Artboards_Diversity_Avatars_by_Netguru-06.svg"
}
alt="profile"
width={30}
height={30}
/>
</span>
</span>
<span className="text-xs text-white">Adriana</span>
<button className="ml-auto text-sm font-semibold py-1 px-4 rounded bg-gray-300 text-green-500">
Join
</button>
<BsThreeDots className="text-white ml-2 hover:cursor-pointer" />
</span>
</span>
{/* recurring room - demo */}
<span className="shrink-0 h-[15rem] backdrop-blur-xl translate-x-30 -translate-y-60 flex flex-col bg-gray-300 bg-opacity-25 rounded-lg justify-between w-96 max-w-screen-sm m-2 overflow-clip">
{/* header */}
<span className="flex flex-row justify-between items-baseline space-x-1 p-5">
{/* meeting details */}
<span className="flex flex-col items-baseline max-w-xs pr-20">
<span className="text-gray-500 font-semibold mr-auto">
Sprint Demo
</span>
<span className="text-xs text-gray-400 overflow-wrap">
All hands on deck...lets have fun :)
</span>
{/* 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>
</span>
{/* room status */}
<span className="flex flex-col items-center justify-start">
<span className="text-yellow-700 bg-yellow-200 p-1 rounded-md text-xs font-bold flex flex-row items-center space-x-1">
<IoTimer />
<span>recurring</span>
</span>
<span className="text-gray-400 text-xs text-center">
biweekly fridays!
</span>
</span>
</span>
{/* footer */}
<span className="flex flex-row items-center bg-gray-400 bg-opacity-30 p-3">
<button className="ml-auto text-sm font-semibold py-1 px-4 rounded bg-gray-300 text-green-500">
Join
</button>
<button className="bg-orange-300 bg-opacity-25 p-2 ml-2 rounded hover:bg-opacity-40">
<FaBell className="text-sm text-orange-500" />
</button>
<BsThreeDots className="text-white ml-2 hover:cursor-pointer" />{" "}
</span>
</span>
{/* recurring room - wine wednesdays */}
<span className=" shrink-0 h-[15rem] backdrop-blur-xl translate-x-32 -translate-y-96 flex flex-col bg-gray-300 bg-opacity-25 rounded-lg justify-between w-96 max-w-screen-sm m-2 overflow-clip">
{/* header */}
<span className="flex flex-row justify-between items-baseline space-x-1 p-5">
{/* meeting details */}
<span className="flex flex-col items-baseline max-w-xs pr-20">
<span className="text-gray-500 font-semibold mr-auto">
Wine Wednesdays
</span>
<span className="text-xs text-gray-400 overflow-wrap">
@JOSH YOU BETTER COME
</span>
{/* badges and tags */}
<span className="flex flex-row flex-wrap space-x-2">
<span className="text-xs my-3 text-white bg-lime-400 p-1 rounded-md font-bold flex flex-row space-x-2 items-center">
<span>party 🎉</span>
</span>
</span>
</span>
{/* room status */}
<span className="flex flex-col items-end">
<span className="text-yellow-700 bg-yellow-200 p-1 rounded-md text-xs font-bold flex flex-row items-center space-x-1">
<IoTimer />
<span>recurring</span>
</span>
<span className="text-gray-400 text-xs text-center">
Wedn. 7-9pm
</span>
</span>
</span>
{/* footer */}
<span className="flex flex-row items-center bg-gray-400 bg-opacity-30 p-3">
<button className="ml-auto text-sm font-semibold py-1 px-4 rounded bg-gray-300 text-green-500">
Join
</button>
<button className="bg-orange-300 bg-opacity-25 p-2 ml-2 rounded hover:bg-opacity-40">
<FaBell className="text-sm text-orange-500" />
</button>
<BsThreeDots className="text-white ml-2 hover:cursor-pointer" />
</span>
</span>
</div>
);
}
+265
View File
@@ -0,0 +1,265 @@
import { FaArrowCircleDown, FaPlus } from "react-icons/fa";
import { UserStatus } from "../../models/user";
import Image from "next/image";
import { IoPulseOutline, IoRemoveOutline } from "react-icons/io5";
import { DemoStep, IVoiceDemoProps } from "./VoiceLineConceptDemo";
import { Popover, Tooltip } from "antd";
import { useCallback, useEffect, useState } from "react";
import { KeyCode } from "../../globals/keycode";
import toast from "react-hot-toast";
import { GlobalHotKeys, HotKeys, KeySequence, KeyMap } from "react-hotkeys";
let testFriends = [
{
name: "Harold",
role: "engineer",
systemAvatar: "29",
status: UserStatus.online,
},
{
name: "Emily",
role: "engineer",
systemAvatar: "02",
status: UserStatus.online,
},
{
name: "Paul",
role: "architect",
systemAvatar: "43",
status: UserStatus.online,
},
{
name: "Mark",
role: "designer",
systemAvatar: "04",
status: UserStatus.busy,
},
];
function statusBubble(status: UserStatus) {
switch (status) {
case UserStatus.online:
return (
<span className="absolute top-0 right-0 w-3 h-3 bg-green-400 rounded-full"></span>
);
case UserStatus.busy:
return (
<span className="absolute top-0 right-0 w-3 h-3 bg-orange-400 rounded-full"></span>
);
case UserStatus.offline:
return (
<span className="absolute top-0 right-0 w-3 h-3 bg-gray-400 rounded-full"></span>
);
default:
return (
<span className="absolute top-0 right-0 w-3 h-3 bg-green-400 rounded-full"></span>
);
}
}
function renderPulse(status: UserStatus) {
switch (status) {
case UserStatus.online:
return (
<IoPulseOutline className="text-green-500 text-2xl animate-pulse mx-2 ml-auto" />
);
case UserStatus.busy:
return (
<IoPulseOutline className="text-orange-400 text-2xl animate-pulse mx-2 ml-auto" />
);
case UserStatus.offline:
return (
<IoRemoveOutline className="text-gray-400 text-2xl mx-2 ml-auto" />
);
default:
return (
<IoPulseOutline className="text-green-400 text-2xl animate-pulse mx-2 ml-auto" />
);
}
}
function TeamVoiceLine(props: IVoiceDemoProps) {
const isVoiceLineTurn =
props.demoStep == DemoStep.playIncomingMessage ||
props.demoStep == DemoStep.hearReply ||
props.demoStep == DemoStep.sendReply ||
props.demoStep == DemoStep.doneDemo;
useEffect(() => {
if (props.demoStep == DemoStep.playIncomingMessage) {
// play audio of paul talking
var audio = new Audio(
"https://firebasestorage.googleapis.com/v0/b/nirvana-for-business.appspot.com/o/messages%2F62ca7369-7c0c-4b3e-a382-12d4a237d1af.mp3?alt=media&token=b6d4acb0-afc9-4a9f-8c58-cf60a6079003"
);
audio.play();
setTimeout(function () {
props.handleChangeDemoStep(DemoStep.sendReply);
}, 10000);
} else if (props.demoStep == DemoStep.sendReply) {
} else if (props.demoStep == DemoStep.hearReply) {
var audio = new Audio(
"https://firebasestorage.googleapis.com/v0/b/nirvana-for-business.appspot.com/o/deep%20fakes%2Fvocodes_728915c1-eb1f-4f16-8f70-c7b0c2235b3e.wav?alt=media&token=76afd74c-7499-4bd8-9c10-dd6ff4f81c48"
);
audio.play();
setTimeout(function () {
props.handleChangeDemoStep(DemoStep.playAnnouncement);
}, 6000);
}
}, [props.demoStep]);
const [isRecording, setIsRecording] = useState<boolean>(false);
function startRecord(event) {
setIsRecording(true);
}
function endRecording(event) {
toast.success("Paul heard it live!");
setIsRecording(false);
setTimeout(function () {
toast.success("Mark is talking...");
props.handleChangeDemoStep(DemoStep.hearReply);
}, 3000);
}
const keyMap: KeyMap = {
RECORD: "r",
STOP_RECORDING: {
name: "Stop recording",
sequence: "r",
action: "keyup",
},
};
const handlers = { RECORD: startRecord, STOP_RECORDING: endRecording };
return (
<section
className={`p-5 flex w-80 flex-col bg-gray-100 bg-opacity-25 rounded-lg shadow-2xl z-10 translate-y-24 translate-x-20 backdrop-blur-xl transition-all duration-300 ${
!isVoiceLineTurn ? "blur-sm" : ""
} ${props.demoStep == DemoStep.doneDemo ? "z-30" : ""}`}
>
{/* keyboard shortcut handler */}
<GlobalHotKeys keyMap={keyMap} handlers={handlers} />
<span className="flex flex-row justify-start items-center pb-5">
<span className="flex flex-col">
<span className="bg-clip-text text-transparent bg-gradient-to-r from-pink-500 to-violet-500">
TEAM
</span>
</span>
<Tooltip title="You will be able to add team members to your voice line.">
<button className="bg-gray-400 bg-opacity-25 p-2 ml-auto rounded hover:bg-opacity-40">
<FaPlus className="text-lg text-gray-500" />
</button>
</Tooltip>
</span>
{/* list of team members */}
{testFriends.map((friend, i) => {
return (
<span
key={i}
className={`flex flex-row items-center py-2 px-2 justify-items-start ease-in-out duration-300 rounded-lg ${
(friend.name == "Paul" &&
props.demoStep == DemoStep.playIncomingMessage) ||
(friend.name == "Mark" && props.demoStep == DemoStep.hearReply)
? "bg-orange-500 bg-opacity-20"
: ""
}`}
>
<span
className={`relative flex mr-2 shrink-0 ${
(friend.name == "Paul" &&
props.demoStep == DemoStep.playIncomingMessage) ||
(friend.name == "Mark" && props.demoStep == DemoStep.hearReply)
? "animate-pulse"
: ""
}`}
>
<span className="bg-gray-200 bg-opacity-30 rounded-full shadow-md absolute w-full h-full"></span>
{statusBubble(friend.status)}
<Image
src={
"/avatars/svg/Artboards_Diversity_Avatars_by_Netguru-" +
friend.systemAvatar +
".svg"
}
alt="profile"
width={50}
height={50}
/>
</span>
<span className="flex flex-col mr-10">
<span className="flex flex-row items-center space-x-2">
<span className="text-sm text-gray-600 font-semibold">
{friend.name}{" "}
</span>
{(friend.name == "Paul" &&
props.demoStep == DemoStep.playIncomingMessage) ||
(friend.name == "Mark" &&
props.demoStep == DemoStep.hearReply) ? (
<FaArrowCircleDown className="text-orange-500 text-xl animate-bounce" />
) : (
<></>
)}
{friend.name == "Paul" &&
props.demoStep == DemoStep.sendReply ? (
<Tooltip
title={
"PRESS AND HOLD R (on your keyboard) to record a message and send"
}
visible={!isRecording}
>
<button
className={`rounded-lg py-1 px-2 shadow-lg
text-center text-sm font-bold ranimate-bounce ${
isRecording
? "bg-orange-500 text-white"
: "text-orange-500"
}`}
>
R
</button>
</Tooltip>
) : (
<button
className={`rounded-lg py-1 px-2 shadow-lg
text-center text-sm font-bold ${
friend.name == "Paul"
? "text-orange-500"
: "text-gray-200 "
}`}
>
{i}
</button>
)}
</span>
<span className={"text-xs span-sans text-gray-400"}>
{friend.role}
</span>
</span>
{renderPulse(friend.status)}
</span>
);
})}
</section>
);
}
export default TeamVoiceLine;
@@ -0,0 +1,64 @@
import { Tooltip } from "antd";
import { useState } from "react";
import { FaPlay } from "react-icons/fa";
import Announcements from "./Announcements";
import TeamVoiceLine from "./TeamVoiceLine";
export enum DemoStep {
playDemo = "play demo",
playIncomingMessage = "play incoming message",
sendReply = "send reply",
hearReply = "hear reply",
playAnnouncement = "play announcement",
doneDemo = "done with demo",
}
export interface IVoiceDemoProps {
demoStep: DemoStep;
handleChangeDemoStep: Function;
}
export default function VoiceLineConceptDemo() {
const [demoStep, setDemoStep] = useState<DemoStep>(DemoStep.playDemo);
function handleChangeStep(newStep: DemoStep) {
setDemoStep(newStep);
}
return (
<>
<TeamVoiceLine
demoStep={demoStep}
handleChangeDemoStep={handleChangeStep}
/>
<Announcements
demoStep={demoStep}
handleChangeDemoStep={handleChangeStep}
/>
{demoStep == DemoStep.playDemo ? (
<Tooltip title={"Make sure to have your speakers on!"}>
<button
onClick={() => setDemoStep(DemoStep.playIncomingMessage)}
className="absolute mx-auto my-auto left-1/2 top-1/2
bg-gray-200 py-2 px-2 rounded font-semibold inline-flex items-center text-teal-600
space-x-2 animate-bounce z-50"
>
<FaPlay /> <span>Play Demo</span>
</button>
</Tooltip>
) : (
<button
onClick={() => setDemoStep(DemoStep.playDemo)}
className="absolute right-0 bottom-0
bg-gray-200 bg-opacity-30 py-2 px-2 rounded inline-flex items-center text-teal-600
space-x-2"
>
<span>Reset Demo</span>
</button>
)}
</>
);
}