mirror of
https://github.com/tiennm99/vigeoguessr.git
synced 2026-10-14 18:19:27 +00:00
refactor
This commit is contained in:
1 parent
6219fc0d83
commit
e682373ff8
19 files changed
+130
-69
No files matched your search
@@ -10,7 +10,8 @@
|
||||
"Bash(npm run lint)",
|
||||
"Bash(npm uninstall:*)",
|
||||
"Bash(find:*)",
|
||||
"Bash(pkill:*)"
|
||||
"Bash(pkill:*)",
|
||||
"Bash(cp:*)"
|
||||
],
|
||||
"deny": []
|
||||
}
|
||||
|
||||
+1
-5
@@ -31,9 +31,5 @@ yarn-error.log*
|
||||
# vercel
|
||||
.vercel
|
||||
|
||||
# typescript
|
||||
*.tsbuildinfo
|
||||
next-env.d.ts
|
||||
|
||||
# legacy files
|
||||
default.js
|
||||
default.js
|
||||
@@ -17,13 +17,41 @@ VIGEOGUESSR is a Vietnamese geography guessing game built with Next.js 14. Playe
|
||||
- `npm run start` - Start production server
|
||||
- `npm run lint` - Run ESLint to check code quality
|
||||
|
||||
## Project Structure (Standard Next.js)
|
||||
|
||||
```
|
||||
src/
|
||||
├── app/ # Next.js App Router pages
|
||||
│ ├── game/ # Game page
|
||||
│ ├── globals.css # Global styles
|
||||
│ ├── layout.js # Root layout
|
||||
│ └── page.js # Home page
|
||||
├── components/ # React components
|
||||
│ ├── ui/ # Reusable UI components
|
||||
│ │ ├── Header.js # Navigation header
|
||||
│ │ ├── DonateModal.js # Donation modal
|
||||
│ │ └── index.js # Component exports
|
||||
│ └── features/ # Game-specific components
|
||||
│ ├── GameMap.js # Interactive Leaflet map
|
||||
│ ├── PanoViewer.js # 360° image viewer
|
||||
│ ├── ResultModal.js # Game result display
|
||||
│ └── index.js # Component exports
|
||||
├── constants/ # Application constants
|
||||
│ └── locations.js # Vietnamese city data
|
||||
├── utils/ # Utility functions
|
||||
│ ├── mapillary.js # Mapillary API integration
|
||||
│ ├── distance.js # Distance calculations
|
||||
│ └── index.js # Utility exports
|
||||
└── types/ # TypeScript definitions (ready for TS migration)
|
||||
```
|
||||
|
||||
## Architecture
|
||||
|
||||
### Core Game Flow
|
||||
1. **Image Fetching**: `lib/gameUtils.js:getRandomMapillaryImage()` fetches random panoramic images from Mapillary API within predefined Vietnamese city boundaries
|
||||
2. **Display**: `components/PanoViewer.js` renders 360° images using Photo Sphere Viewer library with fallback to regular images
|
||||
3. **Interaction**: `components/GameMap.js` provides Leaflet-based interactive map for location guessing
|
||||
4. **Scoring**: Distance calculation using haversine formula in `lib/gameUtils.js:calculateDistance()`
|
||||
1. **Image Fetching**: `src/utils/mapillary.js:getRandomMapillaryImage()` fetches random panoramic images from Mapillary API within predefined Vietnamese city boundaries
|
||||
2. **Display**: `src/components/features/PanoViewer.js` renders 360° images using Photo Sphere Viewer library with fallback to regular images
|
||||
3. **Interaction**: `src/components/features/GameMap.js` provides Leaflet-based interactive map for location guessing
|
||||
4. **Scoring**: Distance calculation using haversine formula in `src/utils/distance.js:calculateDistance()`
|
||||
|
||||
### Key Components
|
||||
- **GameMap**: Leaflet.js integration with click-based location selection, automatic centering based on city choice
|
||||
@@ -31,7 +59,7 @@ VIGEOGUESSR is a Vietnamese geography guessing game built with Next.js 14. Playe
|
||||
- **Game Page**: Main game orchestration with state management for loading, guessing, and results
|
||||
|
||||
### Data Structure
|
||||
- City boundaries defined in `boundingBoxVN` object with coordinates and delta values for API queries
|
||||
- City boundaries defined in `src/constants/locations.js:boundingBoxVN` object with coordinates and delta values for API queries
|
||||
- Location codes: HN (Hanoi), TPHCM (Ho Chi Minh City), HP (Hai Phong), ND (Nam Dinh), DN (Da Nang), DL (Dalat), DHLA (Duc Hoa)
|
||||
|
||||
### External Dependencies
|
||||
@@ -49,4 +77,12 @@ Game state managed at page level with React hooks:
|
||||
- Maps and Photo Sphere Viewer are loaded via CDN and initialized with retry logic
|
||||
- Components use refs for DOM manipulation and cleanup
|
||||
- Mapillary API calls include retry logic for failed requests
|
||||
- Image fallback system handles panoramic vs regular image display
|
||||
- Image fallback system handles panoramic vs regular image display
|
||||
|
||||
### Recent Refactoring (Completed)
|
||||
- **Structure**: Migrated to standard Next.js `src/` directory structure
|
||||
- **Code Organization**: Separated utilities (`src/utils/`), constants (`src/constants/`), UI components (`src/components/ui/`), and feature components (`src/components/features/`)
|
||||
- **Import Optimization**: Updated all import paths and added index files for cleaner imports
|
||||
- **Configuration**: Updated `jsconfig.json` path aliases to work with new src structure
|
||||
- **Cleanup**: Removed 16 unused assets and redundant npm dependencies
|
||||
- Project now follows Next.js 14 best practices and is ready for TypeScript migration
|
||||
+3
-3
@@ -19,9 +19,9 @@
|
||||
],
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"@/*": ["./*"]
|
||||
"@/*": ["./src/*"]
|
||||
}
|
||||
},
|
||||
"include": ["**/*.js", "**/*.jsx", ".next/types/**/*.ts"],
|
||||
"include": ["src/**/*.js", "src/**/*.jsx"],
|
||||
"exclude": ["node_modules"]
|
||||
}
|
||||
}
|
||||
@@ -2,12 +2,13 @@
|
||||
|
||||
import { useState, useEffect, useCallback, Suspense } from 'react';
|
||||
import { useRouter, useSearchParams } from 'next/navigation';
|
||||
import Header from '@/components/Header';
|
||||
import DonateModal from '@/components/DonateModal';
|
||||
import PanoViewer from '@/components/PanoViewer';
|
||||
import GameMap from '@/components/GameMap';
|
||||
import ResultModal from '@/components/ResultModal';
|
||||
import { getRandomMapillaryImage, calculateDistance } from '@/lib/gameUtils';
|
||||
import Header from '@/components/ui/Header';
|
||||
import DonateModal from '@/components/ui/DonateModal';
|
||||
import PanoViewer from '@/components/features/PanoViewer';
|
||||
import GameMap from '@/components/features/GameMap';
|
||||
import ResultModal from '@/components/features/ResultModal';
|
||||
import { getRandomMapillaryImage } from '@/utils/mapillary';
|
||||
import { calculateDistance } from '@/utils/distance';
|
||||
|
||||
function GamePageContent() {
|
||||
const router = useRouter();
|
||||
File renamed without changes.
File renamed without changes.
@@ -2,9 +2,9 @@
|
||||
|
||||
import { useState } from 'react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import Header from '@/components/Header';
|
||||
import DonateModal from '@/components/DonateModal';
|
||||
import { locationNames } from '@/lib/gameUtils';
|
||||
import Header from '@/components/ui/Header';
|
||||
import DonateModal from '@/components/ui/DonateModal';
|
||||
import { locationNames } from '@/constants/locations';
|
||||
|
||||
export default function Home() {
|
||||
const [showDonateModal, setShowDonateModal] = useState(false);
|
||||
@@ -1,7 +1,7 @@
|
||||
'use client';
|
||||
|
||||
import { useEffect, useRef, useState, useCallback } from 'react';
|
||||
import { boundingBoxVN } from '@/lib/gameUtils';
|
||||
import { boundingBoxVN } from '@/constants/locations';
|
||||
|
||||
export default function GameMap({ choiceLocation, onLocationSelect }) {
|
||||
const mapRef = useRef(null);
|
||||
File renamed without changes.
File renamed without changes.
@@ -0,0 +1,3 @@
|
||||
export { default as GameMap } from './GameMap';
|
||||
export { default as PanoViewer } from './PanoViewer';
|
||||
export { default as ResultModal } from './ResultModal';
|
||||
File renamed without changes.
File renamed without changes.
@@ -0,0 +1,2 @@
|
||||
export { default as Header } from './Header';
|
||||
export { default as DonateModal } from './DonateModal';
|
||||
@@ -0,0 +1,26 @@
|
||||
/**
|
||||
* Vietnamese city boundaries for the geography guessing game
|
||||
* Format: [minLong, minLat, maxLong, maxLat, delta]
|
||||
*/
|
||||
export const boundingBoxVN = {
|
||||
'HN': [105.77, 20.96, 105.88, 21.05, 0.003],
|
||||
'TPHCM': [106.62, 10.71, 106.75, 10.83, 0.005],
|
||||
'HP': [106.65, 20.8, 106.75, 20.9, 0.05],
|
||||
'ND': [106, 20.35, 106.25, 20.5, 0.005],
|
||||
'DN': [108.17, 16, 108.25, 16.1, 0.005],
|
||||
'DL': [108.38, 11.89, 108.50, 12.00, 0.005],
|
||||
'DHLA': [106.35, 10.85, 106.45, 10.95, 0.005]
|
||||
};
|
||||
|
||||
/**
|
||||
* Display names for Vietnamese cities
|
||||
*/
|
||||
export const locationNames = {
|
||||
'HN': 'HA NOI',
|
||||
'TPHCM': 'TP. HO CHI MINH',
|
||||
'HP': 'HAI PHONG',
|
||||
'ND': 'NAM DINH',
|
||||
'DN': 'DA NANG',
|
||||
'DL': 'DALAT',
|
||||
'DHLA': 'DUC HOA'
|
||||
};
|
||||
@@ -0,0 +1,32 @@
|
||||
/**
|
||||
* Calculates the distance between two geographic coordinates using the Haversine formula
|
||||
* @param {number} lat1 - Latitude of first point
|
||||
* @param {number} lon1 - Longitude of first point
|
||||
* @param {number} lat2 - Latitude of second point
|
||||
* @param {number} lon2 - Longitude of second point
|
||||
* @returns {number} Distance in meters, rounded to 1 decimal place
|
||||
*/
|
||||
export function calculateDistance(lat1, lon1, lat2, lon2) {
|
||||
if (lat1 === lat2 && lon1 === lon2) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
const radlat1 = Math.PI * lat1 / 180;
|
||||
const radlat2 = Math.PI * lat2 / 180;
|
||||
const theta = lon1 - lon2;
|
||||
const radtheta = Math.PI * theta / 180;
|
||||
|
||||
let dist = Math.sin(radlat1) * Math.sin(radlat2) +
|
||||
Math.cos(radlat1) * Math.cos(radlat2) * Math.cos(radtheta);
|
||||
|
||||
if (dist > 1) {
|
||||
dist = 1;
|
||||
}
|
||||
|
||||
dist = Math.acos(dist);
|
||||
dist = dist * 180 / Math.PI;
|
||||
dist = dist * 60 * 1.1515;
|
||||
dist = dist * 1609.34; // Convert to meters
|
||||
|
||||
return parseFloat(dist.toFixed(1));
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
export { getRandomMapillaryImage } from './mapillary';
|
||||
export { calculateDistance } from './distance';
|
||||
@@ -1,23 +1,10 @@
|
||||
export const boundingBoxVN = {
|
||||
'HN': [105.77, 20.96, 105.88, 21.05, 0.003],
|
||||
'TPHCM': [106.62, 10.71, 106.75, 10.83, 0.005],
|
||||
'HP': [106.65, 20.8, 106.75, 20.9, 0.05],
|
||||
'ND': [106, 20.35, 106.25, 20.5, 0.005],
|
||||
'DN': [108.17, 16, 108.25, 16.1, 0.005],
|
||||
'DL': [108.38, 11.89, 108.50, 12.00, 0.005],
|
||||
'DHLA': [106.35, 10.85, 106.45, 10.95, 0.005]
|
||||
};
|
||||
|
||||
export const locationNames = {
|
||||
'HN': 'HA NOI',
|
||||
'TPHCM': 'TP. HO CHI MINH',
|
||||
'HP': 'HAI PHONG',
|
||||
'ND': 'NAM DINH',
|
||||
'DN': 'DA NANG',
|
||||
'DL': 'DALAT',
|
||||
'DHLA': 'DUC HOA'
|
||||
};
|
||||
import { boundingBoxVN } from '@/constants/locations';
|
||||
|
||||
/**
|
||||
* Fetches a random panoramic image from Mapillary API within the specified Vietnamese city bounds
|
||||
* @param {string} choiceLocation - The location code (e.g., 'HN', 'TPHCM')
|
||||
* @returns {Promise<Object|null>} Image data with coordinates and URL, or null if failed
|
||||
*/
|
||||
export async function getRandomMapillaryImage(choiceLocation) {
|
||||
const [minLong, minLat, maxLong, maxLat, delta] = boundingBoxVN[choiceLocation];
|
||||
|
||||
@@ -59,29 +46,4 @@ export async function getRandomMapillaryImage(choiceLocation) {
|
||||
console.error("Error fetching image:", error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export function calculateDistance(lat1, lon1, lat2, lon2) {
|
||||
if (lat1 === lat2 && lon1 === lon2) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
const radlat1 = Math.PI * lat1 / 180;
|
||||
const radlat2 = Math.PI * lat2 / 180;
|
||||
const theta = lon1 - lon2;
|
||||
const radtheta = Math.PI * theta / 180;
|
||||
|
||||
let dist = Math.sin(radlat1) * Math.sin(radlat2) +
|
||||
Math.cos(radlat1) * Math.cos(radlat2) * Math.cos(radtheta);
|
||||
|
||||
if (dist > 1) {
|
||||
dist = 1;
|
||||
}
|
||||
|
||||
dist = Math.acos(dist);
|
||||
dist = dist * 180 / Math.PI;
|
||||
dist = dist * 60 * 1.1515;
|
||||
dist = dist * 1609.34; // Convert to meters
|
||||
|
||||
return parseFloat(dist.toFixed(1));
|
||||
}
|
||||
Reference in new issue
Block a user