logo first try, #20 cleanup, show today in blue deck-list
This commit is contained in:
parent
2885ece241
commit
ae9017020c
|
|
@ -249,4 +249,21 @@ export class DrizzleService {
|
||||||
|
|
||||||
return { status: 'success' };
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,11 +2,12 @@
|
||||||
import { Body, Controller, HttpException, HttpStatus, Post, Res, UseGuards } from '@nestjs/common';
|
import { Body, Controller, HttpException, HttpStatus, Post, Res, UseGuards } from '@nestjs/common';
|
||||||
import express from 'express';
|
import express from 'express';
|
||||||
import { AuthGuard } from '../service/auth.guard';
|
import { AuthGuard } from '../service/auth.guard';
|
||||||
|
import { DrizzleService } from './drizzle.service';
|
||||||
|
|
||||||
@Controller('')
|
@Controller('')
|
||||||
@UseGuards(AuthGuard)
|
@UseGuards(AuthGuard)
|
||||||
export class ProxyController {
|
export class ProxyController {
|
||||||
constructor() {}
|
constructor(private readonly drizzleService: DrizzleService) {}
|
||||||
// --------------------
|
// --------------------
|
||||||
// Proxy Endpoints
|
// Proxy Endpoints
|
||||||
// --------------------
|
// --------------------
|
||||||
|
|
@ -54,4 +55,50 @@ export class ProxyController {
|
||||||
throw new HttpException('Internal server error', HttpStatus.INTERNAL_SERVER_ERROR);
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 993 B |
Binary file not shown.
|
After Width: | Height: | Size: 1.9 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 3.2 KiB |
|
|
@ -11,8 +11,8 @@ import { PopoverService } from './services/popover.service';
|
||||||
template: `
|
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 *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">
|
<div class="text-center">
|
||||||
<h1 class="text-5xl font-bold mb-4">Master Your Vocabulary</h1>
|
<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>
|
<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">
|
<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">
|
<svg class="w-6 h-6 mr-2" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48">
|
||||||
<path
|
<path
|
||||||
|
|
@ -30,7 +30,7 @@ import { PopoverService } from './services/popover.service';
|
||||||
|
|
||||||
<div *ngIf="isLoggedIn" class="container mx-auto p-4">
|
<div *ngIf="isLoggedIn" class="container mx-auto p-4">
|
||||||
<div class="flex justify-center items-center mb-8">
|
<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">
|
<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" />
|
||||||
|
|
|
||||||
|
|
@ -21,7 +21,7 @@
|
||||||
<span class="text-gray-600">({{ deck.images.length }} Pics)</span>
|
<span class="text-gray-600">({{ deck.images.length }} Pics)</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="text-sm text-gray-600">
|
<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>Words to review: {{ getWordsToReview(deck) }}</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -122,41 +122,3 @@
|
||||||
<!-- MoveImageModalComponent -->
|
<!-- MoveImageModalComponent -->
|
||||||
<app-move-image-modal *ngIf="imageToMove" [image]="imageToMove.image" [sourceDeck]="imageToMove.sourceDeck" [decks]="decks" (moveCompleted)="onImageMoved()" (closed)="imageToMove = null"> </app-move-image-modal>
|
<app-move-image-modal *ngIf="imageToMove" [image]="imageToMove.image" [sourceDeck]="imageToMove.sourceDeck" [decks]="decks" (moveCompleted)="onImageMoved()" (closed)="imageToMove = null"> </app-move-image-modal>
|
||||||
</div>
|
</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> -->
|
|
||||||
|
|
|
||||||
|
|
@ -373,6 +373,9 @@ export class DeckListComponent implements OnInit {
|
||||||
const now = new Date();
|
const now = new Date();
|
||||||
now.setHours(0, 0, 0, 0);
|
now.setHours(0, 0, 0, 0);
|
||||||
const dueDates = deck.images.flatMap(image => image.boxes.map(box => (box.due ? new Date(box.due * 86400000) : null)));
|
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);
|
const futureDueDates = dueDates.filter(date => date && date >= now);
|
||||||
if (futureDueDates.length > 0) {
|
if (futureDueDates.length > 0) {
|
||||||
const nextDate = futureDueDates.reduce((a, b) => (a < b ? a : b));
|
const nextDate = futureDueDates.reduce((a, b) => (a < b ? a : b));
|
||||||
|
|
@ -383,7 +386,11 @@ export class DeckListComponent implements OnInit {
|
||||||
getNextTrainingString(deck: Deck): string {
|
getNextTrainingString(deck: Deck): string {
|
||||||
return this.getNextTrainingDate(deck).toLocaleDateString();
|
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
|
// Methode zur Berechnung der Anzahl der zu bearbeitenden Wörter
|
||||||
getWordsToReview(deck: Deck): number {
|
getWordsToReview(deck: Deck): number {
|
||||||
const nextTraining = this.getNextTrainingDate(deck);
|
const nextTraining = this.getNextTrainingDate(deck);
|
||||||
|
|
|
||||||
|
|
@ -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 |
|
|
@ -1,11 +1,20 @@
|
||||||
<!doctype html>
|
<!DOCTYPE html>
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8">
|
<meta charset="utf-8" />
|
||||||
<title>Vokabeltraining</title>
|
<title>Vokabeltraining</title>
|
||||||
<base href="/">
|
<base href="/" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
<link rel="icon" type="image/x-icon" href="favicon.ico">
|
<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>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<app-root></app-root>
|
<app-root></app-root>
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue