diff --git a/next-env.d.ts b/next-env.d.ts index 1b3be08..830fb59 100644 --- a/next-env.d.ts +++ b/next-env.d.ts @@ -1,5 +1,6 @@ /// /// +/// // NOTE: This file should not be edited // see https://nextjs.org/docs/app/api-reference/config/typescript for more information. diff --git a/next.config.mjs b/next.config.mjs index b7213fb..d43ab70 100644 --- a/next.config.mjs +++ b/next.config.mjs @@ -8,6 +8,7 @@ const __dirname = path.dirname(__filename); /**@type {import('next').NextConfig}*/ const config = { reactStrictMode: true, + typedRoutes: true, experimental: { viewTransition: true }, diff --git a/package.json b/package.json index a899d1f..fd1c3f4 100644 --- a/package.json +++ b/package.json @@ -29,11 +29,11 @@ "dotenv-cli": "^10.0.0", "drizzle-orm": "^0.44.6", "eslint": "^9.32.0", - "eslint-config-next": "^15.4.5", + "eslint-config-next": "^15.5.4", "framer-motion": "^12.23.12", "jotai": "^2.12.5", "lucide-react": "^0.536.0", - "next": "^15.4.5", + "next": "^15.5.4", "next-auth": "5.0.0-beta.29", "next-cloudinary": "^6.16.0", "next-seo": "^6.8.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 6d6cd0a..ea436d6 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -66,8 +66,8 @@ importers: specifier: ^9.32.0 version: 9.32.0(jiti@1.21.7) eslint-config-next: - specifier: ^15.4.5 - version: 15.4.5(eslint@9.32.0(jiti@1.21.7))(typescript@5.9.2) + specifier: ^15.5.4 + version: 15.5.4(eslint@9.32.0(jiti@1.21.7))(typescript@5.9.2) framer-motion: specifier: ^12.23.12 version: 12.23.12(react-dom@19.1.1(react@19.1.1))(react@19.1.1) @@ -78,20 +78,20 @@ importers: specifier: ^0.536.0 version: 0.536.0(react@19.1.1) next: - specifier: ^15.4.5 - version: 15.4.5(react-dom@19.1.1(react@19.1.1))(react@19.1.1) + specifier: ^15.5.4 + version: 15.5.4(react-dom@19.1.1(react@19.1.1))(react@19.1.1) next-auth: specifier: 5.0.0-beta.29 - version: 5.0.0-beta.29(next@15.4.5(react-dom@19.1.1(react@19.1.1))(react@19.1.1))(react@19.1.1) + version: 5.0.0-beta.29(next@15.5.4(react-dom@19.1.1(react@19.1.1))(react@19.1.1))(react@19.1.1) next-cloudinary: specifier: ^6.16.0 - version: 6.16.0(next@15.4.5(react-dom@19.1.1(react@19.1.1))(react@19.1.1))(react@19.1.1) + version: 6.16.0(next@15.5.4(react-dom@19.1.1(react@19.1.1))(react@19.1.1))(react@19.1.1) next-seo: specifier: ^6.8.0 - version: 6.8.0(next@15.4.5(react-dom@19.1.1(react@19.1.1))(react@19.1.1))(react-dom@19.1.1(react@19.1.1))(react@19.1.1) + version: 6.8.0(next@15.5.4(react-dom@19.1.1(react@19.1.1))(react@19.1.1))(react-dom@19.1.1(react@19.1.1))(react@19.1.1) nextjs-progressbar: specifier: ^0.0.16 - version: 0.0.16(next@15.4.5(react-dom@19.1.1(react@19.1.1))(react@19.1.1))(react@19.1.1) + version: 0.0.16(next@15.5.4(react-dom@19.1.1(react@19.1.1))(react@19.1.1))(react@19.1.1) postcss: specifier: ^8.5.6 version: 8.5.6 @@ -763,56 +763,56 @@ packages: '@neon-rs/load@0.0.4': resolution: {integrity: sha512-kTPhdZyTQxB+2wpiRcFWrDcejc4JI6tkPuS7UZCG4l6Zvc5kU/gGQ/ozvHTh1XR5tS+UlfAfGuPajjzQjCiHCw==} - '@next/env@15.4.5': - resolution: {integrity: sha512-ruM+q2SCOVCepUiERoxOmZY9ZVoecR3gcXNwCYZRvQQWRjhOiPJGmQ2fAiLR6YKWXcSAh7G79KEFxN3rwhs4LQ==} + '@next/env@15.5.4': + resolution: {integrity: sha512-27SQhYp5QryzIT5uO8hq99C69eLQ7qkzkDPsk3N+GuS2XgOgoYEeOav7Pf8Tn4drECOVDsDg8oj+/DVy8qQL2A==} - '@next/eslint-plugin-next@15.4.5': - resolution: {integrity: sha512-YhbrlbEt0m4jJnXHMY/cCUDBAWgd5SaTa5mJjzOt82QwflAFfW/h3+COp2TfVSzhmscIZ5sg2WXt3MLziqCSCw==} + '@next/eslint-plugin-next@15.5.4': + resolution: {integrity: sha512-SR1vhXNNg16T4zffhJ4TS7Xn7eq4NfKfcOsRwea7RIAHrjRpI9ALYbamqIJqkAhowLlERffiwk0FMvTLNdnVtw==} - '@next/swc-darwin-arm64@15.4.5': - resolution: {integrity: sha512-84dAN4fkfdC7nX6udDLz9GzQlMUwEMKD7zsseXrl7FTeIItF8vpk1lhLEnsotiiDt+QFu3O1FVWnqwcRD2U3KA==} + '@next/swc-darwin-arm64@15.5.4': + resolution: {integrity: sha512-nopqz+Ov6uvorej8ndRX6HlxCYWCO3AHLfKK2TYvxoSB2scETOcfm/HSS3piPqc3A+MUgyHoqE6je4wnkjfrOA==} engines: {node: '>= 10'} cpu: [arm64] os: [darwin] - '@next/swc-darwin-x64@15.4.5': - resolution: {integrity: sha512-CL6mfGsKuFSyQjx36p2ftwMNSb8PQog8y0HO/ONLdQqDql7x3aJb/wB+LA651r4we2pp/Ck+qoRVUeZZEvSurA==} + '@next/swc-darwin-x64@15.5.4': + resolution: {integrity: sha512-QOTCFq8b09ghfjRJKfb68kU9k2K+2wsC4A67psOiMn849K9ZXgCSRQr0oVHfmKnoqCbEmQWG1f2h1T2vtJJ9mA==} engines: {node: '>= 10'} cpu: [x64] os: [darwin] - '@next/swc-linux-arm64-gnu@15.4.5': - resolution: {integrity: sha512-1hTVd9n6jpM/thnDc5kYHD1OjjWYpUJrJxY4DlEacT7L5SEOXIifIdTye6SQNNn8JDZrcN+n8AWOmeJ8u3KlvQ==} + '@next/swc-linux-arm64-gnu@15.5.4': + resolution: {integrity: sha512-eRD5zkts6jS3VfE/J0Kt1VxdFqTnMc3QgO5lFE5GKN3KDI/uUpSyK3CjQHmfEkYR4wCOl0R0XrsjpxfWEA++XA==} engines: {node: '>= 10'} cpu: [arm64] os: [linux] - '@next/swc-linux-arm64-musl@15.4.5': - resolution: {integrity: sha512-4W+D/nw3RpIwGrqpFi7greZ0hjrCaioGErI7XHgkcTeWdZd146NNu1s4HnaHonLeNTguKnL2Urqvj28UJj6Gqw==} + '@next/swc-linux-arm64-musl@15.5.4': + resolution: {integrity: sha512-TOK7iTxmXFc45UrtKqWdZ1shfxuL4tnVAOuuJK4S88rX3oyVV4ZkLjtMT85wQkfBrOOvU55aLty+MV8xmcJR8A==} engines: {node: '>= 10'} cpu: [arm64] os: [linux] - '@next/swc-linux-x64-gnu@15.4.5': - resolution: {integrity: sha512-N6Mgdxe/Cn2K1yMHge6pclffkxzbSGOydXVKYOjYqQXZYjLCfN/CuFkaYDeDHY2VBwSHyM2fUjYBiQCIlxIKDA==} + '@next/swc-linux-x64-gnu@15.5.4': + resolution: {integrity: sha512-7HKolaj+481FSW/5lL0BcTkA4Ueam9SPYWyN/ib/WGAFZf0DGAN8frNpNZYFHtM4ZstrHZS3LY3vrwlIQfsiMA==} engines: {node: '>= 10'} cpu: [x64] os: [linux] - '@next/swc-linux-x64-musl@15.4.5': - resolution: {integrity: sha512-YZ3bNDrS8v5KiqgWE0xZQgtXgCTUacgFtnEgI4ccotAASwSvcMPDLua7BWLuTfucoRv6mPidXkITJLd8IdJplQ==} + '@next/swc-linux-x64-musl@15.5.4': + resolution: {integrity: sha512-nlQQ6nfgN0nCO/KuyEUwwOdwQIGjOs4WNMjEUtpIQJPR2NUfmGpW2wkJln1d4nJ7oUzd1g4GivH5GoEPBgfsdw==} engines: {node: '>= 10'} cpu: [x64] os: [linux] - '@next/swc-win32-arm64-msvc@15.4.5': - resolution: {integrity: sha512-9Wr4t9GkZmMNcTVvSloFtjzbH4vtT4a8+UHqDoVnxA5QyfWe6c5flTH1BIWPGNWSUlofc8dVJAE7j84FQgskvQ==} + '@next/swc-win32-arm64-msvc@15.5.4': + resolution: {integrity: sha512-PcR2bN7FlM32XM6eumklmyWLLbu2vs+D7nJX8OAIoWy69Kef8mfiN4e8TUv2KohprwifdpFKPzIP1njuCjD0YA==} engines: {node: '>= 10'} cpu: [arm64] os: [win32] - '@next/swc-win32-x64-msvc@15.4.5': - resolution: {integrity: sha512-voWk7XtGvlsP+w8VBz7lqp8Y+dYw/MTI4KeS0gTVtfdhdJ5QwhXLmNrndFOin/MDoCvUaLWMkYKATaCoUkt2/A==} + '@next/swc-win32-x64-msvc@15.5.4': + resolution: {integrity: sha512-1ur2tSHZj8Px/KMAthmuI9FMp/YFusMMGoRNJaRZMOlSkgvLjzosSdQI0cJAKogdHl3qXUQKL9MGaYvKwA7DXg==} engines: {node: '>= 10'} cpu: [x64] os: [win32] @@ -1548,8 +1548,8 @@ packages: resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==} engines: {node: '>=10'} - eslint-config-next@15.4.5: - resolution: {integrity: sha512-IMijiXaZ43qFB+Gcpnb374ipTKD8JIyVNR+6VsifFQ/LHyx+A9wgcgSIhCX5PYSjwOoSYD5LtNHKlM5uc23eww==} + eslint-config-next@15.5.4: + resolution: {integrity: sha512-BzgVVuT3kfJes8i2GHenC1SRJ+W3BTML11lAOYFOOPzrk2xp66jBOAGEFRw+3LkYCln5UzvFsLhojrshb5Zfaw==} peerDependencies: eslint: ^7.23.0 || ^8.0.0 || ^9.0.0 typescript: '>=3.3.1' @@ -2179,8 +2179,8 @@ packages: react: '>=16.0.0' react-dom: '>=16.0.0' - next@15.4.5: - resolution: {integrity: sha512-nJ4v+IO9CPmbmcvsPebIoX3Q+S7f6Fu08/dEWu0Ttfa+wVwQRh9epcmsyCPjmL2b8MxC+CkBR97jgDhUUztI3g==} + next@15.5.4: + resolution: {integrity: sha512-xH4Yjhb82sFYQfY3vbkJfgSDgXvBB6a8xPs9i35k6oZJRoQRihZH+4s9Yo2qsWpzBmZ3lPXaJ2KPXLfkvW4LnA==} engines: {node: ^18.18.0 || ^19.8.0 || >= 20.0.0} hasBin: true peerDependencies: @@ -3320,34 +3320,34 @@ snapshots: '@neon-rs/load@0.0.4': optional: true - '@next/env@15.4.5': {} + '@next/env@15.5.4': {} - '@next/eslint-plugin-next@15.4.5': + '@next/eslint-plugin-next@15.5.4': dependencies: fast-glob: 3.3.1 - '@next/swc-darwin-arm64@15.4.5': + '@next/swc-darwin-arm64@15.5.4': optional: true - '@next/swc-darwin-x64@15.4.5': + '@next/swc-darwin-x64@15.5.4': optional: true - '@next/swc-linux-arm64-gnu@15.4.5': + '@next/swc-linux-arm64-gnu@15.5.4': optional: true - '@next/swc-linux-arm64-musl@15.4.5': + '@next/swc-linux-arm64-musl@15.5.4': optional: true - '@next/swc-linux-x64-gnu@15.4.5': + '@next/swc-linux-x64-gnu@15.5.4': optional: true - '@next/swc-linux-x64-musl@15.4.5': + '@next/swc-linux-x64-musl@15.5.4': optional: true - '@next/swc-win32-arm64-msvc@15.4.5': + '@next/swc-win32-arm64-msvc@15.5.4': optional: true - '@next/swc-win32-x64-msvc@15.4.5': + '@next/swc-win32-x64-msvc@15.5.4': optional: true '@nodelib/fs.scandir@2.1.5': @@ -4109,9 +4109,9 @@ snapshots: escape-string-regexp@4.0.0: {} - eslint-config-next@15.4.5(eslint@9.32.0(jiti@1.21.7))(typescript@5.9.2): + eslint-config-next@15.5.4(eslint@9.32.0(jiti@1.21.7))(typescript@5.9.2): dependencies: - '@next/eslint-plugin-next': 15.4.5 + '@next/eslint-plugin-next': 15.5.4 '@rushstack/eslint-patch': 1.12.0 '@typescript-eslint/eslint-plugin': 8.38.0(@typescript-eslint/parser@8.38.0(eslint@9.32.0(jiti@1.21.7))(typescript@5.9.2))(eslint@9.32.0(jiti@1.21.7))(typescript@5.9.2) '@typescript-eslint/parser': 8.38.0(eslint@9.32.0(jiti@1.21.7))(typescript@5.9.2) @@ -4789,29 +4789,29 @@ snapshots: natural-compare@1.4.0: {} - next-auth@5.0.0-beta.29(next@15.4.5(react-dom@19.1.1(react@19.1.1))(react@19.1.1))(react@19.1.1): + next-auth@5.0.0-beta.29(next@15.5.4(react-dom@19.1.1(react@19.1.1))(react@19.1.1))(react@19.1.1): dependencies: '@auth/core': 0.40.0 - next: 15.4.5(react-dom@19.1.1(react@19.1.1))(react@19.1.1) + next: 15.5.4(react-dom@19.1.1(react@19.1.1))(react@19.1.1) react: 19.1.1 - next-cloudinary@6.16.0(next@15.4.5(react-dom@19.1.1(react@19.1.1))(react@19.1.1))(react@19.1.1): + next-cloudinary@6.16.0(next@15.5.4(react-dom@19.1.1(react@19.1.1))(react@19.1.1))(react@19.1.1): dependencies: '@cloudinary-util/types': 1.5.10 '@cloudinary-util/url-loader': 5.10.4 '@cloudinary-util/util': 4.0.0 - next: 15.4.5(react-dom@19.1.1(react@19.1.1))(react@19.1.1) + next: 15.5.4(react-dom@19.1.1(react@19.1.1))(react@19.1.1) react: 19.1.1 - next-seo@6.8.0(next@15.4.5(react-dom@19.1.1(react@19.1.1))(react@19.1.1))(react-dom@19.1.1(react@19.1.1))(react@19.1.1): + next-seo@6.8.0(next@15.5.4(react-dom@19.1.1(react@19.1.1))(react@19.1.1))(react-dom@19.1.1(react@19.1.1))(react@19.1.1): dependencies: - next: 15.4.5(react-dom@19.1.1(react@19.1.1))(react@19.1.1) + next: 15.5.4(react-dom@19.1.1(react@19.1.1))(react@19.1.1) react: 19.1.1 react-dom: 19.1.1(react@19.1.1) - next@15.4.5(react-dom@19.1.1(react@19.1.1))(react@19.1.1): + next@15.5.4(react-dom@19.1.1(react@19.1.1))(react@19.1.1): dependencies: - '@next/env': 15.4.5 + '@next/env': 15.5.4 '@swc/helpers': 0.5.15 caniuse-lite: 1.0.30001731 postcss: 8.4.31 @@ -4819,23 +4819,23 @@ snapshots: react-dom: 19.1.1(react@19.1.1) styled-jsx: 5.1.6(react@19.1.1) optionalDependencies: - '@next/swc-darwin-arm64': 15.4.5 - '@next/swc-darwin-x64': 15.4.5 - '@next/swc-linux-arm64-gnu': 15.4.5 - '@next/swc-linux-arm64-musl': 15.4.5 - '@next/swc-linux-x64-gnu': 15.4.5 - '@next/swc-linux-x64-musl': 15.4.5 - '@next/swc-win32-arm64-msvc': 15.4.5 - '@next/swc-win32-x64-msvc': 15.4.5 + '@next/swc-darwin-arm64': 15.5.4 + '@next/swc-darwin-x64': 15.5.4 + '@next/swc-linux-arm64-gnu': 15.5.4 + '@next/swc-linux-arm64-musl': 15.5.4 + '@next/swc-linux-x64-gnu': 15.5.4 + '@next/swc-linux-x64-musl': 15.5.4 + '@next/swc-win32-arm64-msvc': 15.5.4 + '@next/swc-win32-x64-msvc': 15.5.4 sharp: 0.34.3 transitivePeerDependencies: - '@babel/core' - babel-plugin-macros - nextjs-progressbar@0.0.16(next@15.4.5(react-dom@19.1.1(react@19.1.1))(react@19.1.1))(react@19.1.1): + nextjs-progressbar@0.0.16(next@15.5.4(react-dom@19.1.1(react@19.1.1))(react@19.1.1))(react@19.1.1): dependencies: '@types/nprogress': 0.2.3 - next: 15.4.5(react-dom@19.1.1(react@19.1.1))(react@19.1.1) + next: 15.5.4(react-dom@19.1.1(react@19.1.1))(react@19.1.1) nprogress: 0.2.0 prop-types: 15.8.1 react: 19.1.1 diff --git a/src/app/(public)/_components/hero.tsx b/src/app/(public)/_components/hero.tsx index 7f2ad7c..f8fd965 100644 --- a/src/app/(public)/_components/hero.tsx +++ b/src/app/(public)/_components/hero.tsx @@ -1,11 +1,10 @@ 'use client'; import { useRouter } from 'next/navigation'; +import { useMemo, useState } from 'react'; import { Search } from 'lucide-react'; -import { LanguageButton } from './language-button'; import { Button } from './button'; -import Link from 'next/link'; import { sortByName } from '@/lib/utils'; import languages from '@/assets/languages.json'; @@ -17,11 +16,38 @@ const { main: mainLanguages, others: otherLanguages } = languages; export function Hero() { const router = useRouter(); + // Track selected languages as a string array + const [selected, setSelected] = useState([]); + + const toggleLanguage = (language: string) => { + setSelected(prev => + prev.includes(language) + ? prev.filter(l => l !== language) + : [...prev, language] + ); + }; + + const sortedOthers = useMemo(() => [...otherLanguages].sort(sortByName), []); + function handleSearch(e: React.FormEvent) { + e.preventDefault(); const formData = new FormData(e.target as HTMLFormElement); - const lang = formData.get('search') as string; - if (lang.trim() === '') return; - router.push(`/repos/${lang}`); + const chosenLanguages = [...selected]; + + // Fallback: if no checkbox selected, use the single input value + if (chosenLanguages.length === 0) { + const typed = String(formData.get('search') || '').trim(); + if (typed) { + chosenLanguages.push(typed); + } + } + + if (chosenLanguages.length === 0) return; // nothing to search + + const params = new URLSearchParams(); + chosenLanguages.forEach(lang => params.append('l', lang.toLowerCase())); + + router.push(`/repos?${params.toString()}`); } return ( @@ -29,60 +55,102 @@ export function Hero() {

