├── .eslintrc.json ├── .github ├── FUNDING.yml ├── ISSUE_TEMPLATE │ ├── bug.yml │ ├── config.yml │ ├── docs.yml │ ├── feature.yml │ └── other.yml └── PULL_REQUEST_TEMPLATE.md ├── .gitignore ├── .husky └── pre-commit ├── .prettierignore ├── .prettierrc ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── package-lock.json ├── package.json ├── public ├── android-chrome-192x192.png ├── android-chrome-512x512.png ├── apple-touch-icon.png ├── browserconfig.xml ├── favicon-16x16.png ├── favicon-32x32.png ├── favicon.ico ├── index.html ├── mstile-150x150.png ├── safari-pinned-tab.svg └── site.webmanifest ├── src ├── App.js ├── App.test.js ├── components │ ├── Editor.js │ ├── Embed.js │ ├── EmbedEditable.js │ ├── LinkCard.js │ ├── LinkCardEditable.js │ ├── Preview.js │ ├── ProfileCard.js │ ├── SocialIconCard.js │ ├── appearance.js │ ├── commons │ │ ├── inputField.js │ │ ├── inputFieldSimple.js │ │ └── toast.js │ ├── embedModal.js │ ├── header.js │ ├── page.js │ └── staticPage.js ├── constants │ └── embed.js ├── context │ ├── adminContext.js │ └── darkModeContext.js ├── hooks │ └── code.js ├── index.css ├── index.js ├── reducers │ └── adminReducer.js ├── reportWebVitals.js └── setupTests.js └── tailwind.config.js /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "browser": true, 4 | "es2021": true 5 | }, 6 | "extends": [ 7 | "plugin:react/recommended", 8 | "standard", 9 | "plugin:prettier/recommended" 10 | ], 11 | "parserOptions": { 12 | "ecmaFeatures": { 13 | "jsx": true 14 | }, 15 | "ecmaVersion": "latest", 16 | "sourceType": "module" 17 | }, 18 | "plugins": ["react"], 19 | "rules": { 20 | "react/react-in-jsx-scope": 0, 21 | "prettier/prettier": [ 22 | "error", 23 | { 24 | "endOfLine": "auto", 25 | "semi": true, 26 | "tabWidth": 2, 27 | "singleQuote": false, 28 | "trailingComma": "all" 29 | } 30 | ] 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] 4 | patreon: # Replace with a single Patreon username 5 | open_collective: # Replace with a single Open Collective username 6 | ko_fi: nathgoutam93 7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 9 | liberapay: # Replace with a single Liberapay username 10 | issuehunt: # Replace with a single IssueHunt username 11 | otechie: # Replace with a single Otechie username 12 | lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry 13 | custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] 14 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug.yml: -------------------------------------------------------------------------------- 1 | name: 🐛 Bug 2 | description: Report an issue to help improve the project. 3 | labels: ["bug"] 4 | body: 5 | - type: textarea 6 | id: description 7 | attributes: 8 | label: Description 9 | description: A brief description of the issue or question 10 | validations: 11 | required: true 12 | - type: textarea 13 | id: screenshots 14 | attributes: 15 | label: Screenshots 16 | description: Please add screenshots if applicable 17 | validations: 18 | required: false 19 | - type: textarea 20 | id: extrainfo 21 | attributes: 22 | label: Additional information 23 | description: Is there anything else we should know about this bug? 24 | validations: 25 | required: false 26 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | blank_issues_enabled: false 2 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/docs.yml: -------------------------------------------------------------------------------- 1 | name: 📄 Documentation issue 2 | description: Found an issue in the documentation? You can use this one! 3 | title: "[DOCS] " 4 | labels: ["documentation"] 5 | body: 6 | - type: textarea 7 | id: description 8 | attributes: 9 | label: Description 10 | description: A brief description of the issue. 11 | validations: 12 | required: true 13 | - type: textarea 14 | id: screenshots 15 | attributes: 16 | label: Screenshots 17 | description: Please add screenshots if applicable 18 | validations: 19 | required: false 20 | - type: textarea 21 | id: extrainfo 22 | attributes: 23 | label: Additional information 24 | description: Is there anything else we should know about this issue? 25 | validations: 26 | required: false 27 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature.yml: -------------------------------------------------------------------------------- 1 | name: 💡 Feature Request 2 | description: Have a new idea/feature? make a feature request. 3 | title: "[FEATURE] " 4 | labels: ["enhancement"] 5 | body: 6 | - type: textarea 7 | id: description 8 | attributes: 9 | label: Description 10 | description: A brief description of the enhancement you propose. 11 | validations: 12 | required: true 13 | - type: textarea 14 | id: screenshots 15 | attributes: 16 | label: Screenshots 17 | description: Please add screenshots if applicable 18 | validations: 19 | required: false 20 | - type: textarea 21 | id: extrainfo 22 | attributes: 23 | label: Additional information 24 | description: Is there anything else we should know about this idea? 25 | validations: 26 | required: false 27 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/other.yml: -------------------------------------------------------------------------------- 1 | name: Other 2 | description: Use this for any other issues. 3 | title: "[OTHER]" 4 | labels: ["🚦 status: awaiting triage"] 5 | body: 6 | - type: markdown 7 | attributes: 8 | value: "# Other issue" 9 | - type: textarea 10 | id: issuedescription 11 | attributes: 12 | label: What would you like to share? 13 | description: Provide a clear and concise explanation of your issue. 14 | validations: 15 | required: true 16 | - type: textarea 17 | id: extrainfo 18 | attributes: 19 | label: Additional information 20 | description: Is there anything else we should know about this issue? 21 | validations: 22 | required: false 23 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | ## Fixes Issue 2 | 3 | 4 | 5 | 6 | 7 | ## Changes proposed 8 | 9 | 10 | 11 | ## Screenshots 12 | 13 | 14 | 15 | # before changes being made 16 | 17 | 18 | 19 | # after changes being made 20 | 21 | 22 | 23 | ## Note to reviewers 24 | 25 | 26 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # production 12 | /build 13 | 14 | # misc 15 | .DS_Store 16 | .env 17 | .env.local 18 | .env.development.local 19 | .env.test.local 20 | .env.production.local 21 | 22 | npm-debug.log* 23 | yarn-debug.log* 24 | yarn-error.log* -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . "$(dirname "$0")/_/husky.sh" 3 | 4 | npx lint-staged -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | package*.json -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "semi": true, 3 | "tabWidth": 2, 4 | "singleQuote": false, 5 | "trailingComma": "all", 6 | "endOfLine": "auto" 7 | } -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # 👨‍🔧How to Contribute 2 | 3 | - Take a look at the existing [Issues](https://github.com/nathgoutam93/linkgen-react/issues) or [create a new issue](https://github.com/nathgoutam93/linkgen-react/issues/new/choose)! 4 | - [Fork](https://github.com/nathgoutam93/linkgen-react/fork) the Repo and install the dependencies. 5 | - create a new branch for any issue that you might willing to work on. 6 | - make your valuable changes and finally, commit your work. 7 | - Create a **[Pull Request](https://github.com/nathgoutam93/linkgen-react/compare)** (_PR_), which will be promptly reviewed and given suggestions for improvements if needed. 8 | - Adding screenshots or screen captures to your Pull Request is a +1. 9 | 10 | # 🤷‍♂️HOW TO INSTALL 11 | 12 | ## Prerequisites 13 | 14 | Before installation, please make sure you have already installed the following tools: 15 | 16 | - [Git](https://git-scm.com/downloads) 17 | - [NodeJs](https://nodejs.org/en/download/) 18 | 19 | ## Installation 20 | 21 | 👉 Start by making a [fork](https://github.com/nathgoutam93/linkgen-react/fork) of the repository. 22 | 23 | 👉 Clone your new fork of the repository: 24 | 25 | ```bash 26 | git clone https://github.com//linkgen-react 27 | ``` 28 | 29 | 👉 Navigate to the new project directory: 30 | 31 | ```bash 32 | cd linkgen-react 33 | ``` 34 | 35 | 👉 install dependencies 36 | 37 | ```bash 38 | npm install 39 | ``` 40 | 41 | 👉 Run 42 | 43 | ```bash 44 | npm start 45 | ``` 46 | 47 | ## 🤷‍♀️HOW TO MAKE A PULL REQUEST: 48 | 49 | Follow the above Installation setup, then 50 | 51 | 👉 Set upstream command: 52 | 53 | ```bash 54 | git remote add upstream https://github.com/nathgoutam93/linkgen-react.git 55 | ``` 56 | 57 | 👉 Create a new branch: 58 | 59 | ```bash 60 | git checkout -b YourBranchName 61 | ``` 62 | 63 | 👉 Sync your fork or your local repository with the origin repository: 64 | 65 | - In your forked repository, click on "Fetch upstream" 66 | - Click "Fetch and merge" 67 | 68 | 😎 Alternatively, Git CLI way to Sync forked repository with origin repository: 69 | 70 | ```bash 71 | git fetch upstream 72 | 73 | git merge upstream/main 74 | ``` 75 | 76 | 📃 [Github Docs](https://docs.github.com/en/github/collaborating-with-pull-requests/addressing-merge-conflicts/resolving-a-merge-conflict-on-github) for Syncing 77 | 78 | 👉 Make your changes to the source code. 79 | 80 | 👉 Stage your changes and commit: 81 | 82 | ```bash 83 | git add . 84 | git commit -m "" 85 | ``` 86 | 87 | 👉 Push your local commits to the remote repository: 88 | 89 | ```bash 90 | git push origin YourBranchName 91 | ``` 92 | 93 | 👉 Create a [Pull Request](https://help.github.com/en/github/collaborating-with-issues-and-pull-requests/creating-a-pull-request)! 94 | 95 | 🎉🎊 **Congratulations!** You've made your first contribution to [**Linkgen**](https://github.com/nathgoutam93/linkgen-react/graphs/contributors)! 🙌 96 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Goutam Nath 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Linkgen 2 | 3 | ### Static 'Link in Bio' generator 4 | 5 | List all of your online links, style them and generate a single HTML file and host it on your own. 6 | 7 | ![Linkgen preview](https://user-images.githubusercontent.com/91387097/155099294-633d6e33-8519-4365-aa36-95780ad5d88a.gif) 8 | 9 | Live site: [Link-gen](https://link-gen.netlify.app/) 10 | 11 | ## ✨ Contributing 12 | 13 | - Contributions are **greatly appreciated**. 14 | - Check out [contribution guidelines](/CONTRIBUTING.md) for more information 15 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "linkgen", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "prettier": "^2.5.1", 7 | "react": "^17.0.2", 8 | "react-beautiful-dnd": "^13.1.0", 9 | "react-colorful": "^5.5.1", 10 | "react-dom": "^17.0.2", 11 | "react-icons": "^4.3.1", 12 | "react-router-dom": "^6.2.1", 13 | "react-scripts": "5.0.0" 14 | }, 15 | "scripts": { 16 | "start": "react-scripts start", 17 | "build": "react-scripts build", 18 | "test": "react-scripts test", 19 | "eject": "react-scripts eject", 20 | "lint": "eslint ./", 21 | "lint-fix": "eslint ./ --fix", 22 | "format": "prettier --write \"**/*.{js,jsx,json,md}\"" 23 | }, 24 | "eslintConfig": { 25 | "extends": [ 26 | "react-app", 27 | "react-app/jest" 28 | ] 29 | }, 30 | "browserslist": { 31 | "production": [ 32 | ">0.2%", 33 | "not dead", 34 | "not op_mini all" 35 | ], 36 | "development": [ 37 | "last 1 chrome version", 38 | "last 1 firefox version", 39 | "last 1 safari version" 40 | ] 41 | }, 42 | "devDependencies": { 43 | "autoprefixer": "^10.4.2", 44 | "eslint": "^7.32.0", 45 | "eslint-config-prettier": "^8.3.0", 46 | "eslint-config-standard": "^16.0.3", 47 | "eslint-plugin-import": "^2.25.4", 48 | "eslint-plugin-node": "^11.1.0", 49 | "eslint-plugin-prettier": "^4.0.0", 50 | "eslint-plugin-promise": "^5.2.0", 51 | "eslint-plugin-react": "^7.28.0", 52 | "husky": "^7.0.4", 53 | "lint-staged": "^12.3.4", 54 | "postcss": "^8.4.6", 55 | "tailwindcss": "^3.0.18" 56 | }, 57 | "lint-staged": { 58 | "*.{js,css,md,json}": [ 59 | "prettier --write" 60 | ], 61 | "*.js": "eslint" 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /public/android-chrome-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nathgoutam93/linkgen-react/e0fc5264e6b84f49a75cf9bbb3b9632c7a933c4a/public/android-chrome-192x192.png -------------------------------------------------------------------------------- /public/android-chrome-512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nathgoutam93/linkgen-react/e0fc5264e6b84f49a75cf9bbb3b9632c7a933c4a/public/android-chrome-512x512.png -------------------------------------------------------------------------------- /public/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nathgoutam93/linkgen-react/e0fc5264e6b84f49a75cf9bbb3b9632c7a933c4a/public/apple-touch-icon.png -------------------------------------------------------------------------------- /public/browserconfig.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | #ffffff 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /public/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nathgoutam93/linkgen-react/e0fc5264e6b84f49a75cf9bbb3b9632c7a933c4a/public/favicon-16x16.png -------------------------------------------------------------------------------- /public/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nathgoutam93/linkgen-react/e0fc5264e6b84f49a75cf9bbb3b9632c7a933c4a/public/favicon-32x32.png -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nathgoutam93/linkgen-react/e0fc5264e6b84f49a75cf9bbb3b9632c7a933c4a/public/favicon.ico -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | Linkgen 19 | 20 | 21 | 22 |
23 | 24 | 25 | -------------------------------------------------------------------------------- /public/mstile-150x150.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nathgoutam93/linkgen-react/e0fc5264e6b84f49a75cf9bbb3b9632c7a933c4a/public/mstile-150x150.png -------------------------------------------------------------------------------- /public/safari-pinned-tab.svg: -------------------------------------------------------------------------------- 1 | 2 | 4 | 7 | 8 | Created by potrace 1.14, written by Peter Selinger 2001-2017 9 | 10 | 12 | 40 | 66 | 81 | 82 | 83 | -------------------------------------------------------------------------------- /public/site.webmanifest: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Link Pile", 3 | "short_name": "LP", 4 | "icons": [ 5 | { 6 | "src": "/android-chrome-192x192.png", 7 | "sizes": "192x192", 8 | "type": "image/png" 9 | }, 10 | { 11 | "src": "/android-chrome-512x512.png", 12 | "sizes": "512x512", 13 | "type": "image/png" 14 | } 15 | ], 16 | "theme_color": "#ffffff", 17 | "background_color": "#ffffff", 18 | "display": "standalone" 19 | } 20 | -------------------------------------------------------------------------------- /src/App.js: -------------------------------------------------------------------------------- 1 | import { useState } from "react"; 2 | import Header from "./components/header"; 3 | import Editor from "./components/Editor"; 4 | import Appearance from "./components/appearance"; 5 | import Preview from "./components/Preview"; 6 | import { useCode } from "./hooks/code"; 7 | import { HiOutlineLink, HiOutlinePencil } from "react-icons/hi"; 8 | import { AiOutlineEye } from "react-icons/ai"; 9 | import { BsBrush } from "react-icons/bs"; 10 | import { FaFileDownload } from "react-icons/fa"; 11 | 12 | export default function App() { 13 | const { getCode } = useCode(); 14 | const [preview, setPreview] = useState(false); 15 | const [showDesign, setShowDesign] = useState(false); 16 | 17 | function download(filename, text) { 18 | const element = document.createElement("a"); 19 | element.setAttribute( 20 | "href", 21 | "data:text/plain;charset=utf-8," + encodeURIComponent(text), 22 | ); 23 | element.setAttribute("download", filename); 24 | 25 | element.style.display = "none"; 26 | document.body.appendChild(element); 27 | 28 | element.click(); 29 | 30 | document.body.removeChild(element); 31 | } 32 | 33 | const handleDownload = () => { 34 | const generatedCode = getCode(); 35 | download("index.html", generatedCode); 36 | }; 37 | 38 | return ( 39 | <> 40 | {!preview &&
} 41 | 42 |
47 |
48 | {showDesign ? : } 49 |
50 | 51 |
56 | 57 | 58 |
63 | {preview ? ( 64 |
setPreview(false)} 66 | className="fixed left-4 top-4 p-4 hidden lg:flex justify-center items-center space-x-1 bg-white dark:bg-secondary border border-gray-300 dark:border-border-dark rounded-3xl cursor-pointer hover:bg-gray-300 dark:hover:bg-secondary-accent" 67 | > 68 | 72 | 73 | Editor 74 | 75 |
76 | ) : ( 77 |
78 | {showDesign ? ( 79 |
setShowDesign(false)} 81 | className="flex p-2 items-center space-x-2 cursor-pointer hover:bg-gray-300 dark:hover:bg-border-dark rounded-xl" 82 | > 83 | 87 | 88 | Links 89 | 90 |
91 | ) : ( 92 |
setShowDesign(true)} 94 | className="flex p-2 items-center space-x-2 cursor-pointer hover:bg-gray-300 dark:hover:bg-border-dark rounded-xl" 95 | > 96 | 100 | 101 | Design 102 | 103 |
104 | )} 105 |
setPreview(true)} 107 | className="flex p-2 items-center space-x-2 cursor-pointer hover:bg-gray-300 dark:hover:bg-border-dark rounded-xl" 108 | > 109 | 113 | 114 | Preview 115 | 116 |
117 |
121 | 125 | 126 | Code 127 | 128 |
129 |
130 | )} 131 |
132 |
133 |
134 |
135 | {preview ? ( 136 |
setPreview(false)} 138 | className="flex justify-center items-center space-x-1 cursor-pointer" 139 | > 140 | 144 | 145 | Editor 146 | 147 |
148 | ) : ( 149 | <> 150 | {showDesign ? ( 151 |
setShowDesign(false)} 153 | className="flex flex-col justify-center items-center cursor-pointer" 154 | > 155 | 159 | 160 | Links 161 | 162 |
163 | ) : ( 164 |
setShowDesign(true)} 166 | className="flex flex-col justify-center items-center cursor-pointer" 167 | > 168 | 172 | 173 | Design 174 | 175 |
176 | )} 177 |
setPreview(true)} 179 | className="flex flex-col justify-center items-center cursor-pointer" 180 | > 181 | 185 | 186 | Preview 187 | 188 |
189 |
193 | 197 | 198 | Code 199 | 200 |
201 | 202 | )} 203 |
204 | 205 | ); 206 | } 207 | -------------------------------------------------------------------------------- /src/App.test.js: -------------------------------------------------------------------------------- 1 | import { render, screen } from "@testing-library/react"; 2 | import App from "./App"; 3 | 4 | test("renders learn react link", () => { 5 | render(); 6 | const linkElement = screen.getByText(/learn react/i); 7 | expect(linkElement).toBeInTheDocument(); 8 | }); 9 | -------------------------------------------------------------------------------- /src/components/Editor.js: -------------------------------------------------------------------------------- 1 | import React, { useState } from "react"; 2 | import { useAdmin } from "../context/adminContext"; 3 | import { DragDropContext, Droppable } from "react-beautiful-dnd"; 4 | import LinkCardEditable from "./LinkCardEditable"; 5 | import ProfileCard from "./ProfileCard"; 6 | import { HiOutlineLink } from "react-icons/hi"; 7 | import EmbedEditable from "./EmbedEditable"; 8 | import EmbedModal from "./embedModal"; 9 | import SocialIconCard from "./SocialIconCard"; 10 | 11 | export default function Editor() { 12 | const { 13 | state: { links }, 14 | dispatch, 15 | } = useAdmin(); 16 | 17 | const [showModal, setShowModal] = useState(false); 18 | 19 | const handleNewLink = () => { 20 | dispatch({ 21 | type: "field", 22 | field: "links", 23 | value: [ 24 | ...links, 25 | { title: "", link: "https://", description: "", active: true }, 26 | ], 27 | }); 28 | }; 29 | 30 | return ( 31 | <> 32 | 33 | 34 | { 36 | const srcI = param.source.index; 37 | const desI = param.destination?.index; 38 | const newLinks = [...links]; 39 | const draggeditem = newLinks.splice(srcI, 1); 40 | newLinks.splice(desI, 0, ...draggeditem); 41 | dispatch({ type: "field", field: "links", value: newLinks }); 42 | }} 43 | > 44 | 45 | {(provided, _) => ( 46 |
52 | {links?.map((link, index) => { 53 | if (link.embed) 54 | return ( 55 | 60 | ); 61 | return ( 62 | 67 | ); 68 | })} 69 | {provided.placeholder} 70 |
71 | )} 72 |
73 |
74 |
75 | 84 | 90 |
91 | {showModal && ( 92 |
setShowModal(!showModal)} 94 | className="fixed inset-0 flex items-center justify-center z-50 bg-black/60" 95 | > 96 | 97 |
98 | )} 99 | 100 | 101 | ); 102 | } 103 | -------------------------------------------------------------------------------- /src/components/Embed.js: -------------------------------------------------------------------------------- 1 | import PropTypes from "prop-types"; 2 | import * as EMBED from "../constants/embed"; 3 | 4 | export default function Embed({ link, linkStyle, linkColor, linkFontColor }) { 5 | const youtubeRegex = 6 | /(?:https?:\/\/)?(?:www\.)?youtu\.?be(?:\.com)?\/?.*(?:watch|embed)?(?:.*v=|v\/|\/)([\w\-_]+)&?/; 7 | 8 | const spotifyRegex = /(?<=(com\/))(?:...)+/; 9 | 10 | if (link.embed === EMBED.YOUTUBE && !!youtubeRegex.exec(link.link)) 11 | return ( 12 | 27 | thumbnail 34 |

{link.title}

35 |
36 | ); 37 | if (link.embed === EMBED.SPOTIFY && !!spotifyRegex.exec(link.link)) 38 | return ( 39 |
49 | 58 |

{link.title}

59 |
60 | ); 61 | return ( 62 |
72 | Invalid Link 73 |
74 | ); 75 | } 76 | 77 | Embed.propTypes = { 78 | link: PropTypes.object.isRequired, 79 | linkStyle: PropTypes.object, 80 | linkColor: PropTypes.string, 81 | linkFontColor: PropTypes.string, 82 | }; 83 | -------------------------------------------------------------------------------- /src/components/EmbedEditable.js: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState } from "react"; 2 | import { Draggable } from "react-beautiful-dnd"; 3 | import { useAdmin } from "../context/adminContext"; 4 | import { GoTrashcan } from "react-icons/go"; 5 | import { MdDragIndicator } from "react-icons/md"; 6 | import { HiOutlinePencil } from "react-icons/hi"; 7 | import PropTypes from "prop-types"; 8 | import * as EMBED from "../constants/embed"; 9 | import { AiFillYoutube } from "react-icons/ai"; 10 | import { BsSpotify } from "react-icons/bs"; 11 | import InputField from "./commons/inputField"; 12 | 13 | export default function EmbedEditable({ id, Link }) { 14 | const { 15 | state: { links }, 16 | dispatch, 17 | } = useAdmin(); 18 | 19 | const [title, setTitle] = useState(""); 20 | const [link, setLink] = useState(""); 21 | const [active, setActive] = useState(false); 22 | const [editMode, setEditMode] = useState(false); 23 | 24 | const handleEdit = (value) => { 25 | const updatedLinks = links; 26 | 27 | updatedLinks[id] = value; 28 | 29 | dispatch({ type: "field", field: "links", value: updatedLinks }); 30 | }; 31 | 32 | const handleRemoveLink = () => { 33 | const updatedLinks = links.filter((_, index) => id !== index); 34 | 35 | dispatch({ type: "field", field: "links", value: updatedLinks }); 36 | }; 37 | 38 | const handleSave = () => { 39 | handleEdit({ 40 | embed: Link.embed, 41 | title: title, 42 | link: link, 43 | description: "", 44 | active: active, 45 | }); 46 | setEditMode(false); 47 | }; 48 | 49 | const handleCancel = () => { 50 | setTitle(Link.title); 51 | setLink(Link.link); 52 | setActive(Link.active); 53 | setEditMode(false); 54 | }; 55 | 56 | const handleChecked = (e) => { 57 | setActive(e.target.checked); 58 | handleEdit({ 59 | embed: Link.embed, 60 | title: title, 61 | link: link, 62 | description: "", 63 | active: e.target.checked, 64 | }); 65 | }; 66 | 67 | useEffect(() => { 68 | if (Link) { 69 | setTitle(Link.title); 70 | setLink(Link.link); 71 | setActive(Link.active); 72 | } 73 | }, [Link]); 74 | 75 | return ( 76 | 81 | {(provided, snapshot) => ( 82 |
88 |
93 | {!editMode ? ( 94 |
95 |
setEditMode(true)} 97 | className={`px-4 flex justify-center items-center flex-1`} 98 | > 99 | 103 |
104 | {Link.embed === EMBED.YOUTUBE && ( 105 | 109 | )} 110 | {Link.embed === EMBED.SPOTIFY && ( 111 | 115 | )} 116 |

117 | {Link.title} 118 |

119 |
120 |
121 |
122 | 138 | 143 |
144 |
145 | ) : ( 146 |
147 | setTitle(e.target.value)} 151 | /> 152 | setLink(e.target.value)} 156 | /> 157 |
158 | 164 | 170 |
171 |
172 | )} 173 |
177 | 181 |
182 |
183 |
184 | )} 185 |
186 | ); 187 | } 188 | 189 | EmbedEditable.propTypes = { 190 | id: PropTypes.number, 191 | Link: PropTypes.object.isRequired, 192 | }; 193 | -------------------------------------------------------------------------------- /src/components/LinkCard.js: -------------------------------------------------------------------------------- 1 | import PropTypes from "prop-types"; 2 | 3 | export default function LinkCard({ 4 | link, 5 | linkStyle, 6 | linkColor, 7 | linkFontColor, 8 | }) { 9 | return ( 10 | 23 |
24 |

{link.title}

25 |

{link.description}

26 |
27 |
28 | ); 29 | } 30 | 31 | LinkCard.propTypes = { 32 | link: PropTypes.object.isRequired, 33 | linkStyle: PropTypes.object, 34 | linkColor: PropTypes.string, 35 | linkFontColor: PropTypes.string, 36 | }; 37 | -------------------------------------------------------------------------------- /src/components/LinkCardEditable.js: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState } from "react"; 2 | import { Draggable } from "react-beautiful-dnd"; 3 | import { useAdmin } from "../context/adminContext"; 4 | import { GoTrashcan } from "react-icons/go"; 5 | import { MdDragIndicator } from "react-icons/md"; 6 | import { HiOutlinePencil } from "react-icons/hi"; 7 | import PropTypes from "prop-types"; 8 | import InputField from "./commons/inputField"; 9 | 10 | export default function LinkCardEditable({ id, Link }) { 11 | const { 12 | state: { links }, 13 | dispatch, 14 | } = useAdmin(); 15 | 16 | const [title, setTitle] = useState(""); 17 | const [link, setLink] = useState(""); 18 | const [description, setDescription] = useState(""); 19 | const [active, setActive] = useState(false); 20 | const [editMode, setEditMode] = useState(false); 21 | 22 | const handleEdit = (value) => { 23 | const updatedLinks = links; 24 | 25 | updatedLinks[id] = value; 26 | 27 | dispatch({ type: "field", field: "links", value: updatedLinks }); 28 | }; 29 | 30 | const handleRemoveLink = () => { 31 | const updatedLinks = links.filter((_, index) => id !== index); 32 | 33 | dispatch({ type: "field", field: "links", value: updatedLinks }); 34 | }; 35 | 36 | const handleSave = () => { 37 | handleEdit({ 38 | title: title, 39 | link: link, 40 | description: description, 41 | active: active, 42 | }); 43 | setEditMode(false); 44 | }; 45 | 46 | const handleCancel = () => { 47 | setTitle(Link.title); 48 | setLink(Link.link); 49 | setDescription(Link.description); 50 | setActive(Link.active); 51 | setEditMode(false); 52 | }; 53 | 54 | const handleChecked = (e) => { 55 | setActive(e.target.checked); 56 | handleEdit({ 57 | title: title, 58 | link: link, 59 | description: description, 60 | active: e.target.checked, 61 | }); 62 | }; 63 | 64 | useEffect(() => { 65 | if (Link) { 66 | setTitle(Link.title); 67 | setLink(Link.link); 68 | setDescription(Link.description); 69 | setActive(Link.active); 70 | } 71 | }, [Link]); 72 | 73 | return ( 74 | 79 | {(provided, snapshot) => ( 80 |
86 |
91 | {!editMode ? ( 92 |
93 |
setEditMode(true)} 95 | className={`px-4 flex justify-center items-center flex-1`} 96 | > 97 | 101 |
102 |

103 | {Link.title} 104 |

105 |

106 | {Link.description} 107 |

108 |
109 |
110 |
111 | 127 | 132 |
133 |
134 | ) : ( 135 |
136 | setTitle(e.target.value)} 140 | /> 141 | setLink(e.target.value)} 145 | /> 146 | setDescription(e.target.value)} 150 | /> 151 |
152 | 158 | 164 |
165 |
166 | )} 167 |
171 | 175 |
176 |
177 |
178 | )} 179 |
180 | ); 181 | } 182 | 183 | LinkCardEditable.propTypes = { 184 | id: PropTypes.number, 185 | Link: PropTypes.object.isRequired, 186 | }; 187 | -------------------------------------------------------------------------------- /src/components/Preview.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { useAdmin } from "../context/adminContext"; 3 | // import Page from "./page"; 4 | import PropTypes from "prop-types"; 5 | import Page from "./page"; 6 | 7 | export default function Preview({ preview }) { 8 | const { state } = useAdmin(); 9 | const { imgSrc, profileName, about, links, appearance, socials } = state; 10 | 11 | return ( 12 |
19 | 30 |
31 | ); 32 | } 33 | 34 | Preview.propTypes = { 35 | preview: PropTypes.bool.isRequired, 36 | }; 37 | -------------------------------------------------------------------------------- /src/components/ProfileCard.js: -------------------------------------------------------------------------------- 1 | import React, { useEffect } from "react"; 2 | import { useAdmin } from "../context/adminContext"; 3 | import { BsPersonFill } from "react-icons/bs"; 4 | import InputFieldSimple from "./commons/inputFieldSimple"; 5 | 6 | export default function ProfileCard() { 7 | const { state, dispatch } = useAdmin(); 8 | const { imgFile, imgSrc, profileName, about } = state; 9 | 10 | const handleFile = (event) => { 11 | event.preventDefault(); 12 | 13 | dispatch({ type: "field", field: "imgFile", value: event.target.files[0] }); 14 | }; 15 | 16 | const handleRemove = async () => { 17 | dispatch({ type: "field", field: "imgSrc", value: null }); 18 | dispatch({ type: "field", field: "imgFile", value: null }); 19 | }; 20 | 21 | useEffect(() => { 22 | if (imgFile) { 23 | const reader = new FileReader(); 24 | 25 | reader.onload = function (e) { 26 | dispatch({ type: "field", field: "imgSrc", value: e.target.result }); 27 | }; 28 | 29 | reader.readAsDataURL(imgFile); 30 | } 31 | }, [imgFile]); 32 | 33 | return ( 34 |
35 |
36 | {imgSrc ? ( 37 | 42 | ) : ( 43 |
44 | 45 |
46 | )} 47 |
48 | 55 | 65 |
66 |
67 | 71 | dispatch({ 72 | type: "field", 73 | field: "profileName", 74 | value: e.target.value, 75 | }) 76 | } 77 | placeholder="@yourname" 78 | /> 79 |
80 | 83 | 95 |
96 |
97 | ); 98 | } 99 | -------------------------------------------------------------------------------- /src/components/SocialIconCard.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { useAdmin } from "../context/adminContext"; 3 | import InputField from "./commons/inputField"; 4 | 5 | export default function SocialIconCard() { 6 | const { 7 | state: { socials }, 8 | dispatch, 9 | } = useAdmin(); 10 | const { 11 | twitter, 12 | instagram, 13 | facebook, 14 | linkedin, 15 | github, 16 | hashnode, 17 | devto, 18 | medium, 19 | whatsapp, 20 | tiktok, 21 | youtube, 22 | } = socials; 23 | 24 | return ( 25 |
26 | 27 | Social Icons 28 | 29 | 33 | dispatch({ 34 | type: "field", 35 | field: "socials", 36 | value: { ...socials, twitter: e.target.value }, 37 | }) 38 | } 39 | /> 40 | 44 | dispatch({ 45 | type: "field", 46 | field: "socials", 47 | value: { ...socials, instagram: e.target.value }, 48 | }) 49 | } 50 | /> 51 | 55 | dispatch({ 56 | type: "field", 57 | field: "socials", 58 | value: { ...socials, facebook: e.target.value }, 59 | }) 60 | } 61 | /> 62 | 66 | dispatch({ 67 | type: "field", 68 | field: "socials", 69 | value: { ...socials, linkedin: e.target.value }, 70 | }) 71 | } 72 | /> 73 | 77 | dispatch({ 78 | type: "field", 79 | field: "socials", 80 | value: { ...socials, github: e.target.value }, 81 | }) 82 | } 83 | /> 84 | 88 | dispatch({ 89 | type: "field", 90 | field: "socials", 91 | value: { ...socials, hashnode: e.target.value }, 92 | }) 93 | } 94 | /> 95 | 99 | dispatch({ 100 | type: "field", 101 | field: "socials", 102 | value: { ...socials, devto: e.target.value }, 103 | }) 104 | } 105 | /> 106 | 110 | dispatch({ 111 | type: "field", 112 | field: "socials", 113 | value: { ...socials, medium: e.target.value }, 114 | }) 115 | } 116 | /> 117 | 121 | dispatch({ 122 | type: "field", 123 | field: "socials", 124 | value: { ...socials, whatsapp: e.target.value }, 125 | }) 126 | } 127 | /> 128 | 132 | dispatch({ 133 | type: "field", 134 | field: "socials", 135 | value: { ...socials, tiktok: e.target.value }, 136 | }) 137 | } 138 | /> 139 | 143 | dispatch({ 144 | type: "field", 145 | field: "socials", 146 | value: { ...socials, youtube: e.target.value }, 147 | }) 148 | } 149 | /> 150 |
151 | ); 152 | } 153 | -------------------------------------------------------------------------------- /src/components/appearance.js: -------------------------------------------------------------------------------- 1 | import React, { useEffect } from "react"; 2 | import { HexColorPicker } from "react-colorful"; 3 | import { useAdmin } from "../context/adminContext"; 4 | import { MdAddPhotoAlternate } from "react-icons/md"; 5 | 6 | export default function Appearance() { 7 | const { 8 | state: { bgImgFile, appearance }, 9 | dispatch, 10 | } = useAdmin(); 11 | const { background, backgroundColor, linkColor, linkFontColor, linkStyle } = 12 | appearance; 13 | 14 | const backgroundPresetColors = [ 15 | "#000000", 16 | "#212529", 17 | "#ff5b5b", 18 | "#fca311", 19 | "#e5e5e5", 20 | ]; 21 | const linkPresetColors = [ 22 | "#000000", 23 | "#212529", 24 | "#ff5b5b", 25 | "#fca311", 26 | "#e5e5e5", 27 | ]; 28 | 29 | const fontColors = ["#fff", "#e5e5e5", "#212529", "#000"]; 30 | const linkfontColors = ["#fff", "#e5e5e5", "#212529", "#000"]; 31 | 32 | const handleFile = (event) => { 33 | event.preventDefault(); 34 | 35 | dispatch({ 36 | type: "field", 37 | field: "bgImgFile", 38 | value: event.target.files[0], 39 | }); 40 | }; 41 | 42 | const handleRemove = () => { 43 | dispatch({ 44 | type: "field", 45 | field: "appearance", 46 | value: { 47 | ...appearance, 48 | background: null, 49 | }, 50 | }); 51 | dispatch({ type: "field", field: "bgImgFile", value: null }); 52 | }; 53 | 54 | useEffect(() => { 55 | if (bgImgFile) { 56 | const reader = new FileReader(); 57 | 58 | reader.onload = function (e) { 59 | dispatch({ 60 | type: "field", 61 | field: "appearance", 62 | value: { ...appearance, background: e.target.result }, 63 | }); 64 | }; 65 | 66 | reader.readAsDataURL(bgImgFile); 67 | } 68 | }, [bgImgFile]); 69 | 70 | return ( 71 |
72 |
73 |
74 |
75 | { 79 | dispatch({ 80 | type: "field", 81 | field: "appearance", 82 | value: { ...appearance, backgroundColor: color }, 83 | }); 84 | }} 85 | /> 86 |
87 | {backgroundPresetColors.map((presetColor) => ( 88 |
105 | 108 |
109 |
110 |
111 | {background ? ( 112 | 117 | ) : ( 118 |
119 | 120 |
121 | )} 122 |
123 | 130 | 131 | 141 |
142 |
143 | 146 |
147 |
148 |
149 |
150 | 151 |
152 | {fontColors.map((presetColor) => ( 153 |
170 |
171 | 172 |
173 |
174 |
175 | { 179 | dispatch({ 180 | type: "field", 181 | field: "appearance", 182 | value: { ...appearance, linkColor: color }, 183 | }); 184 | }} 185 | /> 186 |
187 | {linkPresetColors.map((presetColor) => ( 188 |
205 | 208 |
209 |
210 |
211 | 227 | 243 | 261 | 278 |
279 | 282 |
283 |
284 |
285 |
286 | 289 |
290 | {linkfontColors.map((presetColor) => ( 291 |
308 |
309 |
310 | ); 311 | } 312 | -------------------------------------------------------------------------------- /src/components/commons/inputField.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import PropTypes from "prop-types"; 3 | 4 | export default function InputField({ label, value, onChange }) { 5 | return ( 6 |
7 | onChange(e)} 14 | /> 15 | 21 |
22 | ); 23 | } 24 | 25 | InputField.propTypes = { 26 | label: PropTypes.string.isRequired, 27 | value: PropTypes.string, 28 | onChange: PropTypes.func, 29 | }; 30 | -------------------------------------------------------------------------------- /src/components/commons/inputFieldSimple.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import PropTypes from "prop-types"; 3 | 4 | export default function InputFieldSimple({ 5 | label, 6 | placeholder, 7 | value, 8 | onChange, 9 | }) { 10 | return ( 11 |
12 | 15 | onChange(e)} 22 | /> 23 |
24 | ); 25 | } 26 | 27 | InputFieldSimple.propTypes = { 28 | label: PropTypes.string.isRequired, 29 | placeholder: PropTypes.string, 30 | value: PropTypes.string, 31 | onChange: PropTypes.func, 32 | }; 33 | -------------------------------------------------------------------------------- /src/components/commons/toast.js: -------------------------------------------------------------------------------- 1 | import PropTypes from "prop-types"; 2 | 3 | export default function Toast({ show, message }) { 4 | return ( 5 |
10 | {message} 11 |
12 | ); 13 | } 14 | 15 | Toast.propTypes = { 16 | show: PropTypes.bool, 17 | message: PropTypes.string, 18 | }; 19 | -------------------------------------------------------------------------------- /src/components/embedModal.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { useAdmin } from "../context/adminContext"; 3 | import * as EMBED from "../constants/embed"; 4 | import { AiFillYoutube } from "react-icons/ai"; 5 | import { BsSpotify } from "react-icons/bs"; 6 | 7 | export default function EmbedModal() { 8 | const { 9 | state: { links }, 10 | dispatch, 11 | } = useAdmin(); 12 | 13 | const handleYouTube = () => { 14 | dispatch({ 15 | type: "field", 16 | field: "links", 17 | value: [ 18 | ...links, 19 | { 20 | embed: EMBED.YOUTUBE, 21 | title: "", 22 | link: "", 23 | description: "", 24 | active: true, 25 | }, 26 | ], 27 | }); 28 | }; 29 | 30 | const handleSpotify = () => { 31 | dispatch({ 32 | type: "field", 33 | field: "links", 34 | value: [ 35 | ...links, 36 | { 37 | embed: EMBED.SPOTIFY, 38 | title: "", 39 | link: "", 40 | description: "", 41 | active: true, 42 | }, 43 | ], 44 | }); 45 | }; 46 | 47 | return ( 48 |
49 | 56 | 63 |
64 | ); 65 | } 66 | -------------------------------------------------------------------------------- /src/components/header.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { useDarkMode } from "../context/darkModeContext"; 3 | import { BsFillMoonStarsFill } from "react-icons/bs"; 4 | import { MdLightMode } from "react-icons/md"; 5 | 6 | function Header() { 7 | const { dark, setDark } = useDarkMode(); 8 | 9 | const handleDark = () => { 10 | setDark(!dark); 11 | localStorage.setItem("dark", JSON.stringify(!dark)); 12 | }; 13 | 14 | return ( 15 |
16 |

17 | Link.gen 18 |

19 |
20 | {dark ? ( 21 | 26 | ) : ( 27 | 32 | )} 33 | 39 | Try Linkpile 40 | 41 |
42 |
43 | ); 44 | } 45 | 46 | export default Header; 47 | -------------------------------------------------------------------------------- /src/components/page.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import LinkCard from "../components/LinkCard"; 3 | import PropTypes from "prop-types"; 4 | import { BsPersonFill } from "react-icons/bs"; 5 | import { SiHashnode } from "react-icons/si"; 6 | import { FaDev, FaTiktok } from "react-icons/fa"; 7 | import { GrMedium } from "react-icons/gr"; 8 | import { ImWhatsapp } from "react-icons/im"; 9 | import { 10 | FiGithub, 11 | FiLinkedin, 12 | FiTwitter, 13 | FiInstagram, 14 | FiFacebook, 15 | } from "react-icons/fi"; 16 | import { AiFillYoutube } from "react-icons/ai"; 17 | import Embed from "./Embed"; 18 | 19 | export default function Page({ 20 | imgSrc, 21 | profileName, 22 | about, 23 | links, 24 | appearance, 25 | socials, 26 | styleClasses, 27 | }) { 28 | const { 29 | background, 30 | backgroundColor, 31 | font, 32 | fontColor, 33 | linkStyle, 34 | linkColor, 35 | linkFontColor, 36 | } = appearance; 37 | return ( 38 |
50 | {imgSrc ? ( 51 |
52 | 57 |
58 | ) : ( 59 | 60 | )} 61 | {profileName ? ( 62 |

{profileName}

63 | ) : ( 64 |

@yourname

65 | )} 66 |

{about}

67 |
70 | {links 71 | ?.filter((link) => link.active !== false && link.title && link.link) 72 | .map((link) => { 73 | if (link.embed) 74 | return ( 75 | 82 | ); 83 | return ( 84 | 91 | ); 92 | })} 93 |
94 |
95 | {socials.twitter && ( 96 | 101 | 105 | 106 | )} 107 | {socials.instagram && ( 108 | 113 | 117 | 118 | )} 119 | {socials.facebook && ( 120 | 121 | 125 | 126 | )} 127 | {socials.linkedin && ( 128 | 129 | 133 | 134 | )} 135 | {socials.github && ( 136 | 141 | 145 | 146 | )} 147 | {socials.hashnode && ( 148 | 153 | 157 | 158 | )} 159 | {socials.devto && ( 160 | 165 | 169 | 170 | )} 171 | {socials.medium && ( 172 | 177 | 181 | 182 | )} 183 | {socials.whatsapp && ( 184 | 189 | 193 | 194 | )} 195 | {socials.tiktok && ( 196 | 201 | 205 | 206 | )} 207 | {socials.youtube && ( 208 | 213 | 217 | 218 | )} 219 |
220 |
221 | ); 222 | } 223 | 224 | Page.propTypes = { 225 | username: PropTypes.string, 226 | imgSrc: PropTypes.string, 227 | profileName: PropTypes.string, 228 | about: PropTypes.string, 229 | links: PropTypes.array, 230 | appearance: PropTypes.object, 231 | socials: PropTypes.object, 232 | styleClasses: PropTypes.string, 233 | }; 234 | -------------------------------------------------------------------------------- /src/components/staticPage.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import PropTypes from "prop-types"; 3 | import { BsPersonFill } from "react-icons/bs"; 4 | import * as EMBED from "../constants/embed"; 5 | import { SiHashnode } from "react-icons/si"; 6 | import { FaDev, FaTiktok } from "react-icons/fa"; 7 | import { GrMedium } from "react-icons/gr"; 8 | import { ImWhatsapp } from "react-icons/im"; 9 | import { 10 | FiGithub, 11 | FiLinkedin, 12 | FiTwitter, 13 | FiInstagram, 14 | FiFacebook, 15 | } from "react-icons/fi"; 16 | import { AiFillYoutube } from "react-icons/ai"; 17 | 18 | function LinkCard({ link }) { 19 | return ( 20 | 26 |
27 |

{link.title}

28 |

{link.description}

29 |
30 |
31 | ); 32 | } 33 | 34 | function Embed({ link }) { 35 | const youtubeRegex = 36 | /(?:https?:\/\/)?(?:www\.)?youtu\.?be(?:\.com)?\/?.*(?:watch|embed)?(?:.*v=|v\/|\/)([\w\-_]+)&?/; 37 | 38 | const spotifyRegex = /(?<=(com\/))(?:...)+/; 39 | 40 | if (link.embed === EMBED.YOUTUBE && !!youtubeRegex.exec(link.link)) 41 | return ( 42 | 50 | {" "} 51 |
52 | thumbnail 59 |

{link.title}

60 |
61 |
62 | ); 63 | 64 | if (link.embed === EMBED.SPOTIFY && !!spotifyRegex.exec(link.link)) 65 | return ( 66 |
67 |
68 | 77 |

{link.title}

78 |
79 |
80 | ); 81 | return
Invalid Link
; 82 | } 83 | 84 | export default function StaticPage({ 85 | imgSrc, 86 | profileName, 87 | about, 88 | links, 89 | socials, 90 | }) { 91 | return ( 92 |
93 | {imgSrc ? ( 94 |
95 | 96 |
97 | ) : ( 98 | 105 | )} 106 | {profileName ? ( 107 |

{profileName}

108 | ) : ( 109 |

@yourname

110 | )} 111 |

{about}

112 |
113 | {links 114 | ?.filter((link) => link.active !== false && link.title && link.link) 115 | .map((link) => { 116 | if (link.embed) return ; 117 | return ; 118 | })} 119 |
120 |
121 | {socials.twitter && ( 122 | 128 | 129 | 130 | )} 131 | {socials.instagram && ( 132 | 138 | 139 | 140 | )} 141 | {socials.facebook && ( 142 | 148 | 149 | 150 | )} 151 | {socials.linkedin && ( 152 | 158 | 159 | 160 | )} 161 | {socials.github && ( 162 | 168 | 169 | 170 | )} 171 | {socials.hashnode && ( 172 | 178 | 179 | 180 | )} 181 | {socials.devto && ( 182 | 188 | 189 | 190 | )} 191 | {socials.medium && ( 192 | 198 | 199 | 200 | )} 201 | {socials.whatsapp && ( 202 | 208 | 209 | 210 | )} 211 | {socials.tiktok && ( 212 | 218 | 219 | 220 | )} 221 | {socials.youtube && ( 222 | 228 | 229 | 230 | )} 231 |
232 |
233 | ); 234 | } 235 | 236 | LinkCard.propTypes = { 237 | link: PropTypes.object.isRequired, 238 | }; 239 | 240 | Embed.propTypes = { 241 | link: PropTypes.object.isRequired, 242 | linkStyle: PropTypes.object, 243 | linkColor: PropTypes.string, 244 | linkFontColor: PropTypes.string, 245 | }; 246 | 247 | StaticPage.propTypes = { 248 | imgSrc: PropTypes.string, 249 | profileName: PropTypes.string, 250 | about: PropTypes.string, 251 | links: PropTypes.array, 252 | socials: PropTypes.object, 253 | }; 254 | -------------------------------------------------------------------------------- /src/constants/embed.js: -------------------------------------------------------------------------------- 1 | export const YOUTUBE = "YouTube"; 2 | export const SOUNDCLOUDE = "SoundCloud"; 3 | export const SPOTIFY = "Spotify"; 4 | -------------------------------------------------------------------------------- /src/context/adminContext.js: -------------------------------------------------------------------------------- 1 | import React, { createContext, useContext, useEffect, useReducer } from "react"; 2 | import PropTytpes from "prop-types"; 3 | import { initialState, adminReducer } from "../reducers/adminReducer"; 4 | 5 | const AdminContext = createContext(); 6 | 7 | export function useAdmin() { 8 | return useContext(AdminContext); 9 | } 10 | 11 | export function AdminProvider({ children }) { 12 | const [state, dispatch] = useReducer(adminReducer, initialState); 13 | 14 | useEffect(() => { 15 | dispatch({ 16 | type: "field", 17 | field: "imgSrc", 18 | value: JSON.parse(localStorage.getItem("imgSrc")) || state.imgSrc, 19 | }); 20 | dispatch({ 21 | type: "field", 22 | field: "profileName", 23 | value: 24 | JSON.parse(localStorage.getItem("profileName")) || state.profileName, 25 | }); 26 | dispatch({ 27 | type: "field", 28 | field: "about", 29 | value: JSON.parse(localStorage.getItem("about")) || state.about, 30 | }); 31 | dispatch({ 32 | type: "field", 33 | field: "links", 34 | value: JSON.parse(localStorage.getItem("links")) || state.links, 35 | }); 36 | dispatch({ 37 | type: "field", 38 | field: "appearance", 39 | value: JSON.parse(localStorage.getItem("appearance")) || state.appearance, 40 | }); 41 | dispatch({ 42 | type: "field", 43 | field: "socials", 44 | value: JSON.parse(localStorage.getItem("socials")) || state.socials, 45 | }); 46 | }, []); 47 | 48 | const value = { state, dispatch }; 49 | 50 | return ( 51 | {children} 52 | ); 53 | } 54 | 55 | AdminProvider.propTypes = { 56 | children: PropTytpes.element, 57 | }; 58 | -------------------------------------------------------------------------------- /src/context/darkModeContext.js: -------------------------------------------------------------------------------- 1 | import React, { createContext, useContext, useEffect, useState } from "react"; 2 | import PropTytpes from "prop-types"; 3 | 4 | const DarkModeContext = createContext(); 5 | 6 | export function useDarkMode() { 7 | return useContext(DarkModeContext); 8 | } 9 | 10 | export function DarkModeProvider({ children }) { 11 | const [dark, setDark] = useState(JSON.parse(localStorage.getItem("dark"))); 12 | 13 | useEffect(() => { 14 | if (dark) { 15 | window.document.documentElement.classList.add("dark"); 16 | } else { 17 | window.document.documentElement.classList.remove("dark"); 18 | } 19 | }, [dark]); 20 | 21 | const value = { dark, setDark }; 22 | 23 | return ( 24 | 25 | {children} 26 | 27 | ); 28 | } 29 | 30 | DarkModeProvider.propTypes = { 31 | children: PropTytpes.element, 32 | }; 33 | -------------------------------------------------------------------------------- /src/hooks/code.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { renderToStaticMarkup } from "react-dom/server"; 3 | import { useAdmin } from "../context/adminContext"; 4 | import StaticPage from "../components/staticPage"; 5 | import { format } from "prettier/standalone"; 6 | import htmlParser from "prettier/parser-html"; 7 | 8 | function useCode() { 9 | const { state } = useAdmin(); 10 | const { imgSrc, profileName, about, links, appearance, socials } = state; 11 | const { 12 | background, 13 | backgroundColor, 14 | linkColor, 15 | linkStyle, 16 | linkFontColor, 17 | fontColor, 18 | } = appearance; 19 | 20 | const getCode = () => { 21 | const code = ` 22 | 23 | 24 | 25 | 26 | 27 | Document 28 | 29 | 30 | 31 | 35 | 36 | 165 | 166 | ${renderToStaticMarkup( 167 | , 175 | )} 176 | 177 | `; 178 | 179 | try { 180 | const formatted = format(code, { 181 | parser: "html", 182 | plugins: [htmlParser], 183 | }); 184 | return formatted; 185 | } catch (error) { 186 | console.log(error); 187 | } 188 | }; 189 | 190 | return { getCode }; 191 | } 192 | 193 | export { useCode }; 194 | -------------------------------------------------------------------------------- /src/index.css: -------------------------------------------------------------------------------- 1 | @import url("https://fonts.googleapis.com/css2?family=Inter:wght@200&display=swap"); 2 | @import url("https://fonts.googleapis.com/css2?family=Nunito:wght@900&display=swap"); 3 | 4 | @tailwind base; 5 | @tailwind components; 6 | @tailwind utilities; 7 | 8 | @layer base { 9 | #root { 10 | background-color: #fff; 11 | } 12 | .dark #root { 13 | background-color: #e5e7eb; 14 | } 15 | } 16 | 17 | @layer components { 18 | input:checked ~ .dot { 19 | transform: translateX(100%); 20 | } 21 | input:checked ~ .block { 22 | background-color: #7964ff; 23 | } 24 | 25 | /* Hide scrollbar for Chrome, Safari and Opera */ 26 | .s_hide::-webkit-scrollbar { 27 | display: none; 28 | } 29 | .s_hide { 30 | -ms-overflow-style: none; /* IE and Edge */ 31 | scrollbar-width: none; /* Firefox */ 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import ReactDOM from "react-dom"; 3 | import "./index.css"; 4 | import App from "./App"; 5 | 6 | import { AdminProvider } from "./context/adminContext"; 7 | import { DarkModeProvider } from "./context/darkModeContext"; 8 | 9 | ReactDOM.render( 10 | 11 | 12 | 13 | 14 | 15 | 16 | , 17 | document.getElementById("root"), 18 | ); 19 | -------------------------------------------------------------------------------- /src/reducers/adminReducer.js: -------------------------------------------------------------------------------- 1 | const initialState = { 2 | imgFile: null, 3 | bgImgFile: null, 4 | imgSrc: "", 5 | profileName: "", 6 | about: "", 7 | links: [], 8 | appearance: { 9 | background: "", 10 | backgroundColor: "#000000", 11 | font: "Nunito", 12 | fontColor: "#e5e5e5", 13 | linkStyle: { 14 | rounded: true, 15 | filled: true, 16 | shadow: false, 17 | special: "", 18 | }, 19 | linkColor: "#212529", 20 | linkFontColor: "#fff", 21 | }, 22 | socials: { 23 | twitter: "", 24 | instagram: "", 25 | facebook: "", 26 | linkedin: "", 27 | github: "", 28 | hashnode: "", 29 | devto: "", 30 | medium: "", 31 | whatsapp: "", 32 | }, 33 | }; 34 | 35 | function adminReducer(state, action) { 36 | switch (action.type) { 37 | case "field": 38 | localStorage.setItem(action.field, JSON.stringify(action.value)); 39 | return { 40 | ...state, 41 | [action.field]: action.value, 42 | }; 43 | default: 44 | break; 45 | } 46 | } 47 | 48 | export { initialState, adminReducer }; 49 | -------------------------------------------------------------------------------- /src/reportWebVitals.js: -------------------------------------------------------------------------------- 1 | const reportWebVitals = (onPerfEntry) => { 2 | if (onPerfEntry && onPerfEntry instanceof Function) { 3 | import("web-vitals").then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => { 4 | getCLS(onPerfEntry); 5 | getFID(onPerfEntry); 6 | getFCP(onPerfEntry); 7 | getLCP(onPerfEntry); 8 | getTTFB(onPerfEntry); 9 | }); 10 | } 11 | }; 12 | 13 | export default reportWebVitals; 14 | -------------------------------------------------------------------------------- /src/setupTests.js: -------------------------------------------------------------------------------- 1 | // jest-dom adds custom jest matchers for asserting on DOM nodes. 2 | // allows you to do things like: 3 | // expect(element).toHaveTextContent(/react/i) 4 | // learn more: https://github.com/testing-library/jest-dom 5 | import "@testing-library/jest-dom"; 6 | -------------------------------------------------------------------------------- /tailwind.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | content: ["./public/**/*.html", "./src/**/*.{js,jsx,ts,tsx,vue}"], 3 | darkMode: "class", // or 'media' or 'class' 4 | theme: { 5 | extend: { 6 | fontFamily: { 7 | nunito: ["Nunito", "sans-serif"], 8 | inter: ["Inter", "sans-serif"], 9 | }, 10 | colors: { 11 | primary: "#0A1929", 12 | secondary: "#132F4C", 13 | "border-dark": "#5090D3", 14 | "primary-accent": "#7964FF", 15 | "secondary-accent": "#7f75ef", 16 | }, 17 | animation: { 18 | "gradient-xy": "gradient-xy 10s linear infinite", 19 | }, 20 | }, 21 | }, 22 | plugins: [], 23 | }; 24 | --------------------------------------------------------------------------------