diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 60721b6..6c54e01 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -9,18 +9,13 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 - - name: Use Node.js 20 - uses: actions/setup-node@v4 - with: - node-version: 20.x - cache: 'npm' - - name: Install dependencies - run: npm i - - name: Build - run: npm run build - env: - POSTHOG_API_KEY: ${{ secrets.POSTHOG_API_KEY }} - POSTHOG_PROJECT_ID: ${{ secrets.POSTHOG_PROJECT_ID }} - POSTHOG_HOST: ${{ secrets.POSTHOG_HOST }} - - run: npm run lint:ci + - uses: actions/checkout@v4 + - name: Use Node.js 20 + uses: actions/setup-node@v4 + with: + node-version: 20.x + cache: 'npm' + - name: Install dependencies + run: npm i + - run: npm run build + - run: npm run lint:ci diff --git a/AUTOMATED_RELEASE.md b/AUTOMATED_RELEASE.md index 7ed5329..31f017f 100644 --- a/AUTOMATED_RELEASE.md +++ b/AUTOMATED_RELEASE.md @@ -12,15 +12,16 @@ Builds and (optionally) uploads signed Android and iOS release artifacts from th Common flags on `release.sh`: -| Flag | Effect | -|---|---| -| `--android` / `--ios` | Build only one platform | -| `--bump minor\|major` | Bump type (default: `patch`) | -| `--skip-bump` | Build with the current version, don't bump | -| `--upload` | Upload after building | -| `--track TRACK` | Play Store track for `--upload` (default: `internal`) | +| Flag | Effect | +| --------------------- | ----------------------------------------------------- | +| `--android` / `--ios` | Build only one platform | +| `--bump minor\|major` | Bump type (default: `patch`) | +| `--skip-bump` | Build with the current version, don't bump | +| `--upload` | Upload after building | +| `--track TRACK` | Play Store track for `--upload` (default: `internal`) | Outputs: + - Android: `android/app/build/outputs/bundle/release/app-release.aab` - iOS: `build/ios/Donetick.ipa` @@ -36,15 +37,15 @@ Outputs: All secrets live in Vaultwarden (`https://www.bitwarden.com`) as Secure Notes, named exactly as below: -| Vault item name | Written to | Encoding | -|---|---|---| -| Donetick Google Services Android | `android/app/google-services.json` | raw | -| Donetick Android Keystore | `android/app/release/donetick.jks` | base64 | -| Donetick Keystore Password | (used inline for `android/keystore.properties`) | raw | -| Donetick Google Play Service Account | `android/play-service-account.json` | raw | -| Donetick Google Services iOS | `ios/App/App/GoogleService-Info.plist` | raw | -| Donetick App Store Connect Key | `ios/AuthKey_84F695CDQ3.p8` | base64 | -| Donetick Env Production | `.env.production` | raw | +| Vault item name | Written to | Encoding | +| ------------------------------------ | ----------------------------------------------- | -------- | +| Donetick Google Services Android | `android/app/google-services.json` | raw | +| Donetick Android Keystore | `android/app/release/donetick.jks` | base64 | +| Donetick Keystore Password | (used inline for `android/keystore.properties`) | raw | +| Donetick Google Play Service Account | `android/play-service-account.json` | raw | +| Donetick Google Services iOS | `ios/App/App/GoogleService-Info.plist` | raw | +| Donetick App Store Connect Key | `ios/AuthKey_84F695CDQ3.p8` | base64 | +| Donetick Env Production | `.env.production` | raw | None of these files are committed to git — all covered by `.gitignore`. @@ -57,6 +58,7 @@ The App target uses **manual signing for Release**, not Automatic. This was a de - `fastlane/Fastfile`'s `ios release` lane passes explicit `export_options: { signingStyle: "manual", provisioningProfiles: {...} }` to `build_app` — do **not** switch this back to `-allowProvisioningUpdates`/automatic without a good reason, it re-triggers the ambiguous cert selection. **If you ever add/change an entitlement** (new Capability in Signing & Capabilities, e.g. a new permission), the existing provisioning profile becomes stale and exports will fail with an error like `"App.app" requires a provisioning profile with the X feature`. Fix: + 1. developer.apple.com → Identifiers → `com.donetick.app` → confirm the new capability is checked 2. developer.apple.com → Profiles → find `Donetick App Store(fastline)` → regenerate it → download 3. Install it locally: get its UUID (`security cms -D -i .mobileprovision | plutil -extract UUID xml1 -o - -`) and copy to `~/Library/MobileDevice/Provisioning Profiles/.mobileprovision` diff --git a/android/app/release/output-metadata.json b/android/app/release/output-metadata.json index 2b10993..c6dd734 100644 --- a/android/app/release/output-metadata.json +++ b/android/app/release/output-metadata.json @@ -21,17 +21,13 @@ { "minApi": 28, "maxApi": 30, - "baselineProfiles": [ - "baselineProfiles/1/app-release.dm" - ] + "baselineProfiles": ["baselineProfiles/1/app-release.dm"] }, { "minApi": 31, "maxApi": 2147483647, - "baselineProfiles": [ - "baselineProfiles/0/app-release.dm" - ] + "baselineProfiles": ["baselineProfiles/0/app-release.dm"] } ], "minSdkVersionForDexing": 28 -} \ No newline at end of file +} diff --git a/android/app/src/main/res/.impeccable/hook.cache.json b/android/app/src/main/res/.impeccable/hook.cache.json index ebc7c5d..9f68402 100644 --- a/android/app/src/main/res/.impeccable/hook.cache.json +++ b/android/app/src/main/res/.impeccable/hook.cache.json @@ -1 +1 @@ -{"version":1,"sessions":{}} \ No newline at end of file +{ "version": 1, "sessions": {} } diff --git a/crowdin.yml b/crowdin.yml index 03c65e7..a3c2fc1 100644 --- a/crowdin.yml +++ b/crowdin.yml @@ -1,7 +1,7 @@ # Crowdin configuration for Donetick # See: https://support.crowdin.com/configuration-file/ -project_id: "donetick" +project_id: 'donetick' api_token_env: CROWDIN_API_TOKEN preserve_hierarchy: true diff --git a/e2e/debug.mjs b/e2e/debug.mjs index c575e8f..a278f09 100644 --- a/e2e/debug.mjs +++ b/e2e/debug.mjs @@ -6,7 +6,9 @@ const browser = await chromium.launch() const context = await browser.newContext({ storageState: state }) const page = await context.newPage() page.on('console', msg => console.log('[console]', msg.type(), msg.text())) -page.on('pageerror', err => console.log('[pageerror]', err.message, '\n', err.stack)) +page.on('pageerror', err => + console.log('[pageerror]', err.message, '\n', err.stack), +) await page.goto('http://localhost:5173/chores/create') await page.getByTestId('chore-name-input').fill('Debug Chore ' + Date.now()) diff --git a/e2e/fixtures/auth.js b/e2e/fixtures/auth.js index 06b1747..94b1c32 100644 --- a/e2e/fixtures/auth.js +++ b/e2e/fixtures/auth.js @@ -1,4 +1,4 @@ -import { test as base, expect } from '@playwright/test' +import { expect, test as base } from '@playwright/test' import path from 'path' import { fileURLToPath } from 'url' @@ -9,7 +9,10 @@ const __dirname = path.dirname(fileURLToPath(import.meta.url)) * After successful signup the app auto-logs in and walks through the * onboarding flow (/circle-setup, then /ready) before landing on /chores. */ -export async function signUpViaUI(page, { username, email, password, displayName }) { +export async function signUpViaUI( + page, + { displayName, email, password, username }, +) { await page.goto('/signup') await page.locator('#username').fill(username) await page.locator('#email').fill(email) @@ -30,7 +33,7 @@ export async function signUpViaUI(page, { username, email, password, displayName * Fill and submit the login form through the UI. * After successful login the app redirects to /chores. */ -export async function loginViaUI(page, { username, password }) { +export async function loginViaUI(page, { password, username }) { await page.goto('/login') await page.locator('#username').fill(username) await page.locator('#password').fill(password) @@ -43,12 +46,12 @@ export async function loginViaUI(page, { username, password }) { * shared E2E user (via persisted storage state, no UI interaction required). */ export const test = base.extend({ - authenticatedPage: async ({ browser }, use) => { + authenticatedPage: async ({ browser }, callback) => { const ctx = await browser.newContext({ storageState: path.join(__dirname, '..', '.auth', 'state.json'), }) const page = await ctx.newPage() - await use(page) + await callback(page) await ctx.close() }, }) diff --git a/e2e/global-setup.js b/e2e/global-setup.js index 1d949d9..e509587 100644 --- a/e2e/global-setup.js +++ b/e2e/global-setup.js @@ -1,4 +1,4 @@ -import { writeFile, mkdir } from 'fs/promises' +import { mkdir, writeFile } from 'fs/promises' import path from 'path' import { fileURLToPath } from 'url' @@ -56,7 +56,7 @@ export default async function globalSetup() { throw new Error(`Login failed (${loginRes.status}): ${body}`) } - const { token, expire } = await loginRes.json() + const { expire, token } = await loginRes.json() // Write a Playwright storage-state file containing the token in localStorage const stateDir = path.join(__dirname, '.auth') @@ -84,7 +84,11 @@ export default async function globalSetup() { async function waitForServer(url, retries = 20, delayMs = 1000) { for (let i = 0; i < retries; i++) { try { - const res = await fetch(`${url}/api/v1/auth/login`, { method: 'POST', body: '{}', headers: { 'Content-Type': 'application/json' } }) + const res = await fetch(`${url}/api/v1/auth/login`, { + method: 'POST', + body: '{}', + headers: { 'Content-Type': 'application/json' }, + }) if (res.status < 500) return } catch { // server not up yet diff --git a/e2e/tests/auth.spec.js b/e2e/tests/auth.spec.js index 9c049a7..508e0a3 100644 --- a/e2e/tests/auth.spec.js +++ b/e2e/tests/auth.spec.js @@ -1,12 +1,14 @@ -import { test, expect } from '@playwright/test' -import { signUpViaUI, loginViaUI } from '../fixtures/auth.js' +import { expect, test } from '@playwright/test' + +import { loginViaUI, signUpViaUI } from '../fixtures/auth.js' // Username must match /^[a-z.-]+$/ — no digits allowed. // Generate a random lowercase-only suffix for uniqueness across runs. function randomSuffix(len = 8) { const chars = 'abcdefghijklmnopqrstuvwxyz' - return Array.from({ length: len }, () => - chars[Math.floor(Math.random() * 26)], + return Array.from( + { length: len }, + () => chars[Math.floor(Math.random() * 26)], ).join('') } @@ -41,7 +43,9 @@ test.describe('Auth – Sign Up', () => { test.describe('Auth – Login', () => { // Re-use the shared E2E user that global-setup already created - test('logs in with valid credentials and lands on /chores', async ({ page }) => { + test('logs in with valid credentials and lands on /chores', async ({ + page, + }) => { await loginViaUI(page, { username: 'e2e.user', password: 'E2ePassword123!', diff --git a/eslint.config.mjs b/eslint.config.mjs index 9dbb2d3..53c06bc 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -56,6 +56,15 @@ export default [ globals: globals.node, }, }, + { + files: ['e2e/**/*.{js,mjs}'], + languageOptions: { + globals: { + ...globals.node, + ...globals.browser, + }, + }, + }, { files: ['*.config.{mjs,ts}'], languageOptions: { diff --git a/fastlane/README.md b/fastlane/README.md index e93f221..8f40103 100644 --- a/fastlane/README.md +++ b/fastlane/README.md @@ -31,8 +31,7 @@ Build a signed, App Store-ready .ipa Upload the built .ipa to TestFlight ----- - +--- ## Android @@ -60,7 +59,7 @@ Build a signed release .apk (for sideloading/testing) Upload the built .aab to the Play Store (internal testing track by default) ----- +--- This README.md is auto-generated and will be re-generated every time [_fastlane_](https://fastlane.tools) is run. diff --git a/ionic.config.json b/ionic.config.json index a61dc93..087d13b 100644 --- a/ionic.config.json +++ b/ionic.config.json @@ -4,4 +4,4 @@ "capacitor": {} }, "type": "custom" -} \ No newline at end of file +} diff --git a/ios/App/App/Assets.xcassets/AppIcon.appiconset/Contents.json b/ios/App/App/Assets.xcassets/AppIcon.appiconset/Contents.json index b1a8483..5e294d5 100644 --- a/ios/App/App/Assets.xcassets/AppIcon.appiconset/Contents.json +++ b/ios/App/App/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -11,4 +11,4 @@ "author": "xcode", "version": 1 } -} \ No newline at end of file +} diff --git a/ios/App/App/Assets.xcassets/Contents.json b/ios/App/App/Assets.xcassets/Contents.json index da4a164..97a8662 100644 --- a/ios/App/App/Assets.xcassets/Contents.json +++ b/ios/App/App/Assets.xcassets/Contents.json @@ -1,6 +1,6 @@ { - "info" : { - "version" : 1, - "author" : "xcode" + "info": { + "version": 1, + "author": "xcode" } -} \ No newline at end of file +} diff --git a/ios/App/App/Assets.xcassets/Splash.imageset/Contents.json b/ios/App/App/Assets.xcassets/Splash.imageset/Contents.json index 84a416d..57ba80d 100644 --- a/ios/App/App/Assets.xcassets/Splash.imageset/Contents.json +++ b/ios/App/App/Assets.xcassets/Splash.imageset/Contents.json @@ -53,4 +53,4 @@ "version": 1, "author": "xcode" } -} \ No newline at end of file +} diff --git a/package-lock.json b/package-lock.json index 3a8cd22..84fc72d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -107,6 +107,7 @@ "eslint-plugin-sort-destructure-keys": "^3.0.0", "eslint-plugin-tailwindcss": "^3.18.3", "husky": "^8.0.3", + "lint-staged": "^16.4.0", "patch-package": "^8.0.1", "postcss": "^8.4.32", "prettier": "^3.8.3", @@ -6335,6 +6336,22 @@ "url": "https://github.com/sponsors/epoberezkin" } }, + "node_modules/ansi-escapes": { + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-7.3.0.tgz", + "integrity": "sha512-BvU8nYgGQBxcmMuEeUEmNTvrMVjJNSH7RgW24vXexN4Ven6qCvy4TntnvlnwnMLTVlcRQQdbRY8NKnaIoeWDNg==", + "dev": true, + "license": "MIT", + "dependencies": { + "environment": "^1.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/ansi-regex": { "version": "5.0.1", "license": "MIT", @@ -7142,6 +7159,22 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/cli-cursor": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-5.0.0.tgz", + "integrity": "sha512-aCj4O5wKyszjMmDT4tZj93kxyydN/K5zPWSCe6/0AV/AA1pqe5ZBIw0a2ZfPQV7lL5/yb5HsUreJ6UFAF1tEQw==", + "dev": true, + "license": "MIT", + "dependencies": { + "restore-cursor": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/cli-progress": { "version": "3.12.0", "resolved": "https://registry.npmjs.org/cli-progress/-/cli-progress-3.12.0.tgz", @@ -7155,6 +7188,115 @@ "node": ">=4" } }, + "node_modules/cli-truncate": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/cli-truncate/-/cli-truncate-5.2.0.tgz", + "integrity": "sha512-xRwvIOMGrfOAnM1JYtqQImuaNtDEv9v6oIYAs4LIHwTiKee8uwvIi363igssOC0O5U04i4AlENs79LQLu9tEMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "slice-ansi": "^8.0.0", + "string-width": "^8.2.0" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cli-truncate/node_modules/ansi-regex": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", + "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/cli-truncate/node_modules/ansi-styles": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/cli-truncate/node_modules/is-fullwidth-code-point": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-5.1.0.tgz", + "integrity": "sha512-5XHYaSyiqADb4RnZ1Bdad6cPp8Toise4TzEjcOYDHZkTCbKgiUl7WTUCpNWHuxmDt91wnsZBc9xinNzopv3JMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "get-east-asian-width": "^1.3.1" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cli-truncate/node_modules/slice-ansi": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-8.0.0.tgz", + "integrity": "sha512-stxByr12oeeOyY2BlviTNQlYV5xOj47GirPr4yA1hE9JCtxfQN0+tVbkxwCtYDQWhEKWFHsEK48ORg5jrouCAg==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.2.3", + "is-fullwidth-code-point": "^5.1.0" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/chalk/slice-ansi?sponsor=1" + } + }, + "node_modules/cli-truncate/node_modules/string-width": { + "version": "8.2.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-8.2.2.tgz", + "integrity": "sha512-GaPUh5gfdrYzqeVNZvUfT23vYYxXzKYidUcnMtJg/3rxRV63EFZy3k6xfKlmfeJD0176lnUV/Usr3XcwSvFzpg==", + "dev": true, + "license": "MIT", + "dependencies": { + "get-east-asian-width": "^1.5.0", + "strip-ansi": "^7.1.2" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cli-truncate/node_modules/strip-ansi": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz", + "integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.2.2" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, "node_modules/cliui": { "version": "8.0.1", "dev": true, @@ -8167,6 +8309,19 @@ "node": ">=6" } }, + "node_modules/environment": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/environment/-/environment-1.1.0.tgz", + "integrity": "sha512-xUtoPkMggbz0MPyPiIWr1Kp4aeWJjDZ6SMvURhimjdZgsRuDplF5/s9hcgGhyXMhs+6vpnuoiZ2kFiu3FMnS8Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/error-ex": { "version": "1.3.2", "license": "MIT", @@ -9235,6 +9390,19 @@ "node": "6.* || 8.* || >= 10.*" } }, + "node_modules/get-east-asian-width": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.6.0.tgz", + "integrity": "sha512-QRbvDIbx6YklUe6RxeTeleMR0yv3cYH6PsPZHcnVn7xv7zO1BHN8r0XETu8n6Ye3Q+ahtSarc3WgtNWmehIBfA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/get-intrinsic": { "version": "1.3.0", "license": "MIT", @@ -10711,6 +10879,172 @@ "version": "1.2.4", "license": "MIT" }, + "node_modules/lint-staged": { + "version": "16.4.0", + "resolved": "https://registry.npmjs.org/lint-staged/-/lint-staged-16.4.0.tgz", + "integrity": "sha512-lBWt8hujh/Cjysw5GYVmZpFHXDCgZzhrOm8vbcUdobADZNOK/bRshr2kM3DfgrrtR1DQhfupW9gnIXOfiFi+bw==", + "dev": true, + "license": "MIT", + "dependencies": { + "commander": "^14.0.3", + "listr2": "^9.0.5", + "picomatch": "^4.0.3", + "string-argv": "^0.3.2", + "tinyexec": "^1.0.4", + "yaml": "^2.8.2" + }, + "bin": { + "lint-staged": "bin/lint-staged.js" + }, + "engines": { + "node": ">=20.17" + }, + "funding": { + "url": "https://opencollective.com/lint-staged" + } + }, + "node_modules/lint-staged/node_modules/commander": { + "version": "14.0.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-14.0.3.tgz", + "integrity": "sha512-H+y0Jo/T1RZ9qPP4Eh1pkcQcLRglraJaSLoyOtHxu6AapkjWVCy2Sit1QQ4x3Dng8qDlSsZEet7g5Pq06MvTgw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20" + } + }, + "node_modules/lint-staged/node_modules/picomatch": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.5.tgz", + "integrity": "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/lint-staged/node_modules/yaml": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.9.0.tgz", + "integrity": "sha512-2AvhNX3mb8zd6Zy7INTtSpl1F15HW6Wnqj0srWlkKLcpYl/gMIMJiyuGq2KeI2YFxUPjdlB+3Lc10seMLtL4cA==", + "dev": true, + "license": "ISC", + "bin": { + "yaml": "bin.mjs" + }, + "engines": { + "node": ">= 14.6" + }, + "funding": { + "url": "https://github.com/sponsors/eemeli" + } + }, + "node_modules/listr2": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/listr2/-/listr2-9.0.5.tgz", + "integrity": "sha512-ME4Fb83LgEgwNw96RKNvKV4VTLuXfoKudAmm2lP8Kk87KaMK0/Xrx/aAkMWmT8mDb+3MlFDspfbCs7adjRxA2g==", + "dev": true, + "license": "MIT", + "dependencies": { + "cli-truncate": "^5.0.0", + "colorette": "^2.0.20", + "eventemitter3": "^5.0.1", + "log-update": "^6.1.0", + "rfdc": "^1.4.1", + "wrap-ansi": "^9.0.0" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/listr2/node_modules/ansi-regex": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", + "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/listr2/node_modules/ansi-styles": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/listr2/node_modules/emoji-regex": { + "version": "10.6.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.6.0.tgz", + "integrity": "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==", + "dev": true, + "license": "MIT" + }, + "node_modules/listr2/node_modules/string-width": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", + "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^10.3.0", + "get-east-asian-width": "^1.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/listr2/node_modules/strip-ansi": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz", + "integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.2.2" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/listr2/node_modules/wrap-ansi": { + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-9.0.2.tgz", + "integrity": "sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.2.1", + "string-width": "^7.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, "node_modules/load-json-file": { "version": "4.0.0", "dev": true, @@ -10823,6 +11157,144 @@ "version": "4.1.1", "license": "MIT" }, + "node_modules/log-update": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/log-update/-/log-update-6.1.0.tgz", + "integrity": "sha512-9ie8ItPR6tjY5uYJh8K/Zrv/RMZ5VOlOWvtZdEHYSTFKZfIBPQa9tOAEeAWhd+AnIneLJ22w5fjOYtoutpWq5w==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-escapes": "^7.0.0", + "cli-cursor": "^5.0.0", + "slice-ansi": "^7.1.0", + "strip-ansi": "^7.1.0", + "wrap-ansi": "^9.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/log-update/node_modules/ansi-regex": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", + "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/log-update/node_modules/ansi-styles": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/log-update/node_modules/emoji-regex": { + "version": "10.6.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.6.0.tgz", + "integrity": "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==", + "dev": true, + "license": "MIT" + }, + "node_modules/log-update/node_modules/is-fullwidth-code-point": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-5.1.0.tgz", + "integrity": "sha512-5XHYaSyiqADb4RnZ1Bdad6cPp8Toise4TzEjcOYDHZkTCbKgiUl7WTUCpNWHuxmDt91wnsZBc9xinNzopv3JMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "get-east-asian-width": "^1.3.1" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/log-update/node_modules/slice-ansi": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-7.1.2.tgz", + "integrity": "sha512-iOBWFgUX7caIZiuutICxVgX1SdxwAVFFKwt1EvMYYec/NWO5meOJ6K5uQxhrYBdQJne4KxiqZc+KptFOWFSI9w==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.2.1", + "is-fullwidth-code-point": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/chalk/slice-ansi?sponsor=1" + } + }, + "node_modules/log-update/node_modules/string-width": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", + "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^10.3.0", + "get-east-asian-width": "^1.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/log-update/node_modules/strip-ansi": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz", + "integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.2.2" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/log-update/node_modules/wrap-ansi": { + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-9.0.2.tgz", + "integrity": "sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.2.1", + "string-width": "^7.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, "node_modules/loose-envify": { "version": "1.4.0", "license": "MIT", @@ -11080,6 +11552,19 @@ "node": ">=8" } }, + "node_modules/mimic-function": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/mimic-function/-/mimic-function-5.0.1.tgz", + "integrity": "sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/mimic-response": { "version": "3.1.0", "devOptional": true, @@ -11594,6 +12079,22 @@ "wrappy": "1" } }, + "node_modules/onetime": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-7.0.0.tgz", + "integrity": "sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "mimic-function": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/open": { "version": "8.4.2", "dev": true, @@ -13442,6 +13943,36 @@ "node": ">=4" } }, + "node_modules/restore-cursor": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-5.1.0.tgz", + "integrity": "sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA==", + "dev": true, + "license": "MIT", + "dependencies": { + "onetime": "^7.0.0", + "signal-exit": "^4.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/restore-cursor/node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/reusify": { "version": "1.1.0", "license": "MIT", @@ -13450,6 +13981,13 @@ "node": ">=0.10.0" } }, + "node_modules/rfdc": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.4.1.tgz", + "integrity": "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==", + "dev": true, + "license": "MIT" + }, "node_modules/rimraf": { "version": "6.0.1", "dev": true, @@ -14051,6 +14589,16 @@ "safe-buffer": "~5.1.0" } }, + "node_modules/string-argv": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/string-argv/-/string-argv-0.3.2.tgz", + "integrity": "sha512-aqD2Q0144Z+/RqG52NeHEkZauTAUWJO8c6yTftGJKO3Tja5tUgIfmIl6kExvhtxSDP7fXB6DvzkfMpCd/F3G+Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.6.19" + } + }, "node_modules/string-width": { "version": "4.2.3", "license": "MIT", @@ -14798,6 +15346,16 @@ "version": "1.3.3", "license": "MIT" }, + "node_modules/tinyexec": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.2.4.tgz", + "integrity": "sha512-SHf/r48b7vOrjve9PxJo3MN5v5yuyjHvdUcrQffT3WXMUfnGmHDVbC4k3sHJaJTgZCwpUplIaAo5ANtMyp3YHg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, "node_modules/tinyglobby": { "version": "0.2.16", "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.16.tgz", diff --git a/package.json b/package.json index 53cbd13..b8056ae 100644 --- a/package.json +++ b/package.json @@ -21,7 +21,7 @@ "build-selfhosted": "rm -rf package-lock.json && npm install && npm install --force @rollup/rollup-linux-x64-gnu@4.34.9 @swc/core-linux-x64-gnu && vite build --mode selfhosted", "build-win": "del package-lock.json && npm install && npm install --force @rollup/rollup-win32-x64-msvc @swc/core-win32-x64-msvc && vite build --mode selfhosted", "lint": "eslint && prettier -c .", - "lint:ci": "true # TODO: eslint -f gha && prettier -c .", + "lint:ci": "eslint -f gha && prettier -c .", "lint:fix": "eslint --fix && prettier -w .", "preview": "vite preview", "setup-m1": "rm -rf node_modules package-lock.json && npm install && npm install --force @rollup/rollup-darwin-arm64 @swc/core-darwin-arm64", @@ -139,6 +139,7 @@ "eslint-plugin-sort-destructure-keys": "^3.0.0", "eslint-plugin-tailwindcss": "^3.18.3", "husky": "^8.0.3", + "lint-staged": "^16.4.0", "patch-package": "^8.0.1", "postcss": "^8.4.32", "prettier": "^3.8.3", diff --git a/public/.well-known/apple-app-site-association.json b/public/.well-known/apple-app-site-association.json index 70aab72..6e2da30 100644 --- a/public/.well-known/apple-app-site-association.json +++ b/public/.well-known/apple-app-site-association.json @@ -8,4 +8,4 @@ } ] } -} \ No newline at end of file +} diff --git a/public/locales/he/settings.json b/public/locales/he/settings.json index f2262f8..fa8e1d1 100644 --- a/public/locales/he/settings.json +++ b/public/locales/he/settings.json @@ -492,4 +492,3 @@ } } } - diff --git a/public/locales/zh-CN/chores.json b/public/locales/zh-CN/chores.json index 3f966f1..60ddfe1 100644 --- a/public/locales/zh-CN/chores.json +++ b/public/locales/zh-CN/chores.json @@ -72,4 +72,4 @@ "delete": "删除会话" } } -} \ No newline at end of file +} diff --git a/src/components/NotificationTemplate.jsx b/src/components/NotificationTemplate.jsx index d942681..2db2d90 100644 --- a/src/components/NotificationTemplate.jsx +++ b/src/components/NotificationTemplate.jsx @@ -14,6 +14,7 @@ import Select from '@mui/joy/Select' import Typography from '@mui/joy/Typography' import { useCallback, useEffect, useRef, useState } from 'react' import { useTranslation } from 'react-i18next' + import { NOTIFICATION_TYPE, TASK_COLOR } from '../utils/Colors' import { TIME_UNITS } from '../utils/DurationUtils' @@ -26,7 +27,7 @@ const timingOptions = [ ] function getRelativeLabel(notification, t) { - const { value, unit } = notification + const { unit, value } = notification const numericValue = Number(value) if (numericValue === 0) { return t('notifTemplate.onDueDate') @@ -71,8 +72,8 @@ const NotificationTemplate = ({ // Consumers that own an empty state themselves pass 0. minNotifications = 1, onChange, - value, showTimeline = true, + value, }) => { const { t } = useTranslation('chores') const [notifications, setNotifications] = useState( diff --git a/src/components/SSEConnectionStatus.jsx b/src/components/SSEConnectionStatus.jsx index 718d355..df7b744 100644 --- a/src/components/SSEConnectionStatus.jsx +++ b/src/components/SSEConnectionStatus.jsx @@ -1,13 +1,14 @@ import { Circle, SignalWifi4Bar, SignalWifiOff } from '@mui/icons-material' import { Box, Chip, Tooltip, Typography } from '@mui/joy' + import { useSSEContext } from '../hooks/useSSEContext' const SSEConnectionStatus = ({ - variant = 'minimal', showError = false, sx = {}, + variant = 'minimal', }) => { - const { isConnected, isConnecting, error, getConnectionStatus } = + const { error, getConnectionStatus, isConnected, isConnecting } = useSSEContext() const getStatusColor = () => { diff --git a/src/components/SSESettings.jsx b/src/components/SSESettings.jsx index 3187bdc..679206d 100644 --- a/src/components/SSESettings.jsx +++ b/src/components/SSESettings.jsx @@ -9,22 +9,23 @@ import { Switch, Typography, } from '@mui/joy' +import { useTranslation } from 'react-i18next' + import { useSSEContext } from '../hooks/useSSEContext' import { useUserProfile } from '../queries/UserQueries' import { isPlusAccount } from '../utils/Helpers' import SSEConnectionStatus from './SSEConnectionStatus' -import { useTranslation } from 'react-i18next' const SSESettings = () => { const { t } = useTranslation('settings') const { data: userProfile } = useUserProfile() const { - isConnected, - isConnecting, error, getConnectionStatus, - toggleSSEEnabled, + isConnected, + isConnecting, isSSEEnabled, + toggleSSEEnabled, } = useSSEContext() const handleToggle = () => { diff --git a/src/components/SubscriptionModal.jsx b/src/components/SubscriptionModal.jsx index e5fe314..c4079f4 100644 --- a/src/components/SubscriptionModal.jsx +++ b/src/components/SubscriptionModal.jsx @@ -1,13 +1,14 @@ import { Check, Star } from '@mui/icons-material' import { Box, Card, Chip, Divider, Radio, Typography } from '@mui/joy' import { useState } from 'react' -import AppModal from './common/AppModal' -import ModalActions from './common/ModalActions' -import { useNotification } from '../service/NotificationProvider' -import { GetSubscriptionSession } from '../utils/Fetcher' import { useTranslation } from 'react-i18next' -const SubscriptionModal = ({ open, onClose }) => { +import { useNotification } from '../service/NotificationProvider' +import { GetSubscriptionSession } from '../utils/Fetcher' +import AppModal from './common/AppModal' +import ModalActions from './common/ModalActions' + +const SubscriptionModal = ({ onClose, open }) => { const { t } = useTranslation('settings') const [selectedPlan, setSelectedPlan] = useState('yearly') const [isLoading, setIsLoading] = useState(false) @@ -81,7 +82,11 @@ const SubscriptionModal = ({ open, onClose }) => { footer={ { const { t } = useTranslation('common') @@ -43,11 +44,11 @@ const UserProfileAvatar = () => { const { mode, setMode } = useColorScheme() const { data: userProfile } = useUserProfile() const { + canImpersonate, + getEffectiveUser, isImpersonating, startImpersonation, stopImpersonation, - canImpersonate, - getEffectiveUser, } = useImpersonateUser() const { data: circleMembersData } = useCircleMembers() const [isModalOpen, setIsModalOpen] = useState(false) @@ -129,7 +130,9 @@ const UserProfileAvatar = () => { }} /> { // Handle both children and items patterns let childrenArray if (items && renderItem) { - childrenArray = items.map((item, index) => - React.cloneElement(renderItem(item, index), { - key: keyExtractor ? keyExtractor(item, index) : index - }) + childrenArray = items.map((item, index) => + React.cloneElement(renderItem(item, index), { + key: keyExtractor ? keyExtractor(item, index) : index, + }), ) } else { childrenArray = React.Children.toArray(children) } - + const visibleItems = useStaggeredAnimation(childrenArray.length, staggerDelay) const prefersReducedMotion = useReducedMotion() // If user prefers reduced motion, render without animations if (prefersReducedMotion) { return ( - - {items && renderItem ? childrenArray : children} - + {items && renderItem ? childrenArray : children} ) } @@ -55,7 +58,7 @@ const AnimatedList = ({ {childrenArray.map((child, index) => { const isVisible = visibleItems.has(index) - + return ( { +const LoadingScreen = ({ message = null, showLogo = true, size = 'lg' }) => { const { t } = useTranslation('common') return ( @@ -90,7 +86,7 @@ const LoadingScreen = ({ )} - + - + {message ?? t('loading')} diff --git a/src/components/animations/PageTransition.jsx b/src/components/animations/PageTransition.jsx index bff11ed..cb51333 100644 --- a/src/components/animations/PageTransition.jsx +++ b/src/components/animations/PageTransition.jsx @@ -1,7 +1,8 @@ +import './PageTransition.css' + import { useLayoutEffect, useRef, useState } from 'react' import { flushSync } from 'react-dom' import { useLocation } from 'react-router-dom' -import './PageTransition.css' // Route hierarchy for determining navigation direction const routeHierarchy = { diff --git a/src/components/animations/SkeletonLoader.jsx b/src/components/animations/SkeletonLoader.jsx index 244bddf..af2095f 100644 --- a/src/components/animations/SkeletonLoader.jsx +++ b/src/components/animations/SkeletonLoader.jsx @@ -1,11 +1,11 @@ import { Box, Skeleton } from '@mui/joy' const SkeletonLoader = ({ - type = 'card', count = 1, height = 100, - width = '100%', + type = 'card', variant = 'rectangular', + width = '100%', ...props }) => { const renderSkeleton = () => { diff --git a/src/components/animations/SmoothCard.jsx b/src/components/animations/SmoothCard.jsx index 9d26f1e..839b072 100644 --- a/src/components/animations/SmoothCard.jsx +++ b/src/components/animations/SmoothCard.jsx @@ -1,23 +1,23 @@ -import React from 'react' import { Card } from '@mui/joy' import { styled } from '@mui/joy/styles' +import React from 'react' const AnimatedCard = styled(Card)(({ theme }) => ({ transition: 'all 0.3s cubic-bezier(0.4, 0, 0.2, 1)', transform: 'translateZ(0)', // Enable GPU acceleration cursor: 'pointer', position: 'relative', - + '&:hover': { transform: 'translateY(-4px) translateZ(0)', boxShadow: theme.shadow.lg, }, - + '&:active': { transform: 'translateY(-2px) translateZ(0)', transition: 'all 0.1s cubic-bezier(0.4, 0, 0.2, 1)', }, - + // Subtle background animation on hover '&::before': { content: '""', @@ -26,49 +26,50 @@ const AnimatedCard = styled(Card)(({ theme }) => ({ left: 0, right: 0, bottom: 0, - background: 'linear-gradient(45deg, transparent, rgba(255,255,255,0.1), transparent)', + background: + 'linear-gradient(45deg, transparent, rgba(255,255,255,0.1), transparent)', opacity: 0, transition: 'opacity 0.3s ease', pointerEvents: 'none', borderRadius: 'inherit', }, - + '&:hover::before': { opacity: 1, }, - + // Focus states for accessibility '&:focus-visible': { outline: '2px solid', outlineColor: theme.palette.primary[500], outlineOffset: '2px', }, - + // Reduced motion support '@media (prefers-reduced-motion: reduce)': { transition: 'none', transform: 'none !important', - + '&:hover': { transform: 'none', boxShadow: theme.shadow.md, // Still provide visual feedback }, - + '&:active': { transform: 'none', }, - + '&::before': { display: 'none', }, }, })) -const SmoothCard = ({ - children, - onClick, +const SmoothCard = ({ animationDisabled = false, - ...props + children, + onClick, + ...props }) => { if (animationDisabled) { return ( diff --git a/src/components/animations/StaggeredList.jsx b/src/components/animations/StaggeredList.jsx index 32970c5..96a139e 100644 --- a/src/components/animations/StaggeredList.jsx +++ b/src/components/animations/StaggeredList.jsx @@ -1,13 +1,14 @@ +import './PageTransition.css' + import { Box } from '@mui/joy' import React, { useEffect, useState } from 'react' import { CSSTransition, TransitionGroup } from 'react-transition-group' -import './PageTransition.css' const StaggeredList = ({ - children, - staggerDelay = 50, - initialDelay = 0, animate = true, + children, + initialDelay = 0, + staggerDelay = 50, }) => { const [isVisible, setIsVisible] = useState(!animate) diff --git a/src/components/common/AppModal.jsx b/src/components/common/AppModal.jsx index 84b1677..228bba5 100644 --- a/src/components/common/AppModal.jsx +++ b/src/components/common/AppModal.jsx @@ -17,27 +17,27 @@ const WIDTH_BY_SIZE = { const AppModal = forwardRef( ( { - open, - onClose, + backdropBlur = true, children, - title, + closeOnBackdrop = true, + closeOnEscape = true, + contentSx, description, footer, - size = 'md', + footerSx, fullWidth = true, isMobile: isMobileProp, keepMounted = false, + maxHeight = '90dvh', mobilePresentation = 'sheet', + onClose, + open, role = 'dialog', showCloseButton = true, showHandle = false, - closeOnBackdrop = true, - closeOnEscape = true, - backdropBlur = true, - maxHeight = '90dvh', - contentSx, - footerSx, + size = 'md', sx, + title, unmountDelay = 180, ...modalProps }, diff --git a/src/components/common/DurationInput.jsx b/src/components/common/DurationInput.jsx index a1bac5f..91ece5d 100644 --- a/src/components/common/DurationInput.jsx +++ b/src/components/common/DurationInput.jsx @@ -1,6 +1,7 @@ import { Add, Remove } from '@mui/icons-material' import { Box, IconButton, Input, Option, Select } from '@mui/joy' import { useEffect, useState } from 'react' + import { secondsToValueAndUnit, TIME_UNITS, @@ -16,7 +17,7 @@ import { * size – Joy UI size ('sm' | 'md') * minValue – minimum numeric value (default 1) */ -const DurationInput = ({ value, onChange, size = 'md', minValue = 1 }) => { +const DurationInput = ({ minValue = 1, onChange, size = 'md', value }) => { const derived = value != null && value >= 0 ? secondsToValueAndUnit(value) @@ -26,7 +27,7 @@ const DurationInput = ({ value, onChange, size = 'md', minValue = 1 }) => { useEffect(() => { if (value != null && value >= 0) { - const { value: v, unit: u } = secondsToValueAndUnit(value) + const { unit: u, value: v } = secondsToValueAndUnit(value) setDisplayValue(v) setUnit(u) } diff --git a/src/components/common/EmptyState.jsx b/src/components/common/EmptyState.jsx index 95250fd..b02bf2e 100644 --- a/src/components/common/EmptyState.jsx +++ b/src/components/common/EmptyState.jsx @@ -55,7 +55,7 @@ const SIZES = { } const ActionButton = ({ action, ...buttonProps }) => { - const { label, to, onClick, ...rest } = action + const { label, onClick, to, ...rest } = action return ( ) -export const SocialButton = ({ icon, children, sx, ...props }) => ( +export const SocialButton = ({ children, icon, sx, ...props }) => (