Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions .Jules/changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,14 @@
## [Unreleased]

### Added
- **Web Skeleton Loading:** Implemented comprehensive skeleton loading for the `GroupDetails` page.
- **Features:**
- Replaced the generic spinner with a skeleton layout perfectly matching the page's structure (Hero, Cards, Tabs, Expenses).
- Full dual-theme support (Glassmorphism & Neobrutalism) matching application colors.
- Smooth `framer-motion` pulsing animations on skeleton elements.
- Uses existing `Skeleton` UI primitive while orchestrating complex layouts.
- **Technical:** Created `web/components/skeletons/GroupDetailsSkeleton.tsx`. Integrated into `web/pages/GroupDetails.tsx`.

- **Password Strength Meter:** Added a visual password strength indicator to the signup form.
- **Features:**
- Real-time strength calculation (Length, Uppercase, Lowercase, Number, Symbol).
Expand Down
22 changes: 22 additions & 0 deletions .Jules/knowledge.md
Original file line number Diff line number Diff line change
Expand Up @@ -740,6 +740,28 @@ _Document errors and their solutions here as you encounter them._

---

### ✅ Successful PR Pattern: Group Details Skeleton

**Date:** 2026-03-06
**Context:** Implementing robust loading states for complex pages

**What was implemented:**
1. Mapped the exact UI structure of `GroupDetails.tsx` (Hero, Cards, Tabs, Expenses).
2. Created a cohesive `GroupDetailsSkeleton.tsx` utilizing `framer-motion` and the existing `Skeleton` UI primitive.
3. Supported both Glassmorphism and Neobrutalism natively within the skeleton layouts.
4. Integrated a mocked test script (`verify_group_details_skeleton.py`) using Playwright to forcefully delay API responses and visually verify the loading state.

**Why it succeeded:**
- ✅ Prevented jarring layout shifts on initial load by matching the final rendered layout's dimensions.
- ✅ Respected the dual-theming system fully.
- ✅ Leveraged Playwright's `route.fulfill({ delay })` to reliably capture and test infinite loading states locally.

**Key learnings:**
- When building composite skeletons, breaking them down into structural sub-components (like `SkeletonHero`, `SkeletonCards`, `SkeletonTabs`) makes the code much more maintainable.
- Playwright is incredibly effective for verifying transient UI states (like loading) by intercepting and artificially delaying network requests.

---

## Dependencies Reference

### Web
Expand Down
7 changes: 7 additions & 0 deletions .Jules/todo.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,13 @@
- Impact: Native feel, users can easily refresh data
- Size: ~150 lines

- [x] **[ux]** Complete skeleton loading for GroupDetails page
- Completed: 2026-03-06
- Files modified: `web/pages/GroupDetails.tsx`, `web/components/skeletons/GroupDetailsSkeleton.tsx`
- Context: Replace ActivityIndicator with skeleton layout matching content
- Impact: Better loading experience, less jarring layout shift
- Size: ~120 lines

Comment on lines +60 to +66
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Task entry placed under wrong section.

This Web skeleton loading task is placed under the "### Mobile" section (which starts at line 51), but the affected files are in web/. It should be placed under the "### Web" section (starting at line 11) with the other completed web tasks.

📝 Suggested fix: Move entry under Web section

