├── .env.example ├── .github ├── FUNDING.yml └── workflows │ ├── ci.yml │ ├── docker-image.yml │ ├── gh-pages.yml │ └── playwright.yml ├── .gitignore ├── .npmrc ├── .vscode └── settings.json ├── LICENSE ├── README.en.md ├── README.md ├── bump.config.ts ├── config └── nginx │ └── default.conf.template ├── eslint.config.js ├── package.json ├── packages ├── react │ ├── Dockerfile │ ├── README.md │ ├── index.html │ ├── netlify.toml │ ├── package.json │ ├── public │ │ ├── CNAME │ │ ├── assets │ │ │ ├── ac.svg │ │ │ ├── audio │ │ │ │ ├── ac-work.m4a │ │ │ │ ├── ac-work.mp3 │ │ │ │ ├── air-extractor-fan.m4a │ │ │ │ ├── air-extractor-fan.mp3 │ │ │ │ ├── di.m4a │ │ │ │ └── di.mp3 │ │ │ └── fonts │ │ │ │ └── digital-7-mono.ttf │ │ ├── favicon.svg │ │ ├── images │ │ │ ├── ximalaya-logo-with-banner.png │ │ │ └── ximalaya-logo.png │ │ ├── robots.txt │ │ └── yun-logo.svg │ ├── src │ │ ├── App.scss │ │ ├── App.tsx │ │ ├── components │ │ │ ├── Fade.tsx │ │ │ ├── ProTip.tsx │ │ │ ├── RemoteControl │ │ │ │ ├── RCButton.tsx │ │ │ │ ├── index.scss │ │ │ │ ├── index.tsx │ │ │ │ └── temperature.ts │ │ │ ├── Toast.tsx │ │ │ ├── ac │ │ │ │ ├── AcDisplay.tsx │ │ │ │ ├── AirConditioner.scss │ │ │ │ ├── AirConditioner.tsx │ │ │ │ ├── EnergyLabel.tsx │ │ │ │ └── EnergySavingLabel.tsx │ │ │ └── layouts │ │ │ │ └── Copyright.tsx │ │ ├── config │ │ │ └── index.ts │ │ ├── context │ │ │ ├── ac.tsx │ │ │ ├── index.tsx │ │ │ └── toast.tsx │ │ ├── hooks │ │ │ ├── index.ts │ │ │ ├── useDark.ts │ │ │ └── useDetectStorage.ts │ │ ├── main.tsx │ │ ├── pages │ │ │ ├── Rc.tsx │ │ │ └── index.tsx │ │ ├── styles │ │ │ ├── css-vars.scss │ │ │ ├── helper.scss │ │ │ └── index.scss │ │ ├── types │ │ │ ├── ac.ts │ │ │ └── index.ts │ │ └── utils │ │ │ ├── adsense │ │ │ ├── google.tsx │ │ │ └── index.ts │ │ │ └── index.ts │ ├── tsconfig.json │ ├── tsconfig.node.json │ └── vite.config.ts └── vue │ └── README.md ├── playwright.config.ts ├── pnpm-lock.yaml ├── pnpm-workspace.yaml ├── tests └── example.spec.ts ├── tsconfig.json └── unocss.config.ts /.env.example: -------------------------------------------------------------------------------- 1 | VITE_DISABLE_ADSENSE= 2 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: YunYouJun 2 | custom: https://sponsors.yunyoujun.cn 3 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | - dev 8 | 9 | pull_request: 10 | branches: 11 | - main 12 | - dev 13 | 14 | jobs: 15 | lint: 16 | runs-on: ubuntu-latest 17 | steps: 18 | - uses: actions/checkout@v3 19 | - uses: pnpm/action-setup@v2 20 | - uses: actions/setup-node@v3 21 | with: 22 | node-version: lts/* 23 | cache: pnpm 24 | 25 | - name: Install 26 | run: pnpm install 27 | 28 | - name: Lint 29 | run: pnpm run lint 30 | 31 | typecheck: 32 | runs-on: ubuntu-latest 33 | steps: 34 | - uses: actions/checkout@v3 35 | - uses: pnpm/action-setup@v2 36 | - uses: actions/setup-node@v3 37 | with: 38 | node-version: lts/* 39 | cache: pnpm 40 | 41 | - name: Install 42 | run: pnpm install 43 | 44 | - name: Typecheck 45 | run: pnpm run typecheck 46 | -------------------------------------------------------------------------------- /.github/workflows/docker-image.yml: -------------------------------------------------------------------------------- 1 | # Build and Publish 2 | name: Docker Image 3 | 4 | on: 5 | push: 6 | branches: [main] 7 | pull_request: 8 | branches: [main] 9 | 10 | jobs: 11 | build: 12 | runs-on: ubuntu-latest 13 | steps: 14 | - name: Check Out Repo 15 | uses: actions/checkout@main 16 | - name: Docker meta 17 | id: meta 18 | uses: docker/metadata-action@master 19 | with: 20 | images: ${{ secrets.DOCKER_HUB_USERNAME }}/air-conditioner 21 | tags: | 22 | type=ref,event=branch 23 | type=ref,event=pr 24 | type=semver,pattern={{version}} 25 | type=semver,pattern={{major}}.{{minor}} 26 | # set latest tag for main branch 27 | type=raw,value=latest 28 | - name: Login to Docker Hub 29 | uses: docker/login-action@master 30 | with: 31 | username: ${{ secrets.DOCKER_HUB_USERNAME }} 32 | password: ${{ secrets.DOCKER_HUB_ACCESS_TOKEN }} 33 | - name: Set up Docker Buildx 34 | id: buildx 35 | uses: docker/setup-buildx-action@master 36 | - name: Build and Push 37 | id: docker_build 38 | uses: docker/build-push-action@master 39 | with: 40 | context: ./ 41 | file: ./Dockerfile 42 | push: ${{ github.event_name != 'pull_request' }} 43 | tags: ${{ steps.meta.outputs.tags }} 44 | labels: ${{ steps.meta.outputs.labels }} 45 | - name: Image digest 46 | run: echo ${{ steps.docker_build.outputs.digest }} 47 | -------------------------------------------------------------------------------- /.github/workflows/gh-pages.yml: -------------------------------------------------------------------------------- 1 | name: Github Pages 2 | 3 | on: 4 | push: 5 | branches: 6 | - main # Set a branch name to trigger deployment 7 | 8 | pull_request: 9 | branches: 10 | - main 11 | 12 | jobs: 13 | deploy: 14 | runs-on: ubuntu-latest 15 | steps: 16 | - uses: actions/checkout@v2 17 | - uses: pnpm/action-setup@v2 18 | 19 | - name: Set node version to ${{ matrix.node_version }} 20 | uses: actions/setup-node@v3 21 | with: 22 | node-version: ${{ matrix.node_version }} 23 | cache: pnpm 24 | 25 | - name: Install Dependencies 26 | run: pnpm install --frozen-lockfile 27 | 28 | - name: Build 29 | run: pnpm run build 30 | 31 | - name: Deploy 32 | uses: peaceiris/actions-gh-pages@v3 33 | with: 34 | github_token: ${{ secrets.GITHUB_TOKEN }} 35 | publish_dir: ./packages/react/dist 36 | force_orphan: true 37 | -------------------------------------------------------------------------------- /.github/workflows/playwright.yml: -------------------------------------------------------------------------------- 1 | name: Playwright Tests 2 | 3 | on: 4 | push: 5 | branches: [main, master] 6 | pull_request: 7 | branches: [main, master] 8 | 9 | jobs: 10 | test: 11 | timeout-minutes: 60 12 | runs-on: ubuntu-latest 13 | 14 | steps: 15 | - uses: actions/checkout@v4 16 | - uses: actions/setup-node@v4 17 | with: 18 | node-version: lts/* 19 | 20 | - uses: pnpm/action-setup@v2 21 | name: Install pnpm 22 | with: 23 | run_install: true 24 | 25 | - name: Install Playwright Browsers 26 | run: pnpm exec playwright install --with-deps 27 | - name: Run Playwright tests 28 | run: pnpm exec playwright test 29 | - uses: actions/upload-artifact@v4 30 | if: ${{ !cancelled() }} 31 | with: 32 | name: playwright-report 33 | path: playwright-report/ 34 | retention-days: 30 35 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # env 2 | .env 3 | 4 | *.log 5 | 6 | yarn.lock 7 | package-lock.json 8 | # pnpm-lock.yaml 9 | .eslintcache 10 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 11 | 12 | # dependencies 13 | /.pnp 14 | .pnp.js 15 | 16 | # testing 17 | /coverage 18 | 19 | # production 20 | /build 21 | 22 | # misc 23 | .DS_Store 24 | .env.local 25 | .env.development.local 26 | .env.test.local 27 | .env.production.local 28 | 29 | npm-debug.log* 30 | yarn-debug.log* 31 | yarn-error.log* 32 | 33 | # jetbrains 34 | .idea 35 | 36 | node_modules 37 | dist 38 | dist-ssr 39 | *.local 40 | /test-results/ 41 | /playwright-report/ 42 | /blob-report/ 43 | /playwright/.cache/ 44 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | shamefully-hoist=true 2 | strict-peer-dependencies=false 3 | ignore-workspace-root-check=true 4 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "typescript.tsdk": "node_modules/typescript/lib", 3 | 4 | // Enable the ESlint flat config support 5 | "eslint.experimental.useFlatConfig": true, 6 | // Disable the default formatter, use eslint instead 7 | "prettier.enable": false, 8 | "editor.formatOnSave": false, 9 | // Auto fix 10 | "editor.codeActionsOnSave": { 11 | "source.fixAll.eslint": "explicit", 12 | "source.organizeImports": "never" 13 | }, 14 | // Enable eslint for all supported languages 15 | "eslint.validate": [ 16 | "javascript", 17 | "javascriptreact", 18 | "typescript", 19 | "typescriptreact", 20 | "vue", 21 | "html", 22 | "markdown", 23 | "json", 24 | "jsonc", 25 | "yaml" 26 | ] 27 | } 28 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 YunYouJun 云游君 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. 22 | -------------------------------------------------------------------------------- /README.en.md: -------------------------------------------------------------------------------- 1 |

