diff --git a/.github/workflows/build-mobile.yml b/.github/workflows/build-mobile.yml new file mode 100644 index 0000000..862a1e6 --- /dev/null +++ b/.github/workflows/build-mobile.yml @@ -0,0 +1,95 @@ +name: build mobile app + +on: + pull_request: + types: [opened, ready_for_review] + workflow_dispatch: + +permissions: + contents: write + +jobs: + build-android: + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Install Node.js + uses: actions/setup-node@v4 + with: + node-version: 21 + + - uses: pnpm/action-setup@v2 + name: Install pnpm + with: + version: 8 + run_install: false + + - name: Set up JDK 17 + uses: actions/setup-java@v3 + with: + java-version: '17' + distribution: 'temurin' + + - name: Setup Android SDK + uses: android-actions/setup-android@v3 + + - name: Install dependencies + run: pnpm install + + - name: Build Android app + run: cd apps/expo && pnpm run apk + + - name: Rename apk + run: cd apps/expo && mv android/app/build/outputs/apk/release/app-release.apk android/app/build/outputs/apk/release/movie-web.apk + + - name: Upload movie-web.apk as artifact + uses: actions/upload-artifact@v4 + with: + name: apk + path: ./apps/expo/android/app/build/outputs/apk/release/movie-web.apk + + build-ios: + runs-on: macos-14 + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Xcode Select Version + uses: maxim-lobanov/setup-xcode@v1 + with: + xcode-version: '15.1.0' + + - name: Install Node.js + uses: actions/setup-node@v4 + with: + node-version: 21 + + - uses: pnpm/action-setup@v2 + name: Install pnpm + with: + version: 8 + run_install: false + + - name: Install dependencies + run: pnpm install + + - name: Build iOS app + run: cd apps/expo && pnpm run ipa + + - name: Export .ipa from .app + run: | + cd apps/expo + mkdir -p ios/build/Build/Products/Release-iphoneos/Payload + mv ios/build/Build/Products/Release-iphoneos/movieweb.app ios/build/Build/Products/Release-iphoneos/Payload/ + cd ios/build/Build/Products/Release-iphoneos + zip -r ../../../movie-web.ipa Payload + + - name: Upload movie-web.ipa as artifact + uses: actions/upload-artifact@v4 + with: + name: ipa + path: ./apps/expo/ios/build/movie-web.ipa diff --git a/.github/workflows/release-mobile.yml b/.github/workflows/release-mobile.yml new file mode 100644 index 0000000..d3fd8d5 --- /dev/null +++ b/.github/workflows/release-mobile.yml @@ -0,0 +1,143 @@ +name: release mobile app + +on: + push: + branches: + - master + +permissions: + contents: write + +jobs: + bump-version: + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Automated Version Bump + uses: phips28/gh-action-bump-version@v10.1.1 + with: + skip-tag: 'true' + commit-message: 'chore: bump mobile version to {{version}} [skip ci]' + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + build-android: + runs-on: ubuntu-latest + needs: [bump-version] + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Install Node.js + uses: actions/setup-node@v4 + with: + node-version: 21 + + - uses: pnpm/action-setup@v2 + name: Install pnpm + with: + version: 8 + run_install: false + + - name: Set up JDK 17 + uses: actions/setup-java@v3 + with: + java-version: '17' + distribution: 'temurin' + + - name: Setup Android SDK + uses: android-actions/setup-android@v3 + + - name: Install dependencies + run: pnpm install + + - name: Build Android app + run: cd apps/expo && pnpm run apk + + - name: Rename apk + run: cd apps/expo && mv android/app/build/outputs/apk/release/app-release.apk android/app/build/outputs/apk/release/movie-web.apk + + - name: Upload movie-web.apk as artifact + uses: actions/upload-artifact@v4 + with: + name: apk + path: ./apps/expo/android/app/build/outputs/apk/release/movie-web.apk + + build-ios: + runs-on: macos-14 + needs: [bump-version] + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Xcode Select Version + uses: maxim-lobanov/setup-xcode@v1 + with: + xcode-version: '15.1.0' + + - name: Install Node.js + uses: actions/setup-node@v4 + with: + node-version: 21 + + - uses: pnpm/action-setup@v2 + name: Install pnpm + with: + version: 8 + run_install: false + + - name: Install dependencies + run: pnpm install + + - name: Build iOS app + run: cd apps/expo && pnpm run ipa + + - name: Export .ipa from .app + run: | + cd apps/expo + mkdir -p ios/build/Build/Products/Release-iphoneos/Payload + mv ios/build/Build/Products/Release-iphoneos/movieweb.app ios/build/Build/Products/Release-iphoneos/Payload/ + cd ios/build/Build/Products/Release-iphoneos + zip -r ../../../movie-web.ipa Payload + + - name: Upload movie-web.ipa as artifact + uses: actions/upload-artifact@v4 + with: + name: ipa + path: ./apps/expo/ios/build/movie-web.ipa + + release-app: + runs-on: ubuntu-latest + needs: [build-android, build-ios] + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Download build artifacts + uses: actions/download-artifact@v4 + with: + merge-multiple: true + + - name: Get package version + id: package-version + uses: martinbeentjes/npm-get-version-action@v1.3.1 + + - name: Create GitHub Release + uses: softprops/action-gh-release@v1 + with: + tag_name: v${{ steps.package-version.outputs.current-version }} + files: | + movie-web.apk + movie-web.ipa + fail_on_unmatched_files: true + token: ${{ env.GITHUB_TOKEN }} + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.gitignore b/.gitignore index 95c83c2..7774961 100644 --- a/.gitignore +++ b/.gitignore @@ -17,6 +17,8 @@ coverage dist/ expo-env.d.ts apps/expo/.gitignore +ios/ +android/ # production build diff --git a/README.md b/README.md index bf54fc5..d512e09 100644 --- a/README.md +++ b/README.md @@ -65,75 +65,6 @@ To add a new package, simply run `pnpm turbo gen init` in the monorepo root. Thi The generator sets up the `package.json`, `tsconfig.json` and a `index.ts`, as well as configures all the necessary configurations for tooling around your package such as formatting, linting and typechecking. When the package is created, you're ready to go build out the package. -## Deployment - -### Expo - -Deploying your Expo application works slightly differently compared to Next.js on the web. Instead of "deploying" your app online, you need to submit production builds of your app to app stores, like [Apple App Store](https://www.apple.com/app-store) and [Google Play](https://play.google.com/store/apps). You can read the full [guide to distributing your app](https://docs.expo.dev/distribution/introduction), including best practices, in the Expo docs. - -1. Make sure to modify the `getBaseUrl` function to point to your backend's production URL: - - - -2. Let's start by setting up [EAS Build](https://docs.expo.dev/build/introduction), which is short for Expo Application Services. The build service helps you create builds of your app, without requiring a full native development setup. The commands below are a summary of [Creating your first build](https://docs.expo.dev/build/setup). - - ```bash - # Install the EAS CLI - pnpm add -g eas-cli - - # Log in with your Expo account - eas login - - # Configure your Expo app - cd apps/expo - eas build:configure - ``` - -3. After the initial setup, you can create your first build. You can build for Android and iOS platforms and use different [`eas.json` build profiles](https://docs.expo.dev/build-reference/eas-json) to create production builds or development, or test builds. Let's make a production build for iOS. - - ```bash - eas build --platform ios --profile production - ``` - - > If you don't specify the `--profile` flag, EAS uses the `production` profile by default. - -4. Now that you have your first production build, you can submit this to the stores. [EAS Submit](https://docs.expo.dev/submit/introduction) can help you send the build to the stores. - - ```bash - eas submit --platform ios --latest - ``` - - > You can also combine build and submit in a single command, using `eas build ... --auto-submit`. - -5. Before you can get your app in the hands of your users, you'll have to provide additional information to the app stores. This includes screenshots, app information, privacy policies, etc. _While still in preview_, [EAS Metadata](https://docs.expo.dev/eas/metadata) can help you with most of this information. - -6. Once everything is approved, your users can finally enjoy your app. Let's say you spotted a small typo; you'll have to create a new build, submit it to the stores, and wait for approval before you can resolve this issue. In these cases, you can use EAS Update to quickly send a small bugfix to your users without going through this long process. Let's start by setting up EAS Update. - - The steps below summarize the [Getting started with EAS Update](https://docs.expo.dev/eas-update/getting-started/#configure-your-project) guide. - - ```bash - # Add the `expo-updates` library to your Expo app - cd apps/expo - pnpm expo install expo-updates - - # Configure EAS Update - eas update:configure - ``` - -7. Before we can send out updates to your app, you have to create a new build and submit it to the app stores. For every change that includes native APIs, you have to rebuild the app and submit the update to the app stores. See steps 2 and 3. - -8. Now that everything is ready for updates, let's create a new update for `production` builds. With the `--auto` flag, EAS Update uses your current git branch name and commit message for this update. See [How EAS Update works](https://docs.expo.dev/eas-update/how-eas-update-works/#publishing-an-update) for more information. - - ```bash - cd apps/expo - eas update --auto - ``` - - > Your OTA (Over The Air) updates must always follow the app store's rules. You can't change your app's primary functionality without getting app store approval. But this is a fast way to update your app for minor changes and bug fixes. - -9. Done! Now that you have created your production build, submitted it to the stores, and installed EAS Update, you are ready for anything! - ### References + This app is based on [create-t3-turbo](https://github.com/t3-oss/create-t3-turbo) and [Turborepo](https://turborepo.org). - - diff --git a/apps/expo/app.config.ts b/apps/expo/app.config.ts index bd785ab..223d040 100644 --- a/apps/expo/app.config.ts +++ b/apps/expo/app.config.ts @@ -4,7 +4,7 @@ const defineConfig = (): ExpoConfig => ({ name: "movie-web", slug: "mw-mobile", scheme: "dev.movieweb.app", - version: "1.0.0", + version: "0.1.0", orientation: "portrait", icon: "./assets/images/icon.png", userInterfaceStyle: "automatic", diff --git a/apps/expo/package.json b/apps/expo/package.json index edb2fe3..215186c 100644 --- a/apps/expo/package.json +++ b/apps/expo/package.json @@ -6,16 +6,20 @@ "scripts": { "clean": "git clean -xdf .expo .turbo node_modules", "dev": "expo start", - "dev:android": "expo start --android", - "dev:ios": "expo start --ios", + "dev:android": "expo start -c --android", + "dev:ios": "expo start -c --ios", "android": "expo run:android", "ios": "expo run:ios", + "apk": "expo prebuild --platform=android && cd android && ./gradlew assembleRelease", + "ipa": "expo prebuild --platform=ios && cd ios && xcodebuild -workspace movieweb.xcworkspace -scheme movieweb -sdk iphoneos -configuration Release -derivedDataPath build -destination generic/platform=iOS CODE_SIGN_IDENTITY=\"\" CODE_SIGNING_REQUIRED=NO CODE_SIGNING_ALLOWED=NO", + "ipa:sim": "expo prebuild --platform=ios && cd ios && xcodebuild -workspace movieweb.xcworkspace -scheme movieweb -sdk iphonesimulator -configuration Release -derivedDataPath build -destination \"generic/platform=iOS Simulator\" CODE_SIGN_IDENTITY=\"\" CODE_SIGNING_REQUIRED=NO CODE_SIGNING_ALLOWED=NO", "format": "prettier --check . --ignore-path ../../.gitignore", "lint": "eslint .", "typecheck": "tsc --noEmit" }, "dependencies": { "@expo/metro-config": "^0.17.3", + "@movie-web/tmdb": "*", "class-variance-authority": "^0.7.0", "clsx": "^2.1.0", "expo": "~50.0.5", diff --git a/apps/expo/src/app/(tabs)/_layout.tsx b/apps/expo/src/app/(tabs)/_layout.tsx index 8e94be1..48a098f 100644 --- a/apps/expo/src/app/(tabs)/_layout.tsx +++ b/apps/expo/src/app/(tabs)/_layout.tsx @@ -1,3 +1,4 @@ +import { View } from "react-native"; import { Tabs } from "expo-router"; import Colors from "@movie-web/tailwind-config/colors"; @@ -55,11 +56,9 @@ export default function TabLayout() { title: "Search", tabBarLabel: "", tabBarIcon: () => ( - + + + ), }} /> diff --git a/apps/expo/src/app/(tabs)/search/Searchbar.tsx b/apps/expo/src/app/(tabs)/search/Searchbar.tsx index fef0b7a..5de78f4 100644 --- a/apps/expo/src/app/(tabs)/search/Searchbar.tsx +++ b/apps/expo/src/app/(tabs)/search/Searchbar.tsx @@ -5,7 +5,11 @@ import { FontAwesome5 } from "@expo/vector-icons"; import Colors from "@movie-web/tailwind-config/colors"; -export default function Searchbar() { +export default function Searchbar({ + onSearchChange, +}: { + onSearchChange: (text: string) => void; +}) { const [keyword, setKeyword] = useState(""); const inputRef = useRef(null); @@ -22,6 +26,11 @@ export default function Searchbar() { }, []), ); + const handleChange = (text: string) => { + setKeyword(text); + onSearchChange(text); + }; + return ( @@ -29,7 +38,7 @@ export default function Searchbar() { setKeyword(text)} + onChangeText={handleChange} ref={inputRef} placeholder="What are you looking for?" placeholderTextColor={Colors.secondary[200]} diff --git a/apps/expo/src/app/(tabs)/search/_layout.tsx b/apps/expo/src/app/(tabs)/search/_layout.tsx index 8ea2af4..63937ac 100644 --- a/apps/expo/src/app/(tabs)/search/_layout.tsx +++ b/apps/expo/src/app/(tabs)/search/_layout.tsx @@ -1,11 +1,26 @@ +import React, { useState } from "react"; import { ScrollView, View } from "react-native"; +import { getMediaPoster, searchTitle } from "@movie-web/tmdb"; + +import type { ItemData } from "~/components/item/item"; import Item from "~/components/item/item"; import ScreenLayout from "~/components/layout/ScreenLayout"; import { Text } from "~/components/ui/Text"; import Searchbar from "./Searchbar"; export default function SearchScreen() { + const [searchResults, setSearchResults] = useState([]); + + const handleSearchChange = async (query: string) => { + if (query.length > 0) { + const results = await fetchSearchResults(query).catch(() => []); + setSearchResults(results); + } else { + setSearchResults([]); + } + }; + return ( - + - - - - - - - - - + {searchResults.map((item, index) => ( + + + + ))} ); } + +async function fetchSearchResults(query: string): Promise { + const results = await searchTitle(query); + + return results.map((result) => ({ + id: result.id.toString(), + title: result.media_type === "tv" ? result.name : result.title, + posterUrl: getMediaPoster(result.poster_path), + year: new Date( + result.media_type === "tv" ? result.first_air_date : result.release_date, + ).getFullYear(), + type: result.media_type, + })); +} diff --git a/apps/expo/src/app/_layout.tsx b/apps/expo/src/app/_layout.tsx index 3238d56..623f4b3 100644 --- a/apps/expo/src/app/_layout.tsx +++ b/apps/expo/src/app/_layout.tsx @@ -21,7 +21,7 @@ export { export const unstable_settings = { // Ensure that reloading on `/modal` keeps a back button present. - initialRouteName: "(tabs)/index", + initialRouteName: "(tabs)", }; // Prevent the splash screen from auto-hiding before asset loading is complete. diff --git a/apps/expo/src/app/components/item/item.tsx b/apps/expo/src/app/components/item/item.tsx index e175f7b..f146621 100644 --- a/apps/expo/src/app/components/item/item.tsx +++ b/apps/expo/src/app/components/item/item.tsx @@ -1,25 +1,35 @@ import { Image, View } from "react-native"; -import { TMDB_POSTER_PATH } from "~/app/constants/General"; import { Text } from "~/components/ui/Text"; -export default function Item() { +export interface ItemData { + id: string; + title: string; + type: "movie" | "tv"; + year: number; + posterUrl: string; +} + +export default function Item({ data }: { data: ItemData }) { + const { title, type, year, posterUrl } = data; + return ( - Hamilton + {title} - Movie + + {type === "tv" ? "Show" : "Movie"} + - 2023 + {year} ); diff --git a/apps/expo/src/app/constants/.gitkeep b/apps/expo/src/app/constants/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/apps/expo/src/app/constants/General.ts b/apps/expo/src/app/constants/General.ts deleted file mode 100644 index e12b028..0000000 --- a/apps/expo/src/app/constants/General.ts +++ /dev/null @@ -1 +0,0 @@ -export const TMDB_POSTER_PATH = `https://image.tmdb.org/t/p`; diff --git a/package.json b/package.json index 25ec2ba..412b339 100644 --- a/package.json +++ b/package.json @@ -1,5 +1,5 @@ { - "name": "create-t3-turbo", + "name": "@movie-web/native", "private": true, "engines": { "node": ">=20.11.0" diff --git a/packages/tmdb/package.json b/packages/tmdb/package.json new file mode 100644 index 0000000..a768004 --- /dev/null +++ b/packages/tmdb/package.json @@ -0,0 +1,34 @@ +{ + "name": "@movie-web/tmdb", + "private": true, + "version": "0.1.0", + "type": "module", + "main": "./src/index.ts", + "exports": { + ".": "./src/index.ts" + }, + "license": "MIT", + "scripts": { + "clean": "rm -rf .turbo node_modules", + "format": "prettier --check . --ignore-path ../../.gitignore", + "lint": "eslint .", + "typecheck": "tsc --noEmit" + }, + "devDependencies": { + "@movie-web/eslint-config": "workspace:^0.2.0", + "@movie-web/prettier-config": "workspace:^0.1.0", + "@movie-web/tsconfig": "workspace:^0.1.0", + "eslint": "^8.56.0", + "prettier": "^3.1.1", + "typescript": "^5.3.3" + }, + "eslintConfig": { + "extends": [ + "@movie-web/eslint-config/base" + ] + }, + "prettier": "@movie-web/prettier-config", + "dependencies": { + "tmdb-ts": "^1.6.1" + } +} diff --git a/packages/tmdb/src/details.ts b/packages/tmdb/src/details.ts new file mode 100644 index 0000000..d91b048 --- /dev/null +++ b/packages/tmdb/src/details.ts @@ -0,0 +1,24 @@ +import type { MovieDetails, TvShowDetails } from "tmdb-ts"; + +import { tmdb } from "./util"; + +export async function fetchMediaDetails( + id: string, + type: "movie" | "tv", +): Promise< + { type: "movie" | "tv"; result: TvShowDetails | MovieDetails } | undefined +> { + try { + const result = + type === "movie" + ? await tmdb.movies.details(parseInt(id, 10)) + : await tmdb.tvShows.details(parseInt(id, 10)); + + return { + type, + result, + }; + } catch (ex) { + return undefined; + } +} diff --git a/packages/tmdb/src/index.ts b/packages/tmdb/src/index.ts new file mode 100644 index 0000000..fec6c9f --- /dev/null +++ b/packages/tmdb/src/index.ts @@ -0,0 +1,4 @@ +export const name = "tmdb"; +export * from "./search"; +export * from "./details"; +export * from "./util"; diff --git a/packages/tmdb/src/search.ts b/packages/tmdb/src/search.ts new file mode 100644 index 0000000..3f99ee7 --- /dev/null +++ b/packages/tmdb/src/search.ts @@ -0,0 +1,22 @@ +import type { MovieWithMediaType, TVWithMediaType } from "tmdb-ts"; + +import { tmdb } from "./util"; + +export async function searchTitle(query: string) { + try { + const rawResults = await tmdb.search.multi({ + query, + page: 1, + include_adult: false, + }); + const results = rawResults.results.filter( + (result) => result.media_type === "tv" || result.media_type === "movie", + ); + + if (!results.length) throw new Error("No results found"); + + return results as unknown as MovieWithMediaType[] | TVWithMediaType[]; + } catch (ex) { + throw new Error(`Error searching for title: ${(ex as Error).message}`); + } +} diff --git a/packages/tmdb/src/util.ts b/packages/tmdb/src/util.ts new file mode 100644 index 0000000..9558c2f --- /dev/null +++ b/packages/tmdb/src/util.ts @@ -0,0 +1,9 @@ +import { TMDB } from "tmdb-ts"; + +const TMDB_API_KEY = + "eyJhbGciOiJIUzI1NiJ9.eyJhdWQiOiJkYTM1ZTgyMzE4OTc0NTgxNDJmZjljZTE4ODExNWRlNiIsInN1YiI6IjY0OTM0ZDQ1ODliNTYxMDExYzliZDVhMiIsInNjb3BlcyI6WyJhcGlfcmVhZCJdLCJ2ZXJzaW9uIjoxfQ.AzWnIcxPNgDwGdzeIZ_C3mRC_5_qy-Z-SRPglLjzlNc"; +export const tmdb = new TMDB(TMDB_API_KEY); + +export function getMediaPoster(posterPath: string): string { + return `https://image.tmdb.org/t/p/w185/${posterPath}`; +} diff --git a/packages/tmdb/tsconfig.json b/packages/tmdb/tsconfig.json new file mode 100644 index 0000000..12305a4 --- /dev/null +++ b/packages/tmdb/tsconfig.json @@ -0,0 +1,8 @@ +{ + "extends": "@movie-web/tsconfig/base.json", + "compilerOptions": { + "tsBuildInfoFile": "node_modules/.cache/tsbuildinfo.json", + }, + "include": ["*.ts", "src"], + "exclude": ["node_modules"], +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index bc74054..c0b255e 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -34,6 +34,9 @@ importers: '@expo/metro-config': specifier: ^0.17.3 version: 0.17.3(@react-native/babel-preset@0.73.20) + '@movie-web/tmdb': + specifier: '*' + version: link:../../packages/tmdb class-variance-authority: specifier: ^0.7.0 version: 0.7.0 @@ -135,6 +138,31 @@ importers: specifier: ^5.3.3 version: 5.3.3 + packages/tmdb: + dependencies: + tmdb-ts: + specifier: ^1.6.1 + version: 1.6.1 + devDependencies: + '@movie-web/eslint-config': + specifier: workspace:^0.2.0 + version: link:../../tooling/eslint + '@movie-web/prettier-config': + specifier: workspace:^0.1.0 + version: link:../../tooling/prettier + '@movie-web/tsconfig': + specifier: workspace:^0.1.0 + version: link:../../tooling/typescript + eslint: + specifier: ^8.56.0 + version: 8.56.0 + prettier: + specifier: ^3.1.1 + version: 3.2.4 + typescript: + specifier: ^5.3.3 + version: 5.3.3 + tooling/eslint: dependencies: '@typescript-eslint/eslint-plugin': @@ -9537,6 +9565,14 @@ packages: upper-case: 1.1.3 dev: true + /tmdb-ts@1.6.1: + resolution: {integrity: sha512-TJQYQctzky03z8bhlJtZ9ZjFHNvLhpow3qKHMMZj2LEOvlqcJ/Dyy33IyuBROrhzWLelkmGraAA718B0ENP1Fg==} + dependencies: + cross-fetch: 3.1.8 + transitivePeerDependencies: + - encoding + dev: false + /tmp@0.0.33: resolution: {integrity: sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==} engines: {node: '>=0.6.0'}