├── .env.example ├── .eslintrc.json ├── .gitignore ├── .prettierrc ├── LICENSE ├── README.md ├── actions └── export.ts ├── app ├── (app) │ ├── layout.tsx │ └── map │ │ ├── head.tsx │ │ └── page.tsx ├── globals.css ├── head.tsx ├── layout.tsx ├── page.module.css ├── page.tsx └── providers.tsx ├── components ├── Home │ ├── ActionButtons.tsx │ ├── Features.tsx │ ├── Footer.tsx │ ├── Main.tsx │ ├── Navigation.tsx │ └── TryItButton.tsx ├── Layout │ └── AppLayout │ │ ├── AppLayout.tsx │ │ ├── Menu.tsx │ │ ├── OpenAIAKDialog.tsx │ │ └── Top.tsx ├── Map │ ├── Edges │ │ └── CustomizableEdge.tsx │ ├── Flow.tsx │ ├── FlowWrapper.tsx │ ├── Generator │ │ └── Generator.tsx │ ├── Index.tsx │ ├── Nodes │ │ ├── AIContentModal.tsx │ │ ├── RootNode.tsx │ │ └── TopicNode.tsx │ ├── Panel │ │ └── TopPanel │ │ │ ├── Theme.tsx │ │ │ └── TopPanel.tsx │ └── Plugins │ │ └── DataSaver.tsx ├── Providers │ └── Analytics.tsx └── ui │ ├── AspectRadio.tsx │ ├── BlockContainer.tsx │ ├── Button.tsx │ ├── Dialog.tsx │ ├── DropdownMenu.tsx │ ├── Icon.tsx │ ├── Input.tsx │ ├── Label.tsx │ ├── Loader.tsx │ ├── MenuBar.tsx │ ├── NodeHeader.tsx │ ├── Popover.tsx │ ├── Select.tsx │ ├── Separator.tsx │ ├── Textarea.tsx │ ├── Toast.tsx │ ├── Toaster.tsx │ ├── ToggleGroup.tsx │ ├── ToggleInput.tsx │ ├── ToggleTextarea.tsx │ └── Tooltip.tsx ├── data ├── defaultEdges.ts ├── defaultNodes.ts └── defaultPalettes.ts ├── hooks └── use-toast.ts ├── next.config.js ├── package.json ├── pages └── api │ └── ideas.ts ├── postcss.config.js ├── process-env.d.ts ├── public ├── app-img.jpg ├── app-map.jpg ├── favicon.ico ├── next.svg ├── thirteen.svg └── vercel.svg ├── stores └── mapStore.ts ├── styles └── globals.css ├── tailwind.config.js ├── tsconfig.json ├── utils ├── api │ └── suggestions.ts ├── blob.ts ├── classnames.ts ├── constants │ ├── export.ts │ ├── headerTypes.ts │ ├── modes.ts │ ├── openai.ts │ └── questions.ts ├── data.ts ├── filesystem.ts ├── id.ts ├── json.ts ├── node.ts ├── openai │ ├── client.ts │ └── topics.ts ├── providers │ └── ConfigurationProvider.tsx ├── storage.ts └── types.ts └── yarn.lock /.env.example: -------------------------------------------------------------------------------- 1 | NEXT_PUBLIC_OPENAI_API_KEY="" 2 | NEXT_PUBLIC_OPENAI_COMPLETION_MODEL="gpt-3.5-turbo" -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "next/core-web-vitals" 3 | } 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # next.js 12 | /.next/ 13 | /out/ 14 | 15 | # production 16 | /build 17 | 18 | # misc 19 | .DS_Store 20 | *.pem 21 | 22 | # debug 23 | npm-debug.log* 24 | yarn-debug.log* 25 | yarn-error.log* 26 | .pnpm-debug.log* 27 | 28 | # local env files 29 | .env*.local 30 | 31 | # vercel 32 | .vercel 33 | 34 | # typescript 35 | *.tsbuildinfo 36 | next-env.d.ts 37 | 38 | .vscode -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "arrowParens": "always", 3 | "printWidth": 120, 4 | "semi": true, 5 | "singleQuote": true, 6 | "tabWidth": 2, 7 | "trailingComma": "all", 8 | "useTabs": true, 9 | "jsxSingleQuote": true 10 | } 11 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Fernando Palacios 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Mentalist AI 2 | 3 | An open source map mental application built using Next.js 13, Open AI. 4 | 5 | > **Warning** 6 | > This app is a work in progress. I'm building this in public. You can follow the progress on Twitter [@fernandops26](https://twitter.com/fernandops26). 7 | 8 | ## Demo 9 | 10 | ![website](https://github.com/fernandops26/mentalist-ai/blob/main/public/app-img.jpg) 11 | 12 | ![map](https://github.com/fernandops26/mentalist-ai/blob/main/public/app-map.jpg) 13 | 14 | ## About this project 15 | 16 | Mentalist is a web application built with Next.js 13 and integrated with OpenAI. The goal of Mentalist is to help users generate ideas and think creatively by providing a starting point of a topic. 17 | 18 | ## Getting started 19 | 20 | To get started, clone the repository and install the dependencies: 21 | 22 | ```bash 23 | git clone git@github.com:fernandops26/mentalist-ai.git 24 | 25 | cd mentalist-ai/ 26 | 27 | yarn install 28 | ``` 29 | 30 | Following installation, create a `.env.local` file in the root of the project and add the following environment variables: 31 | 32 | ```bash 33 | NEXT_PUBLIC_OPENAI_API_KEY="" 34 | NEXT_PUBLIC_OPENAI_COMPLETION_MODEL="gpt-3.5-turbo" 35 | ``` 36 | 37 | You can get your OpenAI API key by signing up for an account [here](https://platform.openai.com/). 38 | 39 | Once you have created your `.env.local` file, you can run the development server: 40 | 41 | ```bash 42 | yarn dev 43 | ``` 44 | 45 | ## Features 46 | 47 | Coming soon 48 | 49 | ## Roadmap 50 | 51 | Coming soon 52 | 53 | ## License 54 | 55 | Licensed under the [MIT license](https://github.com/fernandops26/mentalist-ai/blob/main/LICENSE). 56 | -------------------------------------------------------------------------------- /actions/export.ts: -------------------------------------------------------------------------------- 1 | import { palettes } from '@/data/defaultPalettes'; 2 | import { loadFromJSON, saveAsJSON } from '@/utils/data'; 3 | import { readFullContentObj, restoreProject, updateConfig } from '@/utils/storage'; 4 | 5 | export const actionSaveFileToDisk = async () => { 6 | const data = readFullContentObj(); 7 | 8 | await saveAsJSON('Mentalist', data.version, data.map, data.config); 9 | }; 10 | 11 | export const actionLoadFileFromDisk = async () => { 12 | try { 13 | const data = await loadFromJSON(); 14 | 15 | if (data.config?.palette == null) { 16 | data.config = { 17 | ...data.config, 18 | palette: palettes[0].id, 19 | }; 20 | } 21 | 22 | restoreProject(data); 23 | 24 | return true; 25 | } catch (error) { 26 | console.error(error); 27 | } 28 | 29 | return false; 30 | }; 31 | -------------------------------------------------------------------------------- /app/(app)/layout.tsx: -------------------------------------------------------------------------------- 1 | import AppLayout from '@/components/Layout/AppLayout/AppLayout'; 2 | import React from 'react'; 3 | 4 | interface LayoutProps { 5 | children: React.ReactNode; 6 | } 7 | export default function Layout({ children }: LayoutProps) { 8 | return {children}; 9 | } 10 | -------------------------------------------------------------------------------- /app/(app)/map/head.tsx: -------------------------------------------------------------------------------- 1 | export default function Head() { 2 | return ( 3 | <> 4 | Your Content Ideation Map | Mentalist 5 | 6 | 11 | 12 | 13 | ); 14 | } 15 | -------------------------------------------------------------------------------- /app/(app)/map/page.tsx: -------------------------------------------------------------------------------- 1 | import Map from '@/components/Map/Index'; 2 | import Link from 'next/link'; 3 | 4 | export default async function MapPage() { 5 | return ( 6 |
7 |
8 |
9 |
10 | 11 |
12 |
13 |
14 |
15 |
16 |

