diff --git a/.env b/.env deleted file mode 100644 index 572274e..0000000 --- a/.env +++ /dev/null @@ -1 +0,0 @@ -NEXT_PUBLIC_NETWORK=testnet \ No newline at end of file diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..4d3c330 --- /dev/null +++ b/.env.example @@ -0,0 +1,2 @@ +NEXT_PUBLIC_NETWORK=NETWORK +NEXT_PUBLIC_AUTH_URL=AUTH_ENDPOINT \ No newline at end of file diff --git a/components/avatar-group/avatar/index.tsx b/components/avatar-group/avatar/index.tsx index e7ff4a3..f6d002e 100644 --- a/components/avatar-group/avatar/index.tsx +++ b/components/avatar-group/avatar/index.tsx @@ -35,6 +35,7 @@ const Avatar: FC = ({ return (
= ({ - userName, - userAvatar, + id, + bio, + avatar, + username, }) => { + const { handleClose } = useModal(); + const hasAvatar = avatar === '' ? DEFAULT_IMAGE : avatar; + const hasUsername = username === '' ? 'Unknown' : username; + + const onSelect = () => { + const selectedUser = [ + { + id, + bio, + avatar, + username, + }, + ]; + console.log('Selected user _>', selectedUser); + handleClose(); + return selectedUser; + }; return (
= ({ }} >
- + - {userName} + {hasUsername}
-
+
; + data?: Array; } diff --git a/components/engagement-counter/index.tsx b/components/engagement-counter/index.tsx index 470ef60..3d690a5 100644 --- a/components/engagement-counter/index.tsx +++ b/components/engagement-counter/index.tsx @@ -5,11 +5,13 @@ import { v4 } from 'uuid'; import { useModal } from '@/hooks/use-modal'; import { TimesSVG } from '../svg'; -import { DATA } from './engagement-counter.data'; import { EngagementCounterModalProps } from './engagement-counter.types'; import UserLikeItem from './engagement-counter-item'; -const EngagementCounterModal: FC = ({ title }) => { +const EngagementCounterModal: FC = ({ + title, + data, +}) => { const { handleClose } = useModal(); return ( @@ -37,7 +39,7 @@ const EngagementCounterModal: FC = ({ title }) => { {title} - All(14) + All {data?.length ?? 0}
= ({ title }) => {
- {DATA.map(({ userName, userAvatar }) => ( + {data?.map(({ id, bio, username, avatar }) => ( ))}
diff --git a/components/profile/menu-list.tsx b/components/profile/menu-list.tsx index 7798888..a6a7199 100644 --- a/components/profile/menu-list.tsx +++ b/components/profile/menu-list.tsx @@ -2,6 +2,9 @@ import { useDisconnectWallet } from '@mysten/dapp-kit'; import { Div } from '@stylin.js/elements'; import { not } from 'ramda'; import { FC, useState } from 'react'; +import { useLocalStorage } from 'usehooks-ts'; + +import { MEMEZ_FUN_TOKEN_AUTH } from '@/constants'; import { DocIDSVG, @@ -20,9 +23,19 @@ import RPCCollapseMenuInfo from './rpc-collapse-info'; const MenuList: FC = () => { const [isActiveNSFE, setIsActiveNSFE] = useState(false); const [isRPCMenuOpen, setIsRPCMenuOpen] = useState(false); + const { mutate: disconnectWallet } = useDisconnectWallet(); const [isAccountMenuOpen, setIsAccountMenuOpen] = useState(false); const [isExplorerMenuOpen, setIsExplorerMenuOpen] = useState(false); - const { mutate: disconnectWallet } = useDisconnectWallet(); + + const [, setSignedPM] = useLocalStorage<{ + signature: string; + message: string; + cookies?: unknown; + }>(MEMEZ_FUN_TOKEN_AUTH, { signature: '', message: '' }); + const handleDisconnectWallet = () => { + setSignedPM({ signature: '', message: '' }); + disconnectWallet(); + }; return (
@@ -58,7 +71,7 @@ const MenuList: FC = () => { Icon={LogoutSVG} title="Disconnect" color="#E85965" - onClick={() => disconnectWallet()} + onClick={handleDisconnectWallet} />
); diff --git a/components/profile/profile-info.tsx b/components/profile/profile-info.tsx index 5743347..6c16248 100644 --- a/components/profile/profile-info.tsx +++ b/components/profile/profile-info.tsx @@ -2,24 +2,57 @@ import { useCurrentAccount } from '@mysten/dapp-kit'; import { formatAddress } from '@mysten/sui/utils'; import { Div, Img, Span } from '@stylin.js/elements'; import { useRouter } from 'next/router'; -import { FC } from 'react'; +import { FC, useEffect, useState } from 'react'; import toast from 'react-hot-toast'; +import { useLocalStorage } from 'usehooks-ts'; -import { Routes, RoutesEnum } from '@/constants'; +import { + BASE_URL, + DEFAULT_IMAGE, + MEMEZ_FUN_TOKEN_AUTH, + Routes, + RoutesEnum, +} from '@/constants'; import { useCoinBalance } from '@/hooks/use-coin-balance'; +import { UserDetailsProps } from '@/interface'; import { FixedPointMath } from '@/lib/entities/fixed-point-math'; import { BannerProfileSVG, CopySVG } from '../svg'; const ProfileInfo: FC = () => { - const currentAccount = useCurrentAccount(); const { push } = useRouter(); + const currentAccount = useCurrentAccount(); + const [user, setUser] = useState(); + const [signedPM] = useLocalStorage<{ + signature: string; + message: string; + }>(MEMEZ_FUN_TOKEN_AUTH, { signature: '', message: '' }); + + const userPrifleData = () => { + fetch(`${BASE_URL}/users/${currentAccount?.address}`, { + method: 'GET', + mode: 'cors', + headers: { + message: signedPM.message, + signature: signedPM.signature, + address: currentAccount?.address ?? '', + }, + }) + .then((res) => res.json()) + .then((data) => setUser(data)); + }; + + useEffect(() => { + if (currentAccount) return userPrifleData(); + }, [currentAccount]); const { balance } = useCoinBalance('0x2::sui::SUI', currentAccount?.address); const copyAddress = () => { toast.success('Copied!'); - window.navigator.clipboard.writeText('0x2::sui::SUI'); + window.navigator.clipboard.writeText( + formatAddress(currentAccount?.address || '') + ); }; return ( @@ -52,9 +85,10 @@ const ProfileInfo: FC = () => {
{ flexDirection="column" > - Name + {`${user?.firstName === '' && user?.lastName === '' && 'Unknown'} `} - {formatAddress(currentAccount?.address || '')} + {user?.username}
= ({ status, isReview, description, - name = 'imageUrl', + name = 'avatar', }) => { const { setValue, control } = useFormContext(); - const currentImageUrl = useWatch({ control, name }); + const currentavatar = useWatch({ control, name }); const [dragging, setDragging] = useState(false); const handleChangeFile: ChangeEventHandler = async (e) => { @@ -94,7 +94,7 @@ const UploadImage: FC = ({ onDragLeave={() => setDragging(false)} onDragOver={(e) => e.preventDefault()} borderStyle={dragging ? 'solid' : 'dashed'} - backgroundImage={`url('${currentImageUrl}')`} + backgroundImage={`url('${currentavatar}')`} borderColor={dragging ? '#F6C853' : '#90939D'} > {!isReview && ( diff --git a/components/wallet-button/connect-modal.tsx b/components/wallet-button/connect-modal.tsx index 4511d69..7e7af39 100644 --- a/components/wallet-button/connect-modal.tsx +++ b/components/wallet-button/connect-modal.tsx @@ -1,17 +1,14 @@ import { useConnectWallet, useWallets } from '@mysten/dapp-kit'; import { WalletWithRequiredFeatures } from '@mysten/wallet-standard'; import { Div, H2, Img, Li, P, Ul } from '@stylin.js/elements'; -import { useRouter } from 'next/router'; import { FC } from 'react'; import unikey from 'unikey'; import { LoaderSVG, MemeZLogoSVG } from '@/components/svg'; -import { RoutesEnum } from '@/constants'; import { useDialog } from '@/hooks/use-dialog'; const ConnectModal: FC = () => { const wallets = useWallets(); - const { push } = useRouter(); const { mutateAsync } = useConnectWallet(); const { dialog, handleClose } = useDialog(); @@ -19,11 +16,6 @@ const ConnectModal: FC = () => { await mutateAsync({ wallet }); }; - const handleSignInNavigation = () => { - push(RoutesEnum.SignIn); - handleClose(); - }; - const handleConnect = (wallet: WalletWithRequiredFeatures) => { dialog.promise(connectWallet(wallet), { success: () => ({ @@ -83,60 +75,15 @@ const ConnectModal: FC = () => { width={['100vw', '33.25rem']} borderRadius={['1rem 1rem 0 0', '1rem']} > -

- {wallets ? 'Select Your Wallet' : 'Go to sign in'} -

- {wallets ? ( -
    - {wallets.map((wallet) => ( -
  • handleConnect(wallet)} - > -
    - {wallet.name} -

    {wallet.name}

    -
    -

    Installed

    -
  • - ))} -
- ) : ( -
    +

    Select Your Wallet

    +
      + {wallets.map((wallet) => (
    • { border="1px solid transparent" justifyContent="space-between" nHover={{ borderColor: '#F5B72280' }} - onClick={handleSignInNavigation} + onClick={() => handleConnect(wallet)} > - Sign in +
      + {wallet.name} +

      {wallet.name}

      +
      +

      Installed

    • -
    - )} + ))} +
); }; diff --git a/components/wallet-button/connected-modal/connected-wallet-item.tsx b/components/wallet-button/connected-modal/connected-wallet-item.tsx index b0173bd..234cbdc 100644 --- a/components/wallet-button/connected-modal/connected-wallet-item.tsx +++ b/components/wallet-button/connected-modal/connected-wallet-item.tsx @@ -4,14 +4,15 @@ import { useSwitchAccount, } from '@mysten/dapp-kit'; import { formatAddress } from '@mysten/sui/utils'; -import { Div, Span, Strong } from '@stylin.js/elements'; +import { Div, Img, Span, Strong } from '@stylin.js/elements'; import { AnimatePresence, motion } from 'motion/react'; import Link from 'next/link'; import { FC } from 'react'; import toast from 'react-hot-toast'; -import { ChevronDownSVG, CopySVG, LogoutSVG, UserSVG } from '@/components/svg'; +import { ChevronDownSVG, CopySVG, LogoutSVG } from '@/components/svg'; import { ExplorerMode } from '@/constants'; +import { DEFAULT_IMAGE } from '@/constants/index'; import { useCoinBalance } from '@/hooks/use-coin-balance'; import { useGetExplorerUrl } from '@/hooks/use-get-explorer-url'; import { FixedPointMath } from '@/lib/entities/fixed-point-math'; @@ -26,8 +27,8 @@ const ConnectedWalletItem: FC = ({ account }) => { const getExplorerUrl = useGetExplorerUrl(); const { mutate: switchAccount } = useSwitchAccount(); const { mutate: disconnectWallet } = useDisconnectWallet(); - const { balance } = useCoinBalance('0x2::sui::SUI', account.address); + const imageURL = localStorage.getItem('imageURL'); const copyAddress = () => { toast.success('Copied!'); @@ -71,9 +72,14 @@ const ConnectedWalletItem: FC = ({ account }) => { alignItems="center" borderBottom={isCurrentAccount ? '1px solid #242424' : 'none'} > - - - + { const [show, setShow] = useState(false); const { push } = useRouter(); const currentAccount = useCurrentAccount(); + const imageURL = localStorage.getItem('imageURL'); const menuRef = useClickOutsideListenerRef(() => setShow(false) @@ -56,7 +57,14 @@ const ConnectedModal: FC = () => { push(Routes[RoutesEnum.Profile]); }} > - +
{formatAddress(currentAccount!.address)}
diff --git a/components/wallet-button/index.tsx b/components/wallet-button/index.tsx index 1390f85..f0d1983 100644 --- a/components/wallet-button/index.tsx +++ b/components/wallet-button/index.tsx @@ -1,16 +1,136 @@ -import { useCurrentAccount } from '@mysten/dapp-kit'; -import { Button, Div } from '@stylin.js/elements'; -import { useRouter } from 'next/router'; -import { FC } from 'react'; +import { + useCurrentAccount, + useDisconnectWallet, + useSignPersonalMessage, +} from '@mysten/dapp-kit'; +import { Button, Div, Img } from '@stylin.js/elements'; +import { not } from 'ramda'; +import { FC, useEffect, useState } from 'react'; +import { useLocalStorage } from 'usehooks-ts'; +import { v4 } from 'uuid'; -import { WalletSVG } from '@/components/svg'; -import { Routes, RoutesEnum } from '@/constants'; +import { LoaderSVG, MemeZLogoSVG, WalletSVG } from '@/components/svg'; +import { BASE_URL, MEMEZ_FUN_TOKEN_AUTH } from '@/constants'; +import { useDialog } from '@/hooks/use-dialog'; import ConnectedModal from './connected-modal'; +import { useConnectModal } from './wallet-button.hook'; const WalletButton: FC = () => { + const handleOpenConnectModal = useConnectModal(); const currentAccount = useCurrentAccount(); - const { push } = useRouter(); + const { mutate: disconnectWallet } = useDisconnectWallet(); + const [signing, setSigning] = useState(false); + const { dialog, handleClose } = useDialog(); + const { mutate: signPersonalMessage } = useSignPersonalMessage(); + const [signedPM, setSignedPM] = useLocalStorage<{ + signature: string; + message: string; + }>(MEMEZ_FUN_TOKEN_AUTH, { signature: '', message: '' }); + + const handleConnectWallet = () => { + handleOpenConnectModal(); + }; + + useEffect(() => { + if (signedPM.signature || signedPM.message || !currentAccount) return; + + const newMessage = v4(); + + signPersonalMessage( + { + message: new TextEncoder().encode(newMessage), + }, + { + onSuccess: (result) => { + setSigning(not); + setSignedPM({ + signature: result.signature, + message: newMessage, + }); + }, + onError: () => disconnectWallet(), + } + ); + }, [currentAccount]); + + useEffect(() => { + if (!signedPM.signature || !signedPM.message || !currentAccount || !signing) + return; + const signIn = async () => { + await fetch(`${BASE_URL}/auth/sign-in`, { + method: 'POST', + credentials: 'same-origin', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + message: signedPM.message, + signature: signedPM.signature, + address: currentAccount.address, + }), + }) + .then(async (res) => { + if (!res.ok) { + disconnectWallet(); + throw new Error('signin error'); + } + + console.log(res.headers.getSetCookie(), '>>>res'); + }) + .catch(() => { + disconnectWallet(); + setSignedPM({ + signature: '', + message: '', + }); + }); + setSigning(not); + }; + dialog.promise(signIn(), { + success: () => ({ + timeout: 10000, + title: 'Sign in!', + button: { label: 'Continue browsing', onClick: handleClose }, + message: 'Signin successfully', + Icon: ( +
+ +
+ ), + }), + loading: () => ({ + Icon: , + title: 'Signing...', + message: 'Hang tight! Signing. Please wait', + }), + error: (e) => ({ + title: 'Oops! You could not Connect!', + button: { label: 'Try again', onClick: () => signIn() }, + message: + e.message || + 'Sigin-in failed. Try to refresh the page, double-check your inputs, or reconnect your wallet.', + ghostButton: { + label: 'Do not want to connect my wallet', + onClick: handleClose, + }, + Icon: ( + Error + ), + }), + }); + }, [signing]); if (currentAccount) return ; @@ -36,7 +156,7 @@ const WalletButton: FC = () => { color: '#0a090d', backgroundColor: '#F6C853', }} - onClick={() => push(Routes[RoutesEnum.SignIn])} + onClick={handleConnectWallet} >
diff --git a/constants/index.ts b/constants/index.ts index dadc0b8..94df53f 100644 --- a/constants/index.ts +++ b/constants/index.ts @@ -3,3 +3,22 @@ export * from './math'; export * from './network'; export * from './routes'; export const DROPDOWN_DATA = ['Days', 'Weeks', 'Months']; + +export const MEMEZ_FUN_TOKEN_AUTH = 'MEMEZ_FUN_TOKEN_AUTH'; +export const DEFAULT_IMAGE = + 'https://icons.veryicon.com/png/o/internet--web/prejudice/user-128.png'; + +export const MOCK_FOLLOWING_DATA = [ + { + id: '0xa2322d5addc8d76e8478bf57826e524f269139924037f6b0309a6c9b56cb4958', + username: 'marcopitra', + avatar: '', + bio: '', + }, + { + id: '0x9936f0786da648d7e22ba5d606a73f12fed8f2d1d9307025cdfeaedef45b64b3', + username: '', + avatar: '', + bio: '', + }, +]; diff --git a/constants/network.ts b/constants/network.ts index 550c567..de1f3f3 100644 --- a/constants/network.ts +++ b/constants/network.ts @@ -4,3 +4,4 @@ export enum Network { } export const NETWORK = process.env.NEXT_PUBLIC_NETWORK as Network; +export const BASE_URL = process.env.NEXT_PUBLIC_AUTH_URL as string; diff --git a/constants/routes.ts b/constants/routes.ts index 8f88f23..75a9221 100644 --- a/constants/routes.ts +++ b/constants/routes.ts @@ -1,7 +1,6 @@ export enum RoutesEnum { Home = 'home', CreateCoin = 'create-coin', - SignIn = 'sign-in', CreateProfile = 'create-profile', Profile = 'profile', } @@ -10,6 +9,5 @@ export const Routes: Record = { [RoutesEnum.CreateProfile]: '/create-profile', [RoutesEnum.Profile]: '/profile', [RoutesEnum.CreateCoin]: '/create-coin', - [RoutesEnum.SignIn]: '/sign-in', [RoutesEnum.Home]: '/', }; diff --git a/hooks/use-cookie/index.ts b/hooks/use-cookie/index.ts new file mode 100644 index 0000000..1554617 --- /dev/null +++ b/hooks/use-cookie/index.ts @@ -0,0 +1,36 @@ +import { useState } from 'react'; +const setCookie = (name: string, value: string, days: number) => { + const expires = new Date(); + expires.setTime(expires.getTime() + days * 24 * 60 * 60 * 1000); + document.cookie = `${name}=${value}; expires=${expires.toUTCString()}; path=/`; +}; + +const getCookie = (name: string): string | null => { + const value = `; ${document.cookie}`; + const parts = value.split(`; ${name}=`); + if (parts.length === 2) return parts.pop()?.split(';').shift() || null; + return null; +}; + +const deleteCookie = (name: string) => { + document.cookie = `${name}=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/`; +}; + +export const useCookie = (name: string) => { + const [cookie, setCookieState] = useState(() => + getCookie(name) + ); + + const set = (value: string, days: number = 365) => { + setCookie(name, value, days); + setCookieState(value); + }; + + const remove = () => { + deleteCookie(name); + setCookieState(null); + console.log('removed'); + }; + + return { cookie, set, remove } as const; +}; diff --git a/interface/index.ts b/interface/index.ts index ed6ad76..5e5d812 100644 --- a/interface/index.ts +++ b/interface/index.ts @@ -11,3 +11,25 @@ export interface TimedSuiTransactionBlockResponse extends SuiTransactionBlockResponse { time: number; } + +export interface CreateProfileFormProps { + email?: string; + avatar: string; + username: string; + firstName: string; + lastName: string; + bio: string; +} + +export interface UserProps { + id: string; + bio: string; + avatar: string; + username: string; +} + +export interface UserDetailsProps extends CreateProfileFormProps { + following?: number; + followers?: number; + emailVerified: boolean; +} diff --git a/pages/_document.tsx b/pages/_document.tsx index 9a68613..1bbfe76 100644 --- a/pages/_document.tsx +++ b/pages/_document.tsx @@ -2,7 +2,12 @@ import { Head, Html, Main, NextScript } from 'next/document'; const Document = () => ( - + + +
diff --git a/pages/create-profile.tsx b/pages/create-profile.tsx deleted file mode 100644 index 4dc3762..0000000 --- a/pages/create-profile.tsx +++ /dev/null @@ -1,34 +0,0 @@ -import { yupResolver } from '@hookform/resolvers/yup'; -import { NextPage } from 'next'; -import { FormProvider, useForm } from 'react-hook-form'; - -import { SEO } from '@/components'; -import CreateProfile from '@/views/create-profile'; -import { CreateProfileFormProps } from '@/views/create-profile/create-profile.types'; -import { CreateProfileValidationSchema } from '@/views/create-profile/create-profile-validation'; - -const CreateProfilePage: NextPage = () => { - const form = useForm({ - mode: 'onBlur', - reValidateMode: 'onBlur', - resolver: yupResolver(CreateProfileValidationSchema), - defaultValues: { - email: '', - firstName: '', - lastName: '', - username: '', - password: '', - imageUrl: '', - description: '', - }, - }); - - return ( - - - - - ); -}; - -export default CreateProfilePage; diff --git a/pages/profile/create.tsx b/pages/profile/create.tsx deleted file mode 100644 index 72eff02..0000000 --- a/pages/profile/create.tsx +++ /dev/null @@ -1,26 +0,0 @@ -import { yupResolver } from '@hookform/resolvers/yup'; -import { NextPage } from 'next'; -import { FormProvider, useForm } from 'react-hook-form'; - -import { SEO } from '@/components'; -import CreateProfile from '@/views/create-profile'; -import { CreateProfileFormProps } from '@/views/create-profile/create-profile.types'; -import { CreateProfileValidationSchema } from '@/views/create-profile/create-profile-validation'; - -const CreateProfilePage: NextPage = () => { - const form = useForm({ - mode: 'onBlur', - reValidateMode: 'onBlur', - resolver: yupResolver(CreateProfileValidationSchema), - defaultValues: {}, - }); - - return ( - - - - - ); -}; - -export default CreateProfilePage; diff --git a/pages/sign-in.tsx b/pages/sign-in.tsx deleted file mode 100644 index 92cfe50..0000000 --- a/pages/sign-in.tsx +++ /dev/null @@ -1,29 +0,0 @@ -import { yupResolver } from '@hookform/resolvers/yup'; -import { NextPage } from 'next'; -import { FormProvider, useForm } from 'react-hook-form'; - -import { SEO } from '@/components'; -import SignIn from '@/views/sign-in'; -import { SignInFormProps } from '@/views/sign-in/sign-in.types'; -import { SignInValidationSchema } from '@/views/sign-in/sign-in-validations'; - -const SignInPage: NextPage = () => { - const form = useForm({ - mode: 'onBlur', - reValidateMode: 'onBlur', - resolver: yupResolver(SignInValidationSchema), - defaultValues: { - username: '', - password: '', - }, - }); - - return ( - - - - - ); -}; - -export default SignInPage; diff --git a/views/create-coin/components/dex-card/index.tsx b/views/create-coin/components/dex-card/index.tsx index d15d6d8..1254a57 100644 --- a/views/create-coin/components/dex-card/index.tsx +++ b/views/create-coin/components/dex-card/index.tsx @@ -8,7 +8,7 @@ import { DexCardProps } from '../../create-coin.types'; const DexCard: FC> = ({ dexName, onClick, - imageUrl, + avatar, isReview, isSelected, }) => ( @@ -40,7 +40,7 @@ const DexCard: FC> = ({ justifyContent="center" >
- cetus-dex + cetus-dex
diff --git a/views/create-coin/create-coin-buttons.tsx b/views/create-coin/create-coin-buttons.tsx index b599ca4..f073d12 100644 --- a/views/create-coin/create-coin-buttons.tsx +++ b/views/create-coin/create-coin-buttons.tsx @@ -69,7 +69,7 @@ const CreateCoinButtons: FC = () => { let fieldsToValidate: (keyof CreateCoinForm)[] = []; if (currentStep === CreateCoinStepEnum.CoinDetails) { - fieldsToValidate = ['name', 'description', 'dex', 'imageUrl']; + fieldsToValidate = ['name', 'description', 'dex', 'avatar']; } else if (currentStep === CreateCoinStepEnum.DexSocialMedia) { fieldsToValidate = [ 'quoteCoin', diff --git a/views/create-coin/create-coin.data.tsx b/views/create-coin/create-coin.data.tsx index fde01fb..41b67b3 100644 --- a/views/create-coin/create-coin.data.tsx +++ b/views/create-coin/create-coin.data.tsx @@ -39,25 +39,25 @@ export const CreateCoinDexData: ReadonlyArray = [ { dexId: unikey(), dexName: 'Cetus', - imageUrl: + avatar: 'https://assets.coingecko.com/markets/images/1134/large/cetus.png?1706865152', }, { dexId: unikey(), dexName: 'Raidium', - imageUrl: + avatar: 'https://assets.coingecko.com/markets/images/649/large/raydium.jpeg?1706864594', }, { dexId: unikey(), dexName: 'Shadow Exchange', - imageUrl: + avatar: 'https://assets.coingecko.com/markets/images/11810/large/shadow.jpg?1735902352', }, { dexId: unikey(), dexName: 'Hyperliquid', - imageUrl: + avatar: 'https://assets.coingecko.com/markets/images/1571/large/PFP.png?1714470912', }, ]; diff --git a/views/create-coin/create-coin.types.ts b/views/create-coin/create-coin.types.ts index cf7e1e0..e4d4204 100644 --- a/views/create-coin/create-coin.types.ts +++ b/views/create-coin/create-coin.types.ts @@ -47,7 +47,7 @@ export enum CreateCoinStepEnum { export interface DexCardProps { dexId: string; dexName: string; - imageUrl: string; + avatar: string; isReview?: boolean; isSelected?: boolean; onClick?: () => void; @@ -64,7 +64,7 @@ export interface CreateCoinForm { step?: number; success?: boolean; name: string; - imageUrl: string; + avatar: string; description: string; quoteCoin: string; supply: string; diff --git a/views/create-coin/steps/create-coin-details-step.tsx b/views/create-coin/steps/create-coin-details-step.tsx index d968675..466f0cb 100644 --- a/views/create-coin/steps/create-coin-details-step.tsx +++ b/views/create-coin/steps/create-coin-details-step.tsx @@ -32,9 +32,9 @@ const CreateCoinDetailsStep: FC = () => { Basic Details

{ { setValue('dex', dex.dexId); }} diff --git a/views/create-coin/steps/create-coin-review-step.tsx b/views/create-coin/steps/create-coin-review-step.tsx index b484441..c69c518 100644 --- a/views/create-coin/steps/create-coin-review-step.tsx +++ b/views/create-coin/steps/create-coin-review-step.tsx @@ -33,7 +33,7 @@ const CreateCoinReviewStep: FC = () => {

Avatar

- +

Details @@ -71,7 +71,7 @@ const CreateCoinReviewStep: FC = () => { ) diff --git a/views/create-coin/steps/create-coin.validations.ts b/views/create-coin/steps/create-coin.validations.ts index b02c36a..f10c4fe 100644 --- a/views/create-coin/steps/create-coin.validations.ts +++ b/views/create-coin/steps/create-coin.validations.ts @@ -3,7 +3,7 @@ import * as yup from 'yup'; export const createCoinValidationSchema = yup.object({ dex: yup.string().required('Dex is required'), name: yup.string().required('Name is required'), - imageUrl: yup.string().required('Avatar is required'), + avatar: yup.string().required('Avatar is required'), description: yup.string().required('Description is required'), quoteCoin: yup.string().required('Quote coin is required'), supply: yup diff --git a/views/create-profile/create-profile-button.tsx b/views/create-profile/create-profile-button.tsx deleted file mode 100644 index 742e08f..0000000 --- a/views/create-profile/create-profile-button.tsx +++ /dev/null @@ -1,124 +0,0 @@ -import { Button, Div, Span } from '@stylin.js/elements'; -import { useRouter } from 'next/router'; -import { FC } from 'react'; -import { useFormContext } from 'react-hook-form'; - -import DialogCountdown from '@/components/dialog/dialog-countdown'; -import { LoaderSVG } from '@/components/svg'; -import { Routes, RoutesEnum } from '@/constants'; -import { useDialog } from '@/hooks/use-dialog'; - -import { CreateProfileFormProps } from './create-profile.types'; - -const CreateProfileButton: FC = () => { - const { trigger } = useFormContext(); - - const { dialog, handleClose } = useDialog(); - const { push } = useRouter(); - const handleCreateProfile = async () => { - return new Promise((resolve, reject) => { - setTimeout(() => { - const isSuccess = Math.random() > 0.5; - if (isSuccess) { - handleClose(); - resolve('success'); - return; - } - reject('Error'); - }, 1000); - }); - }; - - const handleAuth = async () => { - const isValid = await trigger(); - - if (!isValid) return; - - await dialog.promise(handleCreateProfile(), { - success: () => ({ - title: 'Account created', - message: 'Account successfully created', - button: ( - - ), - ghostButton: ( - - ), - }), - loading: () => ({ - Icon: , - title: 'Creating...', - message: 'Creating an account...', - }), - error: (e) => ({ - title: 'Oops! You can not create an account!', - button: { label: 'Try again', onClick: handleClose }, - message: ( - e.message || 'Make sure you fill every field correctly and try again.' - ).replace('Invariant failed: ', ''), - ghostButton: { - label: 'Do not want to try again!', - onClick: handleClose, - }, - }), - }); - }; - - return ( -

- -
- ); -}; - -export default CreateProfileButton; diff --git a/views/create-profile/create-profile-validation.ts b/views/create-profile/create-profile-validation.ts deleted file mode 100644 index 0654f75..0000000 --- a/views/create-profile/create-profile-validation.ts +++ /dev/null @@ -1,17 +0,0 @@ -import * as yup from 'yup'; - -export const CreateProfileValidationSchema = yup.object({ - firstName: yup.string().required('First name is required'), - lastName: yup.string().required('Last name is required'), - username: yup.string().required('Username is required'), - email: yup - .string() - .email('Invalid email format') - .required('Email is required'), - imageUrl: yup.string().required('Avatar is required'), - password: yup.string().required('Password is required'), - description: yup - .string() - .min(80, 'Must be at least 80 characters') - .required('Description is required'), -}); diff --git a/views/create-profile/create-profile.types.ts b/views/create-profile/create-profile.types.ts deleted file mode 100644 index dad9624..0000000 --- a/views/create-profile/create-profile.types.ts +++ /dev/null @@ -1,9 +0,0 @@ -export interface CreateProfileFormProps { - email: string; - imageUrl: string; - username: string; - firstName: string; - lastName: string; - password: string; - description: string; -} diff --git a/views/create-profile/index.tsx b/views/create-profile/index.tsx deleted file mode 100644 index 9837c0d..0000000 --- a/views/create-profile/index.tsx +++ /dev/null @@ -1,112 +0,0 @@ -import { Div, H1, P } from '@stylin.js/elements'; -import { FC } from 'react'; -import { useFormContext } from 'react-hook-form'; - -import InputField from '@/components/input-field'; -import Layout from '@/components/layout'; -import UploadImage from '@/components/upload-image'; - -import { CreateProfileFormProps } from './create-profile.types'; -import CreateProfileButton from './create-profile-button'; - -const CreateProfile: FC = () => { - const { - register, - formState: { errors }, - } = useFormContext(); - - return ( - -
-
-

- Create profile -

-
-
-
-
-

- Basic Details -

- - - - - - - -
-
- -
-
-
- ); -}; - -export default CreateProfile; diff --git a/views/home/index.tsx b/views/home/index.tsx index 4c2683d..43e91bc 100644 --- a/views/home/index.tsx +++ b/views/home/index.tsx @@ -16,7 +16,7 @@ import { CARDS } from './card.data'; const Home: FC = () => { const { push } = useRouter(); - const handleCreateCoinButtonClick = () => push(Routes[RoutesEnum.CreateCoin]); + const handleCreateCoinButtonClick = () => push(Routes[RoutesEnum.Home]); return ( diff --git a/views/profile/edit-profile-modal/edit-profile-modal-button.tsx b/views/profile/edit-profile-modal/edit-profile-modal-button.tsx index 845c95a..519437b 100644 --- a/views/profile/edit-profile-modal/edit-profile-modal-button.tsx +++ b/views/profile/edit-profile-modal/edit-profile-modal-button.tsx @@ -5,35 +5,37 @@ import { useFormContext } from 'react-hook-form'; import DialogCountdown from '@/components/dialog/dialog-countdown'; import { LoaderSVG } from '@/components/svg'; -import { Routes, RoutesEnum } from '@/constants'; +import { BASE_URL, Routes, RoutesEnum } from '@/constants'; import { useDialog } from '@/hooks/use-dialog'; -import { CreateProfileFormProps } from '@/views/create-profile/create-profile.types'; + +import { IEditProfileForm } from './edit-profile-modal.types'; const EditProfileModalButton: FC = () => { - const { trigger } = useFormContext(); + const { trigger, getValues } = useFormContext(); + const { username, name, bio, avatar } = getValues(); const { dialog, handleClose } = useDialog(); const { push } = useRouter(); - const handleCreateProfile = () => { - return new Promise((resolve, reject) => { - setTimeout(() => { - const isSuccess = Math.random() > 0.5; - if (isSuccess) { - handleClose(); - resolve('success'); - return; - } - reject('Error'); - }, 1000); - }); + + const saveEditProfile = async () => { + fetch(`${BASE_URL}/users`, { + method: 'PATCH', + mode: 'cors', + body: JSON.stringify({ + name: name, + avatar: avatar, + bio: bio, + username: username, + }), + }).then((res) => res.ok); }; - const handleEditProfiel = async () => { + const handleEditProfile = async () => { const isValid = await trigger(); if (!isValid) return; - await dialog.promise(handleCreateProfile(), { + await dialog.promise(saveEditProfile(), { success: () => ({ title: 'Profile Edited', message: 'Profile successfully edited', @@ -110,7 +112,7 @@ const EditProfileModalButton: FC = () => { transition="all .3s" justifyContent="center" border="1px solid #F6C853" - onClick={async () => await handleEditProfiel()} + onClick={async () => handleEditProfile()} nHover={{ transform: 'scale(1.05)', }} diff --git a/views/profile/edit-profile-modal/edit-profile-modal.types.ts b/views/profile/edit-profile-modal/edit-profile-modal.types.ts index d7b1d1c..2dde190 100644 --- a/views/profile/edit-profile-modal/edit-profile-modal.types.ts +++ b/views/profile/edit-profile-modal/edit-profile-modal.types.ts @@ -1,5 +1,6 @@ export interface IEditProfileForm { - imageUrl: string; + name: string; + avatar: string; username: string; - description: string; + bio: string; } diff --git a/views/profile/edit-profile-modal/edit-profile.validations.ts b/views/profile/edit-profile-modal/edit-profile.validations.ts index a3f3f51..bff094f 100644 --- a/views/profile/edit-profile-modal/edit-profile.validations.ts +++ b/views/profile/edit-profile-modal/edit-profile.validations.ts @@ -1,7 +1,8 @@ import * as yup from 'yup'; export const editProfileValidationSchema = yup.object().shape({ + name: yup.string().required('Name is required'), username: yup.string().required('Username is required'), - imageUrl: yup.string().required('Image is required'), - description: yup.string().required('Description is required'), + avatar: yup.string().required('Image is required'), + bio: yup.string().required('Description is required'), }); diff --git a/views/profile/edit-profile-modal/index.tsx b/views/profile/edit-profile-modal/index.tsx index 478c2fb..85c6c19 100644 --- a/views/profile/edit-profile-modal/index.tsx +++ b/views/profile/edit-profile-modal/index.tsx @@ -1,9 +1,14 @@ import { yupResolver } from '@hookform/resolvers/yup'; +import { useCurrentAccount } from '@mysten/dapp-kit'; import { Div, H1, P } from '@stylin.js/elements'; +import { useEffect, useState } from 'react'; import { FormProvider, useForm } from 'react-hook-form'; +import { useLocalStorage } from 'usehooks-ts'; import InputField from '@/components/input-field'; import UploadImage from '@/components/upload-image'; +import { BASE_URL, MEMEZ_FUN_TOKEN_AUTH } from '@/constants'; +import { UserDetailsProps } from '@/interface'; import { editProfileValidationSchema } from './edit-profile.validations'; import { IEditProfileForm } from './edit-profile-modal.types'; @@ -17,12 +22,48 @@ const EditProfileModal = () => { }); const { register, + setValue, formState: { errors }, } = form; + const currentAccount = useCurrentAccount(); + const [user, setUser] = useState(); + const [signedPM] = useLocalStorage<{ + signature: string; + message: string; + }>(MEMEZ_FUN_TOKEN_AUTH, { signature: '', message: '' }); + localStorage.setItem('imageURL', user?.avatar ?? ''); + const userPrifleData = () => { + fetch(`${BASE_URL}/users/${currentAccount?.address}`, { + method: 'GET', + mode: 'cors', + headers: { + message: signedPM?.message, + signature: signedPM?.signature, + address: currentAccount?.address ?? '', + }, + }) + .then((res) => res.json()) + .then((data) => setUser(data)); + }; + + const updateFields = () => { + setValue('avatar', user?.avatar ?? ''); + setValue('name', `${user?.firstName + ' ' + user?.lastName}`); + setValue('username', user?.username ?? ''); + setValue('bio', user?.bio ?? ''); + }; + + useEffect(() => { + if (currentAccount) return userPrifleData(); + }, [currentAccount]); + + useEffect(() => { + updateFields(); + }, [user]); return ( -
+
{ Basic Details

+ {
diff --git a/views/profile/index.tsx b/views/profile/index.tsx index 0add8f9..bc882ee 100644 --- a/views/profile/index.tsx +++ b/views/profile/index.tsx @@ -1,8 +1,12 @@ +import { useCurrentAccount } from '@mysten/dapp-kit'; import { Div, Span } from '@stylin.js/elements'; -import { FC, useState } from 'react'; +import { FC, useEffect, useState } from 'react'; +import { useLocalStorage } from 'usehooks-ts'; import { Layout } from '@/components'; +import { BASE_URL, MEMEZ_FUN_TOKEN_AUTH } from '@/constants'; import { useIsMobile } from '@/hooks/use-is-mobile'; +import { UserDetailsProps } from '@/interface'; import ActivityList from './activity-list'; import HeaderButtons from './header-buttons'; @@ -13,9 +17,33 @@ import { ProfileTabsEnum } from './profile-tabs/profile-tabs.types'; import UserInfo from './user-info'; const Profile: FC = () => { + const isMyProfile = true; const { isMobile } = useIsMobile(); + const currentAccount = useCurrentAccount(); const [tabSelect, setTabSelect] = useState(ProfileTabsEnum.History); - const isMyProfile = true; + const [user, setUser] = useState(); + const [signedPM] = useLocalStorage<{ + signature: string; + message: string; + }>(MEMEZ_FUN_TOKEN_AUTH, { signature: '', message: '' }); + + const userProfileData = () => { + fetch(`${BASE_URL}/users/${currentAccount?.address}`, { + method: 'GET', + mode: 'cors', + headers: { + message: signedPM?.message, + signature: signedPM?.signature, + address: currentAccount?.address ?? '', + }, + }) + .then((res) => res.json()) + .then((data) => setUser(data)); + }; + + useEffect(() => { + if (currentAccount) return userProfileData(); + }, [currentAccount]); const onSelect = (tab: ProfileTabsEnum) => { setTabSelect(tab); @@ -41,11 +69,18 @@ const Profile: FC = () => { borderBottomLeftRadius={['0', '0', '0', '2rem']} borderBottomRightRadius={['0', '0', '0', '2rem']} > - + diff --git a/views/profile/metric/index.tsx b/views/profile/metric/index.tsx index 4afa11b..3498c5e 100644 --- a/views/profile/metric/index.tsx +++ b/views/profile/metric/index.tsx @@ -1,7 +1,14 @@ +import { useCurrentAccount } from '@mysten/dapp-kit'; import { Div, Span } from '@stylin.js/elements'; -import { FC } from 'react'; +import { FC, useEffect, useState } from 'react'; +import { useLocalStorage } from 'usehooks-ts'; import EngagementCounterModal from '@/components/engagement-counter'; +import { + BASE_URL, + MEMEZ_FUN_TOKEN_AUTH, + MOCK_FOLLOWING_DATA, +} from '@/constants'; import { useModal } from '@/hooks/use-modal'; import { MetricProps } from './metric.types'; @@ -13,9 +20,62 @@ const Metric: FC = ({ totalValueCoin, }) => { const { setContent, onClose } = useModal(); + const currentAccount = useCurrentAccount(); + const [followingData, setFollowingData] = useState(); + const [followersData, setFollowersData] = useState(); + const [signedPM] = useLocalStorage<{ + signature: string; + message: string; + cookies?: unknown; + }>(MEMEZ_FUN_TOKEN_AUTH, { signature: '', message: '' }); + + const getFollowing = () => { + fetch(`${BASE_URL}/users/${currentAccount?.address}/following`, { + method: 'GET', + mode: 'cors', + headers: { + message: signedPM?.message, + signature: signedPM?.signature, + address: currentAccount?.address ?? '', + }, + }) + .then((res) => res.json()) + .then((userFollowing) => setFollowingData(userFollowing.data)); + }; + + const getFollowers = () => { + fetch(`${BASE_URL}/users/${currentAccount?.address}/followers`, { + method: 'GET', + mode: 'cors', + headers: { + message: signedPM?.message, + signature: signedPM?.signature, + address: currentAccount?.address ?? '', + }, + }) + .then((res) => res.json()) + .then((userFollowers) => setFollowersData(userFollowers.data)); + }; + + useEffect(() => { + if (currentAccount) { + getFollowing(); + getFollowers(); + } + }, [currentAccount, followersData, followingData]); + + const handleFollowers = () => + setContent( + , + { onClose } + ); + + const handleFollowing = () => + setContent( + , + { onClose } + ); - const handleClick = () => - setContent(, { onClose }); return (
= ({ cursor="pointer" transition="0.3s" alignItems="center" - onClick={handleClick} + onClick={handleFollowers} flexDirection="column" nHover={{ opacity: '0.8', @@ -50,6 +110,7 @@ const Metric: FC = ({
{ +const UserInfo: FC = ({ + avatar, + firstName, + lastName, + username, + emailVerified, +}) => { + const currentAccount = useCurrentAccount(); const clipBoardSuccessMessage = 'Address copied to the clipboard'; return ( @@ -31,29 +41,38 @@ const UserInfo: FC = () => { height="140px" objectFit="cover" borderRadius="100%" - src="/user-default-memez-fun.png" + src={avatar} />
- - Name + + {`${firstName === '' && lastName === '' && 'Unknown'} `} -
+
- Username + {username} -
- -
+ {emailVerified && ( +
+ +
+ )}
@@ -62,16 +81,21 @@ const UserInfo: FC = () => { px="0.5rem" py="0.25rem" display="flex" + width="8rem" color="#E4E7EB" gap="0.625rem" + cursor="pointer" fontSize="0.75rem" alignItems="center" - borderRadius="0.75rem" textAlign="center" + borderRadius="0.75rem" + justifyContent="center" transition="all 300ms ease-in-out" nHover={{ transform: 'scale(1.05)', color: '#F5B722' }} > - 0x2::sui::SUI + + {formatAddress(currentAccount?.address || '')} +
; + isUserProfile: boolean; +} diff --git a/views/sign-in/index.tsx b/views/sign-in/index.tsx deleted file mode 100644 index 80fc16f..0000000 --- a/views/sign-in/index.tsx +++ /dev/null @@ -1,74 +0,0 @@ -import { useCurrentAccount } from '@mysten/dapp-kit'; -import { Div, H1, P, Span } from '@stylin.js/elements'; -import { useRouter } from 'next/router'; -import { FC } from 'react'; - -import Layout from '@/components/layout'; -import { Routes, RoutesEnum } from '@/constants'; - -import SignInButtons from './sign-in-button'; -import SignInForm from './sign-in-form'; - -const SignIn: FC = () => { - const currentAccount = useCurrentAccount(); - const { push } = useRouter(); - - if (currentAccount) push('/'); - - return ( - -
-
-

- Sign in -

-

- Welcome back. Select method to log in -

-
-
- - -
-

- Don‘t have an account?{' '} - { - push(Routes[RoutesEnum.CreateProfile]); - }} - > - Create a Profile - -

-
-
-
-
- ); -}; - -export default SignIn; diff --git a/views/sign-in/sign-in-button.tsx b/views/sign-in/sign-in-button.tsx deleted file mode 100644 index 1a0219c..0000000 --- a/views/sign-in/sign-in-button.tsx +++ /dev/null @@ -1,122 +0,0 @@ -import { Button, Div } from '@stylin.js/elements'; -import { FC } from 'react'; -import { useFormContext } from 'react-hook-form'; - -import DialogCountdown from '@/components/dialog/dialog-countdown'; -import { LoaderSVG } from '@/components/svg'; -import { useConnectModal } from '@/components/wallet-button/wallet-button.hook'; -import { useDialog } from '@/hooks/use-dialog'; - -import { SignInFormProps } from './sign-in.types'; - -const SignInButton: FC = () => { - const { setValue, trigger } = useFormContext(); - const { dialog, handleClose } = useDialog(); - const handleOpenConnectModal = useConnectModal(); - - const handleCreateProfile = () => { - return new Promise((resolve, reject) => { - setTimeout(() => { - const isSuccess = Math.random() > 0.5; - if (isSuccess) { - setValue('success', true); - resolve('success'); - return; - } - reject('Error'); - }, 1000); - }); - }; - - const handleSignIn = async () => { - let fieldsToValidate: (keyof SignInFormProps)[] = []; - - fieldsToValidate = ['username', 'password']; - const isValid = await trigger(fieldsToValidate); - - if (isValid) { - await dialog.promise(handleCreateProfile(), { - success: () => ({ - title: 'Sign-in successful', - button: ( - - ), - ghostButton: ( - - ), - message: 'Now connect your wallet do see all the function', - }), - loading: () => ({ - Icon: , - title: 'Signing...', - message: 'Please wait...', - }), - error: (e) => ({ - title: 'Oops! You can not sign in!', - button: { label: 'Try again', onClick: handleClose }, - message: ( - e.message || - 'Make sure you fill every field correctly and try again.' - ).replace('Invariant failed: ', ''), - ghostButton: { - label: 'Do not want to try again!', - onClick: handleClose, - }, - }), - }); - } - if (!isValid) return; - }; - - return ( -
- -
- ); -}; - -export default SignInButton; diff --git a/views/sign-in/sign-in-form.tsx b/views/sign-in/sign-in-form.tsx deleted file mode 100644 index 46933f0..0000000 --- a/views/sign-in/sign-in-form.tsx +++ /dev/null @@ -1,88 +0,0 @@ -import { Button, Div, Hr, P, Span } from '@stylin.js/elements'; -import { FC } from 'react'; -import { useFormContext } from 'react-hook-form'; - -import InputField from '@/components/input-field'; -import { XSVG } from '@/components/svg'; - -import { SignInFormProps } from './sign-in.types'; - -const SignInForm: FC = () => { - const { - register, - formState: { errors }, - } = useFormContext(); - return ( -
-
- -
-
-

Or

-
-
-
- - -
-
-
- ); -}; - -export default SignInForm; diff --git a/views/sign-in/sign-in-validations.ts b/views/sign-in/sign-in-validations.ts deleted file mode 100644 index 1084c28..0000000 --- a/views/sign-in/sign-in-validations.ts +++ /dev/null @@ -1,11 +0,0 @@ -import * as yup from 'yup'; - -export const SignInValidationSchema = yup.object({ - username: yup.string().required('Username is required'), - password: yup - .string() - .required('Password is required') - .min(8, 'Must be at least 8 characters') - .matches(/[A-Z]/, 'Must contain an uppercase letter') - .matches(/[0-9]/, 'Must contain a number'), -}); diff --git a/views/sign-in/sign-in.types.ts b/views/sign-in/sign-in.types.ts deleted file mode 100644 index 644a574..0000000 --- a/views/sign-in/sign-in.types.ts +++ /dev/null @@ -1,27 +0,0 @@ -export interface CreateProfileProps { - avatar: string; - userName: string; - description: string; -} - -export interface SignInProps { - username: string; - password: string; -} - -export enum SignInStepEnum { - CreateProfileProps, - SignInProps, -} - -export interface SignInHeaderProps { - title: string; - description?: string; - step: SignInStepEnum; -} - -export interface SignInFormProps { - username: string; - password: string; - success?: boolean; -}