logo first try, #20 cleanup, show today in blue deck-list

This commit is contained in:
Your Name 2025-01-31 19:01:10 +01:00
parent 2885ece241
commit ae9017020c
10 changed files with 151 additions and 55 deletions

View File

@ -249,4 +249,21 @@ export class DrizzleService {
return { status: 'success' };
}
/**
* Methode zum Abrufen aller eindeutigen Bild-IDs aus der Datenbank
*/
async getDistinctBildIds(user: User): Promise<string[]> {
try {
const result = await this.db.selectDistinct([Deck.bildid]).from(Deck).all();
// Extrahiere die bildid Werte aus dem Ergebnis
const usedIds = result.map((row: any) => row['0']).filter((id: string | null) => id !== null) as string[];
console.log(usedIds);
return usedIds;
} catch (error) {
this.sqlLogger.logQuery('Error fetching distinct bildids', []);
throw new HttpException(`Fehler beim Abrufen der Bild-IDs - ${error}`, HttpStatus.INTERNAL_SERVER_ERROR);
}
}
}

View File

@ -2,11 +2,12 @@
import { Body, Controller, HttpException, HttpStatus, Post, Res, UseGuards } from '@nestjs/common';
import express from 'express';
import { AuthGuard } from '../service/auth.guard';
import { DrizzleService } from './drizzle.service';
@Controller('')
@UseGuards(AuthGuard)
export class ProxyController {
constructor() {}
constructor(private readonly drizzleService: DrizzleService) {}
// --------------------
// Proxy Endpoints
// --------------------
@ -54,4 +55,50 @@ export class ProxyController {
throw new HttpException('Internal server error', HttpStatus.INTERNAL_SERVER_ERROR);
}
}
// --------------------
// Cleanup Endpoint
// --------------------
@Post('cleanup')
async cleanupEndpoint(@Body() data: { dryrun?: boolean }, @Res() res: express.Response) {
try {
const user = res.req['user']; // Benutzerinformationen aus dem Request
// 2. Nur Benutzer mit der spezifischen E-Mail dürfen fortfahren
if (user.email !== 'andreas.knuth@gmail.com') {
throw new HttpException('Zugriff verweigert.', HttpStatus.FORBIDDEN);
}
// 1. Abrufen der distinct bildid aus der Datenbank
const usedIds = await this.drizzleService.getDistinctBildIds(user);
// 3. Verarbeitung des dryrun Parameters
const dryrun = data.dryrun !== undefined ? data.dryrun : true;
if (typeof dryrun !== 'boolean') {
throw new HttpException("'dryrun' muss ein boolescher Wert sein.", HttpStatus.BAD_REQUEST);
}
// 4. Aufruf des Flask-Backend-Endpunkts
const response = await fetch('http://localhost:5000/api/cleanup', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ dryrun, usedIds }),
});
const result = await response.json();
if (!response.ok) {
throw new HttpException(result.error || 'Cleanup failed', response.status);
}
// 5. Rückgabe der Ergebnisse an den Client
return res.status(HttpStatus.OK).json(result);
} catch (error) {
if (error instanceof HttpException) {
throw error;
}
throw new HttpException('Interner Serverfehler', HttpStatus.INTERNAL_SERVER_ERROR);
}
}
}

BIN
public/favicon-16x16.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 993 B

BIN
public/favicon-32x32.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

BIN
public/favicon-48x48.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 KiB

View File