Hi mentalister! 👋

17 |

18 | We are working to expand the use of mentalist-ai on mobile view, for now you can use it in browser 19 |

20 |

21 |

22 | Follow on{' '} 23 | 29 | Twitter 30 | {' '} 31 | for updates 32 |

33 |
34 |
35 |
36 | ); 37 | } 38 | -------------------------------------------------------------------------------- /app/globals.css: -------------------------------------------------------------------------------- 1 | :root { 2 | --max-width: 1100px; 3 | --border-radius: 12px; 4 | --font-mono: ui-monospace, Menlo, Monaco, 'Cascadia Mono', 'Segoe UI Mono', 'Roboto Mono', 'Oxygen Mono', 5 | 'Ubuntu Monospace', 'Source Code Pro', 'Fira Mono', 'Droid Sans Mono', 'Courier New', monospace; 6 | 7 | --foreground-rgb: 0, 0, 0; 8 | --background-start-rgb: 214, 219, 220; 9 | --background-end-rgb: 255, 255, 255; 10 | 11 | --primary-glow: conic-gradient( 12 | from 180deg at 50% 50%, 13 | #16abff33 0deg, 14 | #0885ff33 55deg, 15 | #54d6ff33 120deg, 16 | #0071ff33 160deg, 17 | transparent 360deg 18 | ); 19 | --secondary-glow: radial-gradient(rgba(255, 255, 255, 1), rgba(255, 255, 255, 0)); 20 | 21 | --tile-start-rgb: 239, 245, 249; 22 | --tile-end-rgb: 228, 232, 233; 23 | --tile-border: conic-gradient(#00000080, #00000040, #00000030, #00000020, #00000010, #00000010, #00000080); 24 | 25 | --callout-rgb: 238, 240, 241; 26 | --callout-border-rgb: 172, 175, 176; 27 | --card-rgb: 180, 185, 188; 28 | --card-border-rgb: 131, 134, 135; 29 | } 30 | 31 | @media (prefers-color-scheme: dark) { 32 | :root { 33 | --foreground-rgb: 255, 255, 255; 34 | --background-start-rgb: 0, 0, 0; 35 | --background-end-rgb: 0, 0, 0; 36 | 37 | --primary-glow: radial-gradient(rgba(1, 65, 255, 0.4), rgba(1, 65, 255, 0)); 38 | --secondary-glow: linear-gradient(to bottom right, rgba(1, 65, 255, 0), rgba(1, 65, 255, 0), rgba(1, 65, 255, 0.3)); 39 | 40 | --tile-start-rgb: 2, 13, 46; 41 | --tile-end-rgb: 2, 5, 19; 42 | --tile-border: conic-gradient(#ffffff80, #ffffff40, #ffffff30, #ffffff20, #ffffff10, #ffffff10, #ffffff80); 43 | 44 | --callout-rgb: 20, 20, 20; 45 | --callout-border-rgb: 108, 108, 108; 46 | --card-rgb: 100, 100, 100; 47 | --card-border-rgb: 200, 200, 200; 48 | } 49 | } 50 | 51 | * { 52 | box-sizing: border-box; 53 | padding: 0; 54 | margin: 0; 55 | } 56 | 57 | html, 58 | body { 59 | max-width: 100vw; 60 | overflow-x: hidden; 61 | } 62 | 63 | body { 64 | color: rgb(var(--foreground-rgb)); 65 | background: linear-gradient(to bottom, transparent, rgb(var(--background-end-rgb))) rgb(var(--background-start-rgb)); 66 | } 67 | 68 | a { 69 | color: inherit; 70 | text-decoration: none; 71 | } 72 | 73 | @media (prefers-color-scheme: dark) { 74 | html { 75 | color-scheme: dark; 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /app/head.tsx: -------------------------------------------------------------------------------- 1 | export default function Head() { 2 | return ( 3 | <> 4 | Your Ultimate Content Ideation Tool | Mentalist 5 | 6 | 11 | 12 | 13 | ); 14 | } 15 | -------------------------------------------------------------------------------- /app/layout.tsx: -------------------------------------------------------------------------------- 1 | import '@/styles/globals.css'; 2 | import Providers from './providers'; 3 | 4 | export default function RootLayout({ children }: { children: React.ReactNode }) { 5 | return ( 6 | 7 | {/* 8 | will contain the components returned by the nearest parent 9 | head.tsx. Find out more at https://beta.nextjs.org/docs/api-reference/file-conventions/head 10 | */} 11 | 12 | 13 | {children} 14 | 15 | 16 | ); 17 | } 18 | -------------------------------------------------------------------------------- /app/page.module.css: -------------------------------------------------------------------------------- 1 | .main { 2 | display: flex; 3 | flex-direction: column; 4 | justify-content: space-between; 5 | align-items: center; 6 | padding: 6rem; 7 | min-height: 100vh; 8 | } 9 | 10 | .description { 11 | display: inherit; 12 | justify-content: inherit; 13 | align-items: inherit; 14 | font-size: 0.85rem; 15 | max-width: var(--max-width); 16 | width: 100%; 17 | z-index: 2; 18 | font-family: var(--font-mono); 19 | } 20 | 21 | .description a { 22 | display: flex; 23 | align-items: center; 24 | justify-content: center; 25 | gap: 0.5rem; 26 | } 27 | 28 | .description p { 29 | position: relative; 30 | margin: 0; 31 | padding: 1rem; 32 | background-color: rgba(var(--callout-rgb), 0.5); 33 | border: 1px solid rgba(var(--callout-border-rgb), 0.3); 34 | border-radius: var(--border-radius); 35 | } 36 | 37 | .code { 38 | font-weight: 700; 39 | font-family: var(--font-mono); 40 | } 41 | 42 | .grid { 43 | display: grid; 44 | grid-template-columns: repeat(3, minmax(33%, auto)); 45 | width: var(--max-width); 46 | max-width: 100%; 47 | } 48 | 49 | .card { 50 | padding: 1rem 1.2rem; 51 | border-radius: var(--border-radius); 52 | background: rgba(var(--card-rgb), 0); 53 | border: 1px solid rgba(var(--card-border-rgb), 0); 54 | transition: background 200ms, border 200ms; 55 | } 56 | 57 | .card span { 58 | display: inline-block; 59 | transition: transform 200ms; 60 | } 61 | 62 | .card h2 { 63 | font-weight: 600; 64 | margin-bottom: 0.7rem; 65 | } 66 | 67 | .card p { 68 | margin: 0; 69 | opacity: 0.6; 70 | font-size: 0.9rem; 71 | line-height: 1.5; 72 | max-width: 34ch; 73 | } 74 | 75 | .center { 76 | display: flex; 77 | justify-content: center; 78 | align-items: center; 79 | position: relative; 80 | padding: 4rem 0; 81 | } 82 | 83 | .center::before { 84 | background: var(--secondary-glow); 85 | border-radius: 50%; 86 | width: 480px; 87 | height: 360px; 88 | margin-left: -400px; 89 | } 90 | 91 | .center::after { 92 | background: var(--primary-glow); 93 | width: 240px; 94 | height: 180px; 95 | z-index: -1; 96 | } 97 | 98 | .center::before, 99 | .center::after { 100 | content: ''; 101 | left: 50%; 102 | position: absolute; 103 | filter: blur(45px); 104 | transform: translateZ(0); 105 | } 106 | 107 | .logo, 108 | .thirteen { 109 | position: relative; 110 | } 111 | 112 | .thirteen { 113 | display: flex; 114 | justify-content: center; 115 | align-items: center; 116 | width: 75px; 117 | height: 75px; 118 | padding: 25px 10px; 119 | margin-left: 16px; 120 | transform: translateZ(0); 121 | border-radius: var(--border-radius); 122 | overflow: hidden; 123 | box-shadow: 0px 2px 8px -1px #0000001a; 124 | } 125 | 126 | .thirteen::before, 127 | .thirteen::after { 128 | content: ''; 129 | position: absolute; 130 | z-index: -1; 131 | } 132 | 133 | /* Conic Gradient Animation */ 134 | .thirteen::before { 135 | animation: 6s rotate linear infinite; 136 | width: 200%; 137 | height: 200%; 138 | background: var(--tile-border); 139 | } 140 | 141 | /* Inner Square */ 142 | .thirteen::after { 143 | inset: 0; 144 | padding: 1px; 145 | border-radius: var(--border-radius); 146 | background: linear-gradient(to bottom right, rgba(var(--tile-start-rgb), 1), rgba(var(--tile-end-rgb), 1)); 147 | background-clip: content-box; 148 | } 149 | 150 | /* Enable hover only on non-touch devices */ 151 | @media (hover: hover) and (pointer: fine) { 152 | .card:hover { 153 | background: rgba(var(--card-rgb), 0.1); 154 | border: 1px solid rgba(var(--card-border-rgb), 0.15); 155 | } 156 | 157 | .card:hover span { 158 | transform: translateX(4px); 159 | } 160 | } 161 | 162 | @media (prefers-reduced-motion) { 163 | .thirteen::before { 164 | animation: none; 165 | } 166 | 167 | .card:hover span { 168 | transform: none; 169 | } 170 | } 171 | 172 | /* Mobile and Tablet */ 173 | @media (max-width: 1023px) { 174 | .content { 175 | padding: 4rem; 176 | } 177 | 178 | .grid { 179 | grid-template-columns: 1fr; 180 | margin-bottom: 120px; 181 | max-width: 320px; 182 | text-align: center; 183 | } 184 | 185 | .card { 186 | padding: 1rem 2.5rem; 187 | } 188 | 189 | .card h2 { 190 | margin-bottom: 0.5rem; 191 | } 192 | 193 | .center { 194 | padding: 8rem 0 6rem; 195 | } 196 | 197 | .center::before { 198 | transform: none; 199 | height: 300px; 200 | } 201 | 202 | .description { 203 | font-size: 0.8rem; 204 | } 205 | 206 | .description a { 207 | padding: 1rem; 208 | } 209 | 210 | .description p, 211 | .description div { 212 | display: flex; 213 | justify-content: center; 214 | position: fixed; 215 | width: 100%; 216 | } 217 | 218 | .description p { 219 | align-items: center; 220 | inset: 0 0 auto; 221 | padding: 2rem 1rem 1.4rem; 222 | border-radius: 0; 223 | border: none; 224 | border-bottom: 1px solid rgba(var(--callout-border-rgb), 0.25); 225 | background: linear-gradient(to bottom, rgba(var(--background-start-rgb), 1), rgba(var(--callout-rgb), 0.5)); 226 | background-clip: padding-box; 227 | backdrop-filter: blur(24px); 228 | } 229 | 230 | .description div { 231 | align-items: flex-end; 232 | pointer-events: none; 233 | inset: auto 0 0; 234 | padding: 2rem; 235 | height: 200px; 236 | background: linear-gradient(to bottom, transparent 0%, rgb(var(--background-end-rgb)) 40%); 237 | z-index: 1; 238 | } 239 | } 240 | 241 | @media (prefers-color-scheme: dark) { 242 | .vercelLogo { 243 | filter: invert(1); 244 | } 245 | 246 | .logo, 247 | .thirteen img { 248 | filter: invert(1) drop-shadow(0 0 0.3rem #ffffff70); 249 | } 250 | } 251 | 252 | @keyframes rotate { 253 | from { 254 | transform: rotate(360deg); 255 | } 256 | to { 257 | transform: rotate(0deg); 258 | } 259 | } 260 | -------------------------------------------------------------------------------- /app/page.tsx: -------------------------------------------------------------------------------- 1 | import { Footer } from '@/components/Home/Footer'; 2 | import { Navigation } from '@/components/Home/Navigation'; 3 | import Main from '@/components/Home/Main'; 4 | import Features from '@/components/Home/Features'; 5 | 6 | export default function Home() { 7 | return ( 8 |
9 |
10 | 11 |
12 |
13 |
14 |
15 |
16 |
17 | 18 |
19 |
20 |
21 |
22 | ); 23 | } 24 | -------------------------------------------------------------------------------- /app/providers.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import FlowWrapper from '@/components/Map/FlowWrapper'; 4 | import { AnalyticsWrapper } from '@/components/Providers/Analytics'; 5 | import { Toaster } from '@/components/ui/Toaster'; 6 | import { TooltipProvider } from '@/components/ui/Tooltip'; 7 | import { ConfigurationProvider } from '@/utils/providers/ConfigurationProvider'; 8 | 9 | interface ProvidersProps { 10 | children: React.ReactNode; 11 | } 12 | 13 | export default function Providers({ children }: ProvidersProps) { 14 | return ( 15 | <> 16 | 17 | 18 | {children} 19 | 20 | 21 | 22 | 23 | 24 | 25 | ); 26 | } 27 | -------------------------------------------------------------------------------- /components/Home/ActionButtons.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | import { Button } from '@/components/ui/Button'; 3 | 4 | export default function ActionButtons() { 5 | const goToMap = () => { 6 | window.open('/map', '_self'); 7 | }; 8 | 9 | const goToGithub = () => { 10 | window.open('https://github.com/fernandops26/mentalist-ai', '_blank', 'noopener'); 11 | }; 12 | 13 | return ( 14 |
15 | 16 | 19 |
20 | ); 21 | } 22 | -------------------------------------------------------------------------------- /components/Home/Features.tsx: -------------------------------------------------------------------------------- 1 | import IconComponent from '../ui/Icon'; 2 | import TryItButton from './TryItButton'; 3 | 4 | const features = [ 5 | { 6 | imgSrc: 'https://tailus.io/sources/blocks/stacked/preview/images/avatars/burger.png', 7 | icon: 'idea', 8 | imgAlt: 'burger illustration', 9 | title: 'Generate content ideas', 10 | description: 'Easily generate ideas for content related to any social media topic', 11 | }, 12 | { 13 | imgSrc: 'https://tailus.io/sources/blocks/stacked/preview/images/avatars/trowel.png', 14 | imgAlt: 'trowel illustration', 15 | icon: 'candy', 16 | title: 'One-Click Title Suggestions', 17 | description: 'Quickly find engaging and effective titles for your content', 18 | }, 19 | { 20 | imgSrc: 'https://tailus.io/sources/blocks/stacked/preview/images/avatars/package-delivery.png', 21 | imgAlt: 'package delivery illustration', 22 | icon: 'twitter', 23 | title: 'Customizable Content Approach', 24 | description: 25 | 'Generate the perfect title for your YouTube, blog, or Twitter content to help it stand out and get noticed', 26 | }, 27 | { 28 | imgSrc: 'https://tailus.io/sources/blocks/stacked/preview/images/avatars/metal.png', 29 | imgAlt: 'metal illustration', 30 | icon: 'code', 31 | title: 'Developed with Next.js 13', 32 | description: 33 | 'Take advantage of the latest features and improvements in Next.js 13 for faster and more efficient development', 34 | }, 35 | ]; 36 | 37 | export default function Features() { 38 | return ( 39 |
40 |
41 |
42 |

Features

43 |
44 |
45 | {features.map((feature) => ( 46 | 47 | ))} 48 |
49 |
50 |
51 | ); 52 | } 53 | 54 | interface FeatureProps { 55 | icon: string; 56 | imgAlt: string; 57 | title: string; 58 | description: string; 59 | } 60 | function Feature({ icon, imgAlt, title, description }: FeatureProps) { 61 | return ( 62 |
63 |
64 | 65 | 66 |
67 |
{title}
68 |

{description}

69 |
70 | 71 | {/* 75 | 76 | */} 77 |
78 |
79 | ); 80 | } 81 | -------------------------------------------------------------------------------- /components/Home/Footer.tsx: -------------------------------------------------------------------------------- 1 | import Link from 'next/link'; 2 | import IconComponent from '../ui/Icon'; 3 | 4 | export function Footer() { 5 | return ( 6 |
7 |
8 |
9 | {/* */} 10 | 11 |

12 | Powered by{' '} 13 | 19 | Open AI 20 | 21 | . Hosted on{' '} 22 | 28 | Vercel 29 | 30 |

31 |
32 |

33 | 39 | 40 | 41 | 47 | 48 | 49 |

50 |
51 |
52 | ); 53 | } 54 | -------------------------------------------------------------------------------- /components/Home/Main.tsx: -------------------------------------------------------------------------------- 1 | import { AspectRatio } from '@/components/ui/AspectRadio'; 2 | import ActionButtons from './ActionButtons'; 3 | 4 | export default function Main() { 5 | return ( 6 |
7 |
8 |
9 |

Your Ultimate Content Ideation Tool

10 |

11 | Easily Create Mind Maps and Generate Content and Title Ideas Based on Topics 12 |

13 |
14 | 15 |
16 |
17 |
18 |

Use your own OpenAI api key

19 |
20 |
21 |
22 | 23 | 29 |
30 |
31 |
32 | ); 33 | } 34 | -------------------------------------------------------------------------------- /components/Home/Navigation.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import Link from 'next/link'; 3 | import IconComponent from '../ui/Icon'; 4 | 5 | interface MainNavProps { 6 | children?: React.ReactNode; 7 | } 8 | 9 | export function Navigation({ children }: MainNavProps) { 10 | return ( 11 |
12 | 13 | 14 | Mentalist 15 | 16 | 17 |
18 | ); 19 | } 20 | -------------------------------------------------------------------------------- /components/Home/TryItButton.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | import { Button } from '../ui/Button'; 3 | 4 | export default function TryItButton() { 5 | const goToMap = () => { 6 | window.open('/map', '_self'); 7 | }; 8 | 9 | return ( 10 | 13 | ); 14 | } 15 | -------------------------------------------------------------------------------- /components/Layout/AppLayout/AppLayout.tsx: -------------------------------------------------------------------------------- 1 | import Top from './Top'; 2 | 3 | interface AppLayoutProps { 4 | children: React.ReactNode; 5 | } 6 | 7 | export default function AppLayout({ children }: AppLayoutProps) { 8 | return ( 9 |
10 | 11 |
{children}
12 |
13 | ); 14 | } 15 | -------------------------------------------------------------------------------- /components/Layout/AppLayout/Menu.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import { actionLoadFileFromDisk, actionSaveFileToDisk } from '@/actions/export'; 4 | import IconComponent from '@/components/ui/Icon'; 5 | import { 6 | Menubar, 7 | MenubarContent, 8 | MenubarItem, 9 | MenubarMenu, 10 | MenubarSeparator, 11 | MenubarTrigger, 12 | } from '@/components/ui/MenuBar'; 13 | import { useToast } from '@/hooks/use-toast'; 14 | import useMapStore from '@/stores/mapStore'; 15 | import Link from 'next/link'; 16 | import { useState } from 'react'; 17 | import { OpenAIAKDialog } from './OpenAIAKDialog'; 18 | 19 | export function Menu() { 20 | const { toast } = useToast(); 21 | const [open, setOpen] = useState(false); 22 | 23 | const loadFromStorage = useMapStore((s) => s.loadFromStorage); 24 | 25 | const onExport = async () => { 26 | await actionSaveFileToDisk(); 27 | 28 | toast({ 29 | title: 'Map successfully exported', 30 | description: 'Your mind map has been exported correctly.', 31 | }); 32 | }; 33 | 34 | const onLoad = async () => { 35 | const loaded = await actionLoadFileFromDisk(); 36 | 37 | if (!loaded) { 38 | return; 39 | } 40 | 41 | loadFromStorage(); 42 | 43 | toast({ 44 | title: 'Map successfully loaded', 45 | description: 'Your mind map has been loaded correctly.', 46 | }); 47 | }; 48 | 49 | return ( 50 | 51 | 52 | 53 | 54 | 55 | 56 | setOpen(true)}> Manage OpenAI ApiKey 57 | 58 | 59 | Open 60 | Export 61 | 62 | 63 | 64 | Follow Updates 65 | {' '} 66 | 67 | 68 | 74 | Github 75 | {' '} 76 | 77 | 78 | 79 | {open && } 80 | 81 | ); 82 | } 83 | -------------------------------------------------------------------------------- /components/Layout/AppLayout/OpenAIAKDialog.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import { Button } from '@/components/ui/Button'; 4 | import { 5 | Dialog, 6 | DialogContent, 7 | DialogDescription, 8 | DialogFooter, 9 | DialogHeader, 10 | DialogTitle, 11 | } from '@/components/ui/Dialog'; 12 | 13 | import { Input } from '@/components/ui/Input'; 14 | import { Label } from '@/components/ui/Label'; 15 | import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/Select'; 16 | import { AvailableModel, AVAILABLE_MODELS, isAvailableModel } from '@/utils/constants/openai'; 17 | 18 | import { useConfiguration } from '@/utils/providers/ConfigurationProvider'; 19 | import React, { useState } from 'react'; 20 | 21 | interface OpenAIAKDialogProps { 22 | isOpen: boolean; 23 | openChange: (isOpen: boolean) => void; 24 | } 25 | 26 | export const OpenAIAKDialog = ({ isOpen, openChange }: OpenAIAKDialogProps) => { 27 | const { token, updateToken, model, updateModel } = useConfiguration(); 28 | 29 | const [apiKey, setApiKey] = useState(() => token ?? ''); 30 | const [apiModel, setApiModel] = useState(() => model); 31 | 32 | const handleSave = () => { 33 | updateToken(apiKey); 34 | updateModel(apiModel); 35 | openChange(false); 36 | }; 37 | 38 | return ( 39 | 40 | 41 | 42 | Manage Open AI Api key 43 | Set your api key from here. Click save when you are done. 44 | 45 | 46 |
47 |
48 | 51 | 52 | setApiKey(e.target.value)} 58 | /> 59 |
60 | 61 |
62 | 65 | 66 | 79 |
80 |
81 | 82 | 83 | 84 | 85 |
86 |
87 | ); 88 | }; 89 | -------------------------------------------------------------------------------- /components/Layout/AppLayout/Top.tsx: -------------------------------------------------------------------------------- 1 | import IconComponent from '@/components/ui/Icon'; 2 | import Link from 'next/link'; 3 | import { Menu } from './Menu'; 4 | 5 | export default function Top() { 6 | return ( 7 |
8 |
9 | 10 | 11 | Mentalist 12 | 13 |
14 | 15 |
16 |
17 |
18 | ); 19 | } 20 | -------------------------------------------------------------------------------- /components/Map/Edges/CustomizableEdge.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { EdgeProps, getBezierPath } from 'reactflow'; 3 | 4 | export default function CustomizableEdge({ 5 | id, 6 | sourceX, 7 | sourceY, 8 | targetX, 9 | targetY, 10 | sourcePosition, 11 | targetPosition, 12 | style = {}, 13 | data, 14 | markerEnd, 15 | }: EdgeProps) { 16 | const [edgePath] = getBezierPath({ 17 | sourceX, 18 | sourceY, 19 | sourcePosition, 20 | targetX, 21 | targetY, 22 | targetPosition, 23 | }); 24 | 25 | return ( 26 | <> 27 | 28 | 29 | 30 | 31 | 32 | ); 33 | } 34 | -------------------------------------------------------------------------------- /components/Map/Flow.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import 'reactflow/dist/style.css'; 4 | 5 | import React, { useEffect, useRef } from 'react'; 6 | import ReactFlow, { Background, Controls, MiniMap } from 'reactflow'; 7 | import useMapStore, { RFState } from '@/stores/mapStore'; 8 | import { shallow } from 'zustand/shallow'; 9 | import { nodeTypes } from '@/data/defaultNodes'; 10 | import { edgeTypes } from '@/data/defaultEdges'; 11 | import DataSaver from './Plugins/DataSaver'; 12 | import TopPanel from './Panel/TopPanel/TopPanel'; 13 | 14 | const panOnDrag = [1, 2]; 15 | 16 | const selector = (state: RFState) => ({ 17 | nodes: state.nodes, 18 | edges: state.edges, 19 | viewport: state.viewport, 20 | onNodesChange: state.onNodesChange, 21 | onEdgesChange: state.onEdgesChange, 22 | onConnectStart: state.onConnectStart, 23 | onConnectEnd: state.onConnectEnd, 24 | setReactFlowWrapper: state.setReactFlowWrapper, 25 | onInit: state.onInit, 26 | instance: state.instance, 27 | }); 28 | 29 | const fitViewOptions = { 30 | padding: 3, 31 | }; 32 | 33 | function Flow() { 34 | const reactFlowWrapper = useRef(null); 35 | const { 36 | nodes, 37 | edges, 38 | viewport, 39 | onNodesChange, 40 | onEdgesChange, 41 | onConnectStart, 42 | onConnectEnd, 43 | setReactFlowWrapper, 44 | onInit, 45 | } = useMapStore(selector, shallow); 46 | 47 | useEffect(() => { 48 | setReactFlowWrapper(reactFlowWrapper); 49 | }, [setReactFlowWrapper]); 50 | 51 | return ( 52 | <> 53 |
54 | 71 | 72 | 73 | 74 | 75 | 76 | 77 |
78 | 79 | ); 80 | } 81 | 82 | export default Flow; 83 | -------------------------------------------------------------------------------- /components/Map/FlowWrapper.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import React from 'react'; 4 | import { ReactFlowProvider } from 'reactflow'; 5 | 6 | interface FlowWrapperProps { 7 | children: React.ReactNode; 8 | } 9 | 10 | export default function FlowWrapper({ children }: FlowWrapperProps) { 11 | return {children}; 12 | } 13 | -------------------------------------------------------------------------------- /components/Map/Generator/Generator.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import { Button } from '@/components/ui/Button'; 4 | import { Label } from '@/components/ui/Label'; 5 | import Loader from '@/components/ui/Loader'; 6 | import { Select, SelectContent, SelectGroup, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/Select'; 7 | import { useState } from 'react'; 8 | 9 | interface GeneratorProps { 10 | onGenerate: ({ accurateFor, type }: { accurateFor: string; type: string }) => void; 11 | isLoading: boolean; 12 | } 13 | export default function Generator({ onGenerate, isLoading }: GeneratorProps) { 14 | const [selected, setSelected] = useState({ 15 | accurateFor: 'blogs', 16 | type: 'content ideas', 17 | }); 18 | 19 | return ( 20 |
21 |
22 |

Generator

23 |

24 | Enter parameters to give direction to the generator. 25 |

26 |
27 |
28 |
29 | 30 |
31 | 43 |
44 |
45 | 46 |
47 | 48 |
49 | 64 |
65 |
66 | 67 |
68 |
69 | 70 | {isLoading && ( 71 |
72 | {' '} 73 | 74 |
75 | )} 76 |
77 |
78 |
79 |
80 | ); 81 | } 82 | -------------------------------------------------------------------------------- /components/Map/Index.tsx: -------------------------------------------------------------------------------- 1 | import Flow from './Flow'; 2 | 3 | export default function Map() { 4 | return ( 5 |
6 | 7 |
8 | ); 9 | } 10 | -------------------------------------------------------------------------------- /components/Map/Nodes/AIContentModal.tsx: -------------------------------------------------------------------------------- 1 | import { Popover, PopoverContent, PopoverAnchor } from '@/components/ui/Popover'; 2 | import { SUBTOPIC } from '@/utils/constants/headerTypes'; 3 | import Generator from '../Generator/Generator'; 4 | import { useToast } from '@/hooks/use-toast'; 5 | import { useConfiguration } from '@/utils/providers/ConfigurationProvider'; 6 | import { ReactNode, useState } from 'react'; 7 | import useMapStore from '@/stores/mapStore'; 8 | import { generateIdeas } from '@/utils/api/suggestions'; 9 | 10 | interface AIContentProps { 11 | id: string; 12 | isOpen: boolean; 13 | onChangeOpen: (open: boolean) => void; 14 | children: ReactNode; 15 | } 16 | 17 | export default function AIContentModal({ id, isOpen, onChangeOpen, children }: AIContentProps) { 18 | const { token, model } = useConfiguration(); 19 | const { toast } = useToast(); 20 | const [isLoading, setIsLoading] = useState(false); 21 | const getNodeContext = useMapStore((s) => s.getNodeContext); 22 | const addChildrenNodes = useMapStore((s) => s.addChildrenNodes); 23 | const removeElement = useMapStore((s) => s.removeElement); 24 | 25 | const generator = async ({ accurateFor, type }: { accurateFor: string; type: string }) => { 26 | if (!token) { 27 | toast({ 28 | variant: 'destructive', 29 | title: 'You need to configure your OpenAI API key first', 30 | description: 'Use the hamburger menu located at the top of the page and configure your OpenAI API key.', 31 | }); 32 | 33 | return; 34 | } 35 | 36 | setIsLoading(true); 37 | const { main, context } = getNodeContext(id); 38 | 39 | const ideas = await generateIdeas({ 40 | main, 41 | context, 42 | token, 43 | accurateFor, 44 | type, 45 | model, 46 | }); 47 | 48 | const newNodes = ideas.map((idea: string) => ({ 49 | text: idea, 50 | type: SUBTOPIC, 51 | parentId: id, 52 | })); 53 | 54 | addChildrenNodes(id, 'topicNode', newNodes); 55 | setIsLoading(false); 56 | onChangeOpen(false); 57 | }; 58 | 59 | return ( 60 | 61 | {children} 62 | 63 | 64 | 65 | 66 | 67 | ); 68 | } 69 | -------------------------------------------------------------------------------- /components/Map/Nodes/RootNode.tsx: -------------------------------------------------------------------------------- 1 | import useMapStore from '@/stores/mapStore'; 2 | import { memo, useEffect, useState } from 'react'; 3 | import { Handle, Position } from 'reactflow'; 4 | import { useDebounce } from 'react-use'; 5 | import ToggleInput from '@/components/ui/ToggleInput'; 6 | import BlockContainer from '@/components/ui/BlockContainer'; 7 | // import NodeHeader from '@/components/ui/NodeHeader'; 8 | import { useConfiguration } from '@/utils/providers/ConfigurationProvider'; 9 | import AIContentModal from './AIContentModal'; 10 | import { AI_MODE } from '@/utils/constants/modes'; 11 | 12 | const RootNode = ({ id, data }: any) => { 13 | const { mode } = useConfiguration(); 14 | const [isOpenAIModal, setIsOpenAIModal] = useState(false); 15 | const [value, setValue] = useState(data.text); 16 | 17 | const updateText = useMapStore((s) => s.updateText); 18 | 19 | const [, cancel] = useDebounce( 20 | () => { 21 | updateText(id, value); 22 | }, 23 | 1000, 24 | [value], 25 | ); 26 | 27 | useEffect(() => { 28 | setValue(data.text); 29 | }, [data.text]); 30 | 31 | const onClick = () => { 32 | switch (mode) { 33 | case AI_MODE: 34 | setIsOpenAIModal(true); 35 | break; 36 | } 37 | }; 38 | 39 | const renderContent = () => { 40 | if (mode === AI_MODE) { 41 | return

