refactor to turbo

This commit is contained in:
Jorrin
2024-02-03 20:33:27 +01:00
parent fc5c60f85b
commit 28467cdf24
113 changed files with 3744 additions and 8508 deletions

View File

@@ -0,0 +1,4 @@
{
"12bb71342c6255bbf50437ec8f4441c083f47cdb74bd89160c15e4f43e52a1cb": true,
"40b842e832070c58deac6aa9e08fa459302ee3f9da492c7e77d93d2fbf4a56fd": true
}

47
apps/expo/app.config.ts Normal file
View File

@@ -0,0 +1,47 @@
import type { ExpoConfig } from "expo/config";
const defineConfig = (): ExpoConfig => ({
name: "movie-web",
slug: "mw-mobile",
scheme: "dev.movieweb.app",
version: "1.0.0",
orientation: "portrait",
icon: "./assets/icon.png",
userInterfaceStyle: "automatic",
splash: {
image: "./assets/images/splash.png",
resizeMode: "contain",
backgroundColor: "#ffffff",
},
updates: {
fallbackToCacheTimeout: 0,
},
assetBundlePatterns: ["**/*"],
ios: {
bundleIdentifier: "dev.movieweb.app",
supportsTablet: true,
},
android: {
package: "dev.movieweb.app",
adaptiveIcon: {
foregroundImage: "./assets/images/adaptive-icon.png",
backgroundColor: "#FFFFFF",
},
},
web: {
favicon: "./assets/images/favicon.png",
bundler: "metro",
},
// extra: {
// eas: {
// projectId: "your-eas-project-id",
// },
// },
experiments: {
tsconfigPaths: true,
typedRoutes: true,
},
plugins: ["expo-router"],
});
export default defineConfig;

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 46 KiB

11
apps/expo/babel.config.js Normal file
View File

@@ -0,0 +1,11 @@
/** @type {import("@babel/core").ConfigFunction} */
module.exports = function (api) {
api.cache(true);
return {
presets: [
["babel-preset-expo", { jsxImportSource: "nativewind" }],
"nativewind/babel",
],
plugins: ["react-native-reanimated/plugin"],
};
};

31
apps/expo/eas.json Normal file
View File

@@ -0,0 +1,31 @@
{
"cli": {
"version": ">= 4.1.2"
},
"build": {
"base": {
"node": "18.16.1",
"ios": {
"resourceClass": "m-medium"
}
},
"development": {
"extends": "base",
"developmentClient": true,
"distribution": "internal"
},
"preview": {
"extends": "base",
"distribution": "internal",
"ios": {
"simulator": true
}
},
"production": {
"extends": "base"
}
},
"submit": {
"production": {}
}
}

60
apps/expo/metro.config.js Normal file
View File

@@ -0,0 +1,60 @@
// Learn more: https://docs.expo.dev/guides/monorepos/
const { getDefaultConfig } = require("expo/metro-config");
const { FileStore } = require("metro-cache");
const { withNativeWind } = require("nativewind/metro");
const path = require("path");
module.exports = withTurborepoManagedCache(
withMonorepoPaths(
withNativeWind(
getDefaultConfig(__dirname, {
isCSSEnabled: true,
}),
{
input: "./src/app/styles/global.css",
configPath: "./tailwind.config.ts",
},
),
),
);
/**
* Add the monorepo paths to the Metro config.
* This allows Metro to resolve modules from the monorepo.
*
* @see https://docs.expo.dev/guides/monorepos/#modify-the-metro-config
* @param {import('expo/metro-config').MetroConfig} config
* @returns {import('expo/metro-config').MetroConfig}
*/
function withMonorepoPaths(config) {
const projectRoot = __dirname;
const workspaceRoot = path.resolve(projectRoot, "../..");
// #1 - Watch all files in the monorepo
config.watchFolders = [workspaceRoot];
// #2 - Resolve modules within the project's `node_modules` first, then all monorepo modules
config.resolver.nodeModulesPaths = [
path.resolve(projectRoot, "node_modules"),
path.resolve(workspaceRoot, "node_modules"),
];
return config;
}
/**
* Move the Metro cache to the `node_modules/.cache/metro` folder.
* This repository configured Turborepo to use this cache location as well.
* If you have any environment variables, you can configure Turborepo to invalidate it when needed.
*
* @see https://turbo.build/repo/docs/reference/configuration#env
* @param {import('expo/metro-config').MetroConfig} config
* @returns {import('expo/metro-config').MetroConfig}
*/
function withTurborepoManagedCache(config) {
config.cacheStores = [
new FileStore({ root: path.join(__dirname, "node_modules/.cache/metro") }),
];
return config;
}

