Skip to content

Commit

Permalink
Post creation details w s3 final (#42)
Browse files Browse the repository at this point in the history
  • Loading branch information
stoneliuCS authored Nov 23, 2024
1 parent 6f3eb07 commit bbf81b8
Show file tree
Hide file tree
Showing 23 changed files with 603 additions and 245 deletions.
1 change: 1 addition & 0 deletions backend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@
"express": "^4.19.2",
"express-session": "^1.18.0",
"express-validator": "^7.2.0",
"fuse.js": "^7.0.0",
"jest": "^29.7.0",
"mongodb": "^6.8.0",
"mongoose": "^8.5.3",
Expand Down
16 changes: 9 additions & 7 deletions backend/src/controllers/divelogs/divelog.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,9 @@ import {
} from '../../services/notificationService';
import { ExpoService, ExpoServiceImpl } from '../../services/expoService';
import sendFilesToS3 from '../../services/filesToS3';
import { NotFoundError } from '../../consts/errors';

const diveLogService: DiveLogService = new DiveLogServiceImpl();
const userService: UserService = new UserServiceImpl();
const notificationService: NotificationService = new NotificationServiceImpl();
const expoService: ExpoService = new ExpoServiceImpl();

Expand All @@ -25,7 +25,7 @@ export const DiveLogController = {
): Promise<void> => {
const errors = validationResult(req);
if (!errors.isEmpty()) {
res.status(400).json({ errors: errors.array() });
res.status(400).json({ error: errors.array() });
return;
}

Expand All @@ -38,6 +38,12 @@ export const DiveLogController = {
});
}

const user = await userService.getUserById(req.body.user);
if (user == null) {
res.status(404).json({ error: 'User not found' });
return;
}

try {
const diveLog: any = await diveLogService.createDiveLog(req.body);
const notifications = await notificationService.createPostNotification(
Expand All @@ -49,11 +55,7 @@ export const DiveLogController = {
}
res.status(201).json(diveLog);
} catch (error) {
if (error instanceof NotFoundError) {
res.status(404).json({ message: error.message });
return;
}
res.status(500).json({ message: 'Internal server error' });
res.status(500).json({ error: 'Internal server error' });
}
},

Expand Down
27 changes: 27 additions & 0 deletions backend/src/controllers/species/get.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import express from 'express';
import { Species } from '../../models/species';
import Fuse from 'fuse.js';

