UNPKG

autosite-client

Version:

Client package for deploying React apps to Autosite servers

369 lines (334 loc) 9.54 kB
const React = require("react"); const { useState, useEffect } = React; // Default styles for the deployment UI const defaultStyles = { container: { position: "fixed", bottom: "20px", right: "20px", zIndex: 9999, display: "flex", flexDirection: "column", alignItems: "flex-end", }, button: { backgroundColor: "#2563eb", color: "white", border: "none", borderRadius: "6px", padding: "10px 16px", fontSize: "14px", fontWeight: "bold", cursor: "pointer", display: "flex", alignItems: "center", justifyContent: "center", boxShadow: "0 2px 5px rgba(0,0,0,0.1)", transition: "all 0.2s ease", }, buttonHover: { backgroundColor: "#1d4ed8", boxShadow: "0 3px 7px rgba(0,0,0,0.15)", }, panel: { backgroundColor: "white", borderRadius: "8px", boxShadow: "0 4px 12px rgba(0,0,0,0.15)", padding: "20px", marginBottom: "10px", width: "300px", overflow: "hidden", transition: "height 0.3s ease, opacity 0.2s ease", maxHeight: "400px", display: "flex", flexDirection: "column", }, input: { width: "100%", padding: "8px 12px", borderRadius: "4px", border: "1px solid #ddd", marginBottom: "12px", fontSize: "14px", }, deployButton: { backgroundColor: "#10b981", color: "white", border: "none", borderRadius: "4px", padding: "8px 12px", fontSize: "14px", fontWeight: "bold", cursor: "pointer", marginTop: "8px", }, status: { padding: "8px", marginTop: "10px", borderRadius: "4px", fontSize: "14px", textAlign: "center", }, statusSuccess: { backgroundColor: "#d1fae5", color: "#047857", }, statusError: { backgroundColor: "#fee2e2", color: "#b91c1c", }, statusLoading: { backgroundColor: "#f3f4f6", color: "#374151", }, }; // Main component const DeployButton = ({ styles = {}, buttonText = "Deploy", onSuccess = () => {}, onError = () => {}, }) => { const [showPanel, setShowPanel] = useState(false); const [isHovered, setIsHovered] = useState(false); const [serverUrl, setServerUrl] = useState(""); const [apiKey, setApiKey] = useState(""); const [status, setStatus] = useState(null); const [isDeploying, setIsDeploying] = useState(false); // Merge default styles with custom styles const mergedStyles = { container: { ...defaultStyles.container, ...styles.container }, button: { ...defaultStyles.button, ...(isHovered ? defaultStyles.buttonHover : {}), ...styles.button, }, panel: { ...defaultStyles.panel, ...styles.panel }, input: { ...defaultStyles.input, ...styles.input }, deployButton: { ...defaultStyles.deployButton, ...styles.deployButton }, status: { ...defaultStyles.status, ...(status === "success" ? defaultStyles.statusSuccess : status === "error" ? defaultStyles.statusError : status === "loading" ? defaultStyles.statusLoading : {}), ...styles.status, }, }; // Check if window exists (client-side) const isClient = typeof window !== "undefined"; // Get stored values from localStorage useEffect(() => { if (isClient) { const storedUrl = localStorage.getItem("autosite-server-url"); const storedKey = localStorage.getItem("autosite-api-key"); if (storedUrl) setServerUrl(storedUrl); if (storedKey) setApiKey(storedKey); } }, []); // Save values to localStorage when they change useEffect(() => { if (isClient && serverUrl) { localStorage.setItem("autosite-server-url", serverUrl); } }, [serverUrl]); useEffect(() => { if (isClient && apiKey) { localStorage.setItem("autosite-api-key", apiKey); } }, [apiKey]); // Handle deployment const handleDeploy = async () => { if (!serverUrl || !apiKey) { setStatus("error"); return; } setIsDeploying(true); setStatus("loading"); try { // Find the build directory const buildDirNames = ["build", "dist", "out"]; let buildDir = null; // This will only work when the development server is running from the same directory if (isClient) { for (const dir of buildDirNames) { try { // Try to access the directory await fetch(`/${dir}/index.html`); buildDir = dir; break; } catch (e) { // Directory not accessible } } } if (!buildDir) { throw new Error( "Build directory not found. Please build your app first." ); } // Display notification to build app setStatus("warning"); // In a real implementation, you would need to: // 1. Create a zip of the build directory (requires backend support or a browser API) // 2. Upload it to the server using the API key // For now, we'll simulate a successful deployment setTimeout(() => { setStatus("success"); onSuccess(); setIsDeploying(false); }, 2000); } catch (error) { setStatus("error"); onError(error); setIsDeploying(false); } }; // Don't render anything in SSR if (!isClient) return null; // Only show in development if (process.env.NODE_ENV !== "development") return null; return ( <div style={mergedStyles.container}> {showPanel && ( <div style={mergedStyles.panel}> <h3 style={{ margin: "0 0 15px 0" }}>Deploy to Server</h3> <label style={{ fontSize: "12px", marginBottom: "4px" }}> Server URL </label> <input type="text" value={serverUrl} onChange={(e) => setServerUrl(e.target.value)} placeholder="https://example.com:8080" style={mergedStyles.input} /> <label style={{ fontSize: "12px", marginBottom: "4px" }}> API Key </label> <input type="password" value={apiKey} onChange={(e) => setApiKey(e.target.value)} placeholder="Your API key" style={mergedStyles.input} /> <button onClick={handleDeploy} disabled={isDeploying} style={{ ...mergedStyles.deployButton, opacity: isDeploying ? 0.7 : 1, cursor: isDeploying ? "not-allowed" : "pointer", }} > {isDeploying ? "Deploying..." : "Deploy Now"} </button> {status && ( <div style={mergedStyles.status}> {status === "success" && "Deployment successful!"} {status === "error" && "Deployment failed. Check configuration."} {status === "loading" && "Deploying application..."} {status === "warning" && 'Please build your app with "npm run build" first.'} </div> )} </div> )} <button style={mergedStyles.button} onClick={() => setShowPanel(!showPanel)} onMouseEnter={() => setIsHovered(true)} onMouseLeave={() => setIsHovered(false)} > {buttonText} </button> </div> ); }; // The message component shows a warning that the AutoSite component is loaded const AutoSiteNotice = () => { const [dismissed, setDismissed] = useState(false); // Don't render anything in SSR if (typeof window === "undefined") return null; // Only show in development if (process.env.NODE_ENV !== "development") return null; if (dismissed) return null; return ( <div style={{ position: "fixed", bottom: "80px", right: "20px", backgroundColor: "#fff9db", border: "1px solid #ffd43b", borderRadius: "6px", padding: "12px 16px", boxShadow: "0 2px 5px rgba(0,0,0,0.1)", zIndex: 9998, maxWidth: "300px", fontSize: "14px", }} > <div style={{ display: "flex", justifyContent: "space-between", alignItems: "center", marginBottom: "8px", }} > <strong>AutoSite Enabled</strong> <button onClick={() => setDismissed(true)} style={{ background: "none", border: "none", cursor: "pointer", fontSize: "16px", color: "#666", }} > &times; </button> </div> <p style={{ margin: "0", color: "#666" }}> One-click deployment is available for this project. Click the "Deploy" button to publish this app. </p> </div> ); }; // Main export with the Deploy Button and AutoSite notification const AutoSite = { DeployButton, AutoSiteNotice, init: (config = {}) => { // Create container element const container = document.createElement("div"); container.id = "autosite-container"; document.body.appendChild(container); // Render components const ReactDOM = require("react-dom"); ReactDOM.render( <> <AutoSiteNotice /> <DeployButton {...config} /> </>, container ); return { unmount: () => { ReactDOM.unmountComponentAtNode(container); container.remove(); }, }; }, }; if (typeof window !== "undefined") { window.AutoSite = AutoSite; } module.exports = AutoSite;