31 Commits

Author SHA1 Message Date
dependabot[bot]
bca92b925a Bump postcss from 8.4.33 to 8.4.35
Bumps [postcss](https://github.com/postcss/postcss) from 8.4.33 to 8.4.35.
- [Release notes](https://github.com/postcss/postcss/releases)
- [Changelog](https://github.com/postcss/postcss/blob/main/CHANGELOG.md)
- [Commits](https://github.com/postcss/postcss/compare/8.4.33...8.4.35)

---
updated-dependencies:
- dependency-name: postcss
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-02-16 10:38:02 +00:00
Ariful Alam
4a5e9a8928 Merge pull request #495 from arifszn/dependabot/npm_and_yarn/vite-plugin-pwa-0.18.1
Bump vite-plugin-pwa from 0.17.4 to 0.18.1
2024-02-16 16:34:58 +06:00
Ariful Alam
30efe53c2f Merge pull request #501 from arifszn/update-versions-github-actions
Update the versions of Github Actions
2024-02-16 16:20:56 +06:00
Ariful Alam
16a9ab0296 Update the versions of Github Actions 2024-02-16 16:18:28 +06:00
Ariful Alam
911af25776 Merge pull request #500 from arifszn/update-node-version-github-actions
Update node version from 16 to 20 in `actions/setup-node`
2024-02-16 16:08:22 +06:00
Ariful Alam
71cfb3bc3d Update node version from 16 to 20 in actions/setup-node 2024-02-16 16:06:30 +06:00
Ariful Alam
c25f3e716a Merge pull request #494 from arifszn/dependabot/npm_and_yarn/types/react-18.2.55
Bump @types/react from 18.2.48 to 18.2.55
2024-02-13 19:05:54 +06:00
dependabot[bot]
69f6e0f908 Bump vite-plugin-pwa from 0.17.4 to 0.18.1
Bumps [vite-plugin-pwa](https://github.com/vite-pwa/vite-plugin-pwa) from 0.17.4 to 0.18.1.
- [Release notes](https://github.com/vite-pwa/vite-plugin-pwa/releases)
- [Commits](https://github.com/vite-pwa/vite-plugin-pwa/compare/v0.17.4...v0.18.1)

---
updated-dependencies:
- dependency-name: vite-plugin-pwa
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-02-12 08:39:47 +00:00
dependabot[bot]
b0248e1080 Bump @types/react from 18.2.48 to 18.2.55
Bumps [@types/react](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/react) from 18.2.48 to 18.2.55.
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/react)

---
updated-dependencies:
- dependency-name: "@types/react"
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-02-12 08:39:25 +00:00
Ariful Alam
d2d606650c Merge pull request #488 from arifszn/dependabot/npm_and_yarn/date-fns-3.3.1
Bump date-fns from 3.3.0 to 3.3.1
2024-02-02 19:17:34 +06:00
Ariful Alam
3b7b0cd1a8 Merge pull request #489 from arifszn/dependabot/npm_and_yarn/axios-1.6.7
Bump axios from 1.6.5 to 1.6.7
2024-02-02 19:17:24 +06:00
dependabot[bot]
947abd1b8e Bump axios from 1.6.5 to 1.6.7
Bumps [axios](https://github.com/axios/axios) from 1.6.5 to 1.6.7.
- [Release notes](https://github.com/axios/axios/releases)
- [Changelog](https://github.com/axios/axios/blob/v1.x/CHANGELOG.md)
- [Commits](https://github.com/axios/axios/compare/v1.6.5...v1.6.7)

---
updated-dependencies:
- dependency-name: axios
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-01-29 08:34:21 +00:00
dependabot[bot]
1d2c043008 Bump date-fns from 3.3.0 to 3.3.1
Bumps [date-fns](https://github.com/date-fns/date-fns) from 3.3.0 to 3.3.1.
- [Release notes](https://github.com/date-fns/date-fns/releases)
- [Changelog](https://github.com/date-fns/date-fns/blob/main/CHANGELOG.md)
- [Commits](https://github.com/date-fns/date-fns/compare/v3.3.0...v3.3.1)

---
updated-dependencies:
- dependency-name: date-fns
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-01-29 08:33:59 +00:00
Ariful Alam
f6a3c8457d Merge pull request #483 from putuwahyu29/main
Remove semicolon (;) in website build
2024-01-29 07:39:10 +06:00
I Putu Agus Wahyu Dupayana
521f9deb55 Remove semicolon (;) in website build 2024-01-29 07:33:43 +08:00
Ariful Alam
d87be16c26 Merge pull request #479 from arifszn/dependabot/npm_and_yarn/vite-5.0.12
Bump vite from 5.0.11 to 5.0.12
2024-01-20 20:18:58 +06:00
Ariful Alam
eed84cf977 Merge pull request #480 from arifszn/dependabot/npm_and_yarn/date-fns-3.3.0
Bump date-fns from 3.2.0 to 3.3.0
2024-01-20 20:18:47 +06:00
dependabot[bot]
7904a16bd2 Bump date-fns from 3.2.0 to 3.3.0
Bumps [date-fns](https://github.com/date-fns/date-fns) from 3.2.0 to 3.3.0.
- [Release notes](https://github.com/date-fns/date-fns/releases)
- [Changelog](https://github.com/date-fns/date-fns/blob/main/CHANGELOG.md)
- [Commits](https://github.com/date-fns/date-fns/compare/v3.2.0...v3.3.0)

---
updated-dependencies:
- dependency-name: date-fns
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-01-20 14:17:31 +00:00
dependabot[bot]
1286062a2b Bump vite from 5.0.11 to 5.0.12
Bumps [vite](https://github.com/vitejs/vite/tree/HEAD/packages/vite) from 5.0.11 to 5.0.12.
- [Release notes](https://github.com/vitejs/vite/releases)
- [Changelog](https://github.com/vitejs/vite/blob/v5.0.12/packages/vite/CHANGELOG.md)
- [Commits](https://github.com/vitejs/vite/commits/v5.0.12/packages/vite)

---
updated-dependencies:
- dependency-name: vite
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-01-20 14:17:03 +00:00
Ariful Alam
f6750b939b Merge pull request #478 from arifszn/migrate-to-typescript
Migrate to TypeScript
2024-01-20 20:06:26 +06:00
Ariful Alam
cac7ee3a16 Update docs 2024-01-20 19:55:48 +06:00
Ariful Alam
c7db8ee0b8 Add 3 new themes and change default theme 2024-01-20 18:48:02 +06:00
Ariful Alam
034dbe0d2c Decide if the card should be displayed at higher level 2024-01-20 18:34:32 +06:00
Ariful Alam
c5318da457 Remove App component 2024-01-20 18:28:08 +06:00
Ariful Alam
aa4c6928d6 Migrate to TypeScript 2024-01-20 18:24:26 +06:00
Ariful Alam
fdf75dabeb Merge pull request #473 from arifszn/dependabot/npm_and_yarn/eslint-8.56.0
Bump eslint from 8.45.0 to 8.56.0
2024-01-08 13:59:37 +06:00
Ariful Alam
34130dd2a1 Merge pull request #465 from arifszn/dependabot/npm_and_yarn/react-icons-4.12.0
Bump react-icons from 4.10.1 to 4.12.0
2023-12-19 20:31:35 +06:00
dependabot[bot]
d2489d1c6f Bump eslint from 8.45.0 to 8.56.0
Bumps [eslint](https://github.com/eslint/eslint) from 8.45.0 to 8.56.0.
- [Release notes](https://github.com/eslint/eslint/releases)
- [Changelog](https://github.com/eslint/eslint/blob/main/CHANGELOG.md)
- [Commits](https://github.com/eslint/eslint/compare/v8.45.0...v8.56.0)

---
updated-dependencies:
- dependency-name: eslint
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-12-18 08:53:40 +00:00
dependabot[bot]
93a55a4c32 Bump react-icons from 4.10.1 to 4.12.0
Bumps [react-icons](https://github.com/react-icons/react-icons) from 4.10.1 to 4.12.0.
- [Release notes](https://github.com/react-icons/react-icons/releases)
- [Commits](https://github.com/react-icons/react-icons/compare/v4.10.1...v4.12.0)

---
updated-dependencies:
- dependency-name: react-icons
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-11-20 08:23:17 +00:00
Ariful Alam
ac61808161 Merge pull request #451 from arifszn/dependabot/npm_and_yarn/daisyui-3.6.4
Bump daisyui from 3.2.1 to 3.6.4
2023-09-11 08:56:29 +06:00
dependabot[bot]
f707b702c2 Bump daisyui from 3.2.1 to 3.6.4
Bumps [daisyui](https://github.com/saadeghi/daisyui) from 3.2.1 to 3.6.4.
- [Changelog](https://github.com/saadeghi/daisyui/blob/master/CHANGELOG.md)
- [Commits](https://github.com/saadeghi/daisyui/compare/v3.2.1...v3.6.4)

---
updated-dependencies:
- dependency-name: daisyui
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-09-04 08:20:28 +00:00
59 changed files with 4878 additions and 3155 deletions

View File

@@ -1,4 +1,3 @@
# Logs
logs logs
*.log *.log
npm-debug.log* npm-debug.log*
@@ -12,7 +11,6 @@ dist
dist-ssr dist-ssr
*.local *.local
# Editor directories and files
.vscode/* .vscode/*
!.vscode/extensions.json !.vscode/extensions.json
.idea .idea

19
.eslintrc.cjs Normal file
View File

@@ -0,0 +1,19 @@
module.exports = {
root: true,
env: { browser: true, es2020: true, node: true },
extends: [
'eslint:recommended',
'plugin:@typescript-eslint/recommended',
'plugin:react-hooks/recommended',
'plugin:prettier/recommended',
],
ignorePatterns: ['dist', '.eslintrc.cjs'],
parser: '@typescript-eslint/parser',
plugins: ['react-refresh'],
rules: {
'react-refresh/only-export-components': [
'warn',
{ allowConstantExport: true },
],
},
};

View File

@@ -1,29 +0,0 @@
// .eslintrc.js
module.exports = {
env: {
browser: true,
es2021: true,
node: true,
},
extends: [
'eslint:recommended',
'plugin:react/recommended',
'plugin:prettier/recommended',
],
settings: {
react: {
version: 'detect',
},
},
parserOptions: {
ecmaFeatures: {
jsx: true,
},
ecmaVersion: 'latest',
sourceType: 'module',
},
plugins: ['react'],
rules: {
'react/react-in-jsx-scope': 'off',
},
};

View File

@@ -20,19 +20,19 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout repository - name: Checkout repository
uses: actions/checkout@v3 uses: actions/checkout@v4
- name: Set up Node - name: Set up Node
uses: actions/setup-node@v3 uses: actions/setup-node@v4
with: with:
node-version: 16.x node-version: 20.x
cache: 'npm' cache: 'npm'
- name: Setup Pages - name: Setup Pages
uses: actions/configure-pages@v3 uses: actions/configure-pages@v4
- name: Restore cache - name: Restore cache
uses: actions/cache@v3 uses: actions/cache@v4
with: with:
path: | path: |
**/node_modules **/node_modules
@@ -45,7 +45,7 @@ jobs:
run: npm run build run: npm run build
- name: Upload artifact - name: Upload artifact
uses: actions/upload-pages-artifact@v1 uses: actions/upload-pages-artifact@v3
with: with:
path: ./dist path: ./dist
@@ -58,4 +58,4 @@ jobs:
steps: steps:
- name: Deploy to GitHub Pages - name: Deploy to GitHub Pages
id: deployment id: deployment
uses: actions/deploy-pages@v1.2.4 uses: actions/deploy-pages@v4

View File

@@ -9,16 +9,16 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout repository - name: Checkout repository
uses: actions/checkout@v3 uses: actions/checkout@v4
- name: Set up Node - name: Set up Node
uses: actions/setup-node@v3 uses: actions/setup-node@v4
with: with:
node-version: 16.x node-version: 20.x
cache: 'npm' cache: 'npm'
- name: Restore cache - name: Restore cache
uses: actions/cache@v3 uses: actions/cache@v4
with: with:
path: | path: |
**/node_modules **/node_modules

View File

@@ -1,14 +0,0 @@
public
src
.eslintignore
.eslintrc.js
.prettierignore
.prettierrc
CODE_OF_CONDUCT.md
CONTRIBUTING.md
gitprofile.config.js
index.html
library.config.js
tailwind.config.mjs
vite.config.js
stats.html

View File

@@ -1,4 +1,3 @@
# Logs
logs logs
*.log *.log
npm-debug.log* npm-debug.log*
@@ -12,7 +11,6 @@ dist
dist-ssr dist-ssr
*.local *.local
# Editor directories and files
.vscode/* .vscode/*
!.vscode/extensions.json !.vscode/extensions.json
.idea .idea

391
README.md
View File

@@ -30,9 +30,6 @@
<a href="https://github.com/arifszn/gitprofile/blob/main/LICENSE"> <a href="https://github.com/arifszn/gitprofile/blob/main/LICENSE">
<img src="https://img.shields.io/github/license/arifszn/gitprofile"/> <img src="https://img.shields.io/github/license/arifszn/gitprofile"/>
</a> </a>
<a href="https://www.buymeacoffee.com/arifszn">
<img src="https://img.shields.io/badge/sponsor-buy%20me%20a%20coffee-yellow?logo=buymeacoffee"/>
</a>
<a href="https://twitter.com/intent/tweet?text=Check%20out%20the%20portfolio%20builder.%20Create%20an%20automatic%20portfolio%20based%20on%20GitHub%20profile.&url=https://github.com/arifszn/gitprofile&hashtags=javascript,opensource,js,webdev,developers"> <a href="https://twitter.com/intent/tweet?text=Check%20out%20the%20portfolio%20builder.%20Create%20an%20automatic%20portfolio%20based%20on%20GitHub%20profile.&url=https://github.com/arifszn/gitprofile&hashtags=javascript,opensource,js,webdev,developers">
<img src="https://img.shields.io/twitter/url?style=social&url=https%3A%2F%2Fgithub.com%2Farifszn%2Fgitprofile"/> <img src="https://img.shields.io/twitter/url?style=social&url=https%3A%2F%2Fgithub.com%2Farifszn%2Fgitprofile"/>
</a> </a>
@@ -49,18 +46,18 @@
<p align="center"> <p align="center">
<a href="https://arifszn.github.io/gitprofile"> <a href="https://arifszn.github.io/gitprofile">
<img src="https://arifszn.netlify.app/assets/img/hosted/gitprofile/preview.gif" alt="Preview" width="60%"/> <img src="https://github.com/arifszn/gitprofile/assets/45073703/eb6c38a4-ac92-4006-869b-e4e24f6f5cf6" alt="Preview" width="60%"/>
</a> </a>
<br/> <br/>
<a href="#arifszn"><img src="https://arifszn.netlify.app/assets/img/drop-shadow.png" width="50%" alt="Shadow"/></a> <a href="#arifszn"><img src="https://github.com/arifszn/gitprofile/assets/45073703/4d2ccd45-e566-4743-bf61-cadc03ece54c" width="50%" alt="Shadow"/></a>
</p> </p>
**GitProfile** is an easy to use portfolio builder where you can create a portfolio page automatically by just providing your GitHub username. It is built using React.js on top of Vite. But it's not necessary to have knowledge on these to get you started. You can make your own copy with zero coding experience. **GitProfile** is a powerful portfolio builder that allows you to create a stunning and personalized portfolio site in minutes, even if you have no coding experience. Simply provide your GitHub username, and GitProfile will automatically generate a portfolio. Best of all, you can easily deploy your portfolio to GitHub Pages with just a few clicks, making it accessible to the world in no time.
**Features:** **Features:**
✓ [Easy to Setup](#-installation--setup) ✓ [Easy to Setup](#-installation--setup)
✓ [30 Themes](#themes) ✓ [33 Themes](#themes)
✓ [Google Analytics](#google-analytics) ✓ [Google Analytics](#google-analytics)
✓ [Hotjar](#hotjar) ✓ [Hotjar](#hotjar)
✓ [SEO](#seo) ✓ [SEO](#seo)
@@ -76,18 +73,8 @@
To view a live example, **[click here](https://arifszn.github.io/gitprofile)**. To view a live example, **[click here](https://arifszn.github.io/gitprofile)**.
Or try it **[online](https://stackblitz.com/edit/gitprofile)**.
<p align="center"> <p align="center">
<img src="https://arifszn.netlify.app/assets/img/hosted/gitprofile/themes.png" alt="Themes"> <img src="https://github.com/arifszn/gitprofile/assets/45073703/406e8368-415a-42ef-89c5-d43cc8bbeb19" alt="Themes">
</p>
## 🎉 Thank you for the 800 ★
<p>
<a href="https://arifszn.github.io/gitprofile">
<img src="https://github.com/arifszn/gitprofile/assets/45073703/b3d34d17-cf55-4c74-bf44-3196b1f0ac66" alt="Thank You" width="50%"/>
</a>
</p> </p>
## 🛠 Installation & Setup ## 🛠 Installation & Setup
@@ -96,7 +83,6 @@ There are three ways to use **GitProfile**. Use any.
- [Forking this repo _(recommended)_](#forking-this-repo) - [Forking this repo _(recommended)_](#forking-this-repo)
- [Setting up locally](#setting-up-locally) - [Setting up locally](#setting-up-locally)
- [Installing as package](#installing-as-package)
### Forking this repo ### Forking this repo
@@ -108,16 +94,16 @@ These instructions will get you a copy of the project and deploy your portfolio
- If you want to host your portfolio at `https://<USERNAME>.github.io/<REPO_NAME>` (e.g. `https://<USERNAME>.github.io/portfolio`), rename your forked repository to `<REPO_NAME>` (e.g. `portfolio`) in GitHub. - If you want to host your portfolio at `https://<USERNAME>.github.io/<REPO_NAME>` (e.g. `https://<USERNAME>.github.io/portfolio`), rename your forked repository to `<REPO_NAME>` (e.g. `portfolio`) in GitHub.
- **Enable workflows:** Go to your repo's **Actions** tab and enable workflows. - **Enable workflows:** Go to your repo's **Actions** tab and enable workflows.
![Workflows](https://arifszn.netlify.app/assets/img/hosted/gitprofile/workflows.png) ![Workflows](https://github.com/arifszn/gitprofile/assets/45073703/7e82f7d4-900c-4cb9-83f9-bcaa1ca2b910)
- **Base Value:** Open `vite.config.js`, and change `base`'s value. - **Base Value:** Open `gitprofile.config.ts`, and change `base`'s value.
- If you are deploying to `https://<USERNAME>.github.io`, set `base` to `'/'`. - If you are deploying to `https://<USERNAME>.github.io`, set `base` to `'/'`.
- If you are deploying to `https://<USERNAME>.github.io/<REPO_NAME>` (e.g. `https://<USERNAME>.github.io/portfolio`), then set `base` to `'/<REPO_NAME>/'` (e.g. `'/portfolio/'`). - If you are deploying to `https://<USERNAME>.github.io/<REPO_NAME>` (e.g. `https://<USERNAME>.github.io/portfolio`), then set `base` to `'/<REPO_NAME>/'` (e.g. `'/portfolio/'`).
```js ```ts
// vite.config.js // gitprofile.config.ts
{ {
base: '/', base: '/',
// ... // ...
@@ -126,11 +112,11 @@ These instructions will get you a copy of the project and deploy your portfolio
- **Commit the changes:** Now commit to your **main** branch with your changes. Wait a few minutes so that the CI/CD pipeline can publish your website to GitHub Pages. You can check the progress in the [Actions](https://github.com/arifszn/gitprofile/actions) tab. - **Commit the changes:** Now commit to your **main** branch with your changes. Wait a few minutes so that the CI/CD pipeline can publish your website to GitHub Pages. You can check the progress in the [Actions](https://github.com/arifszn/gitprofile/actions) tab.
Your portfolio website will be live shortly. Any time you commit a change to the **main** branch, the website will be automatically updated. If you face any issue viewing the website, double-check the `base` value in the `vite.config.js` file. Also, check if **Source** is set to **GitHub Actions** in **Settings** ➜ **Pages** ➜ **Build and deployment**. Your portfolio website will be live shortly. Any time you commit a change to the **main** branch, the website will be automatically updated. If you face any issue viewing the website, double-check the `base` value in the `gitprofile.config.ts` file. Also, check if **Source** is set to **GitHub Actions** in **Settings** ➜ **Pages** ➜ **Build and deployment**.
If you wish to add a custom domain, no CNAME file is required. Just add it to your repo's **Settings** ➜ **Pages** ➜ **Custom domain**. If you wish to add a custom domain, no CNAME file is required. Just add it to your repo's **Settings** ➜ **Pages** ➜ **Custom domain**.
As this is a Vite project, you can also host your website to Netlify, Vercel, Heroku, or other popular services. Please refer to this [doc](https://vitejs.dev/guide/static-deploy.html) for a detailed deployment guide to other services. Also, if you are not deploying to GitHub pages, `base:` might need to be deleted from `vite.config.js`. As this is a Vite project, you can also host your website to Netlify, Vercel, Heroku, or other popular services. Please refer to this [doc](https://vitejs.dev/guide/static-deploy.html) for a detailed deployment guide to other services.
### Setting up locally ### Setting up locally
@@ -155,86 +141,107 @@ As this is a Vite project, you can also host your website to Netlify, Vercel, He
- Finally, visit [`http://localhost:5173/gitprofile/`](http://localhost:5173/gitprofile/) from your browser. - Finally, visit [`http://localhost:5173/gitprofile/`](http://localhost:5173/gitprofile/) from your browser.
### Installing as package
You can also use **GitProfile** by installing as an NPM package.
First Install **GitProfile** via <a href="https://www.npmjs.com/package/@arifszn/gitprofile">NPM</a>.
```sh
npm install @arifszn/gitprofile
```
Or via <a href="https://yarnpkg.com/package/@arifszn/gitprofile">Yarn</a>.
```sh
yarn add @arifszn/gitprofile
```
Then, import the package, import and style and provide the config.
```js
import GitProfile from '@arifszn/gitprofile';
import '@arifszn/gitprofile/dist/style.css';
function App() {
return (
<GitProfile
config={{
github: {
username: 'arifszn',
},
}}
/>
);
}
export default App;
```
List of all config [here](#-customization).
**If you face any problems or have any questions, open an issue [here](https://github.com/arifszn/gitprofile/issues).**
## 🎨 Customization ## 🎨 Customization
All the magic happens in the file `gitprofile.config.js`. Open it and modify it according to your preference. All the magic happens in the file `gitprofile.config.ts`. Open it and modify it according to your preference.
```js ```ts
// gitprofile.config.js // gitprofile.config.ts
const config = { const CONFIG = {
github: { github: {
username: 'arifszn', // Your GitHub org/user name. (Required) username: 'arifszn', // Your GitHub org/user name. (This is the only required config)
sortBy: 'stars', // stars | updated },
limit: 8, // How many projects to display. /**
exclude: { * If you are deploying to https://<USERNAME>.github.io/, for example your repository is at https://github.com/arifszn/arifszn.github.io, set base to '/'.
forks: false, // Forked projects will not be displayed if set to true. * If you are deploying to https://<USERNAME>.github.io/<REPO_NAME>/,
projects: [], // These projects will not be displayed. example: ['my-project1', 'my-project2'] * for example your repository is at https://github.com/arifszn/portfolio, then set base to '/portfolio/'.
*/
base: '/gitprofile/',
projects: {
github: {
display: true, // Display GitHub projects?
header: 'Github Projects',
mode: 'automatic', // Mode can be: 'automatic' or 'manual'
automatic: {
sortBy: 'stars', // Sort projects by 'stars' or 'updated'
limit: 8, // How many projects to display.
exclude: {
forks: false, // Forked projects will not be displayed if set to true.
projects: [], // These projects will not be displayed. example: ['arifszn/my-project1', 'arifszn/my-project2']
},
},
manual: {
// Properties for manually specifying projects
projects: ['arifszn/gitprofile', 'arifszn/pandora'], // List of repository names to display. example: ['arifszn/my-project1', 'arifszn/my-project2']
},
},
external: {
header: 'My Projects',
// To hide the `External Projects` section, keep it empty.
projects: [
{
title: 'Project Name',
description:
'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed euismod, nunc ut.',
imageUrl:
'https://img.freepik.com/free-vector/illustration-gallery-icon_53876-27002.jpg',
link: 'https://example.com',
},
{
title: 'Project Name',
description:
'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed euismod, nunc ut.',
imageUrl:
'https://img.freepik.com/free-vector/illustration-gallery-icon_53876-27002.jpg',
link: 'https://example.com',
},
],
}, },
}, },
seo: {
title: 'Portfolio of Ariful Alam',
description: '',
imageURL: '',
},
social: { social: {
linkedin: '', linkedin: 'ariful-alam',
twitter: '', twitter: 'arif_szn',
mastodon: '', mastodon: 'arifszn@mastodon.social',
facebook: '', facebook: '',
instagram: '', instagram: '',
youtube: '', youtube: '', // example: 'pewdiepie'
dribbble: '', dribbble: '',
behance: '', behance: '',
medium: '', medium: 'arifszn',
dev: '', dev: 'arifszn',
stackoverflow: '', // format: userid/username stackoverflow: '', // example: '1/jeff-atwood'
website: '',
skype: '', skype: '',
telegram: '', telegram: '',
website: 'https://www.arifszn.com',
phone: '', phone: '',
email: '', email: 'arifulalamszn@gmail.com',
}, },
resume: { resume: {
fileUrl: '', // Empty fileUrl will hide the `Download Resume` button. fileUrl:
'https://www.w3.org/WAI/ER/tests/xhtml/testfiles/resources/pdf/dummy.pdf', // Empty fileUrl will hide the `Download Resume` button.
}, },
skills: ['JavaScript', 'React.js'], skills: [
'PHP',
'Laravel',
'JavaScript',
'React.js',
'Node.js',
'Nest.js',
'MySQL',
'PostgreSQL',
'Git',
'Docker',
'PHPUnit',
'CSS',
'Antd',
'Tailwind',
],
experiences: [ experiences: [
{ {
company: 'Company Name', company: 'Company Name',
@@ -253,13 +260,13 @@ const config = {
], ],
certifications: [ certifications: [
{ {
body: 'Certification Body Name', name: 'Lorem ipsum',
name: 'Sample Certification', body: 'Lorem ipsum dolor sit amet',
year: 'March 2022', year: 'March 2022',
link: 'https://example.com', link: 'https://example.com',
}, },
], ],
education: [ educations: [
{ {
institution: 'Institution Name', institution: 'Institution Name',
degree: 'Degree', degree: 'Degree',
@@ -273,28 +280,11 @@ const config = {
to: '2014', to: '2014',
}, },
], ],
// To hide the `My Projects` section, keep it empty. // Display articles from your medium or dev account. (Optional)
externalProjects: [
{
title: 'Project Name',
description:
'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed euismod, nunc ut.',
imageUrl: 'https://via.placeholder.com/250x250',
link: 'https://example.com',
},
{
title: 'Project Name',
description:
'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed euismod, nunc ut.',
imageUrl: 'https://via.placeholder.com/250x250',
link: 'https://example.com',
},
],
// Display blog posts from your medium or dev account. (Optional)
blog: { blog: {
source: 'dev', // medium | dev source: 'dev', // medium | dev
username: 'arifszn', // to hide blog section, keep it empty username: 'arifszn', // to hide blog section, keep it empty
limit: 5, // How many posts to display. Max is 10. limit: 3, // How many articles to display. Max is 10.
}, },
googleAnalytics: { googleAnalytics: {
id: '', // GA3 tracking id/GA4 tag id UA-XXXXXXXXX-X | G-XXXXXXXXXX id: '', // GA3 tracking id/GA4 tag id UA-XXXXXXXXX-X | G-XXXXXXXXXX
@@ -305,18 +295,18 @@ const config = {
snippetVersion: 6, snippetVersion: 6,
}, },
themeConfig: { themeConfig: {
defaultTheme: 'light', defaultTheme: 'nord',
// Hides the theme change switch // Hides the switch in the navbar
// Useful if you want to support a single color mode // Useful if you want to support a single color mode
disableSwitch: false, disableSwitch: false,
// Should use the prefers-color-scheme media-query, // Should use the prefers-color-scheme media-query,
// using user system preferences, instead of the hardcoded defaultTheme // using user system preferences, instead of the hardcoded defaultTheme
respectPrefersColorScheme: true, respectPrefersColorScheme: false,
// Hide the ring in Profile picture // Display the ring in Profile picture
hideAvatarRing: false, displayAvatarRing: true,
// Available themes. To remove any theme, exclude from here. // Available themes. To remove any theme, exclude from here.
themes: [ themes: [
@@ -349,10 +339,13 @@ const config = {
'night', 'night',
'coffee', 'coffee',
'winter', 'winter',
'dim',
'nord',
'sunset',
'procyon', 'procyon',
], ],
// Custom theme // Custom theme, applied to `procyon` theme
customTheme: { customTheme: {
primary: '#fc055b', primary: '#fc055b',
secondary: '#219aaf', secondary: '#219aaf',
@@ -365,19 +358,27 @@ const config = {
}, },
// Optional Footer. Supports plain text or HTML. // Optional Footer. Supports plain text or HTML.
footer: `Copyright © 2023 John Doe`, footer: `Made with <a
class="text-primary" href="https://github.com/arifszn/gitprofile"
target="_blank"
rel="noreferrer"
>GitProfile</a> and ❤️`,
enablePWA: true,
}; };
export default CONFIG;
``` ```
### Themes ### Themes
There are 30 themes available that can be selected from the dropdown. There are 33 themes available that can be selected from the dropdown.
The default theme can be specified. The default theme can be specified.
```js ```ts
// gitprofile.config.js // gitprofile.config.ts
const config = { const CONFIG = {
// ... // ...
themeConfig: { themeConfig: {
defaultTheme: 'light', defaultTheme: 'light',
@@ -387,14 +388,14 @@ const config = {
``` ```
<p align="center"> <p align="center">
<img src="https://arifszn.netlify.app/assets/img/hosted/gitprofile/theme-dropdown.png" alt="Theme Dropdown" width="50%"> <img src="https://github.com/arifszn/gitprofile/assets/45073703/91a2d9e6-67e5-47b4-9752-1881ac0f907f" alt="Theme Dropdown" width="50%">
</p> </p>
You can create your own custom theme by modifying these values. Theme `procyon` will have the custom styles. You can create your own custom theme by modifying these values. Theme `procyon` will have the custom styles.
```js ```ts
// gitprofile.config.js // gitprofile.config.ts
const config = { const CONFIG = {
// ... // ...
themeConfig: { themeConfig: {
customTheme: { customTheme: {
@@ -415,9 +416,9 @@ const config = {
**GitProfile** supports both GA3 and GA4. If you do not want to use Google Analytics, keep the `id` empty. **GitProfile** supports both GA3 and GA4. If you do not want to use Google Analytics, keep the `id` empty.
```js ```ts
// gitprofile.config.js // gitprofile.config.ts
const config = { const CONFIG = {
// ... // ...
googleAnalytics: { googleAnalytics: {
id: '', id: '',
@@ -431,9 +432,9 @@ Besides tracking visitors, it will track `click events` on projects and blog pos
**GitProfile** supports [hotjar](https://www.hotjar.com) to track visitor interaction and behavior. If you do not want to use Hotjar, keep the `id` empty. **GitProfile** supports [hotjar](https://www.hotjar.com) to track visitor interaction and behavior. If you do not want to use Hotjar, keep the `id` empty.
```js ```ts
// gitprofile.config.js // gitprofile.config.ts
const config = { const CONFIG = {
// ... // ...
hotjar: { hotjar: {
id: '', id: '',
@@ -444,11 +445,23 @@ const config = {
### SEO ### SEO
Meta tags will be auto-generated from configs dynamically. However, you can also manually add meta tags in `public/index.html`. You can customize the meta tags for SEO in `seo`.
```ts
// gitprofile.config.ts
const CONFIG = {
// ...
seo: {
title: 'Portfolio of Ariful Alam',
description: '',
imageURL: '',
},
};
```
### PWA ### PWA
GitProfile is PWA enabled. The site can be installed as a Progressive Web App. GitProfile is PWA enabled. The site can be installed as a Progressive Web App. To turn it off, set `enablePWA` to `false`.
![PWA](https://github.com/arifszn/gitprofile/assets/45073703/9dc7cc5c-4262-4445-a7a5-1e3566ef43fa) ![PWA](https://github.com/arifszn/gitprofile/assets/45073703/9dc7cc5c-4262-4445-a7a5-1e3566ef43fa)
@@ -460,9 +473,9 @@ Your avatar and bio will be fetched from GitHub automatically.
You can link your social media services you're using, including LinkedIn, Twitter, Mastodon, Facebook, Instagram, YouTube, Dribbble, Behance, Medium, dev, Stack Overflow, Skype, Telegram, personal website, phone and email. You can link your social media services you're using, including LinkedIn, Twitter, Mastodon, Facebook, Instagram, YouTube, Dribbble, Behance, Medium, dev, Stack Overflow, Skype, Telegram, personal website, phone and email.
```js ```ts
// gitprofile.config.js // gitprofile.config.ts
const config = { const CONFIG = {
// ... // ...
social: { social: {
linkedin: 'ariful-alam', linkedin: 'ariful-alam',
@@ -489,9 +502,9 @@ const config = {
To showcase your skills provide them here. To showcase your skills provide them here.
```js ```ts
// gitprofile.config.js // gitprofile.config.ts
const config = { const CONFIG = {
// ... // ...
skills: ['JavaScript', 'React.js'], skills: ['JavaScript', 'React.js'],
}; };
@@ -503,9 +516,9 @@ Empty array will hide the skills section.
Provide your job history in `experiences`. Provide your job history in `experiences`.
```js ```ts
// gitprofile.config.js // gitprofile.config.ts
const config = { const CONFIG = {
// ... // ...
experiences: [ experiences: [
{ {
@@ -530,13 +543,13 @@ Empty array will hide the experience section.
### Education ### Education
Provide your education history in `education`. Provide your education history in `educations`.
```js ```ts
// gitprofile.config.js // gitprofile.config.ts
const config = { const CONFIG = {
// ... // ...
education: [ educations: [
{ {
institution: 'Institution name 1', institution: 'Institution name 1',
degree: 'Bachelor of Science', degree: 'Bachelor of Science',
@@ -559,9 +572,9 @@ Empty array will hide the education section.
Provide your industry certifications in `certifications`. Provide your industry certifications in `certifications`.
```js ```ts
// gitprofile.config.js // gitprofile.config.ts
const config = { const CONFIG = {
// ... // ...
certifications: [ certifications: [
{ {
@@ -580,19 +593,30 @@ Empty array will hide the certifications section.
#### Github Projects #### Github Projects
Your public repo from GitHub will be displayed in the `Github Projects` section automatically. You can limit how many projects do you want to be displayed. Also, you can hide forked or specific repo. - **Automatic Mode:** Seamlessly showcase your top GitHub projects based on stars or last updated date.
- **Manual Mode:** Choose specific repositories to highlight.
```js ```ts
// gitprofile.config.js // gitprofile.config.ts
const config = { const CONFIG = {
// ... // ...
github: { projects: {
username: 'arifszn', github: {
sortBy: 'stars', display: true, // Display GitHub projects?
limit: 8, header: 'Github Projects',
exclude: { mode: 'automatic', // Mode can be: 'automatic' or 'manual'
forks: false, automatic: {
projects: ['my-project1', 'my-project2'], sortBy: 'stars', // Sort projects by 'stars' or 'updated'
limit: 8, // How many projects to display.
exclude: {
forks: false, // Forked projects will not be displayed if set to true.
projects: [], // These projects will not be displayed. example: ['arifszn/my-project1', 'arifszn/my-project2']
},
},
manual: {
// Properties for manually specifying projects
projects: ['arifszn/gitprofile', 'arifszn/pandora'], // List of repository names to display. example: ['arifszn/my-project1', 'arifszn/my-project2']
},
}, },
}, },
}; };
@@ -600,20 +624,37 @@ const config = {
#### External Projects #### External Projects
In this section you can showcase your external/personal projects. - **Highlight Projects Beyond GitHub:** Feature projects hosted on other platforms or personal websites.
- **Control over Content:** Provide custom titles, descriptions, images, and links for each external project.
```js ```ts
// gitprofile.config.js // gitprofile.config.ts
const config = { const CONFIG = {
// ... // ...
externalProjects: [ projects: {
{ external: {
title: 'Project Name', header: 'My Projects',
description: 'Description', // To hide the `External Projects` section, keep it empty.
link: 'https://example.com', projects: [
imageUrl: 'https://via.placeholder.com/250x250', {
title: 'Project Name',
description:
'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed euismod, nunc ut.',
imageUrl:
'https://img.freepik.com/free-vector/illustration-gallery-icon_53876-27002.jpg',
link: 'https://example.com',
},
{
title: 'Project Name',
description:
'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed euismod, nunc ut.',
imageUrl:
'https://img.freepik.com/free-vector/illustration-gallery-icon_53876-27002.jpg',
link: 'https://example.com',
},
],
}, },
], },
}; };
``` ```
@@ -621,9 +662,9 @@ const config = {
If you have [medium](https://medium.com) or [dev](https://dev.to) account, you can show your recent blog posts in here just by providing your medium/dev username. You can limit how many posts to display (Max is `10`). If you have [medium](https://medium.com) or [dev](https://dev.to) account, you can show your recent blog posts in here just by providing your medium/dev username. You can limit how many posts to display (Max is `10`).
```js ```ts
// gitprofile.config.js // gitprofile.config.ts
const config = { const CONFIG = {
// ... // ...
blog: { blog: {
source: 'dev', source: 'dev',
@@ -633,7 +674,7 @@ const config = {
}; };
``` ```
![Blog](https://arifszn.netlify.app/assets/img/hosted/gitprofile/blog.png) ![Blog](https://github.com/arifszn/gitprofile/assets/45073703/410124f2-a3c2-48f1-8ec8-0c6fae74ae3d)
The posts are fetched by [blog.js](https://github.com/arifszn/blog.js). The posts are fetched by [blog.js](https://github.com/arifszn/blog.js).

View File

@@ -1,41 +1,61 @@
// gitprofile.config.js // gitprofile.config.ts
const config = { const CONFIG = {
github: { github: {
username: 'arifszn', // Your GitHub org/user name. (Required) username: 'arifszn', // Your GitHub org/user name. (This is the only required config)
}, },
/**
* If you are deploying to https://<USERNAME>.github.io/, for example your repository is at https://github.com/arifszn/arifszn.github.io, set base to '/'.
* If you are deploying to https://<USERNAME>.github.io/<REPO_NAME>/,
* for example your repository is at https://github.com/arifszn/portfolio, then set base to '/portfolio/'.
*/
base: '/gitprofile/',
projects: { projects: {
github: { github: {
header: 'Github Projects',
display: true, // Display GitHub projects? display: true, // Display GitHub projects?
sortBy: 'stars', // stars | updated header: 'Github Projects',
limit: 10, // How many projects to display. mode: 'automatic', // Mode can be: 'automatic' or 'manual'
exclude: { automatic: {
forks: false, // Forked projects will not be displayed if set to true. sortBy: 'stars', // Sort projects by 'stars' or 'updated'
projects: [], // These projects will not be displayed. example: ['my-project1', 'my-project2'] limit: 8, // How many projects to display.
exclude: {
forks: false, // Forked projects will not be displayed if set to true.
projects: [], // These projects will not be displayed. example: ['arifszn/my-project1', 'arifszn/my-project2']
},
},
manual: {
// Properties for manually specifying projects
projects: ['arifszn/gitprofile', 'arifszn/pandora'], // List of repository names to display. example: ['arifszn/my-project1', 'arifszn/my-project2']
}, },
}, },
external: { external: {
header: 'External Projects', header: 'My Projects',
display: true, // Display external projects? // To hide the `External Projects` section, keep it empty.
projects: [ projects: [
{ {
title: 'Project Name', title: 'Project Name',
description: description:
'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed euismod, nunc ut.', 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed euismod, nunc ut.',
imageUrl: 'https://via.placeholder.com/250x250', imageUrl:
'https://img.freepik.com/free-vector/illustration-gallery-icon_53876-27002.jpg',
link: 'https://example.com', link: 'https://example.com',
}, },
{ {
title: 'Project Name', title: 'Project Name',
description: description:
'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed euismod, nunc ut.', 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed euismod, nunc ut.',
imageUrl: 'https://via.placeholder.com/250x250', imageUrl:
'https://img.freepik.com/free-vector/illustration-gallery-icon_53876-27002.jpg',
link: 'https://example.com', link: 'https://example.com',
}, },
], ],
}, },
}, },
seo: {
title: 'Portfolio of Ariful Alam',
description: '',
imageURL: '',
},
social: { social: {
linkedin: 'ariful-alam', linkedin: 'ariful-alam',
twitter: 'arif_szn', twitter: 'arif_szn',
@@ -50,7 +70,7 @@ const config = {
stackoverflow: '', // example: '1/jeff-atwood' stackoverflow: '', // example: '1/jeff-atwood'
skype: '', skype: '',
telegram: '', telegram: '',
website: 'https://arifszn.com', website: 'https://www.arifszn.com',
phone: '', phone: '',
email: 'arifulalamszn@gmail.com', email: 'arifulalamszn@gmail.com',
}, },
@@ -90,15 +110,15 @@ const config = {
companyLink: 'https://example.com', companyLink: 'https://example.com',
}, },
], ],
/* certifications: [ certifications: [
{ {
name: 'Lorem ipsum', name: 'Lorem ipsum',
body: 'Lorem ipsum dolor sit amet', body: 'Lorem ipsum dolor sit amet',
year: 'March 2022', year: 'March 2022',
link: 'https://example.com' link: 'https://example.com',
}, },
], */ ],
education: [ educations: [
{ {
institution: 'Institution Name', institution: 'Institution Name',
degree: 'Degree', degree: 'Degree',
@@ -112,12 +132,11 @@ const config = {
to: '2014', to: '2014',
}, },
], ],
// Display articles from your medium or dev account. (Optional)
// Display blog posts from your medium or dev account. (Optional)
blog: { blog: {
source: 'dev', // medium | dev source: 'dev', // medium | dev
username: 'arifszn', // to hide blog section, keep it empty username: 'arifszn', // to hide blog section, keep it empty
limit: 2, // How many posts to display. Max is 10. limit: 3, // How many articles to display. Max is 10.
}, },
googleAnalytics: { googleAnalytics: {
id: '', // GA3 tracking id/GA4 tag id UA-XXXXXXXXX-X | G-XXXXXXXXXX id: '', // GA3 tracking id/GA4 tag id UA-XXXXXXXXX-X | G-XXXXXXXXXX
@@ -128,7 +147,7 @@ const config = {
snippetVersion: 6, snippetVersion: 6,
}, },
themeConfig: { themeConfig: {
defaultTheme: 'wireframe', defaultTheme: 'nord',
// Hides the switch in the navbar // Hides the switch in the navbar
// Useful if you want to support a single color mode // Useful if you want to support a single color mode
@@ -138,8 +157,8 @@ const config = {
// using user system preferences, instead of the hardcoded defaultTheme // using user system preferences, instead of the hardcoded defaultTheme
respectPrefersColorScheme: false, respectPrefersColorScheme: false,
// Hide the ring in Profile picture // Display the ring in Profile picture
hideAvatarRing: false, displayAvatarRing: true,
// Available themes. To remove any theme, exclude from here. // Available themes. To remove any theme, exclude from here.
themes: [ themes: [
@@ -172,10 +191,13 @@ const config = {
'night', 'night',
'coffee', 'coffee',
'winter', 'winter',
'dim',
'nord',
'sunset',
'procyon', 'procyon',
], ],
// Custom theme // Custom theme, applied to `procyon` theme
customTheme: { customTheme: {
primary: '#fc055b', primary: '#fc055b',
secondary: '#219aaf', secondary: '#219aaf',
@@ -193,6 +215,8 @@ const config = {
target="_blank" target="_blank"
rel="noreferrer" rel="noreferrer"
>GitProfile</a> and `, >GitProfile</a> and `,
enablePWA: true,
}; };
export default config; export default CONFIG;

View File

@@ -1,30 +1,32 @@
// Type definitions for GitProfile interface Github {
// Project https://github.com/arifszn/gitprofile
// Author: Ariful Alam <arifulalamszn@gmail.com>
import { Component } from 'react';
export interface Github {
/** /**
* GitHub org/user name * GitHub org/user name
*/ */
username: string; username: string;
} }
export interface Projects { interface GitHubProjects {
github: { /**
/** * Display GitHub projects?
* Header Title */
*/ display?: boolean;
header?: string;
/** /**
* Display GitHub projects? * Header
*/ */
display?: boolean; header?: string;
/**
* 'automatic' | 'manual'
*/
mode?: string;
/**
* Config of automatic mode
*/
automatic?: {
/** /**
* stars | updated * 'stars' | 'updated'
*/ */
sortBy?: string; sortBy?: string;
@@ -51,25 +53,60 @@ export interface Projects {
}; };
}; };
external: { /**
* Config of manual mode
*/
manual?: {
/** /**
* Header Title * These projects will be displayed
*
* example: ['my-project1', 'my-project2']
*/ */
header?: string; projects?: Array<string>;
/**
* Display external projects?
*/
display?: boolean;
/**
* External Projects
*/
projects?: Array<ExternalProjects>;
}; };
} }
export interface Social { interface ExternalProjects {
/**
* Header
*/
header?: string;
/**
* Project list
*/
projects?: {
title: string;
description?: string;
imageUrl?: string;
link: string;
}[];
}
interface Projects {
github?: GitHubProjects;
external?: ExternalProjects;
}
interface SEO {
/**
* Meta title
*/
title?: string;
/**
* Meta description
*/
description?: string;
/**
* Meta image
*/
imageURL?: string;
}
interface Social {
/** /**
* LinkedIn * LinkedIn
*/ */
@@ -151,33 +188,43 @@ export interface Social {
email?: string; email?: string;
} }
export interface Blog { interface Resume {
/** /**
* medium | dev * Resume file urlm
*/ */
source?: string; fileUrl?: string;
/**
* Username
*/
username?: string;
/**
* How many posts to display
*
* Max is 10
*/
limit?: number;
} }
export interface GoogleAnalytics { interface Experience {
company?: string;
position?: string;
from: string;
to: string;
companyLink?: string;
}
interface Certification {
body?: string;
name?: string;
year?: string;
link?: string;
}
interface Education {
institution?: string;
degree?: string;
from: string;
to: string;
}
interface GoogleAnalytics {
/** /**
* GA3 tracking id/GA4 tag id UA-XXXXXXXXX-X | G-XXXXXXXXXX * GA3 tracking id/GA4 tag id UA-XXXXXXXXX-X | G-XXXXXXXXXX
*/ */
id?: string; id?: string;
} }
export interface Hotjar { interface Hotjar {
/** /**
* Hotjar id * Hotjar id
*/ */
@@ -189,7 +236,26 @@ export interface Hotjar {
snippetVersion?: number; snippetVersion?: number;
} }
export interface CustomTheme { interface Blog {
/**
* medium | dev
*/
source?: string;
/**
* Username
*/
username?: string;
/**
* How many articles to display
*
* Max is 10
*/
limit?: number;
}
interface CustomTheme {
/** /**
* Primary color * Primary color
*/ */
@@ -226,7 +292,7 @@ export interface CustomTheme {
'--rounded-btn'?: string; '--rounded-btn'?: string;
} }
export interface ThemeConfig { interface ThemeConfig {
/** /**
* Default theme * Default theme
*/ */
@@ -245,7 +311,7 @@ export interface ThemeConfig {
/** /**
* Hide the ring in Profile picture * Hide the ring in Profile picture
*/ */
hideAvatarRing?: boolean; displayAvatarRing?: boolean;
/** /**
* Available themes * Available themes
@@ -258,59 +324,32 @@ export interface ThemeConfig {
customTheme?: CustomTheme; customTheme?: CustomTheme;
} }
export interface Experience { interface Config {
company?: string;
position?: string;
from?: string;
to?: string;
companyLink?: string;
}
export interface Certifications {
body?: string;
name?: string;
year?: string;
link?: string;
}
export interface ExternalProjects {
title: string;
description: string;
imageUrl?: string;
link: string;
}
export interface Education {
institution?: string;
degree?: string;
from?: string;
to?: string;
}
export interface Resume {
fileUrl?: string;
}
export interface Config {
/** /**
* GitHub Config * GitHub config
*/ */
github: Github; github: Github;
/** /**
* Projects * Vite's base url
*/
base?: string;
/**
* Projects config
*/ */
projects?: Projects; projects?: Projects;
/**
* SEO config
*/
seo?: SEO;
/** /**
* Social links * Social links
*/ */
social?: Social; social?: Social;
/**
* Resume
*/
resume?: Resume;
/** /**
* Skill list * Skill list
*/ */
@@ -324,17 +363,17 @@ export interface Config {
/** /**
* Certifications list * Certifications list
*/ */
certifications?: Array<Certifications>; certifications?: Array<Certification>;
/** /**
* Education list * Education list
*/ */
education?: Array<Education>; educations?: Array<Education>;
/** /**
* Blog config * Resume
*/ */
blog?: Blog; resume?: Resume;
/** /**
* Google Analytics config * Google Analytics config
@@ -346,6 +385,11 @@ export interface Config {
*/ */
hotjar?: Hotjar; hotjar?: Hotjar;
/**
* Blog config
*/
blog?: Blog;
/** /**
* Theme config * Theme config
*/ */
@@ -355,15 +399,11 @@ export interface Config {
* Custom footer * Custom footer
*/ */
footer?: string; footer?: string;
}
export interface GitProfileProps {
/** /**
* Config values * Enable PWA
*/ */
config: Config; enablePWA?: boolean;
} }
declare class GitProfile extends Component<GitProfileProps> {} declare const CONFIG: Config;
export default GitProfile;

View File

@@ -1,15 +1,31 @@
<!DOCTYPE html> <!doctype html>
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/favicon.ico" /> <link rel="icon" type="image/svg+xml" href="/favicon.ico" />
<link rel="apple-touch-icon" href="/apple-touch-icon.png" /> <link rel="apple-touch-icon" href="/apple-touch-icon.png" />
<link rel="manifest" href="/manifest.json" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Portfolio</title> <title><%- metaTitle %></title>
<meta property="title" content="<%- metaTitle %>" />
<meta name="description" content="<%- metaDescription %>" />
<meta itemprop="name" content="<%- metaTitle %>" />
<meta itemprop="description" content="<%- metaDescription %>" />
<meta itemprop="image" content="<%- metaImageURL %>" />
<meta property="og:title" content="<%- metaTitle %>" />
<meta property="og:description" content="<%- metaDescription %>" />
<meta property="og:image" content="<%- metaImageURL %>" />
<meta property="og:image:secure_url" content="<%- metaImageURL %>" />
<meta name="twitter:card" content="summary_large_image" />
<meta name="twitter:title" content="<%- metaTitle %>" />
<meta name="twitter:description" content="<%- metaDescription %>" />
<meta name="twitter:image" content="<%- metaImageURL %>" />
</head> </head>
<body> <body>
<div id="root"></div> <div id="root"></div>
<script type="module" src="/src/main.jsx"></script> <script type="module" src="/src/main.tsx"></script>
</body> </body>
</html> </html>

View File

@@ -1,31 +0,0 @@
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
import tailwind from 'tailwindcss';
import autoprefixer from 'autoprefixer';
import tailwindConfig from './tailwind.config.mjs';
import path from 'path';
// https://vitejs.dev/config/
export default defineConfig({
plugins: [react()],
css: {
postcss: {
plugins: [tailwind(tailwindConfig), autoprefixer],
},
},
build: {
lib: {
entry: path.resolve(__dirname, 'src/components/GitProfile.jsx'),
name: 'GitProfile',
fileName: (format) => `gitprofile.${format}.js`,
},
rollupOptions: {
external: ['react', 'react-dom'],
output: {
globals: {
react: 'React',
},
},
},
},
});

4018
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,7 +1,8 @@
{ {
"name": "@arifszn/gitprofile", "name": "@arifszn/gitprofile",
"description": "Create an automatic portfolio based on GitHub profile", "description": "Create an automatic portfolio based on GitHub profile",
"version": "2.6.0", "version": "3.0.0",
"type": "module",
"license": "MIT", "license": "MIT",
"author": "arifszn", "author": "arifszn",
"repository": { "repository": {
@@ -11,56 +12,45 @@
"bugs": { "bugs": {
"url": "https://github.com/arifszn/gitprofile/issues" "url": "https://github.com/arifszn/gitprofile/issues"
}, },
"files": [
"dist/gitprofile.es.js",
"dist/gitprofile.umd.js",
"dist/style.css",
"types"
],
"main": "./dist/gitprofile.umd.js",
"module": "./dist/gitprofile.es.js",
"exports": {
".": {
"import": "./dist/gitprofile.es.js",
"require": "./dist/gitprofile.umd.js"
},
"./dist/style.css": "./dist/style.css"
},
"typings": "./types/index.d.ts",
"scripts": { "scripts": {
"dev": "vite", "dev": "vite",
"build": "vite build", "build": "tsc && vite build",
"preview": "vite preview", "lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0",
"build:library": "vite build --config library.config.js", "lint:fix": "eslint . --fix --ext ts,tsx --report-unused-disable-directives --max-warnings 0",
"lint": "eslint --ext .js,.jsx .",
"lint:fix": "eslint --ext .js,.jsx --fix .",
"prettier": "prettier --check \"./**/*.{js,jsx,ts,tsx,css,md,json}\"", "prettier": "prettier --check \"./**/*.{js,jsx,ts,tsx,css,md,json}\"",
"prettier:fix": "prettier --write \"./**/*.{js,jsx,ts,tsx,css,md,json}\"" "prettier:fix": "prettier --write \"./**/*.{js,jsx,ts,tsx,css,md,json}\"",
"preview": "vite preview"
}, },
"dependencies": { "dependencies": {
"react": "^18.2.0", "react": "^18.2.0",
"react-dom": "^18.2.0" "react-dom": "^18.2.0"
}, },
"devDependencies": { "devDependencies": {
"@arifszn/blog-js": "^2.0.4", "@arifszn/blog-js": "^2.0.5",
"@vitejs/plugin-react": "^4.0.0", "@types/react": "^18.2.55",
"autoprefixer": "^10.4.4", "@types/react-dom": "^18.2.17",
"axios": "^1.1.3", "@typescript-eslint/eslint-plugin": "^6.14.0",
"daisyui": "^3.0.3", "@typescript-eslint/parser": "^6.14.0",
"date-fns": "^2.28.0", "@vitejs/plugin-react": "^4.2.1",
"eslint": "^8.11.0", "autoprefixer": "^10.4.17",
"eslint-config-prettier": "^8.5.0", "axios": "^1.6.7",
"eslint-plugin-prettier": "^4.0.0", "daisyui": "^4.6.0",
"eslint-plugin-react": "^7.29.4", "date-fns": "^3.3.1",
"postcss": "^8.4.12", "eslint": "^8.55.0",
"prettier": "^2.6.0", "eslint-config-prettier": "^9.1.0",
"prop-types": "^15.8.1", "eslint-plugin-prettier": "^5.1.3",
"react-helmet-async": "^1.2.3", "eslint-plugin-react-hooks": "^4.6.0",
"react-hotjar": "^6.1.0", "eslint-plugin-react-refresh": "^0.4.5",
"react-icons": "^4.3.1", "postcss": "^8.4.35",
"tailwindcss": "^3.0.23", "prettier": "^3.2.4",
"vite": "^4.0.3", "react-helmet-async": "^2.0.4",
"vite-plugin-pwa": "^0.16.3" "react-hotjar": "^6.2.0",
"react-icons": "^5.0.1",
"tailwindcss": "^3.4.1",
"typescript": "^5.2.2",
"vite": "^5.0.12",
"vite-plugin-html": "^3.2.2",
"vite-plugin-pwa": "^0.18.1"
}, },
"keywords": [ "keywords": [
"git-profile", "git-profile",

6
postcss.config.js Normal file
View File

@@ -0,0 +1,6 @@
export default {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
};

View File

@@ -1,25 +0,0 @@
{
"short_name": "GitProfile",
"name": "Personal Portfolio",
"icons": [
{
"src": "favicon.ico",
"sizes": "64x64 32x32 24x24 16x16",
"type": "image/x-icon"
},
{
"src": "logo192.png",
"type": "image/png",
"sizes": "192x192"
},
{
"src": "logo512.png",
"type": "image/png",
"sizes": "512x512"
}
],
"start_url": ".",
"display": "standalone",
"theme_color": "#7a44ee",
"background_color": "#ffffff"
}

View File

@@ -1,8 +0,0 @@
import config from '../gitprofile.config';
import GitProfile from './components/GitProfile';
function App() {
return <GitProfile config={config} />;
}
export default App;

View File

@@ -1,342 +0,0 @@
import axios from 'axios';
import { Fragment, useCallback, useEffect, useState } from 'react';
import HeadTagEditor from './head-tag-editor';
import ErrorPage from './error-page';
import ThemeChanger from './theme-changer';
import AvatarCard from './avatar-card';
import Details from './details';
import Skill from './skill';
import Experience from './experience';
import Certification from './certification';
import Education from './education';
import GitHubProject from './github-project';
import Blog from './blog';
import Footer from './footer';
import {
genericError,
getInitialTheme,
noConfigError,
notFoundError,
setupHotjar,
tooManyRequestError,
sanitizeConfig,
} from '../helpers/utils';
import { HelmetProvider } from 'react-helmet-async';
import PropTypes from 'prop-types';
import '../assets/index.css';
import { formatDistance } from 'date-fns';
import ExternalProject from './external-project';
const bgColor = 'bg-base-300';
const GitProfile = ({ config }) => {
const [error, setError] = useState(
typeof config === 'undefined' && !config ? noConfigError : null
);
const [sanitizedConfig] = useState(
typeof config === 'undefined' && !config ? null : sanitizeConfig(config)
);
const [theme, setTheme] = useState(null);
const [loading, setLoading] = useState(true);
const [profile, setProfile] = useState(null);
const [repo, setRepo] = useState(null);
useEffect(() => {
if (sanitizedConfig) {
setTheme(getInitialTheme(sanitizedConfig.themeConfig));
setupHotjar(sanitizedConfig.hotjar);
loadData();
}
}, [sanitizedConfig]);
useEffect(() => {
theme && document.documentElement.setAttribute('data-theme', theme);
}, [theme]);
const loadData = useCallback(() => {
axios
.get(`https://api.github.com/users/${sanitizedConfig.github.username}`)
.then((response) => {
let data = response.data;
let profileData = {
avatar: data.avatar_url,
name: data.name ? data.name : '',
bio: data.bio ? data.bio : '',
location: data.location ? data.location : '',
company: data.company ? data.company : '',
};
setProfile(profileData);
return data;
})
.then((userData) => {
let excludeRepo = ``;
if (userData.public_repos === 0) {
setRepo([]);
return;
}
sanitizedConfig.projects.github.exclude.projects.forEach((project) => {
excludeRepo += `+-repo:${sanitizedConfig.github.username}/${project}`;
});
let query = `user:${
sanitizedConfig.github.username
}+fork:${!sanitizedConfig.projects.github.exclude.forks}${excludeRepo}`;
let url = `https://api.github.com/search/repositories?q=${query}&sort=${sanitizedConfig.projects.github.sortBy}&per_page=${sanitizedConfig.projects.github.limit}&type=Repositories`;
axios
.get(url, {
headers: {
'Content-Type': 'application/vnd.github.v3+json',
},
})
.then((response) => {
let data = response.data;
setRepo(data.items);
})
.catch((error) => {
handleError(error);
});
})
.catch((error) => {
handleError(error);
})
.finally(() => {
setLoading(false);
});
}, [setLoading]);
const handleError = (error) => {
console.error('Error:', error);
try {
let reset = formatDistance(
new Date(error.response.headers['x-ratelimit-reset'] * 1000),
new Date(),
{
addSuffix: true,
}
);
if (error.response.status === 403) {
setError(tooManyRequestError(reset));
} else if (error.response.status === 404) {
setError(notFoundError);
} else {
setError(genericError);
}
} catch (error2) {
setError(genericError);
}
};
return (
<HelmetProvider>
{sanitizedConfig && (
<HeadTagEditor
profile={profile}
theme={theme}
googleAnalytics={sanitizedConfig.googleAnalytics}
social={sanitizedConfig.social}
/>
)}
<div className="fade-in h-screen">
{error ? (
<ErrorPage
status={`${error.status}`}
title={error.title}
subTitle={error.subTitle}
/>
) : (
sanitizedConfig && (
<Fragment>
<div className={`p-4 lg:p-10 min-h-full ${bgColor}`}>
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6 rounded-box">
<div className="col-span-1">
<div className="grid grid-cols-1 gap-6">
{!sanitizedConfig.themeConfig.disableSwitch && (
<ThemeChanger
theme={theme}
setTheme={setTheme}
loading={loading}
themeConfig={sanitizedConfig.themeConfig}
/>
)}
<AvatarCard
profile={profile}
loading={loading}
avatarRing={!sanitizedConfig.themeConfig.hideAvatarRing}
resume={sanitizedConfig.resume}
/>
<Details
profile={profile}
loading={loading}
github={sanitizedConfig.github}
social={sanitizedConfig.social}
/>
<Skill
loading={loading}
skills={sanitizedConfig.skills}
/>
<Experience
loading={loading}
experiences={sanitizedConfig.experiences}
/>
<Education
loading={loading}
education={sanitizedConfig.education}
/>
<Certification
loading={loading}
certifications={sanitizedConfig.certifications}
/>
</div>
</div>
<div className="lg:col-span-2 col-span-1">
<div className="grid grid-cols-1 gap-6">
<GitHubProject
repo={repo}
loading={loading}
github={sanitizedConfig.github}
googleAnalytics={sanitizedConfig.googleAnalytics}
/>
<ExternalProject
loading={loading}
externalProjects={sanitizedConfig.externalProjects}
googleAnalytics={sanitizedConfig.googleAnalytics}
/>
<Blog
loading={loading}
googleAnalytics={sanitizedConfig.googleAnalytics}
blog={sanitizedConfig.blog}
/>
</div>
</div>
</div>
</div>
<footer
className={`p-4 footer ${bgColor} text-base-content footer-center`}
>
<div className="card compact bg-base-100 shadow">
<Footer content={sanitizedConfig.footer} loading={loading} />
</div>
</footer>
</Fragment>
)
)}
</div>
</HelmetProvider>
);
};
GitProfile.propTypes = {
config: PropTypes.shape({
github: PropTypes.shape({
username: PropTypes.string.isRequired,
}).isRequired,
projects: {
github: PropTypes.shape({
header: PropTypes.string,
display: PropTypes.bool,
sortBy: PropTypes.oneOf(['stars', 'updated']),
limit: PropTypes.number,
exclude: PropTypes.shape({
forks: PropTypes.bool,
projects: PropTypes.array,
}),
}),
projects: PropTypes.shape({
header: PropTypes.string,
display: PropTypes.bool,
projects: PropTypes.arrayOf(
PropTypes.shape({
title: PropTypes.string.isRequired,
description: PropTypes.string.isRequired,
link: PropTypes.string.isRequired,
imageUrl: PropTypes.string,
})
),
}),
},
social: PropTypes.shape({
linkedin: PropTypes.string,
twitter: PropTypes.string,
mastodon: PropTypes.string,
facebook: PropTypes.string,
instagram: PropTypes.string,
youtube: PropTypes.string,
dribbble: PropTypes.string,
behance: PropTypes.string,
medium: PropTypes.string,
dev: PropTypes.string,
stackoverflow: PropTypes.string,
website: PropTypes.string,
skype: PropTypes.string,
telegram: PropTypes.string,
phone: PropTypes.string,
email: PropTypes.string,
}),
resume: PropTypes.shape({
fileUrl: PropTypes.string,
}),
skills: PropTypes.array,
experiences: PropTypes.arrayOf(
PropTypes.shape({
company: PropTypes.string,
position: PropTypes.string,
from: PropTypes.string,
to: PropTypes.string,
})
),
certifications: PropTypes.arrayOf(
PropTypes.shape({
body: PropTypes.string,
name: PropTypes.string,
year: PropTypes.string,
link: PropTypes.string,
})
),
education: PropTypes.arrayOf(
PropTypes.shape({
institution: PropTypes.string,
degree: PropTypes.string,
from: PropTypes.string,
to: PropTypes.string,
})
),
blog: PropTypes.shape({
source: PropTypes.string,
username: PropTypes.string,
limit: PropTypes.number,
}),
googleAnalytics: PropTypes.shape({
id: PropTypes.string,
}),
hotjar: PropTypes.shape({
id: PropTypes.string,
snippetVersion: PropTypes.number,
}),
themeConfig: PropTypes.shape({
defaultTheme: PropTypes.string,
disableSwitch: PropTypes.bool,
respectPrefersColorScheme: PropTypes.bool,
hideAvatarRing: PropTypes.bool,
themes: PropTypes.array,
customTheme: PropTypes.shape({
primary: PropTypes.string,
secondary: PropTypes.string,
accent: PropTypes.string,
neutral: PropTypes.string,
'base-100': PropTypes.string,
'--rounded-box': PropTypes.string,
'--rounded-btn': PropTypes.string,
}),
}),
footer: PropTypes.string,
}).isRequired,
};
export default GitProfile;

View File

@@ -1,8 +1,29 @@
import PropTypes from 'prop-types'; import { FALLBACK_IMAGE } from '../../constants';
import { fallbackImage, skeleton } from '../../helpers/utils'; import { Profile } from '../../interfaces/profile';
import { skeleton } from '../../utils';
import LazyImage from '../lazy-image'; import LazyImage from '../lazy-image';
const AvatarCard = ({ profile, loading, avatarRing, resume }) => { interface AvatarCardProps {
profile: Profile | null;
loading: boolean;
avatarRing: boolean;
resumeFileUrl?: string;
}
/**
* Renders an AvatarCard component.
* @param profile - The profile object.
* @param loading - A boolean indicating if the profile is loading.
* @param avatarRing - A boolean indicating if the avatar should have a ring.
* @param resumeFileUrl - The URL of the resume file.
* @returns JSX element representing the AvatarCard.
*/
const AvatarCard: React.FC<AvatarCardProps> = ({
profile,
loading,
avatarRing,
resumeFileUrl,
}): JSX.Element => {
return ( return (
<div className="card shadow-lg compact bg-base-100"> <div className="card shadow-lg compact bg-base-100">
<div className="grid place-items-center py-8"> <div className="grid place-items-center py-8">
@@ -10,8 +31,8 @@ const AvatarCard = ({ profile, loading, avatarRing, resume }) => {
<div className="avatar opacity-90"> <div className="avatar opacity-90">
<div className="mb-8 rounded-full w-32 h-32"> <div className="mb-8 rounded-full w-32 h-32">
{skeleton({ {skeleton({
width: 'w-full', widthCls: 'w-full',
height: 'h-full', heightCls: 'h-full',
shape: '', shape: '',
})} })}
</div> </div>
@@ -27,11 +48,11 @@ const AvatarCard = ({ profile, loading, avatarRing, resume }) => {
> >
{ {
<LazyImage <LazyImage
src={profile.avatar ? profile.avatar : fallbackImage} src={profile.avatar ? profile.avatar : FALLBACK_IMAGE}
alt={profile.name} alt={profile.name}
placeholder={skeleton({ placeholder={skeleton({
width: 'w-full', widthCls: 'w-full',
height: 'h-full', heightCls: 'h-full',
shape: '', shape: '',
})} })}
/> />
@@ -42,7 +63,7 @@ const AvatarCard = ({ profile, loading, avatarRing, resume }) => {
<div className="text-center mx-auto px-8"> <div className="text-center mx-auto px-8">
<h5 className="font-bold text-2xl"> <h5 className="font-bold text-2xl">
{loading || !profile ? ( {loading || !profile ? (
skeleton({ width: 'w-48', height: 'h-8' }) skeleton({ widthCls: 'w-48', heightCls: 'h-8' })
) : ( ) : (
<span className="text-base-content opacity-70"> <span className="text-base-content opacity-70">
{profile.name} {profile.name}
@@ -51,18 +72,18 @@ const AvatarCard = ({ profile, loading, avatarRing, resume }) => {
</h5> </h5>
<div className="mt-3 text-base-content text-opacity-60 font-mono"> <div className="mt-3 text-base-content text-opacity-60 font-mono">
{loading || !profile {loading || !profile
? skeleton({ width: 'w-48', height: 'h-5' }) ? skeleton({ widthCls: 'w-48', heightCls: 'h-5' })
: profile.bio} : profile.bio}
</div> </div>
</div> </div>
{resume?.fileUrl && {resumeFileUrl &&
(loading ? ( (loading ? (
<div className="mt-6"> <div className="mt-6">
{skeleton({ width: 'w-40', height: 'h-8' })} {skeleton({ widthCls: 'w-40', heightCls: 'h-8' })}
</div> </div>
) : ( ) : (
<a <a
href={resume.fileUrl} href={resumeFileUrl}
target="_blank" target="_blank"
className="btn btn-outline btn-sm text-xs mt-6 opacity-50" className="btn btn-outline btn-sm text-xs mt-6 opacity-50"
download download
@@ -76,13 +97,4 @@ const AvatarCard = ({ profile, loading, avatarRing, resume }) => {
); );
}; };
AvatarCard.propTypes = {
profile: PropTypes.object,
loading: PropTypes.bool.isRequired,
avatarRing: PropTypes.bool.isRequired,
resume: PropTypes.shape({
fileUrl: PropTypes.string,
}),
};
export default AvatarCard; export default AvatarCard;

View File

@@ -1,42 +1,41 @@
import { Fragment, useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import { ga, skeleton } from '../../helpers/utils';
import LazyImage from '../lazy-image'; import LazyImage from '../lazy-image';
import PropTypes from 'prop-types';
import { AiOutlineContainer } from 'react-icons/ai'; import { AiOutlineContainer } from 'react-icons/ai';
import { getDevPost, getMediumPost } from '@arifszn/blog-js'; import { getDevPost, getMediumPost } from '@arifszn/blog-js';
import { formatDistance } from 'date-fns'; import { formatDistance } from 'date-fns';
import { SanitizedBlog } from '../../interfaces/sanitized-config';
import { ga, skeleton } from '../../utils';
import { Article } from '../../interfaces/article';
const displaySection = (blog) => { const BlogCard = ({
if (blog?.source && blog?.username) { loading,
return true; blog,
} else { googleAnalyticsId,
return false; }: {
} loading: boolean;
}; blog: SanitizedBlog;
googleAnalyticsId?: string;
const Blog = ({ loading, blog, googleAnalytics }) => { }) => {
const [articles, setArticles] = useState(null); const [articles, setArticles] = useState<Article[]>([]);
useEffect(() => { useEffect(() => {
if (displaySection(blog)) { if (blog.source === 'medium') {
if (blog.source === 'medium') { getMediumPost({
getMediumPost({ user: blog.username,
user: blog.username, }).then((res) => {
}).then((res) => { setArticles(res);
setArticles(res); });
}); } else if (blog.source === 'dev') {
} else if (blog.source === 'dev') { getDevPost({
getDevPost({ user: blog.username,
user: blog.username, }).then((res) => {
}).then((res) => { setArticles(res);
setArticles(res); });
});
}
} }
}, []); }, [blog.source, blog.username]);
const renderSkeleton = () => { const renderSkeleton = () => {
let array = []; const array = [];
for (let index = 0; index < blog.limit; index++) { for (let index = 0; index < blog.limit; index++) {
array.push( array.push(
<div className="card shadow-lg compact bg-base-100" key={index}> <div className="card shadow-lg compact bg-base-100" key={index}>
@@ -45,8 +44,8 @@ const Blog = ({ loading, blog, googleAnalytics }) => {
<div className="avatar mb-5 md:mb-0"> <div className="avatar mb-5 md:mb-0">
<div className="w-24 h-24 mask mask-squircle"> <div className="w-24 h-24 mask mask-squircle">
{skeleton({ {skeleton({
width: 'w-full', widthCls: 'w-full',
height: 'h-full', heightCls: 'h-full',
shape: '', shape: '',
})} })}
</div> </div>
@@ -56,27 +55,27 @@ const Blog = ({ loading, blog, googleAnalytics }) => {
<div className="w-full"> <div className="w-full">
<h2> <h2>
{skeleton({ {skeleton({
width: 'w-full', widthCls: 'w-full',
height: 'h-8', heightCls: 'h-8',
className: 'mb-2 mx-auto md:mx-0', className: 'mb-2 mx-auto md:mx-0',
})} })}
</h2> </h2>
{skeleton({ {skeleton({
width: 'w-24', widthCls: 'w-24',
height: 'h-3', heightCls: 'h-3',
className: 'mx-auto md:mx-0', className: 'mx-auto md:mx-0',
})} })}
<div className="mt-3"> <div className="mt-3">
{skeleton({ {skeleton({
width: 'w-full', widthCls: 'w-full',
height: 'h-4', heightCls: 'h-4',
className: 'mx-auto md:mx-0', className: 'mx-auto md:mx-0',
})} })}
</div> </div>
<div className="mt-4 flex items-center flex-wrap justify-center md:justify-start"> <div className="mt-4 flex items-center flex-wrap justify-center md:justify-start">
{skeleton({ {skeleton({
width: 'w-32', widthCls: 'w-32',
height: 'h-4', heightCls: 'h-4',
className: 'md:mr-2 mx-auto md:mx-0', className: 'md:mr-2 mx-auto md:mx-0',
})} })}
</div> </div>
@@ -85,7 +84,7 @@ const Blog = ({ loading, blog, googleAnalytics }) => {
</div> </div>
</div> </div>
</div> </div>
</div> </div>,
); );
} }
@@ -103,12 +102,9 @@ const Blog = ({ loading, blog, googleAnalytics }) => {
e.preventDefault(); e.preventDefault();
try { try {
if (googleAnalytics?.id) { if (googleAnalyticsId) {
ga.event({ ga.event('Click Blog Post', {
action: 'Click Blog Post', post: article.title,
params: {
post: article.title,
},
}); });
} }
} catch (error) { } catch (error) {
@@ -126,8 +122,8 @@ const Blog = ({ loading, blog, googleAnalytics }) => {
src={article.thumbnail} src={article.thumbnail}
alt={'thumbnail'} alt={'thumbnail'}
placeholder={skeleton({ placeholder={skeleton({
width: 'w-full', widthCls: 'w-full',
height: 'h-full', heightCls: 'h-full',
shape: '', shape: '',
})} })}
/> />
@@ -175,51 +171,39 @@ const Blog = ({ loading, blog, googleAnalytics }) => {
}; };
return ( return (
<Fragment> <div className="col-span-1 lg:col-span-2">
{displaySection(blog) && ( <div className="grid grid-cols-2 gap-6">
<div className="col-span-1 lg:col-span-2"> <div className="col-span-2">
<div className="grid grid-cols-2 gap-6"> <div
<div className="col-span-2"> className={`card compact bg-base-100 ${
<div loading || (articles && articles.length)
className={`card compact bg-base-100 ${ ? 'shadow bg-opacity-40'
loading || (articles && articles.length) : 'shadow-lg'
? 'shadow bg-opacity-40' }`}
: 'shadow-lg' >
}`} <div className="card-body">
> <div className="mx-3 mb-2">
<div className="card-body"> <h5 className="card-title">
<div className="mx-3 mb-2"> {loading ? (
<h5 className="card-title"> skeleton({ widthCls: 'w-28', heightCls: 'h-8' })
{loading ? ( ) : (
skeleton({ width: 'w-28', height: 'h-8' }) <span className="text-base-content opacity-70">
) : ( My Articles
<span className="text-base-content opacity-70"> </span>
Recent Posts )}
</span> </h5>
)} </div>
</h5> <div className="col-span-2">
</div> <div className="grid grid-cols-1 gap-6">
<div className="col-span-2"> {loading || !articles ? renderSkeleton() : renderArticles()}
<div className="grid grid-cols-1 gap-6">
{loading || !articles
? renderSkeleton()
: renderArticles()}
</div>
</div>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
)} </div>
</Fragment> </div>
); );
}; };
Blog.propTypes = { export default BlogCard;
loading: PropTypes.bool.isRequired,
blog: PropTypes.object.isRequired,
googleAnalytics: PropTypes.object.isRequired,
};
export default Blog;

View File

@@ -0,0 +1,99 @@
import React from 'react';
import { SanitizedCertification } from '../../interfaces/sanitized-config';
import { skeleton } from '../../utils';
const ListItem = ({
year,
name,
body,
link,
}: {
year?: React.ReactNode;
name?: React.ReactNode;
body?: React.ReactNode;
link?: string;
}) => (
<li className="mb-5 ml-4">
<div
className="absolute w-2 h-2 bg-base-300 rounded-full border border-base-300 mt-1.5"
style={{ left: '-4.5px' }}
></div>
<div className="my-0.5 text-xs">{year}</div>
<div className="font-semibold">
<a href={link} target="_blank" rel="noreferrer">
{name}
</a>
</div>
<h3 className="mb-4 font-normal">{body}</h3>
</li>
);
const CertificationCard = ({
certifications,
loading,
}: {
certifications: SanitizedCertification[];
loading: boolean;
}) => {
const renderSkeleton = () => {
const array = [];
for (let index = 0; index < 2; index++) {
array.push(
<ListItem
key={index}
year={skeleton({
widthCls: 'w-5/12',
heightCls: 'h-4',
})}
name={skeleton({
widthCls: 'w-6/12',
heightCls: 'h-4',
className: 'my-1.5',
})}
body={skeleton({ widthCls: 'w-6/12', heightCls: 'h-3' })}
/>,
);
}
return array;
};
return (
<div className="card shadow-lg compact bg-base-100">
<div className="card-body">
<div className="mx-3">
<h5 className="card-title">
{loading ? (
skeleton({ widthCls: 'w-32', heightCls: 'h-8' })
) : (
<span className="text-base-content opacity-70">
Certification
</span>
)}
</h5>
</div>
<div className="text-base-content text-opacity-60">
<ol className="relative border-l border-base-300 border-opacity-30 my-2 mx-4">
{loading ? (
renderSkeleton()
) : (
<>
{certifications.map((certification, index) => (
<ListItem
key={index}
year={`${certification.year}`}
name={certification.name}
body={certification.body}
link={certification.link ? certification.link : undefined}
/>
))}
</>
)}
</ol>
</div>
</div>
</div>
);
};
export default CertificationCard;

View File

@@ -1,99 +0,0 @@
import { skeleton } from '../../helpers/utils';
import { Fragment } from 'react';
import PropTypes from 'prop-types';
const ListItem = ({ year, name, body, link }) => (
<li className="mb-5 ml-4">
<div
className="absolute w-2 h-2 bg-base-300 rounded-full border border-base-300 mt-1.5"
style={{ left: '-4.5px' }}
></div>
<div className="my-0.5 text-xs">{year}</div>
<div className="font-semibold">
<a href={link} target="_blank" rel="noreferrer">
{name}
</a>
</div>
<h3 className="mb-4 font-normal">{body}</h3>
</li>
);
const Certification = ({ certifications, loading }) => {
const renderSkeleton = () => {
let array = [];
for (let index = 0; index < 2; index++) {
array.push(
<ListItem
key={index}
year={skeleton({
width: 'w-5/12',
height: 'h-4',
})}
name={skeleton({
width: 'w-6/12',
height: 'h-4',
className: 'my-1.5',
})}
body={skeleton({ width: 'w-6/12', height: 'h-3' })}
/>
);
}
return array;
};
return (
<>
{certifications?.length !== 0 && (
<div className="card shadow-lg compact bg-base-100">
<div className="card-body">
<div className="mx-3">
<h5 className="card-title">
{loading ? (
skeleton({ width: 'w-32', height: 'h-8' })
) : (
<span className="text-base-content opacity-70">
Certification
</span>
)}
</h5>
</div>
<div className="text-base-content text-opacity-60">
<ol className="relative border-l border-base-300 border-opacity-30 my-2 mx-4">
{loading ? (
renderSkeleton()
) : (
<Fragment>
{certifications.map((certification, index) => (
<ListItem
key={index}
year={`${certification.year}`}
name={certification.name}
body={certification.body}
link={certification.link ? certification.link : null}
/>
))}
</Fragment>
)}
</ol>
</div>
</div>
</div>
)}
</>
);
};
ListItem.propTypes = {
year: PropTypes.node,
name: PropTypes.node,
body: PropTypes.node,
link: PropTypes.string,
};
Certification.propTypes = {
certifications: PropTypes.array.isRequired,
loading: PropTypes.bool.isRequired,
};
export default Certification;

View File

@@ -21,18 +21,32 @@ import {
FaLinkedin, FaLinkedin,
FaYoutube, FaYoutube,
} from 'react-icons/fa'; } from 'react-icons/fa';
import PropTypes from 'prop-types'; import { skeleton } from '../../utils';
import { skeleton } from '../../helpers/utils'; import { Profile } from '../../interfaces/profile';
import {
SanitizedGithub,
SanitizedSocial,
} from '../../interfaces/sanitized-config';
const isCompanyMention = (company) => { type Props = {
profile: Profile | null;
loading: boolean;
social: SanitizedSocial;
github: SanitizedGithub;
};
const isCompanyMention = (company: string): boolean => {
return company.startsWith('@') && !company.includes(' '); return company.startsWith('@') && !company.includes(' ');
}; };
const companyLink = (company) => { const companyLink = (company: string): string => {
return `https://github.com/${company.substring(1)}`; return `https://github.com/${company.substring(1)}`;
}; };
const getFormattedMastodonValue = (mastodonValue, isLink) => { const getFormattedMastodonValue = (
mastodonValue: string,
isLink: boolean,
): string => {
const [username, server] = mastodonValue.split('@'); const [username, server] = mastodonValue.split('@');
if (isLink) { if (isLink) {
@@ -42,7 +56,13 @@ const getFormattedMastodonValue = (mastodonValue, isLink) => {
} }
}; };
const ListItem = ({ icon, title, value, link, skeleton = false }) => { const ListItem: React.FC<{
icon: React.ReactNode;
title: React.ReactNode;
value: React.ReactNode;
link?: string;
skeleton?: boolean;
}> = ({ icon, title, value, link, skeleton = false }) => {
return ( return (
<a <a
href={link} href={link}
@@ -67,18 +87,27 @@ const ListItem = ({ icon, title, value, link, skeleton = false }) => {
); );
}; };
const Details = ({ profile, loading, social, github }) => { /**
* Renders the details card component.
*
* @param {Object} profile - The profile object.
* @param {boolean} loading - Indicates whether the data is loading.
* @param {Object} social - The social object.
* @param {Object} github - The GitHub object.
* @return {JSX.Element} The details card component.
*/
const DetailsCard = ({ profile, loading, social, github }: Props) => {
const renderSkeleton = () => { const renderSkeleton = () => {
let array = []; const array = [];
for (let index = 0; index < 4; index++) { for (let index = 0; index < 4; index++) {
array.push( array.push(
<ListItem <ListItem
key={index} key={index}
skeleton={true} skeleton={true}
icon={skeleton({ width: 'w-4', height: 'h-4' })} icon={skeleton({ widthCls: 'w-4', heightCls: 'h-4' })}
title={skeleton({ width: 'w-24', height: 'h-4' })} title={skeleton({ widthCls: 'w-24', heightCls: 'h-4' })}
value={skeleton({ width: 'w-full', height: 'h-4' })} value={skeleton({ widthCls: 'w-full', heightCls: 'h-4' })}
/> />,
); );
} }
@@ -108,7 +137,7 @@ const Details = ({ profile, loading, social, github }) => {
link={ link={
isCompanyMention(profile.company.trim()) isCompanyMention(profile.company.trim())
? companyLink(profile.company.trim()) ? companyLink(profile.company.trim())
: null : undefined
} }
/> />
)} )}
@@ -210,8 +239,14 @@ const Details = ({ profile, loading, social, github }) => {
<ListItem <ListItem
icon={<FaGlobe />} icon={<FaGlobe />}
title="Website:" title="Website:"
value={social.website} value={social.website
link={social.website} .replace('https://', '')
.replace('http://', '')}
link={
!social.website.startsWith('http')
? `http://${social.website}`
: social.website
}
/> />
)} )}
{social?.skype && ( {social?.skype && (
@@ -255,19 +290,4 @@ const Details = ({ profile, loading, social, github }) => {
); );
}; };
Details.propTypes = { export default DetailsCard;
profile: PropTypes.object,
loading: PropTypes.bool.isRequired,
social: PropTypes.object.isRequired,
github: PropTypes.object.isRequired,
};
ListItem.propTypes = {
icon: PropTypes.node,
title: PropTypes.node,
value: PropTypes.node,
link: PropTypes.string,
skeleton: PropTypes.bool,
};
export default Details;

View File

@@ -0,0 +1,90 @@
import React from 'react';
import { SanitizedEducation } from '../../interfaces/sanitized-config';
import { skeleton } from '../../utils';
const ListItem = ({
time,
degree,
institution,
}: {
time: React.ReactNode;
degree?: React.ReactNode;
institution?: React.ReactNode;
}) => (
<li className="mb-5 ml-4">
<div
className="absolute w-2 h-2 bg-base-300 rounded-full border border-base-300 mt-1.5"
style={{ left: '-4.5px' }}
></div>
<div className="my-0.5 text-xs">{time}</div>
<h3 className="font-semibold">{degree}</h3>
<div className="mb-4 font-normal">{institution}</div>
</li>
);
const EducationCard = ({
loading,
educations,
}: {
loading: boolean;
educations: SanitizedEducation[];
}) => {
const renderSkeleton = () => {
const array = [];
for (let index = 0; index < 2; index++) {
array.push(
<ListItem
key={index}
time={skeleton({
widthCls: 'w-5/12',
heightCls: 'h-4',
})}
degree={skeleton({
widthCls: 'w-6/12',
heightCls: 'h-4',
className: 'my-1.5',
})}
institution={skeleton({ widthCls: 'w-6/12', heightCls: 'h-3' })}
/>,
);
}
return array;
};
return (
<div className="card shadow-lg compact bg-base-100">
<div className="card-body">
<div className="mx-3">
<h5 className="card-title">
{loading ? (
skeleton({ widthCls: 'w-32', heightCls: 'h-8' })
) : (
<span className="text-base-content opacity-70">Education</span>
)}
</h5>
</div>
<div className="text-base-content text-opacity-60">
<ol className="relative border-l border-base-300 border-opacity-30 my-2 mx-4">
{loading ? (
renderSkeleton()
) : (
<>
{educations.map((item, index) => (
<ListItem
key={index}
time={`${item.from} - ${item.to}`}
degree={item.degree}
institution={item.institution}
/>
))}
</>
)}
</ol>
</div>
</div>
</div>
);
};
export default EducationCard;

View File

@@ -1,93 +0,0 @@
import { skeleton } from '../../helpers/utils';
import { Fragment } from 'react';
import PropTypes from 'prop-types';
const ListItem = ({ time, degree, institution }) => (
<li className="mb-5 ml-4">
<div
className="absolute w-2 h-2 bg-base-300 rounded-full border border-base-300 mt-1.5"
style={{ left: '-4.5px' }}
></div>
<div className="my-0.5 text-xs">{time}</div>
<h3 className="font-semibold">{degree}</h3>
<div className="mb-4 font-normal">{institution}</div>
</li>
);
const Education = ({ loading, education }) => {
const renderSkeleton = () => {
let array = [];
for (let index = 0; index < 2; index++) {
array.push(
<ListItem
key={index}
time={skeleton({
width: 'w-5/12',
height: 'h-4',
})}
degree={skeleton({
width: 'w-6/12',
height: 'h-4',
className: 'my-1.5',
})}
institution={skeleton({ width: 'w-6/12', height: 'h-3' })}
/>
);
}
return array;
};
return (
<>
{education?.length !== 0 && (
<div className="card shadow-lg compact bg-base-100">
<div className="card-body">
<div className="mx-3">
<h5 className="card-title">
{loading ? (
skeleton({ width: 'w-32', height: 'h-8' })
) : (
<span className="text-base-content opacity-70">
Education
</span>
)}
</h5>
</div>
<div className="text-base-content text-opacity-60">
<ol className="relative border-l border-base-300 border-opacity-30 my-2 mx-4">
{loading ? (
renderSkeleton()
) : (
<Fragment>
{education.map((item, index) => (
<ListItem
key={index}
time={`${item.from} - ${item.to}`}
degree={item.degree}
institution={item.institution}
/>
))}
</Fragment>
)}
</ol>
</div>
</div>
</div>
)}
</>
);
};
Education.propTypes = {
loading: PropTypes.bool.isRequired,
education: PropTypes.array.isRequired,
};
ListItem.propTypes = {
time: PropTypes.node,
degree: PropTypes.node,
institution: PropTypes.node,
};
export default Education;

View File

@@ -1,13 +1,19 @@
import PropTypes from 'prop-types'; import { CustomError } from '../../constants/errors';
const ErrorPage = (props) => { /**
* Render the ErrorPage component.
*
* @param props - The props for the ErrorPage component.
* @returns The rendered ErrorPage component.
*/
const ErrorPage: React.FC<CustomError> = (props) => {
return ( return (
<div className="min-w-screen min-h-screen bg-base-200 flex items-center p-5 lg:p-20 overflow-hidden relative"> <div className="min-w-screen min-h-screen bg-base-200 flex items-center p-5 lg:p-20 overflow-hidden relative">
<div className="flex-1 min-h-full min-w-full rounded-3xl bg-base-100 shadow-xl p-10 lg:p-20 text-gray-800 relative md:flex items-center text-center md:text-left"> <div className="flex-1 min-h-full min-w-full rounded-3xl bg-base-100 shadow-xl p-10 lg:p-20 text-gray-800 relative md:flex items-center text-center md:text-left">
<div className="w-full"> <div className="w-full">
<div className="mb-10 md:mb-20 mt-10 md:mt-20 text-gray-600 font-light"> <div className="mb-10 md:mb-20 mt-10 md:mt-20 text-gray-600 font-light">
<h1 className="font-black uppercase text-3xl lg:text-5xl text-primary mb-10"> <h1 className="font-black uppercase text-3xl lg:text-5xl text-primary mb-10">
{props.status} {`${props.status}`}
</h1> </h1>
<p className="text-lg pb-2 text-base-content">{props.title}</p> <p className="text-lg pb-2 text-base-content">{props.title}</p>
<div className="text-base-content text-opacity-60"> <div className="text-base-content text-opacity-60">
@@ -22,10 +28,4 @@ const ErrorPage = (props) => {
); );
}; };
ErrorPage.propTypes = {
status: PropTypes.string.isRequired,
title: PropTypes.string.isRequired,
subTitle: PropTypes.node.isRequired,
};
export default ErrorPage; export default ErrorPage;

View File

@@ -0,0 +1,100 @@
import React, { Fragment } from 'react';
import { SanitizedExperience } from '../../interfaces/sanitized-config';
import { skeleton } from '../../utils';
const ListItem = ({
time,
position,
company,
companyLink,
}: {
time: React.ReactNode;
position?: React.ReactNode;
company?: React.ReactNode;
companyLink?: string;
}) => (
<li className="mb-5 ml-4">
<div
className="absolute w-2 h-2 bg-base-300 rounded-full border border-base-300 mt-1.5"
style={{ left: '-4.5px' }}
></div>
<div className="my-0.5 text-xs">{time}</div>
<h3 className="font-semibold">{position}</h3>
<div className="mb-4 font-normal">
<a href={companyLink} target="_blank" rel="noreferrer">
{company}
</a>
</div>
</li>
);
const ExperienceCard = ({
experiences,
loading,
}: {
experiences: SanitizedExperience[];
loading: boolean;
}) => {
const renderSkeleton = () => {
const array = [];
for (let index = 0; index < 2; index++) {
array.push(
<ListItem
key={index}
time={skeleton({
widthCls: 'w-5/12',
heightCls: 'h-4',
})}
position={skeleton({
widthCls: 'w-6/12',
heightCls: 'h-4',
className: 'my-1.5',
})}
company={skeleton({ widthCls: 'w-6/12', heightCls: 'h-3' })}
/>,
);
}
return array;
};
return (
<div className="card shadow-lg compact bg-base-100">
<div className="card-body">
<div className="mx-3">
<h5 className="card-title">
{loading ? (
skeleton({ widthCls: 'w-32', heightCls: 'h-8' })
) : (
<span className="text-base-content opacity-70">Experience</span>
)}
</h5>
</div>
<div className="text-base-content text-opacity-60">
<ol className="relative border-l border-base-300 border-opacity-30 my-2 mx-4">
{loading ? (
renderSkeleton()
) : (
<Fragment>
{experiences.map((experience, index) => (
<ListItem
key={index}
time={`${experience.from} - ${experience.to}`}
position={experience.position}
company={experience.company}
companyLink={
experience.companyLink
? experience.companyLink
: undefined
}
/>
))}
</Fragment>
)}
</ol>
</div>
</div>
</div>
);
};
export default ExperienceCard;

View File

@@ -1,100 +0,0 @@
import { skeleton } from '../../helpers/utils';
import { Fragment } from 'react';
import PropTypes from 'prop-types';
const ListItem = ({ time, position, company, companyLink }) => (
<li className="mb-5 ml-4">
<div
className="absolute w-2 h-2 bg-base-300 rounded-full border border-base-300 mt-1.5"
style={{ left: '-4.5px' }}
></div>
<div className="my-0.5 text-xs">{time}</div>
<h3 className="font-semibold">{position}</h3>
<div className="mb-4 font-normal">
<a href={companyLink} target="_blank" rel="noreferrer">
{company}
</a>
</div>
</li>
);
const Experience = ({ experiences, loading }) => {
const renderSkeleton = () => {
let array = [];
for (let index = 0; index < 2; index++) {
array.push(
<ListItem
key={index}
time={skeleton({
width: 'w-5/12',
height: 'h-4',
})}
position={skeleton({
width: 'w-6/12',
height: 'h-4',
className: 'my-1.5',
})}
company={skeleton({ width: 'w-6/12', height: 'h-3' })}
/>
);
}
return array;
};
return (
<>
{experiences?.length !== 0 && (
<div className="card shadow-lg compact bg-base-100">
<div className="card-body">
<div className="mx-3">
<h5 className="card-title">
{loading ? (
skeleton({ width: 'w-32', height: 'h-8' })
) : (
<span className="text-base-content opacity-70">
Experience
</span>
)}
</h5>
</div>
<div className="text-base-content text-opacity-60">
<ol className="relative border-l border-base-300 border-opacity-30 my-2 mx-4">
{loading ? (
renderSkeleton()
) : (
<Fragment>
{experiences.map((experience, index) => (
<ListItem
key={index}
time={`${experience.from} - ${experience.to}`}
position={experience.position}
company={experience.company}
companyLink={
experience.companyLink ? experience.companyLink : null
}
/>
))}
</Fragment>
)}
</ol>
</div>
</div>
</div>
)}
</>
);
};
ListItem.propTypes = {
time: PropTypes.node,
position: PropTypes.node,
company: PropTypes.node,
companyLink: PropTypes.string,
};
Experience.propTypes = {
experiences: PropTypes.array.isRequired,
loading: PropTypes.bool.isRequired,
};
export default Experience;

View File

@@ -1,23 +1,21 @@
import { Fragment } from 'react'; import { Fragment } from 'react';
import PropTypes from 'prop-types';
import { ga, skeleton } from '../../helpers/utils';
import LazyImage from '../lazy-image'; import LazyImage from '../lazy-image';
import { ga, skeleton } from '../../utils';
import { SanitizedExternalProject } from '../../interfaces/sanitized-config';
const displaySection = (externalProjects) => { const ExternalProjectCard = ({
if ( externalProjects,
externalProjects && header,
Array.isArray(externalProjects) && loading,
externalProjects.length googleAnalyticId,
) { }: {
return true; externalProjects: SanitizedExternalProject[];
} else { header: string;
return false; loading: boolean;
} googleAnalyticId?: string;
}; }) => {
const ExternalProject = ({ externalProjects, loading, googleAnalytics }) => {
const renderSkeleton = () => { const renderSkeleton = () => {
let array = []; const array = [];
for (let index = 0; index < externalProjects.length; index++) { for (let index = 0; index < externalProjects.length; index++) {
array.push( array.push(
<div className="card shadow-lg compact bg-base-100" key={index}> <div className="card shadow-lg compact bg-base-100" key={index}>
@@ -28,31 +26,31 @@ const ExternalProject = ({ externalProjects, loading, googleAnalytics }) => {
<div className="w-full"> <div className="w-full">
<h2> <h2>
{skeleton({ {skeleton({
width: 'w-32', widthCls: 'w-32',
height: 'h-8', heightCls: 'h-8',
className: 'mb-2 mx-auto', className: 'mb-2 mx-auto',
})} })}
</h2> </h2>
<div className="avatar w-full h-full"> <div className="avatar w-full h-full">
<div className="w-20 h-20 mask mask-squircle mx-auto"> <div className="w-20 h-20 mask mask-squircle mx-auto">
{skeleton({ {skeleton({
width: 'w-full', widthCls: 'w-full',
height: 'h-full', heightCls: 'h-full',
shape: '', shape: '',
})} })}
</div> </div>
</div> </div>
<div className="mt-2"> <div className="mt-2">
{skeleton({ {skeleton({
width: 'w-full', widthCls: 'w-full',
height: 'h-4', heightCls: 'h-4',
className: 'mx-auto', className: 'mx-auto',
})} })}
</div> </div>
<div className="mt-2 flex items-center flex-wrap justify-center"> <div className="mt-2 flex items-center flex-wrap justify-center">
{skeleton({ {skeleton({
width: 'w-full', widthCls: 'w-full',
height: 'h-4', heightCls: 'h-4',
className: 'mx-auto', className: 'mx-auto',
})} })}
</div> </div>
@@ -61,7 +59,7 @@ const ExternalProject = ({ externalProjects, loading, googleAnalytics }) => {
</div> </div>
</div> </div>
</div> </div>
</div> </div>,
); );
} }
@@ -78,12 +76,9 @@ const ExternalProject = ({ externalProjects, loading, googleAnalytics }) => {
e.preventDefault(); e.preventDefault();
try { try {
if (googleAnalytics?.id) { if (googleAnalyticId) {
ga.event({ ga.event('Click External Project', {
action: 'Click External Project', post: item.title,
params: {
post: item.title,
},
}); });
} }
} catch (error) { } catch (error) {
@@ -108,8 +103,8 @@ const ExternalProject = ({ externalProjects, loading, googleAnalytics }) => {
src={item.imageUrl} src={item.imageUrl}
alt={'thumbnail'} alt={'thumbnail'}
placeholder={skeleton({ placeholder={skeleton({
width: 'w-full', widthCls: 'w-full',
height: 'h-full', heightCls: 'h-full',
shape: '', shape: '',
})} })}
/> />
@@ -130,42 +125,34 @@ const ExternalProject = ({ externalProjects, loading, googleAnalytics }) => {
return ( return (
<Fragment> <Fragment>
{displaySection(externalProjects) && ( <div className="col-span-1 lg:col-span-2">
<div className="col-span-1 lg:col-span-2"> <div className="grid grid-cols-2 gap-6">
<div className="grid grid-cols-2 gap-6"> <div className="col-span-2">
<div className="col-span-2"> <div className="card compact bg-base-100 shadow bg-opacity-40">
<div className="card compact bg-base-100 shadow bg-opacity-40"> <div className="card-body">
<div className="card-body"> <div className="mx-3 flex items-center justify-between mb-2">
<div className="mx-3 flex items-center justify-between mb-2"> <h5 className="card-title">
<h5 className="card-title"> {loading ? (
{loading ? ( skeleton({ widthCls: 'w-40', heightCls: 'h-8' })
skeleton({ width: 'w-40', height: 'h-8' }) ) : (
) : ( <span className="text-base-content opacity-70">
<span className="text-base-content opacity-70"> {header}
My Projects </span>
</span> )}
)} </h5>
</h5> </div>
</div> <div className="col-span-2">
<div className="col-span-2"> <div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<div className="grid grid-cols-1 md:grid-cols-2 gap-6"> {loading ? renderSkeleton() : renderExternalProjects()}
{loading ? renderSkeleton() : renderExternalProjects()}
</div>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
)} </div>
</Fragment> </Fragment>
); );
}; };
ExternalProject.propTypes = { export default ExternalProjectCard;
externalProjects: PropTypes.array,
loading: PropTypes.bool.isRequired,
googleAnalytics: PropTypes.object,
};
export default ExternalProject;

View File

@@ -1,24 +0,0 @@
import PropTypes from 'prop-types';
import { skeleton } from '../../helpers/utils';
const Footer = ({ content, loading }) => {
if (!content) return null;
return (
<div className="card-body">
{loading ? (
skeleton({ width: 'w-52', height: 'h-6' })
) : (
<div dangerouslySetInnerHTML={{ __html: content }} />
)}
</div>
);
};
Footer.propTypes = {
content: PropTypes.string,
loading: PropTypes.bool.isRequired,
};
export default Footer;

View File

@@ -0,0 +1,23 @@
import { skeleton } from '../../utils';
const Footer = ({
content,
loading,
}: {
content: string | null;
loading: boolean;
}) => {
if (!content) return null;
return (
<div className="card-body">
{loading ? (
skeleton({ widthCls: 'w-52', heightCls: 'h-6' })
) : (
<div dangerouslySetInnerHTML={{ __html: content }} />
)}
</div>
);
};
export default Footer;

View File

@@ -1,17 +1,31 @@
import PropTypes from 'prop-types';
import { Fragment } from 'react'; import { Fragment } from 'react';
import { AiOutlineFork, AiOutlineStar } from 'react-icons/ai'; import { AiOutlineFork, AiOutlineStar } from 'react-icons/ai';
import { MdInsertLink } from 'react-icons/md'; import { MdInsertLink } from 'react-icons/md';
import { ga, languageColor, skeleton } from '../../helpers/utils'; import { ga, getLanguageColor, skeleton } from '../../utils';
import { GithubProject } from '../../interfaces/github-project';
const GitHubProject = ({ repo, loading, github, googleAnalytics }) => { const GithubProjectCard = ({
if (!loading && Array.isArray(repo) && repo.length === 0) { header,
return <></>; githubProjects,
loading,
limit,
username,
googleAnalyticsId,
}: {
header: string;
githubProjects: GithubProject[];
loading: boolean;
limit: number;
username: string;
googleAnalyticsId?: string;
}) => {
if (!loading && githubProjects.length === 0) {
return;
} }
const renderSkeleton = () => { const renderSkeleton = () => {
let array = []; const array = [];
for (let index = 0; index < github.limit; index++) { for (let index = 0; index < limit; index++) {
array.push( array.push(
<div className="card shadow-lg compact bg-base-100" key={index}> <div className="card shadow-lg compact bg-base-100" key={index}>
<div className="flex justify-between flex-col p-8 h-full w-full"> <div className="flex justify-between flex-col p-8 h-full w-full">
@@ -20,8 +34,8 @@ const GitHubProject = ({ repo, loading, github, googleAnalytics }) => {
<span> <span>
<h5 className="card-title text-lg"> <h5 className="card-title text-lg">
{skeleton({ {skeleton({
width: 'w-32', widthCls: 'w-32',
height: 'h-8', heightCls: 'h-8',
className: 'mb-1', className: 'mb-1',
})} })}
</h5> </h5>
@@ -29,30 +43,30 @@ const GitHubProject = ({ repo, loading, github, googleAnalytics }) => {
</div> </div>
<div className="mb-5 mt-1"> <div className="mb-5 mt-1">
{skeleton({ {skeleton({
width: 'w-full', widthCls: 'w-full',
height: 'h-4', heightCls: 'h-4',
className: 'mb-2', className: 'mb-2',
})} })}
{skeleton({ width: 'w-full', height: 'h-4' })} {skeleton({ widthCls: 'w-full', heightCls: 'h-4' })}
</div> </div>
</div> </div>
<div className="flex justify-between"> <div className="flex justify-between">
<div className="flex flex-grow"> <div className="flex flex-grow">
<span className="mr-3 flex items-center"> <span className="mr-3 flex items-center">
{skeleton({ width: 'w-12', height: 'h-4' })} {skeleton({ widthCls: 'w-12', heightCls: 'h-4' })}
</span> </span>
<span className="flex items-center"> <span className="flex items-center">
{skeleton({ width: 'w-12', height: 'h-4' })} {skeleton({ widthCls: 'w-12', heightCls: 'h-4' })}
</span> </span>
</div> </div>
<div> <div>
<span className="flex items-center"> <span className="flex items-center">
{skeleton({ width: 'w-12', height: 'h-4' })} {skeleton({ widthCls: 'w-12', heightCls: 'h-4' })}
</span> </span>
</div> </div>
</div> </div>
</div> </div>
</div> </div>,
); );
} }
@@ -60,7 +74,7 @@ const GitHubProject = ({ repo, loading, github, googleAnalytics }) => {
}; };
const renderProjects = () => { const renderProjects = () => {
return repo.map((item, index) => ( return githubProjects.map((item, index) => (
<a <a
className="card shadow-lg compact bg-base-100 cursor-pointer" className="card shadow-lg compact bg-base-100 cursor-pointer"
href={item.html_url} href={item.html_url}
@@ -69,12 +83,9 @@ const GitHubProject = ({ repo, loading, github, googleAnalytics }) => {
e.preventDefault(); e.preventDefault();
try { try {
if (googleAnalytics?.id) { if (googleAnalyticsId) {
ga.event({ ga.event('Click project', {
action: 'Click project', project: item.name,
params: {
project: item.name,
},
}); });
} }
} catch (error) { } catch (error) {
@@ -111,7 +122,7 @@ const GitHubProject = ({ repo, loading, github, googleAnalytics }) => {
<span className="flex items-center"> <span className="flex items-center">
<div <div
className="w-3 h-3 rounded-full mr-1 opacity-60" className="w-3 h-3 rounded-full mr-1 opacity-60"
style={{ backgroundColor: languageColor(item.language) }} style={{ backgroundColor: getLanguageColor(item.language) }}
/> />
<span>{item.language}</span> <span>{item.language}</span>
</span> </span>
@@ -132,18 +143,18 @@ const GitHubProject = ({ repo, loading, github, googleAnalytics }) => {
<div className="mx-3 flex items-center justify-between mb-2"> <div className="mx-3 flex items-center justify-between mb-2">
<h5 className="card-title"> <h5 className="card-title">
{loading ? ( {loading ? (
skeleton({ width: 'w-40', height: 'h-8' }) skeleton({ widthCls: 'w-40', heightCls: 'h-8' })
) : ( ) : (
<span className="text-base-content opacity-70"> <span className="text-base-content opacity-70">
GitHub Projects {header}
</span> </span>
)} )}
</h5> </h5>
{loading ? ( {loading ? (
skeleton({ width: 'w-10', height: 'h-5' }) skeleton({ widthCls: 'w-10', heightCls: 'h-5' })
) : ( ) : (
<a <a
href={`https://github.com/${github.username}?tab=repositories`} href={`https://github.com/${username}?tab=repositories`}
target="_blank" target="_blank"
rel="noreferrer" rel="noreferrer"
className="text-base-content opacity-50 hover:underline" className="text-base-content opacity-50 hover:underline"
@@ -154,7 +165,7 @@ const GitHubProject = ({ repo, loading, github, googleAnalytics }) => {
</div> </div>
<div className="col-span-2"> <div className="col-span-2">
<div className="grid grid-cols-1 md:grid-cols-2 gap-6"> <div className="grid grid-cols-1 md:grid-cols-2 gap-6">
{loading || !repo ? renderSkeleton() : renderProjects()} {loading ? renderSkeleton() : renderProjects()}
</div> </div>
</div> </div>
</div> </div>
@@ -166,11 +177,4 @@ const GitHubProject = ({ repo, loading, github, googleAnalytics }) => {
); );
}; };
GitHubProject.propTypes = { export default GithubProjectCard;
repo: PropTypes.array,
loading: PropTypes.bool.isRequired,
github: PropTypes.object.isRequired,
googleAnalytics: PropTypes.object.isRequired,
};
export default GitHubProject;

View File

@@ -0,0 +1,296 @@
import { useCallback, useEffect, useState } from 'react';
import axios, { AxiosError } from 'axios';
import { formatDistance } from 'date-fns';
import {
CustomError,
GENERIC_ERROR,
INVALID_CONFIG_ERROR,
INVALID_GITHUB_USERNAME_ERROR,
setTooManyRequestError,
} from '../constants/errors';
import { HelmetProvider } from 'react-helmet-async';
import '../assets/index.css';
import { getInitialTheme, getSanitizedConfig, setupHotjar } from '../utils';
import { SanitizedConfig } from '../interfaces/sanitized-config';
import ErrorPage from './error-page';
import HeadTagEditor from './head-tag-editor';
import { DEFAULT_THEMES } from '../constants/default-themes';
import ThemeChanger from './theme-changer';
import { BG_COLOR } from '../constants';
import AvatarCard from './avatar-card';
import { Profile } from '../interfaces/profile';
import DetailsCard from './details-card';
import SkillCard from './skill-card';
import ExperienceCard from './experience-card';
import EducationCard from './education-card';
import CertificationCard from './certification-card';
import { GithubProject } from '../interfaces/github-project';
import GithubProjectCard from './github-project-card';
import ExternalProjectCard from './external-project-card';
import BlogCard from './blog-card';
import Footer from './footer';
/**
* Renders the GitProfile component.
*
* @param {Object} config - the configuration object
* @return {JSX.Element} the rendered GitProfile component
*/
const GitProfile = ({ config }: { config: Config }) => {
const [sanitizedConfig] = useState<SanitizedConfig | Record<string, never>>(
getSanitizedConfig(config),
);
const [theme, setTheme] = useState<string>(DEFAULT_THEMES[0]);
const [error, setError] = useState<CustomError | null>(null);
const [loading, setLoading] = useState<boolean>(false);
const [profile, setProfile] = useState<Profile | null>(null);
const [githubProjects, setGithubProjects] = useState<GithubProject[]>([]);
const getGithubProjects = useCallback(
async (publicRepoCount: number): Promise<GithubProject[]> => {
if (sanitizedConfig.projects.github.mode === 'automatic') {
if (publicRepoCount === 0) {
return [];
}
const excludeRepo =
sanitizedConfig.projects.github.automatic.exclude.projects
.map((project) => `+-repo:${project}`)
.join('');
const query = `user:${sanitizedConfig.github.username}+fork:${!sanitizedConfig.projects.github.automatic.exclude.forks}${excludeRepo}`;
const url = `https://api.github.com/search/repositories?q=${query}&sort=${sanitizedConfig.projects.github.automatic.sortBy}&per_page=${sanitizedConfig.projects.github.automatic.limit}&type=Repositories`;
const repoResponse = await axios.get(url, {
headers: { 'Content-Type': 'application/vnd.github.v3+json' },
});
const repoData = repoResponse.data;
return repoData.items;
} else {
if (sanitizedConfig.projects.github.manual.projects.length === 0) {
return [];
}
const repos = sanitizedConfig.projects.github.manual.projects
.map((project) => `+repo:${project}`)
.join('');
const url = `https://api.github.com/search/repositories?q=${repos}&type=Repositories`;
const repoResponse = await axios.get(url, {
headers: { 'Content-Type': 'application/vnd.github.v3+json' },
});
const repoData = repoResponse.data;
return repoData.items;
}
},
[
sanitizedConfig.github.username,
sanitizedConfig.projects.github.mode,
sanitizedConfig.projects.github.manual.projects,
sanitizedConfig.projects.github.automatic.sortBy,
sanitizedConfig.projects.github.automatic.limit,
sanitizedConfig.projects.github.automatic.exclude.forks,
sanitizedConfig.projects.github.automatic.exclude.projects,
],
);
const loadData = useCallback(async () => {
try {
setLoading(true);
const response = await axios.get(
`https://api.github.com/users/${sanitizedConfig.github.username}`,
);
const data = response.data;
setProfile({
avatar: data.avatar_url,
name: data.name || ' ',
bio: data.bio || '',
location: data.location || '',
company: data.company || '',
});
if (!sanitizedConfig.projects.github.display) {
return;
}
setGithubProjects(await getGithubProjects(data.public_repos));
} catch (error) {
handleError(error as AxiosError | Error);
} finally {
setLoading(false);
}
}, [
sanitizedConfig.github.username,
sanitizedConfig.projects.github.display,
getGithubProjects,
]);
useEffect(() => {
if (Object.keys(sanitizedConfig).length === 0) {
setError(INVALID_CONFIG_ERROR);
} else {
setError(null);
setTheme(getInitialTheme(sanitizedConfig.themeConfig));
setupHotjar(sanitizedConfig.hotjar);
loadData();
}
}, [sanitizedConfig, loadData]);
useEffect(() => {
theme && document.documentElement.setAttribute('data-theme', theme);
}, [theme]);
const handleError = (error: AxiosError | Error): void => {
console.error('Error:', error);
if (error instanceof AxiosError) {
try {
const reset = formatDistance(
new Date(error.response?.headers?.['x-ratelimit-reset'] * 1000),
new Date(),
{ addSuffix: true },
);
if (typeof error.response?.status === 'number') {
switch (error.response.status) {
case 403:
setError(setTooManyRequestError(reset));
break;
case 404:
setError(INVALID_GITHUB_USERNAME_ERROR);
break;
default:
setError(GENERIC_ERROR);
break;
}
} else {
setError(GENERIC_ERROR);
}
} catch (innerError) {
setError(GENERIC_ERROR);
}
} else {
setError(GENERIC_ERROR);
}
};
return (
<HelmetProvider>
<div className="fade-in h-screen">
{error ? (
<ErrorPage
status={error.status}
title={error.title}
subTitle={error.subTitle}
/>
) : (
<>
<HeadTagEditor
googleAnalyticsId={sanitizedConfig.googleAnalytics.id}
appliedTheme={theme}
/>
<div className={`p-4 lg:p-10 min-h-full ${BG_COLOR}`}>
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6 rounded-box">
<div className="col-span-1">
<div className="grid grid-cols-1 gap-6">
{!sanitizedConfig.themeConfig.disableSwitch && (
<ThemeChanger
theme={theme}
setTheme={setTheme}
loading={loading}
themeConfig={sanitizedConfig.themeConfig}
/>
)}
<AvatarCard
profile={profile}
loading={loading}
avatarRing={sanitizedConfig.themeConfig.displayAvatarRing}
resumeFileUrl={sanitizedConfig.resume.fileUrl}
/>
<DetailsCard
profile={profile}
loading={loading}
github={sanitizedConfig.github}
social={sanitizedConfig.social}
/>
{sanitizedConfig.skills.length !== 0 && (
<SkillCard
loading={loading}
skills={sanitizedConfig.skills}
/>
)}
{sanitizedConfig.experiences.length !== 0 && (
<ExperienceCard
loading={loading}
experiences={sanitizedConfig.experiences}
/>
)}
{sanitizedConfig.certifications.length !== 0 && (
<CertificationCard
loading={loading}
certifications={sanitizedConfig.certifications}
/>
)}
{sanitizedConfig.educations.length !== 0 && (
<EducationCard
loading={loading}
educations={sanitizedConfig.educations}
/>
)}
</div>
</div>
<div className="lg:col-span-2 col-span-1">
<div className="grid grid-cols-1 gap-6">
{sanitizedConfig.projects.github.display && (
<GithubProjectCard
header={sanitizedConfig.projects.github.header}
limit={sanitizedConfig.projects.github.automatic.limit}
githubProjects={githubProjects}
loading={loading}
username={sanitizedConfig.github.username}
googleAnalyticsId={sanitizedConfig.googleAnalytics.id}
/>
)}
{sanitizedConfig.projects.external.projects.length !==
0 && (
<ExternalProjectCard
loading={loading}
header={sanitizedConfig.projects.external.header}
externalProjects={
sanitizedConfig.projects.external.projects
}
googleAnalyticId={sanitizedConfig.googleAnalytics.id}
/>
)}
{sanitizedConfig.blog.display && (
<BlogCard
loading={loading}
googleAnalyticsId={sanitizedConfig.googleAnalytics.id}
blog={sanitizedConfig.blog}
/>
)}
</div>
</div>
</div>
</div>
{sanitizedConfig.footer && (
<footer
className={`p-4 footer ${BG_COLOR} text-base-content footer-center`}
>
<div className="card compact bg-base-100 shadow">
<Footer content={sanitizedConfig.footer} loading={loading} />
</div>
</footer>
)}
</>
)}
</div>
</HelmetProvider>
);
};
export default GitProfile;

View File

@@ -1,69 +0,0 @@
import { Fragment } from 'react';
import { Helmet } from 'react-helmet-async';
import PropTypes from 'prop-types';
import { isDarkishTheme } from '../../helpers/utils';
const HeadTagEditor = ({ profile, theme, googleAnalytics, social }) => {
return (
<Fragment>
{profile && (
<Helmet>
{googleAnalytics?.id && (
<script
async
src={`https://www.googletagmanager.com/gtag/js?id=${googleAnalytics.id}`}
></script>
)}
{googleAnalytics?.id && (
<script>
{`window.dataLayer = window.dataLayer || [];
function gtag(){dataLayer.push(arguments);}
gtag('js', new Date());
gtag('config', '${googleAnalytics.id}');`}
</script>
)}
<title>Portfolio{profile.name && ` of ${profile.name}`}</title>
<meta
name="theme-color"
content={isDarkishTheme(theme) ? '#000000' : '#ffffff'}
/>
<meta name="description" content={profile.bio} />
<meta
itemProp="name"
content={`Portfolio${profile.name && ` of ${profile.name}`}`}
/>
<meta itemProp="description" content={profile.bio} />
<meta itemProp="image" content={profile.avatar} />
<meta property="og:url" content={social?.website || ''} />
<meta property="og:type" content="website" />
<meta
property="og:title"
content={`Portfolio${profile.name && ` of ${profile.name}`}`}
/>
<meta property="og:description" content={profile.bio} />
<meta property="og:image" content={profile.avatar} />
<meta name="twitter:card" content="summary_large_image" />
<meta
name="twitter:title"
content={`Portfolio${profile.name && ` of ${profile.name}`}`}
/>
<meta name="twitter:description" content={profile.bio} />
<meta name="twitter:image" content={profile.avatar} />
</Helmet>
)}
</Fragment>
);
};
HeadTagEditor.propTypes = {
profile: PropTypes.object,
theme: PropTypes.string,
googleAnalytics: PropTypes.object.isRequired,
social: PropTypes.object.isRequired,
};
export default HeadTagEditor;

View File

@@ -0,0 +1,47 @@
import { Helmet } from 'react-helmet-async';
import { isDarkishTheme } from '../../utils';
type HeadTagEditorProps = {
googleAnalyticsId?: string;
appliedTheme: string;
};
/**
* Renders the head tag editor component.
*
* @param {HeadTagEditorProps} googleAnalyticsId - The Google Analytics ID.
* @param {HeadTagEditorProps} appliedTheme - The applied theme.
* @return {React.ReactElement} The head tag editor component.
*/
const HeadTagEditor: React.FC<HeadTagEditorProps> = ({
googleAnalyticsId,
appliedTheme,
}) => {
return (
<Helmet>
<meta
name="theme-color"
content={isDarkishTheme(appliedTheme) ? '#000000' : '#ffffff'}
/>
{googleAnalyticsId && (
<>
<script
async
src={`https://www.googletagmanager.com/gtag/js?id=${googleAnalyticsId}`}
></script>
<script>
{`window.dataLayer = window.dataLayer || [];
function gtag() {
dataLayer.push(arguments);
}
gtag('js', new Date());
gtag('config', '${googleAnalyticsId}');
`}
</script>
</>
)}
</Helmet>
);
};
export default HeadTagEditor;

View File

@@ -1,29 +0,0 @@
import { useState, Fragment, useEffect } from 'react';
import PropTypes from 'prop-types';
const LazyImage = ({ placeholder, src, alt, ...rest }) => {
const [loading, setLoading] = useState(true);
useEffect(() => {
const imageToLoad = new Image();
imageToLoad.src = src;
imageToLoad.onload = () => {
setLoading(false);
};
}, [src]);
return (
<Fragment>
{loading ? placeholder : <img src={src} alt={alt} {...rest} />}
</Fragment>
);
};
LazyImage.propTypes = {
placeholder: PropTypes.node,
alt: PropTypes.string,
src: PropTypes.string,
};
export default LazyImage;

View File

@@ -0,0 +1,38 @@
import { useState, Fragment, useEffect } from 'react';
/**
* LazyImage component.
*
* @param {string} placeholder The placeholder image URL.
* @param {string} src The image URL.
* @param {string} alt The alt text for the image.
* @param {object} rest Additional props for the image element.
*
* @returns {ReactElement} The LazyImage component.
*/
const LazyImage: React.FC<{
placeholder: React.ReactElement;
src: string;
alt: string;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
[key: string]: any;
}> = ({ placeholder, src, alt, ...rest }): React.ReactElement => {
const [loading, setLoading] = useState(true);
useEffect(() => {
const imageToLoad = new Image();
imageToLoad.src = src;
imageToLoad.onload = () => {
setLoading(false);
};
}, [src]);
return (
<Fragment>
{loading ? placeholder : <img src={src} alt={alt} {...rest} />}
</Fragment>
);
};
export default LazyImage;

View File

@@ -0,0 +1,54 @@
import { skeleton } from '../../utils';
const SkillCard = ({
loading,
skills,
}: {
loading: boolean;
skills: string[];
}) => {
const renderSkeleton = () => {
const array = [];
for (let index = 0; index < 12; index++) {
array.push(
<div key={index}>
{skeleton({ widthCls: 'w-16', heightCls: 'h-4', className: 'm-1' })}
</div>,
);
}
return array;
};
return (
<div className="card shadow-lg compact bg-base-100">
<div className="card-body">
<div className="mx-3">
<h5 className="card-title">
{loading ? (
skeleton({ widthCls: 'w-32', heightCls: 'h-8' })
) : (
<span className="text-base-content opacity-70">Tech Stack</span>
)}
</h5>
</div>
<div className="p-3 flow-root">
<div className="-m-1 flex flex-wrap justify-center">
{loading
? renderSkeleton()
: skills.map((skill, index) => (
<div
key={index}
className="m-1 text-xs inline-flex items-center font-bold leading-sm px-3 py-1 badge-primary bg-opacity-90 rounded-full"
>
{skill}
</div>
))}
</div>
</div>
</div>
</div>
);
};
export default SkillCard;

View File

@@ -1,60 +0,0 @@
import { skeleton } from '../../helpers/utils';
import PropTypes from 'prop-types';
const Skill = ({ loading, skills }) => {
const renderSkeleton = () => {
let array = [];
for (let index = 0; index < 12; index++) {
array.push(
<div key={index}>
{skeleton({ width: 'w-16', height: 'h-4', className: 'm-1' })}
</div>
);
}
return array;
};
return (
<>
{skills?.length !== 0 && (
<div className="card shadow-lg compact bg-base-100">
<div className="card-body">
<div className="mx-3">
<h5 className="card-title">
{loading ? (
skeleton({ width: 'w-32', height: 'h-8' })
) : (
<span className="text-base-content opacity-70">
Tech Stack
</span>
)}
</h5>
</div>
<div className="p-3 flow-root">
<div className="-m-1 flex flex-wrap justify-center">
{loading
? renderSkeleton()
: skills.map((skill, index) => (
<div
key={index}
className="m-1 text-xs inline-flex items-center font-bold leading-sm px-3 py-1 badge-primary bg-opacity-90 rounded-full"
>
{skill}
</div>
))}
</div>
</div>
</div>
</div>
)}
</>
);
};
Skill.propTypes = {
loading: PropTypes.bool.isRequired,
skills: PropTypes.array.isRequired,
};
export default Skill;

View File

@@ -1,14 +1,40 @@
import { AiOutlineControl } from 'react-icons/ai'; import { AiOutlineControl } from 'react-icons/ai';
import { skeleton } from '../../helpers/utils'; import { SanitizedThemeConfig } from '../../interfaces/sanitized-config';
import PropTypes from 'prop-types'; import { LOCAL_STORAGE_KEY_NAME } from '../../constants';
import { skeleton } from '../../utils';
import { MouseEvent } from 'react';
const ThemeChanger = ({ theme, setTheme, loading, themeConfig }) => { /**
const changeTheme = (e, selectedTheme) => { * Renders a theme changer component.
*
* @param {Object} props - The props object.
* @param {string} props.theme - The current theme.
* @param {function} props.setTheme - A function to set the theme.
* @param {boolean} props.loading - Whether the component is in a loading state.
* @param {SanitizedThemeConfig} props.themeConfig - The theme configuration object.
* @return {JSX.Element} The rendered theme changer component.
*/
const ThemeChanger = ({
theme,
setTheme,
loading,
themeConfig,
}: {
theme: string;
setTheme: (theme: string) => void;
loading: boolean;
themeConfig: SanitizedThemeConfig;
}) => {
const changeTheme = (
e: MouseEvent<HTMLAnchorElement>,
selectedTheme: string,
) => {
e.preventDefault(); e.preventDefault();
document.querySelector('html').setAttribute('data-theme', selectedTheme);
document.querySelector('html')?.setAttribute('data-theme', selectedTheme);
typeof window !== 'undefined' && typeof window !== 'undefined' &&
localStorage.setItem('gitprofile-theme', selectedTheme); localStorage.setItem(LOCAL_STORAGE_KEY_NAME, selectedTheme);
setTheme(selectedTheme); setTheme(selectedTheme);
}; };
@@ -19,24 +45,28 @@ const ThemeChanger = ({ theme, setTheme, loading, themeConfig }) => {
<div className="flex-1"> <div className="flex-1">
<h5 className="card-title"> <h5 className="card-title">
{loading ? ( {loading ? (
skeleton({ width: 'w-20', height: 'h-8', className: 'mb-1' }) skeleton({
widthCls: 'w-20',
heightCls: 'h-8',
className: 'mb-1',
})
) : ( ) : (
<span className="text-base-content opacity-70">Theme</span> <span className="text-base-content opacity-70">Theme</span>
)} )}
</h5> </h5>
<span className="text-base-content text-opacity-40 capitalize text-sm"> <span className="text-base-content text-opacity-40 capitalize text-sm">
{loading {loading
? skeleton({ width: 'w-16', height: 'h-5' }) ? skeleton({ widthCls: 'w-16', heightCls: 'h-5' })
: theme === themeConfig.defaultTheme : theme === themeConfig.defaultTheme
? 'Default' ? 'Default'
: theme} : theme}
</span> </span>
</div> </div>
<div className="flex-0"> <div className="flex-0">
{loading ? ( {loading ? (
skeleton({ skeleton({
width: 'w-14 md:w-28', widthCls: 'w-14 md:w-28',
height: 'h-10', heightCls: 'h-10',
className: 'mr-6', className: 'mr-6',
}) })
) : ( ) : (
@@ -63,11 +93,11 @@ const ThemeChanger = ({ theme, setTheme, loading, themeConfig }) => {
{[ {[
themeConfig.defaultTheme, themeConfig.defaultTheme,
...themeConfig.themes.filter( ...themeConfig.themes.filter(
(item) => item !== themeConfig.defaultTheme (item) => item !== themeConfig.defaultTheme,
), ),
].map((item, index) => ( ].map((item, index) => (
<li key={index}> <li key={index}>
{/* eslint-disable-next-line */} {}
<a <a
onClick={(e) => changeTheme(e, item)} onClick={(e) => changeTheme(e, item)}
className={`${theme === item ? 'active' : ''}`} className={`${theme === item ? 'active' : ''}`}
@@ -88,11 +118,4 @@ const ThemeChanger = ({ theme, setTheme, loading, themeConfig }) => {
); );
}; };
ThemeChanger.propTypes = {
theme: PropTypes.string,
setTheme: PropTypes.func.isRequired,
loading: PropTypes.bool.isRequired,
themeConfig: PropTypes.object.isRequired,
};
export default ThemeChanger; export default ThemeChanger;

View File

@@ -0,0 +1,9 @@
export const DEFAULT_CUSTOM_THEME = {
primary: '#fc055b',
secondary: '#219aaf',
accent: '#e8d03a',
neutral: '#2A2730',
'base-100': '#E3E3ED',
'--rounded-box': '3rem',
'--rounded-btn': '3rem',
};

View File

@@ -0,0 +1,35 @@
export const DEFAULT_THEMES = [
'light',
'dark',
'cupcake',
'bumblebee',
'emerald',
'corporate',
'synthwave',
'retro',
'cyberpunk',
'valentine',
'halloween',
'garden',
'forest',
'aqua',
'lofi',
'pastel',
'fantasy',
'wireframe',
'black',
'luxury',
'dracula',
'cmyk',
'autumn',
'business',
'acid',
'lemonade',
'night',
'coffee',
'winter',
'dim',
'nord',
'sunset',
'procyon',
];

55
src/constants/errors.tsx Normal file
View File

@@ -0,0 +1,55 @@
import { ReactElement } from 'react';
export interface CustomError {
status: number;
title: string;
subTitle: string | ReactElement;
}
export const INVALID_CONFIG_ERROR: CustomError = {
status: 500,
title: 'Invalid Config!',
subTitle: (
<p>
Please provide correct config in <code>gitprofile.config.ts</code>.
</p>
),
};
export const setTooManyRequestError = (resetTime: string): CustomError => {
return {
status: 429,
title: 'Too Many Requests!',
subTitle: (
<p>
Oh no, you hit the{' '}
<a
href="https://developer.github.com/v3/rate_limit/"
target="_blank"
rel="noopener noreferrer"
className="underline"
>
rate limit
</a>
! Try again later{` ${resetTime}`}.
</p>
),
};
};
export const INVALID_GITHUB_USERNAME_ERROR: CustomError = {
status: 404,
title: 'Invalid GitHub Username!',
subTitle: (
<p>
Please provide correct github username in{' '}
<code>gitprofile.config.ts</code>.
</p>
),
};
export const GENERIC_ERROR: CustomError = {
status: 500,
title: 'Ops!!',
subTitle: 'Something went wrong.',
};

6
src/constants/index.tsx Normal file
View File

@@ -0,0 +1,6 @@
export const LOCAL_STORAGE_KEY_NAME = 'gitprofile-theme';
export const BG_COLOR = 'bg-base-300';
export const FALLBACK_IMAGE =
'data:image/png;base64,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';

View File

@@ -1,249 +0,0 @@
import colors from '../data/colors.json';
import { hotjar } from 'react-hotjar';
export const getInitialTheme = (themeConfig) => {
if (themeConfig.disableSwitch) {
return themeConfig.defaultTheme;
}
if (
typeof window !== 'undefined' &&
!(localStorage.getItem('gitprofile-theme') === null) &&
themeConfig.themes.includes(localStorage.getItem('gitprofile-theme'))
) {
let theme = localStorage.getItem('gitprofile-theme');
return theme;
}
if (themeConfig.respectPrefersColorScheme && !themeConfig.disableSwitch) {
return typeof window !== 'undefined' &&
window.matchMedia('(prefers-color-scheme: dark)').matches
? 'dark'
: themeConfig.defaultTheme;
}
return themeConfig.defaultTheme;
};
export const skeleton = ({
width = null,
height = null,
style = {},
shape = 'rounded-full',
className = null,
}) => {
return (
<div
className={`bg-base-300 animate-pulse ${shape}${
className ? ` ${className}` : ''
}${width ? ` ${width}` : ''}${height ? ` ${height}` : ''}`}
style={style}
/>
);
};
export const languageColor = (language) => {
if (typeof colors[language] !== 'undefined') {
return colors[language].color;
} else {
return 'gray';
}
};
export const fallbackImage =
'data:image/png;base64,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';
export const ga = {
// initialize google analytic
initialize: (id) => {
try {
if (typeof window !== 'undefined') {
window.gtag('js', new Date());
window.gtag('config', id);
}
} catch (error) {
console.error(error);
}
},
// log specific events happening
event: ({ action, params }) => {
try {
window?.gtag('event', action, params);
} catch (error) {
console.error(error);
}
},
};
export const isDarkishTheme = (theme) => {
return ['dark', 'halloween', 'forest', 'black', 'luxury', 'dracula'].includes(
theme
);
};
export const setupHotjar = (hotjarConfig) => {
if (hotjarConfig?.id) {
let snippetVersion = hotjarConfig?.snippetVersion || 6;
hotjar.initialize(hotjarConfig.id, snippetVersion);
}
};
export const sanitizeConfig = (config) => {
const customTheme = config?.themeConfig?.customTheme || {
primary: '#fc055b',
secondary: '#219aaf',
accent: '#e8d03a',
neutral: '#2A2730',
'base-100': '#E3E3ED',
'--rounded-box': '3rem',
'--rounded-btn': '3rem',
};
const themes = config?.themeConfig?.themes || [
'light',
'dark',
'cupcake',
'bumblebee',
'emerald',
'corporate',
'synthwave',
'retro',
'cyberpunk',
'valentine',
'halloween',
'garden',
'forest',
'aqua',
'lofi',
'pastel',
'fantasy',
'wireframe',
'black',
'luxury',
'dracula',
'cmyk',
'autumn',
'business',
'acid',
'lemonade',
'night',
'coffee',
'winter',
'procyon',
];
return {
github: {
username: config?.github?.username || '',
},
projects: {
github: {
header: config?.projects?.github?.header || 'GitHub Projects',
display: config?.projects?.github?.display || true,
sortBy: config?.projects?.github?.sortBy || 'stars',
limit: config?.projects?.github?.limit || 8,
exclude: {
forks: config?.projects?.github?.exclude?.forks || false,
projects: config?.projects?.github?.exclude?.projects || [],
},
},
external: {
header: config?.projects?.external?.header || 'External Projects',
display: config?.projects?.external?.display || true,
projects: config?.projects?.external?.projects || [],
},
},
social: {
linkedin: config?.social?.linkedin,
twitter: config?.social?.twitter,
mastodon: config?.social?.mastodon,
facebook: config?.social?.facebook,
instagram: config?.social?.instagram,
youtube: config?.social?.youtube,
dribbble: config?.social?.dribbble,
behance: config?.social?.behance,
medium: config?.social?.medium,
dev: config?.social?.dev,
stackoverflow: config?.social?.stackoverflow,
website: config?.social?.website,
phone: config?.social?.phone,
email: config?.social?.email,
skype: config?.social?.skype,
telegram: config?.social?.telegram,
},
resume: {
fileUrl: config?.resume?.fileUrl || '',
},
skills: config?.skills || [],
experiences: config?.experiences || [],
certifications: config?.certifications || [],
education: config?.education || [],
blog: {
source: config?.blog?.source,
username: config?.blog?.username,
limit: config?.blog?.limit || 5,
},
googleAnalytics: {
id: config?.googleAnalytics?.id,
},
hotjar: {
id: config?.hotjar?.id,
snippetVersion: config?.hotjar?.snippetVersion || 6,
},
themeConfig: {
defaultTheme: config?.themeConfig?.defaultTheme || themes[0],
disableSwitch: config?.themeConfig?.disableSwitch || false,
respectPrefersColorScheme:
config?.themeConfig?.respectPrefersColorScheme || false,
hideAvatarRing: config?.themeConfig?.hideAvatarRing || false,
themes: themes,
customTheme: customTheme,
},
footer: config?.footer,
};
};
export const noConfigError = {
status: 500,
title: 'No Config is provided.',
subTitle: 'Pass the required config as prop.',
};
export const tooManyRequestError = (reset) => {
return {
status: 429,
title: 'Too Many Requests.',
subTitle: (
<p>
Oh no, you hit the{' '}
<a
href="https://developer.github.com/v3/rate_limit/"
target="_blank"
rel="noopener noreferrer"
>
rate limit
</a>
! Try again later{` ${reset}`}.
</p>
),
};
};
export const notFoundError = {
status: 404,
title: 'The Github Username is Incorrect.',
subTitle: (
<p>
Please provide correct github username in{' '}
<code>gitprofile.config.js</code>.
</p>
),
};
export const genericError = {
status: 500,
title: 'Ops!!',
subTitle: 'Something went wrong.',
};

View File

@@ -0,0 +1,8 @@
export interface Article {
title: string;
thumbnail: string;
link: string;
publishedAt: Date;
description: string;
categories: string[];
}

View File

@@ -0,0 +1,8 @@
export interface GithubProject {
name: string;
html_url: string;
description: string;
stargazers_count: string;
forks_count: string;
language: string;
}

View File

@@ -0,0 +1,7 @@
export interface Profile {
avatar: string;
name: string;
bio?: string;
location?: string;
company?: string;
}

View File

@@ -0,0 +1,141 @@
export interface SanitizedGithub {
username: string;
}
export interface SanitizedGitHubProjects {
display: boolean;
header: string;
mode: string;
automatic: {
sortBy: string;
limit: number;
exclude: {
forks: boolean;
projects: Array<string>;
};
};
manual: {
projects: Array<string>;
};
}
export interface SanitizedExternalProject {
title: string;
description?: string;
imageUrl?: string;
link: string;
}
export interface SanitizedExternalProjects {
header: string;
projects: SanitizedExternalProject[];
}
export interface SanitizedProjects {
github: SanitizedGitHubProjects;
external: SanitizedExternalProjects;
}
export interface SanitizedSEO {
title?: string;
description?: string;
imageURL?: string;
}
export interface SanitizedSocial {
linkedin?: string;
twitter?: string;
mastodon?: string;
facebook?: string;
instagram?: string;
youtube?: string;
dribbble?: string;
behance?: string;
medium?: string;
dev?: string;
stackoverflow?: string;
website?: string;
skype?: string;
telegram?: string;
phone?: string;
email?: string;
}
export interface SanitizedResume {
fileUrl?: string;
}
export interface SanitizedExperience {
company?: string;
position?: string;
from: string;
to: string;
companyLink?: string;
}
export interface SanitizedCertification {
body?: string;
name?: string;
year?: string;
link?: string;
}
export interface SanitizedEducation {
institution?: string;
degree?: string;
from: string;
to: string;
}
export interface SanitizedGoogleAnalytics {
id?: string;
}
export interface SanitizedHotjar {
id?: string;
snippetVersion: number;
}
export interface SanitizedBlog {
display: boolean;
source: string;
username: string;
limit: number;
}
export interface SanitizedCustomTheme {
primary: string;
secondary: string;
accent: string;
neutral: string;
'base-100': string;
'--rounded-box': string;
'--rounded-btn': string;
}
export interface SanitizedThemeConfig {
defaultTheme: string;
disableSwitch: boolean;
respectPrefersColorScheme: boolean;
displayAvatarRing: boolean;
themes: Array<string>;
customTheme: SanitizedCustomTheme;
}
export interface SanitizedConfig {
github: SanitizedGithub;
projects: SanitizedProjects;
seo: SanitizedSEO;
social: SanitizedSocial;
resume: SanitizedResume;
skills: Array<string>;
experiences: Array<SanitizedExperience>;
educations: Array<SanitizedEducation>;
certifications: Array<SanitizedCertification>;
googleAnalytics: SanitizedGoogleAnalytics;
hotjar: SanitizedHotjar;
blog: SanitizedBlog;
themeConfig: SanitizedThemeConfig;
footer?: string;
enablePWA: boolean;
}

View File

@@ -1,9 +0,0 @@
import React from 'react';
import ReactDOM from 'react-dom/client';
import App from './App';
ReactDOM.createRoot(document.getElementById('root')).render(
<React.StrictMode>
<App />
</React.StrictMode>
);

9
src/main.tsx Normal file
View File

@@ -0,0 +1,9 @@
import React from 'react';
import ReactDOM from 'react-dom/client';
import GitProfile from './components/gitprofile.tsx';
ReactDOM.createRoot(document.getElementById('root')!).render(
<React.StrictMode>
<GitProfile config={CONFIG} />
</React.StrictMode>,
);

218
src/utils/index.tsx Normal file
View File

@@ -0,0 +1,218 @@
import { LOCAL_STORAGE_KEY_NAME } from '../constants';
import { DEFAULT_CUSTOM_THEME } from '../constants/default-custom-theme';
import { DEFAULT_THEMES } from '../constants/default-themes';
import {
SanitizedConfig,
SanitizedHotjar,
SanitizedThemeConfig,
} from '../interfaces/sanitized-config';
import { hotjar } from 'react-hotjar';
import colors from '../data/colors.json';
export const isDarkishTheme = (appliedTheme: string): boolean => {
return ['dark', 'halloween', 'forest', 'black', 'luxury', 'dracula'].includes(
appliedTheme,
);
};
type EventParams = {
[key: string]: string;
};
type Colors = {
[key: string]: { color: string | null; url: string };
};
export const getSanitizedConfig = (
config: Config,
): SanitizedConfig | Record<string, never> => {
try {
return {
github: {
username: config.github.username,
},
projects: {
github: {
display: config?.projects?.github?.display ?? true,
header: config?.projects?.github?.header || 'Github Projects',
mode: config?.projects?.github?.mode || 'automatic',
automatic: {
sortBy: config?.projects?.github?.automatic?.sortBy || 'stars',
limit: config?.projects?.github?.automatic?.limit || 8,
exclude: {
forks:
config?.projects?.github?.automatic?.exclude?.forks || false,
projects:
config?.projects?.github?.automatic?.exclude?.projects || [],
},
},
manual: {
projects: config?.projects?.github?.manual?.projects || [],
},
},
external: {
header: config?.projects?.external?.header || 'My Projects',
projects: config?.projects?.external?.projects || [],
},
},
seo: {
title: config?.seo?.title,
description: config?.seo?.description,
imageURL: config?.seo?.imageURL,
},
social: {
linkedin: config?.social?.linkedin,
twitter: config?.social?.twitter,
mastodon: config?.social?.mastodon,
facebook: config?.social?.facebook,
instagram: config?.social?.instagram,
youtube: config?.social?.youtube,
dribbble: config?.social?.dribbble,
behance: config?.social?.behance,
medium: config?.social?.medium,
dev: config?.social?.dev,
stackoverflow: config?.social?.stackoverflow,
website: config?.social?.website,
phone: config?.social?.phone,
email: config?.social?.email,
skype: config?.social?.skype,
telegram: config?.social?.telegram,
},
resume: {
fileUrl: config?.resume?.fileUrl || '',
},
skills: config?.skills || [],
experiences: config?.experiences || [],
certifications: config?.certifications || [],
educations: config?.educations || [],
googleAnalytics: {
id: config?.googleAnalytics?.id,
},
hotjar: {
id: config?.hotjar?.id,
snippetVersion: config?.hotjar?.snippetVersion || 6,
},
blog: {
username: config?.blog?.username || '',
source: config?.blog?.source || 'dev',
limit: config?.blog?.limit || 5,
display: !!config?.blog?.username && !!config?.blog?.source,
},
themeConfig: {
defaultTheme: config?.themeConfig?.defaultTheme || DEFAULT_THEMES[0],
disableSwitch: config?.themeConfig?.disableSwitch || false,
respectPrefersColorScheme:
config?.themeConfig?.respectPrefersColorScheme || false,
displayAvatarRing: config?.themeConfig?.displayAvatarRing ?? true,
themes: config?.themeConfig?.themes || DEFAULT_THEMES,
customTheme: {
primary:
config?.themeConfig?.customTheme?.primary ||
DEFAULT_CUSTOM_THEME.primary,
secondary:
config?.themeConfig?.customTheme?.secondary ||
DEFAULT_CUSTOM_THEME.secondary,
accent:
config?.themeConfig?.customTheme?.accent ||
DEFAULT_CUSTOM_THEME.accent,
neutral:
config?.themeConfig?.customTheme?.neutral ||
DEFAULT_CUSTOM_THEME.neutral,
'base-100':
config?.themeConfig?.customTheme?.['base-100'] ||
DEFAULT_CUSTOM_THEME['base-100'],
'--rounded-box':
config?.themeConfig?.customTheme?.['--rounded-box'] ||
DEFAULT_CUSTOM_THEME['--rounded-box'],
'--rounded-btn':
config?.themeConfig?.customTheme?.['--rounded-btn'] ||
DEFAULT_CUSTOM_THEME['--rounded-btn'],
},
},
footer: config?.footer,
enablePWA: config?.enablePWA ?? true,
};
} catch (error) {
return {};
}
};
export const getInitialTheme = (themeConfig: SanitizedThemeConfig): string => {
if (themeConfig.disableSwitch) {
return themeConfig.defaultTheme;
}
if (
typeof window !== 'undefined' &&
!(localStorage.getItem(LOCAL_STORAGE_KEY_NAME) === null)
) {
const savedTheme = localStorage.getItem(LOCAL_STORAGE_KEY_NAME);
if (savedTheme && themeConfig.themes.includes(savedTheme)) {
return savedTheme;
}
}
if (themeConfig.respectPrefersColorScheme && !themeConfig.disableSwitch) {
return typeof window !== 'undefined' &&
window.matchMedia('(prefers-color-scheme: dark)').matches
? 'dark'
: themeConfig.defaultTheme;
}
return themeConfig.defaultTheme;
};
export const skeleton = ({
widthCls = null,
heightCls = null,
style = {} as React.CSSProperties,
shape = 'rounded-full',
className = null,
}: {
widthCls?: string | null;
heightCls?: string | null;
style?: React.CSSProperties;
shape?: string;
className?: string | null;
}): JSX.Element => {
const classNames = ['bg-base-300', 'animate-pulse', shape];
if (className) {
classNames.push(className);
}
if (widthCls) {
classNames.push(widthCls);
}
if (heightCls) {
classNames.push(heightCls);
}
return <div className={classNames.join(' ')} style={style} />;
};
export const setupHotjar = (hotjarConfig: SanitizedHotjar): void => {
if (hotjarConfig?.id) {
const snippetVersion = hotjarConfig?.snippetVersion || 6;
hotjar.initialize(parseInt(hotjarConfig.id), snippetVersion);
}
};
export const ga = {
event(action: string, params: EventParams): void {
try {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(window as any)?.gtag('event', action, params);
} catch (error) {
console.error(error);
}
},
};
export const getLanguageColor = (language: string): string => {
const languageColors: Colors = colors;
if (typeof languageColors[language] !== 'undefined') {
return languageColors[language].color || 'gray';
} else {
return 'gray';
}
};

1
src/vite-env.d.ts vendored Normal file
View File

@@ -0,0 +1 @@
/// <reference types="vite/client" />

17
tailwind.config.js Normal file
View File

@@ -0,0 +1,17 @@
import CONFIG from './gitprofile.config';
/** @type {import('tailwindcss').Config} */
export default {
content: ['./index.html', './src/**/*.{js,ts,jsx,tsx}'],
theme: {
extend: {},
},
plugins: [require('daisyui')],
daisyui: {
logs: false,
themes: [
...CONFIG.themeConfig.themes,
{ procyon: CONFIG.themeConfig.customTheme },
],
},
};

View File

@@ -1,16 +0,0 @@
import config from './gitprofile.config';
export default {
content: ['./index.html', './src/**/*.{vue,js,ts,jsx,tsx}'],
theme: {
extend: {},
},
plugins: [require('daisyui')],
daisyui: {
logs: false,
themes: [
...config.themeConfig.themes,
{ procyon: config.themeConfig.customTheme },
],
},
};

25
tsconfig.json Normal file
View File

@@ -0,0 +1,25 @@
{
"compilerOptions": {
"target": "ES2020",
"useDefineForClassFields": true,
"lib": ["ES2020", "DOM", "DOM.Iterable"],
"module": "ESNext",
"skipLibCheck": true,
/* Bundler mode */
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,
"jsx": "react-jsx",
/* Linting */
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true,
},
"include": ["src", "global.d.ts"],
"references": [{ "path": "./tsconfig.node.json" }],
}

10
tsconfig.node.json Normal file
View File

@@ -0,0 +1,10 @@
{
"compilerOptions": {
"composite": true,
"skipLibCheck": true,
"module": "ESNext",
"moduleResolution": "bundler",
"allowSyntheticDefaultImports": true
},
"include": ["gitprofile.config.ts", "vite.config.ts"]
}

View File

@@ -1,42 +0,0 @@
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
import tailwind from 'tailwindcss';
import autoprefixer from 'autoprefixer';
import tailwindConfig from './tailwind.config.mjs';
import { VitePWA } from 'vite-plugin-pwa';
// https://vitejs.dev/config/
export default defineConfig({
// If you are deploying to https://<USERNAME>.github.io/, for example your repository is at https://github.com/<USERNAME>, set base to '/'.
// If you are deploying to https://<USERNAME>.github.io/<REPO_NAME>/, for example your repository is at https://github.com/<USERNAME>/<REPO_NAME>, then set base to '/<REPO_NAME>/'.
base: '/gitprofile/',
plugins: [
react(),
VitePWA({
registerType: 'autoUpdate',
workbox: {
navigateFallback: undefined,
},
includeAssets: ['logo.png'],
manifest: {
name: 'Portfolio',
short_name: 'Portfolio',
description: 'Personal Portfolio',
icons: [
{
src: 'logo.png',
sizes: '64x64 32x32 24x24 16x16 192x192 512x512',
type: 'image/png',
},
],
},
}),
],
css: {
postcss: {
plugins: [tailwind(tailwindConfig), autoprefixer],
},
},
});

48
vite.config.ts Normal file
View File

@@ -0,0 +1,48 @@
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
import { VitePWA } from 'vite-plugin-pwa';
import CONFIG from './gitprofile.config';
import { createHtmlPlugin } from 'vite-plugin-html';
// https://vitejs.dev/config/
export default defineConfig({
base: CONFIG.base || '/',
plugins: [
react(),
createHtmlPlugin({
inject: {
data: {
metaTitle: CONFIG.seo.title,
metaDescription: CONFIG.seo.description,
metaImageURL: CONFIG.seo.imageURL,
},
},
}),
...(CONFIG.enablePWA
? [
VitePWA({
registerType: 'autoUpdate',
workbox: {
navigateFallback: undefined,
},
includeAssets: ['logo.png'],
manifest: {
name: 'Portfolio',
short_name: 'Portfolio',
description: 'Personal Portfolio',
icons: [
{
src: 'logo.png',
sizes: '64x64 32x32 24x24 16x16 192x192 512x512',
type: 'image/png',
},
],
},
}),
]
: []),
],
define: {
CONFIG: CONFIG,
},
});