autosite-client
Version:
Client package for deploying React apps to Autosite servers
369 lines (334 loc) • 9.54 kB
JavaScript
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",
}}
>
×
</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;