diff --git a/README.md b/README.md index 84f13b2..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 @@ -52,55 +52,58 @@ cd 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. -- You may also want to update the favicon too, update the `link` tag 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](github.com/sindresorhus) - -![sindresorhus's projects](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/n78j6j4itdbad2cg2h0h.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. -#### Authenticated Requests +> There's no pagination, all repos show up on the same page. + +### Forked Repos + +To show forked repos set `hideForks = false` in `script.js` + +### Authenticated Requests If you are working locally and notice the API is not sending over data, it might be because of rate limit on GitHub API requests. You can either wait for an hour or setup a personal access token on GitHub and pass that into the fetch request in `script.js` -#### Programming Language Icons +### Themes + +Comes with a dark and light theme by default, depends upon your system and browser settings. -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`. +Edit the variables under `:root` in `styles.css` to change color scheme. -## Viewing projects +### Programming Language Icons -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/_config.yml b/_config.yml deleted file mode 100644 index 2f7efbe..0000000 --- a/_config.yml +++ /dev/null @@ -1 +0,0 @@ -theme: jekyll-theme-minimal \ No newline at end of file diff --git a/images/screenshot.png b/images/screenshot.png index a571dba..3ff72d9 100644 Binary files a/images/screenshot.png and b/images/screenshot.png differ diff --git a/index.html b/index.html index 9fd8630..b00a7d9 100644 --- a/index.html +++ b/index.html @@ -1,30 +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 +

      - - + diff --git a/script.js b/script.js index a6cfcde..f23401a 100644 --- a/script.js +++ b/script.js @@ -1,110 +1,226 @@ -const username = '2kabhishek'; -const maxPages = 2; +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 function () { - 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 = function (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 function () { +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; + } } - displayRepos(repos); + return repos; }; -getRepos(); -// display list of all user's public repos -const displayRepos = function (repos) { +const displayRepos = (repos) => { + 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=${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'); + + const starsHtml = repo.stargazers_count > 0 + ? `⭐ ${repo.stargazers_count}` + : ''; + + let langHtml = ''; + if (repo.language) { + const icon = devicons[repo.language]; + langHtml = icon + ? `${icon}` + : `${repo.language}`; + } + + const forksHtml = repo.forks_count > 0 + ? `${devicons['Git']} ${repo.forks_count}` + : ''; + + const statsHtml = (starsHtml || langHtml || forksHtml) + ? `
      ${starsHtml}${langHtml}${forksHtml}
      ` + : ''; + + const homepageHtml = repo.homepage + ? `${devicons['Chrome']} Live` + : ''; + listItem.innerHTML = ` -

      ${repo.name}

      - ${repo.description}

      - ${devicons[repo.language]}
      -
      - View Project`; +
      +

      ${repo.name}

      + ${repo.description || 'No description provided.'} +
      + ${statsHtml} +
      + ${devicons['Github']} Code + ${homepageHtml} +
      + `; + repoList.append(listItem); } }; -// dynamic search -filterInput.addEventListener('input', function (e) { +(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: '', Assembly: ' Assembly', 'C#': ' C#', 'C++': ' C++', C: ' C', Clojure: ' C', - CoffeeScript: ' CoffeeScript', + CoffeeScript: + ' CoffeeScript', Crystal: ' Crystal', CSS: ' CSS', Dart: ' Dart', @@ -127,7 +243,7 @@ const devicons = { Matlab: ' Matlab', Nim: ' Nim', Nix: ' Nix', - ObjectiveC: ' ObjectiveC', + 'Objective-C': ' Objective-C', OCaml: ' OCaml', Perl: ' Perl', PHP: ' PHP', @@ -139,6 +255,7 @@ const devicons = { Ruby: ' Ruby', Rust: ' Rust', Sass: ' Sass', + SCSS: ' SCSS', Scala: ' Scala', Shell: ' Shell', Solidity: ' Solidity', @@ -147,7 +264,18 @@ const devicons = { Swift: ' Swift', Terraform: ' Terraform', TypeScript: ' TypeScript', - 'Vim Script': ' Vim Script', - Vue: ' Vue', - null: ' Markdown' + '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 96f443c..7d9f010 100644 --- a/style.css +++ b/style.css @@ -1,219 +1,354 @@ -:root { - --text: #e5e5e5; +@media (prefers-color-scheme: light) { + :root { + --text: #151515; --accent: #1688f0; - --bg-main: #000; - --bg-repo: #0f0f1a; - --bg-user: #0a0a0f; + --bg-main: #ffffff; + --bg-section: #f0f0f0; + --bg-card: #f5f5f5; + } +} + +@media (prefers-color-scheme: dark) { + :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; +} + +.hide { + /* '!important' is to make sure that when we add the class using JS, it takes precendence */ + display: none !important; } -small { - width: 100%; - text-align: center; - display: block; - margin-bottom: 20px; +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); } .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); } -.repo-list li a { - padding: 10px 20px; - background: var(--bg-main); - border-radius: 30px; - border: 2px solid var(--accent); - color: var(--text); +.link-btn { + 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; } -.repo-list li a:hover { - background: var(--accent); - border: 2px solid var(--text); - transform: scale(1.1); +.link-btn:hover { + 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; + } +} + +@media (max-width: 700px) { + .user-info div p { + margin: 10px; + } +} - .repo-list { - justify-content: space-between; - } +.repo-name { + font-size: 1.75em; +} - .repo-list li { - width: 48%; - } +.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; } -@media (max-width: 700px) { - .user-info div p { - margin: 10px; - } +.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; }