66
apps/expo/package.json Normal file
View File

@@ -0,0 +1,66 @@
{
"name": "@movie-web/mobile",
"version": "0.1.0",
"private": true,
"main": "expo-router/entry",
"scripts": {
"clean": "git clean -xdf .expo .turbo node_modules",
"dev": "expo start -c --web",
"dev:android": "expo start --android",
"dev:ios": "expo start --ios",
"android": "expo run:android",
"ios": "expo run:ios",
"format": "prettier --check . --ignore-path ../../.gitignore",
"lint": "eslint .",
"typecheck": "tsc --noEmit"
},
"dependencies": {
"@expo/metro-config": "^0.17.3",
"class-variance-authority": "^0.7.0",
"clsx": "^2.1.0",
"expo": "~50.0.5",
"expo-constants": "~15.4.5",
"expo-linking": "~6.2.2",
"expo-router": "~3.4.6",
"expo-splash-screen": "~0.26.4",
"expo-status-bar": "~1.11.1",
"expo-web-browser": "^12.8.2",
"nativewind": "~4.0.23",
"react": "18.2.0",
"react-dom": "18.2.0",
"react-native": "0.73.2",
"react-native-css-interop": "~0.0.13",
"react-native-gesture-handler": "~2.14.0",
"react-native-reanimated": "~3.6.2",
"react-native-safe-area-context": "~4.8.2",
"react-native-screens": "~3.29.0",
"react-native-web": "^0.19.10",
"tailwind-merge": "^2.2.1"
},
"devDependencies": {
"@babel/core": "^7.23.9",
"@babel/preset-env": "^7.23.9",
"@babel/runtime": "^7.23.9",
"@movie-web/eslint-config": "workspace:^0.2.0",
"@movie-web/prettier-config": "workspace:^0.1.0",
"@movie-web/tailwind-config": "workspace:^0.1.0",
"@movie-web/tsconfig": "workspace:^0.1.0",
"@types/babel__core": "^7.20.5",
"@types/react": "^18.2.48",
"eslint": "^8.56.0",
"prettier": "^3.1.1",
"tailwindcss": "^3.4.0",
"typescript": "^5.3.3"
},
"eslintConfig": {
"root": true,
"extends": [
"@movie-web/eslint-config/base",
"@movie-web/eslint-config/react"
],
"ignorePatterns": [
"expo-plugins/**"
]
},
"prettier": "@movie-web/prettier-config"
}

View File

@@ -0,0 +1,86 @@
import { Tabs } from "expo-router";
import Colors from "@movie-web/tailwind-config/colors";
import TabBarIcon from "~/components/TabBarIcon";
export default function TabLayout() {
return (
<Tabs
sceneContainerStyle={{
backgroundColor: Colors.background,
}}
screenOptions={{
headerShown: false,
tabBarActiveTintColor: Colors.primary[100],
tabBarStyle: {
backgroundColor: Colors.secondary[700],
borderTopColor: "transparent",
borderTopRightRadius: 20,
borderTopLeftRadius: 20,
height: 80,
},
tabBarItemStyle: {
paddingVertical: 18,
height: 82,
},
tabBarLabelStyle: [
{
marginTop: 2,
},
],
}}
>
<Tabs.Screen
name="index"
options={{
title: "Home",
tabBarIcon: ({ focused }) => (
<TabBarIcon name="home" focused={focused} />
),
}}
/>
<Tabs.Screen
name="about"
options={{
title: "About",
tabBarIcon: ({ focused }) => (
<TabBarIcon name="info-circle" focused={focused} />
),
}}
/>
<Tabs.Screen
name="search"
options={{
title: "Search",
tabBarLabel: "",
tabBarIcon: () => (
<TabBarIcon
className=" bg-primary-400 flex aspect-[1/1] h-14 items-center justify-center rounded-full text-center text-2xl text-white"
name="search"
color="#FFF"
/>
),
}}
/>
<Tabs.Screen
name="settings"
options={{
title: "Settings",
tabBarIcon: ({ focused }) => (
<TabBarIcon name="cog" focused={focused} />
),
}}
/>
<Tabs.Screen
name="account"
options={{
title: "Account",
tabBarIcon: ({ focused }) => (
<TabBarIcon name="user" focused={focused} />
),
}}
/>
</Tabs>
);
}

