Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
88 commits
Select commit Hold shift + click to select a range
483a718
refactor(auth): mock user ID temporarily
hobbescodes Dec 27, 2024
fb7f283
Merge branch 'master' into feature/dashboard-functionality
hobbescodes Dec 27, 2024
eef3f52
Merge branch 'master' into feature/dashboard-functionality
hobbescodes Dec 27, 2024
4fc229c
Merge branch 'master' into feature/dashboard-functionality
hobbescodes Dec 27, 2024
ff60aa6
feature(dashboard): query for feedback aggregate
hobbescodes Dec 28, 2024
c1d29a5
feature(dashboard): query for active users aggregate
hobbescodes Dec 28, 2024
8face19
refactor(organizations): update queries to user userId variable
hobbescodes Dec 28, 2024
6dc92d9
refactor(dashboard): remove average response time aggregate
hobbescodes Dec 28, 2024
875584d
feature(dashboard): add query for recent feedback
hobbescodes Dec 28, 2024
75be978
chore(graphql): add comment regarding user first/last in recent feedb…
hobbescodes Dec 28, 2024
dd2db61
feature(graphql): add query for weekly feedback aggregates
hobbescodes Dec 28, 2024
f55bdc0
feature(dashboard): apply query for generating feedback overview chart
hobbescodes Dec 28, 2024
b741fe1
refactor(dashboard): remove date formatting functions from react context
hobbescodes Dec 28, 2024
4d53b3e
chore: format
hobbescodes Dec 28, 2024
1fac34e
refactor(dashboard): temporarily disabled new project button
hobbescodes Dec 28, 2024
113f9aa
refactor(codegen): update scalars to map BigInt to string
hobbescodes Dec 28, 2024
0d18428
refactor(dashboard): simplify logic for feedback chart data
hobbescodes Dec 29, 2024
5e03335
refactor(dashboard): dedup formatted date from data array in feedback…
hobbescodes Dec 29, 2024
2020874
chore: remove newline
hobbescodes Dec 29, 2024
9c9bdf0
refactor(recent-feedback): update props for response
hobbescodes Dec 29, 2024
0ca4c59
chore: update comments
hobbescodes Dec 29, 2024
6d6c976
docs: update response jsdoc
hobbescodes Dec 29, 2024
fc15b13
refactor(feedback-overview): set yaxis to always scale on natural num…
hobbescodes Dec 29, 2024
a1fc9d9
refactor(graphql): update organization query
hobbescodes Dec 29, 2024
5cb27bd
refactor(organization): cherry-pick partial support for RSC
hobbescodes Dec 29, 2024
1fe3ac0
refactor(organization): cherry-pick partial support for RSC
hobbescodes Dec 29, 2024
aaf75f8
feature(organization): appropriately fetch information for projects
hobbescodes Dec 29, 2024
62a20e7
chore: reorg imports
hobbescodes Dec 29, 2024
4b59109
feature(organization): add query for fetching organization metrics
hobbescodes Dec 29, 2024
356baa7
refactor(organization): use Promise.all to fetch necessary data in pa…
hobbescodes Dec 29, 2024
3b952c6
chore: format
hobbescodes Dec 29, 2024
d5151b9
refactor(organization): remove unnecessary layout file
hobbescodes Dec 29, 2024
79f161d
refactor(project-card): properly query for active users for a project
hobbescodes Dec 29, 2024
2ed117b
refactor(project-card): remove unused containerProps
hobbescodes Dec 29, 2024
46db80f
fix(project-card): remove invalid destructured prop
hobbescodes Dec 29, 2024
0c8228a
Merge branch 'master' into feature/dashboard-functionality
hobbescodes Dec 30, 2024
4c8b12a
chore: format
hobbescodes Dec 30, 2024
0025f52
Merge branch 'feature/dashboard-functionality' into feature/organizat…
hobbescodes Dec 30, 2024
ebecbae
refactor(response): extract date logic from render
hobbescodes Dec 30, 2024
7dbf883
refactor: add proper orderBy to recent feedback query
hobbescodes Dec 30, 2024
43735d9
refactor(dashboard): add appropriate render for empty states
hobbescodes Dec 30, 2024
fe6a7e6
refactor(pinned-organizations): use next link for view all organizati…
hobbescodes Dec 30, 2024
4948e7b
Merge branch 'feature/dashboard-functionality' into feature/organizat…
hobbescodes Dec 30, 2024
f64db1e
chore: update comment regarding the dashboard aggregates total users …
hobbescodes Dec 30, 2024
ad12652
Merge branch 'feature/dashboard-functionality' into feature/organizat…
hobbescodes Dec 30, 2024
042f468
refactor(response): use updated db schema to display username
hobbescodes Dec 30, 2024
9b37f4e
Merge branch 'feature/dashboard-functionality' into feature/organizat…
hobbescodes Dec 30, 2024
49ef6a3
chore: merge master
hobbescodes Dec 30, 2024
a64386e
refactor(projects): add query for organization details for breadcrumb…
hobbescodes Dec 30, 2024
230c87c
chore: format
hobbescodes Dec 30, 2024
5b792c8
feature(projects): update route to use dynamic projects query
hobbescodes Dec 30, 2024
c07f7cc
refactor(projects): update project aggregate data
hobbescodes Dec 30, 2024
9b4bfea
chore: format
hobbescodes Dec 30, 2024
bc81621
refactor(projects): remove status implementations
hobbescodes Dec 30, 2024
3a17244
refactor(projects): update projects to include a destructive action
hobbescodes Dec 30, 2024
1717fd4
chore: format
hobbescodes Dec 30, 2024
dd2b0a8
chore: merge master
hobbescodes Dec 30, 2024
e209edc
Merge branch 'master' into feature/projects-queries
hobbescodes Dec 30, 2024
b24a1e1
refactor(breadcrumbs): update breakpoint and naming isTablet --> isDe…
hobbescodes Dec 30, 2024
1ccde0a
refactor(projects): add pagination for projects list
hobbescodes Dec 30, 2024
39943f1
refactor(projects): adjust query to order projects by number of posts
hobbescodes Dec 31, 2024
6569749
chore: code cleanup
hobbescodes Dec 31, 2024
d42cb04
refactor(project-list): add render for empty state
hobbescodes Dec 31, 2024
842ba17
chore: format
hobbescodes Dec 31, 2024
758f04a
refactor(project-list): adjust action label
hobbescodes Dec 31, 2024
bbf2af4
chore: remove unnecessary TODO
hobbescodes Dec 31, 2024
4bb3086
refactor(organizations): add empty state and pagination
hobbescodes Dec 31, 2024
a8edaf9
chore: update generated artifacts
hobbescodes Dec 31, 2024
1d28613
feature(feedback): query data for feedback details
hobbescodes Dec 31, 2024
fe25faa
chore: format
hobbescodes Dec 31, 2024
2f223f5
feature(feedback): query feedback comments from database
hobbescodes Dec 31, 2024
741f20b
fix(comments): add comments query
hobbescodes Dec 31, 2024
c66a35e
feature(feedback): query downvotes
hobbescodes Dec 31, 2024
5d8f4ef
fix(breadcrumbs): remove useBreakpointValue restriction, apply to dis…
hobbescodes Dec 31, 2024
c57f569
refactor(comments): add render for empty state
hobbescodes Dec 31, 2024
202e5cc
refactor(comments): update infinite query to appropriately expose has…
hobbescodes Dec 31, 2024
8aaee05
refactor(comments): integrate useInfiniteCommentsQuery
hobbescodes Dec 31, 2024
c7f516a
refactor(comments): disable text area and submit button
hobbescodes Dec 31, 2024
8660776
chore: remove TODO comment
hobbescodes Dec 31, 2024
dcecba8
chore: remove TODO comment
hobbescodes Dec 31, 2024
b39734f
Merge branch 'master' into feature/projects-queries
hobbescodes Dec 31, 2024
0d0478b
Merge branch 'master' into feature/organization-queries
hobbescodes Dec 31, 2024
b7e00cc
refactor: remove unnecessary getOrganization util
hobbescodes Jan 2, 2025
e90283f
Merge branch 'feature/projects-queries' into feature/feedback-queries
hobbescodes Jan 2, 2025
a68dd53
refactor: remove unnecessary getFeedbackById util
hobbescodes Jan 2, 2025
69e19c6
chore: update generated artifacts
hobbescodes Jan 2, 2025
645efc7
Merge branch 'feature/projects-queries' into feature/feedback-queries
hobbescodes Jan 2, 2025
b221913
chore: merge master
hobbescodes Jan 2, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,12 @@ import { notFound } from "next/navigation";
import { Comments, FeedbackDetails } from "components/feedback";
import { Page } from "components/layout";
import { app } from "lib/config";
import { sdk } from "lib/graphql";
import { getAuthSession } from "lib/util";

