Merge pull request #2 from WillBaumbach/feat/post-component

A lot of changes related to posts, profile posts, sign up, sign in, permissions, etc.
This commit is contained in:
Will Baumbach
2025-08-06 23:26:47 -05:00
committed by GitHub
16 changed files with 566 additions and 341 deletions

View File

@@ -36,13 +36,17 @@ export const getPost = async (req, res, next) => {
} }
export const getAllPostsByUser = async (req, res, next) => { export const getAllPostsByUser = async (req, res, next) => {
const post = await Post.find({ email: req.params.user }).exec() console.log(req.params)
if (!post) { 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) return next('No document found with that id', 404)
} }
res.status(200).json({ res.status(200).json({
status: 'success', status: 'success',
data: post data: posts
}) })
} }

View File

@@ -1,7 +1,7 @@
import mongoose from 'mongoose' import mongoose from 'mongoose'
const postSchema = new mongoose.Schema({ const postSchema = new mongoose.Schema({
userID: { clerkUserID: {
type: String, type: String,
required: true required: true
}, },

View File

@@ -1,44 +1,23 @@
import mongoose from 'mongoose'
import mongoose from 'mongoose'; import validator from 'validator'
import validator from 'validator';
const userSchema = new mongoose.Schema({ const userSchema = new mongoose.Schema({
name: { name: {
type: String, type: String,
required: [true, 'Please tell us your name!'] required: [true, 'Please tell us your name!']
}, },
email: { email: {
type: String, type: String,
required: [true, 'Please provide your email'], required: [true, 'Please provide your email'],
unique: true, unique: true,
lowercase: true, lowercase: true,
validate: [validator.isEmail, 'Please provide a valid email'] validate: [validator.isEmail, 'Please provide a valid email']
}, },
role: { clerkUserID: {
type: String, type: String
enum: ['user', 'admin'], }
default: 'user' })
}
});
const User = mongoose.model('User', userSchema); const User = mongoose.model('User', userSchema)
export default User; 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!'
// }
// }

View File

@@ -14,7 +14,7 @@ router.route('/').get(getAllPosts).post(createPost)
router.route('/:id').get(getPost).patch(updatePost).delete(deletePost) 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) router.route('/status/:status').get(getAllPostsByStatus)

View File

@@ -1,22 +1,63 @@
import { SignIn } from '@clerk/clerk-react' import { useSignIn } from '@clerk/clerk-expo'
import { dark } from '@clerk/themes' import { router } from 'expo-router'
import React from 'react' 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 ( return (
<View style={styles.container}> <View style={styles.container}>
<SignIn <Text style={styles.text}>Login</Text>
appearance={{ <View style={{ width: '80%', marginTop: 20 }}>
theme: dark, <Text style={styles.textlabel}>Username</Text>
variables: { <TextInput style={styles.input} value={username} placeholder='Enter email' onChangeText={setUsername} />
colorPrimary: '#747b83ff', <Text style={styles.textlabel}>Password</Text>
colorBackground: '#25292e', <TextInput
colorInput: '#383e46ff' style={styles.input}
} value={password}
}} placeholder='Enter password'
signUpUrl='/sign-up' secureTextEntry
/> onChangeText={setPassword}
/>
<TouchableOpacity
style={styles.button}
onPress={() => {
handleSignIn()
}}
>
<Text style={{ color: '#fff', fontWeight: 'bold' }}>Login</Text>
</TouchableOpacity>
<Text style={styles.textCenter}>Don&apos;t have an account?</Text>
<TouchableOpacity
style={styles.signup}
onPress={() => {
router.navigate('/sign-up')
}}
>
<Text style={{ color: '#fff' }}>Sign Up</Text>
</TouchableOpacity>{' '}
</View>
</View> </View>
) )
} }
@@ -25,7 +66,35 @@ const styles = StyleSheet.create({
container: { container: {
flex: 1, flex: 1,
backgroundColor: '#25292e', 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' alignItems: 'center'
} }
}) })

View File

