├── .github └── workflows │ ├── build-android.yml │ └── build.yml ├── .gitignore ├── .vscode ├── extensions.json └── settings.json ├── LICENSE ├── README.md ├── eslint.config.js ├── img ├── check_mate_logic.gif ├── chess-ui-1.png ├── colour-picker.gif ├── enpassant-promote-castle.gif ├── move_logic.gif ├── move_logic2.gif ├── take_logic.gif ├── tauri-chess-android.gif ├── tauri-chess-android.webm └── turns_logic.gif ├── index.html ├── package.json ├── prettier.config.cjs ├── src-tauri ├── .gitignore ├── Cargo.lock ├── Cargo.toml ├── build.rs ├── capabilities │ └── default.json ├── gen │ └── android │ │ ├── .editorconfig │ │ ├── .gitignore │ │ ├── app │ │ ├── .gitignore │ │ ├── build.gradle.kts │ │ ├── proguard-rules.pro │ │ └── src │ │ │ └── main │ │ │ ├── AndroidManifest.xml │ │ │ ├── java │ │ │ └── com │ │ │ │ └── chess │ │ │ │ └── app │ │ │ │ └── MainActivity.kt │ │ │ └── res │ │ │ ├── drawable-v24 │ │ │ └── ic_launcher_foreground.xml │ │ │ ├── drawable │ │ │ └── ic_launcher_background.xml │ │ │ ├── layout │ │ │ └── activity_main.xml │ │ │ ├── mipmap-hdpi │ │ │ ├── ic_launcher.png │ │ │ ├── ic_launcher_foreground.png │ │ │ └── ic_launcher_round.png │ │ │ ├── mipmap-mdpi │ │ │ ├── ic_launcher.png │ │ │ ├── ic_launcher_foreground.png │ │ │ └── ic_launcher_round.png │ │ │ ├── mipmap-xhdpi │ │ │ ├── ic_launcher.png │ │ │ ├── ic_launcher_foreground.png │ │ │ └── ic_launcher_round.png │ │ │ ├── mipmap-xxhdpi │ │ │ ├── ic_launcher.png │ │ │ ├── ic_launcher_foreground.png │ │ │ └── ic_launcher_round.png │ │ │ ├── mipmap-xxxhdpi │ │ │ ├── ic_launcher.png │ │ │ ├── ic_launcher_foreground.png │ │ │ └── ic_launcher_round.png │ │ │ ├── values-night │ │ │ └── themes.xml │ │ │ ├── values │ │ │ ├── colors.xml │ │ │ ├── strings.xml │ │ │ └── themes.xml │ │ │ └── xml │ │ │ └── file_paths.xml │ │ ├── build.gradle.kts │ │ ├── buildSrc │ │ ├── build.gradle.kts │ │ └── src │ │ │ └── main │ │ │ └── java │ │ │ └── com │ │ │ └── chess │ │ │ └── app │ │ │ └── kotlin │ │ │ ├── BuildTask.kt │ │ │ └── RustPlugin.kt │ │ ├── gradle.properties │ │ ├── gradle │ │ └── wrapper │ │ │ ├── gradle-wrapper.jar │ │ │ └── gradle-wrapper.properties │ │ ├── gradlew │ │ ├── gradlew.bat │ │ └── settings.gradle ├── 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 │ ├── chess │ │ ├── api.rs │ │ ├── board.rs │ │ ├── data.rs │ │ ├── mod.rs │ │ ├── moves.rs │ │ ├── pieces.rs │ │ ├── types.rs │ │ ├── unit_tests.rs │ │ └── utils.rs │ ├── lib.rs │ └── main.rs └── tauri.conf.json ├── src ├── assets │ ├── favicon.svg │ ├── logo.svg │ └── tauri.svg ├── components │ ├── Elements │ │ ├── LICENCE.md │ │ ├── README.md │ │ ├── button │ │ │ └── button.tsx │ │ ├── drawer │ │ │ └── drawer.tsx │ │ ├── hamburger-menu │ │ │ └── hamburgerMenu.tsx │ │ ├── icon │ │ │ └── icon.tsx │ │ ├── index.ts │ │ ├── modal │ │ │ └── modal.tsx │ │ ├── navbar │ │ │ └── navbar.tsx │ │ └── sidenav │ │ │ └── sidenav.tsx │ ├── Features │ │ ├── Chessboard │ │ │ ├── Board.tsx │ │ │ ├── BoardSquare.tsx │ │ │ ├── CustomDragLayer.tsx │ │ │ ├── Notation.tsx │ │ │ ├── Piece.tsx │ │ │ ├── helpers.ts │ │ │ ├── index.tsx │ │ │ ├── readme.md │ │ │ └── svg │ │ │ │ ├── chesspieces │ │ │ │ └── standard.tsx │ │ │ │ └── whiteKing.tsx │ │ └── chess │ │ │ ├── index.tsx │ │ │ └── promotions.tsx │ ├── Layout │ │ ├── Footer │ │ │ ├── Copywrite.tsx │ │ │ └── Footer.tsx │ │ ├── Header │ │ │ └── Header.tsx │ │ ├── Layout.tsx │ │ ├── Logo.tsx │ │ ├── Sidebar │ │ │ ├── items.tsx │ │ │ └── sideNavbar.tsx │ │ ├── alert.tsx │ │ └── helpers │ │ │ ├── context.tsx │ │ │ └── overlay.tsx │ └── index.ts ├── hooks │ ├── index.ts │ ├── useEventListener.ts │ ├── useOnClickOutside.ts │ ├── useScrollDirection.ts │ └── useToggle.ts ├── main.tsx ├── pages │ ├── AboutPage.tsx │ ├── HomePage.tsx │ ├── SummaryPage.tsx │ ├── TestPage.tsx │ └── index.ts ├── routes │ └── index.tsx ├── services │ └── notifications.ts ├── style │ ├── global.css │ └── tailwind.css ├── types │ ├── chessboard.ts │ └── index.ts ├── utils │ └── index.ts └── vite-env.d.ts ├── tailwind.config.js ├── tsconfig.json ├── tsconfig.node.json └── vite.config.ts /.github/workflows/build-android.yml: -------------------------------------------------------------------------------- 1 | name: 'publish android' 2 | 3 | on: 4 | pull_request: 5 | branches: 6 | - main 7 | types: 8 | - closed 9 | 10 | env: 11 | APP_NAME: 'tauri-chess' 12 | GITHUB_TOKEN: ${{ secrets.GH_PAT }} 13 | 14 | jobs: 15 | publish-android: 16 | if: ${{ github.event.pull_request.merged }} && github.repository_owner == 'jamessizeland' # This is to ensure that only the owner can publish the app. 17 | permissions: 18 | contents: write 19 | strategy: 20 | fail-fast: true 21 | runs-on: ubuntu-latest 22 | steps: 23 | - uses: actions/checkout@v4 24 | - name: setup bun 25 | uses: oven-sh/setup-bun@v2 26 | with: 27 | bun-version: latest 28 | 29 | - name: Setup Java 30 | uses: actions/setup-java@v4 31 | with: 32 | distribution: 'zulu' 33 | java-version: '17' 34 | 35 | - name: Cache NDK 36 | id: cache-ndk 37 | uses: actions/cache@v4 38 | with: 39 | # Path where NDK version 27.0.11902837 is expected to be installed by setup-android 40 | path: /usr/local/lib/android/sdk/ndk/27.0.11902837 41 | key: ndk-${{ runner.os }}-27.0.11902837 42 | 43 | - name: Setup Android SDK and NDK 44 | uses: android-actions/setup-android@v3 45 | with: 46 | cmdline-tools-version: 12266719 # v16 47 | 48 | - name: Install NDK 49 | run: sdkmanager "ndk;27.0.11902837" 50 | 51 | - name: install Rust stable 52 | uses: dtolnay/rust-toolchain@stable 53 | with: 54 | targets: aarch64-linux-android, armv7-linux-androideabi, i686-linux-android, x86_64-linux-android 55 | 56 | - name: Cache Rust dependencies 57 | uses: swatinem/rust-cache@v2 58 | 59 | - name: setup Android signing 60 | run: | 61 | cd src-tauri/gen/android 62 | echo "keyAlias=${{ secrets.ANDROID_KEY_ALIAS }}" > keystore.properties 63 | echo "password=${{ secrets.ANDROID_KEY_PASSWORD }}" >> keystore.properties 64 | base64 -d <<< "${{ secrets.ANDROID_KEY_BASE64 }}" > $RUNNER_TEMP/keystore.jks 65 | echo "storeFile=$RUNNER_TEMP/keystore.jks" >> keystore.properties 66 | echo "Keystore properties created." 67 | cd ../../.. # Go back to root 68 | 69 | - name: Install dependencies 70 | run: bun install 71 | 72 | - name: Build app bundle 73 | run: bun tauri android build -v 74 | env: 75 | NDK_HOME: /usr/local/lib/android/sdk/ndk/27.0.11902837 76 | 77 | - name: Get Tauri App Version 78 | id: app-version 79 | run: | 80 | CARGO_TOML_PATH="src-tauri/Cargo.toml" 81 | echo "Attempting to read version from $CARGO_TOML_PATH" 82 | 83 | if [ ! -f "$CARGO_TOML_PATH" ]; then 84 | echo "Error: $CARGO_TOML_PATH not found." 85 | exit 1 86 | fi 87 | 88 | echo "--- Relevant content from $CARGO_TOML_PATH (looking for 'version = \"...' line) ---" 89 | grep '^version = "' "$CARGO_TOML_PATH" || echo "No line starting with 'version = \"' found in $CARGO_TOML_PATH." 90 | echo "--- End of relevant content ---" 91 | 92 | # Extract version string from Cargo.toml. 93 | # This looks for a line like 'version = "x.y.z"' in the [package] section. 94 | VERSION_STRING=$(grep '^version = "' "$CARGO_TOML_PATH" | head -n 1 | sed -e 's/version = "//' -e 's/"//') 95 | 96 | echo "Extracted version_string: '$VERSION_STRING'" # Debug output 97 | 98 | if [ -z "$VERSION_STRING" ] || [ "$VERSION_STRING" = "null" ]; then # Check for empty or literal "null" 99 | echo "Error: Version not found or is invalid in $CARGO_TOML_PATH." 100 | echo "Please ensure $CARGO_TOML_PATH contains a line like 'version = \"x.y.z\"' (typically under the [package] section)." 101 | exit 1 102 | fi 103 | 104 | echo "Successfully extracted version: $VERSION_STRING" 105 | echo "version_string=$VERSION_STRING" >> "$GITHUB_OUTPUT" 106 | shell: bash 107 | 108 | - name: Rename APK file 109 | run: | 110 | mv ./src-tauri/gen/android/app/build/outputs/apk/universal/release/app-universal-release.apk ./src-tauri/gen/android/app/build/outputs/apk/universal/release/${{env.APP_NAME}}-${{ steps.app-version.outputs.version_string }}.apk 111 | 112 | - name: Publish 113 | uses: softprops/action-gh-release@v1 114 | with: 115 | name: App v${{ steps.app-version.outputs.version_string }} 116 | tag_name: app-v${{ steps.app-version.outputs.version_string }} 117 | body: 'See the assets to download this version and install.' 118 | draft: false 119 | prerelease: false 120 | files: | 121 | ./src-tauri/gen/android/app/build/outputs/apk/universal/release/${{env.APP_NAME}}-${{ steps.app-version.outputs.version_string }}.apk 122 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: "publish desktop" 2 | 3 | on: 4 | pull_request: 5 | branches: 6 | - main 7 | types: 8 | - closed 9 | 10 | jobs: 11 | publish-tauri: 12 | if: ${{ github.event.pull_request.merged }} && github.repository_owner == 'jamessizeland' # This is to ensure that only the owner can publish the app. 13 | name: "Publish Tauri App" 14 | permissions: 15 | contents: write 16 | strategy: 17 | fail-fast: false 18 | matrix: 19 | include: 20 | - platform: "macos-latest" # for Arm based macs (M1 and above). 21 | args: "--target aarch64-apple-darwin" 22 | - platform: "macos-latest" # for Intel based macs. 23 | args: "--target x86_64-apple-darwin" 24 | - platform: "ubuntu-24.04" # for Tauri v1 you could replace this with ubuntu-20.04. 25 | args: "" 26 | - platform: "windows-latest" 27 | args: "" 28 | 29 | runs-on: ${{ matrix.platform }} 30 | steps: 31 | - uses: actions/checkout@v4 32 | 33 | - name: setup bun 34 | uses: oven-sh/setup-bun@v2 35 | with: 36 | bun-version: latest 37 | 38 | - name: install Rust stable 39 | uses: dtolnay/rust-toolchain@stable 40 | with: 41 | # Those targets are only used on macos runners so it's in an `if` to slightly speed up windows and linux builds. 42 | targets: ${{ matrix.platform == 'macos-latest' && 'aarch64-apple-darwin,x86_64-apple-darwin' || '' }} 43 | 44 | - name: install dependencies (ubuntu only) 45 | # Only run for the Linux desktop build, not the Android build (which also uses ubuntu-24.04 runner) 46 | if: matrix.platform == 'ubuntu-24.04' && matrix.args == '' 47 | run: | 48 | sudo apt-get update 49 | sudo apt-get install -y libwebkit2gtk-4.1-dev libappindicator3-dev librsvg2-dev patchelf 50 | 51 | - name: install frontend dependencies 52 | run: bun install # change this to npm, pnpm or bun depending on which one you use. 53 | 54 | - name: build and publish Tauri app 55 | uses: tauri-apps/tauri-action@v0 56 | env: 57 | GITHUB_TOKEN: ${{ secrets.GH_PAT }} 58 | with: 59 | tagName: app-v__VERSION__ # the action automatically replaces __VERSION__ with the app version. 60 | releaseName: "App v__VERSION__" 61 | releaseBody: "See the assets to download this version and install." 62 | releaseDraft: false 63 | prerelease: false 64 | args: ${{ matrix.args }} 65 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | dist 12 | dist-ssr 13 | *.local 14 | bun.lockb 15 | package-lock.json 16 | yarn.lock 17 | 18 | # Editor directories and files 19 | .idea 20 | .DS_Store 21 | *.suo 22 | *.ntvs* 23 | *.njsproj 24 | *.sln 25 | *.sw? 26 | src-tauri/gen/android/keystore.properties 27 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": [ 3 | "dbaeumer.vscode-eslint", 4 | "tauri-apps.tauri-vscode", 5 | "rust-lang.rust-analyzer", 6 | "fill-labs.dependi" 7 | ] 8 | } 9 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "javascript.validate.enable": false, 3 | "javascript.format.enable": false, 4 | "typescript.format.enable": false, 5 | 6 | "search.exclude": { 7 | ".git": true, 8 | ".eslintcache": true, 9 | "src/build": true, 10 | "bower_components": true, 11 | "dll": true, 12 | "release": true, 13 | "node_modules": true, 14 | "npm-debug.log.*": true, 15 | "test/**/__snapshots__": true, 16 | "*.lock": true, 17 | "*.lockb":true, 18 | "*.{css,sass,scss}.d.ts": true 19 | }, 20 | // force format on save to be enabled for this project 21 | "editor.formatOnSave": true, 22 | "editor.formatOnSaveMode": "file" 23 | } 24 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Tauri Chess App 2 | 3 | [![publish android](https://github.com/jamessizeland/tauri-chess/actions/workflows/build-android.yml/badge.svg)](https://github.com/jamessizeland/tauri-chess/actions/workflows/build-android.yml) 4 | [![publish desktop](https://github.com/jamessizeland/tauri-chess/actions/workflows/build.yml/badge.svg)](https://github.com/jamessizeland/tauri-chess/actions/workflows/build.yml) 5 | ![MIT/Apache-2.0 licensed](https://img.shields.io/badge/license-MIT%2FApache--2.0-blue?labelColor=1C2C2E&style=flat-square) 6 | 7 | Experiment with tauri passing data from rust to javascript, by creating a chess app where all of the logic is done in Rust and just the frontend is done in TypeScript React. 8 | 9 | ## Getting Started 10 | 11 | Check prerequisites: 12 | 13 | - 14 | - 15 | 16 | 1. `git clone https://github.com/jamessizeland/tauri-chess.git` 17 | 2. `cd tauri-chess` 18 | 3. `bun install` 19 | 4. `bun run tauri dev` 20 | 21 | ## UI Basics 22 | 23 | ![Chess UI](./img/move_logic2.gif) 24 | 25 | ## Moves 26 | 27 | ![Chess Moves](./img/take_logic.gif) 28 | 29 | ## Turns 30 | 31 | ![Chess Turns](./img/turns_logic.gif) 32 | 33 | ## Checking 34 | 35 | ![Check and Mate](./img/check_mate_logic.gif) 36 | 37 | ## Special Moves 38 | 39 | ### Enpassant, Promoting & Castling 40 | 41 | ![Enpassand Promoting and Castling](./img/enpassant-promote-castle.gif) 42 | 43 | --- 44 | 45 | ## Mobile Support 46 | 47 | Set up to run on Android too. 48 | 49 | `bun run tauri android dev` 50 | 51 | ![android](./img/tauri-chess-android.gif) 52 | -------------------------------------------------------------------------------- /eslint.config.js: -------------------------------------------------------------------------------- 1 | import globals from "globals"; 2 | import pluginJs from "@eslint/js"; 3 | import tseslint from "typescript-eslint"; 4 | import pluginReact from "eslint-plugin-react"; 5 | 6 | export default [ 7 | { files: ["**/*.{js,mjs,cjs,ts,jsx,tsx}"] }, 8 | { languageOptions: { globals: globals.browser } }, 9 | pluginJs.configs.recommended, 10 | ...tseslint.configs.recommended, 11 | pluginReact.configs.flat.recommended, 12 | { extends: ["plugin:react/jsx-runtime"] }, 13 | ]; 14 | -------------------------------------------------------------------------------- /img/check_mate_logic.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jamessizeland/tauri-chess/53b12d3e20866077c3a56422d040ea1f8447e7a9/img/check_mate_logic.gif -------------------------------------------------------------------------------- /img/chess-ui-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jamessizeland/tauri-chess/53b12d3e20866077c3a56422d040ea1f8447e7a9/img/chess-ui-1.png -------------------------------------------------------------------------------- /img/colour-picker.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jamessizeland/tauri-chess/53b12d3e20866077c3a56422d040ea1f8447e7a9/img/colour-picker.gif -------------------------------------------------------------------------------- /img/enpassant-promote-castle.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jamessizeland/tauri-chess/53b12d3e20866077c3a56422d040ea1f8447e7a9/img/enpassant-promote-castle.gif -------------------------------------------------------------------------------- /img/move_logic.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jamessizeland/tauri-chess/53b12d3e20866077c3a56422d040ea1f8447e7a9/img/move_logic.gif -------------------------------------------------------------------------------- /img/move_logic2.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jamessizeland/tauri-chess/53b12d3e20866077c3a56422d040ea1f8447e7a9/img/move_logic2.gif -------------------------------------------------------------------------------- /img/take_logic.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jamessizeland/tauri-chess/53b12d3e20866077c3a56422d040ea1f8447e7a9/img/take_logic.gif -------------------------------------------------------------------------------- /img/tauri-chess-android.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jamessizeland/tauri-chess/53b12d3e20866077c3a56422d040ea1f8447e7a9/img/tauri-chess-android.gif -------------------------------------------------------------------------------- /img/tauri-chess-android.webm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jamessizeland/tauri-chess/53b12d3e20866077c3a56422d040ea1f8447e7a9/img/tauri-chess-android.webm -------------------------------------------------------------------------------- /img/turns_logic.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jamessizeland/tauri-chess/53b12d3e20866077c3a56422d040ea1f8447e7a9/img/turns_logic.gif -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Tauri Chess 8 | 9 | 10 |
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "chess-tauri", 3 | "version": "2.2.0", 4 | "private": true, 5 | "type": "module", 6 | "author": { 7 | "name": "James Sizeland" 8 | }, 9 | "repository": "https://github.com/jamessizeland/tauri-chess", 10 | "scripts": { 11 | "dev": "vite", 12 | "build": "tsc && vite build", 13 | "preview": "vite preview", 14 | "tauri": "tauri" 15 | }, 16 | "dependencies": { 17 | "@headlessui/react": "^2.1.2", 18 | "@tailwindcss/vite": "^4.1.4", 19 | "@tauri-apps/api": ">=2.0.0", 20 | "@tauri-apps/plugin-shell": ">=2.0.0", 21 | "daisyui": "^5.0.0", 22 | "dayjs": "^1.11.12", 23 | "debounce": "^2.1.0", 24 | "react": "^19.1.0", 25 | "react-dnd": "^16.0.1", 26 | "react-dnd-html5-backend": "^16.0.1", 27 | "react-dom": "^19.1.0", 28 | "react-icons": "^5.5.0", 29 | "react-router-dom": "^7.6.1", 30 | "react-toastify": "^11.0.5" 31 | }, 32 | "devDependencies": { 33 | "@eslint/js": "^9.8.0", 34 | "@tauri-apps/cli": ">=2.0.0", 35 | "@types/node": "^22.1.0", 36 | "@types/react": "^19.1.0", 37 | "@types/react-dom": "^19.1.0", 38 | "@vitejs/plugin-react": "^4.2.1", 39 | "animated-tailwindcss": "^4.0.0", 40 | "autoprefixer": "^10.4.20", 41 | "clsx": "^2.1.1", 42 | "eslint": "9.x", 43 | "eslint-plugin-react": "^7.35.0", 44 | "globals": "^16.2.0", 45 | "postcss": "^8.4.41", 46 | "prettier": "3.3.3", 47 | "tailwind-merge": "^3.3.0", 48 | "tailwindcss": "^4.1.8", 49 | "typescript": "^5.2.2", 50 | "typescript-eslint": "^8.0.1", 51 | "vite": "^6.3.5", 52 | "vite-plugin-svgr": "^4.2.0", 53 | "vite-tsconfig-paths": "^5.0.0" 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /prettier.config.cjs: -------------------------------------------------------------------------------- 1 | // * Applied on save due to .vscode/settings.json configuring this project to format on save. 2 | // https://prettier.io/docs/en/options.html 3 | module.exports = { 4 | semi: true, // Add a semicolon at the end of every statement. 5 | trailingComma: 'all', // Print trailing commas wherever possible when multi-line. (A single-line array, for example, never gets trailing commas.) 6 | jsxSingleQuote: false, // Use single quotes instead of double quotes in JSX. 7 | singleQuote: true, 8 | printWidth: 80, // Specify the line length that the printer will wrap on. 9 | proseWrap: 'preserve', // wrap markdown text as-is since some services use a linebreak-sensitive renderer, e.g. GitHub comment and BitBucket. 10 | endOfLine: 'auto', 11 | }; 12 | -------------------------------------------------------------------------------- /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 = "tauri-chess" 3 | version = "2.2.0" 4 | description = "A Tauri App for playing chess" 5 | authors = ["James Sizeland"] 6 | edition = "2024" 7 | 8 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 9 | 10 | [lib] 11 | # The `_lib` suffix may seem redundant but it is necessary 12 | # to make the lib name unique and wouldn't conflict with the bin name. 13 | # This seems to be only an issue on Windows, see https://github.com/rust-lang/cargo/issues/8519 14 | name = "tauri_chess_lib" 15 | crate-type = ["rlib", "cdylib", "staticlib"] 16 | 17 | [build-dependencies] 18 | tauri-build = { version = "^2.0.0", features = [] } 19 | 20 | [dependencies] 21 | tauri = { version = "^2.0.0", features = [] } 22 | serde = { version = "^1", features = ["derive"] } 23 | serde_json = "^1" 24 | rand = { version = "0.9.1" } 25 | anyhow = "^1.0.80" 26 | 27 | # Tauri Plugins 28 | ############################################ 29 | tauri-plugin-shell = "^2.0.0" 30 | 31 | [profile.release] 32 | panic = "abort" # Strip expensive panic clean-up logic 33 | codegen-units = 1 # Compile crates one after another so the compiler can optimize better 34 | lto = true # Enables link to optimizations 35 | opt-level = "s" # Optimize for binary size 36 | strip = true # Remove debug symbols 37 | -------------------------------------------------------------------------------- /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 | "shell:allow-open" 9 | ] 10 | } 11 | -------------------------------------------------------------------------------- /src-tauri/gen/android/.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig is awesome: https://EditorConfig.org 2 | 3 | # top-most EditorConfig file 4 | root = true 5 | 6 | [*] 7 | indent_style = space 8 | indent_size = 2 9 | end_of_line = lf 10 | charset = utf-8 11 | trim_trailing_whitespace = false 12 | insert_final_newline = false -------------------------------------------------------------------------------- /src-tauri/gen/android/.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | .gradle 3 | /local.properties 4 | /.idea/caches 5 | /.idea/libraries 6 | /.idea/modules.xml 7 | /.idea/workspace.xml 8 | /.idea/navEditor.xml 9 | /.idea/assetWizardSettings.xml 10 | .DS_Store 11 | build 12 | /captures 13 | .externalNativeBuild 14 | .cxx 15 | local.properties 16 | key.properties 17 | 18 | /.tauri 19 | /tauri.settings.gradle -------------------------------------------------------------------------------- /src-tauri/gen/android/app/.gitignore: -------------------------------------------------------------------------------- 1 | /src/main/java/com/chess/app/generated 2 | /src/main/jniLibs/**/*.so 3 | /src/main/assets/tauri.conf.json 4 | /tauri.build.gradle.kts 5 | /proguard-tauri.pro 6 | /tauri.properties -------------------------------------------------------------------------------- /src-tauri/gen/android/app/build.gradle.kts: -------------------------------------------------------------------------------- 1 | import java.io.FileInputStream 2 | import java.util.Properties 3 | 4 | plugins { 5 | id("com.android.application") 6 | id("org.jetbrains.kotlin.android") 7 | id("rust") 8 | } 9 | 10 | val tauriProperties = Properties().apply { 11 | val propFile = file("tauri.properties") 12 | if (propFile.exists()) { 13 | propFile.inputStream().use { load(it) } 14 | } 15 | } 16 | 17 | android { 18 | compileSdk = 34 19 | namespace = "com.chess.app" 20 | defaultConfig { 21 | manifestPlaceholders["usesCleartextTraffic"] = "false" 22 | applicationId = "com.chess.app" 23 | minSdk = 24 24 | targetSdk = 34 25 | versionCode = tauriProperties.getProperty("tauri.android.versionCode", "1").toInt() 26 | versionName = tauriProperties.getProperty("tauri.android.versionName", "1.0") 27 | } 28 | signingConfigs { 29 | create("release") { 30 | val keystorePropertiesFile = rootProject.file("keystore.properties") 31 | val keystoreProperties = Properties() 32 | if (keystorePropertiesFile.exists()) { 33 | keystoreProperties.load(FileInputStream(keystorePropertiesFile)) 34 | } 35 | 36 | keyAlias = keystoreProperties["keyAlias"] as String 37 | keyPassword = keystoreProperties["password"] as String 38 | storeFile = file(keystoreProperties["storeFile"] as String) 39 | storePassword = keystoreProperties["password"] as String 40 | } 41 | } 42 | buildTypes { 43 | getByName("debug") { 44 | manifestPlaceholders["usesCleartextTraffic"] = "true" 45 | isDebuggable = true 46 | isJniDebuggable = true 47 | isMinifyEnabled = false 48 | packaging { jniLibs.keepDebugSymbols.add("*/arm64-v8a/*.so") 49 | jniLibs.keepDebugSymbols.add("*/armeabi-v7a/*.so") 50 | jniLibs.keepDebugSymbols.add("*/x86/*.so") 51 | jniLibs.keepDebugSymbols.add("*/x86_64/*.so") 52 | } 53 | } 54 | getByName("release") { 55 | signingConfig = signingConfigs.getByName("release") 56 | isMinifyEnabled = true 57 | proguardFiles( 58 | *fileTree(".") { include("**/*.pro") } 59 | .plus(getDefaultProguardFile("proguard-android-optimize.txt")) 60 | .toList().toTypedArray() 61 | ) 62 | } 63 | } 64 | kotlinOptions { 65 | jvmTarget = "1.8" 66 | } 67 | buildFeatures { 68 | buildConfig = true 69 | } 70 | } 71 | 72 | rust { 73 | rootDirRel = "../../../" 74 | } 75 | 76 | dependencies { 77 | implementation("androidx.webkit:webkit:1.6.1") 78 | implementation("androidx.appcompat:appcompat:1.6.1") 79 | implementation("com.google.android.material:material:1.8.0") 80 | testImplementation("junit:junit:4.13.2") 81 | androidTestImplementation("androidx.test.ext:junit:1.1.4") 82 | androidTestImplementation("androidx.test.espresso:espresso-core:3.5.0") 83 | } 84 | 85 | apply(from = "tauri.build.gradle.kts") -------------------------------------------------------------------------------- /src-tauri/gen/android/app/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # You can control the set of applied configuration files using the 3 | # proguardFiles setting in build.gradle. 4 | # 5 | # For more details, see 6 | # http://developer.android.com/guide/developing/tools/proguard.html 7 | 8 | # If your project uses WebView with JS, uncomment the following 9 | # and specify the fully qualified class name to the JavaScript interface 10 | # class: 11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 12 | # public *; 13 | #} 14 | 15 | # Uncomment this to preserve the line number information for 16 | # debugging stack traces. 17 | #-keepattributes SourceFile,LineNumberTable 18 | 19 | # If you keep the line number information, uncomment this to 20 | # hide the original source file name. 21 | #-renamesourcefileattribute SourceFile -------------------------------------------------------------------------------- /src-tauri/gen/android/app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 13 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 32 | 35 | 36 | 37 | 38 | -------------------------------------------------------------------------------- /src-tauri/gen/android/app/src/main/java/com/chess/app/MainActivity.kt: -------------------------------------------------------------------------------- 1 | package com.chess.app 2 | 3 | class MainActivity : TauriActivity() -------------------------------------------------------------------------------- /src-tauri/gen/android/app/src/main/res/drawable-v24/ic_launcher_foreground.xml: -------------------------------------------------------------------------------- 1 | 7 | 8 | 9 | 15 | 18 | 21 | 22 | 23 | 24 | 30 | -------------------------------------------------------------------------------- /src-tauri/gen/android/app/src/main/res/drawable/ic_launcher_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 10 | 15 | 20 | 25 | 30 | 35 | 40 | 45 | 50 | 55 | 60 | 65 | 70 | 75 | 80 | 85 | 90 | 95 | 100 | 105 | 110 | 115 | 120 | 125 | 130 | 135 | 140 | 145 | 150 | 155 | 160 | 165 | 170 | 171 | -------------------------------------------------------------------------------- /src-tauri/gen/android/app/src/main/res/layout/activity_main.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | 17 | 18 | -------------------------------------------------------------------------------- /src-tauri/gen/android/app/src/main/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jamessizeland/tauri-chess/53b12d3e20866077c3a56422d040ea1f8447e7a9/src-tauri/gen/android/app/src/main/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /src-tauri/gen/android/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jamessizeland/tauri-chess/53b12d3e20866077c3a56422d040ea1f8447e7a9/src-tauri/gen/android/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png -------------------------------------------------------------------------------- /src-tauri/gen/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jamessizeland/tauri-chess/53b12d3e20866077c3a56422d040ea1f8447e7a9/src-tauri/gen/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /src-tauri/gen/android/app/src/main/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jamessizeland/tauri-chess/53b12d3e20866077c3a56422d040ea1f8447e7a9/src-tauri/gen/android/app/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /src-tauri/gen/android/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jamessizeland/tauri-chess/53b12d3e20866077c3a56422d040ea1f8447e7a9/src-tauri/gen/android/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png -------------------------------------------------------------------------------- /src-tauri/gen/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jamessizeland/tauri-chess/53b12d3e20866077c3a56422d040ea1f8447e7a9/src-tauri/gen/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /src-tauri/gen/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jamessizeland/tauri-chess/53b12d3e20866077c3a56422d040ea1f8447e7a9/src-tauri/gen/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /src-tauri/gen/android/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jamessizeland/tauri-chess/53b12d3e20866077c3a56422d040ea1f8447e7a9/src-tauri/gen/android/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png -------------------------------------------------------------------------------- /src-tauri/gen/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jamessizeland/tauri-chess/53b12d3e20866077c3a56422d040ea1f8447e7a9/src-tauri/gen/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /src-tauri/gen/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jamessizeland/tauri-chess/53b12d3e20866077c3a56422d040ea1f8447e7a9/src-tauri/gen/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /src-tauri/gen/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jamessizeland/tauri-chess/53b12d3e20866077c3a56422d040ea1f8447e7a9/src-tauri/gen/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png -------------------------------------------------------------------------------- /src-tauri/gen/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jamessizeland/tauri-chess/53b12d3e20866077c3a56422d040ea1f8447e7a9/src-tauri/gen/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /src-tauri/gen/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jamessizeland/tauri-chess/53b12d3e20866077c3a56422d040ea1f8447e7a9/src-tauri/gen/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /src-tauri/gen/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jamessizeland/tauri-chess/53b12d3e20866077c3a56422d040ea1f8447e7a9/src-tauri/gen/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png -------------------------------------------------------------------------------- /src-tauri/gen/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jamessizeland/tauri-chess/53b12d3e20866077c3a56422d040ea1f8447e7a9/src-tauri/gen/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /src-tauri/gen/android/app/src/main/res/values-night/themes.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | 7 | -------------------------------------------------------------------------------- /src-tauri/gen/android/app/src/main/res/values/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #FFBB86FC 4 | #FF6200EE 5 | #FF3700B3 6 | #FF03DAC5 7 | #FF018786 8 | #FF000000 9 | #FFFFFFFF 10 | -------------------------------------------------------------------------------- /src-tauri/gen/android/app/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | tauri-chess 3 | tauri-chess 4 | -------------------------------------------------------------------------------- /src-tauri/gen/android/app/src/main/res/values/themes.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | 7 | -------------------------------------------------------------------------------- /src-tauri/gen/android/app/src/main/res/xml/file_paths.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /src-tauri/gen/android/build.gradle.kts: -------------------------------------------------------------------------------- 1 | buildscript { 2 | repositories { 3 | google() 4 | mavenCentral() 5 | } 6 | dependencies { 7 | classpath("com.android.tools.build:gradle:8.5.1") 8 | classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:1.9.25") 9 | } 10 | } 11 | 12 | allprojects { 13 | repositories { 14 | google() 15 | mavenCentral() 16 | } 17 | } 18 | 19 | tasks.register("clean").configure { 20 | delete("build") 21 | } 22 | 23 | -------------------------------------------------------------------------------- /src-tauri/gen/android/buildSrc/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | `kotlin-dsl` 3 | } 4 | 5 | gradlePlugin { 6 | plugins { 7 | create("pluginsForCoolKids") { 8 | id = "rust" 9 | implementationClass = "RustPlugin" 10 | } 11 | } 12 | } 13 | 14 | repositories { 15 | google() 16 | mavenCentral() 17 | } 18 | 19 | dependencies { 20 | compileOnly(gradleApi()) 21 | implementation("com.android.tools.build:gradle:8.5.1") 22 | } 23 | 24 | -------------------------------------------------------------------------------- /src-tauri/gen/android/buildSrc/src/main/java/com/chess/app/kotlin/BuildTask.kt: -------------------------------------------------------------------------------- 1 | import java.io.File 2 | import org.apache.tools.ant.taskdefs.condition.Os 3 | import org.gradle.api.DefaultTask 4 | import org.gradle.api.GradleException 5 | import org.gradle.api.logging.LogLevel 6 | import org.gradle.api.tasks.Input 7 | import org.gradle.api.tasks.TaskAction 8 | 9 | open class BuildTask : DefaultTask() { 10 | @Input 11 | var rootDirRel: String? = null 12 | @Input 13 | var target: String? = null 14 | @Input 15 | var release: Boolean? = null 16 | 17 | @TaskAction 18 | fun assemble() { 19 | val executable = """bun"""; 20 | try { 21 | runTauriCli(executable) 22 | } catch (e: Exception) { 23 | if (Os.isFamily(Os.FAMILY_WINDOWS)) { 24 | runTauriCli("$executable.cmd") 25 | } else { 26 | throw e; 27 | } 28 | } 29 | } 30 | 31 | fun runTauriCli(executable: String) { 32 | val rootDirRel = rootDirRel ?: throw GradleException("rootDirRel cannot be null") 33 | val target = target ?: throw GradleException("target cannot be null") 34 | val release = release ?: throw GradleException("release cannot be null") 35 | val args = listOf("tauri", "android", "android-studio-script"); 36 | 37 | project.exec { 38 | workingDir(File(project.projectDir, rootDirRel)) 39 | executable(executable) 40 | args(args) 41 | if (project.logger.isEnabled(LogLevel.DEBUG)) { 42 | args("-vv") 43 | } else if (project.logger.isEnabled(LogLevel.INFO)) { 44 | args("-v") 45 | } 46 | if (release) { 47 | args("--release") 48 | } 49 | args(listOf("--target", target)) 50 | }.assertNormalExitValue() 51 | } 52 | } -------------------------------------------------------------------------------- /src-tauri/gen/android/buildSrc/src/main/java/com/chess/app/kotlin/RustPlugin.kt: -------------------------------------------------------------------------------- 1 | import com.android.build.api.dsl.ApplicationExtension 2 | import org.gradle.api.DefaultTask 3 | import org.gradle.api.Plugin 4 | import org.gradle.api.Project 5 | import org.gradle.kotlin.dsl.configure 6 | import org.gradle.kotlin.dsl.get 7 | 8 | const val TASK_GROUP = "rust" 9 | 10 | open class Config { 11 | lateinit var rootDirRel: String 12 | } 13 | 14 | open class RustPlugin : Plugin { 15 | private lateinit var config: Config 16 | 17 | override fun apply(project: Project) = with(project) { 18 | config = extensions.create("rust", Config::class.java) 19 | 20 | val defaultAbiList = listOf("arm64-v8a", "armeabi-v7a", "x86", "x86_64"); 21 | val abiList = (findProperty("abiList") as? String)?.split(',') ?: defaultAbiList 22 | 23 | val defaultArchList = listOf("arm64", "arm", "x86", "x86_64"); 24 | val archList = (findProperty("archList") as? String)?.split(',') ?: defaultArchList 25 | 26 | val targetsList = (findProperty("targetList") as? String)?.split(',') ?: listOf("aarch64", "armv7", "i686", "x86_64") 27 | 28 | extensions.configure { 29 | @Suppress("UnstableApiUsage") 30 | flavorDimensions.add("abi") 31 | productFlavors { 32 | create("universal") { 33 | dimension = "abi" 34 | ndk { 35 | abiFilters += abiList 36 | } 37 | } 38 | defaultArchList.forEachIndexed { index, arch -> 39 | create(arch) { 40 | dimension = "abi" 41 | ndk { 42 | abiFilters.add(defaultAbiList[index]) 43 | } 44 | } 45 | } 46 | } 47 | } 48 | 49 | afterEvaluate { 50 | for (profile in listOf("debug", "release")) { 51 | val profileCapitalized = profile.replaceFirstChar { it.uppercase() } 52 | val buildTask = tasks.maybeCreate( 53 | "rustBuildUniversal$profileCapitalized", 54 | DefaultTask::class.java 55 | ).apply { 56 | group = TASK_GROUP 57 | description = "Build dynamic library in $profile mode for all targets" 58 | } 59 | 60 | tasks["mergeUniversal${profileCapitalized}JniLibFolders"].dependsOn(buildTask) 61 | 62 | for (targetPair in targetsList.withIndex()) { 63 | val targetName = targetPair.value 64 | val targetArch = archList[targetPair.index] 65 | val targetArchCapitalized = targetArch.replaceFirstChar { it.uppercase() } 66 | val targetBuildTask = project.tasks.maybeCreate( 67 | "rustBuild$targetArchCapitalized$profileCapitalized", 68 | BuildTask::class.java 69 | ).apply { 70 | group = TASK_GROUP 71 | description = "Build dynamic library in $profile mode for $targetArch" 72 | rootDirRel = config.rootDirRel 73 | target = targetName 74 | release = profile == "release" 75 | } 76 | 77 | buildTask.dependsOn(targetBuildTask) 78 | tasks["merge$targetArchCapitalized${profileCapitalized}JniLibFolders"].dependsOn( 79 | targetBuildTask 80 | ) 81 | } 82 | } 83 | } 84 | } 85 | } -------------------------------------------------------------------------------- /src-tauri/gen/android/gradle.properties: -------------------------------------------------------------------------------- 1 | # Project-wide Gradle settings. 2 | # IDE (e.g. Android Studio) users: 3 | # Gradle settings configured through the IDE *will override* 4 | # any settings specified in this file. 5 | # For more details on how to configure your build environment visit 6 | # http://www.gradle.org/docs/current/userguide/build_environment.html 7 | # Specifies the JVM arguments used for the daemon process. 8 | # The setting is particularly useful for tweaking memory settings. 9 | org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8 10 | # When configured, Gradle will run in incubating parallel mode. 11 | # This option should only be used with decoupled projects. More details, visit 12 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects 13 | # org.gradle.parallel=true 14 | # AndroidX package structure to make it clearer which packages are bundled with the 15 | # Android operating system, and which are packaged with your app"s APK 16 | # https://developer.android.com/topic/libraries/support-library/androidx-rn 17 | android.useAndroidX=true 18 | # Kotlin code style for this project: "official" or "obsolete": 19 | kotlin.code.style=official 20 | # Enables namespacing of each library's R class so that its R class includes only the 21 | # resources declared in the library itself and none from the library's dependencies, 22 | # thereby reducing the size of the R class for that library 23 | android.nonTransitiveRClass=true 24 | android.nonFinalResIds=false -------------------------------------------------------------------------------- /src-tauri/gen/android/gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jamessizeland/tauri-chess/53b12d3e20866077c3a56422d040ea1f8447e7a9/src-tauri/gen/android/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /src-tauri/gen/android/gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Tue May 10 19:22:52 CST 2022 2 | distributionBase=GRADLE_USER_HOME 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.9-bin.zip 4 | distributionPath=wrapper/dists 5 | zipStorePath=wrapper/dists 6 | zipStoreBase=GRADLE_USER_HOME 7 | -------------------------------------------------------------------------------- /src-tauri/gen/android/gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | # 4 | # Copyright 2015 the original author or authors. 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # https://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | 19 | ############################################################################## 20 | ## 21 | ## Gradle start up script for UN*X 22 | ## 23 | ############################################################################## 24 | 25 | # Attempt to set APP_HOME 26 | # Resolve links: $0 may be a link 27 | PRG="$0" 28 | # Need this for relative symlinks. 29 | while [ -h "$PRG" ] ; do 30 | ls=`ls -ld "$PRG"` 31 | link=`expr "$ls" : '.*-> \(.*\)$'` 32 | if expr "$link" : '/.*' > /dev/null; then 33 | PRG="$link" 34 | else 35 | PRG=`dirname "$PRG"`"/$link" 36 | fi 37 | done 38 | SAVED="`pwd`" 39 | cd "`dirname \"$PRG\"`/" >/dev/null 40 | APP_HOME="`pwd -P`" 41 | cd "$SAVED" >/dev/null 42 | 43 | APP_NAME="Gradle" 44 | APP_BASE_NAME=`basename "$0"` 45 | 46 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 47 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' 48 | 49 | # Use the maximum available, or set MAX_FD != -1 to use that value. 50 | MAX_FD="maximum" 51 | 52 | warn () { 53 | echo "$*" 54 | } 55 | 56 | die () { 57 | echo 58 | echo "$*" 59 | echo 60 | exit 1 61 | } 62 | 63 | # OS specific support (must be 'true' or 'false'). 64 | cygwin=false 65 | msys=false 66 | darwin=false 67 | nonstop=false 68 | case "`uname`" in 69 | CYGWIN* ) 70 | cygwin=true 71 | ;; 72 | Darwin* ) 73 | darwin=true 74 | ;; 75 | MINGW* ) 76 | msys=true 77 | ;; 78 | NONSTOP* ) 79 | nonstop=true 80 | ;; 81 | esac 82 | 83 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 84 | 85 | 86 | # Determine the Java command to use to start the JVM. 87 | if [ -n "$JAVA_HOME" ] ; then 88 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 89 | # IBM's JDK on AIX uses strange locations for the executables 90 | JAVACMD="$JAVA_HOME/jre/sh/java" 91 | else 92 | JAVACMD="$JAVA_HOME/bin/java" 93 | fi 94 | if [ ! -x "$JAVACMD" ] ; then 95 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 96 | 97 | Please set the JAVA_HOME variable in your environment to match the 98 | location of your Java installation." 99 | fi 100 | else 101 | JAVACMD="java" 102 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 103 | 104 | Please set the JAVA_HOME variable in your environment to match the 105 | location of your Java installation." 106 | fi 107 | 108 | # Increase the maximum file descriptors if we can. 109 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then 110 | MAX_FD_LIMIT=`ulimit -H -n` 111 | if [ $? -eq 0 ] ; then 112 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 113 | MAX_FD="$MAX_FD_LIMIT" 114 | fi 115 | ulimit -n $MAX_FD 116 | if [ $? -ne 0 ] ; then 117 | warn "Could not set maximum file descriptor limit: $MAX_FD" 118 | fi 119 | else 120 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 121 | fi 122 | fi 123 | 124 | # For Darwin, add options to specify how the application appears in the dock 125 | if $darwin; then 126 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 127 | fi 128 | 129 | # For Cygwin or MSYS, switch paths to Windows format before running java 130 | if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then 131 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 132 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 133 | 134 | JAVACMD=`cygpath --unix "$JAVACMD"` 135 | 136 | # We build the pattern for arguments to be converted via cygpath 137 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 138 | SEP="" 139 | for dir in $ROOTDIRSRAW ; do 140 | ROOTDIRS="$ROOTDIRS$SEP$dir" 141 | SEP="|" 142 | done 143 | OURCYGPATTERN="(^($ROOTDIRS))" 144 | # Add a user-defined pattern to the cygpath arguments 145 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 146 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 147 | fi 148 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 149 | i=0 150 | for arg in "$@" ; do 151 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 152 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 153 | 154 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 155 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 156 | else 157 | eval `echo args$i`="\"$arg\"" 158 | fi 159 | i=`expr $i + 1` 160 | done 161 | case $i in 162 | 0) set -- ;; 163 | 1) set -- "$args0" ;; 164 | 2) set -- "$args0" "$args1" ;; 165 | 3) set -- "$args0" "$args1" "$args2" ;; 166 | 4) set -- "$args0" "$args1" "$args2" "$args3" ;; 167 | 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 168 | 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 169 | 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 170 | 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 171 | 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 172 | esac 173 | fi 174 | 175 | # Escape application args 176 | save () { 177 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done 178 | echo " " 179 | } 180 | APP_ARGS=`save "$@"` 181 | 182 | # Collect all arguments for the java command, following the shell quoting and substitution rules 183 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" 184 | 185 | exec "$JAVACMD" "$@" 186 | -------------------------------------------------------------------------------- /src-tauri/gen/android/gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem https://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | 17 | @if "%DEBUG%" == "" @echo off 18 | @rem ########################################################################## 19 | @rem 20 | @rem Gradle startup script for Windows 21 | @rem 22 | @rem ########################################################################## 23 | 24 | @rem Set local scope for the variables with windows NT shell 25 | if "%OS%"=="Windows_NT" setlocal 26 | 27 | set DIRNAME=%~dp0 28 | if "%DIRNAME%" == "" set DIRNAME=. 29 | set APP_BASE_NAME=%~n0 30 | set APP_HOME=%DIRNAME% 31 | 32 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 33 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 34 | 35 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 36 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 37 | 38 | @rem Find java.exe 39 | if defined JAVA_HOME goto findJavaFromJavaHome 40 | 41 | set JAVA_EXE=java.exe 42 | %JAVA_EXE% -version >NUL 2>&1 43 | if "%ERRORLEVEL%" == "0" goto execute 44 | 45 | echo. 46 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 47 | echo. 48 | echo Please set the JAVA_HOME variable in your environment to match the 49 | echo location of your Java installation. 50 | 51 | goto fail 52 | 53 | :findJavaFromJavaHome 54 | set JAVA_HOME=%JAVA_HOME:"=% 55 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 56 | 57 | if exist "%JAVA_EXE%" goto execute 58 | 59 | echo. 60 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 61 | echo. 62 | echo Please set the JAVA_HOME variable in your environment to match the 63 | echo location of your Java installation. 64 | 65 | goto fail 66 | 67 | :execute 68 | @rem Setup the command line 69 | 70 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 71 | 72 | 73 | @rem Execute Gradle 74 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* 75 | 76 | :end 77 | @rem End local scope for the variables with windows NT shell 78 | if "%ERRORLEVEL%"=="0" goto mainEnd 79 | 80 | :fail 81 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 82 | rem the _cmd.exe /c_ return code! 83 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 84 | exit /b 1 85 | 86 | :mainEnd 87 | if "%OS%"=="Windows_NT" endlocal 88 | 89 | :omega 90 | -------------------------------------------------------------------------------- /src-tauri/gen/android/settings.gradle: -------------------------------------------------------------------------------- 1 | include ':app' 2 | 3 | apply from: 'tauri.settings.gradle' 4 | -------------------------------------------------------------------------------- /src-tauri/icons/128x128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jamessizeland/tauri-chess/53b12d3e20866077c3a56422d040ea1f8447e7a9/src-tauri/icons/128x128.png -------------------------------------------------------------------------------- /src-tauri/icons/128x128@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jamessizeland/tauri-chess/53b12d3e20866077c3a56422d040ea1f8447e7a9/src-tauri/icons/128x128@2x.png -------------------------------------------------------------------------------- /src-tauri/icons/32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jamessizeland/tauri-chess/53b12d3e20866077c3a56422d040ea1f8447e7a9/src-tauri/icons/32x32.png -------------------------------------------------------------------------------- /src-tauri/icons/Square107x107Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jamessizeland/tauri-chess/53b12d3e20866077c3a56422d040ea1f8447e7a9/src-tauri/icons/Square107x107Logo.png -------------------------------------------------------------------------------- /src-tauri/icons/Square142x142Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jamessizeland/tauri-chess/53b12d3e20866077c3a56422d040ea1f8447e7a9/src-tauri/icons/Square142x142Logo.png -------------------------------------------------------------------------------- /src-tauri/icons/Square150x150Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jamessizeland/tauri-chess/53b12d3e20866077c3a56422d040ea1f8447e7a9/src-tauri/icons/Square150x150Logo.png -------------------------------------------------------------------------------- /src-tauri/icons/Square284x284Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jamessizeland/tauri-chess/53b12d3e20866077c3a56422d040ea1f8447e7a9/src-tauri/icons/Square284x284Logo.png -------------------------------------------------------------------------------- /src-tauri/icons/Square30x30Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jamessizeland/tauri-chess/53b12d3e20866077c3a56422d040ea1f8447e7a9/src-tauri/icons/Square30x30Logo.png -------------------------------------------------------------------------------- /src-tauri/icons/Square310x310Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jamessizeland/tauri-chess/53b12d3e20866077c3a56422d040ea1f8447e7a9/src-tauri/icons/Square310x310Logo.png -------------------------------------------------------------------------------- /src-tauri/icons/Square44x44Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jamessizeland/tauri-chess/53b12d3e20866077c3a56422d040ea1f8447e7a9/src-tauri/icons/Square44x44Logo.png -------------------------------------------------------------------------------- /src-tauri/icons/Square71x71Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jamessizeland/tauri-chess/53b12d3e20866077c3a56422d040ea1f8447e7a9/src-tauri/icons/Square71x71Logo.png -------------------------------------------------------------------------------- /src-tauri/icons/Square89x89Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jamessizeland/tauri-chess/53b12d3e20866077c3a56422d040ea1f8447e7a9/src-tauri/icons/Square89x89Logo.png -------------------------------------------------------------------------------- /src-tauri/icons/StoreLogo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jamessizeland/tauri-chess/53b12d3e20866077c3a56422d040ea1f8447e7a9/src-tauri/icons/StoreLogo.png -------------------------------------------------------------------------------- /src-tauri/icons/icon.icns: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jamessizeland/tauri-chess/53b12d3e20866077c3a56422d040ea1f8447e7a9/src-tauri/icons/icon.icns -------------------------------------------------------------------------------- /src-tauri/icons/icon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jamessizeland/tauri-chess/53b12d3e20866077c3a56422d040ea1f8447e7a9/src-tauri/icons/icon.ico -------------------------------------------------------------------------------- /src-tauri/icons/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jamessizeland/tauri-chess/53b12d3e20866077c3a56422d040ea1f8447e7a9/src-tauri/icons/icon.png -------------------------------------------------------------------------------- /src-tauri/src/chess/api.rs: -------------------------------------------------------------------------------- 1 | //! Logic for the chess board actions 2 | 3 | use super::{ 4 | board::BoardState, 5 | data::{GameMetaData, HistoryData, Message, PieceLocation, QueueHandler, SelectedSquare}, 6 | moves::check_castling_moves, 7 | types::{Color, GameMeta, MoveList, MoveType, Piece, Square}, 8 | utils::{check_enemy, remove_invalid_moves, square_to_coord, turn_into_colour, valid_move}, 9 | }; 10 | use anyhow::{anyhow, Context}; 11 | use tauri::Result; 12 | 13 | #[tauri::command] 14 | /// Get the location of all pieces from global memory 15 | pub fn get_state(state: tauri::State) -> BoardState { 16 | let board = state.lock().expect("board state access"); 17 | board.clone() 18 | } 19 | 20 | #[tauri::command] 21 | /// Get the game score from global memory 22 | pub fn get_score(state: tauri::State) -> GameMeta { 23 | let meta_game = state.lock().expect("game state access"); 24 | *meta_game 25 | } 26 | 27 | #[tauri::command] 28 | /// Initialize a new game by sending a starting set of coords 29 | pub fn new_game( 30 | state: tauri::State, 31 | meta: tauri::State, 32 | history_data: tauri::State, 33 | ) -> BoardState { 34 | // Lock the counter(Mutex) to get the current value 35 | let mut board = state.lock().expect("board state access"); 36 | let mut game_meta = meta.lock().expect("game state access"); 37 | let mut history = history_data.lock().expect("game history access"); 38 | *history = Default::default(); 39 | // reset game meta data 40 | game_meta.new_game(); 41 | // reset board to empty 42 | *board = BoardState::new(); 43 | 44 | board.clone() // return dereferenced board state to frontend 45 | } 46 | 47 | #[tauri::command] 48 | /// Highlight available moves for the piece occupying this square 49 | pub fn hover_square( 50 | square: &str, 51 | state: tauri::State, 52 | clicked: tauri::State, 53 | meta: tauri::State, 54 | ) -> Result { 55 | let board = state.lock().expect("board state access"); 56 | let game_meta = meta.lock().expect("game state access"); 57 | let selected = *clicked.lock().expect("selected square access"); 58 | let mut coord: Square = square_to_coord(square)?; 59 | println!("hovering over square {:?}", coord); 60 | let turn = turn_into_colour(game_meta.turn); 61 | if let Some(square) = selected { 62 | coord = square; 63 | } 64 | let piece = board.get(coord); 65 | let mut move_options = piece.get_moves(coord, &board); 66 | if piece.is_king(turn) { 67 | for castle_move in check_castling_moves(coord, turn, &board) { 68 | move_options.push(castle_move); 69 | } 70 | } else if piece == Piece::Pawn(turn, false) { 71 | if let Some(en_passant) = game_meta.en_passant { 72 | // does this piece have a valid en passant target? 73 | for pawn_move in piece.get_en_passant_moves(coord, en_passant) { 74 | move_options.push(pawn_move); 75 | } 76 | } 77 | } 78 | move_options = remove_invalid_moves(move_options, coord, &game_meta, &board); 79 | Ok(move_options) 80 | } 81 | 82 | #[tauri::command] 83 | /// Remove any highlighting for square just left 84 | pub fn unhover_square(_square: &str) -> bool { 85 | true 86 | } 87 | 88 | #[tauri::command] 89 | /// Perform the boardstate change associated with a chess piece being moved 90 | pub fn drop_square(source_square: &str, target_square: &str, piece: &str) { 91 | dbg!(source_square, target_square, piece); 92 | } 93 | 94 | #[tauri::command] 95 | /// Click on a square to select or deselect it. 96 | /// 97 | /// If a square is selected, the hover command will be deactivated. 98 | /// If a square is a valid move of the selected piece, move that piece. 99 | pub fn click_square( 100 | square: &str, 101 | state: tauri::State, 102 | clicked: tauri::State, 103 | meta: tauri::State, 104 | history_data: tauri::State, 105 | queue: tauri::State, 106 | ) -> Result<(MoveList, BoardState, GameMeta)> { 107 | // acquire control of global data 108 | let mut board = state.lock().expect("board state access"); 109 | let mut game_meta = meta.lock().expect("game state access"); 110 | let mut selected = *clicked.lock().expect("selected square access"); 111 | 112 | let mut move_list = MoveList::new(); 113 | if game_meta.game_over { 114 | // game over, do nothing 115 | return Ok((move_list, board.clone(), *game_meta)); 116 | } 117 | let coord = square_to_coord(square)?; 118 | let turn = turn_into_colour(game_meta.turn); 119 | let piece = board.get(coord); 120 | let contains_enemy = check_enemy(turn, &piece); 121 | if selected.is_none() { 122 | // 1.if we have nothing selected and the new coordinate doesn't contain an enemy piece, select it! 123 | if !contains_enemy { 124 | move_list = piece.get_moves(coord, &board); 125 | if piece.is_king(turn) { 126 | for castle_move in check_castling_moves(coord, turn, &board) { 127 | move_list.push(castle_move); 128 | } 129 | } else if piece == Piece::Pawn(turn, false) { 130 | if let Some(en_passant) = game_meta.en_passant { 131 | // does this piece have a valid en passant target? 132 | for pawn_move in piece.get_en_passant_moves(coord, en_passant) { 133 | move_list.push(pawn_move); 134 | } 135 | } 136 | }; 137 | move_list = remove_invalid_moves(move_list, coord, &game_meta, &board); 138 | selected = if move_list.is_empty() { 139 | None 140 | } else { 141 | Some(coord) 142 | } 143 | } 144 | } else if selected == Some(coord) { 145 | // 2. if we have clicked on the same square again, unselect it 146 | selected = None; 147 | } else { 148 | println!("possible valid move, check"); 149 | match valid_move(selected.unwrap(), coord, &board, &game_meta, turn) { 150 | Some(move_type) => { 151 | // 4. if we have clicked a valid move of selected, do move 152 | println!("valid move"); 153 | let source = selected.unwrap(); 154 | let mover = board.get(source).has_moved(); 155 | board.set(coord, Piece::None); // empty the destination square 156 | board.set(source, Piece::None); // take moving out of its square 157 | board.set(coord, mover); // place moving in the new square 158 | game_meta.en_passant = None; // clear any previous en passant target 159 | match move_type { 160 | MoveType::Castle => { 161 | println!("need to move rook too"); 162 | let start_col = if coord.0 > 4 { 7 } else { 0 }; 163 | let dest_col = if coord.0 > 4 { 5 } else { 3 }; 164 | let row = coord.1; 165 | let mover = board.get((start_col, row)).has_moved(); 166 | board.set((start_col, row), Piece::None); // take castling rook out of its square 167 | board.set((dest_col, row), mover); // place castling rook in the new square 168 | } 169 | MoveType::EnPassant => { 170 | println!("need to remove pawn too"); 171 | board.set((coord.0, source.1), Piece::None); 172 | } 173 | MoveType::Double => { 174 | println!("register an en passant target"); 175 | game_meta.en_passant = Some(coord); 176 | } 177 | _ => { 178 | // normal move or capture 179 | if mover.is_promotable_pawn(coord) { 180 | game_meta.promotable_pawn = Some(coord); 181 | queue 182 | .lock() 183 | .expect("queue access") 184 | .blocking_send(Message::new("promotion", &coord)?) 185 | .context("failed to send promotion event")?; 186 | } 187 | } 188 | } 189 | if mover.is_king(turn) { 190 | match turn { 191 | Color::Black => { 192 | game_meta.black_king.piece = mover; 193 | game_meta.black_king.square = coord; 194 | } 195 | Color::White => { 196 | game_meta.white_king.piece = mover; 197 | game_meta.white_king.square = coord; 198 | } 199 | } 200 | } 201 | selected = None; 202 | // 5. update the meta only if something has changed 203 | let mut history = history_data.lock().expect("game history access"); 204 | game_meta.new_turn(&mut board, &mut history); 205 | println!("score history: {:?}", history.score); 206 | } 207 | None => { 208 | // 6. select the new square as this isn't a valid move 209 | selected = Some(coord); 210 | move_list = board.get(coord).get_moves(coord, &board); 211 | move_list = remove_invalid_moves(move_list, coord, &game_meta, &board); 212 | if move_list.is_empty() { 213 | selected = None; 214 | } 215 | } 216 | } 217 | } 218 | *clicked.lock().expect("clicked square access") = selected; 219 | Ok((move_list, board.to_owned(), game_meta.to_owned())) 220 | } 221 | 222 | #[tauri::command] 223 | /// User has selected a piece type for the promotion of a valid pawn 224 | pub fn promote( 225 | choice: char, 226 | state: tauri::State, 227 | meta: tauri::State, 228 | queue: tauri::State, 229 | ) -> Result<()> { 230 | let mut game_meta = meta.lock().expect("game state access"); 231 | let colour = turn_into_colour(game_meta.turn + 1); 232 | if let Some(coord) = game_meta.promotable_pawn { 233 | let mut board = state.lock().expect("board state access"); 234 | let rx = queue.lock().expect("queue access"); 235 | game_meta.promotable_pawn = None; 236 | let promotion = match choice { 237 | 'Q' => Piece::Queen(colour, false), 238 | 'K' => Piece::Knight(colour, false), 239 | 'R' => Piece::Rook(colour, false), 240 | 'B' => Piece::Bishop(colour, false), 241 | _ => return Err(anyhow!("invalid promotion choice"))?, 242 | }; 243 | board.set(coord, promotion); 244 | rx.blocking_send(Message::new("board", &*board)?) 245 | .context("failed to send board state")?; 246 | }; 247 | Ok(()) 248 | } 249 | -------------------------------------------------------------------------------- /src-tauri/src/chess/board.rs: -------------------------------------------------------------------------------- 1 | //! Contains the board state 2 | 3 | use super::types::{Color, Piece, Square}; 4 | use serde::{Deserialize, Serialize}; 5 | 6 | /// 8x8 array containing either pieces or nothing 7 | #[derive(Debug, PartialEq, Serialize, Deserialize, Clone)] 8 | pub struct BoardState([[Piece; 8]; 8]); 9 | 10 | impl BoardState { 11 | /// Create a new board with the default starting position 12 | pub fn new() -> Self { 13 | let (white, black) = (Color::White, Color::Black); 14 | 15 | let mut board = [[Piece::None; 8]; 8]; 16 | // set up white pieces 17 | board[0][0] = Piece::Rook(white, true); 18 | board[1][0] = Piece::Bishop(white, true); 19 | board[2][0] = Piece::Knight(white, true); 20 | board[3][0] = Piece::Queen(white, true); 21 | board[4][0] = Piece::King(white, true, false, false); 22 | board[5][0] = Piece::Knight(white, true); 23 | board[6][0] = Piece::Bishop(white, true); 24 | board[7][0] = Piece::Rook(white, true); 25 | // set up black pieces 26 | board[0][7] = Piece::Rook(black, true); 27 | board[1][7] = Piece::Knight(black, true); 28 | board[2][7] = Piece::Bishop(black, true); 29 | board[3][7] = Piece::Queen(black, true); 30 | board[4][7] = Piece::King(black, true, false, false); 31 | board[5][7] = Piece::Bishop(black, true); 32 | board[6][7] = Piece::Knight(black, true); 33 | board[7][7] = Piece::Rook(black, true); 34 | // set up pawns 35 | for col in board.iter_mut() { 36 | col[1] = Piece::Pawn(white, true); 37 | col[6] = Piece::Pawn(black, true); 38 | } 39 | BoardState(board) 40 | } 41 | /// Set a piece on the board 42 | pub fn set(&mut self, square: Square, piece: Piece) { 43 | self.0[square.0][square.1] = piece; 44 | } 45 | /// Get a piece from the board 46 | pub fn get(&self, square: Square) -> Piece { 47 | self.0[square.0][square.1] 48 | } 49 | pub fn iter(&self) -> std::slice::Iter<[Piece; 8]> { 50 | self.0.iter() 51 | } 52 | } 53 | 54 | impl Default for BoardState { 55 | fn default() -> Self { 56 | BoardState([[Piece::None; 8]; 8]) 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src-tauri/src/chess/data.rs: -------------------------------------------------------------------------------- 1 | use super::types; 2 | use crate::chess::board::BoardState; 3 | use anyhow::Result; 4 | use serde::{Deserialize, Serialize}; 5 | use std::sync::Mutex; 6 | use tauri::{async_runtime::Receiver, Emitter as _, WebviewWindow}; // mutual exclusion wrapper 7 | 8 | #[derive(Clone, Serialize, Deserialize, Debug)] 9 | pub struct Message { 10 | pub event: &'static str, 11 | pub payload: String, 12 | } 13 | impl Message { 14 | pub fn new(event: &'static str, payload: &T) -> Result { 15 | Ok(Self { 16 | event, 17 | payload: serde_json::to_string(payload)?, 18 | }) 19 | } 20 | } 21 | 22 | /// Handler event data from Rust -> Frontend 23 | pub fn queue_handler(window: &WebviewWindow, rx: &mut Receiver) -> Result<()> { 24 | if let Some(res) = rx.blocking_recv() { 25 | match res.event { 26 | "board" => { 27 | let board: BoardState = serde_json::from_str(&res.payload)?; 28 | window.emit("board", board)?; 29 | } 30 | other => { 31 | println!("{}, {}", other, res.payload); 32 | window.emit(other, res.payload)?; 33 | } 34 | } 35 | } 36 | Ok(()) 37 | } 38 | 39 | /// game move history 40 | pub type HistoryData = Mutex; 41 | 42 | /// Game state 8x8 board, filled with empty space or Pieces 43 | pub type PieceLocation = Mutex; 44 | 45 | /// Track which square has been selected in the UI 46 | pub type SelectedSquare = Mutex>; 47 | 48 | /// Game score stored relative to white 49 | pub type GameMetaData = Mutex; 50 | /// queue handler 51 | pub type QueueHandler = Mutex>; 52 | -------------------------------------------------------------------------------- /src-tauri/src/chess/mod.rs: -------------------------------------------------------------------------------- 1 | //! Chess module 2 | 3 | pub mod api; 4 | mod board; 5 | pub mod data; 6 | mod moves; 7 | mod pieces; 8 | mod types; 9 | mod unit_tests; 10 | mod utils; 11 | -------------------------------------------------------------------------------- /src-tauri/src/chess/pieces.rs: -------------------------------------------------------------------------------- 1 | //! Chess pieces traits 2 | 3 | use super::board::BoardState; 4 | use super::moves::{bish_move, en_passant_move, king_move, knight_move, pawn_move, rook_move}; 5 | use super::types::{Color, GameMeta, MoveList, Piece, Square}; 6 | use super::utils::{remove_invalid_moves, under_threat}; 7 | 8 | /// Request state information from a selected piece 9 | impl Piece { 10 | /// Return a list of all available moves for this piece 11 | pub fn get_moves(&self, sq: Square, board: &BoardState) -> MoveList { 12 | //* For each actual piece we need to work out what moves it could do on an empty board, then remove moves that are blocked by other pieces 13 | match self { 14 | // what type of piece am I? 15 | Piece::None => Vec::new(), 16 | Piece::Pawn(color, first_move) => pawn_move(sq, *color, *first_move, board), 17 | Piece::King(color, ..) => king_move(sq, *color, board), 18 | Piece::Queen(color, ..) => { 19 | //* move in any direction until either another piece or the edge of the board 20 | let mut moves = rook_move(sq, *color, board); 21 | let mut diag_moves = bish_move(sq, *color, board); 22 | moves.append(&mut diag_moves); 23 | moves 24 | } 25 | Piece::Bishop(color, ..) => bish_move(sq, *color, board), 26 | Piece::Knight(color, ..) => knight_move(sq, *color, board), 27 | Piece::Rook(color, ..) => rook_move(sq, *color, board), 28 | } 29 | } 30 | /// Return if there is an available en passant move for this pawn 31 | pub fn get_en_passant_moves(&self, square: Square, en_passant_target: Square) -> MoveList { 32 | if let Piece::Pawn(color, ..) = self { 33 | en_passant_move(square, *color, en_passant_target) 34 | } else { 35 | panic!("should only be targetting a pawn") 36 | } 37 | } 38 | /// Return this piece's color 39 | pub fn get_colour(&self) -> Option { 40 | match self { 41 | Piece::None => None, 42 | Piece::Pawn(color, ..) => Some(*color), 43 | Piece::King(color, ..) => Some(*color), 44 | Piece::Queen(color, ..) => Some(*color), 45 | Piece::Bishop(color, ..) => Some(*color), 46 | Piece::Knight(color, ..) => Some(*color), 47 | Piece::Rook(color, ..) => Some(*color), 48 | } 49 | } 50 | /// Check if this piece is a king of a certain color 51 | pub fn is_king(&self, color: Color) -> bool { 52 | match self { 53 | Piece::King(my_color, ..) => my_color == &color, 54 | _ => false, 55 | } 56 | } 57 | /// If this piece is a king, is it in checkmate? 58 | pub fn is_king_mate(&self) -> Option { 59 | if let Piece::King(.., mate) = self { 60 | Some(*mate) 61 | } else { 62 | None 63 | } 64 | } 65 | /// Return the relative weighting value of this piece based on its type 66 | /// 67 | /// https://en.wikipedia.org/wiki/Chess_piece_relative_value 68 | pub fn get_value(&self) -> Option { 69 | match &self { 70 | Piece::None => None, 71 | Piece::Pawn(..) => Some(100), 72 | Piece::King(..) => None, 73 | Piece::Queen(..) => Some(929), 74 | Piece::Bishop(..) => Some(320), 75 | Piece::Knight(..) => Some(280), 76 | Piece::Rook(..) => Some(479), 77 | } 78 | } 79 | /// Ask if this piece is a promotable pawn 80 | pub fn is_promotable_pawn(&self, square: Square) -> bool { 81 | if let Piece::Pawn(color, ..) = self { 82 | match color { 83 | // check if the pawn has reached the other side of the board 84 | Color::Black => square.1 == 0, 85 | Color::White => square.1 == 7, 86 | } 87 | } else { 88 | false 89 | } 90 | } 91 | /// This piece has been moved, so update its First Move status to false 92 | /// 93 | /// Return modified piece 94 | pub fn has_moved(&self) -> Self { 95 | match self { 96 | Piece::None => Self::None, 97 | Piece::Pawn(color, ..) => Self::Pawn(*color, false), 98 | Piece::King(color, _, check, mate) => Piece::King(*color, false, *check, *mate), 99 | Piece::Queen(color, ..) => Self::Queen(*color, false), 100 | Piece::Bishop(color, ..) => Self::Bishop(*color, false), 101 | Piece::Knight(color, ..) => Self::Knight(*color, false), 102 | Piece::Rook(color, ..) => Self::Rook(*color, false), 103 | } 104 | } 105 | /// If this piece is a king, update its check and checkmate states 106 | /// 107 | /// Modifies piece in place 108 | pub fn king_threat(&mut self, location: &Square, board: &BoardState, meta: GameMeta) { 109 | if let Piece::King(color, first_move, ..) = self { 110 | let check = under_threat(*location, *color, board); 111 | let mut team_moves: usize = 0; 112 | for col in 0..8 { 113 | for row in 0..8 { 114 | let piece = board.get((col, row)); 115 | if piece.get_colour() == Some(*color) { 116 | let no_moves = remove_invalid_moves( 117 | piece.get_moves((col, row), board), 118 | (col, row), 119 | &meta, 120 | board, 121 | ) 122 | .len(); 123 | team_moves += no_moves; 124 | } 125 | } 126 | } 127 | let mate = check && (team_moves == 0); 128 | *self = Piece::King(*color, *first_move, check, mate); 129 | } 130 | } 131 | } 132 | -------------------------------------------------------------------------------- /src-tauri/src/chess/types.rs: -------------------------------------------------------------------------------- 1 | //! Specific Types useful for a chess game 2 | 3 | use super::board::BoardState; 4 | use serde::{Deserialize, Serialize}; 5 | use std::fmt::Display; 6 | 7 | #[derive(Clone, Copy, Debug, Serialize, Deserialize)] 8 | pub struct GameMeta { 9 | /// Game turn 10 | pub turn: usize, 11 | /// Game score as a relative sum of piece value 12 | pub score: isize, 13 | /// Register if a pawn that has done a double move in the last turn 14 | pub en_passant: Option, 15 | /// Register if a pawn is awaiting a promotion 16 | pub promotable_pawn: Option, 17 | /// Metadata relating to the black King 18 | pub black_king: KingMeta, 19 | /// Metadata relating to the white King 20 | pub white_king: KingMeta, 21 | /// Register if the game is active or ended 22 | pub game_over: bool, 23 | } 24 | 25 | #[derive(Clone, Debug, Serialize, Deserialize, Default)] 26 | pub struct Hist { 27 | pub score: Vec, 28 | } 29 | 30 | #[derive(Clone, Copy, Debug, Serialize, Deserialize)] 31 | pub struct KingMeta { 32 | /// Clone of the King's Piece Struct 33 | pub piece: Piece, 34 | /// Current location of the King 35 | pub square: Square, 36 | } 37 | 38 | impl GameMeta { 39 | /// Check if kings are under threat and update their status, return if checkmate occurred 40 | pub fn update_king_threat(&mut self, board: &mut BoardState) { 41 | let turn: Color = if self.turn % 2 == 0 { 42 | Color::White 43 | } else { 44 | Color::Black 45 | }; 46 | match turn { 47 | Color::White => { 48 | // check white king status 49 | self.white_king 50 | .piece 51 | .king_threat(&self.white_king.square, board, *self); 52 | board.set(self.white_king.square, self.white_king.piece); 53 | self.game_over = self.white_king.piece.is_king_mate().unwrap() 54 | } 55 | Color::Black => { 56 | // check black king status 57 | self.black_king 58 | .piece 59 | .king_threat(&self.black_king.square, board, *self); 60 | board.set(self.black_king.square, self.black_king.piece); 61 | self.game_over = self.black_king.piece.is_king_mate().unwrap() 62 | } 63 | } 64 | } 65 | /// Increment the turn to the next player, check state of both players and return if game end has occurred 66 | pub fn update_turn(&mut self) { 67 | self.turn += 1; 68 | println!("turn {}", self.turn) 69 | } 70 | /// Set up new game 71 | pub fn new_game(&mut self) { 72 | self.score = 0; 73 | self.turn = 0; 74 | self.game_over = false; 75 | self.white_king.piece = Piece::King(Color::White, true, false, false); 76 | self.white_king.square = (4, 0); 77 | self.black_king.piece = Piece::King(Color::Black, true, false, false); 78 | self.black_king.square = (4, 7); 79 | } 80 | /// run all necessary board state cleanup to start a new turn 81 | pub fn new_turn(&mut self, board: &mut BoardState, history: &mut Hist) { 82 | self.update_king_threat(board); // evaluate at the end of turn 83 | self.update_turn(); // toggle who's turn it is to play 84 | self.update_king_threat(board); // evaluate again start of next turn 85 | self.calc_score(board); // calculate score 86 | history.score.push(self.score); 87 | } 88 | /// Update score based on value of pieces on the board 89 | pub fn calc_score(&mut self, board: &BoardState) { 90 | let (mut white, mut black) = (0, 0); 91 | for col in board.iter() { 92 | for piece in col { 93 | if let Some(color) = piece.get_colour() { 94 | match color { 95 | Color::Black => black += piece.get_value().unwrap_or(0), 96 | Color::White => white += piece.get_value().unwrap_or(0), 97 | } 98 | } 99 | } 100 | } 101 | self.score = white - black; 102 | } 103 | } 104 | 105 | impl Default for GameMeta { 106 | fn default() -> Self { 107 | GameMeta { 108 | turn: 0, 109 | score: 0, 110 | en_passant: None, 111 | promotable_pawn: None, 112 | game_over: false, 113 | white_king: KingMeta { 114 | piece: Piece::King(Color::White, true, false, false), 115 | square: (4, 0), 116 | }, 117 | black_king: KingMeta { 118 | piece: Piece::King(Color::Black, true, false, false), 119 | square: (4, 7), 120 | }, 121 | } 122 | } 123 | } 124 | 125 | /// Square reference in column and row 126 | pub type Square = (usize, usize); 127 | 128 | #[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq)] 129 | #[repr(u8)] 130 | /// Moves can take one of many special types 131 | pub enum MoveType { 132 | Move, 133 | Capture, 134 | Castle, 135 | EnPassant, 136 | Double, 137 | } 138 | 139 | pub type MoveList = Vec<(Square, MoveType)>; 140 | 141 | #[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq)] 142 | pub enum Color { 143 | Black, 144 | White, 145 | } 146 | 147 | /// Some pieces have special behaviour if they haven't moved yet 148 | pub type FirstMove = bool; 149 | /// Is the King in check, meaning we need to consider avaliable moves 150 | pub type Check = bool; 151 | /// Is the King in check with no means to get out of check 152 | pub type CheckMate = bool; 153 | 154 | #[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq, Default)] 155 | #[repr(u8)] 156 | pub enum Piece { 157 | #[default] 158 | None, 159 | Pawn(Color, FirstMove), 160 | King(Color, FirstMove, Check, CheckMate), 161 | Queen(Color, FirstMove), 162 | Bishop(Color, FirstMove), 163 | Knight(Color, FirstMove), 164 | Rook(Color, FirstMove), 165 | } 166 | 167 | impl Display for Piece { 168 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 169 | let code = match *self { 170 | Piece::None => "__", 171 | Piece::Pawn(Color::White, ..) => "wP", 172 | Piece::Pawn(Color::Black, ..) => "bP", 173 | Piece::King(Color::White, ..) => "wK", 174 | Piece::King(Color::Black, ..) => "bK", 175 | Piece::Queen(Color::White, ..) => "wQ", 176 | Piece::Queen(Color::Black, ..) => "bQ", 177 | Piece::Bishop(Color::White, ..) => "wB", 178 | Piece::Bishop(Color::Black, ..) => "bB", 179 | Piece::Knight(Color::White, ..) => "wN", 180 | Piece::Knight(Color::Black, ..) => "bN", 181 | Piece::Rook(Color::White, ..) => "wR", 182 | Piece::Rook(Color::Black, ..) => "bR", 183 | }; 184 | write!(f, "{}", code) 185 | } 186 | } 187 | -------------------------------------------------------------------------------- /src-tauri/src/chess/unit_tests.rs: -------------------------------------------------------------------------------- 1 | #[cfg(test)] 2 | mod tests { 3 | use crate::chess::{ 4 | board::BoardState, 5 | types::{Color, Piece}, 6 | }; 7 | 8 | #[test] 9 | /// put all piece types in all squares and see if get moves doesn't panic 10 | fn all_pieces_happy_in_all_squares() { 11 | let board: BoardState = Default::default(); 12 | // dbg!(&board); 13 | let piece_list = [ 14 | Piece::None, 15 | Piece::Pawn(Color::Black, true), 16 | Piece::Rook(Color::Black, true), 17 | Piece::Bishop(Color::Black, true), 18 | Piece::Knight(Color::Black, true), 19 | Piece::King(Color::Black, true, false, false), 20 | Piece::Queen(Color::Black, true), 21 | Piece::Pawn(Color::White, true), 22 | Piece::Rook(Color::White, true), 23 | Piece::Bishop(Color::White, true), 24 | Piece::Knight(Color::White, true), 25 | Piece::King(Color::White, true, false, false), 26 | Piece::Queen(Color::White, true), 27 | ]; 28 | for piece in piece_list { 29 | for col in 0..8 { 30 | for row in 0..8 { 31 | println!("{:?}", piece); 32 | piece.get_moves((col, row), &board); 33 | } 34 | } 35 | } 36 | assert!(true); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src-tauri/src/chess/utils.rs: -------------------------------------------------------------------------------- 1 | //! General utility functions for chess 2 | 3 | use super::board::BoardState; 4 | use super::moves::check_castling_moves; 5 | use super::types::{Color, GameMeta, MoveList, MoveType, Piece, Square}; 6 | use anyhow::{anyhow, Result}; 7 | 8 | /// Convert letters a-h to a row index from a standard chessboard 9 | pub fn letter_to_row(letter: char) -> Result { 10 | Ok(match letter { 11 | 'a' => 0, 12 | 'b' => 1, 13 | 'c' => 2, 14 | 'd' => 3, 15 | 'e' => 4, 16 | 'f' => 5, 17 | 'g' => 6, 18 | 'h' => 7, 19 | _ => return Err(anyhow!("invalid letter")), 20 | }) 21 | } 22 | 23 | /// check if the piece we are looking is of the opposite colour to us 24 | pub fn check_enemy(our_color: Color, considered_piece: &Piece) -> bool { 25 | (considered_piece.get_colour() == Some(Color::Black) && our_color == Color::White) 26 | || (considered_piece.get_colour() == Some(Color::White) && our_color == Color::Black) 27 | } 28 | 29 | /// convert a square string to a coordinate tuple i.e. b3 = (2,1) 30 | /// col (letter) followed by row (number) 31 | pub fn square_to_coord(square: &str) -> Result<(usize, usize)> { 32 | let sq_vec: Vec = square.chars().collect(); 33 | if sq_vec.len() != 2 { 34 | return Err(anyhow!("square string wasn't 2 characters")); 35 | }; 36 | let Some(digit) = sq_vec[1].to_digit(10) else { 37 | return Err(anyhow!("couldn't convert digit to number")); 38 | }; 39 | Ok((letter_to_row(sq_vec[0])?, (digit - 1) as usize)) 40 | } 41 | 42 | /// Check if the square we clicked on is a valid move of the currently selected piece, and what type 43 | pub fn valid_move( 44 | source: Square, 45 | target: Square, 46 | board: &BoardState, 47 | meta: &GameMeta, 48 | turn: Color, 49 | ) -> Option { 50 | let piece = board.get(source); 51 | let mut move_options = piece.get_moves(source, board); 52 | if piece.is_king(turn) { 53 | for castle_move in check_castling_moves(source, turn, board) { 54 | move_options.push(castle_move); 55 | } 56 | }; 57 | if piece == Piece::Pawn(turn, false) { 58 | if let Some(en_passant_target) = meta.en_passant { 59 | for pawn_move in piece.get_en_passant_moves(source, en_passant_target) { 60 | move_options.push(pawn_move); 61 | } 62 | } 63 | } 64 | let filtered_moves = remove_invalid_moves(move_options, source, meta, board); 65 | filtered_moves 66 | .iter() 67 | .position(|(sq, _)| sq == &target) 68 | .map(|i| filtered_moves[i].1) 69 | } 70 | 71 | /// Check if this square is threatened, by exhaustive search 72 | pub fn under_threat(square: Square, our_color: Color, board: &BoardState) -> bool { 73 | let mut threatened = false; 74 | // println!("checking threat"); 75 | 'outer: for col in 0..8 { 76 | for row in 0..8 { 77 | let potential_threat = board.get((col, row)); 78 | if check_enemy(our_color, &potential_threat) { 79 | for m in potential_threat.get_moves((col, row), board) { 80 | if m.0 == square { 81 | threatened = true; 82 | break 'outer; 83 | } 84 | } 85 | } 86 | } 87 | } 88 | threatened 89 | } 90 | 91 | /// Check each move in this list to check it doesn't leave your king in check 92 | pub fn remove_invalid_moves( 93 | moves: MoveList, 94 | my_square: Square, 95 | meta: &GameMeta, 96 | board: &BoardState, 97 | ) -> MoveList { 98 | // println!("removing invalid moves"); 99 | let mut filtered_moves = MoveList::new(); 100 | let my_piece = board.get(my_square); 101 | if my_piece != Piece::None { 102 | // only do this if we're looking at a non-empty square 103 | let our_color = my_piece.get_colour().expect("square has a color"); 104 | let i_am_king = my_piece.is_king(our_color); 105 | if turn_into_colour(meta.turn) == our_color { 106 | // our turn 107 | let mut theory_board: BoardState = board.clone(); 108 | let our_king = match our_color { 109 | Color::White => meta.white_king, 110 | Color::Black => meta.black_king, 111 | }; 112 | // Is my king not in check or, am I infact the king and could potentially move? 113 | // Am I preventing check by being where I am? 114 | theory_board.set(my_square, Piece::None); 115 | // println!("{:?}", my_piece.is_king()); 116 | if !under_threat(our_king.square, our_color, &theory_board) && !i_am_king { 117 | // println!("king isn't threatened if I'm not there"); 118 | // doesn't become under threat, allow all moves 119 | filtered_moves = moves; 120 | } else { 121 | for (coord, move_type) in moves { 122 | // check if any of the potential moves cause my king to go into check 123 | theory_board = board.clone(); // reset the board 124 | theory_board.set(my_square, Piece::None); // remove my piece 125 | theory_board.set(coord, my_piece); // place it in a potential move spot 126 | let king_square = match i_am_king { 127 | true => coord, 128 | false => our_king.square, 129 | }; 130 | if !under_threat(king_square, our_color, &theory_board) { 131 | filtered_moves.push((coord, move_type)); 132 | } 133 | } 134 | } 135 | } 136 | } 137 | filtered_moves 138 | } 139 | 140 | /// convert turn number into the corresponding color 141 | /// 142 | /// assuming White always goes first 143 | pub fn turn_into_colour(turn: usize) -> Color { 144 | if turn % 2 == 0 { 145 | Color::White 146 | } else { 147 | Color::Black 148 | } 149 | } 150 | 151 | #[allow(dead_code)] 152 | /// Print to console the board state from White's perspective, in a neat form 153 | fn pretty_print_board(board: &BoardState) { 154 | for (i, row) in board.iter().enumerate().rev() { 155 | for (j, _) in row.iter().enumerate() { 156 | if j < 7 { 157 | print!("|{}|", board.get((j, i))); 158 | } else { 159 | println!("|{}|", board.get((j, i))); 160 | } 161 | } 162 | } 163 | print!("\r\n"); 164 | } 165 | -------------------------------------------------------------------------------- /src-tauri/src/lib.rs: -------------------------------------------------------------------------------- 1 | // Learn more about Tauri commands at https://tauri.app/v1/guides/features/command 2 | 3 | mod chess; 4 | 5 | use anyhow::Context; 6 | use chess::data::{queue_handler, Message}; 7 | use std::thread; 8 | use tauri::{async_runtime::channel, Manager, Result}; 9 | 10 | #[tauri::command] 11 | fn event_tester(queue: tauri::State) -> Result<()> { 12 | let rx = queue.lock().expect("failed to lock queue"); 13 | rx.blocking_send(Message::new("test", &"hello from Rust")?) 14 | .context("failed to send event")?; 15 | Ok(()) 16 | } 17 | 18 | #[cfg_attr(mobile, tauri::mobile_entry_point)] 19 | pub fn run() { 20 | let (sender, mut receiver) = channel::(5); 21 | tauri::Builder::default() 22 | .plugin(tauri_plugin_shell::init()) 23 | .setup(|app| { 24 | let window = app 25 | .get_webview_window("main") 26 | .ok_or_else(|| anyhow::anyhow!("Failed to get the window"))?; 27 | thread::spawn(move || { 28 | println!("spawning a new thread to handle unprompted events from Rust to the UI"); 29 | loop { 30 | if let Err(error) = queue_handler(&window, &mut receiver) { 31 | eprintln!("error while handling queue: {:?}", error); 32 | break; 33 | } 34 | } 35 | }); 36 | Ok(()) 37 | }) 38 | .manage(chess::data::PieceLocation::default()) 39 | .manage(chess::data::GameMetaData::default()) 40 | .manage(chess::data::SelectedSquare::default()) 41 | .manage(chess::data::HistoryData::default()) 42 | .manage(chess::data::QueueHandler::new(sender)) 43 | .invoke_handler(tauri::generate_handler![ 44 | chess::api::new_game, 45 | chess::api::get_state, 46 | chess::api::get_score, 47 | chess::api::hover_square, 48 | chess::api::unhover_square, 49 | chess::api::drop_square, 50 | chess::api::click_square, 51 | chess::api::promote, 52 | event_tester, 53 | ]) 54 | .run(tauri::generate_context!()) 55 | .expect("error while running tauri application"); 56 | } 57 | -------------------------------------------------------------------------------- /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 | tauri_chess_lib::run() 6 | } 7 | -------------------------------------------------------------------------------- /src-tauri/tauri.conf.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://schema.tauri.app/config/2.0.0", 3 | "productName": "tauri-chess", 4 | "identifier": "com.chess.app", 5 | "build": { 6 | "beforeDevCommand": "bun run dev", 7 | "devUrl": "http://localhost:1420", 8 | "beforeBuildCommand": "bun run build", 9 | "frontendDist": "../dist" 10 | }, 11 | "app": { 12 | "windows": [ 13 | { 14 | "title": "tauri-chess", 15 | "width": 800, 16 | "height": 800, 17 | "resizable": true, 18 | "center": true, 19 | "fullscreen": false 20 | } 21 | ], 22 | "security": { 23 | "csp": null 24 | } 25 | }, 26 | "bundle": { 27 | "active": true, 28 | "targets": "all", 29 | "icon": [ 30 | "icons/32x32.png", 31 | "icons/128x128.png", 32 | "icons/128x128@2x.png", 33 | "icons/icon.icns", 34 | "icons/icon.ico" 35 | ] 36 | } 37 | } -------------------------------------------------------------------------------- /src/assets/favicon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /src/assets/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /src/assets/tauri.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | tauri 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /src/components/Elements/LICENCE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Enoch Ndika 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. -------------------------------------------------------------------------------- /src/components/Elements/README.md: -------------------------------------------------------------------------------- 1 | # UI Components for React built with Tailwind CSS 3 2 | ![Kimia-UI](https://res.cloudinary.com/beloved/image/upload/v1618040187/Assets/kimia_lpqdlr.png) 3 | 4 | ## Why this approach? 5 | 6 | Tailwind CSS is a utility-first CSS framework for rapidly building custom user interfaces. Il allows you writing your style without leaving your HTML. 7 | 8 | The biggest disadvantage of Tailwind CSS is the risk of having too long classes that will make our code not readable enough. 9 | 10 | As React is component-based, we can extract component logic with its classes and reuse them elsewhere which will result in more readable code with more components and fewer classes. 11 | 12 | That's why I have created this collection of UI components fully customizable. Just copy and paste a component you want to use 13 | 14 | All the components are in the **packages** directory. 15 | 16 | Each component contains 2 sub-directories 17 | * **examples** : contains examples for each variant of the component in TypeScript 18 | 19 | 20 | * **snippets** : contains examples for each variant of the component in plain React 21 | 22 | 23 | ## 📋 Add a new component 24 | To add a new component : 25 | 26 | Create your new directory in **src/packages/{yourComponentName}** Inside your folder, you will create 2 subfolders and one file 27 | 28 | - **examples** : will contains examples for your component in TypeScript** 29 | - **snippets** : Will contains examples in plain React and will be used as code snippet to copy 30 | - **{your component name}.tsx** will contains the logic of your components 31 | 32 | 33 | Create your new file(route) in **src/pages/components/{your component name}**. Then you will import all the examples and snippets for your component 34 | 35 | 36 | ## Browser Support 37 | 38 | These components are compatible with all browsers 39 | 40 | | Chrome | Firefox | Edge | Safari | Opera | 41 | |:---:|:---:|:---:|:---:|:---:| 42 | | | | | | | 43 | 44 | ## Contribution 45 | If you would like to contribute on the project, fixing bugs, improve accessibility or open an issue, please follow our [Contribution guide](https://github.com/enochndika/kimia-UI/blob/main/contributing.md) 46 | 47 | First, run the development server: 48 | 49 | ```bash 50 | npm run dev 51 | # or 52 | yarn dev 53 | ``` 54 | 55 | Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. 56 | 57 | -------------------------------------------------------------------------------- /src/components/Elements/button/button.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { cn } from 'utils'; 3 | 4 | // eslint-disable-next-line @typescript-eslint/no-empty-interface 5 | interface BtnPropsWithChildren {} 6 | 7 | interface BtnProps 8 | extends React.ButtonHTMLAttributes, 9 | BtnPropsWithChildren { 10 | block?: boolean; 11 | children: React.ReactNode; 12 | className?: string; 13 | color?: 'primary' | 'success' | 'danger' | 'warning' | 'indigo' | 'dark'; 14 | disabled?: boolean; 15 | outline?: boolean; 16 | rounded?: boolean; 17 | size?: 'sm' | 'md' | 'lg'; 18 | submit?: boolean; 19 | } 20 | 21 | type ButtonRef = React.ForwardedRef; 22 | 23 | const ngClass = { 24 | rounded: `rounded-full`, 25 | block: `flex justify-center w-full`, 26 | default: `text-white focus:outline-none shadow font-medium transition ease-in duration-200`, 27 | disabled: `opacity-60 cursor-not-allowed`, 28 | sizes: { 29 | sm: 'px-6 py-1 text-sm', 30 | md: 'px-6 py-2', 31 | lg: 'px-6 py-3 text-lg', 32 | }, 33 | color: { 34 | primary: { 35 | bg: `bg-blue-700 focus:ring-2 focus:ring-offset-2 focus:ring-blue-700 focus:ring-offset-blue-200`, 36 | outline: `border-blue-700 border-2 text-blue-700 active:bg-blue-700 active:text-white`, 37 | }, 38 | success: { 39 | bg: `bg-green-700 focus:ring-2 focus:ring-offset-2 focus:ring-green-700 focus:ring-offset-green-200`, 40 | outline: `border-green-700 border-2 text-green-700 active:bg-green-700 active:text-white`, 41 | }, 42 | danger: { 43 | bg: `bg-red-600 focus:ring-2 focus:ring-offset-2 focus:ring-red-600 focus:ring-offset-red-200`, 44 | outline: `border-red-600 border-2 text-red-600 active:bg-red-600 active:text-white`, 45 | }, 46 | dark: { 47 | bg: `bg-black focus:ring-2 focus:ring-offset-2 focus:ring-gray-800 focus:ring-offset-gray-200`, 48 | outline: `border-black border-2 text-gray-900 active:bg-black active:text-white`, 49 | }, 50 | warning: { 51 | bg: `bg-yellow-500 focus:ring-2 focus:ring-offset-2 focus:ring-yellow-500 focus:ring-offset-yellow-200`, 52 | outline: `border-yellow-500 border-2 text-yellow-500 active:bg-yellow-500 active:text-white`, 53 | }, 54 | indigo: { 55 | bg: `bg-indigo-900 focus:ring-2 focus:ring-offset-2 focus:ring-indigo-900 focus:ring-offset-indigo-200`, 56 | outline: `border-indigo-900 border-2 text-indigo-900 active:bg-indigo-900 active:text-white`, 57 | }, 58 | }, 59 | }; 60 | 61 | const colors = (outline: boolean) => ({ 62 | primary: outline ? ngClass.color.primary.outline : ngClass.color.primary.bg, 63 | success: outline ? ngClass.color.success.outline : ngClass.color.success.bg, 64 | danger: outline ? ngClass.color.danger.outline : ngClass.color.danger.bg, 65 | dark: outline ? ngClass.color.dark.outline : ngClass.color.dark.bg, 66 | warning: outline ? ngClass.color.warning.outline : ngClass.color.warning.bg, 67 | indigo: outline ? ngClass.color.indigo.outline : ngClass.color.indigo.bg, 68 | }); 69 | 70 | const Button = React.forwardRef( 71 | ( 72 | { 73 | block = false, 74 | children, 75 | className, 76 | color, 77 | disabled = false, 78 | outline = false, 79 | rounded, 80 | size = 'md', 81 | submit, 82 | ...props 83 | }: BtnProps, 84 | ref: ButtonRef, 85 | ) => ( 86 | 103 | ), 104 | ); 105 | 106 | Button.displayName = 'Button'; 107 | 108 | export { Button }; 109 | -------------------------------------------------------------------------------- /src/components/Elements/drawer/drawer.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useRef } from 'react'; 2 | import { createPortal } from 'react-dom'; 3 | import { cn } from 'utils'; 4 | 5 | interface Props { 6 | children: React.ReactNode; 7 | } 8 | 9 | interface DrawerProps extends Props { 10 | isOpen: boolean; 11 | position?: 'left' | 'right' | 'top' | 'bottom'; 12 | toggle: (isOpen?: boolean) => void; 13 | } 14 | 15 | const ngClass = { 16 | body: `flex-shrink flex-grow p-4`, 17 | headerTitle: `text-2xl md:text-3xl font-light`, 18 | content: `relative flex flex-col bg-white pointer-events-auto w-full`, 19 | header: `items-start justify-between p-4 border-b border-gray-300`, 20 | container: `fixed top-0 left-0 z-40 w-full h-full m-0 overflow-hidden`, 21 | overlay: `fixed top-0 left-0 z-30 w-screen h-screen bg-black opacity-50`, 22 | footer: `flex flex-wrap items-center justify-end p-3 border-t border-gray-300`, 23 | animation: { 24 | top: 'animate-drawer-top', 25 | left: 'animate-drawer-left', 26 | right: 'animate-drawer-right', 27 | bottom: 'animate-drawer-bottom', 28 | }, 29 | orientation: { 30 | top: `flex w-full h-auto absolute top-0 focus:outline-none `, 31 | bottom: `flex w-full h-auto absolute bottom-0 focus:outline-none `, 32 | left: `flex w-8/12 md:w-80 lg:w-96 h-full left-0 mx-0 my-0 absolute focus:outline-none `, 33 | right: `flex w-8/12 md:w-80 lg:w-96 h-full right-0 mx-0 my-0 absolute focus:outline-none `, 34 | }, 35 | }; 36 | 37 | /** Logic */ 38 | 39 | function Drawer({ children, isOpen, toggle, position = 'left' }: DrawerProps) { 40 | const ref = useRef(null); 41 | 42 | useEffect(() => { 43 | const handleOutsideClick = (event: globalThis.MouseEvent) => { 44 | if (!ref.current?.contains(event.target as Node)) { 45 | if (!isOpen) return; 46 | toggle(false); 47 | } 48 | }; 49 | window.addEventListener('click', handleOutsideClick); 50 | return () => window.removeEventListener('click', handleOutsideClick); 51 | }, [isOpen, ref, toggle]); 52 | 53 | // close drawer when you click on "ESC" key 54 | useEffect(() => { 55 | const handleEscape = (event: globalThis.KeyboardEvent) => { 56 | if (!isOpen) return; 57 | 58 | if (event.key === 'Escape') { 59 | toggle(false); 60 | } 61 | }; 62 | document.addEventListener('keyup', handleEscape); 63 | return () => document.removeEventListener('keyup', handleEscape); 64 | }, [isOpen, toggle]); 65 | 66 | // hide scrollbar and prevent body from moving when modal is open 67 | //put focus on modal dialogue 68 | useEffect(() => { 69 | if (!isOpen) return; 70 | ref.current?.focus(); 71 | 72 | const html = document.documentElement; 73 | 74 | const scrollbarWidth = 75 | window.innerWidth - document.documentElement.clientWidth; 76 | 77 | html.style.overflow = 'hidden'; 78 | html.style.paddingRight = `${scrollbarWidth}px`; 79 | 80 | return () => { 81 | html.style.overflow = ''; 82 | html.style.paddingRight = ''; 83 | }; 84 | }, [isOpen]); 85 | 86 | return ( 87 | <> 88 | {createPortal( 89 | isOpen && ( 90 | <> 91 |
92 |
93 |
100 |
103 | {children} 104 |
105 |
106 |
107 | 108 | ), 109 | document.body, 110 | )} 111 | 112 | ); 113 | } 114 | 115 | function DrawerHeader({ children }: Props) { 116 | return ( 117 |
118 |

{children}

119 |
120 | ); 121 | } 122 | 123 | function DrawerBody({ children }: Props) { 124 | return
{children}
; 125 | } 126 | 127 | function DrawerFooter({ children }: Props) { 128 | return
{children}
; 129 | } 130 | 131 | export { Drawer, DrawerHeader, DrawerBody, DrawerFooter }; 132 | -------------------------------------------------------------------------------- /src/components/Elements/hamburger-menu/hamburgerMenu.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Link } from 'react-router-dom'; 3 | import { cn } from 'utils'; 4 | 5 | interface Props { 6 | children: React.ReactNode; 7 | } 8 | 9 | interface LinkProps extends Props { 10 | href: string; 11 | } 12 | 13 | interface HamburgerMenuProps extends React.HTMLAttributes { 14 | textColor?: string; 15 | bgColor?: string; 16 | children: React.ReactNode; 17 | } 18 | 19 | interface HamburgerCollapseProps extends Props { 20 | open: boolean; 21 | } 22 | 23 | interface HamburgerTogglerProps { 24 | toggle: () => void; 25 | } 26 | 27 | const ngClass = { 28 | nav: `block pl-0 mb-0`, 29 | hamburger: `font-light shadow py-2 px-4`, 30 | collapse: `transition-height ease duration-300`, 31 | toggler: `float-right pt-1.5 text-3xl focus:outline-none focus:shadow`, 32 | link: `block cursor-pointer py-1.5 px-4 hover:text-gray-300 font-medium`, 33 | brand: `inline-block pt-1.5 pb-1.5 mr-4 cursor-pointer text-2xl font-bold whitespace-nowrap hover:text-gray-400`, 34 | }; 35 | 36 | function HamburgerMenu({ children, bgColor, textColor }: HamburgerMenuProps) { 37 | return ( 38 | 39 | ); 40 | } 41 | 42 | /* You can wrap the a tag with Link and pass href to Link if you are using either Create-React-App, Next.js or Gatsby */ 43 | function HamburgerMenuBrand({ children, href }: LinkProps) { 44 | return ( 45 | 46 | {children} 47 | 48 | ); 49 | } 50 | 51 | function HamburgerMenuToggler({ toggle }: HamburgerTogglerProps) { 52 | return ( 53 | 62 | ); 63 | } 64 | 65 | function HamburgerMenuCollapse({ open, children }: HamburgerCollapseProps) { 66 | const ref = React.useRef(null); 67 | 68 | const inlineStyle: React.CSSProperties = open 69 | ? { height: ref.current?.scrollHeight } 70 | : { height: 0, visibility: 'hidden', opacity: 0 }; 71 | 72 | return ( 73 |
74 | {children} 75 |
76 | ); 77 | } 78 | 79 | function HamburgerMenuNav({ children }: Props) { 80 | return
    {children}
; 81 | } 82 | 83 | function HamburgerMenuItem({ children }: Props) { 84 | return
  • {children}
  • ; 85 | } 86 | 87 | /* You can wrap the a tag with Link and pass href to Link if you are using either Create-React-App, Next.js or Gatsby */ 88 | function HamburgerMenuLink({ children, href }: LinkProps) { 89 | return ( 90 | 91 | {children} 92 | 93 | ); 94 | } 95 | 96 | export { 97 | HamburgerMenu, 98 | HamburgerMenuBrand, 99 | HamburgerMenuToggler, 100 | HamburgerMenuCollapse, 101 | HamburgerMenuNav, 102 | HamburgerMenuItem, 103 | HamburgerMenuLink, 104 | }; 105 | -------------------------------------------------------------------------------- /src/components/Elements/icon/icon.tsx: -------------------------------------------------------------------------------- 1 | import { ReactNode } from 'react'; 2 | import { cn } from 'utils'; 3 | 4 | interface Props { 5 | className?: string; 6 | } 7 | 8 | interface IconProps extends Props { 9 | viewBox: string; 10 | children: ReactNode; 11 | } 12 | 13 | const Icon = ({ className, viewBox, children }: IconProps) => ( 14 |
    15 | 23 | {children} 24 | 25 |
    26 | ); 27 | 28 | export { Icon }; 29 | -------------------------------------------------------------------------------- /src/components/Elements/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Export all the Atomic Elements from here to be used in the rest of the project 3 | */ 4 | export * from './button/button'; 5 | export * from './drawer/drawer'; 6 | export * from './hamburger-menu/hamburgerMenu'; 7 | export * from './modal/modal'; 8 | export * from './navbar/navbar'; 9 | export * from './sidenav/sidenav'; 10 | export * from './icon/icon'; 11 | -------------------------------------------------------------------------------- /src/components/Elements/modal/modal.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect, ReactNode, useRef } from 'react'; 2 | import { createPortal } from 'react-dom'; 3 | import { cn } from 'utils'; 4 | 5 | interface Props { 6 | children: ReactNode; 7 | } 8 | 9 | interface ModalProps extends Props { 10 | isOpen: boolean; 11 | position?: 'default' | 'large' | 'extraLarge'; 12 | toggle: (isOpen?: boolean) => void; 13 | closeOnClickOutside?: boolean; 14 | animate?: boolean; 15 | } 16 | 17 | const ngClass = { 18 | animate: 'animate-backInDown', 19 | body: `flex-shrink flex-grow p-4`, 20 | headerTitle: `text-2xl md:text-3xl font-light`, 21 | header: `items-start justify-between p-4 border-b border-gray-300`, 22 | container: `fixed top-0 left-0 z-40 w-full h-full m-0 overflow-y-auto`, 23 | overlay: `fixed top-0 left-0 z-30 w-screen h-screen backdrop-blur-sm`, 24 | footer: `flex flex-wrap items-center justify-end p-3 border-t border-gray-300`, 25 | content: { 26 | default: `relative flex flex-col bg-white pointer-events-auto shadow-primary shadow-md`, 27 | }, 28 | orientation: { 29 | default: 30 | 'mt-5 mx-8 pb-0 md:m-auto md:w-6/12 lg:w-4/12 md:pt-0 focus:outline-none rounded-lg', 31 | large: 32 | 'mt-5 mx-8 pb-0 md:m-auto md:w-8/12 lg:w-8/12 md:pt-0 focus:outline-none rounded-lg', 33 | extraLarge: 'mt-5 mx-8 pb-0 focus:outline-none rounded-lg', 34 | }, 35 | }; 36 | 37 | function Modal({ 38 | isOpen, 39 | toggle, 40 | children, 41 | animate = false, 42 | closeOnClickOutside = false, 43 | position = 'default', 44 | }: ModalProps) { 45 | const ref = useRef(null); 46 | 47 | // close modal when you click outside the modal dialogue 48 | useEffect(() => { 49 | const handleOutsideClick = (event: globalThis.MouseEvent) => { 50 | if (closeOnClickOutside && !ref.current?.contains(event.target as Node)) { 51 | if (!isOpen) return; 52 | toggle(false); 53 | } 54 | }; 55 | window.addEventListener('click', handleOutsideClick); 56 | return () => window.removeEventListener('click', handleOutsideClick); 57 | }, [closeOnClickOutside, isOpen, ref, toggle]); 58 | 59 | // close modal when you click on "ESC" key 60 | useEffect(() => { 61 | const handleEscape = (event: KeyboardEvent) => { 62 | if (!isOpen) return; 63 | if (event.key === 'Escape') { 64 | toggle(false); 65 | } 66 | }; 67 | document.addEventListener('keyup', handleEscape); 68 | return () => document.removeEventListener('keyup', handleEscape); 69 | }, [isOpen, toggle]); 70 | 71 | // Put focus on modal dialogue, hide scrollbar and prevent body from moving when modal is open 72 | useEffect(() => { 73 | if (!isOpen) return; 74 | 75 | ref.current?.focus(); 76 | 77 | const html = document.documentElement; 78 | const scrollbarWidth = window.innerWidth - html.clientWidth; 79 | 80 | html.style.overflow = 'hidden'; 81 | html.style.paddingRight = `${scrollbarWidth}px`; 82 | 83 | return () => { 84 | document.documentElement.style.overflow = ''; 85 | document.documentElement.style.paddingRight = ''; 86 | }; 87 | }, [isOpen]); 88 | 89 | return ( 90 | <> 91 | {createPortal( 92 | isOpen && ( 93 | <> 94 |
    95 |
    96 |
    102 |
    109 | {children} 110 |
    111 |
    112 |
    113 | 114 | ), 115 | document.body, 116 | )} 117 | 118 | ); 119 | } 120 | 121 | function ModalHeader({ children }: Props) { 122 | return ( 123 |
    124 |

    {children}

    125 |
    126 | ); 127 | } 128 | 129 | function ModalBody({ children }: Props) { 130 | return
    {children}
    ; 131 | } 132 | 133 | function ModalFooter({ children }: Props) { 134 | return
    {children}
    ; 135 | } 136 | 137 | export { Modal, ModalHeader, ModalBody, ModalFooter }; 138 | -------------------------------------------------------------------------------- /src/components/Elements/navbar/navbar.tsx: -------------------------------------------------------------------------------- 1 | import { ReactNode } from 'react'; 2 | import { Link } from 'react-router-dom'; 3 | import { cn } from 'utils'; 4 | 5 | interface Props { 6 | children: ReactNode; 7 | } 8 | 9 | interface INavbarProps extends Props { 10 | className?: string; 11 | } 12 | 13 | interface INavbarBrandProps extends Props { 14 | href: string; 15 | } 16 | 17 | interface INavbarNavProps extends Props { 18 | position?: 'left' | 'center' | 'right'; 19 | } 20 | 21 | interface INavbarLinkProps extends INavbarBrandProps { 22 | active?: boolean; 23 | activeClass?: string; 24 | external?: boolean; 25 | className?: string; 26 | } 27 | 28 | const ngClass = { 29 | navbar: 'font-light h-16 relative flex items-center flex-row justify-start', 30 | brand: `inline-block cursor-pointer pl-1`, 31 | active: `text-purple-800`, 32 | toggler: `ml-auto flex lg:hidden px-3 py-3 text-5xl focus:outline-none focus:shadow rounded transition-colors hover:text-black border border-white hover:border-black`, 33 | link: `cursor-pointer px-4 text-gray-900 hover:text-black font-medium`, 34 | position: { 35 | center: `flex pl-0 mb-0 mx-auto pr-8 lg:hidden`, 36 | left: `hidden lg:pl-0 lg:mb-0 lg:mr-auto md:flex`, 37 | right: `hidden lg:pl-0 lg:mb-0 lg:ml-auto lg:flex`, 38 | }, 39 | }; 40 | 41 | function Navbar({ children, className }: INavbarProps) { 42 | return ; 43 | } 44 | 45 | // ! You can wrap the a tag with Link and pass href to Link if you are using either Create-React-App, Next.js or Gatsby 46 | function NavbarBrand({ children, href }: INavbarBrandProps) { 47 | return ( 48 | 49 | {children} 50 | 51 | ); 52 | } 53 | 54 | function NavbarToggler({ toggle }: { toggle: () => void }) { 55 | return ( 56 | 65 | ); 66 | } 67 | 68 | function NavbarNav({ children, position = 'right' }: INavbarNavProps) { 69 | return
      {children}
    ; 70 | } 71 | 72 | function NavbarItem({ children, className }: INavbarProps) { 73 | return
  • {children}
  • ; 74 | } 75 | 76 | function NavbarLink({ 77 | children, 78 | href, 79 | active, 80 | external, 81 | className, 82 | ...props 83 | }: INavbarLinkProps) { 84 | return ( 85 |
    86 | {external ? ( 87 | 88 | {children} 89 | 90 | ) : ( 91 | 92 | {children} 93 | 94 | )} 95 |
    96 | ); 97 | } 98 | 99 | export { 100 | Navbar, 101 | NavbarBrand, 102 | NavbarNav, 103 | NavbarItem, 104 | NavbarLink, 105 | NavbarToggler, 106 | }; 107 | -------------------------------------------------------------------------------- /src/components/Elements/sidenav/sidenav.tsx: -------------------------------------------------------------------------------- 1 | import { ReactNode, useEffect, useRef } from 'react'; 2 | import { cn } from 'utils'; 3 | 4 | interface Props { 5 | children: ReactNode; 6 | } 7 | 8 | interface LinkProps extends Props { 9 | href: string; 10 | } 11 | interface SidenavProps extends Props { 12 | open: boolean; 13 | position?: 'left' | 'right'; 14 | toggle: (value?: unknown) => void; 15 | closeOnClickOutside?: boolean; 16 | } 17 | 18 | const ngClass = { 19 | item: `flex justify-start cursor-pointer font-medium hover:text-gray-400 ml-8 mb-10`, 20 | closeIcon: `absolute top-1 focus:outline-none right-3 text-3xl text-white cursor-pointer`, 21 | position: { 22 | left: { 23 | open: `w-7/12 md:w-60 bg-gray-800 text-white overflow-x-hidden`, 24 | close: `w-0 bg-gray-800 text-white overflow-x-hidden`, 25 | default: `h-screen fixed z-20 top-0 left-0 transition-all ease duration-200`, 26 | }, 27 | right: { 28 | open: `w-7/12 md:w-60 bg-gray-800 text-white overflow-x-hidden`, 29 | close: `w-0 bg-gray-800 text-white overflow-x-hidden`, 30 | default: `right-0 h-screen fixed z-20 top-0 transition-all ease duration-200`, 31 | }, 32 | }, 33 | }; 34 | 35 | function Sidenav({ 36 | open, 37 | toggle, 38 | position = 'left', 39 | children, 40 | closeOnClickOutside, 41 | }: SidenavProps) { 42 | const ref = useRef(null); 43 | 44 | //close on click outside 45 | useEffect(() => { 46 | const handleOutsideClick = (event: MouseEvent) => { 47 | if (closeOnClickOutside && !ref.current?.contains(event.target as Node)) { 48 | if (closeOnClickOutside && !open) return; 49 | toggle(false); 50 | } 51 | }; 52 | window.addEventListener('mousedown', handleOutsideClick); 53 | return () => window.removeEventListener('mousedown', handleOutsideClick); 54 | }, [closeOnClickOutside, open, ref, toggle]); 55 | 56 | return ( 57 | 71 | ); 72 | } 73 | 74 | /* You can wrap the a tag with Link and pass href to Link if you are using either Create-React-App, Next.js or Gatsby */ 75 | function SidenavItem({ children, href }: LinkProps) { 76 | return ( 77 | 78 | {children} 79 | 80 | ); 81 | } 82 | 83 | export { Sidenav, SidenavItem }; 84 | -------------------------------------------------------------------------------- /src/components/Features/Chessboard/Board.tsx: -------------------------------------------------------------------------------- 1 | import { cn } from 'utils'; 2 | import type { CSSProperties } from 'react'; 3 | import { coordToSquare } from '../chess'; 4 | import { BoardSquare } from './BoardSquare'; 5 | import Notation from './Notation'; 6 | import type { ChessboardProps, Position, Square } from 'types'; 7 | import ChessPiece from './Piece'; 8 | 9 | /** Styling properties applied to the board element */ 10 | const basicBoardStyle: CSSProperties = { 11 | width: '100%', 12 | height: '100%', 13 | display: 'flex', 14 | flexWrap: 'wrap', 15 | }; 16 | 17 | /** Styling properties applied to each square element */ 18 | const squareStyle: CSSProperties = { width: '12.5%', height: '12.5%' }; 19 | 20 | const Board: React.FC = ({ 21 | orientation, 22 | showNotation, 23 | position, 24 | lightSquareStyle = { backgroundColor: 'rgb(240, 217, 181)' }, 25 | darkSquareStyle = { backgroundColor: 'rgb(181, 136, 99)' }, 26 | boardStyle, 27 | onPieceClick, 28 | onDragOverSquare, 29 | // onDrop, 30 | // onSquareRightClick, 31 | // width, 32 | onMouseOutSquare, 33 | onMouseOverSquare, 34 | onSquareClick, 35 | squareStyles, 36 | }) => { 37 | const squares = []; 38 | 39 | const hasPiece = (currentPosition: Position, square: Square): boolean => 40 | currentPosition && 41 | Object.keys(currentPosition) && 42 | Object.keys(currentPosition).includes(square); 43 | 44 | /** Render the board square appropriate for the coordinate given */ 45 | function renderSquare(col: number, row: number) { 46 | const square = coordToSquare(col, row); 47 | return ( 48 |
    49 | 61 | {showNotation && ( 62 | 69 | )} 70 | {hasPiece(position, square) && ( 71 | 79 | )} 80 | 81 |
    82 | ); 83 | } 84 | for (let row = 0; row <= 7; row++) { 85 | for (let col = 0; col <= 7; col++) { 86 | squares.push(renderSquare(col, 7 - row)); 87 | } 88 | } 89 | const rotate = orientation === 'black' ? 'rotate-180' : ''; 90 | return ( 91 |
    92 | {squares} 93 |
    94 | ); 95 | }; 96 | 97 | export default Board; 98 | -------------------------------------------------------------------------------- /src/components/Features/Chessboard/BoardSquare.tsx: -------------------------------------------------------------------------------- 1 | import { cn } from 'utils'; 2 | import type { CSSProperties, ReactNode } from 'react'; 3 | import { coordToSquare } from '../chess'; 4 | import type { Orientation, Square } from 'types'; 5 | 6 | export interface BoardSquareProps { 7 | col: number; 8 | row: number; 9 | orientation?: Orientation; 10 | children?: ReactNode; 11 | lightSquareStyle?: CSSProperties; 12 | darkSquareStyle?: CSSProperties; 13 | customSquareStyle?: CSSProperties; 14 | onMouseOverSquare?: (square: Square) => void; 15 | onMouseOutSquare?: (square: Square) => void; 16 | onDragOverSquare?: (square: Square) => void; 17 | onDrop?: (square: Square) => void; 18 | onSquareClick?: (square: Square) => void; 19 | onSquareRightClick?: (square: Square) => void; 20 | } 21 | 22 | const squareStyle = { 23 | width: '100%', 24 | height: '100%', 25 | }; 26 | 27 | export const BoardSquare: React.FC = ({ 28 | col, 29 | row, 30 | orientation = 'white', 31 | lightSquareStyle = { backgroundColor: 'rgb(240, 217, 181)' }, 32 | darkSquareStyle = { backgroundColor: 'rgb(181, 136, 99)' }, 33 | customSquareStyle, 34 | onMouseOverSquare = () => null, 35 | onMouseOutSquare = () => null, 36 | onSquareClick = () => null, 37 | children, 38 | }) => { 39 | const square = coordToSquare(col, row); 40 | /** Handles right and left clicks on a board square */ 41 | const handleClick = (event: React.MouseEvent) => { 42 | if (event.type === 'click') { 43 | console.log(`clicked on ${square} ${col},${row}`); 44 | onSquareClick(square); 45 | } else if (event.type === 'contextmenu') { 46 | event.preventDefault(); 47 | console.log(`right-clicked on ${square} ${col},${row}`); 48 | } 49 | }; 50 | const black = !((col + row) % 2 === 1); 51 | const backgroundColor = black ? darkSquareStyle : lightSquareStyle; 52 | const color = black ? 'white' : 'black'; 53 | const rotate = orientation === 'black' ? 'rotate-180' : ''; 54 | return ( 55 |
    onMouseOverSquare(square)} 59 | onMouseLeave={() => onMouseOutSquare(square)} 60 | onClick={handleClick} 61 | onContextMenu={handleClick} 62 | role="Space" 63 | data-testid={`(${col},${row})`} 64 | style={{ 65 | position: 'relative', 66 | width: '100%', 67 | height: '100%', 68 | }} 69 | > 70 |
    78 | {children} 79 |
    80 |
    81 | ); 82 | }; 83 | -------------------------------------------------------------------------------- /src/components/Features/Chessboard/CustomDragLayer.tsx: -------------------------------------------------------------------------------- 1 | import { CSSProperties } from 'react'; 2 | import type { XYCoord } from 'react-dnd'; 3 | import { useDragLayer } from 'react-dnd'; 4 | import type { CustomPieces, Square } from 'types'; 5 | 6 | type Props = { 7 | width: number; 8 | pieces: CustomPieces; 9 | wasPieceTouched: boolean; 10 | sourceSquare: Square; 11 | id?: string | number; 12 | }; 13 | 14 | const layerStyles: CSSProperties = { 15 | position: 'fixed', 16 | pointerEvents: 'none', 17 | zIndex: 10, 18 | left: 0, 19 | top: 0, 20 | }; 21 | 22 | const getItemStyle = ( 23 | currentOffset: XYCoord | null, 24 | wasPieceTouched: boolean, 25 | ): CSSProperties => { 26 | if (!currentOffset) return { display: 'none' }; 27 | 28 | let { x, y } = currentOffset; 29 | const transform = wasPieceTouched 30 | ? `translate(${x}px, ${y + -25}px) scale(2)` 31 | : `translate(${x}px, ${y}px)`; 32 | return { transform }; 33 | }; 34 | 35 | const CustomDragLayer = ({ 36 | // id, 37 | // pieces, 38 | // sourceSquare, 39 | // width, 40 | wasPieceTouched, 41 | }: Props) => { 42 | const { isDragging, currentOffset } = useDragLayer((monitor) => ({ 43 | item: monitor.getItem(), 44 | itemType: monitor.getItemType(), 45 | // initialOffset: monitor.getInitialSourceClientOffset(), 46 | currentOffset: monitor.getSourceClientOffset(), 47 | isDragging: monitor.isDragging(), 48 | })); 49 | 50 | return isDragging ? ( 51 |
    52 |
    53 |

    P

    54 | {/* {renderChessPiece({ 55 | width, 56 | pieces, 57 | piece: item.piece, 58 | isDragging, 59 | customDragLayerStyles: { opacity: 1 }, 60 | sourceSquare, 61 | })} */} 62 |
    63 |
    64 | ) : null; 65 | }; 66 | 67 | export default CustomDragLayer; 68 | -------------------------------------------------------------------------------- /src/components/Features/Chessboard/Notation.tsx: -------------------------------------------------------------------------------- 1 | import { Fragment, CSSProperties } from 'react'; 2 | import { Orientation } from 'types'; 3 | import { COLUMNS } from './helpers'; 4 | import { Property } from 'csstype'; 5 | 6 | //-----------------------------// 7 | type SquareProps = { 8 | row: number; 9 | col: number; 10 | width: number; 11 | orientation?: Orientation; 12 | }; 13 | type NotationProps = SquareProps & { 14 | lightSquareStyle?: CSSProperties; 15 | darkSquareStyle?: CSSProperties; 16 | }; 17 | type WhiteColor = { whiteColor: Property.BackgroundColor | undefined }; 18 | type BlackColor = { blackColor: Property.BackgroundColor | undefined }; 19 | 20 | //-----------------------------// 21 | // helper functions 22 | 23 | /** Get Row as a number */ 24 | const getRow = (orientation: Orientation, row: number) => 25 | orientation === 'white' ? row + 1 : row + 1; 26 | 27 | /** Get Column as a letter */ 28 | const getColumn = (orientation: Orientation, col: number) => 29 | orientation === 'white' ? COLUMNS[col] : COLUMNS[7 - col]; 30 | 31 | const renderBottomLeft = ({ 32 | orientation = 'white', 33 | row, 34 | width, 35 | col, 36 | whiteColor, 37 | }: SquareProps & WhiteColor) => { 38 | return ( 39 | 40 |
    48 | {getRow(orientation, row)} 49 |
    50 |
    58 | {getColumn(orientation, col)} 59 |
    60 |
    61 | ); 62 | }; 63 | 64 | const renderLetters = ({ 65 | orientation = 'white', 66 | width, 67 | col, 68 | whiteColor, 69 | blackColor, 70 | }: Omit & WhiteColor & BlackColor) => { 71 | return ( 72 |
    80 | {getColumn(orientation, col)} 81 |
    82 | ); 83 | }; 84 | 85 | const renderNumbers = ({ 86 | orientation = 'white', 87 | row, 88 | width, 89 | whiteColor, 90 | blackColor, 91 | isRow, 92 | isBottomLeftSquare, 93 | }: Omit & 94 | WhiteColor & 95 | BlackColor & { isRow: boolean; isBottomLeftSquare: boolean }) => { 96 | return ( 97 |
    112 | {getRow(orientation, row)} 113 |
    114 | ); 115 | }; 116 | 117 | const columnStyle = ({ 118 | col, 119 | width, 120 | blackColor, 121 | whiteColor, 122 | orientation, 123 | }: Omit & BlackColor & WhiteColor): CSSProperties => { 124 | let styleColor = col % 2 !== 0 ? blackColor : whiteColor; 125 | if (orientation === 'black') { 126 | styleColor = col % 2 !== 0 ? whiteColor : blackColor; 127 | } 128 | return { 129 | fontSize: width / 48, 130 | color: styleColor, 131 | }; 132 | }; 133 | 134 | const rowStyle = ({ 135 | row, 136 | width, 137 | blackColor, 138 | whiteColor, 139 | orientation, 140 | isBottomLeftSquare, 141 | isRow, 142 | }: Omit & 143 | WhiteColor & 144 | BlackColor & { 145 | isRow: boolean; 146 | isBottomLeftSquare: boolean; 147 | }): CSSProperties => { 148 | return { 149 | fontSize: width / 48, 150 | color: 151 | orientation === 'black' 152 | ? isRow && !isBottomLeftSquare && row % 2 === 0 153 | ? blackColor 154 | : whiteColor 155 | : isRow && !isBottomLeftSquare && row % 2 !== 0 156 | ? blackColor 157 | : whiteColor, 158 | }; 159 | }; 160 | 161 | const alphaStyle = (width: number): CSSProperties => ({ 162 | alignSelf: 'flex-end', 163 | paddingTop: width / 8 - width / 48 - 14, 164 | paddingLeft: width / 8 - width / 48 - 5, 165 | }); 166 | 167 | const numericStyle = (width: number): CSSProperties => ({ 168 | alignSelf: 'flex-start', 169 | paddingLeft: width / 48 - 10, 170 | }); 171 | 172 | const notationStyle: CSSProperties = { 173 | fontFamily: 'Helvetica Neue', 174 | zIndex: 3, 175 | position: 'absolute', 176 | }; 177 | 178 | //-----------------------------// 179 | 180 | /** Render the numbers and letters on the board squares */ 181 | const Notation = ({ 182 | row, 183 | col, 184 | orientation = 'white', 185 | width, 186 | lightSquareStyle = { backgroundColor: 'rgb(240, 217, 181)' }, 187 | darkSquareStyle = { backgroundColor: 'rgb(181, 136, 99)' }, 188 | }: NotationProps) => { 189 | const whiteColor = lightSquareStyle.backgroundColor; 190 | const blackColor = darkSquareStyle.backgroundColor; 191 | const isRow: boolean = 192 | (orientation === 'white' && col === 0) || 193 | (orientation === 'black' && col === 7); 194 | const isColumn: boolean = 195 | (orientation === 'white' && row === 0) || 196 | (orientation === 'black' && row === 7); 197 | const isBottomLeftSquare: boolean = isRow && isColumn; 198 | 199 | if (isBottomLeftSquare) { 200 | return renderBottomLeft({ col, row, width, orientation, whiteColor }); 201 | } 202 | 203 | if (isColumn) { 204 | return renderLetters({ col, width, orientation, whiteColor, blackColor }); 205 | } 206 | 207 | if (isRow) { 208 | return renderNumbers({ 209 | row, 210 | width, 211 | orientation, 212 | whiteColor, 213 | blackColor, 214 | isRow, 215 | isBottomLeftSquare, 216 | }); 217 | } 218 | 219 | return null; 220 | }; 221 | 222 | export default Notation; 223 | -------------------------------------------------------------------------------- /src/components/Features/Chessboard/Piece.tsx: -------------------------------------------------------------------------------- 1 | import type { Orientation, Piece, Square } from 'types'; 2 | import pieceSVG from './svg/chesspieces/standard'; 3 | import { cn } from 'utils'; 4 | 5 | type PieceProps = { 6 | piece: Piece | undefined; 7 | square: Square; 8 | id: number | string; 9 | width: number; 10 | connectDragSource?: () => void; 11 | onPieceClick?: (piece: Piece) => void; 12 | isDragging?: boolean; 13 | orientation?: Orientation; 14 | className?: string; 15 | }; 16 | 17 | export default function ChessPiece({ 18 | piece, 19 | // isDragging = false, 20 | width, 21 | onPieceClick, 22 | className = '', 23 | }: PieceProps) { 24 | return ( 25 |
    26 | {piece && ( 27 | onPieceClick} 33 | > 34 | {pieceSVG[piece]} 35 | 36 | )} 37 |
    38 | ); 39 | } 40 | -------------------------------------------------------------------------------- /src/components/Features/Chessboard/helpers.ts: -------------------------------------------------------------------------------- 1 | import type { FENpiece, Piece, Position, Square } from 'types'; 2 | 3 | export const ItemTypes = { PIECE: 'piece' }; 4 | export const COLUMNS = 'abcdefgh'.split(''); 5 | 6 | function isString(s: any): boolean { 7 | return typeof s === 'string'; 8 | } 9 | 10 | /** convert FEN string to a Position Object i.e. {'a1': 'wK',...} */ 11 | export function fenToObj(fen: string): Position { 12 | // cut off any move, castling, etc info from the end 13 | // we're only interested in position information 14 | fen = fen.replace(/ .+$/, ''); 15 | let [rows, currentRow] = [fen.split('/'), 8]; 16 | let position: Position = {}; 17 | for (let i = 0; i < 8; i++) { 18 | let [row, colIdx] = [rows[i].split(''), 0]; 19 | // loop through each character in the FEN section 20 | for (let j = 0; j < row.length; j++) { 21 | // number / empty squares 22 | if (row[j].search(/[1-8]/) !== -1) { 23 | let numEmptySquares = parseInt(row[j], 10); 24 | colIdx += numEmptySquares; 25 | } else { 26 | // piece 27 | let square = (COLUMNS[colIdx] + currentRow) as Square; 28 | position[square] = fenToPieceCode(row[j] as FENpiece); 29 | colIdx += 1; 30 | } 31 | } 32 | 33 | currentRow = currentRow - 1; 34 | } 35 | 36 | return position; 37 | } 38 | 39 | /** replace summed gaps between pieces with several '1' spacers */ 40 | function expandFenEmptySquares(fen: string) { 41 | return fen 42 | .replace(/8/g, '11111111') 43 | .replace(/7/g, '1111111') 44 | .replace(/6/g, '111111') 45 | .replace(/5/g, '11111') 46 | .replace(/4/g, '1111') 47 | .replace(/3/g, '111') 48 | .replace(/2/g, '11'); 49 | } 50 | 51 | /** check if the input string is a valid FEN string */ 52 | export function validFen(fen: string): boolean { 53 | if (!isString(fen)) return false; 54 | // cut off any move, castling, etc info from the end 55 | // we're only interested in position information 56 | fen = fen.replace(/ .+$/, ''); 57 | // expand the empty square numbers to just 1s 58 | fen = expandFenEmptySquares(fen); 59 | // FEN should be 8 sections separated by slashes 60 | let chunks = fen.split('/'); 61 | if (chunks.length !== 8) return false; 62 | // check each section 63 | for (let i = 0; i < 8; i++) { 64 | if (chunks[i].length !== 8 || chunks[i].search(/[^kqrnbpKQRNBP1]/) !== -1) { 65 | return false; 66 | } 67 | } 68 | return true; 69 | } 70 | 71 | /** convert FEN piece code to bP, wK, etc */ 72 | function fenToPieceCode(piece: FENpiece): Piece { 73 | // black piece 74 | if (piece.toLowerCase() === piece) { 75 | return ('b' + piece.toUpperCase()) as Piece; 76 | } 77 | // white piece 78 | return ('w' + piece.toUpperCase()) as Piece; 79 | } 80 | 81 | /** check if the input square string conforms to the square syntax i.e. 'a1' */ 82 | function validSquare(square: Square | string): boolean { 83 | return isString(square) && square.search(/^[a-h][1-8]$/) !== -1; 84 | } 85 | 86 | /** check if the input piece string conforms to the piece syntax i.e. 'bQ' */ 87 | function validPieceCode(code: Piece | string): boolean { 88 | return isString(code) && code.search(/^[bw][KQRNBP]$/) !== -1; 89 | } 90 | 91 | /** check if the input position object is valid */ 92 | export function validPositionObject(pos: Position | string): boolean { 93 | if (pos === null || typeof pos !== 'object') return false; 94 | 95 | for (let square in Object.keys(pos)) { 96 | if (!pos.hasOwnProperty(square)) continue; 97 | if ( 98 | !validSquare(square) || 99 | !validPieceCode(pos[square as Square] as string) 100 | ) { 101 | // either the object key isn't a square or value isn't a piece 102 | return false; 103 | } 104 | } 105 | return true; 106 | } 107 | 108 | /** replace placeholder '1' spacers with the sum of those spacers */ 109 | function squeezeFenEmptySquares(fen: string) { 110 | return fen 111 | .replace(/11111111/g, '8') 112 | .replace(/1111111/g, '7') 113 | .replace(/111111/g, '6') 114 | .replace(/11111/g, '5') 115 | .replace(/1111/g, '4') 116 | .replace(/111/g, '3') 117 | .replace(/11/g, '2'); 118 | } 119 | 120 | /** convert bP, wK, etc code to FEN structure */ 121 | function pieceCodeToFen(piece: Piece): FENpiece { 122 | let pieceCodeLetters = piece.split(''); 123 | // white piece 124 | if (pieceCodeLetters[0] === 'w') { 125 | return pieceCodeLetters[1].toUpperCase() as FENpiece; 126 | } 127 | // black piece 128 | return pieceCodeLetters[1].toLowerCase() as FENpiece; 129 | } 130 | 131 | /** position object to FEN string 132 | * returns false if the obj is not a valid position object */ 133 | export function objToFen(obj: Position): false | string { 134 | if (!validPositionObject(obj)) return false; 135 | let [fen, currentRow] = ['', 8]; 136 | for (let i = 0; i < 8; i++) { 137 | for (let j = 0; j < 8; j++) { 138 | let square = (COLUMNS[j] + currentRow) as Square; 139 | // if piece exists add it, otherwise add a '1' spacer 140 | fen += obj[square] ? pieceCodeToFen(obj[square] as Piece) : 1; 141 | } 142 | if (i !== 7) fen += '/'; 143 | currentRow -= 1; 144 | } 145 | // squeeze the empty numbers together 146 | return squeezeFenEmptySquares(fen); 147 | } 148 | -------------------------------------------------------------------------------- /src/components/Features/Chessboard/index.tsx: -------------------------------------------------------------------------------- 1 | import { useState, useEffect, CSSProperties } from 'react'; 2 | import type { ChessboardProps } from 'types'; 3 | import { DndProvider } from 'react-dnd'; 4 | import { HTML5Backend } from 'react-dnd-html5-backend'; 5 | import Board from './Board'; 6 | 7 | const ChessBoard: React.FC = ({ 8 | className = '', 9 | // id = 0, 10 | position = {}, 11 | // pieces = {}, 12 | width = 560, 13 | orientation = 'white', 14 | showNotation = true, 15 | // sparePieces = false, 16 | // draggable = true, 17 | // undo = false, 18 | // dropOffBoard = "snapback", 19 | // transitionDuration = 300, 20 | boardStyle = {}, 21 | lightSquareStyle = { backgroundColor: 'rgb(240, 217, 181)' }, 22 | darkSquareStyle = { backgroundColor: 'rgb(181, 136, 99)' }, 23 | // dropSquareStyle = { boxShadow: "inset 0 0 1px 4px yellow" }, 24 | // allowDrag = () => true, 25 | // calcWidth, 26 | // getPosition, 27 | onDragOverSquare, 28 | onDrop, 29 | onMouseOutSquare, 30 | onMouseOverSquare, 31 | onPieceClick, 32 | onSquareClick, 33 | onSquareRightClick, 34 | // roughSquare, 35 | squareStyles, 36 | }) => { 37 | const [screenWidth, setScreenWidth] = useState(width); 38 | // const [screenHeight, setScreenHeight] = useState(width); 39 | 40 | useEffect(() => { 41 | const updateWindowDimensions = () => { 42 | setScreenWidth(window.innerWidth); 43 | }; 44 | updateWindowDimensions(); 45 | window.addEventListener('resize', updateWindowDimensions); 46 | return () => { 47 | window.removeEventListener('resize', updateWindowDimensions); 48 | }; 49 | }, []); 50 | 51 | const containerStyle: CSSProperties = { 52 | width: 500, 53 | height: 500, 54 | // border: '1px solid gray', 55 | }; 56 | 57 | return ( 58 | 59 |
    60 | 78 | {/* */} 85 |
    86 |
    87 | ); 88 | }; 89 | 90 | export default ChessBoard; 91 | -------------------------------------------------------------------------------- /src/components/Features/Chessboard/readme.md: -------------------------------------------------------------------------------- 1 | # Chessboardjsx 2 | 3 | The MIT License (MIT) 4 | Copyright (c) 2018 William J. Bashelor 5 | 6 | These assets are taken from the nolonger supported Chessboardjsx project and converted to TypeScript. 7 | -------------------------------------------------------------------------------- /src/components/Features/Chessboard/svg/whiteKing.tsx: -------------------------------------------------------------------------------- 1 | export default { 2 | whiteKing: ( 3 | 19 | 20 | 27 | 28 | 29 | ), 30 | }; 31 | -------------------------------------------------------------------------------- /src/components/Features/chess/index.tsx: -------------------------------------------------------------------------------- 1 | import { notify } from 'services/notifications'; 2 | import { invoke } from '@tauri-apps/api/core'; 3 | import { 4 | BoardStateArray, 5 | RustPiece, 6 | PieceType, 7 | PositionStyles, 8 | MoveList, 9 | MetaGame, 10 | MoveType, 11 | Square, 12 | Position, 13 | Piece, 14 | } from 'types'; 15 | import { 16 | Button, 17 | Modal, 18 | ModalBody, 19 | ModalFooter, 20 | ModalHeader, 21 | } from 'components/Elements'; 22 | 23 | // https://chessboardjsx.com/ 24 | 25 | //!NOTE coordinates are column then row 26 | 27 | /** Convert a number to its corresponding alphabetical character */ 28 | const numToLetter = (num: number) => (num + 9).toString(36); 29 | 30 | /** Convert a row, col coordinate into a chessboard square i.e. (2,1) = b3 */ 31 | const coordToSquare = (col: number, row: number) => { 32 | return `${numToLetter(col + 1)}${row + 1}` as Square; 33 | }; 34 | 35 | const parseBoardState = (boardArray: BoardStateArray) => { 36 | // get 8x8 array of strings 37 | 38 | const rustToPiece = (pieceObj: RustPiece) => { 39 | let color: 'w' | 'b' = 40 | Object.values(pieceObj)[0][0] === 'Black' ? 'b' : 'w'; 41 | let type = Object.keys(pieceObj)[0] as PieceType; 42 | switch (type) { 43 | case 'Queen': 44 | return `${color}Q` as Piece; 45 | case 'King': 46 | return `${color}K` as Piece; 47 | case 'Bishop': 48 | return `${color}B` as Piece; 49 | case 'Knight': 50 | return `${color}N` as Piece; 51 | case 'Rook': 52 | return `${color}R` as Piece; 53 | case 'Pawn': 54 | return `${color}P` as Piece; 55 | default: 56 | return undefined; 57 | } 58 | }; 59 | let state = boardArray.reduce((result, col, coli) => { 60 | col.forEach((sq, rowi) => { 61 | let piece = rustToPiece(sq as unknown as RustPiece); 62 | if (piece) result[coordToSquare(coli, rowi)] = piece; 63 | }); 64 | return result; 65 | }, {}); 66 | // alert(JSON.stringify(state)); 67 | return state; 68 | }; 69 | 70 | const highlightSquares = ( 71 | moveOptions: MoveList, 72 | square?: Square, 73 | ): PositionStyles => { 74 | // turn this array of squares into an object with cssProperties defined 75 | const props = moveOptions.reduce((result, move) => { 76 | const [moveType, col, row] = [move[1], move[0][0], move[0][1]]; 77 | const highlightColour = (moveType: MoveType) => { 78 | console.log(moveType); 79 | switch (moveType) { 80 | case 'Move': 81 | return '0 0 8px #a5eed9, inset 0 0 1px 4px #a5eed9'; 82 | case 'Capture': 83 | return '0 0 8px #ea4c89, inset 0 0 1px 4px #ea4c89'; 84 | case 'Castle': 85 | return '0 0 8px #11dd71, inset 0 0 1px 4px #11dd71'; 86 | case 'Double': 87 | return '0 0 8px #11dd71, inset 0 0 1px 4px #11dd71'; 88 | case 'EnPassant': 89 | return '0 0 8px #004d71, inset 0 0 1px 4px #004d71'; 90 | default: 91 | return '0 0 8px black, inset 0 1px 4px black'; 92 | } 93 | }; 94 | 95 | result[coordToSquare(col, row)] = { 96 | boxShadow: highlightColour(moveType), 97 | width: '100%', 98 | height: '100%', 99 | }; 100 | return result; 101 | }, {}); 102 | if (square) { 103 | props[square] = { 104 | boxShadow: '0 0 8px black, inset 0 1px 4px black', 105 | width: '100%', 106 | height: '100%', 107 | }; 108 | // { boxShadow: 'inset 0 0 1px 4px rgb(255, 255, 0)' }; 109 | } 110 | return props; 111 | }; 112 | 113 | const AskNewGame: React.FC<{ 114 | setPosition: (position: Position) => void; 115 | setGameMeta: (meta: MetaGame) => void; 116 | isOpen: boolean; 117 | toggle: () => void; 118 | }> = ({ setPosition, setGameMeta, isOpen, toggle }) => { 119 | return ( 120 | 121 | Welcome to Tauri Chess 122 | Do you want to start a new game? 123 | 124 | 127 | 140 | 141 | 142 | ); 143 | }; 144 | 145 | const getGameState = (setPosition: (positions: Position) => void) => { 146 | invoke('get_state').then((board) => { 147 | console.log(board); 148 | setPosition(parseBoardState(board)); 149 | }); 150 | }; 151 | 152 | export { 153 | parseBoardState, 154 | highlightSquares, 155 | AskNewGame, 156 | getGameState, 157 | coordToSquare, 158 | }; 159 | -------------------------------------------------------------------------------- /src/components/Features/chess/promotions.tsx: -------------------------------------------------------------------------------- 1 | import { Button, Modal, ModalBody, ModalHeader } from 'components/Elements'; 2 | import { invoke } from '@tauri-apps/api/core'; 3 | 4 | type PromotionProps = { 5 | toggle: (isOpen?: boolean | undefined) => void; 6 | isOpen: boolean; 7 | }; 8 | 9 | export default function Promotions({ toggle, isOpen }: PromotionProps) { 10 | const promotions = ['Queen', 'Knight', 'Rook', 'Bishop']; 11 | return ( 12 | 13 | Pick Promotion 14 | 15 |
    16 | {promotions.map((promotion, index) => ( 17 |
    18 | 29 |
    30 | ))} 31 |
    32 |
    33 |
    34 | ); 35 | } 36 | -------------------------------------------------------------------------------- /src/components/Layout/Footer/Copywrite.tsx: -------------------------------------------------------------------------------- 1 | import { IoLogoGithub } from 'react-icons/io'; 2 | import pjson from '../../../../package.json'; 3 | import { useEffect, useState } from 'react'; 4 | import { getVersion } from '@tauri-apps/api/app'; 5 | 6 | export default function Copywrite() { 7 | const [version, setVersion] = useState(''); 8 | useEffect(() => { 9 | getVersion().then((version) => setVersion(version)); 10 | }, []); 11 | return ( 12 | 23 | ); 24 | } 25 | -------------------------------------------------------------------------------- /src/components/Layout/Footer/Footer.tsx: -------------------------------------------------------------------------------- 1 | import { cn } from 'utils'; 2 | import Copywrite from './Copywrite'; 3 | 4 | export default function Footer() { 5 | return ( 6 |
    9 | 10 |
    11 | ); 12 | } 13 | -------------------------------------------------------------------------------- /src/components/Layout/Header/Header.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect } from 'react'; 2 | import { cn } from 'utils'; 3 | import debounce from 'debounce'; 4 | import { 5 | Navbar, 6 | NavbarBrand, 7 | NavbarItem, 8 | NavbarToggler, 9 | NavbarLink, 10 | NavbarNav, 11 | } from 'components/Elements'; 12 | import { useToggle } from '../helpers/context'; 13 | import Logo from '../Logo'; 14 | import { routes } from 'routes'; 15 | import { useScrollDirection } from 'hooks'; 16 | 17 | type HeaderProps = { 18 | appName?: string; 19 | }; 20 | 21 | function Header({}: HeaderProps) { 22 | const { toggle } = useToggle(); 23 | const scrollDirection = useScrollDirection({ 24 | initialDirection: 'up', // this is so the navbar is present on page load 25 | thresholdPixels: 50, // this means we need to move a certain speed before the nav displays 26 | }); 27 | 28 | useEffect(() => { 29 | console.log(scrollDirection); 30 | }, [scrollDirection]); 31 | 32 | return ( 33 |
    41 | 42 | 43 | 44 | 45 | 46 | {routes.map(({ title, path }) => { 47 | return ( 48 | 49 | 50 | {title} 51 | 52 | 53 | ); 54 | })} 55 | 56 | 57 | 58 |
    59 | ); 60 | } 61 | 62 | export default Header; 63 | -------------------------------------------------------------------------------- /src/components/Layout/Layout.tsx: -------------------------------------------------------------------------------- 1 | import { cn } from 'utils'; 2 | import Footer from './Footer/Footer'; 3 | import Header from './Header/Header'; 4 | import LayoutProvider from './helpers/context'; 5 | import Overlay from './helpers/overlay'; 6 | import SideNav from './Sidebar/sideNavbar'; 7 | import { ToastContainer } from 'react-toastify'; 8 | import { useEffect, useState } from 'react'; 9 | import { getVersion } from '@tauri-apps/api/app'; 10 | 11 | type LayoutProps = { 12 | children: React.ReactNode; 13 | }; 14 | 15 | export const Layout: React.FC = ({ children }) => { 16 | useEffect(() => { 17 | getVersion().then((version) => setVersion(version)); 18 | }, []); 19 | 20 | const [version, setVersion] = useState(''); 21 | return ( 22 | 23 | 24 |
    25 | 26 |
    27 | 28 |
    29 |
    {children}
    30 |
    31 |
    32 |
    33 |
    34 | ); 35 | }; 36 | -------------------------------------------------------------------------------- /src/components/Layout/Logo.tsx: -------------------------------------------------------------------------------- 1 | import { cn } from 'utils'; 2 | import logo from 'assets/tauri.svg'; 3 | 4 | interface Props { 5 | className?: string; 6 | height?: number; 7 | width?: number; 8 | altText?: string; 9 | } 10 | 11 | const Logo: React.FC = ({ 12 | className = '', 13 | height = 30, 14 | width = 30, 15 | altText = 'logo', 16 | }) => { 17 | return ( 18 | {altText} 30 | ); 31 | }; 32 | 33 | export default Logo; 34 | -------------------------------------------------------------------------------- /src/components/Layout/Sidebar/items.tsx: -------------------------------------------------------------------------------- 1 | import { routes } from 'routes'; 2 | import { NavbarLink, NavbarItem } from 'components/Elements'; 3 | import { useLocation } from 'react-router-dom'; 4 | 5 | const SidenavItems = () => { 6 | const { pathname } = useLocation(); 7 | return ( 8 |
      9 | {routes.map(({ title, path }) => { 10 | return ( 11 | 12 | 16 | {title} 17 | 18 | 19 | ); 20 | })} 21 |
    22 | ); 23 | }; 24 | 25 | export default SidenavItems; 26 | -------------------------------------------------------------------------------- /src/components/Layout/Sidebar/sideNavbar.tsx: -------------------------------------------------------------------------------- 1 | // https://dev.to/fayaz/making-a-navigation-drawer-sliding-sidebar-with-tailwindcss-blueprint-581l 2 | import SidenavItems from './items'; 3 | import { useToggle } from '../helpers/context'; 4 | import { 5 | Drawer, 6 | DrawerHeader, 7 | DrawerBody, 8 | DrawerFooter, 9 | } from 'components/Elements'; 10 | import Logo from '../Logo'; 11 | 12 | const SideNavigation = ({ version }: { version: string }) => { 13 | const { open, toggle } = useToggle(); 14 | 15 | return ( 16 | 17 | 18 | 19 |

    Chess

    20 |
    21 | 22 | 23 | 24 | Version: {version} 25 |
    26 | ); 27 | }; 28 | 29 | export default SideNavigation; 30 | -------------------------------------------------------------------------------- /src/components/Layout/alert.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useRef, useState } from 'react'; 2 | 3 | const Alert = () => { 4 | const timeOut = useRef(null); 5 | const [seen, setSeen] = useState( 6 | localStorage.getItem('info_seen') || 'false', 7 | ); 8 | 9 | const onClose = () => { 10 | setSeen('true'); 11 | localStorage.setItem('info_seen', 'true'); 12 | }; 13 | 14 | useEffect(() => { 15 | timeOut.current = setTimeout(() => onClose(), 12000); 16 | 17 | return () => { 18 | if (timeOut.current) clearTimeout(timeOut.current); 19 | }; 20 | }, []); 21 | 22 | return ( 23 | <> 24 | {seen !== 'true' && ( 25 |
    26 |
    27 | Kimia-UI is now using Tailwind CSS v3, introducing some small 28 | changes. Please use JIT mode or update to Tailwind CSS v3 29 | 32 |
    33 |
    34 | )} 35 | 36 | ); 37 | }; 38 | 39 | export default Alert; 40 | -------------------------------------------------------------------------------- /src/components/Layout/helpers/context.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | useEffect, 3 | useRef, 4 | useContext, 5 | useState, 6 | useCallback, 7 | createContext, 8 | ReactNode, 9 | } from 'react'; 10 | 11 | interface ContextProps { 12 | open: boolean; 13 | ref: any; 14 | toggle: () => void; 15 | } 16 | // create new context 17 | const Context = createContext({ 18 | open: false, 19 | ref: null, 20 | toggle: () => null, 21 | }); 22 | 23 | function LayoutProvider({ children }: { children: ReactNode }) { 24 | const [open, setOpen] = useState(false); 25 | const ref = useRef(null); 26 | // const router = useRouter(); 27 | 28 | const toggle = useCallback(() => { 29 | console.log('sidebar toggle'); 30 | 31 | setOpen(!open); 32 | }, [open]); 33 | 34 | // close side navigation on click outside when viewport is less than 1024px 35 | useEffect(() => { 36 | const handleOutsideClick = (event: MouseEvent) => { 37 | if (window.innerWidth < 1024) { 38 | if (!ref.current?.contains(event.target as Node)) { 39 | if (!open) return; 40 | setOpen(false); 41 | } 42 | } 43 | }; 44 | window.addEventListener('click', handleOutsideClick); 45 | return () => window.removeEventListener('click', handleOutsideClick); 46 | }, [open, ref]); 47 | 48 | return ( 49 | 50 | {children} 51 | 52 | ); 53 | } 54 | 55 | // custom hook to consume all context values { open, ref, toggle } 56 | function useToggle() { 57 | return useContext(Context); 58 | } 59 | 60 | export default LayoutProvider; 61 | 62 | export { useToggle }; 63 | -------------------------------------------------------------------------------- /src/components/Layout/helpers/overlay.tsx: -------------------------------------------------------------------------------- 1 | import { useToggle } from './context'; 2 | 3 | const style = { 4 | overlay: `fixed h-screen left-0 backdrop-blur-sm top-0 w-screen z-30 lg:hidden`, 5 | }; 6 | 7 | // The overlay will only be visible on small screens to emphasize the focus on the side navigation when it is open. 8 | const Overlay = () => { 9 | const { open } = useToggle(); 10 | return
    ; 11 | }; 12 | 13 | export default Overlay; 14 | -------------------------------------------------------------------------------- /src/components/index.ts: -------------------------------------------------------------------------------- 1 | export * from './Layout/Layout'; 2 | -------------------------------------------------------------------------------- /src/hooks/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * https://usehooks.com/ 3 | * https://usehooks-ts.com/ 4 | */ 5 | export { default as useEventListener } from './useEventListener'; 6 | export { default as useOnClickOutside } from './useOnClickOutside'; 7 | export { default as useScrollDirection } from './useScrollDirection'; 8 | export { default as useToggle } from './useToggle'; 9 | -------------------------------------------------------------------------------- /src/hooks/useEventListener.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * https://usehooks-ts.com/react-hook/use-event-listener 3 | * Use EventListener easily with this React Hook. 4 | * It takes as parameters a eventName, a call-back functions (handler) and optionally a reference element. 5 | */ 6 | 7 | import { RefObject, useEffect, useRef } from 'react'; 8 | 9 | function useEventListener( 10 | eventName: keyof WindowEventMap | string, // string to allow custom event 11 | handler: (event: Event) => void, 12 | element?: RefObject, 13 | ) { 14 | // Create a ref that stores handler 15 | const savedHandler = useRef<(event: Event) => void | null>(null); 16 | 17 | useEffect(() => { 18 | // Define the listening target 19 | const targetElement: T | Window = element?.current || window; 20 | if (!(targetElement && targetElement.addEventListener)) { 21 | return; 22 | } 23 | 24 | // Update saved handler if necessary 25 | if (savedHandler.current !== handler) { 26 | savedHandler.current = handler; 27 | } 28 | 29 | // Create event listener that calls handler function stored in ref 30 | const eventListener = (event: Event) => { 31 | if (!!savedHandler?.current) { 32 | savedHandler.current(event); 33 | } 34 | }; 35 | 36 | targetElement.addEventListener(eventName, eventListener); 37 | 38 | // Remove event listener on cleanup 39 | return () => { 40 | targetElement.removeEventListener(eventName, eventListener); 41 | }; 42 | }, [eventName, element, handler]); 43 | } 44 | 45 | export default useEventListener; 46 | -------------------------------------------------------------------------------- /src/hooks/useOnClickOutside.ts: -------------------------------------------------------------------------------- 1 | /** 2 | React hook for listening for clicks outside of a specified element (see useRef). 3 | This can be useful for closing a modal, a dropdown menu etc. 4 | */ 5 | 6 | import { RefObject } from 'react'; 7 | import useEventListener from './useEventListener'; 8 | 9 | type Handler = (event: MouseEvent) => void; 10 | 11 | function useOnClickOutside( 12 | ref: RefObject, 13 | handler: Handler, 14 | mouseEvent: 'mousedown' | 'mouseup' = 'mousedown', 15 | ): void { 16 | useEventListener(mouseEvent, (event) => { 17 | const el = ref?.current; 18 | 19 | // Do nothing if clicking ref's element or descendent elements 20 | if (!el || el.contains(event.target as Node)) { 21 | return; 22 | } 23 | 24 | // Explicit type for "mousedown" event. 25 | handler(event as unknown as MouseEvent); 26 | }); 27 | } 28 | 29 | export default useOnClickOutside; 30 | -------------------------------------------------------------------------------- /src/hooks/useScrollDirection.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Inspired by code from https://github.com/bchiang7/v4 converted to typeScript and tidied up. 3 | * This hook will observe the direction the page is being scrolled, to (i.e.) show or hide a Navbar. 4 | * */ 5 | 6 | import { useState, useEffect } from 'react'; 7 | 8 | type ScrollDir = 'up' | 'down'; 9 | 10 | interface IScrollDir { 11 | initialDirection?: ScrollDir; 12 | thresholdPixels?: number; 13 | watchScroll?: boolean; 14 | } 15 | 16 | const useScrollDirection = ({ 17 | initialDirection = 'down', 18 | thresholdPixels = 0, 19 | watchScroll = true, 20 | }: IScrollDir) => { 21 | const [scrollDir, setScrollDir] = useState(initialDirection); 22 | 23 | useEffect(() => { 24 | let lastScrollY: number = window.pageYOffset; 25 | let ticking = false; 26 | 27 | const updateScrollDir = () => { 28 | const scrollY = window.pageYOffset; 29 | 30 | if (Math.abs(scrollY - lastScrollY) < thresholdPixels) { 31 | // We haven't exceeded the threshold 32 | ticking = false; 33 | return; 34 | } 35 | 36 | setScrollDir(scrollY > lastScrollY ? 'down' : 'up'); 37 | lastScrollY = scrollY > 0 ? scrollY : 0; 38 | ticking = false; 39 | }; 40 | 41 | const onScroll = () => { 42 | if (!ticking) { 43 | window.requestAnimationFrame(updateScrollDir); 44 | ticking = true; 45 | } 46 | }; 47 | 48 | if (watchScroll) { 49 | // Bind the scroll handler if `watchScroll` is set to true. 50 | window.addEventListener('scroll', onScroll); 51 | } else { 52 | // If `watchScroll` is set to false reset the scroll direction. 53 | setScrollDir(initialDirection); 54 | } 55 | 56 | return () => window.removeEventListener('scroll', onScroll); 57 | }, [initialDirection, thresholdPixels, watchScroll]); 58 | 59 | return scrollDir; 60 | }; 61 | 62 | export default useScrollDirection; 63 | -------------------------------------------------------------------------------- /src/hooks/useToggle.ts: -------------------------------------------------------------------------------- 1 | import { useState } from 'react'; 2 | 3 | // const useToggle = () => { 4 | // const [isOpen, setIsOpen] = useState(false); 5 | 6 | // const toggle = (isOpen?: boolean) => { 7 | // setIsOpen(!isOpen); 8 | // }; 9 | // return { toggle, isOpen }; 10 | // }; 11 | 12 | const useToggle = (initialValue = false) => { 13 | const [value, setValue] = useState(initialValue); 14 | const toggleValue = (state?: boolean) => setValue(state ? state : !value); 15 | return [value, toggleValue] as const; 16 | }; 17 | 18 | export default useToggle; 19 | -------------------------------------------------------------------------------- /src/main.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom/client'; 3 | import AppRoutes from 'routes'; 4 | import { BrowserRouter } from 'react-router-dom'; 5 | import { Layout } from 'components'; 6 | 7 | import './style/global.css'; 8 | import './style/tailwind.css'; 9 | 10 | ReactDOM.createRoot(document.getElementById('root')!).render( 11 | 12 | 13 | 14 | 15 | 16 | 17 | , 18 | ); 19 | -------------------------------------------------------------------------------- /src/pages/AboutPage.tsx: -------------------------------------------------------------------------------- 1 | const AboutPage: React.FC = () => ( 2 |
    3 |

    About Page

    4 |
    5 |
      6 |
    • Written by: James Sizeland
    • 7 |
    • Uses fork of chessboardjsx React library
    • 8 |
    • Uses fork of Kimia-UI library
    • 9 |
    • Built using Tauri and Rust for the backend
    • 10 |
    • Built using Vitejs TypeScript React for the frontend
    • 11 |
    12 |
    13 |
    14 | ); 15 | 16 | export default AboutPage; 17 | -------------------------------------------------------------------------------- /src/pages/HomePage.tsx: -------------------------------------------------------------------------------- 1 | import { useState, useEffect } from 'react'; 2 | import type { 3 | Position, 4 | Square, 5 | BoardStateArray, 6 | MoveList, 7 | PositionStyles, 8 | MetaGame, 9 | } from 'types'; 10 | import { Button } from 'components/Elements'; 11 | import { 12 | coordToSquare, 13 | highlightSquares, 14 | parseBoardState, 15 | AskNewGame, 16 | } from 'components/Features/chess'; 17 | import Chessboard from 'components/Features/Chessboard'; 18 | import { useToggle } from 'hooks'; 19 | import { cn } from 'utils'; 20 | import Promotions from 'components/Features/chess/promotions'; 21 | import { invoke } from '@tauri-apps/api/core'; 22 | import { listen } from '@tauri-apps/api/event'; 23 | 24 | const HomePage: React.FC = () => { 25 | const [newGameIsOpen, newGameToggle] = useToggle(true); 26 | const [promoterIsOpen, promoterToggle] = useToggle(false); 27 | const [position, setPosition] = useState({}); 28 | const [gameMeta, setGameMeta] = useState({ 29 | score: 0, 30 | turn: 0, 31 | game_over: false, 32 | en_passant: null, 33 | promotable_pawn: null, 34 | white_king: { 35 | piece: { 36 | King: ['White', true, false, false], 37 | }, 38 | square: [4, 0], 39 | }, 40 | black_king: { 41 | piece: { 42 | King: ['Black', true, false, false], 43 | }, 44 | square: [4, 7], 45 | }, 46 | }); 47 | const [squareStyles, setSquareStyles] = useState(); 48 | // const [dragStyles, setDragStyles] = useState<{}>(); 49 | const [whiteTurn, setWhiteTurn] = useState(true); 50 | const [hoveredSquare, setHoveredSquare] = useState(); 51 | const [rotation, setRotation] = useState(false); 52 | 53 | useEffect(() => { 54 | // ask if we want to start a new game 55 | invoke('get_state').then((board) => 56 | setPosition(parseBoardState(board)), 57 | ); 58 | invoke('get_score').then((meta) => setGameMeta(meta)); 59 | // listen for promotion requests 60 | const promRef = listen('promotion', () => promoterToggle()); 61 | // listen for unexpected board state updates 62 | const boardRef = listen('board', (event) => { 63 | console.log('Rust requests a boardstate update'); 64 | setPosition(parseBoardState(event.payload)); 65 | }); 66 | return () => { 67 | promRef.then((f) => f()); 68 | boardRef.then((f) => f()); 69 | }; 70 | }, []); 71 | 72 | return ( 73 |
    74 |

    Game

    75 |
    76 | 82 | 83 | { 91 | // stop unnecessary repeats of this function call 92 | if (square !== hoveredSquare) { 93 | invoke('hover_square', { square: square }).then((sq) => 94 | setSquareStyles(highlightSquares(sq, square)), 95 | ); 96 | } 97 | setHoveredSquare(square); 98 | }} 99 | boardStyle={{ 100 | borderRadius: '5px', 101 | boxShadow: `0 5px 15px rgba(0,0,0,0.5)`, 102 | }} 103 | squareStyles={squareStyles} 104 | dropSquareStyle={undefined} 105 | onSquareClick={(square) => { 106 | if (!gameMeta.game_over) { 107 | invoke<[MoveList, BoardStateArray, MetaGame]>('click_square', { 108 | square: square, 109 | }).then(([sq, board, gameMeta]) => { 110 | setSquareStyles(highlightSquares(sq, square)); 111 | setPosition(parseBoardState(board)); 112 | setGameMeta(gameMeta); 113 | if (rotation) { 114 | gameMeta.turn % 2 == 0 115 | ? setWhiteTurn(true) 116 | : setWhiteTurn(false); 117 | } 118 | if (gameMeta.game_over) newGameToggle(); // ask if we want to start a new game 119 | }); 120 | } 121 | }} 122 | /> 123 |
    124 | {/* Game State Row */} 125 |
    126 |

    133 | white king:{' '} 134 | {coordToSquare( 135 | gameMeta.white_king.square[0], 136 | gameMeta.white_king.square[1], 137 | )} 138 |

    139 |

    146 | black king:{' '} 147 | {coordToSquare( 148 | gameMeta.black_king.square[0], 149 | gameMeta.black_king.square[1], 150 | )} 151 |

    152 |

    153 | score: {gameMeta.score}, turn: {gameMeta.turn} ( 154 | {gameMeta.turn % 2 == 0 ? 'White' : 'Black'}) 155 |

    156 | 171 |
    172 |
    173 | ); 174 | }; 175 | export default HomePage; 176 | -------------------------------------------------------------------------------- /src/pages/SummaryPage.tsx: -------------------------------------------------------------------------------- 1 | import { useState } from 'react'; 2 | 3 | const SummaryPage: React.FC = () => { 4 | const [score] = useState(0); 5 | return ( 6 |
    7 |

    Summary Page

    8 |
    9 |

    {score}

    10 |
    11 |
    12 | ); 13 | }; 14 | 15 | export default SummaryPage; 16 | -------------------------------------------------------------------------------- /src/pages/TestPage.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect } from 'react'; 2 | import { 3 | Button, 4 | Modal, 5 | ModalBody, 6 | ModalHeader, 7 | ModalFooter, 8 | } from 'components/Elements'; 9 | import { useToggle } from 'hooks'; 10 | import { listen } from '@tauri-apps/api/event'; 11 | import { invoke } from '@tauri-apps/api/core'; 12 | 13 | const TestPage: React.FC = () => { 14 | useEffect(() => { 15 | const unlisten = listen('test', (event) => { 16 | console.log(event); 17 | }); 18 | return () => { 19 | unlisten.then((f) => f()); 20 | }; 21 | }, []); 22 | 23 | // const [score, setScore] = useState(0); 24 | const [isOpen1, toggle1] = useToggle(false); 25 | const [isOpen2, toggle2] = useToggle(false); 26 | return ( 27 |
    28 |

    Test Page

    29 |
    30 | 31 | Popup 32 | 33 | Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do 34 | eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim 35 | ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut 36 | aliquip ex ea commodo consequat. 37 | 38 | 39 | 40 | 41 | 42 | 43 | Popup 44 | 45 | Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do 46 | eiusmod tempor incididunt ut fdsfasdjkfasdf adsfasdfsdfasdf 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 57 | {/*

    {score}

    */} 58 |
    59 |
    60 | ); 61 | }; 62 | 63 | export default TestPage; 64 | -------------------------------------------------------------------------------- /src/pages/index.ts: -------------------------------------------------------------------------------- 1 | export { default as HomePage } from './HomePage'; 2 | export { default as AboutPage } from './AboutPage'; 3 | export { default as TestPage } from './TestPage'; 4 | -------------------------------------------------------------------------------- /src/routes/index.tsx: -------------------------------------------------------------------------------- 1 | import { Routes, Route } from 'react-router-dom'; 2 | import { HomePage, AboutPage, TestPage } from 'pages'; 3 | import { checkEnv } from 'utils'; 4 | 5 | type RouteType = { 6 | title: string; 7 | path: string; 8 | element: React.ReactNode; 9 | }; 10 | 11 | const publicRoutes: RouteType[] = [ 12 | { title: 'Home', path: '/', element: }, 13 | { title: 'About', path: '/about', element: }, 14 | ]; 15 | 16 | const devRoutes: RouteType[] = [ 17 | { title: 'Test', path: '/test', element: }, 18 | ]; 19 | 20 | export const routes: RouteType[] = checkEnv('development') 21 | ? publicRoutes.concat(...devRoutes) 22 | : publicRoutes; 23 | 24 | export default function AppRoutes() { 25 | return ( 26 | 27 | {routes.map(({ path, element, title }) => ( 28 | 29 | ))} 30 | 31 | ); 32 | } 33 | -------------------------------------------------------------------------------- /src/services/notifications.ts: -------------------------------------------------------------------------------- 1 | import { toast, ToastOptions } from 'react-toastify'; 2 | import 'react-toastify/dist/ReactToastify.css'; 3 | 4 | const toastConfig: ToastOptions = { 5 | position: 'bottom-right', 6 | autoClose: 3000, 7 | hideProgressBar: false, 8 | closeOnClick: true, 9 | pauseOnHover: true, 10 | draggable: true, 11 | progress: undefined, 12 | theme: 'colored', 13 | }; 14 | 15 | export const notify = ( 16 | message: string, 17 | id?: string, 18 | timeout = 3000, 19 | ): string => { 20 | toastConfig.autoClose = timeout; 21 | toastConfig.toastId = id ? id : Date.now().toString(16); 22 | toast.info(message, toastConfig); 23 | return toastConfig.toastId; 24 | }; 25 | -------------------------------------------------------------------------------- /src/style/global.css: -------------------------------------------------------------------------------- 1 | * { 2 | box-sizing: border-box; 3 | } 4 | 5 | html, 6 | body { 7 | padding: 0; 8 | margin: 0; 9 | height: 100%; 10 | overflow: hidden; 11 | color: black; 12 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 13 | 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', 14 | sans-serif; 15 | -webkit-font-smoothing: antialiased; 16 | -moz-osx-font-smoothing: grayscale; 17 | } 18 | 19 | code { 20 | font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', 21 | monospace; 22 | } 23 | -------------------------------------------------------------------------------- /src/style/tailwind.css: -------------------------------------------------------------------------------- 1 | @import "tailwindcss"; 2 | @plugin "daisyui"; -------------------------------------------------------------------------------- /src/types/chessboard.ts: -------------------------------------------------------------------------------- 1 | import { CSSProperties } from 'react'; 2 | 3 | // prettier-ignore 4 | export type Square = 'a8' | 'b8' | 'c8' | 'd8' | 'e8' | 'f8' | 'g8' | 'h8' 5 | | 'a7' | 'b7' | 'c7' | 'd7' | 'e7' | 'f7' | 'g7' | 'h7' 6 | | 'a6' | 'b6' | 'c6' | 'd6' | 'e6' | 'f6' | 'g6' | 'h6' 7 | | 'a5' | 'b5' | 'c5' | 'd5' | 'e5' | 'f5' | 'g5' | 'h5' 8 | | 'a4' | 'b4' | 'c4' | 'd4' | 'e4' | 'f4' | 'g4' | 'h4' 9 | | 'a3' | 'b3' | 'c3' | 'd3' | 'e3' | 'f3' | 'g3' | 'h3' 10 | | 'a2' | 'b2' | 'c2' | 'd2' | 'e2' | 'f2' | 'g2' | 'h2' 11 | | 'a1' | 'b1' | 'c1' | 'd1' | 'e1' | 'f1' | 'g1' | 'h1'; 12 | 13 | export type Orientation = 'white' | 'black'; 14 | 15 | // prettier-ignore 16 | export type FENpiece = 'p' | 'r' | 'n' | 'b' | 'k' | 'q' | 'P' | 'R' | 'N' | 'B' | 'K' | 'Q'; 17 | 18 | // prettier-ignore 19 | export type Piece = 'wP' | 'wN' | 'wB' | 'wR' | 'wQ' | 'wK' | 'bP' | 'bN' | 'bB' | 'bR' | 'bQ' | 'bK'; 20 | 21 | export type Position = { 22 | [pos in Square]?: Piece; 23 | }; 24 | 25 | export type CustomPieces = { 26 | [piece in Piece]?: (obj: { 27 | isDragging: boolean; 28 | squareWidth: number; 29 | droppedPiece: Piece; 30 | targetSquare: Square; 31 | sourceSquare: Square; 32 | }) => React.ReactNode; 33 | }; 34 | 35 | export interface ChessboardProps { 36 | /** classes to apply to the board */ 37 | className?: string; 38 | /** 39 | * A function to call when a piece drag is initiated. Returns true if the piece is draggable, 40 | * false if not. 41 | * 42 | * Signature: function( { piece: string, sourceSquare: string } ) => bool 43 | */ 44 | allowDrag?: (obj: { piece: Piece; sourceSquare: Square }) => boolean; 45 | /** 46 | * The style object for the board. 47 | */ 48 | boardStyle?: CSSProperties; 49 | /** 50 | * A function for responsive size control, returns the width of the board. 51 | * 52 | * Signature: function({ screenWidth: number, screenHeight: number }) => number 53 | */ 54 | calcWidth?: (obj: { screenWidth: number; screenHeight: number }) => number; 55 | /** 56 | * The style object for the dark square. 57 | */ 58 | darkSquareStyle?: CSSProperties; 59 | /** 60 | * If false, the pieces will not be draggable 61 | */ 62 | draggable?: boolean; 63 | /** 64 | * The behavior of the piece when dropped off the board. 'snapback' brings the piece 65 | * back to it's original square and 'trash' deletes the piece from the board 66 | */ 67 | dropOffBoard?: 'snapback' | 'trash'; 68 | /** 69 | * The style object for the current drop square. { backgroundColor: 'sienna' } 70 | */ 71 | dropSquareStyle?: CSSProperties; 72 | /** 73 | * A function that gives access to the current position object. 74 | * For example, getPosition = position => this.setState({ myPosition: position }). 75 | * 76 | * Signature: function(currentPosition: object) => void 77 | */ 78 | getPosition?: (currentPosition: Position) => void; 79 | /** 80 | * The id prop is necessary if more than one board is mounted. 81 | * Drag and drop will not work as expected if not provided. 82 | */ 83 | id?: string | number; 84 | /** 85 | * The style object for the light square. 86 | */ 87 | lightSquareStyle?: CSSProperties; 88 | /** 89 | * A function to call when a piece is dragged over a specific square. 90 | * 91 | * Signature: function(square: string) => void 92 | */ 93 | onDragOverSquare?: (square: Square) => void; 94 | /** 95 | * The logic to be performed on piece drop. See chessboardjsx.com/integrations for examples. 96 | * 97 | * Signature: function({ sourceSquare: string, targetSquare: string, piece: string }) => void 98 | */ 99 | onDrop?: (obj: { 100 | sourceSquare: Square; 101 | targetSquare: Square; 102 | piece: Piece; 103 | }) => void; 104 | /** 105 | * A function to call when the mouse has left the square. 106 | * See chessboardjsx.com/integrations/move-validation for an example. 107 | * 108 | * Signature: function(square: string) => void 109 | */ 110 | onMouseOutSquare?: (square: Square) => void; 111 | /** 112 | * A function to call when the mouse is over a square. 113 | * See chessboardjsx.com/integrations/move-validation for an example. 114 | * 115 | * Signature: function(square: string) => void 116 | */ 117 | onMouseOverSquare?: (square: Square) => void; 118 | /** 119 | * A function to call when a piece is clicked. 120 | * 121 | * Signature: function(piece: string) => void 122 | */ 123 | onPieceClick?: (piece: Piece) => void; 124 | /** 125 | * A function to call when a square is clicked. 126 | * 127 | * Signature: function(square: string) => void 128 | */ 129 | onSquareClick?: (square: Square) => void; 130 | /** 131 | * A function to call when a square is right clicked. 132 | * 133 | * Signature: function(square: string) => void 134 | */ 135 | onSquareRightClick?: (square: Square) => void; 136 | /** 137 | * Orientation of the board. 138 | */ 139 | orientation?: 'white' | 'black'; 140 | /** 141 | * An object with functions returning jsx as values(render prop). 142 | * See https://www.chessboardjsx.com/custom 143 | * Signature: { wK: 144 | * function({ isDragging, squareWidth, droppedPiece, targetSquare, sourceSquare }) => jsx } 145 | */ 146 | pieces?: CustomPieces; 147 | /** 148 | * The position to display on the board. 149 | */ 150 | position: Position; 151 | /** 152 | * A function that gives access to the underlying square element. It 153 | * allows for customizations with rough.js. See chessboardjsx.com/custom for an 154 | * example. 155 | * 156 | * Signature: function({ squareElement: node, squareWidth: number }) => void 157 | */ 158 | roughSquare?: (obj: { 159 | squareElement: SVGElement; 160 | squareWidth: number; 161 | }) => void; 162 | /** 163 | * If false, notation will not be shown on the board. 164 | */ 165 | showNotation?: boolean; 166 | /** 167 | * If true, spare pieces will appear above and below the board. 168 | */ 169 | sparePieces?: boolean; 170 | /** 171 | * An object containing custom styles for squares. For example {'e4': {backgroundColor: 'orange'}, 172 | * 'd4': {backgroundColor: 'blue'}}. See chessboardjsx.com/integrations/move-validation for an example 173 | */ 174 | squareStyles?: { [square in Square]?: CSSProperties }; 175 | /** 176 | * The time it takes for a piece to slide to the target square. Only used 177 | * when the next position comes from the position prop. See chessboardjsx.com/integrations/random for an example 178 | */ 179 | transitionDuration?: number; 180 | /** 181 | * The width in pixels. For a responsive width, use calcWidth. 182 | */ 183 | width?: number; 184 | /** 185 | * When set to true it undos previous move 186 | */ 187 | undo?: boolean; 188 | } 189 | -------------------------------------------------------------------------------- /src/types/index.ts: -------------------------------------------------------------------------------- 1 | import { CSSProperties } from 'react'; 2 | import type { 3 | ChessboardProps, 4 | CustomPieces, 5 | FENpiece, 6 | Orientation, 7 | Piece, 8 | Position, 9 | Square, 10 | } from './chessboard'; 11 | 12 | type PositionStyles = { 13 | [pos in Square]?: CSSProperties | undefined; 14 | }; 15 | type BoardStateArray = string[][]; 16 | type Color = 'White' | 'Black'; 17 | type PieceType = 'Queen' | 'King' | 'Bishop' | 'Knight' | 'Rook' | 'Pawn'; 18 | type FirstMove = boolean; 19 | type Check = boolean; 20 | type CheckMate = boolean; 21 | type RustPiece = 22 | | { Queen: [Color, FirstMove] } 23 | | { King: [Color, FirstMove, Check, CheckMate] } 24 | | { Bishop: [Color, FirstMove] } 25 | | { Knight: [Color, FirstMove] } 26 | | { Rook: [Color, FirstMove] } 27 | | { Pawn: [Color, FirstMove] }; 28 | 29 | type MoveType = 'Move' | 'Capture' | 'Castle' | 'EnPassant' | 'Double'; 30 | 31 | type MoveList = [[number, number], MoveType][]; 32 | 33 | type MetaGame = { 34 | score: number; 35 | turn: number; 36 | game_over: boolean; 37 | en_passant: [number, number] | null; 38 | promotable_pawn: [number, number] | null; 39 | black_king: { 40 | piece: { King: [Color, FirstMove, Check, CheckMate] }; 41 | square: [number, number]; 42 | }; 43 | white_king: { 44 | piece: { King: [Color, FirstMove, Check, CheckMate] }; 45 | square: [number, number]; 46 | }; 47 | }; 48 | 49 | export type { 50 | BoardStateArray, 51 | Color, 52 | PieceType, 53 | PositionStyles, 54 | RustPiece, 55 | MoveType, 56 | MoveList, 57 | MetaGame, 58 | // from chessboard.ts 59 | ChessboardProps, 60 | CustomPieces, 61 | FENpiece, 62 | Orientation, 63 | Piece, 64 | Position, 65 | Square, 66 | }; 67 | -------------------------------------------------------------------------------- /src/utils/index.ts: -------------------------------------------------------------------------------- 1 | // Check if the app is being run from a specified environment, or just return the running environment (dev, prod, test) 2 | import clsx, { ClassValue } from 'clsx'; 3 | import { default as dayjs } from 'dayjs'; 4 | import { twMerge } from 'tailwind-merge'; 5 | 6 | type Env = typeof process.env.NODE_ENV; 7 | 8 | export function checkEnv(): string; 9 | export function checkEnv(type: Env): boolean; 10 | export function checkEnv(type?: Env | undefined) { 11 | const environment = process.env.NODE_ENV; 12 | if (type === undefined) return environment as string; 13 | return process.env.NODE_ENV === type; 14 | } 15 | 16 | export const formatDate = (date: number) => 17 | dayjs.unix(date).format('MMM D, YYYY, H:mm:s'); 18 | 19 | export const formatTime = (time_s: number) => { 20 | const seconds = Math.floor(time_s % 60); 21 | let remainder = Math.floor(time_s / 60); 22 | const mins = Math.floor(remainder % 60); 23 | remainder = Math.floor(remainder / 60); 24 | const hours = Math.floor(remainder % 24); 25 | const days = Math.floor(remainder / 24); 26 | return days 27 | ? `${days}days ${hours}hrs ${mins}mins ${seconds}secs` 28 | : `${hours}hrs ${mins}mins ${seconds}secs`; 29 | }; 30 | 31 | /**Combine tailwind-merge and clsx. 32 | * 33 | * Makes composition of tailwind classes, on the fly, more predictable. 34 | */ 35 | export function cn(...classes: ClassValue[]) { 36 | return twMerge(clsx(...classes)); 37 | } 38 | -------------------------------------------------------------------------------- /src/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /tailwind.config.js: -------------------------------------------------------------------------------- 1 | const defaultTheme = require('tailwindcss/defaultTheme'); 2 | const colors = require('tailwindcss/colors'); 3 | const { withAnimations } = require('animated-tailwindcss'); 4 | 5 | module.exports = withAnimations({ 6 | content: ['./src/**/*.{js,jsx,ts,tsx}'], 7 | darkMode: 'media', 8 | theme: { 9 | extend: { 10 | colors: { 11 | transparent: 'transparent', 12 | current: 'currentColor', 13 | primary: '#0694a2', // change this to set global primary colour for the app 14 | secondary: '#9fa6b2', // change this to set global secondary colour for the app 15 | gray: { 16 | 50: '#f9fafb', 17 | 100: '#f4f5f7', 18 | 200: '#e5e7eb', 19 | 300: '#d2d6dc', 20 | 400: '#9fa6b2', 21 | 500: '#6b7280', 22 | 600: '#4b5563', 23 | 700: '#374151', 24 | 800: '#252f3f', 25 | 900: '#161e2e', 26 | }, 27 | coral: { 28 | 50: '#fdf2f2', 29 | 100: '#fde8e8', 30 | 200: '#fbd5d5', 31 | 300: '#f8b4b4', 32 | 400: '#f98080', 33 | 500: '#f05252', 34 | 600: '#e02424', 35 | 700: '#c81e1e', 36 | 800: '#9b1c1c', 37 | 900: '#771d1d', 38 | }, 39 | pumpkin: { 40 | 50: '#fff8f1', 41 | 100: '#feecdc', 42 | 200: '#fcd9bd', 43 | 300: '#fdba8c', 44 | 400: '#ff8a4c', 45 | 500: '#ff5a1f', 46 | 600: '#d03801', 47 | 700: '#b43403', 48 | 800: '#8a2c0d', 49 | 900: '#73230d', 50 | }, 51 | orange: { 52 | 50: '#fdfdea', 53 | 100: '#fdf6b2', 54 | 200: '#fce96a', 55 | 300: '#faca15', 56 | 400: '#e3a008', 57 | 500: '#c27803', 58 | 600: '#9f580a', 59 | 700: '#8e4b10', 60 | 800: '#723b13', 61 | 900: '#633112', 62 | }, 63 | turquoise: { 64 | 50: '#f3faf7', 65 | 100: '#def7ec', 66 | 200: '#bcf0da', 67 | 300: '#84e1bc', 68 | 400: '#31c48d', 69 | 500: '#0e9f6e', 70 | 600: '#057a55', 71 | 700: '#046c4e', 72 | 800: '#03543f', 73 | 900: '#014737', 74 | }, 75 | beach: { 76 | 50: '#edfafa', 77 | 100: '#d5f5f6', 78 | 200: '#afecef', 79 | 300: '#7edce2', 80 | 400: '#16bdca', 81 | 500: '#0694a2', 82 | 600: '#047481', 83 | 700: '#036672', 84 | 800: '#05505c', 85 | 900: '#014451', 86 | }, 87 | azure: { 88 | 50: '#ebf5ff', 89 | 100: '#e1effe', 90 | 200: '#c3ddfd', 91 | 300: '#a4cafe', 92 | 400: '#76a9fa', 93 | 500: '#3f83f8', 94 | 600: '#1c64f2', 95 | 700: '#1a56db', 96 | 800: '#1e429f', 97 | 900: '#233876', 98 | }, 99 | blue: { 100 | 50: '#f0f5ff', 101 | 100: '#e5edff', 102 | 200: '#cddbfe', 103 | 300: '#b4c6fc', 104 | 400: '#8da2fb', 105 | 500: '#6875f5', 106 | 600: '#5850ec', 107 | 700: '#5145cd', 108 | 800: '#42389d', 109 | 900: '#362f78', 110 | }, 111 | indigo: { 112 | 50: '#f6f5ff', 113 | 100: '#edebfe', 114 | 200: '#dcd7fe', 115 | 300: '#cabffd', 116 | 400: '#ac94fa', 117 | 500: '#9061f9', 118 | 600: '#7e3af2', 119 | 700: '#6c2bd9', 120 | 800: '#5521b5', 121 | 900: '#4a1d96', 122 | }, 123 | cerise: { 124 | 50: '#fdf2f8', 125 | 100: '#fce8f3', 126 | 200: '#fad1e8', 127 | 300: '#f8b4d9', 128 | 400: '#f17eb8', 129 | 500: '#e74694', 130 | 600: '#d61f69', 131 | 700: '#bf125d', 132 | 800: '#99154b', 133 | 900: '#751a3d', 134 | }, 135 | }, 136 | animation: { 137 | modal: 'modal 0.5s', 138 | 'drawer-right': 'drawer-right 0.3s', 139 | 'drawer-left': 'drawer-left 0.3s', 140 | 'drawer-top': 'drawer-top 0.3s', 141 | 'drawer-bottom': 'drawer-bottom 0.3s', 142 | }, 143 | keyframes: { 144 | modal: { 145 | '0%, 100%': { opacity: 0 }, 146 | '100%': { opacity: 1 }, 147 | }, 148 | 'drawer-right': { 149 | '0%, 100%': { right: '-500px' }, 150 | '100%': { right: '0' }, 151 | }, 152 | 'drawer-left': { 153 | '0%, 100%': { left: '-500px' }, 154 | '100%': { left: '0' }, 155 | }, 156 | 'drawer-top': { 157 | '0%, 100%': { top: '-500px' }, 158 | '100%': { top: '0' }, 159 | }, 160 | 'drawer-bottom': { 161 | '0%, 100%': { bottom: '-500px' }, 162 | '100%': { bottom: '0' }, 163 | }, 164 | }, 165 | }, 166 | }, 167 | variants: {}, 168 | plugins: [require('daisyui')], 169 | }); 170 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2020", 4 | "useDefineForClassFields": true, 5 | "lib": ["ES2020", "DOM", "DOM.Iterable"], 6 | "module": "ESNext", 7 | "skipLibCheck": true, 8 | 9 | /* Bundler mode */ 10 | "moduleResolution": "bundler", 11 | "allowImportingTsExtensions": true, 12 | "resolveJsonModule": true, 13 | "isolatedModules": true, 14 | "noEmit": true, 15 | "jsx": "react-jsx", 16 | 17 | /* Linting */ 18 | "strict": true, 19 | "noUnusedLocals": true, 20 | "noUnusedParameters": true, 21 | "noFallthroughCasesInSwitch": true, 22 | "baseUrl": "src" 23 | }, 24 | "include": ["src"], 25 | "references": [{ "path": "./tsconfig.node.json" }] 26 | } 27 | -------------------------------------------------------------------------------- /tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "composite": true, 4 | "skipLibCheck": true, 5 | "module": "ESNext", 6 | "moduleResolution": "bundler", 7 | "allowSyntheticDefaultImports": true 8 | }, 9 | "include": ["vite.config.ts"] 10 | } 11 | -------------------------------------------------------------------------------- /vite.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite'; 2 | import react from '@vitejs/plugin-react'; 3 | import tsconfigPaths from 'vite-tsconfig-paths'; 4 | import tailwindcss from '@tailwindcss/vite'; 5 | import svgr from 'vite-plugin-svgr'; 6 | 7 | const host = process.env.TAURI_DEV_HOST; 8 | 9 | // https://vitejs.dev/config/ 10 | export default defineConfig(async () => ({ 11 | plugins: [react(), tsconfigPaths(), svgr(), tailwindcss()], 12 | 13 | // Vite options tailored for Tauri development and only applied in `tauri dev` or `tauri build` 14 | // 15 | // 1. prevent vite from obscuring rust errors 16 | clearScreen: false, 17 | // 2. tauri expects a fixed port, fail if that port is not available 18 | server: { 19 | port: 1420, 20 | strictPort: true, 21 | host: host || false, 22 | hmr: host 23 | ? { 24 | protocol: 'ws', 25 | host, 26 | port: 1421, 27 | } 28 | : undefined, 29 | watch: { 30 | // 3. tell vite to ignore watching `src-tauri` 31 | ignored: ['**/src-tauri/**'], 32 | }, 33 | }, 34 | })); 35 | --------------------------------------------------------------------------------