Add people overview page

Signed-off-by: gearnode <bryan@frimin.fr>
This commit is contained in:
gearnode
2025-02-18 10:05:11 +01:00
parent ba73ffca79
commit 133cd6ac1e
4 changed files with 327 additions and 7 deletions

View File

@@ -10,6 +10,7 @@ import ErrorBoundary from "./components/ErrorBoundary";
import { ErrorPage } from "./pages/ErrorPage";
import ConsoleLayout from "./layouts/ConsoleLayout";
import { RelayEnvironment } from "./RelayEnvironment";
import PeopleOverviewPage from "@/pages/PeopleOverviewPage";
posthog.init(process.env.POSTHOG_KEY!, {
api_host: process.env.POSTHOG_HOST,
@@ -80,6 +81,16 @@ function App() {
</Suspense>
}
/>
<Route
path="/peoples/:peopleId"
element={
<Suspense>
<ErrorBoundaryWithLocation>
<PeopleOverviewPage />
</ErrorBoundaryWithLocation>
</Suspense>
}
/>
<Route
path="/vendors"
element={
@@ -98,8 +109,8 @@ function App() {
<FrameworkListPage />
</ErrorBoundaryWithLocation>
</Suspense>
}
/>
}
/>
<Route
path="/frameworks/:frameworkId"
element={
@@ -118,8 +129,8 @@ function App() {
<VendorOverviewPage />
</ErrorBoundaryWithLocation>
</Suspense>
}
/>
}
/>
<Route
path="/settings"
element={

View File

@@ -112,9 +112,10 @@ function PeopleListPageContent({
</div>
<div className="space-y-2">
{peoples.map((person) => (
<div
<Link
key={person?.id}
className="flex items-center justify-between p-4 rounded-lg border bg-card text-card-foreground shadow-sm"
to={`/peoples/${person?.id}`}
className="flex items-center justify-between p-4 rounded-lg border bg-card text-card-foreground shadow-sm hover:bg-accent/50"
>
<div className="flex items-center gap-4">
<Avatar className="h-10 w-10">
@@ -180,7 +181,7 @@ function PeopleListPageContent({
Delete
</Button>
</div>
</div>
</Link>
))}
<div className="flex gap-2 justify-end mt-4">

View File

@@ -0,0 +1,144 @@
"use client";
import { Card } from "@/components/ui/card";
import {
graphql,
PreloadedQuery,
usePreloadedQuery,
useQueryLoader,
} from "react-relay";
import { Suspense, useEffect } from "react";
import type { PeopleOverviewPageQuery as PeopleOverviewPageQueryType } from "./__generated__/peopleOverviewPageQuery.graphql";
import { useParams } from "react-router";
import { Helmet } from "react-helmet-async";
import { useBreadcrumb } from "@/contexts/BreadcrumbContext";
const peopleOverviewPageQuery = graphql`
query PeopleOverviewPageQuery($peopleId: ID!) {
node(id: $peopleId) {
... on People {
id
fullName
primaryEmailAddress
createdAt
updatedAt
}
}
}
`;
function PeopleOverviewPageContent({
queryRef,
}: {
queryRef: PreloadedQuery<PeopleOverviewPageQueryType>;
}) {
const data = usePreloadedQuery(peopleOverviewPageQuery, queryRef);
const { setBreadcrumbSegment } = useBreadcrumb();
useEffect(() => {
if (data.node?.primaryEmailAddress) {
setBreadcrumbSegment('peoples/:id', data.node.primaryEmailAddress);
}
}, [data.node?.primaryEmailAddress, setBreadcrumbSegment]);
return (
<div className="space-y-6 p-4 md:p-6 lg:p-8">
<div className="mx-auto max-w-4xl space-y-6">
<div className="space-y-2">
<h1 className="text-xl font-semibold text-gray-900">
{data.node?.fullName}
</h1>
<p className="text-gray-600">
View and manage person details
</p>
</div>
<Card className="p-6">
<div className="space-y-4">
<div className="space-y-2">
<h2 className="text-lg font-medium">Personal Information</h2>
<p className="text-sm text-gray-500">
Basic information about the person
</p>
</div>
<div className="grid grid-cols-2 gap-4">
<div className="col-span-2">
<label className="text-sm font-medium text-gray-700">Full Name</label>
<p className="mt-1">{data.node?.fullName}</p>
</div>
<div className="col-span-2">
<label className="text-sm font-medium text-gray-700">Email</label>
<p className="mt-1">{data.node?.primaryEmailAddress}</p>
</div>
</div>
</div>
</Card>
<Card className="p-6">
<div className="space-y-4">
<div className="space-y-2">
<h2 className="text-lg font-medium">System Information</h2>
<p className="text-sm text-gray-500">
System-related information about the person
</p>
</div>
<div className="grid grid-cols-2 gap-4">
<div>
<label className="text-sm font-medium text-gray-700">Created At</label>
<p className="mt-1">{new Date(data.node?.createdAt).toLocaleString()}</p>
</div>
<div>
<label className="text-sm font-medium text-gray-700">Last Updated</label>
<p className="mt-1">{new Date(data.node?.updatedAt).toLocaleString()}</p>
</div>
</div>
</div>
</Card>
</div>
</div>
);
}
function PeopleOverviewPageFallback() {
return (
<div className="p-6 space-y-6">
<div className="space-y-1">
<div className="h-8 w-48 bg-muted animate-pulse rounded" />
<div className="h-4 w-96 bg-muted animate-pulse rounded" />
</div>
<div className="space-y-2">
{[1, 2].map((i) => (
<div key={i} className="h-20 bg-muted animate-pulse rounded-lg" />
))}
</div>
</div>
);
}
export default function PeopleOverviewPage() {
const { peopleId } = useParams();
const [queryRef, loadQuery] = useQueryLoader<PeopleOverviewPageQueryType>(
peopleOverviewPageQuery,
);
useEffect(() => {
loadQuery({ peopleId: peopleId! });
}, [loadQuery, peopleId]);
if (!queryRef) {
return <PeopleOverviewPageFallback />;
}
return (
<>
<Helmet>
<title>People Overview - Probo Console</title>
</Helmet>
<Suspense fallback={<PeopleOverviewPageFallback />}>
<PeopleOverviewPageContent queryRef={queryRef} />
</Suspense>
</>
);
}

View File

@@ -0,0 +1,164 @@
/**
* @generated SignedSource<<ee2731bf27a12fb704f79cf4058e2d80>>
* @lightSyntaxTransform
* @nogrep
*/
/* tslint:disable */
/* eslint-disable */
// @ts-nocheck
import { ConcreteRequest } from 'relay-runtime';
export type PeopleOverviewPageQuery$variables = {
peopleId: string;
};
export type PeopleOverviewPageQuery$data = {
readonly node: {
readonly createdAt?: any;
readonly fullName?: string;
readonly id?: string;
readonly primaryEmailAddress?: string;
readonly updatedAt?: any;
};
};
export type PeopleOverviewPageQuery = {
response: PeopleOverviewPageQuery$data;
variables: PeopleOverviewPageQuery$variables;
};
const node: ConcreteRequest = (function(){
var v0 = [
{
"defaultValue": null,
"kind": "LocalArgument",
"name": "peopleId"
}
],
v1 = [
{
"kind": "Variable",
"name": "id",
"variableName": "peopleId"
}
],
v2 = {
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "id",
"storageKey": null
},
v3 = {
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "fullName",
"storageKey": null
},
v4 = {
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "primaryEmailAddress",
"storageKey": null
},
v5 = {
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "createdAt",
"storageKey": null
},
v6 = {
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "updatedAt",
"storageKey": null
};
return {
"fragment": {
"argumentDefinitions": (v0/*: any*/),
"kind": "Fragment",
"metadata": null,
"name": "PeopleOverviewPageQuery",
"selections": [
{
"alias": null,
"args": (v1/*: any*/),
"concreteType": null,
"kind": "LinkedField",
"name": "node",
"plural": false,
"selections": [
{
"kind": "InlineFragment",
"selections": [
(v2/*: any*/),
(v3/*: any*/),
(v4/*: any*/),
(v5/*: any*/),
(v6/*: any*/)
],
"type": "People",
"abstractKey": null
}
],
"storageKey": null
}
],
"type": "Query",
"abstractKey": null
},
"kind": "Request",
"operation": {
"argumentDefinitions": (v0/*: any*/),
"kind": "Operation",
"name": "PeopleOverviewPageQuery",
"selections": [
{
"alias": null,
"args": (v1/*: any*/),
"concreteType": null,
"kind": "LinkedField",
"name": "node",
"plural": false,
"selections": [
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "__typename",
"storageKey": null
},
(v2/*: any*/),
{
"kind": "InlineFragment",
"selections": [
(v3/*: any*/),
(v4/*: any*/),
(v5/*: any*/),
(v6/*: any*/)
],
"type": "People",
"abstractKey": null
}
],
"storageKey": null
}
]
},
"params": {
"cacheID": "0cd0dd96cb7a84c2016bfe1f088f8245",
"id": null,
"metadata": {},
"name": "PeopleOverviewPageQuery",
"operationKind": "query",
"text": "query PeopleOverviewPageQuery(\n $peopleId: ID!\n) {\n node(id: $peopleId) {\n __typename\n ... on People {\n id\n fullName\n primaryEmailAddress\n createdAt\n updatedAt\n }\n id\n }\n}\n"
}
};
})();
(node as any).hash = "16b3f0915f69d8475d745b0c7f13b029";
export default node;