diff --git a/packages/shared/src/components/sidebar/sections/NetworkSection.tsx b/packages/shared/src/components/sidebar/sections/NetworkSection.tsx
index 0762ae8be2e..e5b659853fb 100644
--- a/packages/shared/src/components/sidebar/sections/NetworkSection.tsx
+++ b/packages/shared/src/components/sidebar/sections/NetworkSection.tsx
@@ -14,6 +14,7 @@ import type { SidebarSectionProps } from './common';
import { useSquadPendingPosts } from '../../../hooks/squads/useSquadPendingPosts';
import { Typography, TypographyColor } from '../../typography/Typography';
import { SourcePostModerationStatus } from '../../../graphql/squads';
+import { SquadFavoriteButton } from '../../squads/SquadFavoriteButton';
export const NetworkSection = ({
isItemsButton,
@@ -42,6 +43,7 @@ export const NetworkSection = ({
),
title: name,
path: `${webappUrl}squads/${handle}`,
+ rightIcon: () => ,
};
}) ?? [];
return [
diff --git a/packages/shared/src/components/squads/SquadFavoriteButton.tsx b/packages/shared/src/components/squads/SquadFavoriteButton.tsx
new file mode 100644
index 00000000000..80138ee553f
--- /dev/null
+++ b/packages/shared/src/components/squads/SquadFavoriteButton.tsx
@@ -0,0 +1,47 @@
+import type { MouseEvent, ReactElement } from 'react';
+import React, { useCallback } from 'react';
+import classNames from 'classnames';
+import type { Squad } from '../../graphql/sources';
+import { StarIcon } from '../icons';
+import type { IconSize } from '../Icon';
+import { useSquadFavorite } from '../../hooks/squads/useSquadFavorite';
+
+interface SquadFavoriteButtonProps {
+ squad: Squad;
+ className?: string;
+ iconSize?: IconSize;
+}
+
+export const SquadFavoriteButton = ({
+ squad,
+ className,
+ iconSize,
+}: SquadFavoriteButtonProps): ReactElement => {
+ const { toggleFavorite, isPending } = useSquadFavorite();
+ const isFavorited = !!squad.favoritedAt;
+
+ const onClick = useCallback(
+ (event: MouseEvent) => {
+ event.preventDefault();
+ event.stopPropagation();
+ toggleFavorite(squad);
+ },
+ [squad, toggleFavorite],
+ );
+
+ return (
+
+ );
+};
diff --git a/packages/shared/src/graphql/sources.ts b/packages/shared/src/graphql/sources.ts
index d3ba47e3594..6f863ca405f 100644
--- a/packages/shared/src/graphql/sources.ts
+++ b/packages/shared/src/graphql/sources.ts
@@ -88,6 +88,7 @@ export interface Squad extends Source {
referralUrl?: string;
category?: SourceCategory;
moderationPostCount: number;
+ favoritedAt?: string | null;
}
interface SourceFlags {
diff --git a/packages/shared/src/graphql/squads.ts b/packages/shared/src/graphql/squads.ts
index 6a7e00ce3a3..d1ec8546936 100644
--- a/packages/shared/src/graphql/squads.ts
+++ b/packages/shared/src/graphql/squads.ts
@@ -472,6 +472,14 @@ export const EXPAND_PINNED_POSTS_MUTATION = gql`
}
`;
+export const TOGGLE_FAVORITE_SOURCE_MUTATION = gql`
+ mutation ToggleFavoriteSource($sourceId: ID!) {
+ toggleFavoriteSource(sourceId: $sourceId) {
+ _
+ }
+ }
+`;
+
export const validateSourceHandle = (handle: string, source: Source): boolean =>
source.handle === handle || source.handle === handle.toLowerCase();
@@ -718,6 +726,16 @@ export const expandPinnedPosts = async (
return res.expandPinnedPosts;
};
+export const toggleFavoriteSource = async (
+ sourceId: string,
+): Promise => {
+ const res = await gqlClient.request(TOGGLE_FAVORITE_SOURCE_MUTATION, {
+ sourceId,
+ });
+
+ return res.toggleFavoriteSource;
+};
+
export const verifyPermission = (
squad: Pick,
permission: SourcePermissions,
diff --git a/packages/shared/src/hooks/squads/useSquadFavorite.ts b/packages/shared/src/hooks/squads/useSquadFavorite.ts
new file mode 100644
index 00000000000..c9716d8cd45
--- /dev/null
+++ b/packages/shared/src/hooks/squads/useSquadFavorite.ts
@@ -0,0 +1,35 @@
+import { useMutation } from '@tanstack/react-query';
+import type { Squad } from '../../graphql/sources';
+import { toggleFavoriteSource } from '../../graphql/squads';
+import { useBoot } from '../useBoot';
+
+type UseSquadFavorite = {
+ toggleFavorite: (squad: Squad) => void;
+ isPending: boolean;
+};
+
+export const useSquadFavorite = (): UseSquadFavorite => {
+ const { updateSquad } = useBoot();
+
+ const { mutate, isPending } = useMutation({
+ mutationFn: (squad: Squad) => {
+ if (!squad.id) {
+ throw new Error('Cannot toggle favorite on squad without id');
+ }
+ return toggleFavoriteSource(squad.id);
+ },
+ onMutate: (squad) => {
+ const previous = squad.favoritedAt ?? null;
+ updateSquad({
+ ...squad,
+ favoritedAt: previous ? null : new Date().toISOString(),
+ });
+ return { previous };
+ },
+ onError: (_err, squad, context) => {
+ updateSquad({ ...squad, favoritedAt: context?.previous ?? null });
+ },
+ });
+
+ return { toggleFavorite: mutate, isPending };
+};
diff --git a/packages/shared/src/hooks/useBoot.ts b/packages/shared/src/hooks/useBoot.ts
index 4aac0b09a78..1ced7ace25a 100644
--- a/packages/shared/src/hooks/useBoot.ts
+++ b/packages/shared/src/hooks/useBoot.ts
@@ -21,10 +21,22 @@ type UseBoot = {
getPlusEntryData: () => MarketingCta | null;
};
-const sortByName = (squads: Squad[]): Squad[] =>
- [...squads].sort((a, b) =>
- a.name.toLocaleLowerCase() > b.name.toLocaleLowerCase() ? 1 : -1,
- );
+const sortSquads = (squads: Squad[]): Squad[] =>
+ [...squads].sort((a, b) => {
+ const aFav = !!a.favoritedAt;
+ const bFav = !!b.favoritedAt;
+ if (aFav !== bFav) {
+ return aFav ? -1 : 1;
+ }
+ if (aFav && bFav) {
+ const aTime = new Date(a.favoritedAt as string).getTime();
+ const bTime = new Date(b.favoritedAt as string).getTime();
+ if (aTime !== bTime) {
+ return bTime - aTime;
+ }
+ }
+ return a.name.toLocaleLowerCase().localeCompare(b.name.toLocaleLowerCase());
+ });
export const useBoot = (): UseBoot => {
const router = useRouter();
@@ -40,7 +52,7 @@ export const useBoot = (): UseBoot => {
return;
}
- const squads = sortByName([...currentSquads, squad]);
+ const squads = sortSquads([...currentSquads, squad]);
client.setQueryData(BOOT_QUERY_KEY, { ...bootData, squads });
};
@@ -57,7 +69,7 @@ export const useBoot = (): UseBoot => {
);
client.setQueryData(BOOT_QUERY_KEY, {
...bootData,
- squads: sortByName(squads ?? []),
+ squads: sortSquads(squads ?? []),
});
};
diff --git a/packages/webapp/pages/squads/discover/my.tsx b/packages/webapp/pages/squads/discover/my.tsx
index bbb4901e7ff..6de75f471fd 100644
--- a/packages/webapp/pages/squads/discover/my.tsx
+++ b/packages/webapp/pages/squads/discover/my.tsx
@@ -2,6 +2,8 @@ import type { ReactElement } from 'react';
import React, { useEffect, useMemo } from 'react';
import { useAuthContext } from '@dailydotdev/shared/src/contexts/AuthContext';
import { SquadList } from '@dailydotdev/shared/src/components/cards/squad/SquadList';
+import { SquadFavoriteButton } from '@dailydotdev/shared/src/components/squads/SquadFavoriteButton';
+import { IconSize } from '@dailydotdev/shared/src/components/Icon';
import { useRouter } from 'next/router';
import {
squadCategoriesPaths,
@@ -60,7 +62,9 @@ const SquadSection = ({ squads, title }: SquadSectionProps): ReactElement => {
key={squad.handle}
squad={squad}
shouldShowCount={false}
- />
+ >
+
+
))}