having one solution for different components of app
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"extends": "next/core-web-vitals"
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
||||
|
||||
# dependencies
|
||||
/node_modules
|
||||
/.pnp
|
||||
.pnp.js
|
||||
|
||||
# testing
|
||||
/coverage
|
||||
|
||||
# next.js
|
||||
/.next/
|
||||
/out/
|
||||
|
||||
# production
|
||||
/build
|
||||
|
||||
# misc
|
||||
.DS_Store
|
||||
*.pem
|
||||
|
||||
# debug
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
|
||||
# local env files
|
||||
.env.local
|
||||
.env.development.local
|
||||
.env.test.local
|
||||
.env.production.local
|
||||
|
||||
# vercel
|
||||
.vercel
|
||||
@@ -0,0 +1,34 @@
|
||||
This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app).
|
||||
|
||||
## Getting Started
|
||||
|
||||
First, run the development server:
|
||||
|
||||
```bash
|
||||
npm run dev
|
||||
# or
|
||||
yarn dev
|
||||
```
|
||||
|
||||
Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
|
||||
|
||||
You can start editing the page by modifying `pages/index.js`. The page auto-updates as you edit the file.
|
||||
|
||||
[API routes](https://nextjs.org/docs/api-routes/introduction) can be accessed on [http://localhost:3000/api/hello](http://localhost:3000/api/hello). This endpoint can be edited in `pages/api/hello.js`.
|
||||
|
||||
The `pages/api` directory is mapped to `/api/*`. Files in this directory are treated as [API routes](https://nextjs.org/docs/api-routes/introduction) instead of React pages.
|
||||
|
||||
## Learn More
|
||||
|
||||
To learn more about Next.js, take a look at the following resources:
|
||||
|
||||
- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API.
|
||||
- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.
|
||||
|
||||
You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your feedback and contributions are welcome!
|
||||
|
||||
## Deploy on Vercel
|
||||
|
||||
The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js.
|
||||
|
||||
Check out our [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more details.
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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",
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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">© 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'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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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 `} />
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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'll create a link for you. Don'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'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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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'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'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'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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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) {}
|
||||
@@ -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" />
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,105 @@
|
||||
import {
|
||||
getAuth,
|
||||
GoogleAuthProvider,
|
||||
onAuthStateChanged,
|
||||
signInWithPopup,
|
||||
signOut,
|
||||
User,
|
||||
} from "firebase/auth";
|
||||
import { useRouter } from "next/router";
|
||||
import React, { useContext, useEffect, useState } from "react";
|
||||
import toast from "react-hot-toast";
|
||||
import Loading from "../components/Loading";
|
||||
import firebase from "../services/firebaseService";
|
||||
import cookie from "js-cookie";
|
||||
|
||||
const AuthContext = React.createContext(null);
|
||||
const googleProvider = new GoogleAuthProvider();
|
||||
|
||||
const auth = getAuth();
|
||||
|
||||
export function AuthProvider({ children }) {
|
||||
const [currUser, setCurrUser] = useState();
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
const router = useRouter();
|
||||
|
||||
useEffect(() => {
|
||||
const unsubscribe = onAuthStateChanged(auth, async (user) => {
|
||||
setLoading(true);
|
||||
|
||||
console.log("change in auth");
|
||||
if (user) {
|
||||
setCurrUser(user);
|
||||
|
||||
// set the token in the cookies for ssr verification
|
||||
const token = await user.getIdToken();
|
||||
// nookies.set(undefined, "token", token, { path: "/" });
|
||||
cookie.set("auth", token);
|
||||
} else {
|
||||
// nookies.set(undefined, "token", "", { path: "/" });
|
||||
cookie.remove("auth");
|
||||
|
||||
setCurrUser(null);
|
||||
}
|
||||
|
||||
setLoading(false);
|
||||
});
|
||||
|
||||
return unsubscribe;
|
||||
}, []);
|
||||
|
||||
const signInGoogle = () => {
|
||||
return signInWithPopup(auth, googleProvider)
|
||||
.then((res) => {
|
||||
// This gives you a Google Access Token. You can use it to access the Google API.
|
||||
const credential = GoogleAuthProvider.credentialFromResult(res);
|
||||
|
||||
const token = credential.accessToken;
|
||||
// The signed-in user info.
|
||||
const user = res.user;
|
||||
|
||||
setCurrUser(user);
|
||||
|
||||
console.log(user);
|
||||
})
|
||||
.catch((error) => {
|
||||
// Handle Errors here.
|
||||
const errorCode = error.code;
|
||||
const errorMessage = error.message;
|
||||
// The email of the user's account used.
|
||||
const email = error.email;
|
||||
// The AuthCredential type that was used.
|
||||
const credential = GoogleAuthProvider.credentialFromError(error);
|
||||
console.log(error);
|
||||
toast.error("something went wrong");
|
||||
});
|
||||
};
|
||||
|
||||
const logOut = async () => {
|
||||
setLoading(true);
|
||||
|
||||
console.log("logging user out");
|
||||
|
||||
Promise.resolve(await signOut(auth));
|
||||
|
||||
router.push("/");
|
||||
};
|
||||
|
||||
const value = {
|
||||
currUser,
|
||||
signInGoogle,
|
||||
logOut,
|
||||
};
|
||||
|
||||
return (
|
||||
<AuthContext.Provider value={value}>
|
||||
{!loading && children}
|
||||
</AuthContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
// custom hook to use the authUserContext and access currUser
|
||||
export function useAuth() {
|
||||
return useContext(AuthContext);
|
||||
}
|
||||
@@ -0,0 +1,646 @@
|
||||
import React, { useCallback, useContext, useEffect, useState } from "react";
|
||||
import toast from "react-hot-toast";
|
||||
import { KeyCode } from "../globals/keycode";
|
||||
import MicRecorder from "mic-recorder-to-mp3";
|
||||
import { v4 as uuidv4 } from "uuid";
|
||||
|
||||
import AudioPlayer from "react-h5-audio-player";
|
||||
import "react-h5-audio-player/lib/styles.css";
|
||||
import CloudStorageService from "../services/cloudStorageService";
|
||||
import { Message } from "../models/message";
|
||||
import { useAuth } from "./authContext";
|
||||
import { SendService } from "../services/sendService";
|
||||
import { useTeamDashboardContext } from "./teamDashboardContext";
|
||||
import isValidHttpUrl from "../helpers/urlHelper";
|
||||
import { GlobalHotKeys, KeyMap } from "react-hotkeys";
|
||||
import Announcement from "../models/announcement";
|
||||
|
||||
interface KeyboardContextInterface {
|
||||
selectedTeammate: string; // can only have one selected
|
||||
selectTeamMember: Function;
|
||||
|
||||
teamShortcutMappings: {};
|
||||
addTeamShortcutBinding: Function;
|
||||
|
||||
isRecording: Boolean; // can only record if someone is selected or maybe for an announcement
|
||||
isRecordingAnnouncement: boolean;
|
||||
|
||||
isMuted: Boolean;
|
||||
isSilenceMode: Boolean; // won't automatically listen to notifications or sounds
|
||||
muteOrUnmute: Function;
|
||||
silenceOrLivenMode: Function;
|
||||
|
||||
hasRecPermit: Boolean; // permission to record or not
|
||||
|
||||
audioInputDeviceId: string;
|
||||
audioOutputDeviceId: string;
|
||||
|
||||
selectAudioOutput: Function;
|
||||
selectAudioInput: Function;
|
||||
|
||||
inputDevices: MediaDeviceInfo[];
|
||||
outputDevices: MediaDeviceInfo[];
|
||||
|
||||
ctrlDown: boolean;
|
||||
|
||||
showModalType: ShowModalType;
|
||||
handleModalType: Function;
|
||||
pastedLink: string;
|
||||
|
||||
handleAddAudioToQueue: Function;
|
||||
}
|
||||
|
||||
export enum ShowModalType {
|
||||
createLink = "link",
|
||||
createRoom = "room",
|
||||
powerPlayer = "powerPlayer",
|
||||
na = "none",
|
||||
}
|
||||
|
||||
const KeyboardContext = React.createContext<KeyboardContextInterface | null>(
|
||||
null
|
||||
);
|
||||
|
||||
function stopBothVideoAndAudio(stream) {
|
||||
stream.getTracks().forEach(function (track) {
|
||||
if (track.readyState == "live") {
|
||||
track.stop();
|
||||
|
||||
console.log("stopped playing anything");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
const cloudStorageService = new CloudStorageService();
|
||||
const sendService = new SendService();
|
||||
|
||||
export default function KeyboardContextProvider({ children }) {
|
||||
const { currUser } = useAuth();
|
||||
|
||||
// SECTION: set up for shortcuts and recording and such
|
||||
const [selectedTeammate, setSelectedTeamMember] = useState<string>(null); // id of selected teammate
|
||||
const [isRecording, setIsRecording] = useState<Boolean>(false);
|
||||
const [isRecordingAnnouncement, setIsRecordingAnnouncement] =
|
||||
useState<boolean>(false);
|
||||
const [hasRecPermit, setHasRecPermit] = useState<Boolean>(false);
|
||||
|
||||
const [ctrlDown, setCtrlDown] = useState<boolean>(false);
|
||||
|
||||
const [teamShortcutMappings, setTeamShortcutMappings] = useState<{}>({});
|
||||
const [audioInputDeviceId, setAudioInputDevice] = useState<string>(null); // device id
|
||||
const [audioOutputDeviceId, setAudioOutputDevice] = useState<string>(null); // device id
|
||||
|
||||
const [inputDevices, setInputDevices] = useState<MediaDeviceInfo[]>([]);
|
||||
const [outputDevices, setOutputDevices] = useState<MediaDeviceInfo[]>([]);
|
||||
|
||||
const [isMuted, setIsMuted] = useState<Boolean>(false);
|
||||
const [isSilenceMode, setIseSilenceMode] = useState<Boolean>(false);
|
||||
|
||||
const [pastedLink, setPastedLink] = useState<string>(null);
|
||||
const [showModalType, setShowModalType] = useState<ShowModalType>(
|
||||
ShowModalType.na
|
||||
);
|
||||
|
||||
const handleModalType = (modalType: ShowModalType) => {
|
||||
handleResetView();
|
||||
setShowModalType(modalType);
|
||||
};
|
||||
|
||||
const [recorder, setRecorder] = useState<MicRecorder>(
|
||||
new MicRecorder({ bitRate: 128 })
|
||||
);
|
||||
|
||||
const [audioQueue, setAudioQueue] = useState<string[]>([]); // queue for the player to keep playing
|
||||
|
||||
// playing incoming messages
|
||||
const { allMessages, messagesByTeamMate, team } = useTeamDashboardContext();
|
||||
|
||||
useEffect(() => {
|
||||
if (!allMessages.length) {
|
||||
toast("no message to play");
|
||||
return;
|
||||
}
|
||||
|
||||
// only play if it's incoming, as I listened to my message before sending it
|
||||
if (allMessages[0].receiverUserId == currUser.uid) {
|
||||
console.log("playing message");
|
||||
console.log(allMessages[0]);
|
||||
|
||||
// select the user
|
||||
setSelectedTeamMember(allMessages[0].senderUserId);
|
||||
|
||||
// start player on the bottom
|
||||
// autoplay message
|
||||
// add to the queue
|
||||
handleAddAudioToQueue([allMessages[0].audioDataUrl]);
|
||||
}
|
||||
}, [allMessages]);
|
||||
|
||||
// manage audio queue
|
||||
useEffect(() => {
|
||||
// every time queue changes,
|
||||
// setPlayerSrc as the next item if there is any to play it
|
||||
|
||||
if (audioQueue && audioQueue.length > 0) {
|
||||
toast("Adding Message to Queue");
|
||||
|
||||
// todo: edge case, if it's the same link again, then still play it somehow
|
||||
if (playerSrc == audioQueue[0]) {
|
||||
// play empty one and then add the next audio file
|
||||
setPlayerSrc("");
|
||||
|
||||
setTimeout(() => {
|
||||
setPlayerSrc(audioQueue[0]);
|
||||
}, 1000);
|
||||
} else {
|
||||
setPlayerSrc(audioQueue[0]);
|
||||
}
|
||||
}
|
||||
|
||||
if (!audioQueue || audioQueue.length == 0) {
|
||||
setPlayerSrc(null);
|
||||
}
|
||||
}, [audioQueue]);
|
||||
|
||||
// show toast when recording
|
||||
useEffect(() => {
|
||||
if (isRecordingAnnouncement || isRecording) {
|
||||
toast.loading("Recording...");
|
||||
} else {
|
||||
toast.dismiss();
|
||||
}
|
||||
}, [isRecording, isRecordingAnnouncement]);
|
||||
|
||||
const value: KeyboardContextInterface = {
|
||||
selectedTeammate,
|
||||
selectTeamMember,
|
||||
addTeamShortcutBinding,
|
||||
isRecording,
|
||||
isRecordingAnnouncement,
|
||||
teamShortcutMappings,
|
||||
|
||||
isMuted,
|
||||
isSilenceMode,
|
||||
muteOrUnmute,
|
||||
silenceOrLivenMode,
|
||||
|
||||
hasRecPermit,
|
||||
|
||||
audioInputDeviceId,
|
||||
audioOutputDeviceId,
|
||||
selectAudioOutput,
|
||||
selectAudioInput,
|
||||
|
||||
inputDevices,
|
||||
outputDevices,
|
||||
|
||||
ctrlDown,
|
||||
|
||||
pastedLink,
|
||||
handleModalType,
|
||||
showModalType,
|
||||
|
||||
handleAddAudioToQueue,
|
||||
};
|
||||
|
||||
function muteOrUnmute() {
|
||||
toast.success(isMuted ? "Unmuted" : "Muted");
|
||||
|
||||
setIsMuted((prevVal) => !prevVal);
|
||||
}
|
||||
|
||||
function silenceOrLivenMode() {
|
||||
toast.success(isSilenceMode ? "Unsilenced" : "Auto listen mode disabled");
|
||||
|
||||
setIseSilenceMode((prevVal) => !prevVal);
|
||||
}
|
||||
|
||||
function selectAudioOutput(deviceId: string) {
|
||||
toast.success("Changed output device");
|
||||
setAudioOutputDevice(deviceId);
|
||||
}
|
||||
|
||||
function selectAudioInput(deviceId: string) {
|
||||
toast.success("Changed input device");
|
||||
setAudioInputDevice(deviceId);
|
||||
}
|
||||
|
||||
// set up audio
|
||||
useEffect(() => {
|
||||
(async function () {
|
||||
try {
|
||||
const devices = await navigator.mediaDevices.enumerateDevices();
|
||||
|
||||
const inputDevices: MediaDeviceInfo[] = devices.filter(
|
||||
(d) => d.kind == "audioinput"
|
||||
);
|
||||
|
||||
setAudioInputDevice(inputDevices ? inputDevices[0].deviceId : null);
|
||||
setInputDevices(inputDevices);
|
||||
|
||||
const outputDevices: MediaDeviceInfo[] = devices.filter(
|
||||
(d) => d.kind == "audiooutput"
|
||||
);
|
||||
|
||||
setAudioOutputDevice(outputDevices ? outputDevices[0].deviceId : null);
|
||||
setOutputDevices(outputDevices);
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
toast.error("Problem in setting up audio devices");
|
||||
}
|
||||
})();
|
||||
}, [hasRecPermit]);
|
||||
|
||||
// first load just force check if permissions enabled
|
||||
// through fake stream
|
||||
// todo: only call this when starting to record and do it with a usecallback
|
||||
useEffect(() => {
|
||||
try {
|
||||
// won't work in https!!!
|
||||
navigator.mediaDevices
|
||||
.getUserMedia({ audio: { deviceId: audioInputDeviceId } })
|
||||
.then((stream) => {
|
||||
// stop playing anything
|
||||
stopBothVideoAndAudio(stream);
|
||||
|
||||
console.log("Permission Granted");
|
||||
setHasRecPermit(true);
|
||||
})
|
||||
.catch((e) => {
|
||||
console.log("Permission Denied");
|
||||
toast.error(
|
||||
"Please make sure that you have connected a microphone and given permissions."
|
||||
);
|
||||
setHasRecPermit(false);
|
||||
});
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
toast.error("something went wrong");
|
||||
}
|
||||
}, []);
|
||||
|
||||
// set recording device
|
||||
useEffect(() => {
|
||||
setRecorder(
|
||||
new MicRecorder({ bitRate: 128, deviceId: audioInputDeviceId })
|
||||
);
|
||||
}, [audioInputDeviceId]); // change it everytime we change the input device
|
||||
|
||||
// SECTION: recording
|
||||
async function startRecording() {
|
||||
recorder
|
||||
.start()
|
||||
.then(() => {
|
||||
// toast.success("started recording");
|
||||
console.log("recording started");
|
||||
})
|
||||
.catch((e) =>
|
||||
toast.error("there was a problem in starting your recording")
|
||||
);
|
||||
}
|
||||
|
||||
async function stopRecording(): Promise<File> {
|
||||
return new Promise((resolve, reject) => {
|
||||
recorder
|
||||
.stop()
|
||||
.getMp3()
|
||||
.then(([buffer, blob]) => {
|
||||
const blobURL = URL.createObjectURL(blob);
|
||||
|
||||
const file = new File(buffer, uuidv4() + ".mp3", {
|
||||
type: blob.type,
|
||||
lastModified: Date.now(),
|
||||
});
|
||||
|
||||
const player = new Audio(URL.createObjectURL(file));
|
||||
player.onended = onEndedPlaying;
|
||||
player.play();
|
||||
|
||||
resolve(file);
|
||||
})
|
||||
.catch((error) => {
|
||||
reject(error.message);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function onEndedPlaying(e) {
|
||||
// toast.success("finished playing");
|
||||
|
||||
// remove from queue and the queue manager will handle the rest
|
||||
setAudioQueue((prevQueue) => {
|
||||
let newQueue: string[] = [...prevQueue];
|
||||
newQueue.shift();
|
||||
return newQueue;
|
||||
});
|
||||
}
|
||||
|
||||
const handleKeyUp = useCallback(
|
||||
(event) => {
|
||||
// if was recording and released R, then stop recording and send message
|
||||
if (event.keyCode == KeyCode.R && selectedTeammate && isRecording) {
|
||||
console.log("stopped recording");
|
||||
setIsRecording(false);
|
||||
|
||||
const currReceiverUserId = selectedTeammate;
|
||||
|
||||
stopRecording()
|
||||
.then((file) => {
|
||||
console.log(file);
|
||||
|
||||
// upload to cloud storage
|
||||
return cloudStorageService.uploadMessageAudioFile(file);
|
||||
})
|
||||
.then((downloadUrl) => {
|
||||
console.log("file is stored: " + downloadUrl);
|
||||
|
||||
// send message to firestore
|
||||
const message = new Message();
|
||||
message.audioDataUrl = downloadUrl;
|
||||
message.senderUserId = currUser.uid;
|
||||
message.receiverUserId = currReceiverUserId;
|
||||
|
||||
return sendService.sendMessage(message);
|
||||
})
|
||||
.then(() => {
|
||||
toast.success("clip sent");
|
||||
})
|
||||
.catch((error) => {
|
||||
toast.error("Problem in sending clip");
|
||||
});
|
||||
|
||||
console.log("sending message to " + selectedTeammate);
|
||||
|
||||
handleResetView();
|
||||
} else if (event.keyCode == KeyCode.Ctrl) {
|
||||
setCtrlDown(false);
|
||||
}
|
||||
},
|
||||
[isRecording, selectedTeammate]
|
||||
);
|
||||
|
||||
const handleKeyboardShortcut = useCallback(
|
||||
(event) => {
|
||||
// no need to monitor repeats
|
||||
if (event.repeat) {
|
||||
return;
|
||||
}
|
||||
|
||||
// handle for when in modals, don't want any of this crap
|
||||
if (showModalType != ShowModalType.na) {
|
||||
return;
|
||||
}
|
||||
|
||||
// recording
|
||||
if (event.keyCode == KeyCode.R && !ctrlDown) {
|
||||
if (!audioInputDeviceId) {
|
||||
toast.error("No microphone selected");
|
||||
} else if (!hasRecPermit) {
|
||||
toast.error(
|
||||
"You did not allow microphone permissions in your browser!"
|
||||
);
|
||||
} else if (!selectedTeammate) {
|
||||
toast.error("Please select a team member or announcements first");
|
||||
} else if (isMuted) {
|
||||
toast.error("You are muted!");
|
||||
} else {
|
||||
// alright now you are good to go
|
||||
console.log("started recording");
|
||||
setIsRecording(true);
|
||||
startRecording();
|
||||
}
|
||||
}
|
||||
// if we have a valid user for such a shortcut, then go ahead...otherwise
|
||||
else if (teamShortcutMappings[event.keyCode]) {
|
||||
setSelectedTeamMember(teamShortcutMappings[event.keyCode]);
|
||||
} else if (event.keyCode == KeyCode.Escape) {
|
||||
handleResetView();
|
||||
} else if (event.keyCode == KeyCode.Space) {
|
||||
// listen to last message in convo
|
||||
if (isSilenceMode) {
|
||||
toast.error("you are in silence mode, please disable it first");
|
||||
} else if (!selectedTeammate) {
|
||||
toast.error("select a team member first to play");
|
||||
} else {
|
||||
// alright now you are good to play last message chunk in conversation with selected user
|
||||
|
||||
const arrayMessagesforUser: Message[] =
|
||||
messagesByTeamMate.get(selectedTeammate);
|
||||
if (
|
||||
messagesByTeamMate.has(selectedTeammate) &&
|
||||
arrayMessagesforUser &&
|
||||
arrayMessagesforUser.length > 0
|
||||
) {
|
||||
var convoChunk: string[] = [];
|
||||
console.log("playing from this cache");
|
||||
|
||||
// wait for x changes, and then break adding to the queue...I want to hear the past loop of conversation maybe
|
||||
const maxConvoChanges: number = 1;
|
||||
var convoChangeCount: number = 0;
|
||||
|
||||
var currTalkerId = arrayMessagesforUser[0].senderUserId;
|
||||
for (const audioMessage of arrayMessagesforUser) {
|
||||
if (audioMessage.senderUserId != currTalkerId) {
|
||||
convoChangeCount += 1;
|
||||
}
|
||||
|
||||
// heard enough of this convo
|
||||
if (convoChangeCount == maxConvoChanges) {
|
||||
break;
|
||||
}
|
||||
|
||||
convoChunk.push(audioMessage.audioDataUrl);
|
||||
currTalkerId = audioMessage.senderUserId;
|
||||
}
|
||||
|
||||
// reverse the chunk so that I listen to the messages in order
|
||||
convoChunk.reverse();
|
||||
|
||||
// add convo chunk to the queue player
|
||||
handleAddAudioToQueue(convoChunk);
|
||||
} else {
|
||||
toast("nothing to play");
|
||||
}
|
||||
}
|
||||
} else if (event.keyCode == KeyCode.Ctrl) {
|
||||
handleResetView();
|
||||
|
||||
setCtrlDown(true);
|
||||
} else if (event.keyCode == KeyCode.Q && ctrlDown) {
|
||||
toast("navigating to create google meet");
|
||||
|
||||
window.open(new URL("https://meet.google.com/"), "_blank");
|
||||
} else if (event.keyCode == KeyCode.V && ctrlDown) {
|
||||
toast("pasting link");
|
||||
|
||||
navigator.clipboard
|
||||
.readText()
|
||||
.then((text) => {
|
||||
console.log("Pasted content: ", text);
|
||||
|
||||
// check that the link is valid
|
||||
if (!isValidHttpUrl(text)) {
|
||||
toast.error("Not a valid link");
|
||||
return;
|
||||
}
|
||||
|
||||
// set it as we should show modal regardless now
|
||||
setPastedLink(text);
|
||||
|
||||
// see which modal to go to based on room vs. attachment
|
||||
// https://meet.google.com/soc-ebwc-rkt
|
||||
if (text.includes("meet.google.com")) {
|
||||
setShowModalType(ShowModalType.createRoom);
|
||||
} else {
|
||||
setShowModalType(ShowModalType.createLink);
|
||||
}
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error("Failed to read clipboard contents: ", err);
|
||||
toast.error("Please enable permissions for clipboard");
|
||||
});
|
||||
} else {
|
||||
// toast("Invalid keyboard shortcut.");
|
||||
console.log("no shortcut logged");
|
||||
}
|
||||
|
||||
// todo play message if pressing space
|
||||
|
||||
// todo if we press the same shortcut twice, deactive selected user
|
||||
},
|
||||
[
|
||||
selectedTeammate,
|
||||
audioInputDeviceId,
|
||||
hasRecPermit,
|
||||
isMuted,
|
||||
teamShortcutMappings,
|
||||
ctrlDown,
|
||||
showModalType,
|
||||
]
|
||||
);
|
||||
|
||||
function handleResetView() {
|
||||
setSelectedTeamMember(null);
|
||||
setShowModalType(ShowModalType.na);
|
||||
|
||||
setAudioQueue([]);
|
||||
}
|
||||
|
||||
// IMPORTANT: shortcut handlers need to be updated as the function has to have the fresh state
|
||||
useEffect(() => {
|
||||
console.log("updating event listeners");
|
||||
document.addEventListener("keydown", handleKeyboardShortcut);
|
||||
document.addEventListener("keyup", handleKeyUp);
|
||||
|
||||
return () => {
|
||||
document.removeEventListener("keydown", handleKeyboardShortcut);
|
||||
document.removeEventListener("keyup", handleKeyUp);
|
||||
};
|
||||
}, [handleKeyboardShortcut, handleKeyUp]);
|
||||
|
||||
function addTeamShortcutBinding(keyCode: number, userId: string) {
|
||||
setTeamShortcutMappings((prevMap) => ({ ...prevMap, [keyCode]: userId }));
|
||||
}
|
||||
|
||||
function selectTeamMember(userId: string) {
|
||||
setSelectedTeamMember(userId);
|
||||
}
|
||||
|
||||
const [playerSrc, setPlayerSrc] = useState<string>(null);
|
||||
|
||||
function handleAddAudioToQueue(
|
||||
urls: string[],
|
||||
clearEverythingElse: boolean = false
|
||||
) {
|
||||
if (clearEverythingElse) {
|
||||
setAudioQueue(urls);
|
||||
} else {
|
||||
setAudioQueue((prevQueue) => [...prevQueue, ...urls]);
|
||||
}
|
||||
}
|
||||
|
||||
function startAnnouncement() {
|
||||
setIsRecordingAnnouncement(true);
|
||||
|
||||
startRecording();
|
||||
}
|
||||
|
||||
async function sendAnnouncement() {
|
||||
setIsRecordingAnnouncement(false);
|
||||
|
||||
try {
|
||||
const file = await stopRecording();
|
||||
|
||||
// confirm with user that he wants to make the announcement
|
||||
// before sending and all
|
||||
// show this 2 seconds after playing entire message
|
||||
setTimeout(async () => {
|
||||
if (
|
||||
confirm(
|
||||
"Send this announcement to team? OR just cancel and create a new one to restate something."
|
||||
)
|
||||
) {
|
||||
const downloadUrl = await cloudStorageService.uploadMessageAudioFile(
|
||||
file
|
||||
);
|
||||
|
||||
console.log("file is stored: " + downloadUrl);
|
||||
|
||||
// create announcement object and send to firestore
|
||||
const newAnnounce = new Announcement(
|
||||
downloadUrl,
|
||||
team.id,
|
||||
currUser.uid
|
||||
);
|
||||
|
||||
await sendService.sendAnnouncement(newAnnounce);
|
||||
|
||||
toast.success("clip sent");
|
||||
} else {
|
||||
toast.error("cancelled announcement");
|
||||
|
||||
return;
|
||||
}
|
||||
}, 2000);
|
||||
} catch (error) {
|
||||
toast.error("problem in sending");
|
||||
}
|
||||
}
|
||||
|
||||
const keyMap: KeyMap = {
|
||||
RECORD_ANNOUNCEMENT: "a",
|
||||
STOP_RECORDING_ANNOUNCEMENT: {
|
||||
name: "Stop recording",
|
||||
sequence: "a",
|
||||
action: "keyup",
|
||||
},
|
||||
};
|
||||
const handlers = {
|
||||
RECORD_ANNOUNCEMENT: startAnnouncement,
|
||||
STOP_RECORDING_ANNOUNCEMENT: sendAnnouncement,
|
||||
};
|
||||
|
||||
return (
|
||||
<KeyboardContext.Provider value={value}>
|
||||
<GlobalHotKeys keyMap={keyMap} handlers={handlers} />
|
||||
|
||||
{children}
|
||||
|
||||
{/* player for audio messages */}
|
||||
{playerSrc && (
|
||||
<AudioPlayer
|
||||
autoPlay
|
||||
src={playerSrc}
|
||||
onPlay={(e) => console.log("onPlay")}
|
||||
showSkipControls={true}
|
||||
onEnded={onEndedPlaying}
|
||||
className="w-screen flex flex-row"
|
||||
/>
|
||||
)}
|
||||
</KeyboardContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
export function useKeyboardContext() {
|
||||
return useContext(KeyboardContext);
|
||||
}
|
||||
@@ -0,0 +1,333 @@
|
||||
import {
|
||||
collection,
|
||||
doc,
|
||||
getFirestore,
|
||||
onSnapshot,
|
||||
orderBy,
|
||||
query,
|
||||
Unsubscribe,
|
||||
where,
|
||||
} from "firebase/firestore";
|
||||
import { useRouter } from "next/router";
|
||||
import React, { useContext, useEffect, useState } from "react";
|
||||
import toast from "react-hot-toast";
|
||||
import Loading from "../components/Loading";
|
||||
import { compareStatus } from "../helpers/userHelper";
|
||||
import { Message } from "../models/message";
|
||||
import Room from "../models/room";
|
||||
import { Team } from "../models/team";
|
||||
import { TeamMember, TeamMemberStatus } from "../models/teamMember";
|
||||
import { User } from "../models/user";
|
||||
import { Collections } from "../services/collections";
|
||||
import TeamService from "../services/teamService";
|
||||
import UserService from "../services/userService";
|
||||
import { useAuth } from "./authContext";
|
||||
|
||||
interface TeamDashboardContextInterface {
|
||||
team: Team;
|
||||
teamMembers: TeamMember[];
|
||||
userTeamMember: TeamMember;
|
||||
|
||||
user: User; // REALTIME - 1
|
||||
|
||||
messagesByTeamMate: Map<string, Message[]>; // REALTIME: string of teammate userid and array of messages - 1
|
||||
allMessages: Message[]; // REALTIME: 1
|
||||
|
||||
teamUsers: User[]; // REALTIME teammates - n team members => n listeners
|
||||
teamUsersMap: {}; // map for easier getting teammate data
|
||||
}
|
||||
|
||||
const TeamDashboardContext =
|
||||
React.createContext<TeamDashboardContextInterface | null>(null);
|
||||
|
||||
// date of yesterday to check if messages are after yesterday
|
||||
const yesterday = new Date();
|
||||
yesterday.setDate(yesterday.getDate() - 1);
|
||||
|
||||
const teamService = new TeamService();
|
||||
const userService = new UserService();
|
||||
const db = getFirestore();
|
||||
|
||||
export function TeamDashboardContextProvider({ children }) {
|
||||
const { currUser } = useAuth();
|
||||
|
||||
const router = useRouter();
|
||||
|
||||
const [loading, setLoading] = useState<Boolean>(true);
|
||||
|
||||
const { teamid } = router.query;
|
||||
|
||||
// this context can only be used if it's a specific route: '/teams'
|
||||
if (!teamid) {
|
||||
router.push("/teams");
|
||||
}
|
||||
|
||||
// update this "global" object to send to children
|
||||
const [value, setValue] = useState<TeamDashboardContextInterface>(
|
||||
{} as TeamDashboardContextInterface
|
||||
);
|
||||
|
||||
// all main data
|
||||
useEffect(() => {
|
||||
var userListener: Unsubscribe;
|
||||
var unsubs: Unsubscribe[] = [] as Unsubscribe[];
|
||||
|
||||
(async function () {
|
||||
try {
|
||||
// SECTION: authentication
|
||||
if (!currUser) {
|
||||
console.log(
|
||||
"not authenticated...routing from dashboard to teams home"
|
||||
);
|
||||
router.push("/teams/login");
|
||||
return;
|
||||
}
|
||||
|
||||
// SECTION: get user details : realtime
|
||||
const returnedUser = await userService.getUser(currUser.uid);
|
||||
|
||||
const docRef = doc(db, Collections.users, currUser.uid);
|
||||
|
||||
userListener = onSnapshot(docRef, (doc) => {
|
||||
const updatedUser = doc.data() as User;
|
||||
|
||||
setValue((prevValue) => ({ ...prevValue, user: updatedUser }));
|
||||
});
|
||||
|
||||
setValue((prevValue) => ({ ...prevValue, user: returnedUser }));
|
||||
|
||||
// SECTION: getting team data and team member information
|
||||
if (typeof teamid !== "string") {
|
||||
console.log("not a proper query with teamid");
|
||||
router.push("/teams");
|
||||
return;
|
||||
}
|
||||
|
||||
// check if the team is a valid team and that user is in it
|
||||
const returnedTeam = await teamService.getTeam(teamid);
|
||||
if (!returnedTeam) {
|
||||
console.log("team doesnt exist");
|
||||
router.push("/teams");
|
||||
return;
|
||||
}
|
||||
setValue((prevValue) => ({ ...prevValue, team: returnedTeam }));
|
||||
|
||||
const returnedTeamMember = await teamService.getTeamMemberByUserId(
|
||||
teamid,
|
||||
currUser.uid
|
||||
);
|
||||
|
||||
// if you are not a member of this team, then get out of here
|
||||
// but have to check through email invite and userid
|
||||
if (
|
||||
returnedTeamMember &&
|
||||
returnedTeamMember.status == TeamMemberStatus.deleted
|
||||
) {
|
||||
console.log("user was deleted from team");
|
||||
router.push("/teams");
|
||||
return;
|
||||
}
|
||||
|
||||
// if there is no team member through user id,
|
||||
// give him last chance and see if he was invited
|
||||
if (!returnedTeamMember) {
|
||||
const invitedTeamMember =
|
||||
await teamService.getTeamMemberByEmailInvite(
|
||||
teamid,
|
||||
currUser.email
|
||||
);
|
||||
|
||||
if (!invitedTeamMember) {
|
||||
console.log("not invited to team either");
|
||||
router.push("/teams");
|
||||
return;
|
||||
}
|
||||
|
||||
// if I am new to the team but I was invited, and this is my first time, then activate me into the team
|
||||
// and proceed with showing the dashboard stuff
|
||||
|
||||
invitedTeamMember.status = TeamMemberStatus.activated;
|
||||
invitedTeamMember.userId = currUser.uid;
|
||||
await teamService.updateTeamMember(invitedTeamMember);
|
||||
|
||||
setValue((prevValue) => ({
|
||||
...prevValue,
|
||||
userTeamMember: invitedTeamMember,
|
||||
}));
|
||||
} else {
|
||||
setValue((prevValue) => ({
|
||||
...prevValue,
|
||||
userTeamMember: returnedTeamMember,
|
||||
}));
|
||||
}
|
||||
|
||||
// SECTION: get all teammembers for team, will have listeners in other subcomponents
|
||||
var teamMembers: TeamMember[] =
|
||||
await teamService.getTeamMembersByTeamId(returnedTeam.id);
|
||||
|
||||
teamMembers = teamMembers.filter(
|
||||
(element) => element.userId != currUser.uid
|
||||
);
|
||||
|
||||
// listeners for all teammates' status
|
||||
if (teamMembers) {
|
||||
teamMembers.map((tmember) => {
|
||||
if (tmember.status == TeamMemberStatus.activated) {
|
||||
const docRef = doc(db, Collections.users, tmember.userId);
|
||||
|
||||
const unsub = onSnapshot(docRef, (doc) => {
|
||||
const updatedteamMateUser = doc.data() as User;
|
||||
|
||||
// update map of team member users
|
||||
setTeamUsersMap((prevMap) => ({
|
||||
...prevMap,
|
||||
[updatedteamMateUser.id]: updatedteamMateUser,
|
||||
}));
|
||||
|
||||
// update array of team member users
|
||||
setTeamUsers((prevTeamUsers) => {
|
||||
const newTeamUsers = prevTeamUsers.filter(
|
||||
(tm) => tm.id != updatedteamMateUser.id
|
||||
);
|
||||
newTeamUsers.push(updatedteamMateUser);
|
||||
|
||||
// order users by status
|
||||
setTeamUsers(newTeamUsers.sort(compareStatus));
|
||||
|
||||
return newTeamUsers;
|
||||
});
|
||||
});
|
||||
|
||||
unsubs.push(unsub);
|
||||
}
|
||||
|
||||
return;
|
||||
});
|
||||
}
|
||||
|
||||
setValue((prevValue) => ({ ...prevValue, teamMembers }));
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
router.push("/");
|
||||
}
|
||||
|
||||
setLoading(false);
|
||||
})();
|
||||
|
||||
return () => {
|
||||
if (userListener) {
|
||||
userListener();
|
||||
}
|
||||
|
||||
unsubs.forEach((unsub) => {
|
||||
unsub();
|
||||
});
|
||||
};
|
||||
}, []);
|
||||
|
||||
const [teamUsers, setTeamUsers] = useState<User[]>([]);
|
||||
const [teamUsersMap, setTeamUsersMap] = useState<{}>({});
|
||||
|
||||
const [allMessages, setAllMessages] = useState<Message[]>([]);
|
||||
const [messagesByTeamMate, setMessagesByTeamMate] = useState<
|
||||
Map<string, Message[]>
|
||||
>(new Map());
|
||||
|
||||
// SECTION: REALTIME listener for all incoming messages
|
||||
useEffect(() => {
|
||||
// todo for new messages, change document.title
|
||||
|
||||
if (!currUser) {
|
||||
console.log("not authenticated...routing from dashboard to teams home");
|
||||
router.push("/teams/login");
|
||||
return;
|
||||
}
|
||||
|
||||
/**
|
||||
* QUERY:
|
||||
* - last 24 hours only
|
||||
* - any message that I have sent or received: relevant messages
|
||||
* - order by: date desc
|
||||
*/
|
||||
const q = query(
|
||||
collection(db, Collections.audioMessages),
|
||||
where("senderReceiver", "array-contains", currUser.uid),
|
||||
where("createdDate", ">", yesterday),
|
||||
orderBy("createdDate", "asc")
|
||||
);
|
||||
|
||||
// return unsubscribe
|
||||
return onSnapshot(q, (snapshot) => {
|
||||
snapshot.docChanges().forEach((change) => {
|
||||
// no need to process results again, just append to arrays instead
|
||||
// messages won't be deleted or updated really
|
||||
if (change.type === "added") {
|
||||
let newMessage = change.doc.data() as Message;
|
||||
|
||||
// update all messages array
|
||||
setAllMessages((prevMessages) => [newMessage, ...prevMessages]);
|
||||
|
||||
// update map of teammate to relevant messages
|
||||
setMessagesByTeamMate((prevMap) => {
|
||||
// if the map contains the teammate userid already, then cool, just unshift to that array
|
||||
var newMap: Map<string, Message[]> = new Map(
|
||||
prevMap.set("dummy", [] as Message[])
|
||||
);
|
||||
if (prevMap.has(newMessage.receiverUserId)) {
|
||||
newMap.set(newMessage.receiverUserId, [
|
||||
newMessage,
|
||||
...prevMap.get(newMessage.receiverUserId),
|
||||
]);
|
||||
} // if this is the first relevant message linked to this receiver,
|
||||
//then create a new array
|
||||
else {
|
||||
newMap.set(newMessage.receiverUserId, [newMessage]);
|
||||
}
|
||||
|
||||
// todo: if I am the receiver, still want to put it in the right conversation
|
||||
if (newMessage.receiverUserId == currUser.uid) {
|
||||
if (prevMap.has(newMessage.senderUserId)) {
|
||||
newMap.set(newMessage.senderUserId, [
|
||||
newMessage,
|
||||
...prevMap.get(newMessage.senderUserId),
|
||||
]);
|
||||
} else {
|
||||
newMap.set(newMessage.senderUserId, [newMessage]);
|
||||
}
|
||||
}
|
||||
|
||||
return newMap;
|
||||
});
|
||||
}
|
||||
if (change.type === "modified") {
|
||||
console.log("Modified message: ", change.doc.data());
|
||||
}
|
||||
if (change.type === "removed") {
|
||||
console.log("Removed message: ", change.doc.data());
|
||||
}
|
||||
});
|
||||
});
|
||||
}, []);
|
||||
|
||||
if (loading) {
|
||||
return <Loading />;
|
||||
}
|
||||
|
||||
const transformedValue = {
|
||||
...value,
|
||||
allMessages,
|
||||
messagesByTeamMate,
|
||||
teamUsers,
|
||||
teamUsersMap,
|
||||
};
|
||||
|
||||
return (
|
||||
<TeamDashboardContext.Provider value={transformedValue}>
|
||||
{children}
|
||||
</TeamDashboardContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
export function useTeamDashboardContext() {
|
||||
return useContext(TeamDashboardContext);
|
||||
}
|
||||
@@ -0,0 +1,127 @@
|
||||
export enum KeyCode {
|
||||
Backspace = 8,
|
||||
Tab = 9,
|
||||
Enter = 13,
|
||||
Shift = 16,
|
||||
Ctrl = 17,
|
||||
Alt = 18,
|
||||
PauseBreak = 19,
|
||||
CapsLock = 20,
|
||||
Escape = 27,
|
||||
Space = 32,
|
||||
PageUp = 33,
|
||||
PageDown = 34,
|
||||
End = 35,
|
||||
Home = 36,
|
||||
|
||||
LeftArrow = 37,
|
||||
UpArrow = 38,
|
||||
RightArrow = 39,
|
||||
DownArrow = 40,
|
||||
|
||||
Insert = 45,
|
||||
Delete = 46,
|
||||
|
||||
Zero = 48,
|
||||
ClosedParen = Zero,
|
||||
One = 49,
|
||||
ExclamationMark = One,
|
||||
Two = 50,
|
||||
AtSign = Two,
|
||||
Three = 51,
|
||||
PoundSign = Three,
|
||||
Hash = PoundSign,
|
||||
Four = 52,
|
||||
DollarSign = Four,
|
||||
Five = 53,
|
||||
PercentSign = Five,
|
||||
Six = 54,
|
||||
Caret = Six,
|
||||
Hat = Caret,
|
||||
Seven = 55,
|
||||
Ampersand = Seven,
|
||||
Eight = 56,
|
||||
Star = Eight,
|
||||
Asterik = Star,
|
||||
Nine = 57,
|
||||
OpenParen = Nine,
|
||||
|
||||
A = 65,
|
||||
B = 66,
|
||||
C = 67,
|
||||
D = 68,
|
||||
E = 69,
|
||||
F = 70,
|
||||
G = 71,
|
||||
H = 72,
|
||||
I = 73,
|
||||
J = 74,
|
||||
K = 75,
|
||||
L = 76,
|
||||
M = 77,
|
||||
N = 78,
|
||||
O = 79,
|
||||
P = 80,
|
||||
Q = 81,
|
||||
R = 82,
|
||||
S = 83,
|
||||
T = 84,
|
||||
U = 85,
|
||||
V = 86,
|
||||
W = 87,
|
||||
X = 88,
|
||||
Y = 89,
|
||||
Z = 90,
|
||||
|
||||
LeftWindowKey = 91,
|
||||
RightWindowKey = 92,
|
||||
SelectKey = 93,
|
||||
|
||||
Numpad0 = 96,
|
||||
Numpad1 = 97,
|
||||
Numpad2 = 98,
|
||||
Numpad3 = 99,
|
||||
Numpad4 = 100,
|
||||
Numpad5 = 101,
|
||||
Numpad6 = 102,
|
||||
Numpad7 = 103,
|
||||
Numpad8 = 104,
|
||||
Numpad9 = 105,
|
||||
|
||||
Multiply = 106,
|
||||
Add = 107,
|
||||
Subtract = 109,
|
||||
DecimalPoint = 110,
|
||||
Divide = 111,
|
||||
|
||||
F1 = 112,
|
||||
F2 = 113,
|
||||
F3 = 114,
|
||||
F4 = 115,
|
||||
F5 = 116,
|
||||
F6 = 117,
|
||||
F7 = 118,
|
||||
F8 = 119,
|
||||
F9 = 120,
|
||||
F10 = 121,
|
||||
F11 = 122,
|
||||
F12 = 123,
|
||||
|
||||
NumLock = 144,
|
||||
ScrollLock = 145,
|
||||
|
||||
SemiColon = 186,
|
||||
Equals = 187,
|
||||
Comma = 188,
|
||||
Dash = 189,
|
||||
Period = 190,
|
||||
UnderScore = Dash,
|
||||
PlusSign = Equals,
|
||||
ForwardSlash = 191,
|
||||
Tilde = 192,
|
||||
GraveAccent = Tilde,
|
||||
|
||||
OpenBracket = 219,
|
||||
ClosedBracket = 221,
|
||||
Quote = 222
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
export enum CookieType {
|
||||
TEAM_SHORTCUTS_ONBOARDING = "TEAM_SHORTCUTS_ONBOARDING",
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
import moment from "moment";
|
||||
|
||||
export function generateGreetings(): string {
|
||||
const hour = moment().hour();
|
||||
|
||||
if (hour > 16) {
|
||||
return "Good evening";
|
||||
}
|
||||
|
||||
if (hour > 11) {
|
||||
return "Good afternoon";
|
||||
}
|
||||
|
||||
return "Good morning";
|
||||
}
|
||||
|
||||
export function getTime(date?: Date) {
|
||||
return date != null ? date.getTime() : 0;
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
export default function isValidHttpUrl(potentialUrl: string) {
|
||||
let url;
|
||||
|
||||
try {
|
||||
url = new URL(potentialUrl);
|
||||
} catch (_) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return url.protocol === "http:" || url.protocol === "https:";
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
import { User, UserStatus } from "../models/user";
|
||||
|
||||
function getStatusValue(status: UserStatus) {
|
||||
switch (status) {
|
||||
case UserStatus.online:
|
||||
return 10;
|
||||
case UserStatus.busy:
|
||||
return 5;
|
||||
case UserStatus.offline:
|
||||
return 0;
|
||||
default:
|
||||
return -5;
|
||||
}
|
||||
}
|
||||
|
||||
export function compareStatus(usera: User, userb: User) {
|
||||
return getStatusValue(userb.userStatus) - getStatusValue(usera.userStatus);
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
import { Timestamp } from "firebase/firestore";
|
||||
|
||||
export default class Announcement {
|
||||
id: string;
|
||||
teamId: string;
|
||||
audioDataUrl: string; // link to cloud storage file
|
||||
|
||||
state: AnnouncementState = AnnouncementState.active;
|
||||
|
||||
createdByUserId: string;
|
||||
createdDate: Timestamp;
|
||||
lastUpdatedDate: Timestamp;
|
||||
|
||||
constructor(_audioUrl: string, _teamId: string, _createdByUserId: string) {
|
||||
this.audioDataUrl = _audioUrl;
|
||||
this.createdByUserId = _createdByUserId;
|
||||
this.teamId = _teamId;
|
||||
}
|
||||
}
|
||||
|
||||
export enum AnnouncementState {
|
||||
active = "active",
|
||||
resolved = "resolved",
|
||||
deleted = "deleted",
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
export default interface IFirestoreSerializable {
|
||||
id: string;
|
||||
|
||||
serialize: () => {};
|
||||
|
||||
// deserialize: (firestoreData: {}) => {};
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
import { Timestamp } from "firebase/firestore";
|
||||
|
||||
export default class Link {
|
||||
id: string;
|
||||
name: string;
|
||||
description: string;
|
||||
link: string; //url for file
|
||||
|
||||
state: LinkState = LinkState.active;
|
||||
type: LinkType;
|
||||
|
||||
teamId: string;
|
||||
// if it's not a teamAttachment, then have a list of members who it's for
|
||||
recipients: string[]; // userIds
|
||||
|
||||
createdByUserId: string;
|
||||
createdDate: Timestamp;
|
||||
|
||||
constructor(
|
||||
_name: string,
|
||||
_description: string,
|
||||
_link: string,
|
||||
_teamId: string,
|
||||
recipientsArr: string[],
|
||||
_createdByUserId: string
|
||||
) {
|
||||
this.name = _name;
|
||||
this.description = _description;
|
||||
this.link = _link;
|
||||
this.teamId = _teamId;
|
||||
this.recipients = recipientsArr;
|
||||
this.createdByUserId = _createdByUserId;
|
||||
|
||||
if (!recipientsArr || recipientsArr?.length == 0) {
|
||||
this.recipients = null;
|
||||
} else {
|
||||
this.recipients = recipientsArr;
|
||||
}
|
||||
|
||||
this.type = Link.getLinkType(_link);
|
||||
}
|
||||
|
||||
static getLinkType(url: string): LinkType {
|
||||
if (url.includes(LinkType.github)) {
|
||||
return LinkType.github;
|
||||
} else if (url.includes(LinkType.atlassian)) {
|
||||
return LinkType.atlassian;
|
||||
} else if (
|
||||
url.includes(LinkType.googleDrive) ||
|
||||
url.includes("docs.google")
|
||||
) {
|
||||
return LinkType.googleDrive;
|
||||
} else if (
|
||||
url.includes(".png") ||
|
||||
url.includes(".jpg") ||
|
||||
url.includes(".svg") ||
|
||||
url.includes(".gif") ||
|
||||
url.includes(LinkType.pastePics)
|
||||
) {
|
||||
return LinkType.image;
|
||||
} else if (url.includes(LinkType.pdf)) {
|
||||
return LinkType.pdf;
|
||||
} else if (url.includes(LinkType.codePile)) {
|
||||
return LinkType.codePile;
|
||||
} else {
|
||||
return LinkType.default;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export enum LinkState {
|
||||
active = "active",
|
||||
archived = "archived",
|
||||
deleted = "deleted",
|
||||
}
|
||||
|
||||
export enum LinkType {
|
||||
default = "default",
|
||||
github = "github",
|
||||
atlassian = "atlassian",
|
||||
googleDrive = "drive.google",
|
||||
onedrive = "onedrive",
|
||||
image = "image",
|
||||
pdf = "pdf",
|
||||
codePile = "codepile",
|
||||
pastePics = "paste.pics",
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
import { Timestamp } from "firebase/firestore";
|
||||
|
||||
export class Message {
|
||||
id: string;
|
||||
audioDataUrl: string;
|
||||
|
||||
senderUserId: string;
|
||||
receiverUserId: string;
|
||||
|
||||
senderReceiver: string[]; // composite to make querying easier in the future
|
||||
|
||||
createdDate: Timestamp;
|
||||
|
||||
// firstListenDate: Timestamp;
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
import { Timestamp } from "firebase/firestore";
|
||||
import { v4 as uuidv4 } from "uuid";
|
||||
|
||||
export default class OfficeRoom {
|
||||
id: string = uuidv4();
|
||||
|
||||
teamId: string;
|
||||
|
||||
name: string; // entrance, kitchen, etc.
|
||||
|
||||
createdDate: Timestamp;
|
||||
createdByUserId: string;
|
||||
|
||||
lastUpdatedDate: Timestamp;
|
||||
|
||||
members: string[] = []; // id's of users in the office room
|
||||
|
||||
state: OfficeRoomState;
|
||||
|
||||
constructor(
|
||||
_name: string,
|
||||
_teamId: string,
|
||||
_createdBy: string,
|
||||
_state: OfficeRoomState = OfficeRoomState.idle
|
||||
) {
|
||||
this.name = _name;
|
||||
this.teamId = _teamId;
|
||||
this.createdByUserId = _createdBy;
|
||||
this.state = _state;
|
||||
}
|
||||
}
|
||||
|
||||
export enum OfficeRoomState {
|
||||
active = "active",
|
||||
idle = "idle",
|
||||
archived = "archived",
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
import { Timestamp } from "firebase/firestore";
|
||||
|
||||
export default class Room {
|
||||
id: string;
|
||||
|
||||
name: string;
|
||||
description: string;
|
||||
|
||||
link: string; // google meet link for now
|
||||
|
||||
members: string[] = []; //userIds of "mandatory"/invited people including the person who created it
|
||||
|
||||
membersInRoom: string[] = [];
|
||||
|
||||
attachments: string[] = []; // the links themselves (NOT Ids)...there will be duplicate entries in the attachments table which will be created
|
||||
|
||||
type: RoomType;
|
||||
status: RoomStatus = RoomStatus.empty;
|
||||
|
||||
approximateDateTime: string; // vaguely say when the meeting should be...give user pointers
|
||||
scheduledDateTime: Timestamp;
|
||||
|
||||
// scheduledJsDateTime(): Date {
|
||||
// return this.scheduledDateTime.toDate();
|
||||
// }
|
||||
|
||||
createdDate: Timestamp;
|
||||
|
||||
// createdJsDate(): Date {
|
||||
// return this.createdDate.toDate();
|
||||
// }
|
||||
|
||||
createdByUserId: string;
|
||||
|
||||
teamId: string;
|
||||
|
||||
lastUpdatedDate: Timestamp;
|
||||
}
|
||||
|
||||
export enum RoomType {
|
||||
now = "now",
|
||||
scheduled = "scheduled", // one time sort of standard meeting
|
||||
recurring = "recurring", //daily standup
|
||||
}
|
||||
|
||||
export enum RoomStatus {
|
||||
live = "live",
|
||||
empty = "empty",
|
||||
archived = "archived", // user marks it over
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
import { Timestamp } from "firebase/firestore";
|
||||
|
||||
export class Team {
|
||||
id: string;
|
||||
name: string;
|
||||
|
||||
status: TeamStatus;
|
||||
|
||||
allowedUserCount: number = 2;
|
||||
// subscriptionPlan: TeamSubscriptionPlan = TeamSubscriptionPlan.basic
|
||||
|
||||
companySite: string;
|
||||
|
||||
createdByUserId: string;
|
||||
createdDate: Timestamp;
|
||||
|
||||
lastUpdatedDate: Timestamp;
|
||||
}
|
||||
|
||||
export enum TeamStatus {
|
||||
created = "created",
|
||||
deactivated = "deactivated",
|
||||
deleted = "deleted",
|
||||
}
|
||||
|
||||
export enum TeamSubscriptionPlan {
|
||||
free = "free",
|
||||
basic = "basic",
|
||||
pro = "pro",
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
import { Timestamp } from "firebase/firestore";
|
||||
|
||||
export class TeamMember {
|
||||
id: string;
|
||||
userId: string;
|
||||
teamId: string;
|
||||
|
||||
inviteEmailAddress: string;
|
||||
invitedByUserId: string;
|
||||
|
||||
role: TeamMemberRole
|
||||
status: TeamMemberStatus
|
||||
|
||||
createdDate: Timestamp
|
||||
lastUpdatedDate: Timestamp
|
||||
}
|
||||
|
||||
export enum TeamMemberRole {
|
||||
admin = "admin"
|
||||
}
|
||||
|
||||
export enum TeamMemberStatus {
|
||||
invited = "invited",
|
||||
activated = "activated",
|
||||
deleted = "deleted"
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
import {
|
||||
documentId,
|
||||
Firestore,
|
||||
serverTimestamp,
|
||||
Timestamp,
|
||||
} from "firebase/firestore";
|
||||
import IFirestoreSerializable from "./firestoreSerializable";
|
||||
|
||||
export class User {
|
||||
id: string;
|
||||
emailAddress: string;
|
||||
nickName: string;
|
||||
firstName: string;
|
||||
lastName: string;
|
||||
avatarUrl: string;
|
||||
userStatus: UserStatus;
|
||||
|
||||
/**designer? dev? */
|
||||
teamRole: string;
|
||||
|
||||
createdDate: Timestamp;
|
||||
lastUpdatedDate: Timestamp;
|
||||
|
||||
// serialize() {
|
||||
// return {
|
||||
// createdDate: Timestamp.fromDate(this.createdDate),
|
||||
// lastUpdatedDate: Timestamp.fromDate(this.lastUpdatedDate)
|
||||
// }
|
||||
// }
|
||||
|
||||
// deserialize(firestoreData: {}) {
|
||||
// this.lastUpdatedDate = firestoreData.lastUpdatedDate.toDate()
|
||||
// }
|
||||
}
|
||||
|
||||
export enum UserStatus {
|
||||
online = "online",
|
||||
offline = "offline",
|
||||
busy = "busy",
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
/// <reference types="next" />
|
||||
/// <reference types="next/image-types/global" />
|
||||
|
||||
// NOTE: This file should not be edited
|
||||
// see https://nextjs.org/docs/basic-features/typescript for more information.
|
||||
@@ -0,0 +1,3 @@
|
||||
module.exports = {
|
||||
reactStrictMode: true,
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
const serviceAccount = {
|
||||
"type": "service_account",
|
||||
"project_id": "nirvana-for-business",
|
||||
"private_key_id": "8720d0ab82c42545bb151e3e394d03d81acdb208",
|
||||
"private_key": "-----BEGIN PRIVATE KEY-----\nMIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQC9aX62Q555qu4D\nUCmyl3NZda5XDJt5Y+ogpE+ModB8poGiiywX2P/zzR6FhmuCVSKseI4ehHsZn2T8\nBAokBJy6ovsg5UhSQ0Q6vbeJ7mVvyEbyofAElz651CTHk2Tx5iQZbE27vgFRD9zn\nuMyNuCaVpc4D1It1zDxwNNk0y5Na5Pitke21h4NznAh5KTfmbc6L0Sf+E1NvhybO\n+DHSgBBTHOiXAgxWMlCH6/XFGo0kEQmykMVzJKaTQPdWVUrNrw8jW5WVb31PldGn\nCqONy7owo/Ik4f29+9hpIfjf6Ti7y+v3lH+sNrfojDvbv/Xw+Qyy7qBUg9jW4RG3\n9HjG0qNPAgMBAAECggEACKGmkxK4xY2bBA6in89UA9cfhzr9WMZLVIp4T43OLOf/\nj1MtQrbyTv9hbS9HdeyARIDH5S8fVvcTCIL8nVCSfdTQgdrA9XK4SL79fq0c9T+Y\nsBTeFa94vcojlO6sEnPs4DW1wyDw+WsyI2Mh+zFRnM8t+LG2Wztf47Mc1NGUGPey\n+La5EW8ZXTFVFYBe+9TlKrvxT506NRdursg+bO6GMbzpHgWHuFj/fx6ij+t2a3I8\nxWcV6uJnoYEHteeQNRAh2pITqaEAQ2Z2iglg3eny/IKhN6I/5ryBBvYknwR1sY/2\n7RVVqxRNav/cmxTc9j6sVlHudil+j/0hmqZyFfysrQKBgQD7S5sdMaL3I4E6KB9A\npw4bHAuQM6zoFkFHIl8oBTJOKU4LESIAiJk6rP7DwJnrNDmtDhTYkxXfCPIU4KNs\n7bW2JOQOiAR/stPAPZnvrAhaBqM3kw4yK3WwVhIDoQq74+PP3XVeLhrOx+ncp/rS\naJFNzLTfTHPR/KtzxFL1rIgpzQKBgQDA9Uxsd7TR5EvQQm18puPWRTPzziAlHhJr\nNNrEWr6YyAlWkGfu1y4JHaXGm12sRhqh+iwt2lRsxYa2Ue0sucHPDfzt5c075UzE\nQ8YWTiC7dVcHQ/9LbG4W+fmUBsJPlPJPs2hmKTTw2/MGRgfpBTNdRBCgqh5HnVUR\nZCIHEq61iwKBgELZWAgqHioUXdo0tiuixRPdIA5aUNLkqLTdpealiz5TqpkzE5KE\nxA5h2twELm3tVLJ+nnkGl7jfTlbTc2YUzlQH+EsNT/UQg1gOixbA8u/P+DdefVZH\nTWV9YAlrG8nj08SGKyTEqwj7EXFLlmPGwXCq1irY0a64wntnbJJCNW65AoGATmNY\nqLf4vUwBgJl74SfrZyC1+lXCuVF+Kg9O0HYM+ARLxt7jWiQQj6N7tTNo2pOgPi1j\nxTztD0MvduHReFn+Yh9MoVi6B5sflJvz3RvgMEd7E3iMPhfOoYbr3TbhuXLI/Zfk\n0Zvr5e10zRemHwP92eXM23hA1NcVt/r11+m/EAECgYEA+AAxyEaaTv63j2PImwvO\nKsqtzpHsf97PbsVnj6A9UAGXm80vU3678X8OdJAw0zFlub7LqcL7ses9MXHgxJmb\n7WYWWJSt6phDXgrQeBx9imLZpPywQKCg2Xsstum9veCyfGB6UHfvDWPK8vIF15lz\nV0IUN4EnE6YsG9dbeYEGX+0=\n-----END PRIVATE KEY-----\n",
|
||||
"client_email": "firebase-adminsdk-e05xh@nirvana-for-business.iam.gserviceaccount.com",
|
||||
"client_id": "109842603410185922994",
|
||||
"auth_uri": "https://accounts.google.com/o/oauth2/auth",
|
||||
"token_uri": "https://oauth2.googleapis.com/token",
|
||||
"auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs",
|
||||
"client_x509_cert_url": "https://www.googleapis.com/robot/v1/metadata/x509/firebase-adminsdk-e05xh%40nirvana-for-business.iam.gserviceaccount.com"
|
||||
}
|
||||
|
||||
export default serviceAccount
|
||||
@@ -0,0 +1,36 @@
|
||||
{
|
||||
"name": "nirvana",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "next dev",
|
||||
"build": "next build",
|
||||
"start": "next start",
|
||||
"lint": "next lint"
|
||||
},
|
||||
"dependencies": {
|
||||
"agora-rtc-sdk-ng": "^4.8.1",
|
||||
"antd": "^4.18.3",
|
||||
"firebase": "^9.6.2",
|
||||
"firebase-admin": "^10.0.1",
|
||||
"js-cookie": "^3.0.1",
|
||||
"mic-recorder-to-mp3": "^2.2.2",
|
||||
"moment": "^2.29.1",
|
||||
"next": "12.0.7",
|
||||
"react": "17.0.2",
|
||||
"react-dom": "17.0.2",
|
||||
"react-h5-audio-player": "^3.8.1",
|
||||
"react-hot-toast": "^2.2.0",
|
||||
"react-hotkeys": "^2.0.0",
|
||||
"react-icons": "^4.3.1",
|
||||
"react-moment": "^1.1.1",
|
||||
"uuid": "^8.3.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/react": "^17.0.38",
|
||||
"autoprefixer": "^10.4.2",
|
||||
"eslint": "8.4.0",
|
||||
"eslint-config-next": "12.0.7",
|
||||
"postcss": "^8.4.5",
|
||||
"tailwindcss": "^3.0.12"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
import "../styles/globals.css";
|
||||
import "antd/dist/antd.css";
|
||||
import Head from "next/head";
|
||||
import { AuthProvider } from "../contexts/authContext";
|
||||
import SiteLayout from "../components/Layouts/SiteLayout";
|
||||
import { Toaster } from "react-hot-toast";
|
||||
|
||||
function MyApp({ Component, pageProps }) {
|
||||
return (
|
||||
<SiteLayout>
|
||||
<AuthProvider>
|
||||
<Component {...pageProps} />
|
||||
</AuthProvider>
|
||||
|
||||
<Toaster position="top-center" />
|
||||
</SiteLayout>
|
||||
);
|
||||
}
|
||||
|
||||
export default MyApp;
|
||||
@@ -0,0 +1,25 @@
|
||||
import { NextApiRequest, NextApiResponse } from "next";
|
||||
// import { adminApp } from "../../../services/firebaseAdminService";
|
||||
import { getAuth } from "firebase/auth";
|
||||
import firebaseAdminAll, { credential } from "firebase-admin";
|
||||
|
||||
// todo do this here instead of cloud functions or just leave as is
|
||||
export default async function handler(
|
||||
req: NextApiRequest,
|
||||
res: NextApiResponse
|
||||
) {
|
||||
console.log(req.cookies);
|
||||
console.log(req.body);
|
||||
|
||||
if (!req.cookies.auth) {
|
||||
res.status(401).send({ reason: "not authorized" });
|
||||
return;
|
||||
}
|
||||
|
||||
// if (!verifiedToken) {
|
||||
// res.status(401).send({ reason: "auth token not valid" });
|
||||
// return;
|
||||
// }
|
||||
|
||||
res.status(200).json({ name: "John Doe" });
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
import fbAdm, { credential } from "firebase-admin";
|
||||
import firebaseAdmin, { App } from "firebase-admin/app";
|
||||
|
||||
import { NextApiRequest, NextApiResponse } from "next";
|
||||
|
||||
// import serviceAccount from "../../../nirvana-for-business-firebase-adminsdk";
|
||||
|
||||
// const adminApp = firebaseAdmin.initializeApp({
|
||||
// credential: credential.cert({
|
||||
// privateKey: serviceAccount.private_key,
|
||||
// clientEmail: serviceAccount.client_email,
|
||||
// projectId: serviceAccount.project_id,
|
||||
// }),
|
||||
// });
|
||||
|
||||
// export default async function(req: NextApiRequest, res: NextApiResponse) {
|
||||
// console.log('Validating token...');
|
||||
|
||||
// try {
|
||||
// const { token } = JSON.parse(req.headers.authorization || '{}');
|
||||
// if (!token) {
|
||||
// return res.status(403).send({
|
||||
// errorCode: 403,
|
||||
// message: 'Auth token missing.'
|
||||
// });
|
||||
// }
|
||||
|
||||
// const verifiedToken = fbAdm.auth().verifyIdToken(token)
|
||||
|
||||
// return res.status(200).send(verifiedToken);
|
||||
// } catch (err) {
|
||||
// return res.status(err.code).send({
|
||||
// errorCode: err.code,
|
||||
// message: err.message,
|
||||
// });
|
||||
// }
|
||||
// }
|
||||
|
||||
export {};
|
||||
@@ -0,0 +1,5 @@
|
||||
// Next.js API route support: https://nextjs.org/docs/api-routes/introduction
|
||||
|
||||
export default function handler(req, res) {
|
||||
res.status(200).json({ name: 'John Doe' })
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
import LangingPageLayout from "../components/Layouts/LandingPageLayout";
|
||||
import {
|
||||
FcAssistant,
|
||||
FcCollaboration,
|
||||
FcFilingCabinet,
|
||||
FcGoogle,
|
||||
FcLandscape,
|
||||
FcOrgUnit,
|
||||
FcVideoCall,
|
||||
} from "react-icons/fc";
|
||||
import LandingPageActionBar from "../components/demo/LandingPageActionBar";
|
||||
|
||||
export default function Features() {
|
||||
return (
|
||||
<LangingPageLayout>
|
||||
<span className="flex flex-col my-20 items-center text-center">
|
||||
<span>FEATURES</span>
|
||||
<span className="text-5xl font-bold">
|
||||
Your WFH <span className="text-teal-600">Hub</span>
|
||||
</span>
|
||||
<span className="text-gray-500 text-lg">
|
||||
Come to work with clarity of mind.
|
||||
</span>
|
||||
</span>
|
||||
|
||||
<span className="flex flex-row mb-2 flex-wrap gap-2 justify-center">
|
||||
<span className="flex flex-col items-start bg-white bg-opacity-40 p-10 max-w-sm rounded backdrop-blur-md">
|
||||
<FcAssistant className="text-5xl -ml-2" />
|
||||
<span className="text-teal-600">async</span>
|
||||
<span className="text-lg font-semibold">Voice Conversations.</span>
|
||||
|
||||
<span className="text-md text-gray-500">
|
||||
Listen to your teammate talk to you realtime if you are online.
|
||||
</span>
|
||||
</span>
|
||||
|
||||
<span className="flex flex-col items-start bg-white bg-opacity-40 p-10 max-w-sm rounded backdrop-blur-md">
|
||||
<FcCollaboration className="text-5xl" />
|
||||
<span className="text-teal-600">informal</span>
|
||||
<span className="text-lg font-semibold">Office Rooms.</span>
|
||||
|
||||
<span className="text-md text-gray-500">
|
||||
Nirvana voice-only rooms to collaborate and work with your closest
|
||||
teammates throughout the day.
|
||||
</span>
|
||||
</span>
|
||||
|
||||
<span className="flex flex-col items-start bg-white bg-opacity-40 p-10 max-w-sm rounded backdrop-blur-md">
|
||||
<FcVideoCall className="text-5xl" />
|
||||
<span className="text-teal-600">formal</span>
|
||||
<span className="text-lg font-semibold">Meeting Rooms.</span>
|
||||
|
||||
<span className="text-md text-gray-500">
|
||||
Spontaneous, scheduled, or recurring <FcGoogle className="inline" />{" "}
|
||||
Meet meetings. All conversations transparent for the team to see.
|
||||
</span>
|
||||
</span>
|
||||
|
||||
<span className="flex flex-col items-start bg-white bg-opacity-40 p-10 max-w-sm rounded backdrop-blur-md">
|
||||
<FcFilingCabinet className="text-5xl -ml-2" />
|
||||
<span className="text-teal-600">desk</span>
|
||||
<span className="text-lg font-semibold">Drawer.</span>
|
||||
|
||||
<span className="text-md text-gray-500">
|
||||
Personal and team drawer of links: Jira, Drive, Dropbox, Github,
|
||||
screenshots, code snippets.
|
||||
</span>
|
||||
</span>
|
||||
|
||||
<span className="flex flex-col items-start bg-white bg-opacity-40 p-10 max-w-sm rounded backdrop-blur-md">
|
||||
<FcOrgUnit className="text-5xl -ml-2" />
|
||||
<span className="text-teal-600">team</span>
|
||||
<span className="text-lg font-semibold">Announcements.</span>
|
||||
|
||||
<span className="text-md text-gray-500">
|
||||
Keep your team focused. Mention blockers, priorities, updates...
|
||||
</span>
|
||||
</span>
|
||||
|
||||
<span className="flex flex-col items-start bg-white bg-opacity-40 p-10 max-w-sm rounded backdrop-blur-md">
|
||||
<FcLandscape className="text-5xl" />
|
||||
<span className="text-teal-600">ephemeral</span>
|
||||
<span className="text-lg font-semibold">Inbox Zero.</span>
|
||||
|
||||
<span className="text-md text-gray-500">
|
||||
Links, conversations, rooms...They all disappear from your main view
|
||||
to keep you and your team focused.
|
||||
</span>
|
||||
</span>
|
||||
</span>
|
||||
|
||||
<LandingPageActionBar />
|
||||
</LangingPageLayout>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,399 @@
|
||||
import Head from "next/head";
|
||||
import {
|
||||
FaSlackHash,
|
||||
FaNewspaper,
|
||||
FaCalendarAlt,
|
||||
FaRocketchat,
|
||||
FaAngleRight,
|
||||
FaCheck,
|
||||
FaInfo,
|
||||
FaInfoCircle,
|
||||
FaClock,
|
||||
FaLink,
|
||||
FaPlay,
|
||||
} from "react-icons/fa";
|
||||
import { FcGoogle } from "react-icons/fc";
|
||||
import { BsThreeDots } from "react-icons/bs";
|
||||
import TeamVoiceLine from "../components/demo/TeamVoiceLine";
|
||||
import Announcements from "../components/demo/Announcements";
|
||||
import Rooms from "../components/demo/Rooms";
|
||||
import VoiceLineConceptDemo from "../components/demo/VoiceLineConceptDemo";
|
||||
|
||||
import MainLogo from "../components/MainLogo";
|
||||
import { Divider, Tooltip, Image as AntDImage } from "antd";
|
||||
import LangingPageLayout from "../components/Layouts/LandingPageLayout";
|
||||
import LandingPageActionBar from "../components/demo/LandingPageActionBar";
|
||||
import Link from "next/link";
|
||||
|
||||
export default function Home() {
|
||||
const handleGetDemo = () => {
|
||||
window.open("https://calendly.com/usenirvana/30min", "_blank");
|
||||
};
|
||||
|
||||
const getStartedButton = (
|
||||
<button
|
||||
onClick={handleGetDemo}
|
||||
className="rounded font-semibold bg-teal-600 p-2 text-white shadow-lg flex flex-row items-center space-x-2"
|
||||
>
|
||||
<span>Get Demo</span>
|
||||
<FaAngleRight />
|
||||
</button>
|
||||
);
|
||||
|
||||
return (
|
||||
<LangingPageLayout>
|
||||
{/* header text */}
|
||||
<div className="flex flex-col items-center backdrop-blur-lg p-10">
|
||||
<span className="flex flex-col items-center text-center text-3xl md:text-5xl font-bold ">
|
||||
<span>
|
||||
<span className="text-teal-800">Minimal </span>
|
||||
Communication Tool for
|
||||
</span>
|
||||
|
||||
<span className="">
|
||||
<span className="text-red-800">Messy, Distracted</span> Remote
|
||||
Teams.
|
||||
</span>
|
||||
</span>
|
||||
|
||||
<span className="my-5 text-lg md:text-xl text-center text-black font-semibold w-max">
|
||||
A <span className="text-teal-600">'less is more'</span>{" "}
|
||||
approach.
|
||||
<br></br>
|
||||
Skip{" "}
|
||||
<img
|
||||
src="/icons/zoom-logo.svg"
|
||||
className="w-[4rem] inline-block h-[2rem] px-1"
|
||||
/>{" "}
|
||||
scheduling, <br className="md:hidden"></br>
|
||||
<img
|
||||
src="/icons/slack-logo.svg"
|
||||
className="inline-block w-[5rem] h-[2rem] px-1"
|
||||
/>{" "}
|
||||
notifications, <br className="md:hidden"></br> and{" "}
|
||||
<img
|
||||
src="/icons/gmail-logo.svg"
|
||||
className="inline-block w-[2rem] h-[1rem]"
|
||||
/>{" "}
|
||||
threads,
|
||||
<br></br>
|
||||
and just talk to your team.
|
||||
<br></br>
|
||||
{/* Improve team focus, performance, and wellbeing. */}
|
||||
</span>
|
||||
|
||||
<span className="flex flex-row items-center space-x-5 my-5">
|
||||
<Link href="/features">Features</Link>
|
||||
|
||||
<button
|
||||
onClick={() => window.open("/teams/login", "_self")}
|
||||
className="rounded font-semibold bg-teal-600 p-2 text-white shadow-lg flex flex-row items-center justify-evenly space-x-2"
|
||||
>
|
||||
<span>Get Started</span>
|
||||
<FaAngleRight />
|
||||
</button>
|
||||
</span>
|
||||
|
||||
<span className="text-center text-xs text-gray-500">
|
||||
Try Nirvana for 2 people for 30 days. <br></br>No credit card
|
||||
required.
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* main above fold product image */}
|
||||
<span className="md:flex w-full justify-evenly hidden">
|
||||
<img
|
||||
onClick={() => window.open("/teams/demo", "_blank")}
|
||||
src="/screenshots/frame_safari_dark.png"
|
||||
className="flex-1 min-h-[20rem] min-w-[30rem]"
|
||||
/>
|
||||
</span>
|
||||
{/* image for mobile, just barely see it kind of thing */}
|
||||
<span className="md:hidden">
|
||||
<AntDImage width={800} src="/screenshots/frame_safari_dark.png" />
|
||||
</span>
|
||||
|
||||
{/* voice line section */}
|
||||
<span className="flex flex-row md:py-32 py-10 flex-wrap">
|
||||
<span className="mx-auto flex flex-col grow max-w-sm rounded-lg shadow-lg bg-gray-200 bg-opacity-40 backdrop-blur-xl p-10">
|
||||
<span className="bg-clip-text font-bold text-transparent bg-gradient-to-r from-pink-500 to-violet-500">
|
||||
VOICE ONLY
|
||||
</span>
|
||||
|
||||
<span className="text-left text-3xl text-gray-700 font-bold">
|
||||
Move 10x <span className="text-teal-600">Faster.</span>
|
||||
</span>
|
||||
<span className="text-sm text-gray-500">
|
||||
{"Resolve issues faster, and improve performance."}
|
||||
</span>
|
||||
|
||||
<span className="text-left text-lg text-gray-700"></span>
|
||||
|
||||
<Divider />
|
||||
|
||||
{/* checkmarks of value add */}
|
||||
<span className="flex flex-row items-center space-x-2">
|
||||
<FaCheck className="text-teal-500" />
|
||||
<span className="text-left text-lg text-gray-600">
|
||||
{"Asynchronous."}
|
||||
</span>
|
||||
<Tooltip
|
||||
title={
|
||||
"If you send a voice clip to someone, and they are online, they hear you instantly without changing tabs/windows."
|
||||
}
|
||||
>
|
||||
<FaInfoCircle className="text-lg text-teal-500 animate-bounce" />
|
||||
</Tooltip>
|
||||
</span>
|
||||
|
||||
<span className="flex flex-row items-center space-x-2">
|
||||
<FaCheck className="text-teal-500" />
|
||||
<span className="text-left text-lg text-gray-600">
|
||||
{"NO low-quality chatter."}
|
||||
</span>
|
||||
</span>
|
||||
|
||||
<span className="flex flex-row items-baseline space-x-2">
|
||||
<FaCheck className="text-teal-500 text-md shrink-0" />
|
||||
|
||||
<span className="flex flex-col">
|
||||
<span className="flex flex-row items-center space-x-2">
|
||||
<span className="text-left text-lg text-gray-600">
|
||||
{"Clear Communication."}
|
||||
</span>
|
||||
</span>
|
||||
<span className="text-sm text-gray-500">
|
||||
{/* {"Resolve issues faster, and improve performance."} */}
|
||||
</span>
|
||||
</span>
|
||||
</span>
|
||||
|
||||
<Divider />
|
||||
|
||||
<span className="text-left text-md text-gray-600">
|
||||
{
|
||||
"An experience as if your team was across the table. And it's still"
|
||||
}
|
||||
<span className="text-teal-600"> asynchronous.</span>
|
||||
</span>
|
||||
<br></br>
|
||||
<span className="text-left text-md text-gray-600">
|
||||
No more days of{" "}
|
||||
<span className="text-orange-500">back and forth texting</span> for
|
||||
complex, technical issues.
|
||||
</span>
|
||||
</span>
|
||||
|
||||
<span className="flex-row justify-end relative items-baseline flex-1 md:flex hidden">
|
||||
<VoiceLineConceptDemo />
|
||||
</span>
|
||||
</span>
|
||||
|
||||
{/* bond/team/collaborate section */}
|
||||
<span className="flex flex-row items-center md:py-20 flex-wrap">
|
||||
<span className="flex-1 flex flex-row flex-wrap backdrop-blur-md p-10 rounded">
|
||||
{/* illustration of collaborating */}
|
||||
<img
|
||||
src="/illustrations/undraw_team_collaboration_re_ow29.svg"
|
||||
className="min-h-[20rem] min-w-[20rem] shrink-0"
|
||||
/>
|
||||
</span>
|
||||
|
||||
{/* description text */}
|
||||
<span className="mx-auto text-left flex flex-col grow max-w-lg shadow-lg rounded-lg bg-gray-200 bg-opacity-40 backdrop-blur-xl p-10">
|
||||
<span className="bg-clip-text font-bold text-transparent bg-gradient-to-r from-pink-500 to-violet-500">
|
||||
TEAM
|
||||
</span>
|
||||
<span className="text-3xl text-gray-700 font-bold">
|
||||
Bond & Collaborate{" "}
|
||||
<span className="text-teal-600">Seamlessly.</span>
|
||||
</span>
|
||||
<span className="text-lg text-gray-700"></span>
|
||||
<Divider />
|
||||
{/* checkmarks of value add */}
|
||||
<span className="flex flex-row items-baseline space-x-2">
|
||||
<FaCheck className="text-teal-500 text-md shrink-0" />
|
||||
|
||||
<span className="flex flex-col">
|
||||
<span className="flex flex-row items-center space-x-2">
|
||||
<span className="text-left text-lg text-gray-600">
|
||||
{"Spontaneous Conversations."}
|
||||
</span>
|
||||
</span>
|
||||
<span className="text-sm text-gray-500">
|
||||
{"No more 'let's find a time to discuss...'"}
|
||||
</span>
|
||||
</span>
|
||||
</span>
|
||||
|
||||
<span className="flex flex-row items-baseline space-x-2">
|
||||
<FaCheck className="text-teal-500 text-md shrink-0" />
|
||||
|
||||
<span className="flex flex-col">
|
||||
<span className="flex flex-row items-center space-x-2">
|
||||
<span className="text-left text-lg text-gray-600">
|
||||
{"Cross Collaborate."}
|
||||
</span>
|
||||
</span>
|
||||
<span className="text-sm text-gray-500">
|
||||
{"Glance all of the conversations going on in your team."}
|
||||
</span>
|
||||
</span>
|
||||
</span>
|
||||
|
||||
<Divider />
|
||||
|
||||
<span className="text-md text-gray-600">
|
||||
Work in the{" "}
|
||||
<Tooltip
|
||||
title={
|
||||
"Nirvana voice-only rooms to hang out and code, create, ideate..."
|
||||
}
|
||||
>
|
||||
<span className="text-teal-600">
|
||||
'corner' office room
|
||||
</span>{" "}
|
||||
<FaInfoCircle className="text-sm text-teal-500 inline animate-bounce" />
|
||||
</Tooltip>{" "}
|
||||
with your closest teammates throughout the day.
|
||||
</span>
|
||||
<span className="text-md text-gray-600">
|
||||
Have lunch in the 'kitchen' with your team.
|
||||
</span>
|
||||
<span className="text-md text-gray-600">
|
||||
Connect in seconds, resolve matters in minutes.
|
||||
</span>
|
||||
|
||||
<br></br>
|
||||
<span className="text-center">or</span>
|
||||
<br></br>
|
||||
<span className="text-md text-gray-600">
|
||||
Create more formal meeting rooms (
|
||||
<FcGoogle className="inline" /> Meet).
|
||||
<span className="text-teal-600">
|
||||
{" "}
|
||||
Spontaneous, scheduled, or recurring.
|
||||
</span>{" "}
|
||||
All within Nirvana.
|
||||
</span>
|
||||
</span>
|
||||
</span>
|
||||
|
||||
{/* focus on today section */}
|
||||
<span className="flex flex-row items-center md:py-20 py-10 flex-wrap">
|
||||
{/* description card */}
|
||||
<span className="mx-auto text-left flex flex-col grow max-w-lg shadow-lg rounded-lg bg-gray-200 bg-opacity-40 backdrop-blur-xl p-10">
|
||||
<span className="bg-clip-text font-bold text-transparent bg-gradient-to-r from-pink-500 to-violet-500">
|
||||
CLARITY
|
||||
</span>
|
||||
<span className="text-3xl text-gray-700 font-bold">
|
||||
Focus on <span className="text-teal-600">Right Now.</span>
|
||||
</span>
|
||||
<span className="text-lg text-gray-700"></span>
|
||||
|
||||
<Divider />
|
||||
|
||||
{/* checkmarks of value add */}
|
||||
|
||||
<span className="flex flex-row items-baseline space-x-2">
|
||||
<FaCheck className="text-teal-500 text-md shrink-0" />
|
||||
|
||||
<span className="flex flex-col">
|
||||
<span className="flex flex-row items-center space-x-2">
|
||||
<span className="text-left text-lg text-gray-600">
|
||||
{"Overall Wellbeing."}
|
||||
</span>
|
||||
</span>
|
||||
<span className="text-sm text-gray-500"></span>
|
||||
</span>
|
||||
</span>
|
||||
|
||||
<span className="flex flex-row items-baseline space-x-2">
|
||||
<FaCheck className="text-teal-500 text-md shrink-0" />
|
||||
|
||||
<span className="flex flex-col">
|
||||
<span className="flex flex-row items-center space-x-2">
|
||||
<span className="text-left text-lg text-gray-600">
|
||||
{"Clarity of Mind."}
|
||||
</span>
|
||||
</span>
|
||||
<span className="text-sm text-gray-500">
|
||||
{"Come to work seeing only what's important today/this week."}
|
||||
</span>
|
||||
</span>
|
||||
</span>
|
||||
|
||||
<span className="flex flex-row items-baseline space-x-2">
|
||||
<FaCheck className="text-teal-500 text-md shrink-0" />
|
||||
|
||||
<span className="flex flex-col">
|
||||
<span className="flex flex-row items-center space-x-2">
|
||||
<span className="text-left text-lg text-gray-600">
|
||||
{"Inbox Zero."}
|
||||
</span>
|
||||
</span>
|
||||
<span className="text-sm text-gray-500">
|
||||
{
|
||||
"Conversations are ephemeral like in real life. We keep you at inbox zero."
|
||||
}
|
||||
</span>
|
||||
</span>
|
||||
</span>
|
||||
|
||||
<Divider />
|
||||
|
||||
<span className="text-md text-gray-600">
|
||||
Your <span className="text-orange-500">current tools</span> bombard
|
||||
you with files, notifications, scrolling, clicking, emojis,
|
||||
threads...
|
||||
</span>
|
||||
|
||||
<br></br>
|
||||
|
||||
<span className="text-md text-gray-600">
|
||||
They are designed to{" "}
|
||||
<span className="text-orange-500"> addict you, </span>and keep you
|
||||
<span className="text-orange-500"> distracted.</span>
|
||||
</span>
|
||||
</span>
|
||||
<span className="flex-1 justify-evenly flex flex-row flex-wrap backdrop-blur-md p-10 rounded">
|
||||
{/* illustration of collaborating */}
|
||||
<img
|
||||
src="/illustrations/undraw_freelancer_re_irh4.svg"
|
||||
className="min-h-[20rem] min-w-[20rem] shrink-0"
|
||||
/>
|
||||
</span>
|
||||
</span>
|
||||
|
||||
{/* action section to get started now */}
|
||||
<LandingPageActionBar />
|
||||
|
||||
{/* customer testimonial section */}
|
||||
<span className="flex my-20 mx-auto flex-col items-center max-w-screen-lg p-10 backdrop-blur-md bg-gray-200 bg-opacity-40 rounded-lg">
|
||||
<span className="text-3xl font-bold">Our Customers</span>
|
||||
<span>
|
||||
<span className="text-teal-600">Listen</span> to what they have to
|
||||
say.
|
||||
</span>
|
||||
|
||||
{/* all voice testimonial cards */}
|
||||
<span className="flex flex-row items-center justify-evenly mt-5">
|
||||
<span className="p-5 flex flex-row items-center bg-white bg-opacity-20 rounded shadow-md">
|
||||
<img
|
||||
src="https://lh3.googleusercontent.com/a-/AOh14GhTB6LbOqohOA3csckho3OA976yp3lMEtl2MDzbgX0=s96-c"
|
||||
className="rounded-full shadow-md h-[3rem]"
|
||||
/>
|
||||
<span className="flex flex-col ml-2 mr-10">
|
||||
<span className="text-lg">Heran Patel</span>
|
||||
<span className="text-gray-400">CEO of FinityOne</span>
|
||||
</span>
|
||||
|
||||
<button className="bg-gray-500 bg-opacity-25 p-2 ml-2 rounded hover:bg-opacity-40">
|
||||
<FaPlay className="text-lg text-gray-500" />
|
||||
</button>
|
||||
</span>
|
||||
</span>
|
||||
</span>
|
||||
</LangingPageLayout>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
import { Divider } from "antd";
|
||||
import Link from "next/link";
|
||||
import { useRouter } from "next/router";
|
||||
import { FaAngleRight } from "react-icons/fa";
|
||||
import LandingPageActionBar from "../components/demo/LandingPageActionBar";
|
||||
import Rooms from "../components/demo/Rooms";
|
||||
import LangingPageLayout from "../components/Layouts/LandingPageLayout";
|
||||
|
||||
export default function Philosophy() {
|
||||
const handleGetDemo = () => {
|
||||
window.open("https://calendly.com/usenirvana/30min", "_blank");
|
||||
};
|
||||
|
||||
const router = useRouter();
|
||||
|
||||
const getStartedButton = (
|
||||
<button
|
||||
onClick={handleGetDemo}
|
||||
className="rounded font-semibold bg-teal-600 p-2 text-white shadow-lg flex flex-row items-center space-x-2"
|
||||
>
|
||||
<span>Get Demo</span>
|
||||
<FaAngleRight />
|
||||
</button>
|
||||
);
|
||||
|
||||
return (
|
||||
<LangingPageLayout>
|
||||
<span className="flex flex-col my-20 items-center">
|
||||
<span>PHILOSOPHY</span>
|
||||
<span className="text-5xl font-bold text-center">
|
||||
We live in a <span className="text-red-800"> distracted world.</span>{" "}
|
||||
<br></br>
|
||||
<span className="text-teal-600">{"Let's fix it."}</span>
|
||||
</span>
|
||||
<span className="text-gray-500 text-lg">
|
||||
Everything starts with the philosophy.
|
||||
</span>
|
||||
</span>
|
||||
|
||||
{/* main mission */}
|
||||
<div className="mx-auto flex flex-col items-start max-w-screen-sm rounded-lg bg-gray-200 bg-opacity-25 backdrop-blur-xl p-10">
|
||||
<span className="bg-clip-text text-transparent bg-gradient-to-r from-pink-500 to-violet-500">
|
||||
Productivity is a Myth
|
||||
</span>
|
||||
|
||||
<span className="text-left text-lg mb-5 text-gray-700">
|
||||
In today's world, we{" "}
|
||||
<span className="text-red-600"> mistake</span> adding software and
|
||||
tools for <span className="text-red-600">productivity.</span>
|
||||
</span>
|
||||
|
||||
<span className="text-left text-lg text-gray-700 mb-5">
|
||||
At Nirvana, we are bringing work back to the basics.{" "}
|
||||
<span className="text-teal-600">Voice-only communication </span>{" "}
|
||||
because voice makes us human and less is more. An experience as if
|
||||
your team was{" "}
|
||||
<span className="text-teal-600">right next to you.</span>
|
||||
</span>
|
||||
|
||||
<span className="text-left text-lg text-gray-700">
|
||||
Improve team{" "}
|
||||
<span className="text-teal-600">
|
||||
focus, performance, and wellbeing.
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* action section to get started now */}
|
||||
<LandingPageActionBar />
|
||||
</LangingPageLayout>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,126 @@
|
||||
import { Divider } from "antd";
|
||||
import Link from "next/link";
|
||||
import {
|
||||
FaAngleRight,
|
||||
FaArrowRight,
|
||||
FaMoneyBill,
|
||||
FaPhone,
|
||||
} from "react-icons/fa";
|
||||
import LandingPageActionBar from "../components/demo/LandingPageActionBar";
|
||||
import LandingPageLayout from "../components/Layouts/LandingPageLayout";
|
||||
|
||||
export default function Pricing() {
|
||||
return (
|
||||
<LandingPageLayout>
|
||||
<div className="flex flex-col items-center text-center backdrop-blur-md p-10 lg:my-20 mb-20">
|
||||
<span>PRICING</span>
|
||||
<span className="text-5xl font-bold">Get Started Now,</span>
|
||||
<span className="text-5xl font-bold text-teal-600 mb-2">
|
||||
Pay for Spots Later.
|
||||
</span>
|
||||
<span className="text-gray-500 mb-2">
|
||||
Hit the ground running with{" "}
|
||||
<span className="text-teal-600">2 free spots</span> on sign up.
|
||||
</span>
|
||||
|
||||
<span className="p-2 rounded-lg font-bold text-sm bg-purple-200 text-purple-700">
|
||||
No credit card required
|
||||
</span>
|
||||
|
||||
<span className="flex flex-row items-center space-x-2 mt-10">
|
||||
<Link href="/features">Features</Link>
|
||||
|
||||
<button
|
||||
onClick={() => window.open("/teams/login", "_self")}
|
||||
className="rounded font-semibold bg-teal-600 p-2 text-white shadow-lg flex flex-row items-center justify-evenly space-x-2"
|
||||
>
|
||||
<span>Get Started</span>
|
||||
<FaAngleRight />
|
||||
</button>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<span className="flex flex-row justify-center space-x-5 mb-32 flex-wrap">
|
||||
{/* lite pricing */}
|
||||
<span className="flex flex-col items-start bg-white bg-opacity-40 p-10 max-w-sm rounded backdrop-blur-md w-[22rem] shadow-lg">
|
||||
<span className="flex flex-row items-center justify-between w-full">
|
||||
<span className="text-3xl font-bold">Lite</span>
|
||||
<span className="p-2 rounded-lg font-bold text-left text-sm bg-green-200 text-green-700">
|
||||
FREE
|
||||
</span>
|
||||
</span>
|
||||
|
||||
<span className="text-gray-500">
|
||||
Hit the ground running with 2 free spots.
|
||||
</span>
|
||||
|
||||
<button
|
||||
onClick={() => window.open("/teams/login", "_self")}
|
||||
className="mt-10 mx-auto rounded font-semibold my-2 p-2 bg-gray-200 text-teal-600 shadow-lg flex flex-row items-center justify-evenly space-x-2"
|
||||
>
|
||||
<span>Get Started</span>
|
||||
<FaAngleRight />
|
||||
</button>
|
||||
<span className="text-gray-500 text-sm text-center w-full">
|
||||
Valid for 30 days.
|
||||
</span>
|
||||
</span>
|
||||
|
||||
{/* startups pricing */}
|
||||
<span className="z-10 flex flex-col items-start bg-white bg-opacity-60 p-10 max-w-sm rounded backdrop-blur-md w-[22rem] scale-125 shadow-lg">
|
||||
<span className="flex flex-row items-center justify-between w-full">
|
||||
<span className="text-3xl font-bold">Startups</span>
|
||||
<span className="p-2 rounded-lg font-bold text-left text-sm bg-sky-200 text-sky-700">
|
||||
POPULAR
|
||||
</span>
|
||||
</span>
|
||||
|
||||
<span className="text-gray-500">
|
||||
Pay per spot per month based on your needs.
|
||||
</span>
|
||||
|
||||
<button
|
||||
onClick={() =>
|
||||
window.open("https://calendly.com/usenirvana/30min", "_blank")
|
||||
}
|
||||
className="mx-auto mt-10 rounded font-semibold my-2 bg-teal-600 p-2 text-white shadow-lg flex flex-row items-center justify-evenly space-x-2"
|
||||
>
|
||||
<FaPhone />
|
||||
<span>Contact Sales</span>
|
||||
</button>
|
||||
<span className="text-gray-500 text-sm text-center w-full mx-auto">
|
||||
<FaMoneyBill className="inline text-xl text-emerald-500" /> 30 day
|
||||
money back guarantee.
|
||||
</span>
|
||||
</span>
|
||||
|
||||
{/* business pricing */}
|
||||
<span className="flex flex-col items-start bg-white bg-opacity-40 p-10 max-w-sm rounded backdrop-blur-md w-[22rem] shadow-lg">
|
||||
<span className="flex flex-row items-center justify-between w-full">
|
||||
<span className="text-3xl font-bold">Business</span>
|
||||
</span>
|
||||
|
||||
<span className="text-gray-500">
|
||||
Pay for enterprise-level features for your large-scale organization.
|
||||
</span>
|
||||
|
||||
<button
|
||||
onClick={() =>
|
||||
window.open("https://calendly.com/usenirvana/30min", "_blank")
|
||||
}
|
||||
className="mx-auto mt-10 rounded font-semibold my-2 p-2 bg-gray-200 text-teal-600 shadow-lg flex flex-row items-center justify-evenly space-x-2"
|
||||
>
|
||||
<FaPhone />
|
||||
<span>Contact Sales</span>
|
||||
</button>
|
||||
<span className="text-gray-500 text-sm text-center w-full mx-auto">
|
||||
<FaMoneyBill className="inline text-xl text-emerald-500" /> 30 day
|
||||
money back guarantee.
|
||||
</span>
|
||||
</span>
|
||||
</span>
|
||||
|
||||
<LandingPageActionBar />
|
||||
</LandingPageLayout>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,134 @@
|
||||
import { GetServerSidePropsContext, GetServerSidePropsResult } from "next";
|
||||
import { useRouter } from "next/router";
|
||||
import { useEffect, useState } from "react";
|
||||
import Header from "../../components/Dashboard/Header";
|
||||
import Rooms from "../../components/Dashboard/Rooms";
|
||||
import TeamVoiceLine from "../../components/Dashboard/TeamVoiceLine";
|
||||
import BackgroundLayout from "../../components/Layouts/BackgroundLayout";
|
||||
import Loading from "../../components/Loading";
|
||||
import AudioContextProvider from "../../contexts/keyboardContext";
|
||||
import { useAuth } from "../../contexts/authContext";
|
||||
import {
|
||||
TeamDashboardContextProvider,
|
||||
useTeamDashboardContext,
|
||||
} from "../../contexts/teamDashboardContext";
|
||||
import { Team } from "../../models/team";
|
||||
import { TeamMemberStatus } from "../../models/teamMember";
|
||||
import { UserStatus } from "../../models/user";
|
||||
import TeamService from "../../services/teamService";
|
||||
import UserService from "../../services/userService";
|
||||
import Announcements from "../../components/Dashboard/Announcements";
|
||||
import Links from "../../components/Dashboard/Links";
|
||||
import Office from "../../components/Dashboard/Office";
|
||||
import ShortcutHelpModal from "../../components/Modals/ShortcutHelpModal";
|
||||
|
||||
// User has switched back to the tab
|
||||
const onFocus = () => {
|
||||
console.log("Tab is in focus");
|
||||
};
|
||||
|
||||
// User has switched away from the tab (AKA tab is hidden)
|
||||
const onBlur = () => {
|
||||
console.log("Tab is blurred");
|
||||
};
|
||||
|
||||
const alertUserAboutClosing = (ev) => {
|
||||
// change user status to offline
|
||||
|
||||
ev.preventDefault();
|
||||
return (ev.returnValue = "are you sure?");
|
||||
};
|
||||
|
||||
const userService = new UserService();
|
||||
|
||||
function TeamDashboard() {
|
||||
const { currUser } = useAuth();
|
||||
const router = useRouter();
|
||||
const teamDashboardContext = useTeamDashboardContext();
|
||||
|
||||
// user goes offline
|
||||
const handleTabClosing = () => {
|
||||
// change status of user
|
||||
userService.updateUserStatus(currUser.uid, UserStatus.offline);
|
||||
};
|
||||
|
||||
// shows browser alert to warn user of exiting
|
||||
const alertUserAboutClosing = (event: any) => {
|
||||
event.preventDefault();
|
||||
event.returnValue = "";
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
// set the document title based on the team
|
||||
// todo: update based on who sent you a message
|
||||
document.title = teamDashboardContext.team.name;
|
||||
|
||||
window.addEventListener("focus", onFocus);
|
||||
window.addEventListener("blur", onBlur);
|
||||
// Calls onFocus when the window first loads
|
||||
onFocus();
|
||||
|
||||
// set user to online until he closes tab/window
|
||||
userService.updateUserStatus(currUser.uid, UserStatus.online);
|
||||
|
||||
window.addEventListener("beforeunload", alertUserAboutClosing);
|
||||
window.addEventListener("unload", handleTabClosing);
|
||||
return () => {
|
||||
// Specify how to clean up after this effect:
|
||||
window.removeEventListener("focus", onFocus);
|
||||
window.removeEventListener("blur", onBlur);
|
||||
window.removeEventListener("beforeunload", alertUserAboutClosing);
|
||||
window.removeEventListener("unload", handleTabClosing);
|
||||
};
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<>
|
||||
<ShortcutHelpModal />
|
||||
|
||||
<div className="container mx-auto py-10 lg:px-10 flex flex-col space-y-5">
|
||||
<Header />
|
||||
|
||||
<div className="flex flex-row items-start lg:space-x-5 h-[56rem]">
|
||||
<div className="hidden lg:flex flex-col max-w-sm space-y-5 h-full">
|
||||
<Office />
|
||||
|
||||
<TeamVoiceLine />
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col space-y-5 flex-1 h-full">
|
||||
<Announcements />
|
||||
|
||||
<Rooms />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Links />
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default function TeamDashboardWrapper() {
|
||||
return (
|
||||
<TeamDashboardContextProvider>
|
||||
<AudioContextProvider>
|
||||
<BackgroundLayout>
|
||||
<TeamDashboard />
|
||||
</BackgroundLayout>
|
||||
</AudioContextProvider>
|
||||
</TeamDashboardContextProvider>
|
||||
);
|
||||
}
|
||||
|
||||
export async function getServerSideProps(context: GetServerSidePropsContext) {
|
||||
// basic check if this team exists
|
||||
|
||||
// todo check if authenticated
|
||||
|
||||
// todo check if this user is in this team
|
||||
|
||||
return {
|
||||
props: {},
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,396 @@
|
||||
import { Divider } from "antd";
|
||||
import { useRouter } from "next/router";
|
||||
import { useEffect, useState } from "react";
|
||||
import Loading from "../../../components/Loading";
|
||||
import {
|
||||
TeamDashboardContextProvider,
|
||||
useTeamDashboardContext,
|
||||
} from "../../../contexts/teamDashboardContext";
|
||||
import {
|
||||
TeamMember,
|
||||
TeamMemberRole,
|
||||
TeamMemberStatus,
|
||||
} from "../../../models/teamMember";
|
||||
import { toast } from "react-hot-toast";
|
||||
import { Team, TeamStatus } from "../../../models/team";
|
||||
|
||||
import {
|
||||
FaMoneyBillWave,
|
||||
FaTrash,
|
||||
FaPaperPlane,
|
||||
FaArrowLeft,
|
||||
} from "react-icons/fa";
|
||||
import TeamService from "../../../services/teamService";
|
||||
import Router from "next/router";
|
||||
import Moment from "react-moment";
|
||||
|
||||
export default function TeamAdminWrapper() {
|
||||
return (
|
||||
<TeamDashboardContextProvider>
|
||||
<TeamAdmin />
|
||||
</TeamDashboardContextProvider>
|
||||
);
|
||||
}
|
||||
|
||||
const initialSite: string = "https://";
|
||||
|
||||
function renderTeamMemberStatus(status: TeamMemberStatus) {
|
||||
switch (status) {
|
||||
case TeamMemberStatus.invited:
|
||||
return (
|
||||
<span className="text-gray-700 ml-1 bg-gray-200 p-1 rounded-md text-xs font-bold mt-2">
|
||||
invited
|
||||
</span>
|
||||
);
|
||||
case TeamMemberStatus.activated:
|
||||
return (
|
||||
<span className="text-sky-700 ml-1 bg-sky-200 p-1 rounded-md text-xs font-bold mt-2">
|
||||
active
|
||||
</span>
|
||||
);
|
||||
case TeamMemberStatus.deleted:
|
||||
return (
|
||||
<span className="text-red-700 ml-1 bg-red-200 p-1 rounded-md text-xs font-bold mt-2">
|
||||
deleted
|
||||
</span>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const teamService = new TeamService();
|
||||
|
||||
function TeamAdmin() {
|
||||
const router = useRouter();
|
||||
const { teamid } = router.query;
|
||||
const { team, userTeamMember, teamMembers, user } = useTeamDashboardContext();
|
||||
|
||||
const [error, setError] = useState<string>("");
|
||||
const [teamName, setTeamName] = useState<string>(team.name);
|
||||
const [companySite, setcompanySite] = useState<string>(team.companySite);
|
||||
const [inviteEmail, setInviteEmail] = useState<string>("");
|
||||
const [loading, setLoading] = useState<Boolean>(true);
|
||||
|
||||
useEffect(() => {
|
||||
(async function () {
|
||||
// have checks for team and part of team or not in context
|
||||
try {
|
||||
// if not admin, I shouldn't be here
|
||||
if (userTeamMember.role != TeamMemberRole.admin) {
|
||||
toast.error("You are not allowed here!");
|
||||
router.push("/teams");
|
||||
}
|
||||
} catch (error) {
|
||||
console.log("something went wrong in admin team page");
|
||||
router.push("/teams");
|
||||
}
|
||||
|
||||
setLoading(false);
|
||||
})();
|
||||
}, []);
|
||||
|
||||
async function handleSubmitTeamUpdate(e) {
|
||||
e.preventDefault();
|
||||
|
||||
setLoading(true);
|
||||
|
||||
if (!teamName) {
|
||||
setError("You must input a team name!");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const updatedTeam = new Team();
|
||||
updatedTeam.id = team.id;
|
||||
updatedTeam.name = teamName;
|
||||
updatedTeam.createdByUserId = user.id;
|
||||
updatedTeam.status = TeamStatus.created;
|
||||
|
||||
if (companySite != initialSite) {
|
||||
// make sure to put null in database for the team
|
||||
updatedTeam.companySite = companySite;
|
||||
}
|
||||
|
||||
// update team name and company site if it changed
|
||||
await teamService.updateTeam(updatedTeam);
|
||||
|
||||
// take user back to team dashboard
|
||||
router.push("/teams/" + team.id);
|
||||
} catch (error) {
|
||||
setError(error.message);
|
||||
console.log(error);
|
||||
}
|
||||
|
||||
setLoading(false);
|
||||
}
|
||||
|
||||
async function handleSubmitNewTeamMember(e) {
|
||||
e.preventDefault();
|
||||
|
||||
setLoading(true);
|
||||
|
||||
if (!inviteEmail) {
|
||||
setError("You must input a valid email!");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const newInviteTeamMember = new TeamMember();
|
||||
newInviteTeamMember.inviteEmailAddress = inviteEmail;
|
||||
newInviteTeamMember.invitedByUserId = user.id;
|
||||
newInviteTeamMember.status = TeamMemberStatus.invited;
|
||||
newInviteTeamMember.teamId = team.id;
|
||||
|
||||
await teamService.createTeamInvite(newInviteTeamMember);
|
||||
|
||||
Router.reload();
|
||||
} catch (error) {
|
||||
toast.error("something went wrong");
|
||||
}
|
||||
}
|
||||
|
||||
async function handleDeleteTeamMember(e, teamMemberId: string) {
|
||||
e.preventDefault();
|
||||
|
||||
setLoading(true);
|
||||
|
||||
try {
|
||||
await teamService.updateTeamMemberStatus(
|
||||
teamMemberId,
|
||||
TeamMemberStatus.deleted
|
||||
);
|
||||
|
||||
Router.reload();
|
||||
} catch (error) {
|
||||
toast.error("unable to delete team member");
|
||||
}
|
||||
}
|
||||
|
||||
if (loading) {
|
||||
return <Loading />;
|
||||
}
|
||||
|
||||
async function handleInviteTeamMember(e, teamMemberId: string) {
|
||||
e.preventDefault();
|
||||
setLoading(true);
|
||||
|
||||
try {
|
||||
await teamService.updateTeamMemberStatus(
|
||||
teamMemberId,
|
||||
TeamMemberStatus.invited
|
||||
);
|
||||
|
||||
Router.reload();
|
||||
} catch (error) {
|
||||
toast.error("unable to delete team member");
|
||||
}
|
||||
}
|
||||
|
||||
if (loading) {
|
||||
return <Loading />;
|
||||
}
|
||||
|
||||
// get count of all active and invited users...
|
||||
const activeOrInvited = teamMembers.filter(
|
||||
(tmember) =>
|
||||
tmember.status == TeamMemberStatus.invited ||
|
||||
tmember.status == TeamMemberStatus.activated
|
||||
);
|
||||
const teamSpotsRemaining =
|
||||
(team.allowedUserCount || 0) - activeOrInvited.length - 1;
|
||||
|
||||
return (
|
||||
<div className="container mx-auto max-w-screen-sm m-10 bg-white p-10 rounded-lg shadow-md">
|
||||
<form
|
||||
onSubmit={handleSubmitTeamUpdate}
|
||||
className="flex flex-col space-y-5"
|
||||
>
|
||||
{/* header */}
|
||||
<div className="text-lg flex flex-row items-center space-x-2">
|
||||
<FaArrowLeft
|
||||
className="hover:cursor-pointer"
|
||||
onClick={() => router.push("/teams/" + teamid)}
|
||||
/>{" "}
|
||||
<span>Manage Team</span>
|
||||
</div>
|
||||
|
||||
{error && <span className="text-red-300 text-md">{error}</span>}
|
||||
|
||||
<Divider />
|
||||
|
||||
<span className="flex flex-col items-start">
|
||||
<span className="text-md">Team Name</span>
|
||||
<span className="text-gray-300 text-xs mb-2">
|
||||
Everyone in your team will see this.
|
||||
</span>
|
||||
<input
|
||||
placeholder="ex. AirBnB"
|
||||
className="w-full rounded-lg bg-gray-50 p-3"
|
||||
value={teamName}
|
||||
onChange={(e) => setTeamName(e.target.value)}
|
||||
/>
|
||||
</span>
|
||||
|
||||
<span className="flex flex-col items-start">
|
||||
<span className="text-md">Company Site</span>
|
||||
<span className="text-gray-300 text-xs mb-2">optional</span>
|
||||
<input
|
||||
placeholder="ex. https://"
|
||||
className="w-full rounded-lg bg-gray-50 p-3"
|
||||
value={companySite}
|
||||
onChange={(e) => setcompanySite(e.target.value)}
|
||||
/>
|
||||
</span>
|
||||
|
||||
<Divider />
|
||||
|
||||
<span className="flex flex-row justify-end space-x-2">
|
||||
<button
|
||||
onClick={() => router.push("/teams/" + team.id)}
|
||||
className="bg-gray-100 py-2 px-5 rounded text-gray-400"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
type="submit"
|
||||
className="text-sm text-white font-semibold py-2 px-5 bg-teal-500 rounded"
|
||||
>
|
||||
Save
|
||||
</button>
|
||||
</span>
|
||||
</form>
|
||||
|
||||
<form
|
||||
className="flex flex-col space-y-5"
|
||||
onSubmit={handleSubmitNewTeamMember}
|
||||
>
|
||||
<Divider />
|
||||
|
||||
<span className="flex flex-row justify-start items-start">
|
||||
<FaMoneyBillWave className="text-5xl text-green-500" />
|
||||
|
||||
<span className="flex flex-col justify-start ml-2">
|
||||
<span className="text-md ">Billing</span>
|
||||
<span className="text-md text-gray-500 mr-20">
|
||||
Please email{" "}
|
||||
<button className="underline decoration-teal-500 text-brown-500">
|
||||
usenirvana@gmail.com
|
||||
</button>{" "}
|
||||
to open more spots for your team.
|
||||
</span>
|
||||
</span>
|
||||
|
||||
<span className="ml-auto text-center text-orange-700 bg-orange-200 p-1 rounded-md text-sm font-bold mt-2">
|
||||
{teamSpotsRemaining} spots remaining
|
||||
</span>
|
||||
</span>
|
||||
|
||||
<Divider />
|
||||
|
||||
<span className="text-md">Team</span>
|
||||
|
||||
{/* current user */}
|
||||
<div className="flex flex-row">
|
||||
<img src={user.avatarUrl} alt="asdf" className="rounded-full w-12" />
|
||||
<span className="flex flex-col items-start ml-2">
|
||||
<span className="text-md text-gray-500">
|
||||
{user.firstName + " " + user.lastName}
|
||||
|
||||
<span className="text-green-700 ml-1 bg-green-200 p-1 rounded-md text-xs font-bold mt-2">
|
||||
admin
|
||||
</span>
|
||||
|
||||
{renderTeamMemberStatus(userTeamMember.status)}
|
||||
</span>
|
||||
<span className="text-xs text-gray-200">{user.emailAddress}</span>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* mapping all team members */}
|
||||
{teamMembers.map((tmember, i) => {
|
||||
return (
|
||||
<div key={i} className="flex flex-row items-center">
|
||||
<div className="rounded-full w-12 animate-pulse bg-gray-200 h-12" />
|
||||
|
||||
<span className="flex flex-col items-start ml-2">
|
||||
<span className="text-md text-gray-500">
|
||||
{tmember.inviteEmailAddress}
|
||||
|
||||
{renderTeamMemberStatus(tmember.status)}
|
||||
</span>
|
||||
|
||||
{/* todo: date joined */}
|
||||
{/* <Moment date={tmember} className="text-xs text-gray-200" /> */}
|
||||
</span>
|
||||
|
||||
{tmember.status == TeamMemberStatus.deleted &&
|
||||
teamSpotsRemaining > 0 ? (
|
||||
<button
|
||||
onClick={(e) => handleInviteTeamMember(e, tmember.id)}
|
||||
className="ml-auto bg-gray-300 bg-opacity-25 p-2 rounded hover:bg-opacity-40"
|
||||
>
|
||||
<FaPaperPlane className="text-sm text-teal-500 " />
|
||||
</button>
|
||||
) : (
|
||||
<></>
|
||||
)}
|
||||
{tmember.status == TeamMemberStatus.invited ||
|
||||
tmember.status == TeamMemberStatus.activated ? (
|
||||
<button
|
||||
onClick={(e) => handleDeleteTeamMember(e, tmember.id)}
|
||||
className="bg-orange-300 bg-opacity-25 p-2 ml-auto rounded hover:bg-opacity-40"
|
||||
>
|
||||
<FaTrash className="text-sm text-orange-500 " />
|
||||
</button>
|
||||
) : (
|
||||
<></>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
|
||||
{/* input for adding people */}
|
||||
{teamSpotsRemaining > 0 ? (
|
||||
<>
|
||||
<span className="flex flex-col items-start">
|
||||
<span className="text-md">Add Members</span>
|
||||
<span className="text-red-300 text-md mb-2">
|
||||
IMPORTANT: Tell them to sign up at{" "}
|
||||
<span className="underline decoration-teal-500">
|
||||
usenirvana.com{" "}
|
||||
</span>{" "}
|
||||
using this same email address.
|
||||
</span>
|
||||
<input
|
||||
placeholder="ex. [email protected]"
|
||||
className="w-full rounded-lg bg-gray-50 p-3"
|
||||
value={inviteEmail}
|
||||
onChange={(e) => setInviteEmail(e.target.value)}
|
||||
/>
|
||||
</span>
|
||||
|
||||
<span className="flex flex-row justify-end space-x-2">
|
||||
{inviteEmail ? (
|
||||
<button
|
||||
type="submit"
|
||||
className="text-sm text-white font-semibold py-2 px-5 bg-teal-500 rounded"
|
||||
>
|
||||
Add
|
||||
</button>
|
||||
) : (
|
||||
<button
|
||||
type="submit"
|
||||
className="text-sm text-white font-semibold py-2 px-5 bg-gray-200 rounded"
|
||||
disabled
|
||||
>
|
||||
Add
|
||||
</button>
|
||||
)}
|
||||
</span>
|
||||
</>
|
||||
) : (
|
||||
<></>
|
||||
)}
|
||||
</form>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,142 @@
|
||||
import { Divider } from "antd";
|
||||
import { useRouter } from "next/router";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useAuth } from "../../contexts/authContext";
|
||||
import { Team, TeamStatus } from "../../models/team";
|
||||
import { User, UserStatus } from "../../models/user";
|
||||
import TeamService from "../../services/teamService";
|
||||
|
||||
const initialSite: string = "https://";
|
||||
|
||||
const teamService = new TeamService();
|
||||
|
||||
export default function CreateTeam() {
|
||||
const { currUser } = useAuth();
|
||||
const [error, setError] = useState<string>("");
|
||||
const [loading, setLoading] = useState<Boolean>(true);
|
||||
const router = useRouter();
|
||||
|
||||
useEffect(() => {
|
||||
(async function () {
|
||||
try {
|
||||
// if not authenticated, take user to the login
|
||||
if (!currUser) {
|
||||
console.log(
|
||||
"not authenticated...routing from dashboard to teams home"
|
||||
);
|
||||
router.push("/teams/login");
|
||||
}
|
||||
|
||||
// todo if user is in a team already, notify them to make sure
|
||||
// buttt most likely will already be routed to the correct place from the router...don't do the router's job
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
router.push("/teams/login");
|
||||
}
|
||||
|
||||
setLoading(false);
|
||||
})();
|
||||
}, []);
|
||||
|
||||
async function handleSubmit(e) {
|
||||
e.preventDefault();
|
||||
|
||||
setLoading(true);
|
||||
|
||||
if (!teamName) {
|
||||
setError("You must input a team name!");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const team = new Team();
|
||||
team.name = teamName;
|
||||
team.createdByUserId = currUser.uid;
|
||||
team.status = TeamStatus.created;
|
||||
team.allowedUserCount = 2;
|
||||
|
||||
if (companySite != initialSite) {
|
||||
// make sure to put null in database for the team
|
||||
team.companySite = companySite;
|
||||
}
|
||||
|
||||
// provision a team with state of initiated
|
||||
const createdTeamId = await teamService.createTeam(team);
|
||||
router.push("/teams/" + createdTeamId);
|
||||
} catch (error) {
|
||||
setError(error.message);
|
||||
}
|
||||
|
||||
setLoading(false);
|
||||
}
|
||||
|
||||
const [teamName, setTeamName] = useState<string>("");
|
||||
const [companySite, setcompanySite] = useState<string>(initialSite);
|
||||
|
||||
return (
|
||||
<form
|
||||
className="container mx-auto flex flex-col max-w-md m-10 bg-white p-10 rounded-lg shadow-md space-y-5"
|
||||
onSubmit={handleSubmit}
|
||||
>
|
||||
{/* header */}
|
||||
<div className="text-lg">🙌Create a Team</div>
|
||||
|
||||
{error && <span className="text-red-300 text-md">{error}</span>}
|
||||
|
||||
<Divider />
|
||||
|
||||
<span className="flex flex-col items-start">
|
||||
<span className="text-md">Team Name</span>
|
||||
<span className="text-gray-300 text-xs mb-2">
|
||||
Everyone in your team will see this. You can always change this later.
|
||||
</span>
|
||||
<input
|
||||
placeholder="ex. AirBnB"
|
||||
className="w-full rounded-lg bg-gray-50 p-3"
|
||||
value={teamName}
|
||||
onChange={(e) => setTeamName(e.target.value)}
|
||||
/>
|
||||
</span>
|
||||
|
||||
<span className="flex flex-col items-start">
|
||||
<span className="text-md">Company Site</span>
|
||||
<span className="text-gray-300 text-xs mb-2">optional</span>
|
||||
<input
|
||||
placeholder="ex. https://"
|
||||
className="w-full rounded-lg bg-gray-50 p-3"
|
||||
value={companySite}
|
||||
onChange={(e) => setcompanySite(e.target.value)}
|
||||
/>
|
||||
</span>
|
||||
|
||||
<Divider />
|
||||
|
||||
<span className="flex flex-row justify-end space-x-2">
|
||||
<button
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
router.push("/teams");
|
||||
}}
|
||||
className="bg-gray-100 py-2 px-5 rounded text-gray-400"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
{loading ? (
|
||||
<div
|
||||
className="spinner-border animate-spin inline-block w-8 h-8 border-4 rounded-full text-blue-600"
|
||||
role="status"
|
||||
>
|
||||
<span className="text-black hidden">Loading...</span>
|
||||
</div>
|
||||
) : (
|
||||
<button
|
||||
type="submit"
|
||||
className="text-sm text-white font-semibold py-2 px-5 bg-teal-500 rounded"
|
||||
>
|
||||
{"Continue ->"}
|
||||
</button>
|
||||
)}
|
||||
</span>
|
||||
</form>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,855 @@
|
||||
import Head from 'next/head'
|
||||
import Image from 'next/image';
|
||||
import React from 'react';
|
||||
import { FaMicrophoneAlt,
|
||||
FaHeadphonesAlt,
|
||||
FaTh,
|
||||
FaAngleDown,
|
||||
FaPlusSquare,
|
||||
FaBroom,
|
||||
FaBell,
|
||||
FaFilePdf,
|
||||
FaCopy,
|
||||
FaClock,
|
||||
FaPlay,
|
||||
FaPlus,
|
||||
FaCheck,
|
||||
FaCode,
|
||||
FaLink,
|
||||
FaExternalLinkAlt,
|
||||
FaArchive,
|
||||
FaAtlassian,
|
||||
FaSearch
|
||||
|
||||
} from "react-icons/fa";
|
||||
import { IoPulseOutline, IoRemoveOutline, IoTimer } from "react-icons/io5";
|
||||
import { BsThreeDots } from "react-icons/bs";
|
||||
|
||||
import { UserStatus, User } from '../../models/user'
|
||||
import { KeyCode } from '../../globals/keycode'
|
||||
|
||||
import BackgroundLayout from '../../components/Layouts/BackgroundLayout'
|
||||
|
||||
let testFriends = [
|
||||
{
|
||||
name: "Liam",
|
||||
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: "engineer",
|
||||
systemAvatar: "04",
|
||||
status: UserStatus.busy
|
||||
},
|
||||
{
|
||||
name: "Adriana",
|
||||
role: "design",
|
||||
systemAvatar: "06",
|
||||
status: UserStatus.busy
|
||||
},
|
||||
{
|
||||
name: "Josh",
|
||||
role: "design",
|
||||
systemAvatar: "05",
|
||||
status: UserStatus.offline
|
||||
}
|
||||
]
|
||||
|
||||
function getUser(name: string) : User {
|
||||
return new User()
|
||||
}
|
||||
|
||||
type MyState = {
|
||||
selectedChannel?: number
|
||||
};
|
||||
|
||||
|
||||
export default class Demo extends React.Component<{ }, MyState> {
|
||||
state: MyState = {
|
||||
selectedChannel: null
|
||||
}
|
||||
|
||||
componentDidMount(): void {
|
||||
document.addEventListener("keydown", this.handleKeyboardShortcut)
|
||||
}
|
||||
|
||||
componentWillUnmount(): void {
|
||||
document.removeEventListener("keydown", this.handleKeyboardShortcut);
|
||||
}
|
||||
|
||||
statusBubble(status: UserStatus) {
|
||||
console.log(status)
|
||||
|
||||
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>
|
||||
}
|
||||
}
|
||||
|
||||
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" />
|
||||
}
|
||||
}
|
||||
|
||||
handleKeyboardShortcut = (event) => {
|
||||
console.log(event.keyCode)
|
||||
|
||||
switch(event.keyCode) {
|
||||
case KeyCode.Escape:
|
||||
this.setState({
|
||||
selectedChannel: null
|
||||
})
|
||||
break
|
||||
case KeyCode.Zero:
|
||||
this.setState({
|
||||
selectedChannel: 0
|
||||
});
|
||||
break
|
||||
case KeyCode.One:
|
||||
this.setState({
|
||||
selectedChannel: 1
|
||||
});
|
||||
break
|
||||
case KeyCode.Two:
|
||||
this.setState({
|
||||
selectedChannel: 2
|
||||
});
|
||||
break
|
||||
case KeyCode.Three:
|
||||
this.setState({
|
||||
selectedChannel: 3
|
||||
});
|
||||
break
|
||||
case KeyCode.Four:
|
||||
this.setState({
|
||||
selectedChannel: 4
|
||||
});
|
||||
break
|
||||
case KeyCode.Five:
|
||||
this.setState({
|
||||
selectedChannel: 5
|
||||
});
|
||||
break
|
||||
default:
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<BackgroundLayout>
|
||||
<div className="container mx-auto max-w-screen-xl py-10 flex flex-col space-y-5">
|
||||
{/* header content */}
|
||||
<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">👋Hey Arjun, Good Afternoon!</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'>Tues, Jan 11th</span>
|
||||
<span className='text-yellow-700 bg-yellow-200 p-1 rounded-md text-xs font-bold mt-2'>1:34am</span>
|
||||
</span>
|
||||
</span>
|
||||
|
||||
{/* avatar */}
|
||||
<span className="flex flex-row items-center space-x-5">
|
||||
{/* search bar */}
|
||||
<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>
|
||||
|
||||
<FaBell className='text-lg text-gray-400 hover:text-white ease-in-out duration-300 hover:scale-110 hover:cursor-pointer' />
|
||||
<FaHeadphonesAlt className='text-lg text-gray-400 hover:text-white ease-in-out duration-300 hover:scale-110 hover:cursor-pointer' />
|
||||
<FaMicrophoneAlt className='text-lg text-gray-400 hover:text-white ease-in-out duration-300 hover:scale-110 hover:cursor-pointer' />
|
||||
<FaTh className='text-lg text-gray-400 hover:text-white ease-in-out duration-300 hover:scale-110 hover:cursor-pointer' />
|
||||
|
||||
{/* avatar */}
|
||||
<span className='relative flex'>
|
||||
<span className="bg-gray-200 bg-opacity-30 rounded-full shadow-md absolute w-full h-full"></span>
|
||||
<span className="absolute top-0 right-0 w-3 h-3 bg-green-400 rounded-full"></span>
|
||||
<Image src="/avatars/svg/Artboards_Diversity_Avatars_by_Netguru-22.svg" alt="profile" width={50} height={50} />
|
||||
</span>
|
||||
</span>
|
||||
</section>
|
||||
|
||||
<div className='flex flex-row items-baseline space-x-5 space-y-5 flex-wrap'>
|
||||
{/* personal line */}
|
||||
<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'>
|
||||
<span className="text-white">TEAM</span>
|
||||
</span>
|
||||
|
||||
<button className='bg-gray-300 bg-opacity-25 p-2 ml-auto rounded hover:bg-opacity-40'>
|
||||
<FaPlus className='text-lg text-white' />
|
||||
</button>
|
||||
</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 " + (this.state.selectedChannel == i ? "bg-white bg-opacity-80 scale-105 shadow-2xl" : " ")}>
|
||||
<span className='relative flex mr-2'>
|
||||
<span className="bg-gray-200 bg-opacity-30 rounded-full shadow-md absolute w-full h-full"></span>
|
||||
|
||||
{this.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-white font-bold">{friend.name} </span>
|
||||
{
|
||||
this.state.selectedChannel == i ?
|
||||
<>
|
||||
<button className="rounded-lg bg-gray-400 shadow-lg text-center
|
||||
text-white text-sm py-1 px-2 font-bold">
|
||||
{i}
|
||||
</button>
|
||||
</>
|
||||
|
||||
:
|
||||
<button className="rounded-lg border py-1 px-2 border-gray-100 shadow-md
|
||||
text-center text-gray-200 text-smf font-bold">
|
||||
{i}
|
||||
</button>
|
||||
}
|
||||
</span>
|
||||
|
||||
|
||||
<span className={"text-xs span-sans text-gray-300" + (this.state.selectedChannel == i ? "text-black" : " ")}>{friend.role}</span>
|
||||
</span>
|
||||
|
||||
{this.renderPulse(friend.status)}
|
||||
</span>
|
||||
)
|
||||
})
|
||||
}
|
||||
</section>
|
||||
|
||||
<div className='flex-1 flex flex-col space-y-5 items-baseline'>
|
||||
{/* pinned */}
|
||||
<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-20'>
|
||||
<span className="text-white mr-auto">ANNOUNCEMENTS
|
||||
<button className="right-1 rounded-lg py-1 px-2 ml-1
|
||||
shadow-md text-center text-white text-sm font-bold">
|
||||
CTRL + A
|
||||
</button>
|
||||
</span>
|
||||
<span className='text-gray-300 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-white text-white hover:text-white hover:cursor-pointer'>Active</span>
|
||||
|
||||
<span className='text-gray-300 hover:text-white hover:cursor-pointer'>Resolved</span>
|
||||
</span>
|
||||
|
||||
<span className='text-sm text-gray-300 flex flex-row items-center'>
|
||||
TODAY <FaAngleDown />
|
||||
</span>
|
||||
|
||||
<button className='bg-gray-300 bg-opacity-25 p-2 ml-2 rounded hover:bg-opacity-40'>
|
||||
<FaMicrophoneAlt className='text-lg text-white' />
|
||||
</button>
|
||||
<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">
|
||||
{/* 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>
|
||||
|
||||
{this.statusBubble(UserStatus.busy)}
|
||||
|
||||
<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-bold text-white'>Adriana</span>
|
||||
<span className='text-xs text-gray-200'>5 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>
|
||||
</span>
|
||||
|
||||
{/* arjun'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>
|
||||
|
||||
{this.statusBubble(UserStatus.busy)}
|
||||
|
||||
<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'>Arjun</span>
|
||||
<span className='text-xs text-gray-200'>30 minutes 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>
|
||||
|
||||
<button className='bg-gray-300 bg-opacity-25 p-2 ml-2 rounded hover:bg-opacity-40'>
|
||||
<FaCheck className='text-lg text-white' />
|
||||
</button>
|
||||
</span>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* rooms */}
|
||||
<section className='p-5 flex flex-col bg-gray-100 bg-opacity-25 rounded-lg shadow-md'>
|
||||
<span className='flex flex-row justify-end space-x-3 pb-5 items-center'>
|
||||
<span className="text-white mr-auto">ROOMS
|
||||
<button className="right-1 rounded-lg py-1 px-2 ml-1
|
||||
shadow-md text-center text-white text-sm font-bold">
|
||||
CTRL + R
|
||||
</button>
|
||||
</span>
|
||||
|
||||
{/* tab pane */}
|
||||
<span className='flex flex-row space-x-5 uppercase mr-5'>
|
||||
<span className='underline underline-offset-8 decoration-white text-white hover:text-white hover:cursor-pointer'>All</span>
|
||||
|
||||
<span className='text-gray-300 hover:text-white hover:cursor-pointer'>Live</span>
|
||||
|
||||
<span className='text-gray-300 hover:text-white hover:cursor-pointer'>Scheduled</span>
|
||||
|
||||
<span className='text-gray-300 hover:text-white hover:cursor-pointer'>Recurring</span>
|
||||
</span>
|
||||
|
||||
<span className='text-sm text-gray-300 flex flex-row items-center uppercase'>
|
||||
Week <FaAngleDown />
|
||||
</span>
|
||||
|
||||
<button 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>
|
||||
|
||||
{/* all rooms */}
|
||||
<span className='flex flex-row flex-wrap'>
|
||||
{/* spontaneous bugs room */}
|
||||
<span className='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 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'>were just fixing that jsx bug thats a paiiinnnn</span>
|
||||
|
||||
{/* badges and tags */}
|
||||
<span className='flex flex-row flex-wrap'>
|
||||
<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='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-white font-semibold mr-auto'>Shopping Cart Experience</span>
|
||||
<span className='text-xs text-gray-200 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-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>
|
||||
<span className='text-gray-200 text-xs text-right mb-auto'>on and off all day</span>
|
||||
|
||||
{/* room attachments */}
|
||||
<span className='flex flex-row space-x-2'>
|
||||
<button className='bg-gray-300 bg-opacity-25 p-2 rounded hover:bg-opacity-40'>
|
||||
<FaLink className='text-sm text-white' />
|
||||
</button>
|
||||
|
||||
<button className='bg-gray-300 bg-opacity-25 p-2 rounded hover:bg-opacity-40'>
|
||||
<FaLink className='text-sm text-white' />
|
||||
</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>
|
||||
|
||||
{/* scheduled room - one on one */}
|
||||
<span className='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-white font-semibold mr-auto'>Arjun and Paul - One on one</span>
|
||||
<span className='text-xs text-gray-200 overflow-wrap'>performance review</span>
|
||||
|
||||
{/* badges and tags */}
|
||||
<span className='flex flex-row flex-wrap space-x-2'>
|
||||
<span className='text-xs my-3 text-white bg-gray-400 p-1 rounded-md font-bold flex flex-row space-x-2 items-center'>
|
||||
<span>private</span>
|
||||
</span>
|
||||
</span>
|
||||
</span>
|
||||
|
||||
{/* room status */}
|
||||
<span className='flex flex-col items-end'>
|
||||
<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-200 text-xs text-center'>3ish? Ill ping you Arjun</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>
|
||||
|
||||
<BsThreeDots className='text-white ml-2 hover:cursor-pointer' /> </span>
|
||||
</span>
|
||||
|
||||
{/* recurring room - dsu */}
|
||||
<span className='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-white font-semibold mr-auto'>
|
||||
Daily Standup
|
||||
</span>
|
||||
<span className='text-xs text-gray-200 overflow-wrap'></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 className='text-xs my-3 text-white bg-indigo-400 p-1 rounded-md font-bold flex flex-row space-x-2 items-center'>
|
||||
<span>engineering</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-200 text-xs'>10am daily</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 - demo */}
|
||||
<span className='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-white font-semibold mr-auto'>
|
||||
Sprint Demo
|
||||
</span>
|
||||
<span className='text-xs text-gray-200 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-200 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='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-white font-semibold mr-auto'>
|
||||
Wine Wednesdays
|
||||
</span>
|
||||
<span className='text-xs text-gray-200 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-200 text-xs text-right'>wednesdays 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>
|
||||
</span>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* attachments */}
|
||||
<section className='p-5 flex flex-col bg-gray-100 bg-opacity-25 rounded-lg shadow-md'>
|
||||
{/* header row */}
|
||||
<span className='flex flex-row justify-start pb-5 items-center'>
|
||||
<span className='flex flex-col mr-20'>
|
||||
<span className="text-white mr-auto">Attachments
|
||||
<button className="rounded-lg border py-1 px-2 ml-1 border-gray-100
|
||||
shadow-md text-center text-gray-200 text-sm font-bold">
|
||||
T
|
||||
</button>
|
||||
<button className="rounded-lg border py-1 px-2 ml-1 border-gray-100
|
||||
shadow-md text-center text-gray-200 text-sm font-bold">
|
||||
CTRL + V
|
||||
</button>
|
||||
</span>
|
||||
<span className='text-gray-300 text-xs'>code, links (jira tickets, drive folders, powerpoints), screenshots</span>
|
||||
</span>
|
||||
|
||||
{/* tab pane */}
|
||||
<span className='ml-auto flex flex-row space-x-5 uppercase mr-5'>
|
||||
<span className='underline underline-offset-8 decoration-white text-white hover:text-white hover:cursor-pointer'>Team</span>
|
||||
|
||||
<span className='text-gray-300 hover:text-white hover:cursor-pointer'>Personal</span>
|
||||
|
||||
<span className='text-gray-300 hover:text-white hover:cursor-pointer'>Favorites</span>
|
||||
</span>
|
||||
|
||||
<span className='text-sm mr-5 text-gray-300 flex flex-row items-center'>
|
||||
TODAY <FaAngleDown />
|
||||
</span>
|
||||
|
||||
<button className='bg-gray-300 bg-opacity-25 p-2 rounded hover:bg-opacity-40'>
|
||||
<FaCode className='text-lg text-white' />
|
||||
</button>
|
||||
|
||||
<button className='bg-gray-300 bg-opacity-25 p-2 ml-2 rounded hover:bg-opacity-40'>
|
||||
<FaLink className='text-lg text-white' />
|
||||
</button>
|
||||
|
||||
<BsThreeDots className='text-xl text-white ml-2' />
|
||||
</span>
|
||||
|
||||
{/* row of attachments */}
|
||||
<div className='flex flex-row flex-wrap space-x-2'>
|
||||
{/* pdf example */}
|
||||
<span className='flex flex-col rounded-lg'>
|
||||
{/* attmnt header */}
|
||||
<span className='flex flex-row bg-gray-300 bg-opacity-25 py-5 px-3 items-center justify-start'>
|
||||
<FaFilePdf className='text-4xl text-orange-300 mr-2' />
|
||||
|
||||
<span className='flex flex-col items-baseline mr-10 space-y-1'>
|
||||
<span className='text-md font-bold text-white'>report.pdf</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>
|
||||
|
||||
{/* attachment actions */}
|
||||
<button className='bg-gray-300 bg-opacity-25 p-2 ml-auto rounded hover:bg-opacity-40'>
|
||||
<FaCopy className='text-sm text-white' />
|
||||
</button>
|
||||
<button className='bg-gray-300 bg-opacity-25 p-2 ml-2 rounded hover:bg-opacity-40'>
|
||||
<FaExternalLinkAlt className='text-sm text-white' />
|
||||
</button>
|
||||
|
||||
<button className='bg-orange-300 bg-opacity-25 p-2 ml-2 rounded hover:bg-opacity-40'>
|
||||
<FaArchive className='text-sm text-orange-500 ' />
|
||||
</button>
|
||||
</span>
|
||||
|
||||
{/* attmnt 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='flex flex-col items-baseline'>
|
||||
<span className='text-xs text-gray-200 font-bold'>Adriana</span>
|
||||
<span className='text-xs text-gray-200 font-extralight'>5 minutes ago</span>
|
||||
</span>
|
||||
|
||||
<BsThreeDots className='text-white ml-auto hover:cursor-pointer' />
|
||||
</span>
|
||||
</span>
|
||||
|
||||
{/* jira example */}
|
||||
<span className='flex flex-col rounded-lg'>
|
||||
{/* attmnt header */}
|
||||
<span className='flex flex-row bg-gray-300 bg-opacity-25 py-5 px-3 items-center justify-start'>
|
||||
<FaAtlassian className='text-4xl text-sky-300 mr-2' />
|
||||
|
||||
<span className='flex flex-col items-baseline mr-10 space-y-1'>
|
||||
<span className='text-md font-bold text-white'>RAM-22</span>
|
||||
<span className='text-xs text-gray-200'>technical doc - stripe</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>
|
||||
|
||||
{/* attachment actions */}
|
||||
<button className='bg-gray-300 bg-opacity-25 p-2 ml-auto rounded hover:bg-opacity-40'>
|
||||
<FaCopy className='text-sm text-white' />
|
||||
</button>
|
||||
<button className='bg-gray-300 bg-opacity-25 p-2 ml-2 rounded hover:bg-opacity-40'>
|
||||
<FaExternalLinkAlt className='text-sm text-white' />
|
||||
</button>
|
||||
|
||||
<button className='bg-orange-300 bg-opacity-25 p-2 ml-2 rounded hover:bg-opacity-40'>
|
||||
<FaArchive className='text-sm text-orange-500' />
|
||||
</button>
|
||||
</span>
|
||||
|
||||
{/* attmnt 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-43.svg"} alt="profile" width={30} height={30} />
|
||||
</span>
|
||||
</span>
|
||||
|
||||
<span className='flex flex-col items-baseline'>
|
||||
<span className='text-xs text-gray-200 font-bold'>Paul</span>
|
||||
<span className='text-xs text-gray-200 font-extralight'>2 hours ago</span>
|
||||
</span>
|
||||
|
||||
<BsThreeDots className='text-white ml-auto hover:cursor-pointer' />
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<div className='fixed bottom-10 w-full -z-10'>
|
||||
{/* dynamic footer based on selection */}
|
||||
<section className={"flex flex-row bg-gray-300 max-w-screen-md mx-auto p-5 shadow-xl rounded-xl justify-between ease-in-out duration-300 scale-0" + (this.state.selectedChannel != null ? "scale-100" : " ")}>
|
||||
<span className='flex flex-row'>
|
||||
<span className='relative flex mr-2'>
|
||||
<span className="bg-gray-200 rounded-full shadow-md absolute w-full h-full"></span>
|
||||
|
||||
{this.statusBubble(UserStatus.online)}
|
||||
|
||||
<Image src={"/avatars/svg/Artboards_Diversity_Avatars_by_Netguru-" + (this.state.selectedChannel == null ? "" : testFriends[this.state.selectedChannel].systemAvatar) + ".svg"} alt="profile" width={50} height={50} />
|
||||
</span>
|
||||
|
||||
<span className='flex flex-col'>
|
||||
<span className="text-sm text-black font-bold">{this.state.selectedChannel == null ? "" : testFriends[this.state.selectedChannel].name} </span>
|
||||
|
||||
<span className={"text-xs text-gray-300"}>{this.state.selectedChannel == null ? "" : testFriends[this.state.selectedChannel].role}</span>
|
||||
</span>
|
||||
</span>
|
||||
|
||||
{/* shortcuts */}
|
||||
<span className='flex flex-row space-x-3'>
|
||||
<span className="rounded-lg p-2 ml-auto bg-gray-400 shadow-lg w-10 h-10 text-center animate-pulse">
|
||||
<span className="text-white text-sm font-bold">R</span>
|
||||
</span>
|
||||
<span className="rounded-lg p-2 ml-auto bg-gray-400 shadow-lg w-20 h-10 text-center animate-pulse">
|
||||
<span className="text-white text-sm font-bold">SPACE</span>
|
||||
</span>
|
||||
</span>
|
||||
</section>
|
||||
</div>
|
||||
</BackgroundLayout>
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,240 @@
|
||||
import { useAuth } from "../../contexts/authContext";
|
||||
import { useRouter } from "next/router";
|
||||
import { useEffect, useState } from "react";
|
||||
import UserService from "../../services/userService";
|
||||
import { User } from "../../models/user";
|
||||
import { GetServerSidePropsContext } from "next";
|
||||
import firebaseAdmin from "firebase-admin";
|
||||
import {
|
||||
FaPeopleCarry,
|
||||
FaArrowRight,
|
||||
FaBullhorn,
|
||||
FaPlus,
|
||||
} from "react-icons/fa";
|
||||
import BackgroundLayout from "../../components/Layouts/BackgroundLayout";
|
||||
import TeamService from "../../services/teamService";
|
||||
import Loading from "../../components/Loading";
|
||||
import { TeamMemberStatus } from "../../models/teamMember";
|
||||
import { Divider, Tooltip } from "antd";
|
||||
import { Team } from "../../models/team";
|
||||
import moment from "moment";
|
||||
|
||||
/**
|
||||
* figure out where to take the user based on everything
|
||||
*/
|
||||
|
||||
const userService: UserService = new UserService();
|
||||
const teamService: TeamService = new TeamService();
|
||||
|
||||
function RouteHandler() {
|
||||
const { currUser } = useAuth();
|
||||
const router = useRouter();
|
||||
|
||||
const [loading, setLoading] = useState<Boolean>(true);
|
||||
const [user, setUser] = useState<User | null>(null);
|
||||
const [teams, setTeams] = useState<Team[]>([]);
|
||||
|
||||
useEffect(() => {
|
||||
(async function () {
|
||||
try {
|
||||
// if not authenticated, take user to the login
|
||||
if (!currUser) {
|
||||
console.log(
|
||||
"not authenticated...routing from dashboard to teams home"
|
||||
);
|
||||
router.push("/teams/login");
|
||||
return;
|
||||
}
|
||||
|
||||
// get user
|
||||
const returnedUser: User | null = await userService.getUser(
|
||||
currUser.uid
|
||||
);
|
||||
|
||||
// if user has no profile, then go to create profile
|
||||
if (
|
||||
!returnedUser ||
|
||||
!returnedUser.firstName ||
|
||||
!returnedUser.lastName ||
|
||||
!returnedUser.nickName
|
||||
) {
|
||||
console.log("no profile for the user, routing him/her there");
|
||||
router.push("/teams/profile");
|
||||
return;
|
||||
}
|
||||
setUser(returnedUser);
|
||||
|
||||
// get all teams that this user id is active in
|
||||
// get all of the teams that this user's email is invited to
|
||||
const teams: Team[] = await teamService.getActiveOrInvitedTeamsbyUser(
|
||||
currUser.uid,
|
||||
returnedUser.emailAddress
|
||||
);
|
||||
|
||||
setTeams(teams);
|
||||
|
||||
setLoading(false);
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
router.push("/teams/login");
|
||||
}
|
||||
})();
|
||||
}, []);
|
||||
|
||||
if (loading || !user) {
|
||||
return <Loading />;
|
||||
}
|
||||
|
||||
// not in a team yet, and have not created a team yet
|
||||
return (
|
||||
<div className="container mx-auto flex flex-col max-w-screen-md m-10 bg-white p-10 rounded-lg shadow-md space-y-5">
|
||||
{/* header */}
|
||||
<div className="flex flex-row items-center justify-between">
|
||||
<span className="flex flex-col justify-start">
|
||||
<div className="text-lg">👋Hey, {user.firstName}</div>
|
||||
<span className="text-gray-300 text-md">
|
||||
Let's get you started.
|
||||
</span>
|
||||
</span>
|
||||
|
||||
<Tooltip title={"Click to Edit Profile"}>
|
||||
<button onClick={() => router.push("/teams/profile")}>
|
||||
<img
|
||||
src={user ? user.avatarUrl : currUser.photoURL}
|
||||
alt="avatar"
|
||||
className="rounded-full w-12 shadow-lg"
|
||||
/>
|
||||
</button>
|
||||
</Tooltip>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-row items-center">
|
||||
<span className="text-gray-400">Your Teams</span>
|
||||
<Tooltip title={"Add members and get started immediately!"}>
|
||||
<button
|
||||
onClick={() => window.open("/teams/create", "_self")}
|
||||
className="ml-auto rounded text-xs font-semibold bg-gray-200 p-2 text-teal-600 shadow-lg flex flex-row items-center space-x-2"
|
||||
>
|
||||
<FaPlus />
|
||||
<span>Create</span>
|
||||
</button>
|
||||
</Tooltip>
|
||||
</div>
|
||||
|
||||
{/* all teams part of */}
|
||||
<div className="flex flex-row flex-wrap">
|
||||
{teams.map((team) => {
|
||||
console.log(team);
|
||||
return (
|
||||
<span
|
||||
key={team.id}
|
||||
className="group hover:bg-teal-600 hover:bg-opacity-10 transition-all
|
||||
p-5 rounded bg-gray-200 bg-opacity-20 flex flex-row justify-between items-center w-[20rem] m-2"
|
||||
>
|
||||
<span className="flex flex-col mr-10 items-start">
|
||||
<span className="flex flex-row items-center space-x-2">
|
||||
<span className="text-lg text-teal-600 group-hover:font-semibold transition-all">
|
||||
{team.name}
|
||||
</span>
|
||||
<Tooltip title={"Number of pro spots purchased"}>
|
||||
<span className="bg-teal-600 bg-opacity-50 text-xs rounded-full w-5 h-5 flex items-center justify-evenly text-white shrink-0">
|
||||
{team.allowedUserCount || 2}
|
||||
</span>
|
||||
</Tooltip>
|
||||
</span>
|
||||
|
||||
{team.companySite && (
|
||||
<span
|
||||
onClick={() => window.open(team.companySite, "_blank")}
|
||||
className="text-xs hover:cursor-pointer font-semibold text-gray-500"
|
||||
>
|
||||
{team.companySite}
|
||||
</span>
|
||||
)}
|
||||
|
||||
<span className="text-xs text-gray-300">
|
||||
{"created: " + moment(team.createdDate.toDate()).fromNow()}
|
||||
</span>
|
||||
</span>
|
||||
|
||||
<Tooltip title={"Go to " + team.name}>
|
||||
<span
|
||||
onClick={() => window.open("/teams/" + team.id, "_blank")}
|
||||
className="ml-auto hover:cursor-pointer transition-all bg-gray-500 bg-opacity-25 p-2 rounded group-hover:bg-opacity-40 group-hover:bg-teal-500"
|
||||
>
|
||||
<FaArrowRight className="text-sm text-white" />
|
||||
</span>
|
||||
</Tooltip>
|
||||
</span>
|
||||
);
|
||||
})}
|
||||
|
||||
{teams.length == 0 ? (
|
||||
<button className="group flex flex-row py-5 px-3 items-center border border-dashed rounded">
|
||||
<FaBullhorn className="text-5xl text-orange-300" />
|
||||
|
||||
<span className="flex flex-col items-start ml-2 mr-10">
|
||||
<span className="text-gray-500">Remind Your Manager</span>
|
||||
<span className="text-gray-300 text-xs text-left">
|
||||
Your account email is {user.emailAddress}
|
||||
</span>
|
||||
</span>
|
||||
</button>
|
||||
) : (
|
||||
""
|
||||
)}
|
||||
</div>
|
||||
|
||||
<span className="text-gray-300 ml-auto text-md mt-10">
|
||||
or learn more about{" "}
|
||||
<button
|
||||
onClick={() => window.open("/", "_self")}
|
||||
className="underline font-satisfy text-xl text-teal-500 decoration-teal-500"
|
||||
>
|
||||
nirvana
|
||||
</button>
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export async function getServerSideProps(context: GetServerSidePropsContext) {
|
||||
// const cookies = nookies.get(context);
|
||||
|
||||
// var user: User | null = null
|
||||
|
||||
// if (cookies.token) {
|
||||
// try {
|
||||
// const headers: HeadersInit = {
|
||||
// 'Content-Type': 'application/json',
|
||||
// Authorization: JSON.stringify({ token: cookies.token })
|
||||
// };
|
||||
// const result = await unfetch('/api/auth/validateToken', { headers });
|
||||
// console.log(result)
|
||||
|
||||
// // get user data
|
||||
// const userService: UserService = new UserService()
|
||||
|
||||
// // the user is authenticated!
|
||||
|
||||
// // FETCH STUFF HERE!! 🚀
|
||||
|
||||
// // user = await userService.getUser("asdf")
|
||||
// } catch (e) {
|
||||
// // let exceptions fail silently
|
||||
// // could be invalid token, just let client-side deal with that
|
||||
|
||||
// console.log(e)
|
||||
// }
|
||||
// }
|
||||
|
||||
// // Pass data to the page via props
|
||||
// return { props: {
|
||||
// user
|
||||
// }
|
||||
// }
|
||||
|
||||
return { props: {} };
|
||||
}
|
||||
|
||||
export default RouteHandler;
|
||||
@@ -0,0 +1,80 @@
|
||||
import Head from 'next/head';
|
||||
import Image from 'next/image';
|
||||
import Link from 'next/link';
|
||||
|
||||
export default function Landing() {
|
||||
return (
|
||||
<div className='container m-20'>
|
||||
<Head>
|
||||
<meta charSet="UTF-8" />
|
||||
|
||||
<title>nirvana for teams</title>
|
||||
|
||||
<link rel="icon" href="/icon.png" />
|
||||
|
||||
<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"
|
||||
/>
|
||||
</Head>
|
||||
|
||||
<main className="space-y-10">
|
||||
<p id="main-title">nirvana</p>
|
||||
|
||||
<p>🍃voice first collaboration for remote teams</p>
|
||||
|
||||
<p>
|
||||
tired of spending most of the work day on{' '}
|
||||
<font className="text-gray-300 text-3xl">slack, <font className="text-2xl">zoom,</font> <font className="text-xl">ms teams,</font> <font className="text-lg">trello,</font> <font className="text-base">outlook,</font> <font className="text-base">gcal, </font><font className="text-sm">notion,</font> <font className="text-xs">gmail...</font>?</font>
|
||||
</p>
|
||||
|
||||
<p>
|
||||
focus on actually working and:
|
||||
<br />
|
||||
- cut the texting, emojis, and notifications
|
||||
<br />
|
||||
- increase collaboration without endless threads
|
||||
<br />
|
||||
- avoid 80% of meetings with asynchronous and spontaneous convos
|
||||
<br />
|
||||
- form better relationships with your team
|
||||
<br />
|
||||
</p>
|
||||
|
||||
<p>
|
||||
wanna test the beta? text us @ <font className="text-gray-300">949.237.2715</font>
|
||||
</p>
|
||||
|
||||
{/* <marquee
|
||||
direction="left"
|
||||
width="100%"
|
||||
height="100%"
|
||||
behavior="scroll"
|
||||
scrollamount="12"
|
||||
offset="0%"
|
||||
>
|
||||
<p id="marquee-text">waitlist currently sitting at 2,324 people...</p>
|
||||
</marquee> */}
|
||||
|
||||
<p id="side-note">
|
||||
why? we live in a distracted world. let's fix it, because less is more.
|
||||
</p>
|
||||
|
||||
<Link exact href='/teams/login' className='text-3xl text-white'>
|
||||
<button className='mt-10 text-sm text-emerald-500 font-semibold py-1 px-4 bg-gray-200 rounded'>👋 Login</button>
|
||||
</Link>
|
||||
</main>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
import { useRouter } from "next/router";
|
||||
import { useEffect } from "react";
|
||||
import { FaArrowLeft } from "react-icons/fa";
|
||||
import { FcGoogle } from "react-icons/fc";
|
||||
import MainLogo from "../../components/MainLogo";
|
||||
import { useAuth } from "../../contexts/authContext";
|
||||
|
||||
export default function Login() {
|
||||
const { currUser, signInGoogle } = useAuth();
|
||||
const router = useRouter();
|
||||
|
||||
useEffect(() => {
|
||||
// if auth, go to dashboard
|
||||
if (currUser) {
|
||||
console.log("already logged in, redirecting to router");
|
||||
router.push("/teams");
|
||||
}
|
||||
}, [currUser]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="h-screen w-screen flex flex-row">
|
||||
{/* signin modal */}
|
||||
<div className="flex-1 bg-white bg-opacity-80 p-10 rounded-lg shadow-lg flex flex-col items-start justify-start">
|
||||
{/* header */}
|
||||
<div className="text-lg flex flex-row items-center space-x-2">
|
||||
<FaArrowLeft
|
||||
className="hover:cursor-pointer"
|
||||
onClick={() => window.open("/", "_self")}
|
||||
/>{" "}
|
||||
</div>
|
||||
|
||||
<div className="mx-auto my-auto flex flex-col items-center">
|
||||
<button
|
||||
onClick={signInGoogle}
|
||||
className=" text-md text-sky-500 py-2 px-5 border border-gray-200 transition-all hover:bg-gray-200 rounded flex flex-row items-center space-x-2"
|
||||
>
|
||||
<FcGoogle className="text-lg" />
|
||||
<span>Continue with Google</span>
|
||||
</button>
|
||||
<span className="text-xs text-center mt-2 text-gray-300">
|
||||
By continuing, you are agreeing <br></br>
|
||||
to the{" "}
|
||||
<a href="https://docs.google.com/document/d/1NRWN-6kDyOcADaUAQ-YWVHnz6i9ccGJ3/edit?usp=sharing&ouid=113470786690353109086&rtpof=true&sd=true">
|
||||
terms and conditions
|
||||
</a>{" "}
|
||||
and{" "}
|
||||
<a href="https://docs.google.com/document/d/1S3JsGqXgkriAsBOybXpVR0JJ4hiRkaNt/edit?usp=sharing&ouid=113470786690353109086&rtpof=true&sd=true">
|
||||
privacy policy.
|
||||
</a>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<span className="mx-auto flex items-center space-x-1">
|
||||
<span>©</span>
|
||||
<MainLogo className=" text-2xl" />
|
||||
</span>
|
||||
</div>
|
||||
{/* nice image on right side */}
|
||||
<div
|
||||
className="hidden md:block landing-page-bg bg-cover bg-no-repeat bg-center flex-1"
|
||||
style={
|
||||
{
|
||||
// background: "url('/wallpapers/superhuman.jpg')",
|
||||
}
|
||||
}
|
||||
></div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,241 @@
|
||||
import { Divider } from "antd";
|
||||
import { useRouter } from "next/router";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useAuth } from "../../contexts/authContext";
|
||||
import { User } from "../../models/user";
|
||||
import UserService from "../../services/userService";
|
||||
import Image from "next/image";
|
||||
|
||||
export default function Profile() {
|
||||
const { currUser, logOut } = useAuth();
|
||||
const [user, setUser] = useState<User | null>(null);
|
||||
const [loading, setLoading] = useState<boolean>(false);
|
||||
|
||||
const router = useRouter();
|
||||
const userService: UserService = new UserService();
|
||||
|
||||
useEffect(() => {
|
||||
(async function () {
|
||||
try {
|
||||
// if not authenticated, take user to the login
|
||||
if (!currUser) {
|
||||
console.log(
|
||||
"not authenticated...routing from dashboard to teams home"
|
||||
);
|
||||
// router.push('/teams/login')
|
||||
}
|
||||
|
||||
// get user
|
||||
const returnedUser: User | null = await userService.getUser(
|
||||
currUser.uid
|
||||
);
|
||||
|
||||
// if the user is null, then go ahead and create the user even though it's not there yet
|
||||
if (!returnedUser) {
|
||||
userService.createUser(
|
||||
currUser.uid,
|
||||
currUser.email,
|
||||
currUser.photoURL
|
||||
);
|
||||
}
|
||||
|
||||
console.log("got user from the backend");
|
||||
|
||||
setUser(returnedUser);
|
||||
setNickname(returnedUser.nickName ?? "");
|
||||
setFirstName(
|
||||
returnedUser.firstName ?? currUser.displayName.split(" ")[0]
|
||||
);
|
||||
setLastName(returnedUser.lastName ?? "");
|
||||
setTeamRole(returnedUser.teamRole ?? "");
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
router.push("/teams/login");
|
||||
}
|
||||
})();
|
||||
}, []);
|
||||
|
||||
async function handleSubmit(e) {
|
||||
setLoading(true);
|
||||
|
||||
e.preventDefault();
|
||||
|
||||
if (!firstName) {
|
||||
setError("Must input first name");
|
||||
return;
|
||||
} else if (!lastName) {
|
||||
setError("Must input last name");
|
||||
return;
|
||||
} else if (!nickname) {
|
||||
setError("Must input nickname");
|
||||
return;
|
||||
} else if (nickname.length > 22) {
|
||||
setError("nickname must be less than 22 characters");
|
||||
return;
|
||||
} else if (!teamRole) {
|
||||
setError("Must input team role!");
|
||||
return;
|
||||
}
|
||||
|
||||
// create the user in the backend or just merge results
|
||||
let newUser = new User();
|
||||
newUser.id = currUser.uid;
|
||||
newUser.firstName = firstName;
|
||||
newUser.lastName = lastName;
|
||||
newUser.nickName = nickname;
|
||||
newUser.avatarUrl = currUser.photoURL;
|
||||
newUser.teamRole = teamRole;
|
||||
|
||||
try {
|
||||
await userService.updateUser(newUser);
|
||||
router.push("/teams");
|
||||
} catch (error) {
|
||||
setError(
|
||||
"Something went wrong in saving your information. Please try again."
|
||||
);
|
||||
}
|
||||
|
||||
setLoading(false);
|
||||
}
|
||||
|
||||
const [firstName, setFirstName] = useState<string>(
|
||||
currUser.displayName.split(" ")[0]
|
||||
);
|
||||
const [lastName, setLastName] = useState<string>("");
|
||||
const [nickname, setNickname] = useState<string>("");
|
||||
const [teamRole, setTeamRole] = useState<string>("");
|
||||
const [error, setError] = useState<string>("");
|
||||
|
||||
return (
|
||||
<form
|
||||
className="container mx-auto max-w-screen-sm flex flex-col m-10 bg-white p-10 rounded-lg shadow-md space-y-5"
|
||||
onSubmit={handleSubmit}
|
||||
>
|
||||
{/* header */}
|
||||
<div className="text-lg">Profile</div>
|
||||
|
||||
{error && <span className="text-red-300 text-md">{error}</span>}
|
||||
|
||||
<Divider />
|
||||
|
||||
<span className="flex flex-col items-start">
|
||||
<span className="text-md">Avatar*</span>
|
||||
<span className="text-gray-300 text-xs">
|
||||
Please change your google image to change this.
|
||||
</span>
|
||||
<img
|
||||
src={user ? user.avatarUrl : currUser.photoURL}
|
||||
alt="asdf"
|
||||
className="rounded-lg mt-2"
|
||||
/>
|
||||
</span>
|
||||
|
||||
<span className="flex flex-col items-start">
|
||||
<span className="text-md">Email*</span>
|
||||
<span className="text-gray-300 text-xs mb-2">
|
||||
This is set from your Google account and cannot be changed.
|
||||
</span>
|
||||
<input
|
||||
disabled
|
||||
className="w-full rounded-lg bg-gray-100 p-3"
|
||||
value={user ? user.emailAddress : currUser.email}
|
||||
readOnly
|
||||
/>
|
||||
</span>
|
||||
|
||||
<div className="flex flex-row justify-between space-x-3">
|
||||
<span className="flex flex-col items-start flex-1">
|
||||
<span className="text-md">First Name*</span>
|
||||
<span className="text-gray-300 text-xs mb-2"></span>
|
||||
<input
|
||||
placeholder="ex. John"
|
||||
className="w-full rounded-lg bg-gray-50 p-3"
|
||||
value={firstName}
|
||||
onChange={(e) => setFirstName(e.target.value)}
|
||||
/>
|
||||
</span>
|
||||
|
||||
<span className="flex flex-col items-start flex-1">
|
||||
<span className="text-md">Last Name*</span>
|
||||
<span className="text-gray-300 text-xs mb-2"></span>
|
||||
<input
|
||||
placeholder="ex. Brown"
|
||||
className="w-full rounded-lg bg-gray-50 p-3"
|
||||
value={lastName}
|
||||
onChange={(e) => setLastName(e.target.value)}
|
||||
/>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<Divider />
|
||||
|
||||
<span className="flex flex-col items-start">
|
||||
<span className="text-md">Nickname*</span>
|
||||
<span className="text-gray-300 text-xs mb-2">
|
||||
What does your team call you? This is what is displayed.
|
||||
</span>
|
||||
<input
|
||||
placeholder="ex. nicky"
|
||||
className="w-full rounded-lg bg-gray-50 p-3"
|
||||
value={nickname}
|
||||
onChange={(e) => setNickname(e.target.value)}
|
||||
/>
|
||||
</span>
|
||||
|
||||
<span className="flex flex-col items-start">
|
||||
<span className="text-md">Role*</span>
|
||||
<span className="text-gray-300 text-xs mb-2">
|
||||
Are you a developer? designer? manager?
|
||||
</span>
|
||||
<input
|
||||
placeholder="ex. designer"
|
||||
className="w-full rounded-lg bg-gray-50 p-3"
|
||||
value={teamRole}
|
||||
onChange={(e) => setTeamRole(e.target.value)}
|
||||
/>
|
||||
</span>
|
||||
|
||||
<Divider />
|
||||
|
||||
<span className="flex flex-row justify-end space-x-2">
|
||||
{/* if they already had a nickname, they are probably revisiting this page */}
|
||||
{/* can't cancel if they are a new user */}
|
||||
{loading ? (
|
||||
<svg
|
||||
className="animate-spin h-5 w-5 mr-3 border rounded-full"
|
||||
viewBox="0 0 24 24"
|
||||
></svg>
|
||||
) : (
|
||||
<>
|
||||
{user && user.nickName ? (
|
||||
<>
|
||||
<button
|
||||
onClick={() => router.push("/teams")}
|
||||
className="bg-gray-100 py-2 px-5 rounded text-gray-400"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
type="submit"
|
||||
className="text-sm text-white font-semibold py-2 px-5 bg-teal-500 rounded"
|
||||
>
|
||||
Save
|
||||
</button>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
{/* if they already had a nickname, they are probably revisiting this page */}
|
||||
<button
|
||||
type="submit"
|
||||
className="text-sm text-white font-semibold py-2 px-5 bg-teal-500 rounded"
|
||||
>
|
||||
{"Continue ->"}
|
||||
</button>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</span>
|
||||
</form>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
module.exports = {
|
||||
plugins: {
|
||||
tailwindcss: {},
|
||||
autoprefixer: {},
|
||||
},
|
||||
}
|
||||
|
After Width: | Height: | Size: 22 KiB |
|
After Width: | Height: | Size: 19 KiB |
|
After Width: | Height: | Size: 12 KiB |
|
After Width: | Height: | Size: 14 KiB |
|
After Width: | Height: | Size: 11 KiB |
|
After Width: | Height: | Size: 10 KiB |
|
After Width: | Height: | Size: 12 KiB |
|
After Width: | Height: | Size: 9.3 KiB |
|
After Width: | Height: | Size: 20 KiB |
|
After Width: | Height: | Size: 8.8 KiB |
|
After Width: | Height: | Size: 22 KiB |
|
After Width: | Height: | Size: 10 KiB |
|
After Width: | Height: | Size: 14 KiB |
|
After Width: | Height: | Size: 15 KiB |
|
After Width: | Height: | Size: 10 KiB |
|
After Width: | Height: | Size: 8.9 KiB |
|
After Width: | Height: | Size: 17 KiB |
|
After Width: | Height: | Size: 11 KiB |
|
After Width: | Height: | Size: 11 KiB |
|
After Width: | Height: | Size: 9.0 KiB |
|
After Width: | Height: | Size: 16 KiB |
|
After Width: | Height: | Size: 15 KiB |
|
After Width: | Height: | Size: 9.5 KiB |
|
After Width: | Height: | Size: 9.7 KiB |
|
After Width: | Height: | Size: 10 KiB |
|
After Width: | Height: | Size: 15 KiB |