diff --git a/api/controllers/postsController.js b/api/controllers/postsController.js index 09f6b48..87d41e9 100644 --- a/api/controllers/postsController.js +++ b/api/controllers/postsController.js @@ -36,13 +36,17 @@ export const getPost = async (req, res, next) => { } export const getAllPostsByUser = async (req, res, next) => { - const post = await Post.find({ email: req.params.user }).exec() - if (!post) { + console.log(req.params) + const posts = await Post.find({ clerkUserID: req.params.id }).exec() + posts.map((el) => { + el.photo = process.env.CLOUDFRONT_URL + el.photo + }) + if (!posts) { return next('No document found with that id', 404) } res.status(200).json({ status: 'success', - data: post + data: posts }) } diff --git a/api/models/postModel.js b/api/models/postModel.js index 6b77959..a73642d 100644 --- a/api/models/postModel.js +++ b/api/models/postModel.js @@ -1,7 +1,7 @@ import mongoose from 'mongoose' const postSchema = new mongoose.Schema({ - userID: { + clerkUserID: { type: String, required: true }, diff --git a/api/models/userModel.js b/api/models/userModel.js index 69136a0..f500aaf 100644 --- a/api/models/userModel.js +++ b/api/models/userModel.js @@ -1,44 +1,23 @@ - -import mongoose from 'mongoose'; -import validator from 'validator'; +import mongoose from 'mongoose' +import validator from 'validator' const userSchema = new mongoose.Schema({ - name: { - type: String, - required: [true, 'Please tell us your name!'] - }, - email: { - type: String, - required: [true, 'Please provide your email'], - unique: true, - lowercase: true, - validate: [validator.isEmail, 'Please provide a valid email'] - }, - role: { - type: String, - enum: ['user', 'admin'], - default: 'user' - } -}); + name: { + type: String, + required: [true, 'Please tell us your name!'] + }, + email: { + type: String, + required: [true, 'Please provide your email'], + unique: true, + lowercase: true, + validate: [validator.isEmail, 'Please provide a valid email'] + }, + clerkUserID: { + type: String + } +}) -const User = mongoose.model('User', userSchema); +const User = mongoose.model('User', userSchema) -export default User; - -// password: { -// type: String, -// required: [true, 'Please provide a password'], -// minlength: 8, -// select: false -// }, -// passwordConfirm: { -// type: String, -// required: [true, 'Please confirm your password'], -// validate: { -// // This only works on CREATE and SAVE!!! -// validator: function(el) { -// return el === this.password; -// }, -// message: 'Passwords are not the same!' -// } -// } \ No newline at end of file +export default User diff --git a/api/routes/posts.js b/api/routes/posts.js index 7ad308a..7be4a0d 100644 --- a/api/routes/posts.js +++ b/api/routes/posts.js @@ -14,7 +14,7 @@ router.route('/').get(getAllPosts).post(createPost) router.route('/:id').get(getPost).patch(updatePost).delete(deletePost) -router.route('/user/:user').get(getAllPostsByUser) +router.route('/user/:id').get(getAllPostsByUser) router.route('/status/:status').get(getAllPostsByStatus) diff --git a/app/(auth)/sign-in.tsx b/app/(auth)/sign-in.tsx index 640cccb..a0d82b1 100644 --- a/app/(auth)/sign-in.tsx +++ b/app/(auth)/sign-in.tsx @@ -1,22 +1,63 @@ -import { SignIn } from '@clerk/clerk-react' -import { dark } from '@clerk/themes' +import { useSignIn } from '@clerk/clerk-expo' +import { router } from 'expo-router' import React from 'react' -import { StyleSheet, View } from 'react-native' +import { StyleSheet, Text, TextInput, TouchableOpacity, View } from 'react-native' + +export default function LoginScreen() { + const { signIn, setActive, isLoaded } = useSignIn() + const [username, setUsername] = React.useState('') + const [password, setPassword] = React.useState('') + + async function handleSignIn() { + if (!isLoaded) return + + try { + const signInAttempt = await signIn.create({ + identifier: username, + password + }) + + if (signInAttempt.status === 'complete') { + await setActive({ session: signInAttempt.createdSessionId }) + router.replace('/') + } + } catch (e) { + console.log(JSON.stringify(e)) + } + } -export default function SignInScreen() { return ( - + Login + + Username + + Password + + { + handleSignIn() + }} + > + Login + + Don't have an account? + { + router.navigate('/sign-up') + }} + > + Sign Up + {' '} + ) } @@ -25,7 +66,35 @@ const styles = StyleSheet.create({ container: { flex: 1, backgroundColor: '#25292e', - padding: 5, + justifyContent: 'center', + alignItems: 'center' + }, + text: { + color: '#fff' + }, + textCenter: { + color: '#ffffff', + alignSelf: 'center', + marginBottom: 6 + }, + textlabel: { + color: '#fff', + marginBottom: 3 + }, + input: { + backgroundColor: '#fff', + borderRadius: 5, + padding: 10, + marginBottom: 15 + }, + button: { + backgroundColor: '#1e90ff', + padding: 12, + borderRadius: 5, + alignItems: 'center', + marginBottom: 15 + }, + signup: { alignItems: 'center' } }) diff --git a/app/(auth)/sign-up.tsx b/app/(auth)/sign-up.tsx index 202a8d8..80f9f46 100644 --- a/app/(auth)/sign-up.tsx +++ b/app/(auth)/sign-up.tsx @@ -1,22 +1,164 @@ -import { SignUp } from '@clerk/clerk-expo/web' -import { dark } from '@clerk/themes' -import * as React from 'react' -import { StyleSheet, View } from 'react-native' +import { isClerkAPIResponseError, useSignUp } from '@clerk/clerk-expo' +import { ClerkAPIError } from '@clerk/types' +import { router } from 'expo-router' +import { useState } from 'react' +import { StyleSheet, Text, TextInput, TouchableOpacity, View } from 'react-native' export default function SignUpScreen() { + const { signUp, setActive, isLoaded } = useSignUp() + const [pendingVerification, setPendingVerification] = useState(false) + const [code, setCode] = useState('') + const [errors, setErrors] = useState([]) + const [fname, setFname] = useState('') + const [lname, setLname] = useState('') + const [email, setUsername] = useState('') + const [password, setPassword] = useState('') + + async function onSignUpPress() { + if (!isLoaded) return + setErrors([]) + + try { + // Start Auth + await signUp.create({ + firstName: fname, + lastName: lname, + emailAddress: email, + password + }) + + // Set confirmation + await signUp.prepareEmailAddressVerification() + setPendingVerification(true) + } catch (e) { + if (isClerkAPIResponseError(e)) setErrors(e.errors) + console.log(JSON.stringify(e)) + } + } + + async function onVerifyPress() { + if (!isLoaded) return + setErrors([]) + + try { + // Use the code the user provided to attempt verification + const signUpAttempt = await signUp.attemptEmailAddressVerification({ + code + }) + + // If verification was completed, set the session to active + // and redirect the user + if (signUpAttempt.status === 'complete') { + await createUser(signUpAttempt.createdUserId) + await setActive({ session: signUpAttempt.createdSessionId }) + } else { + // If the status is not complete, check why. User may need to + // complete further steps. + console.error(JSON.stringify(signUpAttempt, null, 2)) + } + } catch (e: any) { + // See https://clerk.com/docs/custom-flows/error-handling + // for more info on error handling + console.error(JSON.stringify(e, null, 2)) + setErrors(e.errors) + } + } + + async function createUser(clerkUserID: string | null) { + fetch('http://localhost:3000/api/v1/users', { + method: 'POST', + headers: { + Accept: 'application/json, text/plain, */*', + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + name: `${fname} ${lname}`, + email, + clerkUserID + }) + }) + } + + if (pendingVerification) { + return ( + + Enter the verification code we sent to {email} + setCode(code)} + /> + + Verify + + {errors.map((error) => ( + + {error.longMessage} + + ))} + + ) + } + return ( - + <> + Create an account + + First name + + Last name + + Email + + Password + + { + onSignUpPress() + }} + > + Sign Up + + Already have an account? + { + router.navigate('/sign-in') + }} + > + Sign In + {' '} + + {errors.map((error) => ( + + {error.longMessage} + + ))} + ) } @@ -25,7 +167,35 @@ const styles = StyleSheet.create({ container: { flex: 1, backgroundColor: '#25292e', - padding: 5, + justifyContent: 'center', + alignItems: 'center' + }, + text: { + color: '#fff' + }, + textCenter: { + color: '#ffffff', + alignSelf: 'center', + marginBottom: 12 + }, + textlabel: { + color: '#fff', + marginBottom: 3 + }, + input: { + backgroundColor: '#fff', + borderRadius: 5, + padding: 10, + marginBottom: 15 + }, + button: { + backgroundColor: '#1e90ff', + padding: 12, + borderRadius: 5, + alignItems: 'center', + marginBottom: 15 + }, + signIn: { alignItems: 'center' } }) diff --git a/app/(tabs)/_layout.tsx b/app/(tabs)/_layout.tsx index a00d0d2..0aec797 100644 --- a/app/(tabs)/_layout.tsx +++ b/app/(tabs)/_layout.tsx @@ -1,8 +1,11 @@ +import { useUser } from '@clerk/clerk-react' import Ionicons from '@expo/vector-icons/Ionicons' import { Tabs } from 'expo-router' import React from 'react' export default function TabLayout() { + const { user } = useUser() + return ( ( - ) + ), + tabBarItemStyle: { display: user?.publicMetadata.role === 'admin' ? 'flex' : 'none' } }} /> (null) async function resetState() { @@ -34,7 +37,7 @@ export default function App() { 'Content-Type': 'application/json' }, body: JSON.stringify({ - userID: '6883ddb2640ebaa1a12e3791', + clerkUserID: user?.id, date: new Date(), photo, address, @@ -43,6 +46,7 @@ export default function App() { }).then((res) => { console.log(res.status) if (res.status === 200) { + setShowConfirmation(true) resetState() } }) @@ -109,6 +113,26 @@ export default function App() { )} + { + setShowConfirmation(!showConfirmation) + }} + > + + + Your post has been submitted and will be reviewed within 3 business days. Thanks! + + setShowConfirmation(!showConfirmation)} + > + Close + + + ) } @@ -121,6 +145,48 @@ const styles = StyleSheet.create({ paddingHorizontal: 25, paddingVertical: 200 }, + centeredView: { + flex: 1, + justifyContent: 'center', + alignItems: 'center' + }, + modalView: { + flex: 1, + top: '80%', + height: 100, + backgroundColor: '#363c43ff', + borderRadius: 12, + padding: 35, + shadowColor: '#000', + shadowOffset: { + width: 0, + height: 2 + }, + shadowOpacity: 0.25, + shadowRadius: 4, + elevation: 5 + }, + modalText: { + marginBottom: 15, + textAlign: 'center', + color: 'white' + }, + modalButton: { + borderRadius: 20, + padding: 10, + elevation: 2 + }, + buttonClose: { + backgroundColor: '#96a5b1ff', + paddingHorizontal: 20, + paddingVertical: 5, + borderRadius: 12 + }, + textStyle: { + color: 'white', + fontWeight: 'bold', + textAlign: 'center' + }, cameraContainer: { flex: 1 }, diff --git a/app/(tabs)/posts.tsx b/app/(tabs)/posts.tsx index 153a9ec..c326c04 100644 --- a/app/(tabs)/posts.tsx +++ b/app/(tabs)/posts.tsx @@ -1,6 +1,7 @@ import { useFocusEffect } from 'expo-router' import React, { useState } from 'react' -import { Image, StyleSheet, Text, TouchableOpacity, View } from 'react-native' +import { StyleSheet, Text, View } from 'react-native' +import { PostComponent } from '../components/PostComponent' import { Post } from '../models/postModel' export default function PostsScreen() { @@ -27,95 +28,16 @@ export default function PostsScreen() { }) } - async function approvePost(postID: string) { - // add code to update post to approved status - console.log('Approving post ' + postID) - await fetch(`http://localhost:3000/api/v1/posts/${postID}`, { - method: 'PATCH', - headers: { - Accept: 'application/json, text/plain, */*', - 'Content-Type': 'application/json' - }, - body: JSON.stringify({ - status: 'approved' - }) - }).then(() => { - fetchData() - }) - } - - async function denyPost(postID: string) { - // add code to update post to remove status - console.log('Denying post ' + postID) - await fetch(`http://localhost:3000/api/v1/posts/${postID}`, { - method: 'PATCH', - headers: { - Accept: 'application/json, text/plain, */*', - 'Content-Type': 'application/json' - }, - body: JSON.stringify({ - status: 'denied' - }) - }).then(() => { - fetchData() - }) - } - return ( - - Posts - {posts?.length ? ( - posts.map((el) => ( - - {el._id} - - - - {el.notes} - - denyPost(el._id)}> - - Deny - - - approvePost(el._id)} - > - - Approve - - - - - )) - ) : ( - - All caught up! - - )} - + Posts + {posts?.length ? ( + posts.map((el) => ) + ) : ( + + All caught up! + + )} ) } @@ -123,14 +45,12 @@ export default function PostsScreen() { const styles = StyleSheet.create({ wrapper: { flex: 1, - backgroundColor: '#25292e' - }, - container: { - flex: 1, + backgroundColor: '#25292e', + padding: 16, alignItems: 'center', overflow: 'scroll' }, - text: { + title: { color: '#fff', justifyContent: 'center' }, diff --git a/app/(tabs)/profile.tsx b/app/(tabs)/profile.tsx index 6089d36..0afde3c 100644 --- a/app/(tabs)/profile.tsx +++ b/app/(tabs)/profile.tsx @@ -1,10 +1,35 @@ import { useUser } from '@clerk/clerk-react' -import React from 'react' +import { useFocusEffect } from 'expo-router' +import React, { useState } from 'react' import { Image, StyleSheet, Text, View } from 'react-native' +import { PostComponent } from '../components/PostComponent' import { SignOutButton } from '../components/SignOutButton' +import { Post } from '../models/postModel' export default function PostsScreen() { const { user } = useUser() + const [posts, setPosts] = useState() + + useFocusEffect( + React.useCallback(() => { + // Do something when the screen is focused + // TODO: add endpoint to get only non approved or denied status posts + fetchData() + return () => { + // Do something when the screen is unfocused + // Useful for cleanup functions + } + }, []) + ) + + async function fetchData() { + fetch(`http://localhost:3000/api/v1/posts/user/${user?.id}`) + .then((res) => res.json()) + .then((json) => { + console.log(json) + setPosts(json.data) + }) + } return ( <> @@ -23,7 +48,15 @@ export default function PostsScreen() { - + + {posts?.length ? ( + posts.map((el) => ) + ) : ( + + Your posts will show up here! + + )} + ) @@ -33,11 +66,11 @@ const styles = StyleSheet.create({ wrapper: { flex: 1, backgroundColor: '#25292e', - padding: 16 + padding: 16, + overflow: 'scroll' }, profileInfoCard: { backgroundColor: '#373d44ff', - flex: 0.25, marginTop: 12, borderColor: '#626e7aff', borderStyle: 'solid', @@ -45,33 +78,34 @@ const styles = StyleSheet.create({ borderRadius: 12 }, profilePic: { - flex: 0.4, marginTop: 12, display: 'flex', alignItems: 'center' }, image: { height: 75, - width: 75 + width: 75, + borderRadius: 37.5 }, - nameContainer: { flex: 0.3, display: 'flex', alignItems: 'center' }, + nameContainer: { display: 'flex', alignItems: 'center' }, text: { color: 'white', fontSize: 16, marginTop: 6 }, buttonLayout: { - flex: 0.2, alignItems: 'center', - marginTop: 12 + marginVertical: 12 }, userPostsCard: { - backgroundColor: '#373d44ff', - flex: 0.75, - marginTop: 12, - borderColor: '#626e7aff', - borderStyle: 'solid', - borderWidth: 1, - borderRadius: 12 + flex: 1 + }, + noPostsContainer: { + flex: 1, + alignItems: 'center', + justifyContent: 'center' + }, + noPosts: { + color: '#787b80ff' } }) diff --git a/app/components/Post.tsx b/app/components/Post.tsx deleted file mode 100644 index 2594dae..0000000 --- a/app/components/Post.tsx +++ /dev/null @@ -1,3 +0,0 @@ -export const Post = () => { - return null -} diff --git a/app/components/PostComponent.tsx b/app/components/PostComponent.tsx new file mode 100644 index 0000000..dcfa786 --- /dev/null +++ b/app/components/PostComponent.tsx @@ -0,0 +1,125 @@ +import { useUser } from '@clerk/clerk-react' +import React from 'react' +import { Image, StyleSheet, Text, TouchableOpacity, View } from 'react-native' +import { Post, StatusEnum } from '../models/postModel' + +type PostComponentProps = { + post: Post + fetchData: () => void +} + +export const PostComponent: React.FC = ({ post, fetchData }) => { + const { user } = useUser() + + async function approvePost(postID: string) { + console.log('Approving post ' + postID) + await fetch(`http://localhost:3000/api/v1/posts/${postID}`, { + method: 'PATCH', + headers: { + Accept: 'application/json, text/plain, */*', + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + status: 'approved' + }) + }).then(() => { + fetchData() + }) + } + + async function denyPost(postID: string) { + console.log('Denying post ' + postID) + await fetch(`http://localhost:3000/api/v1/posts/${postID}`, { + method: 'PATCH', + headers: { + Accept: 'application/json, text/plain, */*', + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + status: 'denied' + }) + }).then(() => { + fetchData() + }) + } + + return ( + + {post._id} + + + + {post.notes} + {user?.publicMetadata.role !== 'admin' && ( + + {post.status === StatusEnum.Created && ( + Created + )} + {post.status === StatusEnum.Pending && ( + Pending + )} + {post.status === StatusEnum.Denied && Denied} + {post.status === StatusEnum.Approved && ( + Approved + )} + + )} + {user?.publicMetadata.role === 'admin' && ( + + denyPost(post._id)}> + + Deny + + + approvePost(post._id)}> + + Approve + + + + )} + + ) +} + +const styles = StyleSheet.create({ + text: { + color: '#fff', + justifyContent: 'center' + }, + posts: { + marginTop: 10, + backgroundColor: '#373d44ff', + borderColor: '#626e7aff', + borderStyle: 'solid', + borderWidth: 1, + borderRadius: 12, + padding: 10, + width: '100%' + }, + statusTag: { paddingVertical: 3, paddingHorizontal: 10, borderRadius: 6 }, + created: { backgroundColor: '#0d6efd', color: '#ffffff' }, + pending: { backgroundColor: '#ffc107', color: '#000000' }, + denied: { backgroundColor: '#dc3545', color: '#ffffff' }, + approved: { backgroundColor: '#198754', color: '#ffffff' } +}) diff --git a/app/components/SignOutButton.tsx b/app/components/SignOutButton.tsx index 79cdb79..f11aa3d 100644 --- a/app/components/SignOutButton.tsx +++ b/app/components/SignOutButton.tsx @@ -26,7 +26,7 @@ export const SignOutButton = () => { const styles = StyleSheet.create({ button: { width: '40%', - height: '100%', + height: 35, backgroundColor: 'rgba(192, 196, 199, 1)', borderRadius: 5, alignItems: 'center', diff --git a/app/login.tsx b/app/login.tsx deleted file mode 100644 index 92cfe67..0000000 --- a/app/login.tsx +++ /dev/null @@ -1,78 +0,0 @@ -import React from 'react' -import { StyleSheet, Text, TextInput, TouchableOpacity, View } from 'react-native' - -export default function LoginScreen() { - const [username, setUsername] = React.useState('') - const [password, setPassword] = React.useState('') - - return ( - - Login - - Username - - Password - - { - console.log('test') - }} - > - Login - - {/* { - router.navigate('/signup') - }} - > - Sign Up - */} - - - ) -} - -const styles = StyleSheet.create({ - container: { - flex: 1, - backgroundColor: '#25292e', - justifyContent: 'center', - alignItems: 'center' - }, - text: { - color: '#fff' - }, - textlabel: { - color: '#fff', - marginBottom: 3 - }, - input: { - backgroundColor: '#fff', - borderRadius: 5, - padding: 10, - marginBottom: 15 - }, - button: { - backgroundColor: '#1e90ff', - padding: 12, - borderRadius: 5, - alignItems: 'center', - marginBottom: 12 - }, - signup: { - alignItems: 'center' - } -}) diff --git a/app/models/postModel.ts b/app/models/postModel.ts index 8a2a3b7..e3052b7 100644 --- a/app/models/postModel.ts +++ b/app/models/postModel.ts @@ -7,8 +7,8 @@ export interface Post { } export enum StatusEnum { - Created = "created", - Pending = "pending", - Denied = "denied", - Accepted = "accepted" -} \ No newline at end of file + Created = 'created', + Pending = 'pending', + Denied = 'denied', + Approved = 'approved' +} diff --git a/app/useStorageState.tsx b/app/useStorageState.tsx deleted file mode 100644 index d99532d..0000000 --- a/app/useStorageState.tsx +++ /dev/null @@ -1,65 +0,0 @@ -import * as SecureStore from 'expo-secure-store' -import { useCallback, useEffect, useReducer } from 'react' -import { Platform } from 'react-native' - -type UseStateHook = [[boolean, T | null], (value: T | null) => void] - -function useAsyncState(initialValue: [boolean, T | null] = [true, null]): UseStateHook { - return useReducer( - (state: [boolean, T | null], action: T | null = null): [boolean, T | null] => [false, action], - initialValue - ) as UseStateHook -} - -export async function setStorageItemAsync(key: string, value: string | null) { - if (Platform.OS === 'web') { - try { - if (value === null) { - localStorage.removeItem(key) - } else { - localStorage.setItem(key, value) - } - } catch (e) { - console.error('Local storage is unavailable:', e) - } - } else { - if (value == null) { - await SecureStore.deleteItemAsync(key) - } else { - await SecureStore.setItemAsync(key, value) - } - } -} - -export function useStorageState(key: string): UseStateHook { - // Public - const [state, setState] = useAsyncState() - - // Get - useEffect(() => { - if (Platform.OS === 'web') { - try { - if (typeof localStorage !== 'undefined') { - setState(localStorage.getItem(key)) - } - } catch (e) { - console.error('Local storage is unavailable:', e) - } - } else { - SecureStore.getItemAsync(key).then((value) => { - setState(value) - }) - } - }, [key, setState]) - - // Set - const setValue = useCallback( - (value: string | null) => { - setState(value) - setStorageItemAsync(key, value) - }, - [key, setState] - ) - - return [state, setValue] -}