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 (
<>
- 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 (
+
+ );
+}
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 && (
+
+ )}
+
+
+
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/",
+});