diff --git a/.gitignore b/.gitignore index 9a6393f..1f67859 100644 --- a/.gitignore +++ b/.gitignore @@ -5,6 +5,7 @@ /.pnp .pnp.js package-lock.json +.env* # testing /coverage diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index b7bb20a..6759ddd 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -6,17 +6,23 @@ Thank you for your interest in contributing to the Hacktoberfest Projects Finder 1. Fork the repository on GitHub. 2. Clone your forked repository: + ```sh git clone https://github.com/your-username/hacktoberfest-projects.git ``` + 3. Navigate to the project directory: + ```sh cd hacktoberfest-projects ``` + 4. Install dependencies (we recommend using `pnpm`): + ```sh pnpm install ``` + 5. Copy the `.env.example` file to `.env.local` and fill in the required environment variables. ## Creating a GitHub OAuth Application @@ -27,9 +33,11 @@ To use GitHub authentication in the project, you need to create a GitHub OAuth a 2. Navigate to "Developer settings" > "OAuth Apps". 3. Click on "New OAuth App". 4. Fill in the application details: + - Application name: "Hacktoberfest Projects Finder" (or your preferred name) - Homepage URL: `http://localhost:3000` (for local development) - Authorization callback URL: `http://localhost:3000/api/auth/callback/github` + 5. Click "Register application". 6. On the next page, you'll see your Client ID. Click "Generate a new client secret" to create your Client Secret. 7. Copy the Client ID and Client Secret to your `.env.local` file. @@ -41,25 +49,39 @@ Xata is used as the database for this project. Follow these steps to set it up: 1. Sign up for a Xata account at https://lite.xata.io/ 2. Create a new workspace and database from Xata dashboard 3. Install the Xata CLI globally: + ```sh npm install -g "@xata.io/cli@latest" ``` + 4. Authenticate with Xata: + ```sh xata auth login ``` + 5. Initialize the database: + ```sh xata init ``` + 5. Upload the database schema: + ```sh xata schema upload db-schema.json ``` +6. Generate the Xata client: + +```sh +xata codegen +``` + ## Environment Variables Create a `.env.local` file in the root of the project and add the following variables: + ```sh AUTH_SECRET="" # A random string AUTH_URL="" # Should be http://localhost:3000 for local development @@ -72,13 +94,14 @@ NEXT_PUBLIC_CLOUDINARY_CLOUD_NAME="" # Optional NEXT_PUBLIC_ANALYTICS_WEBSITE_ID="" # Optional ``` -Make sure to fill in the required values for each variable. The `AUTH_SECRET` should be a random string, and `AUTH_URL` should be set to `http://localhost:3000` for local development. The `XATA_BRANCH` should typically be set to "main" unless you're using a different branch. +Make sure to fill in the required values for each variable. The `AUTH_SECRET` should be a random string, and `AUTH_URL` should be set to `http://localhost:3000` for local development. The `XATA_BRANCH` should typically be set to "main" unless you're using a different branch. -Remember to remove env variables that are optional and you are empty, they will cause validation errors +Remember to remove env variables that are optional and you are empty, they will cause validation errors ## Running the Project After setting up the environment variables, you can start the development server: + ```sh pnpm dev ``` @@ -88,14 +111,18 @@ The application should now be running at `http://localhost:3000`. ## Making Contributions 1. Create a new branch for your feature or bug fix: + ```sh git checkout -b feature/your-feature-name ``` + 2. Make your changes and commit them with a descriptive commit message. 3. Push your changes to your fork: + ```sh git push origin feature/your-feature-name ``` + 4. Create a pull request from your fork to the main repository. Please ensure your code follows the project's coding standards and includes appropriate tests if applicable. diff --git a/eslint.config.mjs b/eslint.config.mjs index 39ed735..eb2bf00 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -1,21 +1,24 @@ -import { defineConfig } from "eslint/config"; -import path from "node:path"; -import { fileURLToPath } from "node:url"; -import js from "@eslint/js"; -import { FlatCompat } from "@eslint/eslintrc"; +import { defineConfig } from 'eslint/config'; +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; +import js from '@eslint/js'; +import { FlatCompat } from '@eslint/eslintrc'; const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); const compat = new FlatCompat({ - baseDirectory: __dirname, - recommendedConfig: js.configs.recommended, - allConfig: js.configs.all + baseDirectory: __dirname, + recommendedConfig: js.configs.recommended, + allConfig: js.configs.all }); -export default defineConfig([{ - extends: compat.extends("next/core-web-vitals"), +export default defineConfig([ + { + extends: compat.extends('next/core-web-vitals'), rules: { - "@next/next/no-img-element": 0, - }, -}]); \ No newline at end of file + '@next/next/no-img-element': 0, + 'react/no-unescaped-entities': 0 + } + } +]); diff --git a/public/favicon.svg b/public/favicon.svg new file mode 100644 index 0000000..9b88d7b --- /dev/null +++ b/public/favicon.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/app/(public)/_components/button.tsx b/src/app/(public)/_components/button.tsx index 4547cf7..93b78dd 100644 --- a/src/app/(public)/_components/button.tsx +++ b/src/app/(public)/_components/button.tsx @@ -11,15 +11,18 @@ export function Button({ ...props }: PropsWithChildren) { return ( -
+
); diff --git a/src/app/(public)/_components/header.tsx b/src/app/(public)/_components/header.tsx index 8bbf09c..cfc4c10 100644 --- a/src/app/(public)/_components/header.tsx +++ b/src/app/(public)/_components/header.tsx @@ -6,33 +6,36 @@ import { BsPeopleFill } from 'react-icons/bs'; import { SearchForm } from './search-form'; import { auth } from '@/auth'; import { signInAction, signOutAction } from '../../actions'; +import { LogoIconsSvg } from '@/components/Icons'; +import { Button } from './button'; +import { MobileMenu } from './mobile-menu'; export async function Header() { const session = await auth(); return ( -
-
-
- - Hacktoberfest +
+
+
+ + - + {/* Desktop Search - Hidden on mobile */} +
+ +
-
+ {/* Desktop Navigation - Hidden on mobile */} +
- +
@@ -41,11 +44,19 @@ export async function Header() { href="https://github.com/max-programming/hacktoberfest-projects" target="_blank" rel="noreferrer" - className="btn btn-square btn-ghost umami--click--github-button" + className="btn btn-square btn-ghost umami--click--github-button hover:text-hacktoberfest-light transition-colors" >
+ + {/* Mobile Hamburger Menu */} + +
+ + {/* Mobile Search - Visible only on mobile */} +
+
diff --git a/src/app/(public)/_components/hero.tsx b/src/app/(public)/_components/hero.tsx index 40b1717..1e37ce7 100644 --- a/src/app/(public)/_components/hero.tsx +++ b/src/app/(public)/_components/hero.tsx @@ -9,6 +9,8 @@ import Link from 'next/link'; import { sortByName } from '@/lib/utils'; import languages from '@/assets/languages.json'; +import { HeroSectionSvg } from '@/components/Icons'; +import { MarqueTextAnimation } from './marque-text-animation'; const { main: mainLanguages, others: otherLanguages } = languages; @@ -23,15 +25,14 @@ export function Hero() { } return ( -
-
-
-
-

+
+
+
+

Search your language

@@ -39,27 +40,28 @@ export function Hero() {
-

+

Or select the programming language you would like to find repositories for.

- - {mainLanguages.map(language => ( - - ))} - +
+ {mainLanguages.map(language => ( + + ))} +
+ +
+
+

); diff --git a/src/app/(public)/_components/language-button.tsx b/src/app/(public)/_components/language-button.tsx index 2dd30f6..bc5c915 100644 --- a/src/app/(public)/_components/language-button.tsx +++ b/src/app/(public)/_components/language-button.tsx @@ -11,7 +11,7 @@ export function LanguageButton({ language }: LanguageButtonProps) { return ( - diff --git a/src/app/(public)/_components/marque-text-animation.tsx b/src/app/(public)/_components/marque-text-animation.tsx new file mode 100644 index 0000000..b71a78f --- /dev/null +++ b/src/app/(public)/_components/marque-text-animation.tsx @@ -0,0 +1,37 @@ +import React from 'react'; + +const MarqueeItem = () => ( + <> + {Array.from({ length: 4 }).map((_, i) => ( + + + Hacktoberfest 2025 + + + + ))} + +); + +export function MarqueTextAnimation() { + return ( +
+
+
+ +
+
+ +
+
+
+
+ +
+
+ +
+
+
+ ); +} diff --git a/src/app/(public)/_components/mobile-menu.tsx b/src/app/(public)/_components/mobile-menu.tsx new file mode 100644 index 0000000..1e7f2e2 --- /dev/null +++ b/src/app/(public)/_components/mobile-menu.tsx @@ -0,0 +1,67 @@ +'use client'; + +import Link from 'next/link'; +import { useState } from 'react'; + +import { IoLogoGithub } from 'react-icons/io'; +import { BsPeopleFill } from 'react-icons/bs'; +import { HiMenu, HiX } from 'react-icons/hi'; + +import { Button } from './button'; +import { signInAction, signOutAction } from '../../actions'; + +interface MobileMenuProps { + session: any; +} + +export function MobileMenu({ session }: MobileMenuProps) { + const [isOpen, setIsOpen] = useState(false); + + const toggleMenu = () => setIsOpen(!isOpen); + + return ( +
+ {/* Hamburger Button */} + + + {/* Mobile Menu Overlay */} + {isOpen && ( +
+
+
+ +
+ + setIsOpen(false)} + > + + Contributors + + + setIsOpen(false)} + > + + GitHub + +
+
+ )} +
+ ); +} diff --git a/src/app/(public)/_components/search-form.tsx b/src/app/(public)/_components/search-form.tsx index a2281a1..ecd3668 100644 --- a/src/app/(public)/_components/search-form.tsx +++ b/src/app/(public)/_components/search-form.tsx @@ -13,12 +13,14 @@ export function SearchForm() { const pathname = usePathname(); const searchParams = useSearchParams(); - const { register, handleSubmit, reset } = useForm({ + const { register, handleSubmit, reset, watch } = useForm({ defaultValues: { searchQuery: searchParams.get('q') as string } }); + const searchQuery = watch('searchQuery'); + if (!pathname.startsWith('/repos')) { return null; } @@ -41,17 +43,19 @@ export function SearchForm() {
- + {searchQuery && searchQuery.trim() !== '' && ( + + )}
diff --git a/src/app/(public)/contributors/_components/contributor-card.tsx b/src/app/(public)/contributors/_components/contributor-card.tsx index 8907ac2..2afcb1b 100644 --- a/src/app/(public)/contributors/_components/contributor-card.tsx +++ b/src/app/(public)/contributors/_components/contributor-card.tsx @@ -40,29 +40,33 @@ export function ContributorCard({ contributor }: ContributorCardProps) { return ( @@ -28,7 +28,7 @@ export function Pagination({ {totalCount >= MAX_PER_PAGE && page < Math.ceil(totalCount / MAX_PER_PAGE) && ( - diff --git a/src/app/(public)/repos/[language]/_components/repo-card.tsx b/src/app/(public)/repos/[language]/_components/repo-card.tsx index 1af332b..ed54ea0 100644 --- a/src/app/(public)/repos/[language]/_components/repo-card.tsx +++ b/src/app/(public)/repos/[language]/_components/repo-card.tsx @@ -32,33 +32,35 @@ export function RepoCard({ repo }: RepoCardProps) { const hasMoreTopics = sortedTopics?.length > MAX_TOPICS_DISPLAY; return ( -
+
@@ -69,7 +71,7 @@ export function RepoCard({ repo }: RepoCardProps) { href={repo.html_url + '?ref=finder.usmans.me'} target="_blank" rel="noreferrer" - className="text-hacktoberfest-pink ml-2 underline-expand" + className="text-hacktoberfest-light ml-2 underline-expand" > Read more @@ -86,15 +88,15 @@ export function RepoCard({ repo }: RepoCardProps) { className={cn( 'badge inline px-3 py-0.5 h-auto', topic === 'hacktoberfest' - ? 'bg-hacktoberfest-light-green text-hacktoberfest-dark-green' - : 'bg-hacktoberfest-deep-pink text-hacktoberfest-light-pink' + ? 'bg-hacktoberfest-beige text-hacktoberfest-blue' + : 'bg-hacktoberfest-light-blue text-hacktoberfest-light' )} > {topic} ))} {hasMoreTopics && ( - ... + ... )}
@@ -104,43 +106,49 @@ export function RepoCard({ repo }: RepoCardProps) { - +
-
+
{numberFormatter.format(repo.stargazers_count)}
-
Stars
+
+ Stars +
- +
-
+
{numberFormatter.format(repo.forks)}
-
Forks
+
+ Forks +
- + diff --git a/src/app/(public)/repos/[language]/_components/report-button.tsx b/src/app/(public)/repos/[language]/_components/report-button.tsx index 5e1378c..5930140 100644 --- a/src/app/(public)/repos/[language]/_components/report-button.tsx +++ b/src/app/(public)/repos/[language]/_components/report-button.tsx @@ -14,10 +14,9 @@ export function ReportButton({ repo }: ReportButtonProps) { return ( ); } diff --git a/src/app/(public)/repos/[language]/_components/scroll-to-top.tsx b/src/app/(public)/repos/[language]/_components/scroll-to-top.tsx index ade9b47..e0b94b5 100644 --- a/src/app/(public)/repos/[language]/_components/scroll-to-top.tsx +++ b/src/app/(public)/repos/[language]/_components/scroll-to-top.tsx @@ -34,7 +34,7 @@ export function ScrollToTop() { animate={{ opacity: 1 }} exit={{ opacity: 0 }} > - diff --git a/src/app/(public)/repos/[language]/_components/sorter.tsx b/src/app/(public)/repos/[language]/_components/sorter.tsx index 22741cd..5afa2fe 100644 --- a/src/app/(public)/repos/[language]/_components/sorter.tsx +++ b/src/app/(public)/repos/[language]/_components/sorter.tsx @@ -116,40 +116,37 @@ export function Sorter() { } return ( -
+
- -
+
    {mainLanguages.sort(sortByName).map(language => { const sp = new URLSearchParams(searchParams); sp.delete('p'); return ( -
  • - - {language} - -
  • +
  • + + {language} + +
  • ); - })} + })}
- -
+
    {navigationItems.map((item, index) => { const sp = item.onSelect(new URLSearchParams(searchParams)); @@ -160,7 +157,12 @@ export function Sorter() { } return (
  • - {item.name} + + {item.name} +
  • ); })} diff --git a/src/app/(public)/repos/[language]/_components/stars-filter.tsx b/src/app/(public)/repos/[language]/_components/stars-filter.tsx index 8f32d8d..8810bc0 100644 --- a/src/app/(public)/repos/[language]/_components/stars-filter.tsx +++ b/src/app/(public)/repos/[language]/_components/stars-filter.tsx @@ -53,7 +53,7 @@ export function StarsFilter() { return (
    @@ -62,7 +62,7 @@ export function StarsFilter() { render={({ field }) => ( field.onChange(e.target.valueAsNumber)} placeholder="Star's Starting Range" @@ -76,7 +76,7 @@ export function StarsFilter() { render={({ field }) => ( field.onChange(e.target.valueAsNumber)} placeholder="Star's Finish Range" @@ -89,7 +89,7 @@ export function StarsFilter() { {/* Flex container to center the button */}
    +
    + + {/* Decorative Elements */} +
    +
    +
    +
    +
diff --git a/src/components/Icons.tsx b/src/components/Icons.tsx new file mode 100644 index 0000000..0ab5aa5 --- /dev/null +++ b/src/components/Icons.tsx @@ -0,0 +1,1239 @@ +export const LogoIconsSvg = () => { + return ( + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ); +}; +export const HeroSectionSvg = ({ className }: { className?: string }) => { + return ( + + + + + + + + + + ); +}; diff --git a/src/components/footer.tsx b/src/components/footer.tsx index 0044ba9..cc65bcd 100644 --- a/src/components/footer.tsx +++ b/src/components/footer.tsx @@ -8,7 +8,7 @@ const formatter = new Intl.ListFormat('en', { export function Footer() { return ( -