Skip to content

Commit

Permalink
feat(mobile): connect mobile to backend and implement tagging for for…
Browse files Browse the repository at this point in the history
…um questions (#587)

* [wip] feat(mobile): change forum feed API URL to the deployed backend application

* feat(mobile): add Stack Navigator for Login screen in App.tsx

* feat(mobile): connect Login functionality to the deployed backend application

* feat(mobile): update CreateQuestion screen to use the deployed backend application

* chore(mobile): add LoggedinUser type definition

* refactor(mobile): update AuthContext to include user information

* feat(mobile): update initial route in Stack Navigator based on authentication state

* chore(mobile): add Tag, StoredTag, and TagSearchResult types

* feat(mobile): update CreateQuestion screen to include adding tags functionality

* fix(mobile): fix after merge navigation problems
  • Loading branch information
Meminseeker authored Oct 21, 2024
1 parent d95f902 commit 36ccf31
Show file tree
Hide file tree
Showing 8 changed files with 283 additions and 136 deletions.
6 changes: 4 additions & 2 deletions mobile/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -117,7 +117,6 @@ export const Layout = () => {
name="ForumQuestionDetail"
component={ForumQuestionDetail}
/>
<Stack.Screen name="Register" component={Register} />
{/*
{authState?.authenticated
? <Stack.Screen
Expand All @@ -130,7 +129,10 @@ export const Layout = () => {
*/}
</>
) : (
<Stack.Screen name="Login" component={Login} />
<>
<Stack.Screen name="Login" component={Login} />
<Stack.Screen name="Register" component={Register} />
</>
)}
</Stack.Navigator>
</NavigationContainer>
Expand Down
64 changes: 33 additions & 31 deletions mobile/app/context/AuthContext.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,14 @@
import axios from "axios";
import * as SecureStore from "expo-secure-store";
import { createContext, useContext, useEffect, useState } from "react";
import { LoggedinUser } from "../types/user";

