├── .env.example ├── .gitignore ├── LICENSE ├── README.md ├── demo.png ├── eslint.config.js ├── index.html ├── package-lock.json ├── package.json ├── public ├── favicon.ico ├── index.html ├── logo192.png ├── logo512.png ├── manifest.json ├── robots.txt └── youtube.svg ├── src ├── App.css ├── App.jsx ├── api │ └── youtube.js ├── assets │ └── react.svg ├── components │ ├── ApiKeyManager.jsx │ ├── LandingPage.jsx │ ├── body.jsx │ ├── footer.jsx │ └── header.jsx ├── index.css ├── logo.svg └── main.jsx ├── vercel.json └── vite.config.js /.env.example: -------------------------------------------------------------------------------- 1 | # To get your own API key, go to https://console.cloud.google.com/apis/api/youtube.googleapis.com/ 2 | # Create a new project and enable the YouTube Data API v3 3 | # Then create an API key and add it to the .env file 4 | 5 | VITE_YOUTUBE_API_KEY=your_api_key -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Deta 10 | deta 11 | # Heroku 12 | heroku 13 | 14 | # netlify 15 | netlify 16 | 17 | # Distribution / packaging 18 | .Python 19 | # build/ 20 | develop-eggs/ 21 | dist/ 22 | downloads/ 23 | eggs/ 24 | .eggs/ 25 | lib/ 26 | lib64/ 27 | parts/ 28 | sdist/ 29 | var/ 30 | wheels/ 31 | pip-wheel-metadata/ 32 | share/python-wheels/ 33 | *.egg-info/ 34 | .installed.cfg 35 | *.egg 36 | MANIFEST 37 | 38 | # PyInstaller 39 | # Usually these files are written by a python script from a template 40 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 41 | *.manifest 42 | *.spec 43 | 44 | # Installer logs 45 | pip-log.txt 46 | pip-delete-this-directory.txt 47 | 48 | # Unit test / coverage reports 49 | htmlcov/ 50 | .tox/ 51 | .nox/ 52 | .coverage 53 | .coverage.* 54 | .cache 55 | nosetests.xml 56 | coverage.xml 57 | *.cover 58 | *.py,cover 59 | .hypothesis/ 60 | .pytest_cache/ 61 | 62 | # Translations 63 | *.mo 64 | *.pot 65 | 66 | # Django stuff: 67 | *.log 68 | local_settings.py 69 | db.sqlite3 70 | db.sqlite3-journal 71 | 72 | # Flask stuff: 73 | instance/ 74 | .webassets-cache 75 | 76 | # Scrapy stuff: 77 | .scrapy 78 | 79 | # Sphinx documentation 80 | docs/_build/ 81 | 82 | # PyBuilder 83 | target/ 84 | 85 | # Jupyter Notebook 86 | .ipynb_checkpoints 87 | 88 | # IPython 89 | profile_default/ 90 | ipython_config.py 91 | 92 | # pyenv 93 | .python-version 94 | 95 | # pipenv 96 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 97 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 98 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 99 | # install all needed dependencies. 100 | #Pipfile.lock 101 | 102 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 103 | __pypackages__/ 104 | 105 | # Celery stuff 106 | celerybeat-schedule 107 | celerybeat.pid 108 | 109 | # SageMath parsed files 110 | *.sage.py 111 | 112 | # Environments 113 | .env 114 | .venv 115 | env/ 116 | venv/ 117 | ENV/ 118 | env.bak/ 119 | venv.bak/ 120 | 121 | # Spyder project settings 122 | .spyderproject 123 | .spyproject 124 | 125 | # Rope project settings 126 | .ropeproject 127 | 128 | # mkdocs documentation 129 | /site 130 | 131 | # mypy 132 | .mypy_cache/ 133 | .dmypy.json 134 | dmypy.json 135 | 136 | # Pyre type checker 137 | .pyre/ 138 | 139 | # For ReactJS Frontend 140 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 141 | 142 | # dependencies 143 | frontend/node_modules 144 | frontend/.pnp 145 | frontend/.pnp.js 146 | node_modules 147 | .env 148 | build 149 | dist 150 | 151 | # testing 152 | frontend/coverage 153 | 154 | # production 155 | # frontend/build 156 | 157 | # misc 158 | frontend/.DS_Store 159 | frontend/.env.local 160 | frontend/.env.development.local 161 | frontend/.env.test.local 162 | frontend/.env.production.local 163 | 164 | frontend/npm-debug.log* 165 | frontend/yarn-debug.log* 166 | frontend/yarn-error.log* 167 | 168 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | # ‎ ‎ ‎ ‎ ‎‎‎ ‎‎ ‎ ‎ ‎ ‎ ‎ ‎ ‎ ‎ ‎ ‎ ‎ ‎ ‎ ‎ ‎ ‎‎Distraction Free YouTube Logo Player 3 | 4 | 5 | ## GitAds Sponsored 6 | [![Sponsored by GitAds](https://www.gitads.dev/v1/ad-serve?source=hotheadhacker/youtube-player@github)](https://www.gitads.dev/v1/ad-track?source=hotheadhacker/youtube-player@github) 7 | 8 | ## GitAds Sponsored 9 | [![Sponsored by GitAds](https://gitads.dev/v1/ad-serve?source=hotheadhacker/awesome-selfhost-docker@github)](https://gitads.dev/v1/ad-track?source=hotheadhacker/awesome-selfhost-docker@github) 10 | 11 | 12 |
13 | Demo Screenshot 14 |
15 | 16 | ### Welcome to the **Distraction Free YouTube Player**, a platform developed for focused and optimized learning with YouTube videos! 17 | 18 | ## Platform Features 19 | 20 | - **Ad-Free Experience**: Watch tutorials without any advertisements 21 | - **Curated Content**: Handpicked programming tutorials from top educators 22 | - **Open Source**: Community-driven development, free for everyone 23 | - **Distraction-Free**: Clean interface without recommendations or comments 24 | - **Multiple Playlists**: Organized learning paths for different technologies 25 | - **Saved Progress**: Resume where you left off with automatic progress tracking 26 | - **Toggle Between APIs**: Switch between default and custom YouTube API keys 27 | - **Optimized Performance**: Fast loading and smooth playback 28 | 29 | ## Tech Stack 30 | 31 | - React.js 32 | - Tailwind CSS 33 | - YouTube Data API 34 | - Framer Motion 35 | - React Player 36 | 37 | 38 | ## Setup Guide 39 | 40 | To get started with the **Distraction-Free YouTube Player**, follow these steps: 41 | 42 | 1. **Clone the repository**: 43 | ```bash 44 | git clone https://github.com/hotheadhacker/youtube-player.git 45 | cd youtube-player 46 | ``` 47 | 48 | 2. **Copy the example environment file**: 49 | ```bash 50 | cp .env.example .env 51 | ``` 52 | 53 | 3. **Add your API key**: 54 | Open the `.env` file and replace `your_api_key` with your actual YouTube API key. 55 | 56 | 4. **Install the dependencies**: 57 | ```bash 58 | npm install 59 | ``` 60 | 61 | 5. **Run the development server**: 62 | ```bash 63 | npm run dev 64 | ``` 65 | 66 | This will start the application locally. Open [http://localhost:5173](http://localhost:5173) to view it in your browser. 67 | 68 | ## Contributing 69 | 70 | We welcome contributions to make this platform even better! Here's how you can help: 71 | 72 | 1. Fork the repository 73 | 2. Create your feature branch (`git checkout -b feature/AmazingFeature`) 74 | 3. Commit your changes (`git commit -m 'Add some AmazingFeature'`) 75 | 4. Push to the branch (`git push origin feature/AmazingFeature`) 76 | 5. Open a Pull Request 77 | 78 | Please ensure your PR description clearly describes the changes and their benefits. 79 | 80 | ## Credits 81 | 82 | Platform designed by [Salman Qureshi](https://isalman.dev) and redesigned by [Zaid Adil](https://hothead.vercel.app). 83 | 84 | ## Source Code 85 | 86 | Check out the source code on GitHub: [hotheadhacker/youtube-player](https://github.com/hotheadhacker/youtube-player) 87 | 88 | --- 89 | 90 | Thank you for using the Distraction-Free YouTube Player! If you find it helpful, please consider giving it a star ⭐ 91 | 92 | ## Star History 93 | 94 | [![Star History Chart](https://api.star-history.com/svg?repos=hotheadhacker/youtube-player&type=Date)](https://star-history.com/#hotheadhacker/youtube-player&Date) 95 | -------------------------------------------------------------------------------- /demo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hotheadhacker/youtube-player/5c75ad7052e3625497ab8bd73883368851098a66/demo.png -------------------------------------------------------------------------------- /eslint.config.js: -------------------------------------------------------------------------------- 1 | import js from '@eslint/js' 2 | import globals from 'globals' 3 | import react from 'eslint-plugin-react' 4 | import reactHooks from 'eslint-plugin-react-hooks' 5 | import reactRefresh from 'eslint-plugin-react-refresh' 6 | 7 | export default [ 8 | { ignores: ['dist'] }, 9 | { 10 | files: ['**/*.{js,jsx}'], 11 | languageOptions: { 12 | ecmaVersion: 2020, 13 | globals: globals.browser, 14 | parserOptions: { 15 | ecmaVersion: 'latest', 16 | ecmaFeatures: { jsx: true }, 17 | sourceType: 'module', 18 | }, 19 | }, 20 | settings: { react: { version: '18.3' } }, 21 | plugins: { 22 | react, 23 | 'react-hooks': reactHooks, 24 | 'react-refresh': reactRefresh, 25 | }, 26 | rules: { 27 | ...js.configs.recommended.rules, 28 | ...react.configs.recommended.rules, 29 | ...react.configs['jsx-runtime'].rules, 30 | ...reactHooks.configs.recommended.rules, 31 | 'react/jsx-no-target-blank': 'off', 32 | 'react-refresh/only-export-components': [ 33 | 'warn', 34 | { allowConstantExport: true }, 35 | ], 36 | }, 37 | }, 38 | ] 39 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Distraction Free YouTube Player 8 | 9 | 10 |
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "youtube-player", 3 | "private": true, 4 | "version": "0.3.0", 5 | "type": "module", 6 | "scripts": { 7 | "dev": "vite", 8 | "build": "vite build", 9 | "lint": "eslint .", 10 | "preview": "vite preview" 11 | }, 12 | "dependencies": { 13 | "@tailwindcss/vite": "^4.0.8", 14 | "axios": "^1.7.9", 15 | "clsx": "^2.1.1", 16 | "motion": "^12.4.7", 17 | "react": "^19.0.0", 18 | "react-dom": "^19.0.0", 19 | "react-icons": "^5.5.0", 20 | "react-player": "^2.16.0", 21 | "react-router-dom": "^7.2.0", 22 | "react-toastify": "^11.0.3", 23 | "tailwind-merge": "^3.0.2", 24 | "tailwindcss": "^4.0.8" 25 | }, 26 | "devDependencies": { 27 | "@eslint/js": "^9.19.0", 28 | "@types/react": "^19.0.8", 29 | "@types/react-dom": "^19.0.3", 30 | "@vitejs/plugin-react": "^4.3.4", 31 | "eslint": "^9.19.0", 32 | "eslint-plugin-react": "^7.37.4", 33 | "eslint-plugin-react-hooks": "^5.0.0", 34 | "eslint-plugin-react-refresh": "^0.4.18", 35 | "globals": "^15.14.0", 36 | "vite": "^6.1.0" 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hotheadhacker/youtube-player/5c75ad7052e3625497ab8bd73883368851098a66/public/favicon.ico -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 13 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 34 | Youtube Player 35 | 36 | 37 | 38 |
39 | 49 | 50 | 51 | -------------------------------------------------------------------------------- /public/logo192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hotheadhacker/youtube-player/5c75ad7052e3625497ab8bd73883368851098a66/public/logo192.png -------------------------------------------------------------------------------- /public/logo512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hotheadhacker/youtube-player/5c75ad7052e3625497ab8bd73883368851098a66/public/logo512.png -------------------------------------------------------------------------------- /public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | }, 10 | { 11 | "src": "logo192.png", 12 | "type": "image/png", 13 | "sizes": "192x192" 14 | }, 15 | { 16 | "src": "logo512.png", 17 | "type": "image/png", 18 | "sizes": "512x512" 19 | } 20 | ], 21 | "start_url": ".", 22 | "display": "standalone", 23 | "theme_color": "#000000", 24 | "background_color": "#ffffff" 25 | } 26 | -------------------------------------------------------------------------------- /public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /public/youtube.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/App.css: -------------------------------------------------------------------------------- 1 | @import "tailwindcss"; 2 | .pointer {cursor: pointer;} 3 | .skill-img { 4 | /* padding: 50px; */ 5 | /* background-color: green; */ 6 | transition: transform .2s; /* Animation */ 7 | /* width: 200px; */ 8 | /* height: 200px; */ 9 | /* margin: 2 auto; */ 10 | } 11 | 12 | .skill-img:hover { 13 | transform: scale(1.2); /* (150% zoom - Note: if the zoom is too large, it will go outside of the viewport) */ 14 | } 15 | 16 | 17 | .footer{ 18 | text-align: center; 19 | } 20 | 21 | .github:hover{ 22 | color:grey 23 | } 24 | 25 | .twitter:hover{ 26 | color: #00acee; 27 | } 28 | .instagram:hover{ 29 | color: tomato; 30 | } 31 | .website:hover{ 32 | color: greenyellow; 33 | } 34 | .email:hover{ 35 | color: yellow; 36 | } 37 | .blog:hover{ 38 | color: red; 39 | } 40 | 41 | .footer a{ 42 | color: white; 43 | } 44 | 45 | .footer h3{ 46 | color: white; 47 | } 48 | 49 | .footer h3 a{ 50 | color: rgb(0, 0, 0); 51 | } 52 | /* frontend/src/App.css */ 53 | 54 | /* frontend/src/App.css */ 55 | 56 | /* Light Theme */ 57 | body.light { 58 | background-color: #ffffff; 59 | color: #000000; 60 | } 61 | 62 | .header { 63 | background-color: #f8f9fa; /* Light header background */ 64 | } 65 | 66 | /* Dark Theme */ 67 | body.dark { 68 | background-color: #121212; 69 | color: #ffffff; 70 | } 71 | 72 | .header { 73 | background-color: #1e1e1e; /* Dark header background */ 74 | } 75 | 76 | /* Ensure all components respond to the theme */ 77 | .container { 78 | background-color: inherit; /* Inherit background color from body */ 79 | color: inherit; /* Inherit text color from body */ 80 | } 81 | 82 | /* Additional styles for the theme toggle button */ 83 | .theme-toggle { 84 | padding: 0.5rem 1rem; 85 | border: none; 86 | border-radius: 5px; 87 | cursor: pointer; 88 | background-color: #007bff; 89 | color: white; 90 | transition: background-color 0.3s; 91 | } 92 | 93 | .theme-toggle:hover { 94 | background-color: #0056b3; 95 | } 96 | -------------------------------------------------------------------------------- /src/App.jsx: -------------------------------------------------------------------------------- 1 | // frontend/src/App.js 2 | import { BrowserRouter as Router, Routes, Route } from 'react-router-dom'; 3 | import { useState, useEffect } from 'react'; 4 | import './App.css'; 5 | import Header from './components/header.jsx'; 6 | import Body from './components/body.jsx'; 7 | import LandingPage from './components/LandingPage.jsx'; 8 | import Footer from './components/footer.jsx'; 9 | import { setApiKey } from './api/youtube'; 10 | 11 | function App() { 12 | const [apiKey, setCustomApiKey] = useState(localStorage.getItem('youtubeApiKey') || import.meta.env.VITE_YOUTUBE_API_KEY); 13 | 14 | useEffect(() => { 15 | // Check if using custom key 16 | const useCustomKey = localStorage.getItem('useCustomKey') === 'true'; 17 | const savedKey = localStorage.getItem('youtubeApiKey'); 18 | 19 | // Set the appropriate API key on initial load 20 | if (useCustomKey && savedKey) { 21 | setApiKey(savedKey); 22 | } else { 23 | setApiKey(import.meta.env.VITE_YOUTUBE_API_KEY); 24 | } 25 | }, []); 26 | 27 | const handleApiKeyChange = (newKey) => { 28 | setCustomApiKey(newKey); 29 | setApiKey(newKey); 30 | }; 31 | 32 | return ( 33 | 34 |
35 | 36 | } /> 37 | 39 |
40 | 41 |
46 |
47 | ); 48 | } 49 | 50 | export default App; -------------------------------------------------------------------------------- /src/api/youtube.js: -------------------------------------------------------------------------------- 1 | import axios from 'axios'; 2 | 3 | let YOUTUBE_API_KEY = import.meta.env.VITE_YOUTUBE_API_KEY; 4 | const BASE_URL = 'https://www.googleapis.com/youtube/v3'; 5 | 6 | export const setApiKey = (newKey) => { 7 | YOUTUBE_API_KEY = newKey; 8 | }; 9 | 10 | export async function searchVideos(query) { 11 | return axios.get(`${BASE_URL}/search`, { 12 | params: { 13 | part: 'snippet', 14 | key: YOUTUBE_API_KEY, 15 | type: 'video', 16 | maxResults: 20, 17 | q: query 18 | } 19 | }); 20 | } 21 | 22 | export async function getPlaylistItems(playlistId) { 23 | return axios.get(`${BASE_URL}/playlistItems`, { 24 | params: { 25 | part: 'snippet', 26 | maxResults: 100, 27 | key: YOUTUBE_API_KEY, 28 | playlistId 29 | } 30 | }); 31 | } -------------------------------------------------------------------------------- /src/assets/react.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/components/ApiKeyManager.jsx: -------------------------------------------------------------------------------- 1 | import { useState, useEffect } from 'react'; 2 | import { motion } from 'framer-motion'; 3 | import { toast } from 'react-toastify'; 4 | 5 | export default function ApiKeyManager({ onApiKeyChange }) { 6 | const [useCustomKey, setUseCustomKey] = useState(false); 7 | const [customApiKey, setCustomApiKey] = useState(''); 8 | const defaultApiKey = import.meta.env.VITE_YOUTUBE_API_KEY; 9 | 10 | useEffect(() => { 11 | const savedKey = localStorage.getItem('youtubeApiKey'); 12 | const savedUseCustom = localStorage.getItem('useCustomKey') === 'true'; 13 | if (savedKey) setCustomApiKey(savedKey); 14 | if (savedUseCustom) setUseCustomKey(savedUseCustom); 15 | }, []); 16 | 17 | const handleToggle = () => { 18 | const newValue = !useCustomKey; 19 | setUseCustomKey(newValue); 20 | localStorage.setItem('useCustomKey', newValue); 21 | onApiKeyChange(newValue ? customApiKey : defaultApiKey); 22 | }; 23 | 24 | const handleSubmitKey = () => { 25 | if (customApiKey.trim()) { 26 | localStorage.setItem('youtubeApiKey', customApiKey); 27 | onApiKeyChange(customApiKey); 28 | toast.success('API Key updated successfully!'); 29 | } else { 30 | toast.error('Please enter a valid API Key'); 31 | } 32 | }; 33 | 34 | return ( 35 |
36 | 51 | 52 | {useCustomKey && ( 53 |
54 | setCustomApiKey(e.target.value)} 59 | className="w-64 p-2 border rounded-md dark:bg-gray-700 dark:border-gray-600 dark:text-white" 60 | /> 61 | 67 | Apply Key 68 | 69 |
70 | )} 71 |
72 | ); 73 | } -------------------------------------------------------------------------------- /src/components/LandingPage.jsx: -------------------------------------------------------------------------------- 1 | import { motion, useMotionTemplate, useMotionValue } from "framer-motion"; 2 | import { Link } from "react-router-dom"; 3 | 4 | // Utils function included directly 5 | function cn(...inputs) { 6 | return inputs.filter(Boolean).join(' '); 7 | } 8 | 9 | export const BackgroundLines = ({ 10 | children, 11 | className, 12 | svgOptions 13 | }) => { 14 | return ( 15 | (
17 | 18 | {children} 19 |
) 20 | ); 21 | }; 22 | 23 | const pathVariants = { 24 | initial: { strokeDashoffset: 800, strokeDasharray: "50 800" }, 25 | animate: { 26 | strokeDashoffset: 0, 27 | strokeDasharray: "20 800", 28 | opacity: [0, 1, 1, 0], 29 | }, 30 | }; 31 | 32 | const SVG = ({ 33 | svgOptions 34 | }) => { 35 | const paths = [ 36 | "M720 450C720 450 742.459 440.315 755.249 425.626C768.039 410.937 778.88 418.741 789.478 401.499C800.076 384.258 817.06 389.269 826.741 380.436C836.423 371.603 851.957 364.826 863.182 356.242C874.408 347.657 877.993 342.678 898.867 333.214C919.741 323.75 923.618 319.88 934.875 310.177C946.133 300.474 960.784 300.837 970.584 287.701C980.384 274.564 993.538 273.334 1004.85 263.087C1016.15 252.84 1026.42 250.801 1038.22 242.1C1050.02 233.399 1065.19 230.418 1074.63 215.721C1084.07 201.024 1085.49 209.128 1112.65 194.884C1139.8 180.64 1132.49 178.205 1146.43 170.636C1160.37 163.066 1168.97 158.613 1181.46 147.982C1193.95 137.35 1191.16 131.382 1217.55 125.645C1243.93 119.907 1234.19 118.899 1254.53 100.846C1274.86 82.7922 1275.12 92.8914 1290.37 76.09C1305.62 59.2886 1313.91 62.1868 1323.19 56.7536C1332.48 51.3204 1347.93 42.8082 1361.95 32.1468C1375.96 21.4855 1374.06 25.168 1397.08 10.1863C1420.09 -4.79534 1421.41 -3.16992 1431.52 -15.0078", 37 | "M720 450C720 450 741.044 435.759 753.062 410.636C765.079 385.514 770.541 386.148 782.73 370.489C794.918 354.83 799.378 353.188 811.338 332.597C823.298 312.005 825.578 306.419 843.707 295.493C861.837 284.568 856.194 273.248 877.376 256.48C898.558 239.713 887.536 227.843 909.648 214.958C931.759 202.073 925.133 188.092 941.063 177.621C956.994 167.151 952.171 154.663 971.197 135.041C990.222 115.418 990.785 109.375 999.488 96.1291C1008.19 82.8827 1011.4 82.2181 1032.65 61.8861C1053.9 41.5541 1045.74 48.0281 1064.01 19.5798C1082.29 -8.86844 1077.21 -3.89415 1093.7 -19.66C1110.18 -35.4258 1105.91 -46.1146 1127.68 -60.2834C1149.46 -74.4523 1144.37 -72.1024 1154.18 -97.6802C1163.99 -123.258 1165.6 -111.332 1186.21 -135.809C1206.81 -160.285 1203.29 -160.861 1220.31 -177.633C1237.33 -194.406 1236.97 -204.408 1250.42 -214.196", 38 | "M720 450C720 450 712.336 437.768 690.248 407.156C668.161 376.544 672.543 394.253 665.951 365.784C659.358 337.316 647.903 347.461 636.929 323.197C625.956 298.933 626.831 303.639 609.939 281.01C593.048 258.381 598.7 255.282 582.342 242.504C565.985 229.726 566.053 217.66 559.169 197.116C552.284 176.572 549.348 171.846 529.347 156.529C509.345 141.211 522.053 134.054 505.192 115.653C488.33 97.2527 482.671 82.5627 473.599 70.7833C464.527 59.0039 464.784 50.2169 447 32.0721C429.215 13.9272 436.29 0.858563 423.534 -12.6868C410.777 -26.2322 407.424 -44.0808 394.364 -56.4916C381.303 -68.9024 373.709 -72.6804 365.591 -96.1992C357.473 -119.718 358.364 -111.509 338.222 -136.495C318.08 -161.481 322.797 -149.499 315.32 -181.761C307.843 -214.023 294.563 -202.561 285.795 -223.25C277.026 -243.94 275.199 -244.055 258.602 -263.871", 39 | "M720 450C720 450 738.983 448.651 790.209 446.852C841.436 445.052 816.31 441.421 861.866 437.296C907.422 433.172 886.273 437.037 930.656 436.651C975.04 436.264 951.399 432.343 1001.57 425.74C1051.73 419.138 1020.72 425.208 1072.85 424.127C1124.97 423.047 1114.39 420.097 1140.02 414.426C1165.65 408.754 1173.1 412.143 1214.55 411.063C1256.01 409.983 1242.78 406.182 1285.56 401.536C1328.35 396.889 1304.66 400.796 1354.41 399.573C1404.16 398.35 1381.34 394.315 1428.34 389.376C1475.35 384.438 1445.96 386.509 1497.93 385.313C1549.9 384.117 1534.63 382.499 1567.23 381.48", 40 | "M720 450C720 450 696.366 458.841 682.407 472.967C668.448 487.093 673.23 487.471 647.919 492.882C622.608 498.293 636.85 499.899 609.016 512.944C581.182 525.989 596.778 528.494 571.937 533.778C547.095 539.062 551.762 548.656 536.862 556.816C521.962 564.975 515.626 563.279 497.589 575.159C479.552 587.04 484.343 590.435 461.111 598.728C437.879 607.021 442.512 605.226 423.603 618.397C404.694 631.569 402.411 629.541 390.805 641.555C379.2 653.568 369.754 658.175 353.238 663.929C336.722 669.683 330.161 674.689 312.831 684.116C295.5 693.543 288.711 698.815 278.229 704.041C267.747 709.267 258.395 712.506 240.378 726.65C222.361 740.795 230.097 738.379 203.447 745.613C176.797 752.847 193.747 752.523 166.401 767.148C139.056 781.774 151.342 783.641 130.156 791.074C108.97 798.507 116.461 802.688 96.0974 808.817C75.7334 814.946 83.8553 819.505 59.4513 830.576C35.0473 841.648 48.2548 847.874 21.8337 853.886C-4.58739 859.898 10.5966 869.102 -16.396 874.524", 41 | "M720 450C720 450 695.644 482.465 682.699 506.197C669.755 529.929 671.059 521.996 643.673 556.974C616.286 591.951 625.698 590.8 606.938 615.255C588.178 639.71 592.715 642.351 569.76 665.92C546.805 689.49 557.014 687.498 538.136 722.318C519.258 757.137 520.671 760.818 503.256 774.428C485.841 788.038 491.288 790.063 463.484 831.358C435.681 872.653 437.554 867.001 425.147 885.248C412.74 903.495 411.451 911.175 389.505 934.331C367.559 957.486 375.779 966.276 352.213 990.918C328.647 1015.56 341.908 1008.07 316.804 1047.24C291.699 1086.42 301.938 1060.92 276.644 1100.23C251.349 1139.54 259.792 1138.78 243.151 1153.64", 42 | "M719.974 450C719.974 450 765.293 459.346 789.305 476.402C813.318 493.459 825.526 487.104 865.093 495.586C904.659 504.068 908.361 510.231 943.918 523.51C979.475 536.789 963.13 535.277 1009.79 547.428C1056.45 559.579 1062.34 555.797 1089.82 568.96C1117.31 582.124 1133.96 582.816 1159.12 592.861C1184.28 602.906 1182.84 603.359 1233.48 614.514C1284.12 625.67 1254.63 632.207 1306.33 644.465C1358.04 656.723 1359.27 656.568 1378.67 670.21C1398.07 683.852 1406.16 676.466 1456.34 692.827C1506.51 709.188 1497.73 708.471 1527.54 715.212", 43 | "M720 450C720 450 727.941 430.821 734.406 379.251C740.87 327.681 742.857 359.402 757.864 309.798C772.871 260.194 761.947 271.093 772.992 244.308C784.036 217.524 777.105 200.533 786.808 175.699C796.511 150.864 797.141 144.333 808.694 107.307C820.247 70.2821 812.404 88.4169 819.202 37.1016C826 -14.2137 829.525 -0.990829 839.341 -30.3874C849.157 -59.784 844.404 -61.5924 855.042 -98.7516C865.68 -135.911 862.018 -144.559 876.924 -167.488C891.83 -190.418 886.075 -213.535 892.87 -237.945C899.664 -262.355 903.01 -255.031 909.701 -305.588C916.393 -356.144 917.232 -330.612 925.531 -374.777", 44 | "M720 450C720 450 722.468 499.363 726.104 520.449C729.739 541.535 730.644 550.025 738.836 589.07C747.028 628.115 743.766 639.319 746.146 659.812C748.526 680.306 754.006 693.598 757.006 732.469C760.007 771.34 760.322 765.244 763.893 805.195C767.465 845.146 769.92 822.227 773.398 868.469C776.875 914.71 776.207 901.365 778.233 940.19C780.259 979.015 782.53 990.477 787.977 1010.39C793.424 1030.3 791.788 1060.01 797.243 1082.24C802.698 1104.47 801.758 1130.29 808.181 1149.64C814.604 1168.99 813.135 1171.5 818.026 1225.28C822.918 1279.06 820.269 1267.92 822.905 1293.75", 45 | "M720 450C720 450 737.033 492.46 757.251 515.772C777.468 539.084 768.146 548.687 785.517 570.846C802.887 593.005 814.782 609.698 824.589 634.112C834.395 658.525 838.791 656.702 855.55 695.611C872.31 734.519 875.197 724.854 890.204 764.253C905.21 803.653 899.844 790.872 919.927 820.763C940.01 850.654 939.071 862.583 954.382 886.946C969.693 911.309 968.683 909.254 993.997 945.221C1019.31 981.187 1006.67 964.436 1023.49 1007.61C1040.32 1050.79 1046.15 1038.25 1059.01 1073.05C1071.88 1107.86 1081.39 1096.19 1089.45 1131.96C1097.51 1167.73 1106.52 1162.12 1125.77 1196.89", 46 | "M720 450C720 450 687.302 455.326 670.489 467.898C653.676 480.47 653.159 476.959 626.58 485.127C600.002 493.295 599.626 495.362 577.94 503.841C556.254 512.319 556.35 507.426 533.958 517.44C511.566 527.454 505.82 526.441 486.464 539.172C467.108 551.904 461.312 546.36 439.357 553.508C417.402 560.657 406.993 567.736 389.393 572.603C371.794 577.47 371.139 583.76 344.54 587.931C317.941 592.102 327.375 593.682 299.411 607.275C271.447 620.868 283.617 615.022 249.868 622.622C216.119 630.223 227.07 630.86 203.77 638.635C180.47 646.41 168.948 652.487 156.407 657.28C143.866 662.073 132.426 669.534 110.894 675.555C89.3615 681.575 90.3234 680.232 61.1669 689.897C32.0105 699.562 34.3696 702.021 15.9011 709.789C-2.56738 717.558 2.38861 719.841 -29.9494 729.462C-62.2873 739.083 -52.5552 738.225 -77.4307 744.286", 47 | "M720 450C720 450 743.97 465.061 754.884 490.648C765.798 516.235 781.032 501.34 791.376 525.115C801.72 548.889 808.417 538.333 829.306 564.807C850.195 591.281 852.336 582.531 865.086 601.843C877.835 621.155 874.512 621.773 902.383 643.857C930.255 665.94 921.885 655.976 938.025 681.74C954.164 707.505 959.384 709.719 977.273 720.525C995.162 731.33 994.233 731.096 1015.92 757.676C1037.61 784.257 1025.74 768.848 1047.82 795.343C1069.91 821.837 1065.95 815.45 1085.93 834.73C1105.91 854.009 1110.53 848.089 1124.97 869.759C1139.4 891.428 1140.57 881.585 1158.53 911.499C1176.5 941.414 1184.96 933.829 1194.53 948.792C1204.09 963.755 1221.35 973.711 1232.08 986.224C1242.8 998.738 1257.34 1015.61 1269.99 1026.53C1282.63 1037.45 1293.81 1040.91 1307.21 1064.56", 48 | "M720 450C720 450 718.24 412.717 716.359 397.31C714.478 381.902 713.988 362.237 710.785 344.829C707.582 327.42 708.407 322.274 701.686 292.106C694.965 261.937 699.926 270.857 694.84 240.765C689.753 210.674 693.055 217.076 689.674 184.902C686.293 152.728 686.041 149.091 682.676 133.657C679.311 118.223 682.23 106.005 681.826 80.8297C681.423 55.6545 677.891 60.196 675.66 30.0226C673.429 -0.150848 672.665 -7.94842 668.592 -26.771C664.52 -45.5935 664.724 -43.0755 661.034 -78.7766C657.343 -114.478 658.509 -103.181 653.867 -133.45C649.226 -163.719 650.748 -150.38 647.052 -182.682C643.357 -214.984 646.125 -214.921 645.216 -238.402C644.307 -261.883 640.872 -253.4 637.237 -291.706C633.602 -330.012 634.146 -309.868 630.717 -343.769C627.288 -377.669 628.008 -370.682 626.514 -394.844", 49 | "M720 450C720 450 730.384 481.55 739.215 507.557C748.047 533.564 751.618 537.619 766.222 562.033C780.825 586.447 774.187 582.307 787.606 618.195C801.025 654.082 793.116 653.536 809.138 678.315C825.16 703.095 815.485 717.073 829.898 735.518C844.311 753.964 845.351 773.196 852.197 786.599C859.042 800.001 862.876 805.65 872.809 845.974C882.742 886.297 885.179 874.677 894.963 903.246C904.747 931.816 911.787 924.243 921.827 961.809C931.867 999.374 927.557 998.784 940.377 1013.59C953.197 1028.4 948.555 1055.77 966.147 1070.54C983.739 1085.31 975.539 1105.69 988.65 1125.69C1001.76 1145.69 1001.82 1141.59 1007.54 1184.37C1013.27 1227.15 1018.98 1198.8 1029.67 1241.58", 50 | "M720 450C720 450 684.591 447.135 657.288 439.014C629.985 430.894 618.318 435.733 600.698 431.723C583.077 427.714 566.975 425.639 537.839 423.315C508.704 420.991 501.987 418.958 476.29 413.658C450.592 408.359 460.205 410.268 416.97 408.927C373.736 407.586 396.443 401.379 359.262 396.612C322.081 391.844 327.081 393.286 300.224 391.917C273.368 390.547 264.902 385.49 241.279 382.114C217.655 378.739 205.497 378.95 181.98 377.253C158.464 375.556 150.084 369.938 117.474 366.078C84.8644 362.218 81.5401 361.501 58.8734 358.545C36.2067 355.59 33.6442 351.938 -3.92281 346.728C-41.4898 341.519 -18.6466 345.082 -61.4654 341.179C-104.284 337.275 -102.32 338.048 -121.821 332.369", 51 | "M720 450C720 450 711.596 475.85 701.025 516.114C690.455 556.378 697.124 559.466 689.441 579.079C681.758 598.693 679.099 597.524 675.382 642.732C671.665 687.94 663.4 677.024 657.844 700.179C652.288 723.333 651.086 724.914 636.904 764.536C622.723 804.158 631.218 802.853 625.414 827.056C619.611 851.259 613.734 856.28 605.94 892.262C598.146 928.244 595.403 924.314 588.884 957.785C582.364 991.255 583.079 991.176 575.561 1022.63C568.044 1054.08 566.807 1058.45 558.142 1084.32C549.476 1110.2 553.961 1129.13 542.367 1149.25C530.772 1169.37 538.268 1180.37 530.338 1207.27C522.407 1234.17 520.826 1245.53 512.156 1274.2", 52 | "M720 450C720 450 730.571 424.312 761.424 411.44C792.277 398.569 772.385 393.283 804.069 377.232C835.752 361.182 829.975 361.373 848.987 342.782C867.999 324.192 877.583 330.096 890.892 303.897C904.201 277.698 910.277 282.253 937.396 264.293C964.514 246.333 949.357 246.834 978.7 230.438C1008.04 214.042 990.424 217.952 1021.51 193.853C1052.6 169.753 1054.28 184.725 1065.97 158.075C1077.65 131.425 1087.76 139.068 1111.12 120.345C1134.49 101.622 1124.9 104.858 1151.67 86.3162C1178.43 67.7741 1167.09 66.2676 1197.53 47.2606C1227.96 28.2536 1225.78 23.2186 1239.27 12.9649C1252.76 2.7112 1269.32 -9.47929 1282.88 -28.5587C1296.44 -47.6381 1305.81 -41.3853 1323.82 -62.7027C1341.83 -84.0202 1340.32 -82.3794 1368.98 -98.9326", 53 | ]; 54 | 55 | const colors = [ 56 | "#46A5CA", 57 | "#8C2F2F", 58 | "#4FAE4D", 59 | "#D6590C", 60 | "#811010", 61 | "#247AFB", 62 | "#A534A0", 63 | "#A8A438", 64 | "#D6590C", 65 | "#46A29C", 66 | "#670F6D", 67 | "#D7C200", 68 | "#59BBEB", 69 | "#504F1C", 70 | "#55BC54", 71 | "#4D3568", 72 | "#9F39A5", 73 | "#363636", 74 | "#860909", 75 | "#6A286F", 76 | "#604483", 77 | ]; 78 | return ( 79 | ( 87 | {paths.map((path, idx) => ( 88 | 105 | ))} 106 | {/* duplicate for more paths */} 107 | {paths.map((path, idx) => ( 108 | 125 | ))} 126 | ) 127 | ); 128 | }; 129 | 130 | export const SpotlightCard = ({ 131 | children, 132 | className, 133 | containerClassName 134 | }) => { 135 | let mouseX = useMotionValue(0); 136 | let mouseY = useMotionValue(0); 137 | 138 | function handleMouseMove({ 139 | currentTarget, 140 | clientX, 141 | clientY 142 | }) { 143 | if (!currentTarget) return; 144 | let { left, top } = currentTarget.getBoundingClientRect(); 145 | 146 | mouseX.set(clientX - left); 147 | mouseY.set(clientY - top); 148 | } 149 | return ( 150 | (
156 |
158 | 176 |
{children}
177 |
) 178 | ); 179 | }; 180 | 181 | export const Highlight = ({ 182 | children, 183 | className 184 | }) => { 185 | return ( 186 | ( 207 | {children} 208 | ) 209 | ); 210 | }; 211 | 212 | export default function LandingPage() { 213 | return ( 214 | 215 |
216 | 222 |

223 | Distraction Free{" "} 224 | 236 | Youtube Player 237 | 238 |

239 | 248 | A curated collection of programming tutorials without the distractions of regular YouTube. 249 | 250 | 251 | 261 | 265 |
266 |
267 | Get Started 268 |
269 | 270 | 271 | 272 |
273 | 274 | ); 275 | } 276 | -------------------------------------------------------------------------------- /src/components/body.jsx: -------------------------------------------------------------------------------- 1 | import { useState, useRef, useEffect } from 'react'; 2 | import ReactPlayer from 'react-player/youtube'; 3 | import { motion } from 'framer-motion'; 4 | import { SiPython, SiReact, SiGraphql, SiJavascript, SiNodedotjs, 5 | SiDeno, SiTypescript, SiDocker, SiCplusplus, SiGo, SiRust, SiPrisma } from 'react-icons/si'; 6 | import { FaPlay, FaPause, FaForward, FaBackward, FaJava } from 'react-icons/fa'; 7 | import { toast, ToastContainer } from 'react-toastify'; 8 | import 'react-toastify/dist/ReactToastify.css'; 9 | import { searchVideos, getPlaylistItems, setApiKey } from '../api/youtube'; 10 | 11 | export default function Body() { 12 | 13 | const [data, setData] = useState(''); 14 | const [searchBoxFlag, setSearchBoxFlag] = useState(false); 15 | const [skills, setSkills] = useState('Select a skill to load tutorial'); 16 | const [playlistResponse, setPlaylistResponse] = useState([]); 17 | const [currentVideo, setCurrentVideo] = useState(''); 18 | const [title, setTitle] = useState('Search a video or select from the playlist to begin'); 19 | const [description, setDescription] = useState('Select a video'); 20 | const [isDescriptionVisible, setIsDescriptionVisible] = useState(false); 21 | const playerRef = useRef(); 22 | const [isPlaying, setIsPlaying] = useState(false); 23 | const [startTime, setStartTime] = useState(0); // Track video start time for resuming 24 | const [isLoading, setIsLoading] = useState(false); 25 | const [error, setError] = useState(null); 26 | 27 | useEffect(() => { 28 | // Retrieve saved video ID, time, search query, and playlist from localStorage on load 29 | const savedVideoId = localStorage.getItem('savedVideoId'); 30 | const savedTime = parseFloat(localStorage.getItem('savedTime')) || 0; 31 | const savedSearchQuery = localStorage.getItem('savedSearchQuery'); 32 | const savedPlaylist = JSON.parse(localStorage.getItem('savedPlaylist') || '[]'); 33 | 34 | if (savedVideoId) { 35 | setCurrentVideo(`https://www.youtube.com/watch?v=${savedVideoId}`); 36 | setStartTime(savedTime); 37 | } 38 | 39 | if (savedSearchQuery) { 40 | setData(savedSearchQuery); // Restore search term in the input 41 | } 42 | 43 | if (savedPlaylist.length) { 44 | setPlaylistResponse(savedPlaylist); // Restore playlist 45 | } 46 | }, []); 47 | 48 | function getData(val) { 49 | const query = val.target.value; 50 | setData(query); 51 | localStorage.setItem('savedSearchQuery', query); // Save search term to localStorage 52 | } 53 | function submitData() { 54 | if (data === '') { 55 | toast.warning("Please enter a search term!"); 56 | return; 57 | } 58 | 59 | setSearchBoxFlag(true); 60 | searchVideos(data) 61 | .then((response) => { 62 | setPlaylistResponse(response.data.items); 63 | localStorage.setItem('savedPlaylist', JSON.stringify(response.data.items)); 64 | }) 65 | .catch((error) => { 66 | console.error('API Error:', error); 67 | 68 | let errorMessage = "An unexpected error occurred."; 69 | 70 | if (error.response) { 71 | // Handle specific error codes 72 | switch (error.response.status) { 73 | case 403: 74 | errorMessage = "API quota exceeded. Please try again later or use your own YouTube API key."; 75 | break; 76 | case 400: 77 | errorMessage = "Invalid request. Please check your search term."; 78 | break; 79 | case 429: 80 | errorMessage = "Too many requests. Please wait a moment and try again."; 81 | break; 82 | default: 83 | errorMessage = `Error: ${error.response.data.error?.message || "Something went wrong"}`; 84 | } 85 | } else if (error.request) { 86 | errorMessage = "Network error. Please check your internet connection."; 87 | } 88 | 89 | // Show error toast 90 | toast.error(
Error{errorMessage}
, { 91 | position: "top-center", 92 | autoClose: 8000, 93 | hideProgressBar: false, 94 | closeOnClick: true, 95 | pauseOnHover: true, 96 | draggable: true, 97 | }); 98 | 99 | // Clear the playlist if there's an error 100 | setPlaylistResponse([]); 101 | setSearchBoxFlag(false); 102 | }); 103 | } 104 | 105 | const playlistIds = { 106 | python: "PLEiEAq2VkUUJO27b6PyoSd7CJjWIPyHYO", 107 | reactjs: "PLSsAz5wf2lkKm0BG9wUWWSgYWBzDa-dFs", 108 | reactnative: "PLRAV69dS1uWSjBBJ-egNNOd4mdblt1P4c", 109 | graphql: "PL4cUxeGkcC9iK6Qhn-QLcXCXPQUov1U7f", 110 | javascript: "PL4cUxeGkcC9i9Ae2D9Ee1RvylH38dKuET", 111 | nodejs: "PL_cUvD4qzbkwjmjy-KjbieZ8J9cGwxZpC", 112 | deno: "PL4cUxeGkcC9gnaJdxuGvEGYQ9iHb8mxsh", 113 | typescript: "PL4cUxeGkcC9gUgr39Q_yD6v-bSyMwKPUI", 114 | docker: "PLhW3qG5bs-L99pQsZ74f-LC-tOEsBp2rK", 115 | "c++": "PLVlQHNRLflP8_DGKcMoRw-TYJJALgGu4J", 116 | java: "PL9gnSGHSqcnr_DxHsP7AW9ftq0AtAyYqJ", 117 | golang: "PL4cUxeGkcC9gC88BEo9czgyS72A3doDeM", 118 | rust: "PLai5B987bZ9CoVR-QEIN9foz4QCJ0H2Y8", 119 | prisma: "PLsvvBhdpMqBwrKGF8TytiLlcCT3gB_bmW" 120 | }; 121 | 122 | function submitSkillButton(skill) { 123 | setSkills(skill); 124 | setSearchBoxFlag(false); 125 | setIsLoading(true); 126 | setError(null); 127 | 128 | const playlistId = playlistIds[skill]; 129 | 130 | if (!playlistId) { 131 | setError(`No playlist found for ${skill}`); 132 | setIsLoading(false); 133 | return; 134 | } 135 | 136 | getPlaylistItems(playlistId) 137 | .then((response) => { 138 | setPlaylistResponse(response.data.items); 139 | localStorage.setItem('savedPlaylist', JSON.stringify(response.data.items)); 140 | }) 141 | .catch((error) => { 142 | console.error('Playlist API Error:', error); 143 | let errorMessage = "Failed to load playlist."; 144 | 145 | if (error.response?.status === 403) { 146 | errorMessage = "API quota exceeded. Please try again later or use your own YouTube API key."; 147 | } else if (error.response?.status === 404) { 148 | errorMessage = `Playlist for ${skill} not found or is private.`; 149 | } 150 | 151 | setError(errorMessage); 152 | toast.error(errorMessage, { 153 | position: "top-center", 154 | autoClose: 5000, 155 | }); 156 | }) 157 | .finally(() => { 158 | setIsLoading(false); 159 | }); 160 | } 161 | 162 | function nextVideo(video, title, description) { 163 | let videoId = video.id?.videoId || video.snippet?.resourceId?.videoId; 164 | 165 | if (!videoId) { 166 | console.error('Video ID not found'); 167 | return; 168 | } 169 | 170 | setTitle(title); 171 | setDescription(description); 172 | setCurrentVideo(`https://www.youtube.com/watch?v=${videoId}`); 173 | setStartTime(0); // Reset start time for new video 174 | setIsDescriptionVisible(true); 175 | 176 | // Save current video ID to localStorage 177 | localStorage.setItem('savedVideoId', videoId); 178 | } 179 | 180 | const handleProgress = (progress) => { 181 | // Save current time to localStorage 182 | if (progress.playedSeconds > 0) { 183 | localStorage.setItem('savedTime', progress.playedSeconds); 184 | } 185 | }; 186 | 187 | const skillIcons = [ 188 | { icon: SiPython, name: 'python', color: '#3776AB' }, 189 | { icon: SiJavascript, name: 'javascript', color: '#F7DF1E' }, 190 | { icon: SiReact, name: 'reactjs', color: '#61DAFB' }, 191 | { icon: SiNodedotjs, name: 'nodejs', color: '#339933' }, 192 | { icon: SiReact, name: 'reactnative', color: '#61DAFB' }, 193 | { icon: SiTypescript, name: 'typescript', color: '#3178C6' }, 194 | { icon: SiCplusplus, name: 'c++', color: '#00599C' }, 195 | { icon: FaJava, name: 'java', color: '#007396' }, 196 | { icon: SiGo, name: 'golang', color: '#00ADD8' }, 197 | { icon: SiRust, name: 'rust', color: '#c7c3c3' }, 198 | { icon: SiDeno, name: 'deno', color: '#c7c3c3' }, 199 | { icon: SiGraphql, name: 'graphql', color: '#E535AB' }, 200 | { icon: SiPrisma, name: 'prisma', color: '#336791' }, 201 | { icon: SiDocker, name: 'docker', color: '#2496ED' }, 202 | 203 | ]; 204 | 205 | const linkify = (text) => { 206 | const urlRegex = /(https?:\/\/[^\s]+)/g; 207 | return text.split('\n').map((line, lineIndex) => ( 208 |

