Skip to content

Commit a98e8d2

Browse files
committed
Implement Repo UI - Same URI format like Github (https://<url>/<ownerSlug>/<repoSlug>). (#25)
Signed-off-by: Dae❤️ <74119677+daeisbae@users.noreply.github.com>
1 parent 839b8a4 commit a98e8d2

File tree

10 files changed

+389
-2
lines changed

10 files changed

+389
-2
lines changed

package.json

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,12 +16,16 @@
1616
"@google/generative-ai": "^0.21.0",
1717
"@langchain/core": "^0.3.26",
1818
"axios": "^1.7.9",
19+
"class-variance-authority": "^0.7.1",
20+
"clsx": "^2.1.1",
1921
"dotenv": "^16.4.7",
2022
"langchain": "^0.3.7",
23+
"lucide-react": "^0.469.0",
2124
"next": "15.1.0",
2225
"pg": "^8.13.1",
2326
"react": "^19.0.0",
24-
"react-dom": "^19.0.0"
27+
"react-dom": "^19.0.0",
28+
"tailwind-merge": "^2.6.0"
2529
},
2630
"devDependencies": {
2731
"@babel/cli": "^7.26.4",
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import { Skeleton } from '@/components/ui/skeleton'
2+
3+
export default function Loading() {
4+
return (
5+
<div className="space-y-4">
6+
<Skeleton className="h-8 w-[250px]" />
7+
<Skeleton className="h-4 w-[200px]" />
8+
<Skeleton className="h-4 w-[150px]" />
9+
<div className="space-y-2">
10+
<Skeleton className="h-4 w-full" />
11+
<Skeleton className="h-4 w-full" />
12+
<Skeleton className="h-4 w-2/3" />
13+
</div>
14+
</div>
15+
)
16+
}
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
import { JSX, Suspense } from 'react'
2+
import { RepoCard } from '@/components/RepoCard'
3+
import Loading from '@/app/[ownerSlug]/[repoSlug]/loading'
4+
import { FetchRepoService, FullRepository } from '@/db/get-db'
5+
import { notFound } from 'next/navigation'
6+
7+
interface PageProps {
8+
params: {
9+
ownerSlug: string
10+
repoSlug: string
11+
}
12+
}
13+
14+
export default async function RepoPage({
15+
params,
16+
}: PageProps): Promise<JSX.Element> {
17+
const { ownerSlug, repoSlug } = await params
18+
console.log(ownerSlug, repoSlug)
19+
20+
const fetchRepoService = new FetchRepoService()
21+
const repoDetails: FullRepository | null =
22+
await fetchRepoService.getFullRepositoryTree(ownerSlug, repoSlug)
23+
24+
if (!repoDetails) {
25+
console.log('Repo not found')
26+
notFound()
27+
}
28+
29+
return (
30+
<Suspense fallback={<Loading />}>
31+
<RepoCard repoInfo={repoDetails.repository} />
32+
</Suspense>
33+
)
34+
}

src/components/NavBar.tsx

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
import * as React from 'react'
2+
import Link from 'next/link'
3+
4+
import { cn } from '@/lib/utils'
5+
6+
import {
7+
NavigationMenu,
8+
NavigationMenuItem,
9+
NavigationMenuLink,
10+
NavigationMenuList,
11+
navigationMenuTriggerStyle,
12+
} from '@/components/ui/navigation-menu'
13+
14+
export default function Navbar() {
15+
return (
16+
<div className="border-b">
17+
<div className="flex h-16 items-center px-4">
18+
<Link href="/" className="mr-6 flex items-center space-x-2">
19+
<span className="text-xl font-bold">OpenRepoWiki</span>
20+
</Link>
21+
<NavigationMenu className="hidden md:flex">
22+
<NavigationMenuList>
23+
<NavigationMenuItem>
24+
<Link href="/repositories" legacyBehavior passHref>
25+
<NavigationMenuLink
26+
className={cn(
27+
navigationMenuTriggerStyle(),
28+
'w-28'
29+
)}
30+
>
31+
Repo
32+
</NavigationMenuLink>
33+
</Link>
34+
</NavigationMenuItem>
35+
<NavigationMenuItem>
36+
<Link href="/blogs" legacyBehavior passHref>
37+
<NavigationMenuLink
38+
className={cn(
39+
navigationMenuTriggerStyle(),
40+
'w-28'
41+
)}
42+
>
43+
Blogs
44+
</NavigationMenuLink>
45+
</Link>
46+
</NavigationMenuItem>
47+
<NavigationMenuItem>
48+
<Link href="/about" legacyBehavior passHref>
49+
<NavigationMenuLink
50+
className={cn(
51+
navigationMenuTriggerStyle(),
52+
'w-28 h-auto'
53+
)}
54+
>
55+
About
56+
</NavigationMenuLink>
57+
</Link>
58+
</NavigationMenuItem>
59+
</NavigationMenuList>
60+
</NavigationMenu>
61+
</div>
62+
</div>
63+
)
64+
}

src/components/RepoCard.tsx

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
import { Card, CardHeader, CardContent, CardTitle } from '@/components/ui/card'
2+
import { Badge } from '@/components/ui/badge'
3+
import { Separator } from '@/components/ui/separator'
4+
import { Star, GitFork } from 'lucide-react'
5+
import React from 'react'
6+
7+
export interface RepoDetails {
8+
owner: string
9+
repo: string
10+
descriptions: string
11+
stars: number
12+
forks: number
13+
url: string
14+
default_branch: string
15+
topics: string[]
16+
}
17+
18+
export interface RepoCardProps {
19+
repoInfo: RepoDetails
20+
}
21+
22+
export function RepoCard({ repoInfo }: RepoCardProps) {
23+
return (
24+
<Card>
25+
<CardHeader>
26+
<CardTitle>
27+
{repoInfo.owner}/{repoInfo.repo}
28+
</CardTitle>
29+
</CardHeader>
30+
<CardContent>
31+
<p className="text-sm text-muted-foreground mb-4">
32+
{repoInfo.descriptions}
33+
</p>
34+
<div className="flex items-center gap-4 mb-4">
35+
<div className="flex items-center">
36+
<Star className="w-4 h-4 mr-1" />
37+
<span>{repoInfo.stars}</span>
38+
</div>
39+
<div className="flex items-center">
40+
<GitFork className="w-4 h-4 mr-1" />
41+
<span>{repoInfo.forks}</span>
42+
</div>
43+
</div>
44+
<a
45+
href={repoInfo.url}
46+
target="_blank"
47+
rel="noopener noreferrer"
48+
className="text-primary hover:underline"
49+
>
50+
View on GitHub
51+
</a>
52+
<Separator className="my-4" />
53+
<div>
54+
<h4 className="font-semibold mb-2">Topics</h4>
55+
<div className="flex flex-wrap gap-2">
56+
{repoInfo.topics.map((topic, index) => (
57+
<Badge key={index} variant="secondary">
58+
{topic}
59+
</Badge>
60+
))}
61+
</div>
62+
</div>
63+
</CardContent>
64+
</Card>
65+
)
66+
}

src/components/ui/navigation-menu.jsx

Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
import * as React from "react"
2+
import * as NavigationMenuPrimitive from "@radix-ui/react-navigation-menu"
3+
import { cva } from "class-variance-authority"
4+
import { ChevronDown } from "lucide-react"
5+
6+
import { cn } from "@/lib/utils"
7+
8+
const NavigationMenu = React.forwardRef(({ className, children, ...props }, ref) => (
9+
<NavigationMenuPrimitive.Root
10+
ref={ref}
11+
className={cn(
12+
"relative z-10 flex max-w-max flex-1 items-center justify-center",
13+
className
14+
)}
15+
{...props}>
16+
{children}
17+
<NavigationMenuViewport />
18+
</NavigationMenuPrimitive.Root>
19+
))
20+
NavigationMenu.displayName = NavigationMenuPrimitive.Root.displayName
21+
22+
const NavigationMenuList = React.forwardRef(({ className, ...props }, ref) => (
23+
<NavigationMenuPrimitive.List
24+
ref={ref}
25+
className={cn(
26+
"group flex flex-1 list-none items-center justify-center space-x-1",
27+
className
28+
)}
29+
{...props} />
30+
))
31+
NavigationMenuList.displayName = NavigationMenuPrimitive.List.displayName
32+
33+
const NavigationMenuItem = NavigationMenuPrimitive.Item
34+
35+
const navigationMenuTriggerStyle = cva(
36+
"group inline-flex h-9 w-max items-center justify-center rounded-md bg-white px-4 py-2 text-sm font-medium transition-colors hover:bg-neutral-100 hover:text-neutral-900 focus:bg-neutral-100 focus:text-neutral-900 focus:outline-none disabled:pointer-events-none disabled:opacity-50 data-[active]:bg-neutral-100/50 data-[state=open]:bg-neutral-100/50 dark:bg-neutral-950 dark:hover:bg-neutral-800 dark:hover:text-neutral-50 dark:focus:bg-neutral-800 dark:focus:text-neutral-50 dark:data-[active]:bg-neutral-800/50 dark:data-[state=open]:bg-neutral-800/50"
37+
)
38+
39+
const NavigationMenuTrigger = React.forwardRef(({ className, children, ...props }, ref) => (
40+
<NavigationMenuPrimitive.Trigger
41+
ref={ref}
42+
className={cn(navigationMenuTriggerStyle(), "group", className)}
43+
{...props}>
44+
{children}{""}
45+
<ChevronDown
46+
className="relative top-[1px] ml-1 h-3 w-3 transition duration-300 group-data-[state=open]:rotate-180"
47+
aria-hidden="true" />
48+
</NavigationMenuPrimitive.Trigger>
49+
))
50+
NavigationMenuTrigger.displayName = NavigationMenuPrimitive.Trigger.displayName
51+
52+
const NavigationMenuContent = React.forwardRef(({ className, ...props }, ref) => (
53+
<NavigationMenuPrimitive.Content
54+
ref={ref}
55+
className={cn(
56+
"left-0 top-0 w-full data-[motion^=from-]:animate-in data-[motion^=to-]:animate-out data-[motion^=from-]:fade-in data-[motion^=to-]:fade-out data-[motion=from-end]:slide-in-from-right-52 data-[motion=from-start]:slide-in-from-left-52 data-[motion=to-end]:slide-out-to-right-52 data-[motion=to-start]:slide-out-to-left-52 md:absolute md:w-auto",
57+
className
58+
)}
59+
{...props} />
60+
))
61+
NavigationMenuContent.displayName = NavigationMenuPrimitive.Content.displayName
62+
63+
const NavigationMenuLink = NavigationMenuPrimitive.Link
64+
65+
const NavigationMenuViewport = React.forwardRef(({ className, ...props }, ref) => (
66+
<div className={cn("absolute left-0 top-full flex justify-center")}>
67+
<NavigationMenuPrimitive.Viewport
68+
className={cn(
69+
"origin-top-center relative mt-1.5 h-[var(--radix-navigation-menu-viewport-height)] w-full overflow-hidden rounded-md border border-neutral-200 bg-white text-neutral-950 shadow data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-90 md:w-[var(--radix-navigation-menu-viewport-width)] dark:border-neutral-800 dark:bg-neutral-950 dark:text-neutral-50",
70+
className
71+
)}
72+
ref={ref}
73+
{...props} />
74+
</div>
75+
))
76+
NavigationMenuViewport.displayName =
77+
NavigationMenuPrimitive.Viewport.displayName
78+
79+
const NavigationMenuIndicator = React.forwardRef(({ className, ...props }, ref) => (
80+
<NavigationMenuPrimitive.Indicator
81+
ref={ref}
82+
className={cn(
83+
"top-full z-[1] flex h-1.5 items-end justify-center overflow-hidden data-[state=visible]:animate-in data-[state=hidden]:animate-out data-[state=hidden]:fade-out data-[state=visible]:fade-in",
84+
className
85+
)}
86+
{...props}>
87+
<div
88+
className="relative top-[60%] h-2 w-2 rotate-45 rounded-tl-sm bg-neutral-200 shadow-md dark:bg-neutral-800" />
89+
</NavigationMenuPrimitive.Indicator>
90+
))
91+
NavigationMenuIndicator.displayName =
92+
NavigationMenuPrimitive.Indicator.displayName
93+
94+
export {
95+
navigationMenuTriggerStyle,
96+
NavigationMenu,
97+
NavigationMenuList,
98+
NavigationMenuItem,
99+
NavigationMenuContent,
100+
NavigationMenuTrigger,
101+
NavigationMenuLink,
102+
NavigationMenuIndicator,
103+
NavigationMenuViewport,
104+
}

src/components/ui/skeleton.jsx

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
import { cn } from "@/lib/utils"
2+
3+
function Skeleton({
4+
className,
5+
...props
6+
}) {
7+
return (
8+
(<div
9+
className={cn(
10+
"animate-pulse rounded-md bg-neutral-900/10 dark:bg-neutral-50/10",
11+
className
12+
)}
13+
{...props} />)
14+
);
15+
}
16+
17+
export { Skeleton }

src/db/get-db.ts

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
import { Repository, RepositoryData } from '@/db/models/repository'
2+
import { Branch, BranchData } from '@/db/models/branch'
3+
import { Folder, FolderData } from '@/db/models/folder'
4+
import { File, FileData } from '@/db/models/file'
5+
6+
export interface FullFolder extends FolderData {
7+
files: FileData[]
8+
subfolders: FullFolder[]
9+
}
10+
11+
export interface FullRepository {
12+
repository: RepositoryData
13+
branch: BranchData | null
14+
folders: FullFolder[]
15+
}
16+
17+
export class FetchRepoService {
18+
private repository = new Repository()
19+
private branch = new Branch()
20+
private folder = new Folder()
21+
private file = new File()
22+
23+
async getFullRepositoryTree(
24+
owner: string,
25+
repo: string
26+
): Promise<FullRepository | null> {
27+
const repositoryData = await this.repository.select(owner, repo)
28+
29+
if (!repositoryData) return null
30+
31+
const branchData = await this.branch.select(repositoryData.url)
32+
const folders: FullFolder[] = branchData
33+
? await this.getFoldersRecursively(branchData.branch_id, null)
34+
: []
35+
36+
// if (!branchData) {
37+
// throw new Error('No branch found for the repository')
38+
// }
39+
40+
return {
41+
repository: repositoryData,
42+
branch: branchData,
43+
folders,
44+
}
45+
}
46+
47+
private async getFoldersRecursively(
48+
branchId: number,
49+
parentFolderId: number | null
50+
): Promise<FullFolder[]> {
51+
const allFolders = await this.folder.select(branchId)
52+
if (!allFolders) throw new Error('No folders found for the repository')
53+
54+
const currentLevel = allFolders.filter(
55+
(f) => f.parent_folder_id === parentFolderId
56+
)
57+
58+
const foldersWithChildren: FullFolder[] = []
59+
for (const folder of currentLevel) {
60+
const subfolders = await this.getFoldersRecursively(
61+
branchId,
62+
folder.folder_id
63+
)
64+
const files = await this.file.select(folder.folder_id)
65+
foldersWithChildren.push({
66+
...folder,
67+
files,
68+
subfolders,
69+
})
70+
}
71+
return foldersWithChildren
72+
}
73+
}

0 commit comments

Comments
 (0)