interface AuthProps {
authState?: { token: string | null; authenticated: boolean | null };
authState?: {
token: string | null;
authenticated: boolean | null;
user: LoggedinUser | null;
};
onRegister?: (
username: string,
fullname: string,
Expand All @@ -15,7 +20,8 @@ interface AuthProps {
}

const TOKEN_KEY = "my-jwt";
export const API_URL = "https://api.developbetterapps.com";
const USER_KEY = "loggedin-user";
export const API_URL = "http://54.247.125.93/api/v1";
const AuthContext = createContext<AuthProps>({});

export const useAuth = () => {
Expand All @@ -26,14 +32,17 @@ export const AuthProvider = ({ children }: any) => {
const [authState, setAuthState] = useState<{
token: string | null;
authenticated: boolean | null;
user: LoggedinUser | null;
}>({
token: null,
authenticated: null,
user: null,
});

useEffect(() => {
const loadToken = async () => {
const token = await SecureStore.getItemAsync(TOKEN_KEY);
const user = await SecureStore.getItemAsync(USER_KEY);
console.log("stored:", token);

if (token) {
Expand All @@ -42,6 +51,7 @@ export const AuthProvider = ({ children }: any) => {
setAuthState({
token: token,
authenticated: true,
user: user ? JSON.parse(user) : null,
});
}
};
Expand All @@ -58,37 +68,38 @@ export const AuthProvider = ({ children }: any) => {
`Registering username: '${username}', fullname:'${fullname}' email: '${email}' and password: '${password}'`
);
try {
const result = await axios.post(`${API_URL}/users`, { email, password });

setAuthState({
token: result.data.token,
authenticated: true,
return await axios.post(`${API_URL}/auth/register/`, {
username,
password,
email,
full_name: fullname,
});

axios.defaults.headers.common["Authorization"] =
`Bearer ${result.data.token}`;

await SecureStore.setItemAsync(TOKEN_KEY, result.data.token);

return result;
} catch (e) {
return { error: true, message: (e as any).response.data.msg };
}
};

const login = async (email: string, password: string) => {
const login = async (username: string, password: string) => {
try {
const result = await axios.post(`${API_URL}/auth`, { email, password });
const result = await axios.post(`${API_URL}/auth/login/`, {
username,
password,
});

setAuthState({
token: result.data.token,
token: result.data.access,
authenticated: true,
user: result.data.user,
});

axios.defaults.headers.common["Authorization"] =
`Bearer ${result.data.token}`;
`Bearer ${result.data.access}`;

await SecureStore.setItemAsync(TOKEN_KEY, result.data.token);
await SecureStore.setItemAsync(TOKEN_KEY, result.data.access);
await SecureStore.setItemAsync(
USER_KEY,
JSON.stringify(result.data.user)
);

return result;
} catch (e) {
Expand All @@ -97,24 +108,15 @@ export const AuthProvider = ({ children }: any) => {
};

const logout = async () => {
try {
await SecureStore.deleteItemAsync(TOKEN_KEY);
axios.defaults.headers.common["Authorization"] = "";

setAuthState({
token: null,
authenticated: false,
});
} catch (error) {
console.error("Failed to logout:", error);
throw new Error("Logout failed.");
}
await SecureStore.deleteItemAsync(TOKEN_KEY);
await SecureStore.deleteItemAsync(USER_KEY);

axios.defaults.headers.common["Authorization"] = "";

setAuthState({
token: null,
authenticated: false,
user: null,
});
};

Expand Down
180 changes: 143 additions & 37 deletions mobile/app/screens/CreateQuestion.tsx
Original file line number Diff line number Diff line change
@@ -1,61 +1,105 @@
import React, { useState } from "react";
import { View, Text, TextInput, Button, Alert } from "react-native";
import axios from "axios";
import { useNavigation } from "@react-navigation/native";
import axios from "axios";
import React, { useEffect, useState } from "react";
import {
Alert,
Button,
FlatList,
StyleSheet,
Text,
TextInput,
TouchableOpacity,
View,
} from "react-native";
import { Tag, TagSearchResult } from "../types/tag";

const API_URL = "http://10.0.2.2:3000/forum-feed"; // URL to your forum-feed
const API_URL = "http://54.247.125.93/api/v1/forum-questions/"; // URL to your forum-feed

const CreateQuestion: React.FC = () => {
const [title, setTitle] = useState("");
const [body, setBody] = useState("");
const [question, setQuestion] = useState("");
const [tags, setTags] = useState<Tag[]>([]);
const [tagInput, setTagInput] = useState("");
const [suggestedTags, setSuggestedTags] = useState<TagSearchResult[]>([]);
const [loading, setLoading] = useState(false);

const navigation = useNavigation();

// Debounce function to delay API calls
useEffect(() => {
const timeoutId = setTimeout(() => {
if (tagInput.length >= 2) {
fetchTagSuggestions(tagInput);
}
}, 500);

return () => clearTimeout(timeoutId);
}, [tagInput]);

const fetchTagSuggestions = async (input: string) => {
const API_URL = `http://54.247.125.93/api/v1/tagging/?word=${input}&lang=EN`;
try {
const result = await axios.get(`${API_URL}`);
const combinedTags: TagSearchResult[] = [];
if (result.data.NOUN) {
combinedTags.push(...result.data.NOUN);
}
if (result.data.VERB) {
combinedTags.push(...result.data.VERB);
}
if (result.data.ADJ) {
combinedTags.push(...result.data.ADJ);
}
if (result.data.ADV) {
combinedTags.push(...result.data.ADV);
}
setSuggestedTags(combinedTags);
} catch (error) {
console.error("Error fetching tag suggestions", error);
}
};

// Handle tag selection
const handleTagSelect = (tag: TagSearchResult) => {
const convertedTag: Tag = {
name: tagInput,
linked_data_id: tag.id,
description: tag.description,
};

if (!tags.includes(convertedTag)) {
setTags([...tags, convertedTag]);
}
setTagInput("");
setSuggestedTags([]);
};

// Handle tag removal
const handleTagRemove = (tag: Tag) => {
setTags(tags.filter((t) => t !== tag));
};

const handleSubmit = async () => {
if (!title || !body) {
Alert.alert("Error", "Please fill in both the title and body.");
if (!title || !question) {
Alert.alert("Error", "Please fill in both the title and question.");
return;
}

setLoading(true);

// Mock new question data
const newQuestion = {
id: Date.now().toString(),
title,
body,
tags: [
{
id: "1",
name: "English",
description: "English language",
},
],
author: {
full_name: "Current User",
username: "current_user",
avatar: "https://example.com/avatar.jpg",
},
created_at: new Date().toISOString(),
answers_count: 0,
is_bookmarked: false,
is_upvoted: false,
upvotes_count: 0,
is_downvoted: false,
downvotes_count: 0,
question,
tags,
};

try {
await axios.patch(`${API_URL}`, {
questions: [
...(await axios.get(`${API_URL}`)).data.questions,
newQuestion,
],
});
await axios.post(`${API_URL}`, newQuestion);

Alert.alert("Success", "Your question has been submitted!");
setTitle("");
setBody("");
setQuestion("");
setLoading(false);
navigation.goBack();
} catch (error) {
Expand All @@ -77,13 +121,54 @@ const CreateQuestion: React.FC = () => {
onChangeText={setTitle}
style={{ borderWidth: 1, marginVertical: 10, padding: 10 }}
/>

<TextInput
placeholder="Question Body"
value={body}
onChangeText={setBody}
value={question}
onChangeText={setQuestion}
style={{ borderWidth: 1, marginVertical: 10, padding: 10, height: 100 }}
multiline
/>

<Text style={{ fontSize: 18, marginTop: 20 }}>Tags:</Text>

<View
style={{ flexDirection: "row", flexWrap: "wrap", marginVertical: 10 }}
>
{tags.map((tag, index) => (
<TouchableOpacity
key={index}
style={styles.tag}
onPress={() => handleTagRemove(tag)}
>
<Text>{tag.name} x</Text>
</TouchableOpacity>
))}
</View>

<TextInput
placeholder="Type to search for tags..."
value={tagInput}
onChangeText={setTagInput}
style={{ borderWidth: 1, marginVertical: 10, padding: 10 }}
/>

{suggestedTags.length > 0 && (
<FlatList
data={suggestedTags}
keyExtractor={(item) => item.id}
renderItem={({ item }) => (
<TouchableOpacity
onPress={() => handleTagSelect(item)}
style={styles.suggestionItem}
>
<Text>{item.description}</Text>
</TouchableOpacity>
)}
style={styles.suggestionList}
/>
)}

<Button
title={loading ? "Submitting..." : "Submit"}
onPress={handleSubmit}
Expand All @@ -93,4 +178,25 @@ const CreateQuestion: React.FC = () => {
);
};

const styles = StyleSheet.create({
tag: {
backgroundColor: "#ddd",
borderRadius: 10,
padding: 8,
marginRight: 5,
marginBottom: 5,
},
suggestionList: {
maxHeight: 150, // Adjust as needed
borderWidth: 1,
borderColor: "#ccc",
marginVertical: 10,
},
suggestionItem: {
padding: 10,
borderBottomWidth: 1,
borderBottomColor: "#ddd",
},
});

export default CreateQuestion;
Loading

0 comments on commit 36ccf31

Please sign in to comment.