adding picture display with cloudfront and working on ios take photo and post flow

This commit is contained in:
Will Baumbach
2025-07-30 16:24:09 -05:00
parent 913c4cf3f7
commit e49674a755
9 changed files with 13524 additions and 13450 deletions

View File

@@ -30,7 +30,7 @@ class AWSUtil {
console.log('Error', err) console.log('Error', err)
} }
return `https://tattletires.s3.us-east-2.amazonaws.com/${params.Key}` return `${params.Key}`
} }
async deleteFile(Location) { async deleteFile(Location) {

View File

@@ -4,99 +4,87 @@
* Module dependencies. * Module dependencies.
*/ */
import debugLib from 'debug'
import dotenv from 'dotenv'
import http from 'http'
import mongoose from 'mongoose'
import app from '../app.js'
const debug = debugLib('api:server')
dotenv.config({ path: '.env' })
import debugLib from 'debug'; mongoose.connect(process.env.DB_CONNECTION).then(() => console.log('DB connection successful!'))
import dotenv from 'dotenv';
import http from 'http';
import mongoose from 'mongoose';
import app from '../app.js';
const debug = debugLib('api:server');
dotenv.config({path: '.env'});
mongoose
.connect(process.env.DB_CONNECTION).then(() => console.log('DB connection successful!'));
/** /**
* Get port from environment and store in Express. * Get port from environment and store in Express.
*/ */
const port = normalizePort(process.env.PORT || '3000')
const port = normalizePort(process.env.PORT || '3000'); app.set('port', port)
app.set('port', port);
/** /**
* Create HTTP server. * Create HTTP server.
*/ */
const server = http.createServer(app)
const server = http.createServer(app);
/** /**
* Listen on provided port, on all network interfaces. * Listen on provided port, on all network interfaces.
*/ */
server.listen(port)
server.listen(port); server.on('error', onError)
server.on('error', onError); server.on('listening', onListening)
server.on('listening', onListening);
/** /**
* Normalize a port into a number, string, or false. * Normalize a port into a number, string, or false.
*/ */
function normalizePort(val) { function normalizePort(val) {
const port = parseInt(val, 10); const port = parseInt(val, 10)
if (isNaN(port)) { if (isNaN(port)) {
// named pipe // named pipe
return val; return val
} }
if (port >= 0) { if (port >= 0) {
// port number // port number
return port; return port
} }
return false; return false
} }
/** /**
* Event listener for HTTP server "error" event. * Event listener for HTTP server "error" event.
*/ */
function onError(error) { function onError(error) {
if (error.syscall !== 'listen') { if (error.syscall !== 'listen') {
throw error; throw error
} }
const bind = typeof port === 'string' const bind = typeof port === 'string' ? 'Pipe ' + port : 'Port ' + port
? 'Pipe ' + port
: 'Port ' + port;
// handle specific listen errors with friendly messages // handle specific listen errors with friendly messages
switch (error.code) { switch (error.code) {
case 'EACCES': case 'EACCES':
console.error(bind + ' requires elevated privileges'); console.error(bind + ' requires elevated privileges')
process.exit(1); process.exit(1)
case 'EADDRINUSE': case 'EADDRINUSE':
console.error(bind + ' is already in use'); console.error(bind + ' is already in use')
process.exit(1); process.exit(1)
default: default:
throw error; throw error
} }
} }
/** /**
* Event listener for HTTP server "listening" event. * Event listener for HTTP server "listening" event.
*/ */
function onListening() { function onListening() {
const addr = server.address(); const addr = server.address()
const bind = typeof addr === 'string' const bind = typeof addr === 'string' ? 'pipe ' + addr : 'port ' + addr.port
? 'pipe ' + addr debug('Listening on ' + bind)
: 'port ' + addr.port;
debug('Listening on ' + bind);
} }

View File

@@ -3,6 +3,21 @@ import Post from '../models/postModel.js'
export const getAllPosts = async (req, res, next) => { export const getAllPosts = async (req, res, next) => {
const allPosts = await Post.find({}).exec() const allPosts = await Post.find({}).exec()
allPosts.map((el) => {
el.photo = process.env.CLOUDFRONT_URL + el.photo
})
res.status(200).json({
status: 'success',
data: allPosts
})
}
export const getAllPostsByStatus = async (req, res, next) => {
console.log(req.params.status)
const allPosts = await Post.find({ status: req.params.status }).exec()
allPosts.map((el) => {
el.photo = process.env.CLOUDFRONT_URL + el.photo
})
res.status(200).json({ res.status(200).json({
status: 'success', status: 'success',
data: allPosts data: allPosts
@@ -35,13 +50,14 @@ export const createPost = async (req, res, next) => {
const aws = new AWSUtil() const aws = new AWSUtil()
// Grab base64 photo from the req body // Grab base64 photo from the req body
const location = await aws.uploadFile(req.body.photo) const fileName = await aws.uploadFile(req.body.photo)
const payload = { const payload = {
...req.body, ...req.body,
photo: location photo: fileName
} }
console.log(payload)
Post.create(payload).then((result) => { Post.create(payload).then((result) => {
res.status(200).json({ res.status(200).json({
status: 'success', status: 'success',

View File

@@ -1,5 +1,4 @@
import mongoose from 'mongoose'
import mongoose from 'mongoose';
const postSchema = new mongoose.Schema({ const postSchema = new mongoose.Schema({
userID: { userID: {
@@ -7,22 +6,25 @@ const postSchema = new mongoose.Schema({
required: true required: true
}, },
date: { date: {
type: Date, type: Date
}, },
photo: { photo: {
type: String, type: String,
required: true required: true
}, },
notes: { notes: {
type: String, type: String
},
address: {
type: String
}, },
status: { status: {
type: String, type: String,
enum: ['created', 'pending', 'denied', 'approved'], enum: ['created', 'denied', 'approved'],
default: 'created' default: 'created'
} }
}); })
const Post = mongoose.model('Post', postSchema); const Post = mongoose.model('Post', postSchema)
export default Post; export default Post

View File

@@ -1,20 +1,21 @@
import express from 'express'; import express from 'express'
import { createPost, deletePost, getAllPosts, getAllPostsByUser, getPost, updatePost } from './../controllers/postsController.js'; import {
const router = express.Router(); createPost,
deletePost,
getAllPosts,
getAllPostsByStatus,
getAllPostsByUser,
getPost,
updatePost
} from './../controllers/postsController.js'
const router = express.Router()
router router.route('/').get(getAllPosts).post(createPost)
.route('/')
.get(getAllPosts)
.post(createPost);
router router.route('/:id').get(getPost).patch(updatePost).delete(deletePost)
.route('/:id')
.get(getPost)
.patch(updatePost)
.delete(deletePost);
router router.route('/user/:user').get(getAllPostsByUser)
.route('/:user')
.get(getAllPostsByUser)
export default router; router.route('/status/:status').get(getAllPostsByStatus)
export default router

View File

@@ -1,15 +1,21 @@
import { CameraView, useCameraPermissions } from 'expo-camera' import { CameraView, useCameraPermissions } from 'expo-camera'
import { useRef, useState } from 'react' import React, { useRef, useState } from 'react'
import { Button, Image, StyleSheet, Text, TouchableOpacity, View } from 'react-native' import { Button, Image, StyleSheet, Text, TextInput, TouchableOpacity, View } from 'react-native'
export default function App() { export default function App() {
const [permission, requestPermission] = useCameraPermissions() const [permission, requestPermission] = useCameraPermissions()
const [photo, setPhoto] = useState('') const [photo, setPhoto] = useState('')
const [address, setAddress] = useState('')
const [notes, setNotes] = useState('')
const cameraRef = useRef<CameraView>(null) const cameraRef = useRef<CameraView>(null)
async function _takePhoto() { async function _takePhoto() {
if (cameraRef.current) { if (cameraRef.current) {
const pic = await cameraRef.current.takePictureAsync() let pic = await cameraRef.current.takePictureAsync({
quality: 0.1,
base64: true
})
console.log(pic)
setPhoto(pic.base64 || '') setPhoto(pic.base64 || '')
} }
} }
@@ -24,8 +30,9 @@ export default function App() {
body: JSON.stringify({ body: JSON.stringify({
userID: '6883ddb2640ebaa1a12e3791', userID: '6883ddb2640ebaa1a12e3791',
date: new Date(), date: new Date(),
photo: photo, photo,
notes: '3333 W Smoochie St' address,
notes
}) })
}) })
} }
@@ -50,6 +57,26 @@ export default function App() {
{photo ? ( {photo ? (
<View style={styles.cameraContainer}> <View style={styles.cameraContainer}>
<Image source={{ uri: photo }} style={styles.camera} /> <Image source={{ uri: photo }} style={styles.camera} />
<View>
<Text style={styles.label}>Address</Text>
<TextInput
style={styles.input}
placeholder='Add address here...'
value={address}
onChangeText={setAddress}
/>
</View>
<View>
<Text style={styles.label}>Notes</Text>
<TextInput
style={styles.multiline}
placeholder='Add notes here...'
multiline
numberOfLines={4}
value={notes}
onChangeText={setNotes}
/>
</View>
<View style={styles.buttonContainer}> <View style={styles.buttonContainer}>
<TouchableOpacity style={styles.button} onPress={() => setPhoto('')}> <TouchableOpacity style={styles.button} onPress={() => setPhoto('')}>
<Text style={styles.text}>Retake</Text> <Text style={styles.text}>Retake</Text>
@@ -109,5 +136,19 @@ const styles = StyleSheet.create({
fontSize: 24, fontSize: 24,
fontWeight: 'bold', fontWeight: 'bold',
color: 'white' color: 'white'
},
input: {
backgroundColor: 'white',
borderRadius: 5,
fontSize: 18,
textAlignVertical: 'top'
},
label: { color: 'white', fontSize: 18, marginBottom: 5, marginTop: 10 },
multiline: {
backgroundColor: 'white',
borderRadius: 5,
fontSize: 18,
minHeight: 80,
textAlignVertical: 'top'
} }
}) })

View File

@@ -4,19 +4,18 @@ import { Image, StyleSheet, Text, TouchableOpacity, View } from 'react-native'
import { Post } from '../models/postModel' import { Post } from '../models/postModel'
export default function PostsScreen() { export default function PostsScreen() {
const cloudfrontURL = process.env.CLOUDFRONT_URL
const [posts, setPosts] = useState<Post[]>() const [posts, setPosts] = useState<Post[]>()
useFocusEffect( useFocusEffect(
React.useCallback(() => { React.useCallback(() => {
// Do something when the screen is focused // Do something when the screen is focused
// TODO: add endpoint to get only non approved or denied status posts // TODO: add endpoint to get only non approved or denied status posts
fetch('http://localhost:3000/api/v1/posts') fetch('http://localhost:3000/api/v1/posts/status/created')
.then((res) => res.json()) .then((res) => res.json())
.then((json) => { .then((json) => {
console.log(json)
setPosts(json.data) setPosts(json.data)
}); })
return () => { return () => {
// Do something when the screen is unfocused // Do something when the screen is unfocused
// Useful for cleanup functions // Useful for cleanup functions
@@ -55,54 +54,79 @@ export default function PostsScreen() {
} }
return ( return (
<View style={styles.container}> <View style={styles.wrapper}>
<Text style={styles.text}>Posts</Text> <View style={styles.container}>
<View style={{ width: '90%', marginTop: 200 }}></View> <Text style={styles.text}>Posts</Text>
{ {posts &&
posts && posts.map(el => ( posts.map((el) => (
<View key={el._id} style={styles.posts}> <View key={el._id} style={styles.posts}>
<Text style={{ color: '#fff' }}>{el._id}</Text> <Text style={styles.text}>{el._id}</Text>
<View style={{ alignItems: 'center', marginVertical: 10 }}> <View style={{ alignItems: 'center', marginVertical: 10 }}>
<Image <Image
source={{ uri: el.photo }} source={{ uri: el.photo }}
style={{ width: 200, height: 200, borderRadius: 8 }} style={{ width: 200, height: 200, borderRadius: 8 }}
resizeMode="cover" 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>
<Text style={{ color: '#fff' }}>{el.notes}</Text> ))}
<View style={{ flexDirection: 'row', justifyContent: 'space-between', marginTop: 10 }}> </View>
<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> </View>
) )
} }
const styles = StyleSheet.create({ const styles = StyleSheet.create({
container: { wrapper: {
flex: 1, flex: 1,
backgroundColor: '#25292e', backgroundColor: '#25292e',
justifyContent: 'center', padding: 5
},
container: {
flex: 1,
alignItems: 'center', alignItems: 'center',
overflow: 'scroll' overflow: 'scroll'
}, },
text: { text: {
color: '#fff' color: '#fff',
justifyContent: 'center'
}, },
posts: { posts: {
marginBottom: 10, marginTop: 10,
backgroundColor: '#363c43ff', backgroundColor: '#363c43ff',
padding: 10 padding: 10,
width: '90%',
borderRadius: 5
} }
}) })

26549
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,53 +1,54 @@
{ {
"name": "tattletires", "name": "tattletires",
"main": "expo-router/entry", "main": "expo-router/entry",
"version": "1.0.0", "version": "1.0.0",
"scripts": { "scripts": {
"start": "expo start", "start": "expo start",
"reset-project": "node ./scripts/reset-project.js", "reset-project": "node ./scripts/reset-project.js",
"android": "expo start --android", "android": "expo start --android",
"ios": "expo start --ios", "ios": "expo start --ios",
"web": "expo start --web", "web": "expo start --web",
"lint": "expo lint" "lint": "expo lint"
}, },
"dependencies": { "dependencies": {
"@expo/vector-icons": "^14.1.0", "@expo/vector-icons": "^14.1.0",
"@react-navigation/bottom-tabs": "^7.3.10", "@react-navigation/bottom-tabs": "^7.3.10",
"@react-navigation/elements": "^2.3.8", "@react-navigation/elements": "^2.3.8",
"@react-navigation/native": "^7.1.6", "@react-navigation/native": "^7.1.6",
"expo": "~53.0.20", "expo": "~53.0.20",
"expo-blur": "~14.1.5", "expo-blur": "~14.1.5",
"expo-camera": "~16.1.11", "expo-camera": "~16.1.11",
"expo-constants": "~17.1.7", "expo-constants": "~17.1.7",
"expo-font": "~13.3.2", "expo-font": "~13.3.2",
"expo-haptics": "~14.1.4", "expo-haptics": "~14.1.4",
"expo-image": "~2.4.0", "expo-image": "~2.4.0",
"expo-linking": "~7.1.7", "expo-linking": "~7.1.7",
"expo-router": "~5.1.4", "expo-router": "~5.1.4",
"expo-splash-screen": "~0.30.10", "expo-splash-screen": "~0.30.10",
"expo-status-bar": "~2.2.3", "expo-status-bar": "~2.2.3",
"expo-symbols": "~0.4.5", "expo-symbols": "~0.4.5",
"expo-system-ui": "~5.0.10", "expo-system-ui": "~5.0.10",
"expo-web-browser": "~14.2.0", "expo-web-browser": "~14.2.0",
"express": "^5.1.0", "express": "^5.1.0",
"prettier": "^3.6.2", "prettier": "^3.6.2",
"react": "19.0.0", "react": "19.0.0",
"react-dom": "19.0.0", "react-dom": "19.0.0",
"react-native": "0.79.5", "react-native": "0.79.5",
"react-native-gesture-handler": "~2.24.0", "react-native-gesture-handler": "~2.24.0",
"react-native-reanimated": "~3.17.4", "react-native-reanimated": "~3.17.4",
"react-native-safe-area-context": "5.4.0", "react-native-safe-area-context": "5.4.0",
"react-native-screens": "~4.11.1", "react-native-screens": "~4.11.1",
"react-native-web": "~0.20.0", "react-native-web": "~0.20.0",
"react-native-webview": "13.13.5", "react-native-webview": "13.13.5",
"uuid": "^11.1.0" "uuid": "^11.1.0",
}, "expo-file-system": "~18.1.11"
"devDependencies": { },
"@babel/core": "^7.25.2", "devDependencies": {
"@types/react": "~19.0.10", "@babel/core": "^7.25.2",
"eslint": "^9.25.0", "@types/react": "~19.0.10",
"eslint-config-expo": "~9.2.0", "eslint": "^9.25.0",
"typescript": "~5.8.3" "eslint-config-expo": "~9.2.0",
}, "typescript": "~5.8.3"
"private": true },
"private": true
} }