Move lines 60-66 to after line 49 (after the last completed Web task) and before the "### Mobile" section.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.Jules/todo.md around lines 60 - 66, The completed skeleton loading task for
the web should be moved out of the "### Mobile" section into the "### Web"
section; locate the checklist entry referencing web/pages/GroupDetails.tsx and
web/components/skeletons/GroupDetailsSkeleton.tsx (the lines showing "- [x]
**[ux]** Complete skeleton loading for GroupDetails page" and its metadata) and
cut/paste it so it appears with the other completed Web tasks (place it after
the last completed Web task and before the "### Mobile" header) to keep
platform-specific tasks grouped correctly.

- [ ] **[ux]** Complete skeleton loading for HomeScreen groups
- File: `mobile/screens/HomeScreen.js`
- Context: Replace ActivityIndicator with skeleton group cards
Expand Down
136 changes: 136 additions & 0 deletions web/components/skeletons/GroupDetailsSkeleton.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
import React from 'react';
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial

Unused React import.

With React 19 and the new JSX transform, the React import is no longer necessary when you're only using JSX without explicit React.* calls.

♻️ Suggested cleanup
-import React from 'react';
 import { useTheme } from '../../contexts/ThemeContext';
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
import React from 'react';
import { useTheme } from '../../contexts/ThemeContext';
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/components/skeletons/GroupDetailsSkeleton.tsx` at line 1, Remove the
unused React import from GroupDetailsSkeleton by deleting the top-level "import
React from 'react';" statement in the GroupDetailsSkeleton component file;
ensure there are no other direct React.* references (hooks or types) in the
file—if there are, keep or replace them with named imports (e.g., import {
useState } from 'react')—otherwise remove the import to rely on the new JSX
transform.

import { useTheme } from '../../contexts/ThemeContext';
import { THEMES } from '../../constants';
import { Skeleton } from '../ui/Skeleton';
import { motion } from 'framer-motion';

export const GroupDetailsSkeleton = () => {
const { style } = useTheme();

return (
<div className="p-4 md:p-6 max-w-7xl mx-auto space-y-8 min-h-screen">
{/* Hero Section Skeleton */}
<motion.div
initial={{ y: -20, opacity: 0 }}
animate={{ y: 0, opacity: 1 }}
className={`relative overflow-hidden rounded-3xl h-48 md:h-64 ${
style === THEMES.NEOBRUTALISM ? 'border-2 border-black shadow-[8px_8px_0px_0px_rgba(0,0,0,1)]' : 'border border-white/10 shadow-2xl'
}`}
Comment on lines +13 to +18
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Verify the actual hero section's height constraints in GroupDetails.tsx
rg -n 'Immersive Header|motion\.div' web/pages/GroupDetails.tsx | head -20
# Check for h-48, h-64, or min-h classes in the hero section
sed -n '571,624p' web/pages/GroupDetails.tsx | rg 'h-\d+|min-h'

Repository: Devasy/splitwiser

Length of output: 1467


🏁 Script executed:

# Read the full Immersive Header section
sed -n '570,625p' web/pages/GroupDetails.tsx

Repository: Devasy/splitwiser

Length of output: 4108


🏁 Script executed:

# Also read the GroupDetailsSkeleton file to see the skeleton structure
cat -n web/components/skeletons/GroupDetailsSkeleton.tsx

Repository: Devasy/splitwiser

Length of output: 8068


Hero skeleton height (h-48/h-64) does not match actual component height, causing potential layout shift.

The skeleton constrains the hero to fixed heights with h-full, but the actual Immersive Header in GroupDetails.tsx (lines 571-624) is content-driven with no height constraints. Given the large text (text-5xl md:text-7xl), padding (p-8 md:p-12), and avatars, the actual component will likely exceed these fixed dimensions when content loads, creating visible layout shift.

Consider either:

  1. Adding matching height constraints to the actual hero section, or
  2. Making the skeleton height content-driven to match the actual component
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/components/skeletons/GroupDetailsSkeleton.tsx` around lines 13 - 18, The
skeleton's hero in GroupDetailsSkeleton.tsx uses fixed heights (h-48 / h-64 on
the motion.div) which don't match the content-driven Immersive Header in
GroupDetails.tsx (large text sizes text-5xl/text-7xl and padding p-8/p-12),
causing layout shift; fix by either removing the fixed h-48/h-64 from the
motion.div and making it content-driven (use h-auto/min-h plus matching
padding/typography spacing) so it mirrors the Immersive Header rendering, or
apply the same fixed height/padding/typography constraints to the Immersive
Header container so both components (GroupDetailsSkeleton's motion.div and the
Immersive Header in GroupDetails.tsx) share identical sizing and spacing.

>
<div className={`absolute inset-0 ${style === THEMES.NEOBRUTALISM ? 'bg-pink-100' : 'bg-gradient-to-r from-purple-600/20 to-pink-600/20 backdrop-blur-3xl'}`} />
<div className="relative z-10 p-8 md:p-12 h-full flex flex-col md:flex-row justify-between items-start md:items-end gap-6">
<div className="space-y-4 w-full max-w-sm">
<div className="flex items-center gap-3">
<Skeleton className="h-6 w-16" />
<Skeleton className="h-4 w-24" />
</div>
<Skeleton className="h-12 md:h-16 w-3/4" />
</div>
<div className="flex flex-col items-end gap-4">
<div className="flex -space-x-4">
{[1, 2, 3, 4, 5].map((i) => (
<Skeleton
key={i}
className={`w-12 h-12 rounded-full border-4 ${style === THEMES.NEOBRUTALISM ? 'border-black' : 'border-indigo-600'}`}
/>
))}
<Skeleton className={`w-12 h-12 rounded-full border-4 ${style === THEMES.NEOBRUTALISM ? 'border-black' : 'border-indigo-600'}`} />
</div>
Comment on lines +31 to +38
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial

Minor: Avatar count mismatch with actual UI.

The skeleton renders 6 avatar placeholders (5 in loop + 1 outside), but the actual GroupDetails hero renders up to 5 member avatars + an overflow indicator + a Settings button (potentially 7 elements). This is a minor visual discrepancy during the skeleton-to-content transition.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/components/skeletons/GroupDetailsSkeleton.tsx` around lines 31 - 38, The
skeleton currently renders 6 avatar placeholders (five in the map plus one
extra) which doesn't match GroupDetails hero; update the JSX in
GroupDetailsSkeleton.tsx so it renders the same elements as the real hero: five
member avatar Skeletons (the existing map), one overflow indicator Skeleton, and
one Settings-button Skeleton (total 7). Keep the same Skeleton component,
className logic and THEMES.NEOBRUTALISM conditional for border color so the
placeholders visually match the real UI.

<div className="flex gap-3">
<Skeleton className={`h-10 w-28 ${style === THEMES.NEOBRUTALISM ? 'rounded-none border-2 border-black' : 'rounded-xl'}`} />
<Skeleton className={`h-10 w-32 ${style === THEMES.NEOBRUTALISM ? 'rounded-none border-2 border-black' : 'rounded-xl'}`} />
</div>
</div>
</div>
</motion.div>

{/* Group Totals Summary Cards Skeleton */}
<motion.div
initial={{ opacity: 0, y: 10 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.1 }}
className="grid grid-cols-2 md:grid-cols-4 gap-4 max-w-5xl mx-auto"
>
{[1, 2, 3, 4].map((i) => (
<div
key={i}
className={`p-4 flex flex-col items-center justify-center h-28 ${
style === THEMES.NEOBRUTALISM
? 'bg-white border-2 border-black shadow-[4px_4px_0px_0px_rgba(0,0,0,1)]'
: 'bg-white/5 backdrop-blur-sm rounded-2xl border border-white/10'
}`}
>
<Skeleton className="h-3 w-20 mb-3" />
<Skeleton className="h-8 w-24 mb-2" />
<Skeleton className="h-3 w-16" />
</div>
))}
</motion.div>

{/* Navigation Pills Skeleton */}
<motion.div
initial={{ opacity: 0, scale: 0.95 }}
animate={{ opacity: 1, scale: 1 }}
transition={{ delay: 0.2 }}
className="flex justify-center"
>
<div className={`p-1.5 flex gap-2 ${
style === THEMES.NEOBRUTALISM
? 'bg-white border-2 border-black shadow-[4px_4px_0px_0px_rgba(0,0,0,1)] rounded-none'
: 'bg-white/5 border border-white/10 backdrop-blur-xl rounded-2xl'
}`}>
<Skeleton className={`h-10 w-32 ${style === THEMES.NEOBRUTALISM ? 'rounded-none' : 'rounded-xl'}`} />
<Skeleton className={`h-10 w-32 ${style === THEMES.NEOBRUTALISM ? 'rounded-none' : 'rounded-xl'}`} />
<Skeleton className={`h-10 w-32 ${style === THEMES.NEOBRUTALISM ? 'rounded-none' : 'rounded-xl'}`} />
</div>
</motion.div>

{/* Content Area Skeleton (Expenses List) */}
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.3 }}
className="space-y-4 max-w-3xl mx-auto"
>
{/* Search Bar Skeleton */}
<div className="relative mb-6">
<Skeleton className={`w-full h-12 ${
style === THEMES.NEOBRUTALISM ? 'rounded-none border-2 border-black' : 'rounded-2xl'
}`} />
</div>

{/* List Items Skeletons */}
{[1, 2, 3, 4, 5].map((i) => (
<div
key={i}
className={`p-5 flex items-center gap-5 ${
style === THEMES.NEOBRUTALISM
? 'bg-white border-2 border-black shadow-[4px_4px_0px_0px_rgba(0,0,0,1)] rounded-none'
: 'bg-white/5 border border-white/10 rounded-2xl backdrop-blur-sm'
}`}
>
{/* Date Box */}
<Skeleton className={`w-16 h-16 flex-shrink-0 ${
style === THEMES.NEOBRUTALISM ? 'rounded-none border-2 border-black' : 'rounded-xl'
}`} />

{/* Description & Payer */}
<div className="flex-1 space-y-3">
<Skeleton className="h-6 w-3/4 max-w-[200px]" />
<div className="flex items-center gap-2">
<Skeleton className={`w-5 h-5 ${style === THEMES.NEOBRUTALISM ? 'rounded-none' : 'rounded-full'}`} />
<Skeleton className="h-4 w-32" />
</div>
</div>

{/* Amount & Share */}
<div className="text-right space-y-2">
<Skeleton className="h-7 w-20 ml-auto" />
<Skeleton className="h-4 w-16 ml-auto" />
</div>
</div>
))}
</motion.div>
</div>
);
};
Loading
Loading