├── .eslintrc
├── .gitignore
├── .vscode
└── settings.json
├── README.md
├── components
├── 404.jsx
├── AddMessageModal.jsx
├── Loading.jsx
├── Navbar.jsx
├── NotSignedIn.jsx
├── SignIn.jsx
├── StickyNote.jsx
├── StickyNoteGrid.jsx
├── StickyNoteWall.jsx
└── WallGrid.jsx
├── jsconfig.json
├── next.config.js
├── package.json
├── pages
├── _app.jsx
├── _document.jsx
├── api
│ ├── getUser.js
│ └── getUserByEmail.js
├── index.jsx
├── new.jsx
├── profile
│ └── [username]
│ │ ├── edit.jsx
│ │ └── index.jsx
├── sign-in.jsx
└── walls
│ └── [[...username]].jsx
├── postcss.config.js
├── public
├── favicon.png
├── image.png
└── vercel.svg
├── styles
└── globals.css
├── tailwind.config.js
├── utils
├── admin.js
├── firebase.js
├── useUser.jsx
├── userCookies.js
└── userExists.js
└── yarn.lock
/.eslintrc:
--------------------------------------------------------------------------------
1 | {
2 | "extends": ["next", "next/core-web-vitals"],
3 | "rules": {
4 | "react-hooks/exhaustive-deps": "off",
5 | "react/no-unescaped-entities": "off"
6 | }
7 | }
8 |
--------------------------------------------------------------------------------
/.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 | # next.js
12 | /.next/
13 | /out/
14 |
15 | # production
16 | /build
17 |
18 | # misc
19 | .DS_Store
20 | *.pem
21 |
22 | # debug
23 | npm-debug.log*
24 | yarn-debug.log*
25 | yarn-error.log*
26 |
27 | # local env files
28 | .env.local
29 | .env.development.local
30 | .env.test.local
31 | .env.production.local
32 |
33 | # vercel
34 | .vercel
35 |
--------------------------------------------------------------------------------
/.vscode/settings.json:
--------------------------------------------------------------------------------
1 | {
2 | "css.validate": false,
3 | "editor.tabSize": 2,
4 | "editor.defaultFormatter": "esbenp.prettier-vscode",
5 | "editor.formatOnSave": true
6 | }
7 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app).
2 |
3 | ## Getting Started
4 |
5 | First, run the development server:
6 |
7 | ```bash
8 | npm run dev
9 | # or
10 | yarn dev
11 | ```
12 |
13 | Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
14 |
15 | You can start editing the page by modifying `pages/index.js`. The page auto-updates as you edit the file.
16 |
17 | [API routes](https://nextjs.org/docs/api-routes/introduction) can be accessed on [http://localhost:3000/api/hello](http://localhost:3000/api/hello). This endpoint can be edited in `pages/api/hello.js`.
18 |
19 | The `pages/api` directory is mapped to `/api/*`. Files in this directory are treated as [API routes](https://nextjs.org/docs/api-routes/introduction) instead of React pages.
20 |
21 | ## Learn More
22 |
23 | To learn more about Next.js, take a look at the following resources:
24 |
25 | - [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API.
26 | - [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.
27 |
28 | You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your feedback and contributions are welcome!
29 |
30 | ## Deploy on Vercel
31 |
32 | The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js.
33 |
34 | Check out our [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more details.
35 |
--------------------------------------------------------------------------------
/components/404.jsx:
--------------------------------------------------------------------------------
1 | import Link from "next/link";
2 | const FourOhFour = () => {
3 | return (
4 |
13 | );
14 | };
15 |
16 | export default FourOhFour;
17 |
--------------------------------------------------------------------------------
/components/AddMessageModal.jsx:
--------------------------------------------------------------------------------
1 | import { Transition, Dialog, RadioGroup } from "@headlessui/react";
2 | import { useRouter } from "next/router";
3 | import { Fragment, useState } from "react";
4 | import firebase from "utils/firebase";
5 | import useUser from "utils/useUser";
6 | import { FiCheck } from "react-icons/fi";
7 |
8 | const backgroundColors = [
9 | "bg-indigo-600",
10 | "bg-sky-500",
11 | "bg-yellow-200",
12 | "bg-fuchsia-400",
13 | ];
14 |
15 | const textColors = [
16 | "text-white",
17 | "text-gray-900",
18 | "text-gray-900",
19 | "text-gray-900",
20 | ];
21 |
22 | const rotations = [
23 | "rotate-0",
24 | "rotate-1",
25 | "rotate-2",
26 | "rotate-3",
27 | "-rotate-1",
28 | "-rotate-2",
29 | "-rotate-3",
30 | ];
31 |
32 | const AddMessageModal = ({ onClose, isOpen, username, wallId }) => {
33 | const [addedMessage, setAddedMessage] = useState("");
34 | const [colorIdx, setColorIdx] = useState(0);
35 | const { user } = useUser();
36 | const router = useRouter();
37 | const addMessage = async () => {
38 | if (addedMessage === "") {
39 | return;
40 | }
41 | const db = firebase.firestore();
42 | await db
43 | .collection("walls")
44 | .doc(wallId)
45 | .update({
46 | messages: firebase.firestore.FieldValue.arrayUnion({
47 | user: user.uid,
48 | message: addedMessage,
49 | timestamp: Date.now(),
50 | backgroundColor: backgroundColors[colorIdx],
51 | textColor: textColors[colorIdx],
52 | rotation: rotations[Math.floor(Math.random() * rotations.length)],
53 | }),
54 | });
55 | onClose();
56 | router.reload();
57 | };
58 | return (
59 | <>
60 |
61 |
187 |
188 | >
189 | );
190 | };
191 |
192 | export default AddMessageModal;
193 |
--------------------------------------------------------------------------------
/components/Loading.jsx:
--------------------------------------------------------------------------------
1 | const Loading = () => {
2 | return (
3 |
4 | Loading...
5 |
6 | );
7 | };
8 |
9 | export default Loading;
10 |
--------------------------------------------------------------------------------
/components/Navbar.jsx:
--------------------------------------------------------------------------------
1 | import useUser from "utils/useUser";
2 | import Link from "next/link";
3 | import { FiGithub } from "react-icons/fi";
4 |
5 | const Navbar = () => {
6 | const { user, logout } = useUser();
7 | return (
8 |
42 | );
43 | };
44 |
45 | export default Navbar;
46 |
--------------------------------------------------------------------------------
/components/NotSignedIn.jsx:
--------------------------------------------------------------------------------
1 | import Link from "next/link";
2 | const NotSignedIn = ({ message = "to view this page." }) => {
3 | return (
4 |
5 | You're not signed in!{" "}
6 |
7 |
8 | Sign In
9 |
10 | {" "}
11 | {message}
12 |
13 | );
14 | };
15 | export default NotSignedIn;
16 |
--------------------------------------------------------------------------------
/components/SignIn.jsx:
--------------------------------------------------------------------------------
1 | import StyledFirebaseAuth from "react-firebaseui/StyledFirebaseAuth";
2 | import firebase from "utils/firebase";
3 | import { useEffect, useState } from "react";
4 | import { useRouter } from "next/router";
5 | import useUser from "utils/useUser";
6 | import { FcGoogle } from "react-icons/fc";
7 | import { FiMail } from "react-icons/fi";
8 | import userExists from "utils/userExists";
9 |
10 | const processUser = async ({ additionalUserInfo, user }, newUsername) => {
11 | const db = firebase.firestore();
12 | if (additionalUserInfo.isNewUser) {
13 | const id = await db
14 | .collection("walls")
15 | .add({
16 | messages: [],
17 | creator: user.uid,
18 | name: "",
19 | })
20 | .then((doc) => doc.id);
21 | let needsUpdateName = false;
22 | let updatedName = user.displayName;
23 | if (user.displayName) {
24 | // logged in with google
25 | // need to check if username has been used already
26 | const MAX_TRIES = 5;
27 | // if the username already exists
28 | if ((await userExists(user.displayName, false)).exists) {
29 | // try to get a new name by appending a random number
30 | for (let i = 0; i < MAX_TRIES; i++) {
31 | const newName =
32 | user.displayName +
33 | "-" +
34 | Math.floor(Math.random() * 10000).toString();
35 | if (!(await userExists(newName, false)).exists) {
36 | needsUpdateName = true;
37 | updatedName = newName;
38 | break;
39 | }
40 | }
41 | if (!needsUpdateName) {
42 | // rip, hopefully never happens
43 | needsUpdateName = true;
44 | updatedName = user.uid;
45 | }
46 | }
47 | }
48 | // if the user doesn't have a photo (email sign in)
49 | if (!user.photoURL) {
50 | await user.updateProfile({
51 | photoURL: `https://robohash.org/${user.uid}?set=set4`,
52 | });
53 | }
54 | // if the user doesn't have a displayName (email sign in or duplicate)
55 | if (!user.displayName || needsUpdateName) {
56 | const nameToUpdateTo = newUsername || updatedName;
57 | await user.updateProfile({
58 | displayName: nameToUpdateTo,
59 | });
60 | }
61 | await db
62 | .collection("users")
63 | .doc(user.uid)
64 | .set({
65 | walls: [id],
66 | username: user.displayName,
67 | photo: user.photoURL || `https://robohash.org/${user.uid}?set=set4`,
68 | });
69 | } else {
70 | // user might exist but signed in with email first
71 | const existed = await db
72 | .collection("users")
73 | .doc(user.uid)
74 | .get()
75 | .then((doc) => {
76 | if (doc.exists) {
77 | const data = doc.data();
78 | return { photo: data.photo, username: data.username };
79 | }
80 | return false;
81 | });
82 | if (existed) {
83 | await user.updateProfile({
84 | photoURL: existed.photo,
85 | displayName: existed.username,
86 | });
87 | }
88 | }
89 | };
90 |
91 | const Card = ({ children }) => (
92 |
95 | );
96 |
97 | const defaults = {
98 | email: "",
99 | username: "",
100 | password: "",
101 | choseEmailSignIn: false,
102 | enteredEmail: false,
103 | emailValid: true,
104 | canUseEmail: false,
105 | newUser: true,
106 | usernameAlreadyUsed: false,
107 | signInError: "",
108 | };
109 |
110 | const SignIn = () => {
111 | const { signInWithGoogle, signInWithEmail, createUserWithEmail } = useUser();
112 | const router = useRouter();
113 |
114 | const [email, setEmail] = useState(defaults.email);
115 | const [username, setUsername] = useState(defaults.username);
116 | const [password, setPassword] = useState(defaults.password);
117 |
118 | const [choseEmailSignIn, setChoseEmailSignIn] = useState(
119 | defaults.choseEmailSignIn
120 | );
121 | const [enteredEmail, setEnteredEmail] = useState(defaults.enteredEmail);
122 | const [emailValid, setEmailValid] = useState(defaults.emailValid);
123 | const [canUseEmail, setCanUseEmail] = useState(defaults.canUseEmail);
124 | const [newUser, setNewUser] = useState(defaults.newUser);
125 | const [usernameAlreadyUsed, setUsernameAlreadyUsed] = useState(
126 | defaults.usernameAlreadyUsed
127 | );
128 | const [signInError, setSignInError] = useState(defaults.signInError);
129 |
130 | const [loading, setLoading] = useState(false);
131 |
132 | const checkForUserEmail = async () => {
133 | const info = await fetch("/api/getUserByEmail", {
134 | method: "POST",
135 | body: JSON.stringify({ email }),
136 | }).then((res) => res.json());
137 | setNewUser(!info.exists);
138 | setCanUseEmail(!info.otherAccount);
139 | };
140 |
141 | const validateEmail = () => {
142 | if (!email) {
143 | setEmailValid(true);
144 | return;
145 | }
146 | const regex =
147 | /^(?:[a-z0-9!#$%&'*+/=?^_`{|}~-]+(?:\.[a-z0-9!#$%&'*+/=?^_`{|}~-]+)*|"(?:[\x01-\x08\x0b\x0c\x0e-\x1f\x21\x23-\x5b\x5d-\x7f]|\\[\x01-\x09\x0b\x0c\x0e-\x7f])*")@(?:(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?|\[(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?|[a-z0-9-]*[a-z0-9]:(?:[\x01-\x08\x0b\x0c\x0e-\x1f\x21-\x5a\x53-\x7f]|\\[\x01-\x09\x0b\x0c\x0e-\x7f])+)\])$/;
148 | setEmailValid(regex.test(email));
149 | };
150 |
151 | const validateUsername = async () => {
152 | const { exists } = await userExists(username, false);
153 | setUsernameAlreadyUsed(exists);
154 | };
155 |
156 | const createUser = () => {
157 | createUserWithEmail(email, password, username, processUser);
158 | };
159 |
160 | const logInUser = async () => {
161 | try {
162 | await signInWithEmail(email, password);
163 | setSignInError("");
164 | setPassword("");
165 | setUsername("");
166 | } catch (error) {
167 | console.log(error);
168 | setSignInError(error.message);
169 | }
170 | };
171 |
172 | if (loading) {
173 | return null;
174 | }
175 |
176 | if (enteredEmail) {
177 | if (canUseEmail) {
178 | return (
179 |
180 | Sign in with email
181 | {newUser && (
182 |
201 | )}
202 |
217 |
218 |
229 |
245 |
246 |
247 | );
248 | } else {
249 | return (
250 |
251 |
252 | You've already created an account with this email. Use Google to
253 | sign in.
254 |
255 |
271 |
272 | );
273 | }
274 | }
275 | if (choseEmailSignIn) {
276 | return (
277 |
278 | Sign in with email
279 |
296 |
297 |
307 |
320 |
321 |
322 | );
323 | }
324 | return (
325 |
326 |
327 |
339 |
349 |
350 |
351 | );
352 | };
353 |
354 | export default SignIn;
355 |
--------------------------------------------------------------------------------
/components/StickyNote.jsx:
--------------------------------------------------------------------------------
1 | import Image from "next/image";
2 | import Link from "next/link";
3 |
4 | const StickyNote = ({ text, className, userInfo }) => {
5 | const darkText = className.includes("text-gray-900");
6 | return (
7 |
30 | );
31 | };
32 |
33 | export default StickyNote;
34 |
--------------------------------------------------------------------------------
/components/StickyNoteGrid.jsx:
--------------------------------------------------------------------------------
1 | import { useEffect, useState } from "react";
2 | import firebase from "utils/firebase";
3 | import StickyNote from "./StickyNote";
4 | const StickyNoteGrid = ({ notes = [] }) => {
5 | const sorted = [...notes].sort((a, b) => b.timestamp - a.timestamp);
6 | const usersInWall = [...new Set(notes.map((note) => note.user))];
7 | const [userData, setUserData] = useState({});
8 | useEffect(() => {
9 | const getData = async () => {
10 | const db = firebase.firestore();
11 | const userDocs = {};
12 | for (const userInWall of usersInWall) {
13 | //console.log(userInWall);
14 | await db
15 | .collection("users")
16 | .doc(userInWall)
17 | .get()
18 | .then((doc) => {
19 | userDocs[doc.id] = doc.data();
20 | });
21 | }
22 | //console.log(userDocs);
23 | setUserData(userDocs);
24 | };
25 | getData();
26 | }, [notes]);
27 |
28 | if (!Object.keys(userData).length) {
29 | return null;
30 | }
31 | return (
32 |
37 | {sorted.map((note, idx) => (
38 |
44 | ))}
45 |
46 | );
47 | };
48 |
49 | export default StickyNoteGrid;
50 |
--------------------------------------------------------------------------------
/components/StickyNoteWall.jsx:
--------------------------------------------------------------------------------
1 | import { useRouter } from "next/router";
2 | import { useEffect, useState } from "react";
3 | import firebase from "utils/firebase";
4 | import useUser from "utils/useUser";
5 | import AddMessageModal from "./AddMessageModal";
6 | import StickyNoteGrid from "./StickyNoteGrid";
7 | import Link from "next/link";
8 | const StickyNoteWall = ({ wallId, username }) => {
9 | const [messages, setMessages] = useState([]);
10 | const { user } = useUser();
11 | const [isOpen, setIsOpen] = useState(false);
12 | const router = useRouter();
13 | const openModal = () => {
14 | setIsOpen(true);
15 | };
16 | const closeModal = () => {
17 | setIsOpen(false);
18 | };
19 | useEffect(() => {
20 | const getData = async () => {
21 | const db = firebase.firestore();
22 | const m = await db
23 | .collection("walls")
24 | .doc(wallId)
25 | .get()
26 | .then((doc) => doc.data().messages);
27 | setMessages(m);
28 | };
29 | if (wallId) {
30 | getData();
31 | }
32 | }, [wallId]);
33 | return (
34 |
35 | {user ? (
36 |
37 |
45 |
51 |
52 | ) : (
53 |
65 | )}
66 |
74 |
75 |
76 | );
77 | };
78 |
79 | export default StickyNoteWall;
80 |
--------------------------------------------------------------------------------
/components/WallGrid.jsx:
--------------------------------------------------------------------------------
1 | import Link from "next/link";
2 | import Image from "next/image";
3 | import { FiPlusCircle } from "react-icons/fi";
4 | const WallGrid = ({ walls, canAddNew = false }) => {
5 | return (
6 |
45 | );
46 | };
47 |
48 | export default WallGrid;
49 |
--------------------------------------------------------------------------------
/jsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "baseUrl": "."
4 | }
5 | }
6 |
--------------------------------------------------------------------------------
/next.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | reactStrictMode: true,
3 | images: {
4 | domains: ["lh3.googleusercontent.com", "robohash.org"],
5 | },
6 | };
7 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "sticky-notes",
3 | "version": "0.1.0",
4 | "private": true,
5 | "scripts": {
6 | "dev": "next dev -p 3141",
7 | "build": "next build",
8 | "start": "next start -p 3141",
9 | "lint": "next lint"
10 | },
11 | "dependencies": {
12 | "@headlessui/react": "^1.4.0",
13 | "@tailwindcss/forms": "^0.3.3",
14 | "firebase": "^8.8.1",
15 | "firebase-admin": "^9.11.0",
16 | "js-cookie": "^3.0.0",
17 | "next": "11.0.1",
18 | "react": "17.0.2",
19 | "react-dom": "17.0.2",
20 | "react-firebaseui": "^5.0.2",
21 | "react-icons": "^4.2.0",
22 | "react-typing-effect": "^2.0.5"
23 | },
24 | "devDependencies": {
25 | "autoprefixer": "^10.3.1",
26 | "eslint": "7.31.0",
27 | "eslint-config-next": "11.0.1",
28 | "postcss": "^8.3.6",
29 | "tailwindcss": "^2.2.7"
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/pages/_app.jsx:
--------------------------------------------------------------------------------
1 | import "../styles/globals.css";
2 | import { UserContextProvider } from "../utils/useUser";
3 | import Head from "next/head";
4 |
5 | import Navbar from "components/Navbar";
6 |
7 | const MyApp = ({ Component, pageProps }) => {
8 | return (
9 |
10 |
11 |
12 |
13 | );
14 | };
15 |
16 | export default MyApp;
17 |
--------------------------------------------------------------------------------
/pages/_document.jsx:
--------------------------------------------------------------------------------
1 | import Document, { Html, Head, Main, NextScript } from "next/document";
2 |
3 | class MyDocument extends Document {
4 | render() {
5 | return (
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 | );
17 | }
18 | }
19 |
20 | export default MyDocument;
21 |
--------------------------------------------------------------------------------
/pages/api/getUser.js:
--------------------------------------------------------------------------------
1 | import admin from "utils/admin";
2 | const handler = async (req, res) => {
3 | if (req.method !== "POST") {
4 | res.redirect(303, "/404");
5 | return;
6 | }
7 | const body = JSON.parse(req.body);
8 | const { userId, fields } = body;
9 | const userInfo = await admin
10 | .auth()
11 | .getUser(userId)
12 | .then((userRecord) => {
13 | return userRecord;
14 | })
15 | .catch((error) => {
16 | console.log("Error fetching user data:", error);
17 | res.status(400).json(error);
18 | return;
19 | });
20 | const filteredInfo = {};
21 | for (const field of fields) {
22 | filteredInfo[field] = userInfo[field];
23 | }
24 | res.status(200).json(filteredInfo);
25 | return;
26 | };
27 |
28 | export default handler;
29 |
--------------------------------------------------------------------------------
/pages/api/getUserByEmail.js:
--------------------------------------------------------------------------------
1 | import admin from "utils/admin";
2 | const handler = async (req, res) => {
3 | if (req.method !== "POST") {
4 | res.redirect(303, "/404");
5 | return;
6 | }
7 | const body = JSON.parse(req.body);
8 | const { email } = body;
9 | if (!email) {
10 | res.status(400).send("no email");
11 | return;
12 | }
13 | const emailExists = await admin
14 | .auth()
15 | .getUserByEmail(email)
16 | .then((user) => {
17 | const providers = user.providerData.map((p) => p.providerId);
18 | if (!providers.includes("password")) {
19 | return { exists: true, otherAccount: true };
20 | }
21 | return { exists: true, otherAccount: false };
22 | })
23 | .catch(() => ({
24 | exists: false,
25 | }));
26 | res.status(200).json(emailExists);
27 | return;
28 | };
29 |
30 | export default handler;
31 |
--------------------------------------------------------------------------------
/pages/index.jsx:
--------------------------------------------------------------------------------
1 | import Head from "next/head";
2 | import Image from "next/image";
3 | import useUser from "utils/useUser";
4 | import SignIn from "components/SignIn";
5 | import ReactTypingEffect from "react-typing-effect";
6 | import Link from "next/link";
7 | import firebase from "utils/firebase";
8 | import WallGrid from "components/WallGrid";
9 | import exampleimage from "../public/image.png";
10 |
11 | const Home = ({ walls }) => {
12 | const { user, logout } = useUser();
13 | return (
14 |
15 |
20 |
25 |
26 |
31 | Sticky Note Wall
32 |
33 |
34 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
77 |
78 |
83 | View walls
84 |
85 |
86 |
87 |
88 | );
89 | };
90 |
91 | export default Home;
92 |
93 | export const getServerSideProps = async () => {
94 | const db = firebase.firestore();
95 | const walls = {};
96 | await db
97 | .collection("walls")
98 | .get()
99 | .then((snap) =>
100 | snap.forEach((doc) => {
101 | const data = doc.data();
102 | walls[doc.id] = {
103 | creator: data.creator,
104 | name: data.name,
105 | };
106 | })
107 | );
108 | const users = {};
109 | await db
110 | .collection("users")
111 | .where("walls", "!=", [])
112 | .get()
113 | .then((snap) =>
114 | snap.forEach((doc) => {
115 | const data = doc.data();
116 | users[doc.id] = {
117 | username: data.username,
118 | photo: data.photo,
119 | };
120 | })
121 | );
122 | const wallArray = [];
123 | for (const [wallId, value] of Object.entries(walls)) {
124 | wallArray.push({
125 | id: wallId,
126 | ...users[value.creator],
127 | name: value.name,
128 | });
129 | }
130 | return {
131 | props: {
132 | walls: wallArray,
133 | },
134 | };
135 | };
136 |
--------------------------------------------------------------------------------
/pages/new.jsx:
--------------------------------------------------------------------------------
1 | import { useRouter } from "next/router";
2 | import { useState } from "react";
3 | import useUser from "utils/useUser";
4 | import firebase from "utils/firebase";
5 | import NotSignedIn from "components/NotSignedIn";
6 |
7 | const CreateNew = () => {
8 | const [wallName, setWallName] = useState("");
9 | const { user } = useUser();
10 | const router = useRouter();
11 | const createNewWall = async () => {
12 | if (!wallName) return;
13 | const db = firebase.firestore();
14 | const wallId = await db
15 | .collection("walls")
16 | .add({
17 | messages: [],
18 | creator: user.uid,
19 | name: wallName,
20 | })
21 | .then((doc) => doc.id);
22 | await db
23 | .collection("users")
24 | .doc(user.uid)
25 | .update({
26 | walls: firebase.firestore.FieldValue.arrayUnion(wallId),
27 | });
28 | setWallName("");
29 | router.push(`walls/${user.displayName}/${wallName}`);
30 | };
31 | if (!user) {
32 | return ;
33 | }
34 | return (
35 |
36 |
41 | Create a new sticky note wall!
42 |
43 |
44 |
53 |
63 |
64 |
65 | );
66 | };
67 |
68 | export default CreateNew;
69 |
--------------------------------------------------------------------------------
/pages/profile/[username]/edit.jsx:
--------------------------------------------------------------------------------
1 | import { useEffect, useState } from "react";
2 | import userExists from "utils/userExists";
3 | import useUser from "utils/useUser";
4 | import firebase from "utils/firebase";
5 | import Loading from "components/Loading";
6 | import FourOhFour from "components/404";
7 | import Image from "next/image";
8 | import { useRouter } from "next/router";
9 | const Edit = ({ error, userId }) => {
10 | const { user } = useUser();
11 | const [username, setUsername] = useState("");
12 | const [alreadyUsed, setAlreadyUsed] = useState(false);
13 | const router = useRouter();
14 | const changeUsername = async () => {
15 | if (!username || alreadyUsed) {
16 | return;
17 | }
18 | const db = firebase.firestore();
19 | await db.collection("users").doc(user.uid).update({
20 | username: username,
21 | });
22 | await user.updateProfile({
23 | displayName: username,
24 | });
25 | setUsername("");
26 | router.replace(`/profile/${username}/edit`);
27 | };
28 | const validateUsername = async () => {
29 | const { exists } = await userExists(username, false);
30 | setAlreadyUsed(exists);
31 | }
32 | if (error) {
33 | return ;
34 | }
35 | if (!userId || !user) {
36 | return ;
37 | }
38 | if (userId !== user.uid) {
39 | return ;
40 | }
41 | return (
42 |
43 |
44 |
51 |
63 |
73 |
74 |
75 | );
76 | };
77 |
78 | export const getStaticPaths = () => ({
79 | paths: [],
80 | fallback: "blocking",
81 | });
82 |
83 | export const getStaticProps = async ({ params }) => {
84 | const { exists, userId, userData } = await userExists(params.username);
85 | if (!exists) {
86 | return {
87 | props: {
88 | error: true,
89 | },
90 | revalidate: 1000 * 60 * 60,
91 | };
92 | }
93 | return {
94 | props: {
95 | error: false,
96 | userId,
97 | },
98 | };
99 | };
100 |
101 | export default Edit;
102 |
--------------------------------------------------------------------------------
/pages/profile/[username]/index.jsx:
--------------------------------------------------------------------------------
1 | import userExists from "utils/userExists";
2 | import { useEffect, useState } from "react";
3 | import { useRouter } from "next/router";
4 | import useUser from "utils/useUser";
5 | import Image from "next/image";
6 | import Loading from "components/Loading";
7 | import { FiEdit } from "react-icons/fi";
8 | import Link from "next/link";
9 | import FourOhFour from "components/404";
10 | import firebase from "utils/firebase";
11 | import WallGrid from "components/WallGrid";
12 |
13 | const ProfilePage = ({ error, userId, walls }) => {
14 | const router = useRouter();
15 | const [userInfo, setUserInfo] = useState();
16 | const { user } = useUser();
17 | useEffect(() => {
18 | const getUserInfo = async () => {
19 | const userInf = await fetch("/api/getUser", {
20 | method: "POST",
21 | body: JSON.stringify({
22 | userId,
23 | fields: ["photoURL", "displayName"],
24 | }),
25 | }).then((res) => res.json());
26 | setUserInfo(userInf);
27 | };
28 | if (userId) {
29 | getUserInfo();
30 | }
31 | }, [userId]);
32 | if (error) {
33 | return ;
34 | }
35 | if (!userId || !userInfo) {
36 | return ;
37 | }
38 | return (
39 |
40 |
73 |
74 |
79 | {userInfo.displayName}'s Sticky Note Walls
80 |
81 |
82 |
83 |
84 | );
85 | };
86 |
87 | export const getStaticPaths = () => ({
88 | paths: [],
89 | fallback: "blocking",
90 | });
91 |
92 | export const getStaticProps = async ({ params }) => {
93 | const { exists, userId, userData } = await userExists(params.username);
94 | if (!exists) {
95 | return {
96 | props: {
97 | error: true,
98 | },
99 | revalidate: 1000 * 60 * 60,
100 | };
101 | }
102 | const db = firebase.firestore();
103 | const walls = [];
104 | await db
105 | .collection("walls")
106 | .where("creator", "==", userId)
107 | .get()
108 | .then((snap) =>
109 | snap.forEach((doc) => {
110 | const data = doc.data();
111 | walls.push({
112 | id: doc.id,
113 | name: data.name,
114 | creator: data.creator,
115 | username: userData.username,
116 | photo: userData.photo,
117 | });
118 | })
119 | );
120 | return {
121 | props: {
122 | error: false,
123 | userId,
124 | walls,
125 | },
126 | };
127 | };
128 |
129 | export default ProfilePage;
130 |
--------------------------------------------------------------------------------
/pages/sign-in.jsx:
--------------------------------------------------------------------------------
1 | import SignInBox from "components/SignIn";
2 |
3 | const SignIn = () => {
4 | return (
5 |
6 |
9 | Sign In
10 |
11 |
12 |
13 | );
14 | };
15 |
16 | export default SignIn;
17 |
--------------------------------------------------------------------------------
/pages/walls/[[...username]].jsx:
--------------------------------------------------------------------------------
1 | import Loading from "components/Loading";
2 | import StickyNoteWall from "components/StickyNoteWall";
3 | import { useRouter } from "next/router";
4 | import { useEffect } from "react";
5 | import userExists from "utils/userExists";
6 | import Link from "next/link";
7 | import FourOhFour from "components/404";
8 | import firebase from "utils/firebase";
9 | import Image from "next/image";
10 |
11 | const UserPage = ({ username, wallId, error, wallName, profilePic }) => {
12 | if (error) {
13 | return ;
14 | }
15 | if (error || !username) {
16 | return ;
17 | }
18 | return (
19 |
20 | {wallName === "" ? (
21 |
26 |
27 | {username}'s
28 | {" "}
29 | Sticky Note Wall
30 |
31 | ) : (
32 | <>
33 |
38 | {wallName}
39 |
40 |
62 | >
63 | )}
64 |
65 |
66 | );
67 | };
68 |
69 | export default UserPage;
70 |
71 | export const getStaticPaths = () => ({
72 | paths: [],
73 | fallback: "blocking",
74 | });
75 |
76 | export const getStaticProps = async ({ params }) => {
77 | const path = params.username;
78 | if (!params.username || (path.length !== 1 && path.length !== 2)) {
79 | return {
80 | props: {
81 | error: true,
82 | },
83 | };
84 | }
85 | const username = path[0];
86 | const wallName = path.length === 1 ? "" : path[1];
87 | const {
88 | exists: inRegisteredUsers,
89 | userId,
90 | userData,
91 | } = await userExists(username);
92 | if (!inRegisteredUsers) {
93 | return {
94 | props: {
95 | error: true,
96 | },
97 | revalidate: 1000 * 60 * 60,
98 | };
99 | }
100 | const db = firebase.firestore();
101 | let userWallId;
102 | await db
103 | .collection("walls")
104 | .where("creator", "==", userId)
105 | .where("name", "==", wallName)
106 | .limit(1)
107 | .get()
108 | .then((snap) => {
109 | snap.forEach((doc) => {
110 | userWallId = doc.id;
111 | });
112 | });
113 | if (!userWallId) {
114 | return {
115 | props: {
116 | error: true,
117 | },
118 | revalidate: 1000 * 60 * 60,
119 | };
120 | }
121 | return {
122 | props: {
123 | username: username,
124 | wallId: userWallId,
125 | error: false,
126 | wallName,
127 | profilePic: userData.photo,
128 | },
129 | };
130 | };
131 |
--------------------------------------------------------------------------------
/postcss.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | plugins: {
3 | tailwindcss: {},
4 | autoprefixer: {},
5 | },
6 | }
7 |
--------------------------------------------------------------------------------
/public/favicon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/maggie-j-liu/sticky-notes/cb395b740d7925cd9de939a1bedcca214bce35cc/public/favicon.png
--------------------------------------------------------------------------------
/public/image.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/maggie-j-liu/sticky-notes/cb395b740d7925cd9de939a1bedcca214bce35cc/public/image.png
--------------------------------------------------------------------------------
/public/vercel.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/styles/globals.css:
--------------------------------------------------------------------------------
1 | @tailwind base;
2 | @tailwind components;
3 | @tailwind utilities;
4 |
5 | @layer utilities {
6 | @variants hover, focus {
7 | .wavy {
8 | text-decoration-style: wavy !important;
9 | text-decoration: underline;
10 | }
11 | }
12 |
13 | .text-outline {
14 | -webkit-text-stroke: currentColor;
15 | -webkit-text-stroke-width: 2px;
16 | -webkit-text-fill-color: transparent;
17 | }
18 |
19 | .focus-ring {
20 | @apply focus:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:ring-primary-500;
21 | }
22 |
23 | .gradient-button {
24 | @apply bg-gradient-to-br from-indigo-600 via-purple-500 to-pink-400 hover:scale-110 duration-200 hover:shadow-xl;
25 | }
26 |
27 | .input-box {
28 | @apply rounded-md border-gray-300 focus:border-primary-600 focus:ring-primary-600;
29 | }
30 |
31 | .main-button {
32 | @apply bg-primary-700 text-white px-4 py-1 rounded-md hover:bg-primary-600 disabled:cursor-not-allowed disabled:bg-gray-400 disabled:hover:bg-gray-400;
33 | }
34 |
35 | .secondary-button {
36 | @apply text-primary-700 rounded-md border border-primary-700 px-4 py-1 hover:bg-primary-50 disabled:cursor-not-allowed disabled:text-gray-500 disabled:hover:bg-white disabled:border-gray-500;
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/tailwind.config.js:
--------------------------------------------------------------------------------
1 | const colors = require("tailwindcss/colors");
2 | const defaultTheme = require("tailwindcss/defaultTheme");
3 | module.exports = {
4 | mode: "jit",
5 | purge: ["./pages/**/*.{js,ts,jsx,tsx}", "./components/**/*.{js,ts,jsx,tsx}"],
6 | darkMode: false, // or 'media' or 'class'
7 | theme: {
8 | extend: {
9 | colors: {
10 | primary: colors.indigo,
11 | fuchsia: colors.fuchsia,
12 | sky: colors.sky,
13 | },
14 | fontFamily: {
15 | sans: ["Inter", ...defaultTheme.fontFamily.sans],
16 | },
17 | },
18 | },
19 | variants: {
20 | extend: {},
21 | },
22 | plugins: [
23 | require("@tailwindcss/forms")({
24 | strategy: "class",
25 | }),
26 | ],
27 | };
28 |
--------------------------------------------------------------------------------
/utils/admin.js:
--------------------------------------------------------------------------------
1 | import * as admin from "firebase-admin";
2 |
3 | if (!admin.apps.length) {
4 | admin.initializeApp({
5 | credential: admin.credential.cert({
6 | privateKey: process.env.FIREBASE_PRIVATE_KEY.replace(/\\n/g, "\n"),
7 | clientEmail: process.env.FIREBASE_CLIENT_EMAIL,
8 | projectId: process.env.NEXT_PUBLIC_FIREBASE_PROJECT_ID,
9 | }),
10 | });
11 | }
12 |
13 | export default admin;
14 |
--------------------------------------------------------------------------------
/utils/firebase.js:
--------------------------------------------------------------------------------
1 | import firebase from "firebase/app";
2 | import "firebase/auth";
3 | import "firebase/firestore";
4 |
5 | const firebaseConfig = {
6 | apiKey: process.env.NEXT_PUBLIC_FIREBASE_API_KEY,
7 | authDomain: process.env.NEXT_PUBLIC_FIREBASE_AUTH_DOMAIN,
8 | projectId: process.env.NEXT_PUBLIC_FIREBASE_PROJECT_ID,
9 | storageBucket: process.env.NEXT_PUBLIC_FIREBASE_STORAGE_BUCKET,
10 | messagingSenderId: process.env.NEXT_PUBLIC_FIREBASE_MESSAGING_SENDER_ID,
11 | appId: process.env.NEXT_PUBLIC_FIREBASE_APP_ID,
12 | };
13 |
14 | if (!firebase.apps.length) {
15 | firebase.initializeApp(firebaseConfig);
16 | }
17 |
18 | export default firebase;
19 |
--------------------------------------------------------------------------------
/utils/useUser.jsx:
--------------------------------------------------------------------------------
1 | import { createContext, useContext, useEffect, useState } from "react";
2 | import firebase from "./firebase";
3 | import {
4 | setUserCookie,
5 | removeUserCookie,
6 | getUserFromCookie,
7 | } from "./userCookies";
8 | import { useRouter } from "next/router";
9 |
10 | const useAuth = () => {
11 | const [user, setUser] = useState(null);
12 | const [loading, setLoading] = useState(true);
13 | const router = useRouter();
14 | const handleUser = (rawUser) => {
15 | if (rawUser) {
16 | //setUserCookie(rawUser);
17 | setUser(rawUser);
18 | setLoading(false);
19 | } else {
20 | //removeUserCookie();
21 | setUser(null);
22 | setLoading(false);
23 | }
24 | };
25 | const signInWithGoogle = (fn) => {
26 | setLoading(true);
27 | return firebase
28 | .auth()
29 | .signInWithPopup(new firebase.auth.GoogleAuthProvider())
30 | .then(async (response) => {
31 | handleUser(response.user);
32 | await fn(response);
33 | router.back();
34 | });
35 | };
36 | const createUserWithEmail = (email, password, username, fn) => {
37 | setLoading(true);
38 | return firebase
39 | .auth()
40 | .createUserWithEmailAndPassword(email, password)
41 | .then(async (response) => {
42 | handleUser(response.user);
43 | await fn(response, username);
44 | router.back();
45 | });
46 | };
47 |
48 | const signInWithEmail = (email, password) => {
49 | return firebase
50 | .auth()
51 | .signInWithEmailAndPassword(email, password)
52 | .then((response) => {
53 | handleUser(response.user);
54 | router.back();
55 | });
56 | };
57 | const logout = () => {
58 | return firebase
59 | .auth()
60 | .signOut()
61 | .then(() => handleUser(false));
62 | };
63 | useEffect(() => {
64 | const cancelAuthListener = firebase.auth().onIdTokenChanged(handleUser);
65 | /*const userFromCookie = getUserFromCookie();
66 | if (!userFromCookie) {
67 | return;
68 | }
69 | setUser(userFromCookie);*/
70 | return () => {
71 | cancelAuthListener();
72 | };
73 | }, []);
74 | return {
75 | user,
76 | logout,
77 | signInWithGoogle,
78 | signInWithEmail,
79 | createUserWithEmail,
80 | };
81 | };
82 | const UserContext = createContext({ user: null, logout: () => {} });
83 | export const UserContextProvider = ({ children }) => {
84 | const auth = useAuth();
85 | return {children};
86 | };
87 | const useUser = () => useContext(UserContext);
88 | export default useUser;
89 |
--------------------------------------------------------------------------------
/utils/userCookies.js:
--------------------------------------------------------------------------------
1 | import cookies from "js-cookie";
2 |
3 | export const getUserFromCookie = () => {
4 | const cookie = cookies.get("auth");
5 | if (!cookie) {
6 | return;
7 | }
8 | return JSON.parse(cookie);
9 | };
10 |
11 | export const setUserCookie = (user) => {
12 | cookies.set("auth", JSON.stringify(user), {
13 | // firebase id tokens expire in one hour
14 | // set cookie expiry to match
15 | expires: 1 / 24,
16 | });
17 | };
18 |
19 | export const removeUserCookie = () => cookies.remove("auth");
20 |
--------------------------------------------------------------------------------
/utils/userExists.js:
--------------------------------------------------------------------------------
1 | import firebase from "./firebase";
2 | const userExists = async (username, getData = true) => {
3 | const db = firebase.firestore();
4 | return await db
5 | .collection("users")
6 | .where("username", "==", username)
7 | .limit(1)
8 | .get()
9 | .then((snapshot) => {
10 | let exists = false;
11 | let userId, userData;
12 | snapshot.forEach((doc) => {
13 | exists = true;
14 | if (getData) {
15 | const data = doc.data();
16 | userId = doc.id;
17 | userData = data;
18 | }
19 | });
20 | if (getData) {
21 | return { exists, userId, userData };
22 | }
23 | return { exists };
24 | });
25 | };
26 |
27 | export default userExists;
28 |
--------------------------------------------------------------------------------