From 6b8622e812f0ead088e6c77b8740ea065a2ffde0 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 2 Oct 2025 10:42:29 +0000 Subject: [PATCH 01/13] Initial plan From 53d17a8ebd510dbc6bfb69b05105481b73296784 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 8 Oct 2025 08:37:59 +0000 Subject: [PATCH 02/13] Add next payment date feature with billing cycles Co-authored-by: ajnart <49837342+ajnart@users.noreply.github.com> --- .gitignore | 1 + app/components/AddSubscriptionPopover.tsx | 137 ++++++++++++++++++- app/components/EditSubscriptionModal.tsx | 156 +++++++++++++++++++++- app/components/SubscriptionCard.tsx | 44 +++++- app/store/subscriptionStore.ts | 12 +- app/utils/nextPaymentDate.ts | 141 +++++++++++++++++++ 6 files changed, 479 insertions(+), 12 deletions(-) create mode 100644 app/utils/nextPaymentDate.ts diff --git a/.gitignore b/.gitignore index 4f256be..000b1d8 100644 --- a/.gitignore +++ b/.gitignore @@ -4,6 +4,7 @@ /node_modules /.pnp .pnp.js +package-lock.json # testing /coverage diff --git a/app/components/AddSubscriptionPopover.tsx b/app/components/AddSubscriptionPopover.tsx index 204eb9f..969def4 100644 --- a/app/components/AddSubscriptionPopover.tsx +++ b/app/components/AddSubscriptionPopover.tsx @@ -3,15 +3,19 @@ import { Input } from '@/components/ui/input' import { Label } from '@/components/ui/label' import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover' import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select' +import { Switch } from '@/components/ui/switch' +import { Calendar } from '@/components/ui/calendar' import { zodResolver } from '@hookform/resolvers/zod' import { useLoaderData } from '@remix-run/react' -import { PlusCircle } from 'lucide-react' +import { CalendarIcon, PlusCircle } from 'lucide-react' import { useEffect, useState } from 'react' -import { useForm } from 'react-hook-form' +import { Controller, useForm } from 'react-hook-form' import { toast } from 'sonner' import { z } from 'zod' import type { loader } from '~/routes/_index' -import type { Subscription } from '~/store/subscriptionStore' +import type { BillingCycle, Subscription } from '~/store/subscriptionStore' +import { cn } from '~/lib/utils' +import { initializeNextPaymentDate } from '~/utils/nextPaymentDate' import { IconUrlInput } from './IconFinder' interface AddSubscriptionPopoverProps { @@ -24,6 +28,10 @@ const subscriptionSchema = z.object({ currency: z.string().min(1, 'Currency is required'), icon: z.string().optional(), domain: z.string().url('Invalid URL'), + billingCycle: z.enum(['monthly', 'yearly', 'weekly', 'daily']).optional(), + nextPaymentDate: z.string().optional(), + showNextPayment: z.boolean().optional(), + paymentDay: z.number().min(1).max(31).optional(), }) type SubscriptionFormValues = z.infer @@ -32,6 +40,7 @@ export const AddSubscriptionPopover: React.FC = ({ const { rates } = useLoaderData() const [open, setOpen] = useState(false) const [shouldFocus, setShouldFocus] = useState(false) + const [calendarOpen, setCalendarOpen] = useState(false) const { register, @@ -41,6 +50,7 @@ export const AddSubscriptionPopover: React.FC = ({ setFocus, setValue, watch, + control, } = useForm({ resolver: zodResolver(subscriptionSchema), defaultValues: { @@ -49,10 +59,18 @@ export const AddSubscriptionPopover: React.FC = ({ price: 0, currency: 'USD', domain: '', + billingCycle: undefined, + nextPaymentDate: undefined, + showNextPayment: false, + paymentDay: undefined, }, }) const iconValue = watch('icon') + const billingCycleValue = watch('billingCycle') + const showNextPaymentValue = watch('showNextPayment') + const paymentDayValue = watch('paymentDay') + const nextPaymentDateValue = watch('nextPaymentDate') useEffect(() => { if (shouldFocus) { @@ -61,6 +79,17 @@ export const AddSubscriptionPopover: React.FC = ({ } }, [shouldFocus, setFocus]) + // Auto-calculate next payment date when billing cycle or payment day changes + useEffect(() => { + if (billingCycleValue && showNextPaymentValue) { + const currentDate = nextPaymentDateValue + if (!currentDate) { + const newDate = initializeNextPaymentDate(billingCycleValue, paymentDayValue) + setValue('nextPaymentDate', newDate) + } + } + }, [billingCycleValue, showNextPaymentValue, paymentDayValue, nextPaymentDateValue, setValue]) + const onSubmit = (data: SubscriptionFormValues) => { addSubscription(data) toast.success(`${data.name} added successfully.`) @@ -74,6 +103,16 @@ export const AddSubscriptionPopover: React.FC = ({ } }, [open, setFocus]) + const formatDateForDisplay = (dateString: string | undefined) => { + if (!dateString) return 'Pick a date' + const date = new Date(dateString) + return date.toLocaleDateString('en-US', { + month: 'short', + day: 'numeric', + year: 'numeric', + }) + } + return ( @@ -82,7 +121,7 @@ export const AddSubscriptionPopover: React.FC = ({ Add Subscription - +

