├── .env.development ├── .eslintrc.json ├── .github └── workflows │ ├── build.yml │ └── merge.yml ├── .gitignore ├── .prettierignore ├── .prettierrc.json ├── .vscode └── launch.json ├── LICENSE ├── README.md ├── context └── AppContextProvider.tsx ├── i18n ├── i18n.ts └── request.ts ├── jest.config.ts ├── logger.config.mjs ├── messages ├── ar.json ├── en-GB.json ├── en.json ├── es.json ├── ewe-BJ.json ├── fon-BJ.json ├── fr.json ├── hau-NG.json ├── ja.json ├── ko.json ├── vi.json ├── yor-NG.json ├── zh-CN.json └── zh-TW.json ├── next.config.mjs ├── package-lock.json ├── package.json ├── postcss.config.mjs ├── public ├── android-chrome-192x192.png ├── android-chrome-512x512.png ├── apple-touch-icon.png ├── background.svg ├── default.png ├── favicon-16x16.png ├── favicon-32x32.png ├── favicon.ico ├── images │ ├── business │ │ ├── add-item-button.png │ │ ├── product.png │ │ └── upload.jpg │ ├── icons │ │ ├── crosshair.png │ │ ├── map-of-pi-icon.png │ │ ├── map_centers_crosshair.png │ │ ├── map_of_pi_logo.jpeg │ │ └── scope.png │ ├── logo.svg │ └── shared │ │ ├── my_location.png │ │ ├── review_ratings │ │ ├── trust-o-meter_000.PNG │ │ ├── trust-o-meter_050.PNG │ │ ├── trust-o-meter_080.PNG │ │ └── trust-o-meter_100.PNG │ │ ├── sidebar │ │ ├── close.svg │ │ ├── contact.svg │ │ ├── dark.svg │ │ ├── info.svg │ │ ├── language.svg │ │ ├── light.svg │ │ ├── location.svg │ │ ├── question.svg │ │ ├── settings.svg │ │ ├── theme.svg │ │ └── user.svg │ │ ├── social-media │ │ ├── E-mail_icon.svg │ │ ├── discord-icon.png │ │ ├── email-icon.png │ │ ├── email-icon.svg │ │ ├── email-logo.png │ │ ├── facebook.icon.png │ │ ├── instagram-icon.png │ │ ├── map-of-pi-logo.svg │ │ ├── tiktok-icon.svg │ │ ├── x-icon.png │ │ └── youtube-icon.png │ │ ├── upload.png │ │ └── upload_old.png └── validation-key.txt ├── sentry.client.config.mjs ├── src ├── app │ ├── [locale] │ │ ├── layout.tsx │ │ ├── map-center │ │ │ └── page.tsx │ │ ├── page.tsx │ │ └── seller │ │ │ ├── registration │ │ │ └── page.tsx │ │ │ ├── reviews │ │ │ ├── [id] │ │ │ │ └── page.tsx │ │ │ ├── feedback │ │ │ │ └── [id] │ │ │ │ │ └── page.tsx │ │ │ └── util │ │ │ │ └── ratingUtils.ts │ │ │ └── sale-items │ │ │ └── [id] │ │ │ └── page.tsx │ ├── _not-found │ │ └── page.tsx │ ├── global-error.tsx │ ├── global.css │ ├── layout.tsx │ ├── page.tsx │ └── providers.tsx ├── components │ ├── shared │ │ ├── About │ │ │ ├── Info │ │ │ │ ├── Info.module.css │ │ │ │ └── Info.tsx │ │ │ ├── privacy-policy │ │ │ │ ├── PrivacyPolicy.module.css │ │ │ │ └── PrivacyPolicy.tsx │ │ │ └── terms-of-service │ │ │ │ ├── TermOfService.module.css │ │ │ │ └── TermsOfService.tsx │ │ ├── Forms │ │ │ ├── Buttons │ │ │ │ ├── Buttons.module.css │ │ │ │ └── Buttons.tsx │ │ │ └── Inputs │ │ │ │ ├── Inputs.module.css │ │ │ │ └── Inputs.tsx │ │ ├── Review │ │ │ ├── TrustMeter.tsx │ │ │ └── emojipicker.tsx │ │ ├── SearchBar │ │ │ ├── SearchBar.scss │ │ │ └── SearchBar.tsx │ │ ├── Seller │ │ │ ├── ShopItem.tsx │ │ │ └── ToggleCollapse.tsx │ │ ├── confirm.tsx │ │ ├── map │ │ │ ├── Map.tsx │ │ │ ├── MapCenter.css │ │ │ ├── MapCenter.tsx │ │ │ ├── MapMarkerPopup.tsx │ │ │ └── RecenterAutomatically.tsx │ │ ├── navbar │ │ │ ├── Navbar.module.css │ │ │ └── Navbar.tsx │ │ └── sidebar │ │ │ ├── sidebar.module.css │ │ │ └── sidebar.tsx │ └── skeleton │ │ ├── Sidebar.tsx │ │ ├── seller │ │ ├── Registration.tsx │ │ ├── Review.tsx │ │ └── SellerItem.tsx │ │ ├── skeleton.css │ │ └── skeleton.tsx ├── config │ └── client.ts ├── constants │ ├── coordinates.ts │ ├── demoAPI.ts │ ├── menu.ts │ ├── pi.ts │ ├── placeholders.ts │ └── types.ts ├── middleware.ts ├── navigation.ts ├── services │ ├── mapCenterApi.ts │ ├── reviewsApi.ts │ ├── sellerApi.ts │ ├── toggleApi.ts │ └── userSettingsApi.ts ├── typedefs.d.ts └── utils │ ├── api.ts │ ├── auth.ts │ ├── date.ts │ ├── geolocation.ts │ ├── map.ts │ └── sanitize.ts ├── tailwind.config.ts └── tsconfig.json /.env.development: -------------------------------------------------------------------------------- 1 | # This .env file is a template for environment variables. 2 | # Only add new variables or update existing ones here, but DO NOT include actual values. 3 | # Use placeholders in the form of "ADD YOUR $" where $ represents the purpose of the variable. 4 | # Your environment-specific values should be set in your .env.local file, which is not tracked in version control. 5 | 6 | NODE_ENV=development 7 | NEXT_PUBLIC_API_URL=http://localhost:8001/api/v1 8 | NEXT_PUBLIC_PI_SDK_URL=https://sdk.minepi.com/pi-sdk.js 9 | 10 | NEXT_PUBLIC_SENTRY_DSN="ADD YOUR SENTRY DSN" -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "ignorePatterns": ["node_modules/", ".next/"], 3 | "extends": "next", 4 | "rules": { 5 | "react/no-unescaped-entities": "off", 6 | "@next/next/no-page-custom-font": "off", 7 | "react-hooks/exhaustive-deps": "off" 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Map of Pi Build 2 | 3 | # Controls when the workflow will execute 4 | on: 5 | # Triggers the workflow on push or pull request events for the "main" and "dev" branch 6 | push: 7 | branches: 8 | - main 9 | - dev 10 | pull_request: 11 | branches: 12 | - main 13 | - dev 14 | 15 | # Allows you to run this workflow manually from the Actions tab 16 | workflow_dispatch: 17 | 18 | jobs: 19 | build: 20 | name: 🏗️ Build 21 | runs-on: ubuntu-latest 22 | # The sequence of tasks that will be executed as part of the job 23 | steps: 24 | - name: Checkout code 25 | uses: actions/checkout@v3 26 | 27 | - name: Install Node 28 | uses: actions/setup-node@v3 29 | with: 30 | node-version: 18.x 31 | 32 | # Frontend CI/CD Process 33 | - name: Install Dependencies for Frontend 34 | run: npm ci 35 | 36 | - name: Build Frontend 37 | run: npm run build 38 | 39 | - name: Linting Frontend 40 | run: npm run lint 41 | 42 | - name: Execute Tests for Frontend 43 | run: npm run test -- --passWithNoTests 44 | -------------------------------------------------------------------------------- /.github/workflows/merge.yml: -------------------------------------------------------------------------------- 1 | name: Map of Pi Merge 2 | 3 | # Controls when the workflow will execute 4 | on: 5 | # Triggers the workflow when build successfully completes 6 | workflow_run: 7 | workflows: ["Map of Pi Build"] 8 | types: 9 | - completed 10 | 11 | # Allows you to run this workflow manually from the Actions tab 12 | workflow_dispatch: 13 | 14 | jobs: 15 | merge: 16 | name: 🔀 Merge 17 | runs-on: ubuntu-latest 18 | if: ${{ github.event.workflow_run.conclusion == 'success' && github.event.workflow_run.head_branch == 'dev' }} 19 | 20 | # The sequence of tasks that will be executed as part of the job 21 | steps: 22 | - name: Checkout code 23 | uses: actions/checkout@v3 24 | 25 | - name: Merge dev to main 26 | uses: repo-sync/pull-request@v2 27 | with: 28 | github_token: ${{ secrets.GITHUB_TOKEN }} 29 | source_branch: "dev" 30 | destination_branch: "main" 31 | pr_title: "Automated PR to merge dev to main" 32 | pr_body: ":robot: Automated PR from **dev** to **main**; see commits." 33 | pr_label: "auto-pr" 34 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # Compiled output 4 | /dist 5 | 6 | # dependencies 7 | /node_modules 8 | 9 | # testing 10 | /coverage 11 | 12 | # next.js 13 | /.next/ 14 | /out/ 15 | 16 | # production 17 | /build 18 | 19 | # misc 20 | .DS_Store 21 | *.pem 22 | 23 | # debug 24 | npm-debug.log* 25 | yarn-debug.log* 26 | yarn-error.log* 27 | 28 | # local env files 29 | .env 30 | .env.local 31 | 32 | # vercel 33 | .vercel 34 | 35 | # typescript 36 | *.tsbuildinfo 37 | next-env.d.ts 38 | 39 | # Local Netlify folder 40 | .netlify 41 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | # See http://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # Compiled output 4 | /build 5 | /.next 6 | 7 | # Node 8 | /node_modules 9 | 10 | # IDEs and editors 11 | .idea/ 12 | .project 13 | .classpath 14 | .c9/ 15 | *.launch 16 | .settings/ 17 | *.sublime-workspace 18 | 19 | # Visual Studio Code 20 | .vscode/* 21 | !.vscode/settings.json 22 | !.vscode/tasks.json 23 | !.vscode/launch.json 24 | !.vscode/extensions.json 25 | .history/* 26 | 27 | # Miscellaneous 28 | /coverage 29 | 30 | # System files 31 | .DS_Store 32 | Thumbs.db 33 | -------------------------------------------------------------------------------- /.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "tabWidth": 2, 3 | "useTabs": false, 4 | "singleQuote": true, 5 | "semi": true, 6 | "bracketSpacing": true, 7 | "arrowParens": "always", 8 | "bracketSameLine": true, 9 | "endOfLine": "auto" 10 | } 11 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.2.0", 3 | "configurations": [ 4 | { 5 | "name": "Next.js: debug client-side", 6 | "type": "chrome", 7 | "request": "launch", 8 | "url": "http://localhost:4200" 9 | }, 10 | ] 11 | } 12 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | PiOS License 2 | 3 | Copyright (C) 2024 Map Of Pi Team 4 | 5 | Permission is hereby granted by the application software developer (“Software Developer”), free 6 | of charge, to any person obtaining a copy of this application, software and associated 7 | documentation files (the “Software”), which was developed by the Software Developer for use on 8 | Pi Network, whereby the purpose of this license is to permit the development of derivative works 9 | based on the Software, including the right to use, copy, modify, merge, publish, distribute, 10 | sub-license, and/or sell copies of such derivative works and any Software components incorporated 11 | therein, and to permit persons to whom such derivative works are furnished to do so, in each case, 12 | solely to develop, use and market applications for the official Pi Network. For purposes of this 13 | license, Pi Network shall mean any application, software, or other present or future platform 14 | developed, owned or managed by Pi Community Company, and its parents, affiliates or subsidiaries, 15 | for which the Software was developed, or on which the Software continues to operate. However, 16 | you are prohibited from using any portion of the Software or any derivative works thereof in any 17 | manner (a) which infringes on any Pi Network intellectual property rights, (b) to hack any of Pi 18 | Network’s systems or processes or (c) to develop any product or service which is competitive with 19 | the Pi Network. 20 | 21 | The above copyright notice and this permission notice shall be included in all copies or 22 | substantial portions of the Software. 23 | 24 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, 25 | INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE 26 | AND NON-INFRINGEMENT. IN NO EVENT SHALL THE AUTHORS, PUBLISHERS, OR COPYRIGHT HOLDERS OF THIS 27 | SOFTWARE BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY OR CONSEQUENTIAL 28 | DAMAGES (INCLUDING, BUT NOT LIMITED TO BUSINESS INTERRUPTION, LOSS OF USE, DATA OR PROFITS) 29 | HOWEVER CAUSED AND UNDER ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR 30 | TORT (INCLUDING NEGLIGENCE) ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE 31 | OR OTHER DEALINGS IN THE SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. 32 | 33 | Pi, Pi Network and the Pi logo are trademarks of the Pi Community Company. 34 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

