Compare commits
31 Commits
312809f34f
...
v0.2.0
Author | SHA1 | Date | |
---|---|---|---|
c89dfa6786 | |||
8f3e442077 | |||
0430710522 | |||
c1805643c8 | |||
f1c296c0d3 | |||
2041c8998a | |||
cff010f2b4 | |||
986aca3931 | |||
a57a059c2a | |||
b7dc6af2e4 | |||
4fc58aedd7 | |||
bf08eefe57 | |||
6d6afc6274 | |||
82c90c1ceb | |||
4fe4808b3c | |||
b6b9b989c7 | |||
13b223ed5f | |||
04833277e1 | |||
31179d7292 | |||
1c97262ee3 | |||
bf791db47f | |||
fa25015472 | |||
4d02572d0c | |||
d418a53144 | |||
9b850ba88f | |||
41f52e67d3 | |||
2d06e0fb12 | |||
114ab257b2 | |||
f382150da3 | |||
630da41536 | |||
674ee65c29 |
2
api/.gitignore
vendored
@@ -2,3 +2,5 @@ node_modules/
|
|||||||
dist/
|
dist/
|
||||||
package-lock.json
|
package-lock.json
|
||||||
.env
|
.env
|
||||||
|
test-report.html
|
||||||
|
uploads/
|
@@ -36,6 +36,7 @@ app.use(morgan("dev"));
|
|||||||
app.use(express.urlencoded({ extended: true }));
|
app.use(express.urlencoded({ extended: true }));
|
||||||
app.use(express.json());
|
app.use(express.json());
|
||||||
app.use(express.static(path.join(__dirname, "public")));
|
app.use(express.static(path.join(__dirname, "public")));
|
||||||
|
app.use('/public/uploads', express.static(path.join(__dirname, '../uploads')));
|
||||||
app.use(cookieParser());
|
app.use(cookieParser());
|
||||||
|
|
||||||
// Routes
|
// Routes
|
||||||
|
@@ -18,6 +18,7 @@ new Docs(
|
|||||||
export async function add_post(req: Request, res: Response) {
|
export async function add_post(req: Request, res: Response) {
|
||||||
try {
|
try {
|
||||||
const data: IReview = req.body;
|
const data: IReview = req.body;
|
||||||
|
data.user_id = res.locals.user._id
|
||||||
const review = new Review(data);
|
const review = new Review(data);
|
||||||
await review.save();
|
await review.save();
|
||||||
res.status(201).json(Log.info(201, "review was added", review));
|
res.status(201).json(Log.info(201, "review was added", review));
|
||||||
|
@@ -28,6 +28,14 @@ const schema = new Schema<IReview | any>(
|
|||||||
type: Boolean,
|
type: Boolean,
|
||||||
required: true,
|
required: true,
|
||||||
},
|
},
|
||||||
|
beer_id: {
|
||||||
|
type: String,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
user_id: {
|
||||||
|
type: String,
|
||||||
|
required: true,
|
||||||
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
timestamps: true,
|
timestamps: true,
|
||||||
|
@@ -1 +1 @@
|
|||||||
{"version":"2.0.0","endpoints":{"user":{"signup":{"name":"user","operation":"signup","route":"/api/v1/auth/signup","method":"POST","description":"user signup api","body":{"username":"testuser","email":"text@example.com","password":"Test1234"},"response":"status object"},"signin":{"name":"user","operation":"signin","route":"/api/v1/auth/signin","method":"POST","description":"user signin api","body":{"email":"text@example.com","password":"Test1234"},"response":"status object"},"logout":{"name":"user","operation":"logout","route":"/api/v1/auth/logout","method":"POST","description":"user logout api","body":{},"response":"status object"},"status":{"name":"user","operation":"status","route":"/api/v1/auth/status","method":"GET","description":"user login status api","response":"status code | user object"}},"beer":{"add":{"name":"beer","operation":"add","route":"/api/v1/beer/add","method":"POST","description":"beer add api","body":{"brand":"Pilsner Urqell","name":"Kozel","degree":11,"packaging":"can","photos":"optional field | max 4 images | formData"},"response":"status object | beer object"},"get":{"name":"beer","operation":"get","route":"/api/v1/beer/get","method":"GET","description":"beer get api","response":"status object | array of beer objects"},"del":{"name":"beer","operation":"del","route":"/api/v1/beer/del","method":"POST","description":"beer del api","body":{"_id":"6352b303b71cb62222f39895"},"response":"status object"},"edit":{"name":"beer","operation":"edit","route":"/api/v1/beer/edit","method":"POST","description":"beer edit api","body":{"_id":"6355b95dc03fad77bc380146","brand":"Pilsner Urqell","name":"Radegast","degree":12,"packaging":"bottle","imgs":[],"photos":"optional field | max 4 images | formData"},"response":"status object | beer data"}},"docs":{"get_all":{"name":"docs","operation":"get_all","route":"/api/v1","method":"GET","description":"Get docs json","response":"docs json"}}}}
|
{"version":"2.0.0","endpoints":{"user":{"signup":{"name":"user","operation":"signup","route":"/api/v1/auth/signup","method":"POST","description":"user signup api","body":{"username":"testuser","email":"text@example.com","password":"Test1234"},"response":"status object"},"signin":{"name":"user","operation":"signin","route":"/api/v1/auth/signin","method":"POST","description":"user signin api","body":{"email":"text@example.com","password":"Test1234"},"response":"status object"},"logout":{"name":"user","operation":"logout","route":"/api/v1/auth/logout","method":"POST","description":"user logout api","body":{},"response":"status object"},"status":{"name":"user","operation":"status","route":"/api/v1/auth/status","method":"GET","description":"user login status api","response":"status code | user object"}},"beer":{"add":{"name":"beer","operation":"add","route":"/api/v1/beer/add","method":"POST","description":"beer add api","body":{"brand":"Pilsner Urqell","name":"Kozel","degree":11,"packaging":"can","photos":"optional field | max 4 images | formData"},"response":"status object | beer object"},"get":{"name":"beer","operation":"get","route":"/api/v1/beer/get","method":"GET","description":"beer get api","response":"status object | array of beer objects"},"del":{"name":"beer","operation":"del","route":"/api/v1/beer/del","method":"POST","description":"beer del api","body":{"_id":"6352b303b71cb62222f39895"},"response":"status object"},"edit":{"name":"beer","operation":"edit","route":"/api/v1/beer/edit","method":"POST","description":"beer edit api","body":{"_id":"6355b95dc03fad77bc380146","brand":"Pilsner Urqell","name":"Radegast","degree":12,"packaging":"bottle","imgs":[],"photos":"optional field | max 4 images | formData"},"response":"status object | beer data"}},"docs":{"get_all":{"name":"docs","operation":"get_all","route":"/api/v1","method":"GET","description":"Get docs json","response":"docs json"}},"review":{"add":{"name":"review","operation":"add","route":"/api/v1/review/add","method":"POST","description":"review add api","body":{"beer_id":"6352b303b71cb62222f39895","foam":3,"bitter_sweetness":2,"taste":5,"packaging":3,"sourness":false,"would_again":true},"response":"status object | review object"},"get":{"name":"review","operation":"get","route":"/api/v1/review/get","method":"GET","description":"review get api","response":"status object | array of review objects"},"del":{"name":"review","operation":"del","route":"/api/v1/review/del","method":"POST","description":"review del api","body":{"_id":"6352b303b71cb62222f39895"},"response":"status object"}}}}
|
@@ -20,6 +20,7 @@ export const add = yup.object({
|
|||||||
packaging: yup.number().min(1).max(5).required(),
|
packaging: yup.number().min(1).max(5).required(),
|
||||||
sourness: yup.boolean().required(),
|
sourness: yup.boolean().required(),
|
||||||
would_again: yup.boolean().required(),
|
would_again: yup.boolean().required(),
|
||||||
|
user_id: yup.string().notRequired()
|
||||||
});
|
});
|
||||||
export interface IReview extends yup.InferType<typeof add>, mongooseAddition {}
|
export interface IReview extends yup.InferType<typeof add>, mongooseAddition {}
|
||||||
export const addExam: IReview = {
|
export const addExam: IReview = {
|
||||||
|
1
frontend/.gitignore
vendored
@@ -37,3 +37,4 @@ yarn-error.*
|
|||||||
.env
|
.env
|
||||||
|
|
||||||
.vscode/
|
.vscode/
|
||||||
|
*.swp
|
||||||
|
@@ -12,7 +12,9 @@
|
|||||||
"resizeMode": "contain",
|
"resizeMode": "contain",
|
||||||
"backgroundColor": "#ffffff"
|
"backgroundColor": "#ffffff"
|
||||||
},
|
},
|
||||||
"assetBundlePatterns": ["**/*"],
|
"assetBundlePatterns": [
|
||||||
|
"**/*"
|
||||||
|
],
|
||||||
"ios": {
|
"ios": {
|
||||||
"supportsTablet": true
|
"supportsTablet": true
|
||||||
},
|
},
|
||||||
@@ -25,6 +27,17 @@
|
|||||||
"web": {
|
"web": {
|
||||||
"favicon": "./assets/favicon.png"
|
"favicon": "./assets/favicon.png"
|
||||||
},
|
},
|
||||||
"plugins": ["expo-router"]
|
"plugins": [
|
||||||
|
"expo-router",
|
||||||
|
[
|
||||||
|
"expo-image-picker",
|
||||||
|
{
|
||||||
|
"photosPermission": "The app accesses your photos to let you share them with your friends.",
|
||||||
|
"cameraPermission": "The app accesses your camera to let you take photos of your beer and share them with your friends.",
|
||||||
|
"microphonePermission": "The app accesses your microphone to let you record audio and share it with your friends."
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"expo-secure-store"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -43,7 +43,7 @@ export default function TabLayout() {
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<Tabs.Screen
|
<Tabs.Screen
|
||||||
name="review"
|
name="review/index"
|
||||||
options={{
|
options={{
|
||||||
title: "Reviews",
|
title: "Reviews",
|
||||||
tabBarIcon: ({ color }) => (
|
tabBarIcon: ({ color }) => (
|
||||||
@@ -66,6 +66,10 @@ export default function TabLayout() {
|
|||||||
name="beer/add"
|
name="beer/add"
|
||||||
options={{ href: null, title: "Add beer" }}
|
options={{ href: null, title: "Add beer" }}
|
||||||
/>
|
/>
|
||||||
|
<Tabs.Screen
|
||||||
|
name="review/add/[beer_id]"
|
||||||
|
options={{ href: null, title: "Add review" }}
|
||||||
|
/>
|
||||||
</Tabs>
|
</Tabs>
|
||||||
</View>
|
</View>
|
||||||
);
|
);
|
||||||
|
@@ -1,34 +1,155 @@
|
|||||||
import { StyleSheet, TextInput, View } from "react-native";
|
import { StyleSheet, TextInput, View, Image } from "react-native";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import Button from "@components/Button";
|
import Button from "@components/Button";
|
||||||
|
import Text from "@components/Text";
|
||||||
import { colors } from "@components/style";
|
import { colors } from "@components/style";
|
||||||
|
import * as ImagePicker from "expo-image-picker";
|
||||||
|
import DropDownPicker from "react-native-dropdown-picker";
|
||||||
|
const DropdownTheme = require("@components/DropdownTheme");
|
||||||
|
import { Platform } from "react-native";
|
||||||
|
|
||||||
export default function BeerAdd() {
|
export default function BeerAdd() {
|
||||||
const [b_name, setBName] = useState("");
|
const [b_name, setBName] = useState("");
|
||||||
const [b_degree, setBDegree] = useState("");
|
const [b_degree, setBDegree] = useState("");
|
||||||
const [b_packaging, setBPackaging] = useState("");
|
const [b_packaging, setBPackaging] = useState(null);
|
||||||
const [b_brand, setBBrand] = useState("");
|
const [b_brand, setBBrand] = useState("");
|
||||||
|
const [image, setImage] = useState(null);
|
||||||
|
|
||||||
|
const [open, setOpen] = useState(false);
|
||||||
|
const [items, setItems] = useState([
|
||||||
|
{ label: "Tank beer", value: "tank" },
|
||||||
|
{ label: "Cask beer", value: "cask" },
|
||||||
|
{ label: "Glass bottle", value: "glass" },
|
||||||
|
{ label: "Can", value: "can" },
|
||||||
|
{ label: "PET bottle", value: "pet" },
|
||||||
|
]);
|
||||||
|
|
||||||
|
DropDownPicker.addTheme("DropdownTheme", DropdownTheme);
|
||||||
|
DropDownPicker.setTheme("DropdownTheme");
|
||||||
|
|
||||||
|
ImagePicker.getCameraPermissionsAsync(); //check if the user has granted permission to access the camera
|
||||||
|
const pickImage = async () => {
|
||||||
|
const permissionResult =
|
||||||
|
await ImagePicker.requestMediaLibraryPermissionsAsync();
|
||||||
|
|
||||||
|
if (permissionResult.granted === false) {
|
||||||
|
alert("You've refused to allow this appp to access your photos!");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// No permissions request is necessary for launching the image library
|
||||||
|
const result = await ImagePicker.launchImageLibraryAsync({
|
||||||
|
mediaTypes: ImagePicker.MediaTypeOptions.Images,
|
||||||
|
allowsEditing: true,
|
||||||
|
aspect: [3, 4],
|
||||||
|
// quality: 1,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Explore the result
|
||||||
|
console.log(result);
|
||||||
|
|
||||||
|
if (!result.canceled) {
|
||||||
|
setImage(result.assets[0]);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const openCamera = async () => {
|
||||||
|
// Ask the user for the permission to access the camera
|
||||||
|
const permissionResult = await ImagePicker.requestCameraPermissionsAsync();
|
||||||
|
|
||||||
|
if (permissionResult.granted === false) {
|
||||||
|
alert("You've refused to allow this app to access your camera!");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await ImagePicker.launchCameraAsync();
|
||||||
|
|
||||||
|
// Explore the result
|
||||||
|
console.log(result);
|
||||||
|
|
||||||
|
if (!result.canceled) {
|
||||||
|
setImage(result.assets[0]);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
function validateDegreeInput(text) {
|
||||||
|
let newText = "";
|
||||||
|
let numbers = "0123456789.";
|
||||||
|
|
||||||
|
for (var i = 0; i < text.length; i++) {
|
||||||
|
if (numbers.indexOf(text[i]) > -1) {
|
||||||
|
newText = newText + text[i];
|
||||||
|
setBDegree(newText);
|
||||||
|
} else {
|
||||||
|
// your call back function
|
||||||
|
alert("Please enter numbers only.");
|
||||||
|
setBDegree("");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
function dataURItoBlob(dataURI) {
|
||||||
|
// convert base64/URLEncoded data component to raw binary data held in a string
|
||||||
|
var byteString;
|
||||||
|
if (dataURI.split(",")[0].indexOf("base64") >= 0)
|
||||||
|
byteString = atob(dataURI.split(",")[1]);
|
||||||
|
else byteString = unescape(dataURI.split(",")[1]);
|
||||||
|
|
||||||
|
// separate out the mime component
|
||||||
|
var mimeString = dataURI.split(",")[0].split(":")[1].split(";")[0];
|
||||||
|
|
||||||
|
// write the bytes of the string to a typed array
|
||||||
|
var ia = new Uint8Array(byteString.length);
|
||||||
|
for (var i = 0; i < byteString.length; i++) {
|
||||||
|
ia[i] = byteString.charCodeAt(i);
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Blob([ia], { type: mimeString });
|
||||||
|
}
|
||||||
|
|
||||||
async function addBeer() {
|
async function addBeer() {
|
||||||
// TODO: after the request - redirect to /beer/{new_beer_id}?; plus some modal about successful state
|
// TODO: after the request - redirect to /beer/{new_beer_id}?; plus some modal about successful state
|
||||||
|
|
||||||
|
const data = new FormData();
|
||||||
|
if (Platform.OS == "web") {
|
||||||
|
// TODO: On phone its imposibble to upload an image
|
||||||
|
data.append("photos", dataURItoBlob(image.uri));
|
||||||
|
}
|
||||||
|
data.append("brand", b_brand);
|
||||||
|
data.append("name", b_name);
|
||||||
|
data.append("degree", b_degree);
|
||||||
|
data.append("packaging", "can");
|
||||||
|
|
||||||
|
try {
|
||||||
const req = await fetch(`${process.env.EXPO_PUBLIC_API_URL}/beer/add`, {
|
const req = await fetch(`${process.env.EXPO_PUBLIC_API_URL}/beer/add`, {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
credentials: "include",
|
credentials: "include",
|
||||||
headers: { "Content-Type": "application/json" },
|
body: data,
|
||||||
body: JSON.stringify({
|
|
||||||
brand: b_brand,
|
|
||||||
name: b_name,
|
|
||||||
degree: b_degree,
|
|
||||||
packaging: b_packaging,
|
|
||||||
photos: null,
|
|
||||||
}),
|
|
||||||
});
|
});
|
||||||
const res = await req.json();
|
const res = await req.json();
|
||||||
|
|
||||||
|
if (res.code == 201 && res.data._id) {
|
||||||
|
// TODO: reditect using expo router
|
||||||
|
// window.location.href = `/beer/${res.data._id}`;
|
||||||
|
alert("Added");
|
||||||
|
} else {
|
||||||
|
alert(
|
||||||
|
"Beer was not added successfully. Please check your data and try again.",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
alert(
|
||||||
|
"Beer was not added successfully. Please check your data and try again.",
|
||||||
|
);
|
||||||
|
console.error(err);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View style={styles.container}>
|
<View style={styles.container}>
|
||||||
<View style={styles.form}>
|
<View style={styles.form}>
|
||||||
|
<Text style={styles.text}>
|
||||||
|
Spill your thoughts about the beer you just sipped!
|
||||||
|
</Text>
|
||||||
<TextInput
|
<TextInput
|
||||||
style={styles.input}
|
style={styles.input}
|
||||||
placeholder="Name"
|
placeholder="Name"
|
||||||
@@ -47,17 +168,45 @@ export default function BeerAdd() {
|
|||||||
style={styles.input}
|
style={styles.input}
|
||||||
placeholder="Degree"
|
placeholder="Degree"
|
||||||
value={b_degree}
|
value={b_degree}
|
||||||
onChangeText={(text) => setBDegree(text)}
|
onChangeText={(text) => validateDegreeInput(text)}
|
||||||
placeholderTextColor="#aaaaaa"
|
placeholderTextColor="#aaaaaa"
|
||||||
|
keyboardType="numeric"
|
||||||
|
maxLength={3}
|
||||||
/>
|
/>
|
||||||
<TextInput
|
|
||||||
style={styles.input}
|
<DropDownPicker
|
||||||
placeholder="Packaging"
|
open={open}
|
||||||
value={b_packaging}
|
value={b_packaging}
|
||||||
onChangeText={(text) => setBPackaging(text)}
|
items={items}
|
||||||
placeholderTextColor="#aaaaaa"
|
setOpen={setOpen}
|
||||||
|
setValue={setBPackaging}
|
||||||
|
setItems={setItems}
|
||||||
|
placeholder={"What are you drinking from?"}
|
||||||
|
theme="DropdownTheme"
|
||||||
|
//searchable={true} //maybe we can use it later...
|
||||||
/>
|
/>
|
||||||
<Button title="Add beer" color={colors.green} onPress={addBeer} />
|
|
||||||
|
<View style={styles.imageContainer}>
|
||||||
|
<Button
|
||||||
|
title="Open gallery"
|
||||||
|
onPress={pickImage}
|
||||||
|
buttonStyle={styles.imageButton}
|
||||||
|
textStyle={styles.imageTextButton}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{Platform.OS != "web" ? (
|
||||||
|
<Button
|
||||||
|
title="Open camera"
|
||||||
|
onPress={openCamera}
|
||||||
|
buttonStyle={styles.imageButton}
|
||||||
|
textStyle={styles.imageTextButton}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
false
|
||||||
|
)}
|
||||||
|
</View>
|
||||||
|
{image && <Image source={{ uri: image.uri }} style={styles.image} />}
|
||||||
|
<Button title="Add beer" color={colors.gold} onPress={addBeer} />
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
);
|
);
|
||||||
@@ -65,22 +214,50 @@ export default function BeerAdd() {
|
|||||||
|
|
||||||
const styles = StyleSheet.create({
|
const styles = StyleSheet.create({
|
||||||
container: {
|
container: {
|
||||||
flex: 1,
|
width: "100%",
|
||||||
|
height: "100%",
|
||||||
|
alignItems: "center",
|
||||||
|
display: "flex",
|
||||||
},
|
},
|
||||||
form: {
|
form: {
|
||||||
flex: 1,
|
|
||||||
alignItems: "center",
|
alignItems: "center",
|
||||||
paddingTop: "10%",
|
|
||||||
gap: 15,
|
gap: 15,
|
||||||
|
width: "80%",
|
||||||
},
|
},
|
||||||
input: {},
|
|
||||||
input: {
|
input: {
|
||||||
height: "auto",
|
height: "auto",
|
||||||
width: "60%",
|
width: "100%",
|
||||||
borderColor: "gray",
|
borderColor: "gray",
|
||||||
borderWidth: 1,
|
borderWidth: 1,
|
||||||
borderRadius: 5,
|
borderRadius: 10,
|
||||||
padding: 10,
|
padding: 13,
|
||||||
color: "#fff",
|
color: "#fff",
|
||||||
},
|
},
|
||||||
|
imageContainer: {
|
||||||
|
alignItems: "center",
|
||||||
|
justifyContent: "center",
|
||||||
|
display: "flex",
|
||||||
|
flexDirection: "row",
|
||||||
|
gap: 10,
|
||||||
|
},
|
||||||
|
imageButton: {
|
||||||
|
backgroundColor: colors.dark,
|
||||||
|
borderColor: "gray",
|
||||||
|
borderWidth: 1,
|
||||||
|
borderRadius: 10,
|
||||||
|
},
|
||||||
|
imageTextButton: {
|
||||||
|
color: colors.white,
|
||||||
|
},
|
||||||
|
image: {
|
||||||
|
width: 150,
|
||||||
|
height: 150,
|
||||||
|
},
|
||||||
|
text: {
|
||||||
|
color: colors.white,
|
||||||
|
fontSize: 24,
|
||||||
|
textAlign: "center",
|
||||||
|
paddingBottom: "3%",
|
||||||
|
paddingTop: "10%",
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
@@ -1,11 +1,20 @@
|
|||||||
import { StyleSheet, View, FlatList } from "react-native";
|
import {
|
||||||
|
StyleSheet,
|
||||||
|
View,
|
||||||
|
FlatList,
|
||||||
|
Dimensions,
|
||||||
|
StatusBar,
|
||||||
|
Image,
|
||||||
|
} from "react-native";
|
||||||
import Text from "@components/Text";
|
import Text from "@components/Text";
|
||||||
import Button from "@components/Button";
|
import Button from "@components/Button";
|
||||||
import { colors } from "@components/style";
|
import { colors } from "@components/style";
|
||||||
import { router } from "expo-router";
|
import { router } from "expo-router";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
|
// import { FlashList } from "@shopify/flash-list";
|
||||||
|
|
||||||
export default function Tab() {
|
export default function Tab() {
|
||||||
|
const API_HOST = process.env.EXPO_PUBLIC_API_URL.replace("/api/v1", "");
|
||||||
const [data, setData] = useState([]);
|
const [data, setData] = useState([]);
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
fetchData();
|
fetchData();
|
||||||
@@ -28,16 +37,16 @@ export default function Tab() {
|
|||||||
return (
|
return (
|
||||||
<View style={styles.container}>
|
<View style={styles.container}>
|
||||||
<Button
|
<Button
|
||||||
title="Add Beer"
|
title="Add new beer"
|
||||||
color={colors.gold}
|
color={colors.gold}
|
||||||
onPress={() => {
|
onPress={() => {
|
||||||
router.replace("/beer/add");
|
router.replace("/beer/add");
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<FlatList
|
{/* <FlashList
|
||||||
data={data}
|
data={data}
|
||||||
style={styles.beerList}
|
estimatedItemSize={100}
|
||||||
keyExtractor={(item) => String(item._id)}
|
keyExtractor={(item) => String(item._id)}
|
||||||
renderItem={({ item }) => (
|
renderItem={({ item }) => (
|
||||||
<View style={styles.item}>
|
<View style={styles.item}>
|
||||||
@@ -47,6 +56,43 @@ export default function Tab() {
|
|||||||
<Text>Packaging: {item.packaging}</Text>
|
<Text>Packaging: {item.packaging}</Text>
|
||||||
</View>
|
</View>
|
||||||
)}
|
)}
|
||||||
|
/> */}
|
||||||
|
|
||||||
|
<FlatList
|
||||||
|
data={data}
|
||||||
|
style={styles.beerList}
|
||||||
|
keyExtractor={(item) => String(item._id)}
|
||||||
|
renderItem={({ item }) => (
|
||||||
|
<View style={styles.item}>
|
||||||
|
<Image
|
||||||
|
source={
|
||||||
|
item.imgs[0]
|
||||||
|
? {
|
||||||
|
uri: `${API_HOST}/public/uploads/${item.imgs[0]}`,
|
||||||
|
}
|
||||||
|
: {
|
||||||
|
uri: "https://imagesvc.meredithcorp.io/v3/mm/image?url=https:%2F%2Fstatic.onecms.io%2Fwp-content%2Fuploads%2Fsites%2F44%2F2020%2F09%2F29%2Flight-beer.jpg",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
style={styles.itemImg}
|
||||||
|
/>
|
||||||
|
<View style={styles.itemDesc}>
|
||||||
|
<Text>Name: {item.name}</Text>
|
||||||
|
<Text>Brand: {item.brand}</Text>
|
||||||
|
<Text>Degree: {item.degree}</Text>
|
||||||
|
<Text>Packaging: {item.packaging}</Text>
|
||||||
|
</View>
|
||||||
|
<View style={styles.itemAddReview}>
|
||||||
|
<Button
|
||||||
|
title="Add review"
|
||||||
|
color={colors.gold}
|
||||||
|
onPress={() => {
|
||||||
|
router.push(`/review/add/${item._id}`);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
/>
|
/>
|
||||||
</View>
|
</View>
|
||||||
);
|
);
|
||||||
@@ -57,12 +103,12 @@ export const styles = StyleSheet.create({
|
|||||||
flex: 1,
|
flex: 1,
|
||||||
justifyContent: "center",
|
justifyContent: "center",
|
||||||
alignItems: "center",
|
alignItems: "center",
|
||||||
marginTop: "5%",
|
marginTop: "2%",
|
||||||
},
|
},
|
||||||
beerList: {
|
beerList: {
|
||||||
width: "100%",
|
width: "100%",
|
||||||
paddingHorizontal: "15%",
|
paddingHorizontal: "15%",
|
||||||
marginTop: "5%",
|
marginTop: "2%",
|
||||||
},
|
},
|
||||||
item: {
|
item: {
|
||||||
borderColor: "gray",
|
borderColor: "gray",
|
||||||
@@ -71,4 +117,12 @@ export const styles = StyleSheet.create({
|
|||||||
padding: 13,
|
padding: 13,
|
||||||
marginBottom: "5%",
|
marginBottom: "5%",
|
||||||
},
|
},
|
||||||
|
itemImg: {
|
||||||
|
height: 300,
|
||||||
|
resizeMode: "contain",
|
||||||
|
},
|
||||||
|
itemDesc: {
|
||||||
|
alignItems: "center",
|
||||||
|
paddingBottom: "2%",
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
@@ -1,10 +0,0 @@
|
|||||||
import { View } from "react-native";
|
|
||||||
import Text from "@components/Text";
|
|
||||||
|
|
||||||
export default function Tab() {
|
|
||||||
return (
|
|
||||||
<View style={{ justifyContent: "center", alignItems: "center", flex: 1 }}>
|
|
||||||
<Text>Tab REVIEW</Text>
|
|
||||||
</View>
|
|
||||||
);
|
|
||||||
}
|
|
521
frontend/app/(app)/(tabs)/review/add/[beer_id].js
Normal file
@@ -0,0 +1,521 @@
|
|||||||
|
import { StyleSheet, TextInput, View, Image } from "react-native";
|
||||||
|
import { useCallback, useState } from "react";
|
||||||
|
import Button from "@components/Button";
|
||||||
|
import Text from "@components/Text";
|
||||||
|
import { colors } from "@components/style";
|
||||||
|
import * as ImagePicker from "expo-image-picker";
|
||||||
|
import DropDownPicker from "react-native-dropdown-picker";
|
||||||
|
const DropdownTheme = require("@components/DropdownTheme");
|
||||||
|
import { Platform } from "react-native";
|
||||||
|
import { useLocalSearchParams } from "expo-router";
|
||||||
|
|
||||||
|
export default function reviewAdd() {
|
||||||
|
// States for each dropdown
|
||||||
|
const routeParams = useLocalSearchParams();
|
||||||
|
const [openFoam, setOpenFoam] = useState(false);
|
||||||
|
const [openBitterSweetness, setOpenBitterSweetness] = useState(false);
|
||||||
|
const [openTaste, setOpenTaste] = useState(false);
|
||||||
|
const [openPackaging, setOpenPackaging] = useState(false);
|
||||||
|
const [openSourness, setOpenSourness] = useState(false);
|
||||||
|
const [openAgain, setOpenAgain] = useState(false);
|
||||||
|
|
||||||
|
// pěna
|
||||||
|
const [itemFoam, setFoamValue] = useState(null);
|
||||||
|
const [foam, setFoam] = useState([
|
||||||
|
{
|
||||||
|
label: "Bad",
|
||||||
|
value: "1",
|
||||||
|
icon: () => (
|
||||||
|
<Image
|
||||||
|
source={require("@assets/smileys/smiley-x-eyes.png")}
|
||||||
|
style={styles.iconStyle}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Medium",
|
||||||
|
value: "2",
|
||||||
|
icon: () => (
|
||||||
|
<Image
|
||||||
|
source={require("@assets/smileys/smiley-meh.png")}
|
||||||
|
style={styles.iconStyle}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Excellent",
|
||||||
|
value: "3",
|
||||||
|
icon: () => (
|
||||||
|
<Image
|
||||||
|
source={require("@assets/smileys/smiley.png")}
|
||||||
|
style={styles.iconStyle}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
// hořkost / sladkost
|
||||||
|
const [itemBitter_sweetness, setBitter_sweetnessValue] = useState(null);
|
||||||
|
const [bitter_sweetness, setBitter_sweetness] = useState([
|
||||||
|
{
|
||||||
|
label: "Bad",
|
||||||
|
value: "1",
|
||||||
|
icon: () => (
|
||||||
|
<Image
|
||||||
|
source={require("@assets/smileys/smiley-x-eyes.png")}
|
||||||
|
style={styles.iconStyle}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Medium",
|
||||||
|
value: "2",
|
||||||
|
icon: () => (
|
||||||
|
<Image
|
||||||
|
source={require("@assets/smileys/smiley-meh.png")}
|
||||||
|
style={styles.iconStyle}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Excellent",
|
||||||
|
value: "3",
|
||||||
|
icon: () => (
|
||||||
|
<Image
|
||||||
|
source={require("@assets/smileys/smiley.png")}
|
||||||
|
style={styles.iconStyle}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
//chuť
|
||||||
|
const [itemTaste, setTasteValue] = useState(null);
|
||||||
|
const [taste, setTaste] = useState([
|
||||||
|
{
|
||||||
|
label: "Disgust",
|
||||||
|
value: "1",
|
||||||
|
icon: () => (
|
||||||
|
<Image
|
||||||
|
source={require("@assets/smileys/smiley-blank.png")}
|
||||||
|
style={styles.iconStyle}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Not great, not terrible",
|
||||||
|
value: "2",
|
||||||
|
icon: () => (
|
||||||
|
<Image
|
||||||
|
source={require("@assets/smileys/smiley-nervous.png")}
|
||||||
|
style={styles.iconStyle}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Good",
|
||||||
|
value: "3",
|
||||||
|
icon: () => (
|
||||||
|
<Image
|
||||||
|
source={require("@assets/smileys/smiley-meh.png")}
|
||||||
|
style={styles.iconStyle}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Why not",
|
||||||
|
value: "4",
|
||||||
|
icon: () => (
|
||||||
|
<Image
|
||||||
|
source={require("@assets/smileys/smiley-wink.png")}
|
||||||
|
style={styles.iconStyle}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Excellent!",
|
||||||
|
value: "5",
|
||||||
|
icon: () => (
|
||||||
|
<Image
|
||||||
|
source={require("@assets/smileys/smiley.png")}
|
||||||
|
style={styles.iconStyle}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
// packaging
|
||||||
|
const [itemPackaging, setPackagingValue] = useState(null);
|
||||||
|
const [packaging, setPackaging] = useState([
|
||||||
|
{
|
||||||
|
label: "Disgust",
|
||||||
|
value: "1",
|
||||||
|
icon: () => (
|
||||||
|
<Image
|
||||||
|
source={require("@assets/smileys/smiley-blank.png")}
|
||||||
|
style={styles.iconStyle}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Not great, not terrible",
|
||||||
|
value: "2",
|
||||||
|
icon: () => (
|
||||||
|
<Image
|
||||||
|
source={require("@assets/smileys/smiley-nervous.png")}
|
||||||
|
style={styles.iconStyle}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Good",
|
||||||
|
value: "3",
|
||||||
|
icon: () => (
|
||||||
|
<Image
|
||||||
|
source={require("@assets/smileys/smiley-meh.png")}
|
||||||
|
style={styles.iconStyle}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Why not",
|
||||||
|
value: "4",
|
||||||
|
icon: () => (
|
||||||
|
<Image
|
||||||
|
source={require("@assets/smileys/smiley-wink.png")}
|
||||||
|
style={styles.iconStyle}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Excellent!",
|
||||||
|
value: "5",
|
||||||
|
icon: () => (
|
||||||
|
<Image
|
||||||
|
source={require("@assets/smileys/smiley.png")}
|
||||||
|
style={styles.iconStyle}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
//kyselost
|
||||||
|
const [itemSourness, setSournessValue] = useState(null);
|
||||||
|
const [sourness, setSourness] = useState([
|
||||||
|
{
|
||||||
|
label: "True",
|
||||||
|
value: true,
|
||||||
|
icon: () => (
|
||||||
|
<Image
|
||||||
|
source={require("@assets/smileys/smiley-blank.png")}
|
||||||
|
style={styles.iconStyle}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "False",
|
||||||
|
value: false,
|
||||||
|
icon: () => (
|
||||||
|
<Image
|
||||||
|
source={require("@assets/smileys/smiley-nervous.png")}
|
||||||
|
style={styles.iconStyle}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
//dal bych si znovu?
|
||||||
|
const [itemAgain, setAgainValue] = useState(null);
|
||||||
|
const [again, setAgain] = useState([
|
||||||
|
{
|
||||||
|
label: "Yes",
|
||||||
|
value: true,
|
||||||
|
icon: () => (
|
||||||
|
<Image
|
||||||
|
source={require("@assets/smileys/smiley.png")}
|
||||||
|
style={styles.iconStyle}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "No",
|
||||||
|
value: false,
|
||||||
|
icon: () => (
|
||||||
|
<Image
|
||||||
|
source={require("@assets/smileys/smiley-x-eyes.png")}
|
||||||
|
style={styles.iconStyle}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
//podmínky pro zavření ostatních dropdownů, pokud je jiný otevřený
|
||||||
|
const onOpenFoam = useCallback(() => {
|
||||||
|
setOpenBitterSweetness(false);
|
||||||
|
setOpenTaste(false);
|
||||||
|
setOpenPackaging(false);
|
||||||
|
setOpenSourness(false);
|
||||||
|
setOpenAgain(false);
|
||||||
|
setOpenFoam(true);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const onOpenBitterSweetness = useCallback(() => {
|
||||||
|
setOpenFoam(false);
|
||||||
|
setOpenTaste(false);
|
||||||
|
setOpenPackaging(false);
|
||||||
|
setOpenSourness(false);
|
||||||
|
setOpenAgain(false);
|
||||||
|
setOpenBitterSweetness(true);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const onOpenTaste = useCallback(() => {
|
||||||
|
setOpenFoam(false);
|
||||||
|
setOpenBitterSweetness(false);
|
||||||
|
setOpenPackaging(false);
|
||||||
|
setOpenSourness(false);
|
||||||
|
setOpenAgain(false);
|
||||||
|
setOpenTaste(true);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const onOpenPackaging = useCallback(() => {
|
||||||
|
setOpenFoam(false);
|
||||||
|
setOpenBitterSweetness(false);
|
||||||
|
setOpenTaste(false);
|
||||||
|
setOpenSourness(false);
|
||||||
|
setOpenAgain(false);
|
||||||
|
setOpenPackaging(true);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const onOpenSourness = useCallback(() => {
|
||||||
|
setOpenFoam(false);
|
||||||
|
setOpenBitterSweetness(false);
|
||||||
|
setOpenTaste(false);
|
||||||
|
setOpenPackaging(false);
|
||||||
|
setOpenAgain(false);
|
||||||
|
setOpenSourness(true);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const onOpenAgain = useCallback(() => {
|
||||||
|
setOpenFoam(false);
|
||||||
|
setOpenBitterSweetness(false);
|
||||||
|
setOpenTaste(false);
|
||||||
|
setOpenPackaging(false);
|
||||||
|
setOpenSourness(false);
|
||||||
|
setOpenAgain(true);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
DropDownPicker.addTheme("DropdownTheme", DropdownTheme);
|
||||||
|
DropDownPicker.setTheme("DropdownTheme");
|
||||||
|
|
||||||
|
async function addBeer() {
|
||||||
|
const req = await fetch(`${process.env.EXPO_PUBLIC_API_URL}/review/add`, {
|
||||||
|
method: "POST",
|
||||||
|
credentials: "include",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify({
|
||||||
|
beer_id: routeParams.beer_id,
|
||||||
|
foam: itemFoam,
|
||||||
|
bitter_sweetness: itemBitter_sweetness,
|
||||||
|
taste: itemTaste,
|
||||||
|
packaging: itemPackaging,
|
||||||
|
sourness: itemSourness,
|
||||||
|
would_again: itemAgain,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
const res = await req.json();
|
||||||
|
|
||||||
|
if (res.code == 201 && res.data._id) {
|
||||||
|
// window.location.href = `/review/${res.data._id}`;
|
||||||
|
// TODO: use react router for redirect
|
||||||
|
alert("Review was added!");
|
||||||
|
} else {
|
||||||
|
alert(
|
||||||
|
"Review was not added successfully. Please check your data and try again.",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<View style={styles.container}>
|
||||||
|
<View style={styles.form}>
|
||||||
|
<Text style={styles.text}>
|
||||||
|
How does your beer taste? Write a review!
|
||||||
|
</Text>
|
||||||
|
<Text style={styles.dropdownText} zIndex={6000} zIndexInverse={1000}>
|
||||||
|
How does the foam look like?
|
||||||
|
</Text>
|
||||||
|
<DropDownPicker
|
||||||
|
open={openFoam}
|
||||||
|
onOpen={onOpenFoam}
|
||||||
|
value={itemFoam}
|
||||||
|
items={foam}
|
||||||
|
setOpen={setOpenFoam}
|
||||||
|
setValue={setFoamValue}
|
||||||
|
setItems={setFoam}
|
||||||
|
placeholder="Please select..."
|
||||||
|
theme="DropdownTheme"
|
||||||
|
zIndex={6000}
|
||||||
|
zIndexInverse={1000}
|
||||||
|
/>
|
||||||
|
<Text style={styles.dropdownText} zIndex={5000} zIndexInverse={2000}>
|
||||||
|
More bitter, or more sweet?
|
||||||
|
</Text>
|
||||||
|
<DropDownPicker
|
||||||
|
open={openBitterSweetness}
|
||||||
|
onOpen={onOpenBitterSweetness}
|
||||||
|
value={itemBitter_sweetness}
|
||||||
|
items={bitter_sweetness}
|
||||||
|
setOpen={setOpenBitterSweetness}
|
||||||
|
setValue={setBitter_sweetnessValue}
|
||||||
|
setItems={setBitter_sweetness}
|
||||||
|
placeholder="Please select..."
|
||||||
|
theme="DropdownTheme"
|
||||||
|
zIndex={5000}
|
||||||
|
zIndexInverse={2000}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Text style={styles.dropdownText} zIndex={4000} zIndexInverse={3000}>
|
||||||
|
How does it taste?
|
||||||
|
</Text>
|
||||||
|
<DropDownPicker
|
||||||
|
open={openTaste}
|
||||||
|
onOpen={onOpenTaste}
|
||||||
|
value={itemTaste}
|
||||||
|
items={taste}
|
||||||
|
setOpen={setOpenTaste}
|
||||||
|
setValue={setTasteValue}
|
||||||
|
setItems={setTaste}
|
||||||
|
placeholder="Please select..."
|
||||||
|
theme="DropdownTheme"
|
||||||
|
zIndex={4000}
|
||||||
|
zIndexInverse={3000}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Text style={styles.dropdownText} zIndex={5000} zIndexInverse={4000}>
|
||||||
|
How do you like the packaging?
|
||||||
|
</Text>
|
||||||
|
<DropDownPicker
|
||||||
|
open={openPackaging}
|
||||||
|
onOpen={onOpenPackaging}
|
||||||
|
value={itemPackaging}
|
||||||
|
items={packaging}
|
||||||
|
setOpen={setOpenPackaging}
|
||||||
|
setValue={setPackagingValue}
|
||||||
|
setItems={setPackaging}
|
||||||
|
placeholder="Please select..."
|
||||||
|
theme="DropdownTheme"
|
||||||
|
zIndex={3000}
|
||||||
|
zIndexInverse={4000}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Text style={styles.dropdownText} zIndex={4000} zIndexInverse={5000}>
|
||||||
|
Is it sour?
|
||||||
|
</Text>
|
||||||
|
<DropDownPicker
|
||||||
|
open={openSourness}
|
||||||
|
onOpen={onOpenSourness}
|
||||||
|
value={itemSourness}
|
||||||
|
items={sourness}
|
||||||
|
setOpen={setOpenSourness}
|
||||||
|
setValue={setSournessValue}
|
||||||
|
setItems={setSourness}
|
||||||
|
placeholder="Please select..."
|
||||||
|
theme="DropdownTheme"
|
||||||
|
zIndex={2000}
|
||||||
|
zIndexInverse={5000}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Text style={styles.dropdownText} zIndex={5000} zIndexInverse={6000}>
|
||||||
|
Would you drink it again?
|
||||||
|
</Text>
|
||||||
|
<DropDownPicker
|
||||||
|
open={openAgain}
|
||||||
|
onOpen={onOpenAgain}
|
||||||
|
value={itemAgain}
|
||||||
|
items={again}
|
||||||
|
setOpen={setOpenAgain}
|
||||||
|
setValue={setAgainValue}
|
||||||
|
setItems={setAgain}
|
||||||
|
placeholder="Please select..."
|
||||||
|
theme="DropdownTheme"
|
||||||
|
zIndex={1000}
|
||||||
|
zIndexInverse={6000}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<View style={styles.buttonSend}>
|
||||||
|
<Button title="Add review" color={colors.gold} onPress={addBeer} />
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const styles = StyleSheet.create({
|
||||||
|
container: {
|
||||||
|
width: "100%",
|
||||||
|
height: "100%",
|
||||||
|
alignItems: "center",
|
||||||
|
display: "flex",
|
||||||
|
},
|
||||||
|
form: {
|
||||||
|
gap: 5,
|
||||||
|
width: "80%",
|
||||||
|
},
|
||||||
|
buttonSend: {
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
marginTop: "2%",
|
||||||
|
},
|
||||||
|
input: {
|
||||||
|
height: "auto",
|
||||||
|
width: "100%",
|
||||||
|
borderColor: "gray",
|
||||||
|
borderWidth: 1,
|
||||||
|
borderRadius: 10,
|
||||||
|
padding: 13,
|
||||||
|
color: "#fff",
|
||||||
|
},
|
||||||
|
imageContainer: {
|
||||||
|
alignItems: "center",
|
||||||
|
justifyContent: "center",
|
||||||
|
display: "flex",
|
||||||
|
flexDirection: "row",
|
||||||
|
gap: 10,
|
||||||
|
},
|
||||||
|
imageButton: {
|
||||||
|
backgroundColor: colors.dark,
|
||||||
|
borderColor: "gray",
|
||||||
|
borderWidth: 1,
|
||||||
|
borderRadius: 10,
|
||||||
|
},
|
||||||
|
imageTextButton: {
|
||||||
|
color: colors.white,
|
||||||
|
},
|
||||||
|
image: {
|
||||||
|
width: 150,
|
||||||
|
height: 150,
|
||||||
|
},
|
||||||
|
text: {
|
||||||
|
color: colors.white,
|
||||||
|
fontSize: 24,
|
||||||
|
textAlign: "center",
|
||||||
|
paddingBottom: "3%",
|
||||||
|
paddingTop: "10%",
|
||||||
|
},
|
||||||
|
iconStyle: {
|
||||||
|
width: 30,
|
||||||
|
height: 30,
|
||||||
|
},
|
||||||
|
dropdownContainer: {
|
||||||
|
width: "100%",
|
||||||
|
},
|
||||||
|
dropdownText: {
|
||||||
|
color: colors.white,
|
||||||
|
fontSize: 16,
|
||||||
|
paddingBottom: 1,
|
||||||
|
paddingTop: "1%",
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "flex-start",
|
||||||
|
flexDirection: "column",
|
||||||
|
},
|
||||||
|
});
|
139
frontend/app/(app)/(tabs)/review/index.js
Normal file
@@ -0,0 +1,139 @@
|
|||||||
|
import { View, StyleSheet, FlatList, Image } from "react-native";
|
||||||
|
import Text from "@components/Text";
|
||||||
|
import Button from "@components/Button";
|
||||||
|
import { colors } from "@components/style";
|
||||||
|
import { router } from "expo-router";
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import { useAuth } from "@context/AuthContext";
|
||||||
|
|
||||||
|
export default function Tab() {
|
||||||
|
const { authState } = useAuth();
|
||||||
|
const user = authState.user;
|
||||||
|
const [data, setData] = useState([]);
|
||||||
|
useEffect(() => {
|
||||||
|
fetchData();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const API_HOST = process.env.EXPO_PUBLIC_API_URL.replace("/api/v1", "");
|
||||||
|
|
||||||
|
async function fetchData() {
|
||||||
|
try {
|
||||||
|
const res = await fetch(`${process.env.EXPO_PUBLIC_API_URL}/review/get`, {
|
||||||
|
method: "GET",
|
||||||
|
credentials: "include",
|
||||||
|
});
|
||||||
|
let data = await res.json();
|
||||||
|
// show only logged in user's data
|
||||||
|
data = data.data.filter((review) => review.user_id == user._id);
|
||||||
|
|
||||||
|
let beers = await fetch(`${process.env.EXPO_PUBLIC_API_URL}/beer/get`, {
|
||||||
|
method: "GET",
|
||||||
|
credentials: "include",
|
||||||
|
});
|
||||||
|
beers = await beers.json();
|
||||||
|
beers = beers.data;
|
||||||
|
console.log(beers);
|
||||||
|
|
||||||
|
async function getBeerParam(search, beers) {
|
||||||
|
for (let i = 0; i < beers.length; i++) {
|
||||||
|
if (beers[i]._id == search) {
|
||||||
|
return beers[i];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
data.forEach(async (el) => {
|
||||||
|
el.beer = await getBeerParam(el.beer_id, beers);
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log("reviews", data);
|
||||||
|
setData(data);
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
alert("Something went wrong");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const opt3 = ["Bad", "Medium", "Excellent!"];
|
||||||
|
const opt5 = [
|
||||||
|
"Disgust",
|
||||||
|
"Not great, not terrible",
|
||||||
|
"Good",
|
||||||
|
"Why not?",
|
||||||
|
"Excellent!",
|
||||||
|
];
|
||||||
|
const opt2 = ["Yes", "No"];
|
||||||
|
const sourness = ["Good", "Bad"];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<View style={styles.container}>
|
||||||
|
<FlatList
|
||||||
|
data={data}
|
||||||
|
style={styles.reviewList}
|
||||||
|
keyExtractor={(item) => String(item._id)}
|
||||||
|
renderItem={({ item }) => (
|
||||||
|
<View style={styles.itemContainer}>
|
||||||
|
<View>
|
||||||
|
<Text>{item.beer.name}</Text>
|
||||||
|
<Text>{item.beer.brand}</Text>
|
||||||
|
<Text>{item.beer.degree}°</Text>
|
||||||
|
<Text>{item.beer.packaging}</Text>
|
||||||
|
<Image
|
||||||
|
source={
|
||||||
|
item.beer.imgs[0]
|
||||||
|
? {
|
||||||
|
uri: `${API_HOST}/public/uploads/${item.beer.imgs[0]}`,
|
||||||
|
}
|
||||||
|
: {
|
||||||
|
uri: "https://imagesvc.meredithcorp.io/v3/mm/image?url=https:%2F%2Fstatic.onecms.io%2Fwp-content%2Fuploads%2Fsites%2F44%2F2020%2F09%2F29%2Flight-beer.jpg",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
style={styles.itemImg}
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
<View>
|
||||||
|
<Text>Foam → {opt3[item.foam - 1]}</Text>
|
||||||
|
<Text>
|
||||||
|
Bitter / Sweetness → {opt3[item.bitter_sweetness - 1]}
|
||||||
|
</Text>
|
||||||
|
<Text>Taste → {opt5[item.taste - 1]}</Text>
|
||||||
|
<Text>Packaging → {opt5[item.packaging - 1]}</Text>
|
||||||
|
<Text>Sourness → {sourness[item.sourness - 1]}</Text>
|
||||||
|
<Text>Would again? → {opt2[item.would_again - 1]}</Text>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export const styles = StyleSheet.create({
|
||||||
|
container: {
|
||||||
|
flex: 1,
|
||||||
|
justifyContent: "center",
|
||||||
|
alignItems: "center",
|
||||||
|
marginTop: "5%",
|
||||||
|
},
|
||||||
|
reviewList: {
|
||||||
|
width: "100%",
|
||||||
|
paddingHorizontal: "15%",
|
||||||
|
marginTop: "5%",
|
||||||
|
},
|
||||||
|
itemContainer: {
|
||||||
|
borderColor: "gray",
|
||||||
|
borderWidth: 1,
|
||||||
|
borderRadius: 10,
|
||||||
|
padding: 13,
|
||||||
|
marginBottom: "5%",
|
||||||
|
flex: 1,
|
||||||
|
flexDirection: "row",
|
||||||
|
justifyContent: "space-between",
|
||||||
|
},
|
||||||
|
itemImg: {
|
||||||
|
width: 150,
|
||||||
|
aspectRatio: 1,
|
||||||
|
resizeMode: "contain",
|
||||||
|
marginTop: "5%",
|
||||||
|
},
|
||||||
|
});
|
BIN
frontend/assets/DropdownIcons/arrow-down.png
Normal file
After Width: | Height: | Size: 522 B |
BIN
frontend/assets/DropdownIcons/arrow-up.png
Normal file
After Width: | Height: | Size: 807 B |
BIN
frontend/assets/DropdownIcons/close.png
Normal file
After Width: | Height: | Size: 338 B |
BIN
frontend/assets/DropdownIcons/tick.png
Normal file
After Width: | Height: | Size: 1.2 KiB |
BIN
frontend/assets/smileys/smiley-blank.png
Normal file
After Width: | Height: | Size: 7.9 KiB |
BIN
frontend/assets/smileys/smiley-meh.png
Normal file
After Width: | Height: | Size: 8.1 KiB |
BIN
frontend/assets/smileys/smiley-nervous.png
Normal file
After Width: | Height: | Size: 8.9 KiB |
BIN
frontend/assets/smileys/smiley-sad.png
Normal file
After Width: | Height: | Size: 8.9 KiB |
BIN
frontend/assets/smileys/smiley-wink.png
Normal file
After Width: | Height: | Size: 11 KiB |
BIN
frontend/assets/smileys/smiley-x-eyes.png
Normal file
After Width: | Height: | Size: 8.9 KiB |
BIN
frontend/assets/smileys/smiley.png
Normal file
After Width: | Height: | Size: 9.0 KiB |
@@ -7,6 +7,8 @@ export default function Button(props) {
|
|||||||
title = "Button",
|
title = "Button",
|
||||||
color = "black",
|
color = "black",
|
||||||
textColor = "white",
|
textColor = "white",
|
||||||
|
buttonStyle,
|
||||||
|
textStyle,
|
||||||
} = props;
|
} = props;
|
||||||
return (
|
return (
|
||||||
<Pressable
|
<Pressable
|
||||||
@@ -19,10 +21,13 @@ export default function Button(props) {
|
|||||||
: "black",
|
: "black",
|
||||||
},
|
},
|
||||||
styles.button,
|
styles.button,
|
||||||
|
buttonStyle,
|
||||||
]}
|
]}
|
||||||
onPress={onPress}
|
onPress={onPress}
|
||||||
>
|
>
|
||||||
<Text style={[styles.text, { color: textColor }]}>{title}</Text>
|
<Text style={[styles.text, { color: textColor }, textStyle]}>
|
||||||
|
{title}
|
||||||
|
</Text>
|
||||||
</Pressable>
|
</Pressable>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
170
frontend/components/DropdownTheme.js
Normal file
@@ -0,0 +1,170 @@
|
|||||||
|
import { StyleSheet } from "react-native";
|
||||||
|
|
||||||
|
import { colors } from "@components/style";
|
||||||
|
|
||||||
|
export const ICONS = {
|
||||||
|
ARROW_DOWN: require("@assets/DropdownIcons/arrow-down.png"),
|
||||||
|
ARROW_UP: require("@assets/DropdownIcons/arrow-up.png"),
|
||||||
|
TICK: require("@assets/DropdownIcons/tick.png"),
|
||||||
|
CLOSE: require("../assets/DropdownIcons/close.png"),
|
||||||
|
};
|
||||||
|
|
||||||
|
export default StyleSheet.create({
|
||||||
|
container: {
|
||||||
|
width: "100%",
|
||||||
|
},
|
||||||
|
style: {
|
||||||
|
flexDirection: "row",
|
||||||
|
alignItems: "center",
|
||||||
|
justifyContent: "space-between",
|
||||||
|
width: "100%",
|
||||||
|
minHeight: 50,
|
||||||
|
borderRadius: 8,
|
||||||
|
borderWidth: 1,
|
||||||
|
borderColor: "gray",
|
||||||
|
paddingHorizontal: 10,
|
||||||
|
paddingVertical: 3,
|
||||||
|
backgroundColor: colors.dark,
|
||||||
|
},
|
||||||
|
label: {
|
||||||
|
flex: 1,
|
||||||
|
color: colors.placeholder,
|
||||||
|
},
|
||||||
|
labelContainer: {
|
||||||
|
flex: 1,
|
||||||
|
flexDirection: "row",
|
||||||
|
},
|
||||||
|
arrowIcon: {
|
||||||
|
width: 20,
|
||||||
|
height: 20,
|
||||||
|
},
|
||||||
|
tickIcon: {
|
||||||
|
width: 20,
|
||||||
|
height: 20,
|
||||||
|
},
|
||||||
|
closeIcon: {
|
||||||
|
width: 30,
|
||||||
|
height: 30,
|
||||||
|
},
|
||||||
|
badgeStyle: {
|
||||||
|
flexDirection: "row",
|
||||||
|
alignItems: "center",
|
||||||
|
borderRadius: 15,
|
||||||
|
backgroundColor: colors.white,
|
||||||
|
paddingHorizontal: 10,
|
||||||
|
paddingVertical: 5,
|
||||||
|
},
|
||||||
|
badgeDotStyle: {
|
||||||
|
width: 10,
|
||||||
|
height: 10,
|
||||||
|
borderRadius: 10 / 2,
|
||||||
|
marginRight: 8,
|
||||||
|
backgroundColor: colors.white,
|
||||||
|
},
|
||||||
|
badgeSeparator: {
|
||||||
|
width: 5,
|
||||||
|
},
|
||||||
|
listBody: {
|
||||||
|
height: "100%",
|
||||||
|
},
|
||||||
|
listBodyContainer: {
|
||||||
|
flexGrow: 1,
|
||||||
|
alignItems: "center",
|
||||||
|
},
|
||||||
|
dropDownContainer: {
|
||||||
|
position: "absolute",
|
||||||
|
backgroundColor: colors.darkSecondary,
|
||||||
|
borderRadius: 10,
|
||||||
|
borderColor: colors.black,
|
||||||
|
borderWidth: 1,
|
||||||
|
width: "100%",
|
||||||
|
overflow: "hidden",
|
||||||
|
zIndex: 1000,
|
||||||
|
},
|
||||||
|
modalContentContainer: {
|
||||||
|
flexGrow: 1,
|
||||||
|
backgroundColor: colors.white,
|
||||||
|
},
|
||||||
|
listItemContainer: {
|
||||||
|
flexDirection: "row",
|
||||||
|
alignItems: "center",
|
||||||
|
justifyContent: "space-between",
|
||||||
|
paddingHorizontal: 10,
|
||||||
|
height: 40,
|
||||||
|
},
|
||||||
|
listItemLabel: {
|
||||||
|
flex: 1,
|
||||||
|
color: colors.placeholder,
|
||||||
|
},
|
||||||
|
iconContainer: {
|
||||||
|
marginRight: 10,
|
||||||
|
},
|
||||||
|
arrowIconContainer: {
|
||||||
|
marginLeft: 10,
|
||||||
|
},
|
||||||
|
tickIconContainer: {
|
||||||
|
marginLeft: 10,
|
||||||
|
},
|
||||||
|
closeIconContainer: {
|
||||||
|
marginLeft: 10,
|
||||||
|
},
|
||||||
|
listParentLabel: {},
|
||||||
|
listChildLabel: {},
|
||||||
|
listParentContainer: {},
|
||||||
|
listChildContainer: {
|
||||||
|
paddingLeft: 40,
|
||||||
|
},
|
||||||
|
searchContainer: {
|
||||||
|
flexDirection: "row",
|
||||||
|
alignItems: "center",
|
||||||
|
padding: 10,
|
||||||
|
borderBottomColor: colors.darkSecondary,
|
||||||
|
borderBottomWidth: 1,
|
||||||
|
},
|
||||||
|
searchTextInput: {
|
||||||
|
flexGrow: 1,
|
||||||
|
flexShrink: 1,
|
||||||
|
margin: 0,
|
||||||
|
paddingHorizontal: 10,
|
||||||
|
paddingVertical: 5,
|
||||||
|
borderRadius: 8,
|
||||||
|
borderColor: colors.darkSecondary,
|
||||||
|
borderWidth: 1,
|
||||||
|
color: colors.white,
|
||||||
|
},
|
||||||
|
itemSeparator: {
|
||||||
|
height: 1,
|
||||||
|
backgroundColor: colors.darkSecondary,
|
||||||
|
},
|
||||||
|
flatListContentContainer: {
|
||||||
|
flexGrow: 1,
|
||||||
|
},
|
||||||
|
customItemContainer: {},
|
||||||
|
customItemLabel: {
|
||||||
|
fontStyle: "italic",
|
||||||
|
},
|
||||||
|
listMessageContainer: {
|
||||||
|
flex: 1,
|
||||||
|
justifyContent: "center",
|
||||||
|
alignItems: "center",
|
||||||
|
padding: 10,
|
||||||
|
},
|
||||||
|
listMessageText: {
|
||||||
|
color: colors.gold,
|
||||||
|
},
|
||||||
|
selectedItemContainer: {},
|
||||||
|
selectedItemLabel: {},
|
||||||
|
modalTitle: {
|
||||||
|
fontSize: 18,
|
||||||
|
color: colors.gold,
|
||||||
|
},
|
||||||
|
extendableBadgeContainer: {
|
||||||
|
flexDirection: "row",
|
||||||
|
flexWrap: "wrap",
|
||||||
|
flex: 1,
|
||||||
|
},
|
||||||
|
extendableBadgeItemContainer: {
|
||||||
|
marginVertical: 3,
|
||||||
|
marginEnd: 7,
|
||||||
|
},
|
||||||
|
});
|
@@ -8,4 +8,5 @@ export const colors = {
|
|||||||
dark: "#010409",
|
dark: "#010409",
|
||||||
darkSecondary: "#0D1117",
|
darkSecondary: "#0D1117",
|
||||||
white: "#FFFFFF",
|
white: "#FFFFFF",
|
||||||
|
placeholder: "#aaaaaa",
|
||||||
};
|
};
|
||||||
|
2092
frontend/package-lock.json
generated
@@ -6,27 +6,32 @@
|
|||||||
"start": "expo start",
|
"start": "expo start",
|
||||||
"android": "expo start --android",
|
"android": "expo start --android",
|
||||||
"ios": "expo start --ios",
|
"ios": "expo start --ios",
|
||||||
"web": "expo start --web"
|
"web": "expo start --web",
|
||||||
|
"build:web": "npx expo export"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@expo/metro-runtime": "~3.1.3",
|
"@expo/metro-runtime": "~3.2.1",
|
||||||
"@react-native-async-storage/async-storage": "^1.23.1",
|
"@react-native-async-storage/async-storage": "^1.23.1",
|
||||||
"@types/react": "~18.2.45",
|
"@types/react": "~18.2.45",
|
||||||
"axios": "^1.6.8",
|
"axios": "^1.6.8",
|
||||||
"expo": "~50.0.17",
|
"expo": "^51.0.2",
|
||||||
"expo-constants": "~15.4.6",
|
"expo-constants": "~16.0.1",
|
||||||
"expo-linear-gradient": "~12.7.2",
|
"expo-image-picker": "~15.0.4",
|
||||||
"expo-linking": "~6.2.2",
|
"expo-linear-gradient": "~13.0.2",
|
||||||
"expo-router": "~3.4.10",
|
"expo-linking": "~6.3.1",
|
||||||
"expo-secure-store": "^12.8.1",
|
"expo-router": "~3.5.11",
|
||||||
"expo-status-bar": "~1.11.1",
|
"expo-secure-store": "~13.0.1",
|
||||||
|
"expo-status-bar": "~1.12.1",
|
||||||
|
"expo-system-ui": "~3.0.4",
|
||||||
"react": "18.2.0",
|
"react": "18.2.0",
|
||||||
"react-dom": "18.2.0",
|
"react-dom": "18.2.0",
|
||||||
"react-native": "0.73.6",
|
"react-native": "0.74.1",
|
||||||
"react-native-safe-area-context": "4.8.2",
|
"react-native-dropdown-picker": "^5.4.6",
|
||||||
"react-native-screens": "~3.29.0",
|
"react-native-range-slider-expo": "^1.4.3",
|
||||||
|
"react-native-safe-area-context": "4.10.1",
|
||||||
|
"react-native-screens": "3.31.1",
|
||||||
"react-native-web": "~0.19.6",
|
"react-native-web": "~0.19.6",
|
||||||
"expo-system-ui": "~2.9.4"
|
"@shopify/flash-list": "1.6.4"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@babel/core": "^7.20.0",
|
"@babel/core": "^7.20.0",
|
||||||
|