Skip to content

Commit

Permalink
react: useAddProfileInterests and useRemoveProfileInterests
Browse files Browse the repository at this point in the history
  • Loading branch information
krzysu committed Apr 24, 2024
1 parent 0d4338d commit e9a5840
Show file tree
Hide file tree
Showing 13 changed files with 401 additions and 0 deletions.
8 changes: 8 additions & 0 deletions .changeset/quick-onions-guess.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
---
"@lens-protocol/domain": minor
"@lens-protocol/react": minor
"@lens-protocol/react-native": minor
"@lens-protocol/react-web": minor
---

**feat:** added hooks to manage profile interests: useAddProfileInterests and useRemoveProfileInterests
2 changes: 2 additions & 0 deletions examples/web/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ import {
UseProfileActionHistory,
UseProfileFollowers,
UseProfileFollowing,
UseProfileInterests,
UseProfileManagers,
UseProfiles,
UseRecommendProfileToggle,
Expand Down Expand Up @@ -151,6 +152,7 @@ export function App() {
<Route path="useBlockedProfiles" element={<UseBlockedProfiles />} />
<Route path="useReportProfile" element={<UseReportProfile />} />
<Route path="useRecommendProfileToggle" element={<UseRecommendProfileToggle />} />
<Route path="useProfileInterests" element={<UseProfileInterests />} />
</Route>

<Route path="/discovery">
Expand Down
5 changes: 5 additions & 0 deletions examples/web/src/profiles/ProfilesPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,11 @@ const profileHooks = [
description: 'Recommend a profile.',
path: '/profiles/useRecommendProfileToggle',
},
{
label: 'useProfileInterests',
description: 'Add and remove profile interests.',
path: '/profiles/useProfileInterests',
},
];

export function ProfilesPage() {
Expand Down
122 changes: 122 additions & 0 deletions examples/web/src/profiles/UseProfileInterests.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
import {
useAddProfileInterests,
useRemoveProfileInterests,
ProfileInterestTypes,
Profile,
} from '@lens-protocol/react-web';
import { Fragment, useMemo } from 'react';

import { RequireProfileSession } from '../components/auth';

// Capitalizes each word in a string
function capitalize(label: string): string {
return label.toLowerCase().replace(/\b\w/g, (char) => char.toUpperCase());
}

type Interest = {
parent: string;
value: ProfileInterestTypes;
label: string;
};

// Processes raw interest types into structured interests array
function createInterests(categories: ProfileInterestTypes[]): Interest[] {
return categories.map((item) => {
const [parent, subcategory] = item.split('__');
const label = capitalize(
subcategory ? subcategory.replace(/_/g, ' ') : parent.replace(/_/g, ' '),
);
return { parent, value: item, label };
});
}

type ButtonProps = {
isActive: boolean;
onClick: () => void;
children: React.ReactNode;
};

function ToggleButton({ isActive, onClick, children }: ButtonProps) {
const normalStyle = {
backgroundColor: 'transparent',
border: '1px solid grey',
color: '#111',
outline: 'none',
};

const activeStyle = {
...normalStyle,
backgroundColor: '#333',
color: '#eee',
};

return (
<button style={isActive ? activeStyle : normalStyle} onClick={onClick}>
{children}
</button>
);
}

function UseProfileInterestsInner({ profile }: { profile: Profile }) {
const { execute: addInterests } = useAddProfileInterests();
const { execute: removeInterests } = useRemoveProfileInterests();

const groupedInterests = useMemo(() => {
const interests = createInterests(Object.values(ProfileInterestTypes));

// Group interests by category
return interests.reduce((acc, interest) => {
acc[interest.parent] = acc[interest.parent] || [];
acc[interest.parent].push(interest);
return acc;
}, {} as Record<string, Interest[]>);
}, []);

const handleClick = async (interest: ProfileInterestTypes) => {
const request = {
interests: [interest],
};

if (profile.interests.includes(interest)) {
await removeInterests(request);
} else {
await addInterests(request);
}
};

return (
<div>
{Object.entries(groupedInterests).map(([category, items]) => (
<div key={category}>
<h4>{capitalize(category.replace(/_/g, ' '))}</h4>
<div>
{items.map((item) => (
<Fragment key={item.value}>
<ToggleButton
onClick={() => handleClick(item.value)}
isActive={profile.interests.includes(item.value)}
>
{item.label}
</ToggleButton>{' '}
</Fragment>
))}
</div>
</div>
))}
</div>
);
}

export function UseProfileInterests() {
return (
<div>
<h2>
<code>useAddProfileInterests & useRemoveProfileInterests</code>
</h2>

<RequireProfileSession message="Log in to view this example.">
{({ profile }) => <UseProfileInterestsInner profile={profile} />}
</RequireProfileSession>
</div>
);
}
1 change: 1 addition & 0 deletions examples/web/src/profiles/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ export * from './UseProfile';
export * from './UseProfileActionHistory';
export * from './UseProfileFollowers';
export * from './UseProfileFollowing';
export * from './UseProfileInterests';
export * from './UseProfileManagers';
export * from './UseProfiles';
export * from './UseRecommendProfileToggle';
Expand Down
32 changes: 32 additions & 0 deletions packages/domain/src/use-cases/profile/ManageProfileInterests.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { ProfileId } from '../../entities';

export type ProfileInterestsRequest = {
profileId: ProfileId;
};

export interface IProfileInterestsGateway {
add(request: ProfileInterestsRequest): Promise<void>;
remove(request: ProfileInterestsRequest): Promise<void>;
}

export interface IProfileInterestsPresenter {
add(request: ProfileInterestsRequest): Promise<void>;
remove(request: ProfileInterestsRequest): Promise<void>;
}

export class ManageProfileInterests {
constructor(
private readonly gateway: IProfileInterestsGateway,
private readonly presenter: IProfileInterestsPresenter,
) {}

async add(request: ProfileInterestsRequest) {
void this.gateway.add(request);
await this.presenter.add(request);
}

async remove(request: ProfileInterestsRequest) {
void this.gateway.remove(request);
await this.presenter.remove(request);
}
}
1 change: 1 addition & 0 deletions packages/domain/src/use-cases/profile/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ export * from './DismissRecommendedProfiles';
export * from './FollowPolicy';
export * from './FollowProfile';
export * from './LinkHandle';
export * from './ManageProfileInterests';
export * from './ReportProfile';
export * from './SetProfileMetadata';
export * from './ToggleProfileProperty';
Expand Down
43 changes: 43 additions & 0 deletions packages/react/src/profile/adapters/ProfileInterestsGateway.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import {
AddProfileInterestsData,
AddProfileInterestsDocument,
AddProfileInterestsVariables,
ProfileInterestTypes,
RemoveProfileInterestsData,
RemoveProfileInterestsDocument,
RemoveProfileInterestsVariables,
SafeApolloClient,
} from '@lens-protocol/api-bindings';
import { ProfileId } from '@lens-protocol/domain/entities';
import { IProfileInterestsGateway } from '@lens-protocol/domain/use-cases/profile';

export type ProfileInterestsRequest = {
profileId: ProfileId;
interests: ProfileInterestTypes[];
};

export class ProfileInterestsGateway implements IProfileInterestsGateway {
constructor(private apolloClient: SafeApolloClient) {}

async add(request: ProfileInterestsRequest) {
await this.apolloClient.mutate<AddProfileInterestsData, AddProfileInterestsVariables>({
mutation: AddProfileInterestsDocument,
variables: {
request: {
interests: request.interests,
},
},
});
}

async remove(request: ProfileInterestsRequest) {
await this.apolloClient.mutate<RemoveProfileInterestsData, RemoveProfileInterestsVariables>({
mutation: RemoveProfileInterestsDocument,
variables: {
request: {
interests: request.interests,
},
},
});
}
}
29 changes: 29 additions & 0 deletions packages/react/src/profile/adapters/ProfileInterestsPresenter.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { ProfileInterestTypes } from '@lens-protocol/api-bindings';
import { IProfileInterestsPresenter } from '@lens-protocol/domain/use-cases/profile';

import { IProfileCacheManager } from './IProfileCacheManager';
import { ProfileInterestsRequest } from './ProfileInterestsGateway';

export class ProfileInterestsPresenter implements IProfileInterestsPresenter {
constructor(private readonly profileCacheManager: IProfileCacheManager) {}

async add(request: ProfileInterestsRequest) {
this.profileCacheManager.update(request.profileId, (current) => {
return {
...current,
interests: [...current.interests, ...request.interests],
};
});
}

async remove(request: ProfileInterestsRequest) {
this.profileCacheManager.update(request.profileId, (current) => {
return {
...current,
interests: current.interests.filter(
(interest) => !request.interests.includes(interest as ProfileInterestTypes),
),
};
});
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { ManageProfileInterests } from '@lens-protocol/domain/use-cases/profile';

import { useSharedDependencies } from '../../shared';
import { ProfileInterestsGateway, ProfileInterestsRequest } from './ProfileInterestsGateway';
import { ProfileInterestsPresenter } from './ProfileInterestsPresenter';

export function useProfileInterestsController() {
const { apolloClient, profileCacheManager } = useSharedDependencies();

const add = async (request: ProfileInterestsRequest) => {
const presenter = new ProfileInterestsPresenter(profileCacheManager);
const gateway = new ProfileInterestsGateway(apolloClient);
const manageInterests = new ManageProfileInterests(gateway, presenter);

await manageInterests.add(request);
};

const remove = async (request: ProfileInterestsRequest) => {
const presenter = new ProfileInterestsPresenter(profileCacheManager);
const gateway = new ProfileInterestsGateway(apolloClient);
const manageInterests = new ManageProfileInterests(gateway, presenter);

await manageInterests.remove(request);
};

return {
add,
remove,
};
}
2 changes: 2 additions & 0 deletions packages/react/src/profile/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
/**
* Hooks
*/
export * from './useAddProfileInterests';
export * from './useBlockedProfiles';
export * from './useLazyProfile';
export * from './useLazyProfiles';
Expand All @@ -12,6 +13,7 @@ export * from './useProfileFollowing';
export * from './useProfileManagers';
export * from './useProfiles';
export * from './useRecommendProfileToggle';
export * from './useRemoveProfileInterests';
export * from './useReportProfile';
export * from './useWhoActedOnPublication';

Expand Down
61 changes: 61 additions & 0 deletions packages/react/src/profile/useAddProfileInterests.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import { ProfileInterestTypes } from '@lens-protocol/api-bindings';
import { invariant, success } from '@lens-protocol/shared-kernel';

import { SessionType, useSession } from '../authentication';
import { UseDeferredTask, useDeferredTask } from '../helpers/tasks';
import { useProfileInterestsController } from './adapters/useProfileInterestsController';

export { ProfileInterestTypes };

export type AddProfileInterestsArgs = {
interests: ProfileInterestTypes[];
};

/**
* Add profile interests.
*
* You MUST be authenticated via {@link useLogin} to use this hook.
*
* @example
* ```tsx
* function ProfileInterests({ profile }: { profile: Profile }) {
* const { execute: addInterests } = useAddProfileInterests();
* const { execute: removeInterests } = useRemoveProfileInterests();
*
* const handleClick = async (interest: ProfileInterestTypes) => {
* const request = {
* interests: [interest],
* };
*
* if (profile.interests.includes(interest)) {
* await removeInterests(request);
* } else {
* await addInterests(request);
* }
* };
*
* return <button onClick={() => handleClick(ProfileInterestTypes.Business)}>Business</button>;
* }
* ```
*
* @category Profiles
* @group Hooks
*/
export function useAddProfileInterests(): UseDeferredTask<void, never, AddProfileInterestsArgs> {
const { data: session } = useSession();
const { add } = useProfileInterestsController();

return useDeferredTask(async (request) => {
invariant(
session?.type === SessionType.WithProfile,
'You must be authenticated with a profile to use this hook. Use `useLogin` hook to authenticate.',
);

await add({
profileId: session.profile.id,
interests: request.interests,
});

return success();
});
}
Loading

0 comments on commit e9a5840

Please sign in to comment.