diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 0e253006..e59966d0 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -193,6 +193,7 @@ jobs: --memory="650m" \ -p 8081:8081 \ -v /data/pomodify/uploads:/app/uploads \ + -e JAVA_TOOL_OPTIONS="-Duser.timezone=Asia/Manila" \ -e JAVA_OPTS="-Xms256m -Xmx512m -XX:+UseG1GC" \ -e SPRING_PROFILES_ACTIVE=prod \ -e DB_URL=jdbc:postgresql://${{ secrets.DB_HOST }}:${{ secrets.DB_PORT }}/${{ secrets.DB_NAME }}?sslmode=require \ diff --git a/README.md b/README.md index d1368497..93f0ba5e 100644 --- a/README.md +++ b/README.md @@ -1,81 +1,268 @@ -# Pomodify -#### – A customizable Pomodoro productivity tracker. Helps users focus with flexible timers, activity grouping, and progress tracking. +
+ +Pomodify logo + +# ⏲️ Pomodify +### *Your Smart Productivity Companion* + +[![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg?style=for-the-badge)](LICENSE) +[![Status: Active Development](https://img.shields.io/badge/Status-Active%20Development-brightgreen.svg?style=for-the-badge)](https://github.com/PUP-BSIT/project-g-cache) +[![Live Demo](https://img.shields.io/badge/🌐_Live_Demo-Visit_App-5FA9A4?style=for-the-badge)](https://pomodify.site/) + +--- + +*Transform your productivity with AI-powered focus sessions, smart tracking, and seamless organization* + +
+ +Pomodify is a customizable Pomodoro productivity tracker designed to help users focus with flexible timers, activity grouping, and comprehensive progress tracking. + +## πŸš€ What We're Building + +| 🎯 Focus Sessions | πŸ“Š Progress Tracking | 🏷️ Activity Groups | πŸ” Secure Accounts | +|:---:|:---:|:---:|:---:| +| Customizable focus sessions | Track & review progress | Organize by context | Your data, protected | +| ⏱️ Flexible timers | πŸ“ˆ Smart analytics | 🏷️ Easy categorization | πŸ”’ JWT security | + +--- ## πŸ“š Table of Contents + +- [🎯 Quick Start](#-quick-start) - [πŸš€ Features](#-features) -- [🌐 Live Demo](#-live-demo) -- [πŸ› οΈ Tech Stack](#-tech-stack) -- [πŸ“ Project Folder Structure](#-project-folder-structure) -- [πŸ—‚οΈ Git Workflow Guidelines](#-git-workflow-guidelines) -- [πŸ“œ General Coding Guidelines](#-general-coding-guidelines) +- [βš™οΈ Tech Stack](#️-tech-stack) +- [🎨 Design System](#-design-system) +- [πŸ“ Project Structure](#-project-structure) +- [🎯Development](#-development) - [πŸ§‘β€πŸ’» Contributors](#-contributors) -- [πŸ“ Developer Documentation](#-developer-documentation) +- [πŸ€– AI-Powered Features](#-ai-powered-features) + +--- + +## 🎯 Quick Start + +
+ +### οΏ½ [**Pomodify Live**](https://pomodify.site/) 🌟 + +| Badge | οΏ½ E mail | πŸ”‘ Password | +|:---:|:---|:---| +| 1️⃣ | `hann000345@gmail.com` | `Pomodify@123` | +| 2️⃣ | `simonejake@gmail.com` | `Pomodify@123` | +| 3️⃣ | `ivandelumen@gmail.com` | `Pomodify@123` | +| 4️⃣ | `danielvictorioso@gmail.com` | `Pomodify@123` | +| 5️⃣ | `geraldkasan163@gmail.com` | `Pomodify@123` | + +
+ +--- ## πŸš€ Features -- Customizable work/break sessions -- User accounts with authentication -- Activities to categorize sessions -- Session logs & reports with notes -- Responsive UI for web & mobile +| 🎨 Feature | πŸ“ Description | πŸ”§ Tech | +|:---|:---|:---| +| ⏱️ **Customizable Timers** | Flexible work/break sessions tailored to your needs | Angular + RxJS | +| πŸ” **User Authentication** | Secure accounts with JWT & Spring Security | Spring Boot + JWT | +| 🎭 **Activity Grouping** | Organize sessions by category for better context | PostgreSQL + JPA | +| πŸ“Š **Session Tracking** | Log notes, view reports, and monitor progress | Angular Material | +| πŸ“± **Responsive Design** | Seamless experience on web & mobile devices | SCSS + Angular | +| πŸ€– **AI Insights** | Smart suggestions and productivity analytics | Custom AI Integration | +| πŸ“² **Progressive Web App** | Install as app and push notifications | PWA + Service Workers | -## 🌐 Live Demo +
+🎯 Feature Highlights -> - [Pomidify Web Host](https://pomodify.site/) +``` +πŸ… Pomodoro Timer +β”œβ”€β”€ ⏰ Custom work/break intervals +β”œβ”€β”€ πŸ”” Smart notifications +β”œβ”€β”€ ⏸️ Pause & resume functionality +└── πŸ“ˆ Session completion tracking + +πŸ“Š Analytics Dashboard +β”œβ”€β”€ πŸ“… Daily/weekly/monthly views +β”œβ”€β”€ 🎯 Goal setting & tracking +β”œβ”€β”€ πŸ“‹ Detailed session logs +└── πŸ† Achievement system + +πŸ€– AI Features +β”œβ”€β”€ ▢️ Session suggestions +β”œβ”€β”€ πŸ“ Smart note-taking +β”œβ”€β”€ πŸŽ“ Learning blueprints +└── πŸ” Pattern recognition +``` + +
-## πŸ”‘ Test User Account -- Use the following credentials to explore the system: -- **Email**: johndoe@gmail.com -- **Password**: JohnDoe@123 +--- ## πŸ› οΈ Tech Stack -- **Frontend:** Angular, Tailwind CSS -- **Backend:** Spring Boot, OpenAPI (Swagger), Lombok, MapStruct + +
+ +**Frontend Stack** + +![Angular](https://img.shields.io/badge/Angular-DD0031?style=for-the-badge&logo=angular&logoColor=white) +![TypeScript](https://img.shields.io/badge/TypeScript-007ACC?style=for-the-badge&logo=typescript&logoColor=white) +![SCSS](https://img.shields.io/badge/SCSS-CC6699?style=for-the-badge&logo=sass&logoColor=white) +![Material UI](https://img.shields.io/badge/Material_UI-0081CB?style=for-the-badge&logo=material-ui&logoColor=white) + +**Backend Stack** + +![Spring Boot](https://img.shields.io/badge/Spring_Boot-6DB33F?style=for-the-badge&logo=spring-boot&logoColor=white) +![Java](https://img.shields.io/badge/Java-ED8B00?style=for-the-badge&logo=java&logoColor=white) +![PostgreSQL](https://img.shields.io/badge/PostgreSQL-316192?style=for-the-badge&logo=postgresql&logoColor=white) +![JWT](https://img.shields.io/badge/JWT-000000?style=for-the-badge&logo=JSON%20web%20tokens&logoColor=white) + +**DevOps & Tools** + +![Docker](https://img.shields.io/badge/Docker-2496ED?style=for-the-badge&logo=docker&logoColor=white) +![GitHub Actions](https://img.shields.io/badge/GitHub_Actions-2088FF?style=for-the-badge&logo=github-actions&logoColor=white) +![Swagger](https://img.shields.io/badge/Swagger-85EA2D?style=for-the-badge&logo=swagger&logoColor=black) + +
+ +
+πŸ“‹ Detailed Tech Breakdown + +### Frontend +- **Framework:** Angular (v20.3) +- **Styling:** SCSS (component-scoped) +- **UI Library:** Angular Material +- **Testing:** Playwright (E2E), Karma + Jasmine (unit) +- **API Communication:** RxJS, HttpClient + +### Backend +- **Framework:** Spring Boot 3 +- **Documentation:** OpenAPI (Swagger) +- **Utilities:** Lombok, MapStruct +- **Persistence:** JPA/Hibernate + +### Data & Infrastructure - **Database:** PostgreSQL -- **Database Migrations:** Flyway -- **Authentication & Security:** JWT, Spring Security -- **Integrations:** Google Calendar API -- **DevOps & CI/CD:** Docker, GitHub Actions +- **Migrations:** Flyway +- **Authentication:** JWT + Spring Security +- **External APIs:** Google Calendar +- **Deployment:** Docker, GitHub Actions CI/CD + +
+ +## 🎨 Design System + +
+ +| 🎨 Primary Color | 🌈 Style | πŸ“± Approach | +|:---:|:---:|:---:| +| `#5FA9A4` | Modern & Techy | SCSS Components | +| Teal Mint | Clean Typography | Responsive Design | + +**Design Philosophy:** Modern interface with clear typography and subtle motion effects. + +
+ +--- + +## πŸ“ Project Structure -## πŸ“ Project Folder Structure -> Example structure: ``` project-root/ -β”œβ”€β”€ frontend/ # Angular app -β”œβ”€β”€ backend/ # Spring Boot app -β”œβ”€β”€ docs/ # Documentation +β”œβ”€β”€ pomodify-frontend/ # Angular app (SCSS styling) +β”‚ β”œβ”€β”€ src/app/ # Components, services, pages +β”‚ β”œβ”€β”€ e2e/ # Playwright tests +β”‚ └── package.json +β”œβ”€β”€ pomodify-backend/ # Spring Boot app +β”‚ β”œβ”€β”€ src/main/java/ # Application logic +β”‚ β”œβ”€β”€ src/test/java/ # Unit tests +β”‚ └── pom.xml +β”œβ”€β”€ document/ # API docs, diagrams, guides +β”œβ”€β”€ deploy-documentation/ # CI/CD & deployment guides └── README.md ``` -## πŸ—‚οΈ Git Workflow Guidelines - -### 🌿 Branch Types and Naming Conventions -| Branch Type | Description | Naming Convention | -|-------------|-----------------------------|-----------------------| -| main | Production branch | main | -| feature | New feature development | feature/ | -| bugfix | Fixes identified bugs | bugfix/ | -| docs | Document related branch | docs/ | - -### πŸ”§ Branching Guidelines -- Create a branch from main for any feature, bugfix, or enhancement. -- Use descriptive branch names (e.g., feature/user-authentication). -- Commit often with meaningful messages. -- Keep branches focused; one purpose per branch. -- All feature, bugfix, and docs branches are merged directly into main after approval. - -## πŸ“œ General Coding Guidelines -- Follow language-specific style guides -- Write clear, maintainable code -- Add comments where necessary -- Use meaningful variable and function names +--- + +## πŸ”§ Development + +### 🌿 Branch Types & Naming + +| Type | Purpose | Convention | +|------|---------|-----------| +| `main` | Production-ready code | `main` | +| `staging` | Pre-production testing | `staging` | +| `feature` | New features | `feature/descriptive-name` | +| `bugfix` | Bug fixes | `bugfix/issue-description` | +| `docs` | Documentation | `docs/what-changed` | + +### βœ… Best Practices + +- βœ”οΈ Create branches from `main` for any work +- βœ”οΈ Use descriptive branch names +- βœ”οΈ Commit frequently with clear messages +- βœ”οΈ Keep one feature per branch +- βœ”οΈ Submit a PR and request review before merging + +--- + +## πŸ“œ Code Guidelines + +- 🎯 **Style:** Follow language-specific guides (Angular, Java, etc.) +- πŸ“ **Readability:** Write clear, self-documenting code +- πŸ’¬ **Comments:** Add them where logic isn't immediately obvious +- 🏷️ **Naming:** Use meaningful variable and function names +- πŸ§ͺ **Testing:** Aim for good test coverage + +--- ## πŸ§‘β€πŸ’» Contributors -- [Hannah Lorainne Genandoy](https://www.linkedin.com/in/hannah-lorainne-genandoy-3b8a1b2b2/) – Project Manager / Developer -- [Daniel Victorioso](https://www.linkedin.com/in/daniel-victorioso-304688292/) – Technical Lead / Developer -- [Ivan Delumen](https://www.linkedin.com/in/ivan-delumen-53982728a/) – UI/UX / Developer -- Gerald Mamasalanang – Tester / Developer -- [Simone Jake Reyes](https://www.linkedin.com/in/simone-jake-reyes-75199234a/) – UI/UX / Developer - -## πŸ“ Developer Documentation -> See the `document/` folder for API docs, architecture diagrams, and more. \ No newline at end of file + +
+ +| πŸ‘€ Name | 🎯 Role | πŸ”— Links | +|:---:|:---:|:---:| +| **Hannah Lorainne Genandoy** | Project Manager / Developer | [![LinkedIn](https://img.shields.io/badge/LinkedIn-0077B5?style=flat&logo=linkedin&logoColor=white)](https://www.linkedin.com/in/hannah-lorainne-genandoy-3b8a1b2b2/) | +| **Daniel Victorioso** | Technical Lead / Developer | [![LinkedIn](https://img.shields.io/badge/LinkedIn-0077B5?style=flat&logo=linkedin&logoColor=white)](https://www.linkedin.com/in/daniel-victorioso-304688292/) | +| **Ivan Delumen** | UI/UX / Developer | [![LinkedIn](https://img.shields.io/badge/LinkedIn-0077B5?style=flat&logo=linkedin&logoColor=white)](https://www.linkedin.com/in/ivan-delumen-53982728a/) | +| **Gerald Mamasalanang** | DevOps/ Developer | [![LinkedIn](https://img.shields.io/badge/LinkedIn-0077B5?style=flat&logo=linkedin&logoColor=white)](https://www.linkedin.com/in/gerald-kasan-mamasalanang-95a306386) | +| **Simone Jake Reyes** | UI/UX / Developer | [![LinkedIn](https://img.shields.io/badge/LinkedIn-0077B5?style=flat&logo=linkedin&logoColor=white)](https://www.linkedin.com/in/simone-jake-reyes-75199234a/) | + +
+ +--- + +## πŸ€– AI-Powered Features + +Pomodify includes intelligent AI features to enhance productivity: + +- 🎯 **Session Suggestions** β€” Context-aware next-step recommendations for your activities +- 🧠 **Smart Blueprints** β€” AI-generated study/learning plans with beginner & intermediate levels +- πŸ“ **Session Notes** β€” AI-backed suggestions to help you summarize and reflect on work + +--- + +## πŸ“š Documentation + +For detailed guides and technical docs, explore: + +- **API Docs:** [pomodify-backend/api-docs](pomodify-backend/api-docs) +- **Architecture & Diagrams:** [document/](document/) +- **CI/CD & Deployment:** [deploy-documentation/](deploy-documentation/) +- **Developer Guide:** [DEVELOPER_GUIDE.md](DEVELOPER_GUIDE.md) + +--- + +
+ +### πŸŽ‰ Developed by the PUPT-DIT 3 G-Cache Team + +[![Live App](https://img.shields.io/badge/🌐_Live_App-Visit_Now-success?style=for-the-badge)](https://pomodify.site/) +[![Documentation](https://img.shields.io/badge/πŸ“–_Documentation-Read_Docs-blue?style=for-the-badge)](document/) +[![Video Demo](https://img.shields.io/badge/πŸŽ₯_Video_Demo-Watch_Walkthrough-FF0000?style=for-the-badge)](https://youtu.be/sMEqr4PYfWk) +[![Issues](https://img.shields.io/badge/πŸ›_Issues-Report_Bug-red?style=for-the-badge)](https://github.com/PUP-BSIT/project-g-cache/issues) + +

+ +![GitHub stars](https://img.shields.io/github/stars/PUP-BSIT/project-g-cache?style=social) +![GitHub forks](https://img.shields.io/github/forks/PUP-BSIT/project-g-cache?style=social) +![GitHub watchers](https://img.shields.io/github/watchers/PUP-BSIT/project-g-cache?style=social) + +
\ No newline at end of file diff --git a/pomodify-frontend/src/app/core/services/badge-notification.service.ts b/pomodify-frontend/src/app/core/services/badge-notification.service.ts index 99bee218..23a9e005 100644 --- a/pomodify-frontend/src/app/core/services/badge-notification.service.ts +++ b/pomodify-frontend/src/app/core/services/badge-notification.service.ts @@ -3,10 +3,9 @@ import { Badge, BadgeService } from './badge.service'; import { Logger } from './logger.service'; export interface BadgeNotification { - id: string; + id: number; badge: Badge; - isRead: boolean; - createdAt: Date; + isNew: boolean; // Based on dateAwarded being recent (e.g., within last 7 days) } // Badge milestone to sound file mapping @@ -69,53 +68,78 @@ const BADGE_MESSAGES: Record([]); - private lastCheckedBadgesSignal = signal([]); private isDropdownOpenSignal = signal(false); + private isLoadingSignal = signal(false); + + // Set of badge IDs that have been marked as read (persisted in localStorage) + private readBadgeIds = new Set(); // Public readonly signals readonly notifications = this.notificationsSignal.asReadonly(); readonly isDropdownOpen = this.isDropdownOpenSignal.asReadonly(); + readonly isLoading = this.isLoadingSignal.asReadonly(); + // Count badges awarded in the last N days as "new" readonly unreadCount = computed(() => { - return this.notificationsSignal().filter(n => !n.isRead).length; + return this.notificationsSignal().filter(n => n.isNew).length; + }); + + // Check if there are any notifications + readonly hasNotifications = computed(() => { + return this.notificationsSignal().length > 0; }); constructor() { - this.loadFromStorage(); + // Load read badge IDs from localStorage on service init + this.loadReadBadgeIds(); } - // Check for new badges and create notifications - checkForNewBadges(): void { + // Fetch badges from backend and convert to notifications + loadBadgeNotifications(): void { + this.isLoadingSignal.set(true); + this.badgeService.getUserBadges().subscribe({ next: (badges) => { - const lastChecked = this.lastCheckedBadgesSignal(); - const newBadges = badges.filter(b => !lastChecked.includes(b.id)); - - if (newBadges.length > 0) { - const newNotifications: BadgeNotification[] = newBadges.map(badge => ({ - id: `badge-${badge.id}-${Date.now()}`, + const notifications: BadgeNotification[] = badges + .sort((a, b) => new Date(b.dateAwarded).getTime() - new Date(a.dateAwarded).getTime()) + .map(badge => ({ + id: badge.id, badge, - isRead: false, - createdAt: new Date() + // Badge is "new" if it was awarded recently AND hasn't been marked as read + isNew: this.isBadgeNew(badge.dateAwarded) && !this.readBadgeIds.has(badge.id) })); - this.notificationsSignal.update(current => [...newNotifications, ...current]); - this.lastCheckedBadgesSignal.set(badges.map(b => b.id)); - this.saveToStorage(); - } + this.notificationsSignal.set(notifications); + this.isLoadingSignal.set(false); }, error: (_err) => { - // Failed to check badges - silently handle + Logger.warn('Failed to load badge notifications'); + this.isLoadingSignal.set(false); } }); } + // Check if a badge was awarded recently + private isBadgeNew(dateAwarded: string): boolean { + const awardedDate = new Date(dateAwarded); + const now = new Date(); + const diffTime = now.getTime() - awardedDate.getTime(); + const diffDays = diffTime / (1000 * 60 * 60 * 24); + return diffDays <= NEW_BADGE_THRESHOLD_DAYS; + } + // Toggle dropdown toggleDropdown(): void { this.isDropdownOpenSignal.update(v => !v); @@ -125,28 +149,67 @@ export class BadgeNotificationService { this.isDropdownOpenSignal.set(false); } - // Mark notification as read - markAsRead(notificationId: string): void { - this.notificationsSignal.update(notifications => - notifications.map(n => - n.id === notificationId ? { ...n, isRead: true } : n - ) + // Mark all notifications as read (remove "new" status) and persist to localStorage + markAllAsRead(): void { + const notifications = this.notificationsSignal(); + + // Add all current notification IDs to the read set + notifications.forEach(n => { + this.readBadgeIds.add(n.id); + }); + + // Persist to localStorage + this.saveReadBadgeIds(); + + // Update the notifications signal + this.notificationsSignal.update(notifications => + notifications.map(n => ({ ...n, isNew: false })) ); - this.saveToStorage(); } - // Mark all as read - markAllAsRead(): void { - this.notificationsSignal.update(notifications => - notifications.map(n => ({ ...n, isRead: true })) + // Mark a single notification as read + markAsRead(badgeId: number): void { + this.readBadgeIds.add(badgeId); + this.saveReadBadgeIds(); + + this.notificationsSignal.update(notifications => + notifications.map(n => n.id === badgeId ? { ...n, isNew: false } : n) ); - this.saveToStorage(); } - // Clear all notifications - clearAll(): void { + // Clear all notifications from the list + clearAllNotifications(): void { + // Mark all as read before clearing + const notifications = this.notificationsSignal(); + notifications.forEach(n => { + this.readBadgeIds.add(n.id); + }); + this.saveReadBadgeIds(); + this.notificationsSignal.set([]); - this.saveToStorage(); + } + + // Load read badge IDs from localStorage + private loadReadBadgeIds(): void { + try { + const stored = localStorage.getItem(READ_BADGES_STORAGE_KEY); + if (stored) { + const ids: number[] = JSON.parse(stored); + this.readBadgeIds = new Set(ids); + } + } catch (e) { + Logger.warn('Failed to load read badge IDs from localStorage:', e); + } + } + + // Save read badge IDs to localStorage + private saveReadBadgeIds(): void { + try { + const ids = Array.from(this.readBadgeIds); + localStorage.setItem(READ_BADGES_STORAGE_KEY, JSON.stringify(ids)); + } catch (e) { + Logger.warn('Failed to save read badge IDs to localStorage:', e); + } } // Get badge message info @@ -180,35 +243,4 @@ export class BadgeNotificationService { } return null; } - - // Persistence - private saveToStorage(): void { - try { - localStorage.setItem('badge_notifications', JSON.stringify(this.notificationsSignal())); - localStorage.setItem('last_checked_badges', JSON.stringify(this.lastCheckedBadgesSignal())); - } catch (e) { - Logger.warn('Could not save notifications to storage:', e); - } - } - - private loadFromStorage(): void { - try { - const stored = localStorage.getItem('badge_notifications'); - const lastChecked = localStorage.getItem('last_checked_badges'); - - if (stored) { - const parsed = JSON.parse(stored); - this.notificationsSignal.set(parsed.map((n: any) => ({ - ...n, - createdAt: new Date(n.createdAt) - }))); - } - - if (lastChecked) { - this.lastCheckedBadgesSignal.set(JSON.parse(lastChecked)); - } - } catch (e) { - Logger.warn('Could not load notifications from storage:', e); - } - } } diff --git a/pomodify-frontend/src/app/core/services/global-timer.service.ts b/pomodify-frontend/src/app/core/services/global-timer.service.ts index 87e703be..294a6fc1 100644 --- a/pomodify-frontend/src/app/core/services/global-timer.service.ts +++ b/pomodify-frontend/src/app/core/services/global-timer.service.ts @@ -1,6 +1,6 @@ import { Injectable, inject, signal, computed, OnDestroy, PLATFORM_ID } from '@angular/core'; import { isPlatformBrowser, DOCUMENT } from '@angular/common'; -import { interval, Subscription, Subject } from 'rxjs'; +import { interval, Subscription } from 'rxjs'; import { Router, NavigationEnd } from '@angular/router'; import { filter } from 'rxjs/operators'; import { Logger } from './logger.service'; @@ -65,20 +65,27 @@ export class GlobalTimerService implements OnDestroy { const onSessionPage = event.url.includes('/sessions/') && !event.url.endsWith('/sessions'); this._isOnSessionPage.set(onSessionPage); - // Update browser tab title when navigating away from session page - // (session-timer handles its own title when on that page) + // When navigating AWAY from session page, restore original title + // Timer in title tab should ONLY be visible on the session timer page + // Use setTimeout to ensure this runs AFTER any component effects if (!onSessionPage) { - this.updateBrowserTitle(); + setTimeout(() => this.restoreOriginalTitle(), 0); } }); - // Check initial route - this._isOnSessionPage.set(this.router.url.includes('/sessions/') && !this.router.url.endsWith('/sessions')); + // Check initial route and set title accordingly + const initialOnSessionPage = this.router.url.includes('/sessions/') && !this.router.url.endsWith('/sessions'); + this._isOnSessionPage.set(initialOnSessionPage); + + // If not on session page initially, ensure title is correct + if (!initialOnSessionPage) { + this.restoreOriginalTitle(); + } // Load any persisted timer state on init this.loadPersistedState(); - // Start tick interval to update remaining time and browser title + // Start tick interval to update remaining time (title is handled by session-timer component) this.startTicking(); } @@ -99,10 +106,9 @@ export class GlobalTimerService implements OnDestroy { this._timerState.set(state); this.persistState(state); - // Update browser title if not on session page - if (!this._isOnSessionPage()) { - this.updateBrowserTitle(); - } + // Do NOT update browser title here + // Timer in title tab should ONLY be visible on the session timer page + // session-timer component handles its own title } /** @@ -129,27 +135,11 @@ export class GlobalTimerService implements OnDestroy { } /** - * Update the browser tab title with timer info + * Restore the original browser tab title + * Used when navigating away from session timer page */ - private updateBrowserTitle(): void { - if (!isPlatformBrowser(this.platformId)) return; - - const state = this._timerState(); - - if (state && (state.isRunning || state.isPaused)) { - const m = Math.floor(state.remainingSeconds / 60); - const s = state.remainingSeconds % 60; - const timeDisplay = `${m.toString().padStart(2, '0')}:${s.toString().padStart(2, '0')}`; - - // Phase emoji - const phaseEmoji = state.currentPhase === 'FOCUS' ? '🎯' : 'β˜•'; - - // Pause indicator - const pauseIcon = state.isPaused ? '⏸ ' : ''; - - // Format: "⏸ 16:11 🎯 Pomodify" or "16:11 β˜• Pomodify" - this.document.title = `${pauseIcon}${timeDisplay} ${phaseEmoji} Pomodify`; - } else { + private restoreOriginalTitle(): void { + if (isPlatformBrowser(this.platformId)) { this.document.title = this.originalTitle; } } @@ -160,11 +150,12 @@ export class GlobalTimerService implements OnDestroy { if (state && state.isRunning && state.remainingSeconds > 0) { // Decrement locally this._timerState.update(s => s ? { ...s, remainingSeconds: Math.max(0, s.remainingSeconds - 1) } : null); - - // Update browser title if not on session page - if (!this._isOnSessionPage()) { - this.updateBrowserTitle(); - } + } + + // ALWAYS ensure title is correct when NOT on session page + // This overrides any stale effects from session-timer component + if (!this._isOnSessionPage() && isPlatformBrowser(this.platformId)) { + this.document.title = this.originalTitle; } }); } @@ -193,10 +184,8 @@ export class GlobalTimerService implements OnDestroy { } this._timerState.set(state); - // Update browser title on load - if (!this._isOnSessionPage()) { - this.updateBrowserTitle(); - } + // Do NOT update browser title on load + // Timer in title should ONLY show on session timer page } else { this.clearPersistedState(); } diff --git a/pomodify-frontend/src/app/pages/dashboard/dashboard.ts b/pomodify-frontend/src/app/pages/dashboard/dashboard.ts index 21dcd005..dd3f796d 100644 --- a/pomodify-frontend/src/app/pages/dashboard/dashboard.ts +++ b/pomodify-frontend/src/app/pages/dashboard/dashboard.ts @@ -97,8 +97,8 @@ export class Dashboard implements OnInit { } this.loadDashboardMetrics(); this.loadCategories(); - // Check for new badge achievements - this.badgeNotificationService.checkForNewBadges(); + // Load badge notifications from backend + this.badgeNotificationService.loadBadgeNotifications(); this.auth.fetchAndStoreUserProfile().then(user => { Logger.log('[Dashboard] User profile fetched:', user); Logger.log('[Dashboard] profilePictureUrl from API:', user?.profilePictureUrl); diff --git a/pomodify-frontend/src/app/pages/session-timer/session-timer.scss b/pomodify-frontend/src/app/pages/session-timer/session-timer.scss index 2d2a597b..40d0449c 100644 --- a/pomodify-frontend/src/app/pages/session-timer/session-timer.scss +++ b/pomodify-frontend/src/app/pages/session-timer/session-timer.scss @@ -1471,7 +1471,7 @@ $glow-shadow: 0 0 40px rgba(95, 169, 164, 0.3); .todo-item { display: flex; - align-items: center; + align-items: flex-start; gap: 12px; background: $bg-light; border-radius: 10px; @@ -1492,6 +1492,7 @@ $glow-shadow: 0 0 40px rgba(95, 169, 164, 0.3); cursor: pointer; accent-color: var(--activity-color); flex-shrink: 0; + margin-top: 2px; } .todo-text { @@ -1503,14 +1504,17 @@ $glow-shadow: 0 0 40px rgba(95, 169, 164, 0.3); outline: none; padding: 2px 4px; resize: none; - min-height: auto; + min-height: 24px; height: auto; overflow: hidden; + overflow-y: auto; word-wrap: break-word; white-space: pre-wrap; word-break: break-word; line-height: 1.4; margin: 0; + max-height: none; + field-sizing: content; &:focus { background: rgba(95, 169, 164, 0.05) !important; diff --git a/pomodify-frontend/src/app/pages/session-timer/session-timer.ts b/pomodify-frontend/src/app/pages/session-timer/session-timer.ts index c82970ff..c124eb17 100644 --- a/pomodify-frontend/src/app/pages/session-timer/session-timer.ts +++ b/pomodify-frontend/src/app/pages/session-timer/session-timer.ts @@ -280,6 +280,9 @@ export class SessionTimerComponent implements OnDestroy { // Ensure nextTodoId is ahead of the max existing id const maxId = parsed.reduce((max, t) => Math.max(max, t.id), 0); this.nextTodoId = maxId + 1; + + // Auto-expand todos after DOM updates + setTimeout(() => this.autoExpandAllTodos(), 100); } catch (e) { Logger.warn('[Session Timer] Failed to load todos from storage', e); } @@ -342,19 +345,17 @@ export class SessionTimerComponent implements OnDestroy { } // Effect to update browser tab title with timer + // Timer in title should ONLY show on the session timer page effect(() => { if (isPlatformBrowser(this.platformId)) { const timerDisplay = this.timerDisplay(); - const phase = this.currentPhase(); const isRunning = this.isRunning(); const isPaused = this.isPaused(); const sess = this.session(); if (sess && (isRunning || isPaused)) { - // Show timer in tab: "16:11 Focus | Pomodify" or "⏸ 16:11 | Pomodify" - const phaseLabel = phase === 'FOCUS' ? '🎯' : 'β˜•'; - const pauseIcon = isPaused ? '⏸ ' : ''; - this.document.title = `${pauseIcon}${timerDisplay} ${phaseLabel} Pomodify`; + // Show timer in tab: "01:54 Pomodify" (no emojis) + this.document.title = `${timerDisplay} Pomodify`; } else { // Restore original title when not running this.document.title = this.originalTitle; @@ -577,6 +578,8 @@ export class SessionTimerComponent implements OnDestroy { this.todos.set(backendTodos); const maxId = backendTodos.reduce((max: number, t: any) => Math.max(max, t.id), 0); this.nextTodoId = maxId + 1; + // Auto-expand todos after DOM updates + setTimeout(() => this.autoExpandAllTodos(), 100); } else { // Fallback to local storage if no backend todos this.loadTodosFromStorage(sess); @@ -597,6 +600,9 @@ export class SessionTimerComponent implements OnDestroy { // Check for missed notification (backend sent FCM but user wasn't in app) this.checkForMissedNotification(sess); + + // Auto-expand all todos after page load + setTimeout(() => this.autoExpandAllTodos(), 200); } } }); @@ -1744,6 +1750,9 @@ export class SessionTimerComponent implements OnDestroy { this.onTodosChanged(); this.isGeneratingAi.set(false); + // Auto-expand all todo textareas after a short delay to allow DOM to update + setTimeout(() => this.autoExpandAllTodos(), 50); + Logger.log(`βœ… Added ${suggestions.length} AI-suggested todo(s)`); }, error: (_err) => { @@ -1753,6 +1762,19 @@ export class SessionTimerComponent implements OnDestroy { }); } + /** + * Auto-expand all todo textareas to fit their content + */ + private autoExpandAllTodos(): void { + if (typeof document === 'undefined') return; + + const textareas = document.querySelectorAll('.todo-text') as NodeListOf; + textareas.forEach(textarea => { + textarea.style.height = 'auto'; + textarea.style.height = textarea.scrollHeight + 'px'; + }); + } + /** * Parse AI suggestion text into individual todo items */ diff --git a/pomodify-frontend/src/app/pages/signup/signup.scss b/pomodify-frontend/src/app/pages/signup/signup.scss index fafbd05c..400c50ae 100644 --- a/pomodify-frontend/src/app/pages/signup/signup.scss +++ b/pomodify-frontend/src/app/pages/signup/signup.scss @@ -250,6 +250,26 @@ input { padding-right: 52px; /* leave room for eye icon */ + + /* Hide browser's native password reveal icon */ + &::-ms-reveal, + &::-ms-clear { + display: none; + } + + /* Hide Chrome/Edge password reveal icon */ + &::-webkit-credentials-auto-fill-button, + &::-webkit-clear-button { + display: none !important; + } + } + + /* Hide Edge/Chrome password reveal button */ + input[type="password"]::-ms-reveal, + input[type="password"]::-ms-clear, + input[type="text"]::-ms-reveal, + input[type="text"]::-ms-clear { + display: none !important; } } diff --git a/pomodify-frontend/src/app/shared/components/create-activity-modal/create-activity-modal.scss b/pomodify-frontend/src/app/shared/components/create-activity-modal/create-activity-modal.scss index e6471e94..cfc59ff9 100644 --- a/pomodify-frontend/src/app/shared/components/create-activity-modal/create-activity-modal.scss +++ b/pomodify-frontend/src/app/shared/components/create-activity-modal/create-activity-modal.scss @@ -1,11 +1,13 @@ .modal-container { padding: 0; - min-width: 500px; + width: 100%; max-width: 500px; background: var(--card); color: var(--text); border-radius: 16px; overflow: hidden; + max-height: 90vh; + overflow-y: auto; } .modal-header { @@ -318,106 +320,228 @@ @media (max-width: 600px) { .modal-container { - min-width: auto; max-width: 100%; - width: 100%; - margin: 0; + width: calc(100vw - 32px); + margin: 0 auto; border-radius: 16px; + max-height: 85vh; } .modal-header { - padding: 12px 16px; + padding: 16px 20px; border-radius: 0; h2 { - font-size: 15px; + font-size: 20px; } .close-btn { - width: 22px; - height: 22px; - font-size: 12px; + width: 28px; + height: 28px; + font-size: 16px; } } .modal-form { - padding: 10px 16px; - gap: 8px; + padding: 16px 20px; + gap: 16px; } .form-group { - gap: 2px; + gap: 6px; label { - font-size: 11px; + font-size: 13px; } } .helper-text { - font-size: 9px; - margin-bottom: 2px; + font-size: 11px; + margin-bottom: 6px; } .form-input { - padding: 7px 10px; - font-size: 13px; - border-radius: 5px; + padding: 10px 12px; + font-size: 14px; + border-radius: 6px; } .color-picker-card { - padding: 8px; - border-radius: 8px; + padding: 16px; + border-radius: 12px; } .color-preview-large { - height: 36px; - margin-bottom: 6px; - border-radius: 6px; + height: 60px; + margin-bottom: 16px; + border-radius: 10px; } .preview-hex-overlay { - font-size: 8px; - padding: 2px 5px; + font-size: 11px; + padding: 4px 10px; } .slider-section { - gap: 4px; + gap: 10px; } .color-slider-track { - height: 12px; + height: 16px; + border-radius: 8px; + } + + .color-slider-thumb { + width: 26px; + height: 26px; + border-width: 3px; + } + + .slider-label { + font-size: 11px; + } + + .modal-actions { + padding: 16px 20px; + gap: 12px; + border-radius: 0; + } + + .btn { + padding: 10px 16px; + font-size: 13px; + flex: 1; border-radius: 6px; } + .char-counter { + font-size: 11px; + margin-top: 4px; + } + + .error-message { + font-size: 11px; + } +} + +// Extra small devices (phones in portrait) +@media (max-width: 400px) { + .modal-container { + width: calc(100vw - 24px); + max-height: 80vh; + } + + .modal-header { + padding: 14px 16px; + + h2 { + font-size: 18px; + } + + .close-btn { + width: 26px; + height: 26px; + font-size: 14px; + } + } + + .modal-form { + padding: 14px 16px; + gap: 14px; + } + + .form-group { + gap: 4px; + + label { + font-size: 12px; + } + } + + .helper-text { + font-size: 10px; + margin-bottom: 4px; + } + + .form-input { + padding: 8px 10px; + font-size: 13px; + } + + .color-picker-card { + padding: 12px; + border-radius: 10px; + } + + .color-preview-large { + height: 50px; + margin-bottom: 12px; + border-radius: 8px; + } + + .preview-hex-overlay { + font-size: 10px; + padding: 3px 8px; + } + + .color-slider-track { + height: 14px; + } + .color-slider-thumb { - width: 20px; - height: 20px; + width: 22px; + height: 22px; border-width: 2px; } .slider-label { - font-size: 8px; + font-size: 10px; } .modal-actions { - padding: 10px 16px; + padding: 14px 16px; gap: 10px; - border-radius: 0; } .btn { - padding: 8px 12px; - font-size: 11px; - flex: 1; - border-radius: 5px; + padding: 8px 14px; + font-size: 12px; } .char-counter { - font-size: 9px; - margin-top: 1px; + font-size: 10px; } .error-message { - font-size: 9px; + font-size: 10px; + } +} + +// Very small devices or landscape phones +@media (max-height: 600px) { + .modal-container { + max-height: 95vh; + } + + .modal-header { + padding: 12px 16px; + + h2 { + font-size: 16px; + } + } + + .modal-form { + padding: 12px 16px; + gap: 12px; + } + + .color-preview-large { + height: 40px; + margin-bottom: 10px; + } + + .modal-actions { + padding: 12px 16px; } } diff --git a/pomodify-frontend/src/app/shared/components/header/header.html b/pomodify-frontend/src/app/shared/components/header/header.html index a5febdc0..8611d9cf 100644 --- a/pomodify-frontend/src/app/shared/components/header/header.html +++ b/pomodify-frontend/src/app/shared/components/header/header.html @@ -23,8 +23,6 @@ - Contact - diff --git a/pomodify-frontend/src/app/shared/components/notification-bell/notification-bell.component.ts b/pomodify-frontend/src/app/shared/components/notification-bell/notification-bell.component.ts index bcc3aa03..d42ae1ba 100644 --- a/pomodify-frontend/src/app/shared/components/notification-bell/notification-bell.component.ts +++ b/pomodify-frontend/src/app/shared/components/notification-bell/notification-bell.component.ts @@ -1,4 +1,4 @@ -import { Component, inject, HostListener, ElementRef, Input, effect } from '@angular/core'; +import { Component, inject, HostListener, ElementRef, Input, effect, OnInit } from '@angular/core'; import { CommonModule } from '@angular/common'; import { MatDialog } from '@angular/material/dialog'; import { BadgeNotificationService, BadgeNotification } from '../../../core/services/badge-notification.service'; @@ -34,10 +34,10 @@ import { BadgeAchievementDialogComponent } from '../badge-achievement-dialog/bad

Achievements

- @if (notifications().length > 0) { - }