Compare commits

...

22 Commits

Author SHA1 Message Date
Rahul Jain 3fc7776b23 Merge pull request #939 from DipakHalkude/feat/update-demo-video-v2
feat: update demo video to show V2 interface
2025-10-22 16:47:17 +05:30
Dipak Halkude 2262b5bb09 feat: update demo video to show V2 interface 2025-10-22 16:07:00 +05:30
Rahul Jain 61203fd35a chore: add github-sponsor 2025-10-17 16:59:35 +05:30
Rahul Jain be76e4117b chore: update demo link with V2 2025-10-16 14:54:44 +05:30
Rahul Jain 4ee906e1ce chore: load assets with assetPrefix 2025-10-15 21:50:49 +05:30
Rahul Jain 2dbe4c1d94 chore: fix asset path again attempt #4 2025-10-15 21:29:48 +05:30
Rahul Jain 2c3c5981f6 chore: fix asset path again attempt #3 2025-10-15 21:25:57 +05:30
Rahul Jain 99ee31601e chore: fix asset path again and refactor pull_request template 2025-10-15 21:22:53 +05:30
Rahul Jain b42d14166a Merge pull request #928 from rahuldkjain/rahuldkjain/full-revamp
chore: fix asset path & refactor deploy config
2025-10-15 21:14:33 +05:30
Rahul Jain 6ff9db1be1 chore: fix asset path & refactor deploy config 2025-10-15 21:13:50 +05:30
Rahul Jain 723efd95e8 Merge pull request #926 from rahuldkjain/rahuldkjain/full-revamp
chore: fix logo path and add documentation
2025-10-15 20:51:47 +05:30
Rahul Jain 6c91f98e57 chore: fix logo path and add documentation 2025-10-15 20:51:03 +05:30
Rahul Jain 17d7ab5ec2 Merge pull request #923 from rahuldkjain/rahuldkjain/full-revamp
chore: add support of preview URLs in GitHub UI
2025-10-14 12:01:41 +05:30
Rahul Jain ee75554751 chore: add support of preview URLs in GitHub UI 2025-10-14 11:53:30 +05:30
Rahul Jain 8468ec9b96 Merge pull request #922 from rahuldkjain/rahuldkjain/full-revamp
chore: fix BMC widget
2025-10-14 11:47:49 +05:30
Rahul Jain f42abc50f7 chore: fix BMC widget 2025-10-14 11:46:57 +05:30
Rahul Jain d3d2227e19 Merge pull request #921 from rahuldkjain/rahuldkjain/full-revamp
chore: attempt to fix the basePath in preview deployments
2025-10-14 10:53:51 +05:30
Rahul Jain bfaf29d3fc chore: attempt to fix the basePath in preview deployments 2025-10-14 10:53:05 +05:30
Rahul Jain cc1ad37bb9 Merge pull request #920 from rahuldkjain/rahuldkjain/full-revamp
chore: add debug logs for preview deployment step
2025-10-14 10:40:19 +05:30
Rahul Jain b834611004 chore: add debug logs for preview deployment step 2025-10-14 10:38:25 +05:30
Rahul Jain 48c6b29b42 Merge pull request #918 from rahuldkjain/rahuldkjain/full-revamp
feat: revamp entire tech stack and UX
2025-10-14 10:21:49 +05:30
Rahul Jain 04cf37f265 chore: add GPRG V2 2025-10-14 10:20:13 +05:30
138 changed files with 17929 additions and 80660 deletions
+12 -33
View File
@@ -1,7 +1,5 @@
{
"files": [
"README.md"
],
"files": ["README.md"],
"imageSize": 100,
"commit": false,
"contributors": [
@@ -10,82 +8,63 @@
"name": "Sarbik Betal",
"avatar_url": "https://avatars2.githubusercontent.com/u/41508422?v=4",
"profile": "https://github.com/sarbikbetal",
"contributions": [
"code"
]
"contributions": ["code"]
},
{
"login": "Hardik0307",
"name": "Hardik Bagada",
"avatar_url": "https://avatars3.githubusercontent.com/u/41434099?v=4",
"profile": "https://github.com/Hardik0307",
"contributions": [
"code"
]
"contributions": ["code"]
},
{
"login": "antonkomarev",
"name": "Anton Komarev",
"avatar_url": "https://avatars0.githubusercontent.com/u/1849174?v=4",
"profile": "https://komarev.com",
"contributions": [
"plugin"
]
"contributions": ["plugin"]
},
{
"login": "KKVANONYMOUS",
"name": "Kunal Kumar Verma",
"avatar_url": "https://avatars3.githubusercontent.com/u/58628586?v=4",
"profile": "https://kkvanonymous.github.io/",
"contributions": [
"code"
]
"contributions": ["code"]
},
{
"login": "jaideepghosh",
"name": "Jaideep Ghosh",
"avatar_url": "https://avatars2.githubusercontent.com/u/3909648?v=4",
"profile": "http://jaideepghosh.blogspot.com",
"contributions": [
"code"
]
}
"contributions": ["code"]
},
{
"login": "YashKandalkar",
"name": "yash",
"avatar_url": "https://avatars0.githubusercontent.com/u/35102959?v=4",
"profile": "http://yashkandalkar.github.io",
"contributions": [
"code"
]
"contributions": ["code"]
},
{
"login": "abhijit-hota",
"name": "Abhijit Hota",
"avatar_url": "https://avatars0.githubusercontent.com/u/8116174?v=4",
"profile": "https://github.com/abhijit-hota",
"contributions": [
"code",
"test"
]
"contributions": ["code", "test"]
},
{
"login": "Maddoxx88",
"name": "Sunit Shirke",
"avatar_url": "https://avatars1.githubusercontent.com/u/34238672?v=4",
"profile": "https://maddoxx88.github.io/",
"contributions": [
"code"
]
}
"contributions": ["code"]
},
{
"login": "g-savitha",
"name": "Savitha Gollamudi",
"avatar_url": "https://avatars0.githubusercontent.com/u/31612459?v=4",
"profile": "https://www.gsavitha.in",
"contributions": [
"code"
]
"contributions": ["code"]
}
],
"contributorsPerLine": 7,
+2 -3
View File
@@ -1,6 +1,6 @@
# These are supported funding model platforms
github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2]
github: rahuldkjain
patreon: # Replace with a single Patreon username
open_collective: github-profile-readme-generator
ko_fi: rahuldkjain
@@ -9,5 +9,4 @@ community_bridge: # Replace with a single Community Bridge project-name e.g., cl
liberapay: # Replace with a single Liberapay username
issuehunt: # Replace with a single IssueHunt username
otechie: # Replace with a single Otechie username
custom:
["https://paypal.me/rahuldkjain", "https://www.buymeacoffee.com/rahuldkjain"]
custom: ['https://paypal.me/rahuldkjain', 'https://www.buymeacoffee.com/rahuldkjain']
+31 -24
View File
@@ -1,43 +1,50 @@
---
name: Bug report
about: Create a report to help us improve
title: ''
labels: bug
name: 🐛 Bug Report
about: Report a bug in GitHub Profile README Generator
title: '[Bug] '
labels: ['bug']
assignees: ''
---
**Describe the bug**
## 🐛 Bug Description
A clear and concise description of what the bug is.
**To Reproduce**
Steps to reproduce the behavior:
## 🔄 Steps to Reproduce
1. Go to '...'
2. Click on '....'
3. Scroll down to '....'
1. Go to [URL or page]
2. Click on [element]
3. Fill in [specific fields]
4. See error
**Expected behavior**
A clear and concise description of what you expected to happen.
## ✅ Expected Behavior
A clear description of what you expected to happen.
## 📸 Screenshots
**Screenshots**
If applicable, add screenshots to help explain your problem.
**Desktop (please complete the following information):**
## 🖥️ Environment
- OS: [e.g. iOS]
- Browser [e.g. chrome, safari]
- Version [e.g. 22]
**Desktop:**
**Smartphone (please complete the following information):**
- OS: [e.g. macOS, Windows, Linux]
- Browser: [e.g. Chrome 118, Safari 17, Firefox 119]
- Device: [e.g. iPhone6]
- OS: [e.g. iOS8.1]
- Browser [e.g. stock browser, safari]
- Version [e.g. 22]
**Mobile:**
**Additional context**
Add any other context about the problem here.
- Device: [e.g. iPhone 15, Samsung Galaxy S23]
- OS: [e.g. iOS 17.1, Android 14]
- Browser: [e.g. Safari, Chrome Mobile]
## 🔧 Additional Context
- Does this happen in incognito/private mode? [Yes/No]
- Console errors (if any): [Paste console output]
- Network connectivity: [Good/Slow/Offline]
**Note:** Please test at the current version: https://rahuldkjain.github.io/gh-profile-readme-generator
Join the **Discord Server** for further discussions.
+67
View File
@@ -0,0 +1,67 @@
---
name: ✨ Feature Request
about: Suggest a new feature for GitHub Profile README Generator
title: '[Feature] '
labels: ['enhancement']
assignees: ''
---
## ✨ Feature Description
A clear and concise description of the feature you'd like to see.
## 🎯 Problem Statement
What problem does this feature solve? Is your feature request related to a problem?
## 💡 Proposed Solution
Describe the solution you'd like to see implemented.
## 🔄 User Flow
Describe how a user would interact with this feature:
1. User goes to...
2. User clicks/types...
3. System responds with...
## 🎨 Design Considerations
- UI/UX requirements
- Accessibility considerations
- Mobile responsiveness needs
- Theme compatibility (dark/light mode)
## 🔧 Technical Considerations
- Performance impact
- Browser compatibility requirements
- Dependencies needed
- Potential breaking changes
## 📋 Alternative Solutions
Describe alternatives you've considered.
## 📸 Mockups/Examples
If applicable, add mockups, sketches, or examples from other tools.
## 🎯 Priority
- [ ] Low - Nice to have
- [ ] Medium - Would improve UX significantly
- [ ] High - Critical for user workflow
- [ ] Critical - Blocking current functionality
## 📱 Target Platforms
- [ ] Desktop
- [ ] Mobile
- [ ] Tablet
- [ ] All platforms
---
💬 **Join our Discord** for feature discussions: https://discord.gg/HHMs7Eg
+160 -28
View File
@@ -1,42 +1,174 @@
<!--
For Work In Progress Pull Requests, please use the Draft PR feature,
see https://github.blog/2019-02-14-introducing-draft-pull-requests/ for further details.
🚀 Thanks for contributing to GitHub Profile README Generator V2!
For a timely review/response, please avoid force-pushing additional
commits if your PR already received reviews or comments.
Before submitting your Pull Request, please ensure you've done the following:
📖 Read the Contributing Guide: https://github.com/rahuldkjain/github-profile-readme-generator/blob/master/CONTRIBUTING.md
📖 Read the Code of Conduct: https://github.com/rahuldkjain/github-profile-readme-generator/blob/master/CODE_OF_CONDUCT.md
🔄 Follow our Commit Convention: https://github.com/rahuldkjain/github-profile-readme-generator/blob/master/COMMIT_CONVENTION.md
👷‍♀️ Create focused, single-purpose PRs
✅ Test your changes thoroughly
📝 Use conventional commit messages (feat:, fix:, docs:, etc.)
📗 Update documentation and add screenshots for UI changes
Before submitting a Pull Request, please ensure you've done the following:
- 📖 Read the Contributing Guide: https://github.com/rahuldkjain/github-profile-readme-generator/blob/master/CONTRIBUTING.md#create-a-pull-request.
- 📖 Read the Code of Conduct: https://github.com/rahuldkjain/github-profile-readme-generator/blob/master/CODE_OF_CONDUCT.md.
- 👷‍♀️ Create small PRs. In most cases this will be possible.
- ✅ Provide issue number with link.
- 📝 Use descriptive commit messages.
- 📗 Update any related documentation and include any relevant screenshots.
For Work In Progress PRs, please use the Draft PR feature.
Avoid force-pushing after receiving reviews unless requested.
-->
## What type of PR is this? (check all applicable)
# 🔄 Pull Request
- [ ] Refactor
- [ ] Feature
- [ ] Bug Fix
- [ ] Enhancement
- [ ] Documentation Update
## 📋 **Type of Change** (check all applicable)
## Description
- [ ] 🐛 **Bug Fix** - Fixes an issue without breaking existing functionality
- [ ]**Feature** - Adds new functionality
- [ ]**Performance** - Improves performance without changing functionality
- [ ] ♻️ **Refactor** - Code changes that neither fix bugs nor add features
- [ ] 📚 **Documentation** - Updates to documentation, comments, or README
- [ ] 🎨 **Style** - Code style changes (formatting, missing semi-colons, etc.)
- [ ] 🧪 **Test** - Adding or updating tests
- [ ] 🏗️ **Build** - Changes to build system or dependencies
- [ ] 👷 **CI/CD** - Changes to CI/CD workflows
- [ ] 🔒 **Security** - Security improvements or vulnerability fixes
- [ ]**Accessibility** - Improves accessibility compliance
- [ ] 📱 **Mobile** - Mobile-specific improvements
- [ ] 🌐 **i18n** - Internationalization changes
## Related Tickets & Documents
## 📖 **Description**
## QA Instructions, Screenshots, Recordings
<!-- Provide a clear and concise description of what this PR does -->
_Please replace this line with instructions on how to test your changes, as well
as any relevant images for UI changes._
### **What changed?**
<!-- ## Added tests?
<!-- Describe the changes made -->
- [ ] yes
- [ ] no, because they aren't needed
- [ ] no, because I need help -->
### **Why was this change made?**
## Added to documentation?
<!-- Explain the motivation behind this change -->
- [ ] readme
### **How does this change help users?**
<!-- Describe the user benefit -->
## 🔗 **Related Issues**
<!-- Link related issues using keywords: Closes #123, Fixes #456, Related to #789 -->
- Closes #
- Fixes #
- Related to #
## 🧪 **Testing & Quality Assurance**
### **Testing Done** (check all applicable)
- [ ]**Manual testing** - Tested functionality manually
- [ ] 🧪 **Unit tests** - Added/updated unit tests
- [ ] 🔄 **Integration tests** - Tested with other components
- [ ] 📱 **Mobile testing** - Tested on mobile devices
- [ ]**Accessibility testing** - Tested with screen readers/keyboard nav
- [ ] 🌐 **Cross-browser testing** - Tested in multiple browsers
- [ ] 🎨 **Visual testing** - Checked UI/UX in light/dark themes
### **Test Instructions**
<!-- Provide step-by-step instructions for reviewers to test your changes -->
1.
2.
3.
### **Expected Behavior**
<!-- Describe what should happen when testing -->
## 📸 **Screenshots/Recordings**
<!--
For UI changes, please include:
- Before/after screenshots
- Mobile screenshots
- Dark/light theme screenshots
- Screen recordings for complex interactions
-->
### **Before**
<!-- Screenshot/description of current state -->
### **After**
<!-- Screenshot/description of new state -->
## 📋 **Checklist**
### **Code Quality**
- [ ] 🔍 **TypeScript** - No TypeScript errors (`npm run type-check`)
- [ ] 🧹 **Linting** - No ESLint errors (`npm run lint`)
- [ ] 🎨 **Formatting** - Code is properly formatted (`npm run format`)
- [ ] 🏗️ **Build** - Production build succeeds (`npm run build`)
- [ ]**Performance** - No performance regressions introduced
### **Accessibility**
- [ ]**WCAG Compliance** - Follows WCAG 2.1 AA guidelines
- [ ] ⌨️ **Keyboard Navigation** - All interactive elements are keyboard accessible
- [ ] 🔍 **Screen Reader** - Proper ARIA labels and semantic HTML
- [ ] 🎨 **Color Contrast** - Meets contrast requirements
- [ ] 🎯 **Focus Management** - Visible focus indicators
### **Mobile & Responsive**
- [ ] 📱 **Mobile Responsive** - Works on mobile devices (320px+)
- [ ] 🖥️ **Desktop** - Works on desktop (1024px+)
- [ ] 📐 **Tablet** - Works on tablet sizes (768px+)
- [ ] 🔄 **Orientation** - Works in portrait and landscape
### **Browser Compatibility**
- [ ] 🌐 **Chrome** - Latest version
- [ ] 🦊 **Firefox** - Latest version
- [ ] 🧭 **Safari** - Latest version
- [ ] 📱 **Mobile Safari** - iOS Safari
- [ ] 📱 **Chrome Mobile** - Android Chrome
### **Documentation**
- [ ] 📚 **Code Comments** - Added helpful comments for complex logic
- [ ] 📖 **Documentation** - Updated relevant documentation
- [ ] 📝 **README** - Updated README if needed
- [ ] 🔄 **Changelog** - Will be auto-generated from conventional commits
### **Security & Privacy**
- [ ] 🔒 **No Secrets** - No API keys, passwords, or sensitive data exposed
- [ ] 🛡️ **Input Validation** - Proper validation for user inputs
- [ ] 🔐 **XSS Prevention** - Protected against XSS attacks
- [ ] 🍪 **Privacy Compliant** - Follows GDPR/privacy requirements
## 🚀 **Deployment Notes**
<!-- Any special considerations for deployment -->
- [ ] **No breaking changes** - Backward compatible
- [ ] **Database changes** - N/A (static site)
- [ ] **Environment variables** - No new env vars needed
- [ ] **Third-party dependencies** - No new external dependencies
## 📝 **Additional Notes**
<!-- Any additional information, concerns, or context -->
## 👀 **Reviewers**
<!-- Tag specific reviewers if needed -->
<!-- @username for specific reviewers -->
---
**By submitting this PR, I confirm that:**
- ✅ I have read and agree to the [Code of Conduct](https://github.com/rahuldkjain/github-profile-readme-generator/blob/master/CODE_OF_CONDUCT.md)
- ✅ I have followed the [Contributing Guidelines](https://github.com/rahuldkjain/github-profile-readme-generator/blob/master/CONTRIBUTING.md)
- ✅ I have used [Conventional Commits](https://github.com/rahuldkjain/github-profile-readme-generator/blob/master/COMMIT_CONVENTION.md) for my commit messages
- ✅ I have tested my changes thoroughly
- ✅ My code follows the project's coding standards
+182
View File
@@ -0,0 +1,182 @@
name: Build and Deploy
on:
push:
branches: [master, dev]
# Allow concurrent deployments for different environments
concurrency:
group: 'pages-${{ github.ref }}'
cancel-in-progress: true
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Run type check
run: npm run type-check
- name: Run linting
run: npm run lint
- name: Run tests
run: npm run test
- name: Build application
run: |
echo "Building for branch: ${{ github.ref_name }}"
echo "NODE_ENV: $NODE_ENV"
echo "SURGE_PREVIEW: $SURGE_PREVIEW"
npm run build
env:
NEXT_PUBLIC_GA_ID: ${{ secrets.NEXT_PUBLIC_GA_ID }}
NEXT_PUBLIC_REQUIRE_CONSENT: true
NEXT_PUBLIC_ANONYMIZE_IP: true
# Set SURGE_PREVIEW for non-master branches to disable basePath
SURGE_PREVIEW: ${{ github.ref != 'refs/heads/master' && 'true' || '' }}
- name: Upload Pages Artifact (Production)
if: github.ref == 'refs/heads/master'
uses: actions/upload-pages-artifact@v3
with:
path: ./out
- name: Upload Preview Artifact
if: github.ref != 'refs/heads/master'
uses: actions/upload-artifact@v4
with:
name: preview-build-${{ github.run_number }}-${{ github.run_attempt }}
path: ./out
retention-days: 30
# Production deployment to main GitHub Pages
deploy-production:
needs: build
runs-on: ubuntu-latest
# Only deploy to production from master branch
if: github.ref == 'refs/heads/master'
permissions:
pages: write
id-token: write
environment:
name: github-pages
url: ${{ steps.deployment.outputs.page_url }}
steps:
- name: Deploy to GitHub Pages
id: deployment
uses: actions/deploy-pages@v4
# Preview deployment for development branches
deploy-preview:
needs: build
runs-on: ubuntu-latest
# Deploy preview for dev branches (not master, only on direct push)
if: github.ref != 'refs/heads/master' && github.event_name == 'push'
# Add deployment environment to show URL in GitHub UI
environment:
name: preview-${{ github.ref_name }}
steps:
- name: Calculate sanitized branch name
id: branch
run: |
SANITIZED_BRANCH=$(echo "${{ github.ref_name }}" | sed 's/[^a-zA-Z0-9-]/-/g' | tr '[:upper:]' '[:lower:]')
echo "sanitized=$SANITIZED_BRANCH" >> $GITHUB_OUTPUT
echo "url=https://gprg-${SANITIZED_BRANCH}.surge.sh" >> $GITHUB_OUTPUT
- name: Debug deployment conditions
run: |
echo "GitHub ref: ${{ github.ref }}"
echo "GitHub ref name: ${{ github.ref_name }}"
echo "Event name: ${{ github.event_name }}"
echo "Is master?: ${{ github.ref == 'refs/heads/master' }}"
echo "Is push?: ${{ github.event_name == 'push' }}"
echo "Should deploy?: ${{ github.ref != 'refs/heads/master' && github.event_name == 'push' }}"
- name: Download Preview Artifact
uses: actions/download-artifact@v4
with:
name: preview-build-${{ github.run_number }}-${{ github.run_attempt }}
path: ./preview-out
- name: Deploy to Surge.sh (Preview)
id: deploy
run: |
npm install -g surge
echo "Deploying preview for branch: ${{ github.ref_name }}"
echo "Sanitized branch name: ${{ steps.branch.outputs.sanitized }}"
echo "Preview URL: ${{ steps.branch.outputs.url }}"
surge ./preview-out ${{ steps.branch.outputs.url }} --token ${{ secrets.SURGE_TOKEN }}
echo "deployment_url=${{ steps.branch.outputs.url }}" >> $GITHUB_OUTPUT
env:
SURGE_TOKEN: ${{ secrets.SURGE_TOKEN }}
- name: Comment PR with Preview URL
if: github.event_name == 'pull_request'
uses: actions/github-script@v7
with:
script: |
const previewUrl = '${{ steps.deploy.outputs.deployment_url }}';
github.rest.issues.createComment({
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
body: `🚀 **Preview Deployment Ready!**\n\n📱 Preview URL: ${previewUrl}\n\n*This preview will be available for 30 days.*`
});
- name: Create deployment status
uses: actions/github-script@v7
with:
script: |
const previewUrl = '${{ steps.deploy.outputs.deployment_url }}';
// Create a commit status with the deployment URL
github.rest.repos.createCommitStatus({
owner: context.repo.owner,
repo: context.repo.repo,
sha: context.sha,
state: 'success',
target_url: previewUrl,
description: `Preview deployed to ${previewUrl}`,
context: 'deployment/preview'
});
- name: Update Status Check
run: |
echo "🚀 Preview deployed successfully!"
echo "📱 Preview URL: ${{ steps.deploy.outputs.deployment_url }}"
echo ""
echo "You can find this URL in:"
echo "1. GitHub Actions > Environments tab"
echo "2. Commit status checks"
echo "3. This workflow run summary"
# Add to job summary for easy access
echo "## 🚀 Preview Deployment Complete!" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "**Branch:** \`${{ github.ref_name }}\`" >> $GITHUB_STEP_SUMMARY
echo "**Preview URL:** [${{ steps.deploy.outputs.deployment_url }}](${{ steps.deploy.outputs.deployment_url }})" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "### Where to find this URL:" >> $GITHUB_STEP_SUMMARY
echo "- **Environments tab:** Go to your repository → Environments → preview-${{ github.ref_name }}" >> $GITHUB_STEP_SUMMARY
echo "- **Commit status:** Check the commit status checks for 'deployment/preview'" >> $GITHUB_STEP_SUMMARY
echo "- **This summary:** Bookmark this workflow run for easy access" >> $GITHUB_STEP_SUMMARY
+37 -62
View File
@@ -1,70 +1,45 @@
dist/
# Logs
logs
*.log
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# dependencies
/node_modules
/.pnp
.pnp.*
.yarn/*
!.yarn/patches
!.yarn/plugins
!.yarn/releases
!.yarn/versions
# testing
/coverage
# next.js
/.next/
/out/
# production
/build
# misc
.DS_Store
*.pem
# debug
npm-debug.log*
yarn-debug.log*
yarn-error.log*
.pnpm-debug.log*
# Runtime data
pids
*.pid
*.seed
*.pid.lock
# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov
# Coverage directory used by tools like istanbul
coverage
# nyc test coverage
.nyc_output
# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
.grunt
# Bower dependency directory (https://bower.io/)
bower_components
# node-waf configuration
.lock-wscript
# Compiled binary addons (http://nodejs.org/api/addons.html)
build/Release
# Dependency directories
node_modules/
jspm_packages/
# Typescript v1 declaration files
typings/
# Optional npm cache directory
.npm
# Optional eslint cache
.eslintcache
# Optional REPL history
.node_repl_history
# Output of 'npm pack'
*.tgz
# dotenv environment variable files
# env files (can opt-in for committing if needed)
.env*
# gatsby files
.cache/
public
# vercel
.vercel
# Mac files
.DS_Store
# typescript
*.tsbuildinfo
next-env.d.ts
# Yarn
yarn-error.log
.pnp/
.pnp.js
# Yarn Integrity file
.yarn-integrity
# Claude
.cursor/
.claude/
+5 -3
View File
@@ -2,8 +2,10 @@
"singleQuote": true,
"jsxSingleQuote": false,
"tabWidth": 2,
"printWidth": 120,
"trailingComma": "all",
"printWidth": 100,
"trailingComma": "es5",
"semi": true,
"exclude": ["node_modules", "codepipeline"]
"arrowParens": "always",
"endOfLine": "lf",
"plugins": ["prettier-plugin-tailwindcss"]
}
-6
View File
@@ -1,6 +0,0 @@
language: node_js
node_js:
- "14"
cache:
directories:
- "node_modules"
+82
View File
@@ -0,0 +1,82 @@
# Changelog
All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [2.0.0] - 2025-10-15
### ✨ Features
- **Complete rewrite**: Migrated to Next.js 15 with App Router and Turbopack
- **React 19**: Updated to latest React with concurrent features
- **TypeScript 5**: Full type safety with strict configuration
- **Modern UI**: Tailwind CSS 4 with design tokens and CSS variables
- **Accessibility**: WCAG 2.1 AA compliance with accessibility menu
- **Privacy**: GDPR-compliant analytics with opt-in consent
- **Performance**: 3x faster builds and 50% smaller bundle size
- **Auto-fill**: GitHub integration for automatic profile data
- **Export/Import**: JSON functionality for profile data
- **Enhanced UX**: Multi-step wizard with real-time validation
- **Responsive**: Mobile-first design with touch optimization
### 🐛 Bug Fixes
- Fixed skill selection persistence across sessions
- Resolved theme toggle flickering on page load
- Fixed social media icon alignment issues
- Corrected markdown preview rendering edge cases
### ⚡ Performance Improvements
- Implemented code splitting with lazy loading
- Optimized bundle size with Turbopack
- Added image optimization for better loading
- Reduced JavaScript bundle by 50%
### ♻️ Code Refactoring
- Migrated from Gatsby to Next.js 15
- Converted all components to TypeScript
- Implemented modern React patterns (hooks, context)
- Restructured project architecture for scalability
### 📚 Documentation
- Added comprehensive TypeScript documentation
- Created accessibility guidelines
- Updated deployment documentation
- Added contributing guidelines for V2
### 🏗️ Build System
- Migrated to Next.js build system
- Added Turbopack for development
- Implemented ESLint + Prettier configuration
- Added Vitest for testing
### 👷 Continuous Integration
- Enhanced GitHub Actions workflows
- Added preview deployments with environment tracking
- Implemented automated release management
- Added comprehensive testing pipeline
---
## Previous Versions (V1)
For changes in V1, see the [V1 Release Archive](https://github.com/rahuldkjain/github-profile-readme-generator/releases?q=v1&expanded=true).
### Migration from V1 to V2
V2 represents a complete rewrite with breaking changes:
- **Technology Stack**: Gatsby → Next.js 15
- **Styling**: CSS Modules → Tailwind CSS 4
- **State Management**: Local state → Zustand + localStorage
- **Build System**: Webpack → Turbopack
- **Type Safety**: JavaScript → TypeScript 5
All V1 functionality has been preserved and enhanced in V2. See [MIGRATION_STRATEGY.md](./MIGRATION_STRATEGY.md) for detailed migration information.
+434
View File
@@ -0,0 +1,434 @@
# GitHub Profile Generator - Developer Cheatsheet
## 🚀 Quick Start
```bash
npm install && npm run dev
```
Visit: http://localhost:3000
---
## 📁 File Structure (Where to Edit)
| Task | File Location |
|------|--------------|
| Add form field | `src/components/sections/[section]-section.tsx` |
| Add validation | `src/lib/validations.ts` |
| Modify markdown output | `src/lib/markdown-generator.ts` |
| Add new section | Create in `src/components/sections/` + add to `src/app/page.tsx` |
| Create form component | `src/components/forms/` |
| Add skill | `src/constants/skills.ts` |
| Storage logic | `src/lib/storage.ts` |
| Theme colors | `src/styles/globals.css` |
| Header/Footer | `src/components/layout/` |
---
## 🎨 Theme Colors (Always Use These)
```typescript
// Backgrounds
bg-background // Main background
bg-card // Card background
bg-accent // Accent background
bg-muted // Muted background
bg-primary // Primary action background
// Text
text-foreground // Main text
text-muted-foreground // Secondary text
text-primary-foreground // Text on primary bg
text-destructive // Error text
// Borders & Effects
border-border // Border color
border-input // Input border
ring-ring // Focus ring
```
**❌ Never hardcode:** `bg-white`, `text-black`, `border-gray-300`
---
## 📝 Component Templates
### Form Component
```typescript
'use client';
import { forwardRef } from 'react';
import type { InputHTMLAttributes } from 'react';
export interface MyInputProps extends Omit<InputHTMLAttributes<HTMLInputElement>, 'type'> {
label?: string;
error?: string;
}
export const MyInput = forwardRef<HTMLInputElement, MyInputProps>(
({ label, error, className = '', ...props }, ref) => {
return (
<div className="w-full space-y-1">
{label && <label htmlFor={props.id} className="text-foreground text-sm font-medium">{label}</label>}
<input
ref={ref}
className={`border-input bg-background text-foreground focus:ring-ring w-full rounded-lg border px-4 py-2 transition-colors focus:outline-none focus:ring-2 disabled:cursor-not-allowed disabled:opacity-50 ${error ? 'border-destructive' : ''} ${className}`}
aria-invalid={!!error}
{...props}
/>
{error && <p className="text-destructive text-sm" role="alert">{error}</p>}
</div>
);
}
);
MyInput.displayName = 'MyInput';
```
### Section Component
```typescript
'use client';
import { UseFormRegister, FieldErrors } from 'react-hook-form';
import { FormInput } from '@/components/forms/form-input';
import type { MyFormData } from '@/lib/validations';
interface MySectionProps {
register: UseFormRegister<MyFormData>;
errors: FieldErrors<MyFormData>;
}
export function MySection({ register, errors }: MySectionProps) {
return (
<div className="space-y-6">
<div className="border-b border-border pb-4">
<h2 className="text-2xl font-bold">Section Title</h2>
<p className="text-muted-foreground mt-1 text-sm">Description</p>
</div>
<div className="grid gap-4 md:grid-cols-2">
<FormInput
{...register('fieldName')}
id="fieldName"
label="Label"
error={errors.fieldName?.message}
/>
</div>
</div>
);
}
```
---
## 🔒 TypeScript Patterns
```typescript
// Component Props
interface ComponentProps {
title: string; // Required
count?: number; // Optional
onAction: () => void; // Required function
onChange?: (val: string) => void; // Optional function
}
// Zod Schema
const schema = z.object({
email: z.string().email(),
age: z.number().min(0).max(120),
url: z.string().url().optional(),
isActive: z.boolean().default(false),
});
type FormData = z.infer<typeof schema>;
// Form Hook
const { register, watch, setValue, formState: { errors } } = useForm<FormData>({
resolver: zodResolver(schema),
defaultValues: { isActive: false }
});
```
---
## ♿ Accessibility Checklist
```typescript
// ✅ Keyboard Navigation
<button
onClick={handleClick}
onKeyDown={(e) => e.key === 'Enter' && handleClick()}
className="focus:ring-2 focus:ring-primary"
>
// ✅ ARIA Labels
<nav aria-label="Main navigation">
<button aria-label="Close dialog">
<input aria-describedby="help-text" aria-invalid={!!error} />
// ✅ Form Labels
<label htmlFor="email">Email</label>
<input id="email" />
// ✅ Error Messages
<p id="email-error" role="alert" className="text-destructive">
{error}
</p>
// ✅ Active States
<Link href="/" aria-current={isActive ? 'page' : undefined}>
// ✅ Alt Text
<img src={url} alt="Descriptive text" loading="lazy" />
```
---
## 📱 Responsive Patterns
```typescript
// Mobile First
className="text-sm md:text-base lg:text-lg"
// Grid Layouts
className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4"
// Visibility
className="hidden md:block" // Desktop only
className="block md:hidden" // Mobile only
// Spacing
className="gap-2 md:gap-4 lg:gap-6"
className="px-4 md:px-6 lg:px-8"
// Breakpoints
// base: 0-639px
// sm: 640px+
// md: 768px+
// lg: 1024px+
// xl: 1280px+
```
---
## 🎯 Import Order
```typescript
// 1. React & Next.js
import { useState, useEffect } from 'react';
import Link from 'next/link';
// 2. Third-party
import { useForm } from 'react-hook-form';
import { z } from 'zod';
// 3. Internal utils
import { saveFormData } from '@/lib/storage';
import type { ProfileFormData } from '@/lib/validations';
// 4. Components
import { FormInput } from '@/components/forms/form-input';
// 5. Constants
import { skills } from '@/constants/skills';
```
---
## 🏎️ Performance
```typescript
// Memoize calculations
const count = useMemo(() =>
expensiveCalculation(data),
[data]
);
// Memoize callbacks
const handleClick = useCallback(() => {
doSomething();
}, []);
// Memoize components
const MemoizedComponent = React.memo(Component);
// Debounce auto-save
useEffect(() => {
const timer = setTimeout(() => {
saveData();
}, 1000);
return () => clearTimeout(timer);
}, [data]);
// Lazy load images
<img src={url} alt="..." loading="lazy" />
```
---
## 💾 Storage Pattern
```typescript
// Save
import { saveFormData } from '@/lib/storage';
saveFormData({
profile: getProfileValues(),
social: getSocialValues(),
// ...
skills: selectedSkills,
lastSaved: new Date().toISOString(),
});
// Load
import { loadFormData } from '@/lib/storage';
useEffect(() => {
const saved = loadFormData();
if (saved) {
// Restore data
setValue('title', saved.profile.title);
}
}, []);
// Clear
import { clearFormData } from '@/lib/storage';
clearFormData();
```
---
## 🐛 Common Errors & Fixes
| Error | Fix |
|-------|-----|
| "Type 'any' is not assignable" | Add explicit types, avoid `any` |
| "Property does not exist" | Add to interface, check spelling |
| "Cannot find module '@/...'" | Check path, restart TS server |
| "localStorage is not defined" | Wrap in `useEffect`, check 'use client' |
| "Hydration mismatch" | Ensure same HTML on server/client |
| TypeScript errors after install | Run `npm run type-check` |
---
## 🎨 Tailwind Class Order
```
Layout → Size → Spacing → Typography → Colors → Borders → Effects → States → Responsive
```
Example:
```typescript
className="flex items-center gap-2 w-full px-4 py-2 text-sm font-medium text-foreground bg-accent rounded-lg border border-border shadow-sm transition-colors hover:bg-accent/80 focus:ring-2 focus:ring-primary disabled:opacity-50 md:px-6 lg:text-base"
```
---
## 🔧 Useful Commands
```bash
# Development
npm run dev # Start dev server (Turbopack)
npm run build # Production build
npm run start # Start production server
npm run type-check # Check TypeScript
npm run lint # Run ESLint
# Git
git status # Check changes
git add . # Stage all changes
git commit -m "feat: add feature" # Commit
git push # Push to remote
```
---
## 🎯 Git Commit Types
```
feat(scope): # New feature
fix(scope): # Bug fix
refactor(scope): # Code restructure
style(scope): # Formatting
a11y(scope): # Accessibility
perf(scope): # Performance
docs(scope): # Documentation
test(scope): # Tests
chore(scope): # Maintenance
```
---
## 📚 Key Files to Read
1. **REVAMP_SUMMARY.md** - Full architecture
2. **.cursorrules** - Code conventions
3. **.cursorrules-quick** - Quick patterns
4. **DX_GUIDE.md** - Developer guide
5. **src/app/page.tsx** - Main wizard implementation
---
## 🤖 Cursor AI Quick Commands
```
# Include file in context
@filename
# Include folder
@folder
# Create component
@.cursorrules Create a form component for [x]
# Fix code
@.cursorrules Fix accessibility issues in [component]
# Review
@.cursorrules Review this code for [standards]
```
---
## 💡 Pro Tips
1. **Always use 'use client'** for components with state/events
2. **Never hardcode colors** - use CSS variables
3. **Type everything** - avoid `any`
4. **Think accessibility first** - keyboard + ARIA
5. **Mobile first** - base styles for mobile
6. **Test dark mode** - both themes
7. **Debounce saves** - 1 second delay
8. **Memoize when needed** - useMemo/useCallback
9. **Clean up effects** - return cleanup function
10. **Follow patterns** - check existing code first
---
## 🚨 Before Committing
- [ ] No TypeScript errors
- [ ] No ESLint warnings
- [ ] Works on mobile (375px)
- [ ] Works on desktop (1440px)
- [ ] Keyboard navigable
- [ ] Focus indicators visible
- [ ] Dark mode tested
- [ ] Auto-save works (if form)
- [ ] No console errors
- [ ] Follows .cursorrules patterns
---
## 📞 Help Resources
- **Internal**: REVAMP_SUMMARY.md, .cursorrules, DX_GUIDE.md
- **Next.js**: https://nextjs.org/docs
- **React**: https://react.dev
- **Tailwind**: https://tailwindcss.com/docs
- **React Hook Form**: https://react-hook-form.com
- **Zod**: https://zod.dev
- **WCAG**: https://www.w3.org/WAI/WCAG21/quickref/
---
**Print this and keep it handy! 📋**
+287 -70
View File
@@ -1,99 +1,316 @@
# Coding Style
# Coding Style Guide
## File Layout (`src/components/*.js`)
## Project Architecture
1. Imports
2. Reusable components needed for the main component
3. Main component (Eg: Addons in addons.js)
4. export default \<MainComponent\>;
This project uses **Next.js 15** with **TypeScript** and **Tailwind CSS**. We follow modern React patterns with functional components and hooks.
## Reusable components
## File Layout (`src/components/*.tsx`)
- Do not make a new file for smaller components.
- Smaller, reusable components needed in the main components should be added **above** the main component, **not** inside it.
- Use ES6 arrow functions for defining components.
1. **Imports**
- React imports first
- Third-party library imports
- Internal component imports
- Type imports (using `import type`)
## Spacing
2. **Type Definitions**
- Interface definitions for props
- Type aliases if needed
1. **JS:**
3. **Reusable Components**
- Smaller components needed for the main component
- Place **above** the main component, **not** inside it
- Use a space after `if`, `for`, `while`, `switch`.
- Do not use a space after the opening `(` and before the closing `)`.
- Use a space before and after destructuring objects.
4. **Main Component**
- Main exported component (e.g., `SkillsSection` in `skills-section.tsx`)
```js
//good
const { apple, mangoes } = fruits;
5. **Export Statement**
- `export default MainComponent;` or named exports
//bad
const { apple, mangoes } = fruits;
```
## TypeScript Guidelines
### Component Props
//Same for destructuring props:
//good
const BeautifulComponent = ({ prop1, prop2 }) => {}
- Use `interface` for component props with clear naming:
//bad
const UglyComponent = ({prop1, prop2}) => {}
```
```tsx
interface ButtonProps {
variant?: 'primary' | 'secondary';
size?: 'sm' | 'md' | 'lg';
onClick?: () => void;
children: React.ReactNode;
}
```
2. **JSX:**
### Type Safety
- Use a space before the forward slash (`/`) of a self-closing tag
- Avoid `any` types - use `unknown` or proper types
- Use strict TypeScript configuration
- Leverage type inference where possible
- Use `as const` for literal types
```js
//good
<Foo />
```tsx
// Good
const themes = ['light', 'dark'] as const;
type Theme = (typeof themes)[number];
//bad
<Foo/>
```
// Bad
const themes: any = ['light', 'dark'];
```
- Do **not** use spaces for JSX curly braces
## Component Patterns
```js
//good
<Foo bar={baz} />
### Functional Components
//bad
<Foo bar={ baz } />
```
- Use **ES6 arrow functions** for all components
- Use `React.forwardRef` when ref forwarding is needed
- Prefer named exports for reusable components
## **Props:**
```tsx
// Good
export const Button = ({ variant = 'primary', ...props }: ButtonProps) => {
return <button className={`btn btn-${variant}`} {...props} />;
};
- Use camelCase for prop names, or PascalCase if the prop value is a React component.
- Use new lines when props do not fit on the same line.
// Also good for main components
const SkillsSection = ({ skills, onSkillToggle }: SkillsSectionProps) => {
// component logic
};
export default SkillsSection;
```
```js
//good
<Foo
prop1={value1}
prop2={value2}
prop3={value3}
/>
### Hooks Usage
//bad
<Foo prop1={value1} prop2={value2} prop3={value3} />
```
- Use custom hooks for reusable logic
- Keep hooks at the top of components
- Use `useCallback` and `useMemo` for performance optimization
## **Best practices:**
```tsx
const MyComponent = () => {
const [state, setState] = useState();
const { data, loading } = useCustomHook();
- **Always** add semicolons after a line.
- Use ES6 arrow functions.
- Keep the indentation in your code correct.
- Use 4 spaces for tabs.
- Don't Repeat Yourself. If you think you're repeating too much code, make a smaller component, or a function.
- **Always** add alt prop to `img` tags.
- Add `rel="noopener"` for `a` tags which has `target="_blank"`.
- Don't do `outline: none` on user input elements. If you do not want outline, give them faint, visible background on focus. This is for accessibility.
const memoizedValue = useMemo(() => expensiveCalculation(), [dependency]);
### Other things to note
// component JSX
};
```
- We are using [octicons](https://primer.style/octicons/) for icons. Use this if you need to add icons. Do **not** add a new library for icons.
- Try to not commit changes in `package.json`, `package-lock.json`.
- Discuss with contributors on discord if you're planning to add/remove a package.
## Styling with Tailwind CSS
## Further reading:
### Class Organization
This guide is based on [airbnb's react guide](https://github.com/airbnb/javascript/tree/master/react). You can read all the best practices there.
- Use Tailwind utility classes
- Group related classes together
- Use responsive prefixes (`sm:`, `md:`, `lg:`)
```tsx
// Good
<div className="flex flex-col gap-4 rounded-lg border p-4 sm:flex-row sm:gap-6">
<button className="bg-primary text-primary-foreground hover:bg-primary/90 rounded px-4 py-2 transition-colors">
Click me
</button>
</div>
```
### CSS Variables
- Use CSS custom properties for theming
- Follow the design system color palette
- Prefer Tailwind classes over custom CSS
## File Naming Conventions
- **Components**: `kebab-case.tsx` (e.g., `skills-section.tsx`)
- **Hooks**: `use-hook-name.ts` (e.g., `use-local-storage.ts`)
- **Types**: `kebab-case.ts` (e.g., `profile-types.ts`)
- **Utils**: `kebab-case.ts` (e.g., `markdown-generator.ts`)
## Spacing & Formatting
### JavaScript/TypeScript
- Use **2 spaces** for indentation (not 4)
- Use spaces after `if`, `for`, `while`, `switch`
- No spaces after opening `(` and before closing `)`
- Use spaces around destructuring
```tsx
// Good
const { name, email } = user;
const handleClick = ({ target }: MouseEvent) => {
if (target instanceof HTMLElement) {
// logic
}
};
// Bad
const { name, email } = user;
const handleClick = ({ target }: MouseEvent) => {
if (target instanceof HTMLElement) {
// logic
}
};
```
### JSX Formatting
- Space before self-closing tag slash
- No spaces in JSX curly braces
- Use new lines for multiple props
```tsx
// Good
<Input
value={value}
onChange={handleChange}
placeholder="Enter text"
/>
<Icon className="h-4 w-4" />
// Bad
<Input value={value} onChange={handleChange} placeholder="Enter text"/>
<Icon className="h-4 w-4"/>
```
## Props & Event Handling
### Prop Naming
- Use `camelCase` for prop names
- Use `PascalCase` if prop value is a React component
- Use descriptive names with proper prefixes
```tsx
interface FormProps {
initialValues?: Record<string, unknown>;
onSubmit?: (data: FormData) => void;
isLoading?: boolean;
ErrorComponent?: React.ComponentType;
}
```
### Event Handlers
- Prefix with `handle` or `on`
- Use descriptive names
- Type event handlers properly
```tsx
const handleInputChange = (event: React.ChangeEvent<HTMLInputElement>) => {
setValue(event.target.value);
};
const handleSubmit = async (event: React.FormEvent) => {
event.preventDefault();
// submit logic
};
```
## Best Practices
### Code Quality
- **Always** add semicolons
- Use meaningful variable and function names
- Keep components small and focused (< 200 lines)
- Extract complex logic into custom hooks
- Use TypeScript strict mode
### Accessibility
- Always add `alt` prop to `img` tags
- Use semantic HTML elements
- Add proper ARIA attributes
- Don't use `outline: none` without alternative focus styles
- Use proper heading hierarchy
### Performance
- Use `React.memo` for expensive components
- Implement proper dependency arrays for hooks
- Avoid inline objects and functions in JSX
- Use `useCallback` and `useMemo` appropriately
```tsx
// Good
const MemoizedComponent = React.memo(({ data }: Props) => {
const processedData = useMemo(() => processData(data), [data]);
const handleClick = useCallback(() => {
// click logic
}, []);
return <div>{/* JSX */}</div>;
});
```
### Error Handling
- Use error boundaries for component errors
- Handle async operations properly
- Provide fallback UI for loading states
```tsx
const AsyncComponent = () => {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
fetchData()
.then(setData)
.catch(setError)
.finally(() => setLoading(false));
}, []);
if (loading) return <LoadingSpinner />;
if (error) return <ErrorMessage error={error} />;
if (!data) return <EmptyState />;
return <DataDisplay data={data} />;
};
```
## Dependencies & Imports
### Import Organization
```tsx
// 1. React imports
import { useState, useEffect, useCallback } from 'react';
import type { ReactNode } from 'react';
// 2. Third-party libraries
import { motion } from 'framer-motion';
import { ChevronDown } from 'lucide-react';
// 3. Internal imports
import { Button } from '@/components/ui/button';
import { useLocalStorage } from '@/hooks/use-local-storage';
import type { ProfileData } from '@/types/profile';
```
### Package Management
- Prefer stable, well-maintained packages
- Keep dependencies up to date
- Use exact versions for critical dependencies
- Document any custom modifications
## Testing Considerations
- Write testable components with clear props
- Avoid complex side effects in components
- Use dependency injection for external services
- Keep business logic separate from UI logic
## Further Reading
This guide is based on:
- [Next.js Best Practices](https://nextjs.org/docs/app/building-your-application/styling/tailwind-css)
- [React TypeScript Cheatsheet](https://react-typescript-cheatsheet.netlify.app/)
- [Tailwind CSS Best Practices](https://tailwindcss.com/docs/reusing-styles)
For questions about code style, please discuss with maintainers on our [Discord community](https://discord.gg/HHMs7Eg).
+152
View File
@@ -0,0 +1,152 @@
# 📝 Commit Message Convention
This project follows [Conventional Commits](https://www.conventionalcommits.org/) specification for automated changelog generation and semantic versioning.
## Format
```
<type>[optional scope]: <description>
[optional body]
[optional footer(s)]
```
## Types
| Type | Description | Version Bump |
| ---------- | ------------------------ | ------------ |
| `feat` | New feature | Minor |
| `fix` | Bug fix | Patch |
| `perf` | Performance improvement | Patch |
| `refactor` | Code refactoring | Patch |
| `docs` | Documentation changes | Patch |
| `style` | Code style changes | Patch |
| `test` | Adding or updating tests | Patch |
| `build` | Build system changes | Patch |
| `ci` | CI/CD changes | Patch |
| `chore` | Maintenance tasks | No bump |
## Breaking Changes
Add `BREAKING CHANGE:` in the footer or `!` after type to indicate breaking changes:
```bash
feat!: remove deprecated API endpoints
BREAKING CHANGE: The old API endpoints have been removed. Use the new v2 endpoints instead.
```
## Examples
### Features
```bash
feat: add GitHub auto-fill integration
feat(ui): implement dark mode toggle
feat!: migrate to Next.js 15 App Router
```
### Bug Fixes
```bash
fix: resolve skill selection persistence issue
fix(mobile): correct responsive navigation layout
fix(a11y): improve keyboard navigation for forms
```
### Performance
```bash
perf: optimize image loading with next/image
perf(build): reduce bundle size by 30%
```
### Documentation
```bash
docs: update installation instructions
docs(api): add TypeScript examples
docs(readme): fix broken demo links
```
### Refactoring
```bash
refactor: convert components to TypeScript
refactor(store): migrate to Zustand state management
```
## Scopes (Optional)
Use scopes to indicate the area of change:
- `ui` - User interface components
- `api` - API related changes
- `build` - Build system
- `ci` - Continuous integration
- `docs` - Documentation
- `test` - Testing
- `a11y` - Accessibility
- `perf` - Performance
- `mobile` - Mobile-specific changes
## Tools
### Commitizen (Recommended)
Install commitizen for interactive commit messages:
```bash
npm install -g commitizen cz-conventional-changelog
echo '{ "path": "cz-conventional-changelog" }' > ~/.czrc
```
Use `git cz` instead of `git commit`:
```bash
git add .
git cz
```
### VS Code Extension
Install "Conventional Commits" extension for VS Code to get commit message templates.
## Automated Release Process
1. **Commit** using conventional format
2. **Push** to master branch
3. **Release Please** analyzes commits
4. **Creates PR** with changelog and version bump
5. **Merge PR** to trigger release and deployment
## Examples in Practice
```bash
# Adding new feature
git commit -m "feat(ui): add accessibility menu with font size controls"
# Fixing bug
git commit -m "fix(mobile): resolve navigation menu overflow on small screens"
# Breaking change
git commit -m "feat!: migrate to Next.js 15 App Router
BREAKING CHANGE: Pages directory structure has changed.
See migration guide for updating custom pages."
# Performance improvement
git commit -m "perf(build): implement code splitting for 50% bundle reduction"
# Documentation update
git commit -m "docs(contributing): add TypeScript coding standards"
```
## Benefits
-**Automated changelogs** - No manual changelog maintenance
-**Semantic versioning** - Automatic version bumps based on commit types
-**Release notes** - Rich, categorized release notes
-**Consistency** - Standardized commit history
-**Tooling integration** - Works with Release Please, semantic-release, etc.
+280 -12
View File
@@ -1,7 +1,6 @@
# Contributing
# Contributing to GitHub Profile README Generator
When contributing to this repository, please first discuss the change you wish to make via issue,
email, or any other method with the owners of this repository before making a change.
When contributing to this repository, please first discuss the change you wish to make via issue, email, or any other method with the owners of this repository before making a change.
<a href="https://discord.gg/HHMs7Eg" target="blank">
<img src="https://img.shields.io/discord/735303195105951764?color=%23677BC4&label=Join%20Community&style=flat-square" alt="join discord community of github profile readme generator"/>
@@ -9,13 +8,282 @@ email, or any other method with the owners of this repository before making a ch
Please note we have a code of conduct, please follow it in all your interactions with the project.
## Pull Request Process
## 🚀 Tech Stack
1. Ensure any install or build dependencies are removed before the end of the layer when doing a
build.
2. Update the README.md with details of changes to the interface, this includes new environment
variables, exposed ports, useful file locations and container parameters.
3. Increase the version numbers in any examples files and the README.md to the new version that this
Pull Request would represent. The versioning scheme we use is [SemVer](http://semver.org/).
4. You may merge the Pull Request once you have the sign-off of two other developers, or if you
do not have permission to do that, you may request the second reviewer merge it for you.
This project is built with modern web technologies:
- **Framework**: [Next.js 15](https://nextjs.org/) with App Router
- **Language**: [TypeScript](https://www.typescriptlang.org/) for type safety
- **Styling**: [Tailwind CSS](https://tailwindcss.com/) for utility-first styling
- **Icons**: [Lucide React](https://lucide.dev/) for consistent iconography
- **Animations**: [Framer Motion](https://www.framer.com/motion/) for smooth animations
- **Forms**: [React Hook Form](https://react-hook-form.com/) for form management
- **Analytics**: [Google Analytics 4](https://developers.google.com/analytics/devguides/collection/ga4) with privacy compliance
- **Testing**: [Vitest](https://vitest.dev/) for unit testing
- **Linting**: [ESLint](https://eslint.org/) + [Prettier](https://prettier.io/) for code quality
## 🛠️ Development Setup
### Prerequisites
- **Node.js**: Version 18.17 or higher
- **npm**: Version 9 or higher (comes with Node.js)
- **Git**: For version control
### Local Development
1. **Fork & Clone the repository**
```bash
# Fork the repo on GitHub, then clone your fork
git clone https://github.com/YOUR_USERNAME/github-profile-readme-generator.git
cd github-profile-readme-generator
```
2. **Install dependencies**
```bash
npm install
```
3. **Set up environment variables** (optional)
```bash
# Copy the example environment file
cp env.example .env.local
# Add your Google Analytics ID if you want to test analytics
NEXT_PUBLIC_GA_ID=G-XXXXXXXXXX
```
4. **Start the development server**
```bash
npm run dev
```
The app will be available at `http://localhost:3000`
### Available Scripts
```bash
# Development
npm run dev # Start development server
npm run build # Build for production
npm run start # Start production server
npm run export # Export static site
# Code Quality
npm run lint # Run ESLint
npm run lint:fix # Fix ESLint issues automatically
npm run type-check # Run TypeScript type checking
# Testing
npm run test # Run tests
npm run test:watch # Run tests in watch mode
npm run test:ui # Run tests with UI
```
## 📝 Pull Request Process
### Before You Start
1. **Check existing issues** to see if your feature/bug is already being worked on
2. **Create an issue** if one doesn't exist for your contribution
3. **Join our Discord** to discuss your ideas with the community
4. **Read our [Code Style Guide](CODE_STYLE_GUIDE.md)** to understand our coding standards
### Making Changes
1. **Create a feature branch** from `main`
```bash
git checkout -b feature/your-feature-name
# or
git checkout -b fix/bug-description
```
2. **Follow our coding standards**
- Use TypeScript with strict mode
- Follow the existing code style (ESLint + Prettier)
- Write meaningful commit messages
- Add tests for new features
- Update documentation if needed
3. **Test your changes**
```bash
# Run all checks before submitting
npm run lint # Check code style
npm run type-check # Check TypeScript
npm run test # Run tests
npm run build # Ensure it builds successfully
```
4. **Commit your changes**
```bash
# Use conventional commit messages
git add .
git commit -m "feat: add new skill category filter"
# or
git commit -m "fix: resolve mobile navigation issue"
```
### Submitting Your PR
1. **Push your branch**
```bash
git push origin feature/your-feature-name
```
2. **Create a Pull Request** on GitHub with:
- Clear title describing the change
- Detailed description of what was changed and why
- Screenshots for UI changes
- Reference to any related issues
3. **PR Requirements**:
- ✅ All tests pass
- ✅ No TypeScript errors
- ✅ ESLint passes
- ✅ Builds successfully
- ✅ Follows our code style guide
- ✅ Includes tests for new features
- ✅ Updates documentation if needed
### Review Process
1. **Automated checks** will run on your PR
2. **Maintainers** will review your code
3. **Address feedback** by pushing new commits to your branch
4. **Merge** happens after approval from maintainers
## 🎯 Contribution Guidelines
### What We're Looking For
- **Bug fixes** with clear reproduction steps
- **New features** that align with the project's goals
- **Performance improvements** with benchmarks
- **Accessibility improvements** following WCAG guidelines
- **Documentation** improvements and translations
- **Test coverage** improvements
### Areas That Need Help
- 🌍 **Internationalization** (i18n) support
- 📱 **Mobile experience** improvements
-**Accessibility** enhancements
- 🎨 **UI/UX** improvements
- 🧪 **Test coverage** expansion
- 📚 **Documentation** improvements
- 🔧 **Developer experience** tools
### Component Development
When creating new components:
```tsx
// Follow this structure for new components
interface ComponentProps {
// Define clear prop types
title: string;
onAction?: () => void;
variant?: 'primary' | 'secondary';
}
export const Component = ({ title, onAction, variant = 'primary' }: ComponentProps) => {
// Component logic here
return <div className="component-styles">{/* JSX here */}</div>;
};
```
### File Organization
```
src/
├── app/ # Next.js app directory
├── components/ # Reusable UI components
│ ├── forms/ # Form-related components
│ ├── layout/ # Layout components
│ ├── sections/ # Page sections
│ └── ui/ # Basic UI components
├── hooks/ # Custom React hooks
├── lib/ # Utility functions and configurations
├── types/ # TypeScript type definitions
└── constants/ # Application constants
```
## 🐛 Reporting Bugs
When reporting bugs, please include:
1. **Steps to reproduce** the bug
2. **Expected behavior** vs actual behavior
3. **Screenshots** or screen recordings if applicable
4. **Browser/OS information**
5. **Console errors** if any
Use our [bug report template](https://github.com/rahuldkjain/github-profile-readme-generator/issues/new/choose) for consistency.
## 💡 Suggesting Features
For feature requests:
1. **Check existing issues** to avoid duplicates
2. **Describe the problem** you're trying to solve
3. **Propose a solution** with examples
4. **Consider the impact** on existing users
5. **Be open to discussion** and alternative approaches
## 🏷️ Issue Labels
We use labels to organize issues:
- `bug` - Something isn't working
- `enhancement` - New feature or request
- `good first issue` - Good for newcomers
- `help wanted` - Extra attention is needed
- `documentation` - Improvements to docs
- `accessibility` - A11y improvements
- `performance` - Performance improvements
## 📋 Code Review Checklist
Before requesting review, ensure:
- [ ] Code follows our style guide
- [ ] All tests pass locally
- [ ] TypeScript compiles without errors
- [ ] ESLint passes without warnings
- [ ] Component is accessible (proper ARIA labels, keyboard navigation)
- [ ] Mobile-responsive design
- [ ] Performance considerations addressed
- [ ] Documentation updated if needed
- [ ] Commit messages are clear and descriptive
## 🎉 Recognition
Contributors are recognized in:
- **README.md** contributors section
- **All Contributors** bot for automated recognition
- **Release notes** for significant contributions
- **Discord community** shoutouts
## 📞 Getting Help
- **Discord**: [Join our community](https://discord.gg/HHMs7Eg)
- **Issues**: [GitHub Issues](https://github.com/rahuldkjain/github-profile-readme-generator/issues)
- **Discussions**: [GitHub Discussions](https://github.com/rahuldkjain/github-profile-readme-generator/discussions)
## 📄 License
By contributing, you agree that your contributions will be licensed under the same license as the project (MIT License).
---
Thank you for contributing to GitHub Profile README Generator! 🚀
+141
View File
@@ -0,0 +1,141 @@
# 🚀 Production Deployment Guide
## Pre-Deployment Checklist
### ✅ SEO & Performance
- [x] **Meta Tags**: Complete Open Graph and Twitter Card metadata
- [x] **Structured Data**: JSON-LD schema for better search visibility
- [x] **Canonical URLs**: Proper canonical URLs for all pages
- [x] **Sitemap**: Auto-generated XML sitemap at `/sitemap.xml`
- [x] **Robots.txt**: SEO-friendly robots.txt configuration
- [x] **PWA Manifest**: Mobile app-like experience with manifest.json
### ✅ Assets & Performance
- [x] **Static Assets**: All assets properly placed in `/public` directory
- [x] **Image Optimization**: OG image and favicon configured
- [x] **Bundle Optimization**: Turbopack enabled for faster builds
- [x] **CSS Optimization**: Tailwind CSS optimized for production
- [x] **Font Loading**: Local fonts with proper fallbacks
### ✅ Analytics & Tracking
- [x] **Google Analytics**: GA4 integration with environment variable
- [x] **Buy Me Coffee**: Widget properly integrated
- [x] **Error Tracking**: Console error handling
## Environment Configuration
### 1. Create Environment File
```bash
cp .env.example .env.local
```
### 2. Configure Analytics & Privacy
```env
# Required for production analytics
NEXT_PUBLIC_GA_ID=G-XXXXXXXXXX
# Optional: Google Search Console verification
NEXT_PUBLIC_GOOGLE_SITE_VERIFICATION=your-verification-code
# Privacy & GDPR Compliance (recommended)
NEXT_PUBLIC_REQUIRE_CONSENT=true
NEXT_PUBLIC_ANONYMIZE_IP=true
```
**GA4 Setup Instructions:**
1. Create a GA4 property in Google Analytics
2. Copy your Measurement ID (format: G-XXXXXXXXXX)
3. Add it to your environment variables
4. The app includes GDPR-compliant consent management
5. Custom events track: GitHub auto-fill, README completion, file exports
## Build & Deploy
### GitHub Pages Deployment
```bash
# Build for production
npm run build
# The built files will be in the 'out' directory
# GitHub Pages will automatically serve from this directory
```
### Custom Domain Deployment
1. Update the base URL in `next.config.ts`
2. Update URLs in `src/app/layout.tsx` metadata
3. Update sitemap and robots.txt URLs
## Performance Metrics
### Bundle Analysis
- **Main Bundle**: ~282 kB (optimized)
- **First Load JS**: ~174 kB shared
- **Build Time**: ~3.2s with Turbopack
### SEO Score
- **Structured Data**: ✅ Complete
- **Meta Tags**: ✅ All pages covered
- **Performance**: ✅ Optimized bundles
- **Accessibility**: ✅ ARIA labels and semantic HTML
- **PWA**: ✅ Manifest and service worker ready
## Post-Deployment Verification
### 1. SEO Tools
- [ ] Test with [Google Rich Results Test](https://search.google.com/test/rich-results)
- [ ] Verify with [Facebook Sharing Debugger](https://developers.facebook.com/tools/debug/)
- [ ] Check with [Twitter Card Validator](https://cards-dev.twitter.com/validator)
### 2. Performance Testing
- [ ] Run [Google PageSpeed Insights](https://pagespeed.web.dev/)
- [ ] Test with [GTmetrix](https://gtmetrix.com/)
- [ ] Verify mobile responsiveness
### 3. Functionality Testing
- [ ] Test all form submissions
- [ ] Verify GitHub API integration
- [ ] Check markdown generation
- [ ] Test theme switching
- [ ] Verify analytics tracking
## Monitoring
### Analytics Setup
1. **Google Analytics**: Monitor user engagement and conversion
2. **Search Console**: Track search performance and indexing
3. **Error Monitoring**: Monitor console errors and user issues
### Key Metrics to Track
- **Page Load Speed**: < 3 seconds
- **Core Web Vitals**: LCP, FID, CLS scores
- **Conversion Rate**: README generation completion
- **User Engagement**: Time on site, bounce rate
## Troubleshooting
### Common Issues
1. **Build Failures**: Check Node.js version (18+)
2. **Asset Loading**: Verify all assets are in `/public`
3. **Analytics Not Working**: Check environment variables
4. **SEO Issues**: Validate structured data and meta tags
### Support
- **Issues**: [GitHub Issues](https://github.com/rahuldkjain/github-profile-readme-generator/issues)
- **Discussions**: [GitHub Discussions](https://github.com/rahuldkjain/github-profile-readme-generator/discussions)
+34 -10
View File
@@ -1,5 +1,5 @@
<p align="center">
<a href="https://rahuldkjain.github.io/gh-profile-readme-generator">
<a href="https://rahuldkjain.github.io/github-profile-readme-generator">
<img alt="GitHub Profile Readme Generator" src="./src/images/mdg.png" width="60" />
</a>
</p>
@@ -28,10 +28,10 @@
</a>
</p>
<p align="center"><img src="./src/images/github-profile-readme-generator.gif" alt="github-profile-readme-generator gif" /></p>
<p align="center"><img src="/public/demo.gif" alt="github-profile-readme-generator gif" /></p>
<p align="center">
<a href="https://rahuldkjain.github.io/gh-profile-readme-generator" target="blank">View Demo</a>
<a href="https://rahuldkjain.github.io/github-profile-readme-generator" target="blank">View Demo</a>
·
<a href="https://github.com/rahuldkjain/github-profile-readme-generator/issues/new/choose">Report Bug</a>
·
@@ -56,11 +56,11 @@ This tool provides an easy way to create a GitHub profile readme with the latest
## 🚀 Demo
<a href="https://rahuldkjain.github.io/gh-profile-readme-generator" target="blank">
<img src="https://img.shields.io/website?url=https%3A%2F%2Frahuldkjain.github.io%2Fgh-profile-readme-generator&logo=github&style=flat-square" />
<a href="https://rahuldkjain.github.io/github-profile-readme-generator" target="blank">
<img src="https://img.shields.io/website?url=https%3A%2F%2Frahuldkjain.github.io%2Fgithub-profile-readme-generator&logo=github&style=flat-square" />
</a>
Try the tool: [GitHub Profile README Generator](https://rahuldkjain.github.io/gh-profile-readme-generator)
Try the tool: [GitHub Profile README Generator](https://rahuldkjain.github.io/github-profile-readme-generator)
## 🧐 Features
@@ -114,7 +114,7 @@ npm install
4. Run the app
```bash
npm start
npm run dev
```
🌟 You are all set!
@@ -127,9 +127,12 @@ Please read [`CONTRIBUTING`](CONTRIBUTING.md) for details on our [`CODE OF CONDU
## 💻 Built with
- [Gatsby](https://www.gatsbyjs.com/)
- [Tailwind CSS](https://tailwindcss.com/): for styling
- [GSAP](https://greensock.com/gsap/): for small SVG Animations
- [Next.js 15](https://nextjs.org/) - React framework with App Router
- [TypeScript](https://www.typescriptlang.org/) - Type safety and better DX
- [Tailwind CSS](https://tailwindcss.com/) - Utility-first CSS framework
- [Framer Motion](https://www.framer.com/motion/) - Production-ready motion library
- [Lucide React](https://lucide.dev/) - Beautiful & consistent icons
- [React Hook Form](https://react-hook-form.com/) - Performant forms with easy validation
## 🙇 Special Thanks
@@ -147,6 +150,27 @@ Please read [`CONTRIBUTING`](CONTRIBUTING.md) for details on our [`CODE OF CONDU
- [Aadit Kamat](https://github.com/aaditkamat) find the tool useful and showed support with his donation. A big thanks to him.
- [Jean-Michel Fayard](https://github.com/jmfayard) used the generator to create his GitHub Profile README and he loved it. Thanks to him for showing support to the tool with the donation.
## 🔒 Privacy & Analytics
This tool includes privacy-friendly analytics to help improve the user experience:
- **Google Analytics 4** with GDPR-compliant consent management
- **IP anonymization** and privacy-first configuration
- **Custom events** tracking for GitHub auto-fill, README generation, and exports
- **Cookie consent banner** - users can opt-out anytime
- **No personal data** collection - only anonymous usage patterns
## 📄 Font Licensing
This project uses the **Wotfard** font family:
- **Font**: Wotfard Regular
- **Usage**: This font is used under fair use for open source projects
- **Source**: Downloaded from online typography resources
- **Note**: If you're the font creator and have concerns about usage, please [contact us](mailto:rahuldkjain@gmail.com)
For commercial use of this project, please verify font licensing requirements.
## 🙏 Support
<p align="left">
-1
View File
@@ -1 +0,0 @@
module.exports = 'test-file-stub';
-19
View File
@@ -1,19 +0,0 @@
/* eslint-disable no-undef */
const React = require('react');
const gatsby = jest.requireActual('gatsby');
module.exports = {
...gatsby,
graphql: jest.fn(),
Link: jest.fn().mockImplementation(
// these props are invalid for an `a` tag
({ activeClassName, activeStyle, getProps, innerRef, partiallyActive, ref, replace, to, ...rest }) =>
React.createElement('a', {
...rest,
href: to,
}),
),
StaticQuery: jest.fn(),
useStaticQuery: jest.fn(),
};
+27
View File
@@ -0,0 +1,27 @@
# GitHub Profile README Generator - Environment Configuration
# Google Analytics 4 Configuration
# Get your GA4 Measurement ID from Google Analytics
# Format: G-XXXXXXXXXX
NEXT_PUBLIC_GA_ID=G-XXXXXXXXXX
# Optional: Google Search Console Verification
# Get this from Google Search Console -> Settings -> Ownership verification
NEXT_PUBLIC_GOOGLE_SITE_VERIFICATION=your-verification-code
# Privacy & GDPR Compliance
# Set to 'true' to require explicit consent before loading analytics
# Set to 'false' to load analytics by default (not GDPR compliant)
NEXT_PUBLIC_REQUIRE_CONSENT=true
# Analytics Configuration
# Set to 'true' to anonymize IP addresses (recommended for privacy)
NEXT_PUBLIC_ANONYMIZE_IP=true
# Development Configuration
# Set to 'development' to disable analytics in development mode
NODE_ENV=development
# Optional: Custom Domain Configuration
# Update if deploying to a custom domain
NEXT_PUBLIC_SITE_URL=https://your-domain.com
+44
View File
@@ -0,0 +1,44 @@
import { dirname } from "path";
import { fileURLToPath } from "url";
import { FlatCompat } from "@eslint/eslintrc";
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
const compat = new FlatCompat({
baseDirectory: __dirname,
});
const eslintConfig = [
...compat.extends("next/core-web-vitals", "next/typescript", "prettier"),
{
ignores: [
"node_modules/**",
".next/**",
"out/**",
"build/**",
"dist/**",
"coverage/**",
"*.config.js",
"*.config.ts",
"next-env.d.ts",
"old-gatsby-backup/**",
],
},
{
rules: {
"@typescript-eslint/no-unused-vars": [
"warn",
{
argsIgnorePattern: "^_",
varsIgnorePattern: "^_",
},
],
"@typescript-eslint/no-explicit-any": "warn",
"react/no-unescaped-entities": "off",
"react-hooks/exhaustive-deps": "warn",
},
},
];
export default eslintConfig;
-2
View File
@@ -1,2 +0,0 @@
import './src/styles/tailwind.css';
require('prismjs/themes/prism-okaidia.css');
-72
View File
@@ -1,72 +0,0 @@
module.exports = {
pathPrefix: `/gh-profile-readme-generator`,
siteMetadata: {
title: `GitHub Profile Readme Generator`,
description: `Prettify your github profile using this amazing readme generator.`,
author: `@rahuldkjain`,
},
plugins: [
`gatsby-plugin-react-helmet`,
{
resolve: `gatsby-source-filesystem`,
options: {
name: `images`,
path: `${__dirname}/src/images`,
},
},
{
resolve: `gatsby-source-filesystem`,
options: {
name: `markdown-pages`,
path: `${__dirname}/src/markdown-pages`,
},
},
{
resolve: `gatsby-transformer-remark`,
options: {
plugins: [`gatsby-remark-prismjs`],
},
},
`gatsby-transformer-sharp`,
`gatsby-plugin-sharp`,
{
resolve: `gatsby-plugin-manifest`,
options: {
name: `gatsby-starter-default`,
short_name: `starter`,
start_url: `/`,
background_color: `#663399`,
theme_color: `#663399`,
display: `minimal-ui`,
icon: `src/images/mdg.png`, // This path is relative to the root of the site.
},
},
{
resolve: `gatsby-plugin-google-analytics`,
options: {
trackingId: 'UA-168596085-3',
// this option places the tracking script into the head of the DOM
head: true,
// other options
},
},
{
resolve: `gatsby-plugin-postcss`,
options: {
postCssPlugins: [require('tailwindcss')],
},
},
{
resolve: `gatsby-plugin-purgecss`,
options: {
printRejected: false,
develop: false,
tailwind: true,
},
},
`gatsby-plugin-twitter`,
],
// this (optional) plugin enables Progressive Web App + Offline functionality
// To learn more, visit: https://gatsby.dev/offline
// `gatsby-plugin-offline`,
};
-36
View File
@@ -1,36 +0,0 @@
exports.createPages = async ({ actions, graphql, reporter }) => {
const { createPage } = actions;
const blogPostTemplate = require.resolve(`./src/templates/blogTemplate.js`);
const result = await graphql(`
{
allMarkdownRemark(sort: { order: DESC, fields: [frontmatter___date] }, limit: 1000) {
edges {
node {
frontmatter {
slug
}
}
}
}
}
`);
// Handle errors
if (result.errors) {
reporter.panicOnBuild(`Error while running GraphQL query.`);
return;
}
result.data.allMarkdownRemark.edges.forEach(({ node }) => {
createPage({
path: node.frontmatter.slug,
component: blogPostTemplate,
context: {
// additional data can be passed via context
slug: node.frontmatter.slug,
},
});
});
};
-5
View File
@@ -1,5 +0,0 @@
const babelOptions = {
presets: ['babel-preset-gatsby'],
};
module.exports = require('babel-jest').createTransformer(babelOptions);
-26
View File
@@ -1,26 +0,0 @@
module.exports = {
transform: {
'^.+\\.jsx?$': `<rootDir>/jest-preprocess.js`,
},
moduleNameMapper: {
'.+\\.(css|styl|less|sass|scss)$': `identity-obj-proxy`,
'.+\\.(jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$': `<rootDir>/__mocks__/file-mock.js`,
},
testPathIgnorePatterns: [`node_modules`, `\\.cache`, `<rootDir>.*/public`],
transformIgnorePatterns: [`node_modules/(?!(gatsby)/)`],
globals: {
__PATH_PREFIX__: ``,
__BASE_PATH__: ``,
},
setupFiles: [`<rootDir>/loadershim.js`],
setupFilesAfterEnv: ['<rootDir>/setupTests.js'],
snapshotSerializers: ['enzyme-to-json/serializer'],
coverageThreshold: {
global: {
branches: 0,
functions: 75,
lines: 68,
statements: 68,
},
},
};
-3
View File
@@ -1,3 +0,0 @@
global.___loader = {
enqueue: jest.fn(),
};
+115
View File
@@ -0,0 +1,115 @@
import type { NextConfig } from 'next';
import { PHASE_PRODUCTION_BUILD } from 'next/constants';
const nextConfig = (phase: string): NextConfig => {
// Determine if we should use basePath (production build, not Surge preview)
const isProductionBuild = phase === PHASE_PRODUCTION_BUILD;
const isSurgePreview = process.env.SURGE_PREVIEW === 'true';
const shouldUseBasePath = isProductionBuild && !isSurgePreview;
const basePath = shouldUseBasePath ? '/github-profile-readme-generator' : '';
return {
// Output as static site for GitHub Pages
output: 'export',
// Base path for GitHub Pages (only for production builds, not Surge previews)
basePath,
// Asset prefix to ensure all assets use the correct path
assetPrefix: shouldUseBasePath ? '/github-profile-readme-generator/' : '',
// Environment variables
env: {
NEXT_PUBLIC_BASE_PATH: basePath,
},
// Image optimization for static export
images: {
unoptimized: true, // Required for static export
},
// Trailing slashes for better compatibility
trailingSlash: true,
// Enable strict mode for better error catching
reactStrictMode: true,
// Enable experimental features for better performance
experimental: {
// Optimize CSS
optimizeCss: true,
// Enable optimized package imports for heavy libraries
optimizePackageImports: [
'framer-motion',
'@hookform/resolvers',
'react-markdown',
'remark-gfm',
'rehype-raw',
'rehype-sanitize',
'zod',
'zustand',
'lucide-react',
'@headlessui/react',
],
},
// Compiler options for better performance
compiler: {
// Remove console.log in production
removeConsole: isProductionBuild ? { exclude: ['error', 'warn'] } : false,
// Enable React compiler optimizations
reactRemoveProperties: isProductionBuild,
},
// Optimize transpilation
transpilePackages: ['react-markdown', 'remark-gfm', 'rehype-raw', 'rehype-sanitize'],
// Turbopack configuration (replaces webpack config)
turbopack: {
// Enable faster module resolution
resolveAlias: {
// Optimize common imports
'@': './src',
},
},
// Webpack optimizations for development (only when not using Turbopack)
webpack: (config, { dev, isServer }) => {
if (dev && !isServer && !process.env.TURBOPACK) {
// Optimize development builds
config.optimization = {
...config.optimization,
splitChunks: {
chunks: 'all',
cacheGroups: {
vendor: {
test: /[\\/]node_modules[\\/]/,
name: 'vendors',
chunks: 'all',
priority: 10,
},
markdown: {
test: /[\\/]node_modules[\\/](react-markdown|remark-|rehype-)/,
name: 'markdown',
chunks: 'all',
priority: 20,
},
},
},
};
// Enable faster rebuilds
config.cache = {
type: 'filesystem',
buildDependencies: {
config: [__filename],
},
};
}
return config;
},
};
};
export default nextConfig;
+9223 -67246
View File
File diff suppressed because it is too large Load Diff
+49 -81
View File
@@ -1,91 +1,59 @@
{
"name": "github-profile-readme-generator",
"version": "2.0.0",
"description": "Generate GitHub profile README easily with the latest add-ons like visitors count, GitHub stats, etc using minimal UI",
"private": true,
"description": "A simple react app to generate beautiful github profile readme in md(markdown)",
"version": "1.2.0",
"author": "Rahul Jain <rahuldkjain@gmail.com>",
"husky": {
"hooks": {
"pre-commit": "lint-staged"
}
},
"lint-staged": {
"*.{js,jsx}": [
"prettier --write",
"eslint --fix",
"git add"
],
"*.{html,css,less,ejs}": [
"prettier --write",
"git add"
]
"scripts": {
"dev": "TURBOPACK=1 next dev --turbo",
"build": "next build --turbo",
"export": "next build --turbo",
"start": "next start",
"type-check": "tsc --noEmit",
"lint": "eslint",
"format": "prettier --write \"src/**/*.{ts,tsx,js,jsx,json,css,md}\"",
"format:check": "prettier --check \"src/**/*.{ts,tsx,js,jsx,json,css,md}\"",
"test": "vitest",
"test:ui": "vitest --ui",
"test:coverage": "vitest --coverage"
},
"dependencies": {
"@primer/octicons-react": "^10.0.0",
"axios": "^0.24.0",
"enzyme": "^3.11.0",
"enzyme-adapter-react-16": "^1.15.5",
"enzyme-to-json": "^3.6.1",
"gatsby": "^2.23.12",
"gatsby-image": "^2.4.9",
"gatsby-plugin-google-analytics": "^2.3.11",
"gatsby-plugin-manifest": "^2.4.14",
"gatsby-plugin-offline": "^3.2.13",
"gatsby-plugin-react-helmet": "^3.3.6",
"gatsby-plugin-sharp": "2.6.14",
"gatsby-remark-prismjs": "^3.5.10",
"gatsby-source-filesystem": "^2.3.23",
"gatsby-transformer-remark": "^2.8.27",
"gatsby-transformer-sharp": "^2.5.7",
"gsap": "^3.4.0",
"prismjs": "^1.25.0",
"prop-types": "^15.7.2",
"react": "^16.12.0",
"react-dom": "^16.12.0",
"react-helmet": "^6.1.0"
"@headlessui/react": "^2.2.9",
"@hookform/resolvers": "^5.2.2",
"@next/third-parties": "^15.5.4",
"@tailwindcss/typography": "^0.5.19",
"critters": "^0.0.23",
"framer-motion": "^12.23.24",
"lucide-react": "^0.545.0",
"next": "15.5.4",
"react": "19.1.0",
"react-dom": "19.1.0",
"react-hook-form": "^7.65.0",
"react-markdown": "^10.1.0",
"rehype-raw": "^7.0.0",
"rehype-sanitize": "^6.0.0",
"remark-gfm": "^4.0.1",
"zod": "^4.1.12",
"zustand": "^5.0.8"
},
"devDependencies": {
"babel-jest": "26.3.0",
"babel-preset-gatsby": "0.5.11",
"eslint": "^7.32.0",
"eslint-config-airbnb": "^18.2.1",
"eslint-config-prettier": "^8.3.0",
"eslint-plugin-import": "^2.25.2",
"eslint-plugin-jsx-a11y": "^6.4.1",
"eslint-plugin-prettier": "^4.0.0",
"eslint-plugin-react": "^7.26.1",
"eslint-plugin-react-hooks": "^4.2.0",
"gatsby-plugin-postcss": "^2.3.11",
"gatsby-plugin-purgecss": "^5.0.0",
"gatsby-plugin-twitter": "^2.3.10",
"gatsby-remark-embedder": "^3.0.0",
"gh-pages": "^3.1.0",
"husky": "^7.0.4",
"identity-obj-proxy": "3.0.0",
"jest": "26.4.2",
"lint-staged": "^11.2.6",
"prettier": "2.0.5",
"tailwindcss": "^1.7.6"
},
"keywords": [
"gatsby"
],
"license": "0BSD",
"scripts": {
"build": "gatsby build",
"develop": "gatsby develop",
"format": "prettier --write \"**/*.{js,jsx,ts,tsx,json,md}\"",
"start": "npm run develop",
"serve": "gatsby serve",
"clean": "gatsby clean",
"test": "jest -i -u --coverage",
"deploy": "gatsby build --prefix-paths && gh-pages -d public -b master"
},
"repository": {
"type": "git",
"url": "https://github.com/rahuldkjain/github-profile-readme-generator"
},
"bugs": {
"url": "https://github.com/rahuldkjain/github-profile-readme-generator/issues"
"@eslint/eslintrc": "^3",
"@tailwindcss/postcss": "^4",
"@testing-library/jest-dom": "^6.9.1",
"@testing-library/react": "^16.3.0",
"@testing-library/user-event": "^14.6.1",
"@types/node": "^20",
"@types/react": "^19",
"@types/react-dom": "^19",
"@vitejs/plugin-react": "^5.0.4",
"eslint": "^9",
"eslint-config-next": "15.5.4",
"eslint-config-prettier": "^10.1.8",
"jsdom": "^27.0.0",
"prettier": "^3.6.2",
"prettier-plugin-tailwindcss": "^0.6.14",
"tailwindcss": "^4",
"typescript": "^5",
"vitest": "^3.2.4"
}
}
+5
View File
@@ -0,0 +1,5 @@
const config = {
plugins: ["@tailwindcss/postcss"],
};
export default config;
BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.
+29
View File
@@ -0,0 +1,29 @@
{
"name": "GitHub Profile README Generator",
"short_name": "GitHub README Gen",
"description": "Create amazing GitHub profile READMEs in seconds with customizable templates and easy-to-use interface",
"start_url": "./",
"display": "standalone",
"background_color": "#ffffff",
"theme_color": "#000000",
"orientation": "portrait-primary",
"icons": [
{
"src": "/mdg.png",
"sizes": "192x192",
"type": "image/png",
"purpose": "maskable any"
},
{
"src": "/mdg.png",
"sizes": "512x512",
"type": "image/png",
"purpose": "maskable any"
}
],
"categories": ["developer", "productivity", "utilities"],
"lang": "en",
"dir": "ltr",
"scope": "./",
"prefer_related_applications": false
}
BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

+9
View File
@@ -0,0 +1,9 @@
# https://www.robotstxt.org/robotstxt.html
User-agent: *
Allow: /
# Host
Host: https://rahuldkjain.github.io
# Sitemaps
Sitemap: https://rahuldkjain.github.io/gh-profile-readme-generator/sitemap.xml
+196
View File
@@ -0,0 +1,196 @@
import { Header } from '@/components/layout/header';
import { Footer } from '@/components/layout/footer';
import type { Metadata } from 'next';
import Image from 'next/image';
export const metadata: Metadata = {
title: 'About',
description:
'Learn about GitHub Profile README Generator - an open-source tool for creating awesome GitHub profile READMEs with customizable templates, skills showcase, and social links integration.',
alternates: {
canonical: '/about',
},
openGraph: {
title: 'About | GitHub Profile README Generator',
description:
'Learn about GitHub Profile README Generator - an open-source tool for creating awesome GitHub profile READMEs',
url: '/about',
},
};
export default function AboutPage() {
return (
<div className="flex min-h-screen flex-col">
<Header />
<main className="container mx-auto flex-1 px-4 py-12">
<div className="page-content mx-auto max-w-4xl">
<h1 className="mb-6 text-4xl font-bold">👨💻 About</h1>
<div className="mb-8 flex gap-2">
<a
href="https://github.com/rahuldkjain/github-profile-readme-generator/blob/master/LICENSE"
target="_blank"
rel="noopener noreferrer"
>
<Image
src="https://img.shields.io/github/license/rahuldkjain/github-profile-readme-generator?style=flat-square"
alt="github-profile-readme-generator license"
width={100}
height={100}
/>
</a>
</div>
<p className="text-lg">
<strong>GitHub Profile README Generator</strong> is an OSS (Open Source Software) that
provides a cool interface to generate GitHub profile README in markdown.
</p>
<p>
The tool aims to provide hassle-free experience to add trending addons like profile{' '}
<strong>visitors count</strong>, <strong>github-stats</strong>,{' '}
<strong>dynamic blog posts</strong> etc.
</p>
<p>
The profile should be neat and minimal to give a clear overview of the work. Non-uniform
icons, too much content, too much images/gifs distracts visitors to see your actual
work.
</p>
<p>To solve this, GitHub Profile README Generator came into existence.</p>
<p>
So many developers contributed to the project and made it more awesome to use. You can
contribute too to make it grow even further.
</p>
<div className="my-6 flex gap-3">
<a
href="https://github.com/rahuldkjain/github-profile-readme-generator/issues"
target="_blank"
rel="noopener noreferrer"
>
<Image
src="https://img.shields.io/github/issues/rahuldkjain/github-profile-readme-generator?style=flat-square"
alt="github-profile-readme-generator issues"
width={100}
height={100}
/>
</a>
<a
href="https://github.com/rahuldkjain/github-profile-readme-generator/pulls"
target="_blank"
rel="noopener noreferrer"
>
<Image
src="https://img.shields.io/github/issues-pr/rahuldkjain/github-profile-readme-generator?style=flat-square"
alt="github-profile-readme-generator pull-requests"
width={130}
height={130}
/>
</a>
</div>
<h3 className="mt-8 mb-4 text-2xl font-bold">Contributors 🙏</h3>
<p>List of the developers who contributed to the project. A big shout out for them.</p>
<a
href="https://github.com/rahuldkjain/github-profile-readme-generator/graphs/contributors"
target="_blank"
rel="noopener noreferrer"
>
<Image
src="https://contributors-img.web.app/image?repo=rahuldkjain/github-profile-readme-generator"
alt="Contributors"
className="my-4"
width={600}
height={600}
/>
</a>
<hr className="my-8" />
<h2 className="mb-4 text-3xl font-bold">How do I create a profile README?</h2>
<p>
The profile README is created by creating a new repository that's the same name as your
username. For example, my GitHub username is <strong>rahuldkjain</strong> so I created a
new repository with the name <strong>rahuldkjain</strong>. Note: at the time of this
writing, in order to access the profile README feature, the letter-casing must match
your GitHub username.
</p>
<ol className="list-decimal space-y-3 pl-6">
<li>
Create a new repository with the same name (including casing) as your GitHub username:{' '}
<a
href="https://github.com/new"
target="_blank"
rel="noopener noreferrer"
className="text-primary hover:underline"
>
https://github.com/new
</a>
</li>
<li>
Create a README.md file inside the new repo with content (text, GIFs, images, emojis,
etc.)
</li>
<li>
Commit your fancy new README! If you're on GitHub's web interface you can choose to
commit directly to the repo's main branch (i.e., master or main) which will make it
immediately visible on your profile
</li>
<li>
Push changes to GitHub (if you made changes locally i.e., on your computer and not
github.com)
</li>
</ol>
<hr className="my-8" />
<h2 className="mb-4 text-3xl font-bold">How to use?</h2>
<p>
Tired of editing profile README(.md) to add new features like visitors-count badge,
github-stats etc?
</p>
<p>Don't worry. Keep calm, fill the form and let the tool do the work for you</p>
<Image
src="/demo.gif"
alt="github profile readme generator"
width="320"
height={100}
className="my-6"
/>
<hr className="my-8" />
<h2 className="mb-4 text-3xl font-bold">Why visitors count keeps on increasing?</h2>
<p>
So many users raised an issue that the counter keeps on increasing everytime the page
reloads.
</p>
<p>
Well it is visitors count not "unique" visitors count. The goal of the addon is to
provide a good stat of how well the github profile is doing.
</p>
<p>
Proper use or misuse of the addon is the sole responsibility of the user. The developer
of the addon is working on it to fix this issue.
</p>
</div>
</main>
<Footer />
</div>
);
}
+298
View File
@@ -0,0 +1,298 @@
import { Header } from '@/components/layout/header';
import { Footer } from '@/components/layout/footer';
import type { Metadata } from 'next';
export const metadata: Metadata = {
title: 'Addons',
description:
'Discover the awesome open-source addons and tools used in GitHub Profile README Generator. Explore the technology stack and libraries that power this amazing tool.',
alternates: {
canonical: '/addons',
},
openGraph: {
title: 'Addons | GitHub Profile README Generator',
description:
'Discover the awesome open-source addons and tools used in GitHub Profile README Generator',
url: '/addons',
},
};
export default function AddonsPage() {
return (
<div className="flex min-h-screen flex-col">
<Header />
<main className="container mx-auto flex-1 px-4 py-12">
<div className="page-content mx-auto max-w-4xl">
<h1 className="mb-6 text-4xl font-bold">🚀 Addons</h1>
<p className="text-lg">
GitHub Profile README Generator tool uses few open-source addons developed by other
developers. Including such features makes the tool useful. The developers of this tool
is very grateful to use these awesome addons.
</p>
<hr className="my-8" />
<h2 className="text-3xl font-bold">
<a
href="https://github.com/anuraghazra/github-readme-stats"
target="_blank"
rel="noopener noreferrer"
className="text-primary hover:underline"
>
GitHub README Stats
</a>
</h2>
<p> Dynamically generated stats for your github readmes</p>
<h4 className="mt-6 mb-3 text-xl font-semibold">GitHub Stats Card</h4>
<a href="https://github.com/rahuldkjain" target="_blank" rel="noopener noreferrer">
<img
src="https://github-readme-stats.vercel.app/api?username=rahuldkjain&show_icons=true"
width="320"
alt="Rahul's github stats"
/>
</a>
<h4 className="mt-6 mb-3 text-xl font-semibold">Top Skills Card</h4>
<a href="https://github.com/rahuldkjain" target="_blank" rel="noopener noreferrer">
<img
src="https://github-readme-stats.vercel.app/api/top-langs/?username=rahuldkjain&layout=compact&hide=html"
width="320"
alt="Rahul's github top skills"
/>
</a>
<p>
Developed by{' '}
<a
href="https://github.com/anuraghazra"
target="_blank"
rel="noopener noreferrer"
className="text-primary hover:underline"
>
Anurag Hazra
</a>
.
</p>
<p>
You can customize the theme too. See how to customize yours{' '}
<a
href="https://github.com/anuraghazra/github-readme-stats"
target="_blank"
rel="noopener noreferrer"
className="text-primary hover:underline"
>
here
</a>
</p>
<hr className="my-8" />
<h2 className="text-3xl font-bold">
<a
href="https://github.com/DenverCoder1/github-readme-streak-stats"
target="_blank"
rel="noopener noreferrer"
className="text-primary hover:underline"
>
GitHub Readme Streak Stats
</a>
</h2>
<p>
Stay motivated while contributing to open source by displaying your current contribution
streak
</p>
<img
src="https://github-readme-streak-stats.herokuapp.com/?user=rahuldkjain"
alt="rahuldkjain"
className="my-4"
/>
<p>
Developed by{' '}
<a
href="https://github.com/DenverCoder1"
target="_blank"
rel="noopener noreferrer"
className="text-primary hover:underline"
>
Jonah Lawrence
</a>
.
</p>
<p>
See how to customize the theme{' '}
<a
href="https://github.com/DenverCoder1/github-readme-streak-stats"
target="_blank"
rel="noopener noreferrer"
className="text-primary hover:underline"
>
here
</a>
</p>
<hr className="my-8" />
<h2 className="text-3xl font-bold">
<a
href="https://github.com/antonkomarev/github-profile-views-counter"
target="_blank"
rel="noopener noreferrer"
className="text-primary hover:underline"
>
GitHub Profile Views Counter
</a>
</h2>
<p>
It counts how many times your GitHub profile has been viewed. Free cloud micro-service.
</p>
<img
src="https://komarev.com/ghpvc/?username=rahuldkjain&style=flat-square"
alt="rahuldkjain"
className="my-4"
/>
<p>
Developed by{' '}
<a
href="https://github.com/antonkomarev"
target="_blank"
rel="noopener noreferrer"
className="text-primary hover:underline"
>
Anton Komarev
</a>
.
</p>
<p>
You can customize the color, label and style too. See how to customize{' '}
<a
href="https://github.com/antonkomarev/github-profile-views-counter"
target="_blank"
rel="noopener noreferrer"
className="text-primary hover:underline"
>
here
</a>
</p>
<hr className="my-8" />
<h2 className="text-3xl font-bold">
<a
href="https://github.com/gautamkrishnar/blog-post-workflow"
target="_blank"
rel="noopener noreferrer"
className="text-primary hover:underline"
>
Dynamic Latest Blog Posts
</a>
</h2>
<p>
Show your latest blog posts from any sources (like dev.to, medium etc) or StackOverflow
activity on your GitHub profile/project readme automatically using the RSS feed.
</p>
<img
src="https://user-images.githubusercontent.com/8397274/88047382-29b8b280-cb6f-11ea-9efb-2af2b10f3e0c.png"
width="320"
alt="dynamic latest blog example"
className="my-4"
/>
<p>
Developed by{' '}
<a
href="https://github.com/gautamkrishnar"
target="_blank"
rel="noopener noreferrer"
className="text-primary hover:underline"
>
Gautam Krishna R
</a>
</p>
<h3 className="mt-6 mb-3 text-2xl font-semibold">How to use</h3>
<ul className="list-disc space-y-3 pl-6">
<li>Go to your repository</li>
<li>
Add the following section to your **README.md** file, you can give whatever title you
want. Just make sure that you use **&lt;!-- BLOG-POST-LIST:START --&gt;&lt;!--
BLOG-POST-LIST:END --&gt;** in your readme. The workflow will replace this comment
with the actual blog post list:
</li>
</ul>
<pre className="my-4 overflow-x-auto rounded-lg bg-slate-800 p-4 text-sm text-white dark:bg-slate-900">
<code>{`# Blog posts
<!-- BLOG-POST-LIST:START -->
<!-- BLOG-POST-LIST:END -->`}</code>
</pre>
<ul className="list-disc space-y-3 pl-6">
<li>
Create a folder named <code>.github</code> and create <code>workflows</code> folder
inside it if it doesn't exist.
</li>
<li>
Create a new file named <code>blog-post-workflow.yml</code> with the following
contents inside the workflows folder:
</li>
</ul>
<pre className="my-4 overflow-x-auto rounded-lg bg-slate-800 p-4 text-sm text-white dark:bg-slate-900">
<code>{`name: Latest blog post workflow
on:
schedule:
# Runs every hour
- cron: '0 * * * *'
jobs:
update-readme-with-blog:
name: Update this repo's README with latest blog posts
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: gautamkrishnar/blog-post-workflow@master
with:
feed_list: 'https://dev.to/feed/rahuldkjain, https://medium.com/feed/@rahuldkjain'`}</code>
</pre>
<ul className="list-disc space-y-3 pl-6">
<li>Replace the above url list with your own rss feed urls.</li>
<li>Commit and wait for it to run</li>
</ul>
<p>
To know more, check out the{' '}
<a
href="https://github.com/gautamkrishnar/blog-post-workflow"
target="_blank"
rel="noopener noreferrer"
className="text-primary hover:underline"
>
official github repository
</a>
</p>
</div>
</main>
<Footer />
</div>
);
}
+417
View File
@@ -0,0 +1,417 @@
@import 'tailwindcss';
@plugin '@tailwindcss/typography';
/* Screen reader only utility */
.sr-only {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
white-space: nowrap;
border-width: 0;
}
:root {
/* Light mode colors */
--background: #ffffff;
--foreground: #171717;
--card: #f9fafb;
--card-foreground: #171717;
--primary: #2563eb;
--primary-foreground: #ffffff;
--secondary: #64748b;
--secondary-foreground: #ffffff;
--muted: #f1f5f9;
--muted-foreground: #64748b;
--accent: #f1f5f9;
--accent-foreground: #0f172a;
--destructive: #ef4444;
--destructive-foreground: #ffffff;
--border: #e2e8f0;
--input: #e2e8f0;
--ring: #2563eb;
--radius: 0.5rem;
}
@theme inline {
--color-background: var(--background);
--color-foreground: var(--foreground);
--color-card: var(--card);
--color-card-foreground: var(--card-foreground);
--color-primary: var(--primary);
--color-primary-foreground: var(--primary-foreground);
--color-secondary: var(--secondary);
--color-secondary-foreground: var(--secondary-foreground);
--color-muted: var(--muted);
--color-muted-foreground: var(--muted-foreground);
--color-accent: var(--accent);
--color-accent-foreground: var(--accent-foreground);
--color-destructive: var(--destructive);
--color-destructive-foreground: var(--destructive-foreground);
--color-border: var(--border);
--color-input: var(--input);
--color-ring: var(--ring);
}
@media (prefers-color-scheme: dark) {
:root {
/* Dark mode colors */
--background: #0a0a0a;
--foreground: #ededed;
--card: #171717;
--card-foreground: #ededed;
--primary: #3b82f6;
--primary-foreground: #ffffff;
--secondary: #94a3b8;
--secondary-foreground: #0f172a;
--muted: #1e293b;
--muted-foreground: #94a3b8;
--accent: #1e293b;
--accent-foreground: #f1f5f9;
--destructive: #dc2626;
--destructive-foreground: #ffffff;
--border: #1e293b;
--input: #1e293b;
--ring: #3b82f6;
}
}
/* Manual light mode class override (to override system preference) */
.light {
--background: #ffffff;
--foreground: #171717;
--card: #f9fafb;
--card-foreground: #171717;
--primary: #2563eb;
--primary-foreground: #ffffff;
--secondary: #64748b;
--secondary-foreground: #ffffff;
--muted: #f1f5f9;
--muted-foreground: #64748b;
--accent: #f1f5f9;
--accent-foreground: #0f172a;
--destructive: #ef4444;
--destructive-foreground: #ffffff;
--border: #e2e8f0;
--input: #e2e8f0;
--ring: #2563eb;
}
/* Manual dark mode class override */
.dark {
--background: #0a0a0a;
--foreground: #ededed;
--card: #171717;
--card-foreground: #ededed;
--primary: #3b82f6;
--primary-foreground: #ffffff;
--secondary: #94a3b8;
--secondary-foreground: #0f172a;
--muted: #1e293b;
--muted-foreground: #94a3b8;
--accent: #1e293b;
--accent-foreground: #f1f5f9;
--destructive: #dc2626;
--destructive-foreground: #ffffff;
--border: #1e293b;
--input: #1e293b;
--ring: #3b82f6;
}
* {
border-color: var(--border);
}
body {
background: var(--background);
color: var(--foreground);
font-family: var(--font-sans, Arial, Helvetica, sans-serif);
}
/* Optimized transitions for theme changes - only target necessary elements */
html {
transition: background-color 100ms ease-out;
}
body {
transition:
background-color 100ms ease-out,
color 100ms ease-out;
}
/* Target specific UI elements for faster transitions */
[class*='bg-'],
[class*='text-'],
[class*='border-'] {
transition:
background-color 100ms ease-out,
color 100ms ease-out,
border-color 100ms ease-out;
}
/* SVG icons transition */
svg {
transition:
fill 100ms ease-out,
stroke 100ms ease-out,
color 100ms ease-out;
}
/* Button and interactive elements */
button,
a,
input,
select,
textarea {
transition:
background-color 100ms ease-out,
color 100ms ease-out,
border-color 100ms ease-out,
opacity 100ms ease-out;
}
/* Cards and containers */
[class*='card'],
[class*='bg-card'] {
transition:
background-color 100ms ease-out,
border-color 100ms ease-out;
}
/* Optimize theme switching performance */
html.theme-switching * {
transition: none !important;
}
html.theme-switching {
transition: none !important;
}
/* Respect reduced motion preferences */
@media (prefers-reduced-motion: reduce) {
* {
transition: none !important;
animation: none !important;
}
}
/* Reduced motion override via settings */
:root[style*='--motion-reduce'] * {
transition: none !important;
animation: none !important;
}
/* Markdown Preview Styles */
.markdown-preview {
color: var(--foreground);
}
.markdown-preview h1,
.markdown-preview h2,
.markdown-preview h3,
.markdown-preview h4,
.markdown-preview h5,
.markdown-preview h6 {
color: var(--foreground);
}
.markdown-preview p {
color: var(--foreground);
line-height: 1.6;
}
.markdown-preview strong,
.markdown-preview b {
color: var(--foreground);
font-weight: 600;
}
.markdown-preview img {
display: inline-block;
vertical-align: middle;
}
.markdown-preview a img {
margin: 0;
}
/* Custom page content styles to override prose */
.page-content {
color: var(--foreground);
}
.page-content h1,
.page-content h2,
.page-content h3,
.page-content h4,
.page-content h5,
.page-content h6 {
color: var(--foreground);
line-height: 1.2;
margin-bottom: 1rem;
}
.page-content h1 {
font-size: 2.25rem;
font-weight: 700;
}
.page-content h2 {
font-size: 1.875rem;
font-weight: 700;
}
.page-content h3 {
font-size: 1.5rem;
font-weight: 600;
}
.page-content h4 {
font-size: 1.25rem;
font-weight: 600;
}
.page-content p {
color: var(--foreground);
line-height: 1.6;
margin-bottom: 1rem;
}
.page-content strong,
.page-content b {
color: var(--foreground);
font-weight: 600;
}
.page-content a {
color: var(--primary);
text-decoration: underline;
}
.page-content a:hover {
text-decoration: none;
}
.page-content ul,
.page-content ol {
color: var(--foreground);
margin-bottom: 1rem;
padding-left: 1.5rem;
}
.page-content li {
margin-bottom: 0.5rem;
}
.page-content blockquote {
color: var(--foreground);
border-left: 4px solid var(--primary);
padding-left: 1rem;
margin: 1.5rem 0;
font-style: italic;
}
.page-content hr {
border-color: var(--border);
margin: 2rem 0;
}
.page-content pre {
background-color: var(--muted);
color: var(--foreground);
padding: 1rem;
border-radius: 0.5rem;
overflow-x: auto;
margin: 1rem 0;
}
.page-content code {
background-color: var(--muted);
color: var(--foreground);
padding: 0.125rem 0.25rem;
border-radius: 0.25rem;
font-size: 0.875rem;
}
.page-content img {
max-width: 100%;
height: auto;
margin: 1rem 0;
}
/* High Contrast Mode */
.high-contrast {
--background: #000000;
--foreground: #ffffff;
--card: #000000;
--card-foreground: #ffffff;
--primary: #ffff00;
--primary-foreground: #000000;
--secondary: #00ffff;
--secondary-foreground: #000000;
--muted: #333333;
--muted-foreground: #ffffff;
--accent: #00ff00;
--accent-foreground: #000000;
--destructive: #ff0000;
--destructive-foreground: #ffffff;
--border: #ffffff;
--input: #ffffff;
--ring: #ffff00;
}
.high-contrast * {
border-width: 2px;
}
.high-contrast button,
.high-contrast a,
.high-contrast input,
.high-contrast textarea,
.high-contrast select {
outline: 2px solid var(--foreground);
outline-offset: 2px;
}
/* Form placeholder opacity - reduce to avoid looking like filled fields */
input::placeholder,
textarea::placeholder,
select::placeholder {
opacity: 0.4;
}
/* Font Size Variants */
.text-small {
font-size: 14px;
}
.text-small h1 {
font-size: 1.75rem;
}
.text-small h2 {
font-size: 1.5rem;
}
.text-small h3 {
font-size: 1.25rem;
}
.text-large {
font-size: 18px;
}
.text-large h1 {
font-size: 2.5rem;
}
.text-large h2 {
font-size: 2rem;
}
.text-large h3 {
font-size: 1.75rem;
}
+162
View File
@@ -0,0 +1,162 @@
import type { Metadata } from 'next';
import { Roboto_Mono } from 'next/font/google';
import localFont from 'next/font/local';
import './globals.css';
import { ThemeProvider } from '@/components/layout/theme-provider';
import { ToastProvider } from '@/components/ui/toast';
import { BuyMeACoffeeWidget } from '@/components/ui/buy-me-coffee';
import { ConditionalAnalytics } from '@/components/analytics/conditional-analytics';
import { CookieConsent } from '@/components/ui/cookie-consent';
import { getAssetPath } from '@/lib/asset-path';
const robotoMono = Roboto_Mono({
variable: '--font-mono',
subsets: ['latin'],
weight: ['400', '500', '600', '700'],
});
const wotfard = localFont({
src: '../../public/fonts/wotfard/Wotfard-Regular.woff',
variable: '--font-sans',
weight: '400',
});
export const metadata: Metadata = {
title: {
default: 'GitHub Profile README Generator - Create Amazing Profile in Seconds',
template: '%s | GitHub Profile README Generator',
},
description:
'The best profile README generator to create an amazing GitHub profile in seconds. Customize your profile with skills, social links, stats, and more. Free, open-source, and easy to use.',
keywords: [
'github',
'profile',
'readme',
'generator',
'markdown',
'github profile',
'readme generator',
'github readme',
'profile generator',
'github stats',
'github badges',
'developer profile',
'github profile maker',
'readme maker',
],
authors: [{ name: 'Rahul Jain', url: 'https://github.com/rahuldkjain' }],
creator: 'Rahul Jain',
publisher: 'Rahul Jain',
formatDetection: {
email: false,
address: false,
telephone: false,
},
metadataBase: new URL('https://rahuldkjain.github.io/gh-profile-readme-generator/'),
alternates: {
canonical: '/',
},
openGraph: {
title: 'GitHub Profile README Generator - Create Amazing Profile in Seconds',
description:
'Create an amazing GitHub profile README in seconds with customizable templates and easy-to-use interface. Add skills, social links, GitHub stats, and more.',
url: 'https://rahuldkjain.github.io/gh-profile-readme-generator/',
siteName: 'GitHub Profile README Generator',
locale: 'en_US',
type: 'website',
images: [
{
url: '/og-image.png',
width: 1200,
height: 630,
alt: 'GitHub Profile README Generator - Create Amazing Profile in Seconds',
},
],
},
twitter: {
card: 'summary_large_image',
title: 'GitHub Profile README Generator',
description:
'Create an amazing GitHub profile README in seconds with customizable templates. Free and easy to use.',
creator: '@rahuldkjain',
images: ['/og-image.png'],
},
icons: {
icon: [
{ url: '/favicon.ico', sizes: 'any' },
{ url: getAssetPath('/mdg.png'), type: 'image/png' },
],
apple: getAssetPath('/mdg.png'),
},
manifest: getAssetPath('/manifest.json'),
robots: {
index: true,
follow: true,
googleBot: {
index: true,
follow: true,
'max-video-preview': -1,
'max-image-preview': 'large',
'max-snippet': -1,
},
},
verification: {
google: 'google-site-verification-code', // User will need to add their code
},
};
export default function RootLayout({
children,
}: Readonly<{
children: React.ReactNode;
}>) {
const jsonLd = {
'@context': 'https://schema.org',
'@type': 'WebApplication',
name: 'GitHub Profile README Generator',
description:
'Create an amazing GitHub profile README in seconds with customizable templates and easy-to-use interface.',
url: 'https://rahuldkjain.github.io/gh-profile-readme-generator/',
applicationCategory: 'DeveloperApplication',
operatingSystem: 'Any',
offers: {
'@type': 'Offer',
price: '0',
priceCurrency: 'USD',
},
author: {
'@type': 'Person',
name: 'Rahul Jain',
url: 'https://github.com/rahuldkjain',
},
aggregateRating: {
'@type': 'AggregateRating',
ratingValue: '4.8',
ratingCount: '1000',
},
};
return (
<html lang="en" suppressHydrationWarning>
<head>
{/* Favicon and manifest are now handled by Next.js metadata API above */}
<meta name="theme-color" content="#000000" />
<meta name="apple-mobile-web-app-capable" content="yes" />
<meta name="apple-mobile-web-app-status-bar-style" content="default" />
<meta name="apple-mobile-web-app-title" content="GitHub README Gen" />
<script
type="application/ld+json"
dangerouslySetInnerHTML={{ __html: JSON.stringify(jsonLd) }}
/>
</head>
<body className={`${wotfard.variable} ${robotoMono.variable} font-sans antialiased`}>
<ThemeProvider>
<ToastProvider>{children}</ToastProvider>
</ThemeProvider>
<BuyMeACoffeeWidget />
<ConditionalAnalytics />
<CookieConsent />
</body>
</html>
);
}
+633
View File
@@ -0,0 +1,633 @@
'use client';
import { useState, useEffect, useMemo, lazy, Suspense, useCallback } from 'react';
import { useForm } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
import { motion } from 'framer-motion';
import { Download } from 'lucide-react';
import { profileSchema, linksSchema, socialSchema } from '@/lib/validations';
import { DEFAULT_DATA, DEFAULT_LINK, DEFAULT_SOCIAL } from '@/constants/defaults';
import { initialSkillState } from '@/constants/skills';
import { BasicInfoSection } from '@/components/sections/basic-info-section';
import { LinksSection } from '@/components/sections/links-section';
import { SocialSection } from '@/components/sections/social-section';
import { generateMarkdown } from '@/lib/markdown-generator';
import { saveFormData, loadFormData, clearFormData } from '@/lib/storage';
import type { ProfileFormData, LinksFormData, SocialFormData } from '@/lib/validations';
import { DEFAULT_SUPPORT } from '@/constants/defaults';
import { Header } from '@/components/layout/header';
import { Footer } from '@/components/layout/footer';
import { useErrorToast, useSuccessToast } from '@/components/ui/toast';
import { trackReadmeGenerated, trackFileExported } from '@/lib/analytics';
import { useConfirmDialog } from '@/components/ui/confirm-dialog';
// Lazy load heavy components
const SkillsSection = lazy(() =>
import('@/components/sections/skills-section').then((module) => ({
default: module.SkillsSection,
}))
);
const MarkdownPreview = lazy(() =>
import('@/components/ui/markdown-preview').then((module) => ({ default: module.MarkdownPreview }))
);
type Step = 'basic' | 'links' | 'social' | 'skills' | 'preview';
const steps: { id: Step; title: string; description: string }[] = [
{ id: 'basic', title: 'Basic Info', description: 'Tell us about yourself' },
{ id: 'links', title: 'Links', description: 'Portfolio, blog, resume' },
{ id: 'social', title: 'Social', description: 'Social media profiles' },
{ id: 'skills', title: 'Skills', description: 'Technologies you know' },
{ id: 'preview', title: 'Preview', description: 'Review and generate' },
];
export default function GeneratorPage() {
// Toast hooks
const showError = useErrorToast();
const showSuccess = useSuccessToast();
const { showConfirm, ConfirmDialog } = useConfirmDialog();
// Load saved data FIRST before any state initialization
const savedData = useMemo(() => {
if (typeof window === 'undefined') return null;
const data = loadFormData();
console.log('🎯 Initial load - Saved data:', data);
return data;
}, []); // Empty deps - only run once on mount
const [currentStep, setCurrentStep] = useState<Step>('basic');
const [skills, setSkills] = useState(() => {
// Lazy initialization - use saved skills if available
const initialSkills = savedData?.skills || initialSkillState;
console.log(
'🎯 Initial skills state:',
Object.values(initialSkills).filter(Boolean).length,
'selected'
);
return initialSkills;
});
const [lastSaved, setLastSaved] = useState<Date | null>(() => {
if (savedData?.lastSaved) {
return new Date(savedData.lastSaved);
}
return null;
});
const [saveStatus, setSaveStatus] = useState<'idle' | 'saving' | 'saved'>('idle');
const [hasInitialized, setHasInitialized] = useState(false);
const {
register: registerProfile,
formState: { errors: profileErrors },
watch: watchProfile,
reset: resetProfile,
trigger: triggerProfile,
} = useForm<ProfileFormData>({
resolver: zodResolver(profileSchema),
defaultValues: savedData?.profile ? { ...DEFAULT_DATA, ...savedData.profile } : DEFAULT_DATA,
mode: 'onChange',
});
const {
register: registerLinks,
formState: { errors: linksErrors },
watch: watchLinks,
reset: resetLinks,
trigger: triggerLinks,
} = useForm<LinksFormData>({
resolver: zodResolver(linksSchema),
defaultValues: savedData?.links ? { ...DEFAULT_LINK, ...savedData.links } : DEFAULT_LINK,
mode: 'onChange',
});
const {
register: registerSocial,
formState: { errors: socialErrors },
watch: watchSocial,
reset: resetSocial,
trigger: triggerSocial,
} = useForm<SocialFormData>({
resolver: zodResolver(socialSchema),
defaultValues: savedData?.social ? { ...DEFAULT_SOCIAL, ...savedData.social } : DEFAULT_SOCIAL,
mode: 'onChange',
});
// Watch all form values for live preview
const profileData = watchProfile();
const linksData = watchLinks();
const socialData = watchSocial();
// Generate markdown with useMemo to prevent unnecessary recalculations
const markdown = useMemo(() => {
return generateMarkdown({
profile: profileData,
links: linksData,
social: socialData,
support: DEFAULT_SUPPORT,
skills,
});
}, [profileData, linksData, socialData, skills]);
// Mark as initialized after first render to enable auto-save
useEffect(() => {
console.log('🔍 Mount - Data already loaded in initialization');
if (savedData) {
console.log('✅ Mount - Restored from localStorage automatically');
} else {
console.log('🆕 Mount - Starting fresh (no saved data)');
}
// Set initialized to true after a brief delay to ensure forms are fully set up
const timer = setTimeout(() => {
console.log('🎬 Initialization complete - Auto-save now enabled');
setHasInitialized(true);
}, 100);
return () => clearTimeout(timer);
}, [savedData]);
// Auto-save form data - only after initialization complete
useEffect(() => {
// Skip until initialization is complete
if (!hasInitialized) {
console.log('⏭️ Auto-save - Waiting for initialization to complete');
return;
}
console.log('💾 Auto-save - Starting...');
console.log('📊 Auto-save - Profile data:', profileData);
console.log('📊 Auto-save - Links data:', linksData);
console.log('📊 Auto-save - Social data:', socialData);
console.log('📊 Auto-save - Skills selected:', Object.values(skills).filter(Boolean).length);
setSaveStatus('saving');
const timer = setTimeout(() => {
const now = new Date();
const dataToSave = {
profile: profileData,
links: linksData,
social: socialData,
support: DEFAULT_SUPPORT,
skills,
lastSaved: now.toISOString(),
};
console.log('💾 Auto-save - Saving to localStorage:', dataToSave);
saveFormData(dataToSave);
// Verify it was saved
const savedDataCheck = localStorage.getItem('github-profile-generator');
console.log('✅ Auto-save - Verified in localStorage:', savedDataCheck ? 'YES' : 'NO');
console.log('📏 Auto-save - Data size:', savedDataCheck?.length || 0, 'bytes');
setLastSaved(now);
setSaveStatus('saved');
// Reset to idle after animation
setTimeout(() => setSaveStatus('idle'), 2000);
}, 1000); // Save 1 second after last change
return () => clearTimeout(timer);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [
hasInitialized,
JSON.stringify(profileData),
JSON.stringify(linksData),
JSON.stringify(socialData),
JSON.stringify(skills),
]);
const handleSkillChange = (skill: string, checked: boolean) => {
setSkills((prev) => ({ ...prev, [skill]: checked }));
};
const handleGitHubAutoFill = (data: {
profile: Partial<ProfileFormData>;
links: Partial<LinksFormData>;
social: Partial<SocialFormData>;
skills: string[];
}) => {
// Update profile data
if (data.profile.title) {
resetProfile((prev) => ({ ...prev, ...data.profile }));
}
// Update links data
if (data.links.blog) {
resetLinks((prev) => ({ ...prev, ...data.links }));
}
// Update social data
if (data.social.github || data.social.twitter) {
resetSocial((prev) => ({ ...prev, ...data.social }));
}
// Update skills
if (data.skills.length > 0) {
const newSkills = { ...skills };
data.skills.forEach((skill) => {
if (skill in newSkills) {
newSkills[skill] = true;
}
});
setSkills(newSkills);
}
};
// Restore is now automatic on mount, but keep this for manual restore if needed
// This function is no longer needed but kept for backwards compatibility
// Check if there's any meaningful data to clear
const hasAnyData = useMemo(() => {
// Check profile data (excluding empty strings)
const hasProfileData = Object.entries(profileData).some(([key, value]) => {
if (key === 'subtitle' && value === '') return false; // Empty subtitle is now default
return typeof value === 'string' ? value.trim() !== '' : value !== false && value !== null;
});
// Check links data
const hasLinksData = Object.values(linksData).some((value) => value && value.trim() !== '');
// Check social data
const hasSocialData = Object.values(socialData).some((value) =>
typeof value === 'string' ? value.trim() !== '' : value === true
);
// Check skills data
const hasSkillsData = Object.values(skills).some((selected) => selected === true);
return hasProfileData || hasLinksData || hasSocialData || hasSkillsData;
}, [profileData, linksData, socialData, skills]);
const handleClearAll = useCallback(() => {
showConfirm({
title: 'Clear All Data',
message:
'Are you sure you want to clear all data? This will reset all form fields, skills, and settings. This action cannot be undone.',
confirmText: 'Clear All',
cancelText: 'Cancel',
variant: 'warning',
onConfirm: () => {
clearFormData();
resetProfile(DEFAULT_DATA);
resetLinks(DEFAULT_LINK);
resetSocial(DEFAULT_SOCIAL);
setSkills(initialSkillState);
setLastSaved(null);
setSaveStatus('idle');
showSuccess('All data cleared successfully', 'Form has been reset to default values');
},
});
}, [showConfirm, resetProfile, resetLinks, resetSocial, setSkills, showSuccess]);
const handleDownloadJSON = () => {
const data = {
version: '1.0.0',
exportedAt: new Date().toISOString(),
profile: profileData,
links: linksData,
social: socialData,
support: DEFAULT_SUPPORT,
skills: Object.entries(skills)
.filter(([_, selected]) => selected)
.map(([skill]) => skill),
};
const blob = new Blob([JSON.stringify(data, null, 2)], { type: 'application/json' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `github-profile-${new Date().getTime()}.json`;
document.body.appendChild(a);
a.click();
// Track JSON export
trackFileExported('json_export', 'json');
document.body.removeChild(a);
URL.revokeObjectURL(url);
};
const handleImportJSON = (event: React.ChangeEvent<HTMLInputElement>) => {
const file = event.target.files?.[0];
if (!file) return;
const reader = new FileReader();
reader.onload = (e) => {
try {
const imported = JSON.parse(e.target?.result as string);
// Validate and import data
if (imported.profile) {
resetProfile({ ...DEFAULT_DATA, ...imported.profile } as ProfileFormData);
}
if (imported.links) {
resetLinks({ ...DEFAULT_LINK, ...imported.links } as LinksFormData);
}
if (imported.social) {
resetSocial({ ...DEFAULT_SOCIAL, ...imported.social } as SocialFormData);
}
if (imported.skills && Array.isArray(imported.skills)) {
const newSkills = { ...initialSkillState };
imported.skills.forEach((skill: string) => {
if (skill in newSkills) {
newSkills[skill] = true;
}
});
setSkills(newSkills);
}
alert('Profile data imported successfully!');
} catch (error) {
alert('Error importing JSON: ' + (error as Error).message);
}
};
reader.readAsText(file);
// Reset input
event.target.value = '';
};
const currentStepIndex = steps.findIndex((s) => s.id === currentStep);
// Validate current step before navigation
const validateCurrentStep = async (): Promise<boolean> => {
let isValid = true;
const errorMessages: string[] = [];
switch (currentStep) {
case 'basic':
const profileValid = await triggerProfile();
if (!profileValid) {
isValid = false;
// Get specific error messages
if (profileErrors.title) {
errorMessages.push(`Name: ${profileErrors.title.message}`);
}
// Add other field errors as needed
Object.entries(profileErrors).forEach(([field, error]) => {
if (field !== 'title' && error?.message) {
errorMessages.push(`${field}: ${error.message}`);
}
});
}
break;
case 'links':
const linksValid = await triggerLinks();
if (!linksValid) {
isValid = false;
Object.entries(linksErrors).forEach(([field, error]) => {
if (error?.message) {
errorMessages.push(`${field}: ${error.message}`);
}
});
}
break;
case 'social':
const socialValid = await triggerSocial();
if (!socialValid) {
isValid = false;
Object.entries(socialErrors).forEach(([field, error]) => {
if (error?.message) {
errorMessages.push(`${field}: ${error.message}`);
}
});
}
break;
case 'skills':
// Skills don't have validation requirements
break;
case 'preview':
// Preview doesn't need validation
break;
}
if (!isValid) {
const stepName = steps.find((s) => s.id === currentStep)?.title || 'current step';
showError(
`Please fix errors in ${stepName}`,
errorMessages.length > 0 ? errorMessages.join(', ') : 'Please check all required fields'
);
}
return isValid;
};
const goToNextStep = async () => {
// Validate current step before proceeding
const isValid = await validateCurrentStep();
if (!isValid) {
return; // Don't proceed if validation fails
}
const nextIndex = currentStepIndex + 1;
if (nextIndex < steps.length) {
setCurrentStep(steps[nextIndex].id);
// Show success message for completing a step
const currentStepName = steps[currentStepIndex].title;
showSuccess(`${currentStepName} completed!`, 'Moving to next step');
// Track README generation completion when reaching preview step
if (steps[nextIndex].id === 'preview') {
const socialData = watchSocial();
const linksData = watchLinks();
const selectedSkillsCount = Object.values(skills).filter(Boolean).length;
trackReadmeGenerated({
hasSkills: selectedSkillsCount > 0,
hasSocial: Object.values(socialData).some(
(value) => typeof value === 'string' && value.trim() !== ''
),
hasLinks: Object.values(linksData).some(
(value) => typeof value === 'string' && value.trim() !== ''
),
stepCount: currentStepIndex + 1,
});
}
}
};
const goToPrevStep = () => {
const prevIndex = currentStepIndex - 1;
if (prevIndex >= 0) {
setCurrentStep(steps[prevIndex].id);
}
};
return (
<div className="flex min-h-screen flex-col">
{/* Header with Save Status */}
<Header saveStatus={saveStatus} lastSaved={lastSaved} />
<main className="container mx-auto flex-1 px-4 py-8">
<div className="mx-auto max-w-6xl">
{/* Progress Steps - Responsive */}
<nav aria-label="Form progress" className="mb-8">
<div className="flex items-center justify-center overflow-x-auto px-4">
<div className="flex min-w-max items-center">
{steps.map((step, index) => (
<div key={step.id} className="flex items-center">
<button
onClick={() => setCurrentStep(step.id)}
className={`flex flex-col items-center gap-1 px-2 py-1 ${
currentStep === step.id
? 'text-primary'
: index < currentStepIndex
? 'text-foreground'
: 'text-muted-foreground'
}`}
aria-label={`Step ${index + 1}: ${step.title}`}
aria-current={currentStep === step.id ? 'step' : undefined}
>
<div
className={`flex h-8 w-8 items-center justify-center rounded-full border-2 transition-colors sm:h-10 sm:w-10 ${
currentStep === step.id
? 'border-primary bg-primary text-primary-foreground'
: index < currentStepIndex
? 'border-primary bg-primary/20'
: 'border-border'
}`}
>
<span className="text-xs font-medium sm:text-sm">{index + 1}</span>
</div>
<div className="hidden text-center sm:block">
<p className="text-xs font-medium whitespace-nowrap">{step.title}</p>
<p className="text-muted-foreground text-xs whitespace-nowrap">
{step.description}
</p>
</div>
</button>
{index < steps.length - 1 && (
<div
className={`mx-2 h-0.5 w-8 sm:mx-4 sm:w-12 ${
index < currentStepIndex ? 'bg-primary' : 'bg-border'
}`}
/>
)}
</div>
))}
</div>
</div>
{/* Screen reader announcement for step changes */}
<div className="sr-only" aria-live="polite" aria-atomic="true">
Current step: {steps[currentStepIndex].title} - {steps[currentStepIndex].description}
</div>
</nav>
{/* Form Content */}
<motion.div
key={currentStep}
initial={{ opacity: 0, x: 20 }}
animate={{ opacity: 1, x: 0 }}
exit={{ opacity: 0, x: -20 }}
transition={{ duration: 0.3 }}
className="border-border bg-card rounded-lg border p-6 shadow-sm md:p-8"
>
{currentStep === 'basic' && (
<BasicInfoSection
register={registerProfile}
errors={profileErrors}
socialRegister={registerSocial}
watchSocial={watchSocial}
onGitHubAutoFill={handleGitHubAutoFill}
onImportJSON={handleImportJSON}
onClearAll={handleClearAll}
hasClearableData={hasAnyData}
/>
)}
{currentStep === 'links' && (
<LinksSection register={registerLinks} errors={linksErrors} />
)}
{currentStep === 'social' && (
<SocialSection register={registerSocial} errors={socialErrors} watch={watchSocial} />
)}
{currentStep === 'skills' && (
<Suspense
fallback={
<div className="animate-pulse space-y-4">
<div className="h-8 rounded bg-gray-200"></div>
<div className="grid grid-cols-2 gap-4 md:grid-cols-3 lg:grid-cols-4">
{Array.from({ length: 12 }).map((_, i) => (
<div key={i} className="h-12 rounded bg-gray-200"></div>
))}
</div>
</div>
}
>
<SkillsSection
selectedSkills={skills}
onSkillChange={handleSkillChange}
registerProfile={registerProfile}
/>
</Suspense>
)}
{currentStep === 'preview' && (
<div className="space-y-6">
<div className="border-border border-b pb-4">
{/* Mobile: Stack vertically, Desktop: Side by side */}
<div className="flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between">
<div>
<h2 className="text-xl font-bold sm:text-2xl">Preview & Generate</h2>
<p className="text-muted-foreground mt-1 text-sm">
Your README is ready! Copy or download it below.
</p>
</div>
{/* Export Button - With text */}
<button
onClick={handleDownloadJSON}
className="bg-primary text-primary-foreground hover:bg-primary/90 flex items-center justify-center gap-2 rounded-lg px-3 py-2 text-sm transition-colors"
title="Export profile data as JSON"
aria-label="Export profile data as JSON"
>
<Download className="h-4 w-4" />
<span>Export</span>
</button>
</div>
</div>
<Suspense
fallback={
<div className="animate-pulse space-y-4">
<div className="h-8 rounded bg-gray-200"></div>
<div className="h-96 rounded bg-gray-200"></div>
</div>
}
>
<MarkdownPreview markdown={markdown} title="Your GitHub Profile README" />
</Suspense>
</div>
)}
</motion.div>
{/* Navigation Buttons */}
<nav className="mt-6 flex justify-between" aria-label="Form navigation">
<button
onClick={goToPrevStep}
disabled={currentStepIndex === 0}
className="border-border hover:bg-accent rounded-lg border px-6 py-2 font-medium transition-colors disabled:cursor-not-allowed disabled:opacity-50"
aria-label={`Go to previous step${currentStepIndex > 0 ? `: ${steps[currentStepIndex - 1].title}` : ''}`}
>
Previous
</button>
{/* Hide Next button at preview step since we're already at the end */}
{currentStepIndex < steps.length - 1 && (
<button
onClick={goToNextStep}
className="bg-primary text-primary-foreground hover:bg-primary/90 rounded-lg px-6 py-2 font-medium transition-colors"
aria-label={`Go to next step: ${steps[currentStepIndex + 1].title}`}
>
Next
</button>
)}
</nav>
</div>
</main>
<Footer />
{/* Confirmation Dialog */}
<ConfirmDialog />
</div>
);
}
+18
View File
@@ -0,0 +1,18 @@
import { MetadataRoute } from 'next';
export const dynamic = 'force-static';
export default function robots(): MetadataRoute.Robots {
const baseUrl = 'https://rahuldkjain.github.io/gh-profile-readme-generator';
return {
rules: [
{
userAgent: '*',
allow: '/',
disallow: [],
},
],
sitemap: `${baseUrl}/sitemap.xml`,
};
}
+34
View File
@@ -0,0 +1,34 @@
import { MetadataRoute } from 'next';
export const dynamic = 'force-static';
export default function sitemap(): MetadataRoute.Sitemap {
const baseUrl = 'https://rahuldkjain.github.io/gh-profile-readme-generator';
return [
{
url: baseUrl,
lastModified: new Date(),
changeFrequency: 'weekly',
priority: 1.0,
},
{
url: `${baseUrl}/about`,
lastModified: new Date(),
changeFrequency: 'monthly',
priority: 0.7,
},
{
url: `${baseUrl}/addons`,
lastModified: new Date(),
changeFrequency: 'monthly',
priority: 0.7,
},
{
url: `${baseUrl}/support`,
lastModified: new Date(),
changeFrequency: 'monthly',
priority: 0.6,
},
];
}
+141
View File
@@ -0,0 +1,141 @@
import { Header } from '@/components/layout/header';
import { Footer } from '@/components/layout/footer';
import type { Metadata } from 'next';
export const metadata: Metadata = {
title: 'Support',
description:
'Support the development of GitHub Profile README Generator and help make it better. Learn how to contribute, report issues, and sponsor the project.',
alternates: {
canonical: '/support',
},
openGraph: {
title: 'Support | GitHub Profile README Generator',
description:
'Support the development of GitHub Profile README Generator and help make it better',
url: '/support',
},
};
export default function SupportPage() {
return (
<div className="flex min-h-screen flex-col">
<Header />
<main className="container mx-auto flex-1 px-4 py-12">
<div className="page-content mx-auto max-w-4xl">
<h1 className="mb-6 text-4xl font-bold">💵 Support OSS</h1>
<blockquote className="border-primary border-l-4 pl-4 italic">
Think of giving not as a duty but as a privilege - John D. Rockefeller Jr.
</blockquote>
<p className="text-lg">
🚀 GitHub Profile README Generator tool is free and will always be free. Numerous
developers has put their time and efforts to make this tool more powerful. However,
these developers are doing their full time job along with open-source contributions.
</p>
<p>
You can come forward to support the developers by making small donations. You will never
know what this support mean to them. If you find the tool really helpful, then it will
be very grateful to support the tool 🙇.
</p>
<div className="my-6 flex flex-wrap gap-3">
<a
href="https://www.paypal.me/rahuldkjain/10"
target="_blank"
rel="noopener noreferrer"
>
<img src="https://ionicabizau.github.io/badges/paypal.svg" alt="PayPal" />
</a>
<a href="https://ko-fi.com/A0A81XXSX" target="_blank" rel="noopener noreferrer">
<img
height="23"
width="100"
src="https://cdn.ko-fi.com/cdn/kofi3.png?v=2"
alt="Buy Coffee for rahuldkjain"
/>
</a>
<a
href="https://www.buymeacoffee.com/rahuldkjain"
target="_blank"
rel="noopener noreferrer"
>
<img
src="https://cdn.buymeacoffee.com/buttons/default-orange.png"
alt="Buy Me A Coffee"
height="23"
width="100"
style={{ borderRadius: '2px' }}
/>
</a>
</div>
<hr className="my-8" />
<h2 className="mb-4 text-3xl font-bold">Social Support 🤝</h2>
<a
href="https://twitter.com/intent/tweet?text=Wow:&url=https%3A%2F%2Frahuldkjain.github.io%2Fgithub-profile-readme-generator"
target="_blank"
rel="noopener noreferrer"
>
<img
src="https://img.shields.io/twitter/url?style=social&url=https%3A%2F%2Frahuldkjain.github.io%2Fgithub-profile-readme-generator"
alt="tweet github profile readme generator"
/>
</a>
<p>Let the world know how you feel using this tool. Share with others on twitter.</p>
<hr className="my-8" />
<h2 className="mb-4 text-3xl font-bold">Sponsors 🙏</h2>
<ul className="space-y-3">
<li>
<a
href="https://github.com/scottcwilson"
target="_blank"
rel="noopener noreferrer"
className="text-primary hover:underline"
>
Scott C Wilson
</a>{' '}
donated the first ever grant to this tool. A big thanks to him.
</li>
<li>
<a
href="https://github.com/mxschmitt"
target="_blank"
rel="noopener noreferrer"
className="text-primary hover:underline"
>
Max Schmitt
</a>{' '}
loved the tool and showed the support with his donation. Thanks a lot.
</li>
</ul>
<hr className="my-8" />
<div className="bg-primary/5 border-primary/20 rounded-lg border p-6">
<h3 className="mb-3 text-xl font-semibold">Other Ways to Support</h3>
<ul className="space-y-2">
<li> Star the project on GitHub</li>
<li>🐛 Report bugs and issues</li>
<li>💡 Suggest new features</li>
<li>🔧 Contribute code via pull requests</li>
<li>📢 Share the tool with your network</li>
<li>📝 Write articles or tutorials about the tool</li>
</ul>
</div>
</div>
</main>
<Footer />
</div>
);
}
File diff suppressed because it is too large Load Diff
@@ -1,101 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Donate renders correctly 1`] = `
<Fragment>
<div
className="text-center text-4xl my-2"
>
Support 
<span
aria-label="praying hand emoji"
role="img"
>
🙏
</span>
</div>
<div
className="flex flex-col sm:flex-row items-start justify-between"
>
<div
className="w-full sm:w-2/3"
>
<div
className="text-2xl mb-2"
>
Are you using the tool and happy with it to create your GitHub Profile?
</div>
<div
className="text-lg"
>
Your kind support keeps open-source tools like this free for others.
</div>
<div
className="mt-4"
>
<a
className="flex items-center justify-start w-20"
href="https://twitter.com/intent/tweet?text=Wow:&url=https%3A%2F%2Frahuldkjain.github.io%2Fgithub-profile-readme-generator"
>
<img
alt="tweet github profile readme generator"
className="w-20"
src="https://img.shields.io/twitter/url?style=social&url=https%3A%2F%2Frahuldkjain.github.io%2Fgithub-profile-readme-generator"
/>
</a>
Let the world know how you feel using this tool. Share with others on twitter.
</div>
</div>
<div
className="w-full sm:w-1/3 flex flex-col justify-center items-center"
>
<span>
Tip
<span
aria-label="Dollar medal"
role="img"
>
💰
</span>
</span>
<a
className="flex items-center justify-evenly bg-red-500 text-white py-2 px-4 my-2"
href="https://ko-fi.com/A0A81XXSX"
rel="noreferrer"
target="_blank"
>
<img
alt="Buy ko-fi for rahuldkjain"
className="w-6 h-6 mr-2"
src="https://www.vectorlogo.zone/logos/ko-fi/ko-fi-icon.svg"
/>
Buy me a ko-fi
</a>
<a
className="flex items-center justify-evenly bg-white-500 text-white py-2 px-4 my-2 border border-solid"
href="https://www.paypal.me/rahuldkjain/10"
rel="noreferrer"
target="_blank"
>
<img
alt="Donate rahuldkjain via paypal"
className="w-32 h-4"
src="https://cdn.worldvectorlogo.com/logos/paypal-2.svg"
/>
</a>
<a
className="flex items-center justify-evenly bg-orange-500 text-white py-2 px-4 my-2"
href="https://www.buymeacoffee.com/rahuldkjain"
rel="noreferrer"
target="_blank"
>
<img
alt="Buy rahuldkjain A Coffee"
className="w-6 h-6 mr-2"
src="https://www.vectorlogo.zone/logos/buymeacoffee/buymeacoffee-icon.svg"
/>
Buy me a coffee
</a>
</div>
</div>
</Fragment>
`;
@@ -1,189 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Footer component renders correctly 1`] = `
<div
className="bg-gray-100 p-4 flex flex-col justify-center items-center shadow-inner mt-2"
>
<div
className="w-full flex flex-col sm:flex-row justify-evenly py-2"
>
<div
className="sm:ml-0 sm:mr-6 order-last sm:order-none flex"
>
<h1
className="text-base font-bold font-title text-xl sm:text-2xl mt-3 sm:mt-0"
>
<div
className="flex sm:flex-col items-start mb-3 sm:mb-0"
>
<img
alt="github profile markdown generator logo"
className="hidden sm:block h-24"
src="test-file-stub"
/>
<div
className="mr-2 sm:mr-0"
>
GitHub Profile
<img
alt="github profile markdown generator logo"
className="inline sm:hidden h-12"
src="test-file-stub"
/>
<span
className="block sm:inline"
>
README Generator
</span>
</div>
</div>
</h1>
</div>
<div
className="text-xl sm:text-base font-light sm:font-normal"
>
<div
className="font-title font-bold mb-4 sm:mb-2"
>
<strong>
Pages
</strong>
</div>
<div
className="ml-2 sm:ml-0"
>
<mockConstructor
activeStyle={
Object {
"color": "#002ead",
}
}
to="/addons"
>
Addons
</mockConstructor>
</div>
<div
className="ml-2 sm:ml-0"
>
<mockConstructor
activeStyle={
Object {
"color": "#002ead",
}
}
to="/support"
>
Support
</mockConstructor>
</div>
<div
className="ml-2 sm:ml-0"
>
<mockConstructor
activeStyle={
Object {
"color": "#002ead",
}
}
to="/about"
>
About
</mockConstructor>
</div>
</div>
<div
className="text-xl sm:text-base font-light sm:font-normal"
>
<div
className="font-title font-bold my-4 sm:my-0 sm:mb-2"
>
<strong>
More
</strong>
</div>
<div
className="ml-2 sm:ml-0"
>
<a
aria-label="Github rahuldkjain/github-profile-readme-generator"
href="https://github.com/rahuldkjain/github-profile-readme-generator"
target="blank"
>
Github
</a>
</div>
<div
className="ml-2 sm:ml-0"
>
<a
aria-label="Releases on Github rahuldkjain/github-profile-readme-generator"
href="https://github.com/rahuldkjain/github-profile-readme-generator/releases"
target="blank"
>
Releases
</a>
</div>
<div
className="ml-2 sm:ml-0"
>
<a
aria-label="Issues in rahuldkjain/github-profile-readme-generator"
href="https://github.com/rahuldkjain/github-profile-readme-generator/issues"
target="blank"
>
Issues
</a>
</div>
<div
className="ml-2 sm:ml-0"
>
<a
aria-label="Pull Requests in rahuldkjain/github-profile-readme-generator"
href="https://github.com/rahuldkjain/github-profile-readme-generator/pulls"
target="blank"
>
Pull Requests
</a>
</div>
</div>
<div>
<div
className="font-title font-bold text-xl sm:text-base my-4 sm:my-0 sm:mb-2"
>
<strong>
Join Community
</strong>
</div>
<div
className="ml-2 sm:ml-0"
>
<a
aria-label="Discord of the community"
href="https://discord.gg/HHMs7Eg"
target="blank"
>
<img
alt="Discord of the community"
className="h-12"
src="test-file-stub"
/>
</a>
</div>
</div>
</div>
<div
className="py-2 mt-2"
>
Developed in India
<span
aria-label="india"
role="img"
>
🇮🇳
</span>
</div>
</div>
`;
@@ -1,73 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Header renders correctly 1`] = `
<div
className="shadow flex items-center justify-center flex-col mb-2 py-2"
>
<mockConstructor
to="/"
>
<h1
className="text-base font-bold font-title sm:text-2xl font-medium text-blue-800 flex justify-center items-center flex-col"
>
<img
alt="github profile markdown generator logo"
className="w-12 h-12"
src="test-file-stub"
/>
<div>
heading
</div>
</h1>
</mockConstructor>
<div
className="flex justify-center items-center"
>
<a
aria-label="Star rahuldkjain/github-profile-readme-generator on GitHub"
className="mr-2"
href="https://github.com/rahuldkjain/github-profile-readme-generator"
target="blank"
>
<div
className="text-xxs sm:text-sm border-2 border-solid border-gray-900 bg-gray-100 flex items-center justify-center py-1 px-2"
>
<StarIcon
className="px-1 w-6 star"
id="star-icon"
size={16}
verticalAlign="text-bottom"
/>
Star this repo
<span
className="github-count px-1 sm:px-2"
>
0
</span>
</div>
</a>
<a
aria-label="Fork rahuldkjain/github-profile-readme-generator on GitHub"
href="https://github.com/rahuldkjain/github-profile-readme-generator/fork"
target="blank"
>
<div
className="text-xxs sm:text-sm border-2 border-solid border-gray-900 bg-gray-100 flex items-center justify-center py-1 px-2"
>
<RepoForkedIcon
className="px-1 w-6 fork"
id="fork-icon"
size={16}
verticalAlign="text-bottom"
/>
Fork on GitHub
<span
className="github-count px-1 sm:px-2"
>
0
</span>
</div>
</a>
</div>
</div>
`;
@@ -1,23 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Loader renders correctly 1`] = `
<div
className="loader"
>
<span>
</span>
<span>
</span>
<span>
</span>
<span>
</span>
<span>
</span>
</div>
`;
File diff suppressed because it is too large Load Diff
@@ -1,601 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`DisplaySocial Preview renders correctly 1`] = `
<a
className="no-underline text-blue-700 m-2"
href="https://codepen.io/dummy"
target="blank"
>
<img
alt="username"
className="w-6 h-6"
src="https://cdn.jsdelivr.net/npm/simple-icons@3.0.1/icons/codepen.svg"
/>
</a>
`;
exports[`DisplaySocial Preview renders correctly with no username 1`] = `""`;
exports[`DisplayWork Preview renders correctly 1`] = `
<div
className="my-2"
>
[object Object]
<a
className="no-underline text-blue-700"
href="https://dummy.com"
target="blank"
>
readme-generator
</a>
</div>
`;
exports[`DisplayWork Preview renders correctly with no link 1`] = `
<div
className="my-2"
>
[object Object]
<b>
readme-generator
</b>
</div>
`;
exports[`DisplayWork Preview renders correctly with no prefix 1`] = `""`;
exports[`DisplayWork Preview renders correctly with no prefix and link 1`] = `""`;
exports[`DisplayWork Preview renders correctly with no prefix, link and project 1`] = `""`;
exports[`DisplayWork Preview renders correctly with no project 1`] = `
<div
className="my-2"
>
[object Object]
<a
className="no-underline text-blue-700"
href="https://dummy.com"
target="blank"
>
https://dummy.com
</a>
</div>
`;
exports[`DisplayWork Preview renders correctly with no project and link 1`] = `""`;
exports[`DisplayWork Preview renders correctly with no project and prefix 1`] = `""`;
exports[`GitHubStats Preview renders correctly 1`] = `""`;
exports[`GitHubStats Preview renders correctly 2`] = `
<div
className="text-center mx-4 mb-4"
>
<img
alt=""
src="https://github-readme-stats.vercel.app/api?username=&show_icons=true&locale=en"
/>
</div>
`;
exports[`GithubProfileTrophy Preview renders correctly 1`] = `""`;
exports[`GithubProfileTrophy Preview renders correctly with show true 1`] = `
<div
className="text-left my-2"
>
<a
href="https://github.com/ryo-ma/github-profile-trophy"
>
<img
alt=""
src="https://github-profile-trophy.vercel.app/?username="
/>
</a>
</div>
`;
exports[`Markdown Preview renders correctly 1`] = `
<div
id="markdown-preview"
>
<TitlePreview
prefix="Hi 👋, I'm"
title="dummy"
/>
<SubTitlePreview
subtitle="A passionate frontend developer from India"
/>
<VisitorsBadgePreview
badgeOptions={
Object {
"badgeColor": "0e75b6",
"badgeLabel": "Profile%20views",
"badgeStyle": "flat",
}
}
github=""
show={false}
/>
<GithubProfileTrophyPreview
github=""
show={false}
/>
<TwitterBadgePreview
show={false}
twitter=""
/>
<WorkPreview
work={
Object {
"data": Object {
"ama": "",
"badgeColor": "0e75b6",
"badgeLabel": "Profile views",
"badgeStyle": "flat",
"collaborateOn": "",
"contact": "",
"currentLearn": "",
"currentWork": "readme-generator",
"devDynamicBlogs": false,
"funFact": "",
"githubProfileTrophy": false,
"githubStats": false,
"githubStatsOptions": Object {
"bgColor": "",
"cacheSeconds": null,
"hideBorder": false,
"locale": "en",
"textColor": "",
"theme": "",
"titleColor": "",
},
"helpWith": "",
"mediumDynamicBlogs": false,
"rssDynamicBlogs": false,
"subtitle": "A passionate frontend developer from India",
"title": "dummy",
"topLanguages": false,
"topLanguagesOptions": Object {
"bgColor": "",
"cacheSeconds": null,
"hideBorder": false,
"locale": "en",
"textColor": "",
"theme": "",
"titleColor": "",
},
"twitterBadge": false,
"visitorsBadge": false,
},
"link": Object {
"blog": "",
"collaborateOn": "",
"currentWork": "https://dummy.com",
"helpWith": "",
"portfolio": "",
"resume": "",
},
"prefix": Object {
"ama": "💬 Ask me about",
"blog": "📝 I regularly write articles on",
"collaborateOn": "👯 Im looking to collaborate on",
"contact": "📫 How to reach me",
"currentLearn": "🌱 Im currently learning",
"currentWork": "🔭 Im currently working on",
"funFact": "⚡ Fun fact",
"helpWith": "🤝 Im looking for help with",
"portfolio": "👨‍💻 All of my projects are available at",
"resume": "📄 Know about my experiences",
"title": "Hi 👋, I'm",
},
"skills": Object {},
"social": Object {
"behance": "",
"codechef": "",
"codeforces": "",
"codepen": "dummy",
"codesandbox": "",
"dev": "",
"discord": "",
"dribbble": "",
"fb": "",
"geeks_for_geeks": "",
"github": "",
"hackerearth": "",
"hackerrank": "",
"instagram": "",
"kaggle": "",
"leetcode": "",
"linkedin": "",
"medium": "",
"rssurl": "",
"stackoverflow": "",
"topcoder": "",
"twitter": "",
"youtube": "",
},
"support": Object {
"buyMeACoffee": "",
},
}
}
/>
<SocialPreview
social={
Object {
"behance": "",
"codechef": "",
"codeforces": "",
"codepen": "dummy",
"codesandbox": "",
"dev": "",
"discord": "",
"dribbble": "",
"fb": "",
"geeks_for_geeks": "",
"github": "",
"hackerearth": "",
"hackerrank": "",
"instagram": "",
"kaggle": "",
"leetcode": "",
"linkedin": "",
"medium": "",
"rssurl": "",
"stackoverflow": "",
"topcoder": "",
"twitter": "",
"youtube": "",
}
}
/>
<SkillsPreview
skills={Object {}}
/>
<SupportPreview
support={
Object {
"buyMeACoffee": "",
}
}
/>
<div
className="block sm:flex sm:justify-center sm:items-start"
>
<TopLanguagesPreview
github=""
options={
Object {
"bgColor": "",
"cacheSeconds": null,
"hideBorder": false,
"locale": "en",
"textColor": "",
"theme": "",
"titleColor": "",
}
}
show={false}
/>
<GitHubStatsPreview
github=""
options={
Object {
"bgColor": "",
"cacheSeconds": null,
"hideBorder": false,
"locale": "en",
"textColor": "",
"theme": "",
"titleColor": "",
}
}
show={false}
/>
<StreakStatsPreview
github=""
options={Object {}}
show={false}
/>
</div>
</div>
`;
exports[`SectionTitle Preview renders correctly 1`] = `
<h3
className="w-full text-lg sm:text-xl"
>
dummy
</h3>
`;
exports[`SectionTitle Preview renders correctly with no label 1`] = `""`;
exports[`SectionTitle Preview renders correctly with visible false 1`] = `""`;
exports[`Skills Preview renders correctly 1`] = `
<div
className="flex flex-wrap justify-start items-center"
>
<SectionTitle
label="Languages and Tools:"
visible={true}
/>
<a
href="https://unity.com/"
key="unity"
rel="noreferrer"
target="_blank"
>
<img
alt="unity"
className="mb-4 mr-4 h-6 w-6 sm:h-10 sm:w-10"
src="https://www.vectorlogo.zone/logos/unity3d/unity3d-icon.svg"
/>
</a>
</div>
`;
exports[`Skills Preview renders correctly with no skills 1`] = `""`;
exports[`Social Preview renders correctly 1`] = `
<div
className="flex justify-start items-end flex-wrap"
>
<SectionTitle
label="Connect with me:"
visible={true}
/>
<DisplaySocial
base="https://codepen.io"
icon="https://raw.githubusercontent.com/rahuldkjain/github-profile-readme-generator/master/src/images/icons/Social/codepen.svg"
username="dummy"
/>
<DisplaySocial
base="https://dev.to"
icon="https://raw.githubusercontent.com/rahuldkjain/github-profile-readme-generator/master/src/images/icons/Social/devto.svg"
username=""
/>
<DisplaySocial
base="https://twitter.com"
icon="https://raw.githubusercontent.com/rahuldkjain/github-profile-readme-generator/master/src/images/icons/Social/twitter.svg"
username=""
/>
<DisplaySocial
base="https://linkedin.com/in"
icon="https://raw.githubusercontent.com/rahuldkjain/github-profile-readme-generator/master/src/images/icons/Social/linked-in-alt.svg"
username=""
/>
<DisplaySocial
base="https://stackoverflow.com/users"
icon="https://raw.githubusercontent.com/rahuldkjain/github-profile-readme-generator/master/src/images/icons/Social/stack-overflow.svg"
username=""
/>
<DisplaySocial
base="https://codesandbox.com"
icon="https://raw.githubusercontent.com/rahuldkjain/github-profile-readme-generator/master/src/images/icons/Social/codesandbox.svg"
username=""
/>
<DisplaySocial
base="https://kaggle.com"
icon="https://raw.githubusercontent.com/rahuldkjain/github-profile-readme-generator/master/src/images/icons/Social/kaggle.svg"
username=""
/>
<DisplaySocial
base="https://fb.com"
icon="https://raw.githubusercontent.com/rahuldkjain/github-profile-readme-generator/master/src/images/icons/Social/facebook.svg"
username=""
/>
<DisplaySocial
base="https://instagram.com"
icon="https://raw.githubusercontent.com/rahuldkjain/github-profile-readme-generator/master/src/images/icons/Social/instagram.svg"
username=""
/>
<DisplaySocial
base="https://dribbble.com"
icon="https://raw.githubusercontent.com/rahuldkjain/github-profile-readme-generator/master/src/images/icons/Social/dribbble.svg"
username=""
/>
<DisplaySocial
base="https://www.behance.net"
icon="https://raw.githubusercontent.com/rahuldkjain/github-profile-readme-generator/master/src/images/icons/Social/behance.svg"
username=""
/>
<DisplaySocial
base="https://medium.com"
icon="https://raw.githubusercontent.com/rahuldkjain/github-profile-readme-generator/master/src/images/icons/Social/medium.svg"
username=""
/>
<DisplaySocial
base="https://www.youtube.com/c"
icon="https://raw.githubusercontent.com/rahuldkjain/github-profile-readme-generator/master/src/images/icons/Social/youtube.svg"
username=""
/>
<DisplaySocial
base="https://www.codechef.com/users"
icon="https://cdn.jsdelivr.net/npm/simple-icons@3.1.0/icons/codechef.svg"
username=""
/>
<DisplaySocial
base="https://www.hackerrank.com"
icon="https://raw.githubusercontent.com/rahuldkjain/github-profile-readme-generator/master/src/images/icons/Social/hackerrank.svg"
username=""
/>
<DisplaySocial
base="https://codeforces.com/profile"
icon="https://raw.githubusercontent.com/rahuldkjain/github-profile-readme-generator/master/src/images/icons/Social/codeforces.svg"
username=""
/>
<DisplaySocial
base="https://www.leetcode.com"
icon="https://raw.githubusercontent.com/rahuldkjain/github-profile-readme-generator/master/src/images/icons/Social/leet-code.svg"
username=""
/>
<DisplaySocial
base="https://www.hackerearth.com"
icon="https://raw.githubusercontent.com/rahuldkjain/github-profile-readme-generator/master/src/images/icons/Social/hackerearth.svg"
username=""
/>
<DisplaySocial
base="https://auth.geeksforgeeks.org/user"
icon="https://raw.githubusercontent.com/rahuldkjain/github-profile-readme-generator/master/src/images/icons/Social/geeks-for-geeks.svg"
username=""
/>
<DisplaySocial
base="https://www.topcoder.com/members"
icon="https://raw.githubusercontent.com/rahuldkjain/github-profile-readme-generator/master/src/images/icons/Social/topcoder.svg"
username=""
/>
<DisplaySocial
base="https://discord.gg"
icon="https://raw.githubusercontent.com/rahuldkjain/github-profile-readme-generator/master/src/images/icons/Social/discord.svg"
username=""
/>
<DisplaySocial
base=""
icon="https://raw.githubusercontent.com/rahuldkjain/github-profile-readme-generator/master/src/images/icons/Social/rss.svg"
username=""
/>
</div>
`;
exports[`SubTitle Preview renders correctly 1`] = `
<h3
className="text-center font-medium"
>
A passionate frontend developer from India
</h3>
`;
exports[`SubTitle Preview renders correctly with no subtitle 1`] = `""`;
exports[`Title Preview renders correctly 1`] = `
<h1
className="text-center text-xl font-bold"
>
Hi 👋, I'm dummy
</h1>
`;
exports[`Title Preview renders correctly with no prefix 1`] = `""`;
exports[`Title Preview renders correctly with no title 1`] = `""`;
exports[`Title Preview renders correctly with no title and prefix 1`] = `""`;
exports[`TopLanguages Preview renders correctly 1`] = `
<div
className="text-center mx-4 mb-4"
>
 
</div>
`;
exports[`TopLanguages Preview renders correctly with show true 1`] = `
<div
className="text-center mx-4 mb-4"
>
<img
alt=""
src="https://github-readme-stats.vercel.app/api/top-langs?username=&show_icons=true&locale=en&layout=compact"
/>
</div>
`;
exports[`TwitterBadgePreview Preview renders correctly 1`] = `""`;
exports[`TwitterBadgePreview Preview renders correctly with show true 1`] = `
<div
className="text-left my-2"
>
<a
href="https://twitter.com/"
rel="noreferrer"
target="_blank"
>
<img
alt=""
className="h-4 sm:h-6"
src="https://img.shields.io/twitter/follow/?logo=twitter&style=for-the-badge"
/>
</a>
</div>
`;
exports[`VisitorsBadge Preview renders correctly 1`] = `""`;
exports[`VisitorsBadge Preview renders correctly with show true 1`] = `
<div
className="text-left my-2"
>
<img
alt=""
className="h-4 sm:h-6"
src="https://komarev.com/ghpvc/?username=&label=Profile%20views&color=0e75b6&style=flat"
/>
</div>
`;
exports[`Work Preview renders correctly 1`] = `
<Fragment>
<DisplayWork
link="https://dummy.com"
prefix="🔭 Im currently working on"
project="readme-generator"
/>
<DisplayWork
link=""
prefix="🌱 Im currently learning"
project=""
/>
<DisplayWork
link=""
prefix="🤝 Im looking for help with"
project=""
/>
<DisplayWork
link=""
prefix="👯 Im looking to collaborate on"
project=""
/>
<DisplayWork
link=""
prefix="💬 Ask me about"
project=""
/>
<DisplayWork
link=""
prefix="👨‍💻 All of my projects are available at"
project=""
/>
<DisplayWork
link=""
prefix="📝 I regularly write articles on"
project=""
/>
<DisplayWork
link=""
prefix="📄 Know about my experiences"
project=""
/>
<DisplayWork
link=""
prefix="📫 How to reach me"
project=""
/>
<DisplayWork
link=""
prefix="⚡ Fun fact"
project=""
/>
</Fragment>
`;
@@ -1,156 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Skills renders correctly 1`] = `
<div
className="px-2 sm:px-6 mb-10 "
>
<div
className="text-xl sm:text-2xl font-bold font-title mt-2 mb-4 flex justify-between"
>
Skills
<div
className="relative flex"
>
<input
className="leading:none text-xs my-0 py-1 px-2 pr-8 sm:text-xl border-2 border-gray-900 focus:border-blue-700 placeholder-gray-700"
onChange={[Function]}
placeholder="Search Skills"
type="text"
/>
<span
className="absolute"
style={
Object {
"right": "10px",
}
}
>
<SearchIcon
className="mb-1 transform scale-100 md:scale-125"
size={16}
verticalAlign="text-bottom"
/>
</span>
</div>
</div>
<div
className="divide-y divide-gray-500"
key="language"
>
<div
className="text-sm sm:text-xl text-gray-900 text-left py-1"
>
Programming Languages
</div>
<div
className="flex justify-start items-center flex-wrap w-full mb-6 pl-4 sm:pl-10"
>
<div
className="w-1/3 sm:w-1/4 my-6"
key="javascript"
>
<label
className="checkbox-label flex items-center justify-start"
htmlFor="javascript"
>
<input
checked={true}
className="checkbox-label__input"
id="javascript"
onChange={[Function]}
type="checkbox"
/>
<span
className="checkbox-label__control"
/>
<img
alt="javascript"
className="ml-4 w-8 h-8 sm:w-10 sm:h-10"
src="javascript.svg"
/>
<span
className="tooltiptext"
>
javascript
</span>
</label>
</div>
</div>
</div>
<div
className="divide-y divide-gray-500"
key="frontend_dev"
>
<div
className="text-sm sm:text-xl text-gray-900 text-left py-1"
>
Frontend Development
</div>
<div
className="flex justify-start items-center flex-wrap w-full mb-6 pl-4 sm:pl-10"
>
<div
className="w-1/3 sm:w-1/4 my-6"
key="react"
>
<label
className="checkbox-label flex items-center justify-start"
htmlFor="react"
>
<input
className="checkbox-label__input"
id="react"
onChange={[Function]}
type="checkbox"
/>
<span
className="checkbox-label__control"
/>
<img
alt="react"
className="ml-4 w-8 h-8 sm:w-10 sm:h-10"
src="react.svg"
/>
<span
className="tooltiptext"
>
react
</span>
</label>
</div>
<div
className="w-1/3 sm:w-1/4 my-6"
key="svelte"
>
<label
className="checkbox-label flex items-center justify-start"
htmlFor="svelte"
>
<input
className="checkbox-label__input"
id="svelte"
onChange={[Function]}
type="checkbox"
/>
<span
className="checkbox-label__control"
/>
<img
alt="svelte"
className="ml-4 w-8 h-8 sm:w-10 sm:h-10"
src="svelte.svg"
/>
<span
className="tooltiptext"
>
svelte
</span>
</label>
</div>
</div>
</div>
<span
className="flex justify-center text-gray-900"
/>
</div>
`;
@@ -1,383 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Social renders correctly 1`] = `
<div
className="px-2 sm:px-6 mb-4"
>
<div
className="text-xl sm:text-2xl font-bold font-title mt-2 mb-2"
>
Social
</div>
<div
className="flex flex-wrap justify-center items-center"
>
<div
className="w-1/2 flex justify-center items-center text-xxs sm:text-lg py-4 pr-2 sm:pr-0"
>
<img
alt="github"
className="w-6 h-6 sm:w-8 sm:h-8 mr-1 sm:mr-4"
src="https://cdn.jsdelivr.net/npm/simple-icons@3.0.1/icons/github.svg"
/>
<input
className="outline-none placeholder-gray-700 w-32 sm:w-1/2 border-t-0 border-l-0 border-r-0 border solid border-gray-900 py-1 px-1 sm:px-2 focus:border-blue-700"
id="github"
onChange={[Function]}
placeholder="github username"
value="github "
/>
</div>
<div
className="w-1/2 flex justify-center items-center text-xxs sm:text-lg py-4 pr-2 sm:pr-0"
>
<img
alt="twitter"
className="w-6 h-6 sm:w-8 sm:h-8 mr-1 sm:mr-4"
src="https://cdn.jsdelivr.net/npm/simple-icons@v3/icons/twitter.svg"
/>
<input
className="outline-none placeholder-gray-700 w-32 sm:w-1/2 border-t-0 border-l-0 border-r-0 border solid border-gray-900 py-1 px-2 focus:border-blue-700"
id="twitter"
onChange={[Function]}
placeholder="twitter username"
value="twitter"
/>
</div>
<div
className="w-1/2 flex justify-center items-center text-xxs sm:text-lg py-4 pr-2 sm:pr-0"
>
<img
alt="dev.to"
className="w-6 h-6 sm:w-8 sm:h-8 mr-1 sm:mr-4"
src="https://cdn.jsdelivr.net/npm/simple-icons@3.0.1/icons/dev-dot-to.svg"
/>
<input
className="outline-none placeholder-gray-700 w-32 sm:w-1/2 border-t-0 border-l-0 border-r-0 border solid border-gray-900 py-1 px-2 focus:border-blue-700"
id="dev"
onChange={[Function]}
placeholder="dev.to username"
value="dev"
/>
</div>
<div
className="w-1/2 flex justify-center items-center text-xxs sm:text-lg py-4 pr-2 sm:pr-0"
>
<img
alt="codepen"
className="w-6 h-6 sm:w-8 sm:h-8 mr-1 sm:mr-4"
src="https://cdn.jsdelivr.net/npm/simple-icons@3.0.1/icons/codepen.svg"
/>
<input
className="outline-none placeholder-gray-700 w-32 sm:w-1/2 border-t-0 border-l-0 border-r-0 border solid border-gray-900 py-1 px-2 focus:border-blue-700"
id="codepen"
onChange={[Function]}
placeholder="codepen username"
value="codepen"
/>
</div>
<div
className="w-1/2 flex justify-center items-center text-xxs sm:text-lg py-4 pr-2 sm:pr-0"
>
<img
alt="codesandbox"
className="w-6 h-6 sm:w-8 sm:h-8 mr-1 sm:mr-4"
src="https://cdn.jsdelivr.net/npm/simple-icons@3.0.1/icons/codesandbox.svg"
/>
<input
className="outline-none placeholder-gray-700 w-32 sm:w-1/2 border-t-0 border-l-0 border-r-0 border solid border-gray-900 py-1 px-2 focus:border-blue-700"
id="codesandbox"
onChange={[Function]}
placeholder="codesandbox username"
value="codesandbodx"
/>
</div>
<div
className="w-1/2 flex justify-center items-center text-xxs sm:text-lg py-4 pr-2 sm:pr-0"
>
<img
alt="stackoverflow"
className="w-6 h-6 sm:w-8 sm:h-8 mr-1 sm:mr-4"
src="https://cdn.jsdelivr.net/npm/simple-icons@3.0.1/icons/stackoverflow.svg"
/>
<input
className="outline-none placeholder-gray-700 w-32 sm:w-1/2 border-t-0 border-l-0 border-r-0 border solid border-gray-900 py-1 px-2 focus:border-blue-700"
id="stackoverflow"
onChange={[Function]}
placeholder="stackoverflow user ID"
value="stackoverflow"
/>
</div>
<div
className="w-1/2 flex justify-center items-center text-xxs sm:text-lg py-4 pr-2 sm:pr-0"
>
<img
alt="linkedin"
className="w-6 h-6 sm:w-8 sm:h-8 mr-1 sm:mr-4"
src="https://cdn.jsdelivr.net/npm/simple-icons@3.0.1/icons/linkedin.svg"
/>
<input
className="outline-none placeholder-gray-700 w-32 sm:w-1/2 border-t-0 border-l-0 border-r-0 border solid border-gray-900 py-1 px-2 focus:border-blue-700"
id="linkedin"
onChange={[Function]}
placeholder="linkedin username"
value="linkedin"
/>
</div>
<div
className="w-1/2 flex justify-center items-center text-xxs sm:text-lg py-4 pr-2 sm:pr-0"
>
<img
alt="kaggle"
className="w-6 h-6 sm:w-8 sm:h-8 mr-1 sm:mr-4"
src="https://cdn.jsdelivr.net/npm/simple-icons@3.0.1/icons/kaggle.svg"
/>
<input
className="outline-none placeholder-gray-700 w-32 sm:w-1/2 border-t-0 border-l-0 border-r-0 border solid border-gray-900 py-1 px-2 focus:border-blue-700"
id="kaggle"
onChange={[Function]}
placeholder="kaggle username"
value="kaggle"
/>
</div>
<div
className="w-1/2 flex justify-center items-center text-xxs sm:text-lg py-4 pr-2 sm:pr-0"
>
<img
alt="facebook"
className="w-6 h-6 sm:w-8 sm:h-8 mr-1 sm:mr-4"
src="https://cdn.jsdelivr.net/npm/simple-icons@3.0.1/icons/facebook.svg"
/>
<input
className="outline-none placeholder-gray-700 w-32 sm:w-1/2 border-t-0 border-l-0 border-r-0 border solid border-gray-900 py-1 px-2 focus:border-blue-700"
id="fb"
onChange={[Function]}
placeholder="facebook username"
value="fb"
/>
</div>
<div
className="w-1/2 flex justify-center items-center text-xxs sm:text-lg py-4 pr-2 sm:pr-0"
>
<img
alt="instagram"
className="w-6 h-6 sm:w-8 sm:h-8 mr-1 sm:mr-4"
src="https://cdn.jsdelivr.net/npm/simple-icons@3.0.1/icons/instagram.svg"
/>
<input
className="outline-none placeholder-gray-700 w-32 sm:w-1/2 border-t-0 border-l-0 border-r-0 border solid border-gray-900 py-1 px-2 focus:border-blue-700"
id="instagram"
onChange={[Function]}
placeholder="instagram username"
value="instagram"
/>
</div>
<div
className="w-1/2 flex justify-center items-center text-xxs sm:text-lg py-4 pr-2 sm:pr-0"
>
<img
alt="dribbble"
className="w-6 h-6 sm:w-8 sm:h-8 mr-1 sm:mr-4"
src="https://cdn.jsdelivr.net/npm/simple-icons@3.1.0/icons/dribbble.svg"
/>
<input
className="outline-none placeholder-gray-700 w-32 sm:w-1/2 border-t-0 border-l-0 border-r-0 border solid border-gray-900 py-1 px-2 focus:border-blue-700"
id="dribbble"
onChange={[Function]}
placeholder="dribbble username"
/>
</div>
<div
className="w-1/2 flex justify-center items-center text-xxs sm:text-lg py-4 pr-2 sm:pr-0"
>
<img
alt="behance"
className="w-6 h-6 sm:w-8 sm:h-8 mr-1 sm:mr-4"
src="https://cdn.jsdelivr.net/npm/simple-icons@3.1.0/icons/behance.svg"
/>
<input
className="outline-none placeholder-gray-700 w-32 sm:w-1/2 border-t-0 border-l-0 border-r-0 border solid border-gray-900 py-1 px-2 focus:border-blue-700"
id="behance"
onChange={[Function]}
placeholder="behance username"
value="behance"
/>
</div>
<div
className="w-1/2 flex justify-center items-center text-xxs sm:text-lg py-4 pr-2 sm:pr-0"
>
<img
alt="medium"
className="w-6 h-6 sm:w-8 sm:h-8 mr-1 sm:mr-4"
src="https://cdn.jsdelivr.net/npm/simple-icons@3.1.0/icons/medium.svg"
/>
<input
className="outline-none placeholder-gray-700 w-32 sm:w-1/2 border-t-0 border-l-0 border-r-0 border solid border-gray-900 py-1 px-2 focus:border-blue-700"
id="medium"
onChange={[Function]}
placeholder="medium username (with @)"
value="medium"
/>
</div>
<div
className="w-1/2 flex justify-center items-center text-xxs sm:text-lg py-4 pr-2 sm:pr-0"
>
<img
alt="youtube"
className="w-6 h-6 sm:w-8 sm:h-8 mr-1 sm:mr-4"
src="https://cdn.jsdelivr.net/npm/simple-icons@3.1.0/icons/youtube.svg"
/>
<input
className="outline-none placeholder-gray-700 w-32 sm:w-1/2 border-t-0 border-l-0 border-r-0 border solid border-gray-900 py-1 px-2 focus:border-blue-700"
id="youtube"
onChange={[Function]}
placeholder="youtube channel name"
value="youtube"
/>
</div>
<div
className="w-1/2 flex justify-center items-center text-xxs sm:text-lg py-4 pr-2 sm:pr-0"
>
<img
alt="codechef"
className="w-6 h-6 sm:w-8 sm:h-8 mr-1 sm:mr-4"
src="https://cdn.jsdelivr.net/npm/simple-icons@3.1.0/icons/codechef.svg"
/>
<input
className="outline-none placeholder-gray-700 w-32 sm:w-1/2 border-t-0 border-l-0 border-r-0 border solid border-gray-900 py-1 px-2 focus:border-blue-700"
id="codechef"
onChange={[Function]}
placeholder="codechef username"
value="codechef"
/>
</div>
<div
className="w-1/2 flex justify-center items-center text-xxs sm:text-lg py-4 pr-2 sm:pr-0"
>
<img
alt="hackerrank"
className="w-6 h-6 sm:w-8 sm:h-8 mr-1 sm:mr-4"
src="https://cdn.jsdelivr.net/npm/simple-icons@3.1.0/icons/hackerrank.svg"
/>
<input
className="outline-none placeholder-gray-700 w-32 sm:w-1/2 border-t-0 border-l-0 border-r-0 border solid border-gray-900 py-1 px-2 focus:border-blue-700"
id="hackerrank"
onChange={[Function]}
placeholder="hackerrank username"
/>
</div>
<div
className="w-1/2 flex justify-center items-center text-xxs sm:text-lg py-4 pr-2 sm:pr-0"
>
<img
alt="codeforces"
className="w-6 h-6 sm:w-8 sm:h-8 mr-1 sm:mr-4"
src="https://cdn.jsdelivr.net/npm/simple-icons@3.1.0/icons/codeforces.svg"
/>
<input
className="outline-none placeholder-gray-700 w-32 sm:w-1/2 border-t-0 border-l-0 border-r-0 border solid border-gray-900 py-1 px-2 focus:border-blue-700"
id="codeforces"
onChange={[Function]}
placeholder="codeforces username"
value="codeforces"
/>
</div>
<div
className="w-1/2 flex justify-center items-center text-xxs sm:text-lg py-4 pr-2 sm:pr-0"
>
<img
alt="leetcode"
className="w-6 h-6 sm:w-8 sm:h-8 mr-1 sm:mr-4"
src="https://cdn.jsdelivr.net/npm/simple-icons@3.1.0/icons/leetcode.svg"
/>
<input
className="outline-none placeholder-gray-700 w-32 sm:w-1/2 border-t-0 border-l-0 border-r-0 border solid border-gray-900 py-1 px-2 focus:border-blue-700"
id="leetcode"
onChange={[Function]}
placeholder="leetcode username"
value="leetcode"
/>
</div>
<div
className="w-1/2 flex justify-center items-center text-xxs sm:text-lg py-4 pr-2 sm:pr-0"
>
<img
alt="topcoder"
className="w-6 h-6 sm:w-8 sm:h-8 mr-1 sm:mr-4"
src="https://cdn.jsdelivr.net/npm/simple-icons@3.1.0/icons/topcoder.svg"
/>
<input
className="outline-none placeholder-gray-700 w-32 sm:w-1/2 border-t-0 border-l-0 border-r-0 border solid border-gray-900 py-1 px-2 focus:border-blue-700"
id="topcoder"
onChange={[Function]}
placeholder="topcoder username"
value="topcoder"
/>
</div>
<div
className="w-1/2 flex justify-center items-center text-xxs sm:text-lg py-4 pr-2 sm:pr-0"
>
<img
alt="hackerearth"
className="w-6 h-6 sm:w-8 sm:h-8 mr-1 sm:mr-4"
src="https://cdn.jsdelivr.net/npm/simple-icons@3.1.0/icons/hackerearth.svg"
/>
<input
className="outline-none placeholder-gray-700 w-32 sm:w-1/2 border-t-0 border-l-0 border-r-0 border solid border-gray-900 py-1 px-2 focus:border-blue-700"
id="hackerearth"
onChange={[Function]}
placeholder="hackerearth user (with @)"
value="@hackerearth"
/>
</div>
<div
className="w-1/2 flex justify-center items-center text-xxs sm:text-lg py-4 pr-2 sm:pr-0"
>
<img
alt="geeksforgeeks"
className="w-6 h-6 sm:w-8 sm:h-8 mr-1 sm:mr-4"
src="https://cdn.jsdelivr.net/npm/simple-icons@3.1.0/icons/geeksforgeeks.svg"
/>
<input
className="outline-none placeholder-gray-700 w-32 sm:w-1/2 border-t-0 border-l-0 border-r-0 border solid border-gray-900 py-1 px-2 focus:border-blue-700"
id="geeksforgeeks"
onChange={[Function]}
placeholder="GFG (<username>/profile)"
value="geeks_for_geeks"
/>
</div>
<div
className="w-1/2 flex justify-center items-center text-xxs sm:text-lg py-4 pr-2 sm:pr-0"
>
<img
alt="discord"
className="w-6 h-6 sm:w-8 sm:h-8 mr-1 sm:mr-4"
src="https://cdn.jsdelivr.net/npm/simple-icons@3.1.0/icons/discord.svg"
/>
<input
className="outline-none placeholder-gray-700 w-32 sm:w-1/2 border-t-0 border-l-0 border-r-0 border solid border-gray-900 py-1 px-2 focus:border-blue-700"
id="discord"
onChange={[Function]}
placeholder="discord invite (only code)"
value="discord"
/>
</div>
<div
className="w-1/2 flex justify-center items-center text-xxs sm:text-lg py-4 pr-2 sm:pr-0"
>
<img
alt="rssfeed"
className="w-6 h-6 sm:w-8 sm:h-8 mr-1 sm:mr-4"
src="https://cdn.jsdelivr.net/npm/simple-icons@3.1.0/icons/rss.svg"
/>
<input
className="outline-none placeholder-gray-700 w-32 sm:w-1/2 border-t-0 border-l-0 border-r-0 border solid border-gray-900 py-1 px-2 focus:border-blue-700"
id="rssurl"
onChange={[Function]}
placeholder="RSS feed URL"
value="rssurl"
/>
</div>
</div>
</div>
`;
@@ -1,19 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Subtitle renders correctly 1`] = `
<div
className="flex justify-center items-start flex-col w-full px-2 sm:px-6 mb-10"
>
<div
className="text-xl sm:text-2xl font-bold font-title mt-2 mb-2"
>
Subtitle
</div>
<input
className="outline-none w-full text-xs sm:text-lg sm:w-1/2 border-t-0 border-l-0 border-r-0 border solid border-gray-900 py-1 px-2 focus:border-blue-700"
id="subtitle"
onChange={[Function]}
value="A frontend developer"
/>
</div>
`;
@@ -1,30 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Title renders title component correctly 1`] = `
<div
className="flex justify-center items-start flex-col w-full px-2 sm:px-6 mb-10"
>
<div
className="text-xl sm:text-2xl font-bold font-title mt-2 mb-2"
>
Title
</div>
<div
className="flex justify-start items-center w-full text-regular text-xs sm:text-lg"
>
<input
className="outline-none w-24 sm:w-40 mr-10 border-t-0 border-l-0 border-r-0 border solid border-gray-900 py-1 px-2 focus:border-blue-700 prefix"
id="title-prefix"
onChange={[Function]}
value="test_title"
/>
<input
className="outline-none placeholder-gray-700 w-1/2 sm:w-1/3 border-t-0 border-l-0 border-r-0 border solid border-gray-900 py-1 px-2 focus:border-blue-700"
id="title-name"
onChange={[Function]}
placeholder="name"
value="test_data"
/>
</div>
</div>
`;
@@ -1,184 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Work renders work component correctly 1`] = `
<div
className="flex justify-center items-start flex-col w-full px-2 sm:px-6 mb-10"
>
<div
className="text-xl sm:text-2xl font-bold font-title mt-2 mb-2"
>
Work
</div>
<div
className="text-xs sm:text-lg flex flex-col sm:flex-row mb-10 justify-center sm:justify-start items-center sm:items-start w-full px-4 sm:px-0"
>
<input
className="outline-none placeholder-gray-700 mr-8 w-full sm:w-1/3 border-t-0 border-l-0 border-r-0 border solid border-gray-900 py-1 px-2 focus:border-blue-700"
id="currentWork-prefix"
onChange={[Function]}
placeholder="Hi, I'm "
value="test_currentwork"
/>
<input
className="outline-none placeholder-gray-700 mr-8 w-full sm:w-1/4 border-t-0 border-l-0 border-r-0 border solid border-gray-900 py-1 px-2 focus:border-blue-700"
id="currentWork"
onChange={[Function]}
placeholder="project name"
/>
<input
className="outline-none placeholder-gray-700 mr-8 sm:mr-0 text-blue-700 w-full sm:w-1/4 border-t-0 border-l-0 border-r-0 border solid border-gray-900 py-1 px-2 focus:border-blue-700"
id="currentWork-link"
onChange={[Function]}
placeholder="project link"
value="test_currentwork"
/>
</div>
<div
className="text-xs sm:text-lg flex flex-col sm:flex-row mb-10 justify-center sm:justify-start items-center sm:items-start w-full px-4 sm:px-0"
>
<input
className="outline-none mr-8 w-full sm:w-1/3 border-t-0 border-l-0 border-r-0 border solid border-gray-900 py-1 px-2 focus:border-blue-700"
id="collaborateOn-prefix"
onChange={[Function]}
/>
<input
className="outline-none placeholder-gray-700 mr-8 w-full sm:w-1/4 border-t-0 border-l-0 border-r-0 border solid border-gray-900 py-1 px-2 focus:border-blue-700"
id="collaborateOn"
onChange={[Function]}
placeholder="project name"
/>
<input
className="outline-none placeholder-gray-700 mr-8 sm:mr-0 text-blue-700 w-full sm:w-1/4 border-t-0 border-l-0 border-r-0 border solid border-gray-900 py-1 px-2 focus:border-blue-700"
id="collaborateOn-link"
onChange={[Function]}
placeholder="project link"
/>
</div>
<div
className="text-xs sm:text-lg flex flex-col sm:flex-row mb-10 justify-center sm:justify-start items-center sm:items-start w-full px-4 sm:px-0"
>
<input
className="outline-none placeholder-gray-700 mr-8 w-full sm:w-1/3 border-t-0 border-l-0 border-r-0 border solid border-gray-900 py-1 px-2 focus:border-blue-700"
id="helpWith-prefix"
onChange={[Function]}
/>
<input
className="outline-none placeholder-gray-700 mr-8 w-full sm:w-1/4 border-t-0 border-l-0 border-r-0 border solid border-gray-900 py-1 px-2 focus:border-blue-700"
id="helpWith"
onChange={[Function]}
placeholder="project name"
/>
<input
className="outline-none placeholder-gray-700 mr-8 sm:mr-0 text-blue-700 w-full sm:w-1/4 border-t-0 border-l-0 border-r-0 border solid border-gray-900 py-1 px-2 focus:border-blue-700"
id="helpWith-link"
onChange={[Function]}
placeholder="project link"
/>
</div>
<div
className="text-xs sm:text-lg flex flex-col sm:flex-row mb-10 justify-center sm:justify-start items-center sm:items-start w-full px-4 sm:px-0"
>
<input
className="outline-none mr-8 w-full sm:w-1/3 border-t-0 border-l-0 border-r-0 border solid border-gray-900 py-1 px-2 focus:border-blue-700"
id="currentLearn-prefix"
onChange={[Function]}
/>
<input
className="outline-none placeholder-gray-700 w-full sm:w-1/3 border-t-0 border-l-0 border-r-0 border solid border-gray-900 py-1 px-2 focus:border-blue-700"
id="currentLearn"
onChange={[Function]}
placeholder="Frameworks, courses etc."
/>
</div>
<div
className="text-xs sm:text-lg flex flex-col sm:flex-row mb-10 justify-center sm:justify-start items-center sm:items-start w-full px-4 sm:px-0"
>
<input
className="outline-none mr-8 w-full sm:w-1/3 border-t-0 border-l-0 border-r-0 border solid border-gray-900 py-1 px-2 focus:border-blue-700"
id="ama-prefix"
onChange={[Function]}
/>
<input
className="outline-none placeholder-gray-700 mr-8 sm:mr-0 w-full sm:w-1/3 border-t-0 border-l-0 border-r-0 border solid border-gray-900 py-1 px-2 focus:border-blue-700"
id="ama"
onChange={[Function]}
placeholder="react, vue and gsap"
/>
</div>
<div
className="text-xs sm:text-lg flex flex-col sm:flex-row mb-10 justify-center sm:justify-start items-center sm:items-start w-full px-4 sm:px-0"
>
<input
className="outline-none mr-8 w-full sm:w-1/3 border-t-0 border-l-0 border-r-0 border solid border-gray-900 py-1 px-2 focus:border-blue-700"
id="contact-prefix"
onChange={[Function]}
/>
<input
className="outline-none placeholder-gray-700 mr-8 sm:mr-0 w-full sm:w-1/3 border-t-0 border-l-0 border-r-0 border solid border-gray-900 py-1 px-2 focus:border-blue-700"
id="contact"
onChange={[Function]}
placeholder="example@gmail.com"
/>
</div>
<div
className="text-xs sm:text-lg flex flex-col sm:flex-row mb-10 justify-center sm:justify-start items-center sm:items-start w-full px-4 sm:px-0"
>
<input
className="outline-none mr-8 w-full sm:w-1/3 border-t-0 border-l-0 border-r-0 border solid border-gray-900 py-1 px-2 focus:border-blue-700"
id="portfolio-prefix"
onChange={[Function]}
/>
<input
className="outline-none placeholder-gray-700 mr-8 sm:mr-0 text-blue-700 w-full sm:w-1/3 border-t-0 border-l-0 border-r-0 border solid border-gray-900 py-1 px-2 focus:border-blue-700"
id="portfolio"
onChange={[Function]}
placeholder="portfolio link"
/>
</div>
<div
className="text-xs sm:text-lg flex flex-col sm:flex-row mb-10 justify-center sm:justify-start items-center sm:items-start w-full px-4 sm:px-0"
>
<input
className="outline-none mr-8 w-full sm:w-1/3 border-t-0 border-l-0 border-r-0 border solid border-gray-900 py-1 px-2 focus:border-blue-700"
id="blog-prefix"
onChange={[Function]}
/>
<input
className="outline-none placeholder-gray-700 mr-8 sm:mr-0 text-blue-700 w-full sm:w-1/3 border-t-0 border-l-0 border-r-0 border solid border-gray-900 py-1 px-2 focus:border-blue-700"
id="blog"
onChange={[Function]}
placeholder="blog link"
/>
</div>
<div
className="text-xs sm:text-lg flex flex-col sm:flex-row mb-10 justify-center sm:justify-start items-center sm:items-start w-full px-4 sm:px-0"
>
<input
className="outline-none mr-8 w-full sm:w-1/3 border-t-0 border-l-0 border-r-0 border solid border-gray-900 py-1 px-2 focus:border-blue-700"
id="resume-prefix"
onChange={[Function]}
/>
<input
className="outline-none placeholder-gray-700 mr-8 sm:mr-0 text-blue-700 w-full sm:w-1/3 border-t-0 border-l-0 border-r-0 border solid border-gray-900 py-1 px-2 focus:border-blue-700"
id="resume"
onChange={[Function]}
placeholder="resume link"
/>
</div>
<div
className="text-xs sm:text-lg flex flex-col sm:flex-row mb-10 justify-center sm:justify-start items-center sm:items-start w-full px-4 sm:px-0"
>
<input
className="outline-none mr-8 w-full sm:w-1/3 border-t-0 border-l-0 border-r-0 border solid border-gray-900 py-1 px-2 focus:border-blue-700"
id="funFact-prefix"
onChange={[Function]}
/>
<input
className="outline-none placeholder-gray-700 mr-8 sm:mr-0 w-full sm:w-1/3 border-t-0 border-l-0 border-r-0 border solid border-gray-900 py-1 px-2 focus:border-blue-700"
id="funFact"
onChange={[Function]}
placeholder="I think I am funny"
/>
</div>
</div>
`;
-806
View File
@@ -1,806 +0,0 @@
import React from 'react';
import toJson from 'enzyme-to-json';
import { shallow, mount } from 'enzyme';
import Addons from '../addons';
jest.useFakeTimers();
describe('Addons', () => {
const dataInput = {
title: '',
subtitle: 'A passionate frontend developer from India',
currentWork: '',
currentLearn: '',
collaborateOn: '',
helpWith: '',
ama: '',
contact: '',
funFact: '',
twitterBadge: false,
visitorsBadge: false,
badgeStyle: 'flat',
badgeColor: '0e75b6',
badgeLabel: 'Profile views',
githubProfileTrophy: false,
githubStats: false,
githubStatsOptions: {
theme: '',
titleColor: '',
textColor: '',
bgColor: '',
hideBorder: false,
cacheSeconds: null,
locale: 'en',
},
topLanguages: false,
topLanguagesOptions: {
theme: '',
titleColor: '',
textColor: '',
bgColor: '',
hideBorder: false,
cacheSeconds: null,
locale: 'en',
},
devDynamicBlogs: false,
mediumDynamicBlogs: false,
rssDynamicBlogs: false,
};
const socialInput = {
github: '',
dev: '',
linkedin: '',
codepen: '',
stackoverflow: '',
kaggle: '',
codesandbox: '',
fb: '',
instagram: '',
twitter: '',
dribbble: '',
behance: '',
medium: '',
youtube: '',
codechef: '',
hackerrank: '',
codeforces: '',
leetcode: '',
topcoder: '',
hackerearth: '',
geeks_for_geeks: '',
discord: '',
rssurl: '',
};
let mockHandleCheckChange;
let mockHandleDataChange;
beforeEach(() => {
mockHandleCheckChange = jest.fn();
mockHandleDataChange = jest.fn();
});
afterEach(() => {
jest.clearAllMocks();
});
it('renders correctly', () => {
const addOnComponent = shallow(
<Addons
data={dataInput}
social={socialInput}
handleCheckChange={mockHandleCheckChange}
handleDataChange={mockHandleDataChange}
/>,
);
expect(toJson(addOnComponent)).toMatchSnapshot();
});
it('should render Customize Badges', () => {
const newDataInput = {
...dataInput,
mediumDynamicBlogs: 'some-medium-blogs-value',
};
const newSocialInput = {
medium: '@abcd',
};
const addOnComponent = mount(
<Addons
data={newDataInput}
social={newSocialInput}
handleCheckChange={mockHandleCheckChange}
handleDataChange={mockHandleDataChange}
/>,
);
addOnComponent.find('#visitors-count-open-btn').simulate('click', {});
expect(addOnComponent).toMatchSnapshot();
});
it('should handle data change when badge style is changed', () => {
const mockEvent = { target: { value: 'style-new' } };
const newDataInput = {
...dataInput,
mediumDynamicBlogs: 'some-medium-blogs-value',
githubStatsOptions: {
theme: 'theme-1',
titleColor: 'title-abcd',
textColor: 'some-color',
bgColor: 'abcd',
hideBorder: false,
cacheSeconds: null,
locale: 'en',
},
};
const newSocialInput = {
medium: '@abcd',
};
const addOnComponent = mount(
<Addons
data={newDataInput}
social={newSocialInput}
handleCheckChange={mockHandleCheckChange}
handleDataChange={mockHandleDataChange}
/>,
);
addOnComponent.find('#visitors-count-open-btn').simulate('click', {});
addOnComponent.find('#badge-style').simulate('change', mockEvent);
jest.runAllTimers();
expect(mockHandleDataChange).toHaveBeenCalledTimes(1);
expect(mockHandleDataChange).toHaveBeenCalledWith('badgeStyle', { target: { value: 'style-new' } });
});
it('should handle data change when badge color is changed', () => {
const mockEvent = { target: { value: 'new-color-abcd' } };
const newDataInput = {
...dataInput,
mediumDynamicBlogs: 'some-medium-blogs-value',
};
const newSocialInput = {
medium: '@abcd',
};
const addOnComponent = mount(
<Addons
data={newDataInput}
social={newSocialInput}
handleCheckChange={mockHandleCheckChange}
handleDataChange={mockHandleDataChange}
/>,
);
addOnComponent.find('#visitors-count-open-btn').simulate('click', {});
addOnComponent.find('#badge-color').simulate('change', mockEvent);
jest.runAllTimers();
expect(mockHandleDataChange).toHaveBeenCalledTimes(1);
expect(mockHandleDataChange).toHaveBeenCalledWith('badgeColor', { target: { value: 'new-color-abcd' } });
});
it('should handle data change when badge-label-text is changed', () => {
const mockEvent = { target: { value: 'label-abcd-random' } };
const newDataInput = {
...dataInput,
mediumDynamicBlogs: 'some-medium-blogs-value',
};
const newSocialInput = {
medium: '@abcd',
};
const addOnComponent = mount(
<Addons
data={newDataInput}
social={newSocialInput}
handleCheckChange={mockHandleCheckChange}
handleDataChange={mockHandleDataChange}
/>,
);
addOnComponent.find('#visitors-count-open-btn').simulate('click', {});
addOnComponent.find('#badge-label-text').simulate('change', mockEvent);
jest.runAllTimers();
expect(mockHandleDataChange).toHaveBeenCalledTimes(1);
expect(mockHandleDataChange).toHaveBeenCalledWith('badgeLabel', { target: { value: 'label-abcd-random' } });
});
it('should render Customize Github stats card', () => {
const newDataInput = {
...dataInput,
mediumDynamicBlogs: 'some-medium-blogs-value',
};
const newSocialInput = {
medium: '@abcd',
};
const addOnComponent = mount(
<Addons
data={newDataInput}
social={newSocialInput}
handleCheckChange={mockHandleCheckChange}
handleDataChange={mockHandleDataChange}
/>,
);
addOnComponent.find('#github-stats-open-btn').simulate('click', {});
expect(addOnComponent).toMatchSnapshot();
});
it('should handle data change when stats theme is changed', () => {
const mockEvent = { target: { value: 'new-theme-for-stats' } };
const newDataInput = {
...dataInput,
mediumDynamicBlogs: 'some-medium-blogs-value',
githubStatsOptions: {
titleColor: 'title-abcd',
textColor: 'some-color',
bgColor: 'abcd',
hideBorder: false,
cacheSeconds: null,
theme: 'new-theme-for-stats',
locale: 'en',
},
};
const newSocialInput = {
medium: '@abcd',
};
const addOnComponent = mount(
<Addons
data={newDataInput}
social={newSocialInput}
handleCheckChange={mockHandleCheckChange}
handleDataChange={mockHandleDataChange}
/>,
);
addOnComponent.find('#github-stats-open-btn').simulate('click', {});
addOnComponent.find('#stats-theme').simulate('change', mockEvent);
jest.runAllTimers();
expect(mockHandleDataChange).toHaveBeenCalledTimes(1);
expect(mockHandleDataChange).toHaveBeenCalledWith('githubStatsOptions', {
target: {
value: {
theme: 'theme-1',
titleColor: 'title-abcd',
textColor: 'some-color',
bgColor: 'abcd',
hideBorder: false,
cacheSeconds: null,
theme: 'new-theme-for-stats',
locale: 'en',
},
},
});
});
it('should handle data change when stats title color is changed', () => {
const mockEvent = { target: { value: 'red-color' } };
const newDataInput = {
...dataInput,
mediumDynamicBlogs: 'some-medium-blogs-value',
githubStatsOptions: {
theme: 'theme-1',
textColor: 'some-color',
bgColor: 'abcd',
hideBorder: false,
cacheSeconds: null,
theme: 'new-theme-for-stats',
locale: 'en',
},
};
const newSocialInput = {
medium: '@abcd',
};
const addOnComponent = mount(
<Addons
data={newDataInput}
social={newSocialInput}
handleCheckChange={mockHandleCheckChange}
handleDataChange={mockHandleDataChange}
/>,
);
addOnComponent.find('#github-stats-open-btn').simulate('click', {});
addOnComponent.find('#stats-title-color').simulate('change', mockEvent);
jest.runAllTimers();
expect(mockHandleDataChange).toHaveBeenCalledTimes(1);
expect(mockHandleDataChange).toHaveBeenCalledWith('githubStatsOptions', {
target: {
value: {
theme: 'theme-1',
titleColor: 'red-color',
textColor: 'some-color',
bgColor: 'abcd',
hideBorder: false,
cacheSeconds: null,
theme: 'new-theme-for-stats',
locale: 'en',
},
},
});
});
it('should handle data change when stats bg color is changed', () => {
const mockEvent = { target: { value: 'random-color' } };
const newDataInput = {
...dataInput,
mediumDynamicBlogs: 'some-medium-blogs-value',
githubStatsOptions: {
theme: 'theme-1',
titleColor: 'some-color',
textColor: 'abcd',
hideBorder: false,
cacheSeconds: null,
theme: 'new-theme-for-stats',
locale: 'en',
},
};
const newSocialInput = {
medium: '@abcd',
};
const addOnComponent = mount(
<Addons
data={newDataInput}
social={newSocialInput}
handleCheckChange={mockHandleCheckChange}
handleDataChange={mockHandleDataChange}
/>,
);
addOnComponent.find('#github-stats-open-btn').simulate('click', {});
addOnComponent.find('#stats-bg-color').simulate('change', mockEvent);
jest.runAllTimers();
expect(mockHandleDataChange).toHaveBeenCalledTimes(1);
expect(mockHandleDataChange).toHaveBeenCalledWith('githubStatsOptions', {
target: {
value: {
theme: 'theme-1',
titleColor: 'some-color',
textColor: 'abcd',
bgColor: 'random-color',
hideBorder: false,
cacheSeconds: null,
theme: 'new-theme-for-stats',
locale: 'en',
},
},
});
});
it('should handle data change when stats cache seconds is changed', () => {
const mockEvent = { target: { value: 1900 } };
const newDataInput = {
...dataInput,
mediumDynamicBlogs: 'some-medium-blogs-value',
githubStatsOptions: {
theme: 'theme-1',
titleColor: 'some-color',
textColor: 'abcd',
bgColor: 'random-color',
hideBorder: false,
cacheSeconds: null,
theme: 'new-theme-for-stats',
locale: 'en',
},
};
const newSocialInput = {
medium: '@abcd',
};
const addOnComponent = mount(
<Addons
data={newDataInput}
social={newSocialInput}
handleCheckChange={mockHandleCheckChange}
handleDataChange={mockHandleDataChange}
/>,
);
addOnComponent.find('#github-stats-open-btn').simulate('click', {});
addOnComponent.find('#stats-cache-seconds').simulate('change', mockEvent);
jest.runAllTimers();
expect(mockHandleDataChange).toHaveBeenCalledTimes(1);
expect(mockHandleDataChange).toHaveBeenCalledWith('githubStatsOptions', {
target: {
value: {
theme: 'theme-1',
titleColor: 'some-color',
textColor: 'abcd',
bgColor: 'random-color',
hideBorder: false,
cacheSeconds: 1900,
theme: 'new-theme-for-stats',
locale: 'en',
},
},
});
});
it('should handle data change when stats text color is changed', () => {
const mockEvent = { target: { value: 'black-color' } };
const newDataInput = {
...dataInput,
mediumDynamicBlogs: 'some-medium-blogs-value',
githubStatsOptions: {
theme: 'theme-1',
titleColor: 'some-color',
bgColor: 'abcd',
hideBorder: false,
cacheSeconds: null,
theme: 'new-theme-for-stats',
locale: 'en',
},
};
const newSocialInput = {
medium: '@abcd',
};
const addOnComponent = mount(
<Addons
data={newDataInput}
social={newSocialInput}
handleCheckChange={mockHandleCheckChange}
handleDataChange={mockHandleDataChange}
/>,
);
addOnComponent.find('#github-stats-open-btn').simulate('click', {});
addOnComponent.find('#stats-text-color').simulate('change', mockEvent);
jest.runAllTimers();
expect(mockHandleDataChange).toHaveBeenCalledTimes(1);
expect(mockHandleDataChange).toHaveBeenCalledWith('githubStatsOptions', {
target: {
value: {
theme: 'theme-1',
titleColor: 'some-color',
textColor: 'black-color',
bgColor: 'abcd',
hideBorder: false,
cacheSeconds: null,
theme: 'new-theme-for-stats',
locale: 'en',
},
},
});
});
it('should handle data change when stats local is changed', () => {
const mockEvent = { target: { value: 'uk' } };
const newDataInput = {
...dataInput,
mediumDynamicBlogs: 'some-medium-blogs-value',
githubStatsOptions: {
theme: 'theme-1',
titleColor: 'some-color',
textColor: 'abcd',
bgColor: 'random-color',
hideBorder: false,
cacheSeconds: null,
theme: 'new-theme-for-stats',
locale: 'en',
},
};
const newSocialInput = {
medium: '@abcd',
};
const addOnComponent = mount(
<Addons
data={newDataInput}
social={newSocialInput}
handleCheckChange={mockHandleCheckChange}
handleDataChange={mockHandleDataChange}
/>,
);
addOnComponent.find('#github-stats-open-btn').simulate('click', {});
addOnComponent.find('#stats-locale').simulate('change', mockEvent);
jest.runAllTimers();
expect(mockHandleDataChange).toHaveBeenCalledTimes(1);
expect(mockHandleDataChange).toHaveBeenCalledWith('githubStatsOptions', {
target: {
value: {
theme: 'theme-1',
titleColor: 'some-color',
textColor: 'abcd',
bgColor: 'random-color',
hideBorder: false,
cacheSeconds: null,
theme: 'new-theme-for-stats',
locale: 'uk',
},
},
});
});
it('should handle data change when stats local is changed', () => {
const mockEvent = { target: { checked: true } };
const newDataInput = {
...dataInput,
mediumDynamicBlogs: 'some-medium-blogs-value',
githubStatsOptions: {
theme: 'theme-1',
titleColor: 'some-color',
textColor: 'abcd',
bgColor: 'random-color',
hideBorder: false,
cacheSeconds: null,
theme: 'new-theme-for-stats',
locale: 'en',
},
};
const newSocialInput = {
medium: '@abcd',
};
const addOnComponent = mount(
<Addons
data={newDataInput}
social={newSocialInput}
handleCheckChange={mockHandleCheckChange}
handleDataChange={mockHandleDataChange}
/>,
);
addOnComponent.find('#github-stats-open-btn').simulate('click', {});
addOnComponent.find('#stats-hide-border').simulate('change', mockEvent);
jest.runAllTimers();
expect(mockHandleDataChange).toHaveBeenCalledTimes(1);
expect(mockHandleDataChange).toHaveBeenCalledWith('githubStatsOptions', {
target: {
value: {
theme: 'theme-1',
titleColor: 'some-color',
textColor: 'abcd',
bgColor: 'random-color',
hideBorder: true,
cacheSeconds: null,
theme: 'new-theme-for-stats',
locale: 'en',
},
},
});
});
it('should render Customize Top Skills Card', () => {
const newDataInput = {
...dataInput,
mediumDynamicBlogs: 'some-medium-blogs-value',
};
const newSocialInput = {
medium: '@abcd',
};
const addOnComponent = mount(
<Addons
data={newDataInput}
social={newSocialInput}
handleCheckChange={mockHandleCheckChange}
handleDataChange={mockHandleDataChange}
/>,
);
addOnComponent.find('#top-languages-open-btn').simulate('click', {});
expect(addOnComponent).toMatchSnapshot();
});
it('should handle data change when top skills theme is changed', () => {
const mockEvent = { target: { value: 'theme-xyz' } };
const newDataInput = {
...dataInput,
mediumDynamicBlogs: 'some-medium-blogs-value',
topLanguagesOptions: {
theme: 'theme-2',
titleColor: 'title-abcd-new',
textColor: 'random-some-color',
bgColor: '1234',
hideBorder: false,
cacheSeconds: null,
theme: 'theme-xyz',
locale: 'us',
},
};
const newSocialInput = {
medium: '@abcd',
};
const addOnComponent = mount(
<Addons
data={newDataInput}
social={newSocialInput}
handleCheckChange={mockHandleCheckChange}
handleDataChange={mockHandleDataChange}
/>,
);
addOnComponent.find('#top-languages-open-btn').simulate('click', {});
addOnComponent.find('#top-lang-theme').simulate('change', mockEvent);
jest.runAllTimers();
expect(mockHandleDataChange).toHaveBeenCalledTimes(1);
expect(mockHandleDataChange).toHaveBeenCalledWith('topLanguagesOptions', {
target: {
value: {
theme: 'theme-2',
titleColor: 'title-abcd-new',
textColor: 'random-some-color',
bgColor: '1234',
hideBorder: false,
cacheSeconds: null,
theme: 'theme-xyz',
locale: 'us',
},
},
});
});
it('should handle check change when add on item inputs are changed', () => {
const mockEvent = { target: { value: 'This is a mock event' } };
const addOnComponent = mount(
<Addons
data={dataInput}
social={socialInput}
handleCheckChange={mockHandleCheckChange}
handleDataChange={mockHandleDataChange}
/>,
);
addOnComponent.find('#visitors-count').simulate('change', mockEvent);
addOnComponent.find('#github-profile-trophy').simulate('change', mockEvent);
addOnComponent.find('#github-stats').simulate('change', mockEvent);
addOnComponent.find('#top-languages').simulate('change', mockEvent);
addOnComponent.find('#twitter-badge').simulate('change', mockEvent);
addOnComponent.find('#dev-dynamic-blogs').simulate('change', mockEvent);
addOnComponent.find('#rss-dynamic-blogs').simulate('change', mockEvent);
addOnComponent.find('#medium-dynamic-blogs').simulate('change', mockEvent);
expect(mockHandleCheckChange).toHaveBeenCalledTimes(8);
expect(mockHandleCheckChange).toHaveBeenNthCalledWith(1, 'visitorsBadge');
expect(mockHandleCheckChange).toHaveBeenNthCalledWith(2, 'githubProfileTrophy');
expect(mockHandleCheckChange).toHaveBeenNthCalledWith(3, 'githubStats');
expect(mockHandleCheckChange).toHaveBeenNthCalledWith(4, 'topLanguages');
expect(mockHandleCheckChange).toHaveBeenNthCalledWith(5, 'twitterBadge');
expect(mockHandleCheckChange).toHaveBeenNthCalledWith(6, 'devDynamicBlogs');
expect(mockHandleCheckChange).toHaveBeenNthCalledWith(7, 'rssDynamicBlogs');
expect(mockHandleCheckChange).toHaveBeenNthCalledWith(8, 'mediumDynamicBlogs');
});
it('should display workflow details if devDynamicBlogs and dev social data are available', () => {
const newDataInput = {
...dataInput,
devDynamicBlogs: 'some-value',
};
const newSocialInput = {
dev: 'some-value-123',
};
const addOnComponent = shallow(
<Addons
data={newDataInput}
social={newSocialInput}
handleCheckChange={mockHandleCheckChange}
handleDataChange={mockHandleDataChange}
/>,
);
const workflowElement = addOnComponent.find('.workflow');
expect(workflowElement).toMatchSnapshot();
});
it('should display workflow details if rssDynamicBlogs and rss url data are available', () => {
const newDataInput = {
...dataInput,
rssDynamicBlogs: 'some-rss-value',
};
const newSocialInput = {
rssurl: 'url-random',
};
const addOnComponent = shallow(
<Addons
data={newDataInput}
social={newSocialInput}
handleCheckChange={mockHandleCheckChange}
handleDataChange={mockHandleDataChange}
/>,
);
const workflowElement = addOnComponent.find('.workflow');
expect(workflowElement).toMatchSnapshot();
});
it('should display workflow details if mediumDynamicBlogs, medium social data are available', () => {
const newDataInput = {
...dataInput,
mediumDynamicBlogs: 'some-medium-blogs-value',
};
const newSocialInput = {
medium: '@abcd',
};
const addOnComponent = shallow(
<Addons
data={newDataInput}
social={newSocialInput}
handleCheckChange={mockHandleCheckChange}
handleDataChange={mockHandleDataChange}
/>,
);
const workflowElement = addOnComponent.find('.workflow');
expect(workflowElement).toMatchSnapshot();
});
it('should call document create element when blog post workflow is clicked', () => {
let someElement = document.createElement('abcd');
document.createElement = jest.fn().mockReturnValueOnce(someElement);
const newDataInput = {
...dataInput,
mediumDynamicBlogs: 'some-medium-blogs-value',
};
const newSocialInput = {
medium: '@abcd',
};
const addOnComponent = shallow(
<Addons
data={newDataInput}
social={newSocialInput}
handleCheckChange={mockHandleCheckChange}
handleDataChange={mockHandleDataChange}
/>,
);
addOnComponent.find('#blog-post-worklow-span').simulate('click', {});
expect(document.createElement).toHaveBeenCalledTimes(1);
});
});
-11
View File
@@ -1,11 +0,0 @@
import React from 'react';
import toJson from 'enzyme-to-json';
import { shallow } from 'enzyme';
import Donate from '../donate';
describe('Donate', () => {
it('renders correctly', () => {
const component = shallow(<Donate />);
expect(toJson(component)).toMatchSnapshot();
});
});
-13
View File
@@ -1,13 +0,0 @@
import React from 'react';
import { shallow } from 'enzyme';
import toJson from 'enzyme-to-json';
import Footer from '../footer';
describe('Footer component', () => {
const component = shallow(<Footer />);
it('renders correctly', () => {
expect(toJson(component)).toMatchSnapshot();
});
});
-13
View File
@@ -1,13 +0,0 @@
import React from 'react';
import { shallow } from 'enzyme';
import toJson from 'enzyme-to-json';
import Header from '../header';
describe('Header', () => {
const component = shallow(<Header heading="heading" />);
it('renders correctly', () => {
expect(toJson(component)).toMatchSnapshot();
});
});
-13
View File
@@ -1,13 +0,0 @@
import React from 'react';
import { shallow } from 'enzyme';
import toJson from 'enzyme-to-json';
import Loader from '../loader';
describe('Loader', () => {
const component = shallow(<Loader />);
it('renders correctly', () => {
expect(toJson(component)).toMatchSnapshot();
});
});
-214
View File
@@ -1,214 +0,0 @@
import React from 'react';
import { shallow } from 'enzyme';
import toJson from 'enzyme-to-json';
import Markdown from '../markdown';
describe('Markdown', () => {
const props = {
data: {
ama: '',
badgeColor: '0e75b6',
badgeLabel: 'Profile views',
badgeStyle: 'flat',
collaborateOn: '',
contact: '',
currentLearn: '',
currentWork: 'currentWork',
devDynamicBlogs: false,
funFact: '',
githubProfileTrophy: false,
githubStats: false,
githubStatsOptions: {
bgColor: '',
cacheSeconds: null,
hideBorder: false,
locale: 'en',
textColor: '',
theme: '',
titleColor: '',
},
helpWith: '',
mediumDynamicBlogs: false,
rssDynamicBlogs: false,
subtitle: 'A passionate frontend developer from India',
title: 'title',
topLanguages: false,
topLanguagesOptions: {
bgColor: '',
cacheSeconds: null,
hideBorder: false,
locale: 'en',
textColor: '',
theme: '',
titleColor: '',
},
twitterBadge: false,
visitorsBadge: false,
},
link: {
blog: 'blog',
collaborateOn: 'collaborateOn',
currentWork: 'currentWork',
helpWith: 'helpWith',
portfolio: 'portfolio',
resume: 'resume',
},
prefix: {
ama: '💬 Ask me about',
blog: '📝 I regularly write articles on',
collaborateOn: '👯 Im looking to collaborate on',
contact: '📫 How to reach me',
currentLearn: '🌱 Im currently learning',
currentWork: '🔭 Im currently working on',
funFact: '⚡ Fun fact',
helpWith: '🤝 Im looking for help with',
portfolio: '👨‍💻 All of my projects are available at',
resume: '📄 Know about my experiences',
title: "Hi 👋, I'm",
},
skills: {
javascript: true,
express: false,
},
social: {
dev: 'dev',
codechef: '',
},
};
it('renders without subtitle', () => {
const component = shallow(
<Markdown
{...props}
data={{
...props.data,
subtitle: '',
}}
/>,
);
expect(toJson(component)).toMatchSnapshot();
});
it('renders without prefix.title and data.title', () => {
const component = shallow(
<Markdown
{...props}
data={{
...props.data,
title: '',
}}
prefix={{
...props.prefix,
title: '',
}}
/>,
);
expect(toJson(component)).toMatchSnapshot();
});
it('renders topLanguages is true', () => {
const component = shallow(
<Markdown
{...props}
data={{
...props.data,
topLanguages: true,
}}
/>,
);
expect(toJson(component)).toMatchSnapshot();
});
it('renders topLanguages is true and githubStats is true', () => {
const component = shallow(
<Markdown
{...props}
data={{
...props.data,
topLanguages: true,
githubStats: true,
}}
/>,
);
expect(toJson(component)).toMatchSnapshot();
});
it('renders devDynamicBlogs is true', () => {
const component = shallow(
<Markdown
{...props}
data={{
...props.data,
devDynamicBlogs: true,
}}
/>,
);
expect(toJson(component)).toMatchSnapshot();
});
it('renders without link.currentWork', () => {
const component = shallow(
<Markdown
{...props}
link={{
...props.data,
currentWork: '',
}}
/>,
);
expect(toJson(component)).toMatchSnapshot();
});
it('renders visitorsBadge is true', () => {
const component = shallow(
<Markdown
{...props}
data={{
...props.data,
visitorsBadge: true,
}}
/>,
);
expect(toJson(component)).toMatchSnapshot();
});
it('renders twitterBadge is true', () => {
const component = shallow(
<Markdown
{...props}
data={{
...props.data,
twitterBadge: true,
}}
/>,
);
expect(toJson(component)).toMatchSnapshot();
});
it('renders githubProfileTrophy is true', () => {
const component = shallow(
<Markdown
{...props}
data={{
...props.data,
githubProfileTrophy: true,
}}
/>,
);
expect(toJson(component)).toMatchSnapshot();
});
it('renders githubProfileTrophy is true', () => {
const component = shallow(
<Markdown
{...props}
data={{
...props.data,
githubProfileTrophy: true,
}}
/>,
);
expect(toJson(component)).toMatchSnapshot();
});
});
@@ -1,404 +0,0 @@
import React from 'react';
import { shallow, configure } from 'enzyme';
import Adapter from 'enzyme-adapter-react-16';
import MarkdownPreview, {
GithubProfileTrophyPreview,
GitHubStatsPreview,
SkillsPreview,
SocialPreview,
SubTitlePreview,
TitlePreview,
TopLanguagesPreview,
TwitterBadgePreview,
VisitorsBadgePreview,
WorkPreview,
SectionTitle,
DisplayWork,
DisplaySocial,
} from '../markdownPreview';
configure({ adapter: new Adapter() });
const DEFAULT_PREFIX = {
title: "Hi 👋, I'm",
currentWork: '🔭 Im currently working on',
currentLearn: '🌱 Im currently learning',
collaborateOn: '👯 Im looking to collaborate on',
helpWith: '🤝 Im looking for help with',
ama: '💬 Ask me about',
contact: '📫 How to reach me',
resume: '📄 Know about my experiences',
funFact: '⚡ Fun fact',
portfolio: '👨‍💻 All of my projects are available at',
blog: '📝 I regularly write articles on',
};
const DEFAULT_DATA = {
title: 'dummy',
subtitle: 'A passionate frontend developer from India',
currentWork: 'readme-generator',
currentLearn: '',
collaborateOn: '',
helpWith: '',
ama: '',
contact: '',
funFact: '',
twitterBadge: false,
visitorsBadge: false,
badgeStyle: 'flat',
badgeColor: '0e75b6',
badgeLabel: 'Profile views',
githubProfileTrophy: false,
githubStats: false,
githubStatsOptions: {
theme: '',
titleColor: '',
textColor: '',
bgColor: '',
hideBorder: false,
cacheSeconds: null,
locale: 'en',
},
topLanguages: false,
topLanguagesOptions: {
theme: '',
titleColor: '',
textColor: '',
bgColor: '',
hideBorder: false,
cacheSeconds: null,
locale: 'en',
},
devDynamicBlogs: false,
mediumDynamicBlogs: false,
rssDynamicBlogs: false,
};
const DEFAULT_LINK = {
currentWork: 'https://dummy.com',
collaborateOn: '',
helpWith: '',
portfolio: '',
blog: '',
resume: '',
};
const DEFAULT_SOCIAL = {
github: '',
dev: '',
linkedin: '',
codepen: 'dummy',
stackoverflow: '',
kaggle: '',
codesandbox: '',
fb: '',
instagram: '',
twitter: '',
dribbble: '',
behance: '',
medium: '',
youtube: '',
codechef: '',
hackerrank: '',
codeforces: '',
leetcode: '',
topcoder: '',
hackerearth: '',
geeks_for_geeks: '',
discord: '',
rssurl: '',
};
const DUMMY_SKILLS = {
skills: {
unity: true,
android: false,
angularjs: false,
apachecordova: false,
},
};
describe('Markdown Preview', () => {
it('renders correctly', () => {
let prefix = DEFAULT_PREFIX;
let data = DEFAULT_DATA;
let link = DEFAULT_LINK;
let social = DEFAULT_SOCIAL;
let skills = {};
const tree = shallow(<MarkdownPreview prefix={prefix} data={data} link={link} social={social} skills={skills} />);
expect(tree).toMatchSnapshot();
});
});
describe('Title Preview', () => {
it('renders correctly', () => {
let prefix = DEFAULT_PREFIX;
let data = DEFAULT_DATA;
const tree = shallow(<TitlePreview prefix={prefix.title} title={data.title} />);
expect(tree).toMatchSnapshot();
});
it('renders correctly with no prefix', () => {
let prefix = DEFAULT_PREFIX;
const tree = shallow(<TitlePreview prefix={prefix.title} title={''} />);
expect(tree).toMatchSnapshot();
});
it('renders correctly with no title', () => {
let data = DEFAULT_DATA;
const tree = shallow(<TitlePreview title={data.title} prefix={''} />);
expect(tree).toMatchSnapshot();
});
it('renders correctly with no title and prefix', () => {
const tree = shallow(<TitlePreview />);
expect(tree).toMatchSnapshot();
});
});
describe('SubTitle Preview', () => {
it('renders correctly', () => {
let data = DEFAULT_DATA;
const tree = shallow(<SubTitlePreview subtitle={data.subtitle} />);
expect(tree).toMatchSnapshot();
});
it('renders correctly with no subtitle', () => {
const tree = shallow(<SubTitlePreview subtitle={''} />);
expect(tree).toMatchSnapshot();
});
});
describe('SectionTitle Preview', () => {
it('renders correctly', () => {
const tree = shallow(<SectionTitle visible={true} label={'dummy'} />);
expect(tree).toMatchSnapshot();
});
it('renders correctly with no label', () => {
const tree = shallow(<SectionTitle visible={true} label={''} />);
expect(tree).toMatchSnapshot();
});
it('renders correctly with visible false', () => {
const tree = shallow(<SectionTitle visible={false} label={'dummy'} />);
expect(tree).toMatchSnapshot();
});
});
describe('DisplayWork Preview', () => {
it('renders correctly', () => {
let prefix = DEFAULT_PREFIX;
let data = DEFAULT_DATA;
let link = DEFAULT_LINK;
const tree = shallow(<DisplayWork prefix={prefix} project={data.currentWork} link={link.currentWork} />);
expect(tree).toMatchSnapshot();
});
it('renders correctly with no prefix, link and project', () => {
const tree = shallow(<DisplayWork prefix={undefined} project={undefined} link={undefined} />);
expect(tree).toMatchSnapshot();
});
it('renders correctly with no prefix', () => {
let data = DEFAULT_DATA;
let link = DEFAULT_LINK;
const tree = shallow(<DisplayWork prefix={undefined} project={data.currentWork} link={link.currentWork} />);
expect(tree).toMatchSnapshot();
});
it('renders correctly with no project', () => {
let prefix = DEFAULT_PREFIX;
let link = DEFAULT_LINK;
const tree = shallow(<DisplayWork prefix={prefix} project={undefined} link={link.currentWork} />);
expect(tree).toMatchSnapshot();
});
it('renders correctly with no link', () => {
let prefix = DEFAULT_PREFIX;
let data = DEFAULT_DATA;
const tree = shallow(<DisplayWork prefix={prefix} project={data.currentWork} link={undefined} />);
expect(tree).toMatchSnapshot();
});
it('renders correctly with no prefix and link', () => {
let data = DEFAULT_DATA;
const tree = shallow(<DisplayWork project={data.currentWork} />);
expect(tree).toMatchSnapshot();
});
it('renders correctly with no project and link', () => {
let prefix = DEFAULT_PREFIX;
const tree = shallow(<DisplayWork prefix={prefix} />);
expect(tree).toMatchSnapshot();
});
it('renders correctly with no project and prefix', () => {
let link = DEFAULT_LINK;
const tree = shallow(<DisplayWork link={link.currentWork} />);
expect(tree).toMatchSnapshot();
});
});
describe('DisplaySocial Preview', () => {
it('renders correctly', () => {
let social = DEFAULT_SOCIAL;
const tree = shallow(
<DisplaySocial
base="https://codepen.io"
icon="https://cdn.jsdelivr.net/npm/simple-icons@3.0.1/icons/codepen.svg"
username={social.codepen}
/>,
);
expect(tree).toMatchSnapshot();
});
it('renders correctly with no username', () => {
const tree = shallow(
<DisplaySocial
base="https://codepen.io"
icon="https://cdn.jsdelivr.net/npm/simple-icons@3.0.1/icons/codepen.svg"
username={''}
/>,
);
expect(tree).toMatchSnapshot();
});
});
describe('VisitorsBadge Preview', () => {
it('renders correctly', () => {
let data = DEFAULT_DATA;
let social = DEFAULT_SOCIAL;
const tree = shallow(
<VisitorsBadgePreview
show={data.visitorsBadge}
github={social.github}
badgeOptions={{
badgeLabel: encodeURI(data.badgeLabel),
badgeColor: data.badgeColor,
badgeStyle: data.badgeStyle,
}}
/>,
);
expect(tree).toMatchSnapshot();
});
it('renders correctly with show true', () => {
let data = DEFAULT_DATA;
let social = DEFAULT_SOCIAL;
const tree = shallow(
<VisitorsBadgePreview
show={true}
github={social.github}
badgeOptions={{
badgeLabel: encodeURI(data.badgeLabel),
badgeColor: data.badgeColor,
badgeStyle: data.badgeStyle,
}}
/>,
);
expect(tree).toMatchSnapshot();
});
});
describe('GithubProfileTrophy Preview', () => {
it('renders correctly', () => {
let data = DEFAULT_DATA;
let social = DEFAULT_SOCIAL;
const tree = shallow(<GithubProfileTrophyPreview show={data.githubProfileTrophy} github={social.github} />);
expect(tree).toMatchSnapshot();
});
it('renders correctly with show true', () => {
let data = DEFAULT_DATA;
let social = DEFAULT_SOCIAL;
const tree = shallow(<GithubProfileTrophyPreview show={true} github={social.github} />);
expect(tree).toMatchSnapshot();
});
});
describe('TwitterBadgePreview Preview', () => {
it('renders correctly', () => {
let data = DEFAULT_DATA;
let social = DEFAULT_SOCIAL;
const tree = shallow(<TwitterBadgePreview show={data.twitterBadge} twitter={social.twitter} />);
expect(tree).toMatchSnapshot();
});
it('renders correctly with show true', () => {
let data = DEFAULT_DATA;
let social = DEFAULT_SOCIAL;
const tree = shallow(<TwitterBadgePreview show={true} twitter={social.twitter} />);
expect(tree).toMatchSnapshot();
});
});
describe('Work Preview', () => {
it('renders correctly', () => {
let data = DEFAULT_DATA;
let prefix = DEFAULT_PREFIX;
let link = DEFAULT_LINK;
let props = { data: data, prefix: prefix, link: link };
const tree = shallow(<WorkPreview work={props} />);
expect(tree).toMatchSnapshot();
});
});
describe('Social Preview', () => {
it('renders correctly', () => {
let social = DEFAULT_SOCIAL;
const tree = shallow(<SocialPreview social={social} />);
expect(tree).toMatchSnapshot();
});
});
describe('Skills Preview', () => {
it('renders correctly', () => {
let skills = DUMMY_SKILLS.skills;
const tree = shallow(<SkillsPreview skills={skills} />);
expect(tree).toMatchSnapshot();
});
it('renders correctly with no skills', () => {
let skills = {};
const tree = shallow(<SkillsPreview skills={skills} />);
expect(tree).toMatchSnapshot();
});
});
describe('TopLanguages Preview', () => {
it('renders correctly', () => {
let data = DEFAULT_DATA;
let social = DEFAULT_SOCIAL;
const tree = shallow(
<TopLanguagesPreview show={data.topLanguages} github={social.github} options={data.topLanguagesOptions} />,
);
expect(tree).toMatchSnapshot();
});
it('renders correctly with show true', () => {
let data = DEFAULT_DATA;
let social = DEFAULT_SOCIAL;
const tree = shallow(<TopLanguagesPreview show={true} github={social.github} options={data.topLanguagesOptions} />);
expect(tree).toMatchSnapshot();
});
});
describe('GitHubStats Preview', () => {
it('renders correctly', () => {
let data = DEFAULT_DATA;
let social = DEFAULT_SOCIAL;
const tree = shallow(
<GitHubStatsPreview show={data.githubStats} github={social.github} options={data.githubStatsOptions} />,
);
expect(tree).toMatchSnapshot();
});
it('renders correctly', () => {
let data = DEFAULT_DATA;
let social = DEFAULT_SOCIAL;
const tree = shallow(<GitHubStatsPreview show={true} github={social.github} options={data.githubStatsOptions} />);
expect(tree).toMatchSnapshot();
});
});
-40
View File
@@ -1,40 +0,0 @@
import React from 'react';
import { shallow } from 'enzyme';
import toJson from 'enzyme-to-json';
import Skills from '../skills';
jest.mock('../../constants/skills', () => ({
__esModule: true,
categorizedSkills: {
language: {
title: 'Programming Languages',
skills: ['javascript'],
},
frontend_dev: {
title: 'Frontend Development',
skills: ['react', 'svelte'],
},
},
icons: {
javascript: 'javascript.svg',
react: 'react.svg',
svelte: 'svelte.svg',
},
}));
describe('Skills', () => {
it('renders correctly', () => {
const component = shallow(<Skills skills={{ javascript: true }} />);
expect(toJson(component)).toMatchSnapshot();
});
it('calls handleSkillsChange prop when a skill is clicked', () => {
const mockFn = jest.fn();
const component = shallow(<Skills skills={{ javascript: true }} handleSkillsChange={mockFn} />);
component.find('#javascript').simulate('change');
expect(mockFn).toHaveBeenCalledTimes(1);
});
});
-44
View File
@@ -1,44 +0,0 @@
import React from 'react';
import { shallow } from 'enzyme';
import toJson from 'enzyme-to-json';
import Social from '../social';
describe('Social', () => {
const mockEvent = { target: { value: 'This is a mock event' } };
const props = {
social: {
github: 'github ',
twitter: 'twitter',
dev: 'dev',
codepen: 'codepen',
codesandbox: 'codesandbodx',
stackoverflow: 'stackoverflow',
linkedin: 'linkedin',
kaggle: 'kaggle',
fb: 'fb',
instagram: 'instagram',
dribble: 'dribble',
behance: 'behance',
medium: 'medium',
youtube: 'youtube',
codechef: 'codechef',
hackerrack: 'hackerranck',
codeforces: 'codeforces',
leetcode: 'leetcode',
topcoder: 'topcoder',
hackerearth: '@hackerearth',
geeks_for_geeks: 'geeks_for_geeks',
discord: 'discord',
rssurl: 'rssurl',
},
handleSocialChange: jest.fn().mockReturnValue({}),
};
it('renders correctly', () => {
const component = shallow(<Social {...props} />);
for (let i = 0; i < component.find('input').length; i++) {
component.find('input').at(i).simulate('change', mockEvent);
}
expect(toJson(component)).toMatchSnapshot();
});
});
-26
View File
@@ -1,26 +0,0 @@
import React from 'react';
import { shallow } from 'enzyme';
import toJson from 'enzyme-to-json';
import Subtitle from '../subtitle';
describe('Subtitle', () => {
const mockEvent = { target: { value: 'This is a mock event' } };
const props = {
data: {
subtitle: 'A frontend developer',
},
handleDataChange: jest.fn().mockReturnValue({}),
};
const component = shallow(<Subtitle {...props} />);
it('renders correctly', () => {
expect(toJson(component)).toMatchSnapshot();
});
it('calls onChange', () => {
component.find('input').at(0).simulate('change', mockEvent);
expect(props.handleDataChange).toBeCalledWith('subtitle', mockEvent);
});
});
-27
View File
@@ -1,27 +0,0 @@
import React from 'react';
import { shallow } from 'enzyme';
import toJson from 'enzyme-to-json';
import Title from '../title';
describe('Title', () => {
const mockEvent = { target: { value: 'This is a mock event' } };
const props = {
prefix: {
title: 'test_title',
currentWork: 'test_currentwork',
},
data: { title: 'test_data' },
link: { currentWork: 'test_currentwork' },
handlePrefixChange: jest.fn().mockReturnValue({}),
handleLinkChange: jest.fn().mockReturnValue({}),
handleDataChange: jest.fn().mockReturnValue({}),
};
it('renders title component correctly', () => {
const component = shallow(<Title {...props} />);
component.find('input').at(0).simulate('change', mockEvent);
component.find('input').at(1).simulate('change', mockEvent);
expect(toJson(component)).toMatchSnapshot();
});
});
-28
View File
@@ -1,28 +0,0 @@
import React from 'react';
import { shallow } from 'enzyme';
import toJson from 'enzyme-to-json';
import Work from '../work';
describe('Work', () => {
const mockEvent = { target: { value: 'This is a mock event' } };
const props = {
prefix: {
title: 'test_title',
currentWork: 'test_currentwork',
},
data: { title: 'test_data' },
link: { currentWork: 'test_currentwork' },
handlePrefixChange: jest.fn().mockReturnValue({}),
handleLinkChange: jest.fn().mockReturnValue({}),
handleDataChange: jest.fn().mockReturnValue({}),
};
it('renders work component correctly', () => {
const component = shallow(<Work {...props} />);
for (let i = 0; i < component.find('input').length; i++) {
component.find('input').at(i).simulate('change', mockEvent);
}
expect(toJson(component)).toMatchSnapshot();
});
});
-482
View File
@@ -1,482 +0,0 @@
import React, { useState, useEffect } from 'react';
import PropTypes from 'prop-types';
import { withPrefix } from 'gatsby';
import { ToolsIcon, XCircleIcon } from '@primer/octicons-react';
import latestBlogs from '../utils/workflows';
import links from '../constants/page-links';
import { isMediumUsernameValid, isGitHubUsernameValid } from '../utils/validation';
const AddonsItem = (props) => {
const { inputId, inputChecked, onInputChange, Options, children } = props;
const [open, setOpen] = useState(false);
const Icon = open ? XCircleIcon : ToolsIcon;
return (
<>
<div className="py-2 flex justify-start items-center text-sm sm:text-lg">
<label htmlFor={inputId} className="checkbox-label flex items-center">
<input
id={inputId}
type="checkbox"
className="checkbox-label__input"
checked={inputChecked}
onChange={onInputChange}
/>
<span className="checkbox-label__control" />
<span className="pl-4">{children}</span>
</label>
{Options && (
<button
type="button"
id={`${inputId}-open-btn`}
onClick={() => setOpen(!open)}
className="flex ml-3 focus:bg-gray-400"
style={{ outline: 'none' }}
>
<Icon className="transform scale-100 md:scale-125" />
</button>
)}
</div>
{Options && open && Options}
</>
);
};
AddonsItem.propTypes = {
inputId: PropTypes.string.isRequired,
inputChecked: PropTypes.bool.isRequired,
onInputChange: PropTypes.func.isRequired,
Options: PropTypes.element.isRequired,
children: PropTypes.element.isRequired,
};
const CustomizeOptions = ({ title, CustomizationOptions }) => (
<div className="border-2 border-solid border-gray-900 bg-gray-100 p-2 ml-8" style={{ maxWidth: '21rem' }}>
<header className="text-base sm:text-lg">{title}</header>
<hr className="border-gray-500" />
<div className="text-sm sm:text-lg flex flex-col mt-2 ml-0 md:ml-4">{CustomizationOptions}</div>
</div>
);
CustomizeOptions.propTypes = {
title: PropTypes.string.isRequired,
CustomizationOptions: PropTypes.element.isRequired,
};
const CustomizeBadge = ({ githubName, badgeOptions, onBadgeUpdate }) => (
<>
<label htmlFor="badge-style">
Style:&nbsp;
<select
id="badge-style"
onChange={(e) => onBadgeUpdate('badgeStyle', e.target.value)}
value={badgeOptions.badgeStyle}
>
<option value="flat">Flat</option>
<option value="flat-square">Flat Square</option>
<option value="plastic">Plastic</option>
</select>
</label>
<label htmlFor="badge-color">
Color:&nbsp;
<input
type="color"
id="badge-color"
defaultValue={`#${badgeOptions.badgeColor}`}
className="w-6"
onChange={(e) => onBadgeUpdate('badgeColor', e.target.value.replace('#', ''))}
/>
</label>
<label htmlFor="badge-label-text">
Label Text:&nbsp;
<input
type="text"
id="badge-label-text"
placeholder="Profile views"
className="w-2/4 bg-gray-300 pl-2"
onChange={(e) => onBadgeUpdate('badgeLabel', e.target.value.trim())}
defaultValue={badgeOptions.badgeLabel}
/>
</label>
<span className="mt-2 flex items-center">
Preview:&nbsp;
{isGitHubUsernameValid(githubName) ? (
<img
src={`https://komarev.com/ghpvc/?username=${githubName}&label=${encodeURI(badgeOptions.badgeLabel)}&color=${
badgeOptions.badgeColor
}&style=${badgeOptions.badgeStyle}`}
alt="profile-visitors-count"
/>
) : (
<span className="text-xxs md:text-sm text-red-600">Invalid GitHub username</span>
)}
</span>
</>
);
CustomizeBadge.propTypes = {
githubName: PropTypes.string.isRequired,
badgeOptions: PropTypes.object.isRequired,
onBadgeUpdate: PropTypes.func.isRequired,
};
const CustomizeGithubStatsBase = ({ prefix, options, onUpdate }) => (
<>
<label htmlFor={`${prefix}-theme`}>
Theme:&nbsp;
<select
id={`${prefix}-theme`}
onChange={({ target: { value } }) => onUpdate('theme', value)}
defaultValue={options.theme}
>
<option value="none">none</option>
<option value="dark">Dark</option>
<option value="radical">Radical</option>
<option value="merko">Merko</option>
<option value="gruvbox">Gruvbox</option>
<option value="tokyonight">Tokyonight</option>
<option value="onedark">Onedark</option>
<option value="cobalt">Cobalt</option>
<option value="synthwave">Synthwave</option>
<option value="highcontrast">Highcontrast</option>
<option value="dracula">Dracula</option>
</select>
</label>
<label htmlFor={`${prefix}-title-color`}>
Title Color:&nbsp;
<input
type="color"
id={`${prefix}-title-color`}
defaultValue={`#${options.titleColor}`}
className="w-6"
onChange={(e) => onUpdate('titleColor', e.target.value.replace('#', ''))}
/>
</label>
<label htmlFor={`${prefix}-text-color`}>
Text Color:&nbsp;
<input
type="color"
id={`${prefix}-text-color`}
defaultValue={`#${options.textColor}`}
className="w-6"
onChange={(e) => onUpdate('textColor', e.target.value.replace('#', ''))}
/>
</label>
<label htmlFor={`${prefix}-bg-color`}>
Background Color:&nbsp;
<input
type="color"
id={`${prefix}-bg-color`}
defaultValue={`#${options.bgColor}`}
className="w-6"
onChange={(e) => onUpdate('bgColor', e.target.value.replace('#', ''))}
/>
</label>
<label htmlFor={`${prefix}-hide-border`} className="checkbox-label">
Hide border:&nbsp;
<input
id={`${prefix}-hide-border`}
type="checkbox"
className="checkbox-label__input"
checked={options.hideBorder}
onChange={(e) => onUpdate('hideBorder', e.target.checked)}
/>
<span className="checkbox-label__control" />
</label>
<label htmlFor={`${prefix}-cache-seconds`}>
Cache Seconds:&nbsp;
<input
id={`${prefix}-cache-seconds`}
type="number"
min={1800}
max={86400}
placeholder={1800}
defaultValue={options.cacheSeconds}
onChange={(e) => onUpdate('cacheSeconds', e.target.value)}
/>
</label>
<label htmlFor={`${prefix}-locale`}>
Locale:&nbsp;
<input
id={`${prefix}-locale`}
type="text"
placeholder="en"
defaultValue={options.locale}
onChange={(e) => onUpdate('locale', e.target.value)}
size="2"
/>
</label>
</>
);
CustomizeGithubStatsBase.propTypes = {
prefix: PropTypes.string.isRequired,
options: PropTypes.object.isRequired,
onUpdate: PropTypes.func.isRequired,
};
const CustomizeStreakStats = ({ prefix, options, onUpdate }) => (
<>
<label htmlFor={`${prefix}-theme`}>
Theme:&nbsp;
<select
id={`${prefix}-theme`}
onChange={({ target: { value } }) => onUpdate('theme', value)}
defaultValue={options.theme}
>
<option value="default">default</option>
<option value="dark">dark</option>
<option value="highcontrast">highcontrast</option>
</select>
</label>
</>
);
CustomizeStreakStats.propTypes = {
prefix: PropTypes.string.isRequired,
options: PropTypes.object.isRequired,
onUpdate: PropTypes.func.isRequired,
};
const Addons = (props) => {
const { data, social, handleDataChange, handleCheckChange } = props;
const [debounce, setDebounce] = useState(undefined);
const [badgeOptions, setBadgeOptions] = useState({
badgeStyle: data.badgeStyle,
badgeColor: data.badgeColor,
badgeLabel: data.badgeLabel,
});
useEffect(() => {
setBadgeOptions({
badgeStyle: data.badgeStyle,
badgeColor: data.badgeColor,
badgeLabel: data.badgeLabel,
});
}, [data.badgeStyle, data.badgeColor, data.badgeLabel]);
const [githubStatsOptions, setGithubStatsOptions] = useState({
...data.githubStatsOptions,
});
useEffect(() => {
setGithubStatsOptions({
...data.githubStatsOptions,
});
}, [data.githubStatsOptions]);
const [topLanguagesOptions, setTopLanguagesOptions] = useState({
...data.topLanguagesOptions,
});
useEffect(() => {
setTopLanguagesOptions({
...data.topLanguagesOptions,
});
}, [data.topLanguagesOptions]);
const [streakStatsOptions, setStreakStatsOptions] = useState({
...data.streakStatsOptions,
});
useEffect(() => {
setStreakStatsOptions({
...data.streakStatsOptions,
});
}, [data.streakStatsOptions]);
const blogPostPorkflow = () => {
const payload = {
dev: {
show: data.devDynamicBlogs,
username: social.dev,
},
medium: {
show: data.mediumDynamicBlogs,
username: social.medium,
},
rssurl: {
show: data.rssDynamicBlogs,
username: social.rssurl,
},
};
const actionContent = latestBlogs(payload);
const tempElement = document.createElement('a');
tempElement.setAttribute('href', `data:text/yaml;charset=utf-8,${encodeURIComponent(actionContent)}`);
tempElement.setAttribute('download', 'blog-post-workflow.yml');
tempElement.style.display = 'none';
document.body.appendChild(tempElement);
tempElement.click();
document.body.removeChild(tempElement);
};
const onBadgeUpdate = (option, value) => {
const callback = () => {
const newVal = option === 'badgeLabel' && value === '' ? 'Profile views' : value;
setBadgeOptions({ ...badgeOptions, [option]: newVal });
handleDataChange(option, { target: { value: newVal } });
};
clearTimeout(debounce);
setDebounce(setTimeout(callback, 300));
};
const onStatsUpdate = (option, value) => {
const newStatsOptions = { ...githubStatsOptions, [option]: value };
setGithubStatsOptions(newStatsOptions);
handleDataChange('githubStatsOptions', {
target: { value: newStatsOptions },
});
};
const onTopLangUpdate = (option, value) => {
const newLangOptions = { ...topLanguagesOptions, [option]: value };
setTopLanguagesOptions(newLangOptions);
handleDataChange('topLanguagesOptions', {
target: { value: newLangOptions },
});
};
const onStreakStatsUpdate = (option, value) => {
const newStreakStatsOptions = { ...streakStatsOptions, [option]: value };
setStreakStatsOptions(newStreakStatsOptions);
handleDataChange('streakStatsOptions', {
target: { value: newStreakStatsOptions },
});
};
return (
<div className="flex justify-center items-start flex-col w-full px-2 sm:px-6 mb-10">
<div className="text-xl sm:text-2xl font-bold font-title mt-2 mb-2">Add-ons</div>
<AddonsItem
inputId="visitors-count"
inputChecked={data.visitorsBadge}
onInputChange={() => handleCheckChange('visitorsBadge')}
Options={
<CustomizeOptions
title="Customize Badge"
CustomizationOptions={
<CustomizeBadge githubName={social.github} badgeOptions={badgeOptions} onBadgeUpdate={onBadgeUpdate} />
}
/>
}
>
display visitors count badge
</AddonsItem>
<AddonsItem
inputId="github-profile-trophy"
inputChecked={data.githubProfileTrophy}
onInputChange={() => handleCheckChange('githubProfileTrophy')}
>
display github trophy
</AddonsItem>
<AddonsItem
inputId="github-stats"
inputChecked={data.githubStats}
onInputChange={() => handleCheckChange('githubStats')}
Options={
<CustomizeOptions
title="Customize Github Stats Card"
CustomizationOptions={
<CustomizeGithubStatsBase prefix="stats" options={githubStatsOptions} onUpdate={onStatsUpdate} />
}
/>
}
>
display github profile stats card
</AddonsItem>
<AddonsItem
inputId="top-languages"
inputChecked={data.topLanguages}
onInputChange={() => handleCheckChange('topLanguages')}
Options={
<CustomizeOptions
title="Customize Top Skills Card"
CustomizationOptions={
<CustomizeGithubStatsBase prefix="top-lang" options={topLanguagesOptions} onUpdate={onTopLangUpdate} />
}
/>
}
>
display top skills
</AddonsItem>
<AddonsItem
inputId="streak-stats"
inputChecked={data.streakStats}
onInputChange={() => handleCheckChange('streakStats')}
Options={
<CustomizeOptions
title="Customize Streak Stats Card"
CustomizationOptions={
<CustomizeStreakStats prefix="streak-stats" options={streakStatsOptions} onUpdate={onStreakStatsUpdate} />
}
/>
}
>
display github streak stats
</AddonsItem>
<AddonsItem
inputId="twitter-badge"
inputChecked={data.twitterBadge}
onInputChange={() => handleCheckChange('twitterBadge')}
>
display twitter badge
</AddonsItem>
<AddonsItem
inputId="dev-dynamic-blogs"
inputChecked={data.devDynamicBlogs}
onInputChange={() => handleCheckChange('devDynamicBlogs')}
>
display latest dev.to blogs dynamically (GitHub Action)
</AddonsItem>
<AddonsItem
inputId="medium-dynamic-blogs"
inputChecked={data.mediumDynamicBlogs}
onInputChange={() => handleCheckChange('mediumDynamicBlogs')}
>
display latest medium blogs dynamically (GitHub Action)
</AddonsItem>
<AddonsItem
inputId="rss-dynamic-blogs"
inputChecked={data.rssDynamicBlogs}
onInputChange={() => handleCheckChange('rssDynamicBlogs')}
>
display latest blogs from your personal blog dynamically (GitHub Action)
</AddonsItem>
{(data.devDynamicBlogs && social.dev) ||
(data.rssDynamicBlogs && social.rssurl) ||
(data.mediumDynamicBlogs && social.medium && isMediumUsernameValid(social.medium)) ? (
<div className="workflow">
<div>
download
<span
id="blog-post-worklow-span"
onClick={blogPostPorkflow}
onKeyDown={(e) => e.keyCode === 13 && blogPostPorkflow()}
role="button"
tabIndex="0"
style={{ cursor: 'pointer', color: '#002ead' }}
>
{' '}
blog-post-workflow.yml
</span>{' '}
file(learn
<a href={withPrefix(links.addons)} target="blank" style={{ color: '#002ead' }}>
{' '}
how to setup
</a>
)
</div>
</div>
) : (
''
)}
</div>
);
};
export default Addons;
Addons.propTypes = {
data: PropTypes.object.isRequired,
social: PropTypes.object.isRequired,
handleDataChange: PropTypes.func.isRequired,
handleCheckChange: PropTypes.func.isRequired,
};
@@ -0,0 +1,33 @@
'use client';
import { useEffect } from 'react';
import { GoogleAnalytics } from '@next/third-parties/google';
import { useConsent } from '@/hooks/use-consent';
import { initializeAnalytics } from '@/lib/analytics';
/**
* Conditionally loads Google Analytics based on user consent
*/
export function ConditionalAnalytics() {
const { status } = useConsent();
const gaId = process.env.NEXT_PUBLIC_GA_ID;
// Initialize analytics when consent is accepted
useEffect(() => {
if (status === 'accepted' && gaId) {
// Small delay to ensure GA4 is loaded
const timer = setTimeout(() => {
initializeAnalytics();
}, 1000);
return () => clearTimeout(timer);
}
}, [status, gaId]);
// Only render GoogleAnalytics if consent is accepted and GA ID exists
if (status !== 'accepted' || !gaId) {
return null;
}
return <GoogleAnalytics gaId={gaId} />;
}
-88
View File
@@ -1,88 +0,0 @@
import React from 'react';
const Donate = () => (
<>
<div className="text-center text-4xl my-2">
Support&nbsp;
<span role="img" aria-label="praying hand emoji">
🙏
</span>
</div>
<div className="flex flex-col sm:flex-row items-start justify-between">
<div className="w-full sm:w-2/3">
<div className="text-2xl mb-2">Are you using the tool and happy with it to create your GitHub Profile?</div>
<div className="text-lg">Your kind support keeps open-source tools like this free for others.</div>
<div className="mt-4">
<a
className="flex items-center justify-start w-20"
href="https://twitter.com/intent/tweet?text=Wow:&url=https%3A%2F%2Frahuldkjain.github.io%2Fgithub-profile-readme-generator"
>
<img
className="w-20"
src="https://img.shields.io/twitter/url?style=social&url=https%3A%2F%2Frahuldkjain.github.io%2Fgithub-profile-readme-generator"
alt="tweet github profile readme generator"
/>
</a>
Let the world know how you feel using this tool. Share with others on twitter.
</div>
</div>
<div className="w-full sm:w-1/3 flex flex-col justify-center items-center">
<span>
Tip
<span role="img" aria-label="Dollar medal">
💰
</span>
</span>
{/* Ko-Fi */}
<a
href="https://ko-fi.com/A0A81XXSX"
className="flex items-center justify-evenly bg-red-500 text-white py-2 px-4 my-2"
target="_blank"
rel="noreferrer"
>
<img
className="w-6 h-6 mr-2"
src="https://www.vectorlogo.zone/logos/ko-fi/ko-fi-icon.svg"
alt="Buy ko-fi for rahuldkjain"
/>
Buy me a ko-fi
</a>
{/* Paypal */}
<a
href="https://www.paypal.me/rahuldkjain/10"
className="flex items-center justify-evenly bg-white-500 text-white py-2 px-4 my-2 border border-solid"
target="_blank"
rel="noreferrer"
>
<img
className="w-32 h-4"
src="https://cdn.worldvectorlogo.com/logos/paypal-2.svg"
alt="Donate rahuldkjain via paypal"
/>
{/* <img
className="w-6 h-6 mr-2"
src="https://www.vectorlogo.zone/logos/paypal/paypal-ar21.svg"
alt="Donate rahuldkjain via paypal"
/>
Paypal */}
</a>
{/* BuyMeACoffee */}
<a
href="https://www.buymeacoffee.com/rahuldkjain"
className="flex items-center justify-evenly bg-orange-500 text-white py-2 px-4 my-2"
target="_blank"
rel="noreferrer"
>
<img
className="w-6 h-6 mr-2"
src="https://www.vectorlogo.zone/logos/buymeacoffee/buymeacoffee-icon.svg"
alt="Buy rahuldkjain A Coffee"
/>
Buy me a coffee
</a>
</div>
</div>
</>
);
export default Donate;
-103
View File
@@ -1,103 +0,0 @@
import React from 'react';
import { Link } from 'gatsby';
import links from '../constants/page-links';
import logo from '../images/mdg.png';
import discord from '../images/Discord-Logo.png';
const Footer = () => (
<div className="bg-gray-100 p-4 flex flex-col justify-center items-center shadow-inner mt-2">
<div className="w-full flex flex-col sm:flex-row justify-evenly py-2">
<div className="sm:ml-0 sm:mr-6 order-last sm:order-none flex">
<h1 className="text-base font-bold font-title text-xl sm:text-2xl mt-3 sm:mt-0">
<div className="flex sm:flex-col items-start mb-3 sm:mb-0">
<img src={logo} className="hidden sm:block h-24" alt="github profile markdown generator logo" />
<div className="mr-2 sm:mr-0">
GitHub Profile{' '}
<img src={logo} className="inline sm:hidden h-12" alt="github profile markdown generator logo" />
<span className="block sm:inline">README Generator</span>
</div>
</div>
</h1>
</div>
<div className="text-xl sm:text-base font-light sm:font-normal">
<div className="font-title font-bold mb-4 sm:mb-2">
<strong>Pages</strong>
</div>
<div className="ml-2 sm:ml-0">
<Link to={links.addons} activeStyle={{ color: '#002ead' }}>
Addons
</Link>
</div>
<div className="ml-2 sm:ml-0">
<Link to={links.support} activeStyle={{ color: '#002ead' }}>
Support
</Link>
</div>
<div className="ml-2 sm:ml-0">
<Link to={links.about} activeStyle={{ color: '#002ead' }}>
About
</Link>
</div>
</div>
<div className="text-xl sm:text-base font-light sm:font-normal">
<div className="font-title font-bold my-4 sm:my-0 sm:mb-2">
<strong>More</strong>
</div>
<div className="ml-2 sm:ml-0">
<a
href="https://github.com/rahuldkjain/github-profile-readme-generator"
aria-label="Github rahuldkjain/github-profile-readme-generator"
target="blank"
>
Github
</a>
</div>
<div className="ml-2 sm:ml-0">
<a
href="https://github.com/rahuldkjain/github-profile-readme-generator/releases"
aria-label="Releases on Github rahuldkjain/github-profile-readme-generator"
target="blank"
>
Releases
</a>
</div>
<div className="ml-2 sm:ml-0">
<a
href="https://github.com/rahuldkjain/github-profile-readme-generator/issues"
aria-label="Issues in rahuldkjain/github-profile-readme-generator"
target="blank"
>
Issues
</a>
</div>
<div className="ml-2 sm:ml-0">
<a
href="https://github.com/rahuldkjain/github-profile-readme-generator/pulls"
aria-label="Pull Requests in rahuldkjain/github-profile-readme-generator"
target="blank"
>
Pull Requests
</a>
</div>
</div>
<div>
<div className="font-title font-bold text-xl sm:text-base my-4 sm:my-0 sm:mb-2">
<strong>Join Community</strong>
</div>
<div className="ml-2 sm:ml-0">
<a href="https://discord.gg/HHMs7Eg" aria-label="Discord of the community" target="blank">
<img src={discord} className="h-12" alt="Discord of the community" />
</a>
</div>
</div>
</div>
<div className="py-2 mt-2">
Developed in India{' '}
<span role="img" aria-label="india">
{' '}
🇮🇳
</span>
</div>
</div>
);
export default Footer;
@@ -0,0 +1,128 @@
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { describe, it, expect, vi } from 'vitest';
import { FormInput } from '../form-input';
describe('FormInput', () => {
it('renders basic input with label', () => {
render(<FormInput id="test-input" label="Test Label" />);
expect(screen.getByLabelText('Test Label')).toBeInTheDocument();
expect(screen.getByRole('textbox')).toBeInTheDocument();
});
it('renders without label when not provided', () => {
render(<FormInput id="test-input" placeholder="Enter text" />);
expect(screen.getByPlaceholderText('Enter text')).toBeInTheDocument();
expect(screen.queryByRole('label')).not.toBeInTheDocument();
});
it('shows required asterisk when required prop is true', () => {
render(<FormInput id="test-input" label="Required Field" required />);
expect(screen.getByText('*')).toBeInTheDocument();
expect(screen.getByText('*')).toHaveClass('text-destructive');
});
it('displays error message when error prop is provided', () => {
const errorMessage = 'This field is required';
render(<FormInput id="test-input" label="Test Field" error={errorMessage} />);
const errorElement = screen.getByRole('alert');
expect(errorElement).toBeInTheDocument();
expect(errorElement).toHaveTextContent(errorMessage);
expect(errorElement).toHaveClass('text-destructive');
});
it('displays helper text when provided and no error', () => {
const helperText = 'This is helpful information';
render(<FormInput id="test-input" label="Test Field" helperText={helperText} />);
expect(screen.getByText(helperText)).toBeInTheDocument();
expect(screen.getByText(helperText)).toHaveClass('text-muted-foreground');
});
it('hides helper text when error is present', () => {
const helperText = 'This is helpful information';
const errorMessage = 'Error occurred';
render(
<FormInput id="test-input" label="Test Field" helperText={helperText} error={errorMessage} />
);
expect(screen.getByText(errorMessage)).toBeInTheDocument();
expect(screen.queryByText(helperText)).not.toBeInTheDocument();
});
it('applies error styling when error is present', () => {
render(<FormInput id="test-input" label="Test Field" error="Error message" />);
const input = screen.getByRole('textbox');
expect(input).toHaveClass('border-destructive', 'focus:ring-destructive');
});
it('applies custom className', () => {
const customClass = 'custom-input-class';
render(<FormInput id="test-input" className={customClass} />);
const input = screen.getByRole('textbox');
expect(input).toHaveClass(customClass);
});
it('forwards ref correctly', () => {
const ref = vi.fn();
render(<FormInput ref={ref} id="test-input" />);
expect(ref).toHaveBeenCalledWith(expect.any(HTMLInputElement));
});
it('handles user input correctly', async () => {
const user = userEvent.setup();
const handleChange = vi.fn();
render(<FormInput id="test-input" onChange={handleChange} />);
const input = screen.getByRole('textbox');
await user.type(input, 'Hello World');
expect(input).toHaveValue('Hello World');
expect(handleChange).toHaveBeenCalled();
});
it('passes through all HTML input attributes', () => {
render(
<FormInput
id="test-input"
type="email"
placeholder="Enter email"
disabled
maxLength={50}
data-testid="email-input"
/>
);
const input = screen.getByTestId('email-input');
expect(input).toHaveAttribute('type', 'email');
expect(input).toHaveAttribute('placeholder', 'Enter email');
expect(input).toBeDisabled();
expect(input).toHaveAttribute('maxLength', '50');
});
it('associates label with input using htmlFor and id', () => {
render(<FormInput id="test-input" label="Test Label" />);
const label = screen.getByText('Test Label');
const input = screen.getByRole('textbox');
expect(label).toHaveAttribute('for', 'test-input');
expect(input).toHaveAttribute('id', 'test-input');
});
it('has proper accessibility attributes for error state', () => {
render(<FormInput id="test-input" label="Test Field" error="Error message" />);
const errorElement = screen.getByRole('alert');
expect(errorElement).toHaveAttribute('role', 'alert');
});
});
+40
View File
@@ -0,0 +1,40 @@
'use client';
import { forwardRef } from 'react';
import type { InputHTMLAttributes } from 'react';
export interface FormCheckboxProps extends Omit<InputHTMLAttributes<HTMLInputElement>, 'type'> {
label?: string;
error?: string;
}
export const FormCheckbox = forwardRef<HTMLInputElement, FormCheckboxProps>(
({ label, error, className = '', ...props }, ref) => {
return (
<div className="w-full space-y-1">
<div className="flex items-center gap-2">
<input
ref={ref}
type="checkbox"
className={`border-input bg-background text-primary focus:ring-ring h-4 w-4 rounded transition-colors focus:ring-2 focus:ring-offset-2 focus:outline-none disabled:cursor-not-allowed disabled:opacity-50 ${
error ? 'border-destructive' : ''
} ${className}`}
{...props}
/>
{label && (
<label htmlFor={props.id} className="text-foreground text-sm font-medium">
{label}
</label>
)}
</div>
{error && (
<p className="text-destructive text-sm" role="alert">
{error}
</p>
)}
</div>
);
}
);
FormCheckbox.displayName = 'FormCheckbox';
+40
View File
@@ -0,0 +1,40 @@
'use client';
import { forwardRef } from 'react';
import type { InputHTMLAttributes } from 'react';
export interface FormInputProps extends InputHTMLAttributes<HTMLInputElement> {
label?: string;
error?: string;
helperText?: string;
}
export const FormInput = forwardRef<HTMLInputElement, FormInputProps>(
({ label, error, helperText, className = '', ...props }, ref) => {
return (
<div className="w-full space-y-2">
{label && (
<label htmlFor={props.id} className="text-foreground block text-sm font-medium">
{label}
{props.required && <span className="text-destructive ml-1">*</span>}
</label>
)}
<input
ref={ref}
className={`border-input bg-background text-foreground placeholder:text-muted-foreground focus:ring-ring w-full rounded-lg border px-4 py-2 transition-colors focus:ring-2 focus:outline-none disabled:cursor-not-allowed disabled:opacity-50 ${
error ? 'border-destructive focus:ring-destructive' : ''
} ${className}`}
{...props}
/>
{error && (
<p className="text-destructive text-sm" role="alert">
{error}
</p>
)}
{helperText && !error && <p className="text-muted-foreground text-sm">{helperText}</p>}
</div>
);
}
);
FormInput.displayName = 'FormInput';
+24
View File
@@ -0,0 +1,24 @@
'use client';
import { forwardRef } from 'react';
import { Select, type SelectOption } from '@/components/ui/select';
export interface FormSelectProps {
label?: string;
error?: string;
helperText?: string;
placeholder?: string;
options: SelectOption[];
value?: string;
onChange?: (value: string) => void;
disabled?: boolean;
required?: boolean;
id?: string;
className?: string;
}
export const FormSelect = forwardRef<HTMLButtonElement, FormSelectProps>((props, ref) => {
return <Select ref={ref} {...props} />;
});
FormSelect.displayName = 'FormSelect';
+40
View File
@@ -0,0 +1,40 @@
'use client';
import { forwardRef } from 'react';
import type { TextareaHTMLAttributes } from 'react';
export interface FormTextareaProps extends TextareaHTMLAttributes<HTMLTextAreaElement> {
label?: string;
error?: string;
helperText?: string;
}
export const FormTextarea = forwardRef<HTMLTextAreaElement, FormTextareaProps>(
({ label, error, helperText, className = '', ...props }, ref) => {
return (
<div className="w-full space-y-2">
{label && (
<label htmlFor={props.id} className="text-foreground block text-sm font-medium">
{label}
{props.required && <span className="text-destructive ml-1">*</span>}
</label>
)}
<textarea
ref={ref}
className={`border-input bg-background text-foreground placeholder:text-muted-foreground focus:ring-ring w-full rounded-lg border px-4 py-2 transition-colors focus:ring-2 focus:outline-none disabled:cursor-not-allowed disabled:opacity-50 ${
error ? 'border-destructive focus:ring-destructive' : ''
} ${className}`}
{...props}
/>
{error && (
<p className="text-destructive text-sm" role="alert">
{error}
</p>
)}
{helperText && !error && <p className="text-muted-foreground text-sm">{helperText}</p>}
</div>
);
}
);
FormTextarea.displayName = 'FormTextarea';
@@ -0,0 +1,138 @@
'use client';
import { forwardRef } from 'react';
import {
fetchGitHubUser,
mapLanguageToSkills,
generateSmartSubtitle,
type GitHubApiError,
} from '@/lib/github-api';
import { useErrorToast, useToast } from '@/components/ui/toast';
import { trackGitHubAutofill } from '@/lib/analytics';
import type { ProfileFormData, LinksFormData, SocialFormData } from '@/lib/validations';
interface GitHubUsernameInputProps {
value: string;
name?: string;
onChange?: (e: React.ChangeEvent<HTMLInputElement>) => void;
onBlur?: (e: React.FocusEvent<HTMLInputElement>) => void;
onDataFetched?: (data: {
profile: Partial<ProfileFormData>;
links: Partial<LinksFormData>;
social: Partial<SocialFormData>;
skills: string[];
}) => void;
}
export const GitHubUsernameInput = forwardRef<HTMLInputElement, GitHubUsernameInputProps>(
function GitHubUsernameInput({ value, name, onChange, onBlur, onDataFetched }, ref) {
const errorToast = useErrorToast();
const { promise } = useToast();
const handleFetch = async () => {
if (!value.trim()) {
errorToast('Please enter a GitHub username');
return;
}
// Track GitHub auto-fill usage
trackGitHubAutofill(value.trim());
try {
const userData = await promise(fetchGitHubUser(value.trim()), {
loading: `Fetching data for ${value.trim()}...`,
success: (data) => `Successfully loaded ${data?.name || value.trim()}'s profile!`,
error: (error: GitHubApiError) => error.message,
});
if (!userData) {
errorToast('Unable to fetch user data', 'Please check the username and try again.');
return;
}
// Map GitHub data to form data
const suggestedSkills: string[] = [];
userData.topLanguages.forEach((lang) => {
suggestedSkills.push(...mapLanguageToSkills(lang));
});
if (onDataFetched) {
onDataFetched({
profile: {
title: userData.name,
subtitle: generateSmartSubtitle(userData),
},
links: {
blog: userData.blog,
},
social: {
github: userData.username,
twitter: userData.twitter,
},
skills: [...new Set(suggestedSkills)], // Remove duplicates
});
}
} catch (error) {
const apiError = error as GitHubApiError;
// Handle rate limit errors with retry action
if (apiError.type === 'rate_limit') {
errorToast(
'GitHub API Rate Limit Exceeded',
apiError.message,
apiError.retryAfter
? {
label: `Retry in ${apiError.retryAfter}m`,
onClick: () => {
setTimeout(() => handleFetch(), apiError.retryAfter! * 60 * 1000);
},
}
: undefined
);
} else {
// For other errors, show retry action
errorToast('Failed to fetch GitHub data', apiError.message, {
label: 'Retry',
onClick: handleFetch,
});
}
}
};
return (
<div className="space-y-3">
{/* Mobile: Stack vertically, Desktop: Side by side */}
<div className="flex flex-col gap-2 sm:flex-row">
<input
ref={ref}
type="text"
name={name}
value={value}
onChange={onChange}
onBlur={onBlur}
placeholder="Enter GitHub username"
className="border-border bg-input focus:border-ring focus:ring-ring w-full rounded-lg border px-3 py-2 text-sm focus:ring-2 focus:outline-none sm:flex-1"
onKeyDown={(e) => {
if (e.key === 'Enter') {
e.preventDefault();
handleFetch();
}
}}
/>
<button
onClick={handleFetch}
className="bg-primary text-primary-foreground hover:bg-primary/90 w-full rounded-lg px-4 py-2 text-sm font-medium transition-colors sm:w-auto sm:whitespace-nowrap"
aria-label="Auto-fill from GitHub profile"
>
Auto-fill
</button>
</div>
<p className="text-muted-foreground text-xs">
Enter your GitHub username and click "Auto-fill" to populate fields with your profile data
and suggest relevant skills.
</p>
</div>
);
}
);
-95
View File
@@ -1,95 +0,0 @@
import React, { useEffect, useState } from 'react';
import PropTypes from 'prop-types';
import { StarIcon, RepoForkedIcon } from '@primer/octicons-react';
import gsap from 'gsap';
import axios from 'axios';
import { Link } from 'gatsby';
import { act } from 'react-dom/test-utils';
import links from '../constants/page-links';
import logo from '../images/mdg.png';
const Header = (props) => {
const { heading } = props;
const [stats, setstats] = useState({
starsCount: 0,
forksCount: 0,
});
const shouldRequestStats = () => {
const isFirstRequest = stats.starsCount === 0;
const isVisible = window.document.visibilityState === 'visible';
const hasFocus = window.document.hasFocus();
return isFirstRequest || (isVisible && hasFocus);
};
const fetchData = async () => {
if (shouldRequestStats()) {
const response = await axios.get('https://api.github.com/repos/rahuldkjain/github-profile-readme-generator');
const { stargazers_count: stargazersCount, forks_count: forksCount } = response.data;
act(() =>
setstats({
starsCount: stargazersCount,
forksCount,
}),
);
}
};
useEffect(() => {
fetchData();
setInterval(fetchData, 60000);
gsap.set('.star, .fork', {
transformOrigin: 'center',
});
gsap.to('.star, .fork', {
rotateZ: '360',
duration: 2,
ease: 'elastic.inOut',
repeat: -1,
yoyo: true,
});
}, []);
return (
<div className="shadow flex items-center justify-center flex-col mb-2 py-2">
<Link to={links.home}>
<h1 className="text-base font-bold font-title sm:text-2xl font-medium text-blue-800 flex justify-center items-center flex-col">
<img src={logo} className="w-12 h-12" alt="github profile markdown generator logo" />
<div>{heading}</div>
</h1>
</Link>
<div className="flex justify-center items-center">
<a
href="https://github.com/rahuldkjain/github-profile-readme-generator"
aria-label="Star rahuldkjain/github-profile-readme-generator on GitHub"
target="blank"
className="mr-2"
>
<div className="text-xxs sm:text-sm border-2 border-solid border-gray-900 bg-gray-100 flex items-center justify-center py-1 px-2">
<StarIcon size={16} id="star-icon" className="px-1 w-6 star" />
Star this repo
<span className="github-count px-1 sm:px-2">{stats.starsCount}</span>
</div>
</a>
<a
href="https://github.com/rahuldkjain/github-profile-readme-generator/fork"
aria-label="Fork rahuldkjain/github-profile-readme-generator on GitHub"
target="blank"
>
<div className="text-xxs sm:text-sm border-2 border-solid border-gray-900 bg-gray-100 flex items-center justify-center py-1 px-2">
<RepoForkedIcon size={16} id="fork-icon" className="px-1 w-6 fork" />
Fork on GitHub
<span className="github-count px-1 sm:px-2">{stats.forksCount}</span>
</div>
</a>
</div>
</div>
);
};
export default Header;
Header.propTypes = {
heading: PropTypes.string.isRequired,
};
-25
View File
@@ -1,25 +0,0 @@
import React from 'react';
import PropTypes from 'prop-types';
import Header from './header';
import Footer from './footer';
const Layout = ({ children }) => (
<div className="flex flex-col min-h-screen">
<header>
<Header heading="GitHub Profile README Generator" />
</header>
<main className="flex-grow">{children}</main>
<footer>
<Footer />
</footer>
</div>
);
export default Layout;
Layout.defaultProps = {
children: '',
};
Layout.propTypes = {
children: PropTypes.element,
};
+173
View File
@@ -0,0 +1,173 @@
import Link from 'next/link';
import Image from 'next/image';
import { getAssetPath } from '@/lib/asset-path';
export function Footer() {
return (
<footer className="border-border bg-card border-t py-8">
<div className="container mx-auto px-4">
{/* Logo Section */}
<div className="mb-8 flex items-center justify-center gap-3">
<Image
src={getAssetPath('/mdg.png')}
alt="GitHub Profile README Generator Logo"
width={48}
height={48}
className="h-12 w-12"
unoptimized
/>
<span className="text-xl font-bold">GitHub Profile README Generator</span>
</div>
<div className="grid gap-8 md:grid-cols-4">
{/* About */}
<div>
<h3 className="mb-3 font-semibold">About</h3>
<p className="text-muted-foreground text-sm">
Create an awesome GitHub profile README with ease. Made with for the developer
community.
</p>
</div>
{/* Quick Links */}
<div>
<h3 className="mb-3 font-semibold">Quick Links</h3>
<ul className="space-y-2 text-sm">
<li>
<Link
href="/"
className="text-muted-foreground hover:text-primary transition-colors"
>
Generator
</Link>
</li>
<li>
<Link
href="/addons"
className="text-muted-foreground hover:text-primary transition-colors"
>
Addons
</Link>
</li>
<li>
<Link
href="/about"
className="text-muted-foreground hover:text-primary transition-colors"
>
About
</Link>
</li>
<li>
<Link
href="/support"
className="text-muted-foreground hover:text-primary transition-colors"
>
Support
</Link>
</li>
</ul>
</div>
{/* Resources */}
<div>
<h3 className="mb-3 font-semibold">Resources</h3>
<ul className="space-y-2 text-sm">
<li>
<a
href="https://github.com/rahuldkjain/github-profile-readme-generator"
className="text-muted-foreground hover:text-primary transition-colors"
target="_blank"
rel="noopener noreferrer"
>
GitHub Repository
</a>
</li>
<li>
<a
href="https://github.com/rahuldkjain/github-profile-readme-generator/issues"
className="text-muted-foreground hover:text-primary transition-colors"
target="_blank"
rel="noopener noreferrer"
>
Report Issues
</a>
</li>
<li>
<a
href="https://github.com/rahuldkjain/github-profile-readme-generator/blob/master/LICENSE"
className="text-muted-foreground hover:text-primary transition-colors"
target="_blank"
rel="noopener noreferrer"
>
License
</a>
</li>
</ul>
</div>
{/* Connect */}
<div>
<h3 className="mb-3 font-semibold">Connect</h3>
<ul className="space-y-2 text-sm">
<li>
<a
href="https://github.com/rahuldkjain"
className="text-muted-foreground hover:text-primary transition-colors"
target="_blank"
rel="noopener noreferrer"
>
GitHub
</a>
</li>
<li>
<a
href="https://twitter.com/rahuldkjain"
className="text-muted-foreground hover:text-primary transition-colors"
target="_blank"
rel="noopener noreferrer"
>
Twitter
</a>
</li>
<li>
<a
href="https://linkedin.com/in/rahuldkjain"
className="text-muted-foreground hover:text-primary transition-colors"
target="_blank"
rel="noopener noreferrer"
>
LinkedIn
</a>
</li>
</ul>
</div>
</div>
<div className="text-muted-foreground border-border mt-8 border-t pt-6 text-center text-sm">
<p>
© {new Date().getFullYear()} GitHub Profile README Generator. Made with by{' '}
<a
href="https://github.com/rahuldkjain"
className="text-primary font-medium hover:underline"
target="_blank"
rel="noopener noreferrer"
>
Rahul Jain
</a>
</p>
<p className="mt-2">
Open source under the{' '}
<a
href="https://github.com/rahuldkjain/github-profile-readme-generator/blob/master/LICENSE"
className="text-primary hover:underline"
target="_blank"
rel="noopener noreferrer"
>
Apache License 2.0
</a>
</p>
</div>
</div>
</footer>
);
}
+111
View File
@@ -0,0 +1,111 @@
'use client';
import Link from 'next/link';
import Image from 'next/image';
import { usePathname } from 'next/navigation';
import { ThemeToggle } from '@/components/ui/theme-toggle';
import { AccessibilityMenu } from '@/components/ui/accessibility-menu';
import { GitHubStats } from '@/components/ui/github-stats';
import { getAssetPath } from '@/lib/asset-path';
const navigation = [
{ name: 'Generator', href: '/' },
{ name: 'Addons', href: '/addons' },
{ name: 'About', href: '/about' },
{ name: 'Support', href: '/support' },
];
interface HeaderProps {
saveStatus?: 'idle' | 'saving' | 'saved';
lastSaved?: Date | null;
}
export function Header({}: HeaderProps = {}) {
const pathname = usePathname();
return (
<header className="border-border bg-card sticky top-0 z-50 border-b">
<div className="container mx-auto px-4 py-4">
<div className="flex items-center justify-between gap-4">
{/* Logo, Title, and GitHub Stats */}
<div className="flex items-center gap-4">
<Link href="/" prefetch={true} className="flex items-center gap-3 hover:opacity-80">
<Image
src={getAssetPath('/mdg.png')}
alt="GitHub Profile README Generator Logo"
width={40}
height={40}
className="h-10 w-10"
priority
unoptimized
/>
<span className="hidden text-xl font-bold sm:inline-block lg:text-2xl">
GitHub Profile README Generator
</span>
</Link>
<GitHubStats />
</div>
{/* Right side content */}
<div className="flex items-center gap-3">
<nav className="hidden lg:block" aria-label="Main navigation">
<ul className="flex gap-4">
{navigation.map((item) => {
// Normalize paths for comparison (remove trailing slashes)
const normalizedPathname = pathname.replace(/\/$/, '') || '/';
const normalizedHref = item.href.replace(/\/$/, '') || '/';
const isActive = normalizedPathname === normalizedHref;
return (
<li key={item.name}>
<Link
href={item.href}
prefetch={true}
className={`hover:text-primary text-sm font-medium transition-colors ${
isActive ? 'text-primary font-semibold' : 'text-muted-foreground'
}`}
aria-current={isActive ? 'page' : undefined}
>
{item.name}
</Link>
</li>
);
})}
</ul>
</nav>
<div className="flex items-center gap-2">
<AccessibilityMenu />
<ThemeToggle />
</div>
</div>
</div>
{/* Mobile Navigation */}
<nav className="mt-4 lg:hidden" aria-label="Mobile navigation">
<ul className="flex gap-4 overflow-x-auto">
{navigation.map((item) => {
// Normalize paths for comparison (remove trailing slashes)
const normalizedPathname = pathname.replace(/\/$/, '') || '/';
const normalizedHref = item.href.replace(/\/$/, '') || '/';
const isActive = normalizedPathname === normalizedHref;
return (
<li key={item.name}>
<Link
href={item.href}
prefetch={true}
className={`hover:text-primary text-sm font-medium whitespace-nowrap transition-colors ${
isActive ? 'text-primary font-semibold' : 'text-muted-foreground'
}`}
aria-current={isActive ? 'page' : undefined}
>
{item.name}
</Link>
</li>
);
})}
</ul>
</nav>
</div>
</header>
);
}
+51
View File
@@ -0,0 +1,51 @@
'use client';
import { useEffect, useState } from 'react';
import { useTheme } from '@/hooks/use-theme';
import { useThemeStore } from '@/lib/store';
export function ThemeProvider({ children }: { children: React.ReactNode }) {
const [mounted, setMounted] = useState(false);
const { accessibility } = useThemeStore();
useTheme(); // Initialize theme
useEffect(() => {
setMounted(true);
}, []);
// Apply accessibility settings to document
useEffect(() => {
if (!mounted) return;
const root = document.documentElement;
// High contrast mode
if (accessibility.highContrast) {
root.classList.add('high-contrast');
} else {
root.classList.remove('high-contrast');
}
// Font size
root.classList.remove('text-small', 'text-large');
if (accessibility.fontSize === 'small') {
root.classList.add('text-small');
} else if (accessibility.fontSize === 'large') {
root.classList.add('text-large');
}
// Reduced motion (already handled by CSS)
if (accessibility.reducedMotion) {
root.style.setProperty('--motion-reduce', '1');
} else {
root.style.removeProperty('--motion-reduce');
}
}, [mounted, accessibility]);
// Prevent flash of unstyled content
if (!mounted) {
return <div style={{ visibility: 'hidden' }}>{children}</div>;
}
return <>{children}</>;
}
-80
View File
@@ -1,80 +0,0 @@
import React, { useRef, useEffect } from 'react';
import gsap from 'gsap';
const Loader = () => {
const arrow = useRef([]);
useEffect(() => {
const tl = gsap.timeline({ repeat: -1 });
tl.fromTo(
arrow.current,
{
y: 0,
color: '#3b3b4f',
},
{
y: -50,
color: '#d0d0d5',
stagger: 0.1,
duration: 0.5,
ease: 'Linear.easeNone',
},
);
tl.add('cp');
tl.fromTo(
arrow.current,
{
y: -50,
color: '#d0d0d5',
},
{
y: 0,
color: '#3b3b4f',
stagger: 0.1,
duration: 0.5,
ease: 'Linear.easeNone',
},
'cp-=0.3',
);
});
return (
<div className="loader">
<span
ref={(el) => {
arrow.current[0] = el;
}}
>
</span>
<span
ref={(el) => {
arrow.current[1] = el;
}}
>
</span>
<span
ref={(el) => {
arrow.current[2] = el;
}}
>
</span>
<span
ref={(el) => {
arrow.current[3] = el;
}}
>
</span>
<span
ref={(el) => {
arrow.current[4] = el;
}}
>
</span>
</div>
);
};
export default Loader;
-744
View File
@@ -1,744 +0,0 @@
import React from 'react';
import PropTypes from 'prop-types';
import { isMediumUsernameValid } from '../utils/validation';
import { icons, skills as SKILLS, skillWebsites } from '../constants/skills';
import {
githubStatsLinkGenerator,
topLanguagesLinkGenerator,
streakStatsLinkGenerator,
} from '../utils/link-generators';
import { DEFAULT_DATA, DEFAULT_LINK, DEFAULT_PREFIX, DEFAULT_SOCIAL, DEFAULT_SUPPORT } from '../constants/defaults';
const Title = (props) => {
const { prefix, title } = props;
if (prefix && title) {
return (
<>
{`<h1 align="center">${`${prefix} ${title}`}</h1>`}
<br />
</>
);
}
return '';
};
Title.propTypes = {
prefix: PropTypes.string.isRequired,
title: PropTypes.string.isRequired,
};
const SubTitle = (props) => {
const { subtitle } = props;
if (subtitle) {
return (
<>
{`<h3 align="center">${subtitle}</h3>`}
<br />
<br />
</>
);
}
return '';
};
SubTitle.propTypes = {
subtitle: PropTypes.string.isRequired,
};
const SectionTitle = (props) => {
const { label } = props;
if (label) {
return (
<>
{`<h3 align="left">${label}</h3>`}
<br />
</>
);
}
return '';
};
SectionTitle.propTypes = {
label: PropTypes.string.isRequired,
};
const DisplayWork = (props) => {
const { prefix, project, link } = props;
if (prefix && project) {
if (link) {
return (
<>
{`- ${prefix} [${project}](${link})`}
<br />
<br />
</>
);
}
return (
<>
{`- ${prefix} **${project}**`}
<br />
<br />
</>
);
}
if (prefix && link) {
return (
<>
{`- ${prefix} [${link}](${link})`}
<br />
<br />
</>
);
}
return '';
};
DisplayWork.defaultProps = {
prefix: '',
project: '',
link: '',
};
DisplayWork.propTypes = {
prefix: PropTypes.string,
project: PropTypes.string,
link: PropTypes.string,
};
const DisplaySocial = (props) => {
const { username, base, icon } = props;
if (username) {
return (
<>
{`<a href="${base}/${username}" target="blank"><img align="center" src="${icon}" alt="${username}" height="30" width="40" /></a>`}
<br />
</>
);
}
return '';
};
DisplaySocial.propTypes = {
username: PropTypes.string.isRequired,
base: PropTypes.string.isRequired,
icon: PropTypes.string.isRequired,
};
const VisitorsBadge = (props) => {
const { github, badgeOptions, show } = props;
const link = `https://komarev.com/ghpvc/?username=${github}&label=${badgeOptions.badgeLabel}&color=${badgeOptions.badgeColor}&style=${badgeOptions.badgeStyle}`;
if (show) {
return (
<>
{`<p align="left"> <img src="${link}" alt="${github}" /> </p>`}
<br />
<br />
</>
);
}
return '';
};
VisitorsBadge.defaultProps = {
badgeOptions: {
badgeLabel: '',
badgeColor: '',
badgeStyle: '',
},
};
VisitorsBadge.propTypes = {
github: PropTypes.string.isRequired,
badgeOptions: {
badgeLabel: PropTypes.string.isRequired,
badgeColor: PropTypes.string.isRequired,
badgeStyle: PropTypes.string.isRequired,
},
show: PropTypes.bool.isRequired,
};
const TwitterBadge = (props) => {
const { twitter, show, base } = props;
const link = `https://img.shields.io/twitter/follow/${twitter}?logo=twitter&style=for-the-badge`;
if (show) {
return (
<>
{`<p align="left"> <a href="${base}/${twitter}" target="blank"><img src="${link}" alt="${twitter}" /></a> </p>`}
<br />
<br />
</>
);
}
return '';
};
TwitterBadge.propTypes = {
twitter: PropTypes.string.isRequired,
base: PropTypes.string.isRequired,
show: PropTypes.bool.isRequired,
};
const GithubProfileTrophy = (props) => {
const { show, github } = props;
const link = `https://github-profile-trophy.vercel.app/?username=${github}`;
if (show) {
return (
<>
{`<p align="left"> <a href="https://github.com/ryo-ma/github-profile-trophy"><img src="${link}" alt="${github}" /></a> </p>`}
<br />
<br />
</>
);
}
return '';
};
GithubProfileTrophy.propTypes = {
github: PropTypes.string.isRequired,
show: PropTypes.bool.isRequired,
};
const GitHubStats = (props) => {
const { show, github, options } = props;
if (show) {
return (
<>
{`<p>&nbsp;<img align="center" src="${githubStatsLinkGenerator({
github,
options,
})}" alt="${github}" /></p>`}
<br />
<br />
</>
);
}
return '';
};
GitHubStats.defaultProps = {
options: {
theme: '',
titleColor: '',
textColor: '',
bgColor: '',
hideBorder: '',
cacheSeconds: 0,
locale: '',
},
};
GitHubStats.propTypes = {
github: PropTypes.string.isRequired,
options: {
theme: PropTypes.string,
titleColor: PropTypes.string,
textColor: PropTypes.string,
bgColor: PropTypes.string,
hideBorder: PropTypes.string,
cacheSeconds: PropTypes.number,
locale: PropTypes.string,
},
show: PropTypes.bool.isRequired,
};
const isSocial = (social) => {
let status = false;
const SOCIAL_KEYS = Object.keys(DEFAULT_SOCIAL);
Object.keys(social).forEach((key) => {
if (SOCIAL_KEYS.includes(key)) {
status = true;
}
});
return status;
};
const DisplaySkills = (props) => {
const { skills } = props;
const listChosenSkills = [];
SKILLS.forEach((skill) => {
if (skills[skill]) {
listChosenSkills.push(
`
<a href="${skillWebsites[skill]}" target="_blank" rel="noreferrer">
<img src="${icons[skill]}" alt="${skill}" width="40" height="40"/>
</a>
`,
);
}
});
return listChosenSkills.length > 0 ? (
<>
<SectionTitle label="Languages and Tools:" />
{`<p align="left">${listChosenSkills.join(' ')}</p>`}
<br />
<br />
</>
) : (
''
);
};
DisplaySkills.defaultProps = {
skills: [],
};
DisplaySkills.propTypes = {
skills: [],
};
const DisplayDynamicBlogs = (props) => {
const { show } = props;
if (show) {
return (
<>
### Blogs posts
<br />
{'<!-- BLOG-POST-LIST:START -->'}
<br />
{'<!-- BLOG-POST-LIST:END -->'}
<br />
<br />
</>
);
}
return '';
};
DisplayDynamicBlogs.defaultProps = {
show: false,
};
DisplayDynamicBlogs.propTypes = {
show: PropTypes.bool,
};
const DisplayTopLanguages = (props) => {
const { show, showStats, github, options } = props;
if (show) {
if (!showStats) {
return (
<>
{`<p><img align="center" src="${topLanguagesLinkGenerator({
github,
options,
})}" alt="${github}" /></p>`}
<br />
<br />
</>
);
}
return (
<>
{`<p><img align="left" src="${topLanguagesLinkGenerator({
github,
options,
})}" alt="${github}" /></p>`}
<br />
<br />
</>
);
}
return '';
};
DisplayTopLanguages.defaultProps = {
options: {
theme: '',
titleColor: '',
textColor: '',
bgColor: '',
hideBorder: '',
cacheSeconds: '',
locale: '',
},
};
DisplayTopLanguages.propTypes = {
github: PropTypes.string.isRequired,
options: {
theme: PropTypes.string,
titleColor: PropTypes.string,
textColor: PropTypes.string,
bgColor: PropTypes.string,
hideBorder: PropTypes.string,
cacheSeconds: PropTypes.number,
locale: PropTypes.string,
},
show: PropTypes.bool.isRequired,
showStats: PropTypes.bool.isRequired,
};
const DisplayStreakStats = (props) => {
const { show, github, options } = props;
if (show) {
return (
<>
{`<p><img align="center" src="${streakStatsLinkGenerator({
github,
options,
})}" alt="${github}" /></p>`}
<br />
<br />
</>
);
}
return '';
};
DisplayStreakStats.defaultProps = {
options: {
theme: '',
titleColor: '',
textColor: '',
bgColor: '',
hideBorder: '',
cacheSeconds: '',
locale: '',
},
};
DisplayStreakStats.propTypes = {
github: PropTypes.string.isRequired,
options: {
theme: PropTypes.string,
titleColor: PropTypes.string,
textColor: PropTypes.string,
bgColor: PropTypes.string,
hideBorder: PropTypes.string,
cacheSeconds: PropTypes.number,
locale: PropTypes.string,
},
show: PropTypes.bool.isRequired,
};
const DisplaySupport = (props) => {
const { support } = props;
let viewSupport = false;
Object.keys(support).forEach((key) => {
if (support[key]) {
viewSupport = true;
}
});
return viewSupport ? (
<div>
<SectionTitle label="Support:" />
{'<p>'}
{support.buyMeACoffee &&
`<a href="https://www.buymeacoffee.com/${support.buyMeACoffee}">
<img align="left" src="https://cdn.buymeacoffee.com/buttons/v2/default-yellow.png" height="50" width="210" alt="${support.buyMeACoffee}" /></a>`}
{support.buyMeAKofi &&
`<a href="https://ko-fi.com/${support.buyMeAKofi}">
<img align="left" src="https://cdn.ko-fi.com/cdn/kofi3.png?v=3" height="50" width="210" alt="${support.buyMeAKofi}" /></a>`}
{'</p><br><br>'}
<br />
<br />
</div>
) : (
''
);
};
DisplaySupport.defaultProps = {
support: {
buyMeACoffee: '',
buyMeAKofi: '',
},
};
DisplaySupport.propTypes = {
support: {
buyMeACoffee: PropTypes.string,
buyMeAKofi: PropTypes.string,
},
};
const Markdown = (props) => {
const { prefix, data, link, social, skills, support } = props;
const iconBaseUrl =
'https://raw.githubusercontent.com/rahuldkjain/github-profile-readme-generator/master/src/images/icons/Social/';
return (
<div id="markdown-content" className="break-words">
<>
<Title prefix={prefix.title} title={data.title} />
</>
<>
<SubTitle subtitle={data.subtitle} />
</>
<>
<VisitorsBadge
show={data.visitorsBadge}
github={social.github}
badgeOptions={{
badgeLabel: encodeURI(data.badgeLabel),
badgeColor: data.badgeColor,
badgeStyle: data.badgeStyle,
}}
/>
</>
<>
<GithubProfileTrophy show={data.githubProfileTrophy} github={social.github} />
<TwitterBadge base="https://twitter.com" show={data.twitterBadge} twitter={social.twitter} />
</>
<>
<DisplayWork prefix={prefix.currentWork} project={data.currentWork} link={link.currentWork} />
</>
<>
<DisplayWork prefix={prefix.currentLearn} project={data.currentLearn} />
</>
<>
<DisplayWork prefix={prefix.collaborateOn} project={data.collaborateOn} link={link.collaborateOn} />
</>
<>
<DisplayWork prefix={prefix.helpWith} project={data.helpWith} link={link.helpWith} />
</>
<>
<DisplayWork prefix={prefix.portfolio} link={link.portfolio} />
</>
<>
<DisplayWork prefix={prefix.blog} link={link.blog} />
</>
<>
<DisplayWork prefix={prefix.ama} project={data.ama} />
</>
<>
<DisplayWork prefix={prefix.contact} project={data.contact} />
</>
<>
<DisplayWork prefix={prefix.resume} link={link.resume} />
</>
<>
<DisplayWork prefix={prefix.funFact} project={data.funFact} />
</>
<>
<DisplayDynamicBlogs
show={
(data.devDynamicBlogs && social.dev) ||
(data.rssDynamicBlogs && social.rssurl) ||
(data.mediumDynamicBlogs && social.medium && isMediumUsernameValid(social.medium))
}
/>
</>
{isSocial(social) ? (
<>
<SectionTitle label="Connect with me:" />
{'<p align="left">'}
</>
) : (
''
)}
<br />
<>
<DisplaySocial base="https://codepen.io" icon={`${iconBaseUrl}codepen.svg`} username={social.codepen} />
</>
<>
<DisplaySocial base="https://dev.to" icon={`${iconBaseUrl}devto.svg`} username={social.dev} />
</>
<>
<DisplaySocial base="https://twitter.com" icon={`${iconBaseUrl}twitter.svg`} username={social.twitter} />
</>
<>
<DisplaySocial
base="https://linkedin.com/in"
icon={`${iconBaseUrl}linked-in-alt.svg`}
username={social.linkedin}
/>
</>
<>
<DisplaySocial
base="https://stackoverflow.com/users"
icon={`${iconBaseUrl}stack-overflow.svg`}
username={social.stackoverflow}
/>
</>
<>
<DisplaySocial
base="https://codesandbox.com"
icon={`${iconBaseUrl}codesandbox.svg`}
username={social.codesandbox}
/>
</>
<>
<DisplaySocial base="https://kaggle.com" icon={`${iconBaseUrl}kaggle.svg`} username={social.kaggle} />
</>
<>
<DisplaySocial base="https://fb.com" icon={`${iconBaseUrl}facebook.svg`} username={social.fb} />
</>
<>
<DisplaySocial base="https://instagram.com" icon={`${iconBaseUrl}instagram.svg`} username={social.instagram} />
</>
<>
<DisplaySocial base="https://dribbble.com" icon={`${iconBaseUrl}dribbble.svg`} username={social.dribbble} />
</>
<>
<DisplaySocial base="https://www.behance.net" icon={`${iconBaseUrl}behance.svg`} username={social.behance} />
</>
<>
<DisplaySocial base="https://hashnode.com" icon={`${iconBaseUrl}hashnode.svg`} username={social.hashnode} />
</>
<>
<DisplaySocial base="https://medium.com" icon={`${iconBaseUrl}medium.svg`} username={social.medium} />
</>
<>
<DisplaySocial base="https://www.youtube.com/c" icon={`${iconBaseUrl}youtube.svg`} username={social.youtube} />
</>
<>
<DisplaySocial
base="https://www.codechef.com/users"
icon="https://cdn.jsdelivr.net/npm/simple-icons@3.1.0/icons/codechef.svg"
username={social.codechef}
/>
</>
<>
<DisplaySocial
base="https://www.hackerrank.com"
icon={`${iconBaseUrl}hackerrank.svg`}
username={social.hackerrank}
/>
</>
<>
<DisplaySocial
base="https://codeforces.com/profile"
icon={`${iconBaseUrl}codeforces.svg`}
username={social.codeforces}
/>
</>
<>
<DisplaySocial
base="https://www.leetcode.com"
icon={`${iconBaseUrl}leet-code.svg`}
username={social.leetcode}
/>
</>
<>
<DisplaySocial
base="https://www.hackerearth.com"
icon={`${iconBaseUrl}hackerearth.svg`}
username={social.hackerearth}
/>
</>
<>
<DisplaySocial
base="https://auth.geeksforgeeks.org/user"
icon={`${iconBaseUrl}geeks-for-geeks.svg`}
username={social.geeks_for_geeks}
/>
</>
<>
<DisplaySocial
base="https://www.topcoder.com/members"
icon={`${iconBaseUrl}topcoder.svg`}
username={social.topcoder}
/>
</>
<>
<DisplaySocial base="https://discord.gg" icon={`${iconBaseUrl}discord.svg`} username={social.discord} />
</>
<>
<DisplaySocial base="" icon={`${iconBaseUrl}rss.svg`} username={social.rssurl} />
</>
{isSocial(social) ? (
<>
{'</p>'}
<br />
<br />
</>
) : (
''
)}
<>
<DisplaySkills skills={skills} />
</>
<>
<DisplaySupport support={support} />
</>
<>
<DisplayTopLanguages
show={data.topLanguages}
showStats={data.githubStats}
github={social.github}
options={data.topLanguagesOptions}
/>
</>
<>
<GitHubStats show={data.githubStats} github={social.github} options={data.githubStatsOptions} />
</>
<>
<DisplayStreakStats show={data.streakStats} github={social.github} options={data.streakStatsOptions} />
</>
</div>
);
};
export default Markdown;
Markdown.defaultProps = {
prefix: DEFAULT_PREFIX,
data: DEFAULT_DATA,
link: DEFAULT_LINK,
social: DEFAULT_SOCIAL,
support: DEFAULT_SUPPORT,
skills: [],
};
Markdown.propTypes = {
prefix: {
title: PropTypes.string,
currentWork: PropTypes.string,
currentLearn: PropTypes.string,
collaborateOn: PropTypes.string,
helpWith: PropTypes.string,
ama: PropTypes.string,
contact: PropTypes.string,
resume: PropTypes.string,
funFact: PropTypes.string,
portfolio: PropTypes.string,
blog: PropTypes.string,
},
data: {
title: PropTypes.string,
subtitle: PropTypes.string,
currentWork: PropTypes.string,
currentLearn: PropTypes.string,
collaborateOn: PropTypes.string,
helpWith: PropTypes.string,
ama: PropTypes.string,
contact: PropTypes.string,
funFact: PropTypes.string,
twitterBadge: false,
visitorsBadge: false,
badgeStyle: PropTypes.string,
badgeColor: PropTypes.string,
badgeLabel: PropTypes.string,
githubProfileTrophy: false,
githubStats: false,
githubStatsOptions: {
theme: PropTypes.string,
titleColor: PropTypes.string,
textColor: PropTypes.string,
bgColor: PropTypes.string,
hideBorder: false,
cacheSeconds: null,
locale: PropTypes.string,
},
topLanguages: false,
topLanguagesOptions: {
theme: PropTypes.string,
titleColor: PropTypes.string,
textColor: PropTypes.string,
bgColor: PropTypes.string,
hideBorder: false,
cacheSeconds: null,
locale: PropTypes.string,
},
streakStats: false,
streakStatsOptions: {
theme: PropTypes.string,
},
devDynamicBlogs: false,
mediumDynamicBlogs: false,
rssDynamicBlogs: false,
},
link: {},
social: {},
skills: {},
support: {},
};
-510
View File
@@ -1,510 +0,0 @@
import React from 'react';
import PropTypes from 'prop-types';
import { icons, skills as SKILLS, skillWebsites } from '../constants/skills';
import {
githubStatsLinkGenerator,
topLanguagesLinkGenerator,
streakStatsLinkGenerator,
} from '../utils/link-generators';
import { DEFAULT_DATA, DEFAULT_PREFIX, DEFAULT_SOCIAL, DEFAULT_SUPPORT } from '../constants/defaults';
export const TitlePreview = (props) => {
const { prefix, title } = props;
if (prefix && title) {
return <h1 className="text-center text-xl font-bold">{`${prefix} ${title}`}</h1>;
}
return null;
};
TitlePreview.propTypes = {
prefix: PropTypes.string.isRequired,
title: PropTypes.string.isRequired,
};
export const SubTitlePreview = (props) => {
const { subtitle } = props;
if (subtitle) {
return <h3 className="text-center font-medium">{subtitle}</h3>;
}
return null;
};
SubTitlePreview.propTypes = {
subtitle: PropTypes.string.isRequired,
};
export const SectionTitle = (props) => {
const { visible, label } = props;
if (!visible) return null;
if (label) {
return <h3 className="w-full text-lg sm:text-xl">{label}</h3>;
}
return null;
};
SectionTitle.defaultProps = {
visible: false,
};
SectionTitle.propTypes = {
visible: PropTypes.bool,
label: PropTypes.string.isRequired,
};
export const DisplayWork = (props) => {
const { prefix, project, link } = props;
if (prefix && project) {
if (link) {
return (
<div className="my-2">
{`${prefix} `}
<a href={link} className="no-underline text-blue-700" target="blank">
{project}
</a>
</div>
);
}
return (
<div className="my-2">
{`${prefix} `}
<b>{project}</b>
</div>
);
}
if (prefix && link) {
return (
<div className="my-2">
{`${prefix} `}
<a href={link} className="no-underline text-blue-700" target="blank">
{link}
</a>
</div>
);
}
return null;
};
DisplayWork.defaultProps = {
prefix: '',
project: '',
link: '',
};
DisplayWork.propTypes = {
prefix: PropTypes.string,
project: PropTypes.string,
link: PropTypes.string,
};
export const WorkPreview = (props) => {
const { work } = props;
const { prefix, data, link } = work;
return (
<>
<DisplayWork prefix={prefix.currentWork} project={data.currentWork} link={link.currentWork} />
<DisplayWork prefix={prefix.currentLearn} project={data.currentLearn} />
<DisplayWork prefix={prefix.helpWith} project={data.helpWith} link={link.helpWith} />
<DisplayWork prefix={prefix.collaborateOn} project={data.collaborateOn} link={link.collaborateOn} />
<DisplayWork prefix={prefix.ama} project={data.ama} />
<DisplayWork prefix={prefix.portfolio} link={link.portfolio} />
<DisplayWork prefix={prefix.blog} link={link.blog} />
<DisplayWork prefix={prefix.resume} link={link.resume} />
<DisplayWork prefix={prefix.contact} project={data.contact} />
<DisplayWork prefix={prefix.funFact} project={data.funFact} />
</>
);
};
WorkPreview.propTypes = {
work: PropTypes.object.isRequired,
};
export const DisplaySocial = (props) => {
const { username, base, icon } = props;
if (username) {
return (
<a className="no-underline text-blue-700 m-2" href={`${base}/${username}`} target="blank">
<img className="w-6 h-6" src={icon} alt="username" />
</a>
);
}
return null;
};
DisplaySocial.defaultProps = {
username: '',
base: '',
icon: '',
};
DisplaySocial.propTypes = {
username: PropTypes.string,
base: PropTypes.string,
icon: PropTypes.string,
};
export const SocialPreview = (props) => {
const { social } = props;
let viewSocial = false;
const iconBaseUrl =
'https://raw.githubusercontent.com/rahuldkjain/github-profile-readme-generator/master/src/images/icons/Social/';
Object.keys(social).forEach((key) => {
if (social[key] && key !== 'github') viewSocial = true;
});
return (
<div className="flex justify-start items-end flex-wrap">
<SectionTitle label="Connect with me:" visible={viewSocial} />
<>
<DisplaySocial base="https://codepen.io" icon={`${iconBaseUrl}codepen.svg`} username={social.codepen} />
</>
<>
<DisplaySocial base="https://dev.to" icon={`${iconBaseUrl}devto.svg`} username={social.dev} />
</>
<>
<DisplaySocial base="https://twitter.com" icon={`${iconBaseUrl}twitter.svg`} username={social.twitter} />
</>
<>
<DisplaySocial
base="https://linkedin.com/in"
icon={`${iconBaseUrl}linked-in-alt.svg`}
username={social.linkedin}
/>
</>
<>
<DisplaySocial
base="https://stackoverflow.com/users"
icon={`${iconBaseUrl}stack-overflow.svg`}
username={social.stackoverflow}
/>
</>
<>
<DisplaySocial
base="https://codesandbox.com"
icon={`${iconBaseUrl}codesandbox.svg`}
username={social.codesandbox}
/>
</>
<>
<DisplaySocial base="https://kaggle.com" icon={`${iconBaseUrl}kaggle.svg`} username={social.kaggle} />
</>
<>
<DisplaySocial base="https://fb.com" icon={`${iconBaseUrl}facebook.svg`} username={social.fb} />
</>
<>
<DisplaySocial base="https://instagram.com" icon={`${iconBaseUrl}instagram.svg`} username={social.instagram} />
</>
<>
<DisplaySocial base="https://dribbble.com" icon={`${iconBaseUrl}dribbble.svg`} username={social.dribbble} />
</>
<>
<DisplaySocial base="https://www.behance.net" icon={`${iconBaseUrl}behance.svg`} username={social.behance} />
</>
<>
<DisplaySocial base="https://hashnode.com" icon={`${iconBaseUrl}hashnode.svg`} username={social.hashnode} />
</>
<>
<DisplaySocial base="https://medium.com" icon={`${iconBaseUrl}medium.svg`} username={social.medium} />
</>
<>
<DisplaySocial base="https://www.youtube.com/c" icon={`${iconBaseUrl}youtube.svg`} username={social.youtube} />
</>
<>
<DisplaySocial
base="https://www.codechef.com/users"
icon="https://cdn.jsdelivr.net/npm/simple-icons@3.1.0/icons/codechef.svg"
username={social.codechef}
/>
</>
<>
<DisplaySocial
base="https://www.hackerrank.com"
icon={`${iconBaseUrl}hackerrank.svg`}
username={social.hackerrank}
/>
</>
<>
<DisplaySocial
base="https://codeforces.com/profile"
icon={`${iconBaseUrl}codeforces.svg`}
username={social.codeforces}
/>
</>
<>
<DisplaySocial
base="https://www.leetcode.com"
icon={`${iconBaseUrl}leet-code.svg`}
username={social.leetcode}
/>
</>
<>
<DisplaySocial
base="https://www.hackerearth.com"
icon={`${iconBaseUrl}hackerearth.svg`}
username={social.hackerearth}
/>
</>
<>
<DisplaySocial
base="https://auth.geeksforgeeks.org/user"
icon={`${iconBaseUrl}geeks-for-geeks.svg`}
username={social.geeks_for_geeks}
/>
</>
<>
<DisplaySocial
base="https://www.topcoder.com/members"
icon={`${iconBaseUrl}topcoder.svg`}
username={social.topcoder}
/>
</>
<>
<DisplaySocial base="https://discord.gg" icon={`${iconBaseUrl}discord.svg`} username={social.discord} />
</>
<>
<DisplaySocial base="" icon={`${iconBaseUrl}rss.svg`} username={social.rssurl} />
</>
</div>
);
};
SocialPreview.propTypes = {
social: PropTypes.object.isRequired,
};
export const VisitorsBadgePreview = (props) => {
const { github, show, badgeOptions } = props;
const link = `https://komarev.com/ghpvc/?username=${github}&label=${badgeOptions.badgeLabel}&color=${badgeOptions.badgeColor}&style=${badgeOptions.badgeStyle}`;
if (show) {
return (
<div className="text-left my-2">
{' '}
<img className="h-4 sm:h-6" src={link} alt={github} />{' '}
</div>
);
}
return null;
};
VisitorsBadgePreview.defaultProps = {
github: '',
show: false,
badgeOptions: {},
};
VisitorsBadgePreview.propTypes = {
github: PropTypes.string,
show: PropTypes.bool,
badgeOptions: PropTypes.object,
};
export const TwitterBadgePreview = (props) => {
const { twitter, show } = props;
const link = `https://img.shields.io/twitter/follow/${twitter}?logo=twitter&style=for-the-badge`;
if (show) {
return (
<div className="text-left my-2">
{' '}
<a href={`https://twitter.com/${twitter}`} target="_blank" rel="noreferrer">
<img className="h-4 sm:h-6" src={link} alt={twitter} />
</a>{' '}
</div>
);
}
return null;
};
TwitterBadgePreview.defaultProps = {
twitter: '',
show: false,
};
TwitterBadgePreview.propTypes = {
twitter: PropTypes.string,
show: PropTypes.bool,
};
export const GithubProfileTrophyPreview = (props) => {
const { github, show } = props;
const link = `https://github-profile-trophy.vercel.app/?username=${github}`;
if (show) {
return (
<div className="text-left my-2">
{' '}
<a href="https://github.com/ryo-ma/github-profile-trophy">
<img src={link} alt={github} />
</a>{' '}
</div>
);
}
return null;
};
GithubProfileTrophyPreview.defaultProps = {
github: '',
show: false,
};
GithubProfileTrophyPreview.propTypes = {
github: PropTypes.string,
show: PropTypes.bool,
};
export const GitHubStatsPreview = ({ github, options, show }) => {
if (show) {
return (
<div className="text-center mx-4 mb-4">
<img src={githubStatsLinkGenerator({ github, options })} alt={github} />
</div>
);
}
return null;
};
GitHubStatsPreview.defaultProps = {
github: '',
options: {},
show: false,
};
GitHubStatsPreview.propTypes = {
github: PropTypes.string,
options: PropTypes.object,
show: PropTypes.bool,
};
export const TopLanguagesPreview = ({ github, options, show }) => {
if (show) {
return (
<div className="text-center mx-4 mb-4">
<img src={topLanguagesLinkGenerator({ github, options })} alt={github} />
</div>
);
}
return <div className="text-center mx-4 mb-4"> &nbsp;</div>;
};
TopLanguagesPreview.defaultProps = {
github: '',
options: {},
show: false,
};
TopLanguagesPreview.propTypes = {
github: PropTypes.string,
options: PropTypes.object,
show: PropTypes.bool,
};
export const StreakStatsPreview = ({ github, options, show }) => {
if (show) {
return (
<div className="text-center mx-4 mb-4">
<img src={streakStatsLinkGenerator({ github, options })} alt={github} />
</div>
);
}
return null;
};
StreakStatsPreview.defaultProps = {
github: '',
options: {},
show: false,
};
StreakStatsPreview.propTypes = {
github: PropTypes.string,
options: PropTypes.object,
show: PropTypes.bool,
};
export const SkillsPreview = (props) => {
const { skills } = props;
const listSkills = [];
SKILLS.forEach((skill) => {
if (skills[skill]) {
listSkills.push(
<a href={skillWebsites[skill]} key={skill} target="_blank" rel="noreferrer">
<img className="mb-4 mr-4 h-6 w-6 sm:h-10 sm:w-10" src={icons[skill]} alt={skill} />
</a>,
);
}
});
return listSkills.length > 0 ? (
<div className="flex flex-wrap justify-start items-center">
<SectionTitle label="Languages and Tools:" visible />
{listSkills}
</div>
) : (
''
);
};
SkillsPreview.propTypes = {
skills: PropTypes.array.isRequired,
};
export const SupportPreview = (props) => {
const { support } = props;
let viewSupport = false;
Object.keys(support).forEach((key) => {
if (support[key]) {
viewSupport = true;
}
});
return support.buyMeACoffee || support.buyMeAKofi ? (
<div className="flex flex-wrap justify-start items-center">
<SectionTitle label="Support:" visible={viewSupport} />
{support.buyMeACoffee && (
<a href={`https://www.buymeacoffee.com/${support.buyMeACoffee}`} target="_blank" rel="noreferrer">
<img
src="https://cdn.buymeacoffee.com/buttons/v2/default-yellow.png"
alt="Buy Me A Coffee"
className="mb-4 mr-4 w-36 h-8 sm:w-52 sm:h-12"
/>
</a>
)}
{support.buyMeAKofi && (
<a href={`https://ko-fi.com/${support.buyMeAKofi}`} target="_blank" rel="noreferrer">
<img
src="https://cdn.ko-fi.com/cdn/kofi3.png?v=3"
alt="Buy Me A Ko-fi"
className="mb-4 mr-4 w-36 h-8 sm:w-52 sm:h-12"
/>
</a>
)}
</div>
) : (
''
);
};
SupportPreview.propTypes = {
support: PropTypes.object.isRequired,
};
const MarkdownPreview = (props) => {
const { prefix, data, social, skills, support } = props;
return (
<div id="markdown-preview">
<TitlePreview prefix={prefix.title} title={data.title} />
<SubTitlePreview subtitle={data.subtitle} />
<VisitorsBadgePreview
show={data.visitorsBadge}
github={social.github}
badgeOptions={{
badgeLabel: encodeURI(data.badgeLabel),
badgeColor: data.badgeColor,
badgeStyle: data.badgeStyle,
}}
/>
<GithubProfileTrophyPreview show={data.githubProfileTrophy} github={social.github} />
<TwitterBadgePreview show={data.twitterBadge} twitter={social.twitter} />
<WorkPreview work={props} />
<SocialPreview social={social} />
<SkillsPreview skills={skills} />
<SupportPreview support={support} />
<div className="block sm:flex sm:justify-center sm:items-start">
<TopLanguagesPreview show={data.topLanguages} github={social.github} options={data.topLanguagesOptions} />
<GitHubStatsPreview show={data.githubStats} github={social.github} options={data.githubStatsOptions} />
<StreakStatsPreview show={data.streakStats} github={social.github} options={data.streakStatsOptions} />
</div>
</div>
);
};
export default MarkdownPreview;
MarkdownPreview.defaultProps = {
prefix: DEFAULT_PREFIX,
data: DEFAULT_DATA,
social: DEFAULT_SOCIAL,
support: DEFAULT_SUPPORT,
skills: [],
};
MarkdownPreview.propTypes = {
prefix: PropTypes.object,
data: PropTypes.object,
social: PropTypes.object,
skills: PropTypes.object,
support: PropTypes.object,
};
@@ -0,0 +1,319 @@
'use client';
import { Upload } from 'lucide-react';
import { useState, useEffect } from 'react';
import { UseFormRegister, FieldErrors, UseFormWatch } from 'react-hook-form';
import { FormInput } from '@/components/forms/form-input';
import { FormTextarea } from '@/components/forms/form-textarea';
import { FormCheckbox } from '@/components/forms/form-checkbox';
import { GitHubUsernameInput } from '@/components/forms/github-username-input';
import { CollapsibleSection } from '@/components/ui/collapsible-section';
import type { ProfileFormData, LinksFormData, SocialFormData } from '@/lib/validations';
interface BasicInfoSectionProps {
register: UseFormRegister<ProfileFormData>;
errors: FieldErrors<ProfileFormData>;
socialRegister: UseFormRegister<SocialFormData>;
watchSocial: UseFormWatch<SocialFormData>;
onGitHubAutoFill?: (data: {
profile: Partial<ProfileFormData>;
links: Partial<LinksFormData>;
social: Partial<SocialFormData>;
skills: string[];
}) => void;
onImportJSON?: (event: React.ChangeEvent<HTMLInputElement>) => void;
onClearAll?: () => void;
hasClearableData?: boolean;
}
export function BasicInfoSection({
register,
errors,
socialRegister,
watchSocial,
onGitHubAutoFill,
onImportJSON,
onClearAll,
hasClearableData = true,
}: BasicInfoSectionProps) {
const githubUsername = watchSocial('github') || '';
const [isMobile, setIsMobile] = useState(false);
useEffect(() => {
const checkMobile = () => {
setIsMobile(window.innerWidth < 768);
};
checkMobile();
window.addEventListener('resize', checkMobile);
return () => window.removeEventListener('resize', checkMobile);
}, []);
return (
<div className="space-y-6">
<div className="border-border border-b pb-4">
<div className="flex items-center justify-between">
<div>
<h2 className="text-2xl font-bold">Basic Information</h2>
<p className="text-muted-foreground mt-1 text-sm">
Tell us about yourself and what you do
</p>
</div>
{/* Action Buttons */}
<div className="flex items-center gap-2">
{/* Clear All Button */}
{onClearAll && (
<button
onClick={onClearAll}
disabled={!hasClearableData}
className={`flex items-center justify-center rounded-lg border p-3 transition-colors ${
hasClearableData
? 'bg-muted/50 text-muted-foreground hover:bg-muted hover:text-foreground border-border'
: 'bg-muted text-muted-foreground border-border cursor-not-allowed opacity-50'
}`}
title={hasClearableData ? 'Clear all data' : 'No data to clear'}
aria-label={hasClearableData ? 'Clear all data' : 'No data to clear'}
>
<svg className="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"
/>
</svg>
<span className="ml-2 text-sm">Clear All</span>
</button>
)}
{/* Import JSON Button */}
{onImportJSON && (
<label className="bg-secondary text-secondary-foreground hover:bg-secondary/90 flex cursor-pointer items-center justify-center rounded-lg p-3 transition-colors">
<Upload className="h-5 w-5" />
<input
type="file"
accept=".json"
onChange={onImportJSON}
className="hidden"
title="Import profile data from JSON"
aria-label="Import profile data from JSON"
/>
</label>
)}
</div>
</div>
</div>
{/* Quick Start: GitHub Auto-fill */}
{onGitHubAutoFill && (
<div className="border-primary/30 bg-primary/5 rounded-lg border-2 p-4">
<div className="mb-3 flex items-start gap-2">
<span className="text-2xl">🚀</span>
<div className="flex-1">
<h3 className="text-sm font-semibold">Quick Start with GitHub</h3>
<p className="text-muted-foreground mt-1 text-xs">
Enter your GitHub username to automatically populate your profile with smart
defaults
</p>
</div>
</div>
<GitHubUsernameInput
{...socialRegister('github')}
value={githubUsername}
onDataFetched={onGitHubAutoFill}
/>
</div>
)}
{/* Basic Fields - Always visible */}
<div className="grid gap-6 md:grid-cols-2">
<FormInput
{...register('title')}
id="title"
label="Your Name"
placeholder="John Doe"
error={errors.title?.message}
required
/>
<FormInput
{...register('subtitle')}
id="subtitle"
label="Subtitle"
placeholder="A passionate developer"
error={errors.subtitle?.message}
/>
</div>
{/* Additional Fields - Collapsible on mobile */}
{isMobile ? (
<div className="space-y-3">
<CollapsibleSection title="Current Work" icon="🔭" description="What you're working on">
<FormTextarea
{...register('currentWork')}
id="currentWork"
label="I'm currently working on"
placeholder="a MERN Stack project"
rows={2}
error={errors.currentWork?.message}
/>
</CollapsibleSection>
<CollapsibleSection title="Learning" icon="🌱" description="What you're learning">
<FormTextarea
{...register('currentLearn')}
id="currentLearn"
label="I'm currently learning"
placeholder="GraphQL and TypeScript"
rows={2}
error={errors.currentLearn?.message}
/>
</CollapsibleSection>
<CollapsibleSection
title="Collaboration"
icon="👯"
description="What you want to collaborate on"
>
<FormTextarea
{...register('collaborateOn')}
id="collaborateOn"
label="I'm looking to collaborate on"
placeholder="open source projects"
rows={2}
error={errors.collaborateOn?.message}
/>
</CollapsibleSection>
<CollapsibleSection title="Help Needed" icon="🤝" description="What you need help with">
<FormTextarea
{...register('helpWith')}
id="helpWith"
label="I'm looking for help with"
placeholder="learning system design"
rows={2}
error={errors.helpWith?.message}
/>
</CollapsibleSection>
<CollapsibleSection title="Ask Me About" icon="💬" description="Your areas of expertise">
<FormTextarea
{...register('ama')}
id="ama"
label="Ask me about"
placeholder="React, Node.js, and web development"
rows={2}
error={errors.ama?.message}
/>
</CollapsibleSection>
<CollapsibleSection title="Contact" icon="📫" description="How to reach you">
<FormInput
{...register('contact')}
id="contact"
label="How to reach me"
type="email"
placeholder="your.email@example.com"
error={errors.contact?.message}
/>
</CollapsibleSection>
<CollapsibleSection
title="Fun Fact"
icon="⚡"
description="Something interesting about you"
>
<FormTextarea
{...register('funFact')}
id="funFact"
label="Fun fact"
placeholder="I think I am funny"
rows={2}
error={errors.funFact?.message}
/>
</CollapsibleSection>
</div>
) : (
<>
<FormTextarea
{...register('currentWork')}
id="currentWork"
label="🔭 I'm currently working on"
placeholder="a MERN Stack project"
rows={2}
error={errors.currentWork?.message}
/>
<FormTextarea
{...register('currentLearn')}
id="currentLearn"
label="🌱 I'm currently learning"
placeholder="GraphQL and TypeScript"
rows={2}
error={errors.currentLearn?.message}
/>
<FormTextarea
{...register('collaborateOn')}
id="collaborateOn"
label="👯 I'm looking to collaborate on"
placeholder="open source projects"
rows={2}
error={errors.collaborateOn?.message}
/>
<FormTextarea
{...register('helpWith')}
id="helpWith"
label="🤝 I'm looking for help with"
placeholder="learning system design"
rows={2}
error={errors.helpWith?.message}
/>
<FormTextarea
{...register('ama')}
id="ama"
label="💬 Ask me about"
placeholder="React, Node.js, and web development"
rows={2}
error={errors.ama?.message}
/>
<FormInput
{...register('contact')}
id="contact"
label="📫 How to reach me"
type="email"
placeholder="your.email@example.com"
error={errors.contact?.message}
/>
<FormTextarea
{...register('funFact')}
id="funFact"
label="⚡ Fun fact"
placeholder="I think I am funny"
rows={2}
error={errors.funFact?.message}
/>
</>
)}
{/* Profile Badge Option */}
<div className="border-border mt-6 border-t pt-6">
<div className="bg-accent/50 rounded-lg p-4">
<h4 className="mb-2 flex items-center gap-2 text-sm font-semibold">
<span>📊</span>
<span>Profile Enhancement</span>
</h4>
<FormCheckbox
{...register('visitorsBadge')}
id="visitorsBadge"
label="Show profile visitors counter badge"
/>
</div>
</div>
</div>
);
}
+53
View File
@@ -0,0 +1,53 @@
'use client';
import { UseFormRegister, FieldErrors } from 'react-hook-form';
import { FormInput } from '@/components/forms/form-input';
import type { LinksFormData } from '@/lib/validations';
interface LinksSectionProps {
register: UseFormRegister<LinksFormData>;
errors: FieldErrors<LinksFormData>;
}
export function LinksSection({ register, errors }: LinksSectionProps) {
return (
<div className="space-y-6">
<div className="border-b border-border pb-4">
<h2 className="text-2xl font-bold">Links</h2>
<p className="text-muted-foreground mt-1 text-sm">
Add links to your portfolio, blog, and resume
</p>
</div>
<FormInput
{...register('portfolio')}
id="portfolio"
label="👨‍💻 Portfolio"
type="url"
placeholder="https://yourportfolio.com"
error={errors.portfolio?.message}
helperText="Your personal website or portfolio"
/>
<FormInput
{...register('blog')}
id="blog"
label="📝 Blog"
type="url"
placeholder="https://yourblog.com"
error={errors.blog?.message}
helperText="Where you write articles"
/>
<FormInput
{...register('resume')}
id="resume"
label="📄 Resume/CV"
type="url"
placeholder="https://drive.google.com/your-resume"
error={errors.resume?.message}
helperText="Link to your resume or CV"
/>
</div>
);
}
+264
View File
@@ -0,0 +1,264 @@
'use client';
import { useState, useMemo, useEffect } from 'react';
import { Info } from 'lucide-react';
import { UseFormRegister } from 'react-hook-form';
import { FormCheckbox } from '@/components/forms/form-checkbox';
import { FormInput } from '@/components/forms/form-input';
import { Select } from '@/components/ui/select';
import { CollapsibleSection } from '@/components/ui/collapsible-section';
import { categorizedSkills, categories } from '@/constants/skills';
import { getSkillIconUrl } from '@/lib/markdown-generator';
import type { ProfileFormData } from '@/lib/validations';
interface SkillsSectionProps {
selectedSkills: Record<string, boolean>;
onSkillChange: (skill: string, checked: boolean) => void;
registerProfile: UseFormRegister<ProfileFormData>;
}
export function SkillsSection({
selectedSkills,
onSkillChange,
registerProfile,
}: SkillsSectionProps) {
const [searchQuery, setSearchQuery] = useState('');
const [selectedCategory, setSelectedCategory] = useState<string>('all');
const [isMobile, setIsMobile] = useState(false);
// Check if we're on mobile for responsive behavior
useEffect(() => {
const checkMobile = () => {
setIsMobile(window.innerWidth < 768);
};
checkMobile();
window.addEventListener('resize', checkMobile);
return () => window.removeEventListener('resize', checkMobile);
}, []);
const selectedCount = useMemo(() => {
return Object.values(selectedSkills).filter(Boolean).length;
}, [selectedSkills]);
const filteredCategories = useMemo(() => {
if (selectedCategory !== 'all') {
return [selectedCategory];
}
return categories;
}, [selectedCategory]);
const filterSkills = (skills: string[]) => {
if (!searchQuery) return skills;
return skills.filter((skill) => skill.toLowerCase().includes(searchQuery.toLowerCase()));
};
// Create options for the select component
const categoryOptions = [
{ value: 'all', label: 'All Categories' },
...categories.map((category) => ({
value: category,
label: categorizedSkills[category].title,
})),
];
return (
<div className="space-y-6">
<div className="border-border border-b pb-4">
<div className="flex items-center justify-between">
<div>
<h2 className="text-2xl font-bold">Skills & Technologies</h2>
<p className="text-muted-foreground mt-1 text-sm">
Select the skills you want to showcase ({selectedCount} selected)
</p>
</div>
</div>
</div>
{/* Search and Filter - Stack on mobile */}
<div className="flex flex-col gap-4 sm:grid sm:grid-cols-2">
<FormInput
id="skill-search"
placeholder="Search skills..."
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
/>
<Select
value={selectedCategory}
onChange={setSelectedCategory}
options={categoryOptions}
placeholder="Select category"
/>
</div>
{/* Skills Grid - Responsive layout */}
<div className="space-y-6">
{filteredCategories.map((category) => {
const { title, skills } = categorizedSkills[category];
const filtered = filterSkills(skills);
if (filtered.length === 0) return null;
const skillsGrid = (
<div className="grid grid-cols-2 gap-3 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-5">
{filtered.map((skill) => {
const iconUrl = getSkillIconUrl(skill);
const isSelected = selectedSkills[skill] || false;
return (
<button
key={skill}
type="button"
onClick={() => onSkillChange(skill, !isSelected)}
className={`relative flex flex-col items-center gap-2 rounded-lg border-2 p-2 transition-all hover:scale-105 sm:p-3 ${
isSelected
? 'border-primary bg-primary/10'
: 'border-border hover:border-primary/50'
}`}
aria-pressed={isSelected}
>
<img
src={iconUrl}
alt={skill}
className="h-8 w-8 object-contain sm:h-10 sm:w-10"
loading="eager"
/>
<span className="text-center text-xs leading-tight capitalize">
{skill.replace(/_/g, ' ')}
</span>
{isSelected && (
<div className="bg-primary absolute top-1 right-1 h-2 w-2 rounded-full" />
)}
</button>
);
})}
</div>
);
// On mobile, use collapsible sections for better organization
if (isMobile && selectedCategory === 'all') {
return (
<CollapsibleSection
key={category}
title={title}
description={`${filtered.length} skills available`}
icon="🛠️"
defaultOpen={filtered.some((skill) => selectedSkills[skill])}
>
{skillsGrid}
</CollapsibleSection>
);
}
// Desktop layout or when a specific category is selected
return (
<div key={category} className="space-y-4">
<h3 className="text-lg font-semibold">{title}</h3>
{skillsGrid}
</div>
);
})}
</div>
{searchQuery &&
filteredCategories.every(
(cat) => filterSkills(categorizedSkills[cat].skills).length === 0
) && (
<div className="text-muted-foreground py-8 text-center">
<p>No skills found matching "{searchQuery}"</p>
</div>
)}
{/* GitHub Stats & Badges - Mobile-friendly layout */}
<div className="border-border mt-8 border-t pt-6">
<div
className={`space-y-4 rounded-lg p-4 transition-all sm:p-6 ${selectedCount > 0 ? 'bg-accent/50' : 'bg-muted/30'}`}
>
<div>
<h4 className="mb-1 flex items-center gap-2 text-base font-semibold sm:text-lg">
<span>📈</span>
<span>GitHub Profile Enhancements</span>
</h4>
<p className="text-muted-foreground text-sm">
Add visual statistics and achievements to your profile
</p>
{selectedCount === 0 && (
<p className="text-muted-foreground mt-2 flex items-center gap-1 text-xs">
<Info className="h-3 w-3" />
Select at least one skill above to enable these enhancements
</p>
)}
</div>
{/* Mobile: Use collapsible section, Desktop: Show grid */}
{isMobile ? (
<CollapsibleSection
title="Enhancement Options"
description={`${selectedCount > 0 ? 'Available' : 'Disabled'} - Select skills first`}
icon="⚙️"
defaultOpen={selectedCount > 0}
>
<div
className={`space-y-3 ${selectedCount === 0 ? 'pointer-events-none opacity-50' : ''}`}
>
<FormCheckbox
{...registerProfile('githubStats')}
id="githubStats"
label="GitHub Stats Card"
disabled={selectedCount === 0}
/>
<FormCheckbox
{...registerProfile('topLanguages')}
id="topLanguages"
label="Top Languages Card"
disabled={selectedCount === 0}
/>
<FormCheckbox
{...registerProfile('streakStats')}
id="streakStats"
label="GitHub Streak Stats"
disabled={selectedCount === 0}
/>
<FormCheckbox
{...registerProfile('githubProfileTrophy')}
id="githubProfileTrophy"
label="GitHub Profile Trophy"
disabled={selectedCount === 0}
/>
</div>
</CollapsibleSection>
) : (
<div
className={`grid gap-3 sm:grid-cols-2 ${selectedCount === 0 ? 'pointer-events-none opacity-50' : ''}`}
>
<FormCheckbox
{...registerProfile('githubStats')}
id="githubStats"
label="GitHub Stats Card"
disabled={selectedCount === 0}
/>
<FormCheckbox
{...registerProfile('topLanguages')}
id="topLanguages"
label="Top Languages Card"
disabled={selectedCount === 0}
/>
<FormCheckbox
{...registerProfile('streakStats')}
id="streakStats"
label="GitHub Streak Stats"
disabled={selectedCount === 0}
/>
<FormCheckbox
{...registerProfile('githubProfileTrophy')}
id="githubProfileTrophy"
label="GitHub Profile Trophy"
disabled={selectedCount === 0}
/>
</div>
)}
</div>
</div>
</div>
);
}
+134
View File
@@ -0,0 +1,134 @@
'use client';
import { Info } from 'lucide-react';
import { UseFormRegister, FieldErrors, UseFormWatch } from 'react-hook-form';
import { FormInput } from '@/components/forms/form-input';
import { FormCheckbox } from '@/components/forms/form-checkbox';
import type { SocialFormData } from '@/lib/validations';
interface SocialSectionProps {
register: UseFormRegister<SocialFormData>;
errors: FieldErrors<SocialFormData>;
watch: UseFormWatch<SocialFormData>;
}
export function SocialSection({ register, errors, watch }: SocialSectionProps) {
const socialPlatforms = [
{ key: 'github', label: 'GitHub', icon: '🐙', placeholder: 'username' },
{ key: 'linkedin', label: 'LinkedIn', icon: '💼', placeholder: 'username' },
{ key: 'twitter', label: 'Twitter', icon: '🐦', placeholder: 'username' },
{ key: 'dev', label: 'Dev.to', icon: '📝', placeholder: 'username' },
{ key: 'stackoverflow', label: 'Stack Overflow', icon: '📚', placeholder: 'userid/username' },
{ key: 'medium', label: 'Medium', icon: '✍️', placeholder: '@username' },
{ key: 'youtube', label: 'YouTube', icon: '📺', placeholder: 'channel-id' },
{ key: 'instagram', label: 'Instagram', icon: '📷', placeholder: 'username' },
{ key: 'fb', label: 'Facebook', icon: '👤', placeholder: 'username' },
{ key: 'codepen', label: 'CodePen', icon: '🖊️', placeholder: 'username' },
{ key: 'codesandbox', label: 'CodeSandbox', icon: '📦', placeholder: 'username' },
{ key: 'kaggle', label: 'Kaggle', icon: '🔬', placeholder: 'username' },
{ key: 'leetcode', label: 'LeetCode', icon: '💻', placeholder: 'username' },
{ key: 'hackerrank', label: 'HackerRank', icon: '🏆', placeholder: 'username' },
{ key: 'codeforces', label: 'Codeforces', icon: '⚡', placeholder: 'username' },
{ key: 'codechef', label: 'CodeChef', icon: '👨‍🍳', placeholder: 'username' },
{ key: 'topcoder', label: 'TopCoder', icon: '🥇', placeholder: 'username' },
{ key: 'hackerearth', label: 'HackerEarth', icon: '🌍', placeholder: '@username' },
{ key: 'geeks_for_geeks', label: 'GeeksforGeeks', icon: '🤓', placeholder: 'username' },
{ key: 'dribbble', label: 'Dribbble', icon: '🎨', placeholder: 'username' },
{ key: 'behance', label: 'Behance', icon: '🎭', placeholder: 'username' },
{ key: 'discord', label: 'Discord', icon: '💬', placeholder: 'invite-code' },
{ key: 'rssurl', label: 'RSS Feed', icon: '📡', placeholder: 'https://...' },
];
return (
<div className="space-y-6">
<div className="border-border border-b pb-4">
<h2 className="text-2xl font-bold">Social Profiles</h2>
<p className="text-muted-foreground mt-1 text-sm">
Connect your social media and coding platforms
</p>
</div>
{/* Instructions Banner */}
<div className="border-primary/30 bg-primary/5 rounded-lg border-2 p-4">
<div className="flex gap-3">
<div className="flex-shrink-0">
<svg
className="text-primary h-5 w-5"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"
/>
</svg>
</div>
<div className="flex-1">
<h4 className="mb-1 text-sm font-semibold">Enter usernames only, not full URLs</h4>
<p className="text-muted-foreground mb-2 text-sm">
Just provide your username or handle for each platform. We'll automatically generate
the correct URLs.
</p>
<div className="text-muted-foreground space-y-1 text-xs">
<div className="flex items-center gap-2">
<span className="text-green-600 dark:text-green-400">✓</span>
<span>
<strong>Correct:</strong> johndoe
</span>
</div>
<div className="flex items-center gap-2">
<span className="text-red-600 dark:text-red-400">✗</span>
<span>
<strong>Incorrect:</strong> https://twitter.com/johndoe
</span>
</div>
</div>
</div>
</div>
</div>
<div className="grid gap-4 md:grid-cols-2">
{socialPlatforms.map(({ key, label, icon, placeholder }) => (
<FormInput
key={key}
{...register(key as keyof SocialFormData)}
id={key}
label={`${icon} ${label}`}
placeholder={placeholder}
error={errors[key as keyof SocialFormData]?.message}
/>
))}
</div>
{/* Twitter Badge Option - Show always with disabled state and hint */}
<div className="border-border mt-6 border-t pt-6">
<div
className={`rounded-lg p-4 transition-all ${watch('twitter') ? 'bg-accent/50' : 'bg-muted/30'}`}
>
<h4 className="mb-2 flex items-center gap-2 text-sm font-semibold">
<span>🐦</span>
<span>Twitter Enhancement</span>
</h4>
{!watch('twitter') && (
<p className="text-muted-foreground mb-3 flex items-center gap-1 text-xs">
<Info className="h-3 w-3" />
Enter your Twitter username above to enable this feature
</p>
)}
<div className={!watch('twitter') ? 'pointer-events-none opacity-50' : ''}>
<FormCheckbox
{...register('twitterBadge')}
id="twitterBadge"
label="Show Twitter follow badge on profile"
disabled={!watch('twitter')}
/>
</div>
</div>
</div>
</div>
);
}
-88
View File
@@ -1,88 +0,0 @@
/**
* SEO component that queries for data with
* Gatsby's useStaticQuery React hook
*
* See: https://www.gatsbyjs.org/docs/use-static-query/
*/
import React from 'react';
import PropTypes from 'prop-types';
import { Helmet } from 'react-helmet';
import { useStaticQuery, graphql } from 'gatsby';
function SEO({ description, lang, meta, title }) {
const { site } = useStaticQuery(
graphql`
query {
site {
siteMetadata {
title
description
author
}
}
}
`,
);
const metaDescription = description || site.siteMetadata.description;
return (
<Helmet
htmlAttributes={{
lang,
}}
title={title}
titleTemplate={`%s | ${site.siteMetadata.title}`}
meta={[
{
name: `description`,
content: metaDescription,
},
{
property: `og:title`,
content: title,
},
{
property: `og:description`,
content: metaDescription,
},
{
property: `og:type`,
content: `website`,
},
{
name: `twitter:card`,
content: `summary`,
},
{
name: `twitter:creator`,
content: site.siteMetadata.author,
},
{
name: `twitter:title`,
content: title,
},
{
name: `twitter:description`,
content: metaDescription,
},
].concat(meta)}
/>
);
}
SEO.defaultProps = {
lang: `en`,
meta: [],
description: ``,
};
SEO.propTypes = {
description: PropTypes.string,
lang: PropTypes.string,
meta: PropTypes.arrayOf(PropTypes.object),
title: PropTypes.string.isRequired,
};
export default SEO;
-99
View File
@@ -1,99 +0,0 @@
import React, { useState } from 'react';
import PropTypes from 'prop-types';
import { SearchIcon, XIcon } from '@primer/octicons-react';
import { icons, categorizedSkills } from '../constants/skills';
const Skills = (props) => {
const { skills, handleSkillsChange } = props;
const [search, setSearch] = useState('');
const [debounce, setDebounce] = useState(undefined);
const inputRef = React.createRef();
const createSkill = (skill) => (
<div className="w-1/3 sm:w-1/4 my-6" key={skill}>
<label htmlFor={skill} className="checkbox-label flex items-center justify-start">
<input
id={skill}
type="checkbox"
className="checkbox-label__input"
checked={skills[skill]}
onChange={() => handleSkillsChange(skill)}
/>
<span className="checkbox-label__control" />
<img className="ml-4 w-8 h-8 sm:w-10 sm:h-10" src={icons[skill]} alt={skill} />
<span className="tooltiptext">{skill}</span>
</label>
</div>
);
const onSearchChange = (value) => {
const callback = () => {
setSearch(value);
};
clearTimeout(debounce);
setDebounce(setTimeout(callback, 50));
};
return (
<div className="px-2 sm:px-6 mb-10 ">
<div className="text-xl sm:text-2xl font-bold font-title mt-2 mb-4 flex justify-between">
Skills
<div className="relative flex">
<input
type="text"
onChange={(e) => onSearchChange(e.target.value)}
className="leading:none text-xs my-0 py-1 px-2 pr-8 sm:text-xl border-2 border-gray-900 focus:border-blue-700 placeholder-gray-700"
placeholder="Search Skills"
ref={inputRef}
/>
<span className="absolute" style={{ right: '10px' }}>
{search !== '' ? (
<button
type="button"
className="focus:outline-none"
onClick={() => {
setSearch('');
inputRef.current.value = '';
}}
>
<XIcon size={16} className="mb-1 transform scale-100 md:scale-125" />
</button>
) : (
<SearchIcon size={16} className="mb-1 transform scale-100 md:scale-125" />
)}
</span>
</div>
</div>
{Object.keys(categorizedSkills)
.filter((key) => {
const filtered = categorizedSkills[key].skills.filter((skill) => skill.includes(search.toLowerCase()));
return filtered.length !== 0;
})
.map((key) => (
<div key={key} className="divide-y divide-gray-500">
<div className="text-sm sm:text-xl text-gray-900 text-left py-1">{categorizedSkills[key].title}</div>
<div className="flex justify-start items-center flex-wrap w-full mb-6 pl-4 sm:pl-10">
{categorizedSkills[key].skills
.filter((skill) => skill.includes(search.toLowerCase()))
.map((skill) => createSkill(skill))}
</div>
</div>
))}
<span className="flex justify-center text-gray-900">
{Object.keys(categorizedSkills).filter((key) => {
const filtered = categorizedSkills[key].skills.filter((skill) => skill.includes(search.toLowerCase()));
return filtered.length !== 0;
}).length === 0
? 'No Results Found'
: ''}
</span>
</div>
);
};
export default Skills;
Skills.propTypes = {
skills: PropTypes.array.isRequired,
handleSkillsChange: PropTypes.func.isRequired,
};
-355
View File
@@ -1,355 +0,0 @@
import React from 'react';
import PropTypes from 'prop-types';
const Social = (props) => {
const { social, handleSocialChange } = props;
return (
<div className="px-2 sm:px-6 mb-4">
<div className="text-xl sm:text-2xl font-bold font-title mt-2 mb-2">Social</div>
<div className="flex flex-wrap justify-center items-center">
<div className="w-1/2 flex justify-center items-center text-xxs sm:text-lg py-4 pr-2 sm:pr-0">
<img
src="https://cdn.jsdelivr.net/npm/simple-icons@3.0.1/icons/github.svg"
className="w-6 h-6 sm:w-8 sm:h-8 mr-1 sm:mr-4"
alt="github"
/>
<input
id="github"
placeholder="github username"
className="outline-none placeholder-gray-700 w-32 sm:w-1/2 border-t-0 border-l-0 border-r-0 border solid border-gray-900 py-1 px-1 sm:px-2 focus:border-blue-700"
value={social.github}
onChange={(event) => handleSocialChange('github', event)}
/>
</div>
<div className="w-1/2 flex justify-center items-center text-xxs sm:text-lg py-4 pr-2 sm:pr-0">
<img
src="https://cdn.jsdelivr.net/npm/simple-icons@v3/icons/twitter.svg"
className="w-6 h-6 sm:w-8 sm:h-8 mr-1 sm:mr-4"
alt="twitter"
/>
<input
id="twitter"
placeholder="twitter username"
className="outline-none placeholder-gray-700 w-32 sm:w-1/2 border-t-0 border-l-0 border-r-0 border solid border-gray-900 py-1 px-2 focus:border-blue-700"
value={social.twitter}
onChange={(event) => handleSocialChange('twitter', event)}
/>
</div>
<div className="w-1/2 flex justify-center items-center text-xxs sm:text-lg py-4 pr-2 sm:pr-0">
<img
src="https://cdn.jsdelivr.net/npm/simple-icons@3.0.1/icons/dev-dot-to.svg"
className="w-6 h-6 sm:w-8 sm:h-8 mr-1 sm:mr-4"
alt="dev.to"
/>
<input
id="dev"
placeholder="dev.to username"
className="outline-none placeholder-gray-700 w-32 sm:w-1/2 border-t-0 border-l-0 border-r-0 border solid border-gray-900 py-1 px-2 focus:border-blue-700"
value={social.dev}
onChange={(event) => handleSocialChange('dev', event)}
/>
</div>
<div className="w-1/2 flex justify-center items-center text-xxs sm:text-lg py-4 pr-2 sm:pr-0">
<img
src="https://cdn.jsdelivr.net/npm/simple-icons@3.0.1/icons/codepen.svg"
className="w-6 h-6 sm:w-8 sm:h-8 mr-1 sm:mr-4"
alt="codepen"
/>
<input
id="codepen"
placeholder="codepen username"
className="outline-none placeholder-gray-700 w-32 sm:w-1/2 border-t-0 border-l-0 border-r-0 border solid border-gray-900 py-1 px-2 focus:border-blue-700"
value={social.codepen}
onChange={(event) => handleSocialChange('codepen', event)}
/>
</div>
<div className="w-1/2 flex justify-center items-center text-xxs sm:text-lg py-4 pr-2 sm:pr-0">
<img
src="https://cdn.jsdelivr.net/npm/simple-icons@3.0.1/icons/codesandbox.svg"
className="w-6 h-6 sm:w-8 sm:h-8 mr-1 sm:mr-4"
alt="codesandbox"
/>
<input
id="codesandbox"
placeholder="codesandbox username"
className="outline-none placeholder-gray-700 w-32 sm:w-1/2 border-t-0 border-l-0 border-r-0 border solid border-gray-900 py-1 px-2 focus:border-blue-700"
value={social.codesandbox}
onChange={(event) => handleSocialChange('codesandbox', event)}
/>
</div>
<div className="w-1/2 flex justify-center items-center text-xxs sm:text-lg py-4 pr-2 sm:pr-0">
<img
src="https://cdn.jsdelivr.net/npm/simple-icons@3.0.1/icons/stackoverflow.svg"
className="w-6 h-6 sm:w-8 sm:h-8 mr-1 sm:mr-4"
alt="stackoverflow"
/>
<input
id="stackoverflow"
placeholder="stackoverflow user ID"
className="outline-none placeholder-gray-700 w-32 sm:w-1/2 border-t-0 border-l-0 border-r-0 border solid border-gray-900 py-1 px-2 focus:border-blue-700"
value={social.stackoverflow}
onChange={(event) => handleSocialChange('stackoverflow', event)}
/>
</div>
<div className="w-1/2 flex justify-center items-center text-xxs sm:text-lg py-4 pr-2 sm:pr-0">
<img
src="https://cdn.jsdelivr.net/npm/simple-icons@3.0.1/icons/linkedin.svg"
className="w-6 h-6 sm:w-8 sm:h-8 mr-1 sm:mr-4"
alt="linkedin"
/>
<input
id="linkedin"
placeholder="linkedin username"
className="outline-none placeholder-gray-700 w-32 sm:w-1/2 border-t-0 border-l-0 border-r-0 border solid border-gray-900 py-1 px-2 focus:border-blue-700"
value={social.linkedin}
onChange={(event) => handleSocialChange('linkedin', event)}
/>
</div>
<div className="w-1/2 flex justify-center items-center text-xxs sm:text-lg py-4 pr-2 sm:pr-0">
<img
src="https://cdn.jsdelivr.net/npm/simple-icons@3.0.1/icons/kaggle.svg"
className="w-6 h-6 sm:w-8 sm:h-8 mr-1 sm:mr-4"
alt="kaggle"
/>
<input
id="kaggle"
placeholder="kaggle username"
className="outline-none placeholder-gray-700 w-32 sm:w-1/2 border-t-0 border-l-0 border-r-0 border solid border-gray-900 py-1 px-2 focus:border-blue-700"
value={social.kaggle}
onChange={(event) => handleSocialChange('kaggle', event)}
/>
</div>
<div className="w-1/2 flex justify-center items-center text-xxs sm:text-lg py-4 pr-2 sm:pr-0">
<img
src="https://cdn.jsdelivr.net/npm/simple-icons@3.0.1/icons/facebook.svg"
className="w-6 h-6 sm:w-8 sm:h-8 mr-1 sm:mr-4"
alt="facebook"
/>
<input
id="fb"
placeholder="facebook username"
className="outline-none placeholder-gray-700 w-32 sm:w-1/2 border-t-0 border-l-0 border-r-0 border solid border-gray-900 py-1 px-2 focus:border-blue-700"
value={social.fb}
onChange={(event) => handleSocialChange('fb', event)}
/>
</div>
<div className="w-1/2 flex justify-center items-center text-xxs sm:text-lg py-4 pr-2 sm:pr-0">
<img
src="https://cdn.jsdelivr.net/npm/simple-icons@3.0.1/icons/instagram.svg"
className="w-6 h-6 sm:w-8 sm:h-8 mr-1 sm:mr-4"
alt="instagram"
/>
<input
id="instagram"
placeholder="instagram username"
className="outline-none placeholder-gray-700 w-32 sm:w-1/2 border-t-0 border-l-0 border-r-0 border solid border-gray-900 py-1 px-2 focus:border-blue-700"
value={social.instagram}
onChange={(event) => handleSocialChange('instagram', event)}
/>
</div>
<div className="w-1/2 flex justify-center items-center text-xxs sm:text-lg py-4 pr-2 sm:pr-0">
<img
src="https://cdn.jsdelivr.net/npm/simple-icons@3.1.0/icons/dribbble.svg"
className="w-6 h-6 sm:w-8 sm:h-8 mr-1 sm:mr-4"
alt="dribbble"
/>
<input
id="dribbble"
placeholder="dribbble username"
className="outline-none placeholder-gray-700 w-32 sm:w-1/2 border-t-0 border-l-0 border-r-0 border solid border-gray-900 py-1 px-2 focus:border-blue-700"
value={social.dribbble}
onChange={(event) => handleSocialChange('dribbble', event)}
/>
</div>
<div className="w-1/2 flex justify-center items-center text-xxs sm:text-lg py-4 pr-2 sm:pr-0">
<img
src="https://cdn.jsdelivr.net/npm/simple-icons@3.1.0/icons/behance.svg"
className="w-6 h-6 sm:w-8 sm:h-8 mr-1 sm:mr-4"
alt="behance"
/>
<input
id="behance"
placeholder="behance username"
className="outline-none placeholder-gray-700 w-32 sm:w-1/2 border-t-0 border-l-0 border-r-0 border solid border-gray-900 py-1 px-2 focus:border-blue-700"
value={social.behance}
onChange={(event) => handleSocialChange('behance', event)}
/>
</div>
<div className="w-1/2 flex justify-center items-center text-xxs sm:text-lg py-4 pr-2 sm:pr-0">
<img
src="https://cdn.jsdelivr.net/npm/simple-icons@3.0.1/icons/hashnode.svg"
className="w-6 h-6 sm:w-8 sm:h-8 mr-1 sm:mr-4"
alt="hashnode"
/>
<input
id="hashnode"
placeholder="hashnode username (with @)"
className="outline-none placeholder-gray-700 w-32 sm:w-1/2 border-t-0 border-l-0 border-r-0 border solid border-gray-900 py-1 px-2 focus:border-blue-700"
value={social.hashnode}
onChange={(event) => handleSocialChange('hashnode', event)}
/>
</div>
<div className="w-1/2 flex justify-center items-center text-xxs sm:text-lg py-4 pr-2 sm:pr-0">
<img
src="https://cdn.jsdelivr.net/npm/simple-icons@3.1.0/icons/medium.svg"
className="w-6 h-6 sm:w-8 sm:h-8 mr-1 sm:mr-4"
alt="medium"
/>
<input
id="medium"
placeholder="medium username (with @)"
className="outline-none placeholder-gray-700 w-32 sm:w-1/2 border-t-0 border-l-0 border-r-0 border solid border-gray-900 py-1 px-2 focus:border-blue-700"
value={social.medium}
onChange={(event) => handleSocialChange('medium', event)}
/>
</div>
<div className="w-1/2 flex justify-center items-center text-xxs sm:text-lg py-4 pr-2 sm:pr-0">
<img
src="https://cdn.jsdelivr.net/npm/simple-icons@3.1.0/icons/youtube.svg"
className="w-6 h-6 sm:w-8 sm:h-8 mr-1 sm:mr-4"
alt="youtube"
/>
<input
id="youtube"
placeholder="youtube channel name"
className="outline-none placeholder-gray-700 w-32 sm:w-1/2 border-t-0 border-l-0 border-r-0 border solid border-gray-900 py-1 px-2 focus:border-blue-700"
value={social.youtube}
onChange={(event) => handleSocialChange('youtube', event)}
/>
</div>
<div className="w-1/2 flex justify-center items-center text-xxs sm:text-lg py-4 pr-2 sm:pr-0">
<img
src="https://cdn.jsdelivr.net/npm/simple-icons@3.1.0/icons/codechef.svg"
className="w-6 h-6 sm:w-8 sm:h-8 mr-1 sm:mr-4"
alt="codechef"
/>
<input
id="codechef"
placeholder="codechef username"
className="outline-none placeholder-gray-700 w-32 sm:w-1/2 border-t-0 border-l-0 border-r-0 border solid border-gray-900 py-1 px-2 focus:border-blue-700"
value={social.codechef}
onChange={(event) => handleSocialChange('codechef', event)}
/>
</div>
<div className="w-1/2 flex justify-center items-center text-xxs sm:text-lg py-4 pr-2 sm:pr-0">
<img
src="https://cdn.jsdelivr.net/npm/simple-icons@3.1.0/icons/hackerrank.svg"
className="w-6 h-6 sm:w-8 sm:h-8 mr-1 sm:mr-4"
alt="hackerrank"
/>
<input
id="hackerrank"
placeholder="hackerrank username"
className="outline-none placeholder-gray-700 w-32 sm:w-1/2 border-t-0 border-l-0 border-r-0 border solid border-gray-900 py-1 px-2 focus:border-blue-700"
value={social.hackerrank}
onChange={(event) => handleSocialChange('hackerrank', event)}
/>
</div>
<div className="w-1/2 flex justify-center items-center text-xxs sm:text-lg py-4 pr-2 sm:pr-0">
<img
src="https://cdn.jsdelivr.net/npm/simple-icons@3.1.0/icons/codeforces.svg"
className="w-6 h-6 sm:w-8 sm:h-8 mr-1 sm:mr-4"
alt="codeforces"
/>
<input
id="codeforces"
placeholder="codeforces username"
className="outline-none placeholder-gray-700 w-32 sm:w-1/2 border-t-0 border-l-0 border-r-0 border solid border-gray-900 py-1 px-2 focus:border-blue-700"
value={social.codeforces}
onChange={(event) => handleSocialChange('codeforces', event)}
/>
</div>
<div className="w-1/2 flex justify-center items-center text-xxs sm:text-lg py-4 pr-2 sm:pr-0">
<img
src="https://cdn.jsdelivr.net/npm/simple-icons@3.1.0/icons/leetcode.svg"
className="w-6 h-6 sm:w-8 sm:h-8 mr-1 sm:mr-4"
alt="leetcode"
/>
<input
id="leetcode"
placeholder="leetcode username"
className="outline-none placeholder-gray-700 w-32 sm:w-1/2 border-t-0 border-l-0 border-r-0 border solid border-gray-900 py-1 px-2 focus:border-blue-700"
value={social.leetcode}
onChange={(event) => handleSocialChange('leetcode', event)}
/>
</div>
<div className="w-1/2 flex justify-center items-center text-xxs sm:text-lg py-4 pr-2 sm:pr-0">
<img
src="https://cdn.jsdelivr.net/npm/simple-icons@3.1.0/icons/topcoder.svg"
className="w-6 h-6 sm:w-8 sm:h-8 mr-1 sm:mr-4"
alt="topcoder"
/>
<input
id="topcoder"
placeholder="topcoder username"
className="outline-none placeholder-gray-700 w-32 sm:w-1/2 border-t-0 border-l-0 border-r-0 border solid border-gray-900 py-1 px-2 focus:border-blue-700"
value={social.topcoder}
onChange={(event) => handleSocialChange('topcoder', event)}
/>
</div>
<div className="w-1/2 flex justify-center items-center text-xxs sm:text-lg py-4 pr-2 sm:pr-0">
<img
src="https://cdn.jsdelivr.net/npm/simple-icons@3.1.0/icons/hackerearth.svg"
className="w-6 h-6 sm:w-8 sm:h-8 mr-1 sm:mr-4"
alt="hackerearth"
/>
<input
id="hackerearth"
placeholder="hackerearth user (with @)"
className="outline-none placeholder-gray-700 w-32 sm:w-1/2 border-t-0 border-l-0 border-r-0 border solid border-gray-900 py-1 px-2 focus:border-blue-700"
value={social.hackerearth}
onChange={(event) => handleSocialChange('hackerearth', event)}
/>
</div>
<div className="w-1/2 flex justify-center items-center text-xxs sm:text-lg py-4 pr-2 sm:pr-0">
<img
src="https://cdn.jsdelivr.net/npm/simple-icons@3.1.0/icons/geeksforgeeks.svg"
className="w-6 h-6 sm:w-8 sm:h-8 mr-1 sm:mr-4"
alt="geeksforgeeks"
/>
<input
id="geeksforgeeks"
placeholder="GFG (<username>/profile)"
className="outline-none placeholder-gray-700 w-32 sm:w-1/2 border-t-0 border-l-0 border-r-0 border solid border-gray-900 py-1 px-2 focus:border-blue-700"
value={social.geeks_for_geeks}
onChange={(event) => handleSocialChange('geeks_for_geeks', event)}
/>
</div>
<div className="w-1/2 flex justify-center items-center text-xxs sm:text-lg py-4 pr-2 sm:pr-0">
<img
src="https://cdn.jsdelivr.net/npm/simple-icons@3.1.0/icons/discord.svg"
className="w-6 h-6 sm:w-8 sm:h-8 mr-1 sm:mr-4"
alt="discord"
/>
<input
id="discord"
placeholder="discord invite (only code)"
className="outline-none placeholder-gray-700 w-32 sm:w-1/2 border-t-0 border-l-0 border-r-0 border solid border-gray-900 py-1 px-2 focus:border-blue-700"
value={social.discord}
onChange={(event) => handleSocialChange('discord', event)}
/>
</div>
<div className="w-1/2 flex justify-center items-center text-xxs sm:text-lg py-4 pr-2 sm:pr-0">
<img
src="https://cdn.jsdelivr.net/npm/simple-icons@3.1.0/icons/rss.svg"
className="w-6 h-6 sm:w-8 sm:h-8 mr-1 sm:mr-4"
alt="rssfeed"
/>
<input
id="rssurl"
placeholder="RSS feed URL"
className="outline-none placeholder-gray-700 w-32 sm:w-1/2 border-t-0 border-l-0 border-r-0 border solid border-gray-900 py-1 px-2 focus:border-blue-700"
value={social.rssurl}
onChange={(event) => handleSocialChange('rssurl', event)}
/>
</div>
</div>
</div>
);
};
export default Social;
Social.propTypes = {
social: PropTypes.object.isRequired,
handleSocialChange: PropTypes.func.isRequired,
};
-23
View File
@@ -1,23 +0,0 @@
import React from 'react';
import PropTypes from 'prop-types';
const Subtitle = (props) => {
const { data, handleDataChange } = props;
return (
<div className="flex justify-center items-start flex-col w-full px-2 sm:px-6 mb-10">
<div className="text-xl sm:text-2xl font-bold font-title mt-2 mb-2">Subtitle</div>
<input
id="subtitle"
className="outline-none w-full text-xs sm:text-lg sm:w-1/2 border-t-0 border-l-0 border-r-0 border solid border-gray-900 py-1 px-2 focus:border-blue-700"
value={data.subtitle}
onChange={(event) => handleDataChange('subtitle', event)}
/>
</div>
);
};
export default Subtitle;
Subtitle.propTypes = {
data: PropTypes.object.isRequired,
handleDataChange: PropTypes.func.isRequired,
};
-47
View File
@@ -1,47 +0,0 @@
import React from 'react';
import PropTypes from 'prop-types';
const Support = (props) => {
const { support, handleSupportChange } = props;
return (
<div className="px-2 sm:px-6 mb-4">
<div className="text-xl sm:text-2xl font-bold font-title mt-2 mb-2">Support</div>
<div className="flex flex-wrap justify-start items-center">
<div className="w-1/2 flex justify-start items-center text-xxs sm:text-lg py-4 pr-2 sm:pr-0">
<img
src="https://cdn.buymeacoffee.com/buttons/v2/default-yellow.png"
className="w-36 h-8 sm:w-52 sm:h-12 mr-1 sm:mr-4"
alt="buymeacoffee"
/>
<input
id="buy-me-a-coffee"
placeholder="buymeacoffee username"
className="outline-none placeholder-gray-700 w-32 sm:w-1/2 border-t-0 border-l-0 border-r-0 border solid border-gray-900 py-1 px-1 focus:border-blue-700"
value={support.buyMeACoffee || ''}
onChange={(event) => handleSupportChange('buyMeACoffee', event)}
/>
</div>
<div className="w-1/2 flex justify-start items-center text-xxs sm:text-lg py-4 pr-2 sm:pr-0">
<img
src="https://cdn.ko-fi.com/cdn/kofi3.png?v=3"
className="w-36 h-8 sm:w-52 sm:h-12 mr-1 sm:mr-4"
alt="buymeakofi"
/>
<input
id="buy-me-a-kofi"
placeholder="Ko-fi username"
className="outline-none placeholder-gray-700 w-32 sm:w-1/2 border-t-0 border-l-0 border-r-0 border solid border-gray-900 py-1 px-1 sm:px-2 ml-2 sm:ml-0 focus:border-blue-700"
value={support.buyMeAKofi || ''}
onChange={(event) => handleSupportChange('buyMeAKofi', event)}
/>
</div>
</div>
</div>
);
};
export default Support;
Support.propTypes = {
support: PropTypes.object.isRequired,
handleSupportChange: PropTypes.func.isRequired,
};
-34
View File
@@ -1,34 +0,0 @@
import React from 'react';
import PropTypes from 'prop-types';
const Title = (props) => {
const { data, prefix, handlePrefixChange, handleDataChange } = props;
return (
<div className="flex justify-center items-start flex-col w-full px-2 sm:px-6 mb-10">
<div className="text-xl sm:text-2xl font-bold font-title mt-2 mb-2">Title</div>
<div className="flex justify-start items-center w-full text-regular text-xs sm:text-lg">
<input
id="title-prefix"
className="outline-none w-24 sm:w-40 mr-10 border-t-0 border-l-0 border-r-0 border solid border-gray-900 py-1 px-2 focus:border-blue-700 prefix"
value={prefix.title}
onChange={(event) => handlePrefixChange('title', event)}
/>
<input
id="title-name"
placeholder="name"
className="outline-none placeholder-gray-700 w-1/2 sm:w-1/3 border-t-0 border-l-0 border-r-0 border solid border-gray-900 py-1 px-2 focus:border-blue-700"
value={data.title}
onChange={(event) => handleDataChange('title', event)}
/>
</div>
</div>
);
};
export default Title;
Title.propTypes = {
prefix: PropTypes.object.isRequired,
data: PropTypes.object.isRequired,
handlePrefixChange: PropTypes.func.isRequired,
handleDataChange: PropTypes.func.isRequired,
};
+85
View File
@@ -0,0 +1,85 @@
'use client';
import { useState } from 'react';
import { Settings } from 'lucide-react';
import { Select } from '@/components/ui/select';
import { useThemeStore } from '@/lib/store';
export function AccessibilityMenu() {
const [isOpen, setIsOpen] = useState(false);
const { accessibility, setAccessibility } = useThemeStore();
// Font size options for the select component
const fontSizeOptions = [
{ value: 'small', label: 'Small' },
{ value: 'medium', label: 'Medium (Default)' },
{ value: 'large', label: 'Large' },
];
return (
<div className="relative">
<button
onClick={() => setIsOpen(!isOpen)}
className="border-border bg-card hover:bg-accent flex h-11 w-11 items-center justify-center rounded-lg border !p-3 transition-colors"
aria-label="Accessibility settings"
title="Accessibility settings"
aria-expanded={isOpen}
>
<Settings className="h-5 w-5" />
</button>
{isOpen && (
<>
{/* Backdrop */}
<div className="fixed inset-0 z-40" onClick={() => setIsOpen(false)} aria-hidden="true" />
{/* Menu */}
<div className="border-border bg-card absolute top-full right-0 z-50 mt-2 w-72 rounded-lg border p-4 shadow-lg">
<h3 className="mb-4 text-sm font-semibold">Accessibility Settings</h3>
<div className="space-y-4">
{/* High Contrast */}
<label className="flex cursor-pointer items-center justify-between">
<span className="text-sm">High Contrast Mode</span>
<input
type="checkbox"
checked={accessibility.highContrast}
onChange={(e) => setAccessibility({ highContrast: e.target.checked })}
className="border-border bg-input text-primary focus:ring-ring h-4 w-4 rounded focus:ring-2"
/>
</label>
{/* Font Size */}
<div className="space-y-2">
<Select
label="Font Size"
value={accessibility.fontSize}
onChange={(value) =>
setAccessibility({ fontSize: value as 'small' | 'medium' | 'large' })
}
options={fontSizeOptions}
id="fontSize"
/>
</div>
{/* Reduced Motion */}
<label className="flex cursor-pointer items-center justify-between">
<span className="text-sm">Reduce Motion</span>
<input
type="checkbox"
checked={accessibility.reducedMotion}
onChange={(e) => setAccessibility({ reducedMotion: e.target.checked })}
className="border-border bg-input text-primary focus:ring-ring h-4 w-4 rounded focus:ring-2"
/>
</label>
</div>
<p className="border-border text-muted-foreground mt-4 border-t pt-3 text-xs">
These settings are saved locally and persist across sessions.
</p>
</div>
</>
)}
</div>
);
}
+39
View File
@@ -0,0 +1,39 @@
'use client';
import { useEffect } from 'react';
export function BuyMeACoffeeWidget() {
useEffect(() => {
const script = document.createElement('script');
script.setAttribute('data-name', 'BMC-Widget');
script.src = 'https://cdnjs.buymeacoffee.com/1.0.0/widget.prod.min.js';
script.setAttribute('data-id', 'rahuldkjain');
script.setAttribute('data-description', 'Support rahuldkjain on Buy me a coffee!');
script.setAttribute('data-message', '');
script.setAttribute('data-color', '#ffdd00');
script.setAttribute('data-position', 'Right');
script.setAttribute('data-x_margin', '18');
script.setAttribute('data-y_margin', '18');
script.async = true;
script.onload = function () {
const event = new CustomEvent('DOMContentLoaded', {
bubbles: true,
cancelable: true,
});
window.dispatchEvent(event);
};
document.head.appendChild(script);
return () => {
document.head.removeChild(script);
const widget = document.getElementById('bmc-wbtn');
if (widget) {
document.body.removeChild(widget);
}
};
}, []);
return null;
}
+63
View File
@@ -0,0 +1,63 @@
'use client';
import { useState } from 'react';
import { motion, AnimatePresence } from 'framer-motion';
import { ChevronDown } from 'lucide-react';
interface CollapsibleSectionProps {
title: string;
description?: string;
icon?: string;
defaultOpen?: boolean;
children: React.ReactNode;
}
export function CollapsibleSection({
title,
description,
icon,
defaultOpen = false,
children,
}: CollapsibleSectionProps) {
const [isOpen, setIsOpen] = useState(defaultOpen);
return (
<div className="border-border bg-card rounded-lg border">
<button
onClick={() => setIsOpen(!isOpen)}
className="hover:bg-accent/50 flex w-full items-center justify-between p-4 text-left transition-colors"
aria-expanded={isOpen}
aria-label={`${isOpen ? 'Collapse' : 'Expand'} ${title} section`}
>
<div className="flex items-center gap-3">
{icon && <span className="text-xl">{icon}</span>}
<div>
<h3 className="text-sm font-semibold">{title}</h3>
{description && <p className="text-muted-foreground mt-0.5 text-xs">{description}</p>}
</div>
</div>
<motion.div
animate={{ rotate: isOpen ? 180 : 0 }}
transition={{ duration: 0.2 }}
aria-hidden="true"
>
<ChevronDown className="h-5 w-5" />
</motion.div>
</button>
<AnimatePresence initial={false}>
{isOpen && (
<motion.div
initial={{ height: 0, opacity: 0 }}
animate={{ height: 'auto', opacity: 1 }}
exit={{ height: 0, opacity: 0 }}
transition={{ duration: 0.2, ease: 'easeInOut' }}
style={{ overflow: 'hidden' }}
>
<div className="border-border border-t p-4">{children}</div>
</motion.div>
)}
</AnimatePresence>
</div>
);
}

Some files were not shown because too many files have changed in this diff Show More