209 | {line.split(urlRegex).map((part, index) => { 210 | if (part.match(urlRegex)) { 211 | return ( 212 | 219 | {part} 220 | 221 | ); 222 | } 223 | return part; 224 | })} 225 |

226 | )); 227 | }; 228 | 229 | const handleApiKeyChange = (newKey) => { 230 | setApiKey(newKey); 231 | }; 232 | 233 | return ( 234 | 240 | 241 | {/* Header Section */} 242 | 248 | {/*

249 | Welcome To Distraction-Free YouTube Learning Experience 👨‍💻 250 |

*/} 251 |

252 | What Do You Want To Learn Today? 253 |

254 |
255 | 256 | {/* Skills Grid */} 257 | 263 | {skillIcons.map((skill, index) => ( 264 | submitSkillButton(skill.name)} 269 | className="flex flex-col items-center justify-center cursor-pointer" 270 | > 271 | 272 | 273 | {skill.name} 274 | 275 | 276 | ))} 277 | 278 | 279 | {/* Search and API Key Section */} 280 | 286 | e.key === 'Enter' && submitData()} 292 | className="w-full p-4 pr-12 rounded-lg border border-gray-200 dark:border-gray-700 293 | bg-white dark:bg-gray-800 text-gray-900 dark:text-white 294 | focus:ring-2 focus:ring-indigo-500 focus:border-transparent" 295 | /> 296 | 305 | Search 306 | 307 | 308 | 309 | {/* Video and Playlist Section */} 310 |
311 | {/* Video Player */} 312 | 318 |
319 |
320 | setIsPlaying(true)} 328 | onPause={() => setIsPlaying(false)} 329 | onProgress={handleProgress} 330 | onStart={() => playerRef.current.seekTo(startTime)} 331 | className="absolute top-0 left-0" 332 | /> 333 |
334 |
335 | 336 |
337 |

