Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 43 additions & 3 deletions frontend-challenge/README.md
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
# UI Code Challenge!

This small assignment will help evaluate your front end development capabilities. You will be evaluated on design choices (friction, scalability, etc), efficient and effective coding, and style.
This small assignment will help evaluate your front end development capabilities. You will be evaluated on design choices (friction, scalability, etc), efficient and effective coding, and style.

## Challenge

Create a mobile first, two page app for reviewing tennis courts. A user should be able to see a display of courts, search for a specific court, select a court detail view, and leave a review.
Create a mobile first, two page app for reviewing tennis courts. A user should be able to see a display of courts, search for a specific court, select a court detail view, and leave a review.

## Rules

Expand All @@ -15,7 +15,7 @@ Create a mobile first, two page app for reviewing tennis courts. A user should
5. You do not need to write tests for this exercise given the time limit
6. When you are done, submit a PR to this repo.

## Hints
## Hints

- Do not use frameworks outside of the JavaScript/Typescript ecosystem
- Submissions using React Native are preferred but React, Next, or pure JavaScript are acceptable
Expand All @@ -24,3 +24,43 @@ Create a mobile first, two page app for reviewing tennis courts. A user should
- Your job is to delight users

Good luck!

## Submission Implementation

This solution implements the Tennis Court Review App using **React Native (Expo)** and **NativeWind (Tailwind CSS)**.

### Features

- **Mobile-First Design**: Responsive layout optimized for mobile screens using Tailwind CSS.
- **Mock Data**: Generates **60 unique tennis courts** with varied names, locations, ratings, and prices to demonstrate scalability.
- **Search**: Real-time filtering by court name and location.
- **Court Details**: Rich detail view with rating, surface type, price, and scrollable reviews.
- **Review System**: Modal form for submitting reviews, featuring `KeyboardAvoidingView` for better UX on smaller screens.
- **Polished UI**: Includes empty states for search results and interactive touch feedback.

### Tech Stack

- **Framework**: React Native (Expo SDK 52)
- **Routing**: Expo Router (File-based navigation)
- **Styling**: NativeWind v4 (Tailwind CSS)
- **Language**: TypeScript

### AI Usage

As permitted by the rules, this project was architected and implemented with the assistance of an AI coding agent (Google DeepMind Antigravity) to accelerate setup, boilerplate generation, and component implementation while ensuring code quality and adherence to best practices.

### How to Run

1. Navigate to the app directory:
```bash
cd tennis-court-app
```
2. Install dependencies:
```bash
npm install
```
3. Start the development server:
```bash
npx expo start
```
4. Run on Android (`a`), iOS (`i`), or scan the QR code with the Expo Go app.
43 changes: 43 additions & 0 deletions frontend-challenge/tennis-court-app/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
# Learn more https://docs.github.com/en/get-started/getting-started-with-git/ignoring-files

# dependencies
node_modules/

# Expo
.expo/
dist/
web-build/
expo-env.d.ts

# Native
.kotlin/
*.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

app-example

# generated native folders
/ios
/android
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{ "recommendations": ["expo.vscode-expo-tools"] }
7 changes: 7 additions & 0 deletions frontend-challenge/tennis-court-app/.vscode/settings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"editor.codeActionsOnSave": {
"source.fixAll": "explicit",
"source.organizeImports": "explicit",
"source.sortMembers": "explicit"
}
}
50 changes: 50 additions & 0 deletions frontend-challenge/tennis-court-app/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
# Welcome to your Expo app 👋

