diff --git a/package-lock.json b/package-lock.json
index d9957c1..42aeab0 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -29,6 +29,7 @@
"nanostores": "^1.0.1",
"react": "^19.2.0",
"react-dom": "^19.2.0",
+ "react-resizable-panels": "^3.0.6",
"tailwindcss": "^4.1.16"
},
"devDependencies": {
@@ -5803,6 +5804,16 @@
"node": ">=0.10.0"
}
},
+ "node_modules/react-resizable-panels": {
+ "version": "3.0.6",
+ "resolved": "https://registry.npmjs.org/react-resizable-panels/-/react-resizable-panels-3.0.6.tgz",
+ "integrity": "sha512-b3qKHQ3MLqOgSS+FRYKapNkJZf5EQzuf6+RLiq1/IlTHw99YrZ2NJZLk4hQIzTnnIkRg2LUqyVinu6YWWpUYew==",
+ "license": "MIT",
+ "peerDependencies": {
+ "react": "^16.14.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc",
+ "react-dom": "^16.14.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc"
+ }
+ },
"node_modules/readdirp": {
"version": "4.1.2",
"license": "MIT",
diff --git a/package.json b/package.json
index b41ddeb..7a46a8a 100644
--- a/package.json
+++ b/package.json
@@ -34,7 +34,10 @@
"nanostores": "^1.0.1",
"react": "^19.2.0",
"react-dom": "^19.2.0",
- "tailwindcss": "^4.1.16"
+ "react-resizable-panels": "^3.0.6",
+ "sharp": "^0.34.4",
+ "tailwindcss": "^4.1.16",
+ "zustand": "^5.0.8"
},
"devDependencies": {
"@biomejs/biome": "2.3.2",
diff --git a/src/components/shared/editor/CodeEditor.tsx b/src/components/shared/editor/CodeEditor.tsx
deleted file mode 100644
index 7a4105e..0000000
--- a/src/components/shared/editor/CodeEditor.tsx
+++ /dev/null
@@ -1,23 +0,0 @@
-import CodeMirror from "@uiw/react-codemirror"
-import { useCallback, useState } from "react"
-import { rust } from "~/helpers/config"
-import { gruvbox } from "~/helpers/theme"
-
-export function CodeEditor() {
- const [value, setValue] = useState('fn main() {\n println!("Hola, mundo!");\n}\n')
-
- const onChange = useCallback((val: string) => {
- setValue(val)
- }, [])
-
- return (
-
- )
-}
diff --git a/src/components/shared/sections/Section.astro b/src/components/shared/sections/Section.astro
deleted file mode 100644
index 9e04bb3..0000000
--- a/src/components/shared/sections/Section.astro
+++ /dev/null
@@ -1,13 +0,0 @@
----
-interface Props {
- id?: string
- className?: string
- style?: string
-}
-
-const { id, className, style } = Astro.props as Props
----
-
-
diff --git a/src/content.config.ts b/src/content.config.ts
deleted file mode 100644
index 7e8e7d1..0000000
--- a/src/content.config.ts
+++ /dev/null
@@ -1,33 +0,0 @@
-import { defineCollection, getCollection, z } from "astro:content"
-import { glob } from "astro/loaders"
-
-const content = defineCollection({
- loader: glob({ pattern: "*.{md,mdx}", base: "./src/content" }),
- schema: z.object({
- title: z.string().optional(),
- description: z.string().optional(),
- order: z.number(),
- nextPath: z.string().optional(),
- previousPath: z.string().optional(),
- editor: z.boolean().optional(),
- }),
-})
-
-/**
- * Obtiene todo el contenido de la colección "content", lo ordena por el campo 'order'
- * @returns id (número inicial) y slug (nombre) de cada entrada.
- */
-export const allContent = await getCollection("content").then((entries) => {
- return entries
- .sort((a, b) => a.data.order - b.data.order)
- .map((entry) => {
- const [, id, slug] = entry.id.match(/^(\d+)(.+)$/) || []
- return { ...entry, id, slug }
- })
-})
-/**
- * Exporta las colecciones definidas para Astro.
- *
- * ! ADVERTENCIA: Esta es una variable interna de Astro y no debe ser utilizada directamente en el código de la aplicación.
- */
-export const collections = { content }
diff --git a/src/features/content/components/NavButtons.astro b/src/features/content/components/NavButtons.astro
deleted file mode 100644
index d6379a9..0000000
--- a/src/features/content/components/NavButtons.astro
+++ /dev/null
@@ -1,53 +0,0 @@
----
-import { IconArrowBigLeftLinesFilled, IconArrowBigRightLinesFilled } from "@tabler/icons-react"
-
-const { ...pagination } = Astro.props
----
-
-
-
diff --git a/src/features/content/enums/OutputVariant.enum.ts b/src/features/content/enums/OutputVariant.enum.ts
new file mode 100644
index 0000000..ad7714b
--- /dev/null
+++ b/src/features/content/enums/OutputVariant.enum.ts
@@ -0,0 +1,4 @@
+export enum OutputVariant {
+ Stdout = "stdout",
+ Stderr = "stderr",
+}
diff --git a/src/features/content/enums/PanelVariant.enum.ts b/src/features/content/enums/PanelVariant.enum.ts
new file mode 100644
index 0000000..94fe5f3
--- /dev/null
+++ b/src/features/content/enums/PanelVariant.enum.ts
@@ -0,0 +1,4 @@
+export enum PanelVariant {
+ ContentToEditor = "content-playground",
+ EditorToTerminal = "editor-terminal",
+}
diff --git a/src/features/content/hooks/usePanelContainerBreakpoints.ts b/src/features/content/hooks/usePanelContainerBreakpoints.ts
new file mode 100644
index 0000000..1030817
--- /dev/null
+++ b/src/features/content/hooks/usePanelContainerBreakpoints.ts
@@ -0,0 +1,14 @@
+import { useMemo } from "react"
+
+export default function usePanelContainerBreakpoints(width: number) {
+ const size = useMemo(() => {
+ if (width >= 1440) return 4
+ if (width >= 1024) return 5
+ if (width >= 768) return 8
+ if (width >= 640) return 10
+ if (width >= 480) return 14
+ return 18
+ }, [width])
+
+ return { size }
+}
diff --git a/src/features/content/react/ReactCodeEditor.tsx b/src/features/content/react/ReactCodeEditor.tsx
new file mode 100644
index 0000000..f7885da
--- /dev/null
+++ b/src/features/content/react/ReactCodeEditor.tsx
@@ -0,0 +1,29 @@
+import CodeMirror from "@uiw/react-codemirror"
+import { useCallback } from "react"
+import { useRustCompilerStore } from "~/features/content/stores/useRustCompilerStore.ts"
+import { rust } from "~/helpers/config"
+import { gruvbox } from "~/helpers/theme"
+
+export default function ReactCodeEditor() {
+ const code = useRustCompilerStore((state) => state.code)
+ const setCode = useRustCompilerStore((state) => state.setCode)
+
+ const onChange = useCallback(
+ (val: string) => {
+ setCode(val)
+ },
+ [setCode],
+ )
+
+ return (
+
+ )
+}
diff --git a/src/features/content/react/ReactCollapsiblePanel.tsx b/src/features/content/react/ReactCollapsiblePanel.tsx
new file mode 100644
index 0000000..b37cf3a
--- /dev/null
+++ b/src/features/content/react/ReactCollapsiblePanel.tsx
@@ -0,0 +1,31 @@
+import type { ReactNode } from "react"
+
+interface CollapsiblePanelProps {
+ icon: ReactNode
+ title: string
+ children: ReactNode
+ isCollapsed?: boolean
+}
+
+export default function ReactCollapsiblePanel({ icon, title, children, isCollapsed = false }: CollapsiblePanelProps) {
+ if (isCollapsed) {
+ return (
+
+ )
+ }
+
+ return (
+
+
+ {icon}
+ {title}
+
+ {children}
+
+ )
+}
diff --git a/src/features/content/react/ReactExecuteRust.tsx b/src/features/content/react/ReactExecuteRust.tsx
new file mode 100644
index 0000000..f815f56
--- /dev/null
+++ b/src/features/content/react/ReactExecuteRust.tsx
@@ -0,0 +1,16 @@
+import { IconPlayerPlay } from "@tabler/icons-react"
+import { useRustCompilerStore } from "~/features/content/stores/useRustCompilerStore.ts"
+
+export default function ReactExecuteRust() {
+ const execute = useRustCompilerStore((state) => state.execute)
+ return (
+
+ )
+}
diff --git a/src/features/content/react/ReactMarkdownContainer.tsx b/src/features/content/react/ReactMarkdownContainer.tsx
new file mode 100644
index 0000000..8c11f54
--- /dev/null
+++ b/src/features/content/react/ReactMarkdownContainer.tsx
@@ -0,0 +1,21 @@
+import { IconLicense } from "@tabler/icons-react"
+import type { ReactNode } from "react"
+import { PanelVariant } from "~/features/content/enums/PanelVariant.enum.ts"
+import ReactCollapsiblePanel from "~/features/content/react/ReactCollapsiblePanel.tsx"
+import { usePanelStore } from "~/features/content/stores/Panel.store.ts"
+
+interface ReactMarkdownContainerProps {
+ children: ReactNode
+}
+
+export default function ReactMarkdownContainer({ children }: ReactMarkdownContainerProps) {
+ const isCollapsed = usePanelStore((state) => state.isCollapsed(`${PanelVariant.ContentToEditor}-primary`))
+
+ return (
+ } title="Contenido">
+
+ {children}
+
+
+ )
+}
diff --git a/src/features/content/react/ReactPanelContainer.tsx b/src/features/content/react/ReactPanelContainer.tsx
new file mode 100644
index 0000000..ef32413
--- /dev/null
+++ b/src/features/content/react/ReactPanelContainer.tsx
@@ -0,0 +1,81 @@
+import { IconGripVertical } from "@tabler/icons-react"
+import { type ReactNode, useEffect, useMemo, useRef } from "react"
+import { type ImperativePanelHandle, Panel, PanelGroup, PanelResizeHandle } from "react-resizable-panels"
+import { PanelVariant } from "~/features/content/enums/PanelVariant.enum.ts"
+import usePanelContainerBreakpoints from "~/features/content/hooks/usePanelContainerBreakpoints.ts"
+import { usePanelStore } from "~/features/content/stores/Panel.store.ts"
+import useWindowSize from "~/features/shared/hooks/useWindowSize.ts"
+
+interface PanelContainerProps {
+ direction: "horizontal" | "vertical"
+ defaultLayout: [number, number]
+ first?: ReactNode
+ second?: ReactNode
+ variant: PanelVariant
+}
+
+export default function ReactPanelContainer({
+ first,
+ second,
+ direction = "horizontal",
+ defaultLayout = [50, 50],
+ variant,
+}: PanelContainerProps) {
+ const { width } = useWindowSize()
+ const { size } = usePanelContainerBreakpoints(width)
+ const offset = useMemo(() => 4, [])
+
+ const registerPanel = usePanelStore((state) => state.registerPanel)
+ const unregisterPanel = usePanelStore((state) => state.unregisterPanel)
+ const setCollapsed = usePanelStore((state) => state.setCollapsed)
+
+ const primaryId = `${variant}-primary`
+ const secondaryId = `${variant}-secondary`
+
+ const primaryPanelRef = useRef(null)
+ const secondaryPanelRef = useRef(null)
+
+ useEffect(() => {
+ if (primaryPanelRef.current) registerPanel(primaryId, primaryPanelRef.current)
+ if (secondaryPanelRef.current) registerPanel(secondaryId, secondaryPanelRef.current)
+
+ return () => {
+ unregisterPanel(primaryId)
+ unregisterPanel(secondaryId)
+ }
+ }, [registerPanel, unregisterPanel, primaryId, secondaryId])
+
+ return (
+
+ setCollapsed(primaryId, true)}
+ onExpand={() => setCollapsed(primaryId, false)}
+ >
+ {first}
+
+
+
+
+
+
+ setCollapsed(secondaryId, true)}
+ onExpand={() => setCollapsed(secondaryId, false)}
+ >
+ {second}
+
+
+ )
+}
diff --git a/src/features/content/react/ReactPanelHeader.tsx b/src/features/content/react/ReactPanelHeader.tsx
new file mode 100644
index 0000000..570db9a
--- /dev/null
+++ b/src/features/content/react/ReactPanelHeader.tsx
@@ -0,0 +1,15 @@
+import type { ReactNode } from "react"
+
+interface ReactPanelHeaderProps {
+ icon: ReactNode
+ title: string
+}
+
+export default function ReactPanelHeader({ icon, title }: ReactPanelHeaderProps) {
+ return (
+
+ {icon}
+ {title}
+
+ )
+}
diff --git a/src/features/content/react/ReactPlayground.tsx b/src/features/content/react/ReactPlayground.tsx
new file mode 100644
index 0000000..df0b971
--- /dev/null
+++ b/src/features/content/react/ReactPlayground.tsx
@@ -0,0 +1,26 @@
+import { IconCode } from "@tabler/icons-react"
+import { PanelVariant } from "~/features/content/enums/PanelVariant.enum.ts"
+import ReactCodeEditor from "~/features/content/react/ReactCodeEditor.tsx"
+import ReactCollapsiblePanel from "~/features/content/react/ReactCollapsiblePanel.tsx"
+import ReactPanelContainer from "~/features/content/react/ReactPanelContainer.tsx"
+import ReactTerminal from "~/features/content/react/ReactTerminal.tsx"
+import { usePanelStore } from "~/features/content/stores/Panel.store.ts"
+
+export default function ReactPlayground() {
+ const isCollapsed = usePanelStore((state) => state.isCollapsed(`${PanelVariant.ContentToEditor}-secondary`))
+ return (
+ } title="Editor" isCollapsed={isCollapsed}>
+ }
+ first={
+
+
+
+ }
+ />
+
+ )
+}
diff --git a/src/features/content/react/ReactTerminal.tsx b/src/features/content/react/ReactTerminal.tsx
new file mode 100644
index 0000000..065a9d4
--- /dev/null
+++ b/src/features/content/react/ReactTerminal.tsx
@@ -0,0 +1,16 @@
+import { useRustCompilerStore } from "~/features/content/stores/useRustCompilerStore.ts"
+import TerminalHeader from "./terminal/TerminalHeader"
+import TerminalOutput from "./terminal/TerminalOutput"
+
+export default function ReactTerminal() {
+ const isExecuting = useRustCompilerStore((state) => state.isExecuting)
+ const liveOutput = useRustCompilerStore((state) => state.liveOutput)
+ const result = useRustCompilerStore((state) => state.result)
+
+ return (
+
+
+
+
+ )
+}
diff --git a/src/features/content/react/terminal/CompilationStatus.tsx b/src/features/content/react/terminal/CompilationStatus.tsx
new file mode 100644
index 0000000..254fabc
--- /dev/null
+++ b/src/features/content/react/terminal/CompilationStatus.tsx
@@ -0,0 +1,11 @@
+interface CompilationStatusProps {
+ success: boolean
+}
+
+export default function CompilationStatus({ success }: CompilationStatusProps) {
+ return (
+
+ {success ? "✓ Compilado exitosamente" : "✗ Error de compilación"}
+
+ )
+}
diff --git a/src/features/content/react/terminal/ExecutingIndicator.tsx b/src/features/content/react/terminal/ExecutingIndicator.tsx
new file mode 100644
index 0000000..63c814c
--- /dev/null
+++ b/src/features/content/react/terminal/ExecutingIndicator.tsx
@@ -0,0 +1,9 @@
+import { IconLoader2 } from "@tabler/icons-react"
+
+export default function ExecutingIndicator() {
+ return (
+
+ $ cargo run
+
+ )
+}
diff --git a/src/features/content/react/terminal/OutputLine.tsx b/src/features/content/react/terminal/OutputLine.tsx
new file mode 100644
index 0000000..7ed159a
--- /dev/null
+++ b/src/features/content/react/terminal/OutputLine.tsx
@@ -0,0 +1,27 @@
+import { OutputVariant } from "~/features/content/enums/OutputVariant.enum.ts"
+
+interface OutputLineProps {
+ type: "stdout" | "stderr"
+ content: string
+}
+
+export default function OutputLine({ type, content }: OutputLineProps) {
+ if (type === OutputVariant.Stdout) {
+ return (
+
+ {">>> "}
+ {content}
+
+ )
+ }
+
+ if (type === OutputVariant.Stderr) {
+ return (
+
+ {content}
+
+ )
+ }
+
+ return null
+}
diff --git a/src/features/content/react/terminal/TerminalHeader.tsx b/src/features/content/react/terminal/TerminalHeader.tsx
new file mode 100644
index 0000000..2f0dd64
--- /dev/null
+++ b/src/features/content/react/terminal/TerminalHeader.tsx
@@ -0,0 +1,9 @@
+import TerminalTab from "./TerminalTab"
+
+export default function TerminalHeader() {
+ return (
+
+
+
+ )
+}
diff --git a/src/features/content/react/terminal/TerminalOutput.tsx b/src/features/content/react/terminal/TerminalOutput.tsx
new file mode 100644
index 0000000..9b971eb
--- /dev/null
+++ b/src/features/content/react/terminal/TerminalOutput.tsx
@@ -0,0 +1,28 @@
+import CompilationStatus from "./CompilationStatus"
+import ExecutingIndicator from "./ExecutingIndicator"
+import OutputLine from "./OutputLine"
+
+interface TerminalOutputProps {
+ isExecuting: boolean
+ liveOutput: {
+ stdout?: string
+ stderr?: string
+ }
+ result?: {
+ success: boolean
+ } | null
+}
+
+export default function TerminalOutput({ isExecuting, liveOutput, result }: TerminalOutputProps) {
+ return (
+
+ {isExecuting && }
+
+ {liveOutput.stdout && }
+
+ {liveOutput.stderr && }
+
+ {result && !isExecuting && }
+
+ )
+}
diff --git a/src/features/content/react/terminal/TerminalTab.tsx b/src/features/content/react/terminal/TerminalTab.tsx
new file mode 100644
index 0000000..508a3db
--- /dev/null
+++ b/src/features/content/react/terminal/TerminalTab.tsx
@@ -0,0 +1,13 @@
+import { IconTerminal } from "@tabler/icons-react"
+
+export default function TerminalTab() {
+ return (
+
+ )
+}
diff --git a/src/features/content/server/compiler.ts b/src/features/content/server/compiler.ts
new file mode 100644
index 0000000..de3ab68
--- /dev/null
+++ b/src/features/content/server/compiler.ts
@@ -0,0 +1,66 @@
+const RUST_PLAYGROUND_API = "https://play.rust-lang.org/execute"
+
+export interface ExecuteOptions {
+ channel?: "stable" | "beta" | "nightly"
+ mode?: "debug" | "release"
+ edition?: "2015" | "2018" | "2021" | "2024"
+}
+
+export interface ExecutionResult {
+ success: boolean
+ stdout: string
+ stderr: string
+}
+
+type MessageHandler = (data: string) => void
+
+class RustPlayground {
+ async execute(
+ code: string,
+ options: ExecuteOptions = {},
+ onStdout?: MessageHandler,
+ onStderr?: MessageHandler,
+ ): Promise {
+ try {
+ const response = await fetch(RUST_PLAYGROUND_API, {
+ method: "POST",
+ headers: {
+ "Content-Type": "application/json",
+ },
+ body: JSON.stringify({
+ code,
+ channel: options.channel || "stable",
+ mode: options.mode || "debug",
+ edition: options.edition || "2021",
+ crateType: "bin",
+ tests: false,
+ backtrace: false,
+ }),
+ })
+
+ if (!response.ok) {
+ throw new Error(`HTTP error! status: ${response.status}`)
+ }
+
+ const result = await response.json()
+
+ if (onStdout && result.stdout) {
+ onStdout(result.stdout)
+ }
+ if (onStderr && result.stderr) {
+ onStderr(result.stderr)
+ }
+
+ return {
+ success: result.success,
+ stdout: result.stdout || "",
+ stderr: result.stderr || "",
+ }
+ } catch (error) {
+ console.error("Error al ejecutar código:", error)
+ throw error
+ }
+ }
+}
+
+export const rustPlayground = new RustPlayground()
diff --git a/src/features/content/server/websocket.ts b/src/features/content/server/websocket.ts
new file mode 100644
index 0000000..c46505d
--- /dev/null
+++ b/src/features/content/server/websocket.ts
@@ -0,0 +1,106 @@
+const RUST_LANG_WS = "wss://play.rust-lang.org/websocket"
+
+let sequenceNumber = 0
+
+export interface ExecuteOptions {
+ channel?: "stable" | "beta" | "nightly"
+ mode?: "debug" | "release"
+ edition?: "2015" | "2018" | "2021" | "2024"
+}
+
+export interface ExecutionResult {
+ success: boolean
+ stdout: string
+ stderr: string
+}
+
+type MessageHandler = (data: string) => void
+
+class RustPlayground {
+ private ws: WebSocket | null = null
+ private ready = false
+ private execution: {
+ stdout: string
+ stderr: string
+ resolve: (result: ExecutionResult) => void
+ onStdout?: MessageHandler
+ onStderr?: MessageHandler
+ } | null = null
+
+ async connect(): Promise {
+ if (this.ready) return
+
+ return new Promise((resolve) => {
+ this.ws = new WebSocket(RUST_LANG_WS)
+
+ this.ws.onopen = () => {
+ this.ws!.send(
+ JSON.stringify({
+ type: "websocket/connected",
+ payload: { iAcceptThisIsAnUnsupportedApi: true },
+ meta: { websocket: true, sequenceNumber: ++sequenceNumber },
+ }),
+ )
+ this.ready = true
+ resolve()
+ }
+
+ this.ws.onmessage = (event) => {
+ if (!this.execution) return
+
+ const { type, payload } = JSON.parse(event.data)
+
+ if (type === "output/execute/wsExecuteStdout") {
+ this.execution.stdout += payload
+ this.execution.onStdout?.(payload)
+ } else if (type === "output/execute/wsExecuteStderr") {
+ this.execution.stderr += payload
+ this.execution.onStderr?.(payload)
+ } else if (type === "output/execute/wsExecuteEnd") {
+ this.execution.resolve({
+ success: payload.success,
+ stdout: this.execution.stdout,
+ stderr: this.execution.stderr,
+ })
+ this.execution = null
+ }
+ }
+
+ this.ws.onclose = () => {
+ this.ready = false
+ this.ws = null
+ }
+ })
+ }
+
+ async execute(
+ code: string,
+ options: ExecuteOptions,
+ onStdout?: MessageHandler,
+ onStderr?: MessageHandler,
+ ): Promise {
+ await this.connect()
+
+ return new Promise((resolve) => {
+ this.execution = { stdout: "", stderr: "", resolve, onStdout, onStderr }
+
+ this.ws!.send(
+ JSON.stringify({
+ type: "output/execute/wsExecuteRequest",
+ payload: {
+ channel: options.channel,
+ mode: options.mode,
+ edition: options.edition,
+ crateType: "bin",
+ tests: false,
+ code,
+ backtrace: false,
+ },
+ meta: { websocket: true, sequenceNumber: ++sequenceNumber },
+ }),
+ )
+ })
+ }
+}
+
+export const rustPlayground = new RustPlayground()
diff --git a/src/features/content/stores/Panel.store.ts b/src/features/content/stores/Panel.store.ts
new file mode 100644
index 0000000..539f234
--- /dev/null
+++ b/src/features/content/stores/Panel.store.ts
@@ -0,0 +1,46 @@
+import type { ImperativePanelHandle } from "react-resizable-panels"
+import { create } from "zustand/react"
+
+interface PanelState {
+ panels: Map
+ collapsedPanels: Set
+ registerPanel: (id: string, panel: ImperativePanelHandle) => void
+ unregisterPanel: (id: string) => void
+ getPanel: (id: string) => ImperativePanelHandle | null
+ setCollapsed: (id: string, isCollapsed: boolean) => void
+ isCollapsed: (id: string) => boolean
+}
+
+export const usePanelStore = create((set, get) => ({
+ panels: new Map(),
+ collapsedPanels: new Set(),
+
+ registerPanel: (id, panel) =>
+ set((state) => {
+ const newPanels = new Map(state.panels)
+ newPanels.set(id, panel)
+ return { panels: newPanels }
+ }),
+
+ unregisterPanel: (id) =>
+ set((state) => {
+ const newPanels = new Map(state.panels)
+ newPanels.delete(id)
+ return { panels: newPanels }
+ }),
+
+ getPanel: (id) => get().panels.get(id) || null,
+
+ setCollapsed: (id, isCollapsed) =>
+ set((state) => {
+ const newSet = new Set(state.collapsedPanels)
+ if (isCollapsed) {
+ newSet.add(id)
+ } else {
+ newSet.delete(id)
+ }
+ return { collapsedPanels: newSet }
+ }),
+
+ isCollapsed: (id) => get().collapsedPanels.has(id),
+}))
diff --git a/src/features/content/stores/useRustCompilerStore.ts b/src/features/content/stores/useRustCompilerStore.ts
new file mode 100644
index 0000000..080c127
--- /dev/null
+++ b/src/features/content/stores/useRustCompilerStore.ts
@@ -0,0 +1,64 @@
+import { create } from "zustand"
+import { type ExecutionResult, rustPlayground } from "~/features/content/server/compiler.ts"
+
+interface RustCompilerState {
+ code: string
+ isExecuting: boolean
+ result: ExecutionResult | null
+ liveOutput: { stdout: string; stderr: string }
+ setCode: (code: string) => void
+ execute: () => Promise
+ reset: () => void
+}
+
+export const useRustCompilerStore = create((set, get) => ({
+ code: 'fn main() {\n println!("Hello, world!");\n}',
+ isExecuting: false,
+ result: null,
+ liveOutput: { stdout: "", stderr: "" },
+
+ setCode: (code) => set({ code }),
+
+ execute: async () => {
+ set({
+ isExecuting: true,
+ result: null,
+ liveOutput: { stdout: "", stderr: "" },
+ })
+
+ try {
+ const executionResult = await rustPlayground.execute(
+ get().code,
+ {
+ channel: "stable",
+ mode: "debug",
+ edition: "2021",
+ },
+ (data) =>
+ set((state) => ({
+ liveOutput: { ...state.liveOutput, stdout: state.liveOutput.stdout + data },
+ })),
+ (data) =>
+ set((state) => ({
+ liveOutput: { ...state.liveOutput, stderr: state.liveOutput.stderr + data },
+ })),
+ )
+
+ console.log("Resultado de la ejecución:", executionResult)
+
+ set({ result: executionResult })
+ } catch (error) {
+ console.error("Error ejecutando código:", error)
+ } finally {
+ set({ isExecuting: false })
+ }
+ },
+
+ reset: () =>
+ set({
+ code: 'fn main() {\n println!("Hello, world!");\n}',
+ isExecuting: false,
+ result: null,
+ liveOutput: { stdout: "", stderr: "" },
+ }),
+}))
diff --git a/src/features/content/ui/Main.astro b/src/features/content/ui/Main.astro
new file mode 100644
index 0000000..49c3a32
--- /dev/null
+++ b/src/features/content/ui/Main.astro
@@ -0,0 +1,3 @@
+
+
+
diff --git a/src/features/content/ui/NavButtons.astro b/src/features/content/ui/NavButtons.astro
new file mode 100644
index 0000000..76994ad
--- /dev/null
+++ b/src/features/content/ui/NavButtons.astro
@@ -0,0 +1,43 @@
+---
+import { IconArrowBigLeftLinesFilled, IconArrowBigRightLinesFilled } from "@tabler/icons-react"
+
+const { previous, next } = Astro.props
+---
+
+
diff --git a/src/features/content/ui/PanelContent.astro b/src/features/content/ui/PanelContent.astro
new file mode 100644
index 0000000..988ba0a
--- /dev/null
+++ b/src/features/content/ui/PanelContent.astro
@@ -0,0 +1,15 @@
+---
+import { IconLicense } from "@tabler/icons-react"
+---
+
+
diff --git a/src/features/content/components/ToggleSidebarButton.tsx b/src/features/content/ui/ToggleSidebarButton.tsx
similarity index 100%
rename from src/features/content/components/ToggleSidebarButton.tsx
rename to src/features/content/ui/ToggleSidebarButton.tsx
diff --git a/src/features/home/components/Community.astro b/src/features/home/ui/Community.astro
similarity index 91%
rename from src/features/home/components/Community.astro
rename to src/features/home/ui/Community.astro
index 27746c5..bd7102e 100644
--- a/src/features/home/components/Community.astro
+++ b/src/features/home/ui/Community.astro
@@ -1,12 +1,11 @@
---
import { Image } from "astro:assets"
import discordMarkImage from "~/assets/images/webp/discord-mark-white.webp"
-import Section from "~/components/shared/sections/Section.astro"
---
-
+
diff --git a/src/features/home/components/Hero.astro b/src/features/home/ui/Hero.astro
similarity index 79%
rename from src/features/home/components/Hero.astro
rename to src/features/home/ui/Hero.astro
index 8045dfb..c7d33d7 100644
--- a/src/features/home/components/Hero.astro
+++ b/src/features/home/ui/Hero.astro
@@ -2,15 +2,11 @@
import { Image } from "astro:assets"
import { IconBrandGithub } from "@tabler/icons-react"
import logoImage from "~/assets/images/webp/logo.webp"
-import Section from "~/components/shared/sections/Section.astro"
-import { allContent } from "~/content.config"
-
-const content = allContent.find((c) => c.slug)
---
-
+