generated from nhcarrigan/template
feat: add time tracking to all media types
Implements issue #57 with manual time entry functionality: - Added timeSpent field (stored in minutes) to all media models - Supports hours and minutes input in forms - Displays formatted time on media cards Database Schema: - Added timeSpent Int? field to Game, Book, Music, Show, and Manga models - Stored in minutes for consistency and easy calculation Shared Types: - Updated all media type interfaces with timeSpent? field - Added to CreateDto and main interfaces for all media types Frontend (Games - template for other types): - Added hour/minute input fields to Add and Edit forms - Split input with validation (minutes 0-59) - Auto-calculates total minutes on change - Formats display as "Xh Ym", "Xh", or "Ym" as appropriate - Green highlighted time display on cards with ⏱️ icon - Populates edit form from existing time data Implementation Notes: - Backend services require no changes (DTOs handle automatically) - Time conversion helpers for display and storage - Form state properly resets time fields - Edit mode correctly splits minutes back to hours/minutes Next Steps: - Apply same UI pattern to Books, Music, Shows, Manga - Add time statistics to user profiles - Consider aggregate time tracking views
This commit is contained in:
@@ -34,6 +34,7 @@ model Game {
|
||||
links Link[]
|
||||
series String?
|
||||
seriesOrder Int? @db.Int
|
||||
timeSpent Int? @db.Int
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
comments Comment[]
|
||||
@@ -62,6 +63,7 @@ model Book {
|
||||
links Link[]
|
||||
series String?
|
||||
seriesOrder Int? @db.Int
|
||||
timeSpent Int? @db.Int
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
comments Comment[]
|
||||
@@ -89,6 +91,7 @@ model Music {
|
||||
coverArt String?
|
||||
tags String[]
|
||||
links Link[]
|
||||
timeSpent Int? @db.Int
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
comments Comment[]
|
||||
@@ -135,6 +138,7 @@ model Show {
|
||||
coverImage String?
|
||||
tags String[]
|
||||
links Link[]
|
||||
timeSpent Int? @db.Int
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
comments Comment[]
|
||||
@@ -168,6 +172,7 @@ model Manga {
|
||||
coverImage String?
|
||||
tags String[]
|
||||
links Link[]
|
||||
timeSpent Int? @db.Int
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
comments Comment[]
|
||||
|
||||
@@ -104,6 +104,34 @@ import { Game, GameStatus, CreateGameDto, UpdateGameDto, Comment, SuggestionEnti
|
||||
>
|
||||
</div>
|
||||
|
||||
<div class="form-row">
|
||||
<div class="form-group">
|
||||
<label for="timeHours">Time Spent (Hours)</label>
|
||||
<input
|
||||
type="number"
|
||||
id="timeHours"
|
||||
[(ngModel)]="newGameTimeHours"
|
||||
name="timeHours"
|
||||
min="0"
|
||||
placeholder="0"
|
||||
(ngModelChange)="updateNewGameTimeSpent()"
|
||||
>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="timeMinutes">Time Spent (Minutes)</label>
|
||||
<input
|
||||
type="number"
|
||||
id="timeMinutes"
|
||||
[(ngModel)]="newGameTimeMinutes"
|
||||
name="timeMinutes"
|
||||
min="0"
|
||||
max="59"
|
||||
placeholder="0"
|
||||
(ngModelChange)="updateNewGameTimeSpent()"
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="notes">Notes</label>
|
||||
<textarea
|
||||
@@ -277,6 +305,34 @@ import { Game, GameStatus, CreateGameDto, UpdateGameDto, Comment, SuggestionEnti
|
||||
>
|
||||
</div>
|
||||
|
||||
<div class="form-row">
|
||||
<div class="form-group">
|
||||
<label for="edit-timeHours">Time Spent (Hours)</label>
|
||||
<input
|
||||
type="number"
|
||||
id="edit-timeHours"
|
||||
[(ngModel)]="editGameTimeHours"
|
||||
name="timeHours"
|
||||
min="0"
|
||||
placeholder="0"
|
||||
(ngModelChange)="updateEditGameTimeSpent()"
|
||||
>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="edit-timeMinutes">Time Spent (Minutes)</label>
|
||||
<input
|
||||
type="number"
|
||||
id="edit-timeMinutes"
|
||||
[(ngModel)]="editGameTimeMinutes"
|
||||
name="timeMinutes"
|
||||
min="0"
|
||||
max="59"
|
||||
placeholder="0"
|
||||
(ngModelChange)="updateEditGameTimeSpent()"
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="edit-notes">Notes</label>
|
||||
<textarea
|
||||
@@ -574,6 +630,12 @@ import { Game, GameStatus, CreateGameDto, UpdateGameDto, Comment, SuggestionEnti
|
||||
</div>
|
||||
}
|
||||
|
||||
@if (game.timeSpent) {
|
||||
<p class="time-spent">
|
||||
⏱️ Time Played: {{ formatTimeSpent(game.timeSpent) }}
|
||||
</p>
|
||||
}
|
||||
|
||||
<app-like-button
|
||||
entityType="game"
|
||||
[entityId]="game.id"
|
||||
@@ -741,6 +803,13 @@ import { Game, GameStatus, CreateGameDto, UpdateGameDto, Comment, SuggestionEnti
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.form-row {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 1rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.form-actions {
|
||||
display: flex;
|
||||
gap: 1rem;
|
||||
@@ -942,6 +1011,13 @@ import { Game, GameStatus, CreateGameDto, UpdateGameDto, Comment, SuggestionEnti
|
||||
margin: 0.5rem 0;
|
||||
}
|
||||
|
||||
.time-spent {
|
||||
font-size: 0.9rem;
|
||||
color: #10b981;
|
||||
font-weight: 500;
|
||||
margin: 0.5rem 0;
|
||||
}
|
||||
|
||||
.date-started,
|
||||
.date-finished,
|
||||
.date-added,
|
||||
@@ -1369,6 +1445,12 @@ export class GamesListComponent implements OnInit {
|
||||
|
||||
editGame: Partial<UpdateGameDto> = {};
|
||||
|
||||
// Time tracking state
|
||||
newGameTimeHours = 0;
|
||||
newGameTimeMinutes = 0;
|
||||
editGameTimeHours = 0;
|
||||
editGameTimeMinutes = 0;
|
||||
|
||||
// Tags and links input state
|
||||
newTagInput = '';
|
||||
editTagInput = '';
|
||||
@@ -1461,6 +1543,8 @@ export class GamesListComponent implements OnInit {
|
||||
tags: [],
|
||||
links: []
|
||||
};
|
||||
this.newGameTimeHours = 0;
|
||||
this.newGameTimeMinutes = 0;
|
||||
this.newGameImagePreview.set(null);
|
||||
this.imageError.set(null);
|
||||
this.newTagInput = '';
|
||||
@@ -1468,6 +1552,16 @@ export class GamesListComponent implements OnInit {
|
||||
this.newLinkUrl = '';
|
||||
}
|
||||
|
||||
updateNewGameTimeSpent() {
|
||||
const totalMinutes = (this.newGameTimeHours * 60) + this.newGameTimeMinutes;
|
||||
this.newGame.timeSpent = totalMinutes > 0 ? totalMinutes : undefined;
|
||||
}
|
||||
|
||||
updateEditGameTimeSpent() {
|
||||
const totalMinutes = (this.editGameTimeHours * 60) + this.editGameTimeMinutes;
|
||||
this.editGame.timeSpent = totalMinutes > 0 ? totalMinutes : undefined;
|
||||
}
|
||||
|
||||
addTag(target: 'new' | 'edit') {
|
||||
const input = target === 'new' ? this.newTagInput.trim() : this.editTagInput.trim();
|
||||
if (!input) return;
|
||||
@@ -1557,8 +1651,17 @@ export class GamesListComponent implements OnInit {
|
||||
tags: [...(game.tags || [])],
|
||||
links: [...(game.links || [])],
|
||||
series: game.series,
|
||||
seriesOrder: game.seriesOrder
|
||||
seriesOrder: game.seriesOrder,
|
||||
timeSpent: game.timeSpent
|
||||
};
|
||||
// Populate time fields from existing timeSpent
|
||||
if (game.timeSpent) {
|
||||
this.editGameTimeHours = Math.floor(game.timeSpent / 60);
|
||||
this.editGameTimeMinutes = game.timeSpent % 60;
|
||||
} else {
|
||||
this.editGameTimeHours = 0;
|
||||
this.editGameTimeMinutes = 0;
|
||||
}
|
||||
this.editGameImagePreview.set(game.coverImage || null);
|
||||
this.showAddForm.set(false);
|
||||
this.imageError.set(null);
|
||||
@@ -1650,6 +1753,19 @@ export class GamesListComponent implements OnInit {
|
||||
return new Date(date).toLocaleDateString();
|
||||
}
|
||||
|
||||
formatTimeSpent(minutes: number): string {
|
||||
const hours = Math.floor(minutes / 60);
|
||||
const mins = minutes % 60;
|
||||
|
||||
if (hours === 0) {
|
||||
return `${mins}m`;
|
||||
} else if (mins === 0) {
|
||||
return `${hours}h`;
|
||||
} else {
|
||||
return `${hours}h ${mins}m`;
|
||||
}
|
||||
}
|
||||
|
||||
toggleComments(gameId: string) {
|
||||
const expanded = this.expandedComments();
|
||||
const isCurrentlyExpanded = expanded[gameId];
|
||||
|
||||
@@ -29,6 +29,7 @@ interface Book {
|
||||
links: Array<Link>;
|
||||
series?: string;
|
||||
seriesOrder?: number;
|
||||
timeSpent?: number;
|
||||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
}
|
||||
@@ -47,6 +48,7 @@ interface CreateBookDto {
|
||||
links?: Array<Link>;
|
||||
series?: string;
|
||||
seriesOrder?: number;
|
||||
timeSpent?: number;
|
||||
}
|
||||
|
||||
interface UpdateBookDto extends Partial<CreateBookDto> {
|
||||
|
||||
@@ -29,6 +29,7 @@ interface Game {
|
||||
links: Array<Link>;
|
||||
series?: string;
|
||||
seriesOrder?: number;
|
||||
timeSpent?: number;
|
||||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
}
|
||||
@@ -46,6 +47,7 @@ interface CreateGameDto {
|
||||
links?: Array<Link>;
|
||||
series?: string;
|
||||
seriesOrder?: number;
|
||||
timeSpent?: number;
|
||||
}
|
||||
|
||||
interface UpdateGameDto extends Partial<CreateGameDto> {
|
||||
|
||||
@@ -27,6 +27,7 @@ interface Manga {
|
||||
coverImage?: string;
|
||||
tags: Array<string>;
|
||||
links: Array<Link>;
|
||||
timeSpent?: number;
|
||||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
}
|
||||
@@ -42,6 +43,7 @@ interface CreateMangaDto {
|
||||
coverImage?: string;
|
||||
tags?: Array<string>;
|
||||
links?: Array<Link>;
|
||||
timeSpent?: number;
|
||||
}
|
||||
|
||||
interface UpdateMangaDto extends Partial<CreateMangaDto> {
|
||||
|
||||
@@ -34,6 +34,7 @@ interface Music {
|
||||
coverArt?: string;
|
||||
tags: Array<string>;
|
||||
links: Array<Link>;
|
||||
timeSpent?: number;
|
||||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
}
|
||||
@@ -50,6 +51,7 @@ interface CreateMusicDto {
|
||||
coverArt?: string;
|
||||
tags?: Array<string>;
|
||||
links?: Array<Link>;
|
||||
timeSpent?: number;
|
||||
}
|
||||
|
||||
interface UpdateMusicDto extends Partial<CreateMusicDto> {
|
||||
|
||||
@@ -34,6 +34,7 @@ interface Show {
|
||||
coverImage?: string;
|
||||
tags: Array<string>;
|
||||
links: Array<Link>;
|
||||
timeSpent?: number;
|
||||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
}
|
||||
@@ -49,6 +50,7 @@ interface CreateShowDto {
|
||||
coverImage?: string;
|
||||
tags?: Array<string>;
|
||||
links?: Array<Link>;
|
||||
timeSpent?: number;
|
||||
}
|
||||
|
||||
interface UpdateShowDto extends Partial<CreateShowDto> {
|
||||
|
||||
Reference in New Issue
Block a user