diff --git a/apps/web/package.json b/apps/web/package.json index afdb2a916..26b324d02 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -32,11 +32,14 @@ "@vanilla-extract/next-plugin": "^2.4.11", "@vanilla-extract/recipes": "^0.5.7", "@vercel/og": "^0.6.8", + "@zoralabs/coins-sdk": "^0.3.3", + "@zoralabs/protocol-deployments": "^0.6.4", "ai": "^5.0.76", "alchemy-sdk": "^3.6.0", "axios": "^1.12.2", "dayjs": "^1.11.13", "flatpickr": "^4.6.13", + "formik": "^2.4.6", "framer-motion": "^12.12.1", "ioredis": "^5.8.1", "next": "^15.5.9", diff --git a/apps/web/src/modules/dashboard/CreateActions.tsx b/apps/web/src/modules/dashboard/CreateActions.tsx index d1b5912c4..651a5ef2b 100644 --- a/apps/web/src/modules/dashboard/CreateActions.tsx +++ b/apps/web/src/modules/dashboard/CreateActions.tsx @@ -18,6 +18,11 @@ export const CreateActions: React.FC = ({ const [selectorOpen, setSelectorOpen] = useState(false) const [actionType, setActionType] = useState<'post' | 'proposal'>('post') + const handleCreatePost = () => { + setActionType('post') + setSelectorOpen(true) + } + const handleCreateProposal = () => { setActionType('proposal') setSelectorOpen(true) @@ -25,13 +30,20 @@ export const CreateActions: React.FC = ({ return ( <> - - - + + + + + + + + = ({ const [selectorOpen, setSelectorOpen] = useState(false) const [actionType, setActionType] = useState<'post' | 'proposal'>('post') + const handleCreatePost = () => { + setActionType('post') + setSelectorOpen(true) + } + const handleCreateProposal = () => { setActionType('proposal') setSelectorOpen(true) @@ -35,6 +40,16 @@ export const MobileCreateMenu: React.FC = ({ What would you like to create?
+ + + + {isDeploying ? 'Publishing...' : 'Publish Post'} + + + + + ) + }} + + + ) +} diff --git a/apps/web/src/modules/post/components/index.ts b/apps/web/src/modules/post/components/index.ts new file mode 100644 index 000000000..3ba2f8761 --- /dev/null +++ b/apps/web/src/modules/post/components/index.ts @@ -0,0 +1 @@ +export * from './CreateContentCoinForm' diff --git a/apps/web/src/modules/post/styles/postCreate.css.ts b/apps/web/src/modules/post/styles/postCreate.css.ts new file mode 100644 index 000000000..139a4a959 --- /dev/null +++ b/apps/web/src/modules/post/styles/postCreate.css.ts @@ -0,0 +1,18 @@ +import { style } from '@vanilla-extract/css' + +export const twoColumnGrid = style({ + display: 'grid', + gridTemplateColumns: '1fr', + gap: '24px', + width: '100%', + '@media': { + '(min-width: 1024px)': { + gridTemplateColumns: '1fr 1fr', + }, + }, +}) + +export const previewColumn = style({ + display: 'block', + width: '100%', +}) diff --git a/apps/web/src/pages/api/alchemy/token-prices.ts b/apps/web/src/pages/api/alchemy/token-prices.ts new file mode 100644 index 000000000..fe50ef0da --- /dev/null +++ b/apps/web/src/pages/api/alchemy/token-prices.ts @@ -0,0 +1,70 @@ +import { CHAIN_ID } from '@buildeross/types' +import { NextApiRequest, NextApiResponse } from 'next' +import { getCachedTokenPrices } from 'src/services/alchemyService' +import { withCors } from 'src/utils/api/cors' +import { withRateLimit } from 'src/utils/api/rateLimit' + +async function handler(req: NextApiRequest, res: NextApiResponse) { + const { chainId, addresses } = req.query + + if (!chainId) { + return res.status(400).json({ error: 'Missing chainId parameter' }) + } + + if (!addresses) { + return res.status(400).json({ error: 'Missing addresses parameter' }) + } + + const chainIdNum = parseInt(chainId as string, 10) + + if (!Object.values(CHAIN_ID).includes(chainIdNum)) { + return res.status(400).json({ error: 'Invalid chainId' }) + } + + // Parse addresses - can be comma-separated or array + let addressArray: string[] + if (Array.isArray(addresses)) { + addressArray = addresses + } else { + addressArray = (addresses as string).split(',').map((addr) => addr.trim()) + } + + if (addressArray.length === 0) { + return res.status(400).json({ error: 'No addresses provided' }) + } + + // Limit to 25 addresses per request (Alchemy API batch limit) + if (addressArray.length > 25) { + return res.status(400).json({ error: 'Maximum 25 addresses allowed per request' }) + } + + try { + const result = await getCachedTokenPrices( + chainIdNum as CHAIN_ID, + addressArray as `0x${string}`[] + ) + + // Handle null result (unsupported chain or missing API key) + if (!result) { + return res.status(200).json({ + data: [], + source: 'fetched', + }) + } + + // Data is already sanitized by the service + return res.status(200).json({ + data: result.data, + source: result.source, + }) + } catch (error) { + console.error('Token prices API error:', error) + return res.status(500).json({ error: 'Internal server error' }) + } +} + +export default withCors()( + withRateLimit({ + keyPrefix: 'alchemy:tokenPrices', + })(handler) +) diff --git a/apps/web/src/pages/dao/[network]/[token]/post/create.tsx b/apps/web/src/pages/dao/[network]/[token]/post/create.tsx index 23e3e9e91..d9eebf9b4 100644 --- a/apps/web/src/pages/dao/[network]/[token]/post/create.tsx +++ b/apps/web/src/pages/dao/[network]/[token]/post/create.tsx @@ -1,18 +1,21 @@ import { PUBLIC_DEFAULT_CHAINS } from '@buildeross/constants/chains' import { daoOGMetadataRequest } from '@buildeross/sdk/subgraph' import { AddressType, CHAIN_ID } from '@buildeross/types' +import { type CoinFormValues, ContentPostPreview } from '@buildeross/ui' import { DaoAvatar } from '@buildeross/ui/Avatar' -import { Box, Button, Flex, Stack, Text } from '@buildeross/zord' +import { Box, Flex, Stack, Text } from '@buildeross/zord' import { GetServerSideProps } from 'next' -import { useRouter } from 'next/router' import React, { useState } from 'react' import { DefaultLayout } from '../../../../../layouts/DefaultLayout' +import { CreateContentCoinForm } from '../../../../../modules/post/components/CreateContentCoinForm' +import * as styles from '../../../../../modules/post/styles/postCreate.css' interface CreatePostPageProps { daoName: string collectionAddress: AddressType auctionAddress: AddressType + treasuryAddress: AddressType chainId: CHAIN_ID network: string } @@ -21,35 +24,32 @@ export default function CreatePostPage({ daoName, collectionAddress, auctionAddress, + treasuryAddress, chainId, network, }: CreatePostPageProps) { - const router = useRouter() - const [postContent, setPostContent] = useState('') - - const handleCancel = () => { - router.back() - } - - const handleSubmit = (e: React.FormEvent) => { - e.preventDefault() - // TODO: Implement post creation logic - // eslint-disable-next-line no-console - console.log('Create post:', { network, collectionAddress, postContent }) - } + // State to track form values for preview + const [previewData, setPreviewData] = useState({ + name: '', + symbol: '', + description: '', + imageUrl: '', + mediaUrl: '', + mediaMimeType: '', + }) return ( - + {/* Header */} - Create Post + Publish Post - Share an update or announcement with the DAO community + Share your content on-chain and enable supporters to collect and trade @@ -70,7 +70,7 @@ export default function CreatePostPage({ /> - Posting to {daoName} + Creating for {daoName} {network} @@ -79,71 +79,26 @@ export default function CreatePostPage({ - {/* Form */} -
- - {/* Content textarea */} - - - Post Content - -