diff --git a/.husky/post-checkout b/.husky/post-checkout
index c96ec67b..c53fd937 100755
--- a/.husky/post-checkout
+++ b/.husky/post-checkout
@@ -1,30 +1,8 @@
#!/bin/sh
FILE="$(dirname "$0")/_/husky.sh"
-
-DIFF=$(git diff --numstat $1 -- server/package.json)
-DIFF2=$(git diff --numstat $1 -- client/package.json)
-
-# Check if the diff is empty
-if [ -z "$DIFF" ]; then
- echo 'Server Package.json unchanged'
-else
- if [ ! -f "$FILE" ]; then
- cd server && npm ci --legacy-peer-deps && npm run prepare && cd ..
- else
- . "$FILE"
- cd server && npm ci --legacy-peer-deps && cd ..
- fi
-
+if [ ! -f "$FILE" ]; then
+ cd server && npm ci --legacy-peer-deps && npm run prepare && cd ../client && npm ci --legacy-peer-deps && npm run prepare && cd ..
fi
-if [ -z "$DIFF2" ]; then
- echo ' Client Package.json unchanged'
-else
- if [ ! -f "$FILE" ]; then
- cd client && npm ci --legacy-peer-deps && npm run prepare && cd ..
- else
- . "$FILE"
- cd client && npm ci --legacy-peer-deps && cd ..
- fi
-
-fi
+. "$FILE"
+cd server && npm ci --legacy-peer-deps && cd ../client && npm ci --legacy-peer-deps && cd ..
diff --git a/.husky/pre-push b/.husky/pre-push
new file mode 100755
index 00000000..5d049a87
--- /dev/null
+++ b/.husky/pre-push
@@ -0,0 +1,4 @@
+#!/bin/sh
+. "$(dirname "$0")/_/husky.sh"
+
+cd server && npm run checks && cd ../client && npm run checks && cd ..
diff --git a/client/src/FetchActions.ts b/client/src/FetchActions.ts
index c952c980..d6b118a9 100644
--- a/client/src/FetchActions.ts
+++ b/client/src/FetchActions.ts
@@ -1,4 +1,5 @@
import { APP_API_BASE_URL } from './configs';
+import { Message } from './types';
export const getRequest = (
url: string,
@@ -26,6 +27,42 @@ export const getRequest = (
}
});
};
+
+export const patchRequest = (
+ url: string,
+ body: any,
+ onSuccess: Function,
+ abortController?: AbortController,
+ onError = (statusCode?: Number, statusText?: string) => {},
+) => {
+ let options = {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ },
+ signal: abortController?.signal,
+ body: JSON.stringify(body),
+ };
+ fetch(url, options)
+ .then((resp) => {
+ if (!resp.ok) {
+ onError(resp.status, resp.statusText);
+ } else {
+ return resp.json();
+ }
+ })
+ .then((data) => onSuccess(data))
+ .catch((e) => {
+ if (e.name === 'AbortError') {
+ // usually, this abort is intentional due to a change in props
+ console.log('fetch aborted');
+ } else {
+ onError();
+ }
+ });
+};
+// best practice for fetching is to allow fetches to be aborted, in case props change while a fetch is in progress
+
export const fetchNeeds = (
limit: string,
offset: string,
@@ -54,3 +91,21 @@ export const fetchDonations = (
getRequest(assetsApiRequest.href + assetsApiRequest.hash, onSuccess, abortController, onError);
};
+
+export const fetchInbox = (
+ onSuccess: Function,
+ abortController?: AbortController,
+ onError = (statusCode?: Number, statusText?: string) => {},
+) => {
+ const assetsApiRequest = new URL(`${APP_API_BASE_URL}/transactions/inbox`);
+ getRequest(assetsApiRequest.href + assetsApiRequest.hash, onSuccess, abortController, onError);
+};
+
+export const updateMessage = (
+ messageBody: Message,
+ onSuccess: Function,
+ onError = (statusCode?: Number, statusText?: string) => {},
+) => {
+ const messageApiRequest = new URL(`${APP_API_BASE_URL}/messages/${messageBody.id}`);
+ patchRequest(messageApiRequest.href, messageBody, onSuccess, new AbortController(), () => {});
+};
diff --git a/client/src/components/FAQs.tsx b/client/src/components/FAQs.tsx
index 78606870..87853221 100644
--- a/client/src/components/FAQs.tsx
+++ b/client/src/components/FAQs.tsx
@@ -58,7 +58,7 @@ function FAQs() {
return (
-
+
diff --git a/client/src/components/Users/Auth/SetNewPassword.tsx b/client/src/components/Users/Auth/SetNewPassword.tsx
index f3eaf3e0..03939c77 100644
--- a/client/src/components/Users/Auth/SetNewPassword.tsx
+++ b/client/src/components/Users/Auth/SetNewPassword.tsx
@@ -152,7 +152,7 @@ function SetNewPassword() {
return (
-
+
diff --git a/client/src/components/Users/Auth/SignUpCitizen/SignUpCitizen.tsx b/client/src/components/Users/Auth/SignUpCitizen/SignUpCitizen.tsx
index fccd223a..8d477357 100644
--- a/client/src/components/Users/Auth/SignUpCitizen/SignUpCitizen.tsx
+++ b/client/src/components/Users/Auth/SignUpCitizen/SignUpCitizen.tsx
@@ -51,20 +51,6 @@ function SignupCitizen() {
}
}, [user]);
- useEffect(() => {
- if (submitForm) {
- registerUserMutation.mutate(formData);
- setSubmitForm(false);
- }
- }, [submitForm]);
-
- useEffect(() => {
- if (submitProfile) {
- updateProfileMutation.mutate({ file: image!, userId: user!['id'] });
- setSubmitProfile(false);
- }
- }, [submitProfile]);
-
const registerUserMutation = useMutation({
mutationFn: Endpoints.userRegister,
onSuccess: ({ data: user }) => {
@@ -82,6 +68,20 @@ function SignupCitizen() {
onError: (error: AxiosError) => console.log(error),
});
+ useEffect(() => {
+ if (submitForm) {
+ setSubmitForm(false);
+ registerUserMutation.mutate(formData);
+ }
+ }, [submitForm, setSubmitForm, formData]);
+
+ useEffect(() => {
+ if (submitProfile) {
+ updateProfileMutation.mutate({ file: image!, userId: user!['id'] });
+ setSubmitProfile(false);
+ }
+ }, [submitProfile, formData, image, user]);
+
const handleNext = (newFormData: {}, doSubmit = false) => {
setFormData((currFormData) => ({
...currFormData,
diff --git a/client/src/components/Users/Inbox/MessageCard.tsx b/client/src/components/Users/Inbox/MessageCard.tsx
index 17a96fe4..0afa4122 100644
--- a/client/src/components/Users/Inbox/MessageCard.tsx
+++ b/client/src/components/Users/Inbox/MessageCard.tsx
@@ -3,21 +3,23 @@ import { makeStyles } from 'tss-react/mui';
import type { Theme } from '@mui/material/styles';
-import type { Message } from '../../../types';
+import { Typography } from '@mui/material';
+import { Button } from '@mui/base';
const useStyles = makeStyles()((theme: Theme) => ({
currentUserMessage: {
alignSelf: 'flex-end',
- border: '1px solid black',
+ border: '1px solid lightgrey',
borderRadius: '10px',
padding: '5px',
maxWidth: '70%',
marginLeft: '30%',
+ background: 'rgba(196, 196, 196, 0.3)',
},
otherUserMessage: {
alignSelf: 'flex-start',
- background: 'rgba(196, 196, 196, 0.3)',
- border: '1px solid black',
+ background: 'lightblue',
+ border: '1px solid lightgrey',
borderRadius: '10px',
padding: '5px',
maxWidth: '70%',
@@ -25,17 +27,28 @@ const useStyles = makeStyles()((theme: Theme) => ({
}));
function MessageCard({
+ text,
+ senderName,
isCurrentUser,
- message,
+ dateString,
+ messageReadCallback,
}: {
+ senderName: string;
+ text: string;
isCurrentUser: boolean;
- message: Message;
+ dateString: string;
+ messageReadCallback: React.MouseEventHandler;
}): JSX.Element {
const { classes } = useStyles();
-
return (
- {message.user.firstName}: {message.text}
+
+ {text}
+
+
+ {dateString}
+
+
);
}
diff --git a/client/src/components/Users/Inbox/TransactionThreadCard.tsx b/client/src/components/Users/Inbox/TransactionThreadCard.tsx
index 0878ec75..54039d4e 100644
--- a/client/src/components/Users/Inbox/TransactionThreadCard.tsx
+++ b/client/src/components/Users/Inbox/TransactionThreadCard.tsx
@@ -1,39 +1,12 @@
import * as React from 'react';
-import Box from '@mui/material/Box';
-import { makeStyles } from 'tss-react/mui';
import Typography from '@mui/material/Typography';
-import Card from '@mui/material/Card';
-import CardContent from '@mui/material/CardContent';
-import PermIdentityRoundedIcon from '@mui/icons-material/PermIdentityRounded';
-
-import type { Theme } from '@mui/material/styles';
+import Divider from '@mui/material/Divider';
+import ListItemText from '@mui/material/ListItemText';
+import ListItemAvatar from '@mui/material/ListItemAvatar';
+import Avatar from '@mui/material/Avatar';
import type { Transaction, User } from '../../../types';
-
-const useStyles = makeStyles()((theme: Theme) => ({
- threadCardSelected: {
- background: 'rgba(196, 196, 196, 0.3)',
- width: '95%',
- margin: '0 auto',
- },
- threadCard: {
- background: 'white',
- width: '95%',
- margin: '0 auto',
- boxShadow: 'none',
- },
- threadCardContent: {
- padding: '10px 4px 10px 40px',
- },
- threadsSection: {
- marginRight: '20px',
- },
- threadCardTitle: {
- display: 'flex',
- flexDirection: 'row',
- justifyContent: 'space-between',
- },
-}));
+import { ListItemButton } from '@mui/material';
function TransactionThreadCard({
isSelected,
@@ -44,30 +17,77 @@ function TransactionThreadCard({
isSelected: boolean;
onClick: (transaction: Transaction) => void;
transaction: Transaction;
- user?: User;
+ user?: User | null;
}): JSX.Element {
- const { classes } = useStyles();
- const otherUser =
- user?.id === transaction.requester.id
- ? transaction.requester.firstName
- : transaction.donater.firstName;
+ const message = transaction.messages[0];
+ const renderMessage = () => {
+ if (message) {
+ return (
+
+
+ {message.sendingUserId === (user && user.id) ? ' Me: ' : ''}
+
+ {message.text}
+
+ );
+ }
+ };
+ const userOrg =
+ user && user.organizations && user.organizations[0] && user.organizations[0].organization.id;
+ const userIsClaimer = userOrg === transaction.claimer.id;
+ const isCurrentUser =
+ (userOrg && message && message.sendingOrgId === userOrg) ||
+ (user && message && message.sendingUserId === user.id);
+ const donaterIsOrg = !!transaction.donater_organization;
+
+ let otherUserName = '';
+ let otherUserImage = '' as string | undefined;
+ if (userIsClaimer) {
+ // other user is claimer
+ if (donaterIsOrg) {
+ const otherUser = transaction.donater_organization && transaction.donater_organization;
+ otherUserName = otherUser.name;
+ } else {
+ const otherUser = transaction.donater_user && transaction.donater_user;
+ otherUserName = otherUser.firstName;
+ otherUserImage = otherUser.profile_image_url;
+ }
+ } else {
+ // other user is claimer
+ otherUserName = transaction.claimer && transaction.claimer.name;
+ otherUserImage = transaction.claimer && transaction.claimer.image_url;
+ }
return (
- onClick(transaction)}
- variant={isSelected ? 'outlined' : undefined}
- >
-
-
-
- {otherUser}
-
-
-
- Re: {transaction.asset.title}
-
-
+ <>
+ onClick(transaction)}
+ >
+
+ {otherUserImage && }
+
+
+
+ {message ? (isCurrentUser ? 'To : ' : 'From: ') : ''}
+ {otherUserName}
+
+ {`Topic: ${transaction.asset.title}`}
+ >
+ }
+ secondary={renderMessage()}
+ />
+
+
+ >
);
}
diff --git a/client/src/providers/SettingsProvider.tsx b/client/src/providers/SettingsProvider.tsx
index fd8c8e1c..142aa713 100644
--- a/client/src/providers/SettingsProvider.tsx
+++ b/client/src/providers/SettingsProvider.tsx
@@ -48,9 +48,8 @@ export const SettingsProvider = ({ children }: Props) => {
const fetchCategories = async () => {
try {
const res = await fetch(`${APP_API_BASE_URL}/categories`);
- const response = (await res.json()) satisfies Category[];
if (res.ok) {
- _categoriesRef.current = [...response];
+ _categoriesRef.current = [...((await res.json()) satisfies Category[])];
}
} catch (error) {
console.log('Error fetching Categorie', error);
diff --git a/client/src/routes/routes.ts b/client/src/routes/routes.ts
index e61ef9bf..4a88ad7c 100644
--- a/client/src/routes/routes.ts
+++ b/client/src/routes/routes.ts
@@ -24,7 +24,6 @@ import Help from '../views/Help';
import ForgotPassword from '../components/Users/Auth/ForgotPassword';
import SetNewPassword from '../components/Users/Auth/SetNewPassword';
import CookiePolicy from '../views/CookiePolicy';
-import TempChat from '../views/TempChat';
import EmailVerification from '../views/EmailVerification';
import Login from '../views/Login';
@@ -172,11 +171,6 @@ const routes: RouteMap = {
roles: [],
path: '/help',
},
- TempChat: {
- component: TempChat,
- roles: [],
- path: '/chat',
- },
EmailVerification: {
component: EmailVerification,
roles: [],
diff --git a/client/src/types/index.ts b/client/src/types/index.ts
index 30579e10..08a6a0df 100644
--- a/client/src/types/index.ts
+++ b/client/src/types/index.ts
@@ -25,6 +25,7 @@ export type Organization = {
state: string;
ein: string;
nonprofit_classification: string;
+ image_url: string;
};
export enum Role {
@@ -39,12 +40,21 @@ export enum ApprovalStatus {
denied = 'DENIED',
}
+export type UserOrg = {
+ organization: Organization;
+ organizationId: number;
+ user: User;
+ role: string;
+ approvalStatus: string;
+};
+
export type User = {
id?: number;
firstName: string;
last_name?: string;
email?: string;
profile_image_url?: string;
+ organizations: UserOrg[];
city?: string;
state?: string;
zip_code?: string;
@@ -54,16 +64,21 @@ export type User = {
export type Transaction = {
id: number;
- donater: User;
- requester: User;
+ donater_user: User;
+ donater_organization: Organization;
asset: Pick;
+ claimer: Organization;
+ messages: Message[];
};
export type Message = {
id: number;
text: string;
transactionId: number;
- user: User;
+ sendingUserId: number | null;
+ sendingOrgId: number | null;
+ created_date: string;
+ read: boolean;
};
export type Option = {
diff --git a/client/src/views/Inbox.tsx b/client/src/views/Inbox.tsx
index 76f8ff6e..5bcaaee3 100644
--- a/client/src/views/Inbox.tsx
+++ b/client/src/views/Inbox.tsx
@@ -4,20 +4,19 @@ import Box from '@mui/material/Box';
import Grid from '@mui/material/Grid';
import { makeStyles } from 'tss-react/mui';
import Typography from '@mui/material/Typography';
-import TextField from '@mui/material/TextField';
-import SendOutlinedIcon from '@mui/icons-material/SendOutlined';
-import IconButton from '@mui/material/IconButton';
+import List from '@mui/material/List';
import type { Theme } from '@mui/material/styles';
import SubHeader from '../components/Users/Inbox/SubHeader';
import TransactionThreadCard from '../components/Users/Inbox/TransactionThreadCard';
-import MessageCard from '../components/Users/Inbox/MessageCard';
import { UserContext } from '../providers';
import routes from '../routes/routes';
import type { Message, Transaction } from '../types';
import { APP_API_BASE_URL } from '../configs';
+import { fetchInbox } from '../FetchActions';
+import TempChat from './TempChat';
const useStyles = makeStyles()((theme: Theme) => ({
inboxWrapper: {
@@ -27,7 +26,7 @@ const useStyles = makeStyles()((theme: Theme) => ({
textAlign: 'left',
},
sectionWrapper: {
- border: '1px solid black',
+ border: '1px solid lightgrey',
borderRadius: '10px',
display: 'flex',
flexDirection: 'column',
@@ -48,6 +47,7 @@ const useStyles = makeStyles()((theme: Theme) => ({
},
threadsSection: {
marginRight: '20px',
+ maxWidth: '30%',
},
messageInputWrapper: {
width: '100%',
@@ -76,50 +76,17 @@ const useStyles = makeStyles()((theme: Theme) => ({
},
}));
-const fetchTransactions = (): Promise => {
- return Promise.resolve([
- {
- id: 1,
- donater: { id: 1, firstName: 'firstName1' },
- requester: { id: 2, firstName: 'firstName2' },
- asset: {
- id: 1,
- title: 'title 1',
- },
- },
- {
- id: 2,
- donater: { id: 1, firstName: 'firstName1' },
- requester: { id: 3, firstName: 'firstName3' },
- asset: {
- id: 2,
- title: 'title 2',
- },
- },
- ]);
-};
-
// TODO: make the fetch find messages by transaction
// TODO: seed data so that messages appear without manually creating them
-const fetchMessages = async (): Promise => {
- const MESSAGES_API_URL = `${APP_API_BASE_URL}/messages`;
+const fetchMessages = async (id: number): Promise => {
+ const MESSAGES_API_URL = `${APP_API_BASE_URL}/transactions/${id}`;
const res = await fetch(MESSAGES_API_URL);
const data = await res.json();
-
- const messages = await data.map((message: any) => {
- return {
- id: message.id,
- text: message.text,
- transactionId: message.transaction_id,
- user: {
- id: message.user.id,
- firstName: message.user.firstName,
- },
- };
- });
-
- return messages;
+ return data.messages.sort(
+ (message1: Message, message2: Message) =>
+ (new Date(message1.created_date) as any) - (new Date(message2.created_date) as any),
+ );
};
// TODO use SubHeader component in Offer and Assets pages
@@ -132,16 +99,13 @@ function MessageInboxView(): JSX.Element {
const [selectedTransaction, setSelectedTransaction] = React.useState(null);
const [messages, setMessages] = React.useState([]);
- const handleSendMessage = () => {};
-
// todo switch to custom hook
React.useEffect(() => {
if (user) {
- (async function () {
- const transactions = await fetchTransactions(); // user.id
- setTransactions(transactions);
+ fetchInbox((transactions: Transaction[]) => {
setSelectedTransaction(transactions[0]);
- })();
+ setTransactions(transactions);
+ }); // user.id
}
}, [user]);
@@ -149,12 +113,48 @@ function MessageInboxView(): JSX.Element {
React.useEffect(() => {
if (selectedTransaction) {
(async function () {
- const messages = await fetchMessages();
+ const messages = await fetchMessages(selectedTransaction.id);
setMessages(messages);
})();
}
}, [selectedTransaction]);
+ const transactionCards = (
+
+ {transactions &&
+ transactions.length &&
+ transactions.map((t) => (
+ setSelectedTransaction(transaction)}
+ transaction={t}
+ user={user}
+ />
+ ))}
+
+ );
+
+ const noTransactionsMessage = (
+
+ Inbox empty
+
+ Support an organization by contributingsomething they
+ need
+
+ or
+
+ {/* update to prop to use routes once set up */}
+ Post a need
+
+
+ );
+
+ const transactionList = (
+
+ {transactions.length && transactions.length > 0 ? transactionCards : noTransactionsMessage}
+
+ );
+
if (!user) {
return ;
} else {
@@ -162,62 +162,17 @@ function MessageInboxView(): JSX.Element {
<>
-
-
- Inbox
-
- {transactions.length ? (
- transactions.map((t) => (
- setSelectedTransaction(transaction)}
- transaction={t}
- user={user}
- />
- ))
- ) : (
-
- Inbox empty
-
- Support an organization by contributing{' '}
- something they need
-
- or
-
- {/* update to prop to use routes once set up */}
- Post a need
-
-
- )}
-
+ {transactionList}
{selectedTransaction && (
-
-
- Re: {selectedTransaction.asset.title}
-
- {messages?.map((m) => (
-
- ))}
-
+
)}
-
-
-
>
diff --git a/client/src/views/Main.tsx b/client/src/views/Main.tsx
index 7629b4f6..67ef540f 100644
--- a/client/src/views/Main.tsx
+++ b/client/src/views/Main.tsx
@@ -29,7 +29,6 @@ const {
OfferFormSkills,
ContactUs,
Help,
- TempChat,
EmailVerification,
Login,
} = routes;
@@ -113,7 +112,6 @@ function Main() {
{/* support */}
-
diff --git a/client/src/views/Messages.tsx b/client/src/views/Messages.tsx
index 3460d466..0e8a6bb4 100644
--- a/client/src/views/Messages.tsx
+++ b/client/src/views/Messages.tsx
@@ -1,46 +1,50 @@
-import React, { useEffect, useState } from 'react';
+import React from 'react';
+import { Message, Transaction } from '../types';
+import MessageCard from '../components/Users/Inbox/MessageCard';
+import { patchRequest, updateMessage } from '../FetchActions';
-interface IMessage {
- text: string;
- name: string;
- id: string;
- created_date: Date;
-}
-
-function Messages({ socket }: any) {
- const [messages, setMessages] = useState([]);
- const [showTypingGesture, setShowTypingGesture] = useState(false);
- const [userTyping, setUserTyping] = useState('');
-
- useEffect(() => {
- socket.on('message', (res: IMessage[]) => {
- setMessages(res);
- });
-
- socket.on('typing', (res: { name: string; isTyping: boolean }) => {
- setShowTypingGesture(res.isTyping);
- setUserTyping(res.name);
- });
-
- socket.emit('findAllPocChat', (res: IMessage[]) => {
- setMessages(res);
- });
- }, [socket]);
-
- const formatDate = (date: Date): string => {
+function Messages({ messages, transaction, user }: any) {
+ const formatDate = (date: string): string => {
return new Date(date).toLocaleString('en-US');
};
+ const getSenderName = (transaction: Transaction, message: Message) => {
+ if (message.sendingOrgId) {
+ if (message.sendingOrgId === transaction.claimer?.id) {
+ return transaction.claimer.name;
+ } else if (message.sendingOrgId === transaction.donater_organization?.id) {
+ return transaction.donater_organization.name;
+ } else {
+ return '';
+ }
+ } else if (message.sendingUserId === (user && user.id)) {
+ return user.firstName;
+ } else {
+ return transaction.donater_user?.firstName;
+ }
+ };
+
return (
- {messages.map((message: IMessage) => (
-
- {formatDate(message.created_date)}:
- {message.name}:
- {message.text}
-
- ))}
- {showTypingGesture &&
... {userTyping} is typing
}
+ {messages.map((message: Message) => {
+ const markMessageRead: React.MouseEventHandler
= (event) =>
+ updateMessage(
+ message,
+ () => {},
+ () => {
+ console.log('error');
+ },
+ );
+ return (
+
+ );
+ })}
);
}
diff --git a/client/src/views/NewMessage.tsx b/client/src/views/NewMessage.tsx
index 608d44ed..359aaf7e 100644
--- a/client/src/views/NewMessage.tsx
+++ b/client/src/views/NewMessage.tsx
@@ -1,11 +1,15 @@
+import { SendOutlined } from '@mui/icons-material';
+import { IconButton, TextField } from '@mui/material';
import React, { useState } from 'react';
-const NewMessage = ({ socket }: any) => {
+const NewMessage = ({ socket, transactionId, classes }: any) => {
const [value, setValue] = useState('');
const submitForm = (e: any) => {
e.preventDefault();
- socket.emit('createPocChat', { text: value });
- setValue('');
+ if (socket.connected) {
+ socket.emit('message', { text: value, transactionId: transactionId, fromClaimer: false });
+ setValue('');
+ }
};
const handleOnChange = (e: any): void => {
@@ -23,8 +27,9 @@ const NewMessage = ({ socket }: any) => {
};
return (
-
);
};
diff --git a/client/src/views/SearchResults.tsx b/client/src/views/SearchResults.tsx
index 569f6319..1d61444a 100644
--- a/client/src/views/SearchResults.tsx
+++ b/client/src/views/SearchResults.tsx
@@ -85,7 +85,7 @@ function SearchResults(): JSX.Element {
// TODO: create Volunteer Type in types/index.ts and change Object[] to Volunteer[] below
const [volunteer, setVolunteer] = React.useState