81 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
Ariful Alam
cdd8362156 Merge pull request #427 from arifszn/dependabot/npm_and_yarn/postcss-8.4.27
Bump postcss from 8.4.26 to 8.4.27
2023-07-26 21:27:35 +06:00
dependabot[bot]
d3c3881782 Bump postcss from 8.4.26 to 8.4.27
Bumps [postcss](https://github.com/postcss/postcss) from 8.4.26 to 8.4.27.
- [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.26...8.4.27)

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

Signed-off-by: dependabot[bot] <support@github.com>
2023-07-24 15:22:54 +00:00
Ariful Alam
f47963ad43 Merge pull request #428 from arifszn/dependabot/npm_and_yarn/eslint-plugin-react-7.33.0
Bump eslint-plugin-react from 7.32.2 to 7.33.0
2023-07-24 21:21:54 +06:00
Ariful Alam
e3200cd3b7 Merge pull request #429 from arifszn/dependabot/npm_and_yarn/tailwindcss-3.3.3
Bump tailwindcss from 3.3.2 to 3.3.3
2023-07-24 21:21:46 +06:00
Ariful Alam
0838994060 Merge pull request #430 from arifszn/dependabot/npm_and_yarn/eslint-8.45.0
Bump eslint from 8.44.0 to 8.45.0
2023-07-24 21:21:37 +06:00
dependabot[bot]
f71f205054 Bump eslint from 8.44.0 to 8.45.0
Bumps [eslint](https://github.com/eslint/eslint) from 8.44.0 to 8.45.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.44.0...v8.45.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-07-24 08:51:42 +00:00
dependabot[bot]
3dd80aeb26 Bump tailwindcss from 3.3.2 to 3.3.3
Bumps [tailwindcss](https://github.com/tailwindlabs/tailwindcss) from 3.3.2 to 3.3.3.
- [Release notes](https://github.com/tailwindlabs/tailwindcss/releases)
- [Changelog](https://github.com/tailwindlabs/tailwindcss/blob/v3.3.3/CHANGELOG.md)
- [Commits](https://github.com/tailwindlabs/tailwindcss/compare/v3.3.2...v3.3.3)

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

Signed-off-by: dependabot[bot] <support@github.com>
2023-07-24 08:51:29 +00:00
dependabot[bot]
6750afe52f Bump eslint-plugin-react from 7.32.2 to 7.33.0
Bumps [eslint-plugin-react](https://github.com/jsx-eslint/eslint-plugin-react) from 7.32.2 to 7.33.0.
- [Release notes](https://github.com/jsx-eslint/eslint-plugin-react/releases)
- [Changelog](https://github.com/jsx-eslint/eslint-plugin-react/blob/master/CHANGELOG.md)
- [Commits](https://github.com/jsx-eslint/eslint-plugin-react/compare/v7.32.2...v7.33.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
2023-07-24 08:51:17 +00:00
Ariful Alam
3dae9bb7ec Merge pull request #420 from arifszn/dependabot/npm_and_yarn/vitejs/plugin-react-4.0.3
Bump @vitejs/plugin-react from 4.0.1 to 4.0.3
2023-07-23 09:37:44 +06:00
Ariful Alam
1608c0ff7b Merge pull request #422 from arifszn/dependabot/npm_and_yarn/arifszn/blog-js-2.0.5
Bump @arifszn/blog-js from 2.0.4 to 2.0.5
2023-07-18 10:27:44 +06:00
Ariful Alam
838ae6a5b9 Merge pull request #424 from arifszn/dependabot/npm_and_yarn/daisyui-3.2.1
Bump daisyui from 3.1.7 to 3.2.1
2023-07-18 10:27:32 +06:00
Ariful Alam
90b94304d7 Merge pull request #426 from arifszn/dependabot/npm_and_yarn/vite-4.4.4
Bump vite from 4.3.9 to 4.4.4
2023-07-18 10:27:19 +06:00
dependabot[bot]
12a367c25f Bump vite from 4.3.9 to 4.4.4
Bumps [vite](https://github.com/vitejs/vite/tree/HEAD/packages/vite) from 4.3.9 to 4.4.4.
- [Release notes](https://github.com/vitejs/vite/releases)
- [Changelog](https://github.com/vitejs/vite/blob/main/packages/vite/CHANGELOG.md)
- [Commits](https://github.com/vitejs/vite/commits/v4.4.4/packages/vite)

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

Signed-off-by: dependabot[bot] <support@github.com>
2023-07-17 08:16:54 +00:00
dependabot[bot]
a4f63fbc98 Bump daisyui from 3.1.7 to 3.2.1
Bumps [daisyui](https://github.com/saadeghi/daisyui) from 3.1.7 to 3.2.1.
- [Changelog](https://github.com/saadeghi/daisyui/blob/master/CHANGELOG.md)
- [Commits](https://github.com/saadeghi/daisyui/compare/v3.1.7...v3.2.1)

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

Signed-off-by: dependabot[bot] <support@github.com>
2023-07-10 08:57:47 +00:00
dependabot[bot]
9813f0853f Bump @arifszn/blog-js from 2.0.4 to 2.0.5
Bumps [@arifszn/blog-js](https://github.com/arifszn/blog.js) from 2.0.4 to 2.0.5.
- [Release notes](https://github.com/arifszn/blog.js/releases)
- [Commits](https://github.com/arifszn/blog.js/compare/v2.0.4...v2.0.5)

---
updated-dependencies:
- dependency-name: "@arifszn/blog-js"
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-07-10 08:57:24 +00:00
dependabot[bot]
8d7fc38f45 Bump @vitejs/plugin-react from 4.0.1 to 4.0.3
Bumps [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/tree/HEAD/packages/plugin-react) from 4.0.1 to 4.0.3.
- [Release notes](https://github.com/vitejs/vite-plugin-react/releases)
- [Changelog](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react/CHANGELOG.md)
- [Commits](https://github.com/vitejs/vite-plugin-react/commits/v4.0.3/packages/plugin-react)

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

Signed-off-by: dependabot[bot] <support@github.com>
2023-07-10 08:57:08 +00:00
Ariful Alam
731301d857 Merge pull request #413 from arifszn/dependabot/npm_and_yarn/eslint-8.44.0
Bump eslint from 8.43.0 to 8.44.0
2023-07-08 13:32:44 +06:00
Ariful Alam
4ebf2b9742 Merge pull request #418 from arifszn/arifszn-patch-1
Add last commit badge
2023-07-03 16:04:03 +06:00
Ariful Alam
cb0ff53588 Add last commit badge 2023-07-03 16:02:26 +06:00
Ariful Alam
d14c8fb1a7 Merge pull request #417 from arifszn/2.6.0
Bump version to 2.6.0 🚀
2023-07-03 15:43:23 +06:00
Ariful Alam
3cbba324b1 Bump version to 2.6.0 🚀 2023-07-03 15:41:53 +06:00
Ariful Alam
7e714b6a8d Merge pull request #416 from arifszn/social-youtube
Add YouTube as social link
2023-07-03 15:40:31 +06:00
Ariful Alam
f482c2e7b6 Add YouTube as social link 2023-07-03 15:38:04 +06:00
Ariful Alam
205a547439 Merge pull request #414 from arifszn/dependabot/npm_and_yarn/daisyui-3.1.7
Bump daisyui from 3.1.6 to 3.1.7
2023-07-03 11:57:59 +06:00
Ariful Alam
ae5345b33b Merge pull request #415 from arifszn/arifszn-patch-1
Update README.md
2023-07-03 11:56:57 +06:00
dependabot[bot]
d5317bf0c7 Bump daisyui from 3.1.6 to 3.1.7
Bumps [daisyui](https://github.com/saadeghi/daisyui) from 3.1.6 to 3.1.7.
- [Changelog](https://github.com/saadeghi/daisyui/blob/master/CHANGELOG.md)
- [Commits](https://github.com/saadeghi/daisyui/compare/v3.1.6...v3.1.7)

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

Signed-off-by: dependabot[bot] <support@github.com>
2023-07-03 05:56:35 +00:00
dependabot[bot]
31bf2f27d2 Bump eslint from 8.43.0 to 8.44.0
Bumps [eslint](https://github.com/eslint/eslint) from 8.43.0 to 8.44.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.43.0...v8.44.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-07-03 05:56:28 +00:00
Ariful Alam
30f8d05014 Update README.md 2023-07-03 11:52:50 +06:00
Ariful Alam
365f3f25ea Merge pull request #412 from arifszn/arifszn-patch-1
Update deployment guide
2023-07-03 11:50:45 +06:00
Ariful Alam
9abe133b1c Update README.md 2023-07-03 11:50:16 +06:00
Ariful Alam
ac8e6941ab Merge pull request #411 from arifszn/fix/409
Set undefined navigateFallback so that it can redirect to 404
2023-06-28 17:24:44 +06:00
Ariful Alam
b4bb68ae06 Set undefined navigateFallback so that it can redirect to 404 2023-06-28 17:22:56 +06:00
Ariful Alam
1bd77a785f Merge pull request #410 from arifszn/deps
Update dependencies
2023-06-28 17:08:24 +06:00
Ariful Alam
9ae2d382ad Update dependencies 2023-06-28 17:00:11 +06:00
Ariful Alam
06a428a96f Merge pull request #407 from arifszn/dependabot/npm_and_yarn/vitejs/plugin-react-4.0.1
Bump @vitejs/plugin-react from 4.0.0 to 4.0.1
2023-06-26 20:06:38 +06:00
Ariful Alam
8c2224047e Merge pull request #406 from arifszn/dependabot/npm_and_yarn/daisyui-3.1.6
Bump daisyui from 3.1.1 to 3.1.6
2023-06-26 13:48:45 +06:00
dependabot[bot]
d0f67b6c29 Bump @vitejs/plugin-react from 4.0.0 to 4.0.1
Bumps [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/tree/HEAD/packages/plugin-react) from 4.0.0 to 4.0.1.
- [Release notes](https://github.com/vitejs/vite-plugin-react/releases)
- [Changelog](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react/CHANGELOG.md)
- [Commits](https://github.com/vitejs/vite-plugin-react/commits/plugin-react@4.0.1/packages/plugin-react)

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

Signed-off-by: dependabot[bot] <support@github.com>
2023-06-26 05:01:42 +00:00
dependabot[bot]
788640358f Bump daisyui from 3.1.1 to 3.1.6
Bumps [daisyui](https://github.com/saadeghi/daisyui) from 3.1.1 to 3.1.6.
- [Changelog](https://github.com/saadeghi/daisyui/blob/master/CHANGELOG.md)
- [Commits](https://github.com/saadeghi/daisyui/compare/v3.1.1...v3.1.6)

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

Signed-off-by: dependabot[bot] <support@github.com>
2023-06-26 05:01:23 +00:00
Ariful Alam
7ceb0a12e9 Merge pull request #404 from arifszn/dependabot/npm_and_yarn/eslint-8.43.0
Bump eslint from 8.42.0 to 8.43.0
2023-06-21 20:35:09 +06:00
Ariful Alam
e58fb509b0 Merge pull request #405 from arifszn/dependabot/npm_and_yarn/daisyui-3.1.1
Bump daisyui from 3.1.0 to 3.1.1
2023-06-21 20:34:50 +06:00
dependabot[bot]
3dc1b74042 Bump daisyui from 3.1.0 to 3.1.1
Bumps [daisyui](https://github.com/saadeghi/daisyui) from 3.1.0 to 3.1.1.
- [Changelog](https://github.com/saadeghi/daisyui/blob/master/CHANGELOG.md)
- [Commits](https://github.com/saadeghi/daisyui/compare/v3.1.0...v3.1.1)

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

Signed-off-by: dependabot[bot] <support@github.com>
2023-06-19 08:59:12 +00:00
dependabot[bot]
92115ca1b6 Bump eslint from 8.42.0 to 8.43.0
Bumps [eslint](https://github.com/eslint/eslint) from 8.42.0 to 8.43.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.42.0...v8.43.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-06-19 08:59:02 +00:00
Ariful Alam
a873ab7a8f Merge pull request #403 from arifszn/readme-image
Fix broken images
2023-06-18 18:24:15 +06:00
Ariful Alam
8b798f8dea Fix broken images 2023-06-18 18:18:44 +06:00
Ariful Alam
a91de444a2 Merge pull request #398 from arifszn/dependabot/npm_and_yarn/vite-plugin-pwa-0.16.4
Bump vite-plugin-pwa from 0.16.3 to 0.16.4
2023-06-16 00:02:21 +06:00
Ariful Alam
98d550de69 Merge pull request #399 from arifszn/dependabot/npm_and_yarn/daisyui-3.1.0
Bump daisyui from 3.0.20 to 3.1.0
2023-06-12 21:48:39 +06:00
dependabot[bot]
80954fce9f Bump daisyui from 3.0.20 to 3.1.0
Bumps [daisyui](https://github.com/saadeghi/daisyui) from 3.0.20 to 3.1.0.
- [Changelog](https://github.com/saadeghi/daisyui/blob/master/CHANGELOG.md)
- [Commits](https://github.com/saadeghi/daisyui/compare/v3.0.20...v3.1.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
2023-06-12 08:59:03 +00:00
dependabot[bot]
2a4a10d032 Bump vite-plugin-pwa from 0.16.3 to 0.16.4
Bumps [vite-plugin-pwa](https://github.com/antfu/vite-plugin-pwa) from 0.16.3 to 0.16.4.
- [Release notes](https://github.com/antfu/vite-plugin-pwa/releases)
- [Commits](https://github.com/antfu/vite-plugin-pwa/compare/v0.16.3...v0.16.4)

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

Signed-off-by: dependabot[bot] <support@github.com>
2023-06-12 08:58:51 +00:00
Ariful Alam
80a7f5fd1c Merge pull request #397 from arifszn/theme
Update default theme
2023-06-09 15:31:57 +06:00
Ariful Alam
0f36c6f33c Update default theme 2023-06-09 15:30:39 +06:00
59 changed files with 4969 additions and 3162 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

397
README.md
View File

@@ -21,8 +21,8 @@
<a href="https://github.com/arifszn/gitprofile/network/members"> <a href="https://github.com/arifszn/gitprofile/network/members">
<img src="https://img.shields.io/github/forks/arifszn/gitprofile"/> <img src="https://img.shields.io/github/forks/arifszn/gitprofile"/>
</a> </a>
<a href="https://github.com/arifszn/gitprofile/blob/main/package-lock.json"> <a href="https://github.com/arifszn/gitprofile/commits/main">
<img src="https://img.shields.io/snyk/vulnerabilities/github/arifszn/gitprofile"/> <img src="https://img.shields.io/github/last-commit/arifszn/gitprofile/main"/>
</a> </a>
<a href="https://github.com/arifszn/gitprofile/blob/main/CONTRIBUTING.md"> <a href="https://github.com/arifszn/gitprofile/blob/main/CONTRIBUTING.md">
<img src="https://img.shields.io/badge/contributions-welcome-brightgreen.svg?style=flat"/> <img src="https://img.shields.io/badge/contributions-welcome-brightgreen.svg?style=flat"/>
@@ -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.github.io/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.github.io/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.github.io/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,11 +83,10 @@ 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
These instructions will get you a copy of the project and deploy your portfolio online! These instructions will get you a copy of the project and deploy your portfolio online using GitHub Pages!
- **Fork repo:** Click [here](https://github.com/arifszn/gitprofile/fork) to fork the repo so you have your own project to customize. A "fork" is a copy of a repository. - **Fork repo:** Click [here](https://github.com/arifszn/gitprofile/fork) to fork the repo so you have your own project to customize. A "fork" is a copy of a repository.
- **Rename repo:** - **Rename 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.github.io/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,7 +112,7 @@ 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**.
@@ -155,85 +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: '', // 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',
@@ -252,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',
@@ -272,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
@@ -304,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: [
@@ -348,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',
@@ -364,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',
@@ -386,14 +388,14 @@ const config = {
``` ```
<p align="center"> <p align="center">
<img src="https://arifszn.github.io/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: {
@@ -414,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: '',
@@ -430,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: '',
@@ -443,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)
@@ -457,11 +471,11 @@ Your avatar and bio will be fetched from GitHub automatically.
### Social Links ### Social Links
You can link your social media services you're using, including LinkedIn, Twitter, Mastodon, Facebook, Instagram, 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',
@@ -469,6 +483,7 @@ const config = {
mastodon: 'arifszn@mastodon.social', mastodon: 'arifszn@mastodon.social',
facebook: '', facebook: '',
instagram: '', instagram: '',
youtube: '',
dribbble: '', dribbble: '',
behance: '', behance: '',
medium: '', medium: '',
@@ -487,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'],
}; };
@@ -501,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: [
{ {
@@ -528,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',
@@ -557,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: [
{ {
@@ -578,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']
},
}, },
}, },
}; };
@@ -598,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',
},
],
}, },
], },
}; };
``` ```
@@ -619,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',
@@ -631,7 +674,7 @@ const config = {
}; };
``` ```
![Blog](https://arifszn.github.io/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,14 +1,60 @@
// 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: 10, // 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: 'ariful-alam', linkedin: 'ariful-alam',
@@ -16,14 +62,15 @@ const config = {
mastodon: 'arifszn@mastodon.social', mastodon: 'arifszn@mastodon.social',
facebook: '', facebook: '',
instagram: '', instagram: '',
youtube: '', // example: 'pewdiepie'
dribbble: '', dribbble: '',
behance: '', behance: '',
medium: 'arifszn', medium: 'arifszn',
dev: 'arifszn', dev: 'arifszn',
stackoverflow: '', // format: userid/username 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',
}, },
@@ -63,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',
@@ -85,29 +132,11 @@ const config = {
to: '2014', to: '2014',
}, },
], ],
// Display articles from your medium or dev account. (Optional)
// To hide the `My Projects` section, keep it empty.
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: 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
@@ -118,7 +147,7 @@ const config = {
snippetVersion: 6, snippetVersion: 6,
}, },
themeConfig: { themeConfig: {
defaultTheme: 'winter', 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
@@ -128,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: [
@@ -162,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',
@@ -183,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,36 +1,64 @@
// 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;
}
interface GitHubProjects {
/**
* Display GitHub projects?
*/
display?: boolean;
/** /**
* stars | updated * Header
*/ */
sortBy?: string; header?: string;
/** /**
* How many projects to display * 'automatic' | 'manual'
*/ */
limit?: number; mode?: string;
/** /**
* Exclude projects option * Config of automatic mode
*/ */
exclude?: { automatic?: {
/** /**
* Forked projects will not be displayed if set to true * 'stars' | 'updated'
*/ */
forks?: boolean; sortBy?: string;
/** /**
* These projects will not be displayed * How many projects to display
*/
limit?: number;
/**
* Exclude projects option
*/
exclude?: {
/**
* Forked projects will not be displayed if set to true
*/
forks?: boolean;
/**
* These projects will not be displayed
*
* example: ['my-project1', 'my-project2']
*/
projects?: Array<string>;
};
};
/**
* Config of manual mode
*/
manual?: {
/**
* These projects will be displayed
* *
* example: ['my-project1', 'my-project2'] * example: ['my-project1', 'my-project2']
*/ */
@@ -38,7 +66,47 @@ export interface Github {
}; };
} }
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
*/ */
@@ -64,6 +132,11 @@ export interface Social {
*/ */
instagram?: string; instagram?: string;
/**
* YouTube
*/
youtube?: string;
/** /**
* Dribbble * Dribbble
*/ */
@@ -115,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
*/ */
@@ -153,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
*/ */
@@ -190,7 +292,7 @@ export interface CustomTheme {
'--rounded-btn'?: string; '--rounded-btn'?: string;
} }
export interface ThemeConfig { interface ThemeConfig {
/** /**
* Default theme * Default theme
*/ */
@@ -209,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
@@ -222,54 +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;
/**
* Vite's base url
*/
base?: string;
/**
* Projects config
*/
projects?: Projects;
/**
* SEO config
*/
seo?: SEO;
/** /**
* Social links * Social links
*/ */
social?: Social; social?: Social;
/**
* Resume
*/
resume?: Resume;
/** /**
* Skill list * Skill list
*/ */
@@ -280,25 +360,20 @@ export interface Config {
*/ */
experiences?: Array<Experience>; experiences?: Array<Experience>;
/**
* External Projects
*/
externalProjects?: Array<ExternalProjects>;
/** /**
* 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
@@ -310,6 +385,11 @@ export interface Config {
*/ */
hotjar?: Hotjar; hotjar?: Hotjar;
/**
* Blog config
*/
blog?: Blog;
/** /**
* Theme config * Theme config
*/ */
@@ -319,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',
},
},
},
},
});

4076
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.5.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,331 +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 Project from './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.github.exclude.projects.forEach((project) => {
excludeRepo += `+-repo:${sanitizedConfig.github.username}/${project}`;
});
let query = `user:${
sanitizedConfig.github.username
}+fork:${!sanitizedConfig.github.exclude.forks}${excludeRepo}`;
let url = `https://api.github.com/search/repositories?q=${query}&sort=${sanitizedConfig.github.sortBy}&per_page=${sanitizedConfig.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">
<Project
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,
sortBy: PropTypes.oneOf(['stars', 'updated']),
limit: PropTypes.number,
exclude: PropTypes.shape({
forks: PropTypes.bool,
projects: PropTypes.array,
}),
}).isRequired,
social: PropTypes.shape({
linkedin: PropTypes.string,
twitter: PropTypes.string,
mastodon: PropTypes.string,
facebook: PropTypes.string,
instagram: 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,
externalProjects: PropTypes.arrayOf(
PropTypes.shape({
title: PropTypes.string.isRequired,
description: PropTypes.string.isRequired,
link: PropTypes.string.isRequired,
imageUrl: PropTypes.string,
})
),
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

@@ -19,19 +19,34 @@ import {
FaStackOverflow, FaStackOverflow,
FaTelegram, FaTelegram,
FaLinkedin, FaLinkedin,
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) {
@@ -41,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}
@@ -66,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' })}
/> />,
); );
} }
@@ -107,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
} }
/> />
)} )}
@@ -173,6 +203,14 @@ const Details = ({ profile, loading, social, github }) => {
link={`https://www.instagram.com/${social.instagram}`} link={`https://www.instagram.com/${social.instagram}`}
/> />
)} )}
{social?.youtube && (
<ListItem
icon={<FaYoutube />}
title="YouTube:"
value={`@${social.youtube}`}
link={`https://www.youtube.com/@${social.youtube}`}
/>
)}
{social?.medium && ( {social?.medium && (
<ListItem <ListItem
icon={<AiFillMediumSquare />} icon={<AiFillMediumSquare />}
@@ -201,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 && (
@@ -246,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 Project = ({ 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 Project = ({ 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 Project = ({ 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 Project = ({ 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 Project = ({ 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 Project = ({ 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 Project = ({ 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 Project = ({ 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 Project = ({ repo, loading, github, googleAnalytics }) => {
); );
}; };
Project.propTypes = { export default GithubProjectCard;
repo: PropTypes.array,
loading: PropTypes.bool.isRequired,
github: PropTypes.object.isRequired,
googleAnalytics: PropTypes.object.isRequired,
};
export default Project;

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,238 +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,iVBORw0KGgoAAAANSUhEUgAAAMIAAADDCAYAAADQvc6UAAABRWlDQ1BJQ0MgUHJvZmlsZQAAKJFjYGASSSwoyGFhYGDIzSspCnJ3UoiIjFJgf8LAwSDCIMogwMCcmFxc4BgQ4ANUwgCjUcG3awyMIPqyLsis7PPOq3QdDFcvjV3jOD1boQVTPQrgSkktTgbSf4A4LbmgqISBgTEFyFYuLykAsTuAbJEioKOA7DkgdjqEvQHEToKwj4DVhAQ5A9k3gGyB5IxEoBmML4BsnSQk8XQkNtReEOBxcfXxUQg1Mjc0dyHgXNJBSWpFCYh2zi+oLMpMzyhRcASGUqqCZ16yno6CkYGRAQMDKMwhqj/fAIcloxgHQqxAjIHBEugw5sUIsSQpBobtQPdLciLEVJYzMPBHMDBsayhILEqEO4DxG0txmrERhM29nYGBddr//5/DGRjYNRkY/l7////39v///y4Dmn+LgeHANwDrkl1AuO+pmgAAADhlWElmTU0AKgAAAAgAAYdpAAQAAAABAAAAGgAAAAAAAqACAAQAAAABAAAAwqADAAQAAAABAAAAwwAAAAD9b/HnAAAHlklEQVR4Ae3dP3PTWBSGcbGzM6GCKqlIBRV0dHRJFarQ0eUT8LH4BnRU0NHR0UEFVdIlFRV7TzRksomPY8uykTk/zewQfKw/9znv4yvJynLv4uLiV2dBoDiBf4qP3/ARuCRABEFAoBEgghggQAQZQKAnYEaQBAQaASKIAQJEkAEEegJmBElAoBEgghggQAQZQKAnYEaQBAQaASKIAQJEkAEEegJmBElAoBEgghggQAQZQKAnYEaQBAQaASKIAQJEkAEEegJmBElAoBEgghggQAQZQKAnYEaQBAQaASKIAQJEkAEEegJmBElAoBEgghggQAQZQKAnYEaQBAQaASKIAQJEkAEEegJmBElAoBEgghggQAQZQKAnYEaQBAQaASKIAQJEkAEEegJmBElAoBEgghggQAQZQKAnYEaQBAQaASKIAQJEkAEEegJmBElAoBEgghggQAQZQKAnYEaQBAQaASKIAQJEkAEEegJmBElAoBEgghggQAQZQKAnYEaQBAQaASKIAQJEkAEEegJmBElAoBEgghggQAQZQKAnYEaQBAQaASKIAQJEkAEEegJmBElAoBEgghggQAQZQKAnYEaQBAQaASKIAQJEkAEEegJmBElAoBEgghggQAQZQKAnYEaQBAQaASKIAQJEkAEEegJmBElAoBEgghgg0Aj8i0JO4OzsrPv69Wv+hi2qPHr0qNvf39+iI97soRIh4f3z58/u7du3SXX7Xt7Z2enevHmzfQe+oSN2apSAPj09TSrb+XKI/f379+08+A0cNRE2ANkupk+ACNPvkSPcAAEibACyXUyfABGm3yNHuAECRNgAZLuYPgEirKlHu7u7XdyytGwHAd8jjNyng4OD7vnz51dbPT8/7z58+NB9+/bt6jU/TI+AGWHEnrx48eJ/EsSmHzx40L18+fLyzxF3ZVMjEyDCiEDjMYZZS5wiPXnyZFbJaxMhQIQRGzHvWR7XCyOCXsOmiDAi1HmPMMQjDpbpEiDCiL358eNHurW/5SnWdIBbXiDCiA38/Pnzrce2YyZ4//59F3ePLNMl4PbpiL2J0L979+7yDtHDhw8vtzzvdGnEXdvUigSIsCLAWavHp/+qM0BcXMd/q25n1vF57TYBp0a3mUzilePj4+7k5KSLb6gt6ydAhPUzXnoPR0dHl79WGTNCfBnn1uvSCJdegQhLI1vvCk+fPu2ePXt2tZOYEV6/fn31dz+shwAR1sP1cqvLntbEN9MxA9xcYjsxS1jWR4AIa2Ibzx0tc44fYX/16lV6NDFLXH+YL32jwiACRBiEbf5KcXoTIsQSpzXx4N28Ja4BQoK7rgXiydbHjx/P25TaQAJEGAguWy0+2Q8PD6/Ki4R8EVl+bzBOnZY95fq9rj9zAkTI2SxdidBHqG9+skdw43borCXO/ZcJdraPWdv22uIEiLA4q7nvvCug8WTqzQveOH26fodo7g6uFe/a17W3+nFBAkRYENRdb1vkkz1CH9cPsVy/jrhr27PqMYvENYNlHAIesRiBYwRy0V+8iXP8+/fvX11Mr7L7ECueb/r48eMqm7FuI2BGWDEG8cm+7G3NEOfmdcTQw4h9/55lhm7DekRYKQPZF2ArbXTAyu4kDYB2YxUzwg0gi/41ztHnfQG26HbGel/crVrm7tNY+/1btkOEAZ2M05r4FB7r9GbAIdxaZYrHdOsgJ/wCEQY0J74TmOKnbxxT9n3FgGGWWsVdowHtjt9Nnvf7yQM2aZU/TIAIAxrw6dOnAWtZZcoEnBpNuTuObWMEiLAx1HY0ZQJEmHJ3HNvGCBBhY6jtaMoEiJB0Z29vL6ls58vxPcO8/zfrdo5qvKO+d3Fx8Wu8zf1dW4p/cPzLly/dtv9Ts/EbcvGAHhHyfBIhZ6NSiIBTo0LNNtScABFyNiqFCBChULMNNSdAhJyNSiECRCjUbEPNCRAhZ6NSiAARCjXbUHMCRMjZqBQiQIRCzTbUnAARcjYqhQgQoVCzDTUnQIScjUohAkQo1GxDzQkQIWejUogAEQo121BzAkTI2agUIkCEQs021JwAEXI2KoUIEKFQsw01J0CEnI1KIQJEKNRsQ80JECFno1KIABEKNdtQcwJEyNmoFCJAhELNNtScABFyNiqFCBChULMNNSdAhJyNSiECRCjUbEPNCRAhZ6NSiAARCjXbUHMCRMjZqBQiQIRCzTbUnAARcjYqhQgQoVCzDTUnQIScjUohAkQo1GxDzQkQIWejUogAEQo121BzAkTI2agUIkCEQs021JwAEXI2KoUIEKFQsw01J0CEnI1KIQJEKNRsQ80JECFno1KIABEKNdtQcwJEyNmoFCJAhELNNtScABFyNiqFCBChULMNNSdAhJyNSiECRCjUbEPNCRAhZ6NSiAARCjXbUHMCRMjZqBQiQIRCzTbUnAARcjYqhQgQoVCzDTUnQIScjUohAkQo1GxDzQkQIWejUogAEQo121BzAkTI2agUIkCEQs021JwAEXI2KoUIEKFQsw01J0CEnI1KIQJEKNRsQ80JECFno1KIABEKNdtQcwJEyNmoFCJAhELNNtScABFyNiqFCBChULMNNSdAhJyNSiEC/wGgKKC4YMA4TAAAAABJRU5ErkJggg==';
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 || '',
sortBy: config?.github?.sortBy || 'stars',
limit: config?.github?.limit || 8,
exclude: {
forks: config?.github?.exclude?.forks || false,
projects: config?.github?.exclude?.projects || [],
},
},
social: {
linkedin: config?.social?.linkedin,
twitter: config?.social?.twitter,
mastodon: config?.social?.mastodon,
facebook: config?.social?.facebook,
instagram: config?.social?.instagram,
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 || [],
externalProjects: config?.externalProjects || [],
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,39 +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',
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,
},
});