338 | {title} 339 |

340 | 341 | {/* Control Buttons */} 342 |
343 | {/* Backward 10s */} 344 | playerRef.current.seekTo(playerRef.current.getCurrentTime() - 10)} 348 | className="relative inline-flex h-12 w-12 overflow-hidden rounded-full p-[1px] focus:outline-none focus:ring-2 focus:ring-slate-400 focus:ring-offset-2 focus:ring-offset-slate-50" 349 | > 350 | 351 | 352 | 353 | 354 | 355 | 356 | {/* Play/Pause */} 357 | setIsPlaying(!isPlaying)} 361 | className="relative inline-flex h-16 w-16 overflow-hidden rounded-full p-[1px] focus:outline-none focus:ring-2 focus:ring-slate-400 focus:ring-offset-2 focus:ring-offset-slate-50" 362 | > 363 | 364 | 365 | {isPlaying ? ( 366 | 367 | ) : ( 368 | 369 | )} 370 | 371 | 372 | 373 | {/* Forward 10s */} 374 | playerRef.current.seekTo(playerRef.current.getCurrentTime() + 10)} 378 | className="relative inline-flex h-12 w-12 overflow-hidden rounded-full p-[1px] focus:outline-none focus:ring-2 focus:ring-slate-400 focus:ring-offset-2 focus:ring-offset-slate-50" 379 | > 380 | 381 | 382 | 383 | 384 | 385 |
386 | 387 | {/* Description Toggle Button */} 388 | setIsDescriptionVisible(!isDescriptionVisible)} 392 | className="relative inline-flex w-full h-12 overflow-hidden rounded-full p-[1px] focus:outline-none focus:ring-2 focus:ring-slate-400 focus:ring-offset-2 focus:ring-offset-slate-50" 393 | > 394 | 395 | 396 | {isDescriptionVisible ? 'Hide Description' : 'Show Description'} 397 | 398 | 399 | 400 | {isDescriptionVisible && ( 401 | 406 | {linkify(description)} 407 | 408 | )} 409 |
410 |
411 | 412 | {/* Playlist */} 413 | 419 |
420 |