This is an [Expo](https://expo.dev) project created with [`create-expo-app`](https://www.npmjs.com/package/create-expo-app).

## Get started

1. Install dependencies

```bash
npm install
```

2. Start the app

```bash
npx expo start
```

In the output, you'll find options to open the app in a

- [development build](https://docs.expo.dev/develop/development-builds/introduction/)
- [Android emulator](https://docs.expo.dev/workflow/android-studio-emulator/)
- [iOS simulator](https://docs.expo.dev/workflow/ios-simulator/)
- [Expo Go](https://expo.dev/go), a limited sandbox for trying out app development with Expo

You can start developing by editing the files inside the **app** directory. This project uses [file-based routing](https://docs.expo.dev/router/introduction).

## Get a fresh project

When you're ready, run:

```bash
npm run reset-project
```

This command will move the starter code to the **app-example** directory and create a blank **app** directory where you can start developing.

## Learn more

To learn more about developing your project with Expo, look at the following resources:

- [Expo documentation](https://docs.expo.dev/): Learn fundamentals, or go into advanced topics with our [guides](https://docs.expo.dev/guides).
- [Learn Expo tutorial](https://docs.expo.dev/tutorial/introduction/): Follow a step-by-step tutorial where you'll create a project that runs on Android, iOS, and the web.

## Join the community

Join our community of developers creating universal apps.

- [Expo on GitHub](https://github.com/expo/expo): View our open source platform and contribute.
- [Discord community](https://chat.expo.dev): Chat with Expo users and ask questions.
48 changes: 48 additions & 0 deletions frontend-challenge/tennis-court-app/app.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
{
"expo": {
"name": "tennis-court-app",
"slug": "tennis-court-app",
"version": "1.0.0",
"orientation": "portrait",
"icon": "./assets/images/icon.png",
"scheme": "tenniscourtapp",
"userInterfaceStyle": "automatic",
"newArchEnabled": true,
"ios": {
"supportsTablet": true
},
"android": {
"adaptiveIcon": {
"backgroundColor": "#E6F4FE",
"foregroundImage": "./assets/images/android-icon-foreground.png",
"backgroundImage": "./assets/images/android-icon-background.png",
"monochromeImage": "./assets/images/android-icon-monochrome.png"
},
"edgeToEdgeEnabled": true,
"predictiveBackGestureEnabled": false
},
"web": {
"output": "static",
"favicon": "./assets/images/favicon.png"
},
"plugins": [
"expo-router",
[
"expo-splash-screen",
{
"image": "./assets/images/splash-icon.png",
"imageWidth": 200,
"resizeMode": "contain",
"backgroundColor": "#ffffff",
"dark": {
"backgroundColor": "#000000"
}
}
]
],
"experiments": {
"typedRoutes": true,
"reactCompiler": true
}
}
}
21 changes: 21 additions & 0 deletions frontend-challenge/tennis-court-app/app/_layout.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { DarkTheme, DefaultTheme, ThemeProvider } from '@react-navigation/native';
import { Stack } from 'expo-router';
import { StatusBar } from 'expo-status-bar';
import 'react-native-reanimated';
import { useColorScheme } from '@/hooks/use-color-scheme';
import "../global.css";

export default function RootLayout() {
const colorScheme = useColorScheme();

return (
<ThemeProvider value={colorScheme === 'dark' ? DarkTheme : DefaultTheme}>
<Stack>
<Stack.Screen name="index" options={{ title: 'Tennis Courts' }} />
<Stack.Screen name="courts/[id]" options={{ title: 'Court Details' }} />
<Stack.Screen name="modal" options={{ presentation: 'modal', title: 'Review' }} />
</Stack>
<StatusBar style="auto" />
</ThemeProvider>
);
}
71 changes: 71 additions & 0 deletions frontend-challenge/tennis-court-app/app/courts/[id].tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import { View, Text, Image, ScrollView, TouchableOpacity, Alert } from 'react-native';
import { useLocalSearchParams, Stack, Link } from 'expo-router';
import { COURTS } from '../../constants/courts';

export default function CourtDetailScreen() {
const { id } = useLocalSearchParams();
const court = COURTS.find((c) => c.id === id);

if (!court) {
return (
<View className="flex-1 items-center justify-center bg-white dark:bg-black">
<Text className="text-black dark:text-white">Court not found</Text>
</View>
);
}

return (
<ScrollView className="flex-1 bg-white dark:bg-black">
<Stack.Screen options={{ title: court.name, headerBackTitle: 'Back' }} />

<Image source={{ uri: court.image }} className="w-full h-64" />

<View className="p-4 space-y-4">
<View className="flex-row justify-between items-start">
<View className="flex-1">
<Text className="text-2xl font-bold text-black dark:text-white mb-1">{court.name}</Text>
<Text className="text-gray-500 dark:text-gray-400 text-base">{court.location}</Text>
</View>
<View className="bg-green-100 dark:bg-green-900 px-3 py-1 rounded-lg">
<Text className="text-green-800 dark:text-green-100 font-bold text-lg">{court.rating} ★</Text>
</View>
</View>

<View className="flex-row space-x-4">
<View className="bg-gray-100 dark:bg-gray-800 px-3 py-2 rounded-lg">
<Text className="text-xs text-gray-500 dark:text-gray-400 uppercase font-bold">Surface</Text>
<Text className="text-black dark:text-white font-semibold">{court.surface}</Text>
</View>
<View className="bg-gray-100 dark:bg-gray-800 px-3 py-2 rounded-lg">
<Text className="text-xs text-gray-500 dark:text-gray-400 uppercase font-bold">Price</Text>
<Text className="text-black dark:text-white font-semibold">${court.price}/hr</Text>
</View>
</View>

<Text className="text-gray-700 dark:text-gray-300 leading-6 text-base">
{court.description}
</Text>

<View className="mt-6 border-t border-gray-100 dark:border-gray-800 pt-6">
<Text className="text-xl font-bold text-black dark:text-white mb-4">Reviews ({court.reviewCount})</Text>
{/* Mock Reviews List */}
{[1, 2, 3].map((i) => (
<View key={i} className="mb-4 p-3 bg-gray-50 dark:bg-gray-900 rounded-lg">
<View className="flex-row justify-between mb-1">
<Text className="font-semibold text-black dark:text-white">User {i}</Text>
<Text className="text-yellow-500">★★★★★</Text>
</View>
<Text className="text-gray-600 dark:text-gray-400">Great court! Highly recommended.</Text>
</View>
))}
</View>

<Link href="/modal" asChild>
<TouchableOpacity className="bg-blue-600 p-4 rounded-xl items-center mt-4 mb-8 active:opacity-90">
<Text className="text-white font-bold text-lg">Write a Review</Text>
</TouchableOpacity>
</Link>
</View>
</ScrollView>
);
}
61 changes: 61 additions & 0 deletions frontend-challenge/tennis-court-app/app/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import { useState } from 'react';
import { View, FlatList, TextInput, Text, SafeAreaView, Platform, StatusBar as RNStatusBar } from 'react-native';
import { Stack } from 'expo-router';
import CourtCard from '../components/CourtCard';
import { COURTS, Court } from '../constants/courts';

export default function CourtListScreen() {
const [search, setSearch] = useState('');
const [filteredCourts, setFilteredCourts] = useState<Court[]>(COURTS);

const handleSearch = (text: string) => {
setSearch(text);
if (text) {
const lower = text.toLowerCase();
const filtered = COURTS.filter(
(c) =>
c.name.toLowerCase().includes(lower) ||
c.location.toLowerCase().includes(lower)
);
setFilteredCourts(filtered);
} else {
setFilteredCourts(COURTS);
}
};

return (
<SafeAreaView className="flex-1 bg-gray-50 dark:bg-black pt-2">
<Stack.Screen options={{ headerShown: false }} />
<View
className="px-4 pb-2 bg-white dark:bg-black"
style={{
paddingTop: Platform.OS === "android" ? RNStatusBar.currentHeight : 0
}}
>
<Text className="text-3xl font-bold text-black dark:text-white mb-4 mt-2">Find a Court</Text>
<TextInput
className="bg-gray-100 dark:bg-gray-800 text-black dark:text-white px-4 py-3 rounded-xl border border-gray-200 dark:border-gray-700"
placeholder="Search courts..."
placeholderTextColor="#9CA3AF"
value={search}
onChangeText={handleSearch}
/>
</View>

<FlatList
data={filteredCourts}
keyExtractor={(item) => item.id}
renderItem={({ item }) => <CourtCard court={item} />}
contentContainerStyle={{ padding: 16 }}
className="flex-1"
initialNumToRender={10}
windowSize={5}
ListEmptyComponent={
<View className="flex-1 items-center justify-center mt-20">
<Text className="text-gray-500 dark:text-gray-400 text-lg">No courts found</Text>
</View>
}
/>
</SafeAreaView>
);
}
Loading