├── .env.example ├── .eslintrc.json ├── .gitignore ├── LICENSE ├── README.md ├── components.json ├── next.config.mjs ├── package-lock.json ├── package.json ├── postcss.config.js ├── prettier.config.js ├── public ├── next.svg └── vercel.svg ├── src ├── app │ ├── [slug] │ │ └── page.tsx │ ├── api │ │ ├── auth │ │ │ └── [...nextauth] │ │ │ │ └── route.ts │ │ ├── repo │ │ │ └── route.ts │ │ └── repos │ │ │ └── route.ts │ ├── apple-icon.png │ ├── favicon.ico │ ├── github-signin │ │ └── page.tsx │ ├── icon.png │ ├── layout.tsx │ ├── opengraph-image.png │ └── page.tsx ├── components │ ├── code-snippet.tsx │ ├── context-provider.tsx │ ├── header.tsx │ ├── hero.tsx │ ├── mdx.tsx │ ├── repos.tsx │ └── ui │ │ ├── button.tsx │ │ ├── input.tsx │ │ └── pagination.tsx ├── lib │ ├── auth.ts │ └── utils.ts ├── styles │ └── globals.css └── type │ ├── next-auth.d.ts │ └── types.tsx ├── tailwind.config.ts └── tsconfig.json /.env.example: -------------------------------------------------------------------------------- 1 | GITHUB_ID= 2 | GITHUB_SECRET= -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "next/core-web-vitals" 3 | } 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | .yarn/install-state.gz 8 | 9 | # testing 10 | /coverage 11 | 12 | # next.js 13 | /.next/ 14 | /out/ 15 | 16 | # production 17 | /build 18 | 19 | # misc 20 | .DS_Store 21 | *.pem 22 | 23 | # debug 24 | npm-debug.log* 25 | yarn-debug.log* 26 | yarn-error.log* 27 | 28 | # local env files 29 | .env 30 | .env*.local 31 | 32 | # vercel 33 | .vercel 34 | 35 | # typescript 36 | *.tsbuildinfo 37 | next-env.d.ts 38 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Hosna Qasmei 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app). 2 | 3 | ## Getting Started 4 | 5 | First, run the development server: 6 | 7 | ```bash 8 | npm run dev 9 | # or 10 | yarn dev 11 | # or 12 | pnpm dev 13 | # or 14 | bun dev 15 | ``` 16 | 17 | Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. 18 | 19 | You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file. 20 | 21 | This project uses [`next/font`](https://nextjs.org/docs/basic-features/font-optimization) to automatically optimize and load Inter, a custom Google Font. 22 | 23 | ## Learn More 24 | 25 | To learn more about Next.js, take a look at the following resources: 26 | 27 | - [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API. 28 | - [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial. 29 | 30 | You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your feedback and contributions are welcome! 31 | 32 | ## Deploy on Vercel 33 | 34 | The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js. 35 | 36 | Check out our [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more details. 37 | -------------------------------------------------------------------------------- /components.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://ui.shadcn.com/schema.json", 3 | "style": "default", 4 | "rsc": true, 5 | "tsx": true, 6 | "tailwind": { 7 | "config": "tailwind.config.ts", 8 | "css": "src/styles/globals.css", 9 | "baseColor": "slate", 10 | "cssVariables": true, 11 | "prefix": "" 12 | }, 13 | "aliases": { 14 | "components": "@/components", 15 | "utils": "@/lib/utils" 16 | } 17 | } -------------------------------------------------------------------------------- /next.config.mjs: -------------------------------------------------------------------------------- 1 | /** @type {import('next').NextConfig} */ 2 | const nextConfig = {}; 3 | 4 | export default nextConfig; 5 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "repo-mapper", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "dev": "next dev", 7 | "build": "next build", 8 | "start": "next start", 9 | "lint": "next lint" 10 | }, 11 | "dependencies": { 12 | "@ianvs/prettier-plugin-sort-imports": "^4.1.1", 13 | "@iconify/react": "^4.1.1", 14 | "@radix-ui/react-slot": "^1.0.2", 15 | "axios": "^1.6.7", 16 | "class-variance-authority": "^0.7.0", 17 | "clsx": "^2.1.0", 18 | "fuse.js": "^7.0.0", 19 | "lucide-react": "^0.325.0", 20 | "next": "14.1.0", 21 | "next-auth": "^4.24.5", 22 | "next-mdx-remote": "^4.4.1", 23 | "react": "^18", 24 | "react-copy-to-clipboard": "^5.1.0", 25 | "react-dom": "^18", 26 | "sugar-high": "^0.6.0", 27 | "tailwind-merge": "^2.2.1", 28 | "tailwindcss-animate": "^1.0.7" 29 | }, 30 | "devDependencies": { 31 | "@types/node": "^20", 32 | "@types/react": "^18", 33 | "@types/react-copy-to-clipboard": "^5.0.7", 34 | "@types/react-dom": "^18", 35 | "autoprefixer": "^10.0.1", 36 | "eslint": "^8", 37 | "eslint-config-next": "14.1.0", 38 | "postcss": "^8", 39 | "tailwindcss": "^3.3.0", 40 | "typescript": "^5" 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | }; 7 | -------------------------------------------------------------------------------- /prettier.config.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | 3 | /** @type {import("@ianvs/prettier-plugin-sort-imports").PrettierConfig} */ 4 | module.exports = { 5 | // Standard prettier options 6 | singleQuote: true, 7 | semi: true, 8 | // Since prettier 3.0, manually specifying plugins is required 9 | plugins: ['@ianvs/prettier-plugin-sort-imports'], 10 | // This plugin's options 11 | importOrder: [ 12 | '^(react/(.*)$)|^(react$)', 13 | '', 14 | '^(next/(.*)$)|^(next$)', 15 | '', 16 | '', 17 | '', 18 | '^~/(.*)$', 19 | '', 20 | '^[./]', 21 | ], 22 | importOrderParserPlugins: ['typescript', 'jsx', 'decorators-legacy'], 23 | importOrderTypeScriptVersion: '5.0.0', 24 | }; -------------------------------------------------------------------------------- /public/next.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/vercel.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/app/[slug]/page.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import React, { useEffect, useState } from 'react'; 4 | 5 | import Link from 'next/link'; 6 | import { useSearchParams } from 'next/navigation'; 7 | 8 | import CodeSnippet from '@/components/code-snippet'; 9 | import axios from 'axios'; 10 | import { ArrowLeft, Star } from 'lucide-react'; 11 | import { useSession } from 'next-auth/react'; 12 | 13 | const parseRepoData = (data: any[], prefix: string = '') => { 14 | let structure = ''; 15 | 16 | data.forEach((item) => { 17 | if (item.type === 'dir') { 18 | // For directories 19 | structure += `${prefix}├── ${item.name}\n`; 20 | // Placeholder for nested directory contents 21 | // Assuming `item.contents` holds an array of contents if they were fetched 22 | if (item.contents) { 23 | structure += parseRepoData(item.contents, prefix + '| '); 24 | } 25 | } else { 26 | // For files 27 | structure += `${prefix}| ├── ${item.name}\n`; 28 | } 29 | }); 30 | 31 | return structure; 32 | }; 33 | 34 | export default function ProjectPage({ params }: { params: { slug: string } }) { 35 | const { data: session } = useSession(); 36 | const [loading, setLoading] = useState(false); 37 | const [error, setError] = useState(''); 38 | const searchParams = useSearchParams(); 39 | const owner = searchParams.get('owner'); 40 | const [repoStructure, setRepoStructure] = useState(''); 41 | 42 | useEffect(() => { 43 | async function fetchGithubRepo() { 44 | const response = await axios.post('/api/repo', { 45 | owner: owner, 46 | repoName: params.slug, 47 | token: session?.accessToken, 48 | }); 49 | 50 | const respoData = response.data; 51 | 52 | const structure = parseRepoData(respoData); 53 | setRepoStructure(structure); 54 | } 55 | 56 | fetchGithubRepo(); 57 | }, [session, owner, params]); 58 | 59 | if (loading) return
Loading...
; 60 | if (error) return
Error: {error}
; 61 | 62 | return ( 63 |
64 | 65 | 69 | Back 70 | 71 | {params.slug} 72 |
73 | 74 |
75 |
76 | ); 77 | } 78 | -------------------------------------------------------------------------------- /src/app/api/auth/[...nextauth]/route.ts: -------------------------------------------------------------------------------- 1 | import { authConfig } from '@/lib/auth'; 2 | import NextAuth from 'next-auth'; 3 | 4 | const handler = NextAuth(authConfig); 5 | 6 | export { handler as GET, handler as POST }; 7 | -------------------------------------------------------------------------------- /src/app/api/repo/route.ts: -------------------------------------------------------------------------------- 1 | import url from 'url'; 2 | 3 | import { NextResponse } from 'next/server'; 4 | 5 | async function fetchDirectoryContents( 6 | owner: any, 7 | repoName: any, 8 | path: any, 9 | token: any, 10 | ) { 11 | const apiUrl = `https://api.github.com/repos/${owner}/${repoName}/contents/${path}`; 12 | const response = await fetch(apiUrl, { 13 | headers: { 14 | Authorization: `token ${token}`, 15 | }, 16 | }); 17 | 18 | if (!response.ok) { 19 | throw new Error(`Failed to fetch ${apiUrl}: ${response.statusText}`); 20 | } 21 | 22 | let contents = await response.json(); 23 | 24 | // Sort contents: directories first, then files 25 | contents.sort((a: any, b: any) => { 26 | if (a.type === 'dir' && b.type !== 'dir') { 27 | return -1; // a is a directory, b is a file, a comes first 28 | } else if (a.type !== 'dir' && b.type === 'dir') { 29 | return 1; // a is a file, b is a directory, b comes first 30 | } else { 31 | return a.name.localeCompare(b.name); // Both are files or directories, sort alphabetically 32 | } 33 | }); 34 | 35 | for (let i = 0; i < contents.length; i++) { 36 | const item = contents[i]; 37 | if (item.type === 'dir') { 38 | // Recursively fetch contents for directories 39 | const subContents = await fetchDirectoryContents( 40 | owner, 41 | repoName, 42 | item.path, 43 | token, 44 | ); 45 | item.contents = subContents; // Attach the contents to the directory item 46 | } 47 | } 48 | 49 | return contents; 50 | } 51 | 52 | export async function POST(req: Request) { 53 | try { 54 | const body = await req.json(); 55 | const { owner, repoName, token } = body; 56 | 57 | const data = await fetchDirectoryContents(owner, repoName, '', token); 58 | 59 | return NextResponse.json(data || null); 60 | } catch (error) { 61 | console.log('[PROJECT_POST]', error); 62 | return new NextResponse('Internal Error', { status: 500 }); 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/app/api/repos/route.ts: -------------------------------------------------------------------------------- 1 | import url from 'url'; 2 | 3 | import { NextResponse } from 'next/server'; 4 | 5 | import { Repository } from '@/type/types'; 6 | 7 | export async function POST(req: Request) { 8 | try { 9 | const body = await req.json(); 10 | const { token } = body; 11 | const perPage = 100; 12 | let repos: Repository[] = []; 13 | let page = 1; 14 | let hasMore = true; 15 | 16 | while (hasMore) { 17 | const response = await fetch( 18 | `https://api.github.com/user/repos?per_page=${perPage}&page=${page}`, 19 | { 20 | headers: { 21 | Authorization: `token ${token}`, 22 | }, 23 | }, 24 | ); 25 | 26 | if (!response.ok) { 27 | throw new Error(`GitHub API responded with status ${response.status}`); 28 | } 29 | 30 | const data = await response.json(); 31 | repos = repos.concat(data); 32 | 33 | if (data.length < perPage) { 34 | hasMore = false; 35 | } else { 36 | page++; 37 | } 38 | } 39 | 40 | return NextResponse.json(repos || null); 41 | } catch (error) { 42 | console.log('[PROJECT_POST]', error); 43 | return new NextResponse('Internal Error', { status: 500 }); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/app/apple-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hqasmei/repo-mapper/e5cddf677c07fbc821d66bc57563c95013b884e7/src/app/apple-icon.png -------------------------------------------------------------------------------- /src/app/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hqasmei/repo-mapper/e5cddf677c07fbc821d66bc57563c95013b884e7/src/app/favicon.ico -------------------------------------------------------------------------------- /src/app/github-signin/page.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import React, { useEffect } from 'react'; 4 | 5 | import { signIn, useSession } from 'next-auth/react'; 6 | 7 | export default function GithubSigninPage() { 8 | const { data: session, status } = useSession(); 9 | 10 | useEffect(() => { 11 | if (!(status === 'loading') && !session) { 12 | signIn('github'); 13 | } 14 | if (session) { 15 | window.close(); 16 | } 17 | }, [session, status]); 18 | 19 | return ( 20 |
30 | ); 31 | } 32 | -------------------------------------------------------------------------------- /src/app/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hqasmei/repo-mapper/e5cddf677c07fbc821d66bc57563c95013b884e7/src/app/icon.png -------------------------------------------------------------------------------- /src/app/layout.tsx: -------------------------------------------------------------------------------- 1 | import type { Metadata } from 'next'; 2 | import { Inter } from 'next/font/google'; 3 | 4 | import '../styles/globals.css'; 5 | 6 | import { ContextProvider } from '@/components/context-provider'; 7 | import Header from '@/components/header'; 8 | 9 | const inter = Inter({ subsets: ['latin'] }); 10 | 11 | export const metadata: Metadata = { 12 | title: 'RepoMapper', 13 | description: 'Turn your github repo into a visual folder structure.', 14 | }; 15 | 16 | export default function RootLayout({ 17 | children, 18 | }: Readonly<{ 19 | children: React.ReactNode; 20 | }>) { 21 | return ( 22 | 23 | 24 | 30 | 36 | 41 | 42 | 43 | 44 |
45 |
46 |
47 |
48 | {children} 49 |
50 |
51 |
52 |
53 | 54 | 55 | ); 56 | } 57 | -------------------------------------------------------------------------------- /src/app/opengraph-image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hqasmei/repo-mapper/e5cddf677c07fbc821d66bc57563c95013b884e7/src/app/opengraph-image.png -------------------------------------------------------------------------------- /src/app/page.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import Hero from '@/components/hero'; 4 | import Repos from '@/components/repos'; 5 | import { Button } from '@/components/ui/button'; 6 | import { signOut, useSession } from 'next-auth/react'; 7 | 8 | export default function Home() { 9 | const { data: session, status } = useSession(); 10 | if (!session?.user) { 11 | return ( 12 |
13 | 14 |
15 | ); 16 | } 17 | return ( 18 |
19 |
20 |
21 |
22 |

Welcome {session.user.name}!

23 | 24 |
25 |
26 |
27 | 28 |
29 | ); 30 | } 31 | -------------------------------------------------------------------------------- /src/components/code-snippet.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | 3 | import { CopyToClipboard } from 'react-copy-to-clipboard'; 4 | 5 | const CodeSnippet = ({ code, width }: { code: any; width: string }) => { 6 | const [copied, setCopied] = useState(false); 7 | 8 | return ( 9 |
12 |
13 |         {code}
14 |         
15 | setCopied(true)}> 16 | 21 | 22 |
23 |
24 |
25 | ); 26 | }; 27 | 28 | export default CodeSnippet; 29 | -------------------------------------------------------------------------------- /src/components/context-provider.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import { SessionProvider } from 'next-auth/react'; 4 | 5 | export function ContextProvider({ children }: { children: React.ReactNode }) { 6 | return {children}; 7 | } 8 | -------------------------------------------------------------------------------- /src/components/header.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import React from 'react'; 4 | 5 | import Link from 'next/link'; 6 | 7 | import { FolderTree } from 'lucide-react'; 8 | 9 | export default function Header() { 10 | return ( 11 | 12 | 13 | 14 | 15 |

RepoMapper

16 | 17 | ); 18 | } 19 | -------------------------------------------------------------------------------- /src/components/hero.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import React from 'react'; 4 | 5 | import { Button } from '@/components/ui/button'; 6 | import { Icon } from '@iconify/react'; 7 | 8 | import CodeSnippet from './code-snippet'; 9 | 10 | export default function Hero() { 11 | const popupCenter = (url: string) => { 12 | const dualScreenLeft = window.screenLeft ?? window.screenX; 13 | const dualScreenTop = window.screenTop ?? window.screenY; 14 | 15 | const width = 16 | window.innerWidth ?? document.documentElement.clientWidth ?? screen.width; 17 | const height = 18 | window.innerHeight ?? 19 | document.documentElement.clientHeight ?? 20 | screen.height; 21 | 22 | const systemZoom = width / window.screen.availWidth; 23 | const left = (width - 500) / 2 / systemZoom + dualScreenLeft; 24 | const top = (height - 550) / 2 / systemZoom + dualScreenTop; 25 | 26 | const newWindow = window.open( 27 | url, 28 | 'OAuthSignIn', // window name 29 | `width=${500 / systemZoom},height=${ 30 | 550 / systemZoom 31 | },top=${top},left=${left}`, 32 | ); 33 | 34 | if (newWindow) newWindow.focus(); 35 | else 36 | alert( 37 | 'Failed to open the window, it may have been blocked by a popup blocker.', 38 | ); 39 | }; 40 | const code = ` 41 | ├── public 42 | | | ├── next.svg 43 | | | ├── vercel.svg 44 | ├── src 45 | | ├── app 46 | | | | ├── favicon.ico 47 | | | | ├── globals.css 48 | | | | ├── layout.tsx 49 | | | | ├── page.tsx 50 | | ├── .eslintrc.json 51 | | ├── .gitignore 52 | | ├── next.config.js 53 | | ├── package-lock.json 54 | | ├── package.json 55 | | ├── postcss.config.js 56 | | ├── README.md 57 | | ├── tailwind.config.ts 58 | | ├── tsconfig.json 59 | `; 60 | 61 | return ( 62 |
63 |
64 | 65 | Visualize Your GitHub Repos in ASCII. 66 | 67 | 68 | RepoMapper converts your GitHub repository into clear ASCII diagrams, 69 | enhancing documentation and code organization understanding with an 70 | invaluable visualization tool. 71 | 72 |
73 | 80 |
81 |
82 | 83 |
84 | 85 |
86 |
87 | ); 88 | } 89 | -------------------------------------------------------------------------------- /src/components/mdx.tsx: -------------------------------------------------------------------------------- 1 | import React, { createElement } from 'react'; 2 | 3 | import Image from 'next/image'; 4 | import Link from 'next/link'; 5 | 6 | import { MDXRemote } from 'next-mdx-remote/rsc'; 7 | import { highlight } from 'sugar-high'; 8 | 9 | function Table({ data }: { data: any }) { 10 | let headers = data.headers.map((header: any, index: any) => ( 11 | {header} 12 | )); 13 | let rows = data.rows.map((row: any, index: any) => ( 14 | 15 | {row.map((cell: any, cellIndex: any) => ( 16 | {cell} 17 | ))} 18 | 19 | )); 20 | 21 | return ( 22 | 23 | 24 | {headers} 25 | 26 | {rows} 27 |
28 | ); 29 | } 30 | 31 | function CustomLink(props: any) { 32 | let href = props.href; 33 | 34 | if (href.startsWith('/')) { 35 | return ( 36 | 37 | {props.children} 38 | 39 | ); 40 | } 41 | 42 | if (href.startsWith('#')) { 43 | return ; 44 | } 45 | 46 | return ; 47 | } 48 | 49 | function RoundedImage(props: any) { 50 | return {props.alt}; 51 | } 52 | 53 | function Callout(props: any) { 54 | return ( 55 |
56 |
{props.emoji}
57 |
{props.children}
58 |
59 | ); 60 | } 61 | 62 | function Code({ children, ...props }: { children: any }) { 63 | let codeHTML = highlight(children); 64 | return ; 65 | } 66 | 67 | function slugify(str: any) { 68 | return str 69 | .toString() 70 | .toLowerCase() 71 | .trim() // Remove whitespace from both ends of a string 72 | .replace(/\s+/g, '-') // Replace spaces with - 73 | .replace(/&/g, '-and-') // Replace & with 'and' 74 | .replace(/[^\w\-]+/g, '') // Remove all non-word characters except for - 75 | .replace(/\-\-+/g, '-'); // Replace multiple - with single - 76 | } 77 | 78 | // function createHeading(level: any) { 79 | // return ({ children }: { children: any }) => { 80 | // let slug = slugify(children); 81 | // return React.createElement( 82 | // `h${level}`, 83 | // { id: slug }, 84 | // [ 85 | // React.createElement('a', { 86 | // href: `#${slug}`, 87 | // key: `link-${slug}`, 88 | // className: 'anchor', 89 | // }), 90 | // ], 91 | // children, 92 | // ); 93 | // }; 94 | // } 95 | 96 | let components = { 97 | // h1: createHeading(1), 98 | // h2: createHeading(2), 99 | // h3: createHeading(3), 100 | // h4: createHeading(4), 101 | // h5: createHeading(5), 102 | // h6: createHeading(6), 103 | Image: RoundedImage, 104 | a: CustomLink, 105 | Callout, 106 | code: Code, 107 | Table, 108 | }; 109 | 110 | export function CustomMDX(props: any) { 111 | return ( 112 | 116 | ); 117 | } 118 | -------------------------------------------------------------------------------- /src/components/repos.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import React, { useEffect, useState } from 'react'; 4 | 5 | import Link from 'next/link'; 6 | 7 | import { Button } from '@/components/ui/button'; 8 | import { Input } from '@/components/ui/input'; 9 | import { 10 | Pagination, 11 | PaginationContent, 12 | PaginationEllipsis, 13 | PaginationItem, 14 | PaginationLink, 15 | PaginationNext, 16 | PaginationPrevious, 17 | } from '@/components/ui/pagination'; 18 | import { Repository } from '@/type/types'; 19 | import axios from 'axios'; 20 | import Fuse from 'fuse.js'; 21 | import { Star } from 'lucide-react'; 22 | import { signOut, useSession } from 'next-auth/react'; 23 | 24 | function PaginationSection({ 25 | totalPosts, 26 | postsPerPage, 27 | currentPage, 28 | setCurrentPage, 29 | }: { 30 | totalPosts: any; 31 | postsPerPage: any; 32 | currentPage: any; 33 | setCurrentPage: any; 34 | }) { 35 | const pageNumbers = []; 36 | for (let i = 1; i <= Math.ceil(totalPosts / postsPerPage); i++) { 37 | pageNumbers.push(i); 38 | } 39 | 40 | const maxPageNum = 5; // Maximum page numbers to display at once 41 | const pageNumLimit = Math.floor(maxPageNum / 2); // Current page should be in the middle if possible 42 | 43 | let activePages = pageNumbers.slice( 44 | Math.max(0, currentPage - 1 - pageNumLimit), 45 | Math.min(currentPage - 1 + pageNumLimit + 1, pageNumbers.length), 46 | ); 47 | 48 | const handleNextPage = () => { 49 | if (currentPage < pageNumbers.length) { 50 | setCurrentPage(currentPage + 1); 51 | } 52 | }; 53 | 54 | const handlePrevPage = () => { 55 | if (currentPage > 1) { 56 | setCurrentPage(currentPage - 1); 57 | } 58 | }; 59 | 60 | // Function to render page numbers with ellipsis 61 | const renderPages = () => { 62 | const renderedPages = activePages.map((page, idx) => ( 63 | 67 | setCurrentPage(page)}> 68 | {page} 69 | 70 | 71 | )); 72 | 73 | // Add ellipsis at the start if necessary 74 | if (activePages[0] > 1) { 75 | renderedPages.unshift( 76 | setCurrentPage(activePages[0] - 1)} 79 | />, 80 | ); 81 | } 82 | 83 | // Add ellipsis at the end if necessary 84 | if (activePages[activePages.length - 1] < pageNumbers.length) { 85 | renderedPages.push( 86 | 89 | setCurrentPage(activePages[activePages.length - 1] + 1) 90 | } 91 | />, 92 | ); 93 | } 94 | 95 | return renderedPages; 96 | }; 97 | 98 | return ( 99 |
100 | 101 | 102 | 103 | 104 | 105 | 106 | {renderPages()} 107 | 108 | 109 | 110 | 111 | 112 | 113 |
114 | ); 115 | } 116 | 117 | const Repos = () => { 118 | const { data: session } = useSession(); 119 | const [repos, setRepos] = useState([]); 120 | const [loading, setLoading] = useState(false); 121 | const [error, setError] = useState(''); 122 | const [searchTerm, setSearchTerm] = useState(''); 123 | const [filteredRepos, setFilteredRepos] = useState([]); 124 | const [currentPage, setCurrentPage] = useState(1); 125 | const [postsPerPage, setPostsPerPage] = useState(6); 126 | 127 | const lastPostIndex = currentPage * postsPerPage; 128 | const firstPostIndex = lastPostIndex - postsPerPage; 129 | const currentPosts = filteredRepos.slice(firstPostIndex, lastPostIndex); 130 | 131 | const highlightText = (text: string, highlight: string): JSX.Element => { 132 | if (!highlight.trim()) { 133 | return {text}; 134 | } 135 | const parts = text.split(new RegExp(`(${highlight})`, 'gi')); 136 | return ( 137 | 138 | {parts.map((part, index) => 139 | part.toLowerCase() === highlight.toLowerCase() ? ( 140 | 141 | {part} 142 | 143 | ) : ( 144 | part 145 | ), 146 | )} 147 | 148 | ); 149 | }; 150 | 151 | useEffect(() => { 152 | async function fetchAllGithubRepos() { 153 | const response = await axios.post('/api/repos', { 154 | token: session?.accessToken, 155 | }); 156 | setRepos(response.data); 157 | } 158 | 159 | fetchAllGithubRepos(); 160 | }, [session]); 161 | 162 | useEffect(() => { 163 | // Convert the search term and repository names to lowercase for case-insensitive comparison 164 | const searchTermLower = searchTerm.toLowerCase(); 165 | const filtered = repos.filter((repo) => 166 | repo.name.toLowerCase().includes(searchTermLower), 167 | ); 168 | 169 | setFilteredRepos(filtered); 170 | }, [repos, searchTerm]); 171 | 172 | if (loading) return
Loading...
; 173 | if (error) return
Error: {error}
; 174 | 175 | return ( 176 |
177 |

178 | Your Repositories ({repos.length}) 179 |

180 | setSearchTerm(e.target.value)} 184 | /> 185 |
186 | {currentPosts.map((repo, idx) => ( 187 |
191 |
192 | 197 | {highlightText(repo.name, searchTerm)} 198 | 199 |
200 | 201 | {repo.stargazers_count} 202 |
203 |
204 | 205 | 210 |
211 | ))} 212 |
213 | 219 |
220 | ); 221 | }; 222 | 223 | export default Repos; 224 | -------------------------------------------------------------------------------- /src/components/ui/button.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | import { Slot } from "@radix-ui/react-slot" 3 | import { cva, type VariantProps } from "class-variance-authority" 4 | 5 | import { cn } from "@/lib/utils" 6 | 7 | const buttonVariants = cva( 8 | "inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50", 9 | { 10 | variants: { 11 | variant: { 12 | default: "bg-primary text-primary-foreground hover:bg-primary/90", 13 | destructive: 14 | "bg-destructive text-destructive-foreground hover:bg-destructive/90", 15 | outline: 16 | "border border-input bg-background hover:bg-accent hover:text-accent-foreground", 17 | secondary: 18 | "bg-secondary text-secondary-foreground hover:bg-secondary/80", 19 | ghost: "hover:bg-accent hover:text-accent-foreground", 20 | link: "text-primary underline-offset-4 hover:underline", 21 | }, 22 | size: { 23 | default: "h-10 px-4 py-2", 24 | sm: "h-9 rounded-md px-3", 25 | lg: "h-11 rounded-md px-8", 26 | icon: "h-10 w-10", 27 | }, 28 | }, 29 | defaultVariants: { 30 | variant: "default", 31 | size: "default", 32 | }, 33 | } 34 | ) 35 | 36 | export interface ButtonProps 37 | extends React.ButtonHTMLAttributes, 38 | VariantProps { 39 | asChild?: boolean 40 | } 41 | 42 | const Button = React.forwardRef( 43 | ({ className, variant, size, asChild = false, ...props }, ref) => { 44 | const Comp = asChild ? Slot : "button" 45 | return ( 46 | 51 | ) 52 | } 53 | ) 54 | Button.displayName = "Button" 55 | 56 | export { Button, buttonVariants } 57 | -------------------------------------------------------------------------------- /src/components/ui/input.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | 3 | import { cn } from "@/lib/utils" 4 | 5 | export interface InputProps 6 | extends React.InputHTMLAttributes {} 7 | 8 | const Input = React.forwardRef( 9 | ({ className, type, ...props }, ref) => { 10 | return ( 11 | 20 | ) 21 | } 22 | ) 23 | Input.displayName = "Input" 24 | 25 | export { Input } 26 | -------------------------------------------------------------------------------- /src/components/ui/pagination.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | import { ChevronLeft, ChevronRight, MoreHorizontal } from "lucide-react" 3 | 4 | import { cn } from "@/lib/utils" 5 | import { ButtonProps, buttonVariants } from "@/components/ui/button" 6 | 7 | const Pagination = ({ className, ...props }: React.ComponentProps<"nav">) => ( 8 |