added qr scaning and finishing touches

This commit is contained in:
David Katrinka 2026-01-09 21:10:18 +01:00
parent bc347c3f05
commit 2cf7f5dc7b
8 changed files with 153 additions and 9 deletions

View File

@ -9,7 +9,11 @@
"userInterfaceStyle": "automatic", "userInterfaceStyle": "automatic",
"newArchEnabled": true, "newArchEnabled": true,
"ios": { "ios": {
"supportsTablet": true "supportsTablet": true,
"infoPlist": {
"NSCameraUsageDescription": "$(PRODUCT_NAME) needs access to your Camera."
},
"bundleIdentifier": "com.anonymous.ai-questions-app"
}, },
"android": { "android": {
"adaptiveIcon": { "adaptiveIcon": {
@ -19,7 +23,9 @@
"monochromeImage": "./assets/images/android-icon-monochrome.png" "monochromeImage": "./assets/images/android-icon-monochrome.png"
}, },
"edgeToEdgeEnabled": true, "edgeToEdgeEnabled": true,
"predictiveBackGestureEnabled": false "predictiveBackGestureEnabled": false,
"permissions": ["android.permission.CAMERA"],
"package": "com.anonymous.ai_questions_app"
}, },
"web": { "web": {
"output": "static", "output": "static",
@ -39,7 +45,14 @@
} }
} }
], ],
"expo-secure-store" "expo-secure-store",
[
"react-native-vision-camera",
{
"cameraPermissionText": "$(PRODUCT_NAME) needs access to your Camera.",
"enableCodeScanner": true
}
]
], ],
"experiments": { "experiments": {
"typedRoutes": true, "typedRoutes": true,

View File

@ -49,7 +49,7 @@ export default function TestsScreen() {
selectedValue={category ? `${category}` : undefined} selectedValue={category ? `${category}` : undefined}
onValueChange={handleChangeCategory} onValueChange={handleChangeCategory}
placeholder="Select category" /> placeholder="Select category" />
<Button> <Button onPress={() => router.push('/qr')}>
<ButtonText><FontAwesome5 name="qrcode" size={18} /></ButtonText> <ButtonText><FontAwesome5 name="qrcode" size={18} /></ButtonText>
</Button> </Button>
</View> </View>

104
app/qr.tsx Normal file
View File

@ -0,0 +1,104 @@
import { useStartTestMutation } from "@/api/userTests";
import { Button, ButtonText } from "@/components/ui/button";
import { ThemedText } from "@/components/ui/themed-text";
import getErrorAxiosMessage from "@/utils/get-error-axios-message";
import { router, Stack, useFocusEffect } from "expo-router";
import { useCallback, useState } from "react";
import { StyleSheet, Text, View } from "react-native";
import { useToast } from "react-native-toast-notifications";
import {
Camera,
useCameraDevice,
useCameraPermission,
useCodeScanner,
} from "react-native-vision-camera";
const QrScreen = () => {
const device = useCameraDevice("back");
const { hasPermission, requestPermission } = useCameraPermission();
const toast = useToast();
const [scanned, setScanned] = useState(false);
const { mutate, isPending } = useStartTestMutation();
useFocusEffect(
useCallback(() => {
setScanned(false);
}, [])
);
const handleStartTest = (id: number) => {
mutate(
{
test_id: id,
},
{
onSuccess: (data) => {
router.push(`/user-tests/doing/${data.id}`);
},
onError: (error) => {
toast.show(getErrorAxiosMessage(error), { type: "danger" });
},
}
);
};
const codeScanner = useCodeScanner({
codeTypes: ["qr", "ean-13"],
onCodeScanned: (codes) => {
if (scanned) return;
if (!codes || codes.length === 0) return;
const scannedValue = codes[0].value;
if (scannedValue) {
const id = parseInt(scannedValue, 10);
setScanned(true);
toast.show("Going to test", { type: "normal" });
router.push(`/tests/${id}`);
// handleStartTest(id);
} else {
toast.show("Invalid QR code", { type: "warning" });
}
},
});
if (!hasPermission) {
return (
<View style={styles.center}>
<Stack.Screen options={{ title: "Scan QR Code" }} />
<ThemedText>No camera permission</ThemedText>
<Button onPress={requestPermission} className="mt-2">
<ButtonText>Grant Permission</ButtonText>
</Button>
</View>
);
}
if (!device) {
return (
<View style={styles.center}>
<ThemedText>No camera device found</ThemedText>
</View>
);
}
return (
<>
<Stack.Screen options={{ title: "Scan QR Code" }} />
<Camera
codeScanner={codeScanner}
style={StyleSheet.absoluteFill}
device={device}
isActive={true}
/>
</>
);
};
const styles = StyleSheet.create({
center: {
flex: 1,
alignItems: "center",
justifyContent: "center",
},
});
export default QrScreen;

View File

@ -252,7 +252,7 @@ const DoingUserTestScreen = () => {
const styles = StyleSheet.create({ const styles = StyleSheet.create({
container: { container: {
flex: 1, flex: 1,
alignItems: "flex-start", width: "100%"
}, },
top: { top: {
width: "100%", width: "100%",

View File

@ -106,7 +106,7 @@ const Answer = ({
{isTextType ? ( {isTextType ? (
<FontAwesome5 name="quote-left" /> <FontAwesome5 name="quote-left" />
) : ( ) : (
<ThemedText darkColor="#fff" lightColor="#fff"> <ThemedText darkColor="#000" lightColor="#fff">
{answer.id} {answer.id}
</ThemedText> </ThemedText>
)} )}

View File

@ -33,12 +33,13 @@ const CustomSelect = ({
return options; return options;
}, [options, noneOption]); }, [options, noneOption]);
const backgroundColor = useThemeColor({ }, "background"); const backgroundColor = useThemeColor({ }, "background");
const metaColor = useThemeColor({}, "meta");
return ( return (
<Select className={className} onValueChange={onValueChange} selectedValue={selectedValue} defaultValue={defaultValue}> <Select className={className} onValueChange={onValueChange} selectedValue={selectedValue} defaultValue={defaultValue}>
<SelectTrigger className="justify-between" variant="outline" size="md" style={{ backgroundColor }}> <SelectTrigger className="justify-between" variant="outline" size="md" style={{ backgroundColor }}>
<SelectInput placeholder={placeholder} /> <SelectInput placeholder={placeholder} />
<FontAwesome5 className="mr-3" name="chevron-down" /> <FontAwesome5 style={{color: metaColor}} className="mr-3" name="chevron-down" />
</SelectTrigger> </SelectTrigger>
<SelectPortal> <SelectPortal>
<SelectBackdrop /> <SelectBackdrop />

25
package-lock.json generated
View File

@ -47,6 +47,7 @@
"react-native-svg": "^15.15.1", "react-native-svg": "^15.15.1",
"react-native-toast-notifications": "^3.4.0", "react-native-toast-notifications": "^3.4.0",
"react-native-vector-icons": "^10.3.0", "react-native-vector-icons": "^10.3.0",
"react-native-vision-camera": "^4.7.3",
"react-native-web": "~0.21.0", "react-native-web": "~0.21.0",
"react-native-worklets": "^0.5.2", "react-native-worklets": "^0.5.2",
"react-stately": "^3.43.0", "react-stately": "^3.43.0",
@ -14687,6 +14688,30 @@
"node": ">=10" "node": ">=10"
} }
}, },
"node_modules/react-native-vision-camera": {
"version": "4.7.3",
"resolved": "https://registry.npmjs.org/react-native-vision-camera/-/react-native-vision-camera-4.7.3.tgz",
"integrity": "sha512-g1/neOyjSqn1kaAa2FxI/qp5KzNvPcF0bnQw6NntfbxH6tm0+8WFZszlgb5OV+iYlB6lFUztCbDtyz5IpL47OA==",
"license": "MIT",
"peerDependencies": {
"@shopify/react-native-skia": "*",
"react": "*",
"react-native": "*",
"react-native-reanimated": "*",
"react-native-worklets-core": "*"
},
"peerDependenciesMeta": {
"@shopify/react-native-skia": {
"optional": true
},
"react-native-reanimated": {
"optional": true
},
"react-native-worklets-core": {
"optional": true
}
}
},
"node_modules/react-native-web": { "node_modules/react-native-web": {
"version": "0.21.2", "version": "0.21.2",
"resolved": "https://registry.npmjs.org/react-native-web/-/react-native-web-0.21.2.tgz", "resolved": "https://registry.npmjs.org/react-native-web/-/react-native-web-0.21.2.tgz",

View File

@ -5,8 +5,8 @@
"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 run:android",
"ios": "expo start --ios", "ios": "expo run:ios",
"web": "expo start --web", "web": "expo start --web",
"lint": "expo lint" "lint": "expo lint"
}, },
@ -50,6 +50,7 @@
"react-native-svg": "^15.15.1", "react-native-svg": "^15.15.1",
"react-native-toast-notifications": "^3.4.0", "react-native-toast-notifications": "^3.4.0",
"react-native-vector-icons": "^10.3.0", "react-native-vector-icons": "^10.3.0",
"react-native-vision-camera": "^4.7.3",
"react-native-web": "~0.21.0", "react-native-web": "~0.21.0",
"react-native-worklets": "^0.5.2", "react-native-worklets": "^0.5.2",
"react-stately": "^3.43.0", "react-stately": "^3.43.0",