@ -11,8 +11,8 @@ import { PopoverService } from './services/popover.service';
template: `
<div *ngIf="!isLoggedIn" class="min-h-screen flex flex-col items-center justify-center bg-gradient-to-r from-blue-500 to-purple-600 text-white">
<div class="text-center">
<h1 class="text-5xl font-bold mb-4">Master Your Vocabulary</h1>
<p class="text-xl mb-8">Learn smarter, not harder. Start your journey today.</p>
<h1 class="text-5xl font-bold mb-4">Master Your Learning</h1>
<p class="text-xl mb-8">Learn smarter, not harder. Start your journey today</p>
<button (click)="loginWithGoogle()" class="bg-white text-blue-600 px-6 py-3 rounded-lg shadow-lg hover:bg-gray-100 transition duration-300 flex items-center justify-center">
<svg class="w-6 h-6 mr-2" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48">
<path
@ -30,7 +30,7 @@ import { PopoverService } from './services/popover.service';
<div *ngIf="isLoggedIn" class="container mx-auto p-4">
<div class="flex justify-center items-center mb-8">
<h1 class="text-3xl font-bold mx-auto">Vocabulary Training</h1>
<h1 class="text-3xl font-bold mx-auto">Haiky Spaced Repetition Training</h1>
<div class="relative" appClickOutside (clickOutside)="showDropdown = false">
@if(photoURL){
<img [src]="photoURL" alt="User Photo" class="w-10 h-10 rounded-full cursor-pointer" (click)="toggleDropdown()" referrerpolicy="no-referrer" crossorigin="anonymous" />

View File

@ -21,7 +21,7 @@
<span class="text-gray-600">({{ deck.images.length }} Pics)</span>
</div>
<div class="text-sm text-gray-600">
<div>Next training: {{ getNextTrainingString(deck) }}</div>
<div [ngClass]="{ 'text-blue-500 font-bold': isToday(getNextTrainingDate(deck)) }">Next training: {{ getNextTrainingString(deck) }}</div>
<div>Words to review: {{ getWordsToReview(deck) }}</div>
</div>
</div>
@ -122,41 +122,3 @@
<!-- MoveImageModalComponent -->
<app-move-image-modal *ngIf="imageToMove" [image]="imageToMove.image" [sourceDeck]="imageToMove.sourceDeck" [decks]="decks" (moveCompleted)="onImageMoved()" (closed)="imageToMove = null"> </app-move-image-modal>
</div>
<!-- Popover für die Löschbestätigung -->
<!-- <div id="deletePopover" popover="manual" class="popover">
<p>Are you sure you want to delete this deck?</p>
<div class="flex justify-end space-x-2">
<button (click)="confirmDelete()" class="bg-red-500 text-white px-4 py-2 rounded">Delete</button>
<button (click)="cancelDelete()" class="bg-gray-500 text-white px-4 py-2 rounded">Cancel</button>
</div>
</div> -->
<!-- Popover für die Eingabe des neuen Decknamens -->
<!-- <div id="renamePopover" popover="manual" class="popover">
<p>Enter the new name for the deck:</p>
<input type="text" id="newDeckNameInput" class="w-full p-2 border rounded mb-2" [value]="deckToRename?.name" />
<div class="flex justify-end space-x-2">
<button (click)="confirmRename()" class="bg-blue-500 text-white px-4 py-2 rounded">Rename</button>
<button (click)="cancelRename()" class="bg-gray-500 text-white px-4 py-2 rounded">Cancel</button>
</div>
</div> -->
<!-- Statt der alten Popover-Divs -->
<!-- <app-popover [visible]="showDeletePopover" title="Delete Deck" message="Are you sure you want to delete this deck?" confirmText="Delete" (confirmed)="confirmDelete()" (canceled)="cancelDelete()"></app-popover>
<app-popover
[visible]="showRenamePopover"
title="Rename Deck"
[showInput]="true"
[inputValue]="deckToRename?.name || ''"
confirmText="Rename"
(confirmed)="confirmRename($event)"
(canceled)="cancelRename()"
></app-popover>
<app-popover
[visible]="showDeleteImagePopover"
title="Delete Image"
message="Are you sure you want to delete this image?"
confirmText="Delete"
(confirmed)="confirmImageDelete()"
(canceled)="cancelImageDelete()"
></app-popover> -->

View File

@ -373,6 +373,9 @@ export class DeckListComponent implements OnInit {
const now = new Date();
now.setHours(0, 0, 0, 0);
const dueDates = deck.images.flatMap(image => image.boxes.map(box => (box.due ? new Date(box.due * 86400000) : null)));
if (dueDates.includes(null)) {
return now;
}
const futureDueDates = dueDates.filter(date => date && date >= now);
if (futureDueDates.length > 0) {
const nextDate = futureDueDates.reduce((a, b) => (a < b ? a : b));
@ -383,7 +386,11 @@ export class DeckListComponent implements OnInit {
getNextTrainingString(deck: Deck): string {
return this.getNextTrainingDate(deck).toLocaleDateString();
}
// In deiner Component TypeScript Datei
isToday(date: Date): boolean {
const today = new Date();
return date.getDate() === today.getDate() && date.getMonth() === today.getMonth() && date.getFullYear() === today.getFullYear();
}
// Methode zur Berechnung der Anzahl der zu bearbeitenden Wörter
getWordsToReview(deck: Deck): number {
const nextTraining = this.getNextTrainingDate(deck);

54
src/assets/logo.svg Normal file
View File

@ -0,0 +1,54 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 40 40">
<!-- Definitionen für Gradienten und Stile -->
<defs>
<!-- Hauptpfeil-Gradient - Intensivere Farben -->
<linearGradient id="arrowGradient" x1="0%" y1="0%" x2="0%" y2="100%">
<stop offset="0%" style="stop-color:#0066FF"/>
<stop offset="50%" style="stop-color:#7B00FF"/>
<stop offset="100%" style="stop-color:#00FFB3"/>
</linearGradient>
<!-- Kreisgradienten - Intensivere Farben -->
<linearGradient id="circle1Gradient" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" style="stop-color:#FF0000"/>
<stop offset="100%" style="stop-color:#FFD700"/>
</linearGradient>
<linearGradient id="circle2Gradient" x1="100%" y1="0%" x2="0%" y2="100%">
<stop offset="0%" style="stop-color:#00FFE0"/>
<stop offset="100%" style="stop-color:#1E3DEE"/>
</linearGradient>
<linearGradient id="circle3Gradient" x1="0%" y1="100%" x2="100%" y2="0%">
<stop offset="0%" style="stop-color:#FF6B00"/>
<stop offset="100%" style="stop-color:#FF0000"/>
</linearGradient>
</defs>
<!-- Hintergrund - Dunklerer Hintergrund für mehr Kontrast -->
<rect width="40" height="40" fill="#000000"/>
<!-- Äußerer Kreis - Dickere Linien -->
<circle cx="20" cy="20" r="16"
stroke="url(#circle1Gradient)"
stroke-width="2"
stroke-dasharray="4,2"
fill="none"/>
<!-- Mittlerer Kreis -->
<circle cx="20" cy="20" r="12"
stroke="url(#circle2Gradient)"
stroke-width="1.75"
stroke-dasharray="3,1.5"
fill="none"/>
<!-- Innerer Kreis -->
<circle cx="20" cy="20" r="8"
stroke="url(#circle3Gradient)"
stroke-width="1.5"
fill="none"/>
<!-- Kleinerer dynamischer Pfeil im innersten Kreis -->
<path d="M20,14 L18,26 L22,26 Z"
fill="url(#arrowGradient)"/>
</svg>

After

Width:  |  Height:  |  Size: 1.9 KiB

View File

@ -1,13 +1,22 @@
<!doctype html>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<head>
<meta charset="utf-8" />
<title>Vokabeltraining</title>
<base href="/">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="icon" type="image/x-icon" href="favicon.ico">
</head>
<body>
<base href="/" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<link rel="icon" type="image/png" sizes="16x16" href="favicon-16x16.png" />
<link rel="icon" type="image/png" sizes="32x32" href="favicon-32x32.png" />
<link rel="icon" type="image/png" sizes="48x48" href="favicon-48x48.png" />
<style>
body {
margin: 0;
height: 100vh;
background: linear-gradient(to bottom, #f8fafc, #fff7d6);
}
</style>
</head>
<body>
<app-root></app-root>
</body>
</body>
</html>