Repository: MullerEsposito/stark_overflow
Pull request #11: Feat: New Question Page with Markdown Editor
Author: login: codeAKstan
Created: 2025-04-24T19:27:02Z | Merged: 2025-04-26T22:38:05Z

![Screenshot 2025-04-24 at 19-22-11 Stark Oveflow](https://github.com/user-attachments/assets/64f2aa3b-1b95-4c31-8ac9-1983580c2361)



Diff:
diff --git a/frontend/package-lock.json b/frontend/package-lock.json
new file mode 100644
index 0000000..aba25f7
--- /dev/null
+++ b/frontend/package-lock.json
@@ -0,0 +1,6 @@
+{
+  "name": "frontend",
+  "lockfileVersion": 3,
+  "requires": true,
+  "packages": {}
+}
diff --git a/frontend/react/package.json b/frontend/react/package.json
index 979900f..9939490 100644
--- a/frontend/react/package.json
+++ b/frontend/react/package.json
@@ -14,9 +14,12 @@
     "@starknet-react/core": "^3.7.2",
     "phosphor-react": "^1.4.1",
     "polished": "^4.3.1",
-    "react": "^19.0.0",
-    "react-dom": "^19.0.0",
+    "react": "^18.2.0",
+    "react-dom": "^18.2.0",
+    "react-markdown": "^10.1.0",
     "react-router-dom": "^7.2.0",
+    "react-syntax-highlighter": "^15.6.1",
+    "remark-gfm": "^4.0.1",
     "starknet": "^6.23.1",
     "styled-components": "^6.1.15"
   },
@@ -25,6 +28,7 @@
     "@types/node": "^22.13.5",
     "@types/react": "^19.0.10",
     "@types/react-dom": "^19.0.4",
+    "@types/react-syntax-highlighter": "^15.5.13",
     "@vitejs/plugin-react": "^4.3.4",
     "eslint": "^9.21.0",
     "eslint-plugin-react-hooks": "^5.0.0",
