The first internationalized interactive map designed for a city in China, featuring rich points of interest information, searchable catalog, travel guides, and trip suggestions. Built to break down language barriers and make local tourism more accessible for foreigners.
Visit the live app at shenzhengo.net
Features • Development • Contribution • Data • Deployment • Roadmap • License
Interactive Map | Dynamic Regions | Directions and Street View |
---|---|---|
Explore Shenzhen through a user-friendly interactive map with clickable markers. These markers provide detailed information such as name, category, description, and recommendations for each place. | The map dynamically adjusts to display the most relevant places based on your current zoom level. Zoom into a specific area and the region tab will automatically switch to show that area. | Get directions to any place using your current location and view the street view with a single click. These options currently redirect to Chinese navigation apps (no need to input Chinese characters manually). |
Dark Mode: ShenzhenGo adapts to your system's light or dark mode for a comfortable browsing experience.
Mobile Friendly: The app is optimized for all devices, including mobile phones and tablets.
To set up the project locally, follow these steps:
Clone the repository and install dependencies:
git clone https://github.com/kapuic/shenzhengo.git
cd shenzhengo
bun install
Create a file named .dev.vars
in the root of the project with the following content:
ENVIRONMENT=development
GROWTHBOOK_API_HOST="https://features-control.kapui.net" # Replace with your own hostname
GROWTHBOOK_CLIENT_KEY="" # Add your own key which should look like "sdk-iFs3N1jgRATLVc8"
GROWTHBOOK_DECRYPTION_KEY="" # Add your own key which should look like "eqQS5NaK49tkYHIGPQ+MAA=="
AMAP_API_KEY="" # Add your own key which should look like "862dd28d460675460adcda630bbd941c"
AMAP_API_VERSION = "2.0.5"
-
GROWTHBOOK_API_HOST
: The hostname for GrowthBook, a feature management and A/B testing platform. This variable points to your GrowthBook API server, which handles feature flags and experiments. Replace this with your own host to properly configure feature management. -
GROWTHBOOK_CLIENT_KEY
: The SDK client key provided by GrowthBook. This key is essential for communicating with the GrowthBook API and fetching feature flags and experiments. Without this key, the app won't be able to access GrowthBook features, which may cause errors or result in missing functionality. -
GROWTHBOOK_DECRYPTION_KEY
: The decryption key used for securely evaluating feature flags if you choose the Ciphered option for SDK Payload Security in GrowthBook. This key is required if you decide to use encrypted payloads. If you're using Remote Evaluation for feature flags, this key is not required and can be left empty. In such cases, make sure to configure your SDK for Remote Evaluation via GrowthBook's documentation.The GrowthBook environment variables must be set up correctly for feature flags, A/B testing, and experiments. For setup, refer to GrowthBook documentation. Misconfiguration or failure to provide these variables can cause the app to throw errors or disable some features. If you are not using GrowthBook or wish to remove feature flags, consider forking the repository and removing references to GrowthBook, including
GrowthBookProvider
and any usage ofuseFeature
hooks.To configure your SDK for Remote Evaluation (which does not require the
GROWTHBOOK_DECRYPTION_KEY
), select the Remote Evaluated option in the SDK Payload Security settings in GrowthBook. If you choose to use the Ciphered option for SDK Payload Security, you must provide a valid decryption key. In this case, modify theapp/root.tsx
file by commenting out theclientKey
ordecryptionKey
, depending on your security setup. -
AMAP_API_KEY
: The API key for AMap, which is a Chinese map service provider. This key is required for accessing AMap's base layer data within the app. You must sign up for a developer account with AMap and generate your API key by following the instructions in their documentation: AMap JavaScript API V2 Documentation.AMap's base layer data includes minimal map data, such as streets, outlines of buildings, and other foundational geographic information. The project does not use other data, such as points of interest from AMap, as the objective is to create an internationalized map for foreign users by incorporating customized and tailored English data.
-
AMAP_API_VERSION
: Specifies the version of the AMap JavaScript API that the app is using. Currently, the project is set to version2.0.5
, but this should be kept up-to-date with the latest stable release from AMap.
Start the development server using Vite:
bun dev
You can now access the app at http://127.0.0.1:8788.
To emulate the Cloudflare runtime:
bun run build
bun start
We welcome contributions from the community! Below are guidelines for adding places, categories, and activities, as well as general contribution guidelines.
- Fork the repository.
- Create a new branch for your feature or bug fix.
- Commit your changes.
- Submit a pull request for review.
We use Prettier for code formatting and ESLint for linting. To ensure your code follows the project's coding standards, use the following commands:
- Formatting: Run
bun format
to automatically format your code using Prettier. - Linting: Run
bun lint
to check for and fix linting issues using ESLint.
If you are using Visual Studio Code, we recommend installing the Prettier and ESLint extensions for automatic formatting and linting in your editor.
A place represents a specific point of interest on the map, such as a park, restaurant, or landmark. Each place is identified by its geographical location (latitude and longitude) and is associated with a range (region) and category. Places contain additional details such as descriptions, images, street view links, and optionally signature dishes for restaurants. These properties provide a rich, informative experience for users exploring the city.
To add new places, modify the app/data/places.ts
file. Each place follows the Place
interface from app/data/schema.ts
.
-
Required Fields:
-
location
: Latitude and longitude coordinates of the place.location
uniquely identifies a place and is required. It is a tuple of latitude and longitude. You can use the AMap Coordinates Picker to obtain the coordinates for a place. Other tools may not work as expected since China uses a special coordinate system called GCJ-02, and AMap uses a variant of this system called BD-09. While conversion between these systems is possible, it is safer to use AMap's tools to get the coordinates directly. -
name
: Name of the place in English. -
originalName
: Original name of the place in Chinese. -
rangeId
: The ID of the range (region) where this place is located. -
categoryId
: The ID of the category this place belongs to (e.g., restaurant, beach).
-
-
Optional Fields:
-
description
: A brief description of the place. -
coverImage
: URL for a cover image.Cover images are stored in the
public/images
directory. To add a new cover image, place it in the appropriate subdirectorypublic/images/place-name/cover.jpg
and use the relative path/images/place-name/cover.jpg
in thecoverImage
field. -
signatureDishes
: List of signature dishes if the place is a restaurant (include dish name, optional price, and image).Similar to cover images, images for signature dishes are stored in the public/images directory. To add a new image, place it in the appropriate subdirectory
public/images/place-name/1.jpg
. These images are currently named in numeric order starting from 1. Use the relative path, such as/images/place-name/1.jpg
, in theimage
field. -
streetViewUrl
: URL for the street view.streetViewUrl
enables the Street View feature for a place, showing a button that opens the Street View of the location. You can use Baidu Maps's Street View feature to obtain the URL for a place. You may also use other providers, ideally those accessible in China. When adding a Baidu Street View URL, ensure to remove any unnecessary query parameters:- Replace
https://map.baidu.com/(.+)#
→https://map.baidu.com/#
- Delete
&tn=B_NORMAL_MAP&sc=0&newmap=1&shareurl=1
- Delete
&ugc_type=3&ugc_ver=1&device_ratio=2&compat=1&pcevaname=pc4.1&querytype=detailConInfo&da_src=shareurl
- Delete
&pid=([^&"]+)
- Delete
,\d+z,\d+t,[\d.]+h
- Replace
-
authors
: Author(s) who contributed this place (can be a string or an array of strings).
-
Example Entry
{ location: [114.308527, 22.596445], name: "Nayuki", originalName: "奈雪的茶", rangeId: "meisha", categoryId: "beverage", description: "This is a place where you can drink milk teas and eat breads.", coverImage: "/images/nayuki/cover.jpeg", signatureDishes: [ { name: "Brown Sugar Boba Tea", originalName: "黑糖珠珠宝藏茶", price: 21, image: "/images/nayuki/1.jpeg", }, { name: "Strawberry Creamed Bread", originalName: "草莓魔法棒", price: 20, image: "/images/nayuki/2.jpeg", }, { name: "Grape Fruit Tea with Cheese", originalName: "霸气芝士葡萄", price: 28, image: "/images/nayuki/3.jpeg", }, ], streetViewUrl: "https://map.baidu.com/#panoid=2300570012221204205405903OI&panotype=street&heading=335.04&pitch=0&l=21&psp=%7B%22PanoModule%22%3A%7B%22markerUid%22%3A%221bd1f8675627201c081cc795%22%7D%7D", authors: "Rachel", }
A category groups places by type, such as restaurants, parks, or historical landmarks. It also provides visual elements like colors and icons for the map.
To add new categories, modify the app/data/categories.ts file. Each category follows the Category
interface from app/data/schema.ts.
-
Required Fields:
id
: A unique string identifier for the category.name
: The display name of the category.colors
: An object defining the color for the category, both for light and dark modes.
-
Optional Fields:
-
markerIcon
: An optional icon representing the category on the map. If not specified, it defaults to"other"
.Marker icons are stored in the public/assets/markers directory and the
public/assets/markers-(number)x
directories for different sizes. All icons are currently extracted from public/assets/amap-markers.png. Larger marker icons (e.g.,2x
and4x
) are created using the open-source desktop version of Upscayl. To add a new marker icon, place it in the appropriate subdirectorypublic/assets/markers/(category-id).png
, and use the relative path/assets/markers/(category-id).png
in themarkerIcon
field. -
parentId
: An optional string that links the category to a parent category.
-
Icons are an essential part of visualizing categories on the map. The available icons can be found at Tabler Icons. For each category, you need to import the corresponding icon from @tabler/icons-react
and assign it to the category in the categoryIcons
object.
-
Visit Tabler Icons and find an appropriate icon for your category.
-
Import the icon into the
categories.ts
file. For example, for a restaurant category:import { IconChefHat } from "@tabler/icons-react";
-
Add the icon to the
categoryIcons
object in the same file:export const categoryIcons: Record<string, ForwardRefExoticComponent<Omit<IconProps, "ref"> & RefAttributes<Icon>>> = { ... restaurant: IconChefHat, ... };
Example Entry
{ id: "apartment-rental", name: "Apartment Rental", colors: { light: "#B6C4D0", dark: "#7F92A1" }, markerIcon: "other", parentId: "other", }import { IconTrees } from "@tabler/icons-react"; export const categoryIcons: Record<string, ForwardRefExoticComponent<Omit<IconProps, "ref"> & RefAttributes<Icon>>> = { ... "apartment-rental": IconHomeSearch, ... };
A range defines a geographical area, such as neighborhoods, districts, or regions of Shenzhen. Ranges help structure the map's display based on zoom levels and can also have a hierarchical structure.
To add new ranges, modify the app/data/ranges.ts file. Each range follows the Range
interface from app/data/schema.ts.
-
Required Fields:
id
: A unique string identifier for the range.name
: The display name of the range.
-
Optional Fields:
description
: A brief description of the range.zoom
: The default zoom level (defaults to15
).zooms
: A tuple of two numbers defining the minimum and maximum zoom levels at which the range is visible (defaults to[11, 18]
).branding
: Custom branding options for the range, such as custom app names or icons.parentId
: An optional string that links the range to a parent range.showChildren
: A boolean that controls whether child ranges are shown by default (defaults tofalse
).
Example Entry
{ id: "yantian", name: "Yantian", zoom: 13, showChildren: true, parentId: "shenzhen", }
An activity represents a specific type of leisure or adventure tourists can engage in, such as hiking, surfing, or shopping. Activities can be linked to places, categories, and vocabulary cards to help visitors engage with the local culture.
To add new activities, modify the app/data/activities.ts file. Each activity follows the Activity
interface from app/data/schema.ts.
-
Required Fields:
id
: A unique string identifier for the activity.name
: The display name of the activity.
-
Optional Fields:
categoryIds
: An array of category IDs to which the activity belongs.placeLocations
: An array of coordinates linking specific places to the activity.vocab
: An array of vocabulary cards, where each entry contains the English word, its Chinese translation, and the pinyin (including tone marks) for pronunciation.
Example Entry
{ id: "hiking", name: "Hiking", placeLocations: [ [114.300844, 22.600616], [114.312739, 22.59973], [114.307954, 22.592429], ], categoryIds: ["walkway"], vocab: [ { name: "Trail/Walk way", chinese: "栈道", pinyin: "Zhàndào" }, { name: "distance", chinese: "距离", pinyin: "Jùlí" }, { name: "Map", chinese: "地图", pinyin: "Dìtú" }, { name: "Compass", chinese: "指南针", pinyin: "Zhǐnánzhēn" }, { name: "Backpack", chinese: "背包", pinyin: "Bèibāo" }, { name: "Boots", chinese: "靴子", pinyin: "Xuēzi" }, { name: "Trekking Poles", chinese: "徒步行走杆", pinyin: "Túbù xíngzǒu gǎn", }, { name: "Camping", chinese: "露营", pinyin: "Lùyíng" }, { name: "Tent", chinese: "帐篷", pinyin: "Zhàngpéng" }, { name: "Wildlife", chinese: "野生动物", pinyin: "Yěshēng dòngwù" }, { name: "Elevation", chinese: "海拔", pinyin: "Hǎibá" }, { name: "Summit", chinese: "山顶", pinyin: "Shāndǐng" }, { name: "Landscape", chinese: "风景", pinyin: "Fēngjǐng" }, { name: "Terrain", chinese: "地形", pinyin: "Dìxíng" }, { name: "Rain Gear", chinese: "防雨装备", pinyin: "Fángyǔ zhuāngbèi", }, { name: "First Aid Kit", chinese: "急救包", pinyin: "Jíjiù bāo" }, { name: "Hydration", chinese: "水分补充", pinyin: "Shuǐfèn bǔchōng", }, { name: "Snacks", chinese: "小吃", pinyin: "Xiǎochī" }, { name: "Navigation", chinese: "导航", pinyin: "Dǎoháng" }, { name: "Sunscreen", chinese: "防晒霜", pinyin: "Fángshài shuāng" }, { name: "Bug Spray", chinese: "驱虫喷雾", pinyin: "Qūchóng pēnwù" }, { name: "Weather", chinese: "天气", pinyin: "Tiānqì" }, ], }
Before deploying the app, ensure that all environment variables are properly set. Please refer to the Environment Variables section for details on configuring the necessary keys. For deployment, make sure to set ENVIRONMENT=production
and use the appropriate GrowthBook SDK credentials specific to your production environment.
To build and deploy the app, follow these steps:
-
Build the project for production:
bun run build
-
Deploy the project:
bun deploy
Alternatively, you can deploy the app using the Cloudflare dashboard. This method allows for a more user-friendly deployment experience directly through Cloudflare's interface. To learn more about deploying your app through the Cloudflare Pages dashboard, refer to the official documentation: Cloudflare Pages Framework Guide for Remix.
We plan to add the following features in future versions:
- Multi-language support (beyond English and Chinese).
- Offline map support for travelers without reliable internet access.
- User-generated reviews and ratings for places.
This project is fair-code licensed under the Sustainable Use License.
All data and written content, including this README, is licensed under the Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International License.