├── .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 | You need to enable JavaScript to run this app.
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 |
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 |
8 | Logotipo
9 | Links
10 |
11 | Profile
12 |
13 | Sign out
14 |
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 |
105 |
106 |
107 | delete
108 |
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 |
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 |
74 | Choose new profile picture
75 |
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 | Continue
115 |
116 |
117 | );
118 | }
119 | if (state === 4) {
120 | return (
121 |
122 | handleAuth()}>Login
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 | handleAuth()} className={style.provider}>
52 | Login with Google
53 |
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 | Continue
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 |
--------------------------------------------------------------------------------