├── .eslintrc.json ├── .gitignore ├── README.md ├── components.json ├── next.config.js ├── package-lock.json ├── package.json ├── postcss.config.js ├── public ├── next.svg └── vercel.svg ├── src ├── app │ ├── favicon.ico │ ├── globals.css │ ├── layout.tsx │ ├── page.tsx │ └── portfolio │ │ ├── [slug] │ │ └── page.tsx │ │ └── components │ │ ├── header.tsx │ │ ├── languages.tsx │ │ ├── project-card.tsx │ │ ├── projects.tsx │ │ ├── share-button.tsx │ │ └── spinner.tsx ├── components │ └── ui │ │ ├── badge.tsx │ │ ├── button.tsx │ │ ├── card.tsx │ │ ├── dropdown-menu.tsx │ │ ├── input.tsx │ │ ├── popover.tsx │ │ ├── separator.tsx │ │ ├── sheet.tsx │ │ └── theme-toggle.tsx ├── lib │ └── utils.ts ├── providers │ ├── queryProvider.tsx │ └── themeProvider.tsx ├── services │ └── api.ts └── types │ └── index.ts ├── tailwind.config.ts └── tsconfig.json /.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*.local 30 | 31 | # vercel 32 | .vercel 33 | 34 | # typescript 35 | *.tsbuildinfo 36 | next-env.d.ts 37 | -------------------------------------------------------------------------------- /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/app/globals.css", 9 | "baseColor": "slate", 10 | "cssVariables": true 11 | }, 12 | "aliases": { 13 | "components": "@/components", 14 | "utils": "@/lib/utils" 15 | } 16 | } -------------------------------------------------------------------------------- /next.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('next').NextConfig} */ 2 | const nextConfig = { 3 | images: { 4 | domains: ["avatars.githubusercontent.com"], 5 | }, 6 | }; 7 | 8 | module.exports = nextConfig; 9 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "gitfolio", 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 | "@radix-ui/react-dialog": "^1.0.5", 13 | "@radix-ui/react-dropdown-menu": "^2.0.6", 14 | "@radix-ui/react-popover": "^1.0.7", 15 | "@radix-ui/react-separator": "^1.0.3", 16 | "@radix-ui/react-slot": "^1.0.2", 17 | "class-variance-authority": "^0.7.0", 18 | "clsx": "^2.0.0", 19 | "lucide-react": "^0.291.0", 20 | "next": "14.0.1", 21 | "next-themes": "^0.2.1", 22 | "react": "^18", 23 | "react-dom": "^18", 24 | "react-icons": "^4.12.0", 25 | "react-query": "^3.39.3", 26 | "react-share": "^4.4.1", 27 | "react-toastify": "^9.1.3", 28 | "tailwind-merge": "^2.0.0", 29 | "tailwindcss-animate": "^1.0.7", 30 | "tailwindcss-animated": "^1.0.1" 31 | }, 32 | "devDependencies": { 33 | "@types/node": "^20", 34 | "@types/react": "^18", 35 | "@types/react-dom": "^18", 36 | "autoprefixer": "^10.0.1", 37 | "eslint": "^8", 38 | "eslint-config-next": "14.0.1", 39 | "postcss": "^8", 40 | "tailwindcss": "^3.3.0", 41 | "typescript": "^5" 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | } 7 | -------------------------------------------------------------------------------- /public/next.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/vercel.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/app/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wllysses/gitfolio/5cf2a48cf775726ce63334d7afd23da6df0fb306/src/app/favicon.ico -------------------------------------------------------------------------------- /src/app/globals.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | @layer base { 6 | :root { 7 | --background: 222.2 84% 4.9%; 8 | --foreground: 210 40% 98%; 9 | --card: 222.2 84% 4.9%; 10 | --card-foreground: 210 40% 98%; 11 | --popover: 222.2 84% 4.9%; 12 | --popover-foreground: 210 40% 98%; 13 | --primary: 217.2 91.2% 59.8%; 14 | --primary-foreground: 222.2 47.4% 11.2%; 15 | --secondary: 217.2 32.6% 17.5%; 16 | --secondary-foreground: 210 40% 98%; 17 | --muted: 217.2 32.6% 17.5%; 18 | --muted-foreground: 215 20.2% 65.1%; 19 | --accent: 217.2 32.6% 17.5%; 20 | --accent-foreground: 210 40% 98%; 21 | --destructive: 0 62.8% 30.6%; 22 | --destructive-foreground: 210 40% 98%; 23 | --border: 217.2 32.6% 17.5%; 24 | --input: 217.2 32.6% 17.5%; 25 | --ring: 224.3 76.3% 48%; 26 | --radius: 0.5rem; 27 | } 28 | 29 | .light { 30 | --background: 0 0% 100%; 31 | --foreground: 222.2 84% 4.9%; 32 | --card: 0 0% 100%; 33 | --card-foreground: 222.2 84% 4.9%; 34 | --popover: 0 0% 100%; 35 | --popover-foreground: 222.2 84% 4.9%; 36 | --primary: 221.2 83.2% 53.3%; 37 | --primary-foreground: 210 40% 98%; 38 | --secondary: 210 40% 96.1%; 39 | --secondary-foreground: 222.2 47.4% 11.2%; 40 | --muted: 210 40% 96.1%; 41 | --muted-foreground: 215.4 16.3% 46.9%; 42 | --accent: 210 40% 96.1%; 43 | --accent-foreground: 222.2 47.4% 11.2%; 44 | --destructive: 0 84.2% 60.2%; 45 | --destructive-foreground: 210 40% 98%; 46 | --border: 214.3 31.8% 91.4%; 47 | --input: 214.3 31.8% 91.4%; 48 | --ring: 221.2 83.2% 53.3%; 49 | --radius: 0.5rem; 50 | } 51 | } 52 | 53 | @layer base { 54 | * { 55 | @apply border-border; 56 | } 57 | body { 58 | @apply bg-background text-foreground; 59 | } 60 | html { 61 | @apply scroll-smooth; 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/app/layout.tsx: -------------------------------------------------------------------------------- 1 | import type { Metadata } from "next"; 2 | import { Inter } from "next/font/google"; 3 | import "./globals.css"; 4 | 5 | import { ToastContainer } from "react-toastify"; 6 | import "react-toastify/dist/ReactToastify.css"; 7 | 8 | import { QueryProvider } from "@/providers/queryProvider"; 9 | import { ThemeProvider } from "@/providers/themeProvider"; 10 | 11 | const inter = Inter({ subsets: ["latin"] }); 12 | 13 | export const metadata: Metadata = { 14 | title: "Gitfólio", 15 | description: "Crie o seu portfólio com apenas um clique", 16 | }; 17 | 18 | export default function RootLayout({ 19 | children, 20 | }: { 21 | children: React.ReactNode; 22 | }) { 23 | return ( 24 | 25 | 26 | 31 | 37 | {children} 38 | 39 | 40 | 41 | ); 42 | } 43 | -------------------------------------------------------------------------------- /src/app/page.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { FormEvent, useState } from "react"; 4 | import { toast } from "react-toastify"; 5 | import { useQuery } from "react-query"; 6 | import { useRouter } from "next/navigation"; 7 | import { SearchIcon } from "lucide-react"; 8 | import { getUserData } from "@/services/api"; 9 | import { Button } from "@/components/ui/button"; 10 | import { Input } from "@/components/ui/input"; 11 | import { ModeToggle } from "@/components/ui/theme-toggle"; 12 | 13 | export default function Home() { 14 | const router = useRouter(); 15 | 16 | const [input, setInput] = useState(""); 17 | 18 | const { isLoading, isError, refetch } = useQuery( 19 | ["user"], 20 | async () => getUserData(input), 21 | { enabled: false } 22 | ); 23 | 24 | async function handleUserExists(e: FormEvent) { 25 | e.preventDefault(); 26 | 27 | const fetchData = await refetch(); 28 | 29 | if (fetchData.data.message) { 30 | if (fetchData.data.message === "Not Found") { 31 | toast.error("Usuário não existe."); 32 | return; 33 | } 34 | if (fetchData.data.message.includes("API rate limit exceeded")) { 35 | toast.error( 36 | "Número de requisições excedida. Tente novamente mais tarde." 37 | ); 38 | return; 39 | } 40 | } 41 | 42 | router.push(`/portfolio/${input}`); 43 | } 44 | 45 | if (isError) return
Algo deu errado...
; 46 | 47 | return ( 48 |
49 |
50 | 51 |
52 | 53 |

54 | Gitfólio 55 |

56 |

57 | Crie o seu portfólio com apenas um clique 58 |

59 | 60 |
64 | setInput(e.target.value)} 68 | disabled={isLoading} 69 | /> 70 | 78 |
79 | 80 |
81 | Desenvolvido por Wllysses Tavares 82 |
83 |
84 | ); 85 | } 86 | -------------------------------------------------------------------------------- /src/app/portfolio/[slug]/page.tsx: -------------------------------------------------------------------------------- 1 | import Image from "next/image"; 2 | import Link from "next/link"; 3 | import { getUserData } from "@/services/api"; 4 | import { cn } from "@/lib/utils"; 5 | import { Header } from "../components/header"; 6 | import { Button, buttonVariants } from "@/components/ui/button"; 7 | import { Projects } from "../components/projects"; 8 | import Languages from "../components/languages"; 9 | import { ShareButton } from "../components/share-button"; 10 | 11 | interface ParamsProps { 12 | params: { 13 | slug: string; 14 | }; 15 | } 16 | 17 | export default async function Portfolio({ params: { slug } }: ParamsProps) { 18 | const user = await getUserData(slug); 19 | 20 | return ( 21 | <> 22 |
23 |
24 |
28 |
29 | Olá. Eu me chamo 30 |

{user.name}

31 |

{user.bio ?? ""}

32 | 33 |
34 | 42 | Entre em contato 43 | 44 | 47 |
48 |
49 | Github profile avatar 57 |
58 | 59 | 60 |
61 |
62 |

© Todos os direitos reservados

63 |
64 | 65 | ); 66 | } 67 | -------------------------------------------------------------------------------- /src/app/portfolio/components/header.tsx: -------------------------------------------------------------------------------- 1 | import Link from "next/link"; 2 | import { FolderOpenDotIcon, HomeIcon, MenuIcon } from "lucide-react"; 3 | import { cn } from "@/lib/utils"; 4 | import { Button, buttonVariants } from "@/components/ui/button"; 5 | import { Card } from "@/components/ui/card"; 6 | import { Separator } from "@/components/ui/separator"; 7 | import { 8 | Sheet, 9 | SheetContent, 10 | SheetHeader, 11 | SheetTitle, 12 | SheetTrigger, 13 | } from "@/components/ui/sheet"; 14 | import { ModeToggle } from "@/components/ui/theme-toggle"; 15 | 16 | export function Header() { 17 | return ( 18 | 19 |
20 |

21 | <Meu Portfólio /> 22 |

23 | 42 | 43 | 44 | 45 | 48 | 49 | 50 | 51 | 52 | Menu 53 | 54 | 55 |
56 | 63 | 64 | Home 65 | 66 | 73 | 74 | Projetos 75 | 76 | 77 | 78 | 79 | 83 | Sair 84 | 85 |
86 | 87 |
88 | 89 |
90 |
91 |
92 |
93 |
94 | ); 95 | } 96 | -------------------------------------------------------------------------------- /src/app/portfolio/components/languages.tsx: -------------------------------------------------------------------------------- 1 | import { Badge } from "@/components/ui/badge"; 2 | import { getReposWithLanguages } from "@/services/api"; 3 | import { Repo } from "@/types"; 4 | 5 | interface Props { 6 | slug: string; 7 | } 8 | 9 | export default async function Languages({ slug }: Props) { 10 | const allRepos: Repo[] = await getReposWithLanguages(slug); 11 | 12 | const reposWithLanguages = allRepos.filter((repo) => repo.language !== null); 13 | 14 | const languages: string[] = []; 15 | for (const repo of reposWithLanguages) { 16 | languages.push(repo.language); 17 | } 18 | 19 | const set = new Set(languages); 20 | const filteredLanguages = Array.from(set); 21 | 22 | return ( 23 |
24 |

Principais Linguagens

25 |
26 | {filteredLanguages && 27 | filteredLanguages.slice(0, 5).map((language, index) => ( 28 | 29 | {language} 30 | 31 | ))} 32 |
33 |
34 | ); 35 | } 36 | -------------------------------------------------------------------------------- /src/app/portfolio/components/project-card.tsx: -------------------------------------------------------------------------------- 1 | import Link from "next/link"; 2 | import { Code2Icon, GitForkIcon, StarIcon } from "lucide-react"; 3 | import { Repo } from "@/types"; 4 | import { buttonVariants } from "@/components/ui/button"; 5 | import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; 6 | 7 | interface ProjectCardProps extends Repo {} 8 | 9 | export function ProjectCard({ 10 | name, 11 | html_url, 12 | stargazers_count, 13 | forks_count, 14 | language, 15 | homepage, 16 | }: ProjectCardProps) { 17 | return ( 18 | 19 | 20 | 24 | {name} 25 | 26 | 27 | 28 |
    29 |
  • 30 | 31 | {forks_count} 32 |
  • 33 |
  • 34 | 35 | {stargazers_count} 36 |
  • 37 |
  • 38 | 39 | {language} 40 |
  • 41 |
42 | 43 |
44 | 49 | Repositório 50 | 51 | {homepage && ( 52 | 57 | Deploy 58 | 59 | )} 60 |
61 |
62 |
63 | ); 64 | } 65 | -------------------------------------------------------------------------------- /src/app/portfolio/components/projects.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { useInfiniteQuery } from "react-query"; 4 | import { getUserRepos } from "@/services/api"; 5 | import { Repo } from "@/types"; 6 | import { ProjectCard } from "./project-card"; 7 | import { Button } from "@/components/ui/button"; 8 | import { AiOutlineLoading3Quarters as Spinner } from "react-icons/ai"; 9 | 10 | interface ProjectsProps { 11 | slug: string; 12 | totalRepos: number; 13 | } 14 | 15 | export function Projects({ slug, totalRepos }: ProjectsProps) { 16 | const { 17 | data: repos, 18 | isFetching, 19 | isError, 20 | fetchNextPage, 21 | hasNextPage, 22 | } = useInfiniteQuery<{ data: Repo[]; nextPage: number | null }>({ 23 | queryKey: ["repos"], 24 | queryFn: async ({ pageParam = 1 }) => await getUserRepos(slug, pageParam), 25 | getNextPageParam: (lastPage) => lastPage.nextPage, 26 | }); 27 | 28 | if (isError) return
Algo deu errado...
; 29 | 30 | return ( 31 |
35 |

36 | Meus Projetos 37 |

38 |
39 | {repos && 40 | repos.pages.map((repos) => 41 | repos.data.map((repo) => ( 42 | 51 | )) 52 | )} 53 |
54 | {totalRepos === 0 &&
Nenhum repositório público.
} 55 | {hasNextPage && ( 56 | 67 | )} 68 |
69 | ); 70 | } 71 | -------------------------------------------------------------------------------- /src/app/portfolio/components/share-button.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { Button } from "@/components/ui/button"; 4 | import { 5 | Popover, 6 | PopoverContent, 7 | PopoverTrigger, 8 | } from "@/components/ui/popover"; 9 | import { 10 | WhatsappShareButton, 11 | LinkedinShareButton, 12 | FacebookShareButton, 13 | TwitterShareButton, 14 | WhatsappIcon, 15 | LinkedinIcon, 16 | FacebookIcon, 17 | TwitterIcon, 18 | } from "react-share"; 19 | 20 | interface ShareButtonProps { 21 | url: string; 22 | } 23 | 24 | export function ShareButton({ url }: ShareButtonProps) { 25 | return ( 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | ); 46 | } 47 | -------------------------------------------------------------------------------- /src/app/portfolio/components/spinner.tsx: -------------------------------------------------------------------------------- 1 | export function Spinner() { 2 | return ( 3 |
4 |
5 | Carregando projetos 6 |
7 | ); 8 | } 9 | -------------------------------------------------------------------------------- /src/components/ui/badge.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | import { cva, type VariantProps } from "class-variance-authority" 3 | 4 | import { cn } from "@/lib/utils" 5 | 6 | const badgeVariants = cva( 7 | "inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2", 8 | { 9 | variants: { 10 | variant: { 11 | default: 12 | "border-transparent bg-primary text-primary-foreground hover:bg-primary/80", 13 | secondary: 14 | "border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80", 15 | destructive: 16 | "border-transparent bg-destructive text-destructive-foreground hover:bg-destructive/80", 17 | outline: "text-foreground", 18 | }, 19 | }, 20 | defaultVariants: { 21 | variant: "default", 22 | }, 23 | } 24 | ) 25 | 26 | export interface BadgeProps 27 | extends React.HTMLAttributes, 28 | VariantProps {} 29 | 30 | function Badge({ className, variant, ...props }: BadgeProps) { 31 | return ( 32 |
33 | ) 34 | } 35 | 36 | export { Badge, badgeVariants } 37 | -------------------------------------------------------------------------------- /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/card.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | 3 | import { cn } from "@/lib/utils" 4 | 5 | const Card = React.forwardRef< 6 | HTMLDivElement, 7 | React.HTMLAttributes 8 | >(({ className, ...props }, ref) => ( 9 |
17 | )) 18 | Card.displayName = "Card" 19 | 20 | const CardHeader = React.forwardRef< 21 | HTMLDivElement, 22 | React.HTMLAttributes 23 | >(({ className, ...props }, ref) => ( 24 |
29 | )) 30 | CardHeader.displayName = "CardHeader" 31 | 32 | const CardTitle = React.forwardRef< 33 | HTMLParagraphElement, 34 | React.HTMLAttributes 35 | >(({ className, ...props }, ref) => ( 36 |

44 | )) 45 | CardTitle.displayName = "CardTitle" 46 | 47 | const CardDescription = React.forwardRef< 48 | HTMLParagraphElement, 49 | React.HTMLAttributes 50 | >(({ className, ...props }, ref) => ( 51 |

56 | )) 57 | CardDescription.displayName = "CardDescription" 58 | 59 | const CardContent = React.forwardRef< 60 | HTMLDivElement, 61 | React.HTMLAttributes 62 | >(({ className, ...props }, ref) => ( 63 |

64 | )) 65 | CardContent.displayName = "CardContent" 66 | 67 | const CardFooter = React.forwardRef< 68 | HTMLDivElement, 69 | React.HTMLAttributes 70 | >(({ className, ...props }, ref) => ( 71 |
76 | )) 77 | CardFooter.displayName = "CardFooter" 78 | 79 | export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent } 80 | -------------------------------------------------------------------------------- /src/components/ui/dropdown-menu.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import * as React from "react" 4 | import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu" 5 | import { Check, ChevronRight, Circle } from "lucide-react" 6 | 7 | import { cn } from "@/lib/utils" 8 | 9 | const DropdownMenu = DropdownMenuPrimitive.Root 10 | 11 | const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger 12 | 13 | const DropdownMenuGroup = DropdownMenuPrimitive.Group 14 | 15 | const DropdownMenuPortal = DropdownMenuPrimitive.Portal 16 | 17 | const DropdownMenuSub = DropdownMenuPrimitive.Sub 18 | 19 | const DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup 20 | 21 | const DropdownMenuSubTrigger = React.forwardRef< 22 | React.ElementRef, 23 | React.ComponentPropsWithoutRef & { 24 | inset?: boolean 25 | } 26 | >(({ className, inset, children, ...props }, ref) => ( 27 | 36 | {children} 37 | 38 | 39 | )) 40 | DropdownMenuSubTrigger.displayName = 41 | DropdownMenuPrimitive.SubTrigger.displayName 42 | 43 | const DropdownMenuSubContent = React.forwardRef< 44 | React.ElementRef, 45 | React.ComponentPropsWithoutRef 46 | >(({ className, ...props }, ref) => ( 47 | 55 | )) 56 | DropdownMenuSubContent.displayName = 57 | DropdownMenuPrimitive.SubContent.displayName 58 | 59 | const DropdownMenuContent = React.forwardRef< 60 | React.ElementRef, 61 | React.ComponentPropsWithoutRef 62 | >(({ className, sideOffset = 4, ...props }, ref) => ( 63 | 64 | 73 | 74 | )) 75 | DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName 76 | 77 | const DropdownMenuItem = React.forwardRef< 78 | React.ElementRef, 79 | React.ComponentPropsWithoutRef & { 80 | inset?: boolean 81 | } 82 | >(({ className, inset, ...props }, ref) => ( 83 | 92 | )) 93 | DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName 94 | 95 | const DropdownMenuCheckboxItem = React.forwardRef< 96 | React.ElementRef, 97 | React.ComponentPropsWithoutRef 98 | >(({ className, children, checked, ...props }, ref) => ( 99 | 108 | 109 | 110 | 111 | 112 | 113 | {children} 114 | 115 | )) 116 | DropdownMenuCheckboxItem.displayName = 117 | DropdownMenuPrimitive.CheckboxItem.displayName 118 | 119 | const DropdownMenuRadioItem = React.forwardRef< 120 | React.ElementRef, 121 | React.ComponentPropsWithoutRef 122 | >(({ className, children, ...props }, ref) => ( 123 | 131 | 132 | 133 | 134 | 135 | 136 | {children} 137 | 138 | )) 139 | DropdownMenuRadioItem.displayName = DropdownMenuPrimitive.RadioItem.displayName 140 | 141 | const DropdownMenuLabel = React.forwardRef< 142 | React.ElementRef, 143 | React.ComponentPropsWithoutRef & { 144 | inset?: boolean 145 | } 146 | >(({ className, inset, ...props }, ref) => ( 147 | 156 | )) 157 | DropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName 158 | 159 | const DropdownMenuSeparator = React.forwardRef< 160 | React.ElementRef, 161 | React.ComponentPropsWithoutRef 162 | >(({ className, ...props }, ref) => ( 163 | 168 | )) 169 | DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName 170 | 171 | const DropdownMenuShortcut = ({ 172 | className, 173 | ...props 174 | }: React.HTMLAttributes) => { 175 | return ( 176 | 180 | ) 181 | } 182 | DropdownMenuShortcut.displayName = "DropdownMenuShortcut" 183 | 184 | export { 185 | DropdownMenu, 186 | DropdownMenuTrigger, 187 | DropdownMenuContent, 188 | DropdownMenuItem, 189 | DropdownMenuCheckboxItem, 190 | DropdownMenuRadioItem, 191 | DropdownMenuLabel, 192 | DropdownMenuSeparator, 193 | DropdownMenuShortcut, 194 | DropdownMenuGroup, 195 | DropdownMenuPortal, 196 | DropdownMenuSub, 197 | DropdownMenuSubContent, 198 | DropdownMenuSubTrigger, 199 | DropdownMenuRadioGroup, 200 | } 201 | -------------------------------------------------------------------------------- /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/popover.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import * as React from "react" 4 | import * as PopoverPrimitive from "@radix-ui/react-popover" 5 | 6 | import { cn } from "@/lib/utils" 7 | 8 | const Popover = PopoverPrimitive.Root 9 | 10 | const PopoverTrigger = PopoverPrimitive.Trigger 11 | 12 | const PopoverContent = React.forwardRef< 13 | React.ElementRef, 14 | React.ComponentPropsWithoutRef 15 | >(({ className, align = "center", sideOffset = 4, ...props }, ref) => ( 16 | 17 | 27 | 28 | )) 29 | PopoverContent.displayName = PopoverPrimitive.Content.displayName 30 | 31 | export { Popover, PopoverTrigger, PopoverContent } 32 | -------------------------------------------------------------------------------- /src/components/ui/separator.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import * as React from "react" 4 | import * as SeparatorPrimitive from "@radix-ui/react-separator" 5 | 6 | import { cn } from "@/lib/utils" 7 | 8 | const Separator = React.forwardRef< 9 | React.ElementRef, 10 | React.ComponentPropsWithoutRef 11 | >( 12 | ( 13 | { className, orientation = "horizontal", decorative = true, ...props }, 14 | ref 15 | ) => ( 16 | 27 | ) 28 | ) 29 | Separator.displayName = SeparatorPrimitive.Root.displayName 30 | 31 | export { Separator } 32 | -------------------------------------------------------------------------------- /src/components/ui/sheet.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import * as React from "react" 4 | import * as SheetPrimitive from "@radix-ui/react-dialog" 5 | import { cva, type VariantProps } from "class-variance-authority" 6 | import { X } from "lucide-react" 7 | 8 | import { cn } from "@/lib/utils" 9 | 10 | const Sheet = SheetPrimitive.Root 11 | 12 | const SheetTrigger = SheetPrimitive.Trigger 13 | 14 | const SheetClose = SheetPrimitive.Close 15 | 16 | const SheetPortal = SheetPrimitive.Portal 17 | 18 | const SheetOverlay = React.forwardRef< 19 | React.ElementRef, 20 | React.ComponentPropsWithoutRef 21 | >(({ className, ...props }, ref) => ( 22 | 30 | )) 31 | SheetOverlay.displayName = SheetPrimitive.Overlay.displayName 32 | 33 | const sheetVariants = cva( 34 | "fixed z-50 gap-4 bg-background p-6 shadow-lg transition ease-in-out data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:duration-300 data-[state=open]:duration-500", 35 | { 36 | variants: { 37 | side: { 38 | top: "inset-x-0 top-0 border-b data-[state=closed]:slide-out-to-top data-[state=open]:slide-in-from-top", 39 | bottom: 40 | "inset-x-0 bottom-0 border-t data-[state=closed]:slide-out-to-bottom data-[state=open]:slide-in-from-bottom", 41 | left: "inset-y-0 left-0 h-full w-3/4 border-r data-[state=closed]:slide-out-to-left data-[state=open]:slide-in-from-left sm:max-w-sm", 42 | right: 43 | "inset-y-0 right-0 h-full w-3/4 border-l data-[state=closed]:slide-out-to-right data-[state=open]:slide-in-from-right sm:max-w-sm", 44 | }, 45 | }, 46 | defaultVariants: { 47 | side: "right", 48 | }, 49 | } 50 | ) 51 | 52 | interface SheetContentProps 53 | extends React.ComponentPropsWithoutRef, 54 | VariantProps {} 55 | 56 | const SheetContent = React.forwardRef< 57 | React.ElementRef, 58 | SheetContentProps 59 | >(({ side = "right", className, children, ...props }, ref) => ( 60 | 61 | 62 | 67 | {children} 68 | 69 | 70 | Close 71 | 72 | 73 | 74 | )) 75 | SheetContent.displayName = SheetPrimitive.Content.displayName 76 | 77 | const SheetHeader = ({ 78 | className, 79 | ...props 80 | }: React.HTMLAttributes) => ( 81 |
88 | ) 89 | SheetHeader.displayName = "SheetHeader" 90 | 91 | const SheetFooter = ({ 92 | className, 93 | ...props 94 | }: React.HTMLAttributes) => ( 95 |
102 | ) 103 | SheetFooter.displayName = "SheetFooter" 104 | 105 | const SheetTitle = React.forwardRef< 106 | React.ElementRef, 107 | React.ComponentPropsWithoutRef 108 | >(({ className, ...props }, ref) => ( 109 | 114 | )) 115 | SheetTitle.displayName = SheetPrimitive.Title.displayName 116 | 117 | const SheetDescription = React.forwardRef< 118 | React.ElementRef, 119 | React.ComponentPropsWithoutRef 120 | >(({ className, ...props }, ref) => ( 121 | 126 | )) 127 | SheetDescription.displayName = SheetPrimitive.Description.displayName 128 | 129 | export { 130 | Sheet, 131 | SheetPortal, 132 | SheetOverlay, 133 | SheetTrigger, 134 | SheetClose, 135 | SheetContent, 136 | SheetHeader, 137 | SheetFooter, 138 | SheetTitle, 139 | SheetDescription, 140 | } 141 | -------------------------------------------------------------------------------- /src/components/ui/theme-toggle.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import * as React from "react"; 4 | import { Moon, Sun } from "lucide-react"; 5 | import { useTheme } from "next-themes"; 6 | 7 | import { Button } from "@/components/ui/button"; 8 | import { 9 | DropdownMenu, 10 | DropdownMenuContent, 11 | DropdownMenuItem, 12 | DropdownMenuTrigger, 13 | } from "@/components/ui/dropdown-menu"; 14 | 15 | export function ModeToggle() { 16 | const { setTheme } = useTheme(); 17 | 18 | return ( 19 | 20 | 21 | 26 | 27 | 28 | setTheme("light")}> 29 | Light 30 | 31 | setTheme("dark")}> 32 | Dark 33 | 34 | setTheme("system")}> 35 | System 36 | 37 | 38 | 39 | ); 40 | } 41 | -------------------------------------------------------------------------------- /src/lib/utils.ts: -------------------------------------------------------------------------------- 1 | import { type ClassValue, clsx } from "clsx"; 2 | import { twMerge } from "tailwind-merge"; 3 | 4 | export function cn(...inputs: ClassValue[]) { 5 | return twMerge(clsx(inputs)); 6 | } 7 | -------------------------------------------------------------------------------- /src/providers/queryProvider.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { ReactNode } from "react"; 4 | import { QueryClientProvider, QueryClient } from "react-query"; 5 | 6 | interface QueryProviderProps { 7 | children: ReactNode; 8 | } 9 | 10 | export function QueryProvider({ children }: QueryProviderProps) { 11 | const queryClient = new QueryClient({ 12 | defaultOptions: { 13 | queries: { 14 | refetchOnWindowFocus: false, 15 | }, 16 | }, 17 | }); 18 | 19 | return ( 20 | {children} 21 | ); 22 | } 23 | -------------------------------------------------------------------------------- /src/providers/themeProvider.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import * as React from "react"; 4 | import { ThemeProvider as NextThemesProvider } from "next-themes"; 5 | import { type ThemeProviderProps } from "next-themes/dist/types"; 6 | 7 | export function ThemeProvider({ children, ...props }: ThemeProviderProps) { 8 | return {children}; 9 | } 10 | -------------------------------------------------------------------------------- /src/services/api.ts: -------------------------------------------------------------------------------- 1 | export const getUserData = async (profileName: string) => { 2 | const response = await fetch(`https://api.github.com/users/${profileName}`, { 3 | cache: "no-store", 4 | }); 5 | 6 | return await response.json(); 7 | }; 8 | 9 | export const getUserRepos = async (profileName: string, page: number) => { 10 | const response = await fetch( 11 | `https://api.github.com/users/${profileName}/repos?per_page=8&page=${page}`, 12 | { 13 | cache: "no-store", 14 | } 15 | ); 16 | 17 | const data = await response.json(); 18 | const nextPage = data.length === 0 ? null : page + 1; 19 | 20 | return { data, nextPage }; 21 | }; 22 | 23 | export const getReposWithLanguages = async (profileName: string) => { 24 | const response = await fetch( 25 | `https://api.github.com/users/${profileName}/repos`, 26 | { cache: "no-store" } 27 | ); 28 | return await response.json(); 29 | }; 30 | -------------------------------------------------------------------------------- /src/types/index.ts: -------------------------------------------------------------------------------- 1 | export interface Repo { 2 | id?: number; 3 | name: string; 4 | html_url: string; 5 | stargazers_count: number; 6 | forks_count: number; 7 | language: string; 8 | homepage: string | null; 9 | } 10 | -------------------------------------------------------------------------------- /tailwind.config.ts: -------------------------------------------------------------------------------- 1 | /** @type {import('tailwindcss').Config} */ 2 | module.exports = { 3 | darkMode: ["class"], 4 | content: [ 5 | './pages/**/*.{ts,tsx}', 6 | './components/**/*.{ts,tsx}', 7 | './app/**/*.{ts,tsx}', 8 | './src/**/*.{ts,tsx}', 9 | ], 10 | theme: { 11 | container: { 12 | center: true, 13 | padding: "2rem", 14 | screens: { 15 | "2xl": "1400px", 16 | }, 17 | }, 18 | extend: { 19 | colors: { 20 | border: "hsl(var(--border))", 21 | input: "hsl(var(--input))", 22 | ring: "hsl(var(--ring))", 23 | background: "hsl(var(--background))", 24 | foreground: "hsl(var(--foreground))", 25 | primary: { 26 | DEFAULT: "hsl(var(--primary))", 27 | foreground: "hsl(var(--primary-foreground))", 28 | }, 29 | secondary: { 30 | DEFAULT: "hsl(var(--secondary))", 31 | foreground: "hsl(var(--secondary-foreground))", 32 | }, 33 | destructive: { 34 | DEFAULT: "hsl(var(--destructive))", 35 | foreground: "hsl(var(--destructive-foreground))", 36 | }, 37 | muted: { 38 | DEFAULT: "hsl(var(--muted))", 39 | foreground: "hsl(var(--muted-foreground))", 40 | }, 41 | accent: { 42 | DEFAULT: "hsl(var(--accent))", 43 | foreground: "hsl(var(--accent-foreground))", 44 | }, 45 | popover: { 46 | DEFAULT: "hsl(var(--popover))", 47 | foreground: "hsl(var(--popover-foreground))", 48 | }, 49 | card: { 50 | DEFAULT: "hsl(var(--card))", 51 | foreground: "hsl(var(--card-foreground))", 52 | }, 53 | }, 54 | borderRadius: { 55 | lg: "var(--radius)", 56 | md: "calc(var(--radius) - 2px)", 57 | sm: "calc(var(--radius) - 4px)", 58 | }, 59 | keyframes: { 60 | "accordion-down": { 61 | from: { height: 0 }, 62 | to: { height: "var(--radix-accordion-content-height)" }, 63 | }, 64 | "accordion-up": { 65 | from: { height: "var(--radix-accordion-content-height)" }, 66 | to: { height: 0 }, 67 | }, 68 | }, 69 | animation: { 70 | "accordion-down": "accordion-down 0.2s ease-out", 71 | "accordion-up": "accordion-up 0.2s ease-out", 72 | }, 73 | }, 74 | }, 75 | plugins: [require("tailwindcss-animate"), require('tailwindcss-animated')], 76 | } -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": ["dom", "dom.iterable", "esnext"], 5 | "allowJs": true, 6 | "skipLibCheck": true, 7 | "strict": true, 8 | "noEmit": true, 9 | "esModuleInterop": true, 10 | "module": "esnext", 11 | "moduleResolution": "bundler", 12 | "resolveJsonModule": true, 13 | "isolatedModules": true, 14 | "jsx": "preserve", 15 | "incremental": true, 16 | "plugins": [ 17 | { 18 | "name": "next" 19 | } 20 | ], 21 | "paths": { 22 | "@/*": ["./src/*"] 23 | } 24 | }, 25 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], 26 | "exclude": ["node_modules"] 27 | } 28 | --------------------------------------------------------------------------------