Add Subscription

@@ -134,6 +173,96 @@ export const AddSubscriptionPopover: React.FC = ({

{errors.domain?.message}

+
+ + +
+ {billingCycleValue === 'monthly' && ( +
+ + v === '' || v === undefined ? undefined : Number(v), + })} + className={errors.paymentDay ? 'border-red-500' : ''} + /> +

{errors.paymentDay?.message}

+
+ )} + {billingCycleValue && ( + <> +
+ ( + + )} + /> + +
+ {showNextPaymentValue && ( +
+ + ( + + + + + + { + field.onChange(date?.toISOString().split('T')[0]) + setCalendarOpen(false) + }} + disabled={(date) => date < new Date(new Date().setHours(0, 0, 0, 0))} + initialFocus + /> + + + )} + /> +
+ )} + + )}
+
+ + ( + + )} + /> +

{'\u00A0'}

+
+ {watchedFields.billingCycle === 'monthly' && ( +
+ + ( + { + const value = e.target.value === '' ? undefined : Number.parseInt(e.target.value) + field.onChange(value) + }} + value={field.value ?? ''} + className={errors.paymentDay ? 'border-red-500' : ''} + /> + )} + /> +

{errors.paymentDay?.message || '\u00A0'}

+
+ )} + {watchedFields.billingCycle && ( + <> +
+ ( + + )} + /> + +
+ {watchedFields.showNextPayment && ( +
+ + ( + + + + + + { + field.onChange(date?.toISOString().split('T')[0]) + setCalendarOpen(false) + }} + disabled={(date) => date < new Date(new Date().setHours(0, 0, 0, 0))} + initialFocus + /> + + + )} + /> +

{'\u00A0'}

+
+ )} + + )}
{}} onDelete={() => {}} /> diff --git a/app/components/SubscriptionCard.tsx b/app/components/SubscriptionCard.tsx index af666c2..1483e14 100644 --- a/app/components/SubscriptionCard.tsx +++ b/app/components/SubscriptionCard.tsx @@ -1,10 +1,12 @@ import { motion } from 'framer-motion' -import { Edit, Trash2 } from 'lucide-react' +import { Calendar, Edit, Trash2 } from 'lucide-react' import type React from 'react' +import { Badge } from '~/components/ui/badge' import { Button } from '~/components/ui/button' import { Card, CardContent } from '~/components/ui/card' import { LinkPreview } from '~/components/ui/link-preview' import type { Subscription } from '~/store/subscriptionStore' +import { calculateNextPaymentDate } from '~/utils/nextPaymentDate' interface SubscriptionCardProps { subscription: Subscription @@ -14,7 +16,7 @@ interface SubscriptionCardProps { } const SubscriptionCard: React.FC = ({ subscription, onEdit, onDelete, className }) => { - const { id, name, price, currency, domain, icon } = subscription + const { id, name, price, currency, domain, icon, billingCycle, nextPaymentDate, showNextPayment, paymentDay } = subscription // Sanitize the domain URL const sanitizeDomain = (domain: string) => { @@ -31,6 +33,29 @@ const SubscriptionCard: React.FC = ({ subscription, onEdi // Use custom icon if available, otherwise fall back to domain favicon const logoUrl = icon || defaultLogoUrl + // Calculate and format next payment date + const getNextPaymentDisplay = () => { + if (!showNextPayment || !billingCycle) { + return null + } + + const calculatedDate = calculateNextPaymentDate(billingCycle, paymentDay, nextPaymentDate) + if (!calculatedDate) { + return null + } + + const date = new Date(calculatedDate) + const formattedDate = date.toLocaleDateString('en-US', { + month: 'short', + day: 'numeric', + year: 'numeric', + }) + + return formattedDate + } + + const nextPaymentDisplay = getNextPaymentDisplay() + return ( = ({ subscription, onEdi className={`group ${className}`} > -
+
- {billingCycleValue === 'monthly' && ( -
- - (v === '' || v === undefined ? undefined : Number(v)), - })} - className={errors.paymentDay ? 'border-red-500' : ''} - /> -

{errors.paymentDay?.message}

-
- )} {billingCycleValue && ( <>
diff --git a/app/components/EditSubscriptionModal.tsx b/app/components/EditSubscriptionModal.tsx index 5fb7517..5c4a456 100644 --- a/app/components/EditSubscriptionModal.tsx +++ b/app/components/EditSubscriptionModal.tsx @@ -36,7 +36,6 @@ const subscriptionSchema = z.object({ billingCycle: z.enum(['monthly', 'yearly', 'weekly', 'daily']).optional(), nextPaymentDate: z.string().optional(), showNextPayment: z.boolean().optional(), - paymentDay: z.number().min(1).max(31).optional(), }) const EditSubscriptionModal: React.FC = ({ @@ -66,7 +65,6 @@ const EditSubscriptionModal: React.FC = ({ billingCycle: undefined as BillingCycle | undefined, nextPaymentDate: undefined as string | undefined, showNextPayment: false, - paymentDay: undefined as number | undefined, }, }) @@ -83,23 +81,22 @@ const EditSubscriptionModal: React.FC = ({ billingCycle: undefined, nextPaymentDate: undefined, showNextPayment: false, - paymentDay: undefined, }) } }, [editingSubscription, reset]) const watchedFields = watch() - // Auto-calculate next payment date when billing cycle or payment day changes + // Auto-calculate next payment date when billing cycle changes useEffect(() => { if (watchedFields.billingCycle && watchedFields.showNextPayment) { const currentDate = watchedFields.nextPaymentDate if (!currentDate) { - const newDate = initializeNextPaymentDate(watchedFields.billingCycle, watchedFields.paymentDay) + const newDate = initializeNextPaymentDate(watchedFields.billingCycle) setValue('nextPaymentDate', newDate) } } - }, [watchedFields.billingCycle, watchedFields.showNextPayment, watchedFields.paymentDay, setValue]) + }, [watchedFields.billingCycle, watchedFields.showNextPayment, setValue]) const previewSubscription: Subscription = { id: 'preview', @@ -111,7 +108,6 @@ const EditSubscriptionModal: React.FC = ({ billingCycle: watchedFields.billingCycle, nextPaymentDate: watchedFields.nextPaymentDate, showNextPayment: watchedFields.showNextPayment, - paymentDay: watchedFields.paymentDay, } const onSubmit = (data: Omit) => { @@ -231,32 +227,6 @@ const EditSubscriptionModal: React.FC = ({ />

