diff --git a/src/app/(dashboard)/projects/new/actions.ts b/src/app/(dashboard)/projects/new/actions.ts new file mode 100644 index 0000000..c02ac8f --- /dev/null +++ b/src/app/(dashboard)/projects/new/actions.ts @@ -0,0 +1,91 @@ +'use server' + +import type { GiteaRepository, GiteaWebhook } from '@/lib/gitea/types' + +const GITEA_URL = process.env.GITEA_URL || 'https://gitea.mylder.io' +const GITEA_TOKEN = process.env.GITEA_TOKEN || '' +const GITEA_OWNER = 'admin' +const WWS_WEBHOOK_URL = 'https://wws.mylder.io/webhook/gitea' + +interface CreateRepoResult { + success: boolean + repo?: GiteaRepository + webhook?: GiteaWebhook + error?: string +} + +async function giteaRequest(endpoint: string, options: RequestInit = {}): Promise { + const url = `${GITEA_URL}/api/v1${endpoint}` + const response = await fetch(url, { + ...options, + headers: { + 'Authorization': `token ${GITEA_TOKEN}`, + 'Content-Type': 'application/json', + ...options.headers, + }, + }) + + if (!response.ok) { + const error = await response.json().catch(() => ({ + message: `HTTP ${response.status}: ${response.statusText}`, + })) + throw new Error(error.message || `Gitea API error: ${response.status}`) + } + + if (response.status === 204) { + return {} as T + } + + return response.json() +} + +export async function createGiteaRepo(slug: string, description?: string): Promise { + try { + // Check if repo already exists + const existingCheck = await fetch(`${GITEA_URL}/api/v1/repos/${GITEA_OWNER}/${slug}`, { + headers: { 'Authorization': `token ${GITEA_TOKEN}` }, + }) + + if (existingCheck.ok) { + return { success: false, error: 'Repository already exists' } + } + + // Create the repository + const repo = await giteaRequest('/user/repos', { + method: 'POST', + body: JSON.stringify({ + name: slug, + description: description || `Mylder project: ${slug}`, + private: false, + auto_init: true, + default_branch: 'main', + }), + }) + + // Create webhook pointing to WWS (which routes to n8n) + const webhook = await giteaRequest(`/repos/${GITEA_OWNER}/${slug}/hooks`, { + method: 'POST', + body: JSON.stringify({ + type: 'gitea', + config: { + url: WWS_WEBHOOK_URL, + content_type: 'json', + secret: '', + }, + events: ['push', 'pull_request', 'issues', 'create', 'delete'], + active: true, + }), + }) + + return { success: true, repo, webhook } + } catch (error) { + return { + success: false, + error: error instanceof Error ? error.message : 'Failed to create repository', + } + } +} + +export async function getGiteaRepoUrl(slug: string): Promise { + return `${GITEA_URL}/${GITEA_OWNER}/${slug}` +} diff --git a/src/app/(dashboard)/projects/new/page.tsx b/src/app/(dashboard)/projects/new/page.tsx index b3d49f8..7cd3635 100644 --- a/src/app/(dashboard)/projects/new/page.tsx +++ b/src/app/(dashboard)/projects/new/page.tsx @@ -8,13 +8,17 @@ import { Button } from '@/components/ui/button' import { Input } from '@/components/ui/input' import { Label } from '@/components/ui/label' import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card' -import { ArrowLeft, Loader2 } from 'lucide-react' +import { ArrowLeft, Loader2, GitBranch, Check } from 'lucide-react' +import { createGiteaRepo } from './actions' + +type CreationStep = 'idle' | 'creating_project' | 'creating_repo' | 'complete' export default function NewProjectPage() { const router = useRouter() const [name, setName] = useState('') const [description, setDescription] = useState('') const [isLoading, setIsLoading] = useState(false) + const [step, setStep] = useState('idle') const [error, setError] = useState(null) const handleSubmit = async (e: React.FormEvent) => { @@ -37,6 +41,9 @@ export default function NewProjectPage() { .replace(/[^a-z0-9]+/g, '-') .replace(/(^-|-$)/g, '') + // Step 1: Create Supabase project + setStep('creating_project') + // First, get or create the user's default team // eslint-disable-next-line @typescript-eslint/no-explicit-any let { data: team } = await (supabase as any) @@ -60,12 +67,13 @@ export default function NewProjectPage() { if (teamError) { setError('Failed to create team') setIsLoading(false) + setStep('idle') return } team = newTeam } - // Create the project + // Create the project (without gitea_repo initially) // eslint-disable-next-line @typescript-eslint/no-explicit-any const { data: project, error: projectError } = await (supabase as any) .from('projects') @@ -81,9 +89,34 @@ export default function NewProjectPage() { if (projectError) { setError(projectError.message) setIsLoading(false) + setStep('idle') return } + // Step 2: Create Gitea repository + setStep('creating_repo') + const repoResult = await createGiteaRepo(slug, description || name) + + if (repoResult.success && repoResult.repo) { + // Update project with gitea_repo URL + // eslint-disable-next-line @typescript-eslint/no-explicit-any + await (supabase as any) + .from('projects') + .update({ + gitea_repo: repoResult.repo.html_url, + gitea_webhook_id: repoResult.webhook?.id, + }) + .eq('id', project.id) + } else { + // Log warning but don't fail - project still created + console.warn('Gitea repo creation failed:', repoResult.error) + } + + setStep('complete') + + // Small delay to show completion state + await new Promise(resolve => setTimeout(resolve, 500)) + router.push(`/projects/${project.id}`) } @@ -127,11 +160,42 @@ export default function NewProjectPage() { {error && (

{error}

)} + {isLoading && ( +
+
+ {step === 'creating_project' ? ( + + ) : step === 'creating_repo' || step === 'complete' ? ( + + ) : ( +
+ )} + + Creating project + +
+
+ {step === 'creating_repo' ? ( + + ) : step === 'complete' ? ( + + ) : ( +
+ )} + + + Creating Gitea repository + +
+
+ )}