Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,14 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co
- API routes: kebab-case
- Files: kebab-case

### Component Organization
- **Route-specific components**: Place in `_components/` directory under the route folder
- Use `_` prefix to prevent Next.js from treating it as a route
- Example: `app/(dashboard)/settings/_components/github-status-card.tsx`
- **Shared components**: Place in top-level `components/` directory
- Only for components used across multiple routes
- Example: `components/ui/button.tsx`, `components/sidebar.tsx`

### Important Patterns

1. **Always use user-specific K8s service**:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,10 @@
import { useState } from 'react'
import { MdAdd } from 'react-icons/md'

import CreateProjectDialog from '@/components/dialog/create-project-dialog'
import { cn } from '@/lib/utils'

import CreateProjectDialog from '../../_components/create-project-dialog'

export function CreateProjectCard() {
const [isDialogOpen, setIsDialogOpen] = useState(false)

Expand Down
3 changes: 2 additions & 1 deletion app/(dashboard)/projects/(list)/layout.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { SearchBar } from '@/components/search-bar'
import { Sidebar } from '@/components/sidebar'

import { SearchBar } from '../_components/search-bar'

export default function HomeLayout({
children,
}: {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ export function ImportGitHubDialog({ open, onOpenChange }: ImportGitHubDialogPro

const resetState = useCallback(() => {
setStep('check-github-app')
setIsLoading(false)
setIsLoading(true)
setSearchQuery('')
setHasInstallation(false)
setRepos([])
Expand Down Expand Up @@ -173,7 +173,7 @@ export function ImportGitHubDialog({ open, onOpenChange }: ImportGitHubDialogPro
}

return (
<div className="space-y-4">
<div className="space-y-4 w-full">
<div className="p-4 bg-muted/50 border border-border rounded-lg">
<p className="text-sm text-muted-foreground">
Install the GitHub App to grant access to your repositories.
Expand All @@ -193,29 +193,30 @@ export function ImportGitHubDialog({ open, onOpenChange }: ImportGitHubDialogPro

case 'select-repo':
return (
<div className="space-y-4">
<div className="space-y-4 w-full min-w-0">
<Input
placeholder="Search repositories..."
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
className="bg-input border-border"
/>

<ScrollArea className="h-[300px]">
{isLoading ? (
<div className="flex items-center justify-center py-8">
<div className="flex items-center gap-2 text-muted-foreground">
<MdRefresh className="w-4 h-4 animate-spin" />
<span>Loading repositories...</span>
<ScrollArea className="h-[300px] w-full">
<div className="pr-4">
{isLoading ? (
<div className="flex items-center justify-center py-8">
<div className="flex items-center gap-2 text-muted-foreground">
<MdRefresh className="w-4 h-4 animate-spin" />
<span>Loading repositories...</span>
</div>
</div>
</div>
) : filteredRepos.length === 0 ? (
<div className="flex items-center justify-center py-8 text-muted-foreground">
No repositories found
</div>
) : (
<div className="space-y-2 pr-4">
{filteredRepos.map((repo) => (
) : filteredRepos.length === 0 ? (
<div className="flex items-center justify-center py-8 text-muted-foreground">
No repositories found
</div>
) : (
<div className="space-y-2">
{filteredRepos.map((repo) => (
<button
key={repo.id}
onClick={() => handleSelectRepo(repo)}
Expand All @@ -225,32 +226,21 @@ export function ImportGitHubDialog({ open, onOpenChange }: ImportGitHubDialogPro
: 'bg-card/50 border-border hover:bg-secondary/50'
}`}
>
<div className="flex items-center justify-between">
<div className="flex-1 min-w-0">
<div className="flex items-center gap-2">
<span className="text-sm font-medium text-foreground truncate">
{repo.full_name}
</span>
{repo.private && (
<span className="text-xs px-1.5 py-0.5 bg-yellow-500/20 text-yellow-600 dark:text-yellow-500 rounded">
Private
</span>
)}
</div>
{repo.description && (
<p className="text-xs text-muted-foreground mt-1 truncate">
{repo.description}
</p>
)}
</div>
{repo.language && (
<span className="text-xs text-muted-foreground ml-2">{repo.language}</span>
<div className="flex items-center gap-2">
<span className="text-sm font-medium text-foreground truncate">
{repo.full_name}
</span>
{repo.private && (
<span className="text-xs px-1.5 py-0.5 bg-yellow-500/20 text-yellow-600 dark:text-yellow-500 rounded shrink-0 whitespace-nowrap">
Private
</span>
)}
</div>
</button>
))}
</div>
)}
</div>
</ScrollArea>

{selectedRepo && (
Expand Down Expand Up @@ -291,12 +281,12 @@ export function ImportGitHubDialog({ open, onOpenChange }: ImportGitHubDialogPro

return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="max-w-lg bg-card border-border text-foreground">
<DialogContent className="bg-card border-border text-foreground max-w-2xl">
<DialogHeader>
<DialogTitle className="text-xl text-foreground">{getStepTitle()}</DialogTitle>
</DialogHeader>

<div className="py-4">{renderStepContent()}</div>
<div className="py-4 w-full min-w-0">{renderStepContent()}</div>

{/* Step indicators */}
<div className="flex items-center justify-center gap-2 pt-2 border-t border-border">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,13 @@
import { useState } from 'react'
import { MdAdd, MdSearch } from 'react-icons/md'

import CreateProjectDialog from '@/components/dialog/create-project-dialog'
import { ImportGitHubDialog } from '@/components/dialog/import-github-dialog'
import { Button } from '@/components/ui/button'
import { Input } from '@/components/ui/input'
import { Kbd } from '@/components/ui/kbd'

import CreateProjectDialog from './create-project-dialog'
import { ImportGitHubDialog } from './import-github-dialog'

export function SearchBar() {
const [isDialogOpen, setIsDialogOpen] = useState(false)
const [isImportDialogOpen, setIsImportDialogOpen] = useState(false)
Expand Down
160 changes: 160 additions & 0 deletions app/(dashboard)/settings/_components/github-status-card.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,160 @@
'use client'

import { useEffect, useState } from 'react'
import { FaGithub } from 'react-icons/fa'
import { MdCheck } from 'react-icons/md'
import Image from 'next/image'
import { toast } from 'sonner'

import { Button } from '@/components/ui/button'
import { getInstallations, type GitHubInstallation } from '@/lib/actions/github'
import { env } from '@/lib/env'

export function GitHubStatusCard() {
const [isLoading, setIsLoading] = useState(true)
const [installation, setInstallation] = useState<GitHubInstallation | null>(null)

useEffect(() => {
loadData()
}, [])

const loadData = async () => {
setIsLoading(true)
try {
const installationsResult = await getInstallations()

if (installationsResult.success && installationsResult.data.length > 0) {
setInstallation(installationsResult.data[0])
}
} catch (error) {
console.error('Failed to load GitHub data:', error)
} finally {
setIsLoading(false)
}
}

const handleInstallApp = () => {
const appName = env.NEXT_PUBLIC_GITHUB_APP_NAME
if (!appName) {
toast.error('GitHub App is not configured')
return
}

const installUrl = `https://github.com/apps/${appName}/installations/new`

const width = 800
const height = 800
const left = window.screen.width / 2 - width / 2
const top = window.screen.height / 2 - height / 2

const popup = window.open(
installUrl,
'github-app-install',
`width=${width},height=${height},left=${left},top=${top},resizable=yes,scrollbars=yes`
)

if (!popup) {
toast.error('Failed to open popup window. Please allow popups for this site.')
return
}

const checkClosed = setInterval(() => {
if (popup.closed) {
clearInterval(checkClosed)
window.removeEventListener('message', handleMessage)
}
}, 500)

const handleMessage = (event: MessageEvent) => {
if (event.origin !== window.location.origin) return
if (event.data.type !== 'github-app-installed') return

window.removeEventListener('message', handleMessage)
clearInterval(checkClosed)
toast.success('GitHub App installed successfully!')
loadData()
}

window.addEventListener('message', handleMessage)
}

if (isLoading) {
return (
<div className="py-6 animate-pulse">
<div className="flex gap-8">
<div className="flex-shrink-0 space-y-4">
<div className="h-6 w-32 bg-muted/50 rounded" />
<div className="w-20 h-20 bg-muted/50 rounded-xl" />
</div>
<div className="flex-1 space-y-3">
<div className="h-5 w-48 bg-muted/50 rounded" />
<div className="h-4 w-full bg-muted/50 rounded" />
<div className="h-4 w-3/4 bg-muted/50 rounded" />
<div className="h-9 w-36 bg-muted/50 rounded" />
</div>
</div>
</div>
)
}

return (
<div className="py-6">
<div className="flex gap-8">
<div className="flex-shrink-0">
<h3 className="text-lg font-medium text-foreground mb-4">GitHub Integration</h3>
<div className="w-20 h-20 bg-secondary/50 border border-border rounded-xl flex items-center justify-center">
{installation ? (
<div className="relative">
<FaGithub className="w-10 h-10 text-foreground" />
<div className="absolute -bottom-1 -right-1 w-5 h-5 bg-green-500 rounded-full flex items-center justify-center">
<MdCheck className="w-3 h-3 text-white" />
</div>
</div>
) : (
<FaGithub className="w-10 h-10 text-foreground" />
)}
</div>
</div>

<div className="flex-1">
<h4 className="text-sm font-medium text-foreground mb-2">
What does the GitHub Integration do?
</h4>
<p className="text-sm text-muted-foreground mb-4">
Connecting to GitHub allows you to sync your project to Fulling, and automatically deploys your production-ready project to Sealos.
</p>

{installation ? (
<div className="flex items-center gap-3">
{installation.accountAvatarUrl ? (
<Image
src={installation.accountAvatarUrl}
alt={installation.accountLogin}
width={32}
height={32}
className="rounded-full"
/>
) : (
<div className="w-8 h-8 bg-muted rounded-full flex items-center justify-center">
<FaGithub className="w-4 h-4 text-muted-foreground" />
</div>
)}
<span className="text-sm font-medium text-foreground">
{installation.accountLogin}
</span>
<span className="text-xs text-green-600 dark:text-green-500">● Connected</span>
</div>
) : (
<Button
onClick={handleInstallApp}
className="bg-primary hover:bg-primary/90 text-primary-foreground"
>
<FaGithub className="mr-2 h-4 w-4" />
Connect to GitHub
</Button>
)}
</div>
</div>
</div>
)
}
45 changes: 45 additions & 0 deletions app/(dashboard)/settings/_components/settings-sidebar.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
'use client'

import Link from 'next/link'
import { usePathname } from 'next/navigation'

import { Separator } from '@/components/ui/separator'
import { cn } from '@/lib/utils'

const menuItems = [
{ label: 'Integrations', href: '/settings/integrations' },
{ label: 'Account', href: '/settings/account' },
]

export function SettingsSidebar() {
const pathname = usePathname()

return (
<aside className="w-64 flex-shrink-0 border-r border-border bg-sidebar/50">
<div className="p-6">
<h2 className="text-sm font-semibold text-foreground">Settings</h2>
</div>
<Separator />
<nav className="flex flex-col gap-1 p-4">
{menuItems.map((item) => {
const isActive = pathname === item.href

return (
<Link
key={item.label}
href={item.href}
className={cn(
'flex items-center px-3 py-2 rounded-lg',
isActive ? 'text-white' : 'text-muted-foreground'
)}
>
<span className="text-sm font-medium font-display">
{item.label}
</span>
</Link>
)
})}
</nav>
</aside>
)
}
Loading