diff --git a/.github/workflows/ci-cd.yml b/.github/workflows/ci-cd.yml index b5c64b75..1a380853 100644 --- a/.github/workflows/ci-cd.yml +++ b/.github/workflows/ci-cd.yml @@ -9,17 +9,13 @@ jobs: runs-on: ubuntu-latest steps: - name: Validate PR title - uses: amannn/action-semantic-pull-request@v5 - with: - types: | - feat - fix - docs - chore - test - refactor - ci - requireScope: false + run: | + TITLE="${{ github.event.pull_request.title }}" + if ! echo "$TITLE" | grep -qE '^(feat|fix|docs|chore|test|refactor|ci)(\(.+\))?!?: .+'; then + echo "āŒ PR title must follow Conventional Commits: type(scope)!: description" + echo "Allowed types: feat, fix, docs, chore, test, refactor, ci" + exit 1 + fi - name: Validate PR description run: | diff --git a/backend/.gitignore b/backend/.gitignore index 50de04c9..493d0679 100644 --- a/backend/.gitignore +++ b/backend/.gitignore @@ -55,3 +55,5 @@ pids # Diagnostic reports (https://nodejs.org/api/report.html) report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json +*.tsbuildinfo +dist/ diff --git a/backend/diff.txt b/backend/diff.txt new file mode 100644 index 00000000..5452e548 --- /dev/null +++ b/backend/diff.txt @@ -0,0 +1,14636 @@ +diff --git a/backend/src/analytics/analytics.controller.ts b/backend/src/analytics/analytics.controller.ts +deleted file mode 100644 +index 71fcc3c..0000000 +--- a/backend/src/analytics/analytics.controller.ts ++++ /dev/null +@@ -1,20 +0,0 @@ +-import { Body,Controller, Post, UseGuards, ValidationPipe,} from '@nestjs/common'; +-import { AnalyticsQueryDto } from './dtos/analytics-query.dto'; +-import { TrackEventProvider } from './providers/track-event.provider'; +-import { AnalyticsAdminGuard } from './guards/analytics-admin.guard'; +- +-@Controller('analytics') +-export class AnalyticsController { +- constructor( +- private readonly trackEventProvider: TrackEventProvider, +- ) {} +- +- @Post('events/track') +- @UseGuards(AnalyticsAdminGuard) +- async trackEvent( +- @Body(new ValidationPipe()) +- body: AnalyticsQueryDto, +- ) { +- return this.trackEventProvider.trackEvent(body); +- } +-} +\ No newline at end of file +diff --git a/backend/src/analytics/analytics.module.ts b/backend/src/analytics/analytics.module.ts +index 81b46df..cc98c46 100644 +--- a/backend/src/analytics/analytics.module.ts ++++ b/backend/src/analytics/analytics.module.ts +@@ -19,6 +19,12 @@ import { GetRetentionCurveProvider } from './providers/get-retention-curve.provi + GetOnboardingFunnelProvider, + GetRetentionCurveProvider, + ], +- exports: [AnalyticsService, TrackEventProvider, GetOnboardingFunnelProvider, GetRetentionCurveProvider, TypeOrmModule], ++ exports: [ ++ AnalyticsService, ++ TrackEventProvider, ++ GetOnboardingFunnelProvider, ++ GetRetentionCurveProvider, ++ TypeOrmModule, ++ ], + }) +-export class AnalyticsModule {} +\ No newline at end of file ++export class AnalyticsModule {} +diff --git a/backend/src/analytics/dtos/analytics-metric-result.dto.ts b/backend/src/analytics/dtos/analytics-metric-result.dto.ts +index 2fec7b7..7893ac1 100644 +--- a/backend/src/analytics/dtos/analytics-metric-result.dto.ts ++++ b/backend/src/analytics/dtos/analytics-metric-result.dto.ts +@@ -1,19 +1,34 @@ + import { ApiProperty } from '@nestjs/swagger'; + + export class RetentionDataPoint { +- @ApiProperty({ example: '2024-01-15', description: 'Cohort date (YYYY-MM-DD)' }) ++ @ApiProperty({ ++ example: '2024-01-15', ++ description: 'Cohort date (YYYY-MM-DD)', ++ }) + cohortDate: string; + + @ApiProperty({ example: 200, description: 'Total users in this cohort' }) + cohortSize: number; + +- @ApiProperty({ example: 65.5, description: 'Day-1 retention %', nullable: true }) ++ @ApiProperty({ ++ example: 65.5, ++ description: 'Day-1 retention %', ++ nullable: true, ++ }) + day1RetentionPct: number | null; + +- @ApiProperty({ example: 42.0, description: 'Day-7 retention %', nullable: true }) ++ @ApiProperty({ ++ example: 42.0, ++ description: 'Day-7 retention %', ++ nullable: true, ++ }) + day7RetentionPct: number | null; + +- @ApiProperty({ example: 28.5, description: 'Day-30 retention %', nullable: true }) ++ @ApiProperty({ ++ example: 28.5, ++ description: 'Day-30 retention %', ++ nullable: true, ++ }) + day30RetentionPct: number | null; + } + +@@ -32,4 +47,4 @@ export class AnalyticsMetricResult { + + @ApiProperty({ example: 15, description: 'Total cohort rows returned' }) + total: number; +-} +\ No newline at end of file ++} +diff --git a/backend/src/analytics/dtos/analytics-query.dto.ts b/backend/src/analytics/dtos/analytics-query.dto.ts +index 575cfd1..50ad031 100644 +--- a/backend/src/analytics/dtos/analytics-query.dto.ts ++++ b/backend/src/analytics/dtos/analytics-query.dto.ts +@@ -12,4 +12,4 @@ export class AnalyticsQueryDto { + @IsOptional() + @IsObject() + metadata?: Record; +-} +\ No newline at end of file ++} +diff --git a/backend/src/analytics/dtos/date-range.dto.ts b/backend/src/analytics/dtos/date-range.dto.ts +index 31c3241..e6d9728 100644 +--- a/backend/src/analytics/dtos/date-range.dto.ts ++++ b/backend/src/analytics/dtos/date-range.dto.ts +@@ -36,4 +36,4 @@ export class DateRangeDto { + @IsOptional() + @IsIn(['day', 'week', 'month']) + granularity?: RetentionGranularity; +-} +\ No newline at end of file ++} +diff --git a/backend/src/analytics/dtos/track-event.dto.ts b/backend/src/analytics/dtos/track-event.dto.ts +index 66d5682..8e06b4e 100644 +--- a/backend/src/analytics/dtos/track-event.dto.ts ++++ b/backend/src/analytics/dtos/track-event.dto.ts +@@ -7,7 +7,7 @@ export class TrackEventDto { + example: 'puzzle_attempted', + }) + @IsString() +- eventType: string; ++ eventType?: string; + + @ApiPropertyOptional({ + description: 'Arbitrary payload for the event', +@@ -18,7 +18,8 @@ export class TrackEventDto { + payload?: Record; + + @ApiPropertyOptional({ +- description: 'User identifier if the event is tied to an authenticated user', ++ description: ++ 'User identifier if the event is tied to an authenticated user', + example: '123e4567-e89b-12d3-a456-426614174000', + }) + @IsUUID() +diff --git a/backend/src/analytics/entities/analytics-event.entity.ts b/backend/src/analytics/entities/analytics-event.entity.ts +index aefa39a..291e965 100644 +--- a/backend/src/analytics/entities/analytics-event.entity.ts ++++ b/backend/src/analytics/entities/analytics-event.entity.ts +@@ -1,4 +1,9 @@ +-import { Column, CreateDateColumn, Entity, PrimaryGeneratedColumn } from 'typeorm'; ++import { ++ Column, ++ CreateDateColumn, ++ Entity, ++ PrimaryGeneratedColumn, ++} from 'typeorm'; + + @Entity('analytics_events') + export class AnalyticsEvent { +diff --git a/backend/src/analytics/entities/retention-cohort.entity.ts b/backend/src/analytics/entities/retention-cohort.entity.ts +index 5905f30..ff815bf 100644 +--- a/backend/src/analytics/entities/retention-cohort.entity.ts ++++ b/backend/src/analytics/entities/retention-cohort.entity.ts +@@ -42,4 +42,4 @@ export class RetentionCohort { + + @UpdateDateColumn({ type: 'timestamptz' }) + updatedAt: Date; +-} +\ No newline at end of file ++} +diff --git a/backend/src/analytics/listeners/users-analytics.listener.spec.ts b/backend/src/analytics/listeners/users-analytics.listener.spec.ts +index 7934a00..f15efd8 100644 +--- a/backend/src/analytics/listeners/users-analytics.listener.spec.ts ++++ b/backend/src/analytics/listeners/users-analytics.listener.spec.ts +@@ -6,7 +6,9 @@ import { AnalyticsEvent } from '../entities/analytics-event.entity'; + + describe('UsersAnalyticsListener', () => { + let listener: UsersAnalyticsListener; +- let analyticsRepository: Partial, jest.Mock>>; ++ let analyticsRepository: Partial< ++ Record, jest.Mock> ++ >; + + beforeEach(async () => { + analyticsRepository = { +@@ -17,7 +19,10 @@ describe('UsersAnalyticsListener', () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [ + UsersAnalyticsListener, +- { provide: getRepositoryToken(AnalyticsEvent), useValue: analyticsRepository }, ++ { ++ provide: getRepositoryToken(AnalyticsEvent), ++ useValue: analyticsRepository, ++ }, + ], + }).compile(); + +diff --git a/backend/src/analytics/providers/get-onboarding-funnel.provider.spec.ts b/backend/src/analytics/providers/get-onboarding-funnel.provider.spec.ts +index 5642a79..573858b 100644 +--- a/backend/src/analytics/providers/get-onboarding-funnel.provider.spec.ts ++++ b/backend/src/analytics/providers/get-onboarding-funnel.provider.spec.ts +@@ -21,7 +21,9 @@ describe('GetOnboardingFunnelProvider', () => { + ], + }).compile(); + +- provider = module.get(GetOnboardingFunnelProvider); ++ provider = module.get( ++ GetOnboardingFunnelProvider, ++ ); + }); + + afterEach(() => { +diff --git a/backend/src/analytics/providers/get-onboarding-funnel.provider.ts b/backend/src/analytics/providers/get-onboarding-funnel.provider.ts +index 05ac90c..d1dfb3a 100644 +--- a/backend/src/analytics/providers/get-onboarding-funnel.provider.ts ++++ b/backend/src/analytics/providers/get-onboarding-funnel.provider.ts +@@ -2,7 +2,10 @@ import { Injectable } from '@nestjs/common'; + import { InjectRepository } from '@nestjs/typeorm'; + import { Repository, Between } from 'typeorm'; + import { AnalyticsEvent } from '../entities/analytics-event.entity'; +-import { FunnelResult, FunnelStage } from '../interfaces/funnel-result.interface'; ++import { ++ FunnelResult, ++ FunnelStage, ++} from '../interfaces/funnel-result.interface'; + + const ONBOARDING_EVENTS = [ + { name: 'Onboarding Started', eventType: 'onboarding_started' }, +diff --git a/backend/src/analytics/providers/get-retention-curve.provider.spec.ts b/backend/src/analytics/providers/get-retention-curve.provider.spec.ts +index 87e864b..3b7606b 100644 +--- a/backend/src/analytics/providers/get-retention-curve.provider.spec.ts ++++ b/backend/src/analytics/providers/get-retention-curve.provider.spec.ts +@@ -14,7 +14,10 @@ describe('GetRetentionCurveProvider', () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [ + GetRetentionCurveProvider, +- { provide: getRepositoryToken(RetentionCohort), useValue: mockRetentionCohortRepo }, ++ { ++ provide: getRepositoryToken(RetentionCohort), ++ useValue: mockRetentionCohortRepo, ++ }, + ], + }).compile(); + provider = module.get(GetRetentionCurveProvider); +@@ -81,9 +84,36 @@ describe('GetRetentionCurveProvider', () => { + _dateRange: true, + }; + const cohorts: Partial[] = [ +- { id: 'uuid-1', cohortDate: '2024-01-01', cohortSize: 200, retainedDay1: 130, retainedDay7: 100, retainedDay30: 60, createdAt: new Date(), updatedAt: new Date() }, +- { id: 'uuid-2', cohortDate: '2024-01-02', cohortSize: 150, retainedDay1: 90, retainedDay7: 75, retainedDay30: 45, createdAt: new Date(), updatedAt: new Date() }, +- { id: 'uuid-3', cohortDate: '2024-01-03', cohortSize: 180, retainedDay1: 126, retainedDay7: 90, retainedDay30: 54, createdAt: new Date(), updatedAt: new Date() }, ++ { ++ id: 'uuid-1', ++ cohortDate: '2024-01-01', ++ cohortSize: 200, ++ retainedDay1: 130, ++ retainedDay7: 100, ++ retainedDay30: 60, ++ createdAt: new Date(), ++ updatedAt: new Date(), ++ }, ++ { ++ id: 'uuid-2', ++ cohortDate: '2024-01-02', ++ cohortSize: 150, ++ retainedDay1: 90, ++ retainedDay7: 75, ++ retainedDay30: 45, ++ createdAt: new Date(), ++ updatedAt: new Date(), ++ }, ++ { ++ id: 'uuid-3', ++ cohortDate: '2024-01-03', ++ cohortSize: 180, ++ retainedDay1: 126, ++ retainedDay7: 90, ++ retainedDay30: 54, ++ createdAt: new Date(), ++ updatedAt: new Date(), ++ }, + ]; + mockRetentionCohortRepo.find.mockResolvedValue(cohorts); + +@@ -105,8 +135,14 @@ describe('GetRetentionCurveProvider', () => { + _dateRange: true, + }; + const cohort: Partial = { +- id: 'uuid-zero', cohortDate: '2024-01-10', cohortSize: 0, +- retainedDay1: 0, retainedDay7: 0, retainedDay30: 0, createdAt: new Date(), updatedAt: new Date(), ++ id: 'uuid-zero', ++ cohortDate: '2024-01-10', ++ cohortSize: 0, ++ retainedDay1: 0, ++ retainedDay7: 0, ++ retainedDay30: 0, ++ createdAt: new Date(), ++ updatedAt: new Date(), + }; + mockRetentionCohortRepo.find.mockResolvedValue([cohort]); + +@@ -118,7 +154,11 @@ describe('GetRetentionCurveProvider', () => { + }); + + it('should default granularity to "day" when not specified', async () => { +- const query: DateRangeDto = { start: new Date('2024-02-01'), end: new Date('2024-02-05'), _dateRange: true }; ++ const query: DateRangeDto = { ++ start: new Date('2024-02-01'), ++ end: new Date('2024-02-05'), ++ _dateRange: true, ++ }; + mockRetentionCohortRepo.find.mockResolvedValue([]); + + const result = await provider.getRetentionCurve(query); +@@ -127,7 +167,11 @@ describe('GetRetentionCurveProvider', () => { + }); + + it('should query using Between on cohortDate (index-backed)', async () => { +- const query: DateRangeDto = { start: new Date('2024-03-01'), end: new Date('2024-03-31'), _dateRange: true }; ++ const query: DateRangeDto = { ++ start: new Date('2024-03-01'), ++ end: new Date('2024-03-31'), ++ _dateRange: true, ++ }; + mockRetentionCohortRepo.find.mockResolvedValue([]); + + await provider.getRetentionCurve(query); +@@ -141,11 +185,20 @@ describe('GetRetentionCurveProvider', () => { + }); + + it('should round retention percentages to two decimal places', async () => { +- const query: DateRangeDto = { start: new Date('2024-04-01'), end: new Date('2024-04-01'), _dateRange: true }; ++ const query: DateRangeDto = { ++ start: new Date('2024-04-01'), ++ end: new Date('2024-04-01'), ++ _dateRange: true, ++ }; + const cohort: Partial = { +- id: 'uuid-frac', cohortDate: '2024-04-01', cohortSize: 300, +- retainedDay1: 199, retainedDay7: 151, retainedDay30: 99, +- createdAt: new Date(), updatedAt: new Date(), ++ id: 'uuid-frac', ++ cohortDate: '2024-04-01', ++ cohortSize: 300, ++ retainedDay1: 199, ++ retainedDay7: 151, ++ retainedDay30: 99, ++ createdAt: new Date(), ++ updatedAt: new Date(), + }; + mockRetentionCohortRepo.find.mockResolvedValue([cohort]); + +@@ -156,4 +209,4 @@ describe('GetRetentionCurveProvider', () => { + expect(result.data[0].day30RetentionPct).toBe(33); + }); + }); +-}); +\ No newline at end of file ++}); +diff --git a/backend/src/analytics/providers/get-retention-curve.provider.ts b/backend/src/analytics/providers/get-retention-curve.provider.ts +index 7160831..df73a42 100644 +--- a/backend/src/analytics/providers/get-retention-curve.provider.ts ++++ b/backend/src/analytics/providers/get-retention-curve.provider.ts +@@ -22,9 +22,7 @@ export class GetRetentionCurveProvider { + private readonly retentionCohortRepo: Repository, + ) {} + +- async getRetentionCurve( +- query: DateRangeDto, +- ): Promise { ++ async getRetentionCurve(query: DateRangeDto): Promise { + const { start, end, granularity = 'day' } = query; + + // Convert Date objects to YYYY-MM-DD strings for cohortDate comparisons +@@ -71,4 +69,4 @@ export class GetRetentionCurveProvider { + day30RetentionPct: pct(retainedDay30), + }; + } +-} +\ No newline at end of file ++} +diff --git a/backend/src/analytics/providers/track-event.provider.ts b/backend/src/analytics/providers/track-event.provider.ts +index a1e7dd1..8d67041 100644 +--- a/backend/src/analytics/providers/track-event.provider.ts ++++ b/backend/src/analytics/providers/track-event.provider.ts +@@ -1,49 +1,30 @@ +-import { Injectable, OnModuleDestroy } from '@nestjs/common'; ++import { Injectable } from '@nestjs/common'; + import { InjectRepository } from '@nestjs/typeorm'; + import { Repository } from 'typeorm'; + import { AnalyticsEvent } from '../entities/analytics-event.entity'; ++import { TrackEventDto } from '../dtos/track-event.dto'; + + @Injectable() +-export class TrackEventProvider implements OnModuleDestroy { +- private buffer: AnalyticsEvent[] = []; +- private flushInterval: NodeJS.Timeout; +- private readonly BATCH_SIZE = 50; +- private readonly FLUSH_INTERVAL_MS = 5000; +- ++export class TrackEventProvider { + constructor( + @InjectRepository(AnalyticsEvent) + private readonly analyticsEventRepository: Repository, +- ) { +- this.flushInterval = setInterval( +- () => this.flush(), +- this.FLUSH_INTERVAL_MS, +- ); +- } +- +- async track(eventData: Partial): Promise { +- const entity = this.analyticsEventRepository.create(eventData); +- this.buffer.push(entity); +- +- if (this.buffer.length >= this.BATCH_SIZE) { +- await this.flush(); +- } +- } +- +- async flush(): Promise { +- if (this.buffer.length === 0) return; +- +- const batch = [...this.buffer]; +- this.buffer = []; +- +- try { +- await this.analyticsEventRepository.save(batch); +- } catch (error) { +- this.buffer.unshift(...batch); +- throw error; +- } +- } +- +- onModuleDestroy() { +- clearInterval(this.flushInterval); ++ ) {} ++ ++ async track(dto: TrackEventDto) { ++ const event = this.analyticsEventRepository.create({ ++ eventType: dto.eventType, ++ userId: dto.userId ?? '', ++ entityId: dto.payload?.entityId ?? '', ++ payload: dto.payload, ++ }); ++ ++ const saved = await this.analyticsEventRepository.save(event); ++ ++ return { ++ success: true, ++ message: 'Event tracked successfully', ++ data: saved, ++ }; + } + } +diff --git a/backend/src/analytics/validators/date-range.validator.ts b/backend/src/analytics/validators/date-range.validator.ts +index 7118158..a47ddb0 100644 +--- a/backend/src/analytics/validators/date-range.validator.ts ++++ b/backend/src/analytics/validators/date-range.validator.ts +@@ -1,4 +1,8 @@ +-import { ValidatorConstraint, ValidatorConstraintInterface, ValidationArguments } from 'class-validator'; ++import { ++ ValidatorConstraint, ++ ValidatorConstraintInterface, ++ ValidationArguments, ++} from 'class-validator'; + + const ONE_YEAR_MS = 365 * 24 * 60 * 60 * 1000; + +diff --git a/backend/src/app.module.ts b/backend/src/app.module.ts +index ad3b8ff..ea2826a 100644 +--- a/backend/src/app.module.ts ++++ b/backend/src/app.module.ts +@@ -1,4 +1,9 @@ +-import { Module, NestModule, MiddlewareConsumer, RequestMethod } from '@nestjs/common'; ++import { ++ Module, ++ NestModule, ++ MiddlewareConsumer, ++ RequestMethod, ++} from '@nestjs/common'; + import { TypeOrmModule } from '@nestjs/typeorm'; + import { ConfigModule, ConfigService } from '@nestjs/config'; + import { EventEmitterModule } from '@nestjs/event-emitter'; +@@ -16,7 +21,10 @@ import { PuzzlesModule } from './puzzles/puzzles.module'; + import { QuestsModule } from './quests/quests.module'; + import { StreakModule } from './streak/strerak.module'; + import { CategoriesModule } from './categories/categories.module'; +-import { JwtAuthModule, JwtAuthMiddleware } from './auth/middleware/jwt-auth.module'; ++import { ++ JwtAuthModule, ++ JwtAuthMiddleware, ++} from './auth/middleware/jwt-auth.module'; + import { REDIS_CLIENT } from './redis/redis.constants'; + import jwtConfig from './auth/authConfig/jwt.config'; + import { UsersService } from './users/providers/users.service'; +@@ -95,10 +103,15 @@ import { AnalyticsModule } from './analytics/analytics.module'; + JwtAuthModule.registerAsync({ + imports: [ConfigModule, UsersModule, RedisModule], + inject: [ConfigService, UsersService, REDIS_CLIENT], +- useFactory: (configService: ConfigService, usersService: UsersService, redisClient: any) => ({ ++ useFactory: ( ++ configService: ConfigService, ++ usersService: UsersService, ++ redisClient: any, ++ ) => ({ + secret: configService.get('jwt.secret') || '', + redisClient: redisClient, +- validateUser: async (userId: string) => await usersService.findOneById(userId), ++ validateUser: async (userId: string) => ++ await usersService.findOneById(userId), + logging: true, + publicRoutes: ['/auth', '/api', '/docs', '/health'], + }), +@@ -113,9 +126,7 @@ export class AppModule implements NestModule { + * Apply the JWT Authentication Middleware to all routes except public ones. + */ + configure(consumer: MiddlewareConsumer) { +- consumer +- .apply(GeolocationMiddleware) +- .forRoutes('*'); ++ consumer.apply(GeolocationMiddleware).forRoutes('*'); + + consumer + .apply(JwtAuthMiddleware) +diff --git a/backend/src/auth/middleware/jwt-auth.middleware.ts b/backend/src/auth/middleware/jwt-auth.middleware.ts +index 39de808..9e62404 100644 +--- a/backend/src/auth/middleware/jwt-auth.middleware.ts ++++ b/backend/src/auth/middleware/jwt-auth.middleware.ts +@@ -111,7 +111,9 @@ export class JwtAuthMiddleware implements NestMiddleware { + this.logger.warn( + `Blacklisted token used: ${token.substring(0, 10)}...`, + ); +- throw new UnauthorizedException('Token blacklisted (user logged out)'); ++ throw new UnauthorizedException( ++ 'Token blacklisted (user logged out)', ++ ); + } + } + +diff --git a/backend/src/auth/social/providers/google-authentication.service.ts b/backend/src/auth/social/providers/google-authentication.service.ts +index f902f03..fdc7170 100644 +--- a/backend/src/auth/social/providers/google-authentication.service.ts ++++ b/backend/src/auth/social/providers/google-authentication.service.ts +@@ -115,7 +115,7 @@ export class GoogleAuthenticationService implements OnModuleInit { + const newUser = await this.userService.createGoogleUser({ + email: email, + username: given_name, +- ++ + googleId: googleId, + }); + return this.generateTokensProvider.generateTokens(newUser); +diff --git a/backend/src/common/common.module.ts b/backend/src/common/common.module.ts +index efd5f6d..e5b5dde 100644 +--- a/backend/src/common/common.module.ts ++++ b/backend/src/common/common.module.ts +@@ -6,7 +6,11 @@ import { RedisModule } from '../redis/redis.module'; + + @Module({ + imports: [RedisModule], +- providers: [PaginationProvider, CorrelationIdMiddleware, GeolocationMiddleware], ++ providers: [ ++ PaginationProvider, ++ CorrelationIdMiddleware, ++ GeolocationMiddleware, ++ ], + exports: [PaginationProvider, CorrelationIdMiddleware, GeolocationMiddleware], + }) + export class CommonModule {} +@@ -18,4 +22,3 @@ export * from './middleware/correlation-id.middleware'; + export * from './middleware/geolocation.middleware'; + export * from './interfaces/geolocation.interface'; + export * from './decorators/geolocation.decorator'; +- +diff --git a/backend/src/common/filters/http-exception.filter.ts b/backend/src/common/filters/http-exception.filter.ts +index 4155c72..5691f44 100644 +--- a/backend/src/common/filters/http-exception.filter.ts ++++ b/backend/src/common/filters/http-exception.filter.ts +@@ -31,7 +31,10 @@ export interface ErrorResponse { + /** + * TypeORM Postgres error codes mapped to our domain error codes. + */ +-const PG_ERROR_MAP: Record = { ++const PG_ERROR_MAP: Record< ++ string, ++ { code: AppErrorCode; status: HttpStatus; message: string } ++> = { + '23505': { + code: AppErrorCode.DUPLICATE_RESOURCE, + status: HttpStatus.CONFLICT, +@@ -52,7 +55,7 @@ const PG_ERROR_MAP: Record = { + country: 'US', +@@ -20,9 +20,7 @@ export class GeolocationMiddleware implements NestMiddleware { + timezone: 'America/New_York', + }; + +- constructor( +- @Inject(REDIS_CLIENT) private readonly redisClient: Redis, +- ) {} ++ constructor(@Inject(REDIS_CLIENT) private readonly redisClient: Redis) {} + + async use(req: Request, res: Response, next: NextFunction) { + try { +@@ -34,7 +32,7 @@ export class GeolocationMiddleware implements NestMiddleware { + const overrideCountry = req.headers['x-override-country'] as string; + const overrideCity = req.headers['x-override-city'] as string; + const overrideTimezone = req.headers['x-override-timezone'] as string; +- ++ + const ip = this.getClientIp(req); + + if (overrideCountry || overrideCity || overrideTimezone) { +@@ -49,8 +47,13 @@ export class GeolocationMiddleware implements NestMiddleware { + }; + return next(); + } +- +- if (!ip || ip === '127.0.0.1' || ip === '::1' || ip === '::ffff:127.0.0.1') { ++ ++ if ( ++ !ip || ++ ip === '127.0.0.1' || ++ ip === '::1' || ++ ip === '::ffff:127.0.0.1' ++ ) { + // Localhost access fallback + req.location = { + ip: ip || '127.0.0.1', +@@ -70,14 +73,14 @@ export class GeolocationMiddleware implements NestMiddleware { + + if (cachedData) { + const parsed = JSON.parse(cachedData) as Partial; +- req.location = { ++ req.location = { + ip: parsed.ip || ip, + country: parsed.country!, + region: parsed.region!, + city: parsed.city!, + timezone: parsed.timezone!, +- language, +- isOverride: false ++ language, ++ isOverride: false, + }; + return next(); + } +@@ -99,13 +102,17 @@ export class GeolocationMiddleware implements NestMiddleware { + req.location = locationData; + + // Cache result (store only needed parts to comply with privacy) +- await this.redisClient.setex(cacheKey, this.CACHE_TTL, JSON.stringify({ +- ip: locationData.ip, +- country: locationData.country, +- region: locationData.region, +- city: locationData.city, +- timezone: locationData.timezone, +- })); ++ await this.redisClient.setex( ++ cacheKey, ++ this.CACHE_TTL, ++ JSON.stringify({ ++ ip: locationData.ip, ++ country: locationData.country, ++ region: locationData.region, ++ city: locationData.city, ++ timezone: locationData.timezone, ++ }), ++ ); + } else { + // Fallback + req.location = { +@@ -118,10 +125,13 @@ export class GeolocationMiddleware implements NestMiddleware { + isOverride: false, + }; + } +- ++ + next(); + } catch (error) { +- this.logger.error(`Geolocation error: ${(error as Error).message}`, (error as Error).stack); ++ this.logger.error( ++ `Geolocation error: ${(error as Error).message}`, ++ (error as Error).stack, ++ ); + // Don't break application if geolocation fails + req.location = { + ip: this.getClientIp(req), +@@ -144,7 +154,7 @@ export class GeolocationMiddleware implements NestMiddleware { + } + return xForwardedFor.split(',')[0].trim(); + } +- ++ + return req.ip || req.socket.remoteAddress || '127.0.0.1'; + } + +diff --git a/backend/src/health/health.controller.spec.ts b/backend/src/health/health.controller.spec.ts +index 205346c..41fb3c3 100644 +--- a/backend/src/health/health.controller.spec.ts ++++ b/backend/src/health/health.controller.spec.ts +@@ -56,7 +56,9 @@ describe('HealthController', () => { + it('should return 403 when app is shutting down', async () => { + mockHealthService.isAppShuttingDown.mockReturnValue(true); + +- await expect(controller.getBasicHealth()).rejects.toThrow('Application is shutting down'); ++ await expect(controller.getBasicHealth()).rejects.toThrow( ++ 'Application is shutting down', ++ ); + expect(healthService.isAppShuttingDown).toHaveBeenCalled(); + expect(healthService.getBasicHealth).not.toHaveBeenCalled(); + }); +@@ -84,7 +86,9 @@ describe('HealthController', () => { + it('should return 403 when app is shutting down', async () => { + mockHealthService.isAppShuttingDown.mockReturnValue(true); + +- await expect(controller.getLivenessHealth()).rejects.toThrow('Application is shutting down'); ++ await expect(controller.getLivenessHealth()).rejects.toThrow( ++ 'Application is shutting down', ++ ); + expect(healthService.isAppShuttingDown).toHaveBeenCalled(); + expect(healthService.getLivenessHealth).not.toHaveBeenCalled(); + }); +@@ -118,7 +122,9 @@ describe('HealthController', () => { + it('should return 403 when app is shutting down', async () => { + mockHealthService.isAppShuttingDown.mockReturnValue(true); + +- await expect(controller.getReadinessHealth()).rejects.toThrow('Application is shutting down'); ++ await expect(controller.getReadinessHealth()).rejects.toThrow( ++ 'Application is shutting down', ++ ); + expect(healthService.isAppShuttingDown).toHaveBeenCalled(); + expect(healthService.getReadinessHealth).not.toHaveBeenCalled(); + }); +@@ -140,7 +146,9 @@ describe('HealthController', () => { + mockHealthService.isAppShuttingDown.mockReturnValue(false); + mockHealthService.getReadinessHealth.mockResolvedValue(expectedResult); + +- await expect(controller.getReadinessHealth()).rejects.toThrow('Service not ready'); ++ await expect(controller.getReadinessHealth()).rejects.toThrow( ++ 'Service not ready', ++ ); + expect(healthService.isAppShuttingDown).toHaveBeenCalled(); + expect(healthService.getReadinessHealth).toHaveBeenCalled(); + }); +@@ -164,10 +172,26 @@ describe('HealthController', () => { + uptime: 3600, + timestamp: '2023-01-01T00:00:00.000Z', + checks: { +- database: { status: 'healthy', responseTime: 5, details: { connected: true } }, +- redis: { status: 'healthy', responseTime: 2, details: { connected: true } }, +- memory: { status: 'healthy', responseTime: 1, details: { usagePercent: '45%' } }, +- filesystem: { status: 'healthy', responseTime: 1, details: { writable: true } }, ++ database: { ++ status: 'healthy', ++ responseTime: 5, ++ details: { connected: true }, ++ }, ++ redis: { ++ status: 'healthy', ++ responseTime: 2, ++ details: { connected: true }, ++ }, ++ memory: { ++ status: 'healthy', ++ responseTime: 1, ++ details: { usagePercent: '45%' }, ++ }, ++ filesystem: { ++ status: 'healthy', ++ responseTime: 1, ++ details: { writable: true }, ++ }, + }, + }; + +@@ -182,7 +206,9 @@ describe('HealthController', () => { + }); + + it('should return 403 with invalid admin key', async () => { +- await expect(controller.getDetailedHealth('invalid-key')).rejects.toThrow('Admin access required'); ++ await expect(controller.getDetailedHealth('invalid-key')).rejects.toThrow( ++ 'Admin access required', ++ ); + expect(healthService.isAppShuttingDown).not.toHaveBeenCalled(); + expect(healthService.getDetailedHealth).not.toHaveBeenCalled(); + }); +@@ -190,7 +216,9 @@ describe('HealthController', () => { + it('should return 403 when app is shutting down', async () => { + mockHealthService.isAppShuttingDown.mockReturnValue(true); + +- await expect(controller.getDetailedHealth('test-admin-key')).rejects.toThrow('Application is shutting down'); ++ await expect( ++ controller.getDetailedHealth('test-admin-key'), ++ ).rejects.toThrow('Application is shutting down'); + expect(healthService.isAppShuttingDown).toHaveBeenCalled(); + expect(healthService.getDetailedHealth).not.toHaveBeenCalled(); + }); +diff --git a/backend/src/health/health.controller.ts b/backend/src/health/health.controller.ts +index c49de99..056a367 100644 +--- a/backend/src/health/health.controller.ts ++++ b/backend/src/health/health.controller.ts +@@ -1,10 +1,10 @@ +-import { +- Controller, +- Get, +- HttpCode, +- HttpStatus, ++import { ++ Controller, ++ Get, ++ HttpCode, ++ HttpStatus, + Headers, +- ForbiddenException ++ ForbiddenException, + } from '@nestjs/common'; + import { HealthService } from './health.service'; + import { HealthCheckResult } from './health.interfaces'; +@@ -20,7 +20,7 @@ export class HealthController { + if (this.healthService.isAppShuttingDown()) { + throw new ForbiddenException('Application is shutting down'); + } +- ++ + return this.healthService.getBasicHealth(); + } + +@@ -31,7 +31,7 @@ export class HealthController { + if (this.healthService.isAppShuttingDown()) { + throw new ForbiddenException('Application is shutting down'); + } +- ++ + return this.healthService.getLivenessHealth(); + } + +@@ -42,31 +42,33 @@ export class HealthController { + if (this.healthService.isAppShuttingDown()) { + throw new ForbiddenException('Application is shutting down'); + } +- ++ + const health = await this.healthService.getReadinessHealth(); +- ++ + // Return appropriate HTTP status based on health + if (health.status === 'unhealthy') { + throw new ForbiddenException('Service not ready'); + } +- ++ + return health; + } + + @Get('detailed') + @HttpCode(HttpStatus.OK) +- async getDetailedHealth(@Headers('x-admin-key') adminKey?: string): Promise { ++ async getDetailedHealth( ++ @Headers('x-admin-key') adminKey?: string, ++ ): Promise { + // Admin-only detailed health check + const requiredAdminKey = process.env.ADMIN_HEALTH_KEY || 'admin-key'; +- ++ + if (adminKey !== requiredAdminKey) { + throw new ForbiddenException('Admin access required'); + } +- ++ + if (this.healthService.isAppShuttingDown()) { + throw new ForbiddenException('Application is shutting down'); + } +- ++ + return this.healthService.getDetailedHealth(); + } + } +diff --git a/backend/src/health/health.module.ts b/backend/src/health/health.module.ts +index f131ce0..f12bffc 100644 +--- a/backend/src/health/health.module.ts ++++ b/backend/src/health/health.module.ts +@@ -6,11 +6,7 @@ import { HealthService } from './health.service'; + import { RedisModule } from '../redis/redis.module'; + + @Module({ +- imports: [ +- ConfigModule, +- TypeOrmModule, +- RedisModule, +- ], ++ imports: [ConfigModule, TypeOrmModule, RedisModule], + controllers: [HealthController], + providers: [HealthService], + exports: [HealthService], +diff --git a/backend/src/health/health.service.spec.ts b/backend/src/health/health.service.spec.ts +index 6bb450c..ce83336 100644 +--- a/backend/src/health/health.service.spec.ts ++++ b/backend/src/health/health.service.spec.ts +@@ -104,7 +104,9 @@ describe('HealthService', () => { + }); + + it('should return unhealthy when database fails', async () => { +- mockConnection.query.mockRejectedValue(new Error('Database connection failed')); ++ mockConnection.query.mockRejectedValue( ++ new Error('Database connection failed'), ++ ); + mockRedis.ping.mockResolvedValue('PONG'); + + const result = await service.getReadinessHealth(); +@@ -169,13 +171,15 @@ describe('HealthService', () => { + + // Mock high memory usage + const originalMemoryUsage = process.memoryUsage; +- (process.memoryUsage as unknown as jest.Mock) = jest.fn().mockReturnValue({ +- heapTotal: 1000, +- heapUsed: 850, // 85% usage +- rss: 1200, +- external: 100, +- arrayBuffers: 50, +- }); ++ (process.memoryUsage as unknown as jest.Mock) = jest ++ .fn() ++ .mockReturnValue({ ++ heapTotal: 1000, ++ heapUsed: 850, // 85% usage ++ rss: 1200, ++ external: 100, ++ arrayBuffers: 50, ++ }); + + const result = await service.getDetailedHealth(); + +@@ -191,13 +195,15 @@ describe('HealthService', () => { + + // Mock critical memory usage + const originalMemoryUsage = process.memoryUsage; +- (process.memoryUsage as unknown as jest.Mock) = jest.fn().mockReturnValue({ +- heapTotal: 1000, +- heapUsed: 950, // 95% usage +- rss: 1200, +- external: 100, +- arrayBuffers: 50, +- }); ++ (process.memoryUsage as unknown as jest.Mock) = jest ++ .fn() ++ .mockReturnValue({ ++ heapTotal: 1000, ++ heapUsed: 950, // 95% usage ++ rss: 1200, ++ external: 100, ++ arrayBuffers: 50, ++ }); + + const result = await service.getDetailedHealth(); + +@@ -215,14 +221,14 @@ describe('HealthService', () => { + + // First call + const result1 = await service.getDetailedHealth(); +- ++ + // Second call should use cache + const result2 = await service.getDetailedHealth(); + + // Should only call dependencies once due to caching + expect(mockConnection.query).toHaveBeenCalledTimes(1); + expect(mockRedis.ping).toHaveBeenCalledTimes(1); +- ++ + // Results should be identical + expect(result1).toEqual(result2); + }); +@@ -233,7 +239,7 @@ describe('HealthService', () => { + + // First call + await service.getDetailedHealth(); +- ++ + // Second call with skip cache + await service.getDetailedHealth(); + +@@ -258,11 +264,16 @@ describe('HealthService', () => { + const result = await service.getDetailedHealth(); + + expect(result.checks!.externalApis).toBeDefined(); +- expect(result.checks!.externalApis!['https://api.example.com/health']).toBeDefined(); +- expect(fetch).toHaveBeenCalledWith('https://api.example.com/health', expect.objectContaining({ +- method: 'GET', +- signal: expect.any(Object), +- })); ++ expect( ++ result.checks!.externalApis!['https://api.example.com/health'], ++ ).toBeDefined(); ++ expect(fetch).toHaveBeenCalledWith( ++ 'https://api.example.com/health', ++ expect.objectContaining({ ++ method: 'GET', ++ signal: expect.any(Object), ++ }), ++ ); + }); + + it('should handle external API failures', async () => { +@@ -271,12 +282,18 @@ describe('HealthService', () => { + mockRedis.ping.mockResolvedValue('PONG'); + + // Mock fetch failure +- global.fetch = jest.fn().mockRejectedValue(new Error('Network error')) as any; ++ global.fetch = jest ++ .fn() ++ .mockRejectedValue(new Error('Network error')) as any; + + const result = await service.getDetailedHealth(); + +- expect(result.checks!.externalApis!['https://api.example.com/health'].status).toBe('unhealthy'); +- expect(result.checks!.externalApis!['https://api.example.com/health'].error).toBe('Network error'); ++ expect( ++ result.checks!.externalApis!['https://api.example.com/health'].status, ++ ).toBe('unhealthy'); ++ expect( ++ result.checks!.externalApis!['https://api.example.com/health'].error, ++ ).toBe('Network error'); + }); + }); + +diff --git a/backend/src/health/health.service.ts b/backend/src/health/health.service.ts +index ba1f344..df9e8be 100644 +--- a/backend/src/health/health.service.ts ++++ b/backend/src/health/health.service.ts +@@ -48,7 +48,8 @@ export class HealthService { + const status = await this.performHealthChecks(options); + + const isHealthy = Object.values(status).every( +- (check: HealthCheck) => check.status === 'healthy' || check.status === 'degraded' ++ (check: HealthCheck) => ++ check.status === 'healthy' || check.status === 'degraded', + ); + + return { +@@ -61,19 +62,21 @@ export class HealthService { + } + + async getDetailedHealth(): Promise { +- const options: HealthCheckOptions = { +- includeDetails: true, ++ const options: HealthCheckOptions = { ++ includeDetails: true, + timeout: HEALTH_CHECK_TIMEOUT, +- skipCache: false ++ skipCache: false, + }; +- ++ + const status = await this.performHealthChecks(options); +- ++ + // Determine overall status +- const statuses = Object.values(status).map((check: HealthCheck) => check.status); ++ const statuses = Object.values(status).map( ++ (check: HealthCheck) => check.status, ++ ); + const hasUnhealthy = statuses.includes('unhealthy'); + const hasDegraded = statuses.includes('degraded'); +- ++ + let overallStatus: 'healthy' | 'degraded' | 'unhealthy'; + if (hasUnhealthy) { + overallStatus = 'unhealthy'; +@@ -92,9 +95,11 @@ export class HealthService { + }; + } + +- private async performHealthChecks(options: HealthCheckOptions): Promise { ++ private async performHealthChecks( ++ options: HealthCheckOptions, ++ ): Promise { + const cacheKey = `health-checks-${JSON.stringify(options)}`; +- ++ + // Check cache first + if (!options.skipCache) { + const cached = this.cache.get(cacheKey); +@@ -111,7 +116,10 @@ export class HealthService { + }; + + // Add external API checks if configured +- const externalApiUrls = this.configService.get('EXTERNAL_API_HEALTH_CHECKS', []); ++ const externalApiUrls = this.configService.get( ++ 'EXTERNAL_API_HEALTH_CHECKS', ++ [], ++ ); + if (externalApiUrls.length > 0) { + status.externalApis = {}; + for (const url of externalApiUrls) { +@@ -130,11 +138,11 @@ export class HealthService { + + private async checkDatabase(): Promise { + const startTime = Date.now(); +- ++ + try { + await this.connection.query('SELECT 1'); + const responseTime = Date.now() - startTime; +- ++ + return { + status: 'healthy', + responseTime, +@@ -145,11 +153,12 @@ export class HealthService { + }; + } catch (error) { + const responseTime = Date.now() - startTime; +- ++ + return { + status: 'unhealthy', + responseTime, +- error: error instanceof Error ? error.message : 'Unknown database error', ++ error: ++ error instanceof Error ? error.message : 'Unknown database error', + details: { + connected: false, + responseTime: `${responseTime}ms`, +@@ -160,11 +169,11 @@ export class HealthService { + + private async checkRedis(): Promise { + const startTime = Date.now(); +- ++ + try { + await this.redis.ping(); + const responseTime = Date.now() - startTime; +- ++ + return { + status: 'healthy', + responseTime, +@@ -175,7 +184,7 @@ export class HealthService { + }; + } catch (error) { + const responseTime = Date.now() - startTime; +- ++ + return { + status: 'unhealthy', + responseTime, +@@ -190,15 +199,15 @@ export class HealthService { + + private async checkMemory(): Promise { + const startTime = Date.now(); +- ++ + try { + const memUsage = process.memoryUsage(); + const totalMemory = memUsage.heapTotal; + const usedMemory = memUsage.heapUsed; + const memoryUsagePercent = (usedMemory / totalMemory) * 100; +- ++ + const responseTime = Date.now() - startTime; +- ++ + // Consider memory usage > 80% as degraded, > 90% as unhealthy + let status: 'healthy' | 'degraded' | 'unhealthy'; + if (memoryUsagePercent > 90) { +@@ -208,7 +217,7 @@ export class HealthService { + } else { + status = 'healthy'; + } +- ++ + return { + status, + responseTime, +@@ -221,7 +230,7 @@ export class HealthService { + }; + } catch (error) { + const responseTime = Date.now() - startTime; +- ++ + return { + status: 'unhealthy', + responseTime, +@@ -232,13 +241,13 @@ export class HealthService { + + private async checkFilesystem(): Promise { + const startTime = Date.now(); +- ++ + try { + const fs = require('fs').promises; + await fs.access('/tmp', fs.constants.W_OK); +- ++ + const responseTime = Date.now() - startTime; +- ++ + return { + status: 'healthy', + responseTime, +@@ -249,11 +258,12 @@ export class HealthService { + }; + } catch (error) { + const responseTime = Date.now() - startTime; +- ++ + return { + status: 'unhealthy', + responseTime, +- error: error instanceof Error ? error.message : 'Unknown filesystem error', ++ error: ++ error instanceof Error ? error.message : 'Unknown filesystem error', + details: { + writable: false, + responseTime: `${responseTime}ms`, +@@ -264,15 +274,15 @@ export class HealthService { + + private async checkExternalApi(url: string): Promise { + const startTime = Date.now(); +- ++ + try { + const response = await fetch(url, { + method: 'GET', + signal: AbortSignal.timeout(HEALTH_CHECK_TIMEOUT), + }); +- ++ + const responseTime = Date.now() - startTime; +- ++ + if (response.ok) { + return { + status: 'healthy', +@@ -295,7 +305,7 @@ export class HealthService { + } + } catch (error) { + const responseTime = Date.now() - startTime; +- ++ + return { + status: 'unhealthy', + responseTime, +diff --git a/backend/src/main.ts b/backend/src/main.ts +index ec9a1de..2b9d609 100644 +--- a/backend/src/main.ts ++++ b/backend/src/main.ts +@@ -19,7 +19,9 @@ async function bootstrap() { + ); + + // Stamp every request with a correlation ID before any other handler runs +- app.use(new CorrelationIdMiddleware().use.bind(new CorrelationIdMiddleware())); ++ app.use( ++ new CorrelationIdMiddleware().use.bind(new CorrelationIdMiddleware()), ++ ); + + // Enable global exception handling (catches ALL errors, not just HttpExceptions) + app.useGlobalFilters(new AllExceptionsFilter()); +@@ -42,13 +44,13 @@ async function bootstrap() { + + // Graceful shutdown handling + const healthService = app.get(HealthService); +- ++ + const gracefulShutdown = async (signal: string) => { + console.log(`\nšŸ›‘ Received ${signal}. Starting graceful shutdown...`); +- ++ + // Signal health checks that we're shutting down + healthService.setIsShuttingDown(); +- ++ + // Wait a moment for load balancers to detect the unhealthy state + setTimeout(async () => { + console.log('šŸ”„ Closing HTTP server...'); +diff --git a/backend/test/analytics.e2e-spec.ts b/backend/test/analytics.e2e-spec.ts +index 0a291a0..138be4e 100644 +--- a/backend/test/analytics.e2e-spec.ts ++++ b/backend/test/analytics.e2e-spec.ts +@@ -4,7 +4,16 @@ import { + ValidationPipe, + } from '@nestjs/common'; + import { Test, TestingModule } from '@nestjs/testing'; +-import * as request from 'supertest'; ++import { ++ afterAll, ++ beforeAll, ++ beforeEach, ++ describe, ++ expect, ++ it, ++ jest, ++} from '@jest/globals'; ++import request from 'supertest'; + import { App } from 'supertest/types'; + import { AnalyticsController } from '../src/analytics/controllers/analytics.controller'; + import { TrackEventProvider } from '../src/analytics/providers/track-event.provider'; +@@ -16,7 +25,14 @@ import { AnalyticsMetricResult } from '../src/analytics/dtos/analytics-metric-re + describe('GET /analytics/users/retention (e2e)', () => { + let app: INestApplication; + +- const mockGetRetentionCurveProvider = { getRetentionCurve: jest.fn() }; ++ const mockGetRetentionCurveProvider: { ++ getRetentionCurve: jest.MockedFunction< ++ (...args: any[]) => Promise ++ >; ++ } = { ++ getRetentionCurve: ++ jest.fn<(...args: any[]) => Promise>(), ++ }; + + const fakeResult: AnalyticsMetricResult = { + startDate: '2024-01-01', +@@ -97,9 +113,9 @@ describe('GET /analytics/users/retention (e2e)', () => { + .expect(200); + + expect(res.body).toEqual(fakeResult); +- expect(mockGetRetentionCurveProvider.getRetentionCurve).toHaveBeenCalledTimes( +- 1, +- ); ++ expect( ++ mockGetRetentionCurveProvider.getRetentionCurve, ++ ).toHaveBeenCalledTimes(1); + }); + + it('returns 403 for a caller without the admin role', async () => { +@@ -111,7 +127,9 @@ describe('GET /analytics/users/retention (e2e)', () => { + }) + .expect(403); + +- expect(mockGetRetentionCurveProvider.getRetentionCurve).not.toHaveBeenCalled(); ++ expect( ++ mockGetRetentionCurveProvider.getRetentionCurve, ++ ).not.toHaveBeenCalled(); + }); + + it('returns 400 with a clear validation message for an invalid granularity', async () => { +@@ -124,7 +142,9 @@ describe('GET /analytics/users/retention (e2e)', () => { + expect(res.body.message).toEqual( + expect.arrayContaining([expect.stringContaining('granularity')]), + ); +- expect(mockGetRetentionCurveProvider.getRetentionCurve).not.toHaveBeenCalled(); ++ expect( ++ mockGetRetentionCurveProvider.getRetentionCurve, ++ ).not.toHaveBeenCalled(); + }); + + it('returns 400 when start is after end', async () => { +@@ -144,7 +164,9 @@ describe('GET /analytics/users/retention (e2e)', () => { + ), + ]), + ); +- expect(mockGetRetentionCurveProvider.getRetentionCurve).not.toHaveBeenCalled(); ++ expect( ++ mockGetRetentionCurveProvider.getRetentionCurve, ++ ).not.toHaveBeenCalled(); + }); + + it('returns 400 for an unrecognized query param', async () => { +@@ -154,6 +176,8 @@ describe('GET /analytics/users/retention (e2e)', () => { + .query({ unknownParam: 'nope' }) + .expect(400); + +- expect(mockGetRetentionCurveProvider.getRetentionCurve).not.toHaveBeenCalled(); ++ expect( ++ mockGetRetentionCurveProvider.getRetentionCurve, ++ ).not.toHaveBeenCalled(); + }); + }); +diff --git a/frontend/app/analytics/page.tsx b/frontend/app/analytics/page.tsx +index 9e0a751..4acdb8c 100644 +--- a/frontend/app/analytics/page.tsx ++++ b/frontend/app/analytics/page.tsx +@@ -16,4 +16,4 @@ export default function AnalyticsPage() { + + + ); +-} +\ No newline at end of file ++} +diff --git a/frontend/app/auth/check-email/page.tsx b/frontend/app/auth/check-email/page.tsx +index 3ecb3f9..03f314c 100644 +--- a/frontend/app/auth/check-email/page.tsx ++++ b/frontend/app/auth/check-email/page.tsx +@@ -7,7 +7,6 @@ import { useToast } from "@/components/ui/ToastProvider"; + import { useState } from "react"; + import Button from "@/components/ui/Button"; + +- + const CheckEmail = () => { + const { showSuccess, showError } = useToast(); + const [isLoading, setIsLoading] = useState(false); +@@ -16,33 +15,47 @@ const CheckEmail = () => { + setIsLoading(true); + + // Get email from session storage or local storage (assuming it was saved during forgot-password) +- const email = sessionStorage.getItem('resetEmail') || localStorage.getItem('resetEmail'); ++ const email = ++ sessionStorage.getItem("resetEmail") || ++ localStorage.getItem("resetEmail"); + + if (!email) { +- showError('Error', 'Email not found. Please go back and try again.'); ++ showError("Error", "Email not found. Please go back and try again."); + setIsLoading(false); + return; + } + + try { +- const response = await fetch(`${process.env.NEXT_PUBLIC_API_URL}/auth/forgot-password`, { +- method: 'POST', +- headers: { +- 'Content-Type': 'application/json', ++ const response = await fetch( ++ `${process.env.NEXT_PUBLIC_API_URL}/auth/forgot-password`, ++ { ++ method: "POST", ++ headers: { ++ "Content-Type": "application/json", ++ }, ++ body: JSON.stringify({ email }), + }, +- body: JSON.stringify({ email }), +- }); ++ ); + + const data = await response.json(); + + if (response.ok) { +- showSuccess('Success', data.message || 'Password reset link resent to your email.'); ++ showSuccess( ++ "Success", ++ data.message || "Password reset link resent to your email.", ++ ); + } else { +- showError('Error', data.message || 'Failed to resend password reset link.'); ++ showError( ++ "Error", ++ data.message || "Failed to resend password reset link.", ++ ); + } + // eslint-disable-next-line @typescript-eslint/no-unused-vars + } catch (_) { +- showError('Error', 'An unexpected error occurred. Please try again later.'); ++ showError( ++ "Error", ++ "An unexpected error occurred. Please try again later.", ++ ); + } finally { + setIsLoading(false); + } +@@ -61,8 +74,8 @@ const CheckEmail = () => { +
+
+ {/* Mobile header with back arrow */} +-
+-
++
++
+ + + +@@ -79,7 +92,8 @@ const CheckEmail = () => { + {/* Message */} +
+

+- We've sent you a link to your email address to reset your password. Click the link in your inbox to continue. ++ We've sent you a link to your email address to reset your ++ password. Click the link in your inbox to continue. +

+
+ +@@ -90,13 +104,13 @@ const CheckEmail = () => { + disabled={isLoading} + className="w-full h-12 px-[10px] py-[14px] bg-[#3B82F6] hover:bg-[#2663C7] [box-shadow:0px_4px_0px_0px_#2663C7] text-white font-medium rounded-lg transition-colors" + > +- {isLoading ? 'Resending...' : 'Resend Link'} ++ {isLoading ? "Resending..." : "Resend Link"} + +
+
+
+ + ); +-} ++}; + +-export default CheckEmail; +\ No newline at end of file ++export default CheckEmail; +diff --git a/frontend/app/auth/forgot-password/page.tsx b/frontend/app/auth/forgot-password/page.tsx +index c1a4305..89e8f10 100644 +--- a/frontend/app/auth/forgot-password/page.tsx ++++ b/frontend/app/auth/forgot-password/page.tsx +@@ -2,7 +2,7 @@ + + import ErrorBoundary from "@/components/error/ErrorBoundary"; + import Link from "next/link"; +-import Image from 'next/image'; ++import Image from "next/image"; + import { ArrowLeft } from "lucide-react"; + import { useRouter } from "next/navigation"; + import Input from "@/components/ui/Input"; +@@ -14,148 +14,146 @@ const ForgotPassword = () => { + const router = useRouter(); + + const { showSuccess, showError } = useToast(); +- const [formData, setFormData] = useState({ +- email: '', +- }); +- const [isLoading, setIsLoading] = useState(false); +- +- const validateEmail = (email: string) => { +- const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; +- return emailRegex.test(email); +- }; +- +- const handleInputChange = (field: string) => ( +- e: React.ChangeEvent +- ) => { +- const value = e.target.value; +- setFormData(prev => ({ +- ...prev, +- [field]: value +- })); +- }; +- +- const handleForgotPassword = async (e: React.FormEvent) => { +- e.preventDefault(); +- setIsLoading(true); +- +- // Validate email before submission +- if (!validateEmail(formData.email)) { +- showError('Invalid Email', 'Please enter a valid email address'); +- setIsLoading(false); +- return; +- } +- +- // Check if email field is empty +- if (!formData.email.trim()) { +- showError('Email Required', 'Please enter your email address'); +- setIsLoading(false); +- return; +- } +- +- try { +- const response = await fetch( +- `${process.env.NEXT_PUBLIC_API_URL}/auth/forgot-password`, +- { +- method: "POST", +- headers: { +- "Content-Type": "application/json", +- }, +- body: JSON.stringify({ email: formData.email }), +- }, +- ); +- +- const data = await response.json(); +- +- if (response.ok) { +- // Store email for resend functionality +- sessionStorage.setItem("resetEmail", formData.email); +- showSuccess( +- "Success", +- data.message || "Password reset link sent to your email.", +- ); +- router.push("/auth/check-email"); +- } else { +- showError( +- "Error", +- data.message || "Failed to send password reset link.", +- ); +- } +- // eslint-disable-next-line @typescript-eslint/no-unused-vars +- } catch (_) { +- showError('Error', 'An unexpected error occurred. Please try again later.'); +- } finally { +- setIsLoading(false); +- } +- } ++ const [formData, setFormData] = useState({ ++ email: "", ++ }); ++ const [isLoading, setIsLoading] = useState(false); ++ ++ const validateEmail = (email: string) => { ++ const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; ++ return emailRegex.test(email); ++ }; ++ ++ const handleInputChange = ++ (field: string) => (e: React.ChangeEvent) => { ++ const value = e.target.value; ++ setFormData((prev) => ({ ++ ...prev, ++ [field]: value, ++ })); ++ }; ++ ++ const handleForgotPassword = async (e: React.FormEvent) => { ++ e.preventDefault(); ++ setIsLoading(true); ++ ++ // Validate email before submission ++ if (!validateEmail(formData.email)) { ++ showError("Invalid Email", "Please enter a valid email address"); ++ setIsLoading(false); ++ return; ++ } + ++ // Check if email field is empty ++ if (!formData.email.trim()) { ++ showError("Email Required", "Please enter your email address"); ++ setIsLoading(false); ++ return; ++ } + +- return ( ++ try { ++ const response = await fetch( ++ `${process.env.NEXT_PUBLIC_API_URL}/auth/forgot-password`, ++ { ++ method: "POST", ++ headers: { ++ "Content-Type": "application/json", ++ }, ++ body: JSON.stringify({ email: formData.email }), ++ }, ++ ); ++ ++ const data = await response.json(); ++ ++ if (response.ok) { ++ // Store email for resend functionality ++ sessionStorage.setItem("resetEmail", formData.email); ++ showSuccess( ++ "Success", ++ data.message || "Password reset link sent to your email.", ++ ); ++ router.push("/auth/check-email"); ++ } else { ++ showError( ++ "Error", ++ data.message || "Failed to send password reset link.", ++ ); ++ } ++ // eslint-disable-next-line @typescript-eslint/no-unused-vars ++ } catch (_) { ++ showError( ++ "Error", ++ "An unexpected error occurred. Please try again later.", ++ ); ++ } finally { ++ setIsLoading(false); ++ } ++ }; ++ ++ return ( + +
+- {/* Back Arrow - stays at top on desktop, within main content */} +-
+- +- +- +-
+- {/* Main Content */} +-
+-
+- {/* Mobile header with back arrow */} +-
+-
+- +- Home +- ++ {/* Back Arrow - stays at top on desktop, within main content */} ++
++ ++ ++ ++
++ {/* Main Content */} ++
++
++ {/* Mobile header with back arrow */} ++
++
++ ++ Home ++ ++
++

++ Forgot Password? ++

+
+-

++ ++ {/* Desktop title */} ++

+ Forgot Password? +

+-
+ +- {/* Desktop title */} +-

+- Forgot Password? +-

+- +- {/* Sign-in Form */} +-
+- +- +- {/* Sign In Button */} +- +-
+- +- {/* Sign Up Link */} +-
+- +- Back to Sign in +- ++ {/* Sign-in Form */} ++
++ ++ ++ {/* Sign In Button */} ++ ++
++ ++ {/* Sign Up Link */} ++
++ ++ Back to Sign in ++ ++
+
+
+
+-
+ +- ); +-} +- +-export default ForgotPassword; +\ No newline at end of file ++ ); ++}; ++ ++export default ForgotPassword; +diff --git a/frontend/app/auth/reset-password/[token]/page.tsx b/frontend/app/auth/reset-password/[token]/page.tsx +index acbf7c1..9b6a50b 100644 +--- a/frontend/app/auth/reset-password/[token]/page.tsx ++++ b/frontend/app/auth/reset-password/[token]/page.tsx +@@ -15,13 +15,12 @@ const ResetPassword = () => { + + const { showSuccess, showError } = useToast(); + const [formData, setFormData] = useState({ +- password: '', +- confirmPassword: '', ++ password: "", ++ confirmPassword: "", + }); + const [isLoading, setIsLoading] = useState(false); + +- console.log({token}); +- ++ console.log({ token }); + + // Password validation function + const validatePassword = (password: string) => { +@@ -30,15 +29,14 @@ const ResetPassword = () => { + return passwordRegex.test(password); + }; + +- const handleInputChange = (field: string) => ( +- e: React.ChangeEvent +- ) => { +- const value = e.target.value; +- setFormData(prev => ({ +- ...prev, +- [field]: value +- })); +- }; ++ const handleInputChange = ++ (field: string) => (e: React.ChangeEvent) => { ++ const value = e.target.value; ++ setFormData((prev) => ({ ++ ...prev, ++ [field]: value, ++ })); ++ }; + + const handleResetPassword = async (e: React.FormEvent) => { + e.preventDefault(); +@@ -46,15 +44,15 @@ const ResetPassword = () => { + + // Validate password + if (!formData.password.trim()) { +- showError('Password Required', 'Please enter a new password'); ++ showError("Password Required", "Please enter a new password"); + setIsLoading(false); + return; + } + + if (!validatePassword(formData.password)) { + showError( +- 'Weak Password', +- 'Password must be at least 8 characters with uppercase, lowercase, and number' ++ "Weak Password", ++ "Password must be at least 8 characters with uppercase, lowercase, and number", + ); + setIsLoading(false); + return; +@@ -62,14 +60,14 @@ const ResetPassword = () => { + + // Check if passwords match + if (formData.password !== formData.confirmPassword) { +- showError('Password Mismatch', 'Passwords do not match'); ++ showError("Password Mismatch", "Passwords do not match"); + setIsLoading(false); + return; + } + + // Check if confirm password is empty + if (!formData.confirmPassword.trim()) { +- showError('Confirm Password Required', 'Please confirm your password'); ++ showError("Confirm Password Required", "Please confirm your password"); + setIsLoading(false); + return; + } +@@ -108,14 +106,24 @@ const ResetPassword = () => { + } + // eslint-disable-next-line @typescript-eslint/no-unused-vars + } catch (_) { +- showError('Error', 'An unexpected error occurred. Please try again later.'); ++ showError( ++ "Error", ++ "An unexpected error occurred. Please try again later.", ++ ); + } finally { + setIsLoading(false); + } + }; + +- const passwordsMatch = formData.password && formData.confirmPassword && formData.password === formData.confirmPassword; +- const isFormValid = formData.password && formData.confirmPassword && passwordsMatch && validatePassword(formData.password); ++ const passwordsMatch = ++ formData.password && ++ formData.confirmPassword && ++ formData.password === formData.confirmPassword; ++ const isFormValid = ++ formData.password && ++ formData.confirmPassword && ++ passwordsMatch && ++ validatePassword(formData.password); + + return ( + +@@ -123,7 +131,7 @@ const ResetPassword = () => { + {/* Main Content */} +
+
+-
++
+

+ New Password +

+@@ -135,7 +143,7 @@ const ResetPassword = () => { + type="password" + placeholder="New Password" + value={formData.password} +- onChange={handleInputChange('password')} ++ onChange={handleInputChange("password")} + /> + + {/* Password Requirements */} +@@ -143,16 +151,32 @@ const ResetPassword = () => { +
+

Password must contain:

+
    +-
  • = 8 ? 'text-[#10B981]' : ''}> ++
  • = 8 ? "text-[#10B981]" : "" ++ } ++ > + At least 8 characters +
  • +-
  • ++
  • + One uppercase letter +
  • +-
  • ++
  • + One lowercase letter +
  • +-
  • ++
  • + One number +
  • +
+@@ -163,7 +187,7 @@ const ResetPassword = () => { + type="password" + placeholder="Confirm Password" + value={formData.confirmPassword} +- onChange={handleInputChange('confirmPassword')} ++ onChange={handleInputChange("confirmPassword")} + /> + + {/* Password Match Indicator */} +@@ -183,7 +207,7 @@ const ResetPassword = () => { + disabled={isLoading || !isFormValid} + className="w-full h-12 px-[10px] py-[14px] bg-[#3B82F6] hover:bg-[#2663C7] [box-shadow:0px_4px_0px_0px_#2663C7] text-white font-medium rounded-lg transition-colors mt-3" + > +- {isLoading ? 'Resetting...' : 'Reset Password'} ++ {isLoading ? "Resetting..." : "Reset Password"} + + +
+@@ -191,6 +215,6 @@ const ResetPassword = () => { +
+ + ); +-} ++}; + + export default ResetPassword; +diff --git a/frontend/app/auth/signin/page.tsx b/frontend/app/auth/signin/page.tsx +index 3c1e3ea..6b87ede 100644 +--- a/frontend/app/auth/signin/page.tsx ++++ b/frontend/app/auth/signin/page.tsx +@@ -1,35 +1,36 @@ +-'use client'; +- +-import React, { useState } from 'react'; +-import { useRouter } from 'next/navigation'; +-import Link from 'next/link'; +-import Input from '@/components/ui/Input'; +-import Button from '@/components/ui/Button'; +-import { Wallet } from 'lucide-react'; +-import Image from 'next/image'; +-import ErrorBoundary from '@/components/error/ErrorBoundary'; +-import { useToast } from '@/components/ui/ToastProvider'; +-import { useStellarWalletAuth } from '@/hooks/useStellarWalletAuth'; +-import { useAuth } from '@/hooks/useAuth'; +-import { WalletType } from '@/lib/stellar/types'; +-import WalletModal, { WalletType as ModalWalletType } from '@/components/ui/WalletModal'; +-import { useWalletModal } from '@/hooks/useWalletModal'; ++"use client"; ++ ++import React, { useState } from "react"; ++import { useRouter } from "next/navigation"; ++import Link from "next/link"; ++import Input from "@/components/ui/Input"; ++import Button from "@/components/ui/Button"; ++import { Wallet } from "lucide-react"; ++import Image from "next/image"; ++import ErrorBoundary from "@/components/error/ErrorBoundary"; ++import { useToast } from "@/components/ui/ToastProvider"; ++import { useStellarWalletAuth } from "@/hooks/useStellarWalletAuth"; ++import { useAuth } from "@/hooks/useAuth"; ++import { WalletType } from "@/lib/stellar/types"; ++import WalletModal, { ++ WalletType as ModalWalletType, ++} from "@/components/ui/WalletModal"; ++import { useWalletModal } from "@/hooks/useWalletModal"; + + const SignInPage = () => { + const router = useRouter(); + const { showSuccess, showError, showInfo, showWarning } = useToast(); +- const { +- isConnecting, +- isSigning, +- isLoggingIn, +- connectAndLogin, +- clearError, +- } = useStellarWalletAuth(); ++ const { isConnecting, isSigning, isLoggingIn, connectAndLogin, clearError } = ++ useStellarWalletAuth(); + const { loginSuccess, loginFailure, setLoading } = useAuth(); +- const { isOpen: isWalletModalOpen, openModal: openWalletModal, closeModal: closeWalletModal } = useWalletModal(); ++ const { ++ isOpen: isWalletModalOpen, ++ openModal: openWalletModal, ++ closeModal: closeWalletModal, ++ } = useWalletModal(); + const [formData, setFormData] = useState({ +- username: '', +- password: '' ++ username: "", ++ password: "", + }); + const [isLoading, setIsLoading] = useState(false); + +@@ -39,15 +40,14 @@ const SignInPage = () => { + return emailRegex.test(email); + }; + +- const handleInputChange = (field: string) => ( +- e: React.ChangeEvent +- ) => { +- const value = e.target.value; +- setFormData(prev => ({ +- ...prev, +- [field]: value +- })); +- }; ++ const handleInputChange = ++ (field: string) => (e: React.ChangeEvent) => { ++ const value = e.target.value; ++ setFormData((prev) => ({ ++ ...prev, ++ [field]: value, ++ })); ++ }; + + const handleSignIn = async (e: React.FormEvent) => { + e.preventDefault(); +@@ -56,7 +56,7 @@ const SignInPage = () => { + + // Validate email before submission + if (!validateEmail(formData.username)) { +- showError('Invalid Email', 'Please enter a valid email address'); ++ showError("Invalid Email", "Please enter a valid email address"); + setIsLoading(false); + setLoading(false); + return; +@@ -64,28 +64,28 @@ const SignInPage = () => { + + // Check if fields are empty + if (!formData.username.trim()) { +- showError('Email Required', 'Please enter your email address'); ++ showError("Email Required", "Please enter your email address"); + setIsLoading(false); + setLoading(false); + return; + } + + if (!formData.password.trim()) { +- showError('Password Required', 'Please enter your password'); ++ showError("Password Required", "Please enter your password"); + setIsLoading(false); + setLoading(false); + return; + } + + try { +- const response = await fetch('http://localhost:3000/auth/signIn', { +- method: 'POST', ++ const response = await fetch("http://localhost:3000/auth/signIn", { ++ method: "POST", + headers: { +- 'Content-Type': 'application/json', ++ "Content-Type": "application/json", + }, + body: JSON.stringify({ + email: formData.username, +- password: formData.password ++ password: formData.password, + }), + }); + +@@ -95,32 +95,33 @@ const SignInPage = () => { + const errorData = await response.json(); + // Handle specific error messages from server + if (response.status === 401) { +- const errorMsg = 'Invalid email or password.'; +- showError('Login Failed', errorMsg); ++ const errorMsg = "Invalid email or password."; ++ showError("Login Failed", errorMsg); + loginFailure(errorMsg); + setIsLoading(false); + setLoading(false); + } else if (response.status === 404) { +- const errorMsg = 'Account not found.'; +- showError('Account Not Found', errorMsg); ++ const errorMsg = "Account not found."; ++ showError("Account Not Found", errorMsg); + loginFailure(errorMsg); + setIsLoading(false); + setLoading(false); + } else if (response.status === 400) { +- const errorMsg = errorData.message || 'Invalid input.'; +- showError('Invalid Input', errorMsg); ++ const errorMsg = errorData.message || "Invalid input."; ++ showError("Invalid Input", errorMsg); + loginFailure(errorMsg); + setIsLoading(false); + setLoading(false); + } else if (response.status >= 500) { +- const errorMsg = 'Server error. Please try again later.'; +- showError('Server Error', errorMsg); ++ const errorMsg = "Server error. Please try again later."; ++ showError("Server Error", errorMsg); + loginFailure(errorMsg); + setIsLoading(false); + setLoading(false); + } else { +- const errorMsg = errorData.message || 'Login failed. Please try again.'; +- showError('Login Failed', errorMsg); ++ const errorMsg = ++ errorData.message || "Login failed. Please try again."; ++ showError("Login Failed", errorMsg); + loginFailure(errorMsg); + setIsLoading(false); + setLoading(false); +@@ -128,20 +129,20 @@ const SignInPage = () => { + } catch { + // If response isn't JSON, use status text or default message + if (response.status === 401) { +- const errorMsg = 'Invalid email or password.'; +- showError('Login Failed', errorMsg); ++ const errorMsg = "Invalid email or password."; ++ showError("Login Failed", errorMsg); + loginFailure(errorMsg); + setIsLoading(false); + setLoading(false); + } else if (response.status === 404) { +- const errorMsg = 'Account not found.'; +- showError('Account Not Found', errorMsg); ++ const errorMsg = "Account not found."; ++ showError("Account Not Found", errorMsg); + loginFailure(errorMsg); + setIsLoading(false); + setLoading(false); + } else { +- const errorMsg = `Login failed: ${response.statusText || 'Please try again.'}`; +- showError('Login Failed', errorMsg); ++ const errorMsg = `Login failed: ${response.statusText || "Please try again."}`; ++ showError("Login Failed", errorMsg); + loginFailure(errorMsg); + setIsLoading(false); + setLoading(false); +@@ -157,35 +158,35 @@ const SignInPage = () => { + + if (data.accessToken) { + // Store token in localStorage +- localStorage.setItem('accessToken', data.accessToken); +- ++ localStorage.setItem("accessToken", data.accessToken); ++ + // Update Redux state + const user = { + id: data.user?.id || formData.username, + email: formData.username, +- username: data.user?.username || formData.username.split('@')[0], ++ username: data.user?.username || formData.username.split("@")[0], + }; +- ++ + loginSuccess(user, data.accessToken); +- ++ + // Show success toast +- showSuccess('Login Successful', 'Welcome back!'); ++ showSuccess("Login Successful", "Welcome back!"); + setIsLoading(false); + setLoading(false); +- ++ + // Redirect to dashboard +- router.push('/dashboard'); ++ router.push("/dashboard"); + } else { +- const errorMsg = 'Invalid response from server. Please try again.'; +- showError('Invalid Response', errorMsg); ++ const errorMsg = "Invalid response from server. Please try again."; ++ showError("Invalid Response", errorMsg); + loginFailure(errorMsg); + setIsLoading(false); + setLoading(false); + } + } catch (error) { +- console.error('Sign in error:', error); +- const errorMsg = 'Network error.'; +- showError('Network Error', errorMsg); ++ console.error("Sign in error:", error); ++ const errorMsg = "Network error."; ++ showError("Network Error", errorMsg); + loginFailure(errorMsg); + setIsLoading(false); + setLoading(false); +@@ -196,46 +197,60 @@ const SignInPage = () => { + }; + + const handleGoogleSignIn = () => { +- showInfo('Google Sign-In', 'Redirecting to Google authentication...'); ++ showInfo("Google Sign-In", "Redirecting to Google authentication..."); + window.location.href = "http://localhost:3000/auth/google-authentication"; + }; + + const handleWalletSelect = async (walletType: ModalWalletType) => { + closeWalletModal(); + +- if (walletType !== 'freighter') { +- showInfo('Coming Soon', `${walletType.charAt(0).toUpperCase() + walletType.slice(1)} wallet support is coming soon!`); ++ if (walletType !== "freighter") { ++ showInfo( ++ "Coming Soon", ++ `${walletType.charAt(0).toUpperCase() + walletType.slice(1)} wallet support is coming soon!`, ++ ); + return; + } + + clearError(); + + try { +- await connectAndLogin('freighter' as WalletType); +- showSuccess('Login Successful', 'Welcome back!'); +- router.push('/dashboard'); ++ await connectAndLogin("freighter" as WalletType); ++ showSuccess("Login Successful", "Welcome back!"); ++ router.push("/dashboard"); + } catch (error) { + console.error("Wallet Connection Error:", error); + +- const isErrorWithCode = (e: unknown): e is { code?: string; message?: string } => { +- return typeof e === 'object' && e !== null; ++ const isErrorWithCode = ( ++ e: unknown, ++ ): e is { code?: string; message?: string } => { ++ return typeof e === "object" && e !== null; + }; + if (isErrorWithCode(error)) { +- if (error?.code === 'WALLET_NOT_INSTALLED') { ++ if (error?.code === "WALLET_NOT_INSTALLED") { + showError( +- 'Wallet Not Installed', +- 'Please install Freighter wallet from freighter.app to continue' ++ "Wallet Not Installed", ++ "Please install Freighter wallet from freighter.app to continue", ++ ); ++ } else if (error?.code === "USER_REJECTED") { ++ showWarning("Request Cancelled", "You cancelled the wallet request"); ++ } else if (error?.code === "NONCE_EXPIRED") { ++ showError("Authentication Expired", "Please try again"); ++ } else if (error?.code === "INVALID_SIGNATURE") { ++ showError( ++ "Authentication Failed", ++ "Invalid signature or expired nonce", ++ ); ++ } else if (error?.code === "NETWORK_ERROR") { ++ showError( ++ "Network Error", ++ "Unable to connect to server. Please try again.", + ); +- } else if (error?.code === 'USER_REJECTED') { +- showWarning('Request Cancelled', 'You cancelled the wallet request'); +- } else if (error?.code === 'NONCE_EXPIRED') { +- showError('Authentication Expired', 'Please try again'); +- } else if (error?.code === 'INVALID_SIGNATURE') { +- showError('Authentication Failed', 'Invalid signature or expired nonce'); +- } else if (error?.code === 'NETWORK_ERROR') { +- showError('Network Error', 'Unable to connect to server. Please try again.'); + } else { +- showError('Login Failed', error?.message || 'An unexpected error occurred'); ++ showError( ++ "Login Failed", ++ error?.message || "An unexpected error occurred", ++ ); + } + } + } +@@ -244,120 +259,136 @@ const SignInPage = () => { + return ( + +
+- {/* Header */} +-
+-
+- {/* Main Content */} +-
+-
+-
+-
+- +- Home +- ++ {/* Header */} ++
++ {/* Main Content */} ++
++
++
++
++ ++ Home ++ ++
++

++ Your journey continues ++

+
+-

+- Your journey continues +-

+-
+ +- {/* Sign-in Form */} +-
+- +- +- +- +- {/* Forgot Password Link */} +-
+- ++ ++ ++ ++ ++ {/* Forgot Password Link */} ++
++ ++ Forgot Password? ++ ++
++ ++ {/* Sign In Button */} ++ ++ ++ ++ {/* Sign Up Link */} ++
++ ++ Don't have an account?{" "} ++ ++ +- Forgot Password? ++ Sign up + +
+ +- {/* Sign In Button */} +- +- +- +- {/* Sign Up Link */} +-
+- Don't have an account? +- +- Sign up +- +-
++ {/* Divider */} ++
Or
+ +- {/* Divider */} +-
Or
+- +- {/* Social Login Buttons */} +-
+- +- +- +-
++ {/* Social Login Buttons */} ++
++ + +- {/* Terms and Privacy */} +-
+- By signing in to Mind Block, you agree to our{' '} +- +- Terms +- +- {' '}and{' '} +- +- Privacy Policy +- ++ ++
++ ++ {/* Terms and Privacy */} ++
++ By signing in to Mind Block, you agree to our{" "} ++ ++ Terms ++ {" "} ++ and{" "} ++ ++ Privacy Policy ++ ++
+
+
+
+-
+ { + const router = useRouter(); + const { showSuccess, showError, showWarning, showInfo } = useToast(); +- const { +- isConnecting, +- isSigning, +- isLoggingIn, +- connectAndLogin, +- clearError, +- } = useStellarWalletAuth(); ++ const { isConnecting, isSigning, isLoggingIn, connectAndLogin, clearError } = ++ useStellarWalletAuth(); + const [formData, setFormData] = useState({ + username: "", + fullName: "", +@@ -127,16 +121,13 @@ const SignUpPage = () => { + + console.log("Sending request with data:", requestBody); // Debug log + +- const response = await fetch( +- "http://localhost:3000/users", +- { +- method: "POST", +- headers: { +- "Content-Type": "application/json", +- }, +- body: JSON.stringify(requestBody), ++ const response = await fetch("http://localhost:3000/users", { ++ method: "POST", ++ headers: { ++ "Content-Type": "application/json", + }, +- ); ++ body: JSON.stringify(requestBody), ++ }); + + // Log response details for debugging + console.log("Response status:", response.status); +@@ -263,8 +254,7 @@ const SignUpPage = () => { + + const handleGoogleSignUp = () => { + showInfo("Google Sign-Up", "Redirecting to Google authentication..."); +- window.location.href = +- "http://localhost:3000/auth/google-authentication"; ++ window.location.href = "http://localhost:3000/auth/google-authentication"; + }; + + const handleWalletConnect = async () => { +@@ -278,36 +268,40 @@ const SignUpPage = () => { + router.push("/dashboard"); + } catch (error) { + console.error("Wallet Connection Error:", error); +- const isErrorWithCode = (e: unknown): e is { code?: string; message?: string } => { +- return typeof e === 'object' && e !== null; ++ const isErrorWithCode = ( ++ e: unknown, ++ ): e is { code?: string; message?: string } => { ++ return typeof e === "object" && e !== null; + }; +- ++ + if (isErrorWithCode(error)) { + if (error.code === "WALLET_NOT_INSTALLED") { +- +- showError( +- "Wallet Not Installed", +- "Please install Freighter wallet from freighter.app to continue", +- ); +- } else if (error?.code === "USER_REJECTED") { +- showWarning("Request Cancelled", "You cancelled the wallet request"); +- } else if (error?.code === "NONCE_EXPIRED") { +- showError("Authentication Expired", "Please try again"); +- } else if (error?.code === "INVALID_SIGNATURE") { +- showError("Authentication Failed", "Invalid signature or expired nonce"); +- } else if (error?.code === "NETWORK_ERROR") { +- showError( +- "Network Error", +- "Unable to connect to server. Please try again.", +- ); +- } else { +- showError( +- "Login Failed", +- error?.message || "An unexpected error occurred", +- ); ++ showError( ++ "Wallet Not Installed", ++ "Please install Freighter wallet from freighter.app to continue", ++ ); ++ } else if (error?.code === "USER_REJECTED") { ++ showWarning("Request Cancelled", "You cancelled the wallet request"); ++ } else if (error?.code === "NONCE_EXPIRED") { ++ showError("Authentication Expired", "Please try again"); ++ } else if (error?.code === "INVALID_SIGNATURE") { ++ showError( ++ "Authentication Failed", ++ "Invalid signature or expired nonce", ++ ); ++ } else if (error?.code === "NETWORK_ERROR") { ++ showError( ++ "Network Error", ++ "Unable to connect to server. Please try again.", ++ ); ++ } else { ++ showError( ++ "Login Failed", ++ error?.message || "An unexpected error occurred", ++ ); ++ } + } + } +- } + }; + + return ( +@@ -460,4 +454,4 @@ const SignUpPage = () => { + ); + }; + +-export default SignUpPage; +\ No newline at end of file ++export default SignUpPage; +diff --git a/frontend/app/coding/page.tsx b/frontend/app/coding/page.tsx +index 09f7d94..213937f 100644 +--- a/frontend/app/coding/page.tsx ++++ b/frontend/app/coding/page.tsx +@@ -1,21 +1,21 @@ +-'use client'; ++"use client"; + +-import { useState } from 'react'; +-import CodeEditorPanel from '@/components/coding/CodeEditorPanel'; +-import { Language } from '@/lib/types/editor'; ++import { useState } from "react"; ++import CodeEditorPanel from "@/components/coding/CodeEditorPanel"; ++import { Language } from "@/lib/types/editor"; + + export default function CodingPage() { +- const [code, setCode] = useState(''); +- const [language, setLanguage] = useState('javascript'); ++ const [code, setCode] = useState(""); ++ const [language, setLanguage] = useState("javascript"); + + const handleCodeChange = (newCode: string) => { + setCode(newCode); +- console.log('Code updated:', newCode); ++ console.log("Code updated:", newCode); + }; + + const handleLanguageChange = (newLanguage: Language) => { + setLanguage(newLanguage); +- console.log('Language changed to:', newLanguage); ++ console.log("Language changed to:", newLanguage); + }; + + return ( +@@ -25,7 +25,7 @@ export default function CodingPage() { +

Code Challenge

+

Write your solution below

+
+- ++ +
+ { + const stats = data?.stats; + const streak = stats?.streak ?? 0; + const points = stats?.points ?? 0; +- const dailyQuestProgress = stats?.dailyQuestProgress ?? { completed: 0, total: 5 }; ++ const dailyQuestProgress = stats?.dailyQuestProgress ?? { ++ completed: 0, ++ total: 5, ++ }; + + // Get categories from context or use empty array + const categories = data?.categories ?? []; + + return ( +
+-
+-
+-
+-
+-
+- Mind Block +-
+- +- mind block +- +-
+- +-
+-
+- +- +- {streak} Day Streak +- +-
+-
+- +- +- {points} Points ++
++
++
++
++
++ Mind Block ++
++ ++ mind block + +
+-
+- ++ ++
++
++ ++ ++ {streak} Day Streak ++ ++
++
++ ++ ++ {points} Points ++ ++
++
++ ++
+
+
+
+-
+
+ +
+diff --git a/frontend/app/friends/add/page.tsx b/frontend/app/friends/add/page.tsx +index 52e9369..10e4781 100644 +--- a/frontend/app/friends/add/page.tsx ++++ b/frontend/app/friends/add/page.tsx +@@ -5,7 +5,9 @@ export default function AddFriendsPage() { +

+ Placeholder Route +

+-

Add Friends

++

++ Add Friends ++

+

+ Friend discovery will be added in a future backend-connected task. +

+diff --git a/frontend/app/friends/friends-content.tsx b/frontend/app/friends/friends-content.tsx +index b9eb91f..49628c1 100644 +--- a/frontend/app/friends/friends-content.tsx ++++ b/frontend/app/friends/friends-content.tsx +@@ -43,7 +43,7 @@ export default function FriendsPageContent() { + + const friends = useMemo( + () => (activeTab === "following" ? followingFriends : followerFriends), +- [activeTab] ++ [activeTab], + ); + + const handleTabChange = (tab: FriendsTab) => { +@@ -64,7 +64,9 @@ export default function FriendsPageContent() { + key={friend.id} + avatar={friend.avatar} + username={friend.username} +- onClick={() => router.push(`/profile?user=${friend.username.toLowerCase()}`)} ++ onClick={() => ++ router.push(`/profile?user=${friend.username.toLowerCase()}`) ++ } + /> + ))} +
+diff --git a/frontend/app/global-error.tsx b/frontend/app/global-error.tsx +index b2bcdcd..6257331 100644 +--- a/frontend/app/global-error.tsx ++++ b/frontend/app/global-error.tsx +@@ -1,7 +1,7 @@ +-'use client'; ++"use client"; + +-import { useEffect } from 'react'; +-import GenericErrorPage from '../components/error/GenericErrorPage'; ++import { useEffect } from "react"; ++import GenericErrorPage from "../components/error/GenericErrorPage"; + + export default function GlobalError({ + error, +@@ -10,17 +10,17 @@ export default function GlobalError({ + }) { + useEffect(() => { + // Log the error to an error reporting service +- console.error('Global error:', error); ++ console.error("Global error:", error); + }, [error]); + + return ( + + +- + + + ); +-} +\ No newline at end of file ++} +diff --git a/frontend/app/layout.tsx b/frontend/app/layout.tsx +index 5f11f56..f200050 100644 +--- a/frontend/app/layout.tsx ++++ b/frontend/app/layout.tsx +@@ -33,15 +33,13 @@ export default function RootLayout({ + + + +- +- +- +- +- {children} +- +- +- +- ++ ++ ++ ++ {children} ++ ++ ++ + + + +diff --git a/frontend/app/not-found.tsx b/frontend/app/not-found.tsx +index 1459748..8b3ea57 100644 +--- a/frontend/app/not-found.tsx ++++ b/frontend/app/not-found.tsx +@@ -1,4 +1,4 @@ +-import NotFoundPage from '../components/error/NotFoundPage'; ++import NotFoundPage from "../components/error/NotFoundPage"; + + export default function NotFound() { + return ( +@@ -8,4 +8,4 @@ export default function NotFound() { + + + ); +-} +\ No newline at end of file ++} +diff --git a/frontend/app/onboarding/OnboardingContext.tsx b/frontend/app/onboarding/OnboardingContext.tsx +index 6cb90a3..598221e 100644 +--- a/frontend/app/onboarding/OnboardingContext.tsx ++++ b/frontend/app/onboarding/OnboardingContext.tsx +@@ -1,54 +1,62 @@ +-'use client'; +- +-import React, { createContext, useContext, useState, ReactNode } from 'react'; +- +-interface OnboardingData { +- challengeLevel: string; +- challengeTypes: string[]; +- referralSource: string; +- ageGroup: string; +-} +- +-interface OnboardingContextType { +- data: OnboardingData; +- updateData: (section: K, payload: OnboardingData[K]) => void; +- resetData: () => void; +-} +- +-const defaultData: OnboardingData = { +- challengeLevel: '', +- challengeTypes: [], +- referralSource: '', +- ageGroup: '', +-}; +- +-const OnboardingContext = createContext(undefined); +- +-export const OnboardingProvider = ({ children }: { children: ReactNode }) => { +- const [data, setData] = useState(defaultData); +- +- const updateData = (section: K, payload: OnboardingData[K]) => { +- setData((prev) => ({ +- ...prev, +- [section]: payload, +- })); +- }; +- +- const resetData = () => { +- setData(defaultData); +- }; +- +- return ( +- +- {children} +- +- ); +-}; +- +-export const useOnboarding = () => { +- const context = useContext(OnboardingContext); +- if (!context) { +- throw new Error('useOnboarding must be used within an OnboardingProvider'); +- } +- return context; +-}; +\ No newline at end of file ++"use client"; ++ ++import React, { createContext, useContext, useState, ReactNode } from "react"; ++ ++interface OnboardingData { ++ challengeLevel: string; ++ challengeTypes: string[]; ++ referralSource: string; ++ ageGroup: string; ++} ++ ++interface OnboardingContextType { ++ data: OnboardingData; ++ updateData: ( ++ section: K, ++ payload: OnboardingData[K], ++ ) => void; ++ resetData: () => void; ++} ++ ++const defaultData: OnboardingData = { ++ challengeLevel: "", ++ challengeTypes: [], ++ referralSource: "", ++ ageGroup: "", ++}; ++ ++const OnboardingContext = createContext( ++ undefined, ++); ++ ++export const OnboardingProvider = ({ children }: { children: ReactNode }) => { ++ const [data, setData] = useState(defaultData); ++ ++ const updateData = ( ++ section: K, ++ payload: OnboardingData[K], ++ ) => { ++ setData((prev) => ({ ++ ...prev, ++ [section]: payload, ++ })); ++ }; ++ ++ const resetData = () => { ++ setData(defaultData); ++ }; ++ ++ return ( ++ ++ {children} ++ ++ ); ++}; ++ ++export const useOnboarding = () => { ++ const context = useContext(OnboardingContext); ++ if (!context) { ++ throw new Error("useOnboarding must be used within an OnboardingProvider"); ++ } ++ return context; ++}; +diff --git a/frontend/app/onboarding/additional-info/page.tsx b/frontend/app/onboarding/additional-info/page.tsx +index c2ed59a..0840d12 100644 +--- a/frontend/app/onboarding/additional-info/page.tsx ++++ b/frontend/app/onboarding/additional-info/page.tsx +@@ -1,361 +1,412 @@ +-'use client'; +- +-import React, { useState } from 'react'; +-import { useRouter } from 'next/navigation'; +-import Button from '@/components/Button'; +-import { useOnboarding } from '../OnboardingContext'; +-import Image from 'next/image'; +-import { useUpdateUserProfile } from '@/hooks/useUpdateUserProfile'; +-import { useAuth } from '@/hooks/useAuth'; +-import { mapChallengeLevel, mapChallengeType, mapReferralSource, mapAgeGroup } from '@/lib/utils/onboardingMapper'; +- +-// Step 3a: How did you hear about Block Mind? (Selection) +-const referralSources = [ +- 'Google Search', +- 'X (formerly called Twitter)', +- 'Facebook / Instagram', +- 'Friends / family', +- 'Play Store', +- 'App Store', +- 'News / article / blog', +- 'Youtube', +- 'Others' +-]; +- +-// Step 3b: How old are you? (Age Range Selection) +-const ageRanges = [ +- 'From 10 to 17 years old', +- '18 to 24 years old', +- '25 to 34 years old', +- '35 to 44 years old', +- '45 to 54 years old', +- '55 to 64 years old', +- '65+' +-]; +- +-export default function AdditionalInfoPage() { +- const router = useRouter(); +- const { data, updateData, resetData } = useOnboarding(); +- const { updateProfile, isLoading, error, clearError } = useUpdateUserProfile(); +- const { isAuthenticated } = useAuth(); +- const [step, setStep] = useState<'referral' | 'age'>('referral'); +- const [selectedSource, setSelectedSource] = useState(null); +- const [selectedAge, setSelectedAge] = useState(null); +- const [loadingProgress, setLoadingProgress] = useState(0); +- const [showError, setShowError] = useState(false); +- +- const handleSourceSelect = (source: string) => { +- setSelectedSource(source); +- }; +- +- const handleAgeSelect = (age: string) => { +- setSelectedAge(age); +- }; +- +- const handleContinueFromReferral = () => { +- if (selectedSource) { +- updateData('referralSource', selectedSource); +- setStep('age'); +- } +- }; +- +- const handleContinueFromAge = async () => { +- if (!selectedAge) return; +- +- // Check authentication +- if (!isAuthenticated) { +- setShowError(true); +- return; +- } +- +- updateData('ageGroup', selectedAge); +- +- // Prepare data for API with proper enum mapping +- const profileData = { +- challengeLevel: mapChallengeLevel(data.challengeLevel), +- challengeTypes: data.challengeTypes.map(mapChallengeType), +- referralSource: mapReferralSource(selectedSource || data.referralSource), +- ageGroup: mapAgeGroup(selectedAge), +- }; +- +- try { +- await updateProfile(profileData); +- +- // Reset onboarding data after successful save +- resetData(); +- +- // Redirect to dashboard +- router.push('/dashboard'); +- } catch (err) { +- setShowError(true); +- } +- }; +- +- const handleRetry = () => { +- setShowError(false); +- clearError(); +- }; +- +- // Handle loading animation when submitting +- React.useEffect(() => { +- if (!isLoading) { +- setLoadingProgress(0); +- return; +- } +- +- const interval = setInterval(() => { +- setLoadingProgress(prev => { +- if (prev >= 90) { +- return 90; // Stop at 90% until actual completion +- } +- return prev + 2; +- }); +- }, 50); +- +- return () => { +- clearInterval(interval); +- }; +- }, [isLoading]); +- +- // Complete progress bar on success +- React.useEffect(() => { +- if (!isLoading && loadingProgress > 0 && !error) { +- setLoadingProgress(100); +- } +- }, [isLoading, loadingProgress, error]); +- +- // Loading screen with animated progress +- if (isLoading) { +- return ( +-
+- {/* Puzzle Icon */} +-
+- Loading +-
+- +- {/* Message Card */} +-
+- Preparing your account... +-
+- +- {/* Animated Progress Bar */} +-
+-
+-
+-
+- {loadingProgress}% +-
+-
+- ); +- } +- +- // Error screen +- if (showError) { +- return ( +-
+- {/* Error Icon */} +-
+-
+- +- +- +- +- +-
+-
+- +- {/* Error Message */} +-
+-

Something went wrong

+-

+- {error || 'Unable to save your profile. Please try again.'} +-

+-
+- +- {/* Retry Button */} +-
+- +- +-
+-
+- ); +- } +- +- // Step 3a: Referral Source Selection Screen +- if (step === 'referral') { +- return ( +-
+- {/* Top Navigation Bar */} +-
+- +- +- {/* Progress Bar - White track */} +-
+-
+-
+- +-
+-
+- +- {/* Header with Puzzle Icon INLINE with Title */} +-
+- MindBlock +-
+- How do you hear about Block Mind? +-
+-
+- +- {/* Selection Cards - Increased gap */} +-
+- {referralSources.map((source) => ( +- +- ))} +-
+- +-
+- +-
+- +- {/* Background Ambience */} +-
+-
+-
+-
+-
+- ); +- } +- +- // Step 3b: Age Range Selection Screen +- return ( +-
+- {/* Top Navigation Bar */} +-
+- +- +- {/* Progress Bar - White track */} +-
+-
+-
+- +-
+-
+- +- {/* Header with Puzzle Icon INLINE with Title */} +-
+- MindBlock +-
+- How old are you? +-
+-
+- +- {/* Age Range Selection Cards - Increased gap */} +-
+- {ageRanges.map((age) => ( +- +- ))} +-
+- +-
+- +-
+- +- {/* Background Ambience */} +-
+-
+-
+-
+-
+- ); +-} +\ No newline at end of file ++"use client"; ++ ++import React, { useState } from "react"; ++import { useRouter } from "next/navigation"; ++import Button from "@/components/Button"; ++import { useOnboarding } from "../OnboardingContext"; ++import Image from "next/image"; ++import { useUpdateUserProfile } from "@/hooks/useUpdateUserProfile"; ++import { useAuth } from "@/hooks/useAuth"; ++import { ++ mapChallengeLevel, ++ mapChallengeType, ++ mapReferralSource, ++ mapAgeGroup, ++} from "@/lib/utils/onboardingMapper"; ++ ++// Step 3a: How did you hear about Block Mind? (Selection) ++const referralSources = [ ++ "Google Search", ++ "X (formerly called Twitter)", ++ "Facebook / Instagram", ++ "Friends / family", ++ "Play Store", ++ "App Store", ++ "News / article / blog", ++ "Youtube", ++ "Others", ++]; ++ ++// Step 3b: How old are you? (Age Range Selection) ++const ageRanges = [ ++ "From 10 to 17 years old", ++ "18 to 24 years old", ++ "25 to 34 years old", ++ "35 to 44 years old", ++ "45 to 54 years old", ++ "55 to 64 years old", ++ "65+", ++]; ++ ++export default function AdditionalInfoPage() { ++ const router = useRouter(); ++ const { data, updateData, resetData } = useOnboarding(); ++ const { updateProfile, isLoading, error, clearError } = ++ useUpdateUserProfile(); ++ const { isAuthenticated } = useAuth(); ++ const [step, setStep] = useState<"referral" | "age">("referral"); ++ const [selectedSource, setSelectedSource] = useState(null); ++ const [selectedAge, setSelectedAge] = useState(null); ++ const [loadingProgress, setLoadingProgress] = useState(0); ++ const [showError, setShowError] = useState(false); ++ ++ const handleSourceSelect = (source: string) => { ++ setSelectedSource(source); ++ }; ++ ++ const handleAgeSelect = (age: string) => { ++ setSelectedAge(age); ++ }; ++ ++ const handleContinueFromReferral = () => { ++ if (selectedSource) { ++ updateData("referralSource", selectedSource); ++ setStep("age"); ++ } ++ }; ++ ++ const handleContinueFromAge = async () => { ++ if (!selectedAge) return; ++ ++ // Check authentication ++ if (!isAuthenticated) { ++ setShowError(true); ++ return; ++ } ++ ++ updateData("ageGroup", selectedAge); ++ ++ // Prepare data for API with proper enum mapping ++ const profileData = { ++ challengeLevel: mapChallengeLevel(data.challengeLevel), ++ challengeTypes: data.challengeTypes.map(mapChallengeType), ++ referralSource: mapReferralSource(selectedSource || data.referralSource), ++ ageGroup: mapAgeGroup(selectedAge), ++ }; ++ ++ try { ++ await updateProfile(profileData); ++ ++ // Reset onboarding data after successful save ++ resetData(); ++ ++ // Redirect to dashboard ++ router.push("/dashboard"); ++ } catch (err) { ++ setShowError(true); ++ } ++ }; ++ ++ const handleRetry = () => { ++ setShowError(false); ++ clearError(); ++ }; ++ ++ // Handle loading animation when submitting ++ React.useEffect(() => { ++ if (!isLoading) { ++ setLoadingProgress(0); ++ return; ++ } ++ ++ const interval = setInterval(() => { ++ setLoadingProgress((prev) => { ++ if (prev >= 90) { ++ return 90; // Stop at 90% until actual completion ++ } ++ return prev + 2; ++ }); ++ }, 50); ++ ++ return () => { ++ clearInterval(interval); ++ }; ++ }, [isLoading]); ++ ++ // Complete progress bar on success ++ React.useEffect(() => { ++ if (!isLoading && loadingProgress > 0 && !error) { ++ setLoadingProgress(100); ++ } ++ }, [isLoading, loadingProgress, error]); ++ ++ // Loading screen with animated progress ++ if (isLoading) { ++ return ( ++
++ {/* Puzzle Icon */} ++
++ Loading ++
++ ++ {/* Message Card */} ++
++ ++ Preparing your account... ++ ++
++ ++ {/* Animated Progress Bar */} ++
++
++
++
++ ++ {loadingProgress}% ++ ++
++
++ ); ++ } ++ ++ // Error screen ++ if (showError) { ++ return ( ++
++ {/* Error Icon */} ++
++
++ ++ ++ ++ ++ ++
++
++ ++ {/* Error Message */} ++
++

++ Something went wrong ++

++

++ {error || "Unable to save your profile. Please try again."} ++

++
++ ++ {/* Retry Button */} ++
++ ++ ++
++
++ ); ++ } ++ ++ // Step 3a: Referral Source Selection Screen ++ if (step === "referral") { ++ return ( ++
++ {/* Top Navigation Bar */} ++
++ ++ ++ {/* Progress Bar - White track */} ++
++
++
++ ++
++
++ ++ {/* Header with Puzzle Icon INLINE with Title */} ++
++ MindBlock ++
++ ++ How do you hear about Block Mind? ++ ++
++
++ ++ {/* Selection Cards - Increased gap */} ++
++ {referralSources.map((source) => ( ++ ++ ))} ++
++ ++
++ ++
++ ++ {/* Background Ambience */} ++
++
++
++
++
++ ); ++ } ++ ++ // Step 3b: Age Range Selection Screen ++ return ( ++
++ {/* Top Navigation Bar */} ++
++ ++ ++ {/* Progress Bar - White track */} ++
++
++
++ ++
++
++ ++ {/* Header with Puzzle Icon INLINE with Title */} ++
++ MindBlock ++
++ ++ How old are you? ++ ++
++
++ ++ {/* Age Range Selection Cards - Increased gap */} ++
++ {ageRanges.map((age) => ( ++ ++ ))} ++
++ ++
++ ++
++ ++ {/* Background Ambience */} ++
++
++
++
++
++ ); ++} +diff --git a/frontend/app/onboarding/challenge-level/page.tsx b/frontend/app/onboarding/challenge-level/page.tsx +index 55a82a8..e99e682 100644 +--- a/frontend/app/onboarding/challenge-level/page.tsx ++++ b/frontend/app/onboarding/challenge-level/page.tsx +@@ -1,114 +1,142 @@ +-'use client'; +- +-import React from 'react'; +-import { useRouter } from 'next/navigation'; +-import Button from '@/components/Button'; +-import { useOnboarding } from '../OnboardingContext'; +-import Image from 'next/image'; +- +-const levels = [ +- { id: 'BEGINNER', label: 'I am a total beginner', icon: '/icon-level-beginner.svg' }, +- { id: 'INTERMEDIATE', label: 'I am intermediate', icon: '/icon-level-intermediate.svg' }, +- { id: 'ADVANCED', label: 'I am advanced', icon: '/icon-level-advanced.svg' }, +- { id: 'EXPERT', label: 'I am an expert', icon: '/icon-level-expert.svg' }, +-]; +- +-export default function ChallengeLevelPage() { +- const router = useRouter(); +- const { data, updateData } = useOnboarding(); +- +- const handleSelect = (levelId: string) => { +- updateData('challengeLevel', levelId); +- }; +- +- const handleContinue = () => { +- if (data.challengeLevel) { +- router.push('/onboarding/challenge-types'); +- } +- }; +- +- return ( +-
+- {/* Top Navigation Bar */} +-
+- +- +- {/* Progress Bar - White track */} +-
+-
+-
+- +-
+-
+- +- {/* Header with Puzzle Icon INLINE with Title */} +-
+- MindBlock +-
+- Choose Challenge level that matches your skills +-
+-
+- +- {/* Selection Cards - Increased gap */} +-
+- {levels.map((level) => ( +- +- ))} +-
+- +-
+- +-
+- +- {/* Background Ambience */} +-
+-
+-
+-
+-
+- ); +-} ++"use client"; ++ ++import React from "react"; ++import { useRouter } from "next/navigation"; ++import Button from "@/components/Button"; ++import { useOnboarding } from "../OnboardingContext"; ++import Image from "next/image"; ++ ++const levels = [ ++ { ++ id: "BEGINNER", ++ label: "I am a total beginner", ++ icon: "/icon-level-beginner.svg", ++ }, ++ { ++ id: "INTERMEDIATE", ++ label: "I am intermediate", ++ icon: "/icon-level-intermediate.svg", ++ }, ++ { id: "ADVANCED", label: "I am advanced", icon: "/icon-level-advanced.svg" }, ++ { id: "EXPERT", label: "I am an expert", icon: "/icon-level-expert.svg" }, ++]; ++ ++export default function ChallengeLevelPage() { ++ const router = useRouter(); ++ const { data, updateData } = useOnboarding(); ++ ++ const handleSelect = (levelId: string) => { ++ updateData("challengeLevel", levelId); ++ }; ++ ++ const handleContinue = () => { ++ if (data.challengeLevel) { ++ router.push("/onboarding/challenge-types"); ++ } ++ }; ++ ++ return ( ++
++ {/* Top Navigation Bar */} ++
++ ++ ++ {/* Progress Bar - White track */} ++
++
++
++ ++
++
++ ++ {/* Header with Puzzle Icon INLINE with Title */} ++
++ MindBlock ++
++ ++ Choose Challenge level that matches your skills ++ ++
++
++ ++ {/* Selection Cards - Increased gap */} ++
++ {levels.map((level) => ( ++ ++ ))} ++
++ ++
++ ++
++ ++ {/* Background Ambience */} ++
++
++
++
++
++ ); ++} +diff --git a/frontend/app/onboarding/challenge-types/page.tsx b/frontend/app/onboarding/challenge-types/page.tsx +index 0b32133..e28a766 100644 +--- a/frontend/app/onboarding/challenge-types/page.tsx ++++ b/frontend/app/onboarding/challenge-types/page.tsx +@@ -1,121 +1,142 @@ +-'use client'; +- +-import React from 'react'; +-import { useRouter } from 'next/navigation'; +-import Button from '@/components/Button'; +-import Image from 'next/image'; +-import { useOnboarding } from '../OnboardingContext'; +- +-const types = [ +- { id: 'CODING', label: 'Coding Challenges', icon: '/icon-code.svg' }, +- { id: 'LOGIC', label: 'Logic Puzzle', icon: '/icon-puzzle.svg' }, +- { id: 'BLOCKCHAIN', label: 'Blockchain', icon: '/icon-blockchain.svg' }, +-]; +- +-export default function ChallengeTypesPage() { +- const router = useRouter(); +- const { data, updateData } = useOnboarding(); +- +- const handleToggle = (typeId: string) => { +- const current = data.challengeTypes; +- if (current.includes(typeId)) { +- updateData('challengeTypes', current.filter(id => id !== typeId)); +- } else { +- updateData('challengeTypes', [...current, typeId]); +- } +- }; +- +- const handleContinue = () => { +- if (data.challengeTypes.length > 0) { +- router.push('/onboarding/additional-info'); +- } +- }; +- +- return ( +-
+- {/* Top Navigation Bar */} +-
+- +- +- {/* Progress Bar - White track */} +-
+-
+-
+- +-
+-
+- +- {/* Header with Puzzle Icon INLINE with Title */} +-
+- MindBlock +-
+- Choose the Challenge types (select at least one) +-
+-
+- +- {/* Selection Cards - Increased gap */} +-
+- {types.map((type) => { +- const isSelected = data.challengeTypes.includes(type.id); +- return ( +- +- ); +- })} +-
+- +-
+- +-
+- +- {/* Background Ambience */} +-
+-
+-
+-
+-
+- ); +-} ++"use client"; ++ ++import React from "react"; ++import { useRouter } from "next/navigation"; ++import Button from "@/components/Button"; ++import Image from "next/image"; ++import { useOnboarding } from "../OnboardingContext"; ++ ++const types = [ ++ { id: "CODING", label: "Coding Challenges", icon: "/icon-code.svg" }, ++ { id: "LOGIC", label: "Logic Puzzle", icon: "/icon-puzzle.svg" }, ++ { id: "BLOCKCHAIN", label: "Blockchain", icon: "/icon-blockchain.svg" }, ++]; ++ ++export default function ChallengeTypesPage() { ++ const router = useRouter(); ++ const { data, updateData } = useOnboarding(); ++ ++ const handleToggle = (typeId: string) => { ++ const current = data.challengeTypes; ++ if (current.includes(typeId)) { ++ updateData( ++ "challengeTypes", ++ current.filter((id) => id !== typeId), ++ ); ++ } else { ++ updateData("challengeTypes", [...current, typeId]); ++ } ++ }; ++ ++ const handleContinue = () => { ++ if (data.challengeTypes.length > 0) { ++ router.push("/onboarding/additional-info"); ++ } ++ }; ++ ++ return ( ++
++ {/* Top Navigation Bar */} ++
++ ++ ++ {/* Progress Bar - White track */} ++
++
++
++ ++
++
++ ++ {/* Header with Puzzle Icon INLINE with Title */} ++
++ MindBlock ++
++ ++ Choose the Challenge types (select at least one) ++ ++
++
++ ++ {/* Selection Cards - Increased gap */} ++
++ {types.map((type) => { ++ const isSelected = data.challengeTypes.includes(type.id); ++ return ( ++ ++ ); ++ })} ++
++ ++
++ ++
++ ++ {/* Background Ambience */} ++
++
++
++
++
++ ); ++} +diff --git a/frontend/app/onboarding/layout.tsx b/frontend/app/onboarding/layout.tsx +index 2f2bca1..938c308 100644 +--- a/frontend/app/onboarding/layout.tsx ++++ b/frontend/app/onboarding/layout.tsx +@@ -1,15 +1,11 @@ +-'use client'; +- +-import { OnboardingProvider } from './OnboardingContext'; +- +-export default function OnboardingRootLayout({ +- children, +-}: { +- children: React.ReactNode; +-}) { +- return ( +- +- {children} +- +- ); +-} ++"use client"; ++ ++import { OnboardingProvider } from "./OnboardingContext"; ++ ++export default function OnboardingRootLayout({ ++ children, ++}: { ++ children: React.ReactNode; ++}) { ++ return {children}; ++} +diff --git a/frontend/app/onboarding/page.tsx b/frontend/app/onboarding/page.tsx +index 7e09f61..07b23d0 100644 +--- a/frontend/app/onboarding/page.tsx ++++ b/frontend/app/onboarding/page.tsx +@@ -1,18 +1,18 @@ +-'use client'; +- +-import { useEffect } from 'react'; +-import { useRouter } from 'next/navigation'; +- +-export default function OnboardingPage() { +- const router = useRouter(); +- +- useEffect(() => { +- router.replace('/onboarding/challenge-level'); +- }, [router]); +- +- return ( +-
+- {/* Optional loader while redirecting */} +-
+- ); +-} ++"use client"; ++ ++import { useEffect } from "react"; ++import { useRouter } from "next/navigation"; ++ ++export default function OnboardingPage() { ++ const router = useRouter(); ++ ++ useEffect(() => { ++ router.replace("/onboarding/challenge-level"); ++ }, [router]); ++ ++ return ( ++
++ {/* Optional loader while redirecting */} ++
++ ); ++} +diff --git a/frontend/app/page.tsx b/frontend/app/page.tsx +index 74076e0..d0dd7b9 100644 +--- a/frontend/app/page.tsx ++++ b/frontend/app/page.tsx +@@ -1,9 +1,9 @@ +-"use client" ++"use client"; + import React from "react"; + import { useRouter } from "next/navigation"; + import Image from "next/image"; +-import { motion } from 'framer-motion'; +-import { fadeIn, slideUp } from '../lib/animations/variants'; ++import { motion } from "framer-motion"; ++import { fadeIn, slideUp } from "../lib/animations/variants"; + + const MindBlockLanding = () => { + const router = useRouter(); +diff --git a/frontend/app/profile/page.tsx b/frontend/app/profile/page.tsx +index 693de2e..a6bc093 100644 +--- a/frontend/app/profile/page.tsx ++++ b/frontend/app/profile/page.tsx +@@ -1,11 +1,9 @@ + "use client"; + //_________________________________________________________// + +- // This Page will be adjusted after the wave ++// This Page will be adjusted after the wave + //_________________________________________________________// + +- +- + import Button from "@/components/Button"; + import { ProfileHeader } from "@/components/profile/ProfileHeader"; + import { ProfileOverview } from "@/components/profile/ProfileOverview"; +@@ -70,7 +68,6 @@ export default function ProfilePage() { + + {/* Main Content */} +
+- + {/* Left Column - Profile Info & Overview */} +
+ {/* Profile Header Card */} +@@ -118,4 +115,3 @@ export default function ProfilePage() { +
+ ); + } +- +diff --git a/frontend/app/puzzle-list/page.tsx b/frontend/app/puzzle-list/page.tsx +index c130fb9..f086380 100644 +--- a/frontend/app/puzzle-list/page.tsx ++++ b/frontend/app/puzzle-list/page.tsx +@@ -1,41 +1,49 @@ +-'use client'; ++"use client"; + +-import React, { useCallback, useEffect, useState, useMemo } from 'react'; +-import { useRouter, useSearchParams } from 'next/navigation'; +-import FilterBar from '@/components/puzzles/FilterBar'; +-import { usePuzzles } from '@/hooks/usePuzzles'; +-import type { PuzzleDifficulty, PuzzleFilters } from '@/lib/types/puzzles'; +-import { Puzzle as PuzzleIcon, Clock, Zap } from 'lucide-react'; +-import Pagination from '@/components/ui/Pagination'; ++import React, { useCallback, useEffect, useState, useMemo } from "react"; ++import { useRouter, useSearchParams } from "next/navigation"; ++import FilterBar from "@/components/puzzles/FilterBar"; ++import { usePuzzles } from "@/hooks/usePuzzles"; ++import type { PuzzleDifficulty, PuzzleFilters } from "@/lib/types/puzzles"; ++import { Puzzle as PuzzleIcon, Clock, Zap } from "lucide-react"; ++import Pagination from "@/components/ui/Pagination"; + + // ─── Helpers ──────────────────────────────────────────────────────────────── + + const DIFFICULTY_COLORS: Record = { +- BEGINNER: 'text-green-400 bg-green-400/10 border-green-400/20', +- INTERMEDIATE: 'text-yellow-400 bg-yellow-400/10 border-yellow-400/20', +- ADVANCED: 'text-orange-400 bg-orange-400/10 border-orange-400/20', +- EXPERT: 'text-red-400 bg-red-400/10 border-red-400/20', ++ BEGINNER: "text-green-400 bg-green-400/10 border-green-400/20", ++ INTERMEDIATE: "text-yellow-400 bg-yellow-400/10 border-yellow-400/20", ++ ADVANCED: "text-orange-400 bg-orange-400/10 border-orange-400/20", ++ EXPERT: "text-red-400 bg-red-400/10 border-red-400/20", + }; + + const VALID_DIFFICULTIES: PuzzleDifficulty[] = [ +- 'ALL', 'BEGINNER', 'INTERMEDIATE', 'ADVANCED', 'EXPERT', ++ "ALL", ++ "BEGINNER", ++ "INTERMEDIATE", ++ "ADVANCED", ++ "EXPERT", + ]; + + const ITEMS_PER_PAGE_OPTIONS = [10, 20, 50]; + + function parseFiltersFromParams(params: URLSearchParams): PuzzleFilters { +- const category = params.get('category') ?? ''; +- const rawDiff = (params.get('difficulty') ?? '').toUpperCase() as PuzzleDifficulty; +- const difficulty = VALID_DIFFICULTIES.includes(rawDiff) ? rawDiff : 'ALL'; ++ const category = params.get("category") ?? ""; ++ const rawDiff = ( ++ params.get("difficulty") ?? "" ++ ).toUpperCase() as PuzzleDifficulty; ++ const difficulty = VALID_DIFFICULTIES.includes(rawDiff) ? rawDiff : "ALL"; + return { categoryId: category, difficulty }; + } + + function parsePaginationFromParams(params: URLSearchParams) { +- const page = parseInt(params.get('page') ?? '1', 10); +- const itemsPerPage = parseInt(params.get('itemsPerPage') ?? '10', 10); ++ const page = parseInt(params.get("page") ?? "1", 10); ++ const itemsPerPage = parseInt(params.get("itemsPerPage") ?? "10", 10); + return { + currentPage: Math.max(1, page), +- itemsPerPage: ITEMS_PER_PAGE_OPTIONS.includes(itemsPerPage) ? itemsPerPage : 10, ++ itemsPerPage: ITEMS_PER_PAGE_OPTIONS.includes(itemsPerPage) ++ ? itemsPerPage ++ : 10, + }; + } + +@@ -57,7 +65,9 @@ const PuzzleCard: React.FC = ({ + type, + timeLimit, + }) => { +- const diffClass = DIFFICULTY_COLORS[difficulty] ?? 'text-slate-400 bg-slate-400/10 border-slate-400/20'; ++ const diffClass = ++ DIFFICULTY_COLORS[difficulty] ?? ++ "text-slate-400 bg-slate-400/10 border-slate-400/20"; + + return ( +
+@@ -93,15 +103,15 @@ const PuzzleCard: React.FC = ({ + // ─── Main Content Component (Uses useSearchParams) ────────────────────────────── + + function PuzzleListContent() { +- const router = useRouter(); ++ const router = useRouter(); + const searchParams = useSearchParams(); + + const [filters, setFilters] = useState(() => +- parseFiltersFromParams(searchParams) ++ parseFiltersFromParams(searchParams), + ); + + const [pagination, setPagination] = useState(() => +- parsePaginationFromParams(searchParams) ++ parsePaginationFromParams(searchParams), + ); + + // Keep local state in sync if the browser URL changes externally (back/forward) +@@ -112,15 +122,24 @@ function PuzzleListContent() { + + // Sync filters & pagination → URL + const updateURL = useCallback( +- (nextFilters: PuzzleFilters, nextPagination: { currentPage: number; itemsPerPage: number }) => { ++ ( ++ nextFilters: PuzzleFilters, ++ nextPagination: { currentPage: number; itemsPerPage: number }, ++ ) => { + const params = new URLSearchParams(); +- if (nextFilters.categoryId) params.set('category', nextFilters.categoryId); +- if (nextFilters.difficulty !== 'ALL') params.set('difficulty', nextFilters.difficulty); +- if (nextPagination.currentPage > 1) params.set('page', nextPagination.currentPage.toString()); +- if (nextPagination.itemsPerPage !== 10) params.set('itemsPerPage', nextPagination.itemsPerPage.toString()); ++ if (nextFilters.categoryId) ++ params.set("category", nextFilters.categoryId); ++ if (nextFilters.difficulty !== "ALL") ++ params.set("difficulty", nextFilters.difficulty); ++ if (nextPagination.currentPage > 1) ++ params.set("page", nextPagination.currentPage.toString()); ++ if (nextPagination.itemsPerPage !== 10) ++ params.set("itemsPerPage", nextPagination.itemsPerPage.toString()); + + const query = params.toString(); +- router.push(query ? `/puzzle-list?${query}` : '/puzzle-list', { scroll: false }); ++ router.push(query ? `/puzzle-list?${query}` : "/puzzle-list", { ++ scroll: false, ++ }); + }, + [router], + ); +@@ -143,9 +162,9 @@ function PuzzleListContent() { + const newPagination = { ...pagination, currentPage: page }; + setPagination(newPagination); + updateURL(filters, newPagination); +- ++ + // Scroll to top smoothly +- window.scrollTo({ top: 0, behavior: 'smooth' }); ++ window.scrollTo({ top: 0, behavior: "smooth" }); + }, + [filters, pagination, updateURL], + ); +@@ -162,8 +181,8 @@ function PuzzleListContent() { + + // Build query params for usePuzzles — omit 'ALL' (no filter) + const puzzleQuery = { +- ...(filters.categoryId && { categoryId: filters.categoryId }), +- ...(filters.difficulty !== 'ALL' && { difficulty: filters.difficulty }), ++ ...(filters.categoryId && { categoryId: filters.categoryId }), ++ ...(filters.difficulty !== "ALL" && { difficulty: filters.difficulty }), + }; + + const { data: puzzles, isLoading, isError, error } = usePuzzles(puzzleQuery); +@@ -171,7 +190,7 @@ function PuzzleListContent() { + // Calculate pagination + const paginatedData = useMemo(() => { + if (!puzzles) return []; +- ++ + const startIndex = (pagination.currentPage - 1) * pagination.itemsPerPage; + const endIndex = startIndex + pagination.itemsPerPage; + return puzzles.slice(startIndex, endIndex); +@@ -200,11 +219,16 @@ function PuzzleListContent() { + {!isLoading && !isError && puzzles && ( +
+

+- {puzzles.length} puzzle{puzzles.length !== 1 ? 's' : ''} found ++ {puzzles.length} puzzle{puzzles.length !== 1 ? "s" : ""} found + {totalPages > 1 && ( + +- Showing {((pagination.currentPage - 1) * pagination.itemsPerPage) + 1}- +- {Math.min(pagination.currentPage * pagination.itemsPerPage, puzzles.length)} of {puzzles.length} ++ Showing{" "} ++ {(pagination.currentPage - 1) * pagination.itemsPerPage + 1}- ++ {Math.min( ++ pagination.currentPage * pagination.itemsPerPage, ++ puzzles.length, ++ )}{" "} ++ of {puzzles.length} + + )} +

+@@ -213,10 +237,12 @@ function PuzzleListContent() { + Items per page: + +- ++ + +- ++ + +- ++ + +
+- ++ + +diff --git a/frontend/components/StaggeredList.tsx b/frontend/components/StaggeredList.tsx +index e6b4364..4ef1bad 100644 +--- a/frontend/components/StaggeredList.tsx ++++ b/frontend/components/StaggeredList.tsx +@@ -1,13 +1,9 @@ +-import { motion } from 'framer-motion'; +-import { staggerContainer, slideUp } from '../lib/animations/variants'; ++import { motion } from "framer-motion"; ++import { staggerContainer, slideUp } from "../lib/animations/variants"; + + const StaggeredList = ({ items }: { items: string[] }) => { + return ( +- ++ + {items.map((item, index) => ( + + {item} +@@ -17,4 +13,4 @@ const StaggeredList = ({ items }: { items: string[] }) => { + ); + }; + +-export default StaggeredList; +\ No newline at end of file ++export default StaggeredList; +diff --git a/frontend/components/StreakDayIndicator.tsx b/frontend/components/StreakDayIndicator.tsx +index 7267e51..93fdd58 100644 +--- a/frontend/components/StreakDayIndicator.tsx ++++ b/frontend/components/StreakDayIndicator.tsx +@@ -2,44 +2,53 @@ import React from "react"; + import Image from "next/image"; + + export interface StreakDayIndicatorProps { +- status: 'empty' | 'completed' | 'streak'; +- isToday?: boolean; +- inStreakRun?: boolean; ++ status: "empty" | "completed" | "streak"; ++ isToday?: boolean; ++ inStreakRun?: boolean; + } + + export const StreakDayIndicator: React.FC = ({ +- status, +- isToday = false, +- inStreakRun = false, ++ status, ++ isToday = false, ++ inStreakRun = false, + }) => { +- // Base classes for the perfect circle shape to ensure no layout shift +- const baseClasses = "flex items-center justify-center w-[24px] h-[24px] md:w-[28px] md:h-[28px] rounded-full z-10 shrink-0"; +- +- // Status-specific classes +- let statusClasses = ""; +- if (status === "empty") { +- statusClasses = "bg-[#E6E6E6]"; +- } else if (status === "completed") { +- statusClasses = "bg-[#FACC15]"; +- } else if (status === "streak") { +- statusClasses = "bg-[#FACC15] shadow-lg shadow-[#FACC15]/50"; +- } ++ // Base classes for the perfect circle shape to ensure no layout shift ++ const baseClasses = ++ "flex items-center justify-center w-[24px] h-[24px] md:w-[28px] md:h-[28px] rounded-full z-10 shrink-0"; + +- // isToday styling (adding ring to distinguish) +- const todayClasses = isToday ? "ring-2 ring-white ring-offset-2 ring-offset-[#050C16]" : ""; ++ // Status-specific classes ++ let statusClasses = ""; ++ if (status === "empty") { ++ statusClasses = "bg-[#E6E6E6]"; ++ } else if (status === "completed") { ++ statusClasses = "bg-[#FACC15]"; ++ } else if (status === "streak") { ++ statusClasses = "bg-[#FACC15] shadow-lg shadow-[#FACC15]/50"; ++ } + +- return ( +-
+- {/* Optional highlighted background for streak run */} +- {inStreakRun && status === "streak" && ( +-
+- )} +- +-
+- {status === "streak" && ( +- streak fire +- )} +-
+-
+- ); ++ // isToday styling (adding ring to distinguish) ++ const todayClasses = isToday ++ ? "ring-2 ring-white ring-offset-2 ring-offset-[#050C16]" ++ : ""; ++ ++ return ( ++
++ {/* Optional highlighted background for streak run */} ++ {inStreakRun && status === "streak" && ( ++
++ )} ++ ++
++ {status === "streak" && ( ++ streak fire ++ )} ++
++
++ ); + }; +diff --git a/frontend/components/StreakFooter.tsx b/frontend/components/StreakFooter.tsx +index a2365b4..8d4c344 100644 +--- a/frontend/components/StreakFooter.tsx ++++ b/frontend/components/StreakFooter.tsx +@@ -1,16 +1,16 @@ + import React from "react"; + + interface StreakFooterProps { +- onContinue: () => void; ++ onContinue: () => void; + } + + export const StreakFooter: React.FC = ({ onContinue }) => { +- return ( +- +- ); ++ return ( ++ ++ ); + }; +diff --git a/frontend/components/StreakHeader.tsx b/frontend/components/StreakHeader.tsx +index 637299f..9961fef 100644 +--- a/frontend/components/StreakHeader.tsx ++++ b/frontend/components/StreakHeader.tsx +@@ -2,24 +2,24 @@ import React from "react"; + import Image from "next/image"; + + interface StreakHeaderProps { +- streakCount: number; ++ streakCount: number; + } + + export const StreakHeader: React.FC = ({ streakCount }) => { +- return ( +-
+-
+- Flame +-
+-

+- {streakCount} Day Streak +-

+-
+- ); ++ return ( ++
++
++ Flame ++
++

++ {streakCount} Day Streak ++

++
++ ); + }; +diff --git a/frontend/components/StreakNavbar.tsx b/frontend/components/StreakNavbar.tsx +index ab56cb8..bea5c1d 100644 +--- a/frontend/components/StreakNavbar.tsx ++++ b/frontend/components/StreakNavbar.tsx +@@ -1,51 +1,48 @@ +-'use client'; ++"use client"; + +-import Image from 'next/image'; +-import Link from 'next/link'; ++import Image from "next/image"; ++import Link from "next/link"; + + interface StreakNavbarProps { +- streakCount: number; +- points: number; ++ streakCount: number; ++ points: number; + } + +-export default function StreakNavbar({ streakCount, points }: StreakNavbarProps) { +- return ( +-
++ ++ ); + } +diff --git a/frontend/components/StreakScreen.tsx b/frontend/components/StreakScreen.tsx +index b4f37a1..ae837fc 100644 +--- a/frontend/components/StreakScreen.tsx ++++ b/frontend/components/StreakScreen.tsx +@@ -5,24 +5,23 @@ import { WeeklyCalendar, DayData } from "./WeeklyCalendar"; + import { StreakFooter } from "./StreakFooter"; + + interface StreakScreenProps { +- streakCount: number; +- weekData: DayData[]; +- onContinue: () => void; ++ streakCount: number; ++ weekData: DayData[]; ++ onContinue: () => void; + } + + export const StreakScreen: React.FC = ({ +- streakCount, +- weekData, +- onContinue, ++ streakCount, ++ weekData, ++ onContinue, + }) => { +- return ( +-
+-
+- +- +- +-
+-
+- ); ++ return ( ++
++
++ ++ ++ ++
++
++ ); + }; +- +diff --git a/frontend/components/StreakSummaryCard.tsx b/frontend/components/StreakSummaryCard.tsx +index 9cb1a25..91e2083 100644 +--- a/frontend/components/StreakSummaryCard.tsx ++++ b/frontend/components/StreakSummaryCard.tsx +@@ -3,62 +3,62 @@ import Image from "next/image"; + import clsx from "clsx"; + + interface StreakSummaryCardProps { +- streakCount: number; +- isActive?: boolean; +- className?: string; ++ streakCount: number; ++ isActive?: boolean; ++ className?: string; + } + + export const StreakSummaryCard: React.FC = ({ +- streakCount, +- isActive = true, +- className, ++ streakCount, ++ isActive = true, ++ className, + }) => { +- return ( +-
+- {/* Flame Icon */} +-
+- Streak flame +-
++ return ( ++
++ {/* Flame Icon */} ++
++ Streak flame ++
+ +- {/* Streak Badge */} +-
+- +- {streakCount} +- +-
++ {/* Streak Badge */} ++
++ ++ {streakCount} ++ ++
+ +- {/* Streak Label */} +-

+- day streak! +-

+-
+- ); ++ {/* Streak Label */} ++

++ day streak! ++

++
++ ); + }; +diff --git a/frontend/components/WeeklyCalendar.tsx b/frontend/components/WeeklyCalendar.tsx +index d614feb..9f53bc0 100644 +--- a/frontend/components/WeeklyCalendar.tsx ++++ b/frontend/components/WeeklyCalendar.tsx +@@ -2,29 +2,34 @@ import React from "react"; + import { StreakDayIndicator } from "./StreakDayIndicator"; + + export interface DayData { +- day: string; +- completed: boolean; ++ day: string; ++ completed: boolean; + } + + interface WeeklyCalendarProps { +- days: DayData[]; ++ days: DayData[]; + } + + export const WeeklyCalendar: React.FC = ({ days }) => { +- return ( +-
+-
+- {days.map((dayData, index) => ( +-
+- +- {dayData.day} +- +- +-
+- ))} +-
+-
+- ); ++ return ( ++
++
++ {days.map((dayData, index) => ( ++
++ ++ {dayData.day} ++ ++ ++
++ ))} ++
++
++ ); + }; +diff --git a/frontend/components/analytics/DauMauChart.tsx b/frontend/components/analytics/DauMauChart.tsx +index bce5f0e..7ddd2a9 100644 +--- a/frontend/components/analytics/DauMauChart.tsx ++++ b/frontend/components/analytics/DauMauChart.tsx +@@ -148,7 +148,11 @@ export default function DauMauChart() { + > + + +- ++ + +
+ ); +-} +\ No newline at end of file ++} +diff --git a/frontend/components/coding/CodeEditor.tsx b/frontend/components/coding/CodeEditor.tsx +index 8c8e4b5..ddf6af7 100644 +--- a/frontend/components/coding/CodeEditor.tsx ++++ b/frontend/components/coding/CodeEditor.tsx +@@ -1,6 +1,6 @@ +-import Editor from '@monaco-editor/react'; +-import { Language } from '@/lib/types/editor'; +-import { MONACO_LANGUAGE_MAP } from '@/lib/constants/codeTemplates'; ++import Editor from "@monaco-editor/react"; ++import { Language } from "@/lib/types/editor"; ++import { MONACO_LANGUAGE_MAP } from "@/lib/constants/codeTemplates"; + + interface CodeEditorProps { + code: string; +@@ -26,21 +26,21 @@ export default function CodeEditor({ + options={{ + minimap: { enabled: false }, + fontSize: 14, +- lineNumbers: 'on', ++ lineNumbers: "on", + scrollBeyondLastLine: false, + automaticLayout: true, + tabSize: 2, +- wordWrap: 'on', ++ wordWrap: "on", + readOnly, + quickSuggestions: true, + suggestOnTriggerCharacters: true, +- acceptSuggestionOnEnter: 'on', ++ acceptSuggestionOnEnter: "on", + padding: { top: 16, bottom: 16 }, + lineDecorationsWidth: 0, + lineNumbersMinChars: 3, + glyphMargin: false, + folding: true, +- renderLineHighlight: 'all', ++ renderLineHighlight: "all", + scrollbar: { + verticalScrollbarSize: 10, + horizontalScrollbarSize: 10, +diff --git a/frontend/components/coding/CodeEditorPanel.tsx b/frontend/components/coding/CodeEditorPanel.tsx +index 1a20395..2a2b62d 100644 +--- a/frontend/components/coding/CodeEditorPanel.tsx ++++ b/frontend/components/coding/CodeEditorPanel.tsx +@@ -1,11 +1,11 @@ +-'use client'; ++"use client"; + +-import { useState, useEffect } from 'react'; +-import { Code, Maximize2, ChevronUp } from 'lucide-react'; +-import { Language } from '@/lib/types/editor'; +-import { CODE_TEMPLATES } from '@/lib/constants/codeTemplates'; +-import LanguageSelector from './LanguageSelector'; +-import CodeEditor from './CodeEditor'; ++import { useState, useEffect } from "react"; ++import { Code, Maximize2, ChevronUp } from "lucide-react"; ++import { Language } from "@/lib/types/editor"; ++import { CODE_TEMPLATES } from "@/lib/constants/codeTemplates"; ++import LanguageSelector from "./LanguageSelector"; ++import CodeEditor from "./CodeEditor"; + + interface CodeEditorPanelProps { + initialCode?: string; +@@ -17,14 +17,14 @@ interface CodeEditorPanelProps { + + export default function CodeEditorPanel({ + initialCode, +- language: initialLanguage = 'javascript', ++ language: initialLanguage = "javascript", + onCodeChange, + onLanguageChange, + readOnly = false, + }: CodeEditorPanelProps) { + const [language, setLanguage] = useState(initialLanguage); + const [code, setCode] = useState( +- initialCode || CODE_TEMPLATES[initialLanguage] ++ initialCode || CODE_TEMPLATES[initialLanguage], + ); + const [autoComplete, setAutoComplete] = useState(true); + const [isSaved, setIsSaved] = useState(true); +@@ -49,7 +49,7 @@ export default function CodeEditorPanel({ + }; + + const handleCodeChange = (value: string | undefined) => { +- const newCode = value || ''; ++ const newCode = value || ""; + setCode(newCode); + setIsSaved(false); + onCodeChange?.(newCode); +@@ -67,8 +67,14 @@ export default function CodeEditorPanel({ +

Code

+
+
+- +- ++ ++ +
+
+ +@@ -83,12 +89,12 @@ export default function CodeEditorPanel({ + +@@ -122,7 +128,7 @@ export default function CodeEditorPanel({ + +
+ +- {isSaved ? 'Saved' : 'Saving...'} ++ {isSaved ? "Saved" : "Saving..."} + +
+
+diff --git a/frontend/components/coding/LanguageSelector.tsx b/frontend/components/coding/LanguageSelector.tsx +index 2aaee40..cf0f0e1 100644 +--- a/frontend/components/coding/LanguageSelector.tsx ++++ b/frontend/components/coding/LanguageSelector.tsx +@@ -1,13 +1,13 @@ +-import { ChevronDown } from 'lucide-react'; +-import { Language } from '@/lib/types/editor'; +-import { LANGUAGE_LABELS } from '@/lib/constants/codeTemplates'; ++import { ChevronDown } from "lucide-react"; ++import { Language } from "@/lib/types/editor"; ++import { LANGUAGE_LABELS } from "@/lib/constants/codeTemplates"; + + interface LanguageSelectorProps { + selectedLanguage: Language; + onLanguageChange: (language: Language) => void; + } + +-const languages: Language[] = ['javascript', 'python', 'cpp', 'csharp']; ++const languages: Language[] = ["javascript", "python", "cpp", "csharp"]; + + export default function LanguageSelector({ + selectedLanguage, +diff --git a/frontend/components/coding/OutputDisplay.tsx b/frontend/components/coding/OutputDisplay.tsx +index f025151..db114c9 100644 +--- a/frontend/components/coding/OutputDisplay.tsx ++++ b/frontend/components/coding/OutputDisplay.tsx +@@ -1,37 +1,39 @@ +-import React from 'react'; ++import React from "react"; + + interface OutputDisplayProps { + output: string; +- status?: 'pending' | 'correct' | 'wrong'; ++ status?: "pending" | "correct" | "wrong"; + errorMessage?: string; + } + +-const OutputDisplay: React.FC = ({ +- output, ++const OutputDisplay: React.FC = ({ ++ output, + status, +- errorMessage ++ errorMessage, + }) => { +- const isError = status === 'wrong'; +- const isPending = status === 'pending'; +- ++ const isError = status === "wrong"; ++ const isPending = status === "pending"; ++ + let displayText = output; +- let textColor = 'text-gray-300'; +- ++ let textColor = "text-gray-300"; ++ + if (isError && errorMessage) { + displayText = errorMessage; +- textColor = 'text-red-400'; ++ textColor = "text-red-400"; + } else if (isPending) { +- displayText = 'Waiting for your solution...'; +- textColor = 'text-gray-500'; ++ displayText = "Waiting for your solution..."; ++ textColor = "text-gray-500"; + } +- ++ + return ( +
+-
++      
+         {displayText}
+       
+
+ ); + }; + +-export default OutputDisplay; +\ No newline at end of file ++export default OutputDisplay; +diff --git a/frontend/components/coding/TestCaseHeader.tsx b/frontend/components/coding/TestCaseHeader.tsx +index b83d50c..b90d629 100644 +--- a/frontend/components/coding/TestCaseHeader.tsx ++++ b/frontend/components/coding/TestCaseHeader.tsx +@@ -1,23 +1,23 @@ +-import React from 'react'; +-import { ChevronDown, ChevronUp } from 'lucide-react'; ++import React from "react"; ++import { ChevronDown, ChevronUp } from "lucide-react"; + + interface TestCaseHeaderProps { + expanded: boolean; + toggleExpanded: () => void; + } + +-const TestCaseHeader: React.FC = ({ +- expanded, +- toggleExpanded ++const TestCaseHeader: React.FC = ({ ++ expanded, ++ toggleExpanded, + }) => { + return ( +-
{ +- if (e.key === 'Enter' || e.key === ' ') { ++ if (e.key === "Enter" || e.key === " ") { + e.preventDefault(); + toggleExpanded(); + } +@@ -26,18 +26,16 @@ const TestCaseHeader: React.FC = ({ +

+ Testcase +

+- +
+ ); + }; + +-export default TestCaseHeader; +\ No newline at end of file ++export default TestCaseHeader; +diff --git a/frontend/components/coding/TestCasePanel.tsx b/frontend/components/coding/TestCasePanel.tsx +index 9ec0ed2..1a84600 100644 +--- a/frontend/components/coding/TestCasePanel.tsx ++++ b/frontend/components/coding/TestCasePanel.tsx +@@ -1,15 +1,15 @@ +-import React, { useState } from 'react'; +-import TestCaseHeader from './TestCaseHeader'; +-import TestCaseTabs from './TestCaseTabs'; +-import OutputDisplay from './OutputDisplay'; +-import ValidationStatus from './ValidationStatus'; ++import React, { useState } from "react"; ++import TestCaseHeader from "./TestCaseHeader"; ++import TestCaseTabs from "./TestCaseTabs"; ++import OutputDisplay from "./OutputDisplay"; ++import ValidationStatus from "./ValidationStatus"; + + interface TestCase { + id: string; + input: string; + expectedOutput: string; + actualOutput?: string; +- status?: 'pending' | 'correct' | 'wrong'; ++ status?: "pending" | "correct" | "wrong"; + errorMessage?: string; + } + +@@ -19,13 +19,13 @@ interface TestCasePanelProps { + onTabChange?: (caseIndex: number) => void; + } + +-const TestCasePanel: React.FC = ({ +- testCases = [], +- activeCase = 0, +- onTabChange ++const TestCasePanel: React.FC = ({ ++ testCases = [], ++ activeCase = 0, ++ onTabChange, + }) => { + const [expanded, setExpanded] = useState(true); +- ++ + const activeTestCase = testCases[activeCase] || testCases[0]; + + const handleTabChange = (index: number) => { +@@ -37,9 +37,9 @@ const TestCasePanel: React.FC = ({ + if (!expanded) { + return ( +
+- setExpanded(!expanded)} ++ setExpanded(!expanded)} + /> +
+ ); +@@ -47,28 +47,32 @@ const TestCasePanel: React.FC = ({ + + return ( +
+- setExpanded(!expanded)} ++ setExpanded(!expanded)} + /> +- ++ +
+- +- ++ +
+- +- ++ + {activeTestCase && ( +
+- +@@ -80,4 +84,4 @@ const TestCasePanel: React.FC = ({ + ); + }; + +-export default TestCasePanel; +\ No newline at end of file ++export default TestCasePanel; +diff --git a/frontend/components/coding/TestCaseTabs.tsx b/frontend/components/coding/TestCaseTabs.tsx +index 90e9e22..4710592 100644 +--- a/frontend/components/coding/TestCaseTabs.tsx ++++ b/frontend/components/coding/TestCaseTabs.tsx +@@ -1,12 +1,12 @@ +-import React from 'react'; +-import { Plus } from 'lucide-react'; ++import React from "react"; ++import { Plus } from "lucide-react"; + + interface TestCase { + id: string; + input: string; + expectedOutput: string; + actualOutput?: string; +- status?: 'pending' | 'correct' | 'wrong'; ++ status?: "pending" | "correct" | "wrong"; + errorMessage?: string; + } + +@@ -16,16 +16,16 @@ interface TestCaseTabsProps { + onTabChange: (index: number) => void; + } + +-const TestCaseTabs: React.FC = ({ +- testCases, +- activeCase, +- onTabChange ++const TestCaseTabs: React.FC = ({ ++ testCases, ++ activeCase, ++ onTabChange, + }) => { + const handleKeyDown = (e: React.KeyboardEvent, index: number) => { +- if (e.key === 'ArrowRight') { ++ if (e.key === "ArrowRight") { + const nextIndex = (index + 1) % testCases.length; + onTabChange(nextIndex); +- } else if (e.key === 'ArrowLeft') { ++ } else if (e.key === "ArrowLeft") { + const prevIndex = (index - 1 + testCases.length) % testCases.length; + onTabChange(prevIndex); + } +@@ -38,8 +38,8 @@ const TestCaseTabs: React.FC = ({ + key={testCase.id} + className={`px-3 py-2 text-sm font-medium rounded-t-md transition-colors border-b-2 ${ + activeCase === index +- ? 'bg-teal-600/20 border-teal-500 text-teal-400' +- : 'bg-gray-800/50 border-transparent text-gray-400 hover:text-gray-200' ++ ? "bg-teal-600/20 border-teal-500 text-teal-400" ++ : "bg-gray-800/50 border-transparent text-gray-400 hover:text-gray-200" + }`} + onClick={() => onTabChange(index)} + onKeyDown={(e) => handleKeyDown(e, index)} +@@ -60,4 +60,4 @@ const TestCaseTabs: React.FC = ({ + ); + }; + +-export default TestCaseTabs; +\ No newline at end of file ++export default TestCaseTabs; +diff --git a/frontend/components/coding/ValidationStatus.tsx b/frontend/components/coding/ValidationStatus.tsx +index a6c9dd2..a8cacdd 100644 +--- a/frontend/components/coding/ValidationStatus.tsx ++++ b/frontend/components/coding/ValidationStatus.tsx +@@ -1,20 +1,20 @@ +-import React from 'react'; +-import { Check, X } from 'lucide-react'; ++import React from "react"; ++import { Check, X } from "lucide-react"; + + interface ValidationStatusProps { +- status?: 'pending' | 'correct' | 'wrong'; ++ status?: "pending" | "correct" | "wrong"; + errorMessage?: string; + } + +-const ValidationStatus: React.FC = ({ +- status, +- errorMessage ++const ValidationStatus: React.FC = ({ ++ status, ++ errorMessage, + }) => { +- if (status === 'pending') { ++ if (status === "pending") { + return null; // Don't show anything for pending state + } + +- if (status === 'correct') { ++ if (status === "correct") { + return ( +
+
+@@ -25,7 +25,7 @@ const ValidationStatus: React.FC = ({ + ); + } + +- if (status === 'wrong') { ++ if (status === "wrong") { + return ( +
+
+@@ -44,4 +44,4 @@ const ValidationStatus: React.FC = ({ + return null; // Default case - no status + }; + +-export default ValidationStatus; +\ No newline at end of file ++export default ValidationStatus; +diff --git a/frontend/components/dashboard/DashboardHeader.tsx b/frontend/components/dashboard/DashboardHeader.tsx +index 542d3a1..3bca119 100644 +--- a/frontend/components/dashboard/DashboardHeader.tsx ++++ b/frontend/components/dashboard/DashboardHeader.tsx +@@ -5,7 +5,10 @@ interface DashboardHeaderProps { + pointsLabel: string; + } + +-const DashboardHeader = ({ streakLabel, pointsLabel }: DashboardHeaderProps) => { ++const DashboardHeader = ({ ++ streakLabel, ++ pointsLabel, ++}: DashboardHeaderProps) => { + return ( +
+
+diff --git a/frontend/components/error/ErrorBoundary.tsx b/frontend/components/error/ErrorBoundary.tsx +index 8aad7fb..7f92f7a 100644 +--- a/frontend/components/error/ErrorBoundary.tsx ++++ b/frontend/components/error/ErrorBoundary.tsx +@@ -1,6 +1,6 @@ +-'use client'; ++"use client"; + +-import React, { Component, ErrorInfo, ReactNode } from 'react'; ++import React, { Component, ErrorInfo, ReactNode } from "react"; + + interface Props { + children: ReactNode; +@@ -25,25 +25,25 @@ class ErrorBoundary extends Component { + } + + componentDidCatch(error: Error, errorInfo: ErrorInfo) { +- console.error('ErrorBoundary caught an error:', error, errorInfo); +- ++ console.error("ErrorBoundary caught an error:", error, errorInfo); ++ + // Log error to an error reporting service + this.logErrorToService(error, errorInfo); +- ++ + // Call the optional onError prop + if (this.props.onError) { + this.props.onError(error, errorInfo); + } +- ++ + this.setState({ errorInfo }); + } +- ++ + logErrorToService = (error: Error, errorInfo: ErrorInfo) => { + // In the future, this could send to an error tracking service like Sentry + // For now, we'll just log to console +- console.group('Error Report'); +- console.error('Error:', error); +- console.error('Error Info:', errorInfo); ++ console.group("Error Report"); ++ console.error("Error:", error); ++ console.error("Error Info:", errorInfo); + console.groupEnd(); + }; + +@@ -52,57 +52,72 @@ class ErrorBoundary extends Component { + }; + + handleGoHome = () => { +- window.location.href = '/'; ++ window.location.href = "/"; + }; + + render() { + if (this.state.hasError) { +- return this.props.fallback || ( +-
+-
+-
+-
+- +- +- ++ return ( ++ this.props.fallback || ( ++
++
++
++
++ ++ ++ ++
++

++ Something went wrong ++

++

++ We're sorry, but something unexpected happened. ++

+
+-

Something went wrong

+-

+- We're sorry, but something unexpected happened. +-

+-
+- +-
+- +- ++ ++
++ ++ ++
++ ++ {/* Show error details in development */} ++ {process.env.NODE_ENV === "development" && this.state.error && ( ++
++ Error Details ++

{this.state.error.toString()}

++ {this.state.errorInfo && ( ++
++
++                        {this.state.errorInfo.componentStack}
++                      
++
++ )} ++
++ )} +
+- +- {/* Show error details in development */} +- {process.env.NODE_ENV === 'development' && this.state.error && ( +-
+- Error Details +-

{this.state.error.toString()}

+- {this.state.errorInfo && ( +-
+-
+-                      {this.state.errorInfo.componentStack}
+-                    
+-
+- )} +-
+- )} +
+-
++ ) + ); + } + +@@ -111,4 +126,3 @@ class ErrorBoundary extends Component { + } + + export default ErrorBoundary; +- +diff --git a/frontend/components/error/GenericErrorPage.tsx b/frontend/components/error/GenericErrorPage.tsx +index db74e79..bb28a5a 100644 +--- a/frontend/components/error/GenericErrorPage.tsx ++++ b/frontend/components/error/GenericErrorPage.tsx +@@ -1,12 +1,12 @@ +-'use client'; ++"use client"; + +-import { useRouter } from 'next/navigation'; +-import { useState } from 'react'; ++import { useRouter } from "next/navigation"; ++import { useState } from "react"; + +-const GenericErrorPage = ({ +- title = "Something went wrong", ++const GenericErrorPage = ({ ++ title = "Something went wrong", + message = "We're sorry, but an unexpected error occurred. Please try again.", +- showRefresh = true ++ showRefresh = true, + }) => { + const router = useRouter(); + const [isRefreshing, setIsRefreshing] = useState(false); +@@ -19,7 +19,7 @@ const GenericErrorPage = ({ + }; + + const handleGoHome = () => { +- router.push('/'); ++ router.push("/"); + }; + + return ( +@@ -27,14 +27,23 @@ const GenericErrorPage = ({ +
+
+
+- +- ++ ++ + +
+

{title}

+-

+- {message} +-

++

{message}

+

+ Our team has been notified of the issue. +

+@@ -46,15 +55,15 @@ const GenericErrorPage = ({ + onClick={handleRetry} + disabled={isRefreshing} + className={`w-full px-6 py-3 rounded-lg transition-colors ${ +- isRefreshing +- ? 'bg-gray-600 cursor-not-allowed' +- : 'bg-blue-600 hover:bg-blue-700' ++ isRefreshing ++ ? "bg-gray-600 cursor-not-allowed" ++ : "bg-blue-600 hover:bg-blue-700" + } text-white`} + > +- {isRefreshing ? 'Refreshing...' : 'Try Again'} ++ {isRefreshing ? "Refreshing..." : "Try Again"} + + )} +- ++ + + +- ++ +
+- {username} ++ ++ {username} ++ +
+ + +diff --git a/frontend/components/onboarding/OnboardingLayout.tsx b/frontend/components/onboarding/OnboardingLayout.tsx +index 2478006..90cc060 100644 +--- a/frontend/components/onboarding/OnboardingLayout.tsx ++++ b/frontend/components/onboarding/OnboardingLayout.tsx +@@ -1,87 +1,86 @@ +-'use client'; +- +-import React from 'react'; +-import { useRouter } from 'next/navigation'; +-import { ArrowLeft } from 'lucide-react'; +-import Image from 'next/image'; +- +-interface OnboardingLayoutProps { +- children: React.ReactNode; +- currentStep: number; +- totalSteps?: number; +- title?: string; +- onBack?: () => void; +-} +- +-const OnboardingLayout: React.FC = ({ +- children, +- currentStep, +- totalSteps = 4, +- onBack, +-}) => { +- const router = useRouter(); +- +- const handleBack = () => { +- if (onBack) { +- onBack(); +- } else { +- router.back(); +- } +- }; +- +- const progressPercentage = (currentStep / totalSteps) * 100; +- +- return ( +-
+- {/* Top Navigation Bar */} +-
+- +- +- {/* Progress Bar */} +-
+-
+-
+- +- {/* Placeholder for symmetry or explicit step count if needed */} +-
+-
+- +- {/* Main Content Area */} +-
+- +- {/* Puzzle Icon Header */} +-
+- {/* Using tile.svg as the main logo/puzzle piece */} +-
+- MindBlock +-
+-
+- +- {children} +-
+- +- {/* Background Ambience (Optional) */} +-
+-
+-
+-
+-
+- ); +-}; +- +-export default OnboardingLayout; ++"use client"; ++ ++import React from "react"; ++import { useRouter } from "next/navigation"; ++import { ArrowLeft } from "lucide-react"; ++import Image from "next/image"; ++ ++interface OnboardingLayoutProps { ++ children: React.ReactNode; ++ currentStep: number; ++ totalSteps?: number; ++ title?: string; ++ onBack?: () => void; ++} ++ ++const OnboardingLayout: React.FC = ({ ++ children, ++ currentStep, ++ totalSteps = 4, ++ onBack, ++}) => { ++ const router = useRouter(); ++ ++ const handleBack = () => { ++ if (onBack) { ++ onBack(); ++ } else { ++ router.back(); ++ } ++ }; ++ ++ const progressPercentage = (currentStep / totalSteps) * 100; ++ ++ return ( ++
++ {/* Top Navigation Bar */} ++
++ ++ ++ {/* Progress Bar */} ++
++
++
++ ++ {/* Placeholder for symmetry or explicit step count if needed */} ++
++
++ ++ {/* Main Content Area */} ++
++ {/* Puzzle Icon Header */} ++
++ {/* Using tile.svg as the main logo/puzzle piece */} ++
++ MindBlock ++
++
++ ++ {children} ++
++ ++ {/* Background Ambience (Optional) */} ++
++
++
++
++
++ ); ++}; ++ ++export default OnboardingLayout; +diff --git a/frontend/components/profile/AchievementPreview.tsx b/frontend/components/profile/AchievementPreview.tsx +index bae93af..76b6d12 100644 +--- a/frontend/components/profile/AchievementPreview.tsx ++++ b/frontend/components/profile/AchievementPreview.tsx +@@ -51,4 +51,3 @@ export function AchievementPreview({ + + ); + } +- +diff --git a/frontend/components/profile/FollowButton.tsx b/frontend/components/profile/FollowButton.tsx +index e6e3ce5..a546164 100644 +--- a/frontend/components/profile/FollowButton.tsx ++++ b/frontend/components/profile/FollowButton.tsx +@@ -38,7 +38,12 @@ export function FollowButton({ + } + + return ( +- + ); + } +- +diff --git a/frontend/components/profile/OverviewCard.tsx b/frontend/components/profile/OverviewCard.tsx +index f0e75bd..9ee062c 100644 +--- a/frontend/components/profile/OverviewCard.tsx ++++ b/frontend/components/profile/OverviewCard.tsx +@@ -1,4 +1,3 @@ + import { StatCard } from "./StatCard"; + + export { StatCard as OverviewCard }; +- +diff --git a/frontend/components/profile/OverviewGrid.tsx b/frontend/components/profile/OverviewGrid.tsx +index fb8ac5d..71bd0b1 100644 +--- a/frontend/components/profile/OverviewGrid.tsx ++++ b/frontend/components/profile/OverviewGrid.tsx +@@ -1,4 +1,3 @@ + import { ProfileOverview } from "./ProfileOverview"; + + export { ProfileOverview as OverviewGrid }; +- +diff --git a/frontend/components/profile/ProfileOverview.tsx b/frontend/components/profile/ProfileOverview.tsx +index a3db05d..697fa34 100644 +--- a/frontend/components/profile/ProfileOverview.tsx ++++ b/frontend/components/profile/ProfileOverview.tsx +@@ -28,7 +28,7 @@ export function ProfileOverview({ + label: "Total Points", + }, + { +- iconSrc: "/trophy.svg", ++ iconSrc: "/trophy.svg", + iconAlt: "trophy", + value: `#${rank}`, + label: "Rank", +@@ -51,7 +51,13 @@ export function ProfileOverview({ + key={stat.label} + icon={ +
+- {stat.iconAlt} ++ {stat.iconAlt} +
+ } + value={stat.value} +diff --git a/frontend/components/profile/ProfileStats.tsx b/frontend/components/profile/ProfileStats.tsx +index 20e5e77..d8ac90d 100644 +--- a/frontend/components/profile/ProfileStats.tsx ++++ b/frontend/components/profile/ProfileStats.tsx +@@ -34,4 +34,3 @@ export function ProfileStats({ +
+ ); + } +- +diff --git a/frontend/components/profile/UserProfileView.tsx b/frontend/components/profile/UserProfileView.tsx +index 3a00645..2a4a116 100644 +--- a/frontend/components/profile/UserProfileView.tsx ++++ b/frontend/components/profile/UserProfileView.tsx +@@ -1,11 +1,7 @@ + "use client"; + + import { X, Share2 } from "lucide-react"; +-import { +- Avatar, +- AvatarFallback, +- AvatarImage, +-} from "@radix-ui/react-avatar"; ++import { Avatar, AvatarFallback, AvatarImage } from "@radix-ui/react-avatar"; + import { FollowButton } from "./FollowButton"; + import { ProfileStats } from "./ProfileStats"; + import { OverviewGrid } from "./OverviewGrid"; +@@ -78,7 +74,8 @@ export function UserProfileView(props: UserProfileViewProps) { + type="button" + onClick={onClose} + aria-label="Close profile" +- className="absolute left-4 top-4 flex h-8 w-8 items-center justify-center rounded-full border border-gray-800 bg-[#050C16] text-gray-300 hover:bg-gray-800"> ++ className="absolute left-4 top-4 flex h-8 w-8 items-center justify-center rounded-full border border-gray-800 bg-[#050C16] text-gray-300 hover:bg-gray-800" ++ > + + + +@@ -86,7 +83,10 @@ export function UserProfileView(props: UserProfileViewProps) { + {/* Avatar & basic info */} +
+ +- ++ + + {initials} + +@@ -122,24 +122,25 @@ export function UserProfileView(props: UserProfileViewProps) { + type="button" + onClick={onShare} + aria-label="Share profile" +- className="flex h-11 w-11 items-center justify-center rounded-xl border border-gray-800 bg-[#050C16] text-gray-200 transition-colors hover:border-[#3B82F6] hover:text-[#3B82F6]"> ++ className="flex h-11 w-11 items-center justify-center rounded-xl border border-gray-800 bg-[#050C16] text-gray-200 transition-colors hover:border-[#3B82F6] hover:text-[#3B82F6]" ++ > + + +
+ +
+- {/* Overview grid */} +- +- ++ {/* Overview grid */} ++ ++ + +- {/* Achievements preview */} +- +
+@@ -148,4 +149,3 @@ export function UserProfileView(props: UserProfileViewProps) { +
+ ); + } +- +diff --git a/frontend/components/puzzles/CategoryFilter.tsx b/frontend/components/puzzles/CategoryFilter.tsx +index a7153f6..9106f56 100644 +--- a/frontend/components/puzzles/CategoryFilter.tsx ++++ b/frontend/components/puzzles/CategoryFilter.tsx +@@ -1,7 +1,7 @@ +-'use client'; ++"use client"; + +-import React from 'react'; +-import { useCategories } from '@/hooks/useCategories'; ++import React from "react"; ++import { useCategories } from "@/hooks/useCategories"; + + interface CategoryFilterProps { + selectedCategoryId: string; // '' means "All" +@@ -40,14 +40,21 @@ const CategoryFilter: React.FC = ({ + `} + > + + +- {!isLoading && !isError && categories?.map((cat) => ( +- +- ))} ++ {!isLoading && ++ !isError && ++ categories?.map((cat) => ( ++ ++ ))} + + + {/* Custom chevron icon */} +diff --git a/frontend/components/puzzles/DifficultyFilter.tsx b/frontend/components/puzzles/DifficultyFilter.tsx +index 9c66a18..a2da524 100644 +--- a/frontend/components/puzzles/DifficultyFilter.tsx ++++ b/frontend/components/puzzles/DifficultyFilter.tsx +@@ -1,7 +1,7 @@ +-'use client'; ++"use client"; + +-import React from 'react'; +-import type { PuzzleDifficulty } from '@/lib/types/puzzles'; ++import React from "react"; ++import type { PuzzleDifficulty } from "@/lib/types/puzzles"; + + interface DifficultyOption { + value: PuzzleDifficulty; +@@ -10,11 +10,11 @@ interface DifficultyOption { + } + + const DIFFICULTY_OPTIONS: DifficultyOption[] = [ +- { value: 'ALL', label: 'All', dotColor: 'bg-slate-400' }, +- { value: 'BEGINNER', label: 'Beginner', dotColor: 'bg-green-400' }, +- { value: 'INTERMEDIATE', label: 'Intermediate', dotColor: 'bg-yellow-400' }, +- { value: 'ADVANCED', label: 'Advanced', dotColor: 'bg-orange-400' }, +- { value: 'EXPERT', label: 'Expert', dotColor: 'bg-red-500' }, ++ { value: "ALL", label: "All", dotColor: "bg-slate-400" }, ++ { value: "BEGINNER", label: "Beginner", dotColor: "bg-green-400" }, ++ { value: "INTERMEDIATE", label: "Intermediate", dotColor: "bg-yellow-400" }, ++ { value: "ADVANCED", label: "Advanced", dotColor: "bg-orange-400" }, ++ { value: "EXPERT", label: "Expert", dotColor: "bg-red-500" }, + ]; + + interface DifficultyFilterProps { +@@ -45,8 +45,8 @@ const DifficultyFilter: React.FC = ({ + border transition-all duration-200 focus:outline-none focus:ring-2 focus:ring-[#3B82F6]/50 + ${ + isActive +- ? 'border-[#3B82F6] bg-[#3B82F6]/15 text-white' +- : 'border-white/10 bg-white/5 text-slate-400 hover:border-white/25 hover:text-slate-200' ++ ? "border-[#3B82F6] bg-[#3B82F6]/15 text-white" ++ : "border-white/10 bg-white/5 text-slate-400 hover:border-white/25 hover:text-slate-200" + } + `} + > +diff --git a/frontend/components/puzzles/FilterBar.tsx b/frontend/components/puzzles/FilterBar.tsx +index 139b13f..3805674 100644 +--- a/frontend/components/puzzles/FilterBar.tsx ++++ b/frontend/components/puzzles/FilterBar.tsx +@@ -1,11 +1,11 @@ +-'use client'; ++"use client"; + +-import React from 'react'; +-import type { PuzzleDifficulty, PuzzleFilters } from '@/lib/types/puzzles'; +-import DifficultyFilter from './DifficultyFilter'; +-import CategoryFilter from './CategoryFilter'; +-import Button from '@/components/ui/Button'; +-import { X } from 'lucide-react'; ++import React from "react"; ++import type { PuzzleDifficulty, PuzzleFilters } from "@/lib/types/puzzles"; ++import DifficultyFilter from "./DifficultyFilter"; ++import CategoryFilter from "./CategoryFilter"; ++import Button from "@/components/ui/Button"; ++import { X } from "lucide-react"; + + interface FilterBarProps { + filters: PuzzleFilters; +@@ -13,13 +13,13 @@ interface FilterBarProps { + } + + const DEFAULT_FILTERS: PuzzleFilters = { +- categoryId: '', +- difficulty: 'ALL', ++ categoryId: "", ++ difficulty: "ALL", + }; + + const FilterBar: React.FC = ({ filters, onFiltersChange }) => { + const hasActiveFilters = +- filters.categoryId !== '' || filters.difficulty !== 'ALL'; ++ filters.categoryId !== "" || filters.difficulty !== "ALL"; + + const handleCategoryChange = (categoryId: string) => { + onFiltersChange({ ...filters, categoryId }); +diff --git a/frontend/components/puzzles/PuzzleCard.tsx b/frontend/components/puzzles/PuzzleCard.tsx +index cad653e..2de9954 100644 +--- a/frontend/components/puzzles/PuzzleCard.tsx ++++ b/frontend/components/puzzles/PuzzleCard.tsx +@@ -1,8 +1,8 @@ +-'use client'; ++"use client"; + +-import React from 'react'; +-import { useRouter } from 'next/navigation'; +-import type { Puzzle } from '@/lib/types/puzzles'; ++import React from "react"; ++import { useRouter } from "next/navigation"; ++import type { Puzzle } from "@/lib/types/puzzles"; + + interface PuzzleCardProps { + puzzle: Puzzle; +@@ -10,35 +10,56 @@ interface PuzzleCardProps { + + const TYPE_CONFIG = { + logic: { +- label: 'Logic', +- icon: '🧠', +- className: 'bg-purple-500/15 text-purple-300 border-purple-500/25', ++ label: "Logic", ++ icon: "🧠", ++ className: "bg-purple-500/15 text-purple-300 border-purple-500/25", + }, + coding: { +- label: 'Coding', +- icon: 'šŸ’»', +- className: 'bg-blue-500/15 text-blue-300 border-blue-500/25', ++ label: "Coding", ++ icon: "šŸ’»", ++ className: "bg-blue-500/15 text-blue-300 border-blue-500/25", + }, + blockchain: { +- label: 'Blockchain', +- icon: 'ā›“ļø', +- className: 'bg-emerald-500/15 text-emerald-300 border-emerald-500/25', ++ label: "Blockchain", ++ icon: "ā›“ļø", ++ className: "bg-emerald-500/15 text-emerald-300 border-emerald-500/25", + }, + } as const; + + const DIFFICULTY_CONFIG = { +- ALL: { label: 'All', className: 'bg-slate-500/15 text-slate-300 border-slate-500/25', dot: 'bg-slate-400' }, +- BEGINNER: { label: 'Beginner', className: 'bg-green-500/15 text-green-300 border-green-500/25', dot: 'bg-green-400' }, +- INTERMEDIATE: { label: 'Intermediate', className: 'bg-yellow-500/15 text-yellow-300 border-yellow-500/25', dot: 'bg-yellow-400' }, +- ADVANCED: { label: 'Advanced', className: 'bg-orange-500/15 text-orange-300 border-orange-500/25', dot: 'bg-orange-400' }, +- EXPERT: { label: 'Expert', className: 'bg-red-500/15 text-red-300 border-red-500/25', dot: 'bg-red-500' }, ++ ALL: { ++ label: "All", ++ className: "bg-slate-500/15 text-slate-300 border-slate-500/25", ++ dot: "bg-slate-400", ++ }, ++ BEGINNER: { ++ label: "Beginner", ++ className: "bg-green-500/15 text-green-300 border-green-500/25", ++ dot: "bg-green-400", ++ }, ++ INTERMEDIATE: { ++ label: "Intermediate", ++ className: "bg-yellow-500/15 text-yellow-300 border-yellow-500/25", ++ dot: "bg-yellow-400", ++ }, ++ ADVANCED: { ++ label: "Advanced", ++ className: "bg-orange-500/15 text-orange-300 border-orange-500/25", ++ dot: "bg-orange-400", ++ }, ++ EXPERT: { ++ label: "Expert", ++ className: "bg-red-500/15 text-red-300 border-red-500/25", ++ dot: "bg-red-500", ++ }, + } as const; + + const PuzzleCard: React.FC = ({ puzzle }) => { + const router = useRouter(); + + const typeConfig = TYPE_CONFIG[puzzle.type] ?? TYPE_CONFIG.logic; +- const diffConfig = DIFFICULTY_CONFIG[puzzle.difficulty] ?? DIFFICULTY_CONFIG.BEGINNER; ++ const diffConfig = ++ DIFFICULTY_CONFIG[puzzle.difficulty] ?? DIFFICULTY_CONFIG.BEGINNER; + + return ( +
+diff --git a/frontend/components/quiz/LevelComplete.tsx b/frontend/components/quiz/LevelComplete.tsx +index dc1ea89..e30fa4e 100644 +--- a/frontend/components/quiz/LevelComplete.tsx ++++ b/frontend/components/quiz/LevelComplete.tsx +@@ -28,7 +28,9 @@ export function LevelComplete({ + level = 3, + }: LevelCompleteProps) { + return ( +-
++
+ + +
+@@ -43,7 +45,9 @@ export function LevelComplete({ + label="NICE" + value={`${correctAnswers}/${totalQuestions}`} + color="#14B8A6" +- icon={} ++ icon={ ++ ++ } + delay={100} + /> + +
+
{icon}
+- {value} ++ ++ {value} ++ +
+
+ ); +@@ -113,7 +128,10 @@ interface CompletionFooterProps { + isLoading?: boolean; + } + +-export function CompletionFooter({ onClaim, isLoading = false }: CompletionFooterProps) { ++export function CompletionFooter({ ++ onClaim, ++ isLoading = false, ++}: CompletionFooterProps) { + return ( +
+ + {error &&
{error}
} +- {isSuccess &&
Points successfully claimed!
} ++ {isSuccess && ( ++
Points successfully claimed!
++ )} +
+ ); + } +diff --git a/frontend/components/ui/Button.tsx b/frontend/components/ui/Button.tsx +index be40297..c35f9be 100644 +--- a/frontend/components/ui/Button.tsx ++++ b/frontend/components/ui/Button.tsx +@@ -1,8 +1,8 @@ +-import React from 'react'; ++import React from "react"; + + interface ButtonProps { + children: React.ReactNode; +- type?: 'button' | 'submit'; ++ type?: "button" | "submit"; + disabled?: boolean; + onClick?: () => void; + className?: string; +@@ -10,10 +10,10 @@ interface ButtonProps { + + const Button: React.FC = ({ + children, +- type = 'button', ++ type = "button", + disabled = false, + onClick, +- className = '', ++ className = "", + }) => { + return ( + + )} +
+diff --git a/frontend/components/ui/Pagination.tsx b/frontend/components/ui/Pagination.tsx +index 598c283..d68378a 100644 +--- a/frontend/components/ui/Pagination.tsx ++++ b/frontend/components/ui/Pagination.tsx +@@ -1,7 +1,7 @@ +-'use client'; ++"use client"; + +-import React, { useMemo } from 'react'; +-import { ChevronLeft, ChevronRight } from 'lucide-react'; ++import React, { useMemo } from "react"; ++import { ChevronLeft, ChevronRight } from "lucide-react"; + + interface PaginationProps { + currentPage: number; +@@ -23,33 +23,33 @@ const Pagination: React.FC = ({ + } + + const pages: (number | string)[] = []; +- ++ + // Always show first page + pages.push(1); +- ++ + if (currentPage <= 4) { + // Show pages 2-5 when current is near start + for (let i = 2; i <= 5; i++) { + pages.push(i); + } +- pages.push('...'); ++ pages.push("..."); + pages.push(totalPages); + } else if (currentPage >= totalPages - 3) { + // Show ellipsis and last 4 pages when current is near end +- pages.push('...'); ++ pages.push("..."); + for (let i = totalPages - 4; i <= totalPages; i++) { + pages.push(i); + } + } else { + // Show ellipsis on both sides with current page in middle +- pages.push('...'); ++ pages.push("..."); + for (let i = currentPage - 1; i <= currentPage + 1; i++) { + pages.push(i); + } +- pages.push('...'); ++ pages.push("..."); + pages.push(totalPages); + } +- ++ + return pages; + }, [currentPage, totalPages]); + +@@ -83,9 +83,10 @@ const Pagination: React.FC = ({ + disabled={currentPage === 1} + className={` + flex items-center gap-1 px-3 py-2 text-sm font-medium rounded-lg border transition-all duration-200 +- ${currentPage === 1 +- ? 'text-slate-500 border-slate-700 cursor-not-allowed' +- : 'text-slate-300 border-slate-600 hover:border-[#3B82F6]/40 hover:text-white hover:bg-white/[0.05]' ++ ${ ++ currentPage === 1 ++ ? "text-slate-500 border-slate-700 cursor-not-allowed" ++ : "text-slate-300 border-slate-600 hover:border-[#3B82F6]/40 hover:text-white hover:bg-white/[0.05]" + } + `} + aria-label="Previous page" +@@ -96,8 +97,8 @@ const Pagination: React.FC = ({ + + {/* Page Numbers */} +
+- {pageNumbers.map((page, index) => ( +- page === '...' ? ( ++ {pageNumbers.map((page, index) => ++ page === "..." ? ( + + ... + +@@ -107,18 +108,19 @@ const Pagination: React.FC = ({ + onClick={() => handlePageClick(page as number)} + className={` + px-3 py-2 text-sm font-medium rounded-lg border transition-all duration-200 +- ${currentPage === page +- ? 'bg-[#3B82F6] text-white border-[#3B82F6]' +- : 'text-slate-300 border-slate-600 hover:border-[#3B82F6]/40 hover:text-white hover:bg-white/[0.05]' ++ ${ ++ currentPage === page ++ ? "bg-[#3B82F6] text-white border-[#3B82F6]" ++ : "text-slate-300 border-slate-600 hover:border-[#3B82F6]/40 hover:text-white hover:bg-white/[0.05]" + } + `} + aria-label={`Go to page ${page}`} +- aria-current={currentPage === page ? 'page' : undefined} ++ aria-current={currentPage === page ? "page" : undefined} + > + {page} + +- ) +- ))} ++ ), ++ )} +
+ + {/* Next Button */} +@@ -127,9 +129,10 @@ const Pagination: React.FC = ({ + disabled={currentPage === totalPages} + className={` + flex items-center gap-1 px-3 py-2 text-sm font-medium rounded-lg border transition-all duration-200 +- ${currentPage === totalPages +- ? 'text-slate-500 border-slate-700 cursor-not-allowed' +- : 'text-slate-300 border-slate-600 hover:border-[#3B82F6]/40 hover:text-white hover:bg-white/[0.05]' ++ ${ ++ currentPage === totalPages ++ ? "text-slate-500 border-slate-700 cursor-not-allowed" ++ : "text-slate-300 border-slate-600 hover:border-[#3B82F6]/40 hover:text-white hover:bg-white/[0.05]" + } + `} + aria-label="Next page" +@@ -152,7 +155,7 @@ const Pagination: React.FC = ({ + }} + className="bg-slate-800 border border-slate-600 rounded px-2 py-1 text-slate-300 focus:outline-none focus:border-[#3B82F6]/40" + > +- {ITEMS_PER_PAGE_OPTIONS.map(option => ( ++ {ITEMS_PER_PAGE_OPTIONS.map((option) => ( + +diff --git a/frontend/components/ui/Toast.tsx b/frontend/components/ui/Toast.tsx +index 0ba76ee..c5c09c4 100644 +--- a/frontend/components/ui/Toast.tsx ++++ b/frontend/components/ui/Toast.tsx +@@ -1,11 +1,11 @@ +-'use client'; ++"use client"; + +-import React, { useEffect } from 'react'; +-import { X, CheckCircle, AlertCircle, Info, AlertTriangle } from 'lucide-react'; ++import React, { useEffect } from "react"; ++import { X, CheckCircle, AlertCircle, Info, AlertTriangle } from "lucide-react"; + + export interface Toast { + id: string; +- type: 'success' | 'error' | 'warning' | 'info'; ++ type: "success" | "error" | "warning" | "info"; + title: string; + message?: string; + duration?: number; +@@ -27,13 +27,13 @@ const ToastComponent: React.FC = ({ toast, onRemove }) => { + + const getIcon = () => { + switch (toast.type) { +- case 'success': ++ case "success": + return ; +- case 'error': ++ case "error": + return ; +- case 'warning': ++ case "warning": + return ; +- case 'info': ++ case "info": + return ; + default: + return ; +@@ -42,31 +42,31 @@ const ToastComponent: React.FC = ({ toast, onRemove }) => { + + const getBackgroundColor = () => { + switch (toast.type) { +- case 'success': +- return 'bg-green-500/10 border-green-500/20'; +- case 'error': +- return 'bg-red-500/10 border-red-500/20'; +- case 'warning': +- return 'bg-yellow-500/10 border-yellow-500/20'; +- case 'info': +- return 'bg-blue-500/10 border-blue-500/20'; ++ case "success": ++ return "bg-green-500/10 border-green-500/20"; ++ case "error": ++ return "bg-red-500/10 border-red-500/20"; ++ case "warning": ++ return "bg-yellow-500/10 border-yellow-500/20"; ++ case "info": ++ return "bg-blue-500/10 border-blue-500/20"; + default: +- return 'bg-blue-500/10 border-blue-500/20'; ++ return "bg-blue-500/10 border-blue-500/20"; + } + }; + + const getTextColor = () => { + switch (toast.type) { +- case 'success': +- return 'text-green-400'; +- case 'error': +- return 'text-red-400'; +- case 'warning': +- return 'text-yellow-400'; +- case 'info': +- return 'text-blue-400'; ++ case "success": ++ return "text-green-400"; ++ case "error": ++ return "text-red-400"; ++ case "warning": ++ return "text-yellow-400"; ++ case "info": ++ return "text-blue-400"; + default: +- return 'text-blue-400'; ++ return "text-blue-400"; + } + }; + +@@ -108,4 +108,3 @@ const ToastComponent: React.FC = ({ toast, onRemove }) => { + }; + + export default ToastComponent; +- +diff --git a/frontend/components/ui/ToastProvider.tsx b/frontend/components/ui/ToastProvider.tsx +index 0bbff79..cd87421 100644 +--- a/frontend/components/ui/ToastProvider.tsx ++++ b/frontend/components/ui/ToastProvider.tsx +@@ -1,10 +1,16 @@ +-'use client'; ++"use client"; + +-import React, { createContext, useContext, useState, useCallback, ReactNode } from 'react'; +-import ToastComponent, { Toast } from './Toast'; ++import React, { ++ createContext, ++ useContext, ++ useState, ++ useCallback, ++ ReactNode, ++} from "react"; ++import ToastComponent, { Toast } from "./Toast"; + + interface ToastContextType { +- showToast: (toast: Omit) => void; ++ showToast: (toast: Omit) => void; + showSuccess: (title: string, message?: string) => void; + showError: (title: string, message?: string) => void; + showWarning: (title: string, message?: string) => void; +@@ -17,7 +23,7 @@ const ToastContext = createContext(undefined); + export const useToast = () => { + const context = useContext(ToastContext); + if (!context) { +- throw new Error('useToast must be used within a ToastProvider'); ++ throw new Error("useToast must be used within a ToastProvider"); + } + return context; + }; +@@ -30,35 +36,47 @@ export const ToastProvider: React.FC = ({ children }) => { + const [toasts, setToasts] = useState([]); + + const removeToast = useCallback((id: string) => { +- setToasts(prev => prev.filter(toast => toast.id !== id)); ++ setToasts((prev) => prev.filter((toast) => toast.id !== id)); + }, []); + +- const showToast = useCallback((toast: Omit) => { ++ const showToast = useCallback((toast: Omit) => { + const id = Math.random().toString(36).substr(2, 9); + const newToast: Toast = { + ...toast, + id, + duration: toast.duration || 5000, + }; +- +- setToasts(prev => [...prev, newToast]); ++ ++ setToasts((prev) => [...prev, newToast]); + }, []); + +- const showSuccess = useCallback((title: string, message?: string) => { +- showToast({ type: 'success', title, message }); +- }, [showToast]); ++ const showSuccess = useCallback( ++ (title: string, message?: string) => { ++ showToast({ type: "success", title, message }); ++ }, ++ [showToast], ++ ); + +- const showError = useCallback((title: string, message?: string) => { +- showToast({ type: 'error', title, message }); +- }, [showToast]); ++ const showError = useCallback( ++ (title: string, message?: string) => { ++ showToast({ type: "error", title, message }); ++ }, ++ [showToast], ++ ); + +- const showWarning = useCallback((title: string, message?: string) => { +- showToast({ type: 'warning', title, message }); +- }, [showToast]); ++ const showWarning = useCallback( ++ (title: string, message?: string) => { ++ showToast({ type: "warning", title, message }); ++ }, ++ [showToast], ++ ); + +- const showInfo = useCallback((title: string, message?: string) => { +- showToast({ type: 'info', title, message }); +- }, [showToast]); ++ const showInfo = useCallback( ++ (title: string, message?: string) => { ++ showToast({ type: "info", title, message }); ++ }, ++ [showToast], ++ ); + + return ( + = ({ children }) => { + {children} + {/* Toast Container */} +
+- {toasts.map(toast => ( +- ++ {toasts.map((toast) => ( ++ + ))} +
+
+ ); + }; +- +diff --git a/frontend/components/ui/WalletModal.tsx b/frontend/components/ui/WalletModal.tsx +index 29dcbb0..9733358 100644 +--- a/frontend/components/ui/WalletModal.tsx ++++ b/frontend/components/ui/WalletModal.tsx +@@ -1,17 +1,22 @@ +-'use client'; ++"use client"; + +-import React, { useEffect, useCallback, useRef } from 'react'; +-import { X } from 'lucide-react'; +-import WalletOption from './wallet/WalletOption'; ++import React, { useEffect, useCallback, useRef } from "react"; ++import { X } from "lucide-react"; ++import WalletOption from "./wallet/WalletOption"; + import { + PhantomIcon, + MetaMaskIcon, + CoinbaseIcon, + TrustIcon, + FreighterIcon, +-} from './wallet/WalletIcons'; ++} from "./wallet/WalletIcons"; + +-export type WalletType = 'phantom' | 'metamask' | 'coinbase' | 'trust' | 'freighter'; ++export type WalletType = ++ | "phantom" ++ | "metamask" ++ | "coinbase" ++ | "trust" ++ | "freighter"; + + export interface WalletModalProps { + isOpen: boolean; +@@ -28,44 +33,55 @@ interface WalletConfig { + + const WALLETS: WalletConfig[] = [ + { +- id: 'freighter', +- name: 'Freighter', ++ id: "freighter", ++ name: "Freighter", + icon: , +- isDetected: typeof window !== 'undefined' && !!(window as unknown as Record).freighter, ++ isDetected: ++ typeof window !== "undefined" && ++ !!(window as unknown as Record).freighter, + }, + { +- id: 'phantom', +- name: 'Phantom', ++ id: "phantom", ++ name: "Phantom", + icon: , +- isDetected: typeof window !== 'undefined' && !!(window as unknown as Record).phantom, ++ isDetected: ++ typeof window !== "undefined" && ++ !!(window as unknown as Record).phantom, + }, + { +- id: 'metamask', +- name: 'MetaMask', ++ id: "metamask", ++ name: "MetaMask", + icon: , + isDetected: +- typeof window !== 'undefined' && +- !!(window as unknown as { ethereum?: { isMetaMask?: boolean } }).ethereum?.isMetaMask, ++ typeof window !== "undefined" && ++ !!(window as unknown as { ethereum?: { isMetaMask?: boolean } }).ethereum ++ ?.isMetaMask, + }, + { +- id: 'coinbase', +- name: 'Coinbase Wallet', ++ id: "coinbase", ++ name: "Coinbase Wallet", + icon: , + isDetected: +- typeof window !== 'undefined' && +- !!(window as unknown as { ethereum?: { isCoinbaseWallet?: boolean } }).ethereum?.isCoinbaseWallet, ++ typeof window !== "undefined" && ++ !!(window as unknown as { ethereum?: { isCoinbaseWallet?: boolean } }) ++ .ethereum?.isCoinbaseWallet, + }, + { +- id: 'trust', +- name: 'Trust', ++ id: "trust", ++ name: "Trust", + icon: , + isDetected: +- typeof window !== 'undefined' && +- !!(window as unknown as { ethereum?: { isTrust?: boolean } }).ethereum?.isTrust, ++ typeof window !== "undefined" && ++ !!(window as unknown as { ethereum?: { isTrust?: boolean } }).ethereum ++ ?.isTrust, + }, + ]; + +-const WalletModal: React.FC = ({ isOpen, onClose, onSelect }) => { ++const WalletModal: React.FC = ({ ++ isOpen, ++ onClose, ++ onSelect, ++}) => { + const modalRef = useRef(null); + const closeButtonRef = useRef(null); + +@@ -74,11 +90,11 @@ const WalletModal: React.FC = ({ isOpen, onClose, onSelect }) + if (!isOpen) return; + + const handleKeyDown = (e: KeyboardEvent) => { +- if (e.key === 'Escape') onClose(); ++ if (e.key === "Escape") onClose(); + }; + +- document.addEventListener('keydown', handleKeyDown); +- return () => document.removeEventListener('keydown', handleKeyDown); ++ document.addEventListener("keydown", handleKeyDown); ++ return () => document.removeEventListener("keydown", handleKeyDown); + }, [isOpen, onClose]); + + // Focus close button on open, restore focus on close +@@ -93,40 +109,37 @@ const WalletModal: React.FC = ({ isOpen, onClose, onSelect }) + // Prevent body scroll when modal is open + useEffect(() => { + if (isOpen) { +- document.body.style.overflow = 'hidden'; ++ document.body.style.overflow = "hidden"; + } else { +- document.body.style.overflow = ''; ++ document.body.style.overflow = ""; + } + return () => { +- document.body.style.overflow = ''; ++ document.body.style.overflow = ""; + }; + }, [isOpen]); + + // Focus trap within modal +- const handleTabKey = useCallback( +- (e: React.KeyboardEvent) => { +- if (e.key !== 'Tab' || !modalRef.current) return; +- +- const focusable = modalRef.current.querySelectorAll( +- 'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])' +- ); +- const first = focusable[0]; +- const last = focusable[focusable.length - 1]; +- +- if (e.shiftKey) { +- if (document.activeElement === first) { +- e.preventDefault(); +- last?.focus(); +- } +- } else { +- if (document.activeElement === last) { +- e.preventDefault(); +- first?.focus(); +- } ++ const handleTabKey = useCallback((e: React.KeyboardEvent) => { ++ if (e.key !== "Tab" || !modalRef.current) return; ++ ++ const focusable = modalRef.current.querySelectorAll( ++ 'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])', ++ ); ++ const first = focusable[0]; ++ const last = focusable[focusable.length - 1]; ++ ++ if (e.shiftKey) { ++ if (document.activeElement === first) { ++ e.preventDefault(); ++ last?.focus(); + } +- }, +- [] +- ); ++ } else { ++ if (document.activeElement === last) { ++ e.preventDefault(); ++ first?.focus(); ++ } ++ } ++ }, []); + + const handleBackdropClick = (e: React.MouseEvent) => { + if (e.target === e.currentTarget) onClose(); +diff --git a/frontend/components/ui/wallet/WalletIcons.tsx b/frontend/components/ui/wallet/WalletIcons.tsx +index 2e94944..73bd2e3 100644 +--- a/frontend/components/ui/wallet/WalletIcons.tsx ++++ b/frontend/components/ui/wallet/WalletIcons.tsx +@@ -1,6 +1,8 @@ +-import React from 'react'; ++import React from "react"; + +-export const PhantomIcon: React.FC<{ className?: string }> = ({ className }) => ( ++export const PhantomIcon: React.FC<{ className?: string }> = ({ ++ className, ++}) => ( + = ({ className }) => + + ); + +-export const MetaMaskIcon: React.FC<{ className?: string }> = ({ className }) => ( ++export const MetaMaskIcon: React.FC<{ className?: string }> = ({ ++ className, ++}) => ( + = ({ className }) => + aria-hidden="true" + > + +- +- +- +- +- +- +- +- +- +- +- +- ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ + + ); + +-export const CoinbaseIcon: React.FC<{ className?: string }> = ({ className }) => ( ++export const CoinbaseIcon: React.FC<{ className?: string }> = ({ ++ className, ++}) => ( + = ({ className }) => ( + + ); + +-export const FreighterIcon: React.FC<{ className?: string }> = ({ className }) => ( ++export const FreighterIcon: React.FC<{ className?: string }> = ({ ++ className, ++}) => ( + = ({ className }) = + aria-hidden="true" + > + +- +- ++ ++ + + ); +diff --git a/frontend/components/ui/wallet/WalletOption.tsx b/frontend/components/ui/wallet/WalletOption.tsx +index 6c36a95..c402772 100644 +--- a/frontend/components/ui/wallet/WalletOption.tsx ++++ b/frontend/components/ui/wallet/WalletOption.tsx +@@ -1,6 +1,6 @@ +-'use client'; ++"use client"; + +-import React from 'react'; ++import React from "react"; + + export interface WalletOptionProps { + name: string; +@@ -9,12 +9,17 @@ export interface WalletOptionProps { + onClick: () => void; + } + +-const WalletOption: React.FC = ({ name, icon, isDetected, onClick }) => { ++const WalletOption: React.FC = ({ ++ name, ++ icon, ++ isDetected, ++ onClick, ++}) => { + return ( +