diff --git a/src/components/SideBar/SideBar.tsx b/src/components/SideBar/SideBar.tsx index 17c24fc..10e8793 100644 --- a/src/components/SideBar/SideBar.tsx +++ b/src/components/SideBar/SideBar.tsx @@ -2,6 +2,45 @@ import React from "react"; import { Link } from "react-router-dom"; import { Teams } from "./Teams"; import { styled } from "@mui/material/styles"; +import { useTenantMap } from "../../hooks/users"; +import { assignColor, findPinnedByPath, getOrgs, orderBy } from "../../helpers"; +import { usePinned } from "../../services/PinnedProvider"; + +const RenderOrganizations = () => { + const { isLoading, isError, data: tmap } = useTenantMap(); + const { pinned } = usePinned(); + + if (isLoading || isError || !tmap) { + return
; + } + + const orgs = orderBy("company", getOrgs(tmap)).map(assignColor); + + return ( + <> + {orgs.map(({ name, path, color }, index) => { + const pinnedProjects = findPinnedByPath(pinned, path); + return ( +
  • + + +

    {name}

    +
    + + {(pinnedProjects || []).map((project, index) => ( + + {project.name} + + ))} +
  • + ); + })} + + ); +}; export const SideBar = () => { return ( @@ -14,11 +53,12 @@ export const SideBar = () => { -
  • +
  • Projects
  • +
    @@ -28,7 +68,7 @@ export const SideBar = () => { const StyledSideBar = styled("div")` background: var(--white1); height: calc(100vh + 48px); - width: var(--p256); + width: var(--p300); font-size: var(--p18); position: relative; ul { @@ -38,12 +78,44 @@ const StyledSideBar = styled("div")` display: none; } `; + +const OrgLink = styled(Link)` + display: flex; + flex-direction: row; + align-items: center; + font-family: ProximaNova-Semibold; + font-size: var(--p16); + color: var(--gray7); + text-decoration: none; + + aside { + height: 30px; + width: 30px; + margin-right: var(--p14); + } +`; + +const ProjectLink = styled(Link)` + margin-left: 44px; + margin-top: 14px; + display: block; + text-decoration: none; + color: var(--gray7); + font-family: ProximaNova-Light; +`; + const PrimaryNav = styled("ul")` list-style: none; padding: 0; li { padding: var(--p12) var(--p24); } + li.bottom { + margin-bottom: 52px; + } + p { + margin: 0; + } `; const Container = styled("div")` padding: var(--p16) var(--p24); @@ -58,7 +130,9 @@ const Logo = styled("img")` const StyledLink = styled(Link)` text-decoration: none; cursor: pointer; - color: var(--gray3); + color: var(--gray6); + font-family: ProximaNova-Semibold; + font-size: var(--p20); padding-top: var(--p8); &:hover { color: var(--gray5); diff --git a/src/features/Projects/Card/Card.tsx b/src/features/Projects/Card/Card.tsx index b5cbece..65ac6ba 100644 --- a/src/features/Projects/Card/Card.tsx +++ b/src/features/Projects/Card/Card.tsx @@ -1,4 +1,4 @@ -import React from "react"; +import React, { useState } from "react"; import { Link } from "react-router-dom"; import { Project } from "../../Project/types"; import { styled } from "@mui/material/styles"; @@ -7,33 +7,90 @@ import { Memberships } from "../../Project/types"; import { Avatar } from "../../../components/Avatar"; import { useTenantMap } from "../../../hooks/users"; import { Loader } from "../../../components/Loader"; +import PushPinIcon from "@mui/icons-material/PushPin"; +import { + assignColor, + getOrgs, + getPinState, + PinnedProject, + orderBy, +} from "../../../helpers"; +import IconButton from "@mui/material/IconButton"; +import { usePinned } from "../../../services/PinnedProvider"; interface Props { project: Project; - seq: number; } -export const Card = ({ project, seq }: Props) => { +export const Card = ({ project }: Props) => { + const [pinState, setPinState] = useState(getPinState(project)); const { data: memberships } = useTeamMemberships(project.teamId); const { data: team } = useTeam(project.teamId); - const { isLoading, isError, data } = useTenantMap(); + const { isLoading, isError, data: tmap } = useTenantMap(); + const { pinned, setPinned } = usePinned(); - const color = (seq % 9) + 1; - - if (isLoading || isError || data === undefined) { + if (isLoading || isError || !tmap) { return ; } + const orgs = orderBy("company", getOrgs(tmap)).map(assignColor); + const org = (orgs || []).find((org) => org.id === project.tenantId); + + const handlePin = (e: React.MouseEvent) => { + e.preventDefault(); + if (!org) { + return; + } + + const isPinned = !!pinned.find( + (p: PinnedProject) => p.projectId === project.id + ); + + if (isPinned) { + const updatedPins = pinned.filter((p) => p.projectId != project.id); + localStorage.setItem("settings.pinned", JSON.stringify(updatedPins)); + setPinned(updatedPins); + setPinState(false); + } else { + const updatedPins = [ + ...pinned, + { + name: project.name, + projectId: project.id, + tenantPath: org.path, + }, + ]; + + localStorage.setItem("settings.pinned", JSON.stringify(updatedPins)); + setPinned(updatedPins); + setPinState(true); + } + }; + + const path = tmap[project.tenantId].path; + const link = `/${path}/projects/${project.id}`; + + if (!org) { + return <>; + } + return ( - -
    + +
    - {project.name} - Developed by {team ? team.name : "Me"} +
    + {project.name} + Developed by {org.name} +
    +
    + handlePin(e)}> + + +
    {project.description} @@ -116,6 +173,10 @@ const CardHeader = styled("div")` padding: var(--p16); box-sizing: border-box; border-bottom: 1px solid var(--gray1); + display: flex; + justify-content: space-between; + .header { + } `; const CardBody = styled("div")` display: flex; diff --git a/src/features/Projects/List/List.tsx b/src/features/Projects/List/List.tsx index 8cc3673..e46bd8c 100644 --- a/src/features/Projects/List/List.tsx +++ b/src/features/Projects/List/List.tsx @@ -3,21 +3,18 @@ import { CircularProgress, Grid } from "@mui/material"; import { Card } from "../Card"; import { styled } from "@mui/material/styles"; import { Project } from "../../Project/types"; -import { orderBy } from "../helpers"; - -interface Props { - isLoading: boolean; - projects: Project[] | undefined; -} +import { orderBy } from "../../../helpers"; +import { useProjects } from "../../../hooks/project"; const renderProjectCards = (projects: Project[]) => { - const seq = projects.map((_, idx) => idx); - return orderBy(projects, "name").map((project: Project, index: number) => ( - + return orderBy("name", projects).map((project: Project) => ( + )); }; -export const List = ({ isLoading, projects }: Props) => { +export const List = () => { + const { isLoading, data: projects } = useProjects(); + if (isLoading) { return ( diff --git a/src/features/Projects/Projects.tsx b/src/features/Projects/Projects.tsx index 1e069a0..bd02f5d 100644 --- a/src/features/Projects/Projects.tsx +++ b/src/features/Projects/Projects.tsx @@ -1,4 +1,4 @@ -import React, { useState } from "react"; +import React, { useEffect, useState } from "react"; import Grid from "@mui/material/Grid"; import Fab from "@mui/material/Fab"; import Add from "@mui/icons-material/Add"; @@ -10,8 +10,6 @@ import { Layout } from "../../Layout"; export const Projects = () => { const [isOpen, setOpen] = useState(false); - const projects = useProjects(); - const [createProject] = useCreateProject(); const toggleModal = () => { @@ -28,7 +26,7 @@ export const Projects = () => { - + { - return values.sort((a, b) => { - let fieldA, fieldB; - - try { - // Assume it's a string. - fieldA = a[key].toLowerCase(); - fieldB = b[key].toLowerCase(); - } catch { - fieldA = a[key]; - fieldB = b[key]; - } - - if (sortBy === "desc") { - return sortByDesc(fieldA, fieldB); - } else { - return sortByAsc(fieldA, fieldB); - } - }); -}; - -const sortByDesc = (a: any, b: any) => { - return a > b ? 1 : a < b ? -1 : 0; -}; - -const sortByAsc = (a: any, b: any) => { - return a < b ? 1 : a > b ? -1 : 0; -}; - -export { orderBy }; diff --git a/src/helpers.ts b/src/helpers.ts index f7a4aab..e8bd50f 100644 --- a/src/helpers.ts +++ b/src/helpers.ts @@ -1,3 +1,6 @@ +import { Project } from "./features/Project/types"; +import { TMap } from "./hooks/users"; + export function getUserInitials(firstName: string, lastName: string) { return firstName.substring(0, 1) + (lastName || "").substring(0, 1); } @@ -19,3 +22,96 @@ export const formatPath = (company: string | undefined): string => { return company.replaceAll(re, "").toLowerCase(); }; + +interface Organization { + id: string; + path: string; + name: string; + color?: string; +} + +export const getOrgs = (data: TMap) => { + return Object.keys(data).map((id) => ({ + path: data[id].path, + id: data[id].id, + name: data[id].company, + })) as Organization[]; +}; + +export function assignColor(org: T, index: number) { + return { + ...org, + color: `badge${(index % 9) + 1}`, + }; +} + +export interface PinnedProject { + tenantPath: string; + projectId: string; + name: string; +} + +export const getPinState = (project: Project) => { + const pinnedSettings = localStorage.getItem("settings.pinned"); + if (pinnedSettings) { + const data = JSON.parse(pinnedSettings); + if (data && data.length) { + return !!data.find((obj: PinnedProject) => { + return obj.projectId === project.id; + }); + } + } + + return false; +}; + +export const findPinnedByPath = ( + settings: PinnedProject[], + path: string +): PinnedProject[] => { + if (settings) { + if (settings && settings.length) { + return settings.filter((obj: PinnedProject) => { + return obj.tenantPath === path; + }); + } + } + + return []; +}; + +type SortBy = "asc" | "desc"; + +// Sorts values by given key in descending order unless ascending order is specified. +function orderBy(key: string, values: T[], sortBy: SortBy = "desc") { + return values.sort( + (a: { [index: string]: any }, b: { [index: string]: any }) => { + let fieldA, fieldB; + + try { + // Assume it's a string. + fieldA = a[key].toLowerCase(); + fieldB = b[key].toLowerCase(); + } catch { + fieldA = a[key]; + fieldB = b[key]; + } + + if (sortBy === "desc") { + return sortByDesc(fieldA, fieldB); + } else { + return sortByAsc(fieldA, fieldB); + } + } + ); +} + +const sortByDesc = (a: any, b: any) => { + return a > b ? 1 : a < b ? -1 : 0; +}; + +const sortByAsc = (a: any, b: any) => { + return a < b ? 1 : a > b ? -1 : 0; +}; + +export { orderBy }; diff --git a/src/hooks/users.ts b/src/hooks/users.ts index cec9210..4b2fadf 100644 --- a/src/hooks/users.ts +++ b/src/hooks/users.ts @@ -38,8 +38,12 @@ export function useUser() { return data; } -interface TMap { - [index: string]: string; +export interface TMap { + [index: string]: { + path: string; + company: string; + id: string; + }; } export function useTenantMap() { @@ -51,7 +55,11 @@ export function useTenantMap() { const obj = data[key]; return { ...acc, - [obj.id]: obj.path, + [obj.id]: { + path: obj.path, + id: obj.id, + company: obj.companyName, + }, }; }, {}); }); diff --git a/src/index.css b/src/index.css index 118fb1a..6285252 100644 --- a/src/index.css +++ b/src/index.css @@ -86,6 +86,8 @@ --p640: 40rem; --p512: 32rem; --p384: 24rem; + --p300: 18.75rem; + --p288: 18rem; --p256: 16rem; --p192: 12rem; --p128: 8rem; @@ -94,6 +96,7 @@ --p48: 3rem; --p32: 2rem; --p24: 1.5rem; + --p20: 1.25rem; --p18: 1.125rem; --p16: 1rem; --p14: 0.875rem; diff --git a/src/index.tsx b/src/index.tsx index 2b07fcc..1435250 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -29,6 +29,7 @@ import { Loader } from "./components/Loader"; import { SideBar } from "./components/SideBar"; import Copyright from "@mui/icons-material/Copyright"; import { TopBar } from "./components/TopBar/TopBar"; +import { PinnedProvider } from "./services/PinnedProvider"; Amplify.configure({ ...awsconfig, @@ -51,6 +52,7 @@ const TenantPath = () => { const user = useUser(); const { tenantPath } = useParams(); const defaultTenantPath = formatPath(user?.company); + queryClient.invalidateQueries("projects").then(); if (user) { const activeTenant = user.connections[tenantPath!]; @@ -96,11 +98,13 @@ const App = withAuthenticator( <> - - - - - + + + + + + +