diff --git a/.claude/launch.json b/.claude/launch.json new file mode 100644 index 0000000..ffc9348 --- /dev/null +++ b/.claude/launch.json @@ -0,0 +1,20 @@ +{ + "version": "0.0.1", + "configurations": [ + { + "name": "flutter-web", + "runtimeExecutable": "flutter", + "runtimeArgs": [ + "run", + "-d", "web-server", + "--web-port=8080", + "--web-hostname=localhost", + "--dart-define=SUPABASE_URL=https://jqyjvhwlcqcsuwcqgcwf.supabase.co", + "--dart-define=SUPABASE_ANON_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6ImpxeWp2aHdsY3Fjc3V3Y3FnY3dmIiwicm9sZSI6ImFub24iLCJpYXQiOjE3Nzg1MjI4MjgsImV4cCI6MjA5NDA5ODgyOH0.3bF68bNG0IwAc50YbOC3sem4k8O-d1vkvNNqBt1HbRw", + "--dart-define=STRIPE_PUBLISHABLE_KEY=pk_test_51TQvlrPcVRApxzIxJ8RmKYA1WEw7k8zubumbIfsDjRSGgDyAcSU22RhsZRtKIP1lAZ0wtGjpLfzjI4fozMZxGSlo006zuZrbon", + "--dart-define=NVIDIA_API_KEY=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" + ], + "port": 8080 + } + ] +} diff --git a/.claude/settings.local.json b/.claude/settings.local.json index e62f9f4..f98c6d6 100644 --- a/.claude/settings.local.json +++ b/.claude/settings.local.json @@ -90,9 +90,43 @@ "mcp__Claude_in_Chrome__find", "Bash(dir \"G:\\\\GitHub\\\\petfolio\\\\google_fonts\" /b)", "mcp__5d4f3e29-8e00-4c0a-a2e0-3f9d9a2735f2__list_projects", + "Bash(grep -v '\\\\.g\\\\.dart$')", + "Bash(grep -v '\\\\.freezed\\\\.dart$')", + "Bash(gh pr *)", + "Bash(git rm *)", + "Bash(dir \"G:\\\\GitHub\\\\petfolio\\\\PetFolio Redesign\\\\Care Redesign\" /b)", + "mcp__292a7621-3089-4236-bd52-07a54bf59881__list_tables", + "mcp__mobile-mcp__mobile_list_available_devices", + "mcp__mobile-mcp__mobile_take_screenshot", + "mcp__mobile-mcp__mobile_click_on_screen_at_coordinates", + "mcp__mobile-mcp__mobile_list_elements_on_screen", + "mcp__mobile-mcp__mobile_save_screenshot", + "mcp__mobile-mcp__mobile_swipe_on_screen", + "Bash(grep -E \"\\\\.\\(dart\\)$\")", + "Bash(Get-Content \"C:\\\\Users\\\\syedr\\\\AppData\\\\Local\\\\Temp\\\\claude\\\\G--GitHub-petfolio\\\\83d1e98f-3a1c-41fb-a000-787a9c3f0b74\\\\tasks\\\\bn6o3o4cc.output\" -Wait -Tail 30)", + "Bash(Select-Object -First 30)", + "mcp__marionette-mcp__take_screenshots", + "Bash(flutter run *)", + "Bash(Start-Sleep -Seconds 25)", + "Bash(Get-Content \"C:\\\\Users\\\\syedr\\\\AppData\\\\Local\\\\Temp\\\\claude\\\\G--GitHub-petfolio\\\\83d1e98f-3a1c-41fb-a000-787a9c3f0b74\\\\tasks\\\\bbq53bd0b.output\" -ErrorAction SilentlyContinue)", + "Bash(Select-Object -Last 30)", + "mcp__dart__hot_reload", + "mcp__dart__dtd", + "WebFetch(domain:github.com)", + "WebFetch(domain:openjdk.org)", + "Bash(java -version)", + "WebFetch(domain:inside.java)", + "WebFetch(domain:stackoverflow.com)", + "WebFetch(domain:docs.gradle.org)", + "Bash(grep -E \"stripe_android|geolocator_android|flutter_local_notifications|permission_handler_android\" /c/Users/syedr/AppData/Local/Pub/Cache/hosted/pub.dev/../../../../../../../GitHub/petfolio/pubspec.lock | head -20)", "Bash(grep -v \"^$\")", "Bash(supabase status *)", - "mcp__visualize__read_me" + "mcp__visualize__read_me", + "Bash(git fetch *)", + "Bash(git merge *)", + "Bash(git stash *)", + "mcp__plugin_playwright_playwright__browser_navigate", + "mcp__plugin_playwright_playwright__browser_snapshot" ] } } diff --git a/.github/workflows/deploy-web.yml b/.github/workflows/deploy-web.yml new file mode 100644 index 0000000..d5d3253 --- /dev/null +++ b/.github/workflows/deploy-web.yml @@ -0,0 +1,48 @@ +name: Deploy Flutter Web to Vercel + +on: + push: + branches: [main] + pull_request: + branches: [main] + +jobs: + build-and-deploy: + runs-on: ubuntu-latest + + env: + VERCEL_ORG_ID: team_lC8aTJK0XiU9qDfaHeTfCJs6 + VERCEL_PROJECT_ID: prj_hMHouLWimZvr5dDOlZeAhbH8xtop + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup Flutter + uses: subosito/flutter-action@v2 + with: + flutter-version: '3.x' + channel: stable + cache: true + + - name: Install dependencies + run: flutter pub get + + - name: Build Flutter Web (release) + run: | + flutter build web --release \ + --dart-define=SUPABASE_URL=${{ secrets.SUPABASE_URL }} \ + --dart-define=SUPABASE_ANON_KEY=${{ secrets.SUPABASE_ANON_KEY }} \ + --dart-define=STRIPE_PUBLISHABLE_KEY=${{ secrets.STRIPE_PUBLISHABLE_KEY }} \ + --dart-define=NVIDIA_API_KEY=${{ secrets.NVIDIA_API_KEY }} + + - name: Package build output (vercel build) + run: npx vercel build --yes --token=${{ secrets.VERCEL_TOKEN }} + + - name: Deploy preview to Vercel (PRs) + if: github.event_name == 'pull_request' + run: npx vercel deploy --prebuilt --yes --token=${{ secrets.VERCEL_TOKEN }} + + - name: Deploy to Vercel production (main only) + if: github.event_name == 'push' && github.ref == 'refs/heads/main' + run: npx vercel deploy --prebuilt --prod --yes --token=${{ secrets.VERCEL_TOKEN }} diff --git a/.gitignore b/.gitignore index e6ba4cc..681da64 100644 --- a/.gitignore +++ b/.gitignore @@ -142,3 +142,8 @@ tmp_window_dump.xml .dart-tool/ tmp_home/ .idea/caches/ + +# Vercel — keep project.json, ignore the rest +.vercel/output/ +.vercel/*.json +!.vercel/project.json diff --git a/.playwright-mcp/page-2026-06-04T10-51-49-473Z.yml b/.playwright-mcp/page-2026-06-04T10-51-49-473Z.yml new file mode 100644 index 0000000..61920b9 --- /dev/null +++ b/.playwright-mcp/page-2026-06-04T10-51-49-473Z.yml @@ -0,0 +1,3403 @@ +- generic [ref=e2]: + - generic [ref=e3]: + - link "Skip to content" [ref=e4] [cursor=pointer]: + - /url: "#start-of-content" + - banner [ref=e6]: + - heading "Navigation Menu" [level=2] [ref=e7] + - generic [ref=e9]: + - button "Toggle navigation" [ref=e11] [cursor=pointer] + - link "Homepage" [ref=e17] [cursor=pointer]: + - /url: / + - img [ref=e18] + - generic [ref=e20]: + - link "Sign in" [ref=e21] [cursor=pointer]: + - /url: /login?return_to=https%3A%2F%2Fgithub.com%2FCodeStorm-Hub%2Fpetfolio%2Fpull%2F17 + - button "Appearance settings" [ref=e24] [cursor=pointer]: + - img + - main [ref=e28]: + - generic [ref=e29]: + - generic [ref=e30]: + - generic [ref=e32]: + - img [ref=e33] + - link "CodeStorm-Hub" [ref=e36] [cursor=pointer]: + - /url: /CodeStorm-Hub + - generic [ref=e37]: / + - strong [ref=e38]: + - link "petfolio" [ref=e39] [cursor=pointer]: + - /url: /CodeStorm-Hub/petfolio + - generic [ref=e40]: Public + - generic [ref=e41]: + - list: + - listitem [ref=e42]: + - link "You must be signed in to change notification settings" [ref=e43] [cursor=pointer]: + - /url: /login?return_to=%2FCodeStorm-Hub%2Fpetfolio + - img [ref=e44] + - text: Notifications + - listitem [ref=e46]: + - link "Fork 0" [ref=e47] [cursor=pointer]: + - /url: /login?return_to=%2FCodeStorm-Hub%2Fpetfolio + - img [ref=e48] + - text: Fork + - generic "0" [ref=e50] + - listitem [ref=e51]: + - link "You must be signed in to star a repository" [ref=e53] [cursor=pointer]: + - /url: /login?return_to=%2FCodeStorm-Hub%2Fpetfolio + - img [ref=e54] + - text: Star + - generic "0 users starred this repository" [ref=e56]: "0" + - navigation "Repository" [ref=e57]: + - list [ref=e58]: + - listitem [ref=e59]: + - link "Code" [ref=e60] [cursor=pointer]: + - /url: /CodeStorm-Hub/petfolio + - img [ref=e61] + - generic [ref=e63]: Code + - listitem [ref=e64]: + - link "Issues" [ref=e65] [cursor=pointer]: + - /url: /CodeStorm-Hub/petfolio/issues + - img [ref=e66] + - generic [ref=e69]: Issues + - listitem [ref=e70]: + - link "Pull requests 2" [ref=e71] [cursor=pointer]: + - /url: /CodeStorm-Hub/petfolio/pulls + - img [ref=e72] + - generic [ref=e74]: Pull requests + - generic "2" [ref=e75] + - listitem [ref=e76]: + - link "Discussions" [ref=e77] [cursor=pointer]: + - /url: /CodeStorm-Hub/petfolio/discussions + - img [ref=e78] + - generic [ref=e80]: Discussions + - listitem [ref=e81]: + - link "Actions" [ref=e82] [cursor=pointer]: + - /url: /CodeStorm-Hub/petfolio/actions + - img [ref=e83] + - generic [ref=e85]: Actions + - listitem [ref=e86]: + - link "Projects" [ref=e87] [cursor=pointer]: + - /url: /CodeStorm-Hub/petfolio/projects + - img [ref=e88] + - generic [ref=e90]: Projects + - listitem [ref=e91]: + - link "Models" [ref=e92] [cursor=pointer]: + - /url: /CodeStorm-Hub/petfolio/models + - img [ref=e93] + - generic [ref=e95]: Models + - listitem [ref=e96] + - listitem [ref=e97] + - button "Additional navigation options" [ref=e101] [cursor=pointer]: + - img + - generic [ref=e108]: + - generic [ref=e111]: + - 'heading "Care redesign salman #17" [level=1] [ref=e113]': + - text: Care redesign salman + - generic [ref=e115]: "#17" + - generic [ref=e117]: + - generic [ref=e119]: + - img "Pull request" [ref=e120] + - text: Open + - generic [ref=e123]: + - link "syed-reza98" [ref=e124] [cursor=pointer]: + - /url: /syed-reza98 + - text: wants to merge 19 commits into + - generic [ref=e125]: + - link "main" [ref=e126] [cursor=pointer]: + - /url: /CodeStorm-Hub/petfolio/tree/main + - generic [ref=e127]: from + - generic [ref=e128]: + - link "care-redesign-salman" [ref=e129] [cursor=pointer]: + - /url: /CodeStorm-Hub/petfolio/tree/care-redesign-salman + - button "Copy head branch name to clipboard" [ref=e130] [cursor=pointer]: + - img [ref=e131] + - generic [ref=e134]: + - generic [ref=e136]: + - generic [ref=e137]: +3,815 + - generic [ref=e138]: "-1,310" + - generic [ref=e139]: "Lines changed: 3815 additions & 1310 deletions" + - navigation "Pull request navigation tabs" [ref=e148]: + - tablist [ref=e149]: + - tab "Conversation (33)" [selected] [ref=e150] [cursor=pointer]: + - img [ref=e151] + - text: Conversation + - generic [ref=e153]: "33" + - generic [ref=e154]: (33) + - tab "Commits (19)" [ref=e155] [cursor=pointer]: + - img [ref=e156] + - text: Commits + - generic [ref=e158]: "19" + - generic [ref=e159]: (19) + - tab "Checks (2)" [ref=e160] [cursor=pointer]: + - img [ref=e161] + - text: Checks + - generic [ref=e163]: "2" + - generic [ref=e164]: (2) + - tab "Files changed (44)" [ref=e165] [cursor=pointer]: + - img [ref=e166] + - text: Files changed + - generic [ref=e168]: "44" + - generic [ref=e169]: (44) + - generic [ref=e174]: + - generic [ref=e176]: + - heading "Conversation" [level=2] [ref=e177] + - generic [ref=e178]: + - generic [ref=e179]: + - generic [ref=e181]: + - link "@syed-reza98" [ref=e182] [cursor=pointer]: + - /url: /syed-reza98 + - img "@syed-reza98" [ref=e183] + - generic [ref=e185]: + - generic [ref=e186]: + - generic [ref=e187]: + - group [ref=e189]: + - button "Show options" [ref=e190] [cursor=pointer]: + - img "Show options" [ref=e193] + - generic "This user is a member of the CodeStorm-Hub organization." [ref=e196]: + - generic [ref=e197]: Member + - heading "syed-reza98 commented Jun 3, 202613 hours ago" [level=3] [ref=e198]: + - generic [ref=e199]: + - strong [ref=e200]: + - link "syed-reza98" [ref=e201] [cursor=pointer]: + - /url: /syed-reza98 + - text: commented + - link "Jun 3, 202613 hours ago" [ref=e202] [cursor=pointer]: + - /url: "#issue-4583667084" + - generic [ref=e206]: + - paragraph [ref=e207]: This pull request introduces several key improvements to the project, focusing on enhancing web deployment, local development, and UI prototyping. The most significant changes include adding a GitHub Actions workflow for automated Flutter Web deployment to Vercel, introducing a new HTML prototype for the Care Redesign, and updating configuration files for both development and deployment environments. + - paragraph [ref=e208]: + - strong [ref=e209]: "Deployment automation and configuration:" + - list [ref=e210]: + - listitem [ref=e211]: + - text: Added a new GitHub Actions workflow ( + - code [ref=e212]: .github/workflows/deploy-web.yml + - text: ) to automatically build and deploy the Flutter Web app to Vercel on pushes and pull requests to the + - code [ref=e213]: main + - text: branch. This workflow uses environment secrets for sensitive API keys and integrates with Vercel for production deployment. + - listitem [ref=e214]: + - text: Added Vercel project configuration file ( + - code [ref=e215]: .vercel/project.json + - text: ) to specify the organization and project IDs for deployment integration. + - paragraph [ref=e216]: + - strong [ref=e217]: "Development environment enhancements:" + - list [ref=e218]: + - listitem [ref=e219]: + - text: Added a local launch configuration file ( + - code [ref=e220]: .claude/launch.json + - text: ) to streamline running the Flutter Web app locally with all necessary + - code [ref=e221]: dart-define + - text: environment variables, including Supabase, Stripe, and NVIDIA API keys. + - listitem [ref=e222]: + - text: Expanded + - code [ref=e223]: .claude/settings.local.json + - text: with additional bash and MCP commands, improving local development and testing workflows. + - paragraph [ref=e224]: + - strong [ref=e225]: "UI prototyping:" + - list [ref=e226]: + - listitem [ref=e227]: + - text: Added a new HTML file ( + - code [ref=e228]: PetFolio Redesign/Care Redesign/Care Redesign.html + - text: ) that serves as a prototype for the Care Redesign. This file includes a comprehensive set of styles, theme support (light/dark), and scripts for responsive device scaling and React-based component rendering. + - generic [ref=e229]: + - generic [ref=e231]: + - generic [ref=e232]: + - img [ref=e234] + - generic [ref=e236]: + - link "syed-reza98" [ref=e237] [cursor=pointer]: + - /url: /syed-reza98 + - text: added 16 commits + - link "June 1, 2026 13:123 days ago" [ref=e238] [cursor=pointer]: + - /url: "#commits-pushed-fb57c49" + - generic [ref=e239]: + - generic [ref=e240]: + - img [ref=e242] + - generic [ref=e247]: + - link "@syed-reza98" [ref=e250] [cursor=pointer]: + - /url: /syed-reza98 + - img "@syed-reza98" [ref=e251] + - generic [ref=e252]: + - code [ref=e253]: + - link "Add Care Redesign demo and update care UI" [ref=e254] [cursor=pointer]: + - /url: /CodeStorm-Hub/petfolio/pull/17/commits/fb57c49e78cac998e4bbe73b4469a9b04809c852 + - button "Commit message body" [ref=e256] [cursor=pointer]: … + - code [ref=e260]: + - link "fb57c49" [ref=e261] [cursor=pointer]: + - /url: /CodeStorm-Hub/petfolio/pull/17/commits/fb57c49e78cac998e4bbe73b4469a9b04809c852 + - generic [ref=e262]: + - img [ref=e264] + - generic [ref=e269]: + - link "@syed-reza98" [ref=e272] [cursor=pointer]: + - /url: /syed-reza98 + - img "@syed-reza98" [ref=e273] + - generic [ref=e274]: + - code [ref=e275]: + - 'link "Revamp Care UI: trophy room & badge visuals" [ref=e276] [cursor=pointer]': + - /url: /CodeStorm-Hub/petfolio/pull/17/commits/1d8e35ff5145e14271d5105ad6af5ef64d7b0608 + - button "Commit message body" [ref=e278] [cursor=pointer]: … + - code [ref=e282]: + - link "1d8e35f" [ref=e283] [cursor=pointer]: + - /url: /CodeStorm-Hub/petfolio/pull/17/commits/1d8e35ff5145e14271d5105ad6af5ef64d7b0608 + - generic [ref=e284]: + - img [ref=e286] + - generic [ref=e291]: + - link "@syed-reza98" [ref=e294] [cursor=pointer]: + - /url: /syed-reza98 + - img "@syed-reza98" [ref=e295] + - generic [ref=e296]: + - code [ref=e297]: + - link "Add Flutter run and exception logs" [ref=e298] [cursor=pointer]: + - /url: /CodeStorm-Hub/petfolio/pull/17/commits/d472ebddedcbc5bf5045dddc5bdf64e63913de24 + - button "Commit message body" [ref=e300] [cursor=pointer]: … + - code [ref=e304]: + - link "d472ebd" [ref=e305] [cursor=pointer]: + - /url: /CodeStorm-Hub/petfolio/pull/17/commits/d472ebddedcbc5bf5045dddc5bdf64e63913de24 + - generic [ref=e306]: + - img [ref=e308] + - generic [ref=e313]: + - link "@syed-reza98" [ref=e316] [cursor=pointer]: + - /url: /syed-reza98 + - img "@syed-reza98" [ref=e317] + - generic [ref=e318]: + - code [ref=e319]: + - link "Adapt notifications API, bump deps, add KGP notes" [ref=e320] [cursor=pointer]: + - /url: /CodeStorm-Hub/petfolio/pull/17/commits/8ac21da357c5ff7a6a09c912873d9dc6ddb1a385 + - button "Commit message body" [ref=e322] [cursor=pointer]: … + - code [ref=e326]: + - link "8ac21da" [ref=e327] [cursor=pointer]: + - /url: /CodeStorm-Hub/petfolio/pull/17/commits/8ac21da357c5ff7a6a09c912873d9dc6ddb1a385 + - generic [ref=e328]: + - img [ref=e330] + - generic [ref=e335]: + - link "@syed-reza98" [ref=e338] [cursor=pointer]: + - /url: /syed-reza98 + - img "@syed-reza98" [ref=e339] + - generic [ref=e340]: + - code [ref=e341]: + - link "Update Claude settings and flutter run log" [ref=e342] [cursor=pointer]: + - /url: /CodeStorm-Hub/petfolio/pull/17/commits/2c45129e78d93e6e6f4862cd7275d0f53b10763b + - button "Commit message body" [ref=e344] [cursor=pointer]: … + - code [ref=e348]: + - link "2c45129" [ref=e349] [cursor=pointer]: + - /url: /CodeStorm-Hub/petfolio/pull/17/commits/2c45129e78d93e6e6f4862cd7275d0f53b10763b + - generic [ref=e350]: + - img [ref=e352] + - generic [ref=e357]: + - link "@syed-reza98" [ref=e360] [cursor=pointer]: + - /url: /syed-reza98 + - img "@syed-reza98" [ref=e361] + - generic [ref=e362]: + - code [ref=e363]: + - link "Add PWA onboarding, web config, and tooling files" [ref=e364] [cursor=pointer]: + - /url: /CodeStorm-Hub/petfolio/pull/17/commits/d0e21726ac57309556fa1733d77c1a773a425ced + - button "Commit message body" [ref=e366] [cursor=pointer]: … + - code [ref=e370]: + - link "d0e2172" [ref=e371] [cursor=pointer]: + - /url: /CodeStorm-Hub/petfolio/pull/17/commits/d0e21726ac57309556fa1733d77c1a773a425ced + - generic [ref=e372]: + - img [ref=e374] + - generic [ref=e379]: + - link "@syed-reza98" [ref=e382] [cursor=pointer]: + - /url: /syed-reza98 + - img "@syed-reza98" [ref=e383] + - generic [ref=e384]: + - code [ref=e385]: + - link "Move AI call to Supabase function" [ref=e386] [cursor=pointer]: + - /url: /CodeStorm-Hub/petfolio/pull/17/commits/1c1b25a4d91ecc9bf2ae8a25d8808ce301f86a92 + - button "Commit message body" [ref=e388] [cursor=pointer]: … + - group [ref=e392]: + - generic "2 / 2 checks OK" [ref=e393] [cursor=pointer]: + - img "2 / 2 checks OK" [ref=e394] + - code [ref=e397]: + - link "1c1b25a" [ref=e398] [cursor=pointer]: + - /url: /CodeStorm-Hub/petfolio/pull/17/commits/1c1b25a4d91ecc9bf2ae8a25d8808ce301f86a92 + - generic [ref=e399]: + - img [ref=e401] + - generic [ref=e406]: + - link "@syed-reza98" [ref=e409] [cursor=pointer]: + - /url: /syed-reza98 + - img "@syed-reza98" [ref=e410] + - generic [ref=e411]: + - code [ref=e412]: + - link "Add .vercelignore and bump Supabase CLI" [ref=e413] [cursor=pointer]: + - /url: /CodeStorm-Hub/petfolio/pull/17/commits/31fa1729059eb96ac5907a0a88eb663d1589e7b7 + - button "Commit message body" [ref=e415] [cursor=pointer]: … + - group [ref=e419]: + - generic "2 / 2 checks OK" [ref=e420] [cursor=pointer]: + - img "2 / 2 checks OK" [ref=e421] + - code [ref=e424]: + - link "31fa172" [ref=e425] [cursor=pointer]: + - /url: /CodeStorm-Hub/petfolio/pull/17/commits/31fa1729059eb96ac5907a0a88eb663d1589e7b7 + - generic [ref=e426]: + - img [ref=e428] + - generic [ref=e433]: + - link "@syed-reza98" [ref=e436] [cursor=pointer]: + - /url: /syed-reza98 + - img "@syed-reza98" [ref=e437] + - generic [ref=e438]: + - code [ref=e439]: + - link "Add vercel.json and remove .vercelignore" [ref=e440] [cursor=pointer]: + - /url: /CodeStorm-Hub/petfolio/pull/17/commits/c843e29c606ad6ae048d35740dbeb957725bf47e + - button "Commit message body" [ref=e442] [cursor=pointer]: … + - group [ref=e446]: + - generic "1 / 2 checks OK" [ref=e447] [cursor=pointer]: + - img "1 / 2 checks OK" [ref=e448] + - code [ref=e451]: + - link "c843e29" [ref=e452] [cursor=pointer]: + - /url: /CodeStorm-Hub/petfolio/pull/17/commits/c843e29c606ad6ae048d35740dbeb957725bf47e + - generic [ref=e453]: + - img [ref=e455] + - generic [ref=e460]: + - link "@syed-reza98" [ref=e463] [cursor=pointer]: + - /url: /syed-reza98 + - img "@syed-reza98" [ref=e464] + - generic [ref=e465]: + - code [ref=e466]: + - link "Add .vercelignore to ignore .git" [ref=e467] [cursor=pointer]: + - /url: /CodeStorm-Hub/petfolio/pull/17/commits/77b469b0c64284d5cf608a58d42b2ec36c6becb4 + - button "Commit message body" [ref=e469] [cursor=pointer]: … + - group [ref=e473]: + - generic "1 / 2 checks OK" [ref=e474] [cursor=pointer]: + - img "1 / 2 checks OK" [ref=e475] + - code [ref=e478]: + - link "77b469b" [ref=e479] [cursor=pointer]: + - /url: /CodeStorm-Hub/petfolio/pull/17/commits/77b469b0c64284d5cf608a58d42b2ec36c6becb4 + - generic [ref=e480]: + - img [ref=e482] + - generic [ref=e487]: + - link "@syed-reza98" [ref=e490] [cursor=pointer]: + - /url: /syed-reza98 + - img "@syed-reza98" [ref=e491] + - generic [ref=e492]: + - code [ref=e493]: + - link "Revert \"Add .vercelignore to ignore .git\"" [ref=e494] [cursor=pointer]: + - /url: /CodeStorm-Hub/petfolio/pull/17/commits/f5b31e03f0580cb594cf350f9ddff861bc0b321d + - button "Commit message body" [ref=e496] [cursor=pointer]: … + - group [ref=e500]: + - generic "1 / 2 checks OK" [ref=e501] [cursor=pointer]: + - img "1 / 2 checks OK" [ref=e502] + - code [ref=e505]: + - link "f5b31e0" [ref=e506] [cursor=pointer]: + - /url: /CodeStorm-Hub/petfolio/pull/17/commits/f5b31e03f0580cb594cf350f9ddff861bc0b321d + - generic [ref=e507]: + - img [ref=e509] + - generic [ref=e514]: + - link "@syed-reza98" [ref=e517] [cursor=pointer]: + - /url: /syed-reza98 + - img "@syed-reza98" [ref=e518] + - generic [ref=e519]: + - code [ref=e520]: + - link "Revert \"Add vercel.json and remove .vercelignore\"" [ref=e521] [cursor=pointer]: + - /url: /CodeStorm-Hub/petfolio/pull/17/commits/fd34015613ffa5771fc67c3b40d17d211f53c91f + - button "Commit message body" [ref=e523] [cursor=pointer]: … + - code [ref=e527]: + - link "fd34015" [ref=e528] [cursor=pointer]: + - /url: /CodeStorm-Hub/petfolio/pull/17/commits/fd34015613ffa5771fc67c3b40d17d211f53c91f + - generic [ref=e529]: + - img [ref=e531] + - generic [ref=e536]: + - link "@syed-reza98" [ref=e539] [cursor=pointer]: + - /url: /syed-reza98 + - img "@syed-reza98" [ref=e540] + - generic [ref=e541]: + - code [ref=e542]: + - link "Revert \"Add .vercelignore and bump Supabase CLI\"" [ref=e543] [cursor=pointer]: + - /url: /CodeStorm-Hub/petfolio/pull/17/commits/9f565e8da76986783598c587d2a9f0648e6f33b4 + - button "Commit message body" [ref=e545] [cursor=pointer]: … + - code [ref=e549]: + - link "9f565e8" [ref=e550] [cursor=pointer]: + - /url: /CodeStorm-Hub/petfolio/pull/17/commits/9f565e8da76986783598c587d2a9f0648e6f33b4 + - generic [ref=e551]: + - img [ref=e553] + - generic [ref=e558]: + - link "@syed-reza98" [ref=e561] [cursor=pointer]: + - /url: /syed-reza98 + - img "@syed-reza98" [ref=e562] + - generic [ref=e563]: + - code [ref=e564]: + - link "Revert \"Move AI call to Supabase function\"" [ref=e565] [cursor=pointer]: + - /url: /CodeStorm-Hub/petfolio/pull/17/commits/652ef113364e0d812a3773f556b162a4404730d1 + - button "Commit message body" [ref=e567] [cursor=pointer]: … + - group [ref=e571]: + - generic "2 / 2 checks OK" [ref=e572] [cursor=pointer]: + - img "2 / 2 checks OK" [ref=e573] + - code [ref=e576]: + - link "652ef11" [ref=e577] [cursor=pointer]: + - /url: /CodeStorm-Hub/petfolio/pull/17/commits/652ef113364e0d812a3773f556b162a4404730d1 + - generic [ref=e578]: + - img [ref=e580] + - generic [ref=e585]: + - link "@syed-reza98" [ref=e588] [cursor=pointer]: + - /url: /syed-reza98 + - img "@syed-reza98" [ref=e589] + - generic [ref=e590]: + - code [ref=e591]: + - link "Revert \"Add PWA onboarding, web config, and tooling files\"" [ref=e592] [cursor=pointer]: + - /url: /CodeStorm-Hub/petfolio/pull/17/commits/562d48cb82ec5a545b98b389bb7c1984b4259538 + - button "Commit message body" [ref=e594] [cursor=pointer]: … + - group [ref=e598]: + - generic "2 / 2 checks OK" [ref=e599] [cursor=pointer]: + - img "2 / 2 checks OK" [ref=e600] + - code [ref=e603]: + - link "562d48c" [ref=e604] [cursor=pointer]: + - /url: /CodeStorm-Hub/petfolio/pull/17/commits/562d48cb82ec5a545b98b389bb7c1984b4259538 + - generic [ref=e605]: + - img [ref=e607] + - generic [ref=e612]: + - link "@syed-reza98" [ref=e615] [cursor=pointer]: + - /url: /syed-reza98 + - img "@syed-reza98" [ref=e616] + - generic [ref=e617]: + - code [ref=e618]: + - link "Add web deployment, PWA and web-specific fixes" [ref=e619] [cursor=pointer]: + - /url: /CodeStorm-Hub/petfolio/pull/17/commits/7b8aaf186f6047b802df726bad66459f0cfe10ec + - button "Commit message body" [ref=e621] [cursor=pointer]: … + - group [ref=e625]: + - generic "2 / 2 checks OK" [ref=e626] [cursor=pointer]: + - img "2 / 2 checks OK" [ref=e627] + - code [ref=e630]: + - link "7b8aaf1" [ref=e631] [cursor=pointer]: + - /url: /CodeStorm-Hub/petfolio/pull/17/commits/7b8aaf186f6047b802df726bad66459f0cfe10ec + - generic [ref=e632]: + - generic [ref=e633]: + - img [ref=e635] + - generic [ref=e637]: + - link "Copilot PR reviewer" [ref=e638] [cursor=pointer]: + - /url: /apps/copilot-pull-request-reviewer + - img [ref=e640] + - link "Copilot" [ref=e643] [cursor=pointer]: + - /url: /apps/copilot-pull-request-reviewer + - generic [ref=e644]: AI + - text: review requested due to automatic review settings + - link "June 3, 2026 21:1113 hours ago" [ref=e645] [cursor=pointer]: + - /url: "#event-26308103910" + - generic [ref=e646]: + - img [ref=e648] + - generic [ref=e650]: + - strong [ref=e651]: Copilot + - link "started reviewing" [ref=e652] [cursor=pointer]: + - /url: https://github.com/CodeStorm-Hub/petfolio/sessions/4c2f321c-4931-4a36-b116-7c62d2927558 + - text: on behalf of + - link "syed-reza98" [ref=e653] [cursor=pointer]: + - /url: /syed-reza98 + - link "June 3, 2026 21:1113 hours ago" [ref=e654] [cursor=pointer]: + - /url: "#event-26308112066" + - link "View session 4c2f321c-4931-4a36-b116-7c62d2927558" [ref=e655] [cursor=pointer]: + - /url: https://github.com/CodeStorm-Hub/petfolio/sessions/4c2f321c-4931-4a36-b116-7c62d2927558 + - generic [ref=e657]: View session + - generic [ref=e660]: + - generic [ref=e661]: + - link "Copilot PR reviewer" [ref=e662] [cursor=pointer]: + - /url: /apps/copilot-pull-request-reviewer + - img [ref=e664] + - img "Only reviews by reviewers with write access count toward mergeability" [ref=e668] + - generic [ref=e670]: + - generic [ref=e671]: + - strong [ref=e672]: + - link "Copilot" [ref=e673] [cursor=pointer]: + - /url: /apps/copilot-pull-request-reviewer + - generic [ref=e674]: AI + - text: reviewed + - link "Jun 3, 202613 hours ago" [ref=e676] [cursor=pointer]: + - /url: "#pullrequestreview-4422878140" + - link "View reviewed changes" [ref=e678] [cursor=pointer]: + - /url: /CodeStorm-Hub/petfolio/pull/17/files/7b8aaf186f6047b802df726bad66459f0cfe10ec + - generic [ref=e680]: View reviewed changes + - generic [ref=e683]: + - generic [ref=e684]: + - group [ref=e687]: + - button "Show options" [ref=e688] [cursor=pointer]: + - img "Show options" [ref=e691] + - heading "Copilot AI left a comment" [level=3] [ref=e693]: + - generic [ref=e694]: + - strong [ref=e695]: + - link "Copilot" [ref=e696] [cursor=pointer]: + - /url: /apps/copilot-pull-request-reviewer + - generic [ref=e697]: AI + - text: left a comment + - generic [ref=e700]: + - heading "Pull request overview" [level=2] [ref=e701] + - paragraph [ref=e702]: This PR updates PetFolio’s web/PWA deployment setup (Vercel + GitHub Actions), adds an HTML/React-based “Care Redesign” prototype bundle, and implements substantial Flutter UI refinements for the Care module (compact hero header, trophy UI redesign, weekly chart/task card tweaks), alongside several dependency and platform-guard updates to improve web compatibility. + - paragraph [ref=e703]: + - strong [ref=e704]: "Changes:" + - list [ref=e705]: + - listitem [ref=e706]: Add Vercel deployment configuration + a GitHub Actions workflow to build Flutter Web and deploy to Vercel; update web manifest/index for PWA polish (splash, install banner, theme). + - listitem [ref=e707]: Add “PetFolio Redesign / Care Redesign” prototype files (HTML + JSX components) as a UI handoff bundle. + - listitem [ref=e708]: Refactor Care UI in Flutter (hero header, trophy slider/cards, weekly chart/task cards) and adjust services for web/platform behavior (notifications guarded on web; AI routine proxy on web). + - heading "Reviewed changes" [level=3] [ref=e709] + - paragraph [ref=e710]: Copilot reviewed 26 out of 42 changed files in this pull request and generated 13 comments. + - group [ref=e711]: + - generic "Show a summary per file" [ref=e712] [cursor=pointer] + - group [ref=e713]: + - generic "Comments suppressed due to low confidence (1)" [ref=e714] [cursor=pointer] + - separator [ref=e715] + - paragraph [ref=e716]: + - text: 💡 + - link "Add Copilot custom instructions" [ref=e717] [cursor=pointer]: + - /url: /CodeStorm-Hub/petfolio/new/main?filename=.github/instructions/*.instructions.md + - text: for smarter, more guided reviews. + - link "Learn how to get started" [ref=e718] [cursor=pointer]: + - /url: https://docs.github.com/en/copilot/customizing-copilot/adding-repository-custom-instructions-for-github-copilot + - text: . + - generic [ref=e720]: + - generic [ref=e722]: + - generic [ref=e723]: + - button "Comment thread" [expanded] [ref=e724] [cursor=pointer]: + - img + - link "lib/features/pet_profile/presentation/screens/manage_pets_screen.dart" [ref=e726] [cursor=pointer]: + - /url: /CodeStorm-Hub/petfolio/pull/17/files/7b8aaf186f6047b802df726bad66459f0cfe10ec#diff-584838a82bb9a8b7eca64cad5c516652e00e6ad6c1a45bc3c2201e651c17eae6 + - generic [ref=e727]: + - generic [ref=e729]: Comment on lines 306 to 309 + - table [ref=e732]: + - rowgroup [ref=e733]: + - 'row "307 306 sliver: SliverReorderableList(" [ref=e734]': + - cell "307" [ref=e735] [cursor=pointer] + - cell "306" [ref=e736] [cursor=pointer] + - 'cell "sliver: SliverReorderableList(" [ref=e737]': + - generic [ref=e738]: "sliver: SliverReorderableList(" + - 'row "308 307 itemCount: pets.length," [ref=e739]': + - cell "308" [ref=e740] [cursor=pointer] + - cell "307" [ref=e741] [cursor=pointer] + - 'cell "itemCount: pets.length," [ref=e742]': + - generic [ref=e743]: "itemCount: pets.length," + - 'row "309 - onReorder: onReorder," [ref=e744]': + - cell "309" [ref=e745] [cursor=pointer] + - cell [ref=e746] [cursor=pointer] + - 'cell "- onReorder: onReorder," [ref=e747]': + - generic [ref=e748]: "- onReorder: onReorder," + - 'row "308 + onReorderItem: onReorder," [ref=e749]': + - cell [ref=e750] [cursor=pointer] + - cell "308" [ref=e751] [cursor=pointer] + - 'cell "+ onReorderItem: onReorder," [ref=e752]': + - generic [ref=e753]: "+ onReorderItem: onReorder," + - 'row "310 309 proxyDecorator: (child, _, animation) => Material(" [ref=e754]': + - cell "310" [ref=e755] [cursor=pointer] + - cell "309" [ref=e756] [cursor=pointer] + - 'cell "proxyDecorator: (child, _, animation) => Material(" [ref=e757]': + - generic [ref=e758]: "proxyDecorator: (child, _, animation) => Material(" + - generic [ref=e765]: + - generic [ref=e768]: + - heading "Copilot commented on Jun 3, 2026 13 hours ago" [level=3] [ref=e769]: + - text: Copilot commented + - generic "Jun 3, 2026, 5:19 PM EDT" [ref=e770]: on Jun 3, 2026 13 hours ago + - img "Copilot" [ref=e773] + - generic [ref=e774]: + - generic [ref=e775]: + - generic [ref=e776]: Copilot + - generic [ref=e777]: AI + - link "on Jun 3, 202613 hours ago" [ref=e780] [cursor=pointer]: + - /url: https://github.com/CodeStorm-Hub/petfolio/pull/17#discussion_r3351989311 + - generic "Jun 3, 2026, 5:19 PM EDT" [ref=e781]: on Jun 3, 202613 hours ago + - generic [ref=e783]: + - list [ref=e785]: + - listitem + - listitem [ref=e786]: + - generic [ref=e787]: High + - button "Actions for Copilot's comment, 5:19 PM yesterday" [ref=e789] [cursor=pointer]: + - img [ref=e790] + - generic [ref=e793]: + - paragraph [ref=e796]: + - code [ref=e797]: SliverReorderableList + - text: uses the standard + - code [ref=e798]: onReorder + - text: callback; + - code [ref=e799]: onReorderItem + - text: is not a valid parameter for the Flutter widget and will fail to compile. Use + - code [ref=e800]: "onReorder:" + - text: here (and keep the callback signature + - code [ref=e801]: void Function(int oldIndex, int newIndex) + - text: ). + - generic [ref=e802]: + - toolbar "Reactions" + - generic [ref=e804]: + - button "Positive feedback" [ref=e805] [cursor=pointer]: + - img [ref=e806] + - button "Negative feedback" [ref=e808] [cursor=pointer]: + - img [ref=e809] + - generic [ref=e811]: Copilot uses AI. Check for mistakes. + - generic [ref=e813]: + - generic [ref=e814]: + - button "Comment thread" [expanded] [ref=e815] [cursor=pointer]: + - img + - link "lib/features/care/presentation/widgets/gamified_care_ui.dart" [ref=e817] [cursor=pointer]: + - /url: /CodeStorm-Hub/petfolio/pull/17/files/7b8aaf186f6047b802df726bad66459f0cfe10ec#diff-8e2cf796cdb83ed78c034ccd2f1ab099667749421148c7f255df9b4f1317c508 + - generic [ref=e818]: + - generic [ref=e820]: Comment on lines +1104 to +1114 + - table [ref=e823]: + - rowgroup [ref=e824]: + - 'row "1104 + final sheenDuration = Duration(milliseconds: 3800 + widget.index * 200);" [ref=e825]': + - cell [ref=e826] [cursor=pointer] + - cell "1104" [ref=e827] [cursor=pointer] + - 'cell "+ final sheenDuration = Duration(milliseconds: 3800 + widget.index * 200);" [ref=e828]': + - generic [ref=e829]: "+ final sheenDuration = Duration(milliseconds: 3800 + widget.index * 200);" + - 'row "1105 + _sheenCtrl = AnimationController(vsync: this, duration: sheenDuration);" [ref=e830]': + - cell [ref=e831] [cursor=pointer] + - cell "1105" [ref=e832] [cursor=pointer] + - 'cell "+ _sheenCtrl = AnimationController(vsync: this, duration: sheenDuration);" [ref=e833]': + - generic [ref=e834]: "+ _sheenCtrl = AnimationController(vsync: this, duration: sheenDuration);" + - row "1106 +" [ref=e835]: + - cell [ref=e836] [cursor=pointer] + - cell "1106" [ref=e837] [cursor=pointer] + - cell "+" [ref=e838]: + - generic: + + - 'row "1107 + if (widget.owned) {" [ref=e839]': + - cell [ref=e840] [cursor=pointer] + - cell "1107" [ref=e841] [cursor=pointer] + - 'cell "+ if (widget.owned) {" [ref=e842]': + - generic [ref=e843]: "+ if (widget.owned) {" + - row "1108 + final delayFraction =" [ref=e844]: + - cell [ref=e845] [cursor=pointer] + - cell "1108" [ref=e846] [cursor=pointer] + - cell "+ final delayFraction =" [ref=e847]: + - generic [ref=e848]: + final delayFraction = + - row "1109 + (widget.index * 300 / sheenDuration.inMilliseconds).clamp(0.0, 1.0);" [ref=e849]: + - cell [ref=e850] [cursor=pointer] + - cell "1109" [ref=e851] [cursor=pointer] + - cell "+ (widget.index * 300 / sheenDuration.inMilliseconds).clamp(0.0, 1.0);" [ref=e852]: + - generic [ref=e853]: + (widget.index * 300 / sheenDuration.inMilliseconds).clamp(0.0, 1.0); + - 'row "1110 + _sheenCtrl.forward(from: delayFraction);" [ref=e854]': + - cell [ref=e855] [cursor=pointer] + - cell "1110" [ref=e856] [cursor=pointer] + - 'cell "+ _sheenCtrl.forward(from: delayFraction);" [ref=e857]': + - generic [ref=e858]: "+ _sheenCtrl.forward(from: delayFraction);" + - 'row "1111 + _sheenCtrl.addStatusListener((s) {" [ref=e859]': + - cell [ref=e860] [cursor=pointer] + - cell "1111" [ref=e861] [cursor=pointer] + - 'cell "+ _sheenCtrl.addStatusListener((s) {" [ref=e862]': + - generic [ref=e863]: "+ _sheenCtrl.addStatusListener((s) {" + - row "1112 + if (s == AnimationStatus.completed && mounted) _sheenCtrl.repeat();" [ref=e864]: + - cell [ref=e865] [cursor=pointer] + - cell "1112" [ref=e866] [cursor=pointer] + - cell "+ if (s == AnimationStatus.completed && mounted) _sheenCtrl.repeat();" [ref=e867]: + - generic [ref=e868]: + if (s == AnimationStatus.completed && mounted) _sheenCtrl.repeat(); + - 'row "1113 + });" [ref=e869]': + - cell [ref=e870] [cursor=pointer] + - cell "1113" [ref=e871] [cursor=pointer] + - 'cell "+ });" [ref=e872]': + - generic [ref=e873]: "+ });" + - 'row "1114 + }" [ref=e874]': + - cell [ref=e875] [cursor=pointer] + - cell "1114" [ref=e876] [cursor=pointer] + - 'cell "+ }" [ref=e877]': + - generic [ref=e878]: "+ }" + - generic [ref=e885]: + - generic [ref=e888]: + - heading "Copilot commented on Jun 3, 2026 13 hours ago" [level=3] [ref=e889]: + - text: Copilot commented + - generic "Jun 3, 2026, 5:19 PM EDT" [ref=e890]: on Jun 3, 2026 13 hours ago + - img "Copilot" [ref=e893] + - generic [ref=e894]: + - generic [ref=e895]: + - generic [ref=e896]: Copilot + - generic [ref=e897]: AI + - link "on Jun 3, 202613 hours ago" [ref=e900] [cursor=pointer]: + - /url: https://github.com/CodeStorm-Hub/petfolio/pull/17#discussion_r3351989359 + - generic "Jun 3, 2026, 5:19 PM EDT" [ref=e901]: on Jun 3, 202613 hours ago + - generic [ref=e903]: + - list [ref=e905]: + - listitem + - listitem [ref=e906]: + - generic [ref=e907]: Medium + - button "Actions for Copilot's comment, 5:19 PM yesterday" [ref=e909] [cursor=pointer]: + - img [ref=e910] + - generic [ref=e913]: + - paragraph [ref=e916]: + - text: The sheen animation only starts in + - code [ref=e917]: initState + - text: when + - code [ref=e918]: widget.owned + - text: is true. If a badge becomes owned while the screen is open (provider updates), this state object will be reused and + - code [ref=e919]: _sheenCtrl + - text: will never start, leaving owned badges without the intended sheen until a full rebuild/navigation. + - generic [ref=e920]: + - generic [ref=e922]: + - text: Suggested changeset + - generic [ref=e923]: "1" + - generic [ref=e924]: (1) + - generic [ref=e926]: + - button "Open review comment" [ref=e927] [cursor=pointer]: + - img [ref=e928] + - text: lib/features/care/presentation/widgets/gamified_care_ui.dart + - button "Commit suggestion" [ref=e931] [cursor=pointer]: + - generic [ref=e933]: Commit suggestion + - generic [ref=e934]: + - toolbar "Reactions" + - generic [ref=e936]: + - button "Positive feedback" [ref=e937] [cursor=pointer]: + - img [ref=e938] + - button "Negative feedback" [ref=e940] [cursor=pointer]: + - img [ref=e941] + - generic [ref=e943]: Copilot uses AI. Check for mistakes. + - generic [ref=e945]: + - generic [ref=e946]: + - button "Comment thread" [expanded] [ref=e947] [cursor=pointer]: + - img + - link "vercel.json" [ref=e949] [cursor=pointer]: + - /url: /CodeStorm-Hub/petfolio/pull/17/files/7b8aaf186f6047b802df726bad66459f0cfe10ec#diff-a3265310f552fb66876e8bfe8809737e59e5ba946bdf39138b44d9baf4e21240 + - generic [ref=e950]: + - generic [ref=e952]: Comment on lines +18 to +25 + - table [ref=e955]: + - rowgroup [ref=e956]: + - 'row "18 + }," [ref=e957]': + - cell [ref=e958] [cursor=pointer] + - cell "18" [ref=e959] [cursor=pointer] + - 'cell "+ }," [ref=e960]': + - generic [ref=e961]: "+ }," + - 'row "19 + {" [ref=e962]': + - cell [ref=e963] [cursor=pointer] + - cell "19" [ref=e964] [cursor=pointer] + - 'cell "+ {" [ref=e965]': + - generic [ref=e966]: "+ {" + - 'row "20 + \"source\": \"/(.*)\"," [ref=e967]': + - cell [ref=e968] [cursor=pointer] + - cell "20" [ref=e969] [cursor=pointer] + - 'cell "+ \"source\": \"/(.*)\"," [ref=e970]': + - generic [ref=e971]: "+ \"source\": \"/(.*)\"," + - 'row "21 + \"headers\": [" [ref=e972]': + - cell [ref=e973] [cursor=pointer] + - cell "21" [ref=e974] [cursor=pointer] + - 'cell "+ \"headers\": [" [ref=e975]': + - generic [ref=e976]: "+ \"headers\": [" + - 'row "22 + { \"key\": \"Cross-Origin-Embedder-Policy\", \"value\": \"require-corp\" }," [ref=e977]': + - cell [ref=e978] [cursor=pointer] + - cell "22" [ref=e979] [cursor=pointer] + - 'cell "+ { \"key\": \"Cross-Origin-Embedder-Policy\", \"value\": \"require-corp\" }," [ref=e980]': + - generic [ref=e981]: "+ { \"key\": \"Cross-Origin-Embedder-Policy\", \"value\": \"require-corp\" }," + - 'row "23 + { \"key\": \"Cross-Origin-Opener-Policy\", \"value\": \"same-origin\" }" [ref=e982]': + - cell [ref=e983] [cursor=pointer] + - cell "23" [ref=e984] [cursor=pointer] + - 'cell "+ { \"key\": \"Cross-Origin-Opener-Policy\", \"value\": \"same-origin\" }" [ref=e985]': + - generic [ref=e986]: "+ { \"key\": \"Cross-Origin-Opener-Policy\", \"value\": \"same-origin\" }" + - row "24 + ]" [ref=e987]: + - cell [ref=e988] [cursor=pointer] + - cell "24" [ref=e989] [cursor=pointer] + - cell "+ ]" [ref=e990]: + - generic [ref=e991]: + ] + - 'row "25 + }" [ref=e992]': + - cell [ref=e993] [cursor=pointer] + - cell "25" [ref=e994] [cursor=pointer] + - 'cell "+ }" [ref=e995]': + - generic [ref=e996]: "+ }" + - generic [ref=e1003]: + - generic [ref=e1006]: + - heading "Copilot commented on Jun 3, 2026 13 hours ago" [level=3] [ref=e1007]: + - text: Copilot commented + - generic "Jun 3, 2026, 5:19 PM EDT" [ref=e1008]: on Jun 3, 2026 13 hours ago + - img "Copilot" [ref=e1011] + - generic [ref=e1012]: + - generic [ref=e1013]: + - generic [ref=e1014]: Copilot + - generic [ref=e1015]: AI + - link "on Jun 3, 202613 hours ago" [ref=e1018] [cursor=pointer]: + - /url: https://github.com/CodeStorm-Hub/petfolio/pull/17#discussion_r3351989389 + - generic "Jun 3, 2026, 5:19 PM EDT" [ref=e1019]: on Jun 3, 202613 hours ago + - generic [ref=e1021]: + - list [ref=e1023]: + - listitem + - listitem [ref=e1024]: + - generic [ref=e1025]: High + - button "Actions for Copilot's comment, 5:19 PM yesterday" [ref=e1027] [cursor=pointer]: + - img [ref=e1028] + - generic [ref=e1031]: + - paragraph [ref=e1034]: + - text: Setting + - code [ref=e1035]: "Cross-Origin-Embedder-Policy: require-corp" + - text: + + - code [ref=e1036]: "Cross-Origin-Opener-Policy: same-origin" + - text: for + - strong [ref=e1037]: all + - text: routes can break loading cross-origin resources that don't explicitly grant CORP/CORS (for example + - code [ref=e1038]: https://js.stripe.com/v3/ + - text: included in + - code [ref=e1039]: web/index.html + - text: ). Unless the app specifically needs cross-origin isolation globally, limit these headers to the + - code [ref=e1040]: .wasm + - text: route(s) that require them. + - generic [ref=e1041]: + - generic [ref=e1043]: + - text: Suggested changeset + - generic [ref=e1044]: "1" + - generic [ref=e1045]: (1) + - generic [ref=e1046]: + - generic [ref=e1047]: + - button "Close review comment" [ref=e1048] [cursor=pointer]: + - img [ref=e1049] + - text: vercel.json + - table [ref=e1052]: + - rowgroup [ref=e1053]: + - row "Original file line number Diff line number Diff line change" [ref=e1054]: + - columnheader "Original file line number" [ref=e1055] + - columnheader "Diff line number" [ref=e1056] + - columnheader "Diff line change" [ref=e1057] + - rowgroup [ref=e1062]: + - row "@@ -15,13 +15,6 @@" [ref=e1063]: + - cell "@@ -15,13 +15,6 @@" [ref=e1064]: + - generic [ref=e1065]: + - img [ref=e1067] + - code [ref=e1069]: + - generic [ref=e1070]: "@@ -15,13 +15,6 @@" + - 'row "15 15 { \"key\": \"Cross-Origin-Embedder-Policy\", \"value\": \"require-corp\" }," [ref=e1071]': + - cell "15" [ref=e1072]: + - code [ref=e1073]: "15" + - cell "15" [ref=e1074]: + - code [ref=e1075]: "15" + - 'cell "{ \"key\": \"Cross-Origin-Embedder-Policy\", \"value\": \"require-corp\" }," [ref=e1076]': + - code [ref=e1077]: + - generic [ref=e1078]: + - text: "{ \"key\":" + - generic [ref=e1079]: "\"Cross-Origin-Embedder-Policy\"" + - text: ", \"value\":" + - generic [ref=e1080]: "\"require-corp\"" + - text: "}," + - 'row "16 16 { \"key\": \"Cross-Origin-Opener-Policy\", \"value\": \"same-origin\" }" [ref=e1081]': + - cell "16" [ref=e1082]: + - code [ref=e1083]: "16" + - cell "16" [ref=e1084]: + - code [ref=e1085]: "16" + - 'cell "{ \"key\": \"Cross-Origin-Opener-Policy\", \"value\": \"same-origin\" }" [ref=e1086]': + - code [ref=e1087]: + - generic [ref=e1088]: + - text: "{ \"key\":" + - generic [ref=e1089]: "\"Cross-Origin-Opener-Policy\"" + - text: ", \"value\":" + - generic [ref=e1090]: "\"same-origin\"" + - text: "}" + - row "17 17 ]" [ref=e1091]: + - cell "17" [ref=e1092]: + - code [ref=e1093]: "17" + - cell "17" [ref=e1094]: + - code [ref=e1095]: "17" + - cell "]" [ref=e1096]: + - code [ref=e1097]: + - generic [ref=e1098]: "]" + - 'row "18 - }," [ref=e1099]': + - cell "18" [ref=e1100]: + - code [ref=e1101]: "18" + - cell [ref=e1102]: + - code + - 'cell "- }," [ref=e1103]': + - code [ref=e1104]: + - generic [ref=e1105]: "-" + - generic [ref=e1106]: "}," + - 'row "19 - {" [ref=e1107]': + - cell "19" [ref=e1108]: + - code [ref=e1109]: "19" + - cell [ref=e1110]: + - code + - 'cell "- {" [ref=e1111]': + - code [ref=e1112]: + - generic [ref=e1113]: "-" + - generic [ref=e1114]: "{" + - 'row "20 - \"source\": \"/(.*)\"," [ref=e1115]': + - cell "20" [ref=e1116]: + - code [ref=e1117]: "20" + - cell [ref=e1118]: + - code + - 'cell "- \"source\": \"/(.*)\"," [ref=e1119]': + - code [ref=e1120]: + - generic [ref=e1121]: "-" + - generic [ref=e1122]: + - text: "\"source\":" + - generic [ref=e1123]: "\"/(.*)\"" + - text: "," + - 'row "21 - \"headers\": [" [ref=e1124]': + - cell "21" [ref=e1125]: + - code [ref=e1126]: "21" + - cell [ref=e1127]: + - code + - 'cell "- \"headers\": [" [ref=e1128]': + - code [ref=e1129]: + - generic [ref=e1130]: "-" + - generic [ref=e1131]: "\"headers\": [" + - 'row "22 - { \"key\": \"Cross-Origin-Embedder-Policy\", \"value\": \"require-corp\" }," [ref=e1132]': + - cell "22" [ref=e1133]: + - code [ref=e1134]: "22" + - cell [ref=e1135]: + - code + - 'cell "- { \"key\": \"Cross-Origin-Embedder-Policy\", \"value\": \"require-corp\" }," [ref=e1136]': + - code [ref=e1137]: + - generic [ref=e1138]: "-" + - generic [ref=e1139]: + - text: "{ \"key\":" + - generic [ref=e1140]: "\"Cross-Origin-Embedder-Policy\"" + - text: ", \"value\":" + - generic [ref=e1141]: "\"require-corp\"" + - text: "}," + - 'row "23 - { \"key\": \"Cross-Origin-Opener-Policy\", \"value\": \"same-origin\" }" [ref=e1142]': + - cell "23" [ref=e1143]: + - code [ref=e1144]: "23" + - cell [ref=e1145]: + - code + - 'cell "- { \"key\": \"Cross-Origin-Opener-Policy\", \"value\": \"same-origin\" }" [ref=e1146]': + - code [ref=e1147]: + - generic [ref=e1148]: "-" + - generic [ref=e1149]: + - text: "{ \"key\":" + - generic [ref=e1150]: "\"Cross-Origin-Opener-Policy\"" + - text: ", \"value\":" + - generic [ref=e1151]: "\"same-origin\"" + - text: "}" + - row "24 - ]" [ref=e1152]: + - cell "24" [ref=e1153]: + - code [ref=e1154]: "24" + - cell [ref=e1155]: + - code + - cell "- ]" [ref=e1156]: + - code [ref=e1157]: + - generic [ref=e1158]: "-" + - generic [ref=e1159]: "]" + - 'row "25 18 }" [ref=e1160]': + - cell "25" [ref=e1161]: + - code [ref=e1162]: "25" + - cell "18" [ref=e1163]: + - code [ref=e1164]: "18" + - 'cell "}" [ref=e1165]': + - code [ref=e1166]: + - generic [ref=e1167]: "}" + - row "26 19 ]," [ref=e1168]: + - cell "26" [ref=e1169]: + - code [ref=e1170]: "26" + - cell "19" [ref=e1171]: + - code [ref=e1172]: "19" + - cell "]," [ref=e1173]: + - code [ref=e1174]: + - generic [ref=e1175]: "]," + - 'row "27 20 \"rewrites\": [" [ref=e1176]': + - cell "27" [ref=e1177]: + - code [ref=e1178]: "27" + - cell "20" [ref=e1179]: + - code [ref=e1180]: "20" + - 'cell "\"rewrites\": [" [ref=e1181]': + - code [ref=e1182]: + - generic [ref=e1183]: "\"rewrites\": [" + - button "Commit suggestion" [ref=e1185] [cursor=pointer]: + - generic [ref=e1187]: Commit suggestion + - generic [ref=e1188]: + - toolbar "Reactions" + - generic [ref=e1190]: + - button "Positive feedback" [ref=e1191] [cursor=pointer]: + - img [ref=e1192] + - button "Negative feedback" [ref=e1194] [cursor=pointer]: + - img [ref=e1195] + - generic [ref=e1197]: Copilot uses AI. Check for mistakes. + - generic [ref=e1199]: + - generic [ref=e1200]: + - button "Comment thread" [expanded] [ref=e1201] [cursor=pointer]: + - img + - link ".claude/launch.json" [ref=e1203] [cursor=pointer]: + - /url: /CodeStorm-Hub/petfolio/pull/17/files/7b8aaf186f6047b802df726bad66459f0cfe10ec#diff-f02a361fa828af1bddd7c60baccb9ec7036882b427c5e6f2d734cf6414f3b36c + - generic [ref=e1204]: + - generic [ref=e1206]: Comment on lines +12 to +15 + - table [ref=e1209]: + - rowgroup [ref=e1210]: + - row "12 + \"--dart-define=SUPABASE_URL=https://jqyjvhwlcqcsuwcqgcwf.supabase.co\"," [ref=e1211]: + - cell [ref=e1212] [cursor=pointer] + - cell "12" [ref=e1213] [cursor=pointer] + - cell "+ \"--dart-define=SUPABASE_URL=https://jqyjvhwlcqcsuwcqgcwf.supabase.co\"," [ref=e1214]: + - generic [ref=e1215]: + "--dart-define=SUPABASE_URL=https://jqyjvhwlcqcsuwcqgcwf.supabase.co", + - row "13 + \"--dart-define=SUPABASE_ANON_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6ImpxeWp2aHdsY3Fjc3V3Y3FnY3dmIiwicm9sZSI6ImFub24iLCJpYXQiOjE3Nzg1MjI4MjgsImV4cCI6MjA5NDA5ODgyOH0.3bF68bNG0IwAc50YbOC3sem4k8O-d1vkvNNqBt1HbRw\"," [ref=e1216]: + - cell [ref=e1217] [cursor=pointer] + - cell "13" [ref=e1218] [cursor=pointer] + - cell "+ \"--dart-define=SUPABASE_ANON_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6ImpxeWp2aHdsY3Fjc3V3Y3FnY3dmIiwicm9sZSI6ImFub24iLCJpYXQiOjE3Nzg1MjI4MjgsImV4cCI6MjA5NDA5ODgyOH0.3bF68bNG0IwAc50YbOC3sem4k8O-d1vkvNNqBt1HbRw\"," [ref=e1219]: + - generic [ref=e1220]: + "--dart-define=SUPABASE_ANON_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6ImpxeWp2aHdsY3Fjc3V3Y3FnY3dmIiwicm9sZSI6ImFub24iLCJpYXQiOjE3Nzg1MjI4MjgsImV4cCI6MjA5NDA5ODgyOH0.3bF68bNG0IwAc50YbOC3sem4k8O-d1vkvNNqBt1HbRw", + - row "14 + \"--dart-define=STRIPE_PUBLISHABLE_KEY=pk_test_51TQvlrPcVRApxzIxJ8RmKYA1WEw7k8zubumbIfsDjRSGgDyAcSU22RhsZRtKIP1lAZ0wtGjpLfzjI4fozMZxGSlo006zuZrbon\"," [ref=e1221]: + - cell [ref=e1222] [cursor=pointer] + - cell "14" [ref=e1223] [cursor=pointer] + - cell "+ \"--dart-define=STRIPE_PUBLISHABLE_KEY=pk_test_51TQvlrPcVRApxzIxJ8RmKYA1WEw7k8zubumbIfsDjRSGgDyAcSU22RhsZRtKIP1lAZ0wtGjpLfzjI4fozMZxGSlo006zuZrbon\"," [ref=e1224]: + - generic [ref=e1225]: + "--dart-define=STRIPE_PUBLISHABLE_KEY=pk_test_51TQvlrPcVRApxzIxJ8RmKYA1WEw7k8zubumbIfsDjRSGgDyAcSU22RhsZRtKIP1lAZ0wtGjpLfzjI4fozMZxGSlo006zuZrbon", + - row "15 + \"--dart-define=NVIDIA_API_KEY=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\"" [ref=e1226]: + - cell [ref=e1227] [cursor=pointer] + - cell "15" [ref=e1228] [cursor=pointer] + - cell "+ \"--dart-define=NVIDIA_API_KEY=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\"" [ref=e1229]: + - generic [ref=e1230]: + "--dart-define=NVIDIA_API_KEY=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" + - generic [ref=e1237]: + - generic [ref=e1240]: + - heading "Copilot commented on Jun 3, 2026 13 hours ago" [level=3] [ref=e1241]: + - text: Copilot commented + - generic "Jun 3, 2026, 5:19 PM EDT" [ref=e1242]: on Jun 3, 2026 13 hours ago + - img "Copilot" [ref=e1245] + - generic [ref=e1246]: + - generic [ref=e1247]: + - generic [ref=e1248]: Copilot + - generic [ref=e1249]: AI + - link "on Jun 3, 202613 hours ago" [ref=e1252] [cursor=pointer]: + - /url: https://github.com/CodeStorm-Hub/petfolio/pull/17#discussion_r3351989407 + - generic "Jun 3, 2026, 5:19 PM EDT" [ref=e1253]: on Jun 3, 202613 hours ago + - generic [ref=e1255]: + - list [ref=e1257]: + - listitem + - listitem [ref=e1258]: + - generic [ref=e1259]: High + - button "Actions for Copilot's comment, 5:19 PM yesterday" [ref=e1261] [cursor=pointer]: + - img [ref=e1262] + - generic [ref=e1265]: + - paragraph [ref=e1268]: + - text: This launch config commits real credentials/tokens ( + - code [ref=e1269]: SUPABASE_ANON_KEY + - text: ", Stripe publishable key, NVIDIA API key). Even if some are \"public\", committing them makes rotation harder and can trigger automated secret scanning. Prefer reading values from an untracked" + - code [ref=e1270]: .env + - text: (the repo already uses + - code [ref=e1271]: "--dart-define-from-file=.env" + - text: ). + - generic [ref=e1272]: + - generic [ref=e1274]: + - text: Suggested changeset + - generic [ref=e1275]: "1" + - generic [ref=e1276]: (1) + - generic [ref=e1277]: + - generic [ref=e1278]: + - button "Close review comment" [ref=e1279] [cursor=pointer]: + - img [ref=e1280] + - text: .claude/launch.json + - table [ref=e1283]: + - rowgroup [ref=e1284]: + - row "Original file line number Diff line number Diff line change" [ref=e1285]: + - columnheader "Original file line number" [ref=e1286] + - columnheader "Diff line number" [ref=e1287] + - columnheader "Diff line change" [ref=e1288] + - rowgroup [ref=e1293]: + - row "@@ -9,10 +9,7 @@" [ref=e1294]: + - cell "@@ -9,10 +9,7 @@" [ref=e1295]: + - generic [ref=e1296]: + - img [ref=e1298] + - code [ref=e1300]: + - generic [ref=e1301]: "@@ -9,10 +9,7 @@" + - row "9 9 \"-d\", \"web-server\"," [ref=e1302]: + - cell "9" [ref=e1303]: + - code [ref=e1304]: "9" + - cell "9" [ref=e1305]: + - code [ref=e1306]: "9" + - cell "\"-d\", \"web-server\"," [ref=e1307]: + - code [ref=e1308]: + - generic [ref=e1309]: + - generic [ref=e1310]: "\"-d\"" + - text: "," + - generic [ref=e1311]: "\"web-server\"" + - text: "," + - row "10 10 \"--web-port=8080\"," [ref=e1312]: + - cell "10" [ref=e1313]: + - code [ref=e1314]: "10" + - cell "10" [ref=e1315]: + - code [ref=e1316]: "10" + - cell "\"--web-port=8080\"," [ref=e1317]: + - code [ref=e1318]: + - generic [ref=e1319]: + - generic [ref=e1320]: "\"--web-port=8080\"" + - text: "," + - row "11 11 \"--web-hostname=localhost\"," [ref=e1321]: + - cell "11" [ref=e1322]: + - code [ref=e1323]: "11" + - cell "11" [ref=e1324]: + - code [ref=e1325]: "11" + - cell "\"--web-hostname=localhost\"," [ref=e1326]: + - code [ref=e1327]: + - generic [ref=e1328]: + - generic [ref=e1329]: "\"--web-hostname=localhost\"" + - text: "," + - row "12 - \"--dart-define=SUPABASE_URL=https://jqyjvhwlcqcsuwcqgcwf.supabase.co\"," [ref=e1330]: + - cell "12" [ref=e1331]: + - code [ref=e1332]: "12" + - cell [ref=e1333]: + - code + - cell "- \"--dart-define=SUPABASE_URL=https://jqyjvhwlcqcsuwcqgcwf.supabase.co\"," [ref=e1334]: + - code [ref=e1335]: + - generic [ref=e1336]: "-" + - generic [ref=e1337]: + - generic [ref=e1338]: "\"--dart-define=SUPABASE_URL=https://jqyjvhwlcqcsuwcqgcwf.supabase.co\"" + - text: "," + - row "13 - \"--dart-define=SUPABASE_ANON_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6ImpxeWp2aHdsY3Fjc3V3Y3FnY3dmIiwicm9sZSI6ImFub24iLCJpYXQiOjE3Nzg1MjI4MjgsImV4cCI6MjA5NDA5ODgyOH0.3bF68bNG0IwAc50YbOC3sem4k8O-d1vkvNNqBt1HbRw\"," [ref=e1339]: + - cell "13" [ref=e1340]: + - code [ref=e1341]: "13" + - cell [ref=e1342]: + - code + - cell "- \"--dart-define=SUPABASE_ANON_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6ImpxeWp2aHdsY3Fjc3V3Y3FnY3dmIiwicm9sZSI6ImFub24iLCJpYXQiOjE3Nzg1MjI4MjgsImV4cCI6MjA5NDA5ODgyOH0.3bF68bNG0IwAc50YbOC3sem4k8O-d1vkvNNqBt1HbRw\"," [ref=e1343]: + - code [ref=e1344]: + - generic [ref=e1345]: "-" + - generic [ref=e1346]: + - generic [ref=e1347]: "\"--dart-define=SUPABASE_ANON_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6ImpxeWp2aHdsY3Fjc3V3Y3FnY3dmIiwicm9sZSI6ImFub24iLCJpYXQiOjE3Nzg1MjI4MjgsImV4cCI6MjA5NDA5ODgyOH0.3bF68bNG0IwAc50YbOC3sem4k8O-d1vkvNNqBt1HbRw\"" + - text: "," + - row "14 - \"--dart-define=STRIPE_PUBLISHABLE_KEY=pk_test_51TQvlrPcVRApxzIxJ8RmKYA1WEw7k8zubumbIfsDjRSGgDyAcSU22RhsZRtKIP1lAZ0wtGjpLfzjI4fozMZxGSlo006zuZrbon\"," [ref=e1348]: + - cell "14" [ref=e1349]: + - code [ref=e1350]: "14" + - cell [ref=e1351]: + - code + - cell "- \"--dart-define=STRIPE_PUBLISHABLE_KEY=pk_test_51TQvlrPcVRApxzIxJ8RmKYA1WEw7k8zubumbIfsDjRSGgDyAcSU22RhsZRtKIP1lAZ0wtGjpLfzjI4fozMZxGSlo006zuZrbon\"," [ref=e1352]: + - code [ref=e1353]: + - generic [ref=e1354]: "-" + - generic [ref=e1355]: + - generic [ref=e1356]: "\"--dart-define=STRIPE_PUBLISHABLE_KEY=pk_test_51TQvlrPcVRApxzIxJ8RmKYA1WEw7k8zubumbIfsDjRSGgDyAcSU22RhsZRtKIP1lAZ0wtGjpLfzjI4fozMZxGSlo006zuZrbon\"" + - text: "," + - row "15 - \"--dart-define=NVIDIA_API_KEY=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\"" [ref=e1357]: + - cell "15" [ref=e1358]: + - code [ref=e1359]: "15" + - cell [ref=e1360]: + - code + - cell "- \"--dart-define=NVIDIA_API_KEY=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\"" [ref=e1361]: + - code [ref=e1362]: + - generic [ref=e1363]: "-" + - generic [ref=e1365]: "\"--dart-define=NVIDIA_API_KEY=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\"" + - row "12 + \"--dart-define-from-file=.env\"" [ref=e1366]: + - cell [ref=e1367]: + - code + - cell "12" [ref=e1368]: + - code [ref=e1369]: "12" + - cell "+ \"--dart-define-from-file=.env\"" [ref=e1370]: + - code [ref=e1371]: + - generic [ref=e1372]: + + - generic [ref=e1374]: "\"--dart-define-from-file=.env\"" + - row "16 13 ]," [ref=e1375]: + - cell "16" [ref=e1376]: + - code [ref=e1377]: "16" + - cell "13" [ref=e1378]: + - code [ref=e1379]: "13" + - cell "]," [ref=e1380]: + - code [ref=e1381]: + - generic [ref=e1382]: "]," + - 'row "17 14 \"port\": 8080" [ref=e1383]': + - cell "17" [ref=e1384]: + - code [ref=e1385]: "17" + - cell "14" [ref=e1386]: + - code [ref=e1387]: "14" + - 'cell "\"port\": 8080" [ref=e1388]': + - code [ref=e1389]: + - generic [ref=e1390]: "\"port\": 8080" + - 'row "18 15 }" [ref=e1391]': + - cell "18" [ref=e1392]: + - code [ref=e1393]: "18" + - cell "15" [ref=e1394]: + - code [ref=e1395]: "15" + - 'cell "}" [ref=e1396]': + - code [ref=e1397]: + - generic [ref=e1398]: "}" + - button "Commit suggestion" [ref=e1400] [cursor=pointer]: + - generic [ref=e1402]: Commit suggestion + - generic [ref=e1403]: + - toolbar "Reactions" + - generic [ref=e1405]: + - button "Positive feedback" [ref=e1406] [cursor=pointer]: + - img [ref=e1407] + - button "Negative feedback" [ref=e1409] [cursor=pointer]: + - img [ref=e1410] + - generic [ref=e1412]: Copilot uses AI. Check for mistakes. + - generic [ref=e1414]: + - generic [ref=e1415]: + - button "Comment thread" [expanded] [ref=e1416] [cursor=pointer]: + - img + - link "PetFolio Redesign/README.md" [ref=e1418] [cursor=pointer]: + - /url: /CodeStorm-Hub/petfolio/pull/17/files/7b8aaf186f6047b802df726bad66459f0cfe10ec#diff-fd9c484a61afd6b6a220d0eb2d9b8ee1a8a8490653b7a94ced8ea8e43d7860c7 + - generic [ref=e1419]: + - generic [ref=e1421]: Comment on lines +9 to +10 + - table [ref=e1424]: + - rowgroup [ref=e1425]: + - 'row "9 + **Read `petfolio-redesign/project/Care Redesign/Care Redesign.html` in full.** The user had this file open when they triggered the handoff, so it''s almost certainly the primary design they want built. Read it top to bottom — don''t skim. Then **follow its imports**: open every file it pulls in (shared components, CSS, scripts) so you understand how the pieces fit together before you start implementing." [ref=e1426]': + - cell [ref=e1427] [cursor=pointer] + - cell "9" [ref=e1428] [cursor=pointer] + - 'cell "+ **Read `petfolio-redesign/project/Care Redesign/Care Redesign.html` in full.** The user had this file open when they triggered the handoff, so it''s almost certainly the primary design they want built. Read it top to bottom — don''t skim. Then **follow its imports**: open every file it pulls in (shared components, CSS, scripts) so you understand how the pieces fit together before you start implementing." [ref=e1429]': + - generic [ref=e1430]: "+ **Read `petfolio-redesign/project/Care Redesign/Care Redesign.html` in full.** The user had this file open when they triggered the handoff, so it's almost certainly the primary design they want built. Read it top to bottom — don't skim. Then **follow its imports**: open every file it pulls in (shared components, CSS, scripts) so you understand how the pieces fit together before you start implementing." + - row "10 +" [ref=e1431]: + - cell [ref=e1432] [cursor=pointer] + - cell "10" [ref=e1433] [cursor=pointer] + - cell "+" [ref=e1434]: + - generic: + + - generic [ref=e1441]: + - generic [ref=e1444]: + - heading "Copilot commented on Jun 3, 2026 13 hours ago" [level=3] [ref=e1445]: + - text: Copilot commented + - generic "Jun 3, 2026, 5:19 PM EDT" [ref=e1446]: on Jun 3, 2026 13 hours ago + - img "Copilot" [ref=e1449] + - generic [ref=e1450]: + - generic [ref=e1451]: + - generic [ref=e1452]: Copilot + - generic [ref=e1453]: AI + - link "on Jun 3, 202613 hours ago" [ref=e1456] [cursor=pointer]: + - /url: https://github.com/CodeStorm-Hub/petfolio/pull/17#discussion_r3351989432 + - generic "Jun 3, 2026, 5:19 PM EDT" [ref=e1457]: on Jun 3, 202613 hours ago + - generic [ref=e1459]: + - list [ref=e1461]: + - listitem + - listitem [ref=e1462]: + - generic [ref=e1463]: Medium + - button "Actions for Copilot's comment, 5:19 PM yesterday" [ref=e1465] [cursor=pointer]: + - img [ref=e1466] + - generic [ref=e1469]: + - paragraph [ref=e1472]: + - text: The README points agents to + - code [ref=e1473]: petfolio-redesign/project/Care Redesign/Care Redesign.html + - text: ", but in this repo the prototype lives under" + - code [ref=e1474]: PetFolio Redesign/Care Redesign/Care Redesign.html + - text: . This path mismatch makes the handoff instructions incorrect. + - generic [ref=e1475]: + - generic [ref=e1477]: + - text: Suggested changeset + - generic [ref=e1478]: "1" + - generic [ref=e1479]: (1) + - generic [ref=e1480]: + - generic [ref=e1481]: + - button "Close review comment" [ref=e1482] [cursor=pointer]: + - img [ref=e1483] + - text: PetFolio Redesign/README.md + - table [ref=e1486]: + - rowgroup [ref=e1487]: + - row "Original file line number Diff line number Diff line change" [ref=e1488]: + - columnheader "Original file line number" [ref=e1489] + - columnheader "Diff line number" [ref=e1490] + - columnheader "Diff line change" [ref=e1491] + - rowgroup [ref=e1496]: + - row "@@ -6,8 +6,7 @@ A user mocked up designs in HTML/CSS/JS using an AI design tool, then exported t" [ref=e1497]: + - cell "@@ -6,8 +6,7 @@ A user mocked up designs in HTML/CSS/JS using an AI design tool, then exported t" [ref=e1498]: + - generic [ref=e1499]: + - img [ref=e1501] + - code [ref=e1503]: + - generic [ref=e1504]: "@@ -6,8 +6,7 @@ A user mocked up designs in HTML/CSS/JS using an AI design tool, then exported t" + - row "6 6" [ref=e1505]: + - cell "6" [ref=e1506]: + - code [ref=e1507]: "6" + - cell "6" [ref=e1508]: + - code [ref=e1509]: "6" + - cell [ref=e1510]: + - code + - 'row "7 7 ## What you should do — IMPORTANT" [ref=e1511]': + - cell "7" [ref=e1512]: + - code [ref=e1513]: "7" + - cell "7" [ref=e1514]: + - code [ref=e1515]: "7" + - cell "## What you should do — IMPORTANT" [ref=e1516]: + - code [ref=e1517]: + - generic [ref=e1519]: "## What you should do — IMPORTANT" + - row "8 8" [ref=e1520]: + - cell "8" [ref=e1521]: + - code [ref=e1522]: "8" + - cell "8" [ref=e1523]: + - code [ref=e1524]: "8" + - cell [ref=e1525]: + - code + - 'row "9 - **Read `petfolio-redesign/project/Care Redesign/Care Redesign.html` in full.** The user had this file open when they triggered the handoff, so it''s almost certainly the primary design they want built. Read it top to bottom — don''t skim. Then **follow its imports**: open every file it pulls in (shared components, CSS, scripts) so you understand how the pieces fit together before you start implementing." [ref=e1526]': + - cell "9" [ref=e1527]: + - code [ref=e1528]: "9" + - cell [ref=e1529]: + - code + - 'cell "- **Read `petfolio-redesign/project/Care Redesign/Care Redesign.html` in full.** The user had this file open when they triggered the handoff, so it''s almost certainly the primary design they want built. Read it top to bottom — don''t skim. Then **follow its imports**: open every file it pulls in (shared components, CSS, scripts) so you understand how the pieces fit together before you start implementing." [ref=e1530]': + - code [ref=e1531]: + - generic [ref=e1532]: "-" + - generic [ref=e1533]: "**Read `petfolio-redesign/project/Care Redesign/Care Redesign.html` in full.** The user had this file open when they triggered the handoff, so it's almost certainly the primary design they want built. Read it top to bottom — don't skim. Then **follow its imports**: open every file it pulls in (shared components, CSS, scripts) so you understand how the pieces fit together before you start implementing." + - row "10 -" [ref=e1534]: + - cell "10" [ref=e1535]: + - code [ref=e1536]: "10" + - cell [ref=e1537]: + - code + - cell "-" [ref=e1538]: + - code: + - generic [ref=e1539]: "-" + - 'row "9 + **Read `PetFolio Redesign/Care Redesign/Care Redesign.html` in full.** The user had this file open when they triggered the handoff, so it''s almost certainly the primary design they want built. Read it top to bottom — don''t skim. Then **follow its imports**: open every file it pulls in (shared components, CSS, scripts) so you understand how the pieces fit together before you start implementing." [ref=e1540]': + - cell [ref=e1541]: + - code + - cell "9" [ref=e1542]: + - code [ref=e1543]: "9" + - 'cell "+ **Read `PetFolio Redesign/Care Redesign/Care Redesign.html` in full.** The user had this file open when they triggered the handoff, so it''s almost certainly the primary design they want built. Read it top to bottom — don''t skim. Then **follow its imports**: open every file it pulls in (shared components, CSS, scripts) so you understand how the pieces fit together before you start implementing." [ref=e1544]': + - code [ref=e1545]: + - generic [ref=e1546]: + + - generic [ref=e1547]: "**Read `PetFolio Redesign/Care Redesign/Care Redesign.html` in full.** The user had this file open when they triggered the handoff, so it's almost certainly the primary design they want built. Read it top to bottom — don't skim. Then **follow its imports**: open every file it pulls in (shared components, CSS, scripts) so you understand how the pieces fit together before you start implementing." + - row "11 10 **If anything is ambiguous, ask the user to confirm before you start implementing.** It's much cheaper to clarify scope up front than to build the wrong thing." [ref=e1548]: + - cell "11" [ref=e1549]: + - code [ref=e1550]: "11" + - cell "10" [ref=e1551]: + - code [ref=e1552]: "10" + - cell "**If anything is ambiguous, ask the user to confirm before you start implementing.** It's much cheaper to clarify scope up front than to build the wrong thing." [ref=e1553]: + - code [ref=e1554]: + - generic [ref=e1555]: "**If anything is ambiguous, ask the user to confirm before you start implementing.** It's much cheaper to clarify scope up front than to build the wrong thing." + - row "12 11" [ref=e1556]: + - cell "12" [ref=e1557]: + - code [ref=e1558]: "12" + - cell "11" [ref=e1559]: + - code [ref=e1560]: "11" + - cell [ref=e1561]: + - code + - 'row "13 12 ## About the design files" [ref=e1562]': + - cell "13" [ref=e1563]: + - code [ref=e1564]: "13" + - cell "12" [ref=e1565]: + - code [ref=e1566]: "12" + - cell "## About the design files" [ref=e1567]: + - code [ref=e1568]: + - generic [ref=e1570]: "## About the design files" + - button "Commit suggestion" [ref=e1572] [cursor=pointer]: + - generic [ref=e1574]: Commit suggestion + - generic [ref=e1575]: + - toolbar "Reactions" + - generic [ref=e1577]: + - button "Positive feedback" [ref=e1578] [cursor=pointer]: + - img [ref=e1579] + - button "Negative feedback" [ref=e1581] [cursor=pointer]: + - img [ref=e1582] + - generic [ref=e1584]: Copilot uses AI. Check for mistakes. + - generic [ref=e1586]: + - button "3 hidden conversations" [ref=e1587] [cursor=pointer] + - button "Load more…" [ref=e1588] [cursor=pointer] + - generic [ref=e1590]: + - generic [ref=e1591]: + - button "Comment thread" [expanded] [ref=e1592] [cursor=pointer]: + - img + - link "flutter-run-log.md" [ref=e1594] [cursor=pointer]: + - /url: /CodeStorm-Hub/petfolio/pull/17/files/7b8aaf186f6047b802df726bad66459f0cfe10ec#diff-16b408423dbe231e58527cb84ef8afbf35e91e234ca86b7b2301d59ba2351f7d + - generic [ref=e1595]: + - generic [ref=e1597]: Comment on lines +1 to +10 + - table [ref=e1600]: + - rowgroup [ref=e1601]: + - row "1 + PS G:\\GitHub\\petfolio> flutter run --dart-define-from-file=.env" [ref=e1602]: + - cell [ref=e1603] [cursor=pointer] + - cell "1" [ref=e1604] [cursor=pointer] + - cell "+ PS G:\\GitHub\\petfolio> flutter run --dart-define-from-file=.env" [ref=e1605]: + - generic [ref=e1606]: + PS G:\GitHub\petfolio> flutter run --dart-define-from-file=.env + - row "2 + Launching lib\\main.dart on sdk gphone16k x86 64 in debug mode..." [ref=e1607]: + - cell [ref=e1608] [cursor=pointer] + - cell "2" [ref=e1609] [cursor=pointer] + - cell "+ Launching lib\\main.dart on sdk gphone16k x86 64 in debug mode..." [ref=e1610]: + - generic [ref=e1611]: + Launching lib\main.dart on sdk gphone16k x86 64 in debug mode... + - 'row "3 + WARNING: Your Android app project: app located at: G:\\GitHub\\petfolio\\android\\app\\build.gradle.kts" [ref=e1612]': + - cell [ref=e1613] [cursor=pointer] + - cell "3" [ref=e1614] [cursor=pointer] + - 'cell "+ WARNING: Your Android app project: app located at: G:\\GitHub\\petfolio\\android\\app\\build.gradle.kts" [ref=e1615]': + - generic [ref=e1616]: "+ WARNING: Your Android app project: app located at: G:\\GitHub\\petfolio\\android\\app\\build.gradle.kts" + - row "4 + applies the Kotlin Gradle Plugin, which will cause build failures in future versions of Flutter." [ref=e1617]: + - cell [ref=e1618] [cursor=pointer] + - cell "4" [ref=e1619] [cursor=pointer] + - cell "+ applies the Kotlin Gradle Plugin, which will cause build failures in future versions of Flutter." [ref=e1620]: + - generic [ref=e1621]: + applies the Kotlin Gradle Plugin, which will cause build failures in future versions of Flutter. + - 'row "5 + Please migrate your app to Built-in Kotlin using this guide: https://docs.flutter.dev/release/breaking-changes/migrate-to-built-in-kotlin/for-app-developers" [ref=e1622]': + - cell [ref=e1623] [cursor=pointer] + - cell "5" [ref=e1624] [cursor=pointer] + - 'cell "+ Please migrate your app to Built-in Kotlin using this guide: https://docs.flutter.dev/release/breaking-changes/migrate-to-built-in-kotlin/for-app-developers" [ref=e1625]': + - generic [ref=e1626]: "+ Please migrate your app to Built-in Kotlin using this guide: https://docs.flutter.dev/release/breaking-changes/migrate-to-built-in-kotlin/for-app-developers" + - row "6 +" [ref=e1627]: + - cell [ref=e1628] [cursor=pointer] + - cell "6" [ref=e1629] [cursor=pointer] + - cell "+" [ref=e1630]: + - generic: + + - 'row "7 + WARNING: Your app uses the following plugins that apply Kotlin Gradle Plugin (KGP): image_picker_android, share_plus, shared_preferences_android, stripe_android, url_launcher_android" [ref=e1631]': + - cell [ref=e1632] [cursor=pointer] + - cell "7" [ref=e1633] [cursor=pointer] + - 'cell "+ WARNING: Your app uses the following plugins that apply Kotlin Gradle Plugin (KGP): image_picker_android, share_plus, shared_preferences_android, stripe_android, url_launcher_android" [ref=e1634]': + - generic [ref=e1635]: "+ WARNING: Your app uses the following plugins that apply Kotlin Gradle Plugin (KGP): image_picker_android, share_plus, shared_preferences_android, stripe_android, url_launcher_android" + - row "8 + Future versions of Flutter will fail to build if your app uses plugins that apply KGP." [ref=e1636]: + - cell [ref=e1637] [cursor=pointer] + - cell "8" [ref=e1638] [cursor=pointer] + - cell "+ Future versions of Flutter will fail to build if your app uses plugins that apply KGP." [ref=e1639]: + - generic [ref=e1640]: + Future versions of Flutter will fail to build if your app uses plugins that apply KGP. + - row "9 +" [ref=e1641]: + - cell [ref=e1642] [cursor=pointer] + - cell "9" [ref=e1643] [cursor=pointer] + - cell "+" [ref=e1644]: + - generic: + + - row "10 + Please check the changelogs of these plugins and upgrade to a version that supports Built-in Kotlin." [ref=e1645]: + - cell [ref=e1646] [cursor=pointer] + - cell "10" [ref=e1647] [cursor=pointer] + - cell "+ Please check the changelogs of these plugins and upgrade to a version that supports Built-in Kotlin." [ref=e1648]: + - generic [ref=e1649]: + Please check the changelogs of these plugins and upgrade to a version that supports Built-in Kotlin. + - generic [ref=e1656]: + - generic [ref=e1659]: + - heading "Copilot commented on Jun 3, 2026 13 hours ago" [level=3] [ref=e1660]: + - text: Copilot commented + - generic "Jun 3, 2026, 5:19 PM EDT" [ref=e1661]: on Jun 3, 2026 13 hours ago + - img "Copilot" [ref=e1664] + - generic [ref=e1665]: + - generic [ref=e1666]: + - generic [ref=e1667]: Copilot + - generic [ref=e1668]: AI + - link "on Jun 3, 202613 hours ago" [ref=e1671] [cursor=pointer]: + - /url: https://github.com/CodeStorm-Hub/petfolio/pull/17#discussion_r3351989517 + - generic "Jun 3, 2026, 5:19 PM EDT" [ref=e1672]: on Jun 3, 202613 hours ago + - generic [ref=e1674]: + - list [ref=e1676]: + - listitem + - listitem [ref=e1677]: + - generic [ref=e1678]: Medium + - button "Actions for Copilot's comment, 5:19 PM yesterday" [ref=e1680] [cursor=pointer]: + - img [ref=e1681] + - generic [ref=e1684]: + - paragraph [ref=e1687]: + - text: This looks like an interactive local + - code [ref=e1688]: flutter run + - text: console log. Keeping raw run logs in the repository tends to create noise and quickly goes stale; consider moving it to a PR comment/issue or deleting it before merge. + - generic [ref=e1689]: + - toolbar "Reactions" + - generic [ref=e1691]: + - button "Positive feedback" [ref=e1692] [cursor=pointer]: + - img [ref=e1693] + - button "Negative feedback" [ref=e1695] [cursor=pointer]: + - img [ref=e1696] + - generic [ref=e1698]: Copilot uses AI. Check for mistakes. + - generic [ref=e1700]: + - generic [ref=e1701]: + - button "Comment thread" [expanded] [ref=e1702] [cursor=pointer]: + - img + - link "exception-log.md" [ref=e1704] [cursor=pointer]: + - /url: /CodeStorm-Hub/petfolio/pull/17/files/7b8aaf186f6047b802df726bad66459f0cfe10ec#diff-afa976bbd97891e8d87f9b3fdff4b123a691415338224c42c94cccd6780919dc + - generic [ref=e1705]: + - generic [ref=e1707]: Comment on lines +1 to +15 + - table [ref=e1710]: + - rowgroup [ref=e1711]: + - 'row "1 + D/WindowOnBackDispatcher( 8540): setTopOnBackInvokedCallback (unwrapped): androidx.navigationevent.OnBackInvokedInput$createOnBackAnimationCallback$1@94b726b" [ref=e1712]': + - cell [ref=e1713] [cursor=pointer] + - cell "1" [ref=e1714] [cursor=pointer] + - 'cell "+ D/WindowOnBackDispatcher( 8540): setTopOnBackInvokedCallback (unwrapped): androidx.navigationevent.OnBackInvokedInput$createOnBackAnimationCallback$1@94b726b" [ref=e1715]': + - generic [ref=e1716]: "+ D/WindowOnBackDispatcher( 8540): setTopOnBackInvokedCallback (unwrapped): androidx.navigationevent.OnBackInvokedInput$createOnBackAnimationCallback$1@94b726b" + - 'row "2 + D/WindowOnBackDispatcher( 8540): setTopOnBackInvokedCallback (unwrapped): android.app.Activity$$ExternalSyntheticLambda0@671fe96" [ref=e1717]': + - cell [ref=e1718] [cursor=pointer] + - cell "2" [ref=e1719] [cursor=pointer] + - 'cell "+ D/WindowOnBackDispatcher( 8540): setTopOnBackInvokedCallback (unwrapped): android.app.Activity$$ExternalSyntheticLambda0@671fe96" [ref=e1720]': + - generic [ref=e1721]: "+ D/WindowOnBackDispatcher( 8540): setTopOnBackInvokedCallback (unwrapped): android.app.Activity$$ExternalSyntheticLambda0@671fe96" + - 'row "3 + D/WindowOnBackDispatcher( 8540): setTopOnBackInvokedCallback (unwrapped): androidx.navigationevent.OnBackInvokedInput$createOnBackAnimationCallback$1@94b726b" [ref=e1722]': + - cell [ref=e1723] [cursor=pointer] + - cell "3" [ref=e1724] [cursor=pointer] + - 'cell "+ D/WindowOnBackDispatcher( 8540): setTopOnBackInvokedCallback (unwrapped): androidx.navigationevent.OnBackInvokedInput$createOnBackAnimationCallback$1@94b726b" [ref=e1725]': + - generic [ref=e1726]: "+ D/WindowOnBackDispatcher( 8540): setTopOnBackInvokedCallback (unwrapped): androidx.navigationevent.OnBackInvokedInput$createOnBackAnimationCallback$1@94b726b" + - 'row "4 + D/WindowOnBackDispatcher( 8540): setTopOnBackInvokedCallback (unwrapped): android.app.Activity$$ExternalSyntheticLambda0@671fe96" [ref=e1727]': + - cell [ref=e1728] [cursor=pointer] + - cell "4" [ref=e1729] [cursor=pointer] + - 'cell "+ D/WindowOnBackDispatcher( 8540): setTopOnBackInvokedCallback (unwrapped): android.app.Activity$$ExternalSyntheticLambda0@671fe96" [ref=e1730]': + - generic [ref=e1731]: "+ D/WindowOnBackDispatcher( 8540): setTopOnBackInvokedCallback (unwrapped): android.app.Activity$$ExternalSyntheticLambda0@671fe96" + - 'row "5 + D/WindowOnBackDispatcher( 8540): setTopOnBackInvokedCallback (unwrapped): androidx.navigationevent.OnBackInvokedInput$createOnBackAnimationCallback$1@94b726b" [ref=e1732]': + - cell [ref=e1733] [cursor=pointer] + - cell "5" [ref=e1734] [cursor=pointer] + - 'cell "+ D/WindowOnBackDispatcher( 8540): setTopOnBackInvokedCallback (unwrapped): androidx.navigationevent.OnBackInvokedInput$createOnBackAnimationCallback$1@94b726b" [ref=e1735]': + - generic [ref=e1736]: "+ D/WindowOnBackDispatcher( 8540): setTopOnBackInvokedCallback (unwrapped): androidx.navigationevent.OnBackInvokedInput$createOnBackAnimationCallback$1@94b726b" + - row "6 +" [ref=e1737]: + - cell [ref=e1738] [cursor=pointer] + - cell "6" [ref=e1739] [cursor=pointer] + - cell "+" [ref=e1740]: + - generic: + + - row "7 + ══╡ EXCEPTION CAUGHT BY FLUTTER FRAMEWORK ╞═════════════════════════════════════════════════════════" [ref=e1741]: + - cell [ref=e1742] [cursor=pointer] + - cell "7" [ref=e1743] [cursor=pointer] + - cell "+ ══╡ EXCEPTION CAUGHT BY FLUTTER FRAMEWORK ╞═════════════════════════════════════════════════════════" [ref=e1744]: + - generic [ref=e1745]: + ══╡ EXCEPTION CAUGHT BY FLUTTER FRAMEWORK ╞═════════════════════════════════════════════════════════ + - row "8 + The following assertion was thrown:" [ref=e1746]: + - cell [ref=e1747] [cursor=pointer] + - cell "8" [ref=e1748] [cursor=pointer] + - cell "+ The following assertion was thrown:" [ref=e1749]: + - generic [ref=e1750]: "+ The following assertion was thrown:" + - row "9 + ListTile background color or ink splashes may be invisible." [ref=e1751]: + - cell [ref=e1752] [cursor=pointer] + - cell "9" [ref=e1753] [cursor=pointer] + - cell "+ ListTile background color or ink splashes may be invisible." [ref=e1754]: + - generic [ref=e1755]: + ListTile background color or ink splashes may be invisible. + - row "10 + The ListTile is wrapped in a DecoratedBox that has a background color. Because ListTile paints its" [ref=e1756]: + - cell [ref=e1757] [cursor=pointer] + - cell "10" [ref=e1758] [cursor=pointer] + - cell "+ The ListTile is wrapped in a DecoratedBox that has a background color. Because ListTile paints its" [ref=e1759]: + - generic [ref=e1760]: + The ListTile is wrapped in a DecoratedBox that has a background color. Because ListTile paints its + - row "11 + background and ink splashes on the nearest Material ancestor, this DecoratedBox will hide those" [ref=e1761]: + - cell [ref=e1762] [cursor=pointer] + - cell "11" [ref=e1763] [cursor=pointer] + - cell "+ background and ink splashes on the nearest Material ancestor, this DecoratedBox will hide those" [ref=e1764]: + - generic [ref=e1765]: + background and ink splashes on the nearest Material ancestor, this DecoratedBox will hide those + - row "12 + effects." [ref=e1766]: + - cell [ref=e1767] [cursor=pointer] + - cell "12" [ref=e1768] [cursor=pointer] + - cell "+ effects." [ref=e1769]: + - generic [ref=e1770]: + effects. + - row "13 + To fix this, wrap the ListTile in its own Material widget, or remove the background color from the" [ref=e1771]: + - cell [ref=e1772] [cursor=pointer] + - cell "13" [ref=e1773] [cursor=pointer] + - cell "+ To fix this, wrap the ListTile in its own Material widget, or remove the background color from the" [ref=e1774]: + - generic [ref=e1775]: + To fix this, wrap the ListTile in its own Material widget, or remove the background color from the + - row "14 + intermediate DecoratedBox." [ref=e1776]: + - cell [ref=e1777] [cursor=pointer] + - cell "14" [ref=e1778] [cursor=pointer] + - cell "+ intermediate DecoratedBox." [ref=e1779]: + - generic [ref=e1780]: + intermediate DecoratedBox. + - row "15 +" [ref=e1781]: + - cell [ref=e1782] [cursor=pointer] + - cell "15" [ref=e1783] [cursor=pointer] + - cell "+" [ref=e1784]: + - generic: + + - generic [ref=e1791]: + - generic [ref=e1794]: + - heading "Copilot commented on Jun 3, 2026 13 hours ago" [level=3] [ref=e1795]: + - text: Copilot commented + - generic "Jun 3, 2026, 5:19 PM EDT" [ref=e1796]: on Jun 3, 2026 13 hours ago + - img "Copilot" [ref=e1799] + - generic [ref=e1800]: + - generic [ref=e1801]: + - generic [ref=e1802]: Copilot + - generic [ref=e1803]: AI + - link "on Jun 3, 202613 hours ago" [ref=e1806] [cursor=pointer]: + - /url: https://github.com/CodeStorm-Hub/petfolio/pull/17#discussion_r3351989537 + - generic "Jun 3, 2026, 5:19 PM EDT" [ref=e1807]: on Jun 3, 202613 hours ago + - generic [ref=e1809]: + - list [ref=e1811]: + - listitem + - listitem [ref=e1812]: + - generic [ref=e1813]: Medium + - button "Actions for Copilot's comment, 5:19 PM yesterday" [ref=e1815] [cursor=pointer]: + - img [ref=e1816] + - generic [ref=e1819]: + - paragraph [ref=e1822]: This is a captured runtime exception log. Committing transient logs makes the repo noisy and is hard to keep up to date; prefer filing it as an issue (with repro steps) or removing it before merge. + - generic [ref=e1823]: + - toolbar "Reactions" + - generic [ref=e1825]: + - button "Positive feedback" [ref=e1826] [cursor=pointer]: + - img [ref=e1827] + - button "Negative feedback" [ref=e1829] [cursor=pointer]: + - img [ref=e1830] + - generic [ref=e1832]: Copilot uses AI. Check for mistakes. + - generic [ref=e1834]: + - generic [ref=e1835]: + - button "Comment thread" [expanded] [ref=e1836] [cursor=pointer]: + - img + - link ".claude/settings.local.json" [ref=e1838] [cursor=pointer]: + - /url: /CodeStorm-Hub/petfolio/pull/17/files/7b8aaf186f6047b802df726bad66459f0cfe10ec#diff-fca16cae5b0e32edfa6b55eaa32a98ffbf4a0c7d885fb585785fc83b6ea2d9c3 + - generic [ref=e1839]: + - generic [ref=e1841]: Comment on lines +96 to +107 + - table [ref=e1844]: + - rowgroup [ref=e1845]: + - row "96 + \"Bash(git rm *)\"," [ref=e1846]: + - cell [ref=e1847] [cursor=pointer] + - cell "96" [ref=e1848] [cursor=pointer] + - cell "+ \"Bash(git rm *)\"," [ref=e1849]: + - generic [ref=e1850]: + "Bash(git rm *)", + - row "97 + \"Bash(dir \\\"G:\\\\\\\\GitHub\\\\\\\\petfolio\\\\\\\\PetFolio Redesign\\\\\\\\Care Redesign\\\" /b)\"," [ref=e1851]: + - cell [ref=e1852] [cursor=pointer] + - cell "97" [ref=e1853] [cursor=pointer] + - cell "+ \"Bash(dir \\\"G:\\\\\\\\GitHub\\\\\\\\petfolio\\\\\\\\PetFolio Redesign\\\\\\\\Care Redesign\\\" /b)\"," [ref=e1854]: + - generic [ref=e1855]: + "Bash(dir \"G:\\\\GitHub\\\\petfolio\\\\PetFolio Redesign\\\\Care Redesign\" /b)", + - row "98 + \"mcp__292a7621-3089-4236-bd52-07a54bf59881__list_tables\"," [ref=e1856]: + - cell [ref=e1857] [cursor=pointer] + - cell "98" [ref=e1858] [cursor=pointer] + - cell "+ \"mcp__292a7621-3089-4236-bd52-07a54bf59881__list_tables\"," [ref=e1859]: + - generic [ref=e1860]: + "mcp__292a7621-3089-4236-bd52-07a54bf59881__list_tables", + - row "99 + \"mcp__mobile-mcp__mobile_list_available_devices\"," [ref=e1861]: + - cell [ref=e1862] [cursor=pointer] + - cell "99" [ref=e1863] [cursor=pointer] + - cell "+ \"mcp__mobile-mcp__mobile_list_available_devices\"," [ref=e1864]: + - generic [ref=e1865]: + "mcp__mobile-mcp__mobile_list_available_devices", + - row "100 + \"mcp__mobile-mcp__mobile_take_screenshot\"," [ref=e1866]: + - cell [ref=e1867] [cursor=pointer] + - cell "100" [ref=e1868] [cursor=pointer] + - cell "+ \"mcp__mobile-mcp__mobile_take_screenshot\"," [ref=e1869]: + - generic [ref=e1870]: + "mcp__mobile-mcp__mobile_take_screenshot", + - row "101 + \"mcp__mobile-mcp__mobile_click_on_screen_at_coordinates\"," [ref=e1871]: + - cell [ref=e1872] [cursor=pointer] + - cell "101" [ref=e1873] [cursor=pointer] + - cell "+ \"mcp__mobile-mcp__mobile_click_on_screen_at_coordinates\"," [ref=e1874]: + - generic [ref=e1875]: + "mcp__mobile-mcp__mobile_click_on_screen_at_coordinates", + - row "102 + \"mcp__mobile-mcp__mobile_list_elements_on_screen\"," [ref=e1876]: + - cell [ref=e1877] [cursor=pointer] + - cell "102" [ref=e1878] [cursor=pointer] + - cell "+ \"mcp__mobile-mcp__mobile_list_elements_on_screen\"," [ref=e1879]: + - generic [ref=e1880]: + "mcp__mobile-mcp__mobile_list_elements_on_screen", + - row "103 + \"mcp__mobile-mcp__mobile_save_screenshot\"," [ref=e1881]: + - cell [ref=e1882] [cursor=pointer] + - cell "103" [ref=e1883] [cursor=pointer] + - cell "+ \"mcp__mobile-mcp__mobile_save_screenshot\"," [ref=e1884]: + - generic [ref=e1885]: + "mcp__mobile-mcp__mobile_save_screenshot", + - row "104 + \"mcp__mobile-mcp__mobile_swipe_on_screen\"," [ref=e1886]: + - cell [ref=e1887] [cursor=pointer] + - cell "104" [ref=e1888] [cursor=pointer] + - cell "+ \"mcp__mobile-mcp__mobile_swipe_on_screen\"," [ref=e1889]: + - generic [ref=e1890]: + "mcp__mobile-mcp__mobile_swipe_on_screen", + - row "105 + \"Bash(grep -E \\\"\\\\\\\\.\\\\(dart\\\\)$\\\")\"," [ref=e1891]: + - cell [ref=e1892] [cursor=pointer] + - cell "105" [ref=e1893] [cursor=pointer] + - cell "+ \"Bash(grep -E \\\"\\\\\\\\.\\\\(dart\\\\)$\\\")\"," [ref=e1894]: + - generic [ref=e1895]: + "Bash(grep -E \"\\\\.\\(dart\\)$\")", + - row "106 + \"Bash(Get-Content \\\"C:\\\\\\\\Users\\\\\\\\syedr\\\\\\\\AppData\\\\\\\\Local\\\\\\\\Temp\\\\\\\\claude\\\\\\\\G--GitHub-petfolio\\\\\\\\83d1e98f-3a1c-41fb-a000-787a9c3f0b74\\\\\\\\tasks\\\\\\\\bn6o3o4cc.output\\\" -Wait -Tail 30)\"," [ref=e1896]: + - cell [ref=e1897] [cursor=pointer] + - cell "106" [ref=e1898] [cursor=pointer] + - cell "+ \"Bash(Get-Content \\\"C:\\\\\\\\Users\\\\\\\\syedr\\\\\\\\AppData\\\\\\\\Local\\\\\\\\Temp\\\\\\\\claude\\\\\\\\G--GitHub-petfolio\\\\\\\\83d1e98f-3a1c-41fb-a000-787a9c3f0b74\\\\\\\\tasks\\\\\\\\bn6o3o4cc.output\\\" -Wait -Tail 30)\"," [ref=e1899]: + - generic [ref=e1900]: + "Bash(Get-Content \"C:\\\\Users\\\\syedr\\\\AppData\\\\Local\\\\Temp\\\\claude\\\\G--GitHub-petfolio\\\\83d1e98f-3a1c-41fb-a000-787a9c3f0b74\\\\tasks\\\\bn6o3o4cc.output\" -Wait -Tail 30)", + - row "107 + \"Bash(Select-Object -First 30)\"," [ref=e1901]: + - cell [ref=e1902] [cursor=pointer] + - cell "107" [ref=e1903] [cursor=pointer] + - cell "+ \"Bash(Select-Object -First 30)\"," [ref=e1904]: + - generic [ref=e1905]: + "Bash(Select-Object -First 30)", + - generic [ref=e1912]: + - generic [ref=e1915]: + - heading "Copilot commented on Jun 3, 2026 13 hours ago" [level=3] [ref=e1916]: + - text: Copilot commented + - generic "Jun 3, 2026, 5:19 PM EDT" [ref=e1917]: on Jun 3, 2026 13 hours ago + - img "Copilot" [ref=e1920] + - generic [ref=e1921]: + - generic [ref=e1922]: + - generic [ref=e1923]: Copilot + - generic [ref=e1924]: AI + - link "on Jun 3, 202613 hours ago" [ref=e1927] [cursor=pointer]: + - /url: https://github.com/CodeStorm-Hub/petfolio/pull/17#discussion_r3351989556 + - generic "Jun 3, 2026, 5:19 PM EDT" [ref=e1928]: on Jun 3, 202613 hours ago + - generic [ref=e1930]: + - list [ref=e1932]: + - listitem + - listitem [ref=e1933]: + - generic [ref=e1934]: High + - button "Actions for Copilot's comment, 5:19 PM yesterday" [ref=e1936] [cursor=pointer]: + - img [ref=e1937] + - generic [ref=e1940]: + - paragraph [ref=e1943]: + - code [ref=e1944]: .claude/settings.local.json + - text: contains machine-specific allowlists (Windows drive letters, user temp paths, local MCP server IDs). These are not portable and can accidentally whitelist dangerous commands; typically + - code [ref=e1945]: "*.local.json" + - text: files should be untracked and ignored (especially since this PR also adds secrets in + - code [ref=e1946]: .claude/launch.json + - text: ). + - generic [ref=e1947]: + - toolbar "Reactions" + - generic [ref=e1949]: + - button "Positive feedback" [ref=e1950] [cursor=pointer]: + - img [ref=e1951] + - button "Negative feedback" [ref=e1953] [cursor=pointer]: + - img [ref=e1954] + - generic [ref=e1956]: Copilot uses AI. Check for mistakes. + - generic [ref=e1958]: + - generic [ref=e1959]: + - button "Comment thread" [expanded] [ref=e1960] [cursor=pointer]: + - img + - link "web/pwa_banner.js" [ref=e1962] [cursor=pointer]: + - /url: /CodeStorm-Hub/petfolio/pull/17/files/7b8aaf186f6047b802df726bad66459f0cfe10ec#diff-f3b25d0a5691b8d245bc98e0a2e57ae1bd7e9d733905d29562165a353833df3d + - generic [ref=e1963]: + - generic [ref=e1965]: Comment on lines +64 to +67 + - table [ref=e1968]: + - rowgroup [ref=e1969]: + - 'row "64 + document.getElementById(''pwa-banner-close'').addEventListener(''click'', function () {" [ref=e1970]': + - cell [ref=e1971] [cursor=pointer] + - cell "64" [ref=e1972] [cursor=pointer] + - 'cell "+ document.getElementById(''pwa-banner-close'').addEventListener(''click'', function () {" [ref=e1973]': + - generic [ref=e1974]: "+ document.getElementById('pwa-banner-close').addEventListener('click', function () {" + - row "65 + localStorage.setItem('pwa_banner_dismissed', '1');" [ref=e1975]: + - cell [ref=e1976] [cursor=pointer] + - cell "65" [ref=e1977] [cursor=pointer] + - cell "+ localStorage.setItem('pwa_banner_dismissed', '1');" [ref=e1978]: + - generic [ref=e1979]: + localStorage.setItem('pwa_banner_dismissed', '1'); + - row "66 + banner.style.animation = 'slideUp .25s cubic-bezier(.16,1,.3,1) reverse both';" [ref=e1980]: + - cell [ref=e1981] [cursor=pointer] + - cell "66" [ref=e1982] [cursor=pointer] + - cell "+ banner.style.animation = 'slideUp .25s cubic-bezier(.16,1,.3,1) reverse both';" [ref=e1983]: + - generic [ref=e1984]: + banner.style.animation = 'slideUp .25s cubic-bezier(.16,1,.3,1) reverse both'; + - 'row "67 + setTimeout(function () { banner.remove(); }, 280);" [ref=e1985]': + - cell [ref=e1986] [cursor=pointer] + - cell "67" [ref=e1987] [cursor=pointer] + - 'cell "+ setTimeout(function () { banner.remove(); }, 280);" [ref=e1988]': + - generic [ref=e1989]: "+ setTimeout(function () { banner.remove(); }, 280);" + - generic [ref=e1996]: + - generic [ref=e1999]: + - heading "Copilot commented on Jun 3, 2026 13 hours ago" [level=3] [ref=e2000]: + - text: Copilot commented + - generic "Jun 3, 2026, 5:19 PM EDT" [ref=e2001]: on Jun 3, 2026 13 hours ago + - img "Copilot" [ref=e2004] + - generic [ref=e2005]: + - generic [ref=e2006]: + - generic [ref=e2007]: Copilot + - generic [ref=e2008]: AI + - link "on Jun 3, 202613 hours ago" [ref=e2011] [cursor=pointer]: + - /url: https://github.com/CodeStorm-Hub/petfolio/pull/17#discussion_r3351989577 + - generic "Jun 3, 2026, 5:19 PM EDT" [ref=e2012]: on Jun 3, 202613 hours ago + - generic [ref=e2014]: + - list [ref=e2016]: + - listitem + - listitem [ref=e2017]: + - generic [ref=e2018]: Medium + - button "Actions for Copilot's comment, 5:19 PM yesterday" [ref=e2020] [cursor=pointer]: + - img [ref=e2021] + - generic [ref=e2024]: + - paragraph [ref=e2027]: + - code [ref=e2028]: localStorage.setItem(...) + - text: can throw (e.g. iOS Safari private mode / storage disabled). If it throws here, the banner won't dismiss and will keep reappearing. Wrap the write in a try/catch so dismissal still works even when storage is unavailable. + - generic [ref=e2029]: + - generic [ref=e2031]: + - text: Suggested changeset + - generic [ref=e2032]: "1" + - generic [ref=e2033]: (1) + - generic [ref=e2034]: + - generic [ref=e2035]: + - button "Close review comment" [ref=e2036] [cursor=pointer]: + - img [ref=e2037] + - text: web/pwa_banner.js + - table [ref=e2040]: + - rowgroup [ref=e2041]: + - row "Original file line number Diff line number Diff line change" [ref=e2042]: + - columnheader "Original file line number" [ref=e2043] + - columnheader "Diff line number" [ref=e2044] + - columnheader "Diff line change" [ref=e2045] + - rowgroup [ref=e2050]: + - row "@@ -62,8 +62,9 @@" [ref=e2051]: + - cell "@@ -62,8 +62,9 @@" [ref=e2052]: + - generic [ref=e2053]: + - img [ref=e2055] + - code [ref=e2057]: + - generic [ref=e2058]: "@@ -62,8 +62,9 @@" + - row "62 62 document.body.appendChild(banner);" [ref=e2059]: + - cell "62" [ref=e2060]: + - code [ref=e2061]: "62" + - cell "62" [ref=e2062]: + - code [ref=e2063]: "62" + - cell "document.body.appendChild(banner);" [ref=e2064]: + - code [ref=e2065]: + - generic [ref=e2066]: document.body.appendChild(banner); + - row "63 63" [ref=e2067]: + - cell "63" [ref=e2068]: + - code [ref=e2069]: "63" + - cell "63" [ref=e2070]: + - code [ref=e2071]: "63" + - cell [ref=e2072]: + - code + - 'row "64 64 document.getElementById(''pwa-banner-close'').addEventListener(''click'', function () {" [ref=e2073]': + - cell "64" [ref=e2074]: + - code [ref=e2075]: "64" + - cell "64" [ref=e2076]: + - code [ref=e2077]: "64" + - 'cell "document.getElementById(''pwa-banner-close'').addEventListener(''click'', function () {" [ref=e2078]': + - code [ref=e2079]: + - generic [ref=e2080]: "document.getElementById('pwa-banner-close').addEventListener('click', function () {" + - row "65 - localStorage.setItem('pwa_banner_dismissed', '1');" [ref=e2081]: + - cell "65" [ref=e2082]: + - code [ref=e2083]: "65" + - cell [ref=e2084]: + - code + - cell "- localStorage.setItem('pwa_banner_dismissed', '1');" [ref=e2085]: + - code [ref=e2086]: + - generic [ref=e2087]: "-" + - generic [ref=e2088]: localStorage.setItem('pwa_banner_dismissed', '1'); + - 'row "65 + try { localStorage.setItem(''pwa_banner_dismissed'', ''1''); } catch (_) {}" [ref=e2089]': + - cell [ref=e2090]: + - code + - cell "65" [ref=e2091]: + - code [ref=e2092]: "65" + - 'cell "+ try { localStorage.setItem(''pwa_banner_dismissed'', ''1''); } catch (_) {}" [ref=e2093]': + - code [ref=e2094]: + - generic [ref=e2095]: + + - generic [ref=e2096]: "try { localStorage.setItem('pwa_banner_dismissed', '1'); } catch (_) {}" + - row "66 66 banner.style.animation = 'slideUp .25s cubic-bezier(.16,1,.3,1) reverse both';" [ref=e2097]: + - cell "66" [ref=e2098]: + - code [ref=e2099]: "66" + - cell "66" [ref=e2100]: + - code [ref=e2101]: "66" + - cell "banner.style.animation = 'slideUp .25s cubic-bezier(.16,1,.3,1) reverse both';" [ref=e2102]: + - code [ref=e2103]: + - generic [ref=e2104]: banner.style.animation = 'slideUp .25s cubic-bezier(.16,1,.3,1) reverse both'; + - 'row "67 67 setTimeout(function () { banner.remove(); }, 280);" [ref=e2105]': + - cell "67" [ref=e2106]: + - code [ref=e2107]: "67" + - cell "67" [ref=e2108]: + - code [ref=e2109]: "67" + - 'cell "setTimeout(function () { banner.remove(); }, 280);" [ref=e2110]': + - code [ref=e2111]: + - generic [ref=e2112]: "setTimeout(function () { banner.remove(); }, 280);" + - 'row "68 68 });" [ref=e2113]': + - cell "68" [ref=e2114]: + - code [ref=e2115]: "68" + - cell "68" [ref=e2116]: + - code [ref=e2117]: "68" + - 'cell "});" [ref=e2118]': + - code [ref=e2119]: + - generic [ref=e2120]: "});" + - 'row "69 + });" [ref=e2121]': + - cell [ref=e2122]: + - code + - cell "69" [ref=e2123]: + - code [ref=e2124]: "69" + - 'cell "+ });" [ref=e2125]': + - code [ref=e2126]: + - generic [ref=e2127]: + + - generic [ref=e2128]: "});" + - 'row "69 70 }());" [ref=e2129]': + - cell "69" [ref=e2130]: + - code [ref=e2131]: "69" + - cell "70" [ref=e2132]: + - code [ref=e2133]: "70" + - 'cell "}());" [ref=e2134]': + - code [ref=e2135]: + - generic [ref=e2136]: "}());" + - button "Commit suggestion" [ref=e2138] [cursor=pointer]: + - generic [ref=e2140]: Commit suggestion + - generic [ref=e2141]: + - toolbar "Reactions" + - generic [ref=e2143]: + - button "Positive feedback" [ref=e2144] [cursor=pointer]: + - img [ref=e2145] + - button "Negative feedback" [ref=e2147] [cursor=pointer]: + - img [ref=e2148] + - generic [ref=e2150]: Copilot uses AI. Check for mistakes. + - generic [ref=e2152]: + - generic [ref=e2153]: + - button "Comment thread" [expanded] [ref=e2154] [cursor=pointer]: + - img + - link ".github/workflows/deploy-web.yml" [ref=e2156] [cursor=pointer]: + - /url: /CodeStorm-Hub/petfolio/pull/17/files/7b8aaf186f6047b802df726bad66459f0cfe10ec#diff-875a32a9bd9123a01ecb0b1802809c712a8a04f695e979c262eb9380e6359efc + - generic [ref=e2157]: + - generic [ref=e2159]: Comment on lines +35 to +40 + - table [ref=e2162]: + - rowgroup [ref=e2163]: + - 'row "35 + - name: Deploy to Vercel" [ref=e2164]': + - cell [ref=e2165] [cursor=pointer] + - cell "35" [ref=e2166] [cursor=pointer] + - 'cell "+ - name: Deploy to Vercel" [ref=e2167]': + - generic [ref=e2168]: "+ - name: Deploy to Vercel" + - 'row "36 + working-directory: build/web" [ref=e2169]': + - cell [ref=e2170] [cursor=pointer] + - cell "36" [ref=e2171] [cursor=pointer] + - 'cell "+ working-directory: build/web" [ref=e2172]': + - generic [ref=e2173]: "+ working-directory: build/web" + - 'row "37 + run: npx vercel deploy --prod --yes --token=${{ secrets.VERCEL_TOKEN }}" [ref=e2174]': + - cell [ref=e2175] [cursor=pointer] + - cell "37" [ref=e2176] [cursor=pointer] + - 'cell "+ run: npx vercel deploy --prod --yes --token=${{ secrets.VERCEL_TOKEN }}" [ref=e2177]': + - generic [ref=e2178]: "+ run: npx vercel deploy --prod --yes --token=${{ secrets.VERCEL_TOKEN }}" + - row "38 + env:" [ref=e2179]: + - cell [ref=e2180] [cursor=pointer] + - cell "38" [ref=e2181] [cursor=pointer] + - cell "+ env:" [ref=e2182]: + - generic [ref=e2183]: "+ env:" + - 'row "39 + VERCEL_ORG_ID: team_lC8aTJK0XiU9qDfaHeTfCJs6" [ref=e2184]': + - cell [ref=e2185] [cursor=pointer] + - cell "39" [ref=e2186] [cursor=pointer] + - 'cell "+ VERCEL_ORG_ID: team_lC8aTJK0XiU9qDfaHeTfCJs6" [ref=e2187]': + - generic [ref=e2188]: "+ VERCEL_ORG_ID: team_lC8aTJK0XiU9qDfaHeTfCJs6" + - 'row "40 + VERCEL_PROJECT_ID: prj_hMHouLWimZvr5dDOlZeAhbH8xtop" [ref=e2189]': + - cell [ref=e2190] [cursor=pointer] + - cell "40" [ref=e2191] [cursor=pointer] + - 'cell "+ VERCEL_PROJECT_ID: prj_hMHouLWimZvr5dDOlZeAhbH8xtop" [ref=e2192]': + - generic [ref=e2193]: "+ VERCEL_PROJECT_ID: prj_hMHouLWimZvr5dDOlZeAhbH8xtop" + - generic [ref=e2200]: + - generic [ref=e2203]: + - heading "Copilot commented on Jun 3, 2026 13 hours ago" [level=3] [ref=e2204]: + - text: Copilot commented + - generic "Jun 3, 2026, 5:19 PM EDT" [ref=e2205]: on Jun 3, 2026 13 hours ago + - img "Copilot" [ref=e2208] + - generic [ref=e2209]: + - generic [ref=e2210]: + - generic [ref=e2211]: Copilot + - generic [ref=e2212]: AI + - link "on Jun 3, 202613 hours ago" [ref=e2215] [cursor=pointer]: + - /url: https://github.com/CodeStorm-Hub/petfolio/pull/17#discussion_r3351989597 + - generic "Jun 3, 2026, 5:19 PM EDT" [ref=e2216]: on Jun 3, 202613 hours ago + - generic [ref=e2218]: + - list [ref=e2220]: + - listitem + - listitem [ref=e2221]: + - generic [ref=e2222]: High + - button "Actions for Copilot's comment, 5:19 PM yesterday" [ref=e2224] [cursor=pointer]: + - img [ref=e2225] + - generic [ref=e2228]: + - paragraph [ref=e2231]: + - text: The deploy step currently (1) runs with + - code [ref=e2232]: "--prod" + - text: even on + - code [ref=e2233]: pull_request + - text: events, which would deploy PR builds to production, and (2) uses + - code [ref=e2234]: "working-directory: build/web" + - text: ", which prevents Vercel from discovering the root" + - code [ref=e2235]: vercel.json + - text: (rewrites/headers) and + - code [ref=e2236]: .vercel/project.json + - text: . Gate the prod deploy to pushes and run from the repo root so config is applied. + - generic [ref=e2237]: + - generic [ref=e2239]: + - text: Suggested changeset + - generic [ref=e2240]: "1" + - generic [ref=e2241]: (1) + - generic [ref=e2242]: + - generic [ref=e2243]: + - button "Close review comment" [ref=e2244] [cursor=pointer]: + - img [ref=e2245] + - text: .github/workflows/deploy-web.yml + - table [ref=e2248]: + - rowgroup [ref=e2249]: + - row "Original file line number Diff line number Diff line change" [ref=e2250]: + - columnheader "Original file line number" [ref=e2251] + - columnheader "Diff line number" [ref=e2252] + - columnheader "Diff line change" [ref=e2253] + - rowgroup [ref=e2258]: + - row "@@ -33,7 +33,7 @@" [ref=e2259]: + - cell "@@ -33,7 +33,7 @@" [ref=e2260]: + - generic [ref=e2261]: + - img [ref=e2263] + - code [ref=e2265]: + - generic [ref=e2266]: "@@ -33,7 +33,7 @@" + - 'row "33 33 --dart-define=NVIDIA_API_KEY=${{ secrets.NVIDIA_API_KEY }}" [ref=e2267]': + - cell "33" [ref=e2268]: + - code [ref=e2269]: "33" + - cell "33" [ref=e2270]: + - code [ref=e2271]: "33" + - 'cell "--dart-define=NVIDIA_API_KEY=${{ secrets.NVIDIA_API_KEY }}" [ref=e2272]': + - code [ref=e2273]: + - generic [ref=e2274]: "--dart-define=NVIDIA_API_KEY=${{ secrets.NVIDIA_API_KEY }}" + - row "34 34" [ref=e2275]: + - cell "34" [ref=e2276]: + - code [ref=e2277]: "34" + - cell "34" [ref=e2278]: + - code [ref=e2279]: "34" + - cell [ref=e2280]: + - code + - 'row "35 35 - name: Deploy to Vercel" [ref=e2281]': + - cell "35" [ref=e2282]: + - code [ref=e2283]: "35" + - cell "35" [ref=e2284]: + - code [ref=e2285]: "35" + - 'cell "- name: Deploy to Vercel" [ref=e2286]': + - code [ref=e2287]: + - generic [ref=e2288]: "- name: Deploy to Vercel" + - 'row "36 - working-directory: build/web" [ref=e2289]': + - cell "36" [ref=e2290]: + - code [ref=e2291]: "36" + - cell [ref=e2292]: + - code + - 'cell "- working-directory: build/web" [ref=e2293]': + - code [ref=e2294]: + - generic [ref=e2295]: "-" + - generic [ref=e2296]: "working-directory: build/web" + - 'row "36 + if: github.event_name == ''push''" [ref=e2297]': + - cell [ref=e2298]: + - code + - cell "36" [ref=e2299]: + - code [ref=e2300]: "36" + - 'cell "+ if: github.event_name == ''push''" [ref=e2301]': + - code [ref=e2302]: + - generic [ref=e2303]: + + - generic [ref=e2304]: "if: github.event_name == 'push'" + - 'row "37 37 run: npx vercel deploy --prod --yes --token=${{ secrets.VERCEL_TOKEN }}" [ref=e2305]': + - cell "37" [ref=e2306]: + - code [ref=e2307]: "37" + - cell "37" [ref=e2308]: + - code [ref=e2309]: "37" + - 'cell "run: npx vercel deploy --prod --yes --token=${{ secrets.VERCEL_TOKEN }}" [ref=e2310]': + - code [ref=e2311]: + - generic [ref=e2312]: "run: npx vercel deploy --prod --yes --token=${{ secrets.VERCEL_TOKEN }}" + - row "38 38 env:" [ref=e2313]: + - cell "38" [ref=e2314]: + - code [ref=e2315]: "38" + - cell "38" [ref=e2316]: + - code [ref=e2317]: "38" + - cell "env:" [ref=e2318]: + - code [ref=e2319]: + - generic [ref=e2320]: "env:" + - 'row "39 39 VERCEL_ORG_ID: team_lC8aTJK0XiU9qDfaHeTfCJs6" [ref=e2321]': + - cell "39" [ref=e2322]: + - code [ref=e2323]: "39" + - cell "39" [ref=e2324]: + - code [ref=e2325]: "39" + - 'cell "VERCEL_ORG_ID: team_lC8aTJK0XiU9qDfaHeTfCJs6" [ref=e2326]': + - code [ref=e2327]: + - generic [ref=e2328]: "VERCEL_ORG_ID: team_lC8aTJK0XiU9qDfaHeTfCJs6" + - button "Commit suggestion" [ref=e2330] [cursor=pointer]: + - generic [ref=e2332]: Commit suggestion + - generic [ref=e2333]: + - toolbar "Reactions" + - generic [ref=e2335]: + - button "Positive feedback" [ref=e2336] [cursor=pointer]: + - img [ref=e2337] + - button "Negative feedback" [ref=e2339] [cursor=pointer]: + - img [ref=e2340] + - generic [ref=e2342]: Copilot uses AI. Check for mistakes. + - generic [ref=e2346]: + - img [ref=e2348] + - generic [ref=e2353]: + - link "@syed-reza98" [ref=e2356] [cursor=pointer]: + - /url: /syed-reza98 + - img "@syed-reza98" [ref=e2357] + - code [ref=e2359]: + - link "up" [ref=e2360] [cursor=pointer]: + - /url: /CodeStorm-Hub/petfolio/pull/17/commits/610ee87712bfd0f063f722d6cadca2a5289430cb + - group [ref=e2364]: + - generic "2 / 2 checks OK" [ref=e2365] [cursor=pointer]: + - img "2 / 2 checks OK" [ref=e2366] + - code [ref=e2369]: + - link "610ee87" [ref=e2370] [cursor=pointer]: + - /url: /CodeStorm-Hub/petfolio/pull/17/commits/610ee87712bfd0f063f722d6cadca2a5289430cb + - generic [ref=e2371]: + - generic [ref=e2372]: + - img [ref=e2374] + - generic [ref=e2376]: + - link "@anamchowdhury" [ref=e2377] [cursor=pointer]: + - /url: /anamchowdhury + - img "@anamchowdhury" [ref=e2378] + - link "anamchowdhury" [ref=e2379] [cursor=pointer]: + - /url: /anamchowdhury + - text: requested a review from + - link "Copilot" [ref=e2380] [cursor=pointer]: + - /url: /apps/copilot-pull-request-reviewer + - link "June 4, 2026 10:3615 minutes ago" [ref=e2381] [cursor=pointer]: + - /url: "#event-26332843962" + - generic [ref=e2382]: + - img [ref=e2384] + - generic [ref=e2386]: + - strong [ref=e2387]: Copilot + - link "started reviewing" [ref=e2388] [cursor=pointer]: + - /url: https://github.com/CodeStorm-Hub/petfolio/sessions/94f48852-11d4-4e31-a674-b857e438d64a + - text: on behalf of + - link "anamchowdhury" [ref=e2389] [cursor=pointer]: + - /url: /anamchowdhury + - link "June 4, 2026 10:3615 minutes ago" [ref=e2390] [cursor=pointer]: + - /url: "#event-26332852181" + - link "View session 94f48852-11d4-4e31-a674-b857e438d64a" [ref=e2391] [cursor=pointer]: + - /url: https://github.com/CodeStorm-Hub/petfolio/sessions/94f48852-11d4-4e31-a674-b857e438d64a + - generic [ref=e2393]: View session + - generic [ref=e2397]: + - img [ref=e2399] + - generic [ref=e2404]: + - generic [ref=e2406]: + - link "@afsan123" [ref=e2407] [cursor=pointer]: + - /url: /afsan123 + - img "@afsan123" [ref=e2408] + - link "@claude" [ref=e2409] [cursor=pointer]: + - /url: /claude + - img "@claude" [ref=e2410] + - generic [ref=e2411]: + - code [ref=e2412]: + - link "Merge origin/main into care-redesign-salman" [ref=e2413] [cursor=pointer]: + - /url: /CodeStorm-Hub/petfolio/pull/17/commits/518b78658421e8e825bcb284e3724fef601c3054 + - button "Commit message body" [ref=e2415] [cursor=pointer]: … + - group [ref=e2419]: + - generic "2 / 2 checks OK" [ref=e2420] [cursor=pointer]: + - img "2 / 2 checks OK" [ref=e2421] + - code [ref=e2424]: + - link "518b786" [ref=e2425] [cursor=pointer]: + - /url: /CodeStorm-Hub/petfolio/pull/17/commits/518b78658421e8e825bcb284e3724fef601c3054 + - generic [ref=e2428]: + - generic [ref=e2429]: + - link "Copilot PR reviewer" [ref=e2430] [cursor=pointer]: + - /url: /apps/copilot-pull-request-reviewer + - img [ref=e2432] + - img "Only reviews by reviewers with write access count toward mergeability" [ref=e2436] + - generic [ref=e2438]: + - generic [ref=e2439]: + - strong [ref=e2440]: + - link "Copilot" [ref=e2441] [cursor=pointer]: + - /url: /apps/copilot-pull-request-reviewer + - generic [ref=e2442]: AI + - text: reviewed + - link "Jun 4, 20267 minutes ago" [ref=e2444] [cursor=pointer]: + - /url: "#pullrequestreview-4427104976" + - link "View reviewed changes" [ref=e2446] [cursor=pointer]: + - /url: /CodeStorm-Hub/petfolio/pull/17/files/610ee87712bfd0f063f722d6cadca2a5289430cb + - generic [ref=e2448]: View reviewed changes + - generic [ref=e2451]: + - generic [ref=e2452]: + - group [ref=e2455]: + - button "Show options" [ref=e2456] [cursor=pointer]: + - img "Show options" [ref=e2459] + - heading "Copilot AI left a comment" [level=3] [ref=e2461]: + - generic [ref=e2462]: + - strong [ref=e2463]: + - link "Copilot" [ref=e2464] [cursor=pointer]: + - /url: /apps/copilot-pull-request-reviewer + - generic [ref=e2465]: AI + - text: left a comment + - generic [ref=e2468]: + - heading "Pull request overview" [level=2] [ref=e2469] + - paragraph [ref=e2470]: Copilot reviewed 28 out of 44 changed files in this pull request and generated 18 comments. + - generic [ref=e2472]: + - generic [ref=e2474]: + - generic [ref=e2475]: + - button "Comment thread" [expanded] [ref=e2476] [cursor=pointer]: + - img + - link ".claude/launch.json" [ref=e2478] [cursor=pointer]: + - /url: /CodeStorm-Hub/petfolio/pull/17/files/610ee87712bfd0f063f722d6cadca2a5289430cb#diff-f02a361fa828af1bddd7c60baccb9ec7036882b427c5e6f2d734cf6414f3b36c + - generic [ref=e2479]: + - generic [ref=e2481]: Comment on lines +12 to +15 + - table [ref=e2484]: + - rowgroup [ref=e2485]: + - row "12 + \"--dart-define=SUPABASE_URL=https://jqyjvhwlcqcsuwcqgcwf.supabase.co\"," [ref=e2486]: + - cell [ref=e2487] [cursor=pointer] + - cell "12" [ref=e2488] [cursor=pointer] + - cell "+ \"--dart-define=SUPABASE_URL=https://jqyjvhwlcqcsuwcqgcwf.supabase.co\"," [ref=e2489]: + - generic [ref=e2490]: + "--dart-define=SUPABASE_URL=https://jqyjvhwlcqcsuwcqgcwf.supabase.co", + - row "13 + \"--dart-define=SUPABASE_ANON_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6ImpxeWp2aHdsY3Fjc3V3Y3FnY3dmIiwicm9sZSI6ImFub24iLCJpYXQiOjE3Nzg1MjI4MjgsImV4cCI6MjA5NDA5ODgyOH0.3bF68bNG0IwAc50YbOC3sem4k8O-d1vkvNNqBt1HbRw\"," [ref=e2491]: + - cell [ref=e2492] [cursor=pointer] + - cell "13" [ref=e2493] [cursor=pointer] + - cell "+ \"--dart-define=SUPABASE_ANON_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6ImpxeWp2aHdsY3Fjc3V3Y3FnY3dmIiwicm9sZSI6ImFub24iLCJpYXQiOjE3Nzg1MjI4MjgsImV4cCI6MjA5NDA5ODgyOH0.3bF68bNG0IwAc50YbOC3sem4k8O-d1vkvNNqBt1HbRw\"," [ref=e2494]: + - generic [ref=e2495]: + "--dart-define=SUPABASE_ANON_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6ImpxeWp2aHdsY3Fjc3V3Y3FnY3dmIiwicm9sZSI6ImFub24iLCJpYXQiOjE3Nzg1MjI4MjgsImV4cCI6MjA5NDA5ODgyOH0.3bF68bNG0IwAc50YbOC3sem4k8O-d1vkvNNqBt1HbRw", + - row "14 + \"--dart-define=STRIPE_PUBLISHABLE_KEY=pk_test_51TQvlrPcVRApxzIxJ8RmKYA1WEw7k8zubumbIfsDjRSGgDyAcSU22RhsZRtKIP1lAZ0wtGjpLfzjI4fozMZxGSlo006zuZrbon\"," [ref=e2496]: + - cell [ref=e2497] [cursor=pointer] + - cell "14" [ref=e2498] [cursor=pointer] + - cell "+ \"--dart-define=STRIPE_PUBLISHABLE_KEY=pk_test_51TQvlrPcVRApxzIxJ8RmKYA1WEw7k8zubumbIfsDjRSGgDyAcSU22RhsZRtKIP1lAZ0wtGjpLfzjI4fozMZxGSlo006zuZrbon\"," [ref=e2499]: + - generic [ref=e2500]: + "--dart-define=STRIPE_PUBLISHABLE_KEY=pk_test_51TQvlrPcVRApxzIxJ8RmKYA1WEw7k8zubumbIfsDjRSGgDyAcSU22RhsZRtKIP1lAZ0wtGjpLfzjI4fozMZxGSlo006zuZrbon", + - row "15 + \"--dart-define=NVIDIA_API_KEY=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\"" [ref=e2501]: + - cell [ref=e2502] [cursor=pointer] + - cell "15" [ref=e2503] [cursor=pointer] + - cell "+ \"--dart-define=NVIDIA_API_KEY=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\"" [ref=e2504]: + - generic [ref=e2505]: + "--dart-define=NVIDIA_API_KEY=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" + - generic [ref=e2512]: + - generic [ref=e2515]: + - heading "Copilot commented on Jun 4, 2026 7 minutes ago" [level=3] [ref=e2516]: + - text: Copilot commented + - generic "Jun 4, 2026, 6:44 AM EDT" [ref=e2517]: on Jun 4, 2026 7 minutes ago + - img "Copilot" [ref=e2520] + - generic [ref=e2521]: + - generic [ref=e2522]: + - generic [ref=e2523]: Copilot + - generic [ref=e2524]: AI + - link "on Jun 4, 20267 minutes ago" [ref=e2527] [cursor=pointer]: + - /url: https://github.com/CodeStorm-Hub/petfolio/pull/17#discussion_r3355378028 + - generic "Jun 4, 2026, 6:44 AM EDT" [ref=e2528]: on Jun 4, 20267 minutes ago + - generic [ref=e2530]: + - list [ref=e2532]: + - listitem + - listitem [ref=e2533]: + - generic [ref=e2534]: High + - button "Actions for Copilot's comment, 6:44 AM today" [ref=e2536] [cursor=pointer]: + - img [ref=e2537] + - generic [ref=e2540]: + - paragraph [ref=e2543]: This launch config hard-codes real environment values (Supabase URL/anon key, Stripe publishable key, NVIDIA API key). Even if some are “public”, committing them makes rotation harder and can unintentionally expose non-prod resources; API keys (e.g. NVIDIA) should never be in-repo. + - generic [ref=e2544]: + - generic [ref=e2546]: + - text: Suggested changeset + - generic [ref=e2547]: "1" + - generic [ref=e2548]: (1) + - generic [ref=e2549]: + - generic [ref=e2550]: + - button "Close review comment" [ref=e2551] [cursor=pointer]: + - img [ref=e2552] + - text: .claude/launch.json + - table [ref=e2555]: + - rowgroup [ref=e2556]: + - row "Original file line number Diff line number Diff line change" [ref=e2557]: + - columnheader "Original file line number" [ref=e2558] + - columnheader "Diff line number" [ref=e2559] + - columnheader "Diff line change" [ref=e2560] + - rowgroup [ref=e2565]: + - row "@@ -9,10 +9,10 @@" [ref=e2566]: + - cell "@@ -9,10 +9,10 @@" [ref=e2567]: + - generic [ref=e2568]: + - img [ref=e2570] + - code [ref=e2572]: + - generic [ref=e2573]: "@@ -9,10 +9,10 @@" + - row "9 9 \"-d\", \"web-server\"," [ref=e2574]: + - cell "9" [ref=e2575]: + - code [ref=e2576]: "9" + - cell "9" [ref=e2577]: + - code [ref=e2578]: "9" + - cell "\"-d\", \"web-server\"," [ref=e2579]: + - code [ref=e2580]: + - generic [ref=e2581]: + - generic [ref=e2582]: "\"-d\"" + - text: "," + - generic [ref=e2583]: "\"web-server\"" + - text: "," + - row "10 10 \"--web-port=8080\"," [ref=e2584]: + - cell "10" [ref=e2585]: + - code [ref=e2586]: "10" + - cell "10" [ref=e2587]: + - code [ref=e2588]: "10" + - cell "\"--web-port=8080\"," [ref=e2589]: + - code [ref=e2590]: + - generic [ref=e2591]: + - generic [ref=e2592]: "\"--web-port=8080\"" + - text: "," + - row "11 11 \"--web-hostname=localhost\"," [ref=e2593]: + - cell "11" [ref=e2594]: + - code [ref=e2595]: "11" + - cell "11" [ref=e2596]: + - code [ref=e2597]: "11" + - cell "\"--web-hostname=localhost\"," [ref=e2598]: + - code [ref=e2599]: + - generic [ref=e2600]: + - generic [ref=e2601]: "\"--web-hostname=localhost\"" + - text: "," + - row "12 - \"--dart-define=SUPABASE_URL=https://jqyjvhwlcqcsuwcqgcwf.supabase.co\"," [ref=e2602]: + - cell "12" [ref=e2603]: + - code [ref=e2604]: "12" + - cell [ref=e2605]: + - code + - cell "- \"--dart-define=SUPABASE_URL=https://jqyjvhwlcqcsuwcqgcwf.supabase.co\"," [ref=e2606]: + - code [ref=e2607]: + - generic [ref=e2608]: "-" + - generic [ref=e2609]: + - generic [ref=e2610]: "\"--dart-define=SUPABASE_URL=https://jqyjvhwlcqcsuwcqgcwf.supabase.co\"" + - text: "," + - row "13 - \"--dart-define=SUPABASE_ANON_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6ImpxeWp2aHdsY3Fjc3V3Y3FnY3dmIiwicm9sZSI6ImFub24iLCJpYXQiOjE3Nzg1MjI4MjgsImV4cCI6MjA5NDA5ODgyOH0.3bF68bNG0IwAc50YbOC3sem4k8O-d1vkvNNqBt1HbRw\"," [ref=e2611]: + - cell "13" [ref=e2612]: + - code [ref=e2613]: "13" + - cell [ref=e2614]: + - code + - cell "- \"--dart-define=SUPABASE_ANON_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6ImpxeWp2aHdsY3Fjc3V3Y3FnY3dmIiwicm9sZSI6ImFub24iLCJpYXQiOjE3Nzg1MjI4MjgsImV4cCI6MjA5NDA5ODgyOH0.3bF68bNG0IwAc50YbOC3sem4k8O-d1vkvNNqBt1HbRw\"," [ref=e2615]: + - code [ref=e2616]: + - generic [ref=e2617]: "-" + - generic [ref=e2618]: + - generic [ref=e2619]: "\"--dart-define=SUPABASE_ANON_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6ImpxeWp2aHdsY3Fjc3V3Y3FnY3dmIiwicm9sZSI6ImFub24iLCJpYXQiOjE3Nzg1MjI4MjgsImV4cCI6MjA5NDA5ODgyOH0.3bF68bNG0IwAc50YbOC3sem4k8O-d1vkvNNqBt1HbRw\"" + - text: "," + - row "14 - \"--dart-define=STRIPE_PUBLISHABLE_KEY=pk_test_51TQvlrPcVRApxzIxJ8RmKYA1WEw7k8zubumbIfsDjRSGgDyAcSU22RhsZRtKIP1lAZ0wtGjpLfzjI4fozMZxGSlo006zuZrbon\"," [ref=e2620]: + - cell "14" [ref=e2621]: + - code [ref=e2622]: "14" + - cell [ref=e2623]: + - code + - cell "- \"--dart-define=STRIPE_PUBLISHABLE_KEY=pk_test_51TQvlrPcVRApxzIxJ8RmKYA1WEw7k8zubumbIfsDjRSGgDyAcSU22RhsZRtKIP1lAZ0wtGjpLfzjI4fozMZxGSlo006zuZrbon\"," [ref=e2624]: + - code [ref=e2625]: + - generic [ref=e2626]: "-" + - generic [ref=e2627]: + - generic [ref=e2628]: "\"--dart-define=STRIPE_PUBLISHABLE_KEY=pk_test_51TQvlrPcVRApxzIxJ8RmKYA1WEw7k8zubumbIfsDjRSGgDyAcSU22RhsZRtKIP1lAZ0wtGjpLfzjI4fozMZxGSlo006zuZrbon\"" + - text: "," + - row "15 - \"--dart-define=NVIDIA_API_KEY=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\"" [ref=e2629]: + - cell "15" [ref=e2630]: + - code [ref=e2631]: "15" + - cell [ref=e2632]: + - code + - cell "- \"--dart-define=NVIDIA_API_KEY=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\"" [ref=e2633]: + - code [ref=e2634]: + - generic [ref=e2635]: "-" + - generic [ref=e2637]: "\"--dart-define=NVIDIA_API_KEY=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\"" + - 'row "12 + \"--dart-define=SUPABASE_URL=${SUPABASE_URL}\"," [ref=e2638]': + - cell [ref=e2639]: + - code + - cell "12" [ref=e2640]: + - code [ref=e2641]: "12" + - 'cell "+ \"--dart-define=SUPABASE_URL=${SUPABASE_URL}\"," [ref=e2642]': + - code [ref=e2643]: + - generic [ref=e2644]: + + - generic [ref=e2645]: + - generic [ref=e2646]: "\"--dart-define=SUPABASE_URL=${SUPABASE_URL}\"" + - text: "," + - 'row "13 + \"--dart-define=SUPABASE_ANON_KEY=${SUPABASE_ANON_KEY}\"," [ref=e2647]': + - cell [ref=e2648]: + - code + - cell "13" [ref=e2649]: + - code [ref=e2650]: "13" + - 'cell "+ \"--dart-define=SUPABASE_ANON_KEY=${SUPABASE_ANON_KEY}\"," [ref=e2651]': + - code [ref=e2652]: + - generic [ref=e2653]: + + - generic [ref=e2654]: + - generic [ref=e2655]: "\"--dart-define=SUPABASE_ANON_KEY=${SUPABASE_ANON_KEY}\"" + - text: "," + - 'row "14 + \"--dart-define=STRIPE_PUBLISHABLE_KEY=${STRIPE_PUBLISHABLE_KEY}\"," [ref=e2656]': + - cell [ref=e2657]: + - code + - cell "14" [ref=e2658]: + - code [ref=e2659]: "14" + - 'cell "+ \"--dart-define=STRIPE_PUBLISHABLE_KEY=${STRIPE_PUBLISHABLE_KEY}\"," [ref=e2660]': + - code [ref=e2661]: + - generic [ref=e2662]: + + - generic [ref=e2663]: + - generic [ref=e2664]: "\"--dart-define=STRIPE_PUBLISHABLE_KEY=${STRIPE_PUBLISHABLE_KEY}\"" + - text: "," + - 'row "15 + \"--dart-define=NVIDIA_API_KEY=${NVIDIA_API_KEY}\"" [ref=e2665]': + - cell [ref=e2666]: + - code + - cell "15" [ref=e2667]: + - code [ref=e2668]: "15" + - 'cell "+ \"--dart-define=NVIDIA_API_KEY=${NVIDIA_API_KEY}\"" [ref=e2669]': + - code [ref=e2670]: + - generic [ref=e2671]: + + - generic [ref=e2673]: "\"--dart-define=NVIDIA_API_KEY=${NVIDIA_API_KEY}\"" + - row "16 16 ]," [ref=e2674]: + - cell "16" [ref=e2675]: + - code [ref=e2676]: "16" + - cell "16" [ref=e2677]: + - code [ref=e2678]: "16" + - cell "]," [ref=e2679]: + - code [ref=e2680]: + - generic [ref=e2681]: "]," + - 'row "17 17 \"port\": 8080" [ref=e2682]': + - cell "17" [ref=e2683]: + - code [ref=e2684]: "17" + - cell "17" [ref=e2685]: + - code [ref=e2686]: "17" + - 'cell "\"port\": 8080" [ref=e2687]': + - code [ref=e2688]: + - generic [ref=e2689]: "\"port\": 8080" + - 'row "18 18 }" [ref=e2690]': + - cell "18" [ref=e2691]: + - code [ref=e2692]: "18" + - cell "18" [ref=e2693]: + - code [ref=e2694]: "18" + - 'cell "}" [ref=e2695]': + - code [ref=e2696]: + - generic [ref=e2697]: "}" + - button "Commit suggestion" [ref=e2699] [cursor=pointer]: + - generic [ref=e2701]: Commit suggestion + - generic [ref=e2702]: + - toolbar "Reactions" + - generic [ref=e2704]: + - button "Positive feedback" [ref=e2705] [cursor=pointer]: + - img [ref=e2706] + - button "Negative feedback" [ref=e2708] [cursor=pointer]: + - img [ref=e2709] + - generic [ref=e2711]: Copilot uses AI. Check for mistakes. + - generic [ref=e2713]: + - generic [ref=e2714]: + - button "Comment thread" [expanded] [ref=e2715] [cursor=pointer]: + - img + - link ".github/workflows/deploy-web.yml" [ref=e2717] [cursor=pointer]: + - /url: /CodeStorm-Hub/petfolio/pull/17/files/610ee87712bfd0f063f722d6cadca2a5289430cb#diff-875a32a9bd9123a01ecb0b1802809c712a8a04f695e979c262eb9380e6359efc + - generic [ref=e2718]: + - generic [ref=e2720]: Comment on lines +35 to +39 + - table [ref=e2723]: + - rowgroup [ref=e2724]: + - 'row "35 + - name: Deploy to Vercel" [ref=e2725]': + - cell [ref=e2726] [cursor=pointer] + - cell "35" [ref=e2727] [cursor=pointer] + - 'cell "+ - name: Deploy to Vercel" [ref=e2728]': + - generic [ref=e2729]: "+ - name: Deploy to Vercel" + - 'row "36 + working-directory: build/web" [ref=e2730]': + - cell [ref=e2731] [cursor=pointer] + - cell "36" [ref=e2732] [cursor=pointer] + - 'cell "+ working-directory: build/web" [ref=e2733]': + - generic [ref=e2734]: "+ working-directory: build/web" + - 'row "37 + run: npx vercel deploy --prod --yes --token=${{ secrets.VERCEL_TOKEN }}" [ref=e2735]': + - cell [ref=e2736] [cursor=pointer] + - cell "37" [ref=e2737] [cursor=pointer] + - 'cell "+ run: npx vercel deploy --prod --yes --token=${{ secrets.VERCEL_TOKEN }}" [ref=e2738]': + - generic [ref=e2739]: "+ run: npx vercel deploy --prod --yes --token=${{ secrets.VERCEL_TOKEN }}" + - row "38 + env:" [ref=e2740]: + - cell [ref=e2741] [cursor=pointer] + - cell "38" [ref=e2742] [cursor=pointer] + - cell "+ env:" [ref=e2743]: + - generic [ref=e2744]: "+ env:" + - 'row "39 + VERCEL_ORG_ID: team_lC8aTJK0XiU9qDfaHeTfCJs6" [ref=e2745]': + - cell [ref=e2746] [cursor=pointer] + - cell "39" [ref=e2747] [cursor=pointer] + - 'cell "+ VERCEL_ORG_ID: team_lC8aTJK0XiU9qDfaHeTfCJs6" [ref=e2748]': + - generic [ref=e2749]: "+ VERCEL_ORG_ID: team_lC8aTJK0XiU9qDfaHeTfCJs6" + - generic [ref=e2756]: + - generic [ref=e2759]: + - heading "Copilot commented on Jun 4, 2026 7 minutes ago" [level=3] [ref=e2760]: + - text: Copilot commented + - generic "Jun 4, 2026, 6:44 AM EDT" [ref=e2761]: on Jun 4, 2026 7 minutes ago + - img "Copilot" [ref=e2764] + - generic [ref=e2765]: + - generic [ref=e2766]: + - generic [ref=e2767]: Copilot + - generic [ref=e2768]: AI + - link "on Jun 4, 20267 minutes ago" [ref=e2771] [cursor=pointer]: + - /url: https://github.com/CodeStorm-Hub/petfolio/pull/17#discussion_r3355378057 + - generic "Jun 4, 2026, 6:44 AM EDT" [ref=e2772]: on Jun 4, 20267 minutes ago + - generic [ref=e2774]: + - list [ref=e2776]: + - listitem + - listitem [ref=e2777]: + - generic [ref=e2778]: High + - button "Actions for Copilot's comment, 6:44 AM today" [ref=e2780] [cursor=pointer]: + - img [ref=e2781] + - generic [ref=e2784]: + - paragraph [ref=e2787]: + - text: The workflow deploys with + - code [ref=e2788]: "--prod" + - text: "on" + - emphasis [ref=e2789]: pull_request + - text: events too. This can overwrite production for PR builds, and PRs from forks won’t have secrets (causing failing runs). Split preview vs production deploy and gate preview deploys to non-fork PRs. + - generic [ref=e2790]: + - generic [ref=e2792]: + - text: Suggested changeset + - generic [ref=e2793]: "1" + - generic [ref=e2794]: (1) + - generic [ref=e2796]: + - button "Open review comment" [ref=e2797] [cursor=pointer]: + - img [ref=e2798] + - text: .github/workflows/deploy-web.yml + - button "Commit suggestion" [ref=e2801] [cursor=pointer]: + - generic [ref=e2803]: Commit suggestion + - generic [ref=e2804]: + - toolbar "Reactions" + - generic [ref=e2806]: + - button "Positive feedback" [ref=e2807] [cursor=pointer]: + - img [ref=e2808] + - button "Negative feedback" [ref=e2810] [cursor=pointer]: + - img [ref=e2811] + - generic [ref=e2813]: Copilot uses AI. Check for mistakes. + - generic [ref=e2815]: + - generic [ref=e2816]: + - button "Comment thread" [expanded] [ref=e2817] [cursor=pointer]: + - img + - link "vercel.json" [ref=e2819] [cursor=pointer]: + - /url: /CodeStorm-Hub/petfolio/pull/17/files/610ee87712bfd0f063f722d6cadca2a5289430cb#diff-a3265310f552fb66876e8bfe8809737e59e5ba946bdf39138b44d9baf4e21240 + - generic [ref=e2820]: + - generic [ref=e2822]: Comment on lines +11 to +25 + - table [ref=e2825]: + - rowgroup [ref=e2826]: + - 'row "11 + {" [ref=e2827]': + - cell [ref=e2828] [cursor=pointer] + - cell "11" [ref=e2829] [cursor=pointer] + - 'cell "+ {" [ref=e2830]': + - generic [ref=e2831]: "+ {" + - 'row "12 + \"source\": \"/(.*)\\\\.wasm\"," [ref=e2832]': + - cell [ref=e2833] [cursor=pointer] + - cell "12" [ref=e2834] [cursor=pointer] + - 'cell "+ \"source\": \"/(.*)\\\\.wasm\"," [ref=e2835]': + - generic [ref=e2836]: "+ \"source\": \"/(.*)\\\\.wasm\"," + - 'row "13 + \"headers\": [" [ref=e2837]': + - cell [ref=e2838] [cursor=pointer] + - cell "13" [ref=e2839] [cursor=pointer] + - 'cell "+ \"headers\": [" [ref=e2840]': + - generic [ref=e2841]: "+ \"headers\": [" + - 'row "14 + { \"key\": \"Content-Type\", \"value\": \"application/wasm\" }," [ref=e2842]': + - cell [ref=e2843] [cursor=pointer] + - cell "14" [ref=e2844] [cursor=pointer] + - 'cell "+ { \"key\": \"Content-Type\", \"value\": \"application/wasm\" }," [ref=e2845]': + - generic [ref=e2846]: "+ { \"key\": \"Content-Type\", \"value\": \"application/wasm\" }," + - 'row "15 + { \"key\": \"Cross-Origin-Embedder-Policy\", \"value\": \"require-corp\" }," [ref=e2847]': + - cell [ref=e2848] [cursor=pointer] + - cell "15" [ref=e2849] [cursor=pointer] + - 'cell "+ { \"key\": \"Cross-Origin-Embedder-Policy\", \"value\": \"require-corp\" }," [ref=e2850]': + - generic [ref=e2851]: "+ { \"key\": \"Cross-Origin-Embedder-Policy\", \"value\": \"require-corp\" }," + - 'row "16 + { \"key\": \"Cross-Origin-Opener-Policy\", \"value\": \"same-origin\" }" [ref=e2852]': + - cell [ref=e2853] [cursor=pointer] + - cell "16" [ref=e2854] [cursor=pointer] + - 'cell "+ { \"key\": \"Cross-Origin-Opener-Policy\", \"value\": \"same-origin\" }" [ref=e2855]': + - generic [ref=e2856]: "+ { \"key\": \"Cross-Origin-Opener-Policy\", \"value\": \"same-origin\" }" + - row "17 + ]" [ref=e2857]: + - cell [ref=e2858] [cursor=pointer] + - cell "17" [ref=e2859] [cursor=pointer] + - cell "+ ]" [ref=e2860]: + - generic [ref=e2861]: + ] + - 'row "18 + }," [ref=e2862]': + - cell [ref=e2863] [cursor=pointer] + - cell "18" [ref=e2864] [cursor=pointer] + - 'cell "+ }," [ref=e2865]': + - generic [ref=e2866]: "+ }," + - 'row "19 + {" [ref=e2867]': + - cell [ref=e2868] [cursor=pointer] + - cell "19" [ref=e2869] [cursor=pointer] + - 'cell "+ {" [ref=e2870]': + - generic [ref=e2871]: "+ {" + - 'row "20 + \"source\": \"/(.*)\"," [ref=e2872]': + - cell [ref=e2873] [cursor=pointer] + - cell "20" [ref=e2874] [cursor=pointer] + - 'cell "+ \"source\": \"/(.*)\"," [ref=e2875]': + - generic [ref=e2876]: "+ \"source\": \"/(.*)\"," + - 'row "21 + \"headers\": [" [ref=e2877]': + - cell [ref=e2878] [cursor=pointer] + - cell "21" [ref=e2879] [cursor=pointer] + - 'cell "+ \"headers\": [" [ref=e2880]': + - generic [ref=e2881]: "+ \"headers\": [" + - 'row "22 + { \"key\": \"Cross-Origin-Embedder-Policy\", \"value\": \"require-corp\" }," [ref=e2882]': + - cell [ref=e2883] [cursor=pointer] + - cell "22" [ref=e2884] [cursor=pointer] + - 'cell "+ { \"key\": \"Cross-Origin-Embedder-Policy\", \"value\": \"require-corp\" }," [ref=e2885]': + - generic [ref=e2886]: "+ { \"key\": \"Cross-Origin-Embedder-Policy\", \"value\": \"require-corp\" }," + - 'row "23 + { \"key\": \"Cross-Origin-Opener-Policy\", \"value\": \"same-origin\" }" [ref=e2887]': + - cell [ref=e2888] [cursor=pointer] + - cell "23" [ref=e2889] [cursor=pointer] + - 'cell "+ { \"key\": \"Cross-Origin-Opener-Policy\", \"value\": \"same-origin\" }" [ref=e2890]': + - generic [ref=e2891]: "+ { \"key\": \"Cross-Origin-Opener-Policy\", \"value\": \"same-origin\" }" + - row "24 + ]" [ref=e2892]: + - cell [ref=e2893] [cursor=pointer] + - cell "24" [ref=e2894] [cursor=pointer] + - cell "+ ]" [ref=e2895]: + - generic [ref=e2896]: + ] + - 'row "25 + }" [ref=e2897]': + - cell [ref=e2898] [cursor=pointer] + - cell "25" [ref=e2899] [cursor=pointer] + - 'cell "+ }" [ref=e2900]': + - generic [ref=e2901]: "+ }" + - generic [ref=e2908]: + - generic [ref=e2911]: + - heading "Copilot commented on Jun 4, 2026 7 minutes ago" [level=3] [ref=e2912]: + - text: Copilot commented + - generic "Jun 4, 2026, 6:44 AM EDT" [ref=e2913]: on Jun 4, 2026 7 minutes ago + - img "Copilot" [ref=e2916] + - generic [ref=e2917]: + - generic [ref=e2918]: + - generic [ref=e2919]: Copilot + - generic [ref=e2920]: AI + - link "on Jun 4, 20267 minutes ago" [ref=e2923] [cursor=pointer]: + - /url: https://github.com/CodeStorm-Hub/petfolio/pull/17#discussion_r3355378071 + - generic "Jun 4, 2026, 6:44 AM EDT" [ref=e2924]: on Jun 4, 20267 minutes ago + - generic [ref=e2926]: + - list [ref=e2928]: + - listitem + - listitem [ref=e2929]: + - generic [ref=e2930]: High + - button "Actions for Copilot's comment, 6:44 AM today" [ref=e2932] [cursor=pointer]: + - img [ref=e2933] + - generic [ref=e2936]: + - paragraph [ref=e2939]: + - text: Setting COEP/COOP to + - code [ref=e2940]: require-corp + - text: / + - code [ref=e2941]: same-origin + - text: for all routes will block cross-origin subresources that aren’t CORS/CORP-enabled. This app loads Stripe from + - code [ref=e2942]: https://js.stripe.com/v3/ + - text: without + - code [ref=e2943]: crossorigin + - text: ", so these headers are very likely to break Stripe and/or app boot." + - generic [ref=e2944]: + - generic [ref=e2946]: + - text: Suggested changeset + - generic [ref=e2947]: "1" + - generic [ref=e2948]: (1) + - generic [ref=e2949]: + - generic [ref=e2950]: + - button "Close review comment" [ref=e2951] [cursor=pointer]: + - img [ref=e2952] + - text: vercel.json + - table [ref=e2955]: + - rowgroup [ref=e2956]: + - row "Original file line number Diff line number Diff line change" [ref=e2957]: + - columnheader "Original file line number" [ref=e2958] + - columnheader "Diff line number" [ref=e2959] + - columnheader "Diff line change" [ref=e2960] + - rowgroup [ref=e2965]: + - row "@@ -11,16 +11,7 @@" [ref=e2966]: + - cell "@@ -11,16 +11,7 @@" [ref=e2967]: + - generic [ref=e2968]: + - img [ref=e2970] + - code [ref=e2972]: + - generic [ref=e2973]: "@@ -11,16 +11,7 @@" + - 'row "11 11 {" [ref=e2974]': + - cell "11" [ref=e2975]: + - code [ref=e2976]: "11" + - cell "11" [ref=e2977]: + - code [ref=e2978]: "11" + - 'cell "{" [ref=e2979]': + - code [ref=e2980]: + - generic [ref=e2981]: "{" + - 'row "12 12 \"source\": \"/(.*)\\\\.wasm\"," [ref=e2982]': + - cell "12" [ref=e2983]: + - code [ref=e2984]: "12" + - cell "12" [ref=e2985]: + - code [ref=e2986]: "12" + - 'cell "\"source\": \"/(.*)\\\\.wasm\"," [ref=e2987]': + - code [ref=e2988]: + - generic [ref=e2989]: + - text: "\"source\":" + - generic [ref=e2990]: "\"/(.*)\\\\.wasm\"" + - text: "," + - 'row "13 13 \"headers\": [" [ref=e2991]': + - cell "13" [ref=e2992]: + - code [ref=e2993]: "13" + - cell "13" [ref=e2994]: + - code [ref=e2995]: "13" + - 'cell "\"headers\": [" [ref=e2996]': + - code [ref=e2997]: + - generic [ref=e2998]: "\"headers\": [" + - 'row "14 - { \"key\": \"Content-Type\", \"value\": \"application/wasm\" }," [ref=e2999]': + - cell "14" [ref=e3000]: + - code [ref=e3001]: "14" + - cell [ref=e3002]: + - code + - 'cell "- { \"key\": \"Content-Type\", \"value\": \"application/wasm\" }," [ref=e3003]': + - code [ref=e3004]: + - generic [ref=e3005]: "-" + - generic [ref=e3006]: + - text: "{ \"key\":" + - generic [ref=e3007]: "\"Content-Type\"" + - text: ", \"value\":" + - generic [ref=e3008]: "\"application/wasm\"" + - text: "}," + - 'row "15 - { \"key\": \"Cross-Origin-Embedder-Policy\", \"value\": \"require-corp\" }," [ref=e3009]': + - cell "15" [ref=e3010]: + - code [ref=e3011]: "15" + - cell [ref=e3012]: + - code + - 'cell "- { \"key\": \"Cross-Origin-Embedder-Policy\", \"value\": \"require-corp\" }," [ref=e3013]': + - code [ref=e3014]: + - generic [ref=e3015]: "-" + - generic [ref=e3016]: + - text: "{ \"key\":" + - generic [ref=e3017]: "\"Cross-Origin-Embedder-Policy\"" + - text: ", \"value\":" + - generic [ref=e3018]: "\"require-corp\"" + - text: "}," + - 'row "16 - { \"key\": \"Cross-Origin-Opener-Policy\", \"value\": \"same-origin\" }" [ref=e3019]': + - cell "16" [ref=e3020]: + - code [ref=e3021]: "16" + - cell [ref=e3022]: + - code + - 'cell "- { \"key\": \"Cross-Origin-Opener-Policy\", \"value\": \"same-origin\" }" [ref=e3023]': + - code [ref=e3024]: + - generic [ref=e3025]: "-" + - generic [ref=e3026]: + - text: "{ \"key\":" + - generic [ref=e3027]: "\"Cross-Origin-Opener-Policy\"" + - text: ", \"value\":" + - generic [ref=e3028]: "\"same-origin\"" + - text: "}" + - row "17 - ]" [ref=e3029]: + - cell "17" [ref=e3030]: + - code [ref=e3031]: "17" + - cell [ref=e3032]: + - code + - cell "- ]" [ref=e3033]: + - code [ref=e3034]: + - generic [ref=e3035]: "-" + - generic [ref=e3036]: "]" + - 'row "18 - }," [ref=e3037]': + - cell "18" [ref=e3038]: + - code [ref=e3039]: "18" + - cell [ref=e3040]: + - code + - 'cell "- }," [ref=e3041]': + - code [ref=e3042]: + - generic [ref=e3043]: "-" + - generic [ref=e3044]: "}," + - 'row "19 - {" [ref=e3045]': + - cell "19" [ref=e3046]: + - code [ref=e3047]: "19" + - cell [ref=e3048]: + - code + - 'cell "- {" [ref=e3049]': + - code [ref=e3050]: + - generic [ref=e3051]: "-" + - generic [ref=e3052]: "{" + - 'row "20 - \"source\": \"/(.*)\"," [ref=e3053]': + - cell "20" [ref=e3054]: + - code [ref=e3055]: "20" + - cell [ref=e3056]: + - code + - 'cell "- \"source\": \"/(.*)\"," [ref=e3057]': + - code [ref=e3058]: + - generic [ref=e3059]: "-" + - generic [ref=e3060]: + - text: "\"source\":" + - generic [ref=e3061]: "\"/(.*)\"" + - text: "," + - 'row "21 - \"headers\": [" [ref=e3062]': + - cell "21" [ref=e3063]: + - code [ref=e3064]: "21" + - cell [ref=e3065]: + - code + - 'cell "- \"headers\": [" [ref=e3066]': + - code [ref=e3067]: + - generic [ref=e3068]: "-" + - generic [ref=e3069]: "\"headers\": [" + - 'row "22 - { \"key\": \"Cross-Origin-Embedder-Policy\", \"value\": \"require-corp\" }," [ref=e3070]': + - cell "22" [ref=e3071]: + - code [ref=e3072]: "22" + - cell [ref=e3073]: + - code + - 'cell "- { \"key\": \"Cross-Origin-Embedder-Policy\", \"value\": \"require-corp\" }," [ref=e3074]': + - code [ref=e3075]: + - generic [ref=e3076]: "-" + - generic [ref=e3077]: + - text: "{ \"key\":" + - generic [ref=e3078]: "\"Cross-Origin-Embedder-Policy\"" + - text: ", \"value\":" + - generic [ref=e3079]: "\"require-corp\"" + - text: "}," + - 'row "23 - { \"key\": \"Cross-Origin-Opener-Policy\", \"value\": \"same-origin\" }" [ref=e3080]': + - cell "23" [ref=e3081]: + - code [ref=e3082]: "23" + - cell [ref=e3083]: + - code + - 'cell "- { \"key\": \"Cross-Origin-Opener-Policy\", \"value\": \"same-origin\" }" [ref=e3084]': + - code [ref=e3085]: + - generic [ref=e3086]: "-" + - generic [ref=e3087]: + - text: "{ \"key\":" + - generic [ref=e3088]: "\"Cross-Origin-Opener-Policy\"" + - text: ", \"value\":" + - generic [ref=e3089]: "\"same-origin\"" + - text: "}" + - 'row "14 + { \"key\": \"Content-Type\", \"value\": \"application/wasm\" }" [ref=e3090]': + - cell [ref=e3091]: + - code + - cell "14" [ref=e3092]: + - code [ref=e3093]: "14" + - 'cell "+ { \"key\": \"Content-Type\", \"value\": \"application/wasm\" }" [ref=e3094]': + - code [ref=e3095]: + - generic [ref=e3096]: + + - generic [ref=e3097]: + - text: "{ \"key\":" + - generic [ref=e3098]: "\"Content-Type\"" + - text: ", \"value\":" + - generic [ref=e3099]: "\"application/wasm\"" + - text: "}" + - row "24 15 ]" [ref=e3100]: + - cell "24" [ref=e3101]: + - code [ref=e3102]: "24" + - cell "15" [ref=e3103]: + - code [ref=e3104]: "15" + - cell "]" [ref=e3105]: + - code [ref=e3106]: + - generic [ref=e3107]: "]" + - 'row "25 16 }" [ref=e3108]': + - cell "25" [ref=e3109]: + - code [ref=e3110]: "25" + - cell "16" [ref=e3111]: + - code [ref=e3112]: "16" + - 'cell "}" [ref=e3113]': + - code [ref=e3114]: + - generic [ref=e3115]: "}" + - row "26 17 ]," [ref=e3116]: + - cell "26" [ref=e3117]: + - code [ref=e3118]: "26" + - cell "17" [ref=e3119]: + - code [ref=e3120]: "17" + - cell "]," [ref=e3121]: + - code [ref=e3122]: + - generic [ref=e3123]: "]," + - button "Commit suggestion" [ref=e3125] [cursor=pointer]: + - generic [ref=e3127]: Commit suggestion + - generic [ref=e3128]: + - toolbar "Reactions" + - generic [ref=e3130]: + - button "Positive feedback" [ref=e3131] [cursor=pointer]: + - img [ref=e3132] + - button "Negative feedback" [ref=e3134] [cursor=pointer]: + - img [ref=e3135] + - generic [ref=e3137]: Copilot uses AI. Check for mistakes. + - generic [ref=e3139]: + - generic [ref=e3140]: + - button "Comment thread" [expanded] [ref=e3141] [cursor=pointer]: + - img + - link "web/index.html" [ref=e3143] [cursor=pointer]: + - /url: /CodeStorm-Hub/petfolio/pull/17/files/610ee87712bfd0f063f722d6cadca2a5289430cb#diff-8f62b6ced28d3396b501d2e89a2e7cb761d16cd7dc977aebece03d4a5da5c24e + - generic [ref=e3144]: + - generic [ref=e3146]: Comment on lines +10 to +12 + - table [ref=e3149]: + - rowgroup [ref=e3150]: + - row "10 + " [ref=e3161]: + - cell [ref=e3162] [cursor=pointer] + - cell "12" [ref=e3163] [cursor=pointer] + - cell "+ user-scalable=no, viewport-fit=cover\">" [ref=e3164]: + - generic [ref=e3165]: + user-scalable=no, viewport-fit=cover"> + - generic [ref=e3172]: + - generic [ref=e3175]: + - heading "Copilot commented on Jun 4, 2026 7 minutes ago" [level=3] [ref=e3176]: + - text: Copilot commented + - generic "Jun 4, 2026, 6:44 AM EDT" [ref=e3177]: on Jun 4, 2026 7 minutes ago + - img "Copilot" [ref=e3180] + - generic [ref=e3181]: + - generic [ref=e3182]: + - generic [ref=e3183]: Copilot + - generic [ref=e3184]: AI + - link "on Jun 4, 20267 minutes ago" [ref=e3187] [cursor=pointer]: + - /url: https://github.com/CodeStorm-Hub/petfolio/pull/17#discussion_r3355378096 + - generic "Jun 4, 2026, 6:44 AM EDT" [ref=e3188]: on Jun 4, 20267 minutes ago + - generic [ref=e3190]: + - list [ref=e3192]: + - listitem + - listitem [ref=e3193]: + - generic [ref=e3194]: Medium + - button "Actions for Copilot's comment, 6:44 AM today" [ref=e3196] [cursor=pointer]: + - img [ref=e3197] + - generic [ref=e3200]: + - paragraph [ref=e3203]: + - text: The viewport meta disables pinch-zoom ( + - code [ref=e3204]: maximum-scale=1.0 + - text: + + - code [ref=e3205]: user-scalable=no + - text: ). This is an accessibility issue (users who rely on zoom cannot enlarge text/UI). Prefer allowing zoom while keeping + - code [ref=e3206]: viewport-fit=cover + - text: . + - generic [ref=e3207]: + - generic [ref=e3209]: + - text: Suggested changeset + - generic [ref=e3210]: "1" + - generic [ref=e3211]: (1) + - generic [ref=e3212]: + - generic [ref=e3213]: + - button "Close review comment" [ref=e3214] [cursor=pointer]: + - img [ref=e3215] + - text: web/index.html + - table [ref=e3218]: + - rowgroup [ref=e3219]: + - row "Original file line number Diff line number Diff line change" [ref=e3220]: + - columnheader "Original file line number" [ref=e3221] + - columnheader "Diff line number" [ref=e3222] + - columnheader "Diff line change" [ref=e3223] + - rowgroup [ref=e3228]: + - row "@@ -8,8 +8,7 @@" [ref=e3229]: + - cell "@@ -8,8 +8,7 @@" [ref=e3230]: + - generic [ref=e3231]: + - img [ref=e3233] + - code [ref=e3235]: + - generic [ref=e3236]: "@@ -8,8 +8,7 @@" + - row "8 8" [ref=e3237]: + - cell "8" [ref=e3238]: + - code [ref=e3239]: "8" + - cell "8" [ref=e3240]: + - code [ref=e3241]: "8" + - cell [ref=e3242]: + - code + - 'row "9 9 " [ref=e3243]': + - cell "9" [ref=e3244]: + - code [ref=e3245]: "9" + - cell "9" [ref=e3246]: + - code [ref=e3247]: "9" + - 'cell "" [ref=e3248]': + - code [ref=e3249]: + - generic [ref=e3250]: "" + - row "10 10 " [ref=e3267]: + - cell "12" [ref=e3268]: + - code [ref=e3269]: "12" + - cell [ref=e3270]: + - code + - cell "- user-scalable=no, viewport-fit=cover\">" [ref=e3271]: + - code [ref=e3272]: + - generic [ref=e3273]: "-" + - generic [ref=e3274]: user-scalable=no, viewport-fit=cover"> + - row "11 + content=\"width=device-width, initial-scale=1.0, viewport-fit=cover\">" [ref=e3275]: + - cell [ref=e3276]: + - code + - cell "11" [ref=e3277]: + - code [ref=e3278]: "11" + - cell "+ content=\"width=device-width, initial-scale=1.0, viewport-fit=cover\">" [ref=e3279]: + - code [ref=e3280]: + - generic [ref=e3281]: + + - generic [ref=e3282]: content="width=device-width, initial-scale=1.0, viewport-fit=cover"> + - row "13 12" [ref=e3283]: + - cell "13" [ref=e3284]: + - code [ref=e3285]: "13" + - cell "12" [ref=e3286]: + - code [ref=e3287]: "12" + - cell [ref=e3288]: + - code + - row "14 13 " [ref=e3297]: + - cell "15" [ref=e3298]: + - code [ref=e3299]: "15" + - cell "14" [ref=e3300]: + - code [ref=e3301]: "14" + - cell "content=\"PetFolio — Your pet's social network, health tracker & marketplace.\">" [ref=e3302]: + - code [ref=e3303]: + - generic [ref=e3304]: content="PetFolio — Your pet's social network, health tracker & marketplace."> + - button "Commit suggestion" [ref=e3306] [cursor=pointer]: + - generic [ref=e3308]: Commit suggestion + - generic [ref=e3309]: + - toolbar "Reactions" + - generic [ref=e3311]: + - button "Positive feedback" [ref=e3312] [cursor=pointer]: + - img [ref=e3313] + - button "Negative feedback" [ref=e3315] [cursor=pointer]: + - img [ref=e3316] + - generic [ref=e3318]: Copilot uses AI. Check for mistakes. + - generic [ref=e3320]: + - generic [ref=e3321]: + - button "Comment thread" [expanded] [ref=e3322] [cursor=pointer]: + - img + - link "web/index.html" [ref=e3324] [cursor=pointer]: + - /url: /CodeStorm-Hub/petfolio/pull/17/files/610ee87712bfd0f063f722d6cadca2a5289430cb#diff-8f62b6ced28d3396b501d2e89a2e7cb761d16cd7dc977aebece03d4a5da5c24e + - generic [ref=e3325]: + - generic [ref=e3327]: Comment on lines +29 to +31 + - table [ref=e3330]: + - rowgroup [ref=e3331]: + - row "29 + " [ref=e3332]: + - cell [ref=e3333] [cursor=pointer] + - cell "29" [ref=e3334] [cursor=pointer] + - cell "+ " [ref=e3335]: + - generic [ref=e3336]: + + - row "30 + " [ref=e3337]: + - cell [ref=e3338] [cursor=pointer] + - cell "30" [ref=e3339] [cursor=pointer] + - cell "+ " [ref=e3340]: + - generic [ref=e3341]: + + - row "31 + " [ref=e3342]: + - cell [ref=e3343] [cursor=pointer] + - cell "31" [ref=e3344] [cursor=pointer] + - cell "+ " [ref=e3345]: + - generic [ref=e3346]: + + - generic [ref=e3353]: + - generic [ref=e3356]: + - heading "Copilot commented on Jun 4, 2026 7 minutes ago" [level=3] [ref=e3357]: + - text: Copilot commented + - generic "Jun 4, 2026, 6:44 AM EDT" [ref=e3358]: on Jun 4, 2026 7 minutes ago + - img "Copilot" [ref=e3361] + - generic [ref=e3362]: + - generic [ref=e3363]: + - generic [ref=e3364]: Copilot + - generic [ref=e3365]: AI + - link "on Jun 4, 20267 minutes ago" [ref=e3368] [cursor=pointer]: + - /url: https://github.com/CodeStorm-Hub/petfolio/pull/17#discussion_r3355378110 + - generic "Jun 4, 2026, 6:44 AM EDT" [ref=e3369]: on Jun 4, 20267 minutes ago + - generic [ref=e3371]: + - list [ref=e3373]: + - listitem + - listitem [ref=e3374]: + - generic [ref=e3375]: Medium + - button "Actions for Copilot's comment, 6:44 AM today" [ref=e3377] [cursor=pointer]: + - img [ref=e3378] + - generic [ref=e3381]: + - paragraph [ref=e3384]: + - text: The + - code [ref=e3385]: apple-touch-icon + - text: declares + - code [ref=e3386]: sizes="180x180" + - text: but points to + - code [ref=e3387]: Icon-192.png + - text: (192×192). iOS uses the + - code [ref=e3388]: sizes + - text: hint for selection; mismatching metadata can lead to suboptimal icon selection/caching. + - generic [ref=e3389]: + - generic [ref=e3391]: + - text: Suggested changeset + - generic [ref=e3392]: "1" + - generic [ref=e3393]: (1) + - generic [ref=e3394]: + - generic [ref=e3395]: + - button "Close review comment" [ref=e3396] [cursor=pointer]: + - img [ref=e3397] + - text: web/index.html + - table [ref=e3400]: + - rowgroup [ref=e3401]: + - row "Original file line number Diff line number Diff line change" [ref=e3402]: + - columnheader "Original file line number" [ref=e3403] + - columnheader "Diff line number" [ref=e3404] + - columnheader "Diff line change" [ref=e3405] + - rowgroup [ref=e3410]: + - row "@@ -27,7 +27,7 @@" [ref=e3411]: + - cell "@@ -27,7 +27,7 @@" [ref=e3412]: + - generic [ref=e3413]: + - img [ref=e3415] + - code [ref=e3417]: + - generic [ref=e3418]: "@@ -27,7 +27,7 @@" + - row "27 27 " [ref=e3419]: + - cell "27" [ref=e3420]: + - code [ref=e3421]: "27" + - cell "27" [ref=e3422]: + - code [ref=e3423]: "27" + - cell "" [ref=e3424]: + - code [ref=e3425]: + - generic [ref=e3426]: + - row "28 28" [ref=e3427]: + - cell "28" [ref=e3428]: + - code [ref=e3429]: "28" + - cell "28" [ref=e3430]: + - code [ref=e3431]: "28" + - cell [ref=e3432]: + - code + - row "29 29 " [ref=e3433]: + - cell "29" [ref=e3434]: + - code [ref=e3435]: "29" + - cell "29" [ref=e3436]: + - code [ref=e3437]: "29" + - cell "" [ref=e3438]: + - code [ref=e3439]: + - generic [ref=e3440]: + - row "30 - " [ref=e3441]: + - cell "30" [ref=e3442]: + - code [ref=e3443]: "30" + - cell [ref=e3444]: + - code + - cell "- " [ref=e3445]: + - code [ref=e3446]: + - generic [ref=e3447]: "-" + - generic [ref=e3448]: + - row "30 + " [ref=e3449]: + - cell [ref=e3450]: + - code + - cell "30" [ref=e3451]: + - code [ref=e3452]: "30" + - cell "+ " [ref=e3453]: + - code [ref=e3454]: + - generic [ref=e3455]: + + - generic [ref=e3456]: + - row "31 31 " [ref=e3457]: + - cell "31" [ref=e3458]: + - code [ref=e3459]: "31" + - cell "31" [ref=e3460]: + - code [ref=e3461]: "31" + - cell "" [ref=e3462]: + - code [ref=e3463]: + - generic [ref=e3464]: + - row "32 32" [ref=e3465]: + - cell "32" [ref=e3466]: + - code [ref=e3467]: "32" + - cell "32" [ref=e3468]: + - code [ref=e3469]: "32" + - cell [ref=e3470]: + - code + - row "33 33 " [ref=e3471]: + - cell "33" [ref=e3472]: + - code [ref=e3473]: "33" + - cell "33" [ref=e3474]: + - code [ref=e3475]: "33" + - cell "" [ref=e3476]: + - code [ref=e3477]: + - generic [ref=e3478]: + - button "Commit suggestion" [ref=e3480] [cursor=pointer]: + - generic [ref=e3482]: Commit suggestion + - generic [ref=e3483]: + - toolbar "Reactions" + - generic [ref=e3485]: + - button "Positive feedback" [ref=e3486] [cursor=pointer]: + - img [ref=e3487] + - button "Negative feedback" [ref=e3489] [cursor=pointer]: + - img [ref=e3490] + - generic [ref=e3492]: Copilot uses AI. Check for mistakes. + - generic [ref=e3494]: + - button "8 hidden conversations" [ref=e3495] [cursor=pointer] + - button "Load more…" [ref=e3496] [cursor=pointer] + - generic [ref=e3498]: + - generic [ref=e3499]: + - button "Comment thread" [expanded] [ref=e3500] [cursor=pointer]: + - img + - link "lib/features/care/presentation/widgets/gamified_care_ui.dart" [ref=e3502] [cursor=pointer]: + - /url: /CodeStorm-Hub/petfolio/pull/17/files/610ee87712bfd0f063f722d6cadca2a5289430cb#diff-8e2cf796cdb83ed78c034ccd2f1ab099667749421148c7f255df9b4f1317c508 + - generic [ref=e3503]: + - generic [ref=e3505]: Comment on lines +1096 to +1115 + - table [ref=e3508]: + - rowgroup [ref=e3509]: + - row "1096 + @override" [ref=e3510]: + - cell [ref=e3511] [cursor=pointer] + - cell "1096" [ref=e3512] [cursor=pointer] + - cell "+ @override" [ref=e3513]: + - generic [ref=e3514]: + @override + - 'row "1097 + void initState() {" [ref=e3515]': + - cell [ref=e3516] [cursor=pointer] + - cell "1097" [ref=e3517] [cursor=pointer] + - 'cell "+ void initState() {" [ref=e3518]': + - generic [ref=e3519]: "+ void initState() {" + - row "1098 + super.initState();" [ref=e3520]: + - cell [ref=e3521] [cursor=pointer] + - cell "1098" [ref=e3522] [cursor=pointer] + - cell "+ super.initState();" [ref=e3523]: + - generic [ref=e3524]: + super.initState(); + - row "1099 + _floatCtrl = AnimationController(" [ref=e3525]: + - cell [ref=e3526] [cursor=pointer] + - cell "1099" [ref=e3527] [cursor=pointer] + - cell "+ _floatCtrl = AnimationController(" [ref=e3528]: + - generic [ref=e3529]: + _floatCtrl = AnimationController( + - 'row "1100 + vsync: this," [ref=e3530]': + - cell [ref=e3531] [cursor=pointer] + - cell "1100" [ref=e3532] [cursor=pointer] + - 'cell "+ vsync: this," [ref=e3533]': + - generic [ref=e3534]: "+ vsync: this," + - 'row "1101 + duration: Duration(milliseconds: 3200 + widget.index * 200)," [ref=e3535]': + - cell [ref=e3536] [cursor=pointer] + - cell "1101" [ref=e3537] [cursor=pointer] + - 'cell "+ duration: Duration(milliseconds: 3200 + widget.index * 200)," [ref=e3538]': + - generic [ref=e3539]: "+ duration: Duration(milliseconds: 3200 + widget.index * 200)," + - 'row "1102 + )..repeat(reverse: true);" [ref=e3540]': + - cell [ref=e3541] [cursor=pointer] + - cell "1102" [ref=e3542] [cursor=pointer] + - 'cell "+ )..repeat(reverse: true);" [ref=e3543]': + - generic [ref=e3544]: "+ )..repeat(reverse: true);" + - row "1103 +" [ref=e3545]: + - cell [ref=e3546] [cursor=pointer] + - cell "1103" [ref=e3547] [cursor=pointer] + - cell "+" [ref=e3548]: + - generic: + + - 'row "1104 + final sheenDuration = Duration(milliseconds: 3800 + widget.index * 200);" [ref=e3549]': + - cell [ref=e3550] [cursor=pointer] + - cell "1104" [ref=e3551] [cursor=pointer] + - 'cell "+ final sheenDuration = Duration(milliseconds: 3800 + widget.index * 200);" [ref=e3552]': + - generic [ref=e3553]: "+ final sheenDuration = Duration(milliseconds: 3800 + widget.index * 200);" + - 'row "1105 + _sheenCtrl = AnimationController(vsync: this, duration: sheenDuration);" [ref=e3554]': + - cell [ref=e3555] [cursor=pointer] + - cell "1105" [ref=e3556] [cursor=pointer] + - 'cell "+ _sheenCtrl = AnimationController(vsync: this, duration: sheenDuration);" [ref=e3557]': + - generic [ref=e3558]: "+ _sheenCtrl = AnimationController(vsync: this, duration: sheenDuration);" + - row "1106 +" [ref=e3559]: + - cell [ref=e3560] [cursor=pointer] + - cell "1106" [ref=e3561] [cursor=pointer] + - cell "+" [ref=e3562]: + - generic: + + - 'row "1107 + if (widget.owned) {" [ref=e3563]': + - cell [ref=e3564] [cursor=pointer] + - cell "1107" [ref=e3565] [cursor=pointer] + - 'cell "+ if (widget.owned) {" [ref=e3566]': + - generic [ref=e3567]: "+ if (widget.owned) {" + - row "1108 + final delayFraction =" [ref=e3568]: + - cell [ref=e3569] [cursor=pointer] + - cell "1108" [ref=e3570] [cursor=pointer] + - cell "+ final delayFraction =" [ref=e3571]: + - generic [ref=e3572]: + final delayFraction = + - row "1109 + (widget.index * 300 / sheenDuration.inMilliseconds).clamp(0.0, 1.0);" [ref=e3573]: + - cell [ref=e3574] [cursor=pointer] + - cell "1109" [ref=e3575] [cursor=pointer] + - cell "+ (widget.index * 300 / sheenDuration.inMilliseconds).clamp(0.0, 1.0);" [ref=e3576]: + - generic [ref=e3577]: + (widget.index * 300 / sheenDuration.inMilliseconds).clamp(0.0, 1.0); + - 'row "1110 + _sheenCtrl.forward(from: delayFraction);" [ref=e3578]': + - cell [ref=e3579] [cursor=pointer] + - cell "1110" [ref=e3580] [cursor=pointer] + - 'cell "+ _sheenCtrl.forward(from: delayFraction);" [ref=e3581]': + - generic [ref=e3582]: "+ _sheenCtrl.forward(from: delayFraction);" + - 'row "1111 + _sheenCtrl.addStatusListener((s) {" [ref=e3583]': + - cell [ref=e3584] [cursor=pointer] + - cell "1111" [ref=e3585] [cursor=pointer] + - 'cell "+ _sheenCtrl.addStatusListener((s) {" [ref=e3586]': + - generic [ref=e3587]: "+ _sheenCtrl.addStatusListener((s) {" + - row "1112 + if (s == AnimationStatus.completed && mounted) _sheenCtrl.repeat();" [ref=e3588]: + - cell [ref=e3589] [cursor=pointer] + - cell "1112" [ref=e3590] [cursor=pointer] + - cell "+ if (s == AnimationStatus.completed && mounted) _sheenCtrl.repeat();" [ref=e3591]: + - generic [ref=e3592]: + if (s == AnimationStatus.completed && mounted) _sheenCtrl.repeat(); + - 'row "1113 + });" [ref=e3593]': + - cell [ref=e3594] [cursor=pointer] + - cell "1113" [ref=e3595] [cursor=pointer] + - 'cell "+ });" [ref=e3596]': + - generic [ref=e3597]: "+ });" + - 'row "1114 + }" [ref=e3598]': + - cell [ref=e3599] [cursor=pointer] + - cell "1114" [ref=e3600] [cursor=pointer] + - 'cell "+ }" [ref=e3601]': + - generic [ref=e3602]: "+ }" + - 'row "1115 + }" [ref=e3603]': + - cell [ref=e3604] [cursor=pointer] + - cell "1115" [ref=e3605] [cursor=pointer] + - 'cell "+ }" [ref=e3606]': + - generic [ref=e3607]: "+ }" + - generic [ref=e3609]: + - generic [ref=e3610]: + - button "Comment thread" [expanded] [ref=e3611] [cursor=pointer]: + - img + - link ".vercel/project.json" [ref=e3613] [cursor=pointer]: + - /url: /CodeStorm-Hub/petfolio/pull/17/files/610ee87712bfd0f063f722d6cadca2a5289430cb#diff-e2c270517efd9fd0ee81fc3bd3b0a169e5172ec31da017dc7c44eec97b21ae1a + - generic [ref=e3614]: + - generic [ref=e3616]: Comment on lines +1 to +4 + - table [ref=e3619]: + - rowgroup [ref=e3620]: + - 'row "1 + {" [ref=e3621]': + - cell [ref=e3622] [cursor=pointer] + - cell "1" [ref=e3623] [cursor=pointer] + - 'cell "+ {" [ref=e3624]': + - generic [ref=e3625]: "+ {" + - 'row "2 + \"orgId\": \"team_lC8aTJK0XiU9qDfaHeTfCJs6\"," [ref=e3626]': + - cell [ref=e3627] [cursor=pointer] + - cell "2" [ref=e3628] [cursor=pointer] + - 'cell "+ \"orgId\": \"team_lC8aTJK0XiU9qDfaHeTfCJs6\"," [ref=e3629]': + - generic [ref=e3630]: "+ \"orgId\": \"team_lC8aTJK0XiU9qDfaHeTfCJs6\"," + - 'row "3 + \"projectId\": \"prj_hMHouLWimZvr5dDOlZeAhbH8xtop\"" [ref=e3631]': + - cell [ref=e3632] [cursor=pointer] + - cell "3" [ref=e3633] [cursor=pointer] + - 'cell "+ \"projectId\": \"prj_hMHouLWimZvr5dDOlZeAhbH8xtop\"" [ref=e3634]': + - generic [ref=e3635]: "+ \"projectId\": \"prj_hMHouLWimZvr5dDOlZeAhbH8xtop\"" + - 'row "4 + }" [ref=e3636]': + - cell [ref=e3637] [cursor=pointer] + - cell "4" [ref=e3638] [cursor=pointer] + - 'cell "+ }" [ref=e3639]': + - generic [ref=e3640]: "+ }" + - generic [ref=e3642]: + - generic [ref=e3643]: + - button "Comment thread" [expanded] [ref=e3644] [cursor=pointer]: + - img + - link "lib/core/services/notification_service.dart" [ref=e3646] [cursor=pointer]: + - /url: /CodeStorm-Hub/petfolio/pull/17/files/610ee87712bfd0f063f722d6cadca2a5289430cb#diff-5c208823dc1ba76bc93934240d965eb00a4f4b509e8117bd93193e0f9d489b6b + - generic [ref=e3647]: + - generic [ref=e3649]: Comment on lines 26 to 28 + - table [ref=e3652]: + - rowgroup [ref=e3653]: + - row "26 26 await _plugin.initialize(" [ref=e3654]: + - cell "26" [ref=e3655] [cursor=pointer] + - cell "26" [ref=e3656] [cursor=pointer] + - cell "await _plugin.initialize(" [ref=e3657]: + - generic [ref=e3658]: await _plugin.initialize( + - 'row "27 - const InitializationSettings(android: androidSettings, iOS: iosSettings)," [ref=e3659]': + - cell "27" [ref=e3660] [cursor=pointer] + - cell [ref=e3661] [cursor=pointer] + - 'cell "- const InitializationSettings(android: androidSettings, iOS: iosSettings)," [ref=e3662]': + - generic [ref=e3663]: "- const InitializationSettings(android: androidSettings, iOS: iosSettings)," + - 'row "27 + settings: const InitializationSettings(android: androidSettings, iOS: iosSettings)," [ref=e3664]': + - cell [ref=e3665] [cursor=pointer] + - cell "27" [ref=e3666] [cursor=pointer] + - 'cell "+ settings: const InitializationSettings(android: androidSettings, iOS: iosSettings)," [ref=e3667]': + - generic [ref=e3668]: "+ settings: const InitializationSettings(android: androidSettings, iOS: iosSettings)," + - row "28 28 );" [ref=e3669]: + - cell "28" [ref=e3670] [cursor=pointer] + - cell "28" [ref=e3671] [cursor=pointer] + - cell ");" [ref=e3672]: + - generic [ref=e3673]: ); + - generic [ref=e3675]: + - generic [ref=e3676]: + - button "Comment thread" [expanded] [ref=e3677] [cursor=pointer]: + - img + - link "lib/core/services/notification_service.dart" [ref=e3679] [cursor=pointer]: + - /url: /CodeStorm-Hub/petfolio/pull/17/files/610ee87712bfd0f063f722d6cadca2a5289430cb#diff-5c208823dc1ba76bc93934240d965eb00a4f4b509e8117bd93193e0f9d489b6b + - generic [ref=e3680]: + - generic [ref=e3682]: Comment on lines 78 to +83 + - table [ref=e3685]: + - rowgroup [ref=e3686]: + - row "78 78 await _plugin.zonedSchedule(" [ref=e3687]: + - cell "78" [ref=e3688] [cursor=pointer] + - cell "78" [ref=e3689] [cursor=pointer] + - cell "await _plugin.zonedSchedule(" [ref=e3690]: + - generic [ref=e3691]: await _plugin.zonedSchedule( + - row "79 - _idFor(taskId)," [ref=e3692]: + - cell "79" [ref=e3693] [cursor=pointer] + - cell [ref=e3694] [cursor=pointer] + - cell "- _idFor(taskId)," [ref=e3695]: + - generic [ref=e3696]: "- _idFor(taskId)," + - row "80 - 'Care Reminder'," [ref=e3697]: + - cell "80" [ref=e3698] [cursor=pointer] + - cell [ref=e3699] [cursor=pointer] + - cell "- 'Care Reminder'," [ref=e3700]: + - generic [ref=e3701]: "- 'Care Reminder'," + - row "81 - title," [ref=e3702]: + - cell "81" [ref=e3703] [cursor=pointer] + - cell [ref=e3704] [cursor=pointer] + - cell "- title," [ref=e3705]: + - generic [ref=e3706]: "- title," + - row "82 - scheduled," [ref=e3707]: + - cell "82" [ref=e3708] [cursor=pointer] + - cell [ref=e3709] [cursor=pointer] + - cell "- scheduled," [ref=e3710]: + - generic [ref=e3711]: "- scheduled," + - 'row "83 - NotificationDetails(android: androidDetails)," [ref=e3712]': + - cell "83" [ref=e3713] [cursor=pointer] + - cell [ref=e3714] [cursor=pointer] + - 'cell "- NotificationDetails(android: androidDetails)," [ref=e3715]': + - generic [ref=e3716]: "- NotificationDetails(android: androidDetails)," + - 'row "79 + id: _idFor(taskId)," [ref=e3717]': + - cell [ref=e3718] [cursor=pointer] + - cell "79" [ref=e3719] [cursor=pointer] + - 'cell "+ id: _idFor(taskId)," [ref=e3720]': + - generic [ref=e3721]: "+ id: _idFor(taskId)," + - 'row "80 + title: ''Care Reminder''," [ref=e3722]': + - cell [ref=e3723] [cursor=pointer] + - cell "80" [ref=e3724] [cursor=pointer] + - 'cell "+ title: ''Care Reminder''," [ref=e3725]': + - generic [ref=e3726]: "+ title: 'Care Reminder'," + - 'row "81 + body: title," [ref=e3727]': + - cell [ref=e3728] [cursor=pointer] + - cell "81" [ref=e3729] [cursor=pointer] + - 'cell "+ body: title," [ref=e3730]': + - generic [ref=e3731]: "+ body: title," + - 'row "82 + scheduledDate: scheduled," [ref=e3732]': + - cell [ref=e3733] [cursor=pointer] + - cell "82" [ref=e3734] [cursor=pointer] + - 'cell "+ scheduledDate: scheduled," [ref=e3735]': + - generic [ref=e3736]: "+ scheduledDate: scheduled," + - 'row "83 + notificationDetails: NotificationDetails(android: androidDetails)," [ref=e3737]': + - cell [ref=e3738] [cursor=pointer] + - cell "83" [ref=e3739] [cursor=pointer] + - 'cell "+ notificationDetails: NotificationDetails(android: androidDetails)," [ref=e3740]': + - generic [ref=e3741]: "+ notificationDetails: NotificationDetails(android: androidDetails)," + - generic [ref=e3743]: + - generic [ref=e3744]: + - button "Comment thread" [expanded] [ref=e3745] [cursor=pointer]: + - img + - link "lib/features/care/presentation/widgets/gamified_care_ui.dart" [ref=e3747] [cursor=pointer]: + - /url: /CodeStorm-Hub/petfolio/pull/17/files/610ee87712bfd0f063f722d6cadca2a5289430cb#diff-8e2cf796cdb83ed78c034ccd2f1ab099667749421148c7f255df9b4f1317c508 + - generic [ref=e3748]: + - generic [ref=e3750]: Comment on lines +71 to +74 + - table [ref=e3753]: + - rowgroup [ref=e3754]: + - row "71 + final tasks = widget.dashboard.tasks.value ?? [];" [ref=e3755]: + - cell [ref=e3756] [cursor=pointer] + - cell "71" [ref=e3757] [cursor=pointer] + - cell "+ final tasks = widget.dashboard.tasks.value ?? [];" [ref=e3758]: + - generic [ref=e3759]: + final tasks = widget.dashboard.tasks.value ?? []; + - row "72 + final planned = tasks.where((t) => !t.isLogDerived).toList();" [ref=e3760]: + - cell [ref=e3761] [cursor=pointer] + - cell "72" [ref=e3762] [cursor=pointer] + - cell "+ final planned = tasks.where((t) => !t.isLogDerived).toList();" [ref=e3763]: + - generic [ref=e3764]: + final planned = tasks.where((t) => !t.isLogDerived).toList(); + - row "73 + final doneToday = planned.where((t) => t.isCompleted).length;" [ref=e3765]: + - cell [ref=e3766] [cursor=pointer] + - cell "73" [ref=e3767] [cursor=pointer] + - cell "+ final doneToday = planned.where((t) => t.isCompleted).length;" [ref=e3768]: + - generic [ref=e3769]: + final doneToday = planned.where((t) => t.isCompleted).length; + - row "74 + final totalToday = planned.length;" [ref=e3770]: + - cell [ref=e3771] [cursor=pointer] + - cell "74" [ref=e3772] [cursor=pointer] + - cell "+ final totalToday = planned.length;" [ref=e3773]: + - generic [ref=e3774]: + final totalToday = planned.length; + - generic [ref=e3778]: + - img [ref=e3780] + - generic [ref=e3785]: + - link "@afsan123" [ref=e3788] [cursor=pointer]: + - /url: /afsan123 + - img "@afsan123" [ref=e3789] + - code [ref=e3791]: + - link "up" [ref=e3792] [cursor=pointer]: + - /url: /CodeStorm-Hub/petfolio/pull/17/commits/dcea9d5a22080e5be1ac8acebb3cb764362f5b75 + - group [ref=e3796]: + - generic "2 / 2 checks OK" [ref=e3797] [cursor=pointer]: + - img "2 / 2 checks OK" [ref=e3798] + - code [ref=e3801]: + - link "dcea9d5" [ref=e3802] [cursor=pointer]: + - /url: /CodeStorm-Hub/petfolio/pull/17/commits/dcea9d5a22080e5be1ac8acebb3cb764362f5b75 + - generic [ref=e3806]: + - link "Sign up for free" [ref=e3807] [cursor=pointer]: + - /url: /join?source=comment-repo + - strong [ref=e3808]: to join this conversation on GitHub + - text: . Already have an account? + - link "Sign in to comment" [ref=e3809] [cursor=pointer]: + - /url: /login?return_to=https%3A%2F%2Fgithub.com%2FCodeStorm-Hub%2Fpetfolio%2Fpull%2F17 + - generic [ref=e3813]: + - form "Select reviewers" [ref=e3815]: + - heading "Reviewers" [level=3] [ref=e3816] + - paragraph [ref=e3818]: + - generic [ref=e3819]: + - link "Copilot code review" [ref=e3820] [cursor=pointer]: + - /url: /apps/copilot-pull-request-reviewer + - generic [ref=e3821]: Copilot code review + - img [ref=e3823] + - link "Copilot" [ref=e3826] [cursor=pointer]: + - /url: /apps/copilot-pull-request-reviewer + - link "Copilot left review comments" [ref=e3827] [cursor=pointer]: + - /url: /CodeStorm-Hub/petfolio/pull/17/files/610ee87712bfd0f063f722d6cadca2a5289430cb + - img [ref=e3829] + - form "Select assignees" [ref=e3832]: + - heading "Assignees" [level=3] [ref=e3833] + - text: No one assigned + - generic [ref=e3834]: + - heading "Labels" [level=3] [ref=e3835] + - generic [ref=e3836]: None yet + - form "Select projects" [ref=e3838]: + - heading "Projects" [level=3] [ref=e3839] + - text: None yet + - form "Select milestones" [ref=e3841]: + - heading "Milestone" [level=3] [ref=e3842] + - text: No milestone + - form "Link issues" [ref=e3848]: + - heading "Development" [level=3] [ref=e3849] + - paragraph [ref=e3850]: Successfully merging this pull request may close these issues. + - paragraph [ref=e3852]: None yet + - generic [ref=e3854]: + - heading "3 participants" [level=3] [ref=e3855] + - generic [ref=e3856]: + - link "@syed-reza98" [ref=e3857] [cursor=pointer]: + - /url: /syed-reza98 + - img "@syed-reza98" [ref=e3858] + - link [ref=e3859] [cursor=pointer]: + - /url: /apps/copilot-pull-request-reviewer + - img [ref=e3861] + - link "@afsan123" [ref=e3864] [cursor=pointer]: + - /url: /afsan123 + - img "@afsan123" [ref=e3865] + - contentinfo [ref=e3866]: + - heading "Footer" [level=2] [ref=e3867] + - generic [ref=e3868]: + - generic [ref=e3869]: + - link "GitHub Homepage" [ref=e3870] [cursor=pointer]: + - /url: https://github.com + - img [ref=e3871] + - generic [ref=e3873]: © 2026 GitHub, Inc. + - navigation "Footer" [ref=e3874]: + - heading "Footer navigation" [level=3] [ref=e3875] + - list "Footer navigation" [ref=e3876]: + - listitem [ref=e3877]: + - link "Terms" [ref=e3878] [cursor=pointer]: + - /url: https://docs.github.com/site-policy/github-terms/github-terms-of-service + - listitem [ref=e3879]: + - link "Privacy" [ref=e3880] [cursor=pointer]: + - /url: https://docs.github.com/site-policy/privacy-policies/github-privacy-statement + - listitem [ref=e3881]: + - link "Security" [ref=e3882] [cursor=pointer]: + - /url: https://github.com/security + - listitem [ref=e3883]: + - link "Status" [ref=e3884] [cursor=pointer]: + - /url: https://www.githubstatus.com/ + - listitem [ref=e3885]: + - link "Community" [ref=e3886] [cursor=pointer]: + - /url: https://github.community/ + - listitem [ref=e3887]: + - link "Docs" [ref=e3888] [cursor=pointer]: + - /url: https://docs.github.com/ + - listitem [ref=e3889]: + - link "Contact" [ref=e3890] [cursor=pointer]: + - /url: https://support.github.com?tags=dotcom-footer + - listitem [ref=e3891]: + - button "Manage cookies" [ref=e3893] [cursor=pointer] + - listitem [ref=e3894]: + - button "Do not share my personal information" [ref=e3896] [cursor=pointer] \ No newline at end of file diff --git a/.vercel/project.json b/.vercel/project.json new file mode 100644 index 0000000..39cd109 --- /dev/null +++ b/.vercel/project.json @@ -0,0 +1,4 @@ +{ + "orgId": "team_lC8aTJK0XiU9qDfaHeTfCJs6", + "projectId": "prj_hMHouLWimZvr5dDOlZeAhbH8xtop" +} diff --git a/PetFolio Redesign/Care Redesign/Care Redesign.html b/PetFolio Redesign/Care Redesign/Care Redesign.html new file mode 100644 index 0000000..2da2638 --- /dev/null +++ b/PetFolio Redesign/Care Redesign/Care Redesign.html @@ -0,0 +1,167 @@ + + + + +PetFolio — Care · Redesign + + + + + + + +
+
+
+ + + + + + + + + + + diff --git a/PetFolio Redesign/Care Redesign/care_screen.jsx b/PetFolio Redesign/Care Redesign/care_screen.jsx new file mode 100644 index 0000000..082e200 --- /dev/null +++ b/PetFolio Redesign/Care Redesign/care_screen.jsx @@ -0,0 +1,761 @@ +// care3d.jsx — PetFolio Care screen, compact redesign + 3D depth +const { useState, useRef, useEffect, useContext, createContext, useCallback } = React; + +// ───────────────────────────────────────────────────────────── +// Theme / depth context (self-contained tweaks) +// ───────────────────────────────────────────────────────────── +const DepthCtx = createContext(1); + +// ───────────────────────────────────────────────────────────── +// 3D tilt hook — pointer/drag driven rotateX/Y + glare position +// ───────────────────────────────────────────────────────────── +function useTilt({ max = 9, lift = 1.0 } = {}) { + const depth = useContext(DepthCtx); + const ref = useRef(null); + const [t, setT] = useState({ rx: 0, ry: 0, on: false, px: 50, py: 50, press: false }); + + const move = useCallback((e) => { + const el = ref.current; if (!el || depth === 0) return; + const r = el.getBoundingClientRect(); + const x = Math.min(1, Math.max(0, (e.clientX - r.left) / r.width)); + const y = Math.min(1, Math.max(0, (e.clientY - r.top) / r.height)); + const m = max * depth; + setT(s => ({ ...s, rx: (0.5 - y) * m * 2, ry: (x - 0.5) * m * 2, on: true, px: x * 100, py: y * 100 })); + }, [depth, max]); + + const leave = useCallback(() => setT(s => ({ ...s, rx: 0, ry: 0, on: false, px: 50, py: 50, press: false })), []); + const down = useCallback(() => setT(s => ({ ...s, press: true })), []); + const up = useCallback(() => setT(s => ({ ...s, press: false })), []); + + const transform = depth === 0 + ? (t.press ? 'scale(0.97)' : 'none') + : `perspective(640px) rotateX(${t.rx}deg) rotateY(${t.ry}deg) translateZ(0) scale(${t.press ? 0.965 : (t.on ? 1.012 * lift : 1)})`; + + const bind = { + ref, + onPointerMove: move, onPointerLeave: leave, onPointerDown: down, + onPointerUp: up, onPointerCancel: leave, + }; + return { bind, t, transform }; +} + +// Glare overlay used inside tilt cards +function Glare({ t, r = 24, strength = 0.5 }) { + const depth = useContext(DepthCtx); + if (depth === 0) return null; + return ( +
+ ); +} + +// ───────────────────────────────────────────────────────────── +// Tiny icon set +// ───────────────────────────────────────────────────────────── +const Ico = { + paw: (s = 22, f = 'currentColor') => (), + pawO: (s = 22, c = 'currentColor') => (), + heartO: (s = 22, c = 'currentColor') => (), + flame: (s = 22, c = 'currentColor') => (), + bone: (s = 22, c = 'currentColor') => (), + cart: (s = 22, c = 'currentColor') => (), + check: (s = 22, c = '#fff') => (), + chevDown: (s = 16, c = 'currentColor') => (), + chevR: (s = 16, c = 'currentColor') => (), + lock: (s = 12, c = 'currentColor') => (), + moon: (s = 20, c = 'currentColor') => (), + sun: (s = 20, c = 'currentColor') => (), + sparkle: (s = 18, c = 'currentColor') => (), + refresh: (s = 16, c = 'currentColor') => (), + scale: (s = 22, c = 'currentColor') => (), + vault: (s = 22, c = 'currentColor') => (), +}; + +// ───────────────────────────────────────────────────────────── +// Pet avatar (gradient disc + emoji + species ring) +// ───────────────────────────────────────────────────────────── +function Avatar({ size = 40, ring = true, emoji = '🐱', soft = 'var(--poppy-soft)', color = 'var(--poppy)' }) { + const inner = size - (ring ? 6 : 0); + return ( +
+
{emoji}
+
+ ); +} + +// ───────────────────────────────────────────────────────────── +// Section label +// ───────────────────────────────────────────────────────────── +function SectionLabel({ accent, children, right }) { + return ( +
+
+
+

{children}

+
+ {right} +
+ ); +} + +// ───────────────────────────────────────────────────────────── +// 3D streak flame medallion +// ───────────────────────────────────────────────────────────── +function StreakCoin({ streak }) { + const depth = useContext(DepthCtx); + return ( +
+
+
+
+
+
+
+
🔥
+
{streak}
+
DAY STREAK
+
+
+
+ ); +} + +// ───────────────────────────────────────────────────────────── +// Slim hero (streak + level + XP) — replaces tall wave header +// ───────────────────────────────────────────────────────────── +function Hero({ pet, doneToday, totalToday }) { + const pct = (pet.xp / pet.xpMax) * 100; + return ( +
+ {/* decorative paws */} +
{Ico.paw(96, '#fff')}
+
+ +
+
+
+ Lv {pet.level} + · {pet.title} +
+ {doneToday}/{totalToday} today +
+
+
+
+
+
+
{pet.xp} / {pet.xpMax} XP · {pet.xpMax - pet.xp} XP to Lv {pet.level + 1}
+
+
+
+ ); +} + +// ───────────────────────────────────────────────────────────── +// Compact horizontal date strip +// ───────────────────────────────────────────────────────────── +function DateStrip() { + const days = [ + { l: 'T', n: 26 }, { l: 'W', n: 27 }, { l: 'T', n: 28 }, { l: 'F', n: 29 }, + { l: 'S', n: 30, hit: true }, { l: 'S', n: 31, hit: true }, { l: 'M', n: 1, today: true }, + { l: 'T', n: 2, fut: true }, { l: 'W', n: 3, fut: true }, + ]; + return ( +
+ {days.map((d, i) => ( +
+ {d.l} + {d.n} + {d.hit && !d.today ?
: + d.today ?
:
} +
+ ))} +
+ ); +} + +// ───────────────────────────────────────────────────────────── +// Pet-character SVG icons for each badge +// ───────────────────────────────────────────────────────────── +const BIco = { + // Paw print + star burst + firstLog: (s=36) => ( + + + + + + + + + + ), + // Cat face with flame crown + streak3: (s=36) => ( + + {/* flame */} + + {/* cat head */} + + {/* ears */} + + + {/* inner ears */} + + + {/* eyes */} + + + + + {/* nose + mouth */} + + + + ), + // Dog face with superhero star + hero7: (s=36) => ( + + {/* star badge */} + + {/* dog head */} + + {/* floppy ears */} + + + {/* eyes */} + + + + + {/* snout */} + + + + ), + // Clipboard with paw checkmark + routineMaster: (s=36) => ( + + + + {/* paw checkmark rows */} + + + + + + ), + // Crown with paw-print peak tips + legend30: (s=36) => ( + + {/* crown base */} + + {/* crown body */} + + {/* paw tips on peaks */} + + + + {/* toe beans */} + + + + + + + {/* gem */} + + + + ), + // Trophy with heart-paw + champion: (s=36) => ( + + {/* trophy cup */} + + {/* handles */} + + {/* stem */} + + + {/* heart-paw in cup */} + + + + + ), +}; + +// Badge data with custom rim colors and float timing +const BADGES = [ + { id:'firstLog', l:'First Log', c:'var(--mint)', rim:'#1a9970', owned:true, hint:'unlocked', delay:0, dur:3.4 }, + { id:'streak3', l:'3-Day Streak', c:'var(--tangerine)', rim:'#b85a1a', owned:false, hint:'1/3 days', delay:0.5, dur:3.8 }, + { id:'hero7', l:'7-Day Hero', c:'var(--poppy)', rim:'#8a1010', owned:false, hint:'1/7 days', delay:0.9, dur:3.2 }, + { id:'routineMaster', l:'Routine Pro', c:'var(--sunny)', rim:'#9a6500', owned:false, hint:'1/14 days', delay:0.3, dur:4.0 }, + { id:'legend30', l:'30-Day Legend', c:'var(--lilac)', rim:'#4a2fa0', owned:false, hint:'1/30 days', delay:0.7, dur:3.6 }, + { id:'champion', l:'Care Champ', c:'var(--sky)', rim:'#2060a8', owned:false, hint:'6/100 logs',delay:0.15, dur:3.5 }, +]; + +const ICON_MAP = { + firstLog: BIco.firstLog, streak3: BIco.streak3, hero7: BIco.hero7, + routineMaster: BIco.routineMaster, legend30: BIco.legend30, champion: BIco.champion, +}; + +// ───────────────────────────────────────────────────────────── +// 3D Trophy badge card — medal-style, floating animation +// ───────────────────────────────────────────────────────────── +function BadgeMedal({ b, idx, onTap }) { + const depth = useContext(DepthCtx); + const { bind, t, transform } = useTilt({ max: 16, lift: 1.06 }); + const IconComponent = ICON_MAP[b.id]; + const floatAnim = depth ? `pf-badge-float ${b.dur}s ease-in-out ${b.delay}s infinite` : 'none'; + + return ( +
onTap(b)} style={{ cursor: 'pointer', display: 'flex', flexDirection: 'column', alignItems: 'center', gap: 5, animation: floatAnim }} > + {/* tilt wrapper */} +
+ {/* outer glow ring (owned only) */} + {b.owned && ( +
+ )} + {/* medal body */} +
+ {/* concentric ring for depth */} +
+
+ {/* icon */} +
+ {IconComponent ? : {b.e}} +
+ {/* holographic sheen (owned) */} + {b.owned && ( +
+ )} + {/* lock pip */} + {!b.owned && ( +
{Ico.lock(9)}
+ )} + +
+
+ {/* label */} +
{b.l}
+
{b.owned ? '✓ earned' : b.hint}
+
+ ); +} + +function TrophyCarousel({ onTap }) { + return ( +
+ {BADGES.map((b, i) => )} +
+ ); +} + +// ───────────────────────────────────────────────────────────── +// Quest task card — denser 2-line, tilt + press-depth + confetti +// ───────────────────────────────────────────────────────────── +const TASK_META = { + feeding: { e: '🥩', c: 'var(--tangerine)', soft: 'var(--tangerine-soft)' }, + playtime: { e: '🎾', c: 'var(--sunny)', soft: 'var(--sunny-soft)' }, + training: { e: '🎓', c: 'var(--poppy)', soft: 'var(--poppy-soft)' }, + walk: { e: '🦮', c: 'var(--mint)', soft: 'var(--mint-soft)' }, +}; + +function TaskCard({ task, onToggle }) { + const { bind, t, transform } = useTilt({ max: 6 }); + const m = TASK_META[task.type] || TASK_META.feeding; + const done = task.done; + return ( +
+
+
{done ? Ico.check(22, '#fff') : m.e}
+
+
{task.title}
+
{done ? 'Completed' : (task.due ? `Due ${task.time}` : task.time)}
+
+
+ +{task.xp} + +
+ +
+
+ ); +} + +function FreqDivider({ label }) { + return ( +
+
+ {label} +
+
+ ); +} + +// ───────────────────────────────────────────────────────────── +// Compact weekly chart +// ───────────────────────────────────────────────────────────── +function WeeklyChart() { + const bars = [ + { l: 'T', n: 26, h: 0.3, miss: true }, { l: 'W', n: 27, h: 0.3, miss: true }, + { l: 'T', n: 28, h: 0.3, miss: true }, { l: 'F', n: 29, h: 0.3, miss: true }, + { l: 'S', n: 30, h: 0.3, miss: true }, { l: 'S', n: 31, h: 0.85, c: 'var(--mint)' }, + { l: 'M', n: 1, h: 0.16, c: 'var(--poppy)', today: true }, + ]; + return ( +
+
+ 1 / 7 goals this week + 1 🔥 +
+
+ {bars.map((b, i) => ( +
+ {b.today && 🐾} +
+ {b.l} + {b.n} +
+ ))} +
+
+ ); +} + +// ───────────────────────────────────────────────────────────── +// Merged utility banner (Nutrition | Medical Vault) +// ───────────────────────────────────────────────────────────── +function UtilityBanner() { + const half = (icon, bg, icColor, title, line1, line2) => ( +
+
+
{icon}
+ {Ico.chevR(15, 'var(--ink-300)')} +
+
+
{title}
+
{line1}
+
{line2}
+
+
+ ); + return ( +
+ {half(Ico.scale(20), 'var(--sunny-soft)', 'var(--sunny-700)', 'Nutrition', '4.2 kg · May 28', '~280 kcal / day')} +
+ {half(Ico.vault(20), 'var(--mint-soft)', 'var(--mint-700)', 'Medical Vault', '1 due soon', 'Checkup · 3 wks')} +
+ ); +} + +// ───────────────────────────────────────────────────────────── +// Bottom nav +// ───────────────────────────────────────────────────────────── +function BottomNav() { + const tabs = [ + { id: 'pets', label: 'Pets', icon: Ico.pawO, c: 'var(--tangerine)' }, + { id: 'care', label: 'Care', icon: Ico.flame, c: 'var(--tangerine-700)', active: true }, + { id: 'social', label: 'Social', icon: Ico.heartO, c: 'var(--poppy)' }, + { id: 'match', label: 'Match', icon: Ico.bone, c: 'var(--lilac)' }, + { id: 'market', label: 'Market', icon: Ico.cart, c: 'var(--mint-700)' }, + ]; + return ( +
+ {tabs.map(t => ( + + ))} +
+ ); +} + +// ───────────────────────────────────────────────────────────── +// Confetti + XP burst overlay +// ───────────────────────────────────────────────────────────── +function Burst({ burst }) { + if (!burst) return null; + const colors = ['var(--tangerine)', 'var(--poppy)', 'var(--mint)', 'var(--sunny)', 'var(--lilac)']; + return ( +
+
+{burst.xp} XP
+ {Array.from({ length: 14 }).map((_, i) => { + const dx = (Math.random() - 0.5) * 130; + const rot = Math.random() * 720; + const dl = Math.random() * 120; + const c = colors[i % colors.length]; + return
; + })} +
+ ); +} + +// ───────────────────────────────────────────────────────────── +// Badge detail sheet +// ───────────────────────────────────────────────────────────── +function BadgeSheet({ badge, onClose }) { + if (!badge) return null; + return ( +
+
e.stopPropagation()} style={{ width: '100%', background: 'var(--surface)', borderRadius: '28px 28px 0 0', padding: '12px 22px 34px', boxShadow: '0 -10px 40px rgba(0,0,0,0.2)' }}> +
+
+
+ {(() => { const IC = ICON_MAP[badge.id]; return IC ? : null; })()} +
+
{badge.l}
+
{badge.owned ? 'You unlocked this badge — nice work keeping up the routine!' : 'Keep logging care to unlock this badge.'}
+
{badge.owned ? '✅ Earned!' : `Progress: ${badge.hint}`}
+
+
+
+ ); +} + +// ───────────────────────────────────────────────────────────── +// Tweaks panel (self-contained) +// ───────────────────────────────────────────────────────────── +function TweaksPanel({ open, onClose, depth, setDepth, dark, setDark, reduce, setReduce }) { + if (!open) return null; + const seg = (val, cur, set, opts) => ( +
+ {opts.map(o => ( + + ))} +
+ ); + return ( +
+
e.stopPropagation()} style={{ width: '100%', background: 'var(--surface)', borderRadius: '28px 28px 0 0', padding: '12px 20px 30px' }}> +
+
Tweaks
+
+
3D depth
{seg(depth, depth, setDepth, [{ v: 0, l: 'Off' }, { v: 0.5, l: 'Subtle' }, { v: 1, l: 'Full' }])}
+
Appearance
{seg(dark, dark, setDark, [{ v: false, l: '☀︎ Light' }, { v: true, l: '☾ Dark' }])}
+
Motion
{seg(reduce, reduce, setReduce, [{ v: false, l: 'Animated' }, { v: true, l: 'Reduced' }])}
+
+
+
+ ); +} + +// ───────────────────────────────────────────────────────────── +// Care screen +// ───────────────────────────────────────────────────────────── +const PET = { name: 'Jhontu', level: 2, title: 'Curious Pup', xp: 100, xpMax: 250, streak: 1 }; +const INITIAL_TASKS = [ + { id: 't1', title: 'Morning Feeding', type: 'feeding', time: '8:00 AM', xp: 12, due: true, done: false, freq: 'daily' }, + { id: 't2', title: 'Evening Feeding', type: 'feeding', time: '6:00 PM', xp: 12, due: true, done: false, freq: 'daily' }, + { id: 't3', title: 'Interactive Playtime', type: 'playtime', time: 'Daily', xp: 20, done: false, freq: 'daily' }, + { id: 't4', title: 'Clicker Training', type: 'training', time: 'As needed', xp: 15, done: false, freq: 'less' }, + { id: 't5', title: 'Laser Pointer Chase', type: 'playtime', time: 'As needed', xp: 15, done: false, freq: 'less' }, +]; + +function CareScreen({ onSparkle, onMoon, dark }) { + const [tasks, setTasks] = useState(INITIAL_TASKS); + const [burst, setBurst] = useState(null); + const [badge, setBadge] = useState(null); + const scrollRef = useRef(null); + + const daily = tasks.filter(t => t.freq === 'daily'); + const less = tasks.filter(t => t.freq === 'less'); + const doneToday = daily.filter(t => t.done).length; + + function toggle(task, btnEl) { + const nowDone = !task.done; + setTasks(ts => ts.map(t => t.id === task.id ? { ...t, done: nowDone, due: nowDone ? false : t.due } : t)); + if (nowDone && btnEl && scrollRef.current) { + const br = btnEl.getBoundingClientRect(); + const pr = scrollRef.current.getBoundingClientRect(); + setBurst({ x: br.left - pr.left + br.width / 2 - 20, y: br.top - pr.top + scrollRef.current.scrollTop - 4, xp: task.xp }); + setTimeout(() => setBurst(null), 1100); + } + } + + return ( +
+
+ {/* App bar */} +
+ +
+ + +
+
+ + + +
+ + +
+
+ Vault {Ico.chevR(13, 'var(--lilac-700)')}}> + Trophy room + +
+
+
1 / 6 earned
+ +
+ +
+
+
+ TODAY'S QUESTS +
+ {doneToday === daily.length ? 'All done! 🎉' : `${doneToday}/${daily.length} done`} + +
+
+ + {daily.map(t => )} + + {less.map(t => )} +
+ +
+
+ This week + +
+ +
+
+ +
+ + +
+ + + setBadge(null)}/> +
+ ); +} + +// ───────────────────────────────────────────────────────────── +// Root +// ───────────────────────────────────────────────────────────── +function App() { + const [depth, setDepth] = useState(1); + const [dark, setDark] = useState(false); + const [reduce, setReduce] = useState(false); + const [tweaksOpen, setTweaksOpen] = useState(false); + + useEffect(() => { + document.documentElement.setAttribute('data-theme', dark ? 'dark' : 'light'); + }, [dark]); + useEffect(() => { + document.body.classList.toggle('reduce-motion', reduce); + }, [reduce]); + + return ( + + + setTweaksOpen(true)} onMoon={() => setDark(d => !d)} dark={dark}/> + setTweaksOpen(false)} depth={depth} setDepth={setDepth} dark={dark} setDark={setDark} reduce={reduce} setReduce={setReduce}/> + + + ); +} + +ReactDOM.createRoot(document.getElementById('root')).render(); diff --git a/PetFolio Redesign/Care Redesign/ios-frame.jsx b/PetFolio Redesign/Care Redesign/ios-frame.jsx new file mode 100644 index 0000000..8703a44 --- /dev/null +++ b/PetFolio Redesign/Care Redesign/ios-frame.jsx @@ -0,0 +1,348 @@ + +/* BEGIN USAGE */ +// iOS.jsx — Simplified iOS 26 (Liquid Glass) device frame +// Based on the iOS 26 UI Kit + Figma status bar spec. No assets, no deps. +// Exports (to window): IOSDevice, IOSStatusBar, IOSNavBar, IOSGlassPill, IOSList, IOSListRow, IOSKeyboard +// +// Usage — wrap your screen content in to get the bezel, status bar +// and home indicator (props: title, dark, keyboard): +// +// +// ...your screen content... +// +// +/* END USAGE */ + +// ───────────────────────────────────────────────────────────── +// Status bar +// ───────────────────────────────────────────────────────────── +function IOSStatusBar({ dark = false, time = '9:41' }) { + const c = dark ? '#fff' : '#000'; + return ( +
+
+ {time} +
+
+ + + + + + + + + + + + + + + + +
+
+ ); +} + +// ───────────────────────────────────────────────────────────── +// Liquid glass pill — blur + tint + shine +// ───────────────────────────────────────────────────────────── +function IOSGlassPill({ children, dark = false, style = {} }) { + return ( +
+ {/* blur + tint */} +
+ {/* shine */} +
+
+ {children} +
+
+ ); +} + +// ───────────────────────────────────────────────────────────── +// Navigation bar — glass pills + large title +// ───────────────────────────────────────────────────────────── +function IOSNavBar({ title = 'Title', dark = false, trailingIcon = true }) { + const muted = dark ? 'rgba(255,255,255,0.6)' : '#404040'; + const text = dark ? '#fff' : '#000'; + const pillIcon = (content) => ( + +
+ {content} +
+
+ ); + return ( +
+
+ {/* back chevron */} + {pillIcon( + + + + )} + {/* trailing ellipsis */} + {trailingIcon && pillIcon( + + + + + + )} +
+ {/* large title */} +
{title}
+
+ ); +} + +// ───────────────────────────────────────────────────────────── +// Grouped list (inset card, r:26) + row (52px) +// ───────────────────────────────────────────────────────────── +function IOSListRow({ title, detail, icon, chevron = true, isLast = false, dark = false }) { + const text = dark ? '#fff' : '#000'; + const sec = dark ? 'rgba(235,235,245,0.6)' : 'rgba(60,60,67,0.6)'; + const ter = dark ? 'rgba(235,235,245,0.3)' : 'rgba(60,60,67,0.3)'; + const sep = dark ? 'rgba(84,84,88,0.65)' : 'rgba(60,60,67,0.12)'; + return ( +
+ {icon && ( +
+ )} +
{title}
+ {detail && {detail}} + {chevron && ( + + + + )} + {!isLast && ( +
+ )} +
+ ); +} + +function IOSList({ header, children, dark = false }) { + const hc = dark ? 'rgba(235,235,245,0.6)' : 'rgba(60,60,67,0.6)'; + const bg = dark ? '#1C1C1E' : '#fff'; + return ( +
+ {header && ( +
{header}
+ )} +
{children}
+
+ ); +} + +// ───────────────────────────────────────────────────────────── +// Device frame +// ───────────────────────────────────────────────────────────── +function IOSDevice({ + children, width = 402, height = 874, dark = false, + title, keyboard = false, +}) { + return ( +
+ {/* dynamic island */} +
+ {/* status bar (absolute) */} +
+ +
+ {/* nav + content */} +
+ {title !== undefined && } +
{children}
+ {keyboard && } +
+ {/* home indicator — always on top */} +
+
+
+
+ ); +} + +// ───────────────────────────────────────────────────────────── +// Keyboard — iOS 26 liquid glass +// ───────────────────────────────────────────────────────────── +function IOSKeyboard({ dark = false }) { + const glyph = dark ? 'rgba(255,255,255,0.7)' : '#595959'; + const sugg = dark ? 'rgba(255,255,255,0.6)' : '#333'; + const keyBg = dark ? 'rgba(255,255,255,0.22)' : 'rgba(255,255,255,0.85)'; + + // special-key icons + const icons = { + shift: , + del: , + ret: , + }; + + const key = (content, { w, flex, ret, fs = 25, k } = {}) => ( +
{content}
+ ); + + const row = (keys, pad = 0) => ( +
+ {keys.map(l => key(l, { flex: true, k: l }))} +
+ ); + + return ( +
+ {/* liquid glass bg — same recipe as nav pills */} +
+
+ + {/* autocorrect bar */} +
+ {['"The"', 'the', 'to'].map((w, i) => ( + + {i > 0 &&
} +
{w}
+ + ))} +
+ + {/* key layout */} +
+ {row(['q','w','e','r','t','y','u','i','o','p'])} + {row(['a','s','d','f','g','h','j','k','l'], 20)} +
+ {key(icons.shift, { w: 45, k: 'shift' })} +
+ {['z','x','c','v','b','n','m'].map(l => key(l, { flex: true, k: l }))} +
+ {key(icons.del, { w: 45, k: 'del' })} +
+
+ {key('ABC', { w: 92.25, fs: 18, k: 'abc' })} + {key('', { flex: true, k: 'space' })} + {key(icons.ret, { w: 92.25, ret: true, k: 'ret' })} +
+
+ + {/* bottom spacer (emoji+mic area, icons omitted) */} +
+
+ ); +} + +Object.assign(window, { + IOSDevice, IOSStatusBar, IOSNavBar, IOSGlassPill, IOSList, IOSListRow, IOSKeyboard, +}); diff --git a/PetFolio Redesign/Care Redesign/screenshots/badge.png b/PetFolio Redesign/Care Redesign/screenshots/badge.png new file mode 100644 index 0000000..25f6a32 Binary files /dev/null and b/PetFolio Redesign/Care Redesign/screenshots/badge.png differ diff --git a/PetFolio Redesign/Care Redesign/screenshots/confetti.png b/PetFolio Redesign/Care Redesign/screenshots/confetti.png new file mode 100644 index 0000000..0decc8a Binary files /dev/null and b/PetFolio Redesign/Care Redesign/screenshots/confetti.png differ diff --git a/PetFolio Redesign/Care Redesign/screenshots/dark.png b/PetFolio Redesign/Care Redesign/screenshots/dark.png new file mode 100644 index 0000000..f8fa512 Binary files /dev/null and b/PetFolio Redesign/Care Redesign/screenshots/dark.png differ diff --git a/PetFolio Redesign/Care Redesign/screenshots/lower.png b/PetFolio Redesign/Care Redesign/screenshots/lower.png new file mode 100644 index 0000000..29a065b Binary files /dev/null and b/PetFolio Redesign/Care Redesign/screenshots/lower.png differ diff --git a/PetFolio Redesign/Care Redesign/screenshots/lower2.png b/PetFolio Redesign/Care Redesign/screenshots/lower2.png new file mode 100644 index 0000000..341ab65 Binary files /dev/null and b/PetFolio Redesign/Care Redesign/screenshots/lower2.png differ diff --git a/PetFolio Redesign/Care Redesign/screenshots/lower3.png b/PetFolio Redesign/Care Redesign/screenshots/lower3.png new file mode 100644 index 0000000..b80644a Binary files /dev/null and b/PetFolio Redesign/Care Redesign/screenshots/lower3.png differ diff --git a/PetFolio Redesign/Care Redesign/screenshots/lower4.png b/PetFolio Redesign/Care Redesign/screenshots/lower4.png new file mode 100644 index 0000000..9e7fc5c Binary files /dev/null and b/PetFolio Redesign/Care Redesign/screenshots/lower4.png differ diff --git a/PetFolio Redesign/Care Redesign/screenshots/trophy.png b/PetFolio Redesign/Care Redesign/screenshots/trophy.png new file mode 100644 index 0000000..39bee06 Binary files /dev/null and b/PetFolio Redesign/Care Redesign/screenshots/trophy.png differ diff --git a/PetFolio Redesign/Care Redesign/screenshots/trophy2.png b/PetFolio Redesign/Care Redesign/screenshots/trophy2.png new file mode 100644 index 0000000..1a9091d Binary files /dev/null and b/PetFolio Redesign/Care Redesign/screenshots/trophy2.png differ diff --git a/PetFolio Redesign/Care Redesign/screenshots/tweaks.png b/PetFolio Redesign/Care Redesign/screenshots/tweaks.png new file mode 100644 index 0000000..db61fe2 Binary files /dev/null and b/PetFolio Redesign/Care Redesign/screenshots/tweaks.png differ diff --git a/PetFolio Redesign/Care Redesign/screenshots/tweaks2.png b/PetFolio Redesign/Care Redesign/screenshots/tweaks2.png new file mode 100644 index 0000000..60132f3 Binary files /dev/null and b/PetFolio Redesign/Care Redesign/screenshots/tweaks2.png differ diff --git a/PetFolio Redesign/Care Redesign/screenshots/tweaks3.png b/PetFolio Redesign/Care Redesign/screenshots/tweaks3.png new file mode 100644 index 0000000..9259e23 Binary files /dev/null and b/PetFolio Redesign/Care Redesign/screenshots/tweaks3.png differ diff --git a/PetFolio Redesign/Care Redesign/screenshots/v1.png b/PetFolio Redesign/Care Redesign/screenshots/v1.png new file mode 100644 index 0000000..7e5424c Binary files /dev/null and b/PetFolio Redesign/Care Redesign/screenshots/v1.png differ diff --git a/PetFolio Redesign/Care Redesign/screenshots/v2.png b/PetFolio Redesign/Care Redesign/screenshots/v2.png new file mode 100644 index 0000000..7e5424c Binary files /dev/null and b/PetFolio Redesign/Care Redesign/screenshots/v2.png differ diff --git a/PetFolio Redesign/Care Redesign/screenshots/v3.png b/PetFolio Redesign/Care Redesign/screenshots/v3.png new file mode 100644 index 0000000..29a065b Binary files /dev/null and b/PetFolio Redesign/Care Redesign/screenshots/v3.png differ diff --git a/PetFolio Redesign/README.md b/PetFolio Redesign/README.md new file mode 100644 index 0000000..899ddd5 --- /dev/null +++ b/PetFolio Redesign/README.md @@ -0,0 +1,22 @@ +# CODING AGENTS: READ THIS FIRST + +This is a **handoff bundle** from Claude Design (claude.ai/design). + +A user mocked up designs in HTML/CSS/JS using an AI design tool, then exported this bundle so a coding agent can implement the designs for real. + +## What you should do — IMPORTANT + +**Read `PetFolio Redesign/Care Redesign/Care Redesign.html` in full.** The user had this file open when they triggered the handoff, so it's almost certainly the primary design they want built. Read it top to bottom — don't skim. Then **follow its imports**: open every file it pulls in (shared components, CSS, scripts) so you understand how the pieces fit together before you start implementing. + +**If anything is ambiguous, ask the user to confirm before you start implementing.** It's much cheaper to clarify scope up front than to build the wrong thing. + +## About the design files + +The design medium is **HTML/CSS/JS** — these are prototypes, not production code. Your job is to **recreate them pixel-perfectly** in whatever technology makes sense for the target codebase (React, Vue, native, whatever fits). Match the visual output; don't copy the prototype's internal structure unless it happens to fit. + +**Don't render these files in a browser or take screenshots unless the user asks you to.** Everything you need — dimensions, colors, layout rules — is spelled out in the source. Read the HTML and CSS directly; a screenshot won't tell you anything they don't. + +## Bundle contents + +- `petfolio-redesign/README.md` — this file +- `petfolio-redesign/project/` — the `PetFolio Redesign` project files (HTML prototypes, assets, components) diff --git a/README.md b/README.md index eea797e..fb329f0 100644 --- a/README.md +++ b/README.md @@ -74,3 +74,46 @@ flutter build apk --release --dart-define-from-file=.env ## Architecture Feature-first structure under `lib/features/`. Core shared code in `lib/core/`. See [CLAUDE.md](CLAUDE.md) and [docs/](docs/) for full architecture, schema, and implementation status. + +## Pending: Kotlin Gradle Plugin (KGP) Migration + +Flutter 3.44 introduced built-in Kotlin support in AGP, deprecating the explicit `id("kotlin-android")` + `kotlinOptions {}` pattern. The app and several plugins need to migrate before AGP 9.0 enforces the change. + +**Current workaround** — `android/gradle.properties` holds two compat flags added by the Flutter migrator: + +```properties +android.builtInKotlin=false +android.newDsl=false +``` + +These suppress build failures today but will be removed in a future Flutter release. + +**Blocked on these plugins releasing KGP-migrated versions:** + +| Plugin | Status | +|---|---| +| `image_picker_android` | Awaiting upstream release | +| `shared_preferences_android` | Awaiting upstream release | +| `url_launcher_android` | Awaiting upstream release | +| `share_plus` | Awaiting upstream release | +| `stripe_android` | Awaiting upstream release | + +**Migration steps (do all at once, after all plugins above are updated):** + +1. `flutter pub upgrade` — pull in the migrated plugin versions +2. In `android/app/build.gradle.kts`: + - Remove `id("kotlin-android")` from the `plugins {}` block + - Replace `kotlinOptions { jvmTarget = ... }` inside `android {}` with: + ```kotlin + kotlin { + compilerOptions { + jvmTarget = org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_17 + } + } + ``` +3. In `android/gradle.properties`, delete (or flip to `true`) both compat flags: + ```properties + android.builtInKotlin=true + android.newDsl=true + ``` +4. Run `flutter build apk --debug` to confirm a clean build. diff --git a/android/gradle.properties b/android/gradle.properties index fbee1d8..d5da727 100644 --- a/android/gradle.properties +++ b/android/gradle.properties @@ -1,2 +1,6 @@ org.gradle.jvmargs=-Xmx8G -XX:MaxMetaspaceSize=4G -XX:ReservedCodeCacheSize=512m -XX:+HeapDumpOnOutOfMemoryError android.useAndroidX=true +# This builtInKotlin flag was added automatically by Flutter migrator +android.builtInKotlin=false +# This newDsl flag was added automatically by Flutter migrator +android.newDsl=false diff --git a/lib/core/services/notification_service.dart b/lib/core/services/notification_service.dart index aed776b..50ba796 100644 --- a/lib/core/services/notification_service.dart +++ b/lib/core/services/notification_service.dart @@ -24,7 +24,7 @@ class NotificationService { ); await _plugin.initialize( - const InitializationSettings(android: androidSettings, iOS: iosSettings), + settings: const InitializationSettings(android: androidSettings, iOS: iosSettings), ); final androidPlugin = @@ -76,20 +76,18 @@ class NotificationService { ); await _plugin.zonedSchedule( - _idFor(taskId), - 'Care Reminder', - title, - scheduled, - NotificationDetails(android: androidDetails), + id: _idFor(taskId), + title: 'Care Reminder', + body: title, + scheduledDate: scheduled, + notificationDetails: NotificationDetails(android: androidDetails), androidScheduleMode: AndroidScheduleMode.exactAllowWhileIdle, matchDateTimeComponents: repeating ? DateTimeComponents.time : null, - uiLocalNotificationDateInterpretation: - UILocalNotificationDateInterpretation.absoluteTime, ); } Future cancelForTask(String taskId) async { - await _plugin.cancel(_idFor(taskId)); + await _plugin.cancel(id: _idFor(taskId)); } Future cancelAll() async { diff --git a/lib/core/widgets/app_shell.dart b/lib/core/widgets/app_shell.dart index fe6d327..2c85c02 100644 --- a/lib/core/widgets/app_shell.dart +++ b/lib/core/widgets/app_shell.dart @@ -127,7 +127,7 @@ class AppShellHeader extends ConsumerWidget { final eyebrows = [ 'ACTIVE PET', - activePet != null ? 'CARE · ${activePet.name.toUpperCase()}' : 'CARE', + 'CARE', 'PAWSFEED', 'MATCH · NEARBY', 'SHOP FOR', @@ -146,6 +146,7 @@ class AppShellHeader extends ConsumerWidget { final isDark = Theme.of(context).brightness == Brightness.dark; return _HeaderIconBtn( icon: isDark ? Icons.light_mode_rounded : Icons.dark_mode_rounded, + tooltip: isDark ? 'Switch to light mode' : 'Switch to dark mode', onTap: () => ref.read(themeProvider.notifier).toggleTheme(), ); }); @@ -278,14 +279,19 @@ class AppShellHeader extends ConsumerWidget { // ── Header icon button ──────────────────────────────────────────────────────── class _HeaderIconBtn extends StatelessWidget { - const _HeaderIconBtn({required this.icon, required this.onTap}); + const _HeaderIconBtn({ + required this.icon, + required this.onTap, + this.tooltip, + }); final IconData icon; final VoidCallback onTap; + final String? tooltip; @override Widget build(BuildContext context) { - return GestureDetector( + final btn = GestureDetector( onTap: onTap, child: Container( width: 36, height: 36, @@ -297,6 +303,10 @@ class _HeaderIconBtn extends StatelessWidget { child: Icon(icon, color: Colors.white, size: 18), ), ); + if (tooltip != null) { + return Tooltip(message: tooltip!, child: btn); + } + return btn; } } diff --git a/lib/features/care/data/models/pet_level.dart b/lib/features/care/data/models/pet_level.dart index 3975fcc..9f552df 100644 --- a/lib/features/care/data/models/pet_level.dart +++ b/lib/features/care/data/models/pet_level.dart @@ -103,44 +103,44 @@ const kBadgeCatalog = [ BadgeInfo( type: 'first_log', emoji: '🐾', - label: 'First Log', + label: 'First Paw', color: AppColors.mint, - description: 'Logged your first care activity', + description: 'You logged your very first care activity — the journey begins!', ), BadgeInfo( type: '3_day_streak', - emoji: '🔥', - label: '3-Day', + emoji: '🦴', + label: 'Treat Earner', color: AppColors.tangerine, - description: '3 days in a row', + description: 'Three days of care in a row — your pet is loving it!', ), BadgeInfo( type: '7_day_hero', - emoji: '🦸', - label: '7-Day Hero', + emoji: '🌿', + label: 'Thriving Week', color: AppColors.sunny, - description: '7 days in a row', + description: 'A full week of care — your pet is thriving and healthy!', ), BadgeInfo( type: 'routine_master', - emoji: '💯', - label: 'Routine Master', + emoji: '❤️', + label: 'Devoted Carer', color: AppColors.poppy, - description: '14 days in a row', + description: '14 days without missing a beat — true devotion!', ), BadgeInfo( type: '30_day_legend', - emoji: '👑', - label: '30-Day Legend', + emoji: '🌟', + label: 'Star Companion', color: AppColors.lilac, - description: '30 days in a row', + description: 'A whole month of daily care — you\'re a star companion!', ), BadgeInfo( type: 'care_champion', - emoji: '🏆', - label: 'Care Champion', + emoji: '👑', + label: 'Pet Royalty', color: AppColors.sky, - description: '100 total care logs', + description: '100 care logs — your pet lives like royalty!', ), ]; diff --git a/lib/features/care/domain/services/care_recommendation_service.dart b/lib/features/care/domain/services/care_recommendation_service.dart index ea8b4e3..7e4fa00 100644 --- a/lib/features/care/domain/services/care_recommendation_service.dart +++ b/lib/features/care/domain/services/care_recommendation_service.dart @@ -1,5 +1,6 @@ import 'dart:async'; import 'dart:convert'; +import 'package:flutter/foundation.dart' show kIsWeb; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:http/http.dart' as http; import 'package:supabase_flutter/supabase_flutter.dart'; @@ -122,7 +123,7 @@ class CareRecommendationService { existingTasks: existingTasks, ); - if (_apiKey.isEmpty) { + if (!kIsWeb && _apiKey.isEmpty) { throw const CareRecommendationException( 'AI routine suggestions are not configured on this build.', isConfigError: true, @@ -130,37 +131,56 @@ class CareRecommendationService { } try { - final response = await http.post( - Uri.parse(_url), - headers: { - 'Authorization': 'Bearer $_apiKey', - 'Content-Type': 'application/json', - 'Accept': 'application/json', - }, - body: jsonEncode({ - 'model': 'google/gemma-3n-e4b-it', - 'messages': [ - {'role': 'user', 'content': prompt} - ], - 'max_tokens': 1500, - 'temperature': 0.25, - 'top_p': 0.75, - 'stream': false, - 'nvext': {'guided_json': _guidedSchema}, - }), - ).timeout(const Duration(seconds: 45)); - - if (response.statusCode != 200) { - final detail = response.body.length > 300 - ? response.body.substring(0, 300) - : response.body; - throw CareRecommendationException( - 'The suggestion service is unavailable right now. Please try again later.', - cause: 'HTTP ${response.statusCode}: $detail', - ); + final Map body; + + if (kIsWeb) { + // On web, proxy through the Supabase Edge Function to avoid CORS. + final fnResp = await Supabase.instance.client.functions + .invoke('recommend-care-tasks', body: {'prompt': prompt}) + .timeout(const Duration(seconds: 60)); + if (fnResp.status != 200) { + final detail = fnResp.data?.toString() ?? ''; + throw CareRecommendationException( + 'The suggestion service is unavailable right now. Please try again later.', + cause: 'Edge function ${fnResp.status}: ${detail.length > 300 ? detail.substring(0, 300) : detail}', + ); + } + body = (fnResp.data is Map) + ? Map.from(fnResp.data as Map) + : jsonDecode(jsonEncode(fnResp.data)) as Map; + } else { + final response = await http.post( + Uri.parse(_url), + headers: { + 'Authorization': 'Bearer $_apiKey', + 'Content-Type': 'application/json', + 'Accept': 'application/json', + }, + body: jsonEncode({ + 'model': 'google/gemma-3n-e4b-it', + 'messages': [ + {'role': 'user', 'content': prompt} + ], + 'max_tokens': 1500, + 'temperature': 0.25, + 'top_p': 0.75, + 'stream': false, + 'nvext': {'guided_json': _guidedSchema}, + }), + ).timeout(const Duration(seconds: 45)); + + if (response.statusCode != 200) { + final detail = response.body.length > 300 + ? response.body.substring(0, 300) + : response.body; + throw CareRecommendationException( + 'The suggestion service is unavailable right now. Please try again later.', + cause: 'HTTP ${response.statusCode}: $detail', + ); + } + body = jsonDecode(response.body) as Map; } - final body = jsonDecode(response.body) as Map; final content = body['choices'][0]['message']['content'] as String; final cleaned = content diff --git a/lib/features/care/presentation/screens/care_screen.dart b/lib/features/care/presentation/screens/care_screen.dart index ff0cdcd..a075a0d 100644 --- a/lib/features/care/presentation/screens/care_screen.dart +++ b/lib/features/care/presentation/screens/care_screen.dart @@ -61,8 +61,7 @@ class _CareScreenState extends ConsumerState { SnackBar(content: Text(msg), behavior: SnackBarBehavior.floating), ); if (!mounted) return; - if (GoRouterState.of(context).uri.queryParameters['onboardingComplete'] == - '1') { + if (GoRouterState.of(context).uri.queryParameters['onboardingComplete'] == '1') { context.go('/care'); } }); @@ -94,12 +93,8 @@ class _CareScreenState extends ConsumerState { return; } if (tasks != null) { - RoutineRecommendationSheet.show( - context, - activePet, - tasks, - isRefresh: hasTasks, - ); + RoutineRecommendationSheet.show(context, activePet, tasks, + isRefresh: hasTasks); } } @@ -118,10 +113,7 @@ class _CareScreenState extends ConsumerState { children: [ Icon(Icons.wifi_off_rounded, size: 48, color: pt.ink300), const SizedBox(height: 12), - Text( - 'Could not load pets', - style: TextStyle(fontSize: 15, color: pt.ink500), - ), + Text('Could not load pets', style: TextStyle(fontSize: 15, color: pt.ink500)), const SizedBox(height: 16), FilledButton.icon( onPressed: () => ref.invalidate(petListProvider), @@ -136,32 +128,29 @@ class _CareScreenState extends ConsumerState { children: [ Icon(Icons.pets_outlined, size: 48, color: pt.ink300), const SizedBox(height: 12), - Text( - 'Add a pet to track care', - style: TextStyle(fontSize: 15, color: pt.ink500), - ), + Text('Add a pet to track care', + style: TextStyle(fontSize: 15, color: pt.ink500)), ], ) : const TailWagLoader(), ); - return Scaffold( - backgroundColor: pt.surface1, - body: Center(child: body), - ); + return Scaffold(backgroundColor: pt.surface1, body: Center(child: body)); } final dashboard = ref.watch(careDashboardProvider); final species = activePet.speciesEnum; void openAddSheet() => showModalBottomSheet( - context: context, - isScrollControlled: true, - useSafeArea: true, - useRootNavigator: true, - backgroundColor: Colors.transparent, - builder: (_) => - _CareTaskFormSheet(petId: activePet.id, petName: activePet.name), - ); + context: context, + isScrollControlled: true, + useSafeArea: true, + useRootNavigator: true, + backgroundColor: Colors.transparent, + builder: (_) => _CareTaskFormSheet( + petId: activePet.id, + petName: activePet.name, + ), + ); return Scaffold( backgroundColor: pt.surface1, @@ -173,108 +162,137 @@ class _CareScreenState extends ConsumerState { body: LayoutBuilder( builder: (context, constraints) { final wide = constraints.maxWidth >= 600; - final children = [ - CareGamifiedHeader(activePet: activePet, dashboard: dashboard), - Padding( - padding: const EdgeInsets.fromLTRB(16, 12, 16, 0), - child: _HorizontalDatePicker( - selectedDate: dashboard.selectedDate, - onDateSelected: (date) => - ref.read(careDashboardProvider.notifier).selectDate(date), + final list = ListView( + padding: const EdgeInsets.fromLTRB(0, 0, 0, 120), + children: [ + CareGamifiedHeader( + activePet: activePet, + dashboard: dashboard, ), - ), - Padding( - padding: const EdgeInsets.symmetric(horizontal: 16), - child: Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - const SizedBox(height: 12.0), - PfSectionTitle( - title: 'Trophy room', - accent: AppColors.lilac, - trailing: GestureDetector( - onTap: () => context.push('/care/medical-vault'), - child: const Text( - 'Vault →', - style: TextStyle( - fontSize: 13, - fontWeight: FontWeight.w800, - color: AppColors.lilac700, + Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + // ── Space for floating hero card overlap (card at bottom:-28) ── + const SizedBox(height: 44.0), + // ── Date picker ──────────────────────────────── + _HorizontalDatePicker( + selectedDate: dashboard.selectedDate, + onDateSelected: (d) => ref + .read(careDashboardProvider.notifier) + .selectDate(d), + ), + const SizedBox(height: 24.0), + // ── TODAY'S QUESTS header with AI refresh ────── + Padding( + padding: const EdgeInsets.fromLTRB(4, 0, 4, 10), + child: Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Text( + "Today's Quests", + style: TextStyle( + fontSize: 13, + fontWeight: FontWeight.w700, + letterSpacing: 0.2, + color: pt.ink500, + ), + ), + const Spacer(), + _DoneCounter(tasks: dashboard.tasks.value ?? []), + const SizedBox(width: 6), + // AI Routine refresh — 44×44 accessible touch target + GestureDetector( + onTap: _isGeneratingRoutine ? null : () => _generateRoutine(activePet), + child: Tooltip( + message: 'Refresh AI Routine', + child: AnimatedContainer( + duration: PetfolioThemeExtension.durationSm, + width: 44, + height: 44, + decoration: BoxDecoration( + color: _isGeneratingRoutine + ? AppColors.lilacSoft + : AppColors.lilacSoft, + shape: BoxShape.circle, + border: Border.all( + color: AppColors.lilac.withAlpha(50), + width: 1, + ), + ), + alignment: Alignment.center, + child: _isGeneratingRoutine + ? const SizedBox( + width: 18, + height: 18, + child: CircularProgressIndicator( + strokeWidth: 2, + color: AppColors.lilac, + ), + ) + : const Icon( + Icons.auto_awesome_rounded, + size: 18, + color: AppColors.lilac, + ), + ), + ), + ), + ], + ), + ), + // ── AI empty-state full banner ───────────────── + if (dashboard.tasks.value?.isEmpty == true) + _AiRoutineBanner( + activePetId: activePet.id, + hasNoTasks: true, + isGenerating: _isGeneratingRoutine, + onTap: () => _generateRoutine(activePet), + ), + _DailyTasksDashboard( + state: dashboard, + petId: activePet.id, + petName: activePet.name, + species: species, + onAddTask: openAddSheet, + ), + const SizedBox(height: 28), + PfSectionTitle( + title: 'This week', + accent: AppColors.mint, + ), + const SizedBox(height: 8), + CareGamifiedWeeklyChart( + selectedDay: dashboard.selectedDate, + weekHits: dashboard.weekGoalHit.value ?? List.filled(7, false), + progressPercent: () { + final all = dashboard.tasks.value; + if (all == null || all.isEmpty) return 0.0; + final planned = all.where((t) => + !t.isLogDerived && + t.frequency != dbtask.CareFrequency.asNeeded).toList(); + if (planned.isEmpty) return 0.0; + return planned.where((t) => t.isCompleted).length / planned.length; + }(), + ), + const SizedBox(height: 28), + _UtilityBanner(pt: pt), + ], ), ), + ], + ); + if (!wide) return list; + return Align( + alignment: Alignment.topCenter, + child: ConstrainedBox( + constraints: const BoxConstraints(maxWidth: 560), + child: list, ), - ), - CareGamifiedTrophyRoom(petId: activePet.id), - const SizedBox(height: 32), - Padding( - padding: const EdgeInsets.fromLTRB(4, 0, 4, 8), - child: Row( - children: [ - Text( - "TODAY'S QUESTS", - style: TextStyle( - fontSize: 12, - fontWeight: FontWeight.w700, - letterSpacing: 0.08 * 12, - color: pt.ink500, - ), - ), - const Spacer(), - _DoneCounter(tasks: dashboard.tasks.value ?? []), - ], - ), - ), - _AiRoutineBanner( - activePetId: activePet.id, - hasNoTasks: dashboard.tasks.value?.isEmpty == true, - isGenerating: _isGeneratingRoutine, - onTap: () => _generateRoutine(activePet), - ), - _DailyTasksDashboard( - state: dashboard, - petId: activePet.id, - petName: activePet.name, - species: species, - onAddTask: openAddSheet, - ), - const SizedBox(height: 32), - PfSectionTitle(title: 'This week', accent: AppColors.mint), - CareGamifiedWeeklyChart( - selectedDay: dashboard.selectedDate, - weekHits: - dashboard.weekGoalHit.value ?? List.filled(7, false), - progressPercent: - (dashboard.tasks.value != null && - dashboard.tasks.value!.isNotEmpty) - ? (dashboard.tasks.value! - .where((t) => t.isCompleted) - .length / - dashboard.tasks.value!.length) - : 0.0, - ), - const SizedBox(height: 32), - _NutritionBanner(pt: pt), - const SizedBox(height: 16), - _MedicalVaultBanner(pt: pt), - ], + ); + }, ), - ), - ]; - final list = ListView.builder( - padding: const EdgeInsets.fromLTRB(0, 0, 0, 120), - itemCount: children.length, - itemBuilder: (context, index) => children[index], - ); - if (!wide) return list; - return Align( - alignment: Alignment.topCenter, - child: ConstrainedBox( - constraints: const BoxConstraints(maxWidth: 560), - child: list, - ), - ); - }, - ), ); } } @@ -327,15 +345,9 @@ class _AiRoutineBanner extends StatelessWidget { width: 20, height: 20, child: CircularProgressIndicator( - strokeWidth: 2, - color: Colors.white, - ), + strokeWidth: 2, color: Colors.white), ) - : const Icon( - Icons.auto_awesome, - color: Colors.white, - size: 20, - ), + : const Icon(Icons.auto_awesome, color: Colors.white, size: 20), ), const SizedBox(width: 16), Expanded( @@ -407,7 +419,6 @@ class _HorizontalDatePickerState extends State<_HorizontalDatePicker> { } void _scrollToToday() { - if (!mounted) return; if (!_scroll.hasClients) return; final screenW = context.size?.width ?? 360; final todayOffset = @@ -429,86 +440,99 @@ class _HorizontalDatePickerState extends State<_HorizontalDatePicker> { final cs = Theme.of(context).colorScheme; final today = DateUtils.dateOnly(DateTime.now()); - return SizedBox( - height: 76, - child: ListView.builder( - controller: _scroll, - scrollDirection: Axis.horizontal, - padding: EdgeInsets.zero, - itemCount: _totalDays, - itemBuilder: (context, i) { - final date = today.subtract(Duration(days: _daysBack - i)); - final isSelected = DateUtils.dateOnly(widget.selectedDate) == date; - final isToday = date == today; - final isFuture = date.isAfter(today); - - final ymd = - '${date.year}-${date.month.toString().padLeft(2, '0')}-${date.day.toString().padLeft(2, '0')}'; - return Padding( - padding: EdgeInsets.only(right: i < _totalDays - 1 ? _chipGap : 0), - child: GestureDetector( - key: ValueKey('care_date_$ymd'), - onTap: isFuture ? null : () => widget.onDateSelected(date), - child: AnimatedContainer( - duration: PetfolioThemeExtension.durationSm, - width: _chipW, - decoration: BoxDecoration( - color: isSelected - ? cs.primary - : (isToday ? cs.primary.withAlpha(15) : pt.surface2), - borderRadius: BorderRadius.circular(14), - border: Border.all( - color: isSelected - ? Colors.transparent - : (isToday ? cs.primary.withAlpha(80) : pt.line), - width: isToday && !isSelected ? 1.5 : 0.5, - ), - ), - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Text( - _dayLetters[date.weekday - 1], - style: TextStyle( - fontSize: 11, - fontWeight: FontWeight.w600, - letterSpacing: 0.5, - color: isSelected - ? Colors.white.withAlpha(200) - : (isFuture ? pt.ink300 : pt.ink500), - ), - ), - const SizedBox(height: 4), - Text( - '${date.day}', - style: TextStyle( - fontWeight: FontWeight.w700, - fontSize: 18, - height: 1, + return ClipRect( + child: SizedBox( + height: 76, + child: Stack( + children: [ + ListView.builder( + controller: _scroll, + scrollDirection: Axis.horizontal, + padding: EdgeInsets.zero, + itemCount: _totalDays, + itemBuilder: (context, i) { + final date = today.subtract(Duration(days: _daysBack - i)); + final isSelected = + DateUtils.dateOnly(widget.selectedDate) == date; + final isToday = date == today; + final isFuture = date.isAfter(today); + + final ymd = + '${date.year}-${date.month.toString().padLeft(2, '0')}-${date.day.toString().padLeft(2, '0')}'; + return Padding( + padding: EdgeInsets.only( + right: i < _totalDays - 1 ? _chipGap : 0), + child: GestureDetector( + key: ValueKey('care_date_$ymd'), + onTap: isFuture ? null : () => widget.onDateSelected(date), + child: AnimatedContainer( + duration: PetfolioThemeExtension.durationSm, + width: _chipW, + decoration: BoxDecoration( color: isSelected - ? Colors.white + ? cs.primary : (isToday - ? cs.primary - : (isFuture ? pt.ink300 : cs.onSurface)), - ), - ), - if (isToday && !isSelected) ...[ - const SizedBox(height: 4), - Container( - width: 4, - height: 4, - decoration: BoxDecoration( - color: cs.primary, - shape: BoxShape.circle, + ? cs.primary.withAlpha(15) + : pt.surface2), + borderRadius: BorderRadius.circular(14), + border: Border.all( + color: isSelected + ? Colors.transparent + : (isToday + ? cs.primary.withAlpha(80) + : pt.line), + width: isToday && !isSelected ? 1.5 : 0.5, ), ), - ], - ], - ), - ), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text( + _dayLetters[date.weekday - 1], + style: TextStyle( + fontSize: 11, + fontWeight: FontWeight.w600, + letterSpacing: 0.5, + color: isSelected + ? Colors.white.withAlpha(200) + : (isFuture ? pt.ink300 : pt.ink500), + ), + ), + const SizedBox(height: 4), + Text( + '${date.day}', + style: TextStyle( + fontWeight: FontWeight.w700, + fontSize: 18, + height: 1, + color: isSelected + ? Colors.white + : (isToday + ? cs.primary + : (isFuture + ? pt.ink300 + : cs.onSurface)), + ), + ), + if (isToday && !isSelected) ...[ + const SizedBox(height: 4), + Container( + width: 4, + height: 4, + decoration: BoxDecoration( + color: cs.primary, + shape: BoxShape.circle), + ), + ], + ], + ), + ), + ), + ); + }, ), - ); - }, + ], + ), ), ); } @@ -556,31 +580,29 @@ class _DailyTasksDashboard extends ConsumerWidget { ); } - // Check if all planned tasks are done - final planned = tasks.where((t) => !t.isLogDerived).toList(); - final allDone = - planned.isNotEmpty && planned.every((t) => t.isCompleted); + // Check if all scheduled/repeating tasks are done (exclude asNeeded) + final planned = tasks + .where((t) => !t.isLogDerived && t.frequency != dbtask.CareFrequency.asNeeded) + .toList(); + final allDone = planned.isNotEmpty && + planned.every((t) => t.isCompleted); // Group tasks by frequency bucket final daily = tasks - .where( - (t) => - t.frequency == dbtask.CareFrequency.daily || - t.frequency == dbtask.CareFrequency.twiceDaily || - t.frequency == dbtask.CareFrequency.once || - t.isLogDerived, - ) + .where((t) => + t.frequency == dbtask.CareFrequency.daily || + t.frequency == dbtask.CareFrequency.twiceDaily || + t.frequency == dbtask.CareFrequency.once || + t.isLogDerived) .toList(); final weekly = tasks .where((t) => t.frequency == dbtask.CareFrequency.weekly) .toList(); final lessOften = tasks - .where( - (t) => - t.frequency == dbtask.CareFrequency.biweekly || - t.frequency == dbtask.CareFrequency.monthly || - t.frequency == dbtask.CareFrequency.asNeeded, - ) + .where((t) => + t.frequency == dbtask.CareFrequency.biweekly || + t.frequency == dbtask.CareFrequency.monthly || + t.frequency == dbtask.CareFrequency.asNeeded) .toList(); final pt = Theme.of(context).extension()!; @@ -589,40 +611,20 @@ class _DailyTasksDashboard extends ConsumerWidget { crossAxisAlignment: CrossAxisAlignment.start, children: [ // All-done celebration banner - if (allDone) _AllDoneBanner(tasks: planned), + if (allDone) + _AllDoneBanner(tasks: planned), // Daily tasks - if (daily.isNotEmpty) - ..._frequencyGroup( - context, - pt, - 'DAILY', - daily, - petId, - petName, - species, - ), + if (daily.isNotEmpty) ..._frequencyGroup( + context, pt, 'DAILY', daily, petId, petName, species, + ), // Weekly tasks - if (weekly.isNotEmpty) - ..._frequencyGroup( - context, - pt, - 'WEEKLY', - weekly, - petId, - petName, - species, - ), + if (weekly.isNotEmpty) ..._frequencyGroup( + context, pt, 'WEEKLY', weekly, petId, petName, species, + ), // Bi-weekly / Monthly / As-needed - if (lessOften.isNotEmpty) - ..._frequencyGroup( - context, - pt, - 'LESS OFTEN', - lessOften, - petId, - petName, - species, - ), + if (lessOften.isNotEmpty) ..._frequencyGroup( + context, pt, 'LESS OFTEN', lessOften, petId, petName, species, + ), ], ); }, @@ -638,14 +640,25 @@ class _DailyTasksDashboard extends ConsumerWidget { String petName, PetSpecies species, ) { + // Sort: scheduled-time tasks ascending, then no-time tasks alphabetically + final sorted = [...tasks]..sort((a, b) { + final aTime = parseCareScheduledTimeOfDay(a.scheduledTime); + final bTime = parseCareScheduledTimeOfDay(b.scheduledTime); + if (aTime != null && bTime != null) { + return (aTime.hour * 60 + aTime.minute) + .compareTo(bTime.hour * 60 + bTime.minute); + } + if (aTime != null) return -1; + if (bTime != null) return 1; + return a.title.compareTo(b.title); + }); + return [ Padding( - padding: const EdgeInsets.fromLTRB(4, 8, 4, 8), + padding: const EdgeInsets.fromLTRB(4, 10, 4, 10), child: Row( children: [ - Expanded( - child: Divider(color: pt.line, thickness: 1, endIndent: 8), - ), + Expanded(child: Divider(color: pt.line, thickness: 1, endIndent: 10)), Text( label, style: TextStyle( @@ -655,18 +668,16 @@ class _DailyTasksDashboard extends ConsumerWidget { color: pt.ink300, ), ), - Expanded(child: Divider(color: pt.line, thickness: 1, indent: 8)), + Expanded(child: Divider(color: pt.line, thickness: 1, indent: 10)), ], ), ), - ...tasks.map( - (t) => _CareTaskCard( - task: t, - petId: petId, - petName: petName, - species: species, - ), - ), + ...sorted.map((t) => _CareTaskCard( + task: t, + petId: petId, + petName: petName, + species: species, + )), ]; } } @@ -681,7 +692,10 @@ class _DoneCounter extends StatelessWidget { @override Widget build(BuildContext context) { - final planned = tasks.where((t) => !t.isLogDerived).toList(); + // Exclude log-derived entries and asNeeded tasks from the daily quest count + final planned = tasks + .where((t) => !t.isLogDerived && t.frequency != dbtask.CareFrequency.asNeeded) + .toList(); if (planned.isEmpty) return const SizedBox.shrink(); final done = planned.where((t) => t.isCompleted).length; final total = planned.length; @@ -718,7 +732,9 @@ class _AllDoneBanner extends StatelessWidget { @override Widget build(BuildContext context) { - final totalXp = tasks.fold(0, (sum, t) => sum + t.gamificationPoints); + final totalXp = tasks.fold( + 0, (sum, t) => sum + t.gamificationPoints, + ); return Container( margin: const EdgeInsets.only(bottom: 16), padding: const EdgeInsets.symmetric(horizontal: 18, vertical: 14), @@ -794,16 +810,15 @@ class _CareTaskCardState extends ConsumerState<_CareTaskCard> @override void initState() { super.initState(); - _xpCtrl = - AnimationController( - vsync: this, - duration: const Duration(milliseconds: 1100), - )..addStatusListener((s) { - if (s == AnimationStatus.completed && mounted) { - setState(() => _showBurst = false); - _xpCtrl.reset(); - } - }); + _xpCtrl = AnimationController( + vsync: this, + duration: const Duration(milliseconds: 1100), + )..addStatusListener((s) { + if (s == AnimationStatus.completed && mounted) { + setState(() => _showBurst = false); + _xpCtrl.reset(); + } + }); } @override @@ -814,8 +829,7 @@ class _CareTaskCardState extends ConsumerState<_CareTaskCard> void _toggle() { final nowDone = !widget.task.isCompleted; - ref - .read(careDashboardProvider.notifier) + ref.read(careDashboardProvider.notifier) .toggleTaskCompletion(widget.task.id, isCompleted: nowDone); if (nowDone && widget.task.gamificationPoints > 0) { setState(() => _showBurst = true); @@ -825,55 +839,33 @@ class _CareTaskCardState extends ConsumerState<_CareTaskCard> Color get _color { switch (widget.task.taskType) { - case dbtask.CareTaskType.feeding: - return AppColors.tangerine; - case dbtask.CareTaskType.medication: - return AppColors.poppy; - case dbtask.CareTaskType.walk: - return AppColors.mint; - case dbtask.CareTaskType.playtime: - return AppColors.sunny; - case dbtask.CareTaskType.dental: - return AppColors.lilac; - case dbtask.CareTaskType.grooming: - return AppColors.lilac; - case dbtask.CareTaskType.vetVisit: - return AppColors.mint; - case dbtask.CareTaskType.training: - return AppColors.tangerine; - case dbtask.CareTaskType.nailTrim: - return AppColors.lilac; - case dbtask.CareTaskType.bath: - return AppColors.sky; - case dbtask.CareTaskType.other: - return AppColors.sunny; + case dbtask.CareTaskType.feeding: return AppColors.tangerine; + case dbtask.CareTaskType.medication: return AppColors.poppy; + case dbtask.CareTaskType.walk: return AppColors.mint; + case dbtask.CareTaskType.playtime: return AppColors.sunny; + case dbtask.CareTaskType.dental: return AppColors.lilac; + case dbtask.CareTaskType.grooming: return AppColors.lilac; + case dbtask.CareTaskType.vetVisit: return AppColors.mint; + case dbtask.CareTaskType.training: return AppColors.tangerine; + case dbtask.CareTaskType.nailTrim: return AppColors.lilac; + case dbtask.CareTaskType.bath: return AppColors.sky; + case dbtask.CareTaskType.other: return AppColors.sunny; } } String get _emoji { switch (widget.task.taskType) { - case dbtask.CareTaskType.feeding: - return '🥩'; - case dbtask.CareTaskType.walk: - return '🦮'; - case dbtask.CareTaskType.grooming: - return '✂️'; - case dbtask.CareTaskType.medication: - return '💊'; - case dbtask.CareTaskType.vetVisit: - return '🏥'; - case dbtask.CareTaskType.training: - return '🎓'; - case dbtask.CareTaskType.playtime: - return '🎾'; - case dbtask.CareTaskType.dental: - return '🦷'; - case dbtask.CareTaskType.nailTrim: - return '💅'; - case dbtask.CareTaskType.bath: - return '🛁'; - case dbtask.CareTaskType.other: - return '⭐'; + case dbtask.CareTaskType.feeding: return '🥩'; + case dbtask.CareTaskType.walk: return '🦮'; + case dbtask.CareTaskType.grooming: return '✂️'; + case dbtask.CareTaskType.medication: return '💊'; + case dbtask.CareTaskType.vetVisit: return '🏥'; + case dbtask.CareTaskType.training: return '🎓'; + case dbtask.CareTaskType.playtime: return '🎾'; + case dbtask.CareTaskType.dental: return '🦷'; + case dbtask.CareTaskType.nailTrim: return '💅'; + case dbtask.CareTaskType.bath: return '🛁'; + case dbtask.CareTaskType.other: return '⭐'; } } @@ -893,24 +885,19 @@ class _CareTaskCardState extends ConsumerState<_CareTaskCard> final m = tod.minute.toString().padLeft(2, '0'); final period = tod.period == DayPeriod.am ? 'AM' : 'PM'; final formatted = '$h:$m $period'; - return (!t.isCompleted && t.isDueToday) ? 'Due $formatted' : formatted; + return (!t.isCompleted && t.isDueToday) + ? 'Due $formatted' + : formatted; } } switch (t.frequency) { - case dbtask.CareFrequency.once: - return 'Once'; - case dbtask.CareFrequency.daily: - return 'Daily'; - case dbtask.CareFrequency.twiceDaily: - return 'Twice daily'; - case dbtask.CareFrequency.weekly: - return 'Weekly'; - case dbtask.CareFrequency.biweekly: - return 'Every 2 weeks'; - case dbtask.CareFrequency.monthly: - return 'Monthly'; - case dbtask.CareFrequency.asNeeded: - return 'As needed'; + case dbtask.CareFrequency.once: return 'Once'; + case dbtask.CareFrequency.daily: return 'Daily'; + case dbtask.CareFrequency.twiceDaily: return 'Twice daily'; + case dbtask.CareFrequency.weekly: return 'Weekly'; + case dbtask.CareFrequency.biweekly: return 'Every 2 weeks'; + case dbtask.CareFrequency.monthly: return 'Monthly'; + case dbtask.CareFrequency.asNeeded: return 'As needed'; } } @@ -978,8 +965,7 @@ class _CareTaskCardState extends ConsumerState<_CareTaskCard> await _confirmDialog( ctx, title: 'Delete task', - body: - 'Remove "${task.title}" from ${widget.petName}\'s care plan?', + body: 'Remove "${task.title}" from ${widget.petName}\'s care plan?', confirmLabel: 'Delete', onConfirmed: () => ref .read(careDashboardProvider.notifier) @@ -1005,14 +991,12 @@ class _CareTaskCardState extends ConsumerState<_CareTaskCard> content: Text(body), actions: [ TextButton( - onPressed: () => Navigator.pop(d, false), - child: const Text('Cancel'), - ), + onPressed: () => Navigator.pop(d, false), + child: const Text('Cancel')), FilledButton( onPressed: () => Navigator.pop(d, true), style: FilledButton.styleFrom( - backgroundColor: Theme.of(d).colorScheme.error, - ), + backgroundColor: Theme.of(d).colorScheme.error), child: Text(confirmLabel), ), ], @@ -1032,7 +1016,8 @@ class _CareTaskCardState extends ConsumerState<_CareTaskCard> final cs = Theme.of(context).colorScheme; final task = widget.task; final done = task.isCompleted; - final due = !done && task.isDueToday; + // Only flag as urgently due when a specific scheduled time exists + final due = !done && task.isDueToday && task.scheduledTime != null; final color = _color; final yAnim = Tween(begin: 0.0, end: -72.0).animate( @@ -1052,34 +1037,61 @@ class _CareTaskCardState extends ConsumerState<_CareTaskCard> padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 14), decoration: BoxDecoration( color: done - ? Color.alphaBlend(color.withAlpha(36), cs.surface) + ? Color.alphaBlend(color.withAlpha(28), cs.surface) : cs.surface, - borderRadius: BorderRadius.circular(22), - border: Border.all(color: done ? color : pt.line, width: 2), + borderRadius: BorderRadius.circular(20), + border: Border.all( + color: done + ? color.withAlpha(180) + : (due ? AppColors.poppy.withAlpha(160) : pt.line), + width: done ? 1.5 : (due ? 1.5 : 1), + ), boxShadow: due ? [ BoxShadow( - color: AppColors.poppy.withAlpha(64), - blurRadius: 0, - spreadRadius: 4, + color: AppColors.poppy.withAlpha(40), + blurRadius: 16, + offset: const Offset(0, 6), + spreadRadius: -4, ), + ...pt.shadowE1, ] - : pt.shadowE1, + : pt.shadowE2, ), child: Row( children: [ // ── Icon box ────────────────────────────────────────────────── AnimatedContainer( duration: const Duration(milliseconds: 240), - width: 52, - height: 52, + width: 54, + height: 54, decoration: BoxDecoration( borderRadius: BorderRadius.circular(16), - color: done ? color : color.withAlpha(48), + gradient: LinearGradient( + begin: Alignment.topLeft, + end: Alignment.bottomRight, + colors: done + ? [color, Color.lerp(color, Colors.white, 0.28)!] + : [color.withAlpha(55), color.withAlpha(22)], + ), + boxShadow: done + ? [ + BoxShadow( + color: color.withAlpha(90), + blurRadius: 10, + offset: const Offset(0, 4), + spreadRadius: -3, + ), + ] + : null, ), alignment: Alignment.center, child: AnimatedSwitcher( duration: const Duration(milliseconds: 200), + transitionBuilder: (child, anim) => ScaleTransition( + scale: anim, + child: child, + ), child: Text( key: ValueKey(done), done ? '✅' : _emoji, @@ -1099,14 +1111,12 @@ class _CareTaskCardState extends ConsumerState<_CareTaskCard> Flexible( child: AnimatedDefaultTextStyle( duration: const Duration(milliseconds: 200), - style: TextStyle( - fontSize: 15, - fontWeight: FontWeight.w800, + style: Theme.of(context).textTheme.titleSmall!.copyWith( + fontWeight: FontWeight.w700, color: done ? pt.ink500 : cs.onSurface, - decoration: done - ? TextDecoration.lineThrough - : null, + decoration: done ? TextDecoration.lineThrough : null, decorationColor: pt.ink300, + height: 1.2, ), child: Text( task.title, @@ -1119,9 +1129,7 @@ class _CareTaskCardState extends ConsumerState<_CareTaskCard> const SizedBox(width: 6), Container( padding: const EdgeInsets.symmetric( - horizontal: 7, - vertical: 2, - ), + horizontal: 8, vertical: 3), decoration: BoxDecoration( color: AppColors.lilacSoft, borderRadius: BorderRadius.circular(999), @@ -1129,25 +1137,44 @@ class _CareTaskCardState extends ConsumerState<_CareTaskCard> child: Text( _frequencyPill(task.frequency), style: const TextStyle( - fontSize: 10, + fontSize: 9.5, fontWeight: FontWeight.w900, + letterSpacing: 0.3, color: AppColors.lilac700, + height: 1.2, ), ), ), ], ], ), - const SizedBox(height: 3), - Text( - _sublabel, - style: TextStyle( - fontSize: 12, - fontWeight: FontWeight.w700, - color: due ? AppColors.poppy700 : pt.ink500, - ), - maxLines: 1, - overflow: TextOverflow.ellipsis, + const SizedBox(height: 4), + Row( + children: [ + if (due) ...[ + Container( + width: 6, + height: 6, + margin: const EdgeInsets.only(right: 5), + decoration: const BoxDecoration( + color: AppColors.poppy, + shape: BoxShape.circle, + ), + ), + ], + Flexible( + child: Text( + _sublabel, + style: TextStyle( + fontSize: 12, + fontWeight: FontWeight.w600, + color: due ? AppColors.poppy700 : pt.ink500, + ), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + ), + ], ), ], ), @@ -1161,12 +1188,20 @@ class _CareTaskCardState extends ConsumerState<_CareTaskCard> AnimatedContainer( duration: const Duration(milliseconds: 200), padding: const EdgeInsets.symmetric( - horizontal: 9, - vertical: 4, - ), + horizontal: 10, vertical: 5), decoration: BoxDecoration( - color: done ? AppColors.mintSoft : AppColors.sunnySoft, + gradient: LinearGradient( + colors: done + ? [AppColors.mintSoft, AppColors.mintSoft] + : [AppColors.sunnySoft, AppColors.sunnySoft], + ), borderRadius: BorderRadius.circular(999), + border: Border.all( + color: done + ? AppColors.mint.withAlpha(80) + : AppColors.sunny.withAlpha(80), + width: 1, + ), ), child: Row( mainAxisSize: MainAxisSize.min, @@ -1174,40 +1209,41 @@ class _CareTaskCardState extends ConsumerState<_CareTaskCard> Text( '+${task.gamificationPoints}', style: TextStyle( - fontSize: 12, + fontSize: 11, fontWeight: FontWeight.w900, + letterSpacing: 0.2, color: done ? AppColors.mint700 : AppColors.sunny700, + height: 1, ), ), - const SizedBox(width: 2), - const Text('⭐', style: TextStyle(fontSize: 11)), + const SizedBox(width: 3), + const Text('⭐', style: TextStyle(fontSize: 10, height: 1)), ], ), ), - const SizedBox(height: 6), + const SizedBox(height: 8), GestureDetector( key: ValueKey('care_task_check_${task.id}'), onTap: _toggle, behavior: HitTestBehavior.opaque, child: AnimatedContainer( duration: const Duration(milliseconds: 200), - width: 34, - height: 34, + width: 36, + height: 36, decoration: BoxDecoration( shape: BoxShape.circle, color: done ? color : cs.surface, border: Border.all( color: done ? color : pt.line, - width: 2, + width: done ? 0 : 2, ), + boxShadow: done + ? [BoxShadow(color: color.withAlpha(80), blurRadius: 8, offset: const Offset(0, 3), spreadRadius: -2)] + : null, ), alignment: Alignment.center, child: done - ? const Icon( - Icons.check_rounded, - color: Colors.white, - size: 18, - ) + ? const Icon(Icons.check_rounded, color: Colors.white, size: 20) : null, ), ), @@ -1319,11 +1355,7 @@ class _TaskContextMenu extends StatelessWidget { borderRadius: const BorderRadius.vertical(top: Radius.circular(24)), ), padding: EdgeInsets.fromLTRB( - 20, - 0, - 20, - MediaQuery.paddingOf(context).bottom + 16, - ), + 20, 0, 20, MediaQuery.paddingOf(context).bottom + 16), child: Column( mainAxisSize: MainAxisSize.min, children: [ @@ -1334,46 +1366,38 @@ class _TaskContextMenu extends StatelessWidget { width: 36, height: 4, decoration: BoxDecoration( - color: pt.line, - borderRadius: BorderRadius.circular(2), - ), + color: pt.line, + borderRadius: BorderRadius.circular(2)), ), ), ), - Text( - taskTitle, - style: TextStyle( - fontSize: 16, - fontWeight: FontWeight.w800, - color: cs.onSurface, - ), - ), + Text(taskTitle, + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.w800, + color: cs.onSurface)), const SizedBox(height: 12), if (onAddPlan != null) _MenuTile( - icon: Icons.add_task_rounded, - label: 'Add to plan', - onTap: onAddPlan!, - ), + icon: Icons.add_task_rounded, + label: 'Add to plan', + onTap: onAddPlan!), if (onRemoveDay != null) _MenuTile( - icon: Icons.remove_circle_outline_rounded, - label: 'Remove from day', - onTap: onRemoveDay!, - ), + icon: Icons.remove_circle_outline_rounded, + label: 'Remove from day', + onTap: onRemoveDay!), if (onEdit != null) _MenuTile( - icon: Icons.edit_outlined, - label: 'Edit task', - onTap: onEdit!, - ), + icon: Icons.edit_outlined, + label: 'Edit task', + onTap: onEdit!), if (onDelete != null) _MenuTile( - icon: Icons.delete_outline_rounded, - label: 'Delete task', - color: cs.error, - onTap: onDelete!, - ), + icon: Icons.delete_outline_rounded, + label: 'Delete task', + color: cs.error, + onTap: onDelete!), ], ), ); @@ -1381,12 +1405,8 @@ class _TaskContextMenu extends StatelessWidget { } class _MenuTile extends StatelessWidget { - const _MenuTile({ - required this.icon, - required this.label, - required this.onTap, - this.color, - }); + const _MenuTile( + {required this.icon, required this.label, required this.onTap, this.color}); final IconData icon; final String label; final VoidCallback onTap; @@ -1397,10 +1417,7 @@ class _MenuTile extends StatelessWidget { final c = color ?? Theme.of(context).colorScheme.onSurface; return ListTile( leading: Icon(icon, color: c), - title: Text( - label, - style: TextStyle(color: c, fontWeight: FontWeight.w600), - ), + title: Text(label, style: TextStyle(color: c, fontWeight: FontWeight.w600)), onTap: onTap, shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), ); @@ -1429,11 +1446,7 @@ class _TaskCardSkeleton extends StatelessWidget { borderRadius: BorderRadius.circular(16), border: Border.all(color: pt.line, width: 0.5), boxShadow: [ - const BoxShadow( - color: AppColors.shadowE1L, - blurRadius: 2, - offset: Offset(0, 1), - ), + const BoxShadow(color: AppColors.shadowE1L, blurRadius: 2, offset: Offset(0, 1)), ], ), child: Row( @@ -1515,11 +1528,7 @@ class _CareErrorCard extends StatelessWidget { // ───────────────────────────────────────────────────────────────────────────── class _EmptyRoutineState extends StatelessWidget { - const _EmptyRoutineState({ - required this.petName, - required this.date, - this.onAddTask, - }); + const _EmptyRoutineState({required this.petName, required this.date, this.onAddTask}); final String petName; final DateTime date; @@ -1529,8 +1538,7 @@ class _EmptyRoutineState extends StatelessWidget { Widget build(BuildContext context) { final pt = Theme.of(context).extension()!; final cs = Theme.of(context).colorScheme; - final isToday = - DateUtils.dateOnly(date) == DateUtils.dateOnly(DateTime.now()); + final isToday = DateUtils.dateOnly(date) == DateUtils.dateOnly(DateTime.now()); return Padding( padding: const EdgeInsets.symmetric(vertical: 32), @@ -1572,13 +1580,8 @@ class _EmptyRoutineState extends StatelessWidget { icon: const Icon(Icons.add_rounded, size: 16), label: const Text('Add first task'), style: OutlinedButton.styleFrom( - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(12), - ), - padding: const EdgeInsets.symmetric( - horizontal: 20, - vertical: 11, - ), + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), + padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 11), ), ), ], @@ -1594,68 +1597,45 @@ class _EmptyRoutineState extends StatelessWidget { String _frequencyPill(dbtask.CareFrequency f) { switch (f) { - case dbtask.CareFrequency.monthly: - return 'MONTHLY'; - case dbtask.CareFrequency.biweekly: - return 'BIWEEKLY'; - case dbtask.CareFrequency.weekly: - return 'WEEKLY'; - default: - return 'WEEKLY'; + case dbtask.CareFrequency.monthly: return 'MONTHLY'; + case dbtask.CareFrequency.biweekly: return 'BIWEEKLY'; + case dbtask.CareFrequency.weekly: return 'WEEKLY'; + case dbtask.CareFrequency.twiceDaily: return '2× DAILY'; + case dbtask.CareFrequency.daily: return 'DAILY'; + case dbtask.CareFrequency.once: return 'ONCE'; + case dbtask.CareFrequency.asNeeded: return 'AS NEEDED'; } } String _typeLabel(dbtask.CareTaskType type) { switch (type) { - case dbtask.CareTaskType.feeding: - return 'Feeding'; - case dbtask.CareTaskType.walk: - return 'Walk'; - case dbtask.CareTaskType.grooming: - return 'Grooming'; - case dbtask.CareTaskType.medication: - return 'Meds'; - case dbtask.CareTaskType.vetVisit: - return 'Vet Visit'; - case dbtask.CareTaskType.training: - return 'Training'; - case dbtask.CareTaskType.playtime: - return 'Playtime'; - case dbtask.CareTaskType.dental: - return 'Dental'; - case dbtask.CareTaskType.nailTrim: - return 'Nail Trim'; - case dbtask.CareTaskType.bath: - return 'Bath'; - case dbtask.CareTaskType.other: - return 'Other'; + case dbtask.CareTaskType.feeding: return 'Feeding'; + case dbtask.CareTaskType.walk: return 'Walk'; + case dbtask.CareTaskType.grooming: return 'Grooming'; + case dbtask.CareTaskType.medication: return 'Meds'; + case dbtask.CareTaskType.vetVisit: return 'Vet Visit'; + case dbtask.CareTaskType.training: return 'Training'; + case dbtask.CareTaskType.playtime: return 'Playtime'; + case dbtask.CareTaskType.dental: return 'Dental'; + case dbtask.CareTaskType.nailTrim: return 'Nail Trim'; + case dbtask.CareTaskType.bath: return 'Bath'; + case dbtask.CareTaskType.other: return 'Other'; } } String _defaultTitle(dbtask.CareTaskType type) { switch (type) { - case dbtask.CareTaskType.feeding: - return 'Feeding time'; - case dbtask.CareTaskType.walk: - return 'Walk'; - case dbtask.CareTaskType.grooming: - return 'Grooming session'; - case dbtask.CareTaskType.medication: - return 'Medication'; - case dbtask.CareTaskType.vetVisit: - return 'Vet visit'; - case dbtask.CareTaskType.training: - return 'Training'; - case dbtask.CareTaskType.playtime: - return 'Playtime'; - case dbtask.CareTaskType.dental: - return 'Dental care'; - case dbtask.CareTaskType.nailTrim: - return 'Nail trim'; - case dbtask.CareTaskType.bath: - return 'Bath time'; - case dbtask.CareTaskType.other: - return 'New task'; + case dbtask.CareTaskType.feeding: return 'Feeding time'; + case dbtask.CareTaskType.walk: return 'Walk'; + case dbtask.CareTaskType.grooming: return 'Grooming session'; + case dbtask.CareTaskType.medication: return 'Medication'; + case dbtask.CareTaskType.vetVisit: return 'Vet visit'; + case dbtask.CareTaskType.training: return 'Training'; + case dbtask.CareTaskType.playtime: return 'Playtime'; + case dbtask.CareTaskType.dental: return 'Dental care'; + case dbtask.CareTaskType.nailTrim: return 'Nail trim'; + case dbtask.CareTaskType.bath: return 'Bath time'; + case dbtask.CareTaskType.other: return 'New task'; } } @@ -1663,74 +1643,48 @@ String _defaultTitle(dbtask.CareTaskType type) { // Medical vault & nutrition entry banners // ───────────────────────────────────────────────────────────────────────────── -class _MedicalVaultBanner extends StatelessWidget { - const _MedicalVaultBanner({required this.pt}); +// ── Merged utility banner (Nutrition | Medical Vault side-by-side) ──────────── + +class _UtilityBanner extends StatelessWidget { + const _UtilityBanner({required this.pt}); final PetfolioThemeExtension pt; @override Widget build(BuildContext context) { - return GestureDetector( - key: const ValueKey('care_medical_vault_banner'), - onTap: () => context.push('/care/medical-vault'), - child: Container( - decoration: BoxDecoration( - gradient: LinearGradient( - begin: Alignment.topLeft, - end: Alignment.bottomRight, - colors: [ - AppColors.mintSoft, - Color.lerp( - AppColors.mintSoft, - AppColors.mint.withAlpha(40), - 0.5, - )!, - ], - ), - borderRadius: BorderRadius.circular(PetfolioThemeExtension.radiusLg), - border: Border.all(color: AppColors.mint.withAlpha(60)), - boxShadow: pt.shadowE1, - ), - padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 16), + final cs = Theme.of(context).colorScheme; + return Container( + decoration: BoxDecoration( + color: cs.surface, + borderRadius: BorderRadius.circular(22), + border: Border.all(color: pt.line), + boxShadow: pt.shadowE1, + ), + child: IntrinsicHeight( child: Row( + crossAxisAlignment: CrossAxisAlignment.stretch, children: [ - Container( - width: 46, - height: 46, - decoration: BoxDecoration( - color: AppColors.mint.withAlpha(40), - borderRadius: BorderRadius.circular( - PetfolioThemeExtension.radiusMd, - ), - ), - child: const Icon( - Icons.folder_special_outlined, - color: AppColors.mint700, - size: 24, - ), + _UtilityHalf( + key: const ValueKey('care_nutrition_banner'), + icon: Icons.monitor_weight_outlined, + iconBg: AppColors.sunnySoft, + iconColor: AppColors.sunny700, + title: 'Nutrition', + subtitle: 'Weight & caloric needs', + detail: 'Track daily feeding', + onTap: () => context.push('/care/nutrition'), ), - const SizedBox(width: 14), - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - const Text( - 'Medical Vault', - style: TextStyle( - fontSize: 15, - fontWeight: FontWeight.w700, - color: AppColors.mint700, - ), - ), - const SizedBox(height: 2), - Text( - 'Vaccines · Medications · Vet visits', - style: TextStyle(fontSize: 13, color: pt.ink500), - ), - ], - ), + VerticalDivider(width: 1, thickness: 1, color: pt.line), + _UtilityHalf( + key: const ValueKey('care_medical_vault_banner'), + icon: Icons.folder_special_outlined, + iconBg: AppColors.mintSoft, + iconColor: AppColors.mint700, + title: 'Medical Vault', + subtitle: 'Vaccines · Meds · Vet', + detail: 'View health records', + onTap: () => context.push('/care/medical-vault'), ), - const Icon(Icons.chevron_right_rounded, color: AppColors.mint700), ], ), ), @@ -1738,75 +1692,85 @@ class _MedicalVaultBanner extends StatelessWidget { } } -class _NutritionBanner extends StatelessWidget { - const _NutritionBanner({required this.pt}); +class _UtilityHalf extends StatelessWidget { + const _UtilityHalf({ + super.key, + required this.icon, + required this.iconBg, + required this.iconColor, + required this.title, + required this.subtitle, + required this.detail, + required this.onTap, + }); - final PetfolioThemeExtension pt; + final IconData icon; + final Color iconBg; + final Color iconColor; + final String title; + final String subtitle; + final String detail; + final VoidCallback onTap; @override Widget build(BuildContext context) { - return GestureDetector( - key: const ValueKey('care_nutrition_banner'), - onTap: () => context.push('/care/nutrition'), - child: Container( - decoration: BoxDecoration( - gradient: LinearGradient( - begin: Alignment.topLeft, - end: Alignment.bottomRight, - colors: [ - AppColors.sunnySoft, - Color.lerp( - AppColors.sunnySoft, - AppColors.tangerine.withAlpha(40), - 0.5, - )!, - ], - ), - borderRadius: BorderRadius.circular(PetfolioThemeExtension.radiusLg), - border: Border.all(color: AppColors.tangerine.withAlpha(60)), - boxShadow: pt.shadowE1, - ), - padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 16), - child: Row( - children: [ - Container( - width: 46, - height: 46, - decoration: BoxDecoration( - color: AppColors.tangerine.withAlpha(40), - borderRadius: BorderRadius.circular( - PetfolioThemeExtension.radiusMd, - ), - ), - child: const Icon( - Icons.monitor_weight_outlined, - color: AppColors.tangerine, - size: 24, - ), - ), - const SizedBox(width: 14), - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, + final pt = Theme.of(context).extension()!; + return Expanded( + child: GestureDetector( + onTap: onTap, + child: Padding( + padding: const EdgeInsets.all(13), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - const Text( - 'Smart Nutrition & Weight', - style: TextStyle( - fontSize: 15, - fontWeight: FontWeight.w700, - color: AppColors.tangerine, + Container( + width: 36, + height: 36, + decoration: BoxDecoration( + color: iconBg, + borderRadius: BorderRadius.circular(12), ), + alignment: Alignment.center, + child: Icon(icon, size: 20, color: iconColor), ), - const SizedBox(height: 2), - Text( - 'Track weight history · View caloric needs', - style: TextStyle(fontSize: 13, color: pt.ink500), - ), + Icon(Icons.chevron_right_rounded, size: 15, color: pt.ink300), ], ), - ), - const Icon(Icons.chevron_right_rounded, color: AppColors.tangerine), - ], + const SizedBox(height: 8), + Text( + title, + style: TextStyle( + fontSize: 13, + fontWeight: FontWeight.w800, + color: pt.ink950, + height: 1.15, + ), + ), + const SizedBox(height: 3), + Text( + subtitle, + style: TextStyle( + fontSize: 11, + fontWeight: FontWeight.w700, + color: iconColor, + height: 1, + ), + ), + const SizedBox(height: 1), + Text( + detail, + style: TextStyle( + fontSize: 10.5, + fontWeight: FontWeight.w600, + color: pt.ink500, + height: 1, + ), + ), + ], + ), ), ), ); @@ -1867,9 +1831,7 @@ class _CareTaskFormSheetState extends ConsumerState<_CareTaskFormSheet> { _titleCtrl = TextEditingController(text: seed.title); _time = parseCareScheduledTimeOfDay(seed.scheduledTime); } else { - _titleCtrl = TextEditingController( - text: _defaultTitle(dbtask.CareTaskType.feeding), - ); + _titleCtrl = TextEditingController(text: _defaultTitle(dbtask.CareTaskType.feeding)); } _titleFocus.addListener(() { if (mounted) setState(() => _titleFocused = _titleFocus.hasFocus); @@ -1956,10 +1918,7 @@ class _CareTaskFormSheetState extends ConsumerState<_CareTaskFormSheet> { child: Container( width: 36, height: 4, - decoration: BoxDecoration( - color: pt.line, - borderRadius: BorderRadius.circular(2), - ), + decoration: BoxDecoration(color: pt.line, borderRadius: BorderRadius.circular(2)), ), ), ), @@ -2008,11 +1967,8 @@ class _CareTaskFormSheetState extends ConsumerState<_CareTaskFormSheet> { child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ - Icon( - _taskTypeIcon(t), - size: 22, - color: selected ? Colors.white : pt.ink500, - ), + Icon(_taskTypeIcon(t), size: 22, + color: selected ? Colors.white : pt.ink500), const SizedBox(height: 5), Text( _typeLabel(t), @@ -2041,12 +1997,7 @@ class _CareTaskFormSheetState extends ConsumerState<_CareTaskFormSheet> { decoration: BoxDecoration( borderRadius: BorderRadius.circular(12), boxShadow: _titleFocused - ? [ - BoxShadow( - color: cs.primary.withAlpha(30), - blurRadius: 8, - ), - ] + ? [BoxShadow(color: cs.primary.withAlpha(30), blurRadius: 8)] : [], ), child: TextField( @@ -2059,10 +2010,7 @@ class _CareTaskFormSheetState extends ConsumerState<_CareTaskFormSheet> { hintText: 'e.g. Morning feeding', filled: true, fillColor: _titleFocused ? cs.surface : pt.surface2, - contentPadding: const EdgeInsets.symmetric( - horizontal: 16, - vertical: 14, - ), + contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 14), enabledBorder: OutlineInputBorder( borderRadius: BorderRadius.circular(12), borderSide: BorderSide(color: pt.line, width: 0.5), @@ -2090,10 +2038,7 @@ class _CareTaskFormSheetState extends ConsumerState<_CareTaskFormSheet> { onTap: () => setState(() => _frequency = f), child: AnimatedContainer( duration: PetfolioThemeExtension.durationSm, - padding: const EdgeInsets.symmetric( - horizontal: 16, - vertical: 9, - ), + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 9), decoration: BoxDecoration( color: selected ? cs.primary : pt.surface2, borderRadius: BorderRadius.circular(40), @@ -2130,10 +2075,7 @@ class _CareTaskFormSheetState extends ConsumerState<_CareTaskFormSheet> { if (picked != null && mounted) setState(() => _time = picked); }, child: Container( - padding: const EdgeInsets.symmetric( - horizontal: 16, - vertical: 14, - ), + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 14), decoration: BoxDecoration( color: pt.surface2, borderRadius: BorderRadius.circular(12), @@ -2154,18 +2096,10 @@ class _CareTaskFormSheetState extends ConsumerState<_CareTaskFormSheet> { if (_time != null) GestureDetector( onTap: () => setState(() => _time = null), - child: Icon( - Icons.close_rounded, - size: 16, - color: pt.ink300, - ), + child: Icon(Icons.close_rounded, size: 16, color: pt.ink300), ) else - Icon( - Icons.chevron_right_rounded, - size: 18, - color: pt.ink300, - ), + Icon(Icons.chevron_right_rounded, size: 18, color: pt.ink300), ], ), ), @@ -2179,27 +2113,19 @@ class _CareTaskFormSheetState extends ConsumerState<_CareTaskFormSheet> { child: FilledButton( onPressed: _saving ? null : _save, style: FilledButton.styleFrom( - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(14), - ), + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(14)), ), child: _saving ? const SizedBox( width: 20, height: 20, - child: CircularProgressIndicator( - strokeWidth: 2, - color: Colors.white, - ), + child: CircularProgressIndicator(strokeWidth: 2, color: Colors.white), ) : Text( _isEdit ? 'Save changes' : (_isPrefilledCreate ? 'Save plan' : 'Add Task'), - style: const TextStyle( - fontWeight: FontWeight.w700, - fontSize: 15, - ), + style: const TextStyle(fontWeight: FontWeight.w700, fontSize: 15), ), ), ), @@ -2211,20 +2137,13 @@ class _CareTaskFormSheetState extends ConsumerState<_CareTaskFormSheet> { String _freqLabel(dbtask.CareFrequency f) { switch (f) { - case dbtask.CareFrequency.once: - return 'Once'; - case dbtask.CareFrequency.daily: - return 'Daily'; - case dbtask.CareFrequency.twiceDaily: - return 'Twice daily'; - case dbtask.CareFrequency.weekly: - return 'Weekly'; - case dbtask.CareFrequency.biweekly: - return 'Every 2 wks'; - case dbtask.CareFrequency.monthly: - return 'Monthly'; - case dbtask.CareFrequency.asNeeded: - return 'As needed'; + case dbtask.CareFrequency.once: return 'Once'; + case dbtask.CareFrequency.daily: return 'Daily'; + case dbtask.CareFrequency.twiceDaily: return 'Twice daily'; + case dbtask.CareFrequency.weekly: return 'Weekly'; + case dbtask.CareFrequency.biweekly: return 'Every 2 wks'; + case dbtask.CareFrequency.monthly: return 'Monthly'; + case dbtask.CareFrequency.asNeeded: return 'As needed'; } } } @@ -2236,39 +2155,28 @@ class _SheetLabel extends StatelessWidget { @override Widget build(BuildContext context) => Text( - text, - style: TextStyle( - fontSize: 12, - fontWeight: FontWeight.w700, - letterSpacing: 0.08 * 12, - color: pt.ink500, - ), - ); + text, + style: TextStyle( + fontSize: 12, + fontWeight: FontWeight.w700, + letterSpacing: 0.08 * 12, + color: pt.ink500, + ), + ); } IconData _taskTypeIcon(dbtask.CareTaskType type) { switch (type) { - case dbtask.CareTaskType.feeding: - return Icons.restaurant_menu_rounded; - case dbtask.CareTaskType.walk: - return Icons.directions_walk_rounded; - case dbtask.CareTaskType.grooming: - return Icons.content_cut_rounded; - case dbtask.CareTaskType.medication: - return Icons.medication_rounded; - case dbtask.CareTaskType.vetVisit: - return Icons.local_hospital_rounded; - case dbtask.CareTaskType.training: - return Icons.school_rounded; - case dbtask.CareTaskType.playtime: - return Icons.sports_tennis_rounded; - case dbtask.CareTaskType.dental: - return Icons.medical_services_rounded; - case dbtask.CareTaskType.nailTrim: - return Icons.cut_rounded; - case dbtask.CareTaskType.bath: - return Icons.water_drop_rounded; - case dbtask.CareTaskType.other: - return Icons.star_outline_rounded; + case dbtask.CareTaskType.feeding: return Icons.restaurant_menu_rounded; + case dbtask.CareTaskType.walk: return Icons.directions_walk_rounded; + case dbtask.CareTaskType.grooming: return Icons.content_cut_rounded; + case dbtask.CareTaskType.medication: return Icons.medication_rounded; + case dbtask.CareTaskType.vetVisit: return Icons.local_hospital_rounded; + case dbtask.CareTaskType.training: return Icons.school_rounded; + case dbtask.CareTaskType.playtime: return Icons.sports_tennis_rounded; + case dbtask.CareTaskType.dental: return Icons.medical_services_rounded; + case dbtask.CareTaskType.nailTrim: return Icons.cut_rounded; + case dbtask.CareTaskType.bath: return Icons.water_drop_rounded; + case dbtask.CareTaskType.other: return Icons.star_outline_rounded; } } diff --git a/lib/features/care/presentation/widgets/gamified_care_ui.dart b/lib/features/care/presentation/widgets/gamified_care_ui.dart index 317d828..47a7729 100644 --- a/lib/features/care/presentation/widgets/gamified_care_ui.dart +++ b/lib/features/care/presentation/widgets/gamified_care_ui.dart @@ -1,3 +1,5 @@ +import 'dart:math' as math; + import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; @@ -5,12 +7,14 @@ import 'package:petfolio/core/theme/theme.dart'; import 'package:petfolio/core/widgets/widgets.dart'; import '../../../../core/models/pet.dart'; +import '../../data/models/care_task.dart' show CareFrequency; +import '../../data/models/care_task_log.dart'; import '../../data/models/pet_awards_summary.dart'; import '../../data/models/pet_level.dart'; import '../controllers/care_dashboard_controller.dart'; import '../controllers/pet_awards_provider.dart'; -// ── Gamified Care Header ────────────────────────────────────────────────────── +// ── Compact Hero Header ──────────────────────────────────────────────────────── class CareGamifiedHeader extends ConsumerStatefulWidget { const CareGamifiedHeader({ @@ -28,307 +32,515 @@ class CareGamifiedHeader extends ConsumerStatefulWidget { class _CareGamifiedHeaderState extends ConsumerState with TickerProviderStateMixin { - late final AnimationController _bounceCtrl; + late final AnimationController _coinCtrl; late final AnimationController _pulseCtrl; - late final Animation _bounceAnim; - late final Animation _pulseScale; - late final Animation _pulseOpacity; @override void initState() { super.initState(); - _bounceCtrl = AnimationController( + _coinCtrl = AnimationController( vsync: this, - duration: const Duration(milliseconds: 2400), - )..repeat(reverse: true); - _bounceAnim = Tween(begin: 0.0, end: -8.0).animate( - CurvedAnimation(parent: _bounceCtrl, curve: Curves.easeInOut), - ); - + duration: const Duration(milliseconds: 4500), + )..repeat(); _pulseCtrl = AnimationController( vsync: this, - duration: const Duration(milliseconds: 2000), + duration: const Duration(milliseconds: 2200), )..repeat(); - _pulseScale = Tween(begin: 1.0, end: 1.40).animate( - CurvedAnimation(parent: _pulseCtrl, curve: Curves.easeOut), - ); - _pulseOpacity = Tween(begin: 0.75, end: 0.0).animate( - CurvedAnimation(parent: _pulseCtrl, curve: Curves.easeOut), - ); } @override void dispose() { - _bounceCtrl.dispose(); + _coinCtrl.dispose(); _pulseCtrl.dispose(); super.dispose(); } @override Widget build(BuildContext context) { - final petId = widget.activePet.id; + final topPad = MediaQuery.paddingOf(context).top; - // Real streak from realtime stream already in dashboard state. final streak = widget.dashboard.streak.maybeWhen( data: (s) => s.currentStreak, orElse: () => 0, ); - - // Real XP + level from Supabase RPC (same provider used by profile screen). - final awardsAsync = ref.watch(petAwardsSummaryProvider(petId)); - final totalXp = awardsAsync.maybeWhen( - data: (a) => a.totalXp, - orElse: () => 0, + final awardsAsync = ref.watch(petAwardsSummaryProvider(widget.activePet.id)); + final lv = awardsAsync.maybeWhen( + data: (a) => PetLevel.fromXp(a.totalXp), + orElse: () => PetLevel.fromXp(0), ); - final lv = PetLevel.fromXp(totalXp); - final sp = widget.activePet.speciesEnum; - final isDark = Theme.of(context).brightness == Brightness.dark; + final tasks = widget.dashboard.tasks.value ?? []; + final planned = tasks + .where((t) => !t.isLogDerived && t.frequency != CareFrequency.asNeeded) + .toList(); + final doneToday = planned.where((t) => t.isCompleted).length; + final totalToday = planned.length; + final pct = lv.progress.clamp(0.0, 1.0); - Color headerColor = sp.resolvedAccent(isDark); - final dbAccent = widget.activePet.accentColor; - if (dbAccent != null && dbAccent.isNotEmpty && dbAccent != '#FF6B9D') { - try { - final hex = dbAccent.replaceAll('#', ''); - if (hex.length == 6) { - headerColor = Color(int.parse('FF$hex', radix: 16)); - } else if (hex.length == 8) { - headerColor = Color(int.parse(hex, radix: 16)); - } - } catch (_) {} - } + // ── WaveHeader pattern (matches Home/Pets screen architecture) ────────── + // AppShellHeader overlays at Positioned(top:0) with height = topPad + 76. + // We reserve that space at top, then show greeting, then let the hero + // card float at the wave boundary — exactly how PetProfileScreen works. + return Stack( + clipBehavior: Clip.none, + children: [ + WaveHeader( + color: AppColors.tangerine, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // ── Clear AppShellHeader ─────────────────────────────────── + SizedBox(height: topPad + 76), - return WaveHeader( - color: headerColor, - child: Column( - children: [ - SizedBox(height: MediaQuery.paddingOf(context).top + 76.0), - Padding( - padding: const EdgeInsets.fromLTRB(16, 12, 16, 0), - child: Row( - crossAxisAlignment: CrossAxisAlignment.center, + // ── Care greeting — unique copy, never duplicates hero card ── + Padding( + padding: const EdgeInsets.fromLTRB(22, 10, 22, 0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + '${widget.activePet.name}\'s care plan', + style: TextStyle( + fontSize: 13, + fontWeight: FontWeight.w600, + color: Colors.white.withAlpha(210), + letterSpacing: 0.1, + ), + ), + const SizedBox(height: 5), + Text.rich( + TextSpan( + text: streak > 1 + ? '$streak days strong!' + : 'Let\'s crush today!', + children: const [ + TextSpan(text: ' 🔥'), + ], + ), + style: Theme.of(context).textTheme.headlineMedium?.copyWith( + fontWeight: FontWeight.w800, + color: Colors.white, + height: 1.05, + letterSpacing: -0.3, + fontStyle: FontStyle.italic, + ), + ), + ], + ), + ), + + // ── Breathing room: greeting height (~44px) + 33px gap + card top + // card_top = waveHeight + 28 - 115 must be > greetingEnd + // waveHeight = topPad + 76 + 44(greeting) + SizedBox + // SizedBox = 120 → gap ≈ 33px on all device topPad values + const SizedBox(height: 120), + ], + ), + ), + + // ── Floating hero card (streak coin + XP bar) ───────────────────── + // Positioned at bottom:-28 so it straddles the wave edge, matching + // the same floating-card pattern used in PetProfileScreen. + Positioned( + left: 16, + right: 16, + bottom: -28, + child: Container( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(24), + gradient: const LinearGradient( + begin: Alignment.topLeft, + end: Alignment.bottomRight, + colors: [ + Color(0xFFFF2E2E), + Color(0xFFFF5830), + AppColors.tangerine, + ], + stops: [0.0, 0.48, 1.0], + ), + boxShadow: const [ + BoxShadow( + color: Color(0x60FF3D3D), + blurRadius: 32, + offset: Offset(0, 16), + spreadRadius: -12, + ), + BoxShadow( + color: Color(0x20000000), + blurRadius: 8, + offset: Offset(0, 4), + spreadRadius: -2, + ), + ], + ), + clipBehavior: Clip.hardEdge, + child: Stack( children: [ - // ── Streak flame circle ───────────────────────────────────── - SizedBox( - width: 120, - height: 120, - child: Stack( - alignment: Alignment.center, + // Decorative paw watermark + Positioned( + right: -6, + top: -8, + child: Opacity( + opacity: 0.16, + child: Transform.rotate( + angle: 18 * math.pi / 180, + child: const Icon(Icons.pets_rounded, size: 96, color: Colors.white), + ), + ), + ), + Padding( + padding: const EdgeInsets.fromLTRB(16, 15, 16, 16), + child: Row( + crossAxisAlignment: CrossAxisAlignment.center, children: [ - AnimatedBuilder( - animation: _pulseCtrl, - builder: (_, child) => Transform.scale( - scale: _pulseScale.value, - child: Opacity(opacity: _pulseOpacity.value, child: child), - ), - child: Container( - width: 112, - height: 112, - decoration: BoxDecoration( - shape: BoxShape.circle, - border: Border.all(color: AppColors.tangerine, width: 3), - ), - ), + _StreakCoin( + streak: streak, + coinCtrl: _coinCtrl, + pulseCtrl: _pulseCtrl, ), - AnimatedBuilder( - animation: _bounceCtrl, - builder: (_, child) => Transform.translate( - offset: Offset(0, _bounceAnim.value), - child: child, - ), - child: Container( - width: 104, - height: 104, - decoration: const BoxDecoration( - shape: BoxShape.circle, - gradient: RadialGradient( - center: Alignment(0, 0.2), - radius: 0.7, - colors: [AppColors.sunny, AppColors.tangerine], - stops: [0.0, 0.7], - ), - boxShadow: [ - BoxShadow( - color: AppColors.tangerine, - blurRadius: 28, - offset: Offset(0, 14), - spreadRadius: -8, - ), - ], + const SizedBox(width: 14), + Expanded( + child: awardsAsync.when( + loading: () => _HeroLevelContent( + lv: PetLevel.fromXp(0), + pct: 0, + doneToday: 0, + totalToday: totalToday, ), - alignment: Alignment.center, - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - const Text('🔥', style: TextStyle(fontSize: 36, height: 1.0)), - const SizedBox(height: 2), - Text( - '$streak', - style: const TextStyle( - fontSize: 28, - fontWeight: FontWeight.w900, - color: Colors.white, - height: 1.0, - ), - ), - const Text( - 'DAY STREAK', - style: TextStyle( - fontSize: 10, - fontWeight: FontWeight.w800, - color: Colors.white, - letterSpacing: 0.6, - ), - ), - ], + error: (_, _) => _HeroLevelContent( + lv: PetLevel.fromXp(0), + pct: 0, + doneToday: doneToday, + totalToday: totalToday, + ), + data: (_) => _HeroLevelContent( + lv: lv, + pct: pct, + doneToday: doneToday, + totalToday: totalToday, ), ), ), ], ), ), + ], + ), + ), + ), + ], + ); + } +} - const SizedBox(width: 16), +// ── Streak Coin ──────────────────────────────────────────────────────────────── - // ── XP & level (real data) ─────────────────────────────────── - Expanded( - child: awardsAsync.when( - loading: () => _LevelSkeleton(), - error: (e, st) => _LevelContent(lv: PetLevel.fromXp(0)), - data: (_) => _LevelContent(lv: lv), +class _StreakCoin extends StatelessWidget { + const _StreakCoin({ + required this.streak, + required this.coinCtrl, + required this.pulseCtrl, + }); + + final int streak; + final Animation coinCtrl; + final Animation pulseCtrl; + + @override + Widget build(BuildContext context) { + return SizedBox( + width: 84, + height: 84, + child: Stack( + alignment: Alignment.center, + children: [ + // Expanding pulse ring + AnimatedBuilder( + animation: pulseCtrl, + builder: (_, _) { + final scale = 1.0 + pulseCtrl.value * 0.90; + final opacity = ((1.0 - pulseCtrl.value) * 0.50).clamp(0.0, 0.50); + return Transform.scale( + scale: scale, + child: Opacity( + opacity: opacity, + child: Container( + width: 84, + height: 84, + decoration: BoxDecoration( + shape: BoxShape.circle, + border: Border.all( + color: Colors.white.withAlpha(160), + width: 2, + ), + ), ), ), - ], + ); + }, + ), + // 3D Y-rotation coin + AnimatedBuilder( + animation: coinCtrl, + builder: (_, child) { + final angle = math.sin(coinCtrl.value * 2 * math.pi) * 20 * math.pi / 180; + return Transform( + alignment: Alignment.center, + transform: Matrix4.identity() + ..setEntry(3, 2, 0.0018) + ..rotateY(angle), + child: child, + ); + }, + child: Container( + width: 84, + height: 84, + decoration: BoxDecoration( + shape: BoxShape.circle, + gradient: const RadialGradient( + center: Alignment(-0.28, -0.42), + radius: 0.88, + colors: [ + Color(0xFFFFF0B0), + Color(0xFFFFD234), + AppColors.tangerine, + AppColors.tangerine700, + ], + stops: [0.0, 0.32, 0.68, 1.0], + ), + boxShadow: [ + BoxShadow( + color: AppColors.tangerine700.withAlpha(210), + blurRadius: 24, + offset: const Offset(0, 14), + spreadRadius: -10, + ), + BoxShadow( + color: Colors.black.withAlpha(30), + blurRadius: 6, + offset: const Offset(0, 3), + spreadRadius: -2, + ), + ], + ), + child: Stack( + alignment: Alignment.center, + children: [ + // Specular sheen highlight + Positioned( + left: 5, + top: 5, + right: 26, + bottom: 10, + child: Container( + decoration: BoxDecoration( + shape: BoxShape.circle, + gradient: LinearGradient( + begin: Alignment.topLeft, + end: Alignment.bottomRight, + colors: [ + Colors.white.withAlpha(120), + Colors.transparent, + ], + ), + ), + ), + ), + // Concentric inner ring for depth + Container( + width: 70, + height: 70, + decoration: BoxDecoration( + shape: BoxShape.circle, + border: Border.all( + color: Colors.white.withAlpha(60), + width: 1.5, + ), + ), + ), + Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const Text('🔥', style: TextStyle(fontSize: 22, height: 1.0)), + const SizedBox(height: 1), + Text( + '$streak', + style: const TextStyle( + fontSize: 22, + fontWeight: FontWeight.w900, + color: Colors.white, + height: 1.0, + shadows: [ + Shadow( + color: Color(0x80961400), + blurRadius: 6, + offset: Offset(0, 2), + ), + ], + ), + ), + const Text( + 'DAY STREAK', + style: TextStyle( + fontSize: 6.5, + fontWeight: FontWeight.w900, + color: Colors.white, + letterSpacing: 0.4, + height: 1.3, + ), + ), + ], + ), + ], + ), ), ), - const SizedBox(height: 55.0), ], ), ); } } -class _LevelContent extends StatelessWidget { - const _LevelContent({required this.lv}); +// ── Hero Level Content (right side of hero card) ─────────────────────────────── + +class _HeroLevelContent extends StatelessWidget { + const _HeroLevelContent({ + required this.lv, + required this.pct, + required this.doneToday, + required this.totalToday, + }); + final PetLevel lv; + final double pct; + final int doneToday; + final int totalToday; @override Widget build(BuildContext context) { + final allDone = totalToday > 0 && doneToday == totalToday; return Column( crossAxisAlignment: CrossAxisAlignment.start, mainAxisSize: MainAxisSize.min, children: [ Row( - crossAxisAlignment: CrossAxisAlignment.baseline, - textBaseline: TextBaseline.alphabetic, + crossAxisAlignment: CrossAxisAlignment.center, children: [ - Text( - 'Lv ${lv.level}', - style: const TextStyle( - fontSize: 24, - fontWeight: FontWeight.w900, - color: Colors.white, - height: 1.0, + Expanded( + child: Row( + crossAxisAlignment: CrossAxisAlignment.baseline, + textBaseline: TextBaseline.alphabetic, + children: [ + Text( + 'Lv ${lv.level}', + style: Theme.of(context).textTheme.headlineMedium!.copyWith( + fontWeight: FontWeight.w900, + color: Colors.white, + height: 1.0, + fontSize: 26, + ), + ), + const SizedBox(width: 7), + Flexible( + child: Text( + '· ${lv.title}', + style: Theme.of(context).textTheme.labelLarge!.copyWith( + fontWeight: FontWeight.w700, + color: Colors.white.withAlpha(220), + fontSize: 13, + ), + overflow: TextOverflow.ellipsis, + ), + ), + ], ), ), const SizedBox(width: 8), - Text( - lv.title, - style: TextStyle( - fontSize: 13, - fontWeight: FontWeight.w800, - color: Colors.white.withAlpha(200), + // Show chip only when there are tasks and progress has started + if (totalToday > 0) + AnimatedSwitcher( + duration: const Duration(milliseconds: 300), + child: Container( + key: ValueKey('$doneToday/$totalToday'), + padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 4), + decoration: BoxDecoration( + color: allDone + ? AppColors.mint.withAlpha(220) + : Colors.white.withAlpha(230), + borderRadius: BorderRadius.circular(999), + ), + child: Text( + allDone ? 'All done! 🎉' : '$doneToday/$totalToday tasks', + style: TextStyle( + fontSize: 10.5, + fontWeight: FontWeight.w900, + color: allDone ? Colors.white : AppColors.poppy700, + height: 1, + ), + ), + ), ), - ), ], ), - const SizedBox(height: 4), - Text( - '${lv.currentXp} / ${lv.levelEndXp} XP', - style: TextStyle( - fontSize: 12, - fontWeight: FontWeight.w700, - color: Colors.white.withAlpha(200), - ), - ), - const SizedBox(height: 8), + const SizedBox(height: 10), + // XP progress bar with top-shine shimmer Container( - height: 16, - width: double.infinity, + height: 13, decoration: BoxDecoration( borderRadius: BorderRadius.circular(999), - color: Colors.white.withAlpha(56), - border: Border.all(color: Colors.white.withAlpha(80), width: 1.5), - ), - child: FractionallySizedBox( - alignment: Alignment.centerLeft, - widthFactor: lv.progress, - child: Container( - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(999), - gradient: const LinearGradient( - colors: [AppColors.sunny, AppColors.tangerine, AppColors.poppy], - ), - boxShadow: const [ - BoxShadow( - color: AppColors.tangerine, - blurRadius: 8, - spreadRadius: -2, - ), - ], - ), - ), + color: Colors.black.withAlpha(45), ), - ), - const SizedBox(height: 8), - if (!lv.isMaxLevel) - Text.rich( - TextSpan( - text: '${lv.xpToNext} XP to ', - style: TextStyle( - fontSize: 11, - color: Colors.white.withAlpha(200), - fontWeight: FontWeight.w700, - ), + child: ClipRRect( + borderRadius: BorderRadius.circular(999), + child: Stack( children: [ - TextSpan( - text: 'Lv ${lv.level + 1} · ${lv.nextTitle}', - style: const TextStyle( - fontWeight: FontWeight.w900, - color: Colors.white, + AnimatedFractionallySizedBox( + duration: const Duration(milliseconds: 600), + curve: Curves.easeOutCubic, + widthFactor: pct.clamp(0.0, 1.0), + child: Container( + decoration: const BoxDecoration( + gradient: LinearGradient( + colors: [ + Color(0xFFFFE08A), + AppColors.sunny, + AppColors.tangerine, + ], + stops: [0.0, 0.55, 1.0], + ), + ), + ), + ), + // Top shine overlay + Positioned.fill( + child: Container( + decoration: const BoxDecoration( + gradient: LinearGradient( + begin: Alignment.topCenter, + end: Alignment.bottomCenter, + colors: [Color(0x99FFFFFF), Colors.transparent], + stops: [0.0, 0.55], + ), + ), ), ), ], ), - ) - else - Text( - 'Max level reached!', - style: TextStyle( - fontSize: 11, - color: Colors.white.withAlpha(200), - fontWeight: FontWeight.w900, - ), ), - ], - ); - } -} - -class _LevelSkeleton extends StatelessWidget { - @override - Widget build(BuildContext context) { - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisSize: MainAxisSize.min, - children: const [ - SkeletonLoader(width: 100, height: 22, borderRadius: 8), - SizedBox(height: 8), - SkeletonLoader(width: 80, height: 12, borderRadius: 6), - SizedBox(height: 8), - SkeletonLoader(width: double.infinity, height: 12, borderRadius: 999), - SizedBox(height: 8), - SkeletonLoader(width: 130, height: 11, borderRadius: 6), + ), + const SizedBox(height: 7), + Text( + lv.isMaxLevel + ? '${lv.currentXp} XP — Max level! 👑' + : '${lv.currentXp} / ${lv.levelEndXp} XP · ${lv.xpToNext} XP to Lv ${lv.level + 1}', + style: const TextStyle( + fontSize: 10.5, + fontWeight: FontWeight.w700, + color: Color(0xDDFFFFFF), + letterSpacing: 0.1, + ), + overflow: TextOverflow.ellipsis, + ), ], ); } @@ -363,19 +575,22 @@ class CareGamifiedWeeklyChart extends StatelessWidget { final anchor = DateUtils.dateOnly(selectedDay); final weekStart = anchor.subtract(const Duration(days: 6)); - // Count completed days in this 7-day window up to and including today. int hitsCount = 0; for (int i = 0; i < 7; i++) { final dayDate = weekStart.add(Duration(days: i)); if (dayDate.isAfter(today)) break; if (i < weekHits.length && weekHits[i]) hitsCount++; } - // Count today as a hit if all tasks done and DB hasn't recorded it yet. if (anchor == today && progressPercent >= 1.0) { final todaySlot = today.difference(weekStart).inDays; - if (todaySlot >= 0 && todaySlot < weekHits.length && !weekHits[todaySlot]) hitsCount++; + if (todaySlot >= 0 && + todaySlot < weekHits.length && + !weekHits[todaySlot]) { + hitsCount++; + } } - final totalDaysSoFar = (today.difference(weekStart).inDays + 1).clamp(1, 7); + final totalDaysSoFar = + (today.difference(weekStart).inDays + 1).clamp(1, 7); return Container( decoration: BoxDecoration( @@ -387,7 +602,6 @@ class CareGamifiedWeeklyChart extends StatelessWidget { child: Column( mainAxisSize: MainAxisSize.min, children: [ - // Week summary header Row( children: [ Text( @@ -401,13 +615,14 @@ class CareGamifiedWeeklyChart extends StatelessWidget { const Spacer(), if (hitsCount > 0) Container( - padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 3), + padding: + const EdgeInsets.symmetric(horizontal: 8, vertical: 3), decoration: BoxDecoration( color: AppColors.mintSoft, borderRadius: BorderRadius.circular(999), ), child: Text( - '$hitsCount 🔥', + '$hitsCount 🔥 day${hitsCount == 1 ? '' : 's'}', style: const TextStyle( fontSize: 11, fontWeight: FontWeight.w800, @@ -429,10 +644,10 @@ class CareGamifiedWeeklyChart extends StatelessWidget { final hit = i < weekHits.length ? weekHits[i] : false; final h = isFuture - ? 0.10 + ? 0.08 : isToday - ? progressPercent.clamp(0.15, 1.0) - : (hit ? 0.85 : 0.30); + ? progressPercent.clamp(0.06, 1.0) + : (hit ? 0.88 : 0.22); final color = isFuture ? pt.line @@ -442,13 +657,26 @@ class CareGamifiedWeeklyChart extends StatelessWidget { child: Column( mainAxisAlignment: MainAxisAlignment.end, children: [ - // 🐾 paw or empty above bar SizedBox( - height: 18, + height: 26, child: isToday - ? const Align( - alignment: Alignment.bottomCenter, - child: Text('🐾', style: TextStyle(fontSize: 13)), + ? Column( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + Text( + 'Today', + style: TextStyle( + fontSize: 7, + fontWeight: FontWeight.w700, + color: _colors[i], + height: 1.1, + ), + ), + const Text( + '🐾', + style: TextStyle(fontSize: 11, height: 1.1), + ), + ], ) : null, ), @@ -458,27 +686,28 @@ class CareGamifiedWeeklyChart extends StatelessWidget { child: FractionallySizedBox( heightFactor: h, child: Container( - margin: const EdgeInsets.symmetric(horizontal: 4), + margin: + const EdgeInsets.symmetric(horizontal: 4), decoration: BoxDecoration( - borderRadius: BorderRadius.circular(10), - border: isToday && !isFuture + borderRadius: BorderRadius.circular(12), + border: (!hit && !isFuture && !isToday) ? Border.all( - color: _colors[i].withAlpha(180), - width: 2, + color: pt.line, + width: 1, + strokeAlign: BorderSide.strokeAlignInside, ) - : (!hit && !isFuture && !isToday + : (isToday && !isFuture && progressPercent < 0.06) ? Border.all( - color: pt.line, - width: 1, - strokeAlign: BorderSide.strokeAlignInside, + color: _colors[i].withAlpha(160), + width: 1.5, ) - : null), - boxShadow: (isToday && !isFuture) + : null, + boxShadow: (isToday && !isFuture && progressPercent >= 0.06) ? [ BoxShadow( - color: color.withAlpha(120), - blurRadius: 16, - offset: const Offset(0, 6), + color: color.withAlpha(100), + blurRadius: 14, + offset: const Offset(0, 5), spreadRadius: -4, ), ] @@ -489,8 +718,8 @@ class CareGamifiedWeeklyChart extends StatelessWidget { begin: Alignment.topCenter, end: Alignment.bottomCenter, colors: [ + Color.lerp(color, Colors.white, 0.28)!, color, - Color.lerp(color, Colors.white, 0.45)!, ], ), color: isFuture @@ -502,7 +731,6 @@ class CareGamifiedWeeklyChart extends StatelessWidget { ), ), const SizedBox(height: 4), - // Day letter Text( _dayLetters[dayDate.weekday - 1], style: TextStyle( @@ -513,7 +741,6 @@ class CareGamifiedWeeklyChart extends StatelessWidget { : (isFuture ? pt.ink300 : pt.ink500), ), ), - // Day number Text( '${dayDate.day}', style: TextStyle( @@ -536,30 +763,30 @@ class CareGamifiedWeeklyChart extends StatelessWidget { } } -// ── Trophy Room (real badges) ───────────────────────────────────────────────── +// ── Trophy Room (horizontal slider) ────────────────────────────────────────── + +String _badgeProgressHint(BadgeInfo badge, PetAwardsSummary awards) { + switch (badge.type) { + case '3_day_streak': + return '${awards.currentStreak}/3 days 🦴'; + case '7_day_hero': + return '${awards.currentStreak}/7 days 🌿'; + case 'routine_master': + return '${awards.currentStreak}/14 days ❤️'; + case '30_day_legend': + return '${awards.currentStreak}/30 days 🌟'; + case 'care_champion': + return '${awards.logsCount}/100 logs 👑'; + default: + return ''; + } +} class CareGamifiedTrophyRoom extends ConsumerWidget { const CareGamifiedTrophyRoom({super.key, required this.petId}); final String petId; - String _progressHint(BadgeInfo badge, PetAwardsSummary awards) { - switch (badge.type) { - case '3_day_streak': - return '${awards.currentStreak}/3 days 🔥'; - case '7_day_hero': - return '${awards.currentStreak}/7 days 🦸'; - case 'routine_master': - return '${awards.currentStreak}/14 days 💯'; - case '30_day_legend': - return '${awards.currentStreak}/30 days 👑'; - case 'care_champion': - return '${awards.logsCount}/100 logs 🏆'; - default: - return ''; - } - } - @override Widget build(BuildContext context, WidgetRef ref) { final awardsAsync = ref.watch(petAwardsSummaryProvider(petId)); @@ -569,58 +796,33 @@ class CareGamifiedTrophyRoom extends ConsumerWidget { data: (a) => a, orElse: () => PetAwardsSummary.empty, ); - final ownedTypes = awards.unlockedTypes; - final ownedCount = ownedTypes.length; + final ownedCount = awards.unlockedTypes.length; return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - // N/6 earned sub-label Padding( padding: const EdgeInsets.only(bottom: 12), - child: Row( - children: [ - Container( - padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 4), - decoration: BoxDecoration( - color: ownedCount > 0 ? AppColors.mintSoft : pt.surface2, - borderRadius: BorderRadius.circular(999), - ), - child: Text( - '$ownedCount / ${kBadgeCatalog.length} earned', - style: TextStyle( - fontSize: 11, - fontWeight: FontWeight.w800, - color: ownedCount > 0 ? AppColors.mint700 : pt.ink300, - ), - ), + child: Container( + padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 4), + decoration: BoxDecoration( + color: ownedCount > 0 ? AppColors.mintSoft : pt.surface2, + borderRadius: BorderRadius.circular(999), + ), + child: Text( + '$ownedCount / ${kBadgeCatalog.length} earned', + style: TextStyle( + fontSize: 11, + fontWeight: FontWeight.w800, + color: ownedCount > 0 ? AppColors.mint700 : pt.ink300, ), - ], + ), ), ), - GridView.builder( - shrinkWrap: true, - physics: const NeverScrollableScrollPhysics(), - padding: EdgeInsets.zero, - gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( - crossAxisCount: 3, - mainAxisSpacing: 12, - crossAxisSpacing: 12, - childAspectRatio: 0.82, - ), - itemCount: kBadgeCatalog.length, - itemBuilder: (context, i) { - final badge = kBadgeCatalog[i]; - final owned = ownedTypes.contains(badge.type); - final hint = owned ? '' : _progressHint(badge, awards); - return _BadgeTile( - badge: badge, - owned: owned, - progressHint: hint, - index: i, - onTap: () => _showBadgeDetail(context, badge, owned, hint), - ); - }, + _TrophySlider( + awards: awards, + onTapBadge: (badge, owned, hint) => + _showBadgeDetail(context, pt, badge, owned, hint), ), ], ); @@ -628,11 +830,11 @@ class CareGamifiedTrophyRoom extends ConsumerWidget { void _showBadgeDetail( BuildContext context, + PetfolioThemeExtension pt, BadgeInfo badge, bool owned, String hint, ) { - final pt = Theme.of(context).extension()!; showModalBottomSheet( context: context, useRootNavigator: true, @@ -640,10 +842,10 @@ class CareGamifiedTrophyRoom extends ConsumerWidget { builder: (_) => Container( decoration: BoxDecoration( color: pt.surface1, - borderRadius: const BorderRadius.vertical(top: Radius.circular(24)), + borderRadius: const BorderRadius.vertical(top: Radius.circular(28)), ), padding: EdgeInsets.fromLTRB( - 24, 0, 24, MediaQuery.paddingOf(context).bottom + 28), + 24, 0, 24, MediaQuery.paddingOf(context).bottom + 34), child: Column( mainAxisSize: MainAxisSize.min, children: [ @@ -651,7 +853,7 @@ class CareGamifiedTrophyRoom extends ConsumerWidget { child: Padding( padding: const EdgeInsets.symmetric(vertical: 12), child: Container( - width: 36, + width: 38, height: 4, decoration: BoxDecoration( color: pt.line, @@ -661,92 +863,104 @@ class CareGamifiedTrophyRoom extends ConsumerWidget { ), ), Container( - width: 80, - height: 80, + width: 96, + height: 96, decoration: BoxDecoration( - borderRadius: BorderRadius.circular(24), + shape: BoxShape.circle, gradient: owned - ? LinearGradient( - begin: Alignment.topLeft, - end: Alignment.bottomRight, + ? RadialGradient( + center: const Alignment(-0.24, -0.4), + radius: 0.85, colors: [ + Color.lerp(badge.color, Colors.white, 0.55)!, badge.color, - Color.lerp(badge.color, Colors.white, 0.3)!, + Color.lerp(badge.color, Colors.black, 0.4)!, ], + stops: const [0.0, 0.58, 1.0], ) : null, color: owned ? null : pt.surface2, + border: owned ? null : Border.all(color: pt.line), boxShadow: owned ? [ BoxShadow( color: badge.color.withAlpha(120), - blurRadius: 24, - offset: const Offset(0, 8), + blurRadius: 28, + offset: const Offset(0, 10), spreadRadius: -8, ) ] : null, ), alignment: Alignment.center, - child: Text( - badge.emoji, - style: TextStyle( - fontSize: 40, - color: owned ? null : Colors.grey, - ), - ), + child: owned + ? Text(badge.emoji, style: const TextStyle(fontSize: 44)) + : Opacity( + opacity: 0.4, + child: ColorFiltered( + colorFilter: const ColorFilter.mode( + Colors.grey, + BlendMode.saturation, + ), + child: Text(badge.emoji, + style: const TextStyle(fontSize: 44)), + ), + ), ), const SizedBox(height: 16), Text( badge.label, style: TextStyle( - fontSize: 20, + fontSize: 22, fontWeight: FontWeight.w800, - color: Theme.of(context).colorScheme.onSurface, + color: owned + ? badge.color + : Theme.of(context).colorScheme.onSurface, ), ), const SizedBox(height: 6), Text( badge.description, style: TextStyle(fontSize: 14, color: pt.ink500), + textAlign: TextAlign.center, ), - if (!owned && hint.isNotEmpty) ...[ - const SizedBox(height: 12), - Container( - padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), - decoration: BoxDecoration( - color: pt.surface2, - borderRadius: BorderRadius.circular(12), - ), - child: Text( - 'Progress: $hint', - style: TextStyle( - fontSize: 13, - fontWeight: FontWeight.w700, - color: pt.ink500, - ), + const SizedBox(height: 14), + Container( + padding: + const EdgeInsets.symmetric(horizontal: 16, vertical: 10), + decoration: BoxDecoration( + color: owned ? badge.color.withAlpha(20) : pt.surface2, + borderRadius: BorderRadius.circular(12), + border: Border.all( + color: owned ? badge.color.withAlpha(60) : pt.line, ), ), - ], - if (owned) ...[ - const SizedBox(height: 12), - Container( - padding: - const EdgeInsets.symmetric(horizontal: 16, vertical: 8), - decoration: BoxDecoration( - color: AppColors.mintSoft, - borderRadius: BorderRadius.circular(12), - ), - child: const Text( - '✅ Earned!', - style: TextStyle( - fontSize: 13, - fontWeight: FontWeight.w800, - color: AppColors.mint700, + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Icon( + owned + ? Icons.check_circle_rounded + : Icons.lock_rounded, + size: 16, + color: owned ? badge.color : pt.ink300, ), - ), + const SizedBox(width: 8), + Text( + owned + ? 'Earned!' + : (hint.isNotEmpty + ? 'Progress: $hint' + : 'Keep logging care'), + style: TextStyle( + fontSize: 14, + fontWeight: FontWeight.w800, + color: owned ? badge.color : pt.ink500, + ), + ), + ], ), - ], + ), ], ), ), @@ -754,9 +968,113 @@ class CareGamifiedTrophyRoom extends ConsumerWidget { } } -// Individual badge tile with lock overlay and progress hint -class _BadgeTile extends StatelessWidget { - const _BadgeTile({ +// ── Trophy Slider ───────────────────────────────────────────────────────────── + +class _TrophySlider extends StatefulWidget { + const _TrophySlider({ + required this.awards, + required this.onTapBadge, + }); + + final PetAwardsSummary awards; + final void Function(BadgeInfo badge, bool owned, String hint) onTapBadge; + + @override + State<_TrophySlider> createState() => _TrophySliderState(); +} + +class _TrophySliderState extends State<_TrophySlider> { + late final PageController _ctrl; + int _page = 0; + + @override + void initState() { + super.initState(); + _ctrl = PageController(viewportFraction: 0.46); + _ctrl.addListener(_onScroll); + } + + void _onScroll() { + final p = (_ctrl.page ?? 0).round(); + if (p != _page) setState(() => _page = p); + } + + @override + void dispose() { + _ctrl.removeListener(_onScroll); + _ctrl.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + final pt = Theme.of(context).extension()!; + final ownedTypes = widget.awards.unlockedTypes; + + return Column( + children: [ + SizedBox( + height: 172, + child: PageView.builder( + controller: _ctrl, + padEnds: false, + itemCount: kBadgeCatalog.length, + itemBuilder: (context, i) { + final badge = kBadgeCatalog[i]; + final owned = ownedTypes.contains(badge.type); + final hint = + owned ? '' : _badgeProgressHint(badge, widget.awards); + return Padding( + padding: EdgeInsets.fromLTRB( + i == 0 ? 0 : 7, + 4, + i == kBadgeCatalog.length - 1 ? 8 : 7, + 10, + ), + child: _TrophyCard( + badge: badge, + owned: owned, + progressHint: hint, + index: i, + onTap: () => widget.onTapBadge(badge, owned, hint), + ), + ); + }, + ), + ), + const SizedBox(height: 8), + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: List.generate(kBadgeCatalog.length, (i) { + final badge = kBadgeCatalog[i]; + final owned = ownedTypes.contains(badge.type); + final active = i == _page; + return AnimatedContainer( + duration: const Duration(milliseconds: 300), + curve: Curves.easeOutCubic, + width: active ? 20 : 6, + height: 6, + margin: const EdgeInsets.symmetric(horizontal: 2.5), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(999), + color: active + ? badge.color + : (owned + ? badge.color.withAlpha(90) + : pt.line), + ), + ); + }), + ), + ], + ); + } +} + +// ── Trophy Card ─────────────────────────────────────────────────────────────── + +class _TrophyCard extends StatefulWidget { + const _TrophyCard({ required this.badge, required this.owned, required this.progressHint, @@ -770,125 +1088,509 @@ class _BadgeTile extends StatelessWidget { final int index; final VoidCallback onTap; + @override + State<_TrophyCard> createState() => _TrophyCardState(); +} + +class _TrophyCardState extends State<_TrophyCard> with TickerProviderStateMixin { + late final AnimationController _floatCtrl; + late final AnimationController _sheenCtrl; + + @override + void initState() { + super.initState(); + _floatCtrl = AnimationController( + vsync: this, + duration: Duration(milliseconds: 3200 + widget.index * 200), + )..repeat(reverse: true); + + final sheenDuration = Duration(milliseconds: 3800 + widget.index * 200); + _sheenCtrl = AnimationController(vsync: this, duration: sheenDuration); + + if (widget.owned) { + final delayFraction = + (widget.index * 300 / sheenDuration.inMilliseconds).clamp(0.0, 1.0); + _sheenCtrl.forward(from: delayFraction); + _sheenCtrl.addStatusListener((s) { + if (s == AnimationStatus.completed && mounted) _sheenCtrl.repeat(); + }); + } + } + + @override + void didUpdateWidget(_TrophyCard oldWidget) { + super.didUpdateWidget(oldWidget); + if (!oldWidget.owned && widget.owned && !_sheenCtrl.isAnimating) { + final delayFraction = + (widget.index * 300 / _sheenCtrl.duration!.inMilliseconds).clamp(0.0, 1.0); + _sheenCtrl.forward(from: delayFraction); + _sheenCtrl.addStatusListener((s) { + if (s == AnimationStatus.completed && mounted) _sheenCtrl.repeat(); + }); + } + } + + @override + void dispose() { + _floatCtrl.dispose(); + _sheenCtrl.dispose(); + super.dispose(); + } + @override Widget build(BuildContext context) { final pt = Theme.of(context).extension()!; final isDark = Theme.of(context).brightness == Brightness.dark; - final labelColor = isDark ? AppColors.ink950D : AppColors.ink950; - final angle = (index % 2 != 0 ? -2.5 : 2.5) * 3.14159 / 180; + final owned = widget.owned; + final badge = widget.badge; + + final bgTop = owned + ? Color.lerp(badge.color, Colors.white, isDark ? 0.50 : 0.82)! + : (isDark ? const Color(0xFF252020) : const Color(0xFFF9F6F2)); + final bgBottom = owned + ? Color.lerp(badge.color, Colors.white, isDark ? 0.24 : 0.52)! + : (isDark ? const Color(0xFF1C1818) : pt.line.withAlpha(130)); return GestureDetector( - onTap: onTap, - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - Expanded( - child: Transform.rotate( - angle: angle, - child: Stack( - children: [ - // Badge box - Container( - width: double.infinity, - height: double.infinity, - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(22), - color: owned ? null : pt.surface2, - gradient: owned - ? LinearGradient( - begin: Alignment.topLeft, - end: Alignment.bottomRight, - colors: [ - badge.color, - Color.lerp(badge.color, Colors.white, 0.3)!, - ], - ) - : null, - border: owned - ? null - : Border.all(color: pt.line, width: 1), - boxShadow: owned - ? [ - BoxShadow( - color: badge.color.withAlpha(130), - blurRadius: 16, - offset: const Offset(0, 6), - spreadRadius: -8, - ), - ] - : null, - ), - alignment: Alignment.center, - child: owned - ? Text( - badge.emoji, - style: const TextStyle(fontSize: 32), - ) - : Opacity( - opacity: 0.35, - child: ColorFiltered( - colorFilter: const ColorFilter.mode( - Colors.grey, - BlendMode.saturation, - ), - child: Text( - badge.emoji, - style: const TextStyle(fontSize: 32), - ), - ), - ), + onTap: widget.onTap, + child: Container( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(24), + gradient: LinearGradient( + begin: Alignment.topLeft, + end: Alignment.bottomRight, + colors: [bgTop, bgBottom], + ), + border: Border.all( + color: + owned ? badge.color.withAlpha(75) : pt.line, + width: owned ? 1.5 : 1.0, + ), + boxShadow: owned + ? [ + BoxShadow( + color: badge.color.withAlpha(isDark ? 55 : 40), + blurRadius: 20, + offset: const Offset(0, 8), + spreadRadius: -5, ), - // Lock icon overlay for locked badges - if (!owned) - Positioned( - right: 6, - bottom: 6, + ] + : null, + ), + child: ClipRRect( + borderRadius: BorderRadius.circular(23), + child: Stack( + children: [ + // Holographic sheen sweep for owned badges + if (owned) + AnimatedBuilder( + animation: _sheenCtrl, + builder: (_, _) { + final x = _sheenCtrl.value * 3.0 - 0.7; + return Positioned( + top: 0, + bottom: 0, + left: x * 170, + width: 52, child: Container( - padding: const EdgeInsets.all(3), - decoration: BoxDecoration( - color: pt.surface1, - shape: BoxShape.circle, - border: Border.all(color: pt.line), + decoration: const BoxDecoration( + gradient: LinearGradient( + begin: Alignment.topLeft, + end: Alignment.bottomRight, + colors: [ + Colors.transparent, + Color(0x50FFFFFF), + Colors.transparent, + ], + ), ), - child: Icon( - Icons.lock_rounded, - size: 10, - color: pt.ink300, + ), + ); + }, + ), + + Padding( + padding: const EdgeInsets.fromLTRB(10, 14, 10, 10), + child: Column( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + // Floating medal disc + Expanded( + child: Center( + child: AnimatedBuilder( + animation: _floatCtrl, + builder: (_, child) => Transform.translate( + offset: Offset(0, _floatCtrl.value * -6), + child: child, + ), + child: _MedalDisc(badge: badge, owned: owned), ), ), ), - ], + const SizedBox(height: 6), + // Badge name + Text( + badge.label, + textAlign: TextAlign.center, + maxLines: 2, + overflow: TextOverflow.ellipsis, + style: TextStyle( + fontSize: 11, + fontWeight: FontWeight.w800, + height: 1.2, + color: owned + ? (isDark + ? Color.lerp(badge.color, Colors.white, 0.30)! + : badge.color) + : pt.ink500, + ), + ), + const SizedBox(height: 5), + // Progress / earned status pill + Container( + width: double.infinity, + padding: const EdgeInsets.symmetric( + horizontal: 10, vertical: 5), + decoration: BoxDecoration( + color: owned + ? badge.color.withAlpha(isDark ? 50 : 28) + : pt.surface2.withAlpha(200), + borderRadius: BorderRadius.circular(999), + border: Border.all( + color: owned + ? badge.color.withAlpha(65) + : pt.line, + width: 1, + ), + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + mainAxisSize: MainAxisSize.min, + children: [ + Icon( + owned + ? Icons.check_circle_rounded + : Icons.lock_rounded, + size: 11, + color: owned ? badge.color : pt.ink300, + ), + const SizedBox(width: 4), + Flexible( + child: Text( + owned + ? 'Earned!' + : (widget.progressHint.isNotEmpty + ? widget.progressHint + : 'Locked'), + overflow: TextOverflow.ellipsis, + maxLines: 1, + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 10, + fontWeight: FontWeight.w700, + color: owned ? badge.color : pt.ink500, + height: 1, + ), + ), + ), + ], + ), + ), + ], + ), ), - ), + + // Lock pip top-right for locked badges + if (!owned) + Positioned( + top: 10, + right: 10, + child: Container( + width: 22, + height: 22, + decoration: BoxDecoration( + shape: BoxShape.circle, + color: pt.surface1, + border: Border.all(color: pt.line), + ), + child: Icon( + Icons.lock_rounded, + size: 11, + color: pt.ink300, + ), + ), + ), + ], ), - const SizedBox(height: 5), - Text( - badge.label, - textAlign: TextAlign.center, - maxLines: 1, - overflow: TextOverflow.ellipsis, - style: TextStyle( - fontSize: 10, - fontWeight: FontWeight.w800, - color: labelColor, - height: 1.2, + ), + ), + ); + } +} + +// ── Medal Disc ──────────────────────────────────────────────────────────────── + +class _MedalDisc extends StatelessWidget { + const _MedalDisc({required this.badge, required this.owned}); + + final BadgeInfo badge; + final bool owned; + + @override + Widget build(BuildContext context) { + final pt = Theme.of(context).extension()!; + final iconWidget = CustomPaint( + painter: _BadgePainter(badge.type, badge.color, owned), + ); + return Container( + width: 62, + height: 62, + decoration: BoxDecoration( + shape: BoxShape.circle, + gradient: owned + ? RadialGradient( + center: const Alignment(-0.24, -0.40), + radius: 0.88, + colors: [ + Color.lerp(badge.color, Colors.white, 0.56)!, + badge.color, + Color.lerp(badge.color, Colors.black, 0.38)!, + ], + stops: const [0.0, 0.55, 1.0], + ) + : RadialGradient( + center: const Alignment(-0.24, -0.40), + radius: 0.88, + colors: [ + pt.ink300.withAlpha(100), + pt.ink300.withAlpha(160), + ], + ), + boxShadow: owned + ? [ + BoxShadow( + color: badge.color.withAlpha(100), + blurRadius: 16, + offset: const Offset(0, 6), + spreadRadius: -4, + ), + ] + : [ + BoxShadow( + color: Colors.black.withAlpha(22), + blurRadius: 4, + offset: const Offset(0, 2), + ), + ], + ), + child: ClipOval( + child: Stack( + alignment: Alignment.center, + children: [ + // Concentric ring + Positioned.fill( + child: Container( + margin: const EdgeInsets.all(5), + decoration: BoxDecoration( + shape: BoxShape.circle, + border: Border.all( + color: owned + ? Colors.white.withAlpha(60) + : Colors.black.withAlpha(10), + width: 1, + ), + ), + ), ), - ), - if (!owned && progressHint.isNotEmpty) - Text( - progressHint, - textAlign: TextAlign.center, - maxLines: 1, - overflow: TextOverflow.ellipsis, - style: TextStyle( - fontSize: 9, - fontWeight: FontWeight.w600, - color: pt.ink300, - height: 1.2, + // Specular highlight + if (owned) + Positioned( + left: 4, top: 4, right: 20, bottom: 10, + child: Container( + decoration: BoxDecoration( + shape: BoxShape.circle, + gradient: LinearGradient( + begin: Alignment.topLeft, + end: Alignment.bottomRight, + colors: [Colors.white.withAlpha(90), Colors.transparent], + ), + ), + ), ), + // Custom icon + Positioned.fill( + child: owned + ? iconWidget + : Opacity( + opacity: 0.45, + child: ColorFiltered( + colorFilter: const ColorFilter.mode( + Colors.grey, BlendMode.saturation), + child: iconWidget, + ), + ), ), - ], + ], + ), ), ); } } + +// ── Badge Icon Painter ──────────────────────────────────────────────────────── + +class _BadgePainter extends CustomPainter { + _BadgePainter(this.type, this.color, this.owned); + + final String type; + final Color color; + final bool owned; + + Paint _fill(Color c) => Paint()..color = c..style = PaintingStyle.fill; + Paint _stroke(Color c, double w) => Paint() + ..color = c + ..style = PaintingStyle.stroke + ..strokeWidth = w + ..strokeCap = StrokeCap.round + ..strokeJoin = StrokeJoin.round; + + @override + void paint(Canvas canvas, Size size) { + final cx = size.width / 2; + final cy = size.height / 2; + final u = size.width / 42; + final white = Colors.white.withAlpha(owned ? 230 : 180); + + switch (type) { + case 'first_log': _drawPaw(canvas, cx, cy, u, white); break; + case '3_day_streak': _drawBone(canvas, cx, cy, u, white); break; + case '7_day_hero': _drawSprout(canvas, cx, cy, u, white); break; + case 'routine_master': _drawHeartPaw(canvas, cx, cy, u, white); break; + case '30_day_legend': _drawStar(canvas, cx, cy, u, white); break; + case 'care_champion': _drawCrown(canvas, cx, cy, u, white); break; + } + } + + // 🐾 Paw print: palm oval + 4 toe circles + void _drawPaw(Canvas canvas, double cx, double cy, double u, Color c) { + final p = _fill(c); + canvas.drawOval( + Rect.fromCenter(center: Offset(cx, cy + 4 * u), width: 15 * u, height: 11 * u), + p, + ); + canvas.drawCircle(Offset(cx - 7 * u, cy - 1 * u), 3.5 * u, p); + canvas.drawCircle(Offset(cx - 2 * u, cy - 5.5 * u), 3.5 * u, p); + canvas.drawCircle(Offset(cx + 3 * u, cy - 5.5 * u), 3.5 * u, p); + canvas.drawCircle(Offset(cx + 8 * u, cy - 1 * u), 3.5 * u, p); + } + + // 🦴 Dog bone: bar + rounded knobs each end + void _drawBone(Canvas canvas, double cx, double cy, double u, Color c) { + final p = _fill(c); + canvas.drawRRect( + RRect.fromRectAndRadius( + Rect.fromCenter(center: Offset(cx, cy), width: 24 * u, height: 7 * u), + Radius.circular(3.5 * u), + ), + p, + ); + for (final dx in [-13.0 * u, 13.0 * u]) { + canvas.drawCircle(Offset(cx + dx, cy - 4.5 * u), 4.5 * u, p); + canvas.drawCircle(Offset(cx + dx, cy + 4.5 * u), 4.5 * u, p); + } + } + + // 🌱 Sprout: curved stem + two leaves + sun bud + void _drawSprout(Canvas canvas, double cx, double cy, double u, Color c) { + // Stem + final stem = Path() + ..moveTo(cx, cy + 10 * u) + ..cubicTo(cx, cy + 4 * u, cx - 1.5 * u, cy, cx, cy - 1.5 * u); + canvas.drawPath(stem, _stroke(c, 2.8 * u)); + // Left leaf + final left = Path() + ..moveTo(cx, cy + 1 * u) + ..cubicTo(cx - 10 * u, cy - 1 * u, cx - 10 * u, cy - 11 * u, cx, cy - 7 * u) + ..close(); + canvas.drawPath(left, _fill(c)); + // Right leaf + final right = Path() + ..moveTo(cx, cy - 1 * u) + ..cubicTo(cx + 9 * u, cy - 3 * u, cx + 9 * u, cy - 13 * u, cx, cy - 9 * u) + ..close(); + canvas.drawPath(right, _fill(c)); + // Bud + canvas.drawCircle(Offset(cx, cy - 13.5 * u), 3.5 * u, _fill(c)); + } + + // ❤️ Heart + mini paw inside + void _drawHeartPaw(Canvas canvas, double cx, double cy, double u, Color c) { + final heart = Path() + ..moveTo(cx, cy + 9 * u) + ..cubicTo(cx - 14 * u, cy + 1 * u, cx - 17 * u, cy - 11 * u, cx, cy - 7 * u) + ..cubicTo(cx + 17 * u, cy - 11 * u, cx + 14 * u, cy + 1 * u, cx, cy + 9 * u); + canvas.drawPath(heart, _fill(c)); + // Mini paw inside (white) + final pw = Colors.white.withAlpha(owned ? 200 : 140); + canvas.drawOval( + Rect.fromCenter(center: Offset(cx, cy + 1.5 * u), width: 6.5 * u, height: 4.5 * u), + _fill(pw), + ); + for (final d in [Offset(-3.5 * u, -1.5 * u), Offset(-1 * u, -4 * u), Offset(1.5 * u, -4 * u), Offset(4 * u, -1.5 * u)]) { + canvas.drawCircle(Offset(cx + d.dx, cy + d.dy), 1.5 * u, _fill(pw)); + } + } + + // ⭐ 5-pt star + sparkle dots + void _drawStar(Canvas canvas, double cx, double cy, double u, Color c) { + const n = 5; + const outerR = 14.0; + const innerR = 5.5; + final star = Path(); + for (int i = 0; i < n * 2; i++) { + final angle = (i * math.pi / n) - math.pi / 2; + final r = (i.isEven ? outerR : innerR) * u; + final pt = Offset(cx + r * math.cos(angle), cy + r * math.sin(angle)); + i == 0 ? star.moveTo(pt.dx, pt.dy) : star.lineTo(pt.dx, pt.dy); + } + star.close(); + canvas.drawPath(star, _fill(c)); + // Sparkle dots + for (final pos in [ + Offset(-16 * u, -7 * u), Offset(16 * u, 5 * u), Offset(11 * u, -15 * u) + ]) { + canvas.drawCircle(Offset(cx + pos.dx, cy + pos.dy), 1.8 * u, _fill(c)); + } + } + + // 👑 Crown: 3-pointed body + gem circles + base band + void _drawCrown(Canvas canvas, double cx, double cy, double u, Color c) { + final crown = Path() + ..moveTo(cx - 14 * u, cy + 7 * u) + ..lineTo(cx + 14 * u, cy + 7 * u) + ..lineTo(cx + 11 * u, cy - 3 * u) + ..lineTo(cx + 4.5 * u, cy + 2.5 * u) + ..lineTo(cx, cy - 12 * u) + ..lineTo(cx - 4.5 * u, cy + 2.5 * u) + ..lineTo(cx - 11 * u, cy - 3 * u) + ..close(); + canvas.drawPath(crown, _fill(c)); + // Base band + canvas.drawRRect( + RRect.fromRectAndRadius( + Rect.fromCenter(center: Offset(cx, cy + 9 * u), width: 28 * u, height: 5 * u), + Radius.circular(2.5 * u), + ), + _fill(c), + ); + // Gems (white circles) + final gem = Colors.white.withAlpha(owned ? 210 : 150); + canvas.drawCircle(Offset(cx, cy - 12 * u), 2.8 * u, _fill(gem)); + canvas.drawCircle(Offset(cx - 11 * u, cy - 3 * u), 2.2 * u, _fill(gem)); + canvas.drawCircle(Offset(cx + 11 * u, cy - 3 * u), 2.2 * u, _fill(gem)); + } + + @override + bool shouldRepaint(covariant _BadgePainter old) => + old.type != type || old.color != color || old.owned != owned; +} diff --git a/lib/features/matching/presentation/screens/matching_screen.dart b/lib/features/matching/presentation/screens/matching_screen.dart index 6372b88..fd054a8 100644 --- a/lib/features/matching/presentation/screens/matching_screen.dart +++ b/lib/features/matching/presentation/screens/matching_screen.dart @@ -1,3 +1,4 @@ +import 'dart:async'; import 'dart:math' as math; @@ -109,6 +110,7 @@ class _DiscoveryViewState extends ConsumerState<_DiscoveryView> with WidgetsBindingObserver { final Set _shownMatchIds = {}; PetMutualMatch? _celebrationMatch; + Timer? _refreshDebounce; // Track the last known access state so we only reload candidates when it // transitions from blocked → granted (H-4 fix). @@ -125,6 +127,7 @@ class _DiscoveryViewState extends ConsumerState<_DiscoveryView> @override void dispose() { + _refreshDebounce?.cancel(); WidgetsBinding.instance.removeObserver(this); super.dispose(); } @@ -144,11 +147,17 @@ class _DiscoveryViewState extends ConsumerState<_DiscoveryView> _lastKnownAccess = current; } + // Debounced so rapid lifecycle transitions (e.g. debug-VM attach, permission + // dialogs) collapse into a single invalidation instead of a request storm. void _refreshLocationState() { - ref.invalidate(locationAccessProvider); - ref.invalidate(deviceLatLngProvider); - ref.invalidate(discoveryCandidatesControllerProvider); - _lastKnownAccess = ref.read(locationAccessProvider).asData?.value; + _refreshDebounce?.cancel(); + _refreshDebounce = Timer(const Duration(milliseconds: 600), () { + if (!mounted) return; + ref.invalidate(locationAccessProvider); + ref.invalidate(deviceLatLngProvider); + ref.invalidate(discoveryCandidatesControllerProvider); + _lastKnownAccess = ref.read(locationAccessProvider).asData?.value; + }); } @override diff --git a/lib/features/pet_profile/presentation/screens/manage_pets_screen.dart b/lib/features/pet_profile/presentation/screens/manage_pets_screen.dart index 8725973..8a8418a 100644 --- a/lib/features/pet_profile/presentation/screens/manage_pets_screen.dart +++ b/lib/features/pet_profile/presentation/screens/manage_pets_screen.dart @@ -37,7 +37,6 @@ class _ManagePetsScreenState extends ConsumerState { Future _onReorder(List pets, int oldIndex, int newIndex) async { if (_reordering) return; - if (newIndex > oldIndex) newIndex -= 1; if (oldIndex == newIndex) return; final next = [...pets]; @@ -307,7 +306,7 @@ class _PetList extends StatelessWidget { padding: const EdgeInsets.symmetric(horizontal: 20), sliver: SliverReorderableList( itemCount: pets.length, - onReorder: onReorder, + onReorderItem: onReorder, proxyDecorator: (child, _, animation) => Material( color: Colors.transparent, child: AnimatedBuilder( diff --git a/lib/main.dart b/lib/main.dart index f0f4c68..60499d4 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,5 +1,6 @@ import 'dart:ui'; +import 'package:flutter/foundation.dart' show kIsWeb; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_stripe/flutter_stripe.dart'; @@ -64,7 +65,7 @@ Future main() async { await Supabase.initialize(url: _supabaseUrl, anonKey: _supabaseAnonKey); - await NotificationService.instance.initialize(); + if (!kIsWeb) await NotificationService.instance.initialize(); runApp(const ProviderScope(child: PetfolioApp())); } diff --git a/progress.md b/progress.md index 9006b1e..80ee014 100644 --- a/progress.md +++ b/progress.md @@ -51,6 +51,30 @@ - **Error UI Handling**: For optimistic UI actions (like toggling care tasks or seller onboarding), show errors using `AppSnackBar.showError` (via `appSnackBarMessengerKey` on `MaterialApp.router`). Do not put long-lived state providers in `AsyncValue.error` states for transient/action-level failures. - **Web Safe Target**: Marionette execution runs exclusively in debug builds via conditional compiler imports (`marionette_debug_gate_stub.dart` vs `_io.dart`) to keep `main.dart` from importing `dart:io` on web targets. +## 2026-06-01 — Care Screen Redesign (from PetFolio Redesign/Care Redesign.html) + +**Files changed**: `gamified_care_ui.dart`, `care_screen.dart` + +- **`CareGamifiedHeader` rewritten** — Removed tall `WaveHeader`; replaced with compact inline design: + - Status-bar-aware top padding + pet switcher row (`PetAvatar` + "CARE" label + pet name + chevron) + - Slim gradient hero card (`poppy→#FF6B45→tangerine`, radius 26, shadow-pop) with paw watermark + - `_StreakCoin`: 78px golden radial-gradient circle with 3D `Matrix4.rotateY` coin-spin + pulse-ring + - `_HeroLevelContent`: inline Lv + title + done-today pill + XP progress bar + XP-to-next text +- **`_BadgeMedal` (replaces `_BadgeTile`)** — Circular medal design: + - 72px disc with radial gradient (owned: badge color, locked: grey), concentric depth rings, emoji + - Float bob animation (translateY 0→-5px, repeating reverse) + - Holographic sheen sweep (owned only); pulsing outer glow ring with scale+opacity (owned only) + - Lock pip (18px circle, bottom-right) for locked badges; `childAspectRatio: 0.72` grid +- **`_UtilityBanner` (replaces `_NutritionBanner` + `_MedicalVaultBanner`)** — Side-by-side split card: + - Single rounded `Container` with `VerticalDivider` between two `_UtilityHalf` widgets + - Left: Nutrition (sunny palette), Right: Medical Vault (mint palette); both tappable +- All existing Supabase providers and controllers unchanged; same data flow +- `flutter analyze` — **No issues found.** + +**Next step:** Phase complete — please run (/remember) to save tokens before proceeding to the next phase. + +--- + ## 2026-05-31 — Matching Feature P0/P1/P2 Systematic Fixes ### Database (applied to `jqyjvhwlcqcsuwcqgcwf` ✅) diff --git a/pubspec.yaml b/pubspec.yaml index 260e560..f837e63 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -68,12 +68,12 @@ dependencies: share_plus: ^13.1.0 url_launcher: ^6.3.1 - geolocator: ^13.0.4 - permission_handler: ^11.4.0 + geolocator: ^14.0.0 + permission_handler: ^12.0.0 # Local notifications - flutter_local_notifications: ^18.0.0 - timezone: ^0.9.4 + flutter_local_notifications: ^21.0.0 + timezone: ^0.11.0 http: ^1.6.0 uuid: ^4.5.1 diff --git a/supabase/migrations/20260604000000_fix_matching_discovery_candidates_offset_signature.sql b/supabase/migrations/20260604000000_fix_matching_discovery_candidates_offset_signature.sql new file mode 100644 index 0000000..911d7d3 --- /dev/null +++ b/supabase/migrations/20260604000000_fix_matching_discovery_candidates_offset_signature.sql @@ -0,0 +1,111 @@ +-- The cursor-based overload (p_cursor_created_at, p_cursor_pet_id) was applied +-- directly to the DB outside version control, causing a signature mismatch with +-- the Dart client (MatchingSupabaseDataSource) which passes p_offset. +-- Drop the stale overload and restore the offset-based version with correct grants. + +DROP FUNCTION IF EXISTS public.matching_discovery_candidates( + uuid, + double precision, + integer, + timestamp with time zone, + uuid, + text[], + integer, + integer +); + +CREATE OR REPLACE FUNCTION public.matching_discovery_candidates( + p_actor_pet_id uuid, + p_radius_meters double precision DEFAULT 80467, + p_limit integer DEFAULT 20, + p_offset integer DEFAULT 0, + p_species text[] DEFAULT NULL, + p_min_age_years integer DEFAULT NULL, + p_max_age_years integer DEFAULT NULL +) +RETURNS TABLE ( + id uuid, + owner_id uuid, + name text, + species text, + breed text, + date_of_birth date, + avatar_url text, + bio text, + distance_meters double precision, + is_discoverable boolean, + owner jsonb +) +LANGUAGE sql +STABLE +SECURITY DEFINER +SET search_path = public, extensions +AS $$ + WITH origin AS ( + SELECT p.location AS loc, p.owner_id + FROM public.pets p + WHERE p.id = p_actor_pet_id + AND (SELECT auth.uid()) = p.owner_id + AND p.is_discoverable IS TRUE + ) + SELECT + c.id, + c.owner_id, + c.name, + c.species, + c.breed, + c.date_of_birth, + c.avatar_url, + c.bio, + ST_Distance(o.loc, c.location)::double precision AS distance_meters, + c.is_discoverable, + ( + SELECT jsonb_build_object( + 'id', u.id, + 'username', u.username, + 'display_name', u.display_name + ) + FROM public.users u + WHERE u.id = c.owner_id + ) AS owner + FROM origin o + CROSS JOIN public.pets c + LEFT JOIN public.swipes s + ON s.actor_id = p_actor_pet_id + AND s.target_id = c.id + WHERE c.id != p_actor_pet_id + AND c.owner_id != o.owner_id + AND c.is_public IS TRUE + AND c.is_discoverable IS TRUE + AND c.archived_at IS NULL + AND c.location IS NOT NULL + AND o.loc IS NOT NULL + AND ST_DWithin(o.loc, c.location, p_radius_meters) + AND s.id IS NULL + AND ( + p_species IS NULL + OR cardinality(p_species) = 0 + OR EXISTS ( + SELECT 1 + FROM unnest(p_species) AS u(species_text) + WHERE lower(trim(u.species_text)) = lower(trim(c.species)) + ) + ) + AND ( + c.date_of_birth IS NULL + OR ( + (p_min_age_years IS NULL + OR date_part('year', age(current_date, c.date_of_birth))::int >= p_min_age_years) + AND + (p_max_age_years IS NULL + OR date_part('year', age(current_date, c.date_of_birth))::int <= p_max_age_years) + ) + ) + ORDER BY c.created_at DESC + OFFSET greatest(coalesce(p_offset, 0), 0) + LIMIT greatest(coalesce(nullif(p_limit, 0), 20), 1); +$$; + +REVOKE ALL ON FUNCTION public.matching_discovery_candidates(uuid, double precision, integer, integer, text[], integer, integer) FROM PUBLIC; +GRANT EXECUTE ON FUNCTION public.matching_discovery_candidates(uuid, double precision, integer, integer, text[], integer, integer) TO authenticated; +GRANT EXECUTE ON FUNCTION public.matching_discovery_candidates(uuid, double precision, integer, integer, text[], integer, integer) TO service_role; diff --git a/supabase/migrations/20260604180000_pr17_revoke_anon_execute_sensitive_rpcs.sql b/supabase/migrations/20260604180000_pr17_revoke_anon_execute_sensitive_rpcs.sql new file mode 100644 index 0000000..96cfdbc --- /dev/null +++ b/supabase/migrations/20260604180000_pr17_revoke_anon_execute_sensitive_rpcs.sql @@ -0,0 +1,9 @@ +-- Revoke anon EXECUTE on SECURITY DEFINER functions that should only be +-- callable by authenticated users. Flagged by Supabase security advisors. + +-- Chat / social RPCs +REVOKE EXECUTE ON FUNCTION public.ensure_direct_chat_thread(uuid, uuid) FROM anon; +REVOKE EXECUTE ON FUNCTION public.get_chat_inbox(uuid) FROM anon; + +-- Matching discovery (contains location + pet data — authenticated only) +REVOKE EXECUTE ON FUNCTION public.matching_discovery_candidates(uuid, double precision, integer, integer, text[], integer, integer) FROM anon; diff --git a/vercel.json b/vercel.json new file mode 100644 index 0000000..d6cdbdf --- /dev/null +++ b/vercel.json @@ -0,0 +1,25 @@ +{ + "outputDirectory": "build/web", + "buildCommand": "", + "framework": null, + "headers": [ + { + "source": "/flutter_service_worker.js", + "headers": [ + { "key": "Cache-Control", "value": "no-cache, no-store, must-revalidate" }, + { "key": "Service-Worker-Allowed", "value": "/" } + ] + }, + { + "source": "/(.*)\\.wasm", + "headers": [ + { "key": "Content-Type", "value": "application/wasm" }, + { "key": "Cross-Origin-Embedder-Policy", "value": "require-corp" }, + { "key": "Cross-Origin-Opener-Policy", "value": "same-origin" } + ] + } + ], + "rewrites": [ + { "source": "/((?!.*\\.).*)", "destination": "/index.html" } + ] +} diff --git a/web/index.html b/web/index.html index 72bda0f..c99bb71 100644 --- a/web/index.html +++ b/web/index.html @@ -1,47 +1,123 @@ - + - + - This is a placeholder for base href that will be replaced by the value of - the `--base-href` argument provided to `flutter build`. - --> - + - - - + + + - + - - - + + + + + + + - + - petfolio + PetFolio + + + + - + + + + + + + + diff --git a/web/manifest.json b/web/manifest.json index 2e485d6..286e3e8 100644 --- a/web/manifest.json +++ b/web/manifest.json @@ -1,13 +1,16 @@ { - "name": "petfolio", - "short_name": "petfolio", - "start_url": ".", + "name": "PetFolio", + "short_name": "PetFolio", + "description": "Your pet's social network, health tracker & marketplace.", + "start_url": "/", "display": "standalone", - "background_color": "#0175C2", - "theme_color": "#0175C2", - "description": "A new Flutter project.", + "background_color": "#FFF4E6", + "theme_color": "#FF8A4C", "orientation": "portrait-primary", "prefer_related_applications": false, + "categories": ["lifestyle", "social", "health"], + "scope": "/", + "lang": "en", "icons": [ { "src": "icons/Icon-192.png", @@ -31,5 +34,6 @@ "type": "image/png", "purpose": "maskable" } - ] + ], + "screenshots": [] } diff --git a/web/pwa_banner.js b/web/pwa_banner.js new file mode 100644 index 0000000..a6bccc5 --- /dev/null +++ b/web/pwa_banner.js @@ -0,0 +1,69 @@ +(function () { + 'use strict'; + + // Only show on iOS Safari that is NOT already installed as a PWA + var isIos = /iphone|ipad|ipod/i.test(navigator.userAgent); + var isInStandalone = ('standalone' in navigator) && navigator.standalone; + var dismissed = localStorage.getItem('pwa_banner_dismissed'); + + if (!isIos || isInStandalone || dismissed) return; + + // Inject styles + var style = document.createElement('style'); + style.textContent = [ + '#pwa-banner{', + 'position:fixed;bottom:0;left:0;right:0;z-index:99999;', + 'background:#fff;color:#1a1014;', + 'border-radius:20px 20px 0 0;', + 'box-shadow:0 -4px 24px rgba(0,0,0,.15);', + 'padding:16px 20px 32px;', + 'font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,sans-serif;', + 'display:flex;align-items:flex-start;gap:14px;', + 'animation:slideUp .35s cubic-bezier(.16,1,.3,1) both;', + '}', + '@keyframes slideUp{from{transform:translateY(100%)}to{transform:translateY(0)}}', + '#pwa-banner img{width:56px;height:56px;border-radius:13px;flex-shrink:0;box-shadow:0 2px 8px rgba(0,0,0,.18);}', + '#pwa-banner-body{flex:1;}', + '#pwa-banner-title{font-size:15px;font-weight:700;margin:0 0 4px;}', + '#pwa-banner-desc{font-size:13px;color:#555;margin:0 0 12px;line-height:1.45;}', + '#pwa-banner-steps{font-size:12.5px;color:#333;line-height:1.6;margin:0;}', + '#pwa-banner-steps span{background:#f0f0f0;border-radius:6px;padding:1px 6px;font-weight:600;}', + '#pwa-banner-close{', + 'position:absolute;top:14px;right:16px;', + 'background:none;border:none;cursor:pointer;', + 'font-size:20px;color:#999;line-height:1;padding:4px;', + '}', + '@media(prefers-color-scheme:dark){', + '#pwa-banner{background:#1a1014;color:#fff;}', + '#pwa-banner-desc{color:#aaa;}', + '#pwa-banner-steps{color:#ddd;}', + '#pwa-banner-steps span{background:#333;}', + '#pwa-banner-close{color:#666;}', + '}', + ].join(''); + document.head.appendChild(style); + + // Build banner HTML + var banner = document.createElement('div'); + banner.id = 'pwa-banner'; + banner.setAttribute('role', 'banner'); + banner.innerHTML = [ + 'PetFolio icon', + '
', + '

Install PetFolio

', + '

Add to your Home Screen for the full app experience — works offline too.

', + '

', + 'Tap Share ↑ then Add to Home Screen', + '

', + '
', + '', + ].join(''); + + document.body.appendChild(banner); + + document.getElementById('pwa-banner-close').addEventListener('click', function () { + try { localStorage.setItem('pwa_banner_dismissed', '1'); } catch (_) {} + banner.style.animation = 'slideUp .25s cubic-bezier(.16,1,.3,1) reverse both'; + setTimeout(function () { banner.remove(); }, 280); + }); +}());