mirror of
https://github.com/movie-web/native-app.git
synced 2025-09-13 16:43:25 +00:00
refactor to turbo
This commit is contained in:
86
apps/expo/src/app/(tabs)/_layout.tsx
Normal file
86
apps/expo/src/app/(tabs)/_layout.tsx
Normal 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>
|
||||
);
|
||||
}
|
16
apps/expo/src/app/(tabs)/about.tsx
Normal file
16
apps/expo/src/app/(tabs)/about.tsx
Normal 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>
|
||||
);
|
||||
}
|
13
apps/expo/src/app/(tabs)/account.tsx
Normal file
13
apps/expo/src/app/(tabs)/account.tsx
Normal 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>
|
||||
);
|
||||
}
|
10
apps/expo/src/app/(tabs)/index.tsx
Normal file
10
apps/expo/src/app/(tabs)/index.tsx
Normal 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>
|
||||
);
|
||||
}
|
41
apps/expo/src/app/(tabs)/search/Searchbar.tsx
Normal file
41
apps/expo/src/app/(tabs)/search/Searchbar.tsx
Normal 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>
|
||||
);
|
||||
}
|
34
apps/expo/src/app/(tabs)/search/_layout.tsx
Normal file
34
apps/expo/src/app/(tabs)/search/_layout.tsx
Normal 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>
|
||||
);
|
||||
}
|
10
apps/expo/src/app/(tabs)/settings.tsx
Normal file
10
apps/expo/src/app/(tabs)/settings.tsx
Normal 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>
|
||||
);
|
||||
}
|
46
apps/expo/src/app/+html.tsx
Normal file
46
apps/expo/src/app/+html.tsx
Normal 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;
|
||||
}
|
||||
}`;
|
21
apps/expo/src/app/[...missing].tsx
Normal file
21
apps/expo/src/app/[...missing].tsx
Normal 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't exist.
|
||||
</Text>
|
||||
|
||||
<Link href="/" className="mt-4 py-4">
|
||||
<Text className="text-sm text-sky-500">Go to home screen!</Text>
|
||||
</Link>
|
||||
</View>
|
||||
</>
|
||||
);
|
||||
}
|
81
apps/expo/src/app/_layout.tsx
Normal file
81
apps/expo/src/app/_layout.tsx
Normal 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>
|
||||
);
|
||||
}
|
30
apps/expo/src/app/components/ExternalLink.tsx
Normal file
30
apps/expo/src/app/components/ExternalLink.tsx
Normal 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);
|
||||
});
|
||||
}
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
17
apps/expo/src/app/components/TabBarIcon.tsx
Normal file
17
apps/expo/src/app/components/TabBarIcon.tsx
Normal 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}
|
||||
/>
|
||||
);
|
||||
}
|
26
apps/expo/src/app/components/item/item.tsx
Normal file
26
apps/expo/src/app/components/item/item.tsx
Normal 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>
|
||||
);
|
||||
}
|
22
apps/expo/src/app/components/layout/ScreenLayout.tsx
Normal file
22
apps/expo/src/app/components/layout/ScreenLayout.tsx
Normal 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>
|
||||
);
|
||||
}
|
18
apps/expo/src/app/components/ui/Text.tsx
Normal file
18
apps/expo/src/app/components/ui/Text.tsx
Normal 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}
|
||||
/>
|
||||
);
|
||||
}
|
1
apps/expo/src/app/constants/General.ts
Normal file
1
apps/expo/src/app/constants/General.ts
Normal file
@@ -0,0 +1 @@
|
||||
export const TMDB_POSTER_PATH = `https://image.tmdb.org/t/p`;
|
7
apps/expo/src/app/lib/utils.ts
Normal file
7
apps/expo/src/app/lib/utils.ts
Normal 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));
|
||||
}
|
3
apps/expo/src/app/styles/global.css
Normal file
3
apps/expo/src/app/styles/global.css
Normal file
@@ -0,0 +1,3 @@
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
Reference in New Issue
Block a user