diff --git a/README.md b/README.md index dbc27b0..57deb75 100644 --- a/README.md +++ b/README.md @@ -39,7 +39,7 @@ Also shows pretty programming icons using `devicons`. ## Inspiration -Needed a way to display all my projects, used my [portfolio's project section](https://2kabhishek.github.io/#projects) as inspiration. +Needed a way to display all my projects, that should auto update from GitHub ## Getting Projects @@ -50,35 +50,34 @@ git clone https://github.com/2kabhishek/projects cd projects ``` -## Setup Your Own Projects +### Setup Your Own Projects -You can easily set up projects to show your own repos. +You can set up projects for yourself by following these steps: -- Fork the repo -- Clone it +- Fork the repo: https://github.com/2kabhishek/projects +- Clone it locally / Open it in GitHub Codespaces - Open up `script.js` and update the `username` variable to your GitHub username. -- Open up `index.html` and update the `title` tag to make it your username. -- If you want to update the favicon too, update the `link` tag for it in `index.html` -- Push your changes -- Go to repo settings on GitHub and enable GitHub Pages. +- Open up `index.html` and update the `title` tag to make it your own. +- You may also want to update the favicon by updating the `link` tag in `index.html` +- Commit and push your changes +- Go to repo settings on GitHub, under `Pages` enable GitHub Pages. + - Choose 'Deploy from a branch' with the `main` branch and `/` as the root directory. +- Done! The site should be live on `https://.github.io/projects` -Here's the projects page for [@sindresorhus](https://github.com/sindresorhus) +#### Number Of Repos -![sindresorhus's projects](./images/demo.png) - -### Number Of Repos - -The number of repos is controlled by the `maxPages` variable, the GitHub API supports 100 repos per page max. +The number of repos shown changes with the `maxPages` variable, the GitHub API supports 100 repos per page max. If you have less than 100 repos, set `maxPages` to 1, if you have 300 then 3. You can also edit the fetch query to reduce the per page repo count. -> There's no pagination, all repos are shown on the same page. + +> There's no pagination, all repos show up on the same page. ### Forked Repos -Forked repos are hidden by default, to show them set `hideForks = false` in `script.js` +To show forked repos set `hideForks = false` in `script.js` ### Authenticated Requests @@ -88,30 +87,23 @@ You can either wait for an hour or setup a personal access token on GitHub and p ### Themes -Comes with a dark and light theme by default, depends upon your system configs. -Edit the variables under `:root` in `styles.css` to modify colorscheme. +Comes with a dark and light theme by default, depends upon your system and browser settings. -![Light screenshot](./images/screenshot-light.png) +Edit the variables under `:root` in `styles.css` to change color scheme. ### Programming Language Icons -This project uses [Devicon](https://devicon.dev/) for adding language icons, if the language name and icon are not being -displayed correctly for any of your repos, update `devicons` mapping in `script.js`. - -## Viewing projects - -Open `index.html` in your favorite browser or visit [2kabhishek.github.io/projects](https://2kabhishek.github.io/projects). +This project uses [Devicon](https://devicon.dev/) for adding language icons, if the language name and icon are not +displayed, for any of your repos, update `devicons` mapping in `script.js`. ## How it was built -Projects was built using `HTML` `CSS` & `JavaScript`. -It was built on Neovim and the python http server. -Uses GitHub API for data and Devicons for programming icons. +I built `Projects` using `HTML` `CSS` `JavaScript` and Neovim ## What I learned -- Learned about a few quirks of the fetch API, especially implementation of `maxPages`. -- Flex, box-shadow and some other CSS tricks were revisited. +- Learned about some quirks of the fetch API, during implementation of `maxPages`. +- Revisited Flex, box-shadow and some other CSS tricks ## What's next diff --git a/images/demo.png b/images/demo.png deleted file mode 100644 index 9ff02d4..0000000 Binary files a/images/demo.png and /dev/null differ diff --git a/images/screenshot-light.png b/images/screenshot-light.png deleted file mode 100644 index 8bd0383..0000000 Binary files a/images/screenshot-light.png and /dev/null differ diff --git a/images/screenshot.png b/images/screenshot.png index 7989e22..3ff72d9 100644 Binary files a/images/screenshot.png and b/images/screenshot.png differ diff --git a/index.html b/index.html index 8131bc5..b00a7d9 100644 --- a/index.html +++ b/index.html @@ -1,31 +1,47 @@ - + - - + 2KAbhishek's Projects - + + + + - + - + - +
-