diff --git a/frontend/react/src/pages/NewQuestion/index.tsx b/frontend/react/src/pages/NewQuestion/index.tsx
index ed204ab..eef99c2 100644
--- a/frontend/react/src/pages/NewQuestion/index.tsx
+++ b/frontend/react/src/pages/NewQuestion/index.tsx
@@ -1,39 +1,550 @@
-import { PaperPlaneRight, Link, Tag, CurrencyDollar } from "phosphor-react";
-import { Container, Form, TitleInput, DescriptionTextArea, RepositoryInput, TagsInput, Button, AmountInput } from "./style";
-import { NavLink } from "react-router-dom";
+"use client"
+
+import type React from "react"
+import { useState, useRef } from "react"
+import { PaperPlaneRight, Link as LinkIcon, Tag, CurrencyDollar, Info, X, Image, FileArrowUp } from "phosphor-react"
+import {
+  Container,
+  Form,
+  TitleInput,
+  EditorContainer,
+  RepositoryInput,
+  TagsInput,
+  Button,
+  AmountInput,
+  ErrorMessage,
+  ToolbarButton,
+  EditorToolbar,
+  PreviewContainer,
+  TabContainer,
+  Tab,
+  FileUploadArea,
+  UploadedFilePreview,
+  UploadedImage,
+  RemoveFileButton,
+  FormGroup,
+  Label,
+  Tooltip,
+  TooltipText,
+  ValidationIndicator,
+  TransactionStatus,
+  UploadProgress,
+} from "./style"
+import { NavLink, useNavigate } from "react-router-dom"
+import { uploadFile, deleteFile, type UploadedFile } from "../../services/file-upload"
+import { useAccount } from "@starknet-react/core"
+import { useWallet } from "../../providers/wallet-connect-context"
+
+// Import dynamic components for markdown rendering
+const ReactMarkdown = await import("react-markdown").then((mod) => mod.default || mod)
+const remarkGfm = await import("remark-gfm").then((mod) => mod.default || mod)
+
+// Define proper types for the code component props
+interface CodeProps {
+  className?: string
+  inline?: boolean
+  children?: React.ReactNode
+}
+
+// Define proper types for the image component props
+interface ImageProps {
+  src?: string
+  alt?: string
+}
+
+// Custom code component for syntax highlighting
+const CodeBlock: React.FC<CodeProps> = ({ inline, className, children, ...props }) => {
+  const match = /language-(\w+)/.exec(className || "")
+  const language = match ? match[1] : ""
+
+  if (!inline && match) {
+    return (
+      <div className="code-block">
+        <div className="code-header">
+          <span>{language}</span>
+        </div>
+        <pre className={className} {...props}>
+          <code className={className} {...props}>
+            {children}
+          </code>
+        </pre>
+      </div>
+    )
+  }
+
+  return (
+    <code className={className} {...props}>
+      {children}
+    </code>
+  )
+}
 
 export function NewQuestion() {
+  const [title, setTitle] = useState("")
+  const [description, setDescription] = useState("")
+  const [amount, setAmount] = useState("")
+  const [repository, setRepository] = useState("")
+  const [tags, setTags] = useState("")
+  const [activeTab, setActiveTab] = useState<"write" | "preview">("write")
+  const [errors, setErrors] = useState<Record<string, string>>({})
+  const [uploadedFiles, setUploadedFiles] = useState<UploadedFile[]>([])
+  const [isUploading, setIsUploading] = useState(false)
+  const [uploadProgress, setUploadProgress] = useState(0)
+  const [transactionStatus, setTransactionStatus] = useState<"idle" | "processing" | "success" | "error">("idle")
+  const fileInputRef = useRef<HTMLInputElement>(null)
+  const textAreaRef = useRef<HTMLTextAreaElement>(null)
+  const navigate = useNavigate()
+  const { isConnected } = useAccount()
+  const { openConnectModal } = useWallet()
+
+  // Create a map of image URLs to their blob URLs for the preview
+  const [imageMap, setImageMap] = useState<Record<string, string>>({})
+
+  // Form validation
+  const validateForm = () => {
+    const newErrors: Record<string, string> = {}
+
+    if (!title.trim()) {
+      newErrors.title = "Title is required"
+    } else if (title.length < 10) {
+      newErrors.title = "Title should be at least 10 characters"
+    }
+
+    if (!description.trim()) {
+      newErrors.description = "Description is required"
+    } else if (description.length < 30) {
+      newErrors.description = "Description should be at least 30 characters"
+    }
+
+    if (!amount.trim()) {
+      newErrors.amount = "Amount is required"
+    } else if (isNaN(Number(amount)) || Number(amount) <= 0) {
+      newErrors.amount = "Amount must be a positive number"
+    }
+
+    setErrors(newErrors)
+    return Object.keys(newErrors).length === 0
+  }
+
+  // Handle file upload
+  const handleFileUpload = async (e: React.ChangeEvent<HTMLInputElement>) => {
+    if (e.target.files && e.target.files.length > 0) {
+      const file = e.target.files[0]
+
+      // Check if it's an image
+      if (file.type.startsWith("image/")) {
+        setIsUploading(true)
+        setUploadProgress(0)
+
+        // Simulate upload progress
+        const progressInterval = setInterval(() => {
+          setUploadProgress((prev) => {
+            const newProgress = prev + Math.random() * 20
+            return newProgress >= 90 ? 90 : newProgress
+          })
+        }, 200)
+
+        try {
+          // Upload the file
+          const uploadedFile = await uploadFile(file)
+
+          // Clear the interval and set progress to 100%
+          clearInterval(progressInterval)
+          setUploadProgress(100)
+
+          // Add to uploaded files
+          setUploadedFiles((prev) => [...prev, uploadedFile])
+
+          // Add image markdown to description
+          const imageMarkdown = `![${file.name}](${uploadedFile.url})\n`
+          setDescription((prev) => prev + imageMarkdown)
+
+          // Update the image map for preview
+          setImageMap((prev) => ({
+            ...prev,
+            [uploadedFile.url]: uploadedFile.url,
+          }))
+
+          // Reset upload state after a delay
+          setTimeout(() => {
+            setIsUploading(false)
+            setUploadProgress(0)
+          }, 500)
+        } catch (error) {
+          console.error("File upload failed:", error)
+          clearInterval(progressInterval)
+          setIsUploading(false)
+          setUploadProgress(0)
+        }
+      }
+    }
+
+    // Reset file input
+    if (fileInputRef.current) {
+      fileInputRef.current.value = ""
+    }
+  }
+
+  // Remove uploaded file
+  const handleRemoveFile = async (file: UploadedFile, e: React.MouseEvent) => {
+    // Prevent event propagation to avoid form submission
+    e.preventDefault()
+    e.stopPropagation()
+
+    try {
+      // Delete the file
+      await deleteFile(file.id)
+
+      // Remove from state
+      setUploadedFiles((prev) => prev.filter((f) => f.id !== file.id))
+
+      // Remove from description
+      const imageMarkdown = `![${file.name}](${file.url})`
+      setDescription((prev) => prev.replace(imageMarkdown, ""))
+
+      // Remove from image map
+      const newImageMap = { ...imageMap }
+      delete newImageMap[file.url]
+      setImageMap(newImageMap)
+    } catch (error) {
+      console.error("File deletion failed:", error)
+    }
+  }
+
+  // Handle markdown toolbar actions
+  const insertMarkdown = (markdownSyntax: string, placeholder = "") => {
+    if (!textAreaRef.current) return
+
+    const textarea = textAreaRef.current
+    const start = textarea.selectionStart
+    const end = textarea.selectionEnd
+    const selectedText = description.substring(start, end)
+
+    let newText = ""
+    if (selectedText) {
+      // If text is selected, wrap it with markdown syntax
+      newText =
+        description.substring(0, start) + markdownSyntax.replace(placeholder, selectedText) + description.substring(end)
+    } else {
+      // If no text is selected, just insert the markdown syntax with placeholder
+      newText = description.substring(0, start) + markdownSyntax + description.substring(end)
+    }
+
+    setDescription(newText)
+
+    // Focus back on textarea and set cursor position
+    setTimeout(() => {
+      textarea.focus()
+      const newCursorPos = start + markdownSyntax.indexOf(placeholder) + (selectedText ? selectedText.length : 0)
+      textarea.setSelectionRange(newCursorPos, newCursorPos)
+    }, 0)
+  }
+
+  // Handle tab switching
+  const handleTabChange = (tab: "write" | "preview") => {
+    setActiveTab(tab)
+  }
+
+  // Handle form submission
+  const handleSubmit = async (e: React.FormEvent) => {
+    e.preventDefault()
+
+    if (!validateForm()) {
+      return
+    }
+
+    if (!isConnected) {
+      openConnectModal()
+      return
+    }
+
+    try {
+      setTransactionStatus("processing")
+
+      // Simulate blockchain transaction
+      await new Promise((resolve) => setTimeout(resolve, 2000))
+
+      // Success
+      setTransactionStatus("success")
+
+      // Redirect after successful submission
+      setTimeout(() => {
+        navigate("/forum/reactjs")
+      }, 1500)
+    } catch (error) {
+      console.error("Transaction error:", error)
+      setTransactionStatus("error")
+    }
+  }
+
+  // Custom image renderer that uses our image map
+  const ImageRenderer = ({ src, alt }: ImageProps) => {
+    if (!src) return null
+
+    // Use the actual blob URL from our map
+    return <img src={src || "/placeholder.svg"} alt={alt || ""} style={{ maxWidth: "100%" }} />
+  }
+
   return (
     <Container>
       <h2>Create Question</h2>
-      <Form>
-        <TitleInput type="text" placeholder="Title" />
-        <DescriptionTextArea placeholder="Describe your issue..." />
+      <Form onSubmit={handleSubmit}>
+        <FormGroup>
+          <Label htmlFor="title">
+            Title
+            <Tooltip>
+              <Info size={16} />
+              <TooltipText>Be specific and imagine you're asking a question to another person</TooltipText>
+            </Tooltip>
+            <ValidationIndicator valid={!errors.title && title.length > 0} />
+          </Label>
+          <TitleInput
+            id="title"
+            type="text"
+            placeholder="e.g. How to implement a custom hook in React?"
+            value={title}
+            onChange={(e) => setTitle(e.target.value)}
+            onBlur={() => validateForm()}
+            hasError={!!errors.title}
+          />
+          {errors.title && <ErrorMessage>{errors.title}</ErrorMessage>}
+        </FormGroup>
+
+        <FormGroup>
+          <Label htmlFor="description">
+            Description
+            <Tooltip>
+              <Info size={16} />
+              <TooltipText>Include all the information someone would need to answer your question</TooltipText>
+            </Tooltip>
+            <ValidationIndicator valid={!errors.description && description.length > 0} />
+          </Label>
+
+          <TabContainer>
+            <Tab type="button" active={activeTab === "write"} onClick={() => handleTabChange("write")}>
+              Write
+            </Tab>
+            <Tab type="button" active={activeTab === "preview"} onClick={() => handleTabChange("preview")}>
+              Preview
+            </Tab>
+          </TabContainer>
+
+          {activeTab === "write" ? (
+            <>
+              <EditorToolbar>
+                <ToolbarButton
+                  type="button"
+                  title="Bold"
+                  onClick={() => insertMarkdown("**placeholder**", "placeholder")}
+                >
+                  B
+                </ToolbarButton>
+                <ToolbarButton
+                  type="button"
+                  title="Italic"
+                  onClick={() => insertMarkdown("*placeholder*", "placeholder")}
+                >
+                  I
+                </ToolbarButton>
+                <ToolbarButton
+                  type="button"
+                  title="Heading"
+                  onClick={() => insertMarkdown("## placeholder", "placeholder")}
+                >
+                  H
+                </ToolbarButton>
+                <ToolbarButton
+                  type="button"
+                  title="Code"
+                  onClick={() => insertMarkdown("```\nplaceholder\n```", "placeholder")}
+                >
+                  {"</>"}
+                </ToolbarButton>
+                <ToolbarButton
+                  type="button"
+                  title="Link"
+                  onClick={() => insertMarkdown("[placeholder](url)", "placeholder")}
+                >
+                  <LinkIcon size={16} />
+                </ToolbarButton>
+                <ToolbarButton
+                  type="button"
+                  title="List"
+                  onClick={() => insertMarkdown("- placeholder", "placeholder")}
+                >
+                  • •
+                </ToolbarButton>
+                <ToolbarButton
+                  type="button"
+                  title="Upload Image"
+                  onClick={() => fileInputRef.current?.click()}
+                  disabled={isUploading}
+                >
+                  <Image size={16} />
+                </ToolbarButton>
+              </EditorToolbar>
 
-        <AmountInput>
-          <CurrencyDollar size={20} />
-          <input type="text" placeholder="Amount" />
-        </AmountInput>
+              <EditorContainer
+                ref={textAreaRef}
+                placeholder="Describe your issue in detail. You can use markdown for formatting."
+                value={description}
+                onChange={(e) => setDescription(e.target.value)}
+                onBlur={() => validateForm()}
+                hasError={!!errors.description}
+              />
 
-        <RepositoryInput>
-          <Link size={20} />
-          <input type="text" placeholder="Link to repository" />
-        </RepositoryInput>
+              <input
+                type="file"
+                ref={fileInputRef}
+                style={{ display: "none" }}
+                accept="image/*"
+                onChange={handleFileUpload}
+              />
 
-        <TagsInput>
-          <Tag size={20} />
-          <input type="text" placeholder="Tags" />
-        </TagsInput>
+              {isUploading ? (
+                <UploadProgress value={uploadProgress}>
+                  <div style={{ width: `${uploadProgress}%` }}></div>
+                  <span>{Math.round(uploadProgress)}% Uploading...</span>
+                </UploadProgress>
+              ) : uploadedFiles.length === 0 ? (
+                <FileUploadArea onClick={() => fileInputRef.current?.click()}>
+                  <FileArrowUp size={24} />
+                  <p>Click to upload images or drag and drop</p>
+                </FileUploadArea>
+              ) : (
+                <UploadedFilePreview>
+                  {uploadedFiles.map((file) => (
+                    <div key={file.id}>
+                      <UploadedImage src={file.url} alt={file.name} />
+                      <RemoveFileButton onClick={(e) => handleRemoveFile(file, e)}>
+                        <X size={16} />
+                      </RemoveFileButton>
+                    </div>
+                  ))}
+                </UploadedFilePreview>
+              )}
+            </>
+          ) : (
+            <PreviewContainer>
+              {description ? (
+                <div className="markdown-preview">
+                  {/* Render uploaded images directly for preview */}
+                  {uploadedFiles.length > 0 && (
+                    <div className="uploaded-images-preview">
+                      {uploadedFiles.map((file) => (
+                        <img
+                          key={file.id}
+                          src={file.url || "/placeholder.svg"}
+                          alt={file.name}
+                          style={{ maxWidth: "100%", marginBottom: "1rem" }}
+                        />
+                      ))}
+                    </div>
+                  )}
+
+                  {/* Render markdown content */}
+                  <ReactMarkdown
+                    remarkPlugins={[remarkGfm]}
+                    components={{
+                      code: CodeBlock,
+                      img: ImageRenderer,
+                    }}
+                  >
+                    {description}
+                  </ReactMarkdown>
+                </div>
+              ) : (
+                <p className="empty-preview">Your preview will appear here...</p>
+              )}
+            </PreviewContainer>
+          )}
+
+          {errors.description && <ErrorMessage>{errors.description}</ErrorMessage>}
+        </FormGroup>
+
+        <FormGroup>
+          <Label htmlFor="amount">
+            Amount to Stake
+            <Tooltip>
+              <Info size={16} />
+              <TooltipText>The amount you're willing to pay for a solution</TooltipText>
+            </Tooltip>
+            <ValidationIndicator valid={!errors.amount && amount.length > 0} />
+          </Label>
+          <AmountInput hasError={!!errors.amount}>
+            <CurrencyDollar size={20} />
+            <input
+              id="amount"
+              type="text"
+              placeholder="Amount"
+              value={amount}
+              onChange={(e) => setAmount(e.target.value)}
+              onBlur={() => validateForm()}
+            />
+          </AmountInput>
+          {errors.amount && <ErrorMessage>{errors.amount}</ErrorMessage>}
+        </FormGroup>
+
+        <FormGroup>
+          <Label htmlFor="repository">
+            Repository Link (Optional)
+            <Tooltip>
+              <Info size={16} />
+              <TooltipText>Link to a GitHub repository or code sample</TooltipText>
+            </Tooltip>
+          </Label>
+          <RepositoryInput>
+            <LinkIcon size={20} />
+            <input
+              id="repository"
+              type="text"
+              placeholder="https://github.com/username/repo"
+              value={repository}
+              onChange={(e) => setRepository(e.target.value)}
+            />
+          </RepositoryInput>
+        </FormGroup>
+
+        <FormGroup>
+          <Label htmlFor="tags">
+            Tags (Optional)
+            <Tooltip>
+              <Info size={16} />
+              <TooltipText>Add up to 5 tags to describe what your question is about</TooltipText>
+            </Tooltip>
+          </Label>
+          <TagsInput>
+            <Tag size={20} />
+            <input
+              id="tags"
+              type="text"
+              placeholder="e.g. react hooks typescript"
+              value={tags}
+              onChange={(e) => setTags(e.target.value)}
+            />
+          </TagsInput>
+        </FormGroup>
 
         <div className="buttons">
-          <NavLink to="/forum/:name">
-            <Button variant="cancel">Discard</Button>
+          <NavLink to="/forum/reactjs">
+            <Button variant="cancel" type="button">
+              Discard
+            </Button>
           </NavLink>
-          <Button variant="publish">
-            Publish <PaperPlaneRight size={20} />
+          <Button variant="publish" type="submit" disabled={transactionStatus === "processing"}>
+            {transactionStatus === "processing" ? "Publishing..." : "Publish"}
+            {transactionStatus !== "processing" && <PaperPlaneRight size={20} />}
           </Button>
         </div>
+
+        {transactionStatus !== "idle" && (
+          <TransactionStatus status={transactionStatus}>
+            {transactionStatus === "processing" && "Processing transaction..."}
+            {transactionStatus === "success" && "Question published successfully!"}
+            {transactionStatus === "error" && "Transaction failed. Please try again."}
+          </TransactionStatus>
+        )}
       </Form>
     </Container>
-  );
-}
\ No newline at end of file
+  )
+}
diff --git a/frontend/react/src/pages/NewQuestion/style.ts b/frontend/react/src/pages/NewQuestion/style.ts
index 8f32ec6..25d4bbb 100644
--- a/frontend/react/src/pages/NewQuestion/style.ts
+++ b/frontend/react/src/pages/NewQuestion/style.ts
@@ -1,113 +1,551 @@
-import styled from "styled-components";
+import styled, { css, keyframes } from "styled-components"
 
 export const Container = styled.div`
-  background: #121214;
-  color: #E1E1E6;
-  padding: 20px;
+  background: ${(props) => props.theme.cardBackground};
+  color: ${(props) => props.theme.text};
+  padding: 24px;
   border-radius: 8px;
-  width: 500px;
-  margin: auto;
+  width: 100%;
+  max-width: 800px;
+  margin: 24px auto;
+  box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
 
   h2 {
-    margin-bottom: 20px;
+    margin-bottom: 24px;
+    font-size: 1.5rem;
   }
-`;
+
+  @media (max-width: 850px) {
+    width: 90%;
+    padding: 16px;
+  }
+`
 
 export const Form = styled.form`
   display: flex;
   flex-direction: column;