@@ -1,22 +1,164 @@
import { SignUp } from '@clerk/clerk-expo/web' import { isClerkAPIResponseError, useSignUp } from '@clerk/clerk-expo'
import { dark } from '@clerk/themes' import { ClerkAPIError } from '@clerk/types'
import * as React from 'react' import { router } from 'expo-router'
import { StyleSheet, View } from 'react-native' import { useState } from 'react'
import { StyleSheet, Text, TextInput, TouchableOpacity, View } from 'react-native'
export default function SignUpScreen() { export default function SignUpScreen() {
const { signUp, setActive, isLoaded } = useSignUp()
const [pendingVerification, setPendingVerification] = useState(false)
const [code, setCode] = useState('')
const [errors, setErrors] = useState<ClerkAPIError[]>([])
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 (
<View style={styles.container}>
<Text style={styles.textCenter}>Enter the verification code we sent to {email}</Text>
<TextInput
style={styles.input}
value={code}
placeholder='Enter your verification code'
onChangeText={(code) => setCode(code)}
/>
<TouchableOpacity onPress={onVerifyPress} style={styles.button}>
<Text>Verify</Text>
</TouchableOpacity>
{errors.map((error) => (
<Text key={error.longMessage} style={{ color: 'red' }}>
{error.longMessage}
</Text>
))}
</View>
)
}
return ( return (
<View style={styles.container}> <View style={styles.container}>
<SignUp <>
appearance={{ <Text style={styles.text}>Create an account</Text>
theme: dark, <View style={{ width: '80%', marginTop: 20 }}>
variables: { <Text style={styles.textlabel}>First name</Text>
colorPrimary: '#747b83ff', <TextInput
colorBackground: '#25292e', style={styles.input}
colorInput: '#383e46ff' value={fname}
} placeholder='Enter first name'
}} onChangeText={setFname}
signInUrl='/sign-in' />
/> <Text style={styles.textlabel}>Last name</Text>
<TextInput
style={styles.input}
value={lname}
placeholder='Enter last name'
onChangeText={setLname}
/>
<Text style={styles.textlabel}>Email</Text>
<TextInput
style={styles.input}
value={email}
placeholder='Enter email'
onChangeText={setUsername}
/>
<Text style={styles.textlabel}>Password</Text>
<TextInput
style={styles.input}
value={password}
placeholder='Enter password'
secureTextEntry
onChangeText={setPassword}
/>
<TouchableOpacity
style={styles.button}
onPress={() => {
onSignUpPress()
}}
>
<Text style={{ color: '#fff', fontWeight: 'bold' }}>Sign Up</Text>
</TouchableOpacity>
<Text style={styles.textCenter}>Already have an account?</Text>
<TouchableOpacity
style={styles.signIn}
onPress={() => {
router.navigate('/sign-in')
}}
>
<Text style={{ color: '#fff' }}>Sign In</Text>
</TouchableOpacity>{' '}
</View>
{errors.map((error) => (
<Text key={error.longMessage} style={{ color: 'red' }}>
{error.longMessage}
</Text>
))}
</>
</View> </View>
) )
} }
@@ -25,7 +167,35 @@ const styles = StyleSheet.create({
container: { container: {
flex: 1, flex: 1,
backgroundColor: '#25292e', 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' alignItems: 'center'
} }
}) })

View File