{value}

; 42 | } 43 | 44 | return ; 45 | }; 46 | 47 | return ( 48 |
49 | 50 | 51 | {/* */} 52 |
{renderContent()}
53 | 54 |
55 |
56 |
57 | ); 58 | }; 59 | 60 | export default memo(RootNode); 61 | -------------------------------------------------------------------------------- /components/Map/Nodes/TopicNode.tsx: -------------------------------------------------------------------------------- 1 | import useMapStore from '@/stores/mapStore'; 2 | import { memo, useEffect, useState } from 'react'; 3 | import { Handle, Position } from 'reactflow'; 4 | import { useDebounce } from 'react-use'; 5 | import BlockContainer from '@/components/ui/BlockContainer'; 6 | // import NodeHeader from '@/components/ui/NodeHeader'; 7 | import { useConfiguration } from '@/utils/providers/ConfigurationProvider'; 8 | import ToggleTextarea from '@/components/ui/ToggleTextarea'; 9 | import AIContentModal from './AIContentModal'; 10 | import { AI_MODE } from '@/utils/constants/modes'; 11 | 12 | const TopicNode = ({ id, data }: any) => { 13 | const { mode } = useConfiguration(); 14 | 15 | const [value, setValue] = useState(data.text); 16 | const [isOpenAIModal, setIsOpenAIModal] = useState(false); 17 | 18 | const updateText = useMapStore((s) => s.updateText); 19 | // const updateInnerType = useMapStore((s) => s.updateInnerType); 20 | const removeElement = useMapStore((s) => s.removeElement); 21 | 22 | const [, cancel] = useDebounce( 23 | () => { 24 | updateText(id, value); 25 | }, 26 | 1000, 27 | [value], 28 | ); 29 | 30 | useEffect(() => { 31 | setValue(data.text); 32 | }, [data.text]); 33 | 34 | // const updateType = (type: string) => { 35 | // updateInnerType(id, type); 36 | // }; 37 | 38 | const onRemove = () => { 39 | removeElement(id); 40 | }; 41 | 42 | const onClick = () => { 43 | switch (mode) { 44 | case AI_MODE: 45 | setIsOpenAIModal(true); 46 | break; 47 | } 48 | }; 49 | 50 | const renderContent = () => { 51 | if (mode === AI_MODE) { 52 | return