View File

@@ -0,0 +1,16 @@
import ScreenLayout from "~/components/layout/ScreenLayout";
import { Text } from "~/components/ui/Text";
export default function AboutScreen() {
return (
<ScreenLayout
title="About"
subtitle="What is movie-web and how content is served?"
>
<Text>
No content is served from movie-web directly and movie web does not host
anything.
</Text>
</ScreenLayout>
);
}

View File

@@ -0,0 +1,13 @@
import ScreenLayout from "~/components/layout/ScreenLayout";
import { Text } from "~/components/ui/Text";
export default function AccountScreen() {
return (
<ScreenLayout
title="Account"
subtitle="Manage your movie web account from here"
>
<Text>Hey Bro! what are you up to?</Text>
</ScreenLayout>
);
}

View File

@@ -0,0 +1,10 @@
import ScreenLayout from "~/components/layout/ScreenLayout";
import { Text } from "~/components/ui/Text";
export default function HomeScreen() {
return (
<ScreenLayout title="Home" subtitle="This is where all magic happens">
<Text>Movies will be listed here</Text>
</ScreenLayout>
);
}

View File

@@ -0,0 +1,41 @@
import { useCallback, useRef, useState } from "react";
import { View } from "react-native";
import { TextInput } from "react-native-gesture-handler";
import { useFocusEffect } from "expo-router";
import { FontAwesome5 } from "@expo/vector-icons";
import Colors from "@movie-web/tailwind-config/colors";
export default function Searchbar() {
const [keyword, setKeyword] = useState("");
const inputRef = useRef<TextInput>(null);
useFocusEffect(
useCallback(() => {
// When the screen is focused
const focus = () => {
setTimeout(() => {
inputRef?.current?.focus();
}, 20);
};
focus();
return focus; // cleanup
}, []),
);
return (
<View className="border-primary-400 focus-within:border-primary-300 mb-6 mt-4 flex-row items-center rounded-full border">
<View className="ml-1 w-12 items-center justify-center">
<FontAwesome5 name="search" size={18} color={Colors.secondary[200]} />
</View>
<TextInput
value={keyword}
onChangeText={(text) => setKeyword(text)}
ref={inputRef}
placeholder="What are you looking for?"
placeholderTextColor={Colors.secondary[200]}
className="w-full rounded-3xl py-3 pr-5 text-white focus-visible:outline-none"
/>
</View>
);
}

View File

@@ -0,0 +1,34 @@
import { ScrollView, View } from "react-native";
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() {
return (
<ScrollView>
<ScreenLayout
title={
<View className="flex-row items-center">
<Text className="text-2xl font-bold">Search</Text>
</View>
}
subtitle="Looking for something?"
>
<Searchbar />
<View className="flex w-full flex-1 flex-row flex-wrap justify-start">
<View className="basis-1/2 px-3 pb-3">
<Item />
</View>
<View className="basis-1/2 px-3 pb-3">
<Item />
</View>
<View className="basis-1/2 px-3 pb-3">
<Item />
</View>
</View>
</ScreenLayout>
</ScrollView>
);
}

View File

@@ -0,0 +1,10 @@
import ScreenLayout from "~/components/layout/ScreenLayout";
import { Text } from "~/components/ui/Text";
export default function SettingsScreen() {
return (
<ScreenLayout title="Settings" subtitle="Need to change something?">
<Text>Settings would be listed in here. Coming soon</Text>
</ScreenLayout>
);
}

View File

@@ -0,0 +1,46 @@
import { ScrollViewStyleReset } from "expo-router/html";
// This file is web-only and used to configure the root HTML for every
// web page during static rendering.
// The contents of this function only run in Node.js environments and
// do not have access to the DOM or browser APIs.
export default function Root({ children }: { children: React.ReactNode }) {
return (
<html lang="en">
<head>
<meta charSet="utf-8" />
<meta httpEquiv="X-UA-Compatible" content="IE=edge" />
{/*
This viewport disables scaling which makes the mobile website act more like a native app.
However this does reduce built-in accessibility. If you want to enable scaling, use this instead:
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover" />
*/}
<meta
name="viewport"
content="width=device-width,initial-scale=1,minimum-scale=1,maximum-scale=1.00001,viewport-fit=cover"
/>
{/*
Disable body scrolling on web. This makes ScrollView components work closer to how they do on native.
However, body scrolling is often nice to have for mobile web. If you want to enable it, remove this line.
*/}
<ScrollViewStyleReset />
{/* Using raw CSS styles as an escape-hatch to ensure the background color never flickers in dark-mode. */}
<style dangerouslySetInnerHTML={{ __html: responsiveBackground }} />
{/* Add any additional <head> elements that you want globally available on web... */}
</head>
<body>{children}</body>
</html>
);
}
const responsiveBackground = `
body {
background-color: #fff;
}
@media (prefers-color-scheme: dark) {
body {
background-color: #000;
}
}`;

