Skip to content

Commit 414e12b

Browse files
authored
chore: add mono repo with example (#330)
1 parent 6bc9313 commit 414e12b

21 files changed

+9220
-2260
lines changed

.github/workflows/release.yml

+2-2
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ name: release
33
on:
44
workflow_run:
55
workflows: ['test']
6-
branches: [main, beta]
6+
branches: [main]
77
types:
88
- completed
99

@@ -29,7 +29,7 @@ jobs:
2929
- name: Build
3030
run: npm run build
3131
- name: Release
32-
run: npx semantic-release
32+
run: npx nx release publish
3333
env:
3434
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
3535
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}

.gitignore

+8
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,16 @@
44
# Build
55
dist
66

7+
# Expos
8+
.expo
9+
710
# node.js
811
node_modules/
912
npm-debug.log
1013
yarn.lock
1114
yarn-error.log
15+
16+
17+
18+
.nx/cache
19+
.nx/workspace-data

eslint.config.mjs

+7
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,12 @@ import eslint from '@eslint/js';
22
import tsEslint from 'typescript-eslint';
33
import eslintPluginPrettierRecommended from 'eslint-plugin-prettier/recommended';
44
import react from 'eslint-plugin-react';
5+
import globals from 'globals';
56

67
export default tsEslint.config(
8+
{
9+
ignores: ['node_modules', '**/dist'],
10+
},
711
eslint.configs.recommended,
812
...tsEslint.configs.recommended,
913
eslintPluginPrettierRecommended,
@@ -13,6 +17,9 @@ export default tsEslint.config(
1317
react,
1418
},
1519
languageOptions: {
20+
globals: {
21+
...globals.node,
22+
},
1623
parserOptions: {
1724
ecmaFeatures: {
1825
jsx: true,

examples/StarWarsMovieFinder/App.tsx

+114
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
import Autocomplete from 'react-native-autocomplete-input';
2+
import React, { useEffect, useState } from 'react';
3+
import { SWAPI, Movies, Movie, filterMovies } from './swapi';
4+
import { MovieDetails } from './MovieDetails';
5+
import { Platform, StyleSheet, Text, TouchableOpacity, View, SafeAreaView } from 'react-native';
6+
7+
function StarWarsMovieFinder(): React.ReactElement {
8+
const [allMovies, setAllMovies] = useState<Movies>([]);
9+
const [query, setQuery] = useState('');
10+
const queriedMovies = React.useMemo(() => filterMovies(allMovies, query), [allMovies, query]);
11+
12+
const [firstMovieSuggestion] = queriedMovies;
13+
const suggestions: Movies = React.useMemo(
14+
() =>
15+
firstMovieSuggestion?.compareTitle(query)
16+
? [] // Close suggestion list in case movie title matches query
17+
: queriedMovies,
18+
[queriedMovies, query, firstMovieSuggestion],
19+
);
20+
21+
useEffect(() => {
22+
(async function fetchMovies() {
23+
setAllMovies(await SWAPI.getAllMovies());
24+
})();
25+
}, []);
26+
27+
const isLoading = !allMovies.length;
28+
const placeholder = isLoading ? 'Loading data...' : 'Enter Star Wars film title';
29+
30+
return (
31+
<SafeAreaView style={styles.saveView}>
32+
<View style={styles.container}>
33+
<View style={styles.autocompleteContainer}>
34+
<Autocomplete
35+
editable={!isLoading}
36+
autoCorrect={false}
37+
data={suggestions}
38+
value={query}
39+
onChangeText={setQuery}
40+
placeholder={placeholder}
41+
flatListProps={{
42+
keyboardShouldPersistTaps: 'always',
43+
keyExtractor: (movie: Movie) => movie.episodeId.toString(),
44+
renderItem: ({ item }) => (
45+
<TouchableOpacity onPress={() => setQuery(item.title)}>
46+
<Text style={styles.itemText}>{item.title}</Text>
47+
</TouchableOpacity>
48+
),
49+
}}
50+
/>
51+
</View>
52+
53+
<View style={styles.descriptionContainer}>
54+
{firstMovieSuggestion ? (
55+
<MovieDetails movie={firstMovieSuggestion} />
56+
) : (
57+
<Text style={styles.infoText}>Enter Title of a Star Wars movie</Text>
58+
)}
59+
</View>
60+
</View>
61+
</SafeAreaView>
62+
);
63+
}
64+
65+
const styles = StyleSheet.create({
66+
saveView: {
67+
flex: 1,
68+
},
69+
container: {
70+
position: 'relative',
71+
backgroundColor: '#F5FCFF',
72+
flex: 1,
73+
74+
// Android requiers padding to avoid overlapping
75+
// with content and autocomplete
76+
paddingTop: 50,
77+
78+
// Make space for the default top bar
79+
...Platform.select({
80+
android: {
81+
marginTop: 25,
82+
},
83+
default: {
84+
marginTop: 0,
85+
},
86+
}),
87+
},
88+
itemText: {
89+
fontSize: 15,
90+
margin: 2,
91+
},
92+
descriptionContainer: {
93+
// `backgroundColor` needs to be set otherwise the
94+
// autocomplete input will disappear on text input.
95+
backgroundColor: '#F5FCFF',
96+
marginTop: 8,
97+
},
98+
infoText: {
99+
textAlign: 'center',
100+
},
101+
autocompleteContainer: {
102+
// Hack required to make the autocomplete
103+
// work on Andrdoid
104+
flex: 1,
105+
left: 0,
106+
position: 'absolute',
107+
right: 0,
108+
top: 0,
109+
zIndex: 1,
110+
padding: 5,
111+
},
112+
});
113+
114+
export default StarWarsMovieFinder;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
import React from 'react';
2+
import { View, Text, StyleSheet } from 'react-native';
3+
import { Movie } from './swapi';
4+
5+
interface Props {
6+
movie: Movie;
7+
}
8+
9+
export function MovieDetails({ movie }: Props): React.ReactElement {
10+
const { title, director, openingCrawl, episode } = movie;
11+
return (
12+
<View>
13+
<Text style={styles.titleText}>
14+
{episode}. {title}
15+
</Text>
16+
<Text style={styles.directorText}>({director})</Text>
17+
<Text style={styles.openingText}>{openingCrawl}</Text>
18+
</View>
19+
);
20+
}
21+
22+
const styles = StyleSheet.create({
23+
titleText: {
24+
fontSize: 18,
25+
fontWeight: '500',
26+
marginBottom: 10,
27+
marginTop: 10,
28+
textAlign: 'center',
29+
},
30+
directorText: {
31+
color: 'grey',
32+
fontSize: 12,
33+
marginBottom: 10,
34+
textAlign: 'center',
35+
},
36+
openingText: {
37+
textAlign: 'center',
38+
},
39+
});

examples/StarWarsMovieFinder/app.json

+18
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
{
2+
"expo": {
3+
"name": "StarWarsMovieFinder",
4+
"slug": "StarWarsMovieFinder",
5+
"version": "1.0.0",
6+
"orientation": "portrait",
7+
"userInterfaceStyle": "light",
8+
"newArchEnabled": true,
9+
"ios": {
10+
"supportsTablet": true
11+
},
12+
"android": {
13+
"adaptiveIcon": {
14+
"backgroundColor": "#ffffff"
15+
}
16+
}
17+
}
18+
}

examples/StarWarsMovieFinder/index.ts

+8
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
import { registerRootComponent } from 'expo';
2+
3+
import App from './App';
4+
5+
// registerRootComponent calls AppRegistry.registerComponent('main', () => App);
6+
// It also ensures that whether you load the app in Expo Go or in a native build,
7+
// the environment is set up appropriately
8+
registerRootComponent(App);
+23
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
{
2+
"name": "starwarsmoviefinder",
3+
"version": "1.0.0",
4+
"main": "index.ts",
5+
"scripts": {
6+
"start": "expo start",
7+
"android": "expo start --android",
8+
"ios": "expo start --ios",
9+
"web": "expo start --web"
10+
},
11+
"dependencies": {
12+
"expo": "~52.0.23",
13+
"expo-status-bar": "~2.0.0",
14+
"react": "18.3.1",
15+
"react-native": "0.76.5",
16+
"react-native-autocomplete-input": "^5.5.3"
17+
},
18+
"devDependencies": {
19+
"@babel/core": "^7.25.2",
20+
"@types/react": "~18.3.12"
21+
},
22+
"private": true
23+
}

examples/StarWarsMovieFinder/swapi.ts

+62
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
const SWAPI_API = 'https://swapi.py4e.com/api';
2+
3+
type SWAPIMovie = {
4+
title: string;
5+
director: string;
6+
episode_id: number;
7+
opening_crawl: string;
8+
};
9+
10+
type MovieProps = {
11+
title: string;
12+
director: string;
13+
episodeId: number;
14+
openingCrawl: string;
15+
};
16+
17+
export class Movie implements MovieProps {
18+
readonly title: string;
19+
readonly director: string;
20+
readonly openingCrawl: string;
21+
readonly episodeId: number;
22+
23+
constructor({ episodeId, title, director, openingCrawl }: MovieProps) {
24+
this.episodeId = episodeId;
25+
this.title = title;
26+
this.director = director;
27+
this.openingCrawl = openingCrawl;
28+
}
29+
30+
compareTitle(title: string) {
31+
return this.title.toLowerCase() === title.toLowerCase().trim();
32+
}
33+
34+
get episode(): string {
35+
return (
36+
['', 'I', 'II', 'III', 'IV', 'V', 'VI', 'VII', 'VIII'][this.episodeId] ||
37+
this.episodeId.toString()
38+
);
39+
}
40+
}
41+
42+
export type Movies = readonly Movie[];
43+
44+
export function filterMovies(movies: Movies, query?: string): Movies {
45+
if (!query || !movies.length) {
46+
return [];
47+
}
48+
49+
const regex = new RegExp(`${query.trim()}`, 'i');
50+
return movies.filter((movie) => movie.title.search(regex) >= 0);
51+
}
52+
53+
export const SWAPI = {
54+
async getAllMovies(): Promise<Movies> {
55+
const { results } = await fetch(SWAPI_API + '/films').then((res) => res.json());
56+
57+
return results.map(
58+
({ opening_crawl: openingCrawl, episode_id: episodeId, ...otherProps }: SWAPIMovie) =>
59+
new Movie({ ...otherProps, openingCrawl, episodeId }),
60+
);
61+
},
62+
};
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
{
2+
"extends": "expo/tsconfig.base",
3+
"compilerOptions": {
4+
"strict": true
5+
}
6+
}

nx.json

+26
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
{
2+
"$schema": "./node_modules/nx/schemas/nx-schema.json",
3+
"targetDefaults": {
4+
"build": {
5+
"dependsOn": ["^build"],
6+
"outputs": ["{projectRoot}/dist"],
7+
"cache": true
8+
},
9+
"start": {
10+
"cache": true
11+
},
12+
"test": {
13+
"cache": true
14+
}
15+
},
16+
"defaultBase": "main",
17+
"release": {
18+
"projects": ["packages/*"],
19+
"changelog": {
20+
"workspaceChangelog": {
21+
"file": false,
22+
"createRelease": "github"
23+
}
24+
}
25+
}
26+
}

0 commit comments

Comments
 (0)