# Project export: MovieMatch

This document was generated by HackStack to give an AI agent context about a hackathon project. Sections are labeled with their provenance; content marked as truncated was cut to keep this document small.

## Project metadata

- Hackathon: TreeHacks 2024
- Tagline: Join your friends, share favorite movies, and watch as our app creates curated movie recommendations based on your group's collective taste!
- Devpost: https://devpost.com/software/moviematch-sa1ud0
- GitHub: https://github.com/CooperRoalson/MovieMatch
- Video: https://www.youtube.com/embed/cKohg6ydVxU?enablejsapi=1&hl=en_US&rel=0&start=&version=3&wmode=transparent
- Team: 4 GitHub contributor(s) — CooperRoalson (23 commits), Shreya Chakraborty (23 commits), Jack (19 commits), bkotharkar (4 commits)

## Devpost submission (written by the team)

### Inspiration

Our team's shared love for movies inspired us to create MovieMatch. We often found it challenging to decide on a movie to watch together, so we wanted to build an app that could help us and others in similar situations.

### What it does

MovieMatch allows users to create groups with friends, curate a list of favorite movies, and receive personalized movie recommendations based on the group's preferences.

### How we built it

We built MovieMatch using React Native for the frontend to ensure cross-platform compatibility. For the backend, MovieMatch is powered by Convex, providing scalability and efficiency. We also integrated third-party APIs like IMDb using Postman for movie data and recommendations. Postman Workspace Link: link

### Challenges we ran into

As it was our first time working with React Native, creating a full-stack, and integrating APIs, we faced a heap of challenges. Managing these learning curves while ensuring the app's functionality and performance was a significant challenge. Using services like Postman and Convex, tech that is built for ease-of-use and rapid development, helped us immensely.

### Accomplishments we're proud of

We're proud of successfully implementing real-time syncing of group preferences and generating accurate movie recommendations. We also managed to create a user-friendly interface that made it easy for users to create groups and add movies to their lists. Getting past that first step of getting the stack to compile and subsequently building upon it was extremely rewarding.

### What we learned

Through this project, we learned a lot about integrating third-party APIs, managing real-time data updates, and optimizing performance in a mobile app. We also improved our teamwork and communication skills, which were crucial for collaborating effectively in a fast-paced hackathon environment.

### What's next

In the future, we plan to add more features to MovieMatch, such as user profiles, ratings, and reviews. We also want to enhance the recommendation algorithm to provide even more personalized suggestions based on individual user preferences. Additionally, we aim to improve the overall user experience by adding more interactive elements and refining the design.

## README (from the GitHub repository)

# MovieMatch: a hackathon project

Helps people settle those movie-night debates a little bit easier.
Swipe through possible choices catered to your interests.

Made for TreeHacks 2024 by:
 - Cooper Roalson
 - Jack Morby
 - Shreya Chakraborty
 - Bhoomi Kotharkar


## Detected evidence (automated analysis)

Indexed codebase: 20 recognized source files, 41 KB.
- JavaScript (language) — detected in the code
- React (technology) — detected in the code
- TypeScript (language) — detected in the code

## Codebase structure (from repository index)

### Files (25 of 25)

```
.gitignore
app.json
App.jsx
babel.config.js
components/button.component.jsx
components/favmovies.component.jsx
components/movierecs.component.jsx
components/movieview.component.jsx
convex/_generated/api.d.ts
convex/_generated/api.js
convex/_generated/dataModel.d.ts
convex/_generated/server.d.ts
convex/_generated/server.js
convex/functions.js
convex/README.md
convex/schema.ts
convex/tsconfig.json
package.json
README.md
screens/feed_screen.jsx
screens/group_screen.jsx
screens/login_screen.jsx
screens/search_screen.jsx
styles.js
tsconfig.json
```

### Dependencies

- package.json: @babel/core@^7.20.0, @expo/metro-runtime@~3.1.3, @react-navigation/bottom-tabs@^6.5.12, @react-navigation/native@^6.1.10, @types/react@~18.2.45, convex@^1.9.0, dotenv@^16.4.4, expo@^50.0.7, expo-status-bar@~1.11.1, react@18.2.0, react-dom@^18.2.0, react-native@0.73.4, react-native-dotenv@^3.4.10, react-native-get-random-values@~1.8.0, react-native-vector-icons@^10.0.3, react-native-web@~0.19.6, typescript@^5.3.0

