diff --git a/app/(landing)/hackathons/[slug]/submit/page.tsx b/app/(landing)/hackathons/[slug]/submit/page.tsx index f5ca054e2..61e394970 100644 --- a/app/(landing)/hackathons/[slug]/submit/page.tsx +++ b/app/(landing)/hackathons/[slug]/submit/page.tsx @@ -1,6 +1,6 @@ 'use client'; -import { use, useEffect } from 'react'; +import { use, useEffect, useState } from 'react'; import { useRouter } from 'next/navigation'; import { useHackathonData } from '@/lib/providers/hackathonProvider'; import { useAuthStatus } from '@/hooks/use-auth'; @@ -70,15 +70,34 @@ export default function SubmitProjectPage({ router.push(`/hackathons/${hackathonSlug}?tab=submission`); }; - if ( - isLoading || - hackathonLoading || - isLoadingMySubmission || - !currentHackathon - ) { + const [hasInitialLoaded, setHasInitialLoaded] = useState(false); + + useEffect(() => { + if (!isLoading && !hackathonLoading && !isLoadingMySubmission) { + setHasInitialLoaded(true); + } + }, [isLoading, hackathonLoading, isLoadingMySubmission]); + + if (!hasInitialLoaded) { return ; } + if (!currentHackathon) { + return ( +
+

Hackathon Not Found

+ +
+ ); + } + return (
@@ -107,6 +126,8 @@ export default function SubmitProjectPage({ introduction: mySubmission.introduction, links: mySubmission.links, participationType: (mySubmission as any).participationType, + teamName: (mySubmission as any).teamName, + teamMembers: (mySubmission as any).teamMembers, } : undefined } diff --git a/app/me/projects/page.tsx b/app/me/projects/page.tsx index 4c61e8f1e..fd6c819bf 100644 --- a/app/me/projects/page.tsx +++ b/app/me/projects/page.tsx @@ -102,14 +102,23 @@ export default function MyProjectsPage() { ) : (
- {sortedProjects.map(project => ( - { + // Find if this project is a hackathon submission + const submission = + meData.user.hackathonSubmissionsAsParticipant?.find( + s => s.projectId === project.id + ); + + // If it's a submission, use the submission ID for the slug to ensure correct redirection from ProjectCard + const displayId = submission?.id || project.id; + + return ( + - ))} + isSubmission: !!submission, + submissionStatus: submission?.status, + }} + /> + ); + })}
)}
diff --git a/components/hackathons/submissions/SubmissionForm.tsx b/components/hackathons/submissions/SubmissionForm.tsx index 1604189b6..12d5273f6 100644 --- a/components/hackathons/submissions/SubmissionForm.tsx +++ b/components/hackathons/submissions/SubmissionForm.tsx @@ -34,6 +34,7 @@ import { ExpandableScreenContent, ExpandableScreenTrigger, useExpandableScreen, + useOptionalExpandableScreen, } from '@/components/ui/expandable-screen'; import Stepper from '@/components/stepper/Stepper'; import { uploadService } from '@/lib/api/upload'; @@ -201,16 +202,9 @@ const SubmissionFormContent: React.FC = ({ onSuccess, onClose, }) => { - // Use context carefully since it might not be available when used standalone - let collapse = () => {}; - let open = true; - try { - const expandableCtx = useExpandableScreen(); - collapse = expandableCtx.collapse; - open = expandableCtx.isExpanded; - } catch (e) { - // Standalone mode, not in ExpandableScreen - } + const expandableCtx = useOptionalExpandableScreen(); + const collapse = expandableCtx?.collapse ?? (() => {}); + const open = expandableCtx?.isExpanded ?? true; const { currentHackathon } = useHackathonData(); const { user } = useAuthStatus(); @@ -416,7 +410,7 @@ const SubmissionFormContent: React.FC = ({ description: 'An intelligent task management application that uses machine learning to prioritize tasks...', logo: '', - videoUrl: 'https://www.youtube.com/watch?v=dQw4w9WgXcQ', + videoUrl: 'https://youtu.be/rOSZhhblE_8?si=Hf_YvPTMmyWTUOKQ', introduction: 'This project leverages advanced AI algorithms...', links: [ { type: 'github', url: 'https://github.com/example/ai-task-manager' }, @@ -785,12 +779,14 @@ const SubmissionFormContent: React.FC = ({ } else { await create(submissionData); } - if (onClose) { + + if (onSuccess) { + onSuccess(); + } else if (onClose) { onClose(); } else { collapse(); } - onSuccess?.(); } catch { // Error handled in hook } diff --git a/components/hackathons/submissions/submissionCard.tsx b/components/hackathons/submissions/submissionCard.tsx index 1f0d6e59a..f5b511622 100644 --- a/components/hackathons/submissions/submissionCard.tsx +++ b/components/hackathons/submissions/submissionCard.tsx @@ -127,6 +127,16 @@ const SubmissionCard = ({ return formatDistanceToNow(new Date(dateString), { addSuffix: true }); }; + const handleEditSelect = (e: Event) => { + e.stopPropagation(); + onEditClick?.(); + }; + + const handleDeleteSelect = (e: Event) => { + e.stopPropagation(); + onDeleteClick?.(); + }; + return (
@@ -181,14 +192,14 @@ const SubmissionCard = ({ className='border-gray-800 bg-black text-white' > onEditClick?.()} + onSelect={handleEditSelect} className='cursor-pointer text-gray-300 focus:bg-gray-800 focus:text-white' > Edit Submission onDeleteClick?.()} + onSelect={handleDeleteSelect} className='cursor-pointer text-red-500 focus:bg-red-900/20 focus:text-red-400' > diff --git a/components/hackathons/submissions/submissionTab.tsx b/components/hackathons/submissions/submissionTab.tsx index fff6a4974..29193c83a 100644 --- a/components/hackathons/submissions/submissionTab.tsx +++ b/components/hackathons/submissions/submissionTab.tsx @@ -83,6 +83,7 @@ const SubmissionTabContent: React.FC = ({ setSelectedSort, setSelectedCategory, } = useSubmissions(); + const { currentHackathon, loading: isHackathonDataLoading } = useHackathonData(); const { status } = useHackathonStatus( @@ -276,6 +277,7 @@ const SubmissionTabContent: React.FC = ({ {/* Submissions Grid with Create Button if no submission */} {!isLoadingMySubmission && + !isHackathonDataLoading && !mySubmission && isAuthenticated && isRegistered && diff --git a/components/organization/hackathons/settings/GeneralSettingsTab.tsx b/components/organization/hackathons/settings/GeneralSettingsTab.tsx index e0b2ce74d..1c1e84e65 100644 --- a/components/organization/hackathons/settings/GeneralSettingsTab.tsx +++ b/components/organization/hackathons/settings/GeneralSettingsTab.tsx @@ -55,6 +55,8 @@ interface GeneralSettingsTabProps { isPublished?: boolean; } +import TurndownService from 'turndown'; + export default function GeneralSettingsTab({ organizationId, hackathonId, @@ -78,6 +80,24 @@ export default function GeneralSettingsTab({ address: string; } | null>(null); + // Normalize HTML to Markdown for existing descriptions + const normalizedDescription = React.useMemo(() => { + let desc = initialData?.description || ''; + if (desc && /<[a-z][\\s\\S]*>/i.test(desc)) { + try { + const turndownService = new TurndownService({ + headingStyle: 'atx', + codeBlockStyle: 'fenced', + bulletListMarker: '-', + }); + desc = turndownService.turndown(desc); + } catch (err) { + console.error('Failed to convert HTML to Markdown', err); + } + } + return desc; + }, [initialData?.description]); + const form = useForm({ resolver: zodResolver(infoSchema), defaultValues: { @@ -85,7 +105,7 @@ export default function GeneralSettingsTab({ tagline: initialData?.tagline || '', slug: initialData?.slug || '', banner: initialData?.banner || '', - description: initialData?.description || '', + description: normalizedDescription, categories: Array.isArray(initialData?.categories) ? initialData.categories : [], diff --git a/components/profile/ProjectsTab.tsx b/components/profile/ProjectsTab.tsx index ff2808d52..4758ce189 100644 --- a/components/profile/ProjectsTab.tsx +++ b/components/profile/ProjectsTab.tsx @@ -100,38 +100,47 @@ export default function ProjectsTab({ user }: ProjectsTabProps) { onScrollCapture={handleScroll} >
- {projects.map(project => ( - - { + // Find if this project is a hackathon submission + // We need to check if the user object has submissions + const submission = + user.user.hackathonSubmissionsAsParticipant?.find( + s => s.projectId === project.id + ); + + // If it's a submission, use the submission ID for the URL and add ?type=submission + const displayId = submission?.id || project.id; + const href = submission + ? `/projects/${displayId}?type=submission` + : `/projects/${displayId}`; + + return ( + + - - ))} + }, + isSubmission: !!submission, + submissionStatus: submission?.status, + }} + /> + + ); + })} {!hasMore && projects.length > 0 && (
diff --git a/components/profile/ProjectsTabPublic.tsx b/components/profile/ProjectsTabPublic.tsx index 478c1fafd..ccbf7194a 100644 --- a/components/profile/ProjectsTabPublic.tsx +++ b/components/profile/ProjectsTabPublic.tsx @@ -33,14 +33,26 @@ export default function ProjectsTabPublic({ user }: ProjectsTabProps) {
- {user.projects.map(project => ( - - { + // Find if this project is a hackathon submission + const submission = user.hackathonSubmissionsAsParticipant?.find( + s => s.projectId === project.id + ); + + // If it's a submission, use the submission ID for the URL and add ?type=submission + const displayId = submission?.id || project.id; + const href = submission + ? `/projects/${displayId}?type=submission` + : `/projects/${displayId}`; + + return ( + + - - ))} + // Add a custom property to indicate it's a submission for ProjectCard + isSubmission: !!submission, + submissionStatus: submission?.status, + }} + /> + + ); + })}
); diff --git a/components/ui/expandable-screen.tsx b/components/ui/expandable-screen.tsx index 7eaf54e96..4894a27b7 100644 --- a/components/ui/expandable-screen.tsx +++ b/components/ui/expandable-screen.tsx @@ -34,6 +34,10 @@ function useExpandableScreen() { return context; } +export function useOptionalExpandableScreen() { + return useContext(ExpandableScreenContext); +} + // Root Component interface ExpandableScreenProps { children: ReactNode; diff --git a/content/blog/what-do-you-know-about-boundless-on-x-challenge.mdx b/content/blog/what-do-you-know-about-boundless-on-x-challenge.mdx new file mode 100644 index 000000000..baf6dc81f --- /dev/null +++ b/content/blog/what-do-you-know-about-boundless-on-x-challenge.mdx @@ -0,0 +1,157 @@ +--- +title: 'What Do You Know About Boundless? The X Challenge' +excerpt: 'Whether you’re a builder, creator, or meme-lover, the Boundless on X Challenge is your chance to explain the platform in your own words. Share your thoughts and win a share of the $100 USDC prize pool!' +coverImage: 'https://pbs.twimg.com/media/HCwbCFTWoAAdJkq?format=jpg&name=900x900' +publishedAt: '2026-03-09' +author: + name: 'Boundless Team' + image: '' +categories: ['Community', 'Challenge', 'Events'] +tags: + ['boundless', 'challenge', 'x', 'twitter', 'community', 'stellar', 'rewards'] +readingTime: 3 +isFeatured: true +seoTitle: 'What Do You Know About Boundless? Join the X Challenge' +seoDescription: 'Join the Boundless on X Challenge! Explain the platform in your own words through a tweet, thread, or meme, and win your share of $100 USDC.' +--- + +# What Do You Know About Boundless? + +We want to hear your take. + +Whether you’re a builder, creator, or meme-lover, the **Boundless on X Challenge** is your chance to explain the platform in your own words. Share a tweet, thread, or visual post, and the best entries will help shape how the world understands Boundless — and win some **USDC**! + +--- + +## So, What’s the Boundless on X Challenge All About? + +Our mission at **Boundless** is simple: to empower anyone, anywhere, to transform bold ideas into impactful projects with **transparency, community, and accountability** at the core. + +Ecosystems grow through conversation. The more people understand a platform, the easier it is for new builders to join and for great ideas to gain traction. The **Boundless on X Challenge** is your chance to contribute by telling our story in your own way. + +If you’re reading this, you’re probably researching to participate in the challenge. + +Awesome — we’re thrilled to have you. Stick around until the end of this article and you’ll get some **insider tips** to help your post stand out. + +--- + +# Finish the Challenge in Three Simple Steps + +Completing this challenge only takes **three steps** and a couple of minutes. + +## 1. Register and Follow Boundless + +To participate, go to **[boundlesfi.xyz](https://www.boundlessfi.xyz/hackathons/boundless-on-x-test?tab=participants)** and register for **Boundless on X**. + +This is a community challenge, so join the community on **X (formerly Twitter)** `@boundless_fi` and on **Discord** to meet the community. + +--- + +## 2. Choose Your Category and Create Your Post + +Share an impactful and/or entertaining **tweet, thread, or meme** about Boundless. + +**Must-haves:** + +- Your post must include **#BoundlessBuild** +- Tag both **@boundless_fi** and **@BuildOnStellar** + +--- + +## 3. Submit Your Entry + +Copy the link to your post and submit it through the **challenge page** with a short description. + +**Done.** + +--- + +# Categories You Can Join + +You can join the challenge by participating in **any of these categories**. Pick the format that best suits your creative style. + +## Threads + +Share a short, engaging **thread of 3–5 tweets** that explains Boundless or tells a story about the platform. + +## Single Tweets + +Write **one clear, impactful, concise, or clever tweet** that captures what Boundless is all about. + +## Memes or Visuals + +Get creative with an **image, graphic, or short video** that explains or promotes Boundless in a fun way. + +Winners will be selected **in each category**, along with **one overall standout entry** that really captures the spirit of the challenge. + +--- + +# What Are the Best Participants Creating? + +Top entries are the ones that: + +- Explain **Boundless** like you’re telling a friend who’s never heard of it. +- Highlight **why Boundless’ mission matters**. +- Share **what excites you most about the ecosystem**. +- Bring the idea to life with **humor, visuals, or storytelling**. + +No single approach is _“correct,”_ but these tips can help your post stand out: + +- **Keep it personal.** Your voice matters more than AI-generated material — use it sparingly or not at all. +- **Clarity over virality.** Likes and retweets are nice, but a clear, compelling explanation carries the most weight. +- **Be authentic.** Show your enthusiasm and perspective; genuine posts resonate more with the community. + +--- + +# Prizes and Key Dates + +The challenge features a **$100 USDC prize pool**, shared by the top entries selected from across all categories. + +Winning entries may also be **highlighted across the Boundless community**. + +The challenge runs for a short period, so make sure to submit your entry before the deadline: + +- **Challenge kicks off:** Saturday, March 7, 2026 +- **Last chance to submit:** Wednesday, March 11, 2026 at **12:00 PM UTC** + +--- + +# Got Questions? We’ve Got Answers + +## Who can participate? + +Anyone can join. If you have an **X account** and want to share something about Boundless, you’re welcome to take part. + +## Do I need to be a developer? + +Not at all. The challenge is open to **builders, creators, writers, designers, meme makers**, and anyone interested in the ecosystem. + +## Can I participate in more than one category? + +Yes. You can submit entries in **multiple categories**. + +For example, you might create a thread explaining Boundless and also submit a meme or visual. Each entry should be **submitted separately** with its own link and description. + +## What’s off-limits? + +**NSFW, offensive, or harmful content** is a no-go. Stick to posts that **inform, entertain, or inspire** in a way everyone can enjoy. + +## Want to ask more? + +If you have questions that aren’t answered here, don’t be shy. + +**[Tag us on X](https://x.com/boundless_fi)** or **[Start a conversation in Discord](https://discord.gg/tgpFpSHG)**. We love hearing from the community. + +--- + +# Ready to Jump In? + +Your voice matters. + +Create your post, submit it, and share your excitement about **Boundless** — for the love of the game, for a bit of glory, and for the chance to win some fun rewards too! + +Finally, as promised, here are some **insider resources** to help your post stand out and guide you through the challenge: + +- **[Quick start guide to join Boundless](https://docs.boundlessfi.xyz/getting-started/quick-start)** +- **[How Boundless works](https://docs.boundlessfi.xyz/concepts/how-boundless-works)** +- **[General FAQs](https://docs.boundlessfi.xyz/faq/general)** diff --git a/features/projects/components/ProjectCard.tsx b/features/projects/components/ProjectCard.tsx index 8dd194c84..155c4ec3a 100644 --- a/features/projects/components/ProjectCard.tsx +++ b/features/projects/components/ProjectCard.tsx @@ -3,10 +3,36 @@ import { formatNumber, cn } from '@/lib/utils'; import { useRouter } from 'nextjs-toploader/app'; import Image from 'next/image'; import { CountdownTimer } from '@/components/ui/timer'; -import { Crowdfunding } from '@/features/projects/types'; + +export type ProjectCardData = { + id: string; + slug: string; + project: { + id?: string; + title: string; + vision?: string | null; + banner?: string | null; + logo?: string | null; + creator?: { name: string; image?: string | null }; + category?: string | null; + status: string; + _count?: { votes?: number }; + [key: string]: any; + }; + fundingGoal?: number; + fundingRaised?: number; + fundingCurrency?: string; + fundingEndDate?: string | null; + milestones?: any[]; + voteGoal?: number; + voteProgress?: number; + isSubmission?: boolean; + submissionStatus?: string | null; + [key: string]: any; +}; type ProjectCardProps = { - data: Crowdfunding; + data: ProjectCardData; newTab?: boolean; isFullWidth?: boolean; className?: string; @@ -23,13 +49,15 @@ function ProjectCard({ const { slug, project, - fundingGoal, - fundingRaised, - fundingCurrency, - fundingEndDate, - milestones, - voteGoal, - voteProgress, + fundingGoal = 0, + fundingRaised = 0, + fundingCurrency = 'USDC', + fundingEndDate = null, + milestones = [], + voteGoal = 0, + voteProgress = 0, + isSubmission, + submissionStatus, } = data; const { @@ -47,11 +75,22 @@ function ProjectCard({ banner || '/images/placeholders/project-banner-placeholder.png'; const handleClick = () => { - router.push(`/projects/${slug}`); + const url = isSubmission + ? `/projects/${slug}?type=submission` + : `/projects/${slug}`; + router.push(url); }; // Determine display status const getDisplayStatus = () => { + if (isSubmission) { + if (submissionStatus === 'SHORTLISTED' || submissionStatus === 'ACCEPTED') + return 'Shortlisted'; + if (submissionStatus === 'SUBMITTED') return 'Submitted'; + if (submissionStatus === 'DISQUALIFIED') return 'Disqualified'; + return 'Submission'; + } + if (projectStatus === 'IDEA') return 'Validation'; if (projectStatus === 'ACTIVE') return 'Funding'; if (projectStatus === 'LIVE') return 'Funded'; diff --git a/features/projects/types/index.ts b/features/projects/types/index.ts index bce8bcc40..7d0418dab 100644 --- a/features/projects/types/index.ts +++ b/features/projects/types/index.ts @@ -82,6 +82,13 @@ export interface PublicUserProfile { logo: string; createdAt: string; }>; + hackathonSubmissionsAsParticipant?: Array<{ + id: string; + projectId: string; + status: string; + hackathonId: string; + projectName: string; + }>; badges: any[]; stats: { projectsCreated: number; diff --git a/hooks/hackathon/use-participants.ts b/hooks/hackathon/use-participants.ts index f6fe782bc..45e43122b 100644 --- a/hooks/hackathon/use-participants.ts +++ b/hooks/hackathon/use-participants.ts @@ -12,17 +12,17 @@ export function useParticipants() { const [apiParticipants, setApiParticipants] = useState([]); const [isLoading, setIsLoading] = useState(false); - const hackathonId = currentHackathon?.id || (params?.slug as string); + const hackathonId = currentHackathon?.id; // Fetch teams to get accurate team info and roles useEffect(() => { if (hackathonId) { setIsLoading(true); - Promise.all([ - getTeamPosts(hackathonId, { limit: 50 }), - getHackathonParticipants(hackathonId, { limit: 100 }), - ]) - .then(([teamsResponse, participantsResponse]) => { + + const fetchAllData = async () => { + try { + // Fetch teams + const teamsResponse = await getTeamPosts(hackathonId, { limit: 50 }); if (teamsResponse.success && teamsResponse.data) { const teamsArray = (teamsResponse.data as any).teams || @@ -30,16 +30,35 @@ export function useParticipants() { setTeams(teamsArray); } - if (participantsResponse.success && participantsResponse.data) { - setApiParticipants(participantsResponse.data.participants || []); + // Fetch participants with pagination + let allParticipants: any[] = []; + let page = 1; + let hasMore = true; + + while (hasMore) { + const participantsResponse = await getHackathonParticipants( + hackathonId, + { limit: 100, page } + ); + if (participantsResponse.success && participantsResponse.data) { + const newParticipants = + participantsResponse.data.participants || []; + allParticipants = [...allParticipants, ...newParticipants]; + hasMore = participantsResponse.data.pagination?.hasNext || false; + page++; + } else { + hasMore = false; + } } - }) - .catch(err => { + setApiParticipants(allParticipants); + } catch (err) { reportError(err, { context: 'participants-fetchData', hackathonId }); - }) - .finally(() => { + } finally { setIsLoading(false); - }); + } + }; + + fetchAllData(); } }, [hackathonId]); diff --git a/lib/api/types.ts b/lib/api/types.ts index 39010e576..d418161a3 100644 --- a/lib/api/types.ts +++ b/lib/api/types.ts @@ -122,6 +122,7 @@ export interface User { rank?: number | null; submittedAt: string; hackathonId: string; + projectId: string; hackathon?: Hackathon; }>; joinedHackathons?: Array<{ diff --git a/lib/providers/hackathonProvider.tsx b/lib/providers/hackathonProvider.tsx index e6b26fbbe..e21a12c16 100644 --- a/lib/providers/hackathonProvider.tsx +++ b/lib/providers/hackathonProvider.tsx @@ -99,7 +99,7 @@ interface HackathonDataContextType { getHackathonById: (id: string) => Hackathon | undefined; getHackathonBySlug: (slug: string) => Promise; - setCurrentHackathon: (slug: string) => void; + setCurrentHackathon: (slug: string) => Promise; addDiscussion: (content: string) => Promise; addReply: (parentCommentId: string, content: string) => Promise; diff --git a/package-lock.json b/package-lock.json index 32c85f04a..74be24bf5 100644 --- a/package-lock.json +++ b/package-lock.json @@ -125,6 +125,7 @@ "sonner": "^2.0.7", "tailwind-merge": "^3.3.1", "three": "^0.180.0", + "turndown": "^7.2.2", "tw-animate-css": "^1.3.6", "uuid": "^13.0.0", "vaul": "^1.1.2", @@ -137,6 +138,7 @@ "@types/p-limit": "^2.1.0", "@types/react": "19.2.7", "@types/react-dom": "19.2.3", + "@types/turndown": "^5.0.6", "@types/uuid": "^10.0.0", "@typescript-eslint/eslint-plugin": "^8.38.0", "@typescript-eslint/parser": "^8.38.0", @@ -1609,6 +1611,12 @@ "langium": "^4.0.0" } }, + "node_modules/@mixmark-io/domino": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@mixmark-io/domino/-/domino-2.2.0.tgz", + "integrity": "sha512-Y28PR25bHXUg88kCV7nivXrP2Nj2RueZ3/l/jdx6J9f8J4nsEGcgX0Qe6lt7Pa+J79+kPiJU3LguR6O/6zrLOw==", + "license": "BSD-2-Clause" + }, "node_modules/@monogrid/gainmap-js": { "version": "3.4.0", "resolved": "https://registry.npmjs.org/@monogrid/gainmap-js/-/gainmap-js-3.4.0.tgz", @@ -6352,6 +6360,13 @@ "license": "MIT", "optional": true }, + "node_modules/@types/turndown": { + "version": "5.0.6", + "resolved": "https://registry.npmjs.org/@types/turndown/-/turndown-5.0.6.tgz", + "integrity": "sha512-ru00MoyeeouE5BX4gRL+6m/BsDfbRayOskWqUvh7CLGW+UXxHQItqALa38kKnOiZPqJrtzJUgAC2+F0rL1S4Pg==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/unist": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz", @@ -16839,6 +16854,15 @@ } } }, + "node_modules/turndown": { + "version": "7.2.2", + "resolved": "https://registry.npmjs.org/turndown/-/turndown-7.2.2.tgz", + "integrity": "sha512-1F7db8BiExOKxjSMU2b7if62D/XOyQyZbPKq/nUwopfgnHlqXHqQ0lvfUTeUIr1lZJzOPFn43dODyMSIfvWRKQ==", + "license": "MIT", + "dependencies": { + "@mixmark-io/domino": "^2.2.0" + } + }, "node_modules/tw-animate-css": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/tw-animate-css/-/tw-animate-css-1.4.0.tgz", diff --git a/package.json b/package.json index 8e73c10a0..a557459e3 100644 --- a/package.json +++ b/package.json @@ -141,6 +141,7 @@ "sonner": "^2.0.7", "tailwind-merge": "^3.3.1", "three": "^0.180.0", + "turndown": "^7.2.2", "tw-animate-css": "^1.3.6", "uuid": "^13.0.0", "vaul": "^1.1.2", @@ -153,6 +154,7 @@ "@types/p-limit": "^2.1.0", "@types/react": "19.2.7", "@types/react-dom": "19.2.3", + "@types/turndown": "^5.0.6", "@types/uuid": "^10.0.0", "@typescript-eslint/eslint-plugin": "^8.38.0", "@typescript-eslint/parser": "^8.38.0",