From 562789ec78b21ef776b06df5176feb4783541bdf Mon Sep 17 00:00:00 2001 From: jinhojang6 Date: Wed, 23 Oct 2024 00:29:37 +0900 Subject: [PATCH] feat: implement operator apis --- apis/operators/useGetUserInfo.ts | 43 +++++++++++ apis/operators/useStakeOperator.ts | 33 +++++++-- apis/operators/useUnstakeOperator.ts | 33 +++++++-- atoms/filter.ts | 0 atoms/userInfo.ts | 3 + atoms/wallet.ts | 3 + common/api.ts | 2 +- .../Dashboard/OperatorGrid/OperatorCard.tsx | 36 +++++----- .../Dashboard/OperatorGrid/OperatorGrid.tsx | 72 +++++++++++++++---- src/components/Dropdown/Dropdown.tsx | 8 +-- .../Explore/OperatorGrid/OperatorGrid.tsx | 2 +- src/components/Header/Navbar/Navbar.tsx | 13 +++- .../WalletConnect/WalletConnect.tsx | 9 ++- .../Dashboard/DashboardContainer.tsx | 60 +++++----------- src/containers/Explore/ExploreContainer.tsx | 26 +++---- src/states/filterState/filter.state.ts | 2 +- types/operators.ts | 35 +++++++++ utils/operators.ts | 32 +++++++-- 18 files changed, 297 insertions(+), 115 deletions(-) create mode 100644 apis/operators/useGetUserInfo.ts create mode 100644 atoms/filter.ts create mode 100644 atoms/userInfo.ts create mode 100644 atoms/wallet.ts diff --git a/apis/operators/useGetUserInfo.ts b/apis/operators/useGetUserInfo.ts new file mode 100644 index 0000000000..04e7337c1f --- /dev/null +++ b/apis/operators/useGetUserInfo.ts @@ -0,0 +1,43 @@ +import { useQuery, useQueryClient } from '@tanstack/react-query' +import { api } from '../../common/api' + +const useQueryOptions = { + refetchOnWindowFocus: false, + staleTime: 60 * 1000, + retry: 1, +} + +interface Props { + walletAddress: string | null + setUserInfo?: (userInfo: any) => void +} + +const useGetUserInfo = ({ walletAddress, setUserInfo }: Props) => { + const queryKey = ['getUserInfo'] + const queryClient = useQueryClient() + + const fetchData = async () => { + const token = sessionStorage.getItem('accessToken') + api.defaults.headers.common['Authorization'] = `Bearer ${token}` + + return await api.get('/user').then((res) => { + setUserInfo && setUserInfo(res.data) + return res.data + }) + } + + const updateCache = (newData: any) => { + queryClient.setQueryData(queryKey, newData) + } + + const response = useQuery({ + queryKey: queryKey, + queryFn: fetchData, + enabled: !!walletAddress, + ...useQueryOptions, + }) + + return { ...response, updateCache } +} + +export default useGetUserInfo diff --git a/apis/operators/useStakeOperator.ts b/apis/operators/useStakeOperator.ts index 198d816cd7..db8a14787e 100644 --- a/apis/operators/useStakeOperator.ts +++ b/apis/operators/useStakeOperator.ts @@ -4,6 +4,7 @@ import { api } from '../../common/api' interface StakeRequest { operator_id: string + setIsStaked: (isStaked: boolean) => void } interface StakeResponse { @@ -13,11 +14,35 @@ interface StakeResponse { const postStake = async ({ operator_id, + setIsStaked, }: StakeRequest): Promise => { - const response: AxiosResponse = await api.post( - `/operators/${operator_id}/stake`, - ) - return response.data + try { + const token = sessionStorage.getItem('accessToken') + api.defaults.headers.common['Authorization'] = `Bearer ${token}` + + const response: AxiosResponse = await api.post( + `/user/operators/${operator_id}/stake`, + ) + + setIsStaked(true) + return response.data + } catch (error: any) { + if (error.response) { + if (error.response.status === 403) { + alert( + 'Access Denied: You do not have permission to perform this action.', + ) + throw new Error( + 'Access Denied: You do not have permission to perform this action.', + ) + } + if (error.response.status === 404) { + alert('Operator not found. Please check the operator ID.') + throw new Error('Operator not found. Please check the operator ID.') + } + } + throw new Error('An unexpected error occurred. Please try again later.') + } } export const useStakeOperator = () => { diff --git a/apis/operators/useUnstakeOperator.ts b/apis/operators/useUnstakeOperator.ts index fa627993d0..f69436ecbb 100644 --- a/apis/operators/useUnstakeOperator.ts +++ b/apis/operators/useUnstakeOperator.ts @@ -4,6 +4,7 @@ import { api } from '../../common/api' interface UnstakeRequest { operator_id: string + setIsStaked: (isStaked: boolean) => void } interface UnstakeResponse { @@ -13,11 +14,35 @@ interface UnstakeResponse { const postUnstake = async ({ operator_id, + setIsStaked, }: UnstakeRequest): Promise => { - const response: AxiosResponse = await api.post( - `/operators/${operator_id}/unstake`, - ) - return response.data + try { + const token = sessionStorage.getItem('accessToken') + api.defaults.headers.common['Authorization'] = `Bearer ${token}` + + const response: AxiosResponse = await api.post( + `/user/operators/${operator_id}/unstake`, + ) + + setIsStaked(false) + return response.data + } catch (error: any) { + if (error.response) { + if (error.response.status === 403) { + alert( + 'Access Denied: You do not have permission to perform this action.', + ) + throw new Error( + 'Access Denied: You do not have permission to perform this action.', + ) + } + if (error.response.status === 404) { + alert('Operator not found. Please check the operator ID.') + throw new Error('Operator not found. Please check the operator ID.') + } + } + throw new Error('An unexpected error occurred. Please try again later.') + } } export const useUnstakeOperator = () => { diff --git a/atoms/filter.ts b/atoms/filter.ts new file mode 100644 index 0000000000..e69de29bb2 diff --git a/atoms/userInfo.ts b/atoms/userInfo.ts new file mode 100644 index 0000000000..18c7a26edb --- /dev/null +++ b/atoms/userInfo.ts @@ -0,0 +1,3 @@ +import { atom } from 'jotai' + +export const userInfo = atom(null) diff --git a/atoms/wallet.ts b/atoms/wallet.ts new file mode 100644 index 0000000000..32189f6d40 --- /dev/null +++ b/atoms/wallet.ts @@ -0,0 +1,3 @@ +import { atom } from 'jotai' + +export const walletAddressAtom = atom(null) diff --git a/common/api.ts b/common/api.ts index ba59caefbe..16257b58e1 100644 --- a/common/api.ts +++ b/common/api.ts @@ -25,7 +25,7 @@ api.interceptors.response.use( if (error.response?.status === 401) { try { - const refreshToken = await localStorage.getItem('refreshToken') + const refreshToken = await sessionStorage.getItem('refreshToken') await api .post('/token/refresh', { refreshToken, diff --git a/src/components/Dashboard/OperatorGrid/OperatorCard.tsx b/src/components/Dashboard/OperatorGrid/OperatorCard.tsx index b810a8f954..05b302555d 100644 --- a/src/components/Dashboard/OperatorGrid/OperatorCard.tsx +++ b/src/components/Dashboard/OperatorGrid/OperatorCard.tsx @@ -1,9 +1,9 @@ -import { ProcessedOperator } from '@/containers/Dashboard/DashboardContainer' import styled from '@emotion/styled' import Link from 'next/link' import React, { useState } from 'react' import { useStakeOperator } from '../../../../apis/operators/useStakeOperator' import { useUnstakeOperator } from '../../../../apis/operators/useUnstakeOperator' +import { ProcessedOperator } from '../../../../types/operators' interface OperatorCardProps { operator: ProcessedOperator @@ -21,15 +21,13 @@ const OperatorCard: React.FC = ({ if (isStaked) { unstake.mutate({ operator_id: operatorId, + setIsStaked, }) - - setIsStaked(false) } else { stake.mutate({ operator_id: operatorId, + setIsStaked, }) - - setIsStaked(true) } } @@ -41,14 +39,14 @@ const OperatorCard: React.FC = ({ {operator.name} - - {operator.pointsPerHour} rp + + {operator.pointsPerHour} XP handleStake(operator.id)} - isStaked={isStaked} + isStaked={!!isStaked} > {isStaked ? 'Unstake' : 'Stake'} @@ -64,6 +62,17 @@ const OperatorCard: React.FC = ({ ) } +const Container = styled.div` + display: flex; + flex-direction: column; + + color: rgb(var(--lsd-text-primary)); + + a { + display: flex; + } +` + const IconButton = styled.button` background-color: transparent; border-left: none; @@ -92,17 +101,6 @@ const Value = styled.div` line-height: 20px; ` -const Container = styled.div` - display: flex; - flex-direction: column; - - color: rgb(var(--lsd-text-primary)); - - a { - display: flex; - } -` - const OperatorImage = styled.img` width: 100%; aspect-ratio: 1; diff --git a/src/components/Dashboard/OperatorGrid/OperatorGrid.tsx b/src/components/Dashboard/OperatorGrid/OperatorGrid.tsx index 321465976c..927b50e350 100644 --- a/src/components/Dashboard/OperatorGrid/OperatorGrid.tsx +++ b/src/components/Dashboard/OperatorGrid/OperatorGrid.tsx @@ -1,7 +1,8 @@ import { breakpoints } from '@/configs/ui.configs' -import { ProcessedOperator } from '@/containers/Dashboard/DashboardContainer' import styled from '@emotion/styled' +import Link from 'next/link' import React from 'react' +import { ProcessedOperator } from '../../../../types/operators' import OperatorCard from './OperatorCard' interface OperatorGridProps { @@ -13,6 +14,13 @@ const OperatorGrid: React.FC = ({ isLoading, data, }: OperatorGridProps) => { + const stakedOperators = data?.filter((operator) => operator.isStaked) + + const totalXpPerBlock = data?.reduce( + (acc, operator) => acc + (operator.pointsPerHour ?? 0), + 0, + ) + return (
@@ -31,31 +39,45 @@ const OperatorGrid: React.FC = ({ - 7 + {data?.length || 0} - 6 + {stakedOperators?.length || 0} - 1 + {data?.length - (stakedOperators?.length || 0)} - 912 + {totalXpPerBlock || 0} - {isLoading - ? Array.from({ length: 12 }).map((_, index) => ( - - - - )) - : data?.map((operator) => ( - - ))} + {isLoading ? ( + Array.from({ length: 12 }).map((_, index) => ( + + + + )) + ) : data?.length === 0 ? ( + + + Add Operator + + Add Operator + + ) : ( + data?.map((operator) => ( + + )) + )} ) @@ -162,4 +184,26 @@ const Placeholder = styled.div` opacity: 0.5; ` +const AddOperator = styled(Link)` + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + gap: 24px; + width: 100%; + height: 258px; + padding: 24px; + + span { + text-decoration: none; + color: white; + } + + &:hover { + span { + text-decoration: underline; + } + } +` + export default OperatorGrid diff --git a/src/components/Dropdown/Dropdown.tsx b/src/components/Dropdown/Dropdown.tsx index 0fb4495315..6a6d6c3ad6 100644 --- a/src/components/Dropdown/Dropdown.tsx +++ b/src/components/Dropdown/Dropdown.tsx @@ -1,6 +1,6 @@ -import { defaultFilterState } from '@/states/filterState' +import { defaultFilterState, filterAtom } from '@/states/filterState' import styled from '@emotion/styled' -import { atom, useAtom } from 'jotai' +import { useSetAtom } from 'jotai' import React, { useEffect, useRef, useState } from 'react' import Checkbox from './Checkbox' @@ -15,8 +15,6 @@ interface DropdownProps { prefill?: string[] } -const filterAtom = atom(defaultFilterState) - const Dropdown: React.FC = ({ title, options, @@ -30,7 +28,7 @@ const Dropdown: React.FC = ({ const dropdownRef = useRef(null) - const [filter, setFilter] = useAtom(filterAtom) + const setFilter = useSetAtom(filterAtom) const defaultState = defaultFilterState diff --git a/src/components/Explore/OperatorGrid/OperatorGrid.tsx b/src/components/Explore/OperatorGrid/OperatorGrid.tsx index 8fe461a3fb..63dc05be58 100644 --- a/src/components/Explore/OperatorGrid/OperatorGrid.tsx +++ b/src/components/Explore/OperatorGrid/OperatorGrid.tsx @@ -1,9 +1,9 @@ import { breakpoints } from '@/configs/ui.configs' -import { ProcessedOperator } from '@/containers/Dashboard/DashboardContainer' import styled from '@emotion/styled' import 'lazysizes' import Link from 'next/link' import React, { useCallback, useEffect, useRef, useState } from 'react' +import { ProcessedOperator } from '../../../../types/operators' interface OperatorGridProps { isLoading: boolean diff --git a/src/components/Header/Navbar/Navbar.tsx b/src/components/Header/Navbar/Navbar.tsx index d63afeaaf7..bbebc6eef1 100644 --- a/src/components/Header/Navbar/Navbar.tsx +++ b/src/components/Header/Navbar/Navbar.tsx @@ -6,7 +6,14 @@ import React from 'react' interface NavbarProps {} -export const navItems = [ +interface NavItem { + label: string + href: string + isDisabled?: boolean + isSoon?: boolean +} + +export const navItems: NavItem[] = [ { label: 'Countdown', href: '/countdown', @@ -18,8 +25,8 @@ export const navItems = [ { label: 'Dashboard', href: '/dashboard', - isDisabled: true, - isSoon: true, + // isDisabled: true, + // isSoon: true, }, // { // label: 'Leaderboard', diff --git a/src/components/WalletConnect/WalletConnect.tsx b/src/components/WalletConnect/WalletConnect.tsx index a6b9ece0a0..7918176d41 100644 --- a/src/components/WalletConnect/WalletConnect.tsx +++ b/src/components/WalletConnect/WalletConnect.tsx @@ -1,6 +1,8 @@ import { truncateString } from '@/utils/general.utils' import styled from '@emotion/styled' +import { useAtom } from 'jotai' import React, { useEffect, useRef, useState } from 'react' +import { walletAddressAtom } from '../../../atoms/wallet' import { api } from '../../../common/api' import { getMEAddressAndSignature } from './magicEden' import { getOKXAddressAndSignature } from './okx' @@ -17,7 +19,7 @@ const options = [ const Dropdown: React.FC = () => { const [isExpanded, setIsExpanded] = useState(false) - const [walletAddress, setWalletAddress] = useState(null) + const [walletAddress, setWalletAddress] = useAtom(walletAddressAtom) const walletHandlers = { okx: getOKXAddressAndSignature, @@ -40,7 +42,10 @@ const Dropdown: React.FC = () => { setWalletAddress(address) const response = await api.post('/token/pair', { address, signature }) - console.log('Token pair response:', response) + const { access, refresh } = response.data.token + + sessionStorage.setItem('accessToken', access) + sessionStorage.setItem('refreshToken', refresh) } } catch (error) { console.error('Failed to connect or disconnect wallet:', error) diff --git a/src/containers/Dashboard/DashboardContainer.tsx b/src/containers/Dashboard/DashboardContainer.tsx index bcde704259..e916f86e49 100644 --- a/src/containers/Dashboard/DashboardContainer.tsx +++ b/src/containers/Dashboard/DashboardContainer.tsx @@ -3,59 +3,34 @@ import { OperatorPanel } from '@/components/Dashboard/OperatorPanel' import { ProgressBar } from '@/components/Dashboard/ProgressBar' import { breakpoints } from '@/configs/ui.configs' import styled from '@emotion/styled' +import { useAtomValue, useSetAtom } from 'jotai' import React from 'react' -import useGetOperators from '../../../apis/operators/useGetOperators' -import { getRandomSubset, processOperators } from '../../../utils/operators' +import useGetUserInfo from '../../../apis/operators/useGetUserInfo' +import { userInfo } from '../../../atoms/userInfo' +import { walletAddressAtom } from '../../../atoms/wallet' +import { processMyOperators } from '../../../utils/operators' export type DashboardPageProps = React.DetailedHTMLProps< React.HTMLAttributes, HTMLDivElement > -export interface Operator { - id: number - name: string - archetype: string - image_400_url: string - image_400_jpeg_url: string - comp: string - background: string - skin: string - helmet: string - jacket: string -} - -export interface Group { - id: number - name: string - operators: Operator[] -} - -export interface ProcessedOperator { - id: string - image: string - name: string - archetype: string - gif: string - comp: string - background: string - skin: string - helmet: string - jacket: string - pointsPerHour: number - isStaked: boolean - isPinned: boolean -} - const DashboardContainer: React.FC = ({ children, ...props }) => { - const { data, isLoading } = useGetOperators() + const setUserInfo = useSetAtom(userInfo) - const processedOperators = processOperators(data as Group[], []) + const walletAddress = useAtomValue(walletAddressAtom) - const random20Operators = getRandomSubset(processedOperators, 20) + const { data: userInfoData, isLoading: isUserInfoLoading } = useGetUserInfo({ + walletAddress, + setUserInfo, + }) + + // console.log('userInfoData', userInfoData) + + const processedOperators = processMyOperators(userInfoData?.operators) return ( @@ -70,7 +45,10 @@ const DashboardContainer: React.FC = ({ - + diff --git a/src/containers/Explore/ExploreContainer.tsx b/src/containers/Explore/ExploreContainer.tsx index 8f03c1a378..33f453106b 100644 --- a/src/containers/Explore/ExploreContainer.tsx +++ b/src/containers/Explore/ExploreContainer.tsx @@ -1,10 +1,11 @@ import { Dropdown } from '@/components/Dropdown' import { OperatorGrid } from '@/components/Explore/OperatorGrid' -import { defaultFilterState, FilterState } from '@/states/filterState' +import { defaultFilterState, filterAtom } from '@/states/filterState' import styled from '@emotion/styled' -import { atom, useAtom } from 'jotai' +import { useAtom } from 'jotai' import React, { useCallback, useMemo } from 'react' import useGetOperators from '../../../apis/operators/useGetOperators' + import { ARCHETYPE, BACKGROUND, @@ -17,19 +18,18 @@ import { processOperators, shuffleOperators } from '../../../utils/operators' interface ExploreSectionProps {} -const filterAtom = atom(defaultFilterState) - -const ExploreSection: React.FC = () => { +const ExploreContainer: React.FC = () => { const { data, isLoading } = useGetOperators() const [filter, setFilter] = useAtom(filterAtom) const processedOperators = processOperators( data as any, - filter.archetype.slice(), + filter?.archetype?.slice(), ) const selectedOperators = useMemo(() => { + if (!processedOperators || !filter) return [] const filterCopied = JSON.parse(JSON.stringify(filter)) return processedOperators @@ -65,42 +65,42 @@ const ExploreSection: React.FC = () => { options={ARCHETYPE} onSelectionChange={handleFilterChange} filterType="archetype" - prefill={filter.archetype.slice()} + prefill={filter?.archetype.slice()} /> Reset All close-black @@ -181,4 +181,4 @@ const ResetAll = styled.button` } ` -export default ExploreSection +export default ExploreContainer diff --git a/src/states/filterState/filter.state.ts b/src/states/filterState/filter.state.ts index 0678f550eb..ca0252d9e0 100644 --- a/src/states/filterState/filter.state.ts +++ b/src/states/filterState/filter.state.ts @@ -27,7 +27,7 @@ export const defaultFilterState: FilterState = { background: BACKGROUND, } -const filterAtom = atom(defaultFilterState) +export const filterAtom = atom(defaultFilterState) const wrapFilterState = ( filter: FilterState, diff --git a/types/operators.ts b/types/operators.ts index b21e2700f9..81eb94287e 100644 --- a/types/operators.ts +++ b/types/operators.ts @@ -9,3 +9,38 @@ export type Archetype = | 'Outlaw' | 'Philosopher' | 'Polymath' + +export interface Operator { + id: number + name: string + archetype: string + image_400_url: string + image_400_jpeg_url: string + comp: string + background: string + skin: string + helmet: string + jacket: string +} + +export interface Group { + id: number + name: string + operators: Operator[] +} + +export interface ProcessedOperator { + id: string + image: string + name: string + archetype: string + gif: string + comp: string + background: string + skin: string + helmet: string + jacket: string + pointsPerHour?: number + isStaked?: boolean + isPinned?: boolean +} diff --git a/utils/operators.ts b/utils/operators.ts index 0705f99139..4ded3e197c 100644 --- a/utils/operators.ts +++ b/utils/operators.ts @@ -1,14 +1,10 @@ -import { - Group, - ProcessedOperator, -} from '@/containers/Dashboard/DashboardContainer' -import { Archetype } from '../types/operators' +import { Archetype, Group, ProcessedOperator } from '../types/operators' export function processOperators( data: Group[], selectedArchetypes: Archetype[], ): ProcessedOperator[] { - const hasSelectedArchetypes = selectedArchetypes.length > 0 + const hasSelectedArchetypes = selectedArchetypes?.length > 0 return data?.flatMap((group) => { const groupArchetype = group.name.slice(0, -1) as Archetype @@ -23,7 +19,6 @@ export function processOperators( image: operator.image_400_jpeg_url, gif: operator.image_400_url, name: operator.name, - pointsPerHour: Math.floor(Math.random() * 500), comp: operator.comp, background: operator.background, skin: operator.skin, @@ -39,6 +34,29 @@ export function processOperators( }) } +export function processMyOperators(operators: any[]) { + if (!operators) { + return [] + } + + return operators?.map((operator) => ({ + id: operator.id.toString(), + arcgetypeId: operator.archetype_id, + image: operator.image_400_jpeg_url, + gif: operator.image_400_url, + name: operator.name, + pointsPerHour: operator.staking_xp_per_block, + comp: operator.comp, + background: operator.background, + skin: operator.skin, + helmet: operator.helmet, + jacket: operator.jacket, + archetype: operator.archetype, + isStaked: operator.is_currently_staked, + isPinned: operator.is_user_pinned, + })) +} + export function getRandomSubset(array: T[], count: number): T[] { const shuffled = array?.sort(() => 0.5 - Math.random()) return shuffled?.slice(0, count)