From 99d587e8e4f518d55788235f0a7ae4044485257d Mon Sep 17 00:00:00 2001 From: Dustin Loring Date: Thu, 16 Jan 2025 16:50:22 -0500 Subject: [PATCH] feat: push to github added the ability to push projects to github from the workbench --- app/components/workbench/GitHubPushModal.tsx | 100 ++++++++++++++++++ app/components/workbench/Workbench.client.tsx | 15 ++- app/lib/stores/github.ts | 72 +++++++++++++ 3 files changed, 186 insertions(+), 1 deletion(-) create mode 100644 app/components/workbench/GitHubPushModal.tsx create mode 100644 app/lib/stores/github.ts diff --git a/app/components/workbench/GitHubPushModal.tsx b/app/components/workbench/GitHubPushModal.tsx new file mode 100644 index 0000000..9e2fadd --- /dev/null +++ b/app/components/workbench/GitHubPushModal.tsx @@ -0,0 +1,100 @@ +import { useStore } from '@nanostores/react'; +import { useState } from 'react'; +import { toast } from 'react-toastify'; +import { githubStore } from '~/lib/stores/github'; + +interface GitHubPushModalProps { + isOpen: boolean; + onClose: () => void; +} + +export function GitHubPushModal({ isOpen, onClose }: GitHubPushModalProps) { + const [token, setToken] = useState(''); + const [username, setUsername] = useState(''); + const [repoName, setRepoName] = useState(''); + const [isLoading, setIsLoading] = useState(false); + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + setIsLoading(true); + + try { + await githubStore.pushToGitHub(token, username, repoName); + toast.success('Successfully pushed to GitHub!'); + onClose(); + } catch (error) { + toast.error(error instanceof Error ? error.message : 'Failed to push to GitHub'); + } finally { + setIsLoading(false); + } + }; + + if (!isOpen) return null; + + return ( +
+
+

Push to GitHub

+
+
+
+ +
+
+ +
+
+ +
+
+
+ + +
+
+
+
+ ); +} \ No newline at end of file diff --git a/app/components/workbench/Workbench.client.tsx b/app/components/workbench/Workbench.client.tsx index f775aa9..8cf479b 100644 --- a/app/components/workbench/Workbench.client.tsx +++ b/app/components/workbench/Workbench.client.tsx @@ -3,9 +3,10 @@ import { saveAs } from 'file-saver'; import { motion, type HTMLMotionProps, type Variants } from 'framer-motion'; import JSZip from 'jszip'; import { computed } from 'nanostores'; -import { memo, useCallback, useEffect } from 'react'; +import { memo, useCallback, useEffect, useState } from 'react'; import { toast } from 'react-toastify'; import { EditorPanel } from './EditorPanel'; +import { GitHubPushModal } from './GitHubPushModal'; import { Preview } from './Preview'; import { type OnChangeCallback as OnEditorChange, @@ -64,6 +65,7 @@ export const Workbench = memo(({ chatStarted, isStreaming }: WorkspaceProps) => const unsavedFiles = useStore(workbenchStore.unsavedFiles); const files = useStore(workbenchStore.files); const selectedView = useStore(workbenchStore.currentView); + const [showGitHubModal, setShowGitHubModal] = useState(false); const downloadZip = async () => { const zip = new JSZip(); @@ -159,6 +161,13 @@ export const Workbench = memo(({ chatStarted, isStreaming }: WorkspaceProps) =>
Download + setShowGitHubModal(true)} + > +
+ Push to GitHub + { @@ -207,6 +216,10 @@ export const Workbench = memo(({ chatStarted, isStreaming }: WorkspaceProps) =>
+ setShowGitHubModal(false)} + /> ) ); diff --git a/app/lib/stores/github.ts b/app/lib/stores/github.ts new file mode 100644 index 0000000..b334d12 --- /dev/null +++ b/app/lib/stores/github.ts @@ -0,0 +1,72 @@ +import { atom, type WritableAtom } from 'nanostores'; +import { workbenchStore } from './workbench'; + +export interface GitHubConfig { + token?: string; + username?: string; + repoName?: string; +} + +class GitHubStore { + config: WritableAtom = atom({}); + + async pushToGitHub(token: string, username: string, repoName: string): Promise { + try { + // First, create the repository if it doesn't exist + const createRepoResponse = await fetch(`https://api.github.com/user/repos`, { + method: 'POST', + headers: { + 'Authorization': `token ${token}`, + 'Accept': 'application/vnd.github.v3+json', + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + name: repoName, + private: false, + auto_init: false, + }), + }); + + if (!createRepoResponse.ok && createRepoResponse.status !== 422) { // 422 means repo already exists + throw new Error(`Failed to create repository: ${createRepoResponse.statusText}`); + } + + // Get all files from workbench + const files = workbenchStore.files.get(); + + // Create a commit with all files + for (const [filePath, dirent] of Object.entries(files)) { + if (dirent?.type === 'file' && !dirent.isBinary) { + const relativePath = filePath.replace(/^\/home\/project\//, ''); + + // Create/update file in repository + const response = await fetch(`https://api.github.com/repos/${username}/${repoName}/contents/${relativePath}`, { + method: 'PUT', + headers: { + 'Authorization': `token ${token}`, + 'Accept': 'application/vnd.github.v3+json', + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + message: `Add/Update ${relativePath}`, + content: btoa(dirent.content), // Base64 encode content + }), + }); + + if (!response.ok) { + throw new Error(`Failed to push file ${relativePath}: ${response.statusText}`); + } + } + } + + // Update config store + this.config.set({ token, username, repoName }); + + } catch (error) { + console.error('Error pushing to GitHub:', error); + throw error; + } + } +} + +export const githubStore = new GitHubStore(); \ No newline at end of file