From f9f650c77bd4f3c51362dda54238e0b265d45595 Mon Sep 17 00:00:00 2001 From: "claude[bot]" <41898282+claude[bot]@users.noreply.github.com> Date: Thu, 22 Jan 2026 19:07:40 +0000 Subject: [PATCH 1/5] feat: add upvote UI for hot takes - Add numUpvotes and userState fields to UserHotTake interface - Create useVoteHotTake hook with optimistic cache updates - Add upvote button with count display to HotTakeItem component - Integrate upvote functionality in ProfileUserHotTakes - Add HotTake entity type to UserVoteEntity enum - Add HotTakeList origin for analytics tracking Closes #5354 Co-Authored-By: Chris Bongers --- .../components/hotTakes/HotTakeItem.tsx | 82 ++++++-- .../hotTakes/ProfileUserHotTakes.tsx | 11 ++ .../shared/src/graphql/user/userHotTake.ts | 9 + packages/shared/src/hooks/vote/index.ts | 1 + packages/shared/src/hooks/vote/types.ts | 1 + .../shared/src/hooks/vote/useVoteHotTake.ts | 181 ++++++++++++++++++ packages/shared/src/lib/log.ts | 1 + 7 files changed, 265 insertions(+), 21 deletions(-) create mode 100644 packages/shared/src/hooks/vote/useVoteHotTake.ts diff --git a/packages/shared/src/features/profile/components/hotTakes/HotTakeItem.tsx b/packages/shared/src/features/profile/components/hotTakes/HotTakeItem.tsx index 2da789b0d9..385e2a78e3 100644 --- a/packages/shared/src/features/profile/components/hotTakes/HotTakeItem.tsx +++ b/packages/shared/src/features/profile/components/hotTakes/HotTakeItem.tsx @@ -11,14 +11,21 @@ import { Button, ButtonSize, ButtonVariant, + ButtonColor, } from '../../../../components/buttons/Button'; -import { EditIcon, TrashIcon } from '../../../../components/icons'; +import { EditIcon, TrashIcon, UpvoteIcon } from '../../../../components/icons'; +import { UserVote } from '../../../../graphql/posts'; +import InteractionCounter from '../../../../components/InteractionCounter'; +import { IconSize } from '../../../../components/Icon'; +import { QuaternaryButton } from '../../../../components/buttons/QuaternaryButton'; +import { Tooltip } from '../../../../components/tooltip/Tooltip'; interface HotTakeItemProps { item: UserHotTake; isOwner: boolean; onEdit?: (item: UserHotTake) => void; onDelete?: (item: UserHotTake) => void; + onUpvoteClick?: (item: UserHotTake) => void; } export function HotTakeItem({ @@ -26,8 +33,10 @@ export function HotTakeItem({ isOwner, onEdit, onDelete, + onUpvoteClick, }: HotTakeItemProps): ReactElement { const { emoji, title, subtitle } = item; + const isUpvoteActive = item.userState?.vote === UserVote.Up; return (
)}
- {isOwner && ( -
- {onEdit && ( -
- )} + icon={ + + } + > + {item.numUpvotes > 0 && ( + + )} + + + )} + {isOwner && ( +
+ {onEdit && ( +
+ )} + ); } diff --git a/packages/shared/src/features/profile/components/hotTakes/ProfileUserHotTakes.tsx b/packages/shared/src/features/profile/components/hotTakes/ProfileUserHotTakes.tsx index 8528f82b16..07f0b39ace 100644 --- a/packages/shared/src/features/profile/components/hotTakes/ProfileUserHotTakes.tsx +++ b/packages/shared/src/features/profile/components/hotTakes/ProfileUserHotTakes.tsx @@ -21,6 +21,8 @@ import type { } from '../../../../graphql/user/userHotTake'; import { useToastNotification } from '../../../../hooks/useToastNotification'; import { usePrompt } from '../../../../hooks/usePrompt'; +import { useVoteHotTake } from '../../../../hooks/vote/useVoteHotTake'; +import { Origin } from '../../../../lib/log'; interface ProfileUserHotTakesProps { user: PublicProfile; @@ -33,6 +35,7 @@ export function ProfileUserHotTakes({ useUserHotTakes(user); const { displayToast } = useToastNotification(); const { showPrompt } = usePrompt(); + const { toggleUpvote } = useVoteHotTake(); const [isModalOpen, setIsModalOpen] = useState(false); const [editingItem, setEditingItem] = useState(null); @@ -112,6 +115,13 @@ export function ProfileUserHotTakes({ setIsModalOpen(true); }, [canAddMore, displayToast]); + const handleUpvote = useCallback( + async (item: UserHotTake) => { + await toggleUpvote({ payload: item, origin: Origin.HotTakeList }); + }, + [toggleUpvote], + ); + const hasItems = hotTakes.length > 0; if (!hasItems && !isOwner) { @@ -149,6 +159,7 @@ export function ProfileUserHotTakes({ isOwner={isOwner} onEdit={handleEdit} onDelete={handleDelete} + onUpvoteClick={handleUpvote} /> ))} diff --git a/packages/shared/src/graphql/user/userHotTake.ts b/packages/shared/src/graphql/user/userHotTake.ts index cf3b2ae160..3716b01e3a 100644 --- a/packages/shared/src/graphql/user/userHotTake.ts +++ b/packages/shared/src/graphql/user/userHotTake.ts @@ -1,6 +1,7 @@ import { gql } from 'graphql-request'; import type { Connection } from '../common'; import { gqlClient } from '../common'; +import type { UserVote } from '../posts'; export interface UserHotTake { id: string; @@ -9,6 +10,10 @@ export interface UserHotTake { subtitle: string | null; position: number; createdAt: string; + numUpvotes: number; + userState?: { + vote: UserVote; + }; } export interface AddUserHotTakeInput { @@ -36,6 +41,10 @@ const USER_HOT_TAKE_FRAGMENT = gql` subtitle position createdAt + numUpvotes + userState { + vote + } } `; diff --git a/packages/shared/src/hooks/vote/index.ts b/packages/shared/src/hooks/vote/index.ts index 01a022d83b..129b2ab469 100644 --- a/packages/shared/src/hooks/vote/index.ts +++ b/packages/shared/src/hooks/vote/index.ts @@ -3,3 +3,4 @@ export * from './useVotePost'; export * from './useFeedVotePost'; export * from './useReadHistoryVotePost'; export * from './useVoteComment'; +export * from './useVoteHotTake'; diff --git a/packages/shared/src/hooks/vote/types.ts b/packages/shared/src/hooks/vote/types.ts index de70ca16d9..c37cdf0e13 100644 --- a/packages/shared/src/hooks/vote/types.ts +++ b/packages/shared/src/hooks/vote/types.ts @@ -46,6 +46,7 @@ export type UseVotePost = { export enum UserVoteEntity { Comment = 'comment', Post = 'post', + HotTake = 'userHotTake', } export type UseVoteMutationProps = { diff --git a/packages/shared/src/hooks/vote/useVoteHotTake.ts b/packages/shared/src/hooks/vote/useVoteHotTake.ts new file mode 100644 index 0000000000..0854338323 --- /dev/null +++ b/packages/shared/src/hooks/vote/useVoteHotTake.ts @@ -0,0 +1,181 @@ +import { useContext, useCallback } from 'react'; +import { useQueryClient } from '@tanstack/react-query'; +import AuthContext from '../../contexts/AuthContext'; +import { UserVote } from '../../graphql/posts'; +import { AuthTriggers } from '../../lib/auth'; +import type { UserHotTake } from '../../graphql/user/userHotTake'; +import type { Connection } from '../../graphql/common'; +import type { UseVoteProps, ToggleVoteProps } from './types'; +import { voteMutationHandlers, UserVoteEntity } from './types'; +import { useVote } from './useVote'; + +export interface UseVoteHotTakeProps extends Pick { + variables?: unknown; +} + +export interface UseVoteHotTake { + upvoteHotTake: (props: { id: string }) => Promise; + downvoteHotTake: (props: { id: string }) => Promise; + cancelHotTakeVote: (props: { id: string }) => Promise; + toggleUpvote: ( + props: Omit, 'entity'>, + ) => Promise; + toggleDownvote: ( + props: Omit, 'entity'>, + ) => Promise; +} + +const useVoteHotTake = ({ + onMutate, + variables, +}: UseVoteHotTakeProps = {}): UseVoteHotTake => { + const client = useQueryClient(); + const { user, showLogin } = useContext(AuthContext); + + const defaultOnMutate = ({ id, vote }) => { + const mutationHandler = voteMutationHandlers[vote]; + + if (!mutationHandler) { + return undefined; + } + + // Find and update the hot take in cache + const queryKeys = client + .getQueryCache() + .findAll({ queryKey: ['userHotTakes'] }); + + let previousVote: UserVote | undefined; + + queryKeys.forEach((query) => { + const data = query.state.data as { + userHotTakes: Connection; + }; + if (!data?.userHotTakes?.edges) { + return; + } + + const hotTakeEdge = data.userHotTakes.edges.find( + (edge) => edge.node.id === id, + ); + + if (hotTakeEdge) { + previousVote = hotTakeEdge.node.userState?.vote; + + client.setQueryData(query.queryKey, { + ...data, + userHotTakes: { + ...data.userHotTakes, + edges: data.userHotTakes.edges.map((edge) => + edge.node.id === id + ? { + ...edge, + node: { + ...edge.node, + ...mutationHandler(edge.node), + }, + } + : edge, + ), + }, + }); + } + }); + + return () => { + const rollbackMutationHandler = voteMutationHandlers[previousVote]; + + if (!rollbackMutationHandler) { + return; + } + + queryKeys.forEach((query) => { + const data = query.state.data as { + userHotTakes: Connection; + }; + if (!data?.userHotTakes?.edges) { + return; + } + + client.setQueryData(query.queryKey, { + ...data, + userHotTakes: { + ...data.userHotTakes, + edges: data.userHotTakes.edges.map((edge) => + edge.node.id === id + ? { + ...edge, + node: { + ...edge.node, + ...rollbackMutationHandler(edge.node), + }, + } + : edge, + ), + }, + }); + }); + }; + }; + + const { + upvote: upvoteHotTake, + downvote: downvoteHotTake, + cancelVote: cancelHotTakeVote, + } = useVote({ + onMutate: onMutate || defaultOnMutate, + entity: UserVoteEntity.HotTake, + variables, + }); + + const toggleUpvote: UseVoteHotTake['toggleUpvote'] = useCallback( + async ({ payload: hotTake }) => { + if (!hotTake) { + return; + } + + if (!user) { + showLogin({ trigger: AuthTriggers.Upvote }); + return; + } + + if (hotTake?.userState?.vote === UserVote.Up) { + await cancelHotTakeVote({ id: hotTake.id }); + return; + } + + await upvoteHotTake({ id: hotTake.id }); + }, + [cancelHotTakeVote, showLogin, upvoteHotTake, user], + ); + + const toggleDownvote: UseVoteHotTake['toggleDownvote'] = useCallback( + async ({ payload: hotTake }) => { + if (!hotTake) { + return; + } + + if (!user) { + showLogin({ trigger: AuthTriggers.Downvote }); + return; + } + + if (hotTake?.userState?.vote === UserVote.Down) { + await cancelHotTakeVote({ id: hotTake.id }); + return; + } + + await downvoteHotTake({ id: hotTake.id }); + }, + [cancelHotTakeVote, downvoteHotTake, showLogin, user], + ); + + return { + upvoteHotTake, + downvoteHotTake, + cancelHotTakeVote, + toggleUpvote, + toggleDownvote, + }; +}; + +export { useVoteHotTake }; diff --git a/packages/shared/src/lib/log.ts b/packages/shared/src/lib/log.ts index a7cd174998..4782aaf0c0 100644 --- a/packages/shared/src/lib/log.ts +++ b/packages/shared/src/lib/log.ts @@ -78,6 +78,7 @@ export enum Origin { BriefModal = 'brief modal', BriefPage = 'brief page', SquadBoost = 'squad boost', + HotTakeList = 'hot take list', } export enum LogEvent { From 8e618fa27461aacac2afc2dadab999723c63b291 Mon Sep 17 00:00:00 2001 From: Chris Bongers Date: Fri, 23 Jan 2026 11:00:46 +0200 Subject: [PATCH 2/5] feat: add upvote support for hot takes - Update UserHotTake interface to use upvotes/upvoted fields - Fix UserVoteEntity enum to use 'hot_take' matching API - Add hot take specific mutation handlers for optimistic updates - Fix cache query key to match useUserHotTakes - Move upvote button after edit/delete buttons Co-Authored-By: Claude Opus 4.5 --- .../components/hotTakes/HotTakeItem.tsx | 55 +++++---- .../shared/src/graphql/user/userHotTake.ts | 13 +-- packages/shared/src/hooks/vote/types.ts | 2 +- .../shared/src/hooks/vote/useVoteHotTake.ts | 106 +++++++++--------- 4 files changed, 86 insertions(+), 90 deletions(-) diff --git a/packages/shared/src/features/profile/components/hotTakes/HotTakeItem.tsx b/packages/shared/src/features/profile/components/hotTakes/HotTakeItem.tsx index 385e2a78e3..3b37538612 100644 --- a/packages/shared/src/features/profile/components/hotTakes/HotTakeItem.tsx +++ b/packages/shared/src/features/profile/components/hotTakes/HotTakeItem.tsx @@ -14,7 +14,6 @@ import { ButtonColor, } from '../../../../components/buttons/Button'; import { EditIcon, TrashIcon, UpvoteIcon } from '../../../../components/icons'; -import { UserVote } from '../../../../graphql/posts'; import InteractionCounter from '../../../../components/InteractionCounter'; import { IconSize } from '../../../../components/Icon'; import { QuaternaryButton } from '../../../../components/buttons/QuaternaryButton'; @@ -36,7 +35,7 @@ export function HotTakeItem({ onUpvoteClick, }: HotTakeItemProps): ReactElement { const { emoji, title, subtitle } = item; - const isUpvoteActive = item.userState?.vote === UserVote.Up; + const isUpvoteActive = item.upvoted; return (
- “{title}” + {title} {subtitle && (
+ {isOwner && ( +
+ {onEdit && ( +
+ )} {onUpvoteClick && ( } > - {item.numUpvotes > 0 && ( + {item.upvotes > 0 && ( )} )} - {isOwner && ( -
- {onEdit && ( -
- )}
); diff --git a/packages/shared/src/graphql/user/userHotTake.ts b/packages/shared/src/graphql/user/userHotTake.ts index 3716b01e3a..b76a2568b0 100644 --- a/packages/shared/src/graphql/user/userHotTake.ts +++ b/packages/shared/src/graphql/user/userHotTake.ts @@ -1,7 +1,6 @@ import { gql } from 'graphql-request'; import type { Connection } from '../common'; import { gqlClient } from '../common'; -import type { UserVote } from '../posts'; export interface UserHotTake { id: string; @@ -10,10 +9,8 @@ export interface UserHotTake { subtitle: string | null; position: number; createdAt: string; - numUpvotes: number; - userState?: { - vote: UserVote; - }; + upvotes: number; + upvoted?: boolean; } export interface AddUserHotTakeInput { @@ -41,10 +38,8 @@ const USER_HOT_TAKE_FRAGMENT = gql` subtitle position createdAt - numUpvotes - userState { - vote - } + upvotes + upvoted } `; diff --git a/packages/shared/src/hooks/vote/types.ts b/packages/shared/src/hooks/vote/types.ts index c37cdf0e13..36ec03658d 100644 --- a/packages/shared/src/hooks/vote/types.ts +++ b/packages/shared/src/hooks/vote/types.ts @@ -46,7 +46,7 @@ export type UseVotePost = { export enum UserVoteEntity { Comment = 'comment', Post = 'post', - HotTake = 'userHotTake', + HotTake = 'hot_take', } export type UseVoteMutationProps = { diff --git a/packages/shared/src/hooks/vote/useVoteHotTake.ts b/packages/shared/src/hooks/vote/useVoteHotTake.ts index 0854338323..34f1db7437 100644 --- a/packages/shared/src/hooks/vote/useVoteHotTake.ts +++ b/packages/shared/src/hooks/vote/useVoteHotTake.ts @@ -6,9 +6,27 @@ import { AuthTriggers } from '../../lib/auth'; import type { UserHotTake } from '../../graphql/user/userHotTake'; import type { Connection } from '../../graphql/common'; import type { UseVoteProps, ToggleVoteProps } from './types'; -import { voteMutationHandlers, UserVoteEntity } from './types'; +import { UserVoteEntity } from './types'; import { useVote } from './useVote'; +const hotTakeMutationHandlers: Record< + UserVote, + (hotTake: UserHotTake) => Partial +> = { + [UserVote.Up]: (hotTake) => ({ + upvotes: hotTake.upvotes + 1, + upvoted: true, + }), + [UserVote.Down]: (hotTake) => ({ + upvotes: hotTake.upvoted ? hotTake.upvotes - 1 : hotTake.upvotes, + upvoted: false, + }), + [UserVote.None]: (hotTake) => ({ + upvotes: hotTake.upvoted ? hotTake.upvotes - 1 : hotTake.upvotes, + upvoted: false, + }), +}; + export interface UseVoteHotTakeProps extends Pick { variables?: unknown; } @@ -33,7 +51,7 @@ const useVoteHotTake = ({ const { user, showLogin } = useContext(AuthContext); const defaultOnMutate = ({ id, vote }) => { - const mutationHandler = voteMutationHandlers[vote]; + const mutationHandler = hotTakeMutationHandlers[vote]; if (!mutationHandler) { return undefined; @@ -42,76 +60,64 @@ const useVoteHotTake = ({ // Find and update the hot take in cache const queryKeys = client .getQueryCache() - .findAll({ queryKey: ['userHotTakes'] }); + .findAll({ queryKey: ['user_hot_takes'] }); let previousVote: UserVote | undefined; queryKeys.forEach((query) => { - const data = query.state.data as { - userHotTakes: Connection; - }; - if (!data?.userHotTakes?.edges) { + const data = query.state.data as Connection; + if (!data?.edges) { return; } - const hotTakeEdge = data.userHotTakes.edges.find( - (edge) => edge.node.id === id, - ); + const hotTakeEdge = data.edges.find((edge) => edge.node.id === id); if (hotTakeEdge) { - previousVote = hotTakeEdge.node.userState?.vote; + previousVote = hotTakeEdge.node.upvoted ? UserVote.Up : UserVote.None; client.setQueryData(query.queryKey, { ...data, - userHotTakes: { - ...data.userHotTakes, - edges: data.userHotTakes.edges.map((edge) => - edge.node.id === id - ? { - ...edge, - node: { - ...edge.node, - ...mutationHandler(edge.node), - }, - } - : edge, - ), - }, + edges: data.edges.map((edge) => + edge.node.id === id + ? { + ...edge, + node: { + ...edge.node, + ...mutationHandler(edge.node), + }, + } + : edge, + ), }); } }); return () => { - const rollbackMutationHandler = voteMutationHandlers[previousVote]; + const rollbackMutationHandler = hotTakeMutationHandlers[previousVote]; if (!rollbackMutationHandler) { return; } queryKeys.forEach((query) => { - const data = query.state.data as { - userHotTakes: Connection; - }; - if (!data?.userHotTakes?.edges) { + const data = query.state.data as Connection; + if (!data?.edges) { return; } client.setQueryData(query.queryKey, { ...data, - userHotTakes: { - ...data.userHotTakes, - edges: data.userHotTakes.edges.map((edge) => - edge.node.id === id - ? { - ...edge, - node: { - ...edge.node, - ...rollbackMutationHandler(edge.node), - }, - } - : edge, - ), - }, + edges: data.edges.map((edge) => + edge.node.id === id + ? { + ...edge, + node: { + ...edge.node, + ...rollbackMutationHandler(edge.node), + }, + } + : edge, + ), }); }); }; @@ -138,7 +144,7 @@ const useVoteHotTake = ({ return; } - if (hotTake?.userState?.vote === UserVote.Up) { + if (hotTake?.upvoted) { await cancelHotTakeVote({ id: hotTake.id }); return; } @@ -159,14 +165,10 @@ const useVoteHotTake = ({ return; } - if (hotTake?.userState?.vote === UserVote.Down) { - await cancelHotTakeVote({ id: hotTake.id }); - return; - } - - await downvoteHotTake({ id: hotTake.id }); + // Hot takes don't support downvotes, just cancel the vote + await cancelHotTakeVote({ id: hotTake.id }); }, - [cancelHotTakeVote, downvoteHotTake, showLogin, user], + [cancelHotTakeVote, showLogin, user], ); return { From d39396c3e140492281def7aeaf3691d7ee345774 Mon Sep 17 00:00:00 2001 From: Chris Bongers Date: Fri, 23 Jan 2026 12:14:13 +0200 Subject: [PATCH 3/5] feat(profile): update hot take types to match API refactor Rename UserHotTake to HotTake, update GraphQL queries and mutations to use new names (hotTakes, addHotTake, etc.). Update all component and hook imports accordingly. Co-Authored-By: Claude Opus 4.5 --- .../components/hotTakes/HotTakeItem.tsx | 10 +- .../components/hotTakes/HotTakeModal.tsx | 8 +- .../hotTakes/ProfileUserHotTakes.tsx | 16 +-- .../features/profile/hooks/useUserHotTakes.ts | 29 +++-- .../shared/src/graphql/user/userHotTake.ts | 106 +++++++++--------- .../shared/src/hooks/vote/useVoteHotTake.ts | 12 +- 6 files changed, 89 insertions(+), 92 deletions(-) diff --git a/packages/shared/src/features/profile/components/hotTakes/HotTakeItem.tsx b/packages/shared/src/features/profile/components/hotTakes/HotTakeItem.tsx index 3b37538612..0b1b193a94 100644 --- a/packages/shared/src/features/profile/components/hotTakes/HotTakeItem.tsx +++ b/packages/shared/src/features/profile/components/hotTakes/HotTakeItem.tsx @@ -1,7 +1,7 @@ import type { ReactElement } from 'react'; import React from 'react'; import classNames from 'classnames'; -import type { UserHotTake } from '../../../../graphql/user/userHotTake'; +import type { HotTake } from '../../../../graphql/user/userHotTake'; import { Typography, TypographyType, @@ -20,11 +20,11 @@ import { QuaternaryButton } from '../../../../components/buttons/QuaternaryButto import { Tooltip } from '../../../../components/tooltip/Tooltip'; interface HotTakeItemProps { - item: UserHotTake; + item: HotTake; isOwner: boolean; - onEdit?: (item: UserHotTake) => void; - onDelete?: (item: UserHotTake) => void; - onUpvoteClick?: (item: UserHotTake) => void; + onEdit?: (item: HotTake) => void; + onDelete?: (item: HotTake) => void; + onUpvoteClick?: (item: HotTake) => void; } export function HotTakeItem({ diff --git a/packages/shared/src/features/profile/components/hotTakes/HotTakeModal.tsx b/packages/shared/src/features/profile/components/hotTakes/HotTakeModal.tsx index 9b95278c38..aef305ff97 100644 --- a/packages/shared/src/features/profile/components/hotTakes/HotTakeModal.tsx +++ b/packages/shared/src/features/profile/components/hotTakes/HotTakeModal.tsx @@ -11,8 +11,8 @@ import { Button, ButtonVariant } from '../../../../components/buttons/Button'; import { ModalHeader } from '../../../../components/modals/common/ModalHeader'; import { useViewSize, ViewSize } from '../../../../hooks'; import type { - UserHotTake, - AddUserHotTakeInput, + HotTake, + AddHotTakeInput, } from '../../../../graphql/user/userHotTake'; const EmojiPicker = dynamic( @@ -32,8 +32,8 @@ const hotTakeFormSchema = z.object({ type HotTakeFormData = z.infer; type HotTakeModalProps = Omit & { - onSubmit: (input: AddUserHotTakeInput) => Promise; - existingItem?: UserHotTake; + onSubmit: (input: AddHotTakeInput) => Promise; + existingItem?: HotTake; }; export function HotTakeModal({ diff --git a/packages/shared/src/features/profile/components/hotTakes/ProfileUserHotTakes.tsx b/packages/shared/src/features/profile/components/hotTakes/ProfileUserHotTakes.tsx index 07f0b39ace..eda367021c 100644 --- a/packages/shared/src/features/profile/components/hotTakes/ProfileUserHotTakes.tsx +++ b/packages/shared/src/features/profile/components/hotTakes/ProfileUserHotTakes.tsx @@ -16,8 +16,8 @@ import { PlusIcon } from '../../../../components/icons'; import { HotTakeItem } from './HotTakeItem'; import { HotTakeModal } from './HotTakeModal'; import type { - UserHotTake, - AddUserHotTakeInput, + HotTake, + AddHotTakeInput, } from '../../../../graphql/user/userHotTake'; import { useToastNotification } from '../../../../hooks/useToastNotification'; import { usePrompt } from '../../../../hooks/usePrompt'; @@ -38,10 +38,10 @@ export function ProfileUserHotTakes({ const { toggleUpvote } = useVoteHotTake(); const [isModalOpen, setIsModalOpen] = useState(false); - const [editingItem, setEditingItem] = useState(null); + const [editingItem, setEditingItem] = useState(null); const handleAdd = useCallback( - async (input: AddUserHotTakeInput) => { + async (input: AddHotTakeInput) => { try { await add(input); displayToast('Hot take added'); @@ -53,13 +53,13 @@ export function ProfileUserHotTakes({ [add, displayToast], ); - const handleEdit = useCallback((item: UserHotTake) => { + const handleEdit = useCallback((item: HotTake) => { setEditingItem(item); setIsModalOpen(true); }, []); const handleUpdate = useCallback( - async (input: AddUserHotTakeInput) => { + async (input: AddHotTakeInput) => { if (!editingItem) { return; } @@ -82,7 +82,7 @@ export function ProfileUserHotTakes({ ); const handleDelete = useCallback( - async (item: UserHotTake) => { + async (item: HotTake) => { const confirmed = await showPrompt({ title: 'Remove hot take?', description: `Are you sure you want to remove "${item.title}"?`, @@ -116,7 +116,7 @@ export function ProfileUserHotTakes({ }, [canAddMore, displayToast]); const handleUpvote = useCallback( - async (item: UserHotTake) => { + async (item: HotTake) => { await toggleUpvote({ payload: item, origin: Origin.HotTakeList }); }, [toggleUpvote], diff --git a/packages/shared/src/features/profile/hooks/useUserHotTakes.ts b/packages/shared/src/features/profile/hooks/useUserHotTakes.ts index 8d4285ebf9..af190a7c00 100644 --- a/packages/shared/src/features/profile/hooks/useUserHotTakes.ts +++ b/packages/shared/src/features/profile/hooks/useUserHotTakes.ts @@ -2,16 +2,16 @@ import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; import { useMemo, useCallback } from 'react'; import type { PublicProfile } from '../../../lib/user'; import type { - AddUserHotTakeInput, - UpdateUserHotTakeInput, - ReorderUserHotTakeInput, + AddHotTakeInput, + UpdateHotTakeInput, + ReorderHotTakeInput, } from '../../../graphql/user/userHotTake'; import { - getUserHotTakes, - addUserHotTake, - updateUserHotTake, - deleteUserHotTake, - reorderUserHotTakes, + getHotTakes, + addHotTake, + updateHotTake, + deleteHotTake, + reorderHotTakes, } from '../../../graphql/user/userHotTake'; import { generateQueryKey, RequestKey, StaleTime } from '../../../lib/query'; import { useAuthContext } from '../../../contexts/AuthContext'; @@ -27,7 +27,7 @@ export function useUserHotTakes(user: PublicProfile | null) { const query = useQuery({ queryKey, - queryFn: () => getUserHotTakes(user?.id as string), + queryFn: () => getHotTakes(user?.id as string), staleTime: StaleTime.Default, enabled: !!user?.id, }); @@ -44,7 +44,7 @@ export function useUserHotTakes(user: PublicProfile | null) { }, [queryClient, queryKey]); const addMutation = useMutation({ - mutationFn: (input: AddUserHotTakeInput) => addUserHotTake(input), + mutationFn: (input: AddHotTakeInput) => addHotTake(input), onSuccess: invalidateQuery, }); @@ -54,19 +54,18 @@ export function useUserHotTakes(user: PublicProfile | null) { input, }: { id: string; - input: UpdateUserHotTakeInput; - }) => updateUserHotTake(id, input), + input: UpdateHotTakeInput; + }) => updateHotTake(id, input), onSuccess: invalidateQuery, }); const deleteMutation = useMutation({ - mutationFn: (id: string) => deleteUserHotTake(id), + mutationFn: (id: string) => deleteHotTake(id), onSuccess: invalidateQuery, }); const reorderMutation = useMutation({ - mutationFn: (items: ReorderUserHotTakeInput[]) => - reorderUserHotTakes(items), + mutationFn: (items: ReorderHotTakeInput[]) => reorderHotTakes(items), onSuccess: invalidateQuery, }); diff --git a/packages/shared/src/graphql/user/userHotTake.ts b/packages/shared/src/graphql/user/userHotTake.ts index b76a2568b0..07adfa22f0 100644 --- a/packages/shared/src/graphql/user/userHotTake.ts +++ b/packages/shared/src/graphql/user/userHotTake.ts @@ -2,7 +2,7 @@ import { gql } from 'graphql-request'; import type { Connection } from '../common'; import { gqlClient } from '../common'; -export interface UserHotTake { +export interface HotTake { id: string; emoji: string; title: string; @@ -13,25 +13,25 @@ export interface UserHotTake { upvoted?: boolean; } -export interface AddUserHotTakeInput { +export interface AddHotTakeInput { emoji: string; title: string; subtitle?: string; } -export interface UpdateUserHotTakeInput { +export interface UpdateHotTakeInput { emoji?: string; title?: string; subtitle?: string | null; } -export interface ReorderUserHotTakeInput { +export interface ReorderHotTakeInput { id: string; position: number; } -const USER_HOT_TAKE_FRAGMENT = gql` - fragment UserHotTakeFragment on UserHotTake { +const HOT_TAKE_FRAGMENT = gql` + fragment HotTakeFragment on HotTake { id emoji title @@ -43,12 +43,12 @@ const USER_HOT_TAKE_FRAGMENT = gql` } `; -const USER_HOT_TAKES_QUERY = gql` - query UserHotTakes($userId: ID!, $first: Int, $after: String) { - userHotTakes(userId: $userId, first: $first, after: $after) { +const HOT_TAKES_QUERY = gql` + query HotTakes($userId: ID!, $first: Int, $after: String) { + hotTakes(userId: $userId, first: $first, after: $after) { edges { node { - ...UserHotTakeFragment + ...HotTakeFragment } } pageInfo { @@ -57,82 +57,80 @@ const USER_HOT_TAKES_QUERY = gql` } } } - ${USER_HOT_TAKE_FRAGMENT} + ${HOT_TAKE_FRAGMENT} `; -const ADD_USER_HOT_TAKE_MUTATION = gql` - mutation AddUserHotTake($input: AddUserHotTakeInput!) { - addUserHotTake(input: $input) { - ...UserHotTakeFragment +const ADD_HOT_TAKE_MUTATION = gql` + mutation AddHotTake($input: AddHotTakeInput!) { + addHotTake(input: $input) { + ...HotTakeFragment } } - ${USER_HOT_TAKE_FRAGMENT} + ${HOT_TAKE_FRAGMENT} `; -const UPDATE_USER_HOT_TAKE_MUTATION = gql` - mutation UpdateUserHotTake($id: ID!, $input: UpdateUserHotTakeInput!) { - updateUserHotTake(id: $id, input: $input) { - ...UserHotTakeFragment +const UPDATE_HOT_TAKE_MUTATION = gql` + mutation UpdateHotTake($id: ID!, $input: UpdateHotTakeInput!) { + updateHotTake(id: $id, input: $input) { + ...HotTakeFragment } } - ${USER_HOT_TAKE_FRAGMENT} + ${HOT_TAKE_FRAGMENT} `; -const DELETE_USER_HOT_TAKE_MUTATION = gql` - mutation DeleteUserHotTake($id: ID!) { - deleteUserHotTake(id: $id) { +const DELETE_HOT_TAKE_MUTATION = gql` + mutation DeleteHotTake($id: ID!) { + deleteHotTake(id: $id) { _ } } `; -const REORDER_USER_HOT_TAKES_MUTATION = gql` - mutation ReorderUserHotTakes($items: [ReorderUserHotTakeInput!]!) { - reorderUserHotTakes(items: $items) { - ...UserHotTakeFragment +const REORDER_HOT_TAKES_MUTATION = gql` + mutation ReorderHotTakes($items: [ReorderHotTakeInput!]!) { + reorderHotTakes(items: $items) { + ...HotTakeFragment } } - ${USER_HOT_TAKE_FRAGMENT} + ${HOT_TAKE_FRAGMENT} `; -export const getUserHotTakes = async ( +export const getHotTakes = async ( userId: string, first = 50, -): Promise> => { +): Promise> => { const result = await gqlClient.request<{ - userHotTakes: Connection; - }>(USER_HOT_TAKES_QUERY, { userId, first }); - return result.userHotTakes; + hotTakes: Connection; + }>(HOT_TAKES_QUERY, { userId, first }); + return result.hotTakes; }; -export const addUserHotTake = async ( - input: AddUserHotTakeInput, -): Promise => { +export const addHotTake = async (input: AddHotTakeInput): Promise => { const result = await gqlClient.request<{ - addUserHotTake: UserHotTake; - }>(ADD_USER_HOT_TAKE_MUTATION, { input }); - return result.addUserHotTake; + addHotTake: HotTake; + }>(ADD_HOT_TAKE_MUTATION, { input }); + return result.addHotTake; }; -export const updateUserHotTake = async ( +export const updateHotTake = async ( id: string, - input: UpdateUserHotTakeInput, -): Promise => { + input: UpdateHotTakeInput, +): Promise => { const result = await gqlClient.request<{ - updateUserHotTake: UserHotTake; - }>(UPDATE_USER_HOT_TAKE_MUTATION, { id, input }); - return result.updateUserHotTake; + updateHotTake: HotTake; + }>(UPDATE_HOT_TAKE_MUTATION, { id, input }); + return result.updateHotTake; }; -export const deleteUserHotTake = async (id: string): Promise => { - await gqlClient.request(DELETE_USER_HOT_TAKE_MUTATION, { id }); +export const deleteHotTake = async (id: string): Promise => { + await gqlClient.request(DELETE_HOT_TAKE_MUTATION, { id }); }; -export const reorderUserHotTakes = async ( - items: ReorderUserHotTakeInput[], -): Promise => { +export const reorderHotTakes = async ( + items: ReorderHotTakeInput[], +): Promise => { const result = await gqlClient.request<{ - reorderUserHotTakes: UserHotTake[]; - }>(REORDER_USER_HOT_TAKES_MUTATION, { items }); - return result.reorderUserHotTakes; + reorderHotTakes: HotTake[]; + }>(REORDER_HOT_TAKES_MUTATION, { items }); + return result.reorderHotTakes; }; diff --git a/packages/shared/src/hooks/vote/useVoteHotTake.ts b/packages/shared/src/hooks/vote/useVoteHotTake.ts index 34f1db7437..613e056ca8 100644 --- a/packages/shared/src/hooks/vote/useVoteHotTake.ts +++ b/packages/shared/src/hooks/vote/useVoteHotTake.ts @@ -3,7 +3,7 @@ import { useQueryClient } from '@tanstack/react-query'; import AuthContext from '../../contexts/AuthContext'; import { UserVote } from '../../graphql/posts'; import { AuthTriggers } from '../../lib/auth'; -import type { UserHotTake } from '../../graphql/user/userHotTake'; +import type { HotTake } from '../../graphql/user/userHotTake'; import type { Connection } from '../../graphql/common'; import type { UseVoteProps, ToggleVoteProps } from './types'; import { UserVoteEntity } from './types'; @@ -11,7 +11,7 @@ import { useVote } from './useVote'; const hotTakeMutationHandlers: Record< UserVote, - (hotTake: UserHotTake) => Partial + (hotTake: HotTake) => Partial > = { [UserVote.Up]: (hotTake) => ({ upvotes: hotTake.upvotes + 1, @@ -36,10 +36,10 @@ export interface UseVoteHotTake { downvoteHotTake: (props: { id: string }) => Promise; cancelHotTakeVote: (props: { id: string }) => Promise; toggleUpvote: ( - props: Omit, 'entity'>, + props: Omit, 'entity'>, ) => Promise; toggleDownvote: ( - props: Omit, 'entity'>, + props: Omit, 'entity'>, ) => Promise; } @@ -65,7 +65,7 @@ const useVoteHotTake = ({ let previousVote: UserVote | undefined; queryKeys.forEach((query) => { - const data = query.state.data as Connection; + const data = query.state.data as Connection; if (!data?.edges) { return; } @@ -100,7 +100,7 @@ const useVoteHotTake = ({ } queryKeys.forEach((query) => { - const data = query.state.data as Connection; + const data = query.state.data as Connection; if (!data?.edges) { return; } From 9674c94f3e6a2b83ca19ff6c759fdc64ac028923 Mon Sep 17 00:00:00 2001 From: Chris Bongers Date: Fri, 23 Jan 2026 12:56:10 +0200 Subject: [PATCH 4/5] fix: lint --- .../shared/src/features/profile/hooks/useUserHotTakes.ts | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/packages/shared/src/features/profile/hooks/useUserHotTakes.ts b/packages/shared/src/features/profile/hooks/useUserHotTakes.ts index af190a7c00..b7b9b2dc78 100644 --- a/packages/shared/src/features/profile/hooks/useUserHotTakes.ts +++ b/packages/shared/src/features/profile/hooks/useUserHotTakes.ts @@ -49,13 +49,8 @@ export function useUserHotTakes(user: PublicProfile | null) { }); const updateMutation = useMutation({ - mutationFn: ({ - id, - input, - }: { - id: string; - input: UpdateHotTakeInput; - }) => updateHotTake(id, input), + mutationFn: ({ id, input }: { id: string; input: UpdateHotTakeInput }) => + updateHotTake(id, input), onSuccess: invalidateQuery, }); From ae8abecc6adfb925c24acb8310f20cf38135b1bc Mon Sep 17 00:00:00 2001 From: Chris Bongers Date: Fri, 23 Jan 2026 13:03:05 +0200 Subject: [PATCH 5/5] refactor: rename useUserHotTakes to useHotTakes Co-Authored-By: Claude Opus 4.5 --- .../profile/components/hotTakes/ProfileUserHotTakes.tsx | 4 ++-- .../profile/hooks/{useUserHotTakes.ts => useHotTakes.ts} | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) rename packages/shared/src/features/profile/hooks/{useUserHotTakes.ts => useHotTakes.ts} (97%) diff --git a/packages/shared/src/features/profile/components/hotTakes/ProfileUserHotTakes.tsx b/packages/shared/src/features/profile/components/hotTakes/ProfileUserHotTakes.tsx index eda367021c..6a2e56e170 100644 --- a/packages/shared/src/features/profile/components/hotTakes/ProfileUserHotTakes.tsx +++ b/packages/shared/src/features/profile/components/hotTakes/ProfileUserHotTakes.tsx @@ -1,7 +1,7 @@ import type { ReactElement } from 'react'; import React, { useState, useCallback } from 'react'; import type { PublicProfile } from '../../../../lib/user'; -import { useUserHotTakes, MAX_HOT_TAKES } from '../../hooks/useUserHotTakes'; +import { useHotTakes, MAX_HOT_TAKES } from '../../hooks/useHotTakes'; import { Typography, TypographyType, @@ -32,7 +32,7 @@ export function ProfileUserHotTakes({ user, }: ProfileUserHotTakesProps): ReactElement | null { const { hotTakes, isOwner, canAddMore, add, update, remove } = - useUserHotTakes(user); + useHotTakes(user); const { displayToast } = useToastNotification(); const { showPrompt } = usePrompt(); const { toggleUpvote } = useVoteHotTake(); diff --git a/packages/shared/src/features/profile/hooks/useUserHotTakes.ts b/packages/shared/src/features/profile/hooks/useHotTakes.ts similarity index 97% rename from packages/shared/src/features/profile/hooks/useUserHotTakes.ts rename to packages/shared/src/features/profile/hooks/useHotTakes.ts index b7b9b2dc78..4d6b066f61 100644 --- a/packages/shared/src/features/profile/hooks/useUserHotTakes.ts +++ b/packages/shared/src/features/profile/hooks/useHotTakes.ts @@ -18,7 +18,7 @@ import { useAuthContext } from '../../../contexts/AuthContext'; export const MAX_HOT_TAKES = 5; -export function useUserHotTakes(user: PublicProfile | null) { +export const useHotTakes = (user: PublicProfile | null) => { const queryClient = useQueryClient(); const { user: loggedUser } = useAuthContext(); const isOwner = loggedUser?.id === user?.id; @@ -79,4 +79,4 @@ export function useUserHotTakes(user: PublicProfile | null) { isDeleting: deleteMutation.isPending, isReordering: reorderMutation.isPending, }; -} +};