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:
+
+
{
+ setSlug(event.target.value);
+ }}
+ placeholder="Slug (Optional)"
+ />
+
{} : handleSubmit}
+ text={loading ? "In Progress" : "Deploy"}
+ />
+
+ ) : (
+
+ {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 (
+
+ );
+};
+
+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"),
+ },
+ },
+});