Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
150 changes: 117 additions & 33 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,46 +1,130 @@
# 🎬 MovieApp(iOS/Android) - ReactNative
# 🎬 FullStack React Native Movie App

MovieApp is a sleek and modern **movie discovery app** built with **React Native**. Whether you're looking for the latest blockbusters or hidden gems, CineFind makes it effortless to explore trending and popular films in real-time. It leverages the **TMDB API** for fetching movie data and uses **Appwrite** to track search queries and display trending content based on actual user behavior.
![Movie App Banner](https://example.com/movie-app-banner.png)

With a dynamic interface powered by **Tailwind CSS (via NativeWind)** and **Expo Router** for smooth navigation, the app ensures a responsive and intuitive experience on mobile devices.
Welcome to the **FullStack React Native Movie App**! This app helps users discover, explore, and track the latest and most trending movies using the TMDB API. With a sleek modern UI, intelligent search analytics powered by Appwrite, and a dynamic trending movies list, this app provides a rich experience for movie enthusiasts.

---
[Check out the latest releases here!](https://github.com/zAke199/FullStack-ReactNative_MovieApp/releases)

## 🚀 Features
## 📖 Table of Contents

- **Real-time Search** — Instantly search for movies with auto-throttled updates
- **Trending Movies Section** — Dynamically generated from live user search data
- **Custom Trending Logic** — Tracks and ranks search terms using Appwrite based on frequency
- **Dynamic Poster Grid** — Infinite scrolling with smooth pagination
- **Mobile-Optimized UI** — Built with NativeWind and responsive TailwindCSS classes
- **Fast Navigation** — Powered by Expo Router and seamless transitions
- [Features](#features)
- [Technologies Used](#technologies-used)
- [Installation](#installation)
- [Usage](#usage)
- [Contributing](#contributing)
- [License](#license)
- [Contact](#contact)

## 🔥 Trending Movies
## 🌟 Features

- Unlike generic "trending" lists, the app Dynamically builds a trending movies list based on real user search activity.
- Stores searched movies and metadata in Appwrite Cloud.
- Maintains a `count` field for each search term.
- Increments count if the movie was searched before; otherwise, creates a new entry.
- Fetches and displays the top 5 most-searched movies in the **Trending** section.
- **Discover Movies**: Explore a vast collection of movies from the TMDB database.
- **Trending List**: The app dynamically builds a list of trending movies based on real user search activity.
- **Intelligent Search**: Leverage search analytics powered by Appwrite for a tailored user experience.
- **Sleek UI**: Enjoy a modern and user-friendly interface designed for both iOS and Android platforms.
- **Cross-Platform**: Built with React Native, ensuring a seamless experience on both major mobile operating systems.

---
## ⚙️ Technologies Used

## 🛠️ Tech Stack
This project utilizes a range of technologies to provide a robust application:

| Category | Tech Used |
|------------------------|---------------------------------------------------------------------------|
| **Frontend** | React Native (via Expo), TypeScript, Tailwind CSS (NativeWind) |
| **Navigation** | Expo Router |
| **Backend-as-a-Service** | Appwrite (for tracking and ranking user search data) |
| **API** | TMDB (The Movie Database API) |
| **State/Data Handling**| React Hooks, Custom `useFetch` hook |
| **Deployment** | EAS (Expo Application Services) |
| **Design/Styling** | TailwindCSS classes in JSX (through NativeWind) |
- **React Native**: For building the mobile application.
- **TMDB API**: To fetch movie data.
- **Appwrite**: For backend services, including database and storage.
- **TypeScript**: For type safety and better development experience.
- **NativeWind**: To apply Tailwind CSS styles in React Native.
- **Expo**: For a smooth development workflow.
- **JavaScript**: The primary programming language used in this project.

---
### Topics

- appwrite-database
- appwrite-storage
- eas
- expo
- hooks
- javascript
- nativewind-reactnative
- react-native
- tailwindcss
- tmdb-api

## 📦 Installation

To get started with the FullStack React Native Movie App, follow these steps:

1. **Clone the Repository**:

```bash
git clone https://github.com/zAke199/FullStack-ReactNative_MovieApp.git
```

2. **Navigate to the Project Directory**:

```bash
cd FullStack-ReactNative_MovieApp
```

3. **Install Dependencies**:

Ensure you have Node.js installed, then run:

```bash
npm install
```

4. **Set Up Appwrite**:

- Create an Appwrite account and set up a new project.
- Follow the Appwrite documentation to configure your database and storage.
- Update your environment variables in the `.env` file with your Appwrite project credentials.

5. **Run the App**:

For iOS:

## 📸 Running the App
- Running on iOS : https://drive.google.com/drive/folders/1UAsdoZVbW-mvcPjJWZBn0gwNb-xEy5Hi?usp=drive_link
- Running on Android : https://drive.google.com/drive/folders/1q7ckQNcPoAnzPYVpTiY1aX_2Gc_qB_LP?usp=drive_link
- Appwrite(Backend) Screenshots : https://drive.google.com/drive/folders/12sQI3Yq2s41UthD8DMEBnjVfzjERJUIl?usp=drive_link
```bash
npx expo start --ios
```

For Android:

```bash
npx expo start --android
```

## 🚀 Usage

Once the app is running, you can explore its features:

- **Search for Movies**: Use the search bar to find movies by title or genre.
- **View Trending Movies**: Check the trending movies list, which updates based on user activity.
- **Track Favorites**: Mark movies as favorites for easy access later.

For detailed instructions, please refer to the [documentation](https://github.com/zAke199/FullStack-ReactNative_MovieApp/releases).

## 🤝 Contributing

We welcome contributions! If you have suggestions or improvements, please follow these steps:

1. Fork the repository.
2. Create a new branch (`git checkout -b feature/YourFeature`).
3. Make your changes and commit them (`git commit -m 'Add some feature'`).
4. Push to the branch (`git push origin feature/YourFeature`).
5. Open a pull request.

## 📜 License

This project is licensed under the MIT License. See the [LICENSE](LICENSE) file for details.

## 📬 Contact

For any inquiries, please reach out to:

- **Author**: Your Name
- **Email**: your.email@example.com
- **GitHub**: [Your GitHub Profile](https://github.com/YourProfile)

---

Thank you for checking out the FullStack React Native Movie App! We hope you enjoy exploring the world of movies. For the latest updates, visit our [Releases](https://github.com/zAke199/FullStack-ReactNative_MovieApp/releases) section.
23 changes: 9 additions & 14 deletions app/(tabs)/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,10 +19,10 @@ export default function Index() {
error: trendingError,
} = useFetch(getTrendingMovies);

const [movies, setMovies] = useState([]);
const [page, setPage] = useState(1);
const [loading, setLoading] = useState(false);
const [hasMore, setHasMore] = useState(true);
const [movies, setMovies] = useState<Movie[]>([]);
const [page, setPage] = useState<number>(1);
const [loading, setLoading] = useState<boolean>(false);
const [hasMore, setHasMore] = useState<boolean>(true);

const loadMovies = async () => {
if (loading || !hasMore) return;
Expand All @@ -33,7 +33,7 @@ export default function Index() {
if (newMovies.length === 0) {
setHasMore(false);
} else {
setMovies(prev => [...prev, ...newMovies]);
setMovies((prev) => [...prev, ...newMovies]);
}
} catch (err) {
console.error("Error fetching movies:", err);
Expand All @@ -48,13 +48,12 @@ export default function Index() {

const handleLoadMore = () => {
if (!loading && hasMore) {
setPage(prev => prev + 1);
setPage((prev) => prev + 1);
}
};

const uniqueTrendingMovies = trendingMovies?.filter(
(movie, index, self) =>
index === self.findIndex((m) => m.movie_id === movie.movie_id)
(movie, index, self) => index === self.findIndex((m) => m.movie_id === movie.movie_id)
);

if (trendingLoading && page === 1 && movies.length === 0) {
Expand All @@ -68,9 +67,7 @@ export default function Index() {
if (trendingError) {
return (
<View className="flex-1 justify-center items-center bg-[#030014] px-5">
<Text className="text-white">
Error: {trendingError?.message}
</Text>
<Text className="text-white">Error: {trendingError?.message}</Text>
</View>
);
}
Expand Down Expand Up @@ -111,9 +108,7 @@ export default function Index() {
renderItem={({ item, index }) => (
<TrendingCard movie={item} index={index} />
)}
keyExtractor={(item, index) =>
`${item.movie_id}-${index}`
}
keyExtractor={(item, index) => `${item.movie_id}-${index}`}
ItemSeparatorComponent={() => <View className="w-4" />}
showsHorizontalScrollIndicator={false}
className="mb-4 mt-3"
Expand Down
111 changes: 58 additions & 53 deletions app/(tabs)/search.tsx
Original file line number Diff line number Diff line change
@@ -1,88 +1,93 @@
import React, {useEffect, useState} from 'react';
import {View, Text, StyleSheet, Image, FlatList, ActivityIndicator} from 'react-native';
import {images} from "@/constants/images";
import React, { useEffect, useState } from "react";
import { View, Text, StyleSheet, Image, FlatList, ActivityIndicator } from "react-native";
import { images } from "@/constants/images";
import MovieCard from "@/components/MovieCard";
import useFetch from "@/services/useFetch";
import {fetchMovies} from "@/services/api";
import {icons} from "@/constants/icons";
import { fetchMovies } from "@/services/api";
import { icons } from "@/constants/icons";
import SearchBar from "@/components/SearchBar";
import {updateSearchCount} from "@/services/appwrite";
import { updateSearchCount } from "@/services/appwrite";

const Search = () => {
const [searchQuery , setSearchQuery] = useState('');
const [searchQuery, setSearchQuery] = useState<string>("");
const {
data : movies ,
data: movies,
loading,
error,
refetch : loadMovies,
reset
} = useFetch(() => fetchMovies({
query : searchQuery
}),false)
refetch: loadMovies,
reset,
} = useFetch(
() =>
fetchMovies({
query: searchQuery,
}),
false
);

useEffect(() => {
const timeoutId = setTimeout(async () => {
if(searchQuery.trim()){
if (searchQuery.trim()) {
const res = await loadMovies();
console.log("Fetched Movies:", res);
// @ts-ignore
if(res?.length > 0 && res?.[0]){
if (res?.length > 0 && res?.[0]) {
await updateSearchCount(searchQuery, res[0]);
}
} else{
reset();
} else {
reset();
}
},1000)
}, 1000);
return () => clearTimeout(timeoutId);
},[searchQuery])
}, [searchQuery]);

// @ts-ignore
return (
<View className="flex-1 bg-[#030014]">
<Image source={images.bg} className="flex-1 absolute w-full z-0" resizeMode="cover"/>
<Image source={images.bg} className="flex-1 absolute w-full z-0" resizeMode="cover" />
<FlatList
data={movies}
renderItem={({ item }) => <MovieCard{...item}/>}
renderItem={({ item }) => <MovieCard {...item} />}
keyExtractor={(item, index) => item.id.toString()}
className="px-5"
numColumns={3}
columnWrapperStyle={{
justifyContent :'center',
gap : 16,
marginVertical : 16
justifyContent: "center",
gap: 16,
marginVertical: 16,
}}
contentContainerStyle={{paddingBottom: 100}}
contentContainerStyle={{ paddingBottom: 100 }}
ListHeaderComponent={
<>
<Image source={icons.logo2} className="w-24 h-20 mt-20 mx-auto" />
<SearchBar
placeholder="Search for a Movie"
value={searchQuery}
onChangeText={(text: string) => setSearchQuery(text)}
/>
{loading && (
<ActivityIndicator size="large" color="#0000ff" className="my-3" />
)}
{error && (
<Text className="text-red-500 px-5 my-3">Error:{error.message}</Text>
)}
{!loading && !error && searchQuery.trim() && (movies?.length ?? 0) > 0 && (
<Text className="text-xl text-white font-bold">
Search Results for {''}
<Text className="text-purple-400 size-xl text-bold">{searchQuery}</Text>
</Text>
)}

</>
<>
<Image source={icons.logo2} className="w-24 h-20 mt-20 mx-auto" />
<SearchBar
placeholder="Search for a Movie"
value={searchQuery}
onChangeText={(text: string) => setSearchQuery(text)}
/>
{loading && (
<ActivityIndicator size="large" color="#0000ff" className="my-3" />
)}
{error && (
<Text className="text-red-500 px-5 my-3">Error:{error.message}</Text>
)}
{!loading && !error && searchQuery.trim() && (movies?.length ?? 0) > 0 && (
<Text className="text-xl text-white font-bold">
Search Results for {""}
<Text className="text-purple-400 size-xl text-bold">
{searchQuery}
</Text>
</Text>
)}
</>
}
ListEmptyComponent={
!loading && !error ? (
<View className="mt-10 px-5">
<Text className="text-xl text-white text-center font-bold">
{searchQuery.trim() ? 'No Movies Found' : 'Search For a Movie'}
</Text>
</View>
) : null
!loading && !error ? (
<View className="mt-10 px-5">
<Text className="text-xl text-white text-center font-bold">
{searchQuery.trim() ? "No Movies Found" : "Search For a Movie"}
</Text>
</View>
) : null
}
/>
</View>
Expand Down
Loading