mirror of
https://github.com/tiennm99/vigeoguessr.git
synced 2026-10-11 12:28:57 +00:00
feat: temp
This commit is contained in:
1 parent
730e9a157a
commit
2db93c38b9
17 files changed
+7001
-6440
No files matched your search
@@ -11,8 +11,9 @@
|
||||
"Bash(cp:*)",
|
||||
"Bash(curl:*)",
|
||||
"WebFetch",
|
||||
"Bash(grep:*)"
|
||||
"Bash(grep:*)",
|
||||
"Bash(pnpm:*)"
|
||||
],
|
||||
"deny": []
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -260,3 +260,25 @@ Game state managed at page level with React hooks:
|
||||
- Image loading states and error handling
|
||||
- Location selection and guess submission
|
||||
- Distance calculation and result display
|
||||
|
||||
## Recent Implementation Updates
|
||||
|
||||
### Modern Tech Stack Migration (Latest)
|
||||
- **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
|
||||
- **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 new architectural guidelines
|
||||
- **Development Environment**: Updated to modern toolchain with Prettier formatting and enhanced development experience
|
||||
|
||||
### Current Architecture Status
|
||||
- ✅ **Framework**: Next.js 15 + React 19
|
||||
- ✅ **Styling**: Tailwind CSS with custom game design tokens
|
||||
- ✅ **Data Fetching**: React Query hooks for API management
|
||||
- ✅ **Testing**: Jest + RTL with library mocks
|
||||
- ✅ **Package Management**: pnpm with modern scripts
|
||||
- 🚧 **Components**: Existing game components (GameMap, PanoViewer, ResultModal) preserved
|
||||
- 🚧 **UI Components**: Ready for shadcn/ui migration (Header, DonateModal, UsernameModal)
|
||||
|
||||
Project is now modernized and ready for continued development with industry-standard tooling.
|
||||
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"$schema": "https://ui.shadcn.com/schema.json",
|
||||
"style": "default",
|
||||
"rsc": false,
|
||||
"tsx": false,
|
||||
"tailwind": {
|
||||
"config": "tailwind.config.js",
|
||||
"css": "src/app/globals.css",
|
||||
"baseColor": "slate",
|
||||
"cssVariables": true,
|
||||
"prefix": ""
|
||||
},
|
||||
"aliases": {
|
||||
"components": "@/components",
|
||||
"utils": "@/lib/utils"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
const nextJest = require('next/jest')
|
||||
|
||||
const createJestConfig = nextJest({
|
||||
// Provide the path to your Next.js app to load next.config.js and .env files
|
||||
dir: './',
|
||||
})
|
||||
|
||||
// Add any custom config to be passed to Jest
|
||||
const customJestConfig = {
|
||||
setupFilesAfterEnv: ['<rootDir>/jest.setup.js'],
|
||||
testEnvironment: 'jsdom',
|
||||
testPathIgnorePatterns: ['/node_modules/', '/.next/'],
|
||||
transform: {
|
||||
'^.+\\.(js|jsx|ts|tsx)$': ['babel-jest', { presets: ['next/babel'] }],
|
||||
},
|
||||
moduleNameMapping: {
|
||||
'^@/(.*)$': '<rootDir>/src/$1',
|
||||
},
|
||||
}
|
||||
|
||||
// createJestConfig is exported this way to ensure that next/jest can load the Next.js config which is async
|
||||
module.exports = createJestConfig(customJestConfig)
|
||||
@@ -0,0 +1,43 @@
|
||||
import '@testing-library/jest-dom'
|
||||
|
||||
// Mock MapLibre GL JS
|
||||
jest.mock('maplibre-gl', () => ({
|
||||
Map: jest.fn(() => ({
|
||||
on: jest.fn(),
|
||||
off: jest.fn(),
|
||||
remove: jest.fn(),
|
||||
addSource: jest.fn(),
|
||||
addLayer: jest.fn(),
|
||||
fitBounds: jest.fn(),
|
||||
getCanvas: jest.fn(() => ({
|
||||
addEventListener: jest.fn(),
|
||||
removeEventListener: jest.fn(),
|
||||
})),
|
||||
})),
|
||||
LngLatBounds: jest.fn(() => ({
|
||||
extend: jest.fn(),
|
||||
})),
|
||||
}))
|
||||
|
||||
// Mock MapillaryJS
|
||||
jest.mock('mapillary-js', () => ({
|
||||
Viewer: jest.fn(() => ({
|
||||
on: jest.fn(),
|
||||
off: jest.fn(),
|
||||
remove: jest.fn(),
|
||||
getPosition: jest.fn(() => Promise.resolve({ lat: 0, lng: 0 })),
|
||||
})),
|
||||
}))
|
||||
|
||||
// Mock CSS imports
|
||||
jest.mock('maplibre-gl/dist/maplibre-gl.css', () => ({}))
|
||||
jest.mock('mapillary-js/dist/mapillary.css', () => ({}))
|
||||
|
||||
// Mock localStorage
|
||||
const localStorageMock = {
|
||||
getItem: jest.fn(),
|
||||
setItem: jest.fn(),
|
||||
removeItem: jest.fn(),
|
||||
clear: jest.fn(),
|
||||
}
|
||||
global.localStorage = localStorageMock
|
||||
Generated
-6000
File diff suppressed because it is too large.
Load diff
+19
-3
@@ -6,17 +6,33 @@
|
||||
"dev": "next dev",
|
||||
"build": "next build",
|
||||
"start": "next start",
|
||||
"lint": "next lint"
|
||||
"lint": "next lint",
|
||||
"format": "prettier --write .",
|
||||
"test": "jest",
|
||||
"test:watch": "jest --watch"
|
||||
},
|
||||
"dependencies": {
|
||||
"@tanstack/react-query": "^5.84.1",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
"mapillary-js": "^4.0.0",
|
||||
"maplibre-gl": "^5.0.0",
|
||||
"next": "^15.0.0",
|
||||
"react": "^19.0.0",
|
||||
"react-dom": "^19.0.0"
|
||||
"react-dom": "^19.0.0",
|
||||
"tailwind-merge": "^3.3.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@testing-library/jest-dom": "^6.6.4",
|
||||
"@testing-library/react": "^16.3.0",
|
||||
"@testing-library/user-event": "^14.6.1",
|
||||
"autoprefixer": "^10.4.21",
|
||||
"eslint": "^9.0.0",
|
||||
"eslint-config-next": "^15.0.0"
|
||||
"eslint-config-next": "^15.0.0",
|
||||
"jest": "^30.0.5",
|
||||
"jest-environment-jsdom": "^30.0.5",
|
||||
"postcss": "^8.5.6",
|
||||
"prettier": "^3.6.2",
|
||||
"tailwindcss": "^4.1.11"
|
||||
}
|
||||
}
|
||||
Generated
+6679
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,6 @@
|
||||
module.exports = {
|
||||
plugins: {
|
||||
tailwindcss: {},
|
||||
autoprefixer: {},
|
||||
},
|
||||
}
|
||||
+86
-433
@@ -1,460 +1,113 @@
|
||||
@import url('https://fonts.googleapis.com/css2?family=Jersey+15:wght@400&display=swap');
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
/* Reset and base styles */
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
@layer base {
|
||||
* {
|
||||
@apply box-border;
|
||||
}
|
||||
|
||||
body {
|
||||
@apply font-courier bg-background text-text min-h-screen;
|
||||
}
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: 'Courier', monospace;
|
||||
background-color: #1a1a1a;
|
||||
color: #e6f4e6;
|
||||
min-height: 100vh;
|
||||
position: relative;
|
||||
}
|
||||
@layer components {
|
||||
/* Main container */
|
||||
.main-container {
|
||||
@apply relative min-h-screen overflow-hidden;
|
||||
}
|
||||
|
||||
/* Main container */
|
||||
.main-container {
|
||||
position: relative;
|
||||
min-height: 100vh;
|
||||
overflow: hidden;
|
||||
}
|
||||
.background-layer {
|
||||
@apply fixed inset-0 bg-gradient-to-br from-[#0f4c3a] via-[#1a6b4f] to-[#2d8f6d] -z-10;
|
||||
}
|
||||
|
||||
.background-layer {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: linear-gradient(135deg, #0f4c3a 0%, #1a6b4f 50%, #2d8f6d 100%);
|
||||
z-index: -1;
|
||||
}
|
||||
.content-wrapper {
|
||||
@apply relative z-10 min-h-screen flex flex-col;
|
||||
}
|
||||
|
||||
.content-wrapper {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.gradient-overlay {
|
||||
.gradient-overlay {
|
||||
@apply flex-1 flex flex-col;
|
||||
background: linear-gradient(rgba(26, 26, 26, 0.3), rgba(26, 26, 26, 0.7));
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
}
|
||||
|
||||
/* Header styles */
|
||||
.header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 20px 40px;
|
||||
background: rgba(0, 0, 0, 0.2);
|
||||
backdrop-filter: blur(10px);
|
||||
}
|
||||
/* Game sections */
|
||||
.game-section {
|
||||
@apply flex-1 flex flex-col p-4;
|
||||
}
|
||||
|
||||
.header-center {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 15px;
|
||||
}
|
||||
.score-header {
|
||||
@apply flex items-center justify-between mb-4;
|
||||
}
|
||||
|
||||
.logo-icon {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
background-size: contain;
|
||||
background-repeat: no-repeat;
|
||||
background-position: center;
|
||||
}
|
||||
.score-text {
|
||||
@apply font-jersey text-3xl text-primary;
|
||||
}
|
||||
|
||||
.logo-title {
|
||||
font-family: 'Jersey 15', cursive;
|
||||
font-size: 28px;
|
||||
color: #e6f4e6;
|
||||
margin: 0;
|
||||
}
|
||||
.play-section {
|
||||
@apply mb-6;
|
||||
}
|
||||
|
||||
.header-right {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
.play-title {
|
||||
@apply font-jersey text-4xl text-primary text-center;
|
||||
}
|
||||
|
||||
.connect-wallet {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
background: linear-gradient(135deg, #057f6d, #024d42);
|
||||
border: 2px solid #05A38C;
|
||||
border-radius: 8px;
|
||||
padding: 8px 16px;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
.game-content {
|
||||
@apply flex flex-1 gap-4;
|
||||
}
|
||||
|
||||
.wallet-text {
|
||||
font-family: 'Jersey 15', cursive;
|
||||
font-size: 14px;
|
||||
color: #e6f4e6;
|
||||
white-space: nowrap;
|
||||
}
|
||||
/* Panoramic viewer */
|
||||
.pano {
|
||||
@apply flex-1 rounded-lg bg-background-secondary border-4 border-primary min-h-96;
|
||||
}
|
||||
|
||||
/* Game section */
|
||||
.game-section {
|
||||
flex: 1;
|
||||
padding: 20px 40px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
/* Right panel */
|
||||
.right-panel {
|
||||
@apply w-80 flex flex-col gap-4;
|
||||
}
|
||||
|
||||
.score-header {
|
||||
text-align: center;
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
/* Buttons */
|
||||
.bridge-btn {
|
||||
@apply bg-primary hover:bg-primary-hover text-white font-jersey text-xl py-3 px-6 rounded-lg transition-colors border-2 border-primary;
|
||||
}
|
||||
|
||||
.score-text {
|
||||
font-family: 'Jersey 15', cursive;
|
||||
font-size: 24px;
|
||||
color: #e6f4e6;
|
||||
text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.5);
|
||||
}
|
||||
.back-btn {
|
||||
@apply w-12 h-12 bg-primary hover:bg-primary-hover text-white rounded-lg flex items-center justify-center text-2xl transition-colors;
|
||||
}
|
||||
|
||||
.back-btn {
|
||||
position: absolute;
|
||||
top: 20px;
|
||||
left: 20px;
|
||||
background: linear-gradient(135deg, #057f6d, #024d42);
|
||||
border: 2px solid #05A38C;
|
||||
border-radius: 8px;
|
||||
padding: 10px;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
.check-btn {
|
||||
@apply w-full bg-primary hover:bg-primary-hover disabled:bg-gray-600 disabled:cursor-not-allowed text-white font-jersey text-lg py-3 rounded-lg transition-colors;
|
||||
}
|
||||
|
||||
.diamond-icon {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
background-size: contain;
|
||||
background-repeat: no-repeat;
|
||||
background-position: center;
|
||||
}
|
||||
.next-btn {
|
||||
@apply bg-primary hover:bg-primary-hover text-white font-jersey text-lg py-3 px-8 rounded-lg transition-colors;
|
||||
}
|
||||
|
||||
/* Play section */
|
||||
.play-section {
|
||||
text-align: center;
|
||||
margin-bottom: 40px;
|
||||
}
|
||||
/* Modal styles */
|
||||
.modal {
|
||||
@apply fixed inset-0 bg-black/60 flex items-center justify-center z-[9999];
|
||||
}
|
||||
|
||||
.play-title-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 20px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
.modal-content {
|
||||
@apply bg-background-secondary p-5 rounded-xl w-[90%] max-w-2xl text-center text-text border-4 border-primary font-jersey text-xl;
|
||||
}
|
||||
|
||||
.play-title {
|
||||
font-family: 'Jersey 15', cursive;
|
||||
font-size: 36px;
|
||||
color: #e6f4e6;
|
||||
text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.5);
|
||||
margin: 0;
|
||||
}
|
||||
/* Distance and result text */
|
||||
.distance-text {
|
||||
@apply font-jersey text-3xl text-primary;
|
||||
}
|
||||
|
||||
/* Game content */
|
||||
.game-content {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 300px;
|
||||
gap: 30px;
|
||||
flex: 1;
|
||||
min-height: 500px;
|
||||
}
|
||||
/* Instructions */
|
||||
.instruction-list {
|
||||
@apply space-y-2 mb-6;
|
||||
}
|
||||
|
||||
.pano {
|
||||
background: rgba(0, 0, 0, 0.3);
|
||||
border-radius: 12px;
|
||||
padding: 20px;
|
||||
backdrop-filter: blur(5px);
|
||||
border: 2px solid #05A38C;
|
||||
position: relative;
|
||||
min-height: 400px;
|
||||
}
|
||||
.instruction {
|
||||
@apply font-jersey text-lg text-text;
|
||||
}
|
||||
|
||||
.how-title {
|
||||
font-family: 'Jersey 15', cursive;
|
||||
font-size: 28px;
|
||||
color: #e6f4e6;
|
||||
text-align: center;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.instruction-list {
|
||||
text-align: left;
|
||||
max-width: 400px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.instruction {
|
||||
font-family: 'Jersey 15', cursive;
|
||||
font-size: 18px;
|
||||
color: #e6f4e6;
|
||||
margin-bottom: 10px;
|
||||
padding: 10px;
|
||||
background: rgba(5, 163, 140, 0.1);
|
||||
border-radius: 8px;
|
||||
border-left: 4px solid #05A38C;
|
||||
}
|
||||
|
||||
/* Right panel */
|
||||
.right-panel {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 15px;
|
||||
}
|
||||
|
||||
.bridge-btn {
|
||||
background: linear-gradient(135deg, #057f6d, #024d42);
|
||||
border: 2px solid #05A38C;
|
||||
border-radius: 12px;
|
||||
padding: 16px;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.bridge-btn:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 5px 15px rgba(5, 163, 140, 0.3);
|
||||
}
|
||||
|
||||
.bridge-text {
|
||||
font-family: 'Jersey 15', cursive;
|
||||
font-size: 16px;
|
||||
color: #e6f4e6;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.check-btn {
|
||||
background: linear-gradient(135deg, #057f6d, #024d42);
|
||||
border: 2px solid #05A38C;
|
||||
border-radius: 12px;
|
||||
padding: 16px;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
text-align: center;
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.check-btn:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.check-text {
|
||||
font-family: 'Jersey 15', cursive;
|
||||
font-size: 16px;
|
||||
color: #e6f4e6;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.disclaimer {
|
||||
background: rgba(0, 0, 0, 0.2);
|
||||
border-radius: 8px;
|
||||
min-height: 300px;
|
||||
border: 2px solid #05A38C;
|
||||
cursor: crosshair;
|
||||
}
|
||||
|
||||
/* Modal styles */
|
||||
.modal {
|
||||
display: none;
|
||||
position: fixed;
|
||||
z-index: 9999;
|
||||
left: 0;
|
||||
top: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
overflow: auto;
|
||||
background-color: rgba(0, 0, 0, 0.6);
|
||||
}
|
||||
|
||||
.modal-content {
|
||||
background-color: #24453D;
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
padding: 20px;
|
||||
border-radius: 12px;
|
||||
width: 90%;
|
||||
max-width: 600px;
|
||||
text-align: center;
|
||||
color: #e6f4e6;
|
||||
border: #05A38C 5px solid;
|
||||
font-family: 'Jersey 15', cursive;
|
||||
font-size: 20px;
|
||||
line-height: 20px;
|
||||
}
|
||||
|
||||
.modal-donate {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
background-color: #1a1a1a;
|
||||
padding: 30px;
|
||||
border-radius: 12px;
|
||||
text-align: center;
|
||||
border: #05A38C 5px solid;
|
||||
font-family: 'Jersey 15', cursive;
|
||||
color: #e6f4e6;
|
||||
}
|
||||
|
||||
.modal-overlay {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-color: rgba(0, 0, 0, 0.7);
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
z-index: 9999;
|
||||
}
|
||||
|
||||
.username-modal {
|
||||
max-width: 400px;
|
||||
width: 90%;
|
||||
}
|
||||
|
||||
.modal-title {
|
||||
font-family: 'Jersey 15', cursive;
|
||||
font-size: 24px;
|
||||
color: #05A38C;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.modal-subtitle {
|
||||
font-family: 'Jersey 15', cursive;
|
||||
font-size: 16px;
|
||||
color: #e6f4e6;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.username-input {
|
||||
width: 100%;
|
||||
padding: 12px;
|
||||
margin-bottom: 20px;
|
||||
border: 2px solid #05A38C;
|
||||
border-radius: 8px;
|
||||
background-color: #1a1a1a;
|
||||
color: #e6f4e6;
|
||||
font-family: 'Jersey 15', cursive;
|
||||
font-size: 16px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.username-input:focus {
|
||||
outline: none;
|
||||
border-color: #05A38C;
|
||||
box-shadow: 0 0 0 2px rgba(5, 163, 140, 0.3);
|
||||
}
|
||||
|
||||
.username-submit-btn {
|
||||
width: 100%;
|
||||
padding: 12px;
|
||||
background-color: #05A38C;
|
||||
color: #fff;
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
font-family: 'Jersey 15', cursive;
|
||||
font-size: 16px;
|
||||
cursor: pointer;
|
||||
transition: background-color 0.3s;
|
||||
}
|
||||
|
||||
.username-submit-btn:hover:not(:disabled) {
|
||||
background-color: #048a76;
|
||||
}
|
||||
|
||||
.username-submit-btn:disabled {
|
||||
background-color: #666;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.distance-text {
|
||||
font-family: 'Jersey 15', cursive;
|
||||
font-size: 32px;
|
||||
color: #05A38C;
|
||||
display: block;
|
||||
margin: 20px 0;
|
||||
}
|
||||
|
||||
.container {
|
||||
margin: 20px 0;
|
||||
height: 300px;
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.next-btn {
|
||||
margin-top: 20px;
|
||||
background: linear-gradient(135deg, #057f6d, #024d42);
|
||||
border: #05A38C 5px solid;
|
||||
border-radius: 12px;
|
||||
padding: 12px 24px;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
text-align: center;
|
||||
min-width: 200px;
|
||||
}
|
||||
|
||||
.next-text {
|
||||
font-family: 'Jersey 15', cursive;
|
||||
font-size: 16px;
|
||||
line-height: 16px;
|
||||
color: #e6f4e6;
|
||||
}
|
||||
|
||||
.qr {
|
||||
border-radius: 8px;
|
||||
margin: 20px 0;
|
||||
}
|
||||
|
||||
/* Responsive design */
|
||||
@media (max-width: 768px) {
|
||||
.header {
|
||||
padding: 15px 20px;
|
||||
}
|
||||
|
||||
.game-section {
|
||||
padding: 15px 20px;
|
||||
}
|
||||
|
||||
.game-content {
|
||||
grid-template-columns: 1fr;
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.play-title {
|
||||
font-size: 28px;
|
||||
}
|
||||
|
||||
.logo-title {
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
.score-text {
|
||||
font-size: 18px;
|
||||
}
|
||||
}
|
||||
|
||||
/* Animation for loading */
|
||||
@keyframes spin {
|
||||
0% { transform: rotate(0deg); }
|
||||
100% { transform: rotate(360deg); }
|
||||
.how-title {
|
||||
@apply font-jersey text-2xl text-primary text-center mb-4;
|
||||
}
|
||||
}
|
||||
+4
-2
@@ -1,5 +1,5 @@
|
||||
import { Inter } from 'next/font/google'
|
||||
import Script from 'next/script'
|
||||
import ReactQueryProvider from '@/lib/react-query'
|
||||
import './globals.css'
|
||||
|
||||
const inter = Inter({ subsets: ['latin'] })
|
||||
@@ -27,7 +27,9 @@ export default function RootLayout({ children }) {
|
||||
<head>
|
||||
</head>
|
||||
<body className={inter.className}>
|
||||
{children}
|
||||
<ReactQueryProvider>
|
||||
{children}
|
||||
</ReactQueryProvider>
|
||||
</body>
|
||||
</html>
|
||||
)
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
export { useMapillaryImage } from './useMapillary';
|
||||
export { useScoring } from './useScoring';
|
||||
@@ -0,0 +1,18 @@
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
|
||||
export function useMapillaryImage(city) {
|
||||
return useQuery({
|
||||
queryKey: ['mapillary', city],
|
||||
queryFn: async () => {
|
||||
const response = await fetch(`/api/mapillary?location=${city}`);
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to fetch image');
|
||||
}
|
||||
return response.json();
|
||||
},
|
||||
enabled: !!city,
|
||||
staleTime: 5 * 60 * 1000, // 5 minutes
|
||||
retry: 3,
|
||||
retryDelay: attemptIndex => Math.min(1000 * 2 ** attemptIndex, 30000),
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
import { useMutation } from '@tanstack/react-query';
|
||||
|
||||
export function useScoring() {
|
||||
return useMutation({
|
||||
mutationFn: async (scoreData) => {
|
||||
const response = await fetch('/api/scoring', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(scoreData),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to submit score');
|
||||
}
|
||||
|
||||
return response.json();
|
||||
},
|
||||
retry: 2,
|
||||
retryDelay: attemptIndex => Math.min(1000 * 2 ** attemptIndex, 10000),
|
||||
});
|
||||
}
|
||||
Vendored
+22
@@ -0,0 +1,22 @@
|
||||
'use client';
|
||||
|
||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
||||
import { useState } from 'react';
|
||||
|
||||
export default function ReactQueryProvider({ children }) {
|
||||
const [queryClient] = useState(() => new QueryClient({
|
||||
defaultOptions: {
|
||||
queries: {
|
||||
staleTime: 5 * 60 * 1000, // 5 minutes
|
||||
retry: 2,
|
||||
retryDelay: attemptIndex => Math.min(1000 * 2 ** attemptIndex, 30000),
|
||||
},
|
||||
},
|
||||
}));
|
||||
|
||||
return (
|
||||
<QueryClientProvider client={queryClient}>
|
||||
{children}
|
||||
</QueryClientProvider>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
import { clsx } from "clsx";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
|
||||
export function cn(...inputs) {
|
||||
return twMerge(clsx(inputs));
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
/** @type {import('tailwindcss').Config} */
|
||||
module.exports = {
|
||||
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: {
|
||||
primary: {
|
||||
DEFAULT: '#05A38C',
|
||||
hover: '#048a76',
|
||||
},
|
||||
background: {
|
||||
DEFAULT: '#1a1a1a',
|
||||
secondary: '#24453D',
|
||||
},
|
||||
text: {
|
||||
DEFAULT: '#e6f4e6',
|
||||
secondary: '#ccc',
|
||||
}
|
||||
},
|
||||
fontFamily: {
|
||||
'jersey': ['Jersey 15', 'cursive'],
|
||||
'courier': ['Courier', 'monospace'],
|
||||
},
|
||||
},
|
||||
},
|
||||
plugins: [],
|
||||
}
|
||||
Reference in new issue
Block a user