The Admin Page
The admin page
We want the admin to have various capabilities:
- Create a poll
- Invite users to take the poll
- Delete users that should not take the poll
- View the poll results
We’re going to work on functionality 1-3 right now and save the poll results for after we make the poll. The first two capabilities are provided in the file below. You’ll implement the third capability.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
import { View, Text, TextInput, Button, StyleSheet, Alert } from "react-native";
import { useState } from "react";
import { createUserWithEmailAndPassword, signInWithEmailAndPassword} from "firebase/auth";
import { auth, db } from "@/src/config/firebaseConfig";
import { adminEmail, adminPassword } from "@/src/util/hidden";
import { doc, getDoc, setDoc, deleteDoc, collection, getDocs } from "firebase/firestore";
import { router } from "expo-router";
const maxOptions = 5;
export default function Admin() {
const [numOptions, setNumOptions] = useState("");
const [options, setOptions] = useState<string[]>([]);
const [inviteEmail, setInviteEmail] = useState("");
const [question, setQuestion] = useState("");
/* Generates a password 12 characters long */
const generatePassword = (length = 12) => {
const characters =
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
let password = "";
for (let i = 0; i < length; i++) {
password += characters.charAt(
Math.floor(Math.random() * characters.length)
);
}
return password;
};
/* updates UI when number of options changes */
const handleNumOptionsChange = (value: string) => {
setNumOptions(value);
const number = Number(value);
setOptions(Array(number).fill(""));
};
/* updates the list of options when admin changes one */
const handleOptionChange = (index: number, value: string) => {
const newOptions = [...options];
newOptions[index] = value;
setOptions(newOptions);
};
const inviteGuest = async () => {
const userRef = doc(db, "userPasswords", inviteEmail);
const userSnap = await getDoc(userRef);
if (userSnap.exists()) {
const oldPassword = userSnap.data().password;
Alert.alert(
"User Already Created - save these credentials",
`Email: ${inviteEmail}\nPassword: ${oldPassword}`,
[{ text: "OK" }]
);
return;
}
const invitePassword = generatePassword();
try {
await createUserWithEmailAndPassword(
auth,
inviteEmail,
invitePassword
);
await setDoc(doc(db, "userPasswords", inviteEmail), {
email: inviteEmail,
password: invitePassword,
});
await signInWithEmailAndPassword(
auth,
adminEmail,
adminPassword
);
console.log("User created and admin signed back in.");
Alert.alert(
"User Created - save these credentials",
`Email: ${inviteEmail}\nPassword: ${invitePassword}`,
[{ text: "OK" }]
);
} catch (error) {
console.log(error);
}
}
const setPollContent = async () => {
if (!question || options.length === 0) {
Alert.alert("Error", "Please enter a question and at least one option.");
return;
}
try {
const resultsSnap = await getDocs(collection(db, "Results"));
await Promise.all(
resultsSnap.docs.map((resultDoc) =>
deleteDoc(resultDoc.ref)
));
await setDoc(doc(db, "polls", "currentPoll"), {
question: question,
options: options,
});
Alert.alert("Success", "Poll saved!");
Alert.alert("Warning", "This will overwrite any previous polls submitted.");
setOptions([]);
setNumOptions("");
setQuestion("");
} catch (error) {
console.error(error);
Alert.alert("Error", "Could not save poll.");
}
}
return (
<View style={styles.container}>
<Text style={styles.title}>Admin Panel</Text>
<Text style={styles.sectionTitle}>Poll</Text>
<TextInput
style={styles.input}
placeholder="Question"
value={question}
onChangeText={setQuestion}
/>
<TextInput
style={styles.input}
placeholder="Number of options"
keyboardType="numeric"
value={numOptions}
onChangeText={handleNumOptionsChange}
/>
{options.map((option, index) => (
<TextInput
key={index}
style={styles.input}
placeholder={`Option ${index + 1}`}
value={option}
onChangeText={(value) => handleOptionChange(index, value)}
/>
))}
<View style={styles.buttonRow}>
<Button
title="Set Poll Content"
onPress={setPollContent}
/>
<Button
title = "Results"
onPress={() => router.replace("/results")}
/>
</View>
<Text style={styles.sectionTitle}>Guest Management</Text>
<TextInput
style={styles.input}
placeholder="Guest email"
keyboardType="email-address"
value={inviteEmail}
onChangeText={setInviteEmail}
autoCapitalize="none"
/>
<View style={styles.buttonRow}>
<Button title="Invite Guest" onPress={inviteGuest} />
<Button title="Remove Guest"/>
</View>
</View>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
padding: 20,
},
title: {
fontSize: 28,
fontWeight: "bold",
textAlign: "center",
marginBottom: 30,
},
sectionTitle: {
fontSize: 20,
fontWeight: "bold",
marginBottom: 10,
marginTop: 20,
},
input: {
borderWidth: 1,
borderColor: "#aaa",
borderRadius: 5,
padding: 10,
marginBottom: 15,
},
buttonRow: {
flexDirection: "row",
justifyContent: "space-around",
marginTop: 5,
},
});
NOT all the imports are being used. However, they will all be a part of the final product, so keep them for now and they will serve as hints for what you need to add.
You need to add results.tsx to your project! This will be where we create a visualization of the results later on in the project
Walking through the code
Let’s take a look at the key functional components of admin.tsx:
First, there are more helper functions in this file than we’ve had before. There are 3 small helper functions that are self-explanatory.
One of those three helper functions could use the following code to make sure that the admin can’t choose an unreasonable amount of options for the poll:
1
2
3
if (parseInt(value) > maxOptions){
value = maxOptions.toString();
}
You should be able to find where it goes and understand what it’s doing if you look through the helper functions.
The last two helper functions are the most important for the screen! These functions are:
- inviteGuest
- setPollContent
We’ll go through each of these in detail next
inviteGuest
The code to invite a guest is similar to the code that we used to create our admin user. We ultimately want to use createUserWithEmailAndPassword() from firebase auth to add another user.
One important thing to note is that we need to check and see if that email has already been invited. We access the user data from the database (if it exists) and then based on whether it exists we make a decision whether to create a new user or not.
When you use createUserWithEmailAndPassword() it automatically signs in the new user. We want the admin to stay signed in. Pay attention to how this is done
After the new user is made we want to show the admin their credentials so that they can copy them and distribute them to another user. We won’t be doing this, but a good practice to expand upon this would be to give the admin an option to send emails with credentials included or another way to distribute login information.
setPollContent
When we make a new poll we want to do two things:
(1) Reset any results from a previous poll that was made
(2) Create a new poll in the database so that it can be accessed when a guest user logs in
The most important thing to pay attention to is how to add and remove things from the database. You’ll be needing to do this on your own in the coming sections.
Once you feel confident adding and removing from the database you are ready to write some code!
Removing Users
An admin may want to remove a user from the poll. Perhaps they typed in the email wrong or they are changing the poll and who it pertains to. We already have the button to delete a user in the admin screen, but we need to get it working!
First, the following code is going to go in login.tsx before we log in the admin (you should be able to find where this is).
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
if (user.email !== adminEmail) {
const removeRef = doc(db, "toRemove", email);
const removeSnap = await getDoc(removeRef);
if (removeSnap.exists()) {
await deleteDoc(doc(db, "Results", user.uid));
await deleteDoc(doc(db, "userPasswords", email));
await deleteUser(user);
await deleteDoc(removeRef);
setError("This account has been removed.");
return;
}
const resultsRef = doc(db, "Results", user.uid);
const resultsSnap = await getDoc(resultsRef);
if (resultsSnap.exists()) {
router.replace("/completed");
return;
}
router.replace("/poll");
return;
}
You need to include the following imports: doc, getDoc, deleteDoc from firebase/firestore, db from our config file, and deleteUser from firebase/auth
This code deletes a user and all related fields if they have been added to a “toRemove” doc by the admin.
Your job is to add the email to the “toRemove” document when the admin uses the deleteUser button!
- I would recommend also giving the admin user an alert that lets them know the user has successfully been marked for removal.
Don’t forget to add an onPress statement! And pay attention to the name of the document!
Before this works you will also need to create a completed.tsx and poll.tsx files. We will be using these later. The completed screen is redirected to if the user has already completed the poll, and poll.tsx is routed to if the user still needs to take the poll.