diff --git a/.firebase/hosting.ZGlzdA.cache b/.firebase/hosting.ZGlzdA.cache new file mode 100644 index 0000000..06e65e5 --- /dev/null +++ b/.firebase/hosting.ZGlzdA.cache @@ -0,0 +1,6 @@ +index.html,1704176534558,78dbd630820dbab73673c587c12b69f1fcab3a54b55b856404286d483dd14b19 +assets/index-QMTPcTcZ.css,1704176534558,ae20ab9e32f733d718e58156fdfe7e626402487f2b517c774b69b968a80c5fe7 +logo.png,1704176534434,a4f3d73a96357dc00ef4132fd3c12aafb62a1493729a9d6f97252a8d8bcd532d +JetBrainsMono-Light.woff2,1704176534432,5117442cda8e5a3cafaafbbdf6758ed37f7ce347798b4e537a2168ad0e3bd88f +SourceSans3VF-Upright.ttf.woff2,1704176534434,a5e8c4da5456f6b9675784affac017e8d160cf3983598e323352cfae09332c5e +assets/index-AxPC6F22.js,1704176534558,4579863da50676eca6a81f5634280deabf43506db41948b0ed6acd8863255747 diff --git a/.firebaserc b/.firebaserc new file mode 100644 index 0000000..d839c35 --- /dev/null +++ b/.firebaserc @@ -0,0 +1,5 @@ +{ + "projects": { + "default": "rocketgram-b65c3" + } +} diff --git a/bun.lockb b/bun.lockb new file mode 100755 index 0000000..8fcdc90 Binary files /dev/null and b/bun.lockb differ diff --git a/firebase.json b/firebase.json new file mode 100644 index 0000000..1a13c99 --- /dev/null +++ b/firebase.json @@ -0,0 +1,12 @@ +{ + "hosting": { + "public": "dist", + "ignore": [], + "rewrites": [ + { + "source": "**", + "destination": "/index.html" + } + ] + } +} diff --git a/index.html b/index.html index 0c589ec..387fe81 100644 --- a/index.html +++ b/index.html @@ -1,13 +1,14 @@ - + - - Vite + React + + Rocketgram
+ diff --git a/package.json b/package.json index 74deac0..2d1c894 100644 --- a/package.json +++ b/package.json @@ -10,18 +10,19 @@ "preview": "vite preview" }, "dependencies": { - "firebase": "^10.5.2", + "firebase": "^10.7.1", "react": "^18.2.0", - "react-dom": "^18.2.0" + "react-dom": "^18.2.0", + "react-router-dom": "^6.21.0" }, "devDependencies": { - "@types/react": "^18.2.15", - "@types/react-dom": "^18.2.7", - "@vitejs/plugin-react": "^4.0.3", - "eslint": "^8.45.0", - "eslint-plugin-react": "^7.32.2", + "@types/react": "^18.2.45", + "@types/react-dom": "^18.2.18", + "@vitejs/plugin-react": "^4.2.1", + "eslint": "^8.56.0", + "eslint-plugin-react": "^7.33.2", "eslint-plugin-react-hooks": "^4.6.0", - "eslint-plugin-react-refresh": "^0.4.3", - "vite": "^4.4.5" + "eslint-plugin-react-refresh": "^0.4.5", + "vite": "^5.0.10" } } diff --git a/public/JetBrainsMono-Light.woff2 b/public/JetBrainsMono-Light.woff2 new file mode 100644 index 0000000..6538498 Binary files /dev/null and b/public/JetBrainsMono-Light.woff2 differ diff --git a/public/SourceSans3VF-Upright.ttf.woff2 b/public/SourceSans3VF-Upright.ttf.woff2 new file mode 100644 index 0000000..96cc9fd Binary files /dev/null and b/public/SourceSans3VF-Upright.ttf.woff2 differ diff --git a/src/App.css b/src/App.css index 996d6e1..837307d 100644 --- a/src/App.css +++ b/src/App.css @@ -35,4 +35,49 @@ .card { padding: 2em; + width: 50vw; + height: 30vw; + overflow: auto; +} + +.message-box { + text-align: left; + display: flex; + flex-direction: column-reverse; + li { + padding: 0.5em 0em 0.5em 0em; + * { + margin: 0.2em; + } + } +} + +input { + font-size: 1.25em; +} + +.text-messages { + font-size: 1.25em; + font-weight: 420; + width: 15em; + display: inline-flex; + text-wrap: balance; +} + +.buttons { + font-size: 0.8125em; + display: inline-flex; +} + +.info { + font-family: "JetBrains Mono", monospace; + font-weight: 300; + font-size: 0.5625em; + letter-spacing: 0.075em; + font-feature-settings: "cv18", "zero"; +} + +iconify-icon { + position: relative; + top: 0.35em; } diff --git a/src/App.jsx b/src/App.jsx index 372b77f..669e8b6 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -1,51 +1,71 @@ +import { useState, useEffect } from "react"; +import { createHashRouter, RouterProvider } from "react-router-dom"; +import Composer from "./Composer"; +import NewsFeed from "./NewsFeed"; +import AuthForm from "./AuthForm"; +import { onAuthStateChanged } from "firebase/auth"; +import { auth } from "./firebase"; import logo from "/logo.png"; import "./App.css"; -import { onChildAdded, push, ref, set } from "firebase/database"; -import { database } from "./firebase"; -import { useState, useEffect } from "react"; - -// Save the Firebase message folder name as a constant to avoid bugs due to misspelling -const DB_MESSAGES_KEY = "messages"; +import NavBar from "./NavBar"; -function App() { - const [messages, setMessages] = useState([]); +export default function App() { + //check login status + const [isLoggedIn, setIsLoggedIn] = useState(false); + //user info + const [email, setEmail] = useState(""); + const [uid, setUid] = useState(""); useEffect(() => { - const messagesRef = ref(database, DB_MESSAGES_KEY); - // onChildAdded will return data for every child at the reference and every subsequent new child - onChildAdded(messagesRef, (data) => { - // Add the subsequent child to local component state, initialising a new array to trigger re-render - setMessages((prevState) => - // Store message key so we can use it as a key in our list items when rendering messages - [...prevState, { key: data.key, val: data.val() }] - ); + onAuthStateChanged(auth, (user) => { + if (user) { + setIsLoggedIn(true); + setEmail(auth.currentUser.email); + setUid(auth.currentUser.uid); + } else setIsLoggedIn(false); }); }, []); - const writeData = () => { - const messageListRef = ref(database, DB_MESSAGES_KEY); - const newMessageRef = push(messageListRef); - set(newMessageRef, "abc"); - }; - - // Convert messages in state to message JSX elements to render - let messageListItems = messages.map((message) => ( -
  • {message.val}
  • - )); + const router = createHashRouter([ + { + path: "/", + element: ( + <> + + + + ), + }, + { + path: "/NewsFeed", + element: ( + <> + + {isLoggedIn && } +
    +
      + +
    +
    + + ), + }, + ]); return ( <>
    Rocket logo
    -

    Instagram Bootcamp

    -
    - {/* TODO: Add input field and add text input as messages in Firebase */} - -
      {messageListItems}
    -
    +

    Rocketgram

    + ); } - -export default App; diff --git a/src/AuthForm.jsx b/src/AuthForm.jsx new file mode 100644 index 0000000..3232aa5 --- /dev/null +++ b/src/AuthForm.jsx @@ -0,0 +1,92 @@ +import { useState } from "react"; +import { + createUserWithEmailAndPassword, + signInWithEmailAndPassword, + signOut, +} from "firebase/auth"; +import { auth } from "./firebase"; + +export default function AuthForm({ isLoggedIn, email, setEmail, setUid }) { + //Login + const [emailValue, setEmailValue] = useState(""); + const [passwordValue, setPasswordValue] = useState(""); + //error + const [errorMsg, setErrorMsg] = useState(""); + + const signUp = async () => { + try { + const userCredential = await createUserWithEmailAndPassword( + auth, + emailValue, + passwordValue + ); + console.log(userCredential); + setErrorMsg(""); + } catch (error) { + setErrorMsg(error.message); + } + }; + + const logIn = async () => { + try { + const userCredential = await signInWithEmailAndPassword( + auth, + emailValue, + passwordValue + ); + console.log(userCredential); + setErrorMsg(""); + } catch (error) { + setErrorMsg(error.message); + } + }; + + const logOut = async () => { + try { + await signOut(auth); + setErrorMsg(""); + setUid(""); + setEmail(""); + } catch (error) { + setErrorMsg(error.message); + } + }; + return ( + <> + {!isLoggedIn ? ( + <> +

    + + setEmailValue(e.target.value)} + /> +

    +

    + + setPasswordValue(e.target.value)} + /> +

    +

    {errorMsg}

    + + + + ) : ( + <> +

    {errorMsg}

    +

    Welcome: {email}

    + + + )} + + ); +} diff --git a/src/Composer.jsx b/src/Composer.jsx new file mode 100644 index 0000000..341ad81 --- /dev/null +++ b/src/Composer.jsx @@ -0,0 +1,69 @@ +import { useState } from "react"; +import { push, set, ref as databaseRef } from "firebase/database"; +import { + getDownloadURL, + uploadBytes, + ref as storageRef, +} from "firebase/storage"; +import { database, storage } from "./firebase"; + +const DB_MESSAGES_KEY = "messages"; +const DB_IMAGES_KEY = "images"; + +export default function Composer({ uid, email }) { + const [inputValue, setInputValue] = useState(""); + const [file, setFile] = useState(null); + + const messagesRef = databaseRef(database, DB_MESSAGES_KEY); + + const writeData = async () => { + let name = ""; + let url = ""; + if (file) { + const newStorageRef = storageRef( + storage, + DB_IMAGES_KEY + "/" + file.name + ); + await uploadBytes(newStorageRef, file); + url = await getDownloadURL(newStorageRef); + name = file.name; + } + set(push(messagesRef), { + timestamp: `${new Date()}`, + edited: "", + message: inputValue, + fileName: name, + fileUrl: url, + likeCount: 0, + poster: uid, + posterEmail: email, + like: { [uid]: false }, + }); + setInputValue(""); + setFile(null); + }; + + return ( +
    (e.preventDefault(), e.target.reset())}> + setInputValue(e.target.value)} + /> + +
    + setFile(e.target.files[0])} + /> +
    + ); +} diff --git a/src/NavBar.jsx b/src/NavBar.jsx new file mode 100644 index 0000000..5f5ae54 --- /dev/null +++ b/src/NavBar.jsx @@ -0,0 +1,13 @@ +import { Link } from "react-router-dom"; +export default function NavBar() { + return ( + <> + + + + + + + + ); +} diff --git a/src/NewsFeed.jsx b/src/NewsFeed.jsx new file mode 100644 index 0000000..5973be4 --- /dev/null +++ b/src/NewsFeed.jsx @@ -0,0 +1,167 @@ +import { useEffect, useState } from "react"; +import { + onChildAdded, + onChildChanged, + onChildRemoved, + ref as databaseRef, + remove, + update, + off, +} from "firebase/database"; +import { deleteObject, ref as storageRef } from "firebase/storage"; + +import { database, storage } from "./firebase"; + +const DB_MESSAGES_KEY = "messages"; +const DB_IMAGES_KEY = "images"; +const messagesRef = databaseRef(database, DB_MESSAGES_KEY); + +export default function NewsFeed({ isLoggedIn, uid }) { + const [messages, setMessages] = useState([]); + const [isEditing, setIsEditing] = useState([]); + const [editValue, setEditValue] = useState(""); + const [liked, setLiked] = useState([]); + + useEffect(() => { + onChildAdded(messagesRef, (data) => + setMessages((prev) => [...prev, { key: data.key, val: data.val() }]) + ); + onChildRemoved(messagesRef, (data) => + setMessages((prev) => prev.filter((item) => item.key !== data.key)) + ); + onChildChanged(messagesRef, (data) => + setMessages((prev) => + prev.map((item) => + item.key === data.key ? { key: data.key, val: data.val() } : item + ) + ) + ); + return () => off(messagesRef); + }, []); + + useEffect(() => { + //amount of "false" in array to be same as messages.length + setIsEditing([...Array(messages.length)].fill(false)); + setLiked(messages.map((message) => !!message.val.like[uid])); + }, [messages, uid]); + + const likeUnlike = (data, index) => { + if (liked[index]) { + setLiked((prev) => prev.with(index, !prev[index])); + update(databaseRef(database, DB_MESSAGES_KEY + "/" + data.key), { + likeCount: data.val.likeCount - 1, + like: { ...data.val.like, [uid]: false }, + }); + } else { + setLiked((prev) => prev.with(index, !prev[index])); + update(databaseRef(database, DB_MESSAGES_KEY + "/" + data.key), { + likeCount: data.val.likeCount + 1, + like: { ...data.val.like, [uid]: true }, + }); + } + }; + + const editData = (data, index) => { + setEditValue(data?.val?.message); + setIsEditing((prev) => prev.with(index, !prev[index])); + if (isEditing[index]) { + update(databaseRef(database, DB_MESSAGES_KEY + "/" + data.key), { + edited: `${new Date()}`, + message: editValue, + }); + setEditValue(""); + } + }; + + const deleteData = async (data) => { + if (data.val.fileName) { + await deleteObject( + storageRef(storage, DB_IMAGES_KEY + "/" + data.val.fileName) + ); + } + remove(databaseRef(database, DB_MESSAGES_KEY + "/" + data.key)); + }; + + return ( + // Convert messages in state to message JSX elements to render + messages.map((message, index) => ( +
  • + {/* input if editing, else just show message */} + {isEditing[index] ? ( + setEditValue(e.target.value)} + /> + ) : ( +
    {message.val.message}
    + )} +
    + {/* like button that toggles */} + {liked[index] ? ( + + ) : ( + + )} + + {/* edit button that also submits edit value, other edit buttons are disabled while editing */} + + +
    + {/* display image if it exist */} +
    + {!!message?.val?.fileUrl && ( + image + )} +
    +
    +

    Author: {message.val.posterEmail}

    + Sent: + {new Date(message.val.timestamp).toLocaleString()} + {!!message?.val?.edited && ( + <> +
    + Edited: + {new Date(message.val.edited).toLocaleString()} + + )} +
    +
  • + )) + ); +} diff --git a/src/firebase.jsx b/src/firebase.jsx index 6855616..c8cb921 100644 --- a/src/firebase.jsx +++ b/src/firebase.jsx @@ -1,21 +1,27 @@ // Import the functions you need from the SDKs you need import { initializeApp } from "firebase/app"; import { getDatabase } from "firebase/database"; +import { getStorage } from "firebase/storage"; +import { getAuth } from "firebase/auth"; // TODO: Replace with your app's Firebase project configuration const firebaseConfig = { - apiKey: "API_KEY", - authDomain: "PROJECT_ID.firebaseapp.com", + apiKey: import.meta.env.VITE_API_KEY, + authDomain: import.meta.env.VITE_AUTH_DOMAIN, // The value of `databaseURL` depends on the location of the database - databaseURL: "https://DATABASE_NAME.REGION.firebasedatabase.app", - projectId: "PROJECT_ID", - storageBucket: "PROJECT_ID.appspot.com", - messagingSenderId: "SENDER_ID", - appId: "APP_ID", + databaseURL: import.meta.env.VITE_DATABASE_URL, + projectId: import.meta.env.VITE_PROJECT_ID, + storageBucket: import.meta.env.VITE_STORAGE_BUCKET, + messagingSenderId: import.meta.env.VITE_MESSAGING_SENDER_ID, + appId: import.meta.env.VITE_APP_ID, }; // Initialize Firebase const firebaseApp = initializeApp(firebaseConfig); // Get a reference to the database service and export the reference for other modules -export const database = getDatabase(firebaseApp); +const database = getDatabase(firebaseApp); +const storage = getStorage(firebaseApp); +const auth = getAuth(firebaseApp); + +export { database, storage, auth }; diff --git a/src/index.css b/src/index.css index 2c3fac6..134ddaa 100644 --- a/src/index.css +++ b/src/index.css @@ -1,5 +1,13 @@ +@font-face { + font-family: "Jetbrains Mono"; + src: url(/JetBrainsMono-Light.woff2); +} +@font-face { + font-family: "Source Sans 3"; + src: url(/SourceSans3VF-Upright.ttf.woff2); +} :root { - font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif; + font-family: "Source Sans 3", system-ui, Helvetica, Arial, sans-serif; line-height: 1.5; font-weight: 400; @@ -67,3 +75,7 @@ button:focus-visible { background-color: #f9f9f9; } } + +input { + font-family: inherit; +} diff --git a/src/main.jsx b/src/main.jsx index 3fe8cb7..04f48d9 100644 --- a/src/main.jsx +++ b/src/main.jsx @@ -1,6 +1,10 @@ -import React from "react"; import ReactDOM from "react-dom/client"; +import React from "react"; import App from "./App.jsx"; import "./index.css"; -ReactDOM.createRoot(document.getElementById("root")).render(); +ReactDOM.createRoot(document.getElementById("root")).render( + + + +); diff --git a/vite.config.js b/vite.config.js index 5a33944..f54c1f0 100644 --- a/vite.config.js +++ b/vite.config.js @@ -1,7 +1,8 @@ -import { defineConfig } from 'vite' -import react from '@vitejs/plugin-react' +import { defineConfig } from "vite"; +import react from "@vitejs/plugin-react"; // https://vitejs.dev/config/ export default defineConfig({ plugins: [react()], -}) + base: "/instagram-3.2/", +});