All Of My Projects

- Some useful, some stupid, all fun! -
- -
+

All Of My Projects

+
+ +
-
- -
    -
    -

    Made with ❤ by 2KAbhishek X OSS

    +
    + +
      +
      +

      + Made with ❤ by + 2KAbhishek X OSS +

      - - + diff --git a/script.js b/script.js index 6b0eed2..f23401a 100644 --- a/script.js +++ b/script.js @@ -2,139 +2,218 @@ const username = '2KAbhishek'; const maxPages = 3; const hideForks = true; const repoList = document.querySelector('.repo-list'); -const reposSection = document.querySelector('.repos'); const filterInput = document.querySelector('.filter-repos'); -// get information from github profile -const getProfile = async () => { - const res = await fetch( - `https://api.github.com/users/${username}` - // { - // headers: { - // Accept: 'application/vnd.github+json', - // Authorization: 'token your-personal-access-token-here' - // } - // } - ); - const profile = await res.json(); - displayProfile(profile); +const fetchProfile = async () => { + const res = await fetch(`https://api.github.com/users/${username}`); + if (!res.ok) { + if (res.status === 403) { + throw new Error('GitHub API rate limit exceeded. Please try again later.'); + } + throw new Error(`GitHub API error: ${res.status} ${res.statusText}`); + } + return res.json(); }; -getProfile(); -// display infomation from github profile -const displayProfile = (profile) => { +const displayProfile = (profile, totalStars, totalForks) => { const userInfo = document.querySelector('.user-info'); + if (!userInfo) return; + + const userHome = `https://github.com/${profile.login}`; + const blogUrl = profile.blog + ? (profile.blog.startsWith('http') ? profile.blog : `https://${profile.blog}`) + : userHome; + + const companyHtml = profile.company ? `Work: ${profile.company}` : ''; + const locationHtml = profile.location ? `Location: ${profile.location}` : ''; + const extraInfoHtml = (companyHtml || locationHtml) + ? `

      ${companyHtml}${companyHtml && locationHtml ? '  |  ' : ''}${locationHtml}

      ` + : ''; + + const starsText = `${totalStars !== null ? totalStars : '--'}`; + const forksText = `${totalForks !== null ? totalForks : '--'}`; + const followersLink = `${profile.followers ?? 0}`; + const reposLink = `${profile.public_repos ?? 0}`; + const gistsLink = `${profile.public_gists ?? 0}`; + + const displayName = profile.name ? `${profile.name} (${profile.login})` : profile.login; + userInfo.innerHTML = `
      - user avatar + user avatar
      -

      ${profile.name}

      -

      ${profile.bio}

      +

      ${displayName}

      + ${profile.bio ? `

      ${profile.bio}

      ` : ''}

      - Location: ${profile.location} -

      -

      - @${profile.login} - Repos: ${profile.public_repos} - Gists: ${profile.public_gists} + Stars: ${starsText}  |  + Forks: ${forksText}  |  + Followers: ${followersLink}  |  + Repos: ${reposLink}  |  + Gists: ${gistsLink}

      + ${extraInfoHtml}
      `; }; -// get list of user's public repos -const getRepos = async () => { +const fetchRepos = async () => { let repos = []; - let res; for (let i = 1; i <= maxPages; i++) { - res = await fetch( + const res = await fetch( `https://api.github.com/users/${username}/repos?&sort=pushed&per_page=100&page=${i}` - // { - // headers: { - // Accept: 'application/vnd.github+json', - // Authorization: - // 'token your-personal-access-token-here' - // } - // } ); - let data = await res.json(); - repos = repos.concat(data); + if (!res.ok) { + if (res.status === 403) { + throw new Error('GitHub API rate limit exceeded. Please try again later.'); + } + throw new Error(`GitHub API error: ${res.status} ${res.statusText}`); + } + const data = await res.json(); + if (Array.isArray(data)) { + repos = repos.concat(data); + if (data.length < 100) { + break; + } + } else { + break; + } } - repos.sort((a, b) => b.forks_count - a.forks_count); - repos.sort((a, b) => b.stargazers_count - a.stargazers_count); - displayRepos(repos); + return repos; }; -getRepos(); -// display list of all user's public repos const displayRepos = (repos) => { - const userHome = `https://github.com/${username}` + const userHome = `https://github.com/${username}`; filterInput.classList.remove('hide'); + repoList.innerHTML = ''; for (const repo of repos) { if (repo.fork && hideForks) { continue; } - const langUrl = `${userHome}?tab=repositories&q=&language=${repo.language}` - const starsUrl = `${userHome}/${repo.name}/stargazers` - const forksUrl = `${userHome}/${repo.name}/network/members` + const langUrl = `${userHome}?tab=repositories&q=&language=${encodeURIComponent(repo.language || '')}`; + const starsUrl = `${userHome}/${repo.name}/stargazers`; + const forksUrl = `${userHome}/${repo.name}/network/members`; let listItem = document.createElement('li'); listItem.classList.add('repo'); - listItem.innerHTML = ` -

      ${repo.name}

      - ${repo.description}

      ` - if (repo.stargazers_count > 0) { - listItem.innerHTML += ` - ⭐ ${repo.stargazers_count}` - } + const starsHtml = repo.stargazers_count > 0 + ? `⭐ ${repo.stargazers_count}` + : ''; + let langHtml = ''; if (repo.language) { - listItem.innerHTML += ` - ${devicons[repo.language]}` + const icon = devicons[repo.language]; + langHtml = icon + ? `${icon}` + : `${repo.language}`; } - if (repo.forks_count > 0) { - listItem.innerHTML += ` - ${devicons["Git"]} ${repo.forks_count}` - } + const forksHtml = repo.forks_count > 0 + ? `${devicons['Git']} ${repo.forks_count}` + : ''; - if (repo.homepage && repo.homepage !== "") { - listItem.innerHTML += `

      - Code ${devicons["Github"]} - Live ${devicons["Chrome"]}
      `; - } else { - listItem.innerHTML += `

      - View Project ${devicons["Github"]}
      `; - } + const statsHtml = (starsHtml || langHtml || forksHtml) + ? `
      ${starsHtml}${langHtml}${forksHtml}
      ` + : ''; + + const homepageHtml = repo.homepage + ? `${devicons['Chrome']} Live` + : ''; + + listItem.innerHTML = ` +
      +

      ${repo.name}

      + ${repo.description || 'No description provided.'} +
      + ${statsHtml} +
      + ${devicons['Github']} Code + ${homepageHtml} +
      + `; repoList.append(listItem); } }; -// dynamic search +(async () => { + const profilePromise = fetchProfile().catch((err) => { + console.error('Failed to load profile:', err); + const userInfo = document.querySelector('.user-info'); + if (userInfo) { + userInfo.innerHTML = `

      Failed to load profile: ${err.message}

      `; + } + return null; + }); + + const reposPromise = fetchRepos().catch((err) => { + console.error('Failed to load repos:', err); + repoList.innerHTML = `

      Failed to load repositories: ${err.message}

      `; + return null; + }); + + const [profileData, reposData] = await Promise.all([profilePromise, reposPromise]); + + let totalStars = null; + let totalForks = null; + + if (reposData) { + reposData.sort((a, b) => b.forks_count - a.forks_count); + reposData.sort((a, b) => b.stargazers_count - a.stargazers_count); + + totalStars = reposData + .filter((repo) => repo && !repo.fork) + .reduce((sum, repo) => sum + (repo.stargazers_count || 0), 0); + + totalForks = reposData + .filter((repo) => repo && !repo.fork) + .reduce((sum, repo) => sum + (repo.forks_count || 0), 0); + + displayRepos(reposData); + } + + if (profileData) { + displayProfile(profileData, totalStars, totalForks); + } +})(); + filterInput.addEventListener('input', (e) => { const search = e.target.value; const repos = document.querySelectorAll('.repo'); const searchLowerText = search.toLowerCase(); + let visibleCount = 0; for (const repo of repos) { const lowerText = repo.innerText.toLowerCase(); if (lowerText.includes(searchLowerText)) { repo.classList.remove('hide'); + visibleCount++; } else { repo.classList.add('hide'); } } + + let noResultsMsg = document.querySelector('.no-results'); + if (visibleCount === 0) { + if (!noResultsMsg) { + noResultsMsg = document.createElement('p'); + noResultsMsg.classList.add('no-results'); + repoList.appendChild(noResultsMsg); + } + noResultsMsg.textContent = `No projects found matching "${search}"`; + } else { + if (noResultsMsg) { + noResultsMsg.remove(); + } + } }); -// for programming language icons const devicons = { Git: '', - Github: '', - Chrome: '', + Github: '', + Chrome: '', Assembly: ' Assembly', 'C#': ' C#', 'C++': ' C++', @@ -164,7 +243,7 @@ const devicons = { Matlab: ' Matlab', Nim: ' Nim', Nix: ' Nix', - ObjectiveC: ' ObjectiveC', + 'Objective-C': ' Objective-C', OCaml: ' OCaml', Perl: ' Perl', PHP: ' PHP', @@ -176,6 +255,7 @@ const devicons = { Ruby: ' Ruby', Rust: ' Rust', Sass: ' Sass', + SCSS: ' SCSS', Scala: ' Scala', Shell: ' Shell', Solidity: ' Solidity', @@ -184,6 +264,18 @@ const devicons = { Swift: ' Swift', Terraform: ' Terraform', TypeScript: ' TypeScript', - 'Vim Script': ' Vim Script', - Vue: ' Vue', + 'Vim script': ' Vim Script', + Vue: ' Vue' }; + +document.addEventListener('keydown', (e) => { + if (e.key === '/' && document.activeElement !== filterInput) { + e.preventDefault(); + filterInput.focus(); + } + if (e.key === 'Escape') { + filterInput.value = ''; + filterInput.dispatchEvent(new Event('input')); + filterInput.blur(); + } +}); diff --git a/style.css b/style.css index fdd7ec0..7d9f010 100644 --- a/style.css +++ b/style.css @@ -1,235 +1,354 @@ @media (prefers-color-scheme: light) { - :root { - --text: #151515; - --accent: #1688f0; - --bg-main: #fff; - --bg-repo: #f0f0f0; - --bg-user: #f5f5f5; - } + :root { + --text: #151515; + --accent: #1688f0; + --bg-main: #ffffff; + --bg-section: #f0f0f0; + --bg-card: #f5f5f5; + } } @media (prefers-color-scheme: dark) { - :root { - --text: #e5e5e5; - --accent: #1688f0; - --bg-main: #000; - --bg-repo: #0f0f1a; - --bg-user: #0a0a0f; - } + :root { + --text: #dedede; + --accent: #1688f0; + --bg-main: #000000; + --bg-section: #000000; + --bg-card: #000000; + } } * { - box-sizing: border-box; + box-sizing: border-box; } body { - display: flex; - align-items: center; - justify-content: center; - margin: 0; - font-family: 'Open Sans', sans-serif; - background-color: var(--bg-main); - color: var(--text); - font-size: 16px; + display: flex; + align-items: center; + justify-content: center; + margin: 0; + font-family: "Inter", sans-serif; + background-color: var(--bg-main); + color: var(--text); + font-size: 14px; +} + +h1, +h2, +h3, +.repo-name { + font-family: "Inter", sans-serif; } .container { - display: flex; - justify-content: center; - flex-direction: row; - flex-wrap: wrap; - align-items: center; - width: 90%; - max-width: 1280px; + display: flex; + justify-content: center; + flex-direction: row; + flex-wrap: wrap; + align-items: center; + width: 90%; + max-width: 1280px; } -.hide { - /* '!important' is to make sure that when we add the class using JS, it takes precendence */ - display: none !important; +.intro { + width: 100%; } -h1 { - color: var(--accent); - margin-bottom: 4px; +h4 { + width: 100%; + text-align: center; } -small { - width: 100%; - text-align: center; - display: block; - margin-bottom: 20px; +.hide { + /* '!important' is to make sure that when we add the class using JS, it takes precendence */ + display: none !important; +} + +h1 { + background: linear-gradient(135deg, var(--accent), #796bdc); + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; + margin-bottom: 4px; + font-weight: 800; + font-size: 2.8em; + text-align: center; + width: 100%; } h2 { - width: 100%; - text-align: center; - color: var(--text); - margin: 0; + width: 100%; + color: var(--text); + margin: 0; } img { - border: 2px solid var(--accent); - box-shadow: 0 0px 6px var(--accent), 0 0px 6px var(--accent); - max-width: 100%; - height: auto; + border: 2px solid var(--accent); + box-shadow: + 0 0px 6px var(--accent), + 0 0px 6px var(--accent); + max-width: 100%; + height: auto; } a { - color: var(--accent); - text-decoration: none; + color: var(--accent); + text-decoration: none; } a:hover { - color: var(--text); + color: var(--text); } .github-img { - width: 45px; - margin-right: 8px; + width: 45px; + margin-right: 8px; } ul { - list-style: none; - padding: 0; + list-style: none; + padding: 0; } .user-info { - display: flex; - flex-wrap: wrap; - justify-content: center; - background-color: var(--bg-user); - color: var(--text); - font-size: 18px; - padding: 2em; - border-top-left-radius: 20px; - border-top-right-radius: 20px; + display: flex; + flex-wrap: wrap; + justify-content: center; + background-color: var(--bg-card); + color: var(--text); + font-size: 16px; + padding: 1em; + border-top-left-radius: 20px; + border-top-right-radius: 20px; } .user-info figure { - width: 90%; - max-width: 200px; + width: 90%; + max-width: 200px; } .user-info img { - border-radius: 100%; + border-radius: 100%; } .user-info div { - display: flex; - text-align: center; - flex-wrap: wrap; - align-items: center; - width: 100%; - padding-left: 5%; + display: flex; + text-align: center; + flex-wrap: wrap; + align-items: center; + width: 100%; + padding-left: 5%; } .user-info div p { - width: 100%; - margin: 0; - padding: 0; - border-bottom: 1px solid var(--text); - padding-bottom: 18px; + width: 100%; + margin: 0; + padding: 0; + border-bottom: 1px solid var(--text); + padding-bottom: 18px; +} + +.user-info div p a { + color: inherit; +} + +.user-info div p a:hover { + color: var(--accent); } .filter-repos, input { - background: var(--bg-user); - width: 50%; - border-radius: 20px; - min-width: 300px; - margin-top: 1em; - padding: 1em; - margin-bottom: 1em; - border: 2px solid var(--accent); - color: var(--text); - text-align: center; + background: var(--bg-card); + width: 50%; + border-radius: 20px; + min-width: 300px; + margin-top: 1em; + padding: 1em; + margin-bottom: 1em; + border: 2px solid var(--accent); + color: var(--text); + text-align: center; } .repos { - display: flex; - flex-wrap: wrap; - justify-content: center; - background-color: var(--bg-repo); - margin-top: 0; - padding: 2em; - border-bottom-left-radius: 20px; - border-bottom-right-radius: 20px; - margin-bottom: 4em; + display: flex; + flex-wrap: wrap; + justify-content: center; + background-color: var(--bg-section); + margin-top: 0; + padding: 2em; + border-bottom-left-radius: 20px; + border-bottom-right-radius: 20px; + margin-bottom: 4em; + width: 100%; } h3 { - margin: 4px; - width: 100%; - text-align: center; - color: var(--accent); + margin: 4px; + width: 100%; + text-align: center; + color: var(--accent); } .repo-list { - width: 100%; - display: flex; - flex-wrap: wrap; - justify-content: center; - align-items: flex-start; + width: 100%; + display: grid; + grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); + gap: 1.5em; + margin-top: 1em; } .repo-list li { - width: 100%; - min-height: 180px; - border-radius: 10px; - margin: 1em 0; - border: 1px solid var(--accent); - padding: 1em; - text-align: center; - background-color: var(--bg-user); + min-height: 200px; + border-radius: 10px; + border: 2px solid var(--accent); + padding: 1.25em; + text-align: center; + background-color: var(--bg-card); + transition: + transform 0.2s ease, + box-shadow 0.2s ease, + background-color 0.2s ease; + display: flex; + flex-direction: column; + justify-content: flex-start; + align-items: center; + gap: 0.75em; +} + +.no-results { + grid-column: 1 / -1; + width: 100%; + text-align: center; + font-size: 1.2em; + margin-top: 2em; + color: var(--text); } .repo-list li:hover { - background-color: var(--bg-main); - box-shadow: 0 3px 6px var(--accent), 0 3px 6px var(--accent); + background-color: var(--bg-main); + box-shadow: + 0 8px 16px var(--accent), + 0 8px 16px var(--accent); + transform: translateY(-4px); } .link-btn { - padding: 10px 20px; - background: var(--bg-main); - border-radius: 30px; - border: 2px solid var(--accent); - color: var(--text); + display: inline-block; + margin: 0 0.25em; + padding: 8px 16px; + background: var(--bg-main); + border-radius: 10px; + border: 3px solid var(--accent); + color: var(--text); + transition: + transform 0.2s ease, + background-color 0.2s ease, + border-color 0.2s ease, + color 0.2s ease; } .link-btn:hover { - background: var(--accent); - border: 2px solid var(--text); - transform: scale(1.1); + background: var(--accent); + border: 3px solid var(--text); + color: var(--bg-main); + transform: scale(1.05); +} + +.link-btn i { + color: var(--accent); + transition: color 0.2s ease; +} + +.link-btn:hover i { + color: var(--bg-main); } .repo-list p { - padding: 0em 0.5em; - margin: 0.5em 0; + padding: 0.5em; + margin: 0.5em 0; } @media (min-width: 700px) { - .user-info div { - width: 45%; - text-align: left; - } + .user-info div { + flex: 0.7; + text-align: left; + } +} - .repo-list { - justify-content: space-between; - } +@media (max-width: 700px) { + .user-info div p { + margin: 10px; + } +} - .repo-list li { - width: 48%; - } +.repo-name { + font-size: 1.75em; } -@media (max-width: 700px) { - .user-info div p { - margin: 10px; - } +.repo-description { + font-size: 0.95em; + opacity: 0.8; + line-height: 1.5; + display: -webkit-box; + -webkit-line-clamp: 2; + -webkit-box-orient: vertical; + overflow: hidden; + text-overflow: ellipsis; + margin-bottom: 0.5em; + height: 3em; +} + +.repo-header { + width: 100%; + display: flex; + flex-direction: column; + align-items: center; + gap: 0.5em; +} + +.repo-stats { + width: 100%; + display: flex; + flex-wrap: wrap; + justify-content: center; + gap: 0.25em; + margin: 0.5em 0; +} + +.repo-actions { + width: 100%; + display: flex; + justify-content: center; + gap: 0.5em; + margin-top: auto; +} + +span { + margin: 0 0.5em; +} + +.repo-badge { + display: inline-flex; + align-items: center; + padding: 6px 12px; + border-radius: 20px; + background-color: var(--bg-section); + font-size: 0.9em; + border: 1px solid var(--accent); + color: var(--text); + margin: 0.25em !important; + transition: + background-color 0.2s ease, + border-color 0.2s ease, + color 0.2s ease; +} + +.repo-badge:hover { + background-color: var(--accent); + color: var(--bg-main); + border-color: var(--text); } -@media (min-width: 1200px) { - .repo-list li { - width: 30%; - } +.repo-badge i { + margin-right: 4px; }