├── .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 |
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 | [](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 |
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 | [](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 |
--------------------------------------------------------------------------------
/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 |
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 |
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 |
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 |
--------------------------------------------------------------------------------