#14: add timestamp for inserted & updated

This commit is contained in:
Your Name 2025-01-29 17:38:35 +01:00
parent 2f35648264
commit 9f25253ade
8 changed files with 311 additions and 25 deletions

View File

@ -38,6 +38,8 @@ export class DecksController {
reps: entry.reps, reps: entry.reps,
lapses: entry.lapses, lapses: entry.lapses,
isGraduated: Boolean(entry.isGraduated), isGraduated: Boolean(entry.isGraduated),
inserted: new Date(entry.inserted!!),
updated: new Date(entry.updated!!),
}); });
} }
} }
@ -83,6 +85,8 @@ export class DecksController {
reps: entry.reps, reps: entry.reps,
lapses: entry.lapses, lapses: entry.lapses,
isGraduated: Boolean(entry.isGraduated), isGraduated: Boolean(entry.isGraduated),
inserted: new Date(entry.inserted!!),
updated: new Date(entry.updated!!),
}); });
} }
} }

View File

@ -1,6 +1,6 @@
// drizzle.service.ts // drizzle.service.ts
import { HttpException, HttpStatus, Injectable } from '@nestjs/common'; import { HttpException, HttpStatus, Injectable } from '@nestjs/common';
import { and, eq, isNull } from 'drizzle-orm'; import { and, eq, isNull, sql } from 'drizzle-orm';
import { drizzle } from 'drizzle-orm/libsql'; import { drizzle } from 'drizzle-orm/libsql';
import { Deck, User } from '../db/schema'; import { Deck, User } from '../db/schema';
@ -8,15 +8,32 @@ import { Deck, User } from '../db/schema';
export class DrizzleService { export class DrizzleService {
private db = drizzle('file:local.db'); private db = drizzle('file:local.db');
/**
* Methode zum Abrufen aller Decks eines Benutzers
*/
async getDecks(user: User) { async getDecks(user: User) {
return this.db.select().from(Deck).where(eq(Deck.user, user.email)); return this.db.select().from(Deck).where(eq(Deck.user, user.email));
} }
/**
* Methode zum Erstellen eines neuen Decks
*/
async createDeck(deckname: string, user: User) { async createDeck(deckname: string, user: User) {
const result = await this.db.insert(Deck).values({ deckname, user: user.email }).returning(); // 'inserted' und 'updated' werden automatisch von der Datenbank gesetzt
const result = await this.db
.insert(Deck)
.values({
deckname,
user: user.email,
// 'inserted' und 'updated' werden von der DB automatisch gesetzt
})
.returning();
return { status: 'success', deck: result }; return { status: 'success', deck: result };
} }
/**
* Methode zum Abrufen eines Decks nach Name
*/
async getDeckByName(deckname: string, user: User) { async getDeckByName(deckname: string, user: User) {
return this.db return this.db
.select() .select()
@ -24,6 +41,9 @@ export class DrizzleService {
.where(and(eq(Deck.deckname, deckname), eq(Deck.user, user.email))); .where(and(eq(Deck.deckname, deckname), eq(Deck.user, user.email)));
} }
/**
* Methode zum Löschen eines Decks
*/
async deleteDeck(deckname: string, user: User) { async deleteDeck(deckname: string, user: User) {
const existingDeck = await this.getDeckByName(deckname, user); const existingDeck = await this.getDeckByName(deckname, user);
if (existingDeck.length === 0) { if (existingDeck.length === 0) {
@ -34,6 +54,9 @@ export class DrizzleService {
return { status: 'success' }; return { status: 'success' };
} }
/**
* Methode zum Umbenennen eines Decks
*/
async renameDeck(oldDeckname: string, newDeckname: string, user: User) { async renameDeck(oldDeckname: string, newDeckname: string, user: User) {
const existingDeck = await this.getDeckByName(oldDeckname, user); const existingDeck = await this.getDeckByName(oldDeckname, user);
if (existingDeck.length === 0) { if (existingDeck.length === 0) {
@ -47,17 +70,34 @@ export class DrizzleService {
await this.db await this.db
.update(Deck) .update(Deck)
.set({ deckname: newDeckname }) .set({
deckname: newDeckname,
updated: sql`CURRENT_TIMESTAMP`, // Setze 'updated' auf CURRENT_TIMESTAMP
})
.where(and(eq(Deck.deckname, oldDeckname), eq(Deck.user, user.email))); .where(and(eq(Deck.deckname, oldDeckname), eq(Deck.user, user.email)));
return { status: 'success', message: 'Deck renamed successfully' }; return { status: 'success', message: 'Deck renamed successfully' };
} }
async updateImage(data: { deckname: string; bildname: string; bildid: string; boxes: Array<{ x1: number; x2: number; y1: number; y2: number }> }, user: User) { /**
* Methode zum Aktualisieren eines Bildes innerhalb eines Decks
* Die Methode erhält jetzt auch den 'inserted' Timestamp
*/
async updateImage(
data: {
deckname: string;
bildname: string;
bildid: string;
boxes: Array<{ x1: number; x2: number; y1: number; y2: number }>;
inserted: string; // Neuer Parameter für den 'inserted' Timestamp
},
user: User,
) {
const existingDeck = await this.getDeckByName(data.deckname, user); const existingDeck = await this.getDeckByName(data.deckname, user);
if (existingDeck.length === 0) { if (existingDeck.length === 0) {
throw new HttpException('Deck not found', HttpStatus.NOT_FOUND); throw new HttpException('Deck not found', HttpStatus.NOT_FOUND);
} }
// Lösche vorhandene Einträge für das Bild
await this.db.delete(Deck).where(and(eq(Deck.deckname, data.deckname), eq(Deck.bildid, data.bildid), eq(Deck.user, user.email))); await this.db.delete(Deck).where(and(eq(Deck.deckname, data.deckname), eq(Deck.bildid, data.bildid), eq(Deck.user, user.email)));
const insertedImages: any = []; const insertedImages: any = [];
@ -75,6 +115,8 @@ export class DrizzleService {
y1: box.y1, y1: box.y1,
y2: box.y2, y2: box.y2,
user: user.email, user: user.email,
inserted: data.inserted, // Setze 'inserted' auf den übergebenen Wert
// 'updated' wird automatisch von der DB gesetzt
}) })
.returning(); .returning();
insertedImages.push(result); insertedImages.push(result);
@ -83,6 +125,9 @@ export class DrizzleService {
return { status: 'success', inserted_images: insertedImages }; return { status: 'success', inserted_images: insertedImages };
} }
/**
* Methode zum Löschen von Bildern anhand der bildid
*/
async deleteImagesByBildId(bildid: string, user: User) { async deleteImagesByBildId(bildid: string, user: User) {
const affectedDecks = await this.db const affectedDecks = await this.db
.select({ deckname: Deck.deckname }) .select({ deckname: Deck.deckname })
@ -100,7 +145,7 @@ export class DrizzleService {
const remainingImages = await this.db const remainingImages = await this.db
.select() .select()
.from(Deck) .from(Deck)
.where(and(eq(Deck.deckname, deck.deckname), eq(Deck.bildid, bildid))) .where(and(eq(Deck.deckname, deck.deckname), eq(Deck.bildid, bildid), eq(Deck.user, user.email)))
.all(); .all();
if (remainingImages.length === 0) { if (remainingImages.length === 0) {
@ -111,7 +156,11 @@ export class DrizzleService {
.all(); .all();
if (emptyDeckEntry.length === 0) { if (emptyDeckEntry.length === 0) {
await this.db.insert(Deck).values({ deckname: deck.deckname, user: user.email }); await this.db.insert(Deck).values({
deckname: deck.deckname,
user: user.email,
// 'inserted' und 'updated' werden automatisch von der DB gesetzt
});
} }
} }
} }
@ -122,6 +171,9 @@ export class DrizzleService {
}; };
} }
/**
* Methode zum Verschieben eines Bildes in ein anderes Deck
*/
async moveImage(bildid: string, targetDeckId: string, user: User) { async moveImage(bildid: string, targetDeckId: string, user: User) {
const existingImages = await this.db const existingImages = await this.db
.select() .select()
@ -135,12 +187,18 @@ export class DrizzleService {
await this.db await this.db
.update(Deck) .update(Deck)
.set({ deckname: targetDeckId }) .set({
deckname: targetDeckId,
updated: sql`CURRENT_TIMESTAMP`, // Setze 'updated' auf CURRENT_TIMESTAMP
})
.where(and(eq(Deck.bildid, bildid), eq(Deck.user, user.email))); .where(and(eq(Deck.bildid, bildid), eq(Deck.user, user.email)));
return { status: 'success', moved_entries: existingImages.length }; return { status: 'success', moved_entries: existingImages.length };
} }
/**
* Methode zum Aktualisieren einer Box
*/
async updateBox( async updateBox(
boxId: number, boxId: number,
data: { data: {
@ -157,6 +215,7 @@ export class DrizzleService {
if (typeof data.isGraduated === 'boolean') { if (typeof data.isGraduated === 'boolean') {
updateData.isGraduated = Number(data.isGraduated); updateData.isGraduated = Number(data.isGraduated);
} }
updateData.updated = sql`CURRENT_TIMESTAMP`; // Setze 'updated' auf CURRENT_TIMESTAMP
const result = await this.db const result = await this.db
.update(Deck) .update(Deck)

View File

@ -1,3 +1,4 @@
import { sql } from 'drizzle-orm';
import * as t from 'drizzle-orm/sqlite-core'; import * as t from 'drizzle-orm/sqlite-core';
import { integer, real, sqliteTable as table, text } from 'drizzle-orm/sqlite-core'; import { integer, real, sqliteTable as table, text } from 'drizzle-orm/sqlite-core';
@ -20,6 +21,8 @@ export const Deck = table(
lapses: integer('lapses'), lapses: integer('lapses'),
isGraduated: integer('isGraduated'), isGraduated: integer('isGraduated'),
user: text('user').notNull(), user: text('user').notNull(),
inserted: text().default(sql`(CURRENT_TIMESTAMP)`), // Neue Spalte
updated: text().default(sql`(CURRENT_TIMESTAMP)`), // Neue Spalte
}, },
table => { table => {
return { return {

View File

@ -0,0 +1,2 @@
ALTER TABLE `Deck` ADD `inserted` text DEFAULT (CURRENT_TIMESTAMP);--> statement-breakpoint
ALTER TABLE `Deck` ADD `updated` text DEFAULT (CURRENT_TIMESTAMP);

View File

@ -0,0 +1,164 @@
{
"version": "6",
"dialect": "sqlite",
"id": "1e5421d8-df58-434e-93c8-85dcbe6ec9ee",
"prevId": "a3cf5e86-4f1b-4cdc-9688-cf9063ba6936",
"tables": {
"Deck": {
"name": "Deck",
"columns": {
"id": {
"name": "id",
"type": "integer",
"primaryKey": true,
"notNull": true,
"autoincrement": true
},
"deckname": {
"name": "deckname",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"bildname": {
"name": "bildname",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"bildid": {
"name": "bildid",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"iconindex": {
"name": "iconindex",
"type": "integer",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"x1": {
"name": "x1",
"type": "real",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"x2": {
"name": "x2",
"type": "real",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"y1": {
"name": "y1",
"type": "real",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"y2": {
"name": "y2",
"type": "real",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"due": {
"name": "due",
"type": "integer",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"ivl": {
"name": "ivl",
"type": "real",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"factor": {
"name": "factor",
"type": "real",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"reps": {
"name": "reps",
"type": "integer",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"lapses": {
"name": "lapses",
"type": "integer",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"isGraduated": {
"name": "isGraduated",
"type": "integer",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"user": {
"name": "user",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"inserted": {
"name": "inserted",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false,
"default": "(CURRENT_TIMESTAMP)"
},
"updated": {
"name": "updated",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false,
"default": "(CURRENT_TIMESTAMP)"
}
},
"indexes": {
"email_idx": {
"name": "email_idx",
"columns": [
"id"
],
"isUnique": true
}
},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"checkConstraints": {}
}
},
"views": {},
"enums": {},
"_meta": {
"schemas": {},
"tables": {},
"columns": {}
},
"internal": {
"indexes": {}
}
}

View File

@ -15,6 +15,13 @@
"when": 1737229845910, "when": 1737229845910,
"tag": "0001_smooth_iron_lad", "tag": "0001_smooth_iron_lad",
"breakpoints": true "breakpoints": true
},
{
"idx": 2,
"version": "6",
"when": 1738103858394,
"tag": "0002_aromatic_zodiak",
"breakpoints": true
} }
] ]
} }

View File

@ -2,8 +2,10 @@ import { CommonModule } from '@angular/common';
import { Component, inject } from '@angular/core'; import { Component, inject } from '@angular/core';
import { Auth } from '@angular/fire/auth'; import { Auth } from '@angular/fire/auth';
import { GoogleAuthProvider, signInWithPopup } from 'firebase/auth'; import { GoogleAuthProvider, signInWithPopup } from 'firebase/auth';
import { PopoverComponent } from './components/popover.component';
import { DeckListComponent } from './deck-list.component'; import { DeckListComponent } from './deck-list.component';
import { ClickOutsideDirective } from './service/click-outside.directive'; import { ClickOutsideDirective } from './service/click-outside.directive';
import { PopoverService } from './services/popover.service';
@Component({ @Component({
selector: 'app-root', selector: 'app-root',
template: ` template: `
@ -31,11 +33,10 @@ import { ClickOutsideDirective } from './service/click-outside.directive';
<h1 class="text-3xl font-bold mx-auto">Vocabulary Training</h1> <h1 class="text-3xl font-bold mx-auto">Vocabulary Training</h1>
<div class="relative" appClickOutside (clickOutside)="showDropdown = false"> <div class="relative" appClickOutside (clickOutside)="showDropdown = false">
@if(photoURL){ @if(photoURL){
<img [src]="photoURL" alt="User Photo" class="w-10 h-10 rounded-full cursor-pointer" (click)="toggleDropdown()" referrerpolicy="no-referrer" crossorigin="anonymous"/> <img [src]="photoURL" alt="User Photo" class="w-10 h-10 rounded-full cursor-pointer" (click)="toggleDropdown()" referrerpolicy="no-referrer" crossorigin="anonymous" />
} @else { } @else {
<div class="image-placeholder w-10 h-10 rounded-full cursor-pointer">Image</div> <div class="image-placeholder w-10 h-10 rounded-full cursor-pointer">Image</div>
} } @if(showDropdown){
@if(showDropdown){
<div class="absolute right-0 mt-2 w-48 bg-white rounded-lg shadow-lg"> <div class="absolute right-0 mt-2 w-48 bg-white rounded-lg shadow-lg">
<button (click)="logout()" class="block w-full text-left px-4 py-2 text-gray-700 hover:bg-gray-100">Abmelden</button> <button (click)="logout()" class="block w-full text-left px-4 py-2 text-gray-700 hover:bg-gray-100">Abmelden</button>
</div> </div>
@ -44,6 +45,16 @@ import { ClickOutsideDirective } from './service/click-outside.directive';
</div> </div>
<app-deck-list></app-deck-list> <app-deck-list></app-deck-list>
</div> </div>
<app-popover
[visible]="popoverVisible"
[title]="popoverTitle"
[message]="popoverMessage"
[showInput]="popoverShowInput"
[inputValue]="popoverInputValue"
[confirmText]="popoverConfirmText"
(confirmed)="handleConfirm($event)"
(canceled)="handleCancel()"
></app-popover>
`, `,
standalone: true, standalone: true,
styles: ` styles: `
@ -80,7 +91,7 @@ img:hover {
} }
`, `,
imports: [CommonModule, DeckListComponent, ClickOutsideDirective], imports: [CommonModule, DeckListComponent, ClickOutsideDirective, PopoverComponent],
}) })
export class AppComponent { export class AppComponent {
isLoggedIn = false; isLoggedIn = false;
@ -88,14 +99,34 @@ export class AppComponent {
showDropdown = false; showDropdown = false;
photoURL: string = 'https://placehold.co/40'; photoURL: string = 'https://placehold.co/40';
// user: User | null = null; // user: User | null = null;
popoverVisible = false;
popoverTitle = '';
popoverMessage = '';
popoverShowInput = false;
popoverInputValue = '';
popoverConfirmText = 'Confirm';
private confirmCallback?: (inputValue?: string) => void;
private cancelCallback?: () => void;
constructor(public popoverService: PopoverService) {
this.popoverService.popoverState$.subscribe(options => {
this.popoverVisible = true;
this.popoverTitle = options.title;
this.popoverMessage = options.message;
this.popoverShowInput = options.showInput;
this.popoverInputValue = options.inputValue;
this.popoverConfirmText = options.confirmText;
this.confirmCallback = options.onConfirm;
this.cancelCallback = options.onCancel;
});
}
ngOnInit() { ngOnInit() {
// Überprüfen des Login-Status beim Start der Anwendung // Überprüfen des Login-Status beim Start der Anwendung
const isLoggedIn = localStorage.getItem('isLoggedIn') === 'true'; const isLoggedIn = localStorage.getItem('isLoggedIn') === 'true';
const accessToken = localStorage.getItem('accessToken'); const accessToken = localStorage.getItem('accessToken');
const refreshToken = localStorage.getItem('refreshToken'); const refreshToken = localStorage.getItem('refreshToken');
this.photoURL = localStorage.getItem('photoURL'); this.photoURL = localStorage.getItem('photoURL');
if (isLoggedIn && accessToken && refreshToken) { if (isLoggedIn && accessToken && refreshToken) {
this.isLoggedIn = true; this.isLoggedIn = true;
} }
@ -113,9 +144,9 @@ export class AppComponent {
localStorage.setItem('accessToken', await result.user.getIdToken()); localStorage.setItem('accessToken', await result.user.getIdToken());
localStorage.setItem('refreshToken', result.user.refreshToken); localStorage.setItem('refreshToken', result.user.refreshToken);
localStorage.setItem('photoURL', result.user.photoURL); localStorage.setItem('photoURL', result.user.photoURL);
this.showDropdown = false; this.showDropdown = false;
console.log('Logged in with Google', result.user); console.log('Logged in with Google', result.user);
} catch (error) { } catch (error) {
console.error('Google Login failed', error); console.error('Google Login failed', error);
@ -133,4 +164,17 @@ export class AppComponent {
toggleDropdown() { toggleDropdown() {
this.showDropdown = !this.showDropdown; this.showDropdown = !this.showDropdown;
} }
handleConfirm(inputValue?: string) {
this.popoverVisible = false;
if (this.confirmCallback) {
this.confirmCallback(inputValue);
}
}
handleCancel() {
this.popoverVisible = false;
if (this.cancelCallback) {
this.cancelCallback();
}
}
} }

View File

@ -1,6 +1,6 @@
import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http'; import { HttpClient } from '@angular/common/http';
import { map, Observable, switchMap } from 'rxjs'; import { Injectable } from '@angular/core';
import { map, Observable } from 'rxjs';
export interface Deck { export interface Deck {
name: string; name: string;
@ -10,6 +10,7 @@ export interface Deck {
export interface DeckImage { export interface DeckImage {
boxes: Box[]; boxes: Box[];
name: string; name: string;
//bildid: string;
id: string; id: string;
} }
@ -53,19 +54,21 @@ export interface OcrResult {
} }
@Injectable({ @Injectable({
providedIn: 'root' providedIn: 'root',
}) })
export class DeckService { export class DeckService {
private apiUrl = '/api/decks'; private apiUrl = '/api/decks';
constructor(private http: HttpClient) { } constructor(private http: HttpClient) {}
getDecks(): Observable<Deck[]> { getDecks(): Observable<Deck[]> {
return this.http.get<any[]>(this.apiUrl).pipe( return this.http.get<any[]>(this.apiUrl).pipe(
map(decks => decks.map(deck => ({ map(decks =>
name: deck.name, decks.map(deck => ({
images: this.groupImagesByName(deck.images) name: deck.name,
}))) images: this.groupImagesByName(deck.images),
})),
),
); );
} }
@ -77,7 +80,7 @@ export class DeckService {
imageMap[image.id] = { imageMap[image.id] = {
name: image.name, name: image.name,
id: image.id, id: image.id,
boxes: [] boxes: [],
}; };
} }
imageMap[image.id].boxes.push({ imageMap[image.id].boxes.push({
@ -91,7 +94,7 @@ export class DeckService {
factor: image.factor, factor: image.factor,
reps: image.reps, reps: image.reps,
lapses: image.lapses, lapses: image.lapses,
isGraduated: image.isGraduated ? true : false isGraduated: image.isGraduated ? true : false,
}); });
}); });
@ -129,4 +132,4 @@ export class DeckService {
updateBox(box: Box): Observable<any> { updateBox(box: Box): Observable<any> {
return this.http.put(`${this.apiUrl}/boxes/${box.id}`, box); return this.http.put(`${this.apiUrl}/boxes/${box.id}`, box);
} }
} }