diff --git a/packages/app/Root.js b/packages/app/Root.js
index 7ca9e08b..c69011c8 100644
--- a/packages/app/Root.js
+++ b/packages/app/Root.js
@@ -58,7 +58,7 @@ function App() {
} catch (err) {
await logoutUser();
console.log(
- "Something went wrong trying to auto log in with stored access token"
+ "Something went wrong trying to auto log in with stored access token",
);
}
@@ -89,7 +89,7 @@ function App() {
} catch (err) {
await logoutUser();
console.log(
- "Something went wrong trying to auto log in with newly fetched access token from stored refresh token"
+ "Something went wrong trying to auto log in with newly fetched access token from stored refresh token",
);
}
};
diff --git a/packages/app/app.config.js b/packages/app/app.config.js
index ad74d11d..90d6f683 100644
--- a/packages/app/app.config.js
+++ b/packages/app/app.config.js
@@ -55,5 +55,12 @@ module.exports = {
iosUrlScheme: process.env.IOS_GOOGLE_URL_SCHEME,
},
],
+ [
+ "expo-image-picker",
+ {
+ photosPermission:
+ "Allow Icebreak to access your photos for choosing profile icons and thumbnail images.",
+ },
+ ],
],
};
diff --git a/packages/app/components/Dropdown.js b/packages/app/components/Dropdown.js
new file mode 100644
index 00000000..9dbdb1d7
--- /dev/null
+++ b/packages/app/components/Dropdown.js
@@ -0,0 +1,84 @@
+import PropTypes from "prop-types";
+import React, { useState } from "react";
+import { TouchableOpacity, Text, View, StyleSheet } from "react-native";
+import { ScrollView } from "react-native-gesture-handler";
+
+const Dropdown = ({ options, value, setValue }) => {
+ const [isOpen, setIsOpen] = useState(false);
+
+ const toggleDropdown = () => {
+ setIsOpen(!isOpen);
+ };
+
+ const selectOption = (option) => {
+ setValue(option);
+ setIsOpen(false);
+ };
+
+ return (
+
+
+
+ {value || "Select an option"}
+
+
+
+ {isOpen && (
+
+ {options.map((option) => (
+ selectOption(option)}>
+ {option}
+
+ ))}
+
+ )}
+
+ );
+};
+
+const colors = {
+ white: "#fff",
+ grey: "#ccc",
+ black: "#333",
+};
+
+const styles = StyleSheet.create({
+ dropdownButton: {
+ borderColor: colors.grey,
+ borderRadius: 4,
+ borderWidth: 1,
+ padding: 10,
+ },
+ dropdownButtonText: {
+ color: colors.blue,
+ fontSize: 16,
+ },
+ dropdownContainer: {
+ backgroundColor: colors.white,
+ borderRadius: 4,
+ padding: 10,
+ },
+ optionButton: {
+ padding: 10,
+ },
+ optionText: {
+ color: colors.blue,
+ fontSize: 16,
+ },
+ optionsContainer: {
+ marginTop: 10,
+ maxHeight: 200,
+ },
+});
+
+Dropdown.propTypes = {
+ options: PropTypes.arrayOf(PropTypes.oneOfType([PropTypes.string]))
+ .isRequired,
+ value: PropTypes.string.isRequired,
+ setValue: PropTypes.func.isRequired,
+};
+
+export default Dropdown;
diff --git a/packages/app/components/TagInput.js b/packages/app/components/TagInput.js
new file mode 100644
index 00000000..6851381e
--- /dev/null
+++ b/packages/app/components/TagInput.js
@@ -0,0 +1,108 @@
+import PropTypes from "prop-types";
+import React from "react";
+import { TextInput, Button, View, Text, StyleSheet } from "react-native";
+
+const TagInput = ({ value, setValue, tags, setTags, maxTags }) => {
+ const addTag = () => {
+ if (value && tags.length < maxTags && !tags.includes(value)) {
+ setTags([...tags, value]);
+ setValue("");
+ }
+ };
+
+ const removeTag = (index) => {
+ const updatedTags = [...tags];
+ updatedTags.splice(index, 1);
+ setTags(updatedTags);
+ };
+
+ return (
+
+
+ {
+ setValue(newText);
+ }}
+ placeholder="Enter a tag"
+ style={styles.input}
+ />
+
+
+
+ {tags.map((tag, index) => (
+
+ {tag}
+
+ ))}
+
+
+ );
+};
+
+const colors = {
+ blue: "#ff0000",
+ red: "#e0e0e0",
+};
+
+const styles = StyleSheet.create({
+ container: {
+ alignItems: "flex-start",
+ flexDirection: "column",
+ justifyContent: "flex-start",
+ },
+ input: {
+ borderWidth: 1,
+ flex: 1,
+ height: 40,
+ margin: 6,
+ padding: 10,
+ },
+ inputContainer: {
+ alignItems: "center",
+ flexDirection: "row",
+ justifyContent: "space-between",
+ },
+ removeButton: {
+ backgroundColor: colors.blue,
+ borderRadius: 4,
+ paddingHorizontal: 4,
+ paddingVertical: 2,
+ },
+ tag: {
+ alignItems: "center",
+ backgroundColor: colors.red,
+ borderRadius: 8,
+ flexDirection: "row",
+ margin: 4,
+ paddingHorizontal: 4,
+ paddingVertical: 1,
+ },
+ tagText: {
+ marginRight: 2,
+ },
+ tagsContainer: {
+ flexDirection: "row",
+ flexWrap: "wrap",
+ },
+});
+
+TagInput.propTypes = {
+ value: PropTypes.string.isRequired,
+ setValue: PropTypes.func.isRequired,
+ tags: PropTypes.arrayOf(PropTypes.string).isRequired,
+ setTags: PropTypes.func.isRequired,
+ maxTags: PropTypes.number.isRequired,
+};
+
+export default TagInput;
diff --git a/packages/app/package.json b/packages/app/package.json
index 020d3dd5..91e3dd6d 100644
--- a/packages/app/package.json
+++ b/packages/app/package.json
@@ -25,12 +25,14 @@
"expo-auth-session": "~5.4.0",
"expo-constants": "~15.4.5",
"expo-dev-client": "~3.3.10",
+ "expo-image-picker": "~14.7.1",
"expo-random": "~13.6.0",
"expo-secure-store": "~12.8.1",
"expo-status-bar": "~1.11.1",
"expo-web-browser": "~12.8.2",
"prop-types": "^15.8.1",
"react": "18.2.0",
+ "react-hook-form": "^7.51.2",
"react-native": "0.73.6",
"react-native-gesture-handler": "~2.14.0",
"react-native-option-menu": "^1.1.3",
diff --git a/packages/app/screens/feed/FeedDrawer.js b/packages/app/screens/feed/FeedDrawer.js
index 4e5fdb20..815ba536 100644
--- a/packages/app/screens/feed/FeedDrawer.js
+++ b/packages/app/screens/feed/FeedDrawer.js
@@ -15,6 +15,8 @@ import PropTypes from "prop-types";
import Ionicons from "@expo/vector-icons/Ionicons";
+import CreateGroupStack from "./create_group_form/CreateGroupStack";
+
const Feed = createDrawerNavigator();
const DARK_BLUE = "darkblue";
@@ -61,6 +63,13 @@ function FeedDrawer() {
headerShown: false,
}}
/>
+
);
}
diff --git a/packages/app/screens/feed/create_group_form/CreateGroupFormScreen.js b/packages/app/screens/feed/create_group_form/CreateGroupFormScreen.js
new file mode 100644
index 00000000..a83933f8
--- /dev/null
+++ b/packages/app/screens/feed/create_group_form/CreateGroupFormScreen.js
@@ -0,0 +1,643 @@
+import PropTypes from "prop-types";
+import React, { useState, useContext } from "react";
+import {
+ View,
+ Text,
+ TextInput,
+ TouchableWithoutFeedback,
+ Keyboard,
+ Image,
+ Switch,
+ Alert,
+ KeyboardAvoidingView,
+ Platform,
+} from "react-native";
+import Button from "@app/components/Button";
+import * as ImagePicker from "expo-image-picker";
+import { GroupContext } from "@app/utils/GroupContext";
+import { ScrollView } from "react-native-gesture-handler";
+import { styles } from "./CreateGroupFormStyles";
+import Dropdown from "@app/components/Dropdown";
+import TagInput from "@app/components/TagInput";
+import { useForm, Controller } from "react-hook-form";
+
+function CreateGroupFormScreen({ navigation }) {
+ const {
+ name,
+ setName,
+ handler,
+ setHandler,
+ description,
+ setDescription,
+ banner,
+ setBanner,
+ icon,
+ setIcon,
+
+ category,
+ setCategory,
+ tags,
+ setTags,
+ location,
+ setLocation,
+ website,
+ setWebsite,
+ isInviteOnly,
+ setIsInviteOnly,
+
+ twitterLink,
+ setTwitterLink,
+ facebookLink,
+ setFacebookLink,
+ instagramLink,
+ setInstagramLink,
+ discordLink,
+ setDiscordLink,
+ linkedInLink,
+ setLinkedInLink,
+ githubLink,
+ setGithubLink,
+
+ isHandlerUnique,
+ resetForm,
+ submitForm,
+ } = useContext(GroupContext);
+
+ const {
+ control,
+ handleSubmit,
+ formState: { errors },
+ } = useForm({
+ defaultValues: {
+ name,
+ handler,
+ description,
+ banner,
+ icon,
+ category,
+ tags_input: "",
+ location,
+ website,
+ isInviteOnly,
+ twitterLink,
+ facebookLink,
+ instagramLink,
+ discordLink,
+ linkedInLink,
+ githubLink,
+ },
+ mode: "onBlur",
+ });
+
+ const categoryOptions = ["Sports", "Education", "Business", "Gaming"];
+ const toggleSwitch = () => setIsInviteOnly((previousState) => !previousState);
+ const [isButtonDisabled, setIsButtonDisabled] = useState(false);
+
+ const selectImage = async (imageType) => {
+ Keyboard.dismiss();
+
+ const result = await ImagePicker.launchImageLibraryAsync({
+ mediaTypes: ImagePicker.MediaTypeOptions.Images,
+ allowsEditing: true,
+ aspect: [1, 1],
+ quality: 0.2,
+ base64: true,
+ });
+
+ const { assets } = result;
+ if (!result.canceled) {
+ // Process the selected image
+ const { base64 } = assets[0];
+ if (imageType === "banner") {
+ setBanner(base64);
+ } else if (imageType === "icon") {
+ setIcon(base64);
+ }
+ }
+ };
+
+ // Usage
+ const selectBannerImage = () => {
+ selectImage("banner");
+ };
+
+ const selectIconImage = () => {
+ selectImage("icon");
+ };
+
+ const onSubmit = async () => {
+ try {
+ const isSubmitted = await submitForm();
+
+ if (isSubmitted) {
+ navigation.navigate("Initial Create Group");
+ }
+ } catch (error) {
+ Alert.alert("Error", "Failed to submit form.");
+ console.error("Error submitting form:", error);
+ } finally {
+ setIsButtonDisabled(false);
+ }
+ };
+
+ const validateHandler = async (value) => {
+ try {
+ const isUnique = await isHandlerUnique(value);
+ if (!isUnique) {
+ return "Handler is already taken";
+ }
+ return true;
+ } catch (error) {
+ console.error("Error validating handler:", error);
+ return "An error occurred";
+ }
+ };
+
+ return (
+
+
+
+
+
+
+
+
+
+
+ );
+}
+
+export default CreateGroupFormScreen;
+
+CreateGroupFormScreen.propTypes = {
+ navigation: PropTypes.shape({
+ navigate: PropTypes.func.isRequired,
+ }).isRequired,
+};
diff --git a/packages/app/screens/feed/create_group_form/CreateGroupFormStyles.js b/packages/app/screens/feed/create_group_form/CreateGroupFormStyles.js
new file mode 100644
index 00000000..4e9370db
--- /dev/null
+++ b/packages/app/screens/feed/create_group_form/CreateGroupFormStyles.js
@@ -0,0 +1,86 @@
+import { StyleSheet } from "react-native";
+
+export const colors = {
+ red: "red",
+ white: "white",
+ black: "black",
+};
+
+export const styles = StyleSheet.create({
+ bannerDisplay: {
+ borderColor: colors.black,
+ borderWidth: 1,
+ height: 100,
+ width: 200,
+ },
+ btnContainer: {
+ backgroundColor: colors.white,
+ borderColor: colors.black,
+ borderWidth: 2,
+ justifyContent: "center",
+ margin: 20,
+ padding: 4,
+ textAlign: "center",
+ },
+ container: {
+ flex: 1,
+ },
+ header: {
+ fontSize: 20,
+ padding: 10,
+ },
+ iconDisplay: {
+ borderColor: colors.black,
+ borderRadius: 100,
+ borderWidth: 1,
+ height: 100,
+ width: 100,
+ },
+ imageSelectorBtnContainer: {
+ backgroundColor: colors.white,
+ borderColor: colors.black,
+ borderWidth: 1,
+ height: 50,
+ justifyContent: "center",
+ marginTop: 6,
+ textAlign: "center",
+ },
+ imageSelectorContainer: {
+ flexDirection: "row",
+ justifyContent: "space-between",
+ },
+ important: {
+ color: colors.red,
+ fontSize: 15,
+ },
+ initialgroup: {
+ alignItems: "center",
+ flex: 1,
+ justifyContent: "center",
+ },
+ inner: {
+ flex: 1,
+ justifyContent: "space-around",
+ padding: 36,
+ },
+ input: {
+ borderWidth: 1,
+ height: 40,
+ margin: 6,
+ padding: 10,
+ },
+ inputDescription: {
+ borderWidth: 1,
+ height: 100,
+ margin: 6,
+ padding: 10,
+ },
+ keyboard: {
+ flex: 1,
+ flexDirection: "column",
+ justifyContent: "center",
+ },
+ scrollview_extra_margin: {
+ margin: 100,
+ },
+});
diff --git a/packages/app/screens/feed/create_group_form/CreateGroupStack.js b/packages/app/screens/feed/create_group_form/CreateGroupStack.js
new file mode 100644
index 00000000..90678ef1
--- /dev/null
+++ b/packages/app/screens/feed/create_group_form/CreateGroupStack.js
@@ -0,0 +1,28 @@
+import React from "react";
+import { createNativeStackNavigator } from "@react-navigation/native-stack";
+import { GroupProvider } from "@app/utils/GroupContext";
+import InitialCreateGroupScreen from "./InitialCreateGroupScreen";
+import CreateGroupFormScreen from "./CreateGroupFormScreen";
+
+const Stack = createNativeStackNavigator();
+
+function CreateGroupStack() {
+ return (
+
+
+
+
+
+
+ );
+}
+
+export default CreateGroupStack;
diff --git a/packages/app/screens/feed/create_group_form/InitialCreateGroupScreen.js b/packages/app/screens/feed/create_group_form/InitialCreateGroupScreen.js
new file mode 100644
index 00000000..21942aba
--- /dev/null
+++ b/packages/app/screens/feed/create_group_form/InitialCreateGroupScreen.js
@@ -0,0 +1,26 @@
+import React from "react";
+import { View, Text } from "react-native";
+import Button from "@app/components/Button";
+import PropTypes from "prop-types";
+import { styles } from "./CreateGroupFormStyles";
+
+function InitialCreateGroupScreen({ navigation }) {
+ return (
+
+ navigation.navigate("Feed")} title="Back" />
+ navigation.navigate("Create Group Form")}
+ title="CREATE GROUP"
+ />
+ Create Group Scren
+
+ );
+}
+
+InitialCreateGroupScreen.propTypes = {
+ navigation: PropTypes.shape({
+ navigate: PropTypes.func.isRequired,
+ }).isRequired,
+};
+
+export default InitialCreateGroupScreen;
diff --git a/packages/app/utils/EventContext.js b/packages/app/utils/EventContext.js
index 2ec9acde..cedd7e6a 100644
--- a/packages/app/utils/EventContext.js
+++ b/packages/app/utils/EventContext.js
@@ -27,7 +27,7 @@ export function EventProvider({ eventID = EVENTID, children }) {
headers: {
Authorization: token,
},
- }
+ },
);
setEvent(eventResponse.data.event);
@@ -51,7 +51,7 @@ export function useEventContext() {
const eventInfo = useContext(EventContext);
if (!eventInfo) {
throw new Error(
- "You are using guild context outside of EventProvider. Context undefined"
+ "You are using guild context outside of EventProvider. Context undefined",
);
}
return eventInfo;
diff --git a/packages/app/utils/FeedContext.js b/packages/app/utils/FeedContext.js
index 9837860b..aa81147e 100644
--- a/packages/app/utils/FeedContext.js
+++ b/packages/app/utils/FeedContext.js
@@ -27,7 +27,7 @@ export function FeedProvider({ children }) {
headers: {
Authorization: token,
},
- }
+ },
);
const serializeEvents = eventsResponse.data.events.map((feedEvent) => {
@@ -72,7 +72,7 @@ export function useFeedContext() {
const feedInfo = useContext(FeedContext);
if (!feedInfo) {
throw new Error(
- "You are using guild context outside of FeedProvider. Context undefined"
+ "You are using guild context outside of FeedProvider. Context undefined",
);
}
return feedInfo;
diff --git a/packages/app/utils/GroupContext.js b/packages/app/utils/GroupContext.js
new file mode 100644
index 00000000..400ed17c
--- /dev/null
+++ b/packages/app/utils/GroupContext.js
@@ -0,0 +1,213 @@
+import PropTypes from "prop-types";
+import React, { createContext, useState } from "react";
+import { Alert } from "react-native";
+import axios from "axios";
+import { ENDPOINT } from "./constants";
+import * as SecureStore from "@app/utils/SecureStore";
+
+export const GroupContext = createContext();
+
+export function GroupProvider({ children }) {
+ const [name, setName] = useState("");
+ const [handler, setHandler] = useState("");
+ const [description, setDescription] = useState("");
+ const [banner, setBanner] = useState("");
+ const [icon, setIcon] = useState("");
+
+ const [category, setCategory] = useState("");
+ const [tags, setTags] = useState([]);
+ const [website, setWebsite] = useState("");
+ const [location, setLocation] = useState("");
+ const [isInviteOnly, setIsInviteOnly] = useState(false);
+
+ const [twitterLink, setTwitterLink] = useState("");
+ const [facebookLink, setFacebookLink] = useState("");
+ const [instagramLink, setInstagramLink] = useState("");
+ const [discordLink, setDiscordLink] = useState("");
+ const [linkedInLink, setLinkedInLink] = useState("");
+ const [githubLink, setGithubLink] = useState("");
+
+ const resetForm = () => {
+ setName("");
+ setHandler("");
+ setDescription("");
+ setBanner("");
+ setIcon("");
+
+ setCategory("");
+ setTags([]);
+ setWebsite("");
+ setLocation("");
+ setIsInviteOnly(false);
+
+ setTwitterLink("");
+ setFacebookLink("");
+ setInstagramLink("");
+ setDiscordLink("");
+ setLinkedInLink("");
+ setGithubLink("");
+ };
+
+ const isHandlerUnique = async (handler) => {
+ try {
+ const token = await SecureStore.getValueFor("accessToken");
+ const headers = { Authorization: token };
+ const response = await axios.get(
+ `${ENDPOINT}/guilds/handler/${handler}`,
+ { headers }
+ );
+
+ const responseStatus = response.data.status;
+ console.log(`Reponse Status: ${responseStatus}`);
+
+ return false;
+ } catch (error) {
+ if (error.response.status === 404) {
+ console.log(`Reponse Status: ${error.response.status}`);
+ // if handler is unique, return true
+ return true;
+ } else if (error.response) {
+ console.error("Response Error Data:", error.response.data);
+ } else if (error.request) {
+ console.error("Request Error:", error.request);
+ } else {
+ console.error("Error:", error.message);
+ }
+ return false;
+ }
+ };
+
+ // form submission logic
+ const submitForm = async () => {
+ try {
+ const media = [
+ twitterLink,
+ facebookLink,
+ instagramLink,
+ discordLink,
+ linkedInLink,
+ githubLink,
+ ].filter((item) => item);
+
+ const defaultBanner = `https://icebreak-assets.s3.us-west-1.amazonaws.com/guild_banner.5325b147-5524-4539-b652-0549e074a159.jpg`;
+ const defaultAvatar = `https://icebreak-assets.s3.us-west-1.amazonaws.com/guild_avatar.5325b147-5524-4539-b652-0549e074a159.jpg`;
+
+ const guildData = {
+ name,
+ handler,
+ description,
+ banner: defaultBanner,
+ avatar: defaultAvatar,
+ category,
+ location: location || undefined,
+ website: website || undefined,
+ tags,
+ media,
+ isInviteOnly,
+ };
+
+ // submits the basic data
+ const token = await SecureStore.getValueFor("accessToken");
+ const headers = { Authorization: token };
+
+ const response = await axios.post(`${ENDPOINT}/guilds/`, guildData, {
+ headers,
+ });
+
+ // image submission
+ const id = response.data.data.createdGuild.guildId;
+ const iconType = "guild_avatar";
+ const bannerType = "guild_banner";
+
+ if (icon && icon.trim() !== "") {
+ axios
+ .put(
+ `${ENDPOINT}/media/images/${iconType}/${id}`,
+ { imageData: icon },
+ { headers }
+ )
+ .then((response) => {
+ return response;
+ });
+ }
+
+ if (banner && banner.trim() !== "") {
+ axios
+ .put(
+ `${ENDPOINT}/media/images/${bannerType}/${id}`,
+ { imageData: banner },
+ { headers }
+ )
+ .then((response) => {
+ return response;
+ });
+ }
+
+ Alert.alert("Success", "Group created successfully!");
+ resetForm();
+
+ return true;
+ } catch (error) {
+ Alert.alert("Error", "Failed to create group.");
+
+ if (error.response) {
+ console.error("Response Error Data:", error.response.data);
+ } else if (error.request) {
+ console.error("Request Error:", error.request);
+ } else {
+ console.error("Error:", error.message);
+ }
+ return false;
+ }
+ };
+
+ return (
+
+ {children}
+
+ );
+}
+
+GroupProvider.propTypes = {
+ children: PropTypes.any,
+};
diff --git a/packages/app/utils/constants.js b/packages/app/utils/constants.js
index 80994af9..88fff3de 100644
--- a/packages/app/utils/constants.js
+++ b/packages/app/utils/constants.js
@@ -1,4 +1,4 @@
const URL =
- "https://bdb9-2600-1700-7860-56a0-798e-f3fa-ddb3-f01d.ngrok-free.app";
+ "https://0612-2600-1700-7860-56a0-1d8c-bae1-5edf-3caf.ngrok-free.app";
export const ENDPOINT = `${URL}/api`;
diff --git a/packages/server/controllers/guilds.js b/packages/server/controllers/guilds.js
index 32b9b4f9..fc696d2d 100644
--- a/packages/server/controllers/guilds.js
+++ b/packages/server/controllers/guilds.js
@@ -13,6 +13,12 @@ async function getGuild(guildId) {
});
}
+async function getGuildByHandler(handler) {
+ return prisma.$queryRaw`
+ SELECT guild_id, name, handler, avatar FROM guilds
+ WHERE handler = ${handler}`;
+}
+
async function createGuild(guildData) {
return await prisma.guilds.create({
data: guildData,
@@ -128,6 +134,7 @@ async function isGuildMember(guildId, userId) {
module.exports = {
getGuild,
+ getGuildByHandler,
searchGuildByName,
searchGuildByHandler,
getAllGuilds,
diff --git a/packages/server/routes/api/auth.js b/packages/server/routes/api/auth.js
index 5334399d..884b4eba 100644
--- a/packages/server/routes/api/auth.js
+++ b/packages/server/routes/api/auth.js
@@ -183,7 +183,7 @@ router.post("/local", async (request, response) => {
const isValidPassword = await bcrypt.compare(
password,
- requestedUser.password
+ requestedUser.password,
);
if (!isValidPassword) {
@@ -332,7 +332,7 @@ router.post(
// Send the reset link to the user's email.
const sendEmail = await AuthController.sendPasswordResetEmail(
email,
- passwordResetToken
+ passwordResetToken,
);
if (sendEmail === null) {
return response.status(400).json({
@@ -356,7 +356,7 @@ router.post(
message: error.message,
});
}
- }
+ },
);
router.post(
@@ -414,7 +414,7 @@ router.post(
message: error.message,
});
}
- }
+ },
);
module.exports = router;
diff --git a/packages/server/routes/api/events.js b/packages/server/routes/api/events.js
index ff77063b..65ac0781 100644
--- a/packages/server/routes/api/events.js
+++ b/packages/server/routes/api/events.js
@@ -43,7 +43,7 @@ router.get(
const events = await EventController.getEvents(
eventLimit,
action,
- eventId
+ eventId,
);
if (events.length === 0) {
@@ -56,10 +56,10 @@ router.get(
const firstEventId = events[0].eventId;
const lastEventId = events[events.length - 1].eventId;
const prevCursor = Buffer.from(
- `${currentPage - 1}___prev___${firstEventId}`
+ `${currentPage - 1}___prev___${firstEventId}`,
).toString("base64");
const nextCursor = Buffer.from(
- `${currentPage + 1}___next___${lastEventId}`
+ `${currentPage + 1}___next___${lastEventId}`,
).toString("base64");
// follow-up request, not first request to api route
@@ -97,7 +97,7 @@ router.get(
message: error.message,
});
}
- }
+ },
);
router.post(
@@ -135,7 +135,7 @@ router.post(
message: error.message,
});
}
- }
+ },
);
router.get(
@@ -168,7 +168,7 @@ router.get(
message: error.message,
});
}
- }
+ },
);
router.delete(
@@ -203,7 +203,7 @@ router.delete(
message: error.message,
});
}
- }
+ },
);
router.put(
@@ -231,7 +231,7 @@ router.put(
const updatedEvent = await EventController.updateEvent(
eventId,
- validatedData
+ validatedData,
);
response.status(200).json({
@@ -249,7 +249,7 @@ router.put(
});
return;
}
- }
+ },
);
router.get(
@@ -266,9 +266,8 @@ router.get(
}
try {
const { eventId } = matchedData(request);
- const eventAttendeesData = await EventController.getEventAttendees(
- eventId
- );
+ const eventAttendeesData =
+ await EventController.getEventAttendees(eventId);
response.status(200).json({
status: "success",
data: {
@@ -281,7 +280,7 @@ router.get(
message: error.message,
});
}
- }
+ },
);
router.get(
@@ -309,7 +308,7 @@ router.get(
//Should return a list of events from the guild that are upcoming and in ascending order
const upcoming = await EventController.getUpcomingEvents(
currDate,
- guildId
+ guildId,
);
response.status(200).json({
@@ -324,7 +323,7 @@ router.get(
message: error.message,
});
}
- }
+ },
);
router.post(
@@ -348,7 +347,7 @@ router.post(
const eventAttendeeData = await EventController.updateAttendeeStatus(
eventId,
userId,
- "CheckedIn"
+ "CheckedIn",
);
response.status(200).json({
@@ -363,7 +362,7 @@ router.post(
message: error.message,
});
}
- }
+ },
);
router.put(
@@ -403,7 +402,7 @@ router.put(
message: error.message,
});
}
- }
+ },
);
module.exports = router;
diff --git a/packages/server/routes/api/guilds.js b/packages/server/routes/api/guilds.js
index 349037fe..375890b0 100644
--- a/packages/server/routes/api/guilds.js
+++ b/packages/server/routes/api/guilds.js
@@ -90,6 +90,38 @@ router.get(
}
);
+// Get guild by handler
+router.get(
+ "/handler/:guildHandler",
+ AuthController.authenticate,
+ async (request, response) => {
+ const guildHandler = request.params.guildHandler;
+
+ try {
+ const guild = await GuildController.getGuildByHandler(guildHandler);
+
+ if (guild.length === 0) {
+ return response.status(404).json({
+ status: "error",
+ message: `Guild not found via @${guildHandler}`,
+ });
+ }
+
+ return response.status(200).json({
+ status: "success",
+ data: {
+ guild,
+ },
+ });
+ } catch (error) {
+ return response.status(500).json({
+ status: "error",
+ message: error.message,
+ });
+ }
+ }
+);
+
// Create Guild
router.post(
"/",
diff --git a/packages/server/scripts/database.sql b/packages/server/scripts/database.sql
index 8621b750..7646443b 100644
--- a/packages/server/scripts/database.sql
+++ b/packages/server/scripts/database.sql
@@ -10,13 +10,18 @@ CREATE TABLE users (
);
CREATE TABLE Guild (
- guild_id VARCHAR(255),
- name VARCHAR(100),
- handler VARCHAR(50),
- description TEXT,
+ guild_id uuid PRIMARY KEY NOT NULL,
+ name VARCHAR(100) NOT NULL,
+ handler VARCHAR(50) NOT NULL,
+ description TEXT NOT NULL,
+ category VARCHAR(255) NOT NULL,
+ location VARCHAR(255),
+ website VARCHAR(255),
+ tags TEXT[] NOT NULL,
+ banner VARCHAR(255),
+ icon VARCHAR(255),
media TEXT[],
- invite_only BOOLEAN,
- PRIMARY KEY(guild_id)
+ invite_only BOOLEAN NOT NULL
);
CREATE TABLE Event (
diff --git a/packages/server/utils/redis.js b/packages/server/utils/redis.js
index 6e20c350..bbaa3fda 100644
--- a/packages/server/utils/redis.js
+++ b/packages/server/utils/redis.js
@@ -27,7 +27,7 @@ async function checkInvalidPasswordResetToken(token) {
const isMember = await redisClient.sismember(
"password_reset_token_blacklist",
- hashedToken
+ hashedToken,
);
if (isMember === 1) {
return true;
diff --git a/packages/server/utils/token.js b/packages/server/utils/token.js
index 562df0e2..58e8326d 100644
--- a/packages/server/utils/token.js
+++ b/packages/server/utils/token.js
@@ -10,7 +10,7 @@ function generateRefreshToken(user) {
process.env.TOKEN_SECRET,
{
expiresIn: "1d",
- }
+ },
);
}
@@ -27,7 +27,7 @@ function generateAccessToken(user) {
process.env.WEB_CLIENT_SECRET,
{
expiresIn: "1h",
- }
+ },
);
}
@@ -39,7 +39,7 @@ function generateResetPasswordToken(userId) {
process.env.TOKEN_SECRET,
{
expiresIn: "15m",
- }
+ },
);
}
@@ -59,7 +59,7 @@ function verifyAccessToken(accessToken) {
// Return the payload of the access token
return decoded;
}
- }
+ },
);
}
diff --git a/packages/server/validators/auth.js b/packages/server/validators/auth.js
index 6c27e533..3a96d0e3 100644
--- a/packages/server/validators/auth.js
+++ b/packages/server/validators/auth.js
@@ -47,21 +47,21 @@ const passwordResetValidator = [
.exists({ checkFalsy: true })
.withMessage("Password can't be null or empty.")
.matches(
- /^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[@$!%*?&])[A-Za-z\d@$!%*?&]{12,20}$/
+ /^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[@$!%*?&])[A-Za-z\d@$!%*?&]{12,20}$/,
)
.withMessage(
- "Password must be 12-20 characters long and contain at least one uppercase and lowercase letter, number, and special character!"
+ "Password must be 12-20 characters long and contain at least one uppercase and lowercase letter, number, and special character!",
),
body("passwordConfirmation", "Invalid confirmation password.").custom(
(passwordConfirmation, { req }) => {
if (passwordConfirmation !== req.body.password) {
throw new Error(
- "Confirmation password does not match original password!"
+ "Confirmation password does not match original password!",
);
}
return true;
- }
+ },
),
];
diff --git a/packages/server/validators/events.js b/packages/server/validators/events.js
index d4359a27..916a0dad 100644
--- a/packages/server/validators/events.js
+++ b/packages/server/validators/events.js
@@ -202,7 +202,7 @@ const attendeeStatusValidator = [
.withMessage("status cannot be null or empty")
.matches(/^(NotInterested|Interested|Attending)$/)
.withMessage(
- "Invalid status value. Allowed values are: NotInterested, Interested, Attending"
+ "Invalid status value. Allowed values are: NotInterested, Interested, Attending",
),
];
@@ -220,7 +220,7 @@ const checkInTimeValidator = [
const event = await EventController.getEvent(eventId);
const isMember = await GuildController.isGuildMember(
event.guildId,
- userId
+ userId,
);
if (!isMember) {
throw new Error("User is not a member of the guild.");
diff --git a/packages/server/validators/users.js b/packages/server/validators/users.js
index c2eeeb25..54b9d3e9 100644
--- a/packages/server/validators/users.js
+++ b/packages/server/validators/users.js
@@ -41,7 +41,7 @@ const userIdBodyValidator = [
const event = await EventController.getEvent(eventId);
const isMember = await GuildController.isGuildMember(
event.guildId,
- userId
+ userId,
);
if (!isMember) {
throw new Error("User is not a member of the guild.");
diff --git a/yarn.lock b/yarn.lock
index d65e1ee7..a2ebcbde 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -6480,6 +6480,18 @@ expo-font@~11.10.3:
dependencies:
fontfaceobserver "^2.1.0"
+expo-image-loader@~4.6.0:
+ version "4.6.0"
+ resolved "https://registry.yarnpkg.com/expo-image-loader/-/expo-image-loader-4.6.0.tgz#ca7d4fdf53125bff2091d3a2c34a3155f10df147"
+ integrity sha512-RHQTDak7/KyhWUxikn2yNzXL7i2cs16cMp6gEAgkHOjVhoCJQoOJ0Ljrt4cKQ3IowxgCuOrAgSUzGkqs7omj8Q==
+
+expo-image-picker@~14.7.1:
+ version "14.7.1"
+ resolved "https://registry.yarnpkg.com/expo-image-picker/-/expo-image-picker-14.7.1.tgz#c51faff3a3fbffc6ae93d7155370beb1a2d2baea"
+ integrity sha512-ILQVOJgI3aEzrDmCFGDPtpAepYkn8mot8G7vfQ51BfFdQbzL6N3Wm1fS/ofdWlAZJl/qT2DwaIh5xYmf3SyGZA==
+ dependencies:
+ expo-image-loader "~4.6.0"
+
expo-json-utils@~0.12.0:
version "0.12.3"
resolved "https://registry.yarnpkg.com/expo-json-utils/-/expo-json-utils-0.12.3.tgz#cabb704a344d6d75f225cf4032c64479e442a2a9"
@@ -10496,6 +10508,11 @@ react-freeze@^1.0.0:
resolved "https://registry.yarnpkg.com/react-freeze/-/react-freeze-1.0.4.tgz#cbbea2762b0368b05cbe407ddc9d518c57c6f3ad"
integrity sha512-r4F0Sec0BLxWicc7HEyo2x3/2icUTrRmDjaaRyzzn+7aDyFZliszMDOgLVwSnQnYENOlL1o569Ze2HZefk8clA==
+react-hook-form@^7.51.2:
+ version "7.51.2"
+ resolved "https://registry.yarnpkg.com/react-hook-form/-/react-hook-form-7.51.2.tgz#79f7f72ee217c5114ff831012d1a7ec344096e7f"
+ integrity sha512-y++lwaWjtzDt/XNnyGDQy6goHskFualmDlf+jzEZvjvz6KWDf7EboL7pUvRCzPTJd0EOPpdekYaQLEvvG6m6HA==
+
"react-is@^16.12.0 || ^17.0.0 || ^18.0.0", react-is@^18.0.0, react-is@^18.2.0:
version "18.2.0"
resolved "https://registry.yarnpkg.com/react-is/-/react-is-18.2.0.tgz#199431eeaaa2e09f86427efbb4f1473edb47609b"