Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
128 changes: 128 additions & 0 deletions packages/shared/src/components/feedback/FeedbackCard.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
import type { ReactElement } from 'react';
import React from 'react';
import { format } from 'date-fns';
import { Button, ButtonVariant } from '../buttons/Button';
import {
Typography,
TypographyColor,
TypographyType,
} from '../typography/Typography';
import type { FeedbackItem } from '../../graphql/feedback';
import {
getFeedbackCategoryLabel,
getFeedbackStatusClassName,
getFeedbackStatusLabel,
} from '../../lib/feedback';

type FeedbackCardProps = {
item: FeedbackItem;
isExpanded: boolean;
onToggleExpand: () => void;
};

export const FeedbackCard = ({
item,
isExpanded,
onToggleExpand,
}: FeedbackCardProps): ReactElement => {
const isLongDescription = item.description.length > 260;
const description = isExpanded
? item.description
: item.description.slice(0, 260);
const badgeClassName = 'rounded-14 bg-surface-hover px-2 py-1 text-xs';

return (
<article className="rounded-16 border border-border-subtlest-tertiary bg-background-default p-4">
<div className="flex flex-wrap items-center gap-2">
<span className={`${badgeClassName} text-text-secondary`}>
{getFeedbackCategoryLabel(item.category)}
</span>
<span
className={`${badgeClassName} ${getFeedbackStatusClassName(
item.status,
)}`}
>
{getFeedbackStatusLabel(item.status)}
</span>
<Typography
type={TypographyType.Footnote}
color={TypographyColor.Tertiary}
className="ml-auto"
>
{format(new Date(item.createdAt), 'dd MMM yyyy')}
</Typography>
</div>

<Typography
type={TypographyType.Body}
className="mt-3 whitespace-pre-wrap"
>
{description}
{isLongDescription && !isExpanded ? '...' : ''}
</Typography>

{isLongDescription && (
<Button
className="mt-2"
variant={ButtonVariant.Tertiary}
onClick={onToggleExpand}
>
{isExpanded ? 'Show less' : 'Show more'}
</Button>
)}

{item.screenshotUrl && (
<a
href={item.screenshotUrl}
target="_blank"
rel="noopener noreferrer"
className="mt-3 block"
>
<img
src={item.screenshotUrl}
alt="Feedback screenshot"
className="max-h-48 w-auto rounded-12 border border-border-subtlest-tertiary"
/>
</a>
)}

{item.replies.length > 0 && (
<div className="mt-4 border-t border-border-subtlest-tertiary pt-4">
<Typography
type={TypographyType.Footnote}
color={TypographyColor.Tertiary}
className="mb-3"
>
Replies
</Typography>

<div className="flex flex-col gap-3">
{item.replies.map((reply) => (
<div
key={reply.id}
className="rounded-12 bg-surface-hover px-3 py-2"
>
<Typography type={TypographyType.Footnote} bold>
{`${reply.authorName || 'daily.dev team'} from daily.dev`}
</Typography>
<Typography
type={TypographyType.Callout}
className="mt-1 whitespace-pre-wrap"
>
{reply.body}
</Typography>
<Typography
type={TypographyType.Footnote}
color={TypographyColor.Tertiary}
className="mt-2"
>
{format(new Date(reply.createdAt), 'dd MMM yyyy')}
</Typography>
</div>
))}
</div>
</div>
)}
</article>
);
};
76 changes: 76 additions & 0 deletions packages/shared/src/graphql/feedback.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,12 @@ export interface FeedbackItem {
createdAt: string;
updatedAt: string;
replies: FeedbackReply[];
user?: {
id: string;
name?: string | null;
username?: string | null;
image?: string | null;
} | null;
}