{'\u00A0'}

- {watchedFields.billingCycle === 'monthly' && ( -
- - ( - { - const value = e.target.value === '' ? undefined : Number.parseInt(e.target.value) - field.onChange(value) - }} - value={field.value ?? ''} - className={errors.paymentDay ? 'border-red-500' : ''} - /> - )} - /> -

{errors.paymentDay?.message || '\u00A0'}

-
- )} {watchedFields.billingCycle && ( <>
diff --git a/app/components/SubscriptionCard.tsx b/app/components/SubscriptionCard.tsx index 6fc3eda..2eda28a 100644 --- a/app/components/SubscriptionCard.tsx +++ b/app/components/SubscriptionCard.tsx @@ -16,8 +16,7 @@ interface SubscriptionCardProps { } const SubscriptionCard: React.FC = ({ subscription, onEdit, onDelete, className }) => { - const { id, name, price, currency, domain, icon, billingCycle, nextPaymentDate, showNextPayment, paymentDay } = - subscription + const { id, name, price, currency, domain, icon, billingCycle, nextPaymentDate, showNextPayment } = subscription // Sanitize the domain URL const sanitizeDomain = (domain: string) => { @@ -40,7 +39,7 @@ const SubscriptionCard: React.FC = ({ subscription, onEdi return null } - const calculatedDate = calculateNextPaymentDate(billingCycle, paymentDay, nextPaymentDate) + const calculatedDate = calculateNextPaymentDate(billingCycle, nextPaymentDate) if (!calculatedDate) { return null } @@ -82,13 +81,13 @@ const SubscriptionCard: React.FC = ({ subscription, onEdi
- {`${name} + {`${name}

{name}

-

{`${currency} ${price}`}

+

{`${currency} ${price}`}

{billingCycle && ( - + per{' '} {billingCycle === 'monthly' ? 'Monthly' @@ -100,7 +99,7 @@ const SubscriptionCard: React.FC = ({ subscription, onEdi )} {nextPaymentDisplay && ( -
+
Next Payment: {nextPaymentDisplay}
diff --git a/app/store/subscriptionStore.ts b/app/store/subscriptionStore.ts index 5734759..34a202b 100644 --- a/app/store/subscriptionStore.ts +++ b/app/store/subscriptionStore.ts @@ -13,7 +13,6 @@ export interface Subscription { billingCycle?: BillingCycle nextPaymentDate?: string // ISO date string showNextPayment?: boolean - paymentDay?: number // Day of month for monthly subscriptions (1-31) } interface SubscriptionStore { @@ -144,8 +143,7 @@ function isValidSubscription(sub: any): sub is Subscription { (sub.icon === undefined || typeof sub.icon === 'string') && (sub.billingCycle === undefined || ['monthly', 'yearly', 'weekly', 'daily'].includes(sub.billingCycle)) && (sub.nextPaymentDate === undefined || typeof sub.nextPaymentDate === 'string') && - (sub.showNextPayment === undefined || typeof sub.showNextPayment === 'boolean') && - (sub.paymentDay === undefined || typeof sub.paymentDay === 'number') + (sub.showNextPayment === undefined || typeof sub.showNextPayment === 'boolean') ) } diff --git a/app/utils/nextPaymentDate.ts b/app/utils/nextPaymentDate.ts index 4d44aab..9b3f1f8 100644 --- a/app/utils/nextPaymentDate.ts +++ b/app/utils/nextPaymentDate.ts @@ -6,7 +6,6 @@ import type { BillingCycle } from '~/store/subscriptionStore' */ export function calculateNextPaymentDate( billingCycle: BillingCycle | undefined, - paymentDay: number | undefined, currentNextPaymentDate?: string, ): string | undefined { if (!billingCycle) { @@ -26,10 +25,10 @@ export function calculateNextPaymentDate( } // Otherwise, calculate the next occurrence - nextDate = getNextOccurrence(nextDate, billingCycle, paymentDay, now) + nextDate = getNextOccurrence(nextDate, billingCycle, now) } else { // No existing date, calculate from now - nextDate = getNextOccurrence(now, billingCycle, paymentDay, now) + nextDate = getNextOccurrence(now, billingCycle, now) } return nextDate.toISOString().split('T')[0] // Return YYYY-MM-DD format @@ -38,12 +37,7 @@ export function calculateNextPaymentDate( /** * Get the next occurrence of a payment date after the current date */ -function getNextOccurrence( - baseDate: Date, - billingCycle: BillingCycle, - paymentDay: number | undefined, - now: Date, -): Date { +function getNextOccurrence(baseDate: Date, billingCycle: BillingCycle, now: Date): Date { const result = new Date(baseDate) switch (billingCycle) { @@ -62,23 +56,9 @@ function getNextOccurrence( break case 'monthly': - // For monthly, respect the paymentDay if provided - if (paymentDay !== undefined && paymentDay >= 1 && paymentDay <= 31) { - // Start from now and find the next occurrence of paymentDay - result.setFullYear(now.getFullYear()) - result.setMonth(now.getMonth()) - result.setDate(Math.min(paymentDay, getLastDayOfMonth(result.getFullYear(), result.getMonth()))) - - // If this date is in the past, move to next month - while (result <= now) { - result.setMonth(result.getMonth() + 1) - result.setDate(Math.min(paymentDay, getLastDayOfMonth(result.getFullYear(), result.getMonth()))) - } - } else { - // No specific day, just add months until we're in the future - while (result <= now) { - result.setMonth(result.getMonth() + 1) - } + // Add months until we're in the future + while (result <= now) { + result.setMonth(result.getMonth() + 1) } break @@ -93,20 +73,10 @@ function getNextOccurrence( return result } -/** - * Get the last day of a given month - */ -function getLastDayOfMonth(year: number, month: number): number { - return new Date(year, month + 1, 0).getDate() -} - /** * Initialize a next payment date from today based on billing cycle */ -export function initializeNextPaymentDate( - billingCycle: BillingCycle | undefined, - paymentDay?: number, -): string | undefined { +export function initializeNextPaymentDate(billingCycle: BillingCycle | undefined): string | undefined { if (!billingCycle) { return undefined } @@ -124,12 +94,7 @@ export function initializeNextPaymentDate( break case 'monthly': - if (paymentDay !== undefined && paymentDay >= 1 && paymentDay <= 31) { - nextDate.setMonth(nextDate.getMonth() + 1) - nextDate.setDate(Math.min(paymentDay, getLastDayOfMonth(nextDate.getFullYear(), nextDate.getMonth()))) - } else { - nextDate.setMonth(nextDate.getMonth() + 1) - } + nextDate.setMonth(nextDate.getMonth() + 1) break case 'yearly': From 80ff03f1b1f14b12c7c38cc346c93d1b9697ce5a Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 15 Oct 2025 14:45:21 +0000 Subject: [PATCH 05/13] Fix card height consistency with min-height constraint Co-authored-by: ajnart <49837342+ajnart@users.noreply.github.com> --- app/components/SubscriptionCard.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/components/SubscriptionCard.tsx b/app/components/SubscriptionCard.tsx index 2eda28a..1493426 100644 --- a/app/components/SubscriptionCard.tsx +++ b/app/components/SubscriptionCard.tsx @@ -63,7 +63,7 @@ const SubscriptionCard: React.FC = ({ subscription, onEdi transition={{ type: 'spring', stiffness: 300, damping: 20 }} className={`group ${className}`} > - +
- + {`${name}

{name} From 712058f717ed5045b51e8913de6adace890a1a7c Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 17 Oct 2025 08:56:31 +0000 Subject: [PATCH 06/13] Fix card height with aspect-square and add Playwright tests Co-authored-by: ajnart <49837342+ajnart@users.noreply.github.com> --- .gitignore | 3 + app/components/SubscriptionCard.tsx | 4 +- package.json | 4 + playwright.config.ts | 28 ++++++ tests/README.md | 59 +++++++++++ tests/subscription-card-height.spec.ts | 134 +++++++++++++++++++++++++ 6 files changed, 230 insertions(+), 2 deletions(-) create mode 100644 playwright.config.ts create mode 100644 tests/README.md create mode 100644 tests/subscription-card-height.spec.ts diff --git a/.gitignore b/.gitignore index 000b1d8..be87399 100644 --- a/.gitignore +++ b/.gitignore @@ -8,6 +8,9 @@ package-lock.json # testing /coverage +/test-results/ +/playwright-report/ +/playwright/.cache/ # database /prisma/db.sqlite diff --git a/app/components/SubscriptionCard.tsx b/app/components/SubscriptionCard.tsx index 1493426..1277e6e 100644 --- a/app/components/SubscriptionCard.tsx +++ b/app/components/SubscriptionCard.tsx @@ -63,7 +63,7 @@ const SubscriptionCard: React.FC = ({ subscription, onEdi transition={{ type: 'spring', stiffness: 300, damping: 20 }} className={`group ${className}`} > - +
- + {`${name}

{name} diff --git a/package.json b/package.json index 4996118..b7192de 100644 --- a/package.json +++ b/package.json @@ -18,6 +18,9 @@ "format": "biome format", "check": "biome check", "ci": "biome ci", + "test": "playwright test", + "test:ui": "playwright test --ui", + "test:headed": "playwright test --headed", "docker:build": "docker build . -t ajnart/subs", "docker:test": "docker run -p 3000:3000 --rm --name subs ajnart/subs" }, @@ -92,6 +95,7 @@ }, "devDependencies": { "@biomejs/biome": "1.9.4", + "@playwright/test": "^1.56.1", "@remix-run/dev": "^2.13.1", "@types/react": "^18.3.11", "@types/react-dom": "^18.3.1", diff --git a/playwright.config.ts b/playwright.config.ts new file mode 100644 index 0000000..d68c189 --- /dev/null +++ b/playwright.config.ts @@ -0,0 +1,28 @@ +import { defineConfig, devices } from '@playwright/test' + +export default defineConfig({ + testDir: './tests', + fullyParallel: true, + forbidOnly: !!process.env.CI, + retries: process.env.CI ? 2 : 0, + workers: process.env.CI ? 1 : undefined, + reporter: 'html', + use: { + baseURL: 'http://localhost:5173', + trace: 'on-first-retry', + }, + + projects: [ + { + name: 'chromium', + use: { ...devices['Desktop Chrome'] }, + }, + ], + + webServer: { + command: 'npm run dev', + url: 'http://localhost:5173', + reuseExistingServer: !process.env.CI, + timeout: 120000, + }, +}) diff --git a/tests/README.md b/tests/README.md new file mode 100644 index 0000000..440b990 --- /dev/null +++ b/tests/README.md @@ -0,0 +1,59 @@ +# Subscription Card Height Tests + +## Overview +This test suite verifies that subscription cards maintain consistent height regardless of their content (with or without billing cycle information). + +## Prerequisites +1. Install dependencies: `npm install` +2. Install Playwright browsers: `npx playwright install chromium` + +## Running the Tests + +### Run all tests +```bash +npm test +``` + +### Run tests with UI +```bash +npm run test:ui +``` + +### Run tests in headed mode (see browser) +```bash +npm run test:headed +``` + +### Run specific test file +```bash +npx playwright test tests/subscription-card-height.spec.ts +``` + +## Test Coverage + +### 1. All cards have the same height +Verifies that all subscription cards on the page have identical heights. + +### 2. Height remains consistent after adding monthly billing +Tests that adding a monthly billing cycle and next payment date to a subscription doesn't increase the card height. + +### 3. Cards have a square aspect ratio (1:1) +Verifies that cards are approximately square with a 1:1 width-to-height ratio (with 10% tolerance). + +### 4. Cards with billing cycle badge are not taller +Tests that newly created subscriptions with billing cycle information have the same height as existing cards. + +## Implementation Details + +The card height consistency is achieved through: +- Using `aspect-square` Tailwind class to enforce 1:1 aspect ratio +- The CSS Grid layout ensures all cards in a row have the same dimensions +- All cards use `data-testid="subscription-card"` for easy testing + +## Troubleshooting + +If tests fail: +1. Ensure the dev server is running properly +2. Check that the database has at least 2 subscriptions for comparison +3. Verify that the browser window size is consistent +4. Look at test screenshots in `test-results/` directory for visual debugging diff --git a/tests/subscription-card-height.spec.ts b/tests/subscription-card-height.spec.ts new file mode 100644 index 0000000..f4afb1f --- /dev/null +++ b/tests/subscription-card-height.spec.ts @@ -0,0 +1,134 @@ +import { test, expect } from '@playwright/test' + +test.describe('Subscription Card Height Consistency', () => { + test.beforeEach(async ({ page }) => { + await page.goto('/') + // Wait for the page to load + await page.waitForSelector('[data-testid="subscription-card"]', { timeout: 10000 }) + }) + + test('all subscription cards should have the same height', async ({ page }) => { + // Get all subscription cards + const cards = await page.locator('[data-testid="subscription-card"]').all() + + // Ensure we have at least 2 cards to compare + expect(cards.length).toBeGreaterThanOrEqual(2) + + // Get heights of all cards + const heights = await Promise.all( + cards.map(async (card) => { + const box = await card.boundingBox() + return box?.height || 0 + }) + ) + + // All heights should be the same (within 1px tolerance for rounding) + const firstHeight = heights[0] + for (let i = 1; i < heights.length; i++) { + expect(Math.abs(heights[i] - firstHeight)).toBeLessThanOrEqual(1) + } + }) + + test('card height should remain consistent after adding monthly billing', async ({ page }) => { + // Get initial card heights + const cardsBefore = await page.locator('[data-testid="subscription-card"]').all() + const heightsBefore = await Promise.all( + cardsBefore.map(async (card) => { + const box = await card.boundingBox() + return box?.height || 0 + }) + ) + + // Click edit on the first subscription + await page.locator('button:has-text("Edit")').first().click() + + // Wait for modal to open + await page.waitForSelector('text=Edit Subscription', { timeout: 5000 }) + + // Select monthly billing cycle + await page.locator('[id="billingCycle"]').click() + await page.locator('text=Monthly').click() + + // Enable next payment date + await page.locator('switch:has-text("Show next payment date")').click() + + // Save the subscription + await page.locator('button:has-text("Save")').click() + + // Wait for modal to close and page to update + await page.waitForTimeout(1000) + + // Get card heights after adding billing info + const cardsAfter = await page.locator('[data-testid="subscription-card"]').all() + const heightsAfter = await Promise.all( + cardsAfter.map(async (card) => { + const box = await card.boundingBox() + return box?.height || 0 + }) + ) + + // All cards should still have the same height + const firstHeightAfter = heightsAfter[0] + for (let i = 1; i < heightsAfter.length; i++) { + expect(Math.abs(heightsAfter[i] - firstHeightAfter)).toBeLessThanOrEqual(1) + } + + // Heights should be consistent before and after + expect(Math.abs(heightsAfter[0] - heightsBefore[0])).toBeLessThanOrEqual(1) + }) + + test('cards should have a square aspect ratio (approximately 1:1)', async ({ page }) => { + // Get all subscription cards + const cards = await page.locator('[data-testid="subscription-card"]').all() + + // Check aspect ratio for each card + for (const card of cards) { + const box = await card.boundingBox() + if (box) { + const aspectRatio = box.width / box.height + // Aspect ratio should be close to 1 (square), allowing 10% tolerance + expect(aspectRatio).toBeGreaterThan(0.9) + expect(aspectRatio).toBeLessThan(1.1) + } + } + }) + + test('card with billing cycle badge should not be taller than others', async ({ page }) => { + // Add a new subscription with monthly billing + await page.locator('button:has-text("Add Subscription")').click() + await page.waitForSelector('text=Add Subscription', { timeout: 5000 }) + + // Fill in subscription details + await page.locator('[id="name"]').fill('Test Subscription') + await page.locator('[id="price"]').fill('9.99') + await page.locator('[id="domain"]').fill('https://test.com') + + // Select monthly billing + await page.locator('[id="billingCycle"]').click() + await page.locator('text=Monthly').last().click() + + // Enable next payment + await page.locator('switch:has-text("Show next payment date")').click() + + // Save + await page.locator('button:has-text("Save")').click() + + // Wait for page to update + await page.waitForTimeout(1000) + + // Get all card heights + const cards = await page.locator('[data-testid="subscription-card"]').all() + const heights = await Promise.all( + cards.map(async (card) => { + const box = await card.boundingBox() + return box?.height || 0 + }) + ) + + // All heights should be the same + const firstHeight = heights[0] + for (let i = 1; i < heights.length; i++) { + expect(Math.abs(heights[i] - firstHeight)).toBeLessThanOrEqual(1) + } + }) +}) From 2fbb10d4c1e71baf9d78ef61015ca7a122c40f60 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 18 Dec 2025 14:40:42 +0000 Subject: [PATCH 07/13] Add max-height constraint to prevent card resizing Co-authored-by: ajnart <49837342+ajnart@users.noreply.github.com> --- app/components/SubscriptionCard.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/components/SubscriptionCard.tsx b/app/components/SubscriptionCard.tsx index 1277e6e..e3d231d 100644 --- a/app/components/SubscriptionCard.tsx +++ b/app/components/SubscriptionCard.tsx @@ -63,7 +63,7 @@ const SubscriptionCard: React.FC = ({ subscription, onEdi transition={{ type: 'spring', stiffness: 300, damping: 20 }} className={`group ${className}`} > - +
- - {`${name} -

- {name} -

-

{`${currency} ${price}`}

- {billingCycle && ( - - per{' '} - {billingCycle === 'monthly' - ? 'Monthly' - : billingCycle === 'yearly' - ? 'Yearly' - : billingCycle === 'weekly' - ? 'Weekly' - : 'Daily'} - - )} - {nextPaymentDisplay && ( -
- - Next Payment: {nextPaymentDisplay} -
- )} + +
+ {`${name} +
+
+

+ {name} +

+

{`${currency} ${price}`}

+ {billingCycle && ( + + per{' '} + {billingCycle === 'monthly' + ? 'Monthly' + : billingCycle === 'yearly' + ? 'Yearly' + : billingCycle === 'weekly' + ? 'Weekly' + : 'Daily'} + + )} + {nextPaymentDisplay && ( +
+ + Next Payment: {nextPaymentDisplay} +
+ )} +
From 543fd610a1c6aa040cef99ca44f33df23119cc52 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 18 Dec 2025 15:15:54 +0000 Subject: [PATCH 09/13] Reduce spacing by removing pt-6 padding on logo wrapper Co-authored-by: ajnart <49837342+ajnart@users.noreply.github.com> --- app/components/SubscriptionCard.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/components/SubscriptionCard.tsx b/app/components/SubscriptionCard.tsx index c82d5d1..eeb524b 100644 --- a/app/components/SubscriptionCard.tsx +++ b/app/components/SubscriptionCard.tsx @@ -81,7 +81,7 @@ const SubscriptionCard: React.FC = ({ subscription, onEdi

-
+
{`${name}
From 9369545a25d7653a1f153094e42fe5697488f948 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 18 Dec 2025 16:03:00 +0000 Subject: [PATCH 10/13] Revert to justify-center layout for proper logo alignment Co-authored-by: ajnart <49837342+ajnart@users.noreply.github.com> --- app/components/SubscriptionCard.tsx | 52 +++++++++++++---------------- 1 file changed, 24 insertions(+), 28 deletions(-) diff --git a/app/components/SubscriptionCard.tsx b/app/components/SubscriptionCard.tsx index eeb524b..e3d231d 100644 --- a/app/components/SubscriptionCard.tsx +++ b/app/components/SubscriptionCard.tsx @@ -80,34 +80,30 @@ const SubscriptionCard: React.FC = ({ subscription, onEdi
- -
- {`${name} -
-
-

- {name} -

-

{`${currency} ${price}`}

- {billingCycle && ( - - per{' '} - {billingCycle === 'monthly' - ? 'Monthly' - : billingCycle === 'yearly' - ? 'Yearly' - : billingCycle === 'weekly' - ? 'Weekly' - : 'Daily'} - - )} - {nextPaymentDisplay && ( -
- - Next Payment: {nextPaymentDisplay} -
- )} -
+ + {`${name} +

+ {name} +

+

{`${currency} ${price}`}

+ {billingCycle && ( + + per{' '} + {billingCycle === 'monthly' + ? 'Monthly' + : billingCycle === 'yearly' + ? 'Yearly' + : billingCycle === 'weekly' + ? 'Weekly' + : 'Daily'} + + )} + {nextPaymentDisplay && ( +
+ + Next Payment: {nextPaymentDisplay} +
+ )}
From 043b6b34002a7199d6762a4e2337f6750ce122bb Mon Sep 17 00:00:00 2001 From: Thomas Camlong Date: Thu, 18 Dec 2025 20:45:33 +0100 Subject: [PATCH 11/13] Fix Card component max height in SubscriptionCard --- app/components/SubscriptionCard.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/components/SubscriptionCard.tsx b/app/components/SubscriptionCard.tsx index e3d231d..2c2c96b 100644 --- a/app/components/SubscriptionCard.tsx +++ b/app/components/SubscriptionCard.tsx @@ -63,7 +63,7 @@ const SubscriptionCard: React.FC = ({ subscription, onEdi transition={{ type: 'spring', stiffness: 300, damping: 20 }} className={`group ${className}`} > - +
+ {`${name} @@ -86,24 +110,6 @@ const SubscriptionCard: React.FC = ({ subscription, onEdi {name}

{`${currency} ${price}`}

- {billingCycle && ( - - per{' '} - {billingCycle === 'monthly' - ? 'Monthly' - : billingCycle === 'yearly' - ? 'Yearly' - : billingCycle === 'weekly' - ? 'Weekly' - : 'Daily'} - - )} - {nextPaymentDisplay && ( -
- - Next Payment: {nextPaymentDisplay} -
- )}
From b5b0aae4e3462ca44cce437777510e65987daec5 Mon Sep 17 00:00:00 2001 From: Thomas Camlong Date: Tue, 23 Dec 2025 11:37:26 +0100 Subject: [PATCH 13/13] Update SubscriptionCard.tsx --- app/components/SubscriptionCard.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/components/SubscriptionCard.tsx b/app/components/SubscriptionCard.tsx index 834bca5..42c8db9 100644 --- a/app/components/SubscriptionCard.tsx +++ b/app/components/SubscriptionCard.tsx @@ -63,7 +63,7 @@ const SubscriptionCard: React.FC = ({ subscription, onEdi transition={{ type: 'spring', stiffness: 300, damping: 20 }} className={`group ${className}`} > - + {/* Billing Cycle Badge - Top Left */} {billingCycle && (