-  gap: 10px;
+  gap: 20px;
 
   .buttons {
     display: flex;
     justify-content: flex-end;
-    gap: 10px;
-  }
-`;
-
-export const TitleInput = styled.input`
-  padding: 10px;
-  font-size: 16px;
-  border: 1px solid #29292E;
-  border-radius: 5px;
-  background: #202024;
-  color: #E1E1E6;
-`;
-
-export const DescriptionTextArea = styled.textarea`
-  padding: 10px;
-  font-size: 16px;
-  height: 100px;
-  border: 1px solid #29292E;
-  border-radius: 5px;
-  background: #202024;
-  color: #E1E1E6;
-  resize: none;
-`;
-
-export const AmountInput = styled.div`
+    gap: 12px;
+    margin-top: 12px;
+  }
+`
+
+export const FormGroup = styled.div`
+  display: flex;
+  flex-direction: column;
+  gap: 8px;
+`
+
+export const Label = styled.label`
   display: flex;
   align-items: center;
   gap: 8px;
-  padding: 10px;
-  border: 1px solid #29292E;
-  border-radius: 5px;
-  background: #202024;
+  font-weight: 600;
+  font-size: 0.9rem;
+  color: ${(props) => props.theme.text};
+`
 
-  input {
-    background: transparent;
-    border: none;
-    color: #E1E1E6;
+export const Tooltip = styled.div`
+  position: relative;
+  display: inline-flex;
+  align-items: center;
+  cursor: help;
+  color: ${(props) => props.theme.textSecondary};
+
+  &:hover > div {
+    visibility: visible;
+    opacity: 1;
+    transform: translateY(0);
+  }
+`
+
+export const TooltipText = styled.div`
+  visibility: hidden;
+  position: absolute;
+  z-index: 1;
+  width: 200px;
+  bottom: 125%;
+  left: 50%;
+  margin-left: -100px;
+  background-color: ${(props) => props.theme.cardBackground};
+  color: ${(props) => props.theme.text};
+  text-align: center;
+  border-radius: 6px;
+  padding: 8px;
+  opacity: 0;
+  transition: opacity 0.3s, transform 0.3s;
+  transform: translateY(10px);
+  box-shadow: 0 2px 10px rgba(0, 0, 0, 0.2);
+  font-weight: normal;
+  font-size: 0.8rem;
+  border: 1px solid ${(props) => props.theme.borderColor};
+
+  &::after {
+    content: "";
+    position: absolute;
+    top: 100%;
+    left: 50%;
+    margin-left: -5px;
+    border-width: 5px;
+    border-style: solid;
+    border-color: ${(props) => props.theme.borderColor} transparent transparent transparent;
+  }
+`
+
+export const ValidationIndicator = styled.span<{ valid: boolean }>`
+  margin-left: auto;
+  width: 16px;
+  height: 16px;
+  border-radius: 50%;
+  background-color: ${(props) => (props.valid ? "#10b981" : "transparent")};
+  transition: background-color 0.3s;
+  
+  ${(props) =>
+    props.valid &&
+    css`
+    &::after {
+      content: '✓';
+      color: white;
+      font-size: 10px;
+      display: flex;
+      justify-content: center;
+      align-items: center;
+      height: 100%;
+    }
+  `}
+`
+
+interface InputProps {
+  hasError?: boolean
+}
+
+export const TitleInput = styled.input<InputProps>`
+  padding: 12px;
+  font-size: 1rem;
+  border: 1px solid ${(props) => (props.hasError ? "#ef4444" : props.theme.borderColor)};
+  border-radius: 6px;
+  background: ${(props) => props.theme.background};
+  color: ${(props) => props.theme.text};
+  transition: border-color 0.3s;
+
+  &:focus {
     outline: none;
-    width: 100%;
+    border-color: ${(props) => (props.hasError ? "#ef4444" : "#7c3aed")};
+    box-shadow: 0 0 0 2px ${(props) => (props.hasError ? "rgba(239, 68, 68, 0.2)" : "rgba(124, 58, 237, 0.2)")};
   }
 
-  svg {
-    color: #8257E5;
+  &::placeholder {
+    color: ${(props) => props.theme.textSecondary};
+    opacity: 0.7;
+  }
+`
+
+export const TabContainer = styled.div`
+  display: flex;
+  border-bottom: 1px solid ${(props) => props.theme.borderColor};
+  margin-bottom: 8px;
+`
+
+export const Tab = styled.button<{ active: boolean }>`
+  padding: 8px 16px;
+  background: none;
+  border: none;
+  border-bottom: 2px solid ${(props) => (props.active ? "#7c3aed" : "transparent")};
+  color: ${(props) => (props.active ? props.theme.text : props.theme.textSecondary)};
+  font-weight: ${(props) => (props.active ? "600" : "400")};
+  cursor: pointer;
+  transition: all 0.2s;
+
+  &:hover {
+    color: ${(props) => props.theme.text};
+  }
+`
+
+export const EditorToolbar = styled.div`
+  display: flex;
+  gap: 4px;
+  padding: 8px;
+  background: ${(props) => props.theme.background};
+  border: 1px solid ${(props) => props.theme.borderColor};
+  border-bottom: none;
+  border-top-left-radius: 6px;
+  border-top-right-radius: 6px;
+`
+
+export const ToolbarButton = styled.button`
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  width: 32px;
+  height: 32px;
+  background: none;
+  border: 1px solid transparent;
+  border-radius: 4px;
+  color: ${(props) => props.theme.text};
+  font-weight: bold;
+  cursor: pointer;
+  transition: all 0.2s;
+
+  &:hover:not(:disabled) {
+    background: ${(props) => props.theme.borderColor};
+  }
+  
+  &:disabled {
+    opacity: 0.5;
+    cursor: not-allowed;
+  }
+`
+
+export const EditorContainer = styled.textarea<InputProps>`
+  padding: 12px;
+  font-size: 1rem;
+  min-height: 200px;
+  border: 1px solid ${(props) => (props.hasError ? "#ef4444" : props.theme.borderColor)};
+  border-radius: 0 0 6px 6px;
+  background: ${(props) => props.theme.background};
+  color: ${(props) => props.theme.text};
+  resize: vertical;
+  font-family: monospace;
+  line-height: 1.5;
+  transition: border-color 0.3s;
+
+  &:focus {
+    outline: none;
+    border-color: ${(props) => (props.hasError ? "#ef4444" : "#7c3aed")};
+    box-shadow: 0 0 0 2px ${(props) => (props.hasError ? "rgba(239, 68, 68, 0.2)" : "rgba(124, 58, 237, 0.2)")};
+  }
+
+  &::placeholder {
+    color: ${(props) => props.theme.textSecondary};
+    opacity: 0.7;
+  }
+`
+
+export const PreviewContainer = styled.div`
+  padding: 12px;
+  min-height: 200px;
+  border: 1px solid ${(props) => props.theme.borderColor};
+  border-radius: 6px;
+  background: ${(props) => props.theme.background};
+  color: ${(props) => props.theme.text};
+  overflow-y: auto;
+  line-height: 1.6;
+
+  .markdown-preview {
+    img {
+      max-width: 100%;
+      border-radius: 4px;
+      margin: 1rem 0;
+    }
+  }
+
+  .uploaded-images-preview {
+    margin-bottom: 1rem;
+    border-bottom: 1px dashed ${(props) => props.theme.borderColor};
+    padding-bottom: 1rem;
+  }
+
+  h1, h2, h3, h4, h5, h6 {
+    margin-top: 1.5em;
+    margin-bottom: 0.5em;
+  }
+
+  p {
+    margin-bottom: 1em;
+  }
+
+  ul, ol {
+    margin-bottom: 1em;
+    padding-left: 2em;
+  }
+
+  blockquote {
+    border-left: 4px solid ${(props) => props.theme.borderColor};
+    padding-left: 1em;
+    margin-left: 0;
+    color: ${(props) => props.theme.textSecondary};
+  }
+
+  code {
+    background: ${(props) => props.theme.borderColor};
+    padding: 0.2em 0.4em;
+    border-radius: 3px;
+    font-family: monospace;
+  }
+
+  pre {
+    margin-bottom: 1em;
+    overflow-x: auto;
+  }
+
+  img {
+    max-width: 100%;
+    border-radius: 4px;
+  }
+
+  a {
+    color: #7c3aed;
+    text-decoration: none;
+    
+    &:hover {
+      text-decoration: underline;
+    }
+  }
+
+  .code-block {
+    border: 1px solid ${(props) => props.theme.borderColor};
+    border-radius: 4px;
+    margin-bottom: 1rem;
+    overflow: hidden;
+  }
+
+  .code-header {
+    background: ${(props) => props.theme.borderColor};
+    padding: 0.5rem 1rem;
+    font-family: monospace;
+    font-size: 0.8rem;
+    color: ${(props) => props.theme.textSecondary};
+  }
+
+  .empty-preview {
+    color: ${(props) => props.theme.textSecondary};
+    font-style: italic;
+    display: flex;
+    justify-content: center;
+    align-items: center;
+    height: 100%;
+  }
+`
+
+export const FileUploadArea = styled.div`
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  justify-content: center;
+  padding: 24px;
+  border: 2px dashed ${(props) => props.theme.borderColor};
+  border-radius: 6px;
+  cursor: pointer;
+  margin-top: 12px;
+  transition: all 0.2s;
+
+  &:hover {
+    background: ${(props) => props.theme.background};
+    border-color: #7c3aed;
+  }
+
+  p {
+    margin-top: 8px;
+    color: ${(props) => props.theme.textSecondary};
+    font-size: 0.9rem;
+  }
+`
+
+export const UploadedFilePreview = styled.div`
+  display: flex;
+  flex-wrap: wrap;
+  gap: 12px;
+  margin-top: 12px;
+
+  > div {
+    position: relative;
+    width: 100px;
+    height: 100px;
+    border-radius: 4px;
+    overflow: hidden;
   }
-`;
+`
 
-export const RepositoryInput = styled.div`
+export const UploadedImage = styled.img`
+  width: 100%;
+  height: 100%;
+  object-fit: cover;
+`
+
+export const RemoveFileButton = styled.button`
+  position: absolute;
+  top: 4px;
+  right: 4px;
+  width: 20px;
+  height: 20px;
+  border-radius: 50%;
+  background: rgba(0, 0, 0, 0.5);
+  color: white;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  border: none;
+  cursor: pointer;
+  transition: background 0.2s;
+
+  &:hover {
+    background: rgba(0, 0, 0, 0.8);
+  }
+`
+
+export const UploadProgress = styled.div<{ value: number }>`
+  width: 100%;
+  height: 4px;
+  background: ${(props) => props.theme.borderColor};
+  border-radius: 4px;
+  margin-top: 12px;
+  position: relative;
+  overflow: hidden;
+
+  > div {
+    position: absolute;
+    height: 100%;
+    background: #7c3aed;
+    border-radius: 4px;
+    transition: width 0.3s ease-out;
+  }
+
+  > span {
+    position: absolute;
+    top: 8px;
+    left: 0;
+    width: 100%;
+    text-align: center;
+    font-size: 0.8rem;
+    color: ${(props) => props.theme.textSecondary};
+  }
+`
+
+export const AmountInput = styled.div<InputProps>`
   display: flex;
   align-items: center;
   gap: 8px;
-  padding: 10px;
-  border: 1px solid #29292E;
-  border-radius: 5px;
-  background: #202024;
+  padding: 10px 12px;
+  border: 1px solid ${(props) => (props.hasError ? "#ef4444" : props.theme.borderColor)};
+  border-radius: 6px;
+  background: ${(props) => props.theme.background};
+  transition: border-color 0.3s;
+
+  &:focus-within {
+    outline: none;
+    border-color: ${(props) => (props.hasError ? "#ef4444" : "#7c3aed")};
+    box-shadow: 0 0 0 2px ${(props) => (props.hasError ? "rgba(239, 68, 68, 0.2)" : "rgba(124, 58, 237, 0.2)")};
+  }
+
+  svg {
+    color: #7c3aed;
+  }
 
   input {
     background: transparent;
     border: none;
-    color: #E1E1E6;
+    color: ${(props) => props.theme.text};
     outline: none;
     width: 100%;
+    font-size: 1rem;
+
+    &::placeholder {
+      color: ${(props) => props.theme.textSecondary};
+      opacity: 0.7;
+    }
   }
+`
 
+export const RepositoryInput = styled(AmountInput)`
   svg {
-    color: #8257E5;
+    color: #7c3aed;
   }
-`;
+`
 
-export const TagsInput = styled(RepositoryInput)``;
+export const TagsInput = styled(AmountInput)`
+  svg {
+    color: #7c3aed;
+  }
+`
+
+export const ErrorMessage = styled.span`
+  color: #ef4444;
+  font-size: 0.8rem;
+  margin-top: -4px;
+`
 
 export const Button = styled.button<{ variant: "cancel" | "publish" }>`
   padding: 10px 16px;
-  border-radius: 5px;
-  font-size: 14px;
+  border-radius: 6px;
+  font-size: 0.9rem;
+  font-weight: 600;
   display: flex;
   align-items: center;
-  gap: 5px;
+  gap: 8px;
   border: none;
   cursor: pointer;
-  transition: 0.3s;
+  transition: 0.2s;
   
-  ${({ variant }) =>
+  ${({ variant, theme }) =>
     variant === "publish"
-      ? `background: #8257E5; color: white;`
-      : `background: #29292E; color: #E1E1E6;`}
+      ? css`
+          background: #7c3aed;
+          color: white;
+          
+          &:hover:not(:disabled) {
+            background: #6d28d9;
+          }
+          
+          &:disabled {
+            opacity: 0.6;
+            cursor: not-allowed;
+          }
+        `
+      : css`
+          background: ${theme.borderColor};
+          color: ${theme.text};
+          
+          &:hover {
+            background: ${theme.textSecondary};
+            color: ${theme.background};
+          }
+        `}
+`
 
-  &:hover {
-    opacity: 0.8;
+const statusAnimation = keyframes`
+  0% {
+    opacity: 0;
+    transform: translateY(-10px);
   }