@@ -1,8 +1,11 @@
import { useUser } from '@clerk/clerk-react'
import Ionicons from '@expo/vector-icons/Ionicons' import Ionicons from '@expo/vector-icons/Ionicons'
import { Tabs } from 'expo-router' import { Tabs } from 'expo-router'
import React from 'react' import React from 'react'
export default function TabLayout() { export default function TabLayout() {
const { user } = useUser()
return ( return (
<Tabs <Tabs
screenOptions={{ screenOptions={{
@@ -34,7 +37,8 @@ export default function TabLayout() {
headerShown: false, headerShown: false,
tabBarIcon: ({ color, focused }) => ( tabBarIcon: ({ color, focused }) => (
<Ionicons name={focused ? 'rocket-sharp' : 'rocket-outline'} size={24} color={color} /> <Ionicons name={focused ? 'rocket-sharp' : 'rocket-outline'} size={24} color={color} />
) ),
tabBarItemStyle: { display: user?.publicMetadata.role === 'admin' ? 'flex' : 'none' }
}} }}
/> />
<Tabs.Screen <Tabs.Screen

View File

@@ -1,12 +1,15 @@
import { useUser } from '@clerk/clerk-react'
import { CameraView, useCameraPermissions } from 'expo-camera' import { CameraView, useCameraPermissions } from 'expo-camera'
import React, { useRef, useState } from 'react' import React, { useRef, useState } from 'react'
import { Button, Image, StyleSheet, Text, TextInput, TouchableOpacity, View } from 'react-native' import { Button, Image, Modal, StyleSheet, Text, TextInput, TouchableOpacity, View } from 'react-native'
export default function App() { export default function App() {
const { user } = useUser()
const [permission, requestPermission] = useCameraPermissions() const [permission, requestPermission] = useCameraPermissions()
const [photo, setPhoto] = useState('') const [photo, setPhoto] = useState('')
const [address, setAddress] = useState('') const [address, setAddress] = useState('')
const [notes, setNotes] = useState('') const [notes, setNotes] = useState('')
const [showConfirmation, setShowConfirmation] = useState(false)
const cameraRef = useRef<CameraView>(null) const cameraRef = useRef<CameraView>(null)
async function resetState() { async function resetState() {
@@ -34,7 +37,7 @@ export default function App() {
'Content-Type': 'application/json' 'Content-Type': 'application/json'
}, },
body: JSON.stringify({ body: JSON.stringify({
userID: '6883ddb2640ebaa1a12e3791', clerkUserID: user?.id,
date: new Date(), date: new Date(),
photo, photo,
address, address,
@@ -43,6 +46,7 @@ export default function App() {
}).then((res) => { }).then((res) => {
console.log(res.status) console.log(res.status)
if (res.status === 200) { if (res.status === 200) {
setShowConfirmation(true)
resetState() resetState()
} }
}) })
@@ -109,6 +113,26 @@ export default function App() {
</View> </View>
</> </>
)} )}
<Modal
animationType='fade'
transparent={true}
visible={showConfirmation}
onRequestClose={() => {
setShowConfirmation(!showConfirmation)
}}
>
<View style={styles.modalView}>
<Text style={styles.modalText}>
Your post has been submitted and will be reviewed within 3 business days. Thanks!
</Text>
<TouchableOpacity
style={[styles.modalButton, styles.buttonClose]}
onPress={() => setShowConfirmation(!showConfirmation)}
>
<Text style={styles.textStyle}>Close</Text>
</TouchableOpacity>
</View>
</Modal>
</View> </View>
) )
} }
@@ -121,6 +145,48 @@ const styles = StyleSheet.create({
paddingHorizontal: 25, paddingHorizontal: 25,
paddingVertical: 200 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: { cameraContainer: {
flex: 1 flex: 1
}, },

View File

@@ -1,6 +1,7 @@
import { useFocusEffect } from 'expo-router' import { useFocusEffect } from 'expo-router'
import React, { useState } from 'react' 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' import { Post } from '../models/postModel'
export default function PostsScreen() { 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 ( return (
<View style={styles.wrapper}> <View style={styles.wrapper}>
<View style={styles.container}> <Text style={styles.title}>Posts</Text>
<Text style={styles.text}>Posts</Text> {posts?.length ? (
{posts?.length ? ( posts.map((el) => <PostComponent key={el._id} post={el} fetchData={fetchData} />)
posts.map((el) => ( ) : (
<View key={el._id} style={styles.posts}> <View style={styles.caughtUpContainer}>
<Text style={styles.text}>{el._id}</Text> <Text style={styles.caughtUpText}>All caught up!</Text>
<View style={{ alignItems: 'center', marginVertical: 10 }}> </View>
<Image )}
source={{ uri: el.photo }}
style={{ width: 200, height: 200, borderRadius: 8 }}
resizeMode='cover'
/>
</View>
<Text style={{ color: '#fff' }}>{el.notes}</Text>
<View style={{ flexDirection: 'row', justifyContent: 'space-between', marginTop: 10 }}>
<TouchableOpacity style={{ flex: 1, marginRight: 5 }} onPress={() => denyPost(el._id)}>
<Text
style={{
backgroundColor: '#bf3636ff',
color: '#fff',
textAlign: 'center',
padding: 8,
borderRadius: 4
}}
>
Deny
</Text>
</TouchableOpacity>
<TouchableOpacity
style={{ flex: 1, marginLeft: 5 }}
onPress={() => approvePost(el._id)}
>
<Text
style={{
backgroundColor: '#17be3bff',
color: '#fff',
textAlign: 'center',
padding: 8,
borderRadius: 4
}}
>
Approve
</Text>
</TouchableOpacity>
</View>
</View>
))
) : (
<View style={styles.caughtUpContainer}>
<Text style={styles.caughtUpText}>All caught up!</Text>
</View>
)}
</View>
</View> </View>
) )
} }
@@ -123,14 +45,12 @@ export default function PostsScreen() {
const styles = StyleSheet.create({ const styles = StyleSheet.create({
wrapper: { wrapper: {
flex: 1, flex: 1,
backgroundColor: '#25292e' backgroundColor: '#25292e',
}, padding: 16,
container: {
flex: 1,
alignItems: 'center', alignItems: 'center',
overflow: 'scroll' overflow: 'scroll'
}, },
text: { title: {
color: '#fff', color: '#fff',
justifyContent: 'center' justifyContent: 'center'
}, },

View File

@@ -1,10 +1,35 @@
import { useUser } from '@clerk/clerk-react' 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 { Image, StyleSheet, Text, View } from 'react-native'
import { PostComponent } from '../components/PostComponent'
import { SignOutButton } from '../components/SignOutButton' import { SignOutButton } from '../components/SignOutButton'
import { Post } from '../models/postModel'
export default function PostsScreen() { export default function PostsScreen() {
const { user } = useUser() const { user } = useUser()
const [posts, setPosts] = useState<Post[]>()
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 ( return (
<> <>
@@ -23,7 +48,15 @@ export default function PostsScreen() {
<SignOutButton></SignOutButton> <SignOutButton></SignOutButton>
</View> </View>
</View> </View>
<View style={styles.userPostsCard}></View> <View style={styles.userPostsCard}>
{posts?.length ? (
posts.map((el) => <PostComponent key={el._id} post={el} fetchData={fetchData} />)
) : (
<View style={styles.noPostsContainer}>
<Text style={styles.noPosts}>Your posts will show up here!</Text>
</View>
)}
</View>
</View> </View>
</> </>
) )
@@ -33,11 +66,11 @@ const styles = StyleSheet.create({
wrapper: { wrapper: {
flex: 1, flex: 1,
backgroundColor: '#25292e', backgroundColor: '#25292e',
padding: 16 padding: 16,
overflow: 'scroll'
}, },
profileInfoCard: { profileInfoCard: {
backgroundColor: '#373d44ff', backgroundColor: '#373d44ff',
flex: 0.25,
marginTop: 12, marginTop: 12,
borderColor: '#626e7aff', borderColor: '#626e7aff',
borderStyle: 'solid', borderStyle: 'solid',
@@ -45,33 +78,34 @@ const styles = StyleSheet.create({
borderRadius: 12 borderRadius: 12
}, },
profilePic: { profilePic: {
flex: 0.4,
marginTop: 12, marginTop: 12,
display: 'flex', display: 'flex',
alignItems: 'center' alignItems: 'center'
}, },
image: { image: {
height: 75, height: 75,
width: 75 width: 75,
borderRadius: 37.5
}, },
nameContainer: { flex: 0.3, display: 'flex', alignItems: 'center' }, nameContainer: { display: 'flex', alignItems: 'center' },
text: { text: {
color: 'white', color: 'white',
fontSize: 16, fontSize: 16,
marginTop: 6 marginTop: 6
}, },
buttonLayout: { buttonLayout: {
flex: 0.2,
alignItems: 'center', alignItems: 'center',
marginTop: 12 marginVertical: 12
}, },
userPostsCard: { userPostsCard: {
backgroundColor: '#373d44ff', flex: 1
flex: 0.75, },
marginTop: 12, noPostsContainer: {
borderColor: '#626e7aff', flex: 1,
borderStyle: 'solid', alignItems: 'center',
borderWidth: 1, justifyContent: 'center'
borderRadius: 12 },
noPosts: {
color: '#787b80ff'
} }
}) })

View File

@@ -1,3 +0,0 @@
export const Post = () => {
return null
}

View File

@@ -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<PostComponentProps> = ({ 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 (
<View key={post._id} style={styles.posts}>
<Text style={styles.text}>{post._id}</Text>
<View style={{ alignItems: 'center', marginVertical: 10 }}>
<Image
source={{ uri: post.photo }}
style={{ width: 200, height: 200, borderRadius: 8 }}
resizeMode='cover'
/>
</View>
<Text style={{ color: '#fff' }}>{post.notes}</Text>
{user?.publicMetadata.role !== 'admin' && (
<View style={{ flexDirection: 'row', justifyContent: 'space-between', marginTop: 10 }}>
{post.status === StatusEnum.Created && (
<Text style={[styles.created, styles.statusTag]}>Created</Text>
)}
{post.status === StatusEnum.Pending && (
<Text style={[styles.pending, styles.statusTag]}>Pending</Text>
)}
{post.status === StatusEnum.Denied && <Text style={[styles.denied, styles.statusTag]}>Denied</Text>}
{post.status === StatusEnum.Approved && (
<Text style={[styles.approved, styles.statusTag]}>Approved</Text>
)}
</View>
)}
{user?.publicMetadata.role === 'admin' && (
<View style={{ flexDirection: 'row', justifyContent: 'space-between', marginTop: 10 }}>
<TouchableOpacity style={{ flex: 1, marginRight: 5 }} onPress={() => denyPost(post._id)}>
<Text
style={{
backgroundColor: '#bf3636ff',
color: '#fff',
textAlign: 'center',
padding: 8,
borderRadius: 4
}}
>
Deny
</Text>
</TouchableOpacity>
<TouchableOpacity style={{ flex: 1, marginLeft: 5 }} onPress={() => approvePost(post._id)}>
<Text
style={{
backgroundColor: '#17be3bff',
color: '#fff',
textAlign: 'center',
padding: 8,
borderRadius: 4
}}
>
Approve
</Text>
</TouchableOpacity>
</View>
)}
</View>
)
}
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' }
})

View File

@@ -26,7 +26,7 @@ export const SignOutButton = () => {
const styles = StyleSheet.create({ const styles = StyleSheet.create({
button: { button: {
width: '40%', width: '40%',
height: '100%', height: 35,
backgroundColor: 'rgba(192, 196, 199, 1)', backgroundColor: 'rgba(192, 196, 199, 1)',
borderRadius: 5, borderRadius: 5,
alignItems: 'center', alignItems: 'center',

View File

@@ -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 (
<View style={styles.container}>
<Text style={styles.text}>Login</Text>
<View style={{ width: '80%', marginTop: 20 }}>
<Text style={styles.textlabel}>Username</Text>
<TextInput
style={styles.input}
value={username}
placeholder='Enter username'
onChangeText={setUsername}
/>
<Text style={styles.textlabel}>Password</Text>
<TextInput
style={styles.input}
value={password}
placeholder='Enter password'
secureTextEntry
onChangeText={setPassword}
/>
<TouchableOpacity
style={styles.button}
onPress={() => {
console.log('test')
}}
>
<Text style={{ color: '#fff', fontWeight: 'bold' }}>Login</Text>
</TouchableOpacity>
{/* <TouchableOpacity
style={styles.signup}
onPress={() => {
router.navigate('/signup')
}}
>
<Text style={{ color: '#fff' }}>Sign Up</Text>
</TouchableOpacity> */}
</View>
</View>
)
}
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'
}
})

View File

@@ -7,8 +7,8 @@ export interface Post {
} }
export enum StatusEnum { export enum StatusEnum {
Created = "created", Created = 'created',
Pending = "pending", Pending = 'pending',
Denied = "denied", Denied = 'denied',
Accepted = "accepted" Approved = 'approved'
} }

View File

@@ -1,65 +0,0 @@
import * as SecureStore from 'expo-secure-store'
import { useCallback, useEffect, useReducer } from 'react'
import { Platform } from 'react-native'
type UseStateHook<T> = [[boolean, T | null], (value: T | null) => void]
function useAsyncState<T>(initialValue: [boolean, T | null] = [true, null]): UseStateHook<T> {
return useReducer(
(state: [boolean, T | null], action: T | null = null): [boolean, T | null] => [false, action],
initialValue
) as UseStateHook<T>
}
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<string> {
// Public
const [state, setState] = useAsyncState<string>()
// 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]
}