├── .gitignore ├── README.md ├── package-lock.json ├── package.json ├── public ├── favicon.ico ├── index.html ├── logo192.png ├── logo512.png ├── manifest.json └── robots.txt └── src ├── App.css ├── App.js ├── App.test.js ├── components ├── authProvider.jsx ├── dashboardWrapper.jsx ├── dashboardWrapper.module.css ├── link.jsx ├── link.module.css ├── loading.jsx ├── publicLink.jsx └── publicLink.module.css ├── firebase └── firebase.js ├── index.css ├── index.js ├── logo.svg ├── reportWebVitals.js ├── routes ├── dashboard.jsx ├── dashboard.module.css ├── editProfile.jsx ├── editProfile.module.css ├── login.jsx ├── login.module.css ├── loginv2.jsx ├── profile.jsx ├── profile.module.css ├── signout.jsx └── usernameView.jsx └── setupTests.js /.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.local 17 | .env.development.local 18 | .env.test.local 19 | .env.production.local 20 | .env 21 | 22 | npm-debug.log* 23 | yarn-debug.log* 24 | yarn-error.log* 25 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Getting Started with Create React App 2 | 3 | This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app). 4 | 5 | ## Available Scripts 6 | 7 | In the project directory, you can run: 8 | 9 | ### `npm start` 10 | 11 | Runs the app in the development mode.\ 12 | Open [http://localhost:3000](http://localhost:3000) to view it in your browser. 13 | 14 | The page will reload when you make changes.\ 15 | You may also see any lint errors in the console. 16 | 17 | ### `npm test` 18 | 19 | Launches the test runner in the interactive watch mode.\ 20 | See the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information. 21 | 22 | ### `npm run build` 23 | 24 | Builds the app for production to the `build` folder.\ 25 | It correctly bundles React in production mode and optimizes the build for the best performance. 26 | 27 | The build is minified and the filenames include the hashes.\ 28 | Your app is ready to be deployed! 29 | 30 | See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information. 31 | 32 | ### `npm run eject` 33 | 34 | **Note: this is a one-way operation. Once you `eject`, you can't go back!** 35 | 36 | If you aren't satisfied with the build tool and configuration choices, you can `eject` at any time. This command will remove the single build dependency from your project. 37 | 38 | Instead, it will copy all the configuration files and the transitive dependencies (webpack, Babel, ESLint, etc) right into your project so you have full control over them. All of the commands except `eject` will still work, but they will point to the copied scripts so you can tweak them. At this point you're on your own. 39 | 40 | You don't have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldn't feel obligated to use this feature. However we understand that this tool wouldn't be useful if you couldn't customize it when you are ready for it. 41 | 42 | ## Learn More 43 | 44 | You can learn more in the [Create React App documentation](https://facebook.github.io/create-react-app/docs/getting-started). 45 | 46 | To learn React, check out the [React documentation](https://reactjs.org/). 47 | 48 | ### Code Splitting 49 | 50 | This section has moved here: [https://facebook.github.io/create-react-app/docs/code-splitting](https://facebook.github.io/create-react-app/docs/code-splitting) 51 | 52 | ### Analyzing the Bundle Size 53 | 54 | This section has moved here: [https://facebook.github.io/create-react-app/docs/analyzing-the-bundle-size](https://facebook.github.io/create-react-app/docs/analyzing-the-bundle-size) 55 | 56 | ### Making a Progressive Web App 57 | 58 | This section has moved here: [https://facebook.github.io/create-react-app/docs/making-a-progressive-web-app](https://facebook.github.io/create-react-app/docs/making-a-progressive-web-app) 59 | 60 | ### Advanced Configuration 61 | 62 | This section has moved here: [https://facebook.github.io/create-react-app/docs/advanced-configuration](https://facebook.github.io/create-react-app/docs/advanced-configuration) 63 | 64 | ### Deployment 65 | 66 | This section has moved here: [https://facebook.github.io/create-react-app/docs/deployment](https://facebook.github.io/create-react-app/docs/deployment) 67 | 68 | ### `npm run build` fails to minify 69 | 70 | This section has moved here: [https://facebook.github.io/create-react-app/docs/troubleshooting#npm-run-build-fails-to-minify](https://facebook.github.io/create-react-app/docs/troubleshooting#npm-run-build-fails-to-minify) 71 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "linktree-firebase-react", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "@testing-library/jest-dom": "^5.16.2", 7 | "@testing-library/react": "^12.1.3", 8 | "@testing-library/user-event": "^13.5.0", 9 | "dotenv": "^16.0.0", 10 | "firebase": "^9.6.8", 11 | "react": "^17.0.2", 12 | "react-dom": "^17.0.2", 13 | "react-router-dom": "^6.2.2", 14 | "react-scripts": "5.0.0", 15 | "uuid": "^8.3.2", 16 | "web-vitals": "^2.1.4" 17 | }, 18 | "scripts": { 19 | "start": "react-scripts start", 20 | "build": "react-scripts build", 21 | "test": "react-scripts test", 22 | "eject": "react-scripts eject" 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 | } 43 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/marcosrivasr/app-react-firebase/1b37bf0490abfc35c0532ef800e3fd46a32b7bec/public/favicon.ico -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 13 | 17 | 21 | 22 | 31 | React App 32 | 33 | 34 | 35 |
36 | 46 | 47 | 48 | -------------------------------------------------------------------------------- /public/logo192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/marcosrivasr/app-react-firebase/1b37bf0490abfc35c0532ef800e3fd46a32b7bec/public/logo192.png -------------------------------------------------------------------------------- /public/logo512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/marcosrivasr/app-react-firebase/1b37bf0490abfc35c0532ef800e3fd46a32b7bec/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 | -------------------------------------------------------------------------------- /src/App.css: -------------------------------------------------------------------------------- 1 | .App { 2 | text-align: center; 3 | } 4 | 5 | .App-logo { 6 | height: 40vmin; 7 | pointer-events: none; 8 | } 9 | 10 | @media (prefers-reduced-motion: no-preference) { 11 | .App-logo { 12 | animation: App-logo-spin infinite 20s linear; 13 | } 14 | } 15 | 16 | .App-header { 17 | background-color: #282c34; 18 | min-height: 100vh; 19 | display: flex; 20 | flex-direction: column; 21 | align-items: center; 22 | justify-content: center; 23 | font-size: calc(10px + 2vmin); 24 | color: white; 25 | } 26 | 27 | .App-link { 28 | color: #61dafb; 29 | } 30 | 31 | @keyframes App-logo-spin { 32 | from { 33 | transform: rotate(0deg); 34 | } 35 | to { 36 | transform: rotate(360deg); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/App.js: -------------------------------------------------------------------------------- 1 | import logo from "./logo.svg"; 2 | import "./App.css"; 3 | import { Link } from "react-router-dom"; 4 | 5 | function App() { 6 | return ( 7 |
8 |
9 | logo 10 |

11 | Edit src/App.js and save to reload. 12 |

13 | 19 | Learn React 20 | 21 | 31 |
32 |
33 | ); 34 | } 35 | 36 | export default App; 37 | -------------------------------------------------------------------------------- /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/authProvider.jsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from "react"; 2 | import { 3 | auth, 4 | getUserInfo, 5 | userExists, 6 | registerNewUser, 7 | } from "../firebase/firebase"; 8 | import { onAuthStateChanged } from "firebase/auth"; 9 | import { useNavigate } from "react-router-dom"; 10 | 11 | export default function AuthProvider({ 12 | children, 13 | onUserLoggedIn, 14 | onUserNotLoggedIn, 15 | }) { 16 | const navigate = useNavigate(); 17 | useEffect(() => { 18 | onAuthStateChanged(auth, async (user) => { 19 | if (user) { 20 | const uid = user.uid; 21 | 22 | const exists = await userExists(user.uid); 23 | 24 | if (exists) { 25 | const loggedUser = await getUserInfo(uid); 26 | 27 | if (!loggedUser.processCompleted) { 28 | console.log("Falta username"); 29 | navigate("/choose-username"); 30 | } else { 31 | console.log("Usuario logueado completo"); 32 | onUserLoggedIn(loggedUser); 33 | } 34 | } else { 35 | await registerNewUser({ 36 | uid: user.uid, 37 | displayName: user.displayName, 38 | profilePicture: "", 39 | username: "", 40 | processCompleted: false, 41 | }); 42 | navigate("/choose-username"); 43 | } 44 | } else { 45 | onUserNotLoggedIn(); 46 | } 47 | }); 48 | }, []); 49 | 50 | return
{children}
; 51 | } 52 | -------------------------------------------------------------------------------- /src/components/dashboardWrapper.jsx: -------------------------------------------------------------------------------- 1 | import { Link } from "react-router-dom"; 2 | import style from "./dashboardWrapper.module.css"; 3 | 4 | export default function dashboardWrapper({ children }) { 5 | return ( 6 |
7 | 15 | 16 |
{children}
17 |
18 | ); 19 | } 20 | -------------------------------------------------------------------------------- /src/components/dashboardWrapper.module.css: -------------------------------------------------------------------------------- 1 | .nav { 2 | background-color: white; 3 | display: flex; 4 | align-items: center; 5 | padding: 0 20px; 6 | 7 | box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1); 8 | } 9 | 10 | .nav a { 11 | color: #222; 12 | text-decoration: none; 13 | padding: 25px 25px; 14 | border-bottom: solid 2px transparent; 15 | } 16 | 17 | .nav a:hover { 18 | border-bottom: solid 2px #222; 19 | } 20 | -------------------------------------------------------------------------------- /src/components/link.jsx: -------------------------------------------------------------------------------- 1 | import style from "./link.module.css"; 2 | import { deleteLink } from "../firebase/firebase"; 3 | import { useEffect, useRef, useState } from "react"; 4 | 5 | export default function Link({ 6 | docId, 7 | title, 8 | url, 9 | onDeleteLink, 10 | onUpdateLink, 11 | }) { 12 | const [currentTitle, setTitle] = useState(title); 13 | const [currentUrl, setUrl] = useState(url); 14 | 15 | const [editTitle, setEditTitle] = useState(false); 16 | const [editUrl, setEditUrl] = useState(false); 17 | 18 | const refTitle = useRef(null); 19 | const refUrl = useRef(null); 20 | 21 | useEffect(() => { 22 | if (refTitle.current) { 23 | refTitle.current.focus(); 24 | } 25 | }, [editTitle]); 26 | 27 | useEffect(() => { 28 | if (refUrl.current) { 29 | refUrl.current.focus(); 30 | } 31 | }, [editUrl]); 32 | 33 | async function handleRemoveLink() { 34 | await deleteLink(docId); 35 | onDeleteLink(docId); 36 | } 37 | 38 | function handleEditTitle() { 39 | setEditTitle(true); 40 | } 41 | 42 | function handleEditUrl() { 43 | setEditUrl(true); 44 | } 45 | 46 | function handleOnBlurTitle(e) { 47 | setEditTitle(false); 48 | onUpdateLink(docId, e.target.value, currentUrl); 49 | } 50 | function handleOnBlurUrl(e) { 51 | setEditUrl(false); 52 | onUpdateLink(docId, currentTitle, e.target.value); 53 | } 54 | 55 | function handleOnChangeTitle(e) { 56 | setTitle(e.target.value); 57 | } 58 | 59 | function handleOnChangeUrl(e) { 60 | setUrl(e.target.value); 61 | } 62 | 63 | return ( 64 |
65 |
66 |
67 | {editTitle ? ( 68 | <> 69 | 75 | 76 | ) : ( 77 | <> 78 | 81 | {currentTitle} 82 | 83 | )} 84 |
85 |
86 | {editUrl ? ( 87 | <> 88 | 94 | 95 | ) : ( 96 | <> 97 | 100 | {currentUrl} 101 | 102 | )} 103 |
104 |
105 |
106 | 109 |
110 |
111 | ); 112 | } 113 | -------------------------------------------------------------------------------- /src/components/link.module.css: -------------------------------------------------------------------------------- 1 | .linksContainer { 2 | display: flex; 3 | flex-direction: column; 4 | gap: 20px; 5 | } 6 | 7 | .link { 8 | background-color: white; 9 | border-radius: 3px; 10 | 11 | display: flex; 12 | align-items: center; 13 | box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1); 14 | height: 79px; 15 | } 16 | 17 | .linkActions { 18 | display: flex; 19 | align-content: stretch; 20 | align-items: stretch; 21 | height: 100%; 22 | } 23 | 24 | .linkInfo { 25 | padding: 10px 15px; 26 | width: 100%; 27 | } 28 | 29 | .linkTitle { 30 | font-weight: bolder; 31 | padding: 5px 0; 32 | } 33 | 34 | .linkUrl { 35 | padding: 5px 0; 36 | color: #555; 37 | text-transform: lowercase; 38 | } 39 | 40 | .btnEdit { 41 | border: none; 42 | padding: 0; 43 | margin: 0; 44 | background-color: transparent; 45 | height: 16px; 46 | color: #ccc; 47 | cursor: pointer; 48 | } 49 | .btnDelete { 50 | border: none; 51 | margin: 0; 52 | background-color: transparent; 53 | color: #222; 54 | cursor: pointer; 55 | min-height: 100%; 56 | padding: 0 20px; 57 | flex: 1; 58 | } 59 | .btnDelete:hover { 60 | background-color: red; 61 | color: white; 62 | } 63 | .btnDelete .material-icons { 64 | font-size: 24px; 65 | } 66 | .btnEdit:hover { 67 | color: #222; 68 | } 69 | -------------------------------------------------------------------------------- /src/components/loading.jsx: -------------------------------------------------------------------------------- 1 | export default function Loading() { 2 | return
Loading
; 3 | } 4 | -------------------------------------------------------------------------------- /src/components/publicLink.jsx: -------------------------------------------------------------------------------- 1 | import style from "./publicLink.module.css"; 2 | 3 | export default function PublicLink({ link }) { 4 | return ( 5 | 11 |
{link.title}
12 |
13 | ); 14 | } 15 | -------------------------------------------------------------------------------- /src/components/publicLink.module.css: -------------------------------------------------------------------------------- 1 | .publicLinksContainer { 2 | display: flex; 3 | flex-direction: column; 4 | gap: 15px; 5 | } 6 | 7 | .publicLinkContainer { 8 | color: black; 9 | text-decoration: none; 10 | display: block; 11 | padding: 20px; 12 | background-color: #facf23; 13 | border-radius: 10px; 14 | font-weight: bold; 15 | } 16 | 17 | .publicLinkContainer:hover { 18 | background-color: #e7be1a; 19 | } 20 | -------------------------------------------------------------------------------- /src/firebase/firebase.js: -------------------------------------------------------------------------------- 1 | //@typecheck 2 | 3 | import { initializeApp } from "firebase/app"; 4 | import { 5 | getFirestore, 6 | collection, 7 | addDoc, 8 | getDocs, 9 | doc, 10 | getDoc, 11 | query, 12 | where, 13 | setDoc, 14 | deleteDoc, 15 | } from "firebase/firestore"; 16 | import { getAuth } from "firebase/auth"; 17 | import { 18 | getStorage, 19 | ref, 20 | uploadBytes, 21 | getDownloadURL, 22 | getBytes, 23 | } from "firebase/storage"; 24 | 25 | const firebaseConfig = { 26 | apiKey: process.env.REACT_APP_APIKEY, 27 | authDomain: process.env.REACT_APP_AUTHDOMAIN, 28 | projectId: process.env.REACT_APP_PROJECTID, 29 | storageBucket: process.env.REACT_APP_STORAGEBUCKET, 30 | messagingSenderId: process.env.REACT_APP_MESSAGINGSENDERID, 31 | appId: process.env.REACT_APP_APPID, 32 | }; 33 | 34 | // Initialize Firebase 35 | export const app = initializeApp(firebaseConfig); 36 | export const auth = getAuth(app); 37 | export const db = getFirestore(); 38 | export const storage = getStorage(); 39 | 40 | export async function registerNewUser(user) { 41 | try { 42 | const usersRef = collection(db, "users"); 43 | await setDoc(doc(usersRef, user.uid), user); 44 | } catch (e) { 45 | console.error("Error adding document: ", e); 46 | } 47 | } 48 | 49 | export async function getUserInfo(uid) { 50 | const docRef = doc(db, "users", uid); 51 | const docSnap = await getDoc(docRef); 52 | return docSnap.data(); 53 | } 54 | 55 | export async function userExists(uid) { 56 | const docRef = doc(db, "users", uid); 57 | const docSnap = await getDoc(docRef); 58 | 59 | return docSnap.exists(); 60 | } 61 | 62 | export async function updateUser(user) { 63 | console.log(user); 64 | try { 65 | const usersRef = collection(db, "users"); 66 | await setDoc(doc(usersRef, user.uid), user); 67 | } catch (e) { 68 | console.error("Error adding document: ", e); 69 | } 70 | } 71 | 72 | export async function fetchLinkData(uid) { 73 | const links = []; 74 | const q = query(collection(db, "links"), where("uid", "==", uid)); 75 | 76 | const querySnapshot = await getDocs(q); 77 | 78 | querySnapshot.forEach((doc) => { 79 | // doc.data() is never undefined for query doc snapshots 80 | const link = { ...doc.data() }; 81 | link.docId = doc.id; 82 | //console.log(doc.id, " => ", doc.data()); 83 | console.log(link); 84 | links.push(link); 85 | }); 86 | return links; 87 | } 88 | 89 | export async function insertNewLink(link) { 90 | try { 91 | const linksRef = collection(db, "links"); 92 | const res = await addDoc(linksRef, link); 93 | return res; 94 | } catch (e) { 95 | console.error("Error adding document: ", e); 96 | } 97 | } 98 | 99 | export async function existsUsername(username) { 100 | const users = []; 101 | const q = query(collection(db, "users"), where("username", "==", username)); 102 | 103 | const querySnapshot = await getDocs(q); 104 | 105 | querySnapshot.forEach((doc) => { 106 | console.log(doc.id, " => ", doc.data()); 107 | users.push(doc.data()); 108 | }); 109 | return users.length > 0 ? users[0].uid : null; 110 | } 111 | 112 | export async function getUserPublicProfileInfo(uid) { 113 | const profileInfo = await getUserInfo(uid); 114 | const linksInfo = await fetchLinkData(uid); 115 | return { 116 | profile: profileInfo, 117 | links: linksInfo, 118 | }; 119 | } 120 | 121 | export async function getUserProfilePhoto(usernamePhoto) { 122 | // Create a child reference 123 | const imagesRef = ref(storage, `images/${usernamePhoto}`); 124 | // imagesRef now points to 'images' 125 | } 126 | 127 | export async function setUserProfilePhoto(uid, file) { 128 | // Create a root reference 129 | const storage = getStorage(); 130 | 131 | // Create a reference to 'mountains.jpg' 132 | //const mountainsRef = ref(storage, username); 133 | 134 | // Create a reference to 'images/mountains.jpg' 135 | const mountainImagesRef = ref(storage, `images/${uid}`); 136 | 137 | // While the file names are the same, the references point to different files 138 | //mountainsRef.name === mountainImagesRef.name; // true 139 | //mountainsRef.fullPath === mountainImagesRef.fullPath; // false 140 | // 'file' comes from the Blob or File API 141 | const res = await uploadBytes(mountainImagesRef, file); 142 | console.log("file uploaded", res); 143 | return res; 144 | } 145 | 146 | export async function getProfilePhotoUrl(profilePicture) { 147 | const profileRef = ref(storage, profilePicture); 148 | console.log(profilePicture); 149 | 150 | /* const url = await getDownloadURL( 151 | ref(storage, "images/MBr3m7RbiWSlnskhZ94EZ9Vkh542") 152 | ); */ 153 | const url = await getDownloadURL(profileRef); 154 | /* .then((url) => { 155 | // `url` is the download URL for 'images/stars.jpg' 156 | console.log("url", url); 157 | 158 | // Or inserted into an element 159 | const img = document.getElementById("myimg"); 160 | img.setAttribute("src", url); 161 | }) 162 | .catch((error) => { 163 | // Handle any errors 164 | }); */ 165 | console.log({ url }); 166 | return url; 167 | } 168 | 169 | export async function logout() { 170 | await auth.signOut(); 171 | } 172 | 173 | export async function deleteLink(docId) { 174 | await deleteDoc(doc(db, "links", docId)); 175 | } 176 | 177 | export async function updateLink(docId, link) { 178 | const res = await setDoc(doc(db, "links", docId), link); 179 | console.log("update link", docId, link, res); 180 | } 181 | -------------------------------------------------------------------------------- /src/index.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen", 4 | "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue", 5 | sans-serif; 6 | -webkit-font-smoothing: antialiased; 7 | -moz-osx-font-smoothing: grayscale; 8 | background-color: #f5f6f8; 9 | } 10 | 11 | code { 12 | font-family: source-code-pro, Menlo, Monaco, Consolas, "Courier New", 13 | monospace; 14 | } 15 | 16 | .main-container { 17 | width: 500px; 18 | margin: 0 auto; 19 | } 20 | 21 | .material-icons { 22 | font-family: "Material Icons"; 23 | font-weight: normal; 24 | font-style: normal; 25 | font-size: 16px; /* Preferred icon size */ 26 | display: inline-block; 27 | line-height: 1; 28 | text-transform: none; 29 | letter-spacing: normal; 30 | word-wrap: normal; 31 | white-space: nowrap; 32 | direction: ltr; 33 | 34 | /* Support for all WebKit browsers. */ 35 | -webkit-font-smoothing: antialiased; 36 | /* Support for Safari and Chrome. */ 37 | text-rendering: optimizeLegibility; 38 | 39 | /* Support for Firefox. */ 40 | -moz-osx-font-smoothing: grayscale; 41 | 42 | /* Support for IE. */ 43 | font-feature-settings: "liga"; 44 | } 45 | 46 | .btn { 47 | font-weight: bold; 48 | font-size: 18px; 49 | border: none; 50 | background-color: rgb(13, 110, 184); 51 | color: white; 52 | padding: 15px 15px; 53 | border-radius: 3px; 54 | } 55 | 56 | .main-container input[type="text"] { 57 | border: solid 1px #ccc; 58 | border-radius: 3px; 59 | padding: 8px 10px; 60 | } 61 | 62 | .main-container label { 63 | padding-top: 5px; 64 | } 65 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import ReactDOM from "react-dom"; 3 | import "./index.css"; 4 | import App from "./App"; 5 | import reportWebVitals from "./reportWebVitals"; 6 | import { BrowserRouter, Routes, Route } from "react-router-dom"; 7 | import Login from "./routes/login"; 8 | import Dashboard from "./routes/dashboard"; 9 | import Profile from "./routes/profile"; 10 | import UsernameView from "./routes/usernameView"; 11 | import LoginV2 from "./routes/loginv2"; 12 | import EditProfile from "./routes/editProfile"; 13 | import SignOut from "./routes/signout"; 14 | 15 | ReactDOM.render( 16 | 17 | 18 | } /> 19 | } /> 20 | } /> 21 | } /> 22 | } /> 23 | } /> 24 | } /> 25 | 26 | , 27 | document.getElementById("root") 28 | ); 29 | 30 | // If you want to start measuring performance in your app, pass a function 31 | // to log results (for example: reportWebVitals(console.log)) 32 | // or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals 33 | reportWebVitals(); 34 | -------------------------------------------------------------------------------- /src/logo.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /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/routes/dashboard.jsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useRef, useState } from "react"; 2 | import { onAuthStateChanged } from "firebase/auth"; 3 | import { 4 | auth, 5 | getUserInfo, 6 | insertNewLink, 7 | userExists, 8 | fetchLinkData, 9 | logout, 10 | updateLink, 11 | } from "../firebase/firebase"; 12 | import { useNavigate } from "react-router-dom"; 13 | import { v4 as uuid } from "uuid"; 14 | import Link from "../components/link"; 15 | import DashboardWrapper from "../components/dashboardWrapper"; 16 | 17 | import style from "./dashboard.module.css"; 18 | import styleLinks from "../components/link.module.css"; 19 | 20 | export default function Dashboard() { 21 | const [currentUser, setCurrentUser] = useState(null); 22 | const [links, setLinks] = useState([]); 23 | const [link, setLink] = useState({}); 24 | 25 | let navigate = useNavigate(); 26 | 27 | useEffect(() => { 28 | onAuthStateChanged(auth, callBackAuthState); 29 | }, []); 30 | 31 | async function callBackAuthState(user) { 32 | if (user) { 33 | const uid = user.uid; 34 | console.log(user); 35 | 36 | if (userExists(user.uid)) { 37 | const loggedUser = await getUserInfo(uid); 38 | setCurrentUser(loggedUser); 39 | if (loggedUser.username === "") { 40 | console.log("Falta username"); 41 | navigate("/login"); 42 | } else { 43 | console.log("Ya tiene username"); 44 | const asyncLinks = await fetchLinkData(uid); 45 | setLinks([...asyncLinks]); 46 | } 47 | } else { 48 | navigate("/login"); 49 | } 50 | } else { 51 | navigate("/login"); 52 | } 53 | } 54 | 55 | function handleOnDeleteLink(docId) { 56 | const tmp = links.filter((link) => link.docId !== docId); 57 | setLinks([...tmp]); 58 | } 59 | function handleOnUpdateLink(docId, title, url) { 60 | const link = links.find((item) => item.docId === docId); 61 | link.title = title; 62 | link.url = url; 63 | updateLink(docId, link); 64 | } 65 | 66 | function renderLinks() { 67 | if (links.length > 0) { 68 | return links.map((link) => ( 69 | 77 | )); 78 | } 79 | } 80 | 81 | function handleInput(e) { 82 | const value = e.target.value; 83 | 84 | if (e.target.name === "url") { 85 | setLink({ 86 | url: value, 87 | title: link.title, 88 | }); 89 | } 90 | 91 | if (e.target.name === "title") { 92 | setLink({ 93 | url: link.url, 94 | title: value, 95 | }); 96 | } 97 | } 98 | 99 | function handleSubmit(e) { 100 | e.preventDefault(); 101 | addLink(); 102 | } 103 | 104 | async function addLink() { 105 | if (link && link.url && link.title) { 106 | const newLink = { 107 | id: uuid(), 108 | title: link.title, 109 | url: link.url, 110 | uid: currentUser.uid, 111 | }; 112 | const res = await insertNewLink({ ...newLink }); 113 | newLink.docId = res.id; 114 | setLink({ 115 | url: "", 116 | title: "", 117 | }); 118 | setLinks([...links, newLink]); 119 | } 120 | } 121 | 122 | return ( 123 | 124 |

Dashboard

125 | 126 |
127 | 128 | 135 | 136 | 143 | 144 |
145 | 146 |
{renderLinks()}
147 |
148 | ); 149 | } 150 | -------------------------------------------------------------------------------- /src/routes/dashboard.module.css: -------------------------------------------------------------------------------- 1 | .entryContainer { 2 | display: flex; 3 | flex-direction: column; 4 | gap: 10px; 5 | padding: 10px 0; 6 | margin-bottom: 20px; 7 | } 8 | -------------------------------------------------------------------------------- /src/routes/editProfile.jsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useRef, useState } from "react"; 2 | import { 3 | setUserProfilePhoto, 4 | updateUser, 5 | getProfilePhotoUrl, 6 | } from "../firebase/firebase"; 7 | import DashboardWrapper from "../components/dashboardWrapper"; 8 | import { useNavigate } from "react-router-dom"; 9 | 10 | import style from "./editProfile.module.css"; 11 | import AuthProvider from "../components/authProvider"; 12 | 13 | export default function EditProfile() { 14 | const ref = useRef(null); 15 | const [profileUrl, setProfileUrl] = useState(undefined); 16 | const [currentUser, setCurrentUser] = useState(null); 17 | 18 | let navigate = useNavigate(); 19 | 20 | function handleOnClickProfilePicture() { 21 | ref.current.click(); 22 | } 23 | 24 | function handleOnChangeProfileImage(e) { 25 | console.log(e.target); 26 | 27 | var fileList = e.target.files; 28 | var fileReader = new FileReader(); 29 | 30 | if (fileReader && fileList && fileList.length) { 31 | fileReader.readAsArrayBuffer(fileList[0]); 32 | fileReader.onload = async function () { 33 | var imageData = fileReader.result; 34 | 35 | const res = await setUserProfilePhoto(currentUser.uid, imageData); 36 | 37 | if (res) { 38 | const tmpUser = { ...currentUser }; 39 | tmpUser.profilePicture = res.metadata.fullPath; 40 | setCurrentUser({ ...tmpUser }); 41 | await updateUser(currentUser); 42 | const url = await getProfilePhotoUrl(currentUser.profilePicture); 43 | setProfileUrl(url); 44 | //updateUserProfilePhoto(currentUser.uid, res.fullPath); 45 | } 46 | }; 47 | } 48 | } 49 | 50 | async function handleOnUserLoggedIn(loggedUser) { 51 | setCurrentUser(loggedUser); 52 | const url = await getProfilePhotoUrl(loggedUser.profilePicture); 53 | setProfileUrl(url); 54 | } 55 | 56 | function handleOnUserNotLoggedIn() { 57 | navigate("/login"); 58 | } 59 | 60 | return ( 61 | 65 | 66 |
67 |

Edit Profile Info

68 |
69 |
70 | 71 |
72 |
73 | 76 | 82 |
83 |
84 |
85 |
86 |
87 | ); 88 | } 89 | -------------------------------------------------------------------------------- /src/routes/editProfile.module.css: -------------------------------------------------------------------------------- 1 | .profilePictureContainer { 2 | padding: 10px 0; 3 | display: flex; 4 | align-items: center; 5 | gap: 20px; 6 | } 7 | 8 | .profilePictureContainer img { 9 | border-radius: 50%; 10 | width: 100px; 11 | height: 100px; 12 | background-color: #ccc; 13 | } 14 | 15 | .fileInput { 16 | display: none; 17 | } 18 | -------------------------------------------------------------------------------- /src/routes/login.jsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from "react"; 2 | import { GoogleAuthProvider, signInWithPopup } from "firebase/auth"; 3 | import { 4 | auth, 5 | getUserInfo, 6 | read, 7 | registerNewUser, 8 | updateUser, 9 | userExists, 10 | } from "../firebase/firebase"; 11 | import { onAuthStateChanged } from "firebase/auth"; 12 | import { useParams, useNavigate, useLocation } from "react-router-dom"; 13 | 14 | /* 15 | Stages: 16 | 0: initiated 17 | 1: loading 18 | 2: login completed 19 | 3: login but no username 20 | 4: not logged 21 | */ 22 | export default function Login() { 23 | const [currentUser, setCurrentUser] = useState(null); 24 | const [state, setState] = useState(0); 25 | let navigate = useNavigate(); 26 | 27 | useEffect(() => { 28 | onAuthStateChanged(auth, async (user) => { 29 | if (user) { 30 | // User is signed in, see docs for a list of available properties 31 | // https://firebase.google.com/docs/reference/js/firebase.User 32 | const uid = user.uid; 33 | console.log(user); 34 | 35 | if (userExists(user.uid)) { 36 | console.log("user registered"); 37 | const loggedUser = await getUserInfo(uid); 38 | console.log("loggedUser", loggedUser); 39 | setCurrentUser(loggedUser); 40 | if (!loggedUser.processCompleted) { 41 | console.log("Falta username"); 42 | 43 | navigate("/choose-username"); 44 | setState(3); 45 | } else { 46 | console.log("Ya tiene username"); 47 | navigate("/dashboard"); 48 | setState(2); 49 | } 50 | } else { 51 | console.log("Register"); 52 | 53 | registerNewUser({ 54 | uid: user.uid, 55 | displayName: user.displayName, 56 | profilePicture: "", 57 | username: "", 58 | processCompleted: false, 59 | }); 60 | 61 | setState(3); 62 | } 63 | } else { 64 | setState(4); 65 | } 66 | }); 67 | }, []); 68 | 69 | function handleAuth() { 70 | const googleProvider = new GoogleAuthProvider(); 71 | const signInWithGoogle = async () => { 72 | try { 73 | const res = await signInWithPopup(auth, googleProvider); 74 | setCurrentUser(res.user); 75 | registerNewUser(res.user); 76 | } catch (err) { 77 | console.error(err); 78 | alert(err.message); 79 | } 80 | }; 81 | signInWithGoogle(); 82 | } 83 | 84 | function handleInputUsername(e) { 85 | const tmpUser = currentUser; 86 | const value = e.target.value; 87 | tmpUser.username = value; 88 | setCurrentUser({ ...tmpUser }); 89 | } 90 | 91 | async function handleOnClickContinue() { 92 | if (currentUser.username !== "") { 93 | await updateUser(currentUser); 94 | setState(2); 95 | } 96 | } 97 | 98 | if (state === 1) { 99 | return
Loading...
; 100 | } 101 | 102 | if (state === 2) { 103 | return
Bienvenido {currentUser.displayName}
; 104 | } 105 | 106 | if (state === 3) { 107 | return ( 108 |
109 |

Bienvenido {currentUser.displayName}, te falta un username

110 |
111 | 112 |
113 |
114 | 115 |
116 |
117 | ); 118 | } 119 | if (state === 4) { 120 | return ( 121 |
122 | 123 |
124 | ); 125 | } 126 | 127 | return
Loading
; 128 | } 129 | -------------------------------------------------------------------------------- /src/routes/login.module.css: -------------------------------------------------------------------------------- 1 | .loginView { 2 | width: 100%; 3 | height: 100vh; 4 | display: flex; 5 | justify-content: center; 6 | align-items: center; 7 | text-align: center; 8 | } 9 | 10 | .provider { 11 | border: none; 12 | width: 300px; 13 | padding: 10px 0; 14 | color: white; 15 | background-color: blue; 16 | border-radius: 3px; 17 | text-align: center; 18 | } 19 | -------------------------------------------------------------------------------- /src/routes/loginv2.jsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from "react"; 2 | import { GoogleAuthProvider, signInWithPopup } from "firebase/auth"; 3 | import { 4 | auth, 5 | getUserInfo, 6 | read, 7 | registerNewUser, 8 | updateUser, 9 | userExists, 10 | } from "../firebase/firebase"; 11 | import { onAuthStateChanged } from "firebase/auth"; 12 | import { useParams, useNavigate, useLocation } from "react-router-dom"; 13 | import AuthProvider from "../components/authProvider"; 14 | import Loading from "../components/loading"; 15 | 16 | import style from "./login.module.css"; 17 | 18 | /* 19 | Stages: 20 | 0: initiated 21 | 1: loading 22 | 2: login completed 23 | 3: login but no username 24 | 4: not logged 25 | */ 26 | export default function LoginV2() { 27 | const navigate = useNavigate(); 28 | const [state, setState] = useState(1); 29 | 30 | function handleAuth() { 31 | const googleProvider = new GoogleAuthProvider(); 32 | const signInWithGoogle = async () => { 33 | try { 34 | const res = await signInWithPopup(auth, googleProvider); 35 | if (res) { 36 | registerNewUser(res.user); 37 | } 38 | } catch (err) { 39 | console.error(err); 40 | //alert(err.message); 41 | } 42 | }; 43 | signInWithGoogle(); 44 | } 45 | 46 | if (state === 4) { 47 | return ( 48 |
49 |
50 |

Link Tree

51 | 54 |
55 |
56 | ); 57 | } 58 | 59 | return ( 60 | { 62 | navigate("/dashboard"); 63 | }} 64 | onUserNotLoggedIn={() => { 65 | setState(4); 66 | }} 67 | > 68 | 69 | 70 | ); 71 | } 72 | -------------------------------------------------------------------------------- /src/routes/profile.jsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from "react"; 2 | import { Navigate, useNavigate, useParams } from "react-router-dom"; 3 | import Loading from "../components/loading"; 4 | import style from "./profile.module.css"; 5 | import styleLink from "../components/publicLink.module.css"; 6 | import { 7 | existsUsername, 8 | getUserPublicProfileInfo, 9 | getProfilePhotoUrl, 10 | } from "../firebase/firebase"; 11 | import PublicLink from "../components/publicLink"; 12 | /* 13 | Stages: 14 | 0: initiated 15 | 1: loading 16 | 2: login completed 17 | 3: login but no username 18 | 4: not logged 19 | 5: username exists 20 | 6: username correct 21 | 7: username does not exist 22 | */ 23 | export default function Profile() { 24 | const params = useParams(); 25 | const [profile, setProfile] = useState(undefined); 26 | const [profilePhotoUrl, setProfilePhotoUrl] = useState(undefined); 27 | const [state, setState] = useState(0); 28 | 29 | useEffect(async () => { 30 | setState(1); 31 | try { 32 | const userUID = await existsUsername(params.username); 33 | if (userUID) { 34 | const res = await getUserPublicProfileInfo(userUID); 35 | console.log("profile", res); 36 | setProfile(res); 37 | 38 | const url = await getProfilePhotoUrl(res.profile.profilePicture); 39 | setProfilePhotoUrl(url); 40 | setState(5); 41 | } else { 42 | setState(7); 43 | } 44 | } catch (error) { 45 | console.log(error); 46 | } 47 | }, []); 48 | 49 | if (state === 7) { 50 | return ( 51 |
52 |

Profile not found

53 |
54 | ); 55 | } 56 | 57 | if (state === 1) { 58 | return ; 59 | } 60 | 61 | return ( 62 |
63 |
64 | {profilePhotoUrl ? ( 65 | 66 | ) : ( 67 | "" 68 | )} 69 |
70 |

@{profile?.profile?.username}

71 |

{profile?.profile?.displayName}

72 |
73 | {profile?.links?.map((link) => ( 74 | 75 | ))} 76 |
77 |
78 | ); 79 | } 80 | -------------------------------------------------------------------------------- /src/routes/profile.module.css: -------------------------------------------------------------------------------- 1 | .profileContainer { 2 | width: 500px; 3 | margin: 100px auto; 4 | text-align: center; 5 | } 6 | 7 | .profilePicture img { 8 | width: 150px; 9 | height: 150px; 10 | border-radius: 50%; 11 | } 12 | -------------------------------------------------------------------------------- /src/routes/signout.jsx: -------------------------------------------------------------------------------- 1 | import { useEffect } from "react"; 2 | import { useNavigate } from "react-router-dom"; 3 | import AuthProvider from "../components/authProvider"; 4 | 5 | import { logout } from "../firebase/firebase"; 6 | 7 | export default function SignOut() { 8 | useEffect(() => {}, []); 9 | const navigate = useNavigate(); 10 | 11 | return ( 12 | { 14 | await logout(); 15 | navigate("/login"); 16 | }} 17 | onUserNotLoggedIn={() => { 18 | navigate("/login"); 19 | }} 20 | > 21 | ); 22 | } 23 | -------------------------------------------------------------------------------- /src/routes/usernameView.jsx: -------------------------------------------------------------------------------- 1 | import { useState, useEffect } from "react"; 2 | import { onAuthStateChanged } from "firebase/auth"; 3 | import { 4 | useParams, 5 | useNavigate, 6 | useLocation, 7 | Link, 8 | Navigate, 9 | } from "react-router-dom"; 10 | import { 11 | auth, 12 | existsUsername, 13 | getUserInfo, 14 | read, 15 | registerNewUser, 16 | updateUser, 17 | userExists, 18 | } from "../firebase/firebase"; 19 | import Loading from "../components/loading"; 20 | 21 | /* 22 | Stages: 23 | 0: initiated 24 | 1: loading 25 | 2: login completed 26 | 3: login but no username 27 | 4: not logged 28 | 5: username exists 29 | 6: username correct 30 | */ 31 | export default function UsernameView() { 32 | const [currentUser, setCurrentUser] = useState(null); 33 | const [state, setState] = useState(0); 34 | let navigate = useNavigate(); 35 | 36 | useEffect(() => { 37 | setState(1); 38 | onAuthStateChanged(auth, callBackAuthState); 39 | }, []); 40 | 41 | async function callBackAuthState(user) { 42 | if (user) { 43 | const uid = user.uid; 44 | console.log(user); 45 | 46 | if (userExists(user.uid)) { 47 | const loggedUser = await getUserInfo(uid); 48 | setCurrentUser(loggedUser); 49 | if (!loggedUser.processCompleted) { 50 | setState(3); 51 | console.log("Falta username"); 52 | } else { 53 | console.log("Ya tiene username", state); 54 | navigate("/dashboard"); 55 | } 56 | } else { 57 | navigate("/login"); 58 | } 59 | } else { 60 | navigate("/login"); 61 | } 62 | } 63 | 64 | function handleInputUsername(e) { 65 | const tmpUser = currentUser; 66 | const value = e.target.value; 67 | tmpUser.username = value; 68 | setCurrentUser({ ...tmpUser }); 69 | } 70 | 71 | async function handleOnClickContinue() { 72 | if (currentUser.username !== "") { 73 | const exists = await existsUsername(currentUser.username); 74 | if (exists) { 75 | setState(5); 76 | } else { 77 | const tmpUser = currentUser; 78 | tmpUser.processCompleted = true; 79 | await updateUser(tmpUser); 80 | setState(6); 81 | } 82 | } 83 | } 84 | 85 | if (state === 6) { 86 | return ( 87 |
88 |

Congratulations! now you can go to the dashboard

89 | 90 | Continue 91 |
92 | ); 93 | } 94 | 95 | if (state === 3) { 96 | return ( 97 |
98 |

Bienvenido {setCurrentUser.displayName}, te falta un username

99 |
100 | 101 |
102 |
103 | 104 |
105 |
106 | ); 107 | } 108 | 109 | return ; 110 | } 111 | -------------------------------------------------------------------------------- /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 | --------------------------------------------------------------------------------