mirror of
https://github.com/morten-olsen/react-native-ref.git
synced 2026-02-08 00:36:24 +01:00
init
This commit is contained in:
35
packages/app/.gitignore
vendored
Normal file
35
packages/app/.gitignore
vendored
Normal file
@@ -0,0 +1,35 @@
|
||||
# Learn more https://docs.github.com/en/get-started/getting-started-with-git/ignoring-files
|
||||
|
||||
# dependencies
|
||||
node_modules/
|
||||
|
||||
# Expo
|
||||
.expo/
|
||||
dist/
|
||||
web-build/
|
||||
|
||||
# Native
|
||||
*.orig.*
|
||||
*.jks
|
||||
*.p8
|
||||
*.p12
|
||||
*.key
|
||||
*.mobileprovision
|
||||
|
||||
# Metro
|
||||
.metro-health-check*
|
||||
|
||||
# debug
|
||||
npm-debug.*
|
||||
yarn-debug.*
|
||||
yarn-error.*
|
||||
|
||||
# macOS
|
||||
.DS_Store
|
||||
*.pem
|
||||
|
||||
# local env files
|
||||
.env*.local
|
||||
|
||||
# typescript
|
||||
*.tsbuildinfo
|
||||
1
packages/app/.npmrc
Normal file
1
packages/app/.npmrc
Normal file
@@ -0,0 +1 @@
|
||||
node-linker=hoisted
|
||||
33
packages/app/app.json
Normal file
33
packages/app/app.json
Normal file
@@ -0,0 +1,33 @@
|
||||
{
|
||||
"expo": {
|
||||
"name": "react-native-ref",
|
||||
"slug": "react-native-ref",
|
||||
"version": "1.0.0",
|
||||
"orientation": "portrait",
|
||||
"icon": "./assets/icon.png",
|
||||
"userInterfaceStyle": "light",
|
||||
"splash": {
|
||||
"image": "./assets/splash.png",
|
||||
"resizeMode": "contain",
|
||||
"backgroundColor": "#ffffff"
|
||||
},
|
||||
"assetBundlePatterns": [
|
||||
"**/*"
|
||||
],
|
||||
"ios": {
|
||||
"supportsTablet": true
|
||||
},
|
||||
"android": {
|
||||
"adaptiveIcon": {
|
||||
"foregroundImage": "./assets/adaptive-icon.png",
|
||||
"backgroundColor": "#ffffff"
|
||||
}
|
||||
},
|
||||
"web": {
|
||||
"favicon": "./assets/favicon.png"
|
||||
},
|
||||
"experiments": {
|
||||
"baseUrl": "/react-native-ref/app"
|
||||
}
|
||||
}
|
||||
}
|
||||
BIN
packages/app/assets/adaptive-icon.png
Normal file
BIN
packages/app/assets/adaptive-icon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 17 KiB |
BIN
packages/app/assets/favicon.png
Normal file
BIN
packages/app/assets/favicon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.4 KiB |
BIN
packages/app/assets/icon.png
Normal file
BIN
packages/app/assets/icon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 22 KiB |
BIN
packages/app/assets/splash.png
Normal file
BIN
packages/app/assets/splash.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 46 KiB |
6
packages/app/babel.config.js
Normal file
6
packages/app/babel.config.js
Normal file
@@ -0,0 +1,6 @@
|
||||
module.exports = function(api) {
|
||||
api.cache(true);
|
||||
return {
|
||||
presets: ['babel-preset-expo'],
|
||||
};
|
||||
};
|
||||
8
packages/app/index.js
Normal file
8
packages/app/index.js
Normal file
@@ -0,0 +1,8 @@
|
||||
import { registerRootComponent } from 'expo';
|
||||
|
||||
import { App } from './src/app';
|
||||
|
||||
// registerRootComponent calls AppRegistry.registerComponent('main', () => App);
|
||||
// It also ensures that whether you load the app in Expo Go or in a native build,
|
||||
// the environment is set up appropriately
|
||||
registerRootComponent(App);
|
||||
26
packages/app/metro.config.js
Normal file
26
packages/app/metro.config.js
Normal file
@@ -0,0 +1,26 @@
|
||||
// Learn more https://docs.expo.dev/guides/monorepos
|
||||
const { getDefaultConfig } = require('expo/metro-config');
|
||||
const { FileStore } = require('metro-cache');
|
||||
const path = require('path');
|
||||
|
||||
const projectRoot = __dirname;
|
||||
const workspaceRoot = path.resolve(projectRoot, '../..');
|
||||
|
||||
const config = getDefaultConfig(projectRoot);
|
||||
|
||||
// #1 - Watch all files in the monorepo
|
||||
config.watchFolders = [workspaceRoot];
|
||||
// #3 - Force resolving nested modules to the folders below
|
||||
config.resolver.disableHierarchicalLookup = true;
|
||||
// #2 - Try resolving with project modules first, then workspace modules
|
||||
config.resolver.nodeModulesPaths = [
|
||||
path.resolve(projectRoot, 'node_modules'),
|
||||
path.resolve(workspaceRoot, 'node_modules'),
|
||||
];
|
||||
|
||||
// Use turborepo to restore the cache when possible
|
||||
config.cacheStores = [
|
||||
new FileStore({ root: path.join(projectRoot, 'node_modules', '.cache', 'metro') }),
|
||||
];
|
||||
|
||||
module.exports = config;
|
||||
32
packages/app/package.json
Normal file
32
packages/app/package.json
Normal file
@@ -0,0 +1,32 @@
|
||||
{
|
||||
"name": "react-native-ref",
|
||||
"version": "1.0.0",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
"dev:app": "expo start",
|
||||
"dev:android": "expo start --android",
|
||||
"dev:ios": "expo start --ios",
|
||||
"dev:web": "expo start --web",
|
||||
"build:web": "expo export --platform web"
|
||||
},
|
||||
"dependencies": {
|
||||
"@expo/metro-runtime": "~3.1.3",
|
||||
"@react-ref/ui": "workspace:^",
|
||||
"@react-navigation/bottom-tabs": "^6.5.20",
|
||||
"@react-navigation/native": "^6.1.17",
|
||||
"@react-navigation/native-stack": "^6.9.26",
|
||||
"@react-navigation/stack": "^6.3.29",
|
||||
"expo": "~50.0.14",
|
||||
"expo-status-bar": "~1.11.1",
|
||||
"react": "18.2.0",
|
||||
"react-native": "0.73.6",
|
||||
"react-native-safe-area-context": "4.8.2",
|
||||
"react-native-screens": "~3.29.0",
|
||||
"styled-components": "^6.1.8"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.20.0",
|
||||
"babel-plugin-module-resolver": "^5.0.2"
|
||||
},
|
||||
"private": true
|
||||
}
|
||||
58
packages/app/src/api/api.community.ts
Normal file
58
packages/app/src/api/api.community.ts
Normal file
@@ -0,0 +1,58 @@
|
||||
import {
|
||||
useInfiniteQuery,
|
||||
useMutation,
|
||||
useQueryClient,
|
||||
} from "@tanstack/react-query";
|
||||
import * as data from "./api.data";
|
||||
|
||||
const useCommunityMessages = () => {
|
||||
const query = useInfiniteQuery({
|
||||
initialPageParam: 0,
|
||||
queryKey: ["communityMessages"],
|
||||
queryFn: ({ pageParam }) =>
|
||||
data.getCommunityMessages({ cursor: pageParam }),
|
||||
getNextPageParam: (lastPage) => lastPage.nextCursor,
|
||||
});
|
||||
|
||||
return query;
|
||||
};
|
||||
|
||||
const useLikeCommunityMessage = () => {
|
||||
const queryClient = useQueryClient();
|
||||
const mutation = useMutation({
|
||||
mutationFn: (id: string) => data.likeCommunityMessage(id),
|
||||
onSuccess: async () => {
|
||||
await queryClient.invalidateQueries({ queryKey: ["communityMessages"] });
|
||||
},
|
||||
});
|
||||
return mutation;
|
||||
};
|
||||
|
||||
const useUnlikeCommunityMessage = () => {
|
||||
const queryClient = useQueryClient();
|
||||
const mutation = useMutation({
|
||||
mutationFn: (id: string) => data.unlikeCommunityMessage(id),
|
||||
onSuccess: async () => {
|
||||
await queryClient.invalidateQueries({ queryKey: ["communityMessages"] });
|
||||
},
|
||||
});
|
||||
return mutation;
|
||||
};
|
||||
|
||||
const useAddCommunityMessage = () => {
|
||||
const queryClient = useQueryClient();
|
||||
const mutation = useMutation({
|
||||
mutationFn: (content: string) => data.addCommunityMessage(content),
|
||||
onSuccess: async () => {
|
||||
await queryClient.invalidateQueries({ queryKey: ["communityMessages"] });
|
||||
},
|
||||
});
|
||||
return mutation;
|
||||
};
|
||||
|
||||
export {
|
||||
useCommunityMessages,
|
||||
useLikeCommunityMessage,
|
||||
useUnlikeCommunityMessage,
|
||||
useAddCommunityMessage,
|
||||
};
|
||||
73
packages/app/src/api/api.courses.ts
Normal file
73
packages/app/src/api/api.courses.ts
Normal file
@@ -0,0 +1,73 @@
|
||||
import {
|
||||
useQuery,
|
||||
useInfiniteQuery,
|
||||
useMutation,
|
||||
useQueryClient,
|
||||
} from "@tanstack/react-query";
|
||||
import * as data from "./api.data";
|
||||
|
||||
const useCourse = (id: string) => {
|
||||
const query = useQuery({
|
||||
queryKey: ["course", id],
|
||||
queryFn: () => data.getCourse(id),
|
||||
});
|
||||
|
||||
return query;
|
||||
};
|
||||
|
||||
const useCourses = () => {
|
||||
const query = useInfiniteQuery({
|
||||
initialPageParam: 0,
|
||||
queryKey: ["courses"],
|
||||
queryFn: ({ pageParam }) => data.getCourses({ cursor: pageParam }),
|
||||
getNextPageParam: (lastPage) => lastPage.nextCursor,
|
||||
});
|
||||
|
||||
return query;
|
||||
};
|
||||
|
||||
const useBookmarkedCourses = () => {
|
||||
const query = useInfiniteQuery({
|
||||
initialPageParam: 0,
|
||||
queryKey: ["bookmarkedCourses"],
|
||||
queryFn: ({ pageParam }) =>
|
||||
data.getBookmarkedCourses({ cursor: pageParam }),
|
||||
getNextPageParam: (lastPage) => lastPage.nextCursor,
|
||||
});
|
||||
|
||||
return query;
|
||||
};
|
||||
|
||||
const useAddBookmark = () => {
|
||||
const queryClient = useQueryClient();
|
||||
const mutation = useMutation({
|
||||
mutationFn: (id: string) => data.addBookmark(id),
|
||||
onSuccess: async (_, id) => {
|
||||
await queryClient.invalidateQueries({ queryKey: ["bookmarkedCourses"] });
|
||||
await queryClient.invalidateQueries({ queryKey: ["courses"] });
|
||||
await queryClient.invalidateQueries({ queryKey: ["course", id] });
|
||||
},
|
||||
});
|
||||
return mutation;
|
||||
};
|
||||
|
||||
const useRemoveBookmark = () => {
|
||||
const queryClient = useQueryClient();
|
||||
const mutation = useMutation({
|
||||
mutationFn: (id: string) => data.removeBookmark(id),
|
||||
onSuccess: async (_, id) => {
|
||||
await queryClient.invalidateQueries({ queryKey: ["bookmarkedCourses"] });
|
||||
await queryClient.invalidateQueries({ queryKey: ["courses"] });
|
||||
await queryClient.invalidateQueries({ queryKey: ["course", id] });
|
||||
},
|
||||
});
|
||||
return mutation;
|
||||
};
|
||||
|
||||
export {
|
||||
useCourse,
|
||||
useCourses,
|
||||
useBookmarkedCourses,
|
||||
useAddBookmark,
|
||||
useRemoveBookmark,
|
||||
};
|
||||
138
packages/app/src/api/api.data.ts
Normal file
138
packages/app/src/api/api.data.ts
Normal file
@@ -0,0 +1,138 @@
|
||||
import type {
|
||||
CourseThumbProps,
|
||||
CommunityMessageProps,
|
||||
} from "@react-ref/ui";
|
||||
import { addResponseTime } from "./api.utils";
|
||||
|
||||
const PAGE_SIZE = 10;
|
||||
const sortCommunityMessages = (
|
||||
a: CommunityMessageProps,
|
||||
b: CommunityMessageProps,
|
||||
) => b.publishedAt.getDate() - a.publishedAt.getDate();
|
||||
|
||||
const communityMessages = new Array(10)
|
||||
.fill(0)
|
||||
.map<CommunityMessageProps>((_, i) => ({
|
||||
id: i.toString(),
|
||||
user: {
|
||||
name: `User ${i.toString()}`,
|
||||
avatar: `https://via.placeholder.com/300?text=User+${i.toString()}`,
|
||||
},
|
||||
publishedAt: new Date(Date.now() - (i + 1) * 1000 * 60 * 60 * 24),
|
||||
content: `Message ${i.toString()}`,
|
||||
liked: i % 2 === 0,
|
||||
}))
|
||||
.sort(sortCommunityMessages);
|
||||
|
||||
const courses = new Array(10).fill(0).map<CourseThumbProps>((_, i) => ({
|
||||
course: {
|
||||
id: i.toString(),
|
||||
title: `Course ${i.toString()}`,
|
||||
cover: `https://via.placeholder.com/300?text=Course+${i.toString()}`,
|
||||
lessions: 10,
|
||||
duration: 60,
|
||||
},
|
||||
progress:
|
||||
Math.random() > 0.2 ? { percentage: Math.random() * 100 } : undefined,
|
||||
bookmarked: Math.random() > 0.5,
|
||||
}));
|
||||
|
||||
const getCourse = addResponseTime((id: string) =>
|
||||
courses.find((current) => current.course.id === id),
|
||||
);
|
||||
|
||||
interface CoursesRequest {
|
||||
cursor?: number;
|
||||
}
|
||||
|
||||
const getCourses = addResponseTime(({ cursor = 0 }: CoursesRequest) => {
|
||||
const items = courses.slice(cursor, cursor + PAGE_SIZE + 1);
|
||||
const nextCursor =
|
||||
items.length === PAGE_SIZE + 1 ? cursor + PAGE_SIZE : undefined;
|
||||
return {
|
||||
items: items.slice(0, PAGE_SIZE),
|
||||
nextCursor,
|
||||
};
|
||||
});
|
||||
|
||||
const getBookmarkedCourses = addResponseTime(
|
||||
({ cursor = 0 }: CoursesRequest) => {
|
||||
const items = courses
|
||||
.filter((course) => course.bookmarked)
|
||||
.slice(cursor, cursor + PAGE_SIZE + 1);
|
||||
const nextCursor =
|
||||
items.length === PAGE_SIZE + 1 ? cursor + PAGE_SIZE : undefined;
|
||||
return {
|
||||
items: items.slice(0, PAGE_SIZE),
|
||||
nextCursor,
|
||||
};
|
||||
},
|
||||
);
|
||||
|
||||
const addBookmark = addResponseTime((id: string) => {
|
||||
const course = courses.find((current) => current.course.id === id);
|
||||
if (course) {
|
||||
course.bookmarked = true;
|
||||
}
|
||||
});
|
||||
|
||||
const removeBookmark = addResponseTime((id: string) => {
|
||||
const course = courses.find((current) => current.course.id === id);
|
||||
if (course) {
|
||||
course.bookmarked = false;
|
||||
}
|
||||
});
|
||||
|
||||
const getCommunityMessages = addResponseTime(
|
||||
({ cursor = 0 }: CoursesRequest) => {
|
||||
const items = communityMessages
|
||||
.sort(sortCommunityMessages)
|
||||
.slice(cursor, cursor + PAGE_SIZE + 1);
|
||||
const nextCursor =
|
||||
items.length === PAGE_SIZE + 1 ? cursor + PAGE_SIZE : undefined;
|
||||
return {
|
||||
items: items.slice(0, PAGE_SIZE),
|
||||
nextCursor,
|
||||
};
|
||||
},
|
||||
);
|
||||
|
||||
const addCommunityMessage = addResponseTime((message: string) => {
|
||||
const randomId = Math.random().toString();
|
||||
communityMessages.push({
|
||||
id: randomId,
|
||||
user: {
|
||||
name: "Me",
|
||||
avatar: "https://via.placeholder.com/300?text=Me",
|
||||
},
|
||||
publishedAt: new Date(),
|
||||
content: message,
|
||||
liked: false,
|
||||
});
|
||||
});
|
||||
|
||||
const likeCommunityMessage = addResponseTime((id: string) => {
|
||||
const message = communityMessages.find((current) => current.id === id);
|
||||
if (message) {
|
||||
message.liked = true;
|
||||
}
|
||||
});
|
||||
|
||||
const unlikeCommunityMessage = addResponseTime((id: string) => {
|
||||
const message = communityMessages.find((current) => current.id === id);
|
||||
if (message) {
|
||||
message.liked = false;
|
||||
}
|
||||
});
|
||||
|
||||
export {
|
||||
getCourse,
|
||||
getCourses,
|
||||
getBookmarkedCourses,
|
||||
addBookmark,
|
||||
removeBookmark,
|
||||
addCommunityMessage,
|
||||
getCommunityMessages,
|
||||
likeCommunityMessage,
|
||||
unlikeCommunityMessage,
|
||||
};
|
||||
12
packages/app/src/api/api.utils.ts
Normal file
12
packages/app/src/api/api.utils.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));
|
||||
|
||||
const addResponseTime = <TArgs extends unknown[], TResponse>(
|
||||
fn: (...args: TArgs) => TResponse,
|
||||
) => {
|
||||
return async (...args: TArgs): Promise<TResponse> => {
|
||||
await sleep(500);
|
||||
return fn(...args);
|
||||
};
|
||||
};
|
||||
|
||||
export { addResponseTime };
|
||||
33
packages/app/src/app.tsx
Normal file
33
packages/app/src/app.tsx
Normal file
@@ -0,0 +1,33 @@
|
||||
import "./utils/setup/setup";
|
||||
import { SafeAreaProvider } from "react-native-safe-area-context";
|
||||
import { StatusBar } from "expo-status-bar";
|
||||
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
||||
import { Provider, light } from "@react-ref/ui";
|
||||
import { Router } from "./router/router";
|
||||
import styled from "styled-components/native";
|
||||
import { Platform } from "react-native";
|
||||
|
||||
const queryClient = new QueryClient();
|
||||
|
||||
const KeyboardAvoidingView = styled.KeyboardAvoidingView`
|
||||
flex: 1;
|
||||
`;
|
||||
|
||||
const App = () => {
|
||||
return (
|
||||
<KeyboardAvoidingView
|
||||
behavior={Platform.OS === "ios" ? "padding" : "height"}
|
||||
>
|
||||
<SafeAreaProvider>
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<Provider theme={light}>
|
||||
<StatusBar style="auto" />
|
||||
<Router />
|
||||
</Provider>
|
||||
</QueryClientProvider>
|
||||
</SafeAreaProvider>
|
||||
</KeyboardAvoidingView>
|
||||
);
|
||||
};
|
||||
|
||||
export { App };
|
||||
80
packages/app/src/router/router.tsx
Normal file
80
packages/app/src/router/router.tsx
Normal file
@@ -0,0 +1,80 @@
|
||||
import { useMemo } from "react";
|
||||
import { useTheme } from "styled-components/native";
|
||||
import { createBottomTabNavigator } from "@react-navigation/bottom-tabs";
|
||||
import { NavigationContainer, DefaultTheme } from "@react-navigation/native";
|
||||
import { createStackNavigator } from "@react-navigation/stack";
|
||||
import { createNativeStackNavigator } from "@react-navigation/native-stack";
|
||||
import { Platform } from "react-native";
|
||||
|
||||
import type { MainTabParamList, RootStackParamList } from "./types";
|
||||
import { HomeScreen } from "../screens/home/home";
|
||||
import { CommunityScreen } from "../screens/community/community";
|
||||
|
||||
const MainTabsNvaigator = createBottomTabNavigator<MainTabParamList>();
|
||||
|
||||
const MainTabs: React.FC = () => {
|
||||
const theme = useTheme();
|
||||
return (
|
||||
<MainTabsNvaigator.Navigator
|
||||
screenOptions={{
|
||||
tabBarActiveTintColor: theme.colors.primary,
|
||||
}}
|
||||
>
|
||||
<MainTabsNvaigator.Screen
|
||||
options={{
|
||||
headerShown: false,
|
||||
tabBarLabel: "Home",
|
||||
}}
|
||||
name="home"
|
||||
component={HomeScreen}
|
||||
/>
|
||||
<MainTabsNvaigator.Screen
|
||||
options={{
|
||||
headerShown: false,
|
||||
tabBarLabel: "Community",
|
||||
}}
|
||||
name="community"
|
||||
component={CommunityScreen}
|
||||
/>
|
||||
</MainTabsNvaigator.Navigator>
|
||||
);
|
||||
};
|
||||
|
||||
const RootNavigator =
|
||||
Platform.OS === "web"
|
||||
? createStackNavigator<RootStackParamList>()
|
||||
: createNativeStackNavigator<RootStackParamList>();
|
||||
|
||||
const Root: React.FC = () => (
|
||||
<RootNavigator.Navigator
|
||||
screenOptions={{ headerShown: false, animationEnabled: true }}
|
||||
>
|
||||
<RootNavigator.Group>
|
||||
<RootNavigator.Screen name="main" component={MainTabs} />
|
||||
</RootNavigator.Group>
|
||||
</RootNavigator.Navigator>
|
||||
);
|
||||
|
||||
const Router: React.FC = () => {
|
||||
const theme = useTheme();
|
||||
const baseTheme = useMemo(() => DefaultTheme, []);
|
||||
const navigationTheme = useMemo(
|
||||
() => ({
|
||||
...baseTheme,
|
||||
colors: {
|
||||
...baseTheme.colors,
|
||||
background: theme.colors.shade,
|
||||
card: theme.colors.background,
|
||||
text: theme.colors.text,
|
||||
},
|
||||
}),
|
||||
[baseTheme, theme],
|
||||
);
|
||||
return (
|
||||
<NavigationContainer theme={navigationTheme}>
|
||||
<Root />
|
||||
</NavigationContainer>
|
||||
);
|
||||
};
|
||||
|
||||
export { Router };
|
||||
19
packages/app/src/router/types.ts
Normal file
19
packages/app/src/router/types.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
import type {
|
||||
NavigatorScreenParams,
|
||||
RouteProp,
|
||||
} from "@react-navigation/native";
|
||||
import type { NativeStackNavigationProp } from "@react-navigation/native-stack";
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/consistent-type-definitions
|
||||
export type RootStackParamList = {
|
||||
main: undefined;
|
||||
};
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/consistent-type-definitions
|
||||
export type MainTabParamList = {
|
||||
home: NavigatorScreenParams<RootStackParamList>;
|
||||
community: NavigatorScreenParams<RootStackParamList>;
|
||||
};
|
||||
|
||||
export type RootRouteProp = RouteProp<RootStackParamList>;
|
||||
export type RootNavigationProp = NativeStackNavigationProp<RootStackParamList>;
|
||||
90
packages/app/src/screens/community/community.tsx
Normal file
90
packages/app/src/screens/community/community.tsx
Normal file
@@ -0,0 +1,90 @@
|
||||
import { useCallback, useState } from "react";
|
||||
import styled from "styled-components/native";
|
||||
import { CommunityInput, CommunityMessage } from "@react-ref/ui";
|
||||
import {
|
||||
useAddCommunityMessage,
|
||||
useCommunityMessages,
|
||||
useLikeCommunityMessage,
|
||||
useUnlikeCommunityMessage,
|
||||
} from "../../api/api.community";
|
||||
import { Keyboard } from "react-native";
|
||||
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
||||
|
||||
const Wrapper = styled.View`
|
||||
flex: 1;
|
||||
`;
|
||||
|
||||
const Container = styled.View`
|
||||
height: ${({ theme }) => theme.margins.medium}px;
|
||||
`;
|
||||
|
||||
const HeaderElm = styled.View<{ insets: { top: number } }>`
|
||||
padding-top: ${({ insets }) => insets.top}px;
|
||||
`;
|
||||
|
||||
const Header: React.FC = () => {
|
||||
const insets = useSafeAreaInsets();
|
||||
return <HeaderElm insets={insets} />;
|
||||
};
|
||||
|
||||
const List = styled.FlatList`
|
||||
flex: 1;
|
||||
padding: ${({ theme }) => theme.margins.medium}px;
|
||||
`;
|
||||
|
||||
const CommunityScreen: React.FC = () => {
|
||||
const [input, setInput] = useState("");
|
||||
const messages = useCommunityMessages();
|
||||
const likeCommuniytMessage = useLikeCommunityMessage();
|
||||
const unlikeCommuniytMessage = useUnlikeCommunityMessage();
|
||||
const addCommuniytMessage = useAddCommunityMessage();
|
||||
|
||||
const handleLikeChange = useCallback(
|
||||
(id: string, liked: boolean) => {
|
||||
if (liked) {
|
||||
likeCommuniytMessage.mutate(id);
|
||||
} else {
|
||||
unlikeCommuniytMessage.mutate(id);
|
||||
}
|
||||
},
|
||||
[likeCommuniytMessage, unlikeCommuniytMessage],
|
||||
);
|
||||
|
||||
const handleSend = useCallback(() => {
|
||||
addCommuniytMessage.mutate(input, {
|
||||
onSuccess: () => {
|
||||
setInput("");
|
||||
Keyboard.dismiss();
|
||||
},
|
||||
});
|
||||
}, [addCommuniytMessage, input]);
|
||||
|
||||
return (
|
||||
<Wrapper>
|
||||
<List
|
||||
data={
|
||||
messages.data?.pages
|
||||
.flatMap((page) => page.items)
|
||||
.sort(
|
||||
(a, b) => b.publishedAt.getDate() - a.publishedAt.getDate(),
|
||||
) ?? []
|
||||
}
|
||||
keyExtractor={(message) => message.id}
|
||||
inverted
|
||||
ItemHeaderComponent={Header}
|
||||
ItemSeparatorComponent={Container}
|
||||
renderItem={({ item }) => (
|
||||
<CommunityMessage
|
||||
{...item}
|
||||
onLikeChange={(liked) => {
|
||||
handleLikeChange(item.id, liked);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
<CommunityInput value={input} onChange={setInput} onSend={handleSend} />
|
||||
</Wrapper>
|
||||
);
|
||||
};
|
||||
|
||||
export { CommunityScreen };
|
||||
58
packages/app/src/screens/home/home.tsx
Normal file
58
packages/app/src/screens/home/home.tsx
Normal file
@@ -0,0 +1,58 @@
|
||||
import { useCallback } from "react";
|
||||
import { Carusel, CourseThumb, Title1 } from "@react-ref/ui";
|
||||
import {
|
||||
useAddBookmark,
|
||||
useBookmarkedCourses,
|
||||
useCourses,
|
||||
useRemoveBookmark,
|
||||
} from "../../api/api.courses";
|
||||
import { SafeAreaView } from "react-native-safe-area-context";
|
||||
import styled from "styled-components/native";
|
||||
|
||||
const Wrapper = styled(SafeAreaView)``;
|
||||
|
||||
const HomeScreen: React.FC = () => {
|
||||
const courses = useCourses();
|
||||
const bookmarks = useBookmarkedCourses();
|
||||
const addBookmark = useAddBookmark();
|
||||
const removeBookmark = useRemoveBookmark();
|
||||
const onBookmark = useCallback(
|
||||
(id: string, value: boolean) => {
|
||||
if (value) {
|
||||
addBookmark.mutate(id);
|
||||
} else {
|
||||
removeBookmark.mutate(id);
|
||||
}
|
||||
},
|
||||
[addBookmark, removeBookmark],
|
||||
);
|
||||
|
||||
return (
|
||||
<Wrapper>
|
||||
<Title1>Continue learning</Title1>
|
||||
<Carusel
|
||||
items={bookmarks.data?.pages.flatMap((page) => page.items) ?? []}
|
||||
getKey={(course) => course.course.id}
|
||||
renderItem={(course) => (
|
||||
<CourseThumb
|
||||
{...course}
|
||||
onBookmarkChange={onBookmark.bind(null, course.course.id)}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
<Title1>You may also like</Title1>
|
||||
<Carusel
|
||||
items={courses.data?.pages.flatMap((page) => page.items) ?? []}
|
||||
getKey={(course) => course.course.id}
|
||||
renderItem={(course) => (
|
||||
<CourseThumb
|
||||
{...course}
|
||||
onBookmarkChange={onBookmark.bind(null, course.course.id)}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</Wrapper>
|
||||
);
|
||||
};
|
||||
|
||||
export { HomeScreen };
|
||||
9
packages/app/src/screens/noop/noop.tsx
Normal file
9
packages/app/src/screens/noop/noop.tsx
Normal file
@@ -0,0 +1,9 @@
|
||||
const NoopScreen: React.FC = () => {
|
||||
return (
|
||||
<div>
|
||||
<h1>Noop Screen</h1>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export { NoopScreen };
|
||||
1
packages/app/src/utils/setup/setup.ts
Normal file
1
packages/app/src/utils/setup/setup.ts
Normal file
@@ -0,0 +1 @@
|
||||
export default "";
|
||||
2
packages/app/src/utils/setup/setup.web.ts
Normal file
2
packages/app/src/utils/setup/setup.web.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
import "@fontsource/montserrat";
|
||||
export default "";
|
||||
6
packages/app/tsconfig.json
Normal file
6
packages/app/tsconfig.json
Normal file
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"strictNullChecks": true,
|
||||
},
|
||||
"extends": "expo/tsconfig.base"
|
||||
}
|
||||
Reference in New Issue
Block a user