View File

@@ -0,0 +1,21 @@
import { View } from "react-native";
import { Link, Stack } from "expo-router";
import { Text } from "~/components/ui/Text";
export default function NotFoundScreen() {
return (
<>
<Stack.Screen options={{ title: "Oops!" }} />
<View className="flex-1 items-center justify-center p-5">
<Text className="text-lg font-bold">
This screen doesn&apos;t exist.
</Text>
<Link href="/" className="mt-4 py-4">
<Text className="text-sm text-sky-500">Go to home screen!</Text>
</Link>
</View>
</>
);
}

View File

@@ -0,0 +1,81 @@
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
import { useEffect } from "react";
import { useColorScheme } from "react-native";
import { useFonts } from "expo-font";
import { SplashScreen, Stack } from "expo-router";
import FontAwesome from "@expo/vector-icons/FontAwesome";
import {
DarkTheme,
DefaultTheme,
ThemeProvider,
} from "@react-navigation/native";
import Colors from "@movie-web/tailwind-config/colors";
import "./styles/global.css";
export {
// Catch any errors thrown by the Layout component.
ErrorBoundary,
} from "expo-router";
export const unstable_settings = {
// Ensure that reloading on `/modal` keeps a back button present.
initialRouteName: "(tabs)/index",
};
// Prevent the splash screen from auto-hiding before asset loading is complete.
SplashScreen.preventAutoHideAsync().catch(() => {
/* reloading the app might trigger this, so it's safe to ignore */
});
export default function RootLayout() {
const [loaded, error] = useFonts({
OpenSansRegular: require("../../assets/fonts/OpenSans-Regular.ttf"),
OpenSansLight: require("../../assets/fonts/OpenSans-Light.ttf"),
OpenSansMedium: require("../../assets/fonts/OpenSans-Medium.ttf"),
OpenSansBold: require("../../assets/fonts/OpenSans-Bold.ttf"),
OpenSansSemiBold: require("../../assets/fonts/OpenSans-SemiBold.ttf"),
OpenSansExtra: require("../../assets/fonts/OpenSans-ExtraBold.ttf"),
...FontAwesome.font,
});
// Expo Router uses Error Boundaries to catch errors in the navigation tree.
useEffect(() => {
if (error) throw error;
}, [error]);
useEffect(() => {
if (loaded) {
SplashScreen.hideAsync().catch(() => {
/* reloading the app might trigger this, so it's safe to ignore */
});
}
}, [loaded]);
if (!loaded) {
return null;
}
return <RootLayoutNav />;
}
function RootLayoutNav() {
const colorScheme = useColorScheme();
return (
<ThemeProvider value={colorScheme === "dark" ? DarkTheme : DefaultTheme}>
<Stack
screenOptions={{
gestureEnabled: true,
headerShown: false,
contentStyle: {
backgroundColor: Colors.background,
},
}}
>
<Stack.Screen name="(tabs)" options={{ headerShown: false }} />
</Stack>
</ThemeProvider>
);
}

View File

@@ -0,0 +1,30 @@
import React from "react";
import { Platform } from "react-native";
import { Link } from "expo-router";
import * as WebBrowser from "expo-web-browser";
export function ExternalLink(
props: Omit<React.ComponentProps<typeof Link>, "href"> & { href: string },
) {
return (
<Link
hrefAttrs={{
// On web, launch the link in a new tab.
target: "_blank",
}}
{...props}
// @ts-expect-error href is not a valid prop for Link, but we're using it here to pass the URL to the web browser.
href={props.href}
onPress={(e) => {
if (Platform.OS !== "web") {
// Prevent the default behavior of linking to the default browser on native.
e.preventDefault();
// Open the link in an in-app browser.
WebBrowser.openBrowserAsync(props.href).catch((error) => {
console.error("Failed to open browser", error);
});
}
}}
/>
);
}