Playlist

421 |

422 | Total videos: {playlistResponse.length} 423 |

424 |
425 | 426 | {isLoading ? ( 427 |
428 |
429 |
430 | ) : error ? ( 431 |
432 | {error} 433 |
434 | ) : ( 435 |
436 | {playlistResponse.map((video, idx) => ( 437 | nextVideo(video, video.snippet.title, video.snippet.description)} 442 | > 443 |
444 | {video.snippet.title} 449 |

450 | {video.snippet.title} 451 |

452 |
453 |
454 | ))} 455 |
456 | )} 457 |
458 |
459 |
460 | ); 461 | }; -------------------------------------------------------------------------------- /src/components/footer.jsx: -------------------------------------------------------------------------------- 1 | // Footer.js 2 | import { useState } from 'react'; 3 | import { FaGithub, FaKeyboard, FaChevronDown } from 'react-icons/fa'; 4 | import { motion, AnimatePresence } from 'framer-motion'; 5 | import { SiYoutube } from 'react-icons/si'; 6 | import { BsStars } from 'react-icons/bs'; 7 | 8 | export default function Footer() { 9 | const [isInstructionsOpen, setIsInstructionsOpen] = useState(false); 10 | 11 | return ( 12 |
13 |
14 | {/* Main Content */} 15 |
16 | {/* Platform Features */} 17 |
18 |