Map of Pi

2 | 3 |
4 | 5 | [![Hackathon](https://img.shields.io/badge/hackathon-PiCommerce-purple.svg)](https://github.com/pi-apps/PiOS/blob/main/pi-commerce.md) 6 | ![Status](https://img.shields.io/badge/status-active-success.svg) 7 | [![Netlify Status](https://api.netlify.com/api/v1/badges/4d4c5ac2-b6f0-453e-b13c-891e92403749/deploy-status)](https://app.netlify.com/sites/mapofpi-react/deploys) 8 | ![License](https://img.shields.io/badge/license-PIOS-blue.svg) 9 | 10 |
11 | 12 |
13 |

Map of Pi is a mobile application developed to help Pi community members easily locate local businesses that accept Pi as payment. This project was initiated as part of the Pi Commerce Hackathon with the goal of facilitating Pi transactions and connecting businesses with the Pi community.

14 |
15 | 16 | ## Table of Contents 17 | 18 | - [Brand Design](#brand-design) 19 | - [Tech Stack](#tech-stack) 20 | - [Frontend Local Execution](#frontend-local-execution) 21 | - [Team](#team) 22 | - [Contributions](#contributions) 23 | 24 | ## Brand Design 25 | 26 | | App Logo | App Icon | 27 | | ------------- |:-------------:| 28 | | map-of-pi-logo-revised-3 | map-of-pi-app-icon-transparent 29 | 30 | ## Tech Stack 📊 31 | 32 | - **Frontend**: NextJS/ React, TypeScript, HTML, SCSS, CSS 33 | - **Backend**: Express/ NodeJS, REST API 34 | - **Database**: MongoDB 35 | - **DevOps**: GitHub Actions, Netlify, Vercel 36 | 37 | ## Frontend Local Execution 38 | 39 | The Map of Pi Front End is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app) which is a React framework to build web applications. 40 | 41 | ### Build the Project 42 | 43 | - Run `npm run build` to build the project; builds the app for production to the `.next` folder. 44 | - The build artifacts are bundled for production mode and optimized for the best performance. 45 | 46 | ### Execute the Development Server 47 | 48 | - Create .env.local file from the .env.development template and replace placeholders with actual values. 49 | - Execute `npm run dev` to spin up a dev server. 50 | - Navigate to http://localhost:4200/ in your browser. 51 | - Execute **[Backend Local Execution](https://github.com/map-of-pi/map-of-pi-backend-react/blob/dev/README.md#backend-local-execution)** for integration testing. 52 | - The application will automatically reload if you change any of the source files. 53 | - For local debugging in VS Code, attach the runtime server accordingly. 54 | - Lint errors will be displayed in the console. 55 | 56 | ### Execute Unit Tests 57 | 58 | - Run `npm run test` to launch the [Jest](https://jestjs.io/) Testing Framework in the interactive watch mode. 59 | 60 | ### Linting the Project 61 | 62 | - Run `npm run lint` for static code analysis. 63 | 64 | ## Team 🧑👩‍🦱🧔👨🏾‍🦱👨🏾 65 | 66 | ### Project Manager 67 | - Philip Jennings 68 | 69 | ### Marketing 70 | - Bonnie Ford 71 | - Joseph Ciccone 72 | 73 | ### Solution Design / UX 74 | - Femma Ashraf 75 | - Oluwabukola Adesina 76 | - Folorunsho Omotunde 77 | - Henry Fasakin 78 | 79 | ### Technical Lead/ DevOps 80 | - Danny Lee 81 | 82 | ### Technical Advisor 83 | - Zoltan Magyar 84 | 85 | ### Application Developers 86 | - Darin Hajou 87 | - Rokundo Soleil 88 | - Ayomikun Omotosho 89 | - Yusuf Adisa 90 | - Francis Mwaura 91 | - Samuel Oluyomi 92 | 93 | ## Contributions 94 | 95 |
96 |

We welcome contributions from the community to improve the Map of Pi project.

97 |
98 | -------------------------------------------------------------------------------- /context/AppContextProvider.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import 'react-toastify/dist/ReactToastify.css'; 4 | 5 | import { useTranslations } from 'next-intl'; 6 | import { 7 | createContext, 8 | useState, 9 | SetStateAction, 10 | ReactNode, 11 | useEffect 12 | } from 'react'; 13 | 14 | import { Pi } from '@pinetwork-js/sdk'; 15 | import axiosClient, {setAuthToken} from '@/config/client'; 16 | import { onIncompletePaymentFound } from '@/utils/auth'; 17 | import { AuthResult } from '@/constants/pi'; 18 | import { IUser } from '@/constants/types'; 19 | 20 | import logger from '../logger.config.mjs'; 21 | 22 | interface IAppContextProps { 23 | currentUser: IUser | null; 24 | setCurrentUser: React.Dispatch>; 25 | registerUser: () => void; 26 | autoLoginUser: ()=> void; 27 | isSigningInUser: boolean; 28 | reload: boolean; 29 | alertMessage: string | null; 30 | setAlertMessage: React.Dispatch>; 31 | showAlert: (message: string) => void; 32 | setReload: React.Dispatch>; 33 | isSaveLoading: boolean; 34 | setIsSaveLoading: React.Dispatch>; 35 | } 36 | 37 | const initialState: IAppContextProps = { 38 | currentUser: null, 39 | setCurrentUser: () => {}, 40 | registerUser: () => { }, 41 | autoLoginUser: ()=> {}, 42 | isSigningInUser: false, 43 | reload: false, 44 | alertMessage: null, 45 | setAlertMessage: () => {}, 46 | showAlert: () => {}, 47 | setReload: () => {}, 48 | isSaveLoading: false, 49 | setIsSaveLoading: () => {} 50 | }; 51 | 52 | export const AppContext = createContext(initialState); 53 | 54 | interface AppContextProviderProps { 55 | children: ReactNode; 56 | } 57 | 58 | const AppContextProvider = ({ children }: AppContextProviderProps) => { 59 | const t = useTranslations(); 60 | const [currentUser, setCurrentUser] = useState(null); 61 | const [isSigningInUser, setIsSigningInUser] = useState(false); 62 | const [reload, setReload] = useState(false); 63 | const [isSaveLoading, setIsSaveLoading] = useState(false); 64 | 65 | const [alertMessage, setAlertMessage] = useState(null); 66 | 67 | const showAlert = (message: string) => { 68 | setAlertMessage(message); 69 | setTimeout(() => { 70 | setAlertMessage(null); // Clear alert after 5 seconds 71 | }, 5000); 72 | }; 73 | 74 | const registerUser = async () => { 75 | logger.info('Initializing Pi SDK for user registration.'); 76 | await Pi.init({ version: '2.0', sandbox: process.env.NODE_ENV === 'development' }); 77 | let isInitiated = Pi.initialized; 78 | 79 | if (isInitiated) { 80 | try { 81 | setIsSigningInUser(true); 82 | const pioneerAuth: AuthResult = await window.Pi.authenticate(['username', 'payments'], onIncompletePaymentFound); 83 | const res = await axiosClient.post( 84 | "/users/authenticate", 85 | {}, // empty body 86 | { 87 | headers: { 88 | Authorization: `Bearer ${pioneerAuth.accessToken}`, 89 | }, 90 | } 91 | ); 92 | 93 | if (res.status === 200) { 94 | setAuthToken(res.data?.token); 95 | setCurrentUser(res.data.user); 96 | logger.info('User authenticated successfully.'); 97 | setTimeout(() => { 98 | setIsSigningInUser(false); // hide the splash screen after the delay 99 | }, 2500); 100 | } else if (res.status === 500) { 101 | setCurrentUser(null); 102 | logger.error('User authentication failed.'); 103 | setIsSigningInUser(false); 104 | } 105 | } catch (error) { 106 | logger.error('Error during user registration:', error); 107 | setIsSigningInUser(false); 108 | } 109 | } else { 110 | logger.error('PI SDK failed to initialize.'); 111 | } 112 | }; 113 | 114 | const autoLoginUser = async () => { 115 | logger.info('Attempting to auto-login user.'); 116 | try { 117 | setIsSigningInUser(true); 118 | const res = await axiosClient.get('/users/me'); 119 | 120 | if (res.status === 200) { 121 | logger.info('Auto-login successful.'); 122 | setCurrentUser(res.data); 123 | setTimeout(() => { 124 | setIsSigningInUser(false); // hide the splash screen after the delay 125 | }, 2500); 126 | } else { 127 | setCurrentUser(null); 128 | logger.warn('Auto-login failed.'); 129 | setIsSigningInUser(false); 130 | } 131 | } catch (error) { 132 | logger.error('Auto login unresolved; attempting Pi SDK authentication:', error); 133 | await registerUser(); 134 | } 135 | } 136 | 137 | useEffect(() => { 138 | logger.info('AppContextProvider mounted.'); 139 | if (!currentUser) { 140 | registerUser(); 141 | } else { 142 | autoLoginUser(); 143 | } 144 | }, []); 145 | 146 | return ( 147 | 148 | {children} 149 | 150 | ); 151 | }; 152 | 153 | export default AppContextProvider; 154 | -------------------------------------------------------------------------------- /i18n/i18n.ts: -------------------------------------------------------------------------------- 1 | // Can be imported from a shared config 2 | export const locales = [ 3 | 'ar', 4 | 'en', 5 | 'en-GB', 6 | 'es', 7 | 'ewe-BJ', 8 | 'fon-BJ', 9 | 'fr', 10 | 'hau-NG', 11 | 'yor-NG', 12 | 'ja', 13 | 'ko', 14 | 'vi', 15 | 'zh-CN', 16 | 'zh-TW' 17 | ] as const; 18 | 19 | export const defaultLocale = 'en'; -------------------------------------------------------------------------------- /i18n/request.ts: -------------------------------------------------------------------------------- 1 | import { notFound } from 'next/navigation'; 2 | import { getRequestConfig } from 'next-intl/server'; 3 | import { locales } from './i18n'; 4 | 5 | export default getRequestConfig(async ({ locale }) => { 6 | // Validate that the incoming `locale` parameter is valid 7 | if (!locales.includes(locale as any)) notFound(); 8 | 9 | return { 10 | messages: (await import(`../messages/${locale}.json`)).default, 11 | }; 12 | }); -------------------------------------------------------------------------------- /jest.config.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * For a detailed explanation regarding each configuration property, visit: 3 | * https://jestjs.io/docs/configuration 4 | */ 5 | 6 | import type {Config} from 'jest'; 7 | 8 | const config: Config = { 9 | // All imported modules in your tests should be mocked automatically 10 | // automock: false, 11 | 12 | // Stop running tests after `n` failures 13 | // bail: 0, 14 | 15 | // The directory where Jest should store its cached dependency information 16 | // cacheDirectory: "C:\\Users\\swoos\\AppData\\Local\\Temp\\jest", 17 | 18 | // Automatically clear mock calls, instances, contexts and results before every test 19 | clearMocks: true, 20 | 21 | // Indicates whether the coverage information should be collected while executing the test 22 | collectCoverage: true, 23 | 24 | // An array of glob patterns indicating a set of files for which coverage information should be collected 25 | // collectCoverageFrom: undefined, 26 | 27 | // The directory where Jest should output its coverage files 28 | coverageDirectory: "coverage", 29 | 30 | // An array of regexp pattern strings used to skip coverage collection 31 | // coveragePathIgnorePatterns: [ 32 | // "\\\\node_modules\\\\" 33 | // ], 34 | 35 | // Indicates which provider should be used to instrument code for coverage 36 | // coverageProvider: "babel", 37 | 38 | // A list of reporter names that Jest uses when writing coverage reports 39 | // coverageReporters: [ 40 | // "json", 41 | // "text", 42 | // "lcov", 43 | // "clover" 44 | // ], 45 | 46 | // An object that configures minimum threshold enforcement for coverage results 47 | // coverageThreshold: undefined, 48 | 49 | // A path to a custom dependency extractor 50 | // dependencyExtractor: undefined, 51 | 52 | // Make calling deprecated APIs throw helpful error messages 53 | // errorOnDeprecated: false, 54 | 55 | // The default configuration for fake timers 56 | // fakeTimers: { 57 | // "enableGlobally": false 58 | // }, 59 | 60 | // Force coverage collection from ignored files using an array of glob patterns 61 | // forceCoverageMatch: [], 62 | 63 | // A path to a module which exports an async function that is triggered once before all test suites 64 | // globalSetup: undefined, 65 | 66 | // A path to a module which exports an async function that is triggered once after all test suites 67 | // globalTeardown: undefined, 68 | 69 | // A set of global variables that need to be available in all test environments 70 | // globals: {}, 71 | 72 | // The maximum amount of workers used to run your tests. Can be specified as % or a number. E.g. maxWorkers: 10% will use 10% of your CPU amount + 1 as the maximum worker number. maxWorkers: 2 will use a maximum of 2 workers. 73 | // maxWorkers: "50%", 74 | 75 | // An array of directory names to be searched recursively up from the requiring module's location 76 | // moduleDirectories: [ 77 | // "node_modules" 78 | // ], 79 | 80 | // An array of file extensions your modules use 81 | // moduleFileExtensions: [ 82 | // "js", 83 | // "mjs", 84 | // "cjs", 85 | // "jsx", 86 | // "ts", 87 | // "tsx", 88 | // "json", 89 | // "node" 90 | // ], 91 | 92 | // A map from regular expressions to module names or to arrays of module names that allow to stub out resources with a single module 93 | // moduleNameMapper: {}, 94 | 95 | // An array of regexp pattern strings, matched against all module paths before considered 'visible' to the module loader 96 | // modulePathIgnorePatterns: [], 97 | 98 | // Activates notifications for test results 99 | // notify: false, 100 | 101 | // An enum that specifies notification mode. Requires { notify: true } 102 | // notifyMode: "failure-change", 103 | 104 | // A preset that is used as a base for Jest's configuration 105 | preset: 'ts-jest', 106 | 107 | // Run tests from one or more projects 108 | // projects: undefined, 109 | 110 | // Use this configuration option to add custom reporters to Jest 111 | // reporters: undefined, 112 | 113 | // Automatically reset mock state before every test 114 | // resetMocks: false, 115 | 116 | // Reset the module registry before running each individual test 117 | // resetModules: false, 118 | 119 | // A path to a custom resolver 120 | // resolver: undefined, 121 | 122 | // Automatically restore mock state and implementation before every test 123 | // restoreMocks: false, 124 | 125 | // The root directory that Jest should scan for tests and modules within 126 | // rootDir: undefined, 127 | 128 | // A list of paths to directories that Jest should use to search for files in 129 | // roots: [ 130 | // "" 131 | // ], 132 | 133 | // Allows you to use a custom runner instead of Jest's default test runner 134 | // runner: "jest-runner", 135 | 136 | // The paths to modules that run some code to configure or set up the testing environment before each test 137 | // setupFiles: [], 138 | 139 | // A list of paths to modules that run some code to configure or set up the testing framework before each test 140 | // setupFilesAfterEnv: [], 141 | 142 | // The number of seconds after which a test is considered as slow and reported as such in the results. 143 | // slowTestThreshold: 5, 144 | 145 | // A list of paths to snapshot serializer modules Jest should use for snapshot testing 146 | // snapshotSerializers: [], 147 | 148 | // The test environment that will be used for testing 149 | testEnvironment: "jsdom", 150 | 151 | // Options that will be passed to the testEnvironment 152 | // testEnvironmentOptions: {}, 153 | 154 | // Adds a location field to test results 155 | // testLocationInResults: false, 156 | 157 | // The glob patterns Jest uses to detect test files 158 | // testMatch: [ 159 | // "**/__tests__/**/*.[jt]s?(x)", 160 | // "**/?(*.)+(spec|test).[tj]s?(x)" 161 | // ], 162 | 163 | // An array of regexp pattern strings that are matched against all test paths, matched tests are skipped 164 | // testPathIgnorePatterns: [ 165 | // "\\\\node_modules\\\\" 166 | // ], 167 | 168 | // The regexp pattern or array of patterns that Jest uses to detect test files 169 | // testRegex: [], 170 | 171 | // This option allows the use of a custom results processor 172 | // testResultsProcessor: undefined, 173 | 174 | // This option allows use of a custom test runner 175 | // testRunner: "jest-circus/runner", 176 | 177 | // A map from regular expressions to paths to transformers 178 | // transform: undefined, 179 | 180 | // An array of regexp pattern strings that are matched against all source file paths, matched files will skip transformation 181 | // transformIgnorePatterns: [ 182 | // "\\\\node_modules\\\\", 183 | // "\\.pnp\\.[^\\\\]+$" 184 | // ], 185 | 186 | // An array of regexp pattern strings that are matched against all modules before the module loader will automatically return a mock for them 187 | // unmockedModulePathPatterns: undefined, 188 | 189 | // Indicates whether each individual test should be reported during the run 190 | verbose: true, 191 | 192 | // An array of regexp patterns that are matched against all source file paths before re-running tests in watch mode 193 | // watchPathIgnorePatterns: [], 194 | 195 | // Whether to use watchman for file crawling 196 | // watchman: true, 197 | }; 198 | 199 | export default config; 200 | -------------------------------------------------------------------------------- /logger.config.mjs: -------------------------------------------------------------------------------- 1 | import log from 'loglevel'; 2 | import { logToSentry } from './sentry.client.config.mjs'; 3 | 4 | export const configureLogger = () => { 5 | if (process.env.NODE_ENV === 'production') { 6 | // In production, we want to log only to Sentry 7 | log.setLevel('silent'); 8 | 9 | log.error = (...args) => { 10 | logToSentry(args.join(' ')); 11 | }; 12 | 13 | } else if (process.env.NODE_ENV === 'development') { 14 | // In development, we want to log only to the console 15 | log.setLevel('info'); 16 | } 17 | }; 18 | 19 | // Initialize the logger configuration 20 | configureLogger(); 21 | 22 | export default log; 23 | -------------------------------------------------------------------------------- /messages/zh-CN.json: -------------------------------------------------------------------------------- 1 | { 2 | "HOME": { 3 | "ADD_SELLER": "出售", 4 | "SEARCH_CENTER_DEFAULT_MESSAGE": "您的搜索中心当前设置为默认坐标 [0, 0]。为提升您的体验,请更新您的搜索中心位置。", 5 | "SEARCH_BAR_PLACEHOLDER": "搜索卖家或商品", 6 | "LOCATION_SERVICES": { 7 | "ENABLE_LOCATION_SERVICES_MESSAGE": "要使用此功能,请在设备设置中启用位置服务。", 8 | "DISABLED_LOCATION_SERVICES_MESSAGE": "位置服务已禁用。请启用设备的定位设置。" 9 | }, 10 | "AUTHENTICATION": { 11 | "SUCCESSFUL_LOGIN_MESSAGE": "登录成功", 12 | "UNSUCCESSFUL_LOGIN_MESSAGE": "登录失败", 13 | "PI_INFORMATION_NOT_FOUND_MESSAGE": "未找到 Pi 信息" 14 | } 15 | }, 16 | "SIDE_NAVIGATION": { 17 | "USER_PREFERENCES_HEADER": "用户偏好", 18 | "PERSONALIZATION_SUBHEADER": "个性化设置", 19 | "LANGUAGES": "语言", 20 | "ABOUT": { 21 | "ABOUT_MAP_OF_PI": "关于 Map of Pi", 22 | "APP_VERSION": "应用程序版本", 23 | "PRIVACY_POLICY": "隐私政策", 24 | "TERMS_OF_SERVICE": "服务条款" 25 | }, 26 | "FIND_ME_PREFERENCE_LABEL": "查找我偏好", 27 | "FIND_ME_OPTIONS": { 28 | "PREFERRED_AUTO": "自动", 29 | "PREFERRED_DEVICE_GPS": "使用我设备的 GPS", 30 | "PREFERRED_SEARCH_CENTER": "使用搜索中心" 31 | }, 32 | "SEARCH_FILTERS_SUBHEADER": "搜索筛选器", 33 | "SEARCH_FILTERS": { 34 | "INCLUDE_ACTIVE_SELLERS": "包含活跃卖家", 35 | "INCLUDE_INACTIVE_SELLERS": "包含非活跃卖家", 36 | "INCLUDE_TEST_SELLERS": "包含测试卖家" 37 | }, 38 | "CONTACT_MAP_OF_PI": "联络 Map of Pi", 39 | "VALIDATION": { 40 | "SUCCESSFUL_PREFERENCES_SUBMISSION": "偏好设置已成功保存", 41 | "UNSUCCESSFUL_PREFERENCES_SUBMISSION": "保存偏好设置时出错" 42 | } 43 | }, 44 | "SCREEN": { 45 | "SELLER_REGISTRATION": { 46 | "SELLER_REGISTRATION_HEADER": "卖家注册", 47 | "SELLER_DETAILS_LABEL": "卖家详情", 48 | "EMAIL_LABEL": "电子邮件地址", 49 | "PHONE_NUMBER_LABEL": "电话号码", 50 | "CONTACT_PUBLIC_NOTE": "公开显示电子邮件地址和电话号码。", 51 | "SELLER_DETAILS_PLACEHOLDER": "我通过 Pi 支付进行销售。", 52 | "REVIEWS_SUMMARY_LABEL": "评论概述", 53 | "REVIEWS_SCORE_LABEL": "评论评分", 54 | "SELLER_NAME": "卖家姓名", 55 | "SELLER_TYPE": { 56 | "SELLER_TYPE_LABEL": "卖家类型", 57 | "SELLER_TYPE_OPTIONS": { 58 | "ACTIVE_SELLER": "活跃卖家", 59 | "INACTIVE_SELLER": "非活跃卖家", 60 | "TEST_SELLER": "测试卖家", 61 | "RESTRICTED_SELLER": "受限卖家" 62 | } 63 | }, 64 | "SELLER_ADVANCED_SETTINGS_LABEL": "高级卖家设置", 65 | "SELLER_RETAIL_OUTLET_NAME": "零售店名", 66 | "SELLER_ADDRESS_LOCATION_LABEL": "地址或销售地点", 67 | "SELLER_ADDRESS_LOCATION_PLACEHOLDER": "我的地图标记显示我的销售位置", 68 | "SELLER_SETTINGS_LABEL": "卖家设置", 69 | "SELLER_SELL_CENTER": "设定销售中心", 70 | "SELLER_ONLINE_SHOPPING_LABEL": "在线购物", 71 | "MAPPI_ALLOWANCE_LABEL": "剩余的 Mappi 额度", 72 | "FULFILLMENT_METHOD_TYPE": { 73 | "FULFILLMENT_METHOD_TYPE_LABEL": "履行方式", 74 | "FULFILLMENT_METHOD_TYPE_OPTIONS": { 75 | "COLLECTION_BY_BUYER": "买家自取", 76 | "DELIVERED_TO_BUYER": "送货到买家" 77 | } 78 | }, 79 | "FULFILLMENT_INSTRUCTIONS_LABEL": "给买家的履行说明", 80 | "FULFILLMENT_INSTRUCTIONS_PLACEHOLDER": "取货地址为卖家的地址。如果选择配送,请输入买家的地址。", 81 | "VALIDATION": { 82 | "EMAIL_VALIDATION": "请输入有效的电子邮件地址", 83 | "SUCCESSFUL_REGISTRATION_SUBMISSION": "注册成功", 84 | "FAILED_REGISTRATION_SUBMISSION": "保存注册信息时出错", 85 | "REGISTRATION_FAILED_USER_NOT_AUTHENTICATED": "注册失败,因为用户未验证身份", 86 | "UNINITIALIZED_SELL_CENTER": "请设定您的销售中心", 87 | "SUCCESSFUL_SELLER_ITEM_SAVED": "卖家商品已成功保存", 88 | "SUCCESSFUL_SELLER_ITEM_DELETED": "卖家商品已成功删除", 89 | "FAILED_SELLER_ITEM_SAVE": "保存卖家商品时出错", 90 | "FAILED_SELLER_ITEM_DELETE": "删除卖家商品时出错", 91 | "SELLER_ITEM_NOT_FOUND": "未找到卖家商品", 92 | "SUCCESSFUL_SAVE_MAPPI_ALLOWANCE_SUFFICIENT": "保存成功。使用的 Mappi 配额为 {mappi_count}", 93 | "FAILED_SAVE_MAPPI_ALLOWANCE_INSUFFICIENT": "保存失败。可用的 Mappi 配额不足" 94 | }, 95 | "SELLER_ITEMS_FEATURE": { 96 | "ITEM_LABEL": "商品", 97 | "PRICE_LABEL": "价格", 98 | "STOCK_LABEL": "库存水平", 99 | "DESCRIPTION_LABEL": "描述", 100 | "PHOTO": "照片", 101 | "SELLING_DURATION_LABEL": "销售周期(以周为单位)", 102 | "SELLING_STATUS_OPTIONS": { 103 | "ACTIVE": "活动中", 104 | "EXPIRED": "已过期" 105 | }, 106 | "STOCK_LEVEL_OPTIONS": { 107 | "AVAILABLE_1": "1 个可用", 108 | "AVAILABLE_2": "2 个可用", 109 | "AVAILABLE_3": "3 个可用", 110 | "MANY": "多个可用", 111 | "MADE_TO_ORDER": "定制", 112 | "ONGOING_SERVICE": "正在进行的服务", 113 | "SOLD": "已售出" 114 | }, 115 | "SELLING_EXPIRATION_DATE": "{expired_by_date} 前销售" 116 | } 117 | }, 118 | "BUY_FROM_SELLER": { 119 | "BUY_FROM_SELLER_HEADER": "从卖家购买", 120 | "SELLER_DETAILS_LABEL": "卖家详情", 121 | "SELLER_ADDRESS_POSITION_LABEL": "卖家地址或位置", 122 | "LEAVE_A_REVIEW_MESSAGE": "留下评论", 123 | "FACE_SELECTION_REVIEW_MESSAGE": "选择表情以表示您对该卖家的感受", 124 | "ADDITIONAL_COMMENTS_PLACEHOLDER": "在此输入附加评论", 125 | "FEEDBACK_PHOTO_UPLOAD_LABEL": "上传反馈照片", 126 | "REVIEWS_SUMMARY_LABEL": "评论概述", 127 | "REVIEWS_SCORE_MESSAGE": "评论评分:{seller_review_rating} 满分 5.0", 128 | "SELLER_CONTACT_DETAILS_LABEL": "卖家联系信息", 129 | "SELLER_USERNAME_LABEL": "用户名", 130 | "SELLER_PIONEER_ID_LABEL": "先锋 ID", 131 | "SELLER_PHONE_NUMBER_LABEL": "电话号码", 132 | "SELLER_EMAIL_ADDRESS_LABEL": "电子邮件地址", 133 | "ONLINE_SHOPPING": { 134 | "SELLER_ITEMS_FEATURE": { 135 | "ITEM_LABEL": "商品", 136 | "PRICE_LABEL": "价格", 137 | "DESCRIPTION_LABEL": "描述", 138 | "PHOTO": "照片", 139 | "BUYING_QUANTITY_LABEL": "数量", 140 | "PICK_LABEL": "添加", 141 | "UNPICK_LABEL": "移除" 142 | } 143 | } 144 | }, 145 | "REVIEWS": { 146 | "REVIEWS_HEADER": "评论", 147 | "GIVE_REVIEW_SECTION_HEADER": "给这位先锋的评论", 148 | "REVIEWS_GIVEN_SECTION_HEADER": "该先锋给出的评论", 149 | "REVIEWS_RECEIVED_SECTION_HEADER": "该先锋收到的评论", 150 | "VALIDATION": { 151 | "NO_REVIEWS_FOUND": "未找到先锋 {search_value} 的评论", 152 | "NO_PIONEER_FOUND": "未找到先锋 {search_value}" 153 | } 154 | }, 155 | "CHECK_REVIEWS_FEEDBACK": { 156 | "CHECK_REVIEWS_NO_FEEDBACK_HEADER": "没有 {seller_id} 的评论", 157 | "CHECK_REVIEWS_FEEDBACK_HEADER": "{seller_id} 的评论列表", 158 | "BY_REVIEWER": "由 {buyer_id}" 159 | }, 160 | "REPLY_TO_REVIEW": { 161 | "REPLY_TO_REVIEW_STATIC_HEADER": "回复评论", 162 | "REPLY_TO_REVIEW_SUBHEADER": "您正在回复的评论", 163 | "GIVE_REPLY_TO_REVIEW_SUBHEADER": "给评论的回复", 164 | "REPLY_TO_REVIEW_HEADER": "回复给 {seller_id} 的评论", 165 | "BY_REVIEWER": "由 {buyer_id}", 166 | "REPLY_TO_REVIEW_MESSAGE": "留下您对上述评论的回复", 167 | "FACE_SELECTION_REVIEW_MESSAGE": "选择表情以表示您对与该先锋的互动感受", 168 | "ADDITIONAL_COMMENTS_PLACEHOLDER": "在此输入附加评论", 169 | "FEEDBACK_PHOTO_UPLOAD_LABEL": "上传反馈照片", 170 | "VALIDATION": { 171 | "LOADING_REVIEW_FAILURE": "加载评论时出错", 172 | "SELF_REVIEW_NOT_POSSIBLE": "无法对自己进行评论" 173 | } 174 | } 175 | }, 176 | "POPUP": { 177 | "MAP_MARKER": { 178 | "SELLER_SALE_ITEMS_FIELD": "卖家销售的商品", 179 | "DISTANCE_MESSAGE": "距离:XXX 公里" 180 | }, 181 | "APP_VERSION_INFO": { 182 | "REPORTING_MESSAGE": "请报告问题至" 183 | }, 184 | "PRIVACY_POLICY_INFO": { 185 | "TITLE": "隐私政策", 186 | "LAST_UPDATED": "最后更新", 187 | "EMAIL_ADDRESS": "电子邮件地址", 188 | "SECTIONS": { 189 | "HEADER_1": "简介", 190 | "CONTENT_1": "欢迎使用 Map of Pi 应用程序,由 Map of Pi 团队与 Pi 社区合作开发。本隐私政策概述了您使用我们的移动应用程序时,我们如何收集、使用、披露和保护您的个人信息。", 191 | "HEADER_2": "我们收集的信息", 192 | "SUBHEADER_2_1": "用户信息", 193 | "CONTENT_2_1_1": "当您注册为用户时,我们可能会收集您的姓名、电子邮件地址、用户名和其他相关信息。", 194 | "CONTENT_2_1_2": "我们可能会收集关于您设备的信息,包括设备类型、操作系统和唯一设备标识符。", 195 | "SUBHEADER_2_2": "卖家信息", 196 | "CONTENT_2_2_1": "在平台上注册的卖家可能需要提供卖家信息,包括卖家名称、位置和联系方式。", 197 | "CONTENT_2_2_2": "卖家可以是企业或私人个体。", 198 | "HEADER_3": "我们如何使用您的信息", 199 | "SUBHEADER_3_1": "提供服务", 200 | "CONTENT_3_1_1": "我们使用您的信息来提供和增强 Map of Pi 应用程序的服务,包括用户和卖家功能。", 201 | "SUBHEADER_3_2": "通信", 202 | "CONTENT_3_2_1": "我们可能会使用您的联系方式向您发送与 Map of Pi 应用程序相关的更新、通知和促销信息。", 203 | "SUBHEADER_3_3": "数据分析", 204 | "CONTENT_3_3_1": "我们收集和分析数据以改进应用程序的性能、功能和特点。", 205 | "HEADER_4": "共享您的信息", 206 | "SUBHEADER_4_1": "与卖家共享", 207 | "CONTENT_4_1_1": "用户信息可能会与卖家共享,以便完成订单和进行沟通。", 208 | "SUBHEADER_4_2": "与第三方共享", 209 | "CONTENT_4_2_1": "我们可能会与第三方服务提供商共享信息,以实现应用功能、数据分析和支付处理。", 210 | "HEADER_5": "安全措施", 211 | "CONTENT_5_1": "我们采用行业标准的安全措施来保护您的信息。然而,互联网传输或电子存储方法并非绝对安全。", 212 | "HEADER_6": "您的选择", 213 | "CONTENT_6_1": "您可以通过应用程序设置更新您的账户信息。您可以通过删除 Pi 网络账户来删除您的 Map of Pi 账户,并在下次访问 Map of Pi 时选择删除您的 Map of Pi 数据。您可以选择不接收促销信息。", 214 | "HEADER_7": "隐私政策的变更", 215 | "CONTENT_7_1": "我们可能会根据需要更新本隐私政策。最新版本将发布在我们的网站或应用程序内。", 216 | "HEADER_8": "联系我们", 217 | "CONTENT_8_1": "如果您对本隐私政策有任何问题或疑虑,请通过发送电子邮件联系我们" 218 | } 219 | }, 220 | "TERMS_OF_SERVICE_INFO": { 221 | "TITLE": "服务条款", 222 | "LAST_UPDATED": "最后更新", 223 | "EMAIL_ADDRESS": "电子邮件地址", 224 | "SECTIONS": { 225 | "HEADER_1": "接受条款", 226 | "CONTENT_1_1": "欢迎使用由 Map of Pi 团队与 Pi 社区合作开发的 Map of Pi 应用程序。使用本应用程序即表示您同意遵守这些服务条款以及我们的隐私政策。", 227 | "HEADER_2": "应用程序的使用", 228 | "CONTENT_2_1": "您同意按照所有适用的法律法规使用 Map of Pi 应用程序。", 229 | "CONTENT_2_2": "您不得将应用程序用于任何非法或被禁止的目的,也不得以任何可能损害、禁用、过载或削弱应用程序的方式使用该应用程序。", 230 | "CONTENT_2_3": "使用 Map of Pi 需先注册为 Pi 网络的先锋用户。", 231 | "HEADER_3": "知识产权", 232 | "CONTENT_3_1": "Map of Pi 应用程序及其所有内容、特性和功能均归 Map of Pi 所有,并受版权、商标和其他知识产权法律的保护。", 233 | "HEADER_4": "责任限制", 234 | "CONTENT_4_1": "Map of Pi 不对因使用 Map of Pi 应用程序而产生的任何直接、间接、附带、特殊或后果性损害承担责任。", 235 | "HEADER_5": "赔偿", 236 | "CONTENT_5_1": "您同意赔偿并使 Map of Pi 及其附属公司、管理人员、董事、员工和代理人免受任何因您使用 Map of Pi 应用程序而产生的所有索赔、责任、损害、损失或费用(包括律师费)的影响。", 237 | "HEADER_6": "法律适用", 238 | "CONTENT_6_1": "本服务条款应依照英格兰和威尔士的法律解释和执行,不考虑其法律冲突条款,包括因其主题或形成(包括非合同纠纷或索赔)产生的任何争议或索赔。", 239 | "HEADER_7": "条款变更", 240 | "CONTENT_7_1": "我们保留随时更新或修改这些服务条款的权利,恕不另行通知。最新版本将发布在我们的网站或应用程序内。" 241 | } 242 | } 243 | }, 244 | "SHARED": { 245 | "PIONEER_LABEL": "先锋", 246 | "BUY": "购买", 247 | "NAVIGATE": "导航", 248 | "NO_COMMENT": "无评论", 249 | "SEARCH_CENTER": "设置搜索中心", 250 | "CHECK_REVIEWS": "查看评论", 251 | "SEARCH_REVIEWS": "搜索评论", 252 | "SEARCH_LOADING": "搜索加载中...", 253 | "REPLY": "回复", 254 | "ADD_ITEM": "添加项目", 255 | "DELETE": "删除", 256 | "CONFIRM_DELETE": "您确定要删除此项目吗?此操作无法撤销。", 257 | "SAVE": "保存", 258 | "CONFIRM": "确认", 259 | "CONFIRM_DIALOG": "您有未保存的更改。确定要离开吗?", 260 | "USER_INFORMATION": { 261 | "PI_USERNAME_LABEL": "Pi 用户名", 262 | "NAME_LABEL": "姓名", 263 | "PHONE_NUMBER_LABEL": "电话号码", 264 | "EMAIL_LABEL": "电子邮件" 265 | }, 266 | "PHOTO": { 267 | "UPLOAD_PHOTO_LABEL": "上传照片", 268 | "UPLOAD_PHOTO_PLACEHOLDER": "上传图片以吸引买家(PNG、JPG、JPEG、WEBP)", 269 | "UPLOAD_PHOTO_REVIEW_PLACEHOLDER": "上传图片以支持您的评论(PNG、JPG、JPEG、WEBP)", 270 | "IMAGE_DROP_UPLOAD_MESSAGE": "将图片拖放到此处或浏览", 271 | "SUPPORTS_FILE_MESSAGE": "支持文件格式:PNG、JPG、JPEG、WEBP", 272 | "MISC_LABELS": { 273 | "USER_PREFERENCES_LABEL": "个人资料图片", 274 | "SELLER_IMAGE_LABEL": "卖家图片", 275 | "REVIEW_FEEDBACK_IMAGE_LABEL": "评论图片" 276 | } 277 | }, 278 | "REACTION_RATING": { 279 | "UNSAFE": "不安全", 280 | "TRUSTWORTHY": "可信", 281 | "EMOTIONS": { 282 | "DESPAIR": "绝望", 283 | "SAD": "悲伤", 284 | "OKAY": "一般", 285 | "HAPPY": "高兴", 286 | "DELIGHT": "欣喜" 287 | }, 288 | "VALIDATION": { 289 | "SUCCESSFUL_REVIEW_SUBMISSION": "评论提交成功", 290 | "UNSUCCESSFUL_REVIEW_SUBMISSION": "评论提交失败", 291 | "SELECT_EMOJI_EXPRESSION": "请选择表情符号" 292 | } 293 | }, 294 | "MAP_CENTER": { 295 | "SEARCH_BAR_PLACEHOLDER": "搜索位置、城市或地址", 296 | "VALIDATION": { 297 | "MAP_CENTER_SUCCESS_MESSAGE": "中心点保存成功", 298 | "SELL_CENTER_SANCTIONED_MESSAGE": "您选择的位置似乎位于受限区域。如果确认,您的销售标记可能会从地图中移除。" 299 | } 300 | }, 301 | "LOADING_SCREEN_MESSAGE": "加载中...", 302 | "SAVING_SCREEN_MESSAGE": "保存中...", 303 | "VALIDATION": { 304 | "SUBMISSION_FAILED_USER_NOT_AUTHENTICATED": "提交失败,用户未认证", 305 | "UNEXPECTED_ERROR_MESSAGE": "发生意外错误" 306 | } 307 | }, 308 | "ERROR": { 309 | "PAGE_NOT_FOUND_HEADER": "页面未找到", 310 | "PAGE_NOT_FOUND_MESSAGE": "抱歉,您查找的页面不存在。" 311 | } 312 | } -------------------------------------------------------------------------------- /next.config.mjs: -------------------------------------------------------------------------------- 1 | import createNextIntlPlugin from 'next-intl/plugin'; 2 | import { withSentryConfig } from "@sentry/nextjs"; 3 | 4 | const withNextIntl = createNextIntlPlugin('./i18n/request.ts'); 5 | 6 | /** @type {import('next').NextConfig} */ 7 | const nextConfig = { 8 | eslint: { 9 | ignoreDuringBuilds: true, 10 | }, 11 | images: { 12 | remotePatterns: [ 13 | { 14 | protocol: 'https', 15 | hostname: 'example.com', 16 | port: '', 17 | pathname: '/**', 18 | }, 19 | { 20 | protocol: 'http', 21 | hostname: 'localhost', 22 | port: '8001', 23 | pathname: '/**', 24 | }, 25 | { 26 | protocol: 'https', 27 | hostname: 'tse3.mm.bing.net', 28 | port: '', 29 | pathname: '/**', 30 | }, 31 | { 32 | protocol: 'https', 33 | hostname: 'res.cloudinary.com', 34 | port: '', 35 | pathname: '/**', 36 | } 37 | ], 38 | }, 39 | async rewrites() { 40 | return [ 41 | { 42 | source: '/api/v1/:path*', 43 | destination: 'http://localhost:8001/api/v1/:path*', 44 | }, 45 | ]; 46 | }, 47 | }; 48 | 49 | const sentryWebpackPluginOptions = { 50 | silent: true, // suppress Sentry errors during the build process 51 | }; 52 | 53 | // wrap existing configuration with Sentry 54 | const configWithSentry = withSentryConfig(nextConfig, sentryWebpackPluginOptions); 55 | 56 | export default withNextIntl(configWithSentry); 57 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "map-of-pi-frontend-react", 3 | "version": "1.0.0", 4 | "author": "Map of Pi Team", 5 | "description": "Map of Pi Frontend React", 6 | "license": "PiOS", 7 | "private": true, 8 | "scripts": { 9 | "dev": "next dev -p 4200", 10 | "build": "next build", 11 | "start": "next start -p 4200", 12 | "lint": "next lint", 13 | "test": "jest" 14 | }, 15 | "dependencies": { 16 | "@emotion/core": "^10.1.1", 17 | "@emotion/react": "^11.11.4", 18 | "@emotion/styled": "^11.11.5", 19 | "@mui/icons-material": "^5.15.16", 20 | "@mui/material": "^5.15.16", 21 | "@pinetwork-js/sdk": "^0.7.0", 22 | "@sentry/browser": "^8.26.0", 23 | "@sentry/nextjs": "^8.26.0", 24 | "@sentry/node": "^8.27.0", 25 | "axios": "^1.7.5", 26 | "clsx": "^2.1.1", 27 | "cookies-next": "^4.1.1", 28 | "date-fns": "^3.6.0", 29 | "date-fns-tz": "^3.1.3", 30 | "i18next": "^23.11.3", 31 | "js-cookie": "^3.0.5", 32 | "leaflet": "^1.9.4", 33 | "leaflet-control-geocoder": "^2.4.0", 34 | "leaflet-geosearch": "^3.11.1", 35 | "loglevel": "^1.9.1", 36 | "next": "^14.2.28", 37 | "next-intl": "^3.25.3", 38 | "next-logger": "^4.0.0", 39 | "next-themes": "^0.3.0", 40 | "ngx-logger": "^5.0.12", 41 | "react": "^18", 42 | "react-dom": "^18", 43 | "react-i18next": "^14.1.1", 44 | "react-icons": "^5.1.0", 45 | "react-intl": "^6.6.5", 46 | "react-leaflet": "^4.2.1", 47 | "react-logger": "^1.1.0", 48 | "react-phone-number-input": "^3.4.3", 49 | "react-switch": "^7.0.0", 50 | "react-toastify": "^10.0.5", 51 | "sass": "^1.77.0", 52 | "sharp": "^0.33.3", 53 | "zod": "^3.23.8" 54 | }, 55 | "devDependencies": { 56 | "@pinetwork-js/api-typing": "^0.7.0", 57 | "@testing-library/jest-dom": "^6.4.2", 58 | "@testing-library/react": "^15.0.2", 59 | "@types/js-cookie": "^3.0.6", 60 | "@types/leaflet": "^1.9.12", 61 | "@types/lodash": "^4.17.5", 62 | "@types/node": "^20", 63 | "@types/react": "^18", 64 | "@types/react-dom": "^18", 65 | "@types/react-leaflet": "^3.0.0", 66 | "@typescript-eslint/eslint-plugin": "^7.7.1", 67 | "@typescript-eslint/parser": "^7.7.1", 68 | "eslint": "^8", 69 | "eslint-config-next": "14.2.2", 70 | "eslint-config-prettier": "^9.1.0", 71 | "eslint-plugin-prettier": "^5.1.3", 72 | "jest": "^29.7.0", 73 | "jest-environment-jsdom": "^29.7.0", 74 | "postcss": "^8", 75 | "prettier": "^3.2.5", 76 | "tailwindcss": "^3.4.1", 77 | "ts-jest": "^29.1.2", 78 | "ts-node": "^10.9.2", 79 | "typescript": "^5" 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /postcss.config.mjs: -------------------------------------------------------------------------------- 1 | /** @type {import('postcss-load-config').Config} */ 2 | const config = { 3 | plugins: { 4 | tailwindcss: {}, 5 | }, 6 | }; 7 | 8 | export default config; 9 | -------------------------------------------------------------------------------- /public/android-chrome-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/map-of-pi/map-of-pi-frontend-react/e5de66f0fe4656a0cd66d1060cf5bebd7df1bbfe/public/android-chrome-192x192.png -------------------------------------------------------------------------------- /public/android-chrome-512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/map-of-pi/map-of-pi-frontend-react/e5de66f0fe4656a0cd66d1060cf5bebd7df1bbfe/public/android-chrome-512x512.png -------------------------------------------------------------------------------- /public/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/map-of-pi/map-of-pi-frontend-react/e5de66f0fe4656a0cd66d1060cf5bebd7df1bbfe/public/apple-touch-icon.png -------------------------------------------------------------------------------- /public/default.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/map-of-pi/map-of-pi-frontend-react/e5de66f0fe4656a0cd66d1060cf5bebd7df1bbfe/public/default.png -------------------------------------------------------------------------------- /public/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/map-of-pi/map-of-pi-frontend-react/e5de66f0fe4656a0cd66d1060cf5bebd7df1bbfe/public/favicon-16x16.png -------------------------------------------------------------------------------- /public/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/map-of-pi/map-of-pi-frontend-react/e5de66f0fe4656a0cd66d1060cf5bebd7df1bbfe/public/favicon-32x32.png -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/map-of-pi/map-of-pi-frontend-react/e5de66f0fe4656a0cd66d1060cf5bebd7df1bbfe/public/favicon.ico -------------------------------------------------------------------------------- /public/images/business/add-item-button.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/map-of-pi/map-of-pi-frontend-react/e5de66f0fe4656a0cd66d1060cf5bebd7df1bbfe/public/images/business/add-item-button.png -------------------------------------------------------------------------------- /public/images/business/product.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/map-of-pi/map-of-pi-frontend-react/e5de66f0fe4656a0cd66d1060cf5bebd7df1bbfe/public/images/business/product.png -------------------------------------------------------------------------------- /public/images/business/upload.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/map-of-pi/map-of-pi-frontend-react/e5de66f0fe4656a0cd66d1060cf5bebd7df1bbfe/public/images/business/upload.jpg -------------------------------------------------------------------------------- /public/images/icons/crosshair.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/map-of-pi/map-of-pi-frontend-react/e5de66f0fe4656a0cd66d1060cf5bebd7df1bbfe/public/images/icons/crosshair.png -------------------------------------------------------------------------------- /public/images/icons/map-of-pi-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/map-of-pi/map-of-pi-frontend-react/e5de66f0fe4656a0cd66d1060cf5bebd7df1bbfe/public/images/icons/map-of-pi-icon.png -------------------------------------------------------------------------------- /public/images/icons/map_centers_crosshair.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/map-of-pi/map-of-pi-frontend-react/e5de66f0fe4656a0cd66d1060cf5bebd7df1bbfe/public/images/icons/map_centers_crosshair.png -------------------------------------------------------------------------------- /public/images/icons/map_of_pi_logo.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/map-of-pi/map-of-pi-frontend-react/e5de66f0fe4656a0cd66d1060cf5bebd7df1bbfe/public/images/icons/map_of_pi_logo.jpeg -------------------------------------------------------------------------------- /public/images/icons/scope.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/map-of-pi/map-of-pi-frontend-react/e5de66f0fe4656a0cd66d1060cf5bebd7df1bbfe/public/images/icons/scope.png -------------------------------------------------------------------------------- /public/images/shared/my_location.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/map-of-pi/map-of-pi-frontend-react/e5de66f0fe4656a0cd66d1060cf5bebd7df1bbfe/public/images/shared/my_location.png -------------------------------------------------------------------------------- /public/images/shared/review_ratings/trust-o-meter_000.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/map-of-pi/map-of-pi-frontend-react/e5de66f0fe4656a0cd66d1060cf5bebd7df1bbfe/public/images/shared/review_ratings/trust-o-meter_000.PNG -------------------------------------------------------------------------------- /public/images/shared/review_ratings/trust-o-meter_050.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/map-of-pi/map-of-pi-frontend-react/e5de66f0fe4656a0cd66d1060cf5bebd7df1bbfe/public/images/shared/review_ratings/trust-o-meter_050.PNG -------------------------------------------------------------------------------- /public/images/shared/review_ratings/trust-o-meter_080.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/map-of-pi/map-of-pi-frontend-react/e5de66f0fe4656a0cd66d1060cf5bebd7df1bbfe/public/images/shared/review_ratings/trust-o-meter_080.PNG -------------------------------------------------------------------------------- /public/images/shared/review_ratings/trust-o-meter_100.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/map-of-pi/map-of-pi-frontend-react/e5de66f0fe4656a0cd66d1060cf5bebd7df1bbfe/public/images/shared/review_ratings/trust-o-meter_100.PNG -------------------------------------------------------------------------------- /public/images/shared/sidebar/close.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /public/images/shared/sidebar/contact.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /public/images/shared/sidebar/dark.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /public/images/shared/sidebar/info.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /public/images/shared/sidebar/language.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /public/images/shared/sidebar/light.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /public/images/shared/sidebar/location.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /public/images/shared/sidebar/question.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /public/images/shared/sidebar/settings.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /public/images/shared/sidebar/theme.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /public/images/shared/sidebar/user.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /public/images/shared/social-media/E-mail_icon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /public/images/shared/social-media/discord-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/map-of-pi/map-of-pi-frontend-react/e5de66f0fe4656a0cd66d1060cf5bebd7df1bbfe/public/images/shared/social-media/discord-icon.png -------------------------------------------------------------------------------- /public/images/shared/social-media/email-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/map-of-pi/map-of-pi-frontend-react/e5de66f0fe4656a0cd66d1060cf5bebd7df1bbfe/public/images/shared/social-media/email-icon.png -------------------------------------------------------------------------------- /public/images/shared/social-media/email-icon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 6 | 7 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /public/images/shared/social-media/email-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/map-of-pi/map-of-pi-frontend-react/e5de66f0fe4656a0cd66d1060cf5bebd7df1bbfe/public/images/shared/social-media/email-logo.png -------------------------------------------------------------------------------- /public/images/shared/social-media/facebook.icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/map-of-pi/map-of-pi-frontend-react/e5de66f0fe4656a0cd66d1060cf5bebd7df1bbfe/public/images/shared/social-media/facebook.icon.png -------------------------------------------------------------------------------- /public/images/shared/social-media/instagram-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/map-of-pi/map-of-pi-frontend-react/e5de66f0fe4656a0cd66d1060cf5bebd7df1bbfe/public/images/shared/social-media/instagram-icon.png -------------------------------------------------------------------------------- /public/images/shared/social-media/tiktok-icon.svg: -------------------------------------------------------------------------------- 1 | 2 | 4 | 7 | 8 | 10 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /public/images/shared/social-media/x-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/map-of-pi/map-of-pi-frontend-react/e5de66f0fe4656a0cd66d1060cf5bebd7df1bbfe/public/images/shared/social-media/x-icon.png -------------------------------------------------------------------------------- /public/images/shared/social-media/youtube-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/map-of-pi/map-of-pi-frontend-react/e5de66f0fe4656a0cd66d1060cf5bebd7df1bbfe/public/images/shared/social-media/youtube-icon.png -------------------------------------------------------------------------------- /public/images/shared/upload.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/map-of-pi/map-of-pi-frontend-react/e5de66f0fe4656a0cd66d1060cf5bebd7df1bbfe/public/images/shared/upload.png -------------------------------------------------------------------------------- /public/images/shared/upload_old.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/map-of-pi/map-of-pi-frontend-react/e5de66f0fe4656a0cd66d1060cf5bebd7df1bbfe/public/images/shared/upload_old.png -------------------------------------------------------------------------------- /public/validation-key.txt: -------------------------------------------------------------------------------- 1 | 5fc61d0d7eab437117d410f84d49df5116ad216192df5820b15671a6099602beeedb1aa96ab226a03ecbd3abfe125e9cf1ba1b597be2bb71cedef66d5d81d2f8 -------------------------------------------------------------------------------- /sentry.client.config.mjs: -------------------------------------------------------------------------------- 1 | import * as Sentry from "@sentry/nextjs"; 2 | import { replayIntegration } from '@sentry/browser'; 3 | 4 | // initialize Sentry only in production environment 5 | if (process.env.NODE_ENV === 'production') { 6 | try { 7 | Sentry.init({ 8 | dsn: process.env.NEXT_PUBLIC_SENTRY_DSN, 9 | tracesSampleRate: 0.1, 10 | replaysSessionSampleRate: 0.1, 11 | replaysOnErrorSampleRate: 1.0 12 | }); 13 | } catch (error) { 14 | throw new Error(`Failed connection to Sentry: ${error.message}`); 15 | } 16 | } 17 | 18 | export const logToSentry = (message) => { 19 | Sentry.captureException(new Error(message)); 20 | }; -------------------------------------------------------------------------------- /src/app/[locale]/layout.tsx: -------------------------------------------------------------------------------- 1 | import { NextIntlClientProvider, useMessages } from 'next-intl'; 2 | import { setRequestLocale } from 'next-intl/server'; 3 | import { Lato } from 'next/font/google'; 4 | import { locales } from '../../../i18n/i18n'; 5 | import { Providers } from '../providers'; 6 | import Navbar from '@/components/shared/navbar/Navbar'; 7 | import logger from '../../../logger.config.mjs'; 8 | 9 | export const dynamic = 'force-dynamic'; 10 | 11 | const lato = Lato({ weight: '400', subsets: ['latin'], display: 'swap' }); 12 | 13 | export async function generateStaticParams() { 14 | return locales.map((locale) => { locale }) 15 | }; 16 | 17 | export default function LocaleLayout({ 18 | children, 19 | params: { locale }, 20 | }: { 21 | children: React.ReactNode; 22 | params: { locale: string }; 23 | }) { 24 | // Enable static rendering 25 | setRequestLocale(locale); 26 | 27 | // Receive messages provided in `i18n.ts` 28 | const messages = useMessages(); 29 | 30 | // log the locale and messages loading 31 | logger.info(`Rendering LocaleLayout for locale: ${locale}`); 32 | if (messages) { 33 | logger.info('Messages loaded successfully.'); 34 | } else { 35 | logger.warn('No messages found for the given locale.'); 36 | } 37 | 38 | return ( 39 | 40 | 41 | 42 | Map of Pi 43 | 44 | 48 | 52 | 53 | 54 | 58 | 62 | 63 | 64 | 68 | 73 | 79 | 85 | 89 | 93 | 97 | 98 | 99 | {/* Google tag (gtag.js) */} 100 | 101 |