### Recent commits (newest first)

- Merge branch 'main' of https://github.com/CooperRoalson/MovieMatch
- final design changes
- Made feed have more movies
- Merge branch 'main' of https://github.com/CooperRoalson/MovieMatch
- Merge branch 'main' of https://github.com/CooperRoalson/MovieMatch
- Minor formatting adjustments
- style changes
- Added favorites and some formatting
- Created MovieView component
- random feed update
- Merge branch 'main' of https://github.com/CooperRoalson/MovieMatch
- feed added
- Minor tweaks
- Merge branch 'main' of https://github.com/CooperRoalson/MovieMatch
- Added groups!
- Merge branch 'main' of https://github.com/CooperRoalson/MovieMatch
- changing style
- Merge branch 'main' of https://github.com/CooperRoalson/MovieMatch
- movie info collected and listed on user's click
- Merge branch 'main' of https://github.com/CooperRoalson/MovieMatch

## Key source files (fetched from GitHub, selected and truncated for size)

### package.json

```
{
    "name": "react-native-demo",
    "version": "1.0.0",
    "main": "node_modules/expo/AppEntry.js",
    "scripts": {
        "start": "expo start",
        "android": "expo start --android",
        "ios": "expo start --ios",
        "web": "expo start --web"
    },
    "dependencies": {
        "@expo/metro-runtime": "~3.1.3",
        "@react-navigation/bottom-tabs": "^6.5.12",
        "@react-navigation/native": "^6.1.10",
        "@types/react": "~18.2.45",
        "convex": "^1.9.0",
        "dotenv": "^16.4.4",
        "expo": "^50.0.7",
        "expo-status-bar": "~1.11.1",
        "react": "18.2.0",
        "react-dom": "^18.2.0",
        "react-native": "0.73.4",
        "react-native-dotenv": "^3.4.10",
        "react-native-get-random-values": "~1.8.0",
        "react-native-vector-icons": "^10.0.3",
        "react-native-web": "~0.19.6",
        "typescript": "^5.3.0"
    },
    "devDependencies": {
        "@babel/core": "^7.20.0"
    },
    "private": true
}

```

### App.jsx

```javascript
import { ConvexProvider, ConvexReactClient, useAction, useMutation, useQuery } from "convex/react";
import { CONVEX_URL } from "@env";
import React, { useEffect, useState } from "react";

import { NavigationContainer } from '@react-navigation/native';
import { createBottomTabNavigator } from '@react-navigation/bottom-tabs';

import LoginScreen from './screens/login_screen';
import FeedScreen from './screens/feed_screen';
import SearchScreen from './screens/search_screen';
import GroupsScreen from "./screens/group_screen";
import styles from "./styles"
import { api } from "./convex/_generated/api";
import { Text, SafeAreaView } from "react-native";
import { Ionicons } from '@expo/vector-icons';



const Tab = createBottomTabNavigator();
export const UserContext = React.createContext(null);

function MovieMatchApp() {
  const [username, setUsername] = useState("");

  var userData = useQuery(api.functions.searchUsername, { username: username});
  var createUser = useMutation(api.functions.createUser);

  if (username.length > 0 && userData === null) {
    createUser({username: username});
  }

  return username.length == 0 ? (
    <LoginScreen handleLogin={setUsername} />
  ) : userData === undefined || userData === null ? (
      <SafeAreaView style={styles.background}>
        <Text style={styles.loading}>Loading...</Text>
      </SafeAreaView>
    ) :   (
      <UserContext.Provider value={{ userData: userData }}>
        <NavigationContainer>
          <Tab.Navigator screenOptions={{ headerShown: false }}>
            <Tab.Screen
              name='Feed'
              component={FeedScreen}
              options={{
                    title: 'Feed',
                    tabBarLabel: 'Feed',
                    tabBarIcon: ({ color, size }) => (
                      <Ionicons name="home" color={color} size={size} />
                    ),
                  }}
            />
            <Tab.Screen
              name='Search'
              component={SearchScreen}
              options={{
                    title: 'Search',
                    tabBarLabel: 'Search',
                    tabBarIcon: ({ color, size }) => (
                      <Ionicons name="search" color={color} size={size} />
                    ),
                  }}
            />
            <Tab.Screen
              name='Groups'
              component={GroupsScreen}
              options={{
                    title: 'Groups',
                    tabBarLabel: 'Groups',
                    tabBarIcon: ({ color, size }) => (
                      <Ionicons name="list" color={color} size={size} />
                    ),
                  }}
            />
          </Tab.Navigator>
        </NavigationContainer>
      </UserContext.Provider>
  );
}

export default function App() {
  const convex = new ConvexReactClient(CONVEX_URL, {
    // We need to disable this to be compatible with React Native
    unsavedChangesWarning: false,
  });

  return (
  <ConvexProvider client={convex}>
    <MovieMatchApp />
  </ConvexProvider>
  );
}
```

