diff --git a/.babelrc b/.babelrc new file mode 100644 index 00000000..14ef0437 --- /dev/null +++ b/.babelrc @@ -0,0 +1,13 @@ +{ + "presets": ["next/babel"], + "plugins": [ + [ + "styled-components", + { + "ssr": true, + "displayName": true, + "preprocess": false + } + ] + ] +} diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index 85dd4898..ddf1b500 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -2,20 +2,3 @@ Please include a summary of the change and which issue is fixed. Please also include relevant motivation and context. List any dependencies that are required for this change. -# Type of change - -- [ ] Bug fix -- [ ] Design fix -- [ ] New feature -- [ ] Existing feature update -- [ ] Enhancement - -# Checklist: - -- [ ] My code follows the style guidelines of this project -- [ ] I have performed a self-review of my own code -- [ ] I have commented my code, particularly in hard-to-understand areas -- [ ] I have made corresponding changes to the documentation -- [ ] My changes generate no new warnings (`npm run eslint`) -- [ ] I have added tests that prove my fix is effective or that my feature works -- [ ] New and existing unit tests pass locally with my changes (`npm run test`) diff --git a/actions/category.ts b/actions/category.ts new file mode 100644 index 00000000..8bbf3fea --- /dev/null +++ b/actions/category.ts @@ -0,0 +1,11 @@ +import { CategoryType } from "interfaces"; +import { NODE_API_URL } from "utils/constant"; + +export const getCategories = async (codes?: string[]) => { + const filter: any = {} + if (codes) filter.codes = codes + const res = await fetch(`${NODE_API_URL}/api/categories/?filter=${JSON.stringify(filter)}`); + if (!res.ok) throw new Error(); + let result = await res.json() as CategoryType[]; + return result; +}; \ No newline at end of file diff --git a/actions/follower.ts b/actions/follower.ts index 8a880bfe..dae92acb 100644 --- a/actions/follower.ts +++ b/actions/follower.ts @@ -3,22 +3,50 @@ import { NODE_API_URL } from "utils/constant"; import { DEFAULT_LIMIT_PAGINATION } from "../utils/constant"; import Cookies from 'js-cookie'; -export const getFollowers = async (walletId: string, page: string="1", limit: string=DEFAULT_LIMIT_PAGINATION, searchText?: string, certifiedOnly?: string) => { - const query = `${searchText ? `&nameOrAddressSearch=${searchText}`: ""}${certifiedOnly ? `&certifiedOnly=${certifiedOnly}` : ""}` - const res = await fetch(`${NODE_API_URL}/api/follow/followers/${walletId}?page=${page}&limit=${limit}${query}`); +export const getFollowers = async (walletId: string, page: string="1", limit: string=DEFAULT_LIMIT_PAGINATION, searchText?: string, verified?: boolean) => { + const pagination = {page, limit} + const filter: any = {} + if (searchText) filter.searchText = searchText + if (verified !== undefined) filter.verified = verified + const res = await fetch(`${NODE_API_URL}/api/follow/followers/${walletId}?pagination=${JSON.stringify(pagination)}&filter=${JSON.stringify(filter)}`); if (!res.ok) throw new Error(); let result = await res.json() as CustomResponse; return result; }; -export const getFollowed = async (walletId: string, page: string="1", limit: string=DEFAULT_LIMIT_PAGINATION, searchText?: string, certifiedOnly?: string) => { - const query = `${searchText ? `&nameOrAddressSearch=${searchText}`: ""}${certifiedOnly ? `&certifiedOnly=${certifiedOnly}` : ""}` - const res = await fetch(`${NODE_API_URL}/api/follow/followed/${walletId}?page=${page}&limit=${limit}${query}`); +export const getFollowed = async (walletId: string, page: string="1", limit: string=DEFAULT_LIMIT_PAGINATION, searchText?: string, verified?: boolean) => { + const pagination = {page, limit} + const filter: any = {} + if (searchText) filter.searchText = searchText + if (verified !== undefined) filter.verified = verified + const res = await fetch(`${NODE_API_URL}/api/follow/followed/${walletId}?pagination=${JSON.stringify(pagination)}&filter=${JSON.stringify(filter)}`); if (!res.ok) throw new Error(); let result = await res.json() as CustomResponse; return result; }; +export const getFollowersCount = async (walletId: string) => { + const res = await fetch(`${NODE_API_URL}/api/follow/countFollowers/${walletId}`); + if (!res.ok) throw new Error(); + let result = await res.json() as number; + return result; +}; + +export const getFollowedCount = async (walletId: string) => { + const res = await fetch(`${NODE_API_URL}/api/follow/countFollowed/${walletId}`); + if (!res.ok) throw new Error(); + let result = await res.json() as number; + return result; +}; + +export const isUserFollowing = async (walletIdFollowed: string, walletIdFollower: string) => { + const queryString = `?walletIdFollowed=${walletIdFollowed}&walletIdFollower=${walletIdFollower}` + const res = await fetch(`${NODE_API_URL}/api/follow/isUserFollowing/${queryString}`); + if (!res.ok) throw new Error(); + let data: {isFollowing: boolean} = await res.json(); + return data; +}; + export const follow = async (walletIdFollowed: string, walletIdFollower: string) => { const cookie = Cookies.get("token") const queryString = `?walletIdFollowed=${walletIdFollowed}&walletIdFollower=${walletIdFollower}` @@ -49,26 +77,4 @@ export const unfollow = async (walletIdFollowed: string, walletIdFollower: strin }else{ throw new Error("Unvalid authentication"); } -}; - -export const isUserFollowing = async (walletIdFollowed: string, walletIdFollower: string) => { - const queryString = `?walletIdFollowed=${walletIdFollowed}&walletIdFollower=${walletIdFollower}` - const res = await fetch(`${NODE_API_URL}/api/follow/isUserFollowing/${queryString}`); - if (!res.ok) throw new Error(); - let data: {isFollowing: boolean} = await res.json(); - return data; -}; - -export const getFollowersCount = async (walletId: string) => { - const res = await fetch(`${NODE_API_URL}/api/follow/countFollowers/${walletId}`); - if (!res.ok) throw new Error(); - let result = await res.json() as number; - return result; -}; - -export const getFollowedCount = async (walletId: string) => { - const res = await fetch(`${NODE_API_URL}/api/follow/countFollowed/${walletId}`); - if (!res.ok) throw new Error(); - let result = await res.json() as number; - return result; }; \ No newline at end of file diff --git a/actions/nft.ts b/actions/nft.ts index 1c7cd70d..041d81ed 100644 --- a/actions/nft.ts +++ b/actions/nft.ts @@ -1,12 +1,18 @@ import { CustomResponse, NftType } from 'interfaces/index'; import { MARKETPLACE_ID, NODE_API_URL } from 'utils/constant'; -import { envStringToCondition } from '../utils/strings' +import { encryptCookie } from 'utils/cookie'; import { DEFAULT_LIMIT_PAGINATION } from "../utils/constant"; -export const filterNFTs = (data: NftType[]) => data.filter((item) => item.creatorData && item.ownerData && item.media && envStringToCondition(Number(item.id))) +export const filterNFTs = (data: NftType[]) => data.filter((item) => item.properties?.preview.ipfs) -export const getOwnedNFTS = async (id: string, onlyFromMpId: boolean, listed? :number, page: string="1", limit: string=DEFAULT_LIMIT_PAGINATION, noSeriesData: boolean = false) => { - const res = await fetch(`${NODE_API_URL}/api/NFTs/owner/${id}?page=${page}&limit=${limit}${listed!==undefined ? `&listed=${listed}` : ""}${onlyFromMpId ? `&marketplaceId=${MARKETPLACE_ID}` : ""}&noSeriesData=${noSeriesData}`); +export const getOwnedNFTS = async (id: string, onlyFromMpId: boolean, listed? :boolean, page: string="1", limit: string=DEFAULT_LIMIT_PAGINATION, noSeriesData: boolean = false, nftIdsFilter: string[] = []) => { + const paginationOptions = {page, limit} + const filterOptions: any = {owner: id, noSeriesData} + if (listed !== undefined) filterOptions.listed = listed + if (onlyFromMpId) filterOptions.marketplaceId = MARKETPLACE_ID + if (nftIdsFilter.length > 0) filterOptions.ids = nftIdsFilter + const sortOptions = "created_at:desc" + const res = await fetch(`${NODE_API_URL}/api/NFTs/?pagination=${JSON.stringify(paginationOptions)}&filter=${JSON.stringify(filterOptions)}&sort=${sortOptions}`); if (!res.ok) throw new Error('error fetching owned NFTs'); let result: CustomResponse = await res.json(); result.data = filterNFTs(result.data) @@ -14,8 +20,10 @@ export const getOwnedNFTS = async (id: string, onlyFromMpId: boolean, listed? :n }; export const getCreatorNFTS = async (id: string, page: string="1", limit: string=DEFAULT_LIMIT_PAGINATION, noSeriesData: boolean = false) => { + const paginationOptions = {page, limit} + const filterOptions: any = {creator: id, noSeriesData} const res = await fetch( - `${NODE_API_URL}/api/NFTs/creator/${id}?page=${page}&limit=${limit}&noSeriesData=${noSeriesData}` + `${NODE_API_URL}/api/NFTs/?pagination=${JSON.stringify(paginationOptions)}&filter=${JSON.stringify(filterOptions)}` ); if (!res.ok) throw new Error('error fetching created NFTs'); let result: CustomResponse = await res.json(); @@ -23,10 +31,13 @@ export const getCreatorNFTS = async (id: string, page: string="1", limit: string return result; }; -export const getCategoryNFTs = async (codes?: string | string[], page: string="1", limit: string=DEFAULT_LIMIT_PAGINATION, noSeriesData: boolean = false) => { - const queryString = !codes ? "" : (typeof codes==='string' ? `&codes=${codes}` : `&codes=${codes.join("&codes=")}`) +export const getNFTs = async (codes?: string[], page: string="1", limit: string=DEFAULT_LIMIT_PAGINATION, noSeriesData: boolean = false, listed? :Boolean) => { + const paginationOptions = {page, limit} + const filterOptions: any = {noSeriesData, marketplaceId: MARKETPLACE_ID} + if (codes) filterOptions.categories = codes + if (listed !== undefined) filterOptions.listed = listed const res = await fetch( - `${NODE_API_URL}/api/NFTs/category/?marketplaceId=${MARKETPLACE_ID}&listed=1&page=${page}&limit=${limit}&noSeriesData=${noSeriesData}${queryString}` + `${NODE_API_URL}/api/NFTs/?pagination=${JSON.stringify(paginationOptions)}&filter=${JSON.stringify(filterOptions)}` ); if (!res.ok) throw new Error('error fetching NFTs by categories'); let result: CustomResponse = await res.json(); @@ -35,13 +46,39 @@ export const getCategoryNFTs = async (codes?: string | string[], page: string="1 }; export const getNFT = async (id: string, incViews: boolean = false, viewerWalletId: string | null = null, ip?: string, noSeriesData: boolean = false, marketplaceId?: string) => { - const res = await fetch(`${NODE_API_URL}/api/NFTs/${id}?incViews=${incViews}&viewerWalletId=${viewerWalletId}&viewerIp=${ip}&noSeriesData=${noSeriesData}${marketplaceId ? `&marketplaceId=${marketplaceId}` : ""}`); + const filterOptions: any = {noSeriesData, marketplaceId} + const res = await fetch(`${NODE_API_URL}/api/NFTs/${id}?filter=${JSON.stringify(filterOptions)}&incViews=${incViews}&viewerWalletId=${viewerWalletId}&viewerIp=${ip}`); if (!res.ok) throw new Error('error fetching NFT'); let data: NftType = await res.json(); - if (!data.creatorData || !data.ownerData || !data.media) throw new Error(); + if (!data.properties?.preview.ipfs) throw new Error(); return data; }; +export const getLikedNFTs = async (walletId: string, page: string="1", limit: string=DEFAULT_LIMIT_PAGINATION, noSeriesData: boolean = false) => { + const paginationOptions = {page, limit} + const filterOptions: any = {liked: walletId, noSeriesData} + const res = await fetch( + `${NODE_API_URL}/api/NFTs/?pagination=${JSON.stringify(paginationOptions)}&filter=${JSON.stringify(filterOptions)}` + ); + if (!res.ok) throw new Error(); + let result: CustomResponse = await res.json(); + result.data = filterNFTs(result.data) + return result; +} + +export const getByTheSameArtistNFTs = async (walletId: string, page: string="1", limit: string=DEFAULT_LIMIT_PAGINATION, noSeriesData: boolean = false) => { + const paginationOptions = {page, limit} + const filterOptions: any = {creator: walletId, noSeriesData} + const sortOptions: string = "created_at:desc" + const res = await fetch( + `${NODE_API_URL}/api/NFTs/?pagination=${JSON.stringify(paginationOptions)}&filter=${JSON.stringify(filterOptions)}&sort=${sortOptions}` + ); + if (!res.ok) throw new Error(); + let result: CustomResponse = await res.json(); + result.data = filterNFTs(result.data) + return result; +} + export const getUserNFTsStat = async (id: string, onlyFromMpId: boolean): Promise<{ countOwned: number, countOwnedListed: number, @@ -50,8 +87,37 @@ export const getUserNFTsStat = async (id: string, onlyFromMpId: boolean): Promis countFollowers: number, countFollowed: number }> => { - const res = await fetch(`${NODE_API_URL}/api/NFTs/stat/${id}${onlyFromMpId ? `?marketplaceId=${MARKETPLACE_ID}` : ""}`); + const filterOptions: any = {} + if (onlyFromMpId) filterOptions.marketplaceId = MARKETPLACE_ID + const res = await fetch(`${NODE_API_URL}/api/NFTs/stat/${id}?filter=${JSON.stringify(filterOptions)}`); if (!res.ok) throw new Error('error fetching user NFTs stat'); let result = await res.json() return result; -}; \ No newline at end of file +}; + +export const getHistory = async (nftId: string, seriesId: string, grouped: boolean=false) => { + const sortOptions: string = "timestamp:desc" + const filterOptions: any = {grouped: grouped} + const res = await fetch(`${NODE_API_URL}/api/nfts/history/${seriesId}/${nftId}?sort=${sortOptions}&filter=${JSON.stringify(filterOptions)}`); + if (!res.ok) throw new Error('error fetching NFT history'); + let result = await res.json() + return result; +} + +export const canAddToSeries = async (seriesId: string, walletId: string) => { + const res = await fetch(`${NODE_API_URL}/api/nfts/series/can-add?seriesId=${seriesId}&walletId=${walletId}`) + if (!res.ok) throw new Error('error getting information about this series for this user'); + let result = await res.json() + return result as boolean +} + +export const addNFTsToCategories = async (creator: string, chainIds: string[], categories: string[]) => { + const nftsAuthToken = encryptCookie(`${creator},${chainIds.join('-')},${categories.join('-')}`) + const res = await fetch(`${NODE_API_URL}/api/nfts/add-nfts-categories`, { + method: 'POST', + body:JSON.stringify({creator, chainIds, categories, nftsAuthToken}), + }); + if (!res.ok) throw new Error(); + let success: boolean = await res.json(); + return success +} \ No newline at end of file diff --git a/actions/user.ts b/actions/user.ts index dc381a10..4771947c 100644 --- a/actions/user.ts +++ b/actions/user.ts @@ -1,11 +1,10 @@ -import { CustomResponse, NftType, UserType } from 'interfaces/index'; -import { filterNFTs } from "./nft"; +import { CustomResponse, UserType } from 'interfaces/index'; import { DEFAULT_LIMIT_PAGINATION, NODE_API_URL } from "../utils/constant"; import Cookies from 'js-cookie'; -export const getUser = async (token: string) => { +export const getUser = async (token: string, ignoreCache=false) => { const res = await fetch( - `${NODE_API_URL}/api/users/${token}` + `${NODE_API_URL}/api/users/${token}?ignoreCache=${ignoreCache}` ); if (!res.ok) throw new Error(); @@ -43,17 +42,13 @@ export const getAccountBalance = async (id: string) => { return data.capsAmout; }; -export const getUsers = async () => { - const res = await fetch(`${NODE_API_URL}/api/users`); - if (!res.ok) throw new Error(); - const response: CustomResponse = await res.json(); - return response; -}; - -export const getUsersByWalletIds = async (walletIds: string[]) => { - if (walletIds.length === 0) return {totalCount:0, data:[]} - const query = `?walletIds=${walletIds.join("&walletIds=")}` - const res = await fetch(`${NODE_API_URL}/api/users/getUsers${query}`); +export const getUsers = async (walletIds?: string[], artist?: boolean, verified?: boolean, page: string = "1", limit: string = DEFAULT_LIMIT_PAGINATION) => { + const paginationOptions = {page, limit} + const filter:any = {} + if (walletIds) filter.walletIds = walletIds + if (artist !== undefined) filter.artist = artist + if (verified !== undefined) filter.verified = verified + const res = await fetch(`${NODE_API_URL}/api/users/?filter=${JSON.stringify(filter)}&pagination=${JSON.stringify(paginationOptions)}`); if (!res.ok) throw new Error(); const response: CustomResponse = await res.json(); return response; @@ -103,11 +98,3 @@ export const unlikeNFT = async (walletId: string, nftId: string, serieId: string throw new Error("Unvalid authentication"); } } - -export const getLikedNFTs = async (walletId: string, page: string="1", limit: string=DEFAULT_LIMIT_PAGINATION, noSeriesData: boolean = false) => { - const res = await fetch(`${NODE_API_URL}/api/users/${walletId}/liked?page=${page}&limit=${limit}&noSeriesData=${noSeriesData}`) - if (!res.ok) throw new Error(); - let result: CustomResponse = await res.json(); - result.data = filterNFTs(result.data) - return result; -} \ No newline at end of file diff --git a/components/assets/Error404.tsx b/components/assets/Error404.tsx new file mode 100644 index 00000000..4ef149de --- /dev/null +++ b/components/assets/Error404.tsx @@ -0,0 +1,73 @@ +import React from 'react'; + +interface Error404Props { + className?: string; +} + +const Error404 = ({ className }: Error404Props) => ( + + + + + + + + + + + + + + +); + +export default Error404; diff --git a/components/assets/Error500.tsx b/components/assets/Error500.tsx new file mode 100644 index 00000000..725934d1 --- /dev/null +++ b/components/assets/Error500.tsx @@ -0,0 +1,73 @@ +import React from 'react'; + +interface Error500Props { + className?: string; +} + +const Error500 = ({ className }: Error500Props) => ( + + + + + + + + + + + + + + +); + +export default Error500; \ No newline at end of file diff --git a/components/assets/FingerMark.tsx b/components/assets/FingerMark.tsx new file mode 100644 index 00000000..5c446e69 --- /dev/null +++ b/components/assets/FingerMark.tsx @@ -0,0 +1,21 @@ +import React from 'react'; + +interface FingerMarkProps { + className?: string; +} + +const FingerMark = ({ className }: FingerMarkProps) => ( + + + +); + +export default FingerMark; diff --git a/components/assets/Languages/France.tsx b/components/assets/Languages/France.tsx index 2d6cae52..a76ff970 100644 --- a/components/assets/Languages/France.tsx +++ b/components/assets/Languages/France.tsx @@ -1,11 +1,11 @@ import React from 'react'; interface FranceProps { - className: string; - onClick: React.MouseEventHandler; + className?: string; + onClick?: React.MouseEventHandler; } -const France: React.FC = ({ className, onClick }) => ( +const France = ({ className, onClick }: FranceProps) => ( ; + className?: string; + onClick?: React.MouseEventHandler; } -const Japan: React.FC = ({ className, onClick }) => ( +const Japan = ({ className, onClick }: JapanProps) => ( ; + className?: string; + onClick?: React.MouseEventHandler; } -const UK: React.FC = ({ className, onClick }) => ( +const UK = ({ className, onClick }: UKProps) => ( ; + className?: string; + onClick?: React.MouseEventHandler; } -const LogoTernoa: React.FC = ({ className, onClick }) => { +const LogoTernoa = ({ className, onClick }: LogoProps) => { return process.env.NEXT_PUBLIC_APP_LOGO_PATH ? ; + className?: string; + onClick?: React.MouseEventHandler; } -const LogoTernoa: React.FC = ({ className, onClick }) => { +const LogoTernoa = ({ className, onClick }: LogoProps) => { return process.env.NEXT_PUBLIC_APP_LOGO_PATH ? = ({ className }) => ( +const NoNFTImage = ({ className }: NoNFTImageProps) => ( ( + + + + +); + +export default SecretCards; diff --git a/components/assets/SocialMedias/Discord.tsx b/components/assets/SocialMedias/Discord.tsx index ce0eabb9..bea91a11 100644 --- a/components/assets/SocialMedias/Discord.tsx +++ b/components/assets/SocialMedias/Discord.tsx @@ -1,11 +1,11 @@ import React from 'react'; interface DiscordProps { - className: string; - onClick: React.MouseEventHandler; + className?: string; + onClick?: React.MouseEventHandler; } -const Discord: React.FC = ({ className, onClick }) => ( +const Discord = ({ className, onClick }: DiscordProps) => ( ; + className?: string; + onClick?: React.MouseEventHandler; } -const Github: React.FC = ({ className, onClick }) => ( +const Github = ({ className, onClick }: GithubProps) => ( ; + className?: string; + onClick?: React.MouseEventHandler; } -const Instagram: React.FC = ({ className, onClick }) => ( +const Instagram = ({ className, onClick }: InstagramProps) => ( ; + className?: string; + onClick?: React.MouseEventHandler; } -const LinkedIn: React.FC = ({ className, onClick }) => ( +const LinkedIn = ({ className, onClick }: LinkedInProps) => ( ; + className?: string; + onClick?: React.MouseEventHandler; } -const Telegram: React.FC = ({ className, onClick }) => ( +const Telegram = ({ className, onClick }: TelegramProps) => ( ; + className?: string; + onClick?: React.MouseEventHandler; } -const Twitch: React.FC = ({ className, onClick }) => ( +const Twitch = ({ className, onClick }: TwitchProps) => ( ; + className?: string; + onClick?: React.MouseEventHandler; } -const Twitter: React.FC = ({ className, onClick }) => ( +const Twitter = ({ className, onClick }: TwitterProps) => ( ; + className?: string; + onClick?: React.MouseEventHandler; } -const Youtube: React.FC = ({ className, onClick }) => ( +const Youtube = ({ className, onClick }: YoutubeProps) => ( = ({ className }) => ( +const SoundOff = ({ className }: SoundOffProps) => ( = ({ className }) => ( +const SoundOn = ({ className }: SoundOnProps) => ( ( + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +); + +export default SuccessImage; diff --git a/components/assets/Watermark.tsx b/components/assets/Watermark.tsx index 947b5c7f..d2bdb001 100644 --- a/components/assets/Watermark.tsx +++ b/components/assets/Watermark.tsx @@ -1,10 +1,10 @@ import React from 'react'; interface WaterMarkProps { - className: string; + className?: string; } -const WaterMark: React.FC = ({ className }) => ( +const WaterMark = ({ className }: WaterMarkProps) => ( = ({ className }) => ( +const WhiteWaterMark = ({ className }: WhiteWaterMarkProps) => ( = ({ className }) => ( +const ArrowBottom = ({ className }: ArrowBottomProps) => ( = ({ className }) => ( +const ArrowLeft = ({ className }: ArrowLeftProps) => ( = ({ className }) => ( +const ArrowRight = ({ className }: ArrowRightProps) => ( = ({ className }) => ( +const Badge = ({ className }: BadgeProps) => ( = ({ className }) => ( +const Blaze = ({ className }: BlazeProps) => ( = ({ className }) => ( +const Check = ({ className }: CheckProps) => ( = ({ className }) => ( +const CheckMark = ({ className }: CheckMarkProps) => ( ; + className?: string; + onClick?: React.MouseEventHandler; } -const Close: React.FC = ({ className, onClick }) => ( +const Close = ({ className, onClick }: CloseProps) => ( = ({ className }) => ( +const CopyPaste = ({ className }: CopyPasteProps) => ( = ({ className }) => ( +const Edit = ({ className }: EditProps) => ( = ({ className }) => ( +const Eye = ({ className }: EyeProps) => ( ; + className?: string; + onClick?: React.MouseEventHandler; } -const Arr: React.FC = ({ className, onClick }) => ( +const Arr = ({ className, onClick }: ArrProps) => ( = ({ className }) => ( +const Heart = ({ className }: HeartProps) => ( ( + + + +); + +export default Information; diff --git a/components/assets/key.tsx b/components/assets/key.tsx index f7b27a81..052d72de 100644 --- a/components/assets/key.tsx +++ b/components/assets/key.tsx @@ -1,10 +1,10 @@ import React from 'react'; interface KeyProps { - className: string; + className?: string; } -const Key: React.FC = ({ className }) => ( +const Key = ({ className }: KeyProps) => ( = ({ className }) => ( +const PowerOff = ({ className }: PowerOffProps) => ( = ({ className }) => ( +const Scale = ({ className }: ScaleProps) => ( = ({ className }) => ( +const Share = ({ className }: ShareProps) => ( = ({ className }) => ( +const Ternoart = ({ className }: TernoartProps) => ( = ({ className }) => ( +const Upload = ({ className }: UploadProps) => ( = ({ className }) => ( +const Wallet = ({ className }: WalletProps) => ( = ({ className }) => ( +const Fortmatic = ({ className }: FortmaticProps) => ( = ({ className }) => ( +const Metamask = ({ className }: MetamaskProps) => ( = ({ className }) => ( +const WalletConnect = ({ className }: WalletConnectProps) => ( = ({ className }) => ( +const WalletLink = ({ className }: WalletLinkProps) => ( = ({ user, + walletId, showTooltip = true, size, className, @@ -25,7 +27,7 @@ const Creator: React.FC = ({ const [isHovering, setIsHovering] = useState(false); const bgGradient = { - background: user.name ? gradient(user.name) : gradient('ternoa'), + background: gradient(user?.name || walletId), }; function manageClass() { @@ -47,7 +49,7 @@ const Creator: React.FC = ({
- isClickable && user && user.walletId && Router.push(`/${user.walletId}`) + isClickable && walletId && Router.push(`/${walletId}`) } >
= ({ : style.Hide } > -
{user.name}
+
{user?.name || walletId}
- {user.name ? ( -
false} - onBlur={() => false} - onMouseOver={() => setIsHovering(true)} - onMouseOut={() => setIsHovering(false)} - data-tip - data-for={showTooltip && `tooltip${user._id}`} - > - {user.verified && } - {user.picture ? ( - - ) : ( -
-
- {user.name.charAt(0)} -
+
false} + onBlur={() => false} + onMouseOver={() => setIsHovering(true)} + onMouseOut={() => setIsHovering(false)} + data-tip + data-for={showTooltip && `tooltip${walletId}`} + > + {user?.verified && } + {user?.picture ? ( + + ) : ( +
+
+ {user?.name.charAt(0) || 'T'}
- )} -
- ) : ( - '' - )} +
+ )} +
); }; diff --git a/components/base/FloatingHeader/FloatingHeader.tsx b/components/base/FloatingHeader/FloatingHeader.tsx index f647b034..3c9c1d8a 100644 --- a/components/base/FloatingHeader/FloatingHeader.tsx +++ b/components/base/FloatingHeader/FloatingHeader.tsx @@ -11,6 +11,7 @@ import gradient from 'random-gradient'; import { UserType } from 'interfaces/index'; import { onModelOpen } from '../../../utils/model-helpers'; +import { clipboardCopy } from 'utils/functions'; export interface FloatingHeaderProps { user: UserType; setModalExpand: (b: boolean) => void; @@ -152,8 +153,8 @@ const FloatingHeader: React.FC = ({
- -
{user?.name}
+ +
{user.name}
@@ -166,7 +167,7 @@ const FloatingHeader: React.FC = ({ { - navigator.clipboard.writeText(user.walletId); + clipboardCopy(user.walletId); }} > {middleEllipsis(user.walletId, 20)} diff --git a/components/base/Footer/Footer.module.scss b/components/base/Footer/Footer.module.scss index d9b8451e..2e10ce22 100644 --- a/components/base/Footer/Footer.module.scss +++ b/components/base/Footer/Footer.module.scss @@ -5,7 +5,7 @@ flex-direction: column; width: 100%; background-color: black; - height: 450px; + height: auto; box-sizing: border-box; justify-content: space-between; align-items: center; @@ -13,17 +13,17 @@ } .WaterMark { - width: 260px; + width: 11.2rem; position: absolute; left: 25%; - top: 60px; + top: 2rem; z-index: 1; } .Container { display: flex; flex-direction: column; - margin-top: 100px; + margin-top: 8rem; width: 100%; box-sizing: border-box; z-index: 2; @@ -79,7 +79,7 @@ .SocialMedias { display: flex; - margin-top: 24px; + margin: 2.4rem 0; align-items: center; justify-content: space-around; width: 400px; diff --git a/components/base/Footer/Footer.tsx b/components/base/Footer/Footer.tsx index 63a7511b..066db067 100644 --- a/components/base/Footer/Footer.tsx +++ b/components/base/Footer/Footer.tsx @@ -9,11 +9,7 @@ import Instagram from 'components/assets/SocialMedias/Instagram'; import Github from 'components/assets/SocialMedias/Github'; import Youtube from 'components/assets/SocialMedias/Youtube'; -export interface FooterProps { - setNotAvailable: (b: boolean) => void; -} - -const Footer: React.FC = ({ setNotAvailable }) => { +const Footer = () => { const telegramLink = process.env.NEXT_PUBLIC_TELEGRAM_LINK ? process.env.NEXT_PUBLIC_TELEGRAM_LINK : "https://t.me/ternoa" const twitterLink = process.env.NEXT_PUBLIC_TWITTER_LINK ? process.env.NEXT_PUBLIC_TWITTER_LINK : "https://twitter.com/SecretNFT_" const linkedinLink = process.env.NEXT_PUBLIC_LINKEDIN_LINK ? process.env.NEXT_PUBLIC_LINKEDIN_LINK : "https://www.linkedin.com/company/50607388/" @@ -24,17 +20,6 @@ const Footer: React.FC = ({ setNotAvailable }) => {
- Keep in touch ! -
- -
setNotAvailable(true)}> - Go -
-
{telegramLink!=="false" && true} className={style.SVGMedia} /> diff --git a/components/base/MainHeader/MainHeader.tsx b/components/base/MainHeader/MainHeader.tsx index 0d20ae75..4daab5b6 100644 --- a/components/base/MainHeader/MainHeader.tsx +++ b/components/base/MainHeader/MainHeader.tsx @@ -10,6 +10,7 @@ import { computeCaps, computeTiime, middleEllipsis } from 'utils/strings'; import gradient from 'random-gradient'; import { UserType } from 'interfaces/index'; +import { clipboardCopy } from 'utils/functions'; export interface HeaderProps { user: UserType; @@ -112,8 +113,8 @@ const MainHeader: React.FC = ({ setModalExpand, user }) => {
- -
{user?.name}
+ +
{user.name}
@@ -128,7 +129,7 @@ const MainHeader: React.FC = ({ setModalExpand, user }) => { { - navigator.clipboard.writeText(user.walletId); + clipboardCopy(user.walletId); }} > {middleEllipsis(user.walletId, 20)} diff --git a/components/base/Media/Media.tsx b/components/base/Media/Media.tsx index 38cb81ef..ab76350a 100644 --- a/components/base/Media/Media.tsx +++ b/components/base/Media/Media.tsx @@ -44,10 +44,12 @@ const Media: React.FC> = ({ } useEffect(()=>{ checkSrcAvailable() - }, []) + }, [src]) useEffect(()=>{ - if (fetchStatusOk) + if (fetchStatusOk){ setMediaSrc(src) + setFetchStatusOk(false) + } }, [fetchStatusOk]) return ( <> diff --git a/components/base/ModalShare/ModalShare.module.scss b/components/base/ModalShare/ModalShare.module.scss index 69eaa24f..8a904964 100644 --- a/components/base/ModalShare/ModalShare.module.scss +++ b/components/base/ModalShare/ModalShare.module.scss @@ -63,6 +63,7 @@ padding: 5px 10px; margin-top: 10px; width: 90%; + height: 20px; display: flex; justify-content: space-between; align-items: center; @@ -76,13 +77,13 @@ } .CopyPaste{ - width: 16px; display: flex; cursor: pointer; } .CopyPasteSvg{ fill: white; + width: 16px; } @media (max-width: 720px) { diff --git a/components/base/ModalShare/ModalShare.tsx b/components/base/ModalShare/ModalShare.tsx index b4383bdd..4a3506df 100644 --- a/components/base/ModalShare/ModalShare.tsx +++ b/components/base/ModalShare/ModalShare.tsx @@ -17,6 +17,7 @@ import { TwitterIcon, WhatsappIcon } from 'react-share'; +import { clipboardCopy } from 'utils/functions'; export interface ModalWalletProps { setModalExpand: (b: boolean) => void; @@ -85,7 +86,7 @@ const ModalShare: React.FC = ({ setModalExpand, title, subject
{ - navigator.clipboard.writeText(url); + clipboardCopy(url); }}>
{url}
diff --git a/components/base/NftCard/NftCard.module.scss b/components/base/NftCard/NftCard.module.scss index f5c7a20c..cce6166a 100644 --- a/components/base/NftCard/NftCard.module.scss +++ b/components/base/NftCard/NftCard.module.scss @@ -1,47 +1,5 @@ @import "style/colors.scss"; -.NFT { - display: flex; - position: relative; - box-sizing: border-box; - width: 250px; - height: 390px; - border-radius: 12px; - background: linear-gradient(180deg, #f29fff 0%, #878cff 100%); - box-shadow: 0px 0px 14.5243px 5.0835px rgba(0, 0, 0, 0.15); - cursor: pointer; - overflow: hidden; - transform: translateZ(0); -} - -.NFTGrid { - display: flex; - position: relative; - box-sizing: border-box; - width: 250px; - height: 390px; - border-radius: 12px; - background: linear-gradient(180deg, #f29fff 0%, #878cff 100%); - box-shadow: 0px 0px 14.5243px 5.0835px rgba(0, 0, 0, 0.15); - cursor: pointer; - overflow: hidden; - transform: translateZ(0); -} - -.NFTProfile { - display: flex; - position: relative; - box-sizing: border-box; - width: 250px; - height: 390px; - border-radius: 12px; - background: linear-gradient(180deg, #f29fff 0%, #878cff 100%); - box-shadow: 0px 0px 14.5243px 5.0835px rgba(0, 0, 0, 0.15); - cursor: pointer; - overflow: hidden; - transform: translateZ(0); -} - .Container { display: flex; justify-content: space-between; @@ -75,34 +33,6 @@ animation: scaleOut 0.8s cubic-bezier(0.25, 1, 0.5, 1); } -.SecretLabel { - background-color: $primary; - position: absolute; - padding: 2.5px 20px; - z-index: 4; - border-radius: 35px; - font-family: "Airbnb Cereal App Medium"; - font-size: 19px; - margin: 0px; - color: white; - top: 16px; - right: 16px; -} - -.QtyLabel { - background-color: white; - position: absolute; - padding: 2.5px 20px; - z-index: 4; - border-radius: 35px; - font-family: "Airbnb Cereal App Medium"; - font-size: 19px; - margin: 0px; - color: $primary; - top: 16px; - left: 16px; -} - .Favorite { background-color: white; align-self: flex-end; @@ -189,33 +119,6 @@ animation-fill-mode: forwards; } -.Button { - display: flex; - justify-content: space-between; - align-items: center; - border: 2px solid white; - border-radius: 28px; - padding: 8px 18px; - margin-top: 8px; -} - -.ButtonText { - font-family: "Airbnb Cereal App Bold"; - color: white; - font-style: normal; - font-weight: bold; - font-size: 14px; -} - -.Price { - font-family: "Airbnb Cereal App Light"; - text-transform: uppercase; - color: white; - font-size: 13px; - margin-right: 16px; - margin-top: 1px; -} - .Hide { display: none; } @@ -279,21 +182,13 @@ } @media (max-width: 1300px) { - .NFT, - .NFTGrid { - width: 220px; - height: 330px; - } - .Button { padding: 6px 14px; margin-top: 8px; } - .ButtonText { font-size: 12px; } - .Price { font-size: 11px; margin-right: 12px; @@ -301,21 +196,7 @@ } } -@media (max-width: 720px) { - .NFT, - .NFTGrid, - .NFTProfile { - width: 190px; - height: 280px; - } - - .SecretLabel { - padding: 2.5px 16px; - font-size: 16px; - top: 12px; - right: 12px; - } - +@media (max-width: 768px) { .Favorite { padding: 7px 6px; top: 12px; @@ -325,26 +206,3 @@ width: 16px; } } - -@media (max-width: 600px) { - .NFTGrid { - width: 270px; - height: 380px; - } - .NFT { - width: 190px; - height: 280px; - } - - .NFTProfile { - width: 160px; - height: 283px; - } -} - -@media (max-width: 400px) { - .NFTProfile { - width: 130px; - height: 230px; - } -} diff --git a/components/base/NftCard/NftCard.tsx b/components/base/NftCard/NftCard.tsx index 8bc9948c..206db1c4 100644 --- a/components/base/NftCard/NftCard.tsx +++ b/components/base/NftCard/NftCard.tsx @@ -1,18 +1,23 @@ import React, { useState, useEffect } from 'react'; +import styled from 'styled-components'; import style from './NftCard.module.scss'; import Creator from '../Creator'; import Router from 'next/router'; import { useMediaQuery } from 'react-responsive'; import Media from '../Media'; import Heart from 'components/assets/heart'; +import Chip from 'components/ui/Chip'; import { NftType, UserType } from 'interfaces/index'; import { computeCaps, computeTiime } from 'utils/strings'; import { likeNFT, unlikeNFT } from 'actions/user'; import { getNFT } from 'actions/nft'; +import { breakpointMap } from 'style/theme/base'; +export type ModeType = 'grid'; export interface NftCardProps { + className?: string; item: NftType; - mode: string; + mode?: ModeType; isDragging?: boolean; user?: UserType; setUser?: (u: UserType) => void @@ -30,6 +35,7 @@ function manageRouting( } const NftCard: React.FC = ({ + className, item, mode, isDragging, @@ -64,7 +70,7 @@ const NftCard: React.FC = ({ useEffect(() => { async function callBack() { try { - let res = await fetch(item.media!.url, { method: 'HEAD' }); + let res = await fetch(item.properties?.preview.ipfs!, { method: 'HEAD' }); setType(res.headers.get('Content-Type')); return res; } catch (err) { @@ -75,17 +81,8 @@ const NftCard: React.FC = ({ callBack(); }, []); - function manageClass() { - if (mode === 'grid') { - return style.NFTGrid; - } else if (mode === 'profile') { - return style.NFTProfile; - } else { - return style.NFT; - } - } - - const isMobile = useMediaQuery({ query: '(max-device-width: 720px)' }); + const isMobile = useMediaQuery({ query: `(max-width: ${breakpointMap.md - 1}px)` }); + const isLargeDesktop = useMediaQuery({ query: `(min-width: ${breakpointMap.xxl}px)` }); const handleLikeDislike = async (nftId: string, serieId: string) => { try{ @@ -119,29 +116,74 @@ const NftCard: React.FC = ({ } return ( -
!isDragging && Router.push(`/nft/${item.id}`)} - className={manageClass()} + className={className} + mode={mode} onFocus={() => false} onBlur={() => false} onMouseOver={() => !isMobile && setIsHovering(true)} onMouseOut={() => !isMobile && setIsHovering(false)} > - {Number(displayQuantity()) > 1 && - {displayQuantity()} - } - {item.cryptedMedia?.url !== item.media?.url && !isHovering && ( - S + {Number(displayQuantity()) > 1 && ( + + + )} + {item.properties?.cryptedMedia.ipfs !== item.properties?.preview.ipfs && + !isHovering && ( + + + + )} + {((item.smallestPrice && Number(item.smallestPrice)) || + (item.smallestPriceTiime && Number(item.smallestPriceTiime))) && + !isHovering && ( + + + {item.smallestPrice && + Number(item.smallestPrice) > 0 && + `${computeCaps(Number(item.smallestPrice))} CAPS`} + {item.smallestPrice && + Number(item.smallestPrice) && + item.smallestPriceTiime && + Number(item.smallestPriceTiime) && + ` / `} + {item.smallestPriceTiime && + Number(item.smallestPriceTiime) > 0 && + `${computeTiime(Number(item.smallestPriceTiime))} TIIME`} + + } + variant="round" + /> + + )}
= ({ } />
- {isLiked!==undefined ? -
{e.stopPropagation(); handleLikeDislike(item.id, item.serieId);}} + onClick={(e) => { + e.stopPropagation(); + handleLikeDislike(item.id, item.serieId); + }} >
- : -
- } + ) : ( +
+ )}
manageRouting(e, item.creatorData.walletId)} + onClick={(e) => manageRouting(e, item.creator)} className={style.Auth} > - {item.creatorData && ( - - )} - {item.creatorData && ( -
- {item.creatorData.name} -
- )} + +
+ {item.creatorData?.name || `Ternoa #${item.creator.slice(0, 5)}`} +
- {((item.smallestPrice && Number(item.smallestPrice)) || (item.smallestPriceTiime && Number(item.smallestPriceTiime))) && -
-
- {item.smallestPrice && Number(item.smallestPrice)>0 && - `${computeCaps(Number(item.smallestPrice))} CAPS` - } - {item.smallestPrice && Number(item.smallestPrice) && item.smallestPriceTiime && Number(item.smallestPriceTiime) && - ` / ` + {((item.smallestPrice && Number(item.smallestPrice)) || + (item.smallestPriceTiime && Number(item.smallestPriceTiime))) && ( + + + {item.smallestPrice && + Number(item.smallestPrice) > 0 && + `${computeCaps(Number(item.smallestPrice))} CAPS`} + {item.smallestPrice && + Number(item.smallestPrice) && + item.smallestPriceTiime && + Number(item.smallestPriceTiime) && + ` / `} + {item.smallestPriceTiime && + Number(item.smallestPriceTiime) > 0 && + `${computeTiime(Number(item.smallestPriceTiime))} TIIME`} + } - {item.smallestPriceTiime && Number(item.smallestPriceTiime)>0 && - `${computeTiime(Number(item.smallestPriceTiime))} TIIME` - } -
-
Buy
-
- } -
+ variant="round" + /> + + )} +
-
+ ); }; +const SMediaWrapper = styled.div<{ mode?: ModeType }>` + display: flex; + position: relative; + box-sizing: border-box; + border-radius: 12px; + background: linear-gradient(180deg, #f29fff 0%, #878cff 100%); + box-shadow: 0px 0px 14.5243px 5.0835px rgba(0, 0, 0, 0.15); + cursor: pointer; + overflow: hidden; + transform: translateZ(0); + + ${({ mode, theme }) => { + switch (mode) { + case 'grid': { + return ` + height: ${theme.sizes.cardHeight.md}; + width: ${theme.sizes.cardWidth.md}; + + ${theme.mediaQueries.sm} { + height: ${theme.sizes.cardHeight.sm}; + width: ${theme.sizes.cardWidth.sm}; + } + + ${theme.mediaQueries.xxl} { + height: ${theme.sizes.cardHeight.md}; + width: ${theme.sizes.cardWidth.md}; + } + `; + } + default: { + return ` + height: ${theme.sizes.cardHeight.xs}; + width: ${theme.sizes.cardWidth.xs}; + + ${theme.mediaQueries.md} { + height: ${theme.sizes.cardHeight.sm}; + width: ${theme.sizes.cardWidth.sm}; + } + + ${theme.mediaQueries.xxl} { + height: ${theme.sizes.cardHeight.md}; + width: ${theme.sizes.cardWidth.md}; + } + `; + } + } + }} +`; + +const SChipWrapper = styled.div` + background: transparent; + position: absolute; + z-index: 4; +` + +const SAvailableChipWrapper = styled(SChipWrapper)` + top: 1.6rem; + left: 1.6rem; +` + +const SSecretChipWrapper = styled(SChipWrapper)` + top: 1.6rem; + right: 1.6rem; +` + +const SPriceChipWrapper = styled(SChipWrapper)` + width: fit-content; + bottom: 1.6rem; + left: 0; + right: 0; + margin: 0 auto; +` + +const SPriceWrapper = styled.div` + margin-top: 0.8rem; +` + export default NftCard; diff --git a/components/base/NftCard/index.tsx b/components/base/NftCard/index.tsx index beeb980b..59c1ff23 100644 --- a/components/base/NftCard/index.tsx +++ b/components/base/NftCard/index.tsx @@ -1 +1,2 @@ -export { default } from "./NftCard"; +export { default } from './NftCard'; +export type { ModeType } from './NftCard'; \ No newline at end of file diff --git a/components/base/NftPreview/NftPreview.tsx b/components/base/NftPreview/NftPreview.tsx new file mode 100644 index 00000000..c24e4a33 --- /dev/null +++ b/components/base/NftPreview/NftPreview.tsx @@ -0,0 +1,306 @@ +import React from 'react'; +import { useMediaQuery } from 'react-responsive'; +import styled from 'styled-components'; +import Eye from 'components/assets/eye'; +import { NftCardWithEffects, NftUpload } from 'components/base/NftPreview'; +import { updateFile } from 'components/base/NftPreview/components/NftUpload'; +import { HiddenInput, HiddenShell, Subtitle } from 'components/layout'; +import Radio from 'components/ui/Radio'; +import Select from 'components/ui/Select'; +import { + NftEffectType, + NFT_EFFECT_BLUR, + NFT_EFFECT_DEFAULT, + NFT_EFFECT_PROTECT, + NFT_EFFECT_SECRET, + NFT_FILE_TYPE_GIF, + NFT_FILE_TYPE_VIDEO, +} from 'interfaces'; +import { breakpointMap } from 'style/theme/base'; + +interface Props { + blurValue: number; + className?: string; + coverNFT: File | null; + effect: NftEffectType; + isRN?: boolean; + originalNFT: File | null; + setBlurValue: (n: number) => void; + setCoverNFT: (f: File | null) => void; + setEffect: (effect: NftEffectType) => void; + setError: (err: string) => void; + setIsLoading: (b: boolean) => void; + setOriginalNFT: (f: File | null) => void; +} + +const NFT_EFFECTS_ORDERED: NftEffectType[] = [ + NFT_EFFECT_DEFAULT, + NFT_EFFECT_PROTECT, + NFT_EFFECT_SECRET, + NFT_EFFECT_BLUR, +]; + +const NftPreview = ({ + blurValue, + className, + coverNFT, + effect, + isRN, + originalNFT, + setBlurValue, + setCoverNFT, + setEffect, + setError, + setOriginalNFT, +}: Props) => { + const isMobile = useMediaQuery({ + query: `(max-width: ${breakpointMap.md}px)`, + }); + + const handleAllowedEffect = (file: File, effect: NftEffectType) => { + switch (effect) { + case NFT_EFFECT_BLUR: + case NFT_EFFECT_PROTECT: + return ( + !file.type.includes(NFT_FILE_TYPE_VIDEO) && + file.type !== NFT_FILE_TYPE_GIF + ); + default: + return true; + } + }; + + const handleFileUpload = (event: React.ChangeEvent) => { + updateFile( + event, + setError, + (file: File) => { + setOriginalNFT(file); + setEffect(NFT_EFFECT_DEFAULT); + }, + isRN + ); + }; + + if (originalNFT === null) { + return ( + + ); + } + + return ( +
+ + + + NFT Preview + + {originalNFT.name && ( + + + + )} + + {isMobile && effect !== undefined ? ( + + + + + + {(setSelectExpanded) => ( + <> + {NFT_EFFECTS_ORDERED.filter((effectType) => + handleAllowedEffect(originalNFT, effectType) + ).map( + (effectType, id) => + effectType !== effect && ( +
  • { + setSelectExpanded(false); + setEffect(effectType); + }} + > + {effectType} +
  • + ) + )} + + )} +
    + +
    + ) : ( + + {NFT_EFFECTS_ORDERED.filter((effectType) => + handleAllowedEffect(originalNFT, effectType) + ).map((effectType) => ( + + + + + + + setEffect(effectType)} + /> + + + + setEffect(effectType)} + value={effectType} + /> + + + ))} + + )} +
    + ); +}; + +const SHeader = styled.div` + display: flex; + flex-direction: column; + align-items: center; + margin: 0 0 2.4rem; + + ${({ theme }) => theme.mediaQueries.md} { + flex-direction: row; + margin-top: 4rem; + justify-content: start; + } +`; + +const SEyeIcon = styled(Eye)` + width: 2.4rem; + margin-right: 1rem; + fill: black; +`; + +const SReuploadWrapper = styled.div` + margin: 1.6rem 0 0; + + ${({ theme }) => theme.mediaQueries.md} { + margin: 0 0 0 2.4rem; + padding-top: 0.6rem; + } +`; + +const SWrapper = styled.div` + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; +`; + +const SMobileCardWrapper = styled.div` + width: 100%; + display: flex; + justify-content: center; + filter: drop-shadow(0px 0px 10.4276px rgba(0, 0, 0, 0.25)); +`; + +const SSelect = styled(Select)` + margin-top: 2.4rem; +`; + +const SSeparator = styled.div` + width: 15rem; + border-bottom: 2px solid #e0e0e0; + margin-top: 3.2rem; +`; + +const SFieldset = styled.fieldset` + width: 100%; + display: flex; + justify-content: center; + align-items: center; + gap: 1.2rem; + border: none; + padding: 0; +`; + +const SLabelWrapper = styled.label<{ isSelected?: boolean }>` + display: flex; + align-items: center; + cursor: pointer; + flex-direction: column; + + ${({ theme }) => theme.mediaQueries.md} { + flex: 1 1 0; + max-width: 280px; + } +`; + +const SLabel = styled.label<{ isSelected?: boolean }>` + width: 100%; + height: auto; + background: transparent; + border: 3px solid rgb(0, 0, 0, 0); + border-radius: 2rem; + padding: 0.8rem 0.8rem 2.4rem; + + &:hover { + border: 3px dashed #7417ea; + } + + ${({ isSelected }) => + isSelected && + ` + border: 3px dashed #7417ea; + `} +`; + +const SCardWrapper = styled.div<{ isSelected: boolean }>` + width: 100%; + height: auto; + opacity: ${({ isSelected }) => (isSelected ? 1 : 0.4)}; +`; + +const SRadio = styled(Radio)` + margin-top: 3.2rem; +`; + +export default React.memo(NftPreview); diff --git a/components/base/NftPreview/components/NftCardWithEffects.tsx b/components/base/NftPreview/components/NftCardWithEffects.tsx new file mode 100644 index 00000000..f0547b81 --- /dev/null +++ b/components/base/NftPreview/components/NftCardWithEffects.tsx @@ -0,0 +1,275 @@ +import React from 'react'; +import { useMediaQuery } from 'react-responsive'; +import styled, { css } from 'styled-components'; +import Icon from 'components/ui/Icon'; +import { NftUpload } from 'components/base/NftPreview'; +import { updateFile } from 'components/base/NftPreview/components/NftUpload'; +import { + NftEffectType, + NFT_EFFECT_BLUR, + NFT_EFFECT_PROTECT, + NFT_EFFECT_SECRET, + NFT_FILE_TYPE_IMAGE, + NFT_FILE_TYPE_VIDEO, +} from 'interfaces'; +import Chip from 'components/ui/Chip'; +import Slider from 'components/ui/Slider'; +import { breakpointMap } from 'style/theme/base'; + +interface Props { + blurValue: number; + coverNFT: File | null; + className?: string; + effect: NftEffectType; + isRN?: boolean; + originalNFT: File; + setBlurValue: (v: number) => void; + setCoverNFT: (f: File | null) => void; + setEffect: (effect: NftEffectType) => void; + setError: (err: string) => void; +} + +const DefaultEffect = css` + width: 100%; + height: 100%; + border-radius: 1.2rem; + background: linear-gradient(180deg, #f29fff 0%, #878cff 100%); + box-shadow: 0px 0px 14.5243px 5.0835px rgba(0, 0, 0, 0.15); + object-fit: cover; + overflow: hidden; + position: absolute; + transform: translateZ(0); +`; +const SImage = styled.img<{ blurredValue: number }>` + ${DefaultEffect} + filter: ${({ blurredValue }) => `blur(${blurredValue}px)`}; + backdrop-filter: ${({ blurredValue }) => `blur(${blurredValue}px)`}; + -webkit-backdrop-filter: ${({ blurredValue }) => `blur(${blurredValue}px)`}; +`; +const SVideo = styled.video` + ${DefaultEffect} +`; + +function returnType(NFTarg: File, blurredValue: number = 0) { + if (NFTarg!.type.substr(0, 5) === NFT_FILE_TYPE_IMAGE) { + return ( + + ); + } else if (NFTarg!.type.substr(0, 5) === NFT_FILE_TYPE_VIDEO) { + return ( + + + + ); + } +} + +const NftCardWithEffects = ({ + blurValue, + className, + coverNFT, + effect, + isRN, + originalNFT, + setBlurValue, + setCoverNFT, + setEffect, + setError, +}: Props) => { + + const isTablet = useMediaQuery({ + minWidth: breakpointMap.md, + maxWidth: breakpointMap.lg - 1, + }); + + const handleBlurredChange = (event: React.ChangeEvent) => { + const { target } = event; + const newBlur = Number(target.value); + setEffect(NFT_EFFECT_BLUR); + setBlurValue(newBlur); + }; + + const handleSecretFileUpload = ( + event: React.ChangeEvent + ) => { + updateFile( + event, + setError, + (file: File) => { + setCoverNFT(file); + setEffect(NFT_EFFECT_SECRET); + }, + isRN + ); + }; + + return ( + + {returnType(originalNFT, effect === NFT_EFFECT_BLUR ? blurValue : 0)} + {effect === NFT_EFFECT_BLUR && ( + + )} + {effect === NFT_EFFECT_PROTECT && } + {effect === NFT_EFFECT_SECRET && ( + + + {coverNFT === null ? ( + + + Upload the preview of your secret. + + {!isTablet && ( + + Once purchased, the owner will be able to see your NFT + + )} + + } + inputId="uploadOriginalNft" + isRN={isRN} + isSecretOption + note={`JPEG, JPG, PNG, GIF ${ + !isRN ? ', MP4 or MOV' : '' + }. Max 30mb.`} + onChange={handleSecretFileUpload} + /> + ) : ( + + )} + + {(!isTablet || coverNFT) && ( + + )} + + )} + + ); +}; +const SWrapper = styled.div` + position: relative; + width: 100%; + border-radius: 1.2rem; + max-width: 250px; + width: ${({ theme }) => theme.sizes.cardWidth.md}; + height: ${({ theme }) => theme.sizes.cardHeight.md}; + overflow: hidden; + + ${({ theme }) => theme.mediaQueries.md} { + width: auto; + height: ${({ theme }) => theme.sizes.cardHeight.sm}; + } + + ${({ theme }) => theme.mediaQueries.lg} { + height: ${({ theme }) => theme.sizes.cardHeight.md}; + } +`; + +const SCoverWrapper = styled.div` + position: relative; + width: ${({ theme }) => theme.sizes.cardWidth.sm}; + height: ${({ theme }) => theme.sizes.cardHeight.sm}; + + ${({ theme }) => theme.mediaQueries.md} { + width: ${({ theme }) => theme.sizes.cardWidth.xs}; + height: ${({ theme }) => theme.sizes.cardHeight.xs}; + } + + ${({ theme }) => theme.mediaQueries.lg} { + width: ${({ theme }) => theme.sizes.cardWidth.sm}; + height: ${({ theme }) => theme.sizes.cardHeight.sm}; + } +`; + +const SSlider = styled(Slider)` + width: 100%; + position: absolute; + bottom: 4.8rem; + padding: 0 1.6rem; + z-index: 10; +`; + +const SIcon = styled(Icon)` + width: 10rem; + position: absolute; + bottom: 1.6rem; + left: 1.6rem; + z-index: 10; + + ${({ theme }) => theme.mediaQueries.lg} { + width: 14rem; + } +`; + +const SSecretWrapper = styled.div` + position: absolute; + top: 0; + left: 0; + width: 100%; + height: auto; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + padding: 2.4rem 0 0; + + ${({ theme }) => theme.mediaQueries.md} { + padding: 2rem 0 0; + } + + ${({ theme }) => theme.mediaQueries.xl} { + padding: 2.4rem 0 0; + } +`; + +const SecretUploadDescription = styled.div` + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; +`; + +const SecretUploadTopDescription = styled.span` + color: #7417ea; + font-family: ${({ theme }) => theme.fonts.bold}; + margin-bottom: 0.8rem; +`; + +const SChip = styled(Chip)` + width: fit-content; + margin: 1.6rem auto 0; +`; + +export default React.memo(NftCardWithEffects); diff --git a/components/base/NftPreview/components/NftUpload.tsx b/components/base/NftPreview/components/NftUpload.tsx new file mode 100644 index 00000000..c359967a --- /dev/null +++ b/components/base/NftPreview/components/NftUpload.tsx @@ -0,0 +1,189 @@ +import React, { useEffect, useState } from 'react'; +import styled from 'styled-components'; +import Upload from 'components/assets/upload'; +import { HiddenInput, HiddenShell, InsightLight } from 'components/layout'; +import Chip from 'components/ui/Chip'; + +interface Props { + className?: string; + content?: string | React.ReactNode; + inputId: string; + isMinimal?: boolean; + isRN?: boolean; + isSecretOption?: boolean; + note?: string; + onChange: (event: React.ChangeEvent) => void; +} + +export const updateFile = ( + event: React.ChangeEvent, + setError: (s: string) => void, + setFunction: (f: File) => void, + isRN = false +) => { + const { target } = event; + let file = target?.files?.[0]; + let isError = false; + + if (file !== null && file !== undefined) { + if (!isError && isRN && file!.type.substr(0, 5) === 'video') { + setError("You can't select video type on mobile DApp yet."); + isError = true; + } + if ( + !isError && + !( + file!.type.substr(0, 5) === 'video' || + file!.type.substr(0, 5) === 'image' + ) + ) { + setError( + `You can't select files different from ${ + isRN === false ? 'videos or ' : '' + }images.` + ); + isError = true; + } + if (!isError && file.size > 31000000) { + setError('Max file size is 30mb.'); + isError = true; + } + if (!isError) { + setFunction(file); + } + } +}; + +const NftUpload = ({ + className, + content, + inputId, + isMinimal = false, + isRN, + isSecretOption = false, + note, + onChange, +}: Props) => { + const [acceptedFileTypes, setAcceptedFileTypes] = useState([ + '.jpg', + '.jpeg', + '.png', + '.gif', + '.mp4', + '.mov', + ]); + + useEffect(() => { + if (isRN) { + setAcceptedFileTypes(['.jpg', '.jpeg', '.png', '.gif']); + } + }, [isRN]); + + if (isMinimal) { + return ( + <> + + + + + + ); + } + + return ( + + + {isSecretOption && ( + + )} + {!isSecretOption && } + {content && ( + {content} + )} + {isSecretOption && } + {note && {note}} + + + + + + + ); +}; + +const Label = styled.label` + color: #7417ea; + cursor: pointer; + font-family: 'Airbnb Cereal App Light'; + font-size: 1.2rem; + font-style: italic; +`; + +const SLabel = styled.label<{ isSecretOption?: boolean }>` + width: 100%; + height: auto; + background: white; + display: flex; + justify-content: center; + position: relative; + border: 3px dashed #7417ea; + border-radius: 1.6rem; + cursor: pointer; + + ${({ isSecretOption }) => + isSecretOption && + ` + border: none; + box-shadow: 0px 4px 4px rgba(0, 0, 0, 0.25); + padding: 2rem; + `} +`; + +const SWrapper = styled.div<{ isSmall?: boolean }>` + display: flex; + flex-direction: column; + padding: ${({ isSmall }) => (isSmall ? 0 : '8rem 2.4rem')}; + width: 100%; + max-width: ${({ isSmall }) => (isSmall ? '16rem' : '26rem')}; + align-items: center; + justify-content: center; + cursor: pointer; + text-align: center; + + > * { + margin-top: 1.6rem; + + &:first-child { + margin-top: 0; + } + } +`; + +const SUploadIcon = styled(Upload)<{ isSmall?: boolean }>` + width: ${({ isSmall }) => (isSmall ? '4rem' : '8rem')}; +`; + +const SInsightMedium = styled(InsightLight)<{ isSmall?: boolean }>` + color: #686464; + font-size: ${({ isSmall }) => (isSmall ? '1.2rem' : '1.6rem')}; + margin: 1.6rem 0; +`; + +export default NftUpload; diff --git a/components/base/NftPreview/index.tsx b/components/base/NftPreview/index.tsx new file mode 100644 index 00000000..47c6b942 --- /dev/null +++ b/components/base/NftPreview/index.tsx @@ -0,0 +1,3 @@ +export { default } from "./NftPreview"; +export { default as NftCardWithEffects } from './components/NftCardWithEffects'; +export { default as NftUpload } from "./components/NftUpload"; diff --git a/components/base/NoNFTComponent/NoNFTComponent.module.scss b/components/base/NoNFTComponent/NoNFTComponent.module.scss deleted file mode 100644 index 6cc5e1a9..00000000 --- a/components/base/NoNFTComponent/NoNFTComponent.module.scss +++ /dev/null @@ -1,33 +0,0 @@ -@import "style/colors.scss"; - - -.SoldOutWrapper { - display: flex; - justify-content: center; - padding: 70px 0px; -} - -.SoldOutContainer { - display: flex; - flex-direction: column; - align-items: center; -} - -.SoldOutImage { - width: 40%; -} -.SoldOutTitle { - font-family: "Airbnb Cereal App Bold"; - color: $primary; - text-align: center; - font-size: 16px; - margin-top: 20px; -} - -.SoldOutLabel { - font-family: "Airbnb Cereal App Light"; - text-align: center; - font-size: 14px; - margin-bottom: 15px; -} - \ No newline at end of file diff --git a/components/base/NoNFTComponent/NoNFTComponent.tsx b/components/base/NoNFTComponent/NoNFTComponent.tsx index 773bb2d7..44e656aa 100644 --- a/components/base/NoNFTComponent/NoNFTComponent.tsx +++ b/components/base/NoNFTComponent/NoNFTComponent.tsx @@ -1,27 +1,44 @@ import React from 'react'; -import style from './NoNFTComponent.module.scss'; -import NoNFTImage from 'components/assets/NoNFTImage'; +import styled from 'styled-components'; +import { Container, Wrapper } from 'components/layout'; +import Icon from 'components/ui/Icon'; -interface NoNFTComponentProps { -} - -const Code: React.FC = () => { +const Code = () => { return ( -
    -
    - -
    - All NFTs are sold ! -
    -
    - Come later to discover new NFTs. -
    -
    - Thanks ! -
    -
    -
    + + + + All NFTs are sold ! + + Come later to discover new NFTs. +
    +
    + Thanks ! +
    +
    +
    ); }; +const SIcon = styled(Icon)` + width: 16rem; + height: auto; + margin: 0 auto; +`; + +const STitle = styled.span` + color: ${({ theme }) => theme.colors.primary}; + font-family: ${({ theme }) => theme.fonts.bold}; + font-size: 1.6rem; + margin-top: 2.4rem; + text-align: center; +`; + +const SBody = styled.span` + color: ${({ theme }) => theme.colors.contrast}; + font-family: ${({ theme }) => theme.fonts.light}; + font-size: 1.4rem; + text-align: center; +`; + export default Code; diff --git a/components/base/QRCode/QRCode.tsx b/components/base/QRCode/QRCode.tsx index 9bc2b1a9..afba9ed9 100644 --- a/components/base/QRCode/QRCode.tsx +++ b/components/base/QRCode/QRCode.tsx @@ -13,7 +13,8 @@ interface CodeProps { price?: number; walletId?: string; quantity?: number; - uploadSize?: number + uploadSize?: number; + series_id?: string; }; action: string; } diff --git a/components/pages/Landing/Showcase/Showcase.module.scss b/components/base/Showcase/Showcase.module.scss similarity index 93% rename from components/pages/Landing/Showcase/Showcase.module.scss rename to components/base/Showcase/Showcase.module.scss index cb33fdf1..fff84f6e 100644 --- a/components/pages/Landing/Showcase/Showcase.module.scss +++ b/components/base/Showcase/Showcase.module.scss @@ -100,7 +100,7 @@ display: none; } -@media (max-width: 1300px) { +@media (max-width: 1299px) { .Title { font-size: 26px; } @@ -112,7 +112,7 @@ .Showcase { width: 900px; - padding: 40px 0px; + padding: 35px 0px; } .CarouselContainer { @@ -132,15 +132,12 @@ line-height: 15px; } .Showcase { - width: 100%; - padding: 32px 24px; + width: auto; + margin: 0 24px; } } -@media (max-width: 720px) { - .Showcase { - padding: 50px 16px; - } +@media (max-width: 767px) { .Infos { flex-direction: column; align-items: flex-start; diff --git a/components/pages/Landing/Showcase/Showcase.tsx b/components/base/Showcase/Showcase.tsx similarity index 64% rename from components/pages/Landing/Showcase/Showcase.tsx rename to components/base/Showcase/Showcase.tsx index eaa2d9b4..488dd69f 100644 --- a/components/pages/Landing/Showcase/Showcase.tsx +++ b/components/base/Showcase/Showcase.tsx @@ -3,6 +3,7 @@ import Switch from 'react-switch'; import Carousel from 'react-multi-carousel'; import 'react-multi-carousel/lib/styles.css'; import { useMediaQuery } from 'react-responsive'; +import { breakpointMap } from 'style/theme/base'; import style from './Showcase.module.scss'; @@ -14,32 +15,24 @@ import { NftType, UserType } from 'interfaces/index'; const responsive = { desktop: { - breakpoint: { max: 3000, min: 1330 }, + breakpoint: { max: 3000, min: breakpointMap.xxl }, items: 4.2, }, - desktop2: { - breakpoint: { max: 1330, min: 950 }, - items: 3.8, - }, tablet: { - breakpoint: { max: 950, min: 830 }, - items: 3.5, + breakpoint: { max: (breakpointMap.xxl - 1), min: breakpointMap.lg }, + items: 3.8, }, tablet2: { - breakpoint: { max: 830, min: 600 }, - items: 3, + breakpoint: { max: (breakpointMap.lg - 1), min: breakpointMap.md }, + items: 3.5, }, - mobile2: { - breakpoint: { max: 600, min: 530 }, + mobile: { + breakpoint: { max: (breakpointMap.md - 1), min: breakpointMap.sm }, items: 2.4, }, - mobile3: { - breakpoint: { max: 530, min: 450 }, - items: 2, - }, - mobile4: { - breakpoint: { max: 450, min: 0 }, - items: 1.5, + mobile2: { + breakpoint: { max: (breakpointMap.sm - 1), min: 0 }, + items: 1.8, }, }; @@ -54,29 +47,16 @@ const Showcase: React.FC = ({ NFTs, category, user, setUser }) => const [isFiltered, setIsFiltered] = useState(false); const [isDragging, setIsDragging] = useState(false); - const isMobile = useMediaQuery({ query: '(max-width: 720px)' }); + const isMobile = useMediaQuery({ query: `(max-width: ${breakpointMap.md - 1}px)` }); let carousel: Carousel | null = new Carousel({ responsive: {}, children: <>, }); - function returnNFTs(key: string = 'show') { - return NFTs.map((item) => ( -
    - -
    - )); - } return ( <> - { NFTs?.length > 0 && + {NFTs?.length > 0 && (
    @@ -129,28 +109,44 @@ const Showcase: React.FC = ({ NFTs, category, user, setUser }) => onTouchMove={() => setIsDragging(true)} > {isMobile ? ( - <>{returnNFTs('show')} + NFTs.map((item) => ( +
    + +
    + )) ) : ( - <> - { - carousel = el; - }} - responsive={responsive} - infinite - ssr={false} - arrows={false} - className={style.CarouselContainer} - swipeable={true} - > - {returnNFTs('Carousel')} - - - )} + { + carousel = el; + }} + responsive={responsive} + infinite + ssr={false} + arrows={false} + className={style.CarouselContainer} + swipeable={true} + > + {NFTs.map((item) => ( +
    + +
    + ))} +
    + )}
    - } + )} ); }; diff --git a/components/base/Showcase/index.tsx b/components/base/Showcase/index.tsx new file mode 100644 index 00000000..a5114cce --- /dev/null +++ b/components/base/Showcase/index.tsx @@ -0,0 +1 @@ +export { default } from "./Showcase"; \ No newline at end of file diff --git a/components/layout/Container.tsx b/components/layout/Container.tsx new file mode 100644 index 00000000..05916c34 --- /dev/null +++ b/components/layout/Container.tsx @@ -0,0 +1,24 @@ +import styled from 'styled-components' + +export const Container = styled.div` + width: 100%; + display: flex; + flex-direction: column; +`; + +export const Wrapper = styled.div` + width: 100%; + display: flex; + flex-direction: column; + margin: 0 auto; + padding: 3.2rem 2.4rem; + + ${({ theme }) => theme.mediaQueries.md} { + padding: 6.4rem 2.4rem; + } + + ${({ theme }) => theme.mediaQueries.xl} { + max-width: 112rem; + padding: 9.6rem 2.4rem; + } +`; diff --git a/components/layout/Inputs.tsx b/components/layout/Inputs.tsx new file mode 100644 index 00000000..c1208dc8 --- /dev/null +++ b/components/layout/Inputs.tsx @@ -0,0 +1,73 @@ +import styled, { css } from 'styled-components'; + +export const HiddenShell = styled.div` + position: absolute; + z-index: 1; + width: 100%; + height: 100%; + cursor: pointer; + display: none; +`; + +export const HiddenInput = styled.input` + width: 100%; + height: 100%; + outline: none; + opacity: 0; + filter: alpha(opacity=0); + -ms-filter: 'alpha(opacity=0)'; + -khtml-opacity: 0; + -moz-opacity: 0; + cursor: pointer; +`; + +export const InputShell = styled.div` + display: flex; + flex-direction: column; + width: 100%; + position: relative; +`; + +export const InputLabel = styled.h4` + display: flex; + align-items: center; + font-family: 'Airbnb Cereal App Bold'; + font-size: 2rem; + line-height: 1.3; + margin: 0; +`; + +const InputStyle = css<{ + isError?: boolean; +}>` + width: 100%; + background: #f7f7f7; + border: 0.2rem solid; + border-color: ${({ isError }) => (isError ? '#ff5555' : 'rgba(0, 0, 0, 0)')}; + border-radius: 0.8rem; + font-family: 'Airbnb Cereal App Book'; + font-size: 1.6rem; + margin-top: 1.6rem; + outline: none; + padding: 1.6rem; + + &:focus { + border: 0.2rem solid; + border-color: ${({ isError }) => (isError ? '#ff5555' : '#7417EA')}; + } + + &::placeholder { + color: #c1c1c1; + } +`; + +export const Input = styled.input<{ isError?: boolean }>` + ${InputStyle} +`; + +export const Textarea = styled.textarea` + flex: 1; + resize: none; + + ${InputStyle} +`; diff --git a/components/layout/Typography.tsx b/components/layout/Typography.tsx new file mode 100644 index 00000000..2d504a5a --- /dev/null +++ b/components/layout/Typography.tsx @@ -0,0 +1,48 @@ +import styled from 'styled-components'; + +export const Advice = styled.span` + color: #7417ea; + font-family: ${({ theme }) => theme.fonts.regular}; + font-size: 1.6rem; + line-height: 1.3; +`; + +export const Insight = styled.span` + color: #b1b1b1; + font-family: ${({ theme }) => theme.fonts.regular}; + font-size: 1.2rem; + line-height: 1.3; +`; + +export const InsightLight = styled(Insight)` + font-family: ${({ theme }) => theme.fonts.light}; + font-size: 1rem; +`; + +export const Subtitle = styled.h3` + display: flex; + align-items: end; + justify-content: center; + font-family: ${({ theme }) => theme.fonts.medium}; + font-size: 2rem; + line-height: 1.3; + margin: 0; +`; + +export const Title = styled.h2` + display: inline-block; + font-family: ${({ theme }) => theme.fonts.bold}; + font-size: 3.2rem; + line-height: 1.3; + margin: 0; + text-align: center; + + ${({ theme }) => theme.mediaQueries.md} { + display: inline-flex; + align-items: start; + gap: 2.4rem; + max-width: 64rem; + font-size: 6.4rem; + text-align: left; + } +`; diff --git a/components/layout/index.tsx b/components/layout/index.tsx new file mode 100644 index 00000000..83dee5db --- /dev/null +++ b/components/layout/index.tsx @@ -0,0 +1,3 @@ +export * from './Container'; +export * from './Inputs'; +export * from './Typography'; diff --git a/components/pages/Create/Create.module.scss b/components/pages/Create/Create.module.scss deleted file mode 100644 index 278bd442..00000000 --- a/components/pages/Create/Create.module.scss +++ /dev/null @@ -1,536 +0,0 @@ -@import "style/colors.scss"; - -.Container { - width: 100%; - display: flex; - flex-direction: column; - box-sizing: border-box; -} - -.Wrapper { - margin-left: auto; - margin-right: auto; - width: 1100px; - display: flex; - flex-direction: column; - box-sizing: border-box; - padding: 70px 24px; -} - -.Title { - font-family: "Airbnb Cereal App Bold"; - font-size: 42px; - line-height: 48px; - margin: 0px; - margin-top: 18px; - margin-bottom: 14px; -} - -.InnerContainer { - background: #ffffff; - box-shadow: 0px 0px 11px 2px rgba(0, 0, 0, 0.05); - border-radius: 27px; - width: 100%; - padding: 40px 60px; - box-sizing: border-box; - margin-top: 32px; - display: flex; - flex-direction: column; -} - -.Top { - display: flex; - justify-content: space-between; - align-items: center; - margin-bottom: 24px; -} - -.TopInf { - font-size: 18px; - line-height: 26px; - font-family: "Airbnb Cereal App Medium"; - display: flex; - justify-content: center; - align-items: center; -} - -.EyeSVG { - width: 24px; - margin-right: 10px; - fill: black; -} - -.Data { - display: flex; - width: 100%; -} - -.Left { - display: flex; - flex-direction: column; - box-sizing: border-box; - margin-right: 70px; - position: relative; -} - -.Right { - display: flex; - flex-direction: column; - width: 350px; - box-sizing: border-box; -} - -.Subtitle { - font-family: "Airbnb Cereal App Bold"; - font-size: 16px; - line-height: 20px; - margin: 0px; - margin-bottom: 8px; -} -.InputShell { - display: flex; - flex-direction: column; - width: 100%; - margin-bottom: 24px; -} - -.Input { - width: 100%; - background: #ffffff; - border: 1px solid #e1e1e1; - font-family: "Airbnb Cereal App Book"; - box-sizing: border-box; - border-radius: 7px; - outline: none; - width: 100%; - padding: 14px 16px; - font-size: 15px; -} - -.Input::placeholder { - color: #c1c1c1; -} - -.InputError { - border: 1px solid #ff5555; -} - -.Textarea { - font-size: 15px; - width: 100%; - background: #ffffff; - border: 1px solid #e1e1e1; - resize: none; - border-radius: 7px; - height: 100px; - font-family: "Airbnb Cereal App Book"; - padding: 12px; - outline: none; - box-sizing: border-box; -} - -.Textarea::placeholder { - color: #c1c1c1; -} - -.Insight { - font-family: "Airbnb Cereal App Book"; - font-size: 13px; - line-height: 15px; - margin-top: 8px; - margin-left: 8px; - color: #c1c1c1; -} - -.NFTPreview { - width: 278px; - height: 458px; - border-radius: 27px; - display: flex; - box-sizing: border-box; - position: relative; - box-shadow: 0px 4px 4px rgba(0, 0, 0, 0.25); - cursor: pointer; -} - -.NFTSPreview { - width: 190px; - height: 320px; - border-radius: 27px; - display: flex; - box-sizing: border-box; - position: relative; - box-shadow: 0px 4px 4px rgba(0, 0, 0, 0.25); - position: absolute; - z-index: 20; - bottom: -20px; - left: -20px; - background-color: white; - cursor: pointer; -} - -.Hidden { - position: absolute; - z-index: 1; - width: 100%; - height: 100%; - object-fit: cover; - border-radius: 27px; - visibility: hidden; -} - -.IMGBackground { - position: absolute; - z-index: 1; - width: 100%; - height: 100%; - object-fit: cover; - border-radius: 27px; -} - -.HiddenShell { - position: absolute; - z-index: 1; - width: 100%; - height: 100%; - cursor: pointer; - display: none; -} -.HiddenInput { - width: 100%; - height: 100%; - outline: none; - opacity: 0; - filter: alpha(opacity=0); - -ms-filter: "alpha(opacity=0)"; - -khtml-opacity: 0; - -moz-opacity: 0; - cursor: pointer; -} - -.NFTPreviewBorder { - border: 1px dashed $primary; - box-shadow: none; -} - -.OPTN { - width: 100%; - height: 100%; - position: absolute; - border-radius: 27px; -} - -.OPTNCTNR { - position: relative; - width: 100%; - height: 100%; - border-radius: 27px; -} -.WaterMarkSVG { - width: 160px; - position: absolute; - z-index: 10; - bottom: 20px; - left: 20px; -} - -.Blur { - width: 100%; - height: 100%; - position: absolute; - z-index: 3; - backdrop-filter: blur(8px); - -webkit-backdrop-filter: blur(8px); - border-radius: 27px; -} - -.NFTNull { - display: flex; - flex-direction: column; - padding: 80px 24px; - width: 100%; - align-items: center; - justify-content: center; - cursor: pointer; -} - -.NFTSNull { - display: flex; - flex-direction: column; - padding: 16px 18px; - width: 100%; - align-items: center; - justify-content: center; - cursor: pointer; - - .Label { - align-self: center; - margin-top: -40px; - margin-bottom: 30px; - } -} - -.NFTSTips { - color: $primary; - font-family: "Airbnb Cereal App Medium"; - font-size: 13px; - line-height: 15px; - margin: 18px 0px; - text-align: center; - align-self: center; -} - -.NFTSTips2 { - color: $primary; - font-family: "Airbnb Cereal App Light"; - font-size: 10px; - line-height: 14px; - color: #686464; - text-align: center; -} - -.UploadSVG2 { - width: 40px; -} - -.UploadSVG { - width: 80px; -} - -.InsightMedium { - font-family: "Airbnb Cereal App Light"; - font-size: 15px; - line-height: 24px; - text-align: center; - color: #686464; - margin: 18px 0px; - max-width: 150px; -} - -.InsightLight { - font-family: "Airbnb Cereal App Light"; - font-size: 15px; - line-height: 24px; - color: #b1b1b1; - text-align: center; - max-width: 200px; -} - -.SelectShell { - display: flex; - align-items: center; - margin-top: 32px; -} - -.Link { - font-size: 13px; - font-family: "Airbnb Cereal App Medium"; - color: #686464; - margin-left: 24px; -} - -.Create { - display: flex; - justify-content: center; - align-items: center; - background: $primary; - color: white; - padding: 14px 40px; - border-radius: 40px; - cursor: pointer; - color: white; - font-family: "Airbnb Cereal App Bold"; - font-size: 13px; - align-self: center; - margin-top: 56px; - transition: all 0.6s cubic-bezier(0.25, 1, 0.5, 1); - z-index: 1; - - &:hover { - color: white; - background-color: black; - } -} - -.CreateDisabled { - pointer-events: none; - opacity: 0.4; -} - -.SelectContainer { - position: relative; - box-sizing: border-box; -} - -.arrowbtmselect { - position: absolute; - right: 20px; - width: 14px; - top: 16px; - transform: rotate(-90deg); - transition: all 0.6s cubic-bezier(0.25, 1, 0.5, 1); -} - -.arrowbtm { - position: absolute; - right: 20px; - top: 16px; - width: 14px; - transform: rotate(90deg); - transition: all 0.6s cubic-bezier(0.25, 1, 0.5, 1); -} - -.Select { - border: 2px solid $primary; - border-radius: 25px; - padding: 12px 24px; - width: 230px; - font-family: "Airbnb Cereal App Bold"; - font-size: 13px; - outline: none; - box-sizing: border-box; - cursor: pointer; -} - -.SelectDisabled { - border: 2px solid; - border-radius: 25px; - padding: 12px 24px; - width: 230px; - font-family: "Airbnb Cereal App Bold"; - font-size: 13px; - outline: none; - box-sizing: border-box; - cursor: disabled; - opacity: 0.5; -} - -.SelectOptn { - display: flex; - flex-direction: column; - position: absolute; - z-index: 3; - top: 55px; - background-color: white; -} - -.SelectItem { - width: 200px; - color: #686464; - border-radius: 25px; - padding: 12px 24px; - padding: 8px 16px; - cursor: pointer; - font-family: "Airbnb Cereal App Medium"; - transition: all 0.2s cubic-bezier(0.25, 1, 0.5, 1); - background-color: white; - - &:hover { - color: white; - background-color: $primary; - } -} - -.Hide { - display: none; -} - -@media (max-width: 1300px) { - .Wrapper { - width: 950px; - padding: 60px 24px; - } -} - -@media (max-width: 950px) { - .Wrapper { - width: 100%; - padding: 60px 24px; - } -} - -@media (max-width: 720px) { - .Data { - width: 100%; - flex-direction: column; - justify-content: center; - align-items: center; - } - .InnerContainer { - width: 100%; - padding: 24px 24px; - } - - .Left { - margin-right: 0px; - margin-top: 20px; - margin-bottom: 40px; - } - - .Right { - display: flex; - flex-direction: column; - width: 100%; - box-sizing: border-box; - } - - .Label { - font-size: 11px; - } - - .Title { - font-size: 34px; - line-height: 38px; - margin-top: 10px; - margin-bottom: 14px; - } - - .Select { - padding: 8px 16px; - width: 150px; - font-size: 11px; - } - - .arrowbtmselect { - right: 14px; - width: 9px; - top: 14px; - } - - .arrowbtm { - right: 14px; - top: 14px; - width: 9px; - } - - .SelectOptn { - top: 45px; - } - - .SelectItem { - width: 120px; - font-size: 11px; - padding: 8px 16px; - } - - .Subtitle { - font-size: 14px; - } - .NFTPreview { - width: 250px; - height: 430px; - } - - .NFTSPreview { - width: 140px; - height: 240px; - } - - .WaterMarkSVG { - width: 140px; - } - - .UploadSVG2 { - width: 30px; - } -} diff --git a/components/pages/Create/Create.tsx b/components/pages/Create/Create.tsx index 8fead40b..7dcb9c40 100644 --- a/components/pages/Create/Create.tsx +++ b/components/pages/Create/Create.tsx @@ -1,397 +1,433 @@ import React, { useEffect, useState } from 'react'; -import Link from 'next/link'; -import style from './Create.module.scss'; +import styled from 'styled-components'; import Footer from 'components/base/Footer'; import FloatingHeader from 'components/base/FloatingHeader'; -import ArrowBottom from 'components/assets/arrowBottom'; -import Upload from 'components/assets/upload'; -import WhiteWaterMark from 'components/assets/WhiteWaterMark'; -import Eye from 'components/assets/eye'; - -import { UserType } from 'interfaces/index'; +import { + Advice, + Container, + Input, + InputLabel, + InputShell, + Insight, + Textarea, + Title, + Wrapper, +} from 'components/layout'; +import NftPreview from 'components/base/NftPreview'; +import { + NFT_EFFECT_BLUR, + NFT_EFFECT_DEFAULT, + NFT_EFFECT_PROTECT, + NFT_EFFECT_SECRET, + CategoryType, + NftEffectType, + UserType, +} from 'interfaces'; +import Autocomplete from 'components/ui/Autocomplete'; +import Button from 'components/ui/Button'; +import Tooltip from 'components/ui/Tooltip'; import { NFTProps } from 'pages/create'; +import { canAddToSeries } from 'actions/nft'; +import { processFile } from 'utils/imageProcessing/image'; + +const DEFAULT_BLUR_VALUE = 5; + +type QRDataType = { + walletId: string; + quantity: number; +}; export interface CreateProps { + categoriesOptions: CategoryType[]; + NFTData: NFTProps; + originalNFT: File | null; + QRData: QRDataType; user: UserType; + setError: (err: string) => void; setModalExpand: (b: boolean) => void; - setNotAvailable: (b: boolean) => void; setModalCreate: (b: boolean) => void; - NFTData: NFTProps; setNFTData: (o: NFTProps) => void; - NFT: File | null; - setNFT: (f: File | null) => void; - secretNFT: File | null; - setSecretNFT: (f: File | null) => void; - select: string; - setSelect: (s: string) => void; - processFile: () => Promise; - setError: (s: string) => void; - setProcessed: (b: boolean) => void; + setOriginalNFT: (f: File | null) => void; + setOutput: (s: string[]) => void; + setPreviewNFT: (f: File | null) => void; + setQRData: (data: QRDataType) => void; } -const Create: React.FC = ({ - setModalExpand, - setNotAvailable, - setModalCreate, - NFT, - setNFT, - secretNFT, - setSecretNFT, +const Create = ({ + categoriesOptions, NFTData: initalValue, - setNFTData: setNftDataToParent, + originalNFT, + QRData, user, - select, - setSelect, - processFile, setError, - setProcessed, -}) => { - const [exp, setExp] = useState(false); + setModalExpand, + setModalCreate, + setNFTData: setNftDataToParent, + setOriginalNFT, + setOutput, + setPreviewNFT, + setQRData, +}: CreateProps) => { + const [blurValue, setBlurValue] = useState(DEFAULT_BLUR_VALUE); + const [coverNFT, setCoverNFT] = useState(null); // Cover NFT used for secret effect + const [effect, setEffect] = useState(NFT_EFFECT_DEFAULT); + const [isRN, setRN] = useState(false); const [nftData, setNFTData] = useState(initalValue); - const { name, description, quantity } = nftData; - const [isRN, setIsRN] = useState(false) - const [acceptedFileTypes, setAcceptedFileTypes] = useState([".jpg", ".jpeg", ".png", ".gif", ".mp4", ".mov"]) + const [canAddToSeriesValue, setCanAddToSeriesValue] = useState(true); + const [isLoading, setIsLoading] = useState(false); - useEffect(() => { - setIsRN(window.isRNApp); - }, []); + const { categories, description, name, quantity, seriesId } = nftData; useEffect(() => { - if (isRN){ - setAcceptedFileTypes([".jpg", ".jpeg", ".png", ".gif"]) + setIsLoading(true); + const timer = setTimeout(() => { + if (!seriesId || seriesId === '') { + setCanAddToSeriesValue(true); + setIsLoading(false); + } else { + checkAddToSerie(); + } + }, 1000); + return () => clearTimeout(timer); + }, [seriesId, user]); + + const checkAddToSerie = async () => { + try { + if (user) { + const canAdd = await canAddToSeries(seriesId, user.walletId); + setCanAddToSeriesValue(canAdd); + } else { + setCanAddToSeriesValue(true); + } + setIsLoading(false); + } catch (err) { + setCanAddToSeriesValue(false); + setIsLoading(false); + console.log(err); } - }, [isRN]) - + }; + const validateQuantity = (value: number, limit: number) => { - return value && value > 0 && value <= limit; + return value > 0 && value <= limit; }; const isDataValid = name && description && validateQuantity(quantity, 10) && - secretNFT && - (select !== 'Secret' || NFT) && - select !== 'Select NFT Option'; + originalNFT && + (effect !== NFT_EFFECT_SECRET || coverNFT) && + canAddToSeriesValue && + !isLoading; - function onChange( + const handleChange = ( e: | React.ChangeEvent | React.ChangeEvent - ) { + ) => { const nextNftData = { ...nftData, [e.target.name]: e.target.value }; setNFTData(nextNftData); setNftDataToParent(nextNftData); - } - - function returnType(NFTarg: File) { - if (NFTarg!.type.substr(0, 5) === 'image') { - return ( - img - ); - } else if (NFTarg!.type.substr(0, 5) === 'video') { - return ( - - ); - } - } + }; - const updateFile = ( - event: React.ChangeEvent, - setFunction: (f: File | null) => void + const handleCategoryChipDelete = ( + list: CategoryType[], + id: CategoryType['_id'] ) => { - const { target } = event; - let file = null; - let isError = false - if (!(target && target.files && target.files[0])) { - setFunction(file); - setSelect('Select NFT Option'); - return; - } - if (!isError && isRN && target.files[0]!.type.substr(0, 5) === 'video'){ - setError("You can't select video type on mobile DApp yet."); - isError = true - } - if (!isError && !(target.files[0]!.type.substr(0, 5) === 'video' || target.files[0]!.type.substr(0, 5) === 'image')){ - setError(`You can't select files different from ${!isRN ? "videos or " : ""}images.`); - isError = true - } - if (!isError && target.files[0].size > 31000000) { - setError('Max file size is 30mb.'); - isError = true - } - if ( - (target.files[0]!.type.substr(0, 5) === 'video' || target.files[0]!.type === 'image/gif') && - (select === 'Blur' || select === 'Protect') - ) { - setSelect('Select NFT Option'); - } - if (!isError){ - file = target.files[0]; - }else{ - setModalCreate(true); - setSelect('Select NFT Option'); - } - setFunction(file); + const nextNftData = { + ...nftData, + categories: list.filter((item) => item._id !== id), + }; + setNFTData(nextNftData); + setNftDataToParent(nextNftData); }; - function uploadFiles() { - if ( - !name || - !description || - !quantity || - quantity > 10 || - secretNFT === null || - (select === 'Secret' && NFT === null) || - select === 'Select NFT Option' - ) { - setError('Please fill the form entirely.'); + const handleCategoryOptionClick = (option: CategoryType) => { + const nextNftData = { + ...nftData, + categories: categories.concat(option), + }; + setNFTData(nextNftData); + setNftDataToParent(nextNftData); + }; + + const initMintingNFT = async () => { + try { + if (!user) throw new Error('Please login to create an NFT.'); setModalCreate(true); - return false; + + if (originalNFT !== null) { + if (effect === NFT_EFFECT_BLUR || effect === NFT_EFFECT_PROTECT) { + const processedNFT = await processFile( + originalNFT, + effect, + setError, + blurValue + ); + if (processedNFT === undefined || processedNFT === null) + throw new Error( + `Elements are undefined after file processing using ${effect} effect.` + ); + setPreviewNFT(processedNFT); + } else if (effect === NFT_EFFECT_SECRET) { + if (coverNFT === null) + throw new Error('Please add a cover NFT using a secret effect.'); + setPreviewNFT(coverNFT); + } + } + + setQRData({ + ...QRData, + quantity, + }); + setOutput([quantity.toString()]); + } catch (err) { + console.error(err); + if (err instanceof Error) { + setError(err.message); + } else { + setError(err as string); + } } - if ( - secretNFT!.type.substr(0, 5) === 'image' && - select !== 'None' && - select !== 'Secret' - ) { - processFile(); - } else { - setProcessed(true); + }; + + const uploadFiles = async () => { + setOutput([]); + setError(''); + + try { + initMintingNFT(); + } catch (err) { + console.error(err); + if (err instanceof Error) { + setError(err.message); + } else { + setError(err as string); + } } - setModalCreate(true); - } + }; + + useEffect(() => { + setCoverNFT(null); + }, [originalNFT]); + + useEffect(() => { + setRN(window.isRNApp); + }, []); - function checkType() { - if ( - secretNFT!.type.substr(0, 5) === 'video' || - secretNFT!.type === 'image/gif' - ) - return false; - else return true; - } return ( -
    -
    -

    Create NFT

    -
    -
    - - - NFT Preview - -
    -
    -
    -