export const getById = async (req: express.Request, res: express.Response) => {
const id = req.params.id;
Expand All @@ -17,3 +18,29 @@ export const getByScientificName = async (
if (!species) return res.status(404).json({ message: 'Species not found' });
return res.status(200).json(species);
};

export const searchSpecies = async (
req: express.Request,
res: express.Response,
) => {
const allSpecies = await Species.find();
console.log(allSpecies.length);
//More options can be added later
//https://www.fusejs.io/api/options.html
const fuse = new Fuse(allSpecies, {
isCaseSensitive: false,
keys: ['commonNames'],
});

const searchQuery: string = req.params.searchRequest;
let results;
if (searchQuery == '*') {
results = allSpecies.slice(0, 50);
} else {
results = fuse
.search(searchQuery)
.slice(0, 50)
.map((element) => element.item);
}
return res.status(200).json(results);
};
21 changes: 17 additions & 4 deletions frontend/api/divelog.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,30 @@
import { FormFields } from '../app/(app)/(tabs)/post/_layout';
import { apiConfig } from './apiContext';
import { fetchData } from './base';
import { FormFields, PostDiveLogResponse } from '../types/divelog';
import { useAuthStore } from '../auth/authStore';
const API_BASE_URL = apiConfig;

export async function getDiveLogById(id: string): Promise<any> {
return await fetchData(`/divelog/${id}`, 'Failed to fetch divelog');
}

export async function createDiveLog(data: FormFields) {
const response = await fetch(`${apiConfig}/divelog`, {
export async function postDiveLog(postData: FormFields): Promise<any> {
const mongoDBId = useAuthStore.getState().mongoDBId;
if (mongoDBId) {
postData.user = mongoDBId;
}

let fishID = [];
for (let i: number = 0; i < postData.tagData?.length; i++) {
fishID.push(postData.tagData[i].id);
}
postData.speciesTags = fishID;
postData.date = new Date();
return await fetch(`${API_BASE_URL}/divelog`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(data),
body: JSON.stringify(postData),
});
}
17 changes: 17 additions & 0 deletions frontend/api/species.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { SpeciesContent } from '../types/species';
import { apiConfig } from './apiContext';
import { fetchData } from './base';

const API_BASE_URL = apiConfig;

export async function searchSpecies(searchQuery: string): Promise<any> {
if (searchQuery == '') {
searchQuery = '*';
}
return await fetch(`${API_BASE_URL}/species/search/${searchQuery}`, {
method: 'GET',
headers: {
'Content-Type': 'application/json',
},
});
}
Original file line number Diff line number Diff line change
@@ -1,21 +1,8 @@
import { Stack } from 'expo-router';
import React from 'react';
import { FormProvider, useForm } from 'react-hook-form';
import { Photo } from '../../../../types/divelog';

export type Location = {
type: string;
coordinates: number[];
};

export type FormFields = {
tags: string[];
photos: Photo[];
date: Date;
location: Location;
description: string;
user: string;
};
import { router, Stack } from 'expo-router';
import { useForm, FormProvider } from 'react-hook-form';
import Arrow from '../../../components/arrow';
import { FormFields } from '../../../types/divelog';

export default function Layout() {
const methods = useForm<FormFields>();
Expand All @@ -24,12 +11,14 @@ export default function Layout() {
<FormProvider {...methods}>
<Stack>
<Stack.Screen
name="index"
name="post"
options={{
headerShown: false,
headerTitle: '',
headerTransparent: true,
gestureEnabled: false,
headerShown: true,
headerLeft: () => (
<Arrow direction="left" onPress={() => router.push('/(tabs)')} />
),
}}
/>
<Stack.Screen
Expand Down
147 changes: 147 additions & 0 deletions frontend/app/(app)/(postcreation)/components/fish-search.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
import React from 'react';
import {
TextInput,
View,
Image,
Text,
TouchableOpacity,
ScrollView,
} from 'react-native';
import { useFormContext } from 'react-hook-form';
import { useState, useEffect } from 'react';
import Tag from '../../../../components/tag';
import { apiConfig } from '../../../../api/apiContext';
import { SpeciesContent } from '../../../../types/species';
import { TagData } from '../../../../types/divelog';
import { searchSpecies } from '../../../../api/species';

export default function FishSearch() {
const { setValue, watch } = useFormContext();
const [data, setData] = useState<SpeciesContent[]>([]);
const tagData: TagData[] = watch('tagData') || [];
const [search, setSearch] = useState('');

const removeFish = (index: number) => {
const newFish = [...tagData];
newFish.splice(index, 1);
setValue('tagData', newFish);
};

const tagIncludes = (name: string, id: string): boolean => {
for (let i: number = 0; i < tagData.length; i++) {
if (tagData[i].name == name && tagData[i].id == id) {
return true;
}
}
return false;
};

const updateBoolAtIndex = (index: number, value: boolean) => {
const newData = [...data];
if (value && !tagIncludes(data[index].commonNames[0], data[index]._id)) {
setValue('tagData', [
{ name: data[index].commonNames[0], id: data[index]._id },
...tagData,
]);
} else {
setValue(
'tagData',
tagData.filter(
(tag: TagData) => tag.name !== data[index].commonNames[0],
),
);
}
newData[index].visibility = !newData[index].visibility;
setData(newData);
};

useEffect(() => {
const checkTags = () => {
let hasChanged: boolean = false;
const newData: SpeciesContent[] = [...data];
for (let i: number = 0; i < data.length; i++) {
if (
!tagIncludes(data[i].commonNames[0], data[i]._id) &&
newData[i].visibility
) {
newData[i].visibility = false;
hasChanged = true;
}
if (
tagIncludes(data[i].commonNames[0], data[i]._id) &&
!newData[i].visibility
) {
newData[i].visibility = true;
hasChanged = true;
}
}
if (hasChanged) {
setData(newData);
}
};
checkTags();
}, [tagData, data]);

useEffect(() => {
const searchForFish = async (searchQuery: string) => {
try {
const response = await searchSpecies(searchQuery);
const responseBody = await response.json();
setData(responseBody);
} catch (error: any) {}
};
searchForFish(search);
}, [search]);

return (
<View className="flex-1">
<View className="flex flex-row border border-[#d2d9e2] rounded-md items-center px-2 w-full min-h-[5vh] mb-5">
<Image
className="h-[2.5vh] w-[2.5vh]"
source={require('../../../../assets/search.png')}
/>
<View className="flex h-full w-full flex-row items-center flex-wrap items-center gap-2 p-2">
{tagData.map((item, index) => {
return (
<View key={index}>
<Tag fish={item.name} onPress={() => removeFish(index)} />
</View>
);
})}
<TextInput
placeholder="Default"
className="pl-3 font-medium text-[14px] h-[4vh]"
value={search}
onChangeText={(newText: string) => setSearch(newText)}
/>
</View>
</View>
<ScrollView showsVerticalScrollIndicator={false}>
<View className="w-full flex flex-row flex-wrap flex-1">
{data.map((item, index) => {
return (
<View
key={index}
className="flex flex-col basis-1/4 items-center space-y-2"
>
<TouchableOpacity
className={`border h-[7.5vh] w-[7.5vh] rounded-lg ${
item.visibility
? 'border-blue-400 bg-blue-300'
: 'border-gray-400'
}`}
onPress={() => updateBoolAtIndex(index, !item.visibility)}
/>

<Text className="text-[10px] text-center mb-[2vh]">
{' '}
{item.commonNames[0]}{' '}
</Text>
</View>
);
})}
</View>
</ScrollView>
</View>
);
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { faChevronRight } from '@fortawesome/free-solid-svg-icons';
import { FontAwesomeIcon } from '@fortawesome/react-native-fontawesome';
import React from 'react';
import { Text, TouchableOpacity, View } from 'react-native';
import { TouchableOpacity, Text, View } from 'react-native';
import { FontAwesomeIcon } from '@fortawesome/react-native-fontawesome';
import { faChevronRight } from '@fortawesome/free-solid-svg-icons';

interface ButtonProps {
text?: string;
Expand Down
Loading

0 comments on commit bbf81b8

Please sign in to comment.