├── .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 |
52 |
53 |
54 | Gitfólio
55 |
56 |
57 | Crie o seu portfólio com apenas um clique
58 |
59 |
60 |
79 |
80 |
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 |
57 |
58 |
59 |
60 |
61 |
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 |
--------------------------------------------------------------------------------