|
1 | | -import PortSelector from "./PortSelector"; |
2 | | -import ProxyFrame from "./ProxyFrame"; |
3 | | -import BrowserTabs from "./BrowserTabs"; |
| 1 | +import { useState, useRef, useEffect } from "react"; |
| 2 | +import { useBrowserStore, type BrowserTab } from "~/stores/browserStore"; |
| 3 | +import { isPortBlocked } from "~/lib/constants"; |
| 4 | + |
| 5 | +function TabInput({ tab }: { tab: BrowserTab }) { |
| 6 | + const [input, setInput] = useState(tab.port); |
| 7 | + const inputRef = useRef<HTMLInputElement>(null); |
| 8 | + const setTabPort = useBrowserStore((s) => s.setTabPort); |
| 9 | + |
| 10 | + useEffect(() => { |
| 11 | + if (!tab.port) inputRef.current?.focus(); |
| 12 | + }, []); |
| 13 | + |
| 14 | + const val = input.trim(); |
| 15 | + const isNumeric = val !== "" && /^\d+$/.test(val); |
| 16 | + const blocked = isNumeric ? isPortBlocked(parseInt(val, 10)) : null; |
| 17 | + const canConnect = isNumeric && !blocked; |
| 18 | + |
| 19 | + const handleSubmit = () => { |
| 20 | + if (canConnect) setTabPort(tab.id, val); |
| 21 | + }; |
| 22 | + |
| 23 | + return ( |
| 24 | + <div className="flex flex-col items-center justify-center h-full bg-[#0d0d1a] gap-4"> |
| 25 | + <svg className="w-10 h-10 text-gray-600" fill="none" stroke="currentColor" viewBox="0 0 24 24"> |
| 26 | + <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.5} d="M21 12a9 9 0 01-9 9m9-9a9 9 0 00-9-9m9 9H3m9 9a9 9 0 01-9-9m9 9c1.657 0 3-4.03 3-9s-1.343-9-3-9m0 18c-1.657 0-3-4.03-3-9s1.343-9 3-9m-9 9a9 9 0 019-9" /> |
| 27 | + </svg> |
| 28 | + <div className="flex items-center gap-2"> |
| 29 | + <span className="text-sm text-gray-400">localhost:</span> |
| 30 | + <input |
| 31 | + ref={inputRef} |
| 32 | + type="text" |
| 33 | + value={input} |
| 34 | + onChange={(e) => setInput(e.target.value)} |
| 35 | + onKeyDown={(e) => e.key === "Enter" && handleSubmit()} |
| 36 | + placeholder="port" |
| 37 | + className={`w-24 bg-[#1a1a2e] text-white border rounded px-3 py-2 text-sm focus:outline-none focus:ring-1 ${ |
| 38 | + blocked ? "border-red-500 focus:ring-red-500" : "border-gray-600 focus:ring-blue-500" |
| 39 | + }`} |
| 40 | + autoFocus |
| 41 | + /> |
| 42 | + <button |
| 43 | + onClick={handleSubmit} |
| 44 | + disabled={!canConnect} |
| 45 | + className="px-4 py-2 bg-blue-600 hover:bg-blue-700 disabled:bg-gray-700 disabled:text-gray-500 text-white rounded text-sm font-medium transition-colors" |
| 46 | + > |
| 47 | + Connect |
| 48 | + </button> |
| 49 | + </div> |
| 50 | + {blocked && ( |
| 51 | + <span className="text-xs text-red-400">{blocked}</span> |
| 52 | + )} |
| 53 | + </div> |
| 54 | + ); |
| 55 | +} |
4 | 56 |
|
5 | 57 | export default function BrowserPage() { |
| 58 | + const { tabs, activeTabId, addTab, removeTab, setActiveTab, refreshTab } = useBrowserStore(); |
| 59 | + const activeTab = tabs.find((t) => t.id === activeTabId); |
| 60 | + |
6 | 61 | return ( |
7 | | - <div className="flex flex-col h-full"> |
8 | | - <BrowserTabs /> |
9 | | - <PortSelector /> |
| 62 | + <div className="flex flex-col h-full min-h-0"> |
| 63 | + {/* Tab bar */} |
| 64 | + <div className="flex items-center bg-[#0d0d1a] border-b border-gray-800 overflow-x-auto scrollbar-none shrink-0"> |
| 65 | + {tabs.map((tab) => { |
| 66 | + const isActive = tab.id === activeTabId; |
| 67 | + return ( |
| 68 | + <div |
| 69 | + key={tab.id} |
| 70 | + onClick={() => setActiveTab(tab.id)} |
| 71 | + className={`flex items-center gap-1.5 px-3 py-1.5 cursor-pointer text-sm border-r border-gray-800 shrink-0 transition-colors ${ |
| 72 | + isActive |
| 73 | + ? "bg-[#16162a] text-white border-b-2 border-b-blue-500" |
| 74 | + : "text-gray-400 hover:text-white hover:bg-[#16162a]/50" |
| 75 | + }`} |
| 76 | + > |
| 77 | + <svg className="w-3 h-3 shrink-0 text-gray-500" fill="none" stroke="currentColor" viewBox="0 0 24 24"> |
| 78 | + <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M21 12a9 9 0 01-9 9m9-9a9 9 0 00-9-9m9 9H3m9 9a9 9 0 01-9-9m9 9c1.657 0 3-4.03 3-9s-1.343-9-3-9" /> |
| 79 | + </svg> |
| 80 | + <span>{tab.port ? `:${tab.port}` : "new"}</span> |
| 81 | + {/* Refresh — only on active tab with a port */} |
| 82 | + {isActive && tab.port && ( |
| 83 | + <button |
| 84 | + onClick={(e) => { e.stopPropagation(); refreshTab(tab.id); }} |
| 85 | + className="p-0.5 rounded hover:bg-gray-600 text-gray-500 hover:text-gray-200 transition-colors" |
| 86 | + title="Refresh" |
| 87 | + > |
| 88 | + <svg className="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24" strokeWidth={2}> |
| 89 | + <path strokeLinecap="round" strokeLinejoin="round" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15" /> |
| 90 | + </svg> |
| 91 | + </button> |
| 92 | + )} |
| 93 | + <button |
| 94 | + onClick={(e) => { e.stopPropagation(); removeTab(tab.id); }} |
| 95 | + className="p-0.5 rounded hover:bg-gray-600 text-gray-500 hover:text-gray-200 transition-colors" |
| 96 | + title="Close tab" |
| 97 | + > |
| 98 | + <svg className="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24"> |
| 99 | + <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" /> |
| 100 | + </svg> |
| 101 | + </button> |
| 102 | + </div> |
| 103 | + ); |
| 104 | + })} |
| 105 | + <button |
| 106 | + onClick={addTab} |
| 107 | + className="px-3 py-1.5 text-gray-400 hover:text-white hover:bg-[#16162a]/50 transition-colors shrink-0" |
| 108 | + title="New browser tab" |
| 109 | + > |
| 110 | + <svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"> |
| 111 | + <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 4v16m8-8H4" /> |
| 112 | + </svg> |
| 113 | + </button> |
| 114 | + </div> |
| 115 | + |
| 116 | + {/* Content */} |
10 | 117 | <div className="flex-1 relative min-h-0"> |
11 | | - <ProxyFrame /> |
| 118 | + {!activeTab ? ( |
| 119 | + <div className="flex flex-col items-center justify-center h-full bg-[#0d0d1a] text-gray-500 gap-3"> |
| 120 | + <svg className="w-12 h-12 text-gray-600" fill="none" stroke="currentColor" viewBox="0 0 24 24"> |
| 121 | + <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.5} d="M21 12a9 9 0 01-9 9m9-9a9 9 0 00-9-9m9 9H3m9 9a9 9 0 01-9-9m9 9c1.657 0 3-4.03 3-9s-1.343-9-3-9m0 18c-1.657 0-3-4.03-3-9s1.343-9 3-9m-9 9a9 9 0 019-9" /> |
| 122 | + </svg> |
| 123 | + <span className="text-sm">Click + to preview a localhost port</span> |
| 124 | + </div> |
| 125 | + ) : !activeTab.port ? ( |
| 126 | + <TabInput tab={activeTab} /> |
| 127 | + ) : ( |
| 128 | + /* Render all tabs with port, show active */ |
| 129 | + tabs.filter((t) => t.port).map((tab) => ( |
| 130 | + <div |
| 131 | + key={tab.id} |
| 132 | + className="absolute inset-0" |
| 133 | + style={{ visibility: tab.id === activeTabId ? "visible" : "hidden" }} |
| 134 | + > |
| 135 | + <iframe |
| 136 | + key={`${tab.port}-${tab.refreshKey}`} |
| 137 | + src={`/proxy/${tab.port}/`} |
| 138 | + className="w-full h-full border-0 bg-[#0d0d1a]" |
| 139 | + title={`localhost:${tab.port}`} |
| 140 | + sandbox="allow-same-origin allow-scripts allow-forms allow-popups allow-modals" |
| 141 | + /> |
| 142 | + </div> |
| 143 | + )) |
| 144 | + )} |
12 | 145 | </div> |
13 | 146 | </div> |
14 | 147 | ); |
|
0 commit comments