mirror of
https://github.com/formbricks/formbricks.git
synced 2026-02-05 02:58:36 -06:00
Move people page to server components (#479)
* feat: person overview is now a server rendered page * feat: loader component & fix: minor changes as suggested * hide: session count * getAttributeValue always returns string * fix: remove createdAt & updatedAt fields from user for now * fix: use select instead of include to specify retreival fields * feat: suspense streaming * feat: skeleton table for streaming and loading * fix: use integrated loading and cleanup components --------- Co-authored-by: Matthias Nannt <mail@matthiasnannt.com>
This commit is contained in:
committed by
GitHub
parent
419b9d0b90
commit
dbec5426b2
@@ -1,78 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import EmptySpaceFiller from "@/components/shared/EmptySpaceFiller";
|
||||
import LoadingSpinner from "@/components/shared/LoadingSpinner";
|
||||
import { usePeople } from "@/lib/people/people";
|
||||
import { truncateMiddle } from "@/lib/utils";
|
||||
import { ErrorComponent, PersonAvatar } from "@formbricks/ui";
|
||||
import Link from "next/link";
|
||||
|
||||
export default function PeopleList({ environmentId }: { environmentId: string }) {
|
||||
const { people, isLoadingPeople, isErrorPeople } = usePeople(environmentId);
|
||||
|
||||
if (isLoadingPeople) {
|
||||
return <LoadingSpinner />;
|
||||
}
|
||||
if (isErrorPeople) {
|
||||
return <ErrorComponent />;
|
||||
}
|
||||
|
||||
const getAttributeValue = (person, attributeName) => {
|
||||
return person.attributes.find((a) => a.attributeClass.name === attributeName)?.value;
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
{people.length === 0 ? (
|
||||
<EmptySpaceFiller type="table" environmentId={environmentId} />
|
||||
) : (
|
||||
<div className="rounded-lg border border-slate-200">
|
||||
<div className="grid h-12 grid-cols-7 content-center rounded-lg bg-slate-100 text-left text-sm font-semibold text-slate-900">
|
||||
<div className="col-span-3 pl-6 ">User</div>
|
||||
<div className="col-span-2 text-center">User ID</div>
|
||||
<div className="text-center">Email</div>
|
||||
<div className="text-center">Sessions</div>
|
||||
</div>
|
||||
<div className="grid-cols-7">
|
||||
{people.map((person) => (
|
||||
<Link
|
||||
href={`/environments/${environmentId}/people/${person.id}`}
|
||||
key={person.id}
|
||||
className="w-full">
|
||||
<div className="m-2 grid h-16 grid-cols-7 content-center rounded-lg hover:bg-slate-100">
|
||||
<div className="col-span-3 flex items-center pl-6 text-sm">
|
||||
<div className="flex items-center">
|
||||
<div className="ph-no-capture h-10 w-10 flex-shrink-0">
|
||||
<PersonAvatar personId={person.id} />
|
||||
</div>
|
||||
<div className="ml-4">
|
||||
<div className="ph-no-capture font-medium text-slate-900">
|
||||
{getAttributeValue(person, "email") ? (
|
||||
<span>{getAttributeValue(person, "email")}</span>
|
||||
) : (
|
||||
<span>{person.id}</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="col-span-2 my-auto whitespace-nowrap text-center text-sm text-slate-500">
|
||||
<div className="ph-no-capture text-slate-900">
|
||||
{truncateMiddle(getAttributeValue(person, "userId"), 24)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="ph-no-capture my-auto whitespace-nowrap text-center text-sm text-slate-500">
|
||||
<div className="text-slate-900">{getAttributeValue(person, "email")}</div>
|
||||
</div>
|
||||
<div className="ph-no-capture my-auto whitespace-nowrap text-center text-sm text-slate-500">
|
||||
<div className="text-slate-900">{person._count?.sessions}</div>
|
||||
</div>
|
||||
</div>
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
34
apps/web/app/environments/[environmentId]/people/loading.tsx
Normal file
34
apps/web/app/environments/[environmentId]/people/loading.tsx
Normal file
@@ -0,0 +1,34 @@
|
||||
export default function Loading() {
|
||||
return (
|
||||
<div className="rounded-lg border border-slate-200">
|
||||
<div className="grid h-12 grid-cols-7 content-center rounded-lg bg-slate-100 text-left text-sm font-semibold text-slate-900">
|
||||
<div className="col-span-3 pl-6">User</div>
|
||||
<div className="col-span-2 text-center">User ID</div>
|
||||
<div className="col-span-2 text-center">Email</div>
|
||||
</div>
|
||||
<div className="w-full">
|
||||
{[...Array(3)].map((_, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className="m-2 grid h-16 grid-cols-7 content-center rounded-lg hover:bg-slate-100"
|
||||
>
|
||||
<div className="col-span-3 flex items-center pl-6 text-sm">
|
||||
<div className="flex items-center">
|
||||
<div className="ph-no-capture h-10 w-10 flex-shrink-0 animate-pulse bg-gray-200 rounded-full"></div>
|
||||
<div className="ml-4">
|
||||
<div className="ph-no-capture font-medium text-slate-900 animate-pulse bg-gray-200 rounded-full w-28 h-4"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="col-span-2 my-auto whitespace-nowrap text-center text-sm text-slate-500">
|
||||
<div className="ph-no-capture text-slate-900 animate-pulse bg-gray-200 rounded-full m-12 h-4"></div>
|
||||
</div>
|
||||
<div className="col-span-2 my-auto whitespace-nowrap text-center text-sm text-slate-500">
|
||||
<div className="ph-no-capture text-slate-900 animate-pulse bg-gray-200 rounded-full m-12 h-4"></div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,13 +1,62 @@
|
||||
import PeopleList from "./PeopleList";
|
||||
export const revalidate = 0;
|
||||
|
||||
import EmptySpaceFiller from "@/components/shared/EmptySpaceFiller";
|
||||
import { truncateMiddle } from "@/lib/utils";
|
||||
import { TransformPersonOutput, getPeople } from "@formbricks/lib/services/person";
|
||||
import { PersonAvatar } from "@formbricks/ui";
|
||||
|
||||
const getAttributeValue = (person: TransformPersonOutput, attributeName: string) =>
|
||||
person.attributes[attributeName]?.toString();
|
||||
|
||||
export default async function PeoplePage({ params }) {
|
||||
const people = await getPeople();
|
||||
|
||||
export default function PeoplePage({ params }) {
|
||||
return (
|
||||
<>
|
||||
<h1 className="my-2 text-3xl font-bold text-slate-800">People</h1>
|
||||
<p className="mb-6 text-slate-500">
|
||||
A list of all people who used your application since embedding the Formbricks JS widget.
|
||||
</p>
|
||||
<PeopleList environmentId={params.environmentId} />
|
||||
{people.length === 0 ? (
|
||||
<EmptySpaceFiller type="table" environmentId={params.environmentId} />
|
||||
) : (
|
||||
<div className="rounded-lg border border-slate-200">
|
||||
<div className="grid h-12 grid-cols-7 content-center rounded-lg bg-slate-100 text-left text-sm font-semibold text-slate-900">
|
||||
<div className="col-span-3 pl-6 ">User</div>
|
||||
<div className="col-span-2 text-center">User ID</div>
|
||||
<div className="col-span-2 text-center">Email</div>
|
||||
</div>
|
||||
{people.map((person) => (
|
||||
<a
|
||||
href={`/environments/${params.environmentId}/people/${person.id}`}
|
||||
key={person.id}
|
||||
className="w-full">
|
||||
<div className="m-2 grid h-16 grid-cols-7 content-center rounded-lg hover:bg-slate-100">
|
||||
<div className="col-span-3 flex items-center pl-6 text-sm">
|
||||
<div className="flex items-center">
|
||||
<div className="ph-no-capture h-10 w-10 flex-shrink-0">
|
||||
<PersonAvatar personId={person.id} />
|
||||
</div>
|
||||
<div className="ml-4">
|
||||
<div className="ph-no-capture font-medium text-slate-900">
|
||||
{getAttributeValue(person, "email") ? (
|
||||
<span>{getAttributeValue(person, "email")}</span>
|
||||
) : (
|
||||
<span>{person.id}</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="col-span-2 my-auto whitespace-nowrap text-center text-sm text-slate-500">
|
||||
<div className="ph-no-capture text-slate-900">
|
||||
{truncateMiddle(getAttributeValue(person, "userId"), 24)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="col-span-2 my-auto whitespace-nowrap text-center text-sm text-slate-500">
|
||||
<div className="ph-no-capture text-slate-900">{getAttributeValue(person, "email")}</div>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ import { prisma } from "@formbricks/database";
|
||||
import { TPerson } from "@formbricks/types/v1/people";
|
||||
import { Prisma } from "@prisma/client";
|
||||
import { DatabaseError, ResourceNotFoundError } from "@formbricks/errors";
|
||||
import { cache } from "react";
|
||||
|
||||
type TransformPersonInput = {
|
||||
id: string;
|
||||
@@ -68,3 +69,38 @@ export const getPerson = async (personId: string): Promise<TPerson | null> => {
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
export const getPeople = cache(async (): Promise<TPerson[]> => {
|
||||
try {
|
||||
const personsPrisma = await prisma.person.findMany({
|
||||
select: {
|
||||
id: true,
|
||||
attributes: {
|
||||
select: {
|
||||
value: true,
|
||||
attributeClass: {
|
||||
select: {
|
||||
name: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
if (!personsPrisma) {
|
||||
throw new ResourceNotFoundError("Persons", "All Persons");
|
||||
}
|
||||
|
||||
const transformedPersons: TransformPersonOutput[] = personsPrisma
|
||||
.map(transformPrismaPerson)
|
||||
.filter((person: TransformPersonOutput | null): person is TransformPersonOutput => person !== null);
|
||||
|
||||
return transformedPersons;
|
||||
} catch (error) {
|
||||
if (error instanceof Prisma.PrismaClientKnownRequestError) {
|
||||
throw new DatabaseError("Database operation failed");
|
||||
}
|
||||
|
||||
throw error;
|
||||
}
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user