diff --git a/frontend/.eslintrc.cjs b/frontend/.eslintrc.cjs new file mode 100644 index 0000000..3e212e1 --- /dev/null +++ b/frontend/.eslintrc.cjs @@ -0,0 +1,21 @@ +module.exports = { + root: true, + env: { browser: true, es2020: true }, + extends: [ + 'eslint:recommended', + 'plugin:react/recommended', + 'plugin:react/jsx-runtime', + 'plugin:react-hooks/recommended', + ], + ignorePatterns: ['dist', '.eslintrc.cjs'], + parserOptions: { ecmaVersion: 'latest', sourceType: 'module' }, + settings: { react: { version: '18.2' } }, + plugins: ['react-refresh'], + rules: { + 'react/jsx-no-target-blank': 'off', + 'react-refresh/only-export-components': [ + 'warn', + { allowConstantExport: true }, + ], + }, +} diff --git a/frontend/.gitignore b/frontend/.gitignore new file mode 100644 index 0000000..0fd6913 --- /dev/null +++ b/frontend/.gitignore @@ -0,0 +1,25 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* + +node_modules +dist +dist-ssr +*.local + +# Editor directories and files +.vscode/* +!.vscode/extensions.json +.idea +.DS_Store +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? +package-lock.json diff --git a/frontend/README.md b/frontend/README.md new file mode 100644 index 0000000..006fa86 --- /dev/null +++ b/frontend/README.md @@ -0,0 +1,53 @@ +# Vercel Clone + +This project is a clone of Vercel, built with React and Vite. It provides a minimal setup to get React working in Vite with HMR and some ESLint rules. + +## Project Structure + +The frontend directory structure is as follows: +frontend/ .eslintrc.cjs index.html package.json +public/ src/ App.jsx +components/ Button.jsx ErrorBoundary.jsx Header.jsx Input.jsx Welcome.jsx index.css main.jsx +service/ apiService.js +View/ Home.jsx Submission.jsx + + +## Setup + +To set up the frontend project, follow these steps: + +1. Navigate to the `frontend` directory. +2. Run `npm install` to install the project dependencies. + +## Boot + +To start the frontend project, run `npm run dev` in the `frontend` directory. This will start the Vite development server. + +## Dependencies + +The frontend project uses the following dependencies: + +- React for building the UI. +- Vite for building the project and providing a development server. +- Axios for making HTTP requests. +- Socket.io-client for real-time communication with the server. +- @emotion/react and @emotion/styled for styling components. + +For more information, refer to the `package.json` file in the `frontend` directory. + +## How to Use +- Navigate to the Submission Page: The submission page is where you can submit your GitHub repository for deployment. You can navigate to the submission page by clicking on the "Submission" link in the header. + +- Enter your GitHub Repository Link: In the "GitHub Repo Link" field, enter the URL of the GitHub repository you want to deploy. The URL should be in the format https://github.com/username/repo. + +- Enter a Slug (Optional): In the "Slug" field, you can enter a slug for your project. This is optional. If you don't provide a slug, one will be generated for you. + +- Click "Deploy": Click the "Deploy" button to submit your repository for deployment. While your repository is being deployed, the button will display "In Progress". + +- View the Logs: After you've submitted your repository, you can view the logs for your deployment. The logs will automatically update as new logs are generated. + +- View Your Deployed Application: A preview URL will be displayed above the logs. You can click this URL to view your deployed application. (this may take few minutes) + +- Start a New Submission: To start a new submission, click the "New Submission" button. This will take you back to the submission page where you can submit a new repository for deployment. + +- Please note that this application is a clone of Vercel and is intended for educational purposes. It may not support all the features of Vercel. \ No newline at end of file diff --git a/frontend/index.html b/frontend/index.html new file mode 100644 index 0000000..2cd72bc --- /dev/null +++ b/frontend/index.html @@ -0,0 +1,13 @@ + + + + + + + Vercel Clone + + +
+ + + diff --git a/frontend/package.json b/frontend/package.json new file mode 100644 index 0000000..3d2375d --- /dev/null +++ b/frontend/package.json @@ -0,0 +1,31 @@ +{ + "name": "vercel-clone", + "private": true, + "version": "1.0.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "vite build", + "lint": "eslint . --ext js,jsx --report-unused-disable-directives --max-warnings 0", + "preview": "vite preview" + }, + "dependencies": { + "@emotion/react": "^11.11.3", + "@emotion/styled": "^11.11.0", + "axios": "^1.6.7", + "react": "^18.2.0", + "react-dom": "^18.2.0", + "react-router-dom": "^6.22.0", + "socket.io-client": "^4.7.4" + }, + "devDependencies": { + "@types/react": "^18.2.55", + "@types/react-dom": "^18.2.19", + "@vitejs/plugin-react": "^4.2.1", + "eslint": "^8.56.0", + "eslint-plugin-react": "^7.33.2", + "eslint-plugin-react-hooks": "^4.6.0", + "eslint-plugin-react-refresh": "^0.4.5", + "vite": "^5.1.0" + } +} diff --git a/frontend/public/vite.svg b/frontend/public/vite.svg new file mode 100644 index 0000000..e7b8dfb --- /dev/null +++ b/frontend/public/vite.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx new file mode 100644 index 0000000..efad8c0 --- /dev/null +++ b/frontend/src/App.jsx @@ -0,0 +1,40 @@ +import styled from "@emotion/styled"; +import Header from "./components/Header"; +import Home from "./View/Home"; +import Submission from "./View/Submission"; +import { BrowserRouter as Router, Route, Routes } from "react-router-dom"; + +function App() { + const Styled = styled.div` + width: 100vw; + height: 100vh; + overflow: hidden; + .header { + height: 80px; + } + .body { + height: calc(100% - 80px); + display: flex; + justify-content: center; + align-items: center; + overflow-x: hidden; + overflow-y: auto; + } + `; + + return ( + + +
+
+ + } /> + } /> + +
+ + + ); +} + +export default App; diff --git a/frontend/src/View/Home.jsx b/frontend/src/View/Home.jsx new file mode 100644 index 0000000..37f2fd8 --- /dev/null +++ b/frontend/src/View/Home.jsx @@ -0,0 +1,19 @@ +import styled from "@emotion/styled"; +import Welcome from "../components/Welcome"; + +const Home = () => { + const Styled = styled.div` + padding: 20px; + text-align: center; + @media (max-width: 768px) { + padding: 10px; + } + `; + return ( + + + + ); +}; + +export default Home; diff --git a/frontend/src/View/Submission.jsx b/frontend/src/View/Submission.jsx new file mode 100644 index 0000000..d7d69a6 --- /dev/null +++ b/frontend/src/View/Submission.jsx @@ -0,0 +1,219 @@ +import styled from "@emotion/styled"; +import { useCallback, useEffect, useMemo, useRef, useState } from "react"; +import Input from "../components/Input"; +import Button from "../components/Button"; +import apiService from "../service/apiService"; +import { io } from "socket.io-client"; + +const SubmissionPage = () => { + const [repoLink, setRepoLink] = useState(""); + const [slug, setSlug] = useState(""); + const [error, setError] = useState(""); + const [loading, setLoading] = useState(false); + const [activeSlug, setActiveSlug] = useState(""); + const [url, setUrl] = useState(""); + const [logs, setLogs] = useState([]); + + const logContainerRef = useRef(null); + + const handleSocketIncommingMessage = useCallback((message) => { + if (typeof message === "string") { + setLogs((prev) => [...prev, message]); + } else { + const { log } = JSON.parse(message); + setLogs((prev) => [...prev, log]); + } + logContainerRef.current.scrollTop = 0; + }, []); + + useEffect(() => { + document.title = "Submit Your Repo - Vercel Clone"; + }, []); + + const handleChange = (event) => { + setRepoLink(event.target.value); + }; + + const handleSubmit = () => { + console.log(repoLink); + const githubRepoRegex = /^https:\/\/github\.com\/[^/]+\/[^/]+$/; + if (!githubRepoRegex.test(repoLink)) { + setError("Invalid! GitHub repo link."); + } else { + setError(""); + setLoading(true); + apiService("http://localhost:9000/project", "POST", { + gitURL: repoLink, + slug, + }) + .then((data) => { + const { projectSlug, url } = data.data; + setActiveSlug(projectSlug); + setUrl(url); + }) + .catch((error) => { + console.log(error); + window.alert("Error in submitting the repo"); + }) + .finally(() => { + setLoading(false); + }); + } + }; + + let socket = useRef(null); + + useEffect(() => { + if (!socket.current) { + socket.current = io("http://localhost:9002"); + } + }, []); + + useEffect(() => { + if (!activeSlug) return; + setLogs([]); + socket.current.emit("subscribe", `logs:${activeSlug}`); + }, [activeSlug]); + + useEffect(() => { + socket.current.on("message", handleSocketIncommingMessage); + return () => { + socket.current.off("message", handleSocketIncommingMessage); + }; + }, [handleSocketIncommingMessage]); + + const handleReset = () => { + setRepoLink(""); + setSlug(""); + setError(""); + setLoading(false); + setActiveSlug(""); + setUrl(""); + setLogs([]); + }; + const Styled = useMemo( + () => styled.div` + width: 100%; + height: 100%; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + .submit-view, + .log-view { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + padding: 20px; + gap: 12px; + @media (max-width: 768px) { + padding: 10px; + } + } + .error-text { + margin: 4px 2px; + color: red; + font-size: 12px; + text-align: center; + } + .url-container { + padding: 4px 8px; + background: #646cff; + color: white; + border-radius: 4px; + } + .log-container { + min-height: 200px; + width: fit-content; + max-height: 400px; + @media (max-width: 768px) { + max-height: 300px; + } + width: calc(100vw - 40px); + background: #000; + color: #fff; + overflow-y: auto; + padding: 10px; + border-radius: 8px; + text-align: left; + font-size: 8px; + display: flex; + flex-direction: column; + text-align: left; + } + .new-submission { + display: flex; + justify-content: center; + align-items: center; + padding: 20px; + } + .url { + text-align: center; + } + `, + [] + ); + + return ( + + {!activeSlug ? ( +
+
GitHub Repo Link:
+
+ + {error &&
{error}
} +
+ { + setSlug(event.target.value); + }} + placeholder="Slug (Optional)" + /> +
+ ) : ( +
+ {url && ( +
+ Preview URL:{" "} + + {url} + +
+ )} +
Showing logs for: {activeSlug}
+
+ {logs.map((log, index) => ( +
+ {log} +
+ ))} +
+
+ )} + {activeSlug && ( +
+
+ )} +
+ ); +}; + +export default SubmissionPage; diff --git a/frontend/src/components/Button.jsx b/frontend/src/components/Button.jsx new file mode 100644 index 0000000..421148d --- /dev/null +++ b/frontend/src/components/Button.jsx @@ -0,0 +1,30 @@ +import React from "react"; +import styled from "@emotion/styled"; + +function Button({ text, onClick }) { + const Styled = styled.div` + border-radius: 8px; + border: 1px solid transparent; + padding: 0.6em 1.2em; + font-size: 1em; + font-weight: 500; + font-family: inherit; + background-color: #1a1a1a; + cursor: pointer; + transition: border-color 0.25s; + :hover { + border-color: #646cff; + } + :focus, + :focus-visible { + outline: 4px auto -webkit-focus-ring-color; + } + `; + return ( + + {text} + + ); +} + +export default Button; diff --git a/frontend/src/components/ErrorBoundary.jsx b/frontend/src/components/ErrorBoundary.jsx new file mode 100644 index 0000000..4fabf47 --- /dev/null +++ b/frontend/src/components/ErrorBoundary.jsx @@ -0,0 +1,29 @@ +import React from "react"; + +class ErrorBoundary extends React.Component { + constructor(props) { + super(props); + this.state = { hasError: false }; + } + + static getDerivedStateFromError(error) { + // Update state so the next render will show the fallback UI. + return { hasError: true }; + } + + componentDidCatch(error, errorInfo) { + // You can also log the error to an error reporting service + console.log(error, errorInfo); + } + + render() { + if (this.state.hasError) { + // You can render any custom fallback UI + return

Something went wrong.

; + } + + return this.props.children; + } +} + +export default ErrorBoundary; diff --git a/frontend/src/components/Header.jsx b/frontend/src/components/Header.jsx new file mode 100644 index 0000000..9742b1d --- /dev/null +++ b/frontend/src/components/Header.jsx @@ -0,0 +1,122 @@ +import { useState } from "react"; +import styled from "@emotion/styled"; +import { Link, NavLink } from "react-router-dom"; + +const Header = () => { + const [isMenuOpen, setIsMenuOpen] = useState(false); + + const Navbar = styled.div` + padding: 0 20px; + width: 100%; + display: flex; + justify-content: space-between; + align-items: center; + .active { + color: #646cff; + } + `; + + const Menu = styled.ul` + list-style: none; + display: flex; + gap: 8px; + margin: 0px; + text-align: center; + @media (max-width: 768px) { + flex-direction: column; + align-items: flex-start; + display: ${isMenuOpen ? "block" : "none"}; + position: absolute; + top: 80px; + left: 0; + width: 100%; + z-index: 100; + } + `; + + const MenuItem = styled.li` + @media (max-width: 768px) { + margin-bottom: 12px; + } + `; + + const ActiveLink = styled(NavLink)` + text-decoration: none; + cursor: pointer; + padding: 5px; + color: #ccc; + + &:hover { + color: #646cff; + border-bottom: 1px solid #646cff; + } + `; + + const BrandName = styled(Link)` + font-size: 24px; + font-weight: bold; + color: #fff; + + :hover { + color: #646cff; + } + `; + + const HamburgerIcon = styled.div` + display: none; + flex-direction: column; + justify-content: space-around; + width: 2rem; + height: 2rem; + cursor: pointer; + z-index: 5; + @media (max-width: 768px) { + display: flex; + } + div { + width: 2rem; + height: 0.25rem; + background-color: #ccc; + border-radius: 10px; + transform-origin: 1px; + transition: all 0.3s linear; + + &:nth-of-type(1) { + transform: ${({ open }) => (open ? "rotate(45deg)" : "rotate(0)")}; + } + + &:nth-of-type(2) { + transform: ${({ open }) => + open ? "translateX(100%)" : "translateX(0)"}; + opacity: ${({ open }) => (open ? 0 : 1)}; + } + + &:nth-of-type(3) { + transform: ${({ open }) => (open ? "rotate(-45deg)" : "rotate(0)")}; + } + } + `; + + return ( + + Vercel Clone + setIsMenuOpen(!isMenuOpen)} + open={isMenuOpen} + > +
+
+
+ + + + setIsMenuOpen(false)}> + Submission + + + + + ); +}; + +export default Header; diff --git a/frontend/src/components/Input.jsx b/frontend/src/components/Input.jsx new file mode 100644 index 0000000..fe01c10 --- /dev/null +++ b/frontend/src/components/Input.jsx @@ -0,0 +1,28 @@ +import styled from "@emotion/styled"; +import { useMemo } from "react"; + +function Input({ onChange, value, placeholder }) { + const Styled = useMemo( + () => styled.input` + border-radius: 8px; + border: 1px solid #ccc; + padding: 0.6em 1.2em; + font-size: 1em; + font-weight: 500; + outline: none; + width: 300px; + `, + [] + ); + + return ( + + ); +} + +export default Input; diff --git a/frontend/src/components/Welcome.jsx b/frontend/src/components/Welcome.jsx new file mode 100644 index 0000000..f01c23f --- /dev/null +++ b/frontend/src/components/Welcome.jsx @@ -0,0 +1,44 @@ +import React from "react"; +import Button from "./Button"; +import styled from "@emotion/styled"; +import { useNavigate } from "react-router-dom"; + +function Welcome() { + const navigate = useNavigate(); + const handleButtonClick = () => { + navigate("/submission"); + }; + const Styled = styled.div` + .title { + color: #fff; + font-size: 32px; + } + .welcome-text { + color: #666; + font-size: 16px; + line-height: 1.5; + margin: 20px 0; + } + .btn-container { + display: flex; + justify-content: center; + } + `; + + return ( + +
Welcome to the Vercel Clone App!
+
+ This project is a clone of Vercel, a company known for maintaining the + Next.js web development framework. The architecture of Vercel is built + around composable architecture, and deployments are handled through Git + repositories. Vercel is a member of the MACH Alliance. +
+
+
+
+ ); +} + +export default Welcome; diff --git a/frontend/src/index.css b/frontend/src/index.css new file mode 100644 index 0000000..5b746a5 --- /dev/null +++ b/frontend/src/index.css @@ -0,0 +1,74 @@ +:root { + font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif; + line-height: 1.5; + font-weight: 400; + + color-scheme: light dark; + color: rgba(255, 255, 255, 0.87); + background-color: #242424; + + font-synthesis: none; + text-rendering: optimizeLegibility; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} + +a { + font-weight: 500; + color: #646cff; + text-decoration: inherit; +} +a:hover { + color: #535bf2; +} + +body { + margin: 0; + display: flex; + place-items: center; + min-width: 320px; + min-height: 100vh; +} + +h1 { + font-size: 3.2em; + line-height: 1.1; +} + +button { + border-radius: 8px; + border: 1px solid transparent; + padding: 0.6em 1.2em; + font-size: 1em; + font-weight: 500; + font-family: inherit; + background-color: #1a1a1a; + cursor: pointer; + transition: border-color 0.25s; +} +button:hover { + border-color: #646cff; +} +button:focus, +button:focus-visible { + outline: 4px auto -webkit-focus-ring-color; +} + +@media (prefers-color-scheme: light) { + :root { + color: #213547; + background-color: #ffffff; + } + a:hover { + color: #747bff; + } + button { + background-color: #f9f9f9; + } +} + +* { + box-sizing: border-box; + margin: 0px; + padding: 0px; +} diff --git a/frontend/src/main.jsx b/frontend/src/main.jsx new file mode 100644 index 0000000..bd48cf9 --- /dev/null +++ b/frontend/src/main.jsx @@ -0,0 +1,13 @@ +import React from "react"; +import ReactDOM from "react-dom/client"; +import App from "./App.jsx"; +import "./index.css"; +import ErrorBoundary from "./components/ErrorBoundary.jsx"; + +ReactDOM.createRoot(document.getElementById("root")).render( + + + + + +); diff --git a/frontend/src/service/apiService.js b/frontend/src/service/apiService.js new file mode 100644 index 0000000..52bb9dd --- /dev/null +++ b/frontend/src/service/apiService.js @@ -0,0 +1,26 @@ +import axios from 'axios'; + +const apiService = async (url, method = 'GET', body = {}, headers = {}) => { + try { + const response = await axios({ + url, + method, + data: body, + headers, + }); + return response.data; + } catch (error) { + if (error.response) { + console.error('Error', error.response.status, error.response.data); + throw new Error(`Error: ${error.response.data}`); + } else if (error.request) { + console.error('No response received', error.request); + throw new Error('No response received from the server.'); + } else { + console.error('Error', error.message); + throw new Error(`Error: ${error.message}`); + } + } +}; + +export default apiService; \ No newline at end of file diff --git a/frontend/vite.config.js b/frontend/vite.config.js new file mode 100644 index 0000000..cf45b55 --- /dev/null +++ b/frontend/vite.config.js @@ -0,0 +1,12 @@ +import path from "path"; +import react from "@vitejs/plugin-react"; +import { defineConfig } from "vite"; + +export default defineConfig({ + plugins: [react()], + resolve: { + alias: { + "@": path.resolve(__dirname, "./src"), + }, + }, +});