diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 00000000..7a73a41b --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,2 @@ +{ +} \ No newline at end of file diff --git a/uplink-client/.yarn/install-state.gz b/uplink-client/.yarn/install-state.gz index d7e95b87..16a98b7d 100644 Binary files a/uplink-client/.yarn/install-state.gz and b/uplink-client/.yarn/install-state.gz differ diff --git a/uplink-client/next.config.js b/uplink-client/next.config.js index 1c26f280..a78c99c4 100644 --- a/uplink-client/next.config.js +++ b/uplink-client/next.config.js @@ -9,11 +9,6 @@ const nextConfig = { contentDispositionType: 'attachment', contentSecurityPolicy: "default-src 'self'; script-src 'none'; sandbox;", remotePatterns: [ - { - protocol: 'https', - hostname: 'res.cloudinary.com', - port: '', - }, { protocol: 'https', hostname: 'calabara.mypinata.cloud', diff --git a/uplink-client/package.json b/uplink-client/package.json index d8ff5a5c..00df4bc8 100644 --- a/uplink-client/package.json +++ b/uplink-client/package.json @@ -62,6 +62,7 @@ "next-mdx-remote": "^5.0.0", "node-gyp": "^10.0.1", "popmotion": "^11.0.5", + "probe-image-size": "^7.2.3", "react": "^18", "react-day-picker": "^8.9.1", "react-dom": "^18", @@ -103,6 +104,7 @@ "@storybook/testing-library": "^0.2.1", "@tailwindcss/typography": "^0.5.14", "@types/node": "^20", + "@types/probe-image-size": "^7", "@types/react": "^18", "@types/react-dom": "^18", "@types/react-lazyload": "^3", diff --git a/uplink-client/src/app/(home)/feature.tsx b/uplink-client/src/app/(home)/feature.tsx index 7c20fa45..f1f589fe 100644 --- a/uplink-client/src/app/(home)/feature.tsx +++ b/uplink-client/src/app/(home)/feature.tsx @@ -13,13 +13,6 @@ import { BiCategoryAlt, BiTime } from "react-icons/bi"; import { LuCoins, LuSettings2, LuVote } from "react-icons/lu"; import { HiOutlineDocument } from "react-icons/hi2"; import { HiOutlineLockClosed } from "react-icons/hi2"; -import { RenderMintMedia } from '@/ui/Token/MintUtils'; -import { ChainLabel } from "@/ui/ContestLabels/ContestLabels"; -import { PiInfinity } from "react-icons/pi"; -import { TbLoader2 } from "react-icons/tb"; -import { LuMinusSquare, LuPlusSquare } from "react-icons/lu"; -import UplinkImage from "@/lib/UplinkImage"; -import { ImageWrapper } from "@/ui/Submission/MediaWrapper" import { useInView } from "react-intersection-observer"; import Autoplay from "embla-carousel-autoplay" import { Carousel } from '@/ui/DesignKit/Carousel'; @@ -95,223 +88,6 @@ export const FeatureCard = ({ children }: { children: React.ReactNode }) => { ); } -// export const ContestFeatureCard = () => { - - -// return ( -//
-//

-// Contests to reward contributors, onboard new creatives, and unlock superusers. -//

- -//
-//
-//
-//
-//
-//
-//
-//

-// TNS Anniversary art contest -//

-//
-//
-// -//
-//

thenounsquare

-// -//

-// submitting -//

-//
-//
-//

🥳 TNS Season 3 Art Contest Bonanza

-//

We Love Your Art!

-//

The passing of Prop 434 means 6 more months of TNS Art Contests!

-//

Celebrate with us by submitting something Nounish.

-//
-//
-//
-// -// -// -//
-//
-//
-//
-//
- -//
-//
-//
-// { }} -// submission={ -// { -// "id": "7733", -// "contestId": "263", -// "totalVotes": "73", -// "rank": "1", -// "created": "2023-11-23T08:33:53.022Z", -// "type": "twitter", -// "url": "https://uplink.mypinata.cloud/ipfs/QmZJV7cojpmPEXDK45txvaSqfSzcqj4X5oSdfvxYhkDJam", -// "version": "uplink-v1", -// "edition": null, -// /*@ts-ignore*/ -// "author": { -// "id": "554", -// "address": "0x72D4e991040e3B65FdDbE5f340f65Cf03C506e6F", -// "userName": null, -// "displayName": null, -// "profileAvatar": null -// }, -// "data": { -// "title": "TNS S3", -// "thread": [ -// { -// "text": "Season 3 with TNS lezgoooo!!! 🥳", -// "previewAsset": "https://uplink.mypinata.cloud/ipfs/QmRDw9wkBDtSbHYFdRjruWgdMD8uVhUqx5wo3JnA49oQU6", -// "assetType": "image/png", -// "assetSize": 4505009 -// } -// ], -// "type": "image" -// } -// } -// } -// /> -// { }} -// submission={ -// { -// "id": "7714", -// "contestId": "263", -// "totalVotes": "49", -// "rank": "2", -// "created": "2023-11-22T03:27:50.542Z", -// "type": "twitter", -// "url": "https://uplink.mypinata.cloud/ipfs/QmWxJ5nJd14uEnHVvRpmmzLPHbaHukN1pm9TBLWHrQ41SU", -// "version": "uplink-v1", -// "edition": null, -// /*@ts-ignore*/ -// "author": { -// "id": "147", -// "address": "0xe851ED2a5816dC03887dFD2713dcD4217425DFe5", -// "userName": null, -// "displayName": null, -// "profileAvatar": null -// }, -// "data": { -// "title": "N:O:U:N:2:D", -// "thread": [ -// { -// "text": "Finished my @thenounsquare\n Art contest entry! \n\nFrame by frame 2D animation done live on twitch.\n\nThanks for tuning in and sharing.", -// "previewAsset": "https://uplink.mypinata.cloud/ipfs/QmNgPbgipKyh5Cu4igtuXYkF8rszunNo17upJQXUPsWEnH", -// "assetType": "image/gif", -// "assetSize": 1854450 -// } -// ], -// "type": "image" -// } -// } - -// } -// /> -// { }} -// submission={ -// { -// "id": "7728", -// "contestId": "263", -// "totalVotes": "47", -// "rank": "3", -// "created": "2023-11-22T22:21:51.873Z", -// "type": "twitter", -// "url": "https://uplink.mypinata.cloud/ipfs/QmdH1gmW3fvzj3Zj1RTWrFzf6CoPj2SFQVyyDYVYf9KLae", -// "version": "uplink-v1", -// "edition": null, -// /*@ts-ignore*/ -// "author": { -// "id": "552", -// "address": "0x7ce294D2Bcb2dE76a15fcf7055b0B14253Dd3B33", -// "userName": null, -// "displayName": null, -// "profileAvatar": null -// }, -// "data": { -// "title": "Congratulations!", -// "thread": [ -// { -// "text": " @thenounsquare\nBen o’ Clock at your desktop celebrating the proposal approved! 🎉 💛 ⌐◨-◨ \n", -// "previewAsset": "https://uplink.mypinata.cloud/ipfs/QmaTT5w1qMH1fui6kTgJHZYwPjZdmLzunye2aVkfkEgjBr", -// "assetType": "image/jpeg", -// "assetSize": 845756 -// } -// ], -// "type": "image" -// } -// } -// } -// /> -// { }} -// submission={ -// { -// "id": "7710", -// "contestId": "263", -// "totalVotes": "38", -// "rank": "4", -// "created": "2023-11-21T00:39:13.119Z", -// "type": "twitter", -// "url": "https://uplink.mypinata.cloud/ipfs/QmX4s19s1Xr6tzG9nyVyiR4JrUUrWBHA6Z8hWs6Mz1zTs3", -// "version": "uplink-v1", -// "edition": null, -// /*@ts-ignore*/ -// "author": { -// "id": "235", -// "address": "0xA09AA75da763D4aEB692672897C057786Cdd258B", -// "userName": null, -// "displayName": null, -// "profileAvatar": null -// }, -// "data": { -// "title": "gm☕☀ awesome fellas!", -// "thread": [ -// { -// "text": "something NOUNISH! ⌐◨-◨", -// "previewAsset": "https://uplink.mypinata.cloud/ipfs/QmbxaKBrDz7cXvsZmo85giQr1upZKk6dqC1JJpVJqDvNFw", -// "videoAsset": "https://uplink.mypinata.cloud/ipfs/QmW2eNthEddhEHzuGfy1sgNVocYTgcq1Bjy5rUMGju5msW", -// "assetType": "video/mp4", -// "assetSize": 4054436 -// } -// ], -// "type": "video" -// } -// } -// } -// /> -//
-//
-//
-//
-//
-//
-//
-// ) -// } - - export const ContestSubCardA = () => { const steps = [ @@ -420,201 +196,3 @@ export const ContestSubCardB = ({ children }: { children: React.ReactNode }) => ); } - -// export const MintboardCard = () => { -// return ( -//
-// -//
-//

Mint Drop

-//
-//
-//
-// -//
-//
-//
-//

Based Management

-//
-//
-// -// -// -//
-// LGHT -//
-//
-//
-//
-//
-//

Network

-//
-//

Base

-// -//
- -//
-//
-//

Minting

-//
-//

Now

-// -// -// -// -//
-//
-//
-//

Until

-//

Forever

-//
-//
-//

Price

-//

Free

-//
-//
-//

Minted

-//
-//

100

-//

/

-// -//
-//
-//
-//
-//
- -//
-//
-// -//
-// e.currentTarget.blur()} -// value={1} -// className="input w-[1px] min-w-full rounded-none focus:ring-0 focus:border-none focus:outline-none text-center" -// /> -//
-// -//
-// -// < div className="relative w-full" > -// -//
-//
-//
-//
-//
-//
-// -//

-// The Mintboard.
A collective canvas for onchain creation. -//

-//
-// ) -// } - -// export const MintboardSubCardA = () => { -// return ( -// -//
-//

-// Brain to onchain in 2 clicks. -//

-// -//
-//
-//

Post to the Based Management mintboard and earn 0.000333 ETH for every mint!

-//
-//
-//
-//
-// -// -//
-//
-//
-// -//
-//
-//
-//
-// ) -// } - -// export const MintboardSubCardB = () => { -// return ( -//
-// -//
-//

-// Split protocol rewards with your users on every mint. -//

-//
- -// logo - -//
-//
-//
-//
Creator
-//
-//
-//
-//
Referrer
-//
-//
-//
-//
Treasury
-//
-//
- -//
-//
-// -//
-//
-//

Smooth as butter, wow.

-//
-//
-//

{`I've minted on mint.fun, Source, Zora & OpenSea - this was the best experience yet.`}

-//
-//
-//

Congrats on easiest minting experience on the planet. So minty.

-//
-//
-//
-// ) -// } - - diff --git a/uplink-client/src/app/(home)/page.tsx b/uplink-client/src/app/(home)/page.tsx index 71f38b1c..e92fb54e 100644 --- a/uplink-client/src/app/(home)/page.tsx +++ b/uplink-client/src/app/(home)/page.tsx @@ -4,22 +4,20 @@ import Link from "next/link"; import ArtistPfp from "@/../public/pumey_pfp.jpg"; import ArtistSubmission from "@/../public/vinnie_noggles.png"; import landingBg from "@/../public/landing-bg.svg"; -import UplinkImage from "@/lib/UplinkImage"; import { ContestSubCardA, FeatureCard } from "./feature"; import { Button } from "@/ui/DesignKit/Button"; import { FaCircle, FaPalette } from "react-icons/fa"; import { LuCoins } from "react-icons/lu"; import { PiInfinity } from "react-icons/pi"; -import { ChainLabel } from "@/ui/ContestLabels/ContestLabels"; +import { ChainLabel } from "@/ui/ChainLabel/ChainLabel"; import { RenderMintMedia } from "@/ui/Token/MintUtils"; -import { ImageWrapper } from "@/ui/Submission/MediaWrapper"; +import { ImageWrapper } from "@/app/(legacy)/contest/components/MediaWrapper"; import { MdAccessibility, MdDashboardCustomize, MdGroups } from "react-icons/md"; +import OptimizedImage from "@/lib/OptimizedImage"; export const dynamic = 'force-static'; export const runtime = 'nodejs'; - - const BannerSection = () => { return (
@@ -109,14 +107,13 @@ const BannerSection = () => {
-

@@ -124,13 +121,12 @@ const BannerSection = () => {

-
@@ -207,7 +203,7 @@ export default async function Page() {
- +
LGHT @@ -298,7 +294,7 @@ export default async function Page() {
-
-
- { + const [isModalOpen, setIsModalOpen] = useState(false); + return ( +
+ {data.length > 3 && ( + setIsModalOpen(true)} + > + {label} + + )} + setIsModalOpen(false)} className="w-full max-w-[500px]"> + {children} + +
+ ); +}; diff --git a/uplink-client/src/app/(legacy)/contest/[id]/layout.tsx b/uplink-client/src/app/(legacy)/contest/[id]/layout.tsx new file mode 100644 index 00000000..a8edf68b --- /dev/null +++ b/uplink-client/src/app/(legacy)/contest/[id]/layout.tsx @@ -0,0 +1,43 @@ +import fetchLegacyContest from "@/lib/fetch/fetchLegacyContest"; +import { LegacyContestWithPrompt } from "@/types/contest"; +import { Metadata } from "next"; + + +export async function generateMetadata({ + params, +}: { + params: { id: string }; +}): Promise { + const contest: LegacyContestWithPrompt = await fetchLegacyContest(params.id).then(async data => { + const promptData = await fetch(data.promptUrl).then(res => res.json()); + return { ...data, promptData }; + }); + + return { + title: `${contest.promptData.title} on Uplink`, + description: `${contest.promptData.title} on Uplink`, + openGraph: { + title: `${contest.promptData.title}`, + description: `${contest.promptData.title} on Uplink`, + images: [ + { + url: `api/contest/${params.id}/contest_metadata`, + width: 600, + height: 600, + alt: `contest logo`, + }, + ], + locale: "en_US", + type: "website", + }, + }; +} + + +export default function Layout({ children }: { children: React.ReactNode }) { + return ( +
+ {children} +
+ ) +} \ No newline at end of file diff --git a/uplink-client/src/app/(legacy)/contest/[id]/page.tsx b/uplink-client/src/app/(legacy)/contest/[id]/page.tsx new file mode 100644 index 00000000..065077d8 --- /dev/null +++ b/uplink-client/src/app/(legacy)/contest/[id]/page.tsx @@ -0,0 +1,123 @@ +import { + ContestHeadingSkeleton, +} from "@/ui/ContestHeading/ContestHeading"; +import { Suspense } from "react"; +import fetchLegacyContest from "@/lib/fetch/fetchLegacyContest"; +import Link from "next/link"; +import OptimizedImage from "@/lib/OptimizedImage"; +import { ChannelStateLabel } from "@/ui/ChannelSidebar/SidebarUtils"; +import ExpandableTextSection from "@/ui/ExpandableTextSection/ExpandableTextSection"; +import ParseBlocks from "@/lib/blockParser"; +import { ImageWrapper } from "@/app/(legacy)/contest/components/MediaWrapper"; +import { LiveSubmissionDisplay, SubmissionDisplaySkeleton } from "@/app/(legacy)/contest/components/SubmissionDisplay"; +import { DetailsSkeleton } from "@/ui/ChannelSidebar/ContestDetailsV2"; +import ContestDetails from "@/app/(legacy)/contest/components/ContestDetails"; + +const SubmissionWrapper = async ({ + contestId, +}: { + contestId: string; +}) => { + const contest = await fetchLegacyContest(contestId); + + return + +}; + + +const LegacyContestHeading = async ({ contestId }: { contestId: string }) => { + + + const contest = await fetchLegacyContest(contestId).then(async (res) => { + const promptData = await fetch(res.promptUrl).then((res) => res.json()); + return { ...res, prompt: promptData }; + }) + + const { prompt, space, metadata } = contest; + + return ( +
+
+
+
+

+ {prompt.title} +

+
+ + + + + {space.displayName} + + + +
+
+ + + +
+
+
+ {prompt.coverUrl && ( + + + + )} +
+
+
+
+
+ ); +}; + + + + +export default async function Page({ params }: { params: { id: string } }) { + + const contestId = params.id; + + return ( +
+
+ }> + + + + }> + + +
+
+
+ }> + + +
+
+
+ ); +} + diff --git a/uplink-client/src/ui/Submission/CardSubmission.tsx b/uplink-client/src/app/(legacy)/contest/components/CardSubmission.tsx similarity index 97% rename from uplink-client/src/ui/Submission/CardSubmission.tsx rename to uplink-client/src/app/(legacy)/contest/components/CardSubmission.tsx index 4fe0e5d8..bb89e473 100644 --- a/uplink-client/src/ui/Submission/CardSubmission.tsx +++ b/uplink-client/src/app/(legacy)/contest/components/CardSubmission.tsx @@ -5,14 +5,14 @@ import { useInView } from "react-intersection-observer"; import React, { useEffect, useState } from "react"; import { isMobile } from "@/lib/isMobile"; import dynamic from "next/dynamic"; -import { UsernameDisplay, UserAvatar } from "../AddressDisplay/AddressDisplay"; +import { UsernameDisplay, UserAvatar } from "../../../../ui/AddressDisplay/AddressDisplay"; import { ImageWrapper } from "./MediaWrapper"; import { TbCrown } from "react-icons/tb"; import { ParseThread } from "@/lib/threadParser"; import { RenderInteractiveVideoWithLoader } from "@/ui/VideoPlayer"; import { Decimal } from "decimal.js"; import formatDecimal from "@/lib/formatDecimal"; -import UplinkImage from "@/lib/UplinkImage" +import OptimizedImage from "@/lib/OptimizedImage" const ParseBlocks = dynamic(() => import("@/lib/blockParser"), { ssr: false, @@ -103,7 +103,7 @@ const RenderVideoSubmission = ({ const RenderImageSubmission = ({ submission }) => { return ( - { if (subRewards.length === 0) return []; let rewardsObj: { @@ -107,7 +105,7 @@ export const DetailSectionWrapper = ({ ); }; -const SubmitterRestrictionsSection = ({ submitterRestrictions }: { submitterRestrictions: FetchSingleContestResponse['submitterRestrictions'] }) => { +const SubmitterRestrictionsSection = ({ submitterRestrictions }: { submitterRestrictions: LegacyContest['submitterRestrictions'] }) => { return ( { const normalizedRewards: { [rank: number]: SubmitterTokenRewardOption[]; @@ -260,7 +258,7 @@ const SubmitterRewardsSection = ({ const VoterRewardsSection = ({ voterRewards, }: { - voterRewards: FetchSingleContestResponse["voterRewards"]; + voterRewards: LegacyContest["voterRewards"]; }) => { if (voterRewards.length > 0) { return ( @@ -323,8 +321,8 @@ const VoterRewardsSection = ({ const VotingPolicySection = ({ votingPolicy, }: { - votingPolicy: FetchSingleContestResponse["votingPolicy"]; - chainId: FetchSingleContestResponse["chainId"]; + votingPolicy: LegacyContest["votingPolicy"]; + chainId: LegacyContest["chainId"]; }) => { return ( { - const contestData = await fetchContest(contestId).then(async (res) => { + const contestData = await fetchLegacyContest(contestId).then(async (res) => { const promptData = await fetch(res.promptUrl).then((res) => res.json()); return { ...res, promptData }; }); @@ -462,12 +460,6 @@ const ContestDetails = async ({
-
); }; diff --git a/uplink-client/src/ui/Submission/ExpandedSubmission.tsx b/uplink-client/src/app/(legacy)/contest/components/ExpandedSubmission.tsx similarity index 96% rename from uplink-client/src/ui/Submission/ExpandedSubmission.tsx rename to uplink-client/src/app/(legacy)/contest/components/ExpandedSubmission.tsx index 06cef5f2..25483304 100644 --- a/uplink-client/src/ui/Submission/ExpandedSubmission.tsx +++ b/uplink-client/src/app/(legacy)/contest/components/ExpandedSubmission.tsx @@ -1,6 +1,6 @@ // Render the submission in a large format. This is used for modals and the submission page. import type { Submission } from "@/types/submission"; -import UplinkImage from "@/lib/UplinkImage" +import OptimizedImage from "@/lib/OptimizedImage" const ParseBlocks = dynamic(() => import("@/lib/blockParser"), { ssr: false, loading: () => ( @@ -16,7 +16,7 @@ const ParseBlocks = dynamic(() => import("@/lib/blockParser"), { }); import { ParseThread } from "@/lib/threadParser"; import { ImageWrapper } from "./MediaWrapper"; -import { UsernameDisplay, UserAvatar } from "../AddressDisplay/AddressDisplay"; +import { UsernameDisplay, UserAvatar } from "../../../../ui/AddressDisplay/AddressDisplay"; import dynamic from "next/dynamic"; import { RenderStandardVideoWithLoader } from "@/ui/VideoPlayer"; @@ -39,7 +39,7 @@ const RenderSubmissionBody = ({ submission }: { submission: Submission }) => { const RenderImageSubmission = ({ submission }: { submission: Submission }) => { return ( - { + return ( +
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ ); +}; + + + +const HeaderButtons = ({ submission, referrer, context }: { submission: Submission, referrer: string | null, context: string | null }) => { + const [isShareModalOpen, setIsShareModalOpen] = useState(false); + + const handleClose = () => { + setIsShareModalOpen(false); + } + + return ( +
+ setIsShareModalOpen(true)} context={context} /> + + {isShareModalOpen && ( + + )} + + +
+ ) +} + +export const LiveSubmissionDisplay = ({ + contestId, + submissions +}: { + contestId: string; + submissions: Array; +}) => { + + const [isShareModalOpen, setIsShareModalOpen] = useState(false); + const [isExpandModalOpen, setIsExpandModalOpen] = useState(false); + const [isManageModalOpen, setIsManageModalOpen] = useState(false); + + const [submission, setSubmission] = useState(null); + + + const openShareModal = (submission: Submission) => { + setIsShareModalOpen(true) + setSubmission(submission) + } + + const openExpandModal = (submission: Submission) => { + setIsExpandModalOpen(true) + setSubmission(submission) + } + + const handleShare = (event, submission) => { + event.stopPropagation(); + event.preventDefault(); + openShareModal(submission); + } + + const handleClose = () => { + setIsShareModalOpen(false) + setIsExpandModalOpen(false) + setIsManageModalOpen(false); + setSubmission(null); + } + + + const handleExpand = (submission) => { + openExpandModal(submission) + } + + + // base64 the contestId + const context = Buffer.from(encodeURIComponent(`/contest/${contestId}`)).toString('base64') + + return ( +
+
+
+ {submissions.map((submission, idx) => { + return ( + handleExpand(submission)} + footerChildren={ +
+
+
+
+ handleShare(event, submission)} context={context} /> +
+
+
+ } + + /> + ); + })} +
+
+ + {isShareModalOpen && submission && ( + + )} + + {isExpandModalOpen && submission && ( + + } /> + //
+ )} + +
+ ); +}; + diff --git a/uplink-client/src/app/submission/[submissionId]/client.tsx b/uplink-client/src/app/(legacy)/submission/[submissionId]/client.tsx similarity index 63% rename from uplink-client/src/app/submission/[submissionId]/client.tsx rename to uplink-client/src/app/(legacy)/submission/[submissionId]/client.tsx index 169a8ee4..c81e524d 100644 --- a/uplink-client/src/app/submission/[submissionId]/client.tsx +++ b/uplink-client/src/app/(legacy)/submission/[submissionId]/client.tsx @@ -1,75 +1,18 @@ -"use client"; -import { VoteActionProps } from "@/hooks/useVote"; -import { AdminWrapper } from "@/lib/AdminWrapper"; -import { useContestState } from "@/providers/ContestStateProvider"; +"use client";; import { useSession } from "@/providers/SessionProvider"; -import { Submission, isNftSubmission } from "@/types/submission" +import { Submission } from "@/types/submission" import WalletConnectButton from "@/ui/ConnectButton/WalletConnectButton"; import { useRouter } from "next/navigation"; import { useEffect, useState } from "react"; -import { BiLayerPlus } from "react-icons/bi"; import { HiArrowNarrowLeft } from "react-icons/hi"; import { HiCheckBadge } from "react-icons/hi2"; -import { MdOutlineCancelPresentation, MdOutlineSettings } from "react-icons/md"; +import { MdOutlineCancelPresentation } from "react-icons/md"; import { Boundary } from "@/ui/Boundary/Boundary" import { Button } from "@/ui/DesignKit/Button"; -import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/ui/DesignKit/Tooltip"; -import { Info } from "@/ui/DesignKit/Info"; import { Input } from "@/ui/DesignKit/Input"; import { Modal } from "@/ui/Modal/Modal"; -export const AddToCartButton = ({ submission, voteActions }: { submission: Submission, voteActions: VoteActionProps }) => { - const { addProposedVote, currentVotes, proposedVotes } = voteActions; - const { contestState } = useContestState(); - - const [isSelected, setIsSelected] = useState(false); - - useEffect(() => { - const isSelected = currentVotes.some(vote => vote.submissionId === submission.id) || proposedVotes.some(vote => vote.id === submission.id) - setIsSelected(isSelected); - }, [currentVotes, proposedVotes, submission.id]); - - const handleSelect = (event) => { - event.stopPropagation(); - event.preventDefault(); - if (!isSelected) { - addProposedVote(submission); - } - setIsSelected(!isSelected); - }; - - if (contestState === "voting") { - - - return ( - - - - {isSelected ? - - : - - } - - - {isSelected ? 'selected' : 'vote'} - - - - ) - } - - return null; -}; - - export const ManageModalContent = ({ onDelete }: { onDelete: () => void }) => { const [confirmationText, setConfirmationText] = useState(''); const [isConfirmed, setIsConfirmed] = useState(false); @@ -105,20 +48,6 @@ export const ManageModalContent = ({ onDelete }: { onDelete: () => void }) => { } -export const AdminButton = ({ contestId, submission, onClick }: { contestId: string, submission: Submission, onClick: (event?) => void }) => { - const { contestAdmins } = useContestState(); - - return ( - { return { address: admin } })}> - - - ) -} - - - export const BackButton = ({ context }: { context: string | null }) => { const router = useRouter(); @@ -169,7 +98,6 @@ export const HeaderButtons = ({ submission, referrer, context }: { submission: S return (
setIsShareModalOpen(true)} context={context} /> - setIsMintModalOpen(true)} /> {isShareModalOpen && ( @@ -180,16 +108,6 @@ export const HeaderButtons = ({ submission, referrer, context }: { submission: S ) } -export const MintButton = ({ submission, onClick, styleOverride }: { submission: Submission, onClick: (event?) => void, styleOverride?: string }) => { - if (!isNftSubmission(submission)) return null; - return ( - - ) -} export const ShareModalContent = ({ submission, handleClose, context }: { submission: Submission, handleClose: () => void, context: string }) => { const { status, data: session } = useSession(); @@ -253,17 +171,7 @@ export const ShareButton = ({ submission, onClick, context }: { submission: Subm }, 2000); }; const handleShareClick = (event) => { - if (!isNftSubmission(submission)) { - handleShare(event, `${process.env.NEXT_PUBLIC_CLIENT_URL}/submission/${submission.id}?context=${context}`) - } - else { - if (status === 'authenticated') { - handleShare(event, `${process.env.NEXT_PUBLIC_CLIENT_URL}/submission/${submission.id}?context=${context}&referrer=${session?.user?.address}`) - } - else { - onClick(event); - } - } + handleShare(event, `${process.env.NEXT_PUBLIC_CLIENT_URL}/submission/${submission.id}?context=${context}`) } return diff --git a/uplink-client/src/app/submission/[submissionId]/page.tsx b/uplink-client/src/app/(legacy)/submission/[submissionId]/page.tsx similarity index 51% rename from uplink-client/src/app/submission/[submissionId]/page.tsx rename to uplink-client/src/app/(legacy)/submission/[submissionId]/page.tsx index 7ed5b94b..88ee5715 100644 --- a/uplink-client/src/app/submission/[submissionId]/page.tsx +++ b/uplink-client/src/app/(legacy)/submission/[submissionId]/page.tsx @@ -1,69 +1,10 @@ import fetchSingleSubmission from "@/lib/fetch/fetchSingleSubmission" -import ExpandedSubmission from "@/ui/Submission/ExpandedSubmission" +import ExpandedSubmission from "@/app/(legacy)/contest/components/ExpandedSubmission" import { Suspense } from "react"; -import { BackButton, HeaderButtons, MintButton, ShareButton } from "./client"; +import { BackButton, HeaderButtons } from "./client"; import { Metadata } from "next"; -import { Submission, isNftSubmission } from "@/types/submission"; +import { isStandardSubmission } from "@/types/submission"; import { parseIpfsUrl } from "@/lib/ipfs"; -import { getChainName } from "@/lib/chains/supportedChains"; - -type NftMetadata = { - "eth:nft:contract_address": string, - "eth:nft:schema": string, - "eth:nft:chain_id": number, - "eth:nft:chain": string, - "eth:nft:collection": string, - "eth:nft:creator_address": string, - "eth:nft:media_url": string, - "eth:nft:mint_count": string -}; - -const calculateImageAspectRatio = async (url: string) => { - try { - const fileInfo = await fetch(`https://res.cloudinary.com/drrkx8iye/image/fetch/fl_getinfo/${url}`).then(res => res.json()) - const { output } = fileInfo; - if (output.width / output.height > 1.45) return "1.91:1"; - return "1:1" - } catch (e) { - return "1:1" - } -} - - -const generateNftMetadata = async (submission: Submission) => { - const aspect = await calculateImageAspectRatio(parseIpfsUrl(submission.edition.imageURI).gateway) - const warpable = submission.edition.chainId === 8453 || submission.edition.chainId === 7777777 ? true : false; - - const nftMetadata: NftMetadata = { - "eth:nft:contract_address": submission.edition.contractAddress, - "eth:nft:schema": "ERC721", - "eth:nft:chain_id": submission.edition.chainId, - "eth:nft:chain": getChainName(submission.edition.chainId).toLowerCase(), - "eth:nft:collection": submission.edition.name, - "eth:nft:creator_address": submission.edition.defaultAdmin, - "eth:nft:media_url": parseIpfsUrl(submission.edition.imageURI).gateway, - "eth:nft:mint_count": "0" - } - - const fcMetadata: Record = { - "fc:frame": "vNext", - "fc:frame:image": parseIpfsUrl(submission.edition.imageURI).gateway, - "fc:frame:image:aspect_ratio": aspect, - }; - - const warpableMetadata: Record = { - "fc:frame:button:1": "Mint", - "fc:frame:button:1:action": "mint", - "fc:frame:button:1:target": `eip155:${submission.edition.chainId}:${submission.edition.contractAddress}`, - //eip155:8453:0x800243201fb33b86219315f5f44e1795dfb5d97a:19 - } - - return { - nftMetadata, - fcMetadata, - warpableMetadata - } -} export async function generateMetadata({ params, @@ -72,15 +13,13 @@ export async function generateMetadata({ params: { submissionId: string }; searchParams: { [key: string]: string | undefined } }): Promise { - const submission = await fetchSingleSubmission(params.submissionId) + const submission = await fetchSingleSubmission(params.submissionId); + + const previewImage = isStandardSubmission(submission) ? parseIpfsUrl(submission.data.previewAsset).gateway : parseIpfsUrl(submission.data.thread[0].previewAsset).gateway const referrer = searchParams?.referrer ?? null const context = searchParams?.context ?? null - const isNft = isNftSubmission(submission) - - const { nftMetadata, fcMetadata, warpableMetadata } = isNft ? await generateNftMetadata(submission) : { nftMetadata: {}, fcMetadata: {}, warpableMetadata: {} } - return { title: `${submission.data.title}`, description: `${submission.data.title} on Uplink`, @@ -89,7 +28,7 @@ export async function generateMetadata({ description: `${submission.data.title} on Uplink`, images: [ { - url: `api/submission/${params.submissionId}/submission_metadata`, + url: previewImage, width: 600, height: 600, alt: `${submission.data.title} media`, @@ -99,11 +38,6 @@ export async function generateMetadata({ type: "website", }, - other: { - ...fcMetadata, - ...nftMetadata, - ...warpableMetadata - }, alternates: { canonical: `${process.env.NEXT_PUBLIC_CLIENT_URL}/submission/${params.submissionId}?context=${context}&referrer=${referrer}` diff --git a/uplink-client/src/app/(space)/[name]/contest/[contractId]/@sidebar/(enabled)/layout.tsx b/uplink-client/src/app/(space)/[name]/contest/[contractId]/@sidebar/(enabled)/layout.tsx index a78fdcce..b3b07f83 100644 --- a/uplink-client/src/app/(space)/[name]/contest/[contractId]/@sidebar/(enabled)/layout.tsx +++ b/uplink-client/src/app/(space)/[name]/contest/[contractId]/@sidebar/(enabled)/layout.tsx @@ -15,7 +15,7 @@ const VoteCart = dynamic( } ) -const StateSpecificSidebar = async ({ contractId }: { contractId: ContractID }) => { +const StateSpecificSidebar = async ({ spaceName, contractId }: { spaceName: string, contractId: ContractID }) => { const channel = await fetchChannel(contractId) @@ -29,6 +29,7 @@ const StateSpecificSidebar = async ({ contractId }: { contractId: ContractID }) contractId={contractId} detailsChild={
{children}
}> - +
diff --git a/uplink-client/src/app/(space)/[name]/contest/[contractId]/post/[postId]/page.tsx b/uplink-client/src/app/(space)/[name]/contest/[contractId]/post/[postId]/page.tsx index c177bdc8..3b58ae74 100644 --- a/uplink-client/src/app/(space)/[name]/contest/[contractId]/post/[postId]/page.tsx +++ b/uplink-client/src/app/(space)/[name]/contest/[contractId]/post/[postId]/page.tsx @@ -1,9 +1,8 @@ -import { calculateImageAspectRatio } from "@/lib/farcaster/utils"; import fetchChannel from "@/lib/fetch/fetchChannel"; import fetchSingleSpace from "@/lib/fetch/fetchSingleSpace"; import { fetchSingleTokenIntent, fetchSingleTokenV2 } from "@/lib/fetch/fetchTokensV2"; import { parseIpfsUrl } from "@/lib/ipfs"; -import UplinkImage from "@/lib/UplinkImage"; +import OptimizedImage from "@/lib/OptimizedImage"; import { ContractID } from "@/types/channel"; import { Button } from "@/ui/DesignKit/Button"; import { MintTokenSwitch } from "@/ui/Token/MintToken"; @@ -23,15 +22,10 @@ export async function generateMetadata({ const { name: spaceName, contractId, postId } = params const isIntent = searchParams?.intent ? true : false - const referral = searchParams?.referrer ?? "" - - const channel = await fetchChannel(contractId); const token = isIntent ? await fetchSingleTokenIntent(contractId, postId) : await fetchSingleTokenV2(contractId, postId) const author = token.author - const aspect = await calculateImageAspectRatio(parseIpfsUrl(token.metadata.image).gateway) - // const fcMetadata: Record = { // "fc:frame": "vNext", // "fc:frame:image": parseIpfsUrl(token.metadata.image).gateway, @@ -143,7 +137,7 @@ const ChannelDetails = async ({ spaceName, contractId }: { spaceName: string, co href={`/${space.name}`} draggable={false} > - - {/* */} }> diff --git a/uplink-client/src/app/(space)/[name]/mintboard/(redirect)/page.tsx b/uplink-client/src/app/(space)/[name]/mintboard/(redirect)/page.tsx index a28ddb6a..d9caee30 100644 --- a/uplink-client/src/app/(space)/[name]/mintboard/(redirect)/page.tsx +++ b/uplink-client/src/app/(space)/[name]/mintboard/(redirect)/page.tsx @@ -11,9 +11,8 @@ import { TbLoader2 } from "react-icons/tb"; // redirect legacy mintboards to v2 mintboards const Redirect = async ({ spaceName }: { spaceName: string }) => { - const channels = await fetchSpaceChannels(spaceName); - const mintboards = channels.filter(channel => isInfiniteChannel(channel)); - const mintboard = mintboards[0]; + const channels = await fetchSpaceChannels(spaceName, 8453); + const mintboard = channels.infiniteChannels[0]; if (!mintboard) notFound(); diff --git a/uplink-client/src/app/(space)/[name]/mintboard/(redirect)/post/[postId]/page.tsx b/uplink-client/src/app/(space)/[name]/mintboard/(redirect)/post/[postId]/page.tsx index ee4bcd91..564531b2 100644 --- a/uplink-client/src/app/(space)/[name]/mintboard/(redirect)/post/[postId]/page.tsx +++ b/uplink-client/src/app/(space)/[name]/mintboard/(redirect)/post/[postId]/page.tsx @@ -8,9 +8,8 @@ import { TbLoader2 } from "react-icons/tb"; // redirect legacy mintboard posts to v2 mintboard posts const Redirect = async ({ spaceName, postId }: { spaceName: string, postId: string }) => { - const channels = await fetchSpaceChannels(spaceName); - const mintboards = channels.filter(channel => isInfiniteChannel(channel)); - const mintboard = mintboards[0]; + const channels = await fetchSpaceChannels(spaceName, 8453); + const mintboard = channels.infiniteChannels[0]; if (!mintboard) notFound(); diff --git a/uplink-client/src/app/(space)/[name]/mintboard/[contractId]/page.tsx b/uplink-client/src/app/(space)/[name]/mintboard/[contractId]/page.tsx index ff4baa9f..446b0a52 100644 --- a/uplink-client/src/app/(space)/[name]/mintboard/[contractId]/page.tsx +++ b/uplink-client/src/app/(space)/[name]/mintboard/[contractId]/page.tsx @@ -2,14 +2,14 @@ import fetchChannel from '@/lib/fetch/fetchChannel'; import fetchSingleSpace from '@/lib/fetch/fetchSingleSpace'; import { fetchPopularTokens, fetchTokenIntents, fetchTokensV1, fetchTokensV2 } from '@/lib/fetch/fetchTokensV2'; import { parseIpfsUrl } from '@/lib/ipfs'; -import UplinkImage from '@/lib/UplinkImage'; +import OptimizedImage from '@/lib/OptimizedImage'; import SwrProvider from '@/providers/SwrProvider'; import { Boundary } from '@/ui/Boundary/Boundary'; import Link from 'next/link'; import React, { Suspense } from 'react'; import { unstable_serialize } from 'swr'; import { ChannelUpgrades, MintFeeDonut, PostSkeleton, RenderDefaultTokens, RenderPopularTokens, RenderTokenIntents, WhatsNew } from './client'; -import { Channel, ContractID, doesChannelHaveFees, splitContractID } from '@/types/channel'; +import { ContractID, doesChannelHaveFees, splitContractID } from '@/types/channel'; import { notFound } from 'next/navigation'; import { MdNewReleases } from 'react-icons/md'; import { AdminWrapper } from '@/lib/AdminWrapper'; @@ -52,7 +52,7 @@ const BoardInfo = async ({ spaceName, contractId }: { spaceName: string, contrac href={`/${spaceName}`} draggable={false} > - diff --git a/uplink-client/src/app/(space)/[name]/mintboard/new/page.tsx b/uplink-client/src/app/(space)/[name]/mintboard/new/page.tsx index d96cb8ab..aa06cff2 100644 --- a/uplink-client/src/app/(space)/[name]/mintboard/new/page.tsx +++ b/uplink-client/src/app/(space)/[name]/mintboard/new/page.tsx @@ -17,7 +17,6 @@ const LoadingDialog = () => { }; const PageContent = async ({ spaceName }: { spaceName: string }) => { - //const mintBoard = await fetchMintBoard(spaceName).catch(() => { return null }); const space = await fetchSingleSpace(spaceName).catch(() => { return null }); return diff --git a/uplink-client/src/app/(space)/[name]/page.tsx b/uplink-client/src/app/(space)/[name]/page.tsx index b680d425..c3d780c6 100644 --- a/uplink-client/src/app/(space)/[name]/page.tsx +++ b/uplink-client/src/app/(space)/[name]/page.tsx @@ -2,20 +2,19 @@ import Link from "next/link"; import { BiLink } from "react-icons/bi"; import { FaTwitter } from "react-icons/fa"; import fetchSingleSpace from "@/lib/fetch/fetchSingleSpace"; -import fetchSpaceContests, { SpaceContest } from "@/lib/fetch/fetchSpaceContests"; import { Suspense } from "react"; import { HiSparkles } from "react-icons/hi2"; -import UplinkImage from "@/lib/UplinkImage" +import OptimizedImage from "@/lib/OptimizedImage" import { Boundary } from "@/ui/Boundary/Boundary"; import { AdminWrapper } from "@/lib/AdminWrapper"; import { parseIpfsUrl } from "@/lib/ipfs"; -import { ImageWrapper } from "@/ui/Submission/MediaWrapper"; +import { ImageWrapper } from "@/app/(legacy)/contest/components/MediaWrapper"; const compact_formatter = new Intl.NumberFormat('en', { notation: 'compact' }) const round_formatter = new Intl.NumberFormat('en', { maximumFractionDigits: 2 }) -import { Card, CardContent, CardFooter, CardHeader, CardTitle } from "@/ui/Card/Card"; +import { Card, CardContent, CardFooter, CardHeader, CardTitle } from "@/ui/DesignKit/Card"; import { Button } from "@/ui/DesignKit/Button"; import fetchSpaceStats from "@/lib/fetch/fetchSpaceStats"; import fetchSpaceChannels from "@/lib/fetch/fetchSpaceChannels"; @@ -24,6 +23,7 @@ import { fetchPopularTokens } from "@/lib/fetch/fetchTokensV2"; import { IFiniteTransportConfig, ITokenMetadata } from "@tx-kit/sdk/subgraph"; import { ContestStatusLabel, FormatTokenStatistic } from "./client"; import { isAddress } from "viem"; +import { LegacyContest, LegacyContestWithPrompt } from "@/types/contest"; const SpaceContestsSkeleton = () => { return ( @@ -100,7 +100,7 @@ const SpaceInfo = async ({ name }: { name: string }) => {
- { return (
- { } const MintboardDisplay = async ({ spaceName }: { spaceName: string }) => { - const channels = await fetchSpaceChannels(spaceName); - - const mintboards = channels.filter(channel => isInfiniteChannel(channel)); + const mintboards = await fetchSpaceChannels(spaceName, 8453).then(data => data.infiniteChannels); if (mintboards.length > 0) { @@ -363,7 +361,7 @@ const ContestCard = async ({
- , contestsV2: Array, spaceName: string, spaceLogo: string }) => { +const Contests = ({ contestsV1, contestsV2, spaceName, spaceLogo }: { contestsV1: Array, contestsV2: Array, spaceName: string, spaceLogo: string }) => { if (contestsV1.length + contestsV2.length === 0) { return ( @@ -433,25 +431,33 @@ const Contests = ({ contestsV1, contestsV2, spaceName, spaceLogo }: { contestsV1 const ContestDisplay = async ({ spaceName }: { spaceName: string }) => { - const [spaceWithContests, contestsV2] = await Promise.all([ - fetchSpaceContests(spaceName), - fetchSpaceChannels(spaceName) - .then(channels => - channels - .filter(isFiniteChannel) - .sort((a, b) => - Number((b.transportLayer.transportConfig as IFiniteTransportConfig).mintEnd) - - Number((a.transportLayer.transportConfig as IFiniteTransportConfig).mintEnd) - ) - ) - .then(sortedChannels => - Promise.all(sortedChannels.map(async channel => { + const space_promise = fetchSingleSpace(spaceName); + + const contests_promise = fetchSpaceChannels(spaceName, 8453) // TODO chainId + .then(data => { + return { + v1: data.legacyContests, + v2: data.finiteChannels.sort((a, b) => + Number((b.transportLayer.transportConfig as IFiniteTransportConfig).mintEnd) - + Number((a.transportLayer.transportConfig as IFiniteTransportConfig).mintEnd) + ) + } + }) + .then(async data => { + return { + v1: await Promise.all(data.v1.map(async contest => { + const promptData = await fetch(contest.promptUrl).then(res => res.json()); + return { ...contest, promptData }; + })), + v2: await Promise.all(data.v2.map(async channel => { const metadata: ITokenMetadata = await fetch(parseIpfsUrl(channel.uri).gateway).then(res => res.json()); return { ...channel, metadata }; })) - ) - ]); + } + } + ) + const [space, contests] = await Promise.all([space_promise, contests_promise]); return (
@@ -461,7 +467,7 @@ const ContestDisplay = async ({ spaceName }: { spaceName: string }) => {
- +
) } diff --git a/uplink-client/src/app/api/contest/[id]/contest_metadata/route.tsx b/uplink-client/src/app/api/contest/[id]/contest_metadata/route.tsx index d2503712..6c3c8c0e 100644 --- a/uplink-client/src/app/api/contest/[id]/contest_metadata/route.tsx +++ b/uplink-client/src/app/api/contest/[id]/contest_metadata/route.tsx @@ -1,12 +1,12 @@ import { NextRequest } from "next/server"; import { ImageResponse } from "next/og" -import fetchContest from "@/lib/fetch/fetchContest"; +import fetchLegacyContest from "@/lib/fetch/fetchLegacyContest"; export const runtime = 'edge' export async function GET(req: NextRequest) { const contestId = req.nextUrl.pathname.split("/")[3]; - const contest = await fetchContest(contestId).then(async (res) => { + const contest = await fetchLegacyContest(contestId).then(async (res) => { const promptData = await fetch(res.promptUrl).then((res) => res.json()); return { ...res, prompt: promptData }; }); @@ -14,20 +14,20 @@ export async function GET(req: NextRequest) { const RenderPreview = () => { const preview_image = contest?.prompt?.coverUrl ?? null - if(preview_image){ + if (preview_image) { return ( logo - ) + src={preview_image} + alt="logo" + width="100%" + height="100%" + style={{ objectFit: "cover" }} + /> + ) } return null; } - + // const ubuntuBold = fetch( // new URL('@/styles/fonts/Ubuntu-Bold.ttf', import.meta.url) // ).then((res) => res.arrayBuffer()) @@ -59,7 +59,7 @@ export async function GET(req: NextRequest) { background: "#121212" }} > - +
{contest.prompt.title} - - {/*
- logo -

- {contest.space.displayName} -

-
*/}
), { width: 1200, height: 600, - // fonts: [ - // { - // name: 'Ubuntu', - // data: await ubuntuBold, - // style: 'normal', - // weight: 400 - // } - // ] } ); } diff --git a/uplink-client/src/app/api/space/[name]/mintboard/mintboard_metadata/farcaster_frame_handler/route.ts b/uplink-client/src/app/api/space/[name]/mintboard/mintboard_metadata/farcaster_frame_handler/route.ts deleted file mode 100644 index 0960179a..00000000 --- a/uplink-client/src/app/api/space/[name]/mintboard/mintboard_metadata/farcaster_frame_handler/route.ts +++ /dev/null @@ -1,120 +0,0 @@ -import { NextRequest, NextResponse } from "next/server"; -import fetchMintBoard from "@/lib/fetch/fetchMintBoard"; -import { parseIpfsUrl } from "@/lib/ipfs"; -import { parse } from "url"; - -let postsArray = []; -let currentIndex = 0; -let totalCount = 0; -let currentPost; - -export async function POST(req: NextRequest) { - // try { - // const body = await req.json(); - // const buttonIndex = body.untrustedData.buttonIndex; - // const name = req.nextUrl.pathname.split("/")[3]; - // const mintBoard = await fetchMintBoard(name); - - // postsArray = [...mintBoard.posts]; - // postsArray = postsArray.reverse(); - - // totalCount = postsArray.length; - - // if (buttonIndex === 1) { - // currentIndex = getPreviousIndex(postsArray, currentIndex); - // } else if (buttonIndex === 2) { - // currentIndex = getNextIndex(postsArray, currentIndex); - // } else if (buttonIndex === 3) { - // currentIndex = totalCount - 1; - // } else { - // currentIndex = findIndexWithHighestTotalMints(postsArray); - // } - - // currentPost = postsArray[currentIndex]; - - // return new NextResponse( - // ` - // - // - // - // - // - // - // - // - // - // - // - // - // `, - // { status: 200 } - // ); - // } catch (e) { - // console.error(e); - // return NextResponse.json({ status: 500 }); - // } -} - -// helpers -function findIndexWithHighestTotalMints(postsArray) { - if (!Array.isArray(postsArray) || postsArray.length === 0) { - return -1; // return -1 if the data is not an array or is empty - } - - let maxMints = 0; - let indexWithMaxMints = -1; - - for (let i = 0; i < postsArray.length; i++) { - const totalMints = parseInt(postsArray[i].totalMints, 10); - if (totalMints > maxMints) { - maxMints = totalMints; - indexWithMaxMints = i; - } - } - - return indexWithMaxMints; -} - -function getNextIndex(postsArray, currentIndex) { - if (!Array.isArray(postsArray) || postsArray.length === 0) { - return -1; // return -1 if the array is not valid or is empty - } - - // Check if currentIndex is valid - if (currentIndex < 0 || currentIndex >= postsArray.length) { - return 0; - } - - const nextIndex = currentIndex + 1; - - // Check if nextIndex exceeds the array length - if (nextIndex >= postsArray.length) { - return 0; // or return 0 to loop back to the first element - } - - return nextIndex; -} - -function getPreviousIndex(postsArray, currentIndex) { - if (!Array.isArray(postsArray) || postsArray.length === 0) { - return -1; // return -1 if the array is not valid or is empty - } - - // Check if currentIndex is valid - if (currentIndex < 0 || currentIndex >= postsArray.length) { - return postsArray.length - 1; - } - - const previousIndex = currentIndex - 1; - - // Check if previousIndex is before the start of the array - if (previousIndex < 0) { - return 0; // or return array.length - 1 to loop back to the last element - } - - return previousIndex; -} diff --git a/uplink-client/src/app/api/space/[name]/mintboard/mintboard_metadata/route.tsx b/uplink-client/src/app/api/space/[name]/mintboard/mintboard_metadata/route.tsx index 2a59979b..93b893aa 100644 --- a/uplink-client/src/app/api/space/[name]/mintboard/mintboard_metadata/route.tsx +++ b/uplink-client/src/app/api/space/[name]/mintboard/mintboard_metadata/route.tsx @@ -1,24 +1,23 @@ import { NextRequest } from "next/server"; import { ImageResponse } from "next/og" -import fetchMintBoard from "@/lib/fetch/fetchMintBoard"; -import { parseIpfsUrl } from "@/lib/ipfs" +import fetchSingleSpace from "@/lib/fetch/fetchSingleSpace"; export const runtime = 'edge' export async function GET(req: NextRequest) { const name = req.nextUrl.pathname.split("/")[3]; - const mintboard = await fetchMintBoard(name); - const posts = { mintboard } + + const space = await fetchSingleSpace(name); const ubuntuBold = fetch( new URL('@/styles/fonts/Ubuntu-Bold.ttf', import.meta.url) ).then((res) => res.arrayBuffer()) - - // for each post, the optimized img can be constructed via parseIpfsUrl(post.edition.imageURI) - // this function will return an object with 2 keys - // { raw: ipfs://cid, gateway: https://uplink.mypinata.cloud/ipfs/cid } - + + // for each post, the optimized img can be constructed via parseIpfsUrl(post.edition.imageURI) + // this function will return an object with 2 keys + // { raw: ipfs://cid, gateway: https://uplink.mypinata.cloud/ipfs/cid } + return new ImageResponse( ( @@ -48,13 +47,13 @@ export async function GET(req: NextRequest) { }} > logo -
+
-

- Create with {mintboard.space.displayName} + Create with {space.displayName}

diff --git a/uplink-client/src/app/api/space/[name]/mintboard/post/[postId]/post_metadata/route.tsx b/uplink-client/src/app/api/space/[name]/mintboard/post/[postId]/post_metadata/route.tsx deleted file mode 100644 index f0a853e0..00000000 --- a/uplink-client/src/app/api/space/[name]/mintboard/post/[postId]/post_metadata/route.tsx +++ /dev/null @@ -1,59 +0,0 @@ -import { NextRequest } from "next/server"; -import { ImageResponse } from "next/og" -import fetchMintBoard from "@/lib/fetch/fetchMintBoard"; -import { parseIpfsUrl } from "@/lib/ipfs"; -import { fetchSingleMintboardPost } from "@/lib/fetch/fetchMintBoardPosts"; - -export const runtime = 'edge' - -export async function GET(req: NextRequest) { - const name = req.nextUrl.pathname.split("/")[3]; - const postId = req.nextUrl.pathname.split("/")[6]; - const post = await fetchSingleMintboardPost(name, postId); - const previewImage = parseIpfsUrl(post.edition.imageURI).gateway; - - return new ImageResponse( - ( -
-
- logo -
- -
- ), - { - width: 1200, - height: 600, - } - ); -} - diff --git a/uplink-client/src/app/api/space/route.tsx b/uplink-client/src/app/api/space/route.tsx index 1e29f54d..89fbc210 100644 --- a/uplink-client/src/app/api/space/route.tsx +++ b/uplink-client/src/app/api/space/route.tsx @@ -28,7 +28,7 @@ export async function GET(req: NextRequest) { > logo { - if (submission.data.type === 'image') { - if (isTwitterSubmission(submission)) { - return submission.data.thread[0].previewAsset - } - return submission.data.previewAsset - } - return null; - } - - - const RenderPreview = () => { - const preview_image = calculatePreview(); - if(preview_image){ - return ( - logo - ) - } - return null; - } - - const ubuntuBold = fetch( - new URL('@/styles/fonts/Ubuntu-Bold.ttf', import.meta.url) - ).then((res) => res.arrayBuffer()) - - return new ImageResponse( - ( -
-
- -
- -
-

- {submission.data.title} -

-
-
- ), - { - width: 1200, - height: 600, - fonts: [ - { - name: 'Ubuntu', - data: await ubuntuBold, - style: 'normal', - weight: 400 - } - ] - } - ); -} diff --git a/uplink-client/src/app/contest/[id]/layout.tsx b/uplink-client/src/app/contest/[id]/layout.tsx deleted file mode 100644 index c8392595..00000000 --- a/uplink-client/src/app/contest/[id]/layout.tsx +++ /dev/null @@ -1,43 +0,0 @@ -import fetchContest from "@/lib/fetch/fetchContest"; -import { ContestStateProvider } from "@/providers/ContestStateProvider" -import { Metadata } from "next"; - - -export async function generateMetadata({ - params, - }: { - params: { id: string }; - }): Promise { - const contest = await fetchContest(params.id); - - return { - title: `${contest.space.displayName}`, - description: `${contest.space.displayName} on Uplink`, - openGraph: { - title: `${contest.space.displayName}`, - description: `Create with ${contest.space.displayName} on Uplink`, - images: [ - { - url: `api/contest/${params.id}/contest_metadata`, - width: 600, - height: 600, - alt: `${contest.space.displayName} logo`, - }, - ], - locale: "en_US", - type: "website", - }, - }; - } - - -export default async function Layout({ params, children }: { params: { id: string }, children: React.ReactNode }) { - const contest = await fetchContest(params.id); - return ( - -
- {children} -
-
- ) -} \ No newline at end of file diff --git a/uplink-client/src/app/contest/[id]/page.tsx b/uplink-client/src/app/contest/[id]/page.tsx deleted file mode 100644 index 16a586f8..00000000 --- a/uplink-client/src/app/contest/[id]/page.tsx +++ /dev/null @@ -1,64 +0,0 @@ -import { - LiveSubmissionDisplay, - SubmissionDisplaySkeleton, -} from "@/ui/Submission/SubmissionDisplay"; -import ContestHeading, { - ContestHeadingSkeleton, -} from "@/ui/ContestHeading/ContestHeading"; -import ContestDetails, { DetailsSkeleton } from "@/ui/ContestDetails/ContestDetails"; -import { VoteTab } from "@/ui/Vote/Vote"; -import { Suspense } from "react"; -import fetchSubmissions from "@/lib/fetch/fetchSubmissions"; -import SwrProvider from "@/providers/SwrProvider"; -import dynamic from "next/dynamic"; - -const ContestSidebar = dynamic( - () => import("@/ui/ContestSidebar/ContestSidebar"), - { - ssr: false, - loading: () =>
- } -) - -const SubmissionDisplayWrapper = async ({ - contestId, - children, -}: { - contestId: string; - children: React.ReactNode; -}) => { - const submissions = await fetchSubmissions(contestId); - const fallback = { - [`submissions/${contestId}`]: submissions, - }; - return {children}; -}; - - - -export default async function Page({ params }: { params: { id: string } }) { - - const contestId = params.id; - - return ( -
-
- }> - - - - }> - - - - -
-
- }>} - voteChild={} - /> -
-
- ); -} \ No newline at end of file diff --git a/uplink-client/src/app/contest/[id]/studio/loadingDialog.tsx b/uplink-client/src/app/contest/[id]/studio/loadingDialog.tsx deleted file mode 100644 index 6a5a0d18..00000000 --- a/uplink-client/src/app/contest/[id]/studio/loadingDialog.tsx +++ /dev/null @@ -1,17 +0,0 @@ -const LoadingDialog = ({ springUp }: { springUp?: boolean }) => { - return ( -
-

Starting up the studio

-
-
- ); -}; - -export default LoadingDialog; diff --git a/uplink-client/src/app/contest/[id]/studio/page.tsx b/uplink-client/src/app/contest/[id]/studio/page.tsx deleted file mode 100644 index 3f557eef..00000000 --- a/uplink-client/src/app/contest/[id]/studio/page.tsx +++ /dev/null @@ -1,17 +0,0 @@ -import LoadingDialog from "./loadingDialog"; -import dynamic from "next/dynamic"; - -const StandardSubmit = dynamic(() => import("./standardSubmit"), { - ssr: false, - loading: () => , -}); - -export default async function Page({ - params, -}: { - params: { name: string; id: string }; -}) { - - // contest in submit window - return ; -} diff --git a/uplink-client/src/app/contest/[id]/studio/standardSubmit.tsx b/uplink-client/src/app/contest/[id]/studio/standardSubmit.tsx deleted file mode 100644 index df586bec..00000000 --- a/uplink-client/src/app/contest/[id]/studio/standardSubmit.tsx +++ /dev/null @@ -1,459 +0,0 @@ -"use client";; -import type { OutputData } from "@editorjs/editorjs"; -import useSWRMutation from "swr/mutation"; -import { - HiCheckBadge, - HiXCircle, -} from "react-icons/hi2"; -import { useEffect, useState } from "react"; -import { useSession } from "@/providers/SessionProvider"; -import { BiInfoCircle } from "react-icons/bi"; -import { Modal } from "@/ui/Modal/Modal"; -import WalletConnectButton from "@/ui/ConnectButton/WalletConnectButton"; -import { Decimal } from "decimal.js"; -import { UserSubmissionParams, useContestInteractionApi } from "@/hooks/useContestInteractionAPI"; -import { - useStandardSubmissionCreator, - SubmissionBuilderProps, - validateSubmission -} from "@/hooks/useStandardSubmissionCreator"; -import { handleMutationError } from "@/lib/handleMutationError"; -import { HiBadgeCheck } from "react-icons/hi"; -import Link from "next/link"; -import useLiveSubmissions from "@/hooks/useLiveSubmissions"; -import LoadingDialog from "./loadingDialog"; -import { useContestState } from "@/providers/ContestStateProvider"; -import { MediaUpload } from "@/ui/MediaUpload/MediaUpload"; -import { Label } from "@/ui/DesignKit/Label"; -import { Button } from "@/ui/DesignKit/Button"; -import dynamic from "next/dynamic"; -import { Input } from "@/ui/DesignKit/Input"; -const Editor = dynamic(() => import("@/ui/Editor/Editor"), { - ssr: false, - loading: () => ( -
- ), -}); - -async function postSubmission( - url, - { - arg, - }: { - arg: { - contestId: string; - submission: { - title: string; - body: OutputData | null; - previewAsset: string | null; - videoAsset: string | null; - }; - csrfToken: string | null; - }; - } -) { - return fetch(`${process.env.NEXT_PUBLIC_HUB_URL}/graphql`, { - method: "POST", - headers: { - "Content-Type": "application/json", - "X-CSRF-TOKEN": arg.csrfToken, - }, - credentials: "include", - body: JSON.stringify({ - query: ` - mutation Mutation($contestId: ID!, $submission: SubmissionPayload!) { - createSubmission(contestId: $contestId, submission: $submission) { - success - submissionId - userSubmissionParams { - maxSubPower - remainingSubPower - userSubmissions { - type - } - } - } - }`, - variables: { - contestId: arg.contestId, - submission: arg.submission, - }, - }), - }) - .then((res) => res.json()) - .then(handleMutationError) - .then((res) => res.data.createSubmission); -} - -const ErrorLabel = ({ error }: { error?: string }) => { - if (error) - return ( - - ); - return null; -}; - -const SubmissionTitle = ({ - title, - errors, - setSubmissionTitle, -}: { - title: string; - errors: SubmissionBuilderProps["errors"]; - setSubmissionTitle: (val: string) => void; -}) => { - const handleTitleChange = (e: any) => { - setSubmissionTitle(e.target.value); - }; - - const handleTextareaResize = (e: any) => { - e.target.style.height = "auto"; - e.target.style.height = `${e.target.scrollHeight}px`; - }; - - return ( -
- - - - - -
- ); -}; - -const SubmissionBody = ({ - submissionBody, - errors, - setSubmissionBody, -}: { - submissionBody: SubmissionBuilderProps["submissionBody"]; - errors: SubmissionBuilderProps["errors"]; - setSubmissionBody: (val: OutputData) => void; -}) => { - const editorCallback = (data: OutputData) => { - setSubmissionBody(data); - }; - - return ( -
- - - -
- ); -}; - -const StudioSidebar = ({ - state, - contestId, - setErrors, - isUploading, -}: { - state: SubmissionBuilderProps; - contestId: string; - setErrors: (errors: any) => void; - isUploading: boolean; -}) => { - const [isRestrictionModalOpen, setIsRestrictionModalOpen] = useState(false); - const [isSuccessModalOpen, setIsSuccessModalOpen] = useState(false); - - const { userSubmitParams, areUserSubmitParamsLoading: isLoading } = useContestInteractionApi(contestId); - const { stateRemainingTime } = useContestState(); - - const { data: session, status } = useSession(); - const { mutateLiveSubmissions } = useLiveSubmissions(contestId); - const { trigger, data, error, isMutating, reset } = useSWRMutation( - [`/api/userSubmitParams/${contestId}`, session?.user?.address], - postSubmission, - { - onError: (err) => { - console.log(err); - reset(); - }, - } - ); - - const handleSubmit = async () => { - const { payload, isError } = await validateSubmission(state, (data) => setErrors(data)); - - if (isError) return; - try { - await trigger({ - contestId, - submission: payload, - csrfToken: session.csrfToken, - }); - mutateLiveSubmissions(); - setIsSuccessModalOpen(true); - } catch (err) { - console.log(err); - reset(); - } - }; - - useEffect(() => { - () => { - return reset(); - } - }, []); - - // userSubmitParams are undefined if the user is not logged in, if the contest is not in the submit window, or if the fetch hasn't finished yet - // at this stage, we can assume that the contest is in the submit window. - if (isLoading) { - // waiting for results. show loading state - return
; - } else { - return ( -
-
- {userSubmitParams && ( -
-
-

entries remaining

-

{userSubmitParams.remainingSubPower}

-
- {userSubmitParams.restrictionResults.length > 0 && ( -
-
-

satisfies restrictions?

- setIsRestrictionModalOpen(true)} - className="w-4 h-4 text-gray-500 cursor-pointer hover:text-gray-300" - /> -
- {userSubmitParams.restrictionResults.some( - (el) => el.result === true - ) ? ( - - ) : ( - - )} -
- )} -
-

status

- {parseInt(userSubmitParams.remainingSubPower) > 0 && - new Decimal(userSubmitParams.maxSubPower).greaterThan(0) ? ( -

eligible

- ) : ( -

not eligible

- )} -
-
- )} - -
- -

{stateRemainingTime}

-
-
-
- setIsRestrictionModalOpen(false)} - userSubmitParams={userSubmitParams} - /> - {data && data.success && ( - - )} -
- ); - } -}; - -const SuccessModal = ({ isModalOpen, submissionId, isDroppable, contestId }) => { - - return ( - { }} className="w-full max-w-[500px]"> -
- -

{`You're all set`}

-
- - - -
-
-
- ) - -} - -const RestrictionModal = ({ - isModalOpen, - handleClose, - userSubmitParams, -}: { - isModalOpen: boolean; - handleClose: () => void; - userSubmitParams: UserSubmissionParams; -}) => { - return ( - -
-
- -

- Submitters must satisfy at least one restriction to create an entry. -

-
- {userSubmitParams?.restrictionResults?.length ?? 0 > 0 ? ( -
- - - - - - - - - - - {userSubmitParams.restrictionResults.map((el, idx) => { - return ( - - - - - {el.result === true ? ( - - ) : ( - - )} - - ); - })} - -
restrictionsTypeThresholdStatus
{idx + 1}{el.restriction.tokenRestriction.token.type}{el.restriction.tokenRestriction.threshold} - - - -
-
- ) : ( -
-

no restrictions

-
- )} -
-
- ); -}; - - -const StandardSubmit = ({ - params, -}: { - params: { id: string }; -}) => { - const { - submission, - setSubmissionTitle, - setSubmissionBody, - setPreviewAsset, - setVideoAsset, - setErrors, - } = useStandardSubmissionCreator(); - const { contestState } = useContestState(); - const { title, submissionBody, errors } = submission; - const [isUploading, setIsUploading] = useState(false); - - if (!contestState) { - return ; - } else if (contestState !== "submitting") { - return

not in submit window

; - } else { - // contest in submit window - return ( -
-
- -
-
-
-

Create Submission

- -
- { setIsUploading(status) }} - ipfsImageCallback={(url) => setPreviewAsset(url)} - ipfsAnimationCallback={(url) => setVideoAsset(url)} - maxVideoDuration={140} - /> -
- -
-
-
- ); - } -}; - -export default StandardSubmit; diff --git a/uplink-client/src/app/contest/[id]/vote/page.tsx b/uplink-client/src/app/contest/[id]/vote/page.tsx deleted file mode 100644 index 12589d3d..00000000 --- a/uplink-client/src/app/contest/[id]/vote/page.tsx +++ /dev/null @@ -1,34 +0,0 @@ -import Link from "next/link"; -import { VoteTab } from "@/ui/Vote/Vote"; -import { HiArrowNarrowLeft } from "react-icons/hi"; -import { Button } from "@/ui/DesignKit/Button"; - -// this page offers an expanded view of the voting process for a given contest. -// it's mainly used for mobile devices only, but can be called from desktop as well. - -export default async function Page({ - params, -}: { - params: { id: string }; -}) { - return ( -
- - - - -
-

My Selections

- -
-
- ); -} diff --git a/uplink-client/src/app/explore/client.tsx b/uplink-client/src/app/explore/client.tsx index a33ac02f..fea84847 100644 --- a/uplink-client/src/app/explore/client.tsx +++ b/uplink-client/src/app/explore/client.tsx @@ -1,7 +1,7 @@ "use client";; import { Space } from "@/types/space"; import { useEffect, useRef, useState } from "react"; -import UplinkImage from "@/lib/UplinkImage"; +import OptimizedImage from "@/lib/OptimizedImage"; import Link from "next/link"; import { Input } from "@/ui/DesignKit/Input"; @@ -111,7 +111,7 @@ export const SearchSpaces = ({ allSpaces }: { allSpaces: Array }) => { className="flex flex-row items-center gap-2 p-2 hover:bg-base cursor-pointer rounded-lg" >
- { -// const { contestState, stateRemainingTime } = calculateContestStatus( -// contest.deadlines, -// contest.metadata.type, -// contest.tweetId -// ); -// return ( -// -//
-//
-//
-// -//
-//

-// {contest.space.displayName} -//

-//
-// -//
-// -// -//
-// -//
-// -// ); -// }; - -// const ActiveContests = async () => { -// const activeContests = await fetchActiveContests(); -// if (activeContests.length > 0) { -// return ( -//
-//
-//

Active Contests

-//
-// -// {activeContests.map((contest, index) => ( -//
-// -//
-// ))} -//
-//
-// ); -// } -// return null; -// }; - const TrendingChannels = async () => { let trendingChannels = await fetchTrendingChannels(8453) @@ -130,35 +58,36 @@ const TrendingChannels = async () => {
-
- -
+ + +
{channel.tokens[0].metadata.name} - + {/*
{channel.tokens.slice(1, 10).map(token => { return ( -
- -
+ ) })}
-
+ */}
@@ -192,7 +121,7 @@ const ActiveContests = async () => {
- { {channel.tokens.slice(1, 10).map(token => { return (
- { {space.displayName} -
- +
diff --git a/uplink-client/src/app/not-found.tsx b/uplink-client/src/app/not-found.tsx index 37b739df..8419360e 100644 --- a/uplink-client/src/app/not-found.tsx +++ b/uplink-client/src/app/not-found.tsx @@ -2,14 +2,14 @@ import { useRouter } from "next/navigation"; import loadingNoggles from "../../public/loading-noggles.svg"; import Link from "next/link"; -import UplinkImage from "@/lib/UplinkImage"; import { Button } from "@/ui/DesignKit/Button"; +import Image from "next/image"; export default function NotFound() { const router = useRouter(); return (
- + 404

thats a 404

{`We couldn't find what you were looking for`}

diff --git a/uplink-client/src/app/spacebuilder/SpaceForm.tsx b/uplink-client/src/app/spacebuilder/SpaceForm.tsx index 7f8b08c5..b11fc8e4 100644 --- a/uplink-client/src/app/spacebuilder/SpaceForm.tsx +++ b/uplink-client/src/app/spacebuilder/SpaceForm.tsx @@ -1,397 +1,250 @@ "use client";; -import { useState } from "react"; -import { HiTrash } from "react-icons/hi2"; -import { useReducer, useEffect } from "react"; +import { SpaceSettingsInput, SpaceSettingsOutput, SpaceSettingsStateT, useSpaceSettings } from "@/hooks/useSpaceReducer"; import { useSession } from "@/providers/SessionProvider"; -import { - reducer, - SpaceBuilderProps, - validateSpaceBuilderProps, - createSpace, - editSpace, -} from "@/app/spacebuilder/spaceHandler"; -import { useRouter } from "next/navigation"; -import toast from "react-hot-toast"; -import WalletConnectButton from "../../ui/ConnectButton/WalletConnectButton"; -import useSWRMutation from "swr/mutation"; -import { useSWRConfig } from "swr"; -import { mutateSpaces } from "../mutate"; -import { AvatarUpload } from "@/ui/MediaUpload/AvatarUpload"; -import { Label } from "@/ui/DesignKit/Label"; -import { Input } from "@/ui/DesignKit/Input"; import { Button } from "@/ui/DesignKit/Button"; +import { FormInput } from "@/ui/DesignKit/Form"; import { Info } from "@/ui/DesignKit/Info"; - -export default function SpaceForm({ - initialState, - isNewSpace, - referral, - spaceId, +import { Label } from "@/ui/DesignKit/Label"; +import { AvatarUpload } from "@/ui/MediaUpload/AvatarUpload"; +import { useEffect, useState } from "react"; +import { HiTrash } from "react-icons/hi2"; +import useSWRMutation from "swr/mutation"; +import { mutateSpaces } from "../mutate"; +import toast from "react-hot-toast"; +import WalletConnectButton from "@/ui/ConnectButton/WalletConnectButton"; +import { useRouter } from "next/navigation"; +import { insertSpace, InsertSpaceArgs } from "@/lib/fetch/insertSpace"; +import { updateSpace, UpdateSpaceArgs } from "@/lib/fetch/updateSpace"; + + +export const AdminRow = ({ + admins, + setField, + index, + isNewSpace, + error, + onDeleteRow, + onEditRow }: { - initialState: SpaceBuilderProps; - isNewSpace: boolean; - referral?: string; - spaceId?: string; -}) { - const [state, dispatch] = useReducer(reducer, initialState); - const router = useRouter(); - const { data: session, status } = useSession(); - const { mutate } = useSWRConfig(); - const [isUploading, setIsUploading] = useState(false); - const { trigger, error, isMutating, reset } = useSWRMutation( - isNewSpace - ? `/api/createContest/${spaceId}` - : `/api/editContest/${spaceId}`, - isNewSpace ? createSpace : editSpace, - { - onError: (err) => { - console.log(err); - toast.error( - "Oops, something went wrong. Please check the fields and try again." - ); - reset(); - }, - } - ); - - const validate = async () => { - const result = await validateSpaceBuilderProps(state); - - if (!result.isValid) { - dispatch({ - type: "setTotalState", - payload: { spaceBuilderData: result.values, errors: result.errors }, - }); - } - - return result; - }; - - const onFormSubmit = async () => { - const { isValid, values } = await validate(); - if (!isValid) return; - - try { - await trigger({ - ...(!isNewSpace && { spaceId: spaceId }), - spaceData: values, - csrfToken: session.csrfToken, - }).then(({ success, spaceName, errors }) => { - if (success) { - mutateSpaces(spaceName); - toast.success( - isNewSpace - ? "Space created successfully!" - : "Successfully saved your changes", - { - icon: "🚀", + admins: string[]; + setField: any; + index: number; + isNewSpace: boolean; + error?: string; + onDeleteRow: () => void; + onEditRow: (val: string) => void; +}) => { + const { data: session, status } = useSession(); + + // // the user can never remove themself as an admin (if they are one) + // // on session change, check if the user is an admin. if they are, lock the row and set the address + // // if they aren't, unlock the row for editing + + const isLocked = isNewSpace + ? index === 0 + : status === "authenticated" + ? session?.user?.address === admins[index] + : true; + + useEffect(() => { + if (isNewSpace) { + // set the session / address if the user is signed in and is an admin + if (status === "authenticated" && admins[index] === "you") { + setField("admins", admins.map((a, i) => i === index ? session?.user?.address : a)); } - ); - router.refresh(); - router.push(`/${spaceName}`); - } else { - // set the errors - dispatch({ - type: "setErrors", - payload: { - ...(errors?.name && { name: errors.name }), - ...(errors?.website && { website: errors.website }), - ...(errors?.twitter && { twitter: errors.twitter }), - ...(errors?.logoUrl && { logoUrl: errors.logoUrl }), - }, - }); - toast.error( - "Oops, something went wrong. Please check the fields and try again." - ); } - }); - } catch (e) { - reset(); - } - }; - - return ( -
-
-

Space Builder

- {referral === 'home' && ( - - Spaces are like profiles for your organization, project, community, or yourself! After creating a space, you can create contests and mintboards inside of it. - - )} - - - setIsUploading(status)} - ipfsImageCallback={(url) => { - if (url) { - dispatch({ - type: "setLogoUrl", - payload: url, - }) - } - }} - error={state.errors.logoUrl} - /> - - - {/* */} - -
- - + )}
- - -
-
- ); -} - -const SpaceName = ({ - state, - dispatch, -}: { - state: SpaceBuilderProps; - dispatch: any; -}) => { - return ( -
- - { - dispatch({ - type: "setSpaceName", - payload: e.target.value, - }); - }} - placeholder="Nouns" - className="max-w-xs" - /> - {state.errors?.name && ( - - )} -
- ); -}; - -const SpaceWebsite = ({ - state, - dispatch, -}: { - state: SpaceBuilderProps; - dispatch: any; -}) => { - return ( -
- - { - dispatch({ - type: "setWebsite", - payload: e.target.value, - }); - }} - placeholder="nouns.wtf" - className="max-w-xs" - /> - {state.errors?.website && ( - - )} -
- ); +
+ ); }; -const SpaceTwitter = ({ - state, - dispatch, -}: { - state: SpaceBuilderProps; - dispatch: any; -}) => { - return ( -
- - { - dispatch({ - type: "setTwitter", - payload: e.target.value, - }); - }} - placeholder="@nounsdao" - className="max-w-xs" - /> - {state.errors?.twitter && ( - - )} -
- ); -}; - -const SpaceAdmins = ({ - state, - dispatch, - isNewSpace, -}: { - state: SpaceBuilderProps; - dispatch: any; - isNewSpace: boolean; -}) => { - return ( -
- -
- {state.admins.map((admin: string, index: number) => { - return ( - - ); - })} - -
-
- ); -}; - -const AdminRow = ({ - error, - dispatch, - admin, - index, - isNewSpace, -}: { - error: string; - dispatch: any; - admin: string; - index: number; - isNewSpace: boolean; -}) => { - const { data: session, status } = useSession(); + } catch (e) { + reset(); + } + } - // the user can never remove themself as an admin (if they are one) - // on session change, check if the user is an admin. if they are, lock the row and set the address - // if they aren't, unlock the row for editing + return ( +
+
+

Space Builder

+ + {isNewSpace && + + Spaces are like profiles for your organization, project, community, or yourself! After creating a space, you can create contests and mintboards inside of it. + + } + + setIsUploading(status)} + ipfsImageCallback={(url) => { + if (url) { + setField("logoUrl", url) + } + }} + error={state.errors?.logoUrl?._errors} + /> - const isLocked = isNewSpace - ? index === 0 - : status === "authenticated" - ? session?.user?.address === admin - : true; + setField("name", e.target.value)} + error={state.errors?.name?._errors[0]} + /> - // set the - useEffect(() => { - if (isNewSpace) { - // set the session / address if the user is signed in and is an admin - if (status === "authenticated" && admin === "you") { - dispatch({ - type: "setAdmin", - payload: { - index: index, - value: session?.user?.address, - }, - }); - } - } - }, [status, session?.user?.address]); + setField("website", e.target.value)} + error={state.errors?.website?._errors[0]} + /> - return ( -
-
- - dispatch({ - type: "setAdmin", - payload: { index: index, value: e.target.value }, - }) - } - /> - {!isLocked && ( - +
+
+ + + +
+ + + - > - - - )} -
- {error && ( - - )} -
- ); -}; +
+
+ ); +} \ No newline at end of file diff --git a/uplink-client/src/app/spacebuilder/create/page.tsx b/uplink-client/src/app/spacebuilder/create/page.tsx index eca80c85..cfda7261 100644 --- a/uplink-client/src/app/spacebuilder/create/page.tsx +++ b/uplink-client/src/app/spacebuilder/create/page.tsx @@ -1,18 +1,15 @@ -import SpaceForm from "@/app/spacebuilder/SpaceForm"; +import { SpaceForm } from "@/app/spacebuilder/SpaceForm"; +import { SpaceSettingsStateT } from "@/hooks/useSpaceReducer"; export default function Page({ searchParams }: { searchParams: { [key: string]: string | undefined } }) { - const initialState = { + const initialState: SpaceSettingsStateT = { name: "", - logoBlob: "", logoUrl: "", website: "", - twitter: "", admins: ["you", ""], - errors: { - admins: [], - }, + errors: {}, }; - return ; + return ; } diff --git a/uplink-client/src/app/spacebuilder/edit/[name]/page.tsx b/uplink-client/src/app/spacebuilder/edit/[name]/page.tsx index 51cbb3a5..4b0acdaf 100644 --- a/uplink-client/src/app/spacebuilder/edit/[name]/page.tsx +++ b/uplink-client/src/app/spacebuilder/edit/[name]/page.tsx @@ -1,24 +1,22 @@ -import SpaceForm from "@/app/spacebuilder/SpaceForm"; +import { SpaceForm } from "@/app/spacebuilder/SpaceForm"; +import { SpaceSettingsStateT } from "@/hooks/useSpaceReducer"; import fetchSingleSpace from "@/lib/fetch/fetchSingleSpace"; import { Admin } from "@/types/space"; export default async function Page({ params }: { params: { name: string } }) { const spaceData = await fetchSingleSpace(params.name); - const initialState = { + const initialState: SpaceSettingsStateT = { ...spaceData, name: spaceData.displayName, - logoBlob: spaceData.logoUrl, + logoUrl: spaceData.logoUrl, admins: spaceData.admins.map((admin: Admin) => admin.address), - errors: { - admins: Array(spaceData.admins.length).fill(null), - }, + errors: {}, }; return ( ); diff --git a/uplink-client/src/app/spacebuilder/spaceHandler.ts b/uplink-client/src/app/spacebuilder/spaceHandler.ts deleted file mode 100644 index 0c13cfb9..00000000 --- a/uplink-client/src/app/spacebuilder/spaceHandler.ts +++ /dev/null @@ -1,360 +0,0 @@ -import { validateEthAddress } from "@/lib/ethAddress"; -import { handleMutationError } from "@/lib/handleMutationError"; - -export type FormField = { - value: string; - error: string | null; -}; - -export type SpaceBuilderErrors = { - name?: string; - logoUrl?: string; - website?: string; - twitter?: string; - admins: (string | null)[]; -}; - -export type SpaceBuilderProps = { - name: string; - logoUrl: string; - logoBlob: string; - website?: string; - twitter?: string; - admins: string[]; - errors: SpaceBuilderErrors; -}; - -export const reducer = (state: SpaceBuilderProps, action: any) => { - switch (action.type) { - - case "setSpaceName": - return { - ...state, - name: action.payload, - errors: { ...state.errors, name: null }, - }; - case "setLogoBlob": - return { - ...state, - logoBlob: action.payload, - }; - case "setLogoUrl": - return { - ...state, - logoUrl: action.payload, - errors: { ...state.errors, logoUrl: null }, - }; - - case "setWebsite": { - const { website: websiteError, ...otherErrors } = state.errors; - - return { - ...state, - website: action.payload !== "" ? action.payload : undefined, - errors: otherErrors, - }; - } - - case "setTwitter": { - const { twitter: twitterError, ...otherErrors } = state.errors; - - return { - ...state, - twitter: action.payload !== "" ? action.payload : undefined, - errors: otherErrors, - }; - } - - case "setPfp": - return { - ...state, - pfp: action.payload, - errors: { ...state.errors, pfp: null }, - }; - case "addAdmin": - return { - ...state, - admins: [...state.admins, ""], - errors: { ...state.errors, admins: [...state.errors.admins, null] }, - }; - case "removeAdmin": - return { - ...state, - admins: state.admins.filter( - (admin: string, index: number) => index !== action.payload - ), - errors: { - ...state.errors, - admins: state.errors.admins.filter( - (admin: string | null, index: number) => index !== action.payload - ), - }, - }; - case "setAdmin": - return { - ...state, - admins: state.admins.map((admin: string, index: number) => - index === action.payload.index ? action.payload.value : admin - ), - errors: { - ...state.errors, - admins: state.errors.admins.map((admin: string | null, index: number) => - index === action.payload.index ? null : admin - ), - }, - }; - case "setErrors": { - return { - ...state, - errors: { - ...state.errors, - ...action.payload - }, - }; - } - case "setTotalState": - return { - ...state, - ...action.payload.spaceBuilderData, - errors: { - ...state.errors, - ...action.payload.errors - } - }; - default: - return state; - } -}; - - -export const validateSpaceName = (name: SpaceBuilderProps['name']): { error: string | null, value: SpaceBuilderProps['name'] } => { - - const cleanedName = name.trim(); - - if (!cleanedName) return { value: cleanedName, error: "Name is required" }; - if (cleanedName.length < 3) return { value: cleanedName, error: "Name must be at least 3 characters long" } - if (cleanedName.length > 30) return { value: cleanedName, error: "Name must be less than 30 characters long" } - if (!cleanedName.match(/^[a-zA-Z0-9_ ]+$/)) return { value: cleanedName, error: "Name must only contain alphanumeric characters and underscores" } - - return { - error: null, - value: cleanedName - } -} - -export const validateSpaceLogo = (logoUrl: SpaceBuilderProps['logoUrl']): { error: string | null, value: SpaceBuilderProps['logoUrl'] } => { - - if (!logoUrl) return { value: logoUrl, error: "Logo is required" }; - const pattern = /^https:\/\/uplink\.mypinata\.cloud\/ipfs\/[a-zA-Z0-9]+/; - if (!pattern.test(logoUrl)) return { value: logoUrl, error: "Logo is not valid" }; - - return { - error: null, - value: logoUrl - }; -} - -export const validateSpaceWebsite = (website: SpaceBuilderProps['website']): { error: string | null, value?: SpaceBuilderProps['website'] } => { - - if (!website) return { error: null }; - - const cleanedWebsite = website.trim().toLowerCase(); - - const pattern = /^(https?:\/\/)?(www\.)?([a-zA-Z0-9]+)\.([a-z]{2,})(\.[a-z]{2,})?$/; - if (!pattern.test(website)) return { value: cleanedWebsite, error: "Website is not valid" }; - - return { - error: null, - value: cleanedWebsite - }; -} - -export const validateSpaceTwitter = (twitter: SpaceBuilderProps['twitter']): { error: string | null, value?: SpaceBuilderProps['twitter'] } => { - - if (!twitter) return { error: null }; - - const cleanedTwitter = twitter.trim().toLowerCase(); - - const pattern = /^@(\w){1,15}$/; - if (!pattern.test(twitter)) return { value: cleanedTwitter, error: "Twitter handle is not valid" }; - - return { - error: null, - value: cleanedTwitter - }; -} - - - -/** - * - * need to return the cleaned addresses array and the errors array - * errors should be in the same order as the addresses - * - */ - -export const validateSpaceAdmins = async (admins: SpaceBuilderProps['admins']): Promise<{ error: SpaceBuilderErrors['admins'], value: SpaceBuilderProps['admins'] }> => { - type adminField = { - value: string, - error: string | null - } - - const promises = admins.map(async (admin) => { - const field: adminField = { - value: admin, - error: null - } - - if (!field.value || field.value.length === 0) { - return null; - } - - const cleanAddress = await validateEthAddress(field.value); - - if (!cleanAddress) { - field.error = "invalid address"; - return field; - } - - field.value = cleanAddress; - return field; - }) - - const adminFields = await Promise.all(promises); - - - // Filter out undefined and null fields, and remove duplicates - const uniqueAdminFields = adminFields.reduce((acc: adminField[], field) => { - if (field && !acc.some(item => item.value === field.value)) { - acc.push(field); - } - return acc; - }, []); - - // Store errors and values in separate arrays - const errors = uniqueAdminFields.map(field => field.error); - const values = uniqueAdminFields.map(field => field.value); - - // Return the result - return { error: errors, value: values }; - -} - - - -export const validateSpaceBuilderProps = async (props: SpaceBuilderProps) => { - const { error: nameError, value: nameValue } = validateSpaceName(props.name); - const { error: logoErrors, value: logoValue } = validateSpaceLogo(props.logoUrl); - const { error: websiteErrors, value: websiteValue } = validateSpaceWebsite(props.website); - const { error: twitterErrors, value: twitterValue } = validateSpaceTwitter(props.twitter); - const { error: adminsErrors, value: adminsValue } = await validateSpaceAdmins(props.admins); - - const errors = { - ...(nameError ? { name: nameError } : {}), - ...(logoErrors ? { logoUrl: logoErrors } : {}), - ...(websiteErrors ? { website: websiteErrors } : {}), - ...(twitterErrors ? { twitter: twitterErrors } : {}), - admins: adminsErrors - } - - const values = { - name: nameValue, - logoUrl: logoValue, - ...(websiteValue ? { website: websiteValue } : {}), - ...(twitterValue ? { twitter: twitterValue } : {}), - admins: adminsValue, - } - - - const { admins, ...otherErrors } = errors; - - const hasAdminErrors = admins.some((admin) => typeof admin === 'string'); - return { - isValid: Object.keys(otherErrors).length === 0 && !hasAdminErrors, - errors, - values - } - -} - - -export const createSpace = async ( - url, - { - arg, - }: { - arg: any; - } -) => { - return fetch(`${process.env.NEXT_PUBLIC_HUB_URL}/graphql`, { - method: "POST", - headers: { - "Content-Type": "application/json", - "X-CSRF-TOKEN": arg.csrfToken, - }, - credentials: "include", - body: JSON.stringify({ - query: ` - mutation CreateSpace($spaceData: SpaceBuilderInput!){ - createSpace(spaceData: $spaceData){ - spaceName - success - errors{ - name - logoUrl - twitter - website - admins - } - } - }`, - variables: { - spaceData: arg.spaceData, - }, - }), - }) - .then((res) => res.json()) - .then(handleMutationError) - .then((res) => res.data.createSpace); -}; - -export const editSpace = async ( - url, - { - arg, - }: { - arg: any; - } -) => { - return fetch(`${process.env.NEXT_PUBLIC_HUB_URL}/graphql`, { - method: "POST", - headers: { - "Content-Type": "application/json", - "X-CSRF-TOKEN": arg.csrfToken, - }, - credentials: "include", - body: JSON.stringify({ - query: ` - mutation EditSpace($spaceId: ID!, $spaceData: SpaceBuilderInput!){ - editSpace(spaceId: $spaceId, spaceData: $spaceData){ - spaceName - success - errors{ - name - logoUrl - twitter - website - admins - } - } - }`, - variables: { - spaceId: arg.spaceId, - spaceData: arg.spaceData, - }, - }), - }) - .then((res) => res.json()) - .then(handleMutationError) - .then((res) => res.data.editSpace); -}; \ No newline at end of file diff --git a/uplink-client/src/app/user/[identifier]/client.tsx b/uplink-client/src/app/user/[identifier]/client.tsx deleted file mode 100644 index 09dfc51c..00000000 --- a/uplink-client/src/app/user/[identifier]/client.tsx +++ /dev/null @@ -1,319 +0,0 @@ -// "use client" -// import useMe from "@/hooks/useMe" -// import { ZoraAbi, getContractFromChainId } from "@/lib/abi/zoraEdition"; -// import { getChainName, supportedChains } from "@/lib/chains/supportedChains"; -// import { TokenContractApi } from "@/lib/contract"; -// import { useSession } from "@/providers/SessionProvider"; -// import { ChainLabel } from "@/ui/ContestLabels/ContestLabels"; -// import { UserSubmissionDisplay } from "@/ui/Submission/SubmissionDisplay"; -// import { useEffect, useState } from "react"; -// import { Decimal } from 'decimal.js' -// import toast from "react-hot-toast"; -// import { TbLoader2 } from "react-icons/tb"; -// import Link from "next/link"; -// import { AddFundsButton, SwitchNetworkButton } from "@/ui/Zora/common"; -// import { Submission } from "@/types/submission"; -// import WalletConnectButton from "@/ui/ConnectButton/WalletConnectButton"; -// import { User } from "@/types/user"; -// import { HiPencil } from "react-icons/hi2"; -// import { FaTwitter } from "react-icons/fa"; -// import { MdOutlineCancelPresentation } from "react-icons/md"; -// import UplinkImage from "@/lib/UplinkImage" - -// export const ManageAccountButton = ({ }) => { - -// } - -// const hasProfile = (user: User) => { -// return user.userName && user.displayName -// } - -// export const RewardsSkeleton = () => { -// return ( -//
-//
-//
-//
-//
-//
-//
-//
-//
-//
-//
-//
-//
-//
-//
-//
-//
-//
-// ) -// } - -// export const ClientUserProfile = ({ accountAddress }: { accountAddress: string }) => { -// const { me: user, isMeLoading, isMeError } = useMe(accountAddress); -// return ( -//
-//
-//
-// {user.profileAvatar ? () : ( -//
-// )} -//
- -//
-//
-// {hasProfile(user) ? ( -// <> -//
-//

{user.displayName}

-//

{user.userName}

-// -// -// - -//
-// -// ) : ( -//
-// Set up profile -//
-// )} - -//
-// {hasProfile(user) ? ( -// <> -//
-//

{user.displayName}

-//

{user.userName}

-//
-// -// -// -// -// ) : ( -//
-// Set up profile -//
-// )} - -// {user.twitterHandle && user.visibleTwitter && ( -//
-// -// -// {user.twitterHandle} -// -//
-// )} -// {!hasProfile && } -//
-//
-//
-// -//
-// ) -// } - -// export const UserSubmissions = ({ accountAddress, isMintableOnly }: { accountAddress: string, isMintableOnly: boolean }) => { -// const { me, isMeLoading, isMeError } = useMe(accountAddress); -// const filteredSubs = isMintableOnly ? me.submissions.filter((el: Submission) => el.edition) : me.submissions - -// const user: User = { -// ...me, -// submissions: filteredSubs -// } - -// return -// } - -// const useClaimableBalance = (chainId: number, contractAddress: string) => { -// const [balance, setBalance] = useState(null); -// const [triggerRefresh, setTriggerRefresh] = useState(0); -// const { data: session, status } = useSession(); -// const tokenApi = new TokenContractApi(chainId); - -// const getBalance = (userAddress: string) => { -// tokenApi.zoraGetRewardBalance({ contractAddress, userAddress }).then(balance => { -// setBalance(balance.toString()); -// }) -// } - -// useEffect(() => { -// if (session?.user?.address) { -// getBalance(session?.user?.address) -// } - -// }, [session?.user?.address, triggerRefresh]) - -// return { -// balance, -// isLoading: balance === null, -// triggerRefresh: () => { -// setTriggerRefresh(triggerRefresh + 1) -// } -// } -// } - - -// const ProtocolRewards = ({ accountAddress }: { accountAddress: string }) => { -// const { me, isUserAuthorized, isMeLoading, isMeError } = useMe(accountAddress); -// if (isMeLoading) return -// if (!isMeLoading && !isUserAuthorized) return null; -// return ( -//
-//

Protocol Rewards

-// {supportedChains.map(chain => { -// return -// })} -//
-// ) -// } - - -// export const ClaimableUserRewards = ({ accountAddress, chainId }: { accountAddress: string, chainId: number }) => { -// const { data: session, status } = useSession(); -// const { rewards_contract } = getContractFromChainId(chainId); -// const { balance, isLoading: isBalanceLoading, triggerRefresh } = useClaimableBalance(chainId, rewards_contract) -// const [isModalOpen, setIsModalOpen] = useState(false); - -// return ( - -//
-//
-//

{getChainName(chainId)}

-// -//
- -//
-// {isBalanceLoading ? -// () -// : -// (
{`${new Decimal(balance).div(Decimal.pow(10, 18)).toString()} ETH`}
) -// } -//
-//
-// {!isBalanceLoading && new Decimal(balance).greaterThan(0) ? ( -// -// ) : ( -// -// )} -//
-// setIsModalOpen(false)} chainId={chainId} claimableBalance={balance} rewardsContract={rewards_contract} triggerRefresh={triggerRefresh} /> -//
- -// ) -// } - - - - -// const ClaimRewardsModal = ({ isModalOpen, onClose, chainId, claimableBalance, rewardsContract, triggerRefresh }) => { -// const { data: session, status } = useSession(); -// const { config, error: prepareError, isError: isPrepareError } = usePrepareContractWrite({ -// //chainId: chainId, -// address: rewardsContract, -// abi: ZoraAbi, -// functionName: 'withdraw', -// args: [session?.user?.address, BigInt(claimableBalance || '0')], -// enabled: true, -// }); - -// const isInsufficientFundsError = isPrepareError ? prepareError.message.includes("insufficient funds for gas * price + value") : false; - -// const { -// data, -// write, -// isLoading: isWriteLoading, -// error: writeError, -// isError: isWriteError -// } = useContractWrite({ -// ...config, -// onError(err) { -// if (err.message.includes("User rejected the request")) { -// toast.error("Signature request rejected") -// } -// } -// }); - -// const { isLoading: isTxPending, isSuccess: isTxSuccessful } = useWaitForTransaction({ -// hash: data?.hash, -// onSettled: (data, err) => { -// if (err) { -// console.log(err) -// return toast.error('Error claiming rewards') -// } -// if (data) { -// toast.success('Successfully claimed your rewards') -// } -// }, - -// }); - -// useEffect(() => { -// if (isTxSuccessful) { -// triggerRefresh(); -// toast.success('Successfully claimed your rewards') -// onClose(); -// } -// }, [isTxSuccessful]) - - -// if (isModalOpen) { -// return ( -//
-//
-//
-//
-//

Claim Rewards

-// -//
-// -//

Nice work! You have {`${new Decimal(claimableBalance).div(Decimal.pow(10, 18)).toString()} ETH`} in protocol rewards on {getChainName(chainId)}.

-// {isInsufficientFundsError -// ? -// () -// : -// -// -// -// } -//
-//
-//
-// ); -// } -// return null; -// } \ No newline at end of file diff --git a/uplink-client/src/app/user/[identifier]/page.tsx b/uplink-client/src/app/user/[identifier]/page.tsx deleted file mode 100644 index bc6fcf3a..00000000 --- a/uplink-client/src/app/user/[identifier]/page.tsx +++ /dev/null @@ -1,139 +0,0 @@ -import fetchUser from "@/lib/fetch/fetchUser"; -import { Submission } from "@/types/submission"; -import { User, UserIdentifier, isUserAddress } from "@/types/user"; -import { SubmissionDisplaySkeleton, UserSubmissionDisplay } from "@/ui/Submission/SubmissionDisplay"; -import Image from "next/image"; -import Link from "next/link"; -import { FaTwitter } from "react-icons/fa6"; -// import { ClaimableUserRewards, ClientUserProfile, RewardsSkeleton, UserSubmissions } from "./client"; -import SwrProvider from "@/providers/SwrProvider"; -import { Suspense } from "react"; -import { useSession } from "@/providers/SessionProvider"; -import { HiPencil } from "react-icons/hi2"; - - -const SuspendableUserCard = async ({ userPromise, accountAddress }: { userPromise: Promise, accountAddress: string }) => { - - const user = await userPromise; - const fallback = { - [`me/${user.address}`]: user, - }; - return ( - -

loading

- {/* */} -
- ) -} -const SuspendableUserSubmissions = async ({ userPromise, isMintableOnly }: { userPromise: Promise, isMintableOnly: boolean }) => { - - const user = await userPromise; - const fallback = { - [`me/${user.address}`]: user, - }; - return ( - -
- {/*

Collection

*/} - {user.submissions.length > 0 &&
-
- - All - - {!isMintableOnly &&
} -
-
- - - Drops - - {isMintableOnly &&
} -
-
- } - {/* */} -
- - ) -} - -const UserSubmissionSkeleton = () => { - return ( -
- {/*

Collection

*/} -
-
-
-
-
-
-
-
- -
- ) -} - -const UserCardSkeleton = () => { - return ( -
-
-
-
-
-
-
-
-
-
-
-
-
- {/* */} - {/* */} -
- ) -} - - - -export default async function Page({ params, searchParams }: { params: { identifier: UserIdentifier }, searchParams: { [key: string]: string | string[] | undefined } }) { - // const userPromise = fetchUser(params.identifier) - // const isMintableOnly = searchParams?.drops === 'true' - // return ( - //
- //
- // }> - // - // - // }> - // - // - //
- //
- // ) - - return ( -
-

Under Construction

- construction -

Profiles will be back soon!

-
- ) - - -} diff --git a/uplink-client/src/app/user/[identifier]/settings/page.tsx b/uplink-client/src/app/user/[identifier]/settings/page.tsx deleted file mode 100644 index 10b7eac5..00000000 --- a/uplink-client/src/app/user/[identifier]/settings/page.tsx +++ /dev/null @@ -1,26 +0,0 @@ -import fetchUser from "@/lib/fetch/fetchUser" -import { UserIdentifier } from "@/types/user" -import WalletConnectButton from "@/ui/ConnectButton/WalletConnectButton" -import Settings from "./settings" -import SwrProvider from "@/providers/SwrProvider" - -export default async function Page({ params }: { params: { identifier: UserIdentifier } }) { - const user = await fetchUser(params.identifier) - const fallback = { - [`me/${user.address}`]: user, - }; - return ( -
-
-
-

Profile

- - - - - -
-
-
- ) -} \ No newline at end of file diff --git a/uplink-client/src/app/user/[identifier]/settings/settings.tsx b/uplink-client/src/app/user/[identifier]/settings/settings.tsx deleted file mode 100644 index 3ca9b1e3..00000000 --- a/uplink-client/src/app/user/[identifier]/settings/settings.tsx +++ /dev/null @@ -1,285 +0,0 @@ -"use client"; -import useMe from "@/hooks/useMe"; -import { handleMutationError } from "@/lib/handleMutationError"; -import { useSession } from "@/providers/SessionProvider"; -import { useRouter } from "next/navigation"; -import { useReducer, useState } from "react"; -import toast from "react-hot-toast"; -import { TbLoader2 } from "react-icons/tb"; -import useSWRMutation from "swr/mutation"; -import { z } from "zod"; -import { AvatarUpload } from "@/ui/MediaUpload/AvatarUpload"; -import { Label } from "@/ui/DesignKit/Label"; -import { Input } from "@/ui/DesignKit/Input"; -import { Button } from "@/ui/DesignKit/Button"; -import Toggle from "@/ui/DesignKit/Toggle"; - -const configurableUserSettings = z.object({ - profileAvatarUrl: z.string().min(1, { message: "profile picture is required" }), - profileAvatarBlob: z.string(), - displayName: z.string().min(3, { message: "display name must contain at least 3 characters" }).max(20, { message: "display name must not exceed 20 characters" }), - visibleTwitter: z.boolean(), -}) - -type ZodSafeParseErrorFormat = { - [key: string]: { _errors: string[] }; -}; - -type ConfigurableUserSettings = z.infer; - -const BasicInput = ({ value, label, placeholder, onChange, error, inputType }) => { - return ( -
- - e.currentTarget.blur()} - spellCheck="false" - value={value} - onChange={onChange} - placeholder={placeholder} - className="w-full max-w-xs" - /> - {error && ( - - )} -
- ) -} - -const reducer = ( - state: ConfigurableUserSettings & { errors?: ZodSafeParseErrorFormat }, - action: any -) => { - switch (action.type) { - case "SET_FIELD": - return { - ...state, - [action.payload.field]: action.payload.value, - errors: { ...state.errors, [action.payload.field]: undefined }, // Clear error when field is set - }; - case "SET_ERRORS": - return { ...state, errors: action.payload }; - default: - return state; - } -} - -const TwitterDisplayToggle = ({ - state, - dispatch, -}: { - state: ConfigurableUserSettings; - dispatch: any; -}) => { - - return ( -
-
-

Show Twitter Handle

-

- Different parts of uplink may collect your twitter handle. Should we display it publicly in your profile? -

-
-
- - dispatch({ type: "setDisplayTwitter", payload: !isSelected }) - } - /> -
-
- ); - -} - - -const postUser = async (url, - { - arg, - }: { - url: string; - arg: { - displayName: string; - profileAvatar: string; - visibleTwitter: boolean; - csrfToken: string; - } - } -) => { - return fetch(`${process.env.NEXT_PUBLIC_HUB_URL}/graphql`, { - method: "POST", - headers: { - "Content-Type": "application/json", - "X-CSRF-TOKEN": arg.csrfToken, - }, - credentials: "include", - body: JSON.stringify({ - query: ` - mutation UpdateUser($displayName: String!, $profileAvatar: String!, $visibleTwitter: Boolean!){ - updateUser(displayName: $displayName, profileAvatar: $profileAvatar, visibleTwitter: $visibleTwitter){ - success - } - }`, - variables: { - csrfToken: arg.csrfToken, - displayName: arg.displayName, - profileAvatar: arg.profileAvatar, - visibleTwitter: arg.visibleTwitter, - }, - }), - }) - .then((res) => res.json()) - .then(handleMutationError) - .then((res) => res.data.updateUser); -} - -const validateForm = (state: ConfigurableUserSettings, dispatch: any) => { - const result = configurableUserSettings.safeParse(state); - - if (!result.success) { - // Formatting errors and dispatching - const formattedErrors = (result as z.SafeParseError).error.format(); - dispatch({ - type: "SET_ERRORS", - payload: formattedErrors, // Pass the formatted errors directly - }); - } - - return result; -} - -const LoadingDialog = () => { - return ( -
-

Getting ready ...

-
-
- ); -}; - -const Settings = ({ accountAddress }: { accountAddress: string }) => { - const { data: session, status } = useSession(); - const [isUploading, setIsUploading] = useState(false); - const { me: user, isMeLoading, mutateMe } = useMe(accountAddress) - const router = useRouter(); - const [state, dispatch] = useReducer(reducer, { - profileAvatarUrl: user.profileAvatar || "", - profileAvatarBlob: user.profileAvatar || "", - displayName: user.displayName || "", - visibleTwitter: user.visibleTwitter || true, - errors: {} - }); - - const { trigger, data, error, isMutating, reset } = useSWRMutation( - `/api/updateUser/${user.id}}`, - postUser, - { - onError: (err) => { - console.log(err); - reset(); - }, - } - ); - const onSubmit = async () => { - const result = validateForm(state, dispatch); - if (result.success) { - - try { - trigger({ - profileAvatar: result.data.profileAvatarUrl, - displayName: result.data.displayName, - visibleTwitter: result.data.visibleTwitter, - csrfToken: session.csrfToken, - }).then((response) => { - - if (!response.success) { - reset(); - } - - toast.success('Profile updated!') - mutateMe(); - router.refresh(); - router.push(`/user/${user.address}`); - return; - }); - } catch (e) { - console.log(e) - reset(); - } - - } - } - - if (status === 'loading' || isMeLoading) return - if (status === 'authenticated') { - if (session?.user?.address !== user.address) { - return ( -
-

You are not the owner of this account

-
- ) - } else { - return ( -
-
- setIsUploading(status)} - ipfsImageCallback={(url) => { - if (url) { - dispatch({ - type: "SET_FIELD", - payload: { field: "profileAvatarUrl", value: url }, - }); - } - }} - error={state.errors?.profileAvatarUrl?._errors.join(",")} - /> - dispatch({ type: "SET_FIELD", payload: { field: "displayName", value: e.target.value } })} - error={state.errors?.displayName?._errors} - inputType="text" - /> - -
-
- -
- ) - } - } - -} - -export default Settings; \ No newline at end of file diff --git a/uplink-client/src/hooks/useContestInteractionAPI.ts b/uplink-client/src/hooks/useContestInteractionAPI.ts deleted file mode 100644 index 31c279b6..00000000 --- a/uplink-client/src/hooks/useContestInteractionAPI.ts +++ /dev/null @@ -1,244 +0,0 @@ -import { useSession } from "@/providers/SessionProvider"; -import { useContestState } from "@/providers/ContestStateProvider"; -import useSWR from "swr"; -import { IToken } from "@/types/token"; - -export type UserSubmissionParams = { - maxSubPower: string; - remainingSubPower: string; - userSubmissions: { id: string }[]; - restrictionResults: { - result: boolean; - restriction: { - restrictionType: string; - tokenRestriction: { - token: IToken; - threshold: string; - }; - }; - }[]; -}; - -export type UserVote = { - id: string; - votes: string; - submissionId: string; - submissionUrl: string; - -}; - -export type UserVotingParams = - | { - totalVotingPower: string; - votesRemaining: string; - votesSpent: string; - userVotes: Array; - } - | undefined; - -export interface ContestInteractionProps { - userSubmitParams: UserSubmissionParams; - areUserSubmitParamsLoading: boolean; - isUserSubmitParamsError: any; - userVoteParams: UserVotingParams; - areUserVotingParamsLoading: boolean; - isUserVotingParamsError: any; - mutateUserVotingParams: any; //(newParams: UserVotingParams, options?: any) => void; - downloadGnosisResults: () => void; -} - -// fetcher functions - -const getUserSubmissionParams = async ( - contestId: string, - csrfToken: string | null -) => { - return fetch(`${process.env.NEXT_PUBLIC_HUB_URL}/graphql`, { - method: "POST", - credentials: "include", - headers: { - "Content-Type": "application/json", - "X-CSRF-TOKEN": csrfToken, - }, - body: JSON.stringify({ - query: ` - query GetUserSubmissionParams($contestId: ID!) { - getUserSubmissionParams(contestId: $contestId) { - maxSubPower - remainingSubPower - userSubmissions { - id - } - restrictionResults { - result - restriction { - restrictionType - tokenRestriction { - token { - address - decimals - symbol - tokenId - type - } - threshold - } - } - } - } - }`, - variables: { - contestId, - }, - }), - }) - .then((res) => res.json()) - .then((res) => res.data.getUserSubmissionParams) - .catch((err) => { - return { - userSubmissions: [], - maxSubPower: 0, - remainingSubPower: 0, - }; - }); -}; - -const getUserVotingParams = async ( - contestId: string, - csrfToken: string | null -) => { - const response = await fetch(`${process.env.NEXT_PUBLIC_HUB_URL}/graphql`, { - method: "POST", - credentials: "include", - headers: { - "Content-Type": "application/json", - "X-CSRF-TOKEN": csrfToken, - }, - body: JSON.stringify({ - query: ` - query getUserVotingParams($contestId: ID!) { - getUserVotingParams(contestId: $contestId) { - totalVotingPower - votesRemaining - votesSpent - userVotes { - votes - submissionId - submissionUrl - } - } - }`, - variables: { - contestId, - }, - }), - }) - .then((res) => res.json()) - .then((res) => res.data.getUserVotingParams) - .catch((err) => { - return { - totalVotingPower: "0", - votesRemaining: "0", - votesSpent: "0", - userVotes: [], - }; - }); - return response; -}; - -const getGnosisResults = async (contestId: string) => { - const data = await fetch(`${process.env.NEXT_PUBLIC_HUB_URL}/graphql`, { - method: "POST", - headers: { - "Content-Type": "application/json", - }, - body: JSON.stringify({ - query: ` - query Query($contestId: ID!){ - contest(contestId: $contestId){ - gnosisResults - } - }`, - variables: { - contestId, - }, - }), - }) - .then((res) => res.json()) - .then((res) => res.data.contest.gnosisResults); - - return data; -}; - - -export const useContestInteractionApi = (contestId: string) => { - - const { data: session, status } = useSession(); - const { contestState } = useContestState(); - const isAuthed = status === "authenticated"; - const isSubmitPeriod = contestState === "submitting"; - const isVotingPeriod = contestState === "voting"; - - const submitParamsSwrKey = - isAuthed && isSubmitPeriod && session?.user?.address - ? [`/api/userSubmitParams/${contestId}`, session.user.address] - : null; - const voteParamsSwrKey = - isAuthed && isVotingPeriod && session?.user?.address - ? [`/api/userVotingParams/${contestId}`, session.user.address] - : null; - - // user submission params - // The key will be undefined until the user is authenticated and the contest is in the submitting stage - - const { - data: userSubmitParams, - isLoading: areUserSubmitParamsLoading, - error: isUserSubmitParamsError, - }: { data: any; isLoading: boolean; error: any } = useSWR( - submitParamsSwrKey, - () => getUserSubmissionParams(contestId, session.csrfToken) - ); - - // user voting params - // The key will be undefined until the user is authenticated and the contest is in the voting stage - const { - data: userVoteParams, - isLoading: areUserVotingParamsLoading, - error: isUserVotingParamsError, - mutate: mutateUserVotingParams, - }: { - data: UserVotingParams; - isLoading: boolean; - error: any; - mutate: any; - } = useSWR(voteParamsSwrKey, () => - getUserVotingParams(contestId, session.csrfToken) - ); - - const postProcessCsvResults = (results: string, type: string) => { - const endcodedUri = encodeURI(results); - const link = document.createElement("a"); - link.setAttribute("href", endcodedUri); - link.setAttribute("download", `${contestId}-${type}-results.csv`); - document.body.appendChild(link); - link.click(); - }; - - const downloadGnosisResults = () => { - getGnosisResults(contestId).then((res: string) => - postProcessCsvResults(res, "gnosis") - ); - }; - - return { - userSubmitParams, - areUserSubmitParamsLoading, - isUserSubmitParamsError, - userVoteParams, - areUserVotingParamsLoading, - isUserVotingParamsError, - mutateUserVotingParams, - downloadGnosisResults, - } -} \ No newline at end of file diff --git a/uplink-client/src/hooks/useCreateMintBoardTemplate.ts b/uplink-client/src/hooks/useCreateMintBoardTemplate.ts deleted file mode 100644 index 55bd1196..00000000 --- a/uplink-client/src/hooks/useCreateMintBoardTemplate.ts +++ /dev/null @@ -1,164 +0,0 @@ -import { uint64MaxSafe } from "@/utils/uint64"; -import { useReducer, useState } from "react"; -import { z } from "zod"; -import { Decimal } from 'decimal.js'; -import { handleMutationError } from "@/lib/handleMutationError"; -import { Session } from "@/providers/SessionProvider"; -import { validateEthAddress } from "@/lib/ethAddress"; -import { supportedChains } from "@/lib/chains/supportedChains"; - -export const MintBoardTemplateSchema = z.object({ - chainId: z.number().refine((n) => supportedChains.map(chain => chain.id).includes(n), { message: "Must be base network" }), - enabled: z.boolean(), - threshold: z.number(), - boardTitle: z.string().min(1, { message: "Board title is required" }), - boardDescription: z.string().min(1, { message: "Board description is required" }), - name: z.string().min(1, { message: "Name is required" }), - symbol: z.string().min(1, { message: "Symbol is required" }), - editionSize: z.string(), - description: z.string().min(1, { message: "Description is required" }), - publicSalePrice: z.string(), - publicSaleStart: z.string(), - publicSaleEnd: z.string(), - referrer: z.string().min(1, { message: "Referral reward recipient is required" }) -}).superRefine(async (data, ctx) => { - - const isEns = data.referrer.endsWith(".eth"); - if (isEns) { - ctx.addIssue({ - code: z.ZodIssueCode.custom, - path: ["referrer"], - message: "ENS not supported", - }) - } - - const cleanAddress = await validateEthAddress(data.referrer); - if (!Boolean(cleanAddress)) { - ctx.addIssue({ - code: z.ZodIssueCode.custom, - path: ["referrer"], - message: "Invalid Address", - }) - } -}) - - -export type MintBoardTemplate = z.infer; - - -type ZodSafeParseErrorFormat = { - [key: string]: { _errors: string[] }; -}; -export const EditionWizardReducer = (state: MintBoardTemplate & { errors?: ZodSafeParseErrorFormat }, action: any) => { - switch (action.type) { - case "SET_FIELD": - return { - ...state, - [action.payload.field]: action.payload.value, - errors: { ...state.errors, [action.payload.field]: undefined }, // Clear error when field is set - }; - case "SET_ERRORS": - return { - ...state, - errors: action.payload, - }; - default: - return state; - } -} - - - -export const configureMintBoard = async (url, - { - arg, - }: { - url: string; - arg: { - csrfToken: string; - spaceName: string; - mintBoardData: MintBoardTemplate; - } - } -) => { - return fetch(`${process.env.NEXT_PUBLIC_HUB_URL}/graphql`, { - method: "POST", - headers: { - "Content-Type": "application/json", - "X-CSRF-TOKEN": arg.csrfToken, - }, - credentials: "include", - body: JSON.stringify({ - query: ` - mutation ConfigureMintBoard($spaceName: String!, $mintBoardData: MintBoardInput!){ - configureMintBoard(spaceName: $spaceName, mintBoardData: $mintBoardData){ - success - } - }`, - variables: { - csrfToken: arg.csrfToken, - spaceName: arg.spaceName, - mintBoardData: arg.mintBoardData, - }, - }), - }) - .then((res) => res.json()) - .then(handleMutationError) - .then((res) => res.data.configureMintBoard); -} - - - -export default function useCreateMintBoardTemplate(templateConfig?: MintBoardTemplate) { - - const baseConfig = { - chainId: supportedChains[0].id, - enabled: false, - threshold: 0, - boardTitle: "", - boardDescription: "", - name: "", - symbol: "", - editionSize: "open", - description: "", - publicSalePrice: "free", - publicSaleStart: "now", - publicSaleEnd: "3 days", - referrer: "", - errors: {}, - } - - const initState = templateConfig ? { ...baseConfig, ...templateConfig } : baseConfig; - - const [state, dispatch] = useReducer(EditionWizardReducer, initState) - - - const setField = (field: string, value: string | boolean | number) => { - dispatch({ - type: 'SET_FIELD', - payload: { field, value }, - }); - } - - - const validate = async () => { - const { errors, ...rest } = state; - const result = await MintBoardTemplateSchema.safeParseAsync(rest); - if (!result.success) { - const formattedErrors = (result as z.SafeParseError).error.format(); - dispatch({ - type: "SET_ERRORS", - payload: formattedErrors, - }); - } - return result; - } - - - - return { - state, - setField, - validate, - } -} diff --git a/uplink-client/src/hooks/useCreateTokenReducer.ts b/uplink-client/src/hooks/useCreateTokenReducer.ts index 8c174984..9282d182 100644 --- a/uplink-client/src/hooks/useCreateTokenReducer.ts +++ b/uplink-client/src/hooks/useCreateTokenReducer.ts @@ -1,13 +1,12 @@ -"use client"; +"use client";; import { parseIpfsUrl, pinJSONToIpfs, replaceGatewayLinksInString } from '@/lib/ipfs'; import { z } from 'zod'; import { CreateTokenConfig } from "@tx-kit/sdk"; import { validateCreateTokenInputs } from "@tx-kit/sdk/utils"; -import { Address, http, maxUint256 } from 'viem'; -import { useEffect, useReducer, useState } from 'react'; +import { maxUint256 } from 'viem'; +import { useReducer, useState } from 'react'; import { useCreateToken, useCreateTokenIntent } from "@tx-kit/hooks" import { UploadToIpfsTokenMetadata } from '@/types/channel'; -import { useConnect, useConnectors, useWalletClient } from 'wagmi'; const constructTokenMetadata = (input: CreateTokenInputs): UploadToIpfsTokenMetadata => { diff --git a/uplink-client/src/hooks/useCreateZoraEdition.ts b/uplink-client/src/hooks/useCreateZoraEdition.ts deleted file mode 100644 index 350f6c0e..00000000 --- a/uplink-client/src/hooks/useCreateZoraEdition.ts +++ /dev/null @@ -1,421 +0,0 @@ -import { uint64MaxSafe } from "@/utils/uint64"; -import { useReducer, useState } from "react"; -import { z } from "zod"; -import { Decimal } from 'decimal.js'; -import { handleMutationError } from "@/lib/handleMutationError"; -import { Session } from "@/providers/SessionProvider"; -import { parseIpfsUrl } from "@/lib/ipfs"; -import toast from "react-hot-toast"; -import { supportedChains } from "@/lib/chains/supportedChains"; - - - - -export const EditionConfig = z.object({ - name: z.string(), - symbol: z.string(), - editionSize: z.string(), - royaltyBPS: z.number(), - fundsRecipient: z.string(), - defaultAdmin: z.string(), - saleConfig: z.object({ - publicSalePrice: z.string(), - maxSalePurchasePerAddress: z.number(), - publicSaleStart: z.string(), - publicSaleEnd: z.string(), - presaleStart: z.string(), - presaleEnd: z.string(), - presaleMerkleRoot: z.string(), - }), - description: z.string(), - animationURI: z.string(), - imageURI: z.string(), - referrer: z.string(), -}) - -export const ZoraEdition = z.object({ - chainId: z.number().refine((n) => supportedChains.map(val => val.id).includes(n), { message: "Unsupported network" }), - address: z.string(), - config: EditionConfig, -}); - -export const EditionNameSchema = z.string().min(1, { message: "Name is required" }); -export const EditionSymbolSchema = z.string().min(1, { message: "Symbol is required" }); -export const EditionSizeSchema = z.union([z.literal("open"), z.string().min(1, { message: "Edition size is required" })]).transform((val, ctx) => { - if (val === "open") { return uint64MaxSafe.toString(); } - if (val === "one") { return "1"; } - const result = BigInt(val); - if (!result) { - ctx.addIssue({ - code: z.ZodIssueCode.custom, - message: "Edition size must be greater than 0", - path: ['editionSize'], - }) - return z.NEVER; - } - - return result.toString(); -}) - -export const EditionRoyaltyBPSSchema = z.union([z.literal("zero"), z.literal("five"), z.string().min(1, { message: "Royalty % is required" })]).transform((val) => { - if (val === "zero") { return 0 } - if (val === "five") { return 500 } - const bps = parseInt(new Decimal(val).times(100).toString()); - return bps -}) - -export const EditionPublicSalePriceSchema = z.union([z.literal("free"), z.string().min(1, { message: "Edition price is required" })]).transform((val) => { - if (val === "free") return "0"; - return new Decimal(val).times(10 ** 18).toString(); -}) - -const calcSaleStart = (saleStart: string) => { - const unixInS = (str: string | number | Date) => Math.floor(new Date(str).getTime() / 1000); - const now = unixInS(new Date(Date.now())); - if (saleStart === "now") return now; - return unixInS(saleStart); -} - -const calcSaleEnd = (saleEnd: string) => { - const unixInS = (str: string | number | Date) => Math.floor(new Date(str).getTime() / 1000); - const now = unixInS(new Date(Date.now())); - const three_days = now + 259200; - const week = now + 604800; - if (saleEnd === "forever") return uint64MaxSafe; - if (saleEnd === "3 days") return three_days; - if (saleEnd === "week") return week; - return unixInS(saleEnd); -} - -export const EditionSaleConfigSchema = z.object({ - publicSalePrice: EditionPublicSalePriceSchema, - publicSaleStart: z.union([z.string().datetime(), z.literal("now")]), - publicSaleEnd: z.union([z.string().datetime(), z.literal("forever"), z.literal("week"), z.literal("3 days")]), -}).transform((val, ctx) => { - const { publicSalePrice, publicSaleStart, publicSaleEnd } = val; - const unixInS = (str: string | number | Date) => Math.floor(new Date(str).getTime() / 1000); - const now = unixInS(new Date(Date.now())); - const week = now + 604800; - const unixSaleStart = calcSaleStart(publicSaleStart); - const unixSaleEnd = calcSaleEnd(publicSaleEnd); - if (unixSaleStart < now) { - ctx.addIssue({ - code: z.ZodIssueCode.custom, - message: "Public sale start must be in the future", - path: ['publicSaleStart'], - }) - return z.NEVER; - } - - if (unixSaleStart > unixSaleEnd) { - ctx.addIssue({ - code: z.ZodIssueCode.custom, - message: "Public sale end must be after public sale start", - path: ['publicSaleEnd'], - }) - return z.NEVER; - } - - return { - publicSalePrice, - publicSaleStart: unixSaleStart.toString(), - publicSaleEnd: unixSaleEnd.toString(), - } -}) - - -export const ConfigurableZoraEditionSchema = z.object({ - creator: z.string().min(1, { message: "You must be signed in" }), - name: z.string().min(1, { message: "Name is required" }), - symbol: z.string().min(1, { message: "Symbol is required" }), - editionSize: EditionSizeSchema, - royaltyBPS: EditionRoyaltyBPSSchema, - description: z.string().min(1, { message: "Description is required" }), - animationURI: z.string(), - imageURI: z.string(), - saleConfig: EditionSaleConfigSchema, -}).transform((val, ctx) => { - if (val.animationURI && !val.imageURI) { - ctx.addIssue({ - code: z.ZodIssueCode.custom, - message: "Video thumbnail must be set", - path: ['animationURI'], - }) - return z.NEVER; - } - - if (!val.imageURI) { - ctx.addIssue({ - code: z.ZodIssueCode.custom, - message: "Image must be set", - path: ['imageURI'], - }) - return z.NEVER; - } - - const output: ZoraEditionConfig = { - name: val.name, - symbol: val.symbol, - editionSize: val.editionSize, - royaltyBPS: val.royaltyBPS, - fundsRecipient: val.creator, - defaultAdmin: val.creator, - saleConfig: { - publicSalePrice: val.saleConfig.publicSalePrice, - maxSalePurchasePerAddress: 2147483647, // max int32 - publicSaleStart: val.saleConfig.publicSaleStart, - publicSaleEnd: val.saleConfig.publicSaleEnd, - presaleStart: "0", - presaleEnd: "0", - presaleMerkleRoot: "0x0000000000000000000000000000000000000000000000000000000000000000" - }, - description: val.description, - animationURI: parseIpfsUrl(val.animationURI).raw, - imageURI: parseIpfsUrl(val.imageURI).raw, - referrer: "0xa943e039B1Ce670873ccCd4024AB959082FC6Dd8", - } - - return output; -}); - -export type ZoraEditionConfig = z.infer; -export type ConfigurableZoraEdition = z.infer; -export type ConfigurableZoraEditionInput = z.input; -export type ConfigurableZoraEditionOutput = z.output; - - -type ZodSafeParseErrorFormat = { - [key: string]: { _errors: string[] }; -}; -export const EditionWizardReducer = (state: ConfigurableZoraEditionInput & { errors?: ZodSafeParseErrorFormat }, action: any) => { - switch (action.type) { - case "SET_FIELD": - return { - ...state, - [action.payload.field]: action.payload.value, - errors: { ...state.errors, [action.payload.field]: undefined }, // Clear error when field is set - }; - case "SET_ERRORS": - return { - ...state, - errors: action.payload, - }; - default: - return state; - } -} - -export const reserveMintBoardSlot = async (url, - { - arg, - }: { - url: string; - arg: { - csrfToken: string; - spaceName: string; - } - } -) => { - return fetch(`${process.env.NEXT_PUBLIC_HUB_URL}/graphql`, { - method: "POST", - headers: { - "Content-Type": "application/json", - "X-CSRF-TOKEN": arg.csrfToken, - }, - credentials: "include", - body: JSON.stringify({ - query: ` - mutation ReserveMintBoardSlot($spaceName: String!){ - reserveMintBoardSlot(spaceName: $spaceName){ - success - slot - } - }`, - variables: { - csrfToken: arg.csrfToken, - spaceName: arg.spaceName, - }, - }), - }) - .then((res) => res.json()) - .then(handleMutationError) - .then((res) => res.data.reserveMintBoardSlot); -} - - -export const postToMintBoard = async (url, - { - arg, - }: { - url: string; - arg: { - csrfToken: string; - spaceName: string; - contractAddress: string; - chainId: number; - dropConfig: ConfigurableZoraEditionOutput; - } - } -) => { - return fetch(`${process.env.NEXT_PUBLIC_HUB_URL}/graphql`, { - method: "POST", - headers: { - "Content-Type": "application/json", - "X-CSRF-TOKEN": arg.csrfToken, - }, - credentials: "include", - body: JSON.stringify({ - query: ` - mutation CreateMintBoardPost($spaceName: String!, $contractAddress: String!, $chainId: Int!, $dropConfig: DropConfig!){ - createMintBoardPost(spaceName: $spaceName, contractAddress: $contractAddress, chainId: $chainId, dropConfig: $dropConfig){ - success - } - }`, - variables: { - csrfToken: arg.csrfToken, - spaceName: arg.spaceName, - contractAddress: arg.contractAddress, - chainId: arg.chainId, - dropConfig: arg.dropConfig, - }, - }), - }) - .then((res) => res.json()) - .then(handleMutationError) - .then((res) => res.data.createMintBoardPost); -} - - -export const postDrop = async (url, - { - arg, - }: { - url: string; - arg: { - csrfToken: string; - submissionId: string; - contestId: string; - contractAddress: string; - chainId: number; - dropConfig: ConfigurableZoraEditionOutput; - } - } -) => { - return fetch(`${process.env.NEXT_PUBLIC_HUB_URL}/graphql`, { - method: "POST", - headers: { - "Content-Type": "application/json", - "X-CSRF-TOKEN": arg.csrfToken, - }, - credentials: "include", - body: JSON.stringify({ - query: ` - mutation CreateUserDrop($submissionId: ID!, $contestId: ID!, $contractAddress: String!, $chainId: Int!, $dropConfig: DropConfig!){ - createUserDrop(submissionId: $submissionId, contestId: $contestId, contractAddress: $contractAddress, chainId: $chainId, dropConfig: $dropConfig){ - success - } - }`, - variables: { - csrfToken: arg.csrfToken, - submissionId: arg.submissionId, - contestId: arg.contestId, - contractAddress: arg.contractAddress, - chainId: arg.chainId, - dropConfig: arg.dropConfig, - }, - }), - }) - .then((res) => res.json()) - .then(handleMutationError) - .then((res) => res.data.createUserDrop); -} - -export const flattenContractArgs = (args: ConfigurableZoraEditionOutput) => { - if (!args) return null; - return Object.entries(args).map(([key, val], idx) => { - if (key === "saleConfig") return Object.values(val) - return val; - }) -} - -export default function useCreateZoraEdition(referrer?: string, templateConfig?: ConfigurableZoraEditionInput) { - const [contractArguments, setContractArguments] = useState(null); - - const isReferralValid = referrer ? referrer.startsWith('0x') && referrer.length === 42 : false; - - const baseConfig = { - name: "", - symbol: "", - editionSize: "open", - royaltyBPS: "zero", - description: "", - animationURI: "", - imageURI: "", - saleConfig: { - publicSalePrice: "free", - publicSaleStart: "now", - publicSaleEnd: "forever", - }, - errors: {}, - } - - const initState = templateConfig ? { ...baseConfig, ...templateConfig } : baseConfig; - - const [state, dispatch] = useReducer(EditionWizardReducer, initState); - - - const setField = (field: string, value: string) => { - const keys = field.split('.'); - const lastKey = keys.pop(); - const lastObj = keys.reduce((stateObj, key) => stateObj[key] = stateObj[key] || {}, state); - - if (lastKey) { - lastObj[lastKey] = value; - } - - dispatch({ - type: 'SET_FIELD', - payload: { field, value }, - }); - }; - - - const validate = async (userAddress: Session['user']['address']) => { - - //const videoThumbnailUrl = state.animationURI ? await handleThumbnailChoice() : ''; - - const { errors, ...rest } = state; - - const result = ConfigurableZoraEditionSchema.safeParse({ - ...rest, - creator: userAddress, - imageURI: state.imageURI, - animationURI: state.animationURI - }); - - if (!result.success) { - // Formatting errors and dispatching - const formattedErrors = (result as z.SafeParseError).error.format(); - dispatch({ - type: "SET_ERRORS", - payload: formattedErrors, // Pass the formatted errors directly - }); - } - else if (result.success) { - setContractArguments({ - ...result.data, - referrer: isReferralValid ? referrer : "0xa943e039B1Ce670873ccCd4024AB959082FC6Dd8" - }); - } - return result; - } - - - return { - contractArguments, - setContractArguments, - state, - setField, - validate, - } -} diff --git a/uplink-client/src/hooks/useDeleteContestSubmission.ts b/uplink-client/src/hooks/useDeleteContestSubmission.ts deleted file mode 100644 index 1c9ced67..00000000 --- a/uplink-client/src/hooks/useDeleteContestSubmission.ts +++ /dev/null @@ -1,92 +0,0 @@ -import useSWRMutation from "swr/mutation"; -import useLiveSubmissions from './useLiveSubmissions'; -import toast from "react-hot-toast"; -import { handleMutationError } from "@/lib/handleMutationError"; -import { useSession } from "@/providers/SessionProvider"; -import { useRouter } from 'next/navigation'; - - -const deleteContestSubmission = async (url, { arg }: { - url: string; - arg: { - csrfToken: string; - submissionId: string - contestId: string; - } -} -) => { - return fetch(`${process.env.NEXT_PUBLIC_HUB_URL}/graphql`, { - method: "POST", - headers: { - "Content-Type": "application/json", - "X-CSRF-TOKEN": arg.csrfToken, - }, - credentials: "include", - body: JSON.stringify({ - query: ` - mutation DeleteContestSubmission($submissionId: ID!, $contestId: ID!) { - deleteContestSubmission(submissionId: $submissionId, contestId: $contestId) { - success - } - }`, - variables: { - csrfToken: arg.csrfToken, - submissionId: arg.submissionId, - contestId: arg.contestId, - }, - }), - }) - .then((res) => res.json()) - .then(handleMutationError) - .then((res) => res.data.deleteContestSubmission); -} - - - -export const useDeleteContestSubmission = (contestId: string) => { - const { data: session, status } = useSession(); - const { mutateLiveSubmissions } = useLiveSubmissions(contestId); - - const { trigger: triggerDeleteContestSubmission, error, isMutating: isDeleteContestSubmissionMutating, reset: resetDeleteContestSubmission } = useSWRMutation( - `/api/deleteContestSubmission`, - deleteContestSubmission, - { - onError: (err) => { - console.log(err); - toast.error( - "Oops, something went wrong. Please check the fields and try again." - ); - resetDeleteContestSubmission(); - }, - } - ); - - - - const handleDeleteContestSubmission = async (submissionId: string, onSuccess: () => void) => { - try { - await triggerDeleteContestSubmission({ - submissionId, - contestId, - csrfToken: session.csrfToken - }).then(({ success }) => { - if (success) { - mutateLiveSubmissions(); - toast.success("Post successfully deleted", { icon: "🚀" }); - onSuccess(); - } else { - // set the errors - toast.error( - "Oops, something went wrong. Please try again later." - ); - } - }); - } catch (e) { - resetDeleteContestSubmission(); - } - } - - return { - handleDeleteContestSubmission - } -} \ No newline at end of file diff --git a/uplink-client/src/hooks/useDeleteMintboardPost.ts b/uplink-client/src/hooks/useDeleteMintboardPost.ts deleted file mode 100644 index 12e3fa2e..00000000 --- a/uplink-client/src/hooks/useDeleteMintboardPost.ts +++ /dev/null @@ -1,88 +0,0 @@ -import useSWRMutation from "swr/mutation"; -import toast from "react-hot-toast"; -import { handleMutationError } from "@/lib/handleMutationError"; -import { useSession } from "@/providers/SessionProvider"; - - -const deleteMintboardPost = async (url, { arg }: { - url: string; - arg: { - csrfToken: string; - postId: string - spaceId: string; - } -} -) => { - return fetch(`${process.env.NEXT_PUBLIC_HUB_URL}/graphql`, { - method: "POST", - headers: { - "Content-Type": "application/json", - "X-CSRF-TOKEN": arg.csrfToken, - }, - credentials: "include", - body: JSON.stringify({ - query: ` - mutation DeleteMintboardPost($postId: ID!, $spaceId: ID!) { - deleteMintboardPost(postId: $postId, spaceId: $spaceId) { - success - } - }`, - variables: { - csrfToken: arg.csrfToken, - postId: arg.postId, - spaceId: arg.spaceId, - }, - }), - }) - .then((res) => res.json()) - .then(handleMutationError) - .then((res) => res.data.deleteMintboardPost); -} - - - -export const useDeleteMintboardPost = (onSuccess: () => void) => { - const { data: session, status } = useSession(); - - const { trigger: triggerDeleteMintboardPost, error, isMutating: isDeleteMintboardPostMutating, reset: resetDeleteMintboardPost } = useSWRMutation( - `/api/deleteMintboardPost`, - deleteMintboardPost, - { - onError: (err) => { - console.log(err); - toast.error( - "Oops, something went wrong. Please check the fields and try again." - ); - resetDeleteMintboardPost(); - }, - } - ); - - - - const handleDeleteMintboardPost = async (postId: string, spaceId: string) => { - try { - await triggerDeleteMintboardPost({ - postId, - spaceId, - csrfToken: session.csrfToken - }).then(({ success }) => { - if (success) { - toast.success("Post successfully deleted", { icon: "🚀" }); - onSuccess(); - } else { - // set the errors - toast.error( - "Oops, something went wrong. Please try again later." - ); - } - }); - } catch (e) { - resetDeleteMintboardPost(); - } - } - - return { - handleDeleteMintboardPost - } -} \ No newline at end of file diff --git a/uplink-client/src/hooks/useEnsName.ts b/uplink-client/src/hooks/useEnsName.ts deleted file mode 100644 index ed05d9a0..00000000 --- a/uplink-client/src/hooks/useEnsName.ts +++ /dev/null @@ -1,29 +0,0 @@ -import { useState, useEffect } from 'react'; -import { createWeb3Client } from '@/lib/viem'; - -const publicClient = createWeb3Client(1); - -const useEnsName = (address: string) => { - const [ensName, setEnsName] = useState(null); - const [loading, setLoading] = useState(true); - const [error, setError] = useState(null); - - useEffect(() => { - async function fetchENSName() { - try { - const name = await publicClient.getEnsName({ address: address as `0x${string}` }) - setEnsName(name); - setLoading(false); - } catch (err) { - setError(err); - setLoading(false); - } - } - - fetchENSName(); - }, [address]); - - return { ensName, loading, error }; -} - -export default useEnsName; diff --git a/uplink-client/src/hooks/useLiveSubmissions.ts b/uplink-client/src/hooks/useLiveSubmissions.ts deleted file mode 100644 index 585b2ecc..00000000 --- a/uplink-client/src/hooks/useLiveSubmissions.ts +++ /dev/null @@ -1,107 +0,0 @@ -import useSWR from "swr"; -import { mutateSubmissions } from "@/app/mutate"; -import { useEffect } from "react"; -import { Submission } from "@/types/submission"; - -// local client side fetch, don't use the server-only fetch -const fetchSubmissions = async (contestId: string) => { - const data = await fetch(`${process.env.NEXT_PUBLIC_HUB_URL}/graphql`, { - method: "POST", - headers: { - "Content-Type": "application/json", - }, - body: JSON.stringify({ - query: ` - query Query($contestId: ID!){ - contest(contestId: $contestId){ - submissions { - id - contestId - totalVotes - rank - created - type - url - version - edition { - id - chainId - contractAddress - name - symbol - editionSize - royaltyBPS - fundsRecipient - defaultAdmin - saleConfig { - publicSalePrice - maxSalePurchasePerAddress - publicSaleStart - publicSaleEnd - presaleStart - presaleEnd - presaleMerkleRoot - } - description - animationURI - imageURI - referrer - } - author { - id - address - userName - displayName - profileAvatar - } - } - } - }`, - variables: { - contestId, - }, - }), - }) - .then((res) => res.json()) - .then((res) => res.data.contest) - .then(res => res.submissions) - .then(async (submissions) => { - return await Promise.all( - submissions.map(async (submission, idx) => { - const data = await fetch(submission.url).then((res) => res.json()); - return { ...submission, data: data }; - }) - ); - }) - return data; -}; - - - -const useLiveSubmissions = (contestId: string) => { - const { - data: liveSubmissions, - isLoading: areSubmissionsLoading, - error: isSubmissionError, - mutate: mutateSWRSubmissions, - }: { data: Array; isLoading: boolean; error: any; mutate: any } = useSWR( - `submissions/${contestId}`, - () => fetchSubmissions(contestId), - { refreshInterval: 10000 } - ); - - const mutateLiveSubmissions = () => { - mutateSWRSubmissions(); // mutate the SWR cache - mutateSubmissions(contestId); // mutate the server cache - }; - - return { - liveSubmissions, - areSubmissionsLoading, - isSubmissionError, - mutateLiveSubmissions, - } -} - - -export default useLiveSubmissions; \ No newline at end of file diff --git a/uplink-client/src/hooks/useMe.ts b/uplink-client/src/hooks/useMe.ts deleted file mode 100644 index 3189e097..00000000 --- a/uplink-client/src/hooks/useMe.ts +++ /dev/null @@ -1,124 +0,0 @@ -import handleNotFound from "@/lib/handleNotFound"; -import { useSession } from "@/providers/SessionProvider"; -import { BaseSubmission, Submission } from "@/types/submission"; -import { User } from "@/types/user"; -import useSWR, { useSWRConfig } from "swr"; - -const fetchMe = async (csrfToken: string) => { - const data = await fetch(`${process.env.NEXT_PUBLIC_HUB_URL}/graphql`, { - method: "POST", - credentials: "include", - headers: { - "Content-Type": "application/json", - "X-CSRF-TOKEN": csrfToken, - }, - body: JSON.stringify({ - query: ` - query Me { - me { - id - address - displayName - profileAvatar - submissions { - id - contestId - type - version - url - edition { - id - chainId - contractAddress - name - symbol - editionSize - royaltyBPS - fundsRecipient - defaultAdmin - saleConfig { - publicSalePrice - maxSalePurchasePerAddress - publicSaleStart - publicSaleEnd - presaleStart - presaleEnd - presaleMerkleRoot - } - description - animationURI - imageURI - referrer - } - author { - address - id - displayName - userName - profileAvatar - } - } - twitterAvatar - twitterHandle - userName - visibleTwitter - } - }`, - }), - }) - .then((res) => res.json()) - .then((res) => res.data.me) - .then(handleNotFound) - .then(async (res) => { - const subData = await Promise.all( - res.submissions.map(async (submission: BaseSubmission) => { - const data: Submission = await fetch(submission.url).then((res) => res.json()); - return { ...submission, data: data }; - }) - ); - return { - ...res, - submissions: subData - } - }); - return data; - -}; - - - - -const useMe = (accountAddress: string) => { - const { data: session, status } = useSession(); - const { fallback } = useSWRConfig() - - const isAuthed = status === "authenticated" && accountAddress === session?.user?.address - const meParamsSwrKey = isAuthed ? `me/${accountAddress}` : null - - const { - data, - isLoading: isMeLoading, - error: isMeError, - mutate: mutateMe - }: { data: any; isLoading: boolean; error: any; mutate: any } = useSWR( - meParamsSwrKey, - () => fetchMe(session.csrfToken), - ); - - - const getCachedFallback = () => { - return fallback[`me/${accountAddress}`]; - } - - return { - me: meParamsSwrKey ? data : getCachedFallback(), - getFallbackData: () => getCachedFallback(), - isUserAuthorized: isAuthed, - mutateMe, - isMeLoading, - isMeError, - } -} - - -export default useMe; \ No newline at end of file diff --git a/uplink-client/src/hooks/useMintBoardUserStats.ts b/uplink-client/src/hooks/useMintBoardUserStats.ts deleted file mode 100644 index cf7bfd34..00000000 --- a/uplink-client/src/hooks/useMintBoardUserStats.ts +++ /dev/null @@ -1,60 +0,0 @@ -import { handleMutationError } from "@/lib/handleMutationError"; -import { useSession } from "@/providers/SessionProvider"; -import useSWR from "swr"; - -const fetchMintBoardUserStats = async ( - csrfToken: string, - boardId: string -) => { - return fetch(`${process.env.NEXT_PUBLIC_HUB_URL}/graphql`, { - method: "POST", - headers: { - "Content-Type": "application/json", - "X-CSRF-TOKEN": csrfToken, - }, - credentials: "include", - body: JSON.stringify({ - query: ` - query MintBoardUserStats($boardId: ID!) { - mintBoardUserStats(boardId: $boardId) { - totalMints - } - }`, - variables: { - csrfToken: csrfToken, - boardId: boardId, - }, - }), - }) - .then((res) => res.json()) - .then(handleMutationError) - .then((res) => res.data.mintBoardUserStats); -} - - - -export const useMintBoardUserStats = (spaceName: string, boardId: string | null) => { - const { data: session, status } = useSession(); - const key = (boardId && status === 'authenticated') ? `/api/mintBoardUserStats/${spaceName}/${session.user.address}` : null; - - const { - data, - isLoading, - error, - mutate - }: { data: any; isLoading: boolean; error: any; mutate: any } = useSWR( - key, - () => fetchMintBoardUserStats( - session.csrfToken, - boardId - ), { refreshInterval: 10_000 } - ); - - return { - data, - isLoading, - error, - mutate - } - -} \ No newline at end of file diff --git a/uplink-client/src/hooks/useSpaceReducer.ts b/uplink-client/src/hooks/useSpaceReducer.ts new file mode 100644 index 00000000..0dfa599f --- /dev/null +++ b/uplink-client/src/hooks/useSpaceReducer.ts @@ -0,0 +1,126 @@ +"use client";; +import { validateEthAddress } from '@/lib/ethAddress'; +import { createWeb3Client } from '@/lib/viem'; +import { normalize } from 'path'; +import { useReducer } from 'react'; +import { Address, getAddress } from 'viem'; +import { z } from 'zod'; + +const mainnetClient = createWeb3Client(1); + +export const SpaceSettingsSchema = z.object({ + name: z.string().min(3, { message: "Name must contain at least 3 characters" }).max(30).regex(/^[a-zA-Z0-9_ ]+$/, { message: "Name must only contain alphanumeric characters and underscores" }), + logoUrl: z.string().min(1, { message: "Logo is required" }), + website: z.string().optional(), + admins: z.array(z.string()), +}).transform(async (data, ctx) => { + + const name = data.name.trim(); + const logoUrl = data.logoUrl.trim(); + const website = data.website?.trim(); + const admins = await Promise.all(data.admins.map(validateEthAddress)); + + //const anyNulls = admins.some((admin) => !admin); + const adminErrs = admins.map(admin => !admin ? true : false) + const hasErrs = adminErrs.some((err) => err); + + if (hasErrs) { + const firstErrIdx = adminErrs.findIndex((err) => err); + + ctx.addIssue({ + path: ["admins", firstErrIdx], + code: z.ZodIssueCode.custom, + "message": "Invalid Ethereum address", + }) + } + + return { + name, + logoUrl, + website, + admins + } + +}); + +export type SpaceSettingsInput = z.infer; +export type SpaceSettingsOutput = z.output; + +type ZodSafeParseErrorFormat = { + [key: string]: { _errors: string[] }; +}; + +export type SpaceSettingsStateT = SpaceSettingsInput & { errors: ZodSafeParseErrorFormat } + +export const baseConfig: SpaceSettingsStateT = { + name: "", + logoUrl: "", + website: "", + admins: [], + errors: {} +} + +export const StateReducer = (state: SpaceSettingsStateT, action: { type: string, payload: any }) => { + switch (action.type) { + case "SET_FIELD": + return { + ...state, + [action.payload.field]: action.payload.value, + errors: { ...state.errors, [action.payload.field]: undefined }, // Clear error when field is set + }; + case "SET_ERRORS": + return { + ...state, + errors: action.payload, + }; + default: + return state; + } +} + +export const useSpaceSettings = (priorState: SpaceSettingsStateT) => { + + const initialState = { ...baseConfig, ...priorState }; + const [state, dispatch] = useReducer(StateReducer, initialState); + + const setField = (field: string, value: any) => { + dispatch({ + type: 'SET_FIELD', + payload: { field, value }, + }); + } + + + const validateSettings = async () => { + const { errors, ...rest } = state; + const result = await SpaceSettingsSchema.safeParseAsync({ + ...rest, + admins: state.admins.filter((admin) => admin !== "") + }); + if (!result.success) { + const formattedErrors = (result as z.SafeParseError).error.format(); + dispatch({ + type: "SET_ERRORS", + payload: formattedErrors, + }); + } + + return result; + } + + return { + state, + setField, + validateSettings + } + +} + +// export const NewSpaceSettingsSchema = SpaceSettingsSchema.transform(async (data, ctx) => { + +// }) + +// export const EditSpaceSettingsSchema = SpaceSettingsSchema.transform(async (data, ctx) => { + +// }) + diff --git a/uplink-client/src/hooks/useStandardSubmissionCreator.ts b/uplink-client/src/hooks/useStandardSubmissionCreator.ts deleted file mode 100644 index a6f31e60..00000000 --- a/uplink-client/src/hooks/useStandardSubmissionCreator.ts +++ /dev/null @@ -1,173 +0,0 @@ -import type { OutputData } from "@editorjs/editorjs"; -import { useReducer } from "react"; -import { toast } from "react-hot-toast"; - -export type SubmissionBuilderProps = { - title: string; - videoAsset: string | null; - previewAsset: string | null; - submissionBody: OutputData | null; - errors: { - type?: string; - title?: string; - previewAsset?: string; - videoAsset?: string; - submissionBody?: string; - }; -}; - - -export const reducer = (state: SubmissionBuilderProps, action: any) => { - switch (action.type) { - case "SET_FIELD": - return { - ...state, - [action.payload.field]: action.payload.value, - errors: { - ...state.errors, - [action.payload.field]: undefined, - }, - }; - - case "SET_ERRORS": - return { - ...state, - errors: action.payload, - }; - default: - return state; - } -}; - -export const useStandardSubmissionCreator = () => { - const [state, dispatch] = useReducer(reducer, { - title: "", - previewAsset: null, - videoAsset: null, - submissionBody: null, - errors: {}, - }); - - - const setSubmissionTitle = (value: string) => { - dispatch({ - type: "SET_FIELD", - payload: { field: "title", value }, - }); - } - - const setSubmissionBody = (value: OutputData) => { - dispatch({ - type: "SET_FIELD", - payload: { field: "submissionBody", value }, - }); - } - - const setPreviewAsset = (value: string) => { - dispatch({ - type: "SET_FIELD", - payload: { field: "previewAsset", value }, - }); - } - - const setVideoAsset = (value: string) => { - dispatch({ - type: "SET_FIELD", - payload: { field: "videoAsset", value }, - }); - } - - const setErrors = (value: any) => { - dispatch({ - type: "SET_ERRORS", - payload: value, - }); - } - - return { - submission: state, - setSubmissionTitle, - setSubmissionBody, - setPreviewAsset, - setVideoAsset, - setErrors - } - -} - -export const validateSubmission = async (state: SubmissionBuilderProps, onError: (data: any) => void) => { - const { - title, - previewAsset, - videoAsset, - submissionBody, - } = state; - - const isVideo = Boolean(videoAsset) - - if (!title) { - toast.error("Please provide a title"); - onError({ - title: "Please provide a title", - }); - return { - isError: true, - }; - } - - if (title.length < 3) { - toast.error("Title must be at least 3 characters") - onError({ - title: "Title must be at least 3 characters", - }) - return { - isError: true, - }; - } - - if (title.length > 100) { - toast.error("Title must be less than 100 characters") - onError({ - title: "Title must be less than 100 characters", - }) - return { - isError: true, - }; - } - - let type = null; - if (isVideo) type = "video"; - else if (previewAsset) type = "image"; - else if (submissionBody) type = "text"; - - if (!type) { - toast.error("Please upload media or provide body content") - onError({ - type: "Please upload media or provide body content", - }) - return { - isError: true, - }; - } - - if (type === "text" && submissionBody?.blocks.length === 0) { - toast.error("Please provide a submission body") - onError({ - submissionBody: "Please provide a submission body", - }) - return { - isError: true, - }; - } - - return { - isError: false, - payload: { - title, - body: submissionBody, - previewAsset: previewAsset ? previewAsset : null, - videoAsset : videoAsset ? videoAsset : null - } - }; - -} \ No newline at end of file diff --git a/uplink-client/src/hooks/useThreadCreator.ts b/uplink-client/src/hooks/useThreadCreator.ts deleted file mode 100644 index 388c4750..00000000 --- a/uplink-client/src/hooks/useThreadCreator.ts +++ /dev/null @@ -1,393 +0,0 @@ -import { useEffect, useReducer, useState } from "react"; -import { nanoid } from 'nanoid'; -import handleMediaUpload, { IpfsUpload, MediaUploadError } from "@/lib/twitterMediaUpload"; -import { toast } from "react-hot-toast"; - - -// client context thread item -export type ThreadItem = { - id: string; - text: string; - primaryAssetUrl: string | null; - primaryAssetBlob: string | null; - videoThumbnailUrl: string | null; - videoThumbnailBlobIndex: number | null; - videoThumbnailOptions: string[] | null; - assetSize: number | null; - assetType: string | null; - isVideo: boolean; - isUploading: boolean; - error?: string; -}; - -// api context thread item -export type ApiThreadItem = { - text: string; - previewAsset?: string; - videoAsset?: string; - assetSize?: number; - assetType?: string; -}; - - -const threadReducer = ( - state: ThreadItem[], - action: any -): ThreadItem[] => { - switch (action.type) { - case "SET_FIELD": - return state.map((item) => { - if (item.id === action.payload.id) { - return { - ...item, - [action.payload.field]: action.payload.value, - error: undefined, - }; - } else { - return item; - } - }); - - case "ADD_TWEET": - return [...state, action.payload]; - case "REMOVE_TWEET": - return state.filter((item) => item.id !== action.payload); - case "SET_ERROR": - return state.map((item) => { - if (item.id === action.payload.id) { - return { - ...item, - error: action.payload, - } - } - else { - return item; - } - }); - case "RESET": - return action.payload; - default: - return state; - } -} - - - -export const useThreadCreator = (initialThread?: ThreadItem[]) => { - - const [state, dispatch] = useReducer(threadReducer, - initialThread ? initialThread.map((tweet) => { - return { // if initialThread is passed, do nothing - ...tweet, - } - }) : [{ // if initialThread is null, then use this default value - id: nanoid(), - text: "", - primaryAssetUrl: null, - primaryAssetBlob: null, - videoThumbnailUrl: null, - videoThumbnailBlobIndex: null, - videoThumbnailOptions: null, - assetSize: null, - assetType: null, - isVideo: false, - isUploading: false, - }]); - - useEffect(() => { - return () => { - reset(); - } - }, []) - - const addTweet = () => { - dispatch({ - type: "ADD_TWEET", - payload: { - id: nanoid(), - text: "", - primaryAssetUrl: null, - primaryAssetBlob: null, - videoThumbnailUrl: null, - videoThumbnailBlobIndex: null, - videoThumbnailOptions: null, - assetSize: null, - assetType: null, - isVideo: false, - isUploading: false, - } - }); - } - - const removeTweet = (id: string) => { - dispatch({ - type: "REMOVE_TWEET", - payload: id, - }); - } - - const setTweetText = (id: string, text: string) => { - dispatch({ - type: "SET_FIELD", - payload: { id, field: 'text', value: text }, - }); - - } - - const setTweetPrimaryAssetUrl = (id: string, url: string) => { - dispatch({ - type: "SET_FIELD", - payload: { id, field: 'primaryAssetUrl', value: url }, - }); - } - - const setTweetPrimaryAssetBlob = (id: string, blob: string) => { - dispatch({ - type: "SET_FIELD", - payload: { id, field: 'primaryAssetBlob', value: blob }, - }); - } - - const setTweetVideoThumbnailUrl = (id: string, url: string) => { - dispatch({ - type: "SET_FIELD", - payload: { id, field: 'videoThumbnailUrl', value: url }, - }); - } - - const setTweetVideoThumbnailBlobIndex = (id: string, index: number | null) => { - dispatch({ - type: "SET_FIELD", - payload: { id, field: 'videoThumbnailBlobIndex', value: index }, - }); - } - - const setTweetVideoThumbnailOptions = (id: string, options: string[]) => { - dispatch({ - type: "SET_FIELD", - payload: { id, field: 'videoThumbnailOptions', value: options }, - }); - } - - const setTweetAssetSize = (id: string, size: number) => { - dispatch({ - type: "SET_FIELD", - payload: { id, field: 'assetSize', value: size } - }) - } - - - const setTweetAssetType = (id: string, type: string) => { - dispatch({ - type: "SET_FIELD", - payload: { id, field: 'assetType', value: type } - }) - } - - const removeTweetMedia = (id: string) => { - setIsUploading(id, false); - setIsVideo(id, false); - setTweetPrimaryAssetUrl(id, null); - setTweetPrimaryAssetBlob(id, null); - setTweetVideoThumbnailUrl(id, null); - setTweetVideoThumbnailBlobIndex(id, null); - setTweetVideoThumbnailOptions(id, null); - setTweetAssetSize(id, null); - setTweetAssetType(id, null); - } - - - const setIsUploading = (id: string, isUploading: boolean) => { - dispatch({ - type: "SET_FIELD", - payload: { id, field: 'isUploading', value: isUploading }, - }); - } - - const setIsVideo = (id: string, isVideo: boolean) => { - dispatch({ - type: "SET_FIELD", - payload: { id, field: 'isVideo', value: isVideo }, - }); - } - - - const reset = () => { - dispatch({ - type: "RESET", - payload: [{ - id: nanoid(), - text: "", - primaryAssetUrl: null, - primaryAssetBlob: null, - videoThumbnailUrl: null, - videoThumbnailBlobIndex: null, - assetSize: null, - assetType: null, - isVideo: false, - isUploading: false, - }] - }); - } - - const handleFileChange = ({ - id, - event, - isVideo, - mode - }: { - id: string; - event: any; - isVideo: boolean; - mode: "primary" | "thumbnail"; - }) => { - if (mode === "primary") { - setIsUploading(id, true) - - handleMediaUpload( - event, - ["image", "video"], - (mimeType) => { - setIsVideo(id, mimeType.includes("video")); - setTweetAssetType(id, mimeType) - }, - (base64, mimeType) => { - setTweetPrimaryAssetBlob(id, base64); - }, - (ipfsUrl) => { - setTweetPrimaryAssetUrl(id, ipfsUrl); - setIsUploading(id, false); - }, - (thumbnails) => { - setTweetVideoThumbnailOptions(id, thumbnails); - setTweetVideoThumbnailBlobIndex(id, 1); - }, - (size) => { - setTweetAssetSize(id, size) - } - ).catch((err) => { - - if (err instanceof MediaUploadError) { - // toast the message - toast.error(err.message) - } - else { - // log the message and toast a generic error - console.log(err) - toast.error('Something went wrong. Please try again later.') - } - - // clear out all the fields for the users next attempt - removeTweetMedia(id); - - }); - } - else if (mode === "thumbnail") { - handleMediaUpload( - event, - ["image"], - (mimeType) => { }, - (base64) => { - const existingThumbnailOptions = state.filter((item) => item.id === id)[0].videoThumbnailOptions; - setTweetVideoThumbnailOptions(id, [...existingThumbnailOptions, base64]) - setTweetVideoThumbnailBlobIndex(id, existingThumbnailOptions.length); - }, - (ipfsUrl) => { - setTweetVideoThumbnailUrl(id, ipfsUrl); - }, - - ).catch(() => { - - }); - } - }; - - - - // check for errors and produce a clean thread object - const validateThread = async () => { - - if (state.length === 0 || !state) { - return { - isError: true, - } - } - - - if (state.some((item) => item.isUploading)) { - toast.error('One of your tweets is still uploading. Please wait for it to finish before submitting.') - return { - isError: true, - } - } - - - for await (const item of state) { - - const hasText = item.text.trim().length > 0; - const hasMedia = item.primaryAssetUrl !== null || item.primaryAssetBlob !== null; - - if (!hasText && !hasMedia) { - toast.error('One of your tweets is empty. Please add text or media to continue.') - return { - isError: true, - } - } - - if (hasText) { - if (item.text.length > 280) { - toast.error('One of your tweets is too long. Please shorten it to continue.') - return { - isError: true, - } - } - } - - if (hasMedia) { - if (item.isVideo) { - if (!item.videoThumbnailOptions[item.videoThumbnailBlobIndex]) { - toast.error('One of your tweets is missing a video thumbnail. Please add a thumbnail to continue.') - return { - isError: true, - } - } - else { - // if there is a thumbnail, upload the blob and add it to the thread - // convert the base64 to blob first - const blob = await fetch(item.videoThumbnailOptions[item.videoThumbnailBlobIndex]).then(r => r.blob()) - await IpfsUpload(blob).then(url => { - item.videoThumbnailUrl = url; - }).catch(err => { - console.log(err) - toast.error('Something went wrong. Please try again.') - return { - isError: true, - } - }) - } - } - } - } - - return { - isError: false, - } - - } - - return { - thread: state, - addTweet, - removeTweet, - removeTweetMedia, - setTweetText, - setTweetVideoThumbnailBlobIndex, - handleFileChange, - validateThread, - resetThread: reset, - setTweetPrimaryAssetUrl, - setTweetVideoThumbnailUrl, - setTweetPrimaryAssetBlob, - } - -}; \ No newline at end of file diff --git a/uplink-client/src/hooks/useTokenBalance.ts b/uplink-client/src/hooks/useTokenBalance.ts index 5493de5c..10ac5ea0 100644 --- a/uplink-client/src/hooks/useTokenBalance.ts +++ b/uplink-client/src/hooks/useTokenBalance.ts @@ -1,7 +1,7 @@ import { NATIVE_TOKEN } from "@tx-kit/sdk"; import { useEffect, useState } from "react"; import { Address, erc20Abi, zeroAddress } from "viem"; -import { useAccount, useChainId, usePublicClient, useWalletClient } from "wagmi"; +import { usePublicClient, useWalletClient } from "wagmi"; export const useErc20Balance = (tokenContract: Address, chainId: number) => { const [balance, setBalance] = useState(BigInt(0)); diff --git a/uplink-client/src/hooks/useTokenInfo.ts b/uplink-client/src/hooks/useTokenInfo.ts index b8fcf787..b3c95bd2 100644 --- a/uplink-client/src/hooks/useTokenInfo.ts +++ b/uplink-client/src/hooks/useTokenInfo.ts @@ -2,8 +2,8 @@ import { getTokenInfo } from "@/lib/tokenInfo"; import { ChainId } from "@/types/chains"; import { NATIVE_TOKEN } from "@tx-kit/sdk"; -import { useEffect, useState } from "react"; -import { Address, checksumAddress, getAddress, isAddress, zeroAddress } from "viem"; +import { useState } from "react"; +import { Address, checksumAddress, isAddress, zeroAddress } from "viem"; import { useMemo } from "react"; export const useTokenInfo = (tokenContract: string, chainId: ChainId) => { diff --git a/uplink-client/src/hooks/useTokens.ts b/uplink-client/src/hooks/useTokens.ts index 356d5707..ef8c298c 100644 --- a/uplink-client/src/hooks/useTokens.ts +++ b/uplink-client/src/hooks/useTokens.ts @@ -1,8 +1,7 @@ -"use client"; +"use client";; import useSWRInfinite from "swr/infinite"; import { FetchFiniteChannelTokensV2Response, FetchPopularTokensResponse, FetchTokenIntentsResponse, FetchTokensV1Response, FetchTokensV2Response } from "@/lib/fetch/fetchTokensV2"; import { ContractID } from "@/types/channel"; -import { useCallback } from "react"; export const fetchTokensV1_client = async (contractId: ContractID, pageSize: number, skip: number): Promise => { return fetch(`${process.env.NEXT_PUBLIC_HUB_URL}/v2/channel_tokensV1?contractId=${contractId}&pageSize=${pageSize}&skip=${skip}`, { diff --git a/uplink-client/src/hooks/useTweetQueueStatus.ts b/uplink-client/src/hooks/useTweetQueueStatus.ts deleted file mode 100644 index c0c577a4..00000000 --- a/uplink-client/src/hooks/useTweetQueueStatus.ts +++ /dev/null @@ -1,37 +0,0 @@ -import useSWR from "swr"; - - -// return whether a tweet has been queued for a given contest - -const getTweetQueueStatus = async (contestId: string) => { - return fetch(`${process.env.NEXT_PUBLIC_HUB_URL}/graphql`, { - method: "POST", - headers: { - "Content-Type": "application/json", - }, - body: JSON.stringify({ - query: ` - query Query($contestId: ID!) { - isContestTweetQueued(contestId: $contestId) - }`, - variables: { - contestId, - }, - }), - }) - - .then((res) => res.json()) - .then(res => res.data.isContestTweetQueued) -}; - - - - -const useTweetQueueStatus = (contestId: string) => { - const swrKey = `/api/tweetQueueStatus/${contestId}` - const { data: isTweetQueued, error, isLoading, mutate: mutateIsTweetQueued } = useSWR(swrKey, () => getTweetQueueStatus(contestId), { refreshInterval: 1000 * 60 }); - return { isTweetQueued, isLoading, mutateIsTweetQueued }; -} - - -export default useTweetQueueStatus; \ No newline at end of file diff --git a/uplink-client/src/hooks/useVote.ts b/uplink-client/src/hooks/useVote.ts deleted file mode 100644 index 349bbe98..00000000 --- a/uplink-client/src/hooks/useVote.ts +++ /dev/null @@ -1,504 +0,0 @@ -"use client";; -import { useEffect } from "react"; - -import { toast } from "react-hot-toast"; -import { handleMutationError } from "@/lib/handleMutationError"; -import { Decimal } from "decimal.js"; -import { useSession } from "@/providers/SessionProvider"; -import { Submission } from "@/types/submission"; -import { useContestInteractionApi, UserVote, UserVotingParams } from "./useContestInteractionAPI"; -import { useLocalStorage } from "./useLocalStorage"; - -// inherit voting params from contest interaction provider -// provide an API for managing user votes - - - -export type VotableSubmission = Submission & { - votes: string; -} - - -const apiCastVotes = async ( - contestId: string, - castVotePayload: any, - csrfToken: string | null -): Promise => { - return fetch(`${process.env.NEXT_PUBLIC_HUB_URL}/graphql`, { - method: "POST", - headers: { - "Content-Type": "application/json", - "X-CSRF-Token": csrfToken, - }, - credentials: "include", - body: JSON.stringify({ - query: ` - mutation Mutation($contestId: ID!, $castVotePayload: [CastVotePayload!]!){ - castVotes(contestId: $contestId, castVotePayload: $castVotePayload){ - success - userVotingParams { - totalVotingPower - votesRemaining - votesSpent - userVotes { - id - submissionId - submissionUrl - votes - } - } - } - }`, - variables: { - contestId, - castVotePayload, - }, - }), - }) - .then((res) => res.json()) - .then(handleMutationError) - .then((res) => res.data.castVotes); -}; - -const apiRemoveSingleVote = async ( - contestId: string, - submissionId: string, - csrfToken: string | null -): Promise => { - return fetch(`${process.env.NEXT_PUBLIC_HUB_URL}/graphql`, { - method: "POST", - headers: { - "Content-Type": "application/json", - "X-CSRF-Token": csrfToken, - }, - credentials: "include", - body: JSON.stringify({ - query: ` - mutation Mutation($contestId: ID!, $submissionId: ID!){ - removeSingleVote(contestId: $contestId, submissionId: $submissionId){ - success - userVotingParams { - totalVotingPower - votesRemaining - votesSpent - userVotes { - id - votes - submissionId - submissionUrl - votes - } - } - } - }`, - variables: { - contestId, - submissionId, - }, - }), - }) - .then((res) => res.json()) - .then(handleMutationError) - .then((res) => res.data.removeSingleVote); -}; - -const apiRemoveAllVotes = async ( - contestId: string, - csrfToken: string | null -): Promise => { - return fetch(`${process.env.NEXT_PUBLIC_HUB_URL}/graphql`, { - method: "POST", - headers: { - "Content-Type": "application/json", - "X-CSRF-Token": csrfToken, - }, - credentials: "include", - body: JSON.stringify({ - query: ` - mutation Mutation($contestId: ID!){ - removeAllVotes(contestId: $contestId){ - success - userVotingParams { - totalVotingPower - votesRemaining - votesSpent - userVotes { - id - votes - submissionId - submissionUrl - votes - } - } - } - }`, - variables: { - contestId, - }, - }), - }) - .then((res) => res.json()) - .then(handleMutationError) - .then((res) => res.data.removeAllVotes); -}; - -export interface VoteActionProps { - removeAllVotes: () => void; - removeSingleVote: ( - submissionId: string, - mode: "current" | "proposed" - ) => void; - addProposedVote: (el: Submission) => void; - updateVoteAmount: (id: string, newAmount: string, mode: "current" | "proposed") => void; - submitVotes: () => void; - areCurrentVotesDirty: boolean; - areUserVotingParamsLoading: boolean; - proposedVotes: Array; - totalVotingPower: string; - votesSpent: string; - votesRemaining: string; - currentVotes: Array; -} - - -export const useVote = (contestId: string) => { - // userVoteParams will be undefined if not in the voting window TODO: verify this - const { userVoteParams, areUserVotingParamsLoading, mutateUserVotingParams } = useContestInteractionApi(contestId); - const [proposedVotes, setProposedVotes] = useLocalStorage>(`proposedVotes-${contestId}`, []); - const [areCurrentVotesDirty, setAreCurrentVotesDirty] = useLocalStorage(`areVotesDirty-${contestId}`, false); - const { data: session, status } = useSession(); - - // handle cases where the user was signed out, added proposed votes, then signed in - // if proposed votes already exist in current votes, remove them from proposed votes - - useEffect(() => { - if (status === "authenticated" && userVoteParams?.userVotes.length > 0) { - const newProposedVotes = proposedVotes.filter( - (el) => - !userVoteParams?.userVotes.find( - (vote: UserVote) => vote?.submissionId === el.id - ) - ); - setProposedVotes(newProposedVotes); - } - }, [status, userVoteParams?.userVotes]); - - // add submission to proposed votes - const addProposedVote = (el: Submission) => { - if (proposedVotes.find(vote => vote.id === el.id)) return toast.error("This selection is already in your cart."); - if (userVoteParams) { - if (userVoteParams?.userVotes?.find((vote: UserVote) => vote.submissionId === el.id)) return toast.error("This selection is already in your cart."); - } - setProposedVotes([...proposedVotes, { ...el, votes: "" }]); - }; - - // remove a single current or proposed vote. - // proposed votes are local, current votes require an api request - const removeSingleVote = async ( - submissionId: string, - mode: "current" | "proposed" - ) => { - if (mode === "proposed") { - //TODO: need to update the vote differentials here - setProposedVotes( - proposedVotes.filter((el) => el.id !== submissionId) - ); - } - - if (mode === "current") { - const toRemoveIdx = userVoteParams?.userVotes.findIndex( - (el: UserVote) => el.submissionId === submissionId - ); - - // send the mutation request and optimistically update the cache - // once the request completes, the cache will be updated with the response - // if the request fails, the cache will be reverted to original value of userVoteParams - - const options = { - optimisticData: { - ...userVoteParams, - votesRemaining: new Decimal(userVoteParams?.votesRemaining || "0") - .plus(userVoteParams?.userVotes[toRemoveIdx].votes) - .toString(), - votesSpent: new Decimal(userVoteParams?.votesSpent || "0") - .minus(userVoteParams?.userVotes[toRemoveIdx].votes) - .toString(), - userVotes: [ - ...userVoteParams?.userVotes.slice(0, toRemoveIdx), - ...userVoteParams?.userVotes.slice(toRemoveIdx + 1), - ], - }, - populateCache: (newData: { - userVotingParams: UserVotingParams; - success: boolean; - }) => { - return newData.userVotingParams; - }, - rollbackOnError: true, - revalidate: false, - }; - try { - await mutateUserVotingParams( - apiRemoveSingleVote(contestId, submissionId, session.csrfToken), - options - ); - toast.success("Your vote has been removed"); - } catch (err) { - console.log(err); - mutateUserVotingParams(userVoteParams, { revalidate: false }); - } - } - }; - - // reset the total user vote state. - const removeAllVotes = async () => { - // if the user is signed out, just reset the local proposed vote state - - if (status !== "authenticated") return setProposedVotes([]); - - const previousProposedVotes = [...proposedVotes]; - - // send the mutation request and optimistically update the cache - // once the request completes, the cache will be updated with the response - // if the request fails, the cache will be reverted to original value of userVoteParams - - const options = { - optimisticData: () => { - setProposedVotes([]); - return { - ...userVoteParams, - votesRemaining: userVoteParams?.totalVotingPower || "0", - votesSpent: "0", - userVotes: [], - }; - }, - populateCache: (newData: { - userVotingParams: UserVotingParams; - success: boolean; - }) => { - return newData.userVotingParams; - }, - rollbackOnError: true, - revalidate: false, - }; - try { - await mutateUserVotingParams( - apiRemoveAllVotes(contestId, session.csrfToken), - options - ); - toast.success("Your votes have been removed"); - } catch (err) { - console.log(err); - setProposedVotes(previousProposedVotes); - mutateUserVotingParams(userVoteParams, { revalidate: false }); - } - }; - - // update the amount of votes allocated to a submission - const updateVoteAmount = ( - id: string, - newAmount: string, - mode: "current" | "proposed" - ) => { - // in proposed mode, we update the local state of the proposed votes along with the swr state of our user voting params - // in current mode, we only update the swr state of our user voting params - - const constructVoteDifferential = () => { - // return object should adjust the votesSpent and votesRemaining values - const voteToUpdate = - mode === "current" - ? (userVoteParams?.userVotes || []).find( - (vote) => vote.submissionId === id - ) - : proposedVotes.find((vote) => vote.id === id); - - if (!voteToUpdate) return { votesSpent, votesRemaining }; - - const oldAmountDecimal = new Decimal(voteToUpdate.votes || "0"); - const newAmountDecimal = new Decimal(newAmount || "0"); - const difference = newAmountDecimal.minus(oldAmountDecimal); - voteToUpdate.votes = newAmount; - - const newVotesSpent = new Decimal(userVoteParams?.votesSpent ?? "0") - .plus(difference) - .toString(); - - const newVotesRemaining = new Decimal( - userVoteParams?.votesRemaining ?? "0" - ) - .minus(difference) - .toString(); - - return { - votesSpent: newVotesSpent, - votesRemaining: newVotesRemaining, - }; - }; - - const { votesSpent, votesRemaining } = constructVoteDifferential(); - - if (mode === "proposed") { - // update the proposed votes to match new value - setProposedVotes((prevVotes) => { - const voteIndex = prevVotes.findIndex( - (vote) => vote.id === id - ); - if (voteIndex < 0) return prevVotes; - return [ - ...prevVotes.slice(0, voteIndex), - { ...prevVotes[voteIndex], votes: newAmount }, - ...prevVotes.slice(voteIndex + 1), - ]; - }); - // update the user voting params swr state to reflect the new differential - mutateUserVotingParams( - { - ...userVoteParams, - votesSpent, - votesRemaining, - }, - { - revalidate: false, - } - ); - } else if (mode === "current") { - // update the user voting params swr state to reflect the new differential - const voteIndex = userVoteParams.userVotes.findIndex( - (vote: UserVote) => vote.submissionId === id - ); - const newUserVotes = - voteIndex < 0 - ? [...userVoteParams.userVotes] - : [ - ...userVoteParams.userVotes.slice(0, voteIndex), - { ...userVoteParams.userVotes[voteIndex], votes: newAmount }, - ...userVoteParams.userVotes.slice(voteIndex + 1), - ]; - setAreCurrentVotesDirty(true); - mutateUserVotingParams( - { - ...userVoteParams, - userVotes: newUserVotes, - votesSpent, - votesRemaining, - }, - { - revalidate: false, - } - ); - } - }; - // get the votes payload ready for the API request - const prepareVotes = () => { - let runningSum = new Decimal(0); - let castVotePayload = []; - let optimisticVotes = []; - - for (const el of userVoteParams.userVotes) { - const decimalAmount = new Decimal(el.votes || "0"); - if (decimalAmount.greaterThan(0)) { - runningSum = runningSum.plus(decimalAmount); - castVotePayload.push({ - submissionId: el.submissionId, - votes: el.votes, - }); - optimisticVotes.push(el); - } - } - - for (const el of proposedVotes) { - const decimalAmount = new Decimal(el.votes || "0"); - if (decimalAmount.greaterThan(0)) { - runningSum = runningSum.plus(decimalAmount); - castVotePayload.push({ - submissionId: el.id, - votes: el.votes, - }); - optimisticVotes.push({ - submissionId: el.id, - votes: el.votes, - submissionUrl: el.url, - }); - } - } - - - - - return { - runningSum, - castVotePayload, - optimisticVotes, - }; - }; - // send votes to the API - const submitVotes = async () => { - const { runningSum, castVotePayload, optimisticVotes } = prepareVotes(); - if (castVotePayload.length === 0) - return toast.error("Please add votes to your selections"); - if (runningSum.greaterThan(userVoteParams.totalVotingPower)) - return toast.error("Insufficient voting power"); - - const prevProposedVotes = [...proposedVotes]; - const prevVotesDirty = JSON.parse(JSON.stringify(areCurrentVotesDirty)); - - // send the mutation request and optimistically update the cache - // once the request completes, the cache will be updated with the response - // if the request fails, the cache will be reverted to original value of userVoteParams - - const options = { - optimisticData: () => { - setProposedVotes([]); - setAreCurrentVotesDirty(false); - return { - ...userVoteParams, - userVotes: optimisticVotes, - votesRemaining: new Decimal(userVoteParams?.votesRemaining || "0") - .minus(runningSum) - .toString(), - votesSpent: new Decimal(userVoteParams?.votesSpent || "0") - .plus(runningSum) - .toString(), - }; - }, - populateCache: (newData: { - userVotingParams: UserVotingParams; - success: boolean; - }) => { - return newData.userVotingParams; - }, - rollbackOnError: true, - revalidate: false, - }; - - try { - await mutateUserVotingParams( - apiCastVotes(contestId, castVotePayload, session.csrfToken), - options - ); - toast.success("Your votes have been submitted"); - } catch (err) { - console.log(err); - setProposedVotes(prevProposedVotes); - setAreCurrentVotesDirty(prevVotesDirty); - mutateUserVotingParams(userVoteParams, { revalidate: false }); - } - }; - - return { - removeAllVotes, - removeSingleVote, - addProposedVote, - updateVoteAmount, - submitVotes, - // fwd the user voting params from interaction provider - areUserVotingParamsLoading, - totalVotingPower: userVoteParams?.totalVotingPower || "0", - votesSpent: userVoteParams?.votesSpent || "0", - votesRemaining: userVoteParams?.votesRemaining || "0", - currentVotes: userVoteParams?.userVotes || [], - // along with endpoints from this API - areCurrentVotesDirty, - proposedVotes, - } -} diff --git a/uplink-client/src/hooks/useWalletDisplay.ts b/uplink-client/src/hooks/useWalletDisplay.ts index d2bbe732..c1bf38e3 100644 --- a/uplink-client/src/hooks/useWalletDisplay.ts +++ b/uplink-client/src/hooks/useWalletDisplay.ts @@ -1,8 +1,7 @@ -import { useEffect, useState, useCallback } from 'react'; +import { useState, useCallback } from 'react'; import { createWeb3Client } from '@/lib/viem'; // adjust the import path as necessary import debounce from 'lodash/debounce'; import { Address } from 'viem'; -import { normalize } from 'viem/ens'; // Create a Web3 client instance const client = createWeb3Client(1); diff --git a/uplink-client/src/hooks/useWindowSize.ts b/uplink-client/src/hooks/useWindowSize.ts deleted file mode 100644 index e2de8b63..00000000 --- a/uplink-client/src/hooks/useWindowSize.ts +++ /dev/null @@ -1,32 +0,0 @@ -import { useState, useEffect } from 'react'; - -interface Size { - width: number | undefined; - height: number | undefined; -} - -const useWindowSize = (): Size => { - const [windowSize, setWindowSize] = useState({ - width: undefined, - height: undefined, - }); - - useEffect(() => { - function handleResize() { - setWindowSize({ - width: window.innerWidth, - height: window.innerHeight, - }); - } - - window.addEventListener('resize', handleResize); - - handleResize(); - - return () => window.removeEventListener('resize', handleResize); - }, []); - - return windowSize; -} - -export default useWindowSize; diff --git a/uplink-client/src/lib/OptimizedImage.tsx b/uplink-client/src/lib/OptimizedImage.tsx new file mode 100644 index 00000000..df0a2b2b --- /dev/null +++ b/uplink-client/src/lib/OptimizedImage.tsx @@ -0,0 +1,58 @@ +"use client"; +import Image from "next/image"; +import { useEffect, useState } from "react"; + +const OptimizedImage = ({ + src, + alt, + sizes, + width, + height, + fill = false, + quality = 85, + draggable, + className +}: { + src: string, + alt: string, + sizes?: string, + width?: number, + height?: number, + fill?: boolean, + quality?: number, + draggable?: boolean, + className?: string +}) => { + + const [error, setError] = useState(false) + + const urlWidth = width ? `img-width=${width}` : '' + const urlHeight = height ? `&img-height=${height}` : '' + const urlQuality = `&img-quality=${quality}` + const url = `${src}?${urlWidth}${urlHeight}${urlQuality}` + + useEffect(() => { + setError(false) + }, [url]) + + const props = { + ...(width && { width }), + ...(height && { height }), + ...(sizes && { sizes }), + fill, + draggable, + className, + } + + return ( + {alt} setError(true)} + {...props} + /> + ) + +} + +export default OptimizedImage; \ No newline at end of file diff --git a/uplink-client/src/lib/UplinkImage.tsx b/uplink-client/src/lib/UplinkImage.tsx deleted file mode 100644 index b77b7c3a..00000000 --- a/uplink-client/src/lib/UplinkImage.tsx +++ /dev/null @@ -1,85 +0,0 @@ -"use client"; -import Image, { StaticImageData } from "next/image"; -import { useState } from "react"; -const normalizeSrc = (src) => { - return src.startsWith('/') ? process.env.NEXT_PUBLIC_CLIENT_URL + src : src; -}; - - -export const imageLoader = ({ src, width, quality }) => { - const isObjectURL = !src.startsWith('http') - const qualitySetting = quality || 'auto:best'; // default to auto:good if not specified - const modifiers = `w_${width},q_${qualitySetting},c_limit,f_auto`; // c_fill for Cloudinary fill mode - return isObjectURL ? src : `https://res.cloudinary.com/drrkx8iye/image/fetch/${modifiers}/${normalizeSrc(src)}`; -}; - - -export const blurLoader = ({ src, width, quality }) => { - const isObjectURL = !src.startsWith('http') - const adjustedWidth = width > 200 ? 200 : width - const modifiers = `w_${adjustedWidth},q_auto:low,e_blur:2000,c_limit,f_auto`; // c_fill for Cloudinary fill mode - return isObjectURL ? src : `https://res.cloudinary.com/drrkx8iye/image/fetch/${modifiers}/${normalizeSrc(src)}`; -}; - -export default function UplinkImage(props: { src: string | StaticImageData, alt: string, width?: number, height?: number, fill?: boolean, sizes?: string, className?: string, blur?: boolean, quality?: number, draggable?: boolean, priority?: boolean }) { - const { src, sizes, alt, blur = true, className, ...rest } = props; - const [isPlaceholderError, setIsPlaceholderError] = useState(false); - const [hasLoaded, setHasLoaded] = useState(false); - - - - // if (blur) { - // const blurredSrc = typeof src === 'string' ? blurLoader({ src, width: 200, quality: '1' }) : ''; - - // return ( - // <> - // {/* {!isPlaceholderError && !hasLoaded && setIsPlaceholderError(true)} - // alt={alt} - // sizes={sizes} - // className={`${className}`} - // {...rest} - // unoptimized={true} - - // /> - // } - // */} - - // {!isPlaceholderError && !hasLoaded && setIsPlaceholderError(true)} - // alt={alt} - // sizes={sizes} - // className={className} - // {...rest} - // unoptimized={true} - - // /> - // } - // setHasLoaded(true)} - // src={src} - // alt={alt} - // sizes={sizes} - // className={`${className} ${hasLoaded ? 'opacity-100' : 'opacity-0'}`} - // {...rest} - // /> - // - // ); - // } - - - return ( - {alt} - ) - -} diff --git a/uplink-client/src/lib/blockParser.tsx b/uplink-client/src/lib/blockParser.tsx index 3e4075be..dd0e42c1 100644 --- a/uplink-client/src/lib/blockParser.tsx +++ b/uplink-client/src/lib/blockParser.tsx @@ -1,9 +1,10 @@ "use client"; -import { ImageWrapper } from "@/ui/Submission/MediaWrapper"; +import { ImageWrapper } from "@/app/(legacy)/contest/components/MediaWrapper"; import type { OutputData } from "@editorjs/editorjs"; import React, { useEffect } from "react"; import Output, { LinkToolOutput, ListOutput, ParagraphOutput } from 'editorjs-react-renderer'; -import UplinkImage from "@/lib/UplinkImage" +import OptimizedImage from "@/lib/OptimizedImage" + const createLinks = (text: string): string => { const urlRegex = /(https?:\/\/[^\s]+)/g; const twitterRegex = /([^\S]|^)@(\w+)/gi; @@ -22,7 +23,7 @@ const ImageRenderer = ({ data }: { data: any }) => { return (
- { - return ( - - - - ) -} - - diff --git a/uplink-client/src/lib/farcaster/utils.ts b/uplink-client/src/lib/farcaster/utils.ts index cfdc67ca..39664b25 100644 --- a/uplink-client/src/lib/farcaster/utils.ts +++ b/uplink-client/src/lib/farcaster/utils.ts @@ -1,16 +1,24 @@ import { FrameRequest, MockFrameRequest, FrameValidationResponse, FrameMetadataHtmlResponse } from "./types"; import { neynarFrameValidation } from "./neynar"; +import probe from "probe-image-size"; -export const calculateImageAspectRatio = async (url: string) => { +export const calculateImageAspectRatio = async (url: string): Promise => { try { - const fileInfo = await fetch(`https://res.cloudinary.com/drrkx8iye/image/fetch/fl_getinfo/${url}`).then(res => res.json()) - const { output } = fileInfo; - if (output.width / output.height > 1.45) return "1.91:1"; - return "1:1" + const fileInfo = await probe(url); + const ratio = fileInfo.width / fileInfo.height; + + if (ratio > 1.45) { + // Landscape: Width is greater than height + return "1.91:1"; + } + // Square or portrait: Height is equal to or greater than width + return "1:1"; } catch (e) { - return "1:1" + console.error("Error calculating aspect ratio:", e); + return "1:1"; } -} +}; + type FrameMessageOptions = | { diff --git a/uplink-client/src/lib/fetch/fetchContest.ts b/uplink-client/src/lib/fetch/fetchContest.ts deleted file mode 100644 index 53352523..00000000 --- a/uplink-client/src/lib/fetch/fetchContest.ts +++ /dev/null @@ -1,164 +0,0 @@ -import { ReadableContest } from "@/types/contest"; -import handleNotFound from "../handleNotFound"; - - -export type FetchSingleContestResponse = ReadableContest & { - space: { - id: string; - name: string; - displayName: string; - logoUrl: string; - admins: Array<{ - address: string; - }>; - }; -} - - -const fetchContest = async (contestId: string): Promise => { - const data = await fetch(`${process.env.NEXT_PUBLIC_HUB_URL}/graphql`, { - method: "POST", - headers: { - "Content-Type": "application/json", - "X-API-TOKEN": process.env.API_SECRET!, - }, - body: JSON.stringify({ - query: ` - query Contest($contestId: ID!) { - contest(contestId: $contestId) { - id - spaceId - chainId - created - promptUrl - tweetId - space { - id - name - displayName - logoUrl - admins { - address - } - } - metadata { - category - type - } - deadlines { - startTime - voteTime - endTime - snapshot - } - votingPolicy { - ... on ArcadeVotingStrategyOption { - strategyType - arcadeVotingStrategy { - token { - tokenHash - type - symbol - decimals - address - tokenId - } - votingPower - } - } - ... on WeightedVotingStrategyOption { - strategyType - weightedVotingStrategy { - token { - tokenHash - type - symbol - decimals - address - tokenId - } - } - } - } - submitterRewards { - rank - reward { - ... on SubmitterTokenReward { - tokenReward { - ... on FungibleReward { - token { - tokenHash - type - symbol - decimals - address - tokenId - } - amount - } - ... on NonFungibleReward { - token { - tokenHash - type - symbol - decimals - address - tokenId - } - tokenId - } - } - } - } - } - voterRewards { - rank - reward { - ... on VoterTokenReward { - tokenReward { - ... on FungibleReward { - amount - token { - tokenHash - type - symbol - decimals - address - tokenId - } - } - } - } - } - } - submitterRestrictions { - ... on TokenRestrictionOption { - restrictionType - tokenRestriction { - threshold - token { - tokenHash - type - symbol - decimals - address - tokenId - } - } - } - } - } - }`, - variables: { - contestId, - }, - }), - next: { tags: [`contest/${contestId}`], revalidate: 60 }, - }) - .then((res) => res.json()) - .then((res) => res.data.contest) - .then(handleNotFound) - return data; -}; - -export default fetchContest; \ No newline at end of file diff --git a/uplink-client/src/lib/fetch/fetchLegacyContest.ts b/uplink-client/src/lib/fetch/fetchLegacyContest.ts new file mode 100644 index 00000000..83bc7a56 --- /dev/null +++ b/uplink-client/src/lib/fetch/fetchLegacyContest.ts @@ -0,0 +1,30 @@ +import { LegacyContest } from "@/types/contest"; +import handleNotFound from "../handleNotFound"; +import { BaseSubmission, Submission } from "@/types/submission"; + +const fetchLegacyContest = async (contestId: string): Promise => { + const data = await fetch(`${process.env.NEXT_PUBLIC_HUB_URL}/v2/legacy_singleContest?id=${contestId}`, { + method: "GET", + headers: { + "Content-Type": "application/json", + "X-API-TOKEN": process.env.API_SECRET!, + }, + next: { tags: [`contest/${contestId}`] }, + }) + .then((res) => res.json()) + .then(handleNotFound) + .then(async data => { + return { + ...data, + submissions: await Promise.all( + data.submissions.map(async (submission: BaseSubmission) => { + const data: Submission = await fetch(submission.url).then((res) => res.json()); + return { ...submission, data: data }; + }) + ) + } + }) + return data; +} + +export default fetchLegacyContest; \ No newline at end of file diff --git a/uplink-client/src/lib/fetch/fetchMintBoard.ts b/uplink-client/src/lib/fetch/fetchMintBoard.ts deleted file mode 100644 index c3d98065..00000000 --- a/uplink-client/src/lib/fetch/fetchMintBoard.ts +++ /dev/null @@ -1,52 +0,0 @@ -import { MintBoard } from "@/types/mintBoard"; - -const fetchMintBoard = async (spaceName: string): Promise => { - const data = await fetch(`${process.env.NEXT_PUBLIC_HUB_URL}/graphql`, { - method: "POST", - headers: { - "Content-Type": "application/json", - "X-API-TOKEN": process.env.API_SECRET!, - }, - body: JSON.stringify({ - query: ` - query mintBoard($spaceName: String!){ - mintBoard(spaceName: $spaceName) { - id - space { - id - logoUrl - name - displayName - admins { - address - } - } - enabled - threshold - editionSize - description - chainId - created - boardTitle - boardDescription - name - publicSaleEnd - publicSalePrice - publicSaleStart - referrer - spaceId - symbol - } - }`, - variables: { - spaceName, - }, - }), - next: { tags: [`mintBoard/${spaceName}`], revalidate: 60 }, - }) - .then((res) => res.json()) - .then((res) => res.data.mintBoard) - return data; -}; - -export default fetchMintBoard; \ No newline at end of file diff --git a/uplink-client/src/lib/fetch/fetchMintBoardPosts.ts b/uplink-client/src/lib/fetch/fetchMintBoardPosts.ts deleted file mode 100644 index 651677e5..00000000 --- a/uplink-client/src/lib/fetch/fetchMintBoardPosts.ts +++ /dev/null @@ -1,136 +0,0 @@ -import { MintBoardPost, PaginatedMintBoardPosts } from "@/types/mintBoard"; -import handleNotFound from "../handleNotFound"; - -export const fetchPaginatedMintBoardPosts = async (spaceName: string, lastCursor: string | null, limit: number): Promise => { - const data = await fetch(`${process.env.NEXT_PUBLIC_HUB_URL}/graphql`, { - method: "POST", - headers: { - "Content-Type": "application/json", - "X-API-TOKEN": process.env.API_SECRET!, - }, - body: JSON.stringify({ - query: ` - query PaginatedMintBoardPosts($spaceName: String! $lastCursor: String $limit: Int!) { - paginatedMintBoardPosts(spaceName: $spaceName lastCursor: $lastCursor limit: $limit) { - posts { - id - created - totalMints - author { - id - address - userName - displayName - profileAvatar - } - edition { - id - chainId - contractAddress - name - symbol - editionSize - royaltyBPS - fundsRecipient - defaultAdmin - saleConfig { - publicSalePrice - maxSalePurchasePerAddress - publicSaleStart - publicSaleEnd - presaleStart - presaleEnd - presaleMerkleRoot - } - description - animationURI - imageURI - referrer - } - } - pageInfo { - endCursor - hasNextPage - } - } - }`, - variables: { - spaceName, - lastCursor, - limit - }, - }), - next: { tags: [`mintBoard/${spaceName}/posts?lastCursor=${lastCursor}&limit=${limit}`], revalidate: 60 }, - }) - .then((res) => res.json()) - .then((res) => res.data.paginatedMintBoardPosts) - .then(handleNotFound); - return data; -} - - -export const fetchPopularMintBoardPosts = async (spaceName: string): Promise> => { - const data = await fetch(`${process.env.NEXT_PUBLIC_HUB_URL}/graphql`, { - method: "POST", - headers: { - "Content-Type": "application/json", - "X-API-TOKEN": process.env.API_SECRET!, - }, - body: JSON.stringify({ - query: ` - query PopularMintBoardPosts($spaceName: String!){ - popularMintBoardPosts(spaceName: $spaceName) { - id - created - totalMints - author { - id - address - userName - displayName - profileAvatar - } - edition { - id - chainId - contractAddress - name - symbol - editionSize - royaltyBPS - fundsRecipient - defaultAdmin - saleConfig { - publicSalePrice - maxSalePurchasePerAddress - publicSaleStart - publicSaleEnd - presaleStart - presaleEnd - presaleMerkleRoot - } - description - animationURI - imageURI - referrer - } - } - }`, - variables: { - spaceName, - }, - }), - next: { tags: [`mintBoard/${spaceName}/popular`], revalidate: 60 }, - }) - .then((res) => res.json()) - .then((res) => res.data.popularMintBoardPosts) - .then(handleNotFound) - return data; -} - - -export const fetchSingleMintboardPost = async (spaceName: string, postId: string): Promise => { - const offestPostId = parseInt(postId) + 1; - const post = await fetchPaginatedMintBoardPosts(spaceName, offestPostId.toString(), 1).then(data => data.posts[0]); - return post; -} \ No newline at end of file diff --git a/uplink-client/src/lib/fetch/fetchPopularSubmissions.ts b/uplink-client/src/lib/fetch/fetchPopularSubmissions.ts deleted file mode 100644 index 0d2b6168..00000000 --- a/uplink-client/src/lib/fetch/fetchPopularSubmissions.ts +++ /dev/null @@ -1,69 +0,0 @@ -import { BaseSubmission, Submission } from "@/types/submission"; - -const fetchPopularSubmissions = async (): Promise> => { - const data = await fetch(`${process.env.NEXT_PUBLIC_HUB_URL}/graphql`, { - method: "POST", - headers: { - "Content-Type": "application/json", - "X-API-TOKEN": process.env.API_SECRET!, - }, - body: JSON.stringify({ - query: ` - query PopularSubmissions { - popularSubmissions { - contestId - created - id - type - url - version - edition { - id - chainId - contractAddress - name - symbol - editionSize - royaltyBPS - fundsRecipient - defaultAdmin - saleConfig { - publicSalePrice - maxSalePurchasePerAddress - publicSaleStart - publicSaleEnd - presaleStart - presaleEnd - presaleMerkleRoot - } - description - animationURI - imageURI - referrer - } - author { - id - address - userName - displayName - profileAvatar - } - } - }`, - }), - next: { revalidate: 60 } - }) - .then((res) => res.json()) - .then((res) => res.data.popularSubmissions) - .then(async (submissions) => { - return Promise.all( - submissions.map(async (submission: BaseSubmission, idx: number) => { - const data = await fetch(submission.url).then((res) => res.json()); - return { ...submission, data: data }; - }) - ); - }); - return data; -}; - -export default fetchPopularSubmissions; \ No newline at end of file diff --git a/uplink-client/src/lib/fetch/fetchSingleSpace.ts b/uplink-client/src/lib/fetch/fetchSingleSpace.ts index e0771098..b01f22af 100644 --- a/uplink-client/src/lib/fetch/fetchSingleSpace.ts +++ b/uplink-client/src/lib/fetch/fetchSingleSpace.ts @@ -1,52 +1,18 @@ import { Space } from "@/types/space"; -import handleNotFound from "../handleNotFound"; -const fetchSingleSpace = async (name: string): Promise => { - const data = await fetch(`${process.env.NEXT_PUBLIC_HUB_URL}/graphql`, { - method: "POST", +const fetchSingleSpace = async (spaceName: string): Promise => { + + const data = await fetch(`${process.env.NEXT_PUBLIC_HUB_URL}/v2/space?spaceName=${spaceName}`, { + method: "GET", headers: { "Content-Type": "application/json", "X-API-TOKEN": process.env.API_SECRET!, }, - body: JSON.stringify({ - query: ` - query space($name: String!){ - space(name: $name) { - id - name - displayName - logoUrl - twitter - website - admins{ - address - } - spaceTokens { - token { - type - address - decimals - symbol - tokenId - chainId - } - } - } - }`, - variables: { - name, - }, - }), - next: { tags: [`space/${name}`], revalidate: 60 }, + next: { revalidate: 60, tags: [`space/${spaceName}`] }, }) - // .then(data =>{ - // console.log(data); - // return data; - // }) - .then((res) => res.json()) - .then((res) => res.data.space) - .then(handleNotFound); - return data; -}; + .then(res => res.json()) + + return data; +} export default fetchSingleSpace; \ No newline at end of file diff --git a/uplink-client/src/lib/fetch/fetchSingleSubmission.ts b/uplink-client/src/lib/fetch/fetchSingleSubmission.ts index e4611dd3..2ad7e21d 100644 --- a/uplink-client/src/lib/fetch/fetchSingleSubmission.ts +++ b/uplink-client/src/lib/fetch/fetchSingleSubmission.ts @@ -1,73 +1,22 @@ import { BaseSubmission, Submission } from "@/types/submission"; -import handleNotFound from "../handleNotFound"; + const fetchSingleSubmission = async (submissionId: string): Promise => { - const data = await fetch(`${process.env.NEXT_PUBLIC_HUB_URL}/graphql`, { - method: "POST", + return fetch(`${process.env.NEXT_PUBLIC_HUB_URL}/v2/singleLegacyContestSubmission?id=${submissionId}`, { + method: "GET", headers: { "Content-Type": "application/json", "X-API-TOKEN": process.env.API_SECRET!, }, - body: JSON.stringify({ - query: ` - query submission($submissionId: ID!){ - submission(submissionId: $submissionId) { - id - contestId - created - rank - totalVotes - type - url - version - author { - id - address - profileAvatar - userName - displayName - } - edition { - id - chainId - contractAddress - name - symbol - editionSize - royaltyBPS - fundsRecipient - defaultAdmin - saleConfig { - publicSalePrice - maxSalePurchasePerAddress - publicSaleStart - publicSaleEnd - presaleStart - presaleEnd - presaleMerkleRoot - } - description - animationURI - imageURI - referrer - } - } - }`, - variables: { - submissionId, - }, - }), - next: { tags: [`submission/${submissionId}`], revalidate: 60 }, + + next: { tags: [`submission/${submissionId}`] }, + }) - .then((res) => res.json()) - .then((res) => res.data.submission) - .then(handleNotFound) + .then(res => res.json()) .then(async (res: BaseSubmission) => { const data = await fetch(res.url).then((res) => res.json()); return { ...res, data: data }; }) - - return data; -}; +} export default fetchSingleSubmission; \ No newline at end of file diff --git a/uplink-client/src/lib/fetch/fetchSpaceChannels.ts b/uplink-client/src/lib/fetch/fetchSpaceChannels.ts index a65685d8..4104bbed 100644 --- a/uplink-client/src/lib/fetch/fetchSpaceChannels.ts +++ b/uplink-client/src/lib/fetch/fetchSpaceChannels.ts @@ -1,7 +1,16 @@ +import { ChainId } from "@/types/chains"; import { Channel } from "@/types/channel"; +import { LegacyContest } from "@/types/contest"; -const fetchSpaceChannels = async (spaceName: string): Promise> => { - const data = await fetch(`${process.env.NEXT_PUBLIC_HUB_URL}/v2/space_channels?spaceName=${spaceName}`, { + +export type SpaceChannels = { + finiteChannels: Array; + infiniteChannels: Array; + legacyContests: Array; +} + +const fetchSpaceChannels = async (spaceName: string, chainId: ChainId): Promise => { + const data = await fetch(`${process.env.NEXT_PUBLIC_HUB_URL}/v2/space_channels?spaceName=${spaceName}&chainId=${chainId}`, { method: "GET", headers: { "Content-Type": "application/json", diff --git a/uplink-client/src/lib/fetch/fetchSpaceContests.ts b/uplink-client/src/lib/fetch/fetchSpaceContests.ts deleted file mode 100644 index 462e25fa..00000000 --- a/uplink-client/src/lib/fetch/fetchSpaceContests.ts +++ /dev/null @@ -1,83 +0,0 @@ -import handleNotFound from "../handleNotFound"; -import { Deadlines, Metadata, ContestPromptData } from '@/types/contest'; - - - -export type SpaceContest = { - id: string; - tweetId: string | null; - deadlines: Deadlines - metadata: Metadata; - promptUrl: string; - promptData: ContestPromptData; -} - -export type FetchSpaceContestResponse = { - id: string; - logoUrl: string; - contests: Array -} - - -const fetchSpaceContests = async (spaceName: string): Promise => { - - const data = await fetch(`${process.env.NEXT_PUBLIC_HUB_URL}/graphql`, { - method: "POST", - headers: { - "Content-Type": "application/json", - "X-API-TOKEN": process.env.API_SECRET!, - }, - body: JSON.stringify({ - query: ` - query space($name: String!){ - space(name: $name) { - id - logoUrl - contests { - id - tweetId - promptUrl - deadlines { - endTime - snapshot - startTime - voteTime - } - metadata { - category - type - } - } - } - }`, - variables: { - name: spaceName, - }, - }), - next: { tags: [`space/${spaceName}/contests`], revalidate: 60 }, - }) - .then((res) => res.json()) - .then(res => res.data.space) - .then(handleNotFound) - .then(async spaceWithContests => { - const resolvedContests = await Promise.all( - spaceWithContests.contests.map(async (contest) => { - // fetch prompt url - const promptData = await fetch(contest.promptUrl).then((res) => - res.json() - ); - return { - ...contest, - promptData, - }; - }) - ); - return { - ...spaceWithContests, - contests: resolvedContests, - } - }) - return data; -} - -export default fetchSpaceContests; \ No newline at end of file diff --git a/uplink-client/src/lib/fetch/fetchSpaceStats.ts b/uplink-client/src/lib/fetch/fetchSpaceStats.ts index 8a53e1cd..b0f19ceb 100644 --- a/uplink-client/src/lib/fetch/fetchSpaceStats.ts +++ b/uplink-client/src/lib/fetch/fetchSpaceStats.ts @@ -1,8 +1,6 @@ import { SpaceStats } from "@/types/spaceStats"; import { ChainId } from "@/types/chains"; - - const fetchSpaceStats = async (spaceName: string, chainId: ChainId): Promise => { return fetch(`${process.env.NEXT_PUBLIC_HUB_URL}/v2/space_stats?spaceName=${spaceName}&chainId=${chainId}`, { method: "GET", diff --git a/uplink-client/src/lib/fetch/fetchSpaces.ts b/uplink-client/src/lib/fetch/fetchSpaces.ts index 6c748674..713854ea 100644 --- a/uplink-client/src/lib/fetch/fetchSpaces.ts +++ b/uplink-client/src/lib/fetch/fetchSpaces.ts @@ -1,27 +1,18 @@ import { Space } from "@/types/space"; const fetchSpaces = async (): Promise> => { - return fetch(`${process.env.NEXT_PUBLIC_HUB_URL}/graphql`, { - method: "POST", + + const data = await fetch(`${process.env.NEXT_PUBLIC_HUB_URL}/v2/spaces`, { + method: "GET", headers: { "Content-Type": "application/json", "X-API-TOKEN": process.env.API_SECRET!, }, - body: JSON.stringify({ - query: ` - query Spaces{ - spaces{ - name - displayName - members - logoUrl - } - }`, - }), - next: { tags: ["spaces"], revalidate: 60 }, + next: { revalidate: 60, tags: [`spaces`] }, }) - .then((res) => res.json()) - .then((res) => res.data.spaces); -}; -export default fetchSpaces; \ No newline at end of file + .then(res => res.json()) + + return data; +} +export default fetchSpaces; diff --git a/uplink-client/src/lib/fetch/fetchSubmissions.ts b/uplink-client/src/lib/fetch/fetchSubmissions.ts deleted file mode 100644 index e2aac516..00000000 --- a/uplink-client/src/lib/fetch/fetchSubmissions.ts +++ /dev/null @@ -1,80 +0,0 @@ -"use server"; -import { BaseSubmission, Submission } from "@/types/submission"; -import handleNotFound from "../handleNotFound"; - -const fetchSubmissions = async (contestId: string): Promise> => { - const data = await fetch(`${process.env.NEXT_PUBLIC_HUB_URL}/graphql`, { - method: "POST", - headers: { - "Content-Type": "application/json", - "X-API-TOKEN": process.env.API_SECRET, - }, - body: JSON.stringify({ - query: ` - query Query($contestId: ID!){ - contest(contestId: $contestId){ - submissions { - id - contestId - author { - id - address - profileAvatar - userName - displayName - } - totalVotes - rank - created - type - url - version - edition { - id - chainId - contractAddress - name - symbol - editionSize - royaltyBPS - fundsRecipient - defaultAdmin - saleConfig { - publicSalePrice - maxSalePurchasePerAddress - publicSaleStart - publicSaleEnd - presaleStart - presaleEnd - presaleMerkleRoot - } - description - animationURI - imageURI - referrer - } - } - } - }`, - variables: { - contestId, - }, - }), - next: { tags: [`submissions/${contestId}`], revalidate: 60 }, // cache submissions for 60 seconds - }) - .then((res) => res.json()) - .then((res) => res.data.contest) - .then(handleNotFound) - .then(res => res.submissions) - .then(async (submissions) => { - return await Promise.all( - submissions.map(async (submission: BaseSubmission) => { - const data: Submission = await fetch(submission.url).then((res) => res.json()); - return { ...submission, data: data }; - }) - ); - }); - return data; -}; - -export default fetchSubmissions; \ No newline at end of file diff --git a/uplink-client/src/lib/fetch/fetchTrendingSpaces.ts b/uplink-client/src/lib/fetch/fetchTrendingSpaces.ts deleted file mode 100644 index b33522db..00000000 --- a/uplink-client/src/lib/fetch/fetchTrendingSpaces.ts +++ /dev/null @@ -1,30 +0,0 @@ -import {Space} from "@/types/space"; -import handleNotFound from "../handleNotFound"; - -const fetchTrendingSpaces = async (): Promise> => { - return fetch(`${process.env.NEXT_PUBLIC_HUB_URL}/graphql`, { - method: "POST", - headers: { - "Content-Type": "application/json", - "X-API-TOKEN": process.env.API_SECRET!, - }, - body: JSON.stringify({ - query: ` - query trendingSpaces($limit: Int!){ - trendingSpaces(limit: $limit) { - id - name - displayName - logoUrl - } - }`, - variables: { limit: 10 }, - }), - next: { tags: [`trendingSpaces}`], revalidate: 60 }, - }) - .then((res) => res.json()) - .then((res) => res.data.trendingSpaces) - .then(handleNotFound) -}; - -export default fetchTrendingSpaces; \ No newline at end of file diff --git a/uplink-client/src/lib/fetch/fetchUser.ts b/uplink-client/src/lib/fetch/fetchUser.ts deleted file mode 100644 index 1c841c5d..00000000 --- a/uplink-client/src/lib/fetch/fetchUser.ts +++ /dev/null @@ -1,88 +0,0 @@ -import { User } from "@/types/user"; -import handleNotFound from "../handleNotFound"; -import { BaseSubmission, Submission } from "@/types/submission"; - - -const fetchUser = async (userIdentifier: string): Promise => { - const data = await fetch(`${process.env.NEXT_PUBLIC_HUB_URL}/graphql`, { - method: "POST", - headers: { - "Content-Type": "application/json", - "X-API-TOKEN": process.env.API_SECRET!, - }, - body: JSON.stringify({ - query: ` - query User($userIdentifier: String!) { - user(userIdentifier: $userIdentifier) { - id - displayName - userName - address - profileAvatar - twitterHandle - visibleTwitter - submissions { - id - contestId - type - version - url - author { - id - address - displayName - userName - profileAvatar - } - edition { - id - chainId - contractAddress - name - symbol - editionSize - royaltyBPS - fundsRecipient - defaultAdmin - saleConfig { - publicSalePrice - maxSalePurchasePerAddress - publicSaleStart - publicSaleEnd - presaleStart - presaleEnd - presaleMerkleRoot - } - description - animationURI - imageURI - referrer - } - } - } - }`, - variables: { - userIdentifier, - }, - }), - next: { tags: [`user/${userIdentifier}`], revalidate: 60 }, - }) - .then((res) => res.json()) - .then((res) => res.data.user) - .then(handleNotFound) - .then(async (res) => { - const subData = await Promise.all( - res.submissions.map(async (submission: BaseSubmission) => { - const data: Submission = await fetch(submission.url).then((res) => res.json()); - return { ...submission, data: data }; - }) - ); - return { - ...res, - submissions: subData - } - }); - return data; -}; - -export default fetchUser; \ No newline at end of file diff --git a/uplink-client/src/lib/fetch/insertSpace.ts b/uplink-client/src/lib/fetch/insertSpace.ts new file mode 100644 index 00000000..0e3ab337 --- /dev/null +++ b/uplink-client/src/lib/fetch/insertSpace.ts @@ -0,0 +1,36 @@ +"use client";; +import { handleV2MutationError } from "./handleV2MutationError"; +import { SpaceSettingsOutput } from "@/hooks/useSpaceReducer"; + +export type InsertSpaceArgs = SpaceSettingsOutput & { + csrfToken: string + spaceId?: string +} + + +export const insertSpace = async (url, + { + arg, + }: { + url: string + arg: InsertSpaceArgs + } +) => { + return fetch(`${process.env.NEXT_PUBLIC_HUB_URL}/v2/insert_space`, { + method: "POST", + headers: { + "Content-Type": "application/json", + "X-CSRF-TOKEN": arg.csrfToken, + }, + credentials: "include", + body: JSON.stringify({ + spaceId: undefined, + name: arg.name, + logoUrl: arg.logoUrl, + website: arg.website, + admins: arg.admins, + + }) + }) + .then(handleV2MutationError) +} diff --git a/uplink-client/src/lib/fetch/updateSpace.ts b/uplink-client/src/lib/fetch/updateSpace.ts new file mode 100644 index 00000000..247f4e7c --- /dev/null +++ b/uplink-client/src/lib/fetch/updateSpace.ts @@ -0,0 +1,36 @@ +"use client";; +import { handleV2MutationError } from "./handleV2MutationError"; +import { SpaceSettingsOutput } from "@/hooks/useSpaceReducer"; + +export type UpdateSpaceArgs = SpaceSettingsOutput & { + csrfToken: string + spaceId?: string +} + + +export const updateSpace = async (url, + { + arg, + }: { + url: string + arg: UpdateSpaceArgs + } +) => { + return fetch(`${process.env.NEXT_PUBLIC_HUB_URL}/v2/update_space`, { + method: "POST", + headers: { + "Content-Type": "application/json", + "X-CSRF-TOKEN": arg.csrfToken, + }, + credentials: "include", + body: JSON.stringify({ + spaceId: arg.spaceId, + name: arg.name, + logoUrl: arg.logoUrl, + website: arg.website, + admins: arg.admins, + + }) + }) + .then(handleV2MutationError) +} diff --git a/uplink-client/src/lib/gramajo.json b/uplink-client/src/lib/gramajo.json deleted file mode 100644 index d41e1d49..00000000 --- a/uplink-client/src/lib/gramajo.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "name": "The Covenant, Season 0", - "description": "***Nominators:***\n\nHolders of at least one Behind The Screen With Gramajo podcast NFT on Base (via \\[pods.media/behind-the-screen]\\(https://pods.media/behind-the-screen))\n\n***Voters:***\n\nPast guests on Behind The Screen With Gramajo Onchain listeners of the podcast\n\n***Nomination:***\n\nOne guest nomination per proposal (you can submit multiple proposals for multiple guests, just create a new proposal each time)\n\nIn your proposal, please include the following:\n\n* Guest name\n* Guest's affiliation (company or project name)\n* Relevant links to their website or socials\n* Topics you'd like to hear them talk about\n* Why listeners would enjoy hearing from this guest\n\nKeep in mind the following ideas when nominating:\n\n* Our mission at 0773H is to distill the biggest news in web3, recommendations and go learn from up and coming artist, collectors, builders in web3 while being entertaining. High signal to noise ration. No spam--just the best content.\n* My audience is someone who wants to learn, grow and have fun in the space. Art is already high brow; tech can be difficult to understand. We don’t need to scare patrons with our attitudes but instead welcome them.\n* Be cool, do cool shit.\n\n\n\n***Process:***\n\nNominations open for a week through October 7. Voting starts immediately after through October 14th.", - "image": "ipfs://QmVDqE2qcpouKG1jiEDMXaHyYViM7akwWS9Uy831BP7P2v", - "content": { - "uri": "ipfs://QmVDqE2qcpouKG1jiEDMXaHyYViM7akwWS9Uy831BP7P2v" - } -} \ No newline at end of file diff --git a/uplink-client/src/lib/threadParser.tsx b/uplink-client/src/lib/threadParser.tsx index 36d26daf..f536fb55 100644 --- a/uplink-client/src/lib/threadParser.tsx +++ b/uplink-client/src/lib/threadParser.tsx @@ -1,9 +1,9 @@ import type { TwitterSubmission } from "@/types/submission"; -import { ImageWrapper } from "@/ui/Submission/MediaWrapper"; +import { ImageWrapper } from "@/app/(legacy)/contest/components/MediaWrapper"; import { RenderStandardVideoWithLoader } from "@/ui/VideoPlayer"; import Image from "next/image"; import sanitizeHtml from "sanitize-html"; -import UplinkImage from "@/lib/UplinkImage" +import OptimizedImage from "@/lib/OptimizedImage" const createLinks = (text: string): string => { const urlRegex = /(https?:\/\/[^\s]+)/g; const twitterRegex = /([^\S]|^)@(\w+)/gi; @@ -43,7 +43,7 @@ export const ParseThread = ({ ) : (
- { - return fetch(dataUrl) - .then(res => res.blob()) -} - -export const IpfsUpload = async (file: File | Blob) => { - - const formData = new FormData(); - formData.append('file', file); - - try { - const response = await fetch("https://api.pinata.cloud/pinning/pinFileToIPFS", { - method: 'POST', - headers: { - 'Authorization': `Bearer ${process.env.NEXT_PUBLIC_PINATA_JWT}`, - }, - // @ts-expect-error - body: formData - }); - - if (!response.ok) { - console.error(`HTTP error! Status: ${response.status}`); - return null - } - - const responseData = await response.json(); - return `https://uplink.mypinata.cloud/ipfs/${responseData.IpfsHash}`; - } catch (err) { - console.error("Fetch error:", err); - return null; - } -}; - -const loadVideo = (file: File) => - new Promise((resolve, reject) => { - try { - let video = document.createElement('video'); - video.preload = 'metadata'; - - // Create an object URL for the file - const objectUrl = URL.createObjectURL(file); - video.src = objectUrl; - - video.onloadedmetadata = function () { - // Release the object URL after metadata is loaded - URL.revokeObjectURL(objectUrl); - resolve(this); - } - - video.onerror = function () { - reject("Invalid video. Please select a video file."); - } - } catch (e) { - reject(e); - } - }); - -const handleMediaUpload = async ( - event: any, - acceptedFormats: string[], - mimeTypeCallback: (mimeType: string) => void, - readerCallback: (data: any, mimeType: string) => void, - ipfsCallback: (uri: string, mimeType: string) => void, - videoThumbnailCallback?: (thumbnails: string[]) => void, - fileSizeCallback?: (size: number) => void, -) => { - - const acceptedMimeTypes = acceptedFormats.reduce((acc: string[], format: string) => { - if (format === 'image') { - return [...acc, 'image/png', 'image/jpeg', 'image/jpg', 'image/gif']; - } - if (format === 'video') { - return [...acc, 'video/mp4']; - } - if (format === 'svg') { - return [...acc, 'image/svg+xml']; - } - return acc; - }, []); - - const file = event.target.files[0]; - if (!file) { - throw new MediaUploadError({ code: 1, message: 'No file selected' }) - } - - const mimeType = file.type; - mimeTypeCallback(mimeType); - const fileSize = file.size; - if (fileSizeCallback) fileSizeCallback(fileSize) - - if (mimeType.includes("video")) { - const video: any = await loadVideo(file); - - if (video.duration > 140) throw new MediaUploadError({ code: 2, message: 'Videos must be less than 140 seconds' }); - - if (videoThumbnailCallback) { // if its a video and caller wants thumbnails, generate thumbnails - const thumbnails = await generateVideoThumbnails(file, 3, 'jpeg'); - videoThumbnailCallback(thumbnails); - } - } else { - if (fileSize > 5000000) throw new MediaUploadError({ code: 2, message: 'Images must be less than 5MB' }); - } - - if (!acceptedMimeTypes.includes(mimeType)) throw new MediaUploadError({ code: 3, message: 'Invalid file type.' }); - - const reader = new FileReader(); - reader.readAsDataURL(file); - reader.onload = () => { - readerCallback(reader.result, mimeType); - }; - reader.onerror = (error) => { - throw new MediaUploadError({ code: 4, message: 'Error reading file' }) - }; - - const response = await IpfsUpload(file); - if (!response) throw new MediaUploadError({ code: 5, message: 'Error uploading to IPFS' }) - - ipfsCallback(response, mimeType); -}; - -export default handleMediaUpload; diff --git a/uplink-client/src/providers/ContestStateProvider.tsx b/uplink-client/src/providers/ContestStateProvider.tsx deleted file mode 100644 index 540019c8..00000000 --- a/uplink-client/src/providers/ContestStateProvider.tsx +++ /dev/null @@ -1,64 +0,0 @@ -"use client"; -import { useTicks } from "@/hooks/useTicks"; -import { FetchSingleContestResponse } from "@/lib/fetch/fetchContest"; -import { ContestState } from "@/types/contest"; -import { calculateContestStatus } from "@/utils/staticContestState"; -import React, { createContext, useState } from "react" - - -export type ContestStateContextValue = { - contestId: string, - chainId: number, - contestState: ContestState | null, - stateRemainingTime: string | null, - isLoading: boolean, - tweetId: string, - type: string, - category: string, - contestAdmins: string[], -} - -export const ContestStateContext = createContext?.( - undefined -); - -export const ContestStateProvider = ({ contest, children }: { contest: FetchSingleContestResponse, children: React.ReactNode }) => { - const [contestState, setContestState] = useState(null); - const [stateRemainingTime, setStateRemainingTime] = useState(null); - - const update = (data: FetchSingleContestResponse) => { - if (!data) return - const { contestState: v1, stateRemainingTime: v2 } = calculateContestStatus(data.deadlines, data.metadata.type, data.tweetId); - setContestState(v1); - setStateRemainingTime(v2) - } - - useTicks(() => update(contest)) - - const value = { - contestId: contest.id, - chainId: contest.chainId, - isLoading: contestState === null, - contestState, - stateRemainingTime, - tweetId: contest.tweetId, - type: contest.metadata.type, - category: contest.metadata.category, - contestAdmins: contest.space.admins.map((admin) => admin.address), - } - return ( - - {children} - - ) -} - -export const useContestState = () => { - const context = React.useContext(ContestStateContext); - if (context === undefined) { - throw new Error( - "useContestState must be used within a ContestStateProvider" - ); - } - return context; -} \ No newline at end of file diff --git a/uplink-client/src/test/ContestHandler.test.ts b/uplink-client/src/test/ContestHandler.test.ts deleted file mode 100644 index 350c82f1..00000000 --- a/uplink-client/src/test/ContestHandler.test.ts +++ /dev/null @@ -1,610 +0,0 @@ -import { describe, expect, test } from "@jest/globals"; -import { sampleERC1155Token, sampleERC20Token, sampleERC721Token, sampleETHToken } from "./sampleTokens"; -import { Prompt, SubmitterRewards, VoterRewards, VotingPolicyType, validateDeadlines, validateMetadata, validatePrompt, validateSubmitterRewards, validateVoterRewards, validateVotingPolicy } from "@/ui/ContestForm/contestHandler"; - - - -describe("Contest Handler", () => { - describe("Validate Contest Metadata", () => { - test("fail with null values", () => { - const { isError, errors, data } = validateMetadata({ type: null, category: null }) - expect(isError).toBe(true) - expect(errors).toEqual({ - type: "Please select a contest type", - category: "Please select a contest category", - }) - }) - - test("pass with valid inputs", () => { - const { isError, errors, data } = validateMetadata({ - type: "standard", - category: "art", - }) - expect(isError).toBe(false) - expect(errors).toEqual({ - type: "", - category: "", - }) - expect(data).toEqual({ - type: "standard", - category: "art", - }) - }) - }); - - describe("Validate Contest Deadlines", () => { - test("fail with empty string values", () => { - const deadlines = { - startTime: "", - voteTime: "", - endTime: "", - snapshot: "", - } - const { isError, errors, data } = validateDeadlines(deadlines, false) - - expect(isError).toBe(true) - expect(errors).toEqual({ - snapshot: "snapshot date is required", - startTime: "start date is required", - voteTime: "vote date is required", - endTime: "end date is required", - }) - expect(data).toEqual(deadlines) - }) - - test("fail with incorrect order #1", () => { - const deadlines = { - startTime: new Date(Date.now() + 5 * 864e5).toISOString(), - voteTime: new Date(Date.now() + 4 * 864e5).toISOString(), - endTime: new Date(Date.now() + 3 * 864e5).toISOString(), - snapshot: new Date(Date.now() + 6 * 864e5).toISOString(), - } - const { isError, errors, data } = validateDeadlines(deadlines, false) - - expect(isError).toBe(true) - expect(errors).toEqual({ - snapshot: "snapshot date must be less than or equal to start date", - voteTime: "vote date must be after start date", - endTime: "end date must be after start date", - startTime: "", - }) - expect(data).toEqual(deadlines) - }) - - test("fail with incorrect order #2", () => { - const deadlines = { - snapshot: new Date(Date.now() + 1 * 864e5).toISOString(), - startTime: new Date(Date.now() + 2 * 864e5).toISOString(), - voteTime: new Date(Date.now() + 4 * 864e5).toISOString(), - endTime: new Date(Date.now() + 3 * 864e5).toISOString(), - } - const { isError, errors, data } = validateDeadlines(deadlines, false) - - expect(isError).toBe(true) - expect(errors).toEqual({ - snapshot: "", - voteTime: "", - endTime: "end date must be after vote date", - startTime: "", - }) - expect(data).toEqual(deadlines) - }) - - test("pass with correct order", () => { - const deadlines = { - snapshot: new Date(Date.now() + 1 * 864e5).toISOString(), - startTime: new Date(Date.now() + 2 * 864e5).toISOString(), - voteTime: new Date(Date.now() + 3 * 864e5).toISOString(), - endTime: new Date(Date.now() + 4 * 864e5).toISOString(), - } - const { isError, errors, data } = validateDeadlines(deadlines, false) - expect(isError).toBe(false) - expect(errors).toEqual({ - snapshot: "", - startTime: "", - voteTime: "", - endTime: "", - - }) - expect(data).toEqual(deadlines) - }); - - test("pass with now for snapshot", () => { - const deadlines = { - snapshot: "now", - startTime: new Date(Date.now() + 2 * 864e5).toISOString(), - voteTime: new Date(Date.now() + 3 * 864e5).toISOString(), - endTime: new Date(Date.now() + 4 * 864e5).toISOString(), - } - const { isError, errors, data } = validateDeadlines(deadlines, false) - expect(isError).toBe(false) - expect(errors).toEqual({ - snapshot: "", - startTime: "", - voteTime: "", - endTime: "", - - }) - expect(data).toEqual(deadlines) - }) - test("pass with now for startTime", () => { - const deadlines = { - snapshot: new Date(Date.now()).toISOString(), - startTime: "now", - voteTime: new Date(Date.now() + 3 * 864e5).toISOString(), - endTime: new Date(Date.now() + 4 * 864e5).toISOString(), - } - const { isError, errors, data } = validateDeadlines(deadlines, false) - expect(isError).toBe(false) - expect(errors).toEqual({ - snapshot: "", - startTime: "", - voteTime: "", - endTime: "", - - }) - expect(data).toEqual(deadlines) - }) - - test("pass with now for startTime + snapshot", () => { - const deadlines = { - snapshot: "now", - startTime: "now", - voteTime: new Date(Date.now() + 3 * 864e5).toISOString(), - endTime: new Date(Date.now() + 4 * 864e5).toISOString(), - } - const { isError, errors, data } = validateDeadlines(deadlines, false) - expect(isError).toBe(false) - expect(errors).toEqual({ - snapshot: "", - startTime: "", - voteTime: "", - endTime: "", - - }) - expect(data).toEqual(deadlines) - }) - - test("return cleaned data", () => { - const deadlines = { - snapshot: "now", - startTime: "now", - voteTime: new Date(Date.now() + 3 * 864e5).toISOString(), - endTime: new Date(Date.now() + 4 * 864e5).toISOString(), - } - const { isError, errors, data } = validateDeadlines(deadlines, true) - expect(isError).toBe(false) - expect(errors).toEqual({ - snapshot: "", - startTime: "", - voteTime: "", - endTime: "", - - }) - expect(data.voteTime).toEqual(deadlines.voteTime) - expect(data.endTime).toEqual(deadlines.endTime) - expect(data.snapshot.length).toBeGreaterThan(3) // check that it is not "now" anymore - expect(data.startTime.length).toBeGreaterThan(3) // check that it is not "now" anymore - }) - }) - - - describe("Validate Contest Prompt", () => { - - test("fail with invalid values", () => { - const prompt: Prompt = { - title: "", - body: null, - coverUrl: "https://google.com/image.png", - coverBlob: "asdf" - } - const { isError, errors, data } = validatePrompt(prompt) - - expect(isError).toBe(true) - expect(errors).toEqual({ - title: "Please provide a title", - body: "Please provide a prompt body", - coverUrl: "Invalid cover image", - }) - expect(data).toEqual(prompt) - }) - - - test("fail with ", () => { - const prompt: Prompt = { - title: "", - body: null, - coverUrl: "", - coverBlob: "" - } - const { isError, errors, data } = validatePrompt(prompt) - - expect(isError).toBe(true) - expect(errors).toEqual({ - title: "Please provide a title", - body: "Please provide a prompt body", - coverUrl: "", - }) - expect(data).toEqual(prompt) - }) - - test("fail with empty blocks", () => { - const prompt: Prompt = { - title: " aaaaaa ", - body: { - blocks: [] - }, - coverUrl: "", - coverBlob: "" - } - const { isError, errors, data } = validatePrompt(prompt) - - expect(isError).toBe(true) - expect(errors).toEqual({ - title: "", - body: "Please provide a prompt body", - coverUrl: "", - }) - expect(data).toEqual(prompt) - }) - - test("pass with valid inputs", () => { - const prompt = { - title: " aaaaaa ", - body: { - blocks: [{ - type: "paragraph", - data: { - text: "test" - } - }] - }, - coverUrl: "https://uplink.mypinata.cloud/ipfs/QmZ1Z2Z3Z4Z5Z6Z7Z8Z9Z0", - coverBlob: "asdf" - } - const { isError, errors, data } = validatePrompt(prompt) - - expect(isError).toBe(false) - expect(errors).toEqual({ - title: "", - body: "", - coverUrl: "", - }) - expect(data).toEqual(prompt) - }) - }); - - - // remember that this function is also cleaning the data (for now) - describe("Validate Submitter Rewards", () => { - - test("fail with duplicate ranks", () => { - const rewards: SubmitterRewards = { - ETH: sampleETHToken, - payouts: [ - { - rank: 1, - ETH: { amount: '1' }, - }, - { - rank: 1, - ETH: { amount: '1' }, - }, - { - rank: 2, - ETH: { amount: '1' }, - }, - ], - }; - - const { isError, errors, data } = validateSubmitterRewards(rewards); - expect(isError).toBe(true); - expect(errors).toEqual({ - duplicateRanks: [1] - }); - expect(data).toEqual(rewards); - - }) - test("pass with empty rewards", () => { - const rewards: SubmitterRewards = {}; - - const { isError, errors, data } = validateSubmitterRewards(rewards); - expect(isError).toBe(false); - expect(errors).toEqual({ - duplicateRanks: [] - }); - expect(data).toEqual(rewards); - - }) - test("pass with valid rewards", () => { - const rewards: SubmitterRewards = { - ETH: sampleETHToken, - ERC20: sampleERC20Token, - ERC721: sampleERC721Token, - ERC1155: sampleERC1155Token, - payouts: [ - { - rank: 1, - ETH: { amount: '1' }, - ERC20: { amount: '1' }, - ERC721: { tokenId: 1 }, - ERC1155: { amount: '2' }, - }, - { - rank: 2, - ETH: { amount: '1' }, - ERC20: { amount: '1' }, - ERC721: { tokenId: 1 }, - ERC1155: { amount: '2' }, - }, - ], - }; - - const { isError, errors, data } = validateSubmitterRewards(rewards); - expect(isError).toBe(false); - expect(errors).toEqual({ - duplicateRanks: [] - }); - expect(data).toEqual(rewards); - - }) - test("clean empty rewards #1", () => { - const rewards: SubmitterRewards = { - ETH: sampleETHToken, - ERC20: sampleERC20Token, - ERC721: sampleERC721Token, - ERC1155: sampleERC1155Token, - payouts: [ - { - rank: 1, - ETH: { amount: '1' }, - ERC20: { amount: '1' }, - ERC721: { tokenId: 1 }, - }, - { - rank: 2, - ETH: { amount: '1' }, - ERC20: { amount: '1' }, - ERC721: { tokenId: 1 }, - }, - ], - }; - - const { isError, errors, data } = validateSubmitterRewards(rewards); - expect(isError).toBe(false); - expect(errors).toEqual({ - duplicateRanks: [] - }); - expect(data).toEqual({ ...rewards, ERC1155: undefined }); - }) - - test("clean empty rewards #2", () => { - const rewards: SubmitterRewards = { - ETH: sampleETHToken, - ERC20: sampleERC20Token, - ERC721: sampleERC721Token, - payouts: [ - { - rank: 1, - ETH: { amount: '1' }, - ERC20: { amount: '1' }, - ERC721: { tokenId: null }, - }, - { - rank: 2, - ETH: { amount: '1' }, - ERC20: { amount: '' }, - ERC721: { tokenId: null }, - }, - ], - }; - - const { isError, errors, data } = validateSubmitterRewards(rewards); - expect(isError).toBe(false); - expect(errors).toEqual({ - duplicateRanks: [] - }); - expect(data).toEqual({ - ETH: sampleETHToken, - ERC20: sampleERC20Token, - payouts: [ - { - rank: 1, - ETH: { amount: '1' }, - ERC20: { amount: '1' }, - }, - { - rank: 2, - ETH: { amount: '1' }, - }, - ] - }); - }) - - test("clean empty rewards #3", () => { - const rewards: SubmitterRewards = { - ETH: sampleETHToken, - ERC20: sampleERC20Token, - ERC721: sampleERC721Token, - payouts: [ - { - rank: 1, - ETH: { amount: '' }, - ERC20: { amount: '' }, - ERC721: { tokenId: null }, - }, - { - rank: 2, - ETH: { amount: '' }, - ERC20: { amount: '' }, - ERC721: { tokenId: null }, - }, - ], - }; - - const { isError, errors, data } = validateSubmitterRewards(rewards); - expect(isError).toBe(false); - expect(errors).toEqual({ - duplicateRanks: [] - }); - expect(data).toEqual({}); - }) - }) - - - - describe("Validate Voter Rewards", () => { - test("fail with duplicate ranks", () => { - const rewards: VoterRewards = { - ETH: sampleETHToken, - payouts: [ - { - rank: 1, - ETH: { amount: '1' }, - }, - { - rank: 1, - ETH: { amount: '1' }, - }, - ] - } - const { errors, isError, data } = validateVoterRewards(rewards); - expect(isError).toBe(true); - expect(errors).toEqual({ - duplicateRanks: [1] - }); - expect(data).toEqual(rewards); - }); - - test("pass with empty rewards", () => { - const rewards: VoterRewards = {}; - const { errors, isError, data } = validateVoterRewards(rewards); - expect(isError).toBe(false); - expect(errors).toEqual({ - duplicateRanks: [] - }); - expect(data).toEqual(rewards); - }); - - test("pass with valid rewards", () => { - const rewards: VoterRewards = { - ETH: sampleETHToken, - ERC20: sampleERC20Token, - payouts: [ - { - rank: 1, - ETH: { amount: '1' }, - }, - { - rank: 2, - ERC20: { amount: '30' }, - } - ] - } - - const { errors, isError, data } = validateVoterRewards(rewards); - expect(isError).toBe(false); - expect(errors).toEqual({ - duplicateRanks: [] - }); - expect(data).toEqual(rewards); - }); - - test("clean empty rewards #1", () => { - const rewards: VoterRewards = { - ETH: sampleETHToken, - ERC20: sampleERC20Token, - payouts: [ - { - rank: 1, - ETH: { amount: '1' }, - }, - { - rank: 2, - ERC20: { amount: '' }, - } - - ] - } - const { errors, isError, data } = validateVoterRewards(rewards); - expect(isError).toBe(false); - expect(errors).toEqual({ - duplicateRanks: [] - }); - expect(data).toEqual({ - ETH: sampleETHToken, - payouts: [ - { - rank: 1, - ETH: { amount: '1' }, - }, - ] - }); - }); - - test("clean empty rewards #2", () => { - const rewards: VoterRewards = { - ETH: sampleETHToken, - ERC20: sampleERC20Token, - payouts: [ - { - rank: 1, - ETH: { amount: '' }, - }, - { - rank: 2, - ERC20: { amount: '' }, - } - - ] - } - const { errors, isError, data } = validateVoterRewards(rewards); - expect(isError).toBe(false); - expect(errors).toEqual({ - duplicateRanks: [] - }); - expect(data).toEqual({}); - }); - - - - - describe("Validate Voting Policy", () => { - test("fail with empty policy", () => { - const votingPolicy: VotingPolicyType[] = []; - const { errors, isError, data } = validateVotingPolicy(votingPolicy); - expect(isError).toBe(true); - expect(errors).toEqual( - "Please add at least one voting policy", - ); - expect(data).toEqual(votingPolicy); - }); - - test("pass with valid policy", () => { - const votingPolicy: VotingPolicyType[] = [ - { - token: sampleETHToken, - strategy: { - type: "weighted", - } - - }, - { - token: sampleERC20Token, - strategy: { - type: "arcade", - votingPower: '1', - } - } - ] - - const { errors, isError, data } = validateVotingPolicy(votingPolicy); - expect(isError).toBe(false); - expect(errors).toEqual(""); - expect(data).toEqual(votingPolicy); - }); - }) - - }); - -}) \ No newline at end of file diff --git a/uplink-client/src/test/SpaceHandler.test.ts b/uplink-client/src/test/SpaceHandler.test.ts deleted file mode 100644 index 7126f1b0..00000000 --- a/uplink-client/src/test/SpaceHandler.test.ts +++ /dev/null @@ -1,197 +0,0 @@ -import { SpaceBuilderProps, validateSpaceBuilderProps, reducer, validateSpaceAdmins, validateSpaceName, validateSpaceLogo, validateSpaceWebsite, validateSpaceTwitter } from '@/app/spacebuilder/spaceHandler'; -import { describe, expect, test } from "@jest/globals"; - -const calabaraAddress = "0xa943e039B1Ce670873ccCd4024AB959082FC6Dd8" -const vitalikAddress = "0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045" - -describe('Space Handler', () => { - describe('Validate Space Name', () => { - test('should fail with empty string', () => { - const { error, value } = validateSpaceName(''); - expect(error).toEqual('Name is required'); - expect(value).toEqual(''); - }) - test('should fail with string too short', () => { - const { error, value } = validateSpaceName('a'.repeat(2)); - expect(error).toEqual('Name must be at least 3 characters long'); - expect(value).toEqual('a'.repeat(2)); - }) - test('should fail with string too long', () => { - const { error, value } = validateSpaceName('a'.repeat(31)); - expect(error).toEqual('Name must be less than 30 characters long'); - expect(value).toEqual('a'.repeat(31)); - }) - test('should fail with non-alphanumeric chars', () => { - const name = 'test!'; - const { error, value } = validateSpaceName(name); - expect(error).toEqual('Name must only contain alphanumeric characters and underscores'); - expect(value).toEqual(name); - }) - }) - - describe('Validate Space Logo', () => { - test('should succeed with platform ipfs link', () => { - const logo = 'https://uplink.mypinata.cloud/ipfs/QmUxRzCcizzuNdyPRxMdn4LFQQQ5ce9cRqubnUCaR4G7Bz'; - const { error, value } = validateSpaceLogo(logo); - expect(error).toBeNull(); - expect(value).toEqual(logo); - }) - - test('should fail with invalid ipfs link', () => { - const logo = 'https://google.com'; - const { error, value } = validateSpaceLogo(logo); - expect(error).toEqual('Logo is not valid'); - expect(value).toEqual(logo); - }) - - - test('should fail with empty string', () => { - const logo = ''; - const { error, value } = validateSpaceLogo(logo); - expect(error).toEqual('Logo is required'); - expect(value).toEqual(logo); - }) - - }); - - describe('validate space website', () => { - test('should succeed with valid website #1', () => { - const website = 'https://google.com'; - const { error, value } = validateSpaceWebsite(website); - expect(error).toBeNull(); - expect(value).toEqual(website); - }) - - test('should succeed with valid website #2', () => { - const website = 'https://gnars.wtf'; - const { error, value } = validateSpaceWebsite(website); - expect(error).toBeNull(); - expect(value).toEqual(website); - }) - - test('should succeed with valid website #3', () => { - const website = 'nouns.wtf'; - const { error, value } = validateSpaceWebsite(website); - expect(error).toBeNull(); - expect(value).toEqual(website); - }) - - test('should fail with invalid website', () => { - const website = 'test'; - const { error, value } = validateSpaceWebsite(website); - expect(error).toEqual('Website is not valid'); - expect(value).toEqual(website); - }); - }) - - describe('validate space twitter', () => { - test('should succeed with valid twitter handle', () => { - const twitter = '@calabara'; - const { error, value } = validateSpaceTwitter(twitter); - expect(error).toBeNull(); - expect(value).toEqual(twitter); - }) - - test('should fail with invalid twitter handle', () => { - const twitter = 'test'; - const { error, value } = validateSpaceTwitter(twitter); - expect(error).toEqual('Twitter handle is not valid'); - expect(value).toEqual(twitter); - }) - }) - - - describe('validate space admins', () => { - - test('should succeed with valid addresses', async () => { - const admins = ['calabara.eth', 'vitalik.eth']; - const { error, value } = await validateSpaceAdmins(admins); - expect(error).toEqual([null, null]) - expect(value).toEqual([calabaraAddress, vitalikAddress]) - }) - - test('should succeed and strip empty addresses', async () => { - const admins = ['calabara.eth', '']; - const { error, value } = await validateSpaceAdmins(admins); - expect(error).toEqual([null]) - expect(value).toEqual([calabaraAddress]) - }) - - test('should strip empty addresses and return errors', async () => { - const admins = ['calabara.eth', 'test', 'vitalik.eth', '']; - const { error, value } = await validateSpaceAdmins(admins); - expect(error).toEqual([null, 'invalid address', null]) - expect(value).toEqual([calabaraAddress, 'test', vitalikAddress]) - }) - - test('should strip empty addresses, remove duplicates, and return error addresses', async () => { - const admins = ['calabara.eth', 'test', 'vitalik.eth', '', 'vitalik.eth']; - const { error, value } = await validateSpaceAdmins(admins); - expect(error).toEqual([null, 'invalid address', null]) - expect(value).toEqual([calabaraAddress, 'test', vitalikAddress]) - }) - }) - - - describe('Validate Space Builder Props', () => { - - test('should return errors', async () => { - const props: SpaceBuilderProps = { - name: "", - logoBlob: "", - logoUrl: "", - website: "", - twitter: "", - admins: [], - errors: { - admins: [], - }, - } - - const { isValid, errors, values } = await validateSpaceBuilderProps(props); - expect(isValid).toBe(false); - expect(errors).toEqual({ - name: "Name is required", - logoUrl: "Logo is required", - admins: [] - }) - expect(values).toEqual({ - name: "", - logoUrl: "", - admins: [], - }) - }) - - test('should return valid props', async () => { - const props: SpaceBuilderProps = { - name: "sample space", - logoBlob: "asdf", - logoUrl: "https://uplink.mypinata.cloud/ipfs/asdfasdf", - website: "twitter.com", - twitter: "@uplinkwtf", - admins: [calabaraAddress, vitalikAddress], - errors: { - admins: [], - }, - } - - const { isValid, errors, values } = await validateSpaceBuilderProps(props); - expect(isValid).toBe(true); - expect(errors).toEqual({ - admins: [null, null] - }) - expect(values).toEqual({ - name: "sample space", - logoUrl: "https://uplink.mypinata.cloud/ipfs/asdfasdf", - website: "twitter.com", - twitter: "@uplinkwtf", - admins: [calabaraAddress, vitalikAddress], - }) - }) - - }); - - - - -}) diff --git a/uplink-client/src/test/sampleTokens.ts b/uplink-client/src/test/sampleTokens.ts deleted file mode 100644 index 55d3b690..00000000 --- a/uplink-client/src/test/sampleTokens.ts +++ /dev/null @@ -1,35 +0,0 @@ -import { IERCToken, INativeToken, IToken } from "@/types/token"; - -export const sampleERC1155Token: IERCToken = { - type: "ERC1155", - address: "0x7c2748C7Ec984b559EADc39C7a4944398E34911a", - symbol: "TNS", - decimals: 0, - tokenId: 2, - chainId: 1 -} - -export const sampleERC20Token: IERCToken = { - type: "ERC20", - address: "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48", - symbol: "USDC", - decimals: 6, - tokenId: null, - chainId: 1, -} - -export const sampleERC721Token: IERCToken = { - type: "ERC721", - address: "0x9C8fF314C9Bc7F6e59A9d9225Fb22946427eDC03", - symbol: "NOUN", - decimals: 0, - tokenId: null, - chainId: 1, -} - -export const sampleETHToken: INativeToken = { - type: "ETH", - symbol: "ETH", - decimals: 18, - chainId: 1, -} \ No newline at end of file diff --git a/uplink-client/src/test/zora.test.ts b/uplink-client/src/test/zora.test.ts deleted file mode 100644 index 118254a4..00000000 --- a/uplink-client/src/test/zora.test.ts +++ /dev/null @@ -1,368 +0,0 @@ -import { describe, expect, test } from "@jest/globals"; -import { - ConfigurableZoraEditionSchema, - ConfigurableZoraEditionInput, - EditionNameSchema, - EditionSymbolSchema, - EditionSizeSchema, - EditionRoyaltyBPSSchema, - EditionPublicSalePriceSchema, - EditionSalesConfigSchema, -} from "@/hooks/useCreateZoraEdition"; -import { uint64MaxSafe } from "@/utils/uint64"; - -const unixRegex = /^\d{10}$/; - -describe("validate zora config", () => { - - describe("validate edition name", () => { }) - describe("validate edition symbol", () => { }) - describe("validate edition description", () => { }) - - describe("validate edition size", () => { - test("open edition", () => { - const size = "open"; - const result = EditionSizeSchema.safeParse(size); - expect(result.success).toBe(true); - if (result.success) { - expect(result.data).toBe("18446744073709551615"); - } - }) - test("1/1", () => { - const size = "one"; - const result = EditionSizeSchema.safeParse(size); - expect(result.success).toBe(true); - if (result.success) { - expect(result.data).toBe("1"); - } - }) - test("fixed", () => { - const size = "100"; - const result = EditionSizeSchema.safeParse(size); - expect(result.success).toBe(true); - if (result.success) { - expect(result.data).toBe("100"); - } - }) - test("fail with empty string", () => { - const size = ""; - const result: any = EditionSizeSchema.safeParse(size); - const errors = result.error.format(); - expect(result.success).toBe(false); - expect(errors._errors[0]).toBe("Edition size is required"); - }) - }) - - describe("validate edition royaltyBPS", () => { - test("0%", () => { - const bps = "zero"; - const result = EditionRoyaltyBPSSchema.safeParse(bps); - expect(result.success).toBe(true); - if (result.success) { - expect(result.data).toBe(0); - } - }) - test("5%", () => { - const bps = "five"; - const result = EditionRoyaltyBPSSchema.safeParse(bps); - expect(result.success).toBe(true); - if (result.success) { - expect(result.data).toBe(500); - } - }) - test("5.5%", () => { - const bps = "5.5" - const result = EditionRoyaltyBPSSchema.safeParse(bps); - expect(result.success).toBe(true); - if (result.success) { - expect(result.data).toBe(550); - } - }) - test("0.05%", () => { - const bps = "0.05" - const result = EditionRoyaltyBPSSchema.safeParse(bps); - expect(result.success).toBe(true); - if (result.success) { - expect(result.data).toBe(5); - } - }) - test("integer precision", () => { - const bps = "5.00005" - const result = EditionRoyaltyBPSSchema.safeParse(bps); - expect(result.success).toBe(true); - if (result.success) { - expect(result.data).toBe(500); - } - }) - test("fail with empty string", () => { - const bps = ""; - const result: any = EditionRoyaltyBPSSchema.safeParse(bps); - const errors = result.error.format(); - expect(result.success).toBe(false); - expect(errors._errors[0]).toBe("Royalty % is required"); - }) - }) - - describe("validate edition public sale price", () => { - test("free", () => { }) - test("0.001 eth", () => { - const price = "0.001"; - const result = EditionPublicSalePriceSchema.safeParse(price); - expect(result.success).toBe(true); - if (result.success) { - expect(result.data).toBe("1000000000000000"); - } - }) - - test("0.00420", () => { - const price = "0.00420"; - const result = EditionPublicSalePriceSchema.safeParse(price); - expect(result.success).toBe(true); - if (result.success) { - expect(result.data).toBe("4200000000000000"); - } - }) - - test("1.5 eth", () => { - const price = "1.5"; - const result = EditionPublicSalePriceSchema.safeParse(price); - expect(result.success).toBe(true); - if (result.success) { - expect(result.data).toBe("1500000000000000000"); - } - }) - test("fail with empty string", () => { - const price = ""; - const result: any = EditionPublicSalePriceSchema.safeParse(price); - const errors = result.error.format(); - expect(result.success).toBe(false); - expect(errors._errors[0]).toBe("Edition price is required"); - }) - }) - - - describe("validate public sale datetimes", () => { - test("startTime = now, endTime = forever", () => { - const nowUnix = Math.floor(new Date().getTime() / 1000); - const salesConfig = { - publicSalePrice: "1", - publicSaleStart: "now", - publicSaleEnd: "forever", - } - - const result = EditionSalesConfigSchema.safeParse(salesConfig); - expect(result.success).toBe(true); - if (result.success) { - expect(result.data.publicSaleStart).toMatch(unixRegex); - expect(result.data.publicSaleEnd).toBe(uint64MaxSafe.toString()); - expect(nowUnix - parseInt(result.data.publicSaleStart)).toBeLessThan(5); // 5 second tolerance - } - }) - - test("startTime = now, endTime = week", () => { - const salesConfig = { - publicSalePrice: "1", - publicSaleStart: "now", - publicSaleEnd: "week", - } - - const result = EditionSalesConfigSchema.safeParse(salesConfig); - expect(result.success).toBe(true); - if (result.success) { - expect(result.data.publicSaleStart).toMatch(unixRegex); - expect(result.data.publicSaleEnd).toMatch(unixRegex); - expect(parseInt(result.data.publicSaleEnd) - parseInt(result.data.publicSaleStart)).toBe(604800); - } - }) - - test("startTime = custom, endTime = custom", () => { - const plus1day = new Date(Date.now() + 1000 * 60 * 60 * 24).toISOString(); - const plus2day = new Date(Date.now() + 1000 * 60 * 60 * 48).toISOString(); - const unixPlus1Day = Math.floor(new Date(plus1day).getTime() / 1000); - const unixPlus2Day = Math.floor(new Date(plus2day).getTime() / 1000); - const salesConfig = { - publicSalePrice: "1", - publicSaleStart: plus1day, - publicSaleEnd: plus2day, - } - const result = EditionSalesConfigSchema.safeParse(salesConfig); - expect(result.success).toBe(true); - - if (result.success) { - expect(result.data.publicSaleStart).toMatch(unixRegex); - expect(result.data.publicSaleEnd).toMatch(unixRegex); - expect(parseInt(result.data.publicSaleEnd) - parseInt(result.data.publicSaleStart)).toBe(86400); - expect(parseInt(result.data.publicSaleStart)).toBe(unixPlus1Day); - expect(parseInt(result.data.publicSaleEnd)).toBe(unixPlus2Day); - } - }) - - test("startTime < now", () => { - const minus1day = new Date(Date.now() - 1000 * 60 * 60 * 24).toISOString(); - const salesConfig = { - publicSalePrice: "1", - publicSaleStart: minus1day, - publicSaleEnd: "forever", - } - const result = EditionSalesConfigSchema.safeParse(salesConfig); - expect(result.success).toBe(false); - const errrors = result.error.format(); - expect(errrors.publicSaleStart._errors[0]).toBe("Public sale start must be in the future"); - }) - - test("endTime < startTime", () => { - const plus1day = new Date(Date.now() + 1000 * 60 * 60 * 24).toISOString(); - const plus2day = new Date(Date.now() + 1000 * 60 * 60 * 48).toISOString(); - - const salesConfig = { - publicSalePrice: "1", - publicSaleStart: plus2day, - publicSaleEnd: plus1day, - } - const result = EditionSalesConfigSchema.safeParse(salesConfig); - expect(result.success).toBe(false); - const errrors = result.error.format(); - expect(errrors.publicSaleEnd._errors[0]).toBe("Public sale end must be after public sale start"); - }) - }) - - // test("should succeed with valid config", () => { - // const input: ConfigurableZoraEditionInput = { - // name: "test", - // symbol: "TST", - // editionSize: "1", - // royaltyBPS: "1000", - // description: "test", - // animationURI: "", - // imageURI: "https://test.com", - // salesConfig: { - // publicSalePrice: "1", - // publicSaleStart: "now", - // publicSaleEnd: new Date(Date.now() + 1000 * 60 * 60).toISOString(), - // } - // } - - // const result: any = ConfigurableZoraEditionSchema.safeParse(input); - // expect(result.success).toBe(true); - // const { salesConfig, ...output } = result.data; - // const { salesConfig: sc_in, ...rest } = input; - // expect(output).toStrictEqual(rest); - // expect(salesConfig.publicSaleStart).toMatch(unixRegex); - // expect(salesConfig.publicSaleEnd).toMatch(unixRegex); - // }); - - // test("should fail with invalid text inputs", () => { - // const input: ConfigurableZoraEditionInput = { - // name: "", - // symbol: "", - // editionSize: "1", - // royaltyBPS: "1000", - // description: "", - // animationURI: "", - // imageURI: "https://test.com", - // salesConfig: { - // publicSalePrice: "1", - // publicSaleStart: "now", - // publicSaleEnd: new Date(Date.now() + 1000 * 60 * 60).toISOString(), - // } - // } - - // const result: any = ConfigurableZoraEditionSchema.safeParse(input); - // const errors = result.error.format(); - // expect(result.success).toBe(false); - // expect(errors.name._errors[0]).toBe("Name is required"); - // expect(errors.symbol._errors[0]).toBe("Symbol is required"); - // expect(errors.description._errors[0]).toBe("Description is required"); - // }) - - - // test("should fail without imageURI", () => { - // const input: ConfigurableZoraEditionInput = { - // name: "test", - // symbol: "test", - // editionSize: "1", - // royaltyBPS: "1000", - // description: "test", - // animationURI: "", - // imageURI: "", - // salesConfig: { - // publicSalePrice: "1", - // publicSaleStart: "now", - // publicSaleEnd: new Date(Date.now() + 1000 * 60 * 60).toISOString(), - // } - // } - - // const result: any = ConfigurableZoraEditionSchema.safeParse(input); - // const errors = result.error.format(); - // expect(result.success).toBe(false); - // expect(errors.imageURI._errors[0]).toBe("Image must be set"); - // }) - - // test("should fail without video thumbnail", () => { - // const input: ConfigurableZoraEditionInput = { - // name: "test", - // symbol: "test", - // editionSize: "1", - // royaltyBPS: "1000", - // description: "test", - // animationURI: "https://test", - // imageURI: "", - // salesConfig: { - // publicSalePrice: "1", - // publicSaleStart: "now", - // publicSaleEnd: new Date(Date.now() + 1000 * 60 * 60).toISOString(), - // } - // } - - // const result: any = ConfigurableZoraEditionSchema.safeParse(input); - // const errors = result.error.format(); - // expect(result.success).toBe(false); - // expect(errors.animationURI._errors[0]).toBe("Video thumbnail must be set"); - // }) - - // test("should fail with sale start > sale end", () => { - // const input: ConfigurableZoraEditionInput = { - // name: "test", - // symbol: "test", - // editionSize: "1", - // royaltyBPS: "1000", - // description: "test", - // animationURI: "", - // imageURI: "https://test", - // salesConfig: { - // publicSalePrice: "1", - // publicSaleStart: new Date(Date.now() + 1000 * 60 * 60).toISOString(), - // publicSaleEnd: new Date(Date.now() + 1000 * 60 * 30).toISOString(), - // } - // } - - // const result: any = ConfigurableZoraEditionSchema.safeParse(input); - // const errors = result.error.format(); - // expect(result.success).toBe(false); - // expect(errors.salesConfig.publicSaleStart._errors[0]).toBe("Public sale start must be before public sale end"); - // }) - - // test("should fail with sale start in past", () => { - // const input: ConfigurableZoraEditionInput = { - // name: "test", - // symbol: "test", - // editionSize: "1", - // royaltyBPS: "1000", - // description: "test", - // animationURI: "", - // imageURI: "https://test", - // salesConfig: { - // publicSalePrice: "1", - // publicSaleStart: new Date(Date.now() - 1000 * 60 * 60).toISOString(), - // publicSaleEnd: new Date(Date.now() + 1000 * 60 * 30).toISOString(), - // } - // } - - // const result: any = ConfigurableZoraEditionSchema.safeParse(input); - // const errors = result.error.format(); - // expect(result.success).toBe(false); - // expect(errors.salesConfig.publicSaleStart._errors[0]).toBe("Public sale start must be in the future"); - // }) - - -}) \ No newline at end of file diff --git a/uplink-client/src/types/contest.ts b/uplink-client/src/types/contest.ts index 6cf00241..8672a29c 100644 --- a/uplink-client/src/types/contest.ts +++ b/uplink-client/src/types/contest.ts @@ -1,3 +1,5 @@ +import { Space } from "./space"; +import { Submission } from "./submission"; import { IToken } from "./token"; import type { OutputData } from "@editorjs/editorjs"; @@ -152,8 +154,6 @@ export const isWeightedVotingStrategy = (votingStrategy: VotingStrategy): voting return votingStrategy.strategyType === 'weighted'; } -// - export type ContestState = "pending" | "submitting" | "voting" | "closed"; export type ContestPromptData = { @@ -162,7 +162,7 @@ export type ContestPromptData = { body: OutputData; } -export type ReadableContest = { +export type LegacyContest = { id: string; chainId: number; spaceId: string; @@ -176,4 +176,8 @@ export type ReadableContest = { submitterRewards: Array; voterRewards: Array; votingPolicy: Array; + submissions: Array; + space: Space; }; + +export type LegacyContestWithPrompt = LegacyContest & { promptData: ContestPromptData }; \ No newline at end of file diff --git a/uplink-client/src/types/edition.ts b/uplink-client/src/types/edition.ts deleted file mode 100644 index 37ed643d..00000000 --- a/uplink-client/src/types/edition.ts +++ /dev/null @@ -1,27 +0,0 @@ -export type Edition = { - id: string; - chainId: number; - contractAddress: string; - name: string - symbol: string - editionSize: string - royaltyBPS: number - fundsRecipient: string - defaultAdmin: string - saleConfig: EditionSaleConfig; - description: string; - animationURI: string; - imageURI: string; - referrer: string; - -} - -export type EditionSaleConfig = { - publicSalePrice: string; - maxSalePurchasePerAddress: number; - publicSaleStart: string; - publicSaleEnd: string; - presaleStart: string; - presaleEnd: string; - presaleMerkleRoot: string; -} \ No newline at end of file diff --git a/uplink-client/src/types/mintBoard.ts b/uplink-client/src/types/mintBoard.ts deleted file mode 100644 index 346ebad3..00000000 --- a/uplink-client/src/types/mintBoard.ts +++ /dev/null @@ -1,42 +0,0 @@ -import { Edition } from "./edition"; -import { Space } from "./space"; -import { User } from "./user"; -import { z } from "zod"; -import { zeroAddress } from "viem"; - - -export type MintBoard = { - id: string; - space: Space - spaceId: string; - created: string; - chainId: number; - enabled: boolean; - threshold: number; - boardTitle: string; - boardDescription: string; - name: string; - symbol: string; - editionSize: string; - publicSalePrice: string; - publicSaleStart: string; - publicSaleEnd: string; - description: string; - referrer: string; -} - -export type PaginatedMintBoardPosts = { - posts: Array; - pageInfo: { - endCursor: string; - hasNextPage: boolean - } -} - -export type MintBoardPost = { - id: string; - created: string; - edition: Edition; - author: User; - totalMints: number; -} diff --git a/uplink-client/src/types/space.ts b/uplink-client/src/types/space.ts index 35759445..0e801663 100644 --- a/uplink-client/src/types/space.ts +++ b/uplink-client/src/types/space.ts @@ -1,4 +1,3 @@ -import { MintBoard } from "./mintBoard"; import { IToken } from "./token"; export type Admin = { diff --git a/uplink-client/src/types/submission.ts b/uplink-client/src/types/submission.ts index cbd1aa37..62232e6b 100644 --- a/uplink-client/src/types/submission.ts +++ b/uplink-client/src/types/submission.ts @@ -2,7 +2,6 @@ export type SubmissionType = "standard" | "twitter" export type SubmissionFormat = "image" | "video" | "text" import type { OutputData } from "@editorjs/editorjs"; -import { Edition } from "./edition"; import { User } from "./user"; export type BaseSubmission = { @@ -15,7 +14,6 @@ export type BaseSubmission = { author: User | null; rank: string | null; totalVotes: string | null; - edition: Edition | null; }; export type TwitterSubmission = BaseSubmission & { @@ -53,7 +51,3 @@ export const isStandardSubmission = (submission: Submission): submission is Stan export const isTwitterSubmission = (submission: Submission): submission is TwitterSubmission => { return submission.type === 'twitter'; } - -export const isNftSubmission = (submission: Submission): boolean => { - return Boolean(submission.edition); -} \ No newline at end of file diff --git a/uplink-client/src/ui/AddressDisplay/AddressDisplay.tsx b/uplink-client/src/ui/AddressDisplay/AddressDisplay.tsx index 0060eb4f..a139225c 100644 --- a/uplink-client/src/ui/AddressDisplay/AddressDisplay.tsx +++ b/uplink-client/src/ui/AddressDisplay/AddressDisplay.tsx @@ -1,12 +1,9 @@ -"use client"; - +"use client";; import { User } from "@/types/user"; import Noggles from "../Noggles/Noggles"; -import useEnsName from "@/hooks/useEnsName"; import { Session } from "@/providers/SessionProvider"; -import UplinkImage from "@/lib/UplinkImage"; -import { ImageWrapper } from "@/ui/Submission/MediaWrapper" -import { Address } from "viem"; +import OptimizedImage from "@/lib/OptimizedImage"; +import { ImageWrapper } from "@/app/(legacy)/contest/components/MediaWrapper" import { useWalletDisplayText } from "@/hooks/useWalletDisplay"; import { useEffect } from "react"; @@ -120,7 +117,7 @@ export const UserAvatar = ({ if (user?.profileAvatar) return (
- +
) diff --git a/uplink-client/src/ui/Card/Card.tsx b/uplink-client/src/ui/Card/Card.tsx deleted file mode 100644 index 7dbbde27..00000000 --- a/uplink-client/src/ui/Card/Card.tsx +++ /dev/null @@ -1,79 +0,0 @@ -import * as React from "react" - -import { cn } from "@/lib/shadcn" - -const Card = React.forwardRef< - HTMLDivElement, - React.HTMLAttributes ->(({ className, ...props }, ref) => ( -
-)) -Card.displayName = "Card" - -const CardHeader = React.forwardRef< - HTMLDivElement, - React.HTMLAttributes ->(({ className, ...props }, ref) => ( -
-)) -CardHeader.displayName = "CardHeader" - -const CardTitle = React.forwardRef< - HTMLParagraphElement, - React.HTMLAttributes ->(({ className, ...props }, ref) => ( -

-)) -CardTitle.displayName = "CardTitle" - -const CardDescription = React.forwardRef< - HTMLParagraphElement, - React.HTMLAttributes ->(({ className, ...props }, ref) => ( -

-)) -CardDescription.displayName = "CardDescription" - -const CardContent = React.forwardRef< - HTMLDivElement, - React.HTMLAttributes ->(({ className, ...props }, ref) => ( -

-)) -CardContent.displayName = "CardContent" - -const CardFooter = React.forwardRef< - HTMLDivElement, - React.HTMLAttributes ->(({ className, ...props }, ref) => ( -
-)) -CardFooter.displayName = "CardFooter" - -export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent } diff --git a/uplink-client/src/ui/ChainLabel/ChainLabel.tsx b/uplink-client/src/ui/ChainLabel/ChainLabel.tsx new file mode 100644 index 00000000..7e251553 --- /dev/null +++ b/uplink-client/src/ui/ChainLabel/ChainLabel.tsx @@ -0,0 +1,20 @@ + + +const BaseChainLogo = ({ px }: { px: number }) => { + return ( + + + + ) +} + +export const ChainLabel = ({ chainId, px }: { chainId: number, px: number }) => { + if (chainId === 8453 || chainId === 84532) { + return ( + + ) + } + + return null; +} + diff --git a/uplink-client/src/ui/ChannelSettings/ChainSelect.tsx b/uplink-client/src/ui/ChannelSettings/ChainSelect.tsx index 288bc747..b28de604 100644 --- a/uplink-client/src/ui/ChannelSettings/ChainSelect.tsx +++ b/uplink-client/src/ui/ChannelSettings/ChainSelect.tsx @@ -1,6 +1,6 @@ import { Select, SelectContent, SelectGroup, SelectItem, SelectLabel, SelectTrigger, SelectValue } from "@/ui/DesignKit/Select"; import { getChainName, supportedChains } from "@/lib/chains/supportedChains"; -import { ChainLabel } from "../ContestLabels/ContestLabels"; +import { ChainLabel } from "../ChainLabel/ChainLabel"; export const ChainSelect = ({ chainId, setChainId }: { chainId: 8453 | 84532, setChainId: (val: 8453 | 84532) => void }) => { diff --git a/uplink-client/src/ui/ChannelSidebar/ContestDetailsV2.tsx b/uplink-client/src/ui/ChannelSidebar/ContestDetailsV2.tsx index 94364cb3..ba1cd586 100644 --- a/uplink-client/src/ui/ChannelSidebar/ContestDetailsV2.tsx +++ b/uplink-client/src/ui/ChannelSidebar/ContestDetailsV2.tsx @@ -19,7 +19,7 @@ import { TbLoader2 } from "react-icons/tb"; import toast from "react-hot-toast"; import { useTransmissionsErrorHandler } from "@/hooks/useTransmissionsErrorHandler"; import { ChainId } from "@/types/chains"; -import { ExpandSection } from "../ContestDetails/client"; +import { ExpandSection } from "../../app/(legacy)/contest/[id]/client"; const compact_formatter = new Intl.NumberFormat('en', { notation: 'compact' }) @@ -331,11 +331,13 @@ const RewardsSection = ({ // rewards (need types) const ContestDetailsV2 = ({ + spaceName, contractId, transportConfig, creatorLogic, minterLogic }: { + spaceName: string; contractId: ContractID; transportConfig: IFiniteTransportConfig; creatorLogic: ILogicConfig | null; @@ -343,7 +345,6 @@ const ContestDetailsV2 = ({ }) => { const { chainId, contractAddress } = splitContractID(contractId); - const { channelState, stateRemainingTime } = useFiniteTransportLayerState(contractId); const { settle, status, txHash, error } = useSettleFiniteChannel(); const isSettling = status === "pendingApproval" || status === "txInProgress"; const { mutateSwrChannel } = useChannel(contractId); @@ -388,29 +389,6 @@ const ContestDetailsV2 = ({
- {/* */} - - {/* - - - - - - - - */} {({ isLoading, channelState, stateRemainingTime }) => { @@ -425,7 +403,7 @@ const ContestDetailsV2 = ({ {channelState ? stateRemainingTime : }

- +

@@ -449,7 +427,6 @@ const ContestDetailsV2 = ({
) - return null; }}
diff --git a/uplink-client/src/ui/ChannelSidebar/ContestSidebarV2.tsx b/uplink-client/src/ui/ChannelSidebar/ContestSidebarV2.tsx index 2285e093..b3eb9579 100644 --- a/uplink-client/src/ui/ChannelSidebar/ContestSidebarV2.tsx +++ b/uplink-client/src/ui/ChannelSidebar/ContestSidebarV2.tsx @@ -1,6 +1,6 @@ "use client"; import { useState, useEffect } from "react"; -import { DetailsSkeleton } from "@/ui/ContestDetails/ContestDetails"; +import { DetailsSkeleton } from "@/app/(legacy)/contest/components/ContestDetails"; import { Tabs, TabsContent, TabsList, TabsTrigger } from "../DesignKit/Tabs"; import { useFiniteTransportLayerState } from "@/hooks/useFiniteTransportLayerState"; import { ContractID } from "@/types/channel"; diff --git a/uplink-client/src/ui/ChannelSidebar/VoteCart.tsx b/uplink-client/src/ui/ChannelSidebar/VoteCart.tsx index 5e4164ea..1140d64d 100644 --- a/uplink-client/src/ui/ChannelSidebar/VoteCart.tsx +++ b/uplink-client/src/ui/ChannelSidebar/VoteCart.tsx @@ -1,7 +1,7 @@ "use client"; import { useVoteCart } from "@/hooks/useVoteCart"; import { parseIpfsUrl } from "@/lib/ipfs"; -import UplinkImage from "@/lib/UplinkImage"; +import OptimizedImage from "@/lib/OptimizedImage"; import { ContractID, ChannelTokenWithUserBalance } from "@/types/channel"; import { HiSparkles, HiTrash } from "react-icons/hi2"; import { Input } from "../DesignKit/Input"; @@ -17,7 +17,7 @@ const CartMedia = ({ token }: { token: ChannelTokenWithUserBalance }) => { return (
- { - return ( -
-
- {icon} -

- {bannerText} -

-
- {children} -
- ); -}; - -const RenderRemainingTime = () => { - const { stateRemainingTime } = useContestState(); - return <>{stateRemainingTime}; -}; - -export const ExpandSection = ({ - data, - label, - children, -}: { - data: any[]; - label: string; - children: React.ReactNode; -}) => { - const [isModalOpen, setIsModalOpen] = useState(false); - return ( -
- {data.length > 3 && ( - setIsModalOpen(true)} - > - {label} - - )} - setIsModalOpen(false)} className="w-full max-w-[500px]"> - {children} - -
- ); -}; - -export const RenderStateSpecificDialog = ({ - startTime, - prompt, - contestId, - spaceId, -}: { - startTime: string; - prompt: { - title: string; - body: OutputData | null; - coverUrl?: string; - }; - contestId: string; - spaceId: string; -}) => { - const { contestState, type, tweetId } = useContestState(); - if (!contestState) return - else if (contestState === 'pending') { - return - } - else if (contestState === 'submitting') { - return - } - else if (contestState === 'voting') { - return - } - else if (contestState === 'closed') { - return - } - return null; -} - -const Pending = () => { - return ( - } - > -

- {`This contest hasn't started yet. Check back soon!`} -

-
- ); -}; - -const Submit = ({ contestId }: { contestId: string }) => { - return ( -
-
- - - -

- -

-
-
- ); -}; - -const Vote = () => { - return ( -
- -

- -

-
- ); -}; - -const Closed = ({ contestId }: { contestId: string }) => { - const { downloadGnosisResults } = useContestInteractionApi(contestId); - return ( - } - > -
- -
-
- ); -}; diff --git a/uplink-client/src/ui/ContestHeading/ContestHeading.tsx b/uplink-client/src/ui/ContestHeading/ContestHeading.tsx index fe7778e1..d8418d4c 100644 --- a/uplink-client/src/ui/ContestHeading/ContestHeading.tsx +++ b/uplink-client/src/ui/ContestHeading/ContestHeading.tsx @@ -1,25 +1,7 @@ -import dynamic from "next/dynamic"; -import UplinkImage from "@/lib/UplinkImage" -const ParseBlocks = dynamic(() => import("@/lib/blockParser"), { - ssr: false, - loading: () => ( -
-
-
-
-
-
-
-
- ) -}); -import { CategoryLabel } from "../ContestLabels/ContestLabels"; +import OptimizedImage from "@/lib/OptimizedImage" + import Link from "next/link"; -import LiveContestState from "../ContestLabels/LiveContestState"; -import { ImageWrapper } from "../Submission/MediaWrapper"; -import MobileContestActions from "../MobileContestActions/MobileContestActions"; -import ContestDetails from "../ContestDetails/ContestDetails"; -import fetchContest from "@/lib/fetch/fetchContest"; +import { ImageWrapper } from "../../app/(legacy)/contest/components/MediaWrapper"; import ExpandableTextSection from "../ExpandableTextSection/ExpandableTextSection"; import { ContractID, TokenMetadata } from "@/types/channel"; import { parseIpfsUrl } from "@/lib/ipfs"; @@ -28,161 +10,6 @@ import { Space } from "@/types/space"; import { Button } from "../DesignKit/Button"; import { SmallScreenToolbar } from "./ClientUtils"; -const ContestHeading = async ({ - contestId, -}: { - contestId: string; -}) => { - const contestData = await fetchContest(contestId).then(async (res) => { - const promptData = await fetch(res.promptUrl).then((res) => res.json()); - return { ...res, prompt: promptData }; - }); - - const { prompt, space, metadata, chainId } = contestData; - - return ( -
-
-
-
-

- {prompt.title} -

-
- - - - - {space.displayName} - - - - {/* render a details button when the screen gets smaller */} - {/*
- - } - ui={{ - classNames: - "text-sm font-[600] text-t2 hover:underline hover:text-t1 ", - label:

details

, - }} - /> -
*/} -
-
- - - -
-
-
- {prompt.coverUrl && ( - - - - )} -
-
-
-
-
- -
-
- ); -}; - -// export const ContestHeadingV2 = ({ -// space, -// contestMetadata, -// contractId, -// }: { -// space: Space; -// contestMetadata: TokenMetadata; -// contractId: ContractID; -// }) => { - -// return ( -//
-//
-//

-// {contestMetadata.name} -//

-//
-// -// -// -// -// -// -//
-//
-//
-// -// -// -//
-//
-// {contestMetadata.image && ( -// -// -// -// )} -//
-//
-//
-//
-//
-// -//
-//
-// ); - -// } - export const ContestHeadingV2 = ({ space, contestMetadata, @@ -206,7 +33,7 @@ export const ContestHeadingV2 = ({ href={`/${space.name}`} draggable={false} > - {contestMetadata.image && ( - {
); }; - -export default ContestHeading; diff --git a/uplink-client/src/ui/ContestLabels/ContestLabels.tsx b/uplink-client/src/ui/ContestLabels/ContestLabels.tsx deleted file mode 100644 index c882d51b..00000000 --- a/uplink-client/src/ui/ContestLabels/ContestLabels.tsx +++ /dev/null @@ -1,120 +0,0 @@ -import { BiTime } from "react-icons/bi"; -import { ContestCategory, ContestState } from "@/types/contest"; -import { BaseChainLogo } from "@/lib/chains/basechain" -// return color for contest category -const contestCategoryColor = (contestCategory: ContestCategory) => { - switch (contestCategory) { - case "art": - return { - text: "text-orange-300", - bg: "bg-orange-300", - }; - case "music": - return { - text: "text-yellow-300", - bg: "bg-yellow-300", - }; - case "writing": - return { - text: "text-green-300", - bg: "bg-green-300", - }; - case "video": - return { - text: "text-blue-300", - bg: "bg-blue-300", - }; - case "photography": - return { - text: "text-indigo-300", - bg: "bg-indigo-300", - }; - case "design": - return { - text: "text-purple-300", - bg: "bg-purple-300", - }; - case "memes": - return { - text: "text-red-300", - bg: "bg-red-300", - }; - case "other": - return { - text: "text-gray-300", - bg: "bg-gray-300", - }; - } -}; - -export const CategoryLabel = ({ category }: { category: ContestCategory }) => { - const { text, bg } = contestCategoryColor(category); - return ( -

- {category} -

- ); -}; - -// return color for contest state -const contestStatusColor = (contestState: ContestState) => { - switch (contestState) { - case "pending": - return { - text: "text-purple-500", - bg: "bg-purple-500", - }; - case "submitting": - return { - text: "text-green-300", - bg: "bg-green-300", - }; - case "voting": - return { - text: "text-yellow-500", - bg: "bg-yellow-500", - }; - case "closed": - return { - text: "text-gray-600", - bg: "bg-gray-600", - }; - } -}; - -export const StatusLabel = ({ status }: { status: ContestState }) => { - const { text, bg } = contestStatusColor(status); - return ( -

- {status} -

- ); -}; - -export const ChainLabel = ({ chainId, px }: { chainId: number, px: number }) => { - if (chainId === 8453 || chainId === 84532) { - return ( - - ) - } - - return null; -} - - -export const RemainingTimeLabel = ({ - remainingTime, -}: { - remainingTime: string | null; -}) => { - if (remainingTime) - return ( -
- -

- {remainingTime} -

-
- ); - else return null; -}; diff --git a/uplink-client/src/ui/ContestLabels/LiveContestState.tsx b/uplink-client/src/ui/ContestLabels/LiveContestState.tsx deleted file mode 100644 index 5b05a792..00000000 --- a/uplink-client/src/ui/ContestLabels/LiveContestState.tsx +++ /dev/null @@ -1,13 +0,0 @@ -"use client"; -import { useContestState } from "@/providers/ContestStateProvider"; -import { StatusLabel } from "./ContestLabels"; -import type { ContestState } from "@/types/contest"; - -const LiveContestState = () => { - const { contestState } = useContestState(); - if (!contestState) - return
; - return ; -}; - -export default LiveContestState; diff --git a/uplink-client/src/ui/ContestSidebar/ContestSidebar.tsx b/uplink-client/src/ui/ContestSidebar/ContestSidebar.tsx deleted file mode 100644 index b47d1d48..00000000 --- a/uplink-client/src/ui/ContestSidebar/ContestSidebar.tsx +++ /dev/null @@ -1,54 +0,0 @@ -"use client"; -import { useContestState } from "@/providers/ContestStateProvider"; -import { useState, useEffect } from "react"; -import { DetailsSkeleton } from "@/ui/ContestDetails/ContestDetails"; -import { Tabs, TabsContent, TabsList, TabsTrigger } from "../DesignKit/Tabs"; - -const ContestSidebar = ({ detailsChild, voteChild }: { detailsChild: React.ReactNode, voteChild: React.ReactNode }) => { - - const { contestState } = useContestState(); - const [isVoting, setIsVoting] = useState(false); - const [tab, setTab] = useState(null); - - useEffect(() => { - if (contestState) { - if (contestState === "voting") { - setTab(1) - setIsVoting(true); - } - else { - setTab(0) - setIsVoting(false); - } - } - }, [contestState]) - - if (!contestState) return ( -
- -
- ) - return ( -
- {isVoting ? - - - Details - Vote - -
- {detailsChild} - {voteChild} -
-
- : ( -
- {detailsChild} -
- )} - -
- ) -} - -export default ContestSidebar; \ No newline at end of file diff --git a/uplink-client/src/ui/DesignKit/Form.tsx b/uplink-client/src/ui/DesignKit/Form.tsx index 56db2a1d..4e709423 100644 --- a/uplink-client/src/ui/DesignKit/Form.tsx +++ b/uplink-client/src/ui/DesignKit/Form.tsx @@ -1,178 +1,50 @@ -"use client" - -import * as React from "react" -import * as LabelPrimitive from "@radix-ui/react-label" -import { Slot } from "@radix-ui/react-slot" -import { - Controller, - ControllerProps, - FieldPath, - FieldValues, - FormProvider, - useFormContext, -} from "react-hook-form" - -import { cn } from "@/lib/shadcn" +import { Input } from "./Input" import { Label } from "./Label" -const Form = FormProvider - -type FormFieldContextValue< - TFieldValues extends FieldValues = FieldValues, - TName extends FieldPath = FieldPath -> = { - name: TName -} - -const FormFieldContext = React.createContext( - {} as FormFieldContextValue -) - -const FormField = < - TFieldValues extends FieldValues = FieldValues, - TName extends FieldPath = FieldPath ->({ - ...props -}: ControllerProps) => { +export const FormInput = ({ + value, + placeholder, + onChange, + label, + error, + inputType, + disabled, + styleOverrides +}: { + value: string, + placeholder: string, + onChange: (e: React.ChangeEvent) => void, + label?: string, + error?: string, + inputType: string, + disabled?: boolean, + styleOverrides?: string +}) => { return ( - - - +
+ {label && ( + + )} + e.currentTarget.blur()} + spellCheck="false" + value={value} + onChange={onChange} + placeholder={placeholder} + disabled={disabled} + className={styleOverrides} + /> + {error && ( + + )} +
) } -const useFormField = () => { - const fieldContext = React.useContext(FormFieldContext) - const itemContext = React.useContext(FormItemContext) - const { getFieldState, formState } = useFormContext() - - const fieldState = getFieldState(fieldContext.name, formState) - - if (!fieldContext) { - throw new Error("useFormField should be used within ") - } - - const { id } = itemContext - - return { - id, - name: fieldContext.name, - formItemId: `${id}-form-item`, - formDescriptionId: `${id}-form-item-description`, - formMessageId: `${id}-form-item-message`, - ...fieldState, - } -} - -type FormItemContextValue = { - id: string -} - -const FormItemContext = React.createContext( - {} as FormItemContextValue -) - -const FormItem = React.forwardRef< - HTMLDivElement, - React.HTMLAttributes ->(({ className, ...props }, ref) => { - const id = React.useId() - - return ( - -
- - ) -}) -FormItem.displayName = "FormItem" - -const FormLabel = React.forwardRef< - React.ElementRef, - React.ComponentPropsWithoutRef ->(({ className, ...props }, ref) => { - const { error, formItemId } = useFormField() - - return ( -