Skip to content

Commit acbe687

Browse files
Dev (#24)
* feat(web): implement theming system with CSS variables Replace hardcoded colors and styles in dashboard components and layout with CSS custom properties for dynamic theming support. Introduce ThemeToggle component for user-controlled theme switching. * refactor(theme): standardize theming with CSS variables and update defaults Replace remaining hardcoded color values in components and styles with CSS custom properties for full theming support. Switch default theme to dark mode, update light mode color palette for improved contrast and consistency, and refine theme resolution logic in ThemeProvider. This enhances maintainability and ensures uniform application of themes across layouts and UI elements. * feat(ui): implement portal rendering for modals and enhance color theming Add Portal component for improved modal rendering, refactor CreateExpenseModal and CreateGroupModal to use portals with updated z-indexes and backgrounds. Introduce cardSolid color variable for solid backgrounds in themes. Update dashboard to dynamically use group currency and label multi-group balances. BREAKING CHANGE: Modal z-indexes adjusted for layering consistency. * feat(groups): add soft delete functionality for groups Introduce soft delete for groups in the database schema and backend service, allowing group owners to delete groups via a new API endpoint. Update frontend to include a delete group modal in group actions, with proper confirmation and action handling. Modify queries to exclude deleted groups from user summaries and dashboards. BREAKING CHANGE: Group deletion now permanently removes group data from active views and balances. * ci: add e2e tests to workflow Add database services startup, migration, and Playwright installation to CI pipeline. Configure Playwright for api and web test projects with web server setup. Add invalidateUserDashboardCache method in RedisService. Enhance getDashboard in GroupsService to fetch user groups. Add dashboard.spec.ts e2e test file and test-results directory. * build(ci): reorder workflow steps and update turbo dependencies - Moved build step before lint and test in CI pipeline - Changed Prisma migration to db push for faster database setup - Added build dependency to lint task in turbo.json for proper execution order * fix: PR review - restore theme switching, guard soft delete, fix modal animations, cleanup - ThemeProvider: restore data-theme application to document elements - layout.tsx: restore theme init script to prevent FOUC - groups.service.ts: add existence and already-deleted checks in delete method - CreateExpenseModal: restore AnimatePresence for exit animations - ThemeToggle: remove unused imports and variables - .gitignore: add test-results/ and remove committed artifact Agent-Logs-Url: https://github.com/Arun-kushwaha007/FairShare/sessions/3067ae7f-0172-4370-8ac6-04de20b46b7c Co-authored-by: Arun-kushwaha007 <121769639+Arun-kushwaha007@users.noreply.github.com> * chore: remove accidentally committed build artifacts from packages/shared-types/src Agent-Logs-Url: https://github.com/Arun-kushwaha007/FairShare/sessions/3067ae7f-0172-4370-8ac6-04de20b46b7c Co-authored-by: Arun-kushwaha007 <121769639+Arun-kushwaha007@users.noreply.github.com> * test(backend): update receipts and settlements test mocks and expectations - Reorder constructor parameters in ReceiptsService tests to match implementation changes - Add groupId select to expense query expectation in receipts service test - Include findUnique mock for settlement in integration test setup * refactor(theme): simplify theme provider and remove inline initialization script - Remove theme initialization script from layout.tsx - Refactor ThemeProvider to only resolve themes without applying DOM changes - Hardcode dark background and text colors in globals.css - Enhance ThemeToggle with framer-motion animations and resolved theme usage * test(mobile): update login screen test to expect 'sign in' text and clean import - Remove BOM from React import - Change test assertion to check for 'Sign In' instead of 'Login' * ci(config): add supabase database url and direct url environment variables - Updated .env.example and added apps/backend/.env.example with new variables - Modified CI workflow to include SUPABASE_DATABASE_URL and DIRECT_URL for database migration and testing jobs * ci(e2e): expand environment variables for end-to-end testing - Added JWT_REFRESH_SECRET, GOOGLE_CLIENT_ID, GOOGLE_CLIENT_SECRET, REDIS_URL, AWS_REGION, and S3_BUCKET - Updated STRIPE_SECRET_KEY to use test key format for consistency in CI pipeline * ci(e2e): include supabase environment variables in testing pipeline - Added NEXT_PUBLIC_SUPABASE_URL, NEXT_PUBLIC_SUPABASE_ANON_KEY, and NEXT_PUBLIC_API_URL to CI e2e job - Updated .env.example with NEXT_PUBLIC_SUPABASE_URL and NEXT_PUBLIC_SUPABASE_ANON_KEY placeholders for local development consistency --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
1 parent 3766ba5 commit acbe687

41 files changed

Lines changed: 688 additions & 255 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.env.example

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
SUPABASE_DATABASE_URL=
2+
DIRECT_URL=
23
JWT_SECRET=
34
JWT_REFRESH_SECRET=
45
GOOGLE_CLIENT_ID=

.github/workflows/ci.yml

Lines changed: 37 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -50,14 +50,49 @@ jobs:
5050
- name: Generate Prisma client
5151
run: pnpm --filter backend prisma:generate
5252

53+
- name: Build
54+
run: pnpm build
55+
5356
- name: Lint
5457
run: pnpm lint
5558

5659
- name: Test
5760
run: pnpm test
5861

59-
- name: Build
60-
run: pnpm build
62+
- name: Start Database Services
63+
run: docker compose up -d
64+
65+
- name: Migrate Database
66+
env:
67+
DATABASE_URL: postgresql://postgres:postgres@localhost:5432/fairshare?schema=public
68+
SUPABASE_DATABASE_URL: postgresql://postgres:postgres@localhost:5432/fairshare?schema=public
69+
DIRECT_URL: postgresql://postgres:postgres@localhost:5432/fairshare?schema=public
70+
JWT_SECRET: supersecret
71+
STRIPE_SECRET_KEY: test_key
72+
run: pnpm --filter backend exec prisma db push
73+
74+
- name: Install Playwright Browsers
75+
run: pnpm exec playwright install --with-deps
76+
77+
- name: Run E2E Tests
78+
env:
79+
CI: true
80+
DATABASE_URL: postgresql://postgres:postgres@localhost:5432/fairshare?schema=public
81+
SUPABASE_DATABASE_URL: postgresql://postgres:postgres@localhost:5432/fairshare?schema=public
82+
DIRECT_URL: postgresql://postgres:postgres@localhost:5432/fairshare?schema=public
83+
JWT_SECRET: supersecret
84+
JWT_REFRESH_SECRET: refreshsecret
85+
GOOGLE_CLIENT_ID: googleid
86+
GOOGLE_CLIENT_SECRET: googlesecret
87+
REDIS_URL: redis://localhost:6380
88+
AWS_REGION: ap-south-1
89+
S3_BUCKET: fairshare-test-bucket
90+
STRIPE_SECRET_KEY: sk_test_key
91+
NEXT_PUBLIC_APP_URL: http://localhost:3000
92+
NEXT_PUBLIC_SUPABASE_URL: http://localhost:54321
93+
NEXT_PUBLIC_SUPABASE_ANON_KEY: dummy-anon-key
94+
NEXT_PUBLIC_API_URL: http://localhost:3001/api/v1
95+
run: pnpm run e2e
6196

6297
- name: Upload coverage
6398
if: always()

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ node_modules
77
dist
88
.next
99
coverage
10+
test-results
1011
.agents
1112
.agent
1213
.turbo

apps/backend/.env.example

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
SUPABASE_DATABASE_URL=postgresql://postgres:postgres@localhost:5432/fairshare
2+
DIRECT_URL=postgresql://postgres:postgres@localhost:5432/fairshare
3+
JWT_SECRET=fairshare_dev_jwt_secret_change_me
4+
JWT_REFRESH_SECRET=fairshare_dev_jwt_refresh_secret_change_me
5+
GOOGLE_CLIENT_ID=fairshare_dev_google_client_id
6+
GOOGLE_CLIENT_SECRET=fairshare_dev_google_client_secret
7+
REDIS_URL=redis://127.0.0.1:6380
8+
AWS_ACCESS_KEY_ID=fairshare_dev_aws_access_key
9+
AWS_SECRET_ACCESS_KEY=fairshare_dev_aws_secret
10+
AWS_REGION=ap-south-1
11+
S3_BUCKET=fairshare-dev-receipts
12+
STRIPE_SECRET_KEY=sk_test_fairshare_dummy_key
13+
STRIPE_WEBHOOK_SECRET=whsec_fairshare_dummy_webhook_secret
14+
CORS_ORIGINS=http://localhost:3000,http://localhost:8081,exp://*:*
15+
SENTRY_DSN=

apps/backend/prisma/schema.prisma

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,7 @@ model Group {
6767
shareToken String? @unique
6868
shareEnabled Boolean @default(false)
6969
createdAt DateTime @default(now())
70+
deletedAt DateTime?
7071
creator User @relation("group_creator", fields: [createdBy], references: [id])
7172
members GroupMember[]
7273
expenses Expense[]

apps/backend/src/groups/groups.controller.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { Body, Controller, Get, Param, Patch, Post, UseGuards } from '@nestjs/common';
1+
import { Body, Controller, Delete, Get, Param, Patch, Post, UseGuards } from '@nestjs/common';
22
import { Throttle } from '@nestjs/throttler';
33
import { JwtAuthGuard } from '../auth/guards/jwt-auth.guard';
44
import { CurrentUser } from '../common/decorators/current-user.decorator';
@@ -82,4 +82,9 @@ export class GroupsController {
8282
) {
8383
return this.groupsService.toggleShare(id, user.sub, enabled);
8484
}
85+
86+
@Delete(':id')
87+
remove(@Param('id') id: string, @CurrentUser() user: JwtPayload) {
88+
return this.groupsService.delete(id, user.sub);
89+
}
8590
}

apps/backend/src/groups/groups.service.ts

Lines changed: 48 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,7 @@ export class GroupsService {
8383
userId,
8484
},
8585
},
86+
deletedAt: null,
8687
},
8788
orderBy: { createdAt: 'desc' },
8889
});
@@ -235,7 +236,10 @@ export class GroupsService {
235236

236237
async getUserSummary(userId: string): Promise<{ totalBalanceCents: string }> {
237238
const balances = await this.prisma.balance.findMany({
238-
where: { userId },
239+
where: {
240+
userId,
241+
group: { deletedAt: null }
242+
},
239243
select: { amountCents: true },
240244
});
241245

@@ -254,6 +258,7 @@ export class GroupsService {
254258
userId,
255259
},
256260
},
261+
deletedAt: null,
257262
},
258263
include: {
259264
_count: {
@@ -670,6 +675,48 @@ export class GroupsService {
670675
};
671676
}
672677

678+
async delete(groupId: string, actorUserId: string): Promise<{ success: true }> {
679+
const group = await this.prisma.group.findUnique({
680+
where: { id: groupId },
681+
select: { id: true, deletedAt: true },
682+
});
683+
684+
if (!group) {
685+
throw new NotFoundException('Group not found');
686+
}
687+
688+
if (group.deletedAt) {
689+
throw new NotFoundException('Group has already been deleted');
690+
}
691+
692+
const membership = await this.prisma.groupMember.findUnique({
693+
where: {
694+
groupId_userId: {
695+
groupId,
696+
userId: actorUserId,
697+
},
698+
},
699+
});
700+
701+
if (!membership || membership.role !== 'OWNER') {
702+
throw new ForbiddenException('Only the group owner can delete the group');
703+
}
704+
705+
await this.prisma.group.update({
706+
where: { id: groupId },
707+
data: {
708+
deletedAt: new Date(),
709+
shareEnabled: false,
710+
shareToken: null
711+
},
712+
});
713+
714+
await this.redis.invalidateGroupCache(groupId);
715+
await this.redis.invalidateUserDashboardCache(actorUserId);
716+
717+
return { success: true };
718+
}
719+
673720
async resolvePendingInvites(userId: string, email: string): Promise<void> {
674721
const invites = await this.prisma.groupInvite.findMany({
675722
where: { email: email.toLowerCase() },

apps/backend/src/receipts/receipts.service.spec.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ describe('ReceiptsService', () => {
1717
const jobsQueue: any = {
1818
enqueueReceiptProcessing: jest.fn().mockResolvedValue(undefined),
1919
};
20-
const service = new ReceiptsService(prisma, s3, jobsQueue);
20+
const service = new ReceiptsService(prisma, jobsQueue, s3);
2121

2222
await service.createUploadUrl('expense-1', { extension: 'jpg' });
2323

@@ -42,11 +42,11 @@ describe('ReceiptsService', () => {
4242
const jobsQueue: any = {
4343
enqueueReceiptProcessing: jest.fn().mockResolvedValue(undefined),
4444
};
45-
const service = new ReceiptsService(prisma, s3, jobsQueue);
45+
const service = new ReceiptsService(prisma, jobsQueue, s3);
4646

4747
const result = await service.createUploadUrl('expense-1', { extension: 'png' });
4848

49-
expect(prisma.expense.findUnique).toHaveBeenCalledWith({ where: { id: 'expense-1' } });
49+
expect(prisma.expense.findUnique).toHaveBeenCalledWith({ where: { id: 'expense-1' }, select: { groupId: true } });
5050
expect(prisma.receipt.upsert).toHaveBeenCalledWith({
5151
where: { expenseId: 'expense-1' },
5252
update: {
@@ -83,7 +83,7 @@ describe('ReceiptsService', () => {
8383
const jobsQueue: any = {
8484
enqueueReceiptProcessing: jest.fn(),
8585
};
86-
const service = new ReceiptsService(prisma, s3, jobsQueue);
86+
const service = new ReceiptsService(prisma, jobsQueue, s3);
8787

8888
await expect(service.createUploadUrl('missing-expense', {})).rejects.toBeInstanceOf(NotFoundException);
8989
expect(s3.getPresignedUploadUrl).not.toHaveBeenCalled();

apps/backend/src/redis/redis.service.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,14 @@ export class RedisService {
6161
}
6262
}
6363

64+
async invalidateUserDashboardCache(userId: string): Promise<void> {
65+
try {
66+
await this.redis.del(`user:${userId}:dashboard`);
67+
} catch (error) {
68+
this.logger.warn(`Redis invalidate skipped: ${error instanceof Error ? error.message : 'unknown error'}`);
69+
}
70+
}
71+
6472
private async safeGet(key: string): Promise<string | null> {
6573
try {
6674
return await this.redis.get(key);

apps/backend/src/settlements/settlements.integration.spec.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ describe('Settlement Flow (integration-ish)', () => {
2626
const prisma: any = {
2727
settlement: {
2828
findFirst: jest.fn().mockResolvedValue(null),
29+
findUnique: jest.fn().mockResolvedValue(null),
2930
},
3031
groupMember: {
3132
findMany: jest.fn().mockResolvedValue([{ userId: 'u1' }, { userId: 'u2' }]),

0 commit comments

Comments
 (0)