19 | Platform Features 20 |

21 |
22 |
23 | 24 | Ad-Free Experience 25 |
26 |
27 | 28 | Curated Content 29 |
30 |
31 | 32 | Open Source 33 |
34 |
35 | 36 | Distraction-Free 37 |
38 |
39 | 40 | Multiple Playlists 41 |
42 |
43 | 44 | Saved Progress 45 |
46 |
47 |
48 | 49 | {/* Keyboard Shortcuts */} 50 |
51 |

52 | 53 | Keyboard Shortcuts 54 |

55 |
56 |
57 | Play/Pause 58 | Space 59 |
60 |
61 | Forward 10s 62 | 63 |
64 |
65 | Backward 10s 66 | 67 |
68 |
69 | Toggle Fullscreen 70 | F 71 |
72 |
73 |
74 | 75 | {/* GitHub Section with API Instructions */} 76 |
77 | 85 | 86 | Source Code 87 | 88 | 89 | {/* API Key Instructions Dropdown */} 90 |
91 | 102 | 103 | {isInstructionsOpen && ( 104 | 111 |
112 |
113 |

Step 1: Create a Google Cloud Project

114 |
    115 |
  • Go to the Google Cloud Console
  • 116 |
  • Sign in with your Google account
  • 117 |
  • Click on the Select a project dropdown (top left)
  • 118 |
  • Click New Project → Give it a name → Click Create
  • 119 |
120 |
121 |
122 |

Step 2: Enable YouTube Data API v3

123 |
    124 |
  • In the Google Cloud Console, go to API & Services
  • 125 |
  • Click + ENABLE APIS AND SERVICES (top)
  • 126 |
  • Search for YouTube Data API v3
  • 127 |
  • Click on it, then click Enable
  • 128 |
129 |
130 |
131 |

Step 3: Get an API Key

132 |
    133 |
  • Go to Credentials (left sidebar)
  • 134 |
  • Click Create Credentials → Select API Key
  • 135 |
  • Google will generate an API key. Click Copy and Save it
  • 136 |
137 |
138 |
139 |
140 | )} 141 |
142 |
143 |
144 |
145 | 146 | {/* Credits Bar */} 147 |
148 |
149 |
150 |
151 | {"<"}YouTubePlayer {"/>"} 152 |
153 |
154 | 155 | Developed by 156 | Salman Qureshi 158 | 159 | Redesigned by 160 |
161 | Zaid Adil 163 | 164 |
165 |
166 |
167 |
168 |
169 |
170 |
171 | ); 172 | } -------------------------------------------------------------------------------- /src/components/header.jsx: -------------------------------------------------------------------------------- 1 | // frontend/src/components/header.js 2 | 3 | import { useState } from "react"; 4 | import { motion, AnimatePresence } from "framer-motion"; 5 | import { Link } from "react-router-dom"; 6 | import { SiYoutube } from "react-icons/si"; 7 | import { FaGithub } from "react-icons/fa"; 8 | import { toast } from 'react-toastify'; 9 | import { FiInfo } from 'react-icons/fi'; 10 | 11 | export default function Header({ onApiKeyChange }) { 12 | const [isDialogOpen, setIsDialogOpen] = useState(false); 13 | const [customApiKey, setCustomApiKey] = useState(localStorage.getItem('youtubeApiKey') || ''); 14 | const [useCustomKey, setUseCustomKey] = useState(localStorage.getItem('useCustomKey') === 'true'); 15 | const defaultApiKey = import.meta.env.VITE_YOUTUBE_API_KEY; 16 | 17 | const handleSubmitKey = () => { 18 | if (customApiKey.trim()) { 19 | localStorage.setItem('youtubeApiKey', customApiKey); 20 | localStorage.setItem('useCustomKey', 'true'); 21 | setUseCustomKey(true); 22 | onApiKeyChange(customApiKey); 23 | setIsDialogOpen(false); 24 | toast.success('Custom API Key activated!'); 25 | } else { 26 | toast.error('Please enter a valid API Key'); 27 | } 28 | }; 29 | 30 | const toggleApiKey = () => { 31 | if (useCustomKey) { 32 | localStorage.setItem('useCustomKey', 'false'); 33 | setUseCustomKey(false); 34 | onApiKeyChange(defaultApiKey); 35 | toast.info('Switched to default API Key'); 36 | } else { 37 | setIsDialogOpen(true); 38 | } 39 | }; 40 | 41 | return ( 42 | <> 43 | 49 |
50 |
51 | {/* Logo Section */} 52 | 53 | 63 | {"<"}YouTubePlayer {"/>"} 64 | 65 | 66 | 67 | {/* Buttons Section */} 68 |
69 | {/* API Key Button with Tooltip */} 70 |
71 | 81 | {useCustomKey ? 'Custom API Active' : 'Set Custom API'} 82 | 83 |
84 | 85 | Your API key is stored locally and never sent to our servers 86 |
87 |
88 |
89 | 90 | {/* GitHub Button */} 91 | 98 | 99 | Star on GitHub 100 | 101 |
102 |
103 |
104 |
105 | 106 | {/* Dialog */} 107 | 108 | {isDialogOpen && ( 109 | <> 110 | {/* Backdrop */} 111 | setIsDialogOpen(false)} 116 | className="fixed inset-0 bg-black z-40" 117 | /> 118 | 119 | {/* Dialog Content */} 120 | 126 |

127 | Custom YouTube API Key 128 |

129 |

130 | 131 | Your API key is stored locally in your browser and never sent to our servers. 132 |

133 | setCustomApiKey(e.target.value)} 138 | className="w-full p-3 mb-4 border rounded-lg dark:bg-gray-700 dark:border-gray-600 dark:text-white" 139 | /> 140 |
141 | setIsDialogOpen(false)} 145 | className="px-4 py-2 rounded-md bg-gray-200 dark:bg-gray-700 text-gray-800 dark:text-white font-medium" 146 | > 147 | Cancel 148 | 149 | 155 | Apply Key 156 | 157 |
158 |
159 | 160 | )} 161 |
162 | 163 | ); 164 | } -------------------------------------------------------------------------------- /src/index.css: -------------------------------------------------------------------------------- 1 | @import "tailwindcss"; 2 | 3 | /* Custom Scrollbar Styles */ 4 | ::-webkit-scrollbar { 5 | width: 10px; 6 | } 7 | 8 | ::-webkit-scrollbar-track { 9 | background: #1a1a1a; 10 | border-radius: 5px; 11 | } 12 | 13 | ::-webkit-scrollbar-thumb { 14 | background: linear-gradient(to bottom, #6366f1, #a855f7); 15 | border-radius: 5px; 16 | border: 2px solid #1a1a1a; 17 | } 18 | 19 | ::-webkit-scrollbar-thumb:hover { 20 | background: linear-gradient(to bottom, #4f46e5, #9333ea); 21 | } 22 | 23 | /* Playlist Specific Scrollbar */ 24 | .playlist-scroll::-webkit-scrollbar { 25 | width: 8px; 26 | } 27 | 28 | .playlist-scroll::-webkit-scrollbar-track { 29 | background: #1a1a1a; 30 | border-radius: 4px; 31 | } 32 | 33 | .playlist-scroll::-webkit-scrollbar-thumb { 34 | background: linear-gradient(to bottom, #6366f1, #a855f7); 35 | border-radius: 4px; 36 | border: 2px solid #1a1a1a; 37 | } -------------------------------------------------------------------------------- /src/logo.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/main.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom/client'; 3 | import App from './App.jsx'; 4 | import './index.css'; 5 | 6 | ReactDOM.createRoot(document.getElementById('root')).render( 7 | 8 | 9 | 10 | ); -------------------------------------------------------------------------------- /vercel.json: -------------------------------------------------------------------------------- 1 | { 2 | "rewrites": [ 3 | { 4 | "source": "/(.*)", 5 | "destination": "/index.html" 6 | } 7 | ] 8 | } -------------------------------------------------------------------------------- /vite.config.js: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite' 2 | import react from '@vitejs/plugin-react' 3 | import tailwindcss from '@tailwindcss/vite' 4 | 5 | export default defineConfig({ 6 | plugins: [ 7 | react(), 8 | tailwindcss() 9 | ] 10 | }) --------------------------------------------------------------------------------