export interface FeedbackInput {
Expand All @@ -53,6 +59,10 @@ type UserFeedbackData = {
userFeedback: Connection<FeedbackItem>;
};

type UserFeedbackByUserIdData = {
userFeedbackByUserId: Connection<FeedbackItem>;
};

export const SUBMIT_FEEDBACK_MUTATION = gql`
mutation SubmitFeedback($input: FeedbackInput!) {
submitFeedback(input: $input) {
Expand Down Expand Up @@ -90,6 +100,41 @@ export const USER_FEEDBACK_QUERY = gql`
}
`;

export const USER_FEEDBACK_BY_USER_ID_QUERY = gql`
query UserFeedbackByUserId($userId: ID!, $after: String, $first: Int) {
userFeedbackByUserId(userId: $userId, after: $after, first: $first) {
pageInfo {
endCursor
hasNextPage
}
edges {
cursor
node {
id
category
description
status
screenshotUrl
createdAt
updatedAt
user {
id
name
username
image
}
replies {
id
body
authorName
createdAt
}
}
}
}
}
`;

export const submitFeedback = (input: FeedbackInput): Promise<EmptyResponse> =>
gqlClient.request(SUBMIT_FEEDBACK_MUTATION, { input });

Expand All @@ -106,3 +151,34 @@ export const useUserFeedback = (first = 20) => {
getNextPageParam(lastPage?.userFeedback?.pageInfo),
});
};

export const useUserFeedbackByUserId = ({
userId,
first = 20,
enabled = true,
}: {
userId: string;
first?: number;
enabled?: boolean;
}) => {
return useInfiniteQuery({
queryKey: generateQueryKey(
RequestKey.UserFeedbackByUserId,
undefined,
userId,
),
queryFn: ({ pageParam }) =>
gqlClient.request<UserFeedbackByUserIdData>(
USER_FEEDBACK_BY_USER_ID_QUERY,
{
userId,
first,
after: pageParam,
},
),
initialPageParam: null as string | null,
getNextPageParam: (lastPage) =>
getNextPageParam(lastPage?.userFeedbackByUserId?.pageInfo),
enabled: enabled && !!userId,
});
};
1 change: 1 addition & 0 deletions packages/shared/src/lib/query.ts
Original file line number Diff line number Diff line change
Expand Up @@ -253,6 +253,7 @@ export enum RequestKey {
GearCategories = 'gear_categories',
PersonalAccessTokens = 'personal_access_tokens',
UserAchievements = 'user_achievements',
UserFeedbackByUserId = 'user_feedback_by_user_id',
TrackedAchievement = 'tracked_achievement',
AchievementSyncStatus = 'achievement_sync_status',
Arena = 'arena',
Expand Down
125 changes: 2 additions & 123 deletions packages/webapp/pages/settings/feedback.tsx
Original file line number Diff line number Diff line change
@@ -1,27 +1,17 @@
import type { ReactElement } from 'react';
import React, { useMemo, useState } from 'react';
import type { NextSeoProps } from 'next-seo';
import { format } from 'date-fns';
import {
Button,
ButtonVariant,
} from '@dailydotdev/shared/src/components/buttons/Button';
import { Button } from '@dailydotdev/shared/src/components/buttons/Button';
import InfiniteScrolling from '@dailydotdev/shared/src/components/containers/InfiniteScrolling';
import { FeedbackCard } from '@dailydotdev/shared/src/components/feedback/FeedbackCard';
import { Loader } from '@dailydotdev/shared/src/components/Loader';
import { LazyModal } from '@dailydotdev/shared/src/components/modals/common/types';
import {
Typography,
TypographyColor,
TypographyType,
} from '@dailydotdev/shared/src/components/typography/Typography';
import { useLazyModal } from '@dailydotdev/shared/src/hooks/useLazyModal';
import { useUserFeedback } from '@dailydotdev/shared/src/graphql/feedback';
import type { FeedbackItem } from '@dailydotdev/shared/src/graphql/feedback';
import {
getFeedbackCategoryLabel,
getFeedbackStatusClassName,
getFeedbackStatusLabel,
} from '@dailydotdev/shared/src/lib/feedback';
import { AccountPageContainer } from '../../components/layouts/SettingsLayout/AccountPageContainer';
import { getSettingsLayout } from '../../components/layouts/SettingsLayout';
import { defaultSeo } from '../../next-seo';
Expand All @@ -32,117 +22,6 @@ const seo: NextSeoProps = {
title: getTemplatedTitle('Your Feedback'),
};

const FeedbackCard = ({
item,
isExpanded,
onToggleExpand,
}: {
item: FeedbackItem;
isExpanded: boolean;
onToggleExpand: () => void;
}): ReactElement => {
const isLongDescription = item.description.length > 260;
const description = isExpanded
? item.description
: item.description.slice(0, 260);
const badgeClassName = 'rounded-14 bg-surface-hover px-2 py-1 text-xs';

return (
<article className="rounded-16 border border-border-subtlest-tertiary bg-background-default p-4">
<div className="flex flex-wrap items-center gap-2">
<span className={`${badgeClassName} text-text-secondary`}>
{getFeedbackCategoryLabel(item.category)}
</span>
<span
className={`${badgeClassName} ${getFeedbackStatusClassName(
item.status,
)}`}
>
{getFeedbackStatusLabel(item.status)}
</span>
<Typography
type={TypographyType.Footnote}
color={TypographyColor.Tertiary}
className="ml-auto"
>
{format(new Date(item.createdAt), 'dd MMM yyyy')}
</Typography>
</div>

<Typography
type={TypographyType.Body}
className="mt-3 whitespace-pre-wrap"
>
{description}
{isLongDescription && !isExpanded ? '...' : ''}
</Typography>

{isLongDescription && (
<Button
className="mt-2"
variant={ButtonVariant.Tertiary}
onClick={onToggleExpand}
>
{isExpanded ? 'Show less' : 'Show more'}
</Button>
)}

{item.screenshotUrl && (
<a
href={item.screenshotUrl}
target="_blank"
rel="noopener noreferrer"
className="mt-3 block"
>
<img
src={item.screenshotUrl}
alt="Feedback screenshot"
className="max-h-48 w-auto rounded-12 border border-border-subtlest-tertiary"
/>
</a>
)}

{item.replies.length > 0 && (
<div className="mt-4 border-t border-border-subtlest-tertiary pt-4">
<Typography
type={TypographyType.Footnote}
color={TypographyColor.Tertiary}
className="mb-3"
>
Replies
</Typography>

<div className="flex flex-col gap-3">
{item.replies.map((reply) => (
<div
key={reply.id}
className="rounded-12 bg-surface-hover px-3 py-2"
>
<Typography type={TypographyType.Footnote} bold>
{`${reply.authorName || 'daily.dev team'} from daily.dev`}
</Typography>
<Typography
type={TypographyType.Callout}
className="mt-1 whitespace-pre-wrap"
>
{reply.body}
</Typography>
<Typography
type={TypographyType.Footnote}
color={TypographyColor.Tertiary}
className="mt-2"
>
{format(new Date(reply.createdAt), 'dd MMM yyyy')}
</Typography>
</div>
))}
</div>
</div>
)}
</article>
);
};

const AccountFeedbackPage = (): ReactElement => {
const { openModal } = useLazyModal();
const query = useUserFeedback();
Expand Down
Loading