import type { Feedback } from "components/feedback";
export const metadata = {
title: `${app.feedbackPage.breadcrumb} | ${app.name}`,
};

interface Props {
/** Feedback page params. */
Expand All @@ -21,56 +24,41 @@ interface Props {
*/
const FeedbackPage = async ({ params }: Props) => {
const { organizationId, projectId, feedbackId } = await params;
const session = await getAuthSession();

const FEEDBACK: Feedback = {
id: feedbackId,
title: "I Still Like Turtles",
description:
"Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed non risus. Suspendisse lectus tortor, dignissim sit amet, adipiscing nec, ultricies sed, dolor. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed non risus. Suspendisse lectus tortor, dignissim sit amet, adipiscing nec, ultricies sed, dolor. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed non risus. Suspendisse lectus tortor, dignissim sit amet, adipiscing nec, ultricies sed, dolor. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed non risus. Suspendisse lectus tortor, dignissim sit amet, adipiscing nec, ultricies sed, dolor.",
createdAt: "2023-01-01T00:00:00.000Z",
updatedAt: "2023-08-24T00:00:00.000Z",
status: "Planned",
upvotes: 420,
downvotes: 69,
user: {
id: "1",
firstName: "Back",
lastName: "Feed",
},
};
const [session, { post: feedback }] = await Promise.all([
getAuthSession(),
sdk.FeedbackById({ rowId: feedbackId }),
]);

const breadcrumbs = [
{
label: app.organizationsPage.breadcrumb,
href: "/organizations",
},
{
// TODO: Use actual organization name here instead of ID
label: organizationId,
label: feedback?.project?.organization?.name ?? organizationId,
href: `/organizations/${organizationId}`,
},
{
label: app.projectsPage.breadcrumb,
href: `/organizations/${organizationId}/projects`,
},
{
// TODO: Use actual project name here instead of ID
label: projectId,
label: feedback?.project?.name ?? projectId,
href: `/organizations/${organizationId}/projects/${projectId}`,
},
{
label: app.feedbackPage.breadcrumb,
},
];

if (!session) notFound();
if (!session || !feedback) notFound();

return (
<Page breadcrumbs={breadcrumbs}>
<FeedbackDetails feedback={FEEDBACK} />
<FeedbackDetails feedbackId={feedbackId} />

<Comments />
<Comments feedbackId={feedbackId} />
</Page>
);
};
Expand Down
85 changes: 45 additions & 40 deletions src/components/core/Breadcrumb/Breadcrumb.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
"use client";

import { Flex, Icon, Text, useBreakpointValue } from "@omnidev/sigil";
import { Flex, Icon, Text } from "@omnidev/sigil";
import { app } from "lib/config";
import Link from "next/link";
import { LuChevronRight } from "react-icons/lu";
Expand All @@ -20,44 +20,49 @@ interface Props {
/**
* Breadcrumb.
*/
const Breadcrumb = ({ breadcrumbs }: Props) => {
const isDesktop = useBreakpointValue({ base: false, lg: true });

return (
<Flex fontSize="sm">
<Link href="/">
<Text
color="foreground.subtle"
_hover={{ color: "foreground.default" }}
>
{app.breadcrumb}
</Text>
</Link>

{breadcrumbs.map(({ label, href }, index) => {
const isLastItem = breadcrumbs.length - 1 === index;

return (
<Flex key={label} align="center">
<Icon src={LuChevronRight} color="foreground.subtle" mx={2} />

{href ? (
<Link href={href}>
<Text
color="foreground.subtle"
_hover={{ color: "foreground.default" }}
>
{!isDesktop && !isLastItem ? "..." : label}
</Text>
</Link>
) : (
<Text>{label}</Text>
)}
</Flex>
);
})}
</Flex>
);
};
const Breadcrumb = ({ breadcrumbs }: Props) => (
<Flex fontSize="sm">
<Link href="/">
<Text color="foreground.subtle" _hover={{ color: "foreground.default" }}>
{app.breadcrumb}
</Text>
</Link>

{breadcrumbs.map(({ label, href }, index) => {
const isLastItem = breadcrumbs.length - 1 === index;

return (
<Flex key={label} align="center">
<Icon src={LuChevronRight} color="foreground.subtle" mx={2} />

{href ? (
<Link href={href}>
<Text
display={isLastItem ? "none" : { base: "inline", lg: "none" }}
color={{
base: "foreground.subtle",
_hover: "foreground.default",
}}
>
...
</Text>
<Text
display={isLastItem ? "inline" : { base: "none", lg: "inline" }}
color={{
base: "foreground.subtle",
_hover: "foreground.default",
}}
>
{label}
</Text>
</Link>
) : (
<Text>{label}</Text>
)}
</Flex>
);
})}
</Flex>
);

export default Breadcrumb;
8 changes: 4 additions & 4 deletions src/components/feedback/CommentCard/CommentCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,11 @@ import type { StackProps } from "@omnidev/sigil";

interface Props extends StackProps {
/** Comment sender. */
senderName: string;
senderName: string | null | undefined;
/** Comment message. */
message: string;
message: string | null | undefined;
/** Comment date. */
date: string;
date: Date | null | undefined;
}

/**
Expand All @@ -36,7 +36,7 @@ const Comment = ({ senderName, message, date, ...rest }: Props) => (
w={8}
display={{ base: "none", sm: "flex" }}
>
<Text color="foreground.muted">{senderName[0]}</Text>
<Text color="foreground.muted">{senderName?.[0]}</Text>
</VStack>

<Stack gap={1} flex={1}>
Expand Down
142 changes: 52 additions & 90 deletions src/components/feedback/Comments/Comments.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,86 +9,49 @@ import {
Textarea,
VStack,
} from "@omnidev/sigil";
import { useState } from "react";
import { LuMessageSquare } from "react-icons/lu";
import useInfiniteScroll from "react-infinite-scroll-hook";

import { SkeletonArray, Spinner } from "components/core";
import { CommentCard } from "components/feedback";
import { ErrorBoundary, SectionContainer } from "components/layout";
import { EmptyState, ErrorBoundary, SectionContainer } from "components/layout";
import { useInfiniteCommentsQuery } from "generated/graphql";
import { app } from "lib/config";
import { useDataState } from "lib/hooks";

const COMMENTS = {
totalCount: 24,
data: [
{
id: "1",
senderName: "Back Feed",
message: "I still like turtles.",
date: "2024-11-01T00:00:00.000Z",
},
{
id: "2",
senderName: "Feed Back",
message: "The new dashboard layout is much more intuitive!",
date: "2024-04-02T00:00:00.000Z",
},
{
id: "3",
senderName: "Fed Front",
message: "Having issues with the new export feature.",
date: "2024-01-03T00:00:00.000Z",
},
{
id: "4",
senderName: "Back Fed",
message: "Would love to be able to export feedback.",
date: "2023-01-04T00:00:00.000Z",
},
],
};
interface Props {
/** Feedback ID. */
feedbackId: string;
}

/**
* Feedback comments section.
*/
const Comments = () => {
const [shownComments, setShownComments] = useState(COMMENTS.data);

const [pageState, setPageState] = useState<{
currentPage: number;
hasNextPage: boolean;
}>({ currentPage: 1, hasNextPage: true });

const { isLoading, isError } = useDataState({ timeout: 700 });

// NB: temporarily used to mock an infinite query
// TODO replace with real data query
const handleLoadMore = () => {
if (!pageState.hasNextPage) return;

setPageState((prev) => ({
...prev,
currentPage: prev.currentPage + 1,
}));

if (
pageState.currentPage >=
Math.floor(COMMENTS.totalCount / COMMENTS.data.length) - 1
) {
setPageState((prev) => ({
...prev,
hasNextPage: false,
}));
}

setShownComments((prev) => prev.concat(COMMENTS.data));
};
const Comments = ({ feedbackId }: Props) => {
const { data, isLoading, isError, hasNextPage, fetchNextPage } =
useInfiniteCommentsQuery(
{
pageSize: 5,
feedbackId,
},
{
initialPageParam: undefined,
getNextPageParam: (lastPage) =>
lastPage?.comments?.pageInfo?.hasNextPage
? { after: lastPage?.comments?.pageInfo?.endCursor }
: undefined,
}
);

// These are not defined within the `select` function in order to preserve type safety.
const totalCount = data?.pages?.[0]?.comments?.totalCount ?? 0;
const comments = data?.pages?.flatMap((page) =>
page?.comments?.edges?.map((edge) => edge?.node)
);

const [loaderRef, { rootRef }] = useInfiniteScroll({
loading: isLoading,
hasNextPage: pageState.hasNextPage,
onLoadMore: handleLoadMore,
hasNextPage: hasNextPage,
onLoadMore: fetchNextPage,
disabled: isError,
// NB: `rootMargin` is passed to `IntersectionObserver`. We can use it to trigger 'onLoadMore' when the spinner comes *near* to being visible, instead of when it becomes fully visible within the root element.
rootMargin: "0px 0px 400px 0px",
Expand All @@ -107,51 +70,50 @@ const Comments = () => {
borderColor="border.subtle"
fontSize="sm"
minH={16}
disabled
/>

<Stack justify="space-between" direction="row">
<Skeleton isLoaded={!isLoading} h="fit-content">
<Text
fontSize="sm"
color="foreground.muted"
>{`${isError ? 0 : COMMENTS.totalCount} ${app.feedbackPage.comments.totalComments}`}</Text>
>{`${isError ? 0 : totalCount} ${app.feedbackPage.comments.totalComments}`}</Text>
</Skeleton>

<Button
w="fit-content"
placeSelf="flex-end"
// TODO: discuss if disabling this button (mutation) is the right approach if an error is encountered fetching the comments
disabled={isLoading || isError}
>
<Button w="fit-content" placeSelf="flex-end" disabled>
{app.feedbackPage.comments.submit}
</Button>
</Stack>

{isError ? (
<ErrorBoundary message="Error fetching comments" h="sm" />
<ErrorBoundary message="Error fetching comments" h="xs" />
) : (
// NB: the padding is necessary to prevent clipping of the card borders/box shadows
<Grid gap={2} mt={4} maxH="sm" overflow="auto" p="1px">
{isLoading ? (
<SkeletonArray count={5} h={21} />
) : (
) : comments?.length ? (
<VStack>
{shownComments.map(
({ id, senderName, message, date }, index) => (
<CommentCard
// biome-ignore lint/suspicious/noArrayIndexKey: index needed as key for the time being
key={`${id}-${index}`}
senderName={senderName}
message={message}
date={date}
w="full"
minH={21}
/>
)
)}

{pageState.hasNextPage && <Spinner ref={loaderRef} />}
{comments?.map((comment) => (
<CommentCard
key={comment?.rowId}
senderName={comment?.user?.username}
message={comment?.message}
date={comment?.createdAt}
w="full"
minH={21}
/>
))}

{hasNextPage && <Spinner ref={loaderRef} />}
</VStack>
) : (
<EmptyState
message={app.feedbackPage.comments.emptyState.message}
h="xs"
w="full"
/>
)}
</Grid>
)}
Expand Down
Loading