### convex/_generated/server.js

```javascript
/* eslint-disable */
/**
 * Generated utilities for implementing server-side Convex query and mutation functions.
 *
 * THIS CODE IS AUTOMATICALLY GENERATED.
 *
 * Generated by convex@1.9.0.
 * To regenerate, run `npx convex dev`.
 * @module
 */

import {
  actionGeneric,
  httpActionGeneric,
  queryGeneric,
  mutationGeneric,
  internalActionGeneric,
  internalMutationGeneric,
  internalQueryGeneric,
} from "convex/server";

/**
 * Define a query in this Convex app's public API.
 *
 * This function will be allowed to read your Convex database and will be accessible from the client.
 *
 * @param func - The query function. It receives a {@link QueryCtx} as its first argument.
 * @returns The wrapped query. Include this as an `export` to name it and make it accessible.
 */
export const query = queryGeneric;

/**
 * Define a query that is only accessible from other Convex functions (but not from the client).
 *
 * This function will be allowed to read from your Convex database. It will not be accessible from the client.
 *
 * @param func - The query function. It receives a {@link QueryCtx} as its first argument.
 * @returns The wrapped query. Include this as an `export` to name it and make it accessible.
 */
export const internalQuery = internalQueryGeneric;

/**
 * Define a mutation in this Convex app's public API.
 *
 * This function will be allowed to modify your Convex database and will be accessible from the client.
 *
 * @param func - The mutation function. It receives a {@link MutationCtx} as its first argument.
 * @returns The wrapped mutation. Include this as an `export` to name it and make it accessible.
 */
export const mutation = mutationGeneric;

/**
 * Define a mutation that is only accessible from other Convex functions (but not from the client).
 *
 * This function will be allowed to modify your Convex database. It will not be accessible from the client.
 *
 * @param func - The mutation function. It receives a {@link MutationCtx} as its first argument.
 * @returns The wrapped mutation. Include this as an `export` to name it and make it accessible.
 */
export const internalMutation = internalMutationGeneric;

/**
 * Define an action in this Convex app's public API.
 *
 * An action is a function which can execute any JavaScript code, including non-deterministic
 * code and code with side-effects, like calling third-party services.
 * They can be run in Convex's JavaScript environment or in Node.js using the "use node" directive.
 * They can interact with the database indirectly by calling queries and mutations using the {@link ActionCtx}.
 *
 * @param func - The action. It receives an {@link ActionCtx} as its first argument.
 * @returns The wrapped action. Include this as an `export` to name it and make it accessible.
 */
export const action = actionGeneric;

/**
 * Define an action that is only accessible from other Convex functions (but not from the client).
 *
 * @param func - The function. It receives an {@link ActionCtx} as its first argument.
 * @returns The wrapped function. Include this as an `export` to name it and make it accessible.
 */
export const internalAction = internalActionGeneric;

/**
 * Define a Convex HTTP action.
 *
 * @param func - The function. It receives an {@link ActionCtx} as its first argument, and a `Request` object
 * as its second.
 * @returns The wrapped endpoint function. Route a URL path to this function in `convex/http.js`.
 */
export const httpAction = httpActionGeneric;

```

### babel.config.js

