├── .env.example
├── .gitignore
├── CHANGELOG.md
├── LICENSE
├── README.md
├── bun.lock
├── components.json
├── eslint.config.mjs
├── next.config.ts
├── package-lock.json
├── package.json
├── postcss.config.mjs
├── public
├── assets
│ ├── icons
│ │ ├── menu
│ │ │ ├── menu-achievement.webp
│ │ │ ├── menu-multiplayer.webp
│ │ │ ├── menu-quest.webp
│ │ │ ├── menu-singleplayer.webp
│ │ │ └── menu-store.webp
│ │ └── mode
│ │ │ ├── mode-easy.webp
│ │ │ ├── mode-hard.webp
│ │ │ └── mode-medium.webp
│ └── not-found.webp
├── cursor
│ ├── cursor-idle.webp
│ └── cursor-pointer.webp
├── icon
│ ├── icon-128x128.png
│ ├── icon-144x144.png
│ ├── icon-152x152.png
│ ├── icon-192x192.png
│ ├── icon-384x384.png
│ ├── icon-48x48.png
│ ├── icon-512x512.png
│ ├── icon-72x72.png
│ └── icon-96x96.png
├── manifest.json
├── metadata
│ ├── manifest.webp
│ ├── readme-1.webp
│ ├── readme-2.webp
│ ├── readme-3.webp
│ ├── readme-4.webp
│ ├── readme-5.webp
│ ├── readme-6.webp
│ ├── readme-7.webp
│ ├── readme-8.webp
│ └── readme-9.webp
├── sw.js
└── workbox-1bb06f5e.js
├── src-tauri
├── .gitignore
├── Cargo.lock
├── Cargo.toml
├── build.rs
├── capabilities
│ └── default.json
├── icons
│ ├── 128x128.png
│ ├── 128x128@2x.png
│ ├── 32x32.png
│ ├── Square107x107Logo.png
│ ├── Square142x142Logo.png
│ ├── Square150x150Logo.png
│ ├── Square284x284Logo.png
│ ├── Square30x30Logo.png
│ ├── Square310x310Logo.png
│ ├── Square44x44Logo.png
│ ├── Square71x71Logo.png
│ ├── Square89x89Logo.png
│ ├── StoreLogo.png
│ ├── icon.icns
│ ├── icon.ico
│ └── icon.png
├── src
│ ├── lib.rs
│ └── main.rs
└── tauri.conf.json
├── src
├── app
│ ├── api
│ │ └── discord-webhook
│ │ │ └── route.ts
│ ├── favicon.ico
│ ├── globals.css
│ ├── layout.tsx
│ ├── not-found.tsx
│ └── page.tsx
├── components
│ ├── CustomDock.tsx
│ ├── MainMenu.tsx
│ ├── Minesweeper.tsx
│ ├── Timer.tsx
│ ├── TooltipWrapper.tsx
│ ├── Web3Connect.tsx
│ ├── magicui
│ │ ├── confetti.tsx
│ │ ├── dock.tsx
│ │ ├── particles.tsx
│ │ ├── shimmer-button.tsx
│ │ └── sparkles-text.tsx
│ ├── mainmenu
│ │ ├── AchievementCard.tsx
│ │ └── QuestCard.tsx
│ ├── modal
│ │ ├── AchievementModal.tsx
│ │ ├── GameResultModal.tsx
│ │ ├── MultiplayerModal.tsx
│ │ ├── QuestModal.tsx
│ │ ├── SettingsModal.tsx
│ │ └── SingleplayerModal.tsx
│ └── ui
│ │ ├── alert.tsx
│ │ ├── badge.tsx
│ │ ├── button.tsx
│ │ ├── card.tsx
│ │ ├── dialog.tsx
│ │ ├── dropdown-menu.tsx
│ │ ├── input.tsx
│ │ ├── label.tsx
│ │ ├── select.tsx
│ │ ├── separator.tsx
│ │ ├── tabs.tsx
│ │ └── tooltip.tsx
├── configs
│ ├── game.tsx
│ ├── index.ts
│ ├── metadata.ts
│ ├── mock.ts
│ └── settings.ts
├── hooks
│ └── useTranslation.tsx
├── lib
│ └── utils.ts
├── locales
│ ├── en.json
│ ├── jp.json
│ ├── th.json
│ ├── vi.json
│ └── zh.json
├── providers
│ ├── FontProvider.tsx
│ ├── LanguageProvider.tsx
│ ├── SolanaProvider.tsx
│ ├── ThemeProvider.tsx
│ └── Web3Provider.tsx
├── stores
│ ├── commonStore.ts
│ ├── gameStore.ts
│ ├── index.ts
│ ├── languageStore.ts
│ ├── settingStore.ts
│ └── walletStore.ts
├── types
│ ├── css.d.ts
│ ├── index.d.ts
│ └── store.d.ts
└── utils
│ └── discord-webhook.ts
├── tailwind.config.ts
└── tsconfig.json
/.env.example:
--------------------------------------------------------------------------------
1 | # Basic Settings
2 | NODE_ENV= # production | development
3 | ANALYZE= # true | false
4 |
5 | # Discord
6 | DISCORD_SHARE_WEBHOOK=
7 |
8 | # Web3 Connect
9 | NETWORK_RPC=devnet # mainnet | testnet | devnet
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | /node_modules
5 | /.pnp
6 | .pnp.*
7 | .yarn/*
8 | !.yarn/patches
9 | !.yarn/plugins
10 | !.yarn/releases
11 | !.yarn/versions
12 |
13 | # testing
14 | /coverage
15 |
16 | # next.js
17 | /.next/
18 | /out/
19 |
20 | # production
21 | /build
22 |
23 | # misc
24 | .DS_Store
25 | *.pem
26 |
27 | # debug
28 | npm-debug.log*
29 | yarn-debug.log*
30 | yarn-error.log*
31 | .pnpm-debug.log*
32 |
33 | # env files (can opt-in for committing if needed)
34 | .env*
35 | !.env.example
36 |
37 | # vercel
38 | .vercel
39 |
40 | # typescript
41 | *.tsbuildinfo
42 | next-env.d.ts
43 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # Minesweeper Changelog
2 | All notable changes to this project will be documented in this file.
3 |
4 | ## 📍 [1.2.7] - 2025-03-05
5 | ### Fixed
6 | - error share discord on webhook (env issue)
7 |
8 | ## 📍 [1.2.6] - 2025-03-05
9 | ### Added
10 | - package: axios & @radix-ui/react-tabs
11 | - image: menu & mode icon in mainmenu
12 | - font: each languages
13 | - added link Discord Channel in Dock Menu
14 | - added: Shared on discord server system (only Wallet connect)
15 | - added: Multiplayer Menu (MOCKUP Only)
16 | - added: Achievement Menu (MOCKUP Only)
17 | - added: Quests Menu (MOCKUP Only)
18 | - added: Store Menu (Nothing inside)
19 | - added: Icon for Singleplayer each mode.
20 |
21 | ### Changed
22 | - updated: .env.example (add DISCORD_SHARE_WEBHOOK)
23 | - The font will change according to the selected language.
24 | - updated: UX/UI Mainmenu
25 | - Organize MainMenu & Modal files more neatly.
26 |
27 | ## 📍 [1.2.5] - 2025-03-04
28 | ### Fixed
29 | - change web3 wallet connect autoConnect: false > true
30 |
31 | ## 📍 [1.2.4] - 2025-03-04
32 | ### Added
33 | - package: crypto-browserify & path-browserify
34 |
35 | ### Changed
36 | - next.config.ts (assetPrefix & webpack)
37 |
38 | ### Fixed
39 | - Fixed: Web3 Solana wallet connect issue on Desktop version
40 |
41 | ## 📍 [1.2.3] - 2025-03-04
42 | ### Added
43 | - Web3 connect (solana)
44 | - package: @solana/wallet-adapter-base
45 | - package: @solana/wallet-adapter-react
46 | - package: @solana/wallet-adapter-react-ui
47 | - package: @solana/wallet-adapter-wallets
48 | - package: @solana/web3.js
49 | - package: pino-pretty
50 | - package: @tauri-apps/api
51 | - package: @tauri-apps/plugin-opener
52 |
53 | ### Changed
54 | - not-found.webp image
55 |
56 | ## 📍 [1.2.2] - 2025-03-01
57 |
58 | ### Fixed
59 | - Fixed: Viewport (Lighthouse Accessibility score)
60 | - Fixed: VersionGame color (Lighthouse Accessibility score)
61 |
62 | ## 📍 [1.2.1] - 2025-03-01
63 |
64 | ### Fixed
65 | - Game logic: Calculate score
66 | - Game system: When press restart button, not reset Flag placed.
67 |
68 | ## 📍 [1.2.0] - 2025-03-01
69 | Version 1.2.0 adds a PWA system for offline mobile play and integrates Tauri for cross-platform export, allowing the game to run on all platforms (desktop, macOS, Android, iOS, Linux, and web).
70 |
71 | Additionally, we have refined the game logic for a more polished experience and introduced a new sharing system.
72 |
73 | ### Added
74 | - package: @tauri-apps/cli & next-pwa & @types/next-pwa
75 | - script: tauri
76 | - setup: tauri for cross-platform application
77 | - setup: next-pwa
78 | - added: Viewport (next)
79 | - Mainmenu > Game Info > added link CHANGELOG.md
80 | - setting: added menu change Flag icon color
81 | - added: Flag country icon (Language setting)
82 |
83 | ### Changed
84 | - move package to devDependencies: @next/bundle-analyzer & @tailwindcss/postcss & autoprefixer & next-themes & tailwind-merge & tailwindcss-animate
85 | - NextThemeProvider changed defaultTheme "system" > "light"
86 | - Refactor code: SettingsModal.tsx (reusable more / SelectField)
87 | - next.config.ts for withPWA
88 | - updated: Share game result clipboard > save image .png file
89 | - updated: Game Result share image added board game.
90 |
91 | ### Fixed
92 | - Fixed: Gamelogic when pressing Reset button, the time does not reset to 0.
93 | - Fixed: Gamelogic does not remove the flag when opening a tile that has no surrounding mines but has a flag placed on it.
94 | - MainMenu change import "version" to "pkg" for fixed issue Compiled.
95 |
96 | ## 📍 [1.1.0] - 2025-03-01
97 | Here we would have the update steps for 1.1.0 for people to follow.
98 |
99 | In this version, there are significant changes as we aimed to make it more developable and easier to maintain. We've revamped several systems to achieve consistency, with the main changes being the upgrade of TailwindCSS to version 4.0.9 and the implementation of Zustand for global state management instead of passing props across pages.
100 |
101 | ### Added
102 | - file: .env.example & CHANGELOG.md
103 | - package: @next/bundle-analyzer & @tailwindcss/postcss & autoprefixer & tailwindcss-animate & zustand
104 | - file: /public/assets/not-found.webp
105 | - Global State with zustand
106 | ### Changed
107 | - setup: "next.config.ts" bundle-analyzer & images
108 | - package: upgrade tailwindcss 3.4.1 > 4.0.9
109 | - edit README.md (TailwindCSS version)
110 | - refactor: local storage > global state managment with zustand
111 | ### Fixed
112 | - postcss.config.mjs & tailwind.config.ts & global.css for TailwindCSS 4.0.9
113 | - Fixed game logic after playing can continue playing for 1 turn.
114 | - Fixed Theme issue.
115 |
116 | ## 📍 [1.0.0] - 2025-02-14
117 | Release first version - Minesweeper Game with Next.js 15
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2025 Suvijak (Guy)
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | 
2 | # Minesweeper – Old school game with Next.JS
3 | A simple implementation of the classic Minesweeper game built with Next.js. This project showcases the use of React components and modern front-end development techniques. Players can enjoy the Minesweeper game experience directly in their browser, with a clean and responsive UI.
4 |
5 | 
6 | 
7 | 
8 |
9 | ## 📌 Project Overview
10 | Minesweeper is a web-based implementation of the classic puzzle game, built with Next.js 15. It features modern UI elements, customizable settings, and responsive design. Players can enjoy the classic Minesweeper experience with added features like custom themes, multiple languages, and various visual customization options.
11 |
12 | 
13 |
14 | ## ⚡ Key Points to Know
15 | - 🎮 **Classic Gameplay** - Traditional Minesweeper mechanics with modern features
16 | - 🌙 **Dark/Light Mode** - Full theme support with automatic system detection
17 | - 🌍 **Multi-Language** - Supports English, Thai, Japanese, Vietnamese, and Chinese (can add more)
18 | - 📱 **Responsive Design** - Playable on all devices from mobile to desktop
19 | - 🎨 **Customizable UI** - Various options for icons, numbers, and game elements (can add more)
20 | - 💾 **No Backend** - Client-side only with state management
21 |
22 | ## 🔥 Features
23 | - 🎯 **Multiple Difficulty Levels** - Easy (9x9), Medium (16x16) and Hard (16x30)
24 | - 🎨 **Customizable Elements** (can add more)
25 | - Different flag icons (Default, Pyramid, Radar, Sparkles, Sigma)
26 | - Various mine icons (Bomb, Skull, Fire, Flame, Ghost)
27 | - Multiple number styles (Default, Roman, Thai, ABC, Question)
28 | - 🌍 **Language Support** - Available in multiple languages (can add more)
29 | - 🎨 **Theme Options** - Dark and light mode with system preference detection
30 | - 📊 **Score System** - Time-based scoring with difficulty multipliers
31 | - 📱 **Mobile Support** - Touch-friendly interface with flag mode toggle
32 |
33 | ## 🛠️ Tech Stack
34 | -
Next.js 15 – Framework for static site generation.
35 | -
TailwindCSS 4 – Utility-first CSS framework for styling.
36 | -
TypeScript – Strongly typed JavaScript for better maintainability.
37 | -
shadcn/ui – Reusable UI components
38 | -
Magic UI – Reusable UI components
39 | -
Lucide Icons – Modern icon set
40 | -
next-themes – Theme management
41 |
42 | 
43 |
44 | ## 🚀 Live Demo
45 | Try it here: [Minesweeper](https://nextjs-minesweeper-game.vercel.app)
46 |
47 | 
48 | 
49 | 
50 |
51 | ## 📂 Installation & Setup
52 |
53 | To run this project locally, follow these steps:
54 |
55 | ### **1. Clone the repository**
56 | ```bash
57 | git clone https://github.com/guysuvijak/nextjs-minesweeper-game.git
58 | ```
59 | ```bash
60 | cd nextjs-minesweeper-game
61 | ```
62 | ### **2. Install dependencies**
63 | ```bash
64 | npm install
65 | or
66 | bun install
67 | or
68 | pnpm install
69 | ```
70 | ### **3. Start the development server**
71 | ```bash
72 | npm run dev
73 | or
74 | bun run dev
75 | or
76 | pnpm run dev
77 | ```
78 | ### **4. The app will be available**
79 | [http://localhost:3000](http://localhost:3000)
80 | ### **5. Export to cross-platform**
81 | ```bash
82 | npm run tauri
83 | or
84 | bun run tauri
85 | or
86 | pnpm run tauri
87 | ```
88 | note: If you want to Export Cross-platform, please follow setup before [Here](https://v2.tauri.app/start/prerequisites/)
89 |
90 | ## 🗺️ Project Structure
91 | ```bash
92 | nextjs-minesweeper-game/
93 | ├── public/ # Static assets
94 | │ ├── cursor/ # Static Cursor Image
95 | │ ├── icon/ # Website Icon
96 | │ ├── metadata/ # Static Metadata & README Image
97 | │ └── manifest.json # Config Metadata (PWA)
98 | └── src/
99 | ├── app/ # layout & page Next.JS
100 | │ └── page.tsx # Main Page
101 | ├── components/ # React components
102 | │ ├── magicui/ # UI from Magic-UI
103 | │ ├── ui/ # UI from shadcn/ui
104 | │ ├── CustomDock.tsx # Menu Dock in Mainmenu
105 | │ ├── GameResultModal.tsx # Modal Result in Game Screen
106 | │ ├── MainMenu.tsx # Mainmenu Screen
107 | │ ├── Minesweeper.tsx # Game Screen
108 | │ ├── SettingsModal.tsx # Modal Settings in Mainmenu
109 | │ └── Timer.tsx # Timer in Game Screen
110 | ├── configs/ # Configs File (Edit Here)
111 | ├── hooks/ # React Custom Hooks
112 | ├── lib/ # Auto Create from Library
113 | ├── locales/ # Language Files
114 | └── types/ # TypeScript File
115 | ```
116 |
117 |
118 | ## 📄 Modifying the Website
119 | Since this is a static website, all modifications must be made directly within the project files. Here’s where you can edit specific parts of the site:
120 | ```bash
121 | ⚙️ Game Configuration
122 | 📍 File: src/configs/game.tsx
123 | Add or modify game settings like difficulty levels, scoring, and colors.
124 | ```
125 | ```bash
126 | 🎨 UI Settings
127 | 📍 File: src/configs/settings.ts
128 | Configure available options for icons, themes, and number styles.
129 | ```
130 | ```bash
131 | 🌍 Language Files
132 | 📍 Folder: src/locales/
133 | 📍 File: src/configs/language.ts
134 | Add or modify translations for different languages.
135 | ```
136 | ```bash
137 | 🖼️ Custom Cursors
138 | 📍 Folder: public/cursor/
139 | Add or modify custom cursor images.
140 | ```
141 | ```bash
142 | ⚙️ Types Definition
143 | 📍 File: src/types/index.ts
144 | Modify TypeScript types and interfaces.
145 | ```
146 | ```bash
147 | ⚙️ Website Configuration
148 | 📍 Files:
149 | src/configs/metadata.ts – Modify general layout and metadata settings.
150 | public/manifest.json – Update website metadata for PWA settings.
151 | ```
152 |
153 | ## 📜 License
154 | This project is open-source under the MIT License.
155 | Let me know if you need any modifications! 🚀
156 |
157 | ## 🙏 Acknowledgments
158 | Thank you for your interest in Minesweeper from me! Your support means a lot. :heart:
159 | ⭐ If you like this project, please consider giving it a star on GitHub to show your support and encouragement! 🚀
--------------------------------------------------------------------------------
/components.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://ui.shadcn.com/schema.json",
3 | "style": "new-york",
4 | "rsc": true,
5 | "tsx": true,
6 | "tailwind": {
7 | "config": "tailwind.config.ts",
8 | "css": "src/app/globals.css",
9 | "baseColor": "neutral",
10 | "cssVariables": true,
11 | "prefix": ""
12 | },
13 | "aliases": {
14 | "components": "@/components",
15 | "utils": "@/lib/utils",
16 | "ui": "@/components/ui",
17 | "lib": "@/lib",
18 | "hooks": "@/hooks"
19 | },
20 | "iconLibrary": "lucide"
21 | }
--------------------------------------------------------------------------------
/eslint.config.mjs:
--------------------------------------------------------------------------------
1 | import { dirname } from "path";
2 | import { fileURLToPath } from "url";
3 | import { FlatCompat } from "@eslint/eslintrc";
4 |
5 | const __filename = fileURLToPath(import.meta.url);
6 | const __dirname = dirname(__filename);
7 |
8 | const compat = new FlatCompat({
9 | baseDirectory: __dirname,
10 |
11 | });
12 |
13 | const eslintConfig = [
14 | ...compat.extends("next/core-web-vitals", "next/typescript"),
15 |
16 | ];
17 |
18 | export default eslintConfig;
19 |
--------------------------------------------------------------------------------
/next.config.ts:
--------------------------------------------------------------------------------
1 | import type { NextConfig } from 'next';
2 | import NextBundleAnalyzer from '@next/bundle-analyzer';
3 | import createNextPWA from 'next-pwa';
4 |
5 | const isProd = process.env.NODE_ENV === 'production';
6 |
7 | const withBundleAnalyzer = NextBundleAnalyzer({
8 | enabled: process.env.ANALYZE === 'true'
9 | });
10 |
11 | const withPWA = createNextPWA({
12 | dest: 'public',
13 | disable: process.env.NODE_ENV === 'development',
14 | register: true,
15 | skipWaiting: true
16 | });
17 |
18 | const nextConfig = {
19 | output: 'export',
20 | assetPrefix: isProd ? undefined : 'http://localhost:3000',
21 | reactStrictMode: true,
22 | images: {
23 | unoptimized: true,
24 | formats: ['image/webp'],
25 | deviceSizes: [32, 64, 96],
26 | remotePatterns: [
27 | { protocol: 'https', hostname: 'www.google.com' },
28 | { protocol: 'https', hostname: 'nextjs-minesweeper-game.vercel.app' }
29 | ]
30 | },
31 | webpack: (config) => {
32 | config.resolve.fallback = {
33 | fs: false,
34 | net: false,
35 | tls: false,
36 | crypto: require.resolve('crypto-browserify'),
37 | path: require.resolve('path-browserify')
38 | };
39 |
40 | return config;
41 | }
42 | } satisfies NextConfig;
43 |
44 | export default isProd ? withPWA(nextConfig) : withBundleAnalyzer(nextConfig);
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "nextjs-minesweeper-game",
3 | "version": "1.2.7",
4 | "description": "A simple implementation of the classic Minesweeper game built with Next.js. This project showcases the use of React components and modern front-end development techniques. Players can enjoy the Minesweeper game experience directly in their browser, with a clean and responsive UI.",
5 | "repository": "git@github.com:guysuvijak/nextjs-minesweeper-game.git",
6 | "license": "MIT",
7 | "keywords": [
8 | "open-source",
9 | "nextjs-game",
10 | "minesweeper",
11 | "meteorviix",
12 | "guysuvijak",
13 | "typescript",
14 | "frontend",
15 | "nextjs",
16 | "static-site",
17 | "client-side",
18 | "no-database"
19 | ],
20 | "author": "MeteorVIIx | https://github.com/guysuvijak",
21 | "private": true,
22 | "scripts": {
23 | "dev": "next dev --turbopack",
24 | "build": "next build",
25 | "start": "next start",
26 | "lint": "next lint",
27 | "tauri": "NEXT_PUBLIC_BUILD_TARGET=desktop && tauri build",
28 | "info": "tauri info"
29 | },
30 | "dependencies": {
31 | "@radix-ui/react-dialog": "^1.1.6",
32 | "@radix-ui/react-dropdown-menu": "^2.1.6",
33 | "@radix-ui/react-label": "^2.1.2",
34 | "@radix-ui/react-select": "^2.1.6",
35 | "@radix-ui/react-separator": "^1.1.2",
36 | "@radix-ui/react-slot": "^1.1.2",
37 | "@radix-ui/react-tabs": "^1.1.3",
38 | "@radix-ui/react-tooltip": "^1.1.8",
39 | "@solana/wallet-adapter-base": "^0.9.23",
40 | "@solana/wallet-adapter-react": "^0.15.35",
41 | "@solana/wallet-adapter-react-ui": "^0.9.35",
42 | "@solana/wallet-adapter-wallets": "^0.19.32",
43 | "@solana/web3.js": "^1.98.0",
44 | "axios": "^1.8.1",
45 | "canvas-confetti": "^1.9.3",
46 | "class-variance-authority": "^0.7.1",
47 | "clsx": "^2.1.1",
48 | "html2canvas": "^1.4.1",
49 | "lucide-react": "^0.475.0",
50 | "motion": "^12.4.2",
51 | "next": "15.2.3",
52 | "pino-pretty": "^13.0.0",
53 | "react": "^19.0.0",
54 | "react-dom": "^19.0.0",
55 | "zustand": "^5.0.3"
56 | },
57 | "devDependencies": {
58 | "@eslint/eslintrc": "^3",
59 | "@next/bundle-analyzer": "^15.2.0",
60 | "@tailwindcss/postcss": "^4.0.9",
61 | "@tauri-apps/api": "^2.3.0",
62 | "@tauri-apps/cli": "^2.3.1",
63 | "@tauri-apps/plugin-opener": "^2.2.6",
64 | "@types/canvas-confetti": "^1.9.0",
65 | "@types/next-pwa": "^5.6.9",
66 | "@types/node": "^20",
67 | "@types/react": "^19",
68 | "@types/react-dom": "^19",
69 | "autoprefixer": "^10.4.20",
70 | "crypto-browserify": "^3.12.1",
71 | "eslint": "^9",
72 | "eslint-config-next": "15.1.7",
73 | "next-pwa": "^5.6.0",
74 | "next-themes": "^0.4.4",
75 | "path-browserify": "^1.0.1",
76 | "postcss": "^8",
77 | "tailwind-merge": "^3.0.2",
78 | "tailwindcss": "^4.0.9",
79 | "tailwindcss-animate": "^1.0.7",
80 | "typescript": "^5"
81 | },
82 | "optionalDependencies": {
83 | "@tailwindcss/oxide-linux-x64-gnu": "^4.0.1",
84 | "lightningcss-linux-x64-gnu": "^1.29.1"
85 | }
86 | }
87 |
--------------------------------------------------------------------------------
/postcss.config.mjs:
--------------------------------------------------------------------------------
1 | /** @type {import('postcss-load-config').Config} */
2 | const config = {
3 | plugins: {
4 | "@tailwindcss/postcss": {},
5 | "autoprefixer": {}
6 | },
7 | };
8 |
9 | export default config;
10 |
--------------------------------------------------------------------------------
/public/assets/icons/menu/menu-achievement.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/guysuvijak/nextjs-minesweeper-game/85e79839b43c5cd0f218c70e853d3ac6a863a316/public/assets/icons/menu/menu-achievement.webp
--------------------------------------------------------------------------------
/public/assets/icons/menu/menu-multiplayer.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/guysuvijak/nextjs-minesweeper-game/85e79839b43c5cd0f218c70e853d3ac6a863a316/public/assets/icons/menu/menu-multiplayer.webp
--------------------------------------------------------------------------------
/public/assets/icons/menu/menu-quest.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/guysuvijak/nextjs-minesweeper-game/85e79839b43c5cd0f218c70e853d3ac6a863a316/public/assets/icons/menu/menu-quest.webp
--------------------------------------------------------------------------------
/public/assets/icons/menu/menu-singleplayer.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/guysuvijak/nextjs-minesweeper-game/85e79839b43c5cd0f218c70e853d3ac6a863a316/public/assets/icons/menu/menu-singleplayer.webp
--------------------------------------------------------------------------------
/public/assets/icons/menu/menu-store.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/guysuvijak/nextjs-minesweeper-game/85e79839b43c5cd0f218c70e853d3ac6a863a316/public/assets/icons/menu/menu-store.webp
--------------------------------------------------------------------------------
/public/assets/icons/mode/mode-easy.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/guysuvijak/nextjs-minesweeper-game/85e79839b43c5cd0f218c70e853d3ac6a863a316/public/assets/icons/mode/mode-easy.webp
--------------------------------------------------------------------------------
/public/assets/icons/mode/mode-hard.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/guysuvijak/nextjs-minesweeper-game/85e79839b43c5cd0f218c70e853d3ac6a863a316/public/assets/icons/mode/mode-hard.webp
--------------------------------------------------------------------------------
/public/assets/icons/mode/mode-medium.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/guysuvijak/nextjs-minesweeper-game/85e79839b43c5cd0f218c70e853d3ac6a863a316/public/assets/icons/mode/mode-medium.webp
--------------------------------------------------------------------------------
/public/assets/not-found.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/guysuvijak/nextjs-minesweeper-game/85e79839b43c5cd0f218c70e853d3ac6a863a316/public/assets/not-found.webp
--------------------------------------------------------------------------------
/public/cursor/cursor-idle.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/guysuvijak/nextjs-minesweeper-game/85e79839b43c5cd0f218c70e853d3ac6a863a316/public/cursor/cursor-idle.webp
--------------------------------------------------------------------------------
/public/cursor/cursor-pointer.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/guysuvijak/nextjs-minesweeper-game/85e79839b43c5cd0f218c70e853d3ac6a863a316/public/cursor/cursor-pointer.webp
--------------------------------------------------------------------------------
/public/icon/icon-128x128.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/guysuvijak/nextjs-minesweeper-game/85e79839b43c5cd0f218c70e853d3ac6a863a316/public/icon/icon-128x128.png
--------------------------------------------------------------------------------
/public/icon/icon-144x144.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/guysuvijak/nextjs-minesweeper-game/85e79839b43c5cd0f218c70e853d3ac6a863a316/public/icon/icon-144x144.png
--------------------------------------------------------------------------------
/public/icon/icon-152x152.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/guysuvijak/nextjs-minesweeper-game/85e79839b43c5cd0f218c70e853d3ac6a863a316/public/icon/icon-152x152.png
--------------------------------------------------------------------------------
/public/icon/icon-192x192.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/guysuvijak/nextjs-minesweeper-game/85e79839b43c5cd0f218c70e853d3ac6a863a316/public/icon/icon-192x192.png
--------------------------------------------------------------------------------
/public/icon/icon-384x384.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/guysuvijak/nextjs-minesweeper-game/85e79839b43c5cd0f218c70e853d3ac6a863a316/public/icon/icon-384x384.png
--------------------------------------------------------------------------------
/public/icon/icon-48x48.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/guysuvijak/nextjs-minesweeper-game/85e79839b43c5cd0f218c70e853d3ac6a863a316/public/icon/icon-48x48.png
--------------------------------------------------------------------------------
/public/icon/icon-512x512.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/guysuvijak/nextjs-minesweeper-game/85e79839b43c5cd0f218c70e853d3ac6a863a316/public/icon/icon-512x512.png
--------------------------------------------------------------------------------
/public/icon/icon-72x72.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/guysuvijak/nextjs-minesweeper-game/85e79839b43c5cd0f218c70e853d3ac6a863a316/public/icon/icon-72x72.png
--------------------------------------------------------------------------------
/public/icon/icon-96x96.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/guysuvijak/nextjs-minesweeper-game/85e79839b43c5cd0f218c70e853d3ac6a863a316/public/icon/icon-96x96.png
--------------------------------------------------------------------------------
/public/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "Minesweeper",
3 | "short_name": "Minesweeper",
4 | "theme_color": "#000000",
5 | "background_color": "#000000",
6 | "display": "standalone",
7 | "orientation": "portrait",
8 | "scope": "/",
9 | "start_url": "/",
10 | "icons": [
11 | {
12 | "src": "icon/icon-48x48.png",
13 | "sizes": "48x48",
14 | "type": "image/png",
15 | "purpose": "maskable any"
16 | },
17 | {
18 | "src": "icon/icon-72x72.png",
19 | "sizes": "72x72",
20 | "type": "image/png",
21 | "purpose": "maskable any"
22 | },
23 | {
24 | "src": "icon/icon-96x96.png",
25 | "sizes": "96x96",
26 | "type": "image/png",
27 | "purpose": "maskable any"
28 | },
29 | {
30 | "src": "icon/icon-128x128.png",
31 | "sizes": "128x128",
32 | "type": "image/png",
33 | "purpose": "maskable any"
34 | },
35 | {
36 | "src": "icon/icon-144x144.png",
37 | "sizes": "144x144",
38 | "type": "image/png",
39 | "purpose": "maskable any"
40 | },
41 | {
42 | "src": "icon/icon-152x152.png",
43 | "sizes": "152x152",
44 | "type": "image/png",
45 | "purpose": "maskable any"
46 | },
47 | {
48 | "src": "icon/icon-192x192.png",
49 | "sizes": "192x192",
50 | "type": "image/png",
51 | "purpose": "maskable any"
52 | },
53 | {
54 | "src": "icon/icon-384x384.png",
55 | "sizes": "384x384",
56 | "type": "image/png",
57 | "purpose": "maskable any"
58 | },
59 | {
60 | "src": "icon/icon-512x512.png",
61 | "sizes": "512x512",
62 | "type": "image/png",
63 | "purpose": "maskable any"
64 | }
65 | ],
66 | "splash_pages": null
67 | }
--------------------------------------------------------------------------------
/public/metadata/manifest.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/guysuvijak/nextjs-minesweeper-game/85e79839b43c5cd0f218c70e853d3ac6a863a316/public/metadata/manifest.webp
--------------------------------------------------------------------------------
/public/metadata/readme-1.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/guysuvijak/nextjs-minesweeper-game/85e79839b43c5cd0f218c70e853d3ac6a863a316/public/metadata/readme-1.webp
--------------------------------------------------------------------------------
/public/metadata/readme-2.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/guysuvijak/nextjs-minesweeper-game/85e79839b43c5cd0f218c70e853d3ac6a863a316/public/metadata/readme-2.webp
--------------------------------------------------------------------------------
/public/metadata/readme-3.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/guysuvijak/nextjs-minesweeper-game/85e79839b43c5cd0f218c70e853d3ac6a863a316/public/metadata/readme-3.webp
--------------------------------------------------------------------------------
/public/metadata/readme-4.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/guysuvijak/nextjs-minesweeper-game/85e79839b43c5cd0f218c70e853d3ac6a863a316/public/metadata/readme-4.webp
--------------------------------------------------------------------------------
/public/metadata/readme-5.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/guysuvijak/nextjs-minesweeper-game/85e79839b43c5cd0f218c70e853d3ac6a863a316/public/metadata/readme-5.webp
--------------------------------------------------------------------------------
/public/metadata/readme-6.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/guysuvijak/nextjs-minesweeper-game/85e79839b43c5cd0f218c70e853d3ac6a863a316/public/metadata/readme-6.webp
--------------------------------------------------------------------------------
/public/metadata/readme-7.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/guysuvijak/nextjs-minesweeper-game/85e79839b43c5cd0f218c70e853d3ac6a863a316/public/metadata/readme-7.webp
--------------------------------------------------------------------------------
/public/metadata/readme-8.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/guysuvijak/nextjs-minesweeper-game/85e79839b43c5cd0f218c70e853d3ac6a863a316/public/metadata/readme-8.webp
--------------------------------------------------------------------------------
/public/metadata/readme-9.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/guysuvijak/nextjs-minesweeper-game/85e79839b43c5cd0f218c70e853d3ac6a863a316/public/metadata/readme-9.webp
--------------------------------------------------------------------------------
/src-tauri/.gitignore:
--------------------------------------------------------------------------------
1 | # Generated by Cargo
2 | # will have compiled files and executables
3 | /target/
4 |
5 | # Generated by Tauri
6 | # will have schema files for capabilities auto-completion
7 | /gen/schemas
8 |
--------------------------------------------------------------------------------
/src-tauri/Cargo.toml:
--------------------------------------------------------------------------------
1 | [package]
2 | name = "nextjs-minesweeper-game"
3 | version = "1.2.7"
4 | description = "A simple implementation of the classic Minesweeper game built with Next.js. This project showcases the use of React components and modern front-end development techniques. Players can enjoy the Minesweeper game experience directly in their browser, with a clean and responsive UI."
5 | license = "MIT"
6 | authors = ["MeteorVIIx"]
7 | edition = "2021"
8 | rust-version = "1.77.2"
9 |
10 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
11 |
12 | [lib]
13 | # The `_lib` suffix may seem redundant but it is necessary
14 | # to make the lib name unique and wouldn't conflict with the bin name.
15 | # This seems to be only an issue on Windows, see https://github.com/rust-lang/cargo/issues/8519
16 | name = "nextjs_minesweeper_game_lib"
17 | crate-type = ["staticlib", "cdylib", "rlib"]
18 |
19 | [build-dependencies]
20 | tauri-build = { version = "2", features = [] }
21 |
22 | [dependencies]
23 | tauri = { version = "2", features = [] }
24 | tauri-plugin-opener = "2"
25 | serde = { version = "1", features = ["derive"] }
26 | serde_json = "1"
27 |
28 |
--------------------------------------------------------------------------------
/src-tauri/build.rs:
--------------------------------------------------------------------------------
1 | fn main() {
2 | tauri_build::build()
3 | }
4 |
--------------------------------------------------------------------------------
/src-tauri/capabilities/default.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "../gen/schemas/desktop-schema.json",
3 | "identifier": "default",
4 | "description": "Capability for the main window",
5 | "windows": ["main"],
6 | "permissions": [
7 | "core:default",
8 | "opener:default"
9 | ]
10 | }
11 |
--------------------------------------------------------------------------------
/src-tauri/icons/128x128.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/guysuvijak/nextjs-minesweeper-game/85e79839b43c5cd0f218c70e853d3ac6a863a316/src-tauri/icons/128x128.png
--------------------------------------------------------------------------------
/src-tauri/icons/128x128@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/guysuvijak/nextjs-minesweeper-game/85e79839b43c5cd0f218c70e853d3ac6a863a316/src-tauri/icons/128x128@2x.png
--------------------------------------------------------------------------------
/src-tauri/icons/32x32.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/guysuvijak/nextjs-minesweeper-game/85e79839b43c5cd0f218c70e853d3ac6a863a316/src-tauri/icons/32x32.png
--------------------------------------------------------------------------------
/src-tauri/icons/Square107x107Logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/guysuvijak/nextjs-minesweeper-game/85e79839b43c5cd0f218c70e853d3ac6a863a316/src-tauri/icons/Square107x107Logo.png
--------------------------------------------------------------------------------
/src-tauri/icons/Square142x142Logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/guysuvijak/nextjs-minesweeper-game/85e79839b43c5cd0f218c70e853d3ac6a863a316/src-tauri/icons/Square142x142Logo.png
--------------------------------------------------------------------------------
/src-tauri/icons/Square150x150Logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/guysuvijak/nextjs-minesweeper-game/85e79839b43c5cd0f218c70e853d3ac6a863a316/src-tauri/icons/Square150x150Logo.png
--------------------------------------------------------------------------------
/src-tauri/icons/Square284x284Logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/guysuvijak/nextjs-minesweeper-game/85e79839b43c5cd0f218c70e853d3ac6a863a316/src-tauri/icons/Square284x284Logo.png
--------------------------------------------------------------------------------
/src-tauri/icons/Square30x30Logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/guysuvijak/nextjs-minesweeper-game/85e79839b43c5cd0f218c70e853d3ac6a863a316/src-tauri/icons/Square30x30Logo.png
--------------------------------------------------------------------------------
/src-tauri/icons/Square310x310Logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/guysuvijak/nextjs-minesweeper-game/85e79839b43c5cd0f218c70e853d3ac6a863a316/src-tauri/icons/Square310x310Logo.png
--------------------------------------------------------------------------------
/src-tauri/icons/Square44x44Logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/guysuvijak/nextjs-minesweeper-game/85e79839b43c5cd0f218c70e853d3ac6a863a316/src-tauri/icons/Square44x44Logo.png
--------------------------------------------------------------------------------
/src-tauri/icons/Square71x71Logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/guysuvijak/nextjs-minesweeper-game/85e79839b43c5cd0f218c70e853d3ac6a863a316/src-tauri/icons/Square71x71Logo.png
--------------------------------------------------------------------------------
/src-tauri/icons/Square89x89Logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/guysuvijak/nextjs-minesweeper-game/85e79839b43c5cd0f218c70e853d3ac6a863a316/src-tauri/icons/Square89x89Logo.png
--------------------------------------------------------------------------------
/src-tauri/icons/StoreLogo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/guysuvijak/nextjs-minesweeper-game/85e79839b43c5cd0f218c70e853d3ac6a863a316/src-tauri/icons/StoreLogo.png
--------------------------------------------------------------------------------
/src-tauri/icons/icon.icns:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/guysuvijak/nextjs-minesweeper-game/85e79839b43c5cd0f218c70e853d3ac6a863a316/src-tauri/icons/icon.icns
--------------------------------------------------------------------------------
/src-tauri/icons/icon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/guysuvijak/nextjs-minesweeper-game/85e79839b43c5cd0f218c70e853d3ac6a863a316/src-tauri/icons/icon.ico
--------------------------------------------------------------------------------
/src-tauri/icons/icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/guysuvijak/nextjs-minesweeper-game/85e79839b43c5cd0f218c70e853d3ac6a863a316/src-tauri/icons/icon.png
--------------------------------------------------------------------------------
/src-tauri/src/lib.rs:
--------------------------------------------------------------------------------
1 | // Learn more about Tauri commands at https://tauri.app/develop/calling-rust/
2 | #[tauri::command]
3 | fn greet(name: &str) -> String {
4 | format!("Hello, {}! You've been greeted from Rust!", name)
5 | }
6 |
7 | #[cfg_attr(mobile, tauri::mobile_entry_point)]
8 | pub fn run() {
9 | tauri::Builder::default()
10 | .plugin(tauri_plugin_opener::init())
11 | .invoke_handler(tauri::generate_handler![greet])
12 | .run(tauri::generate_context!())
13 | .expect("error while running tauri application");
14 | }
15 |
--------------------------------------------------------------------------------
/src-tauri/src/main.rs:
--------------------------------------------------------------------------------
1 | // Prevents additional console window on Windows in release, DO NOT REMOVE!!
2 | #![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
3 |
4 | fn main() {
5 | nextjs_minesweeper_game_lib::run()
6 | }
7 |
--------------------------------------------------------------------------------
/src-tauri/tauri.conf.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "../node_modules/@tauri-apps/cli/config.schema.json",
3 | "productName": "Minesweeper",
4 | "version": "1.2.7",
5 | "identifier": "com.nextjs-minesweeper-game.app",
6 | "build": {
7 | "frontendDist": "../out",
8 | "devUrl": "http://localhost:3000",
9 | "beforeDevCommand": "bun run dev",
10 | "beforeBuildCommand": "bun run build"
11 | },
12 | "app": {
13 | "windows": [
14 | {
15 | "title": "Minesweeper",
16 | "width": 800,
17 | "height": 760,
18 | "minWidth": 800,
19 | "minHeight": 760,
20 | "resizable": true,
21 | "fullscreen": false,
22 | "dragDropEnabled": false
23 | }
24 | ],
25 | "security": {
26 | "csp": "default-src 'self'; connect-src 'self' https://*.solana.com wss://*.solana.com https://*.phantom.com https://*.solflare.com; script-src 'self' 'unsafe-inline' 'unsafe-eval'; style-src 'self' 'unsafe-inline'; child-src 'self' blob: https://*.solana.com https://*.phantom.com https://*.solflare.com; frame-src 'self' https://*.solana.com https://*.phantom.com https://*.solflare.com; object-src 'none'; base-uri 'self'; form-action 'self'; font-src 'self' data:; img-src 'self' data: blob: https:; manifest-src 'self'; worker-src 'self' blob:;"
27 | }
28 | },
29 | "bundle": {
30 | "active": true,
31 | "targets": "all",
32 | "icon": [
33 | "icons/32x32.png",
34 | "icons/128x128.png",
35 | "icons/128x128@2x.png",
36 | "icons/icon.icns",
37 | "icons/icon.ico"
38 | ]
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/src/app/api/discord-webhook/route.ts:
--------------------------------------------------------------------------------
1 | import { NextRequest, NextResponse } from 'next/server';
2 | import axios from 'axios';
3 |
4 | const shareWebhook = process.env.DISCORD_SHARE_WEBHOOK as string;
5 |
6 | export async function POST(request: NextRequest) {
7 | try {
8 | const formData = await request.formData();
9 | const publicKey = formData.get('publicKey') as string;
10 | const file = formData.get('file') as Blob;
11 |
12 | if (!file) {
13 | return NextResponse.json({ error: 'No file uploaded' }, { status: 400 });
14 | }
15 |
16 | const discordFormData = new FormData();
17 | const payload = {
18 | content: `Minesweeper score shared by: ${publicKey || 'Anonymous Player'}`
19 | };
20 |
21 | discordFormData.append('payload_json', JSON.stringify(payload));
22 | discordFormData.append('file', file, 'minesweeper-score.png');
23 |
24 | const response = await axios.post(shareWebhook, discordFormData, {
25 | headers: {
26 | 'Content-Type': 'multipart/form-data'
27 | }
28 | });
29 |
30 | return NextResponse.json({ success: true, status: response.status });
31 | } catch (error) {
32 | console.error('Error sending Discord webhook:', error);
33 | return NextResponse.json({ error: 'Failed to send webhook' }, { status: 500 });
34 | }
35 | };
--------------------------------------------------------------------------------
/src/app/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/guysuvijak/nextjs-minesweeper-game/85e79839b43c5cd0f218c70e853d3ac6a863a316/src/app/favicon.ico
--------------------------------------------------------------------------------
/src/app/globals.css:
--------------------------------------------------------------------------------
1 | @import "tailwindcss";
2 | @config "../../tailwind.config.ts";
3 |
4 | body {
5 | cursor: url('/cursor/cursor-idle.webp'), auto;
6 | }
7 |
8 | /* Pointer cursor */
9 | a, button, [role="button"], input[type="submit"], input[type="button"], input[type="reset"] {
10 | cursor: url('/cursor/cursor-pointer.webp'), pointer;
11 | }
12 |
13 | .cursor-default {
14 | cursor: url('/cursor/cursor-idle.webp'), pointer;
15 | }
16 |
17 | .cursor-pointer {
18 | cursor: url('/cursor/cursor-pointer.webp'), pointer;
19 | }
20 |
21 | @layer base {
22 | :root {
23 | --background: 0 0% 100%;
24 | --foreground: 0 0% 3.9%;
25 | --card: 0 0% 100%;
26 | --card-foreground: 0 0% 3.9%;
27 | --popover: 0 0% 100%;
28 | --popover-foreground: 0 0% 3.9%;
29 | --primary: 0 0% 9%;
30 | --primary-foreground: 0 0% 98%;
31 | --secondary: 0 0% 96.1%;
32 | --secondary-foreground: 0 0% 9%;
33 | --muted: 0 0% 96.1%;
34 | --muted-foreground: 0 0% 45.1%;
35 | --accent: 0 0% 96.1%;
36 | --accent-foreground: 0 0% 9%;
37 | --destructive: 0 84.2% 60.2%;
38 | --destructive-foreground: 0 0% 98%;
39 | --border: 0 0% 89.8%;
40 | --input: 0 0% 89.8%;
41 | --ring: 0 0% 3.9%;
42 | --chart-1: 12 76% 61%;
43 | --chart-2: 173 58% 39%;
44 | --chart-3: 197 37% 24%;
45 | --chart-4: 43 74% 66%;
46 | --chart-5: 27 87% 67%;
47 | --radius: 0.5rem;
48 |
49 | /* Font family variables */
50 | --font-sans: var(--font-geist-sans), system-ui, sans-serif;
51 | --font-mono: var(--font-geist-mono), monospace;
52 | --font-thai: var(--font-thai), var(--font-geist-sans), system-ui, sans-serif;
53 | --font-japanese: var(--font-japanese), var(--font-geist-sans), system-ui, sans-serif;
54 | --font-korean: var(--font-korean), var(--font-geist-sans), system-ui, sans-serif;
55 | --font-chinese: var(--font-chinese), var(--font-geist-sans), system-ui, sans-serif;
56 | --font-vietnamese: var(--font-vietnamese), var(--font-geist-sans), system-ui, sans-serif;
57 | }
58 | .dark {
59 | --background: 0 0% 3.9%;
60 | --foreground: 0 0% 98%;
61 | --card: 0 0% 3.9%;
62 | --card-foreground: 0 0% 98%;
63 | --popover: 0 0% 3.9%;
64 | --popover-foreground: 0 0% 98%;
65 | --primary: 0 0% 98%;
66 | --primary-foreground: 0 0% 9%;
67 | --secondary: 0 0% 14.9%;
68 | --secondary-foreground: 0 0% 98%;
69 | --muted: 0 0% 14.9%;
70 | --muted-foreground: 0 0% 63.9%;
71 | --accent: 0 0% 14.9%;
72 | --accent-foreground: 0 0% 98%;
73 | --destructive: 0 62.8% 30.6%;
74 | --destructive-foreground: 0 0% 98%;
75 | --border: 0 0% 14.9%;
76 | --input: 0 0% 14.9%;
77 | --ring: 0 0% 83.1%;
78 | --chart-1: 220 70% 50%;
79 | --chart-2: 160 60% 45%;
80 | --chart-3: 30 80% 55%;
81 | --chart-4: 280 65% 60%;
82 | --chart-5: 340 75% 55%;
83 | }
84 | }
85 |
86 | @layer base {
87 | * {
88 | @apply border-border;
89 | }
90 | body {
91 | @apply bg-background text-foreground;
92 | }
93 | }
94 |
95 | @layer utilities {
96 | .scrollbar-game {
97 | scrollbar-width: thin;
98 | scrollbar-color: theme('colors.gray.400') theme('colors.gray.100');
99 | }
100 |
101 | .scrollbar-game::-webkit-scrollbar {
102 | width: 6px;
103 | height: 6px;
104 | }
105 |
106 | .scrollbar-game::-webkit-scrollbar-track {
107 | background: theme('colors.gray.100');
108 | border-radius: 3px;
109 | }
110 |
111 | .scrollbar-game::-webkit-scrollbar-thumb {
112 | background-color: theme('colors.gray.400');
113 | border-radius: 3px;
114 | }
115 | }
116 |
117 | * {
118 | -webkit-user-select: none;
119 | -moz-user-select: none;
120 | -ms-user-select: none;
121 | user-select: none;
122 | }
123 |
124 | input, textarea {
125 | -webkit-user-select: text;
126 | -moz-user-select: text;
127 | -ms-user-select: text;
128 | user-select: text;
129 | }
130 |
131 | /* Font classes */
132 | .font-sans {
133 | font-family: var(--font-sans);
134 | }
135 |
136 | .font-mono {
137 | font-family: var(--font-mono);
138 | }
139 |
140 | .font-thai {
141 | font-family: var(--font-thai);
142 | }
143 |
144 | .font-japanese {
145 | font-family: var(--font-japanese);
146 | }
147 |
148 | .font-korean {
149 | font-family: var(--font-korean);
150 | }
151 |
152 | .font-chinese {
153 | font-family: var(--font-chinese);
154 | }
155 |
156 | .font-vietnamese {
157 | font-family: var(--font-vietnamese);
158 | }
--------------------------------------------------------------------------------
/src/app/layout.tsx:
--------------------------------------------------------------------------------
1 | import { VIEWPORT, METADATA } from '@/configs';
2 | import { ThemeProvider } from '@/providers/ThemeProvider';
3 | import { LanguageProvider } from '@/providers/LanguageProvider';
4 | import { Web3Provider } from '@/providers/Web3Provider';
5 | import { FontProvider } from '@/providers/FontProvider';
6 | import './globals.css';
7 |
8 | export const viewport = VIEWPORT;
9 | export const metadata = METADATA;
10 |
11 | const RootLayout = ({children}: {children: React.ReactNode}) => {
12 | return (
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 | {children}
21 |
22 |
23 |
24 |
25 |
26 |
27 | )
28 | };
29 |
30 | export default RootLayout;
--------------------------------------------------------------------------------
/src/app/not-found.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 | import Image from 'next/image';
3 | import Link from 'next/link';
4 | import { useTranslation } from '@/hooks/useTranslation';
5 | import { Button } from '@/components/ui/button';
6 | import { Card, CardContent } from '@/components/ui/card';
7 | import { useState, useEffect } from 'react';
8 |
9 | const NotFoundPage = () => {
10 | const id = 'not-found';
11 | const { t } = useTranslation();
12 | const [ isLoading, setIsLoading ] = useState(true);
13 |
14 | useEffect(() => {
15 | setIsLoading(false);
16 | }, []);
17 |
18 | if (isLoading) {
19 | return (
20 |
21 | {t(`${id}.loading`)}
22 |
23 | )
24 | }
25 |
26 | return (
27 |
28 |
29 |
30 |
37 | {t(`${id}.title`)}
38 | {t(`${id}.description`)}
39 |
42 |
43 |
44 |
45 | );
46 | };
47 |
48 | export default NotFoundPage;
--------------------------------------------------------------------------------
/src/app/page.tsx:
--------------------------------------------------------------------------------
1 | 'use client'
2 | import { MainMenu } from '@/components/MainMenu';
3 | import { Minesweeper } from '@/components/Minesweeper';
4 | import { SingleplayerModal } from '@/components/modal/SingleplayerModal';
5 | import { MultiplayerModal } from '@/components/modal/MultiplayerModal';
6 | import { AchievementModal } from '@/components/modal/AchievementModal';
7 | import { QuestModal } from '@/components/modal/QuestModal';
8 | import { SettingsModal } from '@/components/modal/SettingsModal';
9 | import { GameResultModal } from '@/components/modal/GameResultModal';
10 | import { useGameStore } from '@/stores';
11 |
12 | export default function Home() {
13 | const { isStartGame } = useGameStore();
14 |
15 | return (
16 | <>
17 | {!isStartGame ? (
18 |
19 | ) : (
20 |
21 | )}
22 |
23 |
24 |
25 |
26 |
27 |
28 | >
29 | )
30 | };
--------------------------------------------------------------------------------
/src/components/CustomDock.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 | import Link from 'next/link';
3 | import React from 'react';
4 | import { buttonVariants } from '@/components/ui/button';
5 | import { Separator } from '@/components/ui/separator';
6 | import { TooltipWrapper } from '@/components/TooltipWrapper';
7 | import { cn } from '@/lib/utils';
8 | import { Dock, DockIcon } from '@/components/magicui/dock';
9 |
10 | export type IconProps = React.HTMLAttributes;
11 |
12 | const Icons = {
13 | youtube: (props: IconProps) => (
14 |
25 | ),
26 | github: (props: IconProps) => (
27 |
33 | ),
34 | facebook: (props: IconProps) => (
35 |
44 | ),
45 | discord: (props: IconProps) => (
46 |
55 | )
56 | };
57 |
58 | const DATA = {
59 | navbar: [
60 | { href: 'https://www.facebook.com/guy.suvijak', icon: Icons.facebook, label: 'Facebook' },
61 | { href: 'https://www.youtube.com/@MeteorVIIx', icon: Icons.youtube, label: 'Youtube' },
62 | { href: 'https://discord.gg/6KbSzbc999', icon: Icons.discord, label: 'Discord' },
63 | ],
64 | contact: {
65 | social: {
66 | GitHub: {
67 | name: 'GitHub',
68 | url: 'https://github.com/guysuvijak',
69 | icon: Icons.github,
70 | },
71 | SourceCode: {
72 | name: 'Source-code',
73 | url: 'https://github.com/guysuvijak/nextjs-minesweeper-game',
74 | icon: Icons.github
75 | }
76 | },
77 | },
78 | };
79 |
80 | export function CustomDock() {
81 | return (
82 |
83 |
84 | {DATA.navbar.map((item) => (
85 |
86 |
87 |
97 |
98 |
99 |
100 |
101 | ))}
102 |
103 |
104 |
105 | {Object.entries(DATA.contact.social).map(([name, social]) => (
106 |
107 |
108 |
118 |
119 |
120 |
121 |
122 | ))}
123 |
124 |
125 | )
126 | };
--------------------------------------------------------------------------------
/src/components/MainMenu.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 | import Link from 'next/link';
3 | import Image from 'next/image';
4 | import { useEffect } from 'react';
5 | import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
6 | import { Button } from '@/components/ui/button';
7 | import { useTranslation } from '@/hooks/useTranslation';
8 | import { Settings } from 'lucide-react';
9 | import { useTheme } from 'next-themes';
10 | import { Particles } from '@/components/magicui/particles';
11 | import { SparklesText } from '@/components/magicui/sparkles-text';
12 | import { CustomDock } from '@/components/CustomDock';
13 | import { useCommonStore } from '@/stores';
14 | import { useWallet } from '@solana/wallet-adapter-react';
15 | import { Web3ConnectButton } from '@/components/Web3Connect';
16 | import pkg from '../../package.json';
17 |
18 | interface PrimaryMenuButtonProps {
19 | title: string;
20 | alt: string;
21 | image: string;
22 | onClick: () => void;
23 | };
24 |
25 | interface SecondaryMenuButtonProps {
26 | title: string;
27 | alt: string;
28 | image: string;
29 | onClick: () => void;
30 | };
31 |
32 | const PrimaryMenuButton = ({ title, alt, image, onClick }: PrimaryMenuButtonProps) => (
33 |
42 | );
43 |
44 | const SecondaryMenuButton = ({ title, alt, image, onClick }: SecondaryMenuButtonProps) => (
45 |
54 | );
55 |
56 | export const MainMenu = () => {
57 | const { t } = useTranslation();
58 | const { setIsMenuSingleplayerOpen, setIsMenuMultiplayerOpen, setIsMenuAchievementOpen, setIsMenuQuestOpen, setIsMenuSettingOpen } = useCommonStore();
59 | const wallet = useWallet();
60 | const { theme } = useTheme();
61 | const versionGame = pkg.version;
62 |
63 | useEffect(() => {
64 | if (!wallet.connected) {
65 | }
66 | }, [wallet]);
67 |
68 | return (
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 | {t('common.game-desc')}
77 |
78 |
79 |
80 | {/* Main Menu - Primary Options */}
81 |
82 |
setIsMenuSingleplayerOpen(true)}
84 | title={t('mainmenu.singleplayer.title')} alt={'Singleplayer'} image={'/assets/icons/menu/menu-singleplayer.webp'}
85 | />
86 | setIsMenuMultiplayerOpen(true)}
88 | title={t('mainmenu.multiplayer.title')} alt={'Multiplayer'} image={'/assets/icons/menu/menu-multiplayer.webp'}
89 | />
90 | {}}
92 | title={t('mainmenu.store.title')} alt={'Store'} image={'/assets/icons/menu/menu-store.webp'}
93 | />
94 |
95 |
96 | {/* Secondary Menu Options */}
97 |
98 | setIsMenuAchievementOpen(true)}
100 | title={t('mainmenu.achievement.title')} alt={'Achievement'} image={'/assets/icons/menu/menu-achievement.webp'}
101 | />
102 | setIsMenuQuestOpen(true)}
104 | title={t('mainmenu.quest.title')} alt={'Quests'} image={'/assets/icons/menu/menu-quest.webp'}
105 | />
106 |
107 |
108 | {/* Bottom Actions */}
109 |
110 |
111 |
112 |
120 |
121 |
122 | {/* Game Info */}
123 |
124 |
125 | {t('mainmenu.description', {
126 | versionGame:
127 |
128 | {versionGame}
129 |
130 | })}
131 |
132 |
133 |
134 |
135 |
136 |
143 |
144 | );
145 | };
--------------------------------------------------------------------------------
/src/components/Timer.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 | import { useEffect, useRef, useCallback } from 'react';
3 | import { Badge } from '@/components/ui/badge';
4 | import { useGameStore } from '@/stores';
5 |
6 | interface TimerProps {
7 | isRunning: boolean;
8 | onTimeUpdate: (time: number) => void;
9 | };
10 |
11 | export function Timer({ isRunning, onTimeUpdate }: TimerProps) {
12 | const { time, setTime } = useGameStore();
13 | const rafRef = useRef(null);
14 | const startTimeRef = useRef(null);
15 |
16 | const updateTime = useCallback(() => {
17 | if (startTimeRef.current !== null) {
18 | const currentTime = performance.now();
19 | const elapsed = Math.floor(
20 | (currentTime - startTimeRef.current) / 1000
21 | );
22 | setTime(elapsed);
23 | onTimeUpdate(elapsed);
24 | rafRef.current = requestAnimationFrame(updateTime);
25 | }
26 | }, [onTimeUpdate, setTime]);
27 |
28 | useEffect(() => {
29 | if (isRunning) {
30 | startTimeRef.current = performance.now() - time * 1000;
31 | rafRef.current = requestAnimationFrame(updateTime);
32 | } else if (rafRef.current) {
33 | cancelAnimationFrame(rafRef.current);
34 | }
35 |
36 | return () => {
37 | if (rafRef.current) {
38 | cancelAnimationFrame(rafRef.current);
39 | }
40 | };
41 | }, [time, isRunning, updateTime]);
42 |
43 | const formatTime = useCallback((seconds: number): string => {
44 | const mins = Math.floor(seconds / 60);
45 | const secs = seconds % 60;
46 | return `${String(mins).padStart(2, '0')}:${String(secs).padStart(2, '0')}`;
47 | }, []);
48 |
49 | return (
50 |
51 | {formatTime(time)}
52 |
53 | )
54 | };
55 |
--------------------------------------------------------------------------------
/src/components/TooltipWrapper.tsx:
--------------------------------------------------------------------------------
1 | import {
2 | Tooltip,
3 | TooltipContent,
4 | TooltipProvider,
5 | TooltipTrigger
6 | } from '@/components/ui/tooltip';
7 |
8 | interface UseTooltipProps {
9 | message: string;
10 | children: React.ReactNode;
11 | position?: 'top' | 'bottom' | 'left' | 'right';
12 | };
13 |
14 | const TooltipWrapper = ({ message, children, position = 'top' }: UseTooltipProps) => {
15 | return (
16 |
17 |
18 |
19 | {children}
20 |
21 |
22 | {message}
23 |
24 |
25 |
26 | )
27 | };
28 |
29 | export { TooltipWrapper };
--------------------------------------------------------------------------------
/src/components/Web3Connect.tsx:
--------------------------------------------------------------------------------
1 | import React, { useEffect, useState } from 'react';
2 | import dynamic from 'next/dynamic';
3 |
4 | const WalletAdapterStyles = () => {
5 | useEffect(() => {
6 | import('@solana/wallet-adapter-react-ui/styles.css').catch(err => console.error('Failed to load wallet styles:', err));
7 | }, []);
8 |
9 | return null;
10 | };
11 |
12 | const WalletMultiButton = dynamic(
13 | () => import('@solana/wallet-adapter-react-ui').then(mod => mod.WalletMultiButton), { ssr: false }
14 | );
15 |
16 | const Web3ConnectButton = () => {
17 | const [ mounted, setMounted ] = useState(false);
18 |
19 | useEffect(() => {
20 | setMounted(true);
21 | }, []);
22 |
23 | return (
24 | <>
25 |
26 | {mounted && }
27 | >
28 | )
29 | };
30 |
31 | export { Web3ConnectButton };
--------------------------------------------------------------------------------
/src/components/magicui/confetti.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import type {
4 | GlobalOptions as ConfettiGlobalOptions,
5 | CreateTypes as ConfettiInstance,
6 | Options as ConfettiOptions,
7 | } from "canvas-confetti";
8 | import confetti from "canvas-confetti";
9 | import type { ReactNode } from "react";
10 | import React, {
11 | createContext,
12 | forwardRef,
13 | useCallback,
14 | useEffect,
15 | useImperativeHandle,
16 | useMemo,
17 | useRef,
18 | } from "react";
19 |
20 | import { Button, ButtonProps } from "@/components/ui/button";
21 |
22 | type Api = {
23 | fire: (options?: ConfettiOptions) => void;
24 | };
25 |
26 | type Props = React.ComponentPropsWithRef<"canvas"> & {
27 | options?: ConfettiOptions;
28 | globalOptions?: ConfettiGlobalOptions;
29 | manualstart?: boolean;
30 | children?: ReactNode;
31 | };
32 |
33 | export type ConfettiRef = Api | null;
34 |
35 | const ConfettiContext = createContext({} as Api);
36 |
37 | // Define component first
38 | const ConfettiComponent = forwardRef((props, ref) => {
39 | const {
40 | options,
41 | globalOptions = { resize: true, useWorker: true },
42 | manualstart = false,
43 | children,
44 | ...rest
45 | } = props;
46 | const instanceRef = useRef(null);
47 |
48 | const canvasRef = useCallback(
49 | (node: HTMLCanvasElement) => {
50 | if (node !== null) {
51 | if (instanceRef.current) return;
52 | instanceRef.current = confetti.create(node, {
53 | ...globalOptions,
54 | resize: true,
55 | });
56 | } else {
57 | if (instanceRef.current) {
58 | instanceRef.current.reset();
59 | instanceRef.current = null;
60 | }
61 | }
62 | },
63 | [globalOptions],
64 | );
65 |
66 | const fire = useCallback(
67 | async (opts = {}) => {
68 | try {
69 | await instanceRef.current?.({ ...options, ...opts });
70 | } catch (error) {
71 | console.error("Confetti error:", error);
72 | }
73 | },
74 | [options],
75 | );
76 |
77 | const api = useMemo(
78 | () => ({
79 | fire,
80 | }),
81 | [fire],
82 | );
83 |
84 | useImperativeHandle(ref, () => api, [api]);
85 |
86 | useEffect(() => {
87 | if (!manualstart) {
88 | (async () => {
89 | try {
90 | await fire();
91 | } catch (error) {
92 | console.error("Confetti effect error:", error);
93 | }
94 | })();
95 | }
96 | }, [manualstart, fire]);
97 |
98 | return (
99 |
100 |
101 | {children}
102 |
103 | );
104 | });
105 |
106 | // Set display name immediately
107 | ConfettiComponent.displayName = "Confetti";
108 |
109 | // Export as Confetti
110 | export const Confetti = ConfettiComponent;
111 |
112 | interface ConfettiButtonProps extends ButtonProps {
113 | options?: ConfettiOptions &
114 | ConfettiGlobalOptions & { canvas?: HTMLCanvasElement };
115 | children?: React.ReactNode;
116 | }
117 |
118 | const ConfettiButtonComponent = ({
119 | options,
120 | children,
121 | ...props
122 | }: ConfettiButtonProps) => {
123 | const handleClick = async (event: React.MouseEvent) => {
124 | try {
125 | const rect = event.currentTarget.getBoundingClientRect();
126 | const x = rect.left + rect.width / 2;
127 | const y = rect.top + rect.height / 2;
128 | await confetti({
129 | ...options,
130 | origin: {
131 | x: x / window.innerWidth,
132 | y: y / window.innerHeight,
133 | },
134 | });
135 | } catch (error) {
136 | console.error("Confetti button error:", error);
137 | }
138 | };
139 |
140 | return (
141 |
144 | );
145 | };
146 |
147 | ConfettiButtonComponent.displayName = "ConfettiButton";
148 |
149 | export const ConfettiButton = ConfettiButtonComponent;
150 |
--------------------------------------------------------------------------------
/src/components/magicui/dock.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { cva, type VariantProps } from "class-variance-authority";
4 | import {
5 | motion,
6 | MotionProps,
7 | MotionValue,
8 | useMotionValue,
9 | useSpring,
10 | useTransform,
11 | } from "motion/react";
12 | import React, { PropsWithChildren, useRef } from "react";
13 |
14 | import { cn } from "@/lib/utils";
15 |
16 | export interface DockProps extends VariantProps {
17 | className?: string;
18 | iconSize?: number;
19 | iconMagnification?: number;
20 | iconDistance?: number;
21 | direction?: "top" | "middle" | "bottom";
22 | children: React.ReactNode;
23 | }
24 |
25 | const DEFAULT_SIZE = 40;
26 | const DEFAULT_MAGNIFICATION = 60;
27 | const DEFAULT_DISTANCE = 140;
28 |
29 | const dockVariants = cva(
30 | "supports-backdrop-blur:bg-white/10 supports-backdrop-blur:dark:bg-black/10 mx-auto mt-8 flex h-[58px] w-max items-center justify-center gap-2 rounded-2xl border p-2 backdrop-blur-md",
31 | );
32 |
33 | const Dock = React.forwardRef(
34 | (
35 | {
36 | className,
37 | children,
38 | iconSize = DEFAULT_SIZE,
39 | iconMagnification = DEFAULT_MAGNIFICATION,
40 | iconDistance = DEFAULT_DISTANCE,
41 | direction = "middle",
42 | ...props
43 | },
44 | ref,
45 | ) => {
46 | const mouseX = useMotionValue(Infinity);
47 |
48 | const renderChildren = () => {
49 | return React.Children.map(children, (child) => {
50 | if (React.isValidElement(child) && child.type === DockIcon) {
51 | return React.cloneElement(child as React.ReactElement, {
52 | mouseX: mouseX,
53 | size: iconSize,
54 | magnification: iconMagnification,
55 | distance: iconDistance,
56 | });
57 | }
58 | return child;
59 | });
60 | };
61 |
62 | return (
63 | mouseX.set(e.pageX)}
66 | onMouseLeave={() => mouseX.set(Infinity)}
67 | {...props}
68 | className={cn(dockVariants({ className }), {
69 | "items-start": direction === "top",
70 | "items-center": direction === "middle",
71 | "items-end": direction === "bottom",
72 | })}
73 | >
74 | {renderChildren()}
75 |
76 | );
77 | },
78 | );
79 |
80 | Dock.displayName = "Dock";
81 |
82 | export interface DockIconProps
83 | extends Omit, "children"> {
84 | size?: number;
85 | magnification?: number;
86 | distance?: number;
87 | mouseX?: MotionValue;
88 | className?: string;
89 | children?: React.ReactNode;
90 | props?: PropsWithChildren;
91 | }
92 |
93 | const DockIcon = ({
94 | size = DEFAULT_SIZE,
95 | magnification = DEFAULT_MAGNIFICATION,
96 | distance = DEFAULT_DISTANCE,
97 | mouseX,
98 | className,
99 | children,
100 | ...props
101 | }: DockIconProps) => {
102 | const ref = useRef(null);
103 | const padding = Math.max(6, size * 0.2);
104 | const defaultMouseX = useMotionValue(Infinity);
105 |
106 | const distanceCalc = useTransform(mouseX ?? defaultMouseX, (val: number) => {
107 | const bounds = ref.current?.getBoundingClientRect() ?? { x: 0, width: 0 };
108 | return val - bounds.x - bounds.width / 2;
109 | });
110 |
111 | const sizeTransform = useTransform(
112 | distanceCalc,
113 | [-distance, 0, distance],
114 | [size, magnification, size],
115 | );
116 |
117 | const scaleSize = useSpring(sizeTransform, {
118 | mass: 0.1,
119 | stiffness: 150,
120 | damping: 12,
121 | });
122 |
123 | return (
124 |
133 | {children}
134 |
135 | );
136 | };
137 |
138 | DockIcon.displayName = "DockIcon";
139 |
140 | export { Dock, DockIcon, dockVariants };
141 |
--------------------------------------------------------------------------------
/src/components/magicui/particles.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { cn } from "@/lib/utils";
4 | import React, {
5 | ComponentPropsWithoutRef,
6 | useEffect,
7 | useRef,
8 | useState,
9 | } from "react";
10 |
11 | interface MousePosition {
12 | x: number;
13 | y: number;
14 | }
15 |
16 | function MousePosition(): MousePosition {
17 | const [mousePosition, setMousePosition] = useState({
18 | x: 0,
19 | y: 0,
20 | });
21 |
22 | useEffect(() => {
23 | const handleMouseMove = (event: MouseEvent) => {
24 | setMousePosition({ x: event.clientX, y: event.clientY });
25 | };
26 |
27 | window.addEventListener("mousemove", handleMouseMove);
28 |
29 | return () => {
30 | window.removeEventListener("mousemove", handleMouseMove);
31 | };
32 | }, []);
33 |
34 | return mousePosition;
35 | }
36 |
37 | interface ParticlesProps extends ComponentPropsWithoutRef<"div"> {
38 | className?: string;
39 | quantity?: number;
40 | staticity?: number;
41 | ease?: number;
42 | size?: number;
43 | refresh?: boolean;
44 | color?: string;
45 | vx?: number;
46 | vy?: number;
47 | }
48 |
49 | function hexToRgb(hex: string): number[] {
50 | hex = hex.replace("#", "");
51 |
52 | if (hex.length === 3) {
53 | hex = hex
54 | .split("")
55 | .map((char) => char + char)
56 | .join("");
57 | }
58 |
59 | const hexInt = parseInt(hex, 16);
60 | const red = (hexInt >> 16) & 255;
61 | const green = (hexInt >> 8) & 255;
62 | const blue = hexInt & 255;
63 | return [red, green, blue];
64 | }
65 |
66 | type Circle = {
67 | x: number;
68 | y: number;
69 | translateX: number;
70 | translateY: number;
71 | size: number;
72 | alpha: number;
73 | targetAlpha: number;
74 | dx: number;
75 | dy: number;
76 | magnetism: number;
77 | };
78 |
79 | export const Particles: React.FC = ({
80 | className = "",
81 | quantity = 100,
82 | staticity = 50,
83 | ease = 50,
84 | size = 0.4,
85 | refresh = false,
86 | color = "#ffffff",
87 | vx = 0,
88 | vy = 0,
89 | ...props
90 | }) => {
91 | const canvasRef = useRef(null);
92 | const canvasContainerRef = useRef(null);
93 | const context = useRef(null);
94 | const circles = useRef([]);
95 | const mousePosition = MousePosition();
96 | const mouse = useRef<{ x: number; y: number }>({ x: 0, y: 0 });
97 | const canvasSize = useRef<{ w: number; h: number }>({ w: 0, h: 0 });
98 | const dpr = typeof window !== "undefined" ? window.devicePixelRatio : 1;
99 | const rafID = useRef(null);
100 | const resizeTimeout = useRef(null);
101 |
102 | useEffect(() => {
103 | if (canvasRef.current) {
104 | context.current = canvasRef.current.getContext("2d");
105 | }
106 | initCanvas();
107 | animate();
108 |
109 | const handleResize = () => {
110 | if (resizeTimeout.current) {
111 | clearTimeout(resizeTimeout.current);
112 | }
113 | resizeTimeout.current = setTimeout(() => {
114 | initCanvas();
115 | }, 200);
116 | };
117 |
118 | window.addEventListener("resize", handleResize);
119 |
120 | return () => {
121 | if (rafID.current != null) {
122 | window.cancelAnimationFrame(rafID.current);
123 | }
124 | if (resizeTimeout.current) {
125 | clearTimeout(resizeTimeout.current);
126 | }
127 | window.removeEventListener("resize", handleResize);
128 | };
129 | // eslint-disable-next-line react-hooks/exhaustive-deps
130 | }, [color]);
131 |
132 | useEffect(() => {
133 | onMouseMove();
134 | // eslint-disable-next-line react-hooks/exhaustive-deps
135 | }, [mousePosition.x, mousePosition.y]);
136 |
137 | useEffect(() => {
138 | initCanvas();
139 | // eslint-disable-next-line react-hooks/exhaustive-deps
140 | }, [refresh]);
141 |
142 | const initCanvas = () => {
143 | resizeCanvas();
144 | drawParticles();
145 | };
146 |
147 | const onMouseMove = () => {
148 | if (canvasRef.current) {
149 | const rect = canvasRef.current.getBoundingClientRect();
150 | const { w, h } = canvasSize.current;
151 | const x = mousePosition.x - rect.left - w / 2;
152 | const y = mousePosition.y - rect.top - h / 2;
153 | const inside = x < w / 2 && x > -w / 2 && y < h / 2 && y > -h / 2;
154 | if (inside) {
155 | mouse.current.x = x;
156 | mouse.current.y = y;
157 | }
158 | }
159 | };
160 |
161 | const resizeCanvas = () => {
162 | if (canvasContainerRef.current && canvasRef.current && context.current) {
163 | canvasSize.current.w = canvasContainerRef.current.offsetWidth;
164 | canvasSize.current.h = canvasContainerRef.current.offsetHeight;
165 |
166 | canvasRef.current.width = canvasSize.current.w * dpr;
167 | canvasRef.current.height = canvasSize.current.h * dpr;
168 | canvasRef.current.style.width = `${canvasSize.current.w}px`;
169 | canvasRef.current.style.height = `${canvasSize.current.h}px`;
170 | context.current.scale(dpr, dpr);
171 |
172 | // Clear existing particles and create new ones with exact quantity
173 | circles.current = [];
174 | for (let i = 0; i < quantity; i++) {
175 | const circle = circleParams();
176 | drawCircle(circle);
177 | }
178 | }
179 | };
180 |
181 | const circleParams = (): Circle => {
182 | const x = Math.floor(Math.random() * canvasSize.current.w);
183 | const y = Math.floor(Math.random() * canvasSize.current.h);
184 | const translateX = 0;
185 | const translateY = 0;
186 | const pSize = Math.floor(Math.random() * 2) + size;
187 | const alpha = 0;
188 | const targetAlpha = parseFloat((Math.random() * 0.6 + 0.1).toFixed(1));
189 | const dx = (Math.random() - 0.5) * 0.1;
190 | const dy = (Math.random() - 0.5) * 0.1;
191 | const magnetism = 0.1 + Math.random() * 4;
192 | return {
193 | x,
194 | y,
195 | translateX,
196 | translateY,
197 | size: pSize,
198 | alpha,
199 | targetAlpha,
200 | dx,
201 | dy,
202 | magnetism,
203 | };
204 | };
205 |
206 | const rgb = hexToRgb(color);
207 |
208 | const drawCircle = (circle: Circle, update = false) => {
209 | if (context.current) {
210 | const { x, y, translateX, translateY, size, alpha } = circle;
211 | context.current.translate(translateX, translateY);
212 | context.current.beginPath();
213 | context.current.arc(x, y, size, 0, 2 * Math.PI);
214 | context.current.fillStyle = `rgba(${rgb.join(", ")}, ${alpha})`;
215 | context.current.fill();
216 | context.current.setTransform(dpr, 0, 0, dpr, 0, 0);
217 |
218 | if (!update) {
219 | circles.current.push(circle);
220 | }
221 | }
222 | };
223 |
224 | const clearContext = () => {
225 | if (context.current) {
226 | context.current.clearRect(
227 | 0,
228 | 0,
229 | canvasSize.current.w,
230 | canvasSize.current.h,
231 | );
232 | }
233 | };
234 |
235 | const drawParticles = () => {
236 | clearContext();
237 | const particleCount = quantity;
238 | for (let i = 0; i < particleCount; i++) {
239 | const circle = circleParams();
240 | drawCircle(circle);
241 | }
242 | };
243 |
244 | const remapValue = (
245 | value: number,
246 | start1: number,
247 | end1: number,
248 | start2: number,
249 | end2: number,
250 | ): number => {
251 | const remapped =
252 | ((value - start1) * (end2 - start2)) / (end1 - start1) + start2;
253 | return remapped > 0 ? remapped : 0;
254 | };
255 |
256 | const animate = () => {
257 | clearContext();
258 | circles.current.forEach((circle: Circle, i: number) => {
259 | // Handle the alpha value
260 | const edge = [
261 | circle.x + circle.translateX - circle.size, // distance from left edge
262 | canvasSize.current.w - circle.x - circle.translateX - circle.size, // distance from right edge
263 | circle.y + circle.translateY - circle.size, // distance from top edge
264 | canvasSize.current.h - circle.y - circle.translateY - circle.size, // distance from bottom edge
265 | ];
266 | const closestEdge = edge.reduce((a, b) => Math.min(a, b));
267 | const remapClosestEdge = parseFloat(
268 | remapValue(closestEdge, 0, 20, 0, 1).toFixed(2),
269 | );
270 | if (remapClosestEdge > 1) {
271 | circle.alpha += 0.02;
272 | if (circle.alpha > circle.targetAlpha) {
273 | circle.alpha = circle.targetAlpha;
274 | }
275 | } else {
276 | circle.alpha = circle.targetAlpha * remapClosestEdge;
277 | }
278 | circle.x += circle.dx + vx;
279 | circle.y += circle.dy + vy;
280 | circle.translateX +=
281 | (mouse.current.x / (staticity / circle.magnetism) - circle.translateX) /
282 | ease;
283 | circle.translateY +=
284 | (mouse.current.y / (staticity / circle.magnetism) - circle.translateY) /
285 | ease;
286 |
287 | drawCircle(circle, true);
288 |
289 | // circle gets out of the canvas
290 | if (
291 | circle.x < -circle.size ||
292 | circle.x > canvasSize.current.w + circle.size ||
293 | circle.y < -circle.size ||
294 | circle.y > canvasSize.current.h + circle.size
295 | ) {
296 | // remove the circle from the array
297 | circles.current.splice(i, 1);
298 | // create a new circle
299 | const newCircle = circleParams();
300 | drawCircle(newCircle);
301 | }
302 | });
303 | rafID.current = window.requestAnimationFrame(animate);
304 | };
305 |
306 | return (
307 |
313 |
314 |
315 | );
316 | };
317 |
--------------------------------------------------------------------------------
/src/components/magicui/shimmer-button.tsx:
--------------------------------------------------------------------------------
1 | import React, { CSSProperties, ComponentPropsWithoutRef } from "react";
2 |
3 | import { cn } from "@/lib/utils";
4 |
5 | export interface ShimmerButtonProps extends ComponentPropsWithoutRef<"button"> {
6 | shimmerColor?: string;
7 | shimmerSize?: string;
8 | borderRadius?: string;
9 | shimmerDuration?: string;
10 | background?: string;
11 | className?: string;
12 | children?: React.ReactNode;
13 | }
14 |
15 | export const ShimmerButton = React.forwardRef<
16 | HTMLButtonElement,
17 | ShimmerButtonProps
18 | >(
19 | (
20 | {
21 | shimmerColor = "#ffffff",
22 | shimmerSize = "0.05em",
23 | shimmerDuration = "3s",
24 | borderRadius = "100px",
25 | background = "rgba(0, 0, 0, 1)",
26 | className,
27 | children,
28 | ...props
29 | },
30 | ref,
31 | ) => {
32 | return (
33 |
92 | );
93 | },
94 | );
95 |
96 | ShimmerButton.displayName = "ShimmerButton";
97 |
--------------------------------------------------------------------------------
/src/components/magicui/sparkles-text.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { motion } from "motion/react";
4 | import { CSSProperties, ReactElement, useEffect, useState } from "react";
5 |
6 | import { cn } from "@/lib/utils";
7 |
8 | interface Sparkle {
9 | id: string;
10 | x: string;
11 | y: string;
12 | color: string;
13 | delay: number;
14 | scale: number;
15 | lifespan: number;
16 | }
17 |
18 | const Sparkle: React.FC = ({ id, x, y, color, delay, scale }) => {
19 | return (
20 |
34 |
38 |
39 | );
40 | };
41 |
42 | interface SparklesTextProps {
43 | /**
44 | * @default
45 | * @type ReactElement
46 | * @description
47 | * The component to be rendered as the text
48 | * */
49 | as?: ReactElement;
50 |
51 | /**
52 | * @default ""
53 | * @type string
54 | * @description
55 | * The className of the text
56 | */
57 | className?: string;
58 |
59 | /**
60 | * @required
61 | * @type string
62 | * @description
63 | * The text to be displayed
64 | * */
65 | text: string;
66 |
67 | /**
68 | * @default 10
69 | * @type number
70 | * @description
71 | * The count of sparkles
72 | * */
73 | sparklesCount?: number;
74 |
75 | /**
76 | * @default "{first: '#9E7AFF', second: '#FE8BBB'}"
77 | * @type string
78 | * @description
79 | * The colors of the sparkles
80 | * */
81 | colors?: {
82 | first: string;
83 | second: string;
84 | };
85 | }
86 |
87 | export const SparklesText: React.FC = ({
88 | text,
89 | colors = { first: "#9E7AFF", second: "#FE8BBB" },
90 | className,
91 | sparklesCount = 10,
92 | ...props
93 | }) => {
94 | const [sparkles, setSparkles] = useState([]);
95 |
96 | useEffect(() => {
97 | const generateStar = (): Sparkle => {
98 | const starX = `${Math.random() * 100}%`;
99 | const starY = `${Math.random() * 100}%`;
100 | const color = Math.random() > 0.5 ? colors.first : colors.second;
101 | const delay = Math.random() * 2;
102 | const scale = Math.random() * 1 + 0.3;
103 | const lifespan = Math.random() * 10 + 5;
104 | const id = `${starX}-${starY}-${Date.now()}`;
105 | return { id, x: starX, y: starY, color, delay, scale, lifespan };
106 | };
107 |
108 | const initializeStars = () => {
109 | const newSparkles = Array.from({ length: sparklesCount }, generateStar);
110 | setSparkles(newSparkles);
111 | };
112 |
113 | const updateStars = () => {
114 | setSparkles((currentSparkles) =>
115 | currentSparkles.map((star) => {
116 | if (star.lifespan <= 0) {
117 | return generateStar();
118 | } else {
119 | return { ...star, lifespan: star.lifespan - 0.1 };
120 | }
121 | }),
122 | );
123 | };
124 |
125 | initializeStars();
126 | const interval = setInterval(updateStars, 100);
127 |
128 | return () => clearInterval(interval);
129 | }, [colors.first, colors.second, sparklesCount]);
130 |
131 | return (
132 |
142 |
143 | {sparkles.map((sparkle) => (
144 |
145 | ))}
146 | {text}
147 |
148 |
149 | );
150 | };
151 |
--------------------------------------------------------------------------------
/src/components/mainmenu/AchievementCard.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 | import { useTranslation } from '@/hooks/useTranslation';
3 | import { Card, CardContent } from '@/components/ui/card';
4 | import { Badge } from '@/components/ui/badge';
5 | import { cn } from '@/lib/utils';
6 |
7 | interface AchievementCardProps {
8 | title: string;
9 | description: string;
10 | collected: boolean;
11 | dateCollected?: string;
12 | icon: string;
13 | };
14 |
15 | const AchievementCard = ({ title, description, collected, dateCollected, icon }: AchievementCardProps) => {
16 | const { t } = useTranslation();
17 |
18 | return (
19 |
23 |
24 |
28 | {icon}
29 |
30 |
31 |
32 | {title}
33 | {collected && {t('achievement.complete')}}
34 |
35 |
{description}
36 | {collected && dateCollected && (
37 |
38 | {t('achievement.achieved', { date: new Date(dateCollected).toLocaleDateString() })}
39 |
40 | )}
41 |
42 |
43 |
44 | );
45 | };
46 |
47 | export { AchievementCard };
--------------------------------------------------------------------------------
/src/components/mainmenu/QuestCard.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 | import { useTranslation } from '@/hooks/useTranslation';
3 | import { Card, CardContent } from '@/components/ui/card';
4 | import { Badge } from '@/components/ui/badge';
5 | import { cn } from '@/lib/utils';
6 |
7 | interface QuestCardProps {
8 | title: string;
9 | description: string;
10 | progress: number;
11 | total: number;
12 | reward: string;
13 | completed?: boolean;
14 | };
15 |
16 | const QuestCard = ({ title, description, progress, total, reward, completed = false }: QuestCardProps) => {
17 | const { t } = useTranslation();
18 | const progressPercent = (progress / total) * 100;
19 |
20 | return (
21 |
25 |
26 |
27 |
{title}
28 | {reward}
29 |
30 | {description}
31 |
32 |
33 | {progress} / {total}
34 | {completed && {t('quest.complete')}}
35 |
36 |
45 |
46 |
47 |
48 | );
49 | };
50 |
51 | export { QuestCard };
--------------------------------------------------------------------------------
/src/components/modal/AchievementModal.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 | import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription } from '@/components/ui/dialog';
3 | import { useTranslation } from '@/hooks/useTranslation';
4 | import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
5 | import { MOCK_ACHIEVEMENT_DATA } from '@/configs/mock';
6 | import { AchievementCard } from '@/components/mainmenu/AchievementCard';
7 | import { useCommonStore } from '@/stores';
8 |
9 | export const AchievementModal = () => {
10 | const { t } = useTranslation();
11 | const { isMenuAchievementOpen, setIsMenuAchievementOpen } = useCommonStore();
12 |
13 | return (
14 |
76 | )
77 | };
--------------------------------------------------------------------------------
/src/components/modal/MultiplayerModal.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 | import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription } from '@/components/ui/dialog';
3 | import { Button } from '@/components/ui/button';
4 | import { Badge } from '@/components/ui/badge';
5 | import { ChevronRight, Trophy, Users } from 'lucide-react';
6 | import { useTranslation } from '@/hooks/useTranslation';
7 | import { useCommonStore } from '@/stores';
8 |
9 | export const MultiplayerModal = () => {
10 | const { t } = useTranslation();
11 | const { isMenuMultiplayerOpen, setIsMenuMultiplayerOpen } = useCommonStore();
12 |
13 | return (
14 |
59 | )
60 | };
--------------------------------------------------------------------------------
/src/components/modal/QuestModal.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 | import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription } from '@/components/ui/dialog';
3 | import { useTranslation } from '@/hooks/useTranslation';
4 | import { MOCK_QUEST_DATA } from '@/configs/mock';
5 | import { QuestCard } from '@/components/mainmenu/QuestCard';
6 | import { useCommonStore } from '@/stores';
7 |
8 | export const QuestModal = () => {
9 | const { t } = useTranslation();
10 | const { isMenuQuestOpen, setIsMenuQuestOpen } = useCommonStore();
11 |
12 | return (
13 |
41 | )
42 | };
--------------------------------------------------------------------------------
/src/components/modal/SettingsModal.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 | import { useMemo } from 'react';
3 | import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription } from '@/components/ui/dialog';
4 | import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
5 | import { Button } from '@/components/ui/button';
6 | import { Input } from '@/components/ui/input';
7 | import { Moon, Sun } from 'lucide-react';
8 | import { useTranslation } from '@/hooks/useTranslation';
9 | import { Label } from '@/components/ui/label';
10 | import { useTheme } from 'next-themes';
11 | import { LANGUAGE_OPTIONS, FLAG_OPTIONS, BOMB_OPTIONS, NUMBER_OPTIONS } from '@/configs';
12 | import { useCommonStore, useLanguageStore, useSettingStore } from '@/stores';
13 |
14 | interface Option {
15 | value: string;
16 | label?: string;
17 | icon?: React.ElementType;
18 | svgIcon?: string;
19 | };
20 |
21 | interface SelectFieldProps {
22 | id: string;
23 | label: string;
24 | color?: string;
25 | value: string;
26 | options: Option[];
27 | onChange: (value: string) => void;
28 | };
29 |
30 | const SelectField: React.FC = ({ id, label, color, value, options, onChange }) => {
31 | const { t } = useTranslation();
32 |
33 | return (
34 | <>
35 |
36 |
57 | >
58 | );
59 | };
60 |
61 | export const SettingsModal = () => {
62 | const { t } = useTranslation();
63 | const { lang, setLang } = useLanguageStore();
64 | const { theme, setTheme } = useTheme();
65 | const { isMenuSettingOpen, setIsMenuSettingOpen } = useCommonStore();
66 | const {
67 | flagIcon, setFlagIcon,
68 | flagColor, setFlagColor,
69 | bombIcon, setBombIcon,
70 | numberStyle, setNumberStyle
71 | } = useSettingStore();
72 |
73 | const Icon = useMemo(() => (theme === 'dark' ? Sun : Moon), [theme]);
74 |
75 | const handleFlagColorChange = (e: React.ChangeEvent) => {
76 | setFlagColor(e.target.value);
77 | };
78 |
79 | return (
80 |
168 | )
169 | };
--------------------------------------------------------------------------------
/src/components/modal/SingleplayerModal.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 | import Image from 'next/image';
3 | import { Button } from '@/components/ui/button';
4 | import { Badge } from '@/components/ui/badge';
5 | import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription } from '@/components/ui/dialog';
6 | import { useTranslation } from '@/hooks/useTranslation';
7 | import { Bomb } from 'lucide-react';
8 | import { useCommonStore, useGameStore } from '@/stores';
9 | import { cn } from '@/lib/utils';
10 | import { DIFFICULTY_DATA } from '@/configs';
11 |
12 | export const SingleplayerModal = () => {
13 | const { t } = useTranslation();
14 | const { isMenuSingleplayerOpen, setIsMenuSingleplayerOpen } = useCommonStore();
15 | const { difficulty, setDifficulty, setIsStartGame } = useGameStore();
16 |
17 | const handleGameStart = () => {
18 | setIsStartGame(true);
19 | };
20 |
21 | return (
22 |
74 | )
75 | };
--------------------------------------------------------------------------------
/src/components/ui/alert.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 | import { cva, type VariantProps } from "class-variance-authority"
3 |
4 | import { cn } from "@/lib/utils"
5 |
6 | const alertVariants = cva(
7 | "relative w-full rounded-lg border px-4 py-3 text-sm [&>svg+div]:translate-y-[-3px] [&>svg]:absolute [&>svg]:left-4 [&>svg]:top-4 [&>svg]:text-foreground [&>svg~*]:pl-7",
8 | {
9 | variants: {
10 | variant: {
11 | default: "bg-background text-foreground",
12 | destructive:
13 | "border-destructive/50 text-destructive dark:border-destructive [&>svg]:text-destructive",
14 | },
15 | },
16 | defaultVariants: {
17 | variant: "default",
18 | },
19 | }
20 | )
21 |
22 | const Alert = React.forwardRef<
23 | HTMLDivElement,
24 | React.HTMLAttributes & VariantProps
25 | >(({ className, variant, ...props }, ref) => (
26 |
32 | ))
33 | Alert.displayName = "Alert"
34 |
35 | const AlertTitle = React.forwardRef<
36 | HTMLParagraphElement,
37 | React.HTMLAttributes
38 | >(({ className, ...props }, ref) => (
39 |
44 | ))
45 | AlertTitle.displayName = "AlertTitle"
46 |
47 | const AlertDescription = React.forwardRef<
48 | HTMLParagraphElement,
49 | React.HTMLAttributes
50 | >(({ className, ...props }, ref) => (
51 |
56 | ))
57 | AlertDescription.displayName = "AlertDescription"
58 |
59 | export { Alert, AlertTitle, AlertDescription }
60 |
--------------------------------------------------------------------------------
/src/components/ui/badge.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 | import { cva, type VariantProps } from "class-variance-authority"
3 |
4 | import { cn } from "@/lib/utils"
5 |
6 | const badgeVariants = cva(
7 | "inline-flex items-center rounded-md border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2",
8 | {
9 | variants: {
10 | variant: {
11 | default:
12 | "border-transparent bg-primary text-primary-foreground shadow hover:bg-primary/80",
13 | secondary:
14 | "border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80",
15 | destructive:
16 | "border-transparent bg-destructive text-destructive-foreground shadow hover:bg-destructive/80",
17 | outline: "text-foreground",
18 | },
19 | },
20 | defaultVariants: {
21 | variant: "default",
22 | },
23 | }
24 | )
25 |
26 | export interface BadgeProps
27 | extends React.HTMLAttributes,
28 | VariantProps {}
29 |
30 | function Badge({ className, variant, ...props }: BadgeProps) {
31 | return (
32 |
33 | )
34 | }
35 |
36 | export { Badge, badgeVariants }
37 |
--------------------------------------------------------------------------------
/src/components/ui/button.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 | import { Slot } from "@radix-ui/react-slot"
3 | import { cva, type VariantProps } from "class-variance-authority"
4 |
5 | import { cn } from "@/lib/utils"
6 |
7 | const buttonVariants = cva(
8 | "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
9 | {
10 | variants: {
11 | variant: {
12 | default:
13 | "bg-primary text-primary-foreground shadow hover:bg-primary/90",
14 | destructive:
15 | "bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90",
16 | outline:
17 | "border border-input bg-background shadow-sm hover:bg-accent hover:text-accent-foreground",
18 | secondary:
19 | "bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80",
20 | ghost: "hover:bg-accent hover:text-accent-foreground",
21 | link: "text-primary underline-offset-4 hover:underline",
22 | },
23 | size: {
24 | default: "h-9 px-4 py-2",
25 | sm: "h-8 rounded-md px-3 text-xs",
26 | lg: "h-10 rounded-md px-8",
27 | icon: "h-9 w-9",
28 | },
29 | },
30 | defaultVariants: {
31 | variant: "default",
32 | size: "default",
33 | },
34 | }
35 | )
36 |
37 | export interface ButtonProps
38 | extends React.ButtonHTMLAttributes,
39 | VariantProps {
40 | asChild?: boolean
41 | }
42 |
43 | const Button = React.forwardRef(
44 | ({ className, variant, size, asChild = false, ...props }, ref) => {
45 | const Comp = asChild ? Slot : "button"
46 | return (
47 |
52 | )
53 | }
54 | )
55 | Button.displayName = "Button"
56 |
57 | export { Button, buttonVariants }
58 |
--------------------------------------------------------------------------------
/src/components/ui/card.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 |
3 | import { cn } from "@/lib/utils"
4 |
5 | const Card = React.forwardRef<
6 | HTMLDivElement,
7 | React.HTMLAttributes
8 | >(({ className, ...props }, ref) => (
9 |
17 | ))
18 | Card.displayName = "Card"
19 |
20 | const CardHeader = React.forwardRef<
21 | HTMLDivElement,
22 | React.HTMLAttributes
23 | >(({ className, ...props }, ref) => (
24 |
29 | ))
30 | CardHeader.displayName = "CardHeader"
31 |
32 | const CardTitle = React.forwardRef<
33 | HTMLDivElement,
34 | React.HTMLAttributes
35 | >(({ className, ...props }, ref) => (
36 |
41 | ))
42 | CardTitle.displayName = "CardTitle"
43 |
44 | const CardDescription = React.forwardRef<
45 | HTMLDivElement,
46 | React.HTMLAttributes
47 | >(({ className, ...props }, ref) => (
48 |
53 | ))
54 | CardDescription.displayName = "CardDescription"
55 |
56 | const CardContent = React.forwardRef<
57 | HTMLDivElement,
58 | React.HTMLAttributes
59 | >(({ className, ...props }, ref) => (
60 |
61 | ))
62 | CardContent.displayName = "CardContent"
63 |
64 | const CardFooter = React.forwardRef<
65 | HTMLDivElement,
66 | React.HTMLAttributes
67 | >(({ className, ...props }, ref) => (
68 |
73 | ))
74 | CardFooter.displayName = "CardFooter"
75 |
76 | export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent }
77 |
--------------------------------------------------------------------------------
/src/components/ui/dialog.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as DialogPrimitive from "@radix-ui/react-dialog"
5 | import { X } from "lucide-react"
6 |
7 | import { cn } from "@/lib/utils"
8 |
9 | const Dialog = DialogPrimitive.Root
10 |
11 | const DialogTrigger = DialogPrimitive.Trigger
12 |
13 | const DialogPortal = DialogPrimitive.Portal
14 |
15 | const DialogClose = DialogPrimitive.Close
16 |
17 | const DialogOverlay = React.forwardRef<
18 | React.ElementRef,
19 | React.ComponentPropsWithoutRef
20 | >(({ className, ...props }, ref) => (
21 |
29 | ))
30 | DialogOverlay.displayName = DialogPrimitive.Overlay.displayName
31 |
32 | const DialogContent = React.forwardRef<
33 | React.ElementRef,
34 | React.ComponentPropsWithoutRef
35 | >(({ className, children, ...props }, ref) => (
36 |
37 |
38 |
46 | {children}
47 |
48 |
49 | Close
50 |
51 |
52 |
53 | ))
54 | DialogContent.displayName = DialogPrimitive.Content.displayName
55 |
56 | const DialogHeader = ({
57 | className,
58 | ...props
59 | }: React.HTMLAttributes) => (
60 |
67 | )
68 | DialogHeader.displayName = "DialogHeader"
69 |
70 | const DialogFooter = ({
71 | className,
72 | ...props
73 | }: React.HTMLAttributes) => (
74 |
81 | )
82 | DialogFooter.displayName = "DialogFooter"
83 |
84 | const DialogTitle = React.forwardRef<
85 | React.ElementRef,
86 | React.ComponentPropsWithoutRef
87 | >(({ className, ...props }, ref) => (
88 |
96 | ))
97 | DialogTitle.displayName = DialogPrimitive.Title.displayName
98 |
99 | const DialogDescription = React.forwardRef<
100 | React.ElementRef,
101 | React.ComponentPropsWithoutRef
102 | >(({ className, ...props }, ref) => (
103 |
108 | ))
109 | DialogDescription.displayName = DialogPrimitive.Description.displayName
110 |
111 | export {
112 | Dialog,
113 | DialogPortal,
114 | DialogOverlay,
115 | DialogTrigger,
116 | DialogClose,
117 | DialogContent,
118 | DialogHeader,
119 | DialogFooter,
120 | DialogTitle,
121 | DialogDescription,
122 | }
123 |
--------------------------------------------------------------------------------
/src/components/ui/dropdown-menu.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu"
5 | import { Check, ChevronRight, Circle } from "lucide-react"
6 |
7 | import { cn } from "@/lib/utils"
8 |
9 | const DropdownMenu = DropdownMenuPrimitive.Root
10 |
11 | const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger
12 |
13 | const DropdownMenuGroup = DropdownMenuPrimitive.Group
14 |
15 | const DropdownMenuPortal = DropdownMenuPrimitive.Portal
16 |
17 | const DropdownMenuSub = DropdownMenuPrimitive.Sub
18 |
19 | const DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup
20 |
21 | const DropdownMenuSubTrigger = React.forwardRef<
22 | React.ElementRef,
23 | React.ComponentPropsWithoutRef & {
24 | inset?: boolean
25 | }
26 | >(({ className, inset, children, ...props }, ref) => (
27 |
36 | {children}
37 |
38 |
39 | ))
40 | DropdownMenuSubTrigger.displayName =
41 | DropdownMenuPrimitive.SubTrigger.displayName
42 |
43 | const DropdownMenuSubContent = React.forwardRef<
44 | React.ElementRef,
45 | React.ComponentPropsWithoutRef
46 | >(({ className, ...props }, ref) => (
47 |
55 | ))
56 | DropdownMenuSubContent.displayName =
57 | DropdownMenuPrimitive.SubContent.displayName
58 |
59 | const DropdownMenuContent = React.forwardRef<
60 | React.ElementRef,
61 | React.ComponentPropsWithoutRef
62 | >(({ className, sideOffset = 4, ...props }, ref) => (
63 |
64 |
74 |
75 | ))
76 | DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName
77 |
78 | const DropdownMenuItem = React.forwardRef<
79 | React.ElementRef,
80 | React.ComponentPropsWithoutRef & {
81 | inset?: boolean
82 | }
83 | >(({ className, inset, ...props }, ref) => (
84 | svg]:size-4 [&>svg]:shrink-0",
88 | inset && "pl-8",
89 | className
90 | )}
91 | {...props}
92 | />
93 | ))
94 | DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName
95 |
96 | const DropdownMenuCheckboxItem = React.forwardRef<
97 | React.ElementRef,
98 | React.ComponentPropsWithoutRef
99 | >(({ className, children, checked, ...props }, ref) => (
100 |
109 |
110 |
111 |
112 |
113 |
114 | {children}
115 |
116 | ))
117 | DropdownMenuCheckboxItem.displayName =
118 | DropdownMenuPrimitive.CheckboxItem.displayName
119 |
120 | const DropdownMenuRadioItem = React.forwardRef<
121 | React.ElementRef,
122 | React.ComponentPropsWithoutRef
123 | >(({ className, children, ...props }, ref) => (
124 |
132 |
133 |
134 |
135 |
136 |
137 | {children}
138 |
139 | ))
140 | DropdownMenuRadioItem.displayName = DropdownMenuPrimitive.RadioItem.displayName
141 |
142 | const DropdownMenuLabel = React.forwardRef<
143 | React.ElementRef,
144 | React.ComponentPropsWithoutRef & {
145 | inset?: boolean
146 | }
147 | >(({ className, inset, ...props }, ref) => (
148 |
157 | ))
158 | DropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName
159 |
160 | const DropdownMenuSeparator = React.forwardRef<
161 | React.ElementRef,
162 | React.ComponentPropsWithoutRef
163 | >(({ className, ...props }, ref) => (
164 |
169 | ))
170 | DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName
171 |
172 | const DropdownMenuShortcut = ({
173 | className,
174 | ...props
175 | }: React.HTMLAttributes) => {
176 | return (
177 |
181 | )
182 | }
183 | DropdownMenuShortcut.displayName = "DropdownMenuShortcut"
184 |
185 | export {
186 | DropdownMenu,
187 | DropdownMenuTrigger,
188 | DropdownMenuContent,
189 | DropdownMenuItem,
190 | DropdownMenuCheckboxItem,
191 | DropdownMenuRadioItem,
192 | DropdownMenuLabel,
193 | DropdownMenuSeparator,
194 | DropdownMenuShortcut,
195 | DropdownMenuGroup,
196 | DropdownMenuPortal,
197 | DropdownMenuSub,
198 | DropdownMenuSubContent,
199 | DropdownMenuSubTrigger,
200 | DropdownMenuRadioGroup,
201 | }
202 |
--------------------------------------------------------------------------------
/src/components/ui/input.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 |
3 | import { cn } from "@/lib/utils"
4 |
5 | function Input({ className, type, ...props }: React.ComponentProps<"input">) {
6 | return (
7 |
18 | )
19 | }
20 |
21 | export { Input }
22 |
--------------------------------------------------------------------------------
/src/components/ui/label.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as LabelPrimitive from "@radix-ui/react-label"
5 | import { cva, type VariantProps } from "class-variance-authority"
6 |
7 | import { cn } from "@/lib/utils"
8 |
9 | const labelVariants = cva(
10 | "text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
11 | )
12 |
13 | const Label = React.forwardRef<
14 | React.ElementRef,
15 | React.ComponentPropsWithoutRef &
16 | VariantProps
17 | >(({ className, ...props }, ref) => (
18 |
23 | ))
24 | Label.displayName = LabelPrimitive.Root.displayName
25 |
26 | export { Label }
27 |
--------------------------------------------------------------------------------
/src/components/ui/select.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as SelectPrimitive from "@radix-ui/react-select"
5 | import { Check, ChevronDown, ChevronUp } from "lucide-react"
6 |
7 | import { cn } from "@/lib/utils"
8 |
9 | const Select = SelectPrimitive.Root
10 |
11 | const SelectGroup = SelectPrimitive.Group
12 |
13 | const SelectValue = SelectPrimitive.Value
14 |
15 | const SelectTrigger = React.forwardRef<
16 | React.ElementRef,
17 | React.ComponentPropsWithoutRef
18 | >(({ className, children, ...props }, ref) => (
19 | span]:line-clamp-1",
23 | className
24 | )}
25 | {...props}
26 | >
27 | {children}
28 |
29 |
30 |
31 |
32 | ))
33 | SelectTrigger.displayName = SelectPrimitive.Trigger.displayName
34 |
35 | const SelectScrollUpButton = React.forwardRef<
36 | React.ElementRef,
37 | React.ComponentPropsWithoutRef
38 | >(({ className, ...props }, ref) => (
39 |
47 |
48 |
49 | ))
50 | SelectScrollUpButton.displayName = SelectPrimitive.ScrollUpButton.displayName
51 |
52 | const SelectScrollDownButton = React.forwardRef<
53 | React.ElementRef,
54 | React.ComponentPropsWithoutRef
55 | >(({ className, ...props }, ref) => (
56 |
64 |
65 |
66 | ))
67 | SelectScrollDownButton.displayName =
68 | SelectPrimitive.ScrollDownButton.displayName
69 |
70 | const SelectContent = React.forwardRef<
71 | React.ElementRef,
72 | React.ComponentPropsWithoutRef
73 | >(({ className, children, position = "popper", ...props }, ref) => (
74 |
75 |
86 |
87 |
94 | {children}
95 |
96 |
97 |
98 |
99 | ))
100 | SelectContent.displayName = SelectPrimitive.Content.displayName
101 |
102 | const SelectLabel = React.forwardRef<
103 | React.ElementRef,
104 | React.ComponentPropsWithoutRef
105 | >(({ className, ...props }, ref) => (
106 |
111 | ))
112 | SelectLabel.displayName = SelectPrimitive.Label.displayName
113 |
114 | const SelectItem = React.forwardRef<
115 | React.ElementRef,
116 | React.ComponentPropsWithoutRef
117 | >(({ className, children, ...props }, ref) => (
118 |
126 |
127 |
128 |
129 |
130 |
131 | {children}
132 |
133 | ))
134 | SelectItem.displayName = SelectPrimitive.Item.displayName
135 |
136 | const SelectSeparator = React.forwardRef<
137 | React.ElementRef,
138 | React.ComponentPropsWithoutRef
139 | >(({ className, ...props }, ref) => (
140 |
145 | ))
146 | SelectSeparator.displayName = SelectPrimitive.Separator.displayName
147 |
148 | export {
149 | Select,
150 | SelectGroup,
151 | SelectValue,
152 | SelectTrigger,
153 | SelectContent,
154 | SelectLabel,
155 | SelectItem,
156 | SelectSeparator,
157 | SelectScrollUpButton,
158 | SelectScrollDownButton,
159 | }
160 |
--------------------------------------------------------------------------------
/src/components/ui/separator.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as SeparatorPrimitive from "@radix-ui/react-separator"
5 |
6 | import { cn } from "@/lib/utils"
7 |
8 | const Separator = React.forwardRef<
9 | React.ElementRef,
10 | React.ComponentPropsWithoutRef
11 | >(
12 | (
13 | { className, orientation = "horizontal", decorative = true, ...props },
14 | ref
15 | ) => (
16 |
27 | )
28 | )
29 | Separator.displayName = SeparatorPrimitive.Root.displayName
30 |
31 | export { Separator }
32 |
--------------------------------------------------------------------------------
/src/components/ui/tabs.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as TabsPrimitive from "@radix-ui/react-tabs"
5 |
6 | import { cn } from "@/lib/utils"
7 |
8 | function Tabs({
9 | className,
10 | ...props
11 | }: React.ComponentProps) {
12 | return (
13 |
18 | )
19 | }
20 |
21 | function TabsList({
22 | className,
23 | ...props
24 | }: React.ComponentProps) {
25 | return (
26 |
34 | )
35 | }
36 |
37 | function TabsTrigger({
38 | className,
39 | ...props
40 | }: React.ComponentProps) {
41 | return (
42 |
50 | )
51 | }
52 |
53 | function TabsContent({
54 | className,
55 | ...props
56 | }: React.ComponentProps) {
57 | return (
58 |
63 | )
64 | }
65 |
66 | export { Tabs, TabsList, TabsTrigger, TabsContent }
67 |
--------------------------------------------------------------------------------
/src/components/ui/tooltip.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as TooltipPrimitive from "@radix-ui/react-tooltip"
5 |
6 | import { cn } from "@/lib/utils"
7 |
8 | const TooltipProvider = TooltipPrimitive.Provider
9 |
10 | const Tooltip = TooltipPrimitive.Root
11 |
12 | const TooltipTrigger = TooltipPrimitive.Trigger
13 |
14 | const TooltipContent = React.forwardRef<
15 | React.ElementRef,
16 | React.ComponentPropsWithoutRef
17 | >(({ className, sideOffset = 4, ...props }, ref) => (
18 |
19 |
28 |
29 | ))
30 | TooltipContent.displayName = TooltipPrimitive.Content.displayName
31 |
32 | export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider }
33 |
--------------------------------------------------------------------------------
/src/configs/game.tsx:
--------------------------------------------------------------------------------
1 | import { FLAG_OPTIONS, BOMB_OPTIONS } from '@/configs/settings';
2 | import { FlagStyle, BombStyle, NumberStyle } from '@/types';
3 |
4 | export const DIFFICULTY_DATA = {
5 | easy: { mines: 10, size: '9x9', rows: 9, cols: 9, image: '/assets/icons/mode/mode-easy.webp' },
6 | medium: { mines: 40, size: '16x16', rows: 16, cols: 16, image: '/assets/icons/mode/mode-medium.webp' },
7 | hard: { mines: 99, size: '16x30', rows: 16, cols: 30, image: '/assets/icons/mode/mode-hard.webp' }
8 | };
9 |
10 | export const SCORE_CONFIG = {
11 | baseScore: 1000,
12 | maxTime: 300,
13 | multipliers: {
14 | easy: 1,
15 | medium: 2,
16 | hard: 3
17 | },
18 | flagBonus: 1.5
19 | };
20 |
21 | const NUMBER_STYLES = {
22 | roman: ['I', 'II', 'III', 'IV', 'V', 'VI', 'VII', 'VIII'],
23 | thai: ['๑', '๒', '๓', '๔', '๕', '๖', '๗', '๘'],
24 | abc: ['A', 'B', 'C', 'D', 'E', 'F', 'G', 'H'],
25 | question: ['?', '?', '?', '?', '?', '?', '?', '?']
26 | };
27 |
28 | export const getFlagIcon = (style: FlagStyle, color: string) => {
29 | const option = FLAG_OPTIONS.find(opt => opt.value === style);
30 | const Icon = option?.icon || FLAG_OPTIONS[0].icon;
31 | return ;
32 | };
33 |
34 | export const getBombIcon = (style: BombStyle) => {
35 | const option = BOMB_OPTIONS.find(opt => opt.value === style);
36 | const Icon = option?.icon || BOMB_OPTIONS[0].icon;
37 | return ;
38 | };
39 |
40 | export const getNumberDisplay = (number: number, style: NumberStyle) => {
41 | if (number === 0) return null;
42 |
43 | if (style === 'default') return number;
44 |
45 | return NUMBER_STYLES[style]?.[number - 1] || number;
46 | };
--------------------------------------------------------------------------------
/src/configs/index.ts:
--------------------------------------------------------------------------------
1 | import { VIEWPORT, METADATA } from '@/configs/metadata';
2 | import { DIFFICULTY_DATA, SCORE_CONFIG, getFlagIcon, getBombIcon, getNumberDisplay } from '@/configs/game';
3 | import { LANGUAGE_OPTIONS, FLAG_OPTIONS, BOMB_OPTIONS, NUMBER_OPTIONS } from '@/configs/settings';
4 |
5 | export {
6 | VIEWPORT, METADATA,
7 | DIFFICULTY_DATA, SCORE_CONFIG, getFlagIcon, getBombIcon, getNumberDisplay,
8 | LANGUAGE_OPTIONS, FLAG_OPTIONS, BOMB_OPTIONS, NUMBER_OPTIONS
9 | };
--------------------------------------------------------------------------------
/src/configs/metadata.ts:
--------------------------------------------------------------------------------
1 | import type { Viewport, Metadata } from 'next';
2 |
3 | export const VIEWPORT: Viewport = {
4 | width: 'device-width',
5 | initialScale: 1,
6 | maximumScale: 5,
7 | themeColor: '#000000'
8 | }
9 |
10 | export const METADATA: Metadata = {
11 | manifest: '/manifest.json',
12 | title: 'Minesweeper',
13 | description: 'A simple implementation of the classic Minesweeper game built with Next.js. This project showcases the use of React components and modern front-end development techniques. Players can enjoy the Minesweeper game experience directly in their browser, with a clean and responsive UI.',
14 | openGraph: {
15 | title: 'Minesweeper',
16 | description: 'A simple implementation of the classic Minesweeper game built with Next.js. This project showcases the use of React components and modern front-end development techniques. Players can enjoy the Minesweeper game experience directly in their browser, with a clean and responsive UI.',
17 | url: 'https://nextjs-minesweeper-game.vercel.app/',
18 | siteName: 'Minesweeper',
19 | images: [
20 | {
21 | url: 'https://nextjs-minesweeper-game.vercel.app/metadata/manifest.webp',
22 | width: 1200,
23 | height: 630
24 | }
25 | ]
26 | },
27 | keywords: ['Minesweeper', 'nextjs', 'game-website', 'meteorviix'],
28 | authors: [
29 | { name: 'Minesweeper' },
30 | {
31 | name: 'Minesweeper',
32 | url: 'https://nextjs-minesweeper-game.vercel.app/',
33 | },
34 | ],
35 | icons: [
36 | { rel: 'apple-touch-icon', url: 'icon/icon-128x128.png' },
37 | { rel: 'icon', url: 'icon/icon-128x128.png' },
38 | ],
39 | appleWebApp: {
40 | capable: true,
41 | statusBarStyle: 'default',
42 | title: 'Minesweeper'
43 | },
44 | applicationName: 'Minesweeper',
45 | formatDetection: {
46 | telephone: false
47 | }
48 | };
--------------------------------------------------------------------------------
/src/configs/mock.ts:
--------------------------------------------------------------------------------
1 | export const MOCK_QUEST_DATA = [
2 | {
3 | "id": 1,
4 | "title": "Quick Victory",
5 | "description": "Win a game on any difficulty in under 60 seconds",
6 | "progress": 0,
7 | "total": 1,
8 | "reward": "50 points",
9 | "completed": false
10 | },
11 | {
12 | "id": 2,
13 | "title": "Mine Sweeper",
14 | "description": "Win 3 games on medium difficulty or higher",
15 | "progress": 2,
16 | "total": 3,
17 | "reward": "100 points",
18 | "completed": false
19 | },
20 | {
21 | "id": 3,
22 | "title": "Perfect Game",
23 | "description": "Complete a game without using any flags",
24 | "progress": 1,
25 | "total": 1,
26 | "reward": "150 points",
27 | "completed": true
28 | }
29 | ];
30 |
31 | export const MOCK_ACHIEVEMENT_DATA = [
32 | {
33 | "title": "First Win",
34 | "description": "Win your first game of Minesweeper",
35 | "collected": true,
36 | "dateCollected": "2023-09-15",
37 | "icon": "🏆"
38 | },
39 | {
40 | "title": "Speed Demon",
41 | "description": "Complete a medium difficulty game in under 30 seconds",
42 | "collected": false,
43 | "dateCollected": null,
44 | "icon": "⚡"
45 | },
46 | {
47 | "title": "Bomb Squad",
48 | "description": "Win 10 games on hard difficulty",
49 | "collected": true,
50 | "dateCollected": "2024-01-20",
51 | "icon": "💣"
52 | },
53 | {
54 | "title": "Undefeated",
55 | "description": "Win 5 games in a row without losing",
56 | "collected": false,
57 | "dateCollected": null,
58 | "icon": "🔥"
59 | }
60 | ];
--------------------------------------------------------------------------------
/src/configs/settings.ts:
--------------------------------------------------------------------------------
1 | import { Language, FlagStyle, BombStyle, NumberStyle } from '@/types';
2 | import {
3 | Pyramid, Radar, Sparkles, Sigma, Flag,
4 | Skull, Flame, FlameKindling, Ghost, Bomb
5 | } from 'lucide-react';
6 |
7 | export const LANGUAGE_OPTIONS: { value: Language; label: string; svgIcon: string; }[] = [
8 | { value: 'en', label: 'settings.language.en', svgIcon: '' },
9 | { value: 'th', label: 'settings.language.th', svgIcon: '' },
10 | { value: 'jp', label: 'settings.language.jp', svgIcon: '' },
11 | { value: 'vi', label: 'settings.language.vi', svgIcon: '' },
12 | { value: 'zh', label: 'settings.language.zh', svgIcon: '' }
13 | ];
14 |
15 | export const FLAG_OPTIONS: { value: FlagStyle; icon: typeof Flag; }[] = [
16 | { value: 'default', icon: Flag },
17 | { value: 'pyramid', icon: Pyramid },
18 | { value: 'radar', icon: Radar },
19 | { value: 'sparkles', icon: Sparkles },
20 | { value: 'sigma', icon: Sigma }
21 | ];
22 |
23 | export const BOMB_OPTIONS: { value: BombStyle; icon: typeof Bomb; }[] = [
24 | { value: 'default', icon: Bomb },
25 | { value: 'skull', icon: Skull },
26 | { value: 'fire', icon: Flame },
27 | { value: 'flame', icon: FlameKindling },
28 | { value: 'ghost', icon: Ghost }
29 | ];
30 |
31 | export const NUMBER_OPTIONS: { value: NumberStyle; label: string; }[] = [
32 | { value: 'default', label: 'settings.number.default' },
33 | { value: 'roman', label: 'settings.number.roman' },
34 | { value: 'thai', label: 'settings.number.thai' },
35 | { value: 'abc', label: 'settings.number.abc' },
36 | { value: 'question', label: 'settings.number.question' }
37 | ];
--------------------------------------------------------------------------------
/src/hooks/useTranslation.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 | import { useState, useEffect, useCallback, ReactNode } from 'react';
3 | import { useLanguageStore } from '@/stores/languageStore';
4 | import { TranslationValue, Variables, Translation } from '@/types';
5 |
6 | const translationCache: { [key: string]: Translation } = {};
7 |
8 | export const useTranslation = () => {
9 | const { lang } = useLanguageStore();
10 | const [ dict, setDict ] = useState({});
11 |
12 | const loadTranslations = useCallback(async () => {
13 | try {
14 | if (translationCache[lang]) {
15 | setDict(translationCache[lang]);
16 | return;
17 | }
18 |
19 | const translation = await import(`@/locales/${lang}.json`);
20 | translationCache[lang] = translation.default;
21 | setDict(translation.default);
22 | } catch (e) {
23 | console.error(`Failed to load ${lang} translation:`, e);
24 | }
25 | }, [lang]);
26 |
27 | useEffect(() => {
28 | loadTranslations();
29 | }, [loadTranslations]);
30 |
31 | const getTranslation = useCallback((key: string, variables?: Variables): string | ReactNode[] => {
32 | try {
33 | const result = key.split('.').reduce((obj, k) => {
34 | if (typeof obj === 'object') {
35 | return obj[k] || key;
36 | }
37 | return key;
38 | }, dict);
39 |
40 | if (typeof result === 'string' && variables) {
41 | const parts = result.split(/({[^}]+})/g);
42 | return parts.map((part, index) => {
43 | const matches = part.match(/^{([^}]+)}$/);
44 | if (matches && variables[matches[1]] !== undefined) {
45 | const value = variables[matches[1]];
46 | return {value};
47 | }
48 | return part;
49 | });
50 | }
51 |
52 | return typeof result === 'string' ? result : key;
53 | } catch {
54 | return key;
55 | }
56 | }, [dict]);
57 |
58 | function t(key: string): string;
59 | function t(key: string, variables: Variables): ReactNode[];
60 | function t(key: string, variables?: Variables): string | ReactNode[] {
61 | return getTranslation(key, variables);
62 | }
63 |
64 | return { t, lang };
65 | };
--------------------------------------------------------------------------------
/src/lib/utils.ts:
--------------------------------------------------------------------------------
1 | import { clsx, type ClassValue } from 'clsx';
2 | import { twMerge } from 'tailwind-merge';
3 |
4 | export function cn(...inputs: ClassValue[]) {
5 | return twMerge(clsx(inputs));
6 | };
--------------------------------------------------------------------------------
/src/locales/en.json:
--------------------------------------------------------------------------------
1 | {
2 | "common": {
3 | "lang": "en",
4 | "game-title": "Minesweeper",
5 | "game-desc": "Digging holes for fun!",
6 | "loading": "Loading..."
7 | },
8 | "not-found": {
9 | "title": "Not Found",
10 | "description": "Please return to the home page.",
11 | "loading": "Loading...",
12 | "button": "Back"
13 | },
14 | "difficulty": {
15 | "easy": "Easy",
16 | "medium": "Medium",
17 | "hard": "Hard"
18 | },
19 | "gameStats": {
20 | "timePlayed": "Time Played",
21 | "flagsPlaced": "Flags Placed",
22 | "difficulty": "Difficulty"
23 | },
24 | "gameresult": {
25 | "description": "Minesweeper is fun!",
26 | "win": "You Win!",
27 | "lose": "Game Over!",
28 | "time": "Time",
29 | "score": "Score",
30 | "difficulty": "Difficulty",
31 | "flag": "Flag Placed",
32 | "share-button": "Share",
33 | "again-button": "Play again"
34 | },
35 | "mainmenu": {
36 | "description": "Create by MeteorVIIx | Version {versionGame}",
37 | "settings": "Settings",
38 | "singleplayer": {
39 | "title": "Singleplayer",
40 | "description": "Select difficulty",
41 | "easy": "Easy",
42 | "medium": "Medium",
43 | "hard": "Hard"
44 | },
45 | "multiplayer": {
46 | "title": "Multiplayer",
47 | "description": "Select a competition",
48 | "casual-title": "Casual Matchmaking",
49 | "casual-desc": "Play for fun with no rank changes",
50 | "rank-title": "Ranked Matchmaking",
51 | "rank-desc": "Compete to climb the leaderboard"
52 | },
53 | "store": {
54 | "title": "Store"
55 | },
56 | "achievement": {
57 | "title": "Achievement",
58 | "description": "All achievements are shown here",
59 | "menu-all": "All",
60 | "menu-collected":"Collected",
61 | "menu-not-collected": "Not Collected"
62 | },
63 | "quest": {
64 | "title": "Quests",
65 | "description": "Complete daily quests to earn rewards. Resets at midnight.",
66 | "reset": "Next reset at 00:00 (GMT+7)"
67 | }
68 | },
69 | "achievement": {
70 | "complete": "Completed",
71 | "achieved": "Achieved on: {date}"
72 | },
73 | "quest": {
74 | "complete": "Completed"
75 | },
76 | "game": {
77 | "mine": "Mines",
78 | "reset-tooltip": "Reset",
79 | "win": "You Win!",
80 | "lose": "Game Over!",
81 | "flag-mode": "Flag Mode",
82 | "dig-mode": "Dig Mode"
83 | },
84 | "settings": {
85 | "title": "Settings",
86 | "description": "Freely customized",
87 | "done-button": "Done",
88 | "theme": {
89 | "title": "Theme",
90 | "system": "System",
91 | "light": "Light",
92 | "dark": "Dark"
93 | },
94 | "language": {
95 | "title": "Languages",
96 | "th": "Thai",
97 | "en": "English",
98 | "jp": "Japanese",
99 | "vi": "Vietnamese",
100 | "zh": "Chinese"
101 | },
102 | "flag": {
103 | "title": "Flag"
104 | },
105 | "flag-color": {
106 | "title": "Flag Color"
107 | },
108 | "bomb": {
109 | "title": "Bomb"
110 | },
111 | "number": {
112 | "title": "Number",
113 | "default": "(1) Normal",
114 | "roman": "(I) Roman",
115 | "thai": "(๑) Thai",
116 | "abc": "(A) ABC",
117 | "question": "(?) Question"
118 | }
119 | }
120 | }
--------------------------------------------------------------------------------
/src/locales/jp.json:
--------------------------------------------------------------------------------
1 | {
2 | "common": {
3 | "lang": "jp",
4 | "game-title": "マインスイーパー",
5 | "game-desc": "楽しく穴掘り!",
6 | "loading": "読み込み中..."
7 | },
8 | "not-found": {
9 | "title": "見つかりませんでした",
10 | "description": "ホームページに戻ってください。",
11 | "loading": "読み込み中...",
12 | "button": "戻る"
13 | },
14 | "difficulty": {
15 | "easy": "簡単",
16 | "medium": "普通",
17 | "hard": "難しい"
18 | },
19 | "gameStats": {
20 | "timePlayed": "プレイ時間",
21 | "flagsPlaced": "配置した旗",
22 | "difficulty": "難易度"
23 | },
24 | "gameresult": {
25 | "description": "Minesweeper は面白い!",
26 | "win": "あなたの勝ち!",
27 | "lose": "ゲームオーバー!",
28 | "time": "時間",
29 | "score": "スコア",
30 | "difficulty": "難易度",
31 | "flag": "配置した旗",
32 | "share-button": "共有",
33 | "again-button": "遊ぶ"
34 | },
35 | "mainmenu": {
36 | "description": "作成者: MeteorVIIx | バージョン {versionGame}",
37 | "settings": "設定",
38 | "singleplayer": {
39 | "title": "シングルプレイヤー",
40 | "description": "難易度を選択",
41 | "easy": "簡単",
42 | "medium": "普通",
43 | "hard": "難しい"
44 | },
45 | "multiplayer": {
46 | "title": "マルチプレイヤー",
47 | "description": "コンテストを選択",
48 | "casual-title": "カジュアル マッチメイキング",
49 | "casual-desc": "ランク変更なしで楽しくプレイ",
50 | "rank-title": "ランク マッチメイキング",
51 | "rank-desc": "ランキング上位を目指して競い合う"
52 | },
53 | "store": {
54 | "title": "ストア"
55 | },
56 | "achievement": {
57 | "title": "実績",
58 | "description": "すべての実績がここに表示されます",
59 | "menu-all": "すべて",
60 | "menu-collected": "収集済み",
61 | "menu-not-collected": "収集されていません"
62 | },
63 | "quest": {
64 | "title": "クエスト",
65 | "description": "毎日のクエストを完了して報酬を獲得します。真夜中にリセットされます。",
66 | "reset": "次回のリセットは00:00(GMT+7)"
67 | }
68 | },
69 | "achievement": {
70 | "complete": "完了しました",
71 | "achieved": "達成日: {date}"
72 | },
73 | "quest": {
74 | "complete": "完了"
75 | },
76 | "game": {
77 | "mine": "地雷",
78 | "reset-tooltip": "リセット",
79 | "win": "あなたの勝ち!",
80 | "lose": "ゲームオーバー!",
81 | "flag-mode": "フラグモード",
82 | "dig-mode": "ディグモード"
83 | },
84 | "settings": {
85 | "title": "設定",
86 | "description": "自由にカスタマイズ",
87 | "done-button": "近い",
88 | "theme": {
89 | "title": "テーマ",
90 | "system": "システム",
91 | "light": "ライト",
92 | "dark": "ダーク"
93 | },
94 | "language": {
95 | "title": "言語",
96 | "th": "Thai",
97 | "en": "English",
98 | "jp": "日本語",
99 | "vi": "Vietnamese",
100 | "zh": "Chinese"
101 | },
102 | "flag": {
103 | "title": "旗"
104 | },
105 | "flag-color": {
106 | "title": "旗の色"
107 | },
108 | "bomb": {
109 | "title": "爆弾"
110 | },
111 | "number": {
112 | "title": "数字",
113 | "default": "(1) 通常",
114 | "roman": "(I) ローマ数字",
115 | "thai": "(๑) タイ数字",
116 | "abc": "(A) ABC",
117 | "question": "(?) 質問"
118 | }
119 | }
120 | }
--------------------------------------------------------------------------------
/src/locales/th.json:
--------------------------------------------------------------------------------
1 | {
2 | "common": {
3 | "lang": "th",
4 | "game-title": "เกมกู้ระเบิด",
5 | "game-desc": "ขุดหลุมเพื่อความสนุก!",
6 | "loading": "กำลังโหลด..."
7 | },
8 | "not-found": {
9 | "title": "ไม่พบหน้านี้",
10 | "description": "กรุณากลับสู่หน้าแรก",
11 | "loading": "กำลังโหลด...",
12 | "button": "ย้อนกลับ"
13 | },
14 | "difficulty": {
15 | "easy": "ง่าย",
16 | "medium": "ปานกลาง",
17 | "hard": "ยาก"
18 | },
19 | "gameStats": {
20 | "timePlayed": "เวลาที่เล่น",
21 | "flagsPlaced": "จำนวนธง",
22 | "difficulty": "ระดับความยาก"
23 | },
24 | "gameresult": {
25 | "description": "Minesweeper สนุก!",
26 | "win": "คุณชนะ!",
27 | "lose": "จบเกม!",
28 | "time": "เวลา",
29 | "score": "คะแนน",
30 | "difficulty": "ระดับความยาก",
31 | "flag": "จำนวนธง",
32 | "share-button": "แชร์",
33 | "again-button": "เล่นใหม่"
34 | },
35 | "mainmenu": {
36 | "description": "สร้างโดย MeteorVIIx | เวอร์ชั่น {versionGame}",
37 | "settings": "ตั้งค่า",
38 | "singleplayer": {
39 | "title": "เล่นคนเดียว",
40 | "description": "เลือกระดับความยาก",
41 | "easy": "ง่าย",
42 | "medium": "ปานกลาง",
43 | "hard": "ยาก"
44 | },
45 | "multiplayer": {
46 | "title": "เล่นหลายคน",
47 | "description": "เลือกการแข่งขัน",
48 | "casual-title": "จับคู่แบบสบายๆ",
49 | "casual-desc": "เล่นเพื่อความสนุกโดยไม่มีการเปลี่ยนแปลงอันดับ",
50 | "rank-title": "จับคู่แบบจัดอันดับ",
51 | "rank-desc": "แข่งขันเพื่อไต่อันดับ"
52 | },
53 | "store": {
54 | "title": "ร้านค้า"
55 | },
56 | "achievement": {
57 | "title": "ความสำเร็จ",
58 | "description": "ความสำเร็จทั้งหมดแสดงที่นี่",
59 | "menu-all": "ทั้งหมด",
60 | "menu-collected":"ได้รับแล้ว",
61 | "menu-not-collected": "ยังไม่ได้รับ"
62 | },
63 | "quest": {
64 | "title": "ภารกิจ",
65 | "description": "ทำภารกิจประจำวันให้สำเร็จเพื่อรับรางวัล รีเซ็ตเวลาเที่ยงคืน",
66 | "reset": "รีเซ็ตครั้งต่อไปเวลา 00:00 น. (GMT+7)"
67 | }
68 | },
69 | "achievement": {
70 | "complete": "สำเร็จ",
71 | "achieved": "ได้รับเมื่อ: {date}"
72 | },
73 | "quest": {
74 | "complete": "สำเร็จ"
75 | },
76 | "game": {
77 | "mine": "ระเบิด",
78 | "reset-tooltip": "เริ่มใหม่",
79 | "win": "คุณชนะ!",
80 | "lose": "จบเกม!",
81 | "flag-mode": "โหมดปักธง",
82 | "dig-mode": "โหมดขุด"
83 | },
84 | "settings": {
85 | "title": "การตั้งค่า",
86 | "description": "ปรับแต่งได้อย่างอิสระ",
87 | "done-button": "ปิด",
88 | "theme": {
89 | "title": "ธีม",
90 | "system": "ระบบ",
91 | "light": "สว่าง",
92 | "dark": "มืด"
93 | },
94 | "language": {
95 | "title": "ภาษา",
96 | "th": "ไทย",
97 | "en": "English",
98 | "jp": "Japanese",
99 | "vi": "Vietnamese",
100 | "zh": "Chinese"
101 | },
102 | "flag": {
103 | "title": "ธง"
104 | },
105 | "flag-color": {
106 | "title": "สีธง"
107 | },
108 | "bomb": {
109 | "title": "ระเบิด"
110 | },
111 | "number": {
112 | "title": "ตัวเลข",
113 | "default": "(1) ปกติ",
114 | "roman": "(I) โรมัน",
115 | "thai": "(๑) ไทย",
116 | "abc": "(A) ABC",
117 | "question": "(?) คำถาม"
118 | }
119 | }
120 | }
--------------------------------------------------------------------------------
/src/locales/vi.json:
--------------------------------------------------------------------------------
1 | {
2 | "common": {
3 | "lang": "vi",
4 | "game-title": "Dò Mìn",
5 | "game-desc": "Đào hố cho vui!",
6 | "loading": "Đang tải..."
7 | },
8 | "not-found": {
9 | "title": "Không tìm thấy",
10 | "description": "Vui lòng quay lại trang chủ.",
11 | "loading": "Đang tải...",
12 | "button": "Mặt sau"
13 | },
14 | "difficulty": {
15 | "easy": "Dễ",
16 | "medium": "Trung bình",
17 | "hard": "Khó"
18 | },
19 | "gameStats": {
20 | "timePlayed": "Thời gian chơi",
21 | "flagsPlaced": "Cờ đã đặt",
22 | "difficulty": "Độ khó"
23 | },
24 | "gameresult": {
25 | "description": "Minesweeper thật thú vị!",
26 | "win": "Bạn đã thắng!",
27 | "lose": "Trò chơi kết thúc!",
28 | "time": "Thời gian",
29 | "score": "Điểm số",
30 | "difficulty": "Độ khó",
31 | "flag": "Cờ đã đặt",
32 | "share-button": "Chia sẻ",
33 | "again-button": "Chơi lại"
34 | },
35 | "mainmenu": {
36 | "description": "Tạo bởi MeteorVIIx | Phiên bản {versionGame}",
37 | "settings": "Cài đặt",
38 | "singleplayer": {
39 | "title": "Chơi đơn",
40 | "description": "Chọn độ khó",
41 | "easy": "Dễ",
42 | "medium": "Trung bình",
43 | "hard": "Khó"
44 | },
45 | "multiplayer": {
46 | "title": "Nhiều người chơi",
47 | "description": "Chọn một cuộc thi",
48 | "casual-title": "Kết hợp thông thường",
49 | "casual-desc": "Chơi vui mà không thay đổi thứ hạng",
50 | "rank-title": "Kết hợp xếp hạng",
51 | "rank-desc": "Cạnh tranh để leo lên bảng xếp hạng"
52 | },
53 | "store": {
54 | "title": "Cửa hàng"
55 | },
56 | "achievement": {
57 | "title": "Thành tích",
58 | "description": "Tất cả thành tích được hiển thị ở đây",
59 | "menu-all": "Tất cả",
60 | "menu-collected":"Đã thu thập",
61 | "menu-not-collected": "Chưa thu thập"
62 | },
63 | "quest": {
64 | "title": "Các nhiệm vụ",
65 | "description": "Hoàn thành nhiệm vụ hàng ngày để kiếm phần thưởng. Đặt lại vào lúc nửa đêm.",
66 | "reset": "Lần đặt lại tiếp theo lúc 00:00 (GMT+7)"
67 | }
68 | },
69 | "achievement": {
70 | "complete": "Hoàn thành",
71 | "achieved": "Đạt được vào ngày: {date}"
72 | },
73 | "quest": {
74 | "complete": "Hoàn thành"
75 | },
76 | "game": {
77 | "mine": "Mìn",
78 | "reset-tooltip": "Chơi lại",
79 | "win": "Bạn đã thắng!",
80 | "lose": "Trò chơi kết thúc!",
81 | "flag-mode": "Chế độ gắn cờ",
82 | "dig-mode": "Chế độ đào"
83 | },
84 | "settings": {
85 | "title": "Cài đặt",
86 | "description": "Tự do tùy chỉnh",
87 | "done-button": "xong",
88 | "theme": {
89 | "title": "Giao diện",
90 | "system": "Hệ thống",
91 | "light": "Sáng",
92 | "dark": "Tối"
93 | },
94 | "language": {
95 | "title": "Ngôn ngữ",
96 | "th": "Thai",
97 | "en": "English",
98 | "jp": "Japanese",
99 | "vi": "Tiếng Việt",
100 | "zh": "Chinese"
101 | },
102 | "flag": {
103 | "title": "Cờ"
104 | },
105 | "flag-color": {
106 | "title": "Màu cờ"
107 | },
108 | "bomb": {
109 | "title": "Mìn"
110 | },
111 | "number": {
112 | "title": "Số",
113 | "default": "(1) Thường",
114 | "roman": "(I) La Mã",
115 | "thai": "(๑) Thái",
116 | "abc": "(A) ABC",
117 | "question": "(?) Câu hỏi"
118 | }
119 | }
120 | }
--------------------------------------------------------------------------------
/src/locales/zh.json:
--------------------------------------------------------------------------------
1 | {
2 | "common": {
3 | "lang": "zh",
4 | "game-title": "扫雷",
5 | "game-desc": "挖洞真好玩!",
6 | "loading": "載入中..."
7 | },
8 | "not-found": {
9 | "title": "未找到",
10 | "description": "請返回主頁。",
11 | "loading": "載入中...",
12 | "button": "後退"
13 | },
14 | "difficulty": {
15 | "easy": "简单",
16 | "medium": "中等",
17 | "hard": "困难"
18 | },
19 | "gameStats": {
20 | "timePlayed": "游戏时间",
21 | "flagsPlaced": "已插旗数",
22 | "difficulty": "难度"
23 | },
24 | "gameresult": {
25 | "description": "Minesweeper 很好玩!",
26 | "win": "胜利!",
27 | "lose": "游戏结束!",
28 | "time": "时间",
29 | "score": "得分",
30 | "difficulty": "难度",
31 | "flag": "已插旗数",
32 | "share-button": "分享",
33 | "again-button": "再玩一次"
34 | },
35 | "mainmenu": {
36 | "description": "由 MeteorVIIx 制作 | 版本 {versionGame}",
37 | "settings": "设置",
38 | "singleplayer": {
39 | "title": "單人遊戲",
40 | "description": "選擇難度",
41 | "easy": "简单",
42 | "medium": "中等",
43 | "hard": "困难"
44 | },
45 | "multiplayer": {
46 | "title": "多人遊戲",
47 | "description": "選擇比賽",
48 | "casual-title": "休閒匹配",
49 | "casual-desc": "享受樂趣,不改變排名",
50 | "rank-title": "排名匹配",
51 | "rank-desc": "競爭攀升排行榜"
52 | },
53 | "store": {
54 | "title": "商店"
55 | },
56 | "achievement": {
57 | "title": "成就",
58 | "description": "所有成就均顯示於此",
59 | "menu-all": "全部",
60 | "menu-collected": "已收藏",
61 | "menu-not-collected": "未收藏"
62 | },
63 | "quest": {
64 | "title": "任務",
65 | "description": "完成日常任務以獲得獎勵。午夜重置。",
66 | "reset": "下次重置時間為 00:00 (GMT+7)"
67 | }
68 | },
69 | "achievement": {
70 | "complete": "已完成",
71 | "achieved": "於完成: {date}"
72 | },
73 | "quest": {
74 | "complete": "完全的"
75 | },
76 | "game": {
77 | "mine": "地雷",
78 | "reset-tooltip": "重新开始",
79 | "win": "胜利!",
80 | "lose": "游戏结束!",
81 | "flag-mode": "标志模式",
82 | "dig-mode": "挖掘模式"
83 | },
84 | "settings": {
85 | "title": "设置",
86 | "description": "自由訂製",
87 | "done-button": "完畢",
88 | "theme": {
89 | "title": "主题",
90 | "system": "系统",
91 | "light": "浅色",
92 | "dark": "深色"
93 | },
94 | "language": {
95 | "title": "语言",
96 | "th": "Thai",
97 | "en": "English",
98 | "jp": "Japanese",
99 | "vi": "Vietnamese",
100 | "zh": "中文"
101 | },
102 | "flag": {
103 | "title": "旗帜"
104 | },
105 | "flag-color": {
106 | "title": "旗帜颜色"
107 | },
108 | "bomb": {
109 | "title": "地雷"
110 | },
111 | "number": {
112 | "title": "数字",
113 | "default": "(1) 普通",
114 | "roman": "(I) 罗马",
115 | "thai": "(๑) 泰文",
116 | "abc": "(A) ABC",
117 | "question": "(?) 问题"
118 | }
119 | }
120 | }
--------------------------------------------------------------------------------
/src/providers/FontProvider.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 | import React, { createContext, useContext, useEffect } from 'react';
3 | import { useLanguageStore } from '@/stores/languageStore';
4 | import {
5 | Geist,
6 | Geist_Mono,
7 | Noto_Sans_Thai,
8 | Noto_Sans_JP,
9 | Noto_Sans_KR,
10 | Noto_Sans_SC,
11 | Be_Vietnam_Pro
12 | } from 'next/font/google';
13 |
14 | const geistSans = Geist({
15 | variable: '--font-geist-sans',
16 | subsets: ['latin'],
17 | display: 'swap',
18 | });
19 |
20 | const geistMono = Geist_Mono({
21 | variable: '--font-geist-mono',
22 | subsets: ['latin'],
23 | display: 'swap'
24 | });
25 |
26 | const notoSansThai = Noto_Sans_Thai({
27 | variable: '--font-thai',
28 | weight: ['400', '500', '600', '700'],
29 | subsets: ['thai'],
30 | display: 'swap'
31 | });
32 |
33 | const notoSansJP = Noto_Sans_JP({
34 | variable: '--font-japanese',
35 | weight: ['400', '500', '700'],
36 | subsets: ['latin'],
37 | display: 'swap'
38 | });
39 |
40 | const notoSansKR = Noto_Sans_KR({
41 | variable: '--font-korean',
42 | weight: ['400', '500', '700'],
43 | subsets: ['latin'],
44 | display: 'swap'
45 | });
46 |
47 | const notoSansSC = Noto_Sans_SC({
48 | variable: '--font-chinese',
49 | weight: ['400', '500', '700'],
50 | subsets: ['latin'],
51 | display: 'swap'
52 | });
53 |
54 | const beVietnamPro = Be_Vietnam_Pro({
55 | variable: '--font-vietnamese',
56 | weight: ['400', '500', '700'],
57 | subsets: ['latin'],
58 | display: 'swap'
59 | });
60 |
61 | const FontContext = createContext<{ fontClass: string }>({ fontClass: '' });
62 |
63 | export const useFontContext = () => useContext(FontContext);
64 |
65 | export const FontProvider = ({ children }: { children: React.ReactNode }) => {
66 | const { lang } = useLanguageStore();
67 |
68 | const getFontClassForLocale = () => {
69 | switch (lang) {
70 | case 'th':
71 | return `${geistSans.variable} ${geistMono.variable} ${notoSansThai.variable} font-thai`;
72 | case 'jp':
73 | return `${geistSans.variable} ${geistMono.variable} ${notoSansJP.variable} font-japanese`;
74 | case 'ko':
75 | return `${geistSans.variable} ${geistMono.variable} ${notoSansKR.variable} font-korean`;
76 | case 'zh':
77 | return `${geistSans.variable} ${geistMono.variable} ${notoSansSC.variable} font-chinese`;
78 | case 'vi':
79 | return `${geistSans.variable} ${geistMono.variable} ${beVietnamPro.variable} font-vietnamese`;
80 | default:
81 | return `${geistSans.variable} ${geistMono.variable} font-sans`;
82 | }
83 | };
84 |
85 | const fontClass = getFontClassForLocale();
86 |
87 | useEffect(() => {
88 | if (typeof document !== 'undefined') {
89 | document.documentElement.className = '';
90 | document.documentElement.classList.add('antialiased', ...fontClass.split(' '));
91 | }
92 | }, [fontClass]);
93 |
94 | return (
95 |
96 | {children}
97 |
98 | )
99 | };
100 |
101 | export const allFonts = [
102 | geistSans.variable,
103 | geistMono.variable,
104 | notoSansThai.variable,
105 | notoSansJP.variable,
106 | notoSansKR.variable,
107 | notoSansSC.variable,
108 | beVietnamPro.variable
109 | ];
--------------------------------------------------------------------------------
/src/providers/LanguageProvider.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 | import { useEffect } from 'react';
3 | import { useLanguageStore } from '@/stores/languageStore';
4 |
5 | const LanguageProvider = () => {
6 | const { lang } = useLanguageStore();
7 |
8 | useEffect(() => {
9 | document.documentElement.lang = lang;
10 | }, [lang]);
11 |
12 | return null;
13 | };
14 |
15 | export { LanguageProvider };
--------------------------------------------------------------------------------
/src/providers/SolanaProvider.tsx:
--------------------------------------------------------------------------------
1 | 'use client'
2 | import React, { useEffect, useMemo, useCallback } from 'react';
3 | import { PhantomWalletAdapter, SolflareWalletAdapter, TorusWalletAdapter, LedgerWalletAdapter } from '@solana/wallet-adapter-wallets';
4 | import { WalletAdapterNetwork } from '@solana/wallet-adapter-base';
5 | import { ConnectionProvider, WalletProvider, useWallet } from '@solana/wallet-adapter-react';
6 | import { WalletModalProvider } from '@solana/wallet-adapter-react-ui';
7 | import { clusterApiUrl } from '@solana/web3.js';
8 | import { useWalletStore } from '@/stores';
9 |
10 | const networkRPC = process.env.NETWORK_RPC as 'mainnet' | 'testnet' | 'devnet';
11 |
12 | export interface SolanaProviderProps {
13 | children: React.ReactNode;
14 | };
15 |
16 | const WalletStateUpdater = () => {
17 | const { publicKey, connected, connecting } = useWallet();
18 | const { setConnected, setPublicKey, setConnecting } = useWalletStore();
19 |
20 | const publicKeyString = useMemo(() => publicKey?.toString() || null, [publicKey]);
21 |
22 | const updateWalletState = useCallback(() => {
23 | setConnected(connected);
24 |
25 | if (publicKeyString !== useWalletStore.getState().publicKey) {
26 | setPublicKey(publicKeyString);
27 | }
28 |
29 | if (connecting !== useWalletStore.getState().connecting) {
30 | setConnecting(connecting);
31 | }
32 | }, [connected, publicKeyString, connecting, setConnected, setPublicKey, setConnecting]);
33 |
34 | useEffect(() => {
35 | updateWalletState();
36 | }, [updateWalletState]);
37 |
38 | return null;
39 | };
40 |
41 | const SolanaProvider = ({ children }: SolanaProviderProps) => {
42 | const network = useMemo(() => {
43 | switch(networkRPC) {
44 | case 'mainnet':
45 | return WalletAdapterNetwork.Mainnet;
46 | case 'testnet':
47 | return WalletAdapterNetwork.Testnet;
48 | case 'devnet':
49 | default:
50 | return WalletAdapterNetwork.Devnet;
51 | }
52 | }, []);
53 |
54 | const endpoint = useMemo(() => clusterApiUrl(network), [network]);
55 |
56 | const wallets = useMemo(() => [
57 | new PhantomWalletAdapter(),
58 | new SolflareWalletAdapter(),
59 | new TorusWalletAdapter(),
60 | new LedgerWalletAdapter()
61 | ], []);
62 |
63 | const walletConfig = useMemo(() => ({
64 | wallets,
65 | autoConnect: true
66 | }), [wallets]);
67 |
68 | return (
69 |
70 |
71 |
72 |
73 | {children}
74 |
75 |
76 |
77 | )
78 | };
79 |
80 | export default SolanaProvider;
--------------------------------------------------------------------------------
/src/providers/ThemeProvider.tsx:
--------------------------------------------------------------------------------
1 | 'use client'
2 | import { useEffect, useState } from 'react';
3 | import { ThemeProvider as NextThemeProvider } from 'next-themes';
4 |
5 | const ThemeProvider = ({ children }: { children: React.ReactNode }) => {
6 | const [ mounted, setMounted ] = useState(false);
7 |
8 | useEffect(() => {
9 | setMounted(true);
10 | }, []);
11 |
12 | if (!mounted) {
13 | return (
14 | <>
15 | {children}
16 | >
17 | )
18 | };
19 |
20 | return (
21 |
26 | {children}
27 |
28 | );
29 | }
30 |
31 | export { ThemeProvider };
--------------------------------------------------------------------------------
/src/providers/Web3Provider.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 | import dynamic from 'next/dynamic';
3 | import React, { useEffect, useState } from 'react';
4 | import { useTheme } from 'next-themes';
5 |
6 | const SolanaProvider = dynamic(() => import('@/providers/SolanaProvider'), { ssr: false });
7 |
8 | const Web3Provider = ({ children }: { children: React.ReactNode }) => {
9 | const { theme } = useTheme();
10 | const [ isTauri, setIsTauri ] = useState(false);
11 | const [ isClient, setIsClient ] = useState(false);
12 |
13 | useEffect(() => {
14 | setIsClient(true);
15 | const checkTauri = () => typeof window !== 'undefined' && 'TAURI' in window;
16 | setIsTauri(checkTauri());
17 | document.documentElement.setAttribute('data-theme', theme || 'dark');
18 | }, [theme]);
19 |
20 | if (!isClient || isTauri) {
21 | return <>{children}>;
22 | }
23 |
24 | return {children};
25 | };
26 |
27 | export { Web3Provider };
--------------------------------------------------------------------------------
/src/stores/commonStore.ts:
--------------------------------------------------------------------------------
1 | import { create } from 'zustand';
2 | import { CommonStateProps } from '@/types';
3 |
4 | export const useCommonStore = create((set) => ({
5 | isMenuQuestOpen: false,
6 | isMenuAchievementOpen: false,
7 | isMenuSettingOpen: false,
8 | isMenuSingleplayerOpen: false,
9 | isMenuMultiplayerOpen: false,
10 | setIsMenuQuestOpen: (isMenuQuestOpen) => set({ isMenuQuestOpen }),
11 | setIsMenuAchievementOpen: (isMenuAchievementOpen) => set({ isMenuAchievementOpen }),
12 | setIsMenuSettingOpen: (isMenuSettingOpen) => set({ isMenuSettingOpen }),
13 | setIsMenuSingleplayerOpen: (isMenuSingleplayerOpen) => set({ isMenuSingleplayerOpen }),
14 | setIsMenuMultiplayerOpen: (isMenuMultiplayerOpen) => set({ isMenuMultiplayerOpen })
15 | }));
--------------------------------------------------------------------------------
/src/stores/gameStore.ts:
--------------------------------------------------------------------------------
1 | import { create } from 'zustand';
2 | import { GameStateProps } from '@/types';
3 |
4 | export const useGameStore = create((set) => ({
5 | isStartGame: false,
6 | isGameOver: false,
7 | isGameWon: false,
8 | isShowResult: false,
9 | board: [],
10 | difficulty: 'easy',
11 | isFlagMode: false,
12 | flagsPlaced: 0,
13 | time: 0,
14 | score: 0,
15 | setIsStartGame: (isStartGame) => set({ isStartGame }),
16 | setIsGameOver: (isGameOver) => set({ isGameOver }),
17 | setIsGameWon: (isGameWon) => set({ isGameWon }),
18 | setIsShowResult: (isShowResult) => set({ isShowResult }),
19 | setBoard: (board) => set({ board }),
20 | setDifficulty: (difficulty) => set({ difficulty }),
21 | setIsFlagMode: (isFlagMode) => set({ isFlagMode }),
22 | setFlagsPlaced: (flagsPlaced) => set({ flagsPlaced }),
23 | setTime: (time) => set({ time }),
24 | setScore: (score) => set({ score })
25 | }));
--------------------------------------------------------------------------------
/src/stores/index.ts:
--------------------------------------------------------------------------------
1 | import { useCommonStore } from './commonStore';
2 | import { useGameStore } from './gameStore';
3 | import { useLanguageStore } from './languageStore';
4 | import { useSettingStore } from './settingStore';
5 | import { useWalletStore } from './walletStore';
6 |
7 | export {
8 | useCommonStore,
9 | useGameStore,
10 | useLanguageStore,
11 | useSettingStore,
12 | useWalletStore
13 | };
--------------------------------------------------------------------------------
/src/stores/languageStore.ts:
--------------------------------------------------------------------------------
1 | import { create } from 'zustand';
2 | import { persist } from 'zustand/middleware';
3 | import { LanguageStateProps } from '@/types';
4 |
5 | export const useLanguageStore = create()(
6 | persist(
7 | (set) => ({
8 | lang: 'en',
9 | setLang: (lang) => set({ lang })
10 | }),
11 | { name: 'language-store' }
12 | )
13 | );
--------------------------------------------------------------------------------
/src/stores/settingStore.ts:
--------------------------------------------------------------------------------
1 | import { create } from 'zustand';
2 | import { persist } from 'zustand/middleware';
3 | import { SettingStateProps } from '@/types';
4 |
5 | export const useSettingStore = create()(
6 | persist(
7 | (set) => ({
8 | flagIcon: 'default',
9 | flagColor: '#FF0000',
10 | bombIcon: 'default',
11 | numberStyle: 'default',
12 | setFlagIcon: (flagIcon) => set({ flagIcon }),
13 | setFlagColor: (flagColor) => set({ flagColor }),
14 | setBombIcon: (bombIcon) => set({ bombIcon }),
15 | setNumberStyle: (numberStyle) => set({ numberStyle }),
16 | }),
17 | {
18 | name: 'setting-storage',
19 | partialize: (state) => ({
20 | flagIcon: state.flagIcon,
21 | flagColor: state.flagColor,
22 | bombIcon: state.bombIcon,
23 | numberStyle: state.numberStyle
24 | }),
25 | }
26 | )
27 | );
--------------------------------------------------------------------------------
/src/stores/walletStore.ts:
--------------------------------------------------------------------------------
1 | import { create } from 'zustand';
2 | import { persist } from 'zustand/middleware';
3 |
4 | interface WalletState {
5 | connected: boolean;
6 | publicKey: string | null;
7 | connecting: boolean;
8 |
9 | setConnected: (status: boolean) => void;
10 | setPublicKey: (key: string | null) => void;
11 | setConnecting: (status: boolean) => void;
12 | };
13 |
14 | export const useWalletStore = create()(
15 | persist(
16 | (set) => ({
17 | connected: false,
18 | publicKey: null,
19 | connecting: false,
20 |
21 | setConnected: (status) => set({ connected: status }),
22 | setPublicKey: (key) => set({ publicKey: key }),
23 | setConnecting: (status) => set({ connecting: status })
24 | }),
25 | {
26 | name: 'wallet-storage',
27 | partialize: (state) => ({
28 | connected: state.connected,
29 | publicKey: state.publicKey
30 | }),
31 | }
32 | )
33 | );
--------------------------------------------------------------------------------
/src/types/css.d.ts:
--------------------------------------------------------------------------------
1 | declare module '*.css' {
2 | const content: { [className: string]: string };
3 | export default content;
4 | }
--------------------------------------------------------------------------------
/src/types/index.d.ts:
--------------------------------------------------------------------------------
1 | import { CommonStateProps, GameStateProps, SettingStateProps } from './store';
2 |
3 | export { CommonStateProps, GameStateProps, SettingStateProps };
4 |
5 | export type Language = 'en' | 'th' | 'jp' | 'vi' | 'zh';
6 | export type FlagStyle = 'default' | 'pyramid' | 'radar' | 'sparkles' | 'sigma';
7 | export type BombStyle = 'default' | 'skull' | 'fire' | 'flame' | 'ghost';
8 | export type NumberStyle = 'default' | 'roman' | 'thai' | 'abc' | 'question';
9 | export type Difficulty = 'easy' | 'medium' | 'hard';
10 |
11 | export type TranslationValue = string | { [key: string]: TranslationValue };
12 |
13 | export type Variables = {
14 | [key: string]: string | number | ReactNode
15 | };
16 | export type Translation = {
17 | [key: string]: TranslationValue
18 | };
19 |
20 | export type TranslationsType = {
21 | [K in Language]: {
22 | [key: string]: TranslationValue;
23 | };
24 | };
25 |
26 | export interface LanguageStateProps {
27 | lang: string;
28 | setLang: (lang: string) => void;
29 | };
30 |
31 | export interface GameStats {
32 | time: number;
33 | difficulty: Difficulty;
34 | flagsPlaced: number;
35 | score: number;
36 | };
37 |
38 | export interface Cell {
39 | isMine: boolean;
40 | isRevealed: boolean;
41 | isFlagged: boolean;
42 | neighborMines: number;
43 | };
44 |
--------------------------------------------------------------------------------
/src/types/store.d.ts:
--------------------------------------------------------------------------------
1 | export interface CommonStateProps {
2 | isMenuQuestOpen: boolean;
3 | isMenuAchievementOpen: boolean;
4 | isMenuSettingOpen: boolean;
5 | isMenuSingleplayerOpen: boolean;
6 | isMenuMultiplayerOpen: boolean;
7 | setIsMenuQuestOpen: (isMenuQuestOpen) => void;
8 | setIsMenuAchievementOpen: (isAchievementOpen) => void;
9 | setIsMenuSettingOpen: (isMenuSettingOpen) => void;
10 | setIsMenuSingleplayerOpen: (isMenuSingleplayerOpen) => void;
11 | setIsMenuMultiplayerOpen: (isMenuMultiplayerOpen) => void;
12 | };
13 | export interface GameStateProps {
14 | isStartGame: boolean;
15 | isGameOver: boolean;
16 | isGameWon: boolean;
17 | isShowResult: boolean;
18 | board: Cell[][];
19 | difficulty: Difficulty;
20 | isFlagMode: boolean;
21 | flagsPlaced: number;
22 | time: number;
23 | score: number;
24 | setIsStartGame: (isStartGame) => void;
25 | setIsGameOver: (isGameOver) => void;
26 | setIsGameWon: (isGameWon) => void;
27 | setIsShowResult: (isShowResult) => void;
28 | setBoard: (board) => void;
29 | setDifficulty: (difficulty) => void;
30 | setIsFlagMode: (isFlagMode) => void;
31 | setFlagsPlaced: (flagsPlaced) => void;
32 | setTime: (time) => void;
33 | setScore: (score) => void;
34 | };
35 |
36 | export interface SettingStateProps {
37 | flagIcon: FlagStyle;
38 | flagColor: string;
39 | bombIcon: BombStyle;
40 | numberStyle: NumberStyle;
41 | setFlagIcon: (flagIcon) => void;
42 | setFlagColor: (flagColor) => void;
43 | setBombIcon: (bombIcon) => void;
44 | setNumberStyle: (numberStyle) => void;
45 | };
--------------------------------------------------------------------------------
/src/utils/discord-webhook.ts:
--------------------------------------------------------------------------------
1 | interface DataProps {
2 | publicKey: string | null;
3 | imageBlob: Blob;
4 | };
5 |
6 | export const sendWebhookDiscordShare = async (data: DataProps) => {
7 | try {
8 | const formData = new FormData();
9 | formData.append('publicKey', data.publicKey || 'Anonymous Player');
10 | formData.append('file', data.imageBlob, 'minesweeper-score.png');
11 |
12 | const response = await fetch('/api/discord-webhook', {
13 | method: 'POST',
14 | body: formData
15 | });
16 |
17 | if (!response.ok) {
18 | throw new Error(`Server responded with ${response.status}`);
19 | }
20 |
21 | return await response.json();
22 | } catch (error) {
23 | console.error('Error calling webhook API:', error);
24 | throw error;
25 | }
26 | };
--------------------------------------------------------------------------------
/tailwind.config.ts:
--------------------------------------------------------------------------------
1 | import type { Config } from 'tailwindcss';
2 |
3 | export default {
4 | content: [
5 | './src/pages/**/*.{js,ts,jsx,tsx,mdx}',
6 | './src/components/**/*.{js,ts,jsx,tsx,mdx}',
7 | './src/app/**/*.{js,ts,jsx,tsx,mdx}',
8 | ],
9 | theme: {
10 | extend: {
11 | colors: {
12 | background: 'hsl(var(--background))',
13 | foreground: 'hsl(var(--foreground))',
14 | card: {
15 | DEFAULT: 'hsl(var(--card))',
16 | foreground: 'hsl(var(--card-foreground))'
17 | },
18 | popover: {
19 | DEFAULT: 'hsl(var(--popover))',
20 | foreground: 'hsl(var(--popover-foreground))'
21 | },
22 | primary: {
23 | DEFAULT: 'hsl(var(--primary))',
24 | foreground: 'hsl(var(--primary-foreground))'
25 | },
26 | secondary: {
27 | DEFAULT: 'hsl(var(--secondary))',
28 | foreground: 'hsl(var(--secondary-foreground))'
29 | },
30 | muted: {
31 | DEFAULT: 'hsl(var(--muted))',
32 | foreground: 'hsl(var(--muted-foreground))'
33 | },
34 | accent: {
35 | DEFAULT: 'hsl(var(--accent))',
36 | foreground: 'hsl(var(--accent-foreground))'
37 | },
38 | destructive: {
39 | DEFAULT: 'hsl(var(--destructive))',
40 | foreground: 'hsl(var(--destructive-foreground))'
41 | },
42 | border: 'hsl(var(--border))',
43 | input: 'hsl(var(--input))',
44 | ring: 'hsl(var(--ring))',
45 | chart: {
46 | '1': 'hsl(var(--chart-1))',
47 | '2': 'hsl(var(--chart-2))',
48 | '3': 'hsl(var(--chart-3))',
49 | '4': 'hsl(var(--chart-4))',
50 | '5': 'hsl(var(--chart-5))'
51 | }
52 | },
53 | borderRadius: {
54 | lg: 'var(--radius)',
55 | md: 'calc(var(--radius) - 2px)',
56 | sm: 'calc(var(--radius) - 4px)'
57 | },
58 | animation: {
59 | 'shimmer-slide': 'shimmer-slide var(--speed) ease-in-out infinite alternate',
60 | 'spin-around': 'spin-around calc(var(--speed) * 2) infinite linear'
61 | },
62 | keyframes: {
63 | 'shimmer-slide': {
64 | to: {
65 | transform: 'translate(calc(100cqw - 100%), 0)'
66 | }
67 | },
68 | 'spin-around': {
69 | '0%': {
70 | transform: 'translateZ(0) rotate(0)'
71 | },
72 | '15%, 35%': {
73 | transform: 'translateZ(0) rotate(90deg)'
74 | },
75 | '65%, 85%': {
76 | transform: 'translateZ(0) rotate(270deg)'
77 | },
78 | '100%': {
79 | transform: 'translateZ(0) rotate(360deg)'
80 | }
81 | }
82 | }
83 | }
84 | },
85 | plugins: [],
86 | } satisfies Config;
87 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "ES2017",
4 | "lib": ["dom", "dom.iterable", "esnext"],
5 | "allowJs": true,
6 | "skipLibCheck": true,
7 | "strict": true,
8 | "noEmit": true,
9 | "esModuleInterop": true,
10 | "module": "esnext",
11 | "moduleResolution": "bundler",
12 | "resolveJsonModule": true,
13 | "isolatedModules": true,
14 | "jsx": "preserve",
15 | "incremental": true,
16 | "plugins": [
17 | {
18 | "name": "next"
19 | }
20 | ],
21 | "paths": {
22 | "@/*": ["./src/*"]
23 | }
24 | },
25 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
26 | "exclude": ["node_modules"]
27 | }
28 |
--------------------------------------------------------------------------------