-`;
+  100% {
+    opacity: 1;
+    transform: translateY(0);
+  }
+`
+
+export const TransactionStatus = styled.div<{ status: "idle" | "processing" | "success" | "error" }>`
+  padding: 12px;
+  border-radius: 6px;
+  margin-top: 16px;
+  text-align: center;
+  animation: ${statusAnimation} 0.3s ease-out;
+  
+  ${({ status }) => {
+    switch (status) {
+      case "processing":
+        return css`
+          background-color: rgba(59, 130, 246, 0.1);
+          color: #3b82f6;
+          border: 1px solid rgba(59, 130, 246, 0.2);
+        `
+      case "success":
+        return css`
+          background-color: rgba(16, 185, 129, 0.1);
+          color: #10b981;
+          border: 1px solid rgba(16, 185, 129, 0.2);
+        `
+      case "error":
+        return css`
+          background-color: rgba(239, 68, 68, 0.1);
+          color: #ef4444;
+          border: 1px solid rgba(239, 68, 68, 0.2);
+        `
+      default:
+        return ""
+    }
+  }}
+`
diff --git a/frontend/react/src/services/file-upload.ts b/frontend/react/src/services/file-upload.ts
new file mode 100644
index 0000000..7a52382
--- /dev/null
+++ b/frontend/react/src/services/file-upload.ts
@@ -0,0 +1,73 @@
+/**
+ * File upload service
+ * In a production environment, this would upload files to a server or cloud storage
+ * For this demo, we'll simulate a server upload with a promise
+ */
+
+// Remove the unused constant or comment it out
+// const UPLOAD_ENDPOINT = "https://api.starkoverflow.example/uploads"
+
+export interface UploadedFile {
+    id: string
+    url: string
+    name: string
+    type: string
+    size: number
+  }
+  
+  /**
+   * Upload a file to the server
+   * @param file The file to upload
+   * @returns Promise that resolves with the uploaded file details
+   */
+  export async function uploadFile(file: File): Promise<UploadedFile> {
+    // In a real implementation, you would use FormData and fetch:
+    // const formData = new FormData();
+    // formData.append('file', file);
+    // const response = await fetch('/api/uploads', {
+    //   method: 'POST',
+    //   body: formData,
+    // });
+    // return await response.json();
+  
+    // For demo purposes, simulate a network request with a delay
+    return new Promise((resolve) => {
+      setTimeout(() => {
+        // Generate a unique ID for the file
+        const fileId = `file_${Math.random().toString(36).substring(2, 15)}`
+  
+        // Create a temporary object URL for the file
+        // In production, this would be a URL from your storage service (S3, etc)
+        const objectUrl = URL.createObjectURL(file)
+  
+        resolve({
+          id: fileId,
+          url: objectUrl,
+          name: file.name,
+          type: file.type,
+          size: file.size,
+        })
+      }, 1000) // Simulate network delay
+    })
+  }
+  
+  /**
+   * Delete a file from the server
+   * @param fileId The ID of the file to delete
+   * @returns Promise that resolves when the file is deleted
+   */
+  export async function deleteFile(fileId: string): Promise<void> {
+    // In a real implementation, you would use fetch:
+    // await fetch(`${UPLOAD_ENDPOINT}/${fileId}`, {
+    //   method: 'DELETE',
+    // });
+  
+    // For demo purposes, simulate a network request with a delay
+    return new Promise((resolve) => {
+      setTimeout(() => {
+        console.log(`File ${fileId} deleted`)
+        resolve()
+      }, 500)
+    })
+  }
+  
\ No newline at end of file
diff --git a/frontend/react/src/styles/themes/dark.tsx b/frontend/react/src/styles/themes/dark.tsx
index b3099d2..651ccc6 100644
--- a/frontend/react/src/styles/themes/dark.tsx
+++ b/frontend/react/src/styles/themes/dark.tsx
@@ -3,13 +3,14 @@ export const darkTheme = {
   text: "#e4e4e4",
   cardBackground: "#1e1e1e",
   borderColor: "#333333",
-  connectBg: "#e4e4e4", 
+  connectBg: "#e4e4e4",
   connectText: "#121212",
+  textSecondary: "#71717a",
   forum: {
     topicCard: {
       background: "#1e1e1e",
       text: "#e4e4e4",
-      meta: "#b3b3b3"
-    }
-  }
-};
\ No newline at end of file
+      meta: "#b3b3b3",
+    },
+  },
+}
diff --git a/frontend/react/src/styles/themes/index.tsx b/frontend/react/src/styles/themes/index.tsx
index d23cfa6..2d372e2 100644
--- a/frontend/react/src/styles/themes/index.tsx
+++ b/frontend/react/src/styles/themes/index.tsx
@@ -1,10 +1,14 @@
-import { lightTheme } from "./light";
-import { darkTheme } from "./dark";
+import { darkTheme } from "./dark"
+import { lightTheme } from "./light"
+
+// Extend the default theme to include textSecondary
+export type Theme = typeof defaultTheme
 
 const defaultTheme = {
-  ...darkTheme
-};
+  ...darkTheme,
+  textSecondary: "#71717a",
+}
 
-export { defaultTheme, lightTheme, darkTheme };
+export { defaultTheme, lightTheme, darkTheme }
 
-export default defaultTheme;
\ No newline at end of file
+export default defaultTheme
diff --git a/frontend/react/src/styles/themes/light.tsx b/frontend/react/src/styles/themes/light.tsx
index 677b7c3..1a40b71 100644
--- a/frontend/react/src/styles/themes/light.tsx
+++ b/frontend/react/src/styles/themes/light.tsx
@@ -3,13 +3,14 @@ export const lightTheme = {
   text: "#212529",
   cardBackground: "#ffffff",
   borderColor: "#dee2e6",
-  connectBg: "#121212", 
+  connectBg: "#121212",
   connectText: "#e4e4e4",
+  textSecondary: "#6c757d",
   forum: {
     topicCard: {
       background: "#e3e0e0",
       text: "#212529",
-      meta: "#949393"
-    }
-  }
-};
+      meta: "#949393",
+    },
+  },
+}


Reviews:
user: login: MullerEsposito
state: COMMENTED
author_association: OWNER
submitted_at: 2025-04-26T22:18:07Z
user: login: MullerEsposito
state: COMMENTED
author_association: OWNER
submitted_at: 2025-04-26T22:20:34Z
user: login: MullerEsposito
state: COMMENTED
author_association: OWNER
submitted_at: 2025-04-26T22:23:20Z
user: login: MullerEsposito
state: COMMENTED
author_association: OWNER
submitted_at: 2025-04-26T22:26:04Z
user: login: MullerEsposito
body: The UI/UX result it's great, but it could be made a better use of the react componentization. The NewQuestion page has become too large, but we can refactor it in the future splitting it in smaller components.

Thanks for the effort and the contribution. 🚀 
state: APPROVED
author_association: OWNER
submitted_at: 2025-04-26T22:34:50Z