```javascript
module.exports = function (api) {
  api.cache(true);
  return {
    presets: ["babel-preset-expo"],
    plugins: ["module:react-native-dotenv"],
  };
};

```

### styles.js

```javascript
import { StyleSheet } from "react-native";

const styles = StyleSheet.create({
  body: {
    flex: 1,
    justifyContent: "center",
    alignItems: "center",
  },
  background: {
    flex: 1, // Make sure the login screen fills the entire screen
    backgroundColor: "#222222", // Set background to black for the login screen
    justifyContent: "center",
    alignItems: "center",
  },
  titleapp: {
    fontSize: 50,
    letterSpacing: 1,
    color: '#AAA',
  },
  title: {
    backgroundColor: "#ffffff",
    fontSize: 36,
    padding: 5,
    marginTop: 20,
    borderWidth: 5,
    textAlign: "center",
  },
  loading: {
    backgroundColor: "#6e0202",
    fontSize: 28,
    borderWidth: 5,
    borderRadius: 10,
    borderColor: "#454545",
    padding: 5,
  },
  input: {
    height: 40,
    width: 200,
    fontSize: 30,
    borderColor: 'gray',
    borderWidth: 1,
    backgroundColor: 'white',
    marginTop: 10,
    paddingHorizontal: 10,
    borderRadius: 5,
    placeholderTextColor: '#CCC',
  },
  button: {
    backgroundColor: '#f0f0f0',
    borderRadius: 8,
    padding: 10,
    borderRadius: 15,
    margin: 10,
    color: '#777',
    borderWidth: 3,
    borderColor: '#ffffff',
  },
  label: {
    fontSize: 36,
    letterSpacing: 1,
    color: '#AAA',
  },
  movieLabel: {
    fontSize: 36,
    letterSpacing: 1,
    color: '#AAA',
    width: 266,
    flexShrink: 1,
  },
  div: {
    borderRadius: 10,
    borderWidth: 1,
    borderColor: 'gray',
    paddingHorizontal: 10,
    marginBottom: 10,
  },
  container: {
    flex: 1,
    justifyContent: 'center',
    alignItems: 'center',
    padding: 20,
    backgroundColor: "black",
  },
  groupTitle: {
    fontSize: 18,
    fontWeight: 'bold',
    textDecorationLine: 'underline',
    marginTop: 60,
    marginBottom: 6,
  },
  membersContainer: {
    fontSize: 17,
    padding: 20,
    borderRadius: 15,
    margin: 10,
    color: '#777',
    borderWidth: 3,
    borderColor: '#e60000',
    width: 300,
    alignContent: 'center',
    height: 200,

  },
  membersTitle: {
    fontSize: 18,
    fontWeight: 'bold',
    color: '#e60000',
    textAlign: 'center',
  },
  groupInfo: {
    fontSize: 17,
  }
});

export default styles;

```

### components/button.component.jsx

```javascript
import { TouchableOpacity, Text } from 'react-native';

const Button = (props) => {
    return (
      <TouchableOpacity
        onPress={props.onPress}
        style={props.style}>
        <Text>{props.title}</Text>
      </TouchableOpacity>
    );
};

export default Button;
```

### convex/schema.ts

```typescript
import { defineSchema, defineTable } from "convex/server";
import { v } from "convex/values";

export default defineSchema({
  user_data: defineTable({
    username: v.string(),
    favorites: v.array(v.string()),
    groupID: v.optional(v.id("group_data")),
  }),
  group_data: defineTable({
    groupname: v.string(),
    members: v.array(v.id("user_data")),
  }),
});
```

### components/movierecs.component.jsx