View File

@@ -0,0 +1,17 @@
import { FontAwesome } from "@expo/vector-icons";
import Colors from "@movie-web/tailwind-config/colors";
type Props = {
focused?: boolean;
} & React.ComponentProps<typeof FontAwesome>;
export default function TabBarIcon({ focused, ...rest }: Props) {
return (
<FontAwesome
color={focused ? Colors.primary[300] : Colors.secondary[300]}
size={24}
{...rest}
/>
);
}

View File

@@ -0,0 +1,26 @@
import { Image, View } from "react-native";
import { TMDB_POSTER_PATH } from "~/app/constants/General";
import { Text } from "~/components/ui/Text";
export default function Item() {
return (
<View className="w-full">
<View className="mb-2 aspect-[9/14] w-full overflow-hidden rounded-2xl">
<Image
source={{
uri: `${TMDB_POSTER_PATH}/w342//gdIrmf2DdY5mgN6ycVP0XlzKzbE.jpg`,
width: 200,
}}
className="h-full w-full object-cover"
/>
</View>
<Text className="font-bold">Hamilton</Text>
<View className="flex-row items-center gap-3">
<Text className="text-xs text-gray-600">Movie</Text>
<View className="h-1 w-1 rounded-3xl bg-gray-600" />
<Text className="text-sm text-gray-600">2023</Text>
</View>
</View>
);
}

View File

@@ -0,0 +1,22 @@
import { View } from "react-native";
import { Text } from "~/components/ui/Text";
interface Props {
title?: React.ReactNode | string;
subtitle?: string;
children?: React.ReactNode;
}
export default function ScreenLayout({ title, subtitle, children }: Props) {
return (
<View className="bg-shade-900 flex-1 p-12">
{typeof title === "string" && (
<Text className="text-2xl font-bold">{title}</Text>
)}
{typeof title !== "string" && title}
<Text className="mt-1 text-sm font-bold">{subtitle}</Text>
<View className="py-3">{children}</View>
</View>
);
}

View File

@@ -0,0 +1,18 @@
import type { TextProps } from "react-native";
import { Text as RNText } from "react-native";
import { cva } from "class-variance-authority";
import { cn } from "~/app/lib/utils";
const textVariants = cva("text-white");
export function Text({ className, ...props }: TextProps) {
return (
<RNText
className={cn(className, textVariants(), {
"font-sans": !className?.includes("font-"),
})}
{...props}
/>
);
}

View File

@@ -0,0 +1 @@
export const TMDB_POSTER_PATH = `https://image.tmdb.org/t/p`;

View File

@@ -0,0 +1,7 @@
import type { ClassValue } from "clsx";
import { clsx } from "clsx";
import { twMerge } from "tailwind-merge";
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs));
}

View File

@@ -0,0 +1,3 @@
@tailwind base;
@tailwind components;
@tailwind utilities;

View File

@@ -0,0 +1 @@
/// <reference types="nativewind/types" />

View File

@@ -0,0 +1,28 @@
import type { Config } from "tailwindcss";
// @ts-expect-error - no types
import nativewind from "nativewind/preset";
import baseConfig from "@movie-web/tailwind-config/native";
export default {
content: ["./src/**/*.{ts,tsx}"],
presets: [
baseConfig,
nativewind,
{
theme: {
extend: {
fontFamily: {
sans: ["OpenSansRegular"],
thin: ["OpenSansLight"],
normal: ["OpenSansRegular"],
medium: ["OpenSansMedium"],
semibold: ["OpenSansSemiBold"],
bold: ["OpenSansBold"],
extrabold: ["OpenSansExtra"],
},
},
},
},
],
} satisfies Config;

16
apps/expo/tsconfig.json Normal file
View File

@@ -0,0 +1,16 @@
{
"extends": ["@movie-web/tsconfig/base.json"],
"compilerOptions": {
"incremental": true,
"baseUrl": ".",
"paths": {
"~/*": ["./src/*"],
"~/components/*": ["./src/app/components/*"]
},
"jsx": "react-native",
"tsBuildInfoFile": "node_modules/.cache/tsbuildinfo.json",
"types": ["nativewind/types"]
},
"include": ["src", "*.ts", "*.js", ".expo/types/**/*.ts", "expo-env.d.ts"],
"exclude": ["node_modules"]
}