- Search your language + Search your language(s)

-

- Or select the programming language you would like to find + Or select one or more programming languages you would like to find repositories for.

+
- {mainLanguages.map(language => ( - - ))} + {mainLanguages.map(language => { + const id = `lang-${language}`; + const checked = selected.includes(language); + return ( + + ); + })}
-
-
    - {otherLanguages.sort(sortByName).map(language => ( -
  • - - {language} - -
  • - ))} + {sortedOthers.map(language => { + const id = `other-${language}`; + const checked = selected.includes(language); + return ( +
  • + +
  • + ); + })}
+
+ +
diff --git a/src/app/(public)/_components/search-form.tsx b/src/app/(public)/_components/search-form.tsx index 9ceacc4..cb52e32 100644 --- a/src/app/(public)/_components/search-form.tsx +++ b/src/app/(public)/_components/search-form.tsx @@ -26,20 +26,20 @@ export function SearchForm() { } function onSubmit({ searchQuery }: FormValues) { + if (!pathname.startsWith('/repos')) return; + + const reposPathname = pathname as `/repos/${string}`; const trimmedQuery = searchQuery.trim(); if (trimmedQuery !== '') { const sp = new URLSearchParams(searchParams); sp.set('q', trimmedQuery); - router.push(`${pathname}?${sp.toString()}`); + router.push(`${reposPathname}?${sp.toString()}`); } } return (
-
+
@@ -43,13 +44,11 @@ export default async function ReposPage({

- {repos.total_count} + {Intl.NumberFormat().format(repos.total_count)} {' '} repositories for{' '} - {sp.q - ? sp.q + ' in ' + capitalize(decodeURIComponent(language)) - : capitalize(decodeURIComponent(language))} + {sp.q ? sp.q + ' in ' + languageName : languageName}

diff --git a/src/app/(public)/repos/[language]/_components/pagination.tsx b/src/app/(public)/repos/_components/pagination.tsx similarity index 100% rename from src/app/(public)/repos/[language]/_components/pagination.tsx rename to src/app/(public)/repos/_components/pagination.tsx diff --git a/src/app/(public)/repos/[language]/_components/repo-card.tsx b/src/app/(public)/repos/_components/repo-card.tsx similarity index 100% rename from src/app/(public)/repos/[language]/_components/repo-card.tsx rename to src/app/(public)/repos/_components/repo-card.tsx diff --git a/src/app/(public)/repos/[language]/_components/report-button.tsx b/src/app/(public)/repos/_components/report-button.tsx similarity index 100% rename from src/app/(public)/repos/[language]/_components/report-button.tsx rename to src/app/(public)/repos/_components/report-button.tsx diff --git a/src/app/(public)/repos/[language]/_components/scroll-to-top.tsx b/src/app/(public)/repos/_components/scroll-to-top.tsx similarity index 100% rename from src/app/(public)/repos/[language]/_components/scroll-to-top.tsx rename to src/app/(public)/repos/_components/scroll-to-top.tsx diff --git a/src/app/(public)/repos/[language]/_components/sorter.tsx b/src/app/(public)/repos/_components/sorter.tsx similarity index 98% rename from src/app/(public)/repos/[language]/_components/sorter.tsx rename to src/app/(public)/repos/_components/sorter.tsx index 5afa2fe..545c35c 100644 --- a/src/app/(public)/repos/[language]/_components/sorter.tsx +++ b/src/app/(public)/repos/_components/sorter.tsx @@ -20,9 +20,11 @@ enum SortTypes { LeastRecentlyUpdated = 'Least recently updated' } +type Pathname = '/repos' | `/repos/${string}`; + export function Sorter() { const searchParams = useSearchParams(); - const pathname = usePathname(); + const pathname = usePathname() as Pathname; const navigationItems = [ { diff --git a/src/app/(public)/repos/[language]/_components/stars-filter.tsx b/src/app/(public)/repos/_components/stars-filter.tsx similarity index 97% rename from src/app/(public)/repos/[language]/_components/stars-filter.tsx rename to src/app/(public)/repos/_components/stars-filter.tsx index 0075b7e..d2fcc1d 100644 --- a/src/app/(public)/repos/[language]/_components/stars-filter.tsx +++ b/src/app/(public)/repos/_components/stars-filter.tsx @@ -14,9 +14,11 @@ interface FormValues { endStars: number | ''; } +type Pathname = '/repos' | `/repos/${string}`; + export function StarsFilter() { const router = useRouter(); - const pathname = usePathname(); + const pathname = usePathname() as Pathname; const searchParams = useSearchParams(); const params = useParams(); const { handleSubmit, control, reset } = useForm({ @@ -95,4 +97,4 @@ export function StarsFilter() {
); -} \ No newline at end of file +} diff --git a/src/app/(public)/repos/loading.tsx b/src/app/(public)/repos/loading.tsx new file mode 100644 index 0000000..7206bf3 --- /dev/null +++ b/src/app/(public)/repos/loading.tsx @@ -0,0 +1,109 @@ +export default function Loading() { + return ( + <> + {/* Header Skeleton */} +
+
+
+
+
+
+
+
+
+
+
+ +
+
+ {/* Title Section Skeleton */} +
+
+
+
+
+
+
+
+ + {/* Sorter Skeleton */} +
+
+
+
+
+
+
+ + {/* Stars Filter Skeleton */} +
+
+
+
+
+
+
+ + {/* Repository Cards Grid Skeleton */} +
+ {Array.from({ length: 21 }).map((_, index) => ( +
+ {/* Repository Title */} +
+
+
+
+
+ + {/* Language and Stats */} +
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ + {/* Topics */} +
+
+
+
+
+ + {/* Action Button */} +
+
+ ))} +
+
+ + {/* Pagination Skeleton */} +
+
+
+ {Array.from({ length: 5 }).map((_, index) => ( +
+ ))} +
+
+
+
+ + ); +} diff --git a/src/app/(public)/repos/page.tsx b/src/app/(public)/repos/page.tsx new file mode 100644 index 0000000..3b03988 --- /dev/null +++ b/src/app/(public)/repos/page.tsx @@ -0,0 +1,157 @@ +import { env } from '@/env.mjs'; +import { notFound } from 'next/navigation'; +import { Header } from '@/app/(public)/_components/header'; +import { ScrollToTop } from './_components/scroll-to-top'; +import { RepoCard } from './_components/repo-card'; +import { Sorter } from './_components/sorter'; +import { StarsFilter } from './_components/stars-filter'; +import { Pagination } from './_components/pagination'; +import { auth } from '@/auth'; +import { db } from '@/lib/db/connection'; +import { accountsTable, reportsTable } from '@/lib/db/migrations/schema'; +import { eq } from 'drizzle-orm'; +import type { RepoResponse, RepoData, RepoItem, SearchParams } from '@/types'; +import { capitalize } from '@/lib/utils'; + +export default async function ReposPage({ + searchParams +}: { + searchParams: Promise; +}) { + const sp = await searchParams; + const langs: string[] = Array.isArray(sp.l) + ? sp.l + : sp.l + ? [String(sp.l)] + : []; + + const reposRes = await getRepos(langs, sp); + if (!reposRes) notFound(); + + const { repos, page } = reposRes; + const languagesList = langs + .map(lang => capitalize(decodeURIComponent(lang))) + .join(', '); + + return ( + <> +
+ +
+
+
+
+
+

+ + {Intl.NumberFormat().format(repos.total_count)} + {' '} + repositories for{' '} + + {sp.q ? sp.q + ' in ' + languagesList : languagesList} + +

+
+
+ + +
+ {repos.items.map(repo => ( + + ))} +
+
+ +
+
+ + ); +} + +async function getRepos( + languages: string[], + searchParams: SearchParams +): Promise { + const session = await auth(); + const { + p: page = '1', + s: sort = '', + o: order = 'desc', + q: searchQuery = '', + startStars = '1', + endStars = '' + } = searchParams; + + const starsQuery = + startStars && endStars + ? `stars:${startStars}..${endStars}` + : startStars && !endStars + ? `stars:>${startStars}` + : !startStars && endStars + ? `stars:<${endStars}` + : ''; + + const combinedLangs = languages.map(l => `language:${l}`).join(' '); + + const apiUrl = new URL('https://api.github.com/search/repositories'); + apiUrl.searchParams.set('page', page.toString()); + apiUrl.searchParams.set('per_page', '21'); + apiUrl.searchParams.set('sort', sort.toString()); + apiUrl.searchParams.set('order', order.toString()); + apiUrl.searchParams.set( + 'q', + `topic:hacktoberfest ${combinedLangs} ${searchQuery} ${starsQuery}` + ); + + const headers: HeadersInit = { + Accept: 'application/vnd.github.mercy-preview+json' + }; + + const userId = session?.user?.id; + + if (userId) { + const [account] = await db + .select() + .from(accountsTable) + .where(eq(accountsTable.userId, userId)) + .limit(1); + + if (account && account.access_token) { + headers.Authorization = `Bearer ${account.access_token}`; + } else if (env.AUTH_GITHUB_TOKEN) { + headers.Authorization = `Bearer ${env.AUTH_GITHUB_TOKEN}`; + } + } else if (env.AUTH_GITHUB_TOKEN) { + headers.Authorization = `Bearer ${env.AUTH_GITHUB_TOKEN}`; + } + + const res = await fetch(apiUrl, { headers }); + if (!res.ok) return undefined; + + const repos = (await res.json()) as RepoData; + const reports = await getReportedRepos(); + + repos.items = repos.items.filter((repo: RepoItem) => { + return !repo.archived && !reports.find(report => report.repoId === repo.id); + }); + + return { + page: +page.toString(), + languageName: languages.join(', '), + repos + }; +} + +async function getReportedRepos() { + const reports = await db + .select() + .from(reportsTable) + .where(eq(reportsTable.valid, false)) + .limit(100); + + return reports; +}