```javascript
import { ScrollView, Image, View, TouchableOpacity, Text, Modal, Button } from 'react-native';
import React, { useEffect, useState } from 'react';
import styles from "../styles";

const MovieRecs = ({ searchWord }) => {
    const [movies, setMovies] = useState([]);
    const [selectedMovie, setSelectedMovie] = useState(null);
    const [loading, setLoading] = useState(true);
    const [isModalVisible, setModalVisible] = useState(false);
    const [movieInfo, setMovieInfo] = useState(null);

    const getMovieRequest = async () => {
        setLoading(true);
        const url = "http://www.omdbapi.com/?s=" + searchWord + "&apikey=3d71d1bb";
    
        try {
            const response = await fetch(url);
            const responseJson = await response.json();
    
            console.log(responseJson);
            setMovies(responseJson.Search);
        } catch (error) {
            console.error("Error fetching movies:", error);
        } finally {
        setLoading(false);
        }
    };
    
    useEffect(() => {
        getMovieRequest();
    }, [searchWord]);

    return loading ? (
        <Text>Loading...</Text>
        ) : (
        <ScrollView horizontal style={{ margin: 10 }}>
            {movies !== undefined ? movies.map((movie, index) => (
                <MovieView key={index} movieID={movie.imdbID} />
            )) :
                <Text>No results.</Text>
            }
        </ScrollView>
    );
};

export default MovieRecs;
```

### screens/login_screen.jsx

```javascript
import { View, Text, TextInput, SafeAreaView, Image } from 'react-native';
import { StatusBar } from 'expo-status-bar';
import React, { useContext, useState } from "react";
import styles from "../styles";
import Button from '../components/button.component';
import bgmovie from '../assets/bgmovie.png';

// <bgmovie source={bgmovie} style={styles.bgmovie}>
//   {/* Your login screen content goes here */}
// </bgmovie>
import { UserContext } from '../App';

const LoginScreen = (props) => {
    const [text, setText] = useState('');

    const handleSubmit = async (event) => {
        event.preventDefault();
        let regex = new RegExp(/^[a-zA-Z]\w*$/);
        if (regex.test(text)) {
            props.handleLogin(text);
        } else {
            setText("");
        }
    };

    return (
        <SafeAreaView style={styles.background}>
            <Text style={styles.titleapp}>MovieMatch</Text>
            <Text style={{ fontSize: 14, color: '#e60000' }}>Your Favorite Movie Matchmaker</Text>
            <Text></Text>
            <Text></Text>
            <TextInput
                style={styles.input}
                onChangeText={setText}
                onSubmitEditing={handleSubmit}
                value={text}
                placeholder="Username"
            />
            <Button
              title='Log In'
              onPress={handleSubmit}
              style={styles.button}
            />
            <Text></Text>
            <Text></Text>
            <Image
                source={require('../assets/logo.png')}
                style={[styles.logo, { width: 350, height: 350 }]}
                resizeMode="contain"
            />
            <StatusBar style='auto' />
        </SafeAreaView>
    );
};

export default LoginScreen

    // const [username, setUsername] = useState("");

```

### screens/search_screen.jsx

```javascript
import React, { useState, useEffect, useContext } from "react";
import { Text, SafeAreaView, Button, StyleSheet, TextInput, View } from "react-native";
import styles from "../styles";
import { UserContext } from "../App";
import MovieRecs from "../components/movierecs.component";

const SearchScreen = ({ navigation, route }) => {
    const { userData } = useContext(UserContext);
    const [text, setText] = useState('');
    const [searchval, setSearch] = useState("");
    

    const handleSubmit = async (event) => {
        setSearch(text);
    };

    
    if (searchval == "") {
        return (
            <SafeAreaView style={[styles.body, {backgroundColor: "#222222"}]}>
                <>
                    <Text style={styles.title}>Search for a movie below:</Text>
                    <TextInput
                        style={styles.input}
                        onChangeText={setText}
                        onSubmitEditing={handleSubmit}
                        value={text}
                        placeholder="Movie Title"
                    />
                </>
            </SafeAreaView>
        );
    } else {
        return (
            <SafeAreaView style={[styles.body, {backgroundColor: "#222222"}]}>
                <>
                    <View style={styles.container}>
                        <View style={styles.box}>
                            <TextInput
                                style={styles.input}
                                onChangeText={setText}
                                onSubmitEditing={handleSubmit}
                                value={text}
                                placeholder="Movie Title"
                            />
                        </View>
                        <Text style={styles.title}>Searching for {searchval}</Text>
                    </View>
                    <MovieRecs searchWord={searchval}></MovieRecs>
                </>
            </SafeAreaView>
        );
    }

};

export default SearchScreen;

```

[9 more indexed source files omitted to keep this export small. The full file list is in the Codebase structure section above.]