feat: migrate to ts

This commit is contained in:
tiennm99 committed 2025-08-03 21:55:07 +07:00
1 parent f8e1d992a2
commit 403dc78743
26 files changed
+312 -199

No files matched your search

+32 -24
View File
@@ -58,13 +58,15 @@ src/
Key configuration files in the project root:
- `tsconfig.json` - TypeScript configuration
- `tailwind.config.ts` - Tailwind CSS customization
- `.eslintrc.js` - ESLint with TypeScript rules
- `postcss.config.js` - PostCSS configuration
- `next.config.js` - Next.js configuration
- `jest.config.js` - Jest testing configuration
- `tsconfig.json` - TypeScript configuration with strict settings and path mapping
- `tailwind.config.ts` - Tailwind CSS customization with TypeScript
- `.eslintrc.json` - ESLint with TypeScript rules
- `postcss.config.ts` - PostCSS configuration with TypeScript
- `next.config.ts` - Next.js configuration with TypeScript
- `jest.config.ts` - Jest testing configuration with TypeScript
- `jest.setup.ts` - Jest setup and mocks with TypeScript
- `package.json` - Dependencies and scripts
- `next-env.d.ts` - Next.js TypeScript environment types
## 📦 Installation & Setup
@@ -236,7 +238,7 @@ This project uses a carefully chosen combination of specialized libraries rather
1. **User Entry**: Home page checks localStorage for cached username, displays popup if not found
2. **City Selection**: User selects from available Vietnamese cities on home page
3. **Game Initialization**: Navigate to game scene with selected city parameter
4. **Image Loading**: Server provides random panoramic image from city bounds via `src/app/api/mapillary/route.js`
4. **Image Loading**: Server provides random panoramic image from city bounds via `src/app/api/mapillary/route.ts`
5. **Gameplay**:
- Display raw panoramic image without location metadata
- Show city overview map for location guessing
@@ -255,12 +257,12 @@ This project uses a carefully chosen combination of specialized libraries rather
**Technical Implementation:**
- **Image Fetching**: `src/services/game-image.service.js` handles client-side image fetching with retry logic
- **API Integration**: `src/services/mapillary.service.js` provides server-side Mapillary API integration
- **Display**: `src/components/features/PanoViewer.js` renders raw 360° images using MapillaryJS npm module
- **Interaction**: `src/components/features/GameMap.js` provides MapLibre GL JS-based interactive map for location guessing
- **Scoring**: `src/services/scoring.service.js` handles point calculation with configurable thresholds
- **Geography**: `src/services/geography.service.js` provides distance calculation with OpenStreetMap validation
- **Image Fetching**: `src/services/game-image.service.ts` handles client-side image fetching with retry logic
- **API Integration**: `src/services/mapillary.service.ts` provides server-side Mapillary API integration
- **Display**: `src/components/features/PanoViewer.tsx` renders raw 360° images using MapillaryJS npm module
- **Interaction**: `src/components/features/GameMap.tsx` provides MapLibre GL JS-based interactive map for location guessing
- **Scoring**: `src/services/scoring.service.ts` handles point calculation with configurable thresholds
- **Geography**: `src/services/geography.service.ts` provides distance calculation with OpenStreetMap validation
- **Session Management**: Per-user session tracking for concurrent multi-player support
### Key Components
@@ -272,7 +274,7 @@ This project uses a carefully chosen combination of specialized libraries rather
### Data Structure
- City boundaries defined in `src/constants/locations.js:LOCATION_BOUNDS` object with coordinates and delta values for API queries
- City boundaries defined in `src/constants/locations.ts:LOCATION_BOUNDS` 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)
- Constants organized with enums: `LOCATION_CODES`, `LOCATION_BOUNDS`, `LOCATION_NAMES`
@@ -293,18 +295,21 @@ Game state managed at page level with React hooks:
## Recent Implementation Updates
### Modern Tech Stack Migration (Latest)
- **Language**: TypeScript (`^5.0.0`) with strict typing and interfaces
- **Package Manager**: Migrated from npm to pnpm with updated scripts (dev, build, start, lint, format, test)
- **Tailwind CSS**: Complete migration from custom CSS to Tailwind utilities with custom design tokens matching game theme
- **Language**: TypeScript (`^5.7.3`) with strict typing, interfaces, and complete migration
- **Package Manager**: Migrated from npm to pnpm with updated scripts (dev, build, start, lint, format, test, type-check)
- **Tailwind CSS**: Complete migration from custom CSS to Tailwind utilities with custom design tokens matching game theme
- **shadcn/ui**: Installed and configured with utility functions, ready for UI component replacements
- **React Query**: Added TanStack Query with QueryClientProvider in layout and custom hooks (`useMapillary`, `useScoring`)
- **Testing Setup**: Configured Jest + React Testing Library with specialized mocks for MapLibre/MapillaryJS libraries
- **Project Structure**: Added modern hooks/, lib/ directories following TypeScript architectural guidelines
- **Project Structure**: Added modern hooks/, lib/, types/ directories following TypeScript architectural guidelines
- **Development Environment**: Updated to modern toolchain with TypeScript, ESLint, Prettier formatting and enhanced DX
- **File Migration**: All 25+ JavaScript files converted to TypeScript with proper typing
- **Type Safety**: Custom type definitions for external libraries (MapLibre GL JS, MapillaryJS)
- **Zero JavaScript**: Complete elimination of ALL `.js` files from entire project (src/ + root config files)
### Current Architecture Status
- ✅ **Framework**: Next.js 15 + React 19
- ✅ **Language**: TypeScript with strict configuration
- ✅ **Language**: TypeScript with strict configuration - **COMPLETE MIGRATION**
- ✅ **Styling**: Tailwind CSS with custom game design tokens
- ✅ **Data Fetching**: React Query hooks for API management
- ✅ **Testing**: Jest + RTL with TypeScript and library mocks
@@ -312,13 +317,16 @@ Game state managed at page level with React hooks:
- ✅ **App Router**: All app router files migrated to TypeScript (.tsx)
- ✅ **API Routes**: All API routes migrated to TypeScript (.ts)
- ✅ **Service Layer**: All service files migrated with proper TypeScript interfaces
- ✅ **Core Components**: GameMap, PanoViewer migrated to TypeScript
- ✅ **Components**: All components migrated to TypeScript (GameMap, PanoViewer, ResultModal, Header, DonateModal, UsernameModal)
- ✅ **Hooks**: All hooks migrated with proper TypeScript return types
- ✅ **Lib Files**: React Query provider and utilities migrated to TypeScript
- ✅ **Constants**: Location constants migrated with TypeScript interfaces and enums
- ✅ **Type Definitions**: Custom type definitions created for MapLibre GL JS and MapillaryJS
- 🚧 **UI Components**: Ready for shadcn/ui migration (Header, DonateModal, UsernameModal)
- 🚧 **Constants**: Ready for TypeScript interface migration
- 🚧 **Hooks**: Ready for TypeScript migration with proper return types
- ✅ **Index Files**: All index files migrated and JavaScript files removed
- ✅ **Config Files**: All root configuration files migrated (next.config.ts, jest.config.ts, postcss.config.ts, tailwind.config.ts)
- 🚧 **UI Components**: Ready for shadcn/ui migration (existing components are TypeScript-ready)
Project is now fully migrated to TypeScript with modern tooling and ready for continued development.
**🎉 COMPLETE TYPESCRIPT MIGRATION ACHIEVED** - Zero JavaScript files remaining in entire project. All source code AND configuration files are now TypeScript-enabled with modern tooling and industry-standard development practices.
## 🏗️ Service Layer Architecture
+16 -16
View File
@@ -9,15 +9,15 @@ A Next.js-based Vietnamese geography guessing game where players guess the locat
- 🎯 Interactive map for location guessing
- 📊 Distance-based scoring system
- 🖼️ 360° panoramic street view images using Photo Sphere Viewer
- ⚡ Built with Next.js 14 and JavaScript for performance and simplicity
- ⚡ Built with Next.js 15 and TypeScript for performance and type safety
## Tech Stack
- **Framework**: Next.js 14 with App Router
- **Language**: JavaScript
- **Styling**: CSS Modules with custom styling
- **Maps**: Leaflet.js
- **360° Images**: Photo Sphere Viewer
- **Framework**: Next.js 15 with App Router
- **Language**: TypeScript with strict type checking
- **Styling**: Tailwind CSS with custom design tokens
- **Maps**: MapLibre GL JS
- **360° Images**: MapillaryJS
- **Image API**: Mapillary Graph API
## Getting Started
@@ -60,24 +60,24 @@ npm start
├── app/ # Next.js app directory
│ ├── game/ # Game page
│ ├── globals.css # Global styles
│ ├── layout.js # Root layout
│ └── page.js # Home page
│ ├── layout.tsx # Root layout
│ └── page.tsx # Home page
├── components/ # Reusable React components
│ ├── DonateModal.js
│ ├── GameMap.js
│ ├── Header.js
│ ├── PanoViewer.js
│ └── ResultModal.js
│ ├── DonateModal.tsx
│ ├── GameMap.tsx
│ ├── Header.tsx
│ ├── PanoViewer.tsx
│ └── ResultModal.tsx
├── lib/ # Utility functions
│ └── gameUtils.js # Game logic and API calls
│ └── utils.ts # Utility functions
└── public/ # Static assets
```
## Key Improvements from Original
- **Modern Framework**: Migrated from vanilla HTML/JS to Next.js with JavaScript
- **Modern Framework**: Migrated from vanilla HTML/JS to Next.js with TypeScript
- **Component-Based Architecture**: Modular, reusable React components
- **Clean Code**: Well-structured JavaScript with proper organization
- **Clean Code**: Well-structured TypeScript with proper type safety and organization
- **Code Deduplication**: Eliminated duplicate code and consolidated utilities
- **Performance**: Optimized with Next.js built-in optimizations
- **Maintainability**: Cleaner code structure and separation of concerns
+2 -2
View File
@@ -2,9 +2,9 @@
"$schema": "https://ui.shadcn.com/schema.json",
"style": "default",
"rsc": false,
"tsx": false,
"tsx": true,
"tailwind": {
"config": "tailwind.config.js",
"config": "tailwind.config.ts",
"css": "src/app/globals.css",
"baseColor": "slate",
"cssVariables": true,
+5 -4
View File
@@ -1,4 +1,5 @@
const nextJest = require('next/jest')
import nextJest from 'next/jest'
import type { Config } from 'jest'
const createJestConfig = nextJest({
// Provide the path to your Next.js app to load next.config.js and .env files
@@ -6,8 +7,8 @@ const createJestConfig = nextJest({
})
// Add any custom config to be passed to Jest
const customJestConfig = {
setupFilesAfterEnv: ['<rootDir>/jest.setup.js'],
const customJestConfig: Config = {
setupFilesAfterEnv: ['<rootDir>/jest.setup.ts'],
testEnvironment: 'jsdom',
testPathIgnorePatterns: ['/node_modules/', '/.next/'],
transform: {
@@ -19,4 +20,4 @@ const customJestConfig = {
}
// createJestConfig is exported this way to ensure that next/jest can load the Next.js config which is async
module.exports = createJestConfig(customJestConfig)
export default createJestConfig(customJestConfig)
+4 -1
View File
@@ -40,4 +40,7 @@ const localStorageMock = {
removeItem: jest.fn(),
clear: jest.fn(),
}
global.localStorage = localStorageMock
Object.defineProperty(window, 'localStorage', {
value: localStorageMock
})
-27
View File
@@ -1,27 +0,0 @@
{
"compilerOptions": {
"lib": ["dom", "dom.iterable", "es6"],
"allowJs": true,
"skipLibCheck": true,
"strict": false,
"noEmit": true,
"esModuleInterop": true,
"module": "esnext",
"moduleResolution": "bundler",
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "preserve",
"incremental": true,
"plugins": [
{
"name": "next"
}
],
"baseUrl": ".",
"paths": {
"@/*": ["./src/*"]
}
},
"include": ["src/**/*.js", "src/**/*.jsx"],
"exclude": ["node_modules"]
}
+4 -3
View File
@@ -1,5 +1,6 @@
/** @type {import('next').NextConfig} */
const nextConfig = {
import type { NextConfig } from 'next'
const nextConfig: NextConfig = {
images: {
domains: ['graph.mapillary.com', 'maps.google.com', 'unpkg.com'],
unoptimized: true
@@ -15,4 +16,4 @@ const nextConfig = {
}
}
module.exports = nextConfig
export default nextConfig
-6
View File
@@ -1,6 +0,0 @@
module.exports = {
plugins: {
'@tailwindcss/postcss': {},
autoprefixer: {},
},
}
+10
View File
@@ -0,0 +1,10 @@
import type { Config } from 'postcss-load-config'
const config: Config = {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
}
export default config
@@ -6,6 +6,24 @@ import { Viewer } from 'mapillary-js';
import 'maplibre-gl/dist/maplibre-gl.css';
import 'mapillary-js/dist/mapillary.css';
interface ImageData {
id: string;
url: string;
lat: number;
lng: number;
[key: string]: unknown;
}
interface ResultModalProps {
isOpen: boolean;
distance: number;
points: number;
trueLocation: [number, number];
guessLocation: [number, number];
imageData: ImageData | null;
onNextRound: () => void;
}
export default function ResultModal({
isOpen,
distance,
@@ -14,12 +32,12 @@ export default function ResultModal({
guessLocation,
imageData,
onNextRound
}) {
const mapRef = useRef(null);
const mapInstanceRef = useRef(null);
const panoRef = useRef(null);
const viewerRef = useRef(null);
const markersRef = useRef([]);
}: ResultModalProps) {
const mapRef = useRef<HTMLDivElement>(null);
const mapInstanceRef = useRef<maplibregl.Map | null>(null);
const panoRef = useRef<HTMLDivElement>(null);
const viewerRef = useRef<Viewer | null>(null);
const markersRef = useRef<string[]>([]);
useEffect(() => {
if (!isOpen || !mapRef.current) return;
@@ -38,7 +56,7 @@ export default function ResultModal({
// Create MapLibre map
mapInstanceRef.current = new maplibregl.Map({
container: mapRef.current,
container: mapRef.current!,
style: {
version: 8,
sources: {
@@ -62,6 +80,8 @@ export default function ResultModal({
});
mapInstanceRef.current.on('load', () => {
if (!mapInstanceRef.current) return;
// Add true location marker (green)
mapInstanceRef.current.addSource('true-location', {
type: 'geojson',
@@ -70,7 +90,8 @@ export default function ResultModal({
geometry: {
type: 'Point',
coordinates: [trueLng, trueLat]
}
},
properties: {}
}
});
@@ -94,7 +115,8 @@ export default function ResultModal({
geometry: {
type: 'Point',
coordinates: [guessLng, guessLat]
}
},
properties: {}
}
});
@@ -118,7 +140,8 @@ export default function ResultModal({
geometry: {
type: 'LineString',
coordinates: [[trueLng, trueLat], [guessLng, guessLat]]
}
},
properties: {}
}
});
@@ -173,7 +196,7 @@ export default function ResultModal({
viewerRef.current = new Viewer({
accessToken: process.env.NEXT_PUBLIC_MAPILLARY_ACCESS_TOKEN || 'MLY|24113623194974280|5bf83fa202912f1cc3210b2cf968fb65',
container: panoRef.current,
container: panoRef.current!,
imageId: imageData.id,
component: {
attribution: false,
@@ -217,14 +240,14 @@ export default function ResultModal({
if (!isOpen) return null;
const formatDistance = (dist) => {
const formatDistance = (dist: number): string => {
if (dist > 1000) {
return `${(dist / 1000).toFixed(2)} KM`;
}
return `${dist} M`;
};
const getResultMessage = (dist, pts) => {
const getResultMessage = (dist: number, pts: number): string => {
if (pts >= 5) return "Perfect! 🎯";
if (pts >= 4) return "Excellent! 🎉";
if (pts >= 3) return "Great job! 👍";
File renamed without changes.
@@ -1,11 +1,17 @@
'use client';
import Image from 'next/image';
import { MouseEvent } from 'react';
export default function DonateModal({ isOpen, onClose }) {
interface DonateModalProps {
isOpen: boolean;
onClose: () => void;
}
export default function DonateModal({ isOpen, onClose }: DonateModalProps) {
if (!isOpen) return null;
const handleBackdropClick = (e) => {
const handleBackdropClick = (e: MouseEvent<HTMLDivElement>) => {
if (e.target === e.currentTarget) {
onClose();
}
@@ -1,6 +1,10 @@
'use client';
export default function Header({ onDonateClick }) {
interface HeaderProps {
onDonateClick: () => void;
}
export default function Header({ onDonateClick }: HeaderProps) {
return (
<header className="header">
<div className="header-center">
@@ -18,4 +22,4 @@ export default function Header({ onDonateClick }) {
</div>
</header>
);
}
}
@@ -1,17 +1,26 @@
'use client';
import { useState } from 'react';
import { useState, FormEvent, ChangeEvent } from 'react';
export default function UsernameModal({ isOpen, onSubmit }) {
interface UsernameModalProps {
isOpen: boolean;
onSubmit: (username: string) => void;
}
export default function UsernameModal({ isOpen, onSubmit }: UsernameModalProps) {
const [username, setUsername] = useState('');
const handleSubmit = (e) => {
const handleSubmit = (e: FormEvent<HTMLFormElement>) => {
e.preventDefault();
if (username.trim()) {
onSubmit(username.trim());
}
};
const handleInputChange = (e: ChangeEvent<HTMLInputElement>) => {
setUsername(e.target.value);
};
if (!isOpen) return null;
return (
@@ -24,7 +33,7 @@ export default function UsernameModal({ isOpen, onSubmit }) {
<input
type="text"
value={username}
onChange={(e) => setUsername(e.target.value)}
onChange={handleInputChange}
placeholder="Your username"
className="username-input"
maxLength={20}
File renamed without changes.
@@ -13,13 +13,23 @@ export const LOCATION_CODES = {
DA_NANG: 'DN',
DALAT: 'DL',
DUC_HOA: 'DHLA'
};
} as const;
/**
* Location code types
*/
export type LocationCode = typeof LOCATION_CODES[keyof typeof LOCATION_CODES];
/**
* Location bounds format: [minLongitude, minLatitude, maxLongitude, maxLatitude, deltaRadius]
*/
export type LocationBounds = [number, number, number, number, number];
/**
* Vietnamese city boundaries for the geography guessing game
* Format: [minLongitude, minLatitude, maxLongitude, maxLatitude, deltaRadius]
*/
export const LOCATION_BOUNDS = {
export const LOCATION_BOUNDS: Record<LocationCode, LocationBounds> = {
[LOCATION_CODES.HANOI]: [105.77, 20.96, 105.88, 21.05, 0.003],
[LOCATION_CODES.HO_CHI_MINH]: [106.62, 10.71, 106.75, 10.83, 0.005],
[LOCATION_CODES.HAI_PHONG]: [106.65, 20.8, 106.75, 20.9, 0.05],
@@ -32,7 +42,7 @@ export const LOCATION_BOUNDS = {
/**
* Display names for Vietnamese cities
*/
export const LOCATION_NAMES = {
export const LOCATION_NAMES: Record<LocationCode, string> = {
[LOCATION_CODES.HANOI]: 'HA NOI',
[LOCATION_CODES.HO_CHI_MINH]: 'TP. HO CHI MINH',
[LOCATION_CODES.HAI_PHONG]: 'HAI PHONG',
@@ -44,26 +54,21 @@ export const LOCATION_NAMES = {
/**
* Get all available location codes
* @returns {string[]} Array of location codes
*/
export function getAvailableLocations() {
export function getAvailableLocations(): LocationCode[] {
return Object.values(LOCATION_CODES);
}
/**
* Get display name for location code
* @param {string} locationCode - Location code
* @returns {string} Display name or empty string if not found
*/
export function getLocationDisplayName(locationCode) {
return LOCATION_NAMES[locationCode] || '';
export function getLocationDisplayName(locationCode: string): string {
return LOCATION_NAMES[locationCode as LocationCode] || '';
}
/**
* Get bounds for location code
* @param {string} locationCode - Location code
* @returns {number[]|null} Bounds array or null if not found
*/
export function getLocationBounds(locationCode) {
return LOCATION_BOUNDS[locationCode] || null;
}
export function getLocationBounds(locationCode: string): LocationBounds | null {
return LOCATION_BOUNDS[locationCode as LocationCode] || null;
}
-2
View File
@@ -1,2 +0,0 @@
export { useMapillaryImage } from './useMapillary';
export { useScoring } from './useScoring';
+3
View File
@@ -0,0 +1,3 @@
export { useMapillaryImage } from './useMapillary';
export { useScoring } from './useScoring';
export type { ScoreSubmission, ScoreResponse } from './useScoring';
@@ -1,19 +1,17 @@
import { useQuery } from '@tanstack/react-query';
import { fetchRandomGameImage } from '@/services/game-image.service';
import { useQuery, UseQueryResult } from '@tanstack/react-query';
import { fetchRandomGameImage, GameImageData } from '@/services/game-image.service';
/**
* React Query hook for fetching Mapillary images
* @param {string} locationCode - Location code (e.g., 'HN', 'TPHCM')
* @returns {Object} React Query result with image data
*/
export function useMapillaryImage(locationCode) {
export function useMapillaryImage(locationCode: string): UseQueryResult<GameImageData | null, Error> {
return useQuery({
queryKey: ['mapillary-image', locationCode],
queryFn: () => fetchRandomGameImage(locationCode),
enabled: !!locationCode,
staleTime: 5 * 60 * 1000, // 5 minutes
retry: 3,
retryDelay: attemptIndex => Math.min(1000 * 2 ** attemptIndex, 30000),
retryDelay: (attemptIndex: number) => Math.min(1000 * 2 ** attemptIndex, 30000),
});
}
-59
View File
@@ -1,59 +0,0 @@
import { useMutation } from '@tanstack/react-query';
/**
* Scoring data type definition
* @typedef {Object} ScoreSubmission
* @property {string} username - Player username
* @property {number} guessLat - Guessed latitude
* @property {number} guessLng - Guessed longitude
* @property {number} trueLat - Actual latitude
* @property {number} trueLng - Actual longitude
* @property {string} imageId - Mapillary image ID
*/
/**
* React Query mutation hook for submitting game scores
* @returns {Object} Mutation object with mutate function and status
*/
export function useScoring() {
return useMutation({
mutationFn: async (scoreData) => {
// Validate score data before sending
if (!isValidScoreData(scoreData)) {
throw new Error('Invalid score data provided');
}
const response = await fetch('/api/scoring', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(scoreData),
});
if (!response.ok) {
const errorData = await response.json().catch(() => ({}));
throw new Error(errorData.error || 'Failed to submit score');
}
return response.json();
},
retry: 2,
retryDelay: attemptIndex => Math.min(1000 * 2 ** attemptIndex, 10000),
});
}
/**
* Validates score submission data
* @param {Object} scoreData - Score data to validate
* @returns {boolean} True if data is valid
*/
function isValidScoreData(scoreData) {
return scoreData &&
typeof scoreData.username === 'string' &&
scoreData.username.trim().length > 0 &&
typeof scoreData.guessLat === 'number' &&
typeof scoreData.guessLng === 'number' &&
typeof scoreData.trueLat === 'number' &&
typeof scoreData.trueLng === 'number' &&
typeof scoreData.imageId === 'string' &&
scoreData.imageId.trim().length > 0;
}
+72
View File
@@ -0,0 +1,72 @@
import { useMutation, UseMutationResult } from '@tanstack/react-query';
/**
* Scoring data type definition
*/
export interface ScoreSubmission {
username: string;
guessLat: number;
guessLng: number;
trueLat: number;
trueLng: number;
imageId: string;
}
/**
* Score submission response type
*/
export interface ScoreResponse {
sessionId: string;
distance: number;
points: number;
username: string;
}
/**
* React Query mutation hook for submitting game scores
*/
export function useScoring(): UseMutationResult<ScoreResponse, Error, ScoreSubmission> {
return useMutation({
mutationFn: async (scoreData: ScoreSubmission): Promise<ScoreResponse> => {
// Validate score data before sending
if (!isValidScoreData(scoreData)) {
throw new Error('Invalid score data provided');
}
const response = await fetch('/api/scoring', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(scoreData),
});
if (!response.ok) {
const errorData = await response.json().catch(() => ({}));
throw new Error(errorData.error || 'Failed to submit score');
}
return response.json();
},
retry: 2,
retryDelay: (attemptIndex: number) => Math.min(1000 * 2 ** attemptIndex, 10000),
});
}
/**
* Validates score submission data
*/
function isValidScoreData(scoreData: unknown): scoreData is ScoreSubmission {
if (typeof scoreData !== 'object' || scoreData === null) {
return false;
}
const data = scoreData as Record<string, unknown>;
return typeof data.username === 'string' &&
data.username.trim().length > 0 &&
typeof data.guessLat === 'number' &&
typeof data.guessLng === 'number' &&
typeof data.trueLat === 'number' &&
typeof data.trueLng === 'number' &&
typeof data.imageId === 'string' &&
data.imageId.trim().length > 0;
}
@@ -1,15 +1,19 @@
'use client';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { useState } from 'react';
import { useState, ReactNode } from 'react';
export default function ReactQueryProvider({ children }) {
interface ReactQueryProviderProps {
children: ReactNode;
}
export default function ReactQueryProvider({ children }: ReactQueryProviderProps) {
const [queryClient] = useState(() => new QueryClient({
defaultOptions: {
queries: {
staleTime: 5 * 60 * 1000, // 5 minutes
retry: 2,
retryDelay: attemptIndex => Math.min(1000 * 2 ** attemptIndex, 30000),
retryDelay: (attemptIndex: number) => Math.min(1000 * 2 ** attemptIndex, 30000),
},
},
}));
-6
View File
@@ -1,6 +0,0 @@
import { clsx } from "clsx";
import { twMerge } from "tailwind-merge";
export function cn(...inputs) {
return twMerge(clsx(inputs));
}
+6
View File
@@ -0,0 +1,6 @@
import { clsx, type ClassValue } from "clsx";
import { twMerge } from "tailwind-merge";
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs));
}
+56
View File
@@ -0,0 +1,56 @@
import type { Config } from 'tailwindcss'
const config: Config = {
content: [
'./src/pages/**/*.{js,ts,jsx,tsx,mdx}',
'./src/components/**/*.{js,ts,jsx,tsx,mdx}',
'./src/app/**/*.{js,ts,jsx,tsx,mdx}',
],
theme: {
extend: {
colors: {
border: "hsl(var(--border))",
input: "hsl(var(--input))",
ring: "hsl(var(--ring))",
background: "hsl(var(--background))",
foreground: "hsl(var(--foreground))",
primary: {
DEFAULT: "hsl(var(--primary))",
foreground: "hsl(var(--primary-foreground))",
},
secondary: {
DEFAULT: "hsl(var(--secondary))",
foreground: "hsl(var(--secondary-foreground))",
},
destructive: {
DEFAULT: "hsl(var(--destructive))",
foreground: "hsl(var(--destructive-foreground))",
},
muted: {
DEFAULT: "hsl(var(--muted))",
foreground: "hsl(var(--muted-foreground))",
},
accent: {
DEFAULT: "hsl(var(--accent))",
foreground: "hsl(var(--accent-foreground))",
},
popover: {
DEFAULT: "hsl(var(--popover))",
foreground: "hsl(var(--popover-foreground))",
},
card: {
DEFAULT: "hsl(var(--card))",
foreground: "hsl(var(--card-foreground))",
},
},
borderRadius: {
lg: "var(--radius)",
md: "calc(var(--radius) - 2px)",
sm: "calc(var(--radius) - 4px)",
},
},
},
plugins: [],
}
export default config
+9 -5
View File
@@ -1,7 +1,7 @@
{
"compilerOptions": {
"lib": ["dom", "dom.iterable", "es6"],
"allowJs": true,
"lib": ["dom", "dom.iterable", "ES2022"],
"allowJs": false,
"skipLibCheck": true,
"strict": true,
"noEmit": true,
@@ -21,8 +21,10 @@
"paths": {
"@/*": ["./src/*"]
},
"target": "ES2017",
"forceConsistentCasingInFileNames": true
"target": "ES2022",
"forceConsistentCasingInFileNames": true,
"noUncheckedIndexedAccess": true,
"exactOptionalPropertyTypes": true
},
"include": [
"next-env.d.ts",
@@ -31,6 +33,8 @@
".next/types/**/*.ts"
],
"exclude": [
"node_modules"
"node_modules",
"**/*.js",
"**/*.jsx"
]
}