{value}

; 53 | } 54 | 55 | return ; 56 | }; 57 | 58 | return ( 59 |
60 | {/* */} 61 | 62 | 63 | 64 | 65 |
{renderContent()}
66 | 67 |
68 |
69 |
70 | ); 71 | }; 72 | 73 | export default memo(TopicNode); 74 | -------------------------------------------------------------------------------- /components/Map/Panel/TopPanel/Theme.tsx: -------------------------------------------------------------------------------- 1 | import { palettes } from '@/data/defaultPalettes'; 2 | import useMapStore from '@/stores/mapStore'; 3 | import cn from '@/utils/classnames'; 4 | import { getConfigKey, updateConfig } from '@/utils/storage'; 5 | import { PaletteElement } from '@/utils/types'; 6 | import { useState } from 'react'; 7 | 8 | export default function Theme() { 9 | const [activePalette, setActivePalette] = useState(getConfigKey('palette')); 10 | const applyPalette = useMapStore((s) => s.applyPalette); 11 | 12 | const onChoosePalette = (selectedPalette: PaletteElement) => { 13 | updateConfig('palette', selectedPalette.id); 14 | setActivePalette(selectedPalette.id); 15 | applyPalette(selectedPalette); 16 | }; 17 | 18 | return ( 19 |
20 | {palettes.map((palette) => { 21 | const rootStyles = palette.root.buildStyles(); 22 | 23 | return ( 24 |
onChoosePalette(palette)} 27 | className={cn( 28 | 'p-2 hover:bg-slate-100 hover:cursor-pointer flex rounded', 29 | palette.id === activePalette ? 'bg-slate-200' : '', 30 | )} 31 | > 32 |
33 | {palette.colors.map((color) => { 34 | const styles = palette.node.buildStyles(color); 35 | 36 | return
; 37 | })} 38 |
39 | ); 40 | })} 41 |
42 | ); 43 | } 44 | -------------------------------------------------------------------------------- /components/Map/Panel/TopPanel/TopPanel.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | import IconComponent from '@/components/ui/Icon'; 3 | import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/Popover'; 4 | import { Separator } from '@/components/ui/Separator'; 5 | import { ToggleGroup, ToggleGroupItem } from '@/components/ui/ToggleGroup'; 6 | import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/Tooltip'; 7 | import cn from '@/utils/classnames'; 8 | import { AI_MODE, AvailableModes, SELECTION_MODE } from '@/utils/constants/modes'; 9 | import { useConfiguration } from '@/utils/providers/ConfigurationProvider'; 10 | import { Panel } from 'reactflow'; 11 | import Theme from './Theme'; 12 | 13 | export default function ModePanel() { 14 | const { mode, updateMode } = useConfiguration(); 15 | 16 | const handleUpdate = (value: string) => { 17 | if (!value) { 18 | return; 19 | } 20 | 21 | updateMode(value as AvailableModes); 22 | }; 23 | 24 | return ( 25 | 26 |
27 |
28 | 35 | 36 | 37 | 38 |
39 | 40 |
41 |
42 | 43 |

Selection Mode

44 |
45 |
46 |
47 | 48 | 49 | 50 |
51 | 52 |
53 |
54 | 55 |

AI Mode

56 |
57 |
58 |
59 |
60 |
61 | 62 | 63 |
64 | 65 | 66 |
67 | 68 | 69 |
77 | 78 |
79 |
80 | 81 |
82 |

Theme

83 | 84 |
85 |
86 |
87 |
88 |
89 | 90 |

Customization

91 |
92 |
93 |
94 |
95 |
96 | ); 97 | } 98 | -------------------------------------------------------------------------------- /components/Map/Plugins/DataSaver.tsx: -------------------------------------------------------------------------------- 1 | import useMapStore from '@/stores/mapStore'; 2 | import { saveMap } from '@/utils/storage'; 3 | import { useEffect } from 'react'; 4 | import { useNodesInitialized, useReactFlow } from 'reactflow'; 5 | 6 | function DataSaver() { 7 | const store = useMapStore(); 8 | const reactFlowInstance = useReactFlow(); 9 | const nodesInitialized = useNodesInitialized(); 10 | 11 | useEffect(() => { 12 | if (nodesInitialized) { 13 | const map = reactFlowInstance.toObject(); 14 | 15 | saveMap(map); 16 | } 17 | }, [store.nodes, store.edges, store.viewport]); 18 | 19 | return null; 20 | } 21 | 22 | export default DataSaver; 23 | -------------------------------------------------------------------------------- /components/Providers/Analytics.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | import { Analytics } from '@vercel/analytics/react'; 3 | 4 | export function AnalyticsWrapper() { 5 | return ; 6 | } 7 | -------------------------------------------------------------------------------- /components/ui/AspectRadio.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import * as AspectRatioPrimitive from '@radix-ui/react-aspect-ratio'; 4 | 5 | const AspectRatio = AspectRatioPrimitive.Root; 6 | 7 | export { AspectRatio }; 8 | -------------------------------------------------------------------------------- /components/ui/BlockContainer.tsx: -------------------------------------------------------------------------------- 1 | import { ReactNode } from 'react'; 2 | import IconComponent from './Icon'; 3 | 4 | interface BlockContainerProps { 5 | children: ReactNode; 6 | onRemove?: () => void; 7 | menu?: ReactNode; 8 | } 9 | 10 | export default function BlockContainer({ children, onRemove, menu }: BlockContainerProps) { 11 | return ( 12 |
13 | {onRemove && ( 14 |
onRemove()} 17 | > 18 |
19 | 20 |
21 |
22 | )} 23 | 24 | {menu &&
{menu}
} 25 | {children} 26 |
27 | ); 28 | } 29 | -------------------------------------------------------------------------------- /components/ui/Button.tsx: -------------------------------------------------------------------------------- 1 | import cn from '@/utils/classnames'; 2 | import { VariantProps, cva } from 'class-variance-authority'; 3 | import React from 'react'; 4 | 5 | const buttonVariants = cva( 6 | 'active:scale-95 inline-flex items-center justify-center rounded-md text-sm font-medium transition-colors focus:outline-none focus:ring-2 focus:ring-slate-400 focus:ring-offset-2 disabled:opacity-50 disabled:pointer-events-none data-[state=open]:bg-slate-100', 7 | { 8 | variants: { 9 | variant: { 10 | default: 'bg-slate-900 text-white hover:bg-slate-700', 11 | destructive: 'bg-red-500 text-white hover:bg-red-600', 12 | outline: 'bg-transparent border border-slate-200 hover:bg-slate-100', 13 | subtle: 'bg-slate-100 text-slate-900 hover:bg-slate-200', 14 | ghost: 'bg-transparent hover:bg-slate-100 data-[state=open]:bg-transparent', 15 | link: 'bg-transparent underline-offset-4 hover:underline text-slate-900 hover:bg-transparent', 16 | }, 17 | size: { 18 | default: 'h-10 py-2 px-4', 19 | sm: 'h-9 px-2 rounded-md', 20 | lg: 'h-11 px-8 rounded-md', 21 | }, 22 | }, 23 | defaultVariants: { 24 | variant: 'default', 25 | size: 'default', 26 | }, 27 | }, 28 | ); 29 | 30 | export interface ButtonProps 31 | extends React.ButtonHTMLAttributes, 32 | VariantProps { 33 | [x: string]: any; 34 | } 35 | 36 | const Button = React.forwardRef(({ className, variant, size, ...props }, ref) => { 37 | return