File size: 4,710 Bytes
19a950f
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
"use client"

import { useEffect, useMemo, useRef, useState } from "react"
import { Button } from "@/components/ui/button"
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
import { buildCodeSandboxHtml } from "@/lib/code-sandbox-html"
import { cn } from "@/lib/utils"

type SandboxLine = {
  type: "log" | "err"
  value: string
}

type CodeSandboxButtonProps = {
  language: string
  code: string
}

export function CodeSandboxButton({ language, code }: CodeSandboxButtonProps) {
  const [open, setOpen] = useState(false)
  const [running, setRunning] = useState(false)
  const [frameKey, setFrameKey] = useState(0)
  const [lines, setLines] = useState<SandboxLine[]>([])
  const iframeRef = useRef<HTMLIFrameElement>(null)

  const srcDoc = useMemo(() => (running ? buildCodeSandboxHtml(language, code) : ""), [code, language, running])
  const errors = lines.filter((line) => line.type === "err")
  const logs = lines.filter((line) => line.type !== "err")

  useEffect(() => {
    if (!open) return

    function handleMessage(event: MessageEvent) {
      if (event.source !== iframeRef.current?.contentWindow) return
      const data = event.data
      if (!data || (data.sb !== "log" && data.sb !== "err")) return
      setLines((current) => [...current, { type: data.sb, value: String(data.v ?? "") }])
    }

    window.addEventListener("message", handleMessage)
    return () => window.removeEventListener("message", handleMessage)
  }, [open])

  function run() {
    setLines([])
    setOpen(true)
    setRunning(true)
    setFrameKey((key) => key + 1)
  }

  function stop() {
    setRunning(false)
  }

  function close() {
    setRunning(false)
    setOpen(false)
  }

  return (
    <>
      <Button
        type="button"
        variant="ghost"
        size="sm"
        onClick={run}
        className="h-auto gap-1 rounded-md px-2 py-1 text-xs text-muted-foreground hover:text-foreground"
      >
        ▶ Chạy
      </Button>

      {open && (
        <div className="fixed inset-0 z-50 flex items-center justify-center bg-background/80 p-4 backdrop-blur-sm" role="dialog" aria-modal="true">
          <Card className="w-full max-w-4xl overflow-hidden shadow-lg">
            <CardHeader className="flex-row items-center justify-between space-y-0 border-b p-4">
              <CardTitle className="text-base">Sandbox: {language || "code"}</CardTitle>
              <div className="flex items-center gap-2">
                {running ? (
                  <Button type="button" variant="secondary" size="sm" onClick={stop}>
                    🛑 Dừng
                  </Button>
                ) : (
                  <Button type="button" size="sm" onClick={run}>
                    ▶ Chạy
                  </Button>
                )}
                <Button type="button" variant="ghost" size="sm" onClick={close}>
                  Đóng
                </Button>
              </div>
            </CardHeader>
            <CardContent className="grid gap-3 p-4 md:grid-cols-2">
              <div className="min-h-[220px] overflow-hidden rounded-md border bg-white">
                {running && <iframe ref={iframeRef} key={frameKey} title="Code sandbox" sandbox="allow-scripts" srcDoc={srcDoc} className="h-[320px] w-full bg-white" />}
                {!running && <div className="p-4 text-sm text-muted-foreground">Sandbox đã dừng.</div>}
              </div>

              <div className="space-y-3">
                <div>
                  <div className="mb-1 text-sm font-medium">Output:</div>
                  <div className="max-h-[300px] overflow-auto rounded-md border bg-muted/30 p-3 font-mono text-xs">
                    {logs.length === 0 ? <div className="text-muted-foreground">Chưa có output.</div> : logs.map((line, index) => <div key={`log-${index}`}>{line.value}</div>)}
                  </div>
                </div>

                <div>
                  <div className="mb-1 text-sm font-medium text-destructive">Lỗi:</div>
                  <div className="max-h-[300px] overflow-auto rounded-md border bg-muted/30 p-3 font-mono text-xs">
                    {errors.length === 0 ? (
                      <div className="text-muted-foreground">Không có lỗi.</div>
                    ) : (
                      errors.map((line, index) => (
                        <div key={`err-${index}`} className={cn("text-destructive")}>
                          {line.value}
                        </div>
                      ))
                    )}
                  </div>
                </div>
              </div>
            </CardContent>
          </Card>
        </div>
      )}
    </>
  )
}