2 | Air Conditioner 3 |

4 | 5 |

6 | 7 | GitHub Pages 8 | 9 |

10 | 11 |

12 | 中文文档 | English Docs 13 |

14 | 15 |

16 | Cloud air conditioner. Portable air conditioner. Invite a cool breeze into your summer life! 17 |

18 |
19 | 20 | > History: [云空调,便携小空调|云游君的小站](https://www.yunyoujun.cn/posts/air-conditioner/) 21 | 22 | - Machine Only[main]:[ac.yunyoujun.cn](https://ac.yunyoujun.cn) 23 | - Test Machine[dev]:[ac.yyj.moe](https://ac.yyj.moe) 24 | - Sample Room: 25 | 26 | ## Features 27 | 28 | ### Advantages 29 | 30 | - 🕐 Turn on the air conditioner any time and anywhere 31 | - 📱 Portable 32 | - 🔋 Low power consumption(Drawn with `HTML CSS` instead of `Canvas`) 33 | - 🔊 Noise is negligible 34 | - 🎮 Easy to use 35 | - 🔧 Swift installation 36 | 37 | ### Limitations 38 | 39 | - 💨 Wind not included 40 | 41 | ## Installation 42 | 43 | ### iframe 44 | 45 | ```html 46 | 47 | ``` 48 | 49 | Quickly install an air conditioner on your website. 50 | 51 | Sample Room:[AC Room](https://www.yunyoujun.cn/air-conditioner-room/) 52 | 53 | ### Home Installation Service 54 | 55 | - Hugo: 56 | 57 | ## Deploy It Yourself 58 | 59 | ### Docker 60 | 61 | You can use the following environment variables to customize the configuration. 62 | 63 | - `AC_NGINX_DOMAIN` Set domain name 64 | - `AC_NGINX_PORT` Set listening port 65 | 66 | ### Tencent CloudBase 67 | 68 | Developed and deployed based on Tencent's open source project [CloudBase Framework](https://github.com/Tencent/cloudbase-framework). One-click deploying is supported. 69 | 70 | [![腾讯云|Deploy to CloudBase](https://main.qcloudimg.com/raw/67f5a389f1ac6f3b4d04c7256438e44f.svg)](https://console.cloud.tencent.com/tcb/env/index?action=CreateAndDeployCloudBaseProject&appUrl=https%3A%2F%2Fgithub.com%2FYunYouJun%2Fair-conditioner%2F&branch=main) 71 | 72 | ## Dev 73 | 74 | ```bash 75 | # yarn dev 76 | yarn start 77 | # http://localhost:3000/ 78 | 79 | yarn build 80 | # ./build 81 | ``` 82 | 83 | ### Environment Variables 84 | 85 | ```bash 86 | cp .env.example .env 87 | ``` 88 | 89 | ```bash 90 | # Disable Advertisement 91 | VITE_DISABLE_ADSENSE=true 92 | ``` 93 | 94 | ## Todo 95 | 96 | - [x] Air Conditioner 97 | - [x] Energy Label 98 | - [x] Temperature Range(16-31˚C) 99 | - [x] Wind css 100 | - [x] Sound Effects 101 | - [x] Buttons 102 | - [x] Running sound 103 | - [ ] Import more sounds from [喜马拉雅](https://m.ximalaya.com/sleepaudio/6?mixedTrackIds=331526646&utm_source=smxkt) 104 | - [x] Follow system color schemes 105 | 106 | ## Ref 107 | 108 | - Numbers font: [Digital 7](https://www.dafont.com/digital-7.font),Free for personal use 109 | - Working sounds of the AC: [Air Extractor Fan | freesound](https://freesound.org/people/InspectorJ/sounds/403664/) 110 | 111 | ## [Sponsors](https://sponsors.yunyoujun.cn) 112 | 113 |

114 | 115 | 116 | 117 |

118 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | Air Conditioner 3 |

4 | 5 |

6 | 7 | GitHub Pages 8 | 9 |

10 | 11 |

12 | 中文文档 | English Docs 13 |

14 | 15 |

16 | 17 | [云空调](https://ac.yunyoujun.cn),便携小空调,为你的夏日带去清凉! 18 | 19 |

20 |
21 | 22 | > 前世今生:[云空调,便携小空调|云游君的小站](https://www.yunyoujun.cn/posts/air-conditioner/) 23 | 24 | | 仓库 | 类型 | 链接 | 25 | | --- | --- | --- | 26 | | 裸机 | main | [ac.yunyoujun.cn](https://ac.yunyoujun.cn) | 27 | | 测试机 | dev | [ac.yyj.moe](https://ac.yyj.moe) | 28 | | 样板房 | 空调房 | [https://www.yunyoujun.cn/air-conditioner-room/](https://www.yunyoujun.cn/air-conditioner-room/) | 29 | 30 | ## Features 31 | 32 | ### 优势 33 | 34 | - 🕐 随时随地打开空调 35 | - 📱 便携 36 | - 🔋 低功耗(使用 HTML CSS 而非 Canvas 绘制) 37 | - 🔊 静音 38 | - 🎮 操作简单 39 | - 🔧 安装便捷 40 | 41 | ### 劣势 42 | 43 | - 💨 没有风 44 | 45 | ## 安装 46 | 47 | ### iframe 48 | 49 | ```html 50 | 51 | ``` 52 | 53 | 您可以快速为您的网站安装空调。 54 | 55 | 样板房:[空调房](https://www.yunyoujun.cn/air-conditioner-room/) 56 | 57 | ### 上门服务 58 | 59 | - Hugo: 60 | 61 | ## 自行部署 62 | 63 | ### Docker 64 | 65 | 部署时可使用以下环境变量进行配置自定义: 66 | 67 | - `AC_NGINX_DOMAIN` 指定域名 68 | - `AC_NGINX_PORT` 指定监听端口 69 | 70 | ### 腾讯云 71 | 72 | 使用 [腾讯云 Webify](https://webify.cloudbase.net/) 一键部署: 73 | 74 | [![cloudbase](https://cloudbase.net/deploy.svg)](https://console.cloud.tencent.com/webify/new?tpl=https%3A%2F%2Fgithub.com%2FYunYouJun%2Fair-conditioner&reponame=my-air-conditioner) 75 | 76 | ## Dev 77 | 78 | ```bash 79 | # 开发预览 80 | # yarn dev 81 | yarn start 82 | # http://localhost:3000/ 83 | 84 | # 构建项目 85 | yarn build 86 | # ./build 87 | ``` 88 | 89 | ### 环境变量 90 | 91 | ```bash 92 | cp .env.example .env 93 | ``` 94 | 95 | ```bash 96 | # 关闭广告 97 | VITE_DISABLE_ADSENSE=true 98 | ``` 99 | 100 | ## Todo 101 | 102 | - [x] 空调 103 | - [x] 能耗标签 104 | - [x] 温度范围(16-31˚C) 105 | - [x] 风 css 106 | - [x] 音效 107 | - [x] 按钮 108 | - [x] 工作声 109 | - [ ] 接入 [喜马拉雅](https://m.ximalaya.com/sleepaudio/6?mixedTrackIds=331526646&utm_source=smxkt) 更多音效 110 | - [x] 适应系统的亮暗模式 111 | 112 | ## Ref 113 | 114 | - 数字字体: [Digital 7](https://www.dafont.com/digital-7.font),Free for personal use 115 | - 空调工作声: [Air Extractor Fan | freesound](https://freesound.org/people/InspectorJ/sounds/403664/) 116 | 117 | ## [Sponsors](https://sponsors.yunyoujun.cn) 118 | 119 |

120 | 121 | 122 | 123 |

124 | -------------------------------------------------------------------------------- /bump.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'bumpp' 2 | 3 | const packages = [ 4 | 'react', 5 | 'vue', 6 | // 'widget', 7 | ] 8 | 9 | export default defineConfig({ 10 | all: true, 11 | files: [ 12 | 'package.json', 13 | ...packages.map(name => `packages/${name}/package.json`), 14 | ], 15 | }) 16 | -------------------------------------------------------------------------------- /config/nginx/default.conf.template: -------------------------------------------------------------------------------- 1 | server { 2 | listen ${AC_NGINX_PORT}; 3 | server_name ${AC_NGINX_DOMAIN}; 4 | 5 | location / { 6 | root /usr/share/nginx/html; 7 | index index.html index.htm; 8 | try_files $uri /index.html; 9 | } 10 | 11 | error_page 500 502 503 504 /50x.html; 12 | location = /50x.html { 13 | root /usr/share/nginx/html; 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /eslint.config.js: -------------------------------------------------------------------------------- 1 | import antfu from '@antfu/eslint-config' 2 | 3 | export default antfu({ 4 | rules: { 5 | 'no-use-before-define': 'off', 6 | 'react/react-in-jsx-scope': 'off', 7 | 'react/display-name': 'off', 8 | }, 9 | }) 10 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "air-conditioner", 3 | "type": "module", 4 | "version": "0.1.3", 5 | "private": true, 6 | "packageManager": "pnpm@9.15.2", 7 | "description": "云空调,便携小空调", 8 | "author": { 9 | "url": "https://www.yunyoujun.cn", 10 | "email": "me@yunyoujun.cn", 11 | "name": "YunYouJun" 12 | }, 13 | "homepage": "https://ac.yunyoujun.cn/", 14 | "repository": { 15 | "type": "git", 16 | "url": "https://github.com/YunYouJun/air-conditioner" 17 | }, 18 | "scripts": { 19 | "build": "pnpm -r run build", 20 | "dev": "pnpm run react:dev", 21 | "react:dev": "pnpm -C packages/react run dev", 22 | "react:build": "pnpm -C packages/react run build", 23 | "vue:dev": "pnpm -C packages/vue run dev", 24 | "vue:build": "pnpm -C packages/vue run build", 25 | "lint": "eslint .", 26 | "release": "bumpp", 27 | "typecheck": "tsc --noEmit" 28 | }, 29 | "browserslist": { 30 | "production": [ 31 | ">0.2%", 32 | "not dead", 33 | "not op_mini all" 34 | ], 35 | "development": [ 36 | "last 1 chrome version", 37 | "last 1 firefox version", 38 | "last 1 safari version" 39 | ] 40 | }, 41 | "devDependencies": { 42 | "@antfu/eslint-config": "catalog:", 43 | "@iconify-json/ic": "catalog:", 44 | "@iconify-json/mdi": "catalog:", 45 | "@playwright/test": "catalog:", 46 | "@types/node": "catalog:", 47 | "bumpp": "catalog:", 48 | "eslint": "catalog:", 49 | "typescript": "catalog:" 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /packages/react/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:lts-alpine as builder 2 | 3 | RUN npm install -g pnpm 4 | 5 | WORKDIR /app 6 | COPY . . 7 | 8 | RUN pnpm install && npm run build 9 | 10 | FROM nginx:alpine 11 | 12 | ENV AC_NGINX_PORT=80 AC_NGINX_DOMAIN=localhost 13 | COPY --from=builder /app/packages/react/dist /usr/share/nginx/html 14 | EXPOSE 80 15 | -------------------------------------------------------------------------------- /packages/react/README.md: -------------------------------------------------------------------------------- 1 | # @air-conditioner/react 2 | 3 | 原始 React 版本 4 | -------------------------------------------------------------------------------- /packages/react/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 14 | 便携小空调 - 为你的夏日带去清凉! 15 | 23 | 24 | 25 | 26 |
27 | 28 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /packages/react/netlify.toml: -------------------------------------------------------------------------------- 1 | [build] 2 | publish = "dist" 3 | command = "npx pnpm i --store=node_modules/.pnpm-store && npx pnpm run build" 4 | 5 | [build.environment] 6 | # bypass npm auto install 7 | NPM_FLAGS = "--version" 8 | NODE_VERSION = "16" 9 | 10 | [[redirects]] 11 | from = "/*" 12 | to = "/index.html" 13 | status = 200 14 | 15 | [[headers]] 16 | for = "/manifest.webmanifest" 17 | 18 | [headers.values] 19 | Content-Type = "application/manifest+json" 20 | -------------------------------------------------------------------------------- /packages/react/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@air-conditioner/react", 3 | "type": "module", 4 | "version": "0.1.3", 5 | "private": true, 6 | "packageManager": "pnpm@9.15.2", 7 | "description": "云空调,便携小空调", 8 | "author": { 9 | "url": "https://www.yunyoujun.cn", 10 | "email": "me@yunyoujun.cn", 11 | "name": "YunYouJun" 12 | }, 13 | "homepage": "https://ac.yunyoujun.cn/", 14 | "repository": { 15 | "type": "git", 16 | "url": "https://github.com/YunYouJun/air-conditioner" 17 | }, 18 | "scripts": { 19 | "dev": "vite", 20 | "build": "vite build", 21 | "serve": "vite preview", 22 | "lint": "eslint \"**/*.{tsx,ts,js}\"", 23 | "lint:fix": "eslint \"**/*.{tsx,ts,js}\" --fix", 24 | "typecheck": "tsc --noEmit" 25 | }, 26 | "browserslist": { 27 | "production": [ 28 | ">0.2%", 29 | "not dead", 30 | "not op_mini all" 31 | ], 32 | "development": [ 33 | "last 1 chrome version", 34 | "last 1 firefox version", 35 | "last 1 safari version" 36 | ] 37 | }, 38 | "dependencies": { 39 | "react": "catalog:", 40 | "react-dom": "catalog:", 41 | "react-ga": "catalog:", 42 | "react-gtm-module": "catalog:", 43 | "react-router-dom": "catalog:", 44 | "sass": "catalog:", 45 | "web-vitals": "catalog:" 46 | }, 47 | "devDependencies": { 48 | "@emotion/react": "catalog:", 49 | "@emotion/styled": "catalog:", 50 | "@iconify-json/ic": "catalog:", 51 | "@iconify-json/mdi": "catalog:", 52 | "@mui/material": "catalog:", 53 | "@types/react": "catalog:", 54 | "@types/react-dom": "catalog:", 55 | "@types/react-gtm-module": "catalog:", 56 | "@types/react-router-dom": "catalog:", 57 | "@vitejs/plugin-react": "catalog:", 58 | "react-transition-group": "catalog:", 59 | "unocss": "catalog:", 60 | "usehooks-ts": "catalog:", 61 | "vite": "catalog:", 62 | "vite-plugin-pages": "catalog:", 63 | "vite-plugin-pwa": "catalog:" 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /packages/react/public/CNAME: -------------------------------------------------------------------------------- 1 | ac.yunyoujun.cn 2 | -------------------------------------------------------------------------------- /packages/react/public/assets/ac.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /packages/react/public/assets/audio/ac-work.m4a: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/YunYouJun/air-conditioner/c28dcb14c19dea6db90f8ea963cc51dc4b694fba/packages/react/public/assets/audio/ac-work.m4a -------------------------------------------------------------------------------- /packages/react/public/assets/audio/ac-work.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/YunYouJun/air-conditioner/c28dcb14c19dea6db90f8ea963cc51dc4b694fba/packages/react/public/assets/audio/ac-work.mp3 -------------------------------------------------------------------------------- /packages/react/public/assets/audio/air-extractor-fan.m4a: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/YunYouJun/air-conditioner/c28dcb14c19dea6db90f8ea963cc51dc4b694fba/packages/react/public/assets/audio/air-extractor-fan.m4a -------------------------------------------------------------------------------- /packages/react/public/assets/audio/air-extractor-fan.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/YunYouJun/air-conditioner/c28dcb14c19dea6db90f8ea963cc51dc4b694fba/packages/react/public/assets/audio/air-extractor-fan.mp3 -------------------------------------------------------------------------------- /packages/react/public/assets/audio/di.m4a: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/YunYouJun/air-conditioner/c28dcb14c19dea6db90f8ea963cc51dc4b694fba/packages/react/public/assets/audio/di.m4a -------------------------------------------------------------------------------- /packages/react/public/assets/audio/di.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/YunYouJun/air-conditioner/c28dcb14c19dea6db90f8ea963cc51dc4b694fba/packages/react/public/assets/audio/di.mp3 -------------------------------------------------------------------------------- /packages/react/public/assets/fonts/digital-7-mono.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/YunYouJun/air-conditioner/c28dcb14c19dea6db90f8ea963cc51dc4b694fba/packages/react/public/assets/fonts/digital-7-mono.ttf -------------------------------------------------------------------------------- /packages/react/public/favicon.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /packages/react/public/images/ximalaya-logo-with-banner.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/YunYouJun/air-conditioner/c28dcb14c19dea6db90f8ea963cc51dc4b694fba/packages/react/public/images/ximalaya-logo-with-banner.png -------------------------------------------------------------------------------- /packages/react/public/images/ximalaya-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/YunYouJun/air-conditioner/c28dcb14c19dea6db90f8ea963cc51dc4b694fba/packages/react/public/images/ximalaya-logo.png -------------------------------------------------------------------------------- /packages/react/public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Allow: / 4 | -------------------------------------------------------------------------------- /packages/react/public/yun-logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Logo 5 | Created with Sketch. 6 | 7 | 8 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /packages/react/src/App.scss: -------------------------------------------------------------------------------- 1 | .hot-color { 2 | filter: saturate(110%); 3 | } 4 | 5 | .cold-color { 6 | filter: saturate(90%); 7 | } 8 | -------------------------------------------------------------------------------- /packages/react/src/App.tsx: -------------------------------------------------------------------------------- 1 | import type { FC } from 'react' 2 | import { Suspense, useEffect } from 'react' 3 | import { BrowserRouter as Router, useRoutes } from 'react-router-dom' 4 | 5 | // @ts-expect-error vite-plugin-pages 6 | import routes from '~react-pages' 7 | import Copyright from '~/components/layouts/Copyright' 8 | import pkg from '../package.json' 9 | 10 | import './App.scss' 11 | 12 | /** 13 | * 控制台输出信息 14 | * @param name 名称 15 | * @param link 链接 16 | * @param color 颜色 17 | * @param emoji 18 | */ 19 | function consoleInfo( 20 | name: string, 21 | link: string, 22 | color = '#0078E7', 23 | emoji = '☁️', 24 | ) { 25 | // eslint-disable-next-line no-console 26 | console.log( 27 | `%c ${emoji} ${name} %c ${link}`, 28 | `color: white; background: ${color}; padding:5px 0;`, 29 | `padding:4px;border:1px solid ${color};`, 30 | ) 31 | } 32 | 33 | /** 34 | * Loading Animation 35 | */ 36 | function Loading() { 37 | return ( 38 |

39 | ) 40 | } 41 | 42 | /** 43 | * https://github.com/hannoeru/vite-plugin-pages 44 | * Must use Suspense 45 | */ 46 | function Routes() { 47 | return ( 48 | }> 49 | {useRoutes(routes)} 50 | 51 | ) 52 | } 53 | 54 | const App: FC = () => { 55 | useEffect(() => { 56 | consoleInfo(pkg.name, pkg.repository.url) 57 | consoleInfo(`@${pkg.author.name}`, pkg.author.url) 58 | }, []) 59 | 60 | return ( 61 |
62 |
63 | 64 | 65 | 66 | 67 | 68 |
69 |
70 | ) 71 | } 72 | 73 | export default App 74 | -------------------------------------------------------------------------------- /packages/react/src/components/Fade.tsx: -------------------------------------------------------------------------------- 1 | import type { TransitionStatus } from 'react-transition-group' 2 | import React, { useRef } from 'react' 3 | import { Transition } from 'react-transition-group' 4 | 5 | const duration = 300 6 | 7 | const defaultStyle = { 8 | transition: `opacity ${duration}ms ease-in-out`, 9 | opacity: 0, 10 | } 11 | 12 | const transitionStyles: Record = { 13 | entering: { opacity: 1 }, 14 | entered: { opacity: 1 }, 15 | exiting: { opacity: 0 }, 16 | exited: { opacity: 0 }, 17 | unmounted: { opacity: 0 }, 18 | } 19 | 20 | export const Fade: React.FC<{ in: boolean, children: React.ReactNode }> = (props) => { 21 | const nodeRef = useRef(null) 22 | return ( 23 | 24 | {state => ( 25 |
32 | { props.children } 33 |
34 | )} 35 |
36 | ) 37 | } 38 | -------------------------------------------------------------------------------- /packages/react/src/components/ProTip.tsx: -------------------------------------------------------------------------------- 1 | import type { FC } from 'react' 2 | import useDark from '~/hooks/useDark' 3 | import { adsenseLink, jumpToAdsense } from '~/utils/adsense' 4 | 5 | /** 6 | * 喜马拉雅链接 7 | * @param props 8 | */ 9 | const AdsenseLink: FC<{ text: string }> = (props) => { 10 | return ( 11 | { 16 | jumpToAdsense() 17 | }} 18 | rel="noreferrer" 19 | > 20 | {props.text || '喜马拉雅'} 21 | 22 | ) 23 | } 24 | 25 | const ProTip: FC = () => { 26 | const { toggleDark } = useDark() 27 | 28 | return ( 29 |
32 |
33 | Tip: 为你的夏日带去 34 | {import.meta.env.VITE_DISABLE_ADSENSE 35 | ? ( 36 | '清凉' 37 | ) 38 | : ( 39 | 40 | )} 41 | ! 42 |
43 | ) 44 | } 45 | 46 | export default ProTip 47 | -------------------------------------------------------------------------------- /packages/react/src/components/RemoteControl/RCButton.tsx: -------------------------------------------------------------------------------- 1 | import { Fab } from '@mui/material' 2 | import React from 'react' 3 | 4 | /** 5 | * 播放「嘀」的音效 6 | */ 7 | function playDi() { 8 | const di = document.getElementById('di') 9 | if (di) 10 | (di as HTMLAudioElement).play() 11 | } 12 | 13 | /** 14 | * 遥控器按钮 15 | * @param props 16 | */ 17 | const RCButton: React.FC void 19 | className?: string 20 | style?: React.CSSProperties 21 | }>> = (props) => { 22 | return ( 23 | { 27 | playDi() 28 | props.onClick && props.onClick() 29 | }} 30 | > 31 | 32 | ) 33 | } 34 | 35 | export default RCButton 36 | -------------------------------------------------------------------------------- /packages/react/src/components/RemoteControl/index.scss: -------------------------------------------------------------------------------- 1 | .rc-button { 2 | margin: 8px !important; 3 | } 4 | -------------------------------------------------------------------------------- /packages/react/src/components/RemoteControl/index.tsx: -------------------------------------------------------------------------------- 1 | import { blue, green, red } from '@mui/material/colors' 2 | 3 | import React from 'react' 4 | import { useAc, useAcCtx } from '~/context' 5 | import { getAssetsUrl } from '~/utils' 6 | import RCButton from './RCButton' 7 | 8 | import { useAcTemperature } from './temperature' 9 | import './index.scss' 10 | 11 | let playStartSoundTimeoutId: any 12 | let playWorkSoundTimeoutId: any 13 | let playWorkSoundIntervalId: any 14 | 15 | /** 16 | * 播放空调启动声音 17 | */ 18 | function playStartSound() { 19 | const acStart = document.getElementById('ac-work') as HTMLAudioElement 20 | acStart.load() 21 | acStart.play() 22 | 23 | playStartSoundTimeoutId = setTimeout(() => { 24 | playWorkSound() 25 | }, 8000) 26 | } 27 | 28 | // 噪音起始时间 29 | const noiseStartTime = 2 30 | // 噪音持续时间 31 | const noiseDuration = 56 32 | 33 | /** 34 | * 播放空调工作声音 35 | */ 36 | function playWorkSound() { 37 | const acWork = document.getElementById( 38 | 'air-extractor-fan', 39 | ) as HTMLAudioElement 40 | acWork.load() 41 | acWork.play() 42 | 43 | playWorkSoundTimeoutId = setTimeout(() => { 44 | playWorkSoundIntervalId = setInterval(() => { 45 | acWork.currentTime = noiseStartTime 46 | }, noiseDuration * 1000) 47 | }, noiseStartTime * 1000) 48 | } 49 | 50 | /** 51 | * 切换空调工作状态 52 | */ 53 | function toggleAC(status: boolean) { 54 | if (status) { 55 | (document.getElementById('ac-work') as HTMLAudioElement).load() 56 | const acWork = document.getElementById( 57 | 'air-extractor-fan', 58 | ) as HTMLAudioElement 59 | if (playStartSoundTimeoutId) 60 | clearTimeout(playStartSoundTimeoutId) 61 | 62 | if (playWorkSoundTimeoutId) 63 | clearTimeout(playWorkSoundTimeoutId) 64 | 65 | if (playWorkSoundIntervalId) 66 | clearInterval(playWorkSoundIntervalId) 67 | 68 | acWork.currentTime = noiseStartTime + noiseDuration 69 | } 70 | else { 71 | playStartSound() 72 | } 73 | } 74 | 75 | const SOUND_DI_PATH = getAssetsUrl('/assets/audio/di.m4a') 76 | const SOUND_AC_WORK_PATH = getAssetsUrl('/assets/audio/ac-work.m4a') 77 | const SOUND_AIR_EXTRACTOR_FAN_PATH = getAssetsUrl( 78 | '/assets/audio/air-extractor-fan.m4a', 79 | ) 80 | 81 | /** 82 | * 遥控 83 | */ 84 | const RemoteControl: React.FC = () => { 85 | const { toggleStatus, toggleMode } = useAc() 86 | const { state: ac } = useAcCtx() 87 | 88 | const { increase, decrease } = useAcTemperature() 89 | 90 | return ( 91 |
92 | 93 | 94 | 100 |
101 | {' '} 102 | { 109 | toggleMode('cold') 110 | }} 111 | > 112 |
113 | 114 | { 117 | toggleAC(ac.status) 118 | toggleStatus() 119 | }} 120 | style={{ 121 | backgroundColor: ac.status ? red[600] : green[600], 122 | color: 'white', 123 | }} 124 | > 125 |
126 | 127 | { 131 | toggleMode('hot') 132 | }} 133 | > 134 |
135 | 136 |
137 | 141 |
142 | 143 | 147 |
148 | 149 |
150 | ) 151 | } 152 | 153 | export default RemoteControl 154 | -------------------------------------------------------------------------------- /packages/react/src/components/RemoteControl/temperature.ts: -------------------------------------------------------------------------------- 1 | import { useAcCtx } from '~/context' 2 | import { useToastCtx } from '~/context/toast' 3 | 4 | export const maxTemperature = 31 5 | export const minTemperature = 16 6 | 7 | export function useAcTemperature() { 8 | const { state, dispatch } = useAcCtx() 9 | const { dispatch: dispatchToast } = useToastCtx() 10 | 11 | /** 12 | * 增加温度 13 | */ 14 | const increase = () => { 15 | if (state.temperature < maxTemperature) { 16 | dispatch({ type: 'increment' }) 17 | } 18 | else { 19 | dispatchToast({ 20 | type: 'update', 21 | payload: { 22 | message: '已经是最大温度啦!', 23 | open: true, 24 | severity: 'error', 25 | }, 26 | }) 27 | } 28 | } 29 | 30 | /** 31 | * 降低温度 32 | */ 33 | const decrease = () => { 34 | if (state.temperature > minTemperature) { 35 | dispatch({ type: 'decrement' }) 36 | } 37 | else { 38 | dispatchToast({ 39 | type: 'update', 40 | payload: { 41 | message: '已经是最小温度啦!', 42 | open: true, 43 | severity: 'error', 44 | }, 45 | }) 46 | } 47 | } 48 | 49 | return { 50 | increase, 51 | decrease, 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /packages/react/src/components/Toast.tsx: -------------------------------------------------------------------------------- 1 | import type { 2 | AlertColor, 3 | AlertProps, 4 | } from '@mui/material' 5 | import { 6 | Alert as MuiAlert, 7 | Snackbar, 8 | } from '@mui/material' 9 | import React from 'react' 10 | import { useToastCtx } from '~/context/toast' 11 | 12 | const Alert = React.forwardRef(( 13 | props, 14 | ref, 15 | ) => { 16 | return 17 | }) 18 | 19 | const Toast: React.FC<{ severity?: AlertColor }> = (props) => { 20 | const { state, dispatch } = useToastCtx() 21 | 22 | return ( 23 | { 31 | dispatch({ type: 'open', open: false }) 32 | }} 33 | > 34 | { 36 | dispatch({ type: 'open', open: false }) 37 | }} 38 | severity={props.severity || state.severity || 'error'} 39 | style={{ width: '100%', minWidth: 318 }} 40 | > 41 | {state.message} 42 | 43 | 44 | ) 45 | } 46 | 47 | export default Toast 48 | -------------------------------------------------------------------------------- /packages/react/src/components/ac/AcDisplay.tsx: -------------------------------------------------------------------------------- 1 | import type { AcMode } from '~/types' 2 | import React from 'react' 3 | import { useAcCtx } from '~/context' 4 | import { acColor } from './AirConditioner' 5 | 6 | /** 7 | * 空调温度 8 | */ 9 | const AcTemperature: React.FC = () => { 10 | const { state } = useAcCtx() 11 | return ( 12 |

13 | {state.temperature} 14 | °C 15 |

16 | ) 17 | } 18 | 19 | /** 20 | * 显示屏(温度/图标) 21 | * @param props 22 | */ 23 | export const AcDisplay: React.FC<{ mode: AcMode }> = React.forwardRef( 24 | (props, ref) => { 25 | return ( 26 |
} 28 | className="absolute top-6 right-8" 29 | style={{ 30 | color: acColor.display, 31 | }} 32 | > 33 |
34 | {props.mode === 'cold' ? '❄' : '☀️'} 35 | ️️ 36 |
37 | 38 |
39 | ) 40 | }, 41 | ) 42 | -------------------------------------------------------------------------------- /packages/react/src/components/ac/AirConditioner.scss: -------------------------------------------------------------------------------- 1 | .ac-temperature { 2 | text-shadow: 0 0 2px rgba(0, 0, 0, 0.1); 3 | } 4 | 5 | .wind-effect { 6 | opacity: 0.3; 7 | } 8 | 9 | :root { 10 | --ac-c-text-dot: black; 11 | } 12 | 13 | .text-dot { 14 | background-color: var(--ac-c-text-dot); 15 | } 16 | 17 | .energy-label-level { 18 | margin-top: 2px; 19 | height: 3px; 20 | } 21 | 22 | .energy-saving-label { 23 | color: black; 24 | opacity: 0.8; 25 | position: absolute; 26 | top: 10px; 27 | left: 63px; 28 | background-color: #4caf50; 29 | padding: 12px; 30 | border-radius: 2px; 31 | transform: scale(0.22); 32 | transform-origin: left top; 33 | 34 | &_bg { 35 | padding: 10px; 36 | width: 200px; 37 | border-radius: 15px; 38 | background-color: #fafafa; 39 | display: flex; 40 | flex-direction: column; 41 | justify-content: center; 42 | align-items: center; 43 | } 44 | 45 | &_title { 46 | font-size: 20px; 47 | display: block; 48 | margin: 2px auto; 49 | } 50 | 51 | &_description { 52 | font-size: 12px; 53 | } 54 | } 55 | 56 | .adsense-text-link { 57 | color: #63a5ef; 58 | text-decoration: none; 59 | cursor: pointer; 60 | } 61 | 62 | .adsense-logo { 63 | margin-bottom: 10px; 64 | font-size: 3.5rem; 65 | 66 | &.animated { 67 | animation: iconAnimate 1.5s ease-in-out infinite; 68 | } 69 | } 70 | 71 | @keyframes iconAnimate { 72 | 0%, 73 | 100% { 74 | transform: scale(1); 75 | } 76 | 77 | 10%, 78 | 30% { 79 | transform: scale(0.9); 80 | } 81 | 82 | 20%, 83 | 40%, 84 | 60%, 85 | 80% { 86 | transform: scale(1.1); 87 | } 88 | 89 | 50%, 90 | 70% { 91 | transform: scale(1.1); 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /packages/react/src/components/ac/AirConditioner.tsx: -------------------------------------------------------------------------------- 1 | import type { AcMode } from '~/types' 2 | 3 | import React from 'react' 4 | import * as pkg from '~/../package.json' 5 | import { Fade } from '../Fade' 6 | import { AcDisplay } from './AcDisplay' 7 | import { EnergyLabel } from './EnergyLabel' 8 | 9 | import { EnergySavingLabel } from './EnergySavingLabel' 10 | 11 | import './AirConditioner.scss' 12 | 13 | // import { adsenseLink, jumpToAdsense } from "../adsense"; 14 | 15 | export const acColor = { 16 | border: '#e0e0e0', 17 | display: '#cccccc', 18 | wind: '#bbbbbb', 19 | } 20 | 21 | const AcBorder: React.FC = (props) => { 22 | return ( 23 |
33 |
34 | ) 35 | } 36 | 37 | /** 38 | * 空调 Logo 39 | */ 40 | const AcLogo: React.FC = () => { 41 | return ( 42 |
43 | 50 | logo 58 | 59 |
60 | ) 61 | } 62 | 63 | /** 64 | * 出风口线 65 | */ 66 | const AirOutlet: React.FC = () => { 67 | return
68 | } 69 | 70 | /** 71 | * 空调状态 72 | * @param props 73 | */ 74 | const AcStatus: React.FC<{ status: boolean }> = (props) => { 75 | // 空调状态小灯 76 | const led = { backgroundColor: props.status ? '#38F709' : acColor.border } 77 | 78 | return ( 79 |
85 |
86 | ) 87 | } 88 | 89 | /** 90 | * 风特效 91 | * @param props 92 | */ 93 | const WindEffect = React.forwardRef((props, ref) => { 94 | return ( 95 |
} className="wind-effect flex justify-center my-5"> 96 |
100 |
101 |
102 |
106 |
107 |
108 | ) 109 | }) 110 | 111 | /** 112 | * 空调 113 | */ 114 | const AirConditioner: React.FC<{ 115 | mode: AcMode 116 | status: boolean 117 | temperature: number 118 | }> = (props) => { 119 | return ( 120 |
121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | {import.meta.env.VITE_DISABLE_ADSENSE ? null : } 130 | 131 | 132 | 133 | 134 |
135 | ) 136 | } 137 | 138 | export default AirConditioner 139 | -------------------------------------------------------------------------------- /packages/react/src/components/ac/EnergyLabel.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | interface TextLabelProps { 4 | num: number 5 | color: string 6 | /** 7 | * 宽高尺寸 8 | */ 9 | size: number 10 | mx: number 11 | my: number 12 | } 13 | 14 | /** 15 | * 文本标签(黑色小点点) 16 | */ 17 | const TextLabel: React.FC = (props) => { 18 | const { color, size, mx, my, num } = props 19 | const titleLength = [...Array.from({ length: num }).keys()] 20 | const titleLabel = titleLength.map(n => ( 21 | 30 | 31 | )) 32 | return ( 33 |
39 | {titleLabel} 40 |
41 | ) 42 | } 43 | 44 | /** 45 | * 功耗标签 46 | */ 47 | export const EnergyLabel: React.FC<{ titleLength: number }> = () => { 48 | return ( 49 |
60 | 61 |
67 |
68 |
69 |
84 |
85 |
86 |
87 |
88 |
89 |
90 |
91 |
92 |
99 | 100 |
101 | 102 | 103 |
104 | 105 |
106 | ) 107 | } 108 | -------------------------------------------------------------------------------- /packages/react/src/components/ac/EnergySavingLabel.tsx: -------------------------------------------------------------------------------- 1 | import { adsense } from '~/config' 2 | 3 | /** 4 | * 节能产品惠民工程 5 | */ 6 | export function EnergySavingLabel() { 7 | // const adsenseLink = 'https://sponsors.yunyoujun.cn' 8 | return ( 9 | 15 |
16 |
17 | 18 | 节能产品  惠民工程 19 | 20 |
21 | {/* 22 | 💰 23 | */} 24 | 25 | 推广上限价格:XXXX 元 26 | 27 | 28 | 政府补助金额:XXXX 元 29 | 30 | 31 | 补助上限价格:XXXX 元 32 | 33 |
34 |
35 |
36 | ) 37 | } 38 | -------------------------------------------------------------------------------- /packages/react/src/components/layouts/Copyright.tsx: -------------------------------------------------------------------------------- 1 | import { IconButton, Tooltip } from '@mui/material' 2 | import React from 'react' 3 | import * as pkg from '~/../package.json' 4 | 5 | const socialList = [ 6 | { 7 | type: 'github', 8 | color: 'inherit', 9 | icon: 'i-mdi-github', 10 | label: 'GitHub: YunYouJun', 11 | href: 'https://github.com/YunYouJun', 12 | }, 13 | { 14 | type: 'telegram', 15 | color: '#1da1f2', 16 | icon: 'i-mdi-telegram', 17 | label: 'Telegram Channel', 18 | href: 'https://t.me/elpsycn', 19 | }, 20 | { 21 | type: 'weibo', 22 | color: '#DB2828', 23 | icon: 'i-mdi-sina-weibo', 24 | label: '微博:机智的云游君', 25 | href: 'http://weibo.com/jizhideyunyoujun', 26 | }, 27 | { 28 | type: 'twitter', 29 | color: '#1da1f2', 30 | icon: 'i-mdi-twitter', 31 | label: 'Twitter: YunYouJun', 32 | href: 'https://twitter.com/YunYouJun', 33 | }, 34 | { 35 | type: 'wechat', 36 | color: '#1AAD19', 37 | icon: 'i-mdi-wechat', 38 | label: '微信公众号:云游君', 39 | href: 'https://cdn.yunyoujun.cn/img/about/white-qrcode-and-search.jpg', 40 | }, 41 | { 42 | type: 'blog', 43 | color: '#6435C9', 44 | icon: 'i-mdi-earth', 45 | label: '博客:yunyoujun.cn', 46 | href: 'http://www.yunyoujun.cn', 47 | }, 48 | ] 49 | 50 | const Copyright: React.FC = () => { 51 | return ( 52 |
53 |
54 | {'© '} 55 | 56 | Yun Air Conditioner 57 | 58 | 65 | 72 |

73 | {`2019 - ${new Date().getFullYear()}`} 74 |

75 |
76 | {socialList.map(item => ( 77 | 78 | 83 |
84 | 85 | 86 | ))} 87 |
88 |
89 | ) 90 | } 91 | 92 | export default Copyright 93 | -------------------------------------------------------------------------------- /packages/react/src/config/index.ts: -------------------------------------------------------------------------------- 1 | export const adsense = { 2 | link: 'https://home.yunle.fun', 3 | icon: 'i-mdi:home-lightbulb-outline', 4 | } 5 | -------------------------------------------------------------------------------- /packages/react/src/context/ac.tsx: -------------------------------------------------------------------------------- 1 | import type { FC, PropsWithChildren } from 'react' 2 | import type { AcMode, AcState } from '~/types' 3 | import { createContext, useContext, useReducer } from 'react' 4 | import { useLocalStorage } from 'usehooks-ts' 5 | import { useToastCtx } from './toast' 6 | 7 | export const acStorageKey = 'ac:state' 8 | 9 | type AcAction = { type: 'increment' | 'decrement' | 'toggleStatus' } | { 10 | type: 'status' 11 | status: AcState['status'] 12 | } | { 13 | type: 'mode' 14 | mode: AcState['mode'] 15 | } | { 16 | type: 'update' 17 | payload: Partial 18 | } 19 | 20 | export const defaultState: AcState = { 21 | mode: 'cold', 22 | status: false, 23 | temperature: 26, 24 | } 25 | 26 | const AcContext = createContext<{ 27 | state: AcState 28 | dispatch: (action: AcAction) => void 29 | } | undefined>(undefined) 30 | // AcContext.displayName = 'AC' 31 | 32 | export const AcProvider: FC = (props) => { 33 | const [initState, setAcState] = useLocalStorage(acStorageKey, defaultState) 34 | 35 | function acReducer(state: AcState, action: AcAction) { 36 | let val = { ...state } 37 | switch (action.type) { 38 | case 'increment': 39 | val.temperature += 1 40 | break 41 | case 'decrement': 42 | val.temperature -= 1 43 | break 44 | case 'toggleStatus': 45 | val.status = !val.status 46 | break 47 | case 'status': 48 | val.status = action.status 49 | break 50 | case 'mode': 51 | val.mode = action.mode 52 | break 53 | case 'update': 54 | val = { 55 | ...state, 56 | ...action.payload, 57 | } 58 | break 59 | default: 60 | throw new Error('Unexpected Ac Action') 61 | } 62 | 63 | setAcState(val) 64 | return val 65 | } 66 | 67 | const [state, dispatch] = useReducer(acReducer, initState) 68 | return ( 69 | 70 | {props.children} 71 | 72 | ) 73 | } 74 | 75 | export function useAcCtx() { 76 | const context = useContext(AcContext) 77 | if (context === undefined) 78 | throw new Error('useAcCtx must be used within a AcProvider') 79 | 80 | return context 81 | } 82 | 83 | export function useAc() { 84 | const { state, dispatch } = useAcCtx() 85 | const { dispatch: dispatchToast } = useToastCtx() 86 | 87 | return { 88 | /** 89 | * 切换开关状态 90 | */ 91 | toggleStatus() { 92 | dispatch({ type: 'toggleStatus' }) 93 | }, 94 | toggleMode(mode: AcMode) { 95 | dispatch({ type: 'mode', mode }) 96 | 97 | const currentTemperature = state.temperature 98 | const goodColdTemperature = 26 99 | const goodHotTemperature = 20 100 | 101 | const recommendedSlogan = (mode: AcMode, temperature: number) => 102 | `建议将空调的制${ 103 | mode === 'cold' ? '冷' : '热' 104 | }温度调至 ${temperature} 度以${ 105 | mode === 'cold' ? '上' : '下' 106 | },为节能减排贡献一份力量!` 107 | 108 | if (mode === 'cold' && currentTemperature < goodColdTemperature) { 109 | dispatchToast({ 110 | type: 'update', 111 | payload: { 112 | message: recommendedSlogan('cold', goodColdTemperature), 113 | open: true, 114 | severity: 'success', 115 | }, 116 | }) 117 | } 118 | else if (mode === 'hot' && currentTemperature > goodHotTemperature) { 119 | dispatchToast({ 120 | type: 'update', 121 | payload: { 122 | message: recommendedSlogan('hot', goodHotTemperature), 123 | open: true, 124 | severity: 'success', 125 | }, 126 | }) 127 | } 128 | }, 129 | } 130 | } 131 | -------------------------------------------------------------------------------- /packages/react/src/context/index.tsx: -------------------------------------------------------------------------------- 1 | import type { PropsWithChildren } from 'react' 2 | import React from 'react' 3 | 4 | export * from './ac' 5 | 6 | export const ComposeContext: React.FC[] }>> = (props) => { 7 | const { items, children } = props 8 | return ( 9 | <> 10 | { 11 | items.reduceRight( 12 | (acc, Comp) => {acc}, 13 | children, 14 | ) 15 | } 16 | 17 | ) 18 | } 19 | 20 | export default ComposeContext 21 | -------------------------------------------------------------------------------- /packages/react/src/context/toast.tsx: -------------------------------------------------------------------------------- 1 | import type { AlertColor } from '@mui/material' 2 | import type { FC, PropsWithChildren } from 'react' 3 | 4 | import { createContext, useContext, useReducer } from 'react' 5 | 6 | export interface ToastState { 7 | /** 8 | * 是否打开 9 | */ 10 | open: boolean 11 | /** 12 | * 消息内容 13 | */ 14 | message: string 15 | /** 16 | * 提示类型 17 | */ 18 | severity: AlertColor 19 | } 20 | 21 | const ToastContext = createContext<{ 22 | state: ToastState 23 | dispatch: (action: ToastAction) => void 24 | } | undefined>(undefined) 25 | ToastContext.displayName = 'toast' 26 | 27 | const initialState: ToastState = { 28 | open: false, 29 | message: '', 30 | severity: 'error', 31 | } 32 | 33 | type ToastAction = { 34 | type: 'message' 35 | message: ToastState['message'] 36 | } | { 37 | type: 'open' 38 | open: ToastState['open'] 39 | } | { 40 | type: 'severity' 41 | severity: ToastState['severity'] 42 | } | { 43 | type: 'update' 44 | payload: Partial 45 | } 46 | 47 | export function toastReducer(state: ToastState, action: ToastAction) { 48 | switch (action.type) { 49 | case 'message': 50 | return { ...state, message: action.message } 51 | case 'open': 52 | return { ...state, open: action.open } 53 | case 'severity': 54 | return { ...state, severity: action.severity } 55 | case 'update': 56 | return { ...state, ...action.payload } 57 | default: 58 | throw new Error('Unexpected Toast Action') 59 | } 60 | } 61 | 62 | export const ToastProvider: FC = ({ children }) => { 63 | const [state, dispatch] = useReducer(toastReducer, initialState) 64 | 65 | return ( 66 | 67 | {children} 68 | 69 | ) 70 | } 71 | 72 | export function useToastCtx() { 73 | const context = useContext(ToastContext) 74 | if (context === undefined) 75 | throw new Error('useToast must be used within a ToastProvider') 76 | 77 | return context 78 | } 79 | -------------------------------------------------------------------------------- /packages/react/src/hooks/index.ts: -------------------------------------------------------------------------------- 1 | export { default as useDark } from './useDark' 2 | export * from './useDetectStorage' 3 | -------------------------------------------------------------------------------- /packages/react/src/hooks/useDark.ts: -------------------------------------------------------------------------------- 1 | import { useEffect } from 'react' 2 | import { useDarkMode } from 'usehooks-ts' 3 | 4 | // https://stackoverflow.com/questions/70996320/enable-hot-reload-for-vite-react-project-instead-of-page-reload 5 | // avoid reload page 6 | export default function useDark() { 7 | const { isDarkMode: isDark, toggle: toggleDark } = useDarkMode() 8 | 9 | useEffect(() => { 10 | if (isDark) 11 | document.documentElement.classList.add('dark') 12 | else 13 | document.documentElement.classList.remove('dark') 14 | }, [isDark]) 15 | 16 | return { 17 | isDark, 18 | toggleDark, 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /packages/react/src/hooks/useDetectStorage.ts: -------------------------------------------------------------------------------- 1 | import type { AcState } from '~/types' 2 | import { useEffect } from 'react' 3 | import { acStorageKey, defaultState, useAcCtx } from '~/context' 4 | 5 | /** 6 | * 通过监听 storage 来更新状态 7 | */ 8 | export function useDetectStorage() { 9 | const { dispatch } = useAcCtx() 10 | 11 | useEffect(() => { 12 | function onStorage(e: StorageEvent) { 13 | // 重复设置相同的键值不会触发该事件 14 | if (e.key === acStorageKey) { 15 | dispatch({ 16 | type: 'update', 17 | payload: e.newValue ? JSON.parse(e.newValue) as AcState : defaultState, 18 | }) 19 | } 20 | } 21 | 22 | window.addEventListener('storage', onStorage) 23 | return () => { 24 | window.removeEventListener('storage', onStorage) 25 | } 26 | }, [dispatch]) 27 | } 28 | -------------------------------------------------------------------------------- /packages/react/src/main.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { createRoot } from 'react-dom/client' 3 | 4 | import TagManager from 'react-gtm-module' 5 | 6 | import App from './App' 7 | import { AcProvider, ComposeContext } from './context' 8 | import { ToastProvider } from './context/toast' 9 | 10 | import '@unocss/reset/tailwind.css' 11 | // your custom styles here 12 | import './styles/css-vars.scss' 13 | import './styles/index.scss' 14 | import 'uno.css' 15 | 16 | const tagManagerArgs = { 17 | gtmId: 'GTM-NFMC9GL', 18 | } 19 | TagManager.initialize(tagManagerArgs) 20 | 21 | createRoot(document.getElementById('root') as HTMLElement).render( 22 | 23 | 24 | 25 | 26 | , 27 | ) 28 | -------------------------------------------------------------------------------- /packages/react/src/pages/Rc.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import RemoteControl from '~/components/RemoteControl' 3 | import { useDetectStorage } from '~/hooks' 4 | 5 | const Rc: React.FC = () => { 6 | useDetectStorage() 7 | 8 | return ( 9 | 10 | ) 11 | } 12 | 13 | export default Rc 14 | -------------------------------------------------------------------------------- /packages/react/src/pages/index.tsx: -------------------------------------------------------------------------------- 1 | import { Button } from '@mui/material' 2 | import React from 'react' 3 | 4 | import AirConditioner from '~/components/ac/AirConditioner' 5 | import ProTip from '~/components/ProTip' 6 | 7 | import RemoteControl from '~/components/RemoteControl' 8 | import Toast from '~/components/Toast' 9 | 10 | import { useAcCtx } from '~/context' 11 | import { useDetectStorage } from '~/hooks' 12 | 13 | /** 14 | * 主页 15 | */ 16 | const Home: React.FC = () => { 17 | const { state: ac } = useAcCtx() 18 | 19 | useDetectStorage() 20 | 21 | /** 22 | * 根据模式返回对应的色温 23 | */ 24 | function getClassByMode() { 25 | if (ac.status) 26 | return ac.mode === 'hot' ? 'hot-color' : 'cold-color' 27 | else 28 | return '' 29 | } 30 | 31 | return ( 32 |
33 |
34 |

