Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Edit protest flow v3 #142

Closed
wants to merge 16 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
76 changes: 70 additions & 6 deletions src/api/index.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import firebase, { firestore } from '../firebase';
import * as geofirestore from 'geofirestore';
import { arrayToHashMap } from '../utils';
const GeoFirestore = geofirestore.initializeApp(firestore);

// async function verifyRecaptcha(token) {
Expand Down Expand Up @@ -120,6 +121,16 @@ export async function fetchProtest(protestId) {
}
}

export async function fetchUser(userId) {
const user = await firestore.collection('users').doc(userId).get();

if (user.exists) {
return user.data();
} else {
return false;
}
}

export async function uploadFile(params) {
const request = await fetch('http://localhost:5001/onekm-50c7f/us-central1/uploadImage', {
method: 'post',
Expand Down Expand Up @@ -180,6 +191,16 @@ export async function getProtestsForLeader(uid) {
return protests;
}

export async function fetchProtestsById(ids) {
const protests = await firestore.collection('protests').where(firestore.FieldPath.documentId(), 'in', ids).get();
return protests.map((doc) => doc.data());
}

export async function fetchUsersById(ids) {
const users = await firestore.collection('users').where(firestore.FieldPath.documentId(), 'in', ids).get();
return users.map((doc) => doc.data());
}

export function createLeaderRequestId(userId, protestId) {
return `${userId}${protestId}`;
}
Expand All @@ -198,6 +219,19 @@ export async function setPhoneNumberForUser(uid, phoneNumber) {
await firestore.collection('users').doc(uid).update({ phoneNumber });
}

export async function setProtestEditsForUser(user, protestId) {
await firestore
.collection('users')
.doc(user.uid)
.update({
edits: [...new Set([...(user.edits || []), protestId])],
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you want to push the new protestsId into the edits list, I would use the following - https://firebase.google.com/docs/reference/js/firebase.firestore.FieldValue#arrayunion

It has been done similarly here -

'roles.leader': firebase.firestore.FieldValue.arrayUnion(userId),

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Each specified element that doesn't already exist in the array will be added to the end.

According to the docs we don't need to use a Set to keep the array unique in firestore.

});
}

export async function setEditAsViewed(id) {
firestore.collection('protest_edits').doc(id).update({ status: 'viewed' });
}

// return true is the protest exist in the database
export async function isProtestValid(protestId) {
try {
Expand Down Expand Up @@ -264,6 +298,32 @@ export function handleSignIn() {
firebase.auth().signInWithRedirect(provider);
}

export async function createProtestEdit(userId, protestId, diff) {
await firestore.collection('protest_edits').add({
userId,
protestId,
diff,
created_at: firebase.firestore.FieldValue.serverTimestamp(),
status: 'pending',
});
}

export async function fetchPendingEdits() {
const editsRef = firestore.collection('protest_edits');
const query = editsRef.where('status', '==', 'pending');

const querySnapshot = await query.get();
const edits = [];

querySnapshot.forEach(function (doc) {
edits.push({ id: doc.id, ...doc.data() });
});

console.log('edits in function', edits);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
console.log('edits in function', edits);


return edits;
}

///////////////////////////////////////////////////////
// functions to be used by the admin page
// in order to show data and complete the process of
Expand All @@ -282,15 +342,19 @@ export async function listLeaderRequests() {
return leaderRequests;
}

export async function makeUserProtestLeader(protestId, userId) {
return firestore
.collection('protests')
.doc(protestId)
.update({
'roles.leader': firebase.firestore.FieldValue.arrayUnion(userId),
});
}

// When super-admin approves a protest-user request
export async function assignRoleOnProtest({ userId, protestId, requestId, status, adminId }) {
if (status === 'approved') {
await firestore
.collection('protests')
.doc(protestId)
.update({
'roles.leader': firebase.firestore.FieldValue.arrayUnion(userId),
});
await makeUserProtestLeader(protestId, userId);
}

// Update request
Expand Down
4 changes: 2 additions & 2 deletions src/components/Button/Button.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,9 @@ import React from 'react';
import styled from 'styled-components/macro';

export default function Button(props) {
const { color, type, onClick, disabled, style, icon, children } = props;
const { color, type, onClick, disabled, style, icon, children, className } = props;
return (
<ButtonWrapper color={color} type={type} onClick={onClick} disabled={disabled} style={style}>
<ButtonWrapper color={color} type={type} onClick={onClick} disabled={disabled} style={style} className={className}>
{icon && <ButtonIcon src={icon} alt="" aria-hidden="true" />}
<span style={{ paddingBottom: 3 }}>{children}</span>
</ButtonWrapper>
Expand Down
21 changes: 21 additions & 0 deletions src/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,27 @@ export function isValidUrl(link) {
return url.protocol === 'http:' || url.protocol === 'https:';
}

export function arrayToHashMap(arr, key) {
const _hashMap = {};
arr.forEach(function (obj){
_hashMap[obj[key]] = obj;
});
return _hashMap;
};

export function objectDiff(oldObj, newObj) {
let diff = Object.keys(newObj).reduce((diff, key) => {
if (JSON.stringify(oldObj[key]) === JSON.stringify(newObj[key])) return diff
return {
...diff,
[key]: { oldValue: oldObj[key], newValue: newObj[key]}
}
}, {});

return diff;
}


export const isLeader = (user, protest) => protest?.roles?.leader?.includes(user?.uid);
export const isAdmin = (user) => user?.admin === true;
export const isVisitor = (user) => user === 'visitor';
Expand Down
138 changes: 138 additions & 0 deletions src/views/Admin/EditsAdmin.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
import { useEffect } from 'react';
import { fetchPendingEdits, fetchProtest, fetchUser, setEditAsViewed } from '../../api';
import React, { useState } from 'react';
import { formatDate } from '../../utils';

function getFieldName(fieldKey) {
switch (fieldKey) {
case 'displayName':
return 'שם המקום';
case 'streetAddress':
return 'כתובת';
case 'coords':
return 'קואורדינטות';
case 'whatsAppLink':
return 'לינק לוואצאפ';
case 'telegramLink':
return 'לינק לטלגרם';
case 'notes':
return 'הערות';
case 'dateTimeList':
return 'תאריך ושעה';
default:
return '';
}
}

function EditField({ diff, keyName, type }) {
const value = (diff[keyName] || {})[type];

switch (keyName) {
case 'dateTimeList':
return value.map((dt) => (
<div key={`${dt.date}-${dt.time}`}>
{formatDate(dt.date)} - {dt.time}
</div>
));
default:
return <div>{value}</div>;
}
}

function EditRow({ created_at, diff = {}, userId, protestId, id }) {
const [expanded, setExpanded] = useState(false);
const [protest, setProtest] = useState(null);
const [user, setUser] = useState(null);
const [viewed, setViewed] = useState(false);

useEffect(() => {
if (expanded) {
fetchProtest(protestId).then((p) => {
if (p) {
setProtest(p);
}
});

fetchUser(userId).then((u) => {
if (u) {
setUser(u);
}
});
}
}, [expanded, protestId, userId]);

if (viewed) {
return null;
}

return (
<>
<div style={{ display: 'flex', alignItems: 'center' }}>
<div>{formatDate(created_at.toDate())}</div>
<div>
{Object.keys(diff).map((key) => (
<div style={{ padding: '16px', display: 'flex' }} key={key}>
<div style={{ marginBottom: '8px' }}>
<b>{getFieldName(key)}</b>
</div>
<div style={{ paddingRight: '10px' }}>
<span style={{ textDecoration: 'line-through' }}>
<EditField keyName={key} diff={diff} type="oldValue" />
</span>
<span>
<EditField keyName={key} diff={diff} type="newValue" />
</span>
</div>
</div>
))}
</div>
<div onClick={() => setExpanded(true)}>פרטים נוספים</div>
<button
onClick={() => {
setEditAsViewed(id);
setViewed(true);
}}
>
ראיתי
</button>
</div>

{expanded && protest && user && (
<div>
{JSON.stringify(protest)} + {JSON.stringify(user)}
</div>
)}
</>
);
}

async function _fetchPendingEdits(setEdits) {
const result = await fetchPendingEdits();
setEdits(result);
}

function useFetchEdits() {
const [edits, setEdits] = useState(null);

useEffect(() => {
_fetchPendingEdits(setEdits);
}, []);

return {
edits,
};
}
export default function EditsAdmin() {
const { edits } = useFetchEdits();
if (!edits) {
return <div>Loading...</div>;
}

return (
<div>
{edits.map((edit) => (
<EditRow {...edit} key={edit.uid} />
))}
</div>
);
}
12 changes: 2 additions & 10 deletions src/views/Admin/components.js
Original file line number Diff line number Diff line change
Expand Up @@ -37,9 +37,7 @@ export const LeaderCard = styled.div`
`;

export const AdminNavButton = styled(Button)`
width: 140px;
height: 35px;
font-size: 14px;
margin-bottom: 20px;
`;

export const SidebarList = styled.ul`
Expand Down Expand Up @@ -89,13 +87,7 @@ export const LeaderPhoto = styled.img`
border-radius: 50%;
`;

export const AdminNavigation = styled(Link)`
grid-column: 2/2;
position: absolute;
left: 0;
top: 15px;
grid-row: 1/1;
`;
export const AdminNavigation = styled(Link)``;

export const SidebarListHead = styled.div`
padding: 0 20px;
Expand Down
28 changes: 19 additions & 9 deletions src/views/Admin/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { Button } from '../../components';
import { AdminWrapper, AdminNavigation, AdminNavButton } from './components';
import LeaderAdmin from './LeaderAdmin';
import ProtestAdmin from './ProtestAdmin';
import EditsAdmin from './EditsAdmin';
import { handleSignIn } from '../../api';
import { isAdmin } from '../../utils';

Expand All @@ -16,6 +17,21 @@ const Admin = ({ user }) => {
<div>טוען...</div>
) : user ? (
<>
<div style={{ display: 'flex' }}>
<AdminNavigation to="/admin/protest-requests">
<AdminNavButton disabled={location.pathname.includes('protest-requests')}>אישור הפגנות</AdminNavButton>
</AdminNavigation>
<AdminNavigation
to="/admin/leader-requests"
disabled={location.pathname.includes('leader-requests')}
style={{ height: 'min-content' }}
>
<AdminNavButton>אישור מובילים</AdminNavButton>
</AdminNavigation>
<AdminNavigation to="/admin/edits" disabled={location.pathname.includes('edits')} style={{ height: 'min-content' }}>
<AdminNavButton>אישור עריכות</AdminNavButton>
</AdminNavigation>
</div>
<Switch>
{!isAdmin(user) && <Redirect to="/" />}
<Route exact path="/admin">
Expand All @@ -27,16 +43,10 @@ const Admin = ({ user }) => {
<Route path="/admin/leader-requests/:leaderId?">
<LeaderAdmin user={user} />
</Route>
<Route path="/admin/edits">
<EditsAdmin />
</Route>
</Switch>
{['/admin/protest-requests/', '/admin/protest-requests'].includes(location.pathname) ? (
<AdminNavigation to="/admin/leader-requests" style={{ height: 'min-content' }}>
<AdminNavButton>אישור מובילים</AdminNavButton>
</AdminNavigation>
) : (
<AdminNavigation to="/admin/protest-requests">
<AdminNavButton>אישור הפגנות</AdminNavButton>
</AdminNavigation>
)}
</>
) : (
<Button onClick={handleSignIn}>התחבר למערכת</Button>
Expand Down
Loading