diff --git a/bs3/package.json b/bs3/package.json
index f8d326d..f8d1f46 100644
--- a/bs3/package.json
+++ b/bs3/package.json
@@ -2,7 +2,7 @@
"name": "bs3",
"version": "0.1.0",
"private": true,
- "main": "src/index.js",
+ "main": "src/index.ts",
"dependencies": {
"@emotion/react": "^11.11.1",
"@emotion/styled": "^11.11.0",
@@ -29,7 +29,14 @@
},
"devDependencies": {
"@babel/plugin-proposal-private-property-in-object": "7.21.11",
- "env-cmd": "^10.1.0"
+ "@types/node": "^22.14.0",
+ "@types/react": "^19.1.0",
+ "@types/react-dom": "^19.1.1",
+ "@typescript-eslint/eslint-plugin": "^8.29.1",
+ "@typescript-eslint/parser": "^8.29.1",
+ "env-cmd": "^10.1.0",
+ "eslint": "^9.24.0",
+ "typescript": "^5.8.3"
},
"scripts": {
"start": "react-scripts start",
diff --git a/bs3/src/App.test.js b/bs3/src/App.test.js
deleted file mode 100644
index 1f03afe..0000000
--- a/bs3/src/App.test.js
+++ /dev/null
@@ -1,8 +0,0 @@
-import { render, screen } from '@testing-library/react';
-import App from './App';
-
-test('renders learn react link', () => {
- render();
- const linkElement = screen.getByText(/learn react/i);
- expect(linkElement).toBeInTheDocument();
-});
diff --git a/bs3/src/App.js b/bs3/src/App.tsx
similarity index 69%
rename from bs3/src/App.js
rename to bs3/src/App.tsx
index 60cb16f..8e18ac4 100644
--- a/bs3/src/App.js
+++ b/bs3/src/App.tsx
@@ -1,16 +1,15 @@
+import React, { useEffect } from 'react';
import './App.css';
import BottomAppBar from './components/bottom-app-bar';
import { Outlet } from 'react-router-dom';
import { useDispatch } from 'react-redux';
import { setLocation } from './app/geolocatorSlice';
import useGeoLocationCheck from './api/geolocation';
-import { useEffect } from 'react';
function App() {
const dispatch = useDispatch();
- const geoLocation = useGeoLocationCheck(); // Custom hook
+ const geoLocation = useGeoLocationCheck();
- // Use useEffect to dispatch the action only when geoLocation is valid
useEffect(() => {
if (geoLocation) {
dispatch(setLocation(geoLocation));
@@ -20,9 +19,9 @@ function App() {
return (
-
+
);
}
-export default App;
+export default App;
\ No newline at end of file
diff --git a/bs3/src/AppWrapper.js b/bs3/src/AppWrapper.js
deleted file mode 100644
index 184ad3a..0000000
--- a/bs3/src/AppWrapper.js
+++ /dev/null
@@ -1,64 +0,0 @@
-import React, { useEffect, useState } from 'react';
-import { useDispatch } from 'react-redux';
-import App from './App';
-import { Box, Typography, Button, Modal } from '@mui/material';
-
-
-export default function AppWrapper() {
- const dispatch = useDispatch();
- const [loading, setLoading] = useState(true);
- const [showModal, setShowModal] = useState(true);
-
- const AppModal = ({ onClose }) => (
-
-
-
-
- Welcome to the BS3 community.
-
-
- Location is optional but without it you will not be able to vote or post.
- Location data will never be stored.
-
-
-
-
- );
-
- useEffect(() => {
- if (!showModal) {
- setLoading(false);
- }
- }, [dispatch, showModal]);
-
- const handleCloseModal = () => {
- setShowModal(false);
- };
-
- if (showModal) {
- return ;
- }
-
- if (loading) {
- return Checking location...
;
- }
-
- return ;
-}
\ No newline at end of file
diff --git a/bs3/src/AppWrapper.tsx b/bs3/src/AppWrapper.tsx
new file mode 100644
index 0000000..26be926
--- /dev/null
+++ b/bs3/src/AppWrapper.tsx
@@ -0,0 +1,72 @@
+import React, { useEffect, useState } from 'react';
+import { useDispatch } from 'react-redux';
+import App from './App';
+import { Box, Typography, Button, Modal } from '@mui/material';
+
+interface AppModalProps {
+ onClose: () => void;
+}
+
+const AppModal: React.FC = ({ onClose }) => {
+ const [showModal, setShowModal] = useState(true);
+
+ return (
+
+
+
+ Welcome to the BS3 community.
+
+
+ Location is optional but without it you will not be able to vote or post.
+ Location data will never be stored.
+
+
+
+
+ );
+};
+
+const AppWrapper: React.FC = () => {
+ const dispatch = useDispatch();
+ const [loading, setLoading] = useState(true);
+ const [showModal, setShowModal] = useState(true);
+
+ useEffect(() => {
+ if (!showModal) {
+ setLoading(false);
+ }
+ }, [dispatch, showModal]);
+
+ const handleCloseModal = () => {
+ setShowModal(false);
+ };
+
+ if (showModal) {
+ return ;
+ }
+
+ if (loading) {
+ return Checking location...
;
+ }
+
+ return ;
+};
+
+export default AppWrapper;
\ No newline at end of file
diff --git a/bs3/src/api/firebaseConfig.js b/bs3/src/api/firebaseConfig.js
deleted file mode 100644
index 20f060e..0000000
--- a/bs3/src/api/firebaseConfig.js
+++ /dev/null
@@ -1,17 +0,0 @@
-import { initializeApp } from "firebase/app";
-import { getFirestore } from "firebase/firestore";
-
-const firebaseConfig = {
- apiKey: process.env.REACT_APP_FIREBASE_API_KEY,
- authDomain: process.env.REACT_APP_FIREBASE_AUTH_DOMAIN,
- projectId: process.env.REACT_APP_FIREBASE_PROJECT_ID,
- storageBucket: process.env.REACT_APP_FIREBASE_STORAGE_BUCKET,
- messagingSenderId: process.env.REACT_APP_FIREBASE_MESSAGING_SENDER_ID,
- appId: process.env.REACT_APP_FIREBASE_APP_ID,
-};
-
-// Initialize Firebase
-const app = initializeApp(firebaseConfig);
-const db = await getFirestore(app);
-
-export { db };
diff --git a/bs3/src/api/firebaseConfig.ts b/bs3/src/api/firebaseConfig.ts
new file mode 100644
index 0000000..486ec51
--- /dev/null
+++ b/bs3/src/api/firebaseConfig.ts
@@ -0,0 +1,16 @@
+import { initializeApp } from "firebase/app";
+import { getFirestore } from "firebase/firestore";
+
+const firebaseConfig = {
+ apiKey: process.env.REACT_APP_FIREBASE_API_KEY as string,
+ authDomain: process.env.REACT_APP_FIREBASE_AUTH_DOMAIN as string,
+ projectId: process.env.REACT_APP_FIREBASE_PROJECT_ID as string,
+ storageBucket: process.env.REACT_APP_FIREBASE_STORAGE_BUCKET as string,
+ messagingSenderId: process.env.REACT_APP_FIREBASE_MESSAGING_SENDER_ID as string,
+ appId: process.env.REACT_APP_FIREBASE_APP_ID as string,
+};
+
+const app = initializeApp(firebaseConfig);
+const db = getFirestore(app);
+
+export { db };
\ No newline at end of file
diff --git a/bs3/src/api/geolocation.js b/bs3/src/api/geolocation.js
deleted file mode 100644
index 20f804c..0000000
--- a/bs3/src/api/geolocation.js
+++ /dev/null
@@ -1,83 +0,0 @@
-import { useState, useEffect, useCallback } from "react";
-
-const options = {
- enableHighAccuracy: true,
- timeout: 10000,
- maximumAge: 0,
-}
-
-const useGeoLocationCheck = () => {
- const [location, setLocation] = useState({
- loaded: false,
- coordinates: { lat: null, lng: null },
- local: false,
- });
-
- const isLocal = (location) => {
-
- const LNG_MIN = -2.6485976706726055;
- const LNG_MAX = -2.5743893376966813;
- const LAT_MIN = 51.42082059829641;
- const LAT_MAX = 51.45543490194706;
-
- const lat = location.coords.latitude;
- const lng = location.coords.longitude
-
- if (lat && lng) {
- if ( lat >= LAT_MIN && lat <= LAT_MAX && lng >= LNG_MIN && lng <= LNG_MAX ) {
- console.log("Local location detected");
- return true;
- } else {
- console.log("Non-local location detected");
- return false;
- }
- } else {
- console.log("Location is not available");
- return false;
- }
- }
-
- const onSuccess = useCallback((location) => {
- console.log("Location obtained: ", location);
- setLocation({
- loaded: true,
- coordinates: {
- lat: location.coords.latitude,
- lng: location.coords.longitude,
- },
- local: isLocal(location),
- });
- }, []);
-
- const onError = useCallback((error) => {
- console.log("Error while obtaining geolocation: ", error);
- setLocation({
- loaded: true,
- error: {
- code: error.code,
- message: error.message,
- },
- });
- }, []);
-
- useEffect(() => {
- if (navigator.geolocation) {
- navigator.permissions.query({ name: "geolocation" }).then((result) => {
- if (result.state === "granted") {
- navigator.geolocation.getCurrentPosition(onSuccess, onError, options);
- } else if (result.state === "prompt") {
- navigator.geolocation.getCurrentPosition(onSuccess, onError, options);
- } else {
- console.log("Geolocation permission denied");
- }
- });
- } else {
- console.log("Geolocation is not supported by this browser.");
- }
- }, [onSuccess, onError, location.loaded]);
-
- return location;
-
-};
-
-export default useGeoLocationCheck;
diff --git a/bs3/src/api/geolocation.ts b/bs3/src/api/geolocation.ts
new file mode 100644
index 0000000..022b91c
--- /dev/null
+++ b/bs3/src/api/geolocation.ts
@@ -0,0 +1,98 @@
+import { useState, useEffect, useCallback } from "react";
+
+interface Coordinates {
+ lat: number | null;
+ lng: number | null;
+}
+
+interface LocationState {
+ loaded: boolean;
+ coordinates: Coordinates;
+ local: boolean;
+ error?: {
+ code: number;
+ message: string;
+ };
+}
+
+const options: PositionOptions = {
+ enableHighAccuracy: true,
+ timeout: 10000,
+ maximumAge: 0,
+};
+
+const useGeoLocationCheck = (): LocationState => {
+ const [location, setLocation] = useState({
+ loaded: false,
+ coordinates: { lat: null, lng: null },
+ local: false,
+ });
+
+ const isLocal = (location: GeolocationPosition): boolean => {
+ const LNG_MIN = -2.6485976706726055;
+ const LNG_MAX = -2.5743893376966813;
+ const LAT_MIN = 51.42082059829641;
+ const LAT_MAX = 51.45543490194706;
+
+ const lat = location.coords.latitude;
+ const lng = location.coords.longitude;
+
+ if (lat && lng) {
+ if (lat >= LAT_MIN && lat <= LAT_MAX && lng >= LNG_MIN && lng <= LNG_MAX) {
+ console.log("Local location detected");
+ return true;
+ } else {
+ console.log("Non-local location detected");
+ return false;
+ }
+ } else {
+ console.log("Location is not available");
+ return false;
+ }
+ };
+
+ const onSuccess = useCallback((location: GeolocationPosition): void => {
+ console.log("Location obtained: ", location);
+ setLocation({
+ loaded: true,
+ coordinates: {
+ lat: location.coords.latitude,
+ lng: location.coords.longitude,
+ },
+ local: isLocal(location),
+ });
+ }, []);
+
+ const onError = useCallback((error: GeolocationPositionError): void => {
+ console.log("Error while obtaining geolocation: ", error);
+ setLocation({
+ loaded: true,
+ coordinates: { lat: null, lng: null },
+ local: false,
+ error: {
+ code: error.code,
+ message: error.message,
+ },
+ });
+ }, []);
+
+ useEffect(() => {
+ if (navigator.geolocation) {
+ navigator.permissions.query({ name: "geolocation" as PermissionName }).then((result) => {
+ if (result.state === "granted") {
+ navigator.geolocation.getCurrentPosition(onSuccess, onError, options);
+ } else if (result.state === "prompt") {
+ navigator.geolocation.getCurrentPosition(onSuccess, onError, options);
+ } else {
+ console.log("Geolocation permission denied");
+ }
+ });
+ } else {
+ console.log("Geolocation is not supported by this browser.");
+ }
+ }, [onSuccess, onError, location.loaded]);
+
+ return location;
+};
+
+export default useGeoLocationCheck;
diff --git a/bs3/src/app/firestoreSlice.js b/bs3/src/app/firestoreSlice.ts
similarity index 52%
rename from bs3/src/app/firestoreSlice.js
rename to bs3/src/app/firestoreSlice.ts
index 7e3e2d3..6225fde 100644
--- a/bs3/src/app/firestoreSlice.js
+++ b/bs3/src/app/firestoreSlice.ts
@@ -2,8 +2,28 @@ import { nanoid } from 'nanoid'
import { createSlice, createAsyncThunk } from '@reduxjs/toolkit';
import { collection, addDoc, getDocs, updateDoc, deleteDoc, doc, arrayRemove, arrayUnion, getDoc } from "firebase/firestore";
import { db } from '../api/firebaseConfig';
+import { PostData, FirestoreState } from '../types/types';
-function rankingScore(upVotes, downVotes) {
+const USER_ID_KEY = 'userId';
+
+function loadOrCreateId(key: string, defaultValue: string): string {
+ const item = localStorage.getItem(key);
+ if (item) {
+ return item;
+ } else {
+ localStorage.setItem(key, defaultValue);
+ return defaultValue;
+ }
+}
+
+const initialState: FirestoreState = {
+ user: loadOrCreateId(USER_ID_KEY, nanoid(10)),
+ posts: [],
+ status: 'idle',
+ error: null,
+};
+
+function rankingScore(upVotes: number, downVotes: number): number {
const n = upVotes + downVotes;
if (n === 0) return -1;
@@ -14,57 +34,55 @@ function rankingScore(upVotes, downVotes) {
return score;
}
-function loadOrCreateId(key, defaultValue) {
- const item = localStorage.getItem(key)
- if (item) {
- return item
- } else {
- localStorage.setItem(key, defaultValue)
- return defaultValue
- }
+function sortPostsByRanking(posts: PostData[]): PostData[] {
+ return posts.sort(
+ (a, b) =>
+ rankingScore(b.upVoted.length, b.downVoted.length) -
+ rankingScore(a.upVoted.length, a.downVoted.length)
+ );
}
-export const fetchPosts = createAsyncThunk(
+// Fix typings for fetchPosts
+export const fetchPosts = createAsyncThunk(
'firestore/fetchPosts',
async () => {
const querySnapshot = await getDocs(collection(db, 'posts'));
- return querySnapshot.docs.map(doc => ({ id: doc.id, ...doc.data() }));
+ return querySnapshot.docs.map(doc => ({ id: doc.id, ...doc.data() } as PostData));
}
);
-export const upVotePost = createAsyncThunk(
+// Fix typings for upVotePost
+export const upVotePost = createAsyncThunk(
'firestore/upVote',
async ({ id, user }) => {
-
const post = doc(db, 'posts', id);
-
await updateDoc(post, {
upVoted: arrayUnion(user),
- downVoted: arrayRemove(user)
+ downVoted: arrayRemove(user),
});
- const updatedPost = (await getDoc(post)).data()
- return { id, ...updatedPost }
+ const updatedPost = (await getDoc(post)).data();
+ return { id, ...updatedPost } as PostData;
}
);
-export const downVotePost = createAsyncThunk(
+// Fix typings for downVotePost
+export const downVotePost = createAsyncThunk(
'firestore/downVote',
async ({ id, user }) => {
-
const post = doc(db, 'posts', id);
-
await updateDoc(post, {
upVoted: arrayRemove(user),
- downVoted: arrayUnion(user)
+ downVoted: arrayUnion(user),
});
- const updatedPost = (await getDoc(post)).data()
- return { id, ...updatedPost }
+ const updatedPost = (await getDoc(post)).data();
+ return { id, ...updatedPost } as PostData;
}
);
-export const insertPost = createAsyncThunk(
+// Fix typings for insertPost
+export const insertPost = createAsyncThunk>(
'firestore/insertPost',
async (data) => {
const docRef = await addDoc(collection(db, 'posts'), data);
@@ -72,50 +90,47 @@ export const insertPost = createAsyncThunk(
}
);
-export const updatePost = createAsyncThunk(
+// Fix typings for updatePost
+export const updatePost = createAsyncThunk }>(
'firestore/updatePost',
async ({ id, data }) => {
const docRef = doc(db, 'posts', id);
- const serialised_data = JSON.parse(JSON.stringify(data))
+ const serialised_data = JSON.parse(JSON.stringify(data));
await updateDoc(docRef, serialised_data);
- return { id, ...data };
+ return { id, ...data } as PostData;
}
);
+// Fix typings for deletePost
export const deletePost = createAsyncThunk(
'posts/deletePost',
- async (id, thunkAPI) => {
+ async (id: string) => {
try {
- console.log("Deleting post with ID:", id);
const docRef = doc(db, 'posts', id);
- console.log("Document reference:", docRef);
await deleteDoc(docRef);
- console.log("Document deleted successfully");
return id;
} catch (error) {
console.error("Error deleting document: ", error);
- return thunkAPI.rejectWithValue({ error: error.message });
+ return id
}
}
);
const firestoreSlice = createSlice({
-
name: 'firestore',
-
- initialState: {
- user: loadOrCreateId("userId", nanoid(10)),
- posts: [],
- status: 'idle',
- error: null
- },
-
+ initialState,
reducers: {
sortPosts: (state) => {
- state.posts.sort((a, b) => rankingScore(b.upVoted.length, b.downVoted.length) - rankingScore(a.upVoted.length, a.downVoted.length))
- }
+ state.posts.sort(
+ (a, b) =>
+ rankingScore(b.upVoted.length, b.downVoted.length) -
+ rankingScore(a.upVoted.length, a.downVoted.length)
+ );
+ },
+ addPost: (state, action) => {
+ state.posts.push(action.payload);
+ },
},
-
extraReducers: (builder) => {
builder
.addCase(fetchPosts.pending, (state) => {
@@ -123,40 +138,39 @@ const firestoreSlice = createSlice({
})
.addCase(fetchPosts.fulfilled, (state, action) => {
state.status = 'succeeded';
- state.posts = action.payload;
- state.posts.sort((a, b) => rankingScore(b.upVoted.length, b.downVoted.length) - rankingScore(a.upVoted.length, a.downVoted.length))
+ state.posts = sortPostsByRanking(action.payload);
})
.addCase(fetchPosts.rejected, (state, action) => {
state.status = 'failed';
- state.error = action.error.message;
+ state.error = action.error.message || null;
})
.addCase(insertPost.fulfilled, (state, action) => {
state.posts.push(action.payload);
})
.addCase(upVotePost.fulfilled, (state, action) => {
- const index = state.posts.findIndex(doc => doc.id === action.payload.id);
+ const index = state.posts.findIndex((doc) => doc.id === action.payload.id);
if (index !== -1) {
state.posts[index] = action.payload;
}
})
.addCase(downVotePost.fulfilled, (state, action) => {
- const index = state.posts.findIndex(doc => doc.id === action.payload.id);
+ const index = state.posts.findIndex((doc) => doc.id === action.payload.id);
if (index !== -1) {
state.posts[index] = action.payload;
}
})
.addCase(updatePost.fulfilled, (state, action) => {
- const index = state.posts.findIndex(doc => doc.id === action.payload.id);
+ const index = state.posts.findIndex((doc) => doc.id === action.payload.id);
if (index !== -1) {
state.posts[index] = action.payload;
}
})
.addCase(deletePost.fulfilled, (state, action) => {
- state.posts = state.posts.filter(doc => doc.id !== action.payload);
+ state.posts = state.posts.filter((doc) => doc.id !== action.payload);
});
- }
+ },
});
-export const { addPost, sortPosts } = firestoreSlice.actions
+export const { addPost, sortPosts } = firestoreSlice.actions;
export default firestoreSlice.reducer;
diff --git a/bs3/src/app/geolocatorSlice.js b/bs3/src/app/geolocatorSlice.js
deleted file mode 100644
index 4a259b2..0000000
--- a/bs3/src/app/geolocatorSlice.js
+++ /dev/null
@@ -1,21 +0,0 @@
-import { createSlice } from '@reduxjs/toolkit';
-
-export const geolocationSlice = createSlice({
- name: 'geolocation',
- initialState: {
- location: {
- loaded: false,
- coordinates: { lat: null, lng: null },
- local: false,
- }
- },
- reducers: {
- setLocation: (state, action) => {
- state.location = action.payload;
- },
- },
-});
-
-export const { setLocation } = geolocationSlice.actions;
-
-export default geolocationSlice.reducer;
\ No newline at end of file
diff --git a/bs3/src/app/geolocatorSlice.ts b/bs3/src/app/geolocatorSlice.ts
new file mode 100644
index 0000000..df710ed
--- /dev/null
+++ b/bs3/src/app/geolocatorSlice.ts
@@ -0,0 +1,35 @@
+import { createSlice, PayloadAction } from '@reduxjs/toolkit';
+
+interface Location {
+ loaded: boolean;
+ coordinates: {
+ lat: number | null;
+ lng: number | null;
+ };
+ local: boolean;
+}
+
+interface GeolocationState {
+ location: Location;
+}
+
+const initialState: GeolocationState = {
+ location: {
+ loaded: false,
+ coordinates: { lat: null, lng: null },
+ local: false,
+ },
+};
+
+export const geolocationSlice = createSlice({
+ name: 'geolocation',
+ initialState,
+ reducers: {
+ setLocation: (state, action: PayloadAction) => {
+ state.location = action.payload;
+ },
+ },
+});
+
+export const { setLocation } = geolocationSlice.actions;
+export default geolocationSlice.reducer;
\ No newline at end of file
diff --git a/bs3/src/app/hooks.ts b/bs3/src/app/hooks.ts
new file mode 100644
index 0000000..99acaa9
--- /dev/null
+++ b/bs3/src/app/hooks.ts
@@ -0,0 +1,5 @@
+import { TypedUseSelectorHook, useDispatch, useSelector } from 'react-redux';
+import type { RootState, AppDispatch } from './store';
+
+export const useAppDispatch = () => useDispatch();
+export const useAppSelector: TypedUseSelectorHook = useSelector;
\ No newline at end of file
diff --git a/bs3/src/app/store.js b/bs3/src/app/store.js
deleted file mode 100644
index 1ae1f94..0000000
--- a/bs3/src/app/store.js
+++ /dev/null
@@ -1,12 +0,0 @@
-import { configureStore } from '@reduxjs/toolkit'
-import firestoreReducer, { fetchPosts } from './firestoreSlice'
-import geolocatorSlice from './geolocatorSlice'
-
-export const store = configureStore({
- reducer: {
- app: firestoreReducer,
- geolocation: geolocatorSlice
- }
-})
-
-store.dispatch(fetchPosts())
\ No newline at end of file
diff --git a/bs3/src/app/store.ts b/bs3/src/app/store.ts
new file mode 100644
index 0000000..677b5f1
--- /dev/null
+++ b/bs3/src/app/store.ts
@@ -0,0 +1,16 @@
+import { configureStore } from '@reduxjs/toolkit';
+import firestoreReducer, { fetchPosts } from './firestoreSlice';
+import geolocatorReducer from './geolocatorSlice';
+
+export const store = configureStore({
+ reducer: {
+ app: firestoreReducer,
+ geolocation: geolocatorReducer,
+ },
+});
+
+store.dispatch(fetchPosts());
+
+// Optional: export types for use with TypeScript elsewhere
+export type RootState = ReturnType;
+export type AppDispatch = typeof store.dispatch;
\ No newline at end of file
diff --git a/bs3/src/components/bottom-app-bar.js b/bs3/src/components/bottom-app-bar.js
deleted file mode 100644
index 2786db0..0000000
--- a/bs3/src/components/bottom-app-bar.js
+++ /dev/null
@@ -1,74 +0,0 @@
-import * as React from 'react';
-import AppBar from '@mui/material/AppBar';
-import Box from '@mui/material/Box';
-import CssBaseline from '@mui/material/CssBaseline';
-import Toolbar from '@mui/material/Toolbar';
-import IconButton from '@mui/material/IconButton';
-import MenuIcon from '@mui/icons-material/Menu';
-import useScrollTrigger from '@mui/material/useScrollTrigger';
-import Slide from '@mui/material/Slide';
-import PropTypes from 'prop-types';
-import { useSelector } from 'react-redux'
-import { Typography } from '@mui/material';
-import CheckIcon from '@mui/icons-material/Check';
-import CrossIcon from '@mui/icons-material/Close';
-
-function HideOnScroll(props) {
- const { children } = props;
- // Note that you normally won't need to set the window ref as useScrollTrigger
- // will default to window.
- // This is only being set here because the demo is in an iframe.
- const trigger = useScrollTrigger();
-
- return (
-
- {children}
-
- );
- }
-
- HideOnScroll.propTypes = {
- children: PropTypes.element.isRequired,
- };
-
- export default function BottomAppBar() {
-
- const user = useSelector(state => state.app.user)
- const location = useSelector(state => state.geolocation.location)
-
- return (
-
-
-
-
-
-
-
-
-
-
-
- User ID - {user}
-
-
- { location.local ? (
-
- Local mode
-
-
-
- ) : (
-
- Guest mode
-
-
-
- )}
-
-
-
-
-
- );
-
- }
\ No newline at end of file
diff --git a/bs3/src/components/bottom-app-bar.tsx b/bs3/src/components/bottom-app-bar.tsx
new file mode 100644
index 0000000..c33ff7b
--- /dev/null
+++ b/bs3/src/components/bottom-app-bar.tsx
@@ -0,0 +1,108 @@
+import * as React from 'react';
+import AppBar from '@mui/material/AppBar';
+import Box from '@mui/material/Box';
+import CssBaseline from '@mui/material/CssBaseline';
+import Toolbar from '@mui/material/Toolbar';
+import IconButton from '@mui/material/IconButton';
+import MenuIcon from '@mui/icons-material/Menu';
+import useScrollTrigger from '@mui/material/useScrollTrigger';
+import Slide from '@mui/material/Slide';
+import PropTypes from 'prop-types';
+import { useSelector } from 'react-redux';
+import { Typography } from '@mui/material';
+import CheckIcon from '@mui/icons-material/Check';
+import CrossIcon from '@mui/icons-material/Close';
+
+// Define the props for the HideOnScroll component
+interface HideOnScrollProps {
+ children: React.ReactElement;
+}
+
+function HideOnScroll(props: HideOnScrollProps): JSX.Element {
+ const { children } = props;
+ const trigger = useScrollTrigger();
+
+ return (
+
+ {children}
+
+ );
+}
+
+HideOnScroll.propTypes = {
+ children: PropTypes.element.isRequired,
+};
+
+// Define the types for the Redux state
+interface AppState {
+ app: {
+ user: string;
+ };
+ geolocation: {
+ location: {
+ local: boolean;
+ };
+ };
+}
+
+export default function BottomAppBar(): JSX.Element {
+ const user = useSelector((state: AppState) => state.app.user);
+ const location = useSelector((state: AppState) => state.geolocation.location);
+
+ return (
+
+
+
+
+
+
+
+
+
+
+
+ User ID - {user}
+
+
+ {location.local ? (
+
+
+ Local mode
+
+
+
+ ) : (
+
+
+ Guest mode
+
+
+
+ )}
+
+
+
+
+
+ );
+}
\ No newline at end of file
diff --git a/bs3/src/components/main-action-buttons.js b/bs3/src/components/main-action-buttons.js
deleted file mode 100644
index 0bfddfc..0000000
--- a/bs3/src/components/main-action-buttons.js
+++ /dev/null
@@ -1,45 +0,0 @@
-import { styled } from '@mui/material/styles';
-import Fab from '@mui/material/Fab';
-import { Link } from 'react-router-dom';
-import AddIcon from '@mui/icons-material/Add';
-import SendIcon from '@mui/icons-material/Send';
-import ArrowBackIcon from '@mui/icons-material/ArrowBack';
-
-const StyledFab = styled(Fab)({
- position: 'fixed',
- zIndex: 1300,
- bottom: 30,
- left: 0,
- right: 0,
- margin: '0 auto',
-});
-
-
-export default function MainActionFab({ type }) {
- switch(type) {
-
- case "new":
- return (
-
-
-
- )
-
- case "submit":
- return (
-
-
-
- )
-
- case "back":
- return (
-
-
-
- )
-
- default:
-
- }
-}
diff --git a/bs3/src/components/main-action-buttons.tsx b/bs3/src/components/main-action-buttons.tsx
new file mode 100644
index 0000000..840ec1a
--- /dev/null
+++ b/bs3/src/components/main-action-buttons.tsx
@@ -0,0 +1,51 @@
+import { styled } from '@mui/material/styles';
+import Fab from '@mui/material/Fab';
+import { Link } from 'react-router-dom';
+import AddIcon from '@mui/icons-material/Add';
+import SendIcon from '@mui/icons-material/Send';
+import ArrowBackIcon from '@mui/icons-material/ArrowBack';
+
+const StyledFab = styled(Fab)({
+ position: 'fixed',
+ zIndex: 1300,
+ bottom: 30,
+ left: 0,
+ right: 0,
+ margin: '0 auto',
+});
+
+interface MainActionFabProps {
+ type: 'new' | 'submit' | 'back';
+}
+
+export default function MainActionFab({ type }: MainActionFabProps): JSX.Element | null {
+ switch (type) {
+ case 'new':
+ return (
+
+
+
+
+
+ );
+
+ case 'submit':
+ return (
+
+
+
+ );
+
+ case 'back':
+ return (
+
+
+
+
+
+ );
+
+ default:
+ return null;
+ }
+}
diff --git a/bs3/src/components/post-card.js b/bs3/src/components/post-card.tsx
similarity index 50%
rename from bs3/src/components/post-card.js
rename to bs3/src/components/post-card.tsx
index 295f5e3..6637bc3 100644
--- a/bs3/src/components/post-card.js
+++ b/bs3/src/components/post-card.tsx
@@ -11,129 +11,134 @@ import ArrowCircleUpIcon from '@mui/icons-material/ArrowCircleUp';
import EditIcon from '@mui/icons-material/Edit';
import DeleteIcon from '@mui/icons-material/Delete';
import { Link, useNavigate } from 'react-router-dom';
-import { useSelector, useDispatch } from 'react-redux'
-import { upVotePost, downVotePost, sortPosts } from '../app/firestoreSlice';
+import { useAppDispatch, useAppSelector } from '../app/hooks';
+import { upVotePost, downVotePost, sortPosts, deletePost } from '../app/firestoreSlice';
import { useState } from 'react'
import ReactMarkdown from 'react-markdown';
-import { deletePost } from '../app/firestoreSlice';
+import { PostData } from '../types/types';
-function ConditionalLink({ children, condition, ...props }) {
- return !!condition && props.to ?
-
- {children}
- : <>{children}>
+// Define types for ConditionalLink props
+interface ConditionalLinkProps {
+ children: React.ReactNode;
+ condition: boolean;
+ to: string;
+ [key: string]: any; // Allow additional props
}
-function timeAgo(timestamp) {
- const now = new Date();
- const postDate = new Date(timestamp);
- const diffInSeconds = Math.floor((now - postDate) / 1000);
+function ConditionalLink({ children, condition, ...props }: ConditionalLinkProps) {
+ return !!condition && props.to ? (
+
+ {children}
+
+ ) : (
+ <>{children}>
+ );
+}
- if (diffInSeconds < 60) {
- return 'now';
- } else if (diffInSeconds < 3600) {
- const minutes = Math.floor(diffInSeconds / 60);
- return `${minutes} minute${minutes > 1 ? 's' : ''} ago`;
- } else if (diffInSeconds < 86400) {
- const hours = Math.floor(diffInSeconds / 3600);
- return `${hours} hour${hours > 1 ? 's' : ''} ago`;
- } else {
- const days = Math.floor(diffInSeconds / 86400);
- return `${days} day${days > 1 ? 's' : ''} ago`;
- }
+interface BasicCardProps {
+ post: PostData;
+ extended?: boolean;
}
+interface ThisPostState {
+ userUpVoted: boolean;
+ userDownVoted: boolean;
+}
-export default function BasicCard({ post, extended = false}) {
- const dispatch = useDispatch()
- const navigate = useNavigate()
-
- const userId = useSelector(state => state.app.user)
- const location = useSelector(state => state.geolocation.location)
- const [deleteWarningOpen, setDeleteWarningOpen] = useState(false);
+export default function BasicCard({ post, extended = false }: BasicCardProps) {
- const [thisPost, setThisPost] = useState({
+ const dispatch = useAppDispatch();
+ const navigate = useNavigate();
+
+ const [deleteWarningOpen, setDeleteWarningOpen] = useState(false);
+
+ const userId = useAppSelector(state => state.app.user);
+ const location = useAppSelector(state => state.geolocation.location);
+
+ const [thisPost, setThisPost] = useState({
userUpVoted: post.upVoted.includes(userId),
- userDownVoted: post.downVoted.includes(userId)
- })
-
+ userDownVoted: post.downVoted.includes(userId),
+ });
+
const handleEdit = () => {
localStorage.setItem('newPostForm', JSON.stringify(post));
- navigate("/new")
+ navigate('/new');
};
const handleDelete = () => {
- setDeleteWarningOpen(true)
+ setDeleteWarningOpen(true);
};
-
+
const handleConfirmDelete = () => {
- dispatch(deletePost(post.id))
- navigate("/")
- setDeleteWarningOpen(false)
- }
+ dispatch(deletePost(post.id));
+ navigate('/');
+ setDeleteWarningOpen(false);
+ };
const handleClose = () => {
setDeleteWarningOpen(false);
};
- function setUpVoted() {
+ const setUpVoted = () => {
setThisPost({
userUpVoted: true,
- userDownVoted: false
- })
- }
+ userDownVoted: false,
+ });
+ };
- function setDownVoted() {
+ const setDownVoted = () => {
setThisPost({
userUpVoted: false,
- userDownVoted: true
- })
- }
+ userDownVoted: true,
+ });
+ };
- if (extended === false) {
- post = {...post, body: post.body.replace(/\n/g, ' ').slice(0, 75) + "..."}
- }
-
- function handleUpVote() {
- dispatch(upVotePost({ id: post.id, user: userId}))
- dispatch(sortPosts())
- setUpVoted()
+ if (!extended) {
+ post = { ...post, body: post.body.replace(/\n/g, ' ').slice(0, 75) + '...' };
}
- function handleDownVote() {
- dispatch(downVotePost({ id: post.id, user: userId}))
- dispatch(sortPosts())
- setDownVoted()
- }
-
+ const handleUpVote = () => {
+ dispatch(upVotePost({ id: post.id, user: userId }));
+ dispatch(sortPosts());
+ setUpVoted();
+ };
+
+ const handleDownVote = () => {
+ dispatch(downVotePost({ id: post.id, user: userId }));
+ dispatch(sortPosts());
+ setDownVoted();
+ };
+
return (
-
-
+
+
{post.userId === userId && extended && (
@@ -170,47 +175,51 @@ export default function BasicCard({ post, extended = false}) {
{timeAgo(post.timestamp)}
-
-
+
+
-
-