35 | 便携小空调 36 |

37 | 38 | 43 |
44 |
45 | 53 |
54 |
55 | 56 |
57 | 58 | 59 |
60 | ) 61 | } 62 | 63 | export default Home 64 | -------------------------------------------------------------------------------- /packages/react/src/styles/css-vars.scss: -------------------------------------------------------------------------------- 1 | :root { 2 | --ac-bg-color: transparent; 3 | --ac-text-color: #141414; 4 | } 5 | 6 | html.dark { 7 | --ac-bg-color: #121212; 8 | --ac-text-color: #fafafa; 9 | } 10 | -------------------------------------------------------------------------------- /packages/react/src/styles/helper.scss: -------------------------------------------------------------------------------- 1 | @font-face { 2 | font-family: "Digital-7 Mono"; 3 | src: url("/assets/fonts/digital-7-mono.ttf") format("truetype"); 4 | } 5 | 6 | .font-digit { 7 | font-family: "Digital-7 Mono"; 8 | } 9 | 10 | .ac-text { 11 | color: var(--ac-text-color); 12 | } 13 | -------------------------------------------------------------------------------- /packages/react/src/styles/index.scss: -------------------------------------------------------------------------------- 1 | @use './helper.scss'; 2 | 3 | body { 4 | margin: 0; 5 | min-height: 90vh; 6 | background-color: var(--ac-bg-color); 7 | color: var(--ac-text-color); 8 | 9 | transition: background-color 0.2s; 10 | 11 | font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen", 12 | "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue", 13 | sans-serif; 14 | -webkit-font-smoothing: antialiased; 15 | -moz-osx-font-smoothing: grayscale; 16 | } 17 | 18 | code { 19 | font-family: source-code-pro, Menlo, Monaco, Consolas, "Courier New", 20 | monospace; 21 | } 22 | -------------------------------------------------------------------------------- /packages/react/src/types/ac.ts: -------------------------------------------------------------------------------- 1 | export type AcMode = 'cold' | 'hot' 2 | 3 | export interface AcState { 4 | /** 5 | * 状态 6 | */ 7 | status: boolean 8 | /** 9 | * 模式 10 | */ 11 | mode: AcMode 12 | /** 13 | * 温度 14 | */ 15 | temperature: number 16 | } 17 | -------------------------------------------------------------------------------- /packages/react/src/types/index.ts: -------------------------------------------------------------------------------- 1 | export * from './ac' 2 | -------------------------------------------------------------------------------- /packages/react/src/utils/adsense/google.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * 谷歌自动广告 3 | */ 4 | export function GoogleAutoAdsense() { 5 | return ( 6 | 12 | ) 13 | } 14 | 15 | /** 16 | * 谷歌广告单元 17 | */ 18 | export function GoogleAdsenseUnit() { 19 | return ( 20 |
21 | 26 | {/* 横向广告 */} 27 | 41 | 42 | 47 |
48 | ) 49 | } 50 | -------------------------------------------------------------------------------- /packages/react/src/utils/adsense/index.ts: -------------------------------------------------------------------------------- 1 | import { ga } from 'react-ga' 2 | 3 | /** 4 | * 广告链接(云空调后记) 5 | */ 6 | export const adsenseLink = 'https://mp.weixin.qq.com/s/WRZgds9PlH5MBxlhJYOj8g' 7 | 8 | /** 9 | * 跳转至公众号广告 10 | * 「你想用钱来收买我吗?这是对我的侮辱!我本想这样大声呵斥他,但钱实在是太多了」 11 | */ 12 | export function jumpToAdsense() { 13 | ga('send', { 14 | hitType: 'event', 15 | eventCategory: 'Outbound Link', 16 | eventAction: 'click', 17 | eventLabel: 'Summer Adsense', 18 | }) 19 | window.open(adsenseLink) 20 | } 21 | -------------------------------------------------------------------------------- /packages/react/src/utils/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * 是否为生产环境 3 | */ 4 | export const isProd = import.meta.env.PROD 5 | 6 | /** 7 | * 获取资源 URL 8 | * @param url 9 | */ 10 | export function getAssetsUrl(url: string) { 11 | const jsdelivrCDN 12 | = 'https://fastly.jsdelivr.net/gh/YunYouJun/air-conditioner/public' 13 | return (isProd ? jsdelivrCDN : import.meta.env.BASE_URL) + url.startsWith('/') 14 | ? url.slice(1) 15 | : url 16 | } 17 | -------------------------------------------------------------------------------- /packages/react/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ESNext", 4 | "jsx": "react-jsx", 5 | "lib": [ 6 | "DOM", 7 | "DOM.Iterable", 8 | "ESNext" 9 | ], 10 | "useDefineForClassFields": true, 11 | "baseUrl": ".", 12 | "module": "ESNext", 13 | "moduleResolution": "Node", 14 | "paths": { 15 | "~/*": ["src/*"] 16 | }, 17 | "resolveJsonModule": true, 18 | "types": [ 19 | "vite/client" 20 | ], 21 | "allowJs": false, 22 | "strict": true, 23 | "noFallthroughCasesInSwitch": true, 24 | "noEmit": true, 25 | "allowSyntheticDefaultImports": true, 26 | "esModuleInterop": false, 27 | "forceConsistentCasingInFileNames": true, 28 | "isolatedModules": true, 29 | "skipLibCheck": false 30 | }, 31 | "references": [{ "path": "./tsconfig.node.json" }], 32 | "include": ["src"] 33 | } 34 | -------------------------------------------------------------------------------- /packages/react/tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "composite": true, 4 | "module": "esnext", 5 | "moduleResolution": "node" 6 | }, 7 | "include": ["vite.config.ts"] 8 | } 9 | -------------------------------------------------------------------------------- /packages/react/vite.config.ts: -------------------------------------------------------------------------------- 1 | import * as path from 'node:path' 2 | import react from '@vitejs/plugin-react' 3 | import Unocss from 'unocss/vite' 4 | 5 | import { defineConfig } from 'vite' 6 | import Pages from 'vite-plugin-pages' 7 | import { VitePWA } from 'vite-plugin-pwa' 8 | 9 | // https://vitejs.dev/config/ 10 | export default defineConfig({ 11 | resolve: { 12 | alias: { 13 | '~/': `${path.resolve(__dirname, 'src')}/`, 14 | }, 15 | }, 16 | plugins: [ 17 | react(), 18 | 19 | Unocss(), 20 | 21 | // https://github.com/hannoeru/vite-plugin-pages 22 | Pages(), 23 | 24 | VitePWA({ 25 | registerType: 'autoUpdate', 26 | includeAssets: ['favicon.svg', 'robots.txt'], 27 | manifest: { 28 | name: '便携小空调', 29 | short_name: '云空调', 30 | theme_color: '#000000', 31 | start_url: '.', 32 | display: 'standalone', 33 | background_color: '#ffffff', 34 | icons: [ 35 | { 36 | src: 'favicon.svg', 37 | type: 'image/png', 38 | sizes: '64x64', 39 | }, 40 | ], 41 | }, 42 | }), 43 | ], 44 | }) 45 | -------------------------------------------------------------------------------- /packages/vue/README.md: -------------------------------------------------------------------------------- 1 | # @air-conditioner/vue 2 | 3 | Vue 重构版本 4 | -------------------------------------------------------------------------------- /playwright.config.ts: -------------------------------------------------------------------------------- 1 | import process from 'node:process' 2 | import { defineConfig, devices } from '@playwright/test' 3 | 4 | /** 5 | * Read environment variables from file. 6 | * https://github.com/motdotla/dotenv 7 | */ 8 | // import dotenv from 'dotenv'; 9 | // import path from 'path'; 10 | // dotenv.config({ path: path.resolve(__dirname, '.env') }); 11 | 12 | /** 13 | * See https://playwright.dev/docs/test-configuration. 14 | */ 15 | export default defineConfig({ 16 | testDir: './tests', 17 | /* Run tests in files in parallel */ 18 | fullyParallel: true, 19 | /* Fail the build on CI if you accidentally left test.only in the source code. */ 20 | forbidOnly: !!process.env.CI, 21 | /* Retry on CI only */ 22 | retries: process.env.CI ? 2 : 0, 23 | /* Opt out of parallel tests on CI. */ 24 | workers: process.env.CI ? 1 : undefined, 25 | /* Reporter to use. See https://playwright.dev/docs/test-reporters */ 26 | reporter: 'html', 27 | /* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */ 28 | use: { 29 | /* Base URL to use in actions like `await page.goto('/')`. */ 30 | baseURL: 'http://127.0.0.1:3000', 31 | 32 | /* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */ 33 | trace: 'on-first-retry', 34 | }, 35 | 36 | /* Configure projects for major browsers */ 37 | projects: [ 38 | { 39 | name: 'chromium', 40 | use: { ...devices['Desktop Chrome'] }, 41 | }, 42 | 43 | { 44 | name: 'firefox', 45 | use: { ...devices['Desktop Firefox'] }, 46 | }, 47 | 48 | { 49 | name: 'webkit', 50 | use: { ...devices['Desktop Safari'] }, 51 | }, 52 | 53 | /* Test against mobile viewports. */ 54 | // { 55 | // name: 'Mobile Chrome', 56 | // use: { ...devices['Pixel 5'] }, 57 | // }, 58 | // { 59 | // name: 'Mobile Safari', 60 | // use: { ...devices['iPhone 12'] }, 61 | // }, 62 | 63 | /* Test against branded browsers. */ 64 | // { 65 | // name: 'Microsoft Edge', 66 | // use: { ...devices['Desktop Edge'], channel: 'msedge' }, 67 | // }, 68 | // { 69 | // name: 'Google Chrome', 70 | // use: { ...devices['Desktop Chrome'], channel: 'chrome' }, 71 | // }, 72 | ], 73 | 74 | /* Run your local dev server before starting the tests */ 75 | // webServer: { 76 | // command: 'npm run start', 77 | // url: 'http://127.0.0.1:3000', 78 | // reuseExistingServer: !process.env.CI, 79 | // }, 80 | }) 81 | -------------------------------------------------------------------------------- /pnpm-workspace.yaml: -------------------------------------------------------------------------------- 1 | packages: 2 | - packages/* 3 | catalog: 4 | '@antfu/eslint-config': ^3.12.1 5 | '@emotion/react': ^11.14.0 6 | '@emotion/styled': ^11.14.0 7 | '@iconify-json/ic': ^1.2.2 8 | '@iconify-json/mdi': ^1.2.2 9 | '@mui/material': ^5.16.13 10 | '@playwright/test': ^1.49.1 11 | '@types/node': ^22.10.2 12 | '@types/react': ^18.3.18 13 | '@types/react-dom': ^18.3.5 14 | '@types/react-gtm-module': 2.0.3 15 | '@types/react-router-dom': ^5.3.3 16 | '@vitejs/plugin-react': ^4.3.4 17 | bumpp: ^9.9.2 18 | eslint: ^9.17.0 19 | react: ^18.3.1 20 | react-dom: ^18.3.1 21 | react-ga: ^3.3.1 22 | react-gtm-module: 2.0.11 23 | react-router-dom: ^6.28.1 24 | react-transition-group: ^4.4.5 25 | sass: ^1.83.0 26 | typescript: ^5.7.2 27 | unocss: ^0.65.3 28 | usehooks-ts: ^3.1.0 29 | vite: ^6.0.6 30 | vite-plugin-pages: ^0.32.4 31 | vite-plugin-pwa: ^0.21.1 32 | web-vitals: ^4.2.4 33 | -------------------------------------------------------------------------------- /tests/example.spec.ts: -------------------------------------------------------------------------------- 1 | import { expect, test } from '@playwright/test' 2 | 3 | test('has title', async ({ page }) => { 4 | await page.goto('https://playwright.dev/') 5 | 6 | // Expect a title "to contain" a substring. 7 | await expect(page).toHaveTitle(/Playwright/) 8 | }) 9 | 10 | test('get started link', async ({ page }) => { 11 | await page.goto('https://playwright.dev/') 12 | 13 | // Click the get started link. 14 | await page.getByRole('link', { name: 'Get started' }).click() 15 | 16 | // Expects page to have a heading with the name of Installation. 17 | await expect(page.getByRole('heading', { name: 'Installation' })).toBeVisible() 18 | }) 19 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ESNext", 4 | "jsx": "preserve", 5 | "lib": ["DOM", "ESNext"], 6 | "baseUrl": ".", 7 | "module": "ESNext", 8 | "moduleResolution": "Bundler", 9 | "paths": { 10 | "~/*": ["./packages/react/src/*"] 11 | }, 12 | "resolveJsonModule": true, 13 | "types": [ 14 | "vite/client" 15 | ], 16 | "allowJs": true, 17 | "strict": true, 18 | "strictNullChecks": true, 19 | "noEmit": true, 20 | "esModuleInterop": true, 21 | "skipDefaultLibCheck": true, 22 | "skipLibCheck": true 23 | }, 24 | "exclude": ["dist", "node_modules"] 25 | } 26 | -------------------------------------------------------------------------------- /unocss.config.ts: -------------------------------------------------------------------------------- 1 | import { 2 | defineConfig, 3 | presetAttributify, 4 | presetIcons, 5 | presetUno, 6 | } from 'unocss' 7 | 8 | export default defineConfig({ 9 | safelist: [], 10 | 11 | presets: [ 12 | presetUno(), 13 | presetAttributify(), 14 | presetIcons({ 15 | scale: 1.2, 16 | warn: true, 17 | }), 18 | ], 19 | }) 20 | --------------------------------------------------------------------------------