Skip to content

Commit

Permalink
feat: add group page design
Browse files Browse the repository at this point in the history
  • Loading branch information
andre-code committed Dec 6, 2024
1 parent 9c19dd5 commit 44902d2
Show file tree
Hide file tree
Showing 11 changed files with 498 additions and 328 deletions.
46 changes: 46 additions & 0 deletions client/src/components/PageNav.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
/*!
* Copyright 2024 - Swiss Data Science Center (SDSC)
* A partnership between École Polytechnique Fédérale de Lausanne (EPFL) and
* Eidgenössische Technische Hochschule Zürich (ETHZ).
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import cx from "classnames";
import { Eye, Sliders } from "react-bootstrap-icons";
import { Nav, NavItem } from "reactstrap";
import RenkuNavLinkV2 from "./RenkuNavLinkV2";

export interface PageNavOptions {
overviewUrl: string;
settingsUrl: string;
}
export default function PageNav({ options }: { options: PageNavOptions }) {
return (
<>
<Nav tabs>
<NavItem>
<RenkuNavLinkV2 end to={options.overviewUrl} title="Overview">
<Eye className={cx("bi", "me-1")} />
Overview
</RenkuNavLinkV2>
</NavItem>
<NavItem>
<RenkuNavLinkV2 end to={options.settingsUrl} title="Settings">
<Sliders className={cx("bi", "me-1")} />
Settings
</RenkuNavLinkV2>
</NavItem>
</Nav>
</>
);
}
30 changes: 30 additions & 0 deletions client/src/features/groupsV2/LazyGroupContainer.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
/*!
* Copyright 2024 - Swiss Data Science Center (SDSC)
* A partnership between École Polytechnique Fédérale de Lausanne (EPFL) and
* Eidgenössische Technische Hochschule Zürich (ETHZ).
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

import { Suspense, lazy } from "react";
import PageLoader from "../../components/PageLoader";

const GroupV2Container = lazy(() => import("./show/GroupPageContainer"));

export default function LazyGroupContainer() {
return (
<Suspense fallback={<PageLoader />}>
<GroupV2Container />
</Suspense>
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,12 +19,12 @@
import { Suspense, lazy } from "react";
import PageLoader from "../../components/PageLoader";

const GroupV2Show = lazy(() => import("./show/GroupV2Show"));
const GroupV2Overview = lazy(() => import("./show/GroupV2Show"));

export default function LazyGroupV2Show() {
export default function LazyGroupV2Overview() {
return (
<Suspense fallback={<PageLoader />}>
<GroupV2Show />
<GroupV2Overview />
</Suspense>
);
}
72 changes: 33 additions & 39 deletions client/src/features/groupsV2/members/GroupV2MemberListDisplay.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,16 +19,17 @@
import cx from "classnames";
import { capitalize } from "lodash-es";
import { useMemo } from "react";
import { People } from "react-bootstrap-icons";
import { Link, generatePath } from "react-router-dom-v5-compat";
import { ListGroup } from "reactstrap";
import { Badge } from "reactstrap";

import { Loader } from "../../../components/Loader";
import { RtkOrNotebooksError } from "../../../components/errors/RtkErrorAlert";
import { ABSOLUTE_ROUTES } from "../../../routing/routes.constants";
import { toSortedMembers } from "../../ProjectPageV2/utils/roleUtils";
import type { ProjectMemberResponse } from "../../projectsV2/api/projectV2.api";
import { useGetGroupsByGroupSlugMembersQuery } from "../../projectsV2/api/projectV2.enhanced-api";
import UserAvatar from "../../usersV2/show/UserAvatar";
import { GroupInformationBox } from "../show/GroupV2Information";

interface GroupV2MemberListDisplayProps {
group: string;
Expand All @@ -48,30 +49,33 @@ export default function GroupV2MemberListDisplay({
[members]
);

if (isLoading)
return (
<div className={cx("d-flex", "justify-content-center", "w-100")}>
<div className={cx("d-flex", "flex-column")}>
<Loader />
<div>Retrieving group members...</div>
</div>
</div>
);

if (error || sortedMembers == null) {
return <RtkOrNotebooksError error={error} dismissible={false} />;
}

if (!sortedMembers.length) {
return <p>There are no members in this group.</p>;
}

return (
<ListGroup className="mb-3">
<GroupInformationBox
icon={<People className="bi" />}
title={
<>
<span>Members</span>
<Badge>{sortedMembers.length ?? 0}</Badge>
</>
}
>
{!sortedMembers.length && <p>There are no members in this group.</p>}
{isLoading && (
<div className={cx("d-flex", "justify-content-center", "w-100")}>
<div className={cx("d-flex", "flex-column")}>
<Loader />
<div>Retrieving group members...</div>
</div>
</div>
)}
{sortedMembers?.map((member) => (
<GroupV2Member key={member.id} member={member} />
))}
</ListGroup>
</GroupInformationBox>
);
}

Expand All @@ -95,32 +99,22 @@ function GroupV2Member({ member }: GroupV2MemberProps) {
return (
<>
<Link
className={cx("list-group-item-action", "list-group-item")}
className={cx("mb-0")}
to={generatePath(ABSOLUTE_ROUTES.v2.users.show, { username })}
>
<div
className={cx(
"align-items-center",
"d-flex",
"flex-wrap",
"gap-2",
"justify-content-between"
)}
>
<div className={cx("d-flex", "gap-2")}>
<div
className={cx("align-items-center", "d-flex", "flex-wrap", "gap-2")}
className={cx(
"d-flex",
"flex-column",
"justify-content-center",
"text-truncate"
)}
>
<div className={cx("align-items-center", "d-flex", "gap-2")}>
<UserAvatar
firstName={firstName}
lastName={lastName}
username={username}
/>
<span className={cx("fw-bold")}>{name ?? "Unknown user"}</span>{" "}
</div>
<p className="m-0">{`@${username}`}</p>
<p className={cx("m-0", "text-truncate")}>
{name ?? "Unknown user"} ({capitalize(role)})
</p>
</div>
<p className="m-0">{capitalize(role)}</p>
</div>
</Link>
</>
Expand Down
19 changes: 2 additions & 17 deletions client/src/features/groupsV2/settings/GroupV2Settings.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,14 +18,13 @@

import { skipToken } from "@reduxjs/toolkit/query";
import cx from "classnames";
import { ArrowLeft, Sliders } from "react-bootstrap-icons";
import { Link, generatePath, useParams } from "react-router-dom-v5-compat";
import { Sliders } from "react-bootstrap-icons";
import { useParams } from "react-router-dom-v5-compat";
import { Card, CardBody, CardHeader } from "reactstrap";

import { Loader } from "../../../components/Loader";
import ContainerWrap from "../../../components/container/ContainerWrap";
import LazyNotFound from "../../../not-found/LazyNotFound";
import { ABSOLUTE_ROUTES } from "../../../routing/routes.constants";
import { useGetGroupsByGroupSlugQuery } from "../../projectsV2/api/projectV2.enhanced-api";
import GroupNotFound from "../../projectsV2/notFound/GroupNotFound";
import GroupSettingsMembers from "./GroupSettingsMembers";
Expand Down Expand Up @@ -55,20 +54,6 @@ export default function GroupV2Settings() {
return (
<ContainerWrap>
<div className={cx("d-flex", "flex-column", "gap-3")}>
<div>
<h2>{group.name ?? "Unknown group"}</h2>
<div>
<Link
to={generatePath(ABSOLUTE_ROUTES.v2.groups.show.root, {
slug: group.slug,
})}
>
<ArrowLeft className={cx("bi", "me-1")} />
Back to group
</Link>
</div>
</div>

<section>
<Card data-cy="group-general-settings">
<CardHeader>
Expand Down
127 changes: 127 additions & 0 deletions client/src/features/groupsV2/show/GroupPageContainer.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
import { skipToken } from "@reduxjs/toolkit/query";
import cx from "classnames";
import { useEffect } from "react";
import {
generatePath,
Outlet,
useNavigate,
useOutletContext,
useParams,
} from "react-router-dom-v5-compat";
import { Col, Row } from "reactstrap";
import ContainerWrap from "../../../components/container/ContainerWrap.tsx";
import { Loader } from "../../../components/Loader.tsx";
import PageNav, { PageNavOptions } from "../../../components/PageNav.tsx";
import LazyNotFound from "../../../not-found/LazyNotFound.tsx";
import { ABSOLUTE_ROUTES } from "../../../routing/routes.constants.ts";
import { GroupResponse } from "../../projectsV2/api/namespace.api.ts";
import {
useGetGroupsByGroupSlugQuery,
useGetNamespacesByNamespaceSlugQuery,
} from "../../projectsV2/api/projectV2.enhanced-api.ts";
import GroupNotFound from "../../projectsV2/notFound/GroupNotFound.tsx";
import UserAvatar, {
AvatarTypeWrap,
UserAvatarSize,
} from "../../usersV2/show/UserAvatar.tsx";

export default function GroupPageContainer() {
const { slug } = useParams<{ slug: string }>();

const navigate = useNavigate();

const {
data: namespace,
isLoading: isLoadingNamespace,
error: namespaceError,
} = useGetNamespacesByNamespaceSlugQuery(
slug ? { namespaceSlug: slug } : skipToken
);
const {
data: group,
isLoading: isLoadingGroup,
error: groupError,
} = useGetGroupsByGroupSlugQuery(slug ? { groupSlug: slug } : skipToken);

const isLoading = isLoadingNamespace || isLoadingGroup;
const error = namespaceError ?? groupError;

useEffect(() => {
if (slug && namespace?.namespace_kind === "user") {
navigate(
generatePath(ABSOLUTE_ROUTES.v2.users.show, { username: slug }),
{
replace: true,
}
);
}
}, [namespace?.namespace_kind, navigate, slug]);

if (!slug) {
return <LazyNotFound />;
}

if (isLoading) {
return <Loader className="align-self-center" />;
}

if (error || !namespace || !group) {
return <GroupNotFound error={error} />;
}

const options: PageNavOptions = {
overviewUrl: generatePath(ABSOLUTE_ROUTES.v2.groups.show.root, {
slug: group.slug,
}),
settingsUrl: generatePath(ABSOLUTE_ROUTES.v2.groups.show.settings, {
slug: group.slug,
}),
};
return (
<ContainerWrap>
<Row>
<Col xs={12}>
<GroupHeader group={group} slug={slug} />
</Col>
<Col xs={12} className="mb-2">
<div className="mb-3">
<PageNav options={options} />
</div>
</Col>
<Col xs={12}>
<main>
<Outlet context={{ group: group } satisfies ContextType} />
</main>
</Col>
</Row>
</ContainerWrap>
);
}

type ContextType = { group: GroupResponse };
export function useGroup() {
return useOutletContext<ContextType>();
}

function GroupHeader({ group, slug }: { group: GroupResponse; slug: string }) {
return (
<div className={cx("d-flex", "flex-row", "flex-nowrap", "gap-2")}>
<div className={cx("d-flex", "gap-2")}>
<AvatarTypeWrap type={"Group"}>
<UserAvatar
username={group.name || slug}
size={UserAvatarSize.extraLarge}
/>
</AvatarTypeWrap>
<div>
<h2 className="mb-0">{group.name ?? "Unknown group"}</h2>
{group.description && (
<section>
<p>{group.description}</p>
</section>
)}
</div>
</div>
</div>
);
}
Loading

0 comments on commit 44902d2

Please sign in to comment.