Review comments:
diff_hunk: @@ -1,39 +1,550 @@
-import { PaperPlaneRight, Link, Tag, CurrencyDollar } from "phosphor-react";
-import { Container, Form, TitleInput, DescriptionTextArea, RepositoryInput, TagsInput, Button, AmountInput } from "./style";
-import { NavLink } from "react-router-dom";
+"use client"
+
+import type React from "react"
+import { useState, useRef } from "react"
+import { PaperPlaneRight, Link as LinkIcon, Tag, CurrencyDollar, Info, X, Image, FileArrowUp } from "phosphor-react"
+import {
+  Container,
+  Form,
+  TitleInput,
+  EditorContainer,
+  RepositoryInput,
+  TagsInput,
+  Button,
+  AmountInput,
+  ErrorMessage,
+  ToolbarButton,
+  EditorToolbar,
+  PreviewContainer,
+  TabContainer,
+  Tab,
+  FileUploadArea,
+  UploadedFilePreview,
+  UploadedImage,
+  RemoveFileButton,
+  FormGroup,
+  Label,
+  Tooltip,
+  TooltipText,
+  ValidationIndicator,
+  TransactionStatus,
+  UploadProgress,
+} from "./style"
+import { NavLink, useNavigate } from "react-router-dom"
+import { uploadFile, deleteFile, type UploadedFile } from "../../services/file-upload"
+import { useAccount } from "@starknet-react/core"
+import { useWallet } from "../../providers/wallet-connect-context"
+
+// Import dynamic components for markdown rendering
+const ReactMarkdown = await import("react-markdown").then((mod) => mod.default || mod)
+const remarkGfm = await import("remark-gfm").then((mod) => mod.default || mod)
+
+// Define proper types for the code component props
+interface CodeProps {
+  className?: string
+  inline?: boolean
+  children?: React.ReactNode
+}
+
+// Define proper types for the image component props
+interface ImageProps {
+  src?: string
+  alt?: string
+}
+
+// Custom code component for syntax highlighting
+const CodeBlock: React.FC<CodeProps> = ({ inline, className, children, ...props }) => {
+  const match = /language-(\w+)/.exec(className || "")
+  const language = match ? match[1] : ""
+
+  if (!inline && match) {
+    return (
+      <div className="code-block">
+        <div className="code-header">
+          <span>{language}</span>
+        </div>
+        <pre className={className} {...props}>
+          <code className={className} {...props}>
+            {children}
+          </code>
+        </pre>
+      </div>
+    )
+  }
+
+  return (
+    <code className={className} {...props}>
+      {children}
+    </code>
+  )
+}
 
 export function NewQuestion() {
+  const [title, setTitle] = useState("")
+  const [description, setDescription] = useState("")
+  const [amount, setAmount] = useState("")
+  const [repository, setRepository] = useState("")
+  const [tags, setTags] = useState("")
+  const [activeTab, setActiveTab] = useState<"write" | "preview">("write")
+  const [errors, setErrors] = useState<Record<string, string>>({})
+  const [uploadedFiles, setUploadedFiles] = useState<UploadedFile[]>([])
+  const [isUploading, setIsUploading] = useState(false)
+  const [uploadProgress, setUploadProgress] = useState(0)
+  const [transactionStatus, setTransactionStatus] = useState<"idle" | "processing" | "success" | "error">("idle")
+  const fileInputRef = useRef<HTMLInputElement>(null)
+  const textAreaRef = useRef<HTMLTextAreaElement>(null)
+  const navigate = useNavigate()
+  const { isConnected } = useAccount()
+  const { openConnectModal } = useWallet()
+
+  // Create a map of image URLs to their blob URLs for the preview
+  const [imageMap, setImageMap] = useState<Record<string, string>>({})
+
+  // Form validation
+  const validateForm = () => {
+    const newErrors: Record<string, string> = {}
+
+    if (!title.trim()) {
+      newErrors.title = "Title is required"
+    } else if (title.length < 10) {
+      newErrors.title = "Title should be at least 10 characters"
+    }
+
+    if (!description.trim()) {
+      newErrors.description = "Description is required"
+    } else if (description.length < 30) {
+      newErrors.description = "Description should be at least 30 characters"
+    }
+
+    if (!amount.trim()) {
+      newErrors.amount = "Amount is required"
+    } else if (isNaN(Number(amount)) || Number(amount) <= 0) {
+      newErrors.amount = "Amount must be a positive number"
+    }
+
+    setErrors(newErrors)
+    return Object.keys(newErrors).length === 0
+  }
+
+  // Handle file upload
+  const handleFileUpload = async (e: React.ChangeEvent<HTMLInputElement>) => {
+    if (e.target.files && e.target.files.length > 0) {
+      const file = e.target.files[0]
+
+      // Check if it's an image
+      if (file.type.startsWith("image/")) {
+        setIsUploading(true)
+        setUploadProgress(0)
+
+        // Simulate upload progress
+        const progressInterval = setInterval(() => {
+          setUploadProgress((prev) => {
+            const newProgress = prev + Math.random() * 20
+            return newProgress >= 90 ? 90 : newProgress
+          })
+        }, 200)
+
+        try {
+          // Upload the file
+          const uploadedFile = await uploadFile(file)
+
+          // Clear the interval and set progress to 100%
+          clearInterval(progressInterval)
+          setUploadProgress(100)
+
+          // Add to uploaded files
+          setUploadedFiles((prev) => [...prev, uploadedFile])
+
+          // Add image markdown to description
+          const imageMarkdown = `![${file.name}](${uploadedFile.url})\n`
+          setDescription((prev) => prev + imageMarkdown)
+
+          // Update the image map for preview
+          setImageMap((prev) => ({
+            ...prev,
+            [uploadedFile.url]: uploadedFile.url,
+          }))
+
+          // Reset upload state after a delay
+          setTimeout(() => {
+            setIsUploading(false)
+            setUploadProgress(0)
+          }, 500)
+        } catch (error) {
+          console.error("File upload failed:", error)
+          clearInterval(progressInterval)
+          setIsUploading(false)
+          setUploadProgress(0)
+        }
+      }
+    }
+
+    // Reset file input
+    if (fileInputRef.current) {
+      fileInputRef.current.value = ""
+    }
+  }
+
+  // Remove uploaded file
+  const handleRemoveFile = async (file: UploadedFile, e: React.MouseEvent) => {
+    // Prevent event propagation to avoid form submission
+    e.preventDefault()
+    e.stopPropagation()
+
+    try {
+      // Delete the file
+      await deleteFile(file.id)
+
+      // Remove from state
+      setUploadedFiles((prev) => prev.filter((f) => f.id !== file.id))
+
+      // Remove from description
+      const imageMarkdown = `![${file.name}](${file.url})`
+      setDescription((prev) => prev.replace(imageMarkdown, ""))
+
+      // Remove from image map
+      const newImageMap = { ...imageMap }
+      delete newImageMap[file.url]
+      setImageMap(newImageMap)
+    } catch (error) {
+      console.error("File deletion failed:", error)
+    }
+  }
+
+  // Handle markdown toolbar actions
+  const insertMarkdown = (markdownSyntax: string, placeholder = "") => {
+    if (!textAreaRef.current) return
+
+    const textarea = textAreaRef.current
+    const start = textarea.selectionStart
+    const end = textarea.selectionEnd
+    const selectedText = description.substring(start, end)
+
+    let newText = ""
+    if (selectedText) {
+      // If text is selected, wrap it with markdown syntax
+      newText =
+        description.substring(0, start) + markdownSyntax.replace(placeholder, selectedText) + description.substring(end)
+    } else {
+      // If no text is selected, just insert the markdown syntax with placeholder
+      newText = description.substring(0, start) + markdownSyntax + description.substring(end)
+    }
+
+    setDescription(newText)
+
+    // Focus back on textarea and set cursor position
+    setTimeout(() => {
+      textarea.focus()
+      const newCursorPos = start + markdownSyntax.indexOf(placeholder) + (selectedText ? selectedText.length : 0)
+      textarea.setSelectionRange(newCursorPos, newCursorPos)
+    }, 0)
+  }
+
+  // Handle tab switching
+  const handleTabChange = (tab: "write" | "preview") => {
+    setActiveTab(tab)
+  }
+
+  // Handle form submission
+  const handleSubmit = async (e: React.FormEvent) => {
+    e.preventDefault()
+
+    if (!validateForm()) {
+      return
+    }
+
+    if (!isConnected) {
+      openConnectModal()
+      return
+    }
+
+    try {
+      setTransactionStatus("processing")
+
+      // Simulate blockchain transaction
+      await new Promise((resolve) => setTimeout(resolve, 2000))
+
+      // Success
+      setTransactionStatus("success")
+
+      // Redirect after successful submission
+      setTimeout(() => {
+        navigate("/forum/reactjs")
+      }, 1500)
+    } catch (error) {
+      console.error("Transaction error:", error)
+      setTransactionStatus("error")
+    }
+  }
+
+  // Custom image renderer that uses our image map
+  const ImageRenderer = ({ src, alt }: ImageProps) => {
+    if (!src) return null
+
+    // Use the actual blob URL from our map
+    return <img src={src || "/placeholder.svg"} alt={alt || ""} style={{ maxWidth: "100%" }} />
+  }
+
   return (
     <Container>
       <h2>Create Question</h2>
-      <Form>
-        <TitleInput type="text" placeholder="Title" />
-        <DescriptionTextArea placeholder="Describe your issue..." />
+      <Form onSubmit={handleSubmit}>
+        <FormGroup>
+          <Label htmlFor="title">
+            Title
+            <Tooltip>
+              <Info size={16} />
+              <TooltipText>Be specific and imagine you're asking a question to another person</TooltipText>
+            </Tooltip>
+            <ValidationIndicator valid={!errors.title && title.length > 0} />
+          </Label>
+          <TitleInput
+            id="title"
+            type="text"
+            placeholder="e.g. How to implement a custom hook in React?"
+            value={title}
+            onChange={(e) => setTitle(e.target.value)}
+            onBlur={() => validateForm()}
+            hasError={!!errors.title}
+          />
+          {errors.title && <ErrorMessage>{errors.title}</ErrorMessage>}
+        </FormGroup>
path: frontend/react/src/pages/NewQuestion/index.tsx
user: login: MullerEsposito
body: That snippet could be a subcomponent of NewQuestion, something like InputForm. It could be reusable for Title, Amount, Repository and Tags.


created_at: 2025-04-26T22:18:07Z
author_association: OWNER
diff_hunk: @@ -1,39 +1,550 @@
-import { PaperPlaneRight, Link, Tag, CurrencyDollar } from "phosphor-react";
-import { Container, Form, TitleInput, DescriptionTextArea, RepositoryInput, TagsInput, Button, AmountInput } from "./style";
-import { NavLink } from "react-router-dom";
+"use client"
+
+import type React from "react"
+import { useState, useRef } from "react"
+import { PaperPlaneRight, Link as LinkIcon, Tag, CurrencyDollar, Info, X, Image, FileArrowUp } from "phosphor-react"
+import {
+  Container,
+  Form,
+  TitleInput,
+  EditorContainer,
+  RepositoryInput,
+  TagsInput,
+  Button,
+  AmountInput,
+  ErrorMessage,
+  ToolbarButton,
+  EditorToolbar,
+  PreviewContainer,
+  TabContainer,
+  Tab,
+  FileUploadArea,
+  UploadedFilePreview,
+  UploadedImage,
+  RemoveFileButton,
+  FormGroup,
+  Label,
+  Tooltip,
+  TooltipText,
+  ValidationIndicator,
+  TransactionStatus,
+  UploadProgress,
+} from "./style"
+import { NavLink, useNavigate } from "react-router-dom"
+import { uploadFile, deleteFile, type UploadedFile } from "../../services/file-upload"
+import { useAccount } from "@starknet-react/core"
+import { useWallet } from "../../providers/wallet-connect-context"
+
+// Import dynamic components for markdown rendering
+const ReactMarkdown = await import("react-markdown").then((mod) => mod.default || mod)
+const remarkGfm = await import("remark-gfm").then((mod) => mod.default || mod)
+
+// Define proper types for the code component props
+interface CodeProps {
+  className?: string
+  inline?: boolean
+  children?: React.ReactNode
+}
+
+// Define proper types for the image component props
+interface ImageProps {
+  src?: string
+  alt?: string
+}
+
+// Custom code component for syntax highlighting
+const CodeBlock: React.FC<CodeProps> = ({ inline, className, children, ...props }) => {
+  const match = /language-(\w+)/.exec(className || "")
+  const language = match ? match[1] : ""
+
+  if (!inline && match) {
+    return (
+      <div className="code-block">
+        <div className="code-header">
+          <span>{language}</span>
+        </div>
+        <pre className={className} {...props}>
+          <code className={className} {...props}>
+            {children}
+          </code>
+        </pre>
+      </div>
+    )
+  }
+
+  return (
+    <code className={className} {...props}>
+      {children}
+    </code>
+  )
+}
 
 export function NewQuestion() {
+  const [title, setTitle] = useState("")
+  const [description, setDescription] = useState("")
+  const [amount, setAmount] = useState("")
+  const [repository, setRepository] = useState("")
+  const [tags, setTags] = useState("")
+  const [activeTab, setActiveTab] = useState<"write" | "preview">("write")
+  const [errors, setErrors] = useState<Record<string, string>>({})
+  const [uploadedFiles, setUploadedFiles] = useState<UploadedFile[]>([])
+  const [isUploading, setIsUploading] = useState(false)
+  const [uploadProgress, setUploadProgress] = useState(0)
+  const [transactionStatus, setTransactionStatus] = useState<"idle" | "processing" | "success" | "error">("idle")
+  const fileInputRef = useRef<HTMLInputElement>(null)
+  const textAreaRef = useRef<HTMLTextAreaElement>(null)
+  const navigate = useNavigate()
+  const { isConnected } = useAccount()
+  const { openConnectModal } = useWallet()
+
+  // Create a map of image URLs to their blob URLs for the preview
+  const [imageMap, setImageMap] = useState<Record<string, string>>({})
+
+  // Form validation
+  const validateForm = () => {
+    const newErrors: Record<string, string> = {}
+
+    if (!title.trim()) {
+      newErrors.title = "Title is required"
+    } else if (title.length < 10) {
+      newErrors.title = "Title should be at least 10 characters"
+    }
+
+    if (!description.trim()) {
+      newErrors.description = "Description is required"
+    } else if (description.length < 30) {
+      newErrors.description = "Description should be at least 30 characters"
+    }
+
+    if (!amount.trim()) {
+      newErrors.amount = "Amount is required"
+    } else if (isNaN(Number(amount)) || Number(amount) <= 0) {
+      newErrors.amount = "Amount must be a positive number"
+    }
+
+    setErrors(newErrors)
+    return Object.keys(newErrors).length === 0
+  }
+
+  // Handle file upload
+  const handleFileUpload = async (e: React.ChangeEvent<HTMLInputElement>) => {
+    if (e.target.files && e.target.files.length > 0) {
+      const file = e.target.files[0]
+
+      // Check if it's an image
+      if (file.type.startsWith("image/")) {
+        setIsUploading(true)
+        setUploadProgress(0)
+
+        // Simulate upload progress
+        const progressInterval = setInterval(() => {
+          setUploadProgress((prev) => {
+            const newProgress = prev + Math.random() * 20
+            return newProgress >= 90 ? 90 : newProgress
+          })
+        }, 200)
+
+        try {
+          // Upload the file
+          const uploadedFile = await uploadFile(file)
+
+          // Clear the interval and set progress to 100%
+          clearInterval(progressInterval)
+          setUploadProgress(100)
+
+          // Add to uploaded files
+          setUploadedFiles((prev) => [...prev, uploadedFile])
+
+          // Add image markdown to description
+          const imageMarkdown = `![${file.name}](${uploadedFile.url})\n`
+          setDescription((prev) => prev + imageMarkdown)
+
+          // Update the image map for preview
+          setImageMap((prev) => ({
+            ...prev,
+            [uploadedFile.url]: uploadedFile.url,
+          }))
+
+          // Reset upload state after a delay
+          setTimeout(() => {
+            setIsUploading(false)
+            setUploadProgress(0)
+          }, 500)
+        } catch (error) {
+          console.error("File upload failed:", error)
+          clearInterval(progressInterval)
+          setIsUploading(false)
+          setUploadProgress(0)
+        }
+      }
+    }
+
+    // Reset file input
+    if (fileInputRef.current) {
+      fileInputRef.current.value = ""
+    }
+  }
+
+  // Remove uploaded file
+  const handleRemoveFile = async (file: UploadedFile, e: React.MouseEvent) => {
+    // Prevent event propagation to avoid form submission
+    e.preventDefault()
+    e.stopPropagation()
+
+    try {
+      // Delete the file
+      await deleteFile(file.id)
+
+      // Remove from state
+      setUploadedFiles((prev) => prev.filter((f) => f.id !== file.id))
+
+      // Remove from description
+      const imageMarkdown = `![${file.name}](${file.url})`
+      setDescription((prev) => prev.replace(imageMarkdown, ""))
+
+      // Remove from image map
+      const newImageMap = { ...imageMap }
+      delete newImageMap[file.url]
+      setImageMap(newImageMap)
+    } catch (error) {
+      console.error("File deletion failed:", error)
+    }
+  }
+
+  // Handle markdown toolbar actions
+  const insertMarkdown = (markdownSyntax: string, placeholder = "") => {
+    if (!textAreaRef.current) return
+
+    const textarea = textAreaRef.current
+    const start = textarea.selectionStart
+    const end = textarea.selectionEnd
+    const selectedText = description.substring(start, end)
+
+    let newText = ""
+    if (selectedText) {
+      // If text is selected, wrap it with markdown syntax
+      newText =
+        description.substring(0, start) + markdownSyntax.replace(placeholder, selectedText) + description.substring(end)
+    } else {
+      // If no text is selected, just insert the markdown syntax with placeholder
+      newText = description.substring(0, start) + markdownSyntax + description.substring(end)
+    }
+
+    setDescription(newText)
+
+    // Focus back on textarea and set cursor position
+    setTimeout(() => {
+      textarea.focus()
+      const newCursorPos = start + markdownSyntax.indexOf(placeholder) + (selectedText ? selectedText.length : 0)
+      textarea.setSelectionRange(newCursorPos, newCursorPos)
+    }, 0)
+  }
+
+  // Handle tab switching
+  const handleTabChange = (tab: "write" | "preview") => {
+    setActiveTab(tab)
+  }
+
+  // Handle form submission
+  const handleSubmit = async (e: React.FormEvent) => {
+    e.preventDefault()
+
+    if (!validateForm()) {
+      return
+    }
+
+    if (!isConnected) {
+      openConnectModal()
+      return
+    }
+
+    try {
+      setTransactionStatus("processing")
+
+      // Simulate blockchain transaction
+      await new Promise((resolve) => setTimeout(resolve, 2000))
+
+      // Success
+      setTransactionStatus("success")
+
+      // Redirect after successful submission
+      setTimeout(() => {
+        navigate("/forum/reactjs")
+      }, 1500)
+    } catch (error) {
+      console.error("Transaction error:", error)
+      setTransactionStatus("error")
+    }
+  }
+
+  // Custom image renderer that uses our image map
+  const ImageRenderer = ({ src, alt }: ImageProps) => {
+    if (!src) return null
+
+    // Use the actual blob URL from our map
+    return <img src={src || "/placeholder.svg"} alt={alt || ""} style={{ maxWidth: "100%" }} />
+  }
+
   return (
     <Container>
       <h2>Create Question</h2>
-      <Form>
-        <TitleInput type="text" placeholder="Title" />
-        <DescriptionTextArea placeholder="Describe your issue..." />
+      <Form onSubmit={handleSubmit}>
+        <FormGroup>
+          <Label htmlFor="title">
+            Title
+            <Tooltip>
+              <Info size={16} />
+              <TooltipText>Be specific and imagine you're asking a question to another person</TooltipText>
+            </Tooltip>
+            <ValidationIndicator valid={!errors.title && title.length > 0} />
+          </Label>
+          <TitleInput
+            id="title"
+            type="text"
+            placeholder="e.g. How to implement a custom hook in React?"
+            value={title}
+            onChange={(e) => setTitle(e.target.value)}
+            onBlur={() => validateForm()}
+            hasError={!!errors.title}
+          />
+          {errors.title && <ErrorMessage>{errors.title}</ErrorMessage>}
+        </FormGroup>
+
+        <FormGroup>
+          <Label htmlFor="description">
+            Description
+            <Tooltip>
+              <Info size={16} />
+              <TooltipText>Include all the information someone would need to answer your question</TooltipText>
+            </Tooltip>
+            <ValidationIndicator valid={!errors.description && description.length > 0} />
+          </Label>
+
+          <TabContainer>
+            <Tab type="button" active={activeTab === "write"} onClick={() => handleTabChange("write")}>
+              Write
+            </Tab>
+            <Tab type="button" active={activeTab === "preview"} onClick={() => handleTabChange("preview")}>
+              Preview
+            </Tab>
+          </TabContainer>
+
+          {activeTab === "write" ? (
+            <>
+              <EditorToolbar>
+                <ToolbarButton
+                  type="button"
+                  title="Bold"
+                  onClick={() => insertMarkdown("**placeholder**", "placeholder")}
+                >
+                  B
+                </ToolbarButton>
+                <ToolbarButton
+                  type="button"
+                  title="Italic"
+                  onClick={() => insertMarkdown("*placeholder*", "placeholder")}
+                >
+                  I
+                </ToolbarButton>
+                <ToolbarButton
+                  type="button"
+                  title="Heading"
+                  onClick={() => insertMarkdown("## placeholder", "placeholder")}
+                >
+                  H
+                </ToolbarButton>
+                <ToolbarButton
+                  type="button"
+                  title="Code"
+                  onClick={() => insertMarkdown("```\nplaceholder\n```", "placeholder")}
+                >
+                  {"</>"}
+                </ToolbarButton>
+                <ToolbarButton
+                  type="button"
+                  title="Link"
+                  onClick={() => insertMarkdown("[placeholder](url)", "placeholder")}
+                >
+                  <LinkIcon size={16} />
+                </ToolbarButton>
+                <ToolbarButton
+                  type="button"
+                  title="List"
+                  onClick={() => insertMarkdown("- placeholder", "placeholder")}
+                >
+                  • •
+                </ToolbarButton>
+                <ToolbarButton
+                  type="button"
+                  title="Upload Image"
+                  onClick={() => fileInputRef.current?.click()}
+                  disabled={isUploading}
+                >
+                  <Image size={16} />
+                </ToolbarButton>
+              </EditorToolbar>
 
-        <AmountInput>
-          <CurrencyDollar size={20} />
-          <input type="text" placeholder="Amount" />
-        </AmountInput>
+              <EditorContainer
+                ref={textAreaRef}
+                placeholder="Describe your issue in detail. You can use markdown for formatting."
+                value={description}
+                onChange={(e) => setDescription(e.target.value)}
+                onBlur={() => validateForm()}
+                hasError={!!errors.description}
+              />
 
-        <RepositoryInput>
-          <Link size={20} />
-          <input type="text" placeholder="Link to repository" />
-        </RepositoryInput>
+              <input
+                type="file"
+                ref={fileInputRef}
+                style={{ display: "none" }}
+                accept="image/*"
+                onChange={handleFileUpload}
+              />
 
-        <TagsInput>
-          <Tag size={20} />
-          <input type="text" placeholder="Tags" />
-        </TagsInput>
+              {isUploading ? (
+                <UploadProgress value={uploadProgress}>
+                  <div style={{ width: `${uploadProgress}%` }}></div>
+                  <span>{Math.round(uploadProgress)}% Uploading...</span>
+                </UploadProgress>
+              ) : uploadedFiles.length === 0 ? (
+                <FileUploadArea onClick={() => fileInputRef.current?.click()}>
+                  <FileArrowUp size={24} />
+                  <p>Click to upload images or drag and drop</p>
+                </FileUploadArea>
+              ) : (
+                <UploadedFilePreview>
+                  {uploadedFiles.map((file) => (
+                    <div key={file.id}>
+                      <UploadedImage src={file.url} alt={file.name} />
+                      <RemoveFileButton onClick={(e) => handleRemoveFile(file, e)}>
+                        <X size={16} />
+                      </RemoveFileButton>
+                    </div>
+                  ))}
+                </UploadedFilePreview>
+              )}
+            </>
+          ) : (
+            <PreviewContainer>
+              {description ? (
+                <div className="markdown-preview">
+                  {/* Render uploaded images directly for preview */}
+                  {uploadedFiles.length > 0 && (
+                    <div className="uploaded-images-preview">
+                      {uploadedFiles.map((file) => (
+                        <img
+                          key={file.id}
+                          src={file.url || "/placeholder.svg"}
+                          alt={file.name}
+                          style={{ maxWidth: "100%", marginBottom: "1rem" }}
+                        />
+                      ))}
+                    </div>
+                  )}
+
+                  {/* Render markdown content */}
+                  <ReactMarkdown
+                    remarkPlugins={[remarkGfm]}
+                    components={{
+                      code: CodeBlock,
+                      img: ImageRenderer,
+                    }}
+                  >
+                    {description}
+                  </ReactMarkdown>
+                </div>
+              ) : (
+                <p className="empty-preview">Your preview will appear here...</p>
+              )}
+            </PreviewContainer>
+          )}
+
+          {errors.description && <ErrorMessage>{errors.description}</ErrorMessage>}
+        </FormGroup>
path: frontend/react/src/pages/NewQuestion/index.tsx
user: login: MullerEsposito
body: That snippet could be a subcomponent of NewQuestion, something like EditorForm. It would be useful to turn the NewQuestion page cleaner.
created_at: 2025-04-26T22:20:34Z
author_association: OWNER
diff_hunk: @@ -1,39 +1,550 @@
-import { PaperPlaneRight, Link, Tag, CurrencyDollar } from "phosphor-react";
-import { Container, Form, TitleInput, DescriptionTextArea, RepositoryInput, TagsInput, Button, AmountInput } from "./style";
-import { NavLink } from "react-router-dom";
+"use client"
+
+import type React from "react"
+import { useState, useRef } from "react"
+import { PaperPlaneRight, Link as LinkIcon, Tag, CurrencyDollar, Info, X, Image, FileArrowUp } from "phosphor-react"
+import {
+  Container,
+  Form,
+  TitleInput,
+  EditorContainer,
+  RepositoryInput,
+  TagsInput,
+  Button,
+  AmountInput,
+  ErrorMessage,
+  ToolbarButton,
+  EditorToolbar,
+  PreviewContainer,
+  TabContainer,
+  Tab,
+  FileUploadArea,
+  UploadedFilePreview,
+  UploadedImage,
+  RemoveFileButton,
+  FormGroup,
+  Label,
+  Tooltip,
+  TooltipText,
+  ValidationIndicator,
+  TransactionStatus,
+  UploadProgress,
+} from "./style"
+import { NavLink, useNavigate } from "react-router-dom"
+import { uploadFile, deleteFile, type UploadedFile } from "../../services/file-upload"
+import { useAccount } from "@starknet-react/core"
+import { useWallet } from "../../providers/wallet-connect-context"
+
+// Import dynamic components for markdown rendering
+const ReactMarkdown = await import("react-markdown").then((mod) => mod.default || mod)
+const remarkGfm = await import("remark-gfm").then((mod) => mod.default || mod)
+
+// Define proper types for the code component props
+interface CodeProps {
+  className?: string
+  inline?: boolean
+  children?: React.ReactNode
+}
+
+// Define proper types for the image component props
+interface ImageProps {
+  src?: string
+  alt?: string
+}
+
+// Custom code component for syntax highlighting
+const CodeBlock: React.FC<CodeProps> = ({ inline, className, children, ...props }) => {
+  const match = /language-(\w+)/.exec(className || "")
+  const language = match ? match[1] : ""
+
+  if (!inline && match) {
+    return (
+      <div className="code-block">
+        <div className="code-header">
+          <span>{language}</span>
+        </div>
+        <pre className={className} {...props}>
+          <code className={className} {...props}>
+            {children}
+          </code>
+        </pre>
+      </div>
+    )
+  }
+
+  return (
+    <code className={className} {...props}>
+      {children}
+    </code>
+  )
+}
 
 export function NewQuestion() {
+  const [title, setTitle] = useState("")
+  const [description, setDescription] = useState("")
+  const [amount, setAmount] = useState("")
+  const [repository, setRepository] = useState("")
+  const [tags, setTags] = useState("")
+  const [activeTab, setActiveTab] = useState<"write" | "preview">("write")
+  const [errors, setErrors] = useState<Record<string, string>>({})
+  const [uploadedFiles, setUploadedFiles] = useState<UploadedFile[]>([])
+  const [isUploading, setIsUploading] = useState(false)
+  const [uploadProgress, setUploadProgress] = useState(0)
+  const [transactionStatus, setTransactionStatus] = useState<"idle" | "processing" | "success" | "error">("idle")
+  const fileInputRef = useRef<HTMLInputElement>(null)
+  const textAreaRef = useRef<HTMLTextAreaElement>(null)
+  const navigate = useNavigate()
+  const { isConnected } = useAccount()
+  const { openConnectModal } = useWallet()
+
+  // Create a map of image URLs to their blob URLs for the preview
+  const [imageMap, setImageMap] = useState<Record<string, string>>({})
+
+  // Form validation
+  const validateForm = () => {
+    const newErrors: Record<string, string> = {}
+
+    if (!title.trim()) {
+      newErrors.title = "Title is required"
+    } else if (title.length < 10) {
+      newErrors.title = "Title should be at least 10 characters"
+    }
+
+    if (!description.trim()) {
+      newErrors.description = "Description is required"
+    } else if (description.length < 30) {
+      newErrors.description = "Description should be at least 30 characters"
+    }
+
+    if (!amount.trim()) {
+      newErrors.amount = "Amount is required"
+    } else if (isNaN(Number(amount)) || Number(amount) <= 0) {
+      newErrors.amount = "Amount must be a positive number"
+    }
+
+    setErrors(newErrors)
+    return Object.keys(newErrors).length === 0
+  }
+
+  // Handle file upload
+  const handleFileUpload = async (e: React.ChangeEvent<HTMLInputElement>) => {
+    if (e.target.files && e.target.files.length > 0) {
+      const file = e.target.files[0]
+
+      // Check if it's an image
+      if (file.type.startsWith("image/")) {
+        setIsUploading(true)
+        setUploadProgress(0)
+
+        // Simulate upload progress
+        const progressInterval = setInterval(() => {
+          setUploadProgress((prev) => {
+            const newProgress = prev + Math.random() * 20
+            return newProgress >= 90 ? 90 : newProgress
+          })
+        }, 200)
+
+        try {
+          // Upload the file
+          const uploadedFile = await uploadFile(file)
+
+          // Clear the interval and set progress to 100%
+          clearInterval(progressInterval)
+          setUploadProgress(100)
+
+          // Add to uploaded files
+          setUploadedFiles((prev) => [...prev, uploadedFile])
+
+          // Add image markdown to description
+          const imageMarkdown = `![${file.name}](${uploadedFile.url})\n`
+          setDescription((prev) => prev + imageMarkdown)
+
+          // Update the image map for preview
+          setImageMap((prev) => ({
+            ...prev,
+            [uploadedFile.url]: uploadedFile.url,
+          }))
+
+          // Reset upload state after a delay
+          setTimeout(() => {
+            setIsUploading(false)
+            setUploadProgress(0)
+          }, 500)
+        } catch (error) {
+          console.error("File upload failed:", error)
+          clearInterval(progressInterval)
+          setIsUploading(false)
+          setUploadProgress(0)
+        }
+      }
+    }
+
+    // Reset file input
+    if (fileInputRef.current) {
+      fileInputRef.current.value = ""
+    }
+  }
+
+  // Remove uploaded file
+  const handleRemoveFile = async (file: UploadedFile, e: React.MouseEvent) => {
+    // Prevent event propagation to avoid form submission
+    e.preventDefault()
+    e.stopPropagation()
+
+    try {
+      // Delete the file
+      await deleteFile(file.id)
+
+      // Remove from state
+      setUploadedFiles((prev) => prev.filter((f) => f.id !== file.id))
+
+      // Remove from description
+      const imageMarkdown = `![${file.name}](${file.url})`
+      setDescription((prev) => prev.replace(imageMarkdown, ""))
+
+      // Remove from image map
+      const newImageMap = { ...imageMap }
+      delete newImageMap[file.url]
+      setImageMap(newImageMap)
+    } catch (error) {
+      console.error("File deletion failed:", error)
+    }
+  }
+
+  // Handle markdown toolbar actions
+  const insertMarkdown = (markdownSyntax: string, placeholder = "") => {
+    if (!textAreaRef.current) return
+
+    const textarea = textAreaRef.current
+    const start = textarea.selectionStart
+    const end = textarea.selectionEnd
+    const selectedText = description.substring(start, end)
+
+    let newText = ""
+    if (selectedText) {
+      // If text is selected, wrap it with markdown syntax
+      newText =
+        description.substring(0, start) + markdownSyntax.replace(placeholder, selectedText) + description.substring(end)
+    } else {
+      // If no text is selected, just insert the markdown syntax with placeholder
+      newText = description.substring(0, start) + markdownSyntax + description.substring(end)
+    }
+
+    setDescription(newText)
+
+    // Focus back on textarea and set cursor position
+    setTimeout(() => {
+      textarea.focus()
+      const newCursorPos = start + markdownSyntax.indexOf(placeholder) + (selectedText ? selectedText.length : 0)
+      textarea.setSelectionRange(newCursorPos, newCursorPos)
+    }, 0)
+  }
+
+  // Handle tab switching
+  const handleTabChange = (tab: "write" | "preview") => {
+    setActiveTab(tab)
+  }
+
+  // Handle form submission
+  const handleSubmit = async (e: React.FormEvent) => {
+    e.preventDefault()
+
+    if (!validateForm()) {
+      return
+    }
+
+    if (!isConnected) {
+      openConnectModal()
+      return
+    }
+
+    try {
+      setTransactionStatus("processing")
+
+      // Simulate blockchain transaction
+      await new Promise((resolve) => setTimeout(resolve, 2000))
+
+      // Success
+      setTransactionStatus("success")
+
+      // Redirect after successful submission
+      setTimeout(() => {
+        navigate("/forum/reactjs")
+      }, 1500)
+    } catch (error) {
+      console.error("Transaction error:", error)
+      setTransactionStatus("error")
+    }
+  }
+
+  // Custom image renderer that uses our image map
+  const ImageRenderer = ({ src, alt }: ImageProps) => {
+    if (!src) return null
+
+    // Use the actual blob URL from our map
+    return <img src={src || "/placeholder.svg"} alt={alt || ""} style={{ maxWidth: "100%" }} />
+  }
+
   return (
     <Container>
       <h2>Create Question</h2>
-      <Form>
-        <TitleInput type="text" placeholder="Title" />
-        <DescriptionTextArea placeholder="Describe your issue..." />
+      <Form onSubmit={handleSubmit}>
+        <FormGroup>
+          <Label htmlFor="title">
+            Title
+            <Tooltip>
+              <Info size={16} />
+              <TooltipText>Be specific and imagine you're asking a question to another person</TooltipText>
+            </Tooltip>
+            <ValidationIndicator valid={!errors.title && title.length > 0} />
+          </Label>
path: frontend/react/src/pages/NewQuestion/index.tsx
user: login: MullerEsposito
body: Label could be an independent component. It could be reusable in InputForm, EditorForm and may be in another ones.
created_at: 2025-04-26T22:23:20Z
author_association: OWNER
diff_hunk: @@ -1,39 +1,550 @@
-import { PaperPlaneRight, Link, Tag, CurrencyDollar } from "phosphor-react";
-import { Container, Form, TitleInput, DescriptionTextArea, RepositoryInput, TagsInput, Button, AmountInput } from "./style";
-import { NavLink } from "react-router-dom";
+"use client"
+
+import type React from "react"
+import { useState, useRef } from "react"
+import { PaperPlaneRight, Link as LinkIcon, Tag, CurrencyDollar, Info, X, Image, FileArrowUp } from "phosphor-react"
+import {
+  Container,
+  Form,
+  TitleInput,
+  EditorContainer,
+  RepositoryInput,
+  TagsInput,
+  Button,
+  AmountInput,
+  ErrorMessage,
+  ToolbarButton,
+  EditorToolbar,
+  PreviewContainer,
+  TabContainer,
+  Tab,
+  FileUploadArea,
+  UploadedFilePreview,
+  UploadedImage,
+  RemoveFileButton,
+  FormGroup,
+  Label,
+  Tooltip,
+  TooltipText,
+  ValidationIndicator,
+  TransactionStatus,
+  UploadProgress,
+} from "./style"
+import { NavLink, useNavigate } from "react-router-dom"
+import { uploadFile, deleteFile, type UploadedFile } from "../../services/file-upload"
+import { useAccount } from "@starknet-react/core"
+import { useWallet } from "../../providers/wallet-connect-context"
+
+// Import dynamic components for markdown rendering
+const ReactMarkdown = await import("react-markdown").then((mod) => mod.default || mod)
+const remarkGfm = await import("remark-gfm").then((mod) => mod.default || mod)
+
+// Define proper types for the code component props
+interface CodeProps {
+  className?: string
+  inline?: boolean
+  children?: React.ReactNode
+}
+
+// Define proper types for the image component props
+interface ImageProps {
+  src?: string
+  alt?: string
+}
+
+// Custom code component for syntax highlighting
+const CodeBlock: React.FC<CodeProps> = ({ inline, className, children, ...props }) => {
+  const match = /language-(\w+)/.exec(className || "")
+  const language = match ? match[1] : ""
+
+  if (!inline && match) {
+    return (
+      <div className="code-block">
+        <div className="code-header">
+          <span>{language}</span>
+        </div>
+        <pre className={className} {...props}>
+          <code className={className} {...props}>
+            {children}
+          </code>
+        </pre>
+      </div>
+    )
+  }
+
+  return (
+    <code className={className} {...props}>
+      {children}
+    </code>
+  )
+}
 
 export function NewQuestion() {
+  const [title, setTitle] = useState("")
+  const [description, setDescription] = useState("")
+  const [amount, setAmount] = useState("")
+  const [repository, setRepository] = useState("")
+  const [tags, setTags] = useState("")
+  const [activeTab, setActiveTab] = useState<"write" | "preview">("write")
+  const [errors, setErrors] = useState<Record<string, string>>({})
+  const [uploadedFiles, setUploadedFiles] = useState<UploadedFile[]>([])
+  const [isUploading, setIsUploading] = useState(false)
+  const [uploadProgress, setUploadProgress] = useState(0)
+  const [transactionStatus, setTransactionStatus] = useState<"idle" | "processing" | "success" | "error">("idle")
+  const fileInputRef = useRef<HTMLInputElement>(null)
+  const textAreaRef = useRef<HTMLTextAreaElement>(null)
+  const navigate = useNavigate()
+  const { isConnected } = useAccount()
+  const { openConnectModal } = useWallet()
+
+  // Create a map of image URLs to their blob URLs for the preview
+  const [imageMap, setImageMap] = useState<Record<string, string>>({})
+
+  // Form validation
+  const validateForm = () => {
+    const newErrors: Record<string, string> = {}
+
+    if (!title.trim()) {
+      newErrors.title = "Title is required"
+    } else if (title.length < 10) {
+      newErrors.title = "Title should be at least 10 characters"
+    }
+
+    if (!description.trim()) {
+      newErrors.description = "Description is required"
+    } else if (description.length < 30) {
+      newErrors.description = "Description should be at least 30 characters"
+    }
+
+    if (!amount.trim()) {
+      newErrors.amount = "Amount is required"
+    } else if (isNaN(Number(amount)) || Number(amount) <= 0) {
+      newErrors.amount = "Amount must be a positive number"
+    }
+
+    setErrors(newErrors)
+    return Object.keys(newErrors).length === 0
+  }
path: frontend/react/src/pages/NewQuestion/index.tsx
user: login: MullerEsposito
body: That's a good simple validation, but we can think in use libraries like zod in the future for it.
created_at: 2025-04-26T22:26:04Z
author_association: OWNER

PR comments:
user: login: vercel[bot]
created_at: 2025-04-24T19:27:06Z
body: [vc]: #gAlzHUdEp1G1bxPs3hzP6T0CoUVnHd1e+sQyGm+t4Bw=:eyJpc01vbm9yZXBvIjp0cnVlLCJ0eXBlIjoiZ2l0aHViIiwicHJvamVjdHMiOlt7Im5hbWUiOiJzdGFyay1vdmVyZmxvdyIsInJvb3REaXJlY3RvcnkiOiJmcm9udGVuZC9sYW5kaW5nIiwiaW5zcGVjdG9yVXJsIjoiaHR0cHM6Ly92ZXJjZWwuY29tL211bGxlcmVzcG9zaXRvcy1wcm9qZWN0cy9zdGFyay1vdmVyZmxvdy83SksxeTdYcGJoNnNWd0FYTTJ4RkUyUER5YXRYIiwicHJldmlld1VybCI6InN0YXJrLW92ZXJmbG93LWdpdC1mb3JrLWNvZGVha3MtZTZjNWM0LW11bGxlcmVzcG9zaXRvcy1wcm9qZWN0cy52ZXJjZWwuYXBwIiwibmV4dENvbW1pdFN0YXR1cyI6IkRFUExPWUVEIiwibGl2ZUZlZWRiYWNrIjp7InJlc29sdmVkIjowLCJ1bnJlc29sdmVkIjowLCJ0b3RhbCI6MCwibGluayI6InN0YXJrLW92ZXJmbG93LWdpdC1mb3JrLWNvZGVha3MtZTZjNWM0LW11bGxlcmVzcG9zaXRvcy1wcm9qZWN0cy52ZXJjZWwuYXBwIn19XX0=
**The latest updates on your projects**. Learn more about [Vercel for Git ↗︎](https://vercel.link/github-learn-more)

| Name | Status | Preview | Comments | Updated (UTC) |
| :--- | :----- | :------ | :------- | :------ |
| **stark-overflow** | ✅ Ready ([Inspect](https://vercel.com/mullerespositos-projects/stark-overflow/7JK1y7Xpbh6sVwAXM2xFE2PDyatX)) | [Visit Preview](https://stark-overflow-git-fork-codeaks-e6c5c4-mullerespositos-projects.vercel.app) | 💬 [**Add feedback**](https://vercel.live/open-feedback/stark-overflow-git-fork-codeaks-e6c5c4-mullerespositos-projects.vercel.app?via=pr-comment-feedback-link) | Apr 24, 2025 7:27pm |


author_association: NONE
user: login: codeAKstan
created_at: 2025-04-27T06:40:44Z
body: > The UI/UX result it's great, but it could be made a better use of the react componentization. The NewQuestion page has become too large, but we can refactor it in the future splitting it in smaller components.
> 
> Thanks for the effort and the contribution. 🚀

I will always break my codes into smaller components. Thank you
author_association: CONTRIBUTOR