This commit is contained in:
tiennm99 committed 2025-08-03 00:26:17 +07:00
1 parent 6219fc0d83
commit e682373ff8
19 files changed
+130 -69

No files matched your search

+2 -1
View File
@@ -10,7 +10,8 @@
"Bash(npm run lint)",
"Bash(npm uninstall:*)",
"Bash(find:*)",
"Bash(pkill:*)"
"Bash(pkill:*)",
"Bash(cp:*)"
],
"deny": []
}
+1 -5
View File
@@ -31,9 +31,5 @@ yarn-error.log*
# vercel
.vercel
# typescript
*.tsbuildinfo
next-env.d.ts
# legacy files
default.js
default.js
+42 -6
View File
@@ -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
View File
@@ -19,9 +19,9 @@
],
"baseUrl": ".",
"paths": {
"@/*": ["./*"]
"@/*": ["./src/*"]
}
},
"include": ["**/*.js", "**/*.jsx", ".next/types/**/*.ts"],
"include": ["src/**/*.js", "src/**/*.jsx"],
"exclude": ["node_modules"]
}
}
+7 -6
View File
@@ -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.
View File
File renamed without changes.
+3 -3
View File
@@ -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.
+3
View File
@@ -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.
+2
View File
@@ -0,0 +1,2 @@
export { default as Header } from './Header';
export { default as DonateModal } from './DonateModal';
+26
View File
@@ -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'
};
+32
View File
@@ -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));
}
+2
View File
@@ -0,0 +1,2 @@
export { getRandomMapillaryImage } from './mapillary';
export { calculateDistance } from './distance';
+6 -44
View File
@@ -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));
}