Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| e5be501550 |
+33
-12
@@ -1,5 +1,7 @@
|
|||||||
{
|
{
|
||||||
"files": ["README.md"],
|
"files": [
|
||||||
|
"README.md"
|
||||||
|
],
|
||||||
"imageSize": 100,
|
"imageSize": 100,
|
||||||
"commit": false,
|
"commit": false,
|
||||||
"contributors": [
|
"contributors": [
|
||||||
@@ -8,63 +10,82 @@
|
|||||||
"name": "Sarbik Betal",
|
"name": "Sarbik Betal",
|
||||||
"avatar_url": "https://avatars2.githubusercontent.com/u/41508422?v=4",
|
"avatar_url": "https://avatars2.githubusercontent.com/u/41508422?v=4",
|
||||||
"profile": "https://github.com/sarbikbetal",
|
"profile": "https://github.com/sarbikbetal",
|
||||||
"contributions": ["code"]
|
"contributions": [
|
||||||
|
"code"
|
||||||
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"login": "Hardik0307",
|
"login": "Hardik0307",
|
||||||
"name": "Hardik Bagada",
|
"name": "Hardik Bagada",
|
||||||
"avatar_url": "https://avatars3.githubusercontent.com/u/41434099?v=4",
|
"avatar_url": "https://avatars3.githubusercontent.com/u/41434099?v=4",
|
||||||
"profile": "https://github.com/Hardik0307",
|
"profile": "https://github.com/Hardik0307",
|
||||||
"contributions": ["code"]
|
"contributions": [
|
||||||
|
"code"
|
||||||
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"login": "antonkomarev",
|
"login": "antonkomarev",
|
||||||
"name": "Anton Komarev",
|
"name": "Anton Komarev",
|
||||||
"avatar_url": "https://avatars0.githubusercontent.com/u/1849174?v=4",
|
"avatar_url": "https://avatars0.githubusercontent.com/u/1849174?v=4",
|
||||||
"profile": "https://komarev.com",
|
"profile": "https://komarev.com",
|
||||||
"contributions": ["plugin"]
|
"contributions": [
|
||||||
|
"plugin"
|
||||||
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"login": "KKVANONYMOUS",
|
"login": "KKVANONYMOUS",
|
||||||
"name": "Kunal Kumar Verma",
|
"name": "Kunal Kumar Verma",
|
||||||
"avatar_url": "https://avatars3.githubusercontent.com/u/58628586?v=4",
|
"avatar_url": "https://avatars3.githubusercontent.com/u/58628586?v=4",
|
||||||
"profile": "https://kkvanonymous.github.io/",
|
"profile": "https://kkvanonymous.github.io/",
|
||||||
"contributions": ["code"]
|
"contributions": [
|
||||||
|
"code"
|
||||||
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"login": "jaideepghosh",
|
"login": "jaideepghosh",
|
||||||
"name": "Jaideep Ghosh",
|
"name": "Jaideep Ghosh",
|
||||||
"avatar_url": "https://avatars2.githubusercontent.com/u/3909648?v=4",
|
"avatar_url": "https://avatars2.githubusercontent.com/u/3909648?v=4",
|
||||||
"profile": "http://jaideepghosh.blogspot.com",
|
"profile": "http://jaideepghosh.blogspot.com",
|
||||||
"contributions": ["code"]
|
"contributions": [
|
||||||
},
|
"code"
|
||||||
|
]
|
||||||
|
}
|
||||||
{
|
{
|
||||||
"login": "YashKandalkar",
|
"login": "YashKandalkar",
|
||||||
"name": "yash",
|
"name": "yash",
|
||||||
"avatar_url": "https://avatars0.githubusercontent.com/u/35102959?v=4",
|
"avatar_url": "https://avatars0.githubusercontent.com/u/35102959?v=4",
|
||||||
"profile": "http://yashkandalkar.github.io",
|
"profile": "http://yashkandalkar.github.io",
|
||||||
"contributions": ["code"]
|
"contributions": [
|
||||||
|
"code"
|
||||||
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"login": "abhijit-hota",
|
"login": "abhijit-hota",
|
||||||
"name": "Abhijit Hota",
|
"name": "Abhijit Hota",
|
||||||
"avatar_url": "https://avatars0.githubusercontent.com/u/8116174?v=4",
|
"avatar_url": "https://avatars0.githubusercontent.com/u/8116174?v=4",
|
||||||
"profile": "https://github.com/abhijit-hota",
|
"profile": "https://github.com/abhijit-hota",
|
||||||
"contributions": ["code", "test"]
|
"contributions": [
|
||||||
|
"code",
|
||||||
|
"test"
|
||||||
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"login": "Maddoxx88",
|
"login": "Maddoxx88",
|
||||||
"name": "Sunit Shirke",
|
"name": "Sunit Shirke",
|
||||||
"avatar_url": "https://avatars1.githubusercontent.com/u/34238672?v=4",
|
"avatar_url": "https://avatars1.githubusercontent.com/u/34238672?v=4",
|
||||||
"profile": "https://maddoxx88.github.io/",
|
"profile": "https://maddoxx88.github.io/",
|
||||||
"contributions": ["code"]
|
"contributions": [
|
||||||
},
|
"code"
|
||||||
|
]
|
||||||
|
}
|
||||||
{
|
{
|
||||||
"login": "g-savitha",
|
"login": "g-savitha",
|
||||||
"name": "Savitha Gollamudi",
|
"name": "Savitha Gollamudi",
|
||||||
"avatar_url": "https://avatars0.githubusercontent.com/u/31612459?v=4",
|
"avatar_url": "https://avatars0.githubusercontent.com/u/31612459?v=4",
|
||||||
"profile": "https://www.gsavitha.in",
|
"profile": "https://www.gsavitha.in",
|
||||||
"contributions": ["code"]
|
"contributions": [
|
||||||
|
"code"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"contributorsPerLine": 7,
|
"contributorsPerLine": 7,
|
||||||
|
|||||||
+3
-2
@@ -1,6 +1,6 @@
|
|||||||
# These are supported funding model platforms
|
# These are supported funding model platforms
|
||||||
|
|
||||||
github: rahuldkjain
|
github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2]
|
||||||
patreon: # Replace with a single Patreon username
|
patreon: # Replace with a single Patreon username
|
||||||
open_collective: github-profile-readme-generator
|
open_collective: github-profile-readme-generator
|
||||||
ko_fi: rahuldkjain
|
ko_fi: rahuldkjain
|
||||||
@@ -9,4 +9,5 @@ community_bridge: # Replace with a single Community Bridge project-name e.g., cl
|
|||||||
liberapay: # Replace with a single Liberapay username
|
liberapay: # Replace with a single Liberapay username
|
||||||
issuehunt: # Replace with a single IssueHunt username
|
issuehunt: # Replace with a single IssueHunt username
|
||||||
otechie: # Replace with a single Otechie 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"]
|
||||||
|
|||||||
@@ -1,50 +1,43 @@
|
|||||||
---
|
---
|
||||||
name: 🐛 Bug Report
|
name: Bug report
|
||||||
about: Report a bug in GitHub Profile README Generator
|
about: Create a report to help us improve
|
||||||
title: '[Bug] '
|
title: ''
|
||||||
labels: ['bug']
|
labels: bug
|
||||||
assignees: ''
|
assignees: ''
|
||||||
---
|
---
|
||||||
|
|
||||||
## 🐛 Bug Description
|
**Describe the bug**
|
||||||
|
|
||||||
A clear and concise description of what the bug is.
|
A clear and concise description of what the bug is.
|
||||||
|
|
||||||
## 🔄 Steps to Reproduce
|
**To Reproduce**
|
||||||
|
Steps to reproduce the behavior:
|
||||||
|
|
||||||
1. Go to [URL or page]
|
1. Go to '...'
|
||||||
2. Click on [element]
|
2. Click on '....'
|
||||||
3. Fill in [specific fields]
|
3. Scroll down to '....'
|
||||||
4. See error
|
4. See error
|
||||||
|
|
||||||
## ✅ Expected Behavior
|
**Expected behavior**
|
||||||
|
A clear and concise description of what you expected to happen.
|
||||||
A clear description of what you expected to happen.
|
|
||||||
|
|
||||||
## 📸 Screenshots
|
|
||||||
|
|
||||||
|
**Screenshots**
|
||||||
If applicable, add screenshots to help explain your problem.
|
If applicable, add screenshots to help explain your problem.
|
||||||
|
|
||||||
## 🖥️ Environment
|
**Desktop (please complete the following information):**
|
||||||
|
|
||||||
**Desktop:**
|
- OS: [e.g. iOS]
|
||||||
|
- Browser [e.g. chrome, safari]
|
||||||
|
- Version [e.g. 22]
|
||||||
|
|
||||||
- OS: [e.g. macOS, Windows, Linux]
|
**Smartphone (please complete the following information):**
|
||||||
- Browser: [e.g. Chrome 118, Safari 17, Firefox 119]
|
|
||||||
|
|
||||||
**Mobile:**
|
- Device: [e.g. iPhone6]
|
||||||
|
- OS: [e.g. iOS8.1]
|
||||||
|
- Browser [e.g. stock browser, safari]
|
||||||
|
- Version [e.g. 22]
|
||||||
|
|
||||||
- Device: [e.g. iPhone 15, Samsung Galaxy S23]
|
**Additional context**
|
||||||
- OS: [e.g. iOS 17.1, Android 14]
|
Add any other context about the problem here.
|
||||||
- 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.
|
Join the **Discord Server** for further discussions.
|
||||||
|
|
||||||
|
|||||||
@@ -1,67 +0,0 @@
|
|||||||
---
|
|
||||||
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
|
|
||||||
@@ -1,174 +1,42 @@
|
|||||||
<!--
|
<!--
|
||||||
🚀 Thanks for contributing to GitHub Profile README Generator V2!
|
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.
|
||||||
|
|
||||||
Before submitting your Pull Request, please ensure you've done the following:
|
For a timely review/response, please avoid force-pushing additional
|
||||||
📖 Read the Contributing Guide: https://github.com/rahuldkjain/github-profile-readme-generator/blob/master/CONTRIBUTING.md
|
commits if your PR already received reviews or comments.
|
||||||
📖 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
|
|
||||||
|
|
||||||
For Work In Progress PRs, please use the Draft PR feature.
|
Before submitting a Pull Request, please ensure you've done the following:
|
||||||
Avoid force-pushing after receiving reviews unless requested.
|
- 📖 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.
|
||||||
-->
|
-->
|
||||||
|
|
||||||
# 🔄 Pull Request
|
## What type of PR is this? (check all applicable)
|
||||||
|
|
||||||
## 📋 **Type of Change** (check all applicable)
|
- [ ] Refactor
|
||||||
|
- [ ] Feature
|
||||||
|
- [ ] Bug Fix
|
||||||
|
- [ ] Enhancement
|
||||||
|
- [ ] Documentation Update
|
||||||
|
|
||||||
- [ ] 🐛 **Bug Fix** - Fixes an issue without breaking existing functionality
|
## Description
|
||||||
- [ ] ✨ **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
|
|
||||||
|
|
||||||
## 📖 **Description**
|
## Related Tickets & Documents
|
||||||
|
|
||||||
<!-- Provide a clear and concise description of what this PR does -->
|
## QA Instructions, Screenshots, Recordings
|
||||||
|
|
||||||
### **What changed?**
|
_Please replace this line with instructions on how to test your changes, as well
|
||||||
|
as any relevant images for UI changes._
|
||||||
|
|
||||||
<!-- Describe the changes made -->
|
<!-- ## Added tests?
|
||||||
|
|
||||||
### **Why was this change made?**
|
- [ ] yes
|
||||||
|
- [ ] no, because they aren't needed
|
||||||
|
- [ ] no, because I need help -->
|
||||||
|
|
||||||
<!-- Explain the motivation behind this change -->
|
## Added to documentation?
|
||||||
|
|
||||||
### **How does this change help users?**
|
- [ ] readme
|
||||||
|
|
||||||
<!-- 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
|
|
||||||
|
|||||||
@@ -1,182 +0,0 @@
|
|||||||
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
|
|
||||||
+62
-37
@@ -1,45 +1,70 @@
|
|||||||
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
dist/
|
||||||
|
# Logs
|
||||||
# dependencies
|
logs
|
||||||
/node_modules
|
*.log
|
||||||
/.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*
|
npm-debug.log*
|
||||||
yarn-debug.log*
|
yarn-debug.log*
|
||||||
yarn-error.log*
|
yarn-error.log*
|
||||||
.pnpm-debug.log*
|
|
||||||
|
|
||||||
# env files (can opt-in for committing if needed)
|
# 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*
|
.env*
|
||||||
|
|
||||||
# vercel
|
# gatsby files
|
||||||
.vercel
|
.cache/
|
||||||
|
public
|
||||||
|
|
||||||
# typescript
|
# Mac files
|
||||||
*.tsbuildinfo
|
.DS_Store
|
||||||
next-env.d.ts
|
|
||||||
|
|
||||||
# Claude
|
# Yarn
|
||||||
.cursor/
|
yarn-error.log
|
||||||
.claude/
|
.pnp/
|
||||||
|
.pnp.js
|
||||||
|
# Yarn Integrity file
|
||||||
|
.yarn-integrity
|
||||||
|
|||||||
+3
-5
@@ -2,10 +2,8 @@
|
|||||||
"singleQuote": true,
|
"singleQuote": true,
|
||||||
"jsxSingleQuote": false,
|
"jsxSingleQuote": false,
|
||||||
"tabWidth": 2,
|
"tabWidth": 2,
|
||||||
"printWidth": 100,
|
"printWidth": 120,
|
||||||
"trailingComma": "es5",
|
"trailingComma": "all",
|
||||||
"semi": true,
|
"semi": true,
|
||||||
"arrowParens": "always",
|
"exclude": ["node_modules", "codepipeline"]
|
||||||
"endOfLine": "lf",
|
|
||||||
"plugins": ["prettier-plugin-tailwindcss"]
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,6 @@
|
|||||||
|
language: node_js
|
||||||
|
node_js:
|
||||||
|
- "14"
|
||||||
|
cache:
|
||||||
|
directories:
|
||||||
|
- "node_modules"
|
||||||
@@ -1,82 +0,0 @@
|
|||||||
# 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
@@ -1,434 +0,0 @@
|
|||||||
# 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! 📋**
|
|
||||||
+70
-287
@@ -1,316 +1,99 @@
|
|||||||
# Coding Style Guide
|
# Coding Style
|
||||||
|
|
||||||
## Project Architecture
|
## File Layout (`src/components/*.js`)
|
||||||
|
|
||||||
This project uses **Next.js 15** with **TypeScript** and **Tailwind CSS**. We follow modern React patterns with functional components and hooks.
|
1. Imports
|
||||||
|
2. Reusable components needed for the main component
|
||||||
|
3. Main component (Eg: Addons in addons.js)
|
||||||
|
4. export default \<MainComponent\>;
|
||||||
|
|
||||||
## File Layout (`src/components/*.tsx`)
|
## Reusable components
|
||||||
|
|
||||||
1. **Imports**
|
- Do not make a new file for smaller components.
|
||||||
- React imports first
|
- Smaller, reusable components needed in the main components should be added **above** the main component, **not** inside it.
|
||||||
- Third-party library imports
|
- Use ES6 arrow functions for defining components.
|
||||||
- Internal component imports
|
|
||||||
- Type imports (using `import type`)
|
|
||||||
|
|
||||||
2. **Type Definitions**
|
## Spacing
|
||||||
- Interface definitions for props
|
|
||||||
- Type aliases if needed
|
|
||||||
|
|
||||||
3. **Reusable Components**
|
1. **JS:**
|
||||||
- Smaller components needed for the main component
|
|
||||||
- Place **above** the main component, **not** inside it
|
|
||||||
|
|
||||||
4. **Main Component**
|
- Use a space after `if`, `for`, `while`, `switch`.
|
||||||
- Main exported component (e.g., `SkillsSection` in `skills-section.tsx`)
|
- Do not use a space after the opening `(` and before the closing `)`.
|
||||||
|
- Use a space before and after destructuring objects.
|
||||||
|
|
||||||
5. **Export Statement**
|
```js
|
||||||
- `export default MainComponent;` or named exports
|
//good
|
||||||
|
const { apple, mangoes } = fruits;
|
||||||
|
|
||||||
## TypeScript Guidelines
|
//bad
|
||||||
|
const { apple, mangoes } = fruits;
|
||||||
|
```
|
||||||
|
|
||||||
### Component Props
|
|
||||||
|
|
||||||
- Use `interface` for component props with clear naming:
|
//Same for destructuring props:
|
||||||
|
//good
|
||||||
|
const BeautifulComponent = ({ prop1, prop2 }) => {}
|
||||||
|
|
||||||
```tsx
|
//bad
|
||||||
interface ButtonProps {
|
const UglyComponent = ({prop1, prop2}) => {}
|
||||||
variant?: 'primary' | 'secondary';
|
```
|
||||||
size?: 'sm' | 'md' | 'lg';
|
|
||||||
onClick?: () => void;
|
|
||||||
children: React.ReactNode;
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Type Safety
|
2. **JSX:**
|
||||||
|
|
||||||
- Avoid `any` types - use `unknown` or proper types
|
- Use a space before the forward slash (`/`) of a self-closing tag
|
||||||
- Use strict TypeScript configuration
|
|
||||||
- Leverage type inference where possible
|
|
||||||
- Use `as const` for literal types
|
|
||||||
|
|
||||||
```tsx
|
```js
|
||||||
// Good
|
//good
|
||||||
const themes = ['light', 'dark'] as const;
|
<Foo />
|
||||||
type Theme = (typeof themes)[number];
|
|
||||||
|
|
||||||
// Bad
|
//bad
|
||||||
const themes: any = ['light', 'dark'];
|
<Foo/>
|
||||||
```
|
```
|
||||||
|
|
||||||
## Component Patterns
|
- Do **not** use spaces for JSX curly braces
|
||||||
|
|
||||||
### Functional Components
|
```js
|
||||||
|
//good
|
||||||
|
<Foo bar={baz} />
|
||||||
|
|
||||||
- Use **ES6 arrow functions** for all components
|
//bad
|
||||||
- Use `React.forwardRef` when ref forwarding is needed
|
<Foo bar={ baz } />
|
||||||
- Prefer named exports for reusable components
|
```
|
||||||
|
|
||||||
```tsx
|
## **Props:**
|
||||||
// Good
|
|
||||||
export const Button = ({ variant = 'primary', ...props }: ButtonProps) => {
|
|
||||||
return <button className={`btn btn-${variant}`} {...props} />;
|
|
||||||
};
|
|
||||||
|
|
||||||
// Also good for main components
|
- Use camelCase for prop names, or PascalCase if the prop value is a React component.
|
||||||
const SkillsSection = ({ skills, onSkillToggle }: SkillsSectionProps) => {
|
- Use new lines when props do not fit on the same line.
|
||||||
// component logic
|
|
||||||
};
|
|
||||||
export default SkillsSection;
|
|
||||||
```
|
|
||||||
|
|
||||||
### Hooks Usage
|
```js
|
||||||
|
//good
|
||||||
|
<Foo
|
||||||
|
prop1={value1}
|
||||||
|
prop2={value2}
|
||||||
|
prop3={value3}
|
||||||
|
/>
|
||||||
|
|
||||||
- Use custom hooks for reusable logic
|
//bad
|
||||||
- Keep hooks at the top of components
|
<Foo prop1={value1} prop2={value2} prop3={value3} />
|
||||||
- Use `useCallback` and `useMemo` for performance optimization
|
```
|
||||||
|
|
||||||
```tsx
|
## **Best practices:**
|
||||||
const MyComponent = () => {
|
|
||||||
const [state, setState] = useState();
|
|
||||||
const { data, loading } = useCustomHook();
|
|
||||||
|
|
||||||
const memoizedValue = useMemo(() => expensiveCalculation(), [dependency]);
|
- **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.
|
||||||
|
|
||||||
// component JSX
|
### Other things to note
|
||||||
};
|
|
||||||
```
|
|
||||||
|
|
||||||
## Styling with Tailwind CSS
|
- 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.
|
||||||
|
|
||||||
### Class Organization
|
## Further reading:
|
||||||
|
|
||||||
- Use Tailwind utility classes
|
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.
|
||||||
- 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).
|
|
||||||
|
|||||||
@@ -1,152 +0,0 @@
|
|||||||
# 📝 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.
|
|
||||||
+13
-281
@@ -1,6 +1,7 @@
|
|||||||
# Contributing to GitHub Profile README Generator
|
# Contributing
|
||||||
|
|
||||||
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">
|
<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"/>
|
<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"/>
|
||||||
@@ -8,282 +9,13 @@ When contributing to this repository, please first discuss the change you wish t
|
|||||||
|
|
||||||
Please note we have a code of conduct, please follow it in all your interactions with the project.
|
Please note we have a code of conduct, please follow it in all your interactions with the project.
|
||||||
|
|
||||||
## 🚀 Tech Stack
|
## Pull Request Process
|
||||||
|
|
||||||
This project is built with modern web technologies:
|
1. Ensure any install or build dependencies are removed before the end of the layer when doing a
|
||||||
|
build.
|
||||||
- **Framework**: [Next.js 15](https://nextjs.org/) with App Router
|
2. Update the README.md with details of changes to the interface, this includes new environment
|
||||||
- **Language**: [TypeScript](https://www.typescriptlang.org/) for type safety
|
variables, exposed ports, useful file locations and container parameters.
|
||||||
- **Styling**: [Tailwind CSS](https://tailwindcss.com/) for utility-first styling
|
3. Increase the version numbers in any examples files and the README.md to the new version that this
|
||||||
- **Icons**: [Lucide React](https://lucide.dev/) for consistent iconography
|
Pull Request would represent. The versioning scheme we use is [SemVer](http://semver.org/).
|
||||||
- **Animations**: [Framer Motion](https://www.framer.com/motion/) for smooth animations
|
4. You may merge the Pull Request once you have the sign-off of two other developers, or if you
|
||||||
- **Forms**: [React Hook Form](https://react-hook-form.com/) for form management
|
do not have permission to do that, you may request the second reviewer merge it for you.
|
||||||
- **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
@@ -1,141 +0,0 @@
|
|||||||
# 🚀 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)
|
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
<p align="center">
|
<p align="center">
|
||||||
<a href="https://rahuldkjain.github.io/github-profile-readme-generator">
|
<a href="https://rahuldkjain.github.io/gh-profile-readme-generator">
|
||||||
<img alt="GitHub Profile Readme Generator" src="./src/images/mdg.png" width="60" />
|
<img alt="GitHub Profile Readme Generator" src="./src/images/mdg.png" width="60" />
|
||||||
</a>
|
</a>
|
||||||
</p>
|
</p>
|
||||||
@@ -28,10 +28,10 @@
|
|||||||
</a>
|
</a>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<p align="center"><img src="/public/demo.gif" alt="github-profile-readme-generator gif" /></p>
|
<p align="center"><img src="./src/images/github-profile-readme-generator.gif" alt="github-profile-readme-generator gif" /></p>
|
||||||
|
|
||||||
<p align="center">
|
<p align="center">
|
||||||
<a href="https://rahuldkjain.github.io/github-profile-readme-generator" target="blank">View Demo</a>
|
<a href="https://rahuldkjain.github.io/gh-profile-readme-generator" target="blank">View Demo</a>
|
||||||
·
|
·
|
||||||
<a href="https://github.com/rahuldkjain/github-profile-readme-generator/issues/new/choose">Report Bug</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
|
## 🚀 Demo
|
||||||
|
|
||||||
<a href="https://rahuldkjain.github.io/github-profile-readme-generator" target="blank">
|
<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%2Fgithub-profile-readme-generator&logo=github&style=flat-square" />
|
<img src="https://img.shields.io/website?url=https%3A%2F%2Frahuldkjain.github.io%2Fgh-profile-readme-generator&logo=github&style=flat-square" />
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
Try the tool: [GitHub Profile README Generator](https://rahuldkjain.github.io/github-profile-readme-generator)
|
Try the tool: [GitHub Profile README Generator](https://rahuldkjain.github.io/gh-profile-readme-generator)
|
||||||
|
|
||||||
## 🧐 Features
|
## 🧐 Features
|
||||||
|
|
||||||
@@ -114,7 +114,7 @@ npm install
|
|||||||
4. Run the app
|
4. Run the app
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
npm run dev
|
npm start
|
||||||
```
|
```
|
||||||
|
|
||||||
🌟 You are all set!
|
🌟 You are all set!
|
||||||
@@ -127,12 +127,9 @@ Please read [`CONTRIBUTING`](CONTRIBUTING.md) for details on our [`CODE OF CONDU
|
|||||||
|
|
||||||
## 💻 Built with
|
## 💻 Built with
|
||||||
|
|
||||||
- [Next.js 15](https://nextjs.org/) - React framework with App Router
|
- [Gatsby](https://www.gatsbyjs.com/)
|
||||||
- [TypeScript](https://www.typescriptlang.org/) - Type safety and better DX
|
- [Tailwind CSS](https://tailwindcss.com/): for styling
|
||||||
- [Tailwind CSS](https://tailwindcss.com/) - Utility-first CSS framework
|
- [GSAP](https://greensock.com/gsap/): for small SVG Animations
|
||||||
- [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
|
## 🙇 Special Thanks
|
||||||
|
|
||||||
@@ -150,28 +147,6 @@ 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.
|
- [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.
|
- [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
|
## 🙏 Support
|
||||||
|
|
||||||
<p align="left">
|
<p align="left">
|
||||||
@@ -188,13 +163,6 @@ For commercial use of this project, please verify font licensing requirements.
|
|||||||
<a href="https://www.buymeacoffee.com/rahuldkjain" target="_blank"><img src="https://cdn.buymeacoffee.com/buttons/default-orange.png" alt="Buy Me A Coffee" height="23" width="100" style="border-radius:2px" />
|
<a href="https://www.buymeacoffee.com/rahuldkjain" target="_blank"><img src="https://cdn.buymeacoffee.com/buttons/default-orange.png" alt="Buy Me A Coffee" height="23" width="100" style="border-radius:2px" />
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
## 🌟 Star History
|
|
||||||
<picture>
|
|
||||||
<source media="(prefers-color-scheme: dark)" srcset="https://api.star-history.com/svg?repos=rahuldkjain/github-profile-readme-generator&type=Date&theme=dark" />
|
|
||||||
<source media="(prefers-color-scheme: light)" srcset="https://api.star-history.com/svg?repos=rahuldkjain/github-profile-readme-generator&type=Date" />
|
|
||||||
<img alt="GPRG Star History Chart" src="https://api.star-history.com/svg?repos=rahuldkjain/github-profile-readme-generator&type=Date" />
|
|
||||||
</picture>
|
|
||||||
|
|
||||||
<hr>
|
<hr>
|
||||||
<p align="center">
|
<p align="center">
|
||||||
Developed with ❤️ in India 🇮🇳
|
Developed with ❤️ in India 🇮🇳
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
module.exports = 'test-file-stub';
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
/* 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
@@ -1,27 +0,0 @@
|
|||||||
# 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
|
|
||||||
@@ -1,44 +0,0 @@
|
|||||||
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;
|
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
import './src/styles/tailwind.css';
|
||||||
|
require('prismjs/themes/prism-okaidia.css');
|
||||||
@@ -0,0 +1,72 @@
|
|||||||
|
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`,
|
||||||
|
};
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
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,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
const babelOptions = {
|
||||||
|
presets: ['babel-preset-gatsby'],
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports = require('babel-jest').createTransformer(babelOptions);
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
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,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
global.___loader = {
|
||||||
|
enqueue: jest.fn(),
|
||||||
|
};
|
||||||
-115
@@ -1,115 +0,0 @@
|
|||||||
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;
|
|
||||||
Generated
+62930
-9840
File diff suppressed because it is too large
Load Diff
+81
-49
@@ -1,59 +1,91 @@
|
|||||||
{
|
{
|
||||||
"name": "github-profile-readme-generator",
|
"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,
|
"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>",
|
"author": "Rahul Jain <rahuldkjain@gmail.com>",
|
||||||
"scripts": {
|
"husky": {
|
||||||
"dev": "TURBOPACK=1 next dev --turbo",
|
"hooks": {
|
||||||
"build": "next build --turbo",
|
"pre-commit": "lint-staged"
|
||||||
"export": "next build --turbo",
|
}
|
||||||
"start": "next start",
|
},
|
||||||
"type-check": "tsc --noEmit",
|
"lint-staged": {
|
||||||
"lint": "eslint",
|
"*.{js,jsx}": [
|
||||||
"format": "prettier --write \"src/**/*.{ts,tsx,js,jsx,json,css,md}\"",
|
"prettier --write",
|
||||||
"format:check": "prettier --check \"src/**/*.{ts,tsx,js,jsx,json,css,md}\"",
|
"eslint --fix",
|
||||||
"test": "vitest",
|
"git add"
|
||||||
"test:ui": "vitest --ui",
|
],
|
||||||
"test:coverage": "vitest --coverage"
|
"*.{html,css,less,ejs}": [
|
||||||
|
"prettier --write",
|
||||||
|
"git add"
|
||||||
|
]
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@headlessui/react": "^2.2.9",
|
"@primer/octicons-react": "^10.0.0",
|
||||||
"@hookform/resolvers": "^5.2.2",
|
"axios": "^0.24.0",
|
||||||
"@next/third-parties": "^15.5.4",
|
"enzyme": "^3.11.0",
|
||||||
"@tailwindcss/typography": "^0.5.19",
|
"enzyme-adapter-react-16": "^1.15.5",
|
||||||
"critters": "^0.0.23",
|
"enzyme-to-json": "^3.6.1",
|
||||||
"framer-motion": "^12.23.24",
|
"gatsby": "^2.23.12",
|
||||||
"lucide-react": "^0.545.0",
|
"gatsby-image": "^2.4.9",
|
||||||
"next": "15.5.4",
|
"gatsby-plugin-google-analytics": "^2.3.11",
|
||||||
"react": "19.1.0",
|
"gatsby-plugin-manifest": "^2.4.14",
|
||||||
"react-dom": "19.1.0",
|
"gatsby-plugin-offline": "^3.2.13",
|
||||||
"react-hook-form": "^7.65.0",
|
"gatsby-plugin-react-helmet": "^3.3.6",
|
||||||
"react-markdown": "^10.1.0",
|
"gatsby-plugin-sharp": "2.6.14",
|
||||||
"rehype-raw": "^7.0.0",
|
"gatsby-remark-prismjs": "^3.5.10",
|
||||||
"rehype-sanitize": "^6.0.0",
|
"gatsby-source-filesystem": "^2.3.23",
|
||||||
"remark-gfm": "^4.0.1",
|
"gatsby-transformer-remark": "^2.8.27",
|
||||||
"zod": "^4.1.12",
|
"gatsby-transformer-sharp": "^2.5.7",
|
||||||
"zustand": "^5.0.8"
|
"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"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@eslint/eslintrc": "^3",
|
"babel-jest": "26.3.0",
|
||||||
"@tailwindcss/postcss": "^4",
|
"babel-preset-gatsby": "0.5.11",
|
||||||
"@testing-library/jest-dom": "^6.9.1",
|
"eslint": "^7.32.0",
|
||||||
"@testing-library/react": "^16.3.0",
|
"eslint-config-airbnb": "^18.2.1",
|
||||||
"@testing-library/user-event": "^14.6.1",
|
"eslint-config-prettier": "^8.3.0",
|
||||||
"@types/node": "^20",
|
"eslint-plugin-import": "^2.25.2",
|
||||||
"@types/react": "^19",
|
"eslint-plugin-jsx-a11y": "^6.4.1",
|
||||||
"@types/react-dom": "^19",
|
"eslint-plugin-prettier": "^4.0.0",
|
||||||
"@vitejs/plugin-react": "^5.0.4",
|
"eslint-plugin-react": "^7.26.1",
|
||||||
"eslint": "^9",
|
"eslint-plugin-react-hooks": "^4.2.0",
|
||||||
"eslint-config-next": "15.5.4",
|
"gatsby-plugin-postcss": "^2.3.11",
|
||||||
"eslint-config-prettier": "^10.1.8",
|
"gatsby-plugin-purgecss": "^5.0.0",
|
||||||
"jsdom": "^27.0.0",
|
"gatsby-plugin-twitter": "^2.3.10",
|
||||||
"prettier": "^3.6.2",
|
"gatsby-remark-embedder": "^3.0.0",
|
||||||
"prettier-plugin-tailwindcss": "^0.6.14",
|
"gh-pages": "^3.1.0",
|
||||||
"tailwindcss": "^4",
|
"husky": "^7.0.4",
|
||||||
"typescript": "^5",
|
"identity-obj-proxy": "3.0.0",
|
||||||
"vitest": "^3.2.4"
|
"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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +0,0 @@
|
|||||||
const config = {
|
|
||||||
plugins: ["@tailwindcss/postcss"],
|
|
||||||
};
|
|
||||||
|
|
||||||
export default config;
|
|
||||||
Binary file not shown.
|
Before Width: | Height: | Size: 3.8 MiB |
Binary file not shown.
|
Before Width: | Height: | Size: 11 KiB |
Binary file not shown.
@@ -1,29 +0,0 @@
|
|||||||
{
|
|
||||||
"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
|
|
||||||
}
|
|
||||||
Binary file not shown.
|
Before Width: | Height: | Size: 11 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 11 KiB |
@@ -1,9 +0,0 @@
|
|||||||
# 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
|
|
||||||
@@ -1,196 +0,0 @@
|
|||||||
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="https://raw.githubusercontent.com/rahuldkjain/github-profile-readme-generator/master/src/images/github-profile-readme-generator.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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,298 +0,0 @@
|
|||||||
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 **<!-- BLOG-POST-LIST:START --><!--
|
|
||||||
BLOG-POST-LIST:END -->** 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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,417 +0,0 @@
|
|||||||
@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;
|
|
||||||
}
|
|
||||||
@@ -1,162 +0,0 @@
|
|||||||
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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,633 +0,0 @@
|
|||||||
'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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,18 +0,0 @@
|
|||||||
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`,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
@@ -1,34 +0,0 @@
|
|||||||
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,
|
|
||||||
},
|
|
||||||
];
|
|
||||||
}
|
|
||||||
@@ -1,141 +0,0 @@
|
|||||||
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
@@ -0,0 +1,101 @@
|
|||||||
|
// 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>
|
||||||
|
`;
|
||||||
@@ -0,0 +1,189 @@
|
|||||||
|
// 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>
|
||||||
|
`;
|
||||||
@@ -0,0 +1,73 @@
|
|||||||
|
// 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>
|
||||||
|
`;
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
// 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
@@ -0,0 +1,601 @@
|
|||||||
|
// 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": "👯 I’m looking to collaborate on",
|
||||||
|
"contact": "📫 How to reach me",
|
||||||
|
"currentLearn": "🌱 I’m currently learning",
|
||||||
|
"currentWork": "🔭 I’m currently working on",
|
||||||
|
"funFact": "⚡ Fun fact",
|
||||||
|
"helpWith": "🤝 I’m 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="🔭 I’m currently working on"
|
||||||
|
project="readme-generator"
|
||||||
|
/>
|
||||||
|
<DisplayWork
|
||||||
|
link=""
|
||||||
|
prefix="🌱 I’m currently learning"
|
||||||
|
project=""
|
||||||
|
/>
|
||||||
|
<DisplayWork
|
||||||
|
link=""
|
||||||
|
prefix="🤝 I’m looking for help with"
|
||||||
|
project=""
|
||||||
|
/>
|
||||||
|
<DisplayWork
|
||||||
|
link=""
|
||||||
|
prefix="👯 I’m 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>
|
||||||
|
`;
|
||||||
@@ -0,0 +1,156 @@
|
|||||||
|
// 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>
|
||||||
|
`;
|
||||||
@@ -0,0 +1,383 @@
|
|||||||
|
// 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>
|
||||||
|
`;
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
// 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>
|
||||||
|
`;
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
// 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>
|
||||||
|
`;
|
||||||
@@ -0,0 +1,184 @@
|
|||||||
|
// 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>
|
||||||
|
`;
|
||||||
@@ -0,0 +1,806 @@
|
|||||||
|
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);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
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();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
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();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
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();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
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();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,214 @@
|
|||||||
|
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: '👯 I’m looking to collaborate on',
|
||||||
|
contact: '📫 How to reach me',
|
||||||
|
currentLearn: '🌱 I’m currently learning',
|
||||||
|
currentWork: '🔭 I’m currently working on',
|
||||||
|
funFact: '⚡ Fun fact',
|
||||||
|
helpWith: '🤝 I’m 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();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,404 @@
|
|||||||
|
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: '🔭 I’m currently working on',
|
||||||
|
currentLearn: '🌱 I’m currently learning',
|
||||||
|
collaborateOn: '👯 I’m looking to collaborate on',
|
||||||
|
helpWith: '🤝 I’m 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();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
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);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
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();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
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);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
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();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
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();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,482 @@
|
|||||||
|
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:
|
||||||
|
<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:
|
||||||
|
<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:
|
||||||
|
<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:
|
||||||
|
{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:
|
||||||
|
<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:
|
||||||
|
<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:
|
||||||
|
<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:
|
||||||
|
<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:
|
||||||
|
<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:
|
||||||
|
<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:
|
||||||
|
<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:
|
||||||
|
<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,
|
||||||
|
};
|
||||||
@@ -1,33 +0,0 @@
|
|||||||
'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} />;
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,88 @@
|
|||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
const Donate = () => (
|
||||||
|
<>
|
||||||
|
<div className="text-center text-4xl my-2">
|
||||||
|
Support
|
||||||
|
<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;
|
||||||
@@ -0,0 +1,103 @@
|
|||||||
|
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;
|
||||||
@@ -1,128 +0,0 @@
|
|||||||
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');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,40 +0,0 @@
|
|||||||
'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';
|
|
||||||
@@ -1,40 +0,0 @@
|
|||||||
'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';
|
|
||||||
@@ -1,24 +0,0 @@
|
|||||||
'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';
|
|
||||||
@@ -1,40 +0,0 @@
|
|||||||
'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';
|
|
||||||
@@ -1,138 +0,0 @@
|
|||||||
'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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
);
|
|
||||||
@@ -0,0 +1,95 @@
|
|||||||
|
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,
|
||||||
|
};
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
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,
|
||||||
|
};
|
||||||
@@ -1,173 +0,0 @@
|
|||||||
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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,111 +0,0 @@
|
|||||||
'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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,51 +0,0 @@
|
|||||||
'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}</>;
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,80 @@
|
|||||||
|
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;
|
||||||
@@ -0,0 +1,744 @@
|
|||||||
|
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> <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: {},
|
||||||
|
};
|
||||||
@@ -0,0 +1,510 @@
|
|||||||
|
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"> </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,
|
||||||
|
};
|
||||||
@@ -1,319 +0,0 @@
|
|||||||
'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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,53 +0,0 @@
|
|||||||
'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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,264 +0,0 @@
|
|||||||
'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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,134 +0,0 @@
|
|||||||
'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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,88 @@
|
|||||||
|
/**
|
||||||
|
* 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;
|
||||||
@@ -0,0 +1,99 @@
|
|||||||
|
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,
|
||||||
|
};
|
||||||
@@ -0,0 +1,355 @@
|
|||||||
|
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,
|
||||||
|
};
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
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,
|
||||||
|
};
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
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,
|
||||||
|
};
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
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,
|
||||||
|
};
|
||||||
@@ -1,85 +0,0 @@
|
|||||||
'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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,39 +0,0 @@
|
|||||||
'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;
|
|
||||||
}
|
|
||||||
@@ -1,63 +0,0 @@
